Skip to content

Commit

Permalink
add support for delayed responses
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonbornsteinMOOV committed Jul 31, 2023
1 parent eafdabd commit e89f3e9
Show file tree
Hide file tree
Showing 8 changed files with 71 additions and 131 deletions.
24 changes: 4 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,9 @@ ACHTestHarness:
accountNumber: "12345678"
traceNumber: "121042880000001"
action:
delay: "12h"
return:
code: "R03"

- match:
amount:
value: 12357 # $123.57
future:
delay: "12h"
return:
code: "R10"
```
The full config for Responses is below:
Expand All @@ -104,22 +97,13 @@ match:
traceNumber: <string> # Exact match of TraceNumber
entryType: <string> # Checks TransactionCode. Accepted values: credit, debit or prenote.
action:
# How long into the future should we wait before making the correction/return available?
delay: <duration>

# Copy the EntryDetail to another directory
copy:
path: <string> # Filepath on the FTP server

# Send the EntryDetail back with the following ACH change code
correction:
code: <string>
data: <string>

# Send the EntryDetail back with the following ACH return code
return:
code: <string>
future:
# How long into the future should we wait before making the correction/return available?
delay: <duration>

# Send the EntryDetail back with the following ACH change code
correction:
code: <string>
Expand Down
2 changes: 1 addition & 1 deletion pkg/response/batch_mirror.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (bm *batchMirror) saveFiles() error {
if filename, err := bm.filename(); err != nil {
return fmt.Errorf("unable to get filename: %v", err)
} else {
bm.writer.Write(filepath.Join(path, filename), &buf)
bm.writer.Write(filepath.Join(path, filename), &buf, nil)
}
}
return nil
Expand Down
9 changes: 6 additions & 3 deletions pkg/response/file_transformer.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func (ft *FileTransfomer) Transform(file *ach.File) error {
return fmt.Errorf("file transform: header validate: %v", err)
}

var delay *time.Duration // TODO JB: delay should be at the entry-level instead of the file-level
for i := range file.Batches {
mirror := newBatchMirror(ft.Writer, file.Batches[i])
batch, err := ach.NewBatch(file.Batches[i].GetHeader())
Expand All @@ -65,7 +66,7 @@ func (ft *FileTransfomer) Transform(file *ach.File) error {
entries := file.Batches[i].GetEntries()
for j := range entries {
// Check if there's a matching Action and perform it - this might be a future-dated action
action, _ := ft.Matcher.FindAction(entries[j])
action := ft.Matcher.FindAction(entries[j])
if action != nil {
entry, err := ft.Entry.MorphEntry(file.Header, entries[j], *action)
if err != nil {
Expand Down Expand Up @@ -93,7 +94,9 @@ func (ft *FileTransfomer) Transform(file *ach.File) error {
}
}

// TODO JB: do something with the `future` object
if action.Delay != nil {
delay = action.Delay
}
}
}
// Save off the entries as requested
Expand All @@ -115,7 +118,7 @@ func (ft *FileTransfomer) Transform(file *ach.File) error {
}
if err := out.Validate(); err == nil {
filepath := filepath.Join(ft.returnPath, generateFilename(out)) // TODO(adam): need to determine return path
if err := ft.Writer.WriteFile(filepath, out); err != nil {
if err := ft.Writer.WriteFile(filepath, out, delay); err != nil {
return fmt.Errorf("transform write %s: %v", filepath, err)
}
} else {
Expand Down
19 changes: 13 additions & 6 deletions pkg/response/file_transformer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"testing"
"time"

"github.com/moov-io/ach"
"github.com/moov-io/ach-test-harness/pkg/service"
Expand All @@ -20,7 +21,7 @@ func TestFileTransformer__CorrectedPrenote(t *testing.T) {
EntryType: service.EntryTypePrenote,
AccountNumber: "810044964044",
},
Action: &service.Action{
Action: service.Action{
Correction: &service.Correction{
Code: "C01",
Data: "445566778",
Expand Down Expand Up @@ -57,7 +58,7 @@ func TestFileTransformer__ReturnedPrenote(t *testing.T) {
EntryType: service.EntryTypePrenote,
AccountNumber: "810044964044",
},
Action: &service.Action{
Action: service.Action{
Return: &service.Return{
Code: "R03",
},
Expand Down Expand Up @@ -88,13 +89,16 @@ func TestFileTransformer__ReturnedPrenote(t *testing.T) {
}

func TestFileTransformer__FutureCorrectedPrenote(t *testing.T) {
var delay, err = time.ParseDuration("12h")
require.NoError(t, err)

resp := service.Response{
Match: service.Match{
EntryType: service.EntryTypePrenote,
AccountNumber: "810044964044",
},
Future: &service.Future{
Delay: "12h",
Action: service.Action{
Delay: &delay,
Correction: &service.Correction{
Code: "C01",
Data: "445566778",
Expand Down Expand Up @@ -126,13 +130,16 @@ func TestFileTransformer__FutureCorrectedPrenote(t *testing.T) {
}

func TestFileTransformer__FutureReturnedPrenote(t *testing.T) {
var delay, err = time.ParseDuration("12h")
require.NoError(t, err)

resp := service.Response{
Match: service.Match{
EntryType: service.EntryTypePrenote,
AccountNumber: "810044964044",
},
Future: &service.Future{
Delay: "12h",
Action: service.Action{
Delay: &delay,
Return: &service.Return{
Code: "R03",
},
Expand Down
35 changes: 22 additions & 13 deletions pkg/response/file_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"time"

"github.com/moov-io/ach"
"github.com/moov-io/ach-test-harness/pkg/service"
Expand All @@ -14,37 +16,39 @@ import (
)

type FileWriter interface {
Write(filepath string, r io.Reader) error
WriteFile(filename string, file *ach.File) error
Write(filepath string, r io.Reader, delay *time.Duration) error
WriteFile(filename string, file *ach.File, delay *time.Duration) error
}

func NewFileWriter(logger log.Logger, cfg service.ServerConfig, ftpServer *ftp.Server) FileWriter {
if cfg.FTP != nil {
return &FTPFileWriter{
cfg: cfg.FTP.Paths,
logger: logger,
server: ftpServer,
cfg: cfg.FTP.Paths,
rootPath: cfg.FTP.RootPath,
logger: logger,
server: ftpServer,
}
}
return nil
}

type FTPFileWriter struct {
cfg service.Paths
logger log.Logger
server *ftp.Server
cfg service.Paths
rootPath string
logger log.Logger
server *ftp.Server
}

func (w *FTPFileWriter) WriteFile(filepath string, file *ach.File) error {
func (w *FTPFileWriter) WriteFile(filepath string, file *ach.File, delay *time.Duration) error {
var buf bytes.Buffer
if err := ach.NewWriter(&buf).Write(file); err != nil {
return fmt.Errorf("write %s: %v", filepath, err)
}
w.logger.Info().Log(fmt.Sprintf("writing %s (%d bytes)", filepath, buf.Len()))
return w.Write(filepath, &buf)
return w.Write(filepath, &buf, delay)
}

func (w *FTPFileWriter) Write(path string, r io.Reader) error {
func (w *FTPFileWriter) Write(path string, r io.Reader, delay *time.Duration) error {
driver, err := w.server.Factory.NewDriver()
if err != nil {
return fmt.Errorf("get driver to write %s: %v", path, err)
Expand All @@ -54,11 +58,16 @@ func (w *FTPFileWriter) Write(path string, r io.Reader) error {
return fmt.Errorf("mkdir: %s: %v", path, err)
}

// TODO JB: see if there's a way to future-date the file's timestamp

if _, err := driver.PutFile(path, r, false); err != nil {
return fmt.Errorf("STOR %s: %v", path, err)
}

if delay != nil {
if err := os.Chtimes(filepath.Join(w.rootPath, path), time.Now(), time.Now().Add(*delay)); err != nil {
return fmt.Errorf("chtimes: %s: %v", path, err)
}
}

return nil
}

Expand Down
45 changes: 8 additions & 37 deletions pkg/response/match/matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func New(logger log.Logger, cfg service.Matching, responses []service.Response)
}
}

func (m Matcher) FindAction(ed *ach.EntryDetail) (*service.Action, *service.Future) {
func (m Matcher) FindAction(ed *ach.EntryDetail) *service.Action {
logger := m.Logger.With(log.Fields{
"entry_trace_number": log.String(ed.TraceNumber),
})
Expand All @@ -37,12 +37,6 @@ func (m Matcher) FindAction(ed *ach.EntryDetail) (*service.Action, *service.Futu
positive, negative := 0, 0 // Matchers are AND'd together
matcher := m.Responses[i].Match
action := m.Responses[i].Action
future := m.Responses[i].Future

if future == nil && action == nil {
logger.Log("future and action are both nil")
continue
}

var copyPath string
var delayTime string
Expand All @@ -52,21 +46,21 @@ func (m Matcher) FindAction(ed *ach.EntryDetail) (*service.Action, *service.Futu
var amount int

// Safely retrieve several values that are needed for the debug log below
if action != nil && action.Copy != nil {
if action.Copy != nil {
copyPath = action.Copy.Path
logger = logger.With(log.Fields{
"copy_path": log.String(copyPath),
})
}

if future != nil {
delayTime = future.Delay
if action.Delay != nil {
delayTime = action.Delay.String()
logger = logger.With(log.Fields{
"delay": log.String(delayTime),
})
}

if action != nil && action.Correction != nil {
if action.Correction != nil {
correctionCode = action.Correction.Code
correctionData = action.Correction.Data
logger = logger.With(log.Fields{
Expand All @@ -75,29 +69,13 @@ func (m Matcher) FindAction(ed *ach.EntryDetail) (*service.Action, *service.Futu
})
}

if future != nil && future.Correction != nil {
correctionCode = future.Correction.Code
correctionData = future.Correction.Data
logger = logger.With(log.Fields{
"correction_code": log.String(correctionCode),
"correction_data": log.String(correctionData),
})
}

if action != nil && action.Return != nil {
if action.Return != nil {
returnCode = action.Return.Code
logger = logger.With(log.Fields{
"return_code": log.String(returnCode),
})
}

if future != nil && future.Return != nil {
returnCode = future.Return.Code
logger = logger.With(log.Fields{
"return_code": log.String(returnCode),
})
}

if matcher.Amount != nil {
amount = matcher.Amount.Value
logger = logger.With(log.Fields{
Expand Down Expand Up @@ -209,17 +187,10 @@ func (m Matcher) FindAction(ed *ach.EntryDetail) (*service.Action, *service.Futu
logger.Logf("FINAL matching score negative=%d positive=%d", negative, positive)

if negative == 0 && positive > 0 {
if m.Responses[i].Future != nil {
// do this so that the entries can be morphed without duplicating code
action = &service.Action{
Correction: future.Correction,
Return: future.Return,
}
}
return action, future
return &m.Responses[i].Action
}
}
return nil, nil
return nil
}

func TraceNumber(m service.Match, ed *ach.EntryDetail) bool {
Expand Down
Loading

0 comments on commit e89f3e9

Please sign in to comment.