Merge pull request #1472 from Tria-plc/freight_feature/usermanagement

feat: Implement handling for partially loaded bookings in train sched…
This commit is contained in:
marshal
2026-09-01 23:56:07 +03:00
committed by GitHub
12 changed files with 1247 additions and 43 deletions

View File

@@ -139,8 +139,17 @@ export function WagonCancellationCard({
// rows this booking opened itself can be paid/withdrawn/rebooked from here.
const rows = data?.items ?? [];
const ownRows = rows.filter((r) => r.bookingId === booking.id);
const openRow = ownRows.find((r) => r.status === "FEE_PENDING");
const creditRow = ownRows.find((r) => r.status === "CREDIT_AVAILABLE");
// A cancellation owes its fee whenever a customer-fault fee is still
// unsettled. FEE_PENDING is the customer-requested flow (cut applies at
// payment); an AT-LOADING cut applies immediately and jumps straight to
// CREDIT_AVAILABLE with its invoice left open — so status alone would hide
// the fee and offer a "no further payment needed" rebook on money still owed.
const owesFee = (r: (typeof ownRows)[number]) =>
r.fault === "CUSTOMER" && Number(r.feeAmount ?? 0) > 0 && !r.feePaidAt;
const openRow = ownRows.find((r) => r.status === "FEE_PENDING" || owesFee(r));
const creditRow = ownRows.find(
(r) => r.status === "CREDIT_AVAILABLE" && !owesFee(r),
);
const feePay = useFeeInvoicePayment(booking.id);
@@ -237,7 +246,14 @@ export function WagonCancellationCard({
toast.error(apiErrorMessage(e, "Could not rebook the wagons. Please try again.")),
});
if (!eligible) return null;
// Showing the card is NOT the same as allowing a new cut. A partial cancel at
// loading leaves the kept wagons to ride, so the booking moves on to
// IN_TRANSIT/ARRIVED/COMPLETED while its cancellation still owes a fee and
// holds a rebookable credit. Gating on PAID/CANCELLED hid exactly that case —
// the customer saw neither the cancelled wagons nor the fee they owe. Any
// booking that HAS cancellation rows keeps the card, whatever its status;
// `canRequest` still limits NEW cuts to a live PAID booking.
if (!eligible && !ownRows.length) return null;
if (!canRequest && !ownRows.length) return null;
return (
@@ -265,8 +281,10 @@ export function WagonCancellationCard({
{fmtMoney(openRow.feeAmount, openRow.feeCurrency)}
</Text>
. The cancelled wagons have left the train. Pay the fee to unlock
the rebooking credit. The request cannot be withdrawn from here
if it was a mistake, contact EDR staff.
the rebooking credit the {Number(openRow.wagonsCancelled)} cancelled
wagon(s) can then be rebooked on a coming train day. The request
cannot be withdrawn from here if it was a mistake, contact EDR
staff.
</Alert>
<Group gap={8}>
<Button

View File

@@ -459,7 +459,15 @@ export default function BookingsListPage() {
const creditByBooking = useMemo(() => {
const m = new Map<string, WagonCancellation>();
for (const r of myCancellations?.items ?? []) {
if (r.status === "CREDIT_AVAILABLE" && !m.has(r.bookingId)) m.set(r.bookingId, r);
// A customer-fault cut invoices a fee. An at-loading cut applies at once
// and opens the credit with that invoice still OPEN, so CREDIT_AVAILABLE
// alone never means the fee was settled — offering "Rebook" here would
// let the customer redeem the wagons without ever paying. Those rows fall
// through to the row's Pay button instead (the fee is on my-payables).
const owesFee =
r.fault === "CUSTOMER" && Number(r.feeAmount ?? 0) > 0 && !r.feePaidAt;
if (r.status === "CREDIT_AVAILABLE" && !owesFee && !m.has(r.bookingId))
m.set(r.bookingId, r);
}
return m;
}, [myCancellations]);

View File

@@ -282,6 +282,8 @@ export interface WagonCancellation {
feeCurrency: string;
feeInvoiceId?: string | null;
feePaidAt?: string | null;
/** Who caused the cut: CUSTOMER pays a fee, EDR never does. */
fault?: "CUSTOMER" | "EDR" | null;
status: WagonCancellationStatus;
reason?: string | null;
rebookedAt?: string | null;