385 lines
14 KiB
JavaScript
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 }; |