# Copy to .env for local/docker compose (not committed). # Set to "dev" or "staging" to bypass OTP (fixed code 000000 also accepted), # payment (invoice auto-marked paid on initiate, no gateway call) and Fayda # (canned verified profile, no eSignet call). Leave unset in production. ENV= 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 here: 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 # Finance confirmed 2026-08-12: VATEX (exempt) for EDR's freight business — set in .env. EIMS_TAX_CODE= EIMS_TAX_RATE_PERCENT=0 EIMS_EXCISE_TAX_VALUE=0 EIMS_INCOME_WITHHOLD_VALUE=0 EIMS_TRANSACTION_WITHHOLD_VALUE=0 # Per-chargeType override, for an invoice whose lines need different MoR tax treatment (e.g. a # zero-rated freight line next to a taxed accessorial) — IRC-P01 compliance-test material. # A charge type not listed here falls back to EIMS_TAX_CODE / EIMS_TAX_RATE_PERCENT above. # EIMS_TAX_CODE_BY_CHARGE_TYPE and EIMS_TAX_RATE_BY_CHARGE_TYPE must list the same charge types. EIMS_TAX_CODE_BY_CHARGE_TYPE= EIMS_TAX_RATE_BY_CHARGE_TYPE= # Same mechanism; charge types not listed fall back to EIMS_EXCISE_TAX_VALUE / 0 respectively. EIMS_EXCISE_BY_CHARGE_TYPE= EIMS_DISCOUNT_BY_CHARGE_TYPE= # 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 # Same mechanism for Wereda. MoR has never named a Wereda regex in an error (only Region's is # confirmed), so this is precautionary — but an unmapped name still fails locally, not filed as a guess. EIMS_BUYER_WEREDA_CODES= 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 # ── Internal chat (Matrix/Element) ────────────────────────────────────────── # Disabled by default; /chat/sso and the nightly room/membership reconcile are # no-ops until enabled. See infrastructure/matrix/. MATRIX_ENABLED=false # Synapse URL reachable from this container (docker-compose service DNS in # prod, e.g. http://synapse:8008 — NOT the public https://matrix.edr.et). MATRIX_BASE_URL=http://localhost:8008 # Synapse's own public_baseurl — what Element itself is configured to call. # Only used to seed the sso.html handoff page's localStorage. MATRIX_PUBLIC_BASE_URL=https://matrix.edr.et MATRIX_CHAT_WEB_URL=https://chat.edr.et MATRIX_SERVER_NAME=matrix.edr.et # Must exactly match infrastructure/matrix/synapse/.env's MATRIX_JWT_SECRET — # this is the whole trust boundary for the SSO handoff. MATRIX_JWT_SECRET= # access_token of a Synapse server-admin account. Bootstrap it once via # infrastructure/matrix/synapse's MATRIX_REGISTRATION_SHARED_SECRET (see that # file's comments) — this app never touches the shared secret itself. MATRIX_ADMIN_TOKEN=