// node_api/jobs/depreciationJob.js const { DepreciationCalculator } = require('./depreciationCalculator'); const { DepreciationDatabase } = require('./depreciationDatabase'); const { logger } = require('../utils/logger'); class DepreciationJob { constructor() { this.calculator = new DepreciationCalculator(); this.database = new DepreciationDatabase(); } /** * Run monthly depreciation calculation for all eligible assets and components */ async run(calculationDate = new Date()) { const startTime = Date.now(); const jobId = `depreciation_${calculationDate.toISOString().slice(0, 7)}`; logger.job(`Starting depreciation job ${jobId}`, { calculationDate }); try { // Get all assets eligible for depreciation const assets = await this.database.getAssetsForDepreciation(calculationDate); // Get all asset components eligible for depreciation const components = await this.database.getAssetComponentsForDepreciation(calculationDate); if (assets.length === 0 && components.length === 0) { logger.job('No assets or components found for depreciation processing'); return { success: true, processedAssets: 0, processedComponents: 0, skippedAssets: 0, skippedComponents: 0, errors: [], duration: Date.now() - startTime }; } const results = { processedAssets: 0, processedComponents: 0, skippedAssets: 0, skippedComponents: 0, errors: [], summary: { totalMonthlyDepreciation: 0, assetDepreciation: 0, componentDepreciation: 0, byMethod: {} } }; // Process each asset for (const asset of assets) { try { const result = await this.processAssetDepreciation(asset, calculationDate); if (result.skipped) { results.skippedAssets++; logger.depreciation(`Skipped asset ${asset.id}: ${result.reason}`); } else { results.processedAssets++; results.summary.totalMonthlyDepreciation += result.monthlyDepreciation; results.summary.assetDepreciation += result.monthlyDepreciation; // Track by method const method = result.method; if (!results.summary.byMethod[method]) { results.summary.byMethod[method] = { count: 0, totalDepreciation: 0, assets: 0, components: 0 }; } results.summary.byMethod[method].count++; results.summary.byMethod[method].assets++; results.summary.byMethod[method].totalDepreciation += result.monthlyDepreciation; } } catch (error) { results.errors.push({ type: 'asset', assetId: asset.id, assetName: asset.name, error: error.message }); logger.error(`Error processing asset ${asset.id}:`, error); } } // Process each asset component for (const component of components) { try { const result = await this.processComponentDepreciation(component, calculationDate); if (result.skipped) { results.skippedComponents++; logger.depreciation(`Skipped component ${component.id}: ${result.reason}`); } else { results.processedComponents++; results.summary.totalMonthlyDepreciation += result.monthlyDepreciation; results.summary.componentDepreciation += result.monthlyDepreciation; // Track by method const method = result.method; if (!results.summary.byMethod[method]) { results.summary.byMethod[method] = { count: 0, totalDepreciation: 0, assets: 0, components: 0 }; } results.summary.byMethod[method].count++; results.summary.byMethod[method].components++; results.summary.byMethod[method].totalDepreciation += result.monthlyDepreciation; } } catch (error) { results.errors.push({ type: 'component', componentId: component.id, componentName: component.name, parentAssetId: component.parent_asset_id, parentAssetName: component.parent_asset_name, error: error.message }); logger.error(`Error processing component ${component.id}:`, error); } } // Generate final report const duration = Date.now() - startTime; const finalResult = { success: true, jobId, calculationDate, duration, ...results }; logger.job(`Completed depreciation job ${jobId}`, finalResult); // Send summary email if configured await this.sendSummaryNotification(finalResult); return finalResult; } catch (error) { logger.error('Depreciation job failed:', error); throw error; } } /** * Process depreciation for a single asset */ async processAssetDepreciation(asset, calculationDate) { // Check if depreciation record already exists for this month const recordExists = await this.database.depreciationRecordExists(asset.id, calculationDate); if (recordExists) { return { skipped: true, reason: 'Depreciation record already exists for this month' }; } // Validate asset for depreciation const validationErrors = this.calculator.validateAsset(asset); if (validationErrors.length > 0) { return { skipped: true, reason: `Validation failed: ${validationErrors.join(', ')}` }; } // Check if asset should be depreciated if (!this.calculator.shouldDepreciate(asset, calculationDate)) { return { skipped: true, reason: 'Asset not eligible for depreciation' }; } // Calculate monthly depreciation const calculation = this.calculator.calculateMonthlyDepreciation(asset, calculationDate); // Prepare depreciation record data const currentAccumulatedDepreciation = parseFloat(asset.total_accumulated_depreciation) || 0; const currentNetBookValue = parseFloat(asset.net_book_value) || parseFloat(asset.acquisition_cost); const newAccumulatedDepreciation = currentAccumulatedDepreciation + calculation.monthlyDepreciation; const newNetBookValue = parseFloat(asset.acquisition_cost) - newAccumulatedDepreciation; // Ensure we don't depreciate below salvage value const salvageValue = parseFloat(asset.salvage_value) || 0; const adjustedNewNetBookValue = Math.max(salvageValue, newNetBookValue); const adjustedMonthlyDepreciation = currentNetBookValue - adjustedNewNetBookValue; const adjustedAccumulatedDepreciation = currentAccumulatedDepreciation + adjustedMonthlyDepreciation; const depreciationData = { asset_id: asset.id, depreciation_date: calculationDate, depreciation_method: asset.depreciation_method, monthly_depreciation: adjustedMonthlyDepreciation, accumulated_depreciation_before: currentAccumulatedDepreciation, accumulated_depreciation_after: adjustedAccumulatedDepreciation, net_book_value_before: currentNetBookValue, net_book_value_after: adjustedNewNetBookValue, calculation_details: calculation.calculation }; // Create depreciation record const recordId = await this.database.createDepreciationRecord(depreciationData); return { skipped: false, recordId, method: asset.depreciation_method, monthlyDepreciation: adjustedMonthlyDepreciation, accumulatedDepreciation: adjustedAccumulatedDepreciation, netBookValue: adjustedNewNetBookValue }; } /** * Process depreciation for a single asset component */ async processComponentDepreciation(component, calculationDate) { // Check if depreciation record already exists for this month const recordExists = await this.database.componentDepreciationRecordExists(component.id, calculationDate); if (recordExists) { return { skipped: true, reason: 'Component depreciation record already exists for this month' }; } // Validate component for depreciation const validationErrors = this.calculator.validateAsset(component); if (validationErrors.length > 0) { return { skipped: true, reason: `Validation failed: ${validationErrors.join(', ')}` }; } // Check if component should be depreciated if (!this.calculator.shouldDepreciate(component, calculationDate)) { return { skipped: true, reason: 'Component not eligible for depreciation' }; } // Calculate monthly depreciation const calculation = this.calculator.calculateMonthlyDepreciation(component, calculationDate); // Prepare depreciation record data const currentAccumulatedDepreciation = parseFloat(component.total_accumulated_depreciation) || 0; const currentNetBookValue = parseFloat(component.net_book_value) || parseFloat(component.acquisition_cost); const newAccumulatedDepreciation = currentAccumulatedDepreciation + calculation.monthlyDepreciation; const newNetBookValue = parseFloat(component.acquisition_cost) - newAccumulatedDepreciation; // Ensure we don't depreciate below salvage value const salvageValue = parseFloat(component.salvage_value) || 0; const adjustedNewNetBookValue = Math.max(salvageValue, newNetBookValue); const adjustedMonthlyDepreciation = currentNetBookValue - adjustedNewNetBookValue; const adjustedAccumulatedDepreciation = currentAccumulatedDepreciation + adjustedMonthlyDepreciation; const depreciationData = { component_id: component.id, asset_id: component.parent_asset_id, depreciation_date: calculationDate, depreciation_method: component.depreciation_method, monthly_depreciation: adjustedMonthlyDepreciation, accumulated_depreciation_before: currentAccumulatedDepreciation, accumulated_depreciation_after: adjustedAccumulatedDepreciation, net_book_value_before: currentNetBookValue, net_book_value_after: adjustedNewNetBookValue, calculation_details: calculation.calculation }; // Create depreciation record const recordId = await this.database.createComponentDepreciationRecord(depreciationData); return { skipped: false, recordId, method: component.depreciation_method, monthlyDepreciation: adjustedMonthlyDepreciation, accumulatedDepreciation: adjustedAccumulatedDepreciation, netBookValue: adjustedNewNetBookValue }; } /** * Send summary notification (email, webhook, etc.) */ async sendSummaryNotification(result) { // TODO: Implement email notification logger.job('Depreciation job summary notification', { processedAssets: result.processedAssets, processedComponents: result.processedComponents, skippedAssets: result.skippedAssets, skippedComponents: result.skippedComponents, errors: result.errors.length, totalDepreciation: result.summary.totalMonthlyDepreciation, assetDepreciation: result.summary.assetDepreciation, componentDepreciation: result.summary.componentDepreciation, duration: result.duration }); } /** * Run depreciation for a specific asset (manual execution) */ async runForAsset(assetId, calculationDate = new Date()) { try { const assets = await this.database.getAssetsForDepreciation(calculationDate); const asset = assets.find(a => a.id === assetId); if (!asset) { throw new Error(`Asset ${assetId} not found or not eligible for depreciation`); } const result = await this.processAssetDepreciation(asset, calculationDate); logger.depreciation(`Manual depreciation calculation for asset ${assetId}`, result); return result; } catch (error) { logger.error(`Error running manual depreciation for asset ${assetId}:`, error); throw error; } } /** * Get depreciation preview without saving to database */ async previewDepreciation(assetId, calculationDate = new Date()) { try { const assets = await this.database.getAssetsForDepreciation(calculationDate); const asset = assets.find(a => a.id === assetId); if (!asset) { throw new Error(`Asset ${assetId} not found or not eligible for depreciation`); } const calculation = this.calculator.calculateMonthlyDepreciation(asset, calculationDate); const currentAccumulatedDepreciation = parseFloat(asset.total_accumulated_depreciation) || 0; const currentNetBookValue = parseFloat(asset.net_book_value) || parseFloat(asset.acquisition_cost); return { asset: { id: asset.id, name: asset.name, acquisitionCost: asset.acquisition_cost, currentAccumulatedDepreciation, currentNetBookValue }, calculation: { method: asset.depreciation_method, monthlyDepreciation: calculation.monthlyDepreciation, newAccumulatedDepreciation: currentAccumulatedDepreciation + calculation.monthlyDepreciation, newNetBookValue: currentNetBookValue - calculation.monthlyDepreciation, calculationDetails: calculation.calculation } }; } catch (error) { logger.error(`Error previewing depreciation for asset ${assetId}:`, error); throw error; } } } // Export function for job scheduler const depreciationJob = async (calculationDate) => { const job = new DepreciationJob(); return await job.run(calculationDate); }; module.exports = { DepreciationJob, depreciationJob };