Files
MixedAssets/server/services/leases.js
2026-06-05 04:15:24 -05:00

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
};