4
\$\begingroup\$

I am working on SEPA XML generator in F#.

I developed two solutions that are roughly equivalent, one relies on the XmlSerializer while the other relies on XmlProvider, I am wondering what is the most idiomatic one?

Using the XmlSerializer:

module SepaConversion.SepaXmlA

open System
open System.Collections.ObjectModel
open System.IO
open System.Xml.Serialization
open SepaConversion.Models
open SepaConversion.Common


[<AutoOpen>]
module Serialization =

    [<CLIMutable>]
    [<XmlRoot(ElementName="InitgPty")>]
    type InitiatingParty = {
        [<XmlElement(ElementName="Nm")>]
        Name: string
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="GrpHdr")>]
    type GroupHeader = {
        [<XmlElement(ElementName="MsgId")>]
        MessageId: string

        [<XmlElement(ElementName="CreDtTm")>]
        CreationDateTime: string

        [<XmlElement(ElementName="NbOfTxs")>]
        TransactionCount: string

        [<XmlElement(ElementName="CtrlSum")>]
        ControlSum: string

        [<XmlElement(ElementName="InitgPty")>]
        InitiatingParty: InitiatingParty
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="SvcLvl")>]
    type ServiceLevel = {
        [<XmlElement(ElementName="Cd")>]
        Code: string
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="PmtTpInf")>]
    type PaymentTypeInformation = {

        [<XmlElement(ElementName="InstrPrty")>]
        InstructionPriority: string

        [<XmlElement(ElementName="SvcLvl")>]
        ServiceLevel: ServiceLevel
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="Dbtr")>]
    type Debtor = {
        [<XmlElement(ElementName="Nm")>]
        Name: string
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="Id")>]
    type AccountId = {
        [<XmlElement(ElementName="IBAN")>]
        Iban: string
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="DbtrAcct")>]
    type DebtorAccount = {
        [<XmlElement(ElementName="Id")>]
        AccountId: AccountId

        [<XmlElement(ElementName="Ccy")>]
        Currency: string
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="FinInstnId")>]
    type FinancialInstitutionId = {
        [<XmlElement(ElementName="BIC")>]
        Bic: string
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="DbtrAgt")>]
    type DebtorAgent = {
        [<XmlElement(ElementName="FinInstnId")>]
        FinancialInstitutionId: FinancialInstitutionId
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="PmtId")>]
    type PaymentIdentification = {
        [<XmlElement(ElementName="EndToEndId")>]
        EndToEndId: string
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="InstdAmt")>]
    type InstructedAmount = {
        [<XmlAttribute(AttributeName="Ccy")>]
        Currency: string
        [<XmlText>]
        Amount: string
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="Amt")>]
    type Amount = {
        [<XmlElement(ElementName="InstdAmt")>]
        InstructedAmount: InstructedAmount
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="CdtrAgt")>]
    type CreditorAgent = {
        [<XmlElement(ElementName="FinInstnId")>]
        FinancialInstitutionId: FinancialInstitutionId
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="Cdtr")>]
    type Creditor = {
        [<XmlElement(ElementName="Nm")>]
        Name: string
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="CdtrAcct")>]
    type CreditorAccount = {
        [<XmlElement(ElementName="Id")>]
        AccountId: AccountId
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="RmtInf")>]
    type RemittanceInformation = {
        [<XmlElement(ElementName="Ustrd")>]
        Unstructured: string
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="CdtTrfTxInf")>]
    type CreditTransferTransactionInformation = {
        [<XmlElement(ElementName="PmtId")>]
        PaymentIdentification: PaymentIdentification

        [<XmlElement(ElementName="Amt")>]
        Amount: Amount

        [<XmlElement(ElementName="CdtrAgt")>]
        CreditorAgent: CreditorAgent

        [<XmlElement(ElementName="Cdtr")>]
        Creditor: Creditor

        [<XmlElement(ElementName="CdtrAcct")>]
        CreditorAccount: CreditorAccount

        [<XmlElement(ElementName="RmtInf")>]
        RemittanceInformation: RemittanceInformation
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="PmtInf")>]
    type PaymentInformation = {
        [<XmlElement(ElementName="PmtInfId")>]
        PaymentInformationId: string

        [<XmlElement(ElementName="PmtMtd")>]
        PaymentMethod: string 

        [<XmlElement(ElementName="NbOfTxs")>]
        TransactionCount: string

        [<XmlElement(ElementName="CtrlSum")>]
        ControlSum: string

        [<XmlElement(ElementName="PmtTpInf")>]
        PaymentTypeInformation: PaymentTypeInformation 

        [<XmlElement(ElementName="ReqdExctnDt")>]
        RequestedExecutionDate: string

        [<XmlElement(ElementName="Dbtr")>]
        Debtor: Debtor 

        [<XmlElement(ElementName="DbtrAcct")>]
        DebtorAccount: DebtorAccount

        [<XmlElement(ElementName="DbtrAgt")>]
        DebtorAgent: DebtorAgent 

        [<XmlElement(ElementName="ChrgBr")>]
        ChargeBearer: string 

        [<XmlElement(ElementName="CdtTrfTxInf")>]
        CreditTransferTransactionInformation: CreditTransferTransactionInformation[]  
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="CstmrCdtTrfInitn")>]
    type CustomerCreditTransferInitialization = {
        [<XmlElement(ElementName="GrpHdr")>]
        GroupHeader: GroupHeader 

        [<XmlElement(ElementName="PmtInf")>]
        PaymentInformation: PaymentInformation 
    }

    [<CLIMutable>]
    [<XmlRoot(ElementName="Document", Namespace="urn:iso:std:iso:20022:tech:xsd:pain.001.001.03")>]
    type Document = {
        [<XmlElement(ElementName="CstmrCdtTrfInitn")>]
        CustomerCreditTransferInitialization: CustomerCreditTransferInitialization 
    }

