Join truck_types via vehicles.truck_type_id (normalized legacy
vehicle_type only as fallback) so type renames can't unmatch detention
rules and FK-less vehicles keep billing.
The customers:* keys were seeded and present in the backoffice constants but
enforced nowhere except reset-password. Customer CRUD sat behind the coarse
edr_freight_app:admin umbrella, and every company read endpoint was unguarded.
Two routes could not be gated on the route alone, because the authority they
need depends on the request BODY, not the path:
- PATCH /companies/:id carries `status` (UpdateCompanyDto extends
PartialType(CreateCompanyDto)), so it both edits fields and blacklists.
- PATCH /company-profiles/:profileId/status is approve, reject, suspend and
blacklist on one route.
Both now take a one-of route guard and assert per-status against a shared
STATUS_PERM map: approving/reactivating needs customers:verify, suspending or
blacklisting needs customers:deactivate. PATCH /companies/:id additionally
requires customers:update when any non-status field is present, so a caller
holding only deactivate cannot rename a company. The backoffice mirrors the
same map so no button is offered that the server would reject.
GET /companies/:companyId/documents is left authenticated-only with the split
in the handler: it is dual-audience. The portal reads its own documents during
onboarding, and the contract-request detail page (gated on contracts:view)
reads the applicant's. Gating it on customers:view alone would have 403'd
customers on their own documents and blanked the contract reviewer's panel.
The two by-company customer-view reads take a one-of guard for the same reason
— otherwise a staffer granted only customers:view gets a detail page whose tabs
403 individually.
Frontend: the customers routes were sidebar-filtered but not wrapped in
RequirePermission, so direct URL navigation rendered them for anyone.
Verified: freight-api type-check clean; backoffice type-check unchanged from
HEAD (pre-existing errors only); 25 tests pass across the companies and
freight-permission suites. Not exercised against a running API.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"Copy permissions from" listed every position type in every
organization, because the dropdown read an unfiltered GET
/position-types. IAM exposes no organization-scoped route and carries no
organizationId on a position type, so the list is now narrowed
client-side to the built-in (isSystem) types plus those whose unit
belongs to the selected organization, with the type being edited
excluded.
Also in position management:
- Invalidate every position-type cache key root after a mutation. React
Query matches prefixes element by element, so ["position-type"] never
reached ["position-types-common", ...] and the department pickers kept
serving a stale list. invalidatePositionTypeQueries() covers all three
roots and is shared by the hook and the form.
- Drop getByOrganizationId and getCommonTypesByOrganizationId. Both
issued the same requests as their unit counterparts and had no callers.
- Surface errors that were being swallowed. Three mutations had empty
onError handlers, hiding IAM's 403 for built-in position types, and
CreatePositionForm's bare catch discarded the reason for every failure.
- Move organization and unit into the zod schema so they validate with
translated messages and inline errors instead of an ad-hoc toast, and
keep submit disabled through the permission-assignment call that
follows the save.
- Report the two outcomes the form used to hide: a save that succeeded
while permission assignment failed, and clearing every permission,
which assign-seconds-for-first cannot express.
- Fix the list page's loading and error states, which rendered the
"Add User" string as a spinner, ignored the unit-scoped query, and
left the export button stuck after a failed download.
- Halve PermissionSearch's requests. It fetched 50 rows, read the total
off the response and immediately refetched, and it re-filtered results
on the undebounced term, blanking the list while typing.
Remove the three record toggles. They never worked: IAM's
PositionTypeConfiguration holds only { id, organizationId,
positionTypeId, timeframe } in every published build, canAssignRecord
and canCreateBankRecord exist nowhere in the package, and the global
ValidationPipe runs with forbidNonWhitelisted, so every write was a 400.
The reads were broken too, passing a positionTypeId to a route that
filters on organizationId. A TODO records where the real flag lives:
PositionConfiguration.canReceiveRecord, keyed by positionId.
Delete ActionsColumn.tsx, which had no references.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Creating a department under a unit required picking a position type,
which is not always known at that point. Sub-department creation is
unchanged and still requires one.
PositionPayload.positionTypeId becomes optional and the field is omitted
from the request body when unset rather than sent as an empty string.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The External Truck Assignment card only exposed the Excel template from inside the Bulk Upload modal. A Download Template button now sits beside Bulk Upload in the card header, matching the container-import pattern in the booking form, so customers can grab the template without opening the modal first.
- Implement full train import journey with six container bookings filling a 54-wagon train.
- Create tests for split offer and rebooking scenarios, handling payment expiry and waiting list promotions.
- Add tests for handling waiting bookings expiration when the train is full.
- Implement tests for reopening booking windows after expired reservations.
- Seed database with necessary corridor data for import flows, including yards, container types, locomotives, and rates.
Added assertCapacity() checks before saving (matches single receive)
Added applyCapacityDelta() after save to increment counters
Now validates warehouse → yard → zone capacity hierarchy
Single receive already had both checks; bulk receive was gap.
Reject lease start/end and monthly payment on PURCHASE acquisitions (create and
update, validated against the resulting record). Add asset_acquisitions.item_name
column + migration. Enforce warehouse/yard/zone capacity on bulk receive and apply
capacity-counter deltas on save. Adds acquisition-guard spec.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes
Download Excel template with instructions
Parse uploaded file, preview trucks
Validate container assignments (1x40ft OR 2x20ft per truck)
Commit bulk upload in one call