Skip to content

Commit 67d8a31

Browse files
authored
Merge pull request #1614 from adamdecaf/feat-merge-api
server: add Merge files endpoint
2 parents 357f597 + 987a406 commit 67d8a31

File tree

5 files changed

+216
-0
lines changed

5 files changed

+216
-0
lines changed

openapi.yaml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,41 @@ paths:
744744
$ref: 'https://raw.githubusercontent.com/moov-io/base/master/api/common.yaml#/components/schemas/Error'
745745
'404':
746746
description: A resource with the specified ID was not found
747+
/merge:
748+
post:
749+
tags: ['ACH Files']
750+
summary: Merge Files
751+
description: Combine multiple fileIDs and files together. Returned is the results of merging
752+
operationId: mergeFiles
753+
parameters:
754+
- name: X-Request-ID
755+
in: header
756+
description: Optional Request ID allows application developer to trace requests through the system's logs
757+
example: "rs4f9915"
758+
schema:
759+
type: string
760+
requestBody:
761+
description: JSON object containing FileIDs and ACH files (in JSON format)
762+
required: true
763+
content:
764+
application/json:
765+
schema:
766+
$ref: '#/components/schemas/MergeFilesRequest'
767+
responses:
768+
'200':
769+
description: Files were merged together
770+
content:
771+
application/json:
772+
schema:
773+
$ref: '#/components/schemas/MergeFilesResponse'
774+
'400':
775+
description: See error in response body
776+
content:
777+
application/json:
778+
schema:
779+
$ref: 'https://raw.githubusercontent.com/moov-io/base/master/api/common.yaml#/components/schemas/Error'
780+
'404':
781+
description: A resource with the specified ID was not found
747782

748783
components:
749784
schemas:
@@ -2607,3 +2642,19 @@ components:
26072642
$ref: '#/components/schemas/SegmentFileConfiguration'
26082643
validateOpts:
26092644
$ref: '#/components/schemas/ValidateOpts'
2645+
MergeFilesRequest:
2646+
properties:
2647+
fileIDs:
2648+
type: array
2649+
items:
2650+
type: string
2651+
files:
2652+
type: array
2653+
items:
2654+
$ref: '#/components/schemas/File'
2655+
MergeFilesResponse:
2656+
properties:
2657+
files:
2658+
type: array
2659+
items:
2660+
$ref: '#/components/schemas/File'

server/files.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package server
1919

2020
import (
2121
"bytes"
22+
"cmp"
2223
"context"
2324
"encoding/json"
2425
"errors"
@@ -821,3 +822,71 @@ func decodeFlattenBatchesRequest(_ context.Context, r *http.Request) (interface{
821822
requestID: moovhttp.GetRequestID(r),
822823
}, nil
823824
}
825+
826+
type mergeFilesRequest struct {
827+
FileIDs []string `json:"fileIDs"`
828+
Files []*ach.File `json:"files"`
829+
830+
RequestID string `json:"requestID"`
831+
}
832+
833+
type mergeFilesResponse struct {
834+
Files []*ach.File `json:"files"`
835+
Err error `json:"error"`
836+
}
837+
838+
func mergeFilesEndpoint(s Service, r Repository, logger log.Logger) endpoint.Endpoint {
839+
return func(_ context.Context, request interface{}) (interface{}, error) {
840+
req, ok := request.(mergeFilesRequest)
841+
if !ok {
842+
return mergeFilesResponse{Err: ErrFoundABug}, ErrFoundABug
843+
}
844+
845+
merged, err := s.MergeFiles(req.FileIDs, req.Files)
846+
if logger != nil {
847+
logger := logger.With(log.Fields{
848+
"file_ids": log.Strings(req.FileIDs),
849+
"requestID": log.String(req.RequestID),
850+
})
851+
if err != nil {
852+
logger.Error().LogError(err)
853+
} else {
854+
logger.Info().Log("merging files")
855+
}
856+
}
857+
if err != nil {
858+
return mergeFilesResponse{Err: err}, err
859+
}
860+
861+
for idx := range merged {
862+
err := r.StoreFile(merged[idx])
863+
if logger != nil {
864+
logger := logger.With(log.Fields{
865+
"file_ids": log.Strings(req.FileIDs),
866+
"requestID": log.String(req.RequestID),
867+
})
868+
if err != nil {
869+
logger.Error().LogError(err)
870+
} else {
871+
logger.Info().Logf("merge created file %s", merged[idx].ID)
872+
}
873+
}
874+
}
875+
876+
return mergeFilesResponse{
877+
Files: merged,
878+
}, nil
879+
}
880+
}
881+
882+
func decodeMergeFilesRequest(_ context.Context, r *http.Request) (interface{}, error) {
883+
var req mergeFilesRequest
884+
err := json.NewDecoder(r.Body).Decode(&req)
885+
if err != nil {
886+
return nil, fmt.Errorf("decoding merge files json: %w", err)
887+
}
888+
889+
req.RequestID = cmp.Or(req.RequestID, moovhttp.GetRequestID(r))
890+
891+
return req, nil
892+
}

server/files_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1442,3 +1442,55 @@ func TestFiles__flattenFileEndpointError(t *testing.T) {
14421442
t.Errorf("resp.Err=%q", resp.Err)
14431443
}
14441444
}
1445+
1446+
func TestFiles_MergeFiles(t *testing.T) {
1447+
logger := log.NewTestLogger()
1448+
repo := NewRepositoryInMemory(testTTLDuration, logger)
1449+
svc := NewService(repo)
1450+
router := MakeHTTPHandler(svc, repo, kitlog.NewLogfmtLogger(os.Stdout))
1451+
1452+
// Write a file into the repository
1453+
file, err := ach.ReadJSONFile(filepath.Join("..", "test", "testdata", "ppd-mixedDebitCredit-valid.json"))
1454+
require.NoError(t, err)
1455+
1456+
file.ID = base.ID()
1457+
1458+
err = repo.StoreFile(file)
1459+
require.NoError(t, err)
1460+
1461+
// Merge this with another file
1462+
file2, err := ach.ReadJSONFile(filepath.Join("..", "test", "testdata", "ppd-valid.json"))
1463+
require.NoError(t, err)
1464+
1465+
var body bytes.Buffer
1466+
err = json.NewEncoder(&body).Encode(mergeFilesRequest{
1467+
FileIDs: []string{file.ID},
1468+
Files: []*ach.File{file2},
1469+
})
1470+
req := httptest.NewRequest("POST", "/merge", &body)
1471+
1472+
w := httptest.NewRecorder()
1473+
router.ServeHTTP(w, req)
1474+
w.Flush()
1475+
1476+
require.Equal(t, http.StatusOK, w.Code)
1477+
1478+
var resp mergeFilesResponse
1479+
err = json.NewDecoder(w.Body).Decode(&resp)
1480+
require.NoError(t, err)
1481+
1482+
require.Len(t, resp.Files, 1)
1483+
1484+
// Check the file
1485+
merged := resp.Files[0]
1486+
require.Len(t, merged.Batches, 2)
1487+
1488+
var foundEntries int
1489+
for idx := range merged.Batches {
1490+
foundEntries += len(merged.Batches[idx].GetEntries())
1491+
}
1492+
require.Equal(t, 3, foundEntries)
1493+
1494+
require.Equal(t, 100000, merged.Control.TotalDebitEntryDollarAmountInFile)
1495+
require.Equal(t, 200000, merged.Control.TotalCreditEntryDollarAmountInFile)
1496+
}

