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 9 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
20 changes: 20 additions & 0 deletions 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
future:
delay: "12h"
return:
code: "R10"
```

The full config for Responses is below:
Expand All @@ -100,6 +108,18 @@ action:
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
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
105 changes: 79 additions & 26 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,8 +52,11 @@ 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])
// Check if there's a matching Action and perform it - this might be a future-dated action
jasonbornsteinMOOV marked this conversation as resolved.
Show resolved Hide resolved
action, future := ft.Matcher.FindAction(entries[j])
if future != nil {
action = &future.Action
}
jasonbornsteinMOOV marked this conversation as resolved.
Show resolved Hide resolved
if action != nil {
entry, err := ft.Entry.MorphEntry(file.Header, entries[j], *action)
if err != nil {
Expand All @@ -83,17 +74,41 @@ func (ft *FileTransfomer) Transform(file *ach.File) error {
}
}

// 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)
if future != 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 futErr
}

futMirror := newBatchMirror(ft.Writer, file.Batches[i])
futBatch, futErr := ach.NewBatch(file.Batches[i].GetHeader())
if futErr != nil {
return 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 fmt.Errorf("problem saving entries: %v", futErr)
}
// Create our Batch's Control and other fields
if futErr := futBatch.Create(); futErr != nil {
return fmt.Errorf("transform batch[%d] create error: %v", i, futErr)
}
futOut.AddBatch(futBatch)

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

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

if err := writeOutFile(out, ft, nil); err != nil {
return err
}
return 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
105 changes: 100 additions & 5 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 All @@ -31,6 +32,7 @@ func TestFileTransformer__CorrectedPrenote(t *testing.T) {

prenote, err := ach.ReadFile(filepath.Join("..", "..", "testdata", "prenote.ach"))
require.NoError(t, err)
require.NotNil(t, prenote)

err = fileTransformer.Transform(prenote)
require.NoError(t, err)
Expand All @@ -56,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 All @@ -66,6 +68,7 @@ func TestFileTransformer__ReturnedPrenote(t *testing.T) {

prenote, err := ach.ReadFile(filepath.Join("..", "..", "testdata", "prenote.ach"))
require.NoError(t, err)
require.NotNil(t, prenote)

err = fileTransformer.Transform(prenote)
require.NoError(t, err)
Expand All @@ -85,16 +88,109 @@ func TestFileTransformer__ReturnedPrenote(t *testing.T) {
require.Contains(t, out.String(), "R03")
}

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: delay,
Action: service.Action{
Correction: &service.Correction{
Code: "C01",
Data: "445566778",
},
},
},
}
fileTransformer, dir := testFileTransformer(t, resp)

prenote, err := ach.ReadFile(filepath.Join("..", "..", "testdata", "prenote.ach"))
require.NoError(t, err)
require.NotNil(t, prenote)

err = fileTransformer.Transform(prenote)
require.NoError(t, err)

retdir := filepath.Join(dir, "returned")

fds, err := os.ReadDir(retdir)
require.NoError(t, err)
require.Len(t, fds, 1)
finfo, err := fds[0].Info()
require.NoError(t, err)
require.Less(t, finfo.ModTime(), time.Now().Add(delay))

found, err := ach.ReadFile(filepath.Join(retdir, fds[0].Name()))
require.NoError(t, err)

var out bytes.Buffer
describe.File(&out, found, nil)
require.Contains(t, out.String(), "26 (Checking Return NOC Debit)")
require.Contains(t, out.String(), "C01")
}

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

resp := service.Response{
Match: service.Match{
EntryType: service.EntryTypePrenote,
AccountNumber: "810044964044",
},
Future: &service.Future{
Delay: delay,
Action: service.Action{
Return: &service.Return{
Code: "R03",
},
},
},
}
fileTransformer, dir := testFileTransformer(t, resp)

prenote, err := ach.ReadFile(filepath.Join("..", "..", "testdata", "prenote.ach"))
require.NoError(t, err)
require.NotNil(t, prenote)

err = fileTransformer.Transform(prenote)
require.NoError(t, err)

retdir := filepath.Join(dir, "returned")

fds, err := os.ReadDir(retdir)
require.NoError(t, err)
require.Len(t, fds, 1)
finfo, err := fds[0].Info()
require.NoError(t, err)
require.Less(t, finfo.ModTime(), time.Now().Add(delay))

found, err := ach.ReadFile(filepath.Join(retdir, fds[0].Name()))
require.NoError(t, err)

var out bytes.Buffer
describe.File(&out, found, nil)
require.Contains(t, out.String(), "28 (Checking Prenote Debit)")
require.Contains(t, out.String(), "R03")
}

func testFileTransformer(t *testing.T, resp service.Response) (*FileTransfomer, string) {
t.Helper()

logger := log.NewTestLogger()
dir, ftpServer := fileBackedFtpServer(t)

cfg := &service.Config{
Matching: service.Matching{
Debug: true,
},
Servers: service.ServerConfig{
FTP: &service.FTPConfig{
RootPath: dir,
Paths: service.Paths{
Return: "./returned/",
},
Expand All @@ -103,8 +199,7 @@ func testFileTransformer(t *testing.T, resp service.Response) (*FileTransfomer,
}
responses := []service.Response{resp}

dir, ftpServer := fileBackedFtpServer(t)

logger := log.NewTestLogger()
w := NewFileWriter(logger, cfg.Servers, ftpServer)

return NewFileTransformer(logger, cfg, responses, w), dir
Expand Down
Loading
Loading