diff --git a/client/README.md b/client/README.md index 8ebbb965..1799270b 100644 --- a/client/README.md +++ b/client/README.md @@ -90,6 +90,7 @@ Class | Method | HTTP request | Description - [ServiceMessage](docs/ServiceMessage.md) - [TypeSubType](docs/TypeSubType.md) - [UnstructuredAddenda](docs/UnstructuredAddenda.md) + - [ValidateOptions](docs/ValidateOptions.md) - [WireAddress](docs/WireAddress.md) - [WireAmount](docs/WireAmount.md) - [WireFile](docs/WireFile.md) diff --git a/client/api/openapi.yaml b/client/api/openapi.yaml index 15f57c52..f4d75019 100644 --- a/client/api/openapi.yaml +++ b/client/api/openapi.yaml @@ -65,7 +65,8 @@ paths: - Wire Files /files/create: post: - description: Create a new File object from either the plaintext or JSON representation. + description: | + Upload a new Wire file, or create one from JSON. When uploading a file, query parameters can be used to configure the FedWireMessage validation options. For JSON requests, validation options are set in the request body under fedWireMessage.validateOptions. operationId: createWireFile parameters: - description: Optional Request ID allows application developer to trace requests @@ -78,6 +79,27 @@ paths: schema: type: string style: simple + - description: Optional flag to skip mandatory IMAD validation + explode: true + in: query + name: skipMandatoryIMAD + required: false + schema: + default: false + example: true + type: boolean + style: form + - description: Optional flag to allow SenderSupplied to be nil, which is generally + the case in incoming files. + explode: true + in: query + name: allowMissingSenderSupplied + required: false + schema: + default: false + example: true + type: boolean + style: form requestBody: content: application/json: @@ -506,6 +528,9 @@ components: receiptTime: "1305" receiptDate: "0401" receiptApplicationIdentification: RB11 + validateOptions: + allowMissingSenderSupplied: true + skipMandatoryIMAD: true previousMessageIdentifier: previousMessageIdentifier: Identifier adjustment: @@ -923,6 +948,9 @@ components: receiptTime: "1305" receiptDate: "0401" receiptApplicationIdentification: RB11 + validateOptions: + allowMissingSenderSupplied: true + skipMandatoryIMAD: true previousMessageIdentifier: previousMessageIdentifier: Identifier adjustment: @@ -1279,6 +1307,8 @@ components: $ref: '#/components/schemas/RemittanceFreeText' serviceMessage: $ref: '#/components/schemas/ServiceMessage' + validateOptions: + $ref: '#/components/schemas/ValidateOptions' required: - amount - businessFunctionCode @@ -3094,6 +3124,23 @@ components: example: Line Twelve Text maxLength: 35 type: string + ValidateOptions: + example: + allowMissingSenderSupplied: true + skipMandatoryIMAD: true + nullable: true + properties: + skipMandatoryIMAD: + default: false + description: Skip validation of the InputMessageAccountabilityData (IMAD) + field + example: true + type: boolean + allowMissingSenderSupplied: + default: false + description: Allow FedWireMessage.SenderSupplied to be nil + example: true + type: boolean Error: properties: error: diff --git a/client/api_wire_files.go b/client/api_wire_files.go index cac6d4bb..97f22e43 100644 --- a/client/api_wire_files.go +++ b/client/api_wire_files.go @@ -109,16 +109,20 @@ func (a *WireFilesApiService) AddFEDWireMessageToFile(ctx _context.Context, file // CreateWireFileOpts Optional parameters for the method 'CreateWireFile' type CreateWireFileOpts struct { - XRequestID optional.String + XRequestID optional.String + SkipMandatoryIMAD optional.Bool + AllowMissingSenderSupplied optional.Bool } /* CreateWireFile Create file -Create a new File object from either the plaintext or JSON representation. +Upload a new Wire file, or create one from JSON. When uploading a file, query parameters can be used to configure the FedWireMessage validation options. For JSON requests, validation options are set in the request body under fedWireMessage.validateOptions. - @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param wireFile Content of the Wire file (in json or raw text) - @param optional nil or *CreateWireFileOpts - Optional Parameters: - @param "XRequestID" (optional.String) - Optional Request ID allows application developer to trace requests through the system's logs + - @param "SkipMandatoryIMAD" (optional.Bool) - Optional flag to skip mandatory IMAD validation + - @param "AllowMissingSenderSupplied" (optional.Bool) - Optional flag to allow SenderSupplied to be nil, which is generally the case in incoming files. @return WireFile */ @@ -139,6 +143,12 @@ func (a *WireFilesApiService) CreateWireFile(ctx _context.Context, wireFile Wire localVarQueryParams := _neturl.Values{} localVarFormParams := _neturl.Values{} + if localVarOptionals != nil && localVarOptionals.SkipMandatoryIMAD.IsSet() { + localVarQueryParams.Add("skipMandatoryIMAD", parameterToString(localVarOptionals.SkipMandatoryIMAD.Value(), "")) + } + if localVarOptionals != nil && localVarOptionals.AllowMissingSenderSupplied.IsSet() { + localVarQueryParams.Add("allowMissingSenderSupplied", parameterToString(localVarOptionals.AllowMissingSenderSupplied.Value(), "")) + } // to determine the Content-Type header localVarHTTPContentTypes := []string{"application/json", "text/plain"} diff --git a/client/client.go b/client/client.go index 6c9220af..2faef75a 100644 --- a/client/client.go +++ b/client/client.go @@ -220,7 +220,7 @@ func (c *APIClient) prepareRequest( } if len(fileBytes) > 0 && fileName != "" { w.Boundary() - //_, fileNm := filepath.Split(fileName) + // _, fileNm := filepath.Split(fileName) part, err := w.CreateFormFile(formFileName, filepath.Base(fileName)) if err != nil { return nil, err diff --git a/client/docs/FedWireMessage.md b/client/docs/FedWireMessage.md index 393c8b5a..97afc5ab 100644 --- a/client/docs/FedWireMessage.md +++ b/client/docs/FedWireMessage.md @@ -65,6 +65,7 @@ Name | Type | Description | Notes **SecondaryRemittanceDocument** | [**SecondaryRemittanceDocument**](SecondaryRemittanceDocument.md) | | [optional] **RemittanceFreeText** | [**RemittanceFreeText**](RemittanceFreeText.md) | | [optional] **ServiceMessage** | [**ServiceMessage**](ServiceMessage.md) | | [optional] +**ValidateOptions** | Pointer to [**ValidateOptions**](ValidateOptions.md) | | [optional] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/client/docs/ValidateOptions.md b/client/docs/ValidateOptions.md new file mode 100644 index 00000000..5c879b1e --- /dev/null +++ b/client/docs/ValidateOptions.md @@ -0,0 +1,12 @@ +# ValidateOptions + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**SkipMandatoryIMAD** | **bool** | Skip validation of the InputMessageAccountabilityData (IMAD) field | [optional] [default to false] +**AllowMissingSenderSupplied** | **bool** | Allow FedWireMessage.SenderSupplied to be nil | [optional] [default to false] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/client/docs/WireFilesApi.md b/client/docs/WireFilesApi.md index 9a312027..5a4b7fe9 100644 --- a/client/docs/WireFilesApi.md +++ b/client/docs/WireFilesApi.md @@ -68,7 +68,7 @@ No authorization required Create file -Create a new File object from either the plaintext or JSON representation. +Upload a new Wire file, or create one from JSON. When uploading a file, query parameters can be used to configure the FedWireMessage validation options. For JSON requests, validation options are set in the request body under fedWireMessage.validateOptions. ### Required Parameters @@ -88,6 +88,8 @@ Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **xRequestID** | **optional.String**| Optional Request ID allows application developer to trace requests through the system's logs | + **skipMandatoryIMAD** | **optional.Bool**| Optional flag to skip mandatory IMAD validation | [default to false] + **allowMissingSenderSupplied** | **optional.Bool**| Optional flag to allow SenderSupplied to be nil, which is generally the case in incoming files. | [default to false] ### Return type diff --git a/client/model_fed_wire_message.go b/client/model_fed_wire_message.go index 0038871e..efe29f98 100644 --- a/client/model_fed_wire_message.go +++ b/client/model_fed_wire_message.go @@ -73,4 +73,5 @@ type FedWireMessage struct { SecondaryRemittanceDocument SecondaryRemittanceDocument `json:"secondaryRemittanceDocument,omitempty"` RemittanceFreeText RemittanceFreeText `json:"remittanceFreeText,omitempty"` ServiceMessage ServiceMessage `json:"serviceMessage,omitempty"` + ValidateOptions *ValidateOptions `json:"validateOptions,omitempty"` } diff --git a/client/model_validate_options.go b/client/model_validate_options.go new file mode 100644 index 00000000..8b32dd39 --- /dev/null +++ b/client/model_validate_options.go @@ -0,0 +1,18 @@ +/* + * Wire API + * + * Moov Wire implements an HTTP API for creating, parsing, and validating Fedwire messages. + * + * API version: v1 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +// ValidateOptions struct for ValidateOptions +type ValidateOptions struct { + // Skip validation of the InputMessageAccountabilityData (IMAD) field + SkipMandatoryIMAD bool `json:"skipMandatoryIMAD,omitempty"` + // Allow FedWireMessage.SenderSupplied to be nil + AllowMissingSenderSupplied bool `json:"allowMissingSenderSupplied,omitempty"` +} diff --git a/cmd/server/files.go b/cmd/server/files.go index 83b9c87f..3b5c3102 100644 --- a/cmd/server/files.go +++ b/cmd/server/files.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "net/http" + "net/url" "strconv" "strings" @@ -96,35 +97,35 @@ func createFile(logger log.Logger, repo WireFileRepository) http.HandlerFunc { w = wrapResponseWriter(logger, w, r) - req := wire.NewFile() - req.ID = base.ID() - + file := wire.NewFile() if strings.Contains(r.Header.Get("Content-Type"), "application/json") { - if err := json.NewDecoder(r.Body).Decode(req); err != nil { + if err := json.NewDecoder(r.Body).Decode(file); err != nil { err = logger.LogErrorf("error reading request body: %v", err).Err() moovhttp.Problem(w, err) return } - if err := req.Validate(); err != nil { + + if err := file.Validate(); err != nil { err = logger.LogErrorf("file validation failed: %v", err).Err() moovhttp.Problem(w, err) return } } else { - file, err := wire.NewReader(r.Body).Read() + f, err := wire.NewReader(r.Body).ReadWithOpts(validateOptsFromQuery(r.URL.Query())) if err != nil { err = logger.LogErrorf("error reading file: %v", err).Err() moovhttp.Problem(w, err) return } - req = &file + file = &f } - if req.ID == "" { - req.ID = base.ID() + + if file.ID == "" { + file.ID = base.ID() } - logger = logger.Set("fileID", log.String(req.ID)) + logger = logger.Set("fileID", log.String(file.ID)) - if err := repo.saveFile(req); err != nil { + if err := repo.saveFile(file); err != nil { err = logger.LogErrorf("problem saving file: %v", err).Err() moovhttp.Problem(w, err) return @@ -136,7 +137,7 @@ func createFile(logger log.Logger, repo WireFileRepository) http.HandlerFunc { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(req) + json.NewEncoder(w).Encode(file) } } @@ -390,3 +391,38 @@ func GetWriter(w io.Writer, r *http.Request) (*wire.Writer, error) { writer = wire.NewWriter(w, lengthFormatOption, newLineFormatOption) return writer, nil } + +// validateOptsFromQuery returns a ValidateOpts struct based on the query params. +// If no validation query params were provided, opts will be nil. +func validateOptsFromQuery(query url.Values) (opts *wire.ValidateOpts) { + if len(query) == 0 { + return opts + } + + const ( + skipMandatoryIMAD = "skipMandatoryIMAD" + allowMissingSenderSupplied = "allowMissingSenderSupplied" + ) + + validationNames := []string{ + skipMandatoryIMAD, + allowMissingSenderSupplied, + } + + for _, param := range validationNames { + if set, _ := strconv.ParseBool(query.Get(param)); set { + if opts == nil { + opts = &wire.ValidateOpts{} + } + + switch param { + case skipMandatoryIMAD: + opts.SkipMandatoryIMAD = true + case allowMissingSenderSupplied: + opts.AllowMissingSenderSupplied = true + } + } + } + + return opts +} diff --git a/cmd/server/files_test.go b/cmd/server/files_test.go index c9088f6d..4e1d9fb1 100644 --- a/cmd/server/files_test.go +++ b/cmd/server/files_test.go @@ -9,8 +9,10 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/http" "net/http/httptest" + "net/url" "os" "path/filepath" "strings" @@ -193,6 +195,7 @@ func TestFiles_createFileJSON(t *testing.T) { require.NoError(t, json.NewDecoder(w.Body).Decode(&resp)) assert.NotEmpty(t, resp.ID) assert.NotEmpty(t, resp.FEDWireMessage) + assert.Nil(t, resp.FEDWireMessage.ValidateOptions) }) t.Run("invalid JSON", func(t *testing.T) { @@ -207,6 +210,138 @@ func TestFiles_createFileJSON(t *testing.T) { }) } +func TestFiles_createFile_missingSenderSupplied(t *testing.T) { + repo := &testWireFileRepository{} + router := mux.NewRouter() + addFileRoutes(log.NewNopLogger(), router, repo) + + // set up a message with no SenderSupplied field + fwm := mockFEDWireMessage() + fwm.ValidateOptions = nil + fwm.SenderSupplied = nil + file := wire.NewFile() + file.AddFEDWireMessage(fwm) + + // create from JSON, without validation options, should fail without sender supplied + resp, _ := routerUploadJSON(t, router, file) + require.Equal(t, http.StatusBadRequest, resp.Code, resp.Body) + require.Contains(t, resp.Body.String(), "SenderSupplied") + + // create from JSON, using validation options, should succeed without sender supplied + file.FEDWireMessage.ValidateOptions = &wire.ValidateOpts{ + AllowMissingSenderSupplied: true, + } + resp, uploaded := routerUploadJSON(t, router, file) + require.Equal(t, http.StatusCreated, resp.Code, resp.Body) + assert.NotEmpty(t, uploaded.ID) + assert.Nil(t, uploaded.FEDWireMessage.SenderSupplied) + + // make sure the file was saved + resp, found := routerGetFile(t, router, uploaded.ID) + require.Equal(t, http.StatusOK, resp.Code, resp.Body) + assert.Equal(t, uploaded.ID, found.ID) + assert.Nil(t, found.FEDWireMessage.SenderSupplied) + assert.NotNil(t, found.FEDWireMessage.ValidateOptions) + assert.True(t, found.FEDWireMessage.ValidateOptions.AllowMissingSenderSupplied) + + // get file contents calls Validate() + // if isIncoming was passed properly, then the file should be valid + resp = routerGetFileContents(t, router, uploaded.ID) + require.Equal(t, http.StatusOK, resp.Code, resp.Body) + assert.NotNil(t, resp.Body) + + // upload raw should succeed without sender supplied + resp, rawUpload := routerUploadRaw(t, router, resp.Body, + setQueryParam("allowMissingSenderSupplied", "true"), + ) + require.Equal(t, http.StatusCreated, resp.Code, resp.Body) + assert.NotEmpty(t, rawUpload.ID) + assert.Nil(t, rawUpload.FEDWireMessage.SenderSupplied) + assert.NotNil(t, rawUpload.FEDWireMessage.ValidateOptions) + assert.True(t, rawUpload.FEDWireMessage.ValidateOptions.AllowMissingSenderSupplied) + + // get new file + resp, found = routerGetFile(t, router, rawUpload.ID) + require.Equal(t, http.StatusOK, resp.Code, resp.Body) + assert.Equal(t, rawUpload.ID, found.ID) + assert.Nil(t, found.FEDWireMessage.SenderSupplied) + + // get new file contents + resp = routerGetFileContents(t, router, rawUpload.ID) + require.Equal(t, http.StatusOK, resp.Code, resp.Body) + assert.NotNil(t, resp.Body) +} + +func setQueryParam(key, value string) func(values url.Values) url.Values { + return func(values url.Values) url.Values { + values.Set(key, value) + return values + } +} + +func routerUploadJSON(t *testing.T, router *mux.Router, file *wire.File) (*httptest.ResponseRecorder, *wire.File) { + bs, err := json.Marshal(file) + require.NoError(t, err) + + req := httptest.NewRequest("POST", "/files/create", bytes.NewReader(bs)) + req.Header.Set("content-type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + w.Flush() + + var resp *wire.File + if w.Code == http.StatusCreated { + require.NoError(t, json.NewDecoder(w.Body).Decode(&resp)) + } + return w, resp +} + +func routerUploadRaw(t *testing.T, router *mux.Router, raw io.Reader, queryOpts ...func(values url.Values) url.Values) (*httptest.ResponseRecorder, *wire.File) { + req := httptest.NewRequest("POST", "/files/create", raw) + req.Header.Set("content-type", "text/plain") + + query := req.URL.Query() + for _, opt := range queryOpts { + query = opt(query) + } + req.URL.RawQuery = query.Encode() + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + w.Flush() + + var resp *wire.File + if w.Code == http.StatusCreated { + _ = json.NewDecoder(w.Body).Decode(&resp) + } + return w, resp +} + +func routerGetFile(t *testing.T, router *mux.Router, id string) (*httptest.ResponseRecorder, *wire.File) { + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/files/"+id, nil) + + router.ServeHTTP(w, req) + w.Flush() + + var file *wire.File + if w.Code == http.StatusOK { + _ = json.NewDecoder(w.Body).Decode(&file) + } + return w, file +} + +func routerGetFileContents(t *testing.T, router *mux.Router, id string) *httptest.ResponseRecorder { + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/files/"+id+"/contents", nil) + + router.ServeHTTP(w, req) + w.Flush() + + return w +} + func TestFiles_getFile(t *testing.T) { req := httptest.NewRequest("GET", "/files/foo", nil) repo := &testWireFileRepository{ diff --git a/fedWireMessage.go b/fedWireMessage.go index f91e584b..ce3e358a 100644 --- a/fedWireMessage.go +++ b/fedWireMessage.go @@ -6,12 +6,6 @@ package wire import "strings" -// ValidateOpts contains specific overrides from the default set of validations -type ValidateOpts struct { - // SkipMandatoryIMAD skips checking that InputMessageAccountabilityData is mandatory tag. - SkipMandatoryIMAD bool `json:"skipMandatoryIMAD"` -} - // FEDWireMessage is a FedWire Message type FEDWireMessage struct { // ID @@ -141,11 +135,19 @@ type FEDWireMessage struct { ValidateOptions *ValidateOpts `json:"validateOptions,omitempty"` } +func (fwm *FEDWireMessage) requireSenderSupplied() bool { + opts := &ValidateOpts{} + if fwm != nil && fwm.ValidateOptions != nil { + opts = fwm.ValidateOptions + } + return !opts.AllowMissingSenderSupplied +} + // verify checks basic WIRE rules. Assumes properly parsed records. Each validation func should // check for the expected relationships between fields within a FedWireMessage. -func (fwm *FEDWireMessage) verify(isIncoming bool) error { +func (fwm *FEDWireMessage) verify() error { - if err := fwm.mandatoryFields(isIncoming); err != nil { + if err := fwm.mandatoryFields(); err != nil { return err } @@ -218,8 +220,8 @@ func (fwm *FEDWireMessage) verify(isIncoming bool) error { // // NOTE: Not specified mandatory elements in each incoming message // Need to specify mandatory elements in this case -func (fwm *FEDWireMessage) mandatoryFields(isIncoming bool) error { - if !isIncoming { +func (fwm *FEDWireMessage) mandatoryFields() error { + if fwm.requireSenderSupplied() { if err := fwm.validateSenderSupplied(); err != nil { return err } diff --git a/file.go b/file.go index a29ebaa9..3bc9178d 100644 --- a/file.go +++ b/file.go @@ -14,8 +14,6 @@ import ( type File struct { ID string `json:"id"` FEDWireMessage FEDWireMessage `json:"fedWireMessage"` - - isIncoming bool `json:"-"` } // NewFile constructs a file template @@ -61,7 +59,7 @@ func (f *File) Create() error { // Validate will never modify the file. func (f *File) Validate() error { - if err := f.FEDWireMessage.verify(f.isIncoming); err != nil { + if err := f.FEDWireMessage.verify(); err != nil { return err } return nil @@ -75,7 +73,7 @@ func (f *File) Validate() error { // be rejected by other Financial Institutions or ACH tools. func FileFromJSON(bs []byte) (*File, error) { if len(bs) == 0 { - //return nil, errors.New("no JSON data provided") + // return nil, errors.New("no JSON data provided") return nil, nil } @@ -88,16 +86,26 @@ func FileFromJSON(bs []byte) (*File, error) { type FilePropertyFunc func(*File) -// OutgoingFile specify that the file is for outgoing +// OutgoingFile configures the FedWireMessage ValidationOpts for an outgoing file func OutgoingFile() FilePropertyFunc { return func(f *File) { - f.isIncoming = false + if f != nil { + if f.FEDWireMessage.ValidateOptions == nil { + f.FEDWireMessage.ValidateOptions = &ValidateOpts{} + } + f.FEDWireMessage.ValidateOptions.AllowMissingSenderSupplied = false + } } } -// IncomingFile specify that the file is for incoming +// IncomingFile configures the FedWireMessage ValidationOpts for an incoming file func IncomingFile() FilePropertyFunc { return func(f *File) { - f.isIncoming = true + if f != nil { + if f.FEDWireMessage.ValidateOptions == nil { + f.FEDWireMessage.ValidateOptions = &ValidateOpts{} + } + f.FEDWireMessage.ValidateOptions.AllowMissingSenderSupplied = true + } } } diff --git a/go.mod b/go.mod index 88a7f2ab..4011a7d2 100644 --- a/go.mod +++ b/go.mod @@ -21,14 +21,12 @@ require ( github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/kr/text v0.2.0 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/rickar/cal/v2 v2.1.13 // indirect - golang.org/x/net v0.18.0 // indirect golang.org/x/sys v0.14.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.31.0 // indirect diff --git a/go.sum b/go.sum index d1b8c726..3c64a76d 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= @@ -16,50 +14,30 @@ github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= -github.com/moov-io/base v0.47.0 h1:r4PALYPSMwCB/smDUcMUk3a2JW6TA4mT17oOBx9ZpEE= -github.com/moov-io/base v0.47.0/go.mod h1:jz9m/WyTr6RTbm1DP0V5j4RVUuXqmq3bUnzJ6NwCPo8= -github.com/moov-io/base v0.47.1 h1:kQXBbTesyqb9ETem/jCeG2bYGuOpZYFbTHHVrwcpkrY= -github.com/moov-io/base v0.47.1/go.mod h1:AVMPYsHbPg+TxqOpfwIIHuT1rLsjxEhIekIKRj91i4I= -github.com/moov-io/base v0.48.1 h1:BVKjtEdR79ZjBeTF8Dl56ko43w3HaAnsUDhUoSVV9g4= -github.com/moov-io/base v0.48.1/go.mod h1:cFHUtEDjzxjc4SBjkntYNF4LB166iLN5eGR2BBSJenI= github.com/moov-io/base v0.48.2 h1:BPSNgmwokOVaVzAMJg71L48LCrDYelMfVXJEiZb2zOY= github.com/moov-io/base v0.48.2/go.mod h1:u1/WC3quR6otC9NrM1TtXSwNti1A/m7MR49RIXY1ee4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= -github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= -github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= -github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= -github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= -github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rickar/cal/v2 v2.1.13 h1:FENBPXxDPyL1OWGf9ZdpWGcEiGoSjt0UZED8VOxvK0c= @@ -71,18 +49,11 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= -golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= -golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -90,19 +61,14 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -110,8 +76,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= diff --git a/issue92_test.go b/issue92_test.go index 071a7abf..4734253c 100644 --- a/issue92_test.go +++ b/issue92_test.go @@ -8,11 +8,7 @@ import ( func TestFedWireMessage_verifyIssue92(t *testing.T) { fwm := issue92FedWireMessage() - require.NoError(t, fwm.verify(false)) - - fwm.SenderSupplied = nil - require.NoError(t, fwm.verify(true)) - require.Error(t, fwm.verify(false)) + require.NoError(t, fwm.verify()) } // this is the payload reported in issue 92 (bug in fwm validation) diff --git a/openapi.yaml b/openapi.yaml index c626b2c8..afc2e902 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -59,7 +59,10 @@ paths: post: tags: ['Wire Files'] summary: Create file - description: Create a new File object from either the plaintext or JSON representation. + description: > + Upload a new Wire file, or create one from JSON. When uploading a file, query parameters can be used to + configure the FedWireMessage validation options. For JSON requests, validation options are set in the + request body under fedWireMessage.validateOptions. operationId: createWireFile security: - bearerAuth: [] @@ -71,6 +74,22 @@ paths: example: rs4f9915 schema: type: string + - name: skipMandatoryIMAD + in: query + description: Optional flag to skip mandatory IMAD validation + required: false + schema: + type: boolean + default: false + example: true + - name: allowMissingSenderSupplied + in: query + description: Optional flag to allow SenderSupplied to be nil, which is generally the case in incoming files. + required: false + schema: + type: boolean + default: false + example: true requestBody: description: Content of the Wire file (in json or raw text) required: true @@ -471,6 +490,8 @@ components: $ref: '#/components/schemas/RemittanceFreeText' serviceMessage: $ref: '#/components/schemas/ServiceMessage' + validateOptions: + $ref: '#/components/schemas/ValidateOptions' required: - senderSupplied - typeSubType @@ -2032,3 +2053,16 @@ components: maxLength: 35 description: LineTwelve example: 'Line Twelve Text' + ValidateOptions: + nullable: true + properties: + skipMandatoryIMAD: + type: boolean + description: Skip validation of the InputMessageAccountabilityData (IMAD) field + default: false + example: true + allowMissingSenderSupplied: + type: boolean + description: Allow FedWireMessage.SenderSupplied to be nil + default: false + example: true diff --git a/reader.go b/reader.go index 1a7f9334..0eb31131 100644 --- a/reader.go +++ b/reader.go @@ -56,10 +56,10 @@ func (r *Reader) parseError(err error) error { } // NewReader returns a new ACH Reader that reads from r. -func NewReader(r io.Reader) *Reader { +func NewReader(r io.Reader, opts ...FilePropertyFunc) *Reader { reader := &Reader{ scanner: bufio.NewScanner(r), - File: *NewFile(IncomingFile()), + File: *NewFile(opts...), } reader.scanner.Split(scanLinesWithSegmentFormat) diff --git a/validate_opts.go b/validate_opts.go new file mode 100644 index 00000000..90a7ce69 --- /dev/null +++ b/validate_opts.go @@ -0,0 +1,10 @@ +package wire + +// ValidateOpts contains specific overrides from the default set of validations +type ValidateOpts struct { + // SkipMandatoryIMAD skips checking that InputMessageAccountabilityData is mandatory tag. + SkipMandatoryIMAD bool `json:"skipMandatoryIMAD"` + + // AllowMissingSenderSupplied allows the senderSupplied field to be omitted. + AllowMissingSenderSupplied bool `json:"allowMissingSenderSupplied"` +} diff --git a/writer.go b/writer.go index 255dcd28..604556e2 100644 --- a/writer.go +++ b/writer.go @@ -167,7 +167,9 @@ func (w *Writer) writeMandatory(fwm FEDWireMessage) error { return err } } else { - return fieldError("SenderSupplied", ErrFieldRequired) + if fwm.requireSenderSupplied() { + return fieldError("SenderSupplied", ErrFieldRequired) + } } if fwm.TypeSubType != nil {