[<AutoOpen>]
module private Impl = 

    [<Literal>]
    let TransferAdvicePaymentMethod = "TRF"

    [<Literal>]
    let SepaServiceCode = "SEPA"

    [<Literal>]
    let HighInstructionPriority = "HIGH"

    [<Literal>]
    let DebtorChargeBearer = "SLEV"

    let createAmount (amount: decimal) currency = {
        InstructedAmount = {
            Currency = currency
            Amount = amount |> string
        }
    }

    let createPaymentIdentification id = {
        EndToEndId = id
    }

    let createCreditorAgent bic = {
        FinancialInstitutionId = {
            Bic = bic
        }
    }

    let createCreditor name = {
        Name = name
    }

    let createCreditorAccount iban = {
        AccountId = {
            Iban = iban
        }
    }

    let createRemittanceInformation reason = {
        Unstructured = reason
    }

    let createInitiatingParty name : InitiatingParty = {
        Name = name
    }

    let createDebtorAccount iban currency = {
        AccountId = {
            Iban = iban
        }
        Currency = currency
    }

    let createDebtorAgent bic : DebtorAgent = {
        FinancialInstitutionId = {
            Bic = bic
        }
    }

    let createGroupHeader (creationDateTime: DateTimeOffset) information = {
        MessageId = newHexGuid()
        CreationDateTime = creationDateTime.ToString("o")
        TransactionCount = information.CreditTransactions.Count |> string
        ControlSum = information.CreditTransactions
                     |> Seq.map (fun x -> x.Amount)
                     |> Seq.sum
                     |> string
        InitiatingParty = createInitiatingParty information.Debtor.Name
    }

    let createCreditTransferTransactionInformation (creditTransactions : ReadOnlyCollection<SepaCreditTransactionInformation>) currency =
        creditTransactions
        |> Seq.map(fun x ->
        {
            PaymentIdentification = createPaymentIdentification (newHexGuid())
            Amount = createAmount x.Amount currency
            CreditorAgent = createCreditorAgent x.Bic
            Creditor = createCreditor x.Name
            CreditorAccount = createCreditorAccount x.Iban
            RemittanceInformation = createRemittanceInformation x.Reason
        })
        |> Seq.toArray

    let createPaymentInformation messageId (requestedExecutionDate: DateTime) information creditTransferTransactionInformation = {
            PaymentInformationId = messageId
            PaymentMethod = TransferAdvicePaymentMethod
            TransactionCount = information.CreditTransactions.Count |> string
            ControlSum = information.CreditTransactions
                         |> Seq.map (fun x -> x.Amount)
                         |> Seq.sum
                         |> string
            PaymentTypeInformation = {
                InstructionPriority = HighInstructionPriority
                ServiceLevel = {
                    Code = SepaServiceCode
                }
            }
            RequestedExecutionDate = requestedExecutionDate.ToString("o")
            Debtor = {
                Name = information.Debtor.Name
            }
            DebtorAccount = createDebtorAccount information.Debtor.Iban information.Currency 
            DebtorAgent = createDebtorAgent information.Debtor.Bic
            ChargeBearer = DebtorChargeBearer
            CreditTransferTransactionInformation = creditTransferTransactionInformation
        }


