import { BadRequestException, Injectable } from '@nestjs/common'; import { ProcurementRepository } from './procurement.repository'; import { Vendor } from './entities/vendor.entity'; import { AcquisitionType, AssetAcquisition } from './entities/asset-acquisition.entity'; import { AssetDisposal } from './entities/asset-disposal.entity'; import { CreateVendorDto, UpdateVendorDto, CreateAcquisitionDto, UpdateAcquisitionDto, CreateDisposalDto, } from './dto/procurement.dto'; export interface DepreciationResult { method: 'STRAIGHT_LINE'; cost: number; salvageValue: number; usefulLifeMonths: number; monthsElapsed: number; monthlyDepreciation: number; bookValue: number; } export interface LifecycleResult { vehicleId: string; acquisition: AssetAcquisition | null; disposal: AssetDisposal | null; depreciation: DepreciationResult | null; } @Injectable() export class ProcurementService { constructor(private readonly procurementRepository: ProcurementRepository) {} // ---- Vendors ---- async createVendor(dto: CreateVendorDto): Promise { return this.procurementRepository.createVendor(dto); } async listVendors(): Promise { return this.procurementRepository.findVendors(); } async updateVendor(id: string, dto: UpdateVendorDto): Promise { return this.procurementRepository.updateVendor(id, dto); } async deleteVendor(id: string): Promise<{ success: boolean }> { await this.procurementRepository.softDeleteVendor(id); return { success: true }; } // ---- Acquisitions ---- /** Lease terms only make sense on LEASE / RENTAL — a PURCHASE must not carry them. */ private assertLeaseFieldsValid(dto: { acquisitionType?: string; leaseStart?: string; leaseEnd?: string; monthlyPayment?: number; }): void { if (dto.acquisitionType !== AcquisitionType.PURCHASE) return; if (dto.leaseStart || dto.leaseEnd || dto.monthlyPayment != null) { throw new BadRequestException( 'Lease start/end and monthly payment are not valid for a PURCHASE acquisition', ); } } async createAcquisition(dto: CreateAcquisitionDto): Promise { this.assertLeaseFieldsValid(dto); return this.procurementRepository.createAcquisition(dto); } async listAcquisitions(vehicleId?: string): Promise { return this.procurementRepository.findAcquisitions(vehicleId); } async getAcquisition(id: string): Promise { return this.procurementRepository.findAcquisitionById(id); } async updateAcquisition(id: string, dto: UpdateAcquisitionDto): Promise { // Validate against the resulting record, not just the patch — switching an // acquisition to PURCHASE must also shed any stored lease terms. const existing = await this.procurementRepository.findAcquisitionById(id); if (existing) { const next = { ...existing, ...dto }; if (next.acquisitionType === AcquisitionType.PURCHASE) { this.assertLeaseFieldsValid({ acquisitionType: next.acquisitionType, leaseStart: dto.leaseStart, leaseEnd: dto.leaseEnd, monthlyPayment: dto.monthlyPayment, }); } } return this.procurementRepository.updateAcquisition(id, dto); } async deleteAcquisition(id: string): Promise<{ success: boolean }> { await this.procurementRepository.softDeleteAcquisition(id); return { success: true }; } // ---- Disposals ---- async createDisposal(dto: CreateDisposalDto): Promise { return this.procurementRepository.createDisposal(dto); } async listDisposals(): Promise { return this.procurementRepository.findDisposals(); } async deleteDisposal(id: string): Promise<{ success: boolean }> { await this.procurementRepository.softDeleteDisposal(id); return { success: true }; } // ---- Lifecycle ---- async lifecycle(vehicleId: string): Promise { const acquisition = await this.procurementRepository.findLatestAcquisitionByVehicle(vehicleId); const disposal = await this.procurementRepository.findLatestDisposalByVehicle(vehicleId); return { vehicleId, acquisition, disposal, depreciation: this.computeStraightLineDepreciation(acquisition), }; } /** * Straight-line depreciation. Requires a cost and a positive useful life. * monthlyDep = (cost - salvageValue) / usefulLifeMonths * bookValue = cost - monthlyDep * monthsElapsedSinceAcquisition, floored at salvageValue. */ private computeStraightLineDepreciation( acquisition: AssetAcquisition | null, ): DepreciationResult | null { if (!acquisition) return null; const cost = acquisition.cost != null ? Number(acquisition.cost) : null; const usefulLifeMonths = acquisition.usefulLifeMonths != null ? Number(acquisition.usefulLifeMonths) : null; if (cost == null || usefulLifeMonths == null || usefulLifeMonths <= 0) { return null; } const salvageValue = acquisition.salvageValue != null ? Number(acquisition.salvageValue) : 0; const monthlyDepreciation = (cost - salvageValue) / usefulLifeMonths; const acquiredAt = new Date(acquisition.acquisitionDate); const now = new Date(); const monthsElapsed = Math.max( 0, (now.getFullYear() - acquiredAt.getFullYear()) * 12 + (now.getMonth() - acquiredAt.getMonth()), ); const bookValue = Math.max(cost - monthlyDepreciation * monthsElapsed, salvageValue); return { method: 'STRAIGHT_LINE', cost, salvageValue, usefulLifeMonths, monthsElapsed, monthlyDepreciation, bookValue, }; } }