Commit Graph

1567 Commits

Author SHA1 Message Date
marshal
e2d32defc0 Merge pull request #1352 from Tria-plc/freight_feature/usermanagement
Freight feature/usermanagement
2026-08-20 07:04:21 +03:00
Marshal
a58157a7ee feat(bookings): show per-document upload/review audit and download icon on clearance detail 2026-08-20 04:03:27 +00:00
Nathnael Wondisha
8e11b3a168 Merge pull request #1349 from Tria-plc/freight/nati-2
Freight/nati 2
2026-08-19 13:17:07 +03:00
Nathnael
e964a9b8f4 chore: more reporting 2026-08-19 10:14:46 +00:00
Marshal
ad479bdcf2 issue fix 2026-08-19 10:10:04 +00:00
Marshal
13f66dfb66 changes 2026-08-19 08:41:51 +00:00
Marshal
f4a654f273 Merge branch 'dev' of github.com:Tria-plc/edr-platform into freight_feature/usermanagement 2026-08-19 06:42:36 +00:00
Marshal
cd1e9c38a7 feat: add new audit endpoints for consolidation approvals and invoice management 2026-08-19 06:42:19 +00:00
marshal
2942ad8d75 Merge pull request #1342 from Tria-plc/freight_feature/usermanagement
feat: implement parity checks for 20ft container bookings to ensure e…
2026-08-19 09:21:43 +03:00
Marshal
e6e4c07077 feat: implement parity checks for 20ft container bookings to ensure even numbers 2026-08-19 06:20:47 +00:00
marshal
c80d9c5abc Merge pull request #1339 from Tria-plc/freight_feature/usermanagement
feat: implement consolidation approval process for shared-wagon bookings
2026-08-18 17:12:28 +03:00
Marshal
db64a1878b feat: implement consolidation approval process for shared-wagon bookings
- Add migration for consolidation approvals table and status enum
- Create ConsolidationApprovalService to handle approval logic
- Implement repository for managing consolidation approvals
- Add entity for consolidation approval with necessary fields
- Develop frontend components for displaying and managing consolidation approvals
- Create tests for consolidation approval service to ensure correct behavior
2026-08-18 13:59:15 +00:00
marshal
c1150f8af2 Merge pull request #1337 from Tria-plc/freight_feature/usermanagement
Freight feature/usermanagement
2026-08-18 16:19:34 +03:00
Marshal
40904049cf feat: implement consolidation approval process for shared-wagon bookings
- Add migration for consolidation approvals table and status enum
- Create ConsolidationApprovalService to handle approval logic
- Implement repository for managing consolidation approvals
- Add entity for consolidation approval with necessary fields
- Develop frontend components for displaying and managing consolidation approvals
- Create tests for consolidation approval service to ensure correct behavior
2026-08-18 13:17:55 +00:00
Nathnael Wondisha
5e96394450 Merge pull request #1336 from Tria-plc/freight/nati-2
lifecycle
2026-08-18 16:14:38 +03:00
Nathnael
dd6df306f2 lifecycle 2026-08-18 13:11:48 +00:00
Nathnael Wondisha
065f34aa67 Merge pull request #1335 from Tria-plc/freight/feat/yard-loc-ac
feat: yard scoping to position
2026-08-18 15:57:03 +03:00
Nathnael
6823a32fee feat: yard scoping to position 2026-08-18 12:55:11 +00:00
Nathnael Wondisha
bd5d6d72b5 Merge pull request #1334 from Tria-plc/freight/feat/foreign-investors
fix: approval window
2026-08-18 15:51:45 +03:00
Marshal
22a3fb98ee feat: Implement consolidated booking functionality
- Added support for viewing and managing consolidated bookings in BookingRequestDetailPage.
- Enhanced BookingRequestsPage to display paired bookings in a single row.
- Introduced pairedDecision method in bookings service to handle decisions for both halves of a consolidated pair.
- Updated contracts service to include methods for manual consolidation of odd-20ft bookings.
- Created new components for selecting and editing consolidation partners.
- Added tests for paired decision logic and manual consolidation scenarios.
- Updated UI to reflect changes in booking handling and provide user feedback for odd container counts.
2026-08-18 12:50:35 +00:00
Nathnael
cbcc9a02e6 fix: approval window 2026-08-18 12:44:02 +00:00
Hagernesh
f7be33eaaf feat(eims): alias bare Addis Ababa sub-city names to the CSV's woreda codes
Confirmed live 2026-08-17: buyer profiles store just the sub-city name
("Bole", "Arada") as their woreda, never the source CSV's specific
example-woreda name ("Bole Woreda 01") — hit for three different
buyers in a row (Bole, Arada, Lemi Kura) before any got past this
check. Since the CSV lists exactly one representative woreda per
Addis sub-city, alias the bare name to that same code instead of
waiting on a fuller table — use what's already been supplied first.

