const { all, audit, now, one, run, tx } = require('../db'); const { annualSchedule, money } = require('../depreciation'); const { ruleSetForBook } = require('./ruleSets'); const { hydrateAsset } = require('./assets'); const DISPOSAL_METHODS = ['sale', 'exchange', 'involuntary_conversion', 'like_kind', 'abandonment']; // Reject a subledger write whose effective date falls in a closed period (primary book). No-op until a // period is actually locked. Lazy-required to avoid the closePeriods↔services cycle. function guardSubledger(assetId, date) { const row = one('SELECT entity_id FROM assets WHERE id = ?', [assetId]); if (row && date) require('./closePeriods').assertSubledgerOpen(row.entity_id, date); } function yearOf(value) { if (!value) return new Date().getFullYear(); return new Date(`${value}T00:00:00`).getFullYear(); } function netAdjustments(assetId, bookType) { // Impairments and write-downs reduce carrying value; write-ups/revaluations restore it. const rows = all('SELECT type, amount FROM asset_adjustments WHERE asset_id = ? AND book_type = ?', [assetId, bookType]); return rows.reduce((sum, row) => { const sign = row.type === 'write_up' || row.type === 'revaluation' ? -1 : 1; return sum + sign * Number(row.amount || 0); }, 0); } function bookFor(asset, bookType) { return asset.books.find((book) => book.book_type === bookType) || asset.books[0] || { book_type: bookType }; } function computeDisposal(asset, input = {}) { const bookType = input.book_type || 'GAAP'; const book = bookFor(asset, bookType); const rules = ruleSetForBook(bookType); const disposalDate = input.disposal_date || new Date().toISOString().slice(0, 10); const year = yearOf(disposalDate); const totalCost = Number(book.cost ?? asset.acquisition_cost ?? 0); const partial = Boolean(input.partial); const disposedCost = partial ? Math.min(totalCost, Number(input.disposed_cost || 0)) : totalCost; const proportion = totalCost > 0 ? disposedCost / totalCost : 1; const row = annualSchedule(asset, book, rules, { startYear: year, endYear: year })[0] || {}; const accumulatedFull = Number(row.accumulated || 0); const nbvFull = row.netBookValue != null ? Number(row.netBookValue) : Math.max(0, totalCost - accumulatedFull); const accumulated = money(accumulatedFull * proportion); const impairment = money(netAdjustments(asset.id, bookType) * proportion); const netBookValue = money(Math.max(0, nbvFull * proportion - impairment)); const price = Number(input.disposal_price || 0); const expense = Number(input.disposal_expense || 0); const realized = money(price - expense); const gainLoss = money(realized - netBookValue); return { book_type: bookType, disposal_date: disposalDate, method: DISPOSAL_METHODS.includes(input.method) ? input.method : 'sale', property_type: input.property_type || asset.property_type || null, partial: partial ? 1 : 0, disposed_cost: money(disposedCost), total_cost: money(totalCost), disposal_price: money(price), disposal_expense: money(expense), accumulated_depreciation: accumulated, impairment, net_book_value: netBookValue, realized, gain_loss: gainLoss }; } function previewDisposal(assetId, input) { const asset = hydrateAsset(assetId); if (!asset) return null; return computeDisposal(asset, input); } function recordDisposal(assetId, input, userId) { const asset = hydrateAsset(assetId); if (!asset) return null; const result = computeDisposal(asset, input); guardSubledger(assetId, result.disposal_date); const timestamp = now(); return tx(() => { const inserted = run( `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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ 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 ] ); if (result.partial) { const totalCost = result.total_cost; const remainingProportion = totalCost > 0 ? Math.max(0, (totalCost - result.disposed_cost) / totalCost) : 1; run('UPDATE assets SET acquisition_cost = acquisition_cost - ?, updated_at = ? WHERE id = ?', [ result.disposed_cost, timestamp, assetId ]); run('UPDATE asset_books SET cost = cost * ? WHERE asset_id = ? AND cost IS NOT NULL', [remainingProportion, assetId]); } else { run( `UPDATE assets SET status = 'disposed', disposal_date = ?, disposal_price = ?, disposal_expense = ?, disposal_type = ?, property_type = ?, updated_at = ? WHERE id = ?`, [result.disposal_date, result.disposal_price, result.disposal_expense, result.method, result.property_type, timestamp, assetId] ); } const disposal = one('SELECT * FROM disposals WHERE id = ?', [inserted.lastInsertRowid]); audit(userId, 'dispose', 'asset', assetId, null, disposal); return { disposal, asset: hydrateAsset(assetId) }; }); } function reverseDisposal(assetId, disposalId, userId) { const disposal = one('SELECT * FROM disposals WHERE id = ? AND asset_id = ?', [disposalId, assetId]); if (!disposal) return null; guardSubledger(assetId, disposal.disposal_date); const timestamp = now(); return tx(() => { run('DELETE FROM disposals WHERE id = ?', [disposalId]); if (disposal.partial) { run('UPDATE assets SET acquisition_cost = acquisition_cost + ?, updated_at = ? WHERE id = ?', [ disposal.disposed_cost, timestamp, assetId ]); } else if (!one('SELECT id FROM disposals WHERE asset_id = ? AND partial = 0', [assetId])) { run( `UPDATE assets SET status = 'in_service', disposal_date = NULL, disposal_price = NULL, disposal_expense = NULL, disposal_type = NULL, updated_at = ? WHERE id = ?`, [timestamp, assetId] ); } audit(userId, 'reverse_disposal', 'asset', assetId, disposal, null); return hydrateAsset(assetId); }); } function recordAdjustment(assetId, input, userId) { const asset = one('SELECT id FROM assets WHERE id = ?', [assetId]); if (!asset) return null; const timestamp = now(); guardSubledger(assetId, input.adjustment_date || timestamp.slice(0, 10)); const result = run( `INSERT INTO asset_adjustments (asset_id, adjustment_date, type, amount, book_type, reason, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [ assetId, input.adjustment_date || timestamp.slice(0, 10), input.type || 'impairment', Number(input.amount || 0), input.book_type || 'GAAP', input.reason || null, userId, timestamp ] ); const adjustment = one('SELECT * FROM asset_adjustments WHERE id = ?', [result.lastInsertRowid]); audit(userId, 'adjust', 'asset', assetId, null, adjustment); return adjustment; } function deleteAdjustment(assetId, adjustmentId, userId) { const result = run('DELETE FROM asset_adjustments WHERE id = ? AND asset_id = ?', [adjustmentId, assetId]); if (!result.changes) return false; audit(userId, 'delete', 'asset_adjustment', adjustmentId, null, { asset_id: assetId }); return true; } module.exports = { DISPOSAL_METHODS, computeDisposal, deleteAdjustment, netAdjustments, previewDisposal, recordAdjustment, recordDisposal, reverseDisposal };