import { randomUUID } from 'crypto'; import type { Pool } from 'pg'; import type { CreateOwnershipCostRequest, UpdateOwnershipCostRequest, OwnershipCost, OwnershipCostResponse, OwnershipCostType, OwnershipCostStats } from './ownership-costs.types'; import { OwnershipCostsRepository } from '../data/ownership-costs.repository'; import pool from '../../../core/config/database'; export class OwnershipCostsService { private readonly repo: OwnershipCostsRepository; private readonly db: Pool; constructor(dbPool?: Pool) { this.db = dbPool || pool; this.repo = new OwnershipCostsRepository(this.db); } async createCost(userId: string, body: CreateOwnershipCostRequest): Promise { await this.assertVehicleOwnership(userId, body.vehicleId); const id = randomUUID(); const cost = await this.repo.insert({ id, userId, vehicleId: body.vehicleId, documentId: body.documentId, costType: body.costType, amount: body.amount, description: body.description, periodStart: body.periodStart, periodEnd: body.periodEnd, notes: body.notes, }); return cost; } async getCost(userId: string, id: string): Promise { const cost = await this.repo.findById(id, userId); if (!cost) return null; return this.toResponse(cost); } async getCosts(userId: string, filters?: { vehicleId?: string; costType?: OwnershipCostType; documentId?: string }): Promise { const costs = await this.repo.findByUserId(userId, filters); return costs.map(c => this.toResponse(c)); } async getCostsByVehicle(userId: string, vehicleId: string): Promise { const costs = await this.repo.findByVehicleId(vehicleId, userId); return costs.map(c => this.toResponse(c)); } async getVehicleOwnershipCosts(vehicleId: string, userId: string): Promise { const costs = await this.repo.findByVehicleId(vehicleId, userId); let totalCost = 0; let insuranceCosts = 0; let registrationCosts = 0; let taxCosts = 0; let otherCosts = 0; for (const c of costs) { if (c.amount === null || c.amount === undefined) continue; const amount = Number(c.amount); if (isNaN(amount)) { throw new Error(`Invalid amount value for ownership cost ${c.id}`); } totalCost += amount; // Breakdown by cost type for backward compatibility switch (c.costType) { case 'insurance': insuranceCosts += amount; break; case 'registration': registrationCosts += amount; break; case 'tax': taxCosts += amount; break; case 'inspection': case 'parking': case 'other': otherCosts += amount; break; } } return { totalCost, recordCount: costs.length, insuranceCosts, registrationCosts, taxCosts, otherCosts }; } // Alias for backward compatibility with vehicles service async getVehicleCostStats(vehicleId: string, userId: string): Promise { return this.getVehicleOwnershipCosts(vehicleId, userId); } async updateCost(userId: string, id: string, patch: UpdateOwnershipCostRequest): Promise { const existing = await this.repo.findById(id, userId); if (!existing) return null; // Convert nulls to undefined for repository compatibility const cleanPatch = Object.fromEntries( Object.entries(patch).map(([k, v]) => [k, v === null ? undefined : v]) ) as Partial>; const updated = await this.repo.update(id, userId, cleanPatch); if (!updated) return null; return this.toResponse(updated); } async deleteCost(userId: string, id: string): Promise { await this.repo.delete(id, userId); } private async assertVehicleOwnership(userId: string, vehicleId: string) { const res = await this.db.query('SELECT id FROM vehicles WHERE id = $1 AND user_id = $2', [vehicleId, userId]); if (!res.rows[0]) { const err: any = new Error('Vehicle not found or not owned by user'); err.statusCode = 403; throw err; } } private toResponse(cost: OwnershipCost): OwnershipCostResponse { return cost; } }