Commit Graph

3829 Commits

Author SHA1 Message Date
Nathnael
a6b2519136 feat(billing): filter invoices and manual payments by invoice type
Adds a free-form `types` CSV filter to the invoice list DTO and query
(same treatment as `paymentMethods` — each billing source mints its own
type string, so an IsIn would drop real values), carries it into the
invoices export dataset, and surfaces a Type column plus filter pill on
both the Invoices and Manual Payments tables.
2026-08-28 09:17:41 +00:00
Nathnael
78c6f8be0a feat(bookings): show the invoice number in Pricing & payment
The booking's freight invoice is looked up through the existing invoice
list endpoint (source=booking, sourceId matched by search), newest first
so a re-issue supersedes the old number.
2026-08-28 09:17:35 +00:00
Nathnael
7d9d1fb518 fix: invoice filter 2026-08-27 12:40:03 +00:00
Nathnael
63bd9197c5 fix: add platofmr transactions to the invoice pdf 2026-08-27 11:56:10 +00:00
Nathnael
3af3017b86 feat(backoffice): add an Account tab with the customer's portal logins
The detail page showed the company's business contact details but not the
credentials anyone actually signs in with, and the two drift apart
routinely — so "the customer says they can't log in" was unanswerable
from this screen.

Adds `GET /backoffice/customers/:companyId/accounts`, joining each
external profile to its IAM account, primary contact first. Deliberately
not filtered to active accounts: a suspended or never-activated login is
exactly the case being looked into. The user query selects columns
explicitly — the entity's relations include credentials and sessions, and
this response reaches a browser.

Rendered as cards rather than a table: it is a handful of rows of
mostly-optional fields, which a table renders as a field of dashes.
"Password never set" is called out on its own, being the usual answer to
"they never got in", and a profile whose IAM user is gone reads as a red
fault rather than an inactive status.
2026-08-27 09:30:24 +00:00
Nathnael
f8e8897f5c feat(backoffice): search customers by trade name and licence, filter by role
Staff search with whatever is in front of them. Company name, TIN, email
and profile reference already matched; a TIN's licence number and the
trade name of the business a role operates as did not, which is most of
what appears on a customer's own paperwork.

Adds a Role filter alongside it. Distinct from the existing Type pill:
that is the company's own kind, this asks "who does X?" — one `customer`
company routinely holds importer and exporter at once.

