// node_api/jobs/depreciationDatabase.js const { Pool } = require('pg'); const { logger } = require('../utils/logger'); class DepreciationDatabase { constructor() { this.pool = new Pool({ host: process.env.DB_HOST || 'localhost', port: process.env.DB_PORT || 5432, database: process.env.DB_DATABASE || 'directus', user: process.env.DB_USERNAME || 'directus', password: process.env.DB_PASSWORD || 'directus', max: 10, idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, }); } /** * Get all assets eligible for depreciation */ async getAssetsForDepreciation(calculationDate = new Date()) { const client = await this.pool.connect(); try { const query = ` SELECT a.id, a.name, a.acquisition_cost, a.acquisition_date, a.depreciation_method, a.depreciation_percentage_rate, a.expected_useful_life_months, a.salvage_value, a.total_accumulated_depreciation, a.net_book_value, a.status, a.expected_total_units, a.current_month_units, a.organization_id, a.date_created, a.date_updated FROM assets a WHERE a.status = 'active' AND a.acquisition_cost > 0 AND a.acquisition_date IS NOT NULL AND a.acquisition_date <= $1 AND a.depreciation_method IS NOT NULL AND ( a.total_accumulated_depreciation IS NULL OR a.total_accumulated_depreciation < (a.acquisition_cost - COALESCE(a.salvage_value, 0)) ) ORDER BY a.acquisition_date ASC `; const result = await client.query(query, [calculationDate]); logger.depreciation(`Found ${result.rows.length} assets eligible for depreciation`, { calculationDate, assetCount: result.rows.length }); return result.rows; } catch (error) { logger.error('Error fetching assets for depreciation:', error); throw error; } finally { client.release(); } } /** * Get the last depreciation record for an asset */ async getLastDepreciationRecord(assetId) { const client = await this.pool.connect(); try { const query = ` SELECT * FROM asset_depreciation_records WHERE asset_id = $1 ORDER BY depreciation_date DESC LIMIT 1 `; const result = await client.query(query, [assetId]); return result.rows[0] || null; } catch (error) { logger.error(`Error fetching last depreciation record for asset ${assetId}:`, error); throw error; } finally { client.release(); } } /** * Check if depreciation record exists for asset and date */ async depreciationRecordExists(assetId, depreciationDate) { const client = await this.pool.connect(); try { const query = ` SELECT COUNT(*) as count FROM asset_depreciation_records WHERE asset_id = $1 AND DATE_TRUNC('month', depreciation_date) = DATE_TRUNC('month', $2) `; const result = await client.query(query, [assetId, depreciationDate]); return parseInt(result.rows[0].count) > 0; } catch (error) { logger.error(`Error checking depreciation record existence:`, error); throw error; } finally { client.release(); } } /** * Create a new depreciation record */ async createDepreciationRecord(depreciationData) { const client = await this.pool.connect(); try { await client.query('BEGIN'); const { asset_id, depreciation_date, depreciation_method, monthly_depreciation, accumulated_depreciation_before, accumulated_depreciation_after, net_book_value_before, net_book_value_after, calculation_details } = depreciationData; // Insert depreciation record const insertQuery = ` INSERT INTO asset_depreciation_records ( id, asset_id, depreciation_date, depreciation_method, monthly_depreciation, accumulated_depreciation_before, accumulated_depreciation_after, net_book_value_before, net_book_value_after, calculation_details, date_created, date_updated ) VALUES ( gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW() ) RETURNING id `; const insertResult = await client.query(insertQuery, [ asset_id, depreciation_date, depreciation_method, monthly_depreciation, accumulated_depreciation_before, accumulated_depreciation_after, net_book_value_before, net_book_value_after, JSON.stringify(calculation_details) ]); // Update asset's accumulated depreciation and net book value const updateAssetQuery = ` UPDATE assets SET total_accumulated_depreciation = $1, net_book_value = $2, date_updated = NOW() WHERE id = $3 `; await client.query(updateAssetQuery, [ accumulated_depreciation_after, net_book_value_after, asset_id ]); await client.query('COMMIT'); logger.depreciation(`Created depreciation record for asset ${asset_id}`, { recordId: insertResult.rows[0].id, assetId: asset_id, monthlyDepreciation: monthly_depreciation, accumulatedDepreciation: accumulated_depreciation_after }); return insertResult.rows[0].id; } catch (error) { await client.query('ROLLBACK'); logger.error(`Error creating depreciation record:`, error); throw error; } finally { client.release(); } } /** * Get depreciation summary for a specific period */ async getDepreciationSummary(startDate, endDate) { const client = await this.pool.connect(); try { const query = ` SELECT COUNT(*) as total_assets, SUM(monthly_depreciation) as total_monthly_depreciation, AVG(monthly_depreciation) as avg_monthly_depreciation, MIN(monthly_depreciation) as min_monthly_depreciation, MAX(monthly_depreciation) as max_monthly_depreciation, depreciation_method, COUNT(*) as method_count FROM asset_depreciation_records WHERE depreciation_date BETWEEN $1 AND $2 GROUP BY depreciation_method ORDER BY method_count DESC `; const result = await client.query(query, [startDate, endDate]); const overallQuery = ` SELECT COUNT(*) as total_assets, SUM(monthly_depreciation) as total_monthly_depreciation, AVG(monthly_depreciation) as avg_monthly_depreciation FROM asset_depreciation_records WHERE depreciation_date BETWEEN $1 AND $2 `; const overallResult = await client.query(overallQuery, [startDate, endDate]); return { overall: overallResult.rows[0], byMethod: result.rows }; } catch (error) { logger.error('Error getting depreciation summary:', error); throw error; } finally { client.release(); } } /** * Get assets with depreciation issues */ async getDepreciationIssues() { const client = await this.pool.connect(); try { const query = ` SELECT a.id, a.name, a.acquisition_cost, a.total_accumulated_depreciation, a.net_book_value, a.depreciation_method, CASE WHEN a.total_accumulated_depreciation > a.acquisition_cost THEN 'over_depreciated' WHEN a.net_book_value < 0 THEN 'negative_book_value' WHEN a.net_book_value != (a.acquisition_cost - COALESCE(a.total_accumulated_depreciation, 0)) THEN 'calculation_mismatch' ELSE 'unknown' END as issue_type FROM assets a WHERE a.status = 'active' AND ( a.total_accumulated_depreciation > a.acquisition_cost OR a.net_book_value < 0 OR a.net_book_value != (a.acquisition_cost - COALESCE(a.total_accumulated_depreciation, 0)) ) ORDER BY a.name `; const result = await client.query(query); return result.rows; } catch (error) { logger.error('Error getting depreciation issues:', error); throw error; } finally { client.release(); } } /** * Fix calculation mismatches */ async fixCalculationMismatches() { const client = await this.pool.connect(); try { await client.query('BEGIN'); const updateQuery = ` UPDATE assets SET net_book_value = acquisition_cost - COALESCE(total_accumulated_depreciation, 0), date_updated = NOW() WHERE status = 'active' AND net_book_value != (acquisition_cost - COALESCE(total_accumulated_depreciation, 0)) `; const result = await client.query(updateQuery); await client.query('COMMIT'); logger.depreciation(`Fixed calculation mismatches for ${result.rowCount} assets`); return result.rowCount; } catch (error) { await client.query('ROLLBACK'); logger.error('Error fixing calculation mismatches:', error); throw error; } finally { client.release(); } } /** * Get monthly depreciation report */ async getMonthlyDepreciationReport(year, month) { const client = await this.pool.connect(); try { const query = ` SELECT a.id, a.name, a.acquisition_cost, dr.monthly_depreciation, dr.accumulated_depreciation_before, dr.accumulated_depreciation_after, dr.net_book_value_before, dr.net_book_value_after, dr.depreciation_method, dr.depreciation_date, c.name as category_name, l.name as location_name FROM asset_depreciation_records dr JOIN assets a ON dr.asset_id = a.id LEFT JOIN categories c ON a.category_id = c.id LEFT JOIN locations l ON a.location_id = l.id WHERE EXTRACT(YEAR FROM dr.depreciation_date) = $1 AND EXTRACT(MONTH FROM dr.depreciation_date) = $2 ORDER BY dr.monthly_depreciation DESC `; const result = await client.query(query, [year, month]); return result.rows; } catch (error) { logger.error('Error getting monthly depreciation report:', error); throw error; } finally { client.release(); } } /** * Get all asset components eligible for depreciation */ async getAssetComponentsForDepreciation(calculationDate = new Date()) { const client = await this.pool.connect(); try { const query = ` SELECT ac.id, ac.name, ac.component_identifier, ac.parent_asset_id, ac.acquisition_cost, ac.acquisition_date, ac.depreciation_method, ac.depreciation_percentage_rate, ac.expected_useful_life_months, ac.salvage_value, ac.total_accumulated_depreciation, ac.net_book_value, ac.status, ac.expected_total_units, ac.current_month_units, ac.organization_id, ac.date_created, ac.date_updated, a.name as parent_asset_name, a.asset_identifier as parent_asset_identifier FROM asset_components ac JOIN assets a ON ac.parent_asset_id = a.id WHERE ac.status = 'active' AND ac.acquisition_cost > 0 AND ac.acquisition_date IS NOT NULL AND ac.acquisition_date <= $1 AND ac.depreciation_method IS NOT NULL AND ( ac.total_accumulated_depreciation IS NULL OR ac.total_accumulated_depreciation < (ac.acquisition_cost - COALESCE(ac.salvage_value, 0)) ) ORDER BY ac.acquisition_date ASC `; const result = await client.query(query, [calculationDate]); logger.depreciation(`Found ${result.rows.length} asset components eligible for depreciation`, { calculationDate, componentCount: result.rows.length }); return result.rows; } catch (error) { logger.error('Error fetching asset components for depreciation:', error); throw error; } finally { client.release(); } } /** * Get the last depreciation record for an asset component */ async getLastComponentDepreciationRecord(componentId) { const client = await this.pool.connect(); try { const query = ` SELECT * FROM asset_component_depreciation_records WHERE component_id = $1 ORDER BY depreciation_date DESC LIMIT 1 `; const result = await client.query(query, [componentId]); return result.rows[0] || null; } catch (error) { logger.error(`Error fetching last depreciation record for component ${componentId}:`, error); throw error; } finally { client.release(); } } /** * Check if depreciation record exists for component and date */ async componentDepreciationRecordExists(componentId, depreciationDate) { const client = await this.pool.connect(); try { const query = ` SELECT COUNT(*) as count FROM asset_component_depreciation_records WHERE component_id = $1 AND DATE_TRUNC('month', depreciation_date) = DATE_TRUNC('month', $2) `; const result = await client.query(query, [componentId, depreciationDate]); return parseInt(result.rows[0].count) > 0; } catch (error) { logger.error(`Error checking component depreciation record existence:`, error); throw error; } finally { client.release(); } } /** * Create a new asset component depreciation record */ async createComponentDepreciationRecord(depreciationData) { const client = await this.pool.connect(); try { await client.query('BEGIN'); const { component_id, asset_id, depreciation_date, depreciation_method, monthly_depreciation, accumulated_depreciation_before, accumulated_depreciation_after, net_book_value_before, net_book_value_after, calculation_details } = depreciationData; // Insert component depreciation record const insertQuery = ` INSERT INTO asset_component_depreciation_records ( id, component_id, asset_id, depreciation_date, depreciation_method, monthly_depreciation, accumulated_depreciation_before, accumulated_depreciation_after, net_book_value_before, net_book_value_after, calculation_details, date_created, date_updated ) VALUES ( gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW() ) RETURNING id `; const insertResult = await client.query(insertQuery, [ component_id, asset_id, depreciation_date, depreciation_method, monthly_depreciation, accumulated_depreciation_before, accumulated_depreciation_after, net_book_value_before, net_book_value_after, JSON.stringify(calculation_details) ]); // Update component's accumulated depreciation and net book value const updateComponentQuery = ` UPDATE asset_components SET total_accumulated_depreciation = $1, net_book_value = $2, date_updated = NOW() WHERE id = $3 `; await client.query(updateComponentQuery, [ accumulated_depreciation_after, net_book_value_after, component_id ]); await client.query('COMMIT'); logger.depreciation(`Created component depreciation record for component ${component_id}`, { recordId: insertResult.rows[0].id, componentId: component_id, assetId: asset_id, monthlyDepreciation: monthly_depreciation, accumulatedDepreciation: accumulated_depreciation_after }); return insertResult.rows[0].id; } catch (error) { await client.query('ROLLBACK'); logger.error(`Error creating component depreciation record:`, error); throw error; } finally { client.release(); } } /** * Get asset components with depreciation issues */ async getComponentDepreciationIssues() { const client = await this.pool.connect(); try { const query = ` SELECT ac.id, ac.name, ac.component_identifier, ac.parent_asset_id, a.name as parent_asset_name, a.asset_identifier as parent_asset_identifier, ac.acquisition_cost, ac.total_accumulated_depreciation, ac.net_book_value, ac.depreciation_method, CASE WHEN ac.total_accumulated_depreciation > ac.acquisition_cost THEN 'over_depreciated' WHEN ac.net_book_value < 0 THEN 'negative_book_value' WHEN ac.net_book_value != (ac.acquisition_cost - COALESCE(ac.total_accumulated_depreciation, 0)) THEN 'calculation_mismatch' ELSE 'unknown' END as issue_type FROM asset_components ac JOIN assets a ON ac.parent_asset_id = a.id WHERE ac.status = 'active' AND ( ac.total_accumulated_depreciation > ac.acquisition_cost OR ac.net_book_value < 0 OR ac.net_book_value != (ac.acquisition_cost - COALESCE(ac.total_accumulated_depreciation, 0)) ) ORDER BY ac.name `; const result = await client.query(query); return result.rows; } catch (error) { logger.error('Error getting component depreciation issues:', error); throw error; } finally { client.release(); } } /** * Fix component calculation mismatches */ async fixComponentCalculationMismatches() { const client = await this.pool.connect(); try { await client.query('BEGIN'); const updateQuery = ` UPDATE asset_components SET net_book_value = acquisition_cost - COALESCE(total_accumulated_depreciation, 0), date_updated = NOW() WHERE status = 'active' AND net_book_value != (acquisition_cost - COALESCE(total_accumulated_depreciation, 0)) `; const result = await client.query(updateQuery); await client.query('COMMIT'); logger.depreciation(`Fixed component calculation mismatches for ${result.rowCount} components`); return result.rowCount; } catch (error) { await client.query('ROLLBACK'); logger.error('Error fixing component calculation mismatches:', error); throw error; } finally { client.release(); } } /** * Get monthly component depreciation report */ async getMonthlyComponentDepreciationReport(year, month) { const client = await this.pool.connect(); try { const query = ` SELECT ac.id, ac.name, ac.component_identifier, ac.parent_asset_id, a.name as parent_asset_name, a.asset_identifier as parent_asset_identifier, ac.acquisition_cost, dr.monthly_depreciation, dr.accumulated_depreciation_before, dr.accumulated_depreciation_after, dr.net_book_value_before, dr.net_book_value_after, dr.depreciation_method, dr.depreciation_date, c.name as category_name, l.name as location_name FROM asset_component_depreciation_records dr JOIN asset_components ac ON dr.component_id = ac.id JOIN assets a ON dr.asset_id = a.id LEFT JOIN asset_categories c ON a.category_id = c.id LEFT JOIN locations l ON a.location_id = l.id WHERE EXTRACT(YEAR FROM dr.depreciation_date) = $1 AND EXTRACT(MONTH FROM dr.depreciation_date) = $2 ORDER BY dr.monthly_depreciation DESC `; const result = await client.query(query, [year, month]); return result.rows; } catch (error) { logger.error('Error getting monthly component depreciation report:', error); throw error; } finally { client.release(); } } /** * Get component depreciation summary for a period */ async getComponentDepreciationSummary(startDate, endDate) { const client = await this.pool.connect(); try { const query = ` SELECT COUNT(*) as total_components, SUM(monthly_depreciation) as total_monthly_depreciation, AVG(monthly_depreciation) as avg_monthly_depreciation, MIN(monthly_depreciation) as min_monthly_depreciation, MAX(monthly_depreciation) as max_monthly_depreciation, depreciation_method, COUNT(*) as method_count FROM asset_component_depreciation_records WHERE depreciation_date BETWEEN $1 AND $2 GROUP BY depreciation_method ORDER BY method_count DESC `; const result = await client.query(query, [startDate, endDate]); const overallQuery = ` SELECT COUNT(*) as total_components, SUM(monthly_depreciation) as total_monthly_depreciation, AVG(monthly_depreciation) as avg_monthly_depreciation FROM asset_component_depreciation_records WHERE depreciation_date BETWEEN $1 AND $2 `; const overallResult = await client.query(overallQuery, [startDate, endDate]); return { overall: overallResult.rows[0], byMethod: result.rows }; } catch (error) { logger.error('Error getting component depreciation summary:', error); throw error; } finally { client.release(); } } /** * Close database connection pool */ async close() { await this.pool.end(); logger.info('Database connection pool closed'); } } module.exports = { DepreciationDatabase };