Skip to content

Instantly share code, notes, and snippets.

@aungkyawminn
Last active September 3, 2025 09:26
Show Gist options
  • Save aungkyawminn/2ad982bc93c38d0625211f40478f8fff to your computer and use it in GitHub Desktop.
Save aungkyawminn/2ad982bc93c38d0625211f40478f8fff to your computer and use it in GitHub Desktop.
Corporate Internet Banking

Corporate Internet Banking – Backend Development Guide

This guide explains how to implement the backend API for the finalized CIB data model with single‑role‑per‑user:

  • Each user has exactly one role via users.role_id.
  • Role scope lives in roles.scope (global, organization).
  • Organization membership is tracked by organization_users.

The guide includes per‑module playbooks, API shapes, status machines, and Mermaid diagrams.


Table of Contents (per‑module guides)

  1. API Conventions & Contracts
  2. Auth, Sessions & RBAC (Single‑Role)
  3. Organizations & Membership
  4. Accounts & Beneficiaries
  5. Approvals Engine (Checking*)
  6. Transfers
  7. Bill Payments & Tax Payments
  8. Bulk Processing
  9. Scheduling & Cut‑off Windows
  10. Notifications
  11. Statements & Reports
  12. Outbox & Audit (EDA Support)
  13. Idempotency & Safe Retries
  14. Database & Migrations (RLS, Constraints, Indexes)
  15. Observability & SLOs
  16. Error Codes & Retry Semantics
  17. OpenAPI Fragments (YAML)
  18. Data Models

Big‑Picture View (C4 Context)

C4Context
title CIB System Context
Person(corpUser, "Corporate User")
System_Boundary(cib, "CIB Backend") {
  System(api, "REST API")
  SystemDb(db, "Postgres DB")
  SystemQueue(outbox, "Outbox Publisher")
}
System(core, "Core Banking", "Payments, Accounts, Statements")
System(mail, "Email/SMS Gateway")

Rel(corpUser, api, "Uses (JWT/Session)")
Rel(api, db, "CRUD / SQL")
Rel(api, outbox, "Insert Outbox Events")
Rel(outbox, core, "Publishes Payment/Beneficiary events")
Rel(api, mail, "Sends notifications")
Loading

Core Request Life Cycle (Sequence)

sequenceDiagram
  autonumber
  participant UI as Portal UI
  participant API as CIB API
  participant DB as Postgres
  participant AP as Approvals Engine
  participant OB as Outbox Worker
  participant CORE as Core Banking

  UI->>API: POST /transfers (draft)
  API->>DB: insert transfers(status='draft')
  API->>AP: materialize approval steps (checking_*)
  AP->>DB: create checking_transactions + assignments
  API-->>UI: 201 (pending_approval)

  UI->>API: POST /approvals/{id}/decisions approve
  API->>DB: insert checking_transaction_decisions
  AP->>DB: update checking_transactions.status=approved (if N-of satisfied)
  API->>DB: enqueue schedules or mark queued (cutoff_windows)
  API-->>UI: 200 (queued | scheduled)

  OB->>DB: read outbox_events pending
  OB->>CORE: push payment
  CORE-->>OB: ack/ref
  OB->>DB: update transfer status (released/settled/failed)
Loading

01 – API Conventions & Contracts

Naming

  • Endpoints are plural nouns: /transfers, /beneficiaries.
  • Polymorphic references use {type, id} pairs: e.g., schedulable_type, schedulable_id.

Versioning

  • Path-based: /v1/... then /v2/... for breaking changes.

Pagination

  • GET /resource?limit=50&cursor=... (forward-only).
  • Response:
{ "data": [...], "next_cursor": "abc123", "limit": 50 }

Idempotency

  • Accept Idempotency-Key on POST writes.
  • Store centrally in idempotency_keys and/or per-table UNIQUE client key.
  • For bulk_rows, use the per-row idempotency_key.

Errors

  • Use RFC 7807:
{
  "type": "https://docs.cib/errors/validation",
  "title": "Invalid request",
  "status": 422,
  "detail": "amount must be > 0",
  "instance": "/v1/transfers"
}

Status Codes

