Compare commits

...

5 Commits

Author SHA1 Message Date
Gerdriaan Mulder 4bbde4c542 WIP of DrctDbtTxInf 6 years ago
Gerdriaan Mulder 2e662f09af Restructure PAIN package into group_header, payment_information (WIP) 6 years ago
Gerdriaan Mulder ca976d67c0 Move PAIN.008.02 files into separate packages 6 years ago
Gerdriaan Mulder ccc2d1efa8 Sort SEPARegexps 6 years ago
Gerdriaan Mulder da5db64883 Add SchmeId validations 6 years ago
  1. 226
      pain.go
  2. 38
      pain/group_header.go
  3. 157
      pain/pain.go
  4. 44
      pain/pain_regexp.go
  5. 130
      pain/payment_information.go
  6. 36
      pain/payment_information_transactions.go
  7. 21
      pain_regexp.go

226
pain.go

@ -1,226 +0,0 @@
package sepa
import (
"fmt"
"regexp"
"strings"
)
var (
SEPARegexps = map[string]*regexp.Regexp{
"MsgId": RestrictedIdentificationSEPA1,
"CreDtTm": ISODateTime,
"ReqdColltnDt": ISODateTime,
"NbOfTxs": Max15NumericText,
"CtrlSum": RestrictedDecimalNumber,
"Nm": Max70Text,
"PmtInfId": RestrictedIdentificationSEPA1,
"PmtMtd": PaymentMethod2Code,
"PmtMetaSvcLvl": ServiceLevelSEPACode,
"PmtMetaLclInstrm": LocalInstrumentSEPACode,
"SeqTp": SequenceType1Code,
"IBAN": IBAN2007Identifier,
}
)
type PainXML struct {
GroupHeader GrpHdr `xml:"GrpHdr"`
PaymentInformation []PmtInf `xml:"PmtInf"`
TransactionInformation []TxInf `xml:"DrctDbtTxInf"`
}
func (p *PainXML) Valid() error {
var err []string
if e := p.GroupHeader.Valid(); e != nil {
err = append(err, fmt.Sprintf("%v", e))
}
if len(p.PaymentInformation) < 1 {
err = append(err, "no payment information")
}
if len(p.TransactionInformation) < 1 {
err = append(err, "no transaction information")
}
if len(err) > 0 {
return fmt.Errorf("pain XML not valid: %v", strings.Join(err, ", "))
}
return nil
}
type GrpHdr struct {
MessageId string `xml:"MsgId"`
Timestamp string `xml:"CreDtTm"`
NumTx string `xml:"NbOfTxs"`
Sum string `xml:"CtrlSum"`
InitiatingParty InitgPty `xml:"InitgPty"`
}
func (g *GrpHdr) Valid() error {
var err []string
if !SEPARegexps["MsgId"].MatchString(g.MessageId) {
err = append(err, "msgId did not match expected format")
}
if !SEPARegexps["CreDtTm"].MatchString(g.Timestamp) {
err = append(err, "timestamp did not match expected format")
}
if !SEPARegexps["NbOfTxs"].MatchString(g.NumTx) {
err = append(err, "numtx did not match expected format")
}
if !SEPARegexps["CtrlSum"].MatchString(g.Sum) {
err = append(err, "sum did not match expected format")
}
// Note: we assume g.InitiatingParty is initialized.
if !SEPARegexps["Nm"].MatchString(g.InitiatingParty.Name) {
err = append(err, "initiating party's name did not match expected format")
}
if len(err) > 0 {
return fmt.Errorf("group header not valid: %v", strings.Join(err, ", "))
}
return nil
}
type PmtInf struct {
Id string `xml:"PmtInfId"`
Method string `xml:"PmtMtd"`
PaymentMeta PmtTpInf `xml:"PmtTpInf"`
CollectionDate string `xml:"ReqdColltnDt"`
Creditor Cdtr `xml:"Cdtr"`
CreditorAccount CdtrAcct `xml:"CdtrAcct"`
}
func (pmt *PmtInf) Valid() error {
var err []string
if !SEPARegexps["PmtInfId"].MatchString(pmt.Id) {
err = append(err, "payment info id does not match expected format")
}
if !SEPARegexps["PmtMtd"].MatchString(pmt.Method) {
err = append(err, "payment method does not match expected format")
}
if e := pmt.PaymentMeta.Valid(); e != nil {
err = append(err, fmt.Sprintf("payment meta not valid: %v", e))
}
if !SEPARegexps["ReqdColltnDt"].MatchString(pmt.CollectionDate) {
err = append(err, "payment collection date does not match expected format")
}
if e := pmt.Creditor.Valid(); e != nil {
err = append(err, fmt.Sprintf("payment creditor not valid: %v", e))
}
if e := pmt.CreditorAccount.Valid(); e != nil {
err = append(err, fmt.Sprintf("payment creditor account not valid: %v", e))
}
if len(err) > 0 {
return fmt.Errorf("payment info (Id: %q) not valid: %v", pmt.Id, strings.Join(err, ", "))
}
return nil
}
type PmtTpInf struct {
ServiceLevel Code `xml:"SvcLvl"`
LocalInstrument Code `xml:"LclIntrm"`
SequenceType string `xml:"SeqTp"`
}
type Cdtr struct {
Name string `xml:"Nm"`
PostalAddress PstlAddr `xml:"PstlAddr"`
}
func (c *Cdtr) Valid() error {
var err []string
if !SEPARegexps["Nm"].MatchString(c.Name) {
err = append(err, "creditor name does not match format")
}
if e := c.PostalAddress.Valid(); e != nil {
err = append(err, fmt.Sprintf("creditor postal address not valid: %v", e))
}
if len(err) > 0 {
return fmt.Errorf("creditor (Nm: %q) not valid: %v", c.Name, strings.Join(err, ", "))
}
return nil
}
type PstlAddr struct {
Country string `xml:"Ctry"`
AddressLines []AdrLine `xml:"AdrLine"`
}
func (p *PstlAddr) Valid() error {
var err []string
if !SEPARegexps["Ctry"].MatchString(p.Country) {
err = append(err, "country does not match format")
}
if len(p.AddressLines) > 2 {
err = append(err, "expected at most 2 address lines")
}
for i, line := range p.AddressLines {
if !SEPARegexps["AdrLine"].MatchString(string(line)) {
err = append(err, fmt.Sprintf("address line %d (%q) does not match format", i, line))
}
}
if len(err) > 0 {
return fmt.Errorf("address not valid: %v", strings.Join(err, ", "))
}
return nil
}
type AdrLine string
type CdtrAcct struct {
Id IBAN `xml:"Id"`
}
type IBAN struct {
IBAN string `xml:"IBAN"`
}
func (c *CdtrAcct) Valid() error {
if e := c.Id.Valid(); e != nil {
return fmt.Errorf("creditor account id not valid: %v", e)
}
return nil
}
func (i *IBAN) Valid() error {
if !SEPARegexps["IBAN"].MatchString(i.IBAN) {
return fmt.Errorf("IBAN does not match format")
}
return nil
}
type Code struct {
Code string `xml:"Cd"`
}
func (meta *PmtTpInf) Valid() error {
var err []string
// TODO: check meta.ServiceLevel for nil
if !SEPARegexps["PmtMetaSvcLvl"].MatchString(meta.ServiceLevel.Code) {
err = append(err, "incorrect service level present (must be 'SEPA')")
}
// TODO: check meta.LocalInstrument for nil
if !SEPARegexps["PmtMetaLclInstrm"].MatchString(meta.LocalInstrument.Code) {
err = append(err, "incorrect local instrument present")
}
if !SEPARegexps["SeqTp"].MatchString(meta.SequenceType) {
err = append(err, "sequence type has incorrect format")
}
if len(err) > 0 {
return fmt.Errorf("payment meta not valid: %v", strings.Join(err, ", "))
}
return nil
}
type TxInf struct {
}
type InitgPty struct {
Name string `xml:"Nm"`
// We do not implement the "Id" element from "PartyIdentificationSEPA1"
}

