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>
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>
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>
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>
`<module>:view` gates the backoffice sidebar entry, the route, and the API
read all at once, so granting a user another module's list endpoint for a form
dropdown also hands them that module's whole page.
Seed a `:read` twin for every `:view` key and teach the freight guards to
accept it wherever the matching `:view` is required — on GET/HEAD/OPTIONS
only, since class and method guards AND together and a write route without its
own method gate would otherwise be reachable. The frontend never checks
`:read`, which is what keeps the module hidden.
Twins are derived, not hand-written, so a new `:view` gets one for free.
Grants stay hand-curated in iam.position_type_permissions.
The chief's typed advance was mandatory, so the rule-based estimate shown in
the approve dialog had to be retyped and could silently diverge from it.
advanceAmount is now optional: the advance defaults to the live last-mile
rate estimate (km x rate) and the typed value is only an override. When no
rate covers the job the request is rejected with a message telling the chief
to enter the amount manually, rather than approving a zero advance.
The advance invoice now bills in the rate's currency from the snapshotted
contract summary, falling back to the booking payment currency only when the
amount came from a manual override.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nest runs class and method guards together, so a class gate naming only
the view key silently required view AND action. Staff granted just an
action were denied before their key was checked. Each class gate now
names every key its routes use, and FleetView accepts an array so the
fleet controllers keep their coarse fallback.
Drops the one-off grant mapping SQL with it: already applied to dev, and
this fix removes the companion-view rule that was its recurring part.
The comment claimed reads were open to authenticated staff, but no guard
enforced it, so customers could list templates and render previews. The
backoffice Templates tab is the only consumer.
Payment summary/all moved off bookings:view onto payments:view, the
booking list now requires bookings:view for staff instead of accepting
any employee, and wagon reads require wagons:view or fleet:view rather
than bare authentication.
Neither was wired into the app, so every /facilities route and
POST /bookings/:id/allocate-containers returned 404 and their permission
keys granted nothing.
Nest runs class and method guards together, so a class-level view key
ANDs with every action key below it. Staff granted only an action were
denied before their key was ever checked: OCC could not fulfil wagon
transfers, dispatchers could not create a yard, and track staff could not
assign first/last-mile vehicles. Reads now carry the view key themselves,
and the warehouses baseline lists every key its routes use.
Gates the previously open support-agent, procurement, compliance,
facilities, list-users and trade-access controllers, separates customer
from staff routes across bookings, contracts, companies, billing,
warehouses, files and train scheduling, and moves billing, overview,
reports and the settings controllers onto their own keys instead of the
blanket admin key. Drops the demo-permissions module and the untested
notification test route.
Adds the support, procurement, compliance, facilities, trade-access,
overview, reports and staff-users keys, plus the split action keys for
bookings, contracts, train scheduling and settings. Retires eight seeded
keys that no feature ever enforced, revoking their grants first.
FreightPermissionGuard now rejects non-employee user types before the key
check, making every BookingStaff route staff-only in one place. Adds
PortalCustomer and MixedAudience for the routes both audiences share, and
stops ServiceAuthGuard failing open when SERVICE_AUTH_TOKEN is unset.
container mode: the Add Rate dialog offers the multi-tier editor in both
modes, each tier is created as its own rate row, and overlapping bulk
bands are rejected. Pricing picks the tier whose half-open band holds
the trip km, falling back to the legacy bandless bulk rate.
The rate matrix currency cell hardcoded USD, so a last-mile rate
priced in ETB still displayed as dollars. formatCell now takes the
row and reads its currency code, falling back to USD.
Last-mile currency select gets defaultValue ETB (new generic
FormFieldDef.defaultValue for create-time pre-selection) and lists
ETB (Birr) first; USD stays selectable. Backend already persisted
and validated the chosen currency.
Approval now snapshots the rate estimate and generates a last-mile
contract instead of invoicing immediately. The customer picks a delivery
date on the confirm form, then reviews and signs the contract in the
portal (saved signature or drawn); the signed PDF is stored as
LM_<CustomerName>.pdf and only then is the advance invoice issued.
Backoffice shows signature status and the contract download.
- tierList field type in rule-engine form dialog (add/remove rows,
overlap + open-ended validation, From km auto-continues)
- create submits one rate row per tier sequentially
- editing a band row keeps the single From/To/value form