Commit Graph

3 Commits

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