This repository has been archived by the owner on Dec 9, 2021. It is now read-only.
forked from moov-io/paygate
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
filetransfer: setup our Controller to return files and transfers
This change adds a setup from a larger PR[0] to create return files and upload them to the ODFI. Issue: moov-io#430 [0]: moov-io#446
- Loading branch information
Showing
8 changed files
with
378 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
// Copyright 2020 The Moov Authors | ||
// Use of this source code is governed by an Apache License | ||
// license that can be found in the LICENSE file. | ||
|
||
package filetransfer | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"time" | ||
|
||
"github.com/moov-io/ach" | ||
"github.com/moov-io/paygate/internal/filetransfer/config" | ||
|
||
"github.com/go-kit/kit/metrics/prometheus" | ||
stdprometheus "github.com/prometheus/client_golang/prometheus" | ||
) | ||
|
||
var ( | ||
returnFilesCreated = prometheus.NewCounterFrom(stdprometheus.CounterOpts{ | ||
Name: "return_ach_files_created", | ||
Help: "Counter of return files created", | ||
}, []string{"destination", "origin"}) // , "code" | ||
) | ||
|
||
func (c *Controller) scratchDir() string { | ||
dir := filepath.Join(c.rootDir, "scratch") | ||
if _, err := os.Stat(dir); os.IsNotExist(err) { | ||
os.MkdirAll(dir, 0777) // ensure directory is created | ||
} | ||
return dir | ||
} | ||
|
||
func (c *Controller) uploadReturnFile(cfg *config.Config, file *ach.File) error { | ||
returnFilesCreated.With("destination", file.Header.ImmediateDestination, "origin", file.Header.ImmediateOrigin).Add(1) | ||
|
||
filename, err := config.RenderACHFilename(cfg.FilenameTemplate(), config.FilenameData{ | ||
RoutingNumber: file.Header.ImmediateDestination, | ||
N: fmt.Sprintf("%d", time.Now().Unix()), | ||
}) | ||
if err != nil { | ||
return fmt.Errorf("problem writing routingNumber=%s return file: %v", cfg.RoutingNumber, err) | ||
} | ||
filename = fmt.Sprintf("return-%s", filename) | ||
|
||
// write and upload return file | ||
scratch := c.scratchDir() | ||
out := &achFile{ | ||
File: file, | ||
filepath: filepath.Join(scratch, filename), | ||
} | ||
if err := out.write(); err != nil { | ||
return fmt.Errorf("problem writing file for return: %v", err) | ||
} | ||
if err := c.maybeUploadFile(out); err != nil { | ||
return fmt.Errorf("problem uploading return file: %v", err) | ||
} | ||
return nil | ||
} | ||
|
||
func (c *Controller) uploadReturnFiles(files []*ach.File) error { | ||
for i := range files { | ||
cfg := c.findFileTransferConfig(files[i].Header.ImmediateOrigin) | ||
|
||
if err := c.uploadReturnFile(cfg, files[i]); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func returnEntireFile(file *ach.File, code string) ([]*ach.File, error) { | ||
var acc []*ach.File | ||
var err error | ||
|
||
for i := range file.Batches { | ||
entries := file.Batches[i].GetEntries() | ||
for j := range entries { | ||
f, err := returnEntry(file.Header, file.Batches[i], entries[j], code) | ||
if err != nil { | ||
return nil, fmt.Errorf("problem returning=%s traceNumber=%s", code, entries[j].TraceNumber) | ||
} | ||
acc, err = ach.MergeFiles(append(acc, f)) | ||
if err != nil { | ||
return nil, fmt.Errorf("problem merging return files: %v", err) | ||
} | ||
} | ||
} | ||
|
||
// Flatten each batch in each returned file before uploading. | ||
for i := range acc { | ||
acc[i], err = acc[i].FlattenBatches() | ||
if err != nil { | ||
err = fmt.Errorf("problem flattening: %v", err) | ||
} | ||
} | ||
|
||
return acc, err | ||
} | ||
|
||
func returnEntry(fh ach.FileHeader, b ach.Batcher, entry *ach.EntryDetail, code string) (*ach.File, error) { | ||
returnCode := ach.LookupReturnCode(code) | ||
if returnCode == nil { | ||
return nil, fmt.Errorf("unknown return code: %s", code) | ||
} | ||
|
||
addenda99 := ach.NewAddenda99() | ||
addenda99.ReturnCode = returnCode.Code | ||
addenda99.OriginalTrace = entry.TraceNumber | ||
addenda99.OriginalDFI = entry.RDFIIdentification | ||
// set TraceNumber as ODFI (we are returning) | ||
// addenda99.TraceNumber = remoteach.CreateTraceNumber(fh.ImmediateDestination) // TODO(adam): export? | ||
|
||
file := ach.NewFile() | ||
file.Header = fh | ||
|
||
// swap origin / destination | ||
file.Header.ImmediateOrigin = fh.ImmediateDestination | ||
file.Header.ImmediateOriginName = fh.ImmediateDestinationName | ||
file.Header.ImmediateDestination = fh.ImmediateOrigin | ||
file.Header.ImmediateDestinationName = fh.ImmediateOriginName | ||
|
||
now := time.Now() | ||
file.Header.FileCreationDate = now.Format("060102") // YYMMDD | ||
file.Header.FileCreationTime = now.Format("1504") // HHMM | ||
|
||
batchHeader := b.GetHeader() | ||
batchHeader.EffectiveEntryDate = now.Format("060102") // YYMMDD | ||
|
||
// Adjust EntryDetail we're going to return | ||
// entry.RDFIIdentification = remoteach.ABA8(file.Header.ImmediateDestination) // TODO(adam): export? | ||
// entry.CheckDigit = remoteach.ABACheckDigit(file.Header.ImmediateDestination) // TODO(adam): export? | ||
|
||
batch, err := ach.NewBatch(batchHeader) | ||
if err != nil { | ||
return nil, err | ||
} | ||
batch.AddEntry(entry) | ||
if err := batch.Create(); err != nil { | ||
return nil, err | ||
} | ||
|
||
file.AddBatch(batch) | ||
if err := file.Create(); err != nil { | ||
return nil, err | ||
} | ||
|
||
return file, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
// Copyright 2020 The Moov Authors | ||
// Use of this source code is governed by an Apache License | ||
// license that can be found in the LICENSE file. | ||
|
||
package filetransfer | ||
|
||
import ( | ||
"io/ioutil" | ||
"path/filepath" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/moov-io/ach" | ||
) | ||
|
||
func TestController__uploadReturnFile(t *testing.T) { | ||
controller := setupTestController(t) | ||
|
||
out, err := parseACHFilepath(filepath.Join("..", "..", "testdata", "cor-c01.ach")) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
if err := controller.uploadReturnFiles([]*ach.File{out}); err != nil { | ||
if !strings.Contains(err.Error(), "connect: connection refused") { | ||
t.Fatalf("unexpected error: %v", err) | ||
} | ||
} | ||
|
||
fds, err := ioutil.ReadDir(controller.scratchDir()) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if len(fds) != 1 { | ||
t.Errorf("got %d files", len(fds)) | ||
} | ||
|
||
file, err := parseACHFilepath(filepath.Join(controller.scratchDir(), fds[0].Name())) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if file == nil { | ||
t.Error("got no file") | ||
} | ||
} | ||
|
||
func TestController__returnEntireFile(t *testing.T) { | ||
in, err := parseACHFilepath(filepath.Join("..", "..", "testdata", "two-micro-deposits.ach")) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
outFiles, err := returnEntireFile(in, "R01") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if len(outFiles) != 1 { | ||
t.Fatalf("got %d unexpected files", len(outFiles)) | ||
} | ||
|
||
out := outFiles[0] | ||
if len(out.Batches) != 1 { | ||
t.Errorf("got %d batches", len(out.Batches)) | ||
} | ||
|
||
entries := outFiles[0].Batches[0].GetEntries() | ||
if len(entries) != 6 { | ||
t.Errorf("got %d entries", len(entries)) | ||
} | ||
} | ||
|
||
func TestController__returnEntry(t *testing.T) { | ||
in, err := parseACHFilepath(filepath.Join("..", "..", "testdata", "prenote-ppd-debit.ach")) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if len(in.Batches) != 1 { | ||
t.Fatalf("batches=%#v", in.Batches) | ||
} | ||
|
||
entry := in.Batches[0].GetEntries()[0] | ||
out, err := returnEntry(in.Header, in.Batches[0], entry, "R01") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
if len(out.Batches) != 1 { | ||
t.Fatalf("batches=%#v", out.Batches) | ||
} | ||
entries := out.Batches[0].GetEntries() | ||
if len(entries) != 1 { | ||
t.Fatalf("entries=%#v", entries) | ||
} | ||
|
||
if ok, err := isPrenoteEntry(entries[0]); !ok || err != nil { | ||
t.Errorf("expected prenote entry: %#v", entries[0]) | ||
t.Error(err) | ||
} | ||
} | ||
|
||
func TestController__returnEntryErr(t *testing.T) { | ||
var fh ach.FileHeader | ||
if _, err := returnEntry(fh, nil, nil, "invalid"); err == nil { | ||
t.Error("expected error") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
// Copyright 2020 The Moov Authors | ||
// Use of this source code is governed by an Apache License | ||
// license that can be found in the LICENSE file. | ||
|
||
package filetransfer | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/moov-io/ach" | ||
) | ||
|
||
func isPrenoteEntry(entry *ach.EntryDetail) (bool, error) { | ||
switch entry.TransactionCode { | ||
case | ||
ach.CheckingPrenoteCredit, ach.CheckingPrenoteDebit, | ||
ach.SavingsPrenoteCredit, ach.SavingsPrenoteDebit, | ||
ach.GLPrenoteCredit, ach.GLPrenoteDebit, ach.LoanPrenoteCredit: | ||
if entry.Amount == 0 { | ||
return true, nil // valid prenotification entry | ||
} else { | ||
return true, fmt.Errorf("non-zero prenotification amount=%d", entry.Amount) | ||
} | ||
default: | ||
return false, nil // TransactionCode isn't pre-note | ||
} | ||
return false, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
// Copyright 2020 The Moov Authors | ||
// Use of this source code is governed by an Apache License | ||
// license that can be found in the LICENSE file. | ||
|
||
package filetransfer | ||
|
||
import ( | ||
"path/filepath" | ||
"testing" | ||
) | ||
|
||
func TestPrenote__isPrenoteEntry(t *testing.T) { | ||
file, err := parseACHFilepath(filepath.Join("..", "..", "testdata", "prenote-ppd-debit.ach")) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
entries := file.Batches[0].GetEntries() | ||
if len(entries) != 1 { | ||
t.Fatalf("unexpected entries: %#v", entries) | ||
} | ||
for i := range entries { | ||
if ok, err := isPrenoteEntry(entries[i]); !ok || err != nil { | ||
t.Errorf("expected prenote entry: %#v", entries[i]) | ||
t.Error(err) | ||
} | ||
} | ||
|
||
// non prenote file | ||
file, err = parseACHFilepath(filepath.Join("..", "..", "testdata", "ppd-debit.ach")) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
entries = file.Batches[0].GetEntries() | ||
for i := range entries { | ||
if ok, err := isPrenoteEntry(entries[i]); ok || err != nil { | ||
t.Errorf("expected no prenote entry: %#v", entries[i]) | ||
t.Error(err) | ||
} | ||
} | ||
} | ||
|
||
func TestPrenote__isPrenoteEntryErr(t *testing.T) { | ||
file, err := parseACHFilepath(filepath.Join("..", "..", "testdata", "prenote-ppd-debit.ach")) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
entries := file.Batches[0].GetEntries() | ||
if len(entries) != 1 { | ||
t.Fatalf("unexpected entries: %#v", entries) | ||
} | ||
|
||
entries[0].Amount = 125 // non-zero amount | ||
if exists, err := isPrenoteEntry(entries[0]); !exists || err == nil { | ||
t.Errorf("expected invalid prenote: %v", err) | ||
} | ||
} |
Oops, something went wrong.