mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
fleet
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import {
|
||||
IsUUID,
|
||||
IsString,
|
||||
IsDateString,
|
||||
IsNumber,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsEnum,
|
||||
IsBoolean,
|
||||
} from 'class-validator';
|
||||
import { VendorType } from '../entities/vendor.entity';
|
||||
import { AcquisitionType, AcquisitionStatus } from '../entities/asset-acquisition.entity';
|
||||
import { DisposalMethod } from '../entities/asset-disposal.entity';
|
||||
|
||||
export class CreateVendorDto {
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(VendorType)
|
||||
type?: VendorType;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contactPerson?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
address?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export class UpdateVendorDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(VendorType)
|
||||
type?: VendorType;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contactPerson?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
address?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export class CreateAcquisitionDto {
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
vehicleId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
vendorId?: string;
|
||||
|
||||
@IsEnum(AcquisitionType)
|
||||
acquisitionType!: AcquisitionType;
|
||||
|
||||
@IsDateString()
|
||||
acquisitionDate!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
cost?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
usefulLifeMonths?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
salvageValue?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
leaseStart?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
leaseEnd?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
monthlyPayment?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(AcquisitionStatus)
|
||||
status?: AcquisitionStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class UpdateAcquisitionDto {
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
vehicleId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
vendorId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(AcquisitionType)
|
||||
acquisitionType?: AcquisitionType;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
acquisitionDate?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
cost?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
usefulLifeMonths?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
salvageValue?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
leaseStart?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
leaseEnd?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
monthlyPayment?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(AcquisitionStatus)
|
||||
status?: AcquisitionStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class CreateDisposalDto {
|
||||
@IsUUID()
|
||||
vehicleId!: string;
|
||||
|
||||
@IsDateString()
|
||||
disposalDate!: string;
|
||||
|
||||
@IsEnum(DisposalMethod)
|
||||
method!: DisposalMethod;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
salePrice?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
buyer?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Entity, Column, ManyToOne, JoinColumn, Index } from 'typeorm';
|
||||
import { Vehicle } from '../../vehicles/entities/vehicle.entity';
|
||||
import { Vendor } from './vendor.entity';
|
||||
|
||||
export enum AcquisitionType {
|
||||
PURCHASE = 'PURCHASE',
|
||||
LEASE = 'LEASE',
|
||||
RENTAL = 'RENTAL',
|
||||
}
|
||||
|
||||
export enum AcquisitionStatus {
|
||||
ACTIVE = 'ACTIVE',
|
||||
LEASE_EXPIRING = 'LEASE_EXPIRING',
|
||||
DISPOSED = 'DISPOSED',
|
||||
}
|
||||
|
||||
@Entity({ name: 'asset_acquisitions', schema: 'freight' })
|
||||
@Index(['vehicleId', 'acquisitionDate'])
|
||||
export class AssetAcquisition extends BaseEntity {
|
||||
@Column({ name: 'vehicle_id', type: 'uuid', nullable: true })
|
||||
vehicleId?: string;
|
||||
|
||||
@ManyToOne(() => Vehicle, { eager: false, nullable: true, onDelete: 'SET NULL' })
|
||||
@JoinColumn({ name: 'vehicle_id' })
|
||||
vehicle?: Vehicle;
|
||||
|
||||
@Column({ name: 'vendor_id', type: 'uuid', nullable: true })
|
||||
vendorId?: string;
|
||||
|
||||
@ManyToOne(() => Vendor, { eager: false, nullable: true, onDelete: 'SET NULL' })
|
||||
@JoinColumn({ name: 'vendor_id' })
|
||||
vendor?: Vendor;
|
||||
|
||||
@Column({ name: 'acquisition_type', type: 'varchar' })
|
||||
acquisitionType!: AcquisitionType;
|
||||
|
||||
@Column({ name: 'acquisition_date', type: 'date' })
|
||||
acquisitionDate!: string;
|
||||
|
||||
@Column({ name: 'cost', type: 'numeric', precision: 14, scale: 2, nullable: true })
|
||||
cost?: number;
|
||||
|
||||
@Column({ name: 'useful_life_months', type: 'int', nullable: true })
|
||||
usefulLifeMonths?: number;
|
||||
|
||||
@Column({ name: 'salvage_value', type: 'numeric', precision: 14, scale: 2, nullable: true })
|
||||
salvageValue?: number;
|
||||
|
||||
@Column({ name: 'lease_start', type: 'date', nullable: true })
|
||||
leaseStart?: string;
|
||||
|
||||
@Column({ name: 'lease_end', type: 'date', nullable: true })
|
||||
leaseEnd?: string;
|
||||
|
||||
@Column({ name: 'monthly_payment', type: 'numeric', precision: 14, scale: 2, nullable: true })
|
||||
monthlyPayment?: number;
|
||||
|
||||
@Column({ name: 'status', type: 'varchar', default: AcquisitionStatus.ACTIVE })
|
||||
status!: AcquisitionStatus;
|
||||
|
||||
@Column({ name: 'notes', type: 'text', nullable: true })
|
||||
notes?: string;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Entity, Column, Index } from 'typeorm';
|
||||
|
||||
export enum DisposalMethod {
|
||||
SALE = 'SALE',
|
||||
SCRAP = 'SCRAP',
|
||||
RETURN_LEASE = 'RETURN_LEASE',
|
||||
TRADE_IN = 'TRADE_IN',
|
||||
}
|
||||
|
||||
@Entity({ name: 'asset_disposals', schema: 'freight' })
|
||||
@Index(['vehicleId', 'disposalDate'])
|
||||
export class AssetDisposal extends BaseEntity {
|
||||
@Column({ name: 'vehicle_id', type: 'uuid' })
|
||||
vehicleId!: string;
|
||||
|
||||
@Column({ name: 'disposal_date', type: 'date' })
|
||||
disposalDate!: string;
|
||||
|
||||
@Column({ name: 'method', type: 'varchar' })
|
||||
method!: DisposalMethod;
|
||||
|
||||
@Column({ name: 'sale_price', type: 'numeric', precision: 14, scale: 2, nullable: true })
|
||||
salePrice?: number;
|
||||
|
||||
@Column({ name: 'buyer', nullable: true })
|
||||
buyer?: string;
|
||||
|
||||
@Column({ name: 'notes', type: 'text', nullable: true })
|
||||
notes?: string;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Entity, Column } from 'typeorm';
|
||||
|
||||
export enum VendorType {
|
||||
DEALER = 'DEALER',
|
||||
LEASING = 'LEASING',
|
||||
PARTS = 'PARTS',
|
||||
SERVICE = 'SERVICE',
|
||||
OTHER = 'OTHER',
|
||||
}
|
||||
|
||||
@Entity({ name: 'vendors', schema: 'freight' })
|
||||
export class Vendor extends BaseEntity {
|
||||
@Column({ name: 'name' })
|
||||
name!: string;
|
||||
|
||||
@Column({ name: 'type', type: 'varchar', nullable: true })
|
||||
type?: VendorType;
|
||||
|
||||
@Column({ name: 'contact_person', nullable: true })
|
||||
contactPerson?: string;
|
||||
|
||||
@Column({ name: 'phone', nullable: true })
|
||||
phone?: string;
|
||||
|
||||
@Column({ name: 'email', nullable: true })
|
||||
email?: string;
|
||||
|
||||
@Column({ name: 'address', nullable: true })
|
||||
address?: string;
|
||||
|
||||
@Column({ name: 'is_active', type: 'boolean', default: true })
|
||||
isActive!: boolean;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Controller, Post, Get, Patch, Delete, Body, Param, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { ProcurementService } from './procurement.service';
|
||||
import {
|
||||
CreateVendorDto,
|
||||
UpdateVendorDto,
|
||||
CreateAcquisitionDto,
|
||||
UpdateAcquisitionDto,
|
||||
CreateDisposalDto,
|
||||
} from './dto/procurement.dto';
|
||||
|
||||
@ApiTags('Procurement & Asset Lifecycle')
|
||||
@Controller('procurement')
|
||||
export class ProcurementController {
|
||||
constructor(private readonly procurementService: ProcurementService) {}
|
||||
|
||||
// ---- Vendors ----
|
||||
@Post('vendors')
|
||||
@ApiOperation({ summary: 'Create a vendor' })
|
||||
async createVendor(@Body() dto: CreateVendorDto) {
|
||||
return this.procurementService.createVendor(dto);
|
||||
}
|
||||
|
||||
@Get('vendors')
|
||||
@ApiOperation({ summary: 'List vendors' })
|
||||
async listVendors() {
|
||||
return this.procurementService.listVendors();
|
||||
}
|
||||
|
||||
@Patch('vendors/:id')
|
||||
@ApiOperation({ summary: 'Update a vendor' })
|
||||
async updateVendor(@Param('id') id: string, @Body() dto: UpdateVendorDto) {
|
||||
return this.procurementService.updateVendor(id, dto);
|
||||
}
|
||||
|
||||
@Delete('vendors/:id')
|
||||
@ApiOperation({ summary: 'Delete a vendor' })
|
||||
async deleteVendor(@Param('id') id: string) {
|
||||
return this.procurementService.deleteVendor(id);
|
||||
}
|
||||
|
||||
// ---- Acquisitions ----
|
||||
@Post('acquisitions')
|
||||
@ApiOperation({ summary: 'Create an asset acquisition' })
|
||||
async createAcquisition(@Body() dto: CreateAcquisitionDto) {
|
||||
return this.procurementService.createAcquisition(dto);
|
||||
}
|
||||
|
||||
@Get('acquisitions')
|
||||
@ApiOperation({ summary: 'List asset acquisitions (optionally filtered by vehicleId)' })
|
||||
async listAcquisitions(@Query('vehicleId') vehicleId?: string) {
|
||||
return this.procurementService.listAcquisitions(vehicleId);
|
||||
}
|
||||
|
||||
@Get('acquisitions/:id')
|
||||
@ApiOperation({ summary: 'Get an asset acquisition by id' })
|
||||
async getAcquisition(@Param('id') id: string) {
|
||||
return this.procurementService.getAcquisition(id);
|
||||
}
|
||||
|
||||
@Patch('acquisitions/:id')
|
||||
@ApiOperation({ summary: 'Update an asset acquisition' })
|
||||
async updateAcquisition(@Param('id') id: string, @Body() dto: UpdateAcquisitionDto) {
|
||||
return this.procurementService.updateAcquisition(id, dto);
|
||||
}
|
||||
|
||||
@Delete('acquisitions/:id')
|
||||
@ApiOperation({ summary: 'Delete an asset acquisition' })
|
||||
async deleteAcquisition(@Param('id') id: string) {
|
||||
return this.procurementService.deleteAcquisition(id);
|
||||
}
|
||||
|
||||
// ---- Disposals ----
|
||||
@Post('disposals')
|
||||
@ApiOperation({ summary: 'Create an asset disposal' })
|
||||
async createDisposal(@Body() dto: CreateDisposalDto) {
|
||||
return this.procurementService.createDisposal(dto);
|
||||
}
|
||||
|
||||
@Get('disposals')
|
||||
@ApiOperation({ summary: 'List asset disposals' })
|
||||
async listDisposals() {
|
||||
return this.procurementService.listDisposals();
|
||||
}
|
||||
|
||||
@Delete('disposals/:id')
|
||||
@ApiOperation({ summary: 'Delete an asset disposal' })
|
||||
async deleteDisposal(@Param('id') id: string) {
|
||||
return this.procurementService.deleteDisposal(id);
|
||||
}
|
||||
|
||||
// ---- Lifecycle ----
|
||||
@Get('lifecycle/:vehicleId')
|
||||
@ApiOperation({ summary: 'Get asset lifecycle (acquisition, disposal, depreciation) for a vehicle' })
|
||||
async lifecycle(@Param('vehicleId') vehicleId: string) {
|
||||
return this.procurementService.lifecycle(vehicleId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Vendor } from './entities/vendor.entity';
|
||||
import { AssetAcquisition } from './entities/asset-acquisition.entity';
|
||||
import { AssetDisposal } from './entities/asset-disposal.entity';
|
||||
import { ProcurementService } from './procurement.service';
|
||||
import { ProcurementRepository } from './procurement.repository';
|
||||
import { ProcurementController } from './procurement.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Vendor, AssetAcquisition, AssetDisposal])],
|
||||
providers: [ProcurementService, ProcurementRepository],
|
||||
controllers: [ProcurementController],
|
||||
exports: [ProcurementService],
|
||||
})
|
||||
export class ProcurementModule {}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { BaseRepository } from '@edr/api-common';
|
||||
import { DeepPartial, Repository } from 'typeorm';
|
||||
import { Vendor } from './entities/vendor.entity';
|
||||
import { AssetAcquisition } from './entities/asset-acquisition.entity';
|
||||
import { AssetDisposal } from './entities/asset-disposal.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ProcurementRepository extends BaseRepository<AssetAcquisition> {
|
||||
constructor(
|
||||
@InjectRepository(AssetAcquisition)
|
||||
private readonly acquisitionRepository: Repository<AssetAcquisition>,
|
||||
@InjectRepository(Vendor)
|
||||
private readonly vendorRepository: Repository<Vendor>,
|
||||
@InjectRepository(AssetDisposal)
|
||||
private readonly disposalRepository: Repository<AssetDisposal>,
|
||||
) {
|
||||
super(acquisitionRepository);
|
||||
}
|
||||
|
||||
// ---- Vendors ----
|
||||
async createVendor(data: DeepPartial<Vendor>): Promise<Vendor> {
|
||||
const vendor = this.vendorRepository.create(data);
|
||||
return this.vendorRepository.save(vendor);
|
||||
}
|
||||
|
||||
async findVendors(): Promise<Vendor[]> {
|
||||
return this.vendorRepository.find({ order: { createdAt: 'DESC' } });
|
||||
}
|
||||
|
||||
async updateVendor(id: string, data: DeepPartial<Vendor>): Promise<Vendor | null> {
|
||||
await this.vendorRepository.update(id, data as never);
|
||||
return this.vendorRepository.findOneBy({ id });
|
||||
}
|
||||
|
||||
async softDeleteVendor(id: string): Promise<void> {
|
||||
await this.vendorRepository.softDelete(id);
|
||||
}
|
||||
|
||||
// ---- Acquisitions ----
|
||||
async createAcquisition(data: DeepPartial<AssetAcquisition>): Promise<AssetAcquisition> {
|
||||
const acquisition = this.acquisitionRepository.create(data);
|
||||
return this.acquisitionRepository.save(acquisition);
|
||||
}
|
||||
|
||||
async findAcquisitions(vehicleId?: string): Promise<AssetAcquisition[]> {
|
||||
return this.acquisitionRepository.find({
|
||||
where: vehicleId ? { vehicleId } : {},
|
||||
relations: ['vehicle', 'vendor'],
|
||||
order: { acquisitionDate: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findAcquisitionById(id: string): Promise<AssetAcquisition | null> {
|
||||
return this.acquisitionRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['vehicle', 'vendor'],
|
||||
});
|
||||
}
|
||||
|
||||
async updateAcquisition(
|
||||
id: string,
|
||||
data: DeepPartial<AssetAcquisition>,
|
||||
): Promise<AssetAcquisition | null> {
|
||||
await this.acquisitionRepository.update(id, data as never);
|
||||
return this.findAcquisitionById(id);
|
||||
}
|
||||
|
||||
async softDeleteAcquisition(id: string): Promise<void> {
|
||||
await this.acquisitionRepository.softDelete(id);
|
||||
}
|
||||
|
||||
async findLatestAcquisitionByVehicle(vehicleId: string): Promise<AssetAcquisition | null> {
|
||||
return this.acquisitionRepository.findOne({
|
||||
where: { vehicleId },
|
||||
relations: ['vehicle', 'vendor'],
|
||||
order: { acquisitionDate: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Disposals ----
|
||||
async createDisposal(data: DeepPartial<AssetDisposal>): Promise<AssetDisposal> {
|
||||
const disposal = this.disposalRepository.create(data);
|
||||
return this.disposalRepository.save(disposal);
|
||||
}
|
||||
|
||||
async findDisposals(): Promise<AssetDisposal[]> {
|
||||
return this.disposalRepository.find({ order: { disposalDate: 'DESC' } });
|
||||
}
|
||||
|
||||
async softDeleteDisposal(id: string): Promise<void> {
|
||||
await this.disposalRepository.softDelete(id);
|
||||
}
|
||||
|
||||
async findLatestDisposalByVehicle(vehicleId: string): Promise<AssetDisposal | null> {
|
||||
return this.disposalRepository.findOne({
|
||||
where: { vehicleId },
|
||||
order: { disposalDate: 'DESC' },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ProcurementRepository } from './procurement.repository';
|
||||
import { Vendor } from './entities/vendor.entity';
|
||||
import { 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<Vendor> {
|
||||
return this.procurementRepository.createVendor(dto);
|
||||
}
|
||||
|
||||
async listVendors(): Promise<Vendor[]> {
|
||||
return this.procurementRepository.findVendors();
|
||||
}
|
||||
|
||||
async updateVendor(id: string, dto: UpdateVendorDto): Promise<Vendor | null> {
|
||||
return this.procurementRepository.updateVendor(id, dto);
|
||||
}
|
||||
|
||||
async deleteVendor(id: string): Promise<{ success: boolean }> {
|
||||
await this.procurementRepository.softDeleteVendor(id);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ---- Acquisitions ----
|
||||
async createAcquisition(dto: CreateAcquisitionDto): Promise<AssetAcquisition> {
|
||||
return this.procurementRepository.createAcquisition(dto);
|
||||
}
|
||||
|
||||
async listAcquisitions(vehicleId?: string): Promise<AssetAcquisition[]> {
|
||||
return this.procurementRepository.findAcquisitions(vehicleId);
|
||||
}
|
||||
|
||||
async getAcquisition(id: string): Promise<AssetAcquisition | null> {
|
||||
return this.procurementRepository.findAcquisitionById(id);
|
||||
}
|
||||
|
||||
async updateAcquisition(id: string, dto: UpdateAcquisitionDto): Promise<AssetAcquisition | null> {
|
||||
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<AssetDisposal> {
|
||||
return this.procurementRepository.createDisposal(dto);
|
||||
}
|
||||
|
||||
async listDisposals(): Promise<AssetDisposal[]> {
|
||||
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<LifecycleResult> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user