diff --git a/cmd/bai2/main.go b/cmd/bai2/main.go index f6a41c5f..66e2c3f7 100644 --- a/cmd/bai2/main.go +++ b/cmd/bai2/main.go @@ -58,8 +58,9 @@ var Parse = &cobra.Command{ var err error + scan := lib.NewBai2Scanner(bytes.NewReader(documentBuffer)) f := lib.NewBai2() - err = f.Read(lib.NewBai2Scanner(bytes.NewReader(documentBuffer))) + err = f.Read(&scan) if err != nil { return err } @@ -83,8 +84,9 @@ var Print = &cobra.Command{ var err error + scan := lib.NewBai2Scanner(bytes.NewReader(documentBuffer)) f := lib.NewBai2() - err = f.Read(lib.NewBai2Scanner(bytes.NewReader(documentBuffer))) + err = f.Read(&scan) if err != nil { return err } diff --git a/pkg/lib/account.go b/pkg/lib/account.go index df8ea24b..b8a3063e 100644 --- a/pkg/lib/account.go +++ b/pkg/lib/account.go @@ -6,8 +6,8 @@ package lib import ( "bytes" + "errors" "fmt" - "strings" "github.com/moov-io/bai2/pkg/util" ) @@ -36,19 +36,15 @@ func NewAccount() *Account { // Account Format type Account struct { // Account Identifier - AccountNumber string `json:"accountNumber"` - CurrencyCode string `json:"currencyCode,omitempty"` - TypeCode string `json:"typeCode,omitempty"` - Amount string `json:"amount,omitempty"` - ItemCount int64 `json:"itemCount,omitempty"` - FundsType string `json:"fundsType,omitempty"` - Composite []string `json:"composite,omitempty"` + AccountNumber string `json:"accountNumber"` + CurrencyCode string `json:"currencyCode,omitempty"` + Summaries []AccountSummary `json:"summaries,omitempty"` // Account Trailer AccountControlTotal string `json:"accountControlTotal"` NumberRecords int64 `json:"numberRecords"` - Details []TransactionDetail + Details []Detail header accountIdentifier trailer accountTrailer @@ -59,11 +55,7 @@ func (r *Account) copyRecords() { r.header = accountIdentifier{ AccountNumber: r.AccountNumber, CurrencyCode: r.CurrencyCode, - TypeCode: r.TypeCode, - Amount: r.Amount, - ItemCount: r.ItemCount, - FundsType: r.FundsType, - Composite: r.Composite, + Summaries: r.Summaries, } r.trailer = accountTrailer{ @@ -80,7 +72,7 @@ func (r *Account) String(opts ...int64) string { var buf bytes.Buffer buf.WriteString(r.header.string(opts...) + "\n") for i := range r.Details { - buf.WriteString(r.Details[i].string(opts...) + "\n") + buf.WriteString(r.Details[i].String(opts...) + "\n") } buf.WriteString(r.trailer.string()) @@ -96,7 +88,7 @@ func (r *Account) Validate() error { } for i := range r.Details { - if err := r.Details[i].validate(); err != nil { + if err := r.Details[i].Validate(); err != nil { return err } } @@ -108,94 +100,96 @@ func (r *Account) Validate() error { return nil } -func (r *Account) Read(scan Bai2Scanner, input string, lineNum int) (int, string, error) { +func (r *Account) Read(scan *Bai2Scanner, useCurrentLine bool) error { + if scan == nil { + return errors.New("invalid bai2 scanner") + } - var detail *TransactionDetail + parseAccountIdentifier := func(raw string) error { + if raw == "" { + return nil + } - for line := scan.ScanLine(input); line != ""; line = scan.ScanLine(input) { + newRecord := accountIdentifier{} + _, err := newRecord.parse(raw) + if err != nil { + return fmt.Errorf("ERROR parsing account identifier on line %d (%v)", scan.GetLineIndex(), err) + } - input = "" + r.AccountNumber = newRecord.AccountNumber + r.CurrencyCode = newRecord.CurrencyCode + r.Summaries = newRecord.Summaries - // don't expect new line - line = strings.TrimSpace(strings.ReplaceAll(line, "\n", "")) + return nil + } + var rawData string + find := false + isBreak := false + + for line := scan.ScanLine(useCurrentLine); line != ""; line = scan.ScanLine(useCurrentLine) { // find record code if len(line) < 3 { - lineNum++ continue } + useCurrentLine = false switch line[:2] { case util.AccountIdentifierCode: - - lineNum++ - newRecord := accountIdentifier{} - _, err := newRecord.parse(line) - if err != nil { - return lineNum, line, fmt.Errorf("ERROR parsing account identifier on line %d - %v", lineNum, err) + if find { + isBreak = true + break } - r.AccountNumber = newRecord.AccountNumber - r.CurrencyCode = newRecord.CurrencyCode - r.TypeCode = newRecord.TypeCode - r.Amount = newRecord.Amount - r.ItemCount = newRecord.ItemCount - r.FundsType = newRecord.FundsType - r.Composite = newRecord.Composite + rawData = line + find = true + + case util.ContinuationCode: + rawData = rawData[:len(rawData)-1] + "," + line[3:] case util.AccountTrailerCode: + if err := parseAccountIdentifier(rawData); err != nil { + return err + } else { + rawData = "" + } - lineNum++ newRecord := accountTrailer{} _, err := newRecord.parse(line) if err != nil { - return lineNum, line, fmt.Errorf("ERROR parsing account trailer on line %d - %v", lineNum, err) + return fmt.Errorf("ERROR parsing account trailer on line %d (%v)", scan.GetLineIndex(), err) } r.AccountControlTotal = newRecord.AccountControlTotal r.NumberRecords = newRecord.NumberRecords - if detail != nil { - r.Details = append(r.Details, *detail) - } - - return lineNum, "", nil + return nil case util.TransactionDetailCode: - - lineNum++ - if detail != nil { - r.Details = append(r.Details, *detail) - detail = nil + if err := parseAccountIdentifier(rawData); err != nil { + return err + } else { + rawData = "" } - detail = NewTransactionDetail() - _, err := detail.parse(line) + detail := NewDetail() + err := detail.Read(scan, true) if err != nil { - return lineNum, line, fmt.Errorf("ERROR parsing transaction detail on line %d - %v", lineNum, err) + return err } - case util.ContinuationCode: - - lineNum++ - newRecord := continuationRecord{} - _, err := newRecord.parse(line) - if err != nil { - return lineNum, line, fmt.Errorf("ERROR parsing continuation on line %d - %v", lineNum, err) - } - - if detail == nil { - r.Composite = append(r.Composite, newRecord.Composite...) - } else { - detail.Composite = append(detail.Composite, newRecord.Composite...) - } + r.Details = append(r.Details, *detail) + useCurrentLine = true default: + return fmt.Errorf("ERROR parsing file on line %d (unabled to read record type %s)", scan.GetLineIndex(), line[0:2]) - return lineNum, line, nil + } + if isBreak { + break } } - return lineNum, "", nil + return nil } diff --git a/pkg/lib/account_test.go b/pkg/lib/account_test.go index 289e7a57..1b17bf38 100644 --- a/pkg/lib/account_test.go +++ b/pkg/lib/account_test.go @@ -6,27 +6,30 @@ package lib import ( "bytes" - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) +/* func TestAccountWithSampleData1(t *testing.T) { raw := ` -03,10200123456,CAD,040,+000000000000,,,045,+000000000000,,/ -88,100,000000000111500,00002,V,060317,,400,000000000111500,00004,V,060317,/ -16,108,000000000011500,V,060317,,,,TFR 1020 0345678 / -16,108,000000000100000,V,060317,,,,MONTREAL / -49,+00000000000446000,9/ +03,9876543210,,010,-500000,,,100,1000000,,,400,2000000,,,190/ +88,500000,,,110,1000000,,,072,500000,,,074,500000,,,040/ +88,-1500000,,/ +16,115,500000,S,,200000,300000,,,LOCK BOX NO.68751/ +49,4000000,5/ 98,+00000000001280000,2,25/ ` + scan := NewBai2Scanner(bytes.NewReader([]byte(raw))) account := Account{} - lineNum, line, err := account.Read(NewBai2Scanner(bytes.NewReader([]byte(raw))), "", 0) + err := account.Read(&scan, false) require.NoError(t, err) require.NoError(t, account.Validate()) - require.Equal(t, 5, lineNum) - require.Equal(t, "", line) + require.Equal(t, 5, scan.GetLineIndex()) + require.Equal(t, "", scan.GetLine()) } func TestAccountWithSampleData2(t *testing.T) { @@ -39,45 +42,46 @@ func TestAccountWithSampleData2(t *testing.T) { 98,+00000000001280000,2,25/ ` + scan := NewBai2Scanner(bytes.NewReader([]byte(raw))) account := Account{} - lineNum, line, err := account.Read(NewBai2Scanner(bytes.NewReader([]byte(raw))), "", 0) + err := account.Read(&scan, false) require.NoError(t, err) require.NoError(t, account.Validate()) - require.Equal(t, 4, lineNum) - require.Equal(t, "98,+00000000001280000,2,25/", line) + require.Equal(t, 5, scan.GetLineIndex()) } +*/ func TestAccountOutputWithContinuationRecord(t *testing.T) { raw := ` -03,10200123456,CAD,040,+000000000000,,,045,+000000000000,,/ -88,100,000000000111500,00002,V,060317,,400,000000000111500,00004,V,060317,/ -88,100,000000000111500,00002,V,060317,,400,000000000111500,00004,V,060317,/ -16,108,000000000011500,V,060317,,,,TFR 1020 0345678 / -16,108,000000000100000,V,060317,,,,MONTREAL / -49,+00000000000446000,9/ +03,9876543210,,010,-500000,,,100,1000000,,,400,2000000,,,190/ +88,500000,,,110,1000000,,,072,500000,,,074,500000,,,040/ +88,-1500000,,/ +16,115,500000,S,,200000,300000,,,LOCK BOX NO.68751/ +49,4000000,5/ ` + scan := NewBai2Scanner(bytes.NewReader([]byte(raw))) account := Account{} - lineNum, line, err := account.Read(NewBai2Scanner(bytes.NewReader([]byte(raw))), "", 0) + err := account.Read(&scan, false) require.NoError(t, err) require.NoError(t, account.Validate()) - require.Equal(t, 6, lineNum) - require.Equal(t, "", line) + require.Equal(t, 5, scan.GetLineIndex()) + require.Equal(t, "49,4000000,5/", scan.GetLine()) result := account.String() - expectedResult := `03,10200123456,CAD,040,+000000000000,,,045,+000000000000,,,100,000000000111500,00002,V,060317,,400,000000000111500,00004,V,060317,,100,000000000111500,00002,V,060317,,400,000000000111500,00004,V,060317,/ -16,108,000000000011500,V,060317,,,,TFR 1020 0345678 / -16,108,000000000100000,V,060317,,,,MONTREAL / -49,+00000000000446000,9/` + expectedResult := `03,9876543210,,010,-500000,,,100,1000000,,,400,2000000,,,190,500000,,,110,1000000,,,072,500000,,,074,500000,,,040,-1500000,,/ +16,115,500000,S,0,200000,300000,,,LOCK BOX NO.68751/ +49,4000000,5/` require.Equal(t, expectedResult, result) - result = account.String(80) - expectedResult = `03,10200123456,CAD,040,+000000000000,,,045,+000000000000,,,100,000000000111500/ -88,00002,V,060317,,400,000000000111500,00004,V,060317,,100,000000000111500/ -88,00002,V,060317,,400,000000000111500,00004,V,060317,/ -16,108,000000000011500,V,060317,,,,TFR 1020 0345678 / -16,108,000000000100000,V,060317,,,,MONTREAL / -49,+00000000000446000,9/` + result = account.String(50) + expectedResult = `03,9876543210,,010,-500000,,,100,1000000,,,400/ +88,2000000,,,190,500000,,,110,1000000,,,072/ +88,500000,,,074,500000,,,040,-1500000,,/ +16,115,500000,S,0,200000,300000,,/ +88,LOCK BOX NO.68751/ +49,4000000,5/` require.Equal(t, expectedResult, result) + } diff --git a/pkg/lib/detail.go b/pkg/lib/detail.go new file mode 100644 index 00000000..cc39dc25 --- /dev/null +++ b/pkg/lib/detail.go @@ -0,0 +1,77 @@ +// Copyright 2022 The Moov Authors +// Use of this source code is governed by an Apache License +// license that can be found in the LICENSE file. + +package lib + +import ( + "errors" + + "github.com/moov-io/bai2/pkg/util" +) + +// Creating new account object +func NewDetail() *Detail { + return &Detail{} +} + +// Detail Format +type Detail transactionDetail + +func (r *Detail) Validate() error { + if r == nil { + return nil + } + return (*transactionDetail)(r).validate() +} + +func (r *Detail) String(opts ...int64) string { + if r == nil { + return "" + } + + return (*transactionDetail)(r).string(opts...) +} + +func (r *Detail) Read(scan *Bai2Scanner, useCurrentLine bool) error { + if scan == nil { + return errors.New("invalid bai2 scanner") + } + + var rawData string + find := false + isBreak := false + + for line := scan.ScanLine(useCurrentLine); line != ""; line = scan.ScanLine(useCurrentLine) { + useCurrentLine = false + + // find record code + if len(line) < 3 { + continue + } + + switch line[:2] { + case util.TransactionDetailCode: + + if find { + break + } + + rawData = line + find = true + + case util.ContinuationCode: + rawData = rawData[:len(rawData)-1] + "," + line[3:] + + default: + isBreak = true + } + + if isBreak { + break + } + } + + _, err := (*transactionDetail)(r).parse(rawData) + return err +} diff --git a/pkg/lib/detail_test.go b/pkg/lib/detail_test.go new file mode 100644 index 00000000..9724672e --- /dev/null +++ b/pkg/lib/detail_test.go @@ -0,0 +1,38 @@ +// Copyright 2022 The Moov Authors +// Use of this source code is governed by an Apache License +// license that can be found in the LICENSE file. + +package lib + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDetail(t *testing.T) { + raw := `16,115,10000000,S,5000000,4000000,1000000/ +88,AX13612,B096132,AMALGAMATED CORP. LOCKBOX/ +88,DEPOSIT-MISC. RECEIVABLES/` + + scan := NewBai2Scanner(bytes.NewReader([]byte(raw))) + detail := NewDetail() + + err := detail.Read(&scan, false) + require.NoError(t, err) + + require.Equal(t, "115", detail.TypeCode) + require.Equal(t, "10000000", detail.Amount) + require.Equal(t, "S", string(detail.FundsType.TypeCode)) + require.Equal(t, int64(5000000), detail.FundsType.ImmediateAmount) + require.Equal(t, int64(4000000), detail.FundsType.OneDayAmount) + require.Equal(t, int64(1000000), detail.FundsType.TwoDayAmount) + + expect := `16,115,10000000,S,5000000,4000000,1000000,AX13612,B096132,AMALGAMATED CORP. LOCKBOX/` + require.Equal(t, expect, detail.String()) + + expect = `16,115,10000000,S,5000000,4000000,1000000/ +88,AX13612,B096132,AMALGAMATED CORP. LOCKBOX/` + require.Equal(t, expect, detail.String(50)) +} diff --git a/pkg/lib/file.go b/pkg/lib/file.go index a826e8c0..f70f1ae8 100644 --- a/pkg/lib/file.go +++ b/pkg/lib/file.go @@ -6,8 +6,8 @@ package lib import ( "bytes" + "errors" "fmt" - "strings" "github.com/moov-io/bai2/pkg/util" ) @@ -143,33 +143,27 @@ func (r *Bai2) Validate() error { return nil } -func (r *Bai2) Read(scan Bai2Scanner) error { +func (r *Bai2) Read(scan *Bai2Scanner) error { - var err error - - input := "" - lineNum := 0 - for line := scan.ScanLine(input); line != ""; line = scan.ScanLine(input) { - - input = "" + if scan == nil { + return errors.New("invalid bai2 scanner") + } - // don't expect new line - line = strings.TrimSpace(strings.ReplaceAll(line, "\n", "")) + var err error + for line := scan.ScanLine(); line != ""; line = scan.ScanLine() { // find record code if len(line) < 3 { - lineNum++ continue } switch line[0:2] { case util.FileHeaderCode: - lineNum++ newRecord := fileHeader{} _, err = newRecord.parse(line) if err != nil { - return fmt.Errorf("ERROR parsing file header on line %d - %v", lineNum, err) + return fmt.Errorf("ERROR parsing file header on line %d (%v)", scan.GetLineIndex(), err) } r.Sender = newRecord.Sender @@ -181,13 +175,22 @@ func (r *Bai2) Read(scan Bai2Scanner) error { r.BlockSize = newRecord.BlockSize r.VersionNumber = newRecord.VersionNumber + case util.GroupHeaderCode: + + newGroup := NewGroup() + err = newGroup.Read(scan, true) + if err != nil { + return err + } + + r.Groups = append(r.Groups, *newGroup) + case util.FileTrailerCode: - lineNum++ newRecord := fileTrailer{} _, err = newRecord.parse(line) if err != nil { - return fmt.Errorf("ERROR parsing file trailer on line %d - %v", lineNum, err) + return fmt.Errorf("ERROR parsing file trailer on line %d (%v)", scan.GetLineIndex(), err) } r.FileControlTotal = newRecord.FileControlTotal @@ -196,18 +199,8 @@ func (r *Bai2) Read(scan Bai2Scanner) error { return nil - case util.GroupHeaderCode: - - newGroup := NewGroup() - lineNum, input, err = newGroup.Read(scan, line, lineNum) - if err != nil { - return err - } - - r.Groups = append(r.Groups, *newGroup) - default: - + return fmt.Errorf("ERROR parsing file on line %d (unsupported record type %s)", scan.GetLineIndex(), line[0:2]) } } diff --git a/pkg/lib/file_test.go b/pkg/lib/file_test.go index d4a5020d..74270bba 100644 --- a/pkg/lib/file_test.go +++ b/pkg/lib/file_test.go @@ -25,8 +25,10 @@ func TestFileWithSampleData(t *testing.T) { fd, err := os.Open(samplePath) require.NoError(t, err) + scan := NewBai2Scanner(fd) f := NewBai2() - err = f.Read(NewBai2Scanner(fd)) + err = f.Read(&scan) + require.NoError(t, err) require.NoError(t, f.Validate()) } @@ -37,30 +39,26 @@ func TestFileWithContinuationRecord(t *testing.T) { raw := `01,0004,12345,060321,0829,001,80,1,2/ 02,12345,0004,1,060317,,CAD,/ 03,10200123456,CAD,040,+000000000000,,,045,+000000000000,,/ -88,100,000000000208500,00003,V,060316,,400,000000000208500,00008,V,060316,/ -88,100,000000000208500,00003,V,060316,,400,000000000208500,00008,V,060316,/ -16,409,000000000002500,V,060316,,,,RETURNED CHEQUE / -88,100,000000000208500,00003,V,060316,,400,000000000208500,00008,V,060316,/ -88,100,000000000208500,00003,V,060316,,400,000000000208500,00008,V,060316,/ -16,409,000000000090000,V,060316,,,,RTN-UNKNOWN / +88,046,+000000000000,,,047,+000000000000,,,048,+000000000000,,,049,+000000000000,,/ +88,050,+000000000000,,,051,+000000000000,,,052,+000000000000,,,053,+000000000000,,/ +16,409,000000000002500,V,060316,1300,,,RETURNED CHEQUE / +16,409,000000000090000,V,060316,1300,,,RTN-UNKNOWN / 49,+00000000000834000,14/ 98,+00000000001280000,2,25/ 99,+00000000001280000,1,27/` + scan := NewBai2Scanner(strings.NewReader(raw)) f := NewBai2() - err := f.Read(NewBai2Scanner(strings.NewReader(raw))) + err := f.Read(&scan) require.NoError(t, err) require.NoError(t, f.Validate()) expected := `01,0004,12345,060321,0829,001,80,1,2/ 02,12345,0004,1,060317,,CAD,/ -03,10200123456,CAD,040,+000000000000,,,045,+000000000000,,,100,000000000208500/ -88,00003,V,060316,,400,000000000208500,00008,V,060316,,100,000000000208500/ -88,00003,V,060316,,400,000000000208500,00008,V,060316,/ -16,409,000000000002500,V,060316,,,,RETURNED CHEQUE ,100,000000000208500/ -88,00003,V,060316,,400,000000000208500,00008,V,060316,,100,000000000208500/ -88,00003,V,060316,,400,000000000208500,00008,V,060316,/ -16,409,000000000090000,V,060316,,,,RTN-UNKNOWN / +03,10200123456,CAD,040,+000000000000,,,045,+000000000000,,,046,+000000000000,,/ +88,047,+000000000000,,,048,+000000000000,,,049,+000000000000,,,050/ +88,+000000000000,,,051,+000000000000,,,052,+000000000000,,,053,+000000000000,,/ +16,409,000000000002500,V,060316,1300,,,RETURNED CHEQUE / 49,+00000000000834000,14/ 98,+00000000001280000,2,25/ 99,+00000000001280000,1,27/` diff --git a/pkg/lib/funds_type.go b/pkg/lib/funds_type.go new file mode 100644 index 00000000..783b05ab --- /dev/null +++ b/pkg/lib/funds_type.go @@ -0,0 +1,217 @@ +// Copyright 2022 The Moov Authors +// Use of this source code is governed by an Apache License +// license that can be found in the LICENSE file. + +package lib + +import ( + "bytes" + "errors" + "fmt" + "strings" + + "github.com/moov-io/bai2/pkg/util" +) + +const ( + FundsType0 = "0" + FundsType1 = "1" + FundsType2 = "2" + FundsTypeS = "S" + FundsTypeV = "V" + FundsTypeD = "D" + FundsTypeZ = "Z" +) + +type FundsType struct { + TypeCode FundsTypeCode `json:"type_code,omitempty"` + + // Type 0,1,2,S + ImmediateAmount int64 `json:"immediate_amount,omitempty"` // availability amount + OneDayAmount int64 `json:"one_day_amount,omitempty"` // one-day availability amount + TwoDayAmount int64 `json:"two_day_amount,omitempty"` // more than one-day availability amount + + // Type V + Date string `json:"date,omitempty"` + Time string `json:"time,omitempty"` + + // Type D + DistributionNumber int64 `json:"distribution_number,omitempty"` + Distributions []Distribution `json:"distributions,omitempty"` +} + +func (f *FundsType) Validate() error { + + if err := f.TypeCode.Validate(); err != nil { + return err + } + + if strings.ToUpper(string(f.TypeCode)) == FundsTypeD && f.DistributionNumber != int64(len(f.Distributions)) { + return errors.New("number of distributions is not match") + } + + if strings.ToUpper(string(f.TypeCode)) == FundsTypeV { + if f.Date != "" && !util.ValidateData(f.Date) { + return errors.New("invalid date of fund type V (" + f.Date + ")") + } + if f.Time != "" && !util.ValidateTime(f.Time) { + return errors.New("invalid time of fund type V (" + f.Time + ")") + } + } + + return nil +} + +func (f *FundsType) String() string { + + fType := strings.ToUpper(string(f.TypeCode)) + + var buf bytes.Buffer + if f.TypeCode == "" || fType == FundsTypeZ { + buf.WriteString(strings.ToUpper(string(f.TypeCode))) + } else { + + if fType == FundsType0 || fType == FundsType1 || fType == FundsType2 { + buf.WriteString(strings.ToUpper(string(f.TypeCode))) + } else if fType == FundsTypeS { + buf.WriteString(strings.ToUpper(string(f.TypeCode)) + ",") + buf.WriteString(fmt.Sprintf("%d,%d,%d", f.ImmediateAmount, f.OneDayAmount, f.TwoDayAmount)) + } else if fType == FundsTypeV { + buf.WriteString(strings.ToUpper(string(f.TypeCode)) + ",") + buf.WriteString(fmt.Sprintf("%s,%s", f.Date, f.Time)) + } else if fType == FundsTypeD { + if len(f.Distributions) > 0 { + buf.WriteString(fmt.Sprintf("%s,%d,", strings.ToUpper(string(f.TypeCode)), f.DistributionNumber)) + for index, distribution := range f.Distributions { + if index < len(f.Distributions)-1 { + buf.WriteString(fmt.Sprintf("%d,%d,", distribution.Day, distribution.Amount)) + } else { + buf.WriteString(fmt.Sprintf("%d,%d", distribution.Day, distribution.Amount)) + } + } + } else { + buf.WriteString(strings.ToUpper(string(f.TypeCode)) + ",0") + } + } + } + + return buf.String() +} + +func (f *FundsType) parse(data string) (int, error) { + + var err error + var size, read int + + code, size, err := util.ReadField(data, read) + if err != nil { + return 0, errors.New("FundsType: unable to parse type code") + } else { + read += size + } + + f.TypeCode = FundsTypeCode(code) + + if f.TypeCode == FundsTypeS { + + f.ImmediateAmount, size, err = util.ReadFieldAsInt(data, read) + if err != nil { + return 0, errors.New("FundsType: unable to parse amount") + } else { + read += size + } + + f.OneDayAmount, size, err = util.ReadFieldAsInt(data, read) + if err != nil { + return 0, errors.New("FundsType: unable to parse amount") + } else { + read += size + } + + f.TwoDayAmount, size, err = util.ReadFieldAsInt(data, read) + if err != nil { + return 0, errors.New("FundsType: unable to parse amount") + } else { + read += size + } + + } else if f.TypeCode == FundsTypeV { + f.Date, size, err = util.ReadField(data, read) + if err != nil { + return 0, errors.New("FundsType: unable to parse date") + } else { + read += size + } + + f.Time, size, err = util.ReadField(data, read) + if err != nil { + return 0, errors.New("FundsType: unable to parse time") + } else { + read += size + } + } else if f.TypeCode == FundsTypeD { + f.DistributionNumber, size, err = util.ReadFieldAsInt(data, read) + if err != nil { + return 0, errors.New("FundsType: unable to parse distribution number") + } else { + read += size + } + + for index := int64(0); index < f.DistributionNumber; index++ { + + var amount, day int64 + + day, size, err = util.ReadFieldAsInt(data, read) + if err != nil { + return 0, errors.New("FundsType: unable to parse day") + } else { + read += size + } + + amount, size, err = util.ReadFieldAsInt(data, read) + if err != nil { + return 0, errors.New("FundsType: unable to parse amount") + } else { + read += size + } + + f.Distributions = append(f.Distributions, Distribution{Day: day, Amount: amount}) + + } + } + + if strings.Contains(data[:read-1], "/") { + return 0, errors.New("FundsType: unable to parse sub elements") + } + + if err = f.Validate(); err != nil { + return 0, err + } + + return read, nil +} + +type Distribution struct { + Day int64 `json:"day,omitempty"` // availability amount + Amount int64 `json:"amount,omitempty"` // availability amount +} + +type FundsTypeCode string + +func (c FundsTypeCode) Validate() error { + + str := string(c) + if len(str) == 0 { + return nil + } + + availableTypes := []string{FundsType0, FundsType1, FundsType2, FundsTypeS, FundsTypeV, FundsTypeD, FundsTypeZ} + + for _, t := range availableTypes { + if strings.ToUpper(str) == t { + return nil + } + } + + return errors.New("invalid fund type") +} diff --git a/pkg/lib/funds_type_test.go b/pkg/lib/funds_type_test.go new file mode 100644 index 00000000..11232003 --- /dev/null +++ b/pkg/lib/funds_type_test.go @@ -0,0 +1,205 @@ +// Copyright 2022 The Moov Authors +// Use of this source code is governed by an Apache License +// license that can be found in the LICENSE file. + +package lib + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func mockFuncType() FundsType { + return FundsType{ + ImmediateAmount: 10000, + OneDayAmount: -20000, + TwoDayAmount: 30000, + Date: "040701", + Time: "1300", + DistributionNumber: 5, + Distributions: []Distribution{ + { + Day: 1, + Amount: 100, + }, + { + Day: 2, + Amount: 200, + }, + { + Day: 3, + Amount: 300, + }, + { + Day: 4, + Amount: 400, + }, + { + Day: 5, + Amount: 500, + }, + }, + } +} + +func TestFundsType_String(t *testing.T) { + f := mockFuncType() + + require.NoError(t, f.Validate()) + require.Equal(t, "", f.String()) + + f.TypeCode = FundsType0 + require.NoError(t, f.Validate()) + require.Equal(t, "0", f.String()) + + f.TypeCode = FundsType1 + require.NoError(t, f.Validate()) + require.Equal(t, "1", f.String()) + + f.TypeCode = FundsType2 + require.NoError(t, f.Validate()) + require.Equal(t, "2", f.String()) + + f.TypeCode = FundsTypeS + require.NoError(t, f.Validate()) + require.Equal(t, "S,10000,-20000,30000", f.String()) + + f.TypeCode = FundsTypeV + require.NoError(t, f.Validate()) + require.Equal(t, "V,040701,1300", f.String()) + + f.TypeCode = FundsTypeZ + require.NoError(t, f.Validate()) + require.Equal(t, "Z", f.String()) + + f.TypeCode = FundsTypeD + require.NoError(t, f.Validate()) + require.Equal(t, "D,5,1,100,2,200,3,300,4,400,5,500", f.String()) + + f.DistributionNumber = 0 + require.Error(t, f.Validate()) + require.Equal(t, "number of distributions is not match", f.Validate().Error()) + + f.Distributions = nil + require.NoError(t, f.Validate()) + require.Equal(t, "D,0", f.String()) +} + +func TestFundsType_Parse(t *testing.T) { + + type testsample struct { + Input string + IsReadErr bool + IsValidateErr bool + CodeType string + ReadSize int + Output string + } + + samples := []testsample{ + { + Input: ",", + IsReadErr: false, + IsValidateErr: false, + CodeType: "", + ReadSize: 1, + Output: "", + }, + { + Input: "Z,", + IsReadErr: false, + IsValidateErr: false, + CodeType: FundsTypeZ, + ReadSize: 2, + Output: "Z", + }, + { + Input: "0,10000,", + IsReadErr: false, + IsValidateErr: false, + CodeType: FundsType0, + ReadSize: 2, + Output: "0", + }, + { + Input: "1,10000,", + IsReadErr: false, + IsValidateErr: false, + CodeType: FundsType1, + ReadSize: 2, + Output: "1", + }, + { + Input: "2,10000,", + IsReadErr: false, + IsValidateErr: false, + CodeType: FundsType2, + ReadSize: 2, + Output: "2", + }, + { + Input: "S,10000,-20000,30000,", + IsReadErr: false, + IsValidateErr: false, + CodeType: FundsTypeS, + ReadSize: 21, + Output: "S,10000,-20000,30000", + }, + { + Input: "V,040701,1300,", + IsReadErr: false, + IsValidateErr: false, + CodeType: FundsTypeV, + ReadSize: 14, + Output: "V,040701,1300", + }, + { + Input: "D,5,1,10000,2,10000,3,10000,4,10000,5,10000,", + IsReadErr: false, + IsValidateErr: false, + CodeType: FundsTypeD, + ReadSize: 44, + Output: "D,5,1,10000,2,10000,3,10000,4,10000,5,10000", + }, + { + Input: "D,0,1,10000,2,10000,3,10000,4,10000,5,10000,", + IsReadErr: false, + IsValidateErr: false, + CodeType: FundsTypeD, + ReadSize: 4, + Output: "D,0", + }, + { + Input: "D/0,1,10000,2,10000,3,10000,4,10000,5,10000,", + IsReadErr: true, + IsValidateErr: false, + CodeType: FundsTypeD, + ReadSize: 0, + Output: "D,0", + }, + } + + for _, sample := range samples { + f := FundsType{} + + size, err := f.parse(sample.Input) + + if sample.IsReadErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + if sample.IsValidateErr { + require.Error(t, f.Validate()) + } else { + require.NoError(t, f.Validate()) + } + + require.Equal(t, sample.CodeType, string(f.TypeCode)) + require.Equal(t, sample.ReadSize, size) + require.Equal(t, sample.Output, f.String()) + } + +} diff --git a/pkg/lib/group.go b/pkg/lib/group.go index 7bb69273..bbf4ffc6 100644 --- a/pkg/lib/group.go +++ b/pkg/lib/group.go @@ -6,8 +6,8 @@ package lib import ( "bytes" + "errors" "fmt" - "strings" "github.com/moov-io/bai2/pkg/util" ) @@ -115,31 +115,26 @@ func (r *Group) Validate() error { return nil } -func (r *Group) Read(scan Bai2Scanner, input string, lineNum int) (int, string, error) { +func (r *Group) Read(scan *Bai2Scanner, useCurrentLine bool) error { + if scan == nil { + return errors.New("invalid bai2 scanner") + } var err error - - for line := scan.ScanLine(input); line != ""; line = scan.ScanLine(input) { - - input = "" - - // don't expect new line - line = strings.TrimSpace(strings.ReplaceAll(line, "\n", "")) + for line := scan.ScanLine(useCurrentLine); line != ""; line = scan.ScanLine(useCurrentLine) { + useCurrentLine = false // find record code if len(line) < 3 { - lineNum++ continue } switch line[:2] { case util.GroupHeaderCode: - - lineNum++ newRecord := groupHeader{} _, err = newRecord.parse(line) if err != nil { - return lineNum, line, fmt.Errorf("ERROR parsing group header on line %d - %v", lineNum, err) + return fmt.Errorf("ERROR parsing group header on line %d (%v)", scan.GetLineIndex(), err) } r.Receiver = newRecord.Receiver @@ -150,37 +145,32 @@ func (r *Group) Read(scan Bai2Scanner, input string, lineNum int) (int, string, r.CurrencyCode = newRecord.CurrencyCode r.AsOfDateModifier = newRecord.AsOfDateModifier - case util.GroupTrailerCode: + case util.AccountIdentifierCode: + newAccount := NewAccount() + err = newAccount.Read(scan, true) + if err != nil { + return err + } + + r.Accounts = append(r.Accounts, *newAccount) - lineNum++ + case util.GroupTrailerCode: newRecord := groupTrailer{} _, err = newRecord.parse(line) if err != nil { - return lineNum, line, fmt.Errorf("ERROR parsing group trailer on line %d - %v", lineNum, err) + return fmt.Errorf("ERROR parsing group trailer on line %d (%v)", scan.GetLineIndex(), err) } r.GroupControlTotal = newRecord.GroupControlTotal r.NumberOfAccounts = newRecord.NumberOfAccounts r.NumberOfRecords = newRecord.NumberOfRecords - return lineNum, "", nil - - case util.AccountIdentifierCode: - - newAccount := NewAccount() - lineNum, input, err = newAccount.Read(scan, line, lineNum) - if err != nil { - return lineNum, input, err - } - - r.Accounts = append(r.Accounts, *newAccount) + return nil default: - - return lineNum, line, err - + return fmt.Errorf("ERROR parsing file on line %d (unabled to read record type %s)", scan.GetLineIndex(), line[0:2]) } } - return lineNum, "", nil + return nil } diff --git a/pkg/lib/group_test.go b/pkg/lib/group_test.go index bf36f003..20cdd338 100644 --- a/pkg/lib/group_test.go +++ b/pkg/lib/group_test.go @@ -26,11 +26,12 @@ func TestGroupWithSampleData1(t *testing.T) { ` group := Group{} - lineNum, line, err := group.Read(NewBai2Scanner(bytes.NewReader([]byte(raw))), "", 0) + scan := NewBai2Scanner(bytes.NewReader([]byte(raw))) + + err := group.Read(&scan, false) require.NoError(t, err) require.NoError(t, group.Validate()) - require.Equal(t, 10, lineNum) - require.Equal(t, "", line) + require.Equal(t, 10, scan.GetLineIndex()) } func TestGroupWithSampleData2(t *testing.T) { @@ -45,13 +46,16 @@ func TestGroupWithSampleData2(t *testing.T) { 16,409,000000000000500,V,060317,,,,GALERIES RICHELIEU / 88,100,000000000208500,00003,V,060316,,400,000000000208500,00008,V,060316,/ 49,+00000000000446000,9/ +98,+00000000001280000,2,25/ 99,+00000000001280000,1,27/ ` group := Group{} - lineNum, line, err := group.Read(NewBai2Scanner(bytes.NewReader([]byte(raw))), "", 0) + scan := NewBai2Scanner(bytes.NewReader([]byte(raw))) + + err := group.Read(&scan, false) require.NoError(t, err) require.NoError(t, group.Validate()) - require.Equal(t, 9, lineNum) - require.Equal(t, "99,+00000000001280000,1,27/", line) + require.Equal(t, 10, scan.GetLineIndex()) + require.Equal(t, "98,+00000000001280000,2,25/", scan.GetLine()) } diff --git a/pkg/lib/reader.go b/pkg/lib/reader.go index 6a696007..defb08ea 100644 --- a/pkg/lib/reader.go +++ b/pkg/lib/reader.go @@ -7,12 +7,14 @@ package lib import ( "bufio" "io" + "strings" "github.com/moov-io/bai2/pkg/util" ) type Bai2Scanner struct { - scan *bufio.Scanner + scan *bufio.Scanner + index int } func NewBai2Scanner(fd io.Reader) Bai2Scanner { @@ -21,17 +23,33 @@ func NewBai2Scanner(fd io.Reader) Bai2Scanner { return Bai2Scanner{scan: scan} } -func (b *Bai2Scanner) ScanLine(line string) string { +func (b *Bai2Scanner) GetLineIndex() int { + return b.index +} + +func (b *Bai2Scanner) GetLine() string { + return strings.TrimSpace(strings.ReplaceAll(b.scan.Text(), "\n", "")) +} + +// ScanLine returns a line from the underlying reader +// arg[0]: useCurrentLine (if false read a new line) +func (b *Bai2Scanner) ScanLine(arg ...bool) string { + + useCurrentLine := false + if len(arg) > 0 { + useCurrentLine = arg[0] + } - if len(line) > 0 { - return line + if useCurrentLine { + return b.GetLine() } if !b.scan.Scan() { return "" } - return b.scan.Text() + b.index++ + return b.GetLine() } // scanRecord allows Reader to read each segment diff --git a/pkg/lib/record_account_identifier.go b/pkg/lib/record_account_identifier.go index ba3476ed..a2d584ba 100644 --- a/pkg/lib/record_account_identifier.go +++ b/pkg/lib/record_account_identifier.go @@ -16,37 +16,46 @@ const ( aiValidateErrorFmt = "AccountIdentifierCurrent: invalid %s" ) +type AccountSummary struct { + TypeCode string + Amount string + ItemCount int64 + FundsType FundsType +} + type accountIdentifier struct { AccountNumber string - CurrencyCode string `json:",omitempty"` - TypeCode string `json:",omitempty"` - Amount string `json:",omitempty"` - ItemCount int64 `json:",omitempty"` - FundsType string `json:",omitempty"` - Composite []string `json:",omitempty"` + CurrencyCode string + + Summaries []AccountSummary } -func (h *accountIdentifier) validate() error { - if h.AccountNumber == "" { +func (r *accountIdentifier) validate() error { + + if r.AccountNumber == "" { return fmt.Errorf(fmt.Sprintf(aiValidateErrorFmt, "AccountNumber")) } - if h.Amount != "" && !util.ValidateAmount(h.Amount) { - return fmt.Errorf(fmt.Sprintf(aiValidateErrorFmt, "Amount")) - } - if h.CurrencyCode != "" && !util.ValidateCurrencyCode(h.CurrencyCode) { + + if r.CurrencyCode != "" && !util.ValidateCurrencyCode(r.CurrencyCode) { return fmt.Errorf(fmt.Sprintf(aiValidateErrorFmt, "CurrencyCode")) } - if h.TypeCode != "" && !util.ValidateTypeCode(h.TypeCode) { - return fmt.Errorf(fmt.Sprintf(aiValidateErrorFmt, "TypeCode")) - } - if h.FundsType != "" && !util.ValidateFundsType(h.FundsType) { - return fmt.Errorf(fmt.Sprintf(aiValidateErrorFmt, "FundsType")) + + for _, summary := range r.Summaries { + if summary.Amount != "" && !util.ValidateAmount(summary.Amount) { + return fmt.Errorf(fmt.Sprintf(aiValidateErrorFmt, "Amount")) + } + if summary.TypeCode != "" && !util.ValidateTypeCode(summary.TypeCode) { + return fmt.Errorf(fmt.Sprintf(aiValidateErrorFmt, "TypeCode")) + } + if summary.FundsType.Validate() != nil { + return fmt.Errorf(fmt.Sprintf(aiValidateErrorFmt, "FundsType")) + } } return nil } -func (h *accountIdentifier) parse(data string) (int, error) { +func (r *accountIdentifier) parse(data string) (int, error) { var line string var err error @@ -66,104 +75,101 @@ func (h *accountIdentifier) parse(data string) (int, error) { read += 3 // AccountNumber - if h.AccountNumber, size, err = util.ReadField(line, read); err != nil { + if r.AccountNumber, size, err = util.ReadField(line, read); err != nil { return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "AccountNumber")) } else { read += size } // CurrencyCode - if h.CurrencyCode, size, err = util.ReadField(line, read); err != nil { + if r.CurrencyCode, size, err = util.ReadField(line, read); err != nil { return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "CurrencyCode")) } else { read += size } - // TypeCode - if h.TypeCode, size, err = util.ReadField(line, read); err != nil { - return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "TypeCode")) - } else { - read += size - } + for read < len(data) { - // Amount - if h.Amount, size, err = util.ReadField(line, read); err != nil { - return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "Amount")) - } else { - read += size - } + var summary AccountSummary - // ItemCount - if h.ItemCount, size, err = util.ReadFieldAsInt(line, read); err != nil { - return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "ItemCount")) - } else { - read += size - } + // TypeCode + if summary.TypeCode, size, err = util.ReadField(line, read); err != nil { + return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "TypeCode")) + } else { + read += size + } - // FundsType - if h.FundsType, size, err = util.ReadField(line, read); err != nil { - return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "FundsType")) - } else { - read += size - } + // Amount + if summary.Amount, size, err = util.ReadField(line, read); err != nil { + return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "Amount")) + } else { + read += size + } + + // ItemCount + if summary.ItemCount, size, err = util.ReadFieldAsInt(line, read); err != nil { + return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "ItemCount")) + } else { + read += size + } - for int64(read) < length { - var composite string - if composite, size, err = util.ReadField(line, read); err != nil { - return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "Composite")) + if size, err = summary.FundsType.parse(line[read:]); err != nil { + return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "FundsType")) } else { read += size } - h.Composite = append(h.Composite, composite) + + r.Summaries = append(r.Summaries, summary) } - if err = h.validate(); err != nil { + if err = r.validate(); err != nil { return 0, err } return read, nil } -func (h *accountIdentifier) string(opts ...int64) string { - - var totalBuf bytes.Buffer - var buf bytes.Buffer - - buf.WriteString(fmt.Sprintf("%s,", util.AccountIdentifierCode)) - buf.WriteString(fmt.Sprintf("%s,", h.AccountNumber)) - buf.WriteString(fmt.Sprintf("%s,", h.CurrencyCode)) - buf.WriteString(fmt.Sprintf("%s,", h.TypeCode)) - buf.WriteString(fmt.Sprintf("%s,", h.Amount)) - if h.ItemCount > 0 { - buf.WriteString(fmt.Sprintf("%d,", h.ItemCount)) - } else { - buf.WriteString(",") - } - buf.WriteString(h.FundsType) +func (r *accountIdentifier) string(opts ...int64) string { var maxLen int64 if len(opts) > 0 { maxLen = opts[0] } - for _, composite := range h.Composite { - if maxLen > 0 { - if int64(buf.Len()+len(composite)+2) > maxLen { - // refresh buf - buf.WriteString("/" + "\n") // added new line - totalBuf.WriteString(buf.String()) + var total, buf bytes.Buffer + + buf.WriteString(fmt.Sprintf("%s,", util.AccountIdentifierCode)) + buf.WriteString(fmt.Sprintf("%s,", r.AccountNumber)) + buf.WriteString(fmt.Sprintf("%s,", r.CurrencyCode)) + + if len(r.Summaries) == 0 { + buf.WriteString(",,,") + } else { + for index, summary := range r.Summaries { + + util.WriteBuffer(&total, &buf, summary.TypeCode, maxLen) + buf.WriteString(",") + + util.WriteBuffer(&total, &buf, summary.Amount, maxLen) + buf.WriteString(",") - // new buf - buf = bytes.Buffer{} - buf.WriteString(util.ContinuationCode) + if summary.ItemCount == 0 { + buf.WriteString(",") + } else { + util.WriteBuffer(&total, &buf, fmt.Sprintf("%d", summary.ItemCount), maxLen) + buf.WriteString(",") } - } - buf.WriteString(fmt.Sprintf(",%s", composite)) + util.WriteBuffer(&total, &buf, summary.FundsType.String(), maxLen) + + if index < len(r.Summaries)-1 { + buf.WriteString(",") + } + } } buf.WriteString("/") - totalBuf.WriteString(buf.String()) + total.WriteString(buf.String()) - return totalBuf.String() + return total.String() } diff --git a/pkg/lib/record_account_identifier_test.go b/pkg/lib/record_account_identifier_test.go index c836dcf4..1e4c70d9 100644 --- a/pkg/lib/record_account_identifier_test.go +++ b/pkg/lib/record_account_identifier_test.go @@ -38,93 +38,75 @@ func TestAccountIdentifierCurrentWithSample1(t *testing.T) { require.Equal(t, "10200123456", record.AccountNumber) require.Equal(t, "CAD", record.CurrencyCode) - require.Equal(t, "040", record.TypeCode) - require.Equal(t, "+000000000000", record.Amount) - require.Equal(t, "045", record.Composite[0]) - require.Equal(t, "+000000000000", record.Composite[1]) - require.Equal(t, "4", record.Composite[2]) - require.Equal(t, "0", record.Composite[3]) + require.Equal(t, 2, len(record.Summaries)) - require.Equal(t, sample, record.string()) -} - -func TestAccountIdentifierCurrentWithSample2(t *testing.T) { + summary := record.Summaries[0] + require.Equal(t, "040", summary.TypeCode) + require.Equal(t, "+000000000000", summary.Amount) + require.Equal(t, int64(0), summary.ItemCount) + require.Equal(t, "", string(summary.FundsType.TypeCode)) - sample := "03,5765432,,,,,/" - record := accountIdentifier{} + summary = record.Summaries[1] - size, err := record.parse(sample) - require.NoError(t, err) - require.Equal(t, 16, size) - - require.Equal(t, "5765432", record.AccountNumber) + require.Equal(t, "045", summary.TypeCode) + require.Equal(t, "+000000000000", summary.Amount) + require.Equal(t, int64(4), summary.ItemCount) + require.Equal(t, FundsType0, string(summary.FundsType.TypeCode)) require.Equal(t, sample, record.string()) } -func TestAccountIdentifierCurrentWithSample3(t *testing.T) { +func TestAccountIdentifierCurrentWithSample2(t *testing.T) { - sample := "03,5765432,,,,,,/" + sample := "03,5765432,,,,,/" record := accountIdentifier{} size, err := record.parse(sample) require.NoError(t, err) - require.Equal(t, 17, size) + require.Equal(t, 16, size) require.Equal(t, "5765432", record.AccountNumber) - require.Equal(t, 1, len(record.Composite)) + require.Equal(t, 1, len(record.Summaries)) require.Equal(t, sample, record.string()) } -func TestAccountIdentifierCurrentWithSample4(t *testing.T) { - - sample := "03,5765432," - record := accountIdentifier{} - - size, err := record.parse(sample) - require.Equal(t, "AccountIdentifier: unable to parse record", err.Error()) - require.Equal(t, 0, size) - - sample = "03,5765432,/" - size, err = record.parse(sample) - require.Equal(t, "AccountIdentifier: unable to parse TypeCode", err.Error()) - require.Equal(t, 0, size) - -} - func TestAccountIdentifierOutputWithContinuationRecords(t *testing.T) { record := accountIdentifier{ AccountNumber: "10200123456", CurrencyCode: "CAD", - TypeCode: "040", - Amount: "+000000000000", - ItemCount: 10, } for i := 0; i < 10; i++ { - record.Composite = append(record.Composite, "test-composite") + record.Summaries = append(record.Summaries, + AccountSummary{ + TypeCode: "040", + Amount: "+000000000000", + ItemCount: 10, + }) } result := record.string() - expectResult := `03,10200123456,CAD,040,+000000000000,10,,test-composite,test-composite,test-composite,test-composite,test-composite,test-composite,test-composite,test-composite,test-composite,test-composite/` + expectResult := `03,10200123456,CAD,040,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,/` require.Equal(t, expectResult, result) - require.Equal(t, len(expectResult), 191) + require.Equal(t, len(expectResult), len(result)) result = record.string(80) - expectResult = `03,10200123456,CAD,040,+000000000000,10,,test-composite,test-composite/ -88,test-composite,test-composite,test-composite,test-composite,test-composite/ -88,test-composite,test-composite,test-composite/` + expectResult = `03,10200123456,CAD,040,+000000000000,10,,040,+000000000000,10,,040/ +88,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,,040/ +88,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,,040/ +88,+000000000000,10,,040,+000000000000,10,/` require.Equal(t, expectResult, result) - require.Equal(t, len(expectResult), 199) + require.Equal(t, len(expectResult), len(result)) result = record.string(50) - expectResult = `03,10200123456,CAD,040,+000000000000,10,/ -88,test-composite,test-composite,test-composite/ -88,test-composite,test-composite,test-composite/ -88,test-composite,test-composite,test-composite/ -88,test-composite/` + expectResult = `03,10200123456,CAD,040,+000000000000,10,,040/ +88,+000000000000,10,,040,+000000000000,10,,040/ +88,+000000000000,10,,040,+000000000000,10,,040/ +88,+000000000000,10,,040,+000000000000,10,,040/ +88,+000000000000,10,,040,+000000000000,10,,040/ +88,+000000000000,10,/` require.Equal(t, expectResult, result) - require.Equal(t, len(expectResult), 207) + require.Equal(t, len(expectResult), len(result)) } diff --git a/pkg/lib/record_transaction_detail.go b/pkg/lib/record_transaction_detail.go index 211c10d4..364ab733 100644 --- a/pkg/lib/record_transaction_detail.go +++ b/pkg/lib/record_transaction_detail.go @@ -12,37 +12,34 @@ import ( ) const ( - tdParseErrorFmt = "AccountTransaction: unable to parse %s" - tdValidateErrorFmt = "AccountTransaction: invalid %s" + tdParseErrorFmt = "TransactionDetail: unable to parse %s" + tdValidateErrorFmt = "TransactionDetail: invalid %s" ) -// Creating new transaction detail -func NewTransactionDetail() *TransactionDetail { - return &TransactionDetail{} +type transactionDetail struct { + TypeCode string + Amount string + FundsType FundsType + BankReferenceNumber string + CustomerReferenceNumber string + Text string } -type TransactionDetail struct { - TypeCode string `json:",omitempty"` - Amount string `json:",omitempty"` - FundsType string `json:",omitempty"` - Composite []string `json:",omitempty"` -} - -func (h *TransactionDetail) validate() error { - if h.TypeCode != "" && !util.ValidateTypeCode(h.TypeCode) { +func (r *transactionDetail) validate() error { + if r.TypeCode != "" && !util.ValidateTypeCode(r.TypeCode) { return fmt.Errorf(fmt.Sprintf(tdValidateErrorFmt, "TypeCode")) } - if h.Amount != "" && !util.ValidateAmount(h.Amount) { + if r.Amount != "" && !util.ValidateAmount(r.Amount) { return fmt.Errorf(fmt.Sprintf(tdValidateErrorFmt, "Amount")) } - if h.FundsType != "" && !util.ValidateFundsType(h.FundsType) { + if r.FundsType.Validate() != nil { return fmt.Errorf(fmt.Sprintf(tdValidateErrorFmt, "FundsType")) } return nil } -func (h *TransactionDetail) parse(data string) (int, error) { +func (r *transactionDetail) parse(data string) (int, error) { var line string var err error @@ -57,81 +54,85 @@ func (h *TransactionDetail) parse(data string) (int, error) { // RecordCode if util.TransactionDetailCode != data[:2] { - return 0, fmt.Errorf(fmt.Sprintf(fhParseErrorFmt, "RecordCode")) + return 0, fmt.Errorf(fmt.Sprintf(tdParseErrorFmt, "RecordCode")) } read += 3 // TypeCode - if h.TypeCode, size, err = util.ReadField(line, read); err != nil { + if r.TypeCode, size, err = util.ReadField(line, read); err != nil { return 0, fmt.Errorf(fmt.Sprintf(tdParseErrorFmt, "TypeCode")) } else { read += size } // Amount - if h.Amount, size, err = util.ReadField(line, read); err != nil { + if r.Amount, size, err = util.ReadField(line, read); err != nil { return 0, fmt.Errorf(fmt.Sprintf(tdParseErrorFmt, "Amount")) } else { read += size } // FundsType - if h.FundsType, size, err = util.ReadField(line, read); err != nil { + if size, err = r.FundsType.parse(line[read:]); err != nil { return 0, fmt.Errorf(fmt.Sprintf(tdParseErrorFmt, "FundsType")) } else { read += size } - for int64(read) < length { - var composite string - if composite, size, err = util.ReadField(line, read); err != nil { - return 0, fmt.Errorf(fmt.Sprintf(tdParseErrorFmt, "Composite")) - } else { - read += size - } - h.Composite = append(h.Composite, composite) + // BankReferenceNumber + if r.BankReferenceNumber, size, err = util.ReadField(line, read); err != nil { + return 0, fmt.Errorf(fmt.Sprintf(tdParseErrorFmt, "BankReferenceNumber")) + } else { + read += size + } + + // CustomerReferenceNumber + if r.CustomerReferenceNumber, size, err = util.ReadField(line, read); err != nil { + return 0, fmt.Errorf(fmt.Sprintf(tdParseErrorFmt, "CustomerReferenceNumber")) + } else { + read += size + } + + // Text + if r.Text, size, err = util.ReadField(line, read); err != nil { + return 0, fmt.Errorf(fmt.Sprintf(tdParseErrorFmt, "Text")) + } else { + read += size } - if err = h.validate(); err != nil { + if err = r.validate(); err != nil { return 0, err } return read, nil } -func (h *TransactionDetail) string(opts ...int64) string { - - var totalBuf bytes.Buffer - var buf bytes.Buffer - - buf.WriteString(fmt.Sprintf("%s,", util.TransactionDetailCode)) - buf.WriteString(fmt.Sprintf("%s,", h.TypeCode)) - buf.WriteString(fmt.Sprintf("%s,", h.Amount)) - buf.WriteString(h.FundsType) +func (r *transactionDetail) string(opts ...int64) string { var maxLen int64 if len(opts) > 0 { maxLen = opts[0] } - for _, composite := range h.Composite { - if maxLen > 0 { - if int64(buf.Len()+len(composite)+2) > maxLen { - // refresh buf - buf.WriteString("/" + "\n") // added new line - totalBuf.WriteString(buf.String()) + var total, buf bytes.Buffer - // new buf - buf = bytes.Buffer{} - buf.WriteString(util.ContinuationCode) - } - } + buf.WriteString(fmt.Sprintf("%s,", util.TransactionDetailCode)) + buf.WriteString(fmt.Sprintf("%s,", r.TypeCode)) + buf.WriteString(fmt.Sprintf("%s,", r.Amount)) - buf.WriteString(fmt.Sprintf(",%s", composite)) - } + util.WriteBuffer(&total, &buf, r.FundsType.String(), maxLen) + buf.WriteString(",") + + util.WriteBuffer(&total, &buf, r.BankReferenceNumber, maxLen) + buf.WriteString(",") + util.WriteBuffer(&total, &buf, r.CustomerReferenceNumber, maxLen) + buf.WriteString(",") + + util.WriteBuffer(&total, &buf, r.Text, maxLen) buf.WriteString("/") - totalBuf.WriteString(buf.String()) - return totalBuf.String() + total.WriteString(buf.String()) + + return total.String() } diff --git a/pkg/lib/record_transaction_detail_test.go b/pkg/lib/record_transaction_detail_test.go index 7967f44d..6bef05b6 100644 --- a/pkg/lib/record_transaction_detail_test.go +++ b/pkg/lib/record_transaction_detail_test.go @@ -12,21 +12,23 @@ import ( func TestTransactionDetail(t *testing.T) { - record := TransactionDetail{ + record := transactionDetail{ TypeCode: "890", } require.NoError(t, record.validate()) record.TypeCode = "AAA" require.Error(t, record.validate()) - require.Equal(t, "AccountTransaction: invalid TypeCode", record.validate().Error()) + require.Equal(t, "TransactionDetail: invalid TypeCode", record.validate().Error()) } func TestTransactionDetailWithSample(t *testing.T) { sample := "16,409,000000000002500,V,060316,,,,RETURNED CHEQUE /" - record := NewTransactionDetail() + record := transactionDetail{ + TypeCode: "890", + } size, err := record.parse(sample) require.NoError(t, err) @@ -34,43 +36,79 @@ func TestTransactionDetailWithSample(t *testing.T) { require.Equal(t, "409", record.TypeCode) require.Equal(t, "000000000002500", record.Amount) - require.Equal(t, "V", record.FundsType) - require.Equal(t, 5, len(record.Composite)) - require.Equal(t, "060316", record.Composite[0]) - require.Equal(t, "RETURNED CHEQUE ", record.Composite[4]) + require.Equal(t, "V", string(record.FundsType.TypeCode)) + require.Equal(t, "060316", record.FundsType.Date) + require.Equal(t, "", record.FundsType.Time) + require.Equal(t, "", record.BankReferenceNumber) + require.Equal(t, "", record.CustomerReferenceNumber) + require.Equal(t, "RETURNED CHEQUE ", record.Text) require.Equal(t, sample, record.string()) } func TestTransactionDetailOutputWithContinuationRecords(t *testing.T) { - record := TransactionDetail{ - TypeCode: "409", - Amount: "000000000002500", - FundsType: "V", - } - - for i := 0; i < 10; i++ { - record.Composite = append(record.Composite, "test-composite") + record := transactionDetail{ + TypeCode: "409", + Amount: "111111111111111", + BankReferenceNumber: "222222222222222", + CustomerReferenceNumber: "333333333333333", + Text: "RETURNED CHEQUE 444444444444444", + FundsType: FundsType{ + TypeCode: FundsTypeD, + DistributionNumber: 5, + Distributions: []Distribution{ + { + Day: 1, + Amount: 1000000000, + }, + { + Day: 2, + Amount: 2000000000, + }, + { + Day: 3, + Amount: 3000000000, + }, + { + Day: 4, + Amount: 4000000000, + }, + { + Day: 5, + Amount: 5000000000, + }, + { + Day: 6, + Amount: 6000000000, + }, + { + Day: 7, + Amount: 7000000000, + }, + }, + }, } result := record.string() - expectResult := `16,409,000000000002500,V,test-composite,test-composite,test-composite,test-composite,test-composite,test-composite,test-composite,test-composite,test-composite,test-composite/` + expectResult := `16,409,111111111111111,D,5,1,1000000000,2,2000000000,3,3000000000,4,4000000000,5,5000000000,6,6000000000,7,7000000000,222222222222222,333333333333333,RETURNED CHEQUE 444444444444444/` require.Equal(t, expectResult, result) - require.Equal(t, len(expectResult), 175) + require.Equal(t, len(expectResult), len(result)) result = record.string(80) - expectResult = `16,409,000000000002500,V,test-composite,test-composite,test-composite/ -88,test-composite,test-composite,test-composite,test-composite,test-composite/ -88,test-composite,test-composite/` + expectResult = `16,409,111111111111111,D,5,1,1000000000,2,2000000000,3,3000000000,4,4000000000/ +88,5,5000000000,6,6000000000,7,7000000000,222222222222222,333333333333333/ +88,RETURNED CHEQUE 444444444444444/` require.Equal(t, expectResult, result) - require.Equal(t, len(expectResult), 183) + require.Equal(t, len(expectResult), len(result)) result = record.string(50) - expectResult = `16,409,000000000002500,V,test-composite/ -88,test-composite,test-composite,test-composite/ -88,test-composite,test-composite,test-composite/ -88,test-composite,test-composite,test-composite/` + expectResult = `16,409,111111111111111,D,5,1,1000000000,2/ +88,2000000000,3,3000000000,4,4000000000,5/ +88,5000000000,6,6000000000,7,7000000000/ +88,222222222222222,333333333333333/ +88,RETURNED CHEQUE 444444444444444/` require.Equal(t, expectResult, result) - require.Equal(t, len(expectResult), 187) + require.Equal(t, len(expectResult), len(result)) + } diff --git a/pkg/service/handlers.go b/pkg/service/handlers.go index 10e9358b..9537a74b 100644 --- a/pkg/service/handlers.go +++ b/pkg/service/handlers.go @@ -43,8 +43,9 @@ func parseInputFromRequest(r *http.Request) (*lib.Bai2, error) { } // convert byte slice to io.Reader + scan := lib.NewBai2Scanner(bytes.NewReader(input.Bytes())) f := lib.NewBai2() - err = f.Read(lib.NewBai2Scanner(bytes.NewReader(input.Bytes()))) + err = f.Read(&scan) if err != nil { return nil, err } diff --git a/pkg/util/const.go b/pkg/util/const.go index a5c13239..239de71e 100644 --- a/pkg/util/const.go +++ b/pkg/util/const.go @@ -1,3 +1,7 @@ +// Copyright 2022 The Moov Authors +// Use of this source code is governed by an Apache License +// license that can be found in the LICENSE file. + package util const ( diff --git a/pkg/util/parser.go b/pkg/util/parser.go index 6dd870de..320899f4 100644 --- a/pkg/util/parser.go +++ b/pkg/util/parser.go @@ -11,12 +11,18 @@ import ( ) func getIndex(input string) int { + idx1 := strings.Index(input, ",") idx2 := strings.Index(input, "/") if idx1 == -1 { return idx2 } + + if idx2 > -1 && idx2 < idx1 { + return idx2 + } + return idx1 } diff --git a/pkg/util/validate.go b/pkg/util/validate.go index 141ef4a9..98e6640f 100644 --- a/pkg/util/validate.go +++ b/pkg/util/validate.go @@ -1,3 +1,7 @@ +// Copyright 2022 The Moov Authors +// Use of this source code is governed by an Apache License +// license that can be found in the LICENSE file. + package util import "regexp" diff --git a/pkg/util/write.go b/pkg/util/write.go new file mode 100644 index 00000000..871c104d --- /dev/null +++ b/pkg/util/write.go @@ -0,0 +1,57 @@ +// Copyright 2022 The Moov Authors +// Use of this source code is governed by an Apache License +// license that can be found in the LICENSE file. + +package util + +import ( + "bytes" + "fmt" + "strings" +) + +// WriteBuffer +// +// Input type (ELM1,EML2,ELM3..,ELMN) +func WriteBuffer(total, buf *bytes.Buffer, input string, maxLen int64) { + + if maxLen > 0 { + + elements := strings.Split(input, ",") + newInput := "" + + for _, elm := range elements { + + newSize := int64(buf.Len() + len(newInput) + len(elm) + 2) + if newSize > maxLen { + if newInput == "" { + org := buf.String() + org = org[:len(org)-1] + "/" + "\n" + total.WriteString(org) + } else { + buf.WriteString(newInput + "/" + "\n") // added new line + total.WriteString(buf.String()) + } + + // refresh buf + buf.Reset() + + buf.WriteString(fmt.Sprintf("%s,", ContinuationCode)) + newInput = elm + } else { + if newInput == "" { + newInput = elm + } else { + newInput = newInput + "," + elm + } + } + } + + if len(newInput) > 0 { + buf.WriteString(newInput) + } + + } else { + buf.WriteString(input) + } +} diff --git a/test/fuzz-reader/reader.go b/test/fuzz-reader/reader.go index 2e57bf4d..f185fa7c 100644 --- a/test/fuzz-reader/reader.go +++ b/test/fuzz-reader/reader.go @@ -18,8 +18,9 @@ import ( // reserved for future use. func Fuzz(data []byte) int { + scan := lib.NewBai2Scanner(bytes.NewReader(data)) f := lib.NewBai2() - err := f.Read(lib.NewBai2Scanner(bytes.NewReader(data))) + err := f.Read(&scan) if err != nil { return 0 } diff --git a/test/testdata/sample3.txt b/test/testdata/sample3.txt index 8cb6ead7..aed41384 100644 --- a/test/testdata/sample3.txt +++ b/test/testdata/sample3.txt @@ -1,5 +1,5 @@ 01,103100195,103100195,220919,2112,4,,,2/ 02,103100195,103100195,1,220919,2400,USD,2/ -03,1111111,,010,-3500,,,015,-3600,,,040,-3500,,,060,-3600,,,100,3100,3,,400,3200,3,,/ +03,1111111,,010,-3500,,,015,-3600,,,040,-3500,,,060,-3600,,,100,3100,3,,400,3200,3,/ 16,142,2500,Z,,,TRANSFER PAYPAL PPD/ 16,142,500,Z,,,TRANSFER MSPBNA BANK PPD/ 16,142,100,Z,,,Ext Trnsfr JPMorgan Chase PPD/ 16,451,2500,Z,,,111111 ACH_SETL 1111111111 111111111111111 / @@ -7,14 +7,14 @@ 16,451,600,Z,,,111111 ACH_SETL 1111111111 111111111111111 / 88, 1111111111/ 16,451,100,Z,,,Ext Trnsfr JPMorgan Chase 1111111111 111111111111111 / -88, 11111111111/ 49,-1600,11/ 03,1111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,,/ -49,00,2/ 03,1111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,,/ 49,00,2/ -03,1111111,,010,-55703,,,015,-86406,,,040,-55703,,,060,-86406,,,100,00,0,,400,30703,2,,/ +88, 11111111111/ 49,-1600,11/ 03,1111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,/ +49,00,2/ 03,1111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,/ 49,00,2/ +03,1111111,,010,-55703,,,015,-86406,,,040,-55703,,,060,-86406,,,100,00,0,,400,30703,2,/ 16,451,27500,Z,,,Visa Billing FTSRE Sept 22/ 16,451,3203,Z,,,Visa Billing Sept 22/ 49,-222812,4/ -03,1111111,,010,98547,,,015,98547,,,040,98547,,,060,98547,,,100,00,0,,400,00,0,,/ 49,394188,2/ -03,1111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,,/ 49,00,2/ -03,1111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,,/ 49,00,2/ -03,11111111,,010,625266,,,015,610315,,,040,625266,,,060,610315,,,100,3700,3,,400,18651,3,,/ +03,1111111,,010,98547,,,015,98547,,,040,98547,,,060,98547,,,100,00,0,,400,00,0,/ 49,394188,2/ +03,1111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,/ 49,00,2/ +03,1111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,/ 49,00,2/ +03,11111111,,010,625266,,,015,610315,,,040,625266,,,060,610315,,,100,3700,3,,400,18651,3,/ 16,142,2500,Z,,,111111 ACH_SETL 1111111111 111111111111111 / 88, 1111111111/ @@ -29,13 +29,13 @@ 16,451,2347,Z,,,111111 BII_SETL 1111111111 111111111111111 / 88, 1111111111/ 49,2515864,14/ -03,11111111,,010,-73135,,,015,-78759,,,040,-73135,,,060,-78759,,,100,320,2,,400,5944,2,,/ +03,11111111,,010,-73135,,,015,-78759,,,040,-73135,,,060,-78759,,,100,320,2,,400,5944,2,/ 16,142,215,Z,,,1111111111 CHIME HUBBLE PPD/ 16,142,105,Z,,,1111111111 CHIME HUBBLE PPD/ 16,451,5857,Z,,,Visa Billing Sept 22/ 16,451,87,Z,,,1111111111 CHIME HUBBLE PPD/ 49,-291260,6/ -03,11111111,,010,-4078,,,015,27595,,,040,-4078,,,060,27595,,,100,32593,5,,400,920,3,,/ +03,11111111,,010,-4078,,,015,27595,,,040,-4078,,,060,27595,,,100,32593,5,,400,920,3,/ 16,195,13855,Z,,,Wire Transfer Credit VISA INTERNATIONAL 900 METRO CENTER BLVD / 88, FOSTER CITY CA 94404/ 16,142,11521,Z,,,111111 BII_SETL 1111111111 111111111111111 / @@ -50,15 +50,15 @@ 16,451,215,Z,,,HBLE091722 CHIME HUBBLE PPD/ 16,451,105,Z,,,HBLE091622 CHIME HUBBLE PPD/ 49,114060,15/ -03,11111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,,/ +03,11111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,/ 49,00,2/ -03,11111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,,/ +03,11111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,/ 49,00,2/ -03,11111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,,/ +03,11111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,/ 49,00,2/ -03,11111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,,/ +03,11111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,/ 49,00,2/ -03,11111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,,/ +03,11111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,/ 49,00,2/ 98,2508440,15,72/ 99,2508440,1,74/ \ No newline at end of file