enterprise_assest_managemen.../node_api/jobs/depreciationJob.js

385 lines
14 KiB
JavaScript

// 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 };