#!/usr/bin/env node /** * Generate `integration/.it-shards.yaml` — one payment API + one gateway mock * per shard. Compose has no loops, so the per-shard services are written out. * * Every shard is the real topology in miniature; nothing crosses between them: * * gateway-mock-it-{i} its own `modes`/`calls`/`orders` — the mock keeps those * process-global, and 20 of 25 specs call * POST /__control/reset in beforeAll, so a shared mock * would have each starting spec wipe every running spec's * live orders (which fails as a wrong *assertion*: an * unknown order still gets a correctly-signed webhook, * just with the mock's default amount). * payment-api-it-{i} its own DB_NAME (the shard's database, `edr_payment` * schema inside it), its own broker vhost, and its own * FREIGHT_API_BASE_URL — the inbound CBE-bill query has * to land on THIS shard's in-process app, and a single * container could only ever point at one of them. * * Written by it.mjs, gitignored. Do not edit by hand. */ import { writeFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const itDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); /** * All shards share one build and one image tag, so the payment API image is * built once and reused rather than N times. */ const PAYMENT_IMAGE = "edr-payment-api-it:local"; export function renderShards({ shards, apiPortBase, paymentPortBase, gatewayPortBase, dbPrefix, telebirrKey, }) { const services = []; for (let i = 0; i < shards; i++) { const gw = `gateway-mock-it-${i}`; const pay = `payment-api-it-${i}`; const db = `${dbPrefix}${i}`; const vhost = `payment_s${i}`; // The freight app for this shard runs on the HOST, inside the vitest worker. const freightBase = `http://host.docker.internal:${apiPortBase + i}/api`; services.push(` ${gw}: 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://${pay}:3003 command: ["node", "server.js"] ports: - "${gatewayPortBase + i}: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 ${pay}: image: ${PAYMENT_IMAGE} build: context: . dockerfile: apps/edr-payment-api/Dockerfile secrets: - npmrc depends_on: postgres-freight-e2e: condition: service_healthy rabbitmq-it: condition: service_healthy ${gw}: condition: service_healthy # The freight app is on the host now, not in this network. extra_hosts: - "host.docker.internal:host-gateway" environment: PORT: "3003" NODE_ENV: test # Payment tables live in their own schema of THIS SHARD's database; # main.ts ensurePaymentSchema() creates it, migrationsRun does the rest. # Same database as freight, so every raw edr_payment.* query in the specs # reads through the one connection. DB_HOST: postgres-freight-e2e DB_PORT: "5432" DB_USER: edr_e2e DB_PASSWORD: edr_e2e DB_NAME: ${db} DB_SCHEMA: edr_payment SERVICE_AUTH_TOKEN: e2e-service-token PUBLISHER_TRANSPORT: rabbitmq PAYMENT_RABBITMQ_URL: amqp://edr:edr_secret@rabbitmq-it:5672/${vhost} PAYMENT_NOTIFY_FREIGHT_URL: ${freightBase}/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 this shard's mock. Paths are per-provider prefixes. CBE_BASE_URL: http://${gw}:4600/cbe-birr CBE_MERCHANT_ID: it-cbe-merchant CBE_SECRET_KEY: it-cbe-secret CBE_NOTIFY_URL: http://${pay}:3003/webhooks/cbe-birr CBE_RETURN_URL: http://localhost/return TELEBIRR_BASE_URL: http://${gw}:4600/telebirr TELEBIRR_WEB_BASE_URL: http://${gw}: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://${pay}: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: ${JSON.stringify(telebirrKey)} EBIRR_BASE_URL: http://${gw}:4600/ebirr DMONEY_BASE_URL: http://${gw}:4600/dmoney CARD_BASE_URL: http://${gw}:4600/card WAAFI_BASE_URL: http://${gw}:4600/waafi CAC_BASE_URL: http://${gw}: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: ${freightBase} ports: - "${paymentPortBase + i}: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`); } return `# GENERATED by integration/scripts/gen-shards.mjs — do not edit. # ${shards} shard(s). Overlaid on docker-compose.e2e.yaml + docker-compose.it.yaml. services:${services.join("\n")} `; } export const SHARD_FILE = join(itDir, ".it-shards.yaml"); export function writeShards(opts) { writeFileSync(SHARD_FILE, renderShards(opts)); return SHARD_FILE; } export const shardServices = (shards) => Array.from({ length: shards }, (_, i) => [`gateway-mock-it-${i}`, `payment-api-it-${i}`]).flat();