@fcgs/filerr-nach
Version:
nACH is a highly customizable Node.js module exposing a high & low-level API for generating ACH files for use within the ACH network
348 lines (299 loc) • 9.84 kB
JavaScript
// File
var fs = require('fs');
var _ = require('lodash');
var async = require('async');
var utils = require('../utils');
var validate = require('../validate');
var highLevelOverrides = [
'immediateDestination',
'immediateOrigin',
'fileCreationDate',
'fileCreationTime',
'fileIdModifier',
'immediateDestinationName',
'immediateOriginName',
'referenceCode',
];
var Batch = require('../batch');
var Entry = require('../entry');
var Addenda = require('../entry-addenda');
var fileHeader = require('./header');
var fileControl = require('./control');
var batchHeader = require('./../batch/header');
var batchControl = require('./../batch/control');
var entryFields = require('./../entry/fields');
var addendaFields = require('./../entry-addenda/fields');
function File(options, autoValidate) {
this._batches = [];
// Allow the batch header/control defaults to be overriden if provided
this.header = options.header
? _.merge(options.header, fileHeader(), _.defaults)
: fileHeader();
this.control = options.control
? _.merge(options.header, fileControl, _.defaults)
: _.cloneDeep(fileControl);
// Configure high-level overrides (these override the low-level settings if provided)
utils.overrideLowLevel(highLevelOverrides, options, this);
// This is done to make sure we have a 9-digit routing number
if (options.immediateDestination) {
this.header.immediateDestination.value = utils.computeCheckDigit(
options.immediateDestination
);
}
this._batchSequenceNumber = Number(options.batchSequenceNumber) || 1;
if (autoValidate !== false) {
// Validate all values
this._validate();
}
return this;
}
File.prototype.get = function (field) {
// If the header has the field, return the value
if (this.header[field]) {
return this.header[field]['value'];
}
// If the control has the field, return the value
if (this.control[field]) {
return this.control[field]['value'];
}
};
File.prototype.set = function (field, value) {
// If the header has the field, set the value
if (this.header[field]) {
this.header[field]['value'] = value;
}
// If the control has the field, set the value
if (this.control[field]) {
this.control[field]['value'] = value;
}
};
File.prototype._validate = function () {
// Validate header field lengths
validate.validateLengths(this.header);
// Validate header data types
validate.validateDataTypes(this.header);
// Validate control field lengths
validate.validateLengths(this.control);
// Validate header data types
validate.validateDataTypes(this.control);
};
File.prototype.addBatch = function (batch) {
// Set the batch number on the header and control records
batch.header.batchNumber.value = this._batchSequenceNumber;
batch.control.batchNumber.value = this._batchSequenceNumber;
// Increment the batchSequenceNumber
++this._batchSequenceNumber;
this._batches.push(batch);
};
File.prototype.getBatches = function () {
return this._batches;
};
File.prototype.generatePaddedRows = function (rows, cb) {
var paddedRows = '';
for (var i = 0; i < rows; i++) {
paddedRows += utils.newLineChar() + utils.pad('', 94, '9');
}
// Return control flow back by calling the callback function
cb(paddedRows);
};
File.prototype.generateBatches = function (done1) {
var self = this;
var result = '';
var rows = 2;
var entryHash = 0;
let entryAddendaCount = 0;
var totalDebit = 0;
var totalCredit = 0;
async.each(
this._batches,
function (batch, done2) {
totalDebit += batch.control.totalDebit.value;
totalCredit += batch.control.totalCredit.value;
async.each(
batch._entries,
function (entry, done3) {
entry.fields.traceNumber.value = entry.fields.traceNumber.value
? entry.fields.traceNumber.value
: self.header.immediateOrigin.value.slice(0, 8) +
utils.pad(entryAddendaCount, 7, false, '0');
entryHash += Number(entry.fields.receivingDFI.value);
// Increment the addenda and block count
entryAddendaCount = entryAddendaCount + entry.getRecordCount();
rows = rows + entry.getRecordCount();
done3();
},
function (err) {
// Only iterate and generate the batch if there is at least one entry in the batch
if (batch._entries.length > 0) {
// Increment the addendaCount of the batch
self.control.batchCount.value++;
// Bump the number of rows only for batches with at least one entry
rows = rows + 2;
// Generate the batch after we've added the trace numbers
batch.generateString(function (batchString) {
result += batchString + utils.newLineChar();
done2();
});
} else {
done2();
}
}
);
},
function (err) {
self.control.totalDebit.value = totalDebit;
self.control.totalCredit.value = totalCredit;
self.control.addendaCount.value = entryAddendaCount;
self.control.blockCount.value = utils.getNextMultiple(rows, 10) / 10;
// Slice the 10 rightmost digits.
self.control.entryHash.value = entryHash.toString().slice(-10);
// Pass the result string as well as the number of rows back
done1(result, rows);
}
);
};
File.prototype.generateHeader = function (cb) {
utils.generateString(this.header, function (string) {
cb(string);
});
};
File.prototype.generateControl = function (cb) {
utils.generateString(this.control, function (string) {
cb(string);
});
};
File.prototype.generateFile = function (cb) {
var self = this;
return new Promise(function (resolve) {
self.generateHeader(function (headerString) {
self.generateBatches(function (batchString, rows) {
self.generateControl(function (controlString) {
// These must be within this callback otherwise rows won't be calculated yet
var paddedRows = utils.getNextMultipleDiff(rows, 10);
self.generatePaddedRows(paddedRows, function (paddedString) {
var str =
headerString +
utils.newLineChar() +
batchString +
controlString +
paddedString;
cb && cb(undefined, str);
resolve(str);
});
});
});
});
});
};
File.prototype.writeFile = function (path, cb) {
var self = this;
return new Promise(function (resolve, reject) {
self.generateFile(function (err, fileSting) {
if (err) {
reject(err);
return cb && cb(err);
}
fs.writeFile(path, fileSting, function (err) {
if (err) {
reject(err);
return cb && cb(err);
}
resolve();
});
});
});
};
File.parseFile = function (filePath, cb) {
return new Promise(function (resolve, reject) {
fs.readFile(filePath, function (err, data) {
if (err) {
reject(err);
return cb && cb(err);
}
resolve(File.parse(data.toString(), cb));
});
});
};
File.parse = function (str, cb) {
return new Promise(function (resolve, reject) {
if (!str || !str.length) {
reject('Input string is empty');
return cb && cb('Input string is empty');
}
var lines = str.split('\n');
if (lines.length <= 1) {
lines = [];
for (var i = 0; i < str.length; i += 94) {
lines.push(str.substr(i, 94));
}
}
var file = {};
var batches = [];
var batchIndex = 0;
var hasAddenda = false;
lines.forEach(function (line) {
if (!line || !line.length) {
return;
}
switch (parseInt(line[0])) {
case 1:
file.header = utils.parseLine(line, fileHeader());
break;
case 9:
file.control = utils.parseLine(line, fileControl);
break;
case 5:
batches.push({
header: utils.parseLine(line, batchHeader),
entry: [],
addenda: [],
});
break;
case 8:
batches[batchIndex].control = utils.parseLine(line, batchControl);
batchIndex++;
break;
case 6:
batches[batchIndex].entry.push(
new Entry(utils.parseLine(line, entryFields))
);
break;
case 7:
batches[batchIndex].entry[
batches[batchIndex].entry.length - 1
].addAddenda(new Addenda(utils.parseLine(line, addendaFields)));
hasAddenda = true;
break;
}
});
if (!file.header || !file.control) {
reject('File records parse error');
return cb && cb('File records parse error');
}
if (!batches || !batches.length) {
reject('No batches found');
return cb && cb('No batches found');
}
try {
var nachFile;
if (!hasAddenda) {
nachFile = new File(file.header);
} else {
nachFile = new File(file.header, false);
}
batches.forEach(function (batchOb) {
var batch = new Batch(batchOb.header);
batchOb.entry.forEach(function (entry) {
batch.addEntry(entry);
});
nachFile.addBatch(batch);
});
cb && cb(undefined, nachFile);
resolve(nachFile);
} catch (e) {
reject(e);
return cb && cb(e);
}
});
};
module.exports = File;