sinka/api/src/freight/freight.service.ts
2026-09-15 21:10:42 -03:00

145 lines
4.7 KiB
TypeScript

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,
};
}