- Added pricing service integration to ContractsService for pre-persistence contract pricing.
- Updated LegCapacityPanel to display cargo weight instead of gross weight for better clarity on booked cargo.
- Enhanced train scheduling service to include cargo weight in booking details.
- Modified ClearanceDocumentsPage to include FULLY_EXECUTED status in booking status options.
- Refactored FileUploadSettingsPage to categorize file upload settings into tabs for better organization.
- Removed legacy onboarding fields and settings from file upload settings seeder.
- Improved type definitions for train scheduling to include cargo weight without wagon tare.
Backoffice can now "Request changes" on a pending settings change
request without rejecting it outright: a new ChangesRequested status
keeps the row open so the customer's next edit appends into the same
request instead of starting a fresh cycle, and the reviewer's note
persists across that round instead of being cleared on resubmit.
Version History and Review History (previously two separate,
differently-shaped lists) are merged into one chronological timeline
under a new History tab, including document changes shown as a real
previous-vs-current diff (both files openable).
Bug fixes surfaced while wiring this up:
- Replacing a single-file document slot left the old file live
alongside the new one instead of retiring it (customer settings +
onboarding uploads).
- The "previous" file in a document diff 404'd once superseded —
the preview route now also matches soft-deleted records.
- A document replace was recorded twice in the timeline (once at
upload, once again at change-request approval).
autoArriveAtFinalYard() bulk-updated booking status to ARRIVED/COMPLETED
via raw SQL but never emitted booking.unloadedAtYard, so
WarehouseInventoryService's listener never created the warehouse_inventory
row — bookings caught by this fallback (e.g. KALITY import arrivals)
stayed on awaiting unload indefinitely. Emit the event per row, covering
both intercity (DOMESTIC) and IMPORT as the listener already expects.
Ticket #181 — eTrade auto-fetch already existed; adds a client-side
download of the fetched record (eTrade returns data, not a document)
on the portal onboarding step and the backoffice customer detail page.
Ticket #238 — pre-approval edits and document uploads write straight
to the live company row with no approval gate and, until now, no
trace. Adds an append-only company_revisions log (diffed field
changes, document uploads) recorded from updateProfile and
uploadCompanyDocuments, exposed via GET /companies/:id/revisions and
shown as "Version history" on the backoffice customer detail page.
Ticket #420 — add approved_at to companies (migration), stamped at
both promotion sites (first-profile auto-approve and manual staff
status change). Surfaced as Submitted on/Approved on in the backoffice
customer list and detail views.
Ticket #422 — surface Pending/Suspended/Approved company status as a
banner, branching AccountReviewBanner on companyStatus instead of only
per-profile review state.
Both callers already hand the provider a major-currency amount before this
call — freight sends invoice.balanceAmount (e.g. 700 for $700), passenger
sends CurrencyService's converted charge amount — with the target currency's
own decimal precision already applied (0dp DJF, 2dp USD/ETB). CAC's
toMajorAmount then divided anything that wasn't DJF by 100 on top of that,
so a USD 700 charge reached the bank as 7.00. This never surfaced because
CAC has only ever been wired for DJF (passenger) until now.
Also scoped the bank's documented 10-100,000 bound to DJF — it's the only
currency the spec states bounds for, so a USD amount outside that DJF-shaped
range is no longer rejected locally; an amount genuinely out of range still
comes back as a bank rejection.
Passenger is unaffected: its CAC currency has always been DJF, and the DJF
branch's behavior (amount unchanged, bounds enforced) is untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CAC Bank's initiate SMSes an OTP before it answers and routinely takes more
than the 10s this client allowed, so every CAC charge came back as a 502
while the bank was still working — and if the bank had answered, the payer
would have been texted a code for an intent the freight side never recorded.
Matches the passenger API, which already budgets 60s and reads the same
PAYMENT_API_HTTP_TIMEOUT_MS override.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The confirm flow settles money on a provider with no webhook, so the two
places it can go wrong are worth pinning: that the OTP is forwarded against
the GATEWAY intent id (not the local projection id), and that a rejected
code leaves the intent open instead of failing the payment.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CAC Bank is an OTP debit with no redirect and no webhook: initiate SMSes a
code to the payer's mobile, and the charge only settles when that code is
confirmed. The payment service already spoke it (passenger uses it); the
freight side had the enum values but none of the flow.
API:
- PaymentClientService.confirmOtp forwards the code to
POST /payments/intents/:id/confirm, mapping 400/404 to BadRequest so a
mistyped code stays retryable instead of surfacing as a gateway failure.
- PaymentService.confirmOtp is keyed by the LOCAL intent id (the invoice's
paymentId) rather than the domain reference, so the right invoice settles
when several share a booking. On success billing settles the invoice.
- payInvoice rejects CAC_BANK without payerAccount before calling the
gateway, and no longer runs the demo auto-settle for a COLLECT_OTP intent
(it is not paid until the payer confirms).
- POST /billing/my-invoices/:id/confirm — ownership-checked, and since
warehouse fee invoices are central invoices it covers those too.
Portal:
- useInvoicePayment owns the whole flow (initiate, redirect-or-OTP, confirm)
and replaces the five near-identical pay mutations at the call sites.
- PaymentMethodModal gains the CAC Bank option, the payer mobile field, and
the OTP step. Click-outside is disabled there so a stray click cannot drop
the payer out of a live OTP window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>