Skip to content

Commit

Permalink
feat: add Addenda98Refused type
Browse files Browse the repository at this point in the history
Fixes: #1235
  • Loading branch information
adamdecaf committed Jul 18, 2023
1 parent c0a92ac commit bc8fc6a
Show file tree
Hide file tree
Showing 11 changed files with 359 additions and 14 deletions.
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

0 comments on commit bc8fc6a

Please sign in to comment.