github.com/moov-io/imagecashletter@v0.10.1/reader.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 "bufio" 9 "encoding/binary" 10 "errors" 11 "fmt" 12 "io" 13 "strconv" 14 15 "github.com/gdamore/encoding" 16 ) 17 18 // ParseError is returned for parsing reader errors. 19 // The first line is 1. 20 type ParseError struct { 21 Line int // Line number where the error occurred 22 Record string // Name of the record type being parsed 23 Err error // The actual error 24 } 25 26 func (e *ParseError) Error() string { 27 if e.Record == "" { 28 return fmt.Sprintf("line:%d %T %s", e.Line, e.Err, e.Err) 29 } 30 return fmt.Sprintf("line:%d record:%s %T %s", e.Line, e.Record, e.Err, e.Err) 31 } 32 33 func (e *ParseError) Unwrap() error { 34 return e.Err 35 } 36 37 // Reader reads records from a ACH-encoded file. 38 type Reader struct { 39 // r handles the IO.Reader sent to be parser. 40 scanner *bufio.Scanner 41 // file is ach.file model being built as r is parsed. 42 File File 43 // func used to decode line to desired encoding ie. ASCII,EBCDIC 44 decodeLine DecodeLineFn 45 // line is the current line being parsed from the input r 46 line string 47 // currentCashLetter is the current CashLetter being parsed 48 currentCashLetter CashLetter 49 // line number of the file being parsed 50 lineNum int 51 // recordName holds the current record name being parsed. 52 recordName string 53 } 54 55 // error creates a new ParseError based on err. 56 func (r *Reader) error(err error) error { 57 return &ParseError{ 58 Line: r.lineNum, 59 Record: r.recordName, 60 Err: err, 61 } 62 } 63 64 // addCurrentCashLetter creates the current cash letter for the file being read. A successful 65 // currentCashLetter will be added to r.File once parsed. 66 func (r *Reader) addCurrentCashLetter(cashLetter CashLetter) { 67 r.currentCashLetter = cashLetter 68 } 69 70 // addCurrentBundle creates the CurrentBundle for the file being read. A successful 71 // currentBundle will be added to r.File once parsed. 72 func (r *Reader) addCurrentBundle(bundle *Bundle) { 73 r.currentCashLetter.currentBundle = bundle 74 } 75 76 // addCurrentRoutingNumberSummary creates the CurrentRoutingNumberSummary for the file being read. A successful 77 // currentRoutingNumberSummary will be added to r.File once parsed. 78 func (r *Reader) addCurrentRoutingNumberSummary(rns *RoutingNumberSummary) { 79 r.currentCashLetter.currentRoutingNumberSummary = rns 80 } 81 82 // NewReader returns a new ACH Reader that reads from r. 83 func NewReader(r io.Reader, opts ...ReaderOption) *Reader { 84 f := NewFile() 85 f.Control = FileControl{} 86 reader := &Reader{ 87 File: *f, 88 scanner: bufio.NewScanner(r), 89 decodeLine: Passthrough, 90 } 91 for _, opt := range opts { 92 opt(reader) 93 } 94 return reader 95 } 96 97 // DecodeLineFn is used to decode a scanned line into desired encoding. 98 // Depending on X9 spec, cashletter could be encoded as ASCII or EBCDIC 99 type DecodeLineFn func(lineIn string) (lineOut string, err error) 100 101 // Passthrough will return line as is 102 func Passthrough(lineIn string) (lineOut string, err error) { 103 return lineIn, nil 104 } 105 106 // DecodeEBCDIC will decode a line from EBCDIC-0037 to UTF-8 107 func DecodeEBCDIC(lineIn string) (string, error) { 108 lineOut, err := encoding.EBCDIC.NewDecoder().String(lineIn) 109 if err != nil { 110 return "", fmt.Errorf("error decoding '%X' as EBCDIC: %v\n", lineIn, err) 111 } 112 return lineOut, nil 113 } 114 115 // ReaderOption can be used to change default behavior of Reader 116 type ReaderOption func(*Reader) 117 118 // ReadVariableLineLengthOption allows Reader to split imagecashletter files based on encoded line lengths 119 func ReadVariableLineLengthOption() ReaderOption { 120 scanVariableLengthLines := func(data []byte, atEOF bool) (advance int, token []byte, err error) { 121 if atEOF && len(data) == 0 { 122 return 0, nil, nil 123 } else if len(data) < 4 && atEOF { 124 // we ran out of bytes and we're at the end of the file 125 return 0, nil, io.ErrUnexpectedEOF 126 } else if len(data) < 4 { 127 // we need at least the control bytes 128 return 0, nil, nil 129 } 130 // line length can be variable 131 // use the 4 control bytes at the beginning of a line to determine its length 132 ctrl := data[0:4] 133 dataLen := int(binary.BigEndian.Uint32(ctrl)) 134 lineLen := 4 + dataLen 135 if lineLen <= len(data) { 136 // return line while accounting for control bytes 137 return lineLen, data[4:lineLen], nil 138 } else if lineLen > len(data) && atEOF { 139 // we need more data, but there is no more data to read 140 return 0, nil, io.ErrUnexpectedEOF 141 } 142 // request more data. 143 return 0, nil, nil 144 } 145 146 return func(r *Reader) { 147 r.scanner.Split(scanVariableLengthLines) 148 } 149 } 150 151 // ReadEbcdicEncodingOption allows Reader to decode scanned lines from EBCDIC to UTF-8 152 func ReadEbcdicEncodingOption() ReaderOption { 153 return func(r *Reader) { 154 r.decodeLine = DecodeEBCDIC 155 } 156 } 157 158 // BufferSizeOption creates a byte slice of the specified size and uses it as the buffer 159 // for the Reader's internal scanner. You may need to set this when processing files that 160 // contain check details exceeding bufio.MaxScanTokenSize (64 kB). 161 func BufferSizeOption(size int) ReaderOption { 162 return func(r *Reader) { 163 r.scanner.Buffer(make([]byte, size), size) 164 } 165 } 166 167 // Read reads each line of the imagecashletter file and defines which parser to use based 168 // on the first character of each line. It also enforces imagecashletter formatting rules and returns 169 // the appropriate error if issues are found. 170 func (r *Reader) Read() (File, error) { 171 r.lineNum = 0 172 // read through the entire file 173 for r.scanner.Scan() { 174 r.line = r.scanner.Text() 175 r.lineNum++ 176 177 lineLength := len(r.line) 178 179 if lineLength < 80 { 180 msg := fmt.Sprintf(msgRecordLength, lineLength) 181 err := &FileError{FieldName: "RecordLength", Value: strconv.Itoa(lineLength), Msg: msg} 182 return r.File, r.error(err) 183 } 184 if err := r.parseLine(); err != nil { 185 return r.File, err 186 } 187 } 188 189 if scanErr := r.scanner.Err(); scanErr != nil { 190 err := &FileError{FieldName: "LineNumber", Value: strconv.Itoa(r.lineNum), Msg: scanErr.Error()} 191 return r.File, r.error(err) 192 } 193 194 if (FileHeader{}) == r.File.Header { 195 // There must be at least one File Header 196 r.recordName = "FileHeader" 197 return r.File, r.error(&FileError{Msg: msgFileHeader}) 198 } 199 if (FileControl{}) == r.File.Control { 200 // There must be at least one File Control 201 r.recordName = "FileControl" 202 return r.File, r.error(&FileError{Msg: msgFileControl}) 203 } 204 return r.File, nil 205 } 206 207 func (r *Reader) parseLine() error { //nolint:gocyclo 208 switch r.line[:2] { 209 case fileHeaderPos, fileHeaderEbcPos: 210 if err := r.parseFileHeader(); err != nil { 211 return err 212 } 213 case cashLetterHeaderPos, cashLetterHeaderEbcPos: 214 if err := r.parseCashLetterHeader(); err != nil { 215 return err 216 } 217 case bundleHeaderPos, bundleHeaderEbcPos: 218 if err := r.parseBundleHeader(); err != nil { 219 return err 220 } 221 case checkDetailPos, checkDetailEbcPos: 222 if err := r.parseCheckDetail(); err != nil { 223 return err 224 } 225 case checkDetailAddendumAPos, checkDetailAddendumAEbcPos: 226 if err := r.parseCheckDetailAddendumA(); err != nil { 227 return err 228 } 229 case checkDetailAddendumBPos, checkDetailAddendumBEbcPos: 230 if err := r.parseCheckDetailAddendumB(); err != nil { 231 return err 232 } 233 case checkDetailAddendumCPos, checkDetailAddendumCEbcPos: 234 if err := r.parseCheckDetailAddendumC(); err != nil { 235 return err 236 } 237 case imageViewDetailPos, imageViewDetailEbcPos: 238 if err := r.parseImageViewDetail(); err != nil { 239 return err 240 } 241 case imageViewDataPos, imageViewDataEbcPos: 242 if err := r.parseImageViewData(); err != nil { 243 return err 244 } 245 case imageViewAnalysisPos, imageViewAnalysisEbcPos: 246 if err := r.parseImageViewAnalysis(); err != nil { 247 return err 248 } 249 case returnDetailPos, returnDetailEbcPos: 250 if err := r.parseReturnDetail(); err != nil { 251 return err 252 } 253 case returnAddendumAPos, returnAddendumAPEbcos: 254 if err := r.parseReturnDetailAddendumA(); err != nil { 255 return err 256 } 257 case returnAddendumBPos, returnAddendumBEbcPos: 258 if err := r.parseReturnDetailAddendumB(); err != nil { 259 return err 260 } 261 case returnAddendumCPos, returnAddendumCEbcPos: 262 if err := r.parseReturnDetailAddendumC(); err != nil { 263 return err 264 } 265 case returnAddendumDPos, returnAddendumDEbcPos: 266 if err := r.parseReturnDetailAddendumD(); err != nil { 267 return err 268 } 269 case creditPos, creditEbcPos: 270 if err := r.parseCredit(); err != nil { 271 return err 272 } 273 case creditItemPos, creditItemEbcPos: 274 if err := r.parseCreditItem(); err != nil { 275 return err 276 } 277 case bundleControlPos, bundleControlEbcPos: 278 if err := r.parseBundleControl(); err != nil { 279 return err 280 } 281 if r.currentCashLetter.currentBundle == nil { 282 r.error(&FileError{Msg: msgFileBundleControl}) 283 } 284 // Add Bundle or ReturnBundle to CashLetter 285 if r.currentCashLetter.currentBundle != nil { 286 if err := r.currentCashLetter.currentBundle.Validate(); err != nil { 287 r.recordName = "Bundles" 288 return r.error(err) 289 } 290 r.currentCashLetter.AddBundle(r.currentCashLetter.currentBundle) 291 r.currentCashLetter.currentBundle = new(Bundle) 292 } 293 case routingNumberSummaryPos, routingNumberSummaryEbcPos: 294 if err := r.parseRoutingNumberSummary(); err != nil { 295 return err 296 } 297 r.currentCashLetter.AddRoutingNumberSummary(r.currentCashLetter.currentRoutingNumberSummary) 298 r.currentCashLetter.currentRoutingNumberSummary = new(RoutingNumberSummary) 299 case cashLetterControlPos, cashLetterControlEbcPos: 300 // This is needed for validation od CashLetterControl since SettlementDate 301 // is a conditional field and is only available for certain types of CashLetters. 302 header := r.currentCashLetter.CashLetterHeader 303 if header == nil { 304 return errors.New("missing CashLetterHeader") 305 } 306 if err := r.parseCashLetterControl(); err != nil { 307 return err 308 } 309 if err := r.currentCashLetter.Validate(); err != nil { 310 r.recordName = "CashLetters" 311 return r.error(err) 312 } 313 r.File.AddCashLetter(r.currentCashLetter) 314 r.currentCashLetter = CashLetter{} 315 case fileControlPos, fileControlEbcPos: 316 if err := r.parseFileControl(); err != nil { 317 return err 318 } 319 default: 320 msg := fmt.Sprintf(msgUnknownRecordType, r.line[:2]) 321 return r.error(&FileError{FieldName: "recordType", Value: r.line[:2], Msg: msg}) 322 } 323 return nil 324 } 325 326 // parseFileHeader takes the input record string and parses the FileHeader values 327 func (r *Reader) parseFileHeader() error { 328 r.recordName = "FileHeader" 329 if (FileHeader{}) != r.File.Header { 330 // There can only be one File Header per File 331 r.error(&FileError{Msg: msgFileHeader}) 332 } 333 lineOut, err := r.decodeLine(r.line) 334 if err != nil { 335 return err 336 } 337 r.File.Header.Parse(lineOut) 338 // Ensure valid FileHeader 339 if err := r.File.Header.Validate(); err != nil { 340 return r.error(err) 341 } 342 return nil 343 } 344 345 // parseCashLetterHeader takes the input record string and parses the CashLetterHeader values 346 func (r *Reader) parseCashLetterHeader() error { 347 r.recordName = "CashLetterHeader" 348 if r.currentCashLetter.CashLetterHeader != nil { 349 // CashLetterHeader inside of current cash letter 350 return r.error(&FileError{Msg: msgFileCashLetterInside}) 351 } 352 lineOut, err := r.decodeLine(r.line) 353 if err != nil { 354 return err 355 } 356 clh := NewCashLetterHeader() 357 clh.Parse(lineOut) 358 // Ensure we have a valid CashLetterHeader 359 if err := clh.Validate(); err != nil { 360 return r.error(err) 361 } 362 // Passing CashLetterHeader into NewCashLetter creates a CashLetter 363 cl := NewCashLetter(clh) 364 r.addCurrentCashLetter(cl) 365 return nil 366 } 367 368 // parseBundleHeader takes the input record string and parses the BundleHeader values 369 func (r *Reader) parseBundleHeader() error { 370 r.recordName = "BundleHeader" 371 if r.currentCashLetter.currentBundle != nil { 372 // BundleHeader inside of current Bundle 373 if r.currentCashLetter.currentBundle.BundleHeader != nil { 374 return r.error(&FileError{Msg: msgFileBundleInside}) 375 } 376 } 377 // Ensure we have a valid bundle header before building a bundle. 378 lineOut, err := r.decodeLine(r.line) 379 if err != nil { 380 return err 381 } 382 bh := NewBundleHeader() 383 bh.Parse(lineOut) 384 if err := bh.Validate(); err != nil { 385 return r.error(err) 386 } 387 // Passing BundleHeader into NewBundle creates a Bundle 388 bundle := NewBundle(bh) 389 r.addCurrentBundle(bundle) 390 return nil 391 392 } 393 394 // parseCheckDetail takes the input record string and parses the CheckDetail values 395 func (r *Reader) parseCheckDetail() error { 396 r.recordName = "CheckDetail" 397 if r.currentCashLetter.currentBundle == nil { 398 return r.error(&FileError{Msg: msgFileBundleOutside}) 399 } 400 lineOut, err := r.decodeLine(r.line) 401 if err != nil { 402 return err 403 } 404 cd := new(CheckDetail) 405 cd.Parse(lineOut) 406 // Ensure valid CheckDetail 407 if err := cd.Validate(); err != nil { 408 return r.error(err) 409 } 410 // Add CheckDetail 411 if r.currentCashLetter.currentBundle.BundleHeader != nil { 412 r.currentCashLetter.currentBundle.AddCheckDetail(cd) 413 } 414 return nil 415 } 416 417 // parseCheckDetailAddendumA takes the input record string and parses the CheckDetailAddendumA values 418 func (r *Reader) parseCheckDetailAddendumA() error { 419 r.recordName = "CheckDetailAddendumA" 420 if r.currentCashLetter.currentBundle.GetChecks() == nil { 421 msg := fmt.Sprint(msgFileBundleOutside) 422 return r.error(&FileError{FieldName: "CheckDetailAddendumA", Msg: msg}) 423 } 424 lineOut, err := r.decodeLine(r.line) 425 if err != nil { 426 return err 427 } 428 cdAddendumA := NewCheckDetailAddendumA() 429 cdAddendumA.Parse(lineOut) 430 if err := cdAddendumA.Validate(); err != nil { 431 return r.error(err) 432 } 433 entryIndex := len(r.currentCashLetter.currentBundle.GetChecks()) - 1 434 // r.currentCashLetter.currentBundle.Checks[entryIndex].CheckDetailAddendumA = cdAddendumA 435 r.currentCashLetter.currentBundle.Checks[entryIndex].AddCheckDetailAddendumA(cdAddendumA) 436 return nil 437 } 438 439 // parseCheckDetailAddendumB takes the input record string and parses the CheckDetailAddendumB values 440 func (r *Reader) parseCheckDetailAddendumB() error { 441 r.recordName = "CheckDetailAddendumB" 442 if r.currentCashLetter.currentBundle.GetChecks() == nil { 443 msg := fmt.Sprint(msgFileBundleOutside) 444 return r.error(&FileError{FieldName: "CheckDetailAddendumB", Msg: msg}) 445 } 446 lineOut, err := r.decodeLine(r.line) 447 if err != nil { 448 return err 449 } 450 cdAddendumB := NewCheckDetailAddendumB() 451 cdAddendumB.Parse(lineOut) 452 if err := cdAddendumB.Validate(); err != nil { 453 return r.error(err) 454 } 455 entryIndex := len(r.currentCashLetter.currentBundle.GetChecks()) - 1 456 r.currentCashLetter.currentBundle.Checks[entryIndex].AddCheckDetailAddendumB(cdAddendumB) 457 return nil 458 } 459 460 // parseCheckDetailAddendumC takes the input record string and parses the CheckDetailAddendumC values 461 func (r *Reader) parseCheckDetailAddendumC() error { 462 r.recordName = "CheckDetailAddendumC" 463 if r.currentCashLetter.currentBundle.GetChecks() == nil { 464 msg := fmt.Sprint(msgFileBundleOutside) 465 return r.error(&FileError{FieldName: "CheckDetailAddendumC", Msg: msg}) 466 } 467 lineOut, err := r.decodeLine(r.line) 468 if err != nil { 469 return err 470 } 471 cdAddendumC := NewCheckDetailAddendumC() 472 cdAddendumC.Parse(lineOut) 473 if err := cdAddendumC.Validate(); err != nil { 474 return r.error(err) 475 } 476 entryIndex := len(r.currentCashLetter.currentBundle.GetChecks()) - 1 477 r.currentCashLetter.currentBundle.Checks[entryIndex].AddCheckDetailAddendumC(cdAddendumC) 478 return nil 479 } 480 481 // parseReturnDetail takes the input record string and parses the ReturnDetail values 482 func (r *Reader) parseReturnDetail() error { 483 r.recordName = "ReturnDetail" 484 if r.currentCashLetter.currentBundle == nil { 485 return r.error(&FileError{Msg: msgFileBundleOutside}) 486 } 487 lineOut, err := r.decodeLine(r.line) 488 if err != nil { 489 return err 490 } 491 rd := new(ReturnDetail) 492 rd.Parse(lineOut) 493 if err := rd.Validate(); err != nil { 494 return r.error(err) 495 } 496 if r.currentCashLetter.currentBundle.BundleHeader != nil { 497 r.currentCashLetter.currentBundle.AddReturnDetail(rd) 498 } 499 return nil 500 } 501 502 // parseReturnDetailAddendumA takes the input record string and parses the ReturnDetailAddendumA values 503 func (r *Reader) parseReturnDetailAddendumA() error { 504 r.recordName = "ReturnDetailAddendumA" 505 if r.currentCashLetter.currentBundle.GetReturns() == nil { 506 msg := fmt.Sprint(msgFileBundleOutside) 507 return r.error(&FileError{FieldName: "ReturnDetailAddendumA", Msg: msg}) 508 } 509 lineOut, err := r.decodeLine(r.line) 510 if err != nil { 511 return err 512 } 513 rdAddendumA := NewReturnDetailAddendumA() 514 rdAddendumA.Parse(lineOut) 515 if err := rdAddendumA.Validate(); err != nil { 516 return r.error(err) 517 } 518 entryIndex := len(r.currentCashLetter.currentBundle.GetReturns()) - 1 519 // r.currentCashLetter.currentBundle.Returns[entryIndex].ReturnDetailAddendumA = rdAddendumA 520 r.currentCashLetter.currentBundle.Returns[entryIndex].AddReturnDetailAddendumA(rdAddendumA) 521 return nil 522 } 523 524 // parseReturnDetailAddendumB takes the input record string and parses the ReturnDetailAddendumB values 525 func (r *Reader) parseReturnDetailAddendumB() error { 526 r.recordName = "ReturnDetailAddendumB" 527 if r.currentCashLetter.currentBundle.GetReturns() == nil { 528 msg := fmt.Sprint(msgFileBundleOutside) 529 return r.error(&FileError{FieldName: "ReturnDetailAddendumB", Msg: msg}) 530 } 531 lineOut, err := r.decodeLine(r.line) 532 if err != nil { 533 return err 534 } 535 rdAddendumB := NewReturnDetailAddendumB() 536 rdAddendumB.Parse(lineOut) 537 if err := rdAddendumB.Validate(); err != nil { 538 return r.error(err) 539 } 540 entryIndex := len(r.currentCashLetter.currentBundle.GetReturns()) - 1 541 r.currentCashLetter.currentBundle.Returns[entryIndex].AddReturnDetailAddendumB(rdAddendumB) 542 return nil 543 } 544 545 // parseReturnDetailAddendumC takes the input record string and parses the ReturnDetailAddendumC values 546 func (r *Reader) parseReturnDetailAddendumC() error { 547 r.recordName = "ReturnDetailAddendumC" 548 if r.currentCashLetter.currentBundle.GetReturns() == nil { 549 msg := fmt.Sprint(msgFileBundleOutside) 550 return r.error(&FileError{FieldName: "ReturnDetailAddendumC", Msg: msg}) 551 } 552 lineOut, err := r.decodeLine(r.line) 553 if err != nil { 554 return err 555 } 556 rdAddendumC := NewReturnDetailAddendumC() 557 rdAddendumC.Parse(lineOut) 558 if err := rdAddendumC.Validate(); err != nil { 559 return r.error(err) 560 } 561 entryIndex := len(r.currentCashLetter.currentBundle.GetReturns()) - 1 562 r.currentCashLetter.currentBundle.Returns[entryIndex].AddReturnDetailAddendumC(rdAddendumC) 563 return nil 564 } 565 566 // parseReturnDetail*AddendumD takes the input record string and parses the ReturnDetail*AddendumD values 567 func (r *Reader) parseReturnDetailAddendumD() error { 568 r.recordName = "ReturnDetailAddendumD" 569 570 if r.currentCashLetter.currentBundle.GetReturns() == nil { 571 msg := fmt.Sprint(msgFileBundleOutside) 572 return r.error(&FileError{FieldName: "ReturnDetailAddendumD", Msg: msg}) 573 } 574 lineOut, err := r.decodeLine(r.line) 575 if err != nil { 576 return err 577 } 578 rdAddendumD := NewReturnDetailAddendumD() 579 rdAddendumD.Parse(lineOut) 580 if err := rdAddendumD.Validate(); err != nil { 581 return r.error(err) 582 } 583 entryIndex := len(r.currentCashLetter.currentBundle.GetReturns()) - 1 584 r.currentCashLetter.currentBundle.Returns[entryIndex].AddReturnDetailAddendumD(rdAddendumD) 585 return nil 586 } 587 588 // parseImageViewDetail takes the input record string and parses the ImageViewDetail values 589 func (r *Reader) parseImageViewDetail() error { 590 r.recordName = "ImageViewDetail" 591 if err := r.ImageViewDetail(); err != nil { 592 return err 593 } 594 return nil 595 } 596 597 // ImageViewDetail takes the input record string and parses ImageViewDetail for a check 598 func (r *Reader) ImageViewDetail() error { 599 if r.currentCashLetter.currentBundle.GetChecks() != nil { 600 lineOut, err := r.decodeLine(r.line) 601 if err != nil { 602 return err 603 } 604 ivDetail := NewImageViewDetail() 605 ivDetail.Parse(lineOut) 606 if err := ivDetail.Validate(); err != nil { 607 return r.error(err) 608 } 609 entryIndex := len(r.currentCashLetter.currentBundle.GetChecks()) - 1 610 r.currentCashLetter.currentBundle.Checks[entryIndex].AddImageViewDetail(ivDetail) 611 612 } else if r.currentCashLetter.currentBundle.GetReturns() != nil { 613 lineOut, err := r.decodeLine(r.line) 614 if err != nil { 615 return err 616 } 617 ivDetail := NewImageViewDetail() 618 ivDetail.Parse(lineOut) 619 if err := ivDetail.Validate(); err != nil { 620 return r.error(err) 621 } 622 entryIndex := len(r.currentCashLetter.currentBundle.GetReturns()) - 1 623 r.currentCashLetter.currentBundle.Returns[entryIndex].AddImageViewDetail(ivDetail) 624 } else { 625 msg := fmt.Sprint(msgFileBundleOutside) 626 return r.error(&FileError{FieldName: "ImageViewDetail", Msg: msg}) 627 } 628 629 return nil 630 } 631 632 // parseImageViewData takes the input record string and parses the ImageViewData values 633 func (r *Reader) parseImageViewData() error { 634 r.recordName = "ImageViewData" 635 if err := r.ImageViewData(); err != nil { 636 return err 637 } 638 return nil 639 } 640 641 // ImageViewData takes the input record string and parses ImageViewData for a check 642 func (r *Reader) ImageViewData() error { 643 if r.currentCashLetter.currentBundle.GetChecks() != nil { 644 ivData := NewImageViewData() 645 ivData.ParseAndDecode(r.line, r.decodeLine) 646 if err := ivData.Validate(); err != nil { 647 return r.error(err) 648 } 649 entryIndex := len(r.currentCashLetter.currentBundle.GetChecks()) - 1 650 r.currentCashLetter.currentBundle.Checks[entryIndex].AddImageViewData(ivData) 651 652 } else if r.currentCashLetter.currentBundle.GetReturns() != nil { 653 ivData := NewImageViewData() 654 ivData.ParseAndDecode(r.line, r.decodeLine) 655 if err := ivData.Validate(); err != nil { 656 return r.error(err) 657 } 658 entryIndex := len(r.currentCashLetter.currentBundle.GetReturns()) - 1 659 r.currentCashLetter.currentBundle.Returns[entryIndex].AddImageViewData(ivData) 660 } else { 661 msg := fmt.Sprint(msgFileBundleOutside) 662 return r.error(&FileError{FieldName: "ImageViewData", Msg: msg}) 663 } 664 665 return nil 666 } 667 668 // parseImageViewAnalysis takes the input record string and parses ImageViewAnalysis values 669 func (r *Reader) parseImageViewAnalysis() error { 670 r.recordName = "ImageViewAnalysis" 671 if err := r.ImageViewAnalysis(); err != nil { 672 return err 673 } 674 return nil 675 } 676 677 // ImageViewAnalysis takes the input record string and parses ImageViewAnalysis for a check 678 func (r *Reader) ImageViewAnalysis() error { 679 if r.currentCashLetter.currentBundle.GetChecks() != nil { 680 lineOut, err := r.decodeLine(r.line) 681 if err != nil { 682 return err 683 } 684 ivAnalysis := NewImageViewAnalysis() 685 ivAnalysis.Parse(lineOut) 686 if err := ivAnalysis.Validate(); err != nil { 687 return r.error(err) 688 } 689 entryIndex := len(r.currentCashLetter.currentBundle.GetChecks()) - 1 690 r.currentCashLetter.currentBundle.Checks[entryIndex].AddImageViewAnalysis(ivAnalysis) 691 692 } else if r.currentCashLetter.currentBundle.GetReturns() != nil { 693 lineOut, err := r.decodeLine(r.line) 694 if err != nil { 695 return err 696 } 697 ivAnalysis := NewImageViewAnalysis() 698 ivAnalysis.Parse(lineOut) 699 if err := ivAnalysis.Validate(); err != nil { 700 return r.error(err) 701 } 702 entryIndex := len(r.currentCashLetter.currentBundle.GetReturns()) - 1 703 r.currentCashLetter.currentBundle.Returns[entryIndex].AddImageViewAnalysis(ivAnalysis) 704 } else { 705 msg := fmt.Sprint(msgFileBundleOutside) 706 return r.error(&FileError{FieldName: "ImageViewAnalysis", Msg: msg}) 707 } 708 709 return nil 710 } 711 712 // parseCredit takes the input record string and parses the Credit values 713 func (r *Reader) parseCredit() error { 714 // Current implementation has the credit letter outside the bundle but within the cash letter 715 r.recordName = "Credit" 716 if r.currentCashLetter.CashLetterHeader == nil { 717 return r.error(&FileError{Msg: msgFileCredit}) 718 } 719 lineOut, err := r.decodeLine(r.line) 720 if err != nil { 721 return err 722 } 723 cr := new(Credit) 724 cr.Parse(lineOut) 725 if err := cr.Validate(); err != nil { 726 return r.error(err) 727 } 728 r.currentCashLetter.AddCredit(cr) 729 return nil 730 } 731 732 // parseCreditItem takes the input record string and parses the CreditItem values 733 func (r *Reader) parseCreditItem() error { 734 // Current implementation has the credit letter outside the bundle but within the cash letter 735 r.recordName = "CreditItem" 736 if r.currentCashLetter.CashLetterHeader == nil { 737 return r.error(&FileError{Msg: msgFileCreditItem}) 738 } 739 lineOut, err := r.decodeLine(r.line) 740 if err != nil { 741 return err 742 } 743 ci := new(CreditItem) 744 ci.Parse(lineOut) 745 if err := ci.Validate(); err != nil { 746 return r.error(err) 747 } 748 r.currentCashLetter.AddCreditItem(ci) 749 return nil 750 } 751 752 // parseBundleControl takes the input record string and parses the BundleControl values 753 func (r *Reader) parseBundleControl() error { 754 r.recordName = "BundleControl" 755 if r.currentCashLetter.currentBundle == nil || r.currentCashLetter.currentBundle.BundleControl == nil { 756 return r.error(&FileError{Msg: msgFileBundleControl}) 757 } 758 lineOut, err := r.decodeLine(r.line) 759 if err != nil { 760 return err 761 } 762 r.currentCashLetter.currentBundle.GetControl().Parse(lineOut) 763 if err := r.currentCashLetter.currentBundle.GetControl().Validate(); err != nil { 764 return r.error(err) 765 } 766 return nil 767 } 768 769 // parseRoutingNumberSummary takes the input record string and parses the RoutingNumberSummary values 770 func (r *Reader) parseRoutingNumberSummary() error { 771 r.recordName = "RoutingNumberSummary" 772 if r.currentCashLetter.CashLetterHeader == nil { 773 return r.error(&FileError{Msg: msgFileRoutingNumberSummary}) 774 } 775 lineOut, err := r.decodeLine(r.line) 776 if err != nil { 777 return err 778 } 779 rns := NewRoutingNumberSummary() 780 rns.Parse(lineOut) 781 if err := rns.Validate(); err != nil { 782 return r.error(err) 783 } 784 return nil 785 } 786 787 // parseCashLetterControl takes the input record string and parses the CashLetterControl values 788 func (r *Reader) parseCashLetterControl() error { 789 r.recordName = "CashLetterControl" 790 if r.currentCashLetter.CashLetterHeader == nil { 791 // CashLetterControl without a current CashLetter 792 return r.error(&FileError{Msg: msgFileCashLetterControl}) 793 } 794 lineOut, err := r.decodeLine(r.line) 795 if err != nil { 796 return err 797 } 798 r.currentCashLetter.GetControl().Parse(lineOut) 799 // Ensure valid CashLetterControl 800 if err := r.currentCashLetter.GetControl().Validate(); err != nil { 801 return r.error(err) 802 } 803 return nil 804 } 805 806 // parseFileControl takes the input record string and parses the FileControl values 807 func (r *Reader) parseFileControl() error { 808 r.recordName = "FileControl" 809 if (FileControl{}) != r.File.Control { 810 // Can be only one file control per file 811 return r.error(&FileError{Msg: msgFileControl}) 812 } 813 lineOut, err := r.decodeLine(r.line) 814 if err != nil { 815 return err 816 } 817 r.File.Control.Parse(lineOut) 818 // Ensure valid FileControl 819 if err := r.File.Control.Validate(); err != nil { 820 return r.error(err) 821 } 822 return nil 823 }