200, 201, 202, 400, 401, 403, 404, 409, 422, 429, 500, 503 (+ Retry-After)

Links

  • Include actionable links (approve/reject, cancel).

Optional Request Signing

  • For admin/internal APIs, HMAC signing in addition to JWT.

02 – Auth, Sessions & RBAC (Single‑Role)

Tables: users, roles, abilities, role_abilities, api_tokens, organization_users.

Model: Each user has exactly one role (users.role_id).
roles.scope is either global or organization.
If the role is organization-scoped, the user must be a member of the target org via organization_users.

Login flow

sequenceDiagram
  participant UI
  participant API
  participant DB
  UI->>API: POST /v1/auth/login {username,password}
  API->>DB: SELECT users + verify password
  API->>DB: INSERT api_tokens (ip, user_agent, expires)
  API-->>UI: 200 { token, user, role:{name, scope}, org_memberships }
Loading

Effective abilities (SQL)

SELECT ab.action, ab.subject, ab.fields
FROM users u
JOIN roles r            ON r.id = u.role_id
JOIN role_abilities ra  ON ra.role_id = r.id
JOIN abilities ab       ON ab.id = ra.ability_id
WHERE u.id = :user_id;

Authorization middleware (pseudo)

function authorize(subject: string, action: string, orgId?: number) {
  const user = ctx.user;               // includes joined role (name, scope)
  const abilities = loadAbilities(user.id);
  assert(has(abilities, {subject, action}), 403);

  if (user.role.scope === 'organization') {
    assert(orgId, 400); // org must be explicit
    assert(isMember(user.id, orgId), 403);
  }
}

Preventing scope mixing

  • Single role inherently prevents mixing global and organization scopes.
  • Ensure UI requires explicit organization selection when role.scope = 'organization'.

Sessions

  • api_tokens: store inet ip_address, user_agent, expiry; support revocation & rotation.

03 – Organizations & Membership

Tables: organizations, organization_users.

Hierarchy

flowchart TD
  Root["Top-level Org (parent_id=null)"]
  A["Subsidiary A"]
  B["Subsidiary B"]
  C["Dept C"]
  Root --> A --> C
  Root --> B
Loading

Resolving the request org

  • UI sets X-Org-Id header for tenant context.
  • Backend checks membership when role.scope='organization':
SELECT 1
FROM organization_users
WHERE user_id=:uid AND organization_id=:org;

Common APIs

  • GET /v1/orgs (tree)
  • POST /v1/orgs (admin)
  • POST /v1/orgs/{id}/members add user
  • DELETE /v1/orgs/{id}/members/{userId}

04 – Accounts & Beneficiaries

Tables: bank_accounts, beneficiaries.

Create Beneficiary

sequenceDiagram
  participant UI
  participant API
  participant DB
  UI->>API: POST /v1/beneficiaries {org_id, name, bank_code, account_no,...}
  API->>DB: INSERT beneficiaries
  API-->>UI: 201 {id, ...}
Loading

Validations

  • account_no format per bank standard.
  • wallet_id mutually exclusive with account_no for wallet-type payees.

Lookup APIs

  • GET /v1/accounts?org_id=
  • GET /v1/beneficiaries?org_id=&q=

05 – Approvals Engine (Checking*)

Tables: checking_flows, checking_conditions, checking_transactions, checking_transaction_assignments, checking_transaction_decisions.

Authoring uses checkable_types in checking_flows. Runtime uses checkable_type + checkable_id in checking_transactions.

Authoring → Runtime materialization

sequenceDiagram
  participant API
  participant DB
  participant ENG as Engine
  API->>DB: read checking_flows + checking_conditions by org + type
  ENG->>DB: create checking_transactions (snapshot steps)
  ENG->>DB: create checking_transaction_assignments (eligible approvers)
Loading

Decision flow

flowchart LR
  DRAFT-->PENDING[pending_approval]
  PENDING -- approve path --> SCH[scheduled/queued]
  PENDING -- reject --> REJ[rejected]
  SCH --> REL[released] --> SETTLED
  SCH --> FAIL[failed]
Loading

Decision insert (SQL)

INSERT INTO checking_transaction_decisions
  (checking_transaction_id, actor_id, decision, comment, decided_at, idempotency_key)
