├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── codeql.yml │ ├── go.yml │ └── release.yml ├── .gitignore ├── .gitleaksignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── achgateway.moov.yml ├── assets.go ├── cmd └── achgateway │ └── main.go ├── configs ├── .gitignore ├── config.default.yml └── config.docker.yml ├── data └── .placeholder ├── docker-compose.yml ├── docs ├── .gitignore ├── 404.html ├── Gemfile ├── Gemfile.lock ├── _config.yml ├── _data │ ├── docs-menu.yml │ └── navigation.yml ├── _posts │ └── 2022-06-23-welcome-to-jekyll.markdown ├── api │ └── index.html ├── concepts │ ├── audit-trail.md │ ├── errors.md │ ├── events.md │ ├── notifications.md │ ├── odfi-files.md │ ├── shards.md │ ├── submission.md │ └── upload.md ├── config.md ├── favicon.png ├── goals.md ├── guides │ └── account-validation.md ├── images │ ├── OSS_Docs_Shard_Mapping.png │ ├── OSS_File_Submission.png │ ├── OSS_Merging_Process.png │ └── shards.svg ├── index.md ├── metrics.md ├── ops │ ├── cleanup.md │ ├── cutoffs.md │ ├── file-options.md │ └── merging.md ├── production │ └── checklist.md ├── runbooks.md └── usage │ ├── docker.md │ └── kubernetes.md ├── examples └── getting-started │ ├── conf │ └── kafka_server_jaas.conf │ ├── config.yml │ └── docker-compose.yml ├── go.mod ├── go.sum ├── internal ├── admin_config.go ├── admin_config_test.go ├── admintest │ └── admin.go ├── alerting │ ├── alerter.go │ ├── alerter_test.go │ ├── pagerduty.go │ ├── pagerduty_test.go │ ├── slack.go │ └── slack_test.go ├── audittrail │ ├── storage.go │ ├── storage_blob.go │ ├── storage_blob_test.go │ ├── storage_mock.go │ └── storage_test.go ├── dbtest │ └── sql.go ├── environment.go ├── environment_test.go ├── events │ ├── service.go │ ├── service_stream.go │ ├── service_stream_test.go │ ├── service_webhook.go │ └── service_webhook_test.go ├── files │ ├── mock_repo_files.go │ ├── model_accepted_file.go │ ├── repo_files.go │ └── repo_files_test.go ├── gpgx │ ├── keys.go │ └── testdata │ │ ├── key.priv │ │ └── key.pub ├── incoming │ ├── models.go │ ├── odfi │ │ ├── admin.go │ │ ├── audit.go │ │ ├── audit_test.go │ │ ├── cleanup.go │ │ ├── cleanup_test.go │ │ ├── corrections.go │ │ ├── corrections_test.go │ │ ├── download.go │ │ ├── download_test.go │ │ ├── incoming.go │ │ ├── incoming_test.go │ │ ├── mock_processor.go │ │ ├── mock_scheduler.go │ │ ├── prenotes.go │ │ ├── prenotes_test.go │ │ ├── processor.go │ │ ├── processor_test.go │ │ ├── reconciliation.go │ │ ├── reconciliation_test.go │ │ ├── returns.go │ │ ├── returns_test.go │ │ ├── scheduler.go │ │ ├── scheduler_test.go │ │ └── testdata │ │ │ ├── forward.ach │ │ │ ├── iat-credit.ach │ │ │ ├── prenote-ppd-debit.ach │ │ │ ├── recon.ach │ │ │ ├── return-no-batch-controls.ach │ │ │ └── return.ach │ ├── stream │ │ ├── publisher.go │ │ ├── publisher_test.go │ │ ├── streamtest │ │ │ └── streamtest.go │ │ └── subscription.go │ └── web │ │ ├── api_files.go │ │ └── api_files_test.go ├── kafka │ └── kafka.go ├── mask │ └── password.go ├── notify │ ├── email.go │ ├── email_test.go │ ├── helpers.go │ ├── helpers_test.go │ ├── kafka.go │ ├── mailslurper_test.go │ ├── mock_sender.go │ ├── multi.go │ ├── multi_test.go │ ├── notify.go │ ├── pagerduty.go │ ├── pagerduty_test.go │ ├── slack.go │ └── slack_test.go ├── output │ ├── base64.go │ ├── base64_test.go │ ├── encrypted.go │ ├── encrypted_test.go │ ├── format.go │ ├── format_test.go │ ├── nacha.go │ └── nacha_test.go ├── pipeline │ ├── aggregate.go │ ├── aggregate_test.go │ ├── cleanup.go │ ├── cleanup_test.go │ ├── events_api.go │ ├── events_api_test.go │ ├── file_receiver.go │ ├── file_receiver_test.go │ ├── manual_cutoff_times.go │ ├── manual_cutoff_times_test.go │ ├── merging.go │ ├── merging_test.go │ ├── metrics.go │ ├── mock_xfer_merging.go │ ├── pending_files_api.go │ ├── pipeline.go │ └── testdata │ │ ├── duplicate-trace.ach │ │ ├── ppd-debit.ach │ │ ├── ppd-debit.json │ │ ├── ppd-debit2.ach │ │ ├── ppd-debit3.ach │ │ └── ppd-debit4.ach ├── schedule │ ├── cutoff.go │ └── cutoff_test.go ├── server.go ├── service │ ├── client.go │ ├── config_test.go │ ├── model_admin.go │ ├── model_audit.go │ ├── model_audit_test.go │ ├── model_config.go │ ├── model_errors.go │ ├── model_events.go │ ├── model_inbound.go │ ├── model_notifications.go │ ├── model_sharding.go │ ├── model_sharding_test.go │ ├── model_tls.go │ ├── model_upload.go │ ├── model_upload_test.go │ ├── termination.go │ └── termination_test.go ├── shards │ ├── api_shard_mapping.go │ ├── api_shard_mapping_test.go │ ├── inmemory_repository.go │ ├── repository.go │ ├── repository_test.go │ ├── scope_shard_mapping_test.go │ ├── service_shard_mapping.go │ └── service_shard_mapping_test.go ├── sshx │ ├── keys.go │ ├── keys_test.go │ └── testdata │ │ ├── rsa-2048.pub │ │ └── rsa-4096.pub ├── storage │ ├── blob.go │ ├── buf.go │ ├── config.go │ ├── encryption.go │ ├── encryption_test.go │ ├── file.go │ ├── filesystem.go │ ├── filesystem_test.go │ ├── storage.go │ └── storage_test.go ├── test │ ├── cancel_test.go │ ├── download_test.go │ └── upload_test.go ├── transform │ ├── gpg.go │ ├── gpg_test.go │ ├── preupload.go │ └── testdata │ │ ├── moov.key │ │ └── moov.pub ├── upload │ ├── agent.go │ ├── agent_test.go │ ├── file.go │ ├── file_test.go │ ├── filename_template.go │ ├── filename_template_test.go │ ├── ftp.go │ ├── ftp_test.go │ ├── mock_agent.go │ ├── network_security.go │ ├── network_security_test.go │ ├── retry.go │ ├── sftp.go │ └── sftp_test.go └── util │ ├── timeout.go │ └── timeout_test.go ├── migrations ├── 000_noop.up.mysql.sql ├── 001_shard_mappings.up.mysql.sql ├── 001_shard_mappings.up.spanner.sql ├── 002_files.up.mysql.sql └── 002_files.up.spanner.sql ├── openapi.yaml ├── pkg ├── compliance │ ├── compliance.go │ ├── compliance_test.go │ ├── crypt.go │ ├── crypt_aes.go │ ├── crypt_aes_test.go │ ├── encode.go │ └── encode_test.go ├── models │ ├── events.go │ ├── events_test.go │ ├── model_transform.go │ ├── model_transform_test.go │ └── testdata │ │ └── partial-recon.ach └── test │ └── integration_test.go ├── renovate.json ├── testdata ├── HMBRAD_ACHEXPORT_1001_08_19_2022_09_10 ├── cor-c01.ach ├── download-test │ ├── inbound │ │ ├── cor-c01.ach │ │ └── iat-credit.ach │ ├── outbound │ │ └── .keep │ ├── reconciliation │ │ ├── .keep │ │ └── ppd-debit.ach │ └── returned │ │ └── return-WEB.ach ├── ftp-server │ ├── inbound │ │ ├── cor-c01.ach │ │ ├── iat-credit.ach │ │ └── prenote-ppd-debit.ach │ ├── outbound │ │ └── .keep │ ├── reconciliation │ │ ├── .keep │ │ └── ppd-debit.ach │ ├── returned │ │ └── return-WEB.ach │ └── scratch │ │ └── existing-file ├── ppd-debit.ach ├── ppd-debit2.ach ├── ppd-valid.json ├── return-WEB.ach ├── sftp-server │ ├── inbound │ │ ├── cor-c01.ach │ │ └── iat-credit.ach │ ├── outbound │ │ └── .keep │ ├── reconciliation │ │ ├── .keep │ │ └── ppd-debit.ach │ └── returned │ │ └── return-WEB.ach └── two-micro-deposits.ach └── version.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # generated-from:e05c3c97954dcd0556408f21c445612c0d12bc82316dccc853e15fd109bf238b DO NOT REMOVE, DO UPDATE 2 | 3 | # This is a comment. 4 | # Each line is a file pattern followed by one or more owners. 5 | 6 | # These owners will be the default owners for everything in 7 | # the repo. Unless a later match takes precedence, 8 | * @adamdecaf -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | # Why Are Changes Being Made 4 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # generated-from:d54019a30998e586e72aaea0fd977e77e59fb44cc41a59eed8d577acb16c06a9 DO NOT REMOVE, DO UPDATE 2 | 3 | name: CodeQL Analysis 4 | 5 | on: 6 | push: 7 | pull_request: 8 | schedule: 9 | - cron: '0 0 * * 0' 10 | 11 | jobs: 12 | CodeQL-Build: 13 | strategy: 14 | fail-fast: false 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Set up Go 1.x 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: 'stable' 21 | id: go 22 | 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Initialize CodeQL 29 | uses: github/codeql-action/init@v3 30 | with: 31 | languages: go 32 | 33 | - name: Install 34 | run: make install 35 | 36 | - name: Perform CodeQL Analysis 37 | uses: github/codeql-action/analyze@v3 38 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # generated-from:2b032db735b20164e9573e897cc0e43f41777de7e64c1a204fe2943d0d98999e DO NOT REMOVE, DO UPDATE 2 | 3 | name: Go 4 | 5 | on: 6 | push: 7 | branches: [ master ] 8 | pull_request: 9 | branches: [ master ] 10 | 11 | jobs: 12 | build: 13 | name: Go Build 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | steps: 19 | - name: Set up Go 1.x 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: 'stable' 23 | id: go 24 | 25 | - name: Check out code into the Go module directory 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | 30 | - name: Install make (Windows) 31 | if: runner.os == 'Windows' 32 | run: choco install -y make mingw 33 | 34 | - name: Install 35 | run: make install 36 | 37 | - name: Setup 38 | if: runner.os == 'Linux' 39 | run: make setup 40 | 41 | - name: Check 42 | if: runner.os == 'Linux' 43 | run: make check 44 | 45 | - name: Go Tests 46 | if: runner.os != 'Linux' 47 | run: go test ./... -count 1 -short 48 | 49 | - uses: actions/upload-artifact@v4 50 | if: ${{ always() }} 51 | with: 52 | name: "go-tooling-reports.zip" 53 | path: | 54 | ./**/coverage.txt 55 | ./**/mem.out 56 | ./**/cpu.out 57 | 58 | - name: Upload Code Coverage 59 | if: runner.os == 'Linux' 60 | run: bash <(curl -s https://codecov.io/bash) 61 | 62 | docker: 63 | name: Docker Build 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: Set up Go 1.x 67 | uses: actions/setup-go@v5 68 | with: 69 | go-version: 'stable' 70 | id: go 71 | 72 | - name: Check out code into the Go module directory 73 | uses: actions/checkout@v4 74 | with: 75 | fetch-depth: 0 76 | 77 | - name: Install 78 | run: make install 79 | 80 | - name: Docker Build 81 | if: runner.os == 'Linux' 82 | run: make dev-docker 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | /bin/ 18 | lint-project.sh 19 | /gitleaks.tar.gz 20 | misspell* 21 | staticcheck* 22 | coverage.txt 23 | 24 | pkged.go 25 | /vendor/ 26 | 27 | /storage/ 28 | /storage-*/ 29 | /testdata/ftp-server/outbound/*.ach 30 | /internal/test/storage/ 31 | /testdata/download-test/reconciliation/empty.txt 32 | 33 | .idea/ -------------------------------------------------------------------------------- /.gitleaksignore: -------------------------------------------------------------------------------- 1 | docs/concepts/events.md:generic-api-key:28 2 | docs/concepts/events.md:generic-api-key:44 3 | docs/concepts/upload.md:private-key:46 4 | internal/gpgx/testdata/key.priv:private-key:1 5 | internal/transform/testdata/moov.key:private-key:1 6 | internal/upload/sftp_test.go:private-key:306 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # generated-from:a1ae955dd09d1cf3d9d820d45ef6354f287ba25a14bff6f3643b446d7825f2b8 DO NOT REMOVE, DO UPDATE 2 | 3 | FROM golang:1.24 as builder 4 | WORKDIR /src 5 | ARG VERSION 6 | 7 | RUN apt-get update && apt-get upgrade -y && apt-get install -y make gcc g++ ca-certificates 8 | 9 | COPY . . 10 | 11 | RUN VERSION=${VERSION} make build 12 | 13 | FROM debian:stable-slim AS runtime 14 | LABEL maintainer="Moov " 15 | 16 | WORKDIR / 17 | 18 | RUN apt-get update && apt-get upgrade -y && apt-get install -y ca-certificates curl \ 19 | && rm -rf /var/lib/apt/lists/* 20 | 21 | COPY --from=builder /src/bin/achgateway /app/ 22 | 23 | ENV HTTP_PORT=8484 24 | ENV HEALTH_PORT=9494 25 | 26 | EXPOSE ${HTTP_PORT}/tcp 27 | EXPOSE ${HEALTH_PORT}/tcp 28 | 29 | HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ 30 | CMD curl -f http://localhost:${HEALTH_PORT}/live || exit 1 31 | 32 | VOLUME [ "/data", "/configs" ] 33 | 34 | ENTRYPOINT ["/app/achgateway"] 35 | -------------------------------------------------------------------------------- /achgateway.moov.yml: -------------------------------------------------------------------------------- 1 | ProjectPath: "." 2 | 3 | Project: 4 | # Github project id 5 | ProjectID: "achgateway" 6 | # Github org 7 | OrgID: "moov-io" 8 | # Human descriptive name for the project 9 | ProjectName: "ACH Gateway" 10 | Description: | 11 | An extensible, highly available, distributed, and fault tolerant ACH uploader and downloader. 12 | ACH Gateway creates events for outside services and transforms files prior to upload to fit real-world 13 | requirements of production systems. 14 | CodeOwners: "@adamdecaf" 15 | OpenSource: true 16 | 17 | MySQL: 18 | Port: 3306 19 | 20 | Templates: 21 | MoovProject: 22 | Run: true 23 | GoService: 24 | ServicePort: 8484 25 | HealthPort: 9494 26 | GoGithubActionsPublic: 27 | - Executable: achgateway 28 | -------------------------------------------------------------------------------- /assets.go: -------------------------------------------------------------------------------- 1 | package achgateway 2 | 3 | import "embed" 4 | 5 | //go:embed migrations/*.up.mysql.sql 6 | var MySqlMigrationFS embed.FS 7 | 8 | //go:embed migrations/*.up.spanner.sql 9 | var SpannerMigrationFS embed.FS 10 | 11 | //go:embed configs/config.default.yml 12 | var ConfigFS embed.FS 13 | -------------------------------------------------------------------------------- /cmd/achgateway/main.go: -------------------------------------------------------------------------------- 1 | // generated-from:9d2e1a7aff438bb75e877b034d21b525c8c10efee44288edf6ce935500a9fe76 DO NOT REMOVE, DO UPDATE 2 | 3 | // Licensed to The Moov Authors under one or more contributor 4 | // license agreements. See the NOTICE file distributed with 5 | // this work for additional information regarding copyright 6 | // ownership. The Moov Authors licenses this file to you under 7 | // the Apache License, Version 2.0 (the "License"); you may 8 | // not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | package main 21 | 22 | import ( 23 | "os" 24 | 25 | "github.com/moov-io/achgateway" 26 | "github.com/moov-io/achgateway/internal" 27 | "github.com/moov-io/achgateway/internal/service" 28 | "github.com/moov-io/base/log" 29 | ) 30 | 31 | func main() { 32 | env := &internal.Environment{ 33 | Logger: log.NewDefaultLogger().Set("app", log.String("achgateway")).Set("version", log.String(achgateway.Version)), 34 | } 35 | 36 | env, err := internal.NewEnvironment(env) 37 | if err != nil { 38 | env.Logger.Fatal().LogErrorf("Error loading up environment: %v", err) 39 | os.Exit(1) 40 | } 41 | defer env.Shutdown() 42 | 43 | termListener := service.NewTerminationListener() 44 | 45 | stopServers := env.RunServers(termListener) 46 | defer stopServers() 47 | 48 | if env.AdminServer != nil && env.ODFIFiles != nil { 49 | env.ODFIFiles.RegisterRoutes(env.AdminServer) 50 | } 51 | 52 | service.AwaitTermination(env.Logger, termListener) 53 | } 54 | -------------------------------------------------------------------------------- /configs/.gitignore: -------------------------------------------------------------------------------- 1 | # generated-from:f73353c8641cb28a45495571038c915eeda50df44848892cde56215068a3239f DO NOT REMOVE, DO UPDATE 2 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 3 | *.o 4 | *.a 5 | *.so 6 | 7 | # Folders 8 | _obj 9 | _test 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | bin/ 27 | *.db 28 | 29 | .idea/ 30 | .vscode/ 31 | 32 | /lint-project.sh 33 | misspell* 34 | staticcheck* 35 | coverage.txt 36 | 37 | pkger* 38 | pkged.go 39 | cover.out 40 | 41 | /vendor/ -------------------------------------------------------------------------------- /configs/config.default.yml: -------------------------------------------------------------------------------- 1 | # generated-from:8e00a406fe64314786fe91ca66cb20ee864e4f95ce4ec9f6611623fae7ea6db3 DO NOT REMOVE, DO UPDATE 2 | 3 | ACHGateway: 4 | Admin: 5 | BindAddress: ":9494" 6 | Telemetry: 7 | ServiceName: "achgateway" 8 | Inbound: 9 | HTTP: 10 | BindAddress: ":8484" 11 | # Database: 12 | # DatabaseName: "achgateway" 13 | # MySQL: 14 | # Address: "tcp(localhost:3306)" 15 | # User: "achgateway" 16 | # Password: "achgateway" 17 | -------------------------------------------------------------------------------- /configs/config.docker.yml: -------------------------------------------------------------------------------- 1 | # generated-from:396efe7e089b16f52703ded1570f07df8f3651f02a0cc2da02b5086a8d5a9417 DO NOT REMOVE, DO UPDATE 2 | 3 | # Add in specific configs for docker -------------------------------------------------------------------------------- /data/.placeholder: -------------------------------------------------------------------------------- 1 | generated-from:36c764f8ca003979597dfc691f8afbe88a21950a46a7348a29b04485c1aaa502 DO NOT REMOVE, DO UPDATE 2 | This is a template file so keep the directory added to git 3 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-cache 4 | .jekyll-metadata 5 | vendor 6 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /404.html 3 | layout: default 4 | --- 5 | 6 | 19 | 20 |
21 |

404

22 | 23 |