server/routing.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,12 @@ func MakeHTTPHandler(s Service, repo Repository, kitlog gokitlog.Logger) http.Ha
211211
encodeResponse,
212212
options...,
213213
))
214+
r.Methods("POST").Path("/merge").Handler(httptransport.NewServer(
215+
mergeFilesEndpoint(s, repo, logger),
216+
decodeMergeFilesRequest,
217+
encodeResponse,
218+
options...,
219+
))
214220
return r
215221
}
216222

server/service.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package server
1919

2020
import (
2121
"bytes"
22+
"crypto/sha256"
23+
"encoding/hex"
2224
"errors"
2325
"fmt"
2426
"io"
@@ -65,6 +67,8 @@ type Service interface {
6567
GetBatches(fileID string) []ach.Batcher
6668
// DeleteBatch takes a fileID and BatchID and removes the batch from the file
6769
DeleteBatch(fileID string, batchID string) error
70+
// MergeFiles will combine all the given files together
71+
MergeFiles(fileIDs []string, files []*ach.File) ([]*ach.File, error)
6872
}
6973

7074
// service a concrete implementation of the service.
@@ -262,3 +266,37 @@ func (s *service) FlattenBatches(fileID string) (*ach.File, error) {
262266
}
263267
return ff, err
264268
}
269+
270+
func (s *service) MergeFiles(fileIDs []string, files []*ach.File) ([]*ach.File, error) {
271+
for idx := range fileIDs {
272+
file, err := s.store.FindFile(fileIDs[idx])
273+
if err != nil {
274+
return nil, fmt.Errorf("file not found: %v", fileIDs[idx])
275+
}
276+
277+
files = append(files, file)
278+
}
279+
280+
merged, err := ach.MergeFiles(files)
281+
if err != nil {
282+
return nil, fmt.Errorf("merging files: %w", err)
283+
}
284+
285+
for idx := range merged {
286+
var buf bytes.Buffer
287+
err := ach.NewWriter(&buf).Write(merged[idx])
288+
if err != nil {
289+
return nil, fmt.Errorf("problem hashing merged file: %w", err)
290+
}
291+
292+
merged[idx].ID = hash(buf.Bytes())
293+
}
294+
295+
return merged, nil
296+
}
297+
298+
func hash(data []byte) string {
299+
ss := sha256.New()
300+
ss.Write(data)
301+
return hex.EncodeToString(ss.Sum(nil))
302+
}

0 commit comments

Comments
 (0)