← all projects

mock-bank

ACME sends mock-bank a file of four payments, and the bank's status report rejects two, giving a reason for each. The bank's own queue says a debit notification is due on the settlement day, which the film shows as a promise. The capture then advances the bank's clock twice: the other two payments are booked and reported in the debit notification, which keeps the promise, and then in a statement. The rejected two appear in neither.
a payment file, half of it rejected drawn by mock-films from a real run

A mock bank that speaks ISO 20022 and NACHA, over HTTP and over a drop folder, so you can test a payment integration without a bank, a test IBAN, or six weeks of onboarding. It is not a ledger and not a payment provider: it holds four accounts, moves balances in minor units, and refuses by name anything it does not implement.

   you ──pain.001──▶  mock-bank
       ◀──pain.002──  2 accepted, 2 rejected (AC04, RC01)
       ◀──camt.054──  debited, on the settlement date
       ◀──camt.053──  the day's statement, and it reconciles
       ◀──pacs.004──  one came back, 3 business days later

Or the same four payments as an ACH file, for an account that banks in US formats. The choice is per account, so one file can produce both.

   you ──NACHA────▶  mock-bank
       ◀──text─────  ENTRY 999999990000002 INV-2026-0102 3400.50
                     REJECTED R02 the creditor account … is closed
       ◀──NACHA────  a return file: one 21 entry and addenda 99 per return
       ◀──BAI2─────  16,447,125000,Z,INV-2026-0101,…,Globex Supplies B.V./
                     16,447,1500000,Z,INV-2026-0104,…,Umbrella Logistics/
  • Zero dependencies. Python 3.8+, the standard library and SQLite, like its siblings. No transport needing cryptography and no screening - each refused by name, with an answer saying what is supported.
  • Two formats, and the mock does not care which door. A pain.001 or a 94-column NACHA file, over POST /payments or dropped in a folder, goes through the same decisions. An account whose format is nacha gets a plain-text acknowledgement where an ISO 20022 account gets a pain.002, an R-coded return file where it gets a pacs.004, and a BAI2 statement where it gets a camt.053. A NACHA file is held to the format: every record 94 bytes of printable ASCII and the file whole blocks of ten, or it is refused FF01 naming the line and the column.
  • Money can arrive, not just leave. POST /_mock/credits lands a customer's payment in an account on its value date, or the next business day for a weekend, a holiday or a missed cutoff. It is reported like everything else: a camt.054 as it books and an entry on the day's camt.053, which still reconciles - the payer as Dbtr, the note in RmtInf/Ustrd, and the invoice reference in Strd apart from it, because a bank that reformats remittance is the reason cash application is hard. A short payment, one that names no invoice, one that settles several, and a reference split across lines are each one request away.
  • Direct debits, as well as payments. A pain.008, or a NACHA file of debit entries, collects from customers: the bank answers it as it answers a payment file, credits the account on the settlement date, and reports the money like any other that arrives. The customer's bank can refuse a collection before it settles or send it back after, as a pacs.004 or an R-coded return file - MD01 or R10 for a mandate the customer says they never gave. A customer whose account is at the same bank decides for itself: closed, or without the money, and the collection is rejected.
  • The failures, not just the happy path. An account that rejects for AC04, one with no money, a bank identifier that is not the bank, a duplicate file, a statement with one payment missing, a payment that settles and then comes back days later, and an account that closes between the day a payment is accepted and the day it settles, so the payment is rejected rather than booked. The three behaviours the bank rejects for have NACHA twins: AM04 is R01, AC04 is R02, RC01 is R03.
  • Nothing sleeps. Settlement dates, the 15:00 cutoff, weekends and holidays run on a bank clock that POST /_mock/advance moves. A three-day return is a line in a test, not a three-day wait. A mock left running over midnight issues that day's statement itself, the next time anything asks. And you need not wait for the day to end either: POST /_mock/accounts/<id>/report writes a camt.052 for the account as it stands now, which a reconciler can compare against the day's statement to find the entry a statement-gap leaves out.
  • Two doors, one pipeline. Most bank connections are two directories on an SFTP host rather than an endpoint, so a file dropped in --drop-dir goes through the same decisions as POST /payments and lands in processed/ or failed/ with the answer written beside it.
  • Its own output is checked against somebody else's files. Every ISO 20022 message is validated against schemas fetched from pinned commits, and the NACHA and BAI2 readers are held to files from moov-io - because a reader and a writer built from one declaration agree with each other even when both are wrong. That is how the mock found it was counting a returned debit as a credit, and that its BAI2 reader could not open a real bank's file.
  • Worked end to end with the other mocks. payment_run does what SAP's F110 does: select the open items that are due from mock-sap, pay them in one file, and post each statement back so SAP clears what was paid and reopens what came back. It lives in mock-acme now, with seventy-six tests run in its CI against the released mocks - twice, once over ISO 20022 and once over ACH, because the same run has to survive both.
ACME pays one invoice of EUR 1250.00 through mock-bank; it is accepted, and the bank's own queue says a debit notification is due on Friday, which the film shows as a promise. On Friday the payment is booked and debited, and the queue then holds two more promises, a camt.054 and a pacs.004 due the next Wednesday. The capture script advances the bank's clock a day at a time: Saturday and Sunday are not business days, and no message about the payment arrives on Monday or Tuesday. On Wednesday the pacs.004 returns the payment with reason AC04 and the camt.054 credits the money back, each keeping its promise. The script had set ACME's account to have payments returned three business days later with AC04, one of the mock's behaviours.
a payment that comes back drawn by mock-films from a real run
ACME sends mock-bank one file of two payments, one due on the day it is sent and one due the following Tuesday, and both are accepted. The first books and is debited, and the bank's queue holds a camt.054 promised for Tuesday. The capture script then closes ACME's account and moves the clock, and the queue no longer holds that camt.054; in its place it holds a pain.002, promised for the same Tuesday, for payments rejected at settlement. The film shows the camt.054 promise never kept; its entry is for an account and a day, so any other payment for ACME that day would share it. On Tuesday the pain.002 arrives and rejects the second payment with AC04, the debtor account closed before the payment settled.
a payment rejected on its due day drawn by mock-films from a real run
ACME, set up to bank in US formats and dollars, sends mock-bank a NACHA file for the same four invoices and amounts as the ISO film, in USD. The bank's own acknowledgement, whose shape is the mock's choice and not NACHA's, rejects two with R02 and R03, and the queue shows two promises, a camt.054 and a return file. The capture script advances the bank's clock: the return file sends the two rejected payments back with the same codes, and the camt.054 books the other two, each keeping its promise. The acknowledgement is shown again, and Wednesday's BAI2 statement closes at 125000.00 and Thursday's at 108750.00 with two entries, the rejected two on neither.
the same invoices, in usd, as nacha drawn by mock-films from a real run
ACME sends mock-bank the bank's own sample file of four direct debits to collect in euro, and the bank's status report rejects two with a reason for each: one debtor, a seeded account, has 12.50 against the 340.00 asked, and the other's account is closed. The bank's own queue says a credit notification is due on the settlement day, which the film shows as a promise. The capture then advances the bank's clock twice: the other two collections, one from an account the bank does not hold, which it treats as being at another bank, and one from Eurodis, an account it does hold and has nothing against, are booked and credited to ACME in the credit notification, which keeps the promise, and then in a statement closing at 127750.50. The rejected two appear in neither.
four direct debits, half of them rejected drawn by mock-films from a real run
ACME collects 1500.50 euro by direct debit from Eurodis, whose account the script has set to send a collection back after the mock's default of three business days. The bank accepts it, and its queue says a credit notification is due on the settlement day, which the film shows as a promise. The clock moves to that day: the collection is booked, the credit notification keeps the promise, and the bank now owes two more messages, a return and a debit notification, both due the Tuesday after the weekend. The clock moves to that Tuesday and the bank keeps both: the return gives the reason AC04, with the dates it settled and was returned, and the debit takes the 1500.50 back out of ACME's account. A day later a statement closes at 125000.00, where the one before it closed at 126500.50.
a direct debit collected, credited, then sent back drawn by mock-films from a real run
pip install mock-bank
mock-bank --port 8090
curl -X POST --data-binary @pain001.xml http://127.0.0.1:8090/payments

Worked examples that use mock-bank: Procure to pay, Pay invoices and Payment run.