sepa 0.3.0

SEPA payment utilities: IBAN validation (ISO 13616 + country registry), BIC validation (ISO 9362), pain.001 SCT + SCT Inst XML builder (pain.001.003.03 / pain.001.001.09), pain.008 SDD CORE + B2B XML builder (pain.008.003.02), pain.002 status report parser, camt.053 statement parser, camt.054 notification types. Zero I/O. No async. No f64 in monetary arithmetic.
Documentation

sepa

Crates.io Docs.rs CI License MSRV

🏦 SEPA payment utilities for Rust. ⚑ Zero I/O. No async. πŸ”’ No f64 in monetary arithmetic.


✨ Features

Module What it provides
iban πŸ” IBAN validation β€” ISO 13616 mod-97 + 56-country length registry
bic πŸ” BIC/SWIFT validation β€” ISO 9362
creditor_id πŸ” SEPA Creditor Identifier β€” EPC AT-02
pain001 πŸ“€ pain.001 XML builder β€” SCT (003.03) + SCT Instant (001.09)
pain008 πŸ“€ pain.008 XML builder β€” SDD CORE + B2B
pain002 πŸ“₯ pain.002 XML parser β€” Payment Status Report (bank β†’ customer)
camt053 πŸ“₯ camt.053 XML parser β€” Bank-to-Customer Statement (end-of-day)
camt054 πŸ“₯ camt.054 typed entry β€” Bank-to-Customer Notification
ct_to_eur_str / ct_from_eur_str πŸ’Ά Integer-safe i64 ct ↔ "1234.56" round-trip

All commonly used types are re-exported at the crate root (sepa::SequenceType, sepa::DirectDebitScheme, …).


πŸš€ Quick start

[dependencies]
sepa = "0.3"

πŸ“€ Building payment files

use sepa::{
    validate_iban,
    Pain008Builder, DirectDebitEntry, SequenceType, DirectDebitScheme,
    Pain001Builder, CreditTransferEntry, LocalInstrument,
};

let creditor = validate_iban("DE89 3704 0044 0532 0130 00").unwrap();
assert_eq!(creditor.bban(), "370400440532013000");           // BBAN accessor
assert_eq!(creditor.to_string(), "DE89 3704 0044 0532 0130 00"); // grouped Display

// pain.008 β€” SEPA Core Direct Debit (Lastschrift)
let dd_xml = Pain008Builder::new("Stadtwerke GmbH", &creditor)
    .msg_id("DD-2026-07-001")
    .sequence_type(SequenceType::Rcur)
    .add_entry(
        DirectDebitEntry::new(
            "MND-001", "2024-06-01",
            "Max Mustermann",
            validate_iban("NL91ABNA0417164300").unwrap(),
            7_500,          // 75.00 EUR β€” integer cents, never f64
            "E2E-001",
        ).with_description("Abschlag Juli 2026"),
    )
    .build_xml();

// pain.008 β€” SEPA B2B Direct Debit
let _b2b = Pain008Builder::new("Stadtwerke GmbH", &creditor)
    .scheme(DirectDebitScheme::B2b)
    .add_entry(DirectDebitEntry::new(
        "MND-B2B-001", "2024-01-01", "Corporate AG",
        validate_iban("DE29100500005001065004").unwrap(), 50_000, "INV-001",
    ))
    .build_xml();

// pain.001 β€” SEPA Credit Transfer (Überweisung)
let ct_xml = Pain001Builder::new("Debtor GmbH", &creditor)
    .msg_id("CT-2026-07-001")
    .add_entry(
        CreditTransferEntry::new(
            "Supplier AG",
            validate_iban("NL91ABNA0417164300").unwrap(),
            12_000,         // 120.00 EUR
            "REFUND-001",
        ).with_description("Erstattung 2025"),
    )
    .build_xml();

// ⚑ pain.001 β€” SCT Instant (10-second settlement, pain.001.001.09 namespace)
let _inst = Pain001Builder::new("Debtor GmbH", &creditor)
    .local_instrument(LocalInstrument::Inst)
    .add_entry(CreditTransferEntry::new(
        "Recipient", creditor.clone(), 500, "INST-001",
    ))
    .build_xml();

πŸ’Ύ Streaming large batches (no intermediate String)

use sepa::{Pain008Builder, validate_iban};
use std::io::BufWriter;
use std::fs::File;

let iban = validate_iban("DE89370400440532013000").unwrap();
let builder = Pain008Builder::new("Payroll GmbH", &iban)
    .msg_id("PAYROLL-2026-07");
// … .add_entry(…) Γ— 10 000 …

// Streams directly to file β€” zero intermediate String allocation
let file = File::create("payroll.xml").unwrap();
builder.write_xml_to_io(&mut BufWriter::new(file)).unwrap();

πŸ“₯ Parsing bank responses

pain.002 β€” Payment Status Report

use sepa::{parse_pain002, PaymentStatus};

// xml: &str received from bank via EBICS/FinTS
let doc = parse_pain002(xml).unwrap();

if doc.is_fully_accepted() {
    println!("βœ… Batch accepted ({})", doc.group_status.unwrap());
} else {
    for tx in doc.rejected_transactions() {
        println!("❌ {} β€” {:?}", tx.original_end_to_end_id, tx.reason_codes);
    }
}

camt.053 β€” End-of-Day Statement

use sepa::parse_camt053;

// xml: &str received from bank (camt.053.001.02 / .06 / .08 all accepted)
let doc = parse_camt053(xml).unwrap();
let stmt = &doc.statements[0];

println!("Account: {}", stmt.account_iban);
println!("Closing: {} ct", stmt.closing_balance().unwrap().signed_ct());

for entry in &stmt.entries {
    println!("{:+} ct  {}", entry.signed_ct(), entry.reference.as_deref().unwrap_or(""));
}

camt.054 β€” Intraday Notifications (json feature)

sepa = { version = "0.3", features = ["json"] }
use sepa::camt054::parse_simple_json;

let entry = parse_simple_json(&serde_json::json!({
    "iban":       "DE89370400440532013000",
    "amount_eur": "155.42",
    "reference":  "Invoice-001",
    "date":       "2026-07-10"
})).unwrap();
assert_eq!(entry.amount_ct, 15_542);
assert_eq!(entry.to_ledger_ct(), -15_542); // credit β†’ negative in open-items

πŸ“‹ Standards

Standard Module Notes
ISO 13616-1 + SWIFT IBAN Registry iban 56-country length table
ISO 9362 bic 8- and 11-char BIC
EPC AT-02 creditor_id SEPA Creditor Identifier
ISO 20022 pain.001.003.03 pain001 SCT β€” DK V2.7, German banking default
ISO 20022 pain.001.001.09 pain001 SCT Instant β€” EPC Rulebook 2021+
ISO 20022 pain.008.003.02 pain008 SDD CORE + B2B
ISO 20022 pain.002 (all variants) pain002 namespace-agnostic parser
ISO 20022 camt.053 (all variants) camt053 namespace-agnostic parser
ISO 20022 camt.054 camt054 notification types
EPC SEPA Rulebooks all SCT, SDD Core, SDD B2B, SCT Inst

πŸ“¦ Dependencies

Crate Role
thiserror Ergonomic error derives (required)
serde Serialize/Deserialize on all types (serde feature)
serde_json camt054::parse_simple_json (json feature)

πŸ“„ License

Licensed under either of MIT or Apache-2.0, at your option.