github.com/moov-io/imagecashletter@v0.10.1/writer.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  	"fmt"
    11  	"io"
    12  
    13  	"github.com/gdamore/encoding"
    14  )
    15  
    16  // Writer writes an ImageCashLetter/X9 File to an encoded format.
    17  //
    18  // Callers should use NewWriter to create a new instance and apply WriterOptions
    19  // as needed to properly encode files for their usecase.
    20  type Writer struct {
    21  	w                  *bufio.Writer
    22  	lineNum            int // current line being written
    23  	VariableLineLength bool
    24  	EbcdicEncoding     bool
    25  }
    26  
    27  // NewWriter returns a new Writer that writes to w.
    28  func NewWriter(w io.Writer, opts ...WriterOption) *Writer {
    29  	writer := &Writer{
    30  		w: bufio.NewWriter(w),
    31  	}
    32  	for _, opt := range opts {
    33  		opt(writer)
    34  	}
    35  	return writer
    36  }
    37  
    38  // WriterOption allows Writer to be configured to write in different formats
    39  type WriterOption func(w *Writer)
    40  
    41  // WriteVariableLineLengthOption allows Writer to write control bytes ahead of record to describe how long the line is
    42  // Follows DSTU microformat as defined https://www.frbservices.org/assets/financial-services/check/setup/frb-x937-standards-reference.pdf
    43  func WriteVariableLineLengthOption() WriterOption {
    44  	return func(w *Writer) {
    45  		w.VariableLineLength = true
    46  	}
    47  }
    48  
    49  // WriteEbcdicEncodingOption allows Writer to write file in EBCDIC
    50  // Follows DSTU microformat as defined https://www.frbservices.org/assets/financial-services/check/setup/frb-x937-standards-reference.pdf
    51  func WriteEbcdicEncodingOption() WriterOption {
    52  	return func(w *Writer) {
    53  		w.EbcdicEncoding = true
    54  	}
    55  }
    56  
    57  func (w *Writer) writeLine(record FileRecord) error {
    58  	line := record.String()
    59  	lineLength := len(line)
    60  
    61  	if w.VariableLineLength {
    62  		ctrl := make([]byte, 4)
    63  		binary.BigEndian.PutUint32(ctrl, uint32(lineLength))
    64  		if _, err := w.w.Write(ctrl); err != nil {
    65  			return err
    66  		}
    67  	}
    68  
    69  	if w.EbcdicEncoding {
    70  		if ivData, ok := record.(*ImageViewData); ok {
    71  			// need to encode everything other than binary image into EBCDIC
    72  			encoded, err := encoding.EBCDIC.NewEncoder().String(ivData.toString(false))
    73  			if err != nil {
    74  				return err
    75  			}
    76  			if _, err := w.w.WriteString(encoded); err != nil {
    77  				return err
    78  			}
    79  			if _, err := w.w.Write(ivData.ImageData); err != nil {
    80  				return err
    81  			}
    82  		} else {
    83  			// no binary data in record, encode entire line
    84  			encoded, err := encoding.EBCDIC.NewEncoder().String(line)
    85  			if err != nil {
    86  				return err
    87  			}
    88  			if _, err := w.w.WriteString(encoded); err != nil {
    89  				return err
    90  			}
    91  		}
    92  	} else {
    93  		// ASCII encoding by default
    94  		if _, err := w.w.WriteString(line); err != nil {
    95  			return err
    96  		}
    97  	}
    98  
    99  	if !w.VariableLineLength {
   100  		if _, err := w.w.WriteString("\n"); err != nil {
   101  			return err
   102  		}
   103  	}
   104  
   105  	w.lineNum++
   106  	return nil
   107  }
   108  
   109  // Writer writes a single imagecashletter.file record to w
   110  func (w *Writer) Write(file *File) error {
   111  	if file == nil {
   112  		return ErrNilFile
   113  	}
   114  	if err := file.Validate(); err != nil {
   115  		return err
   116  	}
   117  	w.lineNum = 0
   118  	// Iterate over all records in the file
   119  	if err := w.writeLine(&file.Header); err != nil {
   120  		return err
   121  	}
   122  	if err := w.writeCashLetter(file); err != nil {
   123  		return err
   124  	}
   125  	if err := w.writeLine(&file.Control); err != nil {
   126  		return err
   127  	}
   128  
   129  	return w.w.Flush()
   130  }
   131  
   132  // Flush writes any buffered data to the underlying io.Writer.
   133  // To check if an error occurred during the Flush, call Error.
   134  func (w *Writer) Flush() {
   135  	w.w.Flush()
   136  }
   137  
   138  // writeCashLetter writes a CashLetter to a file
   139  func (w *Writer) writeCashLetter(file *File) error {
   140  	for _, cl := range file.CashLetters {
   141  		if err := w.writeLine(cl.GetHeader()); err != nil {
   142  			return err
   143  		}
   144  		for _, ci := range cl.GetCreditItems() {
   145  			if err := w.writeLine(ci); err != nil {
   146  				return err
   147  			}
   148  		}
   149  		for _, credit := range cl.GetCredits() {
   150  			if err := w.writeLine(credit); err != nil {
   151  				return err
   152  			}
   153  		}
   154  		if err := w.writeBundle(cl); err != nil {
   155  			return err
   156  		}
   157  		for _, rns := range cl.GetRoutingNumberSummary() {
   158  			if err := w.writeLine(rns); err != nil {
   159  				return err
   160  			}
   161  		}
   162  		if err := w.writeLine(cl.GetControl()); err != nil {
   163  			return err
   164  		}
   165  	}
   166  	return nil
   167  }
   168  
   169  // writeBundle writes a Bundle to a CashLetter
   170  func (w *Writer) writeBundle(cl CashLetter) error {
   171  	for _, b := range cl.GetBundles() {
   172  		if err := w.writeLine(b.GetHeader()); err != nil {
   173  			return err
   174  		}
   175  		if len(b.Checks) > 0 {
   176  			if err := w.writeCheckDetail(b); err != nil {
   177  				return err
   178  			}
   179  		}
   180  		if len(b.Returns) > 0 {
   181  			if err := w.writeReturnDetail(b); err != nil {
   182  				return err
   183  			}
   184  		}
   185  		if err := w.writeLine(b.GetControl()); err != nil {
   186  			return err
   187  		}
   188  	}
   189  	return nil
   190  }
   191  
   192  // writeCheckDetail writes a CheckDetail to a Bundle
   193  func (w *Writer) writeCheckDetail(b *Bundle) error {
   194  	for _, cd := range b.GetChecks() {
   195  		if err := w.writeLine(cd); err != nil {
   196  			return err
   197  		}
   198  		// Write CheckDetailsAddendum (A, B, C)
   199  		if err := w.writeCheckDetailAddendum(cd); err != nil {
   200  			return err
   201  		}
   202  		if err := w.writeCheckImageView(cd); err != nil {
   203  			return err
   204  		}
   205  	}
   206  	return nil
   207  }
   208  
   209  // writeCheckDetailAddendum writes a CheckDetailAddendum (A, B, C) to a CheckDetail
   210  func (w *Writer) writeCheckDetailAddendum(cd *CheckDetail) error {
   211  	addendumA := cd.GetCheckDetailAddendumA()
   212  	for i := range addendumA {
   213  		if err := w.writeLine(&addendumA[i]); err != nil {
   214  			return err
   215  		}
   216  	}
   217  
   218  	addendumB := cd.GetCheckDetailAddendumB()
   219  	for i := range addendumB {
   220  		if err := w.writeLine(&addendumB[i]); err != nil {
   221  			return err
   222  		}
   223  	}
   224  
   225  	addendumC := cd.GetCheckDetailAddendumC()
   226  	for i := range addendumC {
   227  		if err := w.writeLine(&addendumC[i]); err != nil {
   228  			return err
   229  		}
   230  	}
   231  	return nil
   232  }
   233  
   234  // writeCheckImageView writes ImageViews (Detail, Data, Analysis) to a CheckDetail
   235  func (w *Writer) writeCheckImageView(cd *CheckDetail) error {
   236  
   237  	ivDetailSlice := cd.GetImageViewDetail()
   238  	ivDataSlice := cd.GetImageViewData()
   239  	ivAnalysisSlice := cd.GetImageViewAnalysis()
   240  
   241  	// TODO: Add validator to ensure that each imageViewDetail has a corresponding imageViewData and imageViewAnalysis
   242  	// for now enforce that all images have data and analysis or no images have data and analysis
   243  
   244  	if len(ivDataSlice) > 0 && len(ivDataSlice) != len(ivDetailSlice) {
   245  		// should be same number of imageViewData as imageViewDetail
   246  		msg := fmt.Sprintf(msgBundleImageDetailCount, len(ivDataSlice))
   247  		return &BundleError{FieldName: "ImageViewData", Msg: msg}
   248  	}
   249  
   250  	if len(ivAnalysisSlice) > 0 && len(ivAnalysisSlice) != len(ivDetailSlice) {
   251  		// should same number of imageViewAnalysis and imageViewDetail
   252  		msg := fmt.Sprintf(msgBundleImageDetailCount, len(ivAnalysisSlice))
   253  		return &BundleError{FieldName: "ImageViewAnalysis", Msg: msg}
   254  	}
   255  
   256  	// FRB asks that imageViewDetail should immediately be followed by its corresponding data and analysis
   257  	for i := range ivDetailSlice {
   258  		if err := w.writeLine(&ivDetailSlice[i]); err != nil {
   259  			return err
   260  		}
   261  		if len(ivDataSlice) > 0 && len(ivDataSlice) >= i-1 {
   262  			ivData := ivDataSlice[i]
   263  			if err := w.writeLine(&ivData); err != nil {
   264  				return err
   265  			}
   266  		}
   267  		if len(ivAnalysisSlice) > 0 && len(ivAnalysisSlice) >= i-1 {
   268  			ivAnalysis := ivAnalysisSlice[i]
   269  			if err := w.writeLine(&ivAnalysis); err != nil {
   270  				return err
   271  			}
   272  		}
   273  	}
   274  
   275  	return nil
   276  }
   277  
   278  // writeReturnDetail writes a ReturnDetail to a ReturnBundle
   279  func (w *Writer) writeReturnDetail(b *Bundle) error {
   280  	for _, rd := range b.GetReturns() {
   281  		if err := w.writeLine(rd); err != nil {
   282  			return err
   283  		}
   284  		// Write ReturnDetailAddendum (A, B, C, D)
   285  		if err := w.writeReturnDetailAddendum(rd); err != nil {
   286  			return err
   287  		}
   288  		if err := w.writeReturnImageView(rd); err != nil {
   289  			return err
   290  		}
   291  	}
   292  	return nil
   293  }
   294  
   295  // writeReturnDetailAddendum writes a ReturnDetailAddendum (A, B, C, D) to a ReturnDetail
   296  func (w *Writer) writeReturnDetailAddendum(rd *ReturnDetail) error {
   297  	addendumA := rd.GetReturnDetailAddendumA()
   298  	for i := range addendumA {
   299  		if err := w.writeLine(&addendumA[i]); err != nil {
   300  			return err
   301  		}
   302  	}
   303  
   304  	addendumB := rd.GetReturnDetailAddendumB()
   305  	for i := range addendumB {
   306  		if err := w.writeLine(&addendumB[i]); err != nil {
   307  			return err
   308  		}
   309  	}
   310  
   311  	addendumC := rd.GetReturnDetailAddendumC()
   312  	for i := range addendumC {
   313  		if err := w.writeLine(&addendumC[i]); err != nil {
   314  			return err
   315  		}
   316  	}
   317  
   318  	addendumD := rd.GetReturnDetailAddendumD()
   319  	for i := range addendumD {
   320  		if err := w.writeLine(&addendumD[i]); err != nil {
   321  			return err
   322  		}
   323  	}
   324  	return nil
   325  }
   326  
   327  // writeReturnImageView writes ImageViews (Detail, Data, Analysis) to a ReturnDetail
   328  func (w *Writer) writeReturnImageView(rd *ReturnDetail) error {
   329  	ivDetail := rd.GetImageViewDetail()
   330  	for i := range ivDetail {
   331  		if err := w.writeLine(&ivDetail[i]); err != nil {
   332  			return err
   333  		}
   334  	}
   335  
   336  	ivData := rd.GetImageViewData()
   337  	for i := range ivData {
   338  		if err := w.writeLine(&ivData[i]); err != nil {
   339  			return err
   340  		}
   341  	}
   342  
   343  	ivAnalysis := rd.GetImageViewAnalysis()
   344  	for i := range ivAnalysis {
   345  		if err := w.writeLine(&ivAnalysis[i]); err != nil {
   346  			return err
   347  		}
   348  	}
   349  	return nil
   350  }