github.com/moov-io/imagecashletter@v0.10.1/file.go (about) 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 imagecashletter 6 7 import ( 8 "bytes" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "strconv" 13 ) 14 15 // https://en.wikipedia.org/wiki/Substitute_check 16 // 17 // http://www.frbservices.org 18 // 19 // The Federal Reserve Banks uses the Accredited Standards Committee X9’s Specifications (X9.100-187–2016) for 20 // Electronic Exchange of Check and Image Data in providing its suite of Check 21 services. 21 // 22 // Record Types 23 const ( 24 fileHeaderPos = "01" 25 cashLetterHeaderPos = "10" 26 bundleHeaderPos = "20" 27 checkDetailPos = "25" 28 checkDetailAddendumAPos = "26" 29 checkDetailAddendumBPos = "27" 30 checkDetailAddendumCPos = "28" 31 returnDetailPos = "31" 32 returnAddendumAPos = "32" 33 returnAddendumBPos = "33" 34 returnAddendumCPos = "34" 35 returnAddendumDPos = "35" 36 imageViewDetailPos = "50" 37 imageViewDataPos = "52" 38 imageViewAnalysisPos = "54" 39 creditPos = "61" 40 creditItemPos = "62" 41 bundleControlPos = "70" 42 routingNumberSummaryPos = "85" 43 cashLetterControlPos = "90" 44 fileControlPos = "99" 45 // no longer supported by the standard 46 // accountTotalsDetailPos = "40" 47 // nonHitTotalsDetailPos = "41" 48 // boxSummaryPos = "75" 49 ) 50 51 // Record Types in EBCDIC 52 const ( 53 fileHeaderEbcPos = "\xF0\xF1" 54 cashLetterHeaderEbcPos = "\xF1\xF0" 55 bundleHeaderEbcPos = "\xF2\xF0" 56 checkDetailEbcPos = "\xF2\xF5" 57 checkDetailAddendumAEbcPos = "\xF2\xF6" 58 checkDetailAddendumBEbcPos = "\xF2\xF7" 59 checkDetailAddendumCEbcPos = "\xF2\xF8" 60 returnDetailEbcPos = "\xF3\xF1" 61 returnAddendumAPEbcos = "\xF3\xF2" 62 returnAddendumBEbcPos = "\xF3\xF3" 63 returnAddendumCEbcPos = "\xF3\xF4" 64 returnAddendumDEbcPos = "\xF3\xF5" 65 imageViewDetailEbcPos = "\xF5\xF0" 66 imageViewDataEbcPos = "\xF5\xF2" 67 imageViewAnalysisEbcPos = "\xF5\xF4" 68 creditEbcPos = "\xF6\xF1" 69 creditItemEbcPos = "\xF6\xF2" 70 bundleControlEbcPos = "\xF7\xF0" 71 routingNumberSummaryEbcPos = "\xF8\xF5" 72 cashLetterControlEbcPos = "\xF9\xF0" 73 fileControlEbcPos = "\xF9\xF9" 74 ) 75 76 // Errors strings specific to parsing a Batch container 77 var ( 78 msgRecordLength = "Must be at least 80 characters and found %d" 79 msgFileCashLetterInside = "Inside of current cash letter" 80 msgFileCashLetterControl = "Cash letter control without a current cash letter" 81 msgFileRoutingNumberSummary = "Routing Number Summary without a current cash letter" 82 msgFileBundleOutside = "Outside of current bundle" 83 msgFileBundleInside = "Inside of current bundle" 84 msgFileBundleControl = "Bundle control without a current bundle" 85 msgFileControl = "None or more than one file control exists" 86 msgFileHeader = "None or more than one file headers exists" 87 msgUnknownRecordType = "%s is an unknown record type" 88 msgFileCashLetterID = "%s is not unique" 89 msgRecordType = "received expecting %d" 90 msgFileCreditItem = "Credit item outside of cash letter" 91 msgFileCredit = "Credit outside of cash letter" 92 ) 93 94 // FileError is an error describing issues validating a file 95 type FileError struct { 96 FieldName string 97 Value string 98 Msg string 99 } 100 101 func (e *FileError) Error() string { 102 return fmt.Sprintf("%s %s", e.FieldName, e.Msg) 103 } 104 105 type FileRecord interface { 106 setRecordType() 107 String() string 108 } 109 110 // File is an imagecashletter file 111 type File struct { 112 // ID is a client defined string used as a reference to this record 113 ID string `json:"id"` 114 // FileHeader is an imagecashletter FileHeader 115 Header FileHeader `json:"fileHeader"` 116 // CashLetters are imagecashletter Cash Letters 117 CashLetters []CashLetter `json:"cashLetters,omitempty"` 118 // Bundles are imagecashletter Bundles 119 Bundles []Bundle `json:"bundle,omitempty"` 120 // FileControl is an imagecashletter FileControl 121 Control FileControl `json:"fileControl"` 122 } 123 124 // NewFile constructs a file template with a FileHeader and FileControl. 125 func NewFile() *File { 126 return &File{ 127 Header: NewFileHeader(), 128 Control: NewFileControl(), 129 } 130 } 131 132 type fileHeader struct { 133 Header FileHeader `json:"fileHeader"` 134 } 135 136 type fileControl struct { 137 Control FileControl `json:"fileControl"` 138 } 139 140 // FileFromJSON attempts to return a *File object assuming the input is valid JSON. 141 // 142 // Callers should always check for a nil-error before using the returned file. 143 // 144 // The File returned may not be valid and callers should confirm with Validate(). 145 // Invalid files may be rejected by other Financial Institutions or ICL tools. 146 func FileFromJSON(bs []byte) (*File, error) { 147 if len(bs) == 0 { 148 return nil, errors.New("no JSON data provided") 149 } 150 151 // read any root level fields 152 var f File 153 file := NewFile() 154 if err := json.NewDecoder(bytes.NewReader(bs)).Decode(&f); err != nil { 155 return nil, fmt.Errorf("problem reading file: %v", err) 156 } 157 file.ID = f.ID 158 file.CashLetters = f.CashLetters 159 file.Bundles = f.Bundles 160 161 // read the FileHeader 162 header := fileHeader{ 163 Header: file.Header, 164 } 165 if err := json.NewDecoder(bytes.NewReader(bs)).Decode(&header); err != nil { 166 return nil, fmt.Errorf("problem reading FileHeader: %v", err) 167 } 168 file.Header = header.Header 169 170 // read file control 171 control := fileControl{ 172 Control: NewFileControl(), 173 } 174 if err := json.NewDecoder(bytes.NewReader(bs)).Decode(&control); err != nil { 175 return nil, fmt.Errorf("problem reading FileControl: %v", err) 176 } 177 file.Control = control.Control 178 179 file.setRecordTypes() 180 181 if err := file.Create(); err != nil { 182 return file, err 183 } 184 if err := file.Validate(); err != nil { 185 return file, err 186 } 187 return file, nil 188 } 189 190 // Create creates a valid imagecashletter File 191 func (f *File) Create() error { 192 if f == nil { 193 return ErrNilFile 194 } 195 // Requires a valid FileHeader to build FileControl 196 if err := f.Header.Validate(); err != nil { 197 return err 198 } 199 200 if len(f.CashLetters) <= 0 { 201 return &FileError{FieldName: "CashLetters", Value: strconv.Itoa(len(f.CashLetters)), Msg: "must have []*CashLetters to be built"} 202 } 203 204 // File Control Counts 205 fileCashLetterCount := len(f.CashLetters) 206 // add 2 for FileHeader/control and reset if build was called twice do to error 207 fileTotalRecordCount := 2 208 fileTotalItemCount := 0 209 fileTotalAmount := 0 210 creditIndicator := 0 211 212 // CashLetters 213 for _, cl := range f.CashLetters { 214 // Validate CashLetter 215 if err := cl.Validate(); err != nil { 216 return err 217 } 218 // add 2 for each cashletter header/control 219 fileTotalRecordCount = fileTotalRecordCount + 2 220 221 if len(cl.GetCreditItems()) > 0 { 222 fileTotalRecordCount = fileTotalRecordCount + len(cl.GetCreditItems()) 223 creditIndicator = 1 224 } 225 226 // Bundles 227 for _, b := range cl.Bundles { 228 // Validate Bundle 229 if err := b.Validate(); err != nil { 230 return err 231 } 232 233 // add 2 for each bundle header/control 234 fileTotalRecordCount = fileTotalRecordCount + 2 235 236 // Check Items 237 for _, cd := range b.Checks { 238 fileTotalItemCount = fileTotalItemCount + 1 239 240 fileTotalRecordCount = fileTotalRecordCount + 1 241 fileTotalRecordCount = fileTotalRecordCount + len(cd.CheckDetailAddendumA) + len(cd.CheckDetailAddendumB) + len(cd.CheckDetailAddendumC) 242 fileTotalRecordCount = fileTotalRecordCount + len(cd.ImageViewDetail) + len(cd.ImageViewData) + len(cd.ImageViewAnalysis) 243 244 fileTotalAmount = fileTotalAmount + cd.ItemAmount 245 } 246 // Returns Items 247 for _, rd := range b.Returns { 248 fileTotalItemCount = fileTotalItemCount + 1 249 250 fileTotalRecordCount = fileTotalRecordCount + 1 251 fileTotalRecordCount = fileTotalRecordCount + len(rd.ReturnDetailAddendumA) + len(rd.ReturnDetailAddendumB) + len(rd.ReturnDetailAddendumC) + len(rd.ReturnDetailAddendumD) 252 fileTotalRecordCount = fileTotalRecordCount + len(rd.ImageViewDetail) + len(rd.ImageViewData) + len(rd.ImageViewAnalysis) 253 254 fileTotalAmount = fileTotalAmount + rd.ItemAmount 255 } 256 } 257 } 258 259 // create FileControl from calculated values 260 fc := NewFileControl() 261 fc.CashLetterCount = fileCashLetterCount 262 fc.TotalRecordCount = fileTotalRecordCount 263 fc.TotalItemCount = fileTotalItemCount 264 fc.FileTotalAmount = fileTotalAmount 265 fc.ImmediateOriginContactName = f.Control.ImmediateOriginContactName 266 fc.ImmediateOriginContactPhoneNumber = f.Control.ImmediateOriginContactPhoneNumber 267 fc.CreditTotalIndicator = creditIndicator 268 f.Control = fc 269 return nil 270 } 271 272 // Validate validates an ICL File 273 func (f *File) Validate() error { 274 if f == nil { 275 return ErrNilFile 276 } 277 if err := f.CashLetterIDUnique(); err != nil { 278 return err 279 } 280 return nil 281 } 282 283 // SetHeader allows for header to be built. 284 func (f *File) SetHeader(h FileHeader) *File { 285 f.Header = h 286 return f 287 } 288 289 // AddCashLetter appends a CashLetter to the imagecashletter.File 290 func (f *File) AddCashLetter(cashLetter CashLetter) []CashLetter { 291 f.CashLetters = append(f.CashLetters, cashLetter) 292 return f.CashLetters 293 } 294 295 // CashLetterIDUnique verifies multiple CashLetters in a file have a unique CashLetterID 296 func (f *File) CashLetterIDUnique() error { 297 if f == nil || len(f.CashLetters) == 0 { 298 return ErrNilFile 299 } 300 cashLetterID := "" 301 for _, cl := range f.CashLetters { 302 if cl.CashLetterHeader == nil { 303 continue 304 } 305 if cashLetterID == cl.CashLetterHeader.CashLetterID { 306 msg := fmt.Sprintf(msgFileCashLetterID, cashLetterID) 307 return &FileError{FieldName: "CashLetterID", Value: cl.CashLetterHeader.CashLetterID, Msg: msg} 308 } 309 cashLetterID = cl.CashLetterHeader.CashLetterID 310 } 311 return nil 312 } 313 314 func (f *File) setRecordTypes() { 315 if f == nil { 316 return 317 } 318 319 f.Header.setRecordType() 320 for i := range f.CashLetters { 321 f.CashLetters[i].setRecordType() 322 } 323 for i := range f.Bundles { 324 f.Bundles[i].setRecordType() 325 } 326 f.Control.setRecordType() 327 }