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 }