libeufin

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

fetch.rs (70236B)


      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         let cfg = NexusCfg::parse(&cfg).unwrap();
    709         cfg
    710     }
    711 
    712     fn iban_payto(iban: impl AsRef<str>, name: impl AsRef<str>) -> PaytoURI {
    713         IbanPayto::new(BankID {
    714             iban: iban.as_ref().parse().expect("invalid IBAN"),
    715             bic: None,
    716         })
    717         .as_full_uri(name.as_ref())
    718     }
    719 
    720     fn tx_in(
    721         id: (Option<&str>, Option<&str>, Option<&str>),
    722         amount: &str,
    723         credit_fee: &str,
    724         subject: Option<&str>,
    725         execution_time: &str,
    726         debtor: Option<(&str, &str)>,
    727     ) -> InTx {
    728         InTx {
    729             id: InId::new(
    730                 id.0.map(|it| it.parse().unwrap()),
    731                 id.1.map(Into::into),
    732                 id.2.map(Into::into),
    733             ),
    734             amount: Amount::from_str(amount).unwrap(),
    735             credit_fee: Amount::from_str(credit_fee).unwrap(),
    736             subject: subject.map(Into::into),
    737             execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()),
    738             debtor: debtor.map(|(iban, name)| iban_payto(iban, name)),
    739         }
    740     }
    741 
    742     pub fn tx_out(
    743         id: (Option<&str>, Option<&str>, Option<&str>),
    744         amount: &str,
    745         debit_fee: &str,
    746         subject: Option<&str>,
    747         execution_time: &str,
    748         creditor: Option<(&str, &str)>,
    749     ) -> OutTx {
    750         OutTx {
    751             id: OutId::new(
    752                 id.0.map(Into::into),
    753                 id.1.map(Into::into),
    754                 id.2.map(Into::into),
    755             ),
    756             amount: Amount::from_str(amount).unwrap(),
    757             debit_fee: Amount::from_str(debit_fee).unwrap(),
    758             subject: subject.map(Into::into),
    759             execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()),
    760             creditor: creditor.map(|(iban, name)| iban_payto(iban, name)),
    761         }
    762     }
    763 
    764     /** Register batches of initiated payments for reconcciliation */
    765     async fn batches(pool: &PgPool, batches: &[(&str, &[Initiated])]) -> Vec<PaymentBatch> {
    766         let mut tmp = Vec::<PaymentBatch>::new();
    767         for (name, txs) in batches {
    768             for tx in *txs {
    769                 initiate(
    770                     pool,
    771                     &tx.amount,
    772                     &tx.subject,
    773                     &tx.creditor,
    774                     &tx.initiation_time,
    775                     &tx.e2e_id,
    776                 )
    777                 .await
    778                 .unwrap();
    779             }
    780             batch_initiated(pool, &Timestamp::now(), name, false)
    781                 .await
    782                 .unwrap();
    783             for batch in initiated_submittable(pool, &Currency::CHF).await.unwrap() {
    784                 batch_sub_success(
    785                     pool,
    786                     batch.id,
    787                     &Timestamp::now(),
    788                     &name.replace("BATCH", "ORDER"),
    789                 )
    790                 .await
    791                 .unwrap();
    792                 tmp.push(batch);
    793             }
    794         }
    795         tmp
    796     }
    797 
    798     /** Register an XML sample into the database */
    799     async fn register(pool: &PgPool, cfg: &NexusCfg, path: &str, doc: &OrderDoc) {
    800         register_file(
    801             pool,
    802             cfg,
    803             doc,
    804             &std::fs::read(format!("../libeufin-ebics/sample/platform/{path}")).unwrap(),
    805         )
    806         .await
    807         .unwrap()
    808     }
    809 
    810     /** Check database content */
    811     async fn check(
    812         db: &PgPool,
    813         status: &[(
    814             &'static str,
    815             SubmissionState,
    816             &[(&'static str, SubmissionState)],
    817         )],
    818         incoming: &[InTx],
    819         outgoing: &[OutTx],
    820         bounced: &[OutTx],
    821     ) {
    822         // Check batch status
    823         let batch_status = sqlx::query_as::<_, (CompactString, SubmissionState)>(
    824             "SELECT message_id, status FROM initiated_outgoing_batches ORDER BY initiated_outgoing_batch_id"
    825         ).fetch_all(db).await.unwrap();
    826 
    827         assert_eq!(
    828             status
    829                 .iter()
    830                 .map(|it| (it.0.into(), it.1))
    831                 .collect::<Vec<_>>(),
    832             batch_status
    833         );
    834 
    835         // Check transactions status
    836         let batch_tx = sqlx::query_as::<_, (CompactString, CompactString, SubmissionState)>(
    837             "
    838             SELECT message_id, end_to_end_id, initiated_outgoing_transactions.status
    839             FROM initiated_outgoing_transactions
    840                 JOIN initiated_outgoing_batches USING (initiated_outgoing_batch_id)
    841             ORDER BY initiated_outgoing_batch_id, initiated_outgoing_transaction_id
    842             ",
    843         )
    844         .fetch_all(db)
    845         .await
    846         .unwrap()
    847         .into_iter()
    848         .fold(
    849             IndexMap::<_, Vec<_>>::new(),
    850             |mut outer, (key, value, third)| {
    851                 outer.entry(key).or_default().push((value, third));
    852                 outer
    853             },
    854         );
    855         pretty_assertions::assert_eq!(
    856             status
    857                 .iter()
    858                 .map(|it| (it.0, it.2.to_vec()))
    859                 .collect::<Vec<_>>(),
    860             batch_tx
    861                 .iter()
    862                 .map(|it| (
    863                     it.0.as_str(),
    864                     it.1.iter()
    865                         .map(|it| (it.0.as_str(), it.1))
    866                         .collect::<Vec<_>>()
    867                 ))
    868                 .collect::<Vec<_>>()
    869         );
    870 
    871         // Check incoming transactions
    872         let incoming_tx = sqlx::query(
    873             "
    874             SELECT
    875                  uetr
    876                 ,tx_id
    877                 ,acct_svcr_ref
    878                 ,amount
    879                 ,credit_fee
    880                 ,subject
    881                 ,execution_time
    882                 ,debit_payto
    883             FROM incoming_transactions
    884             ORDER BY incoming_transaction_id
    885             ",
    886         )
    887         .try_map(|r: PgRow| {
    888             Ok(InTx {
    889                 id: InId::new(
    890                     r.try_get("uetr")?,
    891                     r.try_get("tx_id")?,
    892                     r.try_get("acct_svcr_ref")?,
    893                 ),
    894                 amount: r.try_get_amount("amount", &Currency::CHF)?,
    895                 credit_fee: r
    896                     .try_get_opt_amount("credit_fee", &Currency::CHF)?
    897                     .unwrap_or(Amount::zero(&Currency::CHF)),
    898                 subject: r.try_get("subject")?,
    899                 execution_time: r.try_get_timestamp("execution_time")?,
    900                 debtor: r.try_get_opt_parse("debit_payto")?,
    901             })
    902         })
    903         .fetch_all(db)
    904         .await
    905         .unwrap();
    906         pretty_assertions::assert_eq!(incoming, incoming_tx);
    907 
    908         // Check outgoing transactions
    909         let outgoing_tx = sqlx::query(
    910             "
    911             SELECT end_to_end_id
    912                 ,acct_svcr_ref
    913                 ,amount
    914                 ,debit_fee
    915                 ,subject
    916                 ,execution_time
    917                 ,credit_payto
    918             FROM outgoing_transactions
    919             ORDER BY outgoing_transaction_id
    920             ",
    921         )
    922         .try_map(|r: PgRow| {
    923             Ok(OutTx {
    924                 id: OutId::new(
    925                     None,
    926                     r.try_get("end_to_end_id")?,
    927                     r.try_get("acct_svcr_ref")?,
    928                 ),
    929                 amount: r.try_get_amount("amount", &Currency::CHF)?,
    930                 debit_fee: r
    931                     .try_get_opt_amount("debit_fee", &Currency::CHF)?
    932                     .unwrap_or(Amount::zero(&Currency::CHF)),
    933                 subject: r.try_get("subject")?,
    934                 execution_time: r.try_get_timestamp("execution_time")?,
    935                 creditor: r.try_get_opt_parse("credit_payto")?,
    936             })
    937         })
    938         .fetch_all(db)
    939         .await
    940         .unwrap();
    941         pretty_assertions::assert_eq!(outgoing, outgoing_tx);
    942 
    943         // Check outgoing transactions
    944         let bounced_tx = sqlx::query(
    945             "
    946             SELECT end_to_end_id
    947                 ,amount
    948                 ,subject
    949                 ,credit_payto
    950             FROM initiated_outgoing_transactions
    951                 JOIN bounced_transactions USING (initiated_outgoing_transaction_id)
    952             ",
    953         )
    954         .try_map(|r: PgRow| {
    955             Ok(OutTx {
    956                 id: OutId::new(None, None, None),
    957                 amount: r.try_get_amount("amount", &Currency::CHF)?,
    958                 debit_fee: Amount::zero(&Currency::CHF),
    959                 subject: r.try_get("subject")?,
    960                 execution_time: Timestamp::UNIX_EPOCH,
    961                 creditor: r.try_get_opt_parse("credit_payto")?,
    962             })
    963         })
    964         .fetch_all(db)
    965         .await
    966         .unwrap();
    967         pretty_assertions::assert_eq!(bounced, bounced_tx)
    968     }
    969 
    970     #[db_test]
    971     async fn pain001(db: PgPool) {
    972         let cfg = setup();
    973         let mut batches = batches(
    974             &db,
    975             &[(
    976                 "MESSAGE_ID",
    977                 &[
    978                     gen_init_pay!("TX_FIRST", "Test 42", amount("EUR:42")),
    979                     gen_init_pay!("TX_SECOND", "Test 5.11", amount("EUR:5.11")),
    980                     gen_init_pay!("TX_THIRD", "Test 0.21", amount("EUR:0.21")),
    981                 ],
    982             )],
    983         )
    984         .await;
    985         let batch = &mut batches[0];
    986         let mut cfg = (*cfg.ebics().unwrap()).clone();
    987         for dialect in Dialect::entries {
    988             cfg.dialect = *dialect;
    989             pretty_assertions::assert_eq!(
    990                 std::fs::read_to_string(format!(
    991                     "../libeufin-ebics/sample/platform/{dialect}_pain001.xml"
    992                 ))
    993                 .unwrap(),
    994                 batch_pain001(
    995                     batch,
    996                     &cfg,
    997                     date_to_utc_ts(&Date::constant(2024, 9, 9)),
    998                     false
    999                 )
   1000                 .unwrap()
   1001             );
   1002         }
   1003     }
   1004 
   1005     /** HAC order id test */
   1006     #[db_test]
   1007     async fn hac(db: PgPool) {
   1008         let cfg = setup();
   1009         batches(
   1010             &db,
   1011             &[
   1012                 (
   1013                     "BATCH_SUCCESS",
   1014                     &[
   1015                         gen_init_pay!("BATCH_SUCCESS_0"),
   1016                         gen_init_pay!("BATCH_SUCCESS_1"),
   1017                     ],
   1018                 ),
   1019                 (
   1020                     "BATCH_FAILURE",
   1021                     &[
   1022                         gen_init_pay!("BATCH_FAILURE_0"),
   1023                         gen_init_pay!("BATCH_FAILURE_1"),
   1024                     ],
   1025                 ),
   1026             ],
   1027         )
   1028         .await;
   1029 
   1030         register(&db, &cfg, "hac.xml", &OrderDoc::acknowledgement).await;
   1031 
   1032         check(
   1033             &db,
   1034             &[
   1035                 (
   1036                     "BATCH_SUCCESS",
   1037                     success,
   1038                     &[("BATCH_SUCCESS_0", pending), ("BATCH_SUCCESS_1", pending)],
   1039                 ),
   1040                 (
   1041                     "BATCH_FAILURE",
   1042                     permanent_failure,
   1043                     &[
   1044                         ("BATCH_FAILURE_0", permanent_failure),
   1045                         ("BATCH_FAILURE_1", permanent_failure),
   1046                     ],
   1047                 ),
   1048             ],
   1049             &[],
   1050             &[],
   1051             &[],
   1052         )
   1053         .await;
   1054     }
   1055 
   1056     /** CreditSuisse dialect test */
   1057     #[db_test]
   1058     async fn cs(db: PgPool) {
   1059         let cfg = setup();
   1060         batches(
   1061             &db,
   1062             &[(
   1063                 "05BD4C5B4A2649B5B08F6EF6A31F197A",
   1064                 &[
   1065                     gen_init_pay!("AQCXNCPWD8PHW5JTN65Y5XTF7R"),
   1066                     gen_init_pay!("EE9SX76FC5YSC657EK3GMVZ9TC"),
   1067                     gen_init_pay!("V5B3MXPEWES9VQW1JDRD6VAET4"),
   1068                     gen_init_pay!("M9NGRCAC1FBX3ENX3XEDEPJ2JW"),
   1069                 ],
   1070             )],
   1071         )
   1072         .await;
   1073 
   1074         register(&db, &cfg, "pain002_part.xml", &OrderDoc::status).await;
   1075 
   1076         check(
   1077             &db,
   1078             &[(
   1079                 "05BD4C5B4A2649B5B08F6EF6A31F197A",
   1080                 success,
   1081                 &[
   1082                     ("AQCXNCPWD8PHW5JTN65Y5XTF7R", permanent_failure),
   1083                     ("EE9SX76FC5YSC657EK3GMVZ9TC", permanent_failure),
   1084                     ("V5B3MXPEWES9VQW1JDRD6VAET4", permanent_failure),
   1085                     ("M9NGRCAC1FBX3ENX3XEDEPJ2JW", pending),
   1086                 ],
   1087             )],
   1088             &[],
   1089             &[],
   1090             &[],
   1091         )
   1092         .await;
   1093     }
   1094 
   1095     /** Valiant dialect test */
   1096     #[db_test]
   1097     async fn valiant(db: PgPool) {
   1098         let cfg = setup_cfg("valiant.conf");
   1099         batches(
   1100             &db,
   1101             &[
   1102                 (
   1103                     "MJDJO2BDDBL7YSL2P96SXHG3TQZEZQD26L",
   1104                     &[gen_init_pay!("4UWWIDGTEIGDU6Z721QE95PYJSIEA48PYE")],
   1105                 ),
   1106                 (
   1107                     "5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U",
   1108                     &[
   1109                         gen_init_pay!("SKMU2891PAAYBDW22DBWX2W7KTFZ1CDFO8"),
   1110                         gen_init_pay!("RC9YD301NZ17YKD6WDWLNOROFHIIN29VJN"),
   1111                         gen_init_pay!("GKDGTHLB82X6XVHBJIJ1CK8MEGU9XJ2EL7"),
   1112                         gen_init_pay!("PXCH2VVVTXEXBVDWICP23HZ4NV0H2CWW28"),
   1113                     ],
   1114                 ),
   1115                 (
   1116                     "X166701F6RV59LP71RVWVIW9SV2AFZYLG4",
   1117                     &[gen_init_pay!("R48UBIIB7B4LX0DMVOSI0ZTJWMMG8FMNKX")],
   1118                 ),
   1119                 (
   1120                     "OLAMDPI6YPMNRZHQ5PQ6JCVUQV2AN5NW6P",
   1121                     &[gen_init_pay!("TU2WJ54DR9Z6HT5VE494BNH4EXUSM0DRF7")],
   1122                 ),
   1123                 (
   1124                     "6OZN5T9W7MK6BIZYE01E62NHGP5JLMUD4X",
   1125                     &[
   1126                         gen_init_pay!("02WDIX4J90Z1M1WNFHLNSXY59SHXQTQCMQ"),
   1127                         gen_init_pay!("XAP5L7HVWPLCEMECU4GZK6GKUPBL0TD13Y"),
   1128                         gen_init_pay!("GM8I8GIETR72LP6CFBGRBUDKNO2CEQBGOE"),
   1129                     ],
   1130                 ),
   1131             ],
   1132         )
   1133         .await;
   1134 
   1135         register(&db, &cfg, "valiant_camt052.xml", &OrderDoc::report).await;
   1136 
   1137         check(
   1138             &db,
   1139             &[
   1140                 (
   1141                     "MJDJO2BDDBL7YSL2P96SXHG3TQZEZQD26L",
   1142                     success,
   1143                     &[("4UWWIDGTEIGDU6Z721QE95PYJSIEA48PYE", success)],
   1144                 ),
   1145                 (
   1146                     "5HIS3433VVIBAANHW3GX9DR1AXRS43KZ4U",
   1147                     success,
   1148                     &[
   1149                         ("SKMU2891PAAYBDW22DBWX2W7KTFZ1CDFO8", success),
   1150                         ("RC9YD301NZ17YKD6WDWLNOROFHIIN29VJN", success),
   1151                         ("GKDGTHLB82X6XVHBJIJ1CK8MEGU9XJ2EL7", success),
   1152                         ("PXCH2VVVTXEXBVDWICP23HZ4NV0H2CWW28", success),
   1153                     ],
   1154                 ),
   1155                 (
   1156                     "X166701F6RV59LP71RVWVIW9SV2AFZYLG4",
   1157                     success,
   1158                     &[("R48UBIIB7B4LX0DMVOSI0ZTJWMMG8FMNKX", late_failure)],
   1159                 ),
   1160                 (
   1161                     "OLAMDPI6YPMNRZHQ5PQ6JCVUQV2AN5NW6P",
   1162                     success,
   1163                     &[("TU2WJ54DR9Z6HT5VE494BNH4EXUSM0DRF7", success)],
   1164                 ),
   1165                 (
   1166                     "6OZN5T9W7MK6BIZYE01E62NHGP5JLMUD4X",
   1167                     success,
   1168                     &[
   1169                         ("02WDIX4J90Z1M1WNFHLNSXY59SHXQTQCMQ", success),
   1170                         ("XAP5L7HVWPLCEMECU4GZK6GKUPBL0TD13Y", late_failure),
   1171                         ("GM8I8GIETR72LP6CFBGRBUDKNO2CEQBGOE", success),
   1172                     ],
   1173                 ),
   1174             ],
   1175             &[
   1176                 tx_in(
   1177                     (None, Some("51030655601.0001"), Some("ZV20251030/514778/1")),
   1178                     "CHF:0.85",
   1179                     "CHF:0",
   1180                     Some("fun stuff"),
   1181                     "2025-10-30",
   1182                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1183                 ),
   1184                 tx_in(
   1185                     (None, Some("51030655601.0002"), Some("ZV20251030/514779/1")),
   1186                     "CHF:0.95",
   1187                     "CHF:0",
   1188                     Some("Taler PC2MKG0B7CK32K1T7DP08P6E1B7FHB6HY6R Q0PT3VTPBPRPYM1B0"),
   1189                     "2025-10-30",
   1190                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1191                 ),
   1192                 tx_in(
   1193                     (
   1194                         Some("7b76d488-05d5-44ab-9d77-31d4165ec158"),
   1195                         Some("00204EQY370"),
   1196                         Some("ZV20251118/685062/1"),
   1197                     ),
   1198                     "CHF:4.55",
   1199                     "CHF:0",
   1200                     Some("TEST"),
   1201                     "2025-11-18",
   1202                     None,
   1203                 ),
   1204             ],
   1205             &[
   1206                 tx_out(
   1207                     (
   1208                         None,
   1209                         Some("4UWWIDGTEIGDU6Z721QE95PYJSIEA48PYE"),
   1210                         Some("ZV20251030/511372/1"),
   1211                     ),
   1212                     "CHF:0.1",
   1213                     "CHF:0",
   1214                     Some("single 2025-10-30T09:46:04.55293090 9Z"),
   1215                     "2025-10-30",
   1216                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1217                 ),
   1218                 tx_out(
   1219                     (
   1220                         None,
   1221                         Some("SKMU2891PAAYBDW22DBWX2W7KTFZ1CDFO8"),
   1222                         Some("ZV20251030/511373/1"),
   1223                     ),
   1224                     "CHF:0.1",
   1225                     "CHF:0",
   1226                     Some("multi 0 2025-10-30T09:46:10.3877961 30Z"),
   1227                     "2025-10-30",
   1228                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1229                 ),
   1230                 tx_out(
   1231                     (
   1232                         None,
   1233                         Some("RC9YD301NZ17YKD6WDWLNOROFHIIN29VJN"),
   1234                         Some("ZV20251030/511373/2"),
   1235                     ),
   1236                     "CHF:0.11",
   1237                     "CHF:0",
   1238                     Some("multi 1 2025-10-30T09:46:10.3877961 30Z"),
   1239                     "2025-10-30",
   1240                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1241                 ),
   1242                 tx_out(
   1243                     (
   1244                         None,
   1245                         Some("GKDGTHLB82X6XVHBJIJ1CK8MEGU9XJ2EL7"),
   1246                         Some("ZV20251030/511373/3"),
   1247                     ),
   1248                     "CHF:0.12",
   1249                     "CHF:0",
   1250                     Some("multi 2 2025-10-30T09:46:10.3877961 30Z"),
   1251                     "2025-10-30",
   1252                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1253                 ),
   1254                 tx_out(
   1255                     (
   1256                         None,
   1257                         Some("PXCH2VVVTXEXBVDWICP23HZ4NV0H2CWW28"),
   1258                         Some("ZV20251030/511373/4"),
   1259                     ),
   1260                     "CHF:0.13",
   1261                     "CHF:0",
   1262                     Some("multi 3 2025-10-30T09:46:10.3877961 30Z"),
   1263                     "2025-10-30",
   1264                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1265                 ),
   1266                 tx_out(
   1267                     (
   1268                         None,
   1269                         Some("R48UBIIB7B4LX0DMVOSI0ZTJWMMG8FMNKX"),
   1270                         Some("ZV20251030/524078/1"),
   1271                     ),
   1272                     "CHF:0.21",
   1273                     "CHF:0",
   1274                     Some("bad name 2025-10-30T12:03:24.997478 811Z"),
   1275                     "2025-10-30",
   1276                     Some(("CH6208704048981247126", "John Smith")),
   1277                 ),
   1278                 tx_out(
   1279                     (
   1280                         None,
   1281                         Some("02WDIX4J90Z1M1WNFHLNSXY59SHXQTQCMQ"),
   1282                         Some("ZV20251030/524079/1"),
   1283                     ),
   1284                     "CHF:0.1",
   1285                     "CHF:0",
   1286                     Some("single 2025-10-30T12:04:00.37042083 6Z"),
   1287                     "2025-10-30",
   1288                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1289                 ),
   1290                 tx_out(
   1291                     (
   1292                         None,
   1293                         Some("XAP5L7HVWPLCEMECU4GZK6GKUPBL0TD13Y"),
   1294                         Some("ZV20251030/524079/2"),
   1295                     ),
   1296                     "CHF:0.21",
   1297                     "CHF:0",
   1298                     Some("bad name 2025-10-30T12:03:53.042190 686Z"),
   1299                     "2025-10-30",
   1300                     Some(("CH6208704048981247126", "John Smith")),
   1301                 ),
   1302                 tx_out(
   1303                     (
   1304                         None,
   1305                         Some("TU2WJ54DR9Z6HT5VE494BNH4EXUSM0DRF7"),
   1306                         Some("ZV20251030/524077/1"),
   1307                     ),
   1308                     "CHF:0.23",
   1309                     "CHF:5",
   1310                     Some("foreign iban 2025-10-30T12:03:44.0972 63765Z"),
   1311                     "2025-10-30",
   1312                     Some(("DE48330605920000686018", "Christian Grothoff")),
   1313                 ),
   1314                 tx_out(
   1315                     (
   1316                         None,
   1317                         Some("GM8I8GIETR72LP6CFBGRBUDKNO2CEQBGOE"),
   1318                         Some("ZV20251030/524080/1"),
   1319                     ),
   1320                     "CHF:0.23",
   1321                     "CHF:5",
   1322                     Some("foreign iban 2025-10-30T12:03:58.0046 73747Z"),
   1323                     "2025-10-30",
   1324                     Some(("DE48330605920000686018", "Christian Grothoff")),
   1325                 ),
   1326             ],
   1327             &[tx_out(
   1328                 (None, None, None),
   1329                 "CHF:0.85",
   1330                 "CHF:0",
   1331                 Some("bounce 51030655601.0001: missing reserve public key"),
   1332                 "1970-01-01",
   1333                 Some(("CH7389144832588726658", "Grothoff Hans")),
   1334             )],
   1335         )
   1336         .await;
   1337     }
   1338 
   1339     /** GLS dialect test */
   1340     #[db_test]
   1341     async fn gls(db: PgPool) {
   1342         let cfg = setup_cfg("gls.conf");
   1343         batches(
   1344             &db,
   1345             &[
   1346                 ("COMPAT_SUCCESS", &[gen_init_pay!("COMPAT_SUCCESS")]),
   1347                 ("COMPAT_FAILURE", &[gen_init_pay!("COMPAT_FAILURE")]),
   1348                 (
   1349                     "BATCH_SINGLE_SUCCESS",
   1350                     &[gen_init_pay!("FD622SMXKT5QWSAHDY0H8NYG3G")],
   1351                 ),
   1352                 // JEYMR3OYZTFM7505OWWENFPAH53LNOWJHS
   1353                 (
   1354                     "BATCH_SINGLE_FAILURE",
   1355                     &[gen_init_pay!("DAFC3NEE4T48WVC560T76ABA2C")],
   1356                 ),
   1357                 (
   1358                     "BATCH_SINGLE_RETURN",
   1359                     &[gen_init_pay!("KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5")],
   1360                 ),
   1361                 (
   1362                     "BATCH_MANY_SUCCESS",
   1363                     &[
   1364                         gen_init_pay!("IVMIGCUIE7Q7VOF73R8GU3KGRYBZPAYC5V"),
   1365                         gen_init_pay!("CDFN7I4FVIZ848DGDQ35DZ2K49H9EWXGAW"),
   1366                         gen_init_pay!("35M1268GW5ZFHS5JCB41UKDQNPMD40T849"),
   1367                         gen_init_pay!("HPOMV7A4E3P1TK9UZJS1WTM94A9V3X2SR1"),
   1368                     ],
   1369                 ),
   1370                 (
   1371                     "BATCH_MANY_PART",
   1372                     &[
   1373                         gen_init_pay!("27SK3166EG36SJ7VP7VFYP0MW8"),
   1374                         gen_init_pay!("KGTDBASWTJ6JM89WXD3Q5KFQC4"),
   1375                         gen_init_pay!("8XK8Z7RAX224FGWK832FD40GYC"),
   1376                     ],
   1377                 ),
   1378                 (
   1379                     "BATCH_MANY_FAILURE",
   1380                     &[
   1381                         gen_init_pay!("4XTPKWE4A9V90PRQJCT8Z3MQZ8"),
   1382                         gen_init_pay!("3VZZHVYJ6XP2SNPKWF4D4YVHNG"),
   1383                     ],
   1384                 ),
   1385             ],
   1386         )
   1387         .await;
   1388 
   1389         register(&db, &cfg, "gls_camt052.xml", &OrderDoc::report).await;
   1390         register(&db, &cfg, "gls_camt053.xml", &OrderDoc::statement).await;
   1391         // TODO camt054 with missing id before and after
   1392 
   1393         check(
   1394             &db,
   1395             &[
   1396                 ("COMPAT_SUCCESS", success, &[("COMPAT_SUCCESS", success)]),
   1397                 (
   1398                     "COMPAT_FAILURE",
   1399                     pending,
   1400                     &[("COMPAT_FAILURE", permanent_failure)],
   1401                 ),
   1402                 (
   1403                     "BATCH_SINGLE_SUCCESS",
   1404                     success,
   1405                     &[("FD622SMXKT5QWSAHDY0H8NYG3G", success)],
   1406                 ),
   1407                 (
   1408                     "BATCH_SINGLE_FAILURE", // TODO success
   1409                     pending,
   1410                     &[("DAFC3NEE4T48WVC560T76ABA2C", pending)], // TODO failure
   1411                 ),
   1412                 (
   1413                     "BATCH_SINGLE_RETURN",
   1414                     success,
   1415                     &[("KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5", late_failure)],
   1416                 ),
   1417                 (
   1418                     "BATCH_MANY_SUCCESS",
   1419                     success,
   1420                     &[
   1421                         ("IVMIGCUIE7Q7VOF73R8GU3KGRYBZPAYC5V", success),
   1422                         ("CDFN7I4FVIZ848DGDQ35DZ2K49H9EWXGAW", success),
   1423                         ("35M1268GW5ZFHS5JCB41UKDQNPMD40T849", success),
   1424                         ("HPOMV7A4E3P1TK9UZJS1WTM94A9V3X2SR1", success),
   1425                     ],
   1426                 ),
   1427                 (
   1428                     "BATCH_MANY_PART",
   1429                     success,
   1430                     &[
   1431                         ("27SK3166EG36SJ7VP7VFYP0MW8", success),
   1432                         ("KGTDBASWTJ6JM89WXD3Q5KFQC4", permanent_failure),
   1433                         ("8XK8Z7RAX224FGWK832FD40GYC", permanent_failure),
   1434                     ],
   1435                 ),
   1436                 (
   1437                     "BATCH_MANY_FAILURE", // TODO success
   1438                     pending,
   1439                     &[
   1440                         ("4XTPKWE4A9V90PRQJCT8Z3MQZ8", pending), // TODO failure
   1441                         ("3VZZHVYJ6XP2SNPKWF4D4YVHNG", pending), // TODO failure
   1442                     ],
   1443                 ),
   1444             ],
   1445             &[tx_in(
   1446                 (
   1447                     None,
   1448                     Some("BYLADEM1WOR-G2910276709458A2"),
   1449                     Some("2024041210041357000"),
   1450                 ),
   1451                 "CHF:3",
   1452                 "CHF:0",
   1453                 Some("Taler FJDQ7W6G7NWX4H9M1MKA12090FRC9K7DA6N0FANDZZFXTR6QHX5G Test.,-"),
   1454                 "2024-04-12",
   1455                 Some(("DE84500105177118117964", "John Smith")),
   1456             )],
   1457             &[
   1458                 tx_out(
   1459                     (None, Some("COMPAT_SUCCESS"), Some("2024041801514102000")),
   1460                     "CHF:2",
   1461                     "CHF:0",
   1462                     Some("TestABC123"),
   1463                     "2024-04-18",
   1464                     Some(("DE20500105172419259181", "John Smith")),
   1465                 ),
   1466                 tx_out(
   1467                     (
   1468                         None,
   1469                         Some("FD622SMXKT5QWSAHDY0H8NYG3G"),
   1470                         Some("2024090216552232000"),
   1471                     ),
   1472                     "CHF:1.1",
   1473                     "CHF:0",
   1474                     Some("single 2024-09-02T14:29:52.875253314Z"),
   1475                     "2024-09-02",
   1476                     Some(("DE89500105173198527518", "Grothoff Hans")),
   1477                 ),
   1478                 tx_out(
   1479                     (
   1480                         None,
   1481                         Some("YF5QBARGQ0MNY0VK59S477VDG4"),
   1482                         Some("2024041810552821000"),
   1483                     ),
   1484                     "CHF:1.1",
   1485                     "CHF:0",
   1486                     Some("Simple tx"),
   1487                     "2024-04-18",
   1488                     Some(("DE20500105172419259181", "John Smith")),
   1489                 ),
   1490                 tx_out(
   1491                     (None, Some("IVMIGCUIE7Q7VOF73R8GU3KGRYBZPAYC5V"), None),
   1492                     "CHF:44",
   1493                     "CHF:0",
   1494                     Some("init payment"),
   1495                     "2024-09-20",
   1496                     Some(("CH4189144589712575493", "Test")),
   1497                 ),
   1498                 tx_out(
   1499                     (None, Some("CDFN7I4FVIZ848DGDQ35DZ2K49H9EWXGAW"), None),
   1500                     "CHF:44",
   1501                     "CHF:0",
   1502                     Some("init payment"),
   1503                     "2024-09-20",
   1504                     Some(("CH4189144589712575493", "Test")),
   1505                 ),
   1506                 tx_out(
   1507                     (None, Some("35M1268GW5ZFHS5JCB41UKDQNPMD40T849"), None),
   1508                     "CHF:44",
   1509                     "CHF:0",
   1510                     Some("init payment"),
   1511                     "2024-09-20",
   1512                     Some(("CH4189144589712575493", "Test")),
   1513                 ),
   1514                 tx_out(
   1515                     (None, Some("HPOMV7A4E3P1TK9UZJS1WTM94A9V3X2SR1"), None),
   1516                     "CHF:44",
   1517                     "CHF:0",
   1518                     Some("init payment"),
   1519                     "2024-09-20",
   1520                     Some(("CH4189144589712575493", "Test")),
   1521                 ),
   1522                 tx_out(
   1523                     (
   1524                         None,
   1525                         Some("KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5"),
   1526                         Some("2024092100252498000"),
   1527                     ),
   1528                     "CHF:0.42",
   1529                     "CHF:0",
   1530                     Some("This should fail because bad iban"),
   1531                     "2024-09-23",
   1532                     Some(("DE18500105173385245163", "John Smith")),
   1533                 ),
   1534                 tx_out(
   1535                     (None, Some("27SK3166EG36SJ7VP7VFYP0MW8"), None),
   1536                     "CHF:44",
   1537                     "CHF:0",
   1538                     Some("init payment"),
   1539                     "2024-09-04",
   1540                     Some(("CH4189144589712575493", "Test")),
   1541                 ),
   1542             ],
   1543             &[],
   1544         )
   1545         .await;
   1546     }
   1547 
   1548     /** Maerki Baumann dialect test */
   1549     #[db_test]
   1550     async fn maerki_baumann(db: PgPool) {
   1551         let cfg = setup_cfg("maerki_baumann.conf");
   1552         batches(
   1553             &db,
   1554             &[(
   1555                 "BATCH_SINGLE_REPORTING",
   1556                 &[
   1557                     gen_init_pay!("5IBJZOWESQGPCSOXSNNBBY49ZURI5W7Q4H"),
   1558                     gen_init_pay!("XZ15UR0XU52QWI7Q4XB88EDS44PLH7DYXH"),
   1559                     gen_init_pay!("A09R35EW0359SZ51464E7TC37A0P2CBK04"),
   1560                     gen_init_pay!("UYXZ78LE9KAIMBY6UNXFYT1K8KNY8VLZLT"),
   1561                 ],
   1562             )],
   1563         )
   1564         .await;
   1565 
   1566         register(
   1567             &db,
   1568             &cfg,
   1569             "maerki_baumann_camt053.xml",
   1570             &OrderDoc::statement,
   1571         )
   1572         .await;
   1573 
   1574         check(
   1575             &db,
   1576             &[(
   1577                 "BATCH_SINGLE_REPORTING",
   1578                 success,
   1579                 &[
   1580                     ("5IBJZOWESQGPCSOXSNNBBY49ZURI5W7Q4H", success),
   1581                     ("XZ15UR0XU52QWI7Q4XB88EDS44PLH7DYXH", success),
   1582                     ("A09R35EW0359SZ51464E7TC37A0P2CBK04", success),
   1583                     ("UYXZ78LE9KAIMBY6UNXFYT1K8KNY8VLZLT", success),
   1584                 ],
   1585             )],
   1586             &[
   1587                 tx_in(
   1588                     (
   1589                         Some("adbe4a5a-6cea-4263-b259-8ab964561a32"),
   1590                         Some("41103099704.0002"),
   1591                         Some("ZV20241104/765446/1"),
   1592                     ),
   1593                     "CHF:1",
   1594                     "CHF:0.2",
   1595                     Some("SFHP6H24C16A5J05Q3FJW2XN1PB3EK70ZPY 5SJ30ADGY68FWN68G"),
   1596                     "2024-11-04",
   1597                     Some(("CH7389144832588726658", "Mr Test")),
   1598                 ),
   1599                 tx_in(
   1600                     (
   1601                         Some("7371795e-62fa-42dd-93b7-da89cc120faa"),
   1602                         Some("41103099704.0003"),
   1603                         Some("ZV20241104/765447/1"),
   1604                     ),
   1605                     "CHF:1",
   1606                     "CHF:0.2",
   1607                     Some("Random subject"),
   1608                     "2024-11-04",
   1609                     Some(("CH7389144832588726658", "Mr Test")),
   1610                 ),
   1611                 tx_in(
   1612                     (None, Some("50523424675.0001"), Some("ZV20250523/851716/1")),
   1613                     "CHF:0.5",
   1614                     "CHF:0.2",
   1615                     None,
   1616                     "2025-05-23",
   1617                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1618                 ),
   1619                 tx_in(
   1620                     (
   1621                         Some("f203fbb4-6e13-4c78-9b2a-d852fea6374a"),
   1622                         Some("41202060702.0001"),
   1623                         Some("ZV20241202/778108/1"),
   1624                     ),
   1625                     "CHF:0.05",
   1626                     "CHF:0.2",
   1627                     Some("mini"),
   1628                     "2024-12-02",
   1629                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1630                 ),
   1631                 tx_in(
   1632                     (
   1633                         Some("81b0d8c6-a677-4577-b75e-a639dcc03681"),
   1634                         Some("41120636093.0001"),
   1635                         Some("ZV20241121/773118/1"),
   1636                     ),
   1637                     "CHF:0.1",
   1638                     "CHF:0.2",
   1639                     Some("small transfer test"),
   1640                     "2024-11-21",
   1641                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1642                 ),
   1643                 tx_in(
   1644                     (None, None, Some("ZV20250114/796191/1")),
   1645                     "CHF:3003",
   1646                     "CHF:0",
   1647                     Some("Fix bad payment by MB."),
   1648                     "2025-01-27",
   1649                     None,
   1650                 ),
   1651                 tx_in(
   1652                     (None, Some("F000787951230001"), Some("ZV20250526/852733/1")),
   1653                     "CHF:1.38",
   1654                     "CHF:0.2",
   1655                     Some("Taler XT3D9MADR4V85JBWX47SMJFDQD2FDZDHHPH8R25YDG1KNVTSEH6G"),
   1656                     "2025-05-26",
   1657                     Some(("DE20500105172419259181", "Mr German")),
   1658                 ),
   1659             ],
   1660             &[
   1661                 tx_out(
   1662                     (
   1663                         None,
   1664                         Some("5IBJZOWESQGPCSOXSNNBBY49ZURI5W7Q4H"),
   1665                         Some("ZV20241121/773541/1"),
   1666                     ),
   1667                     "CHF:0.1",
   1668                     "CHF:0",
   1669                     Some("multi 0 2024-11-21T15:21:59.8859234 63Z"),
   1670                     "2024-11-27",
   1671                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1672                 ),
   1673                 tx_out(
   1674                     (
   1675                         None,
   1676                         Some("XZ15UR0XU52QWI7Q4XB88EDS44PLH7DYXH"),
   1677                         Some("ZV20241121/773541/4"),
   1678                     ),
   1679                     "CHF:0.13",
   1680                     "CHF:0",
   1681                     Some("multi 3 2024-11-21T15:21:59.8859234 63Z"),
   1682                     "2024-11-27",
   1683                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1684                 ),
   1685                 tx_out(
   1686                     (
   1687                         None,
   1688                         Some("A09R35EW0359SZ51464E7TC37A0P2CBK04"),
   1689                         Some("ZV20241121/773541/3"),
   1690                     ),
   1691                     "CHF:0.12",
   1692                     "CHF:0",
   1693                     Some("multi 2 2024-11-21T15:21:59.8859234 63Z"),
   1694                     "2024-11-27",
   1695                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1696                 ),
   1697                 tx_out(
   1698                     (
   1699                         None,
   1700                         Some("UYXZ78LE9KAIMBY6UNXFYT1K8KNY8VLZLT"),
   1701                         Some("ZV20241121/773541/2"),
   1702                     ),
   1703                     "CHF:0.11",
   1704                     "CHF:0",
   1705                     Some("multi 1 2024-11-21T15:21:59.8859234 63Z"),
   1706                     "2024-11-27",
   1707                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1708                 ),
   1709                 tx_out(
   1710                     (None, None, Some("GB20241220/205792/1")),
   1711                     "CHF:3000",
   1712                     "CHF:0",
   1713                     None,
   1714                     "2024-12-20",
   1715                     None,
   1716                 ),
   1717             ],
   1718             &[
   1719                 tx_out(
   1720                     (None, None, None),
   1721                     "CHF:1",
   1722                     "CHF:0",
   1723                     Some("bounce 7371795e-62fa-42dd-93b7-da89cc120faa: missing reserve public key"),
   1724                     "1970-01-01",
   1725                     Some(("CH7389144832588726658", "Mr Test")),
   1726                 ),
   1727                 tx_out(
   1728                     (None, None, None),
   1729                     "CHF:0.5",
   1730                     "CHF:0",
   1731                     Some("bounce 50523424675.0001: missing reserve public key"),
   1732                     "1970-01-01",
   1733                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1734                 ),
   1735                 tx_out(
   1736                     (None, None, None),
   1737                     "CHF:0.05",
   1738                     "CHF:0",
   1739                     Some("bounce f203fbb4-6e13-4c78-9b2a-d852fea6374a: missing reserve public key"),
   1740                     "1970-01-01",
   1741                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1742                 ),
   1743                 tx_out(
   1744                     (None, None, None),
   1745                     "CHF:0.1",
   1746                     "CHF:0",
   1747                     Some("bounce 81b0d8c6-a677-4577-b75e-a639dcc03681: missing reserve public key"),
   1748                     "1970-01-01",
   1749                     Some(("CH7389144832588726658", "Grothoff Hans")),
   1750                 ),
   1751                 tx_out(
   1752                     (None, None, None),
   1753                     "CHF:1.38",
   1754                     "CHF:0",
   1755                     Some("bounce F000787951230001: restricted account"),
   1756                     "1970-01-01",
   1757                     Some(("DE20500105172419259181", "Mr German")),
   1758                 ),
   1759             ],
   1760         )
   1761         .await;
   1762     }
   1763 
   1764     /** PostFinance dialect test */
   1765     #[db_test]
   1766     async fn postfinance(db: PgPool) {
   1767         let cfg = setup_cfg("postfinance.conf");
   1768         batches(
   1769             &db,
   1770             &[
   1771                 (
   1772                     "ZS1PGNTSV0ZNDFAJBBWWB8015G",
   1773                     &[
   1774                         // Legacy notifications use MsgId when EndToEndId is NOTPROVIDED.
   1775                         gen_init_pay!(
   1776                             "ZS1PGNTSV0ZNDFAJBBWWB8015G",
   1777                             "init payment",
   1778                             amount("CHF:3")
   1779                         ),
   1780                         gen_init_pay!(
   1781                             "POSTFINANCE_NOTIFICATION_BATCH",
   1782                             "init payment",
   1783                             amount("CHF:403")
   1784                         ),
   1785                     ],
   1786                 ),
   1787                 (
   1788                     "50820f78-9024-44ff-978d-63a18c",
   1789                     &[gen_init_pay!(
   1790                         "50820f78-9024-44ff-978d-63a18c",
   1791                         "init payment",
   1792                         amount("CHF:1")
   1793                     )],
   1794                 ),
   1795                 (
   1796                     "889d1a80-1267-49bd-8fcc-85701a",
   1797                     &[gen_init_pay!(
   1798                         "889d1a80-1267-49bd-8fcc-85701a",
   1799                         "init payment",
   1800                         amount("CHF:1")
   1801                     )],
   1802                 ),
   1803                 (
   1804                     "4cc61cc7-6230-49c2-b5e2-b40bbb",
   1805                     &[gen_init_pay!(
   1806                         "4cc61cc7-6230-49c2-b5e2-b40bbb",
   1807                         "init payment",
   1808                         amount("CHF:1")
   1809                     )],
   1810                 ),
   1811                 (
   1812                     "EB4D22D428214261B2B3012D2A8CEC36",
   1813                     &[
   1814                         gen_init_pay!(
   1815                             "POSTFINANCE_STATEMENT_FIRST",
   1816                             "init payment",
   1817                             amount("CHF:100")
   1818                         ),
   1819                         gen_init_pay!(
   1820                             "POSTFINANCE_STATEMENT_SECOND",
   1821                             "init payment",
   1822                             amount("CHF:306")
   1823                         ),
   1824                     ],
   1825                 ),
   1826             ],
   1827         )
   1828         .await;
   1829 
   1830         register(
   1831             &db,
   1832             &cfg,
   1833             "postfinance_camt054.xml",
   1834             &OrderDoc::notification,
   1835         )
   1836         .await;
   1837         register(&db, &cfg, "postfinance_camt053.xml", &OrderDoc::statement).await;
   1838 
   1839         check(
   1840             &db,
   1841             &[
   1842                 (
   1843                     "ZS1PGNTSV0ZNDFAJBBWWB8015G",
   1844                     success,
   1845                     &[
   1846                         ("ZS1PGNTSV0ZNDFAJBBWWB8015G", success),
   1847                         ("POSTFINANCE_NOTIFICATION_BATCH", success),
   1848                     ],
   1849                 ),
   1850                 (
   1851                     "50820f78-9024-44ff-978d-63a18c",
   1852                     success,
   1853                     &[("50820f78-9024-44ff-978d-63a18c", permanent_failure)],
   1854                 ),
   1855                 (
   1856                     "889d1a80-1267-49bd-8fcc-85701a",
   1857                     success,
   1858                     &[("889d1a80-1267-49bd-8fcc-85701a", permanent_failure)],
   1859                 ),
   1860                 (
   1861                     "4cc61cc7-6230-49c2-b5e2-b40bbb",
   1862                     success,
   1863                     &[("4cc61cc7-6230-49c2-b5e2-b40bbb", permanent_failure)],
   1864                 ),
   1865                 (
   1866                     "EB4D22D428214261B2B3012D2A8CEC36",
   1867                     success,
   1868                     &[
   1869                         ("POSTFINANCE_STATEMENT_FIRST", success),
   1870                         ("POSTFINANCE_STATEMENT_SECOND", success),
   1871                     ],
   1872                 ),
   1873             ],
   1874             &[
   1875                 tx_in(
   1876                     (
   1877                         Some("62e2b511-7313-4ccd-8d40-c9d8e612cd71"),
   1878                         None,
   1879                         Some("231121CH0AZWCR9T"),
   1880                     ),
   1881                     "CHF:10",
   1882                     "CHF:0",
   1883                     Some("G1XTY6HGWGMVRM7E6XQ4JHJK561ETFDFTJZ7JVGV543XZCB27YBG"),
   1884                     "2023-12-19",
   1885                     Some(("CH7389144832588726658", "Mr Test")),
   1886                 ),
   1887                 tx_in(
   1888                     (
   1889                         Some("62e2b511-7313-4ccd-8d40-c9d8e612cd72"),
   1890                         None,
   1891                         Some("231121CH0AZWCVR1"),
   1892                     ),
   1893                     "CHF:2.53",
   1894                     "CHF:0",
   1895                     Some("G1XTY6HGWGMVRM7E6XQ4JHJK561ETFDFTJZ7JVGV543XZCB27YB"),
   1896                     "2023-12-19",
   1897                     Some(("CH7389144832588726658", "Mr Test")),
   1898                 ),
   1899             ],
   1900             &[
   1901                 tx_out(
   1902                     (None, Some("ZS1PGNTSV0ZNDFAJBBWWB8015G"), None),
   1903                     "CHF:3",
   1904                     "CHF:0",
   1905                     None,
   1906                     "2024-01-15",
   1907                     None,
   1908                 ),
   1909                 tx_out(
   1910                     (None, Some("POSTFINANCE_NOTIFICATION_BATCH"), None),
   1911                     "CHF:403",
   1912                     "CHF:0",
   1913                     Some("init payment"),
   1914                     "2024-01-15",
   1915                     Some(("CH4189144589712575493", "Test")),
   1916                 ),
   1917                 tx_out(
   1918                     (None, Some("POSTFINANCE_STATEMENT_FIRST"), None),
   1919                     "CHF:100",
   1920                     "CHF:0",
   1921                     Some("init payment"),
   1922                     "2024-08-26",
   1923                     Some(("CH4189144589712575493", "Test")),
   1924                 ),
   1925                 tx_out(
   1926                     (None, Some("POSTFINANCE_STATEMENT_SECOND"), None),
   1927                     "CHF:306",
   1928                     "CHF:0",
   1929                     Some("init payment"),
   1930                     "2024-08-26",
   1931                     Some(("CH4189144589712575493", "Test")),
   1932                 ),
   1933             ],
   1934             &[tx_out(
   1935                 (None, None, None),
   1936                 "CHF:2.53",
   1937                 "CHF:0",
   1938                 Some("bounce 62e2b511-7313-4ccd-8d40-c9d8e612cd72: missing reserve public key"),
   1939                 "1970-01-01",
   1940                 Some(("CH7389144832588726658", "Mr Test")),
   1941             )],
   1942         )
   1943         .await;
   1944     }
   1945 }