- Added ShippingLineBookingCompletionController and associated service to handle the completion of shipping line bookings.
- Introduced a new module for booking completion to maintain module separation and avoid cyclic dependencies.
- Updated the train scheduling global rules to set default desk hours to 24 hours.
- Modified existing services and entities to accommodate the new booking completion logic.
- Enhanced the front-end components to support the new booking completion flow, including updates to the booking detail and bookings pages.
- Implemented validation and error handling for booking completion, ensuring that only approved bookings can be completed.
- Added migration to set default desk hours in the database.
New logo-settings module (mirrors stamp-settings): single uploaded logo,
stored via FilesService/MinIO, injected as a data URL into invoice/receipt,
contract, warehouse, train-scheduling, and payment-receipt PDFs. Adds a
matching backoffice settings page and settings:logo:view/manage permissions.
>
- Implement ShippingLineCompaniesService for registering and managing shipping line companies.
- Create ResendActivationAction component for resending activation links to shipping lines.
- Develop ShippingLineCompaniesPage for listing and registering shipping lines with validation.
- Introduce shippingLineCompanies.service for API interactions related to shipping lines.
- Define types for shipping line companies, including registration and pagination.
- Add placeholder pages for shipping line portal, including home, bookings, help, invoices, and settings.
They hold a TIN but no business licence, so there is no eTrade record to look
their registration up in. A checkbox on the first wizard step marks them, and
everything that assumed a trade licence bends around it:
- The company step replaces the eTrade lookup with typed registration details
— name, region, zone, woreda, kebele, house number — required exactly because
they are now on screen. applyEtradeSourcedFields skips the lookup rather than
failing it, so what the customer sends is what is stored.
- No freight-forwarder role. Forwarding is licensed work, so the option is not
offered, and the API refuses it at start-onboarding and at every later
role-add rather than letting approval fail on a document they cannot produce.
- No per-role business-licence upload, client-side or in the completion gate.
- Their own document set (company_onboarding_documents_cooperative) merges on
top of the nationality one, admin-managed like every other set. Nationality
wins a fileKey collision so no slot renders twice, and the DARS paper is not
injected into it — the set it merges onto already carries one.
- The owner is typed in full; with no eTrade manager on file the licence
comparison reports "nothing to compare against", which backoffice now
explains rather than leaving as a bare dash.
Stored as an attributes flag, not a column: everything it changes is
behavioural, and nothing queries or joins on it.
Map EDR invoices onto the MoR EIMS /v1/register document and add the
cryptographic transport needed to talk to core.mor.gov.et.
Mapper: DTOs mirror the supplied Postman collection section by section.
Tax is resolved per line via a caller-supplied resolver and throws when
unresolved -- the app models no tax at all (invoice.taxAmount is always 0,
invoice_lines and the rate catalogue carry no fiscal columns), so a
zero-rated default would assert a tax position the codebase cannot support.
Seller identity, document number, counters and previous IRN are passed in
explicitly; the mapper stays pure.
Transport: config, credential loading, RSA-SHA512 signing and /auth/login
with an in-memory token cache. Signing reproduces the process that produced
a working live token -- compact JSON of the inner request only, exact UTF-8
bytes, base64 signature, and base64 of the certificate file's exact bytes
with no parsing or re-encoding. Concurrent callers share one login via an
in-flight promise. Refresh is deliberately unimplemented: the collection
shows an unsigned refresh body but also ships unsigned examples of calls
that do require signing, so an expired token re-logs in instead.
Errors normalise to EimsApiException carrying only the gateway's own error
fields; secrets, signature, certificate and tokens never reach logs.
Key and certificate file patterns are gitignored.
Nothing calls EIMS automatically and no invoice entity, migration or UI is
touched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neither was wired into the app, so every /facilities route and
POST /bookings/:id/allocate-containers returned 404 and their permission
keys granted nothing.
Gates the previously open support-agent, procurement, compliance,
facilities, list-users and trade-access controllers, separates customer
from staff routes across bookings, contracts, companies, billing,
warehouses, files and train scheduling, and moves billing, overview,
reports and the settings controllers onto their own keys instead of the
blanket admin key. Drops the demo-permissions module and the untested
notification test route.
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.
Intercity cargo is loaded at its origin yard and unloaded at its destination, but
only some yards have the equipment. EDR's facilities are Indode, Sebeta, Modjo,
Adama and Dire Dawa — and the set grows, so it has to be data.
- yards.has_facility marks a yard as a load/unload point; the new yard_facilities
record says what it can do. Only Indode stores cargo (has_warehouse), so only it
accrues storage/demurrage — the rest just move cargo on and off the train.
- facility_handling_events records each load/unload and carries its GRN.
warehouse_inventory cannot: its warehouse/yard/zone are NOT NULL, so a facility
without a warehouse could never have a row. inventory_id links to the storage
record when there is one.
- YardFacilitiesService.facilityForYard is the single resolver the handling flows
share, so they cannot drift on what a facility is.
- The seeder flags EXISTING yards and creates none. The codes are historical and
do not read like the facility names — Indode is KALITY ("Gelan Multi Purpose
Port (Indode)") and Sebeta is LEGACY_DEST — so it maps by code. Creating fresh
INDODE/SEBETA yards would have split data that routes and bookings already
reference.
Negad is deliberately absent: NAGAD ("DCT/SGDT") is in Djibouti while
NEGAD_FY_BCC is in Ethiopia and inactive, and which one is the intercity facility
is unsettled.
No behaviour change yet — nothing reads has_facility until the gate lands.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>