VALUES (:txn_id, :actor_id, 'approved', :comment, now(), :key);

N-of-M

  • Seed checking_transaction_assignments with group_key and n_required.
  • When a group reaches n_required, move to next step or mark approved.

06 – Transfers

Table: transfers

Status machine

  • draft → pending_approval → (scheduled | queued) → released → settled
  • Failures: failed, rejected, cancelled.
stateDiagram-v2
  [*] --> draft
  draft --> pending_approval
  pending_approval --> scheduled
  pending_approval --> queued
  pending_approval --> rejected
  scheduled --> released
  queued --> released
  released --> settled
  released --> failed
  draft --> cancelled
Loading

Create transfer

sequenceDiagram
  participant UI
  participant API
  participant DB
  participant AP as Approvals
  participant CUT as Cutoff
  UI->>API: POST /v1/transfers (Idempotency-Key: X)
  API->>DB: INSERT transfers(status='draft')
  API->>AP: materialize checking_* for this transfer
  AP->>DB: insert checking_transactions + assignments
  API-->>UI: 201 {status:'pending_approval'}
  Note over API,CUT: On approval, compute cutoff & schedule
Loading

Cut-off

  • On final approval, compute window_label from cutoff_windows; set scheduled vs queued.

Outbox

  • When moving to queued/released, insert outbox event: aggregate_type='transfers', event_type='transfer_queued'.

07 – Bill Payments & Tax Payments

Tables: bill_payments, tax_payments (mirror transfers).

Flow

  1. Draft → approvals (checking_*).
  2. On approval, schedule per schedules & cutoff_windows.
  3. Outbox to Core Banking, update by callback.

Sequence (bill payment)

sequenceDiagram
  participant UI
  participant API
  participant DB
  participant AP as Approvals
  participant OB as Outbox
  UI->>API: POST /v1/bill-payments
  API->>DB: insert bill_payments (draft)
  API->>AP: materialize steps
  AP->>DB: checking_transactions/assignments
  UI->>API: approve step
  API->>DB: decisions + schedule
  OB->>DB: read outbox_events
  OB->>Core: push bill payment
Loading

08 – Bulk Processing

Tables: bulk_batches, bulk_rows.

Pipeline

flowchart TD
  UPL[uploaded] --> PARSED
  PARSED --> VALIDATED
  VALIDATED --> ready_for_approval
  ready_for_approval --> processing
  processing --> completed
  processing --> partial_failed
  processing --> failed
Loading

Sequence

sequenceDiagram
  participant UI
  participant API
  participant DB
  participant VAL as Validator
  participant MAP as Mapper
  participant WRK as Worker
  UI->>API: POST /v1/bulk-batches (service, file-ref)
  API->>DB: INSERT bulk_batches(status='uploaded')
  API->>VAL: parse + validate rows -> bulk_rows
  VAL->>DB: INSERT bulk_rows(..., status='staged'|'ok'|'error')
  UI->>API: submit for approval
  API->>DB: update status='ready_for_approval'
  UI->>API: approve
  API->>WRK: enqueue processing (per row)
  WRK->>MAP: map normalized_payload -> specific {transfers|...}
  WRK->>DB: insert mapped transaction and link mapped_transaction_id
Loading

Idempotency

  • Use bulk_rows.idempotency_key; add UNIQUE if needed.

09 – Scheduling & Cut‑off

Tables: schedules, cutoff_windows.

When to schedule

  • If schedule_at present → create a schedules row.
  • Else, on approval compute cut‑off and mark queued/released.

Worker loop

while (true) {
  const rows = pickDueSchedules(now(), 100);
  for (const row of rows) {
    try { execute(row); markDone(row); }
    catch(e) { markFailed(row, e); }
  }
}

Diagram

sequenceDiagram
  participant API
  participant DB
  participant WRK as Scheduler Worker
  API->>DB: INSERT schedules(run_at, subject)
  WRK->>DB: SELECT due schedules
  WRK->>DB: mark picked -> done/failed
Loading

10 – Notifications

Table: notifications.

Send flow

