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

Future actions #167

Merged
merged 20 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from 13 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
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ ACHTestHarness:
action:
return:
code: "R03"

- match:
amount:
value: 12357 # $123.57
action:
delay: "12h"
return:
code: "R10"
```

The full config for Responses is below:
Expand All @@ -95,8 +103,22 @@ match:
routingNumber: <string> # Exact match of ABA routing number (RDFIIdentification and CheckDigit)
traceNumber: <string> # Exact match of TraceNumber
entryType: <string> # Checks TransactionCode. Accepted values: credit, debit or prenote.
# Matching will find at most 2 Actions: 1 Copy Action and 1 Return/Correction Action.
# If the Return/Correction Action as no Delay, the Copy Action will be excluded.
jasonbornsteinMOOV marked this conversation as resolved.
Show resolved Hide resolved
# Valid combinations include:
# 1. Copy
# 2. Return/Correction w/ Delay
# 3. Return/Correction w/o Delay
# 4. Copy and Return/Correction w/ Delay
# 5. Nothing
# Invalid combinations are:
# 1. Copy + Return/Correction w/o Delay
# 2. Copy w/ Delay (validated when reading configuration)
jasonbornsteinMOOV marked this conversation as resolved.
Show resolved Hide resolved
action:
# Copy the EntryDetail to another directory
# How long into the future should we wait before making the correction/return available?
delay: <duration>

# Copy the EntryDetail to another directory (not valid with a delay)
copy:
path: <string> # Filepath on the FTP server

Expand Down
1 change: 1 addition & 0 deletions examples/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ ACHTestHarness:
# This matches ./examples/utility-bill.ach
accountNumber: "744-5678-99"
action:
delay: "12h"
correction:
code: "C01"
data: "744567899"
Expand Down
36 changes: 36 additions & 0 deletions pkg/filedrive/mtime_filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package filedrive

import (
"time"

"goftp.io/server/core"
)

type MTimeFilter struct {
core.Driver
}

func (mtf MTimeFilter) ListDir(path string, callback func(core.FileInfo) error) error {
now := time.Now()

return mtf.Driver.ListDir(path, func(info core.FileInfo) error {
if info.ModTime().Before(now) {
return callback(info)
}
return nil
})
}

type Factory struct {
DriverFactory core.DriverFactory
}

func (f *Factory) NewDriver() (core.Driver, error) {
dd, err := f.DriverFactory.NewDriver()
if err != nil {
return nil, err
}
return MTimeFilter{
Driver: dd,
}, nil
}
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
8 changes: 4 additions & 4 deletions pkg/response/entry_transformer.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import (
)

type EntryTransformer interface {
MorphEntry(fh ach.FileHeader, ed *ach.EntryDetail, action service.Action) (*ach.EntryDetail, error)
MorphEntry(fh ach.FileHeader, ed *ach.EntryDetail, action *service.Action) (*ach.EntryDetail, error)
}

type EntryTransformers []EntryTransformer

func (et EntryTransformers) MorphEntry(fh ach.FileHeader, ed *ach.EntryDetail, action service.Action) (*ach.EntryDetail, error) {
func (et EntryTransformers) MorphEntry(fh ach.FileHeader, ed *ach.EntryDetail, action *service.Action) (*ach.EntryDetail, error) {
var err error
for i := range et {
ed, err = et[i].MorphEntry(fh, ed, action)
Expand All @@ -27,7 +27,7 @@ func (et EntryTransformers) MorphEntry(fh ach.FileHeader, ed *ach.EntryDetail, a

type CorrectionTransformer struct{}

func (t *CorrectionTransformer) MorphEntry(fh ach.FileHeader, ed *ach.EntryDetail, action service.Action) (*ach.EntryDetail, error) {
func (t *CorrectionTransformer) MorphEntry(fh ach.FileHeader, ed *ach.EntryDetail, action *service.Action) (*ach.EntryDetail, error) {
if action.Correction == nil {
return ed, nil
}
Expand Down Expand Up @@ -91,7 +91,7 @@ func generateCorrectedData(cor *service.Correction) string {

type ReturnTransformer struct{}

func (t *ReturnTransformer) MorphEntry(fh ach.FileHeader, ed *ach.EntryDetail, action service.Action) (*ach.EntryDetail, error) {
func (t *ReturnTransformer) MorphEntry(fh ach.FileHeader, ed *ach.EntryDetail, action *service.Action) (*ach.EntryDetail, error) {
if action.Return == nil {
return ed, nil
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/response/entry_transformer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func TestMorphEntry__Correction(t *testing.T) {
},
}
ed := file.Batches[0].GetEntries()[0]
out, err := xform.MorphEntry(file.Header, ed, action)
out, err := xform.MorphEntry(file.Header, ed, &action)
require.NoError(t, err)

if out.Addenda98 == nil {
Expand All @@ -49,7 +49,7 @@ func TestMorphEntry__Return(t *testing.T) {
},
}
ed := file.Batches[0].GetEntries()[0]
out, err := xform.MorphEntry(file.Header, ed, action)
out, err := xform.MorphEntry(file.Header, ed, &action)
require.NoError(t, err)

if out.Addenda98 != nil {
Expand Down
151 changes: 106 additions & 45 deletions pkg/response/file_transformer.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,8 @@ func NewFileTransformer(logger log.Logger, cfg *service.Config, responses []serv
func (ft *FileTransfomer) Transform(file *ach.File) error {
out := ach.NewFile()
out.SetValidation(ft.ValidateOpts)

out.Header = ach.NewFileHeader()
out.Header.SetValidation(ft.ValidateOpts)

out.Header.ImmediateDestination = file.Header.ImmediateOrigin
out.Header.ImmediateDestinationName = file.Header.ImmediateOriginName
out.Header.ImmediateOrigin = file.Header.ImmediateDestination
out.Header.ImmediateOriginName = file.Header.ImmediateDestinationName
out.Header.FileCreationDate = time.Now().Format("060102")
out.Header.FileCreationTime = time.Now().Format("1504")
out.Header.FileIDModifier = "A"

if err := out.Header.Validate(); err != nil {
return fmt.Errorf("file transform: header validate: %v", err)
if err := createOutHeader(out, file, ft.ValidateOpts); err != nil {
return err
}

for i := range file.Batches {
Expand All @@ -64,36 +52,16 @@ 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
action := ft.Matcher.FindAction(entries[j])
if action != nil {
entry, err := ft.Entry.MorphEntry(file.Header, entries[j], *action)
if err != nil {
return fmt.Errorf("transform batch[%d] morph entry[%d] error: %v", i, j, err)
}

// When the entry is corrected we need to change the SEC code
if entry.Category == ach.CategoryNOC {
bh := batch.GetHeader()
bh.StandardEntryClassCode = ach.COR
if b, err := ach.NewBatch(bh); b != nil {
batch = b // replace entire Batch
} else {
return fmt.Errorf("transform batch[%d] NOC entry[%d] error: %v", i, j, err)
}
}

// Save this Entry
if action.Copy != nil {
mirror.saveEntry(action.Copy, entries[j])
} else {
// Add the transformed entry onto the batch
if entry != nil {
batch.AddEntry(entry)
}
}
// Check if there's a matching Action and perform it. There may also be a future-dated action to execute.
copyAction, processAction := ft.Matcher.FindAction(entries[j])
if batch, err = processMatchedAction(copyAction, ft, mirror, file, batch, entries, i, j); err != nil {
return err
}
if batch, err = processMatchedAction(processAction, ft, mirror, file, batch, entries, i, j); err != nil {
return err
}
}

// Save off the entries as requested
if err := mirror.saveFiles(); err != nil {
return fmt.Errorf("problem saving entries: %v", err)
Expand All @@ -107,14 +75,107 @@ func (ft *FileTransfomer) Transform(file *ach.File) error {
}
}

if err := writeOutFile(out, ft, nil); err != nil {
return err
}
return nil
}

func processMatchedAction(action *service.Action, ft *FileTransfomer, mirror *batchMirror, file *ach.File, batch ach.Batcher, entries []*ach.EntryDetail, i int, j int) (ach.Batcher, error) {
if action != nil {
entry, err := ft.Entry.MorphEntry(file.Header, entries[j], action)
if err != nil {
return nil, fmt.Errorf("transform batch[%d] morph entry[%d] error: %v", i, j, err)
}

// When the entry is corrected we need to change the SEC code
if entry.Category == ach.CategoryNOC {
bh := batch.GetHeader()
bh.StandardEntryClassCode = ach.COR
if b, err := ach.NewBatch(bh); b != nil {
batch = b // replace entire Batch
} else {
return nil, fmt.Errorf("transform batch[%d] NOC entry[%d] error: %v", i, j, err)
}
}

if action.Delay != nil {
// need to save off the future-dated entry
futOut := ach.NewFile()
futOut.SetValidation(ft.ValidateOpts)
if futErr := createOutHeader(futOut, file, ft.ValidateOpts); futErr != nil {
return nil, futErr
}

futMirror := newBatchMirror(ft.Writer, file.Batches[i])
futBatch, futErr := ach.NewBatch(file.Batches[i].GetHeader())
if futErr != nil {
return nil, fmt.Errorf("transform batch[%d] problem creating Batch: %v", i, futErr)
}

saveEntry(action, futMirror, futBatch, entry, entries[j])

// Save off the entries as requested
if futErr := futMirror.saveFiles(); futErr != nil {
return nil, fmt.Errorf("problem saving entries: %v", futErr)
}
// Create our Batch's Control and other fields
if futErr := futBatch.Create(); futErr != nil {
return nil, fmt.Errorf("transform batch[%d] create error: %v", i, futErr)
}
futOut.AddBatch(futBatch)

if futErr := writeOutFile(futOut, ft, action.Delay); futErr != nil {
return nil, futErr
}
} else {
saveEntry(action, mirror, batch, entry, entries[j])
}
}

return batch, nil
}

func createOutHeader(out *ach.File, file *ach.File, opts *ach.ValidateOpts) error {
out.Header = ach.NewFileHeader()
out.Header.SetValidation(opts)

out.Header.ImmediateDestination = file.Header.ImmediateOrigin
out.Header.ImmediateDestinationName = file.Header.ImmediateOriginName
out.Header.ImmediateOrigin = file.Header.ImmediateDestination
out.Header.ImmediateOriginName = file.Header.ImmediateDestinationName
out.Header.FileCreationDate = time.Now().Format("060102")
out.Header.FileCreationTime = time.Now().Format("1504")
out.Header.FileIDModifier = "A"

if err := out.Header.Validate(); err != nil {
return fmt.Errorf("file transform: header validate: %v", err)
}

return nil
}

func saveEntry(action *service.Action, mirror *batchMirror, batch ach.Batcher, morphedEntry *ach.EntryDetail, originalEntry *ach.EntryDetail) {
// Save this Entry
if action.Copy != nil {
mirror.saveEntry(action.Copy, originalEntry)
} else {
// Add the transformed entry onto the batch
if morphedEntry != nil {
batch.AddEntry(morphedEntry)
}
}
}

func writeOutFile(out *ach.File, ft *FileTransfomer, delay *time.Duration) error {
if out != nil && len(out.Batches) > 0 {
if err := out.Create(); err != nil {
return fmt.Errorf("transform out create: %v", err)
}
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 {
return fmt.Errorf("transform write %s: %v", filepath, err)
generatedFilePath := filepath.Join(ft.returnPath, generateFilename(out)) // TODO(adam): need to determine return path
if err := ft.Writer.WriteFile(generatedFilePath, out, delay); err != nil {
return fmt.Errorf("transform write %s: %v", generatedFilePath, err)
}
} else {
return fmt.Errorf("transform validate out file: %v", err)
Expand Down
Loading
Loading