Commit Graph

1440 Commits

Author SHA1 Message Date
Nathnael
d4767e0d06 fix: an issue 2026-08-12 07:12:04 +00:00
ghost2023
a603807e8e feat(freight-api): seal EDR side of contracts with the one global stamp 2026-08-12 06:26:46 +03:00
Nathnael
d6e349f329 feat(companies): onboard co-operative unions and farms
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.
2026-08-11 12:54:51 +00:00
Nathnael
72164b0b8e fix(freight-portal): stop owner details vanishing between wizard steps
SourcedField rendered read-only whenever a value existed, so the input a
customer had just typed into turned into a badge as soon as the step saved and
they navigated back — and dropped out of requiredKeys at the same time. It now
locks on ownership instead: a Fayda verification owns what its claims filled,
everything else stays an editable, prefilled input.

The representation step follows the same rule and asks in the right order:

- The power-of-attorney question collapses to its answer once given, with a
  button back to it (none for a freight forwarder, whose answer is forced).
- A foreign company picks how to prove the person outright — Fayda or a
  passport — rather than being shown both at once.
- The representative's own fields appear only once the person is established,
  and only for what the verification did not supply; what it did supply is
  already on the panel above and is no longer repeated beneath it.
- Dropped the freight-forwarder lecture and the DARS blurb; the badge and the
  upload field's own help text already say both.

VAT numbers accept any non-blank value. A foreign tax authority's carries
letters and dashes and a co-operative's follows neither pattern, so the 10-11
digit rule only ever rejected numbers we had no business judging.
2026-08-11 12:54:38 +00:00
Nathnael
8cf041f494 test(companies): rewrite identity specs around the single subject
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.
2026-08-11 11:53:42 +00:00
Nathnael
f611c33722 feat(companies): migrate existing companies off the general manager
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.
2026-08-11 11:53:36 +00:00
Nathnael
a1bcdfb692 refactor(companies): one verified identity per company, no general manager
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.
2026-08-11 11:53:26 +00:00
marshalyordanos
ea6eccbf09 feat(upload): increase document upload size limits to 50MB across the application 2026-08-11 09:26:23 +03:00
marshalyordanos
997998dcb2 feat(wagons): enforce wagon availability limits in transfer requests 2026-08-10 23:30:18 +03:00
marshalyordanos
40672e9c41 feat(freight): surface scheduling clock and gate pay during drain 2026-08-10 21:47:28 +03:00
marshal
d3096939e7 Merge pull request #1223 from Tria-plc/freight_feature/usermanagement
fix(freight-backoffice): render wagon transfer reason as sanitized HT…
2026-08-10 17:54:28 +03:00
Nathnael
615f7e6798 feat: add choice to etrade fetcher 2026-08-10 14:49:17 +00:00
Marshal
e29e3ff837 fix(freight-backoffice): render wagon transfer reason as sanitized HTML in a modal 2026-08-10 14:02:13 +00:00
Nathnael
c24c93bcf3 feat: add 11 digit vat 2026-08-10 13:56:25 +00:00
Hagernesh
b701ae3f47 Revert "docs: trim eims.types.ts file header comment"
This reverts commit f4b8dab58d921791b4b3f69e3c9952b6802835cc.
2026-08-10 12:14:18 +00:00
Hagernesh
1b4a317d92 docs: trim eims.types.ts file header comment
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 12:14:18 +00:00
Hagernesh
bf8eadbc85 fix: portal cannot load last-mile confirmation request
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>
2026-08-10 12:14:18 +00:00
Nathnael Wondisha
29d472351f Merge pull request #1213 from Tria-plc/freight/nati-2
Freight/nati 2
2026-08-10 12:33:07 +03:00
ghost2023
d07d35e438 feat: allow owner to represent company as power of attorney 2026-08-10 12:31:13 +03:00
Nathnael Wondisha
826a313c9d Merge pull request #1207 from Tria-plc/freight/nati-2
Freight/nati 2
2026-08-10 09:56:59 +03:00
Nathnael
124c51071b fix: log email and sms 2026-08-10 06:56:07 +00:00
Nathnael
3d53f67156 fix: gm and poa email requirement 2026-08-10 06:46:25 +00:00
marshal
270ca2a141 Merge pull request #1205 from Tria-plc/freight_feature/usermanagement
Freight feature/usermanagement
2026-08-10 00:17:56 +03:00
Marshal
573e777e8f changes 2026-08-09 21:16:52 +00:00
Hagernesh
cde67e4c6a fix(freight): correct direct CAS lines and sync inventory on schedule load
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.
2026-08-09 16:05:59 +00:00
Nathnael
a47c7f6b7a Merge branch 'dev' into freight/nati-2 2026-08-09 13:44:18 +00:00
Nathnael
ff9bb4954a fix: the content document 2026-08-09 13:27:05 +00:00
Nathnael
182787e143 fix: gm email 2026-08-09 07:57:45 +00:00
Marshal
aedab1b436 keep cents in CBE bills and prices 2026-08-08 21:09:20 +00:00
Marshal
80a14c176b keep cents in CBE bills and prices 2026-08-08 20:45:05 +00:00
Nathnael Wondisha
12eb07752e Merge pull request #1192 from Tria-plc/freight/nati-2
Freight/nati 2
2026-08-08 20:38:37 +03:00
Nathnael
352c873961 fix: add user email fall back 2026-08-08 15:38:00 +00:00
Hagernesh
337ff6cfe3 feat(freight): support direct truck-to-train export handover
Export cargo reaches a train two ways, but the platform only modelled
one. Direct truck-to-train cargo loads straight onto the wagon, never
enters a warehouse and so never has a GRN — yet assertExportReceivedWithGrn
required one before the carriage acceptance sheet could be issued or the
booking loaded from inside its schedule.

