Skip to main content

Backend Development

The bopen backend is a single Rust microservice: the payment-service (services/payment-service).

Tech stack

  • Rust with Cargo workspace
  • Axum HTTP framework
  • Tokio async runtime
  • deadpool-postgres connection pooling
  • rust_decimal for precise monetary arithmetic (no floating-point)
  • Tracing for structured logging

Services

ServiceCrateDefault portPurpose
Payment Servicepayment-service8081Payment intent lifecycle, consent management, fee calculation

API surface

MethodPathAuthDescription
GET/healthNoneHealth check
POST/api/v1/paymentsJWTCreate payment consent + intent; schedules payment.initiated webhook
GET/api/v1/payments/:idJWTFetch payment status, fee breakdown, and audit trail
POST/api/v1/payments/:id/sca-completeNoneComplete SCA callback; advance state machine; schedules payment.authorized/settled/failed webhook
POST/api/v1/payments/:id/refundJWTTransition Settled → Refunded; schedules payment.refunded webhook
POST/api/v1/portal/registerNoneSelf-service acquirer registration (provisions Keycloak realm + merchant DB record)
GET/POST/api/v1/portal/usersJWTList / invite realm staff (Keycloak email invite via SES)
DELETE/api/v1/portal/users/:idJWTRemove staff user from realm
GET/api/v1/portal/webhook-secretJWTMasked signing secret + verification snippet
POST/api/v1/portal/webhook-secret/rotateJWTGenerate new signing secret
GET/api/v1/portal/webhooksJWTLast 50 webhook deliveries for the merchant
POST/api/v1/portal/webhooks/:id/replayJWTRe-queue a DEAD delivery

POST /api/v1/payments — request headers

HeaderRequiredDescription
X-Idempotency-KeyYesUUID; safe retries without duplicate charges
X-Merchant-Client-IdNoCognito client ID; defaults to dev-acquirer-client-0001

Business logic at payment creation

  1. Resolve merchant by X-Merchant-Client-Id; return 400 if unknown, inactive, or kyb_status != APPROVED
  2. Check idempotency key; return existing payment if already created
  3. Calculate fee: fee_amount = round(gross × fee_percentage + fee_flat, 4); return 400 if fee ≥ gross
  4. Generate end_to_end_id: BOP + 32-char UUID hex (35 chars; BACS/FPS field limit)
  5. Insert payment_consent row (AWAITING_AUTHORISATION, expires in 90 minutes)
  6. Insert payment_intent row linked to the consent with fee breakdown snapshotted
  7. Insert initial payment_events row (changed_by: MERCHANT_API)
  8. Return {payment_id, redirect_url, end_to_end_id, gross_amount, fee_amount, net_amount}

Environment variables

VariableDescriptionDefault
PORTHTTP listen port8081
RUST_LOGLog level filterpayment_service=info,tower_http=info
DATABASE_URLFull PostgreSQL connection string
DB_HOSTHost (alternative to DATABASE_URL)
DB_PORTPort5432
DB_USERNAMEDatabase user
DB_PASSWORDDatabase password
DB_NAMEDatabase namebopen
DB_SSL_MODESet to disable for local dev without TLS
MOCK_BANK_BASE_URLBase URL for mock bank redirect stubshttp://localhost:3000
RUN_MIGRATIONSSet to true to apply migrations at startup
KEYCLOAK_ADMIN_USERKeycloak master realm admin username (from Secrets Manager)
KEYCLOAK_ADMIN_PASSWORDKeycloak master realm admin password
SMTP_SECRETJSON blob from SES Secrets Manager ({host,port,from,username,password})

Running locally

DATABASE_URL="postgres://bopen_app:<password>@localhost:5432/bopen" \
MOCK_BANK_BASE_URL="http://localhost:3000" \
DB_SSL_MODE="disable" \
cargo run -p payment-service

The service health check is available at http://localhost:8081/health.

Docker build

docker build \
-f services/payment-service/Dockerfile \
-t bopen/payment-service \
.

docker run \
-p 8081:8081 \
-e DATABASE_URL="postgres://..." \
-e MOCK_BANK_BASE_URL="http://localhost:3000" \
bopen/payment-service

Database migrations

Migration scripts live under database/scripts/migrations/ and are applied in numbered order. When RUN_MIGRATIONS=true is set, the payment-service applies them at startup in the order defined in migrate.rs.

MigrationTables / changes
001_initial_schema.sqlcustomers, accounts, transactions
002_permissions.sqlSchema grants
003_b2b_payments.sqlmerchants, payment_intents, payment_events; payment_status enum
004_audit_enforcement.sqlImmutable ledger: triggers + REVOKE on payment_events
005_merchant_enrichment.sqlKYB status, fee model, merchant_bank_accounts
006_payment_consents.sqlConsent-vs-execution: payment_consents table; fee columns on payment_intents
007_audit_ledger_enrichment.sqlchanged_by, ordinal on payment_events
009_rename_client_id.sqlRenames cognito_client_idclient_id on merchants
010_schema_completions.sqlfailure_code on intents; Refunded enum; raw_payload_sha256 on events; debtor_bank_id on consents
011_payer_data_fields.sqlPayer context fields on payment_consents (AML / OBL)
012_webhook_deliveries.sqlrealm_slug, webhook_signing_secret on merchants; webhook_deliveries table

See Database Schema for the full ER model, state machines, and script run order.

Adding a new service

  1. Create services/<name>/ with Cargo.toml and src/main.rs
  2. Add the crate to the workspace members in the root Cargo.toml
  3. Add a Dockerfile for ECS deployment
  4. Register the service in the ECS CloudFormation template (infra/cloudformation)