Skip to content

Commit 47bc5be

Browse files
authored
Merge pull request #1718 from moov-io/feat-fix-cmd
cmd/achcli: add -fix command
2 parents efc9509 + 2bf63a6 commit 47bc5be

File tree

11 files changed

+296
-0
lines changed

11 files changed

+296
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,5 @@ openapi-generator*.jar
6464
/client/
6565
.idea/
6666
.vscode/
67+
68+
test/testdata/ppd-debit.ach.fix

cmd/achcli/fix/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package fix
2+
3+
type Config struct {
4+
UpdateEED string
5+
}

cmd/achcli/fix/fix.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package fix
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
8+
"github.com/moov-io/ach"
9+
"github.com/moov-io/ach/cmd/achcli/internal/read"
10+
"github.com/moov-io/ach/cmd/achcli/internal/write"
11+
)
12+
13+
func Perform(path string, validateOptsPath *string, skipAll *bool, conf Config) (string, error) {
14+
file, format, err := read.Filepath(path, validateOptsPath, skipAll)
15+
if err != nil {
16+
return "", fmt.Errorf("reading %s failed: %w", path, err)
17+
}
18+
19+
// Build up our fixers
20+
var batchHeaderFixers []batchHeaderFixer
21+
if conf.UpdateEED != "" {
22+
batchHeaderFixers = append(batchHeaderFixers, updateEED(conf))
23+
}
24+
25+
// Fix the file
26+
for idx := range file.Batches {
27+
// Batch headers
28+
bh := file.Batches[idx].GetHeader()
29+
for _, fn := range batchHeaderFixers {
30+
if err := fn(bh); err != nil {
31+
return "", fmt.Errorf("applying %T to batch header: %w", fn, err)
32+
}
33+
}
34+
file.Batches[idx].SetHeader(bh)
35+
}
36+
37+
// Write file
38+
newpath := path + ".fix"
39+
40+
var buf bytes.Buffer
41+
err = write.File(&buf, file, format)
42+
if err != nil {
43+
return "", fmt.Errorf("encoding fixed file as %s: %w", format, err)
44+
}
45+
46+
err = os.WriteFile(newpath, buf.Bytes(), 0600)
47+
if err != nil {
48+
return "", fmt.Errorf("writing %s failed: %w", newpath, err)
49+
}
50+
51+
return newpath, nil
52+
}
53+
54+
type batchHeaderFixer func(bh *ach.BatchHeader) error

cmd/achcli/fix/fix_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package fix_test
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/moov-io/ach/cmd/achcli/fix"
10+
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestPerform(t *testing.T) {
15+
cases := []struct {
16+
inputFilepath string
17+
validateOptsPath *string
18+
skipAll *bool
19+
config fix.Config
20+
expectedFilepath string
21+
}{
22+
{
23+
inputFilepath: filepath.Join("..", "..", "..", "test", "testdata", "ppd-debit.ach"),
24+
config: fix.Config{
25+
UpdateEED: "20251224",
26+
},
27+
expectedFilepath: filepath.Join("..", "..", "..", "test", "testdata", "ppd-debit.ach.fix.expected"),
28+
},
29+
}
30+
for _, tc := range cases {
31+
_, filename := filepath.Split(tc.inputFilepath)
32+
33+
t.Run(filename, func(t *testing.T) {
34+
newpath, err := fix.Perform(tc.inputFilepath, tc.validateOptsPath, tc.skipAll, tc.config)
35+
require.NoError(t, err)
36+
37+
got, err := os.ReadFile(newpath)
38+
require.NoError(t, err)
39+
40+
expected, err := os.ReadFile(tc.expectedFilepath)
41+
require.NoError(t, err)
42+
43+
got = normalize(got)
44+
expected = normalize(expected)
45+
46+
require.Equal(t, string(expected), string(got))
47+
})
48+
}
49+
}
50+
51+
func ptr[T any](in T) *T { return &in }
52+
53+
func normalize(input []byte) []byte {
54+
return bytes.TrimSpace(bytes.ReplaceAll(input, []byte("\r\n"), []byte("\n")))
55+
}

cmd/achcli/fix/fixers.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package fix
2+
3+
import (
4+
"github.com/moov-io/ach"
5+
)
6+
7+
func updateEED(conf Config) batchHeaderFixer {
8+
return func(bh *ach.BatchHeader) error {
9+
bh.EffectiveEntryDate = conf.UpdateEED
10+
return nil
11+
}
12+
}

