diff --git a/README.md b/README.md index 21f3d085..0ae73f24 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,11 @@ match: routingNumber: # Exact match of ABA routing number (RDFIIdentification and CheckDigit) traceNumber: # Exact match of TraceNumber entryType: # Checks TransactionCode. Accepted values: credit, debit or prenote. Also can be Nacha value (e.g. 27, 32) + + # Match on BatchHeader fields + companyIdentification: + companyEntryDescription: + # Matching will find at most two Actions in the config file order. One Copy Action and one Return/Correction Action. # Both actions will be executed if the Return/Correction Action has a delay. # Valid combinations include: diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md deleted file mode 100644 index 412952dc..00000000 --- a/docs/CONFIGURATION.md +++ /dev/null @@ -1,66 +0,0 @@ - -# ACH Test Harness -**[Purpose](README.md)** | **Configuration** | **[Running](RUNNING.md)** | **[Client](../pkg/client/README.md)** - ---- - -## Configuration -Custom configuration for this application may be specified via an environment variable `APP_CONFIG` to a configuration file that will be merged with the default configuration file. - -- [Default Configuration](../configs/config.default.yml) -- [Config Source Code](../pkg/service/model_config.go) -- Full Configuration - ```yaml - ACH Test Harness: - - # Service configurations - Servers: - - # Public service configuration - Public: - Bind: - # Address and port to listen on. - Address: ":8200" - - # Health/Admin service configuration. - Admin: - Bind: - # Address and port to listen on. - Address: ":8201" - - # All database configuration is done here. Only one connector can be configured. - Database: - - # Database name to use for selected connector. - DatabaseName: "identity" - - # MySql configuration - MySQL: - Address: tcp(mysqlidentity:3306) - User: identity - Password: identity - - # OR uses the sqllite db - SQLLite: - Path: ":memory:" - - # Gateway configuration to look up public keys to verify JWT tokens. - Gateway: - - # If neither http or file are specified, the service will generate random keys - Keys: - - # Pulls Keys from endpoints - HTTP: - URLs: - - http://tumbler:8204/.well-known/jwks.json - - # Pulls keys from the disk - File: - Paths: - - ./configs/gateway-jwks-sig-pub.json - - ``` - ---- -**[Next - Running](RUNNING.md)** diff --git a/docs/RUNNING.md b/docs/RUNNING.md deleted file mode 100644 index 8c864802..00000000 --- a/docs/RUNNING.md +++ /dev/null @@ -1,29 +0,0 @@ - -# ACH Test Harness -**[Purpose](README.md)** | **[Configuration](CONFIGURATION.md)** | **Running** | **[Client](../pkg/client/README.md)** - ---- - -## Running - -### Getting started - -More tutorials to come on how to use this as other pieces required to handle authorization are in place! - -- [Using docker-compose](#local-development) -- [Using our Docker image](#docker-image) - -No configuration is required to serve on `:8200` and metrics at `:8201/metrics` in Prometheus format. - -### Docker image - -You can download [our docker image `moov/ach-test-harness`](https://hub.docker.com/r/moov/ach-test-harness/) from Docker Hub or use this repository. - -### Local development - -``` -make run -``` - ---- -**[Next - Client](../pkg/client/README.md)** \ No newline at end of file diff --git a/docs/README.md b/docs/entry-search.md similarity index 62% rename from docs/README.md rename to docs/entry-search.md index 32d15e0a..06dd3872 100644 --- a/docs/README.md +++ b/docs/entry-search.md @@ -1,19 +1,7 @@ - -# ACH Test Harness -**Purpose** | **[Configuration](CONFIGURATION.md)** | **[Running](RUNNING.md)** | **[Client](../pkg/client/README.md)** - ---- - -## Purpose - -A configurable FTP/SFTP server and Go library to interactively test ACH scenarios to replicate real world originations, returns, changes, prenotes, and transfers. - -## Search +## Entry Search ach-test-harness offers search over the files, batches, and entries on the underlying filesystem. This is useful for automated testing as well as dashboards when used as a sandbox environment. -### Entries - ``` GET /entries?traceNumber=YYYYY ``` @@ -72,15 +60,3 @@ This endpoint will return the following response: } ] ``` - -## Getting help - - channel | info - ------- | ------- - [Project Documentation](https://github.com/moov-io/ach-test-harness/tree/master/docs/) | Our project documentation available online. -Twitter [@moov](https://twitter.com/moov) | You can follow Moov.io's Twitter feed to get updates on our project(s). You can also tweet us questions or just share blogs or stories. -[GitHub Issue](https://github.com/moov-io/ach-test-harness/issues) | If you are able to reproduce a problem please open a GitHub Issue under the specific project that caused the error. -[moov slack](https://slack.moov.io/) | Join our slack channel (`#ach-test-harness`) to have an interactive discussion about the development of the project. - ---- -**[Next - Configuration](CONFIGURATION.md)** diff --git a/pkg/response/file_transformer.go b/pkg/response/file_transformer.go index c302ba0b..9b112709 100644 --- a/pkg/response/file_transformer.go +++ b/pkg/response/file_transformer.go @@ -63,7 +63,7 @@ func (ft *FileTransfomer) Transform(ctx context.Context, file *ach.File) error { entries := file.Batches[i].GetEntries() for j := range entries { // Check if there's a matching Action and perform it. There may also be a future-dated action to execute. - copyAction, processAction := ft.Matcher.FindAction(entries[j]) + copyAction, processAction := ft.Matcher.FindAction(bh, entries[j]) if copyAction != nil { // Save this Entry mirror.saveEntry(&file.Batches[i], copyAction.Copy, entries[j]) diff --git a/pkg/response/match/matcher.go b/pkg/response/match/matcher.go index e06f8f6b..fd4161b1 100644 --- a/pkg/response/match/matcher.go +++ b/pkg/response/match/matcher.go @@ -28,7 +28,7 @@ func New(logger log.Logger, cfg service.Matching, responses []service.Response) } } -func (m Matcher) FindAction(ed *ach.EntryDetail) (copyAction *service.Action, processAction *service.Action) { +func (m Matcher) FindAction(bh *ach.BatchHeader, ed *ach.EntryDetail) (copyAction *service.Action, processAction *service.Action) { /* * See https://github.com/moov-io/ach-test-harness#config-schema for more details on how to configure. */ @@ -140,6 +140,26 @@ func (m Matcher) FindAction(ed *ach.EntryDetail) (copyAction *service.Action, pr } } + // BatchHeader fields + if matcher.CompanyIdentification != "" { + if matchedCompanyIdentification(matcher, bh) { + positiveMatchers = append(positiveMatchers, "CompanyIdentification") + positive++ + } else { + negativeMatchers = append(negativeMatchers, "CompanyIdentification") + negative++ + } + } + if matcher.CompanyEntryDescription != "" { + if matchedCompanyEntryDescription(matcher, bh) { + positiveMatchers = append(positiveMatchers, "CompanyEntryDescription") + positive++ + } else { + negativeMatchers = append(negativeMatchers, "CompanyEntryDescription") + negative++ + } + } + // format the list of negative and positive matchers for logging var b strings.Builder @@ -244,3 +264,11 @@ func matchedPrenote(m service.Match, ed *ach.EntryDetail) bool { func matchedIndividualName(m service.Match, ed *ach.EntryDetail) bool { return strings.TrimSpace(ed.IndividualName) == m.IndividualName } + +func matchedCompanyIdentification(m service.Match, bh *ach.BatchHeader) bool { + return strings.EqualFold(strings.TrimSpace(m.CompanyIdentification), strings.TrimSpace(bh.CompanyIdentification)) +} + +func matchedCompanyEntryDescription(m service.Match, bh *ach.BatchHeader) bool { + return strings.EqualFold(strings.TrimSpace(m.CompanyEntryDescription), strings.TrimSpace(bh.CompanyEntryDescription)) +} diff --git a/pkg/response/match/matcher_test.go b/pkg/response/match/matcher_test.go index 600e2407..ca9b9b8b 100644 --- a/pkg/response/match/matcher_test.go +++ b/pkg/response/match/matcher_test.go @@ -225,6 +225,10 @@ func TestMultiMatch(t *testing.T) { var actionDelayCorrection = actionCorrection actionDelayCorrection.Delay = &delay + bh := ach.NewBatchHeader() + bh.CompanyIdentification = "Classbook" + bh.CompanyEntryDescription = "Payment" + t.Run("No Match", func(t *testing.T) { var matcher Matcher matcher.Logger = log.NewTestLogger() @@ -238,12 +242,12 @@ func TestMultiMatch(t *testing.T) { entries := file.Batches[0].GetEntries() // Find our Action - copyAction, processAction := matcher.FindAction(entries[0]) + copyAction, processAction := matcher.FindAction(bh, entries[0]) require.Nil(t, copyAction) require.Nil(t, processAction) // Find our Action - copyAction, processAction = matcher.FindAction(entries[1]) + copyAction, processAction = matcher.FindAction(bh, entries[1]) require.Nil(t, copyAction) require.Nil(t, processAction) }) @@ -266,12 +270,12 @@ func TestMultiMatch(t *testing.T) { entries := file.Batches[0].GetEntries() // Find our Action - copyAction, processAction := matcher.FindAction(entries[0]) + copyAction, processAction := matcher.FindAction(bh, entries[0]) require.Nil(t, copyAction) require.Nil(t, processAction) // Find our Action - copyAction, processAction = matcher.FindAction(entries[1]) + copyAction, processAction = matcher.FindAction(bh, entries[1]) require.NotNil(t, copyAction) require.Equal(t, actionCopy, *copyAction) require.Nil(t, processAction) @@ -295,12 +299,12 @@ func TestMultiMatch(t *testing.T) { entries := file.Batches[0].GetEntries() // Find our Action - copyAction, processAction := matcher.FindAction(entries[0]) + copyAction, processAction := matcher.FindAction(bh, entries[0]) require.Nil(t, copyAction) require.Nil(t, processAction) // Find our Action - copyAction, processAction = matcher.FindAction(entries[1]) + copyAction, processAction = matcher.FindAction(bh, entries[1]) require.Nil(t, copyAction) require.NotNil(t, processAction) require.Equal(t, actionReturn, *processAction) @@ -332,15 +336,60 @@ func TestMultiMatch(t *testing.T) { entries := file.Batches[0].GetEntries() // Find our Action - copyAction, processAction := matcher.FindAction(entries[0]) + copyAction, processAction := matcher.FindAction(bh, entries[0]) require.Nil(t, copyAction) require.Nil(t, processAction) // Find our Action - copyAction, processAction = matcher.FindAction(entries[1]) + copyAction, processAction = matcher.FindAction(bh, entries[1]) require.NotNil(t, copyAction) require.Equal(t, actionCopy, *copyAction) require.NotNil(t, processAction) require.Equal(t, actionDelayCorrection, *processAction) }) + + t.Run("Match BatchHeader Fields", func(t *testing.T) { + var matcher Matcher + matcher.Logger = log.NewTestLogger() + matcher.Responses = []service.Response{} + + // Read our test file + file, err := ach.ReadFile(filepath.Join("..", "..", "..", "testdata", "20230809-144155-102000021.ach")) + require.NoError(t, err) + require.NotNil(t, file) + require.True(t, len(file.Batches) > 0) + + bh := file.Batches[0].GetHeader() + entries := file.Batches[0].GetEntries() + + // Match no entries + copyAction, processAction := matcher.FindAction(bh, entries[0]) + require.Nil(t, copyAction) + require.Nil(t, processAction) + + // Match based on CompanyID + matcher.Responses = append(matcher.Responses, service.Response{ + Match: service.Match{ + CompanyIdentification: "Classbook", + }, + Action: actionReturn, + }) + copyAction, processAction = matcher.FindAction(bh, entries[0]) + require.Nil(t, copyAction) + require.NotNil(t, processAction) + require.Equal(t, actionReturn, *processAction) + + // Match based on CompanyEntryDescription + matcher.Responses = nil + matcher.Responses = append(matcher.Responses, service.Response{ + Match: service.Match{ + CompanyEntryDescription: "Payment", + }, + Action: actionReturn, + }) + copyAction, processAction = matcher.FindAction(bh, entries[0]) + require.Nil(t, copyAction) + require.NotNil(t, processAction) + require.Equal(t, actionReturn, *processAction) + }) } diff --git a/pkg/service/model_config.go b/pkg/service/model_config.go index f00cb214..5a372e17 100644 --- a/pkg/service/model_config.go +++ b/pkg/service/model_config.go @@ -112,6 +112,9 @@ type Match struct { IndividualName string RoutingNumber string TraceNumber string + + CompanyIdentification string + CompanyEntryDescription string } func (m Match) Context() map[string]log.Valuer {