147 lines
4.5 KiB
JavaScript
147 lines
4.5 KiB
JavaScript
const { all, audit, now, one, run } = require('../db');
|
|
const { money } = require('../depreciation');
|
|
|
|
const FREQUENCIES = { monthly: 12, quarterly: 4, semiannual: 2, annual: 1 };
|
|
|
|
function periodsPerYear(frequency) {
|
|
return FREQUENCIES[frequency] || 12;
|
|
}
|
|
|
|
function addMonths(date, count) {
|
|
const next = new Date(date.getTime());
|
|
next.setMonth(next.getMonth() + count);
|
|
return next;
|
|
}
|
|
|
|
function listLeases(query = {}) {
|
|
return all(
|
|
`SELECT l.*, a.asset_id AS asset_code, a.description AS asset_description
|
|
FROM leases l
|
|
LEFT JOIN assets a ON a.id = l.asset_id
|
|
WHERE (? IS NULL OR l.asset_id = ?)
|
|
ORDER BY l.start_date DESC, l.id DESC`,
|
|
[query.asset_id || null, query.asset_id || null]
|
|
);
|
|
}
|
|
|
|
function getLease(id) {
|
|
return one('SELECT * FROM leases WHERE id = ?', [id]);
|
|
}
|
|
|
|
function createLease(input, userId) {
|
|
const timestamp = now();
|
|
const result = run(
|
|
`INSERT INTO leases (asset_id, lessor, contract_value, start_date, end_date, discount_rate, payment_frequency, notes, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
input.asset_id || null,
|
|
input.lessor || null,
|
|
Number(input.contract_value || 0),
|
|
input.start_date || null,
|
|
input.end_date || null,
|
|
Number(input.discount_rate || 0),
|
|
input.payment_frequency || 'monthly',
|
|
input.notes || null,
|
|
timestamp,
|
|
timestamp
|
|
]
|
|
);
|
|
audit(userId, 'create', 'lease', result.lastInsertRowid, null, input);
|
|
return getLease(result.lastInsertRowid);
|
|
}
|
|
|
|
function updateLease(id, input, userId) {
|
|
const before = getLease(id);
|
|
if (!before) return null;
|
|
run(
|
|
`UPDATE leases SET
|
|
lessor = ?, contract_value = ?, start_date = ?, end_date = ?, discount_rate = ?,
|
|
payment_frequency = ?, notes = ?, updated_at = ?
|
|
WHERE id = ?`,
|
|
[
|
|
input.lessor ?? before.lessor,
|
|
Number(input.contract_value ?? before.contract_value),
|
|
input.start_date ?? before.start_date,
|
|
input.end_date ?? before.end_date,
|
|
Number(input.discount_rate ?? before.discount_rate),
|
|
input.payment_frequency ?? before.payment_frequency,
|
|
input.notes ?? before.notes,
|
|
now(),
|
|
id
|
|
]
|
|
);
|
|
audit(userId, 'update', 'lease', id, before, input);
|
|
return getLease(id);
|
|
}
|
|
|
|
function deleteLease(id, userId) {
|
|
const before = getLease(id);
|
|
if (!before) return false;
|
|
run('DELETE FROM leases WHERE id = ?', [id]);
|
|
audit(userId, 'delete', 'lease', id, before, null);
|
|
return true;
|
|
}
|
|
|
|
// Straight-line ROU amortization with an ASC 842-style liability schedule.
|
|
function leaseSchedule(lease) {
|
|
if (!lease) return null;
|
|
const ppy = periodsPerYear(lease.payment_frequency);
|
|
const monthsPerPeriod = 12 / ppy;
|
|
const start = lease.start_date ? new Date(`${lease.start_date}T00:00:00`) : new Date();
|
|
const end = lease.end_date ? new Date(`${lease.end_date}T00:00:00`) : addMonths(start, 12);
|
|
|
|
const totalMonths = Math.max(monthsPerPeriod, (end.getFullYear() - start.getFullYear()) * 12 + (end.getMonth() - start.getMonth()));
|
|
const periods = Math.max(1, Math.round(totalMonths / monthsPerPeriod));
|
|
|
|
const contractValue = Number(lease.contract_value || 0);
|
|
const payment = contractValue / periods;
|
|
const rate = Number(lease.discount_rate || 0) / 100 / ppy;
|
|
const presentValue = rate > 0 ? payment * (1 - (1 + rate) ** -periods) / rate : contractValue;
|
|
|
|
const rouAmortization = presentValue / periods;
|
|
let liability = presentValue;
|
|
let rouBalance = presentValue;
|
|
let totalInterest = 0;
|
|
const rows = [];
|
|
|
|
for (let period = 1; period <= periods; period += 1) {
|
|
const interest = liability * rate;
|
|
const principal = payment - interest;
|
|
liability = Math.max(0, liability - principal);
|
|
rouBalance = Math.max(0, rouBalance - rouAmortization);
|
|
totalInterest += interest;
|
|
rows.push({
|
|
period,
|
|
date: addMonths(start, period * monthsPerPeriod).toISOString().slice(0, 10),
|
|
payment: money(payment),
|
|
interest: money(interest),
|
|
principal: money(principal),
|
|
liability_balance: money(liability),
|
|
rou_amortization: money(rouAmortization),
|
|
rou_balance: money(rouBalance)
|
|
});
|
|
}
|
|
|
|
return {
|
|
summary: {
|
|
periods,
|
|
frequency: lease.payment_frequency,
|
|
periodic_payment: money(payment),
|
|
present_value: money(presentValue),
|
|
total_payments: money(payment * periods),
|
|
total_interest: money(totalInterest)
|
|
},
|
|
rows
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
FREQUENCIES,
|
|
createLease,
|
|
deleteLease,
|
|
getLease,
|
|
leaseSchedule,
|
|
listLeases,
|
|
updateLease
|
|
};
|