Adds export_handover_mode to freight.bookings (null = WAREHOUSE, so
existing bookings are unaffected) and teaches the shared gate to skip
DIRECT_TO_TRAIN. Both call sites are fixed by that single early return.

For direct bookings the carriage acceptance sheet builds its lines from
the booking's own containers, falling back to the declared bulk tonnage,
and is issuable as soon as the mode is chosen. Direct bookings are also
removed from the warehouse receive queue, since that cargo is never
coming to the shed.

Staff choose the mode from the booking detail page via a new endpoint
reusing bookings:operations. Switching to direct is refused once
warehouse inventory exists, so the two flows cannot cross.

Warehouse-then-train keeps every gate it had.
2026-08-08 15:16:31 +00:00
Nathnael
7a32bb95ff refactor: rm company profile 2026-08-08 15:06:46 +00:00
Marshal
88b995d3d7 fix issue 2026-08-08 14:22:38 +00:00
Marshal
4b8119fd3c chnages 2026-08-08 14:02:58 +00:00
Marshal
a42d32c27c feat(billing): USD offline bank-transfer payments 2026-08-08 13:37:05 +00:00
Hagernesh
80e1648672 fix 2026-08-08 13:17:43 +00:00
Hagernesh
204bfd2421 fix: let trains dispatch with cargo still in the warehouse
The export-only assertAllocatedCargoLoaded guard threw a
BadRequestException whenever a booking on the schedule had warehouse
inventory in RECEIVED, STORED or READY_FOR_LOADING, making it
impossible to dispatch a train whose cargo had not been inspected and
loaded onto a wagon.

Leaving cargo behind is an operational decision, not an error state.
The dispatch confirm dialog already lists unassigned and unloaded
bookings and offers Dispatch anyway, so the readiness signal is
preserved — only the hard block is gone.

Wagon/locomotive conflicts and the Djibouti gatepass check still block
dispatch: those are physical and legal conflicts, not cargo readiness.
2026-08-08 13:17:43 +00:00
Marshal
83b9e32670 usd remove from the portal 2026-08-08 12:40:07 +00:00
Nathnael Wondisha
a140add64e Merge pull request #1181 from Tria-plc/freight/nati-2
Freight/nati 2
2026-08-08 12:38:32 +03:00
Nathnael
41e8c08ba3 feat: WIP Contnet managemtn 2026-08-08 09:37:28 +00:00
Hagernesh Tadesse
268737cc4f Merge pull request #1180 from Tria-plc/lastmilerequest
Lastmilerequest
2026-08-08 12:30:43 +03:00
Hagernesh
c20f395f5f fix: stamp export self-haul truck arrival on receive to warehouse
Import's customer_truck_assignments.arrived_at is set by a separate later
gate action (release()'s arrival branch — the truck returning to collect
already-warehoused goods). Export has no equivalent second step: the truck
delivering cargo to the warehouse arrives and is received in the same act,
so its arrival was never recorded anywhere.

