Skip to content

Latest commit

 

History

22 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Payment Bridge Core

Java Spring Boot Build License

A backend-first fintech infrastructure prototype exploring payments, wallets, ledgers, FX conversion, KYC/AML compliance, CBDC bridge infrastructure, ledger/rail reconciliation, and a settlement engine.

Overview

Payment Bridge Core is a fintech infrastructure prototype that models how modern payment systems work under the hood — from JWT auth and wallet management through FX conversion, compliance rules, CBDC settlement via ISO 20022 messaging, reconciliation between the internal ledger and external rails, and a settlement engine that separates "the rail accepted this" from "the money is actually final."

The goal is not to build a production bank.

The goal is to understand the engineering concepts, system boundaries, and architectural decisions that power modern financial infrastructure — including the emerging CBDC ecosystem and the reconciliation/settlement discipline that keeps a ledger trustworthy.


Why This Project Exists

Most developers integrate payment APIs without seeing the infrastructure that sits behind them.

This project explores:

  • payment processing workflows and lifecycle
  • wallet and balance management (ledger-backed)
  • double-entry ledger recording
  • payment rail abstraction
  • FX conversion and multi-currency support
  • KYC level enforcement and AML pattern detection
  • CBDC bridge: MINT, REDEEM, TRANSFER, SWAP operations
  • ISO 20022 messaging standard (pacs.008 / pacs.002)
  • reconciliation between local ledger state and external rail state
  • settlement as a distinct step between rail acceptance and ledger finality
  • role-based access control for administrative operations

Tech Stack

Category Technology
Language Java 21
Framework Spring Boot 3.5.x
Security Spring Security + JWT
Database H2 (in-memory)
Migration Flyway
Documentation OpenAPI / Swagger
Testing JUnit 5, Mockito

Architecture

User
 │
 ▼
Auth & JWT (role: USER | ADMIN)
 │
 ▼
Wallet (balance from ledger)
 │
 ▼
Payment Service
 │
 ├── KYC Check (level-based tx limit)
 ├── AML Check (daily limit + structuring)
 │
 ├── FX Service
 │       ├── MockFxRateProvider (default)
 │       └── ExchangeRatesApiFxProvider (real)
 │
 ▼
RailRouter
 ├── MockRail         (local testing)
 ├── StripeRail       (Stripe sandbox)
 └── CbdcRail         (ISO 20022)
         │
         ├── CbdcBridgeResolver (MINT / REDEEM / TRANSFER / SWAP)
         └── CbdcSandboxServer
                 ├── ECB Sandbox  (USDC proxy)
                 ├── FED Sandbox  (USDT proxy)
                 └── BIS mBridge  (cross-network SWAP)
 │
 ▼
Settlement Service  ← decides: settle immediately, or hold and wait for confirmation
 │
 ├── settled immediately → Ledger Service (DEBIT sender / CREDIT receiver)
 │
 └── pending → Ledger Service (DEBIT sender / CREDIT SETTLEMENT_CLEARING)
         │
         (admin confirms later)
         │
         ├── success → Ledger Service (DEBIT SETTLEMENT_CLEARING / CREDIT receiver)
         └── failure → Ledger Service (DEBIT SETTLEMENT_CLEARING / CREDIT sender — hold released)
 │
 ▼
Reconciliation Service  ← compares COMPLETED payments against rail.checkStatus()

Transaction Flow

Register → Authenticate → Create Wallet → Deposit Funds
      ↓
Initiate Payment
      ↓
KYC Check (transaction limit by level)
      ↓
Balance Check
      ↓
FX Conversion (if receiveCurrency differs)
      ↓
AML Check (daily limit + structuring detection)
      ↓
RailRouter → MockRail | StripeRail | CbdcRail
      ↓
