Commit Graph

2 Commits

Author SHA1 Message Date
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