Updated A LOT
This commit is contained in:
146
server/services/leases.js
Normal file
146
server/services/leases.js
Normal file
@@ -0,0 +1,146 @@
|
||||
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
|
||||
};
|
||||
Reference in New Issue
Block a user