libeufin

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

ebics.rs (24084B)


      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::{borrow::Cow, io::Write as _};
     21 
     22 use aws_lc_rs::{
     23     digest::Digest,
     24     encoding::AsDer,
     25     error::KeyRejected,
     26     rsa::{self, PrivateDecryptingKey, PublicKey},
     27 };
     28 use compact_str::CompactString;
     29 use flate2::write::ZlibDecoder;
     30 use jiff::Timestamp;
     31 use rand::{RngExt as _, distr::Alphanumeric, seq::IndexedRandom as _};
     32 use reqwest::{
     33     Client, ClientBuilder, StatusCode,
     34     header::{CONTENT_TYPE, HeaderValue},
     35 };
     36 use roxmltree::Document;
     37 use serde::{Deserialize, Deserializer, Serialize, Serializer};
     38 use sqlx::PgPool;
     39 use taler_common::encoding::base64;
     40 use thiserror::Error;
     41 use tracing::{debug, info, trace};
     42 
     43 use crate::{
     44     cli::EbicsLogs,
     45     config::EbicsHostCfg,
     46     crypto::{
     47         decrypt_ebics_e002, decrypt_ebics_e002_key, digest_ebics_order_a006, encrypt_ebics_e002,
     48         gen_ebics_e002_key, sign_ebics_a006,
     49     },
     50     db::{ebics_first, ebics_register, ebics_remove},
     51     ebics::{
     52         administrative::{HAA, HKD, VersionNumber, hev_msg, parse_haa, parse_hev, parse_hkd},
     53         bts::{
     54             DInit, DTransfer, DataEncryptionInfo, UInit, d_init, d_transfer, parse_d_init,
     55             parse_d_transfer, parse_receipt, parse_u_init, parse_u_transfer, receipt, u_init,
     56             u_transfer,
     57         },
     58         ebics_code::EbicsReturnCode,
     59         logger::EbicsLogger,
     60         order::Order,
     61     },
     62     keys::{BankKeys, ClientKeys},
     63     utils::deflate,
     64     xml,
     65     xml_sign::{VerifyError, verify_ebics},
     66 };
     67 
     68 pub mod administrative;
     69 pub mod bts;
     70 pub mod ebics_code;
     71 pub mod key_management;
     72 pub mod logger;
     73 pub mod order;
     74 
     75 const EBICS_ID_ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
     76 
     77 pub fn rand_ebics_id() -> CompactString {
     78     let mut rng = rand::rng();
     79     (0..34)
     80         .map(|_| *EBICS_ID_ALPHABET.choose(&mut rng).unwrap() as char)
     81         .collect()
     82 }
     83 
     84 #[derive(Debug, Clone, Copy)]
     85 pub enum Phase {
     86     Interrupt,
     87     Init,
     88     Transfer(usize),
     89     Process,
     90     Receipt,
     91 }
     92 
     93 impl std::fmt::Display for Phase {
     94     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
     95         match self {
     96             Phase::Interrupt => f.write_str("interrupt"),
     97             Phase::Init => f.write_str("init"),
     98             Phase::Transfer(i) => write!(f, "transfer{i}"),
     99             Phase::Process => f.write_str("process"),
    100             Phase::Receipt => f.write_str("receipt"),
    101         }
    102     }
    103 }
    104 
    105 #[derive(Debug)]
    106 pub struct EbicsCtx<'a> {
    107     pub now: Timestamp,
    108     pub order: Cow<'a, Order>,
    109     pub phase: Option<Phase>,
    110     pub tx_id: Option<CompactString>,
    111 }
    112 
    113 impl<'a> EbicsCtx<'a> {
    114     pub fn new(order: &'a Order) -> Self {
    115         Self {
    116             now: Timestamp::now(),
    117             order: Cow::Borrowed(order),
    118             phase: None,
    119             tx_id: None,
    120         }
    121     }
    122 
    123     pub fn init(self) -> Self {
    124         Self {
    125             phase: Some(Phase::Init),
    126             tx_id: None,
    127             ..self
    128         }
    129     }
    130 
    131     pub fn interrupt(self, id: &str) -> Self {
    132         Self {
    133             phase: Some(Phase::Interrupt),
    134             tx_id: Some(
    135                 self.tx_id
    136                     .filter(|it| it != id)
    137                     .unwrap_or_else(|| id.into()),
    138             ),
    139             ..self
    140         }
    141     }
    142 
    143     pub fn transfer(self, id: &str, segment: usize) -> Self {
    144         Self {
    145             phase: Some(Phase::Transfer(segment)),
    146             tx_id: Some(
    147                 self.tx_id
    148                     .filter(|it| it != id)
    149                     .unwrap_or_else(|| id.into()),
    150             ),
    151             ..self
    152         }
    153     }
    154 
    155     pub fn process(self, id: &str) -> Self {
    156         Self {
    157             phase: Some(Phase::Process),
    158             tx_id: Some(
    159                 self.tx_id
    160                     .filter(|it| it != id)
    161                     .unwrap_or_else(|| id.into()),
    162             ),
    163             ..self
    164         }
    165     }
    166 
    167     pub fn receipt(self, id: &str) -> Self {
    168         Self {
    169             phase: Some(Phase::Receipt),
    170             tx_id: Some(
    171                 self.tx_id
    172                     .filter(|it| it != id)
    173                     .unwrap_or_else(|| id.into()),
    174             ),
    175             ..self
    176         }
    177     }
    178 }
    179 
    180 impl std::fmt::Display for EbicsCtx<'_> {
    181     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    182         let Self {
    183             order,
    184             phase,
    185             tx_id,
    186             ..
    187         } = self;
    188         write!(f, "{order}")?;
    189         if let Some(phase) = phase {
    190             write!(f, " {phase}")?;
    191         }
    192         if let Some(tx_id) = tx_id {
    193             write!(f, " {tx_id}")?;
    194         }
    195         Ok(())
    196     }
    197 }
    198 
    199 #[derive(Debug, thiserror::Error)]
    200 pub struct EbicsError {
    201     pub ctx: Box<EbicsCtx<'static>>,
    202     pub kind: EbicsErrKind,
    203 }
    204 
    205 impl std::fmt::Display for EbicsError {
    206     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    207         let Self { ctx, kind } = self;
    208         write!(f, "{ctx}: {kind}")
    209     }
    210 }
    211 
    212 fn fmt_code(
    213     f: &mut std::fmt::Formatter<'_>,
    214     technical: &EbicsReturnCode,
    215     bank: &EbicsReturnCode,
    216 ) -> std::fmt::Result {
    217     if technical.is_error() {
    218         write!(f, "technical error: {technical}")
    219     } else {
    220         write!(f, "bank error: {bank}")
    221     }
    222 }
    223 
    224 pub trait EbicsErrorHelper<T> {
    225     fn ctx(self, ctx: &EbicsCtx<'_>) -> Result<T, EbicsError>;
    226 }
    227 
    228 impl<T, E: Into<EbicsErrKind>> EbicsErrorHelper<T> for Result<T, E> {
    229     fn ctx(self, ctx: &EbicsCtx<'_>) -> Result<T, EbicsError> {
    230         self.map_err(|e| e.into().ctx(ctx))
    231     }
    232 }
    233 
    234 #[derive(Debug, thiserror::Error)]
    235 pub enum EbicsErrKind {
    236     #[error(transparent)]
    237     Network(#[from] reqwest::Error),
    238 
    239     #[error(transparent)]
    240     IO(#[from] std::io::Error),
    241 
    242     #[error("ebics HTTP error {0}")]
    243     HTTP(StatusCode),
    244 
    245     #[error(transparent)]
    246     XML(#[from] xml::Error),
    247 
    248     #[error("{}", std::fmt::from_fn(|f| fmt_code(f, technical, bank)))]
    249     Code {
    250         technical: EbicsReturnCode,
    251         bank: EbicsReturnCode,
    252     },
    253 
    254     #[error(transparent)]
    255     Db(#[from] sqlx::Error),
    256 
    257     #[error(transparent)]
    258     Zip(#[from] zip::result::ZipError),
    259 
    260     #[error(transparent)]
    261     Signature(#[from] VerifyError),
    262 
    263     #[error(transparent)]
    264     DecDe(#[from] DecDeErr),
    265 
    266     #[error("{0}")]
    267     Custom(Cow<'static, str>),
    268 }
    269 
    270 impl EbicsErrKind {
    271     pub fn ctx(self, ctx: &EbicsCtx<'_>) -> EbicsError {
    272         EbicsError {
    273             ctx: Box::new(EbicsCtx {
    274                 now: ctx.now,
    275                 phase: ctx.phase,
    276                 tx_id: ctx.tx_id.clone(),
    277                 order: Cow::Owned(ctx.order.as_ref().clone()),
    278             }),
    279             kind: self,
    280         }
    281     }
    282 }
    283 pub struct EbicsResponse<T> {
    284     pub technical_code: EbicsReturnCode,
    285     pub bank_code: EbicsReturnCode,
    286     pub technical_text: CompactString,
    287     pub content: Option<T>,
    288 }
    289 
    290 impl<T> EbicsResponse<T> {
    291     fn ok_or_fail(self) -> Result<T, EbicsErrKind> {
    292         if let Some(content) = self.content
    293             && !self.technical_code.is_error()
    294             && !self.bank_code.is_error()
    295         {
    296             Ok(content)
    297         } else {
    298             Err(EbicsErrKind::Code {
    299                 technical: self.technical_code,
    300                 bank: self.bank_code,
    301             })
    302         }
    303     }
    304 }
    305 
    306 pub struct EbicsClient<'a> {
    307     cfg: EbicsHostCfg<'a>,
    308     pub http: Client,
    309     logger: EbicsLogger,
    310 }
    311 
    312 impl<'a> EbicsClient<'a> {
    313     pub fn new(cfg: EbicsHostCfg<'a>, log: EbicsLogs) -> anyhow::Result<Self> {
    314         let mut builder = ClientBuilder::new();
    315         if let Some(unix_path) = cfg.unix_path {
    316             builder = builder.unix_socket(unix_path);
    317         }
    318         Ok(Self {
    319             cfg,
    320             http: builder.build()?,
    321             logger: EbicsLogger::new(log.dir)?,
    322         })
    323     }
    324 
    325     async fn post_to_bank(&self, xml: String, ctx: &EbicsCtx<'_>) -> Result<Vec<u8>, EbicsError> {
    326         self.logger.log_request(ctx, &xml)?;
    327         let res = self
    328             .http
    329             .post(self.cfg.base_url)
    330             .header(CONTENT_TYPE, HeaderValue::from_static("application/xml"))
    331             .body(xml)
    332             .send()
    333             .await
    334             .ctx(ctx)?;
    335         let status = res.status();
    336         if status != StatusCode::OK {
    337             self.logger.log_failure(ctx, res).await?;
    338             return Err(EbicsErrKind::HTTP(status).ctx(ctx));
    339         }
    340         let xml = res.bytes().await.ctx(ctx)?;
    341         self.logger.log_response(ctx, &xml)?;
    342         Ok(xml.into())
    343     }
    344 
    345     /** POST an EBICS BTS request [xmlReq] using [client] returning a validated and parsed XML response */
    346     pub async fn post_bts<T>(
    347         &self,
    348         xml: String,
    349         ctx: &EbicsCtx<'_>,
    350         key: &PublicKey,
    351         parse: impl FnOnce(Document) -> xml::Result<EbicsResponse<T>>,
    352     ) -> Result<T, EbicsError> {
    353         let xml = self.post_to_bank(xml, ctx).await?;
    354         let doc = verify_ebics(&xml, key).ctx(ctx)?;
    355         let res = parse(doc).ctx(ctx)?;
    356         trace!(target: "ebics",
    357             "{ctx}: {} {} - {}",
    358             res.technical_code,
    359             res.bank_code,
    360             res.technical_text
    361         );
    362         res.ok_or_fail().ctx(ctx)
    363     }
    364 
    365     pub async fn hev(&self) -> Result<Box<[VersionNumber]>, EbicsError> {
    366         let order = Order::HEV;
    367         info!(target: "ebics", "Doing administrative request {order}");
    368         let msg = hev_msg(&self.cfg);
    369         let ctx = EbicsCtx::new(&order);
    370         let res = self.post_to_bank(msg, &ctx).await?;
    371         parse_hev(&res).ctx(&ctx)?.ok_or_fail().ctx(&ctx)
    372     }
    373 
    374     pub async fn haa(
    375         &self,
    376         db: &PgPool,
    377         client: &ClientKeys,
    378         bank: &BankKeys,
    379         peek: bool,
    380     ) -> Result<HAA, EbicsError> {
    381         self.download(
    382             db,
    383             client,
    384             bank,
    385             &Order::HAA,
    386             &None,
    387             peek,
    388             async |content| Ok(parse_haa(&content)?),
    389         )
    390         .await
    391     }
    392 
    393     pub async fn hkd(
    394         &self,
    395         db: &PgPool,
    396         client: &ClientKeys,
    397         bank: &BankKeys,
    398         peek: bool,
    399     ) -> Result<HKD, EbicsError> {
    400         self.download(
    401             db,
    402             client,
    403             bank,
    404             &Order::HKD,
    405             &None,
    406             peek,
    407             async |content| Ok(parse_hkd(&content)?),
    408         )
    409         .await
    410     }
    411 
    412     /**
    413      * Performs an EBICS download transaction of [order] between [startDate] and [endDate].
    414      * Download content is passed to [processing]
    415      *
    416      * It conducts init -> transfer -> processing -> receipt phases.
    417      *
    418      * Cancellations and failures are handled.
    419      */
    420     pub async fn download<T>(
    421         &self,
    422         db: &PgPool,
    423         client: &ClientKeys,
    424         bank: &BankKeys,
    425         order: &Order,
    426         range: &Option<(Timestamp, Timestamp)>,
    427         peek: bool,
    428         processing: impl AsyncFnOnce(Vec<u8>) -> Result<T, EbicsErrKind>,
    429     ) -> Result<T, EbicsError> {
    430         let mut ctx = EbicsCtx::new(order);
    431         debug!(target: "ebics", "Downloading order {order}{}", std::fmt::from_fn(|f| {
    432             if let Some((start, end)) = range {
    433                 write!(f, " from {start} to {end}")?
    434             }
    435             Ok(())
    436         }));
    437 
    438         // Close interrupted
    439         while let Some(tx_id) = ebics_first(db).await.ctx(&ctx)? {
    440             let ctx = EbicsCtx::new(order).interrupt(&tx_id);
    441             let xml = receipt(&self.cfg, client, order, &tx_id, false);
    442             if let Err(e) = self.post_bts(xml, &ctx, &bank.auth, parse_receipt).await {
    443                 if !matches!(
    444                     e.kind,
    445                     // Transaction already closed or expired - EBICS protocol error
    446                     EbicsErrKind::Code {
    447                         technical: EbicsReturnCode::EBICS_TX_UNKNOWN_TXID,
    448                         ..
    449                     } |
    450                     // Transaction already closed or expired - HTTP protocol error for non compliant banks
    451                     EbicsErrKind::HTTP(StatusCode::BAD_REQUEST)
    452                 ) {
    453                     return Err(e);
    454                 } else {
    455                     debug!(target: "ebics", "{e}")
    456                 }
    457             }
    458             ebics_remove(db, &tx_id).await.ctx(&ctx)?;
    459         }
    460 
    461         // Init phase
    462         ctx = ctx.init();
    463         let xml = d_init(&self.cfg, bank, client, order, range);
    464         let DInit {
    465             tx_id,
    466             nb_segments,
    467             segment,
    468             data_encryption_info,
    469         } = self.post_bts(xml, &ctx, &bank.auth, parse_d_init).await?;
    470         ebics_register(db, &tx_id).await.ctx(&ctx)?;
    471 
    472         // Transfer phase
    473         let mut payload = segment;
    474         for segment_nb in 2..=nb_segments {
    475             ctx = ctx.transfer(&tx_id, segment_nb);
    476             let xml = d_transfer(&self.cfg, client, order, nb_segments, segment_nb, &tx_id);
    477             let DTransfer { segment, .. } = self
    478                 .post_bts(xml, &ctx, &bank.auth, parse_d_transfer)
    479                 .await?;
    480             payload.extend(segment);
    481         }
    482 
    483         // Processing phase
    484         ctx = ctx.process(&tx_id);
    485         let payload = decrypt_and_decompress_payload(&client.enc, data_encryption_info, payload)
    486             .map_err(|e| {
    487                 EbicsErrKind::Custom(Cow::Owned(format!("decrypt & decompress: {e}"))).ctx(&ctx)
    488             })?;
    489         self.logger.log_payload(&ctx, &payload, order.file_type())?;
    490         let res = processing(payload).await.ctx(&ctx);
    491 
    492         // Receipt phase
    493         ctx = ctx.receipt(&tx_id);
    494         let xml = receipt(&self.cfg, client, order, &tx_id, res.is_ok() && !peek);
    495         self.post_bts(xml, &ctx, &bank.auth, parse_receipt).await?;
    496         ebics_remove(db, &tx_id).await.ctx(&ctx)?;
    497 
    498         res
    499     }
    500 
    501     /**
    502      * Performs an EBICS upload transaction of [order] using [payload].
    503      *
    504      * It conducts init -> upload phases.
    505      *
    506      * Returns upload orderID
    507      */
    508     pub async fn upload(
    509         &self,
    510         client: &ClientKeys,
    511         bank: &BankKeys,
    512         order: &Order,
    513         payload: &[u8],
    514     ) -> Result<CompactString, EbicsError> {
    515         debug!(target: "ebics", "Uploading order {order}");
    516         let mut ctx = EbicsCtx::new(order);
    517 
    518         self.logger.log_payload(&ctx, payload, "xml")?;
    519         let payload = prepare_upload_payload(&self.cfg, client, bank, payload);
    520 
    521         // Init phase
    522         ctx = ctx.init();
    523         let xml = u_init(&self.cfg, bank, client, order, &payload);
    524         let UInit { tx_id, order_id } = self.post_bts(xml, &ctx, &bank.auth, parse_u_init).await?;
    525 
    526         // Transfer phase
    527         for segment_nb in 1..=payload.nb_segments() {
    528             ctx = ctx.transfer(&tx_id, segment_nb);
    529             let xml = u_transfer(&self.cfg, client, order, &tx_id, &payload, segment_nb);
    530             self.post_bts(xml, &ctx, &bank.auth, parse_u_transfer)
    531                 .await?;
    532         }
    533 
    534         Ok(order_id)
    535     }
    536 }
    537 
    538 pub struct PreparedUploadData {
    539     encrypted_key: Vec<u8>,
    540     signature_data: String,
    541     digest: Digest,
    542     payload: String,
    543 }
    544 
    545 impl PreparedUploadData {
    546     const CHUNK_SIZE: usize = 1000000;
    547 
    548     pub fn nb_segments(&self) -> usize {
    549         self.payload.len().div_ceil(Self::CHUNK_SIZE)
    550     }
    551 
    552     pub fn segment(&self, nb: usize) -> &str {
    553         let start = (nb - 1) * Self::CHUNK_SIZE;
    554         let end = (start + Self::CHUNK_SIZE).min(self.payload.len());
    555         &self.payload[start..end]
    556     }
    557 }
    558 
    559 #[derive(Debug, Error)]
    560 pub enum DecDeErr {
    561     #[error(transparent)]
    562     Zlib(#[from] std::io::Error),
    563     #[error("bank E002 encryption public-key digest mismatch")]
    564     Digest,
    565     #[error(transparent)]
    566     Key(#[from] KeyRejected),
    567     #[error("E002 decryption failed")]
    568     Decrypt,
    569 }
    570 
    571 /** Decrypts and decompresses EBICS BTS payload */
    572 fn decrypt_and_decompress_payload(
    573     key_pair: &rsa::KeyPair,
    574     encryption_info: DataEncryptionInfo,
    575     payload: Vec<u8>,
    576 ) -> Result<Vec<u8>, DecDeErr> {
    577     let enc_key = PrivateDecryptingKey::from_pkcs8(key_pair.as_der().unwrap().as_ref())?;
    578     let tx_key = decrypt_ebics_e002_key(enc_key, &encryption_info.tx_key);
    579     let mut decoder = ZlibDecoder::new(Vec::new());
    580     let decrypted = decrypt_ebics_e002(&tx_key, payload).map_err(|_| DecDeErr::Decrypt)?;
    581     decoder.write_all(&decrypted)?;
    582     Ok(decoder.finish()?)
    583 }
    584 
    585 /** Signs, encrypts and format EBICS BTS payload */
    586 fn prepare_upload_payload(
    587     cfg: &EbicsHostCfg,
    588     client: &ClientKeys,
    589     bank: &BankKeys,
    590     payload: &[u8],
    591 ) -> PreparedUploadData {
    592     let digest = digest_ebics_order_a006(payload);
    593 
    594     // Generate ephemeral transaction key
    595     let (tx_key, encrypted_key) = gen_ebics_e002_key(&bank.enc);
    596 
    597     // Compress and encrypt order signature
    598     let signature_data = {
    599         let signed = sign_ebics_a006(digest.as_ref(), &client.sign);
    600         let inner_signed_xml = xml!(
    601             "UserSignatureData" "xmlns"="http://www.ebics.org/S002" {
    602                 "OrderSignatureData" {
    603                     "SignatureVersion": "A006",
    604                     "SignatureValue": base64::fmt(signed),
    605                     "PartnerID": cfg.partner_id,
    606                     "UserID": cfg.user_id
    607                 }
    608             }
    609         );
    610         let deflated = deflate(inner_signed_xml.as_bytes());
    611         let encrypted = encrypt_ebics_e002(&tx_key, deflated);
    612         base64::encode(encrypted)
    613     };
    614 
    615     // Compress and encrypt payload
    616     let payload = {
    617         let deflated = deflate(payload);
    618         let encrypted = encrypt_ebics_e002(&tx_key, deflated);
    619         base64::encode(encrypted)
    620     };
    621     PreparedUploadData {
    622         encrypted_key,
    623         signature_data,
    624         digest,
    625         payload,
    626     }
    627 }
    628 
    629 #[derive(Debug)]
    630 pub struct TxCheckResult {
    631     pub concurrent_fetch_and_fetch: bool,
    632     pub concurrent_fetch_and_submit: bool,
    633     pub concurrent_submit_and_submit: bool,
    634     pub idempotent_close: bool,
    635 }
    636 
    637 /**
    638  * Test EBICS implementation's transactions semantic:
    639  * - Can two fetch transactions run concurrently ?
    640  * - Can a fetch & submit transactions run concurrently ?
    641  * - Can two submit transactions run concurrently ?
    642  * - Is closing a submit transaction idempotent
    643  */
    644 pub async fn tx_check(
    645     ebics: &EbicsClient<'_>,
    646     db: &PgPool,
    647     client: &ClientKeys,
    648     bank: &BankKeys,
    649     fetch: &Order,
    650     submit: &Order,
    651 ) -> anyhow::Result<TxCheckResult> {
    652     let mut result = TxCheckResult {
    653         concurrent_fetch_and_fetch: false,
    654         concurrent_fetch_and_submit: false,
    655         concurrent_submit_and_submit: false,
    656         idempotent_close: false,
    657     };
    658 
    659     let ctx = EbicsCtx::new(fetch).init();
    660     let DInit { tx_id, .. } = ebics
    661         .post_bts(
    662             d_init(&ebics.cfg, bank, client, fetch, &None),
    663             &ctx,
    664             &bank.auth,
    665             parse_d_init,
    666         )
    667         .await?;
    668     ebics_register(db, &tx_id).await?;
    669     {
    670         let ctx = EbicsCtx::new(fetch).init();
    671         match ebics
    672             .post_bts(
    673                 d_init(&ebics.cfg, bank, client, fetch, &None),
    674                 &ctx,
    675                 &bank.auth,
    676                 parse_d_init,
    677             )
    678             .await
    679         {
    680             Ok(DInit { tx_id, .. }) => {
    681                 ebics_register(db, &tx_id).await?;
    682                 result.concurrent_fetch_and_fetch = true;
    683                 let ctx = ctx.receipt(&tx_id);
    684                 ebics
    685                     .post_bts(
    686                         receipt(&ebics.cfg, client, fetch, &tx_id, false),
    687                         &ctx,
    688                         &bank.auth,
    689                         parse_receipt,
    690                     )
    691                     .await?;
    692                 ebics_remove(db, &tx_id).await?;
    693             }
    694             Err(e) => {
    695                 if !matches!(e.kind, EbicsErrKind::Code { .. }) {
    696                     return Err(e.into());
    697                 } else {
    698                     debug!(target: "testing", "concurrent_fetch_and_fetch {e}")
    699                 }
    700             }
    701         }
    702     }
    703 
    704     {
    705         let ctx = EbicsCtx::new(submit).init();
    706         let random_string: String = rand::rng()
    707             .sample_iter(&Alphanumeric)
    708             .take(2000000)
    709             .map(char::from)
    710             .collect();
    711         let payload = prepare_upload_payload(&ebics.cfg, client, bank, random_string.as_bytes());
    712         match ebics
    713             .post_bts(
    714                 u_init(&ebics.cfg, bank, client, submit, &payload),
    715                 &ctx,
    716                 &bank.auth,
    717                 parse_u_init,
    718             )
    719             .await
    720         {
    721             Ok(UInit { tx_id, .. }) => {
    722                 result.concurrent_fetch_and_submit = true;
    723                 let ctx = ctx.transfer(&tx_id, 1);
    724                 ebics
    725                     .post_bts(
    726                         u_transfer(&ebics.cfg, client, fetch, &tx_id, &payload, 1),
    727                         &ctx,
    728                         &bank.auth,
    729                         parse_u_transfer,
    730                     )
    731                     .await?;
    732                 let ctx = EbicsCtx::new(submit).init();
    733                 if let Err(e) = ebics
    734                     .post_bts(
    735                         u_init(&ebics.cfg, bank, client, submit, &payload),
    736                         &ctx,
    737                         &bank.auth,
    738                         parse_u_init,
    739                     )
    740                     .await
    741                 {
    742                     if !matches!(e.kind, EbicsErrKind::Code { .. }) {
    743                         return Err(e.into());
    744                     } else {
    745                         debug!(target: "testing", "concurrent_submit_and_submit {e}")
    746                     }
    747                 } else {
    748                     result.concurrent_submit_and_submit = true;
    749                 }
    750             }
    751             Err(e) => {
    752                 if !matches!(e.kind, EbicsErrKind::Code { .. }) {
    753                     return Err(e.into());
    754                 } else {
    755                     debug!(target: "testing", "concurrent_fetch_and_submit {e}")
    756                 }
    757             }
    758         }
    759     }
    760 
    761     // Close first fetch
    762     let ctx = ctx.receipt(&tx_id);
    763     ebics
    764         .post_bts(
    765             receipt(&ebics.cfg, client, fetch, &tx_id, false),
    766             &ctx,
    767             &bank.auth,
    768             parse_receipt,
    769         )
    770         .await?;
    771 
    772     ebics_remove(db, &tx_id).await?;
    773 
    774     // Close first fetch again
    775     let ctx = ctx.interrupt(&tx_id);
    776     if let Err(e) = ebics
    777         .post_bts(
    778             receipt(&ebics.cfg, client, fetch, &tx_id, false),
    779             &ctx,
    780             &bank.auth,
    781             parse_receipt,
    782         )
    783         .await
    784     {
    785         debug!(target: "testing", "idempotent_close {e}")
    786     } else {
    787         result.idempotent_close = true
    788     }
    789 
    790     Ok(result)
    791 }
    792 
    793 #[derive(Debug, Serialize, Deserialize, Clone, Default)]
    794 pub struct TaskStatus {
    795     #[serde(serialize_with = "ser_micros", deserialize_with = "de_micros", default)]
    796     pub last_successfull: Option<Timestamp>,
    797     #[serde(serialize_with = "ser_micros", deserialize_with = "de_micros", default)]
    798     pub last_trial: Option<Timestamp>,
    799 }
    800 
    801 fn ser_micros<S: Serializer>(key: &Option<Timestamp>, serializer: S) -> Result<S::Ok, S::Error> {
    802     key.map(|it| it.as_microsecond()).serialize(serializer)
    803 }
    804 
    805 fn de_micros<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<Timestamp>, D::Error> {
    806     Option::<i64>::deserialize(deserializer)?
    807         .map(Timestamp::from_microsecond)
    808         .transpose()
    809         .map_err(|e| serde::de::Error::custom(e.to_string()))
    810 }