Both are EXISTS subqueries rather than constraints on the joined
`companyProfiles` alias. Filtering the join would drop the company's
other profiles from the loaded entity, so an importer-and-exporter would
render as importer-only.
2026-08-27 09:30:12 +00:00
Nathnael
a48d77aff8 feat(backoffice): show each role's eTrade business on the customer detail page
The reviewer approving a role had no way to see which business it claims
to operate as, so there was nothing to check the uploaded licence
document against. The Role profiles table now carries a column with the
trade name, the licensed activity (does it actually cover this role?),
the licence number (the only unambiguous handle — trade names repeat
across a TIN's licences) and the renewal date.

A role with nothing attached reads as a yellow "Not attached" rather than
a blank: it is a review finding. Yellow, not red, because a co-operative
or investment-licence company legitimately has none.

Two fixes alongside it:

- The profile reference was already rendered but is minted only on
  approval, so every pending role drew an empty line. It now says so.
- `TableCard` gained an optional header section, so padding sits per
  section and the table runs edge to edge. The header stays outside the
  scroll region — inside, a title slides away from its own table.
2026-08-27 09:29:41 +00:00
Nathnael
ab5e7e6db6 feat(billing): show the buyer's trade name on invoice documents
An invoice is billed to one company profile, and that profile's eTrade
licence is usually a different business from the one the company
registered under — so the buyer's name alone does not say which business
was billed.

Both document paths gain a row: the shared invoice/receipt model reads it
off the already-loaded `companyProfile` relation, and the warehouse fee
invoice joins `company_profiles` through the booking.

`sameCompanyName` suppresses the row when it merely repeats the buyer
name, which is the common case. It compares loosely because eTrade spells
one legal suffix three ways (PLC / P L C / PRIVATE LIMITED COMPANY) and
pads names with double spaces; it decides whether a row is worth printing
and nothing else.

The EIMS buyer `LegalName` is deliberately untouched — a MoR filing
carries the registered entity, same rule as the seller side.
2026-08-27 09:29:25 +00:00
Nathnael
604710bd25 feat(companies): attach an eTrade business to each company profile
A TIN holds many business licences split by activity — export of coffee,
freight forwarding, import of vehicles — but the company picked one for
its whole record, so every operational role shared it. Each profile now
names the business it actually operates as.

Stored on `company_profiles.etrade_business` as a snapshot (licence
number, trade name, activity, renewal) rather than a bare licence number,
so the portal and backoffice can show it without an eTrade round-trip —
that API is slow, serves a broken TLS chain and is regularly down. Not
unique: one business may legitimately back several roles.

The licence number is a client input, so it is never stored as sent —
`ETradeService.findBusinessOption` looks it up under the company's own
TIN and persists eTrade's record, which makes another company's licence
simply unfindable.

Choosing one is required wherever the customer adds a role with a TIN
already on file. The onboarding wizard is the exception by necessity: it
picks roles on its first step, before a TIN exists, so there is nothing
to choose from yet. There it is enforced through
`getOnboardingRequirements` instead — an unattached role is reported
outstanding and blocks submission — and the picker sits on the documents
step beside that role's licence upload.

Lifted entirely for a co-operative or investment-licence company: eTrade
holds no record for its TIN, so the requirement would be unsatisfiable.
2026-08-27 09:29:15 +00:00
Nathnael
1f5e6296b1 feat(etrade): take the selected licence's trade name as the company name
A TIN routinely trades under a name that is not its registered one, and
holds several licences with different trade names — of 58 TINs checked
against eTrade, 8 had at least one licence whose trade name differs from
the registered `BusinessName`, one of them across three licences.

`extractRegistrationData` now resolves `companyName` from the selected
licence's `TradeName`, falling back to `BusinessName` (16 of 309 licences
carry a blank trade name, so the fallback is load-bearing).

EIMS is pinned back to `BusinessName` for the seller's `LegalName`: an
invoice is a MoR tax filing and must carry the legal entity, not the
trade name. It is the only other caller.
2026-08-27 09:28:52 +00:00
Nathnael
4abb2af880 Merge branch 'dev' into freight/nati-2 2026-08-27 05:25:43 +00:00
Nathnael
3a7e16a746 fix(reports): stop finance reports dropping general-contract revenue
Every revenue report excluded invoices whose booking carries
contract_kind = 'GENERAL', on the premise that such a booking is an
umbrella contract row paid once and drawn down by many orders, so
counting it alongside those orders would double-count.

That premise does not hold. On the dev database all 169 GENERAL
bookings are real shipments with origin and destination yards, warehouse
receipts and their own invoices; there is no umbrella invoice to
double-count, and no invoice source of that kind exists at all. The
predicate simply deleted 119 invoices and ETB 164.6M of billed revenue
from every Finance report, which is why revenue-by-customer reported
ETB 39.4M collected while /billing/invoices/summary reported ETB 140.1M
- a gap of exactly ETB 100,741,976, the paid total of the 80 PAID
invoices the predicate hid.

Removing it from the shared revenue and invoice ledgers brings the
reports back in line with billing (ETB 210.6M billed, ETB 140.1M paid,
163 invoices), and removing it from the overview repository restores the
same bookings to the operational KPIs.

Also fixes what that exposed in the reports that build their own query:

- GATEWAY_PAID read the booking's whole gateway total onto every invoice
  sharing that booking. With 25 booking ids backing 58 invoices, the
  reconciliation report claimed ETB 113.2M of receipts against ETB 43.6M
  of settlement and showed ~ETB 89.4M of variance that does not exist.
  Receipts are now apportioned across an invoice's siblings by settled
  share, so the gateway column sums to the payments table and total
  variance is the real ETB 13.1M of manual settlements.
