libeufin

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

pain001.kt (5610B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2024-2025 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 package tech.libeufin.nexus.iso20022
     20 
     21 import tech.libeufin.common.*
     22 import tech.libeufin.nexus.*
     23 import tech.libeufin.ebics.XmlBuilder
     24 import java.time.Instant
     25 import java.time.ZoneId
     26 import java.time.ZonedDateTime
     27 import java.time.format.DateTimeFormatter
     28 
     29 /** String representation of a Taler [amount] compatible with EBICS */
     30 fun getAmountNoCurrency(amount: TalerAmount): String {
     31     if (amount.isSubCent()) {
     32         throw Exception("Sub-cent amounts not supported")
     33     }
     34     return amount.number().toString()
     35 }
     36 
     37 /** pain.001 transaction metadata */
     38 data class Pain001Tx(
     39     val creditor: IbanAccountMetadata,
     40     val amount: TalerAmount,
     41     val subject: String,
     42     val endToEndId: String
     43 )
     44 
     45 /** pain.001 message metadata */
     46 data class Pain001Msg(
     47     val messageId: String,
     48     val timestamp: Instant,
     49     val debtor: IbanAccountMetadata,
     50     val sum: TalerAmount,
     51     val txs: List<Pain001Tx>
     52 )
     53 
     54 /** Create a pain.001 XML document [msg] valid for [dialect] */
     55 fun createPain001(
     56     msg: Pain001Msg,
     57     dialect: Dialect,
     58     instant: Boolean
     59 ): ByteArray {
     60     val version = "09"
     61     val suffix = when (dialect.standard()) {
     62         Standard.SIX -> ".ch.03"
     63         Standard.GBIC -> ""
     64     }
     65     val zonedTimestamp = ZonedDateTime.ofInstant(msg.timestamp, ZoneId.of("UTC"))
     66     val totalAmount = getAmountNoCurrency(msg.sum)
     67     return XmlBuilder.toBytes("Document") {
     68         attr("xmlns", "urn:iso:std:iso:20022:tech:xsd:pain.001.001.$version")
     69         attr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
     70         attr("xsi:schemaLocation", "urn:iso:std:iso:20022:tech:xsd:pain.001.001.$version pain.001.001.$version$suffix.xsd")
     71         el("CstmrCdtTrfInitn") {
     72             el("GrpHdr") {
     73                 // Used for idempotency as banks will refuse to process EBICS request with the same MsgId for a pre-agreed period
     74                 // Used to uniquely identify batches of transactions in other files
     75                 el("MsgId", msg.messageId) 
     76                 el("CreDtTm", DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(zonedTimestamp))
     77                 el("NbOfTxs", msg.txs.size.toString())
     78                 el("CtrlSum", totalAmount)
     79                 el("InitgPty/Nm", msg.debtor.name)
     80                 /* TODO fail with GLS: ES_VERIFICATION IncorrectFileStructure - 'Signature verification' 'The file format is incomplete or invalid'
     81                 el("InitnSrc") {
     82                     el("Nm", "LibEuFin")
     83                     el("Prvdr", "Taler Systems SA")
     84                     el("Vrsn", VERSION)
     85                 }*/
     86 
     87                 // LocalInstrument
     88             }
     89             el("PmtInf") {
     90                 el("PmtInfId", "NOTPROVIDED")
     91                 el("PmtMtd", "TRF")
     92                 el("BtchBookg", "false")
     93                 el("NbOfTxs", msg.txs.size.toString())
     94                 el("CtrlSum", totalAmount)
     95                 if (dialect == Dialect.gls) {
     96                     el("PmtTpInf") {
     97                         el("SvcLvl/Cd", "SEPA")
     98                         if (instant) {
     99                             el("LclInstrm/Cd", "INST")
    100                         }
    101                     }
    102                 }
    103                 el("ReqdExctnDt/Dt", DateTimeFormatter.ISO_DATE.format(zonedTimestamp))
    104                 el("Dbtr/Nm", msg.debtor.name)
    105                 el("DbtrAcct/Id/IBAN", msg.debtor.iban.toString())
    106                 el("DbtrAgt/FinInstnId") {
    107                     if (msg.debtor.bic != null) {
    108                         el("BICFI", msg.debtor.bic)
    109                     } else {
    110                         el("Othr/Id", "NOTPROVIDED")
    111                     }
    112                 }
    113                 el("ChrgBr", "SLEV")
    114                 for (tx in msg.txs) {
    115                     el("CdtTrfTxInf") {
    116                         el("PmtId") {
    117                             el("InstrId", tx.endToEndId)
    118                             // Used to uniquely identify transactions in other files
    119                             el("EndToEndId", tx.endToEndId)
    120                         }
    121                         el("Amt/InstdAmt") {
    122                             attr("Ccy", tx.amount.currency)
    123                             text(getAmountNoCurrency(tx.amount))
    124                         }
    125                         if (tx.creditor.bic != null) el("CdtrAgt/FinInstnId/BICFI", tx.creditor.bic)
    126                         el("Cdtr") {
    127                             el("Nm", tx.creditor.name)
    128                             // Addr might become a requirement in the future
    129                             /*el("PstlAdr") {
    130                                 el("TwnNm", "Bochum")
    131                                 el("Ctry", "DE")
    132                             }*/
    133                         }
    134                         el("CdtrAcct/Id/IBAN", tx.creditor.iban.toString())
    135                         el("RmtInf/Ustrd", tx.subject)
    136                     }
    137                 }
    138             }
    139         }
    140     }
    141 }