145 lines
4.7 KiB
TypeScript
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,
|
|
};
|
|
}
|