- Aging Receivables joined companies with an INNER JOIN, dropping
  shipping-line-billed arrears, and summed both currencies under a
  hardcoded ETB label. Both payer joins are now LEFT and the report
  takes a currency filter.
- Invoicing Pipeline summed ETB and USD invoices into one ETB total and
  applied no trade-direction scope, unlike every other Finance report.
  Both are now applied.

Verified against the shared dev database: every new statement passes
EXPLAIN, and the report totals reconcile with billing and with
freight.payments. Type-check and the reports and overview suites pass;
the five failures in billing.service.spec.ts are pre-existing on this
branch and untouched by this change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 05:22:37 +00:00
marshal
3f353e52b8 Merge pull request #1420 from Tria-plc/freight_feature/usermanagement
feat: enhance station work logging with user display names and improv…
2026-08-27 05:30:58 +03:00
Marshal
6ec3c8c5e0 feat: enhance station work logging with user display names and improve loading/unloading logic 2026-08-27 02:29:50 +00:00
Hagernesh
42ae6b9752 feat(train-scheduling): show coupled/uncoupled wagons on intercity marshalling doc
Marshalling 2 rendered only the final consist after a mid-corridor
adjustScheduleConsist call, with no signal that anything changed.
Reuses the existing ScheduleWagonAdjustmentLog (already recording
every ADD/REMOVE/SWITCH) filtered to the printed stop's yard and to
ADD/SWITCH, and prints a Consist changed at this stop box. Origin
import/export docs are untouched — the option is only passed from
intercityMarshallingDocument.
2026-08-26 19:00:43 +00:00
Hagernesh
69062389c7 fix(train-scheduling): marshalling docs respect mid-corridor wagon coupling
Origin marshalling (import + export) now prints leg slots as
TO BE LOADED AT <board yard> with board/alight stations and keeps
their containers out of the loaded tallies. Intercity Marshalling 2 no
longer hides whole-route cargo whose allocation never left PLANNED
(import flow confirms loading at schedule level), so the on-board view
matches the physical consist before and after mid-corridor coupling.
2026-08-26 08:30:38 +00:00
marshal
bcc5daa137 Merge pull request #1415 from Tria-plc/freight_feature/usermanagement
feat: implement MANUAL_ONLY status for bookings and update related logic
2026-08-26 01:02:18 +03:00
Marshal
29371b0b39 feat: implement MANUAL_ONLY status for bookings and update related logic 2026-08-25 22:01:03 +00:00
marshal
1459e646f6 Merge pull request #1414 from Tria-plc/freight_feature/usermanagement
feat: enhance train scheduling and contract management features
2026-08-26 00:46:17 +03:00
Marshal
b926a3116e feat: enhance train scheduling and contract management features
- Added StationWorkControls to manage loading/unloading phases in TrainScheduleV2DetailPage.
- Implemented API endpoints for recording station work and managing wagon detach requests.
- Updated contract templates to include Ethiopian customs handling options.
- Enhanced shipment forms to collect customs clearing agent details for without-customs bookings.
- Introduced NUMBER_OF_WAGONS as a unit of measure for bulk cargo, allowing customers to specify wagon counts.
- Improved validation for customs clearing agent information in shipment forms.
- Updated various components and services to accommodate new features and ensure data integrity.
2026-08-25 21:44:21 +00:00
Nathnael
8adbb05479 Merge branch 'dev' into freight/nati-2 2026-08-25 13:07:09 +00:00
Nathnael
08aba3fb4d fix 2026-08-25 13:05:55 +00:00
Nathnael
ac325a6282 feat(reports): report charged vs actual volume in revenue categories
Four changes the business asked for on Charged and Actual Volumes:

- Split the Leg column into From and To, both sortable.
- Add 20ft and 40ft container counts beside TEU, off the marshalling
  record's container items.
- Classify cargo into the revenue vocabulary rather than the operational
  one, so a corridor's tonnage and its revenue read in the same buckets.
  Charge-only buckets (incidental, first/last mile, customs) cannot be
  emitted — no physical wagon is one.