Lemi Kura itself isn't in the source CSV at all (one of Addis
Ababa's newer sub-cities) — still needs its real code from MoR.
2026-08-18 11:51:03 +00:00
Nathnael Wondisha
b95952d67a Merge pull request #1332 from Tria-plc/freight/feat/foreign-investors
Freight/feat/foreign investors
2026-08-18 14:49:28 +03:00
Nathnael
431bd3c98b feat(companies): let a co-operative switch back to eTrade as well
Both manual-registration routes exist for one reason — eTrade holds no record
to fetch — so leaving one is the same act whichever it is. A co-operative that
has since taken out a trade licence had no way out at all: the flag is chosen
in the wizard, and an onboarded company can no longer reach it.

revert-to-etrade now accepts either flag and strips both, and a co-operative's
ACTIVE roles go back to Pending with it: those approvals were granted without a
per-role business licence, because a co-op owes none, and the moment it stops
being one that licence is due. Rejected, suspended and blacklisted roles are
left alone — promoting a blocked role to "awaiting approval" would launder a
staff decision away. An investor's roles are untouched; their licences were
always due and already reviewed.

Switching INTO a route stays with the wizard's nationality/role step, which
owns the mutually-exclusive rules; a second entry point would restate all three.
2026-08-18 11:37:43 +00:00
Nathnael
0b8b9c39ab fix(companies): clear the typed registration when the manual-entry box is un-ticked
Going back in the wizard and un-ticking co-operative or investment licence used
to write the flag and nothing else. The registration the customer had typed
stayed on the company row, so `hasRegistrationDetails` still read as a passed
eTrade lookup, resume dropped them at their furthest step rather than the
company one, and the application could be finished on unverified data with no
flag left on it for the backoffice to show.

That transition now costs what the settings switch costs: the eTrade-sourced
columns and the manager captured beside them are cleared, and onboarding drops
back to the company step so the TIN actually goes through eTrade. Both the
reset payload and the attribute strip are now shared with
`revertToRegularCompany`, which did this correctly already.
2026-08-18 09:00:21 +00:00
Nathnael
a9763a541a feat(companies): support foreign investors onboarding on an investment licence
A foreign company licensed by the Ethiopian Investment Commission is not on
the trade registry, so eTrade holds no record for its TIN and the lookup the
company step is built around returns nothing. Those customers could not get
past onboarding at all.

They now take the same route a co-operative does: an `investorLicence` flag in
`attributes`, and `applyEtradeSourcedFields` skips the eTrade re-check for any
company `usesManualRegistration` covers, so the registration they type is
persisted as sent instead of 400'ing "no registration found for this TIN".
Unlike a co-operative they still hold a business licence per operational role,
so that requirement is untouched, and the foreign document set already asks for
the investment licence itself — no new set.

Only a foreign company may carry the flag, and never alongside the
co-operative one: the two resolve to different document sets.

`POST /companies/onboarding/revert-to-etrade` gives it back. It clears the
typed registration rather than keeping it — the wizard treats a populated
registration block as a passed lookup, so leaving it would walk the customer
straight past the eTrade step the switch exists to reach — and returns the
company to pending, since an approval granted against typed data must not
carry over to a record that now claims to be eTrade's.
2026-08-18 08:45:15 +00:00
marshal
1e67034968 Merge pull request #1329 from Tria-plc/freight_feature/usermanagement
feat(freight): offer built-train wagons per boarding yard on multi-ya…
2026-08-18 11:28:32 +03:00
Marshal
c723b660e2 feat(freight): offer built-train wagons per boarding yard on multi-yard consists 2026-08-18 08:27:18 +00:00
Hagernesh
e67ccbb9cd fix(eims): stop sending our internal fee-basis tag as MoR's ItemList Unit
Confirmed live 2026-08-17 on INV-20260817-00008: MoR rejected the
document with a SCHEMA ERROR on ItemList[0].Unit — 'PER_CONTAINER'
(from the line's own metadata.unit) fails MoR's enum
(LTR/MTR/101/PCS/ROL/MTS/PKG/SET/KLG), its 8-char max, and its
^[A-Za-z]{3,8}$ regex all at once.

line.metadata.unit is our own fee-basis tag (PER_CONTAINER/PER_TON/
PER_ITEM — how a charge is computed) and was never a MoR unit of
measure; the mapper was reusing the same field name for two unrelated
concepts. Every line now sends the single configured
EIMS_UNIT_DEFAULT instead of guessing a per-line value that doesn't
exist in MoR's vocabulary.
2026-08-17 18:10:55 +00:00
Hagernesh
7d8ab932c2 feat(eims): implement POST /v1/bulkCancel
New endpoint: POST invoices/eims/bulk-cancel, body { items: [{invoiceId,
reasonCode, remark?}] }. Same eimsCancel permission as single cancel — a
batch-scale version of the same irreversible-at-MoR action, not a new
capability.

Same local-eligibility doctrine as single cancel: an already-cancelled or
never-registered invoice is refused right here, no HTTP call, before it
gets a seat in the batch. Only genuinely eligible invoices go into the one
/v1/bulkCancel request; every outcome (local refusal or MoR's own
per-IRN result) is reported back independently — one invoice failing
never blocks the rest.

MoR's bulk response mixes success and error shapes in the same array,
disambiguated by Status (capital, error) vs status (lowercase, success)
— matched back to our invoices by IRN. Notably the bulk success shape
carries no cancellationDate at all, unlike single cancel.

Left out of this pass: bulkRegister. It's async (returns only a
conversationId immediately, results arrive via a webhook callback we
don't have yet) and needs manual counter/previousIrn management per
the collection's own docs — a materially different reservation model
than today's single-invoice TX1/TX2 pattern. Scoping that is a
separate, bigger piece of work.
2026-08-17 17:49:33 +00:00
Hagernesh
7946c16327 feat(eims): bake in a starter table of Ethiopia region/zone/woreda codes
EIMS_BUYER_REGION_CODES/WEREDA_CODES/CITY_CODES were hand-maintained,
empty-by-default env vars — every buyer from a not-yet-seen area
crashed filing until someone found the MoR code and redeployed.
Happened three times in one afternoon (2026-08-17): Somali region,
Fafan zone, Jigjiga woreda, even the Ethiopia country code were all
unset on the triaplc.com deployment.

Ethiopia's administrative divisions are fixed reference data, not
buyer-specific config. Added ethiopia-geo-codes.ts, a static table
(source: ethiopia_administrative_hierarchy_master.csv, supplied
2026-08-17 — a representative sample, not all ~1000 real woredas)
merged in as the fallback under the existing env vars, which still
win on a name collision — for a quick correction without a redeploy,
or a buyer profile spelled differently than the table (already hit:
DB has zone "Fafen", official spelling "Fafan").

Known limitation, documented in the file: zone/woreda names aren't
always unique nationwide (e.g. "North Shewa" is both an Amhara and
an Oromia zone) and Company stores region/zone/woreda as three
independent strings with no parent linkage, so a flat name map can't
always disambiguate. Only affects the optional City field — never
blocks filing, unlike Region/Wereda.
2026-08-17 14:13:16 +00:00
Nathnael
7c6c894c78 Merge branch 'freight/nati-2' into freight/feat/element-chat 2026-08-17 12:53:39 +00:00
Nathnael
ab734aecc3 feat(chat): join users to rooms on sign-in 2026-08-17 12:53:08 +00:00
Nathnael
7aa4405423 Merge branch 'dev' into freight/nati-2
Conflict in ClearanceDocumentsPage: this branch migrated the page to the
pill FilterBar, dev added filters to the Select stack it replaced. Kept
the FilterBar and carried dev's additions across as a "Booked by"
(customerKind) FilterDef plus the shipping-line search placeholder; dev's
startOfDayIso/endOfDayIso went away because dateRangeParams already does
that. The Ship icon import is needed by dev's shipping-line customer cell,
which merged cleanly on its own.
2026-08-17 12:43:35 +00:00
Nathnael
7143ba1040 feat(customers): notify marketing on returned changes, name actors in history
Three gaps on the backoffice customer detail page:

- Rejecting a change request or sending it back for correction notified
  nobody. Adds CompanyNotifierService.changeRequestReturned, which pings
  the customer desk with the reviewer, the outcome and the note. Marketing
  joins that desk via customers:view + customers:get_notification in the
  role preset — grants still come from the IAM UI, the preset only sets
  the default for new environments.
- submitted_by / reviewed_by / actor_id were stored but never resolved, so
  the History tab could say what changed but never who asked or who sent
  it back. Resolves them through a shared iam-user-name util (deduped from
  the private copy in contract-document-history.service) and renders
  "Requested by" / "Sent back to marketing by" lines. The
  changes_requested badge is relabelled to match the workflow.
- "View" opened an in-page modal one document at a time. Adds
  openFileInNewTab, which opens the tab inside the click gesture and fills
  it once the authenticated fetch resolves, and an "Open all" button that
  loops over the documents table so every file lands in its own tab.
2026-08-17 12:38:06 +00:00
Hagernesh
0999bc0b8b fix(eims): a mapper failure after reservation also orphaned the block
toEimsInvoice/buildEimsContext sat outside the try/catch that calls
settleFailure — reservation happens (TX1), then request-building ran
unguarded, then submit() was the only thing actually wrapped. Any
exception during mapping (a validation error like an unmapped buyer
country, or a bug) skipped settleFailure entirely and left the
reservation permanently held: exactly the live incident just seen —
register 500'd, and every subsequent attempt on any invoice 409'd
'already in flight' until manually resolved.

Fix: the try block now starts right after reserve(), covering
request-building and submit() both. settleFailure's determinism
check is generalized to match — any error that is not an
EimsApiException is pre-wire and safe to release, not just
EimsConfigException (still labeled CONFIG; everything else pre-wire
is now labeled the new LOCAL kind). This is exhaustive by
construction: every error that actually touches the wire is already
normalized to EimsApiException inside EimsClientService.send()'s own
catch, so nothing outside that can be ambiguous.
2026-08-17 12:09:25 +00:00
Hagernesh
0464a44de4 fix(eims): a config error before signing must not block the whole system
settleFailure() treated any non-EimsApiException error as ambiguous
("might have reached MoR") and permanently blocked all further
filing until manually resolved. EimsConfigException (bad/missing
key, unparseable cert) is thrown by EimsSignerService before
EimsClientService.send()'s try/catch is even entered — by
construction it never reached the wire, so there is nothing
ambiguous about it.

This is exactly what happened live: a private-key parse failure
during the key/cert migration work reserved a counter, failed before
any HTTP call, and got treated as an unresolved in-flight submission
— blocking every other invoice from filing until someone manually
POSTs /eims/resolve.

Fix: EimsConfigException is now deterministic in settleFailure, same
treatment as a clean MoR rejection — both counters roll back, no
system-wide block, invoice marked FAILED (not UNKNOWN). Added a
CONFIG failure kind so the invoice's eimsLastError and the staff
alert both say plainly that the request never reached MoR, instead
of implying a MoR rejection.
2026-08-17 12:09:25 +00:00
Hagernesh
13f7bea590 feat(eims): accept private key/cert as raw PEM env vars
EIMS_PRIVATE_KEY / EIMS_CERTIFICATE — the PEM text pasted directly,
no encode/decode step at all. Precedence: raw PEM > base64 > path.

Motivated by the base64 path hitting a DECODER::unsupported error in
a live deployment with no way to tell whether the cause was transport
truncation, double-encoding, or an actually-bad file. Two fixes for
that class of problem together:
  - the raw-PEM var removes the encode/decode step entirely, so
    there's nothing left to corrupt in transit
  - a literal \\n (two chars) is unescaped to a real newline, for
    env stores that can't hold a literal line break
  - getPrivateKey() now checks the decoded bytes look like a PEM
    header before handing them to OpenSSL, so a still-bad value fails
    with byte count + safe preview instead of an opaque decoder error
2026-08-17 11:03:22 +00:00
Hagernesh Tadesse
13b0274da2 Merge pull request #1318 from Tria-plc/eims-integration
feat(eims): allow private key/cert as inline base64 env vars
2026-08-17 12:24:36 +03:00
Hagernesh
4a4b1981cb feat(eims): allow private key/cert as inline base64 env vars
EIMS_PRIVATE_KEY_BASE64 / EIMS_CERTIFICATE_BASE64, alternative to the
existing _PATH vars. Wins over the path when set; falls back to the
file otherwise. Neither var required at boot on its own — the
either/or check moved out of the flat REQUIRED_VARS list.

Lets a dockerized deployment receive the key/cert the same way it
already receives every other EIMS_* secret (plain env var into the
container) instead of needing a host bind mount into the container
filesystem.
2026-08-17 09:14:38 +00:00
marshal
76a1b98384 Merge pull request #1316 from Tria-plc/freight_feature/usermanagement
Enhance manual payment processing for USD and ETB invoices
2026-08-17 12:13:57 +03:00
Marshal
1ca7776143 Enhance manual payment processing for USD and ETB invoices
- Updated API documentation and summaries to reflect support for both USD and ETB invoices.
- Modified data structures to include trade direction for invoices.
- Adjusted UI components to accommodate manual payment confirmations and display relevant information.
- Implemented filtering options for currency in the manual payments worklist.
2026-08-17 09:13:09 +00:00
Nathnael
00bd1250ee feat: WIP element Chat intergration 2026-08-17 08:02:00 +00:00
marshal
88dd8894d1 Merge pull request #1312 from Tria-plc/freight_feature/usermanagement
Freight feature/usermanagement
2026-08-17 10:57:20 +03:00
Marshal
c3c3d08a41 fix issue 2026-08-17 07:55:52 +00:00
Hagernesh
28c9dd93e0 feat(eims): seller identity enriched from e-Trade, cached (bootstrap only)
Ten EIMS_SELLER_* env vars were the only source of EDR's own seller identity,
duplicating data the platform already has via the same e-Trade lookup used
for every customer company at onboarding. EimsSellerCacheService now enriches
it — but static config remains the source of truth: MoR validates
SellerDetails against its own taxpayer registry (rule 7017, already cleared
against the current static values), so e-Trade fills a field only when the
static value is blank, never overrides one already confirmed. The static
config is therefore the durable fallback, not the cache; an in-memory
snapshot lost on restart is harmless.

ETradeService has no request timeout of its own and no AbortController, so
the cache enforces one locally (stops waiting, doesn't cancel the request)
and de-duplicates concurrent refresh() calls into the same in-flight promise.
getSellerDetails() is fully synchronous — zero I/O — so live registration
never depends on e-Trade being reachable, at boot or per invoice.

VatNumber and Email stay on static config permanently — confirmed by reading
e-Trade's actual response shapes, neither field exists anywhere in what it
returns. Region/Wereda/City reuse the existing EIMS_BUYER_*_CODES maps rather
than adding seller-specific ones — the geography is objective, not
buyer-specific.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 07:43:04 +00:00
Hagernesh
09bc7c74d7 feat(eims): derive buyer City/Country from company profile, not global config
City: EimsMapperContext.buyerCity was declared but never wired anywhere —
always null, silently, for every buyer. No dedicated city column on Company;
derives from Zone via a new EIMS_BUYER_CITY_CODES map, same lookup mechanism
as Region/Wereda but optional (an unmapped zone resolves to null rather than
throwing) — MoR has already accepted a live filing with City null.

Country: previously a single flat EIMS_BUYER_COUNTRY_CODE applied to every
buyer regardless of Company.country. Now reads company.country, resolved via
a new EIMS_BUYER_COUNTRY_CODES name-to-code map; the flat env var becomes a
domestic-only fallback (applies only when country is empty/Ethiopia), so an
unmapped foreign buyer fails locally instead of silently filing as Ethiopia.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 07:43:04 +00:00
Hagernesh
562ebf485c fix(eims): thermal page-length used viewport height, not content height
scrollHeight is defined as the larger of an element's content height and its
own (viewport) height — for a receipt shorter than the placeholder 1123px
viewport, it silently returned the viewport height back, producing a
correctly-formatted but page-length-tall PDF with a huge trailing blank
strip below the real content. Found by actually rendering one and looking
at it, not caught by unit tests (buildThermalHtml is pure string output,
never exercises page.pdf() sizing).

Fix: use a deliberately tiny (100px) viewport height for the thermal
measurement pass, forcing content to overflow it so scrollHeight always
reflects the receipt's real height. Also round the computed mm value before
templating it into the CSS length string.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 07:43:03 +00:00
Marshal
0e228bfd8d changes 2026-08-17 06:14:25 +00:00
Marshal
5ce5cbe29d feat: enhance booking and scheduling features with shipping line support and improved search functionality 2026-08-16 08:18:40 +00:00