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, "Id": RestrictedPersonIdentifierSEPA, "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 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 } 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"` } 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.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 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 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 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 }