libeufin

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

administrative.rs (7454B)


      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 taler_common::types::{
     24     amount::Currency,
     25     iban::{BIC, IBAN},
     26 };
     27 use taler_macros::EnumMeta;
     28 
     29 use crate::{
     30     config::EbicsHostCfg,
     31     ebics::{
     32         EbicsResponse,
     33         ebics_code::EbicsReturnCode,
     34         order::{BTF, Order},
     35     },
     36     xml,
     37     xml::{Xml, XmlAccess as _},
     38 };
     39 
     40 #[derive(Debug, Clone, PartialEq, Eq)]
     41 pub struct VersionNumber {
     42     pub number: CompactString,
     43     pub schema: CompactString,
     44 }
     45 
     46 impl Display for VersionNumber {
     47     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
     48         let Self { number, schema } = self;
     49         write!(f, "{number}:{schema}")
     50     }
     51 }
     52 
     53 pub struct HKD {
     54     pub partner: PartnerInfo,
     55     pub users: Box<[UserInfo]>,
     56 }
     57 pub struct PartnerInfo {
     58     pub name: Option<CompactString>,
     59     pub accounts: Box<[AccountInfo]>,
     60     pub orders: Vec<OrderInfo>,
     61 }
     62 pub struct OrderInfo {
     63     pub order: Order,
     64     pub description: String,
     65 }
     66 pub struct AccountInfo {
     67     pub currency: Currency,
     68     pub iban: IBAN,
     69     pub bic: BIC,
     70 }
     71 pub struct UserInfo {
     72     pub id: CompactString,
     73     pub status: UserStatus,
     74     pub permissions: Box<[Order]>,
     75 }
     76 
     77 pub struct HAA {
     78     pub orders: Vec<Order>,
     79 }
     80 
     81 #[derive(Debug, Clone, PartialEq, Eq, EnumMeta)]
     82 #[enum_meta(Description)]
     83 pub enum UserStatus {
     84     /// "Subscriber is permitted access"
     85     Ready,
     86     /// "Subscriber is established, pending access permission"
     87     New,
     88     /// "Subscriber has sent INI file, but no HIA file yet"
     89     INI,
     90     /// "Subscriber has sent HIA order, but no INI file yet"
     91     HIA,
     92     /// "Subscriber has sent both HIA order and INI file"
     93     Initialised,
     94     /// "Suspended after several failed attempts, new initialisation via INI and HIA possible"
     95     SuspendedFailedAttempts,
     96     /// "Suspended after SPR order, new initialisation via INI and HIA possible"
     97     SuspendedSPR,
     98     /// "Suspended by bank, new initialisation via INI and HIA is not possible, suspension can only be revoked by the bank"
     99     SuspendedBank,
    100 }
    101 
    102 pub fn hev_msg(cfg: &EbicsHostCfg) -> String {
    103     xml!(
    104         "ebicsHEVRequest" "xmlns"="http://www.ebics.org/H000" {
    105             "HostID": &cfg.host_id
    106         }
    107     )
    108 }
    109 
    110 pub fn parse_hev(xml: &[u8]) -> xml::Result<EbicsResponse<Box<[VersionNumber]>>> {
    111     Xml::parse(xml, "ebicsHEVResponse", |root| {
    112         let s = root.one("SystemReturnCode")?;
    113         Ok(EbicsResponse {
    114             technical_code: s.one("ReturnCode").parse()?,
    115             technical_text: s.one("ReportText").parse()?,
    116             bank_code: EbicsReturnCode::EBICS_OK,
    117             content: Some(
    118                 root.many("VersionNumber")
    119                     .map(|n| {
    120                         Ok(VersionNumber {
    121                             number: n.parse()?,
    122                             schema: n.attr("ProtocolVersion")?.into(),
    123                         })
    124                     })
    125                     .collect::<xml::Result<_>>()?,
    126             ),
    127         })
    128     })
    129 }
    130 
    131 fn service(n: Xml) -> xml::Result<BTF> {
    132     let msg = n.one("MsgName")?;
    133     Ok(BTF {
    134         service: n.one("ServiceName").parse()?,
    135         scope: n.opt("Scope").parse()?,
    136         option: n.opt("ServiceOption").parse()?,
    137         container: n.opt("Container").parse_attr("containerType")?,
    138         msg: msg.parse()?,
    139         version: msg.parse_opt_attr("version")?,
    140     })
    141 }
    142 
    143 pub fn parse_hkd(xml: &[u8]) -> xml::Result<HKD> {
    144     fn order(n: Xml) -> xml::Result<Order> {
    145         let ty = n.one("AdminOrderType")?.text();
    146         Order::from_parts(ty, n.opt("Service")?.map(service).transpose()?)
    147             .ok_or_else(|| n.parse_err(format_args!("Unknown order type {ty}")))
    148     }
    149     Xml::parse(xml, "HKDResponseOrderData", |root| {
    150         let partner = root.one("PartnerInfo")?;
    151 
    152         Ok(HKD {
    153             partner: PartnerInfo {
    154                 name: partner.one("AddressInfo").opt("Name").parse()?,
    155                 accounts: partner
    156                     .many("AccountInfo")
    157                     .map(|account| {
    158                         let currency = account.parse_attr("Currency")?;
    159                         let iban = account
    160                             .many("AccountNumber")
    161                             .find(|nb| nb.opt_attr("international") == Some("true"))
    162                             .unwrap()
    163                             .parse()?;
    164                         let bic = account
    165                             .many("BankCode")
    166                             .find(|nb| nb.opt_attr("international") == Some("true"))
    167                             .unwrap()
    168                             .parse()?;
    169                         Ok(AccountInfo {
    170                             currency,
    171                             iban,
    172                             bic,
    173                         })
    174                     })
    175                     .collect::<xml::Result<_>>()?,
    176                 orders: partner
    177                     .many("OrderInfo")
    178                     .map(|n| {
    179                         Ok(OrderInfo {
    180                             order: order(n)?,
    181                             description: n.one("Description").parse()?,
    182                         })
    183                     })
    184                     .collect::<xml::Result<_>>()?,
    185             },
    186             users: root
    187                 .many("UserInfo")
    188                 .map(|n| {
    189                     let id = n.one("UserID")?;
    190                     Ok(UserInfo {
    191                         id: id.parse()?,
    192                         status: match id.attr("Status")? {
    193                             "1" => UserStatus::Ready,
    194                             "2" => UserStatus::New,
    195                             "3" => UserStatus::INI,
    196                             "4" => UserStatus::HIA,
    197                             "5" => UserStatus::Initialised,
    198                             "6" => UserStatus::SuspendedFailedAttempts,
    199                             // 7 is not applicable per spec
    200                             "8" => UserStatus::SuspendedSPR,
    201                             "9" => UserStatus::SuspendedBank,
    202                             s => return Err(id.parse_err(format_args!("Unknown user status {s}"))),
    203                         },
    204                         permissions: n
    205                             .many("Permission")
    206                             .map(|p| order(p))
    207                             .collect::<xml::Result<_>>()?,
    208                     })
    209                 })
    210                 .collect::<xml::Result<_>>()?,
    211         })
    212     })
    213 }
    214 
    215 pub fn parse_haa(xml: &[u8]) -> xml::Result<HAA> {
    216     Xml::parse(xml, "HAAResponseOrderData", |root| {
    217         Ok(HAA {
    218             orders: root
    219                 .many("Service")
    220                 .map(|n| Ok(Order::BTD(service(n)?)))
    221                 .collect::<xml::Result<_>>()?,
    222         })
    223     })
    224 }