Skip to content

haggish/painkiller

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Painkiller

A Rust implementation of a SEPA Instant Credit Transfer (SCT Inst) payment initiation processor, compliant with EPC121-16 (2025 Version 1.0).

It is currently assumed that the client uploads the pain.001.001.09 XML file directly to the processor, and some external system handles the Kafka messages resulting from the parsing of the XML, and sends Kafka messages reflecting the payment validation result back. The application then compiles the pain.002.001.10 report and saves it to the database for the client to fetch.

Architecture

pain.001.001.09 XML
       │
       ▼
┌──────────────────────────────────────────┐
│  Stage 1: Parse + Validate + Route       │
│                                          │
│  1. Full parse into memory (~30 MB/100K) │
│  2. EPC business rule validation         │
│  3. Pluggable content-based routing      │
│  4. Emit valid transactions to Kafka     │
│  5. Build + store pain.002.001.10        │
└───────────────┬──────────────────────────┘
                │
        ┌───────┴────────┐
        │ internal       │ external
        ▼                ▼
┌──────────────┐  ┌─────────────────┐
│ Kafka events │  │ Forward raw XML │
└──────┬───────┘  └─────────────────┘
       │
       ▼
┌──────────────────────────────────────────┐
│  Stage 2: Processing Workers             │
│  1. Deduplication (Redis)                │
│  2. Balance reservation (Redis)          │
│  3. AML/Sanctions screening (REST)       │
│  4. Emit to clearing topic               │
└───────────────┬──────────────────────────┘
                │
       ┌────────┴────────┐
       ▼                 ▼
  CSM Adapter      Clearing Results
  (pacs.008)       (from Kafka)
                         │
                         ▼
┌──────────────────────────────────────────┐
│  Stage 3: Aggregation                    │
│  Collect clearing verdicts per exec date │
│  Build final pain.002 (ACCP/RJCT)       │
│  Timeout: end-of-day + 25s (bank TZ)    │
│  Store in PostgreSQL                     │
└──────────────────────────────────────────┘

Key Design Decisions

  • No stream parsing: Full payment initiation held in memory. Rust's efficiency (~300 bytes/transaction) makes 100K transactions cost only ~30 MB.
  • Validate before emit: Nothing reaches Kafka until the entire PmtInf block passes validation. No retroactive invalidation needed.
  • EPC-first: Business rules from EPC121-16 are the primary validation layer.
  • Pluggable routing: Content-based routing via a chain of async plugins.

Modules

Module Purpose
model Data model + XML parser for pain.001.001.09
validation EPC business rules engine + structured error reporting
routing Pluggable content-based routing framework
pain002 pain.002.001.10 XML builder (reject/confirm)
kafka Event model for payment transactions + clearing results
processing Stage 2 worker: dedup, balance, AML screening
screening AML/Sanctions REST client trait + mock
deduplication Redis-based MsgId/EndToEndId uniqueness
persistence pain.002 report storage (in-memory store + PostgreSQL repository)
held Future-dated payment holding (in-memory store)
held_executor Background executor for held payments at execution time
api HTTP API layer (axum)
pipeline Stage 1 orchestrator tying everything together
clearing Clearing aggregation storage keyed by (msg_id, execution_date) — InMemory + PostgreSQL
clearing_aggregator Pure business logic: classify clearing results, build final pain.002
clearing_consumer Kafka consumer for payment.clearing.results topic
clearing_sweep Background timeout sweep — deadline is end-of-day on execution date + buffer (bank timezone)

Building

cargo build --release

Running

HTTP server mode (default)

cargo run --release

# With custom configuration
OWN_BIC=NDEAFIHH OWN_NAME="ACME Bank" BIND_ADDR=0.0.0.0:8080 \
  cargo run --release

CLI mode (one-shot file processing)

cargo run --release -- path/to/pain001.xml

REST API

POST /api/v1/payment-initiation

Submit a pain.001.001.09 XML document for processing.

Request:

  • Content-Type: application/xml
  • Body: pain.001.001.09 XML

Responses:

Status Meaning Body
200 OK Processed immediately (accepted or rejected) pain.002.001.10 XML
202 Accepted Future-dated instruction held for later execution pain.002.001.10 XML (acceptance)
400 Bad Request Malformed XML or parse failure Error message (text)

Future-dated responses include an X-Execution-Date header with the scheduled execution timestamp (RFC 3339).

The pain.002 XML body contains the validation result in both 200 and 202 cases. Rejection details (reason codes, per-transaction status) are embedded in the XML per EPC121-16.

Example:

curl -X POST http://localhost:8080/api/v1/payment-initiation \
  -H "Content-Type: application/xml" \
  -d @tests/fixtures/valid_sct_inst.xml

GET /api/v1/payment-initiation/{msg_id}

Query the status of a payment initiation (primarily for held future-dated payments).

Response (200 OK):

{
  "msg_id": "MSG-2025-001",
  "status": "Pending",
  "execute_at": "2025-04-10T00:00:00+00:00",
  "tx_count": 3,
  "received_at": "2025-04-02T12:00:00+00:00",
  "updated_at": "2025-04-02T12:00:00+00:00"
}

Possible status values: Pending, Executing, Executed, Cancelled, Failed.

Returns 404 Not Found if no held payment exists for the given MsgId.

DELETE /api/v1/payment-initiation/{msg_id}

Cancel a future-dated payment initiation before its execution date. Per EPC004-16 §4.2.1, the Originator must be allowed to cancel held instructions.

Responses:

Status Meaning
200 OK Successfully cancelled
404 Not Found No payment initiation found for this MsgId
409 Conflict Payment cannot be cancelled (already executing, executed, or failed)

GET /api/v1/payment-initiation/{msg_id}/reports

Retrieve all pain.002 reports for a given original MsgId. Returns reports ordered by creation time (oldest first). Multiple reports can exist when a payment goes through several stages (e.g., initial validation, then clearing confirmation or timeout).

Response (200 OK):

[
  {
    "report_id": "a1b2c3d4-...",
    "report_msg_id": "RPT-...",
    "report_type": "ValidationReject",
    "processing_source": "Internal",
    "report_date": "2025-04-02",
    "created_at": "2025-04-02T12:00:00+00:00",
    "report_xml": "<?xml version=\"1.0\"?>..."
  }
]

Possible report_type values: ValidationReject, PositiveConfirmation, NegativeConfirmation, ExternalResult.

Returns 404 Not Found if no reports exist for the given MsgId.

GET /health

Liveness probe. Returns 200 OK with body ok.

Middleware

  • Body limit: 50 MB (a 100K-transaction pain.001 is ~30 MB)
  • Compression: gzip response compression
  • Tracing: HTTP request/response logging via tower-http

Testing

# Run all tests
cargo test

# Run with output
cargo test -- --nocapture

# Run specific test
cargo test test_validate_valid_sct_inst_passes

Test Fixtures

File Description
valid_sct_inst.xml 3 valid SCT Inst transactions (domestic FI, cross-border DE, NL)
invalid_mixed_errors.xml 1 valid + 2 invalid transactions (missing name, bad IBAN, bad BIC)
group_level_reject.xml NbOfTxs mismatch — triggers group-level rejection

EPC Business Rules Implemented

Group Header

  • MsgId: 1-35 chars, valid reference format
  • NbOfTxs: must match actual transaction count
  • CtrlSum: mandatory, must match sum of instructed amounts, max 2 decimals
  • InitgPty: Name max 70 chars, identification required

Payment Information

  • PmtMtd: only TRF
  • PmtTpInf: mutual exclusion between PmtInf and transaction levels
  • SvcLvl/Cd: only SEPA
  • LclInstrm/Cd: only INST
  • NbOfTxs + CtrlSum at PmtInf level validated
  • Debtor Name: mandatory, max 70 chars
  • DbtrAcct: IBAN only
  • DbtrAgt: BICFI or NOTPROVIDED
  • ChrgBr: only SLEV
  • Address: structured/hybrid/unstructured rules, max 2 AddressLines

Transaction

  • EndToEndId: 1-35 chars, valid reference format (no leading/trailing /, no //)
  • Amount: EUR only, 0.01–999,999,999.99
  • ChrgBr: only SLEV
  • CdtrAgt: BICFI only
  • Creditor Name: mandatory, max 70 chars
  • CdtrAcct: IBAN only
  • RemittanceInfo: Unstructured (1 occurrence, 140 chars) or Structured (1 occurrence, 140 chars total)
  • Structured ref type: only SCOR
  • Duplicate EndToEndId within PmtInf detected

pain.002 Reason Codes

All EPC121-16 section 2.2.2 codes implemented: AC01 AC04 AC06 AG01 AG02 AM02 AM05 BE04 FF01 MD07 MS02 MS03 RC01 RR01 RR02 RR03 RR04 TM01 DNOR CNOR AB05 AB06 AB07 AB08 AB09 AB10 AG10 AG11 AM23

Future Implementation: EPC004-16 Rulebook Gaps

The following rules from the EPC004-16 (2025 SCT Inst Rulebook v1.0) are not yet implemented and should be addressed in future iterations.

Timing rules (§4.2.3)

  • 10-second timeout (CT-01.11R): If no confirmation from the Beneficiary PSP within 10s of Time Stamp, the Originator PSP must immediately lift the customer reservation but maintain CSM-level settlement certainty until a formal confirmation arrives.
  • Belated confirmation handling (CT-01.12R/13R): Must handle positive or negative confirmations that arrive after the 10s timeout and immediately inform the Originator.
  • Time Stamp precision (AT-T056): Must include at least milliseconds.

Recall handling (§4.3.2.2, DS-05/DS-06)

  • 3 valid reasons: duplicate sending, technical error, fraud.
  • Time windows: 10 Banking Business Days (duplicate/technical), 13 months (fraud).
  • Only 1 recall per transaction; must use same routing path as the original.
  • No recall API or DS-05/DS-06 message generation exists yet.

Request for Recall by Originator (§4.3.2.3, DS-08/DS-09)

  • For other reasons (wrong IBAN, wrong amount, originator request).
  • Must warn originator that recovery is not guaranteed.
  • Only 1 RFRO per transaction; same routing path as the original.

Verification of Payee (§5.7(4)) — new in 2025

  • When required by applicable law, must perform VOP check per the EPC VOP Scheme Rulebook before processing.

Amount limits (§2.5)

  • No scheme-level maximum amount anymore (removed per amended SEPA Regulation).
  • PSP may apply its own per-transaction or aggregate amount limits — only transaction count (MAX_TRANSACTIONS) is enforced today.

Data integrity

  • AT-T014 default: Originator reference must default to "Not provided" when absent, not null.
  • Remittance data (§2.7): Must be forwarded in full and without alteration.
  • Address format deadline: After 22 November 2026 at 03:30 CET, only structured/hybrid address formats are allowed (no more unstructured).

Investigation procedure (PR-04, §4.4)

  • Optional DS-07 status inquiry when no confirmation within 9 seconds.
  • The Originator PSP cannot consider the transaction failed until it receives a formal confirmation message.

24/7/365 availability (§5.7(5))

  • Must process SCT Inst instructions 24 hours a day on all calendar days, including business continuity arrangements.

Structured Creditor Reference

  • Recommended (not mandatory) to validate ISO 11649 format at point of capture.

Infrastructure Dependencies

Component Purpose Required
Apache Kafka Event backbone Yes
Redis Deduplication + balance reservation Yes
PostgreSQL pain.002 report storage Yes
AML REST service Sanctions screening Yes
SFTP External payment forwarding For routing plugins

Environment Variables

Variable Default Description
OWN_BIC NDEAFIHH This PSP's BIC code
OWN_NAME ACME Bank This PSP's name
MAX_TRANSACTIONS 100000 Max transactions per payment initiation
BIND_ADDR 0.0.0.0:8080 HTTP server listen address
KAFKA_BROKERS localhost:9092 Kafka bootstrap servers for Stage 3 consumer
BANK_TIMEZONE Europe/Helsinki IANA timezone for computing clearing deadlines
CLEARING_EOD_BUFFER_SECS 25 Seconds after midnight before timeout sweep fires
HELD_POLL_INTERVAL_SECS 10 How often to check for held payments ready to run
RUST_LOG - Log level filter (e.g. sct_inst_processor=debug)

About

Instant payment processing for fun. Speaks volumes what I consider fun

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages