mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
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>
204 lines
8.6 KiB
Plaintext
204 lines
8.6 KiB
Plaintext
# Copy to .env for local/docker compose (not committed).
|
|
PORT=3001
|
|
# @tria-plc/auditlog's client interceptor stamps every AuditLog row's
|
|
# `application` from this env var directly, bypassing MezgebModule.forRoot's
|
|
# applicationName option (package quirk). audit.controller.ts reads the same
|
|
# var when filtering reads, so this can be anything as long as it's set.
|
|
APPLICATION_NAME=freight-api
|
|
# Also required for @tria-plc/auditlog: its producer (AuditClientModule)
|
|
# reads the RMQ URL at package IMPORT time, before MezgebModule.forRoot's
|
|
# rmqUrl option ever runs, so only an env var reaches it — an in-code
|
|
# override is too late. Without this, audit events are silently dropped
|
|
# (no error, nothing published). Point it at whatever broker/vhost your
|
|
# RabbitMQ actually has a user provisioned on.
|
|
RABBITMQ_URL=amqp://localhost:5672
|
|
# GT06 GPS tracker TCP listener port (raw TCP, must be reachable by tracker SIMs). 0 disables.
|
|
GT06_TCP_PORT=5023
|
|
DB_HOST=localhost
|
|
DB_PORT=5433
|
|
DB_USER=postgres
|
|
DB_PASSWORD=
|
|
DB_NAME=edr_freight
|
|
|
|
# Telebirr payment gateway (freight merchant credentials)
|
|
TELEBIRR_BASE_URL=
|
|
TELEBIRR_WEB_BASE_URL=
|
|
TELEBIRR_FABRIC_APP_ID=
|
|
TELEBIRR_APP_SECRET=
|
|
TELEBIRR_MERCHANT_APP_ID=
|
|
TELEBIRR_MERCHANT_CODE=
|
|
TELEBIRR_NOTIFY_URL=https://freight-api.edr.et/payments/webhooks/telebirr
|
|
TELEBIRR_RETURN_URL=
|
|
TELEBIRR_TIMEOUT_EXPRESS=15m
|
|
TELEBIRR_PRIVATE_KEY=
|
|
TELEBIRR_PUBLIC_KEY=
|
|
TELEBIRR_INSECURE_TLS=false
|
|
|
|
# Public origin of the freight customer portal. Password-reset links sent to
|
|
# customers are built against this — it must be browser-reachable.
|
|
FREIGHT_PORTAL_URL=http://localhost:5173
|
|
|
|
# Portal pages the payment provider redirects the browser to after payment.
|
|
# Point these at the freight portal's public payment result routes.
|
|
PAYMENT_RETURN_URL=http://localhost:5173/payment/success
|
|
PAYMENT_FAILURE_URL=http://localhost:5173/payment/failure
|
|
|
|
# Drain tail (minutes) added to every booking pay window before anything expires:
|
|
# settlement is asynchronous, so a payment made in the window's last seconds lands
|
|
# after the deadline. Nothing is expired, no wagons are resold and no window cycle
|
|
# concludes until the tail passes. Defaults to 5 when unset.
|
|
FREIGHT_PAYMENT_DRAIN_MINUTES=5
|
|
# JWT (used by @tria-plc/api-common SharedAuthModule)
|
|
JWT_SECRET=
|
|
JWT_ACCESS_TOKEN_SECRET=
|
|
JWT_REFRESH_TOKEN_SECRET=
|
|
JWT_EXPIRES_IN=3600
|
|
# JWT expiry for @tria-plc/api-common token utils (jsonwebtoken timespan format)
|
|
JWT_ACCESS_TOKEN_EXPIRES=1h
|
|
JWT_REFRESH_TOKEN_EXPIRES=7d
|
|
|
|
# IAM seed defaults (used by @tria-plc/iamapi-common on first boot)
|
|
SUPER_ADMIN_EMAIL=superadmin@tria.com
|
|
SUPER_ADMIN_PHONE=
|
|
# Super-admin password. Falls back to DEFAULT_PASSWORD when empty.
|
|
SUPER_ADMIN_DEFAULT_PASSWORD=
|
|
DEFAULT_PASSWORD=password@tria
|
|
|
|
# IAM baseline shared with edr-passenger-api (roles, IAM app + permissions,
|
|
# position types, organization types + default units, org/unit settings, super
|
|
# admin). Replaces the seeder that shipped inside @tria-plc/iamapi-common — see
|
|
# packages/iam-seed. Seeds by DEFAULT when unset; every write is insert-only.
|
|
# Set to false to opt out.
|
|
SEED_IAM_BASELINE=true
|
|
|
|
# Freight org + staff (bookings / rule-engine IAM)
|
|
SEED_EDR_ORG=true
|
|
SEED_FREIGHT_STAFF=true
|
|
SEED_EXPORT_DJIBOUTI_INTERCHANGE_DEMO=false
|
|
|
|
# Limits GET /staff/users to employees of this IAM organization (iam.organizations.key).
|
|
# Unset = every employee. A key matching no organization returns no users.
|
|
# Dev seed key: edr_freight
|
|
# Production: ETHIO_DJIBOUTI_STANDARD_GAUGE_RAILWAY_SHARE_COMPANY_001
|
|
FREIGHT_ORG_KEY=edr_freight
|
|
|
|
# MinIO (used by @tria-plc/iamapi-common for file storage)
|
|
MINIO_ENDPOINT=localhost
|
|
MINIO_PORT=9000
|
|
MINIO_USE_SSL=false
|
|
MINIO_ACCESS_KEY=
|
|
MINIO_SECRET_KEY=
|
|
# Preset region so signed URLs are generated locally (no GetBucketLocation
|
|
# network call per sign). MinIO's default is us-east-1.
|
|
MINIO_REGION=us-east-1
|
|
|
|
# Redis
|
|
REDIS_HOST=localhost
|
|
REDIS_PORT=6379
|
|
|
|
# --- Notification broker (RabbitMQ) ---------------------------------------------
|
|
# SMS/email OTP + notifications are queued to RabbitMQ (consumed by the shared
|
|
# SMS/email services). Set RABBITMQ_ENABLED=false to skip the broker entirely
|
|
# (dev without a local broker).
|
|
RABBITMQ_ENABLED=false
|
|
RABBITMQ_URL=amqp://localhost:5672
|
|
SMS_QUEUE=sms_queue
|
|
|
|
# ── VeriFayda 2.0 (eSignet OIDC) identity verification ──────────────────────
|
|
# Disabled by default; /fayda/verification/start returns 503 until enabled.
|
|
FAYDA_ENABLED=false
|
|
FAYDA_CLIENT_ID=
|
|
FAYDA_AUTHORIZATION_ENDPOINT=
|
|
FAYDA_TOKEN_ENDPOINT=
|
|
FAYDA_USERINFO_ENDPOINT=
|
|
# Base64-encoded RSA private JWK used for the private_key_jwt client assertion
|
|
FAYDA_PRIVATE_KEY_BASE64=
|
|
# OAuth redirect_uri for MOBILE clients (must be registered with eSignet)
|
|
FAYDA_REDIRECT_URI=http://localhost:3001/api/fayda/verification/complete
|
|
# OAuth redirect_uri for WEB clients. Defaults to FAYDA_REDIRECT_URI when unset.
|
|
FAYDA_WEB_REDIRECT_URI=http://localhost:3000/fayda/callback
|
|
# OAuth redirect_uri for the customer portal (its own origin — must also be
|
|
# registered with eSignet). Defaults to FAYDA_WEB_REDIRECT_URI when unset.
|
|
FAYDA_PORTAL_REDIRECT_URI=http://localhost:5173/fayda/callback
|
|
CLIENT_ASSERTION_TYPE=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
|
|
FAYDA_SCOPE=openid profile email phone address
|
|
FAYDA_ACR_VALUES=mosip:idp:acr:generated-code
|
|
FAYDA_CLAIMS_LOCALES=en am
|
|
FAYDA_SESSION_TTL_MINUTES=10
|
|
EXPIRATION_TIME=15
|
|
ALGORITHM=RS256
|
|
EMAIL_QUEUE=email_queue
|
|
|
|
# Shared secret for service-to-service calls (payment microservice <-> freight).
|
|
# Required at boot; set ALLOW_UNAUTH_INTERNAL=true instead ONLY for local dev.
|
|
SERVICE_AUTH_TOKEN=change-me
|
|
|
|
# ── MoR EIMS e-invoicing (core.mor.gov.et) ─────────────────────────────────
|
|
# Disabled by default; every EIMS call fails fast with EIMS_NOT_CONFIGURED until enabled.
|
|
EIMS_ENABLED=false
|
|
EIMS_BASE_URL=https://core.mor.gov.et
|
|
EIMS_CLIENT_ID=
|
|
EIMS_CLIENT_SECRET=
|
|
EIMS_API_KEY=
|
|
EIMS_TIN=
|
|
# Source-system identity comes from the access token's systemNumber/systemType claims.
|
|
# Setting these turns them into expected-value checks: a mismatch against the token fails
|
|
# fast rather than one side silently winning. Leave empty to take the gateway's word.
|
|
EIMS_SYSTEM_NUMBER=
|
|
EIMS_SYSTEM_TYPE=
|
|
# Absolute paths to the INSA-issued credentials. Keep them OUTSIDE the repo; the file
|
|
# patterns are gitignored, but a path outside the working tree is safer still.
|
|
# The certificate is transmitted as base64 of this file's exact bytes — do not convert it.
|
|
EIMS_PRIVATE_KEY_PATH=
|
|
EIMS_CERTIFICATE_PATH=
|
|
# Optional tuning
|
|
EIMS_HTTP_TIMEOUT_MS=30000
|
|
EIMS_TOKEN_SKEW_SECONDS=45
|
|
|
|
# ── EIMS invoice registration (required only to register invoices) ─────────
|
|
# Seller identity: EDR's own legal details are not modelled anywhere in the DB.
|
|
# Region and Wereda are MoR *codes* (e.g. 13 / 574), not names.
|
|
EIMS_SELLER_LEGAL_NAME=
|
|
EIMS_SELLER_VAT_NUMBER=
|
|
EIMS_SELLER_PHONE=
|
|
EIMS_SELLER_EMAIL=
|
|
EIMS_SELLER_REGION=
|
|
EIMS_SELLER_WEREDA=
|
|
# Optional seller address parts; sent as null when unset.
|
|
EIMS_SELLER_CITY=
|
|
EIMS_SELLER_SUBCITY=
|
|
EIMS_SELLER_HOUSE_NUMBER=
|
|
EIMS_SELLER_LOCALITY=
|
|
# Tax treatment — REQUIRES FINANCE SIGN-OFF. The application models no tax at all
|
|
# (invoice.taxAmount is always 0), so nothing here is defaulted: registration fails
|
|
# locally, naming the missing variables, until these are set.
|
|
# Required, and deliberately unset: the choice is a tax position, not a default.
|
|
# MoR's enum (from its own 400): TOT10 TOT2 VAT15 VWHT TWHT VATEX VATWH WHOP2 WTHOI VAT0 VWTH
|
|
# Pending finance confirmation of VAT0 (zero-rated) vs VATEX (exempt).
|
|
EIMS_TAX_CODE=
|
|
EIMS_TAX_RATE_PERCENT=0
|
|
EIMS_EXCISE_TAX_VALUE=0
|
|
EIMS_INCOME_WITHHOLD_VALUE=0
|
|
EIMS_TRANSACTION_WITHHOLD_VALUE=0
|
|
# Document classification and payment presentation.
|
|
EIMS_TRANSACTION_TYPE=B2B
|
|
# Lowercase constant: MoR's oneOf branches require exactly 'goods' or 'service'.
|
|
EIMS_NATURE_OF_SUPPLIES=service
|
|
EIMS_PAYMENT_MODE=CASH
|
|
EIMS_PAYMENT_TERM=IMMIDIATE
|
|
EIMS_UNIT_DEFAULT=PCS
|
|
# MoR numeric country code for the buyer; our companies store the country name.
|
|
EIMS_BUYER_COUNTRY_CODE=
|
|
# Buyer region name -> MoR numeric code. companies.region holds names; MoR wants ^[0-9]{1,3}$.
|
|
# An unmapped region fails locally rather than being filed with a guess.
|
|
EIMS_BUYER_REGION_CODES=Addis Ababa=13
|
|
EIMS_CASHIER_NAME=
|
|
EIMS_SALESPERSON_NAME=
|
|
# Automatic filing of issued invoices (@Cron sweep, one invoice per tick).
|
|
# Independent of EIMS_ENABLED on purpose: authentication can be live long before
|
|
# filing is. Both must be true before anything is submitted automatically.
|
|
EIMS_AUTO_SUBMIT=false
|
|
EIMS_AUTO_SUBMIT_CRON=0 */5 * * * *
|
|
# MoR rejects documents older than 3 days; the sweep will not attempt those.
|
|
EIMS_AUTO_SUBMIT_MAX_AGE_DAYS=3
|