Skip to content

Commit

Permalink
feat: batches endpoint for searching files and returning batches (#235)
Browse files Browse the repository at this point in the history
  • Loading branch information
darwinz authored Jun 26, 2024
1 parent 2f2db27 commit c93d8ea
Show file tree
Hide file tree
Showing 9 changed files with 463 additions and 0 deletions.
6 changes: 6 additions & 0 deletions cmd/ach-test-harness/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"

achtestharness "github.com/moov-io/ach-test-harness"
"github.com/moov-io/ach-test-harness/pkg/batches"
"github.com/moov-io/ach-test-harness/pkg/entries"
"github.com/moov-io/ach-test-harness/pkg/response"
"github.com/moov-io/ach-test-harness/pkg/service"
Expand Down Expand Up @@ -37,6 +38,11 @@ func main() {
entryController := entries.NewEntryController(env.Logger, entryService)
entryController.AppendRoutes(env.Router)

batchRepository := batches.NewFTPRepository(env.Logger, env.Config.Servers.FTP)
batchService := batches.NewBatchService(batchRepository)
batchController := batches.NewBatchController(env.Logger, batchService)
batchController.AppendRoutes(env.Router)

fileWriter := response.NewFileWriter(env.Logger, env.Config.Servers, env.FTPServer)
fileTransformer := response.NewFileTransformer(env.Logger, env.Config, env.Config.Responses, fileWriter)
response.Register(env.Logger, env.Config.ValidateOpts, env.FTPServer, fileTransformer)
Expand Down
61 changes: 61 additions & 0 deletions pkg/batches/api_batch.go
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
}
141 changes: 141 additions & 0 deletions pkg/batches/api_batch_test.go
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")
})
}
106 changes: 106 additions & 0 deletions pkg/batches/repository_batch.go
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
}
63 changes: 63 additions & 0 deletions pkg/batches/repository_batch_test.go
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)
}
Loading

0 comments on commit c93d8ea

Please sign in to comment.