Files
edr-platform/apps/edr-freight-api/src/modules/rule-engine/services/rates.service.ts

114 lines
4.2 KiB
TypeScript

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 { 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<string, unknown> = {};
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<Rate[]> {
return this.repository.findLiveRates();
}
/** Get a rate by ID. */
async findById(id: string): Promise<Rate> {
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<Rate> {
return this.repository.create({
rateType: dto.rateType as Rate['rateType'],
containerTypeId: dto.containerTypeId,
tradeDirection: dto.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<Rate> {
const existing = await this.findById(id);
if (existing.status !== 'DRAFT') {
throw new BadRequestException('Only DRAFT rates can be updated');
}
const updates: Partial<Rate> = {};
if (dto.rateType) updates.rateType = dto.rateType as Rate['rateType'];
if (dto.containerTypeId !== undefined) updates.containerTypeId = dto.containerTypeId;
if (dto.tradeDirection !== undefined) updates.tradeDirection = dto.tradeDirection;
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<Rate> {
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<Rate> {
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<void> {
await this.findById(id);
await this.repository.softDelete(id);
}
}