Settlement Service decides:
      │
      ├── settledImmediately = true   → Ledger posts DEBIT/CREDIT now → payment COMPLETED
      │
      └── settledImmediately = false  → Ledger posts a HOLD (DEBIT sender / CREDIT clearing)
              → payment AWAITING_SETTLEMENT
              ↓
              (admin-triggered) confirm settlement → rail.checkStatus()
                    ├── SUCCESS → Ledger posts completion (DEBIT clearing / CREDIT receiver) → COMPLETED
                    └── FAILED  → Ledger posts release (DEBIT clearing / CREDIT sender) → FAILED
      ↓
(later, admin-triggered) Reconciliation → rail.checkStatus() → MATCHED / MISMATCH

Quick Start

Application runs on http://localhost:8080/api.

Swagger UI: http://localhost:8080/api/swagger-ui.html

A default admin user is seeded via V1__initial_schema.sql for admin-only endpoints (KYC upgrade, AML flagged list, reconciliation, settlement confirmation):

email:    admin@paymentbridge.local
password: Admin123!

Sandbox credentials only — replace or remove the seed before any real deployment.


API Reference

Auth

Register

curl -X POST http://localhost:8080/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "password": "Password123!"}'

Save token and userId from the response.

Login

curl -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "password": "Password123!"}'

Wallets

Create Wallet

curl -X POST "http://localhost:8080/api/v1/wallets?currency=USD" \
  -H "Authorization: Bearer <token>"

Supported currencies: USD, EUR, GBP, AED, TRY, USDC, USDT

Deposit Funds (sandbox only)

curl -X POST http://localhost:8080/api/v1/wallets/deposit \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"amount": 1000.00, "currency": "USD"}'

Get My Wallets

curl -X GET http://localhost:8080/api/v1/wallets \
  -H "Authorization: Bearer <token>"

Compliance

Get My KYC Level

curl -X GET http://localhost:8080/api/v1/compliance/kyc/me \
  -H "Authorization: Bearer <token>"

Upgrade KYC Level (admin only)

curl -X PUT http://localhost:8080/api/v1/compliance/kyc/{userId} \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '{"kycLevel": "BASIC", "notes": "verified manually"}'

Get My AML Flags

curl -X GET http://localhost:8080/api/v1/compliance/aml/me \
  -H "Authorization: Bearer <token>"

Get All Flagged Transactions (admin only)

curl -X GET http://localhost:8080/api/v1/compliance/aml/flagged \
  -H "Authorization: Bearer <admin-token>"

Payments

All requests require an X-Idempotency-Key header.

Standard Payment

curl -X POST http://localhost:8080/api/v1/payments \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: pay-001" \
  -d '{
    "receiverWalletAccountCode": "WALLET-{receiverUserId}-USD",
    "amount": 100.00,
    "currency": "USD",
    "railType": "MOCK",
    "description": "test payment"
  }'

Payment with FX Conversion

curl -X POST http://localhost:8080/api/v1/payments \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: pay-002" \
  -d '{
    "receiverWalletAccountCode": "WALLET-{receiverUserId}-EUR",
    "amount": 100.00,
    "currency": "USD",
    "receiveCurrency": "EUR",
    "railType": "MOCK",
    "description": "fx payment"
  }'

Response includes receiveAmount, receiveCurrency, fxRate.

Stripe Sandbox (requires VPN + STRIPE_API_KEY)

curl -X POST http://localhost:8080/api/v1/payments \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: pay-stripe-001" \
  -d '{
    "receiverWalletAccountCode": "WALLET-{receiverUserId}-USD",
    "amount": 50.00,
    "currency": "USD",
    "railType": "STRIPE",
    "description": "stripe payment"
  }'

Not part of the automated test script since it depends on external network access.

CBDC Transfer (USDC → USDC, settles immediately)

curl -X POST http://localhost:8080/api/v1/payments \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: cbdc-001" \
  -d '{
    "receiverWalletAccountCode": "WALLET-{receiverUserId}-USDC",
    "amount": 10.00,
    "currency": "USDC",
    "railType": "CBDC_SANDBOX",
    "description": "cbdc transfer"
  }'