- Carry the empty wagons as rows of their own, the way the marshalling
  document lists them. `allocationLedgerQb` gains `includeEmptyWagons`,
  which starts the ledger from the wagon instead of the allocation; the
  empty and total wagon counts become plain group aggregates, so a
  departure's wagons now add up down its rows instead of every row
  repeating the train's total. Vehicle-Km lands on the empty rows and
  sums across legs.

`LOADED_WAGONS_EXPR` gains a FILTER on the allocation being present — a
no-op for every allocation-grain report, and the fix at the one place
all of them route through. `SCHEDULE_EMPTY_WAGONS` had no callers left
and is deleted.

Verified: type-check clean, 50 report specs pass (incl. a new one
asserting the cargo expression only emits keys the revenue vocabulary
offers), and the report SQL EXPLAINs and runs against edr_dev — 60 empty
wagons over 4 rows, 53,539 Vehicle-Km, 20ft/40ft counts populating.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:05:18 +00:00
Nathnael
6d1e4a630e feat(reports): publish per-stop times when loading & unloading groups by train
Grouped by Train, a row was a period average over the train's stops, which
threw away the one thing that grain is for: when the work actually happened.
The row is now the stop itself — logged arrival, departure, unloading and
loading times, and that stop's own durations. Grouped by Station it still
averages over every train that called there.

The two shapes need different columns, so `ReportColumn.hideWhen` names the
filter values that hide a column, and the runner resolves sort against the
visible set — sorting by an average and then switching to Train falls back to
the default sort instead of a 42703 on a column the query no longer selects.
`ReportFilterDef.defaultValue` pins the unset grain to `train`, so "no value"
is never a third shape. The table, the export field list and the chart toggle
all follow the visible set; the chart is station-grain only, since per-stop
rows have nothing to bar-chart.

Both grains EXPLAIN-validated and run against the dev database.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:01:47 +00:00
Nathnael
01ea05f013 fix(auth): keep secondary positions in permission checks
IAM lets an employee hold several positions, but the vendored JwtGuard
collapses employee.positions[] down to a single employee.position and
drops the rest. Non-delegate secondary positions vanished entirely, so
staff on two posts resolved to one post's permissions and every check
on the other rejected them.

FreightJwtGuard re-attaches the full list from the same session
snapshot the parent guard already read, so nothing extra is fetched
per request beyond a cached session lookup. employee.position is left
untouched, keeping audit logging and delegation unaffected.
collectPermissionKeys and collectPositionTypeKeys now union across
every position, and /me returns them all.

Verified against a real two-position user (djibouti-gl-director +
djibouti-gl-chief) on the local dev database:

  /me positions                     1   -> 2
  /me permissionKeys                17  -> 28
  GET /api/interchange-documents    403 -> 200
  GET /api/trains                   403 -> 200

