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>
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>
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>
Mechanical Family-A port — same server contract BookingRequestsPage
already uses (tradeDirection/freightType/isGovernment/createdFrom/
createdTo/search/page/pageSize all already accepted).
One wrinkle: this hub has no true "unfiltered" state — it always
scopes to a fixed 4-status baseline (customsClearingEnabled=false
self-clearance bookings), with the old status Select's "All statuses"
row just being that baseline spelled out as an option. Modeled as a
normal optional Status enum pill (4 individual statuses, no synthetic
"All" entry) whose absence falls back to the baseline in the query
build, not in the filter defs themselves — `customsClearingEnabled`
stays a fixed, non-user-facing param the same way.
The real live fleet page — FleetCrudPages.tsx (previous commit) turned
out to be dead code; every fleet route (locomotives/trains/wagons/
containers/cargoes/vehicles/drivers) actually renders this one,
config-driven off resources.ts. Highest-leverage single file in the
remaining inventory: 7 routes at once.
- filterDefs are built per slug from `config.listFilters` (status/yard/
wagon type/train…, wired to real server params via each def's
default `{key: value}` toParams) plus a "Registered" date range. The
3 slugs with no server list filters (trains/containers/cargoes) fall
back to a plain client-only Status filter off a fixed enum, not
"whatever status exists in the currently-loaded rows" — the latter
would be circular (filterDefs feeds useFilters, which feeds the
query that produces those rows).
- serverListFilters/pagedFilters derive straight from `controls.params`
instead of hand-picking each field off local state — the def keys
already match the API's param names, so this is mostly free.
- filteredRows keeps the original's exact "usesServerListFilters ⇒
trust the server, don't re-check client-side" short-circuit — some
server list filters (a wagon's `trainNumber` matches either of two
different columns) aren't expressible as a plain client-side field
equality, and re-applying them would silently break.
- Pagination moved from a local `usePagination()` (component state) to
useFilters' URL-backed page/pageSize — the whole point of this pass.
DataTable takes `controls.tableProps(total)` directly; FleetCardGrid
(not a DataTable) is fed the same pieces by hand.
- The two manual "reset on slug/filter change" effects are dropped —
a same-app nav Link to a bare path already clears the query string,
and useFilters already deletes `page` on every filter/search change.
- FleetToolbar's view-mode SegmentedControl moves into FilterBar's
`children` slot; its search/filters props are no longer used here.
GET billing/invoices/:id/document?format=thermal renders a dedicated 80mm
receipt template (72mm printable, 4mm margins each side), not a CSS variant
of the A4 layout — the A4 CSS is absolutely-positioned/fixed-px, tuned for a
210mm page, and doesn't reflow at thermal width. No seal (not a thermal
convention, renders badly on 1-bit thermal heads); line items stack
(description, then qty x rate = amount) instead of a table, since a real
table leaves ~10-14 chars for description at this width.
PdfRenderService gains a thermal render path: full 80mm-width viewport,
content height measured via page.evaluate after settle (continuous-roll
receipts have no fixed page length), and a noFallback option — a Chromium
failure throws a clear error instead of silently degrading to the generic
A4/no-QR fallback, which would hand back a different document than what was
asked for. The frontend surfaces that as a toast pointing at the existing A4
download.
format is strictly validated (a4|thermal only, BadRequestException
otherwise), not silently coerced.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds cancel, sales/withholding receipt filing + listing + PDF download, and
credit/debit memo issuance to EimsFilingCard, each gated on its own
permission.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
eims-receipt-document.mapper.ts maps an EimsReceipt onto the shared
InvoiceDocumentModel layout, reading amounts back out of the stored request
body. Refuses to render anything not REGISTERED. GET
invoices/:id/eims/receipts/:receiptId/document, scoped to the invoice.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
POST billing/invoices/:id/memo files a MoR DEB/CRE memo by reusing
createInvoice unchanged. sourceId is the original invoice's own id, not its
source's — this structurally keeps memos out of findPayable/expirePayable/
billQuery's sourceId-keyed lookups regardless of status. Credit notes are
created settled; debit notes are created open/unpaid as a genuine new
receivable, not force-settled. memoIssue is granted to the chief position,
not the general finance role.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A BadRequestException thrown before reserve() (config assertion, DEB/CRE
validation) left the invoice NOT_SUBMITTED with nothing persisted, so the
same row was retried every tick forever — a permanent head-of-line block on
every invoice behind it. Now marked FAILED, guarded by a fresh status
re-read so a reservation's own SUBMITTING/UNKNOWN/blocked state is never
clobbered.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers 6 pages at once: the shared FleetCrudPage<T> factory
(TrainMasterDataPage, WagonsCrudPage, ContainersCrudPage,
CargoesCrudPage, LocomotivesCrudPage) plus the standalone
WagonTypesCrudPage.
- FleetCrudPage gains an optional `statusOptions` prop; when passed it
builds a Status enum FilterDef and swaps the old plain search Input
for FilterBar + useFilters + applyClientFilters (client-bridge —
these endpoints return bare arrays). Status option lists sourced
from the actual entity/enum definitions, not guessed, and reused in
each page's create/edit form Select instead of duplicating them.
Column-header click-to-sort is untouched (separate mechanism).
- WagonTypesCrudPage (hand-rolled, not on the factory) gets a boolean
Active/Inactive filter the same way, replacing its search-text hack
that string-matched "active"/"inactive" against the query.
- Radio-based filters (single-select enum, boolean) apply the instant
an option is picked and close — no Apply click needed. Checkbox
(multi-select) keeps the explicit Apply, since picking several is a
multi-step gesture.
- Route filter's Select dropdowns portal separately by default, so a
click landed as "outside" the outer pill popover and closed it
before a pick registered — same class of bug DateBody had.
`comboboxProps={{ withinPortal: false }}` fixes it.
- Route filter now pinned by default on both pages instead of behind
More filters.
- Date filter: widened to before/between/after (repository already
applies each bound independently) via a new `dateRangeParams`
helper — the old positional `{v[0]:from, v[1]:to}` mapping silently
put a "before" pick in the from param. Added a calendar icon + sm
sizing, and a wider popover so the range presets sidebar has room.
- Pill's clear (X) button enlarged; search box border/text opacity
strengthened to match the pill trigger restyle.