├── .github ├── ISSUE_TEMPLATE.md └── workflows │ ├── codeql.yaml │ ├── fuzz.yml │ ├── go.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── api ├── api.yaml ├── makefile └── openapitools.json ├── cmd └── bai2 │ ├── cmd_test.go │ └── main.go ├── configs └── config.default.yml ├── docs └── specifications │ └── Cash Management Balance Reporting Specifications Version 2.pdf ├── go.mod ├── go.sum ├── makefile ├── package.go ├── pkg ├── client │ ├── .gitignore │ ├── .openapi-generator-ignore │ ├── .openapi-generator │ │ ├── FILES │ │ └── VERSION │ ├── README.md │ ├── api_bai2_files.go │ ├── client.go │ ├── configuration.go │ ├── docs │ │ ├── Account.md │ │ ├── AccountSummary.md │ │ ├── Bai2FilesApi.md │ │ ├── Detail.md │ │ ├── Distribution.md │ │ ├── File.md │ │ ├── FundsType.md │ │ └── Group.md │ ├── git_push.sh │ ├── model_account.go │ ├── model_account_summary.go │ ├── model_detail.go │ ├── model_distribution.go │ ├── model_file.go │ ├── model_funds_type.go │ ├── model_group.go │ ├── response.go │ └── utils.go ├── lib │ ├── account.go │ ├── account_test.go │ ├── detail.go │ ├── detail_test.go │ ├── file.go │ ├── file_test.go │ ├── funds_type.go │ ├── funds_type_test.go │ ├── group.go │ ├── group_test.go │ ├── reader.go │ ├── record_account_identifier.go │ ├── record_account_identifier_test.go │ ├── record_account_trailer.go │ ├── record_account_trailer_test.go │ ├── record_file_header.go │ ├── record_file_header_test.go │ ├── record_file_trailer.go │ ├── record_file_trailer_test.go │ ├── record_group_header.go │ ├── record_group_header_test.go │ ├── record_group_trailer.go │ ├── record_group_trailer_test.go │ ├── record_transaction_detail.go │ └── record_transaction_detail_test.go ├── service │ ├── config_test.go │ ├── environment.go │ ├── environment_test.go │ ├── handlers.go │ ├── handlers_test.go │ ├── model_config.go │ └── server.go └── util │ ├── const.go │ ├── parse_test.go │ ├── parser.go │ ├── validate.go │ └── write.go ├── renovate.json ├── test ├── fuzz │ ├── fuzz_test.go │ └── testdata │ │ └── fuzz │ │ └── FuzzReaderWriter_ValidFiles │ │ ├── 3940e35d8f932097 │ │ ├── 771e938e4458e983 │ │ ├── e262a7798c82c66e │ │ └── f96b9eec61a21275 └── testdata │ ├── errors │ └── sample-parseError.txt │ ├── sample1.txt │ ├── sample2.txt │ ├── sample3.txt │ ├── sample4-continuations-newline-delimited.txt │ └── sample5-issue113.txt └── version.go /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bai2 Version: `` 4 | 5 | **What were you trying to do?** 6 | 7 | 8 | 9 | **What did you expect to see?** 10 | 11 | 12 | 13 | **What did you see?** 14 | 15 | 16 | 17 | **How can we reproduce the problem?** 18 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yaml: -------------------------------------------------------------------------------- 1 | name: CodeQL Analysis 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * 0' 8 | 9 | jobs: 10 | CodeQL-Build: 11 | strategy: 12 | fail-fast: false 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Initialize CodeQL 19 | uses: github/codeql-action/init@v3 20 | with: 21 | languages: go 22 | 23 | - name: Perform CodeQL Analysis 24 | uses: github/codeql-action/analyze@v3 25 | -------------------------------------------------------------------------------- /.github/workflows/fuzz.yml: -------------------------------------------------------------------------------- 1 | name: Go Fuzz Testing 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | fuzz-writer: 12 | name: Fuzz Writer 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest, windows-latest] 17 | timeout-minutes: 30 18 | 19 | steps: 20 | - name: Set up Go 1.x 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: stable 24 | id: go 25 | 26 | - name: Check out code into the Go module directory 27 | uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 0 30 | 31 | - uses: actions/cache@v4 32 | with: 33 | path: | 34 | ~/.cache/go-build 35 | ~/go/pkg/mod 36 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 37 | restore-keys: | 38 | ${{ runner.os }}-go- 39 | 40 | - name: Fuzz Valid Files 41 | run: | 42 | go test ./test/fuzz/... -fuzz FuzzReaderWriter_ValidFiles -fuzztime 10m 43 | 44 | - name: Fuzz Error Files 45 | run: | 46 | go test ./test/fuzz/... -fuzz FuzzReaderWriter_ErrorFiles -fuzztime 10m 47 | 48 | - name: Report Failures 49 | if: ${{ failure() }} 50 | run: | 51 | find ./test/fuzz/testdata/fuzz/ -type f | xargs -n1 tail -n +1 -v 52 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | name: Go Build 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macos-latest, windows-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: Check out code into the Go module directory 24 | uses: actions/checkout@v4 25 | 26 | - name: Install make (Windows) 27 | if: runner.os == 'Windows' 28 | run: choco install -y make mingw 29 | 30 | - name: Check 31 | run: make check 32 | 33 | - name: Upload Code Coverage 34 | if: runner.os == 'Linux' 35 | run: bash <(curl -s https://codecov.io/bash) 36 | 37 | - name: Docker Build 38 | if: runner.os == 'Linux' 39 | run: make docker 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: [ "v*.*.*" ] 6 | 7 | jobs: 8 | testing: 9 | name: Testing 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, macos-latest, windows-latest] 14 | steps: 15 | - name: Set up Go 1.x 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: stable 19 | id: go 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Install 27 | run: make install 28 | 29 | - name: Check 30 | run: make check 31 | 32 | create_release: 33 | name: Create Release 34 | needs: [testing] 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Create Release 38 | id: create_release 39 | uses: actions/create-release@v1 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | with: 43 | tag_name: ${{ github.ref }} 44 | release_name: Release ${{ github.ref }} 45 | prerelease: true 46 | 47 | - name: Output Release URL File 48 | run: echo "${{ steps.create_release.outputs.upload_url }}" > release_url.txt 49 | 50 | - name: Save Release URL File for publish 51 | uses: actions/upload-artifact@v4 52 | with: 53 | name: release_url 54 | path: release_url.txt 55 | 56 | publish: 57 | name: Publish 58 | needs: [testing, create_release] 59 | runs-on: ${{ matrix.os }} 60 | strategy: 61 | matrix: 62 | os: [ubuntu-latest, macos-latest, windows-latest] 63 | steps: 64 | - name: Set up Go 1.x 65 | uses: actions/setup-go@v5 66 | with: 67 | go-version: stable 68 | id: go 69 | 70 | - name: Check out code into the Go module directory 71 | uses: actions/checkout@v4 72 | 73 | - name: Load Release URL File from release job 74 | uses: actions/download-artifact@v4 75 | with: 76 | name: release_url 77 | 78 | - name: Install 79 | run: make install 80 | 81 | - name: Distribute 82 | run: make dist 83 | 84 | - name: Get Release File Name & Upload URL 85 | id: get_release_info 86 | shell: bash 87 | run: | 88 | value=`cat release_url/release_url.txt` 89 | echo ::set-output name=upload_url::$value 90 | env: 91 | TAG_REF_NAME: ${{ github.ref }} 92 | REPOSITORY_NAME: ${{ github.repository }} 93 | 94 | - name: Upload Linux Binary 95 | if: runner.os == 'Linux' 96 | uses: actions/upload-release-asset@v1 97 | env: 98 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 99 | with: 100 | upload_url: ${{ steps.get_release_info.outputs.upload_url }} 101 | asset_path: ./bin/bai2-linux-amd64 102 | asset_name: bai2-linux-amd64 103 | asset_content_type: application/octet-stream 104 | 105 | - name: Upload macOS Binary 106 | if: runner.os == 'macOS' 107 | uses: actions/upload-release-asset@v1 108 | env: 109 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 110 | with: 111 | upload_url: ${{ steps.get_release_info.outputs.upload_url }} 112 | asset_path: ./bin/bai2-darwin-amd64 113 | asset_name: bai2-darwin-amd64 114 | asset_content_type: application/octet-stream 115 | 116 | - name: Upload Windows Binary 117 | if: runner.os == 'Windows' 118 | uses: actions/upload-release-asset@v1 119 | env: 120 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 121 | with: 122 | upload_url: ${{ steps.get_release_info.outputs.upload_url }} 123 | asset_path: ./bin/bai2.exe 124 | asset_name: bai2.exe 125 | asset_content_type: application/octet-stream 126 | 127 | docker: 128 | name: Docker 129 | needs: [testing, create_release] 130 | runs-on: ubuntu-latest 131 | steps: 132 | - name: Set up Go 1.x 133 | uses: actions/setup-go@v5 134 | with: 135 | go-version: stable 136 | id: go 137 | 138 | - name: Check out code into the Go module directory 139 | uses: actions/checkout@v4 140 | 141 | - name: Install 142 | run: make install 143 | 144 | - name: Docker 145 | run: make docker 146 | 147 | - name: Docker Push 148 | run: |+ 149 | echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 150 | make docker-push 151 | env: 152 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 153 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 154 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | .idea 27 | .vscode/ 28 | 29 | gitleaks.tar.gz 30 | /vendor/ 31 | coverage.html 32 | cover.out 33 | coverage.txt 34 | misspell* 35 | staticcheck* 36 | /lint-project.sh 37 | bin/ 38 | tmp/ 39 | api/*.jar 40 | dist/ 41 | .DS_Store 42 | .idea/ 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.4.0 (Released 2024-06-17) 2 | 3 | IMPROVEMENTS 4 | 5 | - feat: allow to ignore version check with `Options` struct 6 | 7 | BUILD 8 | 9 | - fix(deps): update module github.com/moov-io/base to v0.49.4 10 | - fix(deps): update module github.com/spf13/cobra to v1.8.1 11 | 12 | ## v0.3.1 (Released 2024-05-15) 13 | 14 | IMPROVEMENTS 15 | 16 | - feat: remove unused continuation struct 17 | - fix: allow some records to include `/` character (#113) 18 | 19 | BUILD 20 | 21 | - fix(deps): update module github.com/moov-io/base to v0.49.3 22 | 23 | ## v0.3.0 (Released 2024-04-16) 24 | 25 | IMPROVEMENTS 26 | 27 | - feat: Implement aggregate functions to support setting trailer record fields programatically 28 | - feat: return all Details for an Account 29 | - fix: normalize file paths for windows machines to fix failing test 30 | - fix: read BAI2 rune by rune 31 | - fix: separate fuzzing of valid BAI2 files from error files 32 | - fix: validate BAI2 file after parsing 33 | - fuzz: setup runner and scheduled job 34 | - schema: OpenAPI models for Files, Groups, Accounts, and related objects 35 | 36 | BUILD 37 | 38 | - chore(deps): update golang docker tag to v1.22 39 | - fix(deps): update module github.com/gorilla/mux to v1.8.1 40 | - fix(deps): update module github.com/moov-io/base to v0.48.5 41 | - fix(deps): update module github.com/spf13/cobra to v1.8.0 42 | - fix(deps): update module github.com/stretchr/testify to v1.9.0 43 | - fix(deps): update module golang.org/x/oauth2 to v0.18.0 44 | 45 | ## v0.2.0 (Released 2023-02-03) 46 | 47 | IMPROVEMENTS 48 | 49 | - feat: support varialble length records 50 | - feat: Implemented file structure that included File, Group, Account 51 | - feat: Handle contiuation records by returning merged records for callers 52 | - fix: Updated returned types to return better results 53 | 54 | BUILD 55 | 56 | - chore(deps): update golang docker tag to v1.20 57 | - fix(deps): update module golang.org/x/oauth2 to v0.4.0 58 | - fix(deps): update module github.com/moov-io/base to v0.39.0 59 | 60 | ## v0.1.0 (Released 2022-12-21) 61 | 62 | This is the initial releae of moov-io/bai2. Please join our (`#bai2`) [slack channel](https://slack.moov.io/) for updates and discussions. 63 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24 as builder 2 | WORKDIR /src 3 | ARG VERSION 4 | 5 | RUN apt-get update && apt-get upgrade -y && apt-get install -y make gcc g++ ca-certificates 6 | 7 | COPY . . 8 | 9 | RUN VERSION=${VERSION} make build 10 | 11 | FROM debian:stable-slim AS runtime 12 | LABEL maintainer="Moov " 13 | 14 | WORKDIR / 15 | 16 | RUN apt-get update && apt-get upgrade -y && apt-get install -y ca-certificates curl \ 17 | && rm -rf /var/lib/apt/lists/* 18 | 19 | COPY --from=builder /src/bin/bai2 /app/ 20 | COPY /configs/ /configs/ 21 | 22 | ENV HTTP_PORT=8484 23 | ENV HEALTH_PORT=9494 24 | 25 | EXPOSE ${HTTP_PORT}/tcp 26 | EXPOSE ${HEALTH_PORT}/tcp 27 | 28 | HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ 29 | CMD curl -f http://localhost:${HEALTH_PORT}/live || exit 1 30 | 31 | VOLUME [ "/data", "/configs" ] 32 | 33 | ENTRYPOINT ["/app/bai2"] 34 | -------------------------------------------------------------------------------- /api/makefile: -------------------------------------------------------------------------------- 1 | .PHONY: client 2 | client: 3 | ifeq ($(OS),Windows_NT) 4 | @echo "Please generate ../pkg/client/ on macOS or Linux, currently unsupported on windows." 5 | else 6 | # Versions from https://github.com/OpenAPITools/openapi-generator/releases 7 | # npm install -g @openapitools/openapi-generator-cli 8 | @rm -rf ../pkg/client/ 9 | OPENAPI_GENERATOR_VERSION=7.4.0 openapi-generator-cli generate --package-name client -i ./api.yaml -g go -o ../pkg/client/ 10 | rm -rf ../pkg/client/go.mod ../pkg/client/go.sum ../pkg/client/api/ ../pkg/client/.travis.yml 11 | go fmt ../... 12 | endif 13 | -------------------------------------------------------------------------------- /api/openapitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", 3 | "spaces": 2, 4 | "generator-cli": { 5 | "version": "7.4.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /cmd/bai2/cmd_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 main 6 | 7 | import ( 8 | "bytes" 9 | "github.com/spf13/cobra" 10 | "os" 11 | "path/filepath" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | var ( 18 | testFileName = filepath.Join("..", "..", "test", "testdata", "sample1.txt") 19 | parseErrorFileName = filepath.Join("..", "..", "test", "testdata", "errors", "sample-parseError.txt") 20 | ) 21 | 22 | func TestMain(m *testing.M) { 23 | initRootCmd() 24 | os.Exit(m.Run()) 25 | } 26 | 27 | func executeCommandC(root *cobra.Command, args ...string) (c *cobra.Command, output string, err error) { 28 | buf := new(bytes.Buffer) 29 | root.SetOutput(buf) 30 | root.SetArgs(args) 31 | 32 | c, err = root.ExecuteC() 33 | 34 | return c, buf.String(), err 35 | } 36 | 37 | func executeCommand(root *cobra.Command, args ...string) (output string, err error) { 38 | _, output, err = executeCommandC(root, args...) 39 | return output, err 40 | } 41 | 42 | func TestWebTest(t *testing.T) { 43 | _, err := executeCommand(rootCmd, "web", "--test=true") 44 | if err != nil { 45 | t.Errorf(err.Error()) 46 | } 47 | } 48 | 49 | func TestPrint(t *testing.T) { 50 | _, err := executeCommand(rootCmd, "print", "--input", testFileName) 51 | if err != nil { 52 | t.Errorf(err.Error()) 53 | } 54 | } 55 | 56 | func TestParse(t *testing.T) { 57 | _, err := executeCommand(rootCmd, "parse", "--input", testFileName) 58 | if err != nil { 59 | t.Errorf(err.Error()) 60 | } 61 | } 62 | 63 | func TestFormat(t *testing.T) { 64 | _, err := executeCommand(rootCmd, "format", "--input", testFileName) 65 | if err != nil { 66 | t.Errorf(err.Error()) 67 | } 68 | } 69 | 70 | func TestPrint_ParseError(t *testing.T) { 71 | _, err := executeCommand(rootCmd, "print", "--input", parseErrorFileName) 72 | assert.Equal(t, err.Error(), "ERROR parsing file on line 1 (unsupported record type 00)") 73 | } 74 | 75 | func TestParse_ParseError(t *testing.T) { 76 | _, err := executeCommand(rootCmd, "parse", "--input", parseErrorFileName) 77 | assert.Equal(t, err.Error(), "ERROR parsing file on line 1 (unsupported record type 00)") 78 | } 79 | 80 | func TestFormat_ParseError(t *testing.T) { 81 | _, err := executeCommand(rootCmd, "format", "--input", parseErrorFileName) 82 | assert.Equal(t, err.Error(), "ERROR parsing file on line 1 (unsupported record type 00)") 83 | } 84 | -------------------------------------------------------------------------------- /cmd/bai2/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 main 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "log" 13 | "os" 14 | "path/filepath" 15 | 16 | "github.com/spf13/cobra" 17 | 18 | "github.com/moov-io/bai2/pkg/lib" 19 | "github.com/moov-io/bai2/pkg/service" 20 | baseLog "github.com/moov-io/base/log" 21 | ) 22 | 23 | var ( 24 | documentFileName string 25 | ignoreVersion bool 26 | documentBuffer []byte 27 | ) 28 | 29 | var WebCmd = &cobra.Command{ 30 | Use: "web", 31 | Short: "Launches web server", 32 | Long: "Launches web server", 33 | RunE: func(cmd *cobra.Command, args []string) error { 34 | env := &service.Environment{ 35 | Logger: baseLog.NewDefaultLogger(), 36 | } 37 | 38 | env, err := service.NewEnvironment(env) 39 | if err != nil { 40 | env.Logger.Fatal().LogErrorf("Error loading up environment.", err).Err() 41 | os.Exit(1) 42 | } 43 | defer env.Shutdown() 44 | 45 | env.Logger.Info().Log("Starting web service") 46 | test, _ := cmd.Flags().GetBool("test") 47 | if !test { 48 | shutdown := env.RunServers(true) 49 | defer shutdown() 50 | } 51 | return nil 52 | }, 53 | } 54 | 55 | var Parse = &cobra.Command{ 56 | Use: "parse", 57 | Short: "parse bai2 report", 58 | Long: "Parse an incoming bai2 report", 59 | RunE: func(cmd *cobra.Command, args []string) error { 60 | 61 | var err error 62 | 63 | scan := lib.NewBai2Scanner(bytes.NewReader(documentBuffer)) 64 | f := lib.NewBai2With(lib.Options{ 65 | IgnoreVersion: ignoreVersion, 66 | }) 67 | err = f.Read(&scan) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | err = f.Validate() 73 | if err != nil { 74 | return errors.New("Parsing report was successful, but not valid") 75 | } 76 | 77 | log.Println("Parsing report was successful and the report is valid") 78 | 79 | return nil 80 | }, 81 | } 82 | 83 | var Print = &cobra.Command{ 84 | Use: "print", 85 | Short: "Print bai2 report", 86 | Long: "Print an incoming bai2 report after parse", 87 | RunE: func(cmd *cobra.Command, args []string) error { 88 | 89 | var err error 90 | 91 | scan := lib.NewBai2Scanner(bytes.NewReader(documentBuffer)) 92 | f := lib.NewBai2With(lib.Options{ 93 | IgnoreVersion: ignoreVersion, 94 | }) 95 | err = f.Read(&scan) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | err = f.Validate() 101 | if err != nil { 102 | return err 103 | } 104 | 105 | fmt.Println(f.String()) 106 | return nil 107 | }, 108 | } 109 | 110 | var Format = &cobra.Command{ 111 | Use: "format", 112 | Short: "Format bai2 report", 113 | Long: "Format an incoming bai2 report after parse", 114 | RunE: func(cmd *cobra.Command, args []string) error { 115 | 116 | var err error 117 | 118 | scan := lib.NewBai2Scanner(bytes.NewReader(documentBuffer)) 119 | f := lib.NewBai2With(lib.Options{ 120 | IgnoreVersion: ignoreVersion, 121 | }) 122 | err = f.Read(&scan) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | err = f.Validate() 128 | if err != nil { 129 | return err 130 | } 131 | 132 | body, ferr := json.Marshal(f) 133 | if ferr != nil { 134 | return ferr 135 | } 136 | 137 | fmt.Println(string(body)) 138 | return nil 139 | }, 140 | } 141 | 142 | var rootCmd = &cobra.Command{ 143 | Use: "", 144 | Short: "", 145 | Long: "", 146 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 147 | isWeb := false 148 | cmdNames := make([]string, 0) 149 | getName := func(c *cobra.Command) {} 150 | getName = func(c *cobra.Command) { 151 | if c == nil { 152 | return 153 | } 154 | cmdNames = append([]string{c.Name()}, cmdNames...) 155 | if c.Name() == "web" { 156 | isWeb = true 157 | } 158 | getName(c.Parent()) 159 | } 160 | getName(cmd) 161 | 162 | if !isWeb { 163 | if documentFileName == "" { 164 | path, err := os.Getwd() 165 | if err != nil { 166 | log.Fatal(err) 167 | } 168 | documentFileName = filepath.Join(path, "bai2.bin") 169 | } 170 | 171 | _, err := os.Stat(documentFileName) 172 | if os.IsNotExist(err) { 173 | return errors.New("invalid input file") 174 | } 175 | 176 | documentBuffer, err = os.ReadFile(documentFileName) 177 | if err != nil { 178 | return err 179 | } 180 | } 181 | 182 | return nil 183 | }, 184 | } 185 | 186 | func initRootCmd() { 187 | WebCmd.Flags().BoolP("test", "t", false, "test server") 188 | 189 | rootCmd.SilenceUsage = true 190 | rootCmd.PersistentFlags().StringVar(&documentFileName, "input", "", "bai2 report file") 191 | rootCmd.PersistentFlags().BoolVar(&ignoreVersion, "ignoreVersion", false, "set to ignore bai file version in the header") 192 | rootCmd.AddCommand(WebCmd) 193 | rootCmd.AddCommand(Print) 194 | rootCmd.AddCommand(Parse) 195 | rootCmd.AddCommand(Format) 196 | } 197 | 198 | func main() { 199 | initRootCmd() 200 | 201 | rootCmd.Execute() 202 | } 203 | -------------------------------------------------------------------------------- /configs/config.default.yml: -------------------------------------------------------------------------------- 1 | bai2: 2 | Servers: 3 | Public: 4 | Bind: 5 | Address: ":8208" 6 | Admin: 7 | Bind: 8 | Address: ":8209" 9 | -------------------------------------------------------------------------------- /docs/specifications/Cash Management Balance Reporting Specifications Version 2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moov-io/bai2/0606a7990d3b447f5727f104eb1740aad2007e17/docs/specifications/Cash Management Balance Reporting Specifications Version 2.pdf -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/moov-io/bai2 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/go-kit/log v0.2.1 9 | github.com/gorilla/mux v1.8.1 10 | github.com/markbates/pkger v0.17.1 11 | github.com/moov-io/base v0.55.0 12 | github.com/spf13/cobra v1.9.1 13 | github.com/stretchr/testify v1.10.0 14 | ) 15 | 16 | require ( 17 | github.com/beorn7/perks v1.0.1 // indirect 18 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 19 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 20 | github.com/fsnotify/fsnotify v1.8.0 // indirect 21 | github.com/go-logfmt/logfmt v0.6.0 // indirect 22 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 23 | github.com/gobuffalo/here v0.6.7 // indirect 24 | github.com/hashicorp/hcl v1.0.0 // indirect 25 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 26 | github.com/klauspost/compress v1.18.0 // indirect 27 | github.com/magiconair/properties v1.8.9 // indirect 28 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 29 | github.com/mitchellh/mapstructure v1.5.0 // indirect 30 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 31 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 32 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 33 | github.com/prometheus/client_golang v1.22.0 // indirect 34 | github.com/prometheus/client_model v0.6.1 // indirect 35 | github.com/prometheus/common v0.62.0 // indirect 36 | github.com/prometheus/procfs v0.15.1 // indirect 37 | github.com/sagikazarmark/locafero v0.7.0 // indirect 38 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 39 | github.com/sourcegraph/conc v0.3.0 // indirect 40 | github.com/spf13/afero v1.12.0 // indirect 41 | github.com/spf13/cast v1.7.1 // indirect 42 | github.com/spf13/pflag v1.0.6 // indirect 43 | github.com/spf13/viper v1.20.1 // indirect 44 | github.com/subosito/gotenv v1.6.0 // indirect 45 | go.uber.org/multierr v1.11.0 // indirect 46 | golang.org/x/exp v0.0.0-20250215185904-eff6e970281f // indirect 47 | golang.org/x/sys v0.32.0 // indirect 48 | golang.org/x/text v0.24.0 // indirect 49 | google.golang.org/protobuf v1.36.6 // indirect 50 | gopkg.in/ini.v1 v1.67.0 // indirect 51 | gopkg.in/yaml.v3 v3.0.1 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # generated-from:8ef700b33a05ab58ec9e7fd3ad1a0d8a99a742beeefc09d26bc7e4b6dd2ad699 DO NOT REMOVE, DO UPDATE 2 | 3 | PLATFORM=$(shell uname -s | tr '[:upper:]' '[:lower:]') 4 | PWD := $(shell pwd) 5 | 6 | ifndef VERSION 7 | VERSION := $(shell git describe --tags --abbrev=0) 8 | endif 9 | 10 | ifndef VERSION 11 | VERSION := v.0.0.0 12 | endif 13 | 14 | COMMIT_HASH :=$(shell git rev-parse --short HEAD) 15 | DEV_VERSION := dev-${COMMIT_HASH} 16 | 17 | USERID := $(shell id -u $$USER) 18 | GROUPID:= $(shell id -g $$USER) 19 | 20 | all: install update build 21 | 22 | .PHONY: install 23 | install: 24 | go mod tidy 25 | go get github.com/markbates/pkger/cmd/pkger 26 | go mod vendor 27 | 28 | update: 29 | #pkger -include /configs/config.default.yml 30 | go mod vendor 31 | 32 | build: 33 | go build -mod=vendor -ldflags "-X github.com/moov-io/bai2.Version=${VERSION}" -o bin/bai2 github.com/moov-io/bai2/cmd/bai2 34 | 35 | .PHONY: check 36 | check: 37 | ifeq ($(OS),Windows_NT) 38 | @echo "Skipping checks on Windows, currently unsupported." 39 | else 40 | @wget -O lint-project.sh https://raw.githubusercontent.com/moov-io/infra/master/go/lint-project.sh 41 | @chmod +x ./lint-project.sh 42 | COVER_THRESHOLD=45.0 GOLANGCI_LINTERS=gosec ./lint-project.sh 43 | endif 44 | 45 | .PHONY: teardown 46 | teardown: 47 | -docker-compose down --remove-orphans 48 | 49 | docker: update docker-hub 50 | 51 | docker-hub: 52 | docker build --pull --build-arg VERSION=${VERSION} -t moov/bai2:${VERSION} -f Dockerfile . 53 | docker tag moov/bai2:${VERSION} moov/bai2:latest 54 | 55 | docker-push: 56 | docker push moov/bai2:${VERSION} 57 | docker push moov/bai2:latest 58 | 59 | .PHONY: dev-docker 60 | dev-docker: update 61 | docker build --pull --build-arg VERSION=${DEV_VERSION} -t moov/bai2:${DEV_VERSION} -f Dockerfile . 62 | 63 | .PHONY: dev-push 64 | dev-push: 65 | docker push moov/bai2:${DEV_VERSION} 66 | 67 | # Extra utilities not needed for building 68 | 69 | run: update build 70 | ./bin/bai2 71 | 72 | docker-run: 73 | docker run -v ${PWD}/data:/data -v ${PWD}/configs:/configs --env APP_CONFIG="/configs/config.yml" -it --rm moov-io/bai2:${VERSION} 74 | 75 | test: update 76 | go test -cover github.com/moov-io/bai2/... 77 | 78 | .PHONY: clean 79 | clean: 80 | ifeq ($(OS),Windows_NT) 81 | @echo "Skipping cleanup on Windows, currently unsupported." 82 | else 83 | @rm -rf cover.out coverage.txt misspell* staticcheck* 84 | @rm -rf ./bin/ 85 | endif 86 | 87 | # For open source projects 88 | 89 | # From https://github.com/genuinetools/img 90 | .PHONY: AUTHORS 91 | AUTHORS: 92 | @$(file >$@,# This file lists all individuals having contributed content to the repository.) 93 | @$(file >>$@,# For how it is generated, see `make AUTHORS`.) 94 | @echo "$(shell git log --format='\n%aN <%aE>' | LC_ALL=C.UTF-8 sort -uf)" >> $@ 95 | 96 | dist: clean build 97 | ifeq ($(OS),Windows_NT) 98 | CGO_ENABLED=1 GOOS=windows go build -o bin/bai2.exe cmd/bai2/* 99 | else 100 | CGO_ENABLED=1 GOOS=$(PLATFORM) go build -o bin/bai2-$(PLATFORM)-amd64 cmd/bai2/* 101 | endif 102 | -------------------------------------------------------------------------------- /package.go: -------------------------------------------------------------------------------- 1 | // stub to get pkger to work 2 | package bai2 3 | 4 | import ( 5 | "github.com/markbates/pkger" 6 | ) 7 | 8 | // Add in all includes that pkger should embed into the application here 9 | var _ = pkger.Include("/configs/config.default.yml") 10 | -------------------------------------------------------------------------------- /pkg/client/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /pkg/client/.openapi-generator-ignore: -------------------------------------------------------------------------------- 1 | # OpenAPI Generator Ignore 2 | # Generated by openapi-generator https://github.com/openapitools/openapi-generator 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: 9 | #ApiClient.cs 10 | 11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*): 12 | #foo/*/qux 13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux 14 | 15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**): 16 | #foo/**/qux 17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux 18 | 19 | # You can also negate patterns with an exclamation (!). 20 | # For example, you can ignore all files in a docs folder with the file extension .md: 21 | #docs/*.md 22 | # Then explicitly reverse the ignore rule for a single file: 23 | #!docs/README.md 24 | -------------------------------------------------------------------------------- /pkg/client/.openapi-generator/FILES: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .openapi-generator-ignore 3 | .travis.yml 4 | README.md 5 | api/openapi.yaml 6 | api_bai2_files.go 7 | client.go 8 | configuration.go 9 | docs/Account.md 10 | docs/AccountSummary.md 11 | docs/Bai2FilesAPI.md 12 | docs/Detail.md 13 | docs/Distribution.md 14 | docs/File.md 15 | docs/FundsType.md 16 | docs/Group.md 17 | git_push.sh 18 | go.mod 19 | go.sum 20 | model_account.go 21 | model_account_summary.go 22 | model_detail.go 23 | model_distribution.go 24 | model_file.go 25 | model_funds_type.go 26 | model_group.go 27 | response.go 28 | test/api_bai2_files_test.go 29 | utils.go 30 | -------------------------------------------------------------------------------- /pkg/client/.openapi-generator/VERSION: -------------------------------------------------------------------------------- 1 | 7.4.0 2 | -------------------------------------------------------------------------------- /pkg/client/README.md: -------------------------------------------------------------------------------- 1 | # Go API client for client 2 | 3 | Moov Bai2 ([Automated Clearing House](https://en.wikipedia.org/wiki/Automated_Clearing_House)) implements an HTTP API for creating, parsing and validating Bais files. BAI2- a widely accepted and used Bank Statement Format for Bank Reconciliation. 4 | 5 | ## Overview 6 | This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [OpenAPI-spec](https://www.openapis.org/) from a remote server, you can easily generate an API client. 7 | 8 | - API version: v1 9 | - Package version: 1.0.0 10 | - Generator version: 7.4.0 11 | - Build package: org.openapitools.codegen.languages.GoClientCodegen 12 | For more information, please visit [https://github.com/moov-io/bai2](https://github.com/moov-io/bai2) 13 | 14 | ## Installation 15 | 16 | Install the following dependencies: 17 | 18 | ```sh 19 | go get github.com/stretchr/testify/assert 20 | go get golang.org/x/net/context 21 | ``` 22 | 23 | Put the package under your project folder and add the following in import: 24 | 25 | ```go 26 | import client "github.com/GIT_USER_ID/GIT_REPO_ID" 27 | ``` 28 | 29 | To use a proxy, set the environment variable `HTTP_PROXY`: 30 | 31 | ```go 32 | os.Setenv("HTTP_PROXY", "http://proxy_name:proxy_port") 33 | ``` 34 | 35 | ## Configuration of Server URL 36 | 37 | Default configuration comes with `Servers` field that contains server objects as defined in the OpenAPI specification. 38 | 39 | ### Select Server Configuration 40 | 41 | For using other server than the one defined on index 0 set context value `client.ContextServerIndex` of type `int`. 42 | 43 | ```go 44 | ctx := context.WithValue(context.Background(), client.ContextServerIndex, 1) 45 | ``` 46 | 47 | ### Templated Server URL 48 | 49 | Templated server URL is formatted using default variables from configuration or from context value `client.ContextServerVariables` of type `map[string]string`. 50 | 51 | ```go 52 | ctx := context.WithValue(context.Background(), client.ContextServerVariables, map[string]string{ 53 | "basePath": "v2", 54 | }) 55 | ``` 56 | 57 | Note, enum values are always validated and all unused variables are silently ignored. 58 | 59 | ### URLs Configuration per Operation 60 | 61 | Each operation can use different server URL defined using `OperationServers` map in the `Configuration`. 62 | An operation is uniquely identified by `"{classname}Service.{nickname}"` string. 63 | Similar rules for overriding default operation server index and variables applies by using `client.ContextOperationServerIndices` and `client.ContextOperationServerVariables` context maps. 64 | 65 | ```go 66 | ctx := context.WithValue(context.Background(), client.ContextOperationServerIndices, map[string]int{ 67 | "{classname}Service.{nickname}": 2, 68 | }) 69 | ctx = context.WithValue(context.Background(), client.ContextOperationServerVariables, map[string]map[string]string{ 70 | "{classname}Service.{nickname}": { 71 | "port": "8443", 72 | }, 73 | }) 74 | ``` 75 | 76 | ## Documentation for API Endpoints 77 | 78 | All URIs are relative to *http://localhost:8208* 79 | 80 | Class | Method | HTTP request | Description 81 | ------------ | ------------- | ------------- | ------------- 82 | *Bai2FilesAPI* | [**Format**](docs/Bai2FilesAPI.md#format) | **Post** /format | Format bai2 file after parse bin file 83 | *Bai2FilesAPI* | [**Health**](docs/Bai2FilesAPI.md#health) | **Get** /health | health bai2 service 84 | *Bai2FilesAPI* | [**Parse**](docs/Bai2FilesAPI.md#parse) | **Post** /parse | Parse bai2 file after parse bin file 85 | *Bai2FilesAPI* | [**Print**](docs/Bai2FilesAPI.md#print) | **Post** /print | Print bai2 file after parse bin file 86 | 87 | 88 | ## Documentation For Models 89 | 90 | - [Account](docs/Account.md) 91 | - [AccountSummary](docs/AccountSummary.md) 92 | - [Detail](docs/Detail.md) 93 | - [Distribution](docs/Distribution.md) 94 | - [File](docs/File.md) 95 | - [FundsType](docs/FundsType.md) 96 | - [Group](docs/Group.md) 97 | 98 | 99 | ## Documentation For Authorization 100 | 101 | Endpoints do not require authorization. 102 | 103 | 104 | ## Documentation for Utility Methods 105 | 106 | Due to the fact that model structure members are all pointers, this package contains 107 | a number of utility functions to easily obtain pointers to values of basic types. 108 | Each of these functions takes a value of the given basic type and returns a pointer to it: 109 | 110 | * `PtrBool` 111 | * `PtrInt` 112 | * `PtrInt32` 113 | * `PtrInt64` 114 | * `PtrFloat` 115 | * `PtrFloat32` 116 | * `PtrFloat64` 117 | * `PtrString` 118 | * `PtrTime` 119 | 120 | ## Author 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /pkg/client/configuration.go: -------------------------------------------------------------------------------- 1 | /* 2 | BAI2 API 3 | 4 | Moov Bai2 ([Automated Clearing House](https://en.wikipedia.org/wiki/Automated_Clearing_House)) implements an HTTP API for creating, parsing and validating Bais files. BAI2- a widely accepted and used Bank Statement Format for Bank Reconciliation. 5 | 6 | API version: v1 7 | */ 8 | 9 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 10 | 11 | package client 12 | 13 | import ( 14 | "context" 15 | "fmt" 16 | "net/http" 17 | "strings" 18 | ) 19 | 20 | // contextKeys are used to identify the type of value in the context. 21 | // Since these are string, it is possible to get a short description of the 22 | // context key for logging and debugging using key.String(). 23 | 24 | type contextKey string 25 | 26 | func (c contextKey) String() string { 27 | return "auth " + string(c) 28 | } 29 | 30 | var ( 31 | // ContextServerIndex uses a server configuration from the index. 32 | ContextServerIndex = contextKey("serverIndex") 33 | 34 | // ContextOperationServerIndices uses a server configuration from the index mapping. 35 | ContextOperationServerIndices = contextKey("serverOperationIndices") 36 | 37 | // ContextServerVariables overrides a server configuration variables. 38 | ContextServerVariables = contextKey("serverVariables") 39 | 40 | // ContextOperationServerVariables overrides a server configuration variables using operation specific values. 41 | ContextOperationServerVariables = contextKey("serverOperationVariables") 42 | ) 43 | 44 | // BasicAuth provides basic http authentication to a request passed via context using ContextBasicAuth 45 | type BasicAuth struct { 46 | UserName string `json:"userName,omitempty"` 47 | Password string `json:"password,omitempty"` 48 | } 49 | 50 | // APIKey provides API key based authentication to a request passed via context using ContextAPIKey 51 | type APIKey struct { 52 | Key string 53 | Prefix string 54 | } 55 | 56 | // ServerVariable stores the information about a server variable 57 | type ServerVariable struct { 58 | Description string 59 | DefaultValue string 60 | EnumValues []string 61 | } 62 | 63 | // ServerConfiguration stores the information about a server 64 | type ServerConfiguration struct { 65 | URL string 66 | Description string 67 | Variables map[string]ServerVariable 68 | } 69 | 70 | // ServerConfigurations stores multiple ServerConfiguration items 71 | type ServerConfigurations []ServerConfiguration 72 | 73 | // Configuration stores the configuration of the API client 74 | type Configuration struct { 75 | Host string `json:"host,omitempty"` 76 | Scheme string `json:"scheme,omitempty"` 77 | DefaultHeader map[string]string `json:"defaultHeader,omitempty"` 78 | UserAgent string `json:"userAgent,omitempty"` 79 | Debug bool `json:"debug,omitempty"` 80 | Servers ServerConfigurations 81 | OperationServers map[string]ServerConfigurations 82 | HTTPClient *http.Client 83 | } 84 | 85 | // NewConfiguration returns a new Configuration object 86 | func NewConfiguration() *Configuration { 87 | cfg := &Configuration{ 88 | DefaultHeader: make(map[string]string), 89 | UserAgent: "OpenAPI-Generator/1.0.0/go", 90 | Debug: false, 91 | Servers: ServerConfigurations{ 92 | { 93 | URL: "http://localhost:8208", 94 | Description: "Local development", 95 | }, 96 | }, 97 | OperationServers: map[string]ServerConfigurations{}, 98 | } 99 | return cfg 100 | } 101 | 102 | // AddDefaultHeader adds a new HTTP header to the default header in the request 103 | func (c *Configuration) AddDefaultHeader(key string, value string) { 104 | c.DefaultHeader[key] = value 105 | } 106 | 107 | // URL formats template on a index using given variables 108 | func (sc ServerConfigurations) URL(index int, variables map[string]string) (string, error) { 109 | if index < 0 || len(sc) <= index { 110 | return "", fmt.Errorf("index %v out of range %v", index, len(sc)-1) 111 | } 112 | server := sc[index] 113 | url := server.URL 114 | 115 | // go through variables and replace placeholders 116 | for name, variable := range server.Variables { 117 | if value, ok := variables[name]; ok { 118 | found := bool(len(variable.EnumValues) == 0) 119 | for _, enumValue := range variable.EnumValues { 120 | if value == enumValue { 121 | found = true 122 | } 123 | } 124 | if !found { 125 | return "", fmt.Errorf("the variable %s in the server URL has invalid value %v. Must be %v", name, value, variable.EnumValues) 126 | } 127 | url = strings.Replace(url, "{"+name+"}", value, -1) 128 | } else { 129 | url = strings.Replace(url, "{"+name+"}", variable.DefaultValue, -1) 130 | } 131 | } 132 | return url, nil 133 | } 134 | 135 | // ServerURL returns URL based on server settings 136 | func (c *Configuration) ServerURL(index int, variables map[string]string) (string, error) { 137 | return c.Servers.URL(index, variables) 138 | } 139 | 140 | func getServerIndex(ctx context.Context) (int, error) { 141 | si := ctx.Value(ContextServerIndex) 142 | if si != nil { 143 | if index, ok := si.(int); ok { 144 | return index, nil 145 | } 146 | return 0, reportError("Invalid type %T should be int", si) 147 | } 148 | return 0, nil 149 | } 150 | 151 | func getServerOperationIndex(ctx context.Context, endpoint string) (int, error) { 152 | osi := ctx.Value(ContextOperationServerIndices) 153 | if osi != nil { 154 | if operationIndices, ok := osi.(map[string]int); !ok { 155 | return 0, reportError("Invalid type %T should be map[string]int", osi) 156 | } else { 157 | index, ok := operationIndices[endpoint] 158 | if ok { 159 | return index, nil 160 | } 161 | } 162 | } 163 | return getServerIndex(ctx) 164 | } 165 | 166 | func getServerVariables(ctx context.Context) (map[string]string, error) { 167 | sv := ctx.Value(ContextServerVariables) 168 | if sv != nil { 169 | if variables, ok := sv.(map[string]string); ok { 170 | return variables, nil 171 | } 172 | return nil, reportError("ctx value of ContextServerVariables has invalid type %T should be map[string]string", sv) 173 | } 174 | return nil, nil 175 | } 176 | 177 | func getServerOperationVariables(ctx context.Context, endpoint string) (map[string]string, error) { 178 | osv := ctx.Value(ContextOperationServerVariables) 179 | if osv != nil { 180 | if operationVariables, ok := osv.(map[string]map[string]string); !ok { 181 | return nil, reportError("ctx value of ContextOperationServerVariables has invalid type %T should be map[string]map[string]string", osv) 182 | } else { 183 | variables, ok := operationVariables[endpoint] 184 | if ok { 185 | return variables, nil 186 | } 187 | } 188 | } 189 | return getServerVariables(ctx) 190 | } 191 | 192 | // ServerURLWithContext returns a new server URL given an endpoint 193 | func (c *Configuration) ServerURLWithContext(ctx context.Context, endpoint string) (string, error) { 194 | sc, ok := c.OperationServers[endpoint] 195 | if !ok { 196 | sc = c.Servers 197 | } 198 | 199 | if ctx == nil { 200 | return sc.URL(0, nil) 201 | } 202 | 203 | index, err := getServerOperationIndex(ctx, endpoint) 204 | if err != nil { 205 | return "", err 206 | } 207 | 208 | variables, err := getServerOperationVariables(ctx, endpoint) 209 | if err != nil { 210 | return "", err 211 | } 212 | 213 | return sc.URL(index, variables) 214 | } 215 | -------------------------------------------------------------------------------- /pkg/client/docs/Account.md: -------------------------------------------------------------------------------- 1 | # Account 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **AccountNumber** | Pointer to **string** | | [optional] 8 | **CurrencyCode** | Pointer to **string** | | [optional] 9 | **Summaries** | Pointer to [**[]AccountSummary**](AccountSummary.md) | | [optional] 10 | **AccountControlTotal** | Pointer to **string** | | [optional] 11 | **NumberOfRecords** | Pointer to **int32** | | [optional] 12 | **Details** | Pointer to [**[]Detail**](Detail.md) | | [optional] 13 | 14 | ## Methods 15 | 16 | ### NewAccount 17 | 18 | `func NewAccount() *Account` 19 | 20 | NewAccount instantiates a new Account object 21 | This constructor will assign default values to properties that have it defined, 22 | and makes sure properties required by API are set, but the set of arguments 23 | will change when the set of required properties is changed 24 | 25 | ### NewAccountWithDefaults 26 | 27 | `func NewAccountWithDefaults() *Account` 28 | 29 | NewAccountWithDefaults instantiates a new Account object 30 | This constructor will only assign default values to properties that have it defined, 31 | but it doesn't guarantee that properties required by API are set 32 | 33 | ### GetAccountNumber 34 | 35 | `func (o *Account) GetAccountNumber() string` 36 | 37 | GetAccountNumber returns the AccountNumber field if non-nil, zero value otherwise. 38 | 39 | ### GetAccountNumberOk 40 | 41 | `func (o *Account) GetAccountNumberOk() (*string, bool)` 42 | 43 | GetAccountNumberOk returns a tuple with the AccountNumber field if it's non-nil, zero value otherwise 44 | and a boolean to check if the value has been set. 45 | 46 | ### SetAccountNumber 47 | 48 | `func (o *Account) SetAccountNumber(v string)` 49 | 50 | SetAccountNumber sets AccountNumber field to given value. 51 | 52 | ### HasAccountNumber 53 | 54 | `func (o *Account) HasAccountNumber() bool` 55 | 56 | HasAccountNumber returns a boolean if a field has been set. 57 | 58 | ### GetCurrencyCode 59 | 60 | `func (o *Account) GetCurrencyCode() string` 61 | 62 | GetCurrencyCode returns the CurrencyCode field if non-nil, zero value otherwise. 63 | 64 | ### GetCurrencyCodeOk 65 | 66 | `func (o *Account) GetCurrencyCodeOk() (*string, bool)` 67 | 68 | GetCurrencyCodeOk returns a tuple with the CurrencyCode field if it's non-nil, zero value otherwise 69 | and a boolean to check if the value has been set. 70 | 71 | ### SetCurrencyCode 72 | 73 | `func (o *Account) SetCurrencyCode(v string)` 74 | 75 | SetCurrencyCode sets CurrencyCode field to given value. 76 | 77 | ### HasCurrencyCode 78 | 79 | `func (o *Account) HasCurrencyCode() bool` 80 | 81 | HasCurrencyCode returns a boolean if a field has been set. 82 | 83 | ### GetSummaries 84 | 85 | `func (o *Account) GetSummaries() []AccountSummary` 86 | 87 | GetSummaries returns the Summaries field if non-nil, zero value otherwise. 88 | 89 | ### GetSummariesOk 90 | 91 | `func (o *Account) GetSummariesOk() (*[]AccountSummary, bool)` 92 | 93 | GetSummariesOk returns a tuple with the Summaries field if it's non-nil, zero value otherwise 94 | and a boolean to check if the value has been set. 95 | 96 | ### SetSummaries 97 | 98 | `func (o *Account) SetSummaries(v []AccountSummary)` 99 | 100 | SetSummaries sets Summaries field to given value. 101 | 102 | ### HasSummaries 103 | 104 | `func (o *Account) HasSummaries() bool` 105 | 106 | HasSummaries returns a boolean if a field has been set. 107 | 108 | ### GetAccountControlTotal 109 | 110 | `func (o *Account) GetAccountControlTotal() string` 111 | 112 | GetAccountControlTotal returns the AccountControlTotal field if non-nil, zero value otherwise. 113 | 114 | ### GetAccountControlTotalOk 115 | 116 | `func (o *Account) GetAccountControlTotalOk() (*string, bool)` 117 | 118 | GetAccountControlTotalOk returns a tuple with the AccountControlTotal field if it's non-nil, zero value otherwise 119 | and a boolean to check if the value has been set. 120 | 121 | ### SetAccountControlTotal 122 | 123 | `func (o *Account) SetAccountControlTotal(v string)` 124 | 125 | SetAccountControlTotal sets AccountControlTotal field to given value. 126 | 127 | ### HasAccountControlTotal 128 | 129 | `func (o *Account) HasAccountControlTotal() bool` 130 | 131 | HasAccountControlTotal returns a boolean if a field has been set. 132 | 133 | ### GetNumberOfRecords 134 | 135 | `func (o *Account) GetNumberOfRecords() int32` 136 | 137 | GetNumberOfRecords returns the NumberOfRecords field if non-nil, zero value otherwise. 138 | 139 | ### GetNumberOfRecordsOk 140 | 141 | `func (o *Account) GetNumberOfRecordsOk() (*int32, bool)` 142 | 143 | GetNumberOfRecordsOk returns a tuple with the NumberOfRecords field if it's non-nil, zero value otherwise 144 | and a boolean to check if the value has been set. 145 | 146 | ### SetNumberOfRecords 147 | 148 | `func (o *Account) SetNumberOfRecords(v int32)` 149 | 150 | SetNumberOfRecords sets NumberOfRecords field to given value. 151 | 152 | ### HasNumberOfRecords 153 | 154 | `func (o *Account) HasNumberOfRecords() bool` 155 | 156 | HasNumberOfRecords returns a boolean if a field has been set. 157 | 158 | ### GetDetails 159 | 160 | `func (o *Account) GetDetails() []Detail` 161 | 162 | GetDetails returns the Details field if non-nil, zero value otherwise. 163 | 164 | ### GetDetailsOk 165 | 166 | `func (o *Account) GetDetailsOk() (*[]Detail, bool)` 167 | 168 | GetDetailsOk returns a tuple with the Details field if it's non-nil, zero value otherwise 169 | and a boolean to check if the value has been set. 170 | 171 | ### SetDetails 172 | 173 | `func (o *Account) SetDetails(v []Detail)` 174 | 175 | SetDetails sets Details field to given value. 176 | 177 | ### HasDetails 178 | 179 | `func (o *Account) HasDetails() bool` 180 | 181 | HasDetails returns a boolean if a field has been set. 182 | 183 | 184 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 185 | 186 | 187 | -------------------------------------------------------------------------------- /pkg/client/docs/AccountSummary.md: -------------------------------------------------------------------------------- 1 | # AccountSummary 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **TypeCode** | Pointer to **string** | | [optional] 8 | **Amount** | Pointer to **string** | | [optional] 9 | **ItemCount** | Pointer to **int32** | | [optional] 10 | **FundsType** | Pointer to [**FundsType**](FundsType.md) | | [optional] 11 | 12 | ## Methods 13 | 14 | ### NewAccountSummary 15 | 16 | `func NewAccountSummary() *AccountSummary` 17 | 18 | NewAccountSummary instantiates a new AccountSummary object 19 | This constructor will assign default values to properties that have it defined, 20 | and makes sure properties required by API are set, but the set of arguments 21 | will change when the set of required properties is changed 22 | 23 | ### NewAccountSummaryWithDefaults 24 | 25 | `func NewAccountSummaryWithDefaults() *AccountSummary` 26 | 27 | NewAccountSummaryWithDefaults instantiates a new AccountSummary object 28 | This constructor will only assign default values to properties that have it defined, 29 | but it doesn't guarantee that properties required by API are set 30 | 31 | ### GetTypeCode 32 | 33 | `func (o *AccountSummary) GetTypeCode() string` 34 | 35 | GetTypeCode returns the TypeCode field if non-nil, zero value otherwise. 36 | 37 | ### GetTypeCodeOk 38 | 39 | `func (o *AccountSummary) GetTypeCodeOk() (*string, bool)` 40 | 41 | GetTypeCodeOk returns a tuple with the TypeCode field if it's non-nil, zero value otherwise 42 | and a boolean to check if the value has been set. 43 | 44 | ### SetTypeCode 45 | 46 | `func (o *AccountSummary) SetTypeCode(v string)` 47 | 48 | SetTypeCode sets TypeCode field to given value. 49 | 50 | ### HasTypeCode 51 | 52 | `func (o *AccountSummary) HasTypeCode() bool` 53 | 54 | HasTypeCode returns a boolean if a field has been set. 55 | 56 | ### GetAmount 57 | 58 | `func (o *AccountSummary) GetAmount() string` 59 | 60 | GetAmount returns the Amount field if non-nil, zero value otherwise. 61 | 62 | ### GetAmountOk 63 | 64 | `func (o *AccountSummary) GetAmountOk() (*string, bool)` 65 | 66 | GetAmountOk returns a tuple with the Amount field if it's non-nil, zero value otherwise 67 | and a boolean to check if the value has been set. 68 | 69 | ### SetAmount 70 | 71 | `func (o *AccountSummary) SetAmount(v string)` 72 | 73 | SetAmount sets Amount field to given value. 74 | 75 | ### HasAmount 76 | 77 | `func (o *AccountSummary) HasAmount() bool` 78 | 79 | HasAmount returns a boolean if a field has been set. 80 | 81 | ### GetItemCount 82 | 83 | `func (o *AccountSummary) GetItemCount() int32` 84 | 85 | GetItemCount returns the ItemCount field if non-nil, zero value otherwise. 86 | 87 | ### GetItemCountOk 88 | 89 | `func (o *AccountSummary) GetItemCountOk() (*int32, bool)` 90 | 91 | GetItemCountOk returns a tuple with the ItemCount field if it's non-nil, zero value otherwise 92 | and a boolean to check if the value has been set. 93 | 94 | ### SetItemCount 95 | 96 | `func (o *AccountSummary) SetItemCount(v int32)` 97 | 98 | SetItemCount sets ItemCount field to given value. 99 | 100 | ### HasItemCount 101 | 102 | `func (o *AccountSummary) HasItemCount() bool` 103 | 104 | HasItemCount returns a boolean if a field has been set. 105 | 106 | ### GetFundsType 107 | 108 | `func (o *AccountSummary) GetFundsType() FundsType` 109 | 110 | GetFundsType returns the FundsType field if non-nil, zero value otherwise. 111 | 112 | ### GetFundsTypeOk 113 | 114 | `func (o *AccountSummary) GetFundsTypeOk() (*FundsType, bool)` 115 | 116 | GetFundsTypeOk returns a tuple with the FundsType field if it's non-nil, zero value otherwise 117 | and a boolean to check if the value has been set. 118 | 119 | ### SetFundsType 120 | 121 | `func (o *AccountSummary) SetFundsType(v FundsType)` 122 | 123 | SetFundsType sets FundsType field to given value. 124 | 125 | ### HasFundsType 126 | 127 | `func (o *AccountSummary) HasFundsType() bool` 128 | 129 | HasFundsType returns a boolean if a field has been set. 130 | 131 | 132 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 133 | 134 | 135 | -------------------------------------------------------------------------------- /pkg/client/docs/Bai2FilesApi.md: -------------------------------------------------------------------------------- 1 | # \Bai2FilesAPI 2 | 3 | All URIs are relative to *http://localhost:8208* 4 | 5 | Method | HTTP request | Description 6 | ------------- | ------------- | ------------- 7 | [**Format**](Bai2FilesAPI.md#Format) | **Post** /format | Format bai2 file after parse bin file 8 | [**Health**](Bai2FilesAPI.md#Health) | **Get** /health | health bai2 service 9 | [**Parse**](Bai2FilesAPI.md#Parse) | **Post** /parse | Parse bai2 file after parse bin file 10 | [**Print**](Bai2FilesAPI.md#Print) | **Post** /print | Print bai2 file after parse bin file 11 | 12 | 13 | 14 | ## Format 15 | 16 | > File Format(ctx).Input(input).Execute() 17 | 18 | Format bai2 file after parse bin file 19 | 20 | 21 | 22 | ### Example 23 | 24 | ```go 25 | package main 26 | 27 | import ( 28 | "context" 29 | "fmt" 30 | "os" 31 | openapiclient "github.com/GIT_USER_ID/GIT_REPO_ID" 32 | ) 33 | 34 | func main() { 35 | input := os.NewFile(1234, "some_file") // *os.File | bai2 bin file (optional) 36 | 37 | configuration := openapiclient.NewConfiguration() 38 | apiClient := openapiclient.NewAPIClient(configuration) 39 | resp, r, err := apiClient.Bai2FilesAPI.Format(context.Background()).Input(input).Execute() 40 | if err != nil { 41 | fmt.Fprintf(os.Stderr, "Error when calling `Bai2FilesAPI.Format``: %v\n", err) 42 | fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) 43 | } 44 | // response from `Format`: File 45 | fmt.Fprintf(os.Stdout, "Response from `Bai2FilesAPI.Format`: %v\n", resp) 46 | } 47 | ``` 48 | 49 | ### Path Parameters 50 | 51 | 52 | 53 | ### Other Parameters 54 | 55 | Other parameters are passed through a pointer to a apiFormatRequest struct via the builder pattern 56 | 57 | 58 | Name | Type | Description | Notes 59 | ------------- | ------------- | ------------- | ------------- 60 | **input** | ***os.File** | bai2 bin file | 61 | 62 | ### Return type 63 | 64 | [**File**](File.md) 65 | 66 | ### Authorization 67 | 68 | No authorization required 69 | 70 | ### HTTP request headers 71 | 72 | - **Content-Type**: multipart/form-data 73 | - **Accept**: application/json, text/plain 74 | 75 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 76 | [[Back to Model list]](../README.md#documentation-for-models) 77 | [[Back to README]](../README.md) 78 | 79 | 80 | ## Health 81 | 82 | > string Health(ctx).Execute() 83 | 84 | health bai2 service 85 | 86 | 87 | 88 | ### Example 89 | 90 | ```go 91 | package main 92 | 93 | import ( 94 | "context" 95 | "fmt" 96 | "os" 97 | openapiclient "github.com/GIT_USER_ID/GIT_REPO_ID" 98 | ) 99 | 100 | func main() { 101 | 102 | configuration := openapiclient.NewConfiguration() 103 | apiClient := openapiclient.NewAPIClient(configuration) 104 | resp, r, err := apiClient.Bai2FilesAPI.Health(context.Background()).Execute() 105 | if err != nil { 106 | fmt.Fprintf(os.Stderr, "Error when calling `Bai2FilesAPI.Health``: %v\n", err) 107 | fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) 108 | } 109 | // response from `Health`: string 110 | fmt.Fprintf(os.Stdout, "Response from `Bai2FilesAPI.Health`: %v\n", resp) 111 | } 112 | ``` 113 | 114 | ### Path Parameters 115 | 116 | This endpoint does not need any parameter. 117 | 118 | ### Other Parameters 119 | 120 | Other parameters are passed through a pointer to a apiHealthRequest struct via the builder pattern 121 | 122 | 123 | ### Return type 124 | 125 | **string** 126 | 127 | ### Authorization 128 | 129 | No authorization required 130 | 131 | ### HTTP request headers 132 | 133 | - **Content-Type**: Not defined 134 | - **Accept**: text/plain 135 | 136 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 137 | [[Back to Model list]](../README.md#documentation-for-models) 138 | [[Back to README]](../README.md) 139 | 140 | 141 | ## Parse 142 | 143 | > string Parse(ctx).Input(input).Execute() 144 | 145 | Parse bai2 file after parse bin file 146 | 147 | 148 | 149 | ### Example 150 | 151 | ```go 152 | package main 153 | 154 | import ( 155 | "context" 156 | "fmt" 157 | "os" 158 | openapiclient "github.com/GIT_USER_ID/GIT_REPO_ID" 159 | ) 160 | 161 | func main() { 162 | input := os.NewFile(1234, "some_file") // *os.File | bai2 bin file (optional) 163 | 164 | configuration := openapiclient.NewConfiguration() 165 | apiClient := openapiclient.NewAPIClient(configuration) 166 | resp, r, err := apiClient.Bai2FilesAPI.Parse(context.Background()).Input(input).Execute() 167 | if err != nil { 168 | fmt.Fprintf(os.Stderr, "Error when calling `Bai2FilesAPI.Parse``: %v\n", err) 169 | fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) 170 | } 171 | // response from `Parse`: string 172 | fmt.Fprintf(os.Stdout, "Response from `Bai2FilesAPI.Parse`: %v\n", resp) 173 | } 174 | ``` 175 | 176 | ### Path Parameters 177 | 178 | 179 | 180 | ### Other Parameters 181 | 182 | Other parameters are passed through a pointer to a apiParseRequest struct via the builder pattern 183 | 184 | 185 | Name | Type | Description | Notes 186 | ------------- | ------------- | ------------- | ------------- 187 | **input** | ***os.File** | bai2 bin file | 188 | 189 | ### Return type 190 | 191 | **string** 192 | 193 | ### Authorization 194 | 195 | No authorization required 196 | 197 | ### HTTP request headers 198 | 199 | - **Content-Type**: multipart/form-data 200 | - **Accept**: text/plain 201 | 202 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 203 | [[Back to Model list]](../README.md#documentation-for-models) 204 | [[Back to README]](../README.md) 205 | 206 | 207 | ## Print 208 | 209 | > string Print(ctx).Input(input).Execute() 210 | 211 | Print bai2 file after parse bin file 212 | 213 | 214 | 215 | ### Example 216 | 217 | ```go 218 | package main 219 | 220 | import ( 221 | "context" 222 | "fmt" 223 | "os" 224 | openapiclient "github.com/GIT_USER_ID/GIT_REPO_ID" 225 | ) 226 | 227 | func main() { 228 | input := os.NewFile(1234, "some_file") // *os.File | bai2 bin file (optional) 229 | 230 | configuration := openapiclient.NewConfiguration() 231 | apiClient := openapiclient.NewAPIClient(configuration) 232 | resp, r, err := apiClient.Bai2FilesAPI.Print(context.Background()).Input(input).Execute() 233 | if err != nil { 234 | fmt.Fprintf(os.Stderr, "Error when calling `Bai2FilesAPI.Print``: %v\n", err) 235 | fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) 236 | } 237 | // response from `Print`: string 238 | fmt.Fprintf(os.Stdout, "Response from `Bai2FilesAPI.Print`: %v\n", resp) 239 | } 240 | ``` 241 | 242 | ### Path Parameters 243 | 244 | 245 | 246 | ### Other Parameters 247 | 248 | Other parameters are passed through a pointer to a apiPrintRequest struct via the builder pattern 249 | 250 | 251 | Name | Type | Description | Notes 252 | ------------- | ------------- | ------------- | ------------- 253 | **input** | ***os.File** | bai2 bin file | 254 | 255 | ### Return type 256 | 257 | **string** 258 | 259 | ### Authorization 260 | 261 | No authorization required 262 | 263 | ### HTTP request headers 264 | 265 | - **Content-Type**: multipart/form-data 266 | - **Accept**: text/plain 267 | 268 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 269 | [[Back to Model list]](../README.md#documentation-for-models) 270 | [[Back to README]](../README.md) 271 | 272 | -------------------------------------------------------------------------------- /pkg/client/docs/Detail.md: -------------------------------------------------------------------------------- 1 | # Detail 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **TypeCode** | Pointer to **string** | | [optional] 8 | **Amount** | Pointer to **string** | | [optional] 9 | **FundsType** | Pointer to [**FundsType**](FundsType.md) | | [optional] 10 | **BankReferenceNumber** | Pointer to **string** | | [optional] 11 | **CustomerReferenceNumber** | Pointer to **string** | | [optional] 12 | **Text** | Pointer to **string** | | [optional] 13 | 14 | ## Methods 15 | 16 | ### NewDetail 17 | 18 | `func NewDetail() *Detail` 19 | 20 | NewDetail instantiates a new Detail object 21 | This constructor will assign default values to properties that have it defined, 22 | and makes sure properties required by API are set, but the set of arguments 23 | will change when the set of required properties is changed 24 | 25 | ### NewDetailWithDefaults 26 | 27 | `func NewDetailWithDefaults() *Detail` 28 | 29 | NewDetailWithDefaults instantiates a new Detail object 30 | This constructor will only assign default values to properties that have it defined, 31 | but it doesn't guarantee that properties required by API are set 32 | 33 | ### GetTypeCode 34 | 35 | `func (o *Detail) GetTypeCode() string` 36 | 37 | GetTypeCode returns the TypeCode field if non-nil, zero value otherwise. 38 | 39 | ### GetTypeCodeOk 40 | 41 | `func (o *Detail) GetTypeCodeOk() (*string, bool)` 42 | 43 | GetTypeCodeOk returns a tuple with the TypeCode field if it's non-nil, zero value otherwise 44 | and a boolean to check if the value has been set. 45 | 46 | ### SetTypeCode 47 | 48 | `func (o *Detail) SetTypeCode(v string)` 49 | 50 | SetTypeCode sets TypeCode field to given value. 51 | 52 | ### HasTypeCode 53 | 54 | `func (o *Detail) HasTypeCode() bool` 55 | 56 | HasTypeCode returns a boolean if a field has been set. 57 | 58 | ### GetAmount 59 | 60 | `func (o *Detail) GetAmount() string` 61 | 62 | GetAmount returns the Amount field if non-nil, zero value otherwise. 63 | 64 | ### GetAmountOk 65 | 66 | `func (o *Detail) GetAmountOk() (*string, bool)` 67 | 68 | GetAmountOk returns a tuple with the Amount field if it's non-nil, zero value otherwise 69 | and a boolean to check if the value has been set. 70 | 71 | ### SetAmount 72 | 73 | `func (o *Detail) SetAmount(v string)` 74 | 75 | SetAmount sets Amount field to given value. 76 | 77 | ### HasAmount 78 | 79 | `func (o *Detail) HasAmount() bool` 80 | 81 | HasAmount returns a boolean if a field has been set. 82 | 83 | ### GetFundsType 84 | 85 | `func (o *Detail) GetFundsType() FundsType` 86 | 87 | GetFundsType returns the FundsType field if non-nil, zero value otherwise. 88 | 89 | ### GetFundsTypeOk 90 | 91 | `func (o *Detail) GetFundsTypeOk() (*FundsType, bool)` 92 | 93 | GetFundsTypeOk returns a tuple with the FundsType field if it's non-nil, zero value otherwise 94 | and a boolean to check if the value has been set. 95 | 96 | ### SetFundsType 97 | 98 | `func (o *Detail) SetFundsType(v FundsType)` 99 | 100 | SetFundsType sets FundsType field to given value. 101 | 102 | ### HasFundsType 103 | 104 | `func (o *Detail) HasFundsType() bool` 105 | 106 | HasFundsType returns a boolean if a field has been set. 107 | 108 | ### GetBankReferenceNumber 109 | 110 | `func (o *Detail) GetBankReferenceNumber() string` 111 | 112 | GetBankReferenceNumber returns the BankReferenceNumber field if non-nil, zero value otherwise. 113 | 114 | ### GetBankReferenceNumberOk 115 | 116 | `func (o *Detail) GetBankReferenceNumberOk() (*string, bool)` 117 | 118 | GetBankReferenceNumberOk returns a tuple with the BankReferenceNumber field if it's non-nil, zero value otherwise 119 | and a boolean to check if the value has been set. 120 | 121 | ### SetBankReferenceNumber 122 | 123 | `func (o *Detail) SetBankReferenceNumber(v string)` 124 | 125 | SetBankReferenceNumber sets BankReferenceNumber field to given value. 126 | 127 | ### HasBankReferenceNumber 128 | 129 | `func (o *Detail) HasBankReferenceNumber() bool` 130 | 131 | HasBankReferenceNumber returns a boolean if a field has been set. 132 | 133 | ### GetCustomerReferenceNumber 134 | 135 | `func (o *Detail) GetCustomerReferenceNumber() string` 136 | 137 | GetCustomerReferenceNumber returns the CustomerReferenceNumber field if non-nil, zero value otherwise. 138 | 139 | ### GetCustomerReferenceNumberOk 140 | 141 | `func (o *Detail) GetCustomerReferenceNumberOk() (*string, bool)` 142 | 143 | GetCustomerReferenceNumberOk returns a tuple with the CustomerReferenceNumber field if it's non-nil, zero value otherwise 144 | and a boolean to check if the value has been set. 145 | 146 | ### SetCustomerReferenceNumber 147 | 148 | `func (o *Detail) SetCustomerReferenceNumber(v string)` 149 | 150 | SetCustomerReferenceNumber sets CustomerReferenceNumber field to given value. 151 | 152 | ### HasCustomerReferenceNumber 153 | 154 | `func (o *Detail) HasCustomerReferenceNumber() bool` 155 | 156 | HasCustomerReferenceNumber returns a boolean if a field has been set. 157 | 158 | ### GetText 159 | 160 | `func (o *Detail) GetText() string` 161 | 162 | GetText returns the Text field if non-nil, zero value otherwise. 163 | 164 | ### GetTextOk 165 | 166 | `func (o *Detail) GetTextOk() (*string, bool)` 167 | 168 | GetTextOk returns a tuple with the Text field if it's non-nil, zero value otherwise 169 | and a boolean to check if the value has been set. 170 | 171 | ### SetText 172 | 173 | `func (o *Detail) SetText(v string)` 174 | 175 | SetText sets Text field to given value. 176 | 177 | ### HasText 178 | 179 | `func (o *Detail) HasText() bool` 180 | 181 | HasText returns a boolean if a field has been set. 182 | 183 | 184 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 185 | 186 | 187 | -------------------------------------------------------------------------------- /pkg/client/docs/Distribution.md: -------------------------------------------------------------------------------- 1 | # Distribution 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **Day** | Pointer to **int32** | | [optional] 8 | **Amount** | Pointer to **int32** | | [optional] 9 | 10 | ## Methods 11 | 12 | ### NewDistribution 13 | 14 | `func NewDistribution() *Distribution` 15 | 16 | NewDistribution instantiates a new Distribution object 17 | This constructor will assign default values to properties that have it defined, 18 | and makes sure properties required by API are set, but the set of arguments 19 | will change when the set of required properties is changed 20 | 21 | ### NewDistributionWithDefaults 22 | 23 | `func NewDistributionWithDefaults() *Distribution` 24 | 25 | NewDistributionWithDefaults instantiates a new Distribution object 26 | This constructor will only assign default values to properties that have it defined, 27 | but it doesn't guarantee that properties required by API are set 28 | 29 | ### GetDay 30 | 31 | `func (o *Distribution) GetDay() int32` 32 | 33 | GetDay returns the Day field if non-nil, zero value otherwise. 34 | 35 | ### GetDayOk 36 | 37 | `func (o *Distribution) GetDayOk() (*int32, bool)` 38 | 39 | GetDayOk returns a tuple with the Day field if it's non-nil, zero value otherwise 40 | and a boolean to check if the value has been set. 41 | 42 | ### SetDay 43 | 44 | `func (o *Distribution) SetDay(v int32)` 45 | 46 | SetDay sets Day field to given value. 47 | 48 | ### HasDay 49 | 50 | `func (o *Distribution) HasDay() bool` 51 | 52 | HasDay returns a boolean if a field has been set. 53 | 54 | ### GetAmount 55 | 56 | `func (o *Distribution) GetAmount() int32` 57 | 58 | GetAmount returns the Amount field if non-nil, zero value otherwise. 59 | 60 | ### GetAmountOk 61 | 62 | `func (o *Distribution) GetAmountOk() (*int32, bool)` 63 | 64 | GetAmountOk returns a tuple with the Amount field if it's non-nil, zero value otherwise 65 | and a boolean to check if the value has been set. 66 | 67 | ### SetAmount 68 | 69 | `func (o *Distribution) SetAmount(v int32)` 70 | 71 | SetAmount sets Amount field to given value. 72 | 73 | ### HasAmount 74 | 75 | `func (o *Distribution) HasAmount() bool` 76 | 77 | HasAmount returns a boolean if a field has been set. 78 | 79 | 80 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 81 | 82 | 83 | -------------------------------------------------------------------------------- /pkg/client/git_push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ 3 | # 4 | # Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" 5 | 6 | git_user_id=$1 7 | git_repo_id=$2 8 | release_note=$3 9 | git_host=$4 10 | 11 | if [ "$git_host" = "" ]; then 12 | git_host="github.com" 13 | echo "[INFO] No command line input provided. Set \$git_host to $git_host" 14 | fi 15 | 16 | if [ "$git_user_id" = "" ]; then 17 | git_user_id="GIT_USER_ID" 18 | echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" 19 | fi 20 | 21 | if [ "$git_repo_id" = "" ]; then 22 | git_repo_id="GIT_REPO_ID" 23 | echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" 24 | fi 25 | 26 | if [ "$release_note" = "" ]; then 27 | release_note="Minor update" 28 | echo "[INFO] No command line input provided. Set \$release_note to $release_note" 29 | fi 30 | 31 | # Initialize the local directory as a Git repository 32 | git init 33 | 34 | # Adds the files in the local repository and stages them for commit. 35 | git add . 36 | 37 | # Commits the tracked changes and prepares them to be pushed to a remote repository. 38 | git commit -m "$release_note" 39 | 40 | # Sets the new remote 41 | git_remote=$(git remote) 42 | if [ "$git_remote" = "" ]; then # git remote not defined 43 | 44 | if [ "$GIT_TOKEN" = "" ]; then 45 | echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." 46 | git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git 47 | else 48 | git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git 49 | fi 50 | 51 | fi 52 | 53 | git pull origin master 54 | 55 | # Pushes (Forces) the changes in the local repository up to the remote repository 56 | echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" 57 | git push origin master 2>&1 | grep -v 'To https' 58 | -------------------------------------------------------------------------------- /pkg/client/model_account_summary.go: -------------------------------------------------------------------------------- 1 | /* 2 | BAI2 API 3 | 4 | Moov Bai2 ([Automated Clearing House](https://en.wikipedia.org/wiki/Automated_Clearing_House)) implements an HTTP API for creating, parsing and validating Bais files. BAI2- a widely accepted and used Bank Statement Format for Bank Reconciliation. 5 | 6 | API version: v1 7 | */ 8 | 9 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 10 | 11 | package client 12 | 13 | import ( 14 | "encoding/json" 15 | ) 16 | 17 | // checks if the AccountSummary type satisfies the MappedNullable interface at compile time 18 | var _ MappedNullable = &AccountSummary{} 19 | 20 | // AccountSummary struct for AccountSummary 21 | type AccountSummary struct { 22 | TypeCode *string `json:"TypeCode,omitempty"` 23 | Amount *string `json:"Amount,omitempty"` 24 | ItemCount *int32 `json:"ItemCount,omitempty"` 25 | FundsType *FundsType `json:"FundsType,omitempty"` 26 | } 27 | 28 | // NewAccountSummary instantiates a new AccountSummary object 29 | // This constructor will assign default values to properties that have it defined, 30 | // and makes sure properties required by API are set, but the set of arguments 31 | // will change when the set of required properties is changed 32 | func NewAccountSummary() *AccountSummary { 33 | this := AccountSummary{} 34 | return &this 35 | } 36 | 37 | // NewAccountSummaryWithDefaults instantiates a new AccountSummary object 38 | // This constructor will only assign default values to properties that have it defined, 39 | // but it doesn't guarantee that properties required by API are set 40 | func NewAccountSummaryWithDefaults() *AccountSummary { 41 | this := AccountSummary{} 42 | return &this 43 | } 44 | 45 | // GetTypeCode returns the TypeCode field value if set, zero value otherwise. 46 | func (o *AccountSummary) GetTypeCode() string { 47 | if o == nil || IsNil(o.TypeCode) { 48 | var ret string 49 | return ret 50 | } 51 | return *o.TypeCode 52 | } 53 | 54 | // GetTypeCodeOk returns a tuple with the TypeCode field value if set, nil otherwise 55 | // and a boolean to check if the value has been set. 56 | func (o *AccountSummary) GetTypeCodeOk() (*string, bool) { 57 | if o == nil || IsNil(o.TypeCode) { 58 | return nil, false 59 | } 60 | return o.TypeCode, true 61 | } 62 | 63 | // HasTypeCode returns a boolean if a field has been set. 64 | func (o *AccountSummary) HasTypeCode() bool { 65 | if o != nil && !IsNil(o.TypeCode) { 66 | return true 67 | } 68 | 69 | return false 70 | } 71 | 72 | // SetTypeCode gets a reference to the given string and assigns it to the TypeCode field. 73 | func (o *AccountSummary) SetTypeCode(v string) { 74 | o.TypeCode = &v 75 | } 76 | 77 | // GetAmount returns the Amount field value if set, zero value otherwise. 78 | func (o *AccountSummary) GetAmount() string { 79 | if o == nil || IsNil(o.Amount) { 80 | var ret string 81 | return ret 82 | } 83 | return *o.Amount 84 | } 85 | 86 | // GetAmountOk returns a tuple with the Amount field value if set, nil otherwise 87 | // and a boolean to check if the value has been set. 88 | func (o *AccountSummary) GetAmountOk() (*string, bool) { 89 | if o == nil || IsNil(o.Amount) { 90 | return nil, false 91 | } 92 | return o.Amount, true 93 | } 94 | 95 | // HasAmount returns a boolean if a field has been set. 96 | func (o *AccountSummary) HasAmount() bool { 97 | if o != nil && !IsNil(o.Amount) { 98 | return true 99 | } 100 | 101 | return false 102 | } 103 | 104 | // SetAmount gets a reference to the given string and assigns it to the Amount field. 105 | func (o *AccountSummary) SetAmount(v string) { 106 | o.Amount = &v 107 | } 108 | 109 | // GetItemCount returns the ItemCount field value if set, zero value otherwise. 110 | func (o *AccountSummary) GetItemCount() int32 { 111 | if o == nil || IsNil(o.ItemCount) { 112 | var ret int32 113 | return ret 114 | } 115 | return *o.ItemCount 116 | } 117 | 118 | // GetItemCountOk returns a tuple with the ItemCount field value if set, nil otherwise 119 | // and a boolean to check if the value has been set. 120 | func (o *AccountSummary) GetItemCountOk() (*int32, bool) { 121 | if o == nil || IsNil(o.ItemCount) { 122 | return nil, false 123 | } 124 | return o.ItemCount, true 125 | } 126 | 127 | // HasItemCount returns a boolean if a field has been set. 128 | func (o *AccountSummary) HasItemCount() bool { 129 | if o != nil && !IsNil(o.ItemCount) { 130 | return true 131 | } 132 | 133 | return false 134 | } 135 | 136 | // SetItemCount gets a reference to the given int32 and assigns it to the ItemCount field. 137 | func (o *AccountSummary) SetItemCount(v int32) { 138 | o.ItemCount = &v 139 | } 140 | 141 | // GetFundsType returns the FundsType field value if set, zero value otherwise. 142 | func (o *AccountSummary) GetFundsType() FundsType { 143 | if o == nil || IsNil(o.FundsType) { 144 | var ret FundsType 145 | return ret 146 | } 147 | return *o.FundsType 148 | } 149 | 150 | // GetFundsTypeOk returns a tuple with the FundsType field value if set, nil otherwise 151 | // and a boolean to check if the value has been set. 152 | func (o *AccountSummary) GetFundsTypeOk() (*FundsType, bool) { 153 | if o == nil || IsNil(o.FundsType) { 154 | return nil, false 155 | } 156 | return o.FundsType, true 157 | } 158 | 159 | // HasFundsType returns a boolean if a field has been set. 160 | func (o *AccountSummary) HasFundsType() bool { 161 | if o != nil && !IsNil(o.FundsType) { 162 | return true 163 | } 164 | 165 | return false 166 | } 167 | 168 | // SetFundsType gets a reference to the given FundsType and assigns it to the FundsType field. 169 | func (o *AccountSummary) SetFundsType(v FundsType) { 170 | o.FundsType = &v 171 | } 172 | 173 | func (o AccountSummary) MarshalJSON() ([]byte, error) { 174 | toSerialize, err := o.ToMap() 175 | if err != nil { 176 | return []byte{}, err 177 | } 178 | return json.Marshal(toSerialize) 179 | } 180 | 181 | func (o AccountSummary) ToMap() (map[string]interface{}, error) { 182 | toSerialize := map[string]interface{}{} 183 | if !IsNil(o.TypeCode) { 184 | toSerialize["TypeCode"] = o.TypeCode 185 | } 186 | if !IsNil(o.Amount) { 187 | toSerialize["Amount"] = o.Amount 188 | } 189 | if !IsNil(o.ItemCount) { 190 | toSerialize["ItemCount"] = o.ItemCount 191 | } 192 | if !IsNil(o.FundsType) { 193 | toSerialize["FundsType"] = o.FundsType 194 | } 195 | return toSerialize, nil 196 | } 197 | 198 | type NullableAccountSummary struct { 199 | value *AccountSummary 200 | isSet bool 201 | } 202 | 203 | func (v NullableAccountSummary) Get() *AccountSummary { 204 | return v.value 205 | } 206 | 207 | func (v *NullableAccountSummary) Set(val *AccountSummary) { 208 | v.value = val 209 | v.isSet = true 210 | } 211 | 212 | func (v NullableAccountSummary) IsSet() bool { 213 | return v.isSet 214 | } 215 | 216 | func (v *NullableAccountSummary) Unset() { 217 | v.value = nil 218 | v.isSet = false 219 | } 220 | 221 | func NewNullableAccountSummary(val *AccountSummary) *NullableAccountSummary { 222 | return &NullableAccountSummary{value: val, isSet: true} 223 | } 224 | 225 | func (v NullableAccountSummary) MarshalJSON() ([]byte, error) { 226 | return json.Marshal(v.value) 227 | } 228 | 229 | func (v *NullableAccountSummary) UnmarshalJSON(src []byte) error { 230 | v.isSet = true 231 | return json.Unmarshal(src, &v.value) 232 | } 233 | -------------------------------------------------------------------------------- /pkg/client/model_distribution.go: -------------------------------------------------------------------------------- 1 | /* 2 | BAI2 API 3 | 4 | Moov Bai2 ([Automated Clearing House](https://en.wikipedia.org/wiki/Automated_Clearing_House)) implements an HTTP API for creating, parsing and validating Bais files. BAI2- a widely accepted and used Bank Statement Format for Bank Reconciliation. 5 | 6 | API version: v1 7 | */ 8 | 9 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 10 | 11 | package client 12 | 13 | import ( 14 | "encoding/json" 15 | ) 16 | 17 | // checks if the Distribution type satisfies the MappedNullable interface at compile time 18 | var _ MappedNullable = &Distribution{} 19 | 20 | // Distribution struct for Distribution 21 | type Distribution struct { 22 | Day *int32 `json:"day,omitempty"` 23 | Amount *int32 `json:"amount,omitempty"` 24 | } 25 | 26 | // NewDistribution instantiates a new Distribution object 27 | // This constructor will assign default values to properties that have it defined, 28 | // and makes sure properties required by API are set, but the set of arguments 29 | // will change when the set of required properties is changed 30 | func NewDistribution() *Distribution { 31 | this := Distribution{} 32 | return &this 33 | } 34 | 35 | // NewDistributionWithDefaults instantiates a new Distribution object 36 | // This constructor will only assign default values to properties that have it defined, 37 | // but it doesn't guarantee that properties required by API are set 38 | func NewDistributionWithDefaults() *Distribution { 39 | this := Distribution{} 40 | return &this 41 | } 42 | 43 | // GetDay returns the Day field value if set, zero value otherwise. 44 | func (o *Distribution) GetDay() int32 { 45 | if o == nil || IsNil(o.Day) { 46 | var ret int32 47 | return ret 48 | } 49 | return *o.Day 50 | } 51 | 52 | // GetDayOk returns a tuple with the Day field value if set, nil otherwise 53 | // and a boolean to check if the value has been set. 54 | func (o *Distribution) GetDayOk() (*int32, bool) { 55 | if o == nil || IsNil(o.Day) { 56 | return nil, false 57 | } 58 | return o.Day, true 59 | } 60 | 61 | // HasDay returns a boolean if a field has been set. 62 | func (o *Distribution) HasDay() bool { 63 | if o != nil && !IsNil(o.Day) { 64 | return true 65 | } 66 | 67 | return false 68 | } 69 | 70 | // SetDay gets a reference to the given int32 and assigns it to the Day field. 71 | func (o *Distribution) SetDay(v int32) { 72 | o.Day = &v 73 | } 74 | 75 | // GetAmount returns the Amount field value if set, zero value otherwise. 76 | func (o *Distribution) GetAmount() int32 { 77 | if o == nil || IsNil(o.Amount) { 78 | var ret int32 79 | return ret 80 | } 81 | return *o.Amount 82 | } 83 | 84 | // GetAmountOk returns a tuple with the Amount field value if set, nil otherwise 85 | // and a boolean to check if the value has been set. 86 | func (o *Distribution) GetAmountOk() (*int32, bool) { 87 | if o == nil || IsNil(o.Amount) { 88 | return nil, false 89 | } 90 | return o.Amount, true 91 | } 92 | 93 | // HasAmount returns a boolean if a field has been set. 94 | func (o *Distribution) HasAmount() bool { 95 | if o != nil && !IsNil(o.Amount) { 96 | return true 97 | } 98 | 99 | return false 100 | } 101 | 102 | // SetAmount gets a reference to the given int32 and assigns it to the Amount field. 103 | func (o *Distribution) SetAmount(v int32) { 104 | o.Amount = &v 105 | } 106 | 107 | func (o Distribution) MarshalJSON() ([]byte, error) { 108 | toSerialize, err := o.ToMap() 109 | if err != nil { 110 | return []byte{}, err 111 | } 112 | return json.Marshal(toSerialize) 113 | } 114 | 115 | func (o Distribution) ToMap() (map[string]interface{}, error) { 116 | toSerialize := map[string]interface{}{} 117 | if !IsNil(o.Day) { 118 | toSerialize["day"] = o.Day 119 | } 120 | if !IsNil(o.Amount) { 121 | toSerialize["amount"] = o.Amount 122 | } 123 | return toSerialize, nil 124 | } 125 | 126 | type NullableDistribution struct { 127 | value *Distribution 128 | isSet bool 129 | } 130 | 131 | func (v NullableDistribution) Get() *Distribution { 132 | return v.value 133 | } 134 | 135 | func (v *NullableDistribution) Set(val *Distribution) { 136 | v.value = val 137 | v.isSet = true 138 | } 139 | 140 | func (v NullableDistribution) IsSet() bool { 141 | return v.isSet 142 | } 143 | 144 | func (v *NullableDistribution) Unset() { 145 | v.value = nil 146 | v.isSet = false 147 | } 148 | 149 | func NewNullableDistribution(val *Distribution) *NullableDistribution { 150 | return &NullableDistribution{value: val, isSet: true} 151 | } 152 | 153 | func (v NullableDistribution) MarshalJSON() ([]byte, error) { 154 | return json.Marshal(v.value) 155 | } 156 | 157 | func (v *NullableDistribution) UnmarshalJSON(src []byte) error { 158 | v.isSet = true 159 | return json.Unmarshal(src, &v.value) 160 | } 161 | -------------------------------------------------------------------------------- /pkg/client/response.go: -------------------------------------------------------------------------------- 1 | /* 2 | BAI2 API 3 | 4 | Moov Bai2 ([Automated Clearing House](https://en.wikipedia.org/wiki/Automated_Clearing_House)) implements an HTTP API for creating, parsing and validating Bais files. BAI2- a widely accepted and used Bank Statement Format for Bank Reconciliation. 5 | 6 | API version: v1 7 | */ 8 | 9 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 10 | 11 | package client 12 | 13 | import ( 14 | "net/http" 15 | ) 16 | 17 | // APIResponse stores the API response returned by the server. 18 | type APIResponse struct { 19 | *http.Response `json:"-"` 20 | Message string `json:"message,omitempty"` 21 | // Operation is the name of the OpenAPI operation. 22 | Operation string `json:"operation,omitempty"` 23 | // RequestURL is the request URL. This value is always available, even if the 24 | // embedded *http.Response is nil. 25 | RequestURL string `json:"url,omitempty"` 26 | // Method is the HTTP method used for the request. This value is always 27 | // available, even if the embedded *http.Response is nil. 28 | Method string `json:"method,omitempty"` 29 | // Payload holds the contents of the response body (which may be nil or empty). 30 | // This is provided here as the raw response.Body() reader will have already 31 | // been drained. 32 | Payload []byte `json:"-"` 33 | } 34 | 35 | // NewAPIResponse returns a new APIResponse object. 36 | func NewAPIResponse(r *http.Response) *APIResponse { 37 | 38 | response := &APIResponse{Response: r} 39 | return response 40 | } 41 | 42 | // NewAPIResponseWithError returns a new APIResponse object with the provided error message. 43 | func NewAPIResponseWithError(errorMessage string) *APIResponse { 44 | 45 | response := &APIResponse{Message: errorMessage} 46 | return response 47 | } 48 | -------------------------------------------------------------------------------- /pkg/lib/account.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "fmt" 11 | "regexp" 12 | "strconv" 13 | 14 | "github.com/moov-io/bai2/pkg/util" 15 | ) 16 | 17 | /* 18 | 19 | FILE STRUCTURE 20 | 21 | To simplify processing, balance reporting transmission files are divided into “envelopes” of data. 22 | These envelopes organize data at the following levels: 23 | • Account 24 | • Group 25 | • File 26 | 27 | Account: 28 | The first level of organization is the account. An account envelope includes balance and transaction data. 29 | Example: Account #1256793 at Last National Bank, previous day information as of midnight. 30 | 31 | */ 32 | 33 | // Creating new account object 34 | func NewAccount() *Account { 35 | return &Account{} 36 | } 37 | 38 | // Account Format 39 | type Account struct { 40 | // Account Identifier 41 | AccountNumber string `json:"accountNumber"` 42 | CurrencyCode string `json:"currencyCode,omitempty"` 43 | Summaries []AccountSummary `json:"summaries,omitempty"` 44 | 45 | // Account Trailer 46 | AccountControlTotal string `json:"accountControlTotal"` 47 | NumberRecords int64 `json:"numberRecords"` 48 | 49 | Details []Detail 50 | 51 | header accountIdentifier 52 | trailer accountTrailer 53 | } 54 | 55 | func (r *Account) copyRecords() { 56 | 57 | r.header = accountIdentifier{ 58 | AccountNumber: r.AccountNumber, 59 | CurrencyCode: r.CurrencyCode, 60 | Summaries: r.Summaries, 61 | } 62 | 63 | r.trailer = accountTrailer{ 64 | AccountControlTotal: r.AccountControlTotal, 65 | NumberRecords: r.NumberRecords, 66 | } 67 | 68 | } 69 | 70 | var accountIdentifierCountExpression = regexp.MustCompile(`(?m:^(?:(?:03)|(?:16)|(?:49)|(?:88)))`) 71 | 72 | // Sums the number of 03,16,88,49 records in the account. Maps to the NumberRecords field 73 | func (a *Account) SumRecords(opts ...int64) int64 { 74 | acctString := a.String(opts...) 75 | result := accountIdentifierCountExpression.FindAllStringSubmatch(acctString, 10000) 76 | return int64(len(result)) 77 | } 78 | 79 | // Sums the Amount fields from all 03 and 16 records. Maps to the AccountControlTotal field 80 | func (a *Account) SumDetailAmounts() (string, error) { 81 | if err := a.Validate(); err != nil { 82 | return "0", err 83 | } 84 | var sum int64 85 | for _, detail := range a.Details { 86 | amt, err := strconv.ParseInt(detail.Amount, 10, 64) 87 | if err != nil { 88 | return "0", err 89 | } 90 | switch string(detail.TypeCode[0]) { 91 | case "1", "2", "3": 92 | sum += amt 93 | 94 | case "4", "5", "6": 95 | sum -= amt 96 | default: 97 | return "0", fmt.Errorf("TypeCode %v is invalid for transaction detail", detail.TypeCode) 98 | } 99 | } 100 | for _, summary := range a.Summaries { 101 | amt, err := strconv.ParseInt(summary.Amount, 10, 64) 102 | if err != nil { 103 | return "0", err 104 | } 105 | sum += amt 106 | } 107 | return fmt.Sprint(sum), nil 108 | } 109 | 110 | func (r *Account) String(opts ...int64) string { 111 | 112 | r.copyRecords() 113 | 114 | var buf bytes.Buffer 115 | buf.WriteString(r.header.string(opts...) + "\n") 116 | for i := range r.Details { 117 | buf.WriteString(r.Details[i].String(opts...) + "\n") 118 | } 119 | buf.WriteString(r.trailer.string()) 120 | 121 | return buf.String() 122 | } 123 | 124 | func (r *Account) Validate() error { 125 | 126 | r.copyRecords() 127 | 128 | if err := r.header.validate(); err != nil { 129 | return err 130 | } 131 | 132 | for i := range r.Details { 133 | if err := r.Details[i].Validate(); err != nil { 134 | return err 135 | } 136 | } 137 | 138 | if err := r.trailer.validate(); err != nil { 139 | return err 140 | } 141 | 142 | return nil 143 | } 144 | 145 | func (r *Account) Read(scan *Bai2Scanner, useCurrentLine bool) error { 146 | if scan == nil { 147 | return errors.New("invalid bai2 scanner") 148 | } 149 | 150 | parseAccountIdentifier := func(raw string) error { 151 | if raw == "" { 152 | return nil 153 | } 154 | 155 | newRecord := accountIdentifier{} 156 | _, err := newRecord.parse(raw) 157 | if err != nil { 158 | return fmt.Errorf("ERROR parsing account identifier on line %d (%v)", scan.GetLineIndex(), err) 159 | } 160 | 161 | r.AccountNumber = newRecord.AccountNumber 162 | r.CurrencyCode = newRecord.CurrencyCode 163 | r.Summaries = newRecord.Summaries 164 | 165 | return nil 166 | } 167 | 168 | var rawData string 169 | find := false 170 | 171 | for line := scan.ScanLine(useCurrentLine); line != ""; line = scan.ScanLine(useCurrentLine) { 172 | // find record code 173 | if len(line) < 3 { 174 | continue 175 | } 176 | 177 | useCurrentLine = false 178 | switch line[:2] { 179 | case util.AccountIdentifierCode: 180 | if find { 181 | break 182 | } 183 | 184 | rawData = line 185 | find = true 186 | 187 | case util.ContinuationCode: 188 | if len(rawData) > 0 { 189 | rawData = rawData[:len(rawData)-1] + "," + line[3:] 190 | } 191 | 192 | case util.AccountTrailerCode: 193 | if err := parseAccountIdentifier(rawData); err != nil { 194 | return err 195 | } 196 | 197 | newRecord := accountTrailer{} 198 | _, err := newRecord.parse(line) 199 | if err != nil { 200 | return fmt.Errorf("ERROR parsing account trailer on line %d (%v)", scan.GetLineIndex(), err) 201 | } 202 | 203 | r.AccountControlTotal = newRecord.AccountControlTotal 204 | r.NumberRecords = newRecord.NumberRecords 205 | 206 | return nil 207 | 208 | case util.TransactionDetailCode: 209 | if err := parseAccountIdentifier(rawData); err != nil { 210 | return err 211 | } else { 212 | rawData = "" 213 | } 214 | 215 | detail := NewDetail() 216 | err := detail.Read(scan, true) 217 | if err != nil { 218 | return err 219 | } 220 | 221 | r.Details = append(r.Details, *detail) 222 | useCurrentLine = true 223 | default: 224 | return fmt.Errorf("ERROR parsing account on line %d (unable to read record type %s)", scan.GetLineIndex(), line[0:2]) 225 | 226 | } 227 | } 228 | 229 | return nil 230 | } 231 | -------------------------------------------------------------------------------- /pkg/lib/account_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "bytes" 9 | "strconv" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | /* 16 | func TestAccountWithSampleData1(t *testing.T) { 17 | 18 | raw := ` 19 | 03,9876543210,,010,-500000,,,100,1000000,,,400,2000000,,,190/ 20 | 88,500000,,,110,1000000,,,072,500000,,,074,500000,,,040/ 21 | 88,-1500000,,/ 22 | 16,115,500000,S,,200000,300000,,,LOCK BOX NO.68751/ 23 | 49,4000000,5/ 24 | 98,+00000000001280000,2,25/ 25 | ` 26 | 27 | scan := NewBai2Scanner(bytes.NewReader([]byte(raw))) 28 | account := Account{} 29 | err := account.Read(&scan, false) 30 | require.NoError(t, err) 31 | require.NoError(t, account.Validate()) 32 | require.Equal(t, 5, scan.GetLineIndex()) 33 | require.Equal(t, "", scan.GetLine()) 34 | } 35 | 36 | func TestAccountWithSampleData2(t *testing.T) { 37 | 38 | raw := ` 39 | 03,10200123456,CAD,040,+000000000000,,,045,+000000000000,,/ 40 | 88,100,000000000111500,00002,V,060317,,400,000000000111500,00004,V,060317,/ 41 | 16,108,000000000011500,V,060317,,,,TFR 1020 0345678 / 42 | 16,108,000000000100000,V,060317,,,,MONTREAL / 43 | 98,+00000000001280000,2,25/ 44 | ` 45 | 46 | scan := NewBai2Scanner(bytes.NewReader([]byte(raw))) 47 | account := Account{} 48 | err := account.Read(&scan, false) 49 | require.NoError(t, err) 50 | require.NoError(t, account.Validate()) 51 | require.Equal(t, 5, scan.GetLineIndex()) 52 | } 53 | */ 54 | 55 | func TestAccountOutputWithContinuationRecord(t *testing.T) { 56 | 57 | raw := ` 58 | 03,9876543210,,010,-500000,,,100,1000000,,,400,2000000,,,190/ 59 | 88,500000,,,110,1000000,,,072,500000,,,074,500000,,,040/ 60 | 88,-1500000,,/ 61 | 16,115,500000,S,,200000,300000,,,LOCK BOX NO.68751/ 62 | 49,4000000,5/ 63 | ` 64 | 65 | scan := NewBai2Scanner(bytes.NewReader([]byte(raw))) 66 | account := Account{} 67 | err := account.Read(&scan, false) 68 | require.NoError(t, err) 69 | require.NoError(t, account.Validate()) 70 | require.Equal(t, 5, scan.GetLineIndex()) 71 | require.Equal(t, "49,4000000,5/", scan.GetLine()) 72 | 73 | result := account.String() 74 | expectedResult := `03,9876543210,,010,-500000,,,100,1000000,,,400,2000000,,,190,500000,,,110,1000000,,,072,500000,,,074,500000,,,040,-1500000,,/ 75 | 16,115,500000,S,0,200000,300000,,,LOCK BOX NO.68751/ 76 | 49,4000000,5/` 77 | require.Equal(t, expectedResult, result) 78 | 79 | result = account.String(50) 80 | expectedResult = `03,9876543210,,010,-500000,,,100,1000000,,,400/ 81 | 88,2000000,,,190,500000,,,110,1000000,,,072/ 82 | 88,500000,,,074,500000,,,040,-1500000,,/ 83 | 16,115,500000,S,0,200000,300000,,/ 84 | 88,LOCK BOX NO.68751/ 85 | 49,4000000,5/` 86 | require.Equal(t, expectedResult, result) 87 | 88 | } 89 | 90 | func TestSumAccountRecords(t *testing.T) { 91 | 92 | raw := ` 93 | 03,9876543210,,010,-500000,,,100,1000000,,,400,2000000,,,190/ 94 | 88,500000,,,110,1000000,,,072,500000,,,074,500000,,,040/ 95 | 88,-1500000,,/ 96 | 16,115,500000,S,,200000,300000,,,LOCK BOX NO.68751/ 97 | 49,4000000,5/ 98 | ` 99 | 100 | scan := NewBai2Scanner(bytes.NewReader([]byte(raw))) 101 | account := Account{} 102 | err := account.Read(&scan, false) 103 | require.NoError(t, err) 104 | require.Equal(t, int64(3), account.SumRecords()) 105 | 106 | scan = NewBai2Scanner(bytes.NewReader([]byte(raw))) 107 | account = Account{} 108 | err = account.Read(&scan, false) 109 | require.NoError(t, err) 110 | require.Equal(t, int64(6), account.SumRecords(50)) 111 | 112 | } 113 | 114 | func TestSumAccountTotal(t *testing.T) { 115 | details := []Detail{} 116 | for i := 101; i <= 399; i++ { 117 | detail := NewDetail() 118 | detail.TypeCode = strconv.Itoa(i) 119 | detail.Amount = "27406" 120 | detail.BankReferenceNumber = "1234567" 121 | detail.Text = "TV Purchase" 122 | details = append(details, *detail) 123 | } 124 | account := Account{} 125 | account.AccountNumber = "9876543210" 126 | account.Summaries = append(account.Summaries, AccountSummary{ 127 | TypeCode: "100", 128 | Amount: "20000", 129 | }) 130 | account.Details = details 131 | sum, err := account.SumDetailAmounts() 132 | require.NoError(t, err) 133 | require.Equal(t, "8214394", sum) 134 | 135 | details = []Detail{} 136 | for i := 401; i <= 699; i++ { 137 | detail := NewDetail() 138 | detail.TypeCode = strconv.Itoa(i) 139 | detail.Amount = "27406" 140 | detail.BankReferenceNumber = "1234567" 141 | detail.Text = "TV Purchase" 142 | details = append(details, *detail) 143 | } 144 | account = Account{} 145 | account.AccountNumber = "9876543210" 146 | account.AccountNumber = "9876543210" 147 | account.Summaries = append(account.Summaries, AccountSummary{ 148 | TypeCode: "400", 149 | Amount: "-20000", 150 | }) 151 | account.Details = details 152 | sum, err = account.SumDetailAmounts() 153 | require.NoError(t, err) 154 | require.Equal(t, "-8214394", sum) 155 | 156 | details = []Detail{} 157 | for i := 101; i <= 699; i++ { 158 | detail := NewDetail() 159 | detail.TypeCode = strconv.Itoa(i) 160 | detail.Amount = "27406" 161 | detail.BankReferenceNumber = "1234567" 162 | detail.Text = "TV Purchase" 163 | details = append(details, *detail) 164 | } 165 | account = Account{} 166 | account.AccountNumber = "9876543210" 167 | account.Summaries = append(account.Summaries, AccountSummary{ 168 | TypeCode: "100", 169 | Amount: "27406", 170 | }) 171 | account.Details = details 172 | sum, err = account.SumDetailAmounts() 173 | require.NoError(t, err) 174 | require.Equal(t, "0", sum) 175 | } 176 | -------------------------------------------------------------------------------- /pkg/lib/detail.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "errors" 9 | 10 | "github.com/moov-io/bai2/pkg/util" 11 | ) 12 | 13 | // Creating new account object 14 | func NewDetail() *Detail { 15 | return &Detail{} 16 | } 17 | 18 | // Detail Format 19 | type Detail transactionDetail 20 | 21 | func (r *Detail) Validate() error { 22 | if r == nil { 23 | return nil 24 | } 25 | return (*transactionDetail)(r).validate() 26 | } 27 | 28 | func (r *Detail) String(opts ...int64) string { 29 | if r == nil { 30 | return "" 31 | } 32 | 33 | return (*transactionDetail)(r).string(opts...) 34 | } 35 | 36 | func (r *Detail) Read(scan *Bai2Scanner, useCurrentLine bool) error { 37 | if scan == nil { 38 | return errors.New("invalid bai2 scanner") 39 | } 40 | 41 | var rawData string 42 | find := false 43 | isBreak := false 44 | 45 | for line := scan.ScanLine(useCurrentLine); line != ""; line = scan.ScanLine(useCurrentLine) { 46 | useCurrentLine = false 47 | 48 | // find record code 49 | if len(line) < 3 { 50 | continue 51 | } 52 | 53 | switch line[:2] { 54 | case util.TransactionDetailCode: 55 | 56 | if find { 57 | isBreak = true 58 | break 59 | } 60 | 61 | rawData = line 62 | find = true 63 | 64 | case util.ContinuationCode: 65 | rawData = rawData[:len(rawData)-1] + "," + line[3:] 66 | 67 | default: 68 | isBreak = true 69 | } 70 | 71 | if isBreak { 72 | break 73 | } 74 | } 75 | 76 | _, err := (*transactionDetail)(r).parse(rawData) 77 | return err 78 | } 79 | -------------------------------------------------------------------------------- /pkg/lib/detail_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "bytes" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestDetail(t *testing.T) { 15 | raw := `16,115,10000000,S,5000000,4000000,1000000/ 16 | 88,AX13612,B096132,AMALGAMATED CORP. LOCKBOX/ 17 | 88,DEPOSIT-MISC. RECEIVABLES/` 18 | 19 | scan := NewBai2Scanner(bytes.NewReader([]byte(raw))) 20 | detail := NewDetail() 21 | 22 | err := detail.Read(&scan, false) 23 | require.NoError(t, err) 24 | 25 | require.Equal(t, "115", detail.TypeCode) 26 | require.Equal(t, "10000000", detail.Amount) 27 | require.Equal(t, "S", string(detail.FundsType.TypeCode)) 28 | require.Equal(t, int64(5000000), detail.FundsType.ImmediateAmount) 29 | require.Equal(t, int64(4000000), detail.FundsType.OneDayAmount) 30 | require.Equal(t, int64(1000000), detail.FundsType.TwoDayAmount) 31 | 32 | expect := `16,115,10000000,S,5000000,4000000,1000000,AX13612,B096132,AMALGAMATED CORP. LOCKBOX,DEPOSIT-MISC. RECEIVABLES/` 33 | require.Equal(t, expect, detail.String()) 34 | 35 | expect = `16,115,10000000,S,5000000,4000000,1000000/ 36 | 88,AX13612,B096132,AMALGAMATED CORP. LOCKBOX/ 37 | 88,DEPOSIT-MISC. RECEIVABLES/` 38 | require.Equal(t, expect, detail.String(50)) 39 | } 40 | 41 | /** 42 | * Outlines the behavior of a Detail record when the Detail and Continuations for the detail are terminated 43 | * by a newline character ("\n") rather than a slash ("/"). 44 | */ 45 | func TestDetail_ContinuationRecordWithNewlineDelimiter(t *testing.T) { 46 | data := `16,266,1912,,GI2118700002010,20210706MMQFMPU8000001,Outgoing Wire Return,- 47 | 88,CREF: 20210706MMQFMPU8000001 48 | 88,EREF: 20210706MMQFMPU8000001 49 | 88,DBIC: GSCRUS33 50 | 88,CRNM: ABC Company 51 | 88,DBNM: SAMPLE INC.` 52 | 53 | scan := NewBai2Scanner(bytes.NewReader([]byte(data))) 54 | detail := NewDetail() 55 | 56 | err := detail.Read(&scan, false) 57 | require.NoError(t, err) 58 | 59 | require.Equal(t, "266", detail.TypeCode) 60 | require.Equal(t, "1912", detail.Amount) 61 | require.Equal(t, "", string(detail.FundsType.TypeCode)) 62 | require.Equal(t, "", detail.FundsType.Date) 63 | require.Equal(t, "", detail.FundsType.Time) 64 | require.Equal(t, "GI2118700002010", detail.BankReferenceNumber) 65 | require.Equal(t, "20210706MMQFMPU8000001", detail.CustomerReferenceNumber) 66 | require.Equal(t, "Outgoing Wire Return,-,CREF: 20210706MMQFMPU8000001,EREF: 20210706MMQFMPU8000001,DBIC: GSCRUS33,CRNM: ABC Company,DBNM: SAMPLE INC.", detail.Text) 67 | 68 | expectResult := `16,266,1912,,GI2118700002010,20210706MMQFMPU8000001,Outgoing Wire Return,-,CREF: 20210706MMQFMPU8000001,EREF: 20210706MMQFMPU8000001,DBIC: GSCRUS33,CRNM: ABC Company,DBNM: SAMPLE INC./` 69 | require.Equal(t, expectResult, detail.String()) 70 | 71 | expectResult = `16,266,1912,,GI2118700002010/ 72 | 88,20210706MMQFMPU8000001,Outgoing Wire Return,-/ 73 | 88,CREF: 20210706MMQFMPU8000001/ 74 | 88,EREF: 20210706MMQFMPU8000001,DBIC: GSCRUS33/ 75 | 88,CRNM: ABC Company,DBNM: SAMPLE INC./` 76 | require.Equal(t, expectResult, detail.String(50)) 77 | } 78 | -------------------------------------------------------------------------------- /pkg/lib/file_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "bytes" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | func TestFileWithSampleData(t *testing.T) { 19 | paths := []string{ 20 | "sample1.txt", 21 | "sample2.txt", 22 | "sample3.txt", 23 | "sample4-continuations-newline-delimited.txt", 24 | "sample5-issue113.txt", 25 | } 26 | 27 | for _, path := range paths { 28 | samplePath := filepath.Join("..", "..", "test", "testdata", path) 29 | fd, err := os.Open(samplePath) 30 | require.NoError(t, err) 31 | 32 | scan := NewBai2Scanner(fd) 33 | f := NewBai2() 34 | err = f.Read(&scan) 35 | 36 | require.NoError(t, err) 37 | require.NoError(t, f.Validate()) 38 | } 39 | } 40 | 41 | func TestFileWithContinuationRecord(t *testing.T) { 42 | 43 | raw := `01,0004,12345,060321,0829,001,80,1,2/ 44 | 02,12345,0004,1,060317,,CAD,/ 45 | 03,10200123456,CAD,040,+000000000000,,,045,+000000000000,,/ 46 | 88,046,+000000000000,,,047,+000000000000,,,048,+000000000000,,,049,+000000000000,,/ 47 | 88,050,+000000000000,,,051,+000000000000,,,052,+000000000000,,,053,+000000000000,,/ 48 | 16,409,000000000002500,V,060316,1300,,,RETURNED CHEQUE / 49 | 16,409,000000000090000,V,060316,1300,,,RTN-UNKNOWN / 50 | 49,+00000000000834000,14/ 51 | 98,+00000000001280000,2,25/ 52 | 99,+00000000001280000,1,27/` 53 | 54 | scan := NewBai2Scanner(strings.NewReader(raw)) 55 | f := NewBai2() 56 | err := f.Read(&scan) 57 | require.NoError(t, err) 58 | require.NoError(t, f.Validate()) 59 | 60 | expected := `01,0004,12345,060321,0829,001,80,1,2/ 61 | 02,12345,0004,1,060317,,CAD,/ 62 | 03,10200123456,CAD,040,+000000000000,,,045,+000000000000,,,046,+000000000000,,/ 63 | 88,047,+000000000000,,,048,+000000000000,,,049,+000000000000,,,050/ 64 | 88,+000000000000,,,051,+000000000000,,,052,+000000000000,,,053,+000000000000,,/ 65 | 16,409,000000000002500,V,060316,1300,,,RETURNED CHEQUE / 66 | 16,409,000000000090000,V,060316,1300,,,RTN-UNKNOWN / 67 | 49,+00000000000834000,14/ 68 | 98,+00000000001280000,2,25/ 69 | 99,+00000000001280000,1,27/` 70 | require.Equal(t, expected, f.String()) 71 | } 72 | 73 | func TestSumFileRecords(t *testing.T) { 74 | file := Bai2{} 75 | file.Groups = []Group{ 76 | {NumberOfRecords: 27}, 77 | } 78 | require.Equal(t, int64(29), file.SumRecords()) 79 | } 80 | 81 | func TestScannedFileTrailerRecordCount(t *testing.T) { 82 | 83 | raw := `01,0004,12345,060321,0829,001,80,1,2/ 84 | 02,12345,0004,1,060317,,CAD,/ 85 | 03,10200123456,CAD,040,+000000000000,,,045,+000000000000,,/ 86 | 88,046,+000000000000,,,047,+000000000000,,,048,+000000000000,,,049,+000000000000,,/ 87 | 88,050,+000000000000,,,051,+000000000000,,,052,+000000000000,,,053,+000000000000,,/ 88 | 16,409,000000000002500,V,060316,1300,,,RETURNED CHEQUE / 89 | 16,409,000000000090000,V,060316,1300,,,RTN-UNKNOWN / 90 | 49,+00000000000834000,6/ 91 | 98,+00000000001280000,2,8/ 92 | 99,+00000000001280000,1,10/` 93 | 94 | scan := NewBai2Scanner(bytes.NewReader([]byte(raw))) 95 | file := NewBai2() 96 | err := file.Read(&scan) 97 | require.NoError(t, err) 98 | require.Equal(t, int64(10), file.SumRecords()) 99 | } 100 | 101 | func TestSumNumberOfGroups(t *testing.T) { 102 | file := Bai2{} 103 | file.Groups = []Group{ 104 | {NumberOfRecords: 27}, 105 | {NumberOfRecords: 27}, 106 | {NumberOfRecords: 27}, 107 | } 108 | require.Equal(t, int64(3), file.SumNumberOfGroups()) 109 | } 110 | 111 | func TestSumGroupControlTotals(t *testing.T) { 112 | group := Group{} 113 | group.Receiver = "121000358" 114 | group.Originator = "121000358" 115 | group.GroupStatus = 1 116 | group.AsOfDate = time.Now().Format("060102") 117 | group.AsOfTime = time.Now().Format("1504") 118 | group.AsOfDateModifier = 2 119 | group.GroupControlTotal = "200" 120 | 121 | file := NewBai2() 122 | file.Sender = "121000358" 123 | file.Receiver = "121000358" 124 | file.FileCreatedDate = time.Now().Format("060102") 125 | file.FileCreatedTime = time.Now().Format("1504") 126 | file.FileIdNumber = "01" 127 | file.PhysicalRecordLength = 80 128 | file.BlockSize = 1 129 | file.VersionNumber = 2 130 | file.Groups = append(file.Groups, group) 131 | file.NumberOfRecords = file.SumRecords() 132 | 133 | total, err := file.SumGroupControlTotals() 134 | require.NoError(t, err) 135 | require.Equal(t, "200", total) 136 | } 137 | 138 | func TestBuildFileAggregates(t *testing.T) { 139 | 140 | recordLength := int64(80) 141 | 142 | details := []Detail{} 143 | for i := 0; i < 10; i++ { 144 | detail := NewDetail() 145 | detail.TypeCode = "409" 146 | detail.Amount = "274006" 147 | detail.BankReferenceNumber = "1234567" 148 | detail.Text = "TV Purchase" 149 | details = append(details, *detail) 150 | } 151 | 152 | account1 := Account{} 153 | account1.AccountNumber = "1234567" 154 | account1.CurrencyCode = "USD" 155 | account1.Details = append(account1.Details, details...) 156 | account1.Summaries = []AccountSummary{ 157 | {TypeCode: "040", Amount: "2000"}, 158 | {TypeCode: "045", Amount: "2000"}, 159 | {TypeCode: "046", Amount: "2000"}, 160 | {TypeCode: "047", Amount: "2000"}, 161 | {TypeCode: "048", Amount: "2000"}, 162 | {TypeCode: "049", Amount: "2000"}, 163 | {TypeCode: "050", Amount: "2000"}, 164 | {TypeCode: "051", Amount: "2000"}, 165 | {TypeCode: "052", Amount: "2000"}, 166 | {TypeCode: "053", Amount: "2000"}, 167 | } 168 | controlTotal, _ := account1.SumDetailAmounts() 169 | account1.AccountControlTotal = controlTotal 170 | account1.NumberRecords = account1.SumRecords(recordLength) 171 | 172 | account2 := Account{} 173 | account2.AccountNumber = "1234567" 174 | account2.CurrencyCode = "USD" 175 | account2.Details = append(account2.Details, details...) 176 | controlTotal, _ = account2.SumDetailAmounts() 177 | account2.AccountControlTotal = controlTotal 178 | account2.NumberRecords = account2.SumRecords(recordLength) 179 | 180 | group := Group{} 181 | group.Receiver = "121000358" 182 | group.Originator = "121000358" 183 | group.GroupStatus = 1 184 | group.AsOfDate = time.Now().Format("060102") 185 | group.AsOfTime = time.Now().Format("1504") 186 | group.AsOfDateModifier = 2 187 | group.Accounts = append(group.Accounts, account1, account2) 188 | controlTotal, _ = group.SumAccountControlTotals() 189 | group.GroupControlTotal = controlTotal 190 | group.NumberOfAccounts = group.SumNumberOfAccounts() 191 | group.NumberOfRecords = group.SumRecords() 192 | 193 | file := NewBai2() 194 | file.Sender = "121000358" 195 | file.Receiver = "121000358" 196 | file.FileCreatedDate = time.Now().Format("060102") 197 | file.FileCreatedTime = time.Now().Format("1504") 198 | file.FileIdNumber = "01" 199 | file.PhysicalRecordLength = recordLength 200 | file.BlockSize = 1 201 | file.VersionNumber = 2 202 | file.Groups = append(file.Groups, group) 203 | controlTotal, _ = file.SumGroupControlTotals() 204 | file.FileControlTotal = controlTotal 205 | file.NumberOfGroups = file.SumNumberOfGroups() 206 | file.NumberOfRecords = file.SumRecords() 207 | 208 | require.Equal(t, "-5460120", file.FileControlTotal) 209 | require.Equal(t, int64(1), file.NumberOfGroups) 210 | require.Equal(t, int64(29), file.NumberOfRecords) 211 | 212 | } 213 | -------------------------------------------------------------------------------- /pkg/lib/funds_type.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "fmt" 11 | "strings" 12 | 13 | "github.com/moov-io/bai2/pkg/util" 14 | ) 15 | 16 | const ( 17 | FundsType0 = "0" 18 | FundsType1 = "1" 19 | FundsType2 = "2" 20 | FundsTypeS = "S" 21 | FundsTypeV = "V" 22 | FundsTypeD = "D" 23 | FundsTypeZ = "Z" 24 | ) 25 | 26 | type FundsType struct { 27 | TypeCode FundsTypeCode `json:"type_code,omitempty"` 28 | 29 | // Type 0,1,2,S 30 | ImmediateAmount int64 `json:"immediate_amount,omitempty"` // availability amount 31 | OneDayAmount int64 `json:"one_day_amount,omitempty"` // one-day availability amount 32 | TwoDayAmount int64 `json:"two_day_amount,omitempty"` // more than one-day availability amount 33 | 34 | // Type V 35 | Date string `json:"date,omitempty"` 36 | Time string `json:"time,omitempty"` 37 | 38 | // Type D 39 | DistributionNumber int64 `json:"distribution_number,omitempty"` 40 | Distributions []Distribution `json:"distributions,omitempty"` 41 | } 42 | 43 | func (f *FundsType) Validate() error { 44 | 45 | if err := f.TypeCode.Validate(); err != nil { 46 | return err 47 | } 48 | 49 | if strings.ToUpper(string(f.TypeCode)) == FundsTypeD && f.DistributionNumber != int64(len(f.Distributions)) { 50 | return errors.New("number of distributions is not match") 51 | } 52 | 53 | if strings.ToUpper(string(f.TypeCode)) == FundsTypeV { 54 | if f.Date != "" && !util.ValidateDate(f.Date) { 55 | return errors.New("invalid date of fund type V (" + f.Date + ")") 56 | } 57 | if f.Time != "" && !util.ValidateTime(f.Time) { 58 | return errors.New("invalid time of fund type V (" + f.Time + ")") 59 | } 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func (f *FundsType) String() string { 66 | 67 | fType := strings.ToUpper(string(f.TypeCode)) 68 | 69 | var buf bytes.Buffer 70 | if f.TypeCode == "" || fType == FundsTypeZ { 71 | buf.WriteString(strings.ToUpper(string(f.TypeCode))) 72 | } else { 73 | 74 | if fType == FundsType0 || fType == FundsType1 || fType == FundsType2 { 75 | buf.WriteString(strings.ToUpper(string(f.TypeCode))) 76 | } else if fType == FundsTypeS { 77 | buf.WriteString(strings.ToUpper(string(f.TypeCode)) + ",") 78 | buf.WriteString(fmt.Sprintf("%d,%d,%d", f.ImmediateAmount, f.OneDayAmount, f.TwoDayAmount)) 79 | } else if fType == FundsTypeV { 80 | buf.WriteString(strings.ToUpper(string(f.TypeCode)) + ",") 81 | buf.WriteString(fmt.Sprintf("%s,%s", f.Date, f.Time)) 82 | } else if fType == FundsTypeD { 83 | if len(f.Distributions) > 0 { 84 | buf.WriteString(fmt.Sprintf("%s,%d,", strings.ToUpper(string(f.TypeCode)), f.DistributionNumber)) 85 | for index, distribution := range f.Distributions { 86 | if index < len(f.Distributions)-1 { 87 | buf.WriteString(fmt.Sprintf("%d,%d,", distribution.Day, distribution.Amount)) 88 | } else { 89 | buf.WriteString(fmt.Sprintf("%d,%d", distribution.Day, distribution.Amount)) 90 | } 91 | } 92 | } else { 93 | buf.WriteString(strings.ToUpper(string(f.TypeCode)) + ",0") 94 | } 95 | } 96 | } 97 | 98 | return buf.String() 99 | } 100 | 101 | func (f *FundsType) parse(data string) (int, error) { 102 | 103 | var err error 104 | var size, read int 105 | 106 | code, size, err := util.ReadField(data, read) 107 | if err != nil { 108 | return 0, errors.New("FundsType: unable to parse type code") 109 | } else { 110 | read += size 111 | } 112 | 113 | f.TypeCode = FundsTypeCode(code) 114 | 115 | if f.TypeCode == FundsTypeS { 116 | 117 | f.ImmediateAmount, size, err = util.ReadFieldAsInt(data, read) 118 | if err != nil { 119 | return 0, errors.New("FundsType: unable to parse amount") 120 | } else { 121 | read += size 122 | } 123 | 124 | f.OneDayAmount, size, err = util.ReadFieldAsInt(data, read) 125 | if err != nil { 126 | return 0, errors.New("FundsType: unable to parse amount") 127 | } else { 128 | read += size 129 | } 130 | 131 | f.TwoDayAmount, size, err = util.ReadFieldAsInt(data, read) 132 | if err != nil { 133 | return 0, errors.New("FundsType: unable to parse amount") 134 | } else { 135 | read += size 136 | } 137 | 138 | } else if f.TypeCode == FundsTypeV { 139 | f.Date, size, err = util.ReadField(data, read) 140 | if err != nil { 141 | return 0, errors.New("FundsType: unable to parse date") 142 | } else { 143 | read += size 144 | } 145 | 146 | f.Time, size, err = util.ReadField(data, read) 147 | if err != nil { 148 | return 0, errors.New("FundsType: unable to parse time") 149 | } else { 150 | read += size 151 | } 152 | } else if f.TypeCode == FundsTypeD { 153 | f.DistributionNumber, size, err = util.ReadFieldAsInt(data, read) 154 | if err != nil { 155 | return 0, errors.New("FundsType: unable to parse distribution number") 156 | } else { 157 | read += size 158 | } 159 | 160 | for index := int64(0); index < f.DistributionNumber; index++ { 161 | 162 | var amount, day int64 163 | 164 | day, size, err = util.ReadFieldAsInt(data, read) 165 | if err != nil { 166 | return 0, errors.New("FundsType: unable to parse day") 167 | } else { 168 | read += size 169 | } 170 | 171 | amount, size, err = util.ReadFieldAsInt(data, read) 172 | if err != nil { 173 | return 0, errors.New("FundsType: unable to parse amount") 174 | } else { 175 | read += size 176 | } 177 | 178 | f.Distributions = append(f.Distributions, Distribution{Day: day, Amount: amount}) 179 | 180 | } 181 | } 182 | 183 | if strings.Contains(data[:read-1], "/") { 184 | return 0, errors.New("FundsType: unable to parse sub elements") 185 | } 186 | 187 | if err = f.Validate(); err != nil { 188 | return 0, err 189 | } 190 | 191 | return read, nil 192 | } 193 | 194 | type Distribution struct { 195 | Day int64 `json:"day,omitempty"` // availability amount 196 | Amount int64 `json:"amount,omitempty"` // availability amount 197 | } 198 | 199 | type FundsTypeCode string 200 | 201 | func (c FundsTypeCode) Validate() error { 202 | 203 | str := string(c) 204 | if len(str) == 0 { 205 | return nil 206 | } 207 | 208 | availableTypes := []string{FundsType0, FundsType1, FundsType2, FundsTypeS, FundsTypeV, FundsTypeD, FundsTypeZ} 209 | 210 | for _, t := range availableTypes { 211 | if strings.ToUpper(str) == t { 212 | return nil 213 | } 214 | } 215 | 216 | return errors.New("invalid fund type") 217 | } 218 | -------------------------------------------------------------------------------- /pkg/lib/funds_type_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func mockFuncType() FundsType { 14 | return FundsType{ 15 | ImmediateAmount: 10000, 16 | OneDayAmount: -20000, 17 | TwoDayAmount: 30000, 18 | Date: "040701", 19 | Time: "1300", 20 | DistributionNumber: 5, 21 | Distributions: []Distribution{ 22 | { 23 | Day: 1, 24 | Amount: 100, 25 | }, 26 | { 27 | Day: 2, 28 | Amount: 200, 29 | }, 30 | { 31 | Day: 3, 32 | Amount: 300, 33 | }, 34 | { 35 | Day: 4, 36 | Amount: 400, 37 | }, 38 | { 39 | Day: 5, 40 | Amount: 500, 41 | }, 42 | }, 43 | } 44 | } 45 | 46 | func TestFundsType_String(t *testing.T) { 47 | f := mockFuncType() 48 | 49 | require.NoError(t, f.Validate()) 50 | require.Equal(t, "", f.String()) 51 | 52 | f.TypeCode = FundsType0 53 | require.NoError(t, f.Validate()) 54 | require.Equal(t, "0", f.String()) 55 | 56 | f.TypeCode = FundsType1 57 | require.NoError(t, f.Validate()) 58 | require.Equal(t, "1", f.String()) 59 | 60 | f.TypeCode = FundsType2 61 | require.NoError(t, f.Validate()) 62 | require.Equal(t, "2", f.String()) 63 | 64 | f.TypeCode = FundsTypeS 65 | require.NoError(t, f.Validate()) 66 | require.Equal(t, "S,10000,-20000,30000", f.String()) 67 | 68 | f.TypeCode = FundsTypeV 69 | require.NoError(t, f.Validate()) 70 | require.Equal(t, "V,040701,1300", f.String()) 71 | 72 | f.TypeCode = FundsTypeZ 73 | require.NoError(t, f.Validate()) 74 | require.Equal(t, "Z", f.String()) 75 | 76 | f.TypeCode = FundsTypeD 77 | require.NoError(t, f.Validate()) 78 | require.Equal(t, "D,5,1,100,2,200,3,300,4,400,5,500", f.String()) 79 | 80 | f.DistributionNumber = 0 81 | require.Error(t, f.Validate()) 82 | require.Equal(t, "number of distributions is not match", f.Validate().Error()) 83 | 84 | f.Distributions = nil 85 | require.NoError(t, f.Validate()) 86 | require.Equal(t, "D,0", f.String()) 87 | } 88 | 89 | func TestFundsType_Parse(t *testing.T) { 90 | 91 | type testsample struct { 92 | Input string 93 | IsReadErr bool 94 | IsValidateErr bool 95 | CodeType string 96 | ReadSize int 97 | Output string 98 | } 99 | 100 | samples := []testsample{ 101 | { 102 | Input: ",", 103 | IsReadErr: false, 104 | IsValidateErr: false, 105 | CodeType: "", 106 | ReadSize: 1, 107 | Output: "", 108 | }, 109 | { 110 | Input: "Z,", 111 | IsReadErr: false, 112 | IsValidateErr: false, 113 | CodeType: FundsTypeZ, 114 | ReadSize: 2, 115 | Output: "Z", 116 | }, 117 | { 118 | Input: "0,10000,", 119 | IsReadErr: false, 120 | IsValidateErr: false, 121 | CodeType: FundsType0, 122 | ReadSize: 2, 123 | Output: "0", 124 | }, 125 | { 126 | Input: "1,10000,", 127 | IsReadErr: false, 128 | IsValidateErr: false, 129 | CodeType: FundsType1, 130 | ReadSize: 2, 131 | Output: "1", 132 | }, 133 | { 134 | Input: "2,10000,", 135 | IsReadErr: false, 136 | IsValidateErr: false, 137 | CodeType: FundsType2, 138 | ReadSize: 2, 139 | Output: "2", 140 | }, 141 | { 142 | Input: "S,10000,-20000,30000,", 143 | IsReadErr: false, 144 | IsValidateErr: false, 145 | CodeType: FundsTypeS, 146 | ReadSize: 21, 147 | Output: "S,10000,-20000,30000", 148 | }, 149 | { 150 | Input: "V,040701,1300,", 151 | IsReadErr: false, 152 | IsValidateErr: false, 153 | CodeType: FundsTypeV, 154 | ReadSize: 14, 155 | Output: "V,040701,1300", 156 | }, 157 | { 158 | Input: "D,5,1,10000,2,10000,3,10000,4,10000,5,10000,", 159 | IsReadErr: false, 160 | IsValidateErr: false, 161 | CodeType: FundsTypeD, 162 | ReadSize: 44, 163 | Output: "D,5,1,10000,2,10000,3,10000,4,10000,5,10000", 164 | }, 165 | { 166 | Input: "D,0,1,10000,2,10000,3,10000,4,10000,5,10000,", 167 | IsReadErr: false, 168 | IsValidateErr: false, 169 | CodeType: FundsTypeD, 170 | ReadSize: 4, 171 | Output: "D,0", 172 | }, 173 | { 174 | Input: "D/0,1,10000,2,10000,3,10000,4,10000,5,10000,", 175 | IsReadErr: true, 176 | IsValidateErr: false, 177 | CodeType: FundsTypeD, 178 | ReadSize: 0, 179 | Output: "D,0", 180 | }, 181 | } 182 | 183 | for _, sample := range samples { 184 | f := FundsType{} 185 | 186 | size, err := f.parse(sample.Input) 187 | 188 | if sample.IsReadErr { 189 | require.Error(t, err) 190 | } else { 191 | require.NoError(t, err) 192 | } 193 | 194 | if sample.IsValidateErr { 195 | require.Error(t, f.Validate()) 196 | } else { 197 | require.NoError(t, f.Validate()) 198 | } 199 | 200 | require.Equal(t, sample.CodeType, string(f.TypeCode)) 201 | require.Equal(t, sample.ReadSize, size) 202 | require.Equal(t, sample.Output, f.String()) 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /pkg/lib/group.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "fmt" 11 | "strconv" 12 | 13 | "github.com/moov-io/bai2/pkg/util" 14 | ) 15 | 16 | /* 17 | 18 | FILE STRUCTURE 19 | 20 | To simplify processing, balance reporting transmission files are divided into “envelopes” of data. 21 | These envelopes organize data at the following levels: 22 | • Account 23 | • Group 24 | • File 25 | 26 | Account: 27 | The first level of organization is the account. An account envelope includes balance and transaction data. 28 | Example: Account #1256793 at Last National Bank, previous day information as of midnight. 29 | 30 | Group: 31 | The next level of organization is the group. A group includes one or more account envelopes, all of which represent accounts at the same financial institution. 32 | All information in a group is for the same date and time. 33 | Example: Several accounts from Last National Bank to XYZ Reporting Service, sameday information as of 9:00 AM. 34 | 35 | */ 36 | 37 | // Creating new group object 38 | func NewGroup() *Group { 39 | return &Group{} 40 | } 41 | 42 | // Group Format 43 | type Group struct { 44 | // Group Header 45 | Receiver string `json:"receiver,omitempty"` 46 | Originator string `json:"originator"` 47 | GroupStatus int64 `json:"groupStatus"` 48 | AsOfDate string `json:"asOfDate"` 49 | AsOfTime string `json:"asOfTime,omitempty"` 50 | CurrencyCode string `json:"currencyCode,omitempty"` 51 | AsOfDateModifier int64 `json:"asOfDateModifier,omitempty"` 52 | 53 | // Group Trailer 54 | GroupControlTotal string `json:"groupControlTotal"` 55 | NumberOfAccounts int64 `json:"numberOfAccounts"` 56 | NumberOfRecords int64 `json:"numberOfRecords"` 57 | 58 | Accounts []Account 59 | 60 | header groupHeader 61 | trailer groupTrailer 62 | } 63 | 64 | func (r *Group) copyRecords() { 65 | 66 | r.header = groupHeader{ 67 | Receiver: r.Receiver, 68 | Originator: r.Originator, 69 | GroupStatus: r.GroupStatus, 70 | AsOfDate: r.AsOfDate, 71 | AsOfTime: r.AsOfTime, 72 | CurrencyCode: r.CurrencyCode, 73 | AsOfDateModifier: r.AsOfDateModifier, 74 | } 75 | 76 | r.trailer = groupTrailer{ 77 | GroupControlTotal: r.GroupControlTotal, 78 | NumberOfAccounts: r.NumberOfAccounts, 79 | NumberOfRecords: r.NumberOfRecords, 80 | } 81 | 82 | } 83 | 84 | // Sums the number of 02,03,16,88,49,98 records in the group. Maps to the NumberOfRecords field 85 | func (g *Group) SumRecords() int64 { 86 | var sum int64 87 | for _, account := range g.Accounts { 88 | sum += account.NumberRecords 89 | } 90 | // Add two for the group header and trailer records 91 | return sum + 2 92 | } 93 | 94 | // Sums the number of accounts in the group. Maps to the NumberOfAccounts field 95 | func (g *Group) SumNumberOfAccounts() int64 { 96 | return int64(len(g.Accounts)) 97 | } 98 | 99 | // Sums the account control totals in the group. Maps to the GroupControlTotal field 100 | func (a *Group) SumAccountControlTotals() (string, error) { 101 | if err := a.Validate(); err != nil { 102 | return "0", err 103 | } 104 | var sum int64 105 | for _, account := range a.Accounts { 106 | amt, err := strconv.ParseInt(account.AccountControlTotal, 10, 64) 107 | if err != nil { 108 | return "0", err 109 | } 110 | sum += amt 111 | } 112 | return fmt.Sprint(sum), nil 113 | } 114 | 115 | func (r *Group) String(opts ...int64) string { 116 | 117 | r.copyRecords() 118 | 119 | var buf bytes.Buffer 120 | buf.WriteString(r.header.string() + "\n") 121 | for i := range r.Accounts { 122 | buf.WriteString(r.Accounts[i].String(opts...) + "\n") 123 | } 124 | buf.WriteString(r.trailer.string()) 125 | 126 | return buf.String() 127 | } 128 | 129 | func (r *Group) Validate() error { 130 | 131 | r.copyRecords() 132 | 133 | if err := r.header.validate(); err != nil { 134 | return err 135 | } 136 | 137 | for i := range r.Accounts { 138 | if err := r.Accounts[i].Validate(); err != nil { 139 | return err 140 | } 141 | } 142 | 143 | if err := r.trailer.validate(); err != nil { 144 | return err 145 | } 146 | 147 | return nil 148 | } 149 | 150 | func (r *Group) Read(scan *Bai2Scanner, useCurrentLine bool) error { 151 | if scan == nil { 152 | return errors.New("invalid bai2 scanner") 153 | } 154 | 155 | var err error 156 | for line := scan.ScanLine(useCurrentLine); line != ""; line = scan.ScanLine(useCurrentLine) { 157 | useCurrentLine = false 158 | 159 | // find record code 160 | if len(line) < 3 { 161 | continue 162 | } 163 | 164 | switch line[:2] { 165 | case util.GroupHeaderCode: 166 | newRecord := groupHeader{} 167 | _, err = newRecord.parse(line) 168 | if err != nil { 169 | return fmt.Errorf("ERROR parsing group header on line %d (%v)", scan.GetLineIndex(), err) 170 | } 171 | 172 | r.Receiver = newRecord.Receiver 173 | r.Originator = newRecord.Originator 174 | r.GroupStatus = newRecord.GroupStatus 175 | r.AsOfDate = newRecord.AsOfDate 176 | r.AsOfTime = newRecord.AsOfTime 177 | r.CurrencyCode = newRecord.CurrencyCode 178 | r.AsOfDateModifier = newRecord.AsOfDateModifier 179 | 180 | case util.AccountIdentifierCode: 181 | newAccount := NewAccount() 182 | err = newAccount.Read(scan, true) 183 | if err != nil { 184 | return err 185 | } 186 | 187 | r.Accounts = append(r.Accounts, *newAccount) 188 | 189 | case util.GroupTrailerCode: 190 | newRecord := groupTrailer{} 191 | _, err = newRecord.parse(line) 192 | if err != nil { 193 | return fmt.Errorf("ERROR parsing group trailer on line %d (%v)", scan.GetLineIndex(), err) 194 | } 195 | 196 | r.GroupControlTotal = newRecord.GroupControlTotal 197 | r.NumberOfAccounts = newRecord.NumberOfAccounts 198 | r.NumberOfRecords = newRecord.NumberOfRecords 199 | 200 | return nil 201 | 202 | default: 203 | return fmt.Errorf("ERROR parsing group on line %d (unable to read record type %s)", scan.GetLineIndex(), line[0:2]) 204 | } 205 | } 206 | 207 | return nil 208 | } 209 | -------------------------------------------------------------------------------- /pkg/lib/group_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "bytes" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestGroupWithSampleData1(t *testing.T) { 16 | 17 | raw := ` 18 | 02,12345,0004,1,060317,,CAD,/ 19 | 03,10200123456,CAD,040,+000000000000,,,045,+000000000000,,/ 20 | 88,100,000000000208500,00003,V,060316,,400,000000000208500,00008,V,060316,/ 21 | 16,409,000000000002500,V,060316,,,,RETURNED CHEQUE / 22 | 49,+00000000000834000,14/ 23 | 03,10200123456,CAD,040,+000000000000,,,045,+000000000000,,/ 24 | 16,409,000000000000500,V,060317,,,,GALERIES RICHELIEU / 25 | 88,100,000000000208500,00003,V,060316,,400,000000000208500,00008,V,060316,/ 26 | 49,+00000000000446000,9/ 27 | 98,+00000000001280000,2,25/ 28 | ` 29 | 30 | group := Group{} 31 | scan := NewBai2Scanner(bytes.NewReader([]byte(raw))) 32 | 33 | err := group.Read(&scan, false) 34 | require.NoError(t, err) 35 | require.NoError(t, group.Validate()) 36 | require.Equal(t, 10, scan.GetLineIndex()) 37 | } 38 | 39 | func TestGroupWithSampleData2(t *testing.T) { 40 | 41 | raw := ` 42 | 02,12345,0004,1,060317,,CAD,/ 43 | 03,10200123456,CAD,040,+000000000000,,,045,+000000000000,,/ 44 | 88,100,000000000208500,00003,V,060316,,400,000000000208500,00008,V,060316,/ 45 | 16,409,000000000002500,V,060316,,,,RETURNED CHEQUE / 46 | 49,+00000000000834000,14/ 47 | 03,10200123456,CAD,040,+000000000000,,,045,+000000000000,,/ 48 | 16,409,000000000000500,V,060317,,,,GALERIES RICHELIEU / 49 | 88,100,000000000208500,00003,V,060316,,400,000000000208500,00008,V,060316,/ 50 | 49,+00000000000446000,9/ 51 | 98,+00000000001280000,2,25/ 52 | 99,+00000000001280000,1,27/ 53 | ` 54 | 55 | group := Group{} 56 | scan := NewBai2Scanner(bytes.NewReader([]byte(raw))) 57 | 58 | err := group.Read(&scan, false) 59 | require.NoError(t, err) 60 | require.NoError(t, group.Validate()) 61 | require.Equal(t, 10, scan.GetLineIndex()) 62 | require.Equal(t, "98,+00000000001280000,2,25/", scan.GetLine()) 63 | } 64 | 65 | func TestSumGroupRecords(t *testing.T) { 66 | group := Group{} 67 | group.Accounts = []Account{ 68 | {NumberRecords: 2}, 69 | {NumberRecords: 3}, 70 | {NumberRecords: 4}, 71 | } 72 | require.Equal(t, int64(11), group.SumRecords()) 73 | } 74 | 75 | func TestSumNumberOfAccounts(t *testing.T) { 76 | group := Group{} 77 | group.Accounts = []Account{ 78 | {NumberRecords: 2}, 79 | {NumberRecords: 3}, 80 | {NumberRecords: 4}, 81 | } 82 | require.Equal(t, int64(3), group.SumNumberOfAccounts()) 83 | } 84 | 85 | func TestSumAccountControlTotals(t *testing.T) { 86 | group := Group{} 87 | group.Receiver = "121000358" 88 | group.Originator = "121000358" 89 | group.GroupStatus = 1 90 | group.AsOfDate = time.Now().Format("060102") 91 | group.AsOfTime = time.Now().Format("1504") 92 | group.AsOfDateModifier = 2 93 | group.Accounts = []Account{ 94 | {AccountControlTotal: "100", AccountNumber: "9876543210"}, 95 | {AccountControlTotal: "-100", AccountNumber: "9876543210"}, 96 | {AccountControlTotal: "200", AccountNumber: "9876543210"}, 97 | } 98 | total, err := group.SumAccountControlTotals() 99 | require.NoError(t, err) 100 | require.Equal(t, "200", total) 101 | } 102 | -------------------------------------------------------------------------------- /pkg/lib/reader.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "bufio" 9 | "bytes" 10 | "fmt" 11 | "io" 12 | "log" 13 | "strings" 14 | "unicode" 15 | 16 | "github.com/moov-io/bai2/pkg/util" 17 | ) 18 | 19 | type Bai2Scanner struct { 20 | reader *bufio.Reader 21 | currentLine *bytes.Buffer 22 | index int 23 | } 24 | 25 | func NewBai2Scanner(fd io.Reader) Bai2Scanner { 26 | reader := bufio.NewReader(fd) 27 | currentLine := new(bytes.Buffer) 28 | return Bai2Scanner{reader: reader, currentLine: currentLine} 29 | } 30 | 31 | func (b *Bai2Scanner) GetLineIndex() int { 32 | return b.index 33 | } 34 | 35 | func (b *Bai2Scanner) GetLine() string { 36 | return strings.TrimSpace(b.currentLine.String()) 37 | } 38 | 39 | // ScanLine returns a line from the underlying reader 40 | // arg[0]: useCurrentLine (if false read a new line) 41 | func (b *Bai2Scanner) ScanLine(arg ...bool) string { 42 | 43 | useCurrentLine := false 44 | if len(arg) > 0 { 45 | useCurrentLine = arg[0] 46 | } 47 | 48 | if useCurrentLine { 49 | return b.GetLine() 50 | } 51 | 52 | // Reset the read buffer every time we read a new line. 53 | b.currentLine.Reset() 54 | 55 | for { 56 | // Read each rune in the file until a newline or a `/` or EOF. 57 | rune, _, err := b.reader.ReadRune() 58 | if err != nil { 59 | if err != io.EOF { 60 | log.Fatal(err) 61 | } 62 | break 63 | } 64 | 65 | char := string(rune) 66 | switch char { 67 | case "/": 68 | // Add `/` to line if it exists. Parsers use this to help internally represent the delineation 69 | // between records. 70 | b.currentLine.WriteString(char) 71 | // On observing a `/` character, check to see if we have a full record available 72 | // for processing -- with exception for transaction or continuation records. For those records, 73 | // the record is terminated by a newline followed by record code. 74 | line := strings.TrimSpace(b.currentLine.String()) 75 | if strings.HasPrefix(line, util.TransactionDetailCode) || strings.HasPrefix(line, util.ContinuationCode) { 76 | continue 77 | } 78 | goto fullLine 79 | case "\n", "\r": 80 | // On observing a newline character, check to see if we have a full record available for processing. 81 | goto fullLine 82 | default: 83 | b.currentLine.WriteString(char) 84 | } 85 | 86 | continue 87 | 88 | // This routine processes a "full line". In the context of a BAI2 file, a line is a single record 89 | // and may be terminated either by a `/` or a newline character. In specific circumstances, a logical record 90 | // ("line") may continue onto the next line, and in that event, processing should read the contents of 91 | // the following line before considering the record "complete". 92 | fullLine: 93 | line := strings.TrimSpace(b.currentLine.String()) 94 | // If the current line has only white space, ignore it and continue reading. 95 | if blankLine(line) { 96 | b.currentLine.Reset() 97 | continue 98 | } 99 | 100 | // If the line ends with a `/` delimiter, treat it as a complete record and process it as is. 101 | if strings.HasSuffix(line, "/") { 102 | break 103 | } 104 | 105 | // If a line ends with a newline character, look ahead to the next three bytes. If the next line 106 | // is a new record, it will have a defined and valid record code. If a valid record code is not 107 | // observed, continue parsing lines until a distinct record is observed. 108 | bytes, err := b.reader.Peek(3) 109 | if err != nil && err != io.EOF { 110 | log.Fatal(err) 111 | } 112 | 113 | // If the next three bytes are any of the defined BAI2 record codes (followed by a comma), we consider the next line 114 | // as a new record and process the current line up to this point. 115 | nextThreeBytes := string(bytes) 116 | headerCodes := []string{util.FileHeaderCode, util.GroupHeaderCode, util.AccountIdentifierCode, util.TransactionDetailCode, util.ContinuationCode, util.AccountTrailerCode, util.GroupTrailerCode, util.FileTrailerCode} 117 | nextLineHasNewRecord := false 118 | for _, header := range headerCodes { 119 | if nextThreeBytes == fmt.Sprintf("%s,", header) { 120 | b.currentLine.WriteString("/") 121 | nextLineHasNewRecord = true 122 | break 123 | } 124 | } 125 | 126 | if nextLineHasNewRecord { 127 | break 128 | } 129 | 130 | // Here, the current line "continued" onto the next line without a delimiter and without a new record code on 131 | // the subsequent line. Parse the next line as though it is a continuation of the current line. 132 | continue 133 | } 134 | 135 | b.index++ 136 | return b.GetLine() 137 | } 138 | 139 | func blankLine(line string) bool { 140 | for _, r := range line { 141 | if !unicode.IsSpace(r) { 142 | return false 143 | } 144 | } 145 | return true 146 | } 147 | -------------------------------------------------------------------------------- /pkg/lib/record_account_identifier.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | 11 | "github.com/moov-io/bai2/pkg/util" 12 | ) 13 | 14 | const ( 15 | aiParseErrorFmt = "AccountIdentifier: unable to parse %s" 16 | aiValidateErrorFmt = "AccountIdentifierCurrent: invalid %s" 17 | ) 18 | 19 | type AccountSummary struct { 20 | TypeCode string 21 | Amount string 22 | ItemCount int64 23 | FundsType FundsType 24 | } 25 | 26 | type accountIdentifier struct { 27 | AccountNumber string 28 | CurrencyCode string 29 | 30 | Summaries []AccountSummary 31 | } 32 | 33 | func (r *accountIdentifier) validate() error { 34 | 35 | if r.AccountNumber == "" { 36 | return fmt.Errorf(fmt.Sprintf(aiValidateErrorFmt, "AccountNumber")) 37 | } 38 | 39 | if r.CurrencyCode != "" && !util.ValidateCurrencyCode(r.CurrencyCode) { 40 | return fmt.Errorf(fmt.Sprintf(aiValidateErrorFmt, "CurrencyCode")) 41 | } 42 | 43 | for _, summary := range r.Summaries { 44 | if summary.Amount != "" && !util.ValidateAmount(summary.Amount) { 45 | return fmt.Errorf(fmt.Sprintf(aiValidateErrorFmt, "Amount")) 46 | } 47 | if summary.TypeCode != "" && !util.ValidateTypeCode(summary.TypeCode) { 48 | return fmt.Errorf(fmt.Sprintf(aiValidateErrorFmt, "TypeCode")) 49 | } 50 | if summary.FundsType.Validate() != nil { 51 | return fmt.Errorf(fmt.Sprintf(aiValidateErrorFmt, "FundsType")) 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func (r *accountIdentifier) parse(data string) (int, error) { 59 | 60 | var line string 61 | var err error 62 | var size, read int 63 | 64 | length := util.GetSize(data) 65 | if length < 3 { 66 | return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "record")) 67 | } else { 68 | line = data[:length] 69 | } 70 | 71 | // RecordCode 72 | if util.AccountIdentifierCode != data[:2] { 73 | return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "RecordCode")) 74 | } 75 | read += 3 76 | 77 | // AccountNumber 78 | if r.AccountNumber, size, err = util.ReadField(line, read); err != nil { 79 | return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "AccountNumber")) 80 | } else { 81 | read += size 82 | } 83 | 84 | // CurrencyCode 85 | if r.CurrencyCode, size, err = util.ReadField(line, read); err != nil { 86 | return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "CurrencyCode")) 87 | } else { 88 | read += size 89 | } 90 | 91 | for read < len(data) { 92 | 93 | var summary AccountSummary 94 | 95 | // TypeCode 96 | if summary.TypeCode, size, err = util.ReadField(line, read); err != nil { 97 | return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "TypeCode")) 98 | } else { 99 | read += size 100 | } 101 | 102 | // Amount 103 | if summary.Amount, size, err = util.ReadField(line, read); err != nil { 104 | return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "Amount")) 105 | } else { 106 | read += size 107 | } 108 | 109 | // ItemCount 110 | if summary.ItemCount, size, err = util.ReadFieldAsInt(line, read); err != nil { 111 | return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "ItemCount")) 112 | } else { 113 | read += size 114 | } 115 | 116 | if size, err = summary.FundsType.parse(line[read:]); err != nil { 117 | return 0, fmt.Errorf(fmt.Sprintf(aiParseErrorFmt, "FundsType")) 118 | } else { 119 | read += size 120 | } 121 | 122 | r.Summaries = append(r.Summaries, summary) 123 | } 124 | 125 | if err = r.validate(); err != nil { 126 | return 0, err 127 | } 128 | 129 | return read, nil 130 | } 131 | 132 | func (r *accountIdentifier) string(opts ...int64) string { 133 | 134 | var maxLen int64 135 | if len(opts) > 0 { 136 | maxLen = opts[0] 137 | } 138 | 139 | var total, buf bytes.Buffer 140 | 141 | buf.WriteString(fmt.Sprintf("%s,", util.AccountIdentifierCode)) 142 | buf.WriteString(fmt.Sprintf("%s,", r.AccountNumber)) 143 | buf.WriteString(fmt.Sprintf("%s,", r.CurrencyCode)) 144 | 145 | if len(r.Summaries) == 0 { 146 | buf.WriteString(",,,") 147 | } else { 148 | for index, summary := range r.Summaries { 149 | 150 | util.WriteBuffer(&total, &buf, summary.TypeCode, maxLen) 151 | buf.WriteString(",") 152 | 153 | util.WriteBuffer(&total, &buf, summary.Amount, maxLen) 154 | buf.WriteString(",") 155 | 156 | if summary.ItemCount == 0 { 157 | buf.WriteString(",") 158 | } else { 159 | util.WriteBuffer(&total, &buf, fmt.Sprintf("%d", summary.ItemCount), maxLen) 160 | buf.WriteString(",") 161 | } 162 | 163 | util.WriteBuffer(&total, &buf, summary.FundsType.String(), maxLen) 164 | 165 | if index < len(r.Summaries)-1 { 166 | buf.WriteString(",") 167 | } 168 | } 169 | } 170 | 171 | buf.WriteString("/") 172 | total.WriteString(buf.String()) 173 | 174 | return total.String() 175 | } 176 | -------------------------------------------------------------------------------- /pkg/lib/record_account_identifier_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func mockAccountIdentifier() *accountIdentifier { 14 | return &accountIdentifier{ 15 | AccountNumber: "0004", 16 | } 17 | } 18 | 19 | func TestAccountIdentifierCurrent(t *testing.T) { 20 | 21 | record := mockAccountIdentifier() 22 | require.NoError(t, record.validate()) 23 | 24 | record.AccountNumber = "" 25 | require.Error(t, record.validate()) 26 | require.Equal(t, "AccountIdentifierCurrent: invalid AccountNumber", record.validate().Error()) 27 | 28 | } 29 | 30 | func TestAccountIdentifierCurrentWithSample1(t *testing.T) { 31 | 32 | sample := "03,10200123456,CAD,040,+000000000000,,,045,+000000000000,4,0/" 33 | record := accountIdentifier{} 34 | 35 | size, err := record.parse(sample) 36 | require.NoError(t, err) 37 | require.Equal(t, 61, size) 38 | 39 | require.Equal(t, "10200123456", record.AccountNumber) 40 | require.Equal(t, "CAD", record.CurrencyCode) 41 | require.Equal(t, 2, len(record.Summaries)) 42 | 43 | summary := record.Summaries[0] 44 | require.Equal(t, "040", summary.TypeCode) 45 | require.Equal(t, "+000000000000", summary.Amount) 46 | require.Equal(t, int64(0), summary.ItemCount) 47 | require.Equal(t, "", string(summary.FundsType.TypeCode)) 48 | 49 | summary = record.Summaries[1] 50 | 51 | require.Equal(t, "045", summary.TypeCode) 52 | require.Equal(t, "+000000000000", summary.Amount) 53 | require.Equal(t, int64(4), summary.ItemCount) 54 | require.Equal(t, FundsType0, string(summary.FundsType.TypeCode)) 55 | 56 | require.Equal(t, sample, record.string()) 57 | } 58 | 59 | func TestAccountIdentifierCurrentWithSample2(t *testing.T) { 60 | 61 | sample := "03,5765432,,,,,/" 62 | record := accountIdentifier{} 63 | 64 | size, err := record.parse(sample) 65 | require.NoError(t, err) 66 | require.Equal(t, 16, size) 67 | 68 | require.Equal(t, "5765432", record.AccountNumber) 69 | require.Equal(t, 1, len(record.Summaries)) 70 | 71 | require.Equal(t, sample, record.string()) 72 | } 73 | 74 | func TestAccountIdentifierOutputWithContinuationRecords(t *testing.T) { 75 | 76 | record := accountIdentifier{ 77 | AccountNumber: "10200123456", 78 | CurrencyCode: "CAD", 79 | } 80 | 81 | for i := 0; i < 10; i++ { 82 | record.Summaries = append(record.Summaries, 83 | AccountSummary{ 84 | TypeCode: "040", 85 | Amount: "+000000000000", 86 | ItemCount: 10, 87 | }) 88 | } 89 | 90 | result := record.string() 91 | expectResult := `03,10200123456,CAD,040,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,/` 92 | require.Equal(t, expectResult, result) 93 | require.Equal(t, len(expectResult), len(result)) 94 | 95 | result = record.string(80) 96 | expectResult = `03,10200123456,CAD,040,+000000000000,10,,040,+000000000000,10,,040/ 97 | 88,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,,040/ 98 | 88,+000000000000,10,,040,+000000000000,10,,040,+000000000000,10,,040/ 99 | 88,+000000000000,10,,040,+000000000000,10,/` 100 | require.Equal(t, expectResult, result) 101 | require.Equal(t, len(expectResult), len(result)) 102 | 103 | result = record.string(50) 104 | expectResult = `03,10200123456,CAD,040,+000000000000,10,,040/ 105 | 88,+000000000000,10,,040,+000000000000,10,,040/ 106 | 88,+000000000000,10,,040,+000000000000,10,,040/ 107 | 88,+000000000000,10,,040,+000000000000,10,,040/ 108 | 88,+000000000000,10,,040,+000000000000,10,,040/ 109 | 88,+000000000000,10,/` 110 | require.Equal(t, expectResult, result) 111 | require.Equal(t, len(expectResult), len(result)) 112 | } 113 | -------------------------------------------------------------------------------- /pkg/lib/record_account_trailer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | 11 | "github.com/moov-io/bai2/pkg/util" 12 | ) 13 | 14 | const ( 15 | atParseErrorFmt = "AccountTrailer: unable to parse %s" 16 | atValidateErrorFmt = "AccountTrailer: invalid %s" 17 | ) 18 | 19 | type accountTrailer struct { 20 | AccountControlTotal string 21 | NumberRecords int64 22 | } 23 | 24 | func (h *accountTrailer) validate() error { 25 | if h.AccountControlTotal != "" && !util.ValidateAmount(h.AccountControlTotal) { 26 | return fmt.Errorf(fmt.Sprintf(atValidateErrorFmt, "Amount")) 27 | } 28 | 29 | return nil 30 | } 31 | 32 | func (h *accountTrailer) parse(data string) (int, error) { 33 | 34 | var line string 35 | var err error 36 | var size, read int 37 | 38 | length := util.GetSize(data) 39 | if length < 3 { 40 | return 0, fmt.Errorf(fmt.Sprintf(atParseErrorFmt, "record")) 41 | } else { 42 | line = data[:length] 43 | } 44 | 45 | // RecordCode 46 | if util.AccountTrailerCode != data[:2] { 47 | return 0, fmt.Errorf(fmt.Sprintf(fhParseErrorFmt, "RecordCode")) 48 | } 49 | read += 3 50 | 51 | // AccountControlTotal 52 | if h.AccountControlTotal, size, err = util.ReadField(line, read); err != nil { 53 | return 0, fmt.Errorf(fmt.Sprintf(atParseErrorFmt, "AccountControlTotal")) 54 | } else { 55 | read += size 56 | } 57 | 58 | // NumberRecords 59 | if h.NumberRecords, size, err = util.ReadFieldAsInt(line, read); err != nil { 60 | return 0, fmt.Errorf(fmt.Sprintf(atParseErrorFmt, "NumberRecords")) 61 | } else { 62 | read += size 63 | } 64 | 65 | if err = h.validate(); err != nil { 66 | return 0, err 67 | } 68 | 69 | return read, nil 70 | } 71 | 72 | func (h *accountTrailer) string() string { 73 | var buf bytes.Buffer 74 | 75 | buf.WriteString(fmt.Sprintf("%s,", util.AccountTrailerCode)) 76 | buf.WriteString(fmt.Sprintf("%s,", h.AccountControlTotal)) 77 | buf.WriteString(fmt.Sprintf("%d/", h.NumberRecords)) 78 | 79 | return buf.String() 80 | } 81 | -------------------------------------------------------------------------------- /pkg/lib/record_account_trailer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestAccountTrailer(t *testing.T) { 14 | 15 | record := accountTrailer{} 16 | require.NoError(t, record.validate()) 17 | 18 | } 19 | 20 | func TestAccountTrailerWithSample(t *testing.T) { 21 | 22 | sample := "49,+00000000000446000,9/" 23 | record := accountTrailer{} 24 | 25 | size, err := record.parse(sample) 26 | require.NoError(t, err) 27 | require.Equal(t, 24, size) 28 | 29 | require.Equal(t, "+00000000000446000", record.AccountControlTotal) 30 | require.Equal(t, int64(9), record.NumberRecords) 31 | 32 | require.Equal(t, sample, record.string()) 33 | } 34 | 35 | func TestAccountTrailerWithSample2(t *testing.T) { 36 | 37 | sample := "49,+00000000000446000" 38 | record := accountTrailer{} 39 | 40 | size, err := record.parse(sample) 41 | require.Equal(t, "AccountTrailer: unable to parse NumberRecords", err.Error()) 42 | require.Equal(t, 0, size) 43 | 44 | sample = "49,+00000000000446000/" 45 | size, err = record.parse(sample) 46 | require.Equal(t, "AccountTrailer: unable to parse NumberRecords", err.Error()) 47 | require.Equal(t, 0, size) 48 | 49 | } 50 | -------------------------------------------------------------------------------- /pkg/lib/record_file_header.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | 11 | "github.com/moov-io/bai2/pkg/util" 12 | ) 13 | 14 | const ( 15 | fhParseErrorFmt = "FileHeader: unable to parse %s" 16 | fhValidateErrorFmt = "FileHeader: invalid %s" 17 | ) 18 | 19 | type fileHeader struct { 20 | Sender string 21 | Receiver string 22 | FileCreatedDate string 23 | FileCreatedTime string 24 | FileIdNumber string 25 | PhysicalRecordLength int64 `json:",omitempty"` 26 | BlockSize int64 `json:",omitempty"` 27 | VersionNumber int64 28 | } 29 | 30 | func (h *fileHeader) validate(options Options) error { 31 | if h.Sender == "" { 32 | return fmt.Errorf(fmt.Sprintf(fhValidateErrorFmt, "Sender")) 33 | } 34 | if h.Receiver == "" { 35 | return fmt.Errorf(fmt.Sprintf(fhValidateErrorFmt, "Receiver")) 36 | } 37 | if h.FileCreatedDate == "" { 38 | return fmt.Errorf(fmt.Sprintf(fhValidateErrorFmt, "FileCreatedDate")) 39 | } else if !util.ValidateDate(h.FileCreatedDate) { 40 | return fmt.Errorf(fmt.Sprintf(fhValidateErrorFmt, "FileCreatedDate")) 41 | } 42 | if h.FileCreatedTime == "" { 43 | return fmt.Errorf(fmt.Sprintf(fhValidateErrorFmt, "FileCreatedTime")) 44 | } else if !util.ValidateTime(h.FileCreatedTime) { 45 | return fmt.Errorf(fmt.Sprintf(fhValidateErrorFmt, "FileCreatedTime")) 46 | } 47 | if h.FileIdNumber == "" { 48 | return fmt.Errorf(fmt.Sprintf(fhValidateErrorFmt, "FileIdNumber")) 49 | } 50 | if h.VersionNumber != 2 && !options.IgnoreVersion { 51 | return fmt.Errorf(fmt.Sprintf(fhValidateErrorFmt, "VersionNumber")) 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func (h *fileHeader) parse(data string, options Options) (int, error) { 58 | 59 | var line string 60 | var err error 61 | var size, read int 62 | 63 | if length := util.GetSize(data); length < 3 { 64 | return 0, fmt.Errorf(fmt.Sprintf(fhParseErrorFmt, "record")) 65 | } else { 66 | line = data[:length] 67 | } 68 | 69 | // RecordCode 70 | if util.FileHeaderCode != line[:2] { 71 | return 0, fmt.Errorf(fmt.Sprintf(fhParseErrorFmt, "RecordCode")) 72 | } 73 | read += 3 74 | 75 | // Sender 76 | if h.Sender, size, err = util.ReadField(line, read); err != nil { 77 | return 0, fmt.Errorf(fmt.Sprintf(fhParseErrorFmt, "Sender")) 78 | } else { 79 | read += size 80 | } 81 | 82 | // Receiver 83 | if h.Receiver, size, err = util.ReadField(line, read); err != nil { 84 | return 0, fmt.Errorf(fmt.Sprintf(fhParseErrorFmt, "Receiver")) 85 | } else { 86 | read += size 87 | } 88 | 89 | // FileCreatedDate 90 | if h.FileCreatedDate, size, err = util.ReadField(line, read); err != nil { 91 | return 0, fmt.Errorf(fmt.Sprintf(fhParseErrorFmt, "FileCreatedDate")) 92 | } else { 93 | read += size 94 | } 95 | 96 | // FileCreatedTime 97 | if h.FileCreatedTime, size, err = util.ReadField(line, read); err != nil { 98 | return 0, fmt.Errorf(fmt.Sprintf(fhParseErrorFmt, "FileCreatedTime")) 99 | } else { 100 | read += size 101 | } 102 | 103 | // FileIdNumber 104 | if h.FileIdNumber, size, err = util.ReadField(line, read); err != nil { 105 | return 0, fmt.Errorf(fmt.Sprintf(fhParseErrorFmt, "FileIdNumber")) 106 | } else { 107 | read += size 108 | } 109 | 110 | // PhysicalRecordLength 111 | if h.PhysicalRecordLength, size, err = util.ReadFieldAsInt(line, read); err != nil { 112 | return 0, fmt.Errorf(fmt.Sprintf(fhParseErrorFmt, "PhysicalRecordLength")) 113 | } else { 114 | read += size 115 | } 116 | 117 | // BlockSize 118 | if h.BlockSize, size, err = util.ReadFieldAsInt(line, read); err != nil { 119 | return 0, fmt.Errorf(fmt.Sprintf(fhParseErrorFmt, "BlockSize")) 120 | } else { 121 | read += size 122 | } 123 | 124 | // VersionNumber 125 | if h.VersionNumber, size, err = util.ReadFieldAsInt(line, read); err != nil { 126 | return 0, fmt.Errorf(fmt.Sprintf(fhParseErrorFmt, "VersionNumber")) 127 | } else { 128 | read += size 129 | } 130 | 131 | if err = h.validate(options); err != nil { 132 | return 0, err 133 | } 134 | 135 | return read, nil 136 | } 137 | 138 | func (h *fileHeader) string() string { 139 | var buf bytes.Buffer 140 | 141 | buf.WriteString(fmt.Sprintf("%s,", util.FileHeaderCode)) 142 | buf.WriteString(fmt.Sprintf("%s,", h.Sender)) 143 | buf.WriteString(fmt.Sprintf("%s,", h.Receiver)) 144 | buf.WriteString(fmt.Sprintf("%s,", h.FileCreatedDate)) 145 | buf.WriteString(fmt.Sprintf("%s,", h.FileCreatedTime)) 146 | buf.WriteString(fmt.Sprintf("%s,", h.FileIdNumber)) 147 | if h.PhysicalRecordLength > 0 { 148 | buf.WriteString(fmt.Sprintf("%d,", h.PhysicalRecordLength)) 149 | } else { 150 | buf.WriteString(",") 151 | } 152 | if h.BlockSize > 0 { 153 | buf.WriteString(fmt.Sprintf("%d,", h.BlockSize)) 154 | } else { 155 | buf.WriteString(",") 156 | } 157 | buf.WriteString(fmt.Sprintf("%d/", h.VersionNumber)) 158 | 159 | return buf.String() 160 | } 161 | -------------------------------------------------------------------------------- /pkg/lib/record_file_header_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func mockFileHeader() *fileHeader { 14 | return &fileHeader{ 15 | Sender: "0004", 16 | Receiver: "12345", 17 | FileCreatedDate: "060321", 18 | FileCreatedTime: "0829", 19 | FileIdNumber: "001", 20 | PhysicalRecordLength: 80, 21 | BlockSize: 1, 22 | VersionNumber: 2, 23 | } 24 | } 25 | 26 | func TestFileHeader(t *testing.T) { 27 | var options Options 28 | 29 | record := mockFileHeader() 30 | require.NoError(t, record.validate(options)) 31 | 32 | record.VersionNumber = 0 33 | require.Error(t, record.validate(options)) 34 | require.Equal(t, "FileHeader: invalid VersionNumber", record.validate(options).Error()) 35 | 36 | record.FileIdNumber = "" 37 | require.Error(t, record.validate(options)) 38 | require.Equal(t, "FileHeader: invalid FileIdNumber", record.validate(options).Error()) 39 | 40 | record.FileCreatedTime = "" 41 | require.Error(t, record.validate(options)) 42 | require.Equal(t, "FileHeader: invalid FileCreatedTime", record.validate(options).Error()) 43 | 44 | record.FileCreatedDate = "" 45 | require.Error(t, record.validate(options)) 46 | require.Equal(t, "FileHeader: invalid FileCreatedDate", record.validate(options).Error()) 47 | 48 | record.Receiver = "" 49 | require.Error(t, record.validate(options)) 50 | require.Equal(t, "FileHeader: invalid Receiver", record.validate(options).Error()) 51 | 52 | record.Sender = "" 53 | require.Error(t, record.validate(options)) 54 | require.Equal(t, "FileHeader: invalid Sender", record.validate(options).Error()) 55 | 56 | } 57 | 58 | func TestFileHeaderWithOptional(t *testing.T) { 59 | var options Options 60 | 61 | sample := "01,0004,12345,060321,0829,001,80,1,2/" 62 | record := fileHeader{} 63 | 64 | size, err := record.parse(sample, options) 65 | require.NoError(t, err) 66 | require.Equal(t, 37, size) 67 | 68 | require.Equal(t, "0004", record.Sender) 69 | require.Equal(t, "12345", record.Receiver) 70 | require.Equal(t, "060321", record.FileCreatedDate) 71 | require.Equal(t, "0829", record.FileCreatedTime) 72 | require.Equal(t, "001", record.FileIdNumber) 73 | require.Equal(t, int64(80), record.PhysicalRecordLength) 74 | require.Equal(t, int64(1), record.BlockSize) 75 | require.Equal(t, int64(2), record.VersionNumber) 76 | 77 | require.Equal(t, sample, record.string()) 78 | } 79 | 80 | func TestFileHeaderIgnoreVersion(t *testing.T) { 81 | options := Options{ 82 | IgnoreVersion: true, 83 | } 84 | 85 | sample := "01,0004,12345,060321,0829,001,80,1,3/" 86 | record := fileHeader{} 87 | 88 | size, err := record.parse(sample, options) 89 | require.NoError(t, err) 90 | require.Equal(t, 37, size) 91 | 92 | require.Equal(t, "0004", record.Sender) 93 | require.Equal(t, "12345", record.Receiver) 94 | require.Equal(t, "060321", record.FileCreatedDate) 95 | require.Equal(t, "0829", record.FileCreatedTime) 96 | require.Equal(t, "001", record.FileIdNumber) 97 | require.Equal(t, int64(80), record.PhysicalRecordLength) 98 | require.Equal(t, int64(1), record.BlockSize) 99 | require.Equal(t, int64(3), record.VersionNumber) 100 | 101 | require.Equal(t, sample, record.string()) 102 | } 103 | 104 | func TestFileHeaderWithoutOptional(t *testing.T) { 105 | var options Options 106 | 107 | sample := "01,2,12345,060321,0829,1,,,2/" 108 | record := fileHeader{} 109 | 110 | size, err := record.parse(sample, options) 111 | require.NoError(t, err) 112 | require.Equal(t, 29, size) 113 | 114 | require.Equal(t, "2", record.Sender) 115 | require.Equal(t, "12345", record.Receiver) 116 | require.Equal(t, "060321", record.FileCreatedDate) 117 | require.Equal(t, "0829", record.FileCreatedTime) 118 | require.Equal(t, "1", record.FileIdNumber) 119 | require.Equal(t, int64(0), record.PhysicalRecordLength) 120 | require.Equal(t, int64(0), record.BlockSize) 121 | require.Equal(t, int64(2), record.VersionNumber) 122 | 123 | require.Equal(t, sample, record.string()) 124 | } 125 | 126 | func TestFileHeaderWithInvalidSample(t *testing.T) { 127 | var options Options 128 | 129 | record := fileHeader{} 130 | _, err := record.parse("01,2,12345,06032,0829,1,,,2/", options) 131 | require.Error(t, err) 132 | 133 | _, err = record.parse("01,2,12345,060321,082,1,,,2/", options) 134 | require.Error(t, err) 135 | 136 | _, err = record.parse("01,2,12345,060321,082a,1,,,2/", options) 137 | require.Error(t, err) 138 | } 139 | 140 | func TestFileHeaderWithInvalidSample2(t *testing.T) { 141 | 142 | sample := "01,2,12345,06032,0829,1" 143 | record := accountIdentifier{} 144 | 145 | size, err := record.parse(sample) 146 | require.Equal(t, "AccountIdentifier: unable to parse RecordCode", err.Error()) 147 | require.Equal(t, 0, size) 148 | 149 | sample = "01,2,12345/" 150 | size, err = record.parse(sample) 151 | require.Equal(t, "AccountIdentifier: unable to parse RecordCode", err.Error()) 152 | require.Equal(t, 0, size) 153 | 154 | } 155 | -------------------------------------------------------------------------------- /pkg/lib/record_file_trailer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | 11 | "github.com/moov-io/bai2/pkg/util" 12 | ) 13 | 14 | const ( 15 | ftParseErrorFmt = "FileTrailer: unable to parse %s" 16 | ftValidateErrorFmt = "FileTrailer: invalid %s" 17 | ) 18 | 19 | type fileTrailer struct { 20 | FileControlTotal string 21 | NumberOfGroups int64 22 | NumberOfRecords int64 23 | } 24 | 25 | func (h *fileTrailer) validate() error { 26 | if h.FileControlTotal != "" && !util.ValidateAmount(h.FileControlTotal) { 27 | return fmt.Errorf(fmt.Sprintf(ftValidateErrorFmt, "FileControlTotal")) 28 | } 29 | 30 | return nil 31 | } 32 | 33 | func (h *fileTrailer) parse(data string) (int, error) { 34 | 35 | var line string 36 | var err error 37 | var size, read int 38 | 39 | if length := util.GetSize(data); length < 3 { 40 | return 0, fmt.Errorf(fmt.Sprintf(ftParseErrorFmt, "record")) 41 | } else { 42 | line = data[:length] 43 | } 44 | 45 | // RecordCode 46 | if util.FileTrailerCode != line[:2] { 47 | return 0, fmt.Errorf(fmt.Sprintf(fhParseErrorFmt, "RecordCode")) 48 | } 49 | read += 3 50 | 51 | // GroupControlTotal 52 | if h.FileControlTotal, size, err = util.ReadField(line, read); err != nil { 53 | return 0, fmt.Errorf(fmt.Sprintf(ftParseErrorFmt, "GroupControlTotal")) 54 | } else { 55 | read += size 56 | } 57 | 58 | // NumberOfGroups 59 | if h.NumberOfGroups, size, err = util.ReadFieldAsInt(line, read); err != nil { 60 | return 0, fmt.Errorf(fmt.Sprintf(ftParseErrorFmt, "NumberOfGroups")) 61 | } else { 62 | read += size 63 | } 64 | 65 | // NumberOfRecords 66 | if h.NumberOfRecords, size, err = util.ReadFieldAsInt(line, read); err != nil { 67 | return 0, fmt.Errorf(fmt.Sprintf(ftParseErrorFmt, "NumberOfRecords")) 68 | } else { 69 | read += size 70 | } 71 | 72 | if err = h.validate(); err != nil { 73 | return 0, err 74 | } 75 | 76 | return read, nil 77 | } 78 | 79 | func (h *fileTrailer) string() string { 80 | var buf bytes.Buffer 81 | 82 | buf.WriteString(fmt.Sprintf("%s,", util.FileTrailerCode)) 83 | buf.WriteString(fmt.Sprintf("%s,", h.FileControlTotal)) 84 | buf.WriteString(fmt.Sprintf("%d,", h.NumberOfGroups)) 85 | buf.WriteString(fmt.Sprintf("%d/", h.NumberOfRecords)) 86 | 87 | return buf.String() 88 | } 89 | -------------------------------------------------------------------------------- /pkg/lib/record_file_trailer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestFileTrailer(t *testing.T) { 14 | 15 | record := fileTrailer{} 16 | require.NoError(t, record.validate()) 17 | 18 | } 19 | 20 | func TestFileTrailerWithSample(t *testing.T) { 21 | 22 | sample := "99,+00000000001280000,1,27/" 23 | record := fileTrailer{} 24 | 25 | size, err := record.parse(sample) 26 | require.NoError(t, err) 27 | require.Equal(t, 27, size) 28 | 29 | require.Equal(t, "+00000000001280000", record.FileControlTotal) 30 | require.Equal(t, int64(1), record.NumberOfGroups) 31 | require.Equal(t, int64(27), record.NumberOfRecords) 32 | 33 | require.Equal(t, sample, record.string()) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/lib/record_group_header.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | 11 | "github.com/moov-io/bai2/pkg/util" 12 | ) 13 | 14 | const ( 15 | ghParseErrorFmt = "GroupHeader: unable to parse %s" 16 | ghValidateErrorFmt = "GroupHeader: invalid %s" 17 | ) 18 | 19 | type groupHeader struct { 20 | Receiver string `json:",omitempty"` 21 | Originator string 22 | GroupStatus int64 23 | AsOfDate string 24 | AsOfTime string `json:",omitempty"` 25 | CurrencyCode string `json:",omitempty"` 26 | AsOfDateModifier int64 `json:",omitempty"` 27 | } 28 | 29 | func (h *groupHeader) validate() error { 30 | if h.Originator == "" { 31 | return fmt.Errorf(fmt.Sprintf(ghValidateErrorFmt, "Originator")) 32 | } 33 | if h.GroupStatus < 0 || h.GroupStatus > 4 { 34 | return fmt.Errorf(fmt.Sprintf(ghValidateErrorFmt, "GroupStatus")) 35 | } 36 | if h.AsOfDate == "" { 37 | return fmt.Errorf(fmt.Sprintf(ghValidateErrorFmt, "AsOfDate")) 38 | } else if !util.ValidateDate(h.AsOfDate) { 39 | return fmt.Errorf(fmt.Sprintf(ghValidateErrorFmt, "AsOfDate")) 40 | } 41 | if h.AsOfTime != "" && !util.ValidateTime(h.AsOfTime) { 42 | return fmt.Errorf(fmt.Sprintf(ghValidateErrorFmt, "AsOfTime")) 43 | } 44 | if h.CurrencyCode != "" && !util.ValidateCurrencyCode(h.CurrencyCode) { 45 | return fmt.Errorf(fmt.Sprintf(ghValidateErrorFmt, "CurrencyCode")) 46 | } 47 | if h.AsOfDateModifier < 0 || h.AsOfDateModifier > 4 { 48 | return fmt.Errorf(fmt.Sprintf(ghValidateErrorFmt, "AsOfDateModifier")) 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func (h *groupHeader) parse(data string) (int, error) { 55 | 56 | var line string 57 | var err error 58 | var size, read int 59 | 60 | if length := util.GetSize(data); length < 3 { 61 | return 0, fmt.Errorf(fmt.Sprintf(ghParseErrorFmt, "record")) 62 | } else { 63 | line = data[:length] 64 | } 65 | 66 | // RecordCode 67 | if util.GroupHeaderCode != data[:2] { 68 | return 0, fmt.Errorf(fmt.Sprintf(fhParseErrorFmt, "RecordCode")) 69 | } 70 | read += 3 71 | 72 | // Receiver 73 | if h.Receiver, size, err = util.ReadField(line, read); err != nil { 74 | return 0, fmt.Errorf(fmt.Sprintf(ghParseErrorFmt, "Receiver")) 75 | } else { 76 | read += size 77 | } 78 | 79 | // Originator 80 | if h.Originator, size, err = util.ReadField(line, read); err != nil { 81 | return 0, fmt.Errorf(fmt.Sprintf(ghParseErrorFmt, "Originator")) 82 | } else { 83 | read += size 84 | } 85 | 86 | // GroupStatus 87 | if h.GroupStatus, size, err = util.ReadFieldAsInt(line, read); err != nil { 88 | return 0, fmt.Errorf(fmt.Sprintf(ghParseErrorFmt, "GroupStatus")) 89 | } else { 90 | read += size 91 | } 92 | 93 | // AsOfDate 94 | if h.AsOfDate, size, err = util.ReadField(line, read); err != nil { 95 | return 0, fmt.Errorf(fmt.Sprintf(ghParseErrorFmt, "AsOfDate")) 96 | } else { 97 | read += size 98 | } 99 | 100 | // AsOfTime 101 | if h.AsOfTime, size, err = util.ReadField(line, read); err != nil { 102 | return 0, fmt.Errorf(fmt.Sprintf(ghParseErrorFmt, "AsOfTime")) 103 | } else { 104 | read += size 105 | } 106 | 107 | // CurrencyCode 108 | if h.CurrencyCode, size, err = util.ReadField(line, read); err != nil { 109 | return 0, fmt.Errorf(fmt.Sprintf(ghParseErrorFmt, "CurrencyCode")) 110 | } else { 111 | read += size 112 | } 113 | 114 | // AsOfDateModifier 115 | if h.AsOfDateModifier, size, err = util.ReadFieldAsInt(line, read); err != nil { 116 | return 0, fmt.Errorf(fmt.Sprintf(ghParseErrorFmt, "AsOfDateModifier")) 117 | } else { 118 | read += size 119 | } 120 | 121 | if err = h.validate(); err != nil { 122 | return 0, err 123 | } 124 | 125 | return read, nil 126 | } 127 | 128 | func (h *groupHeader) string() string { 129 | var buf bytes.Buffer 130 | 131 | buf.WriteString(fmt.Sprintf("%s,", util.GroupHeaderCode)) 132 | buf.WriteString(fmt.Sprintf("%s,", h.Receiver)) 133 | buf.WriteString(fmt.Sprintf("%s,", h.Originator)) 134 | buf.WriteString(fmt.Sprintf("%d,", h.GroupStatus)) 135 | buf.WriteString(fmt.Sprintf("%s,", h.AsOfDate)) 136 | buf.WriteString(fmt.Sprintf("%s,", h.AsOfTime)) 137 | buf.WriteString(fmt.Sprintf("%s,", h.CurrencyCode)) 138 | if h.AsOfDateModifier > 0 { 139 | buf.WriteString(fmt.Sprintf("%d/", h.AsOfDateModifier)) 140 | } else { 141 | buf.WriteString("/") 142 | } 143 | 144 | return buf.String() 145 | } 146 | -------------------------------------------------------------------------------- /pkg/lib/record_group_header_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func mockGroupHeader() *groupHeader { 14 | return &groupHeader{ 15 | Receiver: "0004", 16 | Originator: "12345", 17 | GroupStatus: 1, 18 | AsOfDate: "060321", 19 | AsOfTime: "0829", 20 | CurrencyCode: "USD", 21 | AsOfDateModifier: 2, 22 | } 23 | } 24 | 25 | func TestGroupHeader(t *testing.T) { 26 | 27 | record := mockGroupHeader() 28 | require.NoError(t, record.validate()) 29 | 30 | record.AsOfDateModifier = 5 31 | require.Error(t, record.validate()) 32 | require.Equal(t, "GroupHeader: invalid AsOfDateModifier", record.validate().Error()) 33 | 34 | record.CurrencyCode = "A" 35 | require.Error(t, record.validate()) 36 | require.Equal(t, "GroupHeader: invalid CurrencyCode", record.validate().Error()) 37 | 38 | record.AsOfTime = "AAA" 39 | require.Error(t, record.validate()) 40 | require.Equal(t, "GroupHeader: invalid AsOfTime", record.validate().Error()) 41 | 42 | record.AsOfDate = "" 43 | require.Error(t, record.validate()) 44 | require.Equal(t, "GroupHeader: invalid AsOfDate", record.validate().Error()) 45 | 46 | record.GroupStatus = 5 47 | require.Error(t, record.validate()) 48 | require.Equal(t, "GroupHeader: invalid GroupStatus", record.validate().Error()) 49 | 50 | record.Originator = "" 51 | require.Error(t, record.validate()) 52 | require.Equal(t, "GroupHeader: invalid Originator", record.validate().Error()) 53 | 54 | } 55 | 56 | func TestGroupHeaderWithOptional(t *testing.T) { 57 | 58 | sample := "02,12345,0004,1,060317,0000,CAD,2/" 59 | record := groupHeader{} 60 | 61 | size, err := record.parse(sample) 62 | require.NoError(t, err) 63 | require.Equal(t, 34, size) 64 | 65 | require.Equal(t, "0004", record.Originator) 66 | require.Equal(t, "12345", record.Receiver) 67 | require.Equal(t, "060317", record.AsOfDate) 68 | require.Equal(t, "0000", record.AsOfTime) 69 | require.Equal(t, "CAD", record.CurrencyCode) 70 | 71 | require.Equal(t, sample, record.string()) 72 | } 73 | 74 | func TestGroupHeaderWithoutOptional(t *testing.T) { 75 | 76 | sample := "02,,0004,1,060317,,,/" 77 | record := groupHeader{} 78 | 79 | size, err := record.parse(sample) 80 | require.NoError(t, err) 81 | require.Equal(t, 21, size) 82 | 83 | require.Equal(t, "0004", record.Originator) 84 | require.Equal(t, "060317", record.AsOfDate) 85 | 86 | require.Equal(t, sample, record.string()) 87 | } 88 | -------------------------------------------------------------------------------- /pkg/lib/record_group_trailer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | 11 | "github.com/moov-io/bai2/pkg/util" 12 | ) 13 | 14 | const ( 15 | gtParseErrorFmt = "GroupTrailer: unable to parse %s" 16 | gtValidateErrorFmt = "GroupTrailer: invalid %s" 17 | ) 18 | 19 | type groupTrailer struct { 20 | GroupControlTotal string 21 | NumberOfAccounts int64 22 | NumberOfRecords int64 23 | } 24 | 25 | func (h *groupTrailer) validate() error { 26 | if h.GroupControlTotal != "" && !util.ValidateAmount(h.GroupControlTotal) { 27 | return fmt.Errorf(fmt.Sprintf(gtValidateErrorFmt, "GroupControlTotal")) 28 | } 29 | 30 | return nil 31 | } 32 | 33 | func (h *groupTrailer) parse(data string) (int, error) { 34 | 35 | var line string 36 | var err error 37 | var size, read int 38 | 39 | if length := util.GetSize(data); length < 3 { 40 | return 0, fmt.Errorf(fmt.Sprintf(gtParseErrorFmt, "record")) 41 | } else { 42 | line = data[:length] 43 | } 44 | 45 | // RecordCode 46 | if util.GroupTrailerCode != data[:2] { 47 | return 0, fmt.Errorf(fmt.Sprintf(fhParseErrorFmt, "RecordCode")) 48 | } 49 | read += 3 50 | 51 | // GroupControlTotal 52 | if h.GroupControlTotal, size, err = util.ReadField(line, read); err != nil { 53 | return 0, fmt.Errorf(fmt.Sprintf(gtParseErrorFmt, "GroupControlTotal")) 54 | } else { 55 | read += size 56 | } 57 | 58 | // NumberOfAccounts 59 | if h.NumberOfAccounts, size, err = util.ReadFieldAsInt(line, read); err != nil { 60 | return 0, fmt.Errorf(fmt.Sprintf(gtParseErrorFmt, "NumberOfAccounts")) 61 | } else { 62 | read += size 63 | } 64 | 65 | // NumberOfRecords 66 | if h.NumberOfRecords, size, err = util.ReadFieldAsInt(line, read); err != nil { 67 | return 0, fmt.Errorf(fmt.Sprintf(gtParseErrorFmt, "NumberOfRecords")) 68 | } else { 69 | read += size 70 | } 71 | 72 | if err = h.validate(); err != nil { 73 | return 0, err 74 | } 75 | 76 | return read, nil 77 | } 78 | 79 | func (h *groupTrailer) string() string { 80 | var buf bytes.Buffer 81 | 82 | buf.WriteString(fmt.Sprintf("%s,", util.GroupTrailerCode)) 83 | buf.WriteString(fmt.Sprintf("%s,", h.GroupControlTotal)) 84 | buf.WriteString(fmt.Sprintf("%d,", h.NumberOfAccounts)) 85 | buf.WriteString(fmt.Sprintf("%d/", h.NumberOfRecords)) 86 | 87 | return buf.String() 88 | } 89 | -------------------------------------------------------------------------------- /pkg/lib/record_group_trailer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestGroupTrailer(t *testing.T) { 14 | 15 | record := groupTrailer{} 16 | require.NoError(t, record.validate()) 17 | 18 | } 19 | 20 | func TestGroupTrailerWithSample(t *testing.T) { 21 | 22 | sample := "98,+00000000001280000,2,25/" 23 | record := groupTrailer{} 24 | 25 | size, err := record.parse(sample) 26 | require.NoError(t, err) 27 | require.Equal(t, 27, size) 28 | 29 | require.Equal(t, "+00000000001280000", record.GroupControlTotal) 30 | require.Equal(t, int64(2), record.NumberOfAccounts) 31 | require.Equal(t, int64(25), record.NumberOfRecords) 32 | 33 | require.Equal(t, sample, record.string()) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/lib/record_transaction_detail.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "github.com/moov-io/bai2/pkg/util" 11 | "strings" 12 | ) 13 | 14 | const ( 15 | tdParseErrorFmt = "TransactionDetail: unable to parse %s" 16 | tdValidateErrorFmt = "TransactionDetail: invalid %s" 17 | ) 18 | 19 | type transactionDetail struct { 20 | TypeCode string 21 | Amount string 22 | FundsType FundsType 23 | BankReferenceNumber string 24 | CustomerReferenceNumber string 25 | Text string 26 | } 27 | 28 | func (r *transactionDetail) validate() error { 29 | if r.TypeCode != "" && !util.ValidateTypeCode(r.TypeCode) { 30 | return fmt.Errorf(fmt.Sprintf(tdValidateErrorFmt, "TypeCode")) 31 | } 32 | if r.Amount != "" && !util.ValidateAmount(r.Amount) { 33 | return fmt.Errorf(fmt.Sprintf(tdValidateErrorFmt, "Amount")) 34 | } 35 | if r.FundsType.Validate() != nil { 36 | return fmt.Errorf(fmt.Sprintf(tdValidateErrorFmt, "FundsType")) 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func (r *transactionDetail) parse(data string) (int, error) { 43 | 44 | var line string 45 | var err error 46 | var size, read int 47 | 48 | allow_slash_as_character := true 49 | length := util.GetSize(data, allow_slash_as_character) 50 | if length < 3 { 51 | return 0, fmt.Errorf(fmt.Sprintf(tdParseErrorFmt, "record")) 52 | } else { 53 | line = data[:length] 54 | } 55 | 56 | // RecordCode 57 | if util.TransactionDetailCode != data[:2] { 58 | return 0, fmt.Errorf(fmt.Sprintf(tdParseErrorFmt, "RecordCode")) 59 | } 60 | read += 3 61 | 62 | // TypeCode 63 | if r.TypeCode, size, err = util.ReadField(line, read); err != nil { 64 | return 0, fmt.Errorf(fmt.Sprintf(tdParseErrorFmt, "TypeCode")) 65 | } else { 66 | read += size 67 | } 68 | 69 | // Amount 70 | if r.Amount, size, err = util.ReadField(line, read); err != nil { 71 | return 0, fmt.Errorf(fmt.Sprintf(tdParseErrorFmt, "Amount")) 72 | } else { 73 | read += size 74 | } 75 | 76 | // FundsType 77 | if len(line) < read { 78 | return 0, fmt.Errorf(fmt.Sprintf(tdParseErrorFmt, "FundsType") + " too short") 79 | } 80 | if size, err = r.FundsType.parse(line[read:]); err != nil { 81 | return 0, fmt.Errorf(fmt.Sprintf(tdParseErrorFmt, "FundsType")) 82 | } else { 83 | read += size 84 | } 85 | 86 | // BankReferenceNumber 87 | if r.BankReferenceNumber, size, err = util.ReadField(line, read, allow_slash_as_character); err != nil { 88 | return 0, fmt.Errorf(fmt.Sprintf(tdParseErrorFmt, "BankReferenceNumber")) 89 | } else { 90 | read += size 91 | } 92 | 93 | // CustomerReferenceNumber 94 | if r.CustomerReferenceNumber, size, err = util.ReadField(line, read, allow_slash_as_character); err != nil { 95 | return 0, fmt.Errorf(fmt.Sprintf(tdParseErrorFmt, "CustomerReferenceNumber")) 96 | } else { 97 | read += size 98 | } 99 | 100 | // Text 101 | read_remainder_of_line := true 102 | if r.Text, size, err = util.ReadField(line, read, allow_slash_as_character, read_remainder_of_line); err != nil { 103 | return 0, fmt.Errorf(fmt.Sprintf(tdParseErrorFmt, "Text")) 104 | } else { 105 | read += size 106 | } 107 | 108 | if err = r.validate(); err != nil { 109 | return 0, err 110 | } 111 | 112 | return read, nil 113 | } 114 | 115 | func (r *transactionDetail) string(opts ...int64) string { 116 | 117 | var maxLen int64 118 | if len(opts) > 0 { 119 | maxLen = opts[0] 120 | } 121 | 122 | var total, buf bytes.Buffer 123 | 124 | buf.WriteString(fmt.Sprintf("%s,", util.TransactionDetailCode)) 125 | buf.WriteString(fmt.Sprintf("%s,", r.TypeCode)) 126 | buf.WriteString(fmt.Sprintf("%s,", r.Amount)) 127 | 128 | util.WriteBuffer(&total, &buf, r.FundsType.String(), maxLen) 129 | buf.WriteString(",") 130 | 131 | util.WriteBuffer(&total, &buf, r.BankReferenceNumber, maxLen) 132 | buf.WriteString(",") 133 | 134 | util.WriteBuffer(&total, &buf, r.CustomerReferenceNumber, maxLen) 135 | buf.WriteString(",") 136 | 137 | util.WriteBuffer(&total, &buf, r.Text, maxLen) 138 | if !strings.HasSuffix(r.Text, "/") { 139 | buf.WriteString("/") 140 | } 141 | 142 | total.WriteString(buf.String()) 143 | 144 | return total.String() 145 | } 146 | -------------------------------------------------------------------------------- /pkg/lib/record_transaction_detail_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 lib 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestTransactionDetail(t *testing.T) { 14 | 15 | record := transactionDetail{ 16 | TypeCode: "890", 17 | } 18 | require.NoError(t, record.validate()) 19 | 20 | record.TypeCode = "AAA" 21 | require.Error(t, record.validate()) 22 | require.Equal(t, "TransactionDetail: invalid TypeCode", record.validate().Error()) 23 | 24 | } 25 | 26 | func TestTransactionDetailWithSample(t *testing.T) { 27 | 28 | sample := "16,409,000000000002500,V,060316,,,,RETURNED CHEQUE /" 29 | record := transactionDetail{ 30 | TypeCode: "890", 31 | } 32 | 33 | size, err := record.parse(sample) 34 | require.NoError(t, err) 35 | require.Equal(t, 57, size) 36 | 37 | require.Equal(t, "409", record.TypeCode) 38 | require.Equal(t, "000000000002500", record.Amount) 39 | require.Equal(t, "V", string(record.FundsType.TypeCode)) 40 | require.Equal(t, "060316", record.FundsType.Date) 41 | require.Equal(t, "", record.FundsType.Time) 42 | require.Equal(t, "", record.BankReferenceNumber) 43 | require.Equal(t, "", record.CustomerReferenceNumber) 44 | require.Equal(t, "RETURNED CHEQUE /", record.Text) 45 | 46 | require.Equal(t, sample, record.string()) 47 | } 48 | 49 | func TestTransactionDetailOutputWithContinuationRecords(t *testing.T) { 50 | 51 | record := transactionDetail{ 52 | TypeCode: "409", 53 | Amount: "111111111111111", 54 | BankReferenceNumber: "222222222222222", 55 | CustomerReferenceNumber: "333333333333333", 56 | Text: "RETURNED CHEQUE 444444444444444", 57 | FundsType: FundsType{ 58 | TypeCode: FundsTypeD, 59 | DistributionNumber: 5, 60 | Distributions: []Distribution{ 61 | { 62 | Day: 1, 63 | Amount: 1000000000, 64 | }, 65 | { 66 | Day: 2, 67 | Amount: 2000000000, 68 | }, 69 | { 70 | Day: 3, 71 | Amount: 3000000000, 72 | }, 73 | { 74 | Day: 4, 75 | Amount: 4000000000, 76 | }, 77 | { 78 | Day: 5, 79 | Amount: 5000000000, 80 | }, 81 | { 82 | Day: 6, 83 | Amount: 6000000000, 84 | }, 85 | { 86 | Day: 7, 87 | Amount: 7000000000, 88 | }, 89 | }, 90 | }, 91 | } 92 | 93 | result := record.string() 94 | expectResult := `16,409,111111111111111,D,5,1,1000000000,2,2000000000,3,3000000000,4,4000000000,5,5000000000,6,6000000000,7,7000000000,222222222222222,333333333333333,RETURNED CHEQUE 444444444444444/` 95 | require.Equal(t, expectResult, result) 96 | require.Equal(t, len(expectResult), len(result)) 97 | 98 | result = record.string(80) 99 | expectResult = `16,409,111111111111111,D,5,1,1000000000,2,2000000000,3,3000000000,4,4000000000/ 100 | 88,5,5000000000,6,6000000000,7,7000000000,222222222222222,333333333333333/ 101 | 88,RETURNED CHEQUE 444444444444444/` 102 | require.Equal(t, expectResult, result) 103 | require.Equal(t, len(expectResult), len(result)) 104 | 105 | result = record.string(50) 106 | expectResult = `16,409,111111111111111,D,5,1,1000000000,2/ 107 | 88,2000000000,3,3000000000,4,4000000000,5/ 108 | 88,5000000000,6,6000000000,7,7000000000/ 109 | 88,222222222222222,333333333333333/ 110 | 88,RETURNED CHEQUE 444444444444444/` 111 | require.Equal(t, expectResult, result) 112 | require.Equal(t, len(expectResult), len(result)) 113 | 114 | } 115 | 116 | /** 117 | * Outlines the behavior of a Detail record when the Detail and Continuations for the detail are terminated 118 | * by a newline character ("\n") rather than a slash ("/"). 119 | * 120 | * Note: continuation parsing is implemented in `detail.go`, which is why this particular test doesn't parse 121 | * all of the continuation lines. 122 | */ 123 | func TestTransactionDetailOutput_ContinuationRecordWithNewlineDelimiter(t *testing.T) { 124 | data := `16,266,1912,,GI2118700002010,20210706MMQFMPU8000001,Outgoing Wire Return,- 125 | 88,CREF: 20210706MMQFMPU8000001 126 | 88,EREF: 20210706MMQFMPU8000001 127 | 88,DBIC: GSCRUS33 128 | 88,CRNM: ABC Company 129 | 88,DBNM: SAMPLE INC.` 130 | 131 | record := transactionDetail{} 132 | 133 | size, err := record.parse(data) 134 | require.NoError(t, err) 135 | 136 | require.Equal(t, "266", record.TypeCode) 137 | require.Equal(t, "1912", record.Amount) 138 | require.Equal(t, "", string(record.FundsType.TypeCode)) 139 | require.Equal(t, "", record.FundsType.Date) 140 | require.Equal(t, "", record.FundsType.Time) 141 | require.Equal(t, "GI2118700002010", record.BankReferenceNumber) 142 | require.Equal(t, "20210706MMQFMPU8000001", record.CustomerReferenceNumber) 143 | require.Equal(t, "Outgoing Wire Return,-", record.Text) 144 | require.Equal(t, 75, size) 145 | 146 | result := record.string() 147 | expectResult := `16,266,1912,,GI2118700002010,20210706MMQFMPU8000001,Outgoing Wire Return,-/` 148 | require.Equal(t, expectResult, result) 149 | } 150 | -------------------------------------------------------------------------------- /pkg/service/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 service_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/moov-io/bai2/pkg/service" 11 | "github.com/moov-io/base/config" 12 | "github.com/moov-io/base/log" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func Test_ConfigLoading(t *testing.T) { 17 | logger := log.NewNopLogger() 18 | 19 | ConfigService := config.NewService(logger) 20 | 21 | gc := &service.GlobalConfig{} 22 | err := ConfigService.Load(gc) 23 | require.Nil(t, err) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/service/environment.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 service 6 | 7 | import ( 8 | "github.com/gorilla/mux" 9 | "github.com/moov-io/base/config" 10 | "github.com/moov-io/base/log" 11 | "github.com/moov-io/base/stime" 12 | ) 13 | 14 | // Environment - Contains everything thats been instantiated for this service. 15 | type Environment struct { 16 | Logger log.Logger 17 | Config *Config 18 | TimeService *stime.TimeService 19 | PublicRouter *mux.Router 20 | Shutdown func() 21 | } 22 | 23 | // NewEnvironment - Generates a new default environment. Overrides can be specified via configs. 24 | func NewEnvironment(env *Environment) (*Environment, error) { 25 | if env.Logger == nil { 26 | env.Logger = log.NewDefaultLogger() 27 | } 28 | 29 | if env.Config == nil { 30 | ConfigService := config.NewService(env.Logger) 31 | 32 | global := &GlobalConfig{} 33 | if err := ConfigService.Load(global); err != nil { 34 | return nil, err 35 | } 36 | 37 | env.Config = &global.Bai2 38 | } 39 | 40 | if env.TimeService == nil { 41 | t := stime.NewSystemTimeService() 42 | env.TimeService = &t 43 | } 44 | 45 | // router 46 | if env.PublicRouter == nil { 47 | env.PublicRouter = mux.NewRouter() 48 | } 49 | 50 | // configure custom handlers 51 | ConfigureHandlers(env.PublicRouter) 52 | 53 | env.Shutdown = func() {} 54 | 55 | return env, nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/service/environment_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 service_test 6 | 7 | import ( 8 | "os" 9 | "testing" 10 | 11 | "github.com/go-kit/log" 12 | baseLog "github.com/moov-io/base/log" 13 | 14 | "github.com/moov-io/bai2/pkg/service" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func Test_Environment_Startup(t *testing.T) { 19 | a := assert.New(t) 20 | 21 | env := &service.Environment{ 22 | Logger: baseLog.NewLogger(log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr))), 23 | } 24 | 25 | env, err := service.NewEnvironment(env) 26 | a.Nil(err) 27 | 28 | shutdown := env.RunServers(false) 29 | 30 | env1, err := service.NewEnvironment(&service.Environment{}) 31 | a.Nil(err) 32 | env1.Shutdown() 33 | 34 | t.Cleanup(shutdown) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/service/handlers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 service 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "io" 11 | "net/http" 12 | 13 | "github.com/gorilla/mux" 14 | "github.com/moov-io/bai2/pkg/lib" 15 | ) 16 | 17 | func outputError(w http.ResponseWriter, code int, err error) { 18 | w.WriteHeader(code) 19 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 20 | json.NewEncoder(w).Encode(map[string]interface{}{ 21 | "error": err.Error(), 22 | }) 23 | } 24 | 25 | func outputSuccess(w http.ResponseWriter, output string) { 26 | w.WriteHeader(http.StatusOK) 27 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 28 | json.NewEncoder(w).Encode(map[string]interface{}{ 29 | "status": output, 30 | }) 31 | } 32 | 33 | func parseInputFromRequest(r *http.Request) (*lib.Bai2, error) { 34 | inputFile, _, err := r.FormFile("input") 35 | if err != nil { 36 | return nil, err 37 | } 38 | defer inputFile.Close() 39 | 40 | var input bytes.Buffer 41 | if _, err = io.Copy(&input, inputFile); err != nil { 42 | return nil, err 43 | } 44 | 45 | // convert byte slice to io.Reader 46 | scan := lib.NewBai2Scanner(bytes.NewReader(input.Bytes())) 47 | f := lib.NewBai2() 48 | 49 | err = f.Read(&scan) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return f, nil 55 | } 56 | 57 | func outputBufferToWriter(w http.ResponseWriter, f *lib.Bai2) { 58 | w.WriteHeader(http.StatusOK) 59 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 60 | w.Write([]byte(f.String())) 61 | } 62 | 63 | func outputJsonBufferToWriter(w http.ResponseWriter, f *lib.Bai2) { 64 | w.WriteHeader(http.StatusOK) 65 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 66 | json.NewEncoder(w).Encode(f) 67 | } 68 | 69 | // parse - parse bai2 report 70 | func parse(w http.ResponseWriter, r *http.Request) { 71 | f, err := parseInputFromRequest(r) 72 | if err != nil { 73 | outputError(w, http.StatusBadRequest, err) 74 | return 75 | } 76 | 77 | err = f.Validate() 78 | if err != nil { 79 | outputError(w, http.StatusNotImplemented, err) 80 | return 81 | } 82 | 83 | outputSuccess(w, "valid file") 84 | } 85 | 86 | // print - print bai2 report after parse 87 | func print(w http.ResponseWriter, r *http.Request) { 88 | f, err := parseInputFromRequest(r) 89 | if err != nil { 90 | outputError(w, http.StatusBadRequest, err) 91 | return 92 | } 93 | 94 | err = f.Validate() 95 | if err != nil { 96 | outputError(w, http.StatusNotImplemented, err) 97 | return 98 | } 99 | 100 | outputBufferToWriter(w, f) 101 | } 102 | 103 | // format - format bai2 report after parse 104 | func format(w http.ResponseWriter, r *http.Request) { 105 | f, err := parseInputFromRequest(r) 106 | if err != nil { 107 | outputError(w, http.StatusBadRequest, err) 108 | return 109 | } 110 | 111 | err = f.Validate() 112 | if err != nil { 113 | outputError(w, http.StatusNotImplemented, err) 114 | return 115 | } 116 | 117 | outputJsonBufferToWriter(w, f) 118 | } 119 | 120 | // health - health check 121 | func health(w http.ResponseWriter, r *http.Request) { 122 | outputSuccess(w, "alive") 123 | } 124 | 125 | // configure handlers 126 | func ConfigureHandlers(r *mux.Router) error { 127 | 128 | r.HandleFunc("/health", health).Methods("GET") 129 | r.HandleFunc("/print", print).Methods("POST") 130 | r.HandleFunc("/parse", parse).Methods("POST") 131 | r.HandleFunc("/format", format).Methods("POST") 132 | 133 | return nil 134 | } 135 | -------------------------------------------------------------------------------- /pkg/service/model_config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 service 6 | 7 | type GlobalConfig struct { 8 | Bai2 Config 9 | } 10 | 11 | // Config defines all the configuration for the app 12 | type Config struct { 13 | Servers ServerConfig 14 | } 15 | 16 | // ServerConfig - Groups all the http configs for the servers and ports that get opened. 17 | type ServerConfig struct { 18 | Public HTTPConfig 19 | Admin HTTPConfig 20 | } 21 | 22 | // HTTPConfig configuration for running an http server 23 | type HTTPConfig struct { 24 | Bind BindAddress 25 | } 26 | 27 | // BindAddress specifies where the http server should bind to. 28 | type BindAddress struct { 29 | Address string 30 | } 31 | -------------------------------------------------------------------------------- /pkg/service/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 service 6 | 7 | import ( 8 | "context" 9 | "crypto/tls" 10 | "fmt" 11 | "net/http" 12 | "os" 13 | "os/signal" 14 | "syscall" 15 | "time" 16 | 17 | "github.com/gorilla/mux" 18 | "github.com/moov-io/base/admin" 19 | "github.com/moov-io/base/log" 20 | ) 21 | 22 | // RunServers - Boots up all the servers and awaits till they are stopped. 23 | func (env *Environment) RunServers(await bool) func() { 24 | 25 | // Listen for application termination. 26 | terminationListener := newTerminationListener() 27 | 28 | adminServer := bootAdminServer(terminationListener, env.Logger, env.Config.Servers.Admin) 29 | 30 | _, shutdownPublicServer := bootHTTPServer("public", env.PublicRouter, terminationListener, env.Logger, env.Config.Servers.Public) 31 | 32 | if await { 33 | awaitTermination(env.Logger, terminationListener) 34 | } 35 | 36 | return func() { 37 | adminServer.Shutdown() 38 | shutdownPublicServer() 39 | } 40 | } 41 | 42 | func newTerminationListener() chan error { 43 | errs := make(chan error) 44 | go func() { 45 | c := make(chan os.Signal, 1) 46 | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) 47 | errs <- fmt.Errorf("%s", <-c) 48 | }() 49 | 50 | return errs 51 | } 52 | 53 | func awaitTermination(logger log.Logger, terminationListener chan error) { 54 | if err := <-terminationListener; err != nil { 55 | logger.Fatal().LogError(err).Err() 56 | } 57 | } 58 | 59 | func bootHTTPServer(name string, routes *mux.Router, errs chan<- error, logger log.Logger, config HTTPConfig) (*http.Server, func()) { 60 | 61 | // Create main HTTP server 62 | serve := &http.Server{ 63 | Addr: config.Bind.Address, 64 | Handler: routes, 65 | TLSConfig: &tls.Config{ 66 | InsecureSkipVerify: false, 67 | PreferServerCipherSuites: true, 68 | MinVersion: tls.VersionTLS12, 69 | }, 70 | ReadTimeout: 30 * time.Second, 71 | ReadHeaderTimeout: 30 * time.Second, 72 | WriteTimeout: 30 * time.Second, 73 | IdleTimeout: 60 * time.Second, 74 | } 75 | 76 | // Start main HTTP server 77 | go func() { 78 | logger.Info().Log(fmt.Sprintf("%s listening on %s", name, config.Bind.Address)) 79 | if err := serve.ListenAndServe(); err != nil { 80 | errs <- logger.Fatal().LogErrorf("problem starting http: %w", err).Err() 81 | } 82 | }() 83 | 84 | shutdownServer := func() { 85 | if err := serve.Shutdown(context.TODO()); err != nil { 86 | logger.Fatal().LogError(err).Err() 87 | } 88 | } 89 | 90 | return serve, shutdownServer 91 | } 92 | 93 | func bootAdminServer(errs chan<- error, logger log.Logger, config HTTPConfig) *admin.Server { 94 | adminServer, err := admin.New(admin.Opts{ 95 | Addr: config.Bind.Address, 96 | }) 97 | if err != nil { 98 | errs <- logger.Fatal().LogErrorf("problem creating admin server: %v", err).Err() 99 | } 100 | 101 | go func() { 102 | logger.Info().Log(fmt.Sprintf("listening on %s", adminServer.BindAddr())) 103 | if err := adminServer.Listen(); err != nil { 104 | errs <- logger.Fatal().LogErrorf("problem starting admin http: %w", err).Err() 105 | } 106 | }() 107 | 108 | return adminServer 109 | } 110 | -------------------------------------------------------------------------------- /pkg/util/const.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 | const ( 8 | FileHeaderCode = "01" 9 | GroupHeaderCode = "02" 10 | AccountIdentifierCode = "03" 11 | TransactionDetailCode = "16" 12 | ContinuationCode = "88" 13 | AccountTrailerCode = "49" 14 | GroupTrailerCode = "98" 15 | FileTrailerCode = "99" 16 | ) 17 | -------------------------------------------------------------------------------- /pkg/util/parse_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | type input struct { 14 | Data string 15 | Start int 16 | } 17 | 18 | type want struct { 19 | Error bool 20 | ErrorMsg string 21 | Read int 22 | Value string 23 | IntValue int64 24 | } 25 | 26 | type testSample struct { 27 | Input input 28 | Want want 29 | } 30 | 31 | func TestReadField(t *testing.T) { 32 | 33 | samples := []testSample{ 34 | { 35 | Input: input{ 36 | Data: ",", 37 | Start: 0, 38 | }, 39 | Want: want{ 40 | Error: false, 41 | Read: 1, 42 | Value: "", 43 | }, 44 | }, 45 | { 46 | Input: input{ 47 | Data: "/", 48 | Start: 0, 49 | }, 50 | Want: want{ 51 | Error: false, 52 | Read: 1, 53 | Value: "", 54 | }, 55 | }, 56 | { 57 | Input: input{ 58 | Data: "01,", 59 | Start: 0, 60 | }, 61 | Want: want{ 62 | Error: false, 63 | Read: 3, 64 | Value: "01", 65 | }, 66 | }, 67 | { 68 | Input: input{ 69 | Data: "01/", 70 | Start: 0, 71 | }, 72 | Want: want{ 73 | Error: false, 74 | Read: 3, 75 | Value: "01", 76 | }, 77 | }, 78 | { 79 | Input: input{ 80 | Data: "ODFI’,", 81 | Start: 0, 82 | }, 83 | Want: want{ 84 | Error: false, 85 | Read: 8, 86 | Value: "ODFI’", 87 | }, 88 | }, 89 | { 90 | Input: input{ 91 | Data: "ODFI’,", 92 | Start: 6, 93 | }, 94 | Want: want{ 95 | Error: false, 96 | Read: 2, 97 | Value: "\x99", 98 | }, 99 | }, 100 | { 101 | Input: input{ 102 | Data: "ODFI’,", 103 | Start: 7, 104 | }, 105 | Want: want{ 106 | Error: false, 107 | Read: 1, 108 | Value: "", 109 | }, 110 | }, 111 | { 112 | Input: input{ 113 | Data: "ODFI’,", 114 | Start: 8, 115 | }, 116 | Want: want{ 117 | Error: true, 118 | ErrorMsg: "doesn't enough input string", 119 | Read: 0, 120 | Value: "", 121 | }, 122 | }, 123 | { 124 | Input: input{ 125 | Data: "ODFI’,", 126 | Start: 10, 127 | }, 128 | Want: want{ 129 | Error: true, 130 | ErrorMsg: "doesn't enough input string", 131 | Read: 0, 132 | Value: "", 133 | }, 134 | }, 135 | } 136 | 137 | for _, sample := range samples { 138 | value, size, err := ReadField(sample.Input.Data, sample.Input.Start) 139 | if !sample.Want.Error { 140 | require.NoError(t, err) 141 | } else { 142 | 143 | require.Error(t, err) 144 | require.Equal(t, sample.Want.ErrorMsg, err.Error()) 145 | } 146 | require.Equal(t, sample.Want.Read, size) 147 | require.Equal(t, sample.Want.Value, value) 148 | } 149 | } 150 | 151 | func TestReadFieldAsInt(t *testing.T) { 152 | 153 | samples := []testSample{ 154 | { 155 | Input: input{ 156 | Data: "11,", 157 | Start: 0, 158 | }, 159 | Want: want{ 160 | Error: false, 161 | Read: 3, 162 | IntValue: 11, 163 | }, 164 | }, 165 | { 166 | Input: input{ 167 | Data: "01/", 168 | Start: 0, 169 | }, 170 | Want: want{ 171 | Error: false, 172 | Read: 3, 173 | IntValue: 1, 174 | }, 175 | }, 176 | { 177 | Input: input{ 178 | Data: "ODFI’,", 179 | Start: 0, 180 | }, 181 | Want: want{ 182 | Error: true, 183 | ErrorMsg: "doesn't have valid value", 184 | Read: 0, 185 | IntValue: 0, 186 | }, 187 | }, 188 | { 189 | Input: input{ 190 | Data: "ODFI’,", 191 | Start: 6, 192 | }, 193 | Want: want{ 194 | Error: true, 195 | ErrorMsg: "doesn't have valid value", 196 | Read: 0, 197 | IntValue: 0, 198 | }, 199 | }, 200 | { 201 | Input: input{ 202 | Data: "ODFI’,", 203 | Start: 7, 204 | }, 205 | Want: want{ 206 | Error: false, 207 | Read: 1, 208 | IntValue: 0, 209 | }, 210 | }, 211 | { 212 | Input: input{ 213 | Data: "ODFI’,", 214 | Start: 8, 215 | }, 216 | Want: want{ 217 | Error: true, 218 | ErrorMsg: "doesn't enough input string", 219 | Read: 0, 220 | IntValue: 0, 221 | }, 222 | }, 223 | { 224 | Input: input{ 225 | Data: "ODFI’,", 226 | Start: 10, 227 | }, 228 | Want: want{ 229 | Error: true, 230 | ErrorMsg: "doesn't enough input string", 231 | Read: 0, 232 | IntValue: 0, 233 | }, 234 | }, 235 | { 236 | Input: input{ 237 | Data: "/", 238 | Start: 0, 239 | }, 240 | Want: want{ 241 | Error: false, 242 | Read: 1, 243 | IntValue: 0, 244 | }, 245 | }, 246 | { 247 | Input: input{ 248 | Data: ",", 249 | Start: 0, 250 | }, 251 | Want: want{ 252 | Error: false, 253 | Read: 1, 254 | IntValue: 0, 255 | }, 256 | }, 257 | } 258 | 259 | for _, sample := range samples { 260 | value, size, err := ReadFieldAsInt(sample.Input.Data, sample.Input.Start) 261 | if !sample.Want.Error { 262 | require.NoError(t, err) 263 | } else { 264 | 265 | require.Error(t, err) 266 | require.Equal(t, sample.Want.ErrorMsg, err.Error()) 267 | } 268 | require.Equal(t, sample.Want.Read, size) 269 | require.Equal(t, sample.Want.IntValue, value) 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /pkg/util/parser.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 | "fmt" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | func getIndex(input string, opts ...bool) int { 14 | index_comma := strings.Index(input, ",") 15 | index_slash := strings.Index(input, "/") 16 | index_newline := strings.Index(input, "\n") 17 | 18 | // NB. `opts[0]`: if true, slash character is allowed in text and does not signify a line termination. this is the 19 | // case for Transaction Detail and Continuation records. 20 | // `opts[1]`: if true, returns the index of the first newline, or the full length of the input if no newline exists. 21 | allow_slash_as_character := len(opts) > 0 && opts[0] 22 | read_remainder_of_line := len(opts) > 1 && opts[1] 23 | 24 | if read_remainder_of_line { 25 | if index_newline != -1 { 26 | return index_newline 27 | } 28 | return len(input) 29 | } 30 | 31 | // If there is no `,` separator in the input, return either the index of the next explicit terminating character (`/`) 32 | // or the index of the next newline character, if no terminating character is present. 33 | // 34 | // If slash is allowed as a non-terminating character, only newlines are respected here. 35 | if index_comma == -1 { 36 | if !allow_slash_as_character && index_slash != -1 { 37 | return index_slash 38 | } 39 | if index_newline != -1 { 40 | return index_newline 41 | } 42 | return len(input) 43 | } 44 | 45 | // If a line is terminated with a `/` character and the terminator is BEFORE the next `,` character, return 46 | // the index of the `/` character. 47 | // 48 | // If slash is allowed as a non-terminating character, this check is skipped. 49 | if !allow_slash_as_character && index_slash > -1 && index_slash < index_comma { 50 | return index_slash 51 | } 52 | 53 | // If a line is terminated with a `\n` character (and is NOT terminated with a `/` character, or if `/` is an allowed character) 54 | // and the `\n` is BEFORE the next `,` character, return the index of the `\n` character. 55 | if (index_slash < 0 || allow_slash_as_character) && index_newline > -1 && index_newline < index_comma { 56 | return index_newline 57 | } 58 | 59 | // Otherwise, return the index of the next `,` character. Value will not be `-1` due to earlier function logic. 60 | return index_comma 61 | } 62 | 63 | func ReadField(input string, start int, opts ...bool) (string, int, error) { 64 | 65 | data := "" 66 | 67 | if start < len(input) { 68 | data = input[start:] 69 | } 70 | 71 | if data == "" { 72 | return "", 0, fmt.Errorf("doesn't enough input string") 73 | } 74 | 75 | idx := getIndex(data, opts...) 76 | if idx == -1 { 77 | return "", 0, fmt.Errorf("doesn't have valid delimiter") 78 | } 79 | 80 | return data[:idx], idx + 1, nil 81 | } 82 | 83 | func ReadFieldAsInt(input string, start int) (int64, int, error) { 84 | 85 | data := "" 86 | 87 | if start < len(input) { 88 | data = input[start:] 89 | } 90 | 91 | if data == "" { 92 | return 0, 0, fmt.Errorf("doesn't enough input string") 93 | } 94 | 95 | idx := getIndex(data) 96 | if idx == -1 { 97 | return 0, 0, fmt.Errorf("doesn't have valid delimiter") 98 | } 99 | 100 | if data[:idx] == "" { 101 | return 0, 1, nil 102 | } 103 | 104 | value, err := strconv.ParseInt(data[:idx], 10, 64) 105 | if err != nil { 106 | return 0, 0, fmt.Errorf("doesn't have valid value") 107 | } 108 | 109 | return value, idx + 1, nil 110 | } 111 | 112 | func GetSize(line string, opts ...bool) int64 { 113 | allow_slash_as_character := len(opts) > 0 && opts[0] 114 | read_remainder_of_line := len(opts) > 1 && opts[1] 115 | if read_remainder_of_line { 116 | return int64(len(line)) 117 | } 118 | 119 | size := strings.Index(line, "/") 120 | if !allow_slash_as_character && size >= 0 { 121 | return int64(size + 1) 122 | } 123 | 124 | size = strings.Index(line, "\n") 125 | if size >= 0 { 126 | return int64(size + 1) 127 | } 128 | 129 | return int64(len(line)) 130 | } 131 | -------------------------------------------------------------------------------- /pkg/util/validate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 "regexp" 8 | 9 | var dateYYMMDDTypeRegex = regexp.MustCompile(`[0-9][0-9](0[1-9]|1[0-2])(0[1-9]|1[0-9]|2[0-9]|3[01])`) 10 | var timeTypeRegex = regexp.MustCompile(`[0-9][0-9][0-9][0-9]`) 11 | var singedNumber = regexp.MustCompile(`^(-|\+|)?[0-9]\d*$`) 12 | var currencyCodeRegex = regexp.MustCompile(`^[a-zA-Z]{3}$`) 13 | var typeCodeRegex = regexp.MustCompile(`^[0-9]{3}$`) 14 | 15 | func ValidateDate(input string) bool { 16 | return dateYYMMDDTypeRegex.MatchString(input) 17 | } 18 | 19 | func ValidateTime(input string) bool { 20 | return timeTypeRegex.MatchString(input) 21 | } 22 | 23 | func ValidateFundsType(input string) bool { 24 | if input == "0" || input == "1" || input == "2" || input == "Z" || input == "V" || 25 | input == "S" || input == "D" { 26 | return true 27 | } 28 | return false 29 | } 30 | 31 | func ValidateAmount(input string) bool { 32 | return singedNumber.MatchString(input) 33 | } 34 | 35 | func ValidateCurrencyCode(input string) bool { 36 | return currencyCodeRegex.MatchString(input) 37 | } 38 | 39 | func ValidateTypeCode(input string) bool { 40 | return typeCodeRegex.MatchString(input) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/util/write.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 | "bytes" 9 | "fmt" 10 | "strings" 11 | ) 12 | 13 | // WriteBuffer 14 | // 15 | // Input type (ELM1,EML2,ELM3..,ELMN) 16 | func WriteBuffer(total, buf *bytes.Buffer, input string, maxLen int64) { 17 | 18 | if maxLen > 0 { 19 | 20 | elements := strings.Split(input, ",") 21 | newInput := "" 22 | 23 | for _, elm := range elements { 24 | 25 | newSize := int64(buf.Len() + len(newInput) + len(elm) + 2) 26 | if newSize > maxLen { 27 | if newInput == "" { 28 | org := buf.String() 29 | org = org[:len(org)-1] + "/" + "\n" 30 | total.WriteString(org) 31 | } else { 32 | buf.WriteString(newInput + "/" + "\n") // added new line 33 | total.WriteString(buf.String()) 34 | } 35 | 36 | // refresh buf 37 | buf.Reset() 38 | 39 | buf.WriteString(fmt.Sprintf("%s,", ContinuationCode)) 40 | newInput = elm 41 | } else { 42 | if newInput == "" { 43 | newInput = elm 44 | } else { 45 | newInput = newInput + "," + elm 46 | } 47 | } 48 | } 49 | 50 | if len(newInput) > 0 { 51 | buf.WriteString(newInput) 52 | } 53 | 54 | } else { 55 | buf.WriteString(input) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/fuzz/fuzz_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 fuzz 6 | 7 | import ( 8 | "io/fs" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "testing" 13 | 14 | "github.com/moov-io/bai2/pkg/lib" 15 | 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func FuzzReaderWriter_ValidFiles(f *testing.F) { 20 | populateCorpus(f, false) 21 | 22 | f.Fuzz(func(t *testing.T, contents string) { 23 | scan := lib.NewBai2Scanner(strings.NewReader(contents)) 24 | file := lib.NewBai2() 25 | 26 | require.NotPanics(t, func() { file.Read(&scan) }) 27 | require.NotPanics(t, func() { file.Validate() }) 28 | 29 | out := file.String() 30 | require.Greater(t, len(out), 0) 31 | }) 32 | } 33 | 34 | func FuzzReaderWriter_ErrorFiles(f *testing.F) { 35 | populateCorpus(f, true) 36 | 37 | f.Fuzz(func(t *testing.T, contents string) { 38 | scan := lib.NewBai2Scanner(strings.NewReader(contents)) 39 | file := lib.NewBai2() 40 | 41 | require.NotPanics(t, func() { file.Read(&scan) }) 42 | require.NotPanics(t, func() { file.Validate() }) 43 | 44 | out := file.String() 45 | require.Greater(t, len(out), 0) 46 | }) 47 | } 48 | 49 | func populateCorpus(f *testing.F, errorFiles bool) { 50 | f.Helper() 51 | 52 | err := filepath.Walk(filepath.Join("..", "testdata"), func(path string, info fs.FileInfo, _ error) error { 53 | path = filepath.ToSlash(path) 54 | 55 | // Skip directories and some files 56 | if info.IsDir() { 57 | return nil 58 | } 59 | if strings.HasSuffix(path, ".output") { 60 | return nil // skip 61 | } 62 | if errorFiles && !strings.Contains(path, "errors/") { 63 | f.Logf("skipping %s", path) 64 | return nil 65 | } 66 | if !errorFiles && strings.Contains(path, "errors/") { 67 | f.Logf("skipping %s", path) 68 | return nil 69 | } 70 | 71 | f.Logf("adding %s", path) 72 | 73 | bs, err := os.ReadFile(path) 74 | if err != nil { 75 | f.Fatal(err) 76 | } 77 | f.Add(string(bs)) 78 | return nil 79 | }) 80 | if err != nil { 81 | f.Fatal(err) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /test/fuzz/testdata/fuzz/FuzzReaderWriter_ValidFiles/3940e35d8f932097: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("\xfa") 3 | -------------------------------------------------------------------------------- /test/fuzz/testdata/fuzz/FuzzReaderWriter_ValidFiles/771e938e4458e983: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("0") 3 | -------------------------------------------------------------------------------- /test/fuzz/testdata/fuzz/FuzzReaderWriter_ValidFiles/e262a7798c82c66e: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("020,0,,000110,,,\n03,0,\n16,,,,,,\n03,\n88,0") 3 | -------------------------------------------------------------------------------- /test/fuzz/testdata/fuzz/FuzzReaderWriter_ValidFiles/f96b9eec61a21275: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("020,0,,000101,,,\n03,0,\n16,,0") 3 | -------------------------------------------------------------------------------- /test/testdata/errors/sample-parseError.txt: -------------------------------------------------------------------------------- 1 | 00,0004,1/2/345,06/0321,0829,001,80,1,2/ 2 | -------------------------------------------------------------------------------- /test/testdata/sample1.txt: -------------------------------------------------------------------------------- 1 | 01,0004,12345,060321,0829,001,80,1,2/ 2 | 02,12345,0004,1,060317,,CAD,/ 3 | 03,10200123456,CAD,040,+000000000000,,,045,+000000000000,,/ 4 | 88,100,000000000208500,00003,V,060316,,400,000000000208500,00008,V,060316,/ 5 | 16,409,000000000002500,V,060316,,,,RETURNED CHEQUE / 6 | 16,409,000000000090000,V,060316,,,,RTN-UNKNOWN / 7 | 16,409,000000000000500,V,060316,,,,RTD CHQ SERVICE CHRG/ 8 | 16,108,000000000203500,V,060316,,,,TFR 1020 0345678 / 9 | 16,108,000000000002500,V,060316,,,,MACLEOD MALL / 10 | 16,108,000000000002500,V,060316,,,,MASCOUCHE QUE / 11 | 16,409,000000000020000,V,060316,,,,1000 ISLANDS MALL / 12 | 16,409,000000000090000,V,060316,,,,PENHORA MALL / 13 | 16,409,000000000002000,V,060316,,,,CAPILANO MALL / 14 | 16,409,000000000002500,V,060316,,,,GALERIES LA CAPITALE/ 15 | 16,409,000000000001000,V,060316,,,,PLAZA ROCK FOREST / 16 | 49,+00000000000834000,14/ 17 | 03,10200123456,CAD,040,+000000000000,,,045,+000000000000,,/ 18 | 88,100,000000000111500,00002,V,060317,,400,000000000111500,00004,V,060317,/ 19 | 16,108,000000000011500,V,060317,,,,TFR 1020 0345678 / 20 | 16,108,000000000100000,V,060317,,,,MONTREAL / 21 | 16,409,000000000100000,V,060317,,,,GRANDFALL NB / 22 | 16,409,000000000009000,V,060317,,,,HAMILTON ON / 23 | 16,409,000000000002000,V,060317,,,,WOODSTOCK NB / 24 | 16,409,000000000000500,V,060317,,,,GALERIES RICHELIEU / 25 | 49,+00000000000446000,9/ 26 | 98,+00000000001280000,2,25/ 27 | 99,+00000000001280000,1,27/ -------------------------------------------------------------------------------- /test/testdata/sample2.txt: -------------------------------------------------------------------------------- 1 | 01,122099999,123456789,040621,0200,1,65,,2/ 2 | 02,031001234,122099999,1,040620,2359,,2/ 3 | 03,0123456789,,010,+4350000,,,040,2830000,,/ 4 | 88,072,1020000,,,074,500000,,/ 5 | 16,115,450000,S,100000,200000,150000,,,/ 6 | 49,9150000,4/ 7 | 03,9876543210,,010,-500000,,,100,1000000,,,400,2000000,,,190/ 8 | 88,500000,,,110,1000000,,,072,500000,,,074,500000,,,040/ 9 | 88,-1500000,,/ 10 | 16,115,500000,S,,200000,300000,,,LOCK BOX NO.68751/ 11 | 49,4000000,5/ 12 | 98,13150000,2,11/ 13 | 02,053003456,122099999,1,040620,2359,,2/ 14 | 03,4589761203,,010,10000000,,,040,5000000,,,074,4000000,,/ 15 | 88,400,50000000,,,100,60000000,,,110,20000000,,,072,1000000,,/ 16 | 16,218,20000000,V,040622,,SP4738,YRC065321/ 17 | 88,PROCEEDS OF LETTER OF CREDIT FROM THE ARAMCO OIL CO/ 18 | 16,195,10000000,1,,,/ 19 | 49,180000000,6/ 20 | 98,180000000,1,8/ 21 | 02,071207890,122099999,1,040620,2359,,2/ 22 | 03,0975312468,,010,500000,,,190,70000000,4,0,110/ 23 | 88,70000000,15,D,3,0,20000000,1,30000000,3,20000000/ 24 | 49,140500000,3/ 25 | 98,140500000,1,5/ 26 | 02,071207890,122099999,3,040620,2359,,2/ 27 | 03,7890654321,,010,800000,,,040,6000000,,,110,5000000/ 28 | 88,4,/ 29 | 49,11800000,3/ 30 | 98,11800000,1,5/ 31 | 99,345450000,4,31/ -------------------------------------------------------------------------------- /test/testdata/sample3.txt: -------------------------------------------------------------------------------- 1 | 01,103100195,103100195,220919,2112,4,,,2/ 02,103100195,103100195,1,220919,2400,USD,2/ 2 | 03,1111111,,010,-3500,,,015,-3600,,,040,-3500,,,060,-3600,,,100,3100,3,,400,3200,3,/ 3 | 16,142,2500,Z,,,TRANSFER PAYPAL PPD/ 16,142,500,Z,,,TRANSFER MSPBNA BANK PPD/ 4 | 16,142,100,Z,,,Ext Trnsfr JPMorgan Chase PPD/ 5 | 16,451,2500,Z,,,111111 ACH_SETL 1111111111 111111111111111 / 6 | 88, 1111111111/ 7 | 16,451,600,Z,,,111111 ACH_SETL 1111111111 111111111111111 / 8 | 88, 1111111111/ 9 | 16,451,100,Z,,,Ext Trnsfr JPMorgan Chase 1111111111 111111111111111 / 10 | 88, 11111111111/ 49,-1600,11/ 03,1111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,/ 11 | 49,00,2/ 03,1111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,/ 49,00,2/ 12 | 03,1111111,,010,-55703,,,015,-86406,,,040,-55703,,,060,-86406,,,100,00,0,,400,30703,2,/ 13 | 16,451,27500,Z,,,Visa Billing FTSRE Sept 22/ 16,451,3203,Z,,,Visa Billing Sept 22/ 49,-222812,4/ 14 | 03,1111111,,010,98547,,,015,98547,,,040,98547,,,060,98547,,,100,00,0,,400,00,0,/ 49,394188,2/ 15 | 03,1111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,/ 49,00,2/ 16 | 03,1111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,/ 49,00,2/ 17 | 03,11111111,,010,625266,,,015,610315,,,040,625266,,,060,610315,,,100,3700,3,,400,18651,3,/ 18 | 16,142,2500,Z,,,111111 ACH_SETL 1111111111 19 | 111111111111111 / 20 | 88, 1111111111/ 21 | 16,142,600,Z,,,111111 ACH_SETL 1111111111 111111111111111 / 22 | 88, 1111111111/ 23 | 16,142,600,Z,,,111111 VSN_SETL 1111111111 111111111111111 / 24 | 88, 1111111111/ 25 | 16,451,11521,Z,,,111111 BII_SETL 1111111111 111111111111111 / 26 | 88, 1111111111/ 27 | 16,451,4783,Z,,,111111 BII_SETL 1111111111 111111111111111 / 28 | 88, 1111111111/ 29 | 16,451,2347,Z,,,111111 BII_SETL 1111111111 111111111111111 / 30 | 88, 1111111111/ 31 | 49,2515864,14/ 32 | 03,11111111,,010,-73135,,,015,-78759,,,040,-73135,,,060,-78759,,,100,320,2,,400,5944,2,/ 33 | 16,142,215,Z,,,1111111111 CHIME HUBBLE PPD/ 34 | 16,142,105,Z,,,1111111111 CHIME HUBBLE PPD/ 35 | 16,451,5857,Z,,,Visa Billing Sept 22/ 36 | 16,451,87,Z,,,1111111111 CHIME HUBBLE PPD/ 37 | 49,-291260,6/ 38 | 03,11111111,,010,-4078,,,015,27595,,,040,-4078,,,060,27595,,,100,32593,5,,400,920,3,/ 39 | 16,195,13855,Z,,,Wire Transfer Credit VISA INTERNATIONAL 900 METRO CENTER BLVD / 40 | 88, FOSTER CITY CA 94404/ 41 | 16,142,11521,Z,,,111111 BII_SETL 1111111111 111111111111111 / 42 | 88, 1111111111/ 43 | 16,142,4783,Z,,,111111 BII_SETL 1111111111 111111111111111 / 44 | 88, 1111111111/ 45 | 16,142,2347,Z,,,111111 BII_SETL 1111111111 111111111111111 / 46 | 88, 1111111111/ 47 | 16,142,87,Z,,,1111111111 CHIME HUBBLE PPD/ 48 | 16,451,600,Z,,,111111 VSN_SETL 1111111111 111111111111111 / 49 | 88, 1111111111/ 50 | 16,451,215,Z,,,HBLE091722 CHIME HUBBLE PPD/ 51 | 16,451,105,Z,,,HBLE091622 CHIME HUBBLE PPD/ 52 | 49,114060,15/ 53 | 03,11111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,/ 54 | 49,00,2/ 55 | 03,11111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,/ 56 | 49,00,2/ 57 | 03,11111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,/ 58 | 49,00,2/ 59 | 03,11111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,/ 60 | 49,00,2/ 61 | 03,11111111,,010,00,,,015,00,,,040,00,,,060,00,,,100,00,0,,400,00,0,/ 62 | 49,00,2/ 63 | 98,2508440,15,72/ 64 | 99,2508440,1,74/ -------------------------------------------------------------------------------- /test/testdata/sample4-continuations-newline-delimited.txt: -------------------------------------------------------------------------------- 1 | 01,GSBI,cont001,210706,1249,1,,,2/ 2 | 02,cont001,026015079,1,230906,2000,,/ 3 | 03,107049924,USD,,,,,060,13053325440,,,100,000,0,,400,000,0,/ 4 | 49,13053325440,2/ 5 | 03,107049932,USD,,,,,060,6865898,,,100,1912,1,,400,000,0,/ 6 | 16,447,60000,,SPB2322984714570,1111,ACH Credit Payment,Entry Description: EXP; -, SEC: CCD, Client Ref ID: 1111, GS ID: SPB2322984714570 7 | 88,EREF: 1111 8 | 88,DBNM: TEST INC 9 | 88,CACT: ACHCONTROLOUTUSD01 10 | 16,261,143500,,SB2322600000404,GSQ4FBGFDGWGKY,ACH Credit Reject,From: TEST INC, Remittance Info: "ACH- Test - Addenda Record", Entry Description: TRADE; -, SEC: CTX, Client Ref ID: GSQ4FBGFDGWGKY, GS ID: SB2322600000404 11 | 88,CREF: 12 | 88,REMI: ACH- Test - Addenda Record 13 | 88,EREF: GSQ4FBGFDGWGKY 14 | 88,CRNM: Test 15 | 88,DBNM: SAMPLE INC 16 | 88,DACT: 101152046 17 | 88,DABA: 026015079 18 | 16,447,928650,,SPB2322684598521,AB-GS-RPFILERP0001-RPBA0001,ACH Credit Payment,Entry Description: TRADE; -, SEC: CTX, Client Ref ID: AB-GS-TEST0001-RPBA0001, GS ID: SPB2322684598521 19 | 88,EREF: AB-GS-RPFILERP0001-RPBA0001 20 | 88,DBNM: SAMPLE INC 21 | 88,CACT: ACHCONTROLOUTUSD01 22 | 49,-1260161341762,26/ 23 | 03,104108339,USD,010,159581194,,,015,159381194,,,040,158568897,,,045,158368897,,,100,000,0,,400,200000,1,/ 24 | 16,557,200000,,SB2322600000214,021000080000030,ACH Credit Receipt Return,Return To: Test, Remittance Info: "SB2322300000052", Entry Description: EXP; -, SEC: CCD, Reason: "R02", Return of Client Ref ID: 021000080000030, GS ID: SB2322600000214 25 | 88,CREF: 026015076104300 26 | 88,IDNM: 1114 27 | 88,EREF: 021000080000030 28 | 88,CRNM: Test 29 | 88,DBNM: SAMPLE INC. 30 | 88,CABA: 021000089 31 | 16,451,55555,,SB2322600000455,021000020000021,ACH Debit Payment,To: TEST, Entry Description: INVOICES; 210630, SEC: CCD, Client Ref ID: 021000020000021, GS ID: SB2322600000455 32 | 88,CREF: 021000020000021 33 | 88,IDNM: 2009282 34 | 88,EREF: 021000020000021 35 | 88,CRNM: TEST 36 | 88,DBNM: SAMPLE INC 37 | 88,CABA: 021000021 38 | 16,266,1912,,GI2118700002010,20210706MMQFMPU8000001,Outgoing Wire Return,- 39 | 88,CREF: 20210706MMQFMPU8000001 40 | 88,EREF: 20210706MMQFMPU8000001 41 | 88,DBIC: GSCRUS33 42 | 88,CRNM: ABC Company 43 | 88,DBNM: SAMPLE INC. 44 | 16,495,50500,,GI2321400000090,GSV0DL6RKT,Outgoing Wire,To: TEST COMPANY, Remittance Info: "QWERTIOP", Client Ref ID: GSV0DL6RKT, GS ID: GI2321400000090, Settled Amt: EUR 322.00, FX Rate: 156.833677 45 | 88,REMI: QWERTIOP 46 | 88,EREF: GSV0DL6RKT 47 | 88,CBIC: COBADEFF 48 | 88,CRNM: TEST COMPANY 49 | 88,DBNM: SAMPLE TEST 50 | 16,195,1125,,GI2229300000187,GS0D9VGMP1IWPLW,Incoming Wire,- 51 | 88,EREF: GS0D9VGMP1IWPLW 52 | 88,DBIC: CITIUS30XXX 53 | 88,CRNM: ABC CORPORATION 54 | 88,DACT: 8348572423 55 | 88,CHKN: GSIL2X6103UNCRSF 56 | 16,257,60000,,SB2225800001203,028000020000335,ACH Debit Payment Return,Return From: Company1, Entry Description: TRADE; -, SEC: CCD, Reason: "R02", Return of Client Ref ID: 028000020000335, GS ID: SB2225800001203 57 | 88,IDNM: 1 58 | 88,EREF: 028000020000335 59 | 88,CRNM: TEST INC 60 | 88,DBNM: Company1 61 | 88,DABA: 028000024 62 | 16,255,931,,SC2134800001999,,Check Return,Return From: Test2 Customer, Check Serial Number: 0009000000, Return Reason: "Payee does not exist", Client Ref ID: 74564762445, GS ID: SC213480000120999 63 | 88,EREF: 07370568132 64 | 88,CRNM: Test Inc. 65 | 88,DBNM: Test2 Customer 66 | 88,CABA: 12345 67 | 88,CHKN: 0009000000 68 | 16,195,50050,,GI2228400005800,RTR60880840833,RTP Incoming,From: SAMPLE INC, Remittance Info: "Test Remittance", Client Ref ID: RTR60880840833, GS ID: GI2228400005800, Clearing Ref: 001 69 | 88,REMI: Test Remittance 70 | 88,EREF: RTR60880840833 71 | 88,CRNM: RTR-CdtrName 72 | 88,DBNM: SAMPLE INC 73 | 88,DACT: 02122056789012205 74 | 88,DABA: 000000010 75 | 16,175,527,,SX22293073766088,GS4N04L1COP45VY,Check Deposit,- 76 | 88,EREF: GS4N04L1COP45VY 77 | 88,DACT: 100168723 78 | 16,475,10100,,SC2229300000152,01030340329,Check Paid,- 79 | 88,REMI: UAT testing for Checks 80 | 88,EREF: 01030340329 81 | 88,CRNM: TEST INC 82 | 88,DBNM: ABC CORP 83 | 88,CABA: 12345 84 | 88,CHKN: 006034594478 85 | 16,275,337686,,GI2318000014342,e457328416d411eeaf020a58a9feac02,Cash Concentration,From: SAMPLE INC, Account: 290000020437, GS Cash Concentration, "Structure ID: CC0000000", GS ID: GI2318000212121 86 | 88,REMI: Structure ID: CC0000082 87 | 88,EREF: e123456786d411eeaf020a58a9feac02 88 | 88,DBIC: GSCRUS33VIA 89 | 88,CRNM: SAMPLE INC 90 | 88,DBNM: SAMPLE INC 91 | 88,DACT: 290000020437 92 | 16,165,5000,,SPB2321284264201,AB-GS-DDFILEAB0001-DDBAB0001,ACH Debit Collection,Entry Description: BILL PMT; -, SEC: CCD, Client Ref ID: AB-GS-DDFILEAB0001-DDBAB0001, GS ID: SPB2321284264201 93 | 88,EREF: AB-GS-DDFILEAB0001-DDBAB0001 94 | 88,CRNM: SAMPLE LLP 95 | 88,DACT: ACHCONTROLINUSD01 96 | 16,475,44250,,SC2323300002416,8ce1829175a74ec88d67010dd7fb6132,Check Paid,To: TEST AND COMPANY LLC, Check Serial Number: 24108, GS ID: SC2323300002416 97 | 88,EREF: 8ce1829175a74ec88d67010dd7fb6132 98 | 88,CRNM: TEST AND COMPANY LLC 99 | 88,DBNM: Sample Inc. 100 | 88,CABA: 0 101 | 88,CHKN: 24108 102 | 16,495,30000000,,GI2323300009168,3785726,Outgoing Wire,To: TEST AND COMPANY, Remittance Info: "081823 Invoice - Sample", Client Ref ID: 3785726, GS ID: GI2323300009168, Clearing Ref: 20230821MMQFMPU7004100 103 | 88,CREF: 20230821MMQFMPU7004100 104 | 88,REMI: 081823 Invoice - Sample 105 | 88,EREF: 3785726 106 | 88,CRNM: TEST AND COMPANY 107 | 88,DBNM: Sample Inc. 108 | 88,CACT: 609873838 109 | 88,CABA: 021000021 110 | 49,6869722,8/ 111 | 03,260000033037,USD,,,,,060,000,,,100,000,0,,400,000,0,/ 112 | 49,000,2/ 113 | 03,280000010657,USD,,,,,060,000,,,100,000,0,,400,000,0,/ 114 | 49,000,2/ 115 | 98,13060195162,4,16/ 116 | 99,13060195162,1,18/ 117 | -------------------------------------------------------------------------------- /test/testdata/sample5-issue113.txt: -------------------------------------------------------------------------------- 1 | 01,GSBI,cont001,210706,1249,1,,,2/ 2 | 02,cont001,026015079,1,230906,2000,,/ 3 | 03,107049924,USD,,,,,060,13053325440,,,100,000,0,,400,000,0,/ 4 | 49,13053325440,2/ 5 | 03,107049932,USD,,,,,060,6865898,,,100,1912,1,,400,000,0,/ 6 | 16,447,60000,,SPB2322984714570,1111,ACH Credit Payment,Entry Description: EXP; -, SEC: CCD, Client Ref ID: 1111, GS ID: SPB2322984714570 7 | 88,EREF: 1111 8 | 88,DBNM: TEST INC 9 | 88,CACT: ACHCONTROLOUTUSD01 10 | 16,261,143500,,SB2322600000404,GSQ4FBGFDGWGKY,ACH Credit Reject,From: TEST INC, Remittance Info: "ACH- Test - Addenda Record", Entry Description: TRADE; -, SEC: CTX, Client Ref ID: GSQ4FBGFDGWGKY, GS ID: SB2322600000404 11 | 88,CREF: 12 | 88,REMI: ACH- Test - Addenda Record 13 | 88,EREF: GSQ4FBGFDGWGKY 14 | 88,CRNM: Test 15 | 88,DBNM: SAMPLE INC 16 | 88,DACT: 101152046 17 | 88,DABA: 026015079 18 | 16,447,928650,,SPB2322684598521,AB/GS/RPFILERP0001/RPBA0001,ACH Credit Payment,Entry Description: TRADE; -, SEC: CTX, Client Ref ID: AB/GS/TEST0001/RPBA0001, GS ID: SPB2322684598521 19 | 88,EREF: AB/GS/RPFILERP0001/RPBA0001 20 | 88,DBNM: SAMPLE INC 21 | 88,CACT: ACHCONTROLOUTUSD01 22 | 49,-1260161341762,26/ 23 | 03,104108339,USD,010,159581194,,,015,159381194,,,040,158568897,,,045,158368897,,,100,000,0,,400,200000,1,/ 24 | 16,557,200000,,SB2322600000214,021000080000030,ACH Credit Receipt Return,Return To: Test, Remittance Info: "SB2322300000052", Entry Description: EXP; -, SEC: CCD, Reason: "R02", Return of Client Ref ID: 021000080000030, GS ID: SB2322600000214 25 | 88,CREF: 026015076104300 26 | 88,IDNM: 1114 27 | 88,EREF: 021000080000030 28 | 88,CRNM: Test 29 | 88,DBNM: SAMPLE INC. 30 | 88,CABA: 021000089 31 | 16,451,55555,,SB2322600000455,021000020000021,ACH Debit Payment,To: TEST, Entry Description: INVOICES; 210630, SEC: CCD, Client Ref ID: 021000020000021, GS ID: SB2322600000455 32 | 88,CREF: 021000020000021 33 | 88,IDNM: 2009282 34 | 88,EREF: 021000020000021 35 | 88,CRNM: TEST 36 | 88,DBNM: SAMPLE INC 37 | 88,CABA: 021000021 38 | 16,266,1912,,GI2118700002010,20210706MMQFMPU8000001,Outgoing Wire Return,- 39 | 88,CREF: 20210706MMQFMPU8000001 40 | 88,EREF: 20210706MMQFMPU8000001 41 | 88,DBIC: GSCRUS33 42 | 88,CRNM: ABC Company 43 | 88,DBNM: SAMPLE INC. 44 | 16,495,50500,,GI2321400000090,GSV0DL6RKT,Outgoing Wire,To: TEST COMPANY, Remittance Info: "QWERTIOP", Client Ref ID: GSV0DL6RKT, GS ID: GI2321400000090, Settled Amt: EUR 322.00, FX Rate: 156.833677 45 | 88,REMI: QWERTIOP 46 | 88,EREF: GSV0DL6RKT 47 | 88,CBIC: COBADEFF 48 | 88,CRNM: TEST COMPANY 49 | 88,DBNM: SAMPLE TEST 50 | 16,195,1125,,GI2229300000187,GS0D9VGMP1IWPLW,Incoming Wire,- 51 | 88,EREF: GS0D9VGMP1IWPLW 52 | 88,DBIC: CITIUS30XXX 53 | 88,CRNM: ABC CORPORATION 54 | 88,DACT: 8348572423 55 | 88,CHKN: GSIL2X6103UNCRSF 56 | 16,257,60000,,SB2225800001203,028000020000335,ACH Debit Payment Return,Return From: Company1, Entry Description: TRADE; -, SEC: CCD, Reason: "R02", Return of Client Ref ID: 028000020000335, GS ID: SB2225800001203 57 | 88,IDNM: 1 58 | 88,EREF: 028000020000335 59 | 88,CRNM: TEST INC 60 | 88,DBNM: Company1 61 | 88,DABA: 028000024 62 | 16,255,931,,SC2134800001999,,Check Return,Return From: Test2 Customer, Check Serial Number: 0009000000, Return Reason: "Payee does not exist", Client Ref ID: 74564762445, GS ID: SC213480000120999 63 | 88:EREF: 07370568132 64 | 88,CRNM: Test Inc. 65 | 88,DBNM: Test2 Customer 66 | 88,CABA: 12345 67 | 88,CHKN: 0009000000 68 | 16,195,50050,,GI2228400005800,RTR60880840833,RTP Incoming,From: SAMPLE INC, Remittance Info: "Test Remittance", Client Ref ID: RTR60880840833, GS ID: GI2228400005800, Clearing Ref: 001 69 | 88,REMI: Test Remittance 70 | 88,EREF: RTR60880840833 71 | 88,CRNM: RTR-CdtrName 72 | 88,DBNM: SAMPLE INC 73 | 88,DACT: 02122056789012205 74 | 88,DABA: 000000010 75 | 16,175,527,,SX22293073766088,GS4N04L1COP45VY,Check Deposit,- 76 | 88,EREF: GS4N04L1COP45VY 77 | 88,DACT: 100168723 78 | 16,475,10100,,SC2229300000152,01030340329,Check Paid,- 79 | 88,REMI: UAT testing for Checks 80 | 88,EREF: 01030340329 81 | 88,CRNM: TEST INC 82 | 88,DBNM: ABC CORP 83 | 88,CABA: 12345 84 | 88,CHKN: 006034594478 85 | 16,275,337686,,GI2318000014342,e457328416d411eeaf020a58a9feac02,Cash Concentration,From: SAMPLE INC, Account: 290000020437, GS Cash Concentration, "Structure ID: CC0000000", GS ID: GI2318000212121 86 | 88,REMI: Structure ID: CC0000082 87 | 88,EREF: e123456786d411eeaf020a58a9feac02 88 | 88,DBIC: GSCRUS33VIA 89 | 88,CRNM: SAMPLE INC 90 | 88,DBNM: SAMPLE INC 91 | 88,DACT: 290000020437 92 | 16,165,5000,,SPB2321284264201,AB/GS/DDFILEAB0001/DDBAB0001,ACH Debit Collection,Entry Description: BILL PMT; -, SEC: CCD, Client Ref ID: AB/GS/DDFILEAB0001/DDBAB0001, GS ID: SPB2321284264201 93 | 88,EREF: AB/GS/DDFILEAB0001/DDBAB0001 94 | 88,CRNM: SAMPLE LLP 95 | 88,DACT: ACHCONTROLINUSD01 96 | 16,475,44250,,SC2323300002416,8ce1829175a74ec88d67010dd7fb6132,Check Paid,To: TEST AND COMPANY LLC, Check Serial Number: 24108, GS ID: SC2323300002416 97 | 88,EREF: 8ce1829175a74ec88d67010dd7fb6132 98 | 88,CRNM: TEST AND COMPANY LLC 99 | 88,DBNM: Sample Inc. 100 | 88,CABA: 0 101 | 88,CHKN: 24108 102 | 16,495,30000000,,GI2323300009168,3785726,Outgoing Wire,To: TEST AND COMPANY, Remittance Info: "08/18/23 Invoice - Sample", Client Ref ID: 3785726, GS ID: GI2323300009168, Clearing Ref: 20230821MMQFMPU7004100 103 | 88,CREF: 20230821MMQFMPU7004100 104 | 88,REMI: 08/18/23 Invoice - Sample 105 | 88,EREF: 3785726 106 | 88,CRNM: TEST AND COMPANY 107 | 88,DBNM: Sample Inc. 108 | 88,CACT: 609873838 109 | 88,CABA: 021000021 110 | 16,195,3797999624,,GI2323300007089,20230821J1Q5040C000707,Incoming Wire,From: TEST AND COMPANY, Client Ref ID: 20230821J1Q5040C000707, GS ID: GI2323300007089, Clearing Ref: 20230821J1Q5040C000707 111 | 88,CREF: 20230821J1Q5040C000707 112 | 88,EREF: 20230821J1Q5040C000707 113 | 88,CRNM: SAMPLE INC 114 | 88,DBNM: TEST AND COMPANY 115 | 88,DACT: 000001000600427 116 | 16,698,463462,,M8916_20230818_001,M8916_20230818_001,Fees,Fees For Account: XXXXXXXX-0186 117 | 16,354,1764,,SBD85710_20230731_0021,SBD85710_20230731_0021,Interest,Interest For Account: XXXXXXXX-3074, Period: Jul 1, 2023 to Jul 31, 2023 118 | 49,6869722,8/ 119 | 03,260000033037,USD,,,,,060,000,,,100,000,0,,400,000,0,/ 120 | 49,000,2/ 121 | 03,280000010657,USD,,,,,060,000,,,100,000,0,,400,000,0,/ 122 | 49,000,2/ 123 | 98,13060195162,4,16/ 124 | 99,13060195162,1,18/ -------------------------------------------------------------------------------- /version.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 bai2 19 | 20 | var Version string 21 | --------------------------------------------------------------------------------