TRANSFERECB_SANDBOXSTLD (immediate) → settledImmediately = true → payment COMPLETED right away.

CBDC Mint (USD → USDC, goes through Settlement)

curl -X POST http://localhost:8080/api/v1/payments \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: cbdc-mint-001" \
  -d '{
    "receiverWalletAccountCode": "WALLET-{receiverUserId}-USDC",
    "amount": 50.00,
    "currency": "USD",
    "receiveCurrency": "USDC",
    "railType": "CBDC_SANDBOX",
    "description": "mint usdc"
  }'

MINTECB_SANDBOXACCP (not STLD) → settledImmediately = false → payment AWAITING_SETTLEMENT, funds held in SETTLEMENT_CLEARING until an admin confirms.

CBDC Swap (USDC → USDT, cross-network, goes through Settlement)

curl -X POST http://localhost:8080/api/v1/payments \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: cbdc-swap-001" \
  -d '{
    "receiverWalletAccountCode": "WALLET-{receiverUserId}-USDT",
    "amount": 10.00,
    "currency": "USDC",
    "receiveCurrency": "USDT",
    "railType": "CBDC_SANDBOX",
    "description": "cross-network swap"
  }'

SWAPBIS_MBRIDGEACCPAWAITING_SETTLEMENT, same hold/confirm flow as MINT.

Simulate Failed Payment

curl -X POST http://localhost:8080/api/v1/payments \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: pay-fail-001" \
  -d '{
    "receiverWalletAccountCode": "WALLET-{receiverUserId}-USD",
    "amount": 50.00,
    "currency": "USD",
    "railType": "MOCK",
    "description": "fail this payment"
  }'

Duplicate Idempotency Key (reuse a key already used above → 409 CONFLICT)

curl -X POST http://localhost:8080/api/v1/payments \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: pay-001" \
  -d '{
    "receiverWalletAccountCode": "WALLET-{receiverUserId}-USD",
    "amount": 100.00,
    "currency": "USD",
    "railType": "MOCK"
  }'

Get Payment by ID

curl -X GET http://localhost:8080/api/v1/payments/{paymentId} \
  -H "Authorization: Bearer <token>"

Get My Payments

curl -X GET http://localhost:8080/api/v1/payments \
  -H "Authorization: Bearer <token>"

Ledger

Get Ledger for a Payment

curl -X GET http://localhost:8080/api/v1/ledger/payments/{paymentId} \
  -H "Authorization: Bearer <token>"

Returns two entries for an immediately-settled payment (DEBIT + CREDIT), or two entries for a held payment that hasn't confirmed yet (DEBIT sender + CREDIT SYS-SETTLEMENT-CLEARING) — with two more appearing once an admin confirms it.

Get Account History

curl -X GET "http://localhost:8080/api/v1/ledger/accounts/WALLET-{userId}-USD" \
  -H "Authorization: Bearer <token>"

The same endpoint also works for system accounts, e.g. SYS-SETTLEMENT-CLEARING, SYS-FEE, SYS-SUSPENSE.


CBDC Sandbox (no auth required)

List Available Networks

curl -X GET http://localhost:8080/api/cbdc-sandbox/networks

Query Transaction Status

curl -X GET http://localhost:8080/api/cbdc-sandbox/status/{txId}

Reconciliation (admin only)

Compares every COMPLETED payment against the real status reported by its rail. Payments still AWAITING_SETTLEMENT are correctly excluded — they haven't reached finality yet, so there's nothing to reconcile.

Trigger a Reconciliation Run

curl -X POST http://localhost:8080/api/v1/reconciliation/runs \
  -H "Authorization: Bearer <admin-token>"

List All Runs

curl -X GET http://localhost:8080/api/v1/reconciliation/runs \
  -H "Authorization: Bearer <admin-token>"

Get a Specific Run