cmd/achcli/internal/read/read.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package read
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
9+
"github.com/moov-io/ach"
10+
)
11+
12+
type Format string
13+
14+
var (
15+
FormatUnknown Format = "unknown"
16+
FormatNacha Format = "nacha"
17+
FormatJSON Format = "json"
18+
)
19+
20+
func Filepath(path string, validateOptsPath *string, skipAll *bool) (*ach.File, Format, error) {
21+
validateOpts, err := readValidationOpts(validateOptsPath, skipAll)
22+
if err != nil {
23+
return nil, FormatUnknown, err
24+
}
25+
return readFile(path, validateOpts)
26+
}
27+
28+
func readValidationOpts(path *string, skipAll *bool) (*ach.ValidateOpts, error) {
29+
var opts ach.ValidateOpts
30+
31+
if skipAll != nil && *skipAll {
32+
opts.SkipAll = true
33+
return &opts, nil
34+
}
35+
36+
if path != nil && *path != "" {
37+
// read config file
38+
bs, readErr := os.ReadFile(*path)
39+
if readErr != nil {
40+
return nil, fmt.Errorf("reading %s for validate opts failed: %w", *path, readErr)
41+
}
42+
43+
if err := json.Unmarshal(bs, &opts); err != nil {
44+
return nil, fmt.Errorf("unmarshal of validate opts failed: %v", err)
45+
}
46+
return &opts, nil
47+
}
48+
49+
return nil, nil
50+
}
51+
52+
func readFile(path string, validateOpts *ach.ValidateOpts) (*ach.File, Format, error) {
53+
bs, err := os.ReadFile(path)
54+
if err != nil {
55+
return nil, FormatUnknown, err
56+
}
57+
if json.Valid(bs) {
58+
return readJsonFile(bs, validateOpts)
59+
}
60+
return readACHFile(bs, validateOpts)
61+
}
62+
63+
func readACHFile(input []byte, validateOpts *ach.ValidateOpts) (*ach.File, Format, error) {
64+
r := ach.NewReader(bytes.NewReader(input))
65+
r.SetValidation(validateOpts)
66+
67+
f, err := r.Read()
68+
69+
return &f, FormatNacha, err
70+
}
71+
72+
func readJsonFile(input []byte, validateOpts *ach.ValidateOpts) (*ach.File, Format, error) {
73+
file, err := ach.FileFromJSONWith(input, validateOpts)
74+
return file, FormatJSON, err
75+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package read_test
2+
3+
import (
4+
"path/filepath"
5+
"testing"
6+
7+
"github.com/moov-io/ach/cmd/achcli/internal/read"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestRead(t *testing.T) {
13+
cases := []struct {
14+
inputFilepath string
15+
validateOptsPath *string
16+
skipAll *bool
17+
18+
expectedFormat read.Format
19+
}{
20+
{
21+
inputFilepath: filepath.Join("..", "..", "..", "..", "test", "testdata", "ppd-debit-invalid-entryDetail-checkDigit.ach"),
22+
validateOptsPath: ptr(filepath.Join("..", "..", "..", "..", "test", "testdata", "ppd-debit-invalid-entryDetail-checkDigit.json")),
23+
expectedFormat: read.FormatNacha,
24+
},
25+
{
26+
inputFilepath: filepath.Join("..", "..", "..", "..", "test", "testdata", "ppd-valid-preserve-spaces.json"),
27+
expectedFormat: read.FormatJSON,
28+
},
29+
}
30+
for _, tc := range cases {
31+
_, filename := filepath.Split(tc.inputFilepath)
32+
33+
t.Run(filename, func(t *testing.T) {
34+
file, format, err := read.Filepath(tc.inputFilepath, tc.validateOptsPath, tc.skipAll)
35+
require.NoError(t, err)
36+
require.Equal(t, tc.expectedFormat, format)
37+
require.NotNil(t, file)
38+
})
39+
}
40+
}
41+
42+
func ptr[T any](in T) *T { return &in }

cmd/achcli/internal/write/write.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package write
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
8+
"github.com/moov-io/ach"
9+
"github.com/moov-io/ach/cmd/achcli/internal/read"
10+
)
11+
12+
func File(w io.Writer, file *ach.File, format read.Format) error {
13+
switch format {
14+
case read.FormatNacha:
15+
return ach.NewWriter(w).Write(file)
16+
case read.FormatJSON:
17+
return json.NewEncoder(w).Encode(file)
18+
}
19+
return fmt.Errorf("unknown format %v", format)
20+
}

cmd/achcli/main.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"strings"
1313

1414
"github.com/moov-io/ach"
15+
"github.com/moov-io/ach/cmd/achcli/fix"
1516
)
1617

1718
var (
@@ -33,6 +34,10 @@ var (
3334

3435
flagSkipValidation = flag.Bool("skip-validation", false, "Skip all validation checks")
3536
flagValidateOpts = flag.String("validate", "", "Path to config file in json format to enable validation opts")
37+
38+
// Fix commands
39+
flagFix = flag.Bool("fix", false, "Trigger fix tasks")
40+
flagUpdateEED = flag.String("update-eed", "", "Set the EffectiveEntryDate to a new value")
3641
)
3742

3843
func main() {
@@ -78,6 +83,21 @@ func main() {
7883
os.Exit(1)
7984
}
8085

86+
case *flagFix:
87+
if len(args) != 1 {
88+
fmt.Printf("ERROR: unexpected %d arguments: %#v\n", len(args), args)
89+
os.Exit(1)
90+
}
91+
conf := fix.Config{
92+
UpdateEED: *flagUpdateEED,
93+
}
94+
newpath, err := fix.Perform(args[0], flagValidateOpts, flagSkipValidation, conf)
95+
if err != nil {
96+
fmt.Printf("ERROR: %v\n", err)
97+
os.Exit(1)
98+
}
99+
fmt.Printf("Fixed file: %s\n", newpath)
100+
81101
case *flagReformat != "" && len(args) == 1:
82102
if err := reformat(*flagReformat, args[0], validateOpts); err != nil {
83103
fmt.Printf("ERROR: %v\n", err)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"allowInvalidCheckDigit": true}

0 commit comments

Comments
 (0)