libeufin

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

fetch.rs (70214B)


      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::{
     21     collections::BTreeMap,
     22     io::{Cursor, Read as _},
     23 };
     24 
     25 use anyhow::{anyhow, bail};
     26 use compact_str::CompactStringExt;
     27 use jiff::{SignedDuration, Timestamp, Zoned, tz::TimeZone};
     28 use libeufin_ebics::{
     29     db::{get_task_status, update_task_status},
     30     ebics::{
     31         EbicsClient, EbicsErrKind,
     32         ebics_code::EbicsReturnCode,
     33         order::{Order, OrderDoc},
     34     },
     35     iso20022::{
     36         HacAction,
     37         camt::{AccountId, parse_camt},
     38         hac::parse_hac,
     39         model::{InTx, OutBatch, OutTx, Tx},
     40         pain002::parse_pain002,
     41         status_code::{PaymentGroupStatus, PaymentTransactionStatus},
     42     },
     43     keys::{BankKeys, ClientKeys},
     44     ws::listen_for_notification,
     45 };
     46 use sqlx::PgPool;
     47 use taler_api::subject::{
     48     IncomingSubject, parse_incoming_unstructured, parse_outgoing, subject_is_qr_bill,
     49 };
     50 use taler_common::types::{amount::Currency, payto::ParsedQuery};
     51 use tokio::{time::timeout, try_join};
     52 use tracing::{debug, error, info, trace, warn};
     53 
     54 use crate::{
     55     config::{AccountType, NexusCfg, NexusIngestCfg},
     56     constants::{CHECKPOINT_KEY, FETCH_TASK_KEY},
     57     db::{
     58         initiated::{
     59             batch_status_update, order_failure, order_step, order_success, tx_status_update,
     60             unsettled_tx_in_batch,
     61         },
     62         payment::{
     63             InResult, IncomingBounceRegistrationResult, IncomingRegistrationResult,
     64             OutgoingRegistrationResult, register_in, register_in_malformed, register_in_qr_bill,
     65             register_in_talerable, register_out_tx,
     66         },
     67     },
     68     model::SubmissionState,
     69     rand_ebics_id,
     70 };
     71 
     72 pub async fn ebics_fetch(
     73     ebics: &EbicsClient<'_>,
     74     cfg: &NexusCfg,
     75     client: &ClientKeys,
     76     bank: &BankKeys,
     77     db: &PgPool,
     78     documents: &[OrderDoc],
     79     pinned_start: &Option<Timestamp>,
     80     peek: bool,
     81     transient: bool,
     82     transient_checkpoint: bool,
     83 ) -> anyhow::Result<()> {
     84     let ebics_cfg = cfg.ebics()?;
     85     let register_payload = async |doc: &OrderDoc, content: &[u8]| -> anyhow::Result<()> {
     86         // Unzip payload if necessary
     87         match doc {
     88             OrderDoc::acknowledgement => register_file(db, cfg, doc, content).await,
     89             OrderDoc::status | OrderDoc::report | OrderDoc::statement | OrderDoc::notification => {
     90                 let mut z = zip::ZipArchive::new(Cursor::new(content))?;
     91                 for i in 0..z.len() {
     92                     let mut file = z.by_index(i)?;
     93                     trace!(target: "fetch", "parse {}", file.name()?);
     94                     let mut buf = Vec::new();
     95                     file.read_to_end(&mut buf)?;
     96                     register_file(db, cfg, doc, &buf).await?;
     97                 }
     98                 Ok(())
     99             }
    100         }
    101     };
    102     let fetch = async |orders: &[Order], since: Option<Timestamp>| -> anyhow::Result<bool> {
    103         let mut grouped_orders = BTreeMap::new();
    104 
    105         for order in orders {
    106             grouped_orders
    107                 .entry(order.doc())
    108                 .or_insert_with(Vec::new)
    109                 .push(order);
    110         }
    111 
    112         let mut success = true;
    113         for (doc, orders) in grouped_orders {
    114             if let Some(doc) = doc {
    115                 for order in orders {
    116                     if let Err(e) = ebics
    117                         .download(
    118                             db,
    119                             client,
    120                             bank,
    121                             order,
    122                             &since.map(|it| (it, Timestamp::now())),
    123                             transient && peek,
    124                             async |content| {
    125                                 register_payload(&doc, &content)
    126                                     .await
    127                                     .map_err(|e| EbicsErrKind::Custom(e.to_string().into()))
    128                             },
    129                         )
    130                         .await
    131                     {
    132                         if let EbicsErrKind::Code { bank, .. } = e.kind {
    133                             match bank {
    134                                 EbicsReturnCode::EBICS_NO_DOWNLOAD_DATA_AVAILABLE => continue,
    135                                 EbicsReturnCode::EBICS_AUTHORISATION_ORDER_IDENTIFIER_FAILED => {
    136                                     error!(target: "fetch", "{e}");
    137                                     success = false;
    138                                     continue;
    139                                 }
    140                                 _ => {}
    141                             }
    142                         }
    143                         return Err(e.into());
    144                     }
    145                 }
    146             } else {
    147                 debug!(target: "fetch", "Skip unsupported orders {orders:?}")
    148             }
    149         }
    150         Ok(success)
    151     };
    152 
    153     // EBICS order than should be fetched
    154     let orders: Vec<_> = if documents.is_empty() {
    155         OrderDoc::entries
    156     } else {
    157         documents
    158     }
    159     .iter()
    160     .flat_map(|it| ebics_cfg.dialect.standard().downloads(it))
    161     .collect();
    162 
    163     let fetch_cfg = cfg.fetch()?;
    164 
    165     let (sender, mut receiver) = tokio::sync::mpsc::channel::<Vec<Order>>(10);
    166 
    167     let fetch = async {
    168         if transient {
    169             info!(target: "fetch", "Transient mode: fetching once and returning");
    170         } else {
    171             info!(target: "fetch", "Running with a frequency of {}", fetch_cfg.frequency_raw);
    172         }
    173 
    174         let mut last_fetch = Timestamp::UNIX_EPOCH;
    175         loop {
    176             let now = Timestamp::now();
    177             let checkpoint = get_task_status(db, CHECKPOINT_KEY)
    178                 .await?
    179                 .unwrap_or_default();
    180             let next_fetch = last_fetch + fetch_cfg.frequency;
    181             let next_checkpoint = {
    182                 if let Some(last_trial) = checkpoint.last_trial {
    183                     // We run today at checkpoint_time
    184                     let checkpoint_date = Zoned::new(now, TimeZone::UTC)
    185                         .with()
    186                         .time(fetch_cfg.checkpoint_time)
    187                         .build()
    188                         .unwrap();
    189                     // If we already ran today we ran tomorrow
    190                     if last_trial > checkpoint_date.timestamp() {
    191                         checkpoint_date.tomorrow().unwrap().timestamp()
    192                     } else {
    193                         checkpoint_date.timestamp()
    194                     }
    195                 } else {
    196                     // We never ran, we must checkpoint now
    197                     Timestamp::UNIX_EPOCH
    198                 }
    199             };
    200 
    201             let mut success = true;
    202             if
    203             // Run transient checkpoint at request
    204             (transient && transient_checkpoint)
    205                 // Or run recurrent checkpoint
    206                 || (!transient && now > next_checkpoint)
    207             {
    208                 info!(target: "fetch", "Running checkpoint");
    209 
    210                 let since = if let Some(pinned_start) = pinned_start
    211                     && transient
    212                     && checkpoint
    213                         .last_successfull
    214                         .map(|it| *pinned_start <= it)
    215                         .unwrap_or(true)
    216                 {
    217                     Some(*pinned_start)
    218                 } else {
    219                     checkpoint.last_successfull
    220                 };
    221                 let res = async {
    222                     // We fetch HKD to only fetch supported EBICS orders and get the document versions
    223                     let hkd = ebics.hkd(db, client, bank, false).await?;
    224                     let mut supported_orders = hkd
    225                         .partner
    226                         .orders
    227                         .into_iter()
    228                         .map(|it| it.order)
    229                         .collect::<Vec<_>>();
    230                     debug!(target: "fetch", "HKD: {}", supported_orders.iter().map(|it| it.to_string()).join_compact(", "));
    231                     supported_orders
    232                         .retain(|order| orders.iter().find(|it| order.eq(it)).is_some());
    233                     fetch(&supported_orders, since).await
    234                 }
    235                 .await;
    236                 if let Err(e) = res {
    237                     success = false;
    238                     error!(target: "fetch", "{e}");
    239                 }
    240                 try_join!(
    241                     update_task_status(db, CHECKPOINT_KEY, &now, success),
    242                     update_task_status(db, FETCH_TASK_KEY, &now, success)
    243                 )?;
    244                 last_fetch = now;
    245             } else if transient || now > next_fetch {
    246                 if !transient {
    247                     info!(target: "fetch", "Running at frequency");
    248                 }
    249                 let res = async {
    250                     // We fetch HAA to only fetch pending & supported EBICS orders and get the document versions
    251                     let mut haa = ebics.haa(db, client, bank, false).await?;
    252                     debug!(target: "fetch", "HAA: {}", haa.orders.iter().map(|it| it.to_string()).join_compact(", "));
    253                     haa.orders
    254                         .retain(|order| orders.iter().find(|it| order.eq(it)).is_some());
    255                     fetch(&haa.orders, *pinned_start).await
    256                 }
    257                 .await;
    258                 if let Err(e) = res {
    259                     success = false;
    260                     error!(target: "fetch", "{e}");
    261                 }
    262                 update_task_status(db, FETCH_TASK_KEY, &now, success).await?;
    263                 last_fetch = now;
    264             }
    265 
    266             if transient {
    267                 if success {
    268                     return anyhow::Ok(());
    269                 } else {
    270                     return Err(anyhow!("fetch failed"));
    271                 }
    272             }
    273 
    274             let delay = now
    275                 .duration_until(next_fetch.min(next_checkpoint))
    276                 .max(SignedDuration::ZERO)
    277                 .unsigned_abs();
    278             let tx = timeout(delay, receiver.recv()).await;
    279             if let Ok(Some(mut notification)) = tx {
    280                 notification.retain(|order| orders.iter().find(|it| order.eq(it)).is_some());
    281                 if !notification.is_empty() {
    282                     info!(target: "fetch", "Running at real-time notifications reception");
    283                     if let Err(e) = fetch(&notification, None).await {
    284                         error!(target: "fetch", "{e}");
    285                     }
    286                 }
    287             }
    288         }
    289     };
    290 
    291     if transient {
    292         fetch.await?;
    293     } else {
    294         tokio::try_join!(fetch, async {
    295             listen_for_notification(ebics, db, client, bank, sender).await;
    296             Ok(())
    297         })?;
    298     }
    299 
    300     Ok(())
    301 }
    302 
    303 async fn register_file(
    304     db: &PgPool,
    305     cfg: &NexusCfg,
    306     doc: &OrderDoc,
    307     xml: &[u8],
    308 ) -> anyhow::Result<()> {
    309     match doc {
    310         OrderDoc::acknowledgement => {
    311             for ack in parse_hac(xml)? {
    312                 debug!(target: "fetch", "{ack}");
    313                 if let Some(order_id) = &ack.order_id {
    314                     match ack.action {
    315                         HacAction::ORDER_HAC_FINAL_POS => {
    316                             if let Some(msg_id) = order_success(db, order_id).await? {
    317                                 info!(target: "fetch", "Batch {msg_id} order {order_id} accepted at {}", ack.timestamp);
    318                             }
    319                         }
    320                         HacAction::ORDER_HAC_FINAL_NEG => {
    321                             if let Some((msg_id, msg)) = order_failure(db, order_id).await? {
    322                                 info!(target: "fetch", "Batch {msg_id} order {order_id} refused at {}{}", ack.timestamp, std::fmt::from_fn( |f| if let Some(msg) = &msg {
    323                                     write!(f, ": {msg}")
    324                                 } else {
    325                                     Ok(())
    326                                 }));
    327                             }
    328                         }
    329                         _ => {
    330                             order_step(db, order_id, &ack.to_string()).await?;
    331                         }
    332                     }
    333                 }
    334             }
    335         }
    336         OrderDoc::status => {
    337             let msg_status = parse_pain002(xml)?;
    338             debug!(target: "fetch", "{msg_status}");
    339             if let Some(code) = msg_status.status {
    340                 let msg = msg_status.msg();
    341                 batch_status_update(
    342                     db,
    343                     &msg_status.id,
    344                     match code {
    345                         PaymentGroupStatus::AcceptedSettlementCompletedDebitorAccount => {
    346                             SubmissionState::success
    347                         }
    348                         PaymentGroupStatus::Rejected => {
    349                             error!(target: "fetch", "Batch {} failed: {msg}", msg_status.id);
    350                             SubmissionState::permanent_failure
    351                         }
    352                         _ => SubmissionState::pending,
    353                     },
    354                     &msg,
    355                 )
    356                 .await?;
    357             }
    358             for p_status in msg_status.payments {
    359                 if p_status.id != "NOTPROVIDED" {
    360                     warn!(target: "fetch", "Unexpected payment status for {}.{}", msg_status.id, p_status.id);
    361                 } else if let Some(code) = p_status.status {
    362                     let msg = p_status.msg();
    363                     batch_status_update(
    364                         db,
    365                         &msg_status.id,
    366                         match code {
    367                             PaymentGroupStatus::AcceptedSettlementCompletedDebitorAccount => {
    368                                 SubmissionState::success
    369                             }
    370                             PaymentGroupStatus::Rejected => {
    371                                 error!(target: "fetch", "Batch {} failed: {msg}", msg_status.id);
    372                                 SubmissionState::permanent_failure
    373                             }
    374                             _ => SubmissionState::pending,
    375                         },
    376                         &msg,
    377                     )
    378                     .await?;
    379                 }
    380                 for tx_status in p_status.txs {
    381                     let msg = tx_status.msg();
    382                     tx_status_update(
    383                         db,
    384                         &tx_status.e2e_id,
    385                         &msg_status.id,
    386                         match tx_status.status {
    387                             PaymentTransactionStatus::Rejected | PaymentTransactionStatus::Blocked => {
    388                                 error!(target: "fetch", "Transaction {} failed: {msg}", tx_status.e2e_id);
    389                                 SubmissionState::permanent_failure
    390                             }
    391                             _ => SubmissionState::pending
    392                         },
    393                         &msg,
    394                     )
    395                     .await?;
    396                 }
    397             }
    398         }
    399         OrderDoc::report | OrderDoc::statement | OrderDoc::notification => {
    400             register_camt(db, cfg, xml).await?;
    401         }
    402     }
    403     Ok(())
    404 }
    405 
    406 pub async fn register_camt(db: &PgPool, cfg: &NexusCfg, xml: &[u8]) -> anyhow::Result<usize> {
    407     let account = &cfg.ebics()?.account;
    408     let ingest_cfg = cfg.ingest()?;
    409     let mut nb_tx = 0;
    410     for actx in parse_camt(xml)? {
    411         if let AccountId::Iban(iban) = &actx.id
    412             && iban == &account.iban
    413         {
    414             if let Some(currency) = actx.currency
    415                 && currency != cfg.currency
    416             {
    417                 bail!(
    418                     "Expected transactions of currency {} got {currency}",
    419                     cfg.currency
    420                 )
    421             }
    422             for tx in actx.txs {
    423                 match tx {
    424                     Tx::In(InTx { amount, .. }) | Tx::Out(OutTx { amount, .. }) => {
    425                         if amount.currency != cfg.currency {
    426                             bail!(
    427                                 "Expected transactions of currency {} got {}",
    428                                 cfg.currency,
    429                                 amount.currency
    430                             )
    431                         }
    432                     }
    433                     Tx::Batch(_) | Tx::Reversal(_) => {}
    434                 }
    435                 register_tx(db, &ingest_cfg, &tx).await?;
    436                 nb_tx += 1;
    437             }
    438         } else {
    439             warn!(target: "fetch", "Skip transaction for unknown account {}", actx.id);
    440         }
    441     }
    442     Ok(nb_tx)
    443 }
    444 
    445 pub async fn register_incoming(
    446     db: &PgPool,
    447     cfg: &NexusIngestCfg,
    448     payment: &InTx,
    449 ) -> sqlx::Result<()> {
    450     let log_res = |res: InResult, kind: &str, suffix: &str| {
    451         let fmt = std::fmt::from_fn(|f| {
    452             write!(f, "{payment}")?;
    453             if !kind.is_empty() {
    454                 write!(f, " {kind}")?;
    455             }
    456             if res.new {
    457                 if let Some(id) = &res.bounce_id {
    458                     write!(f, " bounced in {id}")?;
    459                 }
    460             } else {
    461                 if res.completed {
    462                     f.write_str(" completed")?;
    463                     if let Some(id) = &res.bounce_id {
    464                         write!(f, " bounced in {id}")?;
    465                     }
    466                 } else {
    467                     if let Some(id) = &res.bounce_id {
    468                         write!(f, " already bounced in {id}")?;
    469                     }
    470                 }
    471             }
    472             if !suffix.is_empty() {
    473                 write!(f, " {suffix}")?;
    474             }
    475             Ok(())
    476         });
    477 
    478         if res.completed || res.new {
    479             info!(target: "fetch", "{fmt}")
    480         } else {
    481             debug!(target: "fetch", "{fmt}")
    482         }
    483     };
    484     let bounce = async |cause: &str| {
    485         match cfg.account_type {
    486             AccountType::exchange => {
    487                 if payment.execution_time < cfg.ignore_bounces_before {
    488                     let res = register_in(db, payment).await?;
    489                     log_res(res, "", &format!("ignored bounce: {cause}"));
    490                 } else {
    491                     let mut bounce_amount = payment.amount;
    492                     if !payment.credit_fee.is_zero() && cfg.bounce_deduce_fee {
    493                         if let Some(res) = bounce_amount.try_sub(&payment.credit_fee) {
    494                             bounce_amount = res
    495                         } else {
    496                             let res = register_in(db, payment).await?;
    497                             log_res(
    498                                 res,
    499                                 "",
    500                                 &format!("skip bounce (transfer fee higher than amount): {cause}"),
    501                             );
    502                             return Ok(());
    503                         }
    504                     }
    505                     if let Some(res) = bounce_amount.try_sub(&cfg.bounce_fee) {
    506                         bounce_amount = res
    507                     } else {
    508                         let res = register_in(db, payment).await?;
    509                         log_res(
    510                             res,
    511                             "",
    512                             &format!("skip bounce (bounce fee higher than amount): {cause}"),
    513                         );
    514                         return Ok(());
    515                     }
    516                     let res = register_in_malformed(
    517                         db,
    518                         payment,
    519                         &bounce_amount,
    520                         &rand_ebics_id(),
    521                         &Timestamp::now(),
    522                         cause,
    523                     )
    524                     .await?;
    525                     match res {
    526                         IncomingBounceRegistrationResult::Talerable => {
    527                             warn!(target: "fetch", "{payment} tried to bounce a talerable transaction");
    528                         }
    529                         IncomingBounceRegistrationResult::Success(res) => {
    530                             log_res(res, "", &format!(": {cause}"));
    531                         }
    532                     }
    533                 }
    534             }
    535             AccountType::normal => {
    536                 let res = register_in(db, payment).await?;
    537                 log_res(res, "", "");
    538             }
    539         }
    540         sqlx::Result::<_, sqlx::Error>::Ok(())
    541     };
    542 
    543     // Check we have enough info to handle this transaction
    544     if match &payment.debtor {
    545         Some(payto) => match payto.query::<ParsedQuery>() {
    546             Ok(q) => q.receiver_name.is_none(),
    547             Err(_) => true,
    548         },
    549         None => true,
    550     } {
    551         let res = register_in(db, payment).await?;
    552         log_res(res, "incomplete", "");
    553         return Ok(());
    554     }
    555     if let Some(regex) = &cfg.restriction_payto_regex
    556         && let Some(debtor) = &payment.debtor
    557         && !regex
    558             .find(debtor.as_ref().as_str())
    559             .is_some_and(|m| m.as_str() == debtor.as_ref().as_str())
    560     {
    561         bounce("restricted account").await?;
    562         return Ok(());
    563     }
    564 
    565     if let Some(subject) = &payment.subject
    566         && subject_is_qr_bill(subject)
    567     {
    568         match register_in_qr_bill(db, payment, subject).await? {
    569             IncomingRegistrationResult::ReservePubReuse => bounce("reverse pub reuse").await?,
    570             IncomingRegistrationResult::MappingReuse => bounce("mapping reuse").await?,
    571             IncomingRegistrationResult::UnknownMapping => bounce("unknown mapping").await?,
    572             IncomingRegistrationResult::Success(res) => {
    573                 log_res(res, "", "");
    574             }
    575         }
    576     } else {
    577         match parse_incoming_unstructured(payment.subject.as_deref().unwrap_or_default()) {
    578             Ok(IncomingSubject::AdminBalanceAdjust) => {
    579                 let res = register_in(db, payment).await?;
    580                 log_res(res, "admin balance adjust", "");
    581             }
    582             Ok(IncomingSubject::Key(subject)) => match register_in_talerable(db, payment, &subject)
    583                 .await?
    584             {
    585                 IncomingRegistrationResult::ReservePubReuse => bounce("reverse pub reuse").await?,
    586                 IncomingRegistrationResult::MappingReuse => bounce("mapping reuse").await?,
    587                 IncomingRegistrationResult::UnknownMapping => bounce("unknown mapping").await?,
    588                 IncomingRegistrationResult::Success(res) => {
    589                     log_res(res, "", "");
    590                 }
    591             },
    592             Err(e) => {
    593                 bounce(&e.to_string()).await?;
    594             }
    595         }
    596     }
    597 
    598     Ok(())
    599 }
    600 
    601 pub async fn register_outgoing(
    602     db: &PgPool,
    603     payment: &OutTx,
    604 ) -> sqlx::Result<OutgoingRegistrationResult> {
    605     let metadata = payment
    606         .subject
    607         .as_ref()
    608         .and_then(|s| parse_outgoing(s).ok());
    609     let res = register_out_tx(db, payment, metadata.as_ref()).await?;
    610     if res.new {
    611         if res.initiated {
    612             info!(target: "fetch", "{payment}");
    613         } else {
    614             warn!(target: "fetch", "{payment} recovered");
    615         }
    616     } else {
    617         debug!(target: "fetch", "{payment} already seen");
    618     }
    619     Ok(res)
    620 }
    621 
    622 pub async fn register_outgoing_batch(
    623     db: &PgPool,
    624     currency: &Currency,
    625     batch: &OutBatch,
    626 ) -> sqlx::Result<()> {
    627     info!(target: "fetch", "{batch}");
    628     let txs = unsettled_tx_in_batch(db, currency, &batch.msg_id, &batch.execution_time).await?;
    629     for tx in txs {
    630         register_outgoing(db, &tx).await?;
    631     }
    632     Ok(())
    633 }
    634 
    635 pub async fn register_tx(db: &PgPool, cfg: &NexusIngestCfg, tx: &Tx) -> sqlx::Result<()> {
    636     if tx.execution_time() < &cfg.ignore_txs_before {
    637         debug!(target: "fetch", "IGNORE {tx}");
    638     } else {
    639         match tx {
    640             Tx::In(payment) => {
    641                 register_incoming(db, cfg, payment).await?;
    642             }
    643             Tx::Out(payment) => {
    644                 register_outgoing(db, payment).await?;
    645             }
    646             Tx::Batch(batch) => {
    647                 register_outgoing_batch(db, &cfg.currency, batch).await?;
    648             }
    649             Tx::Reversal(reversal) => {
    650                 error!(target: "fetch", "{reversal}");
    651                 tx_status_update(
    652                     db,
    653                     &reversal.e2e_id,
    654                     reversal.msg_id.as_deref().unwrap_or_default(),
    655                     SubmissionState::permanent_failure,
    656                     &format!("Payment bounces: {}", reversal.reason),
    657                 )
    658                 .await?;
    659             }
    660         }
    661     }
    662     Ok(())
    663 }
    664 
    665 #[cfg(test)]
    666 pub mod test {
    667     use std::str::FromStr as _;
    668 
    669     use compact_str::CompactString;
    670     use indexmap::IndexMap;
    671     use jiff::{Timestamp, civil::Date};
    672     use libeufin_ebics::{
    673         dialect::Dialect,
    674         ebics::order::OrderDoc,
    675         iso20022::model::{InId, InTx, OutId, OutTx},
    676     };
    677     use sqlx::{PgPool, Row as _, postgres::PgRow};
    678     use taler_api::db::TypeHelper;
    679     use taler_common::{
    680         config::Config,
    681         types::{
    682             amount::{Amount, Currency, amount},
    683             payto::{BankID, IbanPayto, PaytoImpl, PaytoURI},
    684             utils::date_to_utc_ts,
    685         },
    686     };
    687     use taler_macros::db_test;
    688 
    689     use crate::{
    690         batch_pain001,
    691         config::NexusCfg,
    692         constants::CONFIG_SOURCE,
    693         db::initiated::{batch_initiated, batch_sub_success, initiate, initiated_submittable},
    694         fetch::register_file,
    695         gen_init_pay,
    696         model::{
    697             Initiated, PaymentBatch,
    698             SubmissionState::{self, *},
    699         },
    700     };
    701 
    702     fn setup() -> NexusCfg {
    703         setup_cfg("test.conf")
    704     }
    705 
    706     fn setup_cfg(name: &str) -> NexusCfg {
    707         let cfg = Config::load(CONFIG_SOURCE, Some(format!("conf/{name}"))).unwrap();
    708         NexusCfg::parse(&cfg).unwrap()
    709     }
    710 
    711     fn iban_payto(iban: impl AsRef<str>, name: impl AsRef<str>) -> PaytoURI {
    712         IbanPayto::new(BankID {
    713             iban: iban.as_ref().parse().expect("invalid IBAN"),
    714             bic: None,
    715         })
    716         .as_full_uri(name.as_ref())
    717     }
    718 
    719     fn tx_in(
    720         id: (Option<&str>, Option<&str>, Option<&str>),
    721         amount: &str,
    722         credit_fee: &str,
    723         subject: Option<&str>,
    724         execution_time: &str,
    725         debtor: Option<(&str, &str)>,
    726     ) -> InTx {
    727         InTx {
    728             id: InId::new(
    729                 id.0.map(|it| it.parse().unwrap()),
    730                 id.1.map(Into::into),
    731                 id.2.map(Into::into),
    732             ),
    733             amount: Amount::from_str(amount).unwrap(),
    734             credit_fee: Amount::from_str(credit_fee).unwrap(),
    735             subject: subject.map(Into::into),
    736             execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()),
    737             debtor: debtor.map(|(iban, name)| iban_payto(iban, name)),
    738         }
    739     }
    740 
    741     pub fn tx_out(
    742         id: (Option<&str>, Option<&str>, Option<&str>),
    743         amount: &str,
    744         debit_fee: &str,
    745         subject: Option<&str>,
    746         execution_time: &str,
    747         creditor: Option<(&str, &str)>,
    748     ) -> OutTx {
    749         OutTx {
    750             id: OutId::new(
    751                 id.0.map(Into::into),
    752                 id.1.map(Into::into),
    753                 id.2.map(Into::into),
    754             ),
    755             amount: Amount::from_str(amount).unwrap(),
    756             debit_fee: Amount::from_str(debit_fee).unwrap(),
    757             subject: subject.map(Into::into),
    758             execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()),
    759             creditor: creditor.map(|(iban, name)| iban_payto(iban, name)),
    760         }
    761     }
    762 
    763     /** Register batches of initiated payments for reconcciliation */
    764     async fn batches(pool: &PgPool, batches: &[(&str, &[Initiated])]) -> Vec<PaymentBatch> {
    765         let mut tmp = Vec::<PaymentBatch>::new();
    766         for (name, txs) in batches {
    767             for tx in *txs {
    768                 initiate(
    769                     pool,
    770                     &tx.amount,
    771                     &tx.subject,
    772                     &tx.creditor,
    773                     &tx.initiation_time,
    774                     &tx.e2e_id,
    775                 )
    776                 .await
    777                 .unwrap();
    778             }
    779             batch_initiated(pool, &Timestamp::now(), name, false)
    780                 .await
    781                 .unwrap();
    782             for batch in initiated_submittable(pool, &Currency::CHF).await.unwrap() {
    783                 batch_sub_success(
    784                     pool,
    785                     batch.id,
    786                     &Timestamp::now(),
    787                     &name.replace("BATCH", "ORDER"),
    788                 )
    789                 .await
    790                 .unwrap();
    791                 tmp.push(batch);
    792             }
    793         }
    794         tmp
    795     }
    796 
    797     /** Register an XML sample into the database */
    798     async fn register(pool: &PgPool, cfg: &NexusCfg, path: &str, doc: &OrderDoc) {
    799         register_file(
    800             pool,
    801             cfg,
    802             doc,
    803             &std::fs::read(format!("../libeufin-ebics/sample/platform/{path}")).unwrap(),
    804         )
    805         .await
    806         .unwrap()
    807     }
    808 
    809     /** Check database content */
    810     async fn check(
    811         db: &PgPool,
    812         status: &[(
    813             &'static str,
    814             SubmissionState,
    815             &[(&'static str, SubmissionState)],
    816         )],
    817         incoming: &[InTx],
    818         outgoing: &[OutTx],
    819         bounced: &[OutTx],
    820     ) {
    821         // Check batch status
    822         let batch_status = sqlx::query_as::<_, (CompactString, SubmissionState)>(
    823             "SELECT message_id, status FROM initiated_outgoing_batches ORDER BY initiated_outgoing_batch_id"
    824         ).fetch_all(db).await.unwrap();
    825 
    826         assert_eq!(
    827             status
    828                 .iter()
    829                 .map(|it| (it.0.into(), it.1))
    830                 .collect::<Vec<_>>(),
    831             batch_status
    832         );
    833 
    834         // Check transactions status
    835         let batch_tx = sqlx::query_as::<_, (CompactString, CompactString, SubmissionState)>(
    836             "
    837             SELECT message_id, end_to_end_id, initiated_outgoing_transactions.status
    838             FROM initiated_outgoing_transactions
    839                 JOIN initiated_outgoing_batches USING (initiated_outgoing_batch_id)
    840             ORDER BY initiated_outgoing_batch_id, initiated_outgoing_transaction_id
    841             ",
    842         )
    843         .fetch_all(db)
    844         .await
    845         .unwrap()
    846         .into_iter()
    847         .fold(
    848             IndexMap::<_, Vec<_>>::new(),
    849             |mut outer, (key, value, third)| {
    850                 outer.entry(key).or_default().push((value, third));
    851                 outer
    852             },
    853         );
    854         pretty_assertions::assert_eq!(
    855             status
    856                 .iter()
    857                 .map(|it| (it.0, it.2.to_vec()))
    858                 .collect::<Vec<_>>(),
    859             batch_tx
    860                 .iter()
    861                 .map(|it| (
    862                     it.0.as_str(),
    863                     it.1.iter()
    864                         .map(|it| (it.0.as_str(), it.1))
    865                         .collect::<Vec<_>>()
    866                 ))
    867                 .collect::<Vec<_>>()
    868         );
    869 
    870         // Check incoming transactions
    871         let incoming_tx = sqlx::query(
    872             "
    873             SELECT
    874                  uetr
    875                 ,tx_id
    876                 ,acct_svcr_ref
    877                 ,amount
    878                 ,credit_fee
    879                 ,subject
    880                 ,execution_time
    881                 ,debit_payto
    882             FROM incoming_transactions
    883             ORDER BY incoming_transaction_id
    884             ",
    885         )
    886         .try_map(|r: PgRow| {
    887             Ok(InTx {
    888                 id: InId::new(
    889                     r.try_get("uetr")?,
    890                     r.try_get("tx_id")?,
    891                     r.try_get("acct_svcr_ref")?,
    892                 ),
    893                 amount: r.try_get_amount("amount", &Currency::CHF)?,
    894                 credit_fee: r
    895                     .try_get_opt_amount("credit_fee", &Currency::CHF)?
    896                     .unwrap_or(Amount::zero(&Currency::CHF)),
    897                 subject: r.try_get("subject")?,
    898                 execution_time: r.try_get_timestamp("execution_time")?,
    899                 debtor: r.try_get_opt_parse("debit_payto")?,
    900             })
    901         })
    902         .fetch_all(db)
    903         .await
    904         .unwrap();
    905         pretty_assertions::assert_eq!(incoming, incoming_tx);
    906 
    907         // Check outgoing transactions
    908         let outgoing_tx = sqlx::query(
    909             "
    910             SELECT end_to_end_id
    911                 ,acct_svcr_ref
    912                 ,amount
    913                 ,debit_fee
    914                 ,subject
    915                 ,execution_time
    916                 ,credit_payto
    917             FROM outgoing_transactions
    918             ORDER BY outgoing_transaction_id
    919             ",
    920         )
    921         .try_map(|r: PgRow| {
    922             Ok(OutTx {
    923                 id: OutId::new(
    924                     None,
    925                     r.try_get("end_to_end_id")?,
    926                     r.try_get("acct_svcr_ref")?,
    927                 ),
    928                 amount: r.try_get_amount("amount", &Currency::CHF)?,
    929                 debit_fee: r
    930                     .try_get_opt_amount("debit_fee", &Currency::CHF)?
    931                     .unwrap_or(Amount::zero(&Currency::CHF)),
    932                 subject: r.try_get("subject")?,
    933                 execution_time: r.try_get_timestamp("execution_time")?,
    934                 creditor: r.try_get_opt_parse("credit_payto")?,
    935             })
    936         })
    937         .fetch_all(db)
    938         .await
    939         .unwrap();
    940         pretty_assertions::assert_eq!(outgoing, outgoing_tx);
    941 
    942         // Check outgoing transactions
    943         let bounced_tx = sqlx::query(
    944             "
    945             SELECT end_to_end_id
    946                 ,amount
    947                 ,subject
    948                 ,credit_payto
    949             FROM initiated_outgoing_transactions
    950                 JOIN bounced_transactions USING (initiated_outgoing_transaction_id)
    951             ",
    952         )
    953         .try_map(|r: PgRow| {
    954             Ok(OutTx {
    955                 id: OutId::new(None, None, None),
    956                 amount: r.try_get_amount("amount", &Currency::CHF)?,
    957                 debit_fee: Amount::zero(&Currency::CHF),
    958                 subject: r.try_get("subject")?,
    959                 execution_time: Timestamp::UNIX_EPOCH,
    960                 creditor: r.try_get_opt_parse("credit_payto")?,
    961             })
    962         })
    963         .fetch_all(db)
    964         .await
    965         .unwrap();
    966         pretty_assertions::assert_eq!(bounced, bounced_tx)
    967     }
    968 
    969     #[db_test]
    970     async fn pain001(db: PgPool) {
    971         let cfg = setup();
    972         let mut batches = batches(
    973             &db,
    974             &[(
    975                 "MESSAGE_ID",
    976                 &[
    977                     gen_init_pay!("TX_FIRST", "Test 42", amount("EUR:42")),
    978                     gen_init_pay!("TX_SECOND", "Test 5.11", amount("EUR:5.11")),
    979                     gen_init_pay!("TX_THIRD", "Test 0.21", amount("EUR:0.21")),
    980                 ],
    981             )],
    982         )
    983         .await;
    984         let batch = &mut batches[0];
    985         let mut cfg = (*cfg.ebics().unwrap()).clone();
    986         for dialect in Dialect::entries {
    987             cfg.dialect = *dialect;
    988             pretty_assertions::assert_eq!(
    989                 std::fs::read_to_string(format!(
    990                     "../libeufin-ebics/sample/platform/{dialect}_pain001.xml"
    991                 ))
    992                 .unwrap(),
    993                 batch_pain001(
    994                     batch,
    995                     &cfg,
    996                     date_to_utc_ts(&Date::constant(2024, 9, 9)),
    997                     false
    998                 )
    999                 .unwrap()
   1000             );
   1001         }
   1002     }
   1003 
   1004     /** HAC order id test */
   1005     #[db_test]
   1006     async fn hac(db: PgPool) {
   1007         let cfg = setup();
   1008         batches(
   1009             &db,
   1010             &[
   1011                 (
   1012                     "BATCH_SUCCESS",
   1013                     &[
   1014                         gen_init_pay!("BATCH_SUCCESS_0"),
   1015                         gen_init_pay!("BATCH_SUCCESS_1"),
   1016                     ],
   1017                 ),
   1018                 (
   1019                     "BATCH_FAILURE",
   1020                     &[
   1021                         gen_init_pay!("BATCH_FAILURE_0"),
   1022                         gen_init_pay!("BATCH_FAILURE_1"),
   1023                     ],
   1024                 ),
   1025             ],
   1026         )
   1027         .await;
   1028 
   1029         register(&db, &cfg, "hac.xml", &OrderDoc::acknowledgement).await;
   1030 
   1031         check(
   1032             &db,
   1033             &[
   1034                 (
   1035                     "BATCH_SUCCESS",
   1036                     success,
   1037                     &[("BATCH_SUCCESS_0", pending), ("BATCH_SUCCESS_1", pending)],
   1038                 ),
   1039                 (
   1040                     "BATCH_FAILURE",
   1041                     permanent_failure,
   1042                     &[
   1043                         ("BATCH_FAILURE_0", permanent_failure),
   1044                         ("BATCH_FAILURE_1", permanent_failure),
   1045                     ],
   1046                 ),
   1047             ],
   1048             &[],
   1049             &[],
   1050             &[],
   1051         )
   1052         .await;
   1053     }
   1054 
   1055     /** CreditSuisse dialect test */
   1056     #[db_test]
   1057     async fn cs(db: PgPool) {
   1058         let cfg = setup();
   1059         batches(
   1060             &db,
   1061             &[(
   1062                 "05BD4C5B4A2649B5B08F6EF6A31F197A",
   1063                 &[
   1064                     gen_init_pay!("AQCXNCPWD8PHW5JTN65Y5XTF7R"),
   1065                     gen_init_pay!("EE9SX76FC5YSC657EK3GMVZ9TC"),
   1066                     gen_init_pay!("V5B3MXPEWES9VQW1JDRD6VAET4"),
   1067                     gen_init_pay!("M9NGRCAC1FBX3ENX3XEDEPJ2JW"),
   1068                 ],
   1069             )],
   1070         )
   1071         .await;
   1072 
   1073         register(&db, &cfg, "pain002_part.xml", &OrderDoc::status).await;
   1074 
   1075         check(
   1076             &db,
   1077             &[(
   1078                 "05BD4C5B4A2649B5B08F6EF6A31F197A",
   1079                 success,
   1080                 &[
   1081                     ("AQCXNCPWD8PHW5JTN65Y5XTF7R", permanent_failure),
   1082                     ("EE9SX76FC5YSC657EK3GMVZ9TC", permanent_failure),
   1083                     ("V5B3MXPEWES9VQW1JDRD6VAET4", permanent_failure),
   1084                     ("M9NGRCAC1FBX3ENX3XEDEPJ2JW", pending),
   1085                 ],
   1086             )],
   1087             &[],
   1088             &[],
   1089             &[],
   1090         )
   1091         .await;
   1092     }
   1093 
   1094     /** Valiant dialect test */
   1095     #[db_test]
   1096     async fn valiant(db: PgPool) {
   1097         let cfg = setup_cfg("valiant.conf");
   1098         batches(
   1099             &db,
   1100             &[
   1101                 (
   1102                     "MJDJO2BDDBL7YSL2P96SXHG3TQZEZQD26L",
   1103                     &[gen_init_pay!("4UWWIDGTEIGDU6Z721QE95PYJSIEA48PYE")],
   1104                 ),
   1105                 (
   1106                     "5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U",
   1107                     &[
   1108                         gen_init_pay!("SKMU2891PAAYBDW22DBWX2W7KTFZ1CDFO8"),
   1109                         gen_init_pay!("RC9YD301NZ17YKD6WDWLNOROFHIIN29VJN"),
   1110                         gen_init_pay!("GKDGTHLB82X6XVHBJIJ1CK8MEGU9XJ2EL7"),
   1111                         gen_init_pay!("PXCH2VVVTXEXBVDWICP23HZ4NV0H2CWW28"),
   1112                     ],
   1113                 ),
   1114                 (
   1115                     "X166701F6RV59LP71RVWVIW9SV2AFZYLG4",
   1116                     &[gen_init_pay!("R48UBIIB7B4LX0DMVOSI0ZTJWMMG8FMNKX")],
   1117                 ),
   1118                 (
   1119                     "OLAMDPI6YPMNRZHQ5PQ6JCVUQV2AN5NW6P",
   1120                     &[gen_init_pay!("TU2WJ54DR9Z6HT5VE494BNH4EXUSM0DRF7")],
   1121                 ),
   1122                 (
   1123                     "6OZN5T9W7MK6BIZYE01E62NHGP5JLMUD4X",
   1124                     &[
   1125                         gen_init_pay!("02WDIX4J90Z1M1WNFHLNSXY59SHXQTQCMQ"),
   1126                         gen_init_pay!("XAP5L7HVWPLCEMECU4GZK6GKUPBL0TD13Y"),
   1127                         gen_init_pay!("GM8I8GIETR72LP6CFBGRBUDKNO2CEQBGOE"),
   1128                     ],
   1129                 ),
   1130             ],
   1131         )
   1132         .await;
   1133 
   1134         register(&db, &cfg, "valiant_camt052.xml", &OrderDoc::report).await;
   1135 
   1136         check(
   1137             &db,
   1138             &[
   1139                 (
   1140                     "MJDJO2BDDBL7YSL2P96SXHG3TQZEZQD26L",
   1141                     success,
   1142                     &[("4UWWIDGTEIGDU6Z721QE95PYJSIEA48PYE", success)],
   1143                 ),
   1144                 (
   1145                     "5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U",
   1146                     success,
   1147                     &[
   1148                         ("SKMU2891PAAYBDW22DBWX2W7KTFZ1CDFO8", success),
   1149                         ("RC9YD301NZ17YKD6WDWLNOROFHIIN29VJN", success),
   1150                         ("GKDGTHLB82X6XVHBJIJ1CK8MEGU9XJ2EL7", success),
   1151                         ("PXCH2VVVTXEXBVDWICP23HZ4NV0H2CWW28", success),
   1152                     ],
   1153                 ),
   1154                 (
   1155                     "X166701F6RV59LP71RVWVIW9SV2AFZYLG4",
   1156                     success,
   1157                     &[("R48UBIIB7B4LX0DMVOSI0ZTJWMMG8FMNKX", late_failure)],
   1158                 ),
   1159                 (
   1160                     "OLAMDPI6YPMNRZHQ5PQ6JCVUQV2AN5NW6P",
   1161                     success,
   1162                     &[("TU2WJ54DR9Z6HT5VE494BNH4EXUSM0DRF7", success)],
   1163                 ),
   1164                 (
   1165                     "6OZN5T9W7MK6BIZYE01E62NHGP5JLMUD4X",
   1166                     success,
   1167                     &[
   1168                         ("02WDIX4J90Z1M1WNFHLNSXY59SHXQTQCMQ", success),
   1169                         ("XAP5L7HVWPLCEMECU4GZK6GKUPBL0TD13Y", late_failure),
   1170                         ("GM8I8GIETR72LP6CFBGRBUDKNO2CEQBGOE", success),
   1171                     ],
   1172                 ),
   1173             ],
   1174             &[
   1175                 tx_in(
   1176                     (None, Some("51030655601.0001"), Some("ZV20251030/514778/1")),
   1177                     "CHF:0.85",
   1178                     "CHF:0",
   1179                     Some("fun stuff"),
   1180                     "2025-10-30",
   1181                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1182                 ),
   1183                 tx_in(
   1184                     (None, Some("51030655601.0002"), Some("ZV20251030/514779/1")),
   1185                     "CHF:0.95",
   1186                     "CHF:0",
   1187                     Some("Taler PC2MKG0B7CK32K1T7DP08P6E1B7FHB6HY6R Q0PT3VTPBPRPYM1B0"),
   1188                     "2025-10-30",
   1189                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1190                 ),
   1191                 tx_in(
   1192                     (
   1193                         Some("7b76d488-05d5-44ab-9d77-31d4165ec158"),
   1194                         Some("00204EQY370"),
   1195                         Some("ZV20251118/685062/1"),
   1196                     ),
   1197                     "CHF:4.55",
   1198                     "CHF:0",
   1199                     Some("TEST"),
   1200                     "2025-11-18",
   1201                     None,
   1202                 ),
   1203             ],
   1204             &[
   1205                 tx_out(
   1206                     (
   1207                         None,
   1208                         Some("4UWWIDGTEIGDU6Z721QE95PYJSIEA48PYE"),
   1209                         Some("ZV20251030/511372/1"),
   1210                     ),
   1211                     "CHF:0.1",
   1212                     "CHF:0",
   1213                     Some("single 2025-10-30T09:46:04.55293090 9Z"),
   1214                     "2025-10-30",
   1215                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1216                 ),
   1217                 tx_out(
   1218                     (
   1219                         None,
   1220                         Some("SKMU2891PAAYBDW22DBWX2W7KTFZ1CDFO8"),
   1221                         Some("ZV20251030/511373/1"),
   1222                     ),
   1223                     "CHF:0.1",
   1224                     "CHF:0",
   1225                     Some("multi 0 2025-10-30T09:46:10.3877961 30Z"),
   1226                     "2025-10-30",
   1227                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1228                 ),
   1229                 tx_out(
   1230                     (
   1231                         None,
   1232                         Some("RC9YD301NZ17YKD6WDWLNOROFHIIN29VJN"),
   1233                         Some("ZV20251030/511373/2"),
   1234                     ),
   1235                     "CHF:0.11",
   1236                     "CHF:0",
   1237                     Some("multi 1 2025-10-30T09:46:10.3877961 30Z"),
   1238                     "2025-10-30",
   1239                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1240                 ),
   1241                 tx_out(
   1242                     (
   1243                         None,
   1244                         Some("GKDGTHLB82X6XVHBJIJ1CK8MEGU9XJ2EL7"),
   1245                         Some("ZV20251030/511373/3"),
   1246                     ),
   1247                     "CHF:0.12",
   1248                     "CHF:0",
   1249                     Some("multi 2 2025-10-30T09:46:10.3877961 30Z"),
   1250                     "2025-10-30",
   1251                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1252                 ),
   1253                 tx_out(
   1254                     (
   1255                         None,
   1256                         Some("PXCH2VVVTXEXBVDWICP23HZ4NV0H2CWW28"),
   1257                         Some("ZV20251030/511373/4"),
   1258                     ),
   1259                     "CHF:0.13",
   1260                     "CHF:0",
   1261                     Some("multi 3 2025-10-30T09:46:10.3877961 30Z"),
   1262                     "2025-10-30",
   1263                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1264                 ),
   1265                 tx_out(
   1266                     (
   1267                         None,
   1268                         Some("R48UBIIB7B4LX0DMVOSI0ZTJWMMG8FMNKX"),
   1269                         Some("ZV20251030/524078/1"),
   1270                     ),
   1271                     "CHF:0.21",
   1272                     "CHF:0",
   1273                     Some("bad name 2025-10-30T12:03:24.997478 811Z"),
   1274                     "2025-10-30",
   1275                     Some(("CH6208704048981247126", "John Smith")),
   1276                 ),
   1277                 tx_out(
   1278                     (
   1279                         None,
   1280                         Some("02WDIX4J90Z1M1WNFHLNSXY59SHXQTQCMQ"),
   1281                         Some("ZV20251030/524079/1"),
   1282                     ),
   1283                     "CHF:0.1",
   1284                     "CHF:0",
   1285                     Some("single 2025-10-30T12:04:00.37042083 6Z"),
   1286                     "2025-10-30",
   1287                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1288                 ),
   1289                 tx_out(
   1290                     (
   1291                         None,
   1292                         Some("XAP5L7HVWPLCEMECU4GZK6GKUPBL0TD13Y"),
   1293                         Some("ZV20251030/524079/2"),
   1294                     ),
   1295                     "CHF:0.21",
   1296                     "CHF:0",
   1297                     Some("bad name 2025-10-30T12:03:53.042190 686Z"),
   1298                     "2025-10-30",
   1299                     Some(("CH6208704048981247126", "John Smith")),
   1300                 ),
   1301                 tx_out(
   1302                     (
   1303                         None,
   1304                         Some("TU2WJ54DR9Z6HT5VE494BNH4EXUSM0DRF7"),
   1305                         Some("ZV20251030/524077/1"),
   1306                     ),
   1307                     "CHF:0.23",
   1308                     "CHF:5",
   1309                     Some("foreign iban 2025-10-30T12:03:44.0972 63765Z"),
   1310                     "2025-10-30",
   1311                     Some(("DE48330605920000686018", "Christian Grothoff")),
   1312                 ),
   1313                 tx_out(
   1314                     (
   1315                         None,
   1316                         Some("GM8I8GIETR72LP6CFBGRBUDKNO2CEQBGOE"),
   1317                         Some("ZV20251030/524080/1"),
   1318                     ),
   1319                     "CHF:0.23",
   1320                     "CHF:5",
   1321                     Some("foreign iban 2025-10-30T12:03:58.0046 73747Z"),
   1322                     "2025-10-30",
   1323                     Some(("DE48330605920000686018", "Christian Grothoff")),
   1324                 ),
   1325             ],
   1326             &[tx_out(
   1327                 (None, None, None),
   1328                 "CHF:0.85",
   1329                 "CHF:0",
   1330                 Some("bounce 51030655601.0001: missing reserve public key"),
   1331                 "1970-01-01",
   1332                 Some(("CH7389144832588726658", "Grothoff Hans")),
   1333             )],
   1334         )
   1335         .await;
   1336     }
   1337 
   1338     /** GLS dialect test */
   1339     #[db_test]
   1340     async fn gls(db: PgPool) {
   1341         let cfg = setup_cfg("gls.conf");
   1342         batches(
   1343             &db,
   1344             &[
   1345                 ("COMPAT_SUCCESS", &[gen_init_pay!("COMPAT_SUCCESS")]),
   1346                 ("COMPAT_FAILURE", &[gen_init_pay!("COMPAT_FAILURE")]),
   1347                 (
   1348                     "BATCH_SINGLE_SUCCESS",
   1349                     &[gen_init_pay!("FD622SMXKT5QWSAHDY0H8NYG3G")],
   1350                 ),
   1351                 // JEYMR3OYZTFM7505OWWENFPAH53LNOWJHS
   1352                 (
   1353                     "BATCH_SINGLE_FAILURE",
   1354                     &[gen_init_pay!("DAFC3NEE4T48WVC560T76ABA2C")],
   1355                 ),
   1356                 (
   1357                     "BATCH_SINGLE_RETURN",
   1358                     &[gen_init_pay!("KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5")],
   1359                 ),
   1360                 (
   1361                     "BATCH_MANY_SUCCESS",
   1362                     &[
   1363                         gen_init_pay!("IVMIGCUIE7Q7VOF73R8GU3KGRYBZPAYC5V"),
   1364                         gen_init_pay!("CDFN7I4FVIZ848DGDQ35DZ2K49H9EWXGAW"),
   1365                         gen_init_pay!("35M1268GW5ZFHS5JCB41UKDQNPMD40T849"),
   1366                         gen_init_pay!("HPOMV7A4E3P1TK9UZJS1WTM94A9V3X2SR1"),
   1367                     ],
   1368                 ),
   1369                 (
   1370                     "BATCH_MANY_PART",
   1371                     &[
   1372                         gen_init_pay!("27SK3166EG36SJ7VP7VFYP0MW8"),
   1373                         gen_init_pay!("KGTDBASWTJ6JM89WXD3Q5KFQC4"),
   1374                         gen_init_pay!("8XK8Z7RAX224FGWK832FD40GYC"),
   1375                     ],
   1376                 ),
   1377                 (
   1378                     "BATCH_MANY_FAILURE",
   1379                     &[
   1380                         gen_init_pay!("4XTPKWE4A9V90PRQJCT8Z3MQZ8"),
   1381                         gen_init_pay!("3VZZHVYJ6XP2SNPKWF4D4YVHNG"),
   1382                     ],
   1383                 ),
   1384             ],
   1385         )
   1386         .await;
   1387 
   1388         register(&db, &cfg, "gls_camt052.xml", &OrderDoc::report).await;
   1389         register(&db, &cfg, "gls_camt053.xml", &OrderDoc::statement).await;
   1390         // TODO camt054 with missing id before and after
   1391 
   1392         check(
   1393             &db,
   1394             &[
   1395                 ("COMPAT_SUCCESS", success, &[("COMPAT_SUCCESS", success)]),
   1396                 (
   1397                     "COMPAT_FAILURE",
   1398                     pending,
   1399                     &[("COMPAT_FAILURE", permanent_failure)],
   1400                 ),
   1401                 (
   1402                     "BATCH_SINGLE_SUCCESS",
   1403                     success,
   1404                     &[("FD622SMXKT5QWSAHDY0H8NYG3G", success)],
   1405                 ),
   1406                 (
   1407                     "BATCH_SINGLE_FAILURE", // TODO success
   1408                     pending,
   1409                     &[("DAFC3NEE4T48WVC560T76ABA2C", pending)], // TODO failure
   1410                 ),
   1411                 (
   1412                     "BATCH_SINGLE_RETURN",
   1413                     success,
   1414                     &[("KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5", late_failure)],
   1415                 ),
   1416                 (
   1417                     "BATCH_MANY_SUCCESS",
   1418                     success,
   1419                     &[
   1420                         ("IVMIGCUIE7Q7VOF73R8GU3KGRYBZPAYC5V", success),
   1421                         ("CDFN7I4FVIZ848DGDQ35DZ2K49H9EWXGAW", success),
   1422                         ("35M1268GW5ZFHS5JCB41UKDQNPMD40T849", success),
   1423                         ("HPOMV7A4E3P1TK9UZJS1WTM94A9V3X2SR1", success),
   1424                     ],
   1425                 ),
   1426                 (
   1427                     "BATCH_MANY_PART",
   1428                     success,
   1429                     &[
   1430                         ("27SK3166EG36SJ7VP7VFYP0MW8", success),
   1431                         ("KGTDBASWTJ6JM89WXD3Q5KFQC4", permanent_failure),
   1432                         ("8XK8Z7RAX224FGWK832FD40GYC", permanent_failure),
   1433                     ],
   1434                 ),
   1435                 (
   1436                     "BATCH_MANY_FAILURE", // TODO success
   1437                     pending,
   1438                     &[
   1439                         ("4XTPKWE4A9V90PRQJCT8Z3MQZ8", pending), // TODO failure
   1440                         ("3VZZHVYJ6XP2SNPKWF4D4YVHNG", pending), // TODO failure
   1441                     ],
   1442                 ),
   1443             ],
   1444             &[tx_in(
   1445                 (
   1446                     None,
   1447                     Some("BYLADEM1WOR-G2910276709458A2"),
   1448                     Some("2024041210041357000"),
   1449                 ),
   1450                 "CHF:3",
   1451                 "CHF:0",
   1452                 Some("Taler FJDQ7W6G7NWX4H9M1MKA12090FRC9K7DA6N0FANDZZFXTR6QHX5G Test.,-"),
   1453                 "2024-04-12",
   1454                 Some(("DE84500105177118117964", "John Smith")),
   1455             )],
   1456             &[
   1457                 tx_out(
   1458                     (None, Some("COMPAT_SUCCESS"), Some("2024041801514102000")),
   1459                     "CHF:2",
   1460                     "CHF:0",
   1461                     Some("TestABC123"),
   1462                     "2024-04-18",
   1463                     Some(("DE20500105172419259181", "John Smith")),
   1464                 ),
   1465                 tx_out(
   1466                     (
   1467                         None,
   1468                         Some("FD622SMXKT5QWSAHDY0H8NYG3G"),
   1469                         Some("2024090216552232000"),
   1470                     ),
   1471                     "CHF:1.1",
   1472                     "CHF:0",
   1473                     Some("single 2024-09-02T14:29:52.875253314Z"),
   1474                     "2024-09-02",
   1475                     Some(("DE89500105173198527518", "Grothoff Hans")),
   1476                 ),
   1477                 tx_out(
   1478                     (
   1479                         None,
   1480                         Some("YF5QBARGQ0MNY0VK59S477VDG4"),
   1481                         Some("2024041810552821000"),
   1482                     ),
   1483                     "CHF:1.1",
   1484                     "CHF:0",
   1485                     Some("Simple tx"),
   1486                     "2024-04-18",
   1487                     Some(("DE20500105172419259181", "John Smith")),
   1488                 ),
   1489                 tx_out(
   1490                     (None, Some("IVMIGCUIE7Q7VOF73R8GU3KGRYBZPAYC5V"), None),
   1491                     "CHF:44",
   1492                     "CHF:0",
   1493                     Some("init payment"),
   1494                     "2024-09-20",
   1495                     Some(("CH4189144589712575493", "Test")),
   1496                 ),
   1497                 tx_out(
   1498                     (None, Some("CDFN7I4FVIZ848DGDQ35DZ2K49H9EWXGAW"), None),
   1499                     "CHF:44",
   1500                     "CHF:0",
   1501                     Some("init payment"),
   1502                     "2024-09-20",
   1503                     Some(("CH4189144589712575493", "Test")),
   1504                 ),
   1505                 tx_out(
   1506                     (None, Some("35M1268GW5ZFHS5JCB41UKDQNPMD40T849"), None),
   1507                     "CHF:44",
   1508                     "CHF:0",
   1509                     Some("init payment"),
   1510                     "2024-09-20",
   1511                     Some(("CH4189144589712575493", "Test")),
   1512                 ),
   1513                 tx_out(
   1514                     (None, Some("HPOMV7A4E3P1TK9UZJS1WTM94A9V3X2SR1"), None),
   1515                     "CHF:44",
   1516                     "CHF:0",
   1517                     Some("init payment"),
   1518                     "2024-09-20",
   1519                     Some(("CH4189144589712575493", "Test")),
   1520                 ),
   1521                 tx_out(
   1522                     (
   1523                         None,
   1524                         Some("KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5"),
   1525                         Some("2024092100252498000"),
   1526                     ),
   1527                     "CHF:0.42",
   1528                     "CHF:0",
   1529                     Some("This should fail because bad iban"),
   1530                     "2024-09-23",
   1531                     Some(("DE18500105173385245163", "John Smith")),
   1532                 ),
   1533                 tx_out(
   1534                     (None, Some("27SK3166EG36SJ7VP7VFYP0MW8"), None),
   1535                     "CHF:44",
   1536                     "CHF:0",
   1537                     Some("init payment"),
   1538                     "2024-09-04",
   1539                     Some(("CH4189144589712575493", "Test")),
   1540                 ),
   1541             ],
   1542             &[],
   1543         )
   1544         .await;
   1545     }
   1546 
   1547     /** Maerki Baumann dialect test */
   1548     #[db_test]
   1549     async fn maerki_baumann(db: PgPool) {
   1550         let cfg = setup_cfg("maerki_baumann.conf");
   1551         batches(
   1552             &db,
   1553             &[(
   1554                 "BATCH_SINGLE_REPORTING",
   1555                 &[
   1556                     gen_init_pay!("5IBJZOWESQGPCSOXSNNBBY49ZURI5W7Q4H"),
   1557                     gen_init_pay!("XZ15UR0XU52QWI7Q4XB88EDS44PLH7DYXH"),
   1558                     gen_init_pay!("A09R35EW0359SZ51464E7TC37A0P2CBK04"),
   1559                     gen_init_pay!("UYXZ78LE9KAIMBY6UNXFYT1K8KNY8VLZLT"),
   1560                 ],
   1561             )],
   1562         )
   1563         .await;
   1564 
   1565         register(
   1566             &db,
   1567             &cfg,
   1568             "maerki_baumann_camt053.xml",
   1569             &OrderDoc::statement,
   1570         )
   1571         .await;
   1572 
   1573         check(
   1574             &db,
   1575             &[(
   1576                 "BATCH_SINGLE_REPORTING",
   1577                 success,
   1578                 &[
   1579                     ("5IBJZOWESQGPCSOXSNNBBY49ZURI5W7Q4H", success),
   1580                     ("XZ15UR0XU52QWI7Q4XB88EDS44PLH7DYXH", success),
   1581                     ("A09R35EW0359SZ51464E7TC37A0P2CBK04", success),
   1582                     ("UYXZ78LE9KAIMBY6UNXFYT1K8KNY8VLZLT", success),
   1583                 ],
   1584             )],
   1585             &[
   1586                 tx_in(
   1587                     (
   1588                         Some("adbe4a5a-6cea-4263-b259-8ab964561a32"),
   1589                         Some("41103099704.0002"),
   1590                         Some("ZV20241104/765446/1"),
   1591                     ),
   1592                     "CHF:1",
   1593                     "CHF:0.2",
   1594                     Some("SFHP6H24C16A5J05Q3FJW2XN1PB3EK70ZPY 5SJ30ADGY68FWN68G"),
   1595                     "2024-11-04",
   1596                     Some(("CH7389144832588726658", "Mr Test")),
   1597                 ),
   1598                 tx_in(
   1599                     (
   1600                         Some("7371795e-62fa-42dd-93b7-da89cc120faa"),
   1601                         Some("41103099704.0003"),
   1602                         Some("ZV20241104/765447/1"),
   1603                     ),
   1604                     "CHF:1",
   1605                     "CHF:0.2",
   1606                     Some("Random subject"),
   1607                     "2024-11-04",
   1608                     Some(("CH7389144832588726658", "Mr Test")),
   1609                 ),
   1610                 tx_in(
   1611                     (None, Some("50523424675.0001"), Some("ZV20250523/851716/1")),
   1612                     "CHF:0.5",
   1613                     "CHF:0.2",
   1614                     None,
   1615                     "2025-05-23",
   1616                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1617                 ),
   1618                 tx_in(
   1619                     (
   1620                         Some("f203fbb4-6e13-4c78-9b2a-d852fea6374a"),
   1621                         Some("41202060702.0001"),
   1622                         Some("ZV20241202/778108/1"),
   1623                     ),
   1624                     "CHF:0.05",
   1625                     "CHF:0.2",
   1626                     Some("mini"),
   1627                     "2024-12-02",
   1628                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1629                 ),
   1630                 tx_in(
   1631                     (
   1632                         Some("81b0d8c6-a677-4577-b75e-a639dcc03681"),
   1633                         Some("41120636093.0001"),
   1634                         Some("ZV20241121/773118/1"),
   1635                     ),
   1636                     "CHF:0.1",
   1637                     "CHF:0.2",
   1638                     Some("small transfer test"),
   1639                     "2024-11-21",
   1640                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1641                 ),
   1642                 tx_in(
   1643                     (None, None, Some("ZV20250114/796191/1")),
   1644                     "CHF:3003",
   1645                     "CHF:0",
   1646                     Some("Fix bad payment by MB."),
   1647                     "2025-01-27",
   1648                     None,
   1649                 ),
   1650                 tx_in(
   1651                     (None, Some("F000787951230001"), Some("ZV20250526/852733/1")),
   1652                     "CHF:1.38",
   1653                     "CHF:0.2",
   1654                     Some("Taler XT3D9MADR4V85JBWX47SMJFDQD2FDZDHHPH8R25YDG1KNVTSEH6G"),
   1655                     "2025-05-26",
   1656                     Some(("DE20500105172419259181", "Mr German")),
   1657                 ),
   1658             ],
   1659             &[
   1660                 tx_out(
   1661                     (
   1662                         None,
   1663                         Some("5IBJZOWESQGPCSOXSNNBBY49ZURI5W7Q4H"),
   1664                         Some("ZV20241121/773541/1"),
   1665                     ),
   1666                     "CHF:0.1",
   1667                     "CHF:0",
   1668                     Some("multi 0 2024-11-21T15:21:59.8859234 63Z"),
   1669                     "2024-11-27",
   1670                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1671                 ),
   1672                 tx_out(
   1673                     (
   1674                         None,
   1675                         Some("XZ15UR0XU52QWI7Q4XB88EDS44PLH7DYXH"),
   1676                         Some("ZV20241121/773541/4"),
   1677                     ),
   1678                     "CHF:0.13",
   1679                     "CHF:0",
   1680                     Some("multi 3 2024-11-21T15:21:59.8859234 63Z"),
   1681                     "2024-11-27",
   1682                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1683                 ),
   1684                 tx_out(
   1685                     (
   1686                         None,
   1687                         Some("A09R35EW0359SZ51464E7TC37A0P2CBK04"),
   1688                         Some("ZV20241121/773541/3"),
   1689                     ),
   1690                     "CHF:0.12",
   1691                     "CHF:0",
   1692                     Some("multi 2 2024-11-21T15:21:59.8859234 63Z"),
   1693                     "2024-11-27",
   1694                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1695                 ),
   1696                 tx_out(
   1697                     (
   1698                         None,
   1699                         Some("UYXZ78LE9KAIMBY6UNXFYT1K8KNY8VLZLT"),
   1700                         Some("ZV20241121/773541/2"),
   1701                     ),
   1702                     "CHF:0.11",
   1703                     "CHF:0",
   1704                     Some("multi 1 2024-11-21T15:21:59.8859234 63Z"),
   1705                     "2024-11-27",
   1706                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1707                 ),
   1708                 tx_out(
   1709                     (None, None, Some("GB20241220/205792/1")),
   1710                     "CHF:3000",
   1711                     "CHF:0",
   1712                     None,
   1713                     "2024-12-20",
   1714                     None,
   1715                 ),
   1716             ],
   1717             &[
   1718                 tx_out(
   1719                     (None, None, None),
   1720                     "CHF:1",
   1721                     "CHF:0",
   1722                     Some("bounce 7371795e-62fa-42dd-93b7-da89cc120faa: missing reserve public key"),
   1723                     "1970-01-01",
   1724                     Some(("CH7389144832588726658", "Mr Test")),
   1725                 ),
   1726                 tx_out(
   1727                     (None, None, None),
   1728                     "CHF:0.5",
   1729                     "CHF:0",
   1730                     Some("bounce 50523424675.0001: missing reserve public key"),
   1731                     "1970-01-01",
   1732                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1733                 ),
   1734                 tx_out(
   1735                     (None, None, None),
   1736                     "CHF:0.05",
   1737                     "CHF:0",
   1738                     Some("bounce f203fbb4-6e13-4c78-9b2a-d852fea6374a: missing reserve public key"),
   1739                     "1970-01-01",
   1740                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1741                 ),
   1742                 tx_out(
   1743                     (None, None, None),
   1744                     "CHF:0.1",
   1745                     "CHF:0",
   1746                     Some("bounce 81b0d8c6-a677-4577-b75e-a639dcc03681: missing reserve public key"),
   1747                     "1970-01-01",
   1748                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1749                 ),
   1750                 tx_out(
   1751                     (None, None, None),
   1752                     "CHF:1.38",
   1753                     "CHF:0",
   1754                     Some("bounce F000787951230001: restricted account"),
   1755                     "1970-01-01",
   1756                     Some(("DE20500105172419259181", "Mr German")),
   1757                 ),
   1758             ],
   1759         )
   1760         .await;
   1761     }
   1762 
   1763     /** PostFinance dialect test */
   1764     #[db_test]
   1765     async fn postfinance(db: PgPool) {
   1766         let cfg = setup_cfg("postfinance.conf");
   1767         batches(
   1768             &db,
   1769             &[
   1770                 (
   1771                     "ZS1PGNTSV0ZNDFAJBBWWB8015G",
   1772                     &[
   1773                         // Legacy notifications use MsgId when EndToEndId is NOTPROVIDED.
   1774                         gen_init_pay!(
   1775                             "ZS1PGNTSV0ZNDFAJBBWWB8015G",
   1776                             "init payment",
   1777                             amount("CHF:3")
   1778                         ),
   1779                         gen_init_pay!(
   1780                             "POSTFINANCE_NOTIFICATION_BATCH",
   1781                             "init payment",
   1782                             amount("CHF:403")
   1783                         ),
   1784                     ],
   1785                 ),
   1786                 (
   1787                     "50820f78-9024-44ff-978d-63a18c",
   1788                     &[gen_init_pay!(
   1789                         "50820f78-9024-44ff-978d-63a18c",
   1790                         "init payment",
   1791                         amount("CHF:1")
   1792                     )],
   1793                 ),
   1794                 (
   1795                     "889d1a80-1267-49bd-8fcc-85701a",
   1796                     &[gen_init_pay!(
   1797                         "889d1a80-1267-49bd-8fcc-85701a",
   1798                         "init payment",
   1799                         amount("CHF:1")
   1800                     )],
   1801                 ),
   1802                 (
   1803                     "4cc61cc7-6230-49c2-b5e2-b40bbb",
   1804                     &[gen_init_pay!(
   1805                         "4cc61cc7-6230-49c2-b5e2-b40bbb",
   1806                         "init payment",
   1807                         amount("CHF:1")
   1808                     )],
   1809                 ),
   1810                 (
   1811                     "EB4D22D428214261B2B3012D2A8CEC36",
   1812                     &[
   1813                         gen_init_pay!(
   1814                             "POSTFINANCE_STATEMENT_FIRST",
   1815                             "init payment",
   1816                             amount("CHF:100")
   1817                         ),
   1818                         gen_init_pay!(
   1819                             "POSTFINANCE_STATEMENT_SECOND",
   1820                             "init payment",
   1821                             amount("CHF:306")
   1822                         ),
   1823                     ],
   1824                 ),
   1825             ],
   1826         )
   1827         .await;
   1828 
   1829         register(
   1830             &db,
   1831             &cfg,
   1832             "postfinance_camt054.xml",
   1833             &OrderDoc::notification,
   1834         )
   1835         .await;
   1836         register(&db, &cfg, "postfinance_camt053.xml", &OrderDoc::statement).await;
   1837 
   1838         check(
   1839             &db,
   1840             &[
   1841                 (
   1842                     "ZS1PGNTSV0ZNDFAJBBWWB8015G",
   1843                     success,
   1844                     &[
   1845                         ("ZS1PGNTSV0ZNDFAJBBWWB8015G", success),
   1846                         ("POSTFINANCE_NOTIFICATION_BATCH", success),
   1847                     ],
   1848                 ),
   1849                 (
   1850                     "50820f78-9024-44ff-978d-63a18c",
   1851                     success,
   1852                     &[("50820f78-9024-44ff-978d-63a18c", permanent_failure)],
   1853                 ),
   1854                 (
   1855                     "889d1a80-1267-49bd-8fcc-85701a",
   1856                     success,
   1857                     &[("889d1a80-1267-49bd-8fcc-85701a", permanent_failure)],
   1858                 ),
   1859                 (
   1860                     "4cc61cc7-6230-49c2-b5e2-b40bbb",
   1861                     success,
   1862                     &[("4cc61cc7-6230-49c2-b5e2-b40bbb", permanent_failure)],
   1863                 ),
   1864                 (
   1865                     "EB4D22D428214261B2B3012D2A8CEC36",
   1866                     success,
   1867                     &[
   1868                         ("POSTFINANCE_STATEMENT_FIRST", success),
   1869                         ("POSTFINANCE_STATEMENT_SECOND", success),
   1870                     ],
   1871                 ),
   1872             ],
   1873             &[
   1874                 tx_in(
   1875                     (
   1876                         Some("62e2b511-7313-4ccd-8d40-c9d8e612cd71"),
   1877                         None,
   1878                         Some("231121CH0AZWCR9T"),
   1879                     ),
   1880                     "CHF:10",
   1881                     "CHF:0",
   1882                     Some("G1XTY6HGWGMVRM7E6XQ4JHJK561ETFDFTJZ7JVGV543XZCB27YBG"),
   1883                     "2023-12-19",
   1884                     Some(("CH7389144832588726658", "Mr Test")),
   1885                 ),
   1886                 tx_in(
   1887                     (
   1888                         Some("62e2b511-7313-4ccd-8d40-c9d8e612cd72"),
   1889                         None,
   1890                         Some("231121CH0AZWCVR1"),
   1891                     ),
   1892                     "CHF:2.53",
   1893                     "CHF:0",
   1894                     Some("G1XTY6HGWGMVRM7E6XQ4JHJK561ETFDFTJZ7JVGV543XZCB27YB"),
   1895                     "2023-12-19",
   1896                     Some(("CH7389144832588726658", "Mr Test")),
   1897                 ),
   1898             ],
   1899             &[
   1900                 tx_out(
   1901                     (None, Some("ZS1PGNTSV0ZNDFAJBBWWB8015G"), None),
   1902                     "CHF:3",
   1903                     "CHF:0",
   1904                     None,
   1905                     "2024-01-15",
   1906                     None,
   1907                 ),
   1908                 tx_out(
   1909                     (None, Some("POSTFINANCE_NOTIFICATION_BATCH"), None),
   1910                     "CHF:403",
   1911                     "CHF:0",
   1912                     Some("init payment"),
   1913                     "2024-01-15",
   1914                     Some(("CH4189144589712575493", "Test")),
   1915                 ),
   1916                 tx_out(
   1917                     (None, Some("POSTFINANCE_STATEMENT_FIRST"), None),
   1918                     "CHF:100",
   1919                     "CHF:0",
   1920                     Some("init payment"),
   1921                     "2024-08-26",
   1922                     Some(("CH4189144589712575493", "Test")),
   1923                 ),
   1924                 tx_out(
   1925                     (None, Some("POSTFINANCE_STATEMENT_SECOND"), None),
   1926                     "CHF:306",
   1927                     "CHF:0",
   1928                     Some("init payment"),
   1929                     "2024-08-26",
   1930                     Some(("CH4189144589712575493", "Test")),
   1931                 ),
   1932             ],
   1933             &[tx_out(
   1934                 (None, None, None),
   1935                 "CHF:2.53",
   1936                 "CHF:0",
   1937                 Some("bounce 62e2b511-7313-4ccd-8d40-c9d8e612cd72: missing reserve public key"),
   1938                 "1970-01-01",
   1939                 Some(("CH7389144832588726658", "Mr Test")),
   1940             )],
   1941         )
   1942         .await;
   1943     }
   1944 }