libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

camt.rs (46762B)


      1 /*
      2 * This file is part of LibEuFin.
      3 * Copyright (C) 2026 Taler Systems S.A.
      4 
      5 * LibEuFin is free software; you can redistribute it and/or modify
      6 * it under the terms of the GNU Affero General Public License as
      7 * published by the Free Software Foundation; either version 3, or
      8 * (at your option) any later version.
      9 
     10 * LibEuFin is distributed in the hope that it will be useful, but
     11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13 * Public License for more details.
     14 
     15 * You should have received a copy of the GNU Affero General Public
     16 * License along with LibEuFin; see the file COPYING.  If not, see
     17 * <http://www.gnu.org/licenses/>
     18 */
     19 
     20 use std::{fmt::Write as _, str::FromStr};
     21 
     22 use compact_str::CompactString;
     23 use jiff::{
     24     Timestamp,
     25     civil::{self},
     26     tz::TimeZone,
     27 };
     28 use taler_common::types::{
     29     amount::{Amount, Currency},
     30     iban::IBAN,
     31     payto::{BankID, IbanPayto, PaytoImpl, PaytoURI},
     32 };
     33 use taler_macros::EnumMeta;
     34 use tracing::{trace, warn};
     35 use uuid::Uuid;
     36 
     37 use crate::{
     38     iso20022::{
     39         ChargeBearer,
     40         bank_tx_code::{BankTxDomainCode, BankTxFamilyCode, BankTxSubFamilyCode},
     41         model::{BatchId, InId, InTx, OutBatch, OutId, OutReversal, OutTx, Tx},
     42         status_code::ReturnReason,
     43     },
     44     xml::{self, Xml, XmlAccess as _},
     45 };
     46 
     47 #[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)]
     48 #[enum_meta(Str)]
     49 #[allow(clippy::upper_case_acronyms)]
     50 enum Kind {
     51     CRDT,
     52     DBIT,
     53 }
     54 
     55 #[derive(Debug, Clone, PartialEq, Eq)]
     56 pub enum AccountId {
     57     Iban(IBAN),
     58     Other(CompactString),
     59 }
     60 
     61 impl std::fmt::Display for AccountId {
     62     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
     63         match self {
     64             AccountId::Iban(iban) => iban.fmt(f),
     65             AccountId::Other(id) => id.fmt(f),
     66         }
     67     }
     68 }
     69 
     70 #[derive(Debug, Clone, PartialEq, Eq)]
     71 pub struct AccountTransactions {
     72     pub id: AccountId,
     73     pub currency: Option<Currency>,
     74     pub txs: Vec<Tx>,
     75 }
     76 
     77 #[derive(Debug, Clone, PartialEq, Eq)]
     78 enum OutIds {
     79     Tx(OutId),
     80     Batch(BatchId),
     81 }
     82 
     83 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     84 struct BankTxCode {
     85     domain: BankTxDomainCode,
     86     family: BankTxFamilyCode,
     87     subfamily: BankTxSubFamilyCode,
     88 }
     89 
     90 impl BankTxCode {
     91     fn is_reversal(&self) -> bool {
     92         matches!(
     93             self.subfamily,
     94             BankTxSubFamilyCode::RRTN
     95                 | BankTxSubFamilyCode::RPCR
     96                 | BankTxSubFamilyCode::PSTE
     97                 | BankTxSubFamilyCode::XRTN
     98                 | BankTxSubFamilyCode::RCDD
     99                 | BankTxSubFamilyCode::UPDD
    100                 | BankTxSubFamilyCode::PRDD
    101                 | BankTxSubFamilyCode::RCOV
    102         )
    103     }
    104 }
    105 
    106 impl std::fmt::Display for BankTxCode {
    107     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    108         let Self {
    109             domain,
    110             family,
    111             subfamily,
    112         } = self;
    113         write!(
    114             f,
    115             "{domain} {family} {subfamily} - '{}' '{}' '{}'",
    116             domain.description(),
    117             family.description(),
    118             subfamily.description()
    119         )
    120     }
    121 }
    122 
    123 /** Parse the instruction execution date */
    124 fn execution_date(n: Xml) -> xml::Result<Timestamp> {
    125     // Value date if present else booking date
    126     let date = n
    127         .opt("ValDt")
    128         .transpose()
    129         .unwrap_or_else(|| n.one("BookgDt"))?;
    130     let date = if let Some(date) = date.opt("Dt")? {
    131         date.parse::<civil::Date>()?.into()
    132     } else {
    133         date.one("DtTm").parse::<civil::DateTime>()?
    134     };
    135     Ok(date.to_zoned(TimeZone::UTC).unwrap().timestamp())
    136 }
    137 
    138 /** Parse a payto */
    139 fn payto(n: Xml, prefix: &str) -> xml::Result<Option<PaytoURI>> {
    140     let Some(parties) = n.opt("RltdPties")? else {
    141         return Ok(None);
    142     };
    143 
    144     let Some(iban) = parties
    145         .opt(&format!("{prefix}Acct"))
    146         .one("Id")
    147         .opt("IBAN")
    148         .parse()?
    149     else {
    150         return Ok(None);
    151     };
    152     // TODO parse BIC
    153     let bank_id = BankID { iban, bic: None };
    154     Ok(Some(if let Some(p) = parties.opt(prefix)? {
    155         let name = p
    156             .opt("Nm")
    157             .transpose()
    158             .unwrap_or_else(|| p.one("Pty").one("Nm"))?
    159             .text();
    160         IbanPayto::new(bank_id).as_full_uri(name)
    161     } else {
    162         IbanPayto::new(bank_id).as_uri()
    163     }))
    164 }
    165 
    166 /** Parse batch message ID and transaction end-to-end ID as generated by libeufin-nexus */
    167 fn outgoing_id(n: Xml, sref: Option<&str>) -> xml::Result<OutIds> {
    168     Ok(if let Some(refs) = n.opt("Refs")? {
    169         let e2e_id: Option<CompactString> = refs.opt("EndToEndId").parse()?;
    170         let msg_id: Option<CompactString> = refs.opt("MsgId").parse()?;
    171         let sref: Option<CompactString> =
    172             sref.filter(|it| *it != "NOTPROVIDED").map(|it| it.into());
    173         match (e2e_id, msg_id) {
    174             // This is a batch representation
    175             (None, Some(msg_id)) => OutIds::Batch(BatchId { msg_id, sref }),
    176             // If not set use MsgId as end-to-end ID for retrocompatibility
    177             (Some(e2e_id), msg_id) if &e2e_id == "NOTPROVIDED" => OutIds::Tx(OutId {
    178                 e2e_id: msg_id.clone(),
    179                 msg_id,
    180                 sref,
    181             }),
    182             (e2e_id, msg_id) => OutIds::Tx(OutId {
    183                 msg_id,
    184                 e2e_id,
    185                 sref,
    186             }),
    187         }
    188     } else {
    189         OutIds::Tx(OutId {
    190             msg_id: None,
    191             e2e_id: None,
    192             sref: sref.map(|it| it.into()),
    193         })
    194     })
    195 }
    196 
    197 /** Parse transaction ids as provided by bank */
    198 fn incoming_id(n: Xml, sref: Option<&str>) -> xml::Result<InId> {
    199     if let Some(refs) = n.opt("Refs")? {
    200         let uetr: Option<Uuid> = refs.opt("UETR").parse()?;
    201         let tx_id: Option<CompactString> = refs.opt("TxId").parse()?;
    202         Ok(InId {
    203             uetr,
    204             tx_id,
    205             sref: sref.map(|it| it.into()),
    206         })
    207     } else {
    208         Ok(InId {
    209             uetr: None,
    210             tx_id: None,
    211             sref: sref.map(|it| it.into()),
    212         })
    213     }
    214 }
    215 
    216 /** Parse transaction wire transfer subject */
    217 fn wire_transfer_subject(n: Xml) -> xml::Result<Option<String>> {
    218     Ok(n.opt("RmtInf")?.map(|n| {
    219         // Concat all lines
    220         let mut s = n.many("Ustrd").map(|n| n.text()).collect::<String>();
    221         // Merge whitespace
    222         let mut pending_space = false;
    223         s.retain(|c| {
    224             if c.is_ascii_whitespace() {
    225                 if pending_space {
    226                     false
    227                 } else {
    228                     pending_space = true;
    229                     true
    230                 }
    231             } else if pending_space {
    232                 pending_space = false;
    233                 true
    234             } else {
    235                 true
    236             }
    237         });
    238         s
    239     }))
    240 }
    241 
    242 /** Parse and format transaction return reasons */
    243 fn return_reason(n: Xml) -> xml::Result<String> {
    244     let mut buf = String::new();
    245     if let Some(n) = n.opt("RtrInf")? {
    246         let code: ReturnReason = n.one("Rsn").one("Cd").parse()?;
    247 
    248         write!(&mut buf, "{code} '{}'", code.description()).unwrap();
    249         let mut infos = n.many("AddtlInf");
    250         if let Some(first) = infos.next() {
    251             buf.push_str(" - '");
    252             buf.push_str(first.text());
    253             for info in infos {
    254                 buf.push_str(info.text());
    255             }
    256             buf.push('\'');
    257         }
    258     } else if let Some(n) = wire_transfer_subject(n)? {
    259         return Ok(n);
    260     }
    261     Ok(buf)
    262 }
    263 /** Parse amount */
    264 fn amount(n: Xml) -> xml::Result<Amount> {
    265     let amt = n.one("Amt")?;
    266     let currency = amt.attr("Ccy")?;
    267     let amount = amt.text();
    268     let concat = format!("{currency}:0{amount}");
    269     Amount::from_str(&concat).map_err(|e| amt.parse_err(e))
    270 }
    271 
    272 #[derive(Debug, Clone, Copy)]
    273 struct ComplexAmount {
    274     /// Transaction amount
    275     amount: Amount,
    276     /// The applied fee
    277     fee: Amount,
    278 }
    279 
    280 impl ComplexAmount {
    281     /// Check that entry and tx amount are compatible and return the result
    282     fn resolve(&self, tx: &ComplexAmount) -> xml::Result<ComplexAmount> {
    283         // Most time transaction will match
    284         if self.amount == tx.amount && self.fee == tx.fee {
    285             return Ok(*self);
    286         }
    287 
    288         // Or one of the level is missing the fee
    289         if (tx.amount.decimal() > tx.fee.decimal()
    290             && tx.amount.try_sub(&tx.fee).unwrap() == self.amount)
    291             || self.amount.try_sub(&self.fee).unwrap() == tx.amount
    292         {
    293             return if tx.fee.is_zero() { Ok(*self) } else { Ok(*tx) };
    294         }
    295 
    296         // Or the conversion information are only present at the entry layer
    297         if tx.amount.currency != self.amount.currency {
    298             return Ok(*self);
    299         }
    300 
    301         panic!("Amount mismatch, got {self:?} in the entry and {tx:?} in the tx")
    302     }
    303 }
    304 
    305 struct ChargeRecord {
    306     amount: Amount,
    307     kind: Kind,
    308     included: bool,
    309     bearer: ChargeBearer,
    310 }
    311 
    312 fn charges(n: Xml) -> xml::Result<Vec<ChargeRecord>> {
    313     if let Some(n) = n.opt("Chrgs")? {
    314         n.many("Rcrd")
    315             .map(|n| {
    316                 Ok(ChargeRecord {
    317                     amount: amount(n)?,
    318                     kind: n.opt("CdtDbtInd").parse()?.unwrap_or(Kind::CRDT),
    319                     included: n.opt("ChrgInclInd").parse()? == Some(true), // TODO not clear in spec
    320                     bearer: n.opt("Br").parse()?.unwrap_or(ChargeBearer::SHAR),
    321                 })
    322             })
    323             .collect()
    324     } else {
    325         Ok(Vec::new())
    326     }
    327 }
    328 
    329 fn complex_amount(amt: Xml, charges: &[ChargeRecord]) -> xml::Result<ComplexAmount> {
    330     // Amount before charges
    331     let currency = amt.attr("Ccy")?;
    332     // In case of fee overflow it's possible to have a negative amount here
    333     // We ignore this as it will be handled elsewhere correctly
    334     let amount = amt.text().trim_start_matches('-');
    335     let concat = format!("{currency}:0{amount}");
    336 
    337     let mut amount = Amount::from_str(&concat).map_err(|e| amt.parse_err(e))?;
    338     let mut fee = Amount::zero(&amount.currency);
    339 
    340     for chr in charges {
    341         if chr.included && !chr.amount.is_zero() {
    342             fee = fee.try_add(&chr.amount).expect("Should never overflow");
    343             if chr.kind == Kind::DBIT {
    344                 if chr.bearer == ChargeBearer::DEBT {
    345                     if chr.amount.decimal() > amount.decimal() {
    346                         // This can happen when an incoming transaction fail because of debit fee
    347                         amount = chr.amount.try_sub(&amount).expect("Should never overflow");
    348                     } else {
    349                         amount = amount.try_sub(&chr.amount).expect("Should never overflow");
    350                     }
    351                 } else if chr.bearer == ChargeBearer::CRED {
    352                     amount = amount.try_add(&chr.amount).expect("Should never overflow");
    353                 } else {
    354                     return Err(amt.parse_err(format_args!(
    355                         "Included charge {} with bearer {}",
    356                         chr.kind, chr.bearer
    357                     )));
    358                 }
    359             }
    360         }
    361     }
    362 
    363     Ok(ComplexAmount { amount, fee })
    364 }
    365 
    366 /** Parse bank transaction code */
    367 fn bank_tx_code(n: Xml) -> xml::Result<BankTxCode> {
    368     let domnd = n.one("Domn")?;
    369     let fmly = domnd.one("Fmly")?;
    370     Ok(BankTxCode {
    371         domain: domnd.one("Cd").parse()?,
    372         family: fmly.one("Cd").parse()?,
    373         subfamily: fmly.one("SubFmlyCd").parse()?,
    374     })
    375 }
    376 
    377 /** Parse camt files */
    378 pub fn parse_camt(xml: &[u8]) -> xml::Result<Vec<AccountTransactions>> {
    379     /*
    380         In ISO 20022 specifications, most fields are optional and the same information
    381         can be written several times in different places. For libeufin, we're only
    382         interested in a subset of the available values that can be found in both camt.052,
    383         camt.053 and camt.054. This function should not fail on legitimate files and should
    384         simply warn when available information are insufficient.
    385 
    386         EBICS and ISO20022 do not provide a perfect transaction identifier. The best is the
    387         UETR (unique end-to-end transaction reference), which is a universally unique
    388         identifier (UUID). However, it is not supplied by all banks. TxId (TransactionIdentification)
    389         is a unique identification as assigned by the first instructing agent. As its format
    390         is ambiguous, its uniqueness is not guaranteed by the standard, and it is only
    391         supposed to be unique for a “pre-agreed period”, whatever that means. These two
    392         identifiers are optional in the standard, but have the advantage of being unique
    393         and can be used to track a transaction between banks so we use them when available.
    394 
    395         It is also possible to use AccountServicerReference, which is a unique reference
    396         assigned by the account servicing institution. They can be present at several levels
    397         (batch level, transaction level, etc.) and are often optional. They also have the
    398         disadvantage of being known only by the account servicing institution. They should
    399         therefore only be used as a last resort.
    400     */
    401     trace!("Parse transactions camt file");
    402 
    403     fn parse_inner(root: Xml) -> xml::Result<AccountTransactions> {
    404         let (id, currency) = {
    405             let account = root.one("Acct")?;
    406             let id = account.one("Id")?;
    407             let account_id = if let Some(iban) = id.opt("IBAN")? {
    408                 AccountId::Iban(iban.parse()?)
    409             } else {
    410                 AccountId::Other(id.one("Othr").one("Id").parse()?)
    411             };
    412             let currency: Option<Currency> = account.opt("Ccy").parse()?;
    413             (account_id, currency)
    414         };
    415         let txs = root.many("Ntry").try_fold(Vec::new(), |mut txs, entry| {
    416             // Skip if not booked
    417             if !{
    418                 let status = entry.one("Sts")?;
    419                 let status = status
    420                     .opt("Cd")?
    421                     .map(|n| n.text())
    422                     .unwrap_or_else(|| status.text());
    423                 status == "BOOK"
    424             } {
    425                 return Ok(txs);
    426             }
    427 
    428             let reversal = entry.opt("RvslInd").parse()? == Some(true);
    429             let entry_code = bank_tx_code(entry.one("BkTxCd")?)?;
    430             let entry_kind = entry.opt("CdtDbtInd").parse::<Kind>()?;
    431             let entry_ref = entry.opt("AcctSvcrRef").parse::<CompactString>()?;
    432             let date = execution_date(entry)?;
    433             let entry_charges = charges(entry)?;
    434             let entry_amount = complex_amount(entry.one("Amt")?, &entry_charges)?;
    435 
    436             let Some(details) = entry.opt("NtryDtls")? else {
    437                 return Ok(txs);
    438             };
    439             // When an entry only contain a single transactions information will sometimes only be stored at the entry level
    440             let unique = details.many("TxDtls").count() == 1;
    441             for tx in details.many("TxDtls") {
    442                 // Check information are present and coherent
    443                 let kind = tx.opt("CdtDbtInd").parse()?.or(entry_kind).unwrap();
    444 
    445                 // Sometimes the transaction level have a more precise bank transaction code
    446                 let code = tx
    447                     .opt("BkTxCd")?
    448                     .map(bank_tx_code)
    449                     .transpose()?
    450                     .unwrap_or(entry_code);
    451 
    452                 let tx_charges = charges(tx)?;
    453                 // Amount
    454                 let amount = if unique {
    455                     // When unique the charges can be only at the entry level
    456                     if let Some(amt) = tx.opt("Amt")? {
    457                         let tx_amount = complex_amount(
    458                             amt,
    459                             if tx_charges.is_empty() {
    460                                 &entry_charges
    461                             } else {
    462                                 &tx_charges
    463                             },
    464                         )?;
    465                         // Check coherence
    466                         entry_amount.resolve(&tx_amount)?
    467                     } else {
    468                         entry_amount
    469                     }
    470                 } else {
    471                     // When many inner transaction the entry level is an aggregate of them
    472                     // We only use the transaction level information
    473                     complex_amount(tx.one("Amt")?, &tx_charges)?
    474                 };
    475 
    476                 // We can only use the entry ref as the transaction ref if there is a single transaction in the batch
    477                 let sref: Option<CompactString> = tx
    478                     .opt("Refs")
    479                     .opt("AcctSvcrRef")
    480                     .parse::<CompactString>()?
    481                     .or_else(|| unique.then(|| entry_ref.clone()).flatten());
    482 
    483                 match (kind, code.is_reversal() || reversal) {
    484                     (Kind::CRDT, true) => {
    485                         let out_id = outgoing_id(tx, sref.as_deref())?;
    486                         if let OutIds::Tx(OutId {
    487                             msg_id,
    488                             e2e_id: Some(e2e_id),
    489                             ..
    490                         }) = out_id
    491                         {
    492                             txs.push(Tx::Reversal(OutReversal {
    493                                 e2e_id,
    494                                 msg_id,
    495                                 reason: return_reason(tx)?,
    496                                 execution_time: date,
    497                             }))
    498                         } else {
    499                             warn!("missing unique ID for Credit reversal {out_id:?}");
    500                         }
    501                     }
    502                     (Kind::DBIT, true) | (Kind::CRDT, false) => {
    503                         let id = incoming_id(tx, sref.as_deref())?;
    504                         if id.uetr.is_none() && id.tx_id.is_none() && id.sref.is_none() {
    505                             warn!("missing unique ID for Credit")
    506                         } else {
    507                             txs.push(Tx::In(InTx {
    508                                 id,
    509                                 amount: amount.amount,
    510                                 credit_fee: amount.fee,
    511                                 subject: wire_transfer_subject(tx)?,
    512                                 execution_time: date,
    513                                 debtor: payto(tx, "Dbtr")?,
    514                             }));
    515                         }
    516                     }
    517                     (Kind::DBIT, false) => {
    518                         let id = outgoing_id(tx, sref.as_deref())?;
    519                         match id {
    520                             OutIds::Tx(id) => {
    521                                 if id.e2e_id.is_none() && id.msg_id.is_none() && id.sref.is_none() {
    522                                     warn!("missing unique ID for Debit")
    523                                 } else {
    524                                     txs.push(Tx::Out(OutTx {
    525                                         id,
    526                                         amount: amount.amount,
    527                                         debit_fee: amount.fee,
    528                                         subject: wire_transfer_subject(tx)?,
    529                                         execution_time: date,
    530                                         creditor: payto(tx, "Cdtr")?,
    531                                     }));
    532                                 }
    533                             }
    534                             OutIds::Batch(BatchId { msg_id, .. }) => {
    535                                 txs.push(Tx::Batch(OutBatch {
    536                                     msg_id,
    537                                     execution_time: date,
    538                                 }));
    539                             }
    540                         }
    541                     }
    542                 }
    543             }
    544             Ok(txs)
    545         })?;
    546         Ok(AccountTransactions { id, currency, txs })
    547     }
    548 
    549     Xml::parse(xml, "Document", |root| {
    550         if let Some(camt053) = root.opt("BkToCstmrStmt")? {
    551             camt053.many("Stmt").map(parse_inner).collect()
    552         } else if let Some(camt052) = root.opt("BkToCstmrAcctRpt")? {
    553             camt052.many("Rpt").map(parse_inner).collect()
    554         } else if let Some(camt054) = root.opt("BkToCstmrDbtCdtNtfctn")? {
    555             camt054.many("Ntfctn").map(parse_inner).collect()
    556         } else {
    557             Err(root.parse_err("Malformed camt file"))
    558         }
    559     })
    560 }
    561 
    562 #[cfg(test)]
    563 pub mod test {
    564     use std::str::FromStr;
    565 
    566     use jiff::civil::Date;
    567     use taler_common::types::{
    568         amount::{Amount, Currency},
    569         iban::IBAN,
    570         payto::{BankID, IbanPayto, PaytoImpl as _, PaytoURI},
    571         utils::date_to_utc_ts,
    572     };
    573 
    574     use crate::iso20022::{
    575         camt::{AccountId, parse_camt},
    576         model::{InId, InTx, OutBatch, OutId, OutReversal, OutTx, Tx},
    577     };
    578 
    579     fn iban_payto(iban: impl AsRef<str>, name: impl AsRef<str>) -> PaytoURI {
    580         IbanPayto::new(BankID {
    581             iban: iban.as_ref().parse().expect("invalid IBAN"),
    582             bic: None,
    583         })
    584         .as_full_uri(name.as_ref())
    585     }
    586 
    587     #[track_caller]
    588     pub fn check_tx(path: &str, iban: &str, currency: Option<&str>, txs: &[Tx]) {
    589         let content = std::fs::read(path).unwrap();
    590         let res = parse_camt(&content).unwrap();
    591         assert_eq!(res.len(), 1);
    592 
    593         let first = &res[0];
    594         assert_eq!(first.id, AccountId::Iban(IBAN::from_str(iban).unwrap()));
    595         assert_eq!(
    596             first.currency,
    597             currency.map(|it| Currency::from_str(it).unwrap())
    598         );
    599         pretty_assertions::assert_eq!(first.txs, txs);
    600     }
    601 
    602     pub fn tx_out(
    603         id: (Option<&str>, Option<&str>, Option<&str>),
    604         amount: &str,
    605         debit_fee: &str,
    606         subject: Option<&str>,
    607         execution_time: &str,
    608         creditor: Option<(&str, &str)>,
    609     ) -> Tx {
    610         Tx::Out(OutTx {
    611             id: OutId::new(
    612                 id.0.map(Into::into),
    613                 id.1.map(Into::into),
    614                 id.2.map(Into::into),
    615             ),
    616             amount: Amount::from_str(amount).unwrap(),
    617             debit_fee: Amount::from_str(debit_fee).unwrap(),
    618             subject: subject.map(Into::into),
    619             execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()),
    620             creditor: creditor.map(|(iban, name)| iban_payto(iban, name)),
    621         })
    622     }
    623 
    624     pub fn tx_in(
    625         id: (Option<&str>, Option<&str>, Option<&str>),
    626         amount: &str,
    627         credit_fee: &str,
    628         subject: Option<&str>,
    629         execution_time: &str,
    630         debtor: Option<(&str, &str)>,
    631     ) -> Tx {
    632         Tx::In(InTx {
    633             id: InId::new(
    634                 id.0.map(|it| it.parse().unwrap()),
    635                 id.1.map(Into::into),
    636                 id.2.map(Into::into),
    637             ),
    638             amount: Amount::from_str(amount).unwrap(),
    639             credit_fee: Amount::from_str(credit_fee).unwrap(),
    640             subject: subject.map(Into::into),
    641             execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()),
    642             debtor: debtor.map(|(iban, name)| iban_payto(iban, name)),
    643         })
    644     }
    645 
    646     pub fn tx_reversal(
    647         e2e_id: &str,
    648         msg_id: Option<&str>,
    649         reason: &str,
    650         execution_time: &str,
    651     ) -> Tx {
    652         Tx::Reversal(OutReversal {
    653             e2e_id: e2e_id.parse().unwrap(),
    654             msg_id: msg_id.map(Into::into),
    655             reason: reason.into(),
    656             execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()),
    657         })
    658     }
    659 
    660     pub fn tx_batch(msg_id: &str, execution_time: &str) -> Tx {
    661         Tx::Batch(OutBatch {
    662             msg_id: msg_id.into(),
    663             execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()),
    664         })
    665     }
    666 
    667     #[test]
    668     fn postfinance_camt054() {
    669         check_tx(
    670             "sample/platform/postfinance_camt054.xml",
    671             "CH9289144596463965762",
    672             Some("CHF"),
    673             &[
    674                 tx_out(
    675                     (
    676                         Some("ZS1PGNTSV0ZNDFAJBBWWB8015G"),
    677                         Some("ZS1PGNTSV0ZNDFAJBBWWB8015G"),
    678                         None,
    679                     ),
    680                     "CHF:3.00",
    681                     "CHF:0",
    682                     None,
    683                     "2024-01-15",
    684                     None,
    685                 ),
    686                 tx_in(
    687                     (
    688                         Some("62e2b511-7313-4ccd-8d40-c9d8e612cd71"),
    689                         None,
    690                         Some("231121CH0AZWCR9T"),
    691                     ),
    692                     "CHF:10",
    693                     "CHF:0",
    694                     Some("G1XTY6HGWGMVRM7E6XQ4JHJK561ETFDFTJZ7JVGV543XZCB27YBG"),
    695                     "2023-12-19",
    696                     Some(("CH7389144832588726658", "Mr Test")),
    697                 ),
    698                 tx_in(
    699                     (
    700                         Some("62e2b511-7313-4ccd-8d40-c9d8e612cd72"),
    701                         None,
    702                         Some("231121CH0AZWCVR1"),
    703                     ),
    704                     "CHF:2.53",
    705                     "CHF:0",
    706                     Some("G1XTY6HGWGMVRM7E6XQ4JHJK561ETFDFTJZ7JVGV543XZCB27YB"),
    707                     "2023-12-19",
    708                     Some(("CH7389144832588726658", "Mr Test")),
    709                 ),
    710                 tx_reversal(
    711                     "50820f78-9024-44ff-978d-63a18c",
    712                     Some("50820f78-9024-44ff-978d-63a18c"),
    713                     "",
    714                     "2024-01-15",
    715                 ),
    716                 tx_batch("ZS1PGNTSV0ZNDFAJBBWWB8015G", "2024-01-15"),
    717             ],
    718         );
    719     }
    720 
    721     #[test]
    722     fn postfinance_camt053() {
    723         check_tx(
    724             "sample/platform/postfinance_camt053.xml",
    725             "CH9289144596463965762",
    726             Some("CHF"),
    727             &[
    728                 tx_reversal(
    729                     "889d1a80-1267-49bd-8fcc-85701a",
    730                     Some("889d1a80-1267-49bd-8fcc-85701a"),
    731                     "InconsistenWithEndCustomer 'Identification of end customer is not consistent with associated account number, organisation ID or private ID' - 'more info here ...'",
    732                     "2023-11-22",
    733                 ),
    734                 tx_reversal(
    735                     "4cc61cc7-6230-49c2-b5e2-b40bbb",
    736                     Some("4cc61cc7-6230-49c2-b5e2-b40bbb"),
    737                     "MissingCreditorNameOrAddress 'Specification of the creditor’s name and/or address needed for regulatory requirements is insufficient or missing' - 'more info here ...'",
    738                     "2023-11-22",
    739                 ),
    740                 tx_batch("EB4D22D428214261B2B3012D2A8CEC36", "2024-08-26"),
    741             ],
    742         );
    743     }
    744 
    745     #[test]
    746     fn raiffeisen_camt053() {
    747         check_tx(
    748             "sample/platform/raiffeisen_camt053.xml",
    749             "CH7389144832588726658",
    750             None,
    751             &[
    752                 tx_in(
    753                     (None, None, Some("A200020494367552")),
    754                     "CHF:20000",
    755                     "CHF:0",
    756                     Some("1. TZ 2025"),
    757                     "2025-12-23",
    758                     Some(("CH7389144832588726658", "KANTON BERN")),
    759                 ),
    760                 tx_out(
    761                     (None, None, Some("19868398389")),
    762                     "CHF:15",
    763                     "CHF:0",
    764                     None,
    765                     "2025-12-31",
    766                     None,
    767                 ),
    768                 tx_out(
    769                     (None, None, Some("19890406743")),
    770                     "CHF:2",
    771                     "CHF:0",
    772                     None,
    773                     "2025-12-31",
    774                     None,
    775                 ),
    776                 tx_out(
    777                     (None, None, Some("19885172770")),
    778                     "CHF:3",
    779                     "CHF:0",
    780                     None,
    781                     "2025-12-31",
    782                     None,
    783                 ),
    784             ],
    785         );
    786     }
    787 
    788     #[test]
    789     fn valiant_camt052() {
    790         check_tx(
    791             "sample/platform/valiant_camt052.xml",
    792             "CH7389144832588726658",
    793             Some("CHF"),
    794             &[
    795                 tx_out(
    796                     (
    797                         Some("MJDJO2BDDBL7YSL2P96SXHG3TQZEZQD26L"),
    798                         Some("4UWWIDGTEIGDU6Z721QE95PYJSIEA48PYE"),
    799                         Some("ZV20251030/511372/1"),
    800                     ),
    801                     "CHF:0.1",
    802                     "CHF:0",
    803                     Some("single 2025-10-30T09:46:04.55293090 9Z"),
    804                     "2025-10-30",
    805                     Some(("CH7389144832588726658", "Grothoff Hans")),
    806                 ),
    807                 tx_out(
    808                     (
    809                         Some("5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U"),
    810                         Some("SKMU2891PAAYBDW22DBWX2W7KTFZ1CDFO8"),
    811                         Some("ZV20251030/511373/1"),
    812                     ),
    813                     "CHF:0.1",
    814                     "CHF:0",
    815                     Some("multi 0 2025-10-30T09:46:10.3877961 30Z"),
    816                     "2025-10-30",
    817                     Some(("CH7389144832588726658", "Grothoff Hans")),
    818                 ),
    819                 tx_out(
    820                     (
    821                         Some("5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U"),
    822                         Some("RC9YD301NZ17YKD6WDWLNOROFHIIN29VJN"),
    823                         Some("ZV20251030/511373/2"),
    824                     ),
    825                     "CHF:0.11",
    826                     "CHF:0",
    827                     Some("multi 1 2025-10-30T09:46:10.3877961 30Z"),
    828                     "2025-10-30",
    829                     Some(("CH7389144832588726658", "Grothoff Hans")),
    830                 ),
    831                 tx_out(
    832                     (
    833                         Some("5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U"),
    834                         Some("GKDGTHLB82X6XVHBJIJ1CK8MEGU9XJ2EL7"),
    835                         Some("ZV20251030/511373/3"),
    836                     ),
    837                     "CHF:0.12",
    838                     "CHF:0",
    839                     Some("multi 2 2025-10-30T09:46:10.3877961 30Z"),
    840                     "2025-10-30",
    841                     Some(("CH7389144832588726658", "Grothoff Hans")),
    842                 ),
    843                 tx_out(
    844                     (
    845                         Some("5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U"),
    846                         Some("PXCH2VVVTXEXBVDWICP23HZ4NV0H2CWW28"),
    847                         Some("ZV20251030/511373/4"),
    848                     ),
    849                     "CHF:0.13",
    850                     "CHF:0",
    851                     Some("multi 3 2025-10-30T09:46:10.3877961 30Z"),
    852                     "2025-10-30",
    853                     Some(("CH7389144832588726658", "Grothoff Hans")),
    854                 ),
    855                 tx_in(
    856                     (None, Some("51030655601.0001"), Some("ZV20251030/514778/1")),
    857                     "CHF:0.85",
    858                     "CHF:0",
    859                     Some("fun stuff"),
    860                     "2025-10-30",
    861                     Some(("CH7389144832588726658", "Grothoff Hans")),
    862                 ),
    863                 tx_in(
    864                     (None, Some("51030655601.0002"), Some("ZV20251030/514779/1")),
    865                     "CHF:0.95",
    866                     "CHF:0",
    867                     Some("Taler PC2MKG0B7CK32K1T7DP08P6E1B7FHB6HY6R Q0PT3VTPBPRPYM1B0"),
    868                     "2025-10-30",
    869                     Some(("CH7389144832588726658", "Grothoff Hans")),
    870                 ),
    871                 tx_out(
    872                     (
    873                         Some("X166701F6RV59LP71RVWVIW9SV2AFZYLG4"),
    874                         Some("R48UBIIB7B4LX0DMVOSI0ZTJWMMG8FMNKX"),
    875                         Some("ZV20251030/524078/1"),
    876                     ),
    877                     "CHF:0.21",
    878                     "CHF:0",
    879                     Some("bad name 2025-10-30T12:03:24.997478 811Z"),
    880                     "2025-10-30",
    881                     Some(("CH6208704048981247126", "John Smith")),
    882                 ),
    883                 tx_out(
    884                     (
    885                         Some("6OZN5T9W7MK6BIZYE01E62NHGP5JLMUD4X"),
    886                         Some("02WDIX4J90Z1M1WNFHLNSXY59SHXQTQCMQ"),
    887                         Some("ZV20251030/524079/1"),
    888                     ),
    889                     "CHF:0.1",
    890                     "CHF:0",
    891                     Some("single 2025-10-30T12:04:00.37042083 6Z"),
    892                     "2025-10-30",
    893                     Some(("CH7389144832588726658", "Grothoff Hans")),
    894                 ),
    895                 tx_out(
    896                     (
    897                         Some("6OZN5T9W7MK6BIZYE01E62NHGP5JLMUD4X"),
    898                         Some("XAP5L7HVWPLCEMECU4GZK6GKUPBL0TD13Y"),
    899                         Some("ZV20251030/524079/2"),
    900                     ),
    901                     "CHF:0.21",
    902                     "CHF:0",
    903                     Some("bad name 2025-10-30T12:03:53.042190 686Z"),
    904                     "2025-10-30",
    905                     Some(("CH6208704048981247126", "John Smith")),
    906                 ),
    907                 tx_reversal(
    908                     "XAP5L7HVWPLCEMECU4GZK6GKUPBL0TD13Y",
    909                     None,
    910                     "Error msg in german",
    911                     "2025-10-30",
    912                 ),
    913                 tx_reversal(
    914                     "R48UBIIB7B4LX0DMVOSI0ZTJWMMG8FMNKX",
    915                     None,
    916                     "Error msg in german",
    917                     "2025-10-30",
    918                 ),
    919                 tx_out(
    920                     (
    921                         Some("OLAMDPI6YPMNRZHQ5PQ6JCVUQV2AN5NW6P"),
    922                         Some("TU2WJ54DR9Z6HT5VE494BNH4EXUSM0DRF7"),
    923                         Some("ZV20251030/524077/1"),
    924                     ),
    925                     "CHF:0.23",
    926                     "CHF:5",
    927                     Some("foreign iban 2025-10-30T12:03:44.0972 63765Z"),
    928                     "2025-10-30",
    929                     Some(("DE48330605920000686018", "Christian Grothoff")),
    930                 ),
    931                 tx_out(
    932                     (
    933                         Some("6OZN5T9W7MK6BIZYE01E62NHGP5JLMUD4X"),
    934                         Some("GM8I8GIETR72LP6CFBGRBUDKNO2CEQBGOE"),
    935                         Some("ZV20251030/524080/1"),
    936                     ),
    937                     "CHF:0.23",
    938                     "CHF:5",
    939                     Some("foreign iban 2025-10-30T12:03:58.0046 73747Z"),
    940                     "2025-10-30",
    941                     Some(("DE48330605920000686018", "Christian Grothoff")),
    942                 ),
    943                 tx_in(
    944                     (
    945                         Some("7b76d488-05d5-44ab-9d77-31d4165ec158"),
    946                         Some("00204EQY370"),
    947                         Some("ZV20251118/685062/1"),
    948                     ),
    949                     "CHF:4.55",
    950                     "CHF:0",
    951                     Some("TEST"),
    952                     "2025-11-18",
    953                     None,
    954                 ),
    955             ],
    956         )
    957     }
    958 
    959     #[test]
    960     fn gls_camt052() {
    961         check_tx(
    962             "sample/platform/gls_camt052.xml",
    963             "DE84500105177118117964",
    964             Some("EUR"),
    965             &[
    966                 tx_out(
    967                     (
    968                         Some("COMPAT_SUCCESS"),
    969                         Some("COMPAT_SUCCESS"),
    970                         Some("2024041801514102000"),
    971                     ),
    972                     "EUR:2",
    973                     "EUR:0",
    974                     Some("TestABC123"),
    975                     "2024-04-18",
    976                     Some(("DE20500105172419259181", "John Smith")),
    977                 ),
    978                 tx_reversal(
    979                     "8XK8Z7RAX224FGWK832FD40GYC",
    980                     None,
    981                     "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN fehlerhaft und ungültig'",
    982                     "2024-09-05",
    983                 ),
    984                 tx_in(
    985                     (
    986                         None,
    987                         Some("BYLADEM1WOR-G2910276709458A2"),
    988                         Some("2024041210041357000"),
    989                     ),
    990                     "EUR:3",
    991                     "EUR:0",
    992                     Some("Taler FJDQ7W6G7NWX4H9M1MKA12090FRC9K7DA6N0FANDZZFXTR6QHX5G Test.,-"),
    993                     "2024-04-12",
    994                     Some(("DE84500105177118117964", "John Smith")),
    995                 ),
    996                 tx_reversal(
    997                     "COMPAT_FAILURE",
    998                     None,
    999                     "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN ...'",
   1000                     "2024-04-12",
   1001                 ),
   1002                 tx_out(
   1003                     (
   1004                         Some("BATCH_SINGLE_SUCCESS"),
   1005                         Some("FD622SMXKT5QWSAHDY0H8NYG3G"),
   1006                         Some("2024090216552232000"),
   1007                     ),
   1008                     "EUR:1.1",
   1009                     "EUR:0",
   1010                     Some("single 2024-09-02T14:29:52.875253314Z"),
   1011                     "2024-09-02",
   1012                     Some(("DE89500105173198527518", "Grothoff Hans")),
   1013                 ),
   1014                 tx_out(
   1015                     (
   1016                         Some("YF5QBARGQ0MNY0VK59S477VDG4"),
   1017                         Some("YF5QBARGQ0MNY0VK59S477VDG4"),
   1018                         Some("2024041810552821000"),
   1019                     ),
   1020                     "EUR:1.1",
   1021                     "EUR:0",
   1022                     Some("Simple tx"),
   1023                     "2024-04-18",
   1024                     Some(("DE20500105172419259181", "John Smith")),
   1025                 ),
   1026                 tx_batch("BATCH_MANY_SUCCESS", "2024-09-20"),
   1027                 tx_out(
   1028                     (
   1029                         Some("BATCH_SINGLE_RETURN"),
   1030                         Some("KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5"),
   1031                         Some("2024092100252498000"),
   1032                     ),
   1033                     "EUR:0.42",
   1034                     "EUR:0",
   1035                     Some("This should fail because bad iban"),
   1036                     "2024-09-23",
   1037                     Some(("DE18500105173385245163", "John Smith")),
   1038                 ),
   1039                 tx_reversal(
   1040                     "KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5",
   1041                     None,
   1042                     "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN fehlerhaft und ungültig'",
   1043                     "2024-09-24",
   1044                 ),
   1045             ],
   1046         )
   1047     }
   1048 
   1049     #[test]
   1050     fn gls_camt053() {
   1051         check_tx(
   1052             "sample/platform/gls_camt053.xml",
   1053             "DE84500105177118117964",
   1054             Some("EUR"),
   1055             &[
   1056                 tx_out(
   1057                     (
   1058                         Some("COMPAT_SUCCESS"),
   1059                         Some("COMPAT_SUCCESS"),
   1060                         Some("2024041801514102000"),
   1061                     ),
   1062                     "EUR:2",
   1063                     "EUR:0",
   1064                     Some("TestABC123"),
   1065                     "2024-04-18",
   1066                     Some(("DE20500105172419259181", "John Smith")),
   1067                 ),
   1068                 tx_reversal(
   1069                     "KGTDBASWTJ6JM89WXD3Q5KFQC4",
   1070                     None,
   1071                     "Retoure aus SEPA Überweisung multi line",
   1072                     "2024-09-04",
   1073                 ),
   1074                 tx_batch("BATCH_MANY_PART", "2024-09-04"),
   1075                 tx_in(
   1076                     (
   1077                         None,
   1078                         Some("BYLADEM1WOR-G2910276709458A2"),
   1079                         Some("2024041210041357000"),
   1080                     ),
   1081                     "EUR:3",
   1082                     "EUR:0",
   1083                     Some("Taler FJDQ7W6G7NWX4H9M1MKA12090FRC9K7DA6N0FANDZZFXTR6QHX5G Test.,-"),
   1084                     "2024-04-12",
   1085                     Some(("DE84500105177118117964", "John Smith")),
   1086                 ),
   1087                 tx_reversal(
   1088                     "COMPAT_FAILURE",
   1089                     None,
   1090                     "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN ...'",
   1091                     "2024-04-12",
   1092                 ),
   1093                 tx_out(
   1094                     (
   1095                         Some("BATCH_SINGLE_SUCCESS"),
   1096                         Some("FD622SMXKT5QWSAHDY0H8NYG3G"),
   1097                         Some("2024090216552232000"),
   1098                     ),
   1099                     "EUR:1.1",
   1100                     "EUR:0",
   1101                     Some("single 2024-09-02T14:29:52.875253314Z"),
   1102                     "2024-09-02",
   1103                     Some(("DE89500105173198527518", "Grothoff Hans")),
   1104                 ),
   1105                 tx_out(
   1106                     (
   1107                         Some("YF5QBARGQ0MNY0VK59S477VDG4"),
   1108                         Some("YF5QBARGQ0MNY0VK59S477VDG4"),
   1109                         Some("2024041810552821000"),
   1110                     ),
   1111                     "EUR:1.1",
   1112                     "EUR:0",
   1113                     Some("Simple tx"),
   1114                     "2024-04-18",
   1115                     Some(("DE20500105172419259181", "John Smith")),
   1116                 ),
   1117             ],
   1118         )
   1119     }
   1120 
   1121     #[test]
   1122     fn gls_camt054() {
   1123         check_tx(
   1124             "sample/platform/gls_camt054.xml",
   1125             "DE84500105177118117964",
   1126             Some("EUR"),
   1127             &[tx_in(
   1128                 (None, Some("IS11PGENODEFF2DA8899900378806"), None),
   1129                 "EUR:2.5",
   1130                 "EUR:0",
   1131                 Some("Test ICT"),
   1132                 "2024-05-05",
   1133                 Some(("DE84500105177118117964", "Mr Test")),
   1134             )],
   1135         );
   1136     }
   1137 
   1138     #[test]
   1139     fn maerki_baumann_camt053() {
   1140         check_tx(
   1141             "sample/platform/maerki_baumann_camt053.xml",
   1142             "CH7389144832588726658",
   1143             Some("CHF"),
   1144             &[
   1145                 tx_in(
   1146                     (
   1147                         Some("adbe4a5a-6cea-4263-b259-8ab964561a32"),
   1148                         Some("41103099704.0002"),
   1149                         Some("ZV20241104/765446/1"),
   1150                     ),
   1151                     "CHF:1",
   1152                     "CHF:0.2",
   1153                     Some("SFHP6H24C16A5J05Q3FJW2XN1PB3EK70ZPY 5SJ30ADGY68FWN68G"),
   1154                     "2024-11-04",
   1155                     Some(("CH7389144832588726658", "Mr Test")),
   1156                 ),
   1157                 tx_in(
   1158                     (
   1159                         Some("7371795e-62fa-42dd-93b7-da89cc120faa"),
   1160                         Some("41103099704.0003"),
   1161                         Some("ZV20241104/765447/1"),
   1162                     ),
   1163                     "CHF:1",
   1164                     "CHF:0.2",
   1165                     Some("Random subject"),
   1166                     "2024-11-04",
   1167                     Some(("CH7389144832588726658", "Mr Test")),
   1168                 ),
   1169                 tx_in(
   1170                     (None, Some("50523424675.0001"), Some("ZV20250523/851716/1")),
   1171                     "CHF:0.5",
   1172                     "CHF:0.2",
   1173                     None,
   1174                     "2025-05-23",
   1175                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1176                 ),
   1177                 tx_out(
   1178                     (
   1179                         Some("BATCH_SINGLE_REPORTING"),
   1180                         Some("5IBJZOWESQGPCSOXSNNBBY49ZURI5W7Q4H"),
   1181                         Some("ZV20241121/773541/1"),
   1182                     ),
   1183                     "CHF:0.1",
   1184                     "CHF:0",
   1185                     Some("multi 0 2024-11-21T15:21:59.8859234 63Z"),
   1186                     "2024-11-27",
   1187                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1188                 ),
   1189                 tx_out(
   1190                     (
   1191                         Some("BATCH_SINGLE_REPORTING"),
   1192                         Some("XZ15UR0XU52QWI7Q4XB88EDS44PLH7DYXH"),
   1193                         Some("ZV20241121/773541/4"),
   1194                     ),
   1195                     "CHF:0.13",
   1196                     "CHF:0",
   1197                     Some("multi 3 2024-11-21T15:21:59.8859234 63Z"),
   1198                     "2024-11-27",
   1199                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1200                 ),
   1201                 tx_out(
   1202                     (
   1203                         Some("BATCH_SINGLE_REPORTING"),
   1204                         Some("A09R35EW0359SZ51464E7TC37A0P2CBK04"),
   1205                         Some("ZV20241121/773541/3"),
   1206                     ),
   1207                     "CHF:0.12",
   1208                     "CHF:0",
   1209                     Some("multi 2 2024-11-21T15:21:59.8859234 63Z"),
   1210                     "2024-11-27",
   1211                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1212                 ),
   1213                 tx_out(
   1214                     (
   1215                         Some("BATCH_SINGLE_REPORTING"),
   1216                         Some("UYXZ78LE9KAIMBY6UNXFYT1K8KNY8VLZLT"),
   1217                         Some("ZV20241121/773541/2"),
   1218                     ),
   1219                     "CHF:0.11",
   1220                     "CHF:0",
   1221                     Some("multi 1 2024-11-21T15:21:59.8859234 63Z"),
   1222                     "2024-11-27",
   1223                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1224                 ),
   1225                 tx_in(
   1226                     (
   1227                         Some("f203fbb4-6e13-4c78-9b2a-d852fea6374a"),
   1228                         Some("41202060702.0001"),
   1229                         Some("ZV20241202/778108/1"),
   1230                     ),
   1231                     "CHF:0.05",
   1232                     "CHF:0.2",
   1233                     Some("mini"),
   1234                     "2024-12-02",
   1235                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1236                 ),
   1237                 tx_in(
   1238                     (
   1239                         Some("81b0d8c6-a677-4577-b75e-a639dcc03681"),
   1240                         Some("41120636093.0001"),
   1241                         Some("ZV20241121/773118/1"),
   1242                     ),
   1243                     "CHF:0.1",
   1244                     "CHF:0.2",
   1245                     Some("small transfer test"),
   1246                     "2024-11-21",
   1247                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1248                 ),
   1249                 tx_out(
   1250                     (None, None, Some("GB20241220/205792/1")),
   1251                     "CHF:3000",
   1252                     "CHF:0",
   1253                     None,
   1254                     "2024-12-20",
   1255                     None,
   1256                 ),
   1257                 tx_in(
   1258                     (None, None, Some("ZV20250114/796191/1")),
   1259                     "CHF:3003",
   1260                     "CHF:0",
   1261                     Some("Fix bad payment by MB."),
   1262                     "2025-01-27",
   1263                     None,
   1264                 ),
   1265                 tx_in(
   1266                     (None, Some("F000787951230001"), Some("ZV20250526/852733/1")),
   1267                     "CHF:1.38",
   1268                     "CHF:0.2",
   1269                     Some("Taler XT3D9MADR4V85JBWX47SMJFDQD2FDZDHHPH8R25YDG1KNVTSEH6G"),
   1270                     "2025-05-26",
   1271                     Some(("DE20500105172419259181", "Mr German")),
   1272                 ),
   1273             ],
   1274         )
   1275     }
   1276 }