Steps are now company -> owner -> representation -> contact -> documents;
PersonnelStep and PoaStep are gone.
- OwnerStep shows whoever the eTrade licence names, read-only with a
provenance badge (SourcedField), and renders an input for every gap eTrade
and Fayda left. It warns when the stored owner does not match eTrade.
- RepresentationStep asks outright whether anyone holds power of attorney,
then branches: "no" verifies the owner, "yes" collects the representative
and the DARS delegation letter. Freight forwarders cannot answer "no".
- Passport input appears only for a foreign company whose subject has not
verified with Fayda.
- Drop every same-as-owner copy-across and the auth-user fallbacks for the
owner's email and phone. Re-picking a business licence now clears the
eTrade owner prefill, since licences under one TIN can name different
managers.
- Settings: TabGeneralManager replaced by TabOwner; TabPowerOfAttorney
reworked around poaDeclared.
fayda-identity covers the two subjects, the passport alternative and the
eTrade owner comparison in place of ~40 general-manager assertions.
poa-delegation drops the poaSameAsOwner waiver case and asserts the opposite:
a forwarder is refused without a delegation letter however it represents
itself.
Data rescue, not just schema tidying — the order matters:
1. Backfill companies.email from generalManagerEmail before the key is
stripped, or companies whose only address lived there stop receiving mail.
2. Backfill ownerName/Email/Phone (now required) from owner, GM, contact
person, then the columns; seed etradeManagerName so existing rows read as
matching rather than as a false mismatch.
3. Convert poaSameAsOwner: non-forwarders become poaDeclared='no' with their
poa* details cleared; forwarders become 'yes' and keep them, so they now
owe a DARS letter they were previously waived.
4. Derive poaDeclared for everyone else.
5. Move drafts off the deleted 'personnel' wizard step.
6. Drop the general_manager_* columns and strip gm*/poaSameAsOwner.
down() restores the column shape only; the stripped values are gone by design.
Replaces the owner/general-manager/PoA trio with a single identity whose
subject is the PoA when the company declares one and the owner otherwise.
- Drop the general manager everywhere: entity columns, DTOs, required-field
list, self-service attributes, gm* identity handling.
- New explicit poaDeclared answer ("yes"/"no") replaces poaSameAsOwner. A
DARS delegation letter is required iff it is "yes"; a freight forwarder is
forced to "yes" server-side and gets no waiver.
- Owner name/email/phone become typeable and required, prefilled from the
eTrade lookup, never falling back to the authenticated account.
- Store eTrade's manager separately (etradeManagerName/Phone) and expose
ownerMatchesEtrade so backoffice compares the asserted owner against the
licence instead of against itself.
- Foreign companies satisfy the identity with Fayda or a passport number, on
whichever subject is verifying (poaPassportNumber added).
- Write companies.email/phone from the owner unconditionally, so a company
without a Fayda-verified owner still has a notification address.
The director position held no warehouse permissions at all. It now
carries the same warehouse set as the chief tier: dashboard, warehouse
/yard/zone CRUD, allocation and fee rule CRUD, the inventory operation
set, inspection reports, interchange documents and fee invoices.
Unlike the dispatcher, the director owns the allocation and fee rules
themselves. The positions seeder backfills existing environments on
boot.
feat(train-builder): update permissions checks for train actions in UI
feat(contracts): enhance contract view and shipment request pages with new features
test(shipment-preview): add tests for customs clearing logic in booking preview
style(contract-sign-bar): create CSS for fixed sign bar layout
LAST_MILE_REQUESTS URL constants were missing the /api prefix every other
endpoint in URLS.ts carries — all five calls (get, submit, contract
view/document/sign) 404'd against the deployed API, so the departure
notification's confirm link never loaded for the customer.
Also widen GET /last-mile-requests/:id from @BookingStaff to @MixedAudience
with the same ownership check submit()/sign() already use — the confirm
page calls this as its first request, before the customer has done
anything else, so it can't be staff-only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
End-to-end testing of the direct truck-to-train flow surfaced two defects
in the carriage acceptance sheet's no-wagon fallback. Container numbers
were read from freight.containers, which only gains rows at allocation,
so a direct booking's declared containers never appeared; they are read
from booking_container_units now, seal numbers included. The weight used
bulkTotalWeightTons alone, which is null for CONTAINER and PER_TON bulk
bookings, printing 0.00 — cargoTotalWeightVgm is the real weight there
and only holds an item count for PER_ITEM break-bulk.
Separately, warehouse cargo can be loaded from the Load-to-Train queue or
from the schedule, but loading from the schedule never advanced
warehouse_inventory. The booking went IN_TRANSIT while its cargo still
read as sitting in the warehouse — the same inconsistency that produced
the spurious dispatch block. loadBooking now moves the inventory to
LOADED in the same transaction, and no-ops for direct bookings.