let generateSepaXml (information: SepaInformation) =

    let creationTime = DateTimeOffset.UtcNow
    let groupHeader = createGroupHeader creationTime information
    let requestedExecutionDate = creationTime.UtcDateTime.AddDays(1.0)

    let creditTransferTransactionInformation = createCreditTransferTransactionInformation
                                                   information.CreditTransactions
                                                   information.Currency

    let paymentInformation = createPaymentInformation
                                 groupHeader.MessageId
                                 requestedExecutionDate
                                 information
                                 creditTransferTransactionInformation

    let document = {
        CustomerCreditTransferInitialization = {
            GroupHeader = groupHeader
            PaymentInformation = paymentInformation
        }
    }

    let xmlSerializer = XmlSerializer(typeof<Document>)
    use stringWriter = new StringWriter()
    xmlSerializer.Serialize(stringWriter, document)
    stringWriter |> string

Using the XmlProvider with a XSD file: pain.001.001.03.xsd:

module SepaConversion.SepaXmlB

open System
open FSharp.Data
open SepaConversion.Models
open SepaConversion.Common


[<AutoOpen>]
module private Impl = 

    type Sepa = XmlProvider<Schema="pain.001.001.03.xsd", EmbeddedResource="SepaConversion, pain.001.001.03.xsd">

    [<Literal>]
    let TransferAdvicePaymentMethod = "TRF"

    [<Literal>]
    let SepaServiceCode = "SEPA"

    [<Literal>]
    let HighInstructionPriority = "HIGH"

    [<Literal>]
    let SepaChargeBearer = "SLEV"

    let sepaServiceLevel =
        Sepa.SvcLvl(cd = Some SepaServiceCode,
                    prtry = None)

    let createInitiatingParty name =
        Sepa.InitgPty(nm = Some name,
                      pstlAdr = None,
                      id = None,
                      ctryOfRes = None,
                      ctctDtls = None)

    let emptyAuthorizations = [||]

    let paymentTypeInformation =
        Sepa.PmtTpInf(instrPrty = Some HighInstructionPriority,
                      svcLvl = Some sepaServiceLevel,
                      lclInstrm = None,
                      ctgyPurp = None)

    let createFinancialInstitutionId bic =
        Sepa.FinInstnId(bic = Some bic,
                        clrSysMmbId = None,
                        nm = None,
                        pstlAdr = None,
                        othr = None)

    let createIbanId iban =
         Sepa.Id2(iban = Some iban, othr = None)

    let createDebtorAgent bic =
        Sepa.DbtrAgt(finInstnId = createFinancialInstitutionId bic,
                     brnchId = None)

    let createDebtorAccount iban currency  =
        Sepa.DbtrAcct(id = createIbanId iban,
                      tp = None,
                      ccy = Some currency,
                      nm = None)

    let createDebtor name =
        Sepa.Dbtr(nm = Some name,
                  pstlAdr = None,
                  id = None,
                  ctryOfRes = None,
                  ctctDtls = None)

    let createGroupHeader messageId creationTime (transactionCount : int32) controlSum initiatingParty =
        Sepa.GrpHdr(msgId = messageId, 
                    creDtTm = creationTime,
                    authstns = emptyAuthorizations,
                    nbOfTxs = (transactionCount |> string),
                    ctrlSum = Some controlSum,
                    initgPty = initiatingParty,
                    fwdgAgt = None)

    let createPaymentId id =
        Sepa.PmtId(instrId = None,
                   endToEndId = id)

    let createInstructedAmount amount currency =
        Sepa.InstdAmt(ccy = currency,
                      value = amount)

    let createAmount amount currency =
        Sepa.Amt(instdAmt = Some (createInstructedAmount amount currency),
                 eqvtAmt = None)

    let createCreditorAgent bic =
        Sepa.CdtrAgt(finInstnId = createFinancialInstitutionId bic,
                     brnchId = None)

    let createCreditorAccount iban  =
        Sepa.CdtrAcct(id = createIbanId iban,
                      tp = None,
                      ccy = None,
                      nm = None)

    let createCreditor name =
        Sepa.Cdtr(nm = Some name,
                  pstlAdr = None,
                  id = None,
                  ctryOfRes = None,
                  ctctDtls = None)

    let createRemittanceInformation reason =
        Sepa.RmtInf(ustrds = [| reason |], strds = null)

    let createCreditTransferTransactionInformation creditTransaction currency =
        Sepa.CdtTrfTxInf(pmtId = createPaymentId (newHexGuid()),
                         pmtTpInf = None,
                         amt = createAmount creditTransaction.Amount currency,
                         xchgRateInf = None,
                         chrgBr = None,
                         chqInstr = None,
                         ultmtDbtr = None,
                         intrmyAgt1 = None,
                         intrmyAgt1Acct = None,
                         intrmyAgt2 = None,
                         intrmyAgt2Acct = None,
                         intrmyAgt3 = None,
                         intrmyAgt3Acct = None,
                         cdtrAgt = Some (createCreditorAgent creditTransaction.Bic),
                         cdtrAgtAcct = None,
                         cdtr = Some (createCreditor creditTransaction.Name),
                         cdtrAcct = Some (createCreditorAccount creditTransaction.Iban),
                         ultmtCdtr = None,
                         instrForCdtrAgts = [||],
                         instrForDbtrAgt = None,
                         purp = None,
                         rgltryRptgs = [||],
                         tax = None,
                         rltdRmtInfs = [||],
                         rmtInf = Some (createRemittanceInformation creditTransaction.Reason))

    let createCreditTransferTransactionInformationArray creditTransactions currency =
        creditTransactions
        |> Seq.map(fun x -> createCreditTransferTransactionInformation x currency)
        |> Seq.toArray

    let createPaymentInformation messageId (transactionCount: int32) controlSum requestedExecutionDate information =
         Sepa.PmtInf(pmtInfId = messageId,
                     pmtMtd = TransferAdvicePaymentMethod,
                     btchBookg = None,
                     nbOfTxs = Some (transactionCount |> string),
                     ctrlSum = Some controlSum,
                     pmtTpInf = Some paymentTypeInformation,
                     reqdExctnDt = requestedExecutionDate,
                     poolgAdjstmntDt = None,
                     dbtr = createDebtor information.Debtor.Name,
                     dbtrAcct = createDebtorAccount information.Debtor.Iban information.Currency,
                     dbtrAgt = createDebtorAgent information.Debtor.Bic,
                     dbtrAgtAcct = None,
                     ultmtDbtr = None,
                     chrgBr = Some SepaChargeBearer,
                     chrgsAcct = None,
                     chrgsAcctAgt = None,
                     cdtTrfTxInfs = createCreditTransferTransactionInformationArray
                                        information.CreditTransactions
                                        information.Currency)

    let createCustomerCreditTransferInitialization groupHeader paymentInformation =
        new Sepa.CstmrCdtTrfInitn(grpHdr = groupHeader,
                                  pmtInfs = [| paymentInformation |])

    let createDocument (customerCreditTransferInitialization: Sepa.CstmrCdtTrfInitn) =
        Sepa.Document(customerCreditTransferInitialization)