curl -X GET http://localhost:8080/api/v1/reconciliation/runs/{runId} \
  -H "Authorization: Bearer <admin-token>"

Get Results (optionally mismatches only)

curl -X GET "http://localhost:8080/api/v1/reconciliation/runs/{runId}/results?mismatchesOnly=true" \
  -H "Authorization: Bearer <admin-token>"

Settlement (admin only)

Handles payments that a rail accepted but hasn't finalized yet (AWAITING_SETTLEMENT). Every hold, completion, and release is a balanced double-entry pair against the internal SETTLEMENT_CLEARING account — never a single unpaired entry.

List Pending Settlements

curl -X GET http://localhost:8080/api/v1/settlements/pending \
  -H "Authorization: Bearer <admin-token>"

Get a Settlement Record by ID

curl -X GET http://localhost:8080/api/v1/settlements/{settlementId} \
  -H "Authorization: Bearer <admin-token>"

Get the Settlement Record for a Payment

curl -X GET http://localhost:8080/api/v1/settlements/payment/{paymentId} \
  -H "Authorization: Bearer <admin-token>"

Confirm a Pending Settlement

curl -X POST http://localhost:8080/api/v1/settlements/{settlementId}/confirm \
  -H "Authorization: Bearer <admin-token>"

Re-checks the rail via checkStatus(). If the rail now reports success, the hold is released to the receiver and the payment becomes COMPLETED. If the rail reports failure, the hold is released back to the sender, the payment becomes FAILED, and an AML flag is raised automatically.


Automated Test

chmod +x test.sh
./test.sh

The script exercises every endpoint documented above — except the Stripe payment, which needs external network access — across the full lifecycle: register sender and receiver, admin login, wallet creation, KYC check and admin upgrade, deposits, a standard payment, an FX payment, get-payment-by-id, get-my-payments, balance check, ledger and account-history lookups, a simulated failure, idempotency rejection, AML flags (self and admin-wide), a CBDC transfer (immediate settlement), a CBDC mint and a CBDC swap (both going through the settlement hold/confirm flow), CBDC network listing and transaction-status lookup, the full reconciliation cycle, and the full settlement cycle (list pending, get by id, get by payment id, confirm).


Configuration

FX Provider

app:
  fx:
    provider: mock                # default — no API key needed
    # provider: exchangeratesapi  # real rates from exchangeratesapi.io
    api-key: your_key_here

Mock rates (USD base):

Currency Rate
EUR 0.91
GBP 0.79
AED 3.67
TRY 32.50
USDT 1.00
USDC 1.00

Reference Tables

KYC Levels

Level Max Transaction Daily Limit
UNVERIFIED 100 USD 500 USD
BASIC 5,000 USD 20,000 USD
FULL 1,000,000 USD 1,000,000 USD

AML Rules

Rule Description
Daily Limit Total daily payments cannot exceed KYC daily limit
Structuring Round amounts ≥ 5,000 are flagged for review
Reconciliation Mismatch A STATUS_MISMATCH found during reconciliation is auto-flagged
Settlement Rejection A settlement rejected by the rail on confirm is auto-flagged

CBDC Bridge Operations

From To Operation Network Status Settles Via
USD/EUR USDC MINT ECB Sandbox ACCP Settlement hold + confirm
USD/EUR USDT MINT FED Sandbox ACCP Settlement hold + confirm
USDC USD/EUR REDEEM ECB Sandbox ACCP Settlement hold + confirm
USDT USD/EUR REDEEM FED Sandbox ACCP Settlement hold + confirm
USDC USDC TRANSFER ECB Sandbox STLD (immediate) Immediate
USDT USDT TRANSFER FED Sandbox STLD (immediate) Immediate
USDC USDT SWAP BIS mBridge ACCP Settlement hold + confirm
USDT USDC SWAP BIS mBridge ACCP Settlement hold + confirm

ISO 20022 Status Codes

