# EDR Freight — API integration stack (headless). # # Overlay on docker-compose.e2e.yaml. Same base services (postgres, minio, # mocks, freight-api), except the payment microservice is REAL here instead of # `payment-mock-e2e`, and only the bank gateways are stubbed: # # freight-api-it ──HTTP──> payment-api-it ──HTTP──> gateway-mock-it # ^ │ # └────── RabbitMQ ───────┘ (outbox → payment.events → consumer) # # Its own compose project (`name:` below overrides the base) and its own host # ports, so it can run side by side with the Cypress e2e stack. # # node integration/scripts/it.mjs up|test|down|logs # # Never start it with plain `docker compose -f docker-compose.it.yaml` — it is # an OVERLAY and needs the base file first: # docker compose -f docker-compose.e2e.yaml -f docker-compose.it.yaml ... name: edr-freight-it services: # Outbox transport. The payment API publishes payment.succeeded/failed here # and freight consumes it — the production path. Copied from the passenger # harness (e2e/docker-compose.yml). rabbitmq-it: image: rabbitmq:3-management environment: RABBITMQ_DEFAULT_USER: edr RABBITMQ_DEFAULT_PASS: edr_secret RABBITMQ_DEFAULT_VHOST: payment ports: - "${IT_RABBIT_PORT:-5772}:5672" - "${IT_RABBIT_UI_PORT:-15772}:15672" healthcheck: test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] interval: 5s timeout: 5s retries: 20 # Stand-in for every bank/wallet gateway the payment API talks to, plus a # control plane the tests drive (force a provider to fail/hang, fire a # correctly-signed webhook, read back what was called). See # integration/gateway-mock/server.js. gateway-mock-it: image: node:20-alpine volumes: - ./integration/gateway-mock:/app:ro working_dir: /app environment: PORT: "4600" # Same secrets the payment API gets — so webhooks the mock signs pass the # API's REAL signature verification instead of bypassing it. CBE_SECRET_KEY: it-cbe-secret CBE_MERCHANT_ID: it-cbe-merchant PAYMENT_API_URL: http://payment-api-it:3003 command: ["node", "server.js"] ports: - "${IT_GATEWAY_PORT:-4600}:4600" healthcheck: test: [ "CMD", "node", "-e", "fetch('http://localhost:4600/__control/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", ] interval: 3s timeout: 3s retries: 10 payment-api-it: build: context: . dockerfile: apps/edr-payment-api/Dockerfile secrets: - npmrc depends_on: postgres-freight-e2e: condition: service_healthy rabbitmq-it: condition: service_healthy gateway-mock-it: condition: service_healthy environment: PORT: "3003" NODE_ENV: test # Payment tables live in their own schema of the same throwaway DB; # main.ts ensurePaymentSchema() creates it, migrationsRun does the rest. DB_HOST: postgres-freight-e2e DB_PORT: "5432" DB_USER: edr_e2e DB_PASSWORD: edr_e2e DB_NAME: edr_freight_e2e DB_SCHEMA: edr_payment # Same token freight already uses in the base stack. SERVICE_AUTH_TOKEN: e2e-service-token PUBLISHER_TRANSPORT: rabbitmq PAYMENT_RABBITMQ_URL: amqp://edr:edr_secret@rabbitmq-it:5672/payment # HTTP fallback targets (only used with PUBLISHER_TRANSPORT=http). PAYMENT_NOTIFY_FREIGHT_URL: http://freight-api-e2e:3001/api/internal/payments/mark-paid # Fast relay + sweep so retry/reconciliation are observable inside a test # rather than a minute later. OUTBOX_RELAY_INTERVAL_MS: "1000" RECONCILE_STALE_AFTER_MS: "5000" # Every gateway points at the one mock. Paths are per-provider prefixes. CBE_BASE_URL: http://gateway-mock-it:4600/cbe-birr CBE_MERCHANT_ID: it-cbe-merchant CBE_SECRET_KEY: it-cbe-secret CBE_NOTIFY_URL: http://payment-api-it:3003/webhooks/cbe-birr CBE_RETURN_URL: http://localhost/return TELEBIRR_BASE_URL: http://gateway-mock-it:4600/telebirr TELEBIRR_WEB_BASE_URL: http://gateway-mock-it:4600/telebirr/web TELEBIRR_FABRIC_APP_ID: it-fabric TELEBIRR_APP_SECRET: it-secret TELEBIRR_MERCHANT_APP_ID: it-merchant-app TELEBIRR_MERCHANT_CODE: "999999" TELEBIRR_NOTIFY_URL: http://payment-api-it:3003/webhooks/telebirr # Telebirr PSS-signs every request object — a throwaway key generated per # launch by it.mjs (nothing key-shaped lives in git). TELEBIRR_PRIVATE_KEY: ${IT_TELEBIRR_PRIVATE_KEY} EBIRR_BASE_URL: http://gateway-mock-it:4600/ebirr DMONEY_BASE_URL: http://gateway-mock-it:4600/dmoney CARD_BASE_URL: http://gateway-mock-it:4600/card WAAFI_BASE_URL: http://gateway-mock-it:4600/waafi CAC_BASE_URL: http://gateway-mock-it:4600/cac CAC_USERNAME: it-cac CAC_PASSWORD: it-cac CAC_APP_KEY: it-cac-key CAC_API_KEY: it-cac-api CAC_COMPANY_SERVICES_ID: "1" # Inbound CBE Unified Bill — we are the biller; bill-query hops back into # the freight API, so this direction runs real code on both sides. CBE_BILL_ENABLED: "true" CBE_BILL_CLIENT_ID: it-cbe-bill CBE_BILL_CLIENT_SECRET: it-cbe-bill-secret CBE_BILL_JWT_SECRET: it-cbe-bill-jwt FREIGHT_API_BASE_URL: http://freight-api-e2e:3001/api ports: - "${IT_PAYMENT_PORT:-3113}:3003" healthcheck: test: [ "CMD", "node", "-e", "fetch('http://localhost:3003/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", ] interval: 5s timeout: 5s retries: 12 start_period: 40s # Base-stack service, re-pointed at the real payment API. freight-api-e2e: depends_on: payment-api-it: condition: service_healthy environment: PAYMENT_API_URL: http://payment-api-it:3003 # Freight's payment module skips RabbitMQModule entirely when this is # unset (payment.module.ts) — without it, outbox events never arrive. PAYMENT_RABBITMQ_URL: amqp://edr:edr_secret@rabbitmq-it:5672/payment # RABBITMQ_ENABLED stays "false" (base stack) — it only gates the SMS/email # clients, which must remain off. The payment consumer is wired by # PAYMENT_RABBITMQ_URL alone. # Drain tail on every pay window. Production defaults to 5 minutes; a # reservation here lives ~60s, so 5 would push every natural expiry past # the suite's 180s timeouts. One minute keeps the tail real and observable # (src/expired-invoice-late-settle.it.ts asserts both sides of it). FREIGHT_PAYMENT_DRAIN_MINUTES: "1"