import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { CreateRateDto } from '../dto/create-rate.dto'; import { UpdateRateDto } from '../dto/update-rate.dto'; import { Rate } from '../entities/rate.entity'; import { deriveRateType } from '../entities/rate-type.util'; import { IRatesRepository, RATES_REPOSITORY } from '../interfaces/rates.repository.interface'; @Injectable() export class RatesService { constructor( @Inject(RATES_REPOSITORY) private readonly repository: IRatesRepository, ) {} /** List rates with pagination. */ async findAll(filter: { status?: string; rateType?: string; page?: number; pageSize?: number; }): Promise<{ data: Rate[]; meta: { total: number; page: number; pageSize: number; totalPages: number } }> { const page = filter.page ?? 1; const pageSize = filter.pageSize ?? 20; const where: Record = {}; if (filter.status) where.status = filter.status; if (filter.rateType) where.rateType = filter.rateType; const [data, total] = await this.repository.findAndCount({ where, order: { effectiveFrom: 'DESC' }, skip: (page - 1) * pageSize, take: pageSize, }); return { data, meta: { total, page, pageSize, totalPages: Math.max(1, Math.ceil(total / pageSize)) } }; } /** Return all currently LIVE rates. */ async findLiveRates(): Promise { return this.repository.findLiveRates(); } /** Get a rate by ID. */ async findById(id: string): Promise { const entity = await this.repository.findById(id); if (!entity) throw new NotFoundException(`Rate ${id} not found`); return entity; } /** Create a rate in DRAFT status. */ async create(dto: CreateRateDto, proposedByStaffId: string): Promise { const appliesTo = dto.appliesTo as Rate['appliesTo']; const trigger = dto.trigger as Rate['trigger']; // Surcharges (trigger ≠ ALWAYS) carry no direction/scope — clear them so // the engine never accidentally narrows a surcharge by container/direction. const isSurcharge = trigger !== 'ALWAYS'; const containerTypeId = isSurcharge ? null : (dto.containerTypeId ?? null); const cargoTypeId = isSurcharge ? null : (dto.cargoTypeId ?? null); const tradeDirection = isSurcharge ? null : (dto.tradeDirection ?? null); return this.repository.create({ appliesTo, trigger, rateType: deriveRateType({ appliesTo, trigger, tradeDirection, isBulk: Boolean(cargoTypeId), }), containerTypeId, cargoTypeId, tradeDirection, currency: dto.currency ?? 'USD', rateValue: dto.rateValue, rateUnit: dto.rateUnit as Rate['rateUnit'], status: 'DRAFT', proposedByStaffId, effectiveFrom: new Date(dto.effectiveFrom), effectiveTo: dto.effectiveTo ? new Date(dto.effectiveTo) : undefined, }); } /** Update a DRAFT rate. */ async update(id: string, dto: UpdateRateDto): Promise { const existing = await this.findById(id); if (existing.status !== 'DRAFT') { throw new BadRequestException('Only DRAFT rates can be updated'); } const updates: Partial = {}; const appliesTo = (dto.appliesTo as Rate['appliesTo']) ?? existing.appliesTo; const trigger = (dto.trigger as Rate['trigger']) ?? existing.trigger; const isSurcharge = trigger !== 'ALWAYS'; if (dto.appliesTo) updates.appliesTo = appliesTo; if (dto.trigger) updates.trigger = trigger; const containerTypeId = isSurcharge ? null : dto.containerTypeId !== undefined ? dto.containerTypeId : existing.containerTypeId; const cargoTypeId = isSurcharge ? null : dto.cargoTypeId !== undefined ? dto.cargoTypeId : existing.cargoTypeId; const tradeDirection = isSurcharge ? null : dto.tradeDirection !== undefined ? dto.tradeDirection : existing.tradeDirection; updates.containerTypeId = containerTypeId; updates.cargoTypeId = cargoTypeId; updates.tradeDirection = tradeDirection; // Keep the derived rateType in sync with whatever changed. updates.rateType = deriveRateType({ appliesTo, trigger, tradeDirection, isBulk: Boolean(cargoTypeId), }); updates.currency = dto.currency ?? existing.currency ?? 'USD'; if (dto.rateValue !== undefined) updates.rateValue = dto.rateValue; if (dto.rateUnit) updates.rateUnit = dto.rateUnit as Rate['rateUnit']; if (dto.effectiveFrom) updates.effectiveFrom = new Date(dto.effectiveFrom); if (dto.effectiveTo) updates.effectiveTo = new Date(dto.effectiveTo); const updated = await this.repository.update(id, updates); if (!updated) throw new NotFoundException(`Rate ${id} not found`); return updated; } /** Submit a DRAFT rate for CEO approval. */ async submitForApproval(id: string): Promise { const rate = await this.findById(id); if (rate.status !== 'DRAFT') { throw new BadRequestException('Only DRAFT rates can be submitted for approval'); } const updated = await this.repository.update(id, { status: 'PENDING_APPROVAL' }); return updated!; } /** CEO approves a rate — moves to LIVE. */ async approve(id: string, approverUserId: string): Promise { const rate = await this.findById(id); if (rate.status !== 'PENDING_APPROVAL') { throw new BadRequestException('Only PENDING_APPROVAL rates can be approved'); } const updated = await this.repository.update(id, { status: 'LIVE', approvedByCeoId: approverUserId, approvedAt: new Date(), }); return updated!; } /** Soft-delete a rate. */ async remove(id: string): Promise { await this.findById(id); await this.repository.softDelete(id); } }