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.