let generateSepaXml information =
    let messageId = newHexGuid()
    let creationTime = DateTimeOffset.UtcNow
    let requestedExecutionDate = creationTime.UtcDateTime.AddDays(1.0)
    let transactionCount = information.CreditTransactions.Count
    let controlSum = information.CreditTransactions
                     |> Seq.map (fun x -> x.Amount)
                     |> Seq.sum

    let initiatingParty = createInitiatingParty information.Debtor.Name

    let groupHeader = createGroupHeader
                          messageId
                          creationTime
                          transactionCount
                          controlSum
                          initiatingParty

    let paymentInformation = createPaymentInformation
                                 messageId
                                 transactionCount
                                 controlSum
                                 requestedExecutionDate
                                 information

    let customerCreditTransferInitialization = createCustomerCreditTransferInitialization
                                                   groupHeader
                                                   paymentInformation
    let document = createDocument customerCreditTransferInitialization

    document.XElement |> string

The XmlProvider way seems more FP-ish and does not require to maintain a set of record types to generation the XML. However, the ctors generated by the provider are quite lengthy, I was surprised that I had to fill up everything and I was expecting that additional ctors would be generated of the fly, for example setting None option arguments so that not everything needs to be filled which would have made the experience a bit more convenient. Also it's a bit more cryptic in a sense that the maintainer has to know the meaning of each xml tags.

Surely I can add more wrapper functions to add some meanings, or literals /constants like:

[<Literal>]
let private PaymentMethod = "TRF"

[<Literal>]
let private SepaCode = "SEPA"

[<Literal>]
let private InstructionPriority = "NORM"

Not sure if it's just a matter of taste, or if there is a real and clear benefit using one method over the other.

[EDIT]

Another issue with the XmlProvider, the need to copy to the output the schema file when building. Embedded resource only works xml samples =/

Another cons for XmlSerializer, it cannot serializer internal classes without a lof trickeries.

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.