Page not found :(

24 |

The requested page could not be found.

25 |
26 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | # Hello! This is where you manage which Jekyll version is used to run. 3 | # When you want to use a different version, change it below, save the 4 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: 5 | # 6 | # bundle exec jekyll serve 7 | # 8 | # This will help ensure the proper Jekyll version is running. 9 | # Happy Jekylling! 10 | gem "jekyll", "~> 3.9" 11 | # This is the default theme for new Jekyll sites. You may change this to anything you like. 12 | gem "bulma-clean-theme" 13 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and 14 | # uncomment the line below. To upgrade, run `bundle update github-pages`. 15 | gem "github-pages", ">= 226", group: :jekyll_plugins 16 | # If you have any plugins, put them here! 17 | group :jekyll_plugins do 18 | gem "jekyll-feed", ">= 0.14" 19 | end 20 | 21 | # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem 22 | # and associated library. 23 | platforms :mingw, :x64_mingw, :mswin, :jruby do 24 | gem "tzinfo", "~> 1.2" 25 | gem "tzinfo-data" 26 | end 27 | 28 | # Performance-booster for watching directories on Windows 29 | gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] 30 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Jekyll! 2 | # 3 | # This config file is meant for settings that affect your whole blog, values 4 | # which you are expected to set up once and rarely edit after that. If you find 5 | # yourself editing this file very often, consider using Jekyll's data files 6 | # feature for the data you need to update frequently. 7 | # 8 | # For technical reasons, this file is *NOT* reloaded automatically when you use 9 | # 'bundle exec jekyll serve'. If you change this file, please restart the server process. 10 | # 11 | # If you need help with YAML syntax, here are some quick references for you: 12 | # https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml 13 | # https://learnxinyminutes.com/docs/yaml/ 14 | # 15 | # Site settings 16 | # These are used to personalize your new site. If you look in the HTML files, 17 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. 18 | # You can create any custom variable you would like, and they will be accessible 19 | # in the templates via {{ site.myvariable }}. 20 | 21 | title: Moov ACHGateway 22 | email: oss@moov.io 23 | description: >- # this means to ignore newlines until "baseurl:" 24 | Moov ACHGateway is an extensible, highly-available, distributed, and fault-tolerant ACH uploader and downloader. 25 | ACHGateway creates events for outside services and transforms files prior to upload to fit real-world requirements of production systems. 26 | url: "https://moov-io.github.io" # the base hostname & protocol for your site, e.g. http://example.com 27 | baseurl: "/achgateway" # the subpath of your site, e.g. /blog 28 | twitter_username: moov 29 | github_username: moov-io 30 | source_code: https://github.com/moov-io/achgateway 31 | permalink: pretty 32 | 33 | # Build settings 34 | remote_theme: moov-io/bulma-clean-theme 35 | plugins: 36 | - jekyll-feed 37 | - github-pages 38 | 39 | # Exclude from processing. 40 | # The following items will not be processed, by default. 41 | # Any item listed under the `exclude:` key here will be automatically added to 42 | # the internal "default list". 43 | # 44 | # Excluded items can be processed by explicitly listing the directories or 45 | # their entries' file path in the `include:` list. 46 | # 47 | # exclude: 48 | # - .sass-cache/ 49 | # - .jekyll-cache/ 50 | # - gemfiles/ 51 | # - Gemfile 52 | # - Gemfile.lock 53 | # - node_modules/ 54 | # - vendor/bundle/ 55 | # - vendor/cache/ 56 | # - vendor/gems/ 57 | # - vendor/ruby/ 58 | -------------------------------------------------------------------------------- /docs/_data/docs-menu.yml: -------------------------------------------------------------------------------- 1 | - label: Getting started 2 | items: 3 | - name: Overview 4 | link: / 5 | - name: Project Goals 6 | link: /goals/ 7 | 8 | - label: Usage 9 | items: 10 | - name: Docker 11 | link: /usage/docker/ 12 | # - name: Kubernetes 13 | # link: /usage/kubernetes/ 14 | - name: Configuration 15 | link: /config/ 16 | 17 | - label: Concepts 18 | items: 19 | - name: Submitting Files 20 | link: /concepts/submission/ 21 | - name: ODFI Files 22 | link: /concepts/odfi-files/ 23 | - name: Shards 24 | link: /concepts/shards/ 25 | - name: Upload Agents 26 | link: /concepts/upload/ 27 | - name: Audit Trail 28 | link: /concepts/audit-trail/ 29 | - name: Events 30 | link: /concepts/events/ 31 | - name: Errors 32 | link: /concepts/errors/ 33 | - name: Notifications 34 | link: /concepts/notifications/ 35 | 36 | - label: Use Cases 37 | items: 38 | - name: Account Validation 39 | link: /guides/account-validation/ 40 | 41 | - label: Operations 42 | items: 43 | - name: Cutoffs 44 | link: /ops/cutoffs/ 45 | - name: Merging 46 | link: /ops/merging/ 47 | - name: File Options 48 | link: /ops/file-options/ 49 | - name: File Cleanup 50 | link: /ops/cleanup/ 51 | 52 | - label: Production 53 | items: 54 | - name: Prometheus Metrics 55 | link: /metrics/ 56 | # - name: Runbooks 57 | # link: /runbooks/ 58 | - name: Checklist 59 | link: /production/checklist/ 60 | -------------------------------------------------------------------------------- /docs/_data/navigation.yml: -------------------------------------------------------------------------------- 1 | - name: API 2 | link: https://moov-io.github.io/achgateway/api/#overview 3 | - name: Community 4 | dropdown: 5 | - name: Awesome Fintech 6 | link: https://github.com/moov-io/awesome-fintech 7 | - name: Slack 8 | link: https://slack.moov.io/ 9 | - name: Terms Dictionary 10 | link: https://github.com/moov-io/terms-dictionary 11 | - name: Other Projects 12 | dropdown: 13 | - name: Moov ACH 14 | link: https://moov-io.github.io/ach/ 15 | - name: Moov ACH Test Harness 16 | link: https://github.com/moov-io/ach-test-harness 17 | - name: Moov Fed 18 | link: https://moov-io.github.io/fed/ 19 | - name: Moov FinCEN 20 | link: https://moov-io.github.io/fincen/ 21 | - name: Moov Image Cash Letter 22 | link: https://moov-io.github.io/imagecashletter/ 23 | - name: Moov Metro 2 24 | link: https://moov-io.github.io/metro2/ 25 | - name: Moov Watchman 26 | link: https://moov-io.github.io/watchman/ 27 | - name: Moov Wire 28 | link: https://moov-io.github.io/wire/ 29 | -------------------------------------------------------------------------------- /docs/_posts/2022-06-23-welcome-to-jekyll.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Welcome to Jekyll!" 4 | date: 2022-06-23 08:32:45 -0500 5 | categories: jekyll update 6 | --- 7 | You’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve`, which launches a web server and auto-regenerates your site when a file is updated. 8 | 9 | Jekyll requires blog post files to be named according to the following format: 10 | 11 | `YEAR-MONTH-DAY-title.MARKUP` 12 | 13 | Where `YEAR` is a four-digit number, `MONTH` and `DAY` are both two-digit numbers, and `MARKUP` is the file extension representing the format used in the file. After that, include the necessary front matter. Take a look at the source for this post to get an idea about how it works. 14 | 15 | Jekyll also offers powerful support for code snippets: 16 | 17 | {% highlight ruby %} 18 | def print_hi(name) 19 | puts "Hi, #{name}" 20 | end 21 | print_hi('Tom') 22 | #=> prints 'Hi, Tom' to STDOUT. 23 | {% endhighlight %} 24 | 25 | Check out the [Jekyll docs][jekyll-docs] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll Talk][jekyll-talk]. 26 | 27 | [jekyll-docs]: https://jekyllrb.com/docs/home 28 | [jekyll-gh]: https://github.com/jekyll/jekyll 29 | [jekyll-talk]: https://talk.jekyllrb.com/ 30 | -------------------------------------------------------------------------------- /docs/api/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Moov ACHGateway Endpoints 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/concepts/audit-trail.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Audit Trail 4 | hide_hero: true 5 | show_sidebar: false 6 | menubar: docs-menu 7 | --- 8 | 9 | ## Audit Trail 10 | 11 | Complying with Nacha regulations and ODFI requirements, ACHGateway ensures the retention of submitted files for a mandated duration. This feature is not just about regulatory compliance; it's a valuable tool for debugging and recreating specific files and entries, enhancing the reliability and traceability of transactions. ACHGateway achieves this through encryption and storage of these files in an S3-compatible storage layer. Additionally, Moov introduces the [ach-web-viewer project](https://github.com/moov-io/ach-web-viewer), a utility for browsing and displaying individual files, further simplifying audit and review processes. 12 | 13 | ### Pending Files 14 | 15 | 16 | ACHGateway facilitates the easy management of pending files through specific endpoints. These allow for the listing and retrieval of pending file contents, streamlining the process of handling ACH transactions before final submission. 17 | 18 | ``` 19 | GET /shards/{shardName}/files 20 | GET /shards/{shardName}/files/{filepath} 21 | ``` 22 | 23 | For more details on working with pending files, please visit the [pending file endpoints documentation](https://moov-io.github.io/achgateway/api/#tag--Operations). 24 | 25 | 26 | ### Organized Storage Layout 27 | 28 | To ensure efficient file management and retrieval, ACHGateway adopts a systematic approach to storing files within an S3-compatible bucket. The layout is designed to differentiate easily between files received from the ODFI and those prepared for upload, as detailed below. 29 | 30 | #### Received from the ODFI 31 | 32 | Files retrieved from the ODFI are stored as follows: 33 | 34 | ``` 35 | /odfi/$hostname/$yyyy-mm-dd/$filename 36 | ``` 37 | 38 | Example: `/odfi/sftp.bank.com/inbound/2022-01-17/BANK_ACH_DOWNLOAD_20220601_123051.ach` 39 | 40 | 41 | #### Uploaded to the ODFI 42 | 43 | Files intended for upload to the ODFI follow this layout: 44 | 45 | ``` 46 | /outbound/$hostname/$dir/$yyyy-mm-dd/$filename 47 | ``` 48 | 49 | Example: `/outbound/sftp.bank.com/2022-01-17/BANK_ACH_UPLOAD_20220601_123051.ach` 50 | 51 | This structured approach not only complies with regulatory requirements but also aids in the efficient management and retrieval of ACH files, supporting a more streamlined and secure transaction process. 52 | -------------------------------------------------------------------------------- /docs/concepts/errors.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Errors 4 | hide_hero: true 5 | show_sidebar: false 6 | menubar: docs-menu 7 | --- 8 | 9 | # Errors 10 | 11 | When ACHGateway encounters processing errors, it proactively notifies designated external systems. This mechanism is crucial for engaging human intervention for resolution or monitoring. While ACHGateway implements fundamental retry strategies for most errors, some issues may persist beyond automated recovery efforts. 12 | 13 | **Related Configuration**: Explore setting up [`Errors` notifications](../../config/#error-alerting) for detailed alert management. 14 | 15 | ## PagerDuty 16 | 17 | For critical incidents, PagerDuty alerts are generated, providing essential error details. Common triggers include issues related to file uploads, such as network failures or incorrect credentials, which typically necessitate manual intervention. 18 | 19 | ## Slack 20 | 21 | Slack channels receive notifications about problems like network disruptions or credential verification failures. These alerts aim to promptly inform team members, allowing for swift action to address the underlying issues. 22 | -------------------------------------------------------------------------------- /docs/concepts/events.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Events 4 | hide_hero: true 5 | show_sidebar: false 6 | menubar: docs-menu 7 | --- 8 | 9 | # Events 10 | 11 | As ACHGateway uploads and retrieves files with the remote servers it will emit events. These are defined in the [`models` package](https://pkg.go.dev/github.com/moov-io/achgateway/pkg/models) and include both Submission and ODFI events. 12 | 13 | Events can be dispatched via HTTP webhooks or through a supported streaming provider (e.g., Kafka), with all events being formatted in JSON. For added security, event data may also undergo optional encryption. The decryption and interpretation of these events are facilitated by the [`compliance` package](https://pkg.go.dev/github.com/moov-io/achgateway/pkg/compliance). 14 | 15 | **See Also**: Configure the [`Events` object](../../config/#eventing) 16 | 17 | ## Event Examples 18 | 19 | ### `FileUploaded` Event 20 | 21 | This event signifies the successful upload of an ACH file to the server: 22 | 23 | [Specification](https://pkg.go.dev/github.com/moov-io/achgateway/pkg/models#FileUploaded): 24 | 25 | ``` 26 | { 27 | "fileID": "2d05191f-381b-4e93-b8b4-b999f892a95a", 28 | "shardKey": "SD-bank1-live", 29 | "filename": "SD-BANK1-LIVE-20240201-111500-1.ach", 30 | "uploadedAt": "2009-11-10T23:00:00Z" 31 | } 32 | ``` 33 | 34 | ### `InvalidQueueFile` Event 35 | 36 | This event alerts to a problem with a file in the queue, such as a structural or validation error: 37 | 38 | [Specification](https://pkg.go.dev/github.com/moov-io/achgateway/pkg/models#InvalidQueueFile): 39 | 40 | ``` 41 | { 42 | "file": { 43 | "id": "01d5af6b-0f77-4976-b681-69947ccc9ea1", 44 | "shardKey": "SD-bank1-live", 45 | "file": { 46 | // ach.File JSON 47 | } 48 | }, 49 | "error": "batches out of order" 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/concepts/notifications.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Notifications 4 | hide_hero: true 5 | show_sidebar: false 6 | menubar: docs-menu 7 | --- 8 | 9 | # Notifications 10 | 11 | ACHGateway enhances operational transparency by generating notifications for file uploads, providing teams and ODFIs with immediate insights into ACH activities. 12 | 13 | ## Email 14 | 15 | Emails serve as a critical communication channel, especially for ODFIs that correlate notifications with ACH file uploads to avoid manual processing errors or accidental uploads. 16 | 17 | Subject: `BANK_ACH_UPLOAD_20220601_123051.ach uploaded by Company` 18 | 19 | ``` 20 | A file has been uploaded to sftp.bank.com - BANK_ACH_UPLOAD_20220601_123051.ach 21 | Name: Your Company 22 | Debits: 100.01 23 | Credits: 100.01 24 | 25 | Batches: 1 26 | Total Entries: 24 27 | ``` 28 | 29 | ## Slack 30 | 31 | Slack notifications offer a real-time, asynchronous method to report on the status of file uploads, enabling teams to stay informed about successful operations and address failures promptly. 32 | 33 | **Success Notification:** 34 | 35 | ``` 36 | SUCCESSFUL upload of BANK_ACH_UPLOAD_20220601_123051.ach to sftp.bank.com:22 with ODFI server 37 | 8 entries | Debits: 3,442.66 | Credits: 3,442.66 38 | ``` 39 | 40 | **Failure Notification:** 41 | 42 | ``` 43 | FAILED upload of BANK_ACH_UPLOAD_20220601_123051.ach to sftp.bank.com:22 with ODFI server 44 | 2 entries | Debits: 31.03 | Credits: 31.03 45 | ``` 46 | 47 | ## PagerDuty 48 | 49 | PagerDuty notifications ensure that high-priority issues are immediately brought to the attention of the responsible parties, enabling rapid response and resolution to maintain seamless ACH file processing operations. 50 | -------------------------------------------------------------------------------- /docs/concepts/upload.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Upload Agents 4 | hide_hero: true 5 | show_sidebar: false 6 | menubar: docs-menu 7 | --- 8 | 9 | # Upload Agents 10 | 11 | ACHGateway facilitates the secure and efficient uploading of ACH files to financial institutions (FIs) using protocols such as FTP(s) ([File Transport Protocol](https://en.wikipedia.org/wiki/File_Transfer_Protocol) with TLS) or SFTP ([SSH File Transfer Protocol](https://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol)). The system adheres to specific filename conventions, for example `BANKNAME_20181222_301234567.ach`. These operations are fully configurable within the ACHGateway's settings. 12 | 13 | **Further Reading**: See how to configure the [`Upload` object](../../config/#upload-agents) for detailed upload instructions. 14 | 15 | ## IP Whitelisting for Enhanced Security 16 | 17 | To bolster security during the upload process to the ODFI server, ACHGateway supports IP whitelisting. This feature ensures the server's hostname resolves only to pre-approved IP addresses or CIDR ranges, offering a safeguard against DNS poisoning or routing errors. 18 | 19 | ### Configuring `UploadAgent`'s `AllowedIPs`: 20 | 21 | - Specific IP Address: `35.211.43.9` 22 | - CIDR Range: `10.4.0.0/16` 23 | - Multiple Values: `10.1.0.12,10.3.0.0/16` 24 | 25 | This configuration helps enforce strict network controls, providing an additional layer of security. 26 | 27 | ## SFTP Host and Client Key Verification 28 | 29 | For secure SFTP file uploads, ACHGateway can verify the host key of the remote server and utilize a client key for mutual authentication. This double-layered approach ensures a trusted connection between ACHGateway and the remote server before any file transfer occurs. 30 | 31 | ### Remote Server's Host Key Configuration: 32 | 33 | **Public Key** (in SSH Authorized Key Format): 34 | 35 | ``` 36 | SFTP Config: HostPublicKey 37 | Format: ssh-rsa AAAAB...wwW95ttP3pdwb7Z computer-hostname 38 | ``` 39 | 40 | **Private Key** (PKCS#8) 41 | 42 | ``` 43 | SFTP Config: ClientPrivateKey 44 | 45 | Format: 46 | -----BEGIN RSA PRIVATE KEY----- 47 | ... 48 | 33QwOLPLAkEA0NNUb+z4ebVVHyvSwF5jhfJxigim+s49KuzJ1+A2RaSApGyBZiwS 49 | ... 50 | -----END RSA PRIVATE KEY----- 51 | ``` 52 | 53 | Note: Public and private keys can either be directly used in their original formats or encoded in base64, adhering to Go's `base64.StdEncoding` (not URL encoding). This flexibility allows for secure and adaptable key management in line with your security protocols. 54 | -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moov-io/achgateway/5afe04b3026a1b0dfebd059810c4cec849e3936e/docs/favicon.png -------------------------------------------------------------------------------- /docs/guides/account-validation.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Account Validation 4 | hide_hero: true 5 | show_sidebar: false 6 | menubar: docs-menu 7 | --- 8 | 9 | # micro-deposits 10 | 11 | Validating accounts is often done with two small credits submitted to a bank account and those amounts verified by a user. The experience can be improved by originating same-day batches so the amounts settle quickly. To implement this submission of ACH files with a shard key of `micro-deposit` could be used and configured for your ODFIs Same-Day processing windows. 12 | 13 | The following are entry detail records you could create and submit to ACHGateway: 14 | 15 | | Entry Type | Bank Account | Amount (in cents) | 16 | |---|---|---| 17 | | Debit | Your Company Checking | 7 | 18 | | Credit | Customer Checking | 3 | 19 | | Credit | Customer Checking | 4 | 20 | 21 | Putting those EntryDetail records into a CCD or WEB batch: 22 | 23 | | Batch Number | SEC Code | Service Class Code | Company Name | Identification | EntryDescription | EffectiveEntryDate | 24 | |---|---|---|---|---|---|---| 25 | | 1 | WEB | 200 (Mixed Debits and Credits) | Your Startup | CORPTESTER | Acct Verify | 220627 | 26 | 27 | See also: [Example WEB file creation](https://github.com/moov-io/ach/blob/master/examples/example_webWrite_credit_test.go) 28 | -------------------------------------------------------------------------------- /docs/images/OSS_Docs_Shard_Mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moov-io/achgateway/5afe04b3026a1b0dfebd059810c4cec849e3936e/docs/images/OSS_Docs_Shard_Mapping.png -------------------------------------------------------------------------------- /docs/images/OSS_File_Submission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moov-io/achgateway/5afe04b3026a1b0dfebd059810c4cec849e3936e/docs/images/OSS_File_Submission.png -------------------------------------------------------------------------------- /docs/images/OSS_Merging_Process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moov-io/achgateway/5afe04b3026a1b0dfebd059810c4cec849e3936e/docs/images/OSS_Merging_Process.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Overview 4 | hide_hero: true 5 | show_sidebar: false 6 | menubar: docs-menu 7 | --- 8 | 9 | # Overview 10 | 11 | Moov’s mission is to give developers an easy way to create and integrate bank processing into their own software products. Our open source projects are each focused on solving a single responsibility in financial services and designed around performance, scalability, and ease of use. 12 | 13 | ACHGateway is an extensible, highly-available, distributed, and fault-tolerant Nacha ACH uploader and downloader. ACHGateway facilitates the uploading and downloading of ACH files, generating events for external services and modifying files to meet the practical demands of production environments. For an introduction to ACH [refer to our guide](https://moov-io.github.io/ach/intro/). 14 | 15 | Payment systems need to be adaptable for all kinds of uses and this is the primary goal of ACHGateway. Typically businesses submit ACH files (in their Nacha format) to initiate debits/credits against bank accounts. An Originating Depository Financial Institution (ODFI) can be your legal and financial partner. ACHGateway can optimize and streamline the integration to your ODFI. 16 | 17 | Next: [Project Goals](./goals/) or [Submit a File](./concepts/submission/) 18 | 19 | ## Project status 20 | 21 | ACHGateway is trusted and utilized in production environments across various companies, achieving a stable milestone. Our current focus is on refining ACHGateway's configuration based on user feedback. We invite you to contribute your experiences and suggestions to help us enhance the project. 22 | 23 | ## Getting help 24 | 25 | channel | info 26 | ------- | ------- 27 | [GitHub Issue](https://github.com/moov-io/achgateway/issues) | If you are able to reproduce a problem please open a GitHub Issue under the specific project that caused the error. 28 | [moov-io slack](https://slack.moov.io/) | Join our slack channel (`#ach`) to have an interactive discussion about the development of the project. 29 | -------------------------------------------------------------------------------- /docs/metrics.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Prometheus Metrics 4 | hide_hero: true 5 | show_sidebar: false 6 | menubar: docs-menu 7 | --- 8 | 9 | # Prometheus Metrics 10 | 11 | ACHGateway emits Prometheus metrics on the admin HTTP server at `/metrics`. Typically [Alertmanager](https://github.com/prometheus/alertmanager) is set up to aggregate the metrics and alert teams. 12 | 13 | There is a [`GET :9494/metrics` endpoint](https://github.com/moov-io/base/tree/master/admin#endpoints) that serves all Prometheus metrics produced. 14 | 15 | ### HTTP Server 16 | 17 | - `http_response_duration_seconds`: Histogram representing the http response durations 18 | 19 | ### Database 20 | 21 | - `mysql_connections`: How many MySQL connections and what status they're in. 22 | - `sqlite_connections`: How many sqlite connections and what status they're in. 23 | 24 | ### ODFI Files 25 | 26 | - `correction_codes_processed`: Counter of correction (COR/NOC) files processed 27 | - `files_downloaded`: Counter of files downloaded from a remote server 28 | - `missing_return_transfers`: Counter of return EntryDetail records handled without a fund transfer 29 | - `prenote_entries_processed`: Counter of prenote EntryDetail records processed 30 | - `return_entries_processed`: Counter of return EntryDetail records processed 31 | 32 | 33 | ## Incoming Files 34 | 35 | - `incoming_http_files`: Counter of ACH files submitted through the http interface 36 | - `incoming_stream_files`: Counter of ACH files received through stream interface 37 | - `http_file_processing_errors`: Counter of http submitted ACH files that failed processing 38 | - `stream_file_processing_errors`: Counter of stream submitted ACH files that failed processing 39 | 40 | ## Outbound Files 41 | 42 | - `pending_files`: Counter of ACH files waiting to be uploaded 43 | - `files_missing_shard_aggregators`: Counter of ACH files unable to be matched with a shard aggregator 44 | - `ach_uploaded_files`: Counter of ACH files uploaded through the pipeline to the ODFI 45 | - `ach_upload_errors`: Counter of errors encountered when attempting ACH files upload 46 | 47 | ### Remote File Servers 48 | 49 | - `ftp_agent_up`: Status of FTP agent connection 50 | - `sftp_agent_up`: Status of SFTP agent connection 51 | -------------------------------------------------------------------------------- /docs/ops/cutoffs.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Cutoffs 4 | hide_hero: true 5 | show_sidebar: false 6 | menubar: docs-menu 7 | --- 8 | 9 | # Cutoff Times 10 | 11 | A cutoff time is a wall-clock time that ACH files must be delivered to the Federal Reserve. As ACH is a batch payment method these cutoff times are the key component to batching payments. The Federal Reserve [publishes their Processing Schedule](https://www.frbservices.org/resources/resource-centers/same-day-ach/fedach-processing-schedule.html) but ODFIs typically require uploads 15-30mins prior to the Federal Reserve window. The Federal Reserve also [publishes a list of holidays](https://www.frbservices.org/about/holiday-schedules) where processing does not occur. 12 | 13 | Example with 30-min ODFI deadline 14 | 15 | | Schedule | ODFI Deadline | Fed Deadline | Target Distribution | Settlement Schedule | 16 | |----|----|----|----|----| 17 | | Same-Day | 2:15pm ET | 2:45pm ET | 4:00pm ET | 5:00pm ET | 18 | | Future Date | 4:15pm ET | 4:45pm ET | 5:30 pm ET | 8:30 am ET (Next Day) | 19 | 20 | ## Developers 21 | 22 | Moov publishes [a `Time` object in moov-io/base](https://pkg.go.dev/github.com/moov-io/base?utm_source=godoc#Time) to assist with calculating banking days and when holidays are observed. There is also a [`bankcron` Docker image](https://github.com/moov-io/bankcron) for running tasks only on banking days. 23 | 24 | ## Manual Triggers 25 | 26 | ACHGateway supports manually triggering inbound or cutoff processing. A list of shards can be specified or all shards can be triggered. 27 | 28 | ### Flushing ACH Files 29 | 30 | There is an endpoint to initiate cutoff processing as if a window has approached. This involves merging transfers into files, upload attempts, and audit trail storage. 31 | 32 | ``` 33 | $ curl -XPUT http://localhost:9494/trigger-cutoff --data '{"shardNames":["testing"]}' 34 | { 35 | "shards": { 36 | "testing": null, 37 | "SD-live": "ERROR: unknown host" 38 | } 39 | } 40 | ``` 41 | 42 | ### Processing ODFI Files 43 | 44 | There is an endpoint to initiate processing of ODFI files which could be incoming transfers, returned files, corrected files, and pre-notifications. 45 | 46 | ``` 47 | $ curl -XPUT http://localhost:9494/trigger-inbound 48 | { 49 | "shards": { 50 | "testing": null, 51 | "SD-live": "ERROR: unknown host" 52 | } 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/ops/file-options.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: File Options 4 | hide_hero: true 5 | show_sidebar: false 6 | menubar: docs-menu 7 | --- 8 | 9 | # File Options 10 | 11 | When submitting files to ACHGateway there may be requirements which break Nacha's specification for field formats and values. These may be static values your ODFI requires or custom codes/rules that ehnance your product. The moov-io/ach library supports [custom validation options](https://moov-io.github.io/ach/custom-validation/) and the full [set of options is supported by ACHGateway](https://pkg.go.dev/github.com/moov-io/ach?utm_source=godoc#ValidateOpts). 12 | 13 | ### Events 14 | 15 | Using the Go package in [achgateway's `models` package](https://pkg.go.dev/github.com/moov-io/achgateway/pkg/models) each event has a method to set `ValidateOpts` on the file. This will be carried through the File for use during merge and upload. Submissions should contain the same `ValidateOpts` to ensure the merged files have the correct overrides. 16 | 17 | `func (Event) SetValidation(opts *ach.ValidateOpts)` 18 | 19 | #### Errors 20 | 21 | If you encounter the following errors you should verify that events sent to ACHGateway are wrapped properly. Try verifying the output of wrapping `pkg/models.Event`'s `MarshalJSON` around your specific events. 22 | 23 | ``` 24 | nil pubsub message 25 | ``` 26 | ``` 27 | unhandled message 28 | ``` 29 | 30 | Example: 31 | ``` 32 | { 33 | "type": "QueueACHFile", 34 | "event": { 35 | "fileID": "uuid", 36 | "shardKey": "uuid", 37 | "file": { 38 | ... 39 | } 40 | } 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/runbooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Runbooks 4 | hide_hero: true 5 | show_sidebar: false 6 | menubar: docs-menu 7 | --- 8 | -------------------------------------------------------------------------------- /docs/usage/kubernetes.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Usage | Kubernetes 4 | hide_hero: true 5 | show_sidebar: false 6 | menubar: docs-menu 7 | --- 8 | 9 | # Kubernetes 10 | 11 | TODO(adam): Helm chart 12 | https://github.com/moov-io/charts/tree/master/stable 13 | -------------------------------------------------------------------------------- /examples/getting-started/conf/kafka_server_jaas.conf: -------------------------------------------------------------------------------- 1 | KafkaServer { 2 | org.apache.kafka.common.security.plain.PlainLoginModule required 3 | username="admin" 4 | password="secret" 5 | 6 | user_admin="secret"; 7 | }; 8 | Client {}; -------------------------------------------------------------------------------- /examples/getting-started/config.yml: -------------------------------------------------------------------------------- 1 | ACHGateway: 2 | Admin: 3 | BindAddress: ":9494" 4 | Inbound: 5 | HTTP: 6 | BindAddress: ":8484" 7 | # Transform: 8 | # Encryption: 9 | # AES: 10 | # Key: "secret" 11 | Kafka: 12 | brokers: 13 | - "kafka1:9092" 14 | key: "admin" 15 | secret: "secret" 16 | group: "achgateway" 17 | tls: false 18 | topic: "ach.outgoing-files" 19 | ODFI: 20 | Interval: "1m" 21 | Processors: 22 | Corrections: 23 | Enabled: true 24 | Prenotes: 25 | Enabled: true 26 | Returns: 27 | Enabled: true 28 | ShardNames: 29 | - "testing" 30 | Storage: 31 | Directory: "./storage/" 32 | CleanupLocalDirectory: true 33 | Events: 34 | Stream: 35 | Kafka: 36 | Brokers: 37 | - "kafka1:9092" 38 | Key: admin 39 | Secret: secret 40 | TLS: false 41 | Topic: "ach.odfi-file-events" 42 | Transform: 43 | Encoding: 44 | Base64: true 45 | Sharding: 46 | Shards: 47 | - name: "testing" 48 | cutoffs: 49 | timezone: "America/Los_Angeles" 50 | windows: 51 | - "10:30" 52 | - "14:00" 53 | uploadAgent: "local-ftp" 54 | output: 55 | format: "nacha" 56 | Mappings: 57 | - shardKey: "foo" # Could be random value (UUID, fixed string) 58 | shardName: "testing" # Maps to Sharding.Shards[_].name 59 | Default: "testing" 60 | Upload: 61 | agents: 62 | - id: "local-ftp" 63 | ftp: 64 | hostname: "ftp:2121" 65 | username: "admin" 66 | password: "123456" 67 | paths: 68 | inbound: "/inbound/" 69 | outbound: "/outbound/" 70 | reconciliation: "/reconciliation/" 71 | return: "/returned/" 72 | merging: 73 | directory: "./storage/" 74 | cleanup: 75 | enabled: false # Set to true to enable automatic cleanup 76 | retentionDuration: "24h" # Keep files for 24 hours after processing 77 | checkInterval: "1h" # Check for files to clean up every hour 78 | -------------------------------------------------------------------------------- /internal/admin_config.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package internal 19 | 20 | import ( 21 | "encoding/json" 22 | "net/http" 23 | ) 24 | 25 | func (env *Environment) registerConfigRoute() { 26 | env.AdminServer.AddHandler("/config", env.configRouteHandler()) 27 | } 28 | 29 | func (env *Environment) configRouteHandler() http.HandlerFunc { 30 | return func(w http.ResponseWriter, r *http.Request) { 31 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 32 | w.WriteHeader(http.StatusOK) 33 | json.NewEncoder(w).Encode(env.Config) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/admin_config_test.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package internal 19 | 20 | import ( 21 | "net/http" 22 | "testing" 23 | 24 | "github.com/moov-io/achgateway/internal/service" 25 | 26 | "github.com/gorilla/mux" 27 | "github.com/stretchr/testify/require" 28 | ) 29 | 30 | func TestAdminConfig(t *testing.T) { 31 | if testing.Short() { 32 | t.Skip("-short flag specified") 33 | } 34 | 35 | r := mux.NewRouter() 36 | env := NewTestEnvironment(t, r) 37 | t.Cleanup(env.Shutdown) 38 | 39 | env.RunServers(service.NewTerminationListener()) 40 | env.registerConfigRoute() 41 | 42 | req, err := http.NewRequest("GET", "http://"+env.AdminServer.BindAddr()+"/config", nil) 43 | require.NoError(t, err) 44 | 45 | resp, err := http.DefaultClient.Do(req) 46 | require.NoError(t, err) 47 | require.NotNil(t, resp) 48 | require.Equal(t, http.StatusOK, resp.StatusCode) 49 | 50 | if resp.Body != nil { 51 | resp.Body.Close() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/admintest/admin.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package admintest 19 | 20 | import ( 21 | "testing" 22 | 23 | "github.com/moov-io/base/admin" 24 | 25 | "github.com/stretchr/testify/require" 26 | ) 27 | 28 | func Server(t *testing.T) *admin.Server { 29 | t.Helper() 30 | 31 | adminServer, err := admin.New(admin.Opts{ 32 | Addr: ":0", 33 | }) 34 | require.NoError(t, err) 35 | require.NotNil(t, adminServer) 36 | 37 | go adminServer.Listen() 38 | 39 | t.Cleanup(func() { 40 | adminServer.Shutdown() 41 | }) 42 | 43 | return adminServer 44 | } 45 | -------------------------------------------------------------------------------- /internal/alerting/alerter.go: -------------------------------------------------------------------------------- 1 | package alerting 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/moov-io/achgateway/internal/service" 7 | ) 8 | 9 | type Alerters []Alerter 10 | 11 | type Alerter interface { 12 | AlertError(err error) error 13 | } 14 | 15 | type MockAlerter struct{} 16 | 17 | func (mn *MockAlerter) AlertError(e error) error { 18 | return nil 19 | } 20 | 21 | func NewAlerters(cfg service.ErrorAlerting) (Alerters, error) { 22 | var alerters []Alerter 23 | 24 | if cfg.Slack != nil { 25 | alerter, err := NewSlackAlerter(cfg.Slack) 26 | if err != nil { 27 | return nil, err 28 | } 29 | alerters = append(alerters, alerter) 30 | } 31 | 32 | if cfg.PagerDuty != nil { 33 | alerter, err := NewPagerDutyAlerter(cfg.PagerDuty) 34 | if err != nil { 35 | return nil, err 36 | } 37 | alerters = append(alerters, alerter) 38 | } 39 | 40 | if len(alerters) == 0 { 41 | return []Alerter{&MockAlerter{}}, nil 42 | } 43 | 44 | return alerters, nil 45 | } 46 | 47 | func (s Alerters) AlertError(e error) error { 48 | if e == nil { 49 | return nil 50 | } 51 | 52 | for _, alerter := range s { 53 | err := alerter.AlertError(e) 54 | if err != nil { 55 | return fmt.Errorf("alerting error: %v", err) 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /internal/alerting/alerter_test.go: -------------------------------------------------------------------------------- 1 | package alerting 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "testing" 7 | 8 | "github.com/moov-io/achgateway/internal/service" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestNewAlertersPagerDuty(t *testing.T) { 13 | if os.Getenv("PD_API_KEY") == "" { 14 | t.Skip("Skip TestNewAlertersPagerDuty as PD_API_KEY is not set") 15 | } 16 | var cfg service.ErrorAlerting 17 | var alerters []Alerter 18 | var err error 19 | 20 | cfg = service.ErrorAlerting{ 21 | PagerDuty: &service.PagerDutyAlerting{ 22 | ApiKey: os.Getenv("PD_API_KEY"), 23 | RoutingKey: os.Getenv("PD_ROUTING_KEY"), 24 | }, 25 | } 26 | 27 | alerters, err = NewAlerters(cfg) 28 | require.NoError(t, err) 29 | require.Len(t, alerters, 1) 30 | } 31 | 32 | func TestNewAlertersSlack(t *testing.T) { 33 | if os.Getenv("SLACK_ACCESS_TOKEN") == "" { 34 | t.Skip("Skip TestNewAlertersSlack as SLACK_ACCESS_TOKEN is not set") 35 | } 36 | var cfg service.ErrorAlerting 37 | var alerters []Alerter 38 | var err error 39 | 40 | cfg = service.ErrorAlerting{ 41 | Slack: &service.SlackAlerting{ 42 | AccessToken: os.Getenv("SLACK_ACCESS_TOKEN"), 43 | ChannelID: os.Getenv("SLACK_CHANNEL_ID"), 44 | }, 45 | } 46 | 47 | alerters, err = NewAlerters(cfg) 48 | require.NoError(t, err) 49 | require.Len(t, alerters, 1) 50 | } 51 | 52 | func TestNewAlertersPagerDutyAndSlack(t *testing.T) { 53 | if os.Getenv("PD_API_KEY") == "" && os.Getenv("SLACK_ACCESS_TOKEN") == "" { 54 | t.Skip("Skip as PD_API_KEY and SLACK_ACCESS_TOKEN are not set") 55 | } 56 | var cfg service.ErrorAlerting 57 | var alerters Alerters 58 | var err error 59 | 60 | cfg = service.ErrorAlerting{ 61 | PagerDuty: &service.PagerDutyAlerting{ 62 | ApiKey: os.Getenv("PD_API_KEY"), 63 | RoutingKey: os.Getenv("PD_ROUTING_KEY"), 64 | }, 65 | Slack: &service.SlackAlerting{ 66 | AccessToken: os.Getenv("SLACK_ACCESS_TOKEN"), 67 | ChannelID: os.Getenv("SLACK_CHANNEL_ID"), 68 | }, 69 | } 70 | 71 | alerters, err = NewAlerters(cfg) 72 | require.NoError(t, err) 73 | require.Len(t, alerters, 2) 74 | 75 | err = alerters.AlertError(errors.New("error message")) 76 | require.NoError(t, err) 77 | } 78 | -------------------------------------------------------------------------------- /internal/alerting/pagerduty_test.go: -------------------------------------------------------------------------------- 1 | package alerting 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "testing" 7 | 8 | "github.com/moov-io/achgateway/internal/service" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestPagerDutyErrorAlert(t *testing.T) { 13 | if os.Getenv("PD_API_KEY") == "" { 14 | t.Skip("Skip PagerDuty notification as PD_API_KEY and PD_ROUTING_KEY are not set") 15 | } 16 | 17 | cfg := &service.PagerDutyAlerting{ 18 | ApiKey: os.Getenv("PD_API_KEY"), 19 | RoutingKey: os.Getenv("PD_ROUTING_KEY"), 20 | } 21 | 22 | notifier, err := NewPagerDutyAlerter(cfg) 23 | require.NoError(t, err) 24 | require.NotNil(t, notifier) 25 | 26 | err = notifier.AlertError(errors.New("error message")) 27 | require.NoError(t, err) 28 | } 29 | -------------------------------------------------------------------------------- /internal/alerting/slack.go: -------------------------------------------------------------------------------- 1 | package alerting 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/moov-io/achgateway/internal/service" 8 | "github.com/slack-go/slack" 9 | ) 10 | 11 | type Slack struct { 12 | accessToken string 13 | channelID string 14 | client *slack.Client 15 | } 16 | 17 | func NewSlackAlerter(cfg *service.SlackAlerting) (*Slack, error) { 18 | notifier := &Slack{ 19 | accessToken: cfg.AccessToken, 20 | channelID: cfg.ChannelID, 21 | client: slack.New(cfg.AccessToken), 22 | } 23 | if err := notifier.AuthTest(); err != nil { 24 | return nil, err 25 | } 26 | return notifier, nil 27 | } 28 | 29 | func (s *Slack) AlertError(e error) error { 30 | if e == nil { 31 | return nil 32 | } 33 | 34 | _, _, err := s.client.PostMessage( 35 | s.channelID, 36 | slack.MsgOptionText(fmt.Sprintf("%v", e), false), 37 | slack.MsgOptionAsUser(false), 38 | ) 39 | if err != nil { 40 | return fmt.Errorf("sending slack message: %v", err) 41 | } 42 | 43 | return nil 44 | } 45 | 46 | func (s *Slack) AuthTest() error { 47 | if s == nil || s.client == nil { 48 | return errors.New("slack: nil or no slack client") 49 | } 50 | 51 | // make a call and verify we don't error 52 | resp, err := s.client.AuthTest() 53 | if err != nil { 54 | return fmt.Errorf("slack auth test: %v", err) 55 | } 56 | if resp.UserID == "" { 57 | return errors.New("slack: missing user_id") 58 | } 59 | 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /internal/alerting/slack_test.go: -------------------------------------------------------------------------------- 1 | package alerting 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "testing" 7 | 8 | "github.com/moov-io/achgateway/internal/service" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestSlackErrorAlert(t *testing.T) { 13 | if os.Getenv("SLACK_ACCESS_TOKEN") == "" { 14 | t.Skip("Skip Slack notification as SLACK_ACCESS_TOKEN and SLACK_CHANNEL_ID are not set") 15 | } 16 | 17 | cfg := &service.SlackAlerting{ 18 | AccessToken: os.Getenv("SLACK_ACCESS_TOKEN"), 19 | ChannelID: os.Getenv("SLACK_CHANNEL_ID"), 20 | } 21 | 22 | notifier, err := NewSlackAlerter(cfg) 23 | require.NoError(t, err) 24 | require.NotNil(t, notifier) 25 | 26 | err = notifier.AlertError(errors.New("error message")) 27 | require.NoError(t, err) 28 | } 29 | -------------------------------------------------------------------------------- /internal/audittrail/storage.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package audittrail 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "io" 11 | 12 | "github.com/moov-io/achgateway/internal/service" 13 | 14 | "github.com/go-kit/kit/metrics/prometheus" 15 | stdprometheus "github.com/prometheus/client_golang/prometheus" 16 | ) 17 | 18 | var ( 19 | uploadedFilesCounter = prometheus.NewCounterFrom(stdprometheus.CounterOpts{ 20 | Name: "audittrail_uploaded_files", 21 | Help: "Counter of ACH files uploaded to audit trail storage", 22 | }, []string{"type", "id"}) 23 | 24 | uploadFilesErrors = prometheus.NewCounterFrom(stdprometheus.CounterOpts{ 25 | Name: "audittrail_upload_errors", 26 | Help: "Counter of errors encountered when attempting ACH files upload", 27 | }, []string{"type", "id"}) 28 | ) 29 | 30 | // Storage is an interface for saving and encrypting ACH files for 31 | // records retention. This is often a requirement of agreements. 32 | // 33 | // File retention after upload is not part of this storage. 34 | type Storage interface { 35 | // SaveFile will encrypt and copy the ACH file to the configured file storage. 36 | SaveFile(ctx context.Context, filepath string, data []byte) error 37 | 38 | GetFile(ctx context.Context, filepath string) (io.ReadCloser, error) 39 | 40 | Close() error 41 | } 42 | 43 | func NewStorage(cfg *service.AuditTrail) (Storage, error) { 44 | if cfg == nil { 45 | return newMockStorage(), nil 46 | } 47 | if cfg.BucketURI != "" { 48 | return newBlobStorage(cfg) 49 | } 50 | return nil, errors.New("unknown storage config") 51 | } 52 | -------------------------------------------------------------------------------- /internal/audittrail/storage_blob_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package audittrail 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "io" 11 | "path/filepath" 12 | "testing" 13 | 14 | "github.com/moov-io/achgateway/internal/service" 15 | 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | var ( 20 | publicKeyPath = filepath.Join("..", "gpgx", "testdata", "key.pub") 21 | ) 22 | 23 | func TestBlobStorage(t *testing.T) { 24 | cfg := &service.AuditTrail{ 25 | BucketURI: "mem://", 26 | GPG: &service.GPG{ 27 | KeyFile: publicKeyPath, 28 | }, 29 | } 30 | store, err := newBlobStorage(cfg) 31 | require.NoError(t, err) 32 | defer store.Close() 33 | 34 | data := []byte("nacha formatted data") 35 | if err := store.SaveFile(context.Background(), "ftp.dev.com/saved.ach", data); err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | r, err := store.GetFile(context.Background(), "ftp.dev.com/saved.ach") 40 | require.NoError(t, err) 41 | defer r.Close() 42 | 43 | bs, err := io.ReadAll(r) 44 | require.NoError(t, err) 45 | if !bytes.Contains(bs, []byte("BEGIN PGP MESSAGE")) { 46 | t.Errorf("unexpected blob\n%s", string(bs)) 47 | } 48 | } 49 | 50 | func TestBlobStorage__NoGPG(t *testing.T) { 51 | cfg := &service.AuditTrail{ 52 | BucketURI: "mem://", 53 | } 54 | 55 | store, err := newBlobStorage(cfg) 56 | require.NoError(t, err) 57 | defer store.Close() 58 | 59 | data := []byte("nacha formatted data") 60 | err = store.SaveFile(context.Background(), "ftp.dev.com/saved.ach", data) 61 | require.NoError(t, err) 62 | 63 | r, err := store.GetFile(context.Background(), "ftp.dev.com/saved.ach") 64 | require.NoError(t, err) 65 | defer r.Close() 66 | 67 | bs, err := io.ReadAll(r) 68 | require.NoError(t, err) 69 | require.Equal(t, data, bs) 70 | } 71 | 72 | func TestBlobStorageErr(t *testing.T) { 73 | cfg := &service.AuditTrail{ 74 | BucketURI: "bad://", 75 | } 76 | if _, err := NewStorage(cfg); err == nil { 77 | t.Error("expected error") 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /internal/audittrail/storage_mock.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package audittrail 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "io" 11 | ) 12 | 13 | type MockStorage struct { 14 | Err error 15 | 16 | SavedFilepath string 17 | SavedContents []byte 18 | } 19 | 20 | func newMockStorage() *MockStorage { 21 | // default values for metrics 22 | uploadFilesErrors.With("type", "mock", "id", "mock").Add(0) 23 | uploadedFilesCounter.With("type", "mock", "id", "mock").Add(0) 24 | 25 | return &MockStorage{} 26 | } 27 | 28 | func (s *MockStorage) Close() error { 29 | return s.Err 30 | } 31 | 32 | func (s *MockStorage) SaveFile(_ context.Context, path string, data []byte) error { 33 | if s.Err != nil { 34 | uploadFilesErrors.With("type", "mock", "id", "mock").Add(1) 35 | } else { 36 | uploadedFilesCounter.With("type", "mock", "id", "mock").Add(1) 37 | 38 | s.SavedFilepath = path 39 | s.SavedContents = data 40 | } 41 | return s.Err 42 | } 43 | 44 | func (s *MockStorage) GetFile(_ context.Context, _ string) (io.ReadCloser, error) { 45 | if s.Err != nil { 46 | return nil, s.Err 47 | } 48 | return io.NopCloser(bytes.NewReader(s.SavedContents)), nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/audittrail/storage_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package audittrail 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/moov-io/achgateway/internal/service" 11 | ) 12 | 13 | func TestStorageErr(t *testing.T) { 14 | if store, err := NewStorage(nil); store == nil || err != nil { 15 | t.Errorf("unexpected error: %v", err) 16 | } 17 | if store, err := NewStorage(&service.AuditTrail{}); store != nil || err == nil { 18 | t.Errorf("unexpected store: %v", store) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/events/service_stream.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package events 19 | 20 | import ( 21 | "context" 22 | "errors" 23 | "fmt" 24 | 25 | "github.com/moov-io/achgateway/internal/incoming/stream" 26 | "github.com/moov-io/achgateway/internal/service" 27 | "github.com/moov-io/achgateway/pkg/compliance" 28 | "github.com/moov-io/achgateway/pkg/models" 29 | "github.com/moov-io/base/log" 30 | 31 | "gocloud.dev/pubsub" 32 | ) 33 | 34 | type streamService struct { 35 | transformConfig *models.TransformConfig 36 | topic stream.Publisher 37 | } 38 | 39 | func newStreamService(logger log.Logger, transformConfig *models.TransformConfig, cfg *service.EventsStream) (*streamService, error) { 40 | if cfg == nil { 41 | return nil, errors.New("nil EventsStream config") 42 | } 43 | 44 | topicConf := &service.Config{ 45 | Inbound: service.Inbound{}, 46 | } 47 | if cfg.InMem != nil { 48 | topicConf.Inbound.InMem = cfg.InMem 49 | } 50 | if cfg.Kafka != nil { 51 | topicConf.Inbound.Kafka = cfg.Kafka 52 | } 53 | 54 | topic, err := stream.Topic(logger, topicConf) 55 | if err != nil { 56 | return nil, fmt.Errorf("%T events stream: %v", topicConf.Inbound, err) 57 | } 58 | return &streamService{ 59 | topic: topic, 60 | transformConfig: transformConfig, 61 | }, nil 62 | } 63 | 64 | func (ss *streamService) Send(ctx context.Context, evt models.Event) error { 65 | bs, err := compliance.Protect(ss.transformConfig, evt) 66 | if err != nil { 67 | return err 68 | } 69 | err = ss.topic.Send(ctx, &pubsub.Message{ 70 | Body: bs, 71 | }) 72 | if err != nil { 73 | return fmt.Errorf("error emitting %s: %w", evt.Type, err) 74 | } 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /internal/events/service_stream_test.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package events 19 | 20 | import ( 21 | "context" 22 | "testing" 23 | 24 | "github.com/moov-io/achgateway/internal/incoming/stream/streamtest" 25 | "github.com/moov-io/achgateway/pkg/models" 26 | "github.com/moov-io/base" 27 | 28 | "github.com/stretchr/testify/require" 29 | ) 30 | 31 | func TestStreamService(t *testing.T) { 32 | pub, sub := streamtest.InmemStream(t) 33 | svc := &streamService{topic: pub} 34 | 35 | shardKey, fileID := base.ID(), base.ID() 36 | err := svc.Send(context.Background(), models.Event{ 37 | Event: models.FileUploaded{ 38 | FileID: fileID, 39 | ShardKey: shardKey, 40 | }, 41 | }) 42 | require.NoError(t, err) 43 | 44 | msg, err := sub.Receive(context.Background()) 45 | require.NoError(t, err) 46 | msg.Ack() 47 | 48 | var body models.FileUploaded 49 | require.NoError(t, models.ReadEvent(msg.Body, &body)) 50 | 51 | require.Equal(t, shardKey, body.ShardKey) 52 | require.Equal(t, fileID, body.FileID) 53 | } 54 | -------------------------------------------------------------------------------- /internal/events/service_webhook_test.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package events 19 | 20 | import ( 21 | "context" 22 | "io" 23 | "net/http" 24 | "testing" 25 | 26 | "github.com/moov-io/achgateway/internal/admintest" 27 | "github.com/moov-io/achgateway/internal/service" 28 | "github.com/moov-io/achgateway/pkg/models" 29 | "github.com/moov-io/base" 30 | "github.com/moov-io/base/log" 31 | 32 | "github.com/stretchr/testify/require" 33 | ) 34 | 35 | func TestWebhookService(t *testing.T) { 36 | adminServer := admintest.Server(t) 37 | 38 | var body *models.FileUploaded 39 | adminServer.AddHandler("/hook", func(w http.ResponseWriter, r *http.Request) { 40 | bs, _ := io.ReadAll(r.Body) 41 | 42 | var wrapper models.FileUploaded 43 | if err := models.ReadEvent(bs, &wrapper); err != nil { 44 | w.WriteHeader(http.StatusBadRequest) 45 | } else { 46 | body = &wrapper 47 | w.WriteHeader(http.StatusOK) 48 | } 49 | }) 50 | 51 | svc, err := newWebhookService(log.NewTestLogger(), nil, &service.WebhookConfig{ 52 | Endpoint: "http://" + adminServer.BindAddr() + "/hook", 53 | }) 54 | require.NoError(t, err) 55 | 56 | shardKey, fileID := base.ID(), base.ID() 57 | err = svc.Send(context.Background(), models.Event{ 58 | Event: models.FileUploaded{ 59 | FileID: fileID, 60 | ShardKey: shardKey, 61 | }, 62 | }) 63 | require.NoError(t, err) 64 | 65 | require.Equal(t, shardKey, body.ShardKey) 66 | require.Equal(t, fileID, body.FileID) 67 | } 68 | -------------------------------------------------------------------------------- /internal/files/mock_repo_files.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import "context" 4 | 5 | type MockRepository struct { 6 | Err error 7 | } 8 | 9 | func NewMockRepository() Repository { 10 | return &MockRepository{} 11 | } 12 | 13 | func (r *MockRepository) Record(_ context.Context, file AcceptedFile) error { 14 | return r.Err 15 | } 16 | 17 | func (r *MockRepository) Cancel(_ context.Context, fileID string) error { 18 | return r.Err 19 | } 20 | -------------------------------------------------------------------------------- /internal/files/model_accepted_file.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import "time" 4 | 5 | type AcceptedFile struct { 6 | FileID string 7 | ShardKey string 8 | 9 | Hostname string 10 | 11 | AcceptedAt time.Time 12 | CanceledAt time.Time 13 | } 14 | -------------------------------------------------------------------------------- /internal/files/repo_files_test.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/moov-io/achgateway/internal/dbtest" 9 | "github.com/moov-io/base" 10 | "github.com/moov-io/base/database" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestRepository(t *testing.T) { 16 | if testing.Short() { 17 | t.Skip("-short flag was specified") 18 | } 19 | 20 | conf := dbtest.CreateTestDatabase(t, dbtest.LocalDatabaseConfig()) 21 | db := dbtest.LoadDatabase(t, conf) 22 | require.NoError(t, db.Ping()) 23 | 24 | repo := NewRepository(db) 25 | if _, ok := repo.(*sqlRepository); !ok { 26 | t.Errorf("unexpected repository type: %T", repo) 27 | } 28 | 29 | ctx := context.Background() 30 | fileID1 := base.ID() 31 | accepted := AcceptedFile{ 32 | FileID: fileID1, 33 | ShardKey: base.ID(), 34 | Hostname: "achgateway-0", 35 | AcceptedAt: time.Now(), 36 | } 37 | 38 | // Record 39 | err := repo.Record(ctx, accepted) 40 | require.NoError(t, err) 41 | 42 | err = repo.Record(ctx, accepted) 43 | require.ErrorContains(t, err, "Duplicate entry") 44 | require.True(t, database.UniqueViolation(err)) 45 | 46 | // Second File 47 | fileID2 := base.ID() 48 | accepted.FileID = fileID2 49 | err = repo.Record(ctx, accepted) 50 | require.NoError(t, err) 51 | 52 | // Cancel 53 | err = repo.Cancel(ctx, fileID1) 54 | require.NoError(t, err) 55 | 56 | err = repo.Cancel(ctx, base.ID()) 57 | require.NoError(t, err) 58 | } 59 | -------------------------------------------------------------------------------- /internal/gpgx/keys.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package gpgx 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "os" 11 | 12 | "github.com/ProtonMail/go-crypto/openpgp" 13 | ) 14 | 15 | // ReadArmoredKeyFile attempts to read the filepath and parses an armored GPG key 16 | func ReadArmoredKeyFile(path string) (openpgp.EntityList, error) { 17 | bs, err := os.ReadFile(path) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return openpgp.ReadArmoredKeyRing(bytes.NewBuffer(bs)) 22 | } 23 | 24 | // ReadPrivateKeyFile attempts to read the filepath and parses an armored GPG private key 25 | func ReadPrivateKeyFile(path string, password []byte) (openpgp.EntityList, error) { 26 | // Read the private key 27 | entityList, err := ReadArmoredKeyFile(path) 28 | if err != nil { 29 | return nil, err 30 | } 31 | if len(entityList) == 0 { 32 | return nil, errors.New("gpg: no entities found") 33 | } 34 | 35 | entity := entityList[0] 36 | 37 | // Get the passphrase and read the private key. 38 | if entity.PrivateKey != nil && len(password) > 0 { 39 | entity.PrivateKey.Decrypt(password) 40 | } 41 | for _, subkey := range entity.Subkeys { 42 | if subkey.PrivateKey != nil && len(password) > 0 { 43 | subkey.PrivateKey.Decrypt(password) 44 | } 45 | } 46 | 47 | return entityList, nil 48 | } 49 | func Sign(message []byte, pubKey openpgp.EntityList) ([]byte, error) { 50 | if len(pubKey) == 0 { 51 | return nil, errors.New("sign: missing Entity") 52 | } 53 | 54 | var out bytes.Buffer 55 | r := bytes.NewReader(message) 56 | if err := openpgp.ArmoredDetachSign(&out, pubKey[0], r, nil); err != nil { 57 | return nil, err 58 | } 59 | return out.Bytes(), nil 60 | } 61 | -------------------------------------------------------------------------------- /internal/gpgx/testdata/key.priv: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PRIVATE KEY BLOCK----- 2 | 3 | lIYEZmsBdxYJKwYBBAHaRw8BAQdAMYFnHaCtVOpI8BsRl6CRAnlsH+8LLBgfL8SX 4 | RgP2YbH+BwMCo7j/C2AnkUj0tkpn6AJUq7NCgQesbcXKqNUHvI/QbW9ff+qEeTir 5 | vot/iK6ZRKveaxMZyOj9LfLInflpStls4rjiDZNBCVT6Tf3GYTWpS7QaTW9vdiBj 6 | cnlwdGZzIDxvc3NAbW9vdi5pbz6ImQQTFgoAQRYhBD32aIcVt/MQgPv/BvGsfUqy 7 | oCkdBQJmawF3AhsDBQkFo5qABQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJ 8 | EPGsfUqyoCkdDfwBAPUU8Kcxfofe0lO21ajGZuWCJc2IuxyoNrxAdH9e2XBkAP92 9 | Mf0NmLSyMyLPVm1q4MHUOnJt9nZlict/vmweqtcBApyLBGZrAXcSCisGAQQBl1UB 10 | BQEBB0DxqA97yU80lA4IuWFvGz5MEgHkYb5i67/gdpQrze+XKgMBCAf+BwMC/dkQ 11 | CA0+nH70+G8vA3sBRSmVCyuOKJ78IqqzDziUeV/bbdXgObYV7pQBMrbl8pqftq2p 12 | Cunzh3pzfEKvoHddZHYYn+J2Nkpiw867UwaL9Yh+BBgWCgAmFiEEPfZohxW38xCA 13 | +/8G8ax9SrKgKR0FAmZrAXcCGwwFCQWjmoAACgkQ8ax9SrKgKR1nxwEA0Tc1fR+O 14 | wHVuMMGFA/7SyJ/TvvhyH+wKPo7f6wpMa2QA/iy66iBqgkETREG3iE/Lribgl6eK 15 | pgbhIdztvBIAY2EH 16 | =66/G 17 | -----END PGP PRIVATE KEY BLOCK----- 18 | -------------------------------------------------------------------------------- /internal/gpgx/testdata/key.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mDMEZmsBdxYJKwYBBAHaRw8BAQdAMYFnHaCtVOpI8BsRl6CRAnlsH+8LLBgfL8SX 4 | RgP2YbG0Gk1vb3YgY3J5cHRmcyA8b3NzQG1vb3YuaW8+iJkEExYKAEEWIQQ99miH 5 | FbfzEID7/wbxrH1KsqApHQUCZmsBdwIbAwUJBaOagAULCQgHAgIiAgYVCgkICwIE 6 | FgIDAQIeBwIXgAAKCRDxrH1KsqApHQ38AQD1FPCnMX6H3tJTttWoxmblgiXNiLsc 7 | qDa8QHR/XtlwZAD/djH9DZi0sjMiz1ZtauDB1DpybfZ2ZYnLf75sHqrXAQK4OARm 8 | awF3EgorBgEEAZdVAQUBAQdA8agPe8lPNJQOCLlhbxs+TBIB5GG+Yuu/4HaUK83v 9 | lyoDAQgHiH4EGBYKACYWIQQ99miHFbfzEID7/wbxrH1KsqApHQUCZmsBdwIbDAUJ 10 | BaOagAAKCRDxrH1KsqApHWfHAQDRNzV9H47AdW4wwYUD/tLIn9O++HIf7Ao+jt/r 11 | CkxrZAD+LLrqIGqCQRNEQbeIT8uuJuCXp4qmBuEh3O28EgBjYQc= 12 | =m3Mj 13 | -----END PGP PUBLIC KEY BLOCK----- 14 | -------------------------------------------------------------------------------- /internal/incoming/models.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package incoming 19 | 20 | import ( 21 | "errors" 22 | 23 | "github.com/moov-io/ach" 24 | ) 25 | 26 | type ACHFile struct { 27 | FileID string `json:"id"` 28 | ShardKey string `json:"shardKey"` 29 | File *ach.File `json:"file"` 30 | } 31 | 32 | func (f ACHFile) Validate() error { 33 | if f.FileID == "" { 34 | return errors.New("missing fileID") 35 | } 36 | if f.ShardKey == "" { 37 | return errors.New("missing shardKey") 38 | } 39 | if f.File == nil { 40 | return errors.New("missing File") 41 | } 42 | return nil 43 | } 44 | 45 | type QueueACHFileResponse struct { 46 | FileID string `json:"id"` 47 | ShardKey string `json:"shardKey"` 48 | Error string `json:"error"` 49 | } 50 | 51 | type CancelACHFile struct { 52 | FileID string `json:"id"` 53 | ShardKey string `json:"shardKey"` 54 | } 55 | 56 | type FileCancellationResponse struct { 57 | FileID string `json:"id"` 58 | ShardKey string `json:"shardKey"` 59 | Successful bool `json:"successful"` 60 | } 61 | -------------------------------------------------------------------------------- /internal/incoming/odfi/admin.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package odfi 19 | 20 | import ( 21 | "net/http" 22 | 23 | "github.com/moov-io/base/admin" 24 | moovhttp "github.com/moov-io/base/http" 25 | ) 26 | 27 | func (s *PeriodicScheduler) RegisterRoutes(svc *admin.Server) { 28 | svc.AddHandler("/trigger-inbound", s.triggerInboundProcessing()) 29 | } 30 | 31 | type manuallyTriggeredInbound struct { 32 | C chan error 33 | } 34 | 35 | func (s *PeriodicScheduler) triggerInboundProcessing() http.HandlerFunc { 36 | return func(w http.ResponseWriter, r *http.Request) { 37 | if r.Method != http.MethodPut { 38 | w.WriteHeader(http.StatusBadRequest) 39 | return 40 | } 41 | 42 | // send off the manual request 43 | waiter := manuallyTriggeredInbound{ 44 | C: make(chan error, 1), 45 | } 46 | s.inboundTrigger <- waiter 47 | 48 | if err := <-waiter.C; err != nil { 49 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 50 | moovhttp.Problem(w, err) 51 | } else { 52 | w.WriteHeader(http.StatusOK) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/incoming/odfi/audit.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package odfi 19 | 20 | import ( 21 | "context" 22 | "fmt" 23 | 24 | "github.com/moov-io/achgateway/internal/audittrail" 25 | "github.com/moov-io/achgateway/internal/service" 26 | "github.com/moov-io/base/strx" 27 | ) 28 | 29 | type AuditSaver struct { 30 | storage audittrail.Storage 31 | basePath string 32 | hostname string 33 | } 34 | 35 | func (as *AuditSaver) save(ctx context.Context, filepath string, data []byte) error { 36 | if as == nil { 37 | return nil 38 | } 39 | if len(data) == 0 { 40 | return nil 41 | } 42 | return as.storage.SaveFile(ctx, filepath, data) 43 | } 44 | 45 | func newAuditSaver(hostname string, cfg *service.AuditTrail) (*AuditSaver, error) { 46 | if cfg == nil { 47 | return nil, nil 48 | } 49 | 50 | storage, err := audittrail.NewStorage(cfg) 51 | if err != nil { 52 | return nil, fmt.Errorf("odfi: audit: %v", err) 53 | } 54 | 55 | return &AuditSaver{ 56 | storage: storage, 57 | basePath: strx.Or(cfg.BasePath, "odfi"), 58 | hostname: hostname, 59 | }, nil 60 | } 61 | -------------------------------------------------------------------------------- /internal/incoming/odfi/audit_test.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package odfi 19 | 20 | import ( 21 | "context" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | func TestAuditSaver(t *testing.T) { 28 | var saver *AuditSaver = nil 29 | require.NoError(t, saver.save(context.Background(), "foo.ach", nil)) 30 | 31 | saver = &AuditSaver{} 32 | require.NoError(t, saver.save(context.Background(), "foo.ach", nil)) 33 | } 34 | -------------------------------------------------------------------------------- /internal/incoming/odfi/corrections_test.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package odfi 19 | 20 | import ( 21 | "testing" 22 | 23 | "github.com/moov-io/achgateway/internal/events" 24 | "github.com/moov-io/achgateway/internal/service" 25 | "github.com/moov-io/base/log" 26 | 27 | "github.com/stretchr/testify/require" 28 | ) 29 | 30 | func TestCorrections(t *testing.T) { 31 | cfg := service.ODFICorrections{ 32 | Enabled: true, 33 | } 34 | logger := log.NewTestLogger() 35 | eventsService, err := events.NewEmitter(logger, &service.EventsConfig{ 36 | Webhook: &service.WebhookConfig{ 37 | Endpoint: "https://cb.moov.io/incoming", 38 | }, 39 | }) 40 | require.NoError(t, err) 41 | 42 | emitter := CorrectionEmitter(cfg, eventsService) 43 | require.NotNil(t, emitter) 44 | } 45 | -------------------------------------------------------------------------------- /internal/incoming/odfi/mock_processor.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package odfi 19 | 20 | import ( 21 | "context" 22 | 23 | "github.com/moov-io/base/log" 24 | ) 25 | 26 | type MockProcessor struct { 27 | HandledFile *File 28 | Err error 29 | } 30 | 31 | func (pc *MockProcessor) Type() string { 32 | return "mock" 33 | } 34 | 35 | func (pc *MockProcessor) Handle(_ context.Context, logger log.Logger, file File) error { 36 | pc.HandledFile = &file 37 | return pc.Err 38 | } 39 | -------------------------------------------------------------------------------- /internal/incoming/odfi/mock_scheduler.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package odfi 19 | 20 | import ( 21 | "github.com/moov-io/base/admin" 22 | ) 23 | 24 | type MockScheduler struct { 25 | Err error 26 | } 27 | 28 | func (s *MockScheduler) Start() error { 29 | return s.Err 30 | } 31 | 32 | func (*MockScheduler) Shutdown() {} 33 | 34 | func (*MockScheduler) RegisterRoutes(_ *admin.Server) {} 35 | -------------------------------------------------------------------------------- /internal/incoming/odfi/prenotes_test.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package odfi 19 | 20 | import ( 21 | "path/filepath" 22 | "testing" 23 | 24 | "github.com/moov-io/ach" 25 | 26 | "github.com/stretchr/testify/require" 27 | ) 28 | 29 | func TestPrenote__isPrenoteEntry(t *testing.T) { 30 | file, err := ach.ReadFile(filepath.Join("testdata", "prenote-ppd-debit.ach")) 31 | require.NoError(t, err) 32 | entries := file.Batches[0].GetEntries() 33 | if len(entries) != 1 { 34 | t.Fatalf("unexpected entries: %#v", entries) 35 | } 36 | for i := range entries { 37 | if ok, err := isPrenoteEntry(entries[i]); !ok || err != nil { 38 | t.Errorf("expected prenote entry: %#v", entries[i]) 39 | t.Error(err) 40 | } 41 | } 42 | 43 | // non prenote file 44 | file, err = ach.ReadFile(filepath.Join("..", "..", "..", "testdata", "ppd-debit.ach")) 45 | require.NoError(t, err) 46 | entries = file.Batches[0].GetEntries() 47 | for i := range entries { 48 | if ok, err := isPrenoteEntry(entries[i]); ok || err != nil { 49 | t.Errorf("expected no prenote entry: %#v", entries[i]) 50 | t.Error(err) 51 | } 52 | } 53 | } 54 | 55 | func TestPrenote__isPrenoteEntryErr(t *testing.T) { 56 | file, err := ach.ReadFile(filepath.Join("testdata", "prenote-ppd-debit.ach")) 57 | require.NoError(t, err) 58 | entries := file.Batches[0].GetEntries() 59 | if len(entries) != 1 { 60 | t.Fatalf("unexpected entries: %#v", entries) 61 | } 62 | 63 | entries[0].Amount = 125 // non-zero amount 64 | if exists, err := isPrenoteEntry(entries[0]); !exists || err == nil { 65 | t.Errorf("expected invalid prenote: %v", err) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /internal/incoming/odfi/returns_test.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package odfi 19 | 20 | import ( 21 | "testing" 22 | 23 | "github.com/moov-io/achgateway/internal/events" 24 | "github.com/moov-io/achgateway/internal/service" 25 | "github.com/moov-io/base/log" 26 | 27 | "github.com/stretchr/testify/require" 28 | ) 29 | 30 | func TestReturns(t *testing.T) { 31 | cfg := service.ODFIReturns{ 32 | Enabled: true, 33 | } 34 | eventsService, err := events.NewEmitter(log.NewTestLogger(), &service.EventsConfig{ 35 | Webhook: &service.WebhookConfig{ 36 | Endpoint: "https://cb.moov.io/incoming", 37 | }, 38 | }) 39 | require.NoError(t, err) 40 | 41 | emitter := ReturnEmitter(cfg, eventsService) 42 | require.NotNil(t, emitter) 43 | } 44 | -------------------------------------------------------------------------------- /internal/incoming/odfi/scheduler_test.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package odfi 19 | 20 | import ( 21 | "context" 22 | "testing" 23 | "time" 24 | 25 | "github.com/moov-io/achgateway/internal/service" 26 | "github.com/moov-io/base/log" 27 | 28 | "github.com/stretchr/testify/require" 29 | ) 30 | 31 | func TestScheduler(t *testing.T) { 32 | cfg := &service.Config{ 33 | Logger: log.NewTestLogger(), 34 | Inbound: service.Inbound{ 35 | ODFI: &service.ODFIFiles{ 36 | Interval: 10 * time.Second, 37 | ShardNames: []string{"mock"}, 38 | Storage: service.ODFIStorage{ 39 | CleanupLocalDirectory: true, 40 | KeepRemoteFiles: false, 41 | RemoveZeroByteFiles: true, 42 | }, 43 | }, 44 | }, 45 | Upload: service.UploadAgents{ 46 | Agents: []service.UploadAgent{ 47 | { 48 | ID: "ftp-test", 49 | Mock: &service.MockAgent{}, 50 | }, 51 | }, 52 | DefaultAgentID: "ftp-test", 53 | }, 54 | } 55 | cfg.Logger = log.NewTestLogger() 56 | 57 | processors := SetupProcessors(&MockProcessor{}) 58 | schd, err := NewPeriodicScheduler(cfg.Logger, cfg, processors) 59 | require.NoError(t, err) 60 | require.NotNil(t, schd) 61 | 62 | ss, ok := schd.(*PeriodicScheduler) 63 | if !ok { 64 | t.Fatalf("unexpected scheduler: %T", schd) 65 | } 66 | 67 | mock := &service.Shard{ 68 | Name: "mock", 69 | UploadAgent: "ftp-test", 70 | } 71 | if err := ss.tick(context.Background(), cfg.Logger, mock); err != nil { 72 | t.Fatal(err) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/incoming/odfi/testdata/forward.ach: -------------------------------------------------------------------------------- 1 | 5225Adam Shannon MOOVYYYYYYPPDtest trans2005262005271491273976360000188 2 | 62627397636915XXXXXXXXXX1 0000000146ae1300b1c1cac5cVeridian Credit Union te1273976368613175 3 | 5220Robert Smith MOOVYYYYYYPPDaccount va2005262005271491323274270000085 4 | 6212739763691XXXXXXXXXXX2 0000000008dcd5f1ce42ae2e1Adam Shannon ac1273976360000001 5 | -------------------------------------------------------------------------------- /internal/incoming/odfi/testdata/iat-credit.ach: -------------------------------------------------------------------------------- 1 | 101 121042882 2313801041812180000A094101Bank My Bank Name 2 | 5225 FF3 US123456789 IATTRADEPAYMTCADUSD181219 1231380100000001 3 | 6221210428820007 0000100000123456789 1231380100000001 4 | 710ANN000000000000100000928383-23938 BEK Enterprises 0000001 5 | 711BEK Solutions 15 West Place Street 0000001 6 | 712JacobsTown*PA\ US*19305\ 0000001 7 | 713Wells Fargo 01231380104 US 0000001 8 | 714Citadel Bank 01121042882 CA 0000001 9 | 7159874654932139872121 Front Street 0000001 10 | 716LetterTown*AB\ CA*80014\ 0000001 11 | 717This is an international payment 00010000001 12 | 718Bank of France 01456456456987987 FR 00010000001 13 | 82250000100012104288000000000000000000100000 231380100000001 14 | 9000001000002000000100012104288000000000000000000100000 15 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 16 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 17 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 18 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 19 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 20 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 21 | -------------------------------------------------------------------------------- /internal/incoming/odfi/testdata/prenote-ppd-debit.ach: -------------------------------------------------------------------------------- 1 | 101 076401251 0764012510807291511A094101achdestname companyname 2 | 5225companyname origid PPDCHECKPAYMT000002080730 1076401250000001 3 | 63805320001912345 0000000000c-1 Bachman Eric DD0076401255655291 4 | 82250000010005320001000000000000000000000000origid 076401250000001 5 | 9000001000001000000010005320001000000000000000000000000 6 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 7 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 8 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 9 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 10 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 11 | -------------------------------------------------------------------------------- /internal/incoming/odfi/testdata/recon.ach: -------------------------------------------------------------------------------- 1 | 5220Robert Smith MOOVYYYYYYPPDaccount va2005262005271491323274270000085 2 | 6212739763691XXXXXXXXXXX2 0000000008dcd5f1ce42ae2e1Adam Shannon ac1273976360000001 3 | -------------------------------------------------------------------------------- /internal/incoming/odfi/testdata/return-no-batch-controls.ach: -------------------------------------------------------------------------------- 1 | 5200CoinLion 123456789 WEBTRANSFER 000101 1091000010000001 2 | 626091400606123456789 0000012354MjMxNDAwMjAtOGQPaul Jones S 1091000017611242 3 | 799R01091400600000001 09100001 091000017611242 4 | 5220Your Company, in 121042882 CORVendor Pay 000000 1121042880000001 5 | 621231380104744-5678-99 0000000000location #23 Best Co. #23 S 1121042880000001 6 | 798C01121042880000001 121042881918171614 091012980000088 -------------------------------------------------------------------------------- /internal/incoming/odfi/testdata/return.ach: -------------------------------------------------------------------------------- 1 | 5225Adam Shannon MOOVYYYYYYPPDtest trans2005262005271491273976360000188 2 | 62627397636915XXXXXXXXXX1 0000000146ae1300b1c1cac5cVeridian Credit Union te1273976361273620 3 | 799R02273976368613175 27397636 273976361273620 4 | 5220Robert Smith MOOVYYYYYYPPDaccount va2005262005271491323274270000085 5 | 6212739763691XXXXXXXXXXX2 0000000008dcd5f1ce42ae2e1Adam Shannon ac1323274270765782 6 | 799R03273976360000001 32327427 323274270765782 7 | 6212739763691XXXXXXXXXXX2 0000000019908ac8a237911d6Adam Shannon ac1323274270765784 8 | 799R03273976360000001 32327427 323274270765784 9 | -------------------------------------------------------------------------------- /internal/incoming/stream/streamtest/streamtest.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package streamtest 19 | 20 | import ( 21 | "context" 22 | 23 | "testing" 24 | 25 | "github.com/moov-io/achgateway/internal/incoming/stream" 26 | "github.com/moov-io/achgateway/internal/service" 27 | "github.com/moov-io/base/log" 28 | 29 | "github.com/stretchr/testify/require" 30 | "gocloud.dev/pubsub" 31 | ) 32 | 33 | func InmemStream(t *testing.T) (stream.Publisher, stream.Subscription) { 34 | t.Helper() 35 | 36 | conf := &service.Config{ 37 | Inbound: service.Inbound{ 38 | InMem: &service.InMemory{ 39 | URL: "mem://" + t.Name(), 40 | }, 41 | }, 42 | } 43 | topic, err := stream.Topic(log.NewTestLogger(), conf) 44 | require.NoError(t, err) 45 | 46 | sub, err := stream.OpenSubscription(log.NewTestLogger(), conf) 47 | require.NoError(t, err) 48 | t.Cleanup(func() { sub.Shutdown(t.Context()) }) 49 | 50 | return topic, sub 51 | } 52 | 53 | type FailedSubscription struct { 54 | Err error 55 | } 56 | 57 | func (s *FailedSubscription) Receive(ctx context.Context) (*pubsub.Message, error) { 58 | return nil, s.Err 59 | } 60 | 61 | func (s *FailedSubscription) Shutdown(ctx context.Context) error { 62 | return nil 63 | } 64 | 65 | func FailingSubscription(err error) *FailedSubscription { 66 | return &FailedSubscription{Err: err} 67 | } 68 | -------------------------------------------------------------------------------- /internal/mask/password.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package mask 6 | 7 | import ( 8 | "fmt" 9 | "strings" 10 | "unicode/utf8" 11 | ) 12 | 13 | func Password(s string) string { 14 | if utf8.RuneCountInString(s) < 3 { 15 | return "**" // too short, we can't mask anything 16 | } else { 17 | // turn 'password' into 'p******d' 18 | first, last := s[0:1], s[len(s)-1:] 19 | return fmt.Sprintf("%s%s%s", first, strings.Repeat("*", len(s)-2), last) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/notify/helpers.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "golang.org/x/text/language" 5 | "golang.org/x/text/message" 6 | "golang.org/x/text/number" 7 | ) 8 | 9 | func convertDollar(in int) string { 10 | printer := message.NewPrinter(language.Und) 11 | formatter := number.Decimal(float64(in)/100.0, number.MinFractionDigits(2)) 12 | return printer.Sprint(formatter) 13 | } 14 | -------------------------------------------------------------------------------- /internal/notify/helpers_test.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestConvertDollar(t *testing.T) { 10 | require.Equal(t, "0.00", convertDollar(0)) 11 | require.Equal(t, "0.01", convertDollar(1)) 12 | require.Equal(t, "0.67", convertDollar(67)) 13 | require.Equal(t, "5.67", convertDollar(567)) 14 | require.Equal(t, "345.67", convertDollar(34567)) 15 | require.Equal(t, "2,345.67", convertDollar(234567)) 16 | require.Equal(t, "12,345.67", convertDollar(1234567)) 17 | require.Equal(t, "1,234,567.89", convertDollar(123456789)) 18 | } 19 | -------------------------------------------------------------------------------- /internal/notify/mailslurper_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package notify 6 | 7 | import ( 8 | "net" 9 | "testing" 10 | "time" 11 | 12 | "github.com/moov-io/base/docker" 13 | "github.com/stretchr/testify/require" 14 | 15 | "github.com/ory/dockertest/v3" 16 | ) 17 | 18 | type mailslurpDeployment struct { 19 | container *dockertest.Resource 20 | } 21 | 22 | func (dep *mailslurpDeployment) SMTPPort() string { 23 | return dep.container.GetPort("1025/tcp") 24 | } 25 | 26 | func (dep *mailslurpDeployment) Close() error { 27 | return dep.container.Close() 28 | } 29 | 30 | func spawnMailslurp(t *testing.T) *mailslurpDeployment { 31 | if testing.Short() || !docker.Enabled() { 32 | t.Skip("skipping docker test") 33 | } 34 | 35 | pool, err := dockertest.NewPool("") 36 | require.NoError(t, err) 37 | 38 | container, err := pool.RunWithOptions(&dockertest.RunOptions{ 39 | Repository: "oryd/mailslurper", 40 | Tag: "latest-smtps", 41 | ExposedPorts: []string{"1025"}, 42 | }) 43 | require.NoError(t, err) 44 | 45 | dep := &mailslurpDeployment{ 46 | container: container, 47 | } 48 | 49 | err = pool.Retry(func() error { 50 | time.Sleep(1 * time.Second) 51 | 52 | conn, err := net.Dial("tcp", "localhost:"+dep.SMTPPort()) 53 | if err != nil { 54 | return err 55 | } 56 | return conn.Close() 57 | }) 58 | require.NoError(t, err) 59 | 60 | return dep 61 | } 62 | -------------------------------------------------------------------------------- /internal/notify/mock_sender.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import "context" 4 | 5 | type MockSender struct { 6 | infoCalled bool 7 | criticalCalled bool 8 | Err error 9 | msg *Message 10 | } 11 | 12 | func (s *MockSender) Info(_ context.Context, msg *Message) error { 13 | s.infoCalled = true 14 | s.msg = msg 15 | return s.Err 16 | } 17 | 18 | func (s *MockSender) Critical(_ context.Context, msg *Message) error { 19 | s.criticalCalled = true 20 | s.msg = msg 21 | return s.Err 22 | } 23 | 24 | func (s *MockSender) InfoWasCalled() bool { 25 | return s.infoCalled 26 | } 27 | 28 | func (s *MockSender) CriticalWasCalled() bool { 29 | return s.criticalCalled 30 | } 31 | 32 | func (s *MockSender) CapturedMessage() *Message { 33 | return s.msg 34 | } 35 | -------------------------------------------------------------------------------- /internal/notify/multi_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package notify 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "testing" 11 | "time" 12 | 13 | "github.com/moov-io/achgateway/internal/service" 14 | "github.com/moov-io/base/log" 15 | 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func TestMultiSender(t *testing.T) { 20 | logger := log.NewTestLogger() 21 | cfg := &service.Notifications{} 22 | notifiers := &service.UploadNotifiers{} 23 | 24 | sender, err := NewMultiSender(logger, cfg, notifiers) 25 | require.NoError(t, err) 26 | 27 | msg := &Message{Direction: Upload} 28 | 29 | ctx := context.Background() 30 | require.NoError(t, sender.Info(ctx, msg)) 31 | require.NoError(t, sender.Critical(ctx, msg)) 32 | 33 | sender.senders = append(sender.senders, &MockSender{}) 34 | 35 | require.NoError(t, sender.Info(ctx, msg)) 36 | require.NoError(t, sender.Critical(ctx, msg)) 37 | } 38 | 39 | func TestMultiSender_senderTypes(t *testing.T) { 40 | logger := log.NewTestLogger() 41 | cfg := &service.Notifications{ 42 | Email: []service.Email{ 43 | { 44 | ID: "testing", 45 | From: "user:pass@localhost:4133", 46 | }, 47 | }, 48 | } 49 | notifiers := &service.UploadNotifiers{ 50 | Email: []string{"testing"}, 51 | } 52 | 53 | ms, err := NewMultiSender(logger, cfg, notifiers) 54 | require.NoError(t, err) 55 | 56 | ms.senders = append(ms.senders, &MockSender{}) 57 | 58 | require.Equal(t, "Email, MockSender", ms.senderTypes()) // no password leaked 59 | } 60 | 61 | func TestMultiSenderErr(t *testing.T) { 62 | sendErr := errors.New("bad error") 63 | 64 | sender := &MultiSender{ 65 | logger: log.NewTestLogger(), 66 | senders: []Sender{ 67 | &MockSender{Err: sendErr}, 68 | }, 69 | } 70 | 71 | ctx := context.Background() 72 | msg := &Message{Direction: Upload} 73 | 74 | require.Equal(t, sender.Info(ctx, msg), sendErr) 75 | require.Equal(t, sender.Critical(ctx, msg), sendErr) 76 | } 77 | 78 | func TestMulti__Retry(t *testing.T) { 79 | cfg := &service.Notifications{ 80 | Retry: &service.NotificationRetries{ 81 | Interval: 1 * time.Second, 82 | MaxRetries: 3, 83 | }, 84 | } 85 | ms, err := NewMultiSender(log.NewTestLogger(), cfg, &service.UploadNotifiers{}) 86 | require.NoError(t, err) 87 | require.NotNil(t, ms.retryConfig) 88 | } 89 | -------------------------------------------------------------------------------- /internal/notify/notify.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package notify 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/moov-io/ach" 11 | ) 12 | 13 | type Direction string 14 | 15 | const ( 16 | Upload Direction = "upload" 17 | Download Direction = "download" 18 | ) 19 | 20 | type Message struct { 21 | Direction Direction 22 | Filename string 23 | File *ach.File 24 | Hostname string 25 | 26 | // Contents will be used instead of the above fields 27 | Contents string 28 | } 29 | 30 | type Sender interface { 31 | Info(ctx context.Context, msg *Message) error 32 | Critical(ctx context.Context, msg *Message) error 33 | } 34 | -------------------------------------------------------------------------------- /internal/notify/pagerduty_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package notify 6 | 7 | import ( 8 | "context" 9 | "os" 10 | "testing" 11 | 12 | "github.com/moov-io/ach" 13 | "github.com/moov-io/achgateway/internal/service" 14 | 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | func testPagerDutyClient(t *testing.T) *PagerDuty { 19 | t.Helper() 20 | 21 | cfg := &service.PagerDuty{ 22 | ID: "testing", 23 | ApiKey: os.Getenv("PAGERDUTY_API_KEY"), 24 | From: "adam@moov.io", 25 | ServiceKey: "PM8YUZY", // testing 26 | } 27 | if cfg.ApiKey == "" { 28 | t.Skip("missing PagerDuty api key") 29 | } 30 | 31 | client, err := NewPagerDuty(cfg) 32 | require.NoError(t, err) 33 | 34 | return client 35 | } 36 | 37 | func TestPagerDuty(t *testing.T) { 38 | pd := testPagerDutyClient(t) 39 | 40 | if err := pd.Ping(); err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | file := ach.NewFile() 45 | ctx := context.Background() 46 | 47 | if err := pd.Info(ctx, &Message{ 48 | Direction: Download, 49 | Filename: "20200529-140002-1.ach", 50 | File: file, 51 | }); err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | if err := pd.Critical(ctx, &Message{ 56 | Direction: Upload, 57 | Filename: "20200529-140002-2.ach", 58 | File: file, 59 | }); err != nil { 60 | t.Fatal(err) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /internal/output/base64.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package output 6 | 7 | import ( 8 | "bytes" 9 | "encoding/base64" 10 | 11 | "github.com/moov-io/achgateway/internal/transform" 12 | ) 13 | 14 | type Base64 struct { 15 | lineEnding string 16 | } 17 | 18 | // Format converts any encrypted bytes into standard Base64 encoding. If no encrypted 19 | // bytes are passed then the file is encoded with NACHA formatting and then Base64 encoded. 20 | func (b *Base64) Format(buf *bytes.Buffer, res *transform.Result) error { 21 | if len(res.Encrypted) > 0 { 22 | buf.WriteString(base64.StdEncoding.EncodeToString(res.Encrypted)) 23 | } else { 24 | var buf2 bytes.Buffer 25 | 26 | nacha := &NACHA{ 27 | lineEnding: b.lineEnding, 28 | } 29 | if err := nacha.Format(&buf2, res); err != nil { 30 | return err 31 | } 32 | 33 | buf.WriteString(base64.StdEncoding.EncodeToString(buf2.Bytes())) 34 | } 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/output/base64_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package output 6 | 7 | import ( 8 | "bytes" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestBase64(t *testing.T) { 14 | enc := &Base64{} 15 | 16 | var buf bytes.Buffer 17 | res := testResult(t) 18 | 19 | if err := enc.Format(&buf, res); err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if buf.Len() == 0 { 24 | t.Error("encoded zero bytes") 25 | } 26 | 27 | if !strings.HasPrefix(buf.String(), `MTAxIDA3NjQwMTI1MSAwNzY0MDEyNTE`) { 28 | t.Errorf("unexpected output: %v", buf.String()) 29 | } 30 | } 31 | 32 | func TestBase64Encrypted(t *testing.T) { 33 | enc := &Base64{} 34 | 35 | var buf bytes.Buffer 36 | res := testResult(t) 37 | res.Encrypted = []byte("hello, world") 38 | 39 | if err := enc.Format(&buf, res); err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | if buf.Len() == 0 { 44 | t.Error("encoded zero bytes") 45 | } 46 | 47 | if !strings.HasPrefix(buf.String(), `aGVsbG8sIHdvcmxk`) { 48 | t.Errorf("unexpected output: %v", buf.String()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/output/encrypted.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package output 6 | 7 | import ( 8 | "bytes" 9 | 10 | "github.com/moov-io/achgateway/internal/transform" 11 | ) 12 | 13 | type Encrypted struct{} 14 | 15 | func (*Encrypted) Format(buf *bytes.Buffer, res *transform.Result) error { 16 | buf.Write(res.Encrypted) 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /internal/output/encrypted_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package output 6 | 7 | import ( 8 | "bytes" 9 | "testing" 10 | ) 11 | 12 | func TestEncrypted(t *testing.T) { 13 | enc := &Encrypted{} 14 | 15 | var buf bytes.Buffer 16 | res := testResult(t) 17 | res.Encrypted = []byte("hello, world") 18 | 19 | if err := enc.Format(&buf, res); err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if s := buf.String(); s != "hello, world" { 24 | t.Errorf("unexpected output: %q", s) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/output/format.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package output 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "strings" 11 | 12 | "github.com/moov-io/achgateway/internal/service" 13 | "github.com/moov-io/achgateway/internal/transform" 14 | ) 15 | 16 | // Formatter is a structure for encoding an encrypted or plaintext ACH file. 17 | type Formatter interface { 18 | Format(buf *bytes.Buffer, res *transform.Result) error 19 | } 20 | 21 | func NewFormatter(cfg *service.Output) (Formatter, error) { 22 | if cfg == nil || cfg.Format == "" { 23 | return &NACHA{}, nil 24 | } 25 | 26 | format := strings.ToLower(cfg.Format) 27 | lineEnding := "\n" 28 | if strings.HasSuffix(format, "-crlf") { 29 | lineEnding = "\r\n" 30 | } 31 | 32 | switch { 33 | case strings.EqualFold(format, "encrypted-bytes"): 34 | return &Encrypted{}, nil 35 | 36 | case strings.HasPrefix(format, "base64"): 37 | return &Base64{ 38 | lineEnding: lineEnding, 39 | }, nil 40 | 41 | case strings.HasPrefix(format, "nacha"): 42 | return &NACHA{ 43 | lineEnding: lineEnding, 44 | }, nil 45 | } 46 | return nil, errors.New("unknown output format") 47 | } 48 | -------------------------------------------------------------------------------- /internal/output/format_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package output 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/moov-io/achgateway/internal/service" 11 | ) 12 | 13 | func TestFormatter(t *testing.T) { 14 | cfg := &service.Output{ 15 | Format: "other", 16 | } 17 | enc, err := NewFormatter(cfg) 18 | if err == nil { 19 | t.Fatal("expected error") 20 | } 21 | if enc != nil { 22 | t.Errorf("unexpected Formatter: %#v", enc) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /internal/output/nacha.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package output 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | 11 | "github.com/moov-io/ach" 12 | "github.com/moov-io/achgateway/internal/transform" 13 | ) 14 | 15 | type NACHA struct { 16 | lineEnding string 17 | } 18 | 19 | func (n *NACHA) Format(buf *bytes.Buffer, res *transform.Result) error { 20 | w := ach.NewWriter(buf) 21 | if n.lineEnding != "" { 22 | w.LineEnding = n.lineEnding 23 | } 24 | if err := w.Write(res.File); err != nil { 25 | return fmt.Errorf("unable to write Nacha file: %v", err) 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/output/nacha_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package output 6 | 7 | import ( 8 | "bytes" 9 | "path/filepath" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/moov-io/ach" 14 | "github.com/moov-io/achgateway/internal/transform" 15 | 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func testResult(t *testing.T) *transform.Result { 20 | t.Helper() 21 | 22 | path := filepath.Join("..", "..", "testdata", "ppd-debit.ach") 23 | 24 | file, err := ach.ReadFile(path) 25 | require.NoError(t, err) 26 | 27 | return &transform.Result{ 28 | File: file, 29 | } 30 | } 31 | 32 | func TestNACHA(t *testing.T) { 33 | enc := &NACHA{} 34 | 35 | var buf bytes.Buffer 36 | res := testResult(t) 37 | 38 | if err := enc.Format(&buf, res); err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | if s := buf.String(); !strings.HasPrefix(s, `101 076401251 076401251080729`) { 43 | t.Errorf("unexpected output:\n%v", s) 44 | } 45 | } 46 | 47 | func TestNacha__CRLF(t *testing.T) { 48 | enc := &NACHA{ 49 | lineEnding: "\r\n", 50 | } 51 | 52 | var buf bytes.Buffer 53 | res := testResult(t) 54 | 55 | if err := enc.Format(&buf, res); err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | if s := buf.String(); !strings.Contains(s, "\r\n") { 60 | t.Errorf("unexpected output:\n%v", s) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /internal/pipeline/mock_xfer_merging.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package pipeline 19 | 20 | import ( 21 | "context" 22 | 23 | "github.com/moov-io/ach" 24 | "github.com/moov-io/achgateway/internal/incoming" 25 | "github.com/moov-io/achgateway/internal/upload" 26 | ) 27 | 28 | type MockXferMerging struct { 29 | LatestFile *incoming.ACHFile 30 | LatestCancel *incoming.CancelACHFile 31 | CancellationResponse incoming.FileCancellationResponse 32 | merged mergedFiles 33 | 34 | Err error 35 | } 36 | 37 | func (merge *MockXferMerging) HandleXfer(_ context.Context, xfer incoming.ACHFile) error { 38 | merge.LatestFile = &xfer 39 | return merge.Err 40 | } 41 | 42 | func (merge *MockXferMerging) HandleCancel(_ context.Context, cancel incoming.CancelACHFile) (incoming.FileCancellationResponse, error) { 43 | merge.LatestCancel = &cancel 44 | return merge.CancellationResponse, merge.Err 45 | } 46 | 47 | func (merge *MockXferMerging) WithEachMerged(ctx context.Context, f func(context.Context, int, upload.Agent, *ach.File) (string, error)) (mergedFiles, error) { 48 | if merge.Err != nil { 49 | return nil, merge.Err 50 | } 51 | return merge.merged, nil 52 | } 53 | -------------------------------------------------------------------------------- /internal/pipeline/testdata/duplicate-trace.ach: -------------------------------------------------------------------------------- 1 | 101 076401251 0764012510807291511A094101achdestname companyname 2 | 5225companyname origid PPDCHECKPAYMT000002080730 1076401250000001 3 | 62712345678012345 0000010500c-1 Bachman Dave DD0076401255655291 4 | 82250000010012345678000000010500000000000000origid 076401250000001 5 | 9000001000001000000010005320001000000010500000000000000 6 | -------------------------------------------------------------------------------- /internal/pipeline/testdata/ppd-debit.ach: -------------------------------------------------------------------------------- 1 | 101 076401251 0764012510807291511A094101achdestname companyname 2 | 5225companyname origid PPDCHECKPAYMT000002080730 1076401250000001 3 | 62705320001912345 0000010500c-1 Bachman Eric DD0076401255655291 4 | 82250000010005320001000000010500000000000000origid 076401250000001 5 | 9000001000001000000010005320001000000010500000000000000 -------------------------------------------------------------------------------- /internal/pipeline/testdata/ppd-debit.json: -------------------------------------------------------------------------------- 1 | {"requireABAOrigin":true} 2 | -------------------------------------------------------------------------------- /internal/pipeline/testdata/ppd-debit2.ach: -------------------------------------------------------------------------------- 1 | 101 076401251 0764012510807291511A094101achdestname companyname 2 | 5225companyname origid PPDCHECKPAYMT000002080730 1076401250000001 3 | 62705320001912345 0000010500c-1 Bachman Eric DD0076401255655292 4 | 82250000010005320001000000010500000000000000origid 076401250000001 5 | 9000001000001000000010005320001000000010500000000000000 6 | -------------------------------------------------------------------------------- /internal/pipeline/testdata/ppd-debit3.ach: -------------------------------------------------------------------------------- 1 | 101 076401251 0764012510807291511A094101achdestname companyname 2 | 5225companyname origid PPDCHECKPAYMT000002080730 1076401250000001 3 | 62705320001912345 0000010500c-1 Bachman Eric DD0076401255655293 4 | 82250000010005320001000000010500000000000000origid 076401250000001 5 | 9000001000001000000010005320001000000010500000000000000 6 | -------------------------------------------------------------------------------- /internal/pipeline/testdata/ppd-debit4.ach: -------------------------------------------------------------------------------- 1 | 101 076401251 0764012510807291511A094101achdestname companyname 2 | 5225companyname origid PPDCHECKPAYMT000002080730 1076401250000001 3 | 62705320001912345 0000010500c-1 Bachman Eric DD0076401255655294 4 | 82250000010005320001000000010500000000000000origid 076401250000001 5 | 9000001000001000000010005320001000000010500000000000000 6 | -------------------------------------------------------------------------------- /internal/service/client.go: -------------------------------------------------------------------------------- 1 | // generated-from:aac4f94179a969295e94b4572607e42b1419ca91e6a2c905c76717dc6a2f2525 DO NOT REMOVE, DO UPDATE 2 | 3 | // Licensed to The Moov Authors under one or more contributor 4 | // license agreements. See the NOTICE file distributed with 5 | // this work for additional information regarding copyright 6 | // ownership. The Moov Authors licenses this file to you under 7 | // the Apache License, Version 2.0 (the "License"); you may 8 | // not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | package service 21 | 22 | import ( 23 | "net/http" 24 | "time" 25 | 26 | "github.com/moov-io/base/log" 27 | ) 28 | 29 | type ClientConfig struct { 30 | Timeout time.Duration 31 | MaxIdleConns int 32 | MaxIdleConnsPerHost int 33 | MaxConnsPerHost int 34 | } 35 | 36 | func NewInternalClient(logger log.Logger, config *ClientConfig, name string) *http.Client { 37 | if config == nil { 38 | config = &ClientConfig{ 39 | Timeout: 60 * time.Second, 40 | MaxIdleConns: 20, 41 | MaxIdleConnsPerHost: 20, 42 | MaxConnsPerHost: 20, 43 | } 44 | } 45 | 46 | // Default settings we approve of 47 | internalClient := &http.Client{ 48 | Timeout: config.Timeout, 49 | Transport: &http.Transport{ 50 | ForceAttemptHTTP2: true, 51 | MaxIdleConns: config.MaxIdleConns, 52 | MaxIdleConnsPerHost: config.MaxIdleConnsPerHost, 53 | MaxConnsPerHost: config.MaxConnsPerHost, 54 | }, 55 | } 56 | 57 | return internalClient 58 | } 59 | -------------------------------------------------------------------------------- /internal/service/config_test.go: -------------------------------------------------------------------------------- 1 | // generated-from:441ae94818c824e252f84ad979ce3b376d077307353125e1e53d4b1343013dc4 DO NOT REMOVE, DO UPDATE 2 | 3 | // Licensed to The Moov Authors under one or more contributor 4 | // license agreements. See the NOTICE file distributed with 5 | // this work for additional information regarding copyright 6 | // ownership. The Moov Authors licenses this file to you under 7 | // the Apache License, Version 2.0 (the "License"); you may 8 | // not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | package service_test 21 | 22 | import ( 23 | "testing" 24 | 25 | "github.com/moov-io/base/config" 26 | "github.com/moov-io/base/log" 27 | "github.com/stretchr/testify/require" 28 | 29 | "github.com/moov-io/achgateway/internal/service" 30 | ) 31 | 32 | func Test_ConfigLoading(t *testing.T) { 33 | logger := log.NewTestLogger() 34 | ConfigService := config.NewService(logger) 35 | 36 | gc := &service.GlobalConfig{} 37 | err := ConfigService.Load(gc) 38 | require.NoError(t, err) 39 | 40 | // Validate config 41 | require.NoError(t, gc.ACHGateway.Validate()) 42 | } 43 | -------------------------------------------------------------------------------- /internal/service/model_admin.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package service 19 | 20 | type Admin struct { 21 | BindAddress string 22 | } 23 | 24 | func (cfg Admin) Validate() error { 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /internal/service/model_audit.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package service 19 | 20 | import ( 21 | "encoding/json" 22 | "errors" 23 | "os" 24 | 25 | "github.com/moov-io/achgateway/internal/mask" 26 | "github.com/moov-io/base/strx" 27 | ) 28 | 29 | type AuditTrail struct { 30 | ID string 31 | BucketURI string 32 | BasePath string // e.g. 'incoming' or 'outgoing' 33 | GPG *GPG 34 | } 35 | 36 | func (cfg *AuditTrail) Validate() error { 37 | if cfg == nil { 38 | return nil 39 | } 40 | if cfg.BucketURI == "" { 41 | return errors.New("missing bucket_uri") 42 | } 43 | return nil 44 | } 45 | 46 | type GPG struct { 47 | KeyFile string 48 | Signer *Signer 49 | } 50 | 51 | type Signer struct { 52 | KeyFile string 53 | KeyPassword string 54 | } 55 | 56 | func (cfg *Signer) Password() string { 57 | return strx.Or(os.Getenv("PIPELINE_SIGNING_KEY_PASSWORD"), cfg.KeyPassword) 58 | } 59 | 60 | func (cfg *Signer) MarshalJSON() ([]byte, error) { 61 | type Aux struct { 62 | KeyFile string 63 | KeyPassword string 64 | } 65 | return json.Marshal(Aux{ 66 | KeyFile: cfg.KeyFile, 67 | KeyPassword: mask.Password(cfg.Password()), 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /internal/service/model_audit_test.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package service 19 | 20 | import ( 21 | "encoding/json" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | func TestSignerMasking(t *testing.T) { 28 | cfg := &Signer{KeyFile: "/foo.pem", KeyPassword: "secret"} 29 | bs, err := json.Marshal(cfg) 30 | require.NoError(t, err) 31 | require.JSONEq(t, string(bs), `{"KeyFile":"/foo.pem","KeyPassword":"s****t"}`) 32 | } 33 | -------------------------------------------------------------------------------- /internal/service/model_config.go: -------------------------------------------------------------------------------- 1 | // generated-from:9e0782b937278abaee17ffb9be40bb7928f6d9aeac4d35aa713f071163fd474c DO NOT REMOVE, DO UPDATE 2 | 3 | // Licensed to The Moov Authors under one or more contributor 4 | // license agreements. See the NOTICE file distributed with 5 | // this work for additional information regarding copyright 6 | // ownership. The Moov Authors licenses this file to you under 7 | // the Apache License, Version 2.0 (the "License"); you may 8 | // not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | package service 21 | 22 | import ( 23 | "fmt" 24 | 25 | "github.com/moov-io/base/database" 26 | "github.com/moov-io/base/log" 27 | "github.com/moov-io/base/telemetry" 28 | ) 29 | 30 | type GlobalConfig struct { 31 | ACHGateway Config 32 | } 33 | 34 | type Config struct { 35 | Logger log.Logger `json:"-"` 36 | Clients *ClientConfig 37 | Database database.DatabaseConfig 38 | Telemetry telemetry.Config 39 | Admin Admin 40 | Inbound Inbound 41 | Events *EventsConfig 42 | Sharding Sharding 43 | Upload UploadAgents 44 | Errors ErrorAlerting 45 | } 46 | 47 | func (cfg *Config) Validate() error { 48 | if err := cfg.Admin.Validate(); err != nil { 49 | return fmt.Errorf("admin: %v", err) 50 | } 51 | if err := cfg.Inbound.Validate(); err != nil { 52 | return fmt.Errorf("inbound: %v", err) 53 | } 54 | if err := cfg.Events.Validate(); err != nil { 55 | return fmt.Errorf("events: %v", err) 56 | } 57 | if err := cfg.Sharding.Validate(); err != nil { 58 | return fmt.Errorf("sharding: %v", err) 59 | } 60 | if err := cfg.Upload.Validate(); err != nil { 61 | return fmt.Errorf("upload: %v", err) 62 | } 63 | if err := cfg.Errors.Validate(); err != nil { 64 | return fmt.Errorf("errors: %v", err) 65 | } 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /internal/service/model_errors.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package service 19 | 20 | import ( 21 | "errors" 22 | "fmt" 23 | ) 24 | 25 | type ErrorAlerting struct { 26 | PagerDuty *PagerDutyAlerting 27 | Slack *SlackAlerting 28 | } 29 | 30 | func (n ErrorAlerting) Validate() error { 31 | if n.PagerDuty != nil { 32 | if err := n.PagerDuty.Validate(); err != nil { 33 | return fmt.Errorf("pager duty config: %v", err) 34 | } 35 | } 36 | return nil 37 | } 38 | 39 | type PagerDutyAlerting struct { 40 | ApiKey string 41 | 42 | // To send an alert event we need to provide the value of 43 | // the Integration Key (add API integration to service in PD to get it) 44 | // as RoutingKey 45 | RoutingKey string 46 | } 47 | 48 | func (cfg PagerDutyAlerting) Validate() error { 49 | if cfg.ApiKey == "" { 50 | return errors.New("pagerduty error alerting: apiKey is missing") 51 | } 52 | if cfg.RoutingKey == "" { 53 | return errors.New("pagerduty error alerting: routingKey is missing") 54 | } 55 | return nil 56 | } 57 | 58 | type SlackAlerting struct { 59 | // Oauth 2.0 access tokens are generated manually when creating a slack app 60 | // https://api.slack.com/authentication/token-types 61 | AccessToken string 62 | 63 | // A default channel can be specified when creating a Slack app, and this config 64 | // can override or be used as the default 65 | ChannelID string 66 | } 67 | 68 | func (cfg SlackAlerting) Validate() error { 69 | if cfg.AccessToken == "" { 70 | return errors.New("slack error alerting: access token is missing") 71 | } 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /internal/service/model_events.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package service 19 | 20 | import ( 21 | "errors" 22 | 23 | "github.com/moov-io/achgateway/pkg/models" 24 | ) 25 | 26 | type EventsConfig struct { 27 | Stream *EventsStream 28 | Webhook *WebhookConfig 29 | Transform *models.TransformConfig 30 | } 31 | 32 | func (cfg *EventsConfig) Validate() error { 33 | if cfg == nil { 34 | return nil 35 | } 36 | if err := cfg.Stream.Validate(); err != nil { 37 | return err 38 | } 39 | if err := cfg.Webhook.Validate(); err != nil { 40 | return err 41 | } 42 | return nil 43 | } 44 | 45 | type EventsStream struct { 46 | InMem *InMemory 47 | Kafka *KafkaConfig 48 | } 49 | 50 | func (cfg *EventsStream) Validate() error { 51 | if cfg == nil { 52 | return nil 53 | } 54 | if err := cfg.Kafka.Validate(); err != nil { 55 | return err 56 | } 57 | return nil 58 | } 59 | 60 | type WebhookConfig struct { 61 | Endpoint string 62 | } 63 | 64 | func (cfg *WebhookConfig) Validate() error { 65 | if cfg == nil { 66 | return nil 67 | } 68 | if cfg.Endpoint == "" { 69 | return errors.New("missing endpoint") 70 | } 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /internal/service/model_sharding_test.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package service 19 | 20 | import ( 21 | "strings" 22 | "testing" 23 | 24 | "github.com/spf13/viper" 25 | "github.com/stretchr/testify/require" 26 | ) 27 | 28 | func TestShardRead(t *testing.T) { 29 | data := strings.NewReader(` 30 | name: "testing" 31 | cutoffs: 32 | timezone: "America/New_York" 33 | windows: ["12:30"] 34 | uploadAgent: "testing" 35 | outboundFilenameTemplate: |+ 36 | 37 | {{ .ShardName }}-{{ .Index }}.ach 38 | `) 39 | var cfg Shard 40 | 41 | deflt := viper.New() 42 | deflt.SetConfigType("yaml") 43 | 44 | require.NoError(t, deflt.ReadConfig(data)) 45 | require.NoError(t, deflt.Unmarshal(&cfg)) 46 | 47 | require.Equal(t, "{{ .ShardName }}-{{ .Index }}.ach", cfg.FilenameTemplate()) 48 | } 49 | -------------------------------------------------------------------------------- /internal/service/model_tls.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package service 19 | 20 | // TLSConfig specifies filepaths where a TLS certificate chain and private key can be found. 21 | type TLSConfig struct { 22 | // CertFile points to a filename containing an X.509 certificate chain usable to 23 | // wrap HTTP connections with TLS. 24 | CertFile string 25 | 26 | // KeyFile points to a filename containing a matching private key for encrypting 27 | // and signing TLS connections found in CertFile. 28 | KeyFile string 29 | } 30 | -------------------------------------------------------------------------------- /internal/service/model_upload_test.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package service 19 | 20 | import ( 21 | "encoding/json" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | func TestFTPMasking(t *testing.T) { 28 | cfg := &FTP{Password: "secret"} 29 | bs, err := json.Marshal(cfg) 30 | require.NoError(t, err) 31 | require.Contains(t, string(bs), `,"Password":"s****t",`) 32 | } 33 | 34 | func TestSFTPMasking(t *testing.T) { 35 | cfg := &SFTP{Password: "secret"} 36 | bs, err := json.Marshal(cfg) 37 | require.NoError(t, err) 38 | require.Contains(t, string(bs), `,"Password":"s****t",`) 39 | } 40 | -------------------------------------------------------------------------------- /internal/service/termination.go: -------------------------------------------------------------------------------- 1 | // generated-from:1707fd7fce48bdd1cbfbbd9efcc7347ad3bdc8b6b8286d28dde59f4d919c4df0 DO NOT REMOVE, DO UPDATE 2 | 3 | // Licensed to The Moov Authors under one or more contributor 4 | // license agreements. See the NOTICE file distributed with 5 | // this work for additional information regarding copyright 6 | // ownership. The Moov Authors licenses this file to you under 7 | // the Apache License, Version 2.0 (the "License"); you may 8 | // not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | package service 21 | 22 | import ( 23 | "fmt" 24 | "os" 25 | "os/signal" 26 | "syscall" 27 | 28 | "github.com/moov-io/base/log" 29 | ) 30 | 31 | func NewTerminationListener() chan error { 32 | errs := make(chan error) 33 | go func() { 34 | c := make(chan os.Signal, 1) 35 | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) 36 | errs <- fmt.Errorf("%s", <-c) 37 | }() 38 | 39 | return errs 40 | } 41 | 42 | func AwaitTermination(logger log.Logger, terminationListener chan error) error { 43 | if err := <-terminationListener; err != nil { 44 | return logger.Fatal().LogErrorf("Terminated: %v", err).Err() 45 | } 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/service/termination_test.go: -------------------------------------------------------------------------------- 1 | // generated-from:1707fd7fce48bdd1cbfbbd9efcc7347ad3bdc8b6b8286d28dde59f4d919c4df0 DO NOT REMOVE, DO UPDATE 2 | 3 | // Licensed to The Moov Authors under one or more contributor 4 | // license agreements. See the NOTICE file distributed with 5 | // this work for additional information regarding copyright 6 | // ownership. The Moov Authors licenses this file to you under 7 | // the Apache License, Version 2.0 (the "License"); you may 8 | // not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | package service_test 21 | 22 | import ( 23 | "errors" 24 | "testing" 25 | 26 | "github.com/moov-io/achgateway/internal/service" 27 | "github.com/moov-io/base/log" 28 | 29 | "github.com/stretchr/testify/assert" 30 | "github.com/stretchr/testify/require" 31 | ) 32 | 33 | func TestTermination(t *testing.T) { 34 | listener := service.NewTerminationListener() 35 | err := make(chan error) 36 | go func() { 37 | err <- service.AwaitTermination(log.NewTestLogger(), listener) 38 | }() 39 | listener <- errors.New("foo") 40 | 41 | got := <-err 42 | require.Error(t, got) 43 | assert.Contains(t, got.Error(), "Terminated: foo") 44 | } 45 | -------------------------------------------------------------------------------- /internal/shards/inmemory_repository.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package shards 19 | 20 | import ( 21 | "database/sql" 22 | "fmt" 23 | 24 | "github.com/moov-io/achgateway/internal/service" 25 | "github.com/moov-io/base/database" 26 | ) 27 | 28 | type InMemoryRepository struct { 29 | Shards []service.ShardMapping 30 | } 31 | 32 | func NewInMemoryRepository(shards ...service.ShardMapping) *InMemoryRepository { 33 | repo := &InMemoryRepository{} 34 | repo.Shards = append(repo.Shards, shards...) 35 | return repo 36 | } 37 | 38 | func (r *InMemoryRepository) Lookup(shardKey string) (string, error) { 39 | for i := range r.Shards { 40 | if r.Shards[i].ShardKey == shardKey { 41 | return r.Shards[i].ShardName, nil 42 | } 43 | } 44 | return "", fmt.Errorf("unknown shardKey %s: %w", shardKey, sql.ErrNoRows) 45 | } 46 | 47 | func (r *InMemoryRepository) List() ([]service.ShardMapping, error) { 48 | list := make([]service.ShardMapping, 0, len(r.Shards)) 49 | list = append(list, r.Shards...) 50 | return list, nil 51 | } 52 | 53 | func (r *InMemoryRepository) Add(create service.ShardMapping, _ database.RunInTx) error { 54 | r.Shards = append(r.Shards, create) 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /internal/shards/repository_test.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package shards 19 | 20 | import ( 21 | "testing" 22 | 23 | "github.com/moov-io/achgateway/internal/dbtest" 24 | "github.com/moov-io/base" 25 | 26 | "github.com/stretchr/testify/require" 27 | ) 28 | 29 | func TestRepository(t *testing.T) { 30 | if testing.Short() { 31 | t.Skip("-short flag was specified") 32 | } 33 | 34 | conf := dbtest.CreateTestDatabase(t, dbtest.LocalDatabaseConfig()) 35 | db := dbtest.LoadDatabase(t, conf) 36 | require.NoError(t, db.Ping()) 37 | 38 | shardKey := base.ID() 39 | shardName := "ftp-live" 40 | 41 | repo := NewRepository(db, nil) 42 | rr, ok := repo.(*sqlRepository) 43 | require.True(t, ok) 44 | 45 | err := rr.write(shardKey, shardName) 46 | require.NoError(t, err) 47 | 48 | found, err := repo.Lookup(shardKey) 49 | require.NoError(t, err) 50 | require.Equal(t, shardName, found) 51 | } 52 | -------------------------------------------------------------------------------- /internal/shards/service_shard_mapping_test.go: -------------------------------------------------------------------------------- 1 | package shards_test 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/moov-io/achgateway/internal/service" 6 | "github.com/stretchr/testify/require" 7 | "testing" 8 | ) 9 | 10 | func TestFacilitatorService_Create(t *testing.T) { 11 | s := ShardMappingTestSetup(t) 12 | 13 | shardKey := "test" 14 | shardName := "tester" 15 | create := &service.ShardMapping{ 16 | ShardKey: shardKey, 17 | ShardName: shardName, 18 | } 19 | 20 | shard, err := s.Service.Create(create) 21 | require.NoError(t, err) 22 | require.Equal(t, shardName, shard.ShardName) 23 | } 24 | 25 | func TestFacilitatorService_List(t *testing.T) { 26 | s := ShardMappingTestSetup(t) 27 | 28 | create1 := &service.ShardMapping{ 29 | ShardKey: "test1", 30 | ShardName: "tester1", 31 | } 32 | create2 := &service.ShardMapping{ 33 | ShardKey: "test2", 34 | ShardName: "tester2", 35 | } 36 | create3 := &service.ShardMapping{ 37 | ShardKey: "test3", 38 | ShardName: "tester3", 39 | } 40 | 41 | _, err := s.Service.Create(create1) 42 | require.NoError(t, err) 43 | 44 | _, err = s.Service.Create(create2) 45 | require.NoError(t, err) 46 | 47 | _, err = s.Service.Create(create3) 48 | require.NoError(t, err) 49 | 50 | list, err := s.Service.List() 51 | require.NoError(t, err) 52 | require.Len(t, list, 3) 53 | } 54 | 55 | func TestFacilitatorService_Get(t *testing.T) { 56 | s := ShardMappingTestSetup(t) 57 | 58 | shardKey := uuid.NewString() 59 | shardName := "someName" 60 | 61 | create := &service.ShardMapping{ 62 | ShardKey: shardKey, 63 | ShardName: shardName, 64 | } 65 | 66 | _, err := s.Service.Create(create) 67 | require.NoError(t, err) 68 | 69 | foundName, err := s.Service.Lookup(shardKey) 70 | require.NoError(t, err) 71 | require.Equal(t, shardName, foundName) 72 | } 73 | -------------------------------------------------------------------------------- /internal/sshx/keys.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package sshx 6 | 7 | import ( 8 | "encoding/base64" 9 | 10 | "golang.org/x/crypto/ssh" 11 | ) 12 | 13 | // ReadPubKey attempts to parse data and return an ssh PublicKey. 14 | // 15 | // It attempts formats such as: 16 | // - an authorized_keys file used with OpenSSH according to the sshd(8) manual page 17 | // - used in the SSH wire protocol according to RFC 4253, section 6.6 18 | func ReadPubKey(data []byte) (ssh.PublicKey, error) { 19 | readAuthd := func(data []byte) (ssh.PublicKey, error) { 20 | pub, _, _, _, err := ssh.ParseAuthorizedKey(data) 21 | return pub, err 22 | } 23 | 24 | decoded, err := base64.StdEncoding.DecodeString(string(data)) 25 | if len(decoded) > 0 && err == nil { 26 | if pub, err := readAuthd(decoded); pub != nil && err == nil { 27 | return pub, nil 28 | } 29 | return ssh.ParsePublicKey(decoded) 30 | } 31 | 32 | if pub, err := readAuthd(data); pub != nil && err == nil { 33 | return pub, nil 34 | } 35 | return ssh.ParsePublicKey(data) 36 | } 37 | -------------------------------------------------------------------------------- /internal/sshx/keys_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package sshx 6 | 7 | import ( 8 | "bytes" 9 | "crypto/rsa" 10 | "encoding/base64" 11 | "os" 12 | "path/filepath" 13 | "testing" 14 | "time" 15 | 16 | "github.com/ProtonMail/go-crypto/openpgp" 17 | "github.com/ProtonMail/go-crypto/openpgp/armor" 18 | "github.com/ProtonMail/go-crypto/openpgp/packet" 19 | "github.com/stretchr/testify/require" 20 | "golang.org/x/crypto/ssh" 21 | ) 22 | 23 | func TestSSHX__read(t *testing.T) { 24 | data, err := os.ReadFile(filepath.Join("testdata", "rsa-2048.pub")) 25 | require.NoError(t, err) 26 | key, err := ReadPubKey(data) 27 | require.NoError(t, err) 28 | 29 | if pk, ok := key.(ssh.CryptoPublicKey); ok { 30 | t.Logf("ssh: pk=%#v", pk) 31 | if pk, ok := pk.CryptoPublicKey().(*rsa.PublicKey); ok { 32 | t.Logf("rsa: pk=%#v", pk) 33 | 34 | var buf bytes.Buffer 35 | w, err := armor.Encode(&buf, openpgp.PublicKeyType, make(map[string]string)) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | pgpKey := packet.NewRSAPublicKey(time.Now(), pk) 41 | require.NotNil(t, pgpKey) 42 | 43 | w.Close() 44 | } 45 | } 46 | } 47 | 48 | func TestSSHX_ReadPubKey(t *testing.T) { 49 | // TODO(adam): test with '-----BEGIN RSA PRIVATE KEY-----' PKCS#8 format 50 | 51 | check := func(t *testing.T, data []byte) { 52 | key, err := ReadPubKey(data) 53 | if key == nil || err != nil { 54 | t.Fatalf("PublicKey=%v error=%v", key, err) 55 | } 56 | 57 | // base64 Encoded 58 | data = []byte(base64.StdEncoding.EncodeToString(data)) 59 | key, err = ReadPubKey(data) 60 | if key == nil || err != nil { 61 | t.Fatalf("PublicKey=%v error=%v", key, err) 62 | } 63 | } 64 | 65 | // Keys generated with 'ssh-keygen -t rsa -b 2048 -f test' (or 4096) 66 | data, err := os.ReadFile(filepath.Join("testdata", "rsa-2048.pub")) 67 | require.NoError(t, err) 68 | check(t, data) 69 | 70 | data, err = os.ReadFile(filepath.Join("testdata", "rsa-4096.pub")) 71 | require.NoError(t, err) 72 | check(t, data) 73 | } 74 | -------------------------------------------------------------------------------- /internal/sshx/testdata/rsa-2048.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCrrAOL3EmhYyOWQ3nrmKTC5u64owChNpF8oTyByjHnD15tF9U3mjVfkQnp5jAdjH4UeTdrwKXgPWm0IwlFqDXjoFhckvyYHocH0Y7E1zva1C6ztr4aeKGi3r9jHKVoWJFOl/BcONFW+imXj03m5s74PE7AVI60vwX5ZRP1KEf6Z74A7XWKhl5cOGDCgjuT43t5A531LT+zJiZJ0+oVl/NMoJZCTb7oFD6tKeoJI10Sd4YqUkl5ivqLaGaP4WySywRvJ107+8PFBfM1lhvfg8tpXcb0Hhn7HSVAGEKXb5dN5y7Id6d5oR/7vEFW1dCHemjBEBXOZInz2F4NdJHWlecf adam@Adams-MacBook-Pro.local 2 | -------------------------------------------------------------------------------- /internal/sshx/testdata/rsa-4096.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCucFoXj7Zy2DJ+LMzyY0LLdZxcmSJZNtlea3dGlsiZJaVPoL75eVaz9uN+gcPNBSDKOAkbPK/DqAGiHtABIfanTEkkYTZQJ4xUIFzSd5W+aWIN8Q0mTRS1u3WISe2bg48UZPDCcpzSey2IaTyDItMQxtouiTtbhufOhCzxR0Ntb+w0D1EzFD3ME+HollSeNsjbra0qlZCF8C2ddDHJTziNwdhT9dqH/B41dSnXhCL6pNMBQYLhBfeM8H68EOjIAej+ZIW7ByqswH43K2nfPR5cshh/iX2X9BJ/gbJON5/IqScCFBrBL4VGc+GQ+WGfHU0XfbMsmVepZ5ELB0Ylf+p66Pirldyi0loOUPwSKKoWH4CRf8xZMm4TxDjmvZ+75IONKgJ7PTw9JH3Aso8ZJuL6XHc+wg493h681zoEvJ10Fxv7+NHbs21ArNVwkmYGo6KyJKutw1vNA9KfVdNnMLUFagVgIF9RZzuPa2WkTqE5ntFRM4o8FFUyRHI1l4/ftBPPg8rhVYEw+9nTHIeySgz7pahWkmh4c3eoLS0GdUt2zFBhQkTn97+67pM84iaQ1qmJoGTzlnuhYdQO5EnDgGmul4i0zDizNZn4OpnfYc+do+6E+Lk7+pN9yuJnstxPKLZtH5ZVfPGHJAizHV79D04of9cUSdCnqZBRqhT1bNJYNw== adam@Adams-MacBook-Pro.local 2 | -------------------------------------------------------------------------------- /internal/storage/blob.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | // TODO(adam): There might be a problem with implementing m.isolateMergableDir() 4 | // with blob storage. Those buckets don't typically have "paths" (directories) and 5 | // can't move them in an atomic fashion. 6 | 7 | // TODO(adam): There might be a problem supporting 'Glob(pattern string) ([]string, error)' 8 | // with blob storage. 9 | -------------------------------------------------------------------------------- /internal/storage/buf.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "bytes" 5 | "io/fs" 6 | ) 7 | 8 | type buffer struct { 9 | b *bytes.Buffer 10 | 11 | info fs.FileInfo 12 | 13 | filename string 14 | fullpath string 15 | } 16 | 17 | func (b *buffer) Filename() string { 18 | return b.filename 19 | } 20 | 21 | func (b *buffer) FullPath() string { 22 | return b.fullpath 23 | } 24 | 25 | func (b *buffer) Stat() (fs.FileInfo, error) { 26 | return b.info, nil 27 | } 28 | 29 | func (b *buffer) Read(data []byte) (int, error) { 30 | return b.b.Read(data) 31 | } 32 | 33 | func (b *buffer) Close() error { 34 | b.b.Reset() 35 | return nil 36 | } 37 | 38 | var _ File = (&buffer{}) 39 | -------------------------------------------------------------------------------- /internal/storage/config.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | type Config struct { 4 | Filesystem FilesystemConfig 5 | Encryption EncryptionConfig 6 | } 7 | 8 | type FilesystemConfig struct { 9 | Directory string 10 | } 11 | 12 | type EncryptionConfig struct { 13 | AES *AESConfig 14 | Encoding string 15 | } 16 | 17 | type AESConfig struct { 18 | Base64Key string 19 | } 20 | -------------------------------------------------------------------------------- /internal/storage/encryption_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/moov-io/cryptfs" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestEncrypted(t *testing.T) { 12 | dir := t.TempDir() 13 | 14 | chest, err := NewFilesystem(dir) 15 | require.NoError(t, err) 16 | 17 | aes, err := cryptfs.NewAESCryptor([]byte("1234567887654321")) 18 | require.NoError(t, err) 19 | 20 | crypt, err := cryptfs.New(aes) 21 | require.NoError(t, err) 22 | crypt.SetCoder(cryptfs.Base64()) 23 | 24 | testStorage(t, NewEncrypted(chest, crypt)) 25 | 26 | finalContents := readFinalContents(t, chest) 27 | require.NotEqual(t, "nacha", finalContents) 28 | 29 | decrypted, err := crypt.Reveal([]byte(finalContents)) 30 | require.NoError(t, err) 31 | require.Equal(t, []byte("nacha"), decrypted) 32 | } 33 | -------------------------------------------------------------------------------- /internal/storage/file.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "time" 7 | ) 8 | 9 | type File interface { 10 | Filename() string 11 | FullPath() string 12 | 13 | Stat() (fs.FileInfo, error) 14 | Read([]byte) (int, error) 15 | Close() error 16 | } 17 | 18 | type file struct { 19 | *os.File 20 | 21 | filename string 22 | fullpath string 23 | } 24 | 25 | func (f *file) Filename() string { 26 | return f.filename 27 | } 28 | 29 | func (f *file) FullPath() string { 30 | return f.fullpath 31 | } 32 | 33 | var ( 34 | _ fs.File = (&file{}) 35 | _ File = (&file{}) 36 | ) 37 | 38 | type FileStat struct { 39 | RelativePath string 40 | ModTime time.Time 41 | } 42 | -------------------------------------------------------------------------------- /internal/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "io/fs" 7 | "strings" 8 | 9 | "github.com/moov-io/cryptfs" 10 | ) 11 | 12 | type Chest interface { 13 | Open(path string) (fs.File, error) 14 | Glob(pattern string) ([]FileStat, error) 15 | ReadDir(name string) ([]fs.DirEntry, error) 16 | 17 | ReplaceFile(oldpath, newpath string) error 18 | ReplaceDir(oldpath, newpath string) error 19 | 20 | MkdirAll(path string) error 21 | RmdirAll(path string) error 22 | 23 | WriteFile(path string, contents []byte) error 24 | } 25 | 26 | // New returns a Chest given the configuration provided. It can also wrap that Chest 27 | // in an encryption routine on each operation. 28 | func New(cfg Config) (Chest, error) { 29 | underlying, err := NewFilesystem(cfg.Filesystem.Directory) 30 | if err != nil { 31 | return nil, fmt.Errorf("error initializing filesystem storage: %w", err) 32 | } 33 | 34 | if cfg.Encryption.AES != nil { 35 | enc, err := createBase64AESCryptor(cfg.Encryption.AES.Base64Key) 36 | if err != nil { 37 | return nil, fmt.Errorf("error creating AES cryptor: %w", err) 38 | } 39 | 40 | fs, err := cryptfs.New(enc) 41 | if err != nil { 42 | return nil, fmt.Errorf("error creating cryptfs: %w", err) 43 | } 44 | 45 | switch strings.ToLower(cfg.Encryption.Encoding) { 46 | case "base64": 47 | fs.SetCoder(cryptfs.Base64()) 48 | } 49 | 50 | return NewEncrypted(underlying, fs), nil 51 | } 52 | 53 | return underlying, nil 54 | } 55 | 56 | func createBase64AESCryptor(key string) (*cryptfs.AESCryptor, error) { 57 | decoded, err := base64.RawStdEncoding.DecodeString(key) 58 | if err != nil { 59 | return nil, err 60 | } 61 | return cryptfs.NewAESCryptor(decoded) 62 | } 63 | -------------------------------------------------------------------------------- /internal/storage/storage_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func testStorage(t *testing.T, chest Chest) { 11 | t.Helper() 12 | 13 | // read / write testing 14 | file, err := chest.Open("foo/bar.txt") 15 | require.Error(t, err) 16 | require.Nil(t, file) 17 | 18 | err = chest.WriteFile("foo/bar.txt", []byte("hello, world")) 19 | require.NoError(t, err) 20 | 21 | file, err = chest.Open("foo/bar.txt") 22 | require.NoError(t, err) 23 | 24 | bs, err := io.ReadAll(file) 25 | require.NoError(t, err) 26 | require.Equal(t, bs, []byte("hello, world")) 27 | require.NoError(t, file.Close()) 28 | 29 | // replace file 30 | err = chest.WriteFile("test-20210101-0101/foo.ach", []byte("nacha")) 31 | require.NoError(t, err) 32 | err = chest.ReplaceFile("test-20210101-0101/foo.ach", "after/foo.ach.canceled") 33 | require.NoError(t, err) 34 | file, err = chest.Open("after/foo.ach.canceled") 35 | require.NoError(t, err) 36 | require.NoError(t, file.Close()) 37 | 38 | // replace dir 39 | err = chest.ReplaceDir("after/", "final/") 40 | require.NoError(t, err) 41 | file, err = chest.Open("final/foo.ach.canceled") 42 | require.NoError(t, err) 43 | require.NoError(t, file.Close()) 44 | } 45 | 46 | func readFinalContents(t *testing.T, chest Chest) string { 47 | t.Helper() 48 | 49 | file, err := chest.Open("final/foo.ach.canceled") 50 | require.NoError(t, err) 51 | 52 | defer func() { 53 | if file != nil { 54 | file.Close() 55 | } 56 | }() 57 | 58 | bs, err := io.ReadAll(file) 59 | require.NoError(t, err) 60 | 61 | return string(bs) 62 | } 63 | -------------------------------------------------------------------------------- /internal/transform/gpg.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package transform 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | 11 | "github.com/moov-io/ach" 12 | "github.com/moov-io/achgateway/internal/gpgx" 13 | "github.com/moov-io/achgateway/internal/service" 14 | "github.com/moov-io/cryptfs" 15 | 16 | "github.com/ProtonMail/go-crypto/openpgp" 17 | ) 18 | 19 | type GPGEncryption struct { 20 | encryptor *cryptfs.FS 21 | signingKey openpgp.EntityList 22 | } 23 | 24 | func NewGPGEncryptor(cfg *service.GPG) (*GPGEncryption, error) { 25 | if cfg == nil { 26 | return nil, errors.New("missing GPG config") 27 | } 28 | 29 | out := &GPGEncryption{} 30 | 31 | cc, err := cryptfs.FromCryptor(cryptfs.NewGPGEncryptorFile(cfg.KeyFile)) 32 | if err != nil { 33 | return nil, err 34 | } 35 | out.encryptor = cc 36 | 37 | // Read a signing key if it exists 38 | if cfg.Signer != nil { 39 | privKey, err := gpgx.ReadPrivateKeyFile(cfg.Signer.KeyFile, []byte(cfg.Signer.Password())) 40 | if err != nil { 41 | return nil, err 42 | } 43 | out.signingKey = privKey 44 | } 45 | 46 | return out, nil 47 | } 48 | 49 | func (morph *GPGEncryption) Transform(res *Result) (*Result, error) { 50 | var buf bytes.Buffer 51 | if err := ach.NewWriter(&buf).Write(res.File); err != nil { 52 | return res, err 53 | } 54 | 55 | bs, err := morph.encryptor.Disfigure(buf.Bytes()) 56 | if err != nil { 57 | return res, err 58 | } 59 | 60 | // Sign the file after encrypting it 61 | if len(morph.signingKey) > 0 { 62 | bs, err = gpgx.Sign(bs, morph.signingKey) 63 | if err != nil { 64 | return res, err 65 | } 66 | } 67 | 68 | res.Encrypted = bs 69 | return res, nil 70 | } 71 | 72 | func (morph *GPGEncryption) String() string { 73 | if morph == nil { 74 | return "GPG: " 75 | } 76 | return "GPG{...}" 77 | } 78 | -------------------------------------------------------------------------------- /internal/transform/preupload.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package transform 6 | 7 | import ( 8 | "github.com/moov-io/ach" 9 | 10 | "github.com/moov-io/achgateway/internal/service" 11 | 12 | "github.com/moov-io/base/log" 13 | ) 14 | 15 | type Result struct { 16 | File *ach.File 17 | Original []byte 18 | Encrypted []byte 19 | } 20 | 21 | type PreUpload interface { 22 | Transform(res *Result) (*Result, error) 23 | } 24 | 25 | // ForUpload iterates each Transformer over an ACH file and mutates it along the way 26 | func ForUpload(file *ach.File, funcs []PreUpload) (*Result, error) { 27 | res := &Result{File: file} 28 | 29 | var err error 30 | for i := range funcs { 31 | res, err = funcs[i].Transform(res) 32 | if err != nil { 33 | return res, err 34 | } 35 | } 36 | 37 | return res, nil 38 | } 39 | 40 | // Multi is a constructor from our config package for PreUpload transformers 41 | func Multi(logger log.Logger, cfg *service.PreUpload) ([]PreUpload, error) { 42 | if cfg == nil { 43 | return nil, nil 44 | } 45 | var processors []PreUpload 46 | if cfg.GPG != nil { 47 | pc, err := NewGPGEncryptor(cfg.GPG) 48 | if err != nil { 49 | return nil, err 50 | } 51 | processors = append(processors, pc) 52 | } 53 | return processors, nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/transform/testdata/moov.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQENBF7axTsBCADYyHvREj/LhjhxXxUa/dFVTtLnx6rlQeAWT37D0FsmoOLQF+dY 4 | JsBGVOLnxwSyXrXsMWbGT558SV3pbhDDp2kcOpcHj7q8hHPsT4hUnfoT/Ds03vkj 5 | jawtiqdKly16PU0+3XRvmKDU5YyUxVEqVF7siaP2PnNb1AotsK3ejuj98uewzc/F 6 | 90wJe5+n1vruqUly1h+85q8luLDST9cKkWTXNLRRwPQ9hyTWqcbF3QhtxcJpEn/e 7 | KLE3dUyLPYbeDB+h1WFlbSDHKXTeDccSnku0CFjMCDhAGLhSbpl1JPg7SmLBZ7Pd 8 | /spGkpr7XHO+5UGHJcl9QQTrirs23/b5EhjTABEBAAG0G01vb3YsIEluYyA8c3Vw 9 | cG9ydEBtb292LmlvPokBVAQTAQgAPhYhBN07iskY8VKmydbyjlnCkplxa2vFBQJe 10 | 2sU7AhsDBQkDwmcABQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEFnCkplxa2vF 11 | hIYIAJAFYlH17mekM4Wd8wN/eGtPeqrVfTlupnPCAi3mZ4uOPn5P4y/dC0/FwiYP 12 | z5SNgwNUuhsOVnqz04uaYFMhAknFvqRLyuLMJJvxRJgDjaGS6gSx0+9RGJab0YIn 13 | 8D+1tLm/Kgg9zKwU80uVt78wk0pUc6pAqlkHHH7dCGKs0t6OIB3unhidOZ8Ffy1N 14 | hAH1Hxwt4vGcUiZn77VLxboOdDABaPpQM+ya9YhZdiNT3IQZk3VMDb9hLYBYIljl 15 | xsStZBOy/54qtF1h8LGZnVLxkTkeYyGJtuJexZVPJjWzPoUe0ZH0mbCIAPNe/Jft 16 | gPNHOazjD9RxW15DX0fRtNXrqQG5AQ0EXtrFOwEIAPxMJvdlv8fKxBlE5cSLOaP7 17 | mR4VKhKBbNFjT5+4XfoWQl296b95Dfg1b0l2xIO8hKgPQ/A6DxiB4zBo6NM0fVJb 18 | P/ne9p/tjcsDKeofMKP5/GVZB9jRougpzYL1QL4UPrTQRUsh0mdu3xVASgHZsw8U 19 | /dWYRYJnZHRBiC4DiB846uj2QpU8PU2YsNvVC7Ey1FV62UYMElWol0iNnpO70rzb 20 | kFV31WnktCqLxSiN44iOL05Wok5ttq2E2qNwnNuEnOrLJLhXF4FOz1+uq+hP4e5b 21 | /M4IRXl59FX5bIMaBRXsJQevhPJa9x2LWHZsWnwkyjDGUBN1+b0vhpSc3siffskA 22 | EQEAAYkBPAQYAQgAJhYhBN07iskY8VKmydbyjlnCkplxa2vFBQJe2sU7AhsMBQkD 23 | wmcAAAoJEFnCkplxa2vFsxIH/3P6s4BuSmuDirVEjzuiUTO7b8GvNDosNGXBnVWU 24 | fSfyx+pO9O9uyegKljcTQQxPeB/KONb34Mt74dFLxTnXtrCTjTd2xXIXaMWLO4Iu 25 | DytIJ+DCIs/ssExebEhzg/OBZy0n1mfxd0cWg0nDArWG4B954xfS0Gw6gRfyDTOa 26 | 8LVFcvPmjX7mbd3CwcRPF0kjFHdO38uFTsxB9yyAEppQrfhG9HrXEG/QLwX7SZx7 27 | 7p7VwYwXvxHlcHp7gze0It7QkPoODF6OmoOFEcMA1aHQgtsUImfZaEuWrXGwfl9I 28 | 91Se7MhkbC5WBo6OjEu9o4ydksPSSh3HoJ9xP6PmV0ZfBUA= 29 | =15+W 30 | -----END PGP PUBLIC KEY BLOCK----- 31 | -------------------------------------------------------------------------------- /internal/upload/agent_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package upload 6 | 7 | import ( 8 | "testing" 9 | "time" 10 | 11 | "github.com/moov-io/achgateway/internal/service" 12 | "github.com/moov-io/base/log" 13 | 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestAgent(t *testing.T) { 18 | cfg := service.UploadAgents{ 19 | Agents: []service.UploadAgent{ 20 | { 21 | ID: "mock", 22 | Mock: &service.MockAgent{}, 23 | }, 24 | }, 25 | } 26 | agent, err := New(log.NewTestLogger(), cfg, "mock") 27 | require.NoError(t, err) 28 | 29 | if aa, ok := agent.(*MockAgent); !ok { 30 | t.Errorf("unexpected agent: %#v", aa) 31 | } 32 | 33 | // check Agent was registered 34 | require.Len(t, createdAgents.agents, 1) 35 | 36 | _, ok := createdAgents.agents[0].(*MockAgent) 37 | require.True(t, ok) 38 | 39 | // setup a second (retrying) agent 40 | cfg.Retry = &service.UploadRetry{ 41 | Interval: 1 * time.Second, 42 | MaxRetries: 3, 43 | } 44 | agent, err = New(log.NewTestLogger(), cfg, "mock") 45 | require.NoError(t, err) 46 | 47 | if aa, ok := agent.(*RetryAgent); !ok { 48 | t.Errorf("unexpected agent: %#v", agent) 49 | } else { 50 | if aa, ok := aa.underlying.(*MockAgent); !ok { 51 | t.Errorf("unexpected agent: %#v", aa) 52 | } 53 | } 54 | 55 | // check Agent was registered 56 | require.Len(t, createdAgents.agents, 2) 57 | 58 | retr, ok := createdAgents.agents[1].(*RetryAgent) 59 | require.True(t, ok) 60 | 61 | _, ok = retr.underlying.(*MockAgent) 62 | require.True(t, ok) 63 | } 64 | -------------------------------------------------------------------------------- /internal/upload/file.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package upload 6 | 7 | import ( 8 | "io" 9 | ) 10 | 11 | type File struct { 12 | Filepath string 13 | Contents io.ReadCloser 14 | } 15 | 16 | func (f File) Close() error { 17 | if f.Contents != nil { 18 | return f.Contents.Close() 19 | } 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/upload/file_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package upload 6 | 7 | import ( 8 | "io" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestFile__close(t *testing.T) { 14 | var f File 15 | if err := f.Close(); err != nil { 16 | t.Error(err) 17 | } 18 | 19 | f.Contents = io.NopCloser(strings.NewReader("test")) 20 | if err := f.Close(); err != nil { 21 | t.Error(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/upload/filename_template.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package upload 6 | 7 | import ( 8 | "bytes" 9 | "os" 10 | "strings" 11 | "text/template" 12 | "time" 13 | ) 14 | 15 | type FilenameData struct { 16 | RoutingNumber string 17 | 18 | // GPG is true if the file has been encrypted with GPG 19 | GPG bool 20 | 21 | // Index is the Nth file uploaded for a shard during a cutoff time 22 | Index int 23 | 24 | // ShardName is the name of a shard uploading this file 25 | ShardName string 26 | } 27 | 28 | var filenameFunctions template.FuncMap = map[string]interface{}{ 29 | "date": func(pattern string) string { 30 | return time.Now().Format(pattern) 31 | }, 32 | "env": func(name string) string { 33 | return os.Getenv(name) 34 | }, 35 | "lower": func(s string) string { 36 | return strings.ToLower(s) 37 | }, 38 | "upper": func(s string) string { 39 | return strings.ToUpper(s) 40 | }, 41 | } 42 | 43 | func RenderACHFilename(raw string, data FilenameData) (string, error) { 44 | t, err := template.New(data.RoutingNumber).Funcs(filenameFunctions).Parse(strings.TrimSpace(raw)) 45 | if err != nil { 46 | return "", err 47 | } 48 | 49 | var buf bytes.Buffer 50 | if err := t.Execute(&buf, data); err != nil { 51 | return "", err 52 | } 53 | return strings.TrimSpace(buf.String()), nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/upload/network_security.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package upload 6 | 7 | import ( 8 | "fmt" 9 | "net" 10 | "strings" 11 | ) 12 | 13 | func rejectOutboundIPRange(allowedIPs []string, hostname string) error { 14 | // perform an initial check to see if we can resolve the hostname 15 | if strings.Contains(hostname, ":") { 16 | if host, _, err := net.SplitHostPort(hostname); err != nil { 17 | return err 18 | } else { 19 | hostname = host 20 | } 21 | } 22 | addrs, err := net.LookupIP(hostname) 23 | if len(addrs) == 0 || err != nil { 24 | return fmt.Errorf("unable to resolve (found %d) %s: %v", len(addrs), hostname, err) 25 | } 26 | // skip whitelist check if none were specified, assume it was empty in the config 27 | if len(allowedIPs) == 0 { 28 | return nil 29 | } 30 | for i := range allowedIPs { 31 | if strings.Contains(allowedIPs[i], "/") { 32 | ip, ipnet, err := net.ParseCIDR(allowedIPs[i]) 33 | if err != nil { 34 | return err 35 | } 36 | for j := range addrs { 37 | if ip.Equal(addrs[j]) || ipnet.Contains(addrs[j]) { 38 | return nil // whitelisted 39 | } 40 | } 41 | } else { 42 | for j := range addrs { 43 | ip := net.ParseIP(allowedIPs[i]) 44 | if ip != nil && ip.Equal(addrs[j]) { 45 | return nil // whitelisted 46 | } 47 | } 48 | } 49 | } 50 | return fmt.Errorf("%s is not whitelisted", addrs[0].String()) 51 | } 52 | -------------------------------------------------------------------------------- /internal/upload/network_security_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package upload 6 | 7 | import ( 8 | "net" 9 | "testing" 10 | 11 | "github.com/moov-io/achgateway/internal/service" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestRejectOutboundIPRange(t *testing.T) { 16 | addrs, err := net.LookupIP("moov.io") 17 | require.NoError(t, err) 18 | 19 | var addr net.IP 20 | for i := range addrs { 21 | if a := addrs[i].To4(); a != nil { 22 | addr = a 23 | break 24 | } 25 | } 26 | 27 | cfg := &service.UploadAgent{AllowedIPs: addr.String()} 28 | 29 | // exact IP match 30 | if err := rejectOutboundIPRange(cfg.SplitAllowedIPs(), "moov.io"); err != nil { 31 | t.Error(err) 32 | } 33 | 34 | // multiple whitelisted, but exact IP match 35 | cfg.AllowedIPs = "127.0.0.1/24," + addr.String() 36 | if err := rejectOutboundIPRange(cfg.SplitAllowedIPs(), "moov.io"); err != nil { 37 | t.Error(err) 38 | } 39 | 40 | // multiple whitelisted, match range (convert IP to /24) 41 | cfg.AllowedIPs = addr.Mask(net.IPv4Mask(0xFF, 0xFF, 0xFF, 0x0)).String() + "/24" 42 | if err := rejectOutboundIPRange(cfg.SplitAllowedIPs(), "moov.io"); err != nil { 43 | t.Error(err) 44 | } 45 | 46 | // no match 47 | cfg.AllowedIPs = "8.8.8.0/24" 48 | if err := rejectOutboundIPRange(cfg.SplitAllowedIPs(), "moov.io"); err == nil { 49 | t.Error("expected error") 50 | } 51 | 52 | // empty whitelist, allow all 53 | cfg.AllowedIPs = "" 54 | if err := rejectOutboundIPRange(cfg.SplitAllowedIPs(), "moov.io"); err != nil { 55 | t.Errorf("expected no error: %v", err) 56 | } 57 | 58 | // error cases 59 | cfg.AllowedIPs = "afkjsafkjahfa" 60 | if err := rejectOutboundIPRange(cfg.SplitAllowedIPs(), "moov.io"); err == nil { 61 | t.Error("expected error") 62 | } 63 | cfg.AllowedIPs = "10.0.0.0/8" 64 | if err := rejectOutboundIPRange(cfg.SplitAllowedIPs(), "lsjafkshfaksjfhas"); err == nil { 65 | t.Error("expected error") 66 | } 67 | cfg.AllowedIPs = "10...../8" 68 | if err := rejectOutboundIPRange(cfg.SplitAllowedIPs(), "moov.io"); err == nil { 69 | t.Error("expected error") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/util/timeout.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package util 6 | 7 | import ( 8 | "errors" 9 | "time" 10 | ) 11 | 12 | var ( 13 | ErrTimeout = errors.New("timeout exceeded") 14 | ) 15 | 16 | // Timeout will attempt to call f, but only for as long as t. If the function is still 17 | // processing after t has elapsed then ErrTimeout will be returned. 18 | func Timeout(f func() error, t time.Duration) error { 19 | answer := make(chan error) 20 | go func() { 21 | answer <- f() 22 | }() 23 | select { 24 | case err := <-answer: 25 | return err 26 | case <-time.After(t): 27 | return ErrTimeout 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/util/timeout_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | 5 | package util 6 | 7 | import ( 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestTimeout(t *testing.T) { 15 | start := time.Now() 16 | 17 | err := Timeout(func() error { 18 | time.Sleep(50 * time.Millisecond) 19 | return nil 20 | }, 1*time.Second) 21 | 22 | require.NoError(t, err) 23 | 24 | diff := time.Since(start) 25 | 26 | if diff < 50*time.Millisecond { 27 | t.Errorf("%v was under 50ms", diff) 28 | } 29 | if limit := 2 * 100 * time.Millisecond; diff > limit { 30 | t.Errorf("%v was over %v", diff, limit) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /migrations/000_noop.up.mysql.sql: -------------------------------------------------------------------------------- 1 | /* generated-from:1e9f80916cef2deb53c06de8bb1f3e1fdba2b8bef069d74d61d743ae6c65610f DO NOT REMOVE, DO UPDATE */ 2 | 3 | SELECT 1; -------------------------------------------------------------------------------- /migrations/001_shard_mappings.up.mysql.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE shard_mappings( 2 | shard_key VARCHAR(50) PRIMARY KEY, 3 | shard_name VARCHAR(50) NOT NULL, 4 | 5 | CONSTRAINT shard_mappings_unq_idx UNIQUE (shard_key, shard_name) 6 | ); 7 | -------------------------------------------------------------------------------- /migrations/001_shard_mappings.up.spanner.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE shard_mappings ( 2 | shard_key STRING(50) NOT NULL, 3 | shard_name STRING(50) NOT NULL, 4 | ) PRIMARY KEY (shard_key); 5 | -------------------------------------------------------------------------------- /migrations/002_files.up.mysql.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE files( 2 | file_id VARCHAR(128) PRIMARY KEY, 3 | shard_key VARCHAR(50) NOT NULL, 4 | hostname VARCHAR(100) NOT NULL, 5 | accepted_at TIMESTAMP NOT NULL, 6 | canceled_at TIMESTAMP 7 | ); 8 | -------------------------------------------------------------------------------- /migrations/002_files.up.spanner.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE files ( 2 | file_id STRING(MAX) NOT NULL, 3 | shard_key STRING(MAX) NOT NULL, 4 | hostname STRING(MAX) NOT NULL, 5 | accepted_at TIMESTAMP NOT NULL, 6 | canceled_at TIMESTAMP, 7 | ) PRIMARY KEY (file_id); 8 | -------------------------------------------------------------------------------- /pkg/compliance/compliance.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package compliance 19 | 20 | import ( 21 | "encoding/json" 22 | 23 | "github.com/moov-io/achgateway/pkg/models" 24 | ) 25 | 26 | func Protect(cfg *models.TransformConfig, evt models.Event) ([]byte, error) { 27 | bs, err := json.Marshal(evt) 28 | if err != nil { 29 | return nil, err 30 | } 31 | // Return early if there are no encode/encrypt actions to take 32 | if cfg == nil { 33 | return bs, nil 34 | } 35 | 36 | // Encrypt 37 | cc, err := newCryptor(cfg.Encryption) 38 | if err != nil { 39 | return nil, err 40 | } 41 | bs, err = cc.Encrypt(bs) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | // Encode 47 | ec, err := newCoder(cfg.Encoding) 48 | if err != nil { 49 | return nil, err 50 | } 51 | bs, err = ec.Encode(bs) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return bs, nil 57 | } 58 | 59 | func Reveal(cfg *models.TransformConfig, data []byte) ([]byte, error) { 60 | if cfg == nil { 61 | return data, nil 62 | } 63 | 64 | // Decode 65 | ec, err := newCoder(cfg.Encoding) 66 | if err != nil { 67 | return nil, err 68 | } 69 | bs, err := ec.Decode(data) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | // Decrypt 75 | cc, err := newCryptor(cfg.Encryption) 76 | if err != nil { 77 | return nil, err 78 | } 79 | bs, err = cc.Decrypt(bs) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | return bs, nil 85 | } 86 | -------------------------------------------------------------------------------- /pkg/compliance/compliance_test.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package compliance 19 | 20 | import ( 21 | "strings" 22 | "testing" 23 | "time" 24 | 25 | "github.com/moov-io/achgateway/pkg/models" 26 | "github.com/moov-io/base" 27 | 28 | "github.com/stretchr/testify/require" 29 | ) 30 | 31 | func TestCompliance(t *testing.T) { 32 | cfg := &models.TransformConfig{ 33 | Encryption: &models.EncryptionConfig{ 34 | AES: &models.AESConfig{ 35 | Key: strings.Repeat("1", 16), 36 | }, 37 | }, 38 | } 39 | // randomly decide if we're going to base64 encode or not 40 | if time.Now().Unix()/2 == 0 { 41 | cfg.Encoding = &models.EncodingConfig{ 42 | Base64: true, 43 | } 44 | } 45 | 46 | fileID, shardKey := base.ID(), base.ID() 47 | evt := models.Event{ 48 | Event: models.FileUploaded{ 49 | FileID: fileID, 50 | ShardKey: shardKey, 51 | UploadedAt: time.Now(), 52 | }, 53 | } 54 | 55 | encrypted, err := Protect(cfg, evt) 56 | require.NoError(t, err) 57 | require.NotEmpty(t, encrypted) 58 | 59 | decrypted, err := Reveal(cfg, encrypted) 60 | require.NoError(t, err) 61 | require.NotEmpty(t, decrypted) 62 | 63 | var uploaded models.FileUploaded 64 | require.NoError(t, models.ReadEvent(decrypted, &uploaded)) 65 | require.Equal(t, fileID, uploaded.FileID) 66 | require.Equal(t, shardKey, uploaded.ShardKey) 67 | } 68 | -------------------------------------------------------------------------------- /pkg/compliance/crypt.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package compliance 19 | 20 | import ( 21 | "errors" 22 | 23 | "github.com/moov-io/achgateway/pkg/models" 24 | ) 25 | 26 | type cryptor interface { 27 | Encrypt(data []byte) ([]byte, error) 28 | Decrypt(data []byte) ([]byte, error) 29 | } 30 | 31 | func newCryptor(cfg *models.EncryptionConfig) (cryptor, error) { 32 | switch { 33 | case cfg == nil: 34 | return &mockCryptor{}, nil 35 | 36 | case cfg.AES != nil: 37 | return newAESCryptor(cfg.AES) 38 | } 39 | return nil, errors.New("unknown encryption") 40 | } 41 | 42 | type mockCryptor struct{} 43 | 44 | func (c *mockCryptor) Encrypt(data []byte) ([]byte, error) { 45 | return data, nil 46 | } 47 | 48 | func (c *mockCryptor) Decrypt(data []byte) ([]byte, error) { 49 | return data, nil 50 | } 51 | -------------------------------------------------------------------------------- /pkg/compliance/crypt_aes.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package compliance 19 | 20 | import ( 21 | "crypto/aes" 22 | "crypto/cipher" 23 | "crypto/rand" 24 | "errors" 25 | "io" 26 | 27 | "github.com/moov-io/achgateway/pkg/models" 28 | ) 29 | 30 | type aesCryptor struct { 31 | cfg *models.AESConfig 32 | } 33 | 34 | func newAESCryptor(cfg *models.AESConfig) (*aesCryptor, error) { 35 | return &aesCryptor{cfg}, nil 36 | } 37 | 38 | func (c *aesCryptor) Encrypt(data []byte) ([]byte, error) { 39 | cphr, err := aes.NewCipher([]byte(c.cfg.Key)) 40 | if err != nil { 41 | return nil, err 42 | } 43 | gcm, err := cipher.NewGCM(cphr) 44 | if err != nil { 45 | return nil, err 46 | } 47 | nonce := make([]byte, gcm.NonceSize()) 48 | if _, err = io.ReadFull(rand.Reader, nonce); err != nil { 49 | return nil, err 50 | } 51 | out := gcm.Seal(nonce, nonce, data, nil) 52 | return out, nil 53 | } 54 | 55 | func (c *aesCryptor) Decrypt(ciphertext []byte) ([]byte, error) { 56 | cphr, err := aes.NewCipher([]byte(c.cfg.Key)) 57 | if err != nil { 58 | return nil, err 59 | } 60 | gcm, err := cipher.NewGCM(cphr) 61 | if err != nil { 62 | return nil, err 63 | } 64 | nonceSize := gcm.NonceSize() 65 | if len(ciphertext) < nonceSize { 66 | return nil, errors.New("nonce is too small") 67 | } 68 | nonce, encryptedMessage := ciphertext[:nonceSize], ciphertext[nonceSize:] 69 | plaintext, err := gcm.Open(nil, nonce, encryptedMessage, nil) 70 | if err != nil { 71 | return nil, err 72 | } 73 | return plaintext, nil 74 | } 75 | -------------------------------------------------------------------------------- /pkg/compliance/crypt_aes_test.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package compliance 19 | 20 | import ( 21 | "strings" 22 | "testing" 23 | 24 | "github.com/moov-io/achgateway/pkg/models" 25 | 26 | "github.com/stretchr/testify/require" 27 | ) 28 | 29 | func TestCryptor__AES(t *testing.T) { 30 | cc, err := newCryptor(&models.EncryptionConfig{ 31 | AES: &models.AESConfig{ 32 | Key: strings.Repeat("1", 16), 33 | }, 34 | }) 35 | require.NoError(t, err) 36 | 37 | enc, err := cc.Encrypt([]byte("hello, world")) 38 | require.NoError(t, err) 39 | require.NotEmpty(t, enc) 40 | 41 | dec1, err := cc.Decrypt(enc) 42 | require.NoError(t, err) 43 | require.Equal(t, "hello, world", string(dec1)) 44 | 45 | dec2, err := cc.Decrypt(enc) 46 | require.NoError(t, err) 47 | require.Equal(t, "hello, world", string(dec2)) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/compliance/encode_test.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package compliance 19 | 20 | import ( 21 | "strings" 22 | "testing" 23 | "unicode/utf8" 24 | 25 | "github.com/moov-io/achgateway/pkg/models" 26 | 27 | "github.com/stretchr/testify/require" 28 | ) 29 | 30 | func TestCoder(t *testing.T) { 31 | ec, err := newCoder(&models.EncodingConfig{ 32 | Base64: true, 33 | }) 34 | require.NoError(t, err) 35 | 36 | enc, err := ec.Encode([]byte("hello, world")) 37 | require.NoError(t, err) 38 | require.Equal(t, "aGVsbG8sIHdvcmxk", string(enc)) 39 | 40 | dec, err := ec.Decode(enc) 41 | require.NoError(t, err) 42 | require.Equal(t, "hello, world", string(dec)) 43 | } 44 | 45 | func TestGzipCoder(t *testing.T) { 46 | ec, err := newCoder(&models.EncodingConfig{ 47 | Compress: true, 48 | }) 49 | require.NoError(t, err) 50 | 51 | input := strings.Repeat("1234567890", 21) 52 | expectedLength := utf8.RuneCountInString(input) 53 | 54 | encoded, err := ec.Encode([]byte(input)) 55 | require.NoError(t, err) 56 | require.Len(t, encoded, 37) 57 | 58 | decoded, err := ec.Decode(encoded) 59 | require.NoError(t, err) 60 | require.Len(t, decoded, expectedLength) 61 | require.Equal(t, input, string(decoded)) 62 | 63 | // .Decode should skip gzip if the input isn't compressed 64 | decoded, err = ec.Decode([]byte(input)) 65 | require.NoError(t, err) 66 | require.Len(t, string(decoded), expectedLength) 67 | require.Equal(t, input, string(decoded)) 68 | } 69 | -------------------------------------------------------------------------------- /pkg/models/model_transform.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package models 19 | 20 | import ( 21 | "encoding/json" 22 | 23 | "github.com/moov-io/base/mask" 24 | ) 25 | 26 | type TransformConfig struct { 27 | Encoding *EncodingConfig 28 | Encryption *EncryptionConfig 29 | } 30 | 31 | type EncodingConfig struct { 32 | Base64 bool 33 | Compress bool 34 | } 35 | 36 | type EncryptionConfig struct { 37 | AES *AESConfig 38 | } 39 | 40 | type AESConfig struct { 41 | Key string 42 | } 43 | 44 | func (cfg *AESConfig) MarshalJSON() ([]byte, error) { 45 | type Aux struct { 46 | Key string 47 | } 48 | return json.Marshal(Aux{ 49 | Key: mask.Password(cfg.Key), 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /pkg/models/model_transform_test.go: -------------------------------------------------------------------------------- 1 | // Licensed to The Moov Authors under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. The Moov Authors licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package models 19 | 20 | import ( 21 | "encoding/json" 22 | "strings" 23 | "testing" 24 | 25 | "github.com/stretchr/testify/require" 26 | ) 27 | 28 | func TestAESConfigMasking(t *testing.T) { 29 | cfg := &AESConfig{Key: strings.Repeat("1", 32)} 30 | bs, err := json.Marshal(cfg) 31 | require.NoError(t, err) 32 | require.JSONEq(t, string(bs), `{"Key":"1*****1"}`) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/models/testdata/partial-recon.ach: -------------------------------------------------------------------------------- 1 | 5225companyname origid PPDCHECKPAYMT000002080730 1076401250000001 2 | 62705320001912345 0000010500c-1 Bachman Eric DD0076401255655291 3 | 82250000010005320001000000010500000000000000origid 076401250000001 4 | 5200CoinLion 123456789 WEBTRANSFER 000101 1091000010000002 5 | 627091400606123456789 0000012354MjMxNDAwMjAtOGQPaul Jones S 1091000017611242 6 | 82000000010009140060000000012354000000000000 123456789 091000010000002 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "groupName": "all", 6 | "packageRules": [ 7 | { 8 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 9 | "automerge": true 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /testdata/HMBRAD_ACHEXPORT_1001_08_19_2022_09_10: -------------------------------------------------------------------------------- 1 | 100 000000039 0000000392220819442A094101HMBRADLEY NYCB 2 | 5220ABC TEST COMPANY 1116001925CCDHAP 2208192311021406660000001 3 | 6222260710041000000000000001 0000100001113504464 Test User 01 0021406660004489 4 | 6222260710041000000000000002 0000100002562309616 Test User 02 0021406660004856 5 | 6222260710041000000000000003 0000100003233054806 Test User 03 0021406660004949 6 | 6222260710041000000000000004 0000100004233054806 Test User 04 0021406660004950 7 | 6222260710041000000000000005 0000100005233054806 Test User 05 0021406660004951 8 | 6222260710041000000000000006 0000100006233054806 Test User 06 0021406660004952 9 | 6222260710041000000000000007 0000100007233054806 Test User 07 0021406660004953 10 | 6222260710041000000000000008 0000100008233054806 Test User 08 0021406660004954 11 | 6222260710041000000000000009 0000100009233054806 Test User 09 0021406660004955 12 | 6222260710041000000000000010 0000100010233054806 Test User 10 0021406660004956 13 | 6222260710041000000000000011 0000100011233054806 Test User 11 0021406660004957 14 | 6222260710041000000000000012 0000100012233054806 Test User 12 0021406660004958 15 | 6222260710041000000000000013 0000100013233054806 Test User 13 0021406660004959 16 | 822000001302938923000000000000000000013000911116001925 021406660000001 17 | 9000001000002000000130293892300000000000000000001300091 18 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 19 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 20 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 21 | 22 | -------------------------------------------------------------------------------- /testdata/cor-c01.ach: -------------------------------------------------------------------------------- 1 | 101 23138010401210428821908291236A094101Federal Reserve Bank My Bank Name 2 | 5220Your Company, in 121042882 CORVendor Pay 000000 1121042880000001 3 | 621231380104744-5678-99 0000000000location #23 Best Co. #23 S 1121042880000001 4 | 798C01121042880000001 121042881918171614 091012980000088 5 | 82200000020023138010000000000000000000000000121042882 121042880000001 6 | 9000001000001000000020023138010000000000000000000000000 7 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 8 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 9 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 10 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 11 | -------------------------------------------------------------------------------- /testdata/download-test/inbound/cor-c01.ach: -------------------------------------------------------------------------------- 1 | 101 23138010401210428821908291236A094101Federal Reserve Bank My Bank Name 2 | 5220Your Company, in 121042882 CORVendor Pay 000000 1121042880000001 3 | 621231380104744-5678-99 0000000000location #23 Best Co. #23 S 1121042880000001 4 | 798C01121042880000001 121042881918171614 091012980000088 5 | 82200000020023138010000000000000000000000000121042882 121042880000001 6 | 9000001000001000000020023138010000000000000000000000000 7 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 8 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 9 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 10 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 11 | -------------------------------------------------------------------------------- /testdata/download-test/inbound/iat-credit.ach: -------------------------------------------------------------------------------- 1 | 101 121042882 2313801041812180000A094101Bank My Bank Name 2 | 5225 FF3 US123456789 IATTRADEPAYMTCADUSD181219 1231380100000001 3 | 6221210428820007 0000100000123456789 1231380100000001 4 | 710ANN000000000000100000928383-23938 BEK Enterprises 0000001 5 | 711BEK Solutions 15 West Place Street 0000001 6 | 712JacobsTown*PA\ US*19305\ 0000001 7 | 713Wells Fargo 01231380104 US 0000001 8 | 714Citadel Bank 01121042882 CA 0000001 9 | 7159874654932139872121 Front Street 0000001 10 | 716LetterTown*AB\ CA*80014\ 0000001 11 | 717This is an international payment 00010000001 12 | 718Bank of France 01456456456987987 FR 00010000001 13 | 82250000100012104288000000000000000000100000 231380100000001 14 | 9000001000002000000100012104288000000000000000000100000 15 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 16 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 17 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 18 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 19 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 20 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 21 | -------------------------------------------------------------------------------- /testdata/download-test/outbound/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moov-io/achgateway/5afe04b3026a1b0dfebd059810c4cec849e3936e/testdata/download-test/outbound/.keep -------------------------------------------------------------------------------- /testdata/download-test/reconciliation/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moov-io/achgateway/5afe04b3026a1b0dfebd059810c4cec849e3936e/testdata/download-test/reconciliation/.keep -------------------------------------------------------------------------------- /testdata/download-test/reconciliation/ppd-debit.ach: -------------------------------------------------------------------------------- 1 | 5225companyname origid PPDCHECKPAYMT000002080730 1076401250000001 2 | 62705320001912345 0000010500c-1 Bachman Eric DD0076401255655291 3 | 82250000010005320001000000010500000000000000origid 076401250000001 4 | -------------------------------------------------------------------------------- /testdata/download-test/returned/return-WEB.ach: -------------------------------------------------------------------------------- 1 | 101 091400606 6910001341810170306A094101FIRST BANK & TRUST ASF APPLICATION SUPERVI 2 | 5200CoinLion 123456789 WEBTRANSFER 000101 1091000010000001 3 | 626091400606123456789 0000012354MjMxNDAwMjAtOGQPaul Jones S 1091000017611242 4 | 799R01091400600000001 09100001 091000017611242 5 | 82000000020009140060000000012354000000000000 123456789 091000010000001 6 | 5200CoinLion 123456789 WEBTRANSFER 000101 1021000020000002 7 | 621091400606867530999999 0000004565NmRjZTJmMzItMGNBob Marley S 1021000029461242 8 | 799R03091400600000003 02100002 021000029461242 9 | 82000000020009140060000000000000000000004565 123456789 021000020000002 10 | 9000002000001000000040018280120000000012354000000004565 -------------------------------------------------------------------------------- /testdata/ftp-server/inbound/cor-c01.ach: -------------------------------------------------------------------------------- 1 | 101 23138010401210428821908291236A094101Federal Reserve Bank My Bank Name 2 | 5220Your Company, in 121042882 CORVendor Pay 000000 1121042880000001 3 | 621231380104744-5678-99 0000000000location #23 Best Co. #23 S 1121042880000001 4 | 798C01121042880000001 121042881918171614 091012980000088 5 | 82200000020023138010000000000000000000000000121042882 121042880000001 6 | 9000001000001000000020023138010000000000000000000000000 7 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 8 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 9 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 10 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 11 | -------------------------------------------------------------------------------- /testdata/ftp-server/inbound/iat-credit.ach: -------------------------------------------------------------------------------- 1 | 101 121042882 2313801041812180000A094101Bank My Bank Name 2 | 5225 FF3 US123456789 IATTRADEPAYMTCADUSD181219 1231380100000001 3 | 6221210428820007 0000100000123456789 1231380100000001 4 | 710ANN000000000000100000928383-23938 BEK Enterprises 0000001 5 | 711BEK Solutions 15 West Place Street 0000001 6 | 712JacobsTown*PA\ US*19305\ 0000001 7 | 713Wells Fargo 01231380104 US 0000001 8 | 714Citadel Bank 01121042882 CA 0000001 9 | 7159874654932139872121 Front Street 0000001 10 | 716LetterTown*AB\ CA*80014\ 0000001 11 | 717This is an international payment 00010000001 12 | 718Bank of France 01456456456987987 FR 00010000001 13 | 82250000100012104288000000000000000000100000 231380100000001 14 | 9000001000002000000100012104288000000000000000000100000 15 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 16 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 17 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 18 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 19 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 20 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 21 | -------------------------------------------------------------------------------- /testdata/ftp-server/inbound/prenote-ppd-debit.ach: -------------------------------------------------------------------------------- 1 | 101 076401251 0764012510807291511A094101achdestname companyname 2 | 5225companyname origid PPDCHECKPAYMT000002080730 1076401250000001 3 | 63805320001912345 0000000000c-1 Bachman Eric DD0076401255655291 4 | 82250000010005320001000000000000000000000000origid 076401250000001 5 | 9000001000001000000010005320001000000000000000000000000 6 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 7 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 8 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 9 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 10 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 11 | -------------------------------------------------------------------------------- /testdata/ftp-server/outbound/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moov-io/achgateway/5afe04b3026a1b0dfebd059810c4cec849e3936e/testdata/ftp-server/outbound/.keep -------------------------------------------------------------------------------- /testdata/ftp-server/reconciliation/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moov-io/achgateway/5afe04b3026a1b0dfebd059810c4cec849e3936e/testdata/ftp-server/reconciliation/.keep -------------------------------------------------------------------------------- /testdata/ftp-server/reconciliation/ppd-debit.ach: -------------------------------------------------------------------------------- 1 | 5225companyname origid PPDCHECKPAYMT000002080730 1076401250000001 2 | 62705320001912345 0000010500c-1 Bachman Eric DD0076401255655291 3 | 82250000010005320001000000010500000000000000origid 076401250000001 4 | -------------------------------------------------------------------------------- /testdata/ftp-server/returned/return-WEB.ach: -------------------------------------------------------------------------------- 1 | 101 091400606 6910001341810170306A094101FIRST BANK & TRUST ASF APPLICATION SUPERVI 2 | 5200CoinLion 123456789 WEBTRANSFER 000101 1091000010000001 3 | 626091400606123456789 0000012354MjMxNDAwMjAtOGQPaul Jones S 1091000017611242 4 | 799R01091400600000001 09100001 091000017611242 5 | 82000000020009140060000000012354000000000000 123456789 091000010000001 6 | 5200CoinLion 123456789 WEBTRANSFER 000101 1021000020000002 7 | 621091400606867530999999 0000004565NmRjZTJmMzItMGNBob Marley S 1021000029461242 8 | 799R03091400600000003 02100002 021000029461242 9 | 82000000020009140060000000000000000000004565 123456789 021000020000002 10 | 9000002000001000000040018280120000000012354000000004565 -------------------------------------------------------------------------------- /testdata/ftp-server/scratch/existing-file: -------------------------------------------------------------------------------- 1 | Hello, World! -------------------------------------------------------------------------------- /testdata/ppd-debit.ach: -------------------------------------------------------------------------------- 1 | 101 076401251 0764012510807291511A094101achdestname companyname 2 | 5225companyname origid PPDCHECKPAYMT000002080730 1076401250000001 3 | 62705320001912345 0000010500c-1 Bachman Eric DD0076401255655291 4 | 82250000010005320001000000010500000000000000origid 076401250000001 5 | 9000001000001000000010005320001000000010500000000000000 -------------------------------------------------------------------------------- /testdata/ppd-debit2.ach: -------------------------------------------------------------------------------- 1 | 101 076401251 0764012510807291511A094101achdestname companyname 2 | 5225companyname origid PPDCHECKPAYMT000002080730 1076401250000002 3 | 62705320001912345 0000010500c-1 Bachman Eric DD0076401255655292 4 | 82250000010005320001000000010500000000000000origid 076401250000002 5 | 9000001000001000000010005320001000000010500000000000000 6 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 7 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 8 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 9 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 10 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 11 | -------------------------------------------------------------------------------- /testdata/ppd-valid.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "fileId", 3 | "fileHeader": { 4 | "id": "fileId", 5 | "immediateDestination": "231380104", 6 | "immediateOrigin": "121042882", 7 | "fileCreationDate": "2018-10-08T00:00:00Z", 8 | "fileCreationTime": "2018-10-08T01:01:02Z", 9 | "fileIDModifier": "A", 10 | "immediateDestinationName": "Citadel", 11 | "immediateOriginName": "Wells Fargo" 12 | }, 13 | "batches": [ 14 | { 15 | "batchHeader": { 16 | "id": "fileId", 17 | "serviceClassCode": 200, 18 | "companyName": "Wells Fargo", 19 | "companyIdentification": "121042882", 20 | "standardEntryClassCode": "PPD", 21 | "companyEntryDescription": "Trans. Des", 22 | "effectiveEntryDate": "2018-10-09T00:00:00Z", 23 | "ODFIIdentification": "12104288", 24 | "batchNumber": 1 25 | }, 26 | "entryDetails": [ 27 | { 28 | "id": "fileId", 29 | "transactionCode": 22, 30 | "RDFIIdentification": "23138010", 31 | "checkDigit": "4", 32 | "DFIAccountNumber": "81967038518 ", 33 | "amount": 100000, 34 | "identificationNumber": "#83738AB# ", 35 | "individualName": "Steven Tander ", 36 | "discretionaryData": " ", 37 | "addendaRecordIndicator": 0, 38 | "traceNumber": "121042880000001", 39 | "category": "Forward" 40 | }, 41 | { 42 | "id": "fileId", 43 | "transactionCode": 27, 44 | "RDFIIdentification": "12104288", 45 | "checkDigit": "2", 46 | "DFIAccountNumber": "17124411", 47 | "amount": 100000, 48 | "identificationNumber": "#83738AB#", 49 | "individualName": "My ODFI", 50 | "discretionaryData": " ", 51 | "addendaRecordIndicator": 0, 52 | "traceNumber": "121042880000002", 53 | "category": "Forward" 54 | } 55 | ] 56 | } 57 | ], 58 | "IATBatches": null, 59 | "NotificationOfChange": null, 60 | "ReturnEntries": null, 61 | "validateOpts": { 62 | "preserveSpaces": true 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /testdata/return-WEB.ach: -------------------------------------------------------------------------------- 1 | 101 091400606 6910001341810170306A094101FIRST BANK & TRUST ASF APPLICATION SUPERVI 2 | 5200CoinLion 123456789 WEBTRANSFER 000101 1091000010000001 3 | 626091400606123456789 0000012354MjMxNDAwMjAtOGQPaul Jones S 1091000017611242 4 | 799R01091400600000001 09100001 091000017611242 5 | 82000000020009140060000000012354000000000000 123456789 091000010000001 6 | 5200CoinLion 123456789 WEBTRANSFER 000101 1021000020000002 7 | 621091400606867530999999 0000004565NmRjZTJmMzItMGNBob Marley S 1021000029461242 8 | 799R03091400600000003 02100002 021000029461242 9 | 82000000020009140060000000000000000000004565 123456789 021000020000002 10 | 9000002000001000000040018280120000000012354000000004565 -------------------------------------------------------------------------------- /testdata/sftp-server/inbound/cor-c01.ach: -------------------------------------------------------------------------------- 1 | 101 23138010401210428821908291236A094101Federal Reserve Bank My Bank Name 2 | 5220Your Company, in 121042882 CORVendor Pay 000000 1121042880000001 3 | 621231380104744-5678-99 0000000000location #23 Best Co. #23 S 1121042880000001 4 | 798C01121042880000001 121042881918171614 091012980000088 5 | 82200000020023138010000000000000000000000000121042882 121042880000001 6 | 9000001000001000000020023138010000000000000000000000000 7 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 8 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 9 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 10 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 11 | -------------------------------------------------------------------------------- /testdata/sftp-server/inbound/iat-credit.ach: -------------------------------------------------------------------------------- 1 | 101 121042882 2313801041812180000A094101Bank My Bank Name 2 | 5225 FF3 US123456789 IATTRADEPAYMTCADUSD181219 1231380100000001 3 | 6221210428820007 0000100000123456789 1231380100000001 4 | 710ANN000000000000100000928383-23938 BEK Enterprises 0000001 5 | 711BEK Solutions 15 West Place Street 0000001 6 | 712JacobsTown*PA\ US*19305\ 0000001 7 | 713Wells Fargo 01231380104 US 0000001 8 | 714Citadel Bank 01121042882 CA 0000001 9 | 7159874654932139872121 Front Street 0000001 10 | 716LetterTown*AB\ CA*80014\ 0000001 11 | 717This is an international payment 00010000001 12 | 718Bank of France 01456456456987987 FR 00010000001 13 | 82250000100012104288000000000000000000100000 231380100000001 14 | 9000001000002000000100012104288000000000000000000100000 15 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 16 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 17 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 18 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 19 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 20 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 21 | -------------------------------------------------------------------------------- /testdata/sftp-server/outbound/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moov-io/achgateway/5afe04b3026a1b0dfebd059810c4cec849e3936e/testdata/sftp-server/outbound/.keep -------------------------------------------------------------------------------- /testdata/sftp-server/reconciliation/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moov-io/achgateway/5afe04b3026a1b0dfebd059810c4cec849e3936e/testdata/sftp-server/reconciliation/.keep -------------------------------------------------------------------------------- /testdata/sftp-server/reconciliation/ppd-debit.ach: -------------------------------------------------------------------------------- 1 | 5225companyname origid PPDCHECKPAYMT000002080730 1076401250000001 2 | 62705320001912345 0000010500c-1 Bachman Eric DD0076401255655291 3 | 82250000010005320001000000010500000000000000origid 076401250000001 4 | -------------------------------------------------------------------------------- /testdata/sftp-server/returned/return-WEB.ach: -------------------------------------------------------------------------------- 1 | 101 091400606 6910001341810170306A094101FIRST BANK & TRUST ASF APPLICATION SUPERVI 2 | 5200CoinLion 123456789 WEBTRANSFER 000101 1091000010000001 3 | 626091400606123456789 0000012354MjMxNDAwMjAtOGQPaul Jones S 1091000017611242 4 | 799R01091400600000001 09100001 091000017611242 5 | 82000000020009140060000000012354000000000000 123456789 091000010000001 6 | 5200CoinLion 123456789 WEBTRANSFER 000101 1021000020000002 7 | 621091400606867530999999 0000004565NmRjZTJmMzItMGNBob Marley S 1021000029461242 8 | 799R03091400600000003 02100002 021000029461242 9 | 82000000020009140060000000000000000000004565 123456789 021000020000002 10 | 9000002000001000000040018280120000000012354000000004565 -------------------------------------------------------------------------------- /testdata/two-micro-deposits.ach: -------------------------------------------------------------------------------- 1 | 101 121042882 12104288220032415591094101Moov Bank Moov, Inc 2 | 5200Moov - paygate m 001 PPDMoov, Inc 200324200325 1121042880000001 3 | 632121042882322580734 0000000044e681e50d1cc83dcDistracted Austin Mo1121042886829038 4 | 705paygate transaction 00016829038 5 | 632121042882322580734 0000000032e681e50d1cc83dcDistracted Austin Mo1121042886829039 6 | 705paygate transaction 00016829039 7 | 627121042882322580734 0000000076e681e50d1cc83dcDistracted Austin Mo1121042886829040 8 | 705paygate transaction 00016829040 9 | 82000000060036312864000000000076000000000076001 121042880000001 10 | 5200Moov - paygate m 001 PPDMoov, Inc 200324200325 1121042880000002 11 | 632121042882191759324 000000000234e5b6f1cf14232Distracted Austin Mo1121042889211556 12 | 705paygate transaction 00019211556 13 | 632121042882191759324 000000004234e5b6f1cf14232Distracted Austin Mo1121042889211557 14 | 705paygate transaction 00019211557 15 | 627121042882191759324 000000004434e5b6f1cf14232Distracted Austin Mo1121042889211558 16 | 705paygate transaction 00019211558 17 | 82000000060036312864000000000044000000000044001 121042880000002 18 | 9000002000002000000120072625728000000000120000000000120 19 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 20 | 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 21 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Moov Authors 2 | // Use of this source code is governed by an Apache License 3 | // license that can be found in the LICENSE file. 4 | // generated-from:891e377361929cf0fa2e83c6a243ccb2300edd5dbb1d2014a83e01696bfa0312 DO NOT REMOVE, DO UPDATE 5 | 6 | package achgateway 7 | 8 | var Version string 9 | --------------------------------------------------------------------------------