Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Addenda98Refused type #1263

Merged
merged 1 commit into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion addenda98.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ type Addenda98 struct {
OriginalTrace string `json:"originalTrace"`
// OriginalDFI field contains the Receiving DFI Identification (addenda.RDFIIdentification) as originally included on the forward Entry or Prenotification that the RDFI is returning or correcting.
OriginalDFI string `json:"originalDFI"`
// CorrectedData
// CorrectedData is the corrected data
CorrectedData string `json:"correctedData"`
// TraceNumber matches the Entry Detail Trace Number of the entry being returned.
//
Expand Down Expand Up @@ -215,6 +215,14 @@ func makeChangeCodeDict() map[string]*ChangeCode {
return dict
}

func IsRefusedChangeCode(code string) bool {
switch strings.ToUpper(code) {
case "C61", "C62", "C63", "C64", "C65", "C66", "C67", "C68", "C69":
return true
}
return false
}

// CorrectedData is a struct returned from our helper method for parsing the NOC/COR
// corrected data from Addenda98 records.
//
Expand Down
189 changes: 189 additions & 0 deletions addenda98_refused.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// Licensed to The Moov Authors under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. The Moov Authors licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package ach

import (
"strings"
"unicode/utf8"
)

type Addenda98Refused struct {
// ID is a client defined string used as a reference to this record.
ID string `json:"id"`

// TypeCode Addenda types code '98'
TypeCode string `json:"typeCode"`

// RefusedChangeCode is the code specifying why the Notification of Change is being refused.
RefusedChangeCode string `json:"refusedChangeCode"`

// OriginalTrace This field contains the Trace Number as originally included on the forward Entry or Prenotification.
// The RDFI must include the Original Entry Trace Number in the Addenda Record of an Entry being returned to an ODFI,
// in the Addenda Record of an 98, within an Acknowledgment Entry, or with an RDFI request for a copy of an authorization.
OriginalTrace string `json:"originalTrace"`

// OriginalDFI field contains the Receiving DFI Identification (addenda.RDFIIdentification) as originally included on the
// forward Entry or Prenotification that the RDFI is returning or correcting.
OriginalDFI string `json:"originalDFI"`

// CorrectedData is the corrected data
CorrectedData string `json:"correctedData"`

// ChangeCode field contains a standard code used by an ACH Operator or RDFI to describe the reason for a change Entry.
ChangeCode string `json:"changeCode"`

// TraceSequenceNumber is the last seven digits of the TraceNumber in the original Notification of Change
TraceSequenceNumber string `json:"traceSequenceNumber"`

// TraceNumber matches the Entry Detail Trace Number of the entry being returned.
//
// Use TraceNumberField for a properly formatted string representation.
TraceNumber string `json:"traceNumber"`

// validator is composed for data validation
validator
// converters is composed for ACH to GoLang Converters
converters
}

// NewAddenda98Refused returns an reference to an instantiated Addenda98Refused with default values
func NewAddenda98Refused() *Addenda98Refused {
addenda98Refused := &Addenda98Refused{
TypeCode: "98",
}
return addenda98Refused
}

// Parse takes the input record string and parses the Addenda98Refused values
//
// Parse provides no guarantee about all fields being filled in. Callers should make a Validate call to confirm successful parsing and data validity.
func (addenda98Refused *Addenda98Refused) Parse(record string) {
if utf8.RuneCountInString(record) != 94 {
return
}

// 1-1 Always 7
// 2-3 Always "98"
addenda98Refused.TypeCode = strings.TrimSpace(record[1:3])
addenda98Refused.RefusedChangeCode = strings.TrimSpace(record[3:6])
addenda98Refused.OriginalTrace = strings.TrimSpace(record[6:21])
// Positions 22-27 are Reserved
addenda98Refused.OriginalDFI = addenda98Refused.parseStringField(record[27:35])
addenda98Refused.CorrectedData = strings.TrimSpace(record[35:64])
addenda98Refused.ChangeCode = strings.TrimSpace(record[64:67])
addenda98Refused.TraceSequenceNumber = strings.TrimSpace(record[67:74])
// Positions 75-79 are Reserved
addenda98Refused.TraceNumber = strings.TrimSpace(record[79:94])
}

// String writes the Addenda98 struct to a 94 character string
func (addenda98Refused *Addenda98Refused) String() string {
if addenda98Refused == nil {
return ""
}

var buf strings.Builder
buf.Grow(94)
buf.WriteString(entryAddendaPos)
buf.WriteString(addenda98Refused.TypeCode)
buf.WriteString(addenda98Refused.RefusedChangeCode)
buf.WriteString(addenda98Refused.OriginalTraceField())
buf.WriteString(strings.Repeat(" ", 6))
buf.WriteString(addenda98Refused.OriginalDFIField())
buf.WriteString(addenda98Refused.CorrectedDataField())
buf.WriteString(addenda98Refused.ChangeCode)
buf.WriteString(addenda98Refused.TraceSequenceNumberField())
buf.WriteString(strings.Repeat(" ", 5))
buf.WriteString(addenda98Refused.TraceNumberField())
return buf.String()
}

// Validate verifies NACHA rules for Addenda98
func (addenda98Refused *Addenda98Refused) Validate() error {
if addenda98Refused.TypeCode == "" {
return fieldError("TypeCode", ErrConstructor, addenda98Refused.TypeCode)
}
// Type Code must be 98
if addenda98Refused.TypeCode != "98" {
return fieldError("TypeCode", ErrAddendaTypeCode, addenda98Refused.TypeCode)
}

// RefusedChangeCode must be valid
_, ok := changeCodeDict[addenda98Refused.RefusedChangeCode]
if !ok {
return fieldError("RefusedChangeCode", ErrAddenda98RefusedChangeCode, addenda98Refused.RefusedChangeCode)
}

// Addenda98 Record must contain the corrected information corresponding to the Change Code used
if addenda98Refused.CorrectedData == "" {
return fieldError("CorrectedData", ErrAddenda98CorrectedData, addenda98Refused.CorrectedData)
}

// ChangeCode must be valid
_, ok = changeCodeDict[addenda98Refused.ChangeCode]
if !ok {
return fieldError("ChangeCode", ErrAddenda98ChangeCode, addenda98Refused.ChangeCode)
}

// TraceSequenceNumber must be valid
if addenda98Refused.TraceSequenceNumber == "" {
return fieldError("TraceSequenceNumber", ErrAddenda98RefusedTraceSequenceNumber, addenda98Refused.TraceSequenceNumber)
}

return nil
}

func (addenda98Refused *Addenda98Refused) RefusedChangeCodeField() *ChangeCode {
code, ok := changeCodeDict[addenda98Refused.RefusedChangeCode]
if ok {
return code
}
return nil
}

// OriginalTraceField returns a zero padded OriginalTrace string
func (addenda98Refused *Addenda98Refused) OriginalTraceField() string {
return addenda98Refused.stringField(addenda98Refused.OriginalTrace, 15)
}

// OriginalDFIField returns a zero padded OriginalDFI string
func (addenda98Refused *Addenda98Refused) OriginalDFIField() string {
return addenda98Refused.stringField(addenda98Refused.OriginalDFI, 8)
}

// CorrectedDataField returns a space padded CorrectedData string
func (addenda98Refused *Addenda98Refused) CorrectedDataField() string {
return addenda98Refused.alphaField(addenda98Refused.CorrectedData, 29)
}

func (addenda98Refused *Addenda98Refused) ChangeCodeField() *ChangeCode {
code, ok := changeCodeDict[addenda98Refused.ChangeCode]
if ok {
return code
}
return nil
}

func (addenda98Refused *Addenda98Refused) TraceSequenceNumberField() string {
return addenda98Refused.stringField(addenda98Refused.TraceSequenceNumber, 7)
}

// TraceNumberField returns a zero padded traceNumber string
func (addenda98Refused *Addenda98Refused) TraceNumberField() string {
return addenda98Refused.stringField(addenda98Refused.TraceNumber, 15)
}
100 changes: 100 additions & 0 deletions addenda98_refused_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Licensed to The Moov Authors under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. The Moov Authors licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package ach

import (
"bytes"
"testing"

"github.com/stretchr/testify/require"
)

func mockAddenda98Refused() *Addenda98Refused {
add := NewAddenda98Refused()
add.RefusedChangeCode = "C62"
add.OriginalTrace = "059999990000003"
add.OriginalDFI = "05999999"
add.CorrectedData = "68-6547"
add.ChangeCode = "C01"
add.TraceSequenceNumber = "0000002"
add.TraceNumber = "059999990000001"
return add
}

func TestAddenda98Refused_Fields(t *testing.T) {
add := mockAddenda98Refused()

// shorten some fields
add.OriginalTrace = "059993"
add.TraceNumber = "000123"

require.Equal(t, "C62", add.RefusedChangeCodeField().Code)
require.Equal(t, "000000000059993", add.OriginalTraceField())
require.Equal(t, "05999999", add.OriginalDFIField())
require.Equal(t, "68-6547 ", add.CorrectedDataField())
require.Equal(t, "C01", add.ChangeCodeField().Code)
require.Equal(t, "0000002", add.TraceSequenceNumberField())
require.Equal(t, "000000000000123", add.TraceNumberField())
}

func TestAddenda98Refused_Read(t *testing.T) {
original := mockAddenda98Refused()

read := &Addenda98Refused{}
read.Parse(original.String())

require.Equal(t, "C62", read.RefusedChangeCodeField().Code)
require.Equal(t, "059999990000003", read.OriginalTraceField())
require.Equal(t, "05999999", read.OriginalDFIField())
require.Equal(t, "68-6547 ", read.CorrectedDataField())
require.Equal(t, "C01", read.ChangeCodeField().Code)
require.Equal(t, "0000002", read.TraceSequenceNumberField())
require.Equal(t, "059999990000001", read.TraceNumberField())
}

func TestAddenda98Refused_File(t *testing.T) {
file := NewFile()
file.Header = mockFileHeader()

b := mockBatchCOR(t)
b.Entries[0].AddendaRecordIndicator = 1
b.Entries[0].Addenda98Refused = mockAddenda98Refused()
b.Entries[0].TraceNumber = "121042880000002"
require.NoError(t, b.Create())
file.AddBatch(b)
require.NoError(t, file.Create())

var buf bytes.Buffer
err := NewWriter(&buf).Write(file)
require.NoError(t, err)
require.Contains(t, buf.String(), "C62")

read, err := NewReader(&buf).Read()
require.NoError(t, err)

require.Len(t, read.Batches, 1)
entries := read.Batches[0].GetEntries()
require.Len(t, entries, 1)

ed := entries[0]
require.NotNil(t, ed.Addenda98)
require.Equal(t, "C01", ed.Addenda98.ChangeCode)
require.NotNil(t, ed.Addenda98Refused)
require.Equal(t, "C62", ed.Addenda98Refused.RefusedChangeCode)
require.Equal(t, "C01", ed.Addenda98Refused.ChangeCode)
}
18 changes: 14 additions & 4 deletions batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,11 @@ func (batch *Batch) isFieldInclusion() error {
return err
}
}
if entry.Addenda98Refused != nil {
if err := entry.Addenda98Refused.Validate(); err != nil {
return err
}
}
if entry.Addenda99 != nil {
if err := entry.Addenda99.Validate(); err != nil {
return err
Expand Down Expand Up @@ -857,6 +862,11 @@ func (batch *Batch) isAddendaSequence() error {
return batch.Error("AddendaRecordIndicator", ErrBatchAddendaIndicator)
}
}
if entry.Addenda98Refused != nil {
if entry.AddendaRecordIndicator != 1 {
return batch.Error("AddendaRecordIndicator", ErrBatchAddendaIndicator)
}
}
if entry.Addenda99 != nil {
if entry.AddendaRecordIndicator != 1 {
return batch.Error("AddendaRecordIndicator", ErrBatchAddendaIndicator)
Expand Down Expand Up @@ -956,7 +966,7 @@ func (batch *Batch) addendaFieldInclusionForward(entry *EntryDetail) error {
}
}
if batch.Header.StandardEntryClassCode != COR {
if entry.Addenda98 != nil {
if entry.Addenda98 != nil || entry.Addenda98Refused != nil {
return batch.Error("Addenda98", ErrBatchAddendaCategory, entry.Category)
}
}
Expand All @@ -975,7 +985,7 @@ func (batch *Batch) addendaFieldInclusionNOC(entry *EntryDetail) error {
return batch.Error("Addenda05", ErrBatchAddendaCategory, entry.Category)
}
if batch.Header.StandardEntryClassCode != COR {
if entry.Addenda98 != nil {
if entry.Addenda98 != nil || entry.Addenda98Refused != nil {
return batch.Error("Addenda98", ErrFieldInclusion)
}
}
Expand All @@ -998,7 +1008,7 @@ func (batch *Batch) addendaFieldInclusionReturn(entry *EntryDetail) error {
return batch.Error("Addenda05", ErrBatchAddendaCategory, entry.Category)
}
}
if entry.Addenda98 != nil {
if entry.Addenda98 != nil || entry.Addenda98Refused != nil {
return batch.Error("Addenda98", ErrBatchAddendaCategory, entry.Category)
}
if entry.Addenda99 == nil && entry.Addenda99Dishonored == nil && entry.Addenda99Contested == nil {
Expand All @@ -1024,7 +1034,7 @@ func (batch *Batch) ValidAmountForCodes(entry *EntryDetail) error {
if batch.validateOpts != nil && batch.validateOpts.AllowInvalidAmounts {
return nil
}
if entry != nil && entry.Addenda98 != nil {
if entry != nil && (entry.Addenda98 != nil || entry.Addenda98Refused != nil) {
// NOC entries will have a zero'd amount value
if entry.Amount != 0 {
return ErrBatchAmountNonZero
Expand Down
2 changes: 1 addition & 1 deletion batchCOR.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func (batch *BatchCOR) Create() error {
// isAddenda98 verifies that a Addenda98 exists for each EntryDetail and is Validated
func (batch *BatchCOR) isAddenda98() error {
for _, entry := range batch.Entries {
if entry.Addenda98 == nil {
if entry.Addenda98 == nil && entry.Addenda98Refused == nil {
return batch.Error("Addenda98", ErrBatchCORAddenda)
}
}
Expand Down
Loading
Loading