38
pain/group_header.go

@ -0,0 +1,38 @@
package pain
import (
"fmt"
)
type GrpHdr struct {
MessageId string `xml:"MsgId"`
Timestamp string `xml:"CreDtTm"`
NumTx string `xml:"NbOfTxs"`
Sum string `xml:"CtrlSum"`
InitiatingParty PartyIdSEPA1 `xml:"InitgPty"`
}
func (g *GrpHdr) Valid() error {
var err []string
if !SEPARegexps["MsgId"].MatchString(g.MessageId) {
err = append(err, "msgId did not match expected format")
}
if !SEPARegexps["CreDtTm"].MatchString(g.Timestamp) {
err = append(err, "timestamp did not match expected format")
}
if !SEPARegexps["NbOfTxs"].MatchString(g.NumTx) {
err = append(err, "numtx did not match expected format")
}
if !SEPARegexps["CtrlSum"].MatchString(g.Sum) {
err = append(err, "sum did not match expected format")
}
// Note: we assume g.InitiatingParty is initialized.
if !SEPARegexps["Nm"].MatchString(g.InitiatingParty.Name) {
err = append(err, "initiating party's name did not match expected format")
}
if len(err) > 0 {
return fmt.Errorf("group header not valid: %v", strings.Join(err, ", "))
}
return nil
}

