-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: batches endpoint for searching files and returning batches (#235)
- Loading branch information
Showing
9 changed files
with
463 additions
and
0 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package batches | ||
|
||
import ( | ||
"encoding/json" | ||
"net/http" | ||
|
||
"github.com/moov-io/base/log" | ||
|
||
"github.com/gorilla/mux" | ||
) | ||
|
||
func NewBatchController(logger log.Logger, service BatchService) *batchController { | ||
return &batchController{ | ||
logger: logger, | ||
service: service, | ||
} | ||
} | ||
|
||
type batchController struct { | ||
logger log.Logger | ||
service BatchService | ||
} | ||
|
||
func (c *batchController) AppendRoutes(router *mux.Router) *mux.Router { | ||
router. | ||
Name("Batch.search"). | ||
Methods("GET"). | ||
Path("/batches"). | ||
HandlerFunc(c.Search()) | ||
|
||
return router | ||
} | ||
|
||
func (c *batchController) Search() func(w http.ResponseWriter, r *http.Request) { | ||
return func(w http.ResponseWriter, r *http.Request) { | ||
w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||
|
||
batches, err := c.service.Search(readSearchOptions(r)) | ||
if err != nil { | ||
w.WriteHeader(http.StatusInternalServerError) | ||
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) | ||
|
||
return | ||
} | ||
|
||
w.WriteHeader(http.StatusOK) | ||
json.NewEncoder(w).Encode(batches) | ||
} | ||
} | ||
|
||
func readSearchOptions(r *http.Request) SearchOptions { | ||
query := r.URL.Query() | ||
opts := SearchOptions{ | ||
AccountNumber: query.Get("accountNumber"), | ||
RoutingNumber: query.Get("routingNumber"), | ||
TraceNumber: query.Get("traceNumber"), | ||
CreatedAfter: query.Get("createdAfter"), | ||
Path: query.Get("path"), | ||
} | ||
return opts | ||
} |
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,141 @@ | ||
package batches | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/moov-io/ach-test-harness/pkg/service" | ||
"github.com/moov-io/base/log" | ||
|
||
jsonpatch "github.com/evanphx/json-patch" | ||
"github.com/gorilla/mux" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestBatchController(t *testing.T) { | ||
router := mux.NewRouter() | ||
logger := log.NewDefaultLogger() | ||
|
||
t.Run("/batches returns list of batches", func(t *testing.T) { | ||
repo := NewFTPRepository(logger, &service.FTPConfig{ | ||
RootPath: "./testdata", | ||
}) | ||
newBatchService := NewBatchService(repo) | ||
controller := NewBatchController(logger, newBatchService) | ||
controller.AppendRoutes(router) | ||
|
||
req, _ := http.NewRequest("GET", "/batches", nil) | ||
rr := httptest.NewRecorder() | ||
router.ServeHTTP(rr, req) | ||
|
||
wantJSON := []byte(` | ||
[ | ||
{ | ||
"batchHeader": { | ||
"id": "", | ||
"serviceClassCode": 225, | ||
"companyName": "Name on Account", | ||
"companyIdentification": "231380104", | ||
"standardEntryClassCode": "CCD", | ||
"companyEntryDescription": "Vndr Pay", | ||
"effectiveEntryDate": "190816", | ||
"settlementDate": " ", | ||
"originatorStatusCode": 1, | ||
"ODFIIdentification": "03130001", | ||
"batchNumber": 1 | ||
}, | ||
"entryDetails": [ | ||
{ | ||
"id": "", | ||
"transactionCode": 27, | ||
"RDFIIdentification": "23138010", | ||
"checkDigit": "4", | ||
"DFIAccountNumber": "744-5678-99", | ||
"amount": 500000, | ||
"identificationNumber": "location1234567", | ||
"individualName": "Best Co. #123456789012", | ||
"discretionaryData": "S ", | ||
"traceNumber": "031300010000001", | ||
"category": "Forward" | ||
}, | ||
{ | ||
"id": "", | ||
"transactionCode": 27, | ||
"RDFIIdentification": "23138010", | ||
"checkDigit": "4", | ||
"DFIAccountNumber": "744-5678-99", | ||
"amount": 125, | ||
"identificationNumber": "Fee123456789012", | ||
"individualName": "Best Co. #123456789012", | ||
"discretionaryData": "S ", | ||
"traceNumber": "031300010000002", | ||
"category": "Forward" | ||
} | ||
], | ||
"batchControl": { | ||
"id": "", | ||
"serviceClassCode": 225, | ||
"entryAddendaCount": 2, | ||
"entryHash": 46276020, | ||
"totalDebit": 500125, | ||
"totalCredit": 0, | ||
"companyIdentification": "231380104", | ||
"ODFIIdentification": "03130001", | ||
"batchNumber": 1 | ||
}, | ||
"offset": null | ||
}, | ||
{ | ||
"batchHeader": { | ||
"id": "", | ||
"serviceClassCode": 220, | ||
"companyName": "Name on Account", | ||
"companyIdentification": "231380104", | ||
"standardEntryClassCode": "PPD", | ||
"companyEntryDescription": "REG.SALARY", | ||
"effectiveEntryDate": "190816", | ||
"settlementDate": " ", | ||
"originatorStatusCode": 1, | ||
"ODFIIdentification": "12104288", | ||
"batchNumber": 1 | ||
}, | ||
"entryDetails": [ | ||
{ | ||
"id": "", | ||
"transactionCode": 22, | ||
"RDFIIdentification": "23138010", | ||
"checkDigit": "4", | ||
"DFIAccountNumber": "987654321", | ||
"amount": 100000000, | ||
"identificationNumber": " ", | ||
"individualName": "Credit Account 1 ", | ||
"discretionaryData": " ", | ||
"traceNumber": "121042880000002", | ||
"category": "Forward" | ||
} | ||
], | ||
"batchControl": { | ||
"id": "", | ||
"serviceClassCode": 220, | ||
"entryAddendaCount": 1, | ||
"entryHash": 23138010, | ||
"totalDebit": 0, | ||
"totalCredit": 100000000, | ||
"companyIdentification": "231380104", | ||
"ODFIIdentification": "12104288", | ||
"batchNumber": 1 | ||
}, | ||
"offset": null | ||
} | ||
] | ||
`) | ||
|
||
gotJSON := rr.Body.Bytes() | ||
|
||
fmt.Printf("\n\n%s\n\n", string(gotJSON)) | ||
|
||
require.Truef(t, jsonpatch.Equal(wantJSON, gotJSON), "received JSON does not match expected json") | ||
}) | ||
} |
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 @@ | ||
package batches | ||
|
||
import ( | ||
"fmt" | ||
"io/fs" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/moov-io/ach" | ||
"github.com/moov-io/ach-test-harness/pkg/response/match" | ||
"github.com/moov-io/ach-test-harness/pkg/service" | ||
"github.com/moov-io/base/log" | ||
) | ||
|
||
type BatchRepository interface { | ||
Search(opts SearchOptions) ([]ach.Batcher, error) | ||
} | ||
|
||
type batchRepository struct { | ||
logger log.Logger | ||
rootPath string | ||
} | ||
|
||
func NewFTPRepository(logger log.Logger, cfg *service.FTPConfig) *batchRepository { | ||
return &batchRepository{ | ||
logger: logger, | ||
rootPath: cfg.RootPath, | ||
} | ||
} | ||
|
||
func (r *batchRepository) Search(opts SearchOptions) ([]ach.Batcher, error) { | ||
out := make([]ach.Batcher, 0) | ||
|
||
//nolint:gosimple | ||
var search fs.WalkDirFunc | ||
search = func(path string, d fs.DirEntry, err error) error { | ||
if d.IsDir() { | ||
return nil | ||
} | ||
if err != nil { | ||
r.logger.Logf("error walking dir %s. %v", d.Name(), err) | ||
return nil | ||
} | ||
|
||
r.logger.Logf("reading %s", path) | ||
// read only *.ach files | ||
if strings.ToLower(filepath.Ext(path)) != ".ach" { | ||
return nil | ||
} | ||
|
||
batches, err := filterBatches(path, opts) | ||
if err != nil { | ||
return err | ||
} | ||
out = append(out, batches...) | ||
return nil | ||
} | ||
|
||
var walkingPath = r.rootPath | ||
if opts.Path != "" { | ||
walkingPath = filepath.Join(r.rootPath, opts.Path) | ||
} | ||
|
||
r.logger.Logf("Waling directory %s", walkingPath) | ||
if err := filepath.WalkDir(walkingPath, search); err != nil { | ||
return nil, fmt.Errorf("failed reading directory content %s: %v", walkingPath, err) | ||
} | ||
|
||
return out, nil | ||
} | ||
|
||
func filterBatches(path string, opts SearchOptions) ([]ach.Batcher, error) { | ||
file, _ := ach.ReadFile(path) | ||
if file == nil { | ||
return nil, nil | ||
} | ||
|
||
tooOld, err := opts.fileTooOld(file) | ||
if tooOld || err != nil { | ||
return nil, err | ||
} | ||
|
||
mm := service.Match{ | ||
AccountNumber: opts.AccountNumber, | ||
RoutingNumber: opts.RoutingNumber, | ||
TraceNumber: opts.TraceNumber, | ||
} | ||
|
||
var out []ach.Batcher | ||
for i := range file.Batches { | ||
entries := file.Batches[i].GetEntries() | ||
if mm.Empty() { | ||
out = append(out, file.Batches[i]) | ||
continue | ||
} | ||
for j := range entries { | ||
if match.TraceNumber(mm, entries[j]) || match.AccountNumber(mm, entries[j]) || | ||
match.RoutingNumber(mm, entries[j]) { | ||
// accumulate batch | ||
out = append(out, file.Batches[i]) | ||
continue | ||
} | ||
} | ||
} | ||
return out, 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,63 @@ | ||
package batches | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/moov-io/ach-test-harness/pkg/service" | ||
"github.com/moov-io/base/log" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestRepository(t *testing.T) { | ||
logger := log.NewDefaultLogger() | ||
|
||
repo := NewFTPRepository(logger, &service.FTPConfig{ | ||
RootPath: "./testdata", | ||
Paths: service.Paths{ | ||
Files: "/outbound/", | ||
Return: "/returned/", | ||
}, | ||
}) | ||
|
||
// return all | ||
batches, err := repo.Search(SearchOptions{}) | ||
require.NoError(t, err) | ||
require.Len(t, batches, 2) | ||
|
||
// search by account number | ||
batches, err = repo.Search(SearchOptions{ | ||
AccountNumber: "987654321", | ||
}) | ||
|
||
require.NoError(t, err) | ||
require.Len(t, batches, 1) | ||
|
||
// search by timestamp in our files: | ||
// returned/2.ach was created on 1908161055 and has 1 entry | ||
// outbound/1.ach was created on 1908161059 and has 2 batches | ||
batches, err = repo.Search(SearchOptions{ | ||
CreatedAfter: "2019-08-16T10:56:00+00:00", | ||
}) | ||
|
||
// expect to get batches from outbound/1.ach | ||
require.NoError(t, err) | ||
require.Len(t, batches, 1) | ||
|
||
// search by subdirectory in our files: | ||
// outbound/1.ach was created on 1908161059 and has 2 batches | ||
batches, err = repo.Search(SearchOptions{ | ||
Path: "outbound", | ||
}) | ||
|
||
// expect to get batches from outbound/1.ach | ||
require.NoError(t, err) | ||
require.Len(t, batches, 1) | ||
} | ||
|
||
func TestRepository__filterBatches(t *testing.T) { | ||
var opts SearchOptions | ||
|
||
batches, err := filterBatches("/tmp/noexist/foobar", opts) | ||
require.NoError(t, err) | ||
require.Len(t, batches, 0) | ||
} |
Oops, something went wrong.