import { Injectable, NotFoundException } from '@nestjs/common'; import { ContainerTypesService } from '../rule-engine/services/container-types.service'; import { RatesService } from '../rule-engine/services/rates.service'; import { ServiceTypesService } from '../rule-engine/services/service-types.service'; import { Rate } from '../rule-engine/entities/rate.entity'; import { AppliedCargoModifier, BookingEvaluationInput, RuleEngineService, } from '../rule-engine/rule-engine.service'; import { BookingsRepository } from './bookings.repository'; import { GeneratePriceResponseDto, PriceLineItemDto } from './dto/generate-price-response.dto'; import { Booking } from './entities/booking.entity'; import { assertBookingStatus } from './booking-status.util'; @Injectable() export class BookingPricingService { constructor( private readonly bookingsRepository: BookingsRepository, private readonly ruleEngineService: RuleEngineService, private readonly containerTypesService: ContainerTypesService, private readonly ratesService: RatesService, private readonly serviceTypesService: ServiceTypesService, ) {} async generatePrice(bookingId: string): Promise { const booking = await this.requireBooking(bookingId); assertBookingStatus(booking, ['DRAFT']); const evalInput = await this.buildEvalInputForBooking(booking); console.log('evalInput----', evalInput); const ruleResult = await this.ruleEngineService.evaluate(evalInput); this.ruleEngineService.assertNoHardBlocks(ruleResult); const lineItems: PriceLineItemDto[] = []; let total = 0; const baseLines = await this.computeBaseRailLines(booking, evalInput); for (const line of baseLines) { lineItems.push(line); total += line.amount; } for (const mod of ruleResult.appliedModifiers) { const item: PriceLineItemDto = { code: mod.surchargeTypeCode, description: `Surcharge: ${mod.surchargeTypeCode}`, amount: mod.calculatedAmount, currency: mod.currency, }; lineItems.push(item); total += mod.calculatedAmount; } await this.persistPriceRun(bookingId, ruleResult.appliedModifiers, total); await this.bookingsRepository.update(bookingId, { totalAmount: total, priorityScore: ruleResult.priorityScore, pricingBreakdown: { lineItems, totalAmount: total, currency: booking.paymentCurrency, generatedAt: new Date().toISOString(), }, } as never); return { bookingId, totalAmount: total, currency: booking.paymentCurrency, lineItems, warnings: ruleResult.warnings, }; } async buildEvalInputForBooking(booking: Booking): Promise { const containers = await Promise.all( (booking.bookingContainers ?? []).map(async (bc) => { const ct = await this.containerTypesService.findById(bc.containerTypeId); const vgm = Number(bc.vgmPerUnitTons); const qty = bc.quantity; return { containerTypeId: bc.containerTypeId, quantity: qty, vgmPerUnitTons: vgm, totalVgmTons: qty * vgm, isReefer: ct.isReefer, }; }), ); return { freightType: booking.freightType as 'CONTAINER' | 'BULK', cargoTypeId: booking.cargoTypeId ?? null, serviceTypeId: booking.serviceTypeId, paymentCurrency: booking.paymentCurrency, tradeDirection: booking.tradeDirection, isHazardous: booking.isHazardous, allowConsolidation: booking.allowConsolidation, shippingLineId: booking.shippingLineId, containers, }; } private async requireBooking(id: string): Promise { const booking = await this.bookingsRepository.findByIdWithFiles(id); if (!booking) throw new NotFoundException(`Booking ${id} not found`); return booking; } /** Line items for contract schedule (uses stored breakdown or recomputes). */ async computeContractLineItems(booking: Booking): Promise<{ lineItems: PriceLineItemDto[]; totalAmount: number; currency: string; }> { const stored = booking.pricingBreakdown as { lineItems?: PriceLineItemDto[]; totalAmount?: number; currency?: string; } | null; if (stored?.lineItems?.length) { return { lineItems: stored.lineItems, totalAmount: Number(stored.totalAmount ?? booking.totalAmount), currency: stored.currency ?? booking.paymentCurrency, }; } const evalInput = await this.buildEvalInputForBooking(booking); const ruleResult = await this.ruleEngineService.evaluate(evalInput); const lineItems: PriceLineItemDto[] = []; let total = 0; const baseLines = await this.computeBaseRailLines(booking, evalInput); for (const line of baseLines) { lineItems.push(line); total += line.amount; } for (const mod of ruleResult.appliedModifiers) { lineItems.push({ code: mod.surchargeTypeCode, description: `Surcharge: ${mod.surchargeTypeCode}`, amount: mod.calculatedAmount, currency: mod.currency, }); total += mod.calculatedAmount; } if (lineItems.length === 0) { total = Number(booking.totalAmount); lineItems.push({ code: 'TOTAL', description: 'Contract total', amount: total, currency: booking.paymentCurrency, }); } return { lineItems, totalAmount: total || Number(booking.totalAmount), currency: booking.paymentCurrency, }; } /** Recompute priority on submit (USD + service tier). */ async computeSubmitPriorityScore(booking: Booking): Promise { const evalInput = await this.buildEvalInputForBooking(booking); const ruleResult = await this.ruleEngineService.evaluate(evalInput); let score = ruleResult.priorityScore; const serviceType = await this.serviceTypesService.findById(booking.serviceTypeId); if (booking.paymentCurrency === 'USD' && serviceType) { const code = (serviceType.code ?? '').toUpperCase(); const hasForwarding = serviceType.includesFirstMile || serviceType.includesLastMile || code.includes('FORWARD') || code.includes('Y'); const railOnly = code.includes('RAIL') && !hasForwarding; if (hasForwarding) score += 1000; else if (railOnly || code.includes('X')) score += 500; } return score; } private async computeBaseRailLines( booking: Booking, evalInput: BookingEvaluationInput, ): Promise { const liveRates = await this.ratesService.findLiveRates(); const currency = booking.paymentCurrency; const isBulk = booking.freightType === 'BULK'; console.log('liveRates----', liveRates); const rateType = booking.tradeDirection === 'IMPORT' ? isBulk ? 'BULK_IMPORT' : 'CONTAINER_IMPORT' : booking.tradeDirection === 'EXPORT' ? isBulk ? 'BULK_EXPORT' : 'CONTAINER_EXPORT' : 'INTERCITY_CONTAINER'; console.log('rateType----', rateType); const lines: PriceLineItemDto[] = []; const wagonCount = await this.bookingsRepository.calculateWagonCount(booking.id); for (const container of evalInput.containers) { console.log('container----', container); const rate = this.pickRate(liveRates, rateType, container.containerTypeId, currency); console.log('rate----', rate); if (!rate) continue; const amount = this.amountForRate(rate, container.quantity, wagonCount); lines.push({ code: rateType, description: `Base rail (${rateType})`, amount, currency: rate.currency, }); } if (lines.length === 0) { const fallback = liveRates.find( (r) => r.rateType === rateType && r.currency === currency && r.status === 'LIVE', ); if (fallback) { const amount = this.amountForRate(fallback, 1, wagonCount); lines.push({ code: rateType, description: `Base rail (${rateType})`, amount, currency: fallback.currency, }); } } return lines; } private pickRate( rates: Rate[], rateType: string, containerTypeId: string, currency: string, ): Rate | undefined { return ( rates.find( (r) => r.rateType === rateType && r.currency === currency && r.containerTypeId === containerTypeId, ) ?? rates.find((r) => r.rateType === rateType && r.currency === currency && !r.containerTypeId) ); } private amountForRate(rate: Rate, quantity: number, wagonCount: number): number { const value = Number(rate.rateValue); switch (rate.rateUnit) { case 'PER_CONTAINER': return value * quantity; case 'PER_WAGON': return value * wagonCount; case 'PER_TON': return value * quantity; case 'FLAT': return value; default: return value * quantity; } } private async persistPriceRun( bookingId: string, modifiers: AppliedCargoModifier[], _total: number, ): Promise { await this.bookingsRepository.clearPricingArtifacts(bookingId); const snapshots = await this.ruleEngineService.snapshotLiveRates(bookingId); const snapshotByRateId = new Map(snapshots.map((s) => [s.rateId, s.id])); const rows = modifiers .map((m) => { const snapshotId = snapshotByRateId.get(m.rateId); if (!snapshotId) return null; return { bookingId, surchargeTypeId: m.surchargeTypeId, triggerValue: m.triggerValue, calculatedAmount: m.calculatedAmount, rateSnapshotId: snapshotId, }; }) .filter((r): r is NonNullable => r !== null); if (rows.length > 0) { await this.bookingsRepository.createCargoModifiers(rows); } } }