157
pain/pain.go

@ -0,0 +1,157 @@
package pain
import (
"fmt"
"regexp"
"strings"
)
type PainXML struct {
GroupHeader GrpHdr `xml:"GrpHdr"`
PaymentInformation []PmtInf `xml:"PmtInf"`
TransactionInformation []TxInf `xml:"DrctDbtTxInf"`
}
func (p *PainXML) Valid() error {
var err []string
if e := p.GroupHeader.Valid(); e != nil {
err = append(err, fmt.Sprintf("%v", e))
}
if len(p.PaymentInformation) < 1 {
err = append(err, "no payment information")
}
if len(p.TransactionInformation) < 1 {
err = append(err, "no transaction information")
}
if len(err) > 0 {
return fmt.Errorf("pain XML not valid: %v", strings.Join(err, ", "))
}
return nil
}
type CdtrAgt struct {
InstitutionId FinInstnId `xml:"FinInstId"`
}
type FinInstnId struct {
BIC string `xml:"BIC"`
}
type CdtrSchmeId struct {
Id PartyIdSEPA3 `xml:"Id"`
}
func (c *CdtrSchmeId) Valid() error {
return c.Id.Valid()
}
type CdtrAcct struct {
Id IBAN `xml:"Id"`
}
type IBAN struct {
IBAN string `xml:"IBAN"`
}
func (c *CdtrAcct) Valid() error {
if e := c.Id.Valid(); e != nil {
return fmt.Errorf("creditor account id not valid: %v", e)
}
return nil
}
func (i *IBAN) Valid() error {
if !SEPARegexps["IBAN"].MatchString(i.IBAN) {
return fmt.Errorf("IBAN does not match format")
}
return nil
}
type Code struct {
Code string `xml:"Cd"`
}
func (meta *PmtTpInf) Valid() error {
var err []string
// TODO: check meta.ServiceLevel for nil
if !SEPARegexps["PmtMetaSvcLvl"].MatchString(meta.ServiceLevel.Code) {
err = append(err, "incorrect service level present (must be 'SEPA')")
}
// TODO: check meta.LocalInstrument for nil
if !SEPARegexps["PmtMetaLclInstrm"].MatchString(meta.LocalInstrument.Code) {
err = append(err, "incorrect local instrument present")
}
if !SEPARegexps["SeqTp"].MatchString(meta.SequenceType) {
err = append(err, "sequence type has incorrect format")
}
if len(err) > 0 {
return fmt.Errorf("payment meta not valid: %v", strings.Join(err, ", "))
}
return nil
}
type TxInf struct {
}
type PartySEPA2 struct {
PrivateId PersonIdSEPA2 `xml:"PrvtId"`
}
func (p *PartySEPA2) Valid() error {
return p.PrivateId.Valid()
}
type PartyIdSEPA1 struct {
Name string `xml:"Nm"`
// We do not implement the "Id" element from "PartyIdentificationSEPA1"
}
type PartyIdSEPA3 struct {
Id PartySEPA2 `xml:"Id"`
}
func (p *PartyIdSEPA3) Valid() error {
return p.Id.Valid()
}
type PersonIdSEPA2 struct {
Other RestrictedPersonIdSEPA `xml:"Othr"`
}
func (p *PersonIdSEPA2) Valid() error {
return p.Other.Valid()
}
type RestrictedPersonIdSEPA struct {
Id string `xml:"Id"` // RestrictedPersonIdentifierSEPA
SchemeName RestrictedPersonIdSchemeNameSEPA `xml:"SchmeNm"`
}
func (r *RestrictedPersonIdSEPA) Valid() error {
var err []string
if !SEPARegexps["Id"].MatchString(r.Id) {
err = append(err, "id of RestrictedPersonIdSEPA does not match format")
}
if e := r.SchemeName.Valid(); e != nil {
err = append(err, fmt.Sprintf("SchemeName not valid: %v", e))
}
if len(err) > 0 {
return fmt.Errorf("restricted person id SEPA not valid: %v", strings.Join(err, ", "))
}
return nil
}
type RestrictedPersonIdSchemeNameSEPA struct {
Party string `xml:"Prty"` // IdentificationSchemeNameSEPA
}
func (r *RestrictedPersonIdSchemeNameSEPA) Valid() error {
if r.Party != "SEPA" {
return fmt.Errorf("party should be 'SEPA', got %v", r.Party)
}
return nil
}

