libeufin

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

hac.rs (6430B)


      1 /*
      2 * This file is part of LibEuFin.
      3 * Copyright (C) 2026 Taler Systems S.A.
      4 
      5 * LibEuFin is free software; you can redistribute it and/or modify
      6 * it under the terms of the GNU Affero General Public License as
      7 * published by the Free Software Foundation; either version 3, or
      8 * (at your option) any later version.
      9 
     10 * LibEuFin is distributed in the hope that it will be useful, but
     11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13 * Public License for more details.
     14 
     15 * You should have received a copy of the GNU Affero General Public
     16 * License along with LibEuFin; see the file COPYING.  If not, see
     17 * <http://www.gnu.org/licenses/>
     18 */
     19 
     20 use std::fmt::Display;
     21 
     22 use compact_str::CompactString;
     23 use jiff::Timestamp;
     24 use taler_common::types::utils::date_time_to_utc_ts;
     25 
     26 use crate::{
     27     iso20022::{HacAction, status_code::StatusReason},
     28     xml::{self, Xml, XmlAccess},
     29 };
     30 
     31 #[derive(Debug, Clone, PartialEq, Eq)]
     32 pub struct CustomerAck {
     33     pub action: HacAction,
     34     pub order_id: Option<CompactString>,
     35     pub code: Option<StatusReason>,
     36     pub info: Box<str>,
     37     pub timestamp: Timestamp,
     38 }
     39 
     40 impl CustomerAck {
     41     fn msg_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
     42         let Self {
     43             action, code, info, ..
     44         } = self;
     45         write!(f, "{action}")?;
     46         if let Some(code) = code {
     47             write!(f, "{}", code.code())?;
     48         }
     49         write!(f, " - '{}'", action.description())?;
     50         if let Some(code) = code {
     51             write!(f, " '{}'", code.description())?;
     52         }
     53         if !info.is_empty() {
     54             write!(f, " - '{info}'")?;
     55         }
     56         Ok(())
     57     }
     58 }
     59 
     60 impl Display for CustomerAck {
     61     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
     62         let Self {
     63             order_id,
     64             timestamp,
     65             ..
     66         } = self;
     67         write!(f, "{timestamp}")?;
     68         if let Some(id) = order_id {
     69             write!(f, "{id}")?;
     70         }
     71         write!(f, " {}", std::fmt::from_fn(|f| self.msg_fmt(f)))
     72     }
     73 }
     74 
     75 /** Parse HAC pain.002 XML file */
     76 pub fn parse_hac(xml: &[u8]) -> xml::Result<Vec<CustomerAck>> {
     77     Xml::parse(xml, "Document", |root| {
     78         root.one("CstmrPmtStsRpt")?
     79             .many("OrgnlPmtInfAndSts")
     80             .map(|n| {
     81                 let mut timestamp = None;
     82                 let mut order_id = None;
     83                 let info = n.one("StsRsnInf")?;
     84                 for entry in info.one("Orgtr").one("Id").one("OrgId")?.many("Othr") {
     85                     let value = entry.one("Id");
     86                     let key = entry.one("SchmeNm").one("Prtry")?.text();
     87                     match key {
     88                         "TimeStamp" => {
     89                             timestamp = Some(date_time_to_utc_ts(
     90                                 &value.decode(|dt| dt.trim_end_matches('Z').parse())?,
     91                             ))
     92                         }
     93                         "OrderID" => order_id = Some(value.parse()?),
     94                         _ => {}
     95                     }
     96                 }
     97                 Ok(CustomerAck {
     98                     action: n.one("OrgnlPmtInfId").parse()?,
     99                     order_id,
    100                     code: info.opt("Rsn").one("Cd").parse()?,
    101                     info: info.many("AddtlInf").map(|n| n.text()).collect(),
    102                     timestamp: timestamp.unwrap(),
    103                 })
    104             })
    105             .collect()
    106     })
    107 }
    108 
    109 #[cfg(test)]
    110 mod test {
    111     use taler_common::types::utils::date_time_to_utc_ts;
    112 
    113     use crate::iso20022::{
    114         HacAction,
    115         hac::{CustomerAck, parse_hac},
    116         status_code::StatusReason,
    117     };
    118 
    119     #[test]
    120     fn hac() {
    121         pub fn ack(
    122             action: HacAction,
    123             order_id: Option<&str>,
    124             code: Option<StatusReason>,
    125             info: &str,
    126             timestamp: &str,
    127         ) -> CustomerAck {
    128             CustomerAck {
    129                 action,
    130                 order_id: order_id.map(Into::into),
    131                 code,
    132                 info: info.into(),
    133                 timestamp: date_time_to_utc_ts(&timestamp.trim_end_matches('Z').parse().unwrap()),
    134             }
    135         }
    136         pretty_assertions::assert_eq!(
    137             parse_hac(&std::fs::read("sample/platform/hac.xml").unwrap()).unwrap(),
    138             [
    139                 ack(
    140                     HacAction::FILE_DOWNLOAD,
    141                     None,
    142                     Some(StatusReason::TransmissionSuccessful),
    143                     "",
    144                     "2024-09-02T15:47:30.350Z"
    145                 ),
    146                 ack(
    147                     HacAction::FILE_UPLOAD,
    148                     Some("ORDER_SUCCESS"),
    149                     Some(StatusReason::TransmissionSuccessful),
    150                     "",
    151                     "2024-09-02T20:48:43.153Z"
    152                 ),
    153                 ack(
    154                     HacAction::ES_VERIFICATION,
    155                     Some("ORDER_SUCCESS"),
    156                     Some(StatusReason::ElectronicSignaturesCorrect),
    157                     "",
    158                     "2024-09-02T20:48:43.153Z"
    159                 ),
    160                 ack(
    161                     HacAction::ORDER_HAC_FINAL_POS,
    162                     Some("ORDER_SUCCESS"),
    163                     None,
    164                     "Some multiline info",
    165                     "2024-09-02T20:48:43.153Z"
    166                 ),
    167                 ack(
    168                     HacAction::FILE_DOWNLOAD,
    169                     None,
    170                     Some(StatusReason::NoDataAvailable),
    171                     "",
    172                     "2024-09-02T15:47:31.754Z"
    173                 ),
    174                 ack(
    175                     HacAction::FILE_UPLOAD,
    176                     Some("ORDER_FAILURE"),
    177                     Some(StatusReason::TransmissionSuccessful),
    178                     "",
    179                     "2024-08-23T15:34:11.987Z"
    180                 ),
    181                 ack(
    182                     HacAction::ES_VERIFICATION,
    183                     Some("ORDER_FAILURE"),
    184                     Some(StatusReason::IncorrectFileStructure),
    185                     "",
    186                     "2024-08-23T15:34:13.307Z"
    187                 ),
    188                 ack(
    189                     HacAction::ORDER_HAC_FINAL_NEG,
    190                     Some("ORDER_FAILURE"),
    191                     None,
    192                     "",
    193                     "2024-08-23T15:34:13.307Z"
    194                 ),
    195             ]
    196         )
    197     }
    198 }