github.com/moov-io/imagecashletter@v0.10.1/credit.go (about)

     1  // Copyright 2022 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  	"encoding/json"
     9  	"fmt"
    10  	"strings"
    11  	"unicode/utf8"
    12  )
    13  
    14  // Errors specific to a Credit Record
    15  
    16  // Credit Record
    17  type Credit struct {
    18  	// ID is a client defined string used as a reference to this record.
    19  	ID string `json:"id"`
    20  	// RecordType defines the type of record.
    21  	recordType string
    22  	// AuxiliaryOnUs identifies a code used on commercial checks at the discretion of the payor bank.
    23  	AuxiliaryOnUs string `json:"auxiliaryOnUs,omitempty"`
    24  	// ExternalProcessingCode identifies a code used for special purposes as authorized by the AS Committee X9.
    25  	ExternalProcessingCode string `json:"externalProcessingCode,omitempty"`
    26  	// PayorBankRoutingNumber identifies a number that identifies the institution by or through
    27  	// which the item is payable.
    28  	PayorBankRoutingNumber string `json:"payorBankRoutingNumber"`
    29  	// CreditAccountNumberOnUs identifies data specified by the payor bank.
    30  	// Usually an account number, serial number or transaction code or both.
    31  	CreditAccountNumberOnUs string `json:"creditAccountNumberOnUs"`
    32  	// ItemAmount identifies amount of the credit in U.S. dollars.
    33  	ItemAmount int `json:"itemAmount"`
    34  	// InstitutionItemSequenceNumber identifies sequence number assigned by the ECE company/institution.
    35  	ECEInstitutionItemSequenceNumber string `json:"eceInstitutionItemSequenceNumber,omitempty"`
    36  	// DocumentationTypeIndicator identifies a code that indicates the type of documentation
    37  	// that supports the check record.
    38  	DocumentationTypeIndicator string `json:"documentationTypeIndicator,omitempty"`
    39  	// AccountTypeCode identifies a code to designate account type.
    40  	AccountTypeCode string `json:"accountTypeCode,omitempty"`
    41  	// SourceWorkCode identifies a code that identifies the incoming work.
    42  	SourceWorkCode string `json:"sourceWorkCode,omitempty"`
    43  	// WorkType identifies a code that identifies the type of work.
    44  	WorkType string `json:"workType,omitempty"`
    45  	// InstitutionItemSequenceNumber identifies a code that identifies whether this record represents
    46  	// a debit or credit adjustment.
    47  	DebitCreditIndicator string `json:"debitCreditIndicator,omitempty"`
    48  	// reserved is a field reserved for future use.  Reserved should be blank.
    49  	reserved string
    50  	// validator is composed for image cash letter data validation
    51  	validator
    52  	// converters is composed for image cash letter to golang Converters
    53  	converters
    54  }
    55  
    56  // NewCredit returns a new credit with default values for non exported fields
    57  func NewCredit() *Credit {
    58  	cr := &Credit{}
    59  	cr.setRecordType()
    60  	return cr
    61  }
    62  
    63  func (cr *Credit) setRecordType() {
    64  	if cr == nil {
    65  		return
    66  	}
    67  	cr.recordType = "61"
    68  	cr.reserved = "   "
    69  }
    70  
    71  // Parse takes the input record string and parses the BundleControl values
    72  func (cr *Credit) Parse(record string) {
    73  	if utf8.RuneCountInString(record) < 77 {
    74  		return
    75  	}
    76  
    77  	// Character position 1-2, Always "61"
    78  	cr.setRecordType()
    79  	// 03–17
    80  	cr.AuxiliaryOnUs = cr.parseStringField(record[2:17])
    81  	// 18
    82  	cr.ExternalProcessingCode = cr.parseStringField(record[17:18])
    83  	// 19–27
    84  	cr.PayorBankRoutingNumber = cr.parseStringField(record[18:27])
    85  	// 28–47
    86  	cr.CreditAccountNumberOnUs = cr.parseStringField(record[27:47])
    87  	// 48–57
    88  	cr.ItemAmount = cr.parseNumField(record[47:57])
    89  	// 58–72
    90  	cr.ECEInstitutionItemSequenceNumber = record[57:72]
    91  	// 73
    92  	cr.DocumentationTypeIndicator = cr.parseStringField(record[72:73])
    93  	// 74
    94  	cr.AccountTypeCode = cr.parseStringField(record[73:74])
    95  	// 75
    96  	cr.SourceWorkCode = cr.parseStringField(record[74:75])
    97  	// 76
    98  	cr.WorkType = cr.parseStringField(record[75:76])
    99  	// 77
   100  	cr.DebitCreditIndicator = cr.parseStringField(record[76:77])
   101  	// 78–80
   102  	cr.reserved = "   "
   103  
   104  }
   105  
   106  func (cr *Credit) UnmarshalJSON(data []byte) error {
   107  	type Alias Credit
   108  	aux := struct {
   109  		*Alias
   110  	}{
   111  		(*Alias)(cr),
   112  	}
   113  	if err := json.Unmarshal(data, &aux); err != nil {
   114  		return err
   115  	}
   116  	cr.setRecordType()
   117  	return nil
   118  }
   119  
   120  // String writes the BundleControl struct to a string.
   121  func (cr *Credit) String() string {
   122  	if cr == nil {
   123  		return ""
   124  	}
   125  
   126  	var buf strings.Builder
   127  	buf.Grow(80)
   128  	buf.WriteString(cr.recordType)
   129  	buf.WriteString(cr.AuxiliaryOnUsField())
   130  	buf.WriteString(cr.ExternalProcessingCodeField())
   131  	buf.WriteString(cr.PayorBankRoutingNumberField())
   132  	buf.WriteString(cr.CreditAccountNumberOnUsField())
   133  	buf.WriteString(cr.ItemAmountField())
   134  	buf.WriteString(cr.ECEInstitutionItemSequenceNumberField())
   135  	buf.WriteString(cr.DocumentationTypeIndicatorField())
   136  	buf.WriteString(cr.AccountTypeCodeField())
   137  	buf.WriteString(cr.SourceWorkCodeField())
   138  	buf.WriteString(cr.WorkTypeField())
   139  	buf.WriteString(cr.DebitCreditIndicatorField())
   140  	buf.WriteString(cr.reservedField())
   141  	return buf.String()
   142  }
   143  
   144  // Validate performs image cash letter format rule checks on the record and returns an error if not Validated
   145  // The first error encountered is returned and stops the parsing.
   146  func (cr *Credit) Validate() error {
   147  	if err := cr.fieldInclusion(); err != nil {
   148  		return err
   149  	}
   150  	if cr.recordType != "61" {
   151  		msg := fmt.Sprintf(msgRecordType, 61)
   152  		return &FieldError{FieldName: "recordType", Value: cr.recordType, Msg: msg}
   153  	}
   154  	if cr.SourceWorkCode != "" {
   155  		if cr.SourceWorkCode != "3" {
   156  			return &FieldError{FieldName: "SourceWorkCode", Value: cr.SourceWorkCode, Msg: msgInvalid}
   157  		}
   158  	}
   159  	if cr.AccountTypeCode != "" {
   160  		if err := cr.isAccountTypeCode(cr.AccountTypeCode); err != nil {
   161  			return &FieldError{FieldName: "AccountTypeCode", Value: cr.AccountTypeCode, Msg: err.Error()}
   162  		}
   163  	}
   164  	if cr.DocumentationTypeIndicator != "" {
   165  		if cr.DocumentationTypeIndicator != "G" {
   166  			return &FieldError{FieldName: "DocumentationTypeIndicator", Value: cr.DocumentationTypeIndicator, Msg: msgInvalid}
   167  		}
   168  	}
   169  	if cr.ECEInstitutionItemSequenceNumber != "" {
   170  		if err := cr.isNumeric(cr.ECEInstitutionItemSequenceNumber); err != nil {
   171  			return &FieldError{FieldName: "ECEInstitutionItemSequenceNumber", Value: cr.ECEInstitutionItemSequenceNumber, Msg: msgInvalid}
   172  		}
   173  	}
   174  	if err := cr.isNumeric(cr.PayorBankRoutingNumber); err != nil {
   175  		return &FieldError{FieldName: "PayorBankRoutingNumber",
   176  			Value: cr.PayorBankRoutingNumber, Msg: err.Error()}
   177  	}
   178  	// Should not contain forward or back slashes
   179  	if strings.Contains(cr.AuxiliaryOnUs, `\`) || strings.Contains(cr.AuxiliaryOnUs, `/`) {
   180  		return &FieldError{FieldName: "AuxiliaryOnUs", Value: cr.AuxiliaryOnUsField(), Msg: msgInvalid}
   181  	}
   182  
   183  	return nil
   184  }
   185  
   186  // fieldInclusion validate mandatory fields are not default values. If fields are
   187  // invalid the Electronic Exchange will be returned.
   188  func (cr *Credit) fieldInclusion() error {
   189  	if cr.recordType == "" {
   190  		return &FieldError{FieldName: "recordType",
   191  			Value: cr.recordType,
   192  			Msg:   msgFieldInclusion + ", did you use Credit()?"}
   193  	}
   194  	if cr.PayorBankRoutingNumberField() == "000000000" {
   195  		return &FieldError{FieldName: "PayorBankRoutingNumber",
   196  			Value: cr.PayorBankRoutingNumberField(),
   197  			Msg:   msgFieldInclusion + ", did you use Credit()?"}
   198  	}
   199  	if cr.CreditAccountNumberOnUs == "" {
   200  		return &FieldError{FieldName: "CreditAccountNumberOnUs",
   201  			Value: cr.CreditAccountNumberOnUsField(),
   202  			Msg:   msgFieldInclusion + ", did you use Credit()?"}
   203  	}
   204  	if cr.ItemAmount == 0 {
   205  		return &FieldError{FieldName: "ItemAmount",
   206  			Value: cr.ItemAmountField(),
   207  			Msg:   msgFieldInclusion + ", did you use Credit()?"}
   208  	}
   209  	return nil
   210  }
   211  
   212  // AuxiliaryOnUsField gets a string of the AuxiliaryOnUs
   213  func (cr *Credit) AuxiliaryOnUsField() string {
   214  	return cr.alphaField(cr.AuxiliaryOnUs, 15)
   215  }
   216  
   217  // ExternalProcessingCodeField gets a string of the ExternalProcessingCode
   218  func (cr *Credit) ExternalProcessingCodeField() string {
   219  	return cr.alphaField(cr.ExternalProcessingCode, 1)
   220  }
   221  
   222  // PayorBankRoutingNumberField gets a string of the PayorBankRoutingNumber zero padded
   223  func (cr *Credit) PayorBankRoutingNumberField() string {
   224  	return cr.alphaField(cr.PayorBankRoutingNumber, 9)
   225  }
   226  
   227  // CreditAccountNumberOnUsField gets a string of the CreditAccountNumberOnUs
   228  func (cr *Credit) CreditAccountNumberOnUsField() string {
   229  	return cr.alphaField(cr.CreditAccountNumberOnUs, 20)
   230  }
   231  
   232  // ItemAmountField gets a string of the ItemAmount zero padded
   233  func (cr *Credit) ItemAmountField() string {
   234  	return cr.numericField(cr.ItemAmount, 10)
   235  }
   236  
   237  // ECEInstitutionItemSequenceNumberField gets a string of the ECEInstitutionItemSequenceNumber
   238  func (cr *Credit) ECEInstitutionItemSequenceNumberField() string {
   239  	return cr.alphaField(cr.ECEInstitutionItemSequenceNumber, 15)
   240  }
   241  
   242  // DocumentationTypeIndicatorField gets a string of the DocumentationTypeIndicator
   243  func (cr *Credit) DocumentationTypeIndicatorField() string {
   244  	return cr.alphaField(cr.DocumentationTypeIndicator, 1)
   245  }
   246  
   247  // AccountTypeCodeField gets a string of the AccountTypeCode
   248  func (cr *Credit) AccountTypeCodeField() string {
   249  	return cr.alphaField(cr.AccountTypeCode, 1)
   250  }
   251  
   252  // SourceWorkCodeField gets a string of the SourceOfWorkCode
   253  func (cr *Credit) SourceWorkCodeField() string {
   254  	return cr.alphaField(cr.SourceWorkCode, 1)
   255  }
   256  
   257  // WorkTypeField gets a string of the WorkType
   258  func (cr *Credit) WorkTypeField() string {
   259  	return cr.alphaField(cr.WorkType, 1)
   260  }
   261  
   262  // DebitCreditIndicatorField gets a string of the DebitCreditIndicator
   263  func (cr *Credit) DebitCreditIndicatorField() string {
   264  	return cr.alphaField(cr.DebitCreditIndicator, 1)
   265  }
   266  
   267  // reservedField gets reserved - blank space
   268  func (cr *Credit) reservedField() string {
   269  	return cr.alphaField(cr.reserved, 3)
   270  }