sequenceDiagram
  participant API
  participant DB
  participant NTF as Notifier
  API->>DB: INSERT notifications(status='queued')
  NTF->>DB: SELECT queued
  NTF->>Gateway: send (email/in_app)
  Gateway-->>NTF: OK/ERR
  NTF->>DB: UPDATE status ('sent'|'failed')
Loading

11 – Statements & Reports

Tables: statement_requests, reports.

Statement request

sequenceDiagram
  participant UI
  participant API
  participant DB
  participant CORE as Core Banking
  UI->>API: POST /v1/statements {org_id, account_id, date_from,to,format}
  API->>DB: INSERT statement_requests
  API->>CORE: fetch statement stream
  CORE-->>API: stream
  API-->>UI: stream/URL
Loading

12 – Outbox & Audit (EDA)

Tables: outbox_events, audit_logs.

Outbox

sequenceDiagram
  participant API
  participant DB
  participant PUB as Outbox Publisher
  API->>DB: INSERT transfer + outbox_events(pending)
  PUB->>DB: SELECT pending
  PUB->>Core: send
  PUB->>DB: UPDATE published_at, status='published'
Loading

Audit

  • Append-only. Log actor, subject, action, and metadata (IP, UA).

13 – Idempotency & Safe Retries

Tables: idempotency_keys (optional), plus per-table idempotent keys.

Strategy

  • Accept header Idempotency-Key for POST. Deduplicate by returning the original response for the same key.
  • For bulk_rows, use idempotency_key per row.

14 – Database & Migrations (RLS, Constraints, Indexes)

RLS (tenant isolation)

  • Enable RLS on tables with organization_id.
  • Policy example (pseudo):
CREATE POLICY p_org_on_transfers ON transfers
USING (
  current_setting('app.role_scope') = 'global'
  OR organization_id = current_setting('app.org_id')::bigint
);

Constraints

  • checking_transactions.checking_condition_id ON DELETE CASCADE.
  • bulk_rows (batch_id, row_no) UNIQUE.
  • Optional: CHECK on roles.scope to be in (global, organization).

Indexes

  • (organization_id, status, created_at) on transactional tables.
  • (run_at, status) on schedules.
  • (organization_id, schedule_at) on transfers for scheduling queries.

Migrations

  • Apply enums → tables → fkeys → indexes → RLS and policies.

15 – Observability & SLOs

  • Logs: JSON with trace_id, org_id, user_id, subject_type, subject_id.
  • Metrics: approval latency P50/P95, outbox lag, cutoff miss rate, bulk row error rate.
  • Tracing: correlate API → DB → Outbox → Core callbacks.

16 – Error Codes & Retry Semantics

  • Client-safe retries via idempotency on POST.
  • For 429/503, send Retry-After.

Domain codes:

  • E_BENEFICIARY_INVALID
  • E_CUTOFF_MISSED
  • E_APPROVAL_NOT_ALLOWED
  • E_DUPLICATE_IDEMPOTENCY_KEY

17 – OpenAPI Fragments (YAML)

paths:
  /v1/transfers:
    post:
      summary: Create a transfer
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [organization_id, source_account_id, amount, currency]
              properties:
                organization_id: { type: integer, format: int64 }
                source_account_id: { type: integer, format: int64 }
                dest_account_no: { type: string }
                dest_bank_code: { type: string }
                dest_wallet_id: { type: string }
                beneficiary_id: { type: integer, format: int64 }
                amount: { type: number, format: double }
                currency: { type: string, minLength: 3, maxLength: 3 }
                reference: { type: string, maxLength: 140 }
                customer_ref: { type: string, maxLength: 64 }
                schedule_at: { type: string, format: date-time }
      responses:
        '201':
          description: Created
  /v1/approvals/{checking_transaction_id}/decisions:
    post:
      summary: Approve/Reject a step
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [decision]
              properties:
                decision: { type: string, enum: [approved, rejected] }
                comment: { type: string }
components:
  schemas:
    Transfer:
      type: object
      properties:
        id: { type: integer, format: int64 }
        status: { type: string, enum: [draft, pending_approval, scheduled, queued, released, settled, failed, rejected, cancelled] }
        amount: { type: number, format: double }
        currency: { type: string }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment