Browse Source

Allow importing batch lines via CSV

master
Gerdriaan Mulder 6 years ago
parent
commit
20adbcd59e
  1. 64
      cmd/createbatch/import_csv.go
  2. 48
      cmd/createbatch/main.go
  3. 30
      pain/api.go
  4. 1
      pain/pain_regexp.go
  5. 47
      pain/payment_information_transactions.go

64
cmd/createbatch/import_csv.go

@ -0,0 +1,64 @@
package main
import (
"encoding/csv"
"fmt"
"os"
"strconv"
"src.wolkict.net/sepa/pain"
)
// FieldsPerBatchLine are the expected number of fields in each CSV line.
const FieldsPerBatchLine = 9
// ImportIndex keeps track of what column corresponds to what data.
type ImportIndex int
// These are our numbered input fields
const (
ImportName ImportIndex = iota
ImportAddr1
ImportAddr2
ImportCountry
ImportIBAN
ImportMandateId
ImportSignatureDate
ImportAmount
ImportInfo
)
func ImportCSV(fn string) ([]pain.DrctDbtTxInf, error) {
f, err := os.Open(fn)
if err != nil {
return nil, fmt.Errorf("opening file %q failed: %v", fn, err)
}
defer f.Close()
csvReader := csv.NewReader(f)
csvReader.FieldsPerRecord = FieldsPerBatchLine
all, err := csvReader.ReadAll()
if err != nil {
return nil, fmt.Errorf("reading csv %q failed: %v", fn, err)
}
ret := make([]pain.DrctDbtTxInf, len(all))
for i, line := range all {
if line[0] == "name" {
// Header line
continue
}
dbtr := pain.NewDebtor(line[ImportName], line[ImportAddr1], line[ImportAddr2], line[ImportCountry])
dbtrAcct := pain.NewDbtrAcct(line[ImportIBAN])
mandateInfo := pain.NewMandateInfo(line[ImportMandateId], line[ImportSignatureDate])
amount, err := strconv.ParseFloat(line[ImportAmount], 64)
if err != nil {
return nil, fmt.Errorf("error importing line %d, cannot parse amount: %v", i, err)
}
ret[i] = pain.NewDirectDebitTransaction(dbtr, dbtrAcct, mandateInfo, amount, line[ImportInfo], i)
}
return ret, nil
}

48
cmd/createbatch/main.go

@ -2,18 +2,58 @@ package main
import (
"encoding/xml"
"flag"
"fmt"
"log"
"os"
"time"
"src.wolkict.net/sepa/pain"
)
var (
csvFile = flag.String("csv", "", "use this batch import CSV file")
execDate = flag.String("date", time.Now().AddDate(0, 0, 7).Format("2006-01-02"), "manually set batch execution date")
msgId = flag.String("msgId", time.Now().Format("20060102T150405"), "manually set this msgId")
)
func usage() {
fmt.Println("Usage: createbatch -csv <file.csv>")
flag.PrintDefaults()
}
func main() {
d := pain.NewDocument("PayPro B.V")
d.SetMeta(CREDITOR_NAME, CREDITOR_ADDR1, CREDITOR_ADDR2, CREDITOR_IBAN, CREDITOR_BIC, CREDITOR_ID,
time.Date(2020, time.January, 30, 12, 0, 0, 0, time.UTC), "RCUR")
err := d.Finalize("asdfasdf")
flag.Parse()
if *csvFile == "" {
usage()
log.Fatalf("mandatory -csv not set")
}
if _, err := os.Open(*csvFile); err != nil {
log.Fatalf("cannot access CSV %q: %v", *csvFile, err)
}
collectionDate, err := time.Parse("2006-01-02", *execDate)
if err != nil {
log.Fatalf("could not parse execution date: %v", err)
}
batchLines, err := ImportCSV(*csvFile)
if err != nil {
log.Fatalf("cannot import batch CSV: %v", err)
}
d := pain.NewDocument(INITIATOR_NAME)
d.SetMeta(CREDITOR_NAME, CREDITOR_ADDR1, CREDITOR_ADDR2, CREDITOR_IBAN, CREDITOR_BIC, CREDITOR_ID, collectionDate, "RCUR")
for _, tx := range batchLines {
if tx.Id == (pain.PaymentId{}) {
continue
}
d.AddTransaction(tx)
}
err = d.Finalize(*msgId)
if err != nil {
log.Fatalf("finalizing failed: %v", err)
}

30
pain/api.go

@ -2,6 +2,7 @@ package pain
import (
"fmt"
"strconv"
"time"
)
@ -65,17 +66,12 @@ func (d *Document) SetMeta(creditorName, creditorAddr1, creditorAddr2, creditorI
}
func (d *Document) Finalize(msgId string) error {
// GroupHeader:
// Set msgId
/*
if len(d.Contents.PaymentInformation) == 0 {
return fmt.Errorf("no payment information, aborting")
}
if len(d.Contents.TransactionInformation) == 0 {
return fmt.Errorf("no transaction information, aborting")
}
*/
if len(d.Contents.PaymentInformation) == 0 {
return fmt.Errorf("no payment information, aborting")
}
if len(d.Contents.PaymentInformation[0].Transactions) == 0 {
return fmt.Errorf("no transaction information, aborting")
}
csum := 0.0
d.Contents.GroupHeader.MessageId = msgId
@ -84,13 +80,23 @@ func (d *Document) Finalize(msgId string) error {
d.Contents.GroupHeader.Sum = fmt.Sprintf("%.2f", csum)
d.Contents.PaymentInformation[0].Id = msgId
for _, tx := range d.Contents.PaymentInformation[0].Transactions {
flt, err := strconv.ParseFloat(tx.Amount.Value, 64)
if err != nil {
panic(fmt.Errorf("could not parse float from %q: %v", tx.Amount.Value, err))
}
csum += flt
}
d.Contents.GroupHeader.Sum = fmt.Sprintf("%.2f", csum)
return d.Valid()
}
func (d *Document) AddTransaction(tx *DrctDbtTxInf) error {
func (d *Document) AddTransaction(tx DrctDbtTxInf) error {
if len(d.Contents.PaymentInformation) == 0 {
return fmt.Errorf("payment information not yet initialized")
}
d.Contents.PaymentInformation[0].Transactions = append(d.Contents.PaymentInformation[0].Transactions, tx)
return nil
}

1
pain/pain_regexp.go

@ -33,6 +33,7 @@ var (
"CreDtTm": ISODateTime,
"CtrlSum": RestrictedDecimalNumber,
"Ctry": CountryCode,
"DtOfSgntr": ISODate,
"EndToEndId": RestrictedIdentificationSEPA1,
"IBAN": IBAN2007Identifier,
"Id": RestrictedPersonIdentifierSEPA,

47
pain/payment_information_transactions.go

@ -3,6 +3,7 @@ package pain
import (
"fmt"
"strings"
"time"
)
type DrctDbtTxInf struct {
@ -15,12 +16,11 @@ type DrctDbtTxInf struct {
Info RmtInf `xml:"RmtInf"`
}
func NewDirectDebitTransaction(debtor Dbtr, account DbtrAcct, mandateInfo MndtRltdInf, amount float64, additionalInfo string) DrctDbtTxInf {
e2eid := fmt.Sprintf("%v%4d", time.Now().Format("20060102"), 4) // TODO: inform this function of current number of transactions
func NewDirectDebitTransaction(debtor Dbtr, account DbtrAcct, mandateInfo MndtRltdInf, amount float64, additionalInfo string, counter int) DrctDbtTxInf {
e2eid := fmt.Sprintf("%v%04d", time.Now().Format("20060102"), counter)
return DrctDbtTxInf{
Id: PaymentId{
InstrumentId: "asdfasdf",
EndToEndId: e2eid,
EndToEndId: e2eid,
},
Amount: CurrencyWithAmount{
Currency: "EUR",
@ -31,12 +31,14 @@ func NewDirectDebitTransaction(debtor Dbtr, account DbtrAcct, mandateInfo MndtRl
},
Agent: DbtrAgt{
InstitutionId: FinInstnId{
BIC: "KNABNL2H", // fixme
BIC: "NOTPROVIDED",
},
},
Debtor: debtor,
Account: account,
Info: additionalInfo,
Info: RmtInf{
Value: additionalInfo,
},
}
}
@ -88,7 +90,7 @@ func (c *CurrencyWithAmount) Valid() error {
}
type PaymentId struct {
InstrumentId string `xml:"InstrId"`
InstrumentId string `xml:"InstrId,omitempty"`
EndToEndId string `xml:"EndToEndId"`
}
@ -115,6 +117,14 @@ type MndtRltdInf struct {
//ElctrncSgntr is optional
}
func NewMandateInfo(id, signatureDate string) MndtRltdInf {
return MndtRltdInf{
Id: id,
SignatureDate: signatureDate,
IsAmended: false,
}
}
func (m *MndtRltdInf) Valid() error {
var err []string
if !SEPARegexps["MndtId"].MatchString(m.Id) {
@ -152,6 +162,19 @@ type Dbtr struct {
Address PstlAdr `xml:"PstlAdr"`
}
func NewDebtor(name, line1, line2, country string) Dbtr {
return Dbtr{
Name: name,
Address: PstlAdr{
Country: country,
AddressLines: []AdrLine{
AdrLine(line1),
AdrLine(line2),
},
},
}
}
func (d *Dbtr) Valid() error {
var err []string
if !SEPARegexps["Nm"].MatchString(d.Name) {
@ -168,7 +191,15 @@ func (d *Dbtr) Valid() error {
}
type DbtrAcct struct {
Id IBAN `xml:"IBAN"`
Id IBAN `xml:"Id"`
}
func NewDbtrAcct(iban string) DbtrAcct {
return DbtrAcct{
Id: IBAN{
IBAN: iban,
},
}
}
func (d *DbtrAcct) Valid() error {

Loading…
Cancel
Save