import { BadRequestException, Injectable } from '@nestjs/common'; import type { Prisma } from '../generated/tenant'; import type { AuthUser } from '../auth/auth.types'; import { GeoService } from '../geo/geo.service'; import { tenantClient } from '../tenancy/actor'; import { TenantConnectionService } from '../tenancy/tenant-connection.service'; import type { SimulateFreightDto } from './dto/simulate-freight.dto'; import { quoteCarrier, rankQuotes, type CarrierQuote } from './freight-calculator'; @Injectable() export class FreightService { constructor( private readonly tenants: TenantConnectionService, private readonly geo: GeoService, ) {} async history(actor: AuthUser) { const db = await tenantClient(this.tenants, actor); const rows = await db.freightSimulation.findMany({ orderBy: { createdAt: 'desc' }, take: 50, include: { customer: { select: { id: true, name: true } } }, }); return rows.map(serializeSimulation); } async simulate(actor: AuthUser, dto: SimulateFreightDto, ip?: string) { const db = await tenantClient(this.tenants, actor); const carriers = await db.carrier.findMany({ where: { active: true } }); if (!carriers.length) { throw new BadRequestException('Cadastre ao menos uma transportadora ativa.'); } if (dto.customerId) { const customer = await db.customer.findUnique({ where: { id: dto.customerId } }); if (!customer) { throw new BadRequestException('Cliente não encontrado.'); } } const route = await this.geo.distanceKm(dto.originZip, dto.destinationZip); const cargo = { weightKg: dto.weightKg, widthCm: dto.widthCm, heightCm: dto.heightCm, lengthCm: dto.lengthCm, cargoValue: dto.cargoValue, distanceKm: route.km, }; const quotes = rankQuotes( carriers.map((carrier) => quoteCarrier( { id: carrier.id, name: carrier.name, baseFee: Number(carrier.baseFee), pricePerKg: Number(carrier.pricePerKg), pricePerKm: Number(carrier.pricePerKm), adValoremRate: Number(carrier.adValoremRate), }, cargo, ), ), ); const best = quotes[0]; const saved = await db.freightSimulation.create({ data: { originZip: route.origin.zip, destinationZip: route.destination.zip, originLabel: route.origin.label, destinationLabel: route.destination.label, weightKg: dto.weightKg, widthCm: dto.widthCm, heightCm: dto.heightCm, lengthCm: dto.lengthCm, cargoValue: dto.cargoValue, distanceKm: route.km, chargeableKg: best.chargeableKg, quotedPrice: best.price, bestCarrierName: best.carrierName, quotes: quotes as unknown as Prisma.InputJsonValue, status: 'CALCULATED', customerId: dto.customerId, }, include: { customer: { select: { id: true, name: true } } }, }); await db.auditLog.create({ data: { userId: actor.sub, action: 'freight.simulate', entity: 'FreightSimulation', entityId: saved.id, ip, metadata: { best: best.carrierName, price: best.price, km: route.km }, }, }); return { ...serializeSimulation(saved), distanceSource: route.source }; } } function serializeSimulation(row: { id: string; originZip: string; destinationZip: string; originLabel: string | null; destinationLabel: string | null; weightKg: { toString(): string }; widthCm: { toString(): string }; heightCm: { toString(): string }; lengthCm: { toString(): string }; cargoValue: { toString(): string }; quotedPrice: { toString(): string } | null; distanceKm: { toString(): string } | null; chargeableKg: { toString(): string } | null; bestCarrierName: string | null; quotes: unknown; status: string; createdAt: Date; customer: { id: string; name: string } | null; }) { return { id: row.id, originZip: row.originZip, destinationZip: row.destinationZip, originLabel: row.originLabel, destinationLabel: row.destinationLabel, weightKg: Number(row.weightKg), widthCm: Number(row.widthCm), heightCm: Number(row.heightCm), lengthCm: Number(row.lengthCm), cargoValue: Number(row.cargoValue), quotedPrice: row.quotedPrice === null ? null : Number(row.quotedPrice), distanceKm: row.distanceKm === null ? null : Number(row.distanceKm), chargeableKg: row.chargeableKg === null ? null : Number(row.chargeableKg), bestCarrierName: row.bestCarrierName, quotes: (row.quotes as CarrierQuote[] | null) ?? [], status: row.status, createdAt: row.createdAt, customer: row.customer, }; }