-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathindex.js
147 lines (129 loc) · 3.56 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
const moment = require('moment');
const BigNumber = require('bignumber.js');
const sprintf = require('printf');
const toCents = (number = 0) =>
new BigNumber(number)
.round(2)
.times(100)
.toFixed(0);
const sum = totals =>
totals.reduce((p, v) => p.add(new BigNumber(v)), new BigNumber(0)).toFixed(2);
const difference = (credit, debit) =>
Math.abs(new BigNumber(credit).sub(new BigNumber(debit)).toFixed(2));
const format = bsb => {
const value = bsb.replace(/(\s|-)+/, '').trim();
return value ? `${value.slice(0, 3)}-${value.slice(3, 6)}` : '';
};
const PAYMENT_FORMAT = [
'1',
'%(bsb)7s',
'%(account)9s',
'%(tax)1s',
'%(transactionCode)02d',
'%(amount)010d',
'%(accountTitle)-32s',
'%(reference)-18s',
'%(traceBsb)7s',
'%(traceAccount)9s',
'%(remitter)-16s',
'%(taxAmount)08d',
].join('');
// NOTE: Assuming the account in header is blank filled and right justified
// like the detail record, even though the spec from ANZ didn't specify it.
const HEADER_FORMAT = [
'0',
'%(bsb)7s',
'%(account)9s',
' ',
'01',
'%(bank)-3s',
' '.repeat(7),
'%(user)-26s',
'%(userNumber)06d',
'%(description)-12s',
'%(date)6s',
'%(time)4s',
' '.repeat(36),
].join('');
const FOOTER_FORMAT = [
'7',
'999-999',
' '.repeat(12),
'%(net)010d',
'%(credit)010d',
'%(debit)010d',
' '.repeat(24),
'%(length)06d',
' '.repeat(40),
].join('');
class ABA {
constructor(opts) {
this.options = Object.assign({}, ABA.defaults, opts);
}
transaction(transaction) {
return sprintf(
PAYMENT_FORMAT,
Object.assign({}, transaction, {
amount: toCents(transaction.amount),
bsb: format(transaction.bsb),
account: transaction.account.trim(),
traceBsb: format(transaction.traceBsb),
taxAmount: toCents(transaction.taxAmount),
})
);
}
formatHeader() {
return sprintf(HEADER_FORMAT, this.getHeader());
}
getHeader() {
const header = this.options;
const time = moment(header.time || header.date || new Date());
return Object.assign({}, header, {
date: time.format('DDMMYY'),
bsb: format(header.bsb),
time: header.time ? time.format('HHmm') : '',
});
}
formatFooter(transactions) {
return sprintf(FOOTER_FORMAT, this.getFooter(transactions));
}
getFooter(transactions) {
const credits = transactions.filter(p => p.transactionCode === ABA.CREDIT);
const debits = transactions.filter(p => p.transactionCode === ABA.DEBIT);
const credit = sum(credits.map(c => c.amount));
const debit = sum(debits.map(d => d.amount));
return {
// According to spec the net total was supposed to be an unsigned value of
// credit minus debit (with no mention of underflow), but turns out they
// really meant merely the difference between credit and debit.
net: toCents(difference(credit, debit)),
credit: toCents(credit),
debit: toCents(debit),
length: transactions.length,
};
}
generate(transactions = []) {
// ABA requires at least one detail record.
if (!transactions.length) {
throw new Error('Please pass in at least one payment');
}
const formatted = transactions.map(payment =>
this.transaction(Object.assign({}, ABA.PAYMENT_DEFAULTS, payment))
);
const footer = this.formatFooter(transactions);
return [this.formatHeader(), ...formatted, footer].join('\r\n');
}
}
ABA.PAYMENT_DEFAULTS = {
tax: '',
taxAmount: 0,
};
ABA.CREDIT = 50;
ABA.DEBIT = 13;
ABA.defaults = {
bsb: '',
account: '',
description: '',
time: '',
};
module.exports = ABA;