44
pain/pain_regexp.go

@ -0,0 +1,44 @@
package pain
import (
"regexp"
)
var (
ISODateTime = regexp.MustCompile(`\d{4}(-\d\d){2}T\d\d(:\d\d){2}`)
RestrictedIdentificationSEPA1 = regexp.MustCompile(`([A-Za-z0-9]|[\+|\?|/|\-|:|\(|\)|\.|,|'| ]){1,35}`)
RestrictedIdentificationSEPA2 = regexp.MustCompile(`([A-Za-z0-9]|[\+|\?|/|\-|:|\(|\)|\.|,|']){1,35}`)
Max15NumericText = regexp.MustCompile(`[0-9]{1,15}`)
RestrictedDecimalNumber = regexp.MustCompile(`[+-]?\d+\.\d\d`)
Max70Text = regexp.MustCompile(`.{1,70}`)
ServiceLevelSEPACode = regexp.MustCompile(`SEPA`)
SequenceType1Code = regexp.MustCompile(`FRST|RCUR|FNAL|OOFF`)
PaymentMethod2Code = regexp.MustCompile(`DD`)
LocalInstrumentSEPACode = regexp.MustCompile(`CORE|B2B`)
IBAN2007Identifier = regexp.MustCompile(`[A-Z]{2,2}[0-9]{2,2}[a-zA-Z0-9]{1,30}`)
RestrictedPersonIdentifierSEPA = regexp.MustCompile(`[a-zA-Z]{2,2}[0-9]{2,2}([A-Za-z0-9]|[\+|\?|/|\-|:|\(|\)|\.|,|']){3,3}([A-Za-z0-9]|[\+|\?|/|\-|:|\(|\)|\.|,|']){1,28}`)
BICIdentifier = regexp.MustCompile(`[A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1}`)
)
var (
SEPARegexps = map[string]*regexp.Regexp{
"BIC": BICIdentifier,
"CreDtTm": ISODateTime,
"CtrlSum": RestrictedDecimalNumber,
"EndToEndId": RestrictionIdentificationSEPA1,
"IBAN": IBAN2007Identifier,
"Id": RestrictedPersonIdentifierSEPA,
"InstrId": RestrictionIdentificationSEPA1,
"MsgId": RestrictedIdentificationSEPA1,
"NbOfTxs": Max15NumericText,
"Nm": Max70Text,
"PmtInfId": RestrictedIdentificationSEPA1,
"PmtMetaLclInstrm": LocalInstrumentSEPACode,
"PmtMetaSvcLvl": ServiceLevelSEPACode,
"PmtMtd": PaymentMethod2Code,
"ReqdColltnDt": ISODateTime,
"SeqTp": SequenceType1Code,
}
)

130
pain/payment_information.go

@ -0,0 +1,130 @@
package pain
import (
"fmt"
)
type PmtInf struct {
Id string `xml:"PmtInfId"`
Method string `xml:"PmtMtd"`
PaymentMeta PmtTpInf `xml:"PmtTpInf"`
CollectionDate string `xml:"ReqdColltnDt"`
Creditor Cdtr `xml:"Cdtr"`
CreditorAccount CdtrAcct `xml:"CdtrAcct"`
CreditorAgent CdtrAgt `xml:"CdtrAgt"`
SchemeId CdtrSchmeId `xml:"CdtrSchmeId"`
Transactions []DrctDbtTxInf `xml:"DrctDbtTxInf"`
}
func (pmt *PmtInf) Valid() error {
var err []string
if !SEPARegexps["PmtInfId"].MatchString(pmt.Id) {
err = append(err, "payment info id does not match expected format")
}
if !SEPARegexps["PmtMtd"].MatchString(pmt.Method) {
err = append(err, "payment method does not match expected format")
}
if e := pmt.PaymentMeta.Valid(); e != nil {
err = append(err, fmt.Sprintf("payment meta not valid: %v", e))
}
if !SEPARegexps["ReqdColltnDt"].MatchString(pmt.CollectionDate) {
err = append(err, "payment collection date does not match expected format")
}
if e := pmt.Creditor.Valid(); e != nil {
err = append(err, fmt.Sprintf("payment creditor not valid: %v", e))
}
if e := pmt.CreditorAccount.Valid(); e != nil {
err = append(err, fmt.Sprintf("payment creditor account not valid: %v", e))
}
if e := pmt.CreditorAgent.Valid(); e != nil {
err = append(err, fmt.Sprintf("payment creditor agent not valid: %v", e))
}
if e := pmt.SchemeId.Valid(); e != nil {
err = append(err, fmt.Sprintf("payment scheme id not valid: %v", e))
}
if len(err) > 0 {
return fmt.Errorf("payment info (Id: %q) not valid: %v", pmt.Id, strings.Join(err, ", "))
}
return nil
}
type PmtTpInf struct {
ServiceLevel Code `xml:"SvcLvl"`
LocalInstrument Code `xml:"LclIntrm"`
SequenceType string `xml:"SeqTp"`
}
type Cdtr struct {
Name string `xml:"Nm"`
PostalAddress PstlAddr `xml:"PstlAddr"`
}
func (c *Cdtr) Valid() error {
var err []string
if !SEPARegexps["Nm"].MatchString(c.Name) {
err = append(err, "creditor name does not match format")
}
if e := c.PostalAddress.Valid(); e != nil {
err = append(err, fmt.Sprintf("creditor postal address not valid: %v", e))
}
if len(err) > 0 {
return fmt.Errorf("creditor (Nm: %q) not valid: %v", c.Name, strings.Join(err, ", "))
}
return nil
}
type AdrLine string
type PstlAddr struct {
Country string `xml:"Ctry"`
AddressLines []AdrLine `xml:"AdrLine"`
}
func (p *PstlAddr) Valid() error {
var err []string
if !SEPARegexps["Ctry"].MatchString(p.Country) {
err = append(err, "country does not match format")
}
if len(p.AddressLines) > 2 {
err = append(err, "expected at most 2 address lines")
}
for i, line := range p.AddressLines {
if !SEPARegexps["AdrLine"].MatchString(string(line)) {
err = append(err, fmt.Sprintf("address line %d (%q) does not match format", i, line))
}
}
if len(err) > 0 {
return fmt.Errorf("address not valid: %v", strings.Join(err, ", "))
}
return nil
}
type CdtrAgt struct {
InstitutionId FinInstnId `xml:"FinInstId"`
}
func (c *CdtrAgt) Valid() error {
return c.InstituionId.Valid()
}
type FinInstnId struct {
BIC string `xml:"BIC"`
}
func (f *FinInstnId) Valid() error {
if !SEPARegexps["BIC"].MatchString(f.BIC) {
return fmt.Errorf("BIC not in expected format")
}
return nil
}
type CdtrSchmeId struct {
Id PartyIdSEPA3 `xml:"Id"`
}
func (c *CdtrSchmeId) Valid() error {
return c.Id.Valid()
}

