Commit Graph

3614 Commits

Author SHA1 Message Date
Marshal
88b995d3d7 fix issue 2026-08-08 14:22:38 +00:00
Marshal
4b8119fd3c chnages 2026-08-08 14:02:58 +00:00
Marshal
1700ae0747 chnages 2026-08-08 14:00:45 +00:00
Nathnael
d337fa0d85 fix(freight:backoffice): remove coarse fleet.view/fleet.manage/admin fallbacks
Now that every fleet-resource page and settings page has its own
dedicated permission key (previous commit), the broad fallbacks are
redundant and over-grant: anyone holding only fleet:view/fleet:manage
or admin could reach every page in that whole section, not just one.

Removed fleet.view fallback from: Routes, Locomotives, Train Builder,
Wagons, Containers, Cargoes, Compliance & Alerts, Procurement, and the
Overview dashboard's Fleet KPI tab.

Removed fleet.manage fallback from: canFleetAction() (per-resource
fleet CRUD, lib/permissions.ts) and TrainBuilderDetailPage's wagon-
assignment check. Hard-delete already had no such fallback.

Removed admin fallback from: File settings, Dropdown settings,
Contract templates, Portal content, Trade access, Exchange rate.

Left untouched: Incidents (sole gate is fleet.view — no dedicated
edr_freight_app:incidents:* key exists on the backend yet, so there's
nothing to fall back FROM; removing it would make the page
super-admin-only).

Access-narrowing change: anyone currently relying on the coarse grant
without also holding the specific resource/settings key will lose
access to these pages until roles are updated to grant the specific
keys directly. Audit role assignments before this deploys.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 13:59:06 +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
Marshal
7a84826947 usd remove from the portal 2026-08-08 12:39:56 +00:00
marshal
1b0978bc1e Merge pull request #1183 from Tria-plc/freight_feature/usermanagement
feat(freight-web): restrict booking currency to ETB and portal ETB pa…
2026-08-08 13:49:16 +03:00
Marshal
229d9c6651 feat(freight-web): restrict booking currency to ETB and portal ETB payments to CBE bill only 2026-08-08 10:47:48 +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
Nathnael
ee25de8817 fix(freight:backoffice): wire dedicated permission keys instead of broad fallbacks
Compliance & Alerts, Procurement, File settings, Dropdown settings,
Trade access, and Exchange rate all fell back to a broad permission
(fleet:view or admin) even though a dedicated key already existed in
FREIGHT_PERMS — meaning granting one of these pages meant granting
several unrelated ones too. Each now checks its own key first, with
the broad permission kept as a fallback for existing role grants.

Incidents left as-is: no dedicated edr_freight_app:incidents:* key
exists yet on the backend.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 09:36:51 +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
Stephanos A
3251901aa7 Merge branch 'alpha' of github.com:Tria-plc/edr-platform into alpha 2026-08-08 11:14:17 +03:00
Stephanos A
734351b357 Overall report and passenger list updates 2026-08-08 11:13:08 +03:00
Hagernesh
375cf55e5f LM map missing map api fall back fix 2026-08-08 07:51:41 +00: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
e12cb78e9b feat(portal): pay-later last-mile note, gated by service type
Last-mile toggle explains deferred billing (confirm containers after
Djibouti departure, sign supplementary LM contract on truck approval,
pay advance) — shown only when the service defers mile billing, not
for RAIL_CONTAINER_PAID_MILE where the mile is priced into the booking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 05:24:22 +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
Marshal
cd90ccb0f5 feat(contracts): per-cargo bulk contract templates 2026-08-07 23:50:27 +00:00
marshal
7d8a2847e0 Merge pull request #1172 from Tria-plc/freight_feature/usermanagement
Freight feature/usermanagement
2026-08-08 02:00:59 +03:00
Marshal
d2eb47d14b feat(ui): 2026-08-07 23:00:06 +00:00
Nathnael Wondisha
a9d0a7e9e6 Merge pull request #1170 from Tria-plc/freight/nati-2
fix: iam migration
2026-08-08 01:19:40 +03:00
Nathnael
7f1d8fa260 fix: iam migration 2026-08-07 22:18:41 +00:00
Nathnael Wondisha
55e0c6fb0b Merge pull request #1168 from Tria-plc/freight/nati-2
fix: rm iam migration from migrate
2026-08-08 01:10:56 +03:00
Nathnael
d44c3f3a38 fix: rm iam migration from migrate 2026-08-07 22:10:05 +00:00
Marshal
6b3c055a93 Change the value of Field Status = SUCCESS to Success 2026-08-07 15:21:55 +00:00
Hagernesh
b8e702dbc0 CAS total 2026-08-07 15:08:40 +00:00
Hagernesh
67573d0835 feat(eims): file issued invoices on a cron sweep, off by default
Invoices are produced by the freight workflow rather than by a person, so the
production path for filing is a sweep, not the manual endpoint.

A @Cron picks the oldest never-submitted invoice and hands it to the existing
EimsInvoiceRegistrationService -- no registration logic is duplicated, and the
durable reservation still decides whether the submission may proceed. Sweeping
rather than hooking the eleven places an invoice can be created or issued keeps
the workflow untouched, puts the HTTP call outside the invoice transaction by
construction, and lets a crash or restart be picked up on the next tick.

