Skip to content
This repository has been archived by the owner on Dec 9, 2021. It is now read-only.

Commit

Permalink
filetransfer: setup our Controller to return files and transfers
Browse files Browse the repository at this point in the history
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
adamdecaf committed Mar 31, 2020
1 parent 5fc3993 commit 74a2dfc
Show file tree
Hide file tree
Showing 8 changed files with 378 additions and 1 deletion.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/lopezator/migrator v0.2.0
github.com/mattn/go-sqlite3 v1.13.0
github.com/moov-io/accounts v0.4.1
github.com/moov-io/ach v1.3.2-0.20200124170558-e517f03c8034
github.com/moov-io/ach v1.4.0-rc1.0.20200331222826-d4d5e567be63
github.com/moov-io/base v0.11.0
github.com/moov-io/customers v0.4.0-rc1
github.com/moov-io/fed v0.4.1
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ github.com/Azure/azure-service-bus-go v0.4.1/go.mod h1:d9ho9e/06euiTwGpKxmlbpPhF
github.com/Azure/azure-service-bus-go v0.9.1/go.mod h1:yzBx6/BUGfjfeqbRZny9AQIbIe3AcV9WZbAdpkoXOa0=
github.com/Azure/azure-storage-blob-go v0.6.0/go.mod h1:oGfmITT1V6x//CswqY2gtAHND+xIP64/qL7a5QJix0Y=
github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-autorest v11.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest v11.1.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
Expand All @@ -41,6 +42,7 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20190418212003-6ac0b49e7197/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo=
github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20190605020000-c4ba1fdf4d36/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo=
github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20191009163259-e802c2cb94ae/go.mod h1:mjwGPas4yKduTyubHvD1Atl9r1rUq8DfVy+gkVvZ+oo=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
Expand Down Expand Up @@ -213,9 +215,11 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
Expand Down Expand Up @@ -255,6 +259,8 @@ github.com/moov-io/accounts v0.4.1 h1:KHei7s7jGwhrMVKkMXQTyQ8IEdKAhZ6+fB80gtgWDI
github.com/moov-io/accounts v0.4.1/go.mod h1:03s57sOGduvBS5VV3nBqrYa1xSZbzmJ9zggOXYuEZUw=
github.com/moov-io/ach v1.3.2-0.20200124170558-e517f03c8034 h1:XWcAbId30lqXWRWkzwbZ2zT3pAvKncjlQtAc+xRetQ4=
github.com/moov-io/ach v1.3.2-0.20200124170558-e517f03c8034/go.mod h1:Ye481bCN2amtLDs7uAuNX8K19HsBcSRMTOVXrburjXs=
github.com/moov-io/ach v1.4.0-rc1.0.20200331222826-d4d5e567be63 h1:KDMqqVpH9USqDeRw92xadtPL70DTad3u+cO618NwAT0=
github.com/moov-io/ach v1.4.0-rc1.0.20200331222826-d4d5e567be63/go.mod h1:jCcKVxrzxKp0D4HdE3xddu/vuslD0T3QpdppVLKMmEU=
github.com/moov-io/base v0.10.0/go.mod h1:pPu/TAc9PkaaegbREVEeDHsGqyAlvji9vqTuARuAnd0=
github.com/moov-io/base v0.11.0-rc1.0.20191121181647-cd3e7a9609db/go.mod h1:oXZRZBQiO7HX+Wc7Q3jJHLfkDPxrMXRbgYhURQqfXD8=
github.com/moov-io/base v0.11.0 h1:1dhI8qsIxsUjMmbzE52ycyy2S1z3DGUN6swGYjSmOLs=
Expand Down Expand Up @@ -482,6 +488,7 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.4 h1:WiKh4+/eMB2HaY7QhCfW/R7MuRAoA8QMCSJA6jP5/fo=
google.golang.org/appengine v1.6.4/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
Expand Down
150 changes: 150 additions & 0 deletions internal/filetransfer/create_returns.go
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
}
106 changes: 106 additions & 0 deletions internal/filetransfer/create_returns_test.go
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")
}
}
28 changes: 28 additions & 0 deletions internal/filetransfer/prenote.go
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
}
56 changes: 56 additions & 0 deletions internal/filetransfer/prenote_test.go
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)
}
}
Loading

0 comments on commit 74a2dfc

Please sign in to comment.