Pub 225 Compliance

This commit is contained in:
2026-06-10 15:23:02 -05:00
parent 221ccb7826
commit 5074a3ad63
16 changed files with 434 additions and 24 deletions

View File

@@ -6,7 +6,7 @@
salvage value, and conventions is included.
*/
const { parseJson } = require('./db');
const { one, parseJson } = require('./db');
const { buildDepreciationMethods } = require('./rule-catalog');
const { getZone } = require('./services/zones');
const { getLimit: getSection179Limit } = require('./services/section179Limits');
@@ -233,6 +233,17 @@ function listedAdsRequired(asset, book, methodRule) {
return use > 0 && use <= 0.5;
}
// ADS recovery period (in months) for an asset, from its IRS asset class when available; otherwise the
// asset's configured life. Used when ADS is forced (listed property ≤50% business use).
function adsRecoveryMonths(asset, book) {
const classNumber = asset.asset_class_number;
if (classNumber) {
const row = one('SELECT ads FROM asset_classes WHERE class_number = ? AND ads IS NOT NULL AND ads > 0 ORDER BY id LIMIT 1', [String(classNumber)]);
if (row && Number(row.ads) > 0) return Math.round(Number(row.ads) * 12);
}
return Number(book.useful_life_months || asset.useful_life_months || 60);
}
// §280F passenger-automobile annual depreciation cap for a given recovery year (0-based index), reduced
// by business-use percentage. Returns Infinity when the asset isn't a flagged passenger auto or no cap
// table is configured for the placed-in-service year (leaving depreciation uncapped).
@@ -430,6 +441,18 @@ function decliningBalance(asset, book, index, totalYears, multiplier, methodRule
}
// Rate table depreciation: (basis * rate). The basis is net of Section 179, bonus, and salvage, since those reduce the amount recoverable through standard depreciation methods. The rate is determined by the method rule's rates array based on the year index, with an optional fallback to straight-line when no rates are provided.
// MACRS optional percentage tables (Pub 946 Appendix A). Applies the published half-year percentage to
// the depreciable basis when a literal table exists for the recovery period; otherwise (mid-quarter or an
// unsupported period) falls back to the equivalent declining-balance formula.
function macrsTable(asset, book, index, methodRule, convention) {
const years = Math.max(1, Math.round((methodRule.lifeMonths || book.useful_life_months || asset.useful_life_months || 60) / 12));
const table = convention === 'half_year' ? require('./macrs-tables').gdsHalfYear(years) : null;
if (!table) {
return decliningBalance(asset, book, index, years, Number(methodRule.rateMultiplier || 2), methodRule);
}
return depreciableBasis(asset, book, methodRule) * (Number(table[index] || 0) / 100);
}
function rateTable(asset, book, index, methodRule) {
console.log(`${moment().format()} 🕒 Calculating rate table depreciation for asset ${asset.id}, index ${index}`);
const basis = depreciableBasis(asset, book, methodRule);
@@ -461,8 +484,9 @@ function annualSchedule(asset, book, ruleSet, options = {}) {
// or bonus allowance (the ADS recovery period uses the asset's configured life). `ignoreListedAds`
// computes the as-if-accelerated path used by the §280F recapture preview.
if (!options.ignoreListedAds && listedAdsRequired(asset, book, methodRule)) {
book = { ...book, section_179_amount: 0, bonus_percent: 0 };
methodRule = { ...methodRule, formula: 'straight_line', label: `${methodRule.label || methodRule.code} (ADS — listed ≤50% use)` };
const adsMonths = adsRecoveryMonths(asset, book);
book = { ...book, section_179_amount: 0, bonus_percent: 0, useful_life_months: adsMonths };
methodRule = { ...methodRule, formula: 'straight_line', lifeMonths: adsMonths, label: `${methodRule.label || methodRule.code} (ADS — listed ≤50% use)` };
}
// Apply a special-zone §168 allowance when the book has no explicit bonus of its own.
if (!(Number(book.bonus_percent) > 0)) {
@@ -493,7 +517,10 @@ function annualSchedule(asset, book, ruleSet, options = {}) {
}
console.log(`${moment().format()} 🕒 Evaluating method rule for asset ${asset.id}, year ${year}`);
if (methodRule.formula === 'rate_table') {
if (methodRule.formula === 'macrs_table') {
// MACRS optional percentage tables (literal Pub 946 Appendix A) with formula fallback.
depreciation += macrsTable(asset, book, index, methodRule, conventionFor(book, methodRule, asset));
} else if (methodRule.formula === 'rate_table') {
// Note: for the rate table formula, we look up the rate for the current year index and apply it to the depreciable basis; if no rates are defined but a fallback to straight-line is specified, we use the straight-line formula instead with the total years determined by the method rule's lifeMonths or the book's useful_life_months. This allows for flexible handling of cases where a rate table is desired but specific rates haven't been configured, while still providing a reasonable default calculation.
console.log(`${moment().format()} 🕒 Using rate table formula for asset ${asset.id}, year ${year}`);
depreciation += rateTable(asset, book, index, methodRule);
@@ -529,6 +556,12 @@ function annualSchedule(asset, book, ruleSet, options = {}) {
if (allowed <= 0 && remaining > 0) allowed = Math.min(cap280f, remaining);
depreciation = allowed;
}
// Short tax year: prorate the year's MACRS depreciation by (months ÷ 12) for the company (simplified
// method; default fraction 1 leaves normal years unchanged). Lazy-required to avoid a load cycle.
if (index >= 0 && asset.entity_id) {
const f = require('./services/shortTaxYear').fraction(asset.entity_id, year);
if (f !== 1) depreciation *= f;
}
const cap = recoverableBasis(asset, book, methodRule);
depreciation = Math.max(0, Math.min(depreciation, Math.max(0, cap - accumulated)));
accumulated += depreciation;