Add markCustomerTruckArrived, mirroring release()'s existing self-haul
departure UPDATE (plate-matched, COALESCE(arrived_at, NOW())), and call it
from receive()/bulkReceive() for EXPORT bookings
2026-08-08 09:28:14 +00:00
Stephanos A.
3a78605893 Merge pull request #1177 from Tria-plc/alpha
Alpha
2026-08-08 11:18:49 +03:00
Hagernesh
8d3dfa4113 fix(eims): map buyer Wereda to a MoR code too, fail locally if unmapped
BuyerDetails.Wereda had the same problem Region did: companies.woreda holds
names ("Yeka") MoR has no confirmed regex for, but every Wereda value MoR has
actually shown us (seller "12"/"13", the collection's "574") is 1-3 digits
like Region. Precautionary, not confirmed -- but the fix is identical either
way: resolve through EIMS_BUYER_WEREDA_CODES and refuse to file rather than
send a guessed code.

Generalises the Region resolver (resolveRegionCode -> resolveLocationCode) to
cover both fields instead of duplicating it.

No code was invented for "Yeka" -- EIMS_BUYER_WEREDA_CODES ships empty, so
this buyer now fails locally (new stop) instead of silently sending a name
that was never verified against MoR's schema.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 07:51:41 +00:00
Hagernesh
aec4f3d654 fix(eims): match MoR's payload rules found by live rejections
Three live attempts turned six guesses into facts. Each fix below is the
gateway's own words, not a reading of the collection.

DocumentNumber and InvoiceCounter move differently, because MoR constrains
them differently. The counter must not skip -- "Invoice counter is not
correct. expected : 1" -- so a definitively refused document hands it back.
The document number must not repeat, so the attempt burns it. Both stay spent
after an ambiguous result, where MoR may have stored the document.

NatureOfSupplies is normalised to MoR's exact lowercase constant and rejected
outright if it is neither 'goods' nor 'service'; its schema branches on this
as a oneOf, so "Service" invalidated the whole ItemList.

Buyer region resolves through a name->code map and now FAILS locally when
unmapped. MoR validates Region against ^[0-9]{1,3}$ on both the seller and
buyer sides, so a name can never be sent and a guessed code on a tax document
is worse than refusing to file.

Seller phone, email, region and wereda are checked against MoR's own regexes
before anything is sent, so a placeholder like "_" fails locally instead of
costing a request and a counter.

EIMS_TAX_CODE stays required and unset in .env.example: the choice between
VAT0 (zero-rated) and VATEX (exempt) is a tax position awaiting finance, and
MoR's enum is recorded there for whoever decides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 06:57:50 +00:00
Hagernesh
2e26936bf1 fix(eims): retimestamp the EIMS migration to 3330000000000
3300000000000 collided with BookingWagonCancellations after the rebase.
3320000000000 is also unavailable: BulkContractTemplates3320000000000 is
already recorded in freight.migrations on the shared dev database from a
branch not present in this checkout, so checking only src/migrations is not
sufficient.

3330000000000 is unique across src/migrations and greater than the current
maximum timestamp recorded in freight.migrations.

Rename the migration file and class. The migration has no explicit name field
and no other code references its previous identity.

Verify migration discovery through the actual runtime path:
scripts/migrate.js loads compiled dist/migrations/*.js migrations, while
application boot does not run migrations automatically. Confirm the renamed
migration is present in dist.

For controlled dev verification, remove its migration-history row and run
pnpm migration:run again. The migration is discovered and applied under
3330000000000; its idempotent DDL produces no schema changes where the EIMS
schema already exists.
2026-08-08 04:56:57 +00:00
Hagernesh
6b1ffa831f feat(eims): surface filing state in backoffice and alert on failures
Two gaps that only bite in production: nobody could see an invoice's filing
state, and a blocked chain was visible only in the logs.

A failed filing now notifies the staff who can act on it. An ambiguous result
is HIGH priority because it blocks every further invoice for the system number
until someone resolves it, and nothing else would surface that -- the sweep
just goes quiet. A deterministic rejection affects one invoice, so it is
normal priority. The alert never throws: it must not mask the filing outcome.

The backoffice invoice detail page gains an EIMS card showing status, IRN,
counter, submitted and acknowledged timestamps, and the gateway's own error
message, with actions gated on invoices:eims_register. FAILED offers "File
again" -- the reservation model already allows re-registering a rejected
invoice, so retry needed no new endpoint. UNKNOWN offers no re-file button at
all, since resubmitting risks a duplicate registration, and instead explains
that a supervisor must record the IRN or discard the attempt.

Also aligns the migration class name with its renamed file. The DDL is
idempotent, so re-applying under the new name is a no-op against the columns;
it leaves one superseded row in freight.migrations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 04:56:57 +00:00
Marshal
a05753f4a6 Merge branch 'dev' of github.com:Tria-plc/edr-platform into freight_feature/usermanagement
# Conflicts:
#	apps/edr-freight-web/backoffice/src/App.tsx
2026-08-07 23:55:30 +00:00