11 permissions recovered, none lost. Six single-position users return
byte-identical payloads before and after.
2026-08-25 12:00:43 +00:00
marshal
a4e9466915 Merge pull request #1412 from Tria-plc/freight_feature/usermanagement
feat: enhance booking and audit log functionalities
2026-08-25 02:50:34 +03:00
Marshal
d5a5085d6d feat: enhance booking and audit log functionalities
- Implemented read-only locking for customer-requested container sizes and billing currency in the GlCreateBookingForm component.
- Added functionality to lock partner quantities based on shipment requests in the ConsolidationPartnerPanel.
- Introduced a new Leave action in the LogPassYardWorkModal to unassign bookings from trains.
- Enhanced the AuditLogsPage to support filtering by action and added a Go button for direct navigation to entity detail pages.
- Updated WagonCancellationsPage to handle odd-20ft credits requiring partner selection during rebooking.
- Improved TrainScheduleV2DetailPage to allow manual loading of cargo and display warnings for unassigned bookings.
- Added a new reference field to the audit logs for better searchability and tracking of actions.
- Created a migration to add the reference column to the audit logs table and established an index for efficient querying.
- Defined a registry for audit reference sources to streamline the retrieval of human identifiers for various entities.
2026-08-24 23:49:20 +00:00
Roba Boru
5c2100e76d Fix passenger list alignment 2026-08-24 18:14:10 +03:00
Roba Boru
2037e8bf30 Merge branch 'dev' of https://github.com/Tria-plc/edr-platform into feature/group-booking 2026-08-24 18:05:21 +03:00
Roba Boru
a5385e1462 Added group booking option 2026-08-24 18:04:13 +03:00
Nathnael
0a85dbe60d Merge branch 'dev' into freight/nati-2 2026-08-24 14:02:03 +00:00
Nathnael
562c8b48ba feat: updateing the loading and unloading 2026-08-24 14:01:19 +00:00
Hagernesh Tadesse
cde346af57 Merge pull request #1410 from Tria-plc/credit-invoice
feat(eims): derive sales receipt fields from recorded invoice payment
2026-08-24 16:21:38 +03:00
Hagernesh
565265af79 feat(eims): derive sales receipt fields from recorded invoice payment 2026-08-24 13:04:03 +00:00
marshal
6da8adc6e9 Merge pull request #1409 from Tria-plc/freight_feature/usermanagement
fix issue -> consolidation
2026-08-24 15:51:12 +03:00
Marshal
2a107e8ba3 fix issue -> consolidation 2026-08-24 12:24:23 +00:00
Nathnael
17c2dca3cc feat(reports): record loading and unloading time per stop
The OCC report publishes total loading and unloading time and the other
activity left over from a station stay, but nothing recorded when handling
started or ended — the July 2026 seed had to write the figure into a
checkpoint note. Four nullable stamps now ride the stop's arrival row, which
is the row the staying-time report builds a stay from (a turnaround's
departure belongs to a different schedule).

Handling is unloading start to loading end, so a container stop reads as one
window and a bulk station that only loads or only unloads still reports its
half; other activity is the rest of the stay. Both stay NULL where nothing
was logged rather than collapsing to zero.

- station-staying-time: + loading/unloading and other activity per stop
- turnaround-cycle: + the same, summed over the cycle's stops
- loading-unloading (new): per train per station per period, so a week or
  month view is that train's average over its stops
- the stop/stay query moves to operations-classification, shared by both
2026-08-24 12:09:04 +00:00
Hagernesh
c9a68570e4 feat(eims): import MoR location master and fix importer arg parsing 2026-08-24 09:08:34 +00:00
Nathnael
2286135228 feat(reports): add port warehouse operations summary
Reproduces the monthly count sheet a port warehouse publishes: trains,
containers by size and laden state, wagons, TEU and bulk wagons per cargo
type, each split into export and import beside an overall total.

Two departures from the spreadsheet it replaces:

- Wagons are counted distinctly from the marshalling record rather than
  derived as 20ft/2 + 40ft + bulk wagons, which overstates whenever a wagon
  ran part-loaded.
- Total is counted over everything rather than summed across the direction
  columns — a train carrying both an import and an export booking belongs to
  both and would otherwise count twice.

Demurrage is billed on invoice lines and is left to Revenue by Category.

Adds a station filter matching either end of the corridor, so one warehouse
can report the trains it worked in both directions.
2026-08-24 08:58:58 +00:00
Nathnael
ee2caa332c Merge branch 'dev' into freight/nati-2 2026-08-24 08:38:37 +00:00
Nathnael
eba09c73ae fix: locomotive filtering 2026-08-24 08:21:29 +00:00
Nathnael
2e5dadeee9 fix(reports): apply the trade scope to six unscoped reports
These reports resolved ctx.directions and never read it, so a user restricted
to one trade direction saw every row, and a user with no trade access — where
directions is [] and the rule is show nothing — saw all of them.

first-last-mile-bookings scopes on the booking's own direction; the other five
scope on ts.direction. global-logistics-wagons uses the fragment form so a log
row whose schedule is gone stays visible, which is the rule the other ledgers
apply to rows carrying no direction.