Code Meaning
PDNG Pending network confirmation
ACCP Accepted by network
STLD Settled — final, irreversible
RJCT Rejected (with reason code)

Reconciliation Result Types

Type Meaning
MATCHED Local and rail status/amount agree
STATUS_MISMATCH Status differs between local and rail — auto-flagged for AML
AMOUNT_MISMATCH Status matches but amount differs
ORPHANED_LOCAL Rail does not recognize the transaction
ORPHANED_REMOTE Rail has a transaction we never recorded (documented, not triggerable in sandbox)
UNVERIFIABLE Rail was unreachable or returned no data

Settlement Statuses

Status Meaning
PENDING Rail accepted the payment (ACCP) but hasn't settled it (STLD) yet
SETTLED Admin confirmed and the rail reports success — funds released to receiver
FAILED Admin confirmed and the rail reports failure — hold released back to sender

System Ledger Accounts

Account Purpose
SYS-FEE Fee collection
SYS-SUSPENSE General suspense account
SYS-SETTLEMENT-CLEARING Temporary holding account while a payment awaits settlement confirmation

Project Structure

payment-bridge-core
├── config
├── security
├── common
│   └── enums (Currency, KycLevel, AmlStatus, CbdcNetwork, CbdcTxStatus, CbdcOperationType,
│              ReconciliationResultType, SettlementStatus, SystemLedgerAccount, UserRole)
├── exception
├── user
├── payment
├── wallet
├── ledger
├── compliance
│   ├── entity (KycProfile, AmlFlag)
│   ├── service (KycService, AmlService)
│   └── controller (ComplianceController)
├── rails
│   ├── MockRail
│   ├── stripe (StripeRail)
│   └── cbdc
│       ├── CbdcRail
│       ├── CbdcBridgeResolver
│       ├── CbdcProperties
│       ├── dto (CbdcSettlementRequest, CbdcSettlementResponse)
│       └── sandbox (CbdcSandboxServer)
├── fx
│   ├── provider (MockFxRateProvider, ExchangeRatesApiFxProvider)
│   └── service (FxService)
├── reconciliation
│   ├── entity (ReconciliationRun, ReconciliationResult)
│   ├── dto (ReconciliationRunResponse, ReconciliationResultResponse)
│   ├── repository
│   ├── service (ReconciliationService)
│   └── controller (ReconciliationController)
└── settlement
    ├── entity (SettlementRecord)
    ├── dto (SettlementRecordResponse)
    ├── repository (SettlementRecordRepository)
    ├── service (SettlementService)
    └── controller (SettlementController)

Features

Feature Status Notes
Payment Core lifecycle, idempotency, status tracking
Auth & User JWT, register, login, role-based access
Wallet multi-currency, balance from ledger
FX Engine mock + real provider, pluggable
StripeRail Stripe sandbox integration
KYC / AML level-based limits, daily limit, flagging
CBDC Layer ISO 20022, 4 operations, 4 networks
Reconciliation rail/ledger comparison, 6 result types, admin-only
Settlement hold/confirm via a clearing account, admin-only, double-entry throughout

Design Principles

  • balance derived from ledger — never stored separately
  • every payment rail behind a common interface
  • every financial event recorded in the ledger
  • compliance checks before every payment
  • reconciliation as a first-class citizen, not an afterthought
  • rail acceptance and payment finality are two different things
  • ledger entries are immutable — corrections are new entries, never edits
  • complexity added only when justified

Disclaimer

This project is for educational and architectural exploration only.

Not designed for production use, regulatory compliance, or real-money handling.

About

Payment Bridge Core — a Spring Boot fintech prototype exploring modern payment infrastructure: JWT auth, multi-currency wallets, double-entry ledger, FX conversion, KYC/AML compliance, pluggable rails (Mock + Stripe sandbox), and a CBDC bridge with ISO 20022 messaging. Built with Java 21, H2, Flyway.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Contributors

Languages