invoices.eims_status is the queue; nothing new is persisted. Only NOT_SUBMITTED
is eligible: UNKNOWN is never retried automatically because the document may
already be filed, and FAILED waits for an explicit retry policy. The tick also
refuses to start while eims_system_state holds an in-flight submission or a
block, and only one invoice is filed per tick so a misconfiguration costs one
rejected document rather than a burst.

Requires both EIMS_ENABLED and EIMS_AUTO_SUBMIT; the second defaults to false
so authentication can be live long before filing is. Logs carry the invoice
number, status and IRN only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:08:40 +00:00
Hagernesh
02db3d2e73 fix(eims): keep EIMS filing off the Finance role preset
Invoices are produced by the freight workflow, not by a person, so filing is
not a Finance job function. The manual endpoints exist for controlled testing
and exceptional operations, and are left out of every role preset so they are
assigned to named admins instead.

Split resolve onto its own permission, invoices:eims_resolve: resolving an
unacknowledged submission clears the system-wide chain block and can record an
IRN against an invoice, which is a supervisor action rather than an
operational one. eims/status stays on the ordinary invoices:view.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:08:40 +00:00
Hagernesh
2e7ef40d9e feat(eims): take the source system from the access token
MoR stamps systemNumber and systemType into the access token it issues for
the authenticating credentials, which makes the token the authority on them.
Registration now reads both from there instead of from configuration, so the
SourceSystem block cannot drift from what the gateway believes we are.

EimsAuthService decodes the token payload after login, requires both claims
to be non-empty, and exposes them through getSessionContext(). The token is
decoded but never verified -- it is MoR's, signed with MoR's key -- and is
kept out of the log line, which names only the system it identified.

EIMS_SYSTEM_NUMBER and EIMS_SYSTEM_TYPE become optional expectations rather
than inputs: when set they are compared against the claims and a mismatch
fails fast, so neither side silently wins. Neither is required to register
any more.

Registration and manual resolution both resolve the session before touching
the state row, which is keyed by the system number: a login failure now
costs nothing because no counter has been reserved yet.

Test fixtures move to eims-test-fixtures.ts. They previously lived in
eims-auth.service.spec.ts, which made jest execute that suite again inside
every importing spec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:08:40 +00:00
Hagernesh
eadecf3fcf chore(eims): default the EIMS tax treatment to 0
Set EIMS_TAX_CODE=0 and EIMS_TAX_RATE_PERCENT=0 in .env.example as
instructed. Every line is emitted with TaxAmount 0 and TotalLineAmount equal
to PreTaxValue.

The collection's only observed TaxCode is "VAT15", so "0" is unverified
against the gateway and may draw a 406 rule-validation error. Both values
are env-only, so correcting them needs no code change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:08:40 +00:00
Hagernesh
7573019038 feat(eims): register invoices with MoR EIMS and persist the outcome
Add manual single-invoice registration, verification and reconciliation.
Nothing submits automatically; invoice creation is untouched.

Sequencing uses a durable reservation. The counter is consumed and the
holder recorded in a committed transaction before the request leaves the
process, and the HTTP call runs outside every transaction. A counter is
therefore never reused once an attempt begins, a crash mid-flight leaves the
reservation standing instead of inviting a blind resubmission, and an
ambiguous result blocks the whole system number rather than one invoice --
PreviousIrn is unknown, so any later document would chain to a stale IRN.

Deterministic rejections (400/406/401/403) mark the invoice FAILED and clear
the block. Timeouts and 5xx mark it UNKNOWN and keep it. Since /v1/verify
takes an IRN we never received in that case, POST :id/eims/resolve is the
exit: record the IRN confirmed in the MoR portal, or discard. A recorded IRN
is verified against the gateway first and refused unless EIMS reports it
against this invoice's document number.

Business and tax configuration is validated locally before anything is
locked, allocated or sent, so a missing tax code fails naming the exact
environment variables instead of at the gateway. No tax value is defaulted.

Filing gets its own permission (invoices:eims_register) rather than riding
on invoices:export -- registration is irreversible at MoR and must not
follow from the right to download a PDF.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:08:40 +00:00
Hagernesh
2644d5e52d feat(eims): add invoice mapper and signed EIMS transport
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>
2026-08-07 15:08:40 +00:00
Nathnael Wondisha
22e6e550bc Merge pull request #1164 from Tria-plc/freight/nati-2
Freight/nati 2
2026-08-07 15:58:02 +03:00
Nathnael
c9bb105e94 feat: add nationality indicator to the customer 2026-08-07 12:54:00 +00:00
Nathnael
f330f486e5 feat: add notification to intercity user 2026-08-07 12:53:19 +00:00
Nathnael
116b479bb0 feat: scope notification to permission actions 2026-08-07 12:42:33 +00:00
Nathnael Wondisha
116ea97be8 Merge pull request #1163 from Tria-plc/freight/nati-2
Freight/nati 2
2026-08-07 15:26:32 +03:00
Nathnael
1e9149ce00 feat: better navigation in backoffice 2026-08-07 12:24:30 +00:00
marshal
db5a13d7c3 Merge pull request #1162 from Tria-plc/freight_feature/usermanagement
feat(bookings): pending export requests hold wagons
2026-08-07 15:23:12 +03:00
Marshal
84c3f7a584 feat(bookings): pending export requests hold wagons 2026-08-07 12:22:43 +00:00
marshal
caa3bf5503 Merge pull request #1161 from Tria-plc/freight_feature/usermanagement
feat(wagons): audited maintenance/availability toggle
2026-08-07 15:11:35 +03:00