108 lines
5.1 KiB
JavaScript
108 lines
5.1 KiB
JavaScript
const { all, audit, now, one, run } = require('../db');
|
|
const { getLimit } = require('./section179Limits');
|
|
|
|
// §179 return-level taxable-income limitation and carryover (IRS Pub 946 / IRC §179(b)). The per-asset
|
|
// engine already caps each asset's §179 at the dollar limit and phase-out; THIS layer aggregates the
|
|
// §179 elected across a company's assets for a tax year, applies the single annual dollar limit (reduced
|
|
// by the investment phase-out) AND the business taxable-income limitation, and carries any disallowed
|
|
// amount forward to the next year. Stateful: the prior year's carryover feeds the next year's available
|
|
// deduction. Per company + book + year.
|
|
|
|
function yearOf(value) {
|
|
const s = String(value || '');
|
|
return s ? Number(s.slice(0, 4)) : 0;
|
|
}
|
|
function round2(n) {
|
|
return Math.round((Number(n) || 0) * 100) / 100;
|
|
}
|
|
|
|
// §179 elected per asset and the cost of §179 property placed in service, for a (company, book, year).
|
|
function electedFor(companyId, bookCode, year) {
|
|
const rows = all(
|
|
`SELECT a.acquisition_cost, a.in_service_date, a.acquired_date, b.section_179_amount, b.cost
|
|
FROM assets a JOIN asset_books b ON b.asset_id = a.id
|
|
WHERE a.entity_id = ? AND b.book_type = ? AND b.active = 1 AND a.status != 'disposed'`,
|
|
[companyId, bookCode]
|
|
);
|
|
let elected = 0;
|
|
let propertyCost = 0;
|
|
let count = 0;
|
|
for (const r of rows) {
|
|
if (yearOf(r.in_service_date || r.acquired_date) !== Number(year)) continue;
|
|
const amt = Number(r.section_179_amount || 0);
|
|
if (amt <= 0) continue;
|
|
elected += amt;
|
|
propertyCost += Number(r.cost ?? r.acquisition_cost ?? 0);
|
|
count += 1;
|
|
}
|
|
return { elected: round2(elected), propertyCost: round2(propertyCost), count };
|
|
}
|
|
|
|
function savedRow(companyId, bookCode, year) {
|
|
return one('SELECT * FROM section_179_carryover WHERE entity_id = ? AND book_code = ? AND tax_year = ?', [companyId, bookCode, Number(year)]);
|
|
}
|
|
|
|
// Compute the limitation. `taxableIncome` overrides the stored value when provided (null/undefined uses
|
|
// the stored value, if any). The business-income limitation is only applied when an income figure exists.
|
|
function summary(companyId, bookCode, year, taxableIncome) {
|
|
const yr = Number(year);
|
|
const limitRow = getLimit(yr);
|
|
const { elected, propertyCost, count } = electedFor(companyId, bookCode, yr);
|
|
const priorRow = savedRow(companyId, bookCode, yr - 1);
|
|
const priorCarryover = round2(priorRow ? priorRow.carryover_to_next : 0);
|
|
|
|
const baseLimit = limitRow ? Number(limitRow.base_limit) : Infinity;
|
|
const phaseoutThreshold = limitRow ? Number(limitRow.phaseout_threshold) : Infinity;
|
|
const phaseoutReduction = Number.isFinite(phaseoutThreshold) ? Math.max(0, propertyCost - phaseoutThreshold) : 0;
|
|
const dollarLimit = Number.isFinite(baseLimit) ? Math.max(0, baseLimit - phaseoutReduction) : Infinity;
|
|
|
|
const available = round2(elected + priorCarryover);
|
|
const dollarAllowed = round2(Math.min(available, dollarLimit));
|
|
|
|
const stored = savedRow(companyId, bookCode, yr);
|
|
const income = taxableIncome !== undefined && taxableIncome !== null
|
|
? Number(taxableIncome)
|
|
: (stored && stored.taxable_income !== null ? Number(stored.taxable_income) : null);
|
|
const incomeApplied = income !== null && !Number.isNaN(income);
|
|
const allowed = round2(incomeApplied ? Math.min(dollarAllowed, Math.max(0, income)) : dollarAllowed);
|
|
const carryover = round2(Math.max(0, available - allowed));
|
|
|
|
return {
|
|
company_id: companyId,
|
|
book_code: bookCode,
|
|
tax_year: yr,
|
|
asset_count: count,
|
|
elected,
|
|
property_cost: propertyCost,
|
|
prior_carryover: priorCarryover,
|
|
base_limit: Number.isFinite(baseLimit) ? baseLimit : null,
|
|
phaseout_threshold: Number.isFinite(phaseoutThreshold) ? phaseoutThreshold : null,
|
|
phaseout_reduction: round2(phaseoutReduction),
|
|
dollar_limit: Number.isFinite(dollarLimit) ? round2(dollarLimit) : null,
|
|
available,
|
|
dollar_allowed: dollarAllowed,
|
|
taxable_income: incomeApplied ? round2(income) : null,
|
|
income_limited: incomeApplied && allowed < dollarAllowed,
|
|
allowed_deduction: allowed,
|
|
carryover_to_next: carryover
|
|
};
|
|
}
|
|
|
|
// Persist the entered taxable income and the resulting carryover (so the next year picks it up). Audited.
|
|
function save(companyId, bookCode, year, taxableIncome, userId) {
|
|
const result = summary(companyId, bookCode, year, taxableIncome);
|
|
const income = taxableIncome === undefined || taxableIncome === null || taxableIncome === '' ? null : Number(taxableIncome);
|
|
run(
|
|
`INSERT INTO section_179_carryover (entity_id, book_code, tax_year, taxable_income, carryover_to_next, updated_by, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(entity_id, book_code, tax_year) DO UPDATE SET
|
|
taxable_income = excluded.taxable_income, carryover_to_next = excluded.carryover_to_next,
|
|
updated_by = excluded.updated_by, updated_at = excluded.updated_at`,
|
|
[companyId, bookCode, Number(year), income, result.carryover_to_next, userId, now()]
|
|
);
|
|
audit(userId, 'update', 'section_179_limitation', `${bookCode}:${year}`, null, { taxable_income: income, carryover: result.carryover_to_next });
|
|
return result;
|
|
}
|
|
|
|
module.exports = { summary, save };
|