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(×tamp.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 }