36
pain/payment_information_transactions.go

@ -0,0 +1,36 @@
package pain
import (
"fmt"
"strings"
)
type DrctDbtTxInf struct {
Id PaymentId `xml:"PmtId"`
Amount CurrencyWithAmount `xml:"InstdAmt"`
}
type CurrencyWithAmount struct {
Currency string `xml:"Ccy,attr"`
Value string `xml:",innerxml"`
}
type PaymentId struct {
InstrumentId string `xml:"InstrId"`
EndToEndId string `xml:"EndToEndId"`
}
func (p *PaymentId) Valid() error {
var err []string
if !SEPARegexps["InstrId"].MatchString(p.InstrumentId) {
err = append(err, "instrument id does not match format")
}
if !SEPARegexps["EndToEndId"].MatchString(p.EndToEndId) {
err = append(err, "end-to-end id does not match format")
}
if len(err) > 0 {
return fmt.Errorf("payment id not valid: %v", strings.Join(err, ", "))
}
return nil
}

21
pain_regexp.go

@ -1,21 +0,0 @@
package sepa
import (
"regexp"
)
var (
ISODateTime = regexp.MustCompile(`\d{4}(-\d\d){2}T\d\d(:\d\d){2}`)
RestrictedIdentificationSEPA1 = regexp.MustCompile(`([A-Za-z0-9]|[\+|\?|/|\-|:|\(|\)|\.|,|'| ]){1,35}`)
RestrictedIdentificationSEPA2 = regexp.MustCompile(`([A-Za-z0-9]|[\+|\?|/|\-|:|\(|\)|\.|,|']){1,35}`)
Max15NumericText = regexp.MustCompile(`[0-9]{1,15}`)
RestrictedDecimalNumber = regexp.MustCompile(`[+-]?\d+\.\d\d`)
Max70Text = regexp.MustCompile(`.{1,70}`)
ServiceLevelSEPACode = regexp.MustCompile(`SEPA`)
SequenceType1Code = regexp.MustCompile(`FRST|RCUR|FNAL|OOFF`)
PaymentMethod2Code = regexp.MustCompile(`DD`)
LocalInstrumentSEPACode = regexp.MustCompile(`CORE|B2B`)
IBAN2007Identifier = regexp.MustCompile(`[A-Z]{2,2}[0-9]{2,2}[a-zA-Z0-9]{1,30}`)
)
Loading…
Cancel
Save