Also adds the missing soft-delete guards: b.deleted_at on the first/last mile
booking join, ts.deleted_at on the wagon-teu-utilization and
global-logistics-wagons schedule joins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 08:01:20 +00:00
Nathnael
b7f8a436fb fix(reports): filter the plan side of plan-versus-actual reports
The cargo filter was applied only to the operated and attainment subqueries.
The plan side selected targets by metric and dimension alone, and the FULL
OUTER JOIN put every filtered-out key back as a row of zeros —
?categories=FERTILIZER returned all ten planned categories.

planKeyFilter restricts targets to the selected categories (or container
classes), reading ot.cargo_category for a station plan and ot.dimension_key
otherwise. Values are whitelisted against the vocabulary and inlined, because
the fragment is assembled into raw CTE text and the runner does not validate
multiselect values.

Two related grain leaks close with it:

- planCountryFilter narrows a station plan to the chosen country. All seven
  station targets are Ethiopian, so the Djibouti view was listing 33 Ethiopian
  targets as stations that moved nothing.
- planGrainFilter drops the plan entirely when origin, destination, train
  number or direction is set. No target carries a route, so the plan there was
  the whole corridor's target sitting beside one slice of its work, and the
  implement rate read as a miss that never happened.

Fixes cargo-volume-performance, cargo-volume-by-station, trainset-performance
and teu-performance together.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 08:01:12 +00:00
Nathnael
de407c0014 feat(reports): clamp report descriptions with a show more toggle
Report descriptions run to a paragraph. ReportDescription wraps Mantine's
Spoiler to clamp them to two lines, and the toggle only renders when the
text actually overflows.

PageHeader kept its subtitle on a hard truncate class, which would have
pinned the spoiler to one line, so a ReactNode subtitle now renders as-is
and owns its own layout. A string subtitle still truncates as before.
2026-08-24 07:54:36 +00:00
marshal
27b4191afe Merge pull request #1406 from Tria-plc/freight_feature/usermanagement
feat: enhance booking cancellation logic for consolidated pairs with …
2026-08-24 10:15:36 +03:00
Marshal
979f024869 feat: enhance booking cancellation logic for consolidated pairs with one paid side 2026-08-24 07:13:50 +00:00
Nathnael
baef14c847 style(reports): reformat revenue transactions with prettier
Whole-file requote to double quotes plus wrapped column literals. No
behaviour change.

Kept as its own commit because it is out of step with the rest of
reports/definitions, which is single-quoted: bare prettier ignores
@edr/prettier-config, so running it on one file requotes that file alone.
Drop this commit if the directory should stay consistent.
2026-08-24 07:12:51 +00:00
Nathnael
e8f46675dc feat(reports): add a total wagons column to charged vs actual volume
Loaded and empty wagons were both shown but never summed, so the train's
actual consist had to be added up by hand on every row. MAX() on the empty
count for the same reason the distance uses it: the value is constant within
a group that includes ts.id and the leg.
2026-08-24 07:12:40 +00:00
Nathnael
a3f06597cc feat(reports): break charged vs actual volume down by leg
The report priced every departure against its planned origin-to-destination
corridor, so a train that worked several station-to-station moves showed one
row and one distance. Ton/Km and Vehicle-Km were then computed against that
single corridor and understated the work actually done.

The row grain is now the leg: consecutive checkpoint events at different
yards, read with lead() over each schedule. DISTINCT because a train that
works the same pair twice in one departure is still one leg — without it the
join fans the cargo out and doubles every SUM in the group. Both ends fall
back to the schedule's own corridor, so a departure with no checkpoints
logged keeps exactly the single row it had before.

Only query() joins the legs. The KPIs stay corridor-level and would count the
same cargo once per leg if they had that join.
2026-08-24 07:12:32 +00:00
Nathnael
cdb2f234c3 feat(reports): drop the raw booking ID from revenue transactions
The bare UUID column sat next to the booking reference it duplicates, and a
reference is what anyone reading or exporting this report actually quotes. The
report is the audit trail for an export, so a column nobody can act on is
weight in every downloaded file.
2026-08-24 07:12:17 +00:00