Updated A LOT

This commit is contained in:
2026-06-05 04:15:24 -05:00
parent 8335f1af1b
commit 4072695432
92 changed files with 16139 additions and 570 deletions

View File

@@ -1,4 +1,14 @@
const { parseJson } = require('./db');
const { buildDepreciationMethods } = require('./rule-catalog');
const { getZone } = require('./services/zones');
// Built-in method catalog, used as a fallback so codes like MF200/DB200 always resolve
// even if a particular rule set hasn't been re-expanded to include them.
let methodCatalog = null;
function catalog() {
if (!methodCatalog) methodCatalog = buildDepreciationMethods();
return methodCatalog;
}
function money(value) {
return Math.round((Number(value || 0) + Number.EPSILON) * 100) / 100;
@@ -19,25 +29,59 @@ function parseMaybeJson(value, fallback = null) {
return value ?? fallback;
}
function totalDepreciableBasis(asset, book) {
function businessUseFactor(asset, book) {
return Number(book.business_use_percent ?? asset.business_use_percent ?? 100) / 100;
}
// Cost basis before salvage: acquisition value less land, scaled by business-use %.
function grossBasis(asset, book) {
const cost = Number(book.cost ?? asset.acquisition_cost ?? 0);
const land = Number(asset.land_value || 0);
const salvage = Number(asset.salvage_value || 0);
const businessUse = Number(book.business_use_percent ?? asset.business_use_percent ?? 100) / 100;
return Math.max(0, (cost - land - salvage) * businessUse);
return Math.max(0, (cost - land) * businessUseFactor(asset, book));
}
// Salvage reduces what standard methods recover, but MACRS ignores it (ignoreSalvage flag).
function salvageValue(asset, book, methodRule) {
if (methodRule && methodRule.ignoreSalvage) return 0;
return Math.max(0, Number(asset.salvage_value || 0) * businessUseFactor(asset, book));
}
function section179Amount(asset, book) {
return Math.min(totalDepreciableBasis(asset, book), Number(book.section_179_amount || 0));
return Math.min(grossBasis(asset, book), Number(book.section_179_amount || 0));
}
// Special-zone §168 allowance (e.g. New York Liberty Zone): applies to the federal tax book
// when the asset is flagged for a zone and its placed-in-service date is within the window.
// Returns the allowance % (used as the §168 bonus) or 0.
function zoneAllowance(asset, book) {
if (!asset.special_zone || String(book.book_type) !== 'FEDERAL') return 0;
const zone = getZone(asset.special_zone);
if (!zone || !Number(zone.allowance_percent)) return 0;
const pis = asset.in_service_date || asset.acquired_date || '';
if (pis && zone.pis_start && pis < zone.pis_start) return 0;
if (pis && zone.pis_end && pis > zone.pis_end) return 0;
return Number(zone.allowance_percent);
}
// Section 168 (bonus) allowance, applied after Section 179.
function bonusAmount(asset, book) {
const after179 = Math.max(0, totalDepreciableBasis(asset, book) - section179Amount(asset, book));
const after179 = Math.max(0, grossBasis(asset, book) - section179Amount(asset, book));
return after179 * (Number(book.bonus_percent || 0) / 100);
}
function depreciableBasis(asset, book) {
return Math.max(0, totalDepreciableBasis(asset, book) - section179Amount(asset, book) - bonusAmount(asset, book));
// Total amount recoverable over the asset's life (the schedule cap): cost basis less salvage.
function recoverableBasis(asset, book, methodRule) {
return Math.max(0, grossBasis(asset, book) - salvageValue(asset, book, methodRule));
}
// Amount a straight-line / SYD method depreciates: net of Section 179, bonus, and salvage.
function depreciableBasis(asset, book, methodRule) {
return Math.max(0, grossBasis(asset, book) - section179Amount(asset, book) - bonusAmount(asset, book) - salvageValue(asset, book, methodRule));
}
// Cost basis net of salvage (no 179/bonus) — retained for reports and disposals.
function totalDepreciableBasis(asset, book) {
return Math.max(0, grossBasis(asset, book) - salvageValue(asset, book, null));
}
function conventionFor(book, methodRule) {
@@ -83,31 +127,35 @@ function fractionForIndex(index, convention, asset, totalYears) {
}
function straightLine(asset, book, index, totalYears, methodRule) {
const basis = depreciableBasis(asset, book);
const basis = depreciableBasis(asset, book, methodRule);
return (basis / totalYears) * fractionForIndex(index, conventionFor(book, methodRule), asset, totalYears);
}
function sumOfYearsDigits(asset, book, index, totalYears) {
const basis = depreciableBasis(asset, book);
function sumOfYearsDigits(asset, book, index, totalYears, methodRule) {
const basis = depreciableBasis(asset, book, methodRule);
const denominator = (totalYears * (totalYears + 1)) / 2;
return basis * ((totalYears - index) / denominator);
}
function decliningBalance(asset, book, index, totalYears, multiplier, methodRule) {
const basis = depreciableBasis(asset, book);
let bookValue = basis;
// The declining-balance rate applies to the full (post-179/bonus) book value; depreciation
// is floored at salvage. MACRS methods set salvage to 0 via ignoreSalvage.
const start = Math.max(0, grossBasis(asset, book) - section179Amount(asset, book) - bonusAmount(asset, book));
const salvage = salvageValue(asset, book, methodRule);
let bookValue = start;
let elapsed = 0;
const convention = conventionFor(book, methodRule);
for (let i = 0; i <= index; i += 1) {
const fraction = fractionForIndex(i, convention, asset, totalYears);
if (fraction <= 0 || bookValue <= 0) return 0;
const recoverable = Math.max(0, bookValue - salvage);
if (fraction <= 0 || recoverable <= 0) return 0;
const dbAmount = bookValue * (multiplier / totalYears) * fraction;
const remainingLife = Math.max(0.0001, totalYears - elapsed);
const slAmount = (bookValue / remainingLife) * fraction;
const slAmount = (recoverable / remainingLife) * fraction;
const selected = methodRule.switchToStraightLine ? Math.max(dbAmount, slAmount) : dbAmount;
const amount = Math.min(bookValue, selected);
const amount = Math.min(recoverable, selected);
if (i === index) return amount;
bookValue -= amount;
@@ -117,7 +165,7 @@ function decliningBalance(asset, book, index, totalYears, multiplier, methodRule
}
function rateTable(asset, book, index, methodRule) {
const basis = depreciableBasis(asset, book);
const basis = depreciableBasis(asset, book, methodRule);
const rates = methodRule.rates || [];
if (!rates.length && methodRule.fallbackFormula === 'straight_line') {
return straightLine(asset, book, index, Math.max(1, Math.ceil((methodRule.lifeMonths || book.useful_life_months || 60) / 12)), methodRule);
@@ -127,15 +175,18 @@ function rateTable(asset, book, index, methodRule) {
function getMethodRule(ruleSet, code) {
const rules = typeof ruleSet === 'string' ? parseJson(ruleSet, {}) : ruleSet || {};
return (rules.methods || []).find((method) => method.code === code) || {
code,
formula: code,
label: code
};
return (rules.methods || []).find((method) => method.code === code)
|| catalog().find((method) => method.code === code)
|| { code, formula: code, label: code };
}
function annualSchedule(asset, book, ruleSet, options = {}) {
const methodRule = getMethodRule(ruleSet, book.depreciation_method || 'straight_line');
// Apply a special-zone §168 allowance when the book has no explicit bonus of its own.
if (!(Number(book.bonus_percent) > 0)) {
const allowance = zoneAllowance(asset, book);
if (allowance > 0) book = { ...book, bonus_percent: allowance };
}
const usefulLifeMonths = Number(methodRule.lifeMonths || book.useful_life_months || asset.useful_life_months || 60);
const totalYears = Math.max(1, Math.ceil(usefulLifeMonths / 12));
const placedInServiceYear = yearFromDate(asset.in_service_date || asset.acquired_date);
@@ -156,7 +207,7 @@ function annualSchedule(asset, book, ruleSet, options = {}) {
if (methodRule.formula === 'rate_table') {
depreciation += rateTable(asset, book, index, methodRule);
} else if (methodRule.formula === 'sum_of_years_digits') {
depreciation += sumOfYearsDigits(asset, book, index, totalYears);
depreciation += sumOfYearsDigits(asset, book, index, totalYears, methodRule);
} else if (methodRule.formula === 'declining_balance' || methodRule.formula === 'macrs_declining_balance') {
depreciation += decliningBalance(asset, book, index, totalYears, Number(methodRule.rateMultiplier || 2), methodRule);
} else if (methodRule.formula === 'acrs_alternate_straight_line') {
@@ -168,8 +219,8 @@ function annualSchedule(asset, book, ruleSet, options = {}) {
}
}
const basis = totalDepreciableBasis(asset, book);
depreciation = Math.max(0, Math.min(depreciation, Math.max(0, basis - accumulated)));
const cap = recoverableBasis(asset, book, methodRule);
depreciation = Math.max(0, Math.min(depreciation, Math.max(0, cap - accumulated)));
accumulated += depreciation;
if (year >= startYear) {