@midlandsbank/node-nacha
Version:
NACHA ACH EFT File Parser/Formatter for CCD+ / PPD+ / CTX+
1,310 lines (1,288 loc) • 36 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name2 in all)
__defProp(target, name2, { get: all[name2], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
SEC_CODES: () => SEC_CODES,
TRANSACTION_CODES: () => TRANSACTION_CODES,
default: () => index_default,
format: () => formatNacha,
parse: () => parseNacha
});
module.exports = __toCommonJS(index_exports);
// src/utils/dates.ts
function toYYMMDD(date) {
if (date === void 0) return void 0;
const year = String(date.getFullYear() % 100).padStart(2, "0");
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}${month}${day}`;
}
function parseYYMMDD(dateStr, timeStr) {
if (!dateStr) return void 0;
if (!/^\d{6}$/.test(dateStr)) {
throw new Error(`Invalid date format. Expected YYMMDD. Received ${dateStr}`);
}
if (timeStr && !/^\d{4}$/.test(timeStr)) {
throw new Error("Invalid time format. Expected HHMM.");
}
const currentDate = /* @__PURE__ */ new Date();
const currentCentury = currentDate.getFullYear() - currentDate.getFullYear() % 100;
const year = Number(dateStr.slice(0, 2));
const month = Number(dateStr.slice(2, 4)) - 1;
const day = Number(dateStr.slice(4, 6));
let parsedDate = new Date(currentCentury + year, month, day);
if (timeStr) {
if (!/^\d{4}$/.test(timeStr)) {
throw new Error("Invalid time format. Expected HHMM.");
}
const hour = Number(timeStr.slice(0, 2));
const minute = Number(timeStr.slice(2, 4));
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
throw new Error("Invalid time values in timeStr");
}
parsedDate.setHours(hour, minute, 0, 0);
} else {
parsedDate.setHours(0, 0, 0, 0);
}
return parsedDate;
}
function toHHMM(date) {
if (!date) return void 0;
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
return `${hours}${minutes}`;
}
// src/utils/sumArray.ts
function sumArray(numArr) {
return numArr.reduce((acc, curVal) => acc + curVal, 0);
}
// src/types/Nacha/RecordTypes.ts
var RecordTypes = {
FILE_HEADER: 1,
BATCH_HEADER: 5,
ENTRY: 6,
ADDENDA: 7,
BATCH_FOOTER: 8,
FILE_FOOTER: 9
};
var RecordTypeCodes = Object.values(RecordTypes);
// src/fieldDefinitions/addenda.ts
var addenda = {
addendaTypeCode: {
position: [2, 3],
allow: ["05", "98", "99"]
},
paymentRelatedInfo: {
position: [4, 83],
optional: true
},
addendaSequenceNum: {
position: [84, 87],
numeric: true,
validator: {
regex: /\d{4}/,
message: "addendaSequenceNum must be 4 digits"
}
},
entryDetailSequenceNum: {
position: [88, 94],
validator: {
regex: /\d{7}/,
message: "entryDetailSequenceNum must be 7 digits"
}
}
};
// src/types/Nacha/SEC.ts
var SEC_CODES = ["WEB", "TEL", "PPD", "CCD", "CTX"];
// src/validators.ts
function aba(val) {
const routNum = val.toString().trim();
const issues = [];
if (!/^[0-9]{9}$/.test(routNum)) {
issues.push({
message: "should be 9 digits long and only digits",
level: "NACHA",
value: val
});
return issues;
}
if (!isRoutingInRange(routNum)) {
issues.push({
message: "First 2 digits ABA routing not within valid ranges",
level: "NACHA",
value: val
});
}
if (!isCheckDigitValid(routNum)) {
issues.push({
message: "check digit is invalid",
level: "NACHA",
value: val
});
}
return issues;
}
function dfi(val) {
const dfiNum = val.toString().trim();
const issues = [];
if (!/^[0-9]{8}$/.test(dfiNum)) {
issues.push({
message: "should be 8 digits long and only digits",
level: "NACHA",
value: val
});
return issues;
}
if (!isRoutingInRange(dfiNum)) {
issues.push({
message: "First 2 digits ABA routing not within valid ranges",
level: "NACHA",
value: val
});
}
return issues;
}
function isCheckDigitValid(rtn) {
const d = rtn.split("").map(Number);
return (3 * (d[0] + d[3] + d[6]) + 7 * (d[1] + d[4] + d[7]) + 1 * (d[2] + d[5] + d[8])) % 10 === 0;
}
function isRoutingInRange(val) {
const firstTwoDigits = Number(val.slice(0, 2));
return firstTwoDigits === 80 || isNumberInRanges(firstTwoDigits, [0, 12], [21, 32], [61, 72]);
}
function isNumberInRanges(num, ...ranges) {
return ranges.some(([lo, hi]) => num >= lo && num <= hi);
}
// src/fieldDefinitions/batch.ts
var batchHeader = {
serviceClassCode: {
position: [2, 4],
numeric: true,
allow: [200, 220, 225]
},
companyName: {
position: [5, 20]
},
companyDiscretionaryData: {
position: [21, 40],
optional: true
},
companyId: {
position: [41, 50],
trim: false
},
standardEntryClass: {
position: [51, 53],
allow: SEC_CODES
},
companyEntryDescription: {
position: [54, 63]
},
companyEntryDescriptiveDate: {
position: [64, 69],
optional: true
},
effectiveDate: {
position: [70, 75]
},
settlementDate: {
position: [76, 78],
isBlank: true,
optional: true
},
originatorStatusCode: {
numeric: true,
position: [79, 79]
},
originatingDFIIdentification: {
position: [80, 87]
},
batchNumber: {
position: [88, 94],
numeric: true
}
};
var batchFooter = {
serviceClassCode: {
position: [2, 4],
numeric: true,
allow: [200, 220, 225]
},
entryAndAddendaCount: {
position: [5, 10],
numeric: true
},
entryHash: {
position: [11, 20],
numeric: true
},
totalDebits: {
position: [21, 32],
numeric: true
},
totalCredits: {
position: [33, 44],
numeric: true
},
companyId: {
position: [45, 54],
trim: false
},
messageAuthCode: {
position: [55, 73],
trim: false,
optional: true
},
reserved: {
position: [74, 79],
isBlank: true,
trim: false
},
originatingDFIIdentification: {
position: [80, 87],
validator: dfi
},
batchNumber: {
position: [88, 94],
numeric: true
}
};
// src/types/EntryHelpers.ts
var AccountTypeMap = {
Checking: 2,
Savings: 3,
GL: 4,
Loan: 5
};
var AccountTypeCodes = Object.values(AccountTypeMap);
var AccountCodeMap = Object.fromEntries(
Object.entries(AccountTypeMap).map(([k, v]) => [v, k])
);
var EntryTypeCodes = [2, 3, 4, 6, 7, 8, 9];
var TRANSACTION_CODES = AccountTypeCodes.flatMap(
(a) => EntryTypeCodes.map((e) => a * 10 + e)
);
// src/fieldDefinitions/entry.ts
var transactionCode = {
// length: 2,
numeric: true,
position: [2, 3],
allow: TRANSACTION_CODES
};
var routingNumber = {
// length: 9,
position: [4, 12],
validator: aba
};
var accountNumber = {
// length: 17,
position: [13, 29]
};
var amount = {
// length: 10,
numeric: true,
position: [30, 39]
};
var addendaRecordIndicator = {
// length: 1,
allow: [0, 1],
numeric: true
};
var idNumber = {
// length: 15,
position: [40, 54],
optional: true
};
var discretionaryData = {
// length: 2,
optional: true
// position: 8,
};
var name = {
// length: 22,
optional: true
// position: 7,
};
var PPD = {
transactionCode,
routingNumber,
accountNumber,
amount,
idNumber,
name: { position: [55, 76], ...name },
discretionaryData: { position: [77, 78], ...discretionaryData },
addendaRecordIndicator: { position: [79, 79], ...addendaRecordIndicator },
traceNumber: { position: [80, 94] }
};
var CCD = PPD;
var TEL = PPD;
var WEB = {
// recordTypeCode,
transactionCode,
routingNumber,
accountNumber,
amount,
idNumber,
name: { position: [55, 76], ...name },
paymentTypeCode: {
// length: 2,
optional: true,
position: [77, 78],
allow: ["R", "S"]
},
addendaRecordIndicator: { position: [79, 79], ...addendaRecordIndicator },
traceNumber: { position: [80, 94] }
};
var CTX = {
transactionCode,
routingNumber,
accountNumber,
amount,
idNumber,
numberOfAddendas: {
// length: 4,
numeric: true,
position: [55, 58]
},
name: { ...name, position: [59, 74] },
reserved: {
position: [75, 76],
// length: 2,
isBlank: true,
trim: false
},
discretionaryData: {
position: [77, 78],
...discretionaryData
},
addendaRecordIndicator: { position: [79, 79], ...addendaRecordIndicator },
traceNumber: { position: [80, 94] }
};
// src/fieldDefinitions/file.ts
var fileHeader = {
priorityCode: {
numeric: true,
position: [2, 3]
},
destination: {
validator: aba,
// trim: false,
position: [4, 13],
justify: "right"
},
origin: {
validator: aba,
// trim: false,
position: [14, 23],
justify: "right"
},
fileCreationDate: {
position: [24, 29]
// type: "date",
},
fileCreationTime: {
position: [30, 33]
},
fileIdModifier: {
position: [34, 34],
validator: {
regex: /[A-Z0-9]/,
message: "fileIdModifier must be alphanumeric"
}
},
recordSize: {
position: [35, 37],
mustEqual: 94,
numeric: true
},
blockingFactor: {
position: [38, 39],
mustEqual: 10,
numeric: true
},
formatCode: {
position: [40, 40],
mustEqual: 1,
numeric: true
},
destinationName: {
position: [41, 63],
optional: true
},
originName: {
position: [64, 86],
optional: true
},
referenceCode: {
position: [87, 94],
optional: true
}
};
var fileFooter = {
batchCount: {
numeric: true,
position: [2, 7]
},
blockCount: {
numeric: true,
position: [8, 13]
},
entryAndAddendaCount: {
numeric: true,
position: [14, 21]
},
entryHash: {
numeric: true,
position: [22, 31]
},
totalDebits: {
numeric: true,
position: [32, 43]
},
totalCredits: {
numeric: true,
position: [44, 55]
},
reserved: {
isBlank: true,
trim: false,
position: [56, 94]
}
};
// src/fieldDefinitions/index.ts
var fieldDefinitions = {
[RecordTypes.FILE_HEADER]: fileHeader,
[RecordTypes.BATCH_HEADER]: batchHeader,
[RecordTypes.ENTRY]: {
CCD,
PPD,
WEB,
TEL,
CTX
},
[RecordTypes.ADDENDA]: addenda,
[RecordTypes.BATCH_FOOTER]: batchFooter,
[RecordTypes.FILE_FOOTER]: fileFooter
};
var fieldDefinitions_default = fieldDefinitions;
function getNonEntryFieldDef(recordTypeCode) {
return fieldDefinitions[recordTypeCode];
}
function getEntryFieldDef(sec) {
return fieldDefinitions[6][sec];
}
// src/utils/formatNacha.ts
function formatNacha(data) {
let records = [data.header];
data.batches.sort((a, b) => a.header.batchNumber - b.header.batchNumber).forEach((batch) => {
records.push(batch.header);
batch.entries.forEach(({ entry, addenda: addenda2 }) => {
records.push(entry);
(addenda2 ?? []).sort((a, b) => a.addendaSequenceNum - b.addendaSequenceNum).forEach((addenda3) => records.push(addenda3));
});
records.push(batch.footer);
});
records.push(data.footer);
let padding = Array(data.padding).fill("9".repeat(94));
let fileContents = records.map(formatField);
fileContents.push(...padding);
return fileContents.join("\n");
}
function formatField(data) {
let fieldDefs = data.recordTypeCode === 6 ? fieldDefinitions_default[6][data.type] : fieldDefinitions_default[data.recordTypeCode];
let charArray = [data.recordTypeCode.toString()];
charArray.push(..." ".repeat(93).split(""));
Object.keys(fieldDefs).forEach((key) => {
let { position, numeric, justify } = fieldDefs[key];
let [start, end] = position;
let val = data[key]?.toString() ?? "";
let length = end - start + 1;
let justifySide = justify ?? (numeric ? "right" : "left");
let padChar = numeric ? "0" : " ";
val = justifySide === "right" ? val.padStart(length, padChar) : val.padEnd(length, padChar);
charArray.splice(start - 1, end, ...val.split(""));
});
return charArray.join("");
}
// src/utils/isBlank.ts
function isBlank(str) {
for (const char of str) {
if (char !== " " && char !== " " && char !== "\r" && char !== "\n")
return false;
}
return true;
}
// src/utils/parse/parseLine.ts
function parseLine(recordTypeCode, raw, sec) {
if (raw.length !== 94) throw new Error("line must be 94 chars long");
const fieldDefs = recordTypeCode === 6 ? getEntryFieldDef(sec) : getNonEntryFieldDef(recordTypeCode);
const record = { recordTypeCode };
for (const key in fieldDefs) {
const def = fieldDefs[key];
const [start, end] = def.position;
let valStr = raw.slice(start - 1, end);
if (def.optional && isBlank(valStr)) continue;
if (def.trim !== false) {
valStr = valStr.trim();
}
let value = valStr;
if (def.numeric) {
const num = Number(valStr);
if (!Number.isFinite(num)) {
throw new Error(`${String(key)} is not a valid number`);
}
value = num;
}
if (def.mustEqual !== void 0 && def.mustEqual !== value) {
throw new Error(
`${String(key)} must equal ${def.mustEqual}. Found "${value}"`
);
}
if (def.allow && def.allow.length > 0) {
if (!def.allow.includes(value)) {
const allowed = def.allow.join(", ");
throw new Error(
`${String(key)} must be one of: ${allowed}. Found "${value}"`
);
}
}
record[key] = value;
}
if (recordTypeCode === 6) record.type = sec;
return record;
}
// src/utils/parse/parseNacha.ts
var RECORD_LEN = 94;
var PADDING = "9".repeat(RECORD_LEN);
function parseNacha(raw) {
const lines = raw.indexOf("\r") >= 0 ? raw.split("\r\n") : raw.split("\n");
while (lines.length && isBlank(lines[lines.length - 1])) lines.pop();
let fileHeader2;
let fileFooter2;
let filePadding = 0;
let currentBatchHeader;
let currentEntries = [];
const batches = [];
for (let idx = 0; idx < lines.length; idx++) {
const line = lines[idx];
const lineNum = idx + 1;
let isNines = /^9+$/;
if (line.length === RECORD_LEN && isNines.test(line)) {
if (line === PADDING) {
if (!fileHeader2 || !fileFooter2) {
throw new Error("File Padding found before end of file");
}
filePadding++;
continue;
}
}
if (line.length === 0) continue;
const recordTypeCode = Number(line[0]);
switch (recordTypeCode) {
// File Header
case 1: {
if (fileHeader2) throw new Error("More than one File Header found in file");
fileHeader2 = parseLine(1, line);
break;
}
// 5 Batch Header
case 5: {
if (currentBatchHeader) {
const batchNum = currentBatchHeader.batchNumber;
throw new Error(`Batch #${batchNum} missing footer`);
}
currentBatchHeader = parseLine(5, line);
break;
}
// Entry Detail
case 6: {
if (!currentBatchHeader) {
throw new Error(`Entry found outside of batch at ${lineNum}`);
}
const sec = currentBatchHeader.standardEntryClass;
const entry = parseLine(6, line, sec);
currentEntries.push({ entry, addenda: [] });
break;
}
// Addenda
case 7: {
if (!currentBatchHeader) {
throw new Error(`Addenda found outside of batch at ${lineNum}`);
}
const current = currentEntries[currentEntries.length - 1];
if (!current) throw new Error("Addenda found before entry");
current.addenda.push(parseLine(7, line));
break;
}
// Batch Control/Footer
case 8: {
if (!currentBatchHeader) {
throw new Error(`Batch footer without corresponding header found at ${lineNum}`);
}
const footer = parseLine(8, line);
batches.push({
header: currentBatchHeader,
entries: currentEntries,
footer
});
currentBatchHeader = void 0;
currentEntries = [];
break;
}
// File Footer (non-padding)
case 9: {
if (!fileHeader2) {
throw new Error(`File footer found before file header at ${lineNum}`);
}
fileFooter2 = parseLine(9, line);
break;
}
default: {
const recordTypeCode2 = line[0];
throw new Error(
`Line #:${lineNum} has an invalid record type code of ${recordTypeCode2}`
);
}
}
}
if (!fileHeader2) throw new Error("No file header found");
if (!fileFooter2) throw new Error("No file footer found");
return {
header: fileHeader2,
batches,
footer: fileFooter2,
padding: filePadding
};
}
// src/utils/tranCodeHelpers.ts
function getTranCodeDetails(tranCode) {
const { entryTypeCode, accountTypeCode } = splitTranCode(tranCode);
return {
accountType: AccountCodeMap[accountTypeCode],
// Credit codes are 2/3/4; debit are 7/8/9
direction: entryTypeCode < 5 ? "credit" : "debit",
purpose: getPurpose(entryTypeCode)
};
}
function splitTranCode(tranCode) {
const entryTypeCode = tranCode % 10;
const accountTypeCode = Math.floor(tranCode / 10);
return { entryTypeCode, accountTypeCode };
}
function getPurpose(entryTypeCode) {
switch (entryTypeCode) {
case 3:
case 8:
return "prenote";
case 4:
case 9:
return "remittance";
default:
return "live";
}
}
// src/api/Entry/BaseEntry.ts
var BaseEntryWrapper = class {
_batch;
/** SEC for this entry */
type;
/** Two digit code identifying the account type & purpose at the
* receiving financial institution */
transactionCode;
accountNumber;
routingNumber;
/** amount of the transaction in dollars */
amount;
/** determines the purpose of the entry. Non-live transactions should
* have an amount of 0.
*/
// public purpose: Purpose;
/** Receiver's identification number. This number may
* be printed on the receiver's bank statement by the
* Receiving Financial Institution */
idNumber;
/** Name of receiver. */
name;
/** This number will be unique to the transaction and will help
* identify the transaction in case of an inquiry */
traceNumber;
constructor(batch, opts) {
this._batch = batch;
if ("entry" in opts) {
this.type = opts.entry.type;
this.transactionCode = opts.entry.transactionCode;
this.accountNumber = opts.entry.accountNumber;
this.routingNumber = opts.entry.routingNumber;
this.amount = opts.entry.amount;
this.idNumber = opts.entry.idNumber;
this.name = opts.entry.name;
this.traceNumber = opts.entry.traceNumber;
return;
}
let tranCode;
if (opts.accountType && opts.direction) {
tranCode = this.getTranCode(
opts.accountType,
opts.direction,
opts.purpose ?? "live"
);
} else {
tranCode = opts.transactionCode;
}
this.transactionCode = tranCode;
this.type = batch.sec;
this.accountNumber = opts.accountNumber;
this.routingNumber = opts.routingNumber;
this.amount = opts.amount;
this.idNumber = opts.idNumber;
this.name = opts.name;
this.traceNumber = opts.traceNumber ?? this.generateTraceNum();
}
// --------------
// Helper methods
// --------------
getBaseEntry() {
return {
recordTypeCode: 6,
transactionCode: this.transactionCode,
routingNumber: this.routingNumber,
accountNumber: this.accountNumber,
amount: Math.round(this.amount * 100),
name: this.name,
idNumber: this.idNumber,
addendaRecordIndicator: this.getAddendaCount() > 0 ? 1 : 0,
traceNumber: this.traceNumber
};
}
toAddenda(addenda2, addendaNumber = 1) {
const { paymentRelatedInfo, typeCode } = typeof addenda2 === "string" ? { paymentRelatedInfo: addenda2, typeCode: "05" } : addenda2;
return {
recordTypeCode: 7,
addendaTypeCode: typeCode,
paymentRelatedInfo,
addendaSequenceNum: addendaNumber,
entryDetailSequenceNum: this.traceNumber.slice(-7)
};
}
toAddendaArray(addenda2) {
return addenda2?.map((a, i) => this.toAddenda(a, i + 1)) ?? [];
}
getTranCode(accountType, direction, purpose) {
const accountCode = typeof accountType === "number" ? accountType : AccountTypeMap[accountType];
let entryTypeCode;
const isCredit = direction === "credit";
switch (purpose) {
case "live":
entryTypeCode = isCredit ? 2 : 7;
break;
case "prenote":
entryTypeCode = isCredit ? 3 : 8;
break;
case "remittance":
entryTypeCode = isCredit ? 4 : 9;
break;
default:
entryTypeCode = 2;
}
return accountCode * 10 + entryTypeCode;
}
generateTraceNum() {
const originDFI = this._batch.origin.slice(0, 7);
const seq = this._batch.entries.length + 1;
return originDFI + String(seq).padStart(7, "0");
}
// -----------------
// Common accessors
// -----------------
/** Credit/Debit */
get direction() {
let { direction } = getTranCodeDetails(this.transactionCode);
return direction;
}
get purpose() {
let { purpose } = getTranCodeDetails(this.transactionCode);
return purpose;
}
get accountType() {
let { accountType } = getTranCodeDetails(this.transactionCode);
return accountType;
}
/** Signed amount in dollars. Credit are positives & Debits are negative. */
get amountSigned() {
return this.direction === "credit" ? this.amount : -this.amount;
}
get cents() {
return Math.round(this.amount * 100);
}
set cents(val) {
this.amount = val / 100;
}
get centsSigned() {
return this.direction === "credit" ? this.cents : -this.cents;
}
};
// src/api/Entry/index.ts
var hasEntry = (opts) => typeof opts === "object" && !!opts && "entry" in opts;
function ensureArray(val) {
if (val == null) return [];
return Array.isArray(val) ? val : [val];
}
function rawSingle(wrapper, type, extra, addenda2) {
const base = wrapper.getBaseEntry();
const addendaArr = ensureArray(addenda2);
return {
entry: {
...base,
type,
...extra
},
addenda: addendaArr
};
}
var CCDPPDEntryWrapper = class extends BaseEntryWrapper {
discretionaryData;
addenda;
constructor(batch, opts) {
super(batch, opts);
if (hasEntry(opts)) {
this.discretionaryData = opts.entry.discretionaryData;
this.addenda = opts.addenda?.[0];
} else {
this.discretionaryData = opts.discretionaryData;
if (opts.addenda) this.addenda = this.toAddenda(opts.addenda);
}
}
getAddendaCount() {
return this.addenda ? 1 : 0;
}
toJSON() {
return rawSingle(
this,
this._batch.sec,
{
discretionaryData: this.discretionaryData
},
this.addenda
);
}
};
var CCDEntryWrapper = class extends CCDPPDEntryWrapper {
};
var PPDEntryWrapper = class extends CCDPPDEntryWrapper {
};
var WEBEntryWrapper = class extends BaseEntryWrapper {
paymentTypeCode;
addenda;
constructor(batch, opts) {
super(batch, opts);
if (hasEntry(opts)) {
this.paymentTypeCode = opts.entry.paymentTypeCode;
this.addenda = opts.addenda?.[0];
} else {
this.paymentTypeCode = opts.paymentTypeCode;
if (opts.addenda) this.addenda = this.toAddenda(opts.addenda);
}
}
getAddendaCount() {
return this.addenda ? 1 : 0;
}
toJSON() {
return rawSingle(
this,
"WEB",
{ paymentTypeCode: this.paymentTypeCode },
this.addenda
);
}
};
var TELEntryWrapper = class extends BaseEntryWrapper {
discretionaryData;
constructor(batch, opts) {
super(batch, opts);
this.discretionaryData = hasEntry(opts) ? opts.entry.discretionaryData : opts.discretionaryData;
}
getAddendaCount() {
return 0;
}
toJSON() {
return rawSingle(this, "TEL", {
discretionaryData: this.discretionaryData
});
}
};
var CTXEntryWrapper = class extends BaseEntryWrapper {
discretionaryData;
addenda;
constructor(batch, opts) {
super(batch, opts);
if (hasEntry(opts)) {
this.discretionaryData = opts.entry.discretionaryData;
this.addenda = opts.addenda;
} else {
this.discretionaryData = opts.discretionaryData;
this.addenda = this.toAddendaArray(opts.addenda);
}
}
getAddendaCount() {
return this.addenda?.length ?? 0;
}
get numberOfAddendas() {
return this.getAddendaCount();
}
toJSON() {
const base = this.getBaseEntry();
return {
entry: {
...base,
type: "CTX",
numberOfAddendas: this.numberOfAddendas,
reserved: " ",
discretionaryData: this.discretionaryData
},
addenda: this.addenda ?? []
};
}
};
var EntryClassMap = {
PPD: PPDEntryWrapper,
CCD: CCDEntryWrapper,
WEB: WEBEntryWrapper,
TEL: TELEntryWrapper,
CTX: CTXEntryWrapper
};
function makeEntry(sec, batch, entryDetails) {
const EntryClass = EntryClassMap[sec];
return new EntryClass(batch, entryDetails);
}
// src/api/BatchWrapper.ts
var BatchWrapper = class {
constructor(_parentFile, sec, opts) {
this._parentFile = _parentFile;
this.sec = sec;
if ("header" in opts && "footer" in opts && "entries" in opts) {
this.company = {
id: opts.header.companyId,
name: opts.header.companyName,
discretionaryData: opts.header.companyDiscretionaryData,
descriptiveDate: opts.header.companyEntryDescriptiveDate
};
this.effectiveDate = parseYYMMDD(opts.header.effectiveDate);
this.entryDescription = opts.header.companyEntryDescription;
this.origin = opts.header.originatingDFIIdentification;
this.messageAuthCode = opts.footer.messageAuthCode;
this.entries = opts.entries.map((e) => makeEntry(sec, this, e));
return;
}
this.effectiveDate = opts.effectiveDate ?? this._parentFile.creationDate;
this.company = opts.company;
this.entryDescription = opts.entryDescription;
this.origin = opts.origin ?? this._parentFile.origin.routing;
this.messageAuthCode = opts.messageAuthCode;
this.entries = [];
}
sec;
company;
effectiveDate;
entryDescription;
originatorStatusCode = 1;
origin;
messageAuthCode;
entries;
// ---------- Builder-ish API when constructing new ----------
addEntry(opts) {
let entryOpts = Array.isArray(opts) ? opts : [opts];
let entries = entryOpts.map((e) => makeEntry(this.sec, this, e));
this.entries.push(...entries);
return this;
}
credit(opts) {
return this.addEntry({ direction: "credit", ...opts });
}
debit(opts) {
return this.addEntry({ direction: "debit", ...opts });
}
/** Returns parent file */
done() {
return this._parentFile;
}
/** Alias of {@link done} */
end = this.done;
// Service Class Code (non-mutable)
get serviceClassCode() {
if (this.entries.length === 0) return 200;
const hasDebits = this.entries.some((e) => e.direction === "debit");
const hasCredits = this.entries.some((e) => e.direction === "credit");
if (hasCredits && hasDebits) return 200;
if (hasCredits) return 220;
if (hasDebits) return 225;
return 200;
}
get standardEntryClass() {
return this.sec;
}
get batchNumber() {
return this._parentFile.batches.indexOf(this) + 1;
}
get entryAndAddendaCount() {
return this.entries.reduce(
(acc, entry) => acc + entry.getAddendaCount() + 1,
0
// Default
);
}
get entryHash() {
let entryDFIs = this.entries.map((e) => e.routingNumber).map((routing) => Number(routing.slice(0, 8)));
return sumArray(entryDFIs) % (1e9 * 10);
}
get totalDebits() {
let debits = this.entries.filter((e) => e.direction === "debit").map((e) => e.cents);
return sumArray(debits);
}
get totalCredits() {
let credits = this.entries.filter((e) => e.direction === "credit").map((e) => e.cents);
return sumArray(credits);
}
toJSON() {
let { id, name: name2, discretionaryData: discretionaryData2, descriptiveDate } = this.company;
return {
header: {
recordTypeCode: 5,
serviceClassCode: this.serviceClassCode,
companyName: name2,
companyDiscretionaryData: discretionaryData2,
companyId: id,
standardEntryClass: this.sec,
companyEntryDescription: this.entryDescription,
companyEntryDescriptiveDate: typeof descriptiveDate === "string" ? descriptiveDate : toYYMMDD(descriptiveDate),
effectiveDate: toYYMMDD(this.effectiveDate),
settlementDate: " ".repeat(3),
originatorStatusCode: 1,
originatingDFIIdentification: this.origin,
batchNumber: this.batchNumber
},
entries: this.entries.map((entry) => entry.toJSON()),
footer: {
recordTypeCode: 8,
serviceClassCode: this.serviceClassCode,
entryAndAddendaCount: this.entryAndAddendaCount,
entryHash: this.entryHash,
totalDebits: this.totalDebits,
totalCredits: this.totalCredits,
companyId: id,
messageAuthCode: this.messageAuthCode,
reserved: " ".repeat(6),
originatingDFIIdentification: this.origin,
batchNumber: this.batchNumber
}
};
}
};
// src/api/Nacha.ts
var Nacha = class _Nacha {
/** The lower the number, the higher processing priority. */
priorityCode = 1;
/** RDFI Details */
destination;
/** ODFI Details */
origin;
/** Date file was created */
creationDate = /* @__PURE__ */ new Date();
/** Code to distinguish among multiple input files. Label the first
* (or only) file "A", and continue in sequence (A-Z). If more than
* one file is delivered, they must have different modifiers. */
fileIdModifier = "A";
/** Number of bytes per record. Should only be "094". */
recordSize = 94;
/** How many blocks the file is made up of. Should always be 10 */
blockingFactor = 10;
/** Currently there is only one code. Enter 1. */
formatCode = 1;
/** Optional field you may use to describe input
* file for internal accounting purposes. */
referenceCode;
batches = [];
constructor(opts) {
if ("header" in opts && "footer" in opts && "batches" in opts) {
this.batches = opts.batches.map(
(batch) => new BatchWrapper(
this,
batch.header.standardEntryClass,
batch
)
);
this.priorityCode = opts.header.priorityCode;
this.destination = {
name: opts.header.destinationName,
routing: opts.header.destination
};
this.origin = {
name: opts.header.originName,
routing: opts.header.origin
};
this.creationDate = parseYYMMDD(
opts.header.fileCreationDate,
opts.header.fileCreationTime
);
this.fileIdModifier = opts.header.fileIdModifier;
this.referenceCode = opts.header.referenceCode;
return;
}
let {
priorityCode,
destination,
origin,
creationDate,
fileIdModifier,
referenceCode
} = opts;
if (priorityCode) this.priorityCode = priorityCode;
this.destination = destination;
this.origin = origin;
if (creationDate) this.creationDate = creationDate;
if (fileIdModifier) this.fileIdModifier = fileIdModifier;
this.referenceCode = referenceCode;
}
addBatch(sec, opts, entries) {
const batch = new BatchWrapper(this, sec, opts);
this.batches.push(batch);
if (entries) {
batch.addEntry(entries);
return this;
}
return batch;
}
static {
const define = (ctor, sec) => {
const key = sec.toLowerCase();
Object.defineProperty(ctor.prototype, key, {
value(opts, entries) {
return entries !== void 0 ? this.addBatch(sec, opts, entries) : this.addBatch(sec, opts);
},
configurable: true,
writable: true
});
};
SEC_CODES.forEach((sec) => define(this, sec));
}
/** The total number of batch header records in the file. */
get batchCount() {
return this.batches.length;
}
/**
* The total number of physical blocks on the file,
* including the File Header and File Control records.
*/
get blockCount() {
return Math.ceil(this.totalRecordCount / this.blockingFactor);
}
get totalRecordCount() {
return 2 + // File Header & Footer
this.entryAndAddendaCount + this.batchCount * 2;
}
/** Total number of entry detail and addenda records on the file. */
get entryAndAddendaCount() {
let entryAndAddendaCounts = this.batches.map((b) => b.entryAndAddendaCount);
return sumArray(entryAndAddendaCounts);
}
/**
* Total of all entries' DFI(Routing # excluding final check digit).
* Only use the final 10 positions in the entry.
*/
get entryHash() {
let entryDFIs = this.batches.flatMap((b) => b.entries.map((e) => e.routingNumber)).map((routing) => Number(routing.slice(0, 8)));
return sumArray(entryDFIs) % (1e8 * 10);
}
/** Dollar totals of debit entries within the file. */
get totalDebits() {
let allDebits = this.batches.map((b) => b.totalDebits);
return sumArray(allDebits);
}
/** Dollar totals of credit entries within the file. */
get totalCredits() {
let allCredits = this.batches.map((b) => b.totalCredits);
return sumArray(allCredits);
}
/**
* Amount of file padding the file will have.
* File padding is a line of 94 '9's.
*/
get padding() {
return 10 - this.totalRecordCount % 10;
}
/** @returns A NACHA formatted file */
toString() {
return formatNacha(this.toJSON());
}
/** @returns a JSON representation of the file */
toJSON() {
return {
header: {
recordTypeCode: 1,
priorityCode: this.priorityCode,
destination: this.destination.routing,
origin: this.origin.routing,
fileCreationDate: toYYMMDD(this.creationDate),
fileCreationTime: toHHMM(this.creationDate),
fileIdModifier: this.fileIdModifier,
recordSize: this.recordSize,
blockingFactor: this.blockingFactor,
formatCode: this.formatCode,
destinationName: this.destination.name,
originName: this.origin.name,
referenceCode: this.referenceCode
},
batches: this.batches.map((b) => b.toJSON()),
footer: {
recordTypeCode: 9,
batchCount: this.batchCount,
blockCount: this.blockCount,
entryAndAddendaCount: this.entryAndAddendaCount,
entryHash: this.entryHash,
totalDebits: this.totalDebits,
totalCredits: this.totalCredits,
reserved: " ".repeat(39)
},
padding: this.padding
};
}
/** Factory: parses NACHA text and return an initialized Nacha file */
static fromNacha(raw) {
let data = parseNacha(raw);
return new _Nacha(data);
}
/**
* Accepts a NACHA file in the form of a string & returns an object
* representation of the file. Any issues preventing the file from being
* parsed will cause the function to throw.
*/
static parse = parseNacha;
/** Takes a NACHA object and returns NACHA formatted file as a string */
static format = formatNacha;
};
// src/index.ts
var index_default = Nacha;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
SEC_CODES,
TRANSACTION_CODES,
format,
parse
});
//# sourceMappingURL=index.cjs.map