Pub 946 Compliance

This commit is contained in:
2026-06-10 02:09:25 -05:00
parent 7a54d1a386
commit 221ccb7826
18 changed files with 863 additions and 19 deletions

View File

@@ -1,5 +1,5 @@
const { all, audit, now, one, run, tx } = require('../db');
const { annualSchedule, money } = require('../depreciation');
const { annualSchedule, money, section280fRecapture } = require('../depreciation');
const { ruleSetForBook } = require('./ruleSets');
const { hydrateAsset } = require('./assets');
@@ -30,6 +30,44 @@ function bookFor(asset, bookType) {
return asset.books.find((book) => book.book_type === bookType) || asset.books[0] || { book_type: bookType };
}
// Real property (§1250) vs personal property (§1245), by recovery period: 27.5-year residential rental
// and 39-year nonresidential real property are §1250; everything else is §1245.
function isRealProperty(asset, book) {
const months = Number(book.useful_life_months || asset.useful_life_months || 0);
const pt = String(asset.property_type || '').toLowerCase();
return months >= 330 || /real|realty|1250|building/.test(pt);
}
// Characterize the gain on disposal into ordinary-income recapture vs. §1231 (capital) gain (Form 4797).
// §1245: ordinary recapture = lesser of gain or total depreciation taken (incl. §179/bonus). §1250:
// ordinary recapture = lesser of gain or "additional depreciation" (accelerated over straight-line); the
// remaining gain up to straight-line taken is unrecaptured §1250 gain (a 25% capital bucket).
function characterizeRecapture(asset, book, rules, year, proportion, gainLoss, accumulated) {
const section = isRealProperty(asset, book) ? '1250' : '1245';
const gain = Math.max(0, gainLoss);
let ordinary = 0;
let unrecaptured1250 = 0;
if (gain > 0) {
if (section === '1245') {
ordinary = Math.min(gain, accumulated);
} else {
// Straight-line accumulated for the same period (additional depreciation = accelerated SL).
const slBook = { ...book, depreciation_method: 'straight_line', section_179_amount: 0, bonus_percent: 0 };
const slRow = annualSchedule(asset, slBook, rules, { startYear: year, endYear: year })[0] || {};
const slAccum = money(Number(slRow.accumulated || 0) * proportion);
const additional = Math.max(0, money(accumulated - slAccum));
ordinary = Math.min(gain, additional);
unrecaptured1250 = Math.min(money(gain - ordinary), slAccum);
}
}
return {
recapture_section: section,
ordinary_recapture: money(ordinary),
unrecaptured_1250_gain: money(unrecaptured1250),
section_1231_gain: money(gainLoss - ordinary) // negative for a §1231 loss
};
}
function computeDisposal(asset, input = {}) {
const bookType = input.book_type || 'GAAP';
const book = bookFor(asset, bookType);
@@ -55,6 +93,10 @@ function computeDisposal(asset, input = {}) {
const realized = money(price - expense);
const gainLoss = money(realized - netBookValue);
// Recapture characterization is computed on the disposed book's depreciation (use a tax book — FEDERAL —
// for tax recapture). "Depreciation taken" includes the impairment write-down already reflected in NBV.
const recapture = characterizeRecapture(asset, book, rules, year, proportion, gainLoss, money(accumulated + impairment));
return {
book_type: bookType,
disposal_date: disposalDate,
@@ -69,7 +111,8 @@ function computeDisposal(asset, input = {}) {
impairment,
net_book_value: netBookValue,
realized,
gain_loss: gainLoss
gain_loss: gainLoss,
...recapture
};
}
@@ -79,7 +122,18 @@ function previewDisposal(assetId, input) {
return computeDisposal(asset, input);
}
// §280F recapture preview for a listed/passenger asset converting to ≤50% business use in a given year.
function previewSection280fRecapture(assetId, input = {}) {
const asset = hydrateAsset(assetId);
if (!asset) return null;
const bookType = input.book || 'FEDERAL';
const book = bookFor(asset, bookType);
const year = Number(input.year) || new Date().getFullYear();
return section280fRecapture(asset, book, ruleSetForBook(bookType), year);
}
function recordDisposal(assetId, input, userId) {
require('./midQuarter').clearCache();
const asset = hydrateAsset(assetId);
if (!asset) return null;
const result = computeDisposal(asset, input);
@@ -91,13 +145,15 @@ function recordDisposal(assetId, input, userId) {
`INSERT INTO disposals (
asset_id, disposal_date, method, property_type, partial, disposed_cost,
disposal_price, disposal_expense, accumulated_depreciation, net_book_value,
gain_loss, book_type, notes, created_by, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
gain_loss, book_type, recapture_section, ordinary_recapture, section_1231_gain,
unrecaptured_1250_gain, notes, created_by, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
assetId, result.disposal_date, result.method, result.property_type, result.partial,
result.disposed_cost, result.disposal_price, result.disposal_expense,
result.accumulated_depreciation, result.net_book_value, result.gain_loss,
result.book_type, input.notes || null, userId, timestamp
result.book_type, result.recapture_section, result.ordinary_recapture, result.section_1231_gain,
result.unrecaptured_1250_gain, input.notes || null, userId, timestamp
]
);
@@ -127,6 +183,7 @@ function recordDisposal(assetId, input, userId) {
}
function reverseDisposal(assetId, disposalId, userId) {
require('./midQuarter').clearCache();
const disposal = one('SELECT * FROM disposals WHERE id = ? AND asset_id = ?', [disposalId, assetId]);
if (!disposal) return null;
guardSubledger(assetId, disposal.disposal_date);
@@ -191,6 +248,7 @@ module.exports = {
deleteAdjustment,
netAdjustments,
previewDisposal,
previewSection280fRecapture,
recordAdjustment,
recordDisposal,
reverseDisposal