mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
fix: update payment currency handling to require explicit selection by customer
This commit is contained in:
@@ -265,8 +265,10 @@ function NewShipmentBookingForm({
|
||||
// still flip it per shipment.
|
||||
withReturn: contract.equipmentReturn === "WITH_RETURN",
|
||||
// The contract quotes USD; the customer bills this shipment in the
|
||||
// currency they pick here. Intercity is always ETB.
|
||||
paymentCurrency: contract.tradeDirection === "DOMESTIC" ? "ETB" : "USD",
|
||||
// currency they pick here. Intercity is always ETB, so it is preset;
|
||||
// everything else starts empty so the customer picks deliberately
|
||||
// instead of silently inheriting USD.
|
||||
paymentCurrency: contract.tradeDirection === "DOMESTIC" ? "ETB" : "",
|
||||
},
|
||||
resolver: zodResolver(
|
||||
createShipmentFormSchema({
|
||||
@@ -340,7 +342,11 @@ function NewShipmentBookingForm({
|
||||
...(values.contractRouteId
|
||||
? { contractRouteId: values.contractRouteId }
|
||||
: {}),
|
||||
paymentCurrency: values.paymentCurrency,
|
||||
// Validation guarantees a currency by here; the guard keeps an empty
|
||||
// value out of the payload rather than tripping the API's @IsIn check.
|
||||
...(values.paymentCurrency
|
||||
? { paymentCurrency: values.paymentCurrency }
|
||||
: {}),
|
||||
// Intercity bookings carry no date — staff assign a passing train later.
|
||||
...(values.scheduledDate
|
||||
? { scheduledDate: new Date(values.scheduledDate).toISOString() }
|
||||
@@ -1131,23 +1137,32 @@ function ScheduleStep({
|
||||
<Controller
|
||||
name="paymentCurrency"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
render={({ field, fieldState }) => (
|
||||
<Box mb="lg">
|
||||
<StepLabel>Billing currency *</StepLabel>
|
||||
<Text fz={12.5} c="dimmed" mt={4} mb={10}>
|
||||
Your contract is quoted in USD. Pick the currency this shipment is
|
||||
invoiced in — the total is converted for you.
|
||||
</Text>
|
||||
{/* Rendered unselected until the customer chooses: SegmentedControl
|
||||
highlights whatever value it is given, so passing a fallback
|
||||
here would look like a made choice. */}
|
||||
<SegmentedControl
|
||||
value={field.value ?? "USD"}
|
||||
value={field.value || ""}
|
||||
onChange={(v) => field.onChange(v)}
|
||||
data={[
|
||||
{ label: "Select…", value: "", disabled: true },
|
||||
{ label: "USD", value: "USD" },
|
||||
{ label: "ETB", value: "ETB" },
|
||||
]}
|
||||
color="edr-green"
|
||||
radius={10}
|
||||
/>
|
||||
{fieldState.error && (
|
||||
<Text fz={12.5} c="red.6" mt={6}>
|
||||
{fieldState.error.message}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createShipmentFormSchema, initialShipmentFormValues } from "./schema";
|
||||
|
||||
const schema = createShipmentFormSchema({
|
||||
isContainer: false,
|
||||
isHazardous: false,
|
||||
isReefer: false,
|
||||
requiresDate: false,
|
||||
});
|
||||
|
||||
const values = (over: Record<string, unknown> = {}) => ({
|
||||
...initialShipmentFormValues,
|
||||
cargoWeightTons: "10",
|
||||
...over,
|
||||
});
|
||||
|
||||
const currencyIssues = (input: Record<string, unknown>) => {
|
||||
const result = schema.safeParse(input);
|
||||
return result.success
|
||||
? []
|
||||
: result.error.issues.filter((i) => i.path[0] === "paymentCurrency");
|
||||
};
|
||||
|
||||
describe("paymentCurrency validation", () => {
|
||||
it("defaults to empty rather than silently picking USD", () => {
|
||||
expect(initialShipmentFormValues.paymentCurrency ?? "").toBe("");
|
||||
});
|
||||
|
||||
it("rejects a submit with no currency chosen", () => {
|
||||
const issues = currencyIssues(values({ paymentCurrency: "" }));
|
||||
expect(issues).toHaveLength(1);
|
||||
expect(issues[0].message).toBe("Select the billing currency for this shipment.");
|
||||
});
|
||||
|
||||
it("accepts either currency once chosen", () => {
|
||||
expect(currencyIssues(values({ paymentCurrency: "USD" }))).toHaveLength(0);
|
||||
expect(currencyIssues(values({ paymentCurrency: "ETB" }))).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -76,8 +76,9 @@ const shipmentFormBase = z.object({
|
||||
// EXPORT rail: the specific train picked for the shipment day (schedule id).
|
||||
trainScheduleId: z.string().default(""),
|
||||
// The contract quotes in USD; the customer picks the billing currency for
|
||||
// THIS shipment. Intercity is forced to ETB (server-enforced too).
|
||||
paymentCurrency: z.enum(["USD", "ETB"]).default("USD"),
|
||||
// THIS shipment. Starts empty so the choice is deliberate — validated as
|
||||
// required below. Intercity is forced to ETB (server-enforced too).
|
||||
paymentCurrency: z.enum(["USD", "ETB", ""]).default(""),
|
||||
// Container contracts only: return the empty container(s) to EDR after
|
||||
// unloading. Seeded from the contract's equipment return; bulk ignores it.
|
||||
withReturn: z.boolean().default(false),
|
||||
@@ -101,6 +102,15 @@ export function createShipmentFormSchema(ctx: ShipmentValidationContext) {
|
||||
});
|
||||
}
|
||||
|
||||
// No default currency — the customer must pick one before submitting.
|
||||
if (!data.paymentCurrency) {
|
||||
refineCtx.addIssue({
|
||||
code: "custom",
|
||||
path: ["paymentCurrency"],
|
||||
message: "Select the billing currency for this shipment.",
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.isContainer) {
|
||||
// Containerized cargo must say WHAT is inside — required per booking.
|
||||
if (!data.cargoDescription.trim()) {
|
||||
@@ -311,6 +321,6 @@ export const shipmentStepFields: Record<
|
||||
"bulkReeferQuantity",
|
||||
"withReturn",
|
||||
],
|
||||
2: ["scheduledDate"],
|
||||
2: ["paymentCurrency", "scheduledDate"],
|
||||
3: ["notes"],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user