feat: store payment installments in a transactions collection
Move embedded payment rows into Transaction documents with due dates so remaining tuition can be tracked separately from paid amounts.
This commit is contained in:
+26
-1
@@ -22,9 +22,34 @@ const sanitizeNotes = (notes) => {
|
||||
return String(notes).trim().slice(0, NOTES_MAX_LENGTH);
|
||||
};
|
||||
|
||||
const rialsToToman = (value) => Math.floor(toNonNegativeNumber(value) / 10);
|
||||
|
||||
const isPaidTransaction = (trx = {}) => {
|
||||
const status = String(trx.status || '').toLowerCase();
|
||||
if (status === 'pending') return false;
|
||||
if (status === 'paid') return true;
|
||||
return Boolean(trx.date);
|
||||
};
|
||||
|
||||
const sumPaidTransactions = (transactions = []) => {
|
||||
if (!Array.isArray(transactions)) return 0;
|
||||
return transactions.reduce((sum, trx) => {
|
||||
if (!isPaidTransaction(trx)) return sum;
|
||||
return sum + toNonNegativeNumber(trx.amount);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const remainingPayable = (payment = {}, transactions = []) => {
|
||||
return Math.max(0, getPayableAmount(payment) - sumPaidTransactions(transactions));
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
NOTES_MAX_LENGTH,
|
||||
getPayableAmount,
|
||||
normalizeDiscount,
|
||||
sanitizeNotes
|
||||
sanitizeNotes,
|
||||
rialsToToman,
|
||||
isPaidTransaction,
|
||||
sumPaidTransactions,
|
||||
remainingPayable
|
||||
};
|
||||
|
||||
@@ -6,7 +6,11 @@ const {
|
||||
getPayableAmount,
|
||||
normalizeDiscount,
|
||||
sanitizeNotes,
|
||||
NOTES_MAX_LENGTH
|
||||
NOTES_MAX_LENGTH,
|
||||
rialsToToman,
|
||||
isPaidTransaction,
|
||||
sumPaidTransactions,
|
||||
remainingPayable
|
||||
} = require('./paymentAmount');
|
||||
|
||||
describe('payment amount helpers', () => {
|
||||
@@ -36,6 +40,41 @@ describe('payment amount helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('rialsToToman', () => {
|
||||
it('drops one zero so spreadsheet Rials become website Toman', () => {
|
||||
assert.equal(rialsToToman(110_000_000), 11_000_000);
|
||||
assert.equal(rialsToToman(5_000_000), 500_000);
|
||||
assert.equal(rialsToToman(0), 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('paid vs pending transactions', () => {
|
||||
it('counts only paid transactions toward the paid total', () => {
|
||||
const transactions = [
|
||||
{ amount: 8_000_000, status: 'paid', date: '2026-08-03' },
|
||||
{ amount: 3_000_000, status: 'pending', dueDate: '2026-08-23' }
|
||||
];
|
||||
assert.equal(sumPaidTransactions(transactions), 8_000_000);
|
||||
assert.equal(
|
||||
remainingPayable({ amount: 11_000_000, discount: 0 }, transactions),
|
||||
3_000_000
|
||||
);
|
||||
});
|
||||
|
||||
it('treats a 10m billed course as 1m off an 11m tuition', () => {
|
||||
assert.equal(getPayableAmount({ amount: 11_000_000, discount: 1_000_000 }), 10_000_000);
|
||||
});
|
||||
|
||||
it('treats an 8m billed course as 3m off an 11m tuition', () => {
|
||||
assert.equal(getPayableAmount({ amount: 11_000_000, discount: 3_000_000 }), 8_000_000);
|
||||
});
|
||||
|
||||
it('does not treat pending remainder rows as paid', () => {
|
||||
assert.equal(isPaidTransaction({ status: 'pending', amount: 2_000_000 }), false);
|
||||
assert.equal(isPaidTransaction({ status: 'paid', amount: 8_000_000 }), true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeNotes', () => {
|
||||
it('trims notes and caps length', () => {
|
||||
assert.equal(sanitizeNotes(' hello '), 'hello');
|
||||
|
||||
Reference in New Issue
Block a user