37 Commits

Author SHA1 Message Date
Hagernesh
c937290019 feat(eims): resolve buyer geography from the MoR location master
BuyerDetails Country/Region/City/Wereda now resolve from the Ministry's own
EIMS_COUNTRY_REGION_VW master instead of the EIMS_BUYER_*_CODES env maps and
the ethiopia-geo-codes table. Both invented their codes and looked names up
globally, so KERSA/GORO/BABILE/BURE — each present in several zones with
different LOCALITY_NOs — could be filed against the wrong jurisdiction.

Resolution is hierarchical and refuses to guess: an unknown or ambiguous
address raises a local validation error naming the level that failed, and
never selects the first matching row. Spelling differences between EDR and
MoR live in a reviewed, parent-scoped alias layer; the dataset itself stays
verbatim so it remains traceable to the Ministry sheet.

Resolution now runs before the counter reservation in both the single and
bulk paths, so a bad company address no longer burns an EIMS sequence number.

Adds eims:import-locations to regenerate the dataset from a future workbook,
reporting duplicate rows and same-hierarchy code conflicts.
2026-08-24 06:59:50 +00:00
Nathnael
00bd1250ee feat: WIP element Chat intergration 2026-08-17 08:02:00 +00:00
Nathnael
69805315d9 feat: dev and staging bypass 2026-08-15 08:10:47 +00:00
Hagernesh
2d4da8110b eims integration master test complete 2026-08-12 14:12:23 +00:00
Hagernesh
8d3dfa4113 fix(eims): map buyer Wereda to a MoR code too, fail locally if unmapped
BuyerDetails.Wereda had the same problem Region did: companies.woreda holds
names ("Yeka") MoR has no confirmed regex for, but every Wereda value MoR has
actually shown us (seller "12"/"13", the collection's "574") is 1-3 digits
like Region. Precautionary, not confirmed -- but the fix is identical either
way: resolve through EIMS_BUYER_WEREDA_CODES and refuse to file rather than
send a guessed code.

Generalises the Region resolver (resolveRegionCode -> resolveLocationCode) to
cover both fields instead of duplicating it.

No code was invented for "Yeka" -- EIMS_BUYER_WEREDA_CODES ships empty, so
this buyer now fails locally (new stop) instead of silently sending a name
that was never verified against MoR's schema.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 07:51:41 +00:00
Hagernesh
aec4f3d654 fix(eims): match MoR's payload rules found by live rejections
Three live attempts turned six guesses into facts. Each fix below is the
gateway's own words, not a reading of the collection.

DocumentNumber and InvoiceCounter move differently, because MoR constrains
them differently. The counter must not skip -- "Invoice counter is not
correct. expected : 1" -- so a definitively refused document hands it back.
The document number must not repeat, so the attempt burns it. Both stay spent
after an ambiguous result, where MoR may have stored the document.

NatureOfSupplies is normalised to MoR's exact lowercase constant and rejected
outright if it is neither 'goods' nor 'service'; its schema branches on this
as a oneOf, so "Service" invalidated the whole ItemList.

Buyer region resolves through a name->code map and now FAILS locally when
unmapped. MoR validates Region against ^[0-9]{1,3}$ on both the seller and
buyer sides, so a name can never be sent and a guessed code on a tax document
is worse than refusing to file.

Seller phone, email, region and wereda are checked against MoR's own regexes
before anything is sent, so a placeholder like "_" fails locally instead of
costing a request and a counter.

EIMS_TAX_CODE stays required and unset in .env.example: the choice between
VAT0 (zero-rated) and VATEX (exempt) is a tax position awaiting finance, and
MoR's enum is recorded there for whoever decides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 06:57:50 +00:00
Hagernesh
2e26936bf1 fix(eims): retimestamp the EIMS migration to 3330000000000
3300000000000 collided with BookingWagonCancellations after the rebase.
3320000000000 is also unavailable: BulkContractTemplates3320000000000 is
already recorded in freight.migrations on the shared dev database from a
branch not present in this checkout, so checking only src/migrations is not
sufficient.

3330000000000 is unique across src/migrations and greater than the current
maximum timestamp recorded in freight.migrations.

Rename the migration file and class. The migration has no explicit name field
and no other code references its previous identity.

Verify migration discovery through the actual runtime path:
scripts/migrate.js loads compiled dist/migrations/*.js migrations, while
application boot does not run migrations automatically. Confirm the renamed
migration is present in dist.

For controlled dev verification, remove its migration-history row and run
pnpm migration:run again. The migration is discovered and applied under
3330000000000; its idempotent DDL produces no schema changes where the EIMS
schema already exists.
2026-08-08 04:56:57 +00:00
marshal
7d8a2847e0 Merge pull request #1172 from Tria-plc/freight_feature/usermanagement
Freight feature/usermanagement
2026-08-08 02:00:59 +03:00
Marshal
d2eb47d14b feat(ui): 2026-08-07 23:00:06 +00:00
Hagernesh
67573d0835 feat(eims): file issued invoices on a cron sweep, off by default
Invoices are produced by the freight workflow rather than by a person, so the
production path for filing is a sweep, not the manual endpoint.

A @Cron picks the oldest never-submitted invoice and hands it to the existing
EimsInvoiceRegistrationService -- no registration logic is duplicated, and the
durable reservation still decides whether the submission may proceed. Sweeping
rather than hooking the eleven places an invoice can be created or issued keeps
the workflow untouched, puts the HTTP call outside the invoice transaction by
construction, and lets a crash or restart be picked up on the next tick.

invoices.eims_status is the queue; nothing new is persisted. Only NOT_SUBMITTED
is eligible: UNKNOWN is never retried automatically because the document may
already be filed, and FAILED waits for an explicit retry policy. The tick also
refuses to start while eims_system_state holds an in-flight submission or a
block, and only one invoice is filed per tick so a misconfiguration costs one
rejected document rather than a burst.

Requires both EIMS_ENABLED and EIMS_AUTO_SUBMIT; the second defaults to false
so authentication can be live long before filing is. Logs carry the invoice
number, status and IRN only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:08:40 +00:00
Hagernesh
2e7ef40d9e feat(eims): take the source system from the access token
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>
2026-08-07 15:08:40 +00:00
Hagernesh
eadecf3fcf chore(eims): default the EIMS tax treatment to 0
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>
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
Nathnael
79fec14c9a feat(auth): deny by default with employee/customer audience guards
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.
2026-08-07 07:32:25 +00:00
Nathnael
595c165820 feat: implement audit logs 2026-08-05 14:17:00 +00:00
Nathnael
3a69b961d4 refactor(freight): move the Fayda callback to /fayda/callback
Namespaces the OAuth landing path in all three places it exists: the API's
ack controller, both web apps' routes, and the redirect_uri env values.
A bare /callback claimed a generic top-level path in every app for one
provider's redirect.

The API side needed care. The ack controller moves to @Controller
('fayda/callback'), and the global-prefix exclusion has to name that exact
path — setGlobalPrefix's exclude is an exact route match, not a subtree, so
excluding "fayda" would have left /fayda/callback served at
/api/fayda/callback and 404ing at the registered redirect_uri, while
reading as though it covered everything under /fayda. Naming the full path
also keeps /api/fayda/verification/* prefixed, which every client calls.

Also drops a stale comment on the portal's callback route describing the
popup that no longer exists, and records why the route is public: behind
RequireAuth the onboarding gate redirects to /portal before the code+state
exchange can run.

NOT verified at runtime — this changes route registration, so boot the API
and confirm GET /fayda/callback answers un-prefixed and
/api/fayda/verification/start still resolves before relying on it.

Deploying this requires registering the new redirect_uri with eSignet
first; FAYDA_WEB_REDIRECT_URI, FAYDA_PORTAL_REDIRECT_URI and any mobile
client must be updated in step or verification breaks with a redirect_uri
mismatch.
2026-08-04 12:43:25 +00:00
Nathnael
acd2cfbe8c feat: add drain tail to the payments 2026-08-04 08:59:33 +00:00
Nathnael
dcda8d7d37 feat(freight-api): drop fan claim from fayda verification
esignet userinfo carries no national id number. keep sub/name/email/
phone/address, remove fanClaims config and the hard-fail gate that
would've blocked every real verification.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 06:57:27 +00:00
Nathnael
03b8ca0ce7 refactor: centralized the iam seeder 2026-07-27 08:42:27 +00:00
Nathnael
1fe10ee3c2 feat(seed): implement local IAM baseline seeder with configurable settings 2026-07-27 07:09:09 +00:00
Nathnael
6420c72e89 feat(auth): implement staff-triggered password-reset links 2026-07-20 11:20:50 +00:00
natib21
5a7133b599 fix 2026-07-07 08:42:01 +00:00
Marshal
1b92c57e23 refactor contract handling to support single route per contract and improve reference generation logic 2026-07-06 08:36:11 +00:00
yaschalew
47f87e8435 fix conflict 2026-07-03 17:31:29 +03:00
yaschalew
83887471a4 fix 2026-07-03 17:25:33 +03:00
Nathnael
4e6e614b48 feat: add email to notification and otp 2026-07-03 10:59:58 +00:00
natib21
e4b0c73c63 fix fayda 2026-07-03 09:10:05 +00:00
hagiye
3decc9aeea interchange document generation and acknowledgement 2026-06-27 13:39:42 +03:00
Nathnael
6fd9b6d519 chore: setup sms rabbit mq 2026-06-25 07:41:33 +00:00
Marshal
5b9fed57fe add payment success and failure pages with environment variable support 2026-06-17 17:13:19 +00:00
Eyosiyas
a1940c4901 telebirr out in the payment 2026-06-08 11:49:50 +03:00
Abubeker Yasin
1d11cbda4d feat: ( payment ) create payment microservice 2026-06-05 21:29:55 +03:00
marshal
d226d7ef22 booking flow,summtion, approval, contract, mock payemnt and integration to back office, and also add permissions 2026-06-05 10:38:31 +03:00
SennayT
342ccb63de modify pipeline to speed up deployment 2026-05-28 12:52:56 +03:00
Michael Abebe
2940d20e06 fix:(freight:auth): finished integrating the freight api with the freight portal 2026-05-18 11:07:09 +03:00
Michael Abebe
0e0d2ea450 feat(freight:api): initial backend work 2026-05-18 10:25:33 +03:00
Muluhabt
3b8b6979db Project Initialization 2026-05-12 15:17:16 +03:00