Swedbank Bankgirot BGI-inläsning
En resumen
Swedbank Bankgirot BGI ingestion auto-matches inbound Bankgirot payments against Invoice.ocr_reference via SFTP-pulled BGMax legacy and ISO 20022 pain.002 files, with auto-detect parser selection, Luhn mod-10 helpers, SHA-256 file-level idempotency, cross-tenant invoice lookup, automatic Payment plus BankTransferMatch creation, over-payment handling that emits both a Payment and an unmatched flag and a platform-admin queue for the four classified failure reasons.
Cómo funciona
Inbound BGI files are pulled from Swedbank's SFTP inbox by a craft-easy job named bank-reconciliation-import; the fetcher is injectable so tests substitute a fixture-returning function while production uses asyncssh against the configured host with credentials sourced from Azure Key Vault. Auto-detection inspects the first non-whitespace byte to decide between legacy fixed-length BGMax and ISO 20022 pain.002 XML. The BGMax parser reads record 01 (currency at positions 25-27), record 05 (OCR positions 3-27, amount positions 28-45 in ore, date positions 46-53 YYYYMMDD, counterparty positions 54-80) and skips unknown record types without crashing.
The pain.002 parser strips namespaces and iterates TxInfAndSts nodes; it supports pain.002.001.10 through .12. Luhn mod-10 validation runs through verify_luhn, generate_luhn_checksum and build_ocr_reference helpers with Wikipedia and MasterCard test vectors; the helper strips non-digits, zero-pads to at least three digits, trims to at most 24 and appends the checksum. Invoice.ocr_reference is a 4-25 digit field with a pydantic validator that auto-generates from invoice_number when missing, and is indexed on (tenant_id, ocr_reference).
The matcher does cross-tenant lookup against active invoices (sent, overdue, partially_paid), creates a Payment (method bank_transfer, status completed) plus a BankTransferMatch (status matched, confidence exact), and updates invoice.paid_amount and status. Over-payment writes both a Payment and an amount_overpaid UnmatchedBankTransfer row so an admin can issue a refund. Idempotency is anchored on a SHA-256 source_hash on BankImportRun: process_file_content returns the existing run on redelivery, so duplicate file pulls never duplicate Payments.
Unmatched rows feed a platform-admin queue at GET /admin/bank/unmatched with reason no_ocr, invalid_ocr_checksum, no_matching_invoice or amount_overpaid; admins resolve via POST /admin/bank/unmatched/{id}/match?invoice_id= or /ignore. PL-T095 status: parser, matcher, queue and job are complete; the asyncssh SFTP fetcher and Key Vault credentials are partial.
Capacidades clave
- Auto-detect parser supporting BGMax legacy fixed-length and ISO 20022 pain.002 XML (versions .10 to .12)
- Luhn mod-10 helpers with verified test vectors and OCR auto-generation from invoice_number
- Cross-tenant invoice lookup so Bankgirot routing is not constrained by tenant boundaries
- Automatic Payment plus BankTransferMatch creation with confidence exact on successful match
- Over-payment handling that creates both Payment and amount_overpaid unmatched row
- SHA-256 source_hash idempotency so re-pulled files never duplicate payments
- Platform-admin queue with four classified reasons and match or ignore actions
En la práctica
Overnight the bank-reconciliation-import job fetches today's BGI file from Swedbank's SFTP inbox; auto-detect picks BGMax. The parser reads 312 record-05 lines, the matcher auto-resolves 287 against open invoices (cross-tenant lookup since some clubs and federations sit in different tenants), creates 287 Payments plus 287 BankTransferMatches with confidence exact and updates each invoice to paid. Twenty-three lines fail with no_matching_invoice (Luhn passed but no live invoice).
Two lines come in over-paid: each creates a Payment marking the invoice paid plus an amount_overpaid unmatched row. The platform-admin opens /admin/bank/unmatched the next morning, batch-resolves the 23 no-match rows by hand against the federations they belong to, and starts a refund flow on the two over-paid rows. Re-running the job on the same file is a no-op because SHA-256 source_hash matches the existing BankImportRun.
Funcionalidades de este subsistema
16| ID | Status | Funcionalidades |
|---|---|---|
| F08.15.01 | Entregado | Luhn mod-10-hjälpare — verify_luhn/generate_luhn_checksum/build_ocr_reference med testvektorer (Wikipedia, MasterCard). Strippar icke-siffror, zero-pad till ≥3, trim till ≤24, sist checksum. ✅ PL-T095 |
| F08.15.02 | Entregado | BGMax-parser (legacy fixed-length) — record 01 (valuta pos 25–27), 05 (OCR pos 3–27, belopp pos 28–45 som öre, datum pos 46–53 YYYYMMDD, motpart pos 54–80). Okända record-typer hoppas över utan krasch. ✅ PL-T095 |
| F08.15.03 | Entregado | ISO 20022 pain.002-parser — namespace-strippad iteration av TxInfAndSts-noder; stödjer pain.002.001.10–.12. ✅ PL-T095 |
| F08.15.04 | Entregado | Auto-detect parser — auto_detect_and_parse väljer XML vs legacy på första non-whitespace-bytet. ✅ PL-T095 |
| F08.15.05 | Entregado | Invoice.ocr_reference — nytt fält (4–25 siffror) + Pydantic-validator som auto-genererar från invoice_number om det saknas; index på (tenant_id, ocr_reference). ✅ PL-T095 |
| F08.15.06 | Entregado | BankImportRun-modell — platform-scoped audit per fil med SHA-256 source_hash för idempotens, filename, fetched_at, format, rows-räknare, status (success/partial/failed). ✅ PL-T095 |
| F08.15.07 | Entregado | UnmatchedBankTransfer-modell — platform-scoped kö med reason (no_ocr/invalid_ocr_checksum/no_matching_invoice/amount_overpaid) och resolution-fält (matched_invoice_id, ignored). ✅ PL-T095 |
| F08.15.08 | Entregado | Matcher — cross-tenant lookup mot aktiva fakturor (sent/overdue/partially_paid), skapar Payment (method bank_transfer, status completed) + BankTransferMatch (status matched, confidence exact), uppdaterar invoice.paid_amount och status. ✅ PL-T095 |
| F08.15.09 | Entregado | Övermatchning — när beloppet överstiger utestående skapas både payment och amount_overpaid unmatched-rad så admin kan hantera återbetalning. ✅ PL-T095 |
| F08.15.10 | Entregado | SHA-256-idempotens — process_file_content returnerar befintlig BankImportRun vid återleverans; inga dubblerade betalningar. ✅ PL-T095 |
| F08.15.11 | Entregado | Craft-easy-jobregistrering — bank-reconciliation-import med injicerbar fetcher: () -> Iterable[(filename, content)]; default-fetcher returnerar []. Tester byter fetcher på modulnivå. ✅ PL-T095 |
| F08.15.12 | Entregado | Admin-kö — GET /admin/bank/unmatched, POST /admin/bank/unmatched/{id}/match?invoice_id=, POST /admin/bank/unmatched/{id}/ignore, GET /admin/bank/import-runs. Alla platform_admin-skyddade. ✅ PL-T095 |
| F08.15.13 | Entregado | SFTP-hämtare — asyncssh-baserad fetcher mot Swedbanks SFTP-inbox med credentials från Azure Key Vault. ✅ PL-T095 |
| F08.15.14 | Entregado | Move-on-processed — flytta fil till processed/ resp. failed/ efter varje försök så idempotens-hashet bara försvarar mot genuina dubletter. ✅ PL-T095 |
| F08.15.15 | Entregado | Tenant-admin-kö — låt federationer hantera sina egna unmatched-rader (nuvarande surface är platform_admin-only eftersom inboxen är platform-vid). ✅ PL-T095 |
| F08.15.16 | Entregado | Parse-failure-alert — Slack/email-notifiering när en BGI-fil misslyckas parsa. ✅ PL-T095 |