mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 05:18:11 +00:00
change price logic on the ,rule engine ui, auto generate the contrat
This commit is contained in:
@@ -14,6 +14,11 @@ export function computeNextStep(
|
||||
const { status } = booking;
|
||||
|
||||
switch (status) {
|
||||
case 'PRICE_CHANGED_PENDING_CONFIRM':
|
||||
return {
|
||||
action: 'CONFIRM_SUBMIT',
|
||||
description: 'Price has changed since preview; confirm to submit booking',
|
||||
};
|
||||
case 'SUBMITTED':
|
||||
return {
|
||||
action: 'ACCEPT_INTAKE',
|
||||
|
||||
@@ -14,6 +14,24 @@ import { GeneratePriceResponseDto, PriceLineItemDto } from './dto/generate-price
|
||||
import { Booking } from './entities/booking.entity';
|
||||
import { assertBookingStatus } from './booking-status.util';
|
||||
|
||||
export interface ComputedPriceResult {
|
||||
lineItems: PriceLineItemDto[];
|
||||
totalAmount: number;
|
||||
currency: string;
|
||||
usedRates: Rate[];
|
||||
appliedModifiers: AppliedCargoModifier[];
|
||||
priorityScore: number;
|
||||
warnings: string[];
|
||||
hardBlocked: string[];
|
||||
}
|
||||
|
||||
type StoredPricingBreakdown = {
|
||||
lineItems?: PriceLineItemDto[];
|
||||
totalAmount?: number;
|
||||
currency?: string;
|
||||
generatedAt?: string;
|
||||
} | null;
|
||||
|
||||
@Injectable()
|
||||
export class BookingPricingService {
|
||||
constructor(
|
||||
@@ -26,22 +44,56 @@ export class BookingPricingService {
|
||||
|
||||
async generatePrice(bookingId: string): Promise<GeneratePriceResponseDto> {
|
||||
const booking = await this.requireBooking(bookingId);
|
||||
assertBookingStatus(booking, ['DRAFT']);
|
||||
assertBookingStatus(booking, ['DRAFT', 'CHANGES_REQUESTED']);
|
||||
|
||||
const computed = await this.computePriceForBooking(booking);
|
||||
this.ruleEngineService.assertNoHardBlocks({
|
||||
priorityScore: computed.priorityScore,
|
||||
appliedModifiers: computed.appliedModifiers,
|
||||
containerWeightResults: [],
|
||||
warnings: computed.warnings,
|
||||
hardBlocked: computed.hardBlocked,
|
||||
requiresDirectorApproval: false,
|
||||
});
|
||||
|
||||
await this.bookingsRepository.update(bookingId, {
|
||||
totalAmount: computed.totalAmount,
|
||||
priorityScore: computed.priorityScore,
|
||||
pricingBreakdown: {
|
||||
lineItems: computed.lineItems,
|
||||
totalAmount: computed.totalAmount,
|
||||
currency: computed.currency,
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
} as never);
|
||||
|
||||
return {
|
||||
bookingId,
|
||||
totalAmount: computed.totalAmount,
|
||||
currency: computed.currency,
|
||||
lineItems: computed.lineItems,
|
||||
warnings: computed.warnings,
|
||||
};
|
||||
}
|
||||
|
||||
async computePriceForBooking(booking: Booking): Promise<ComputedPriceResult> {
|
||||
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);
|
||||
const { lineItems: baseLines, usedRates: baseRates } =
|
||||
await this.computeBaseRailLinesWithRates(booking, evalInput);
|
||||
for (const line of baseLines) {
|
||||
lineItems.push(line);
|
||||
total += line.amount;
|
||||
}
|
||||
|
||||
const liveRates = await this.ratesService.findLiveRates();
|
||||
const rateById = new Map(liveRates.map((r) => [r.id, r]));
|
||||
const usedRatesMap = new Map(baseRates.map((r) => [r.id, r]));
|
||||
|
||||
for (const mod of ruleResult.appliedModifiers) {
|
||||
const item: PriceLineItemDto = {
|
||||
code: mod.surchargeTypeCode,
|
||||
@@ -51,30 +103,60 @@ export class BookingPricingService {
|
||||
};
|
||||
lineItems.push(item);
|
||||
total += mod.calculatedAmount;
|
||||
|
||||
const rate = rateById.get(mod.rateId);
|
||||
if (rate) usedRatesMap.set(rate.id, rate);
|
||||
}
|
||||
|
||||
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,
|
||||
lineItems,
|
||||
totalAmount: total,
|
||||
currency: booking.paymentCurrency,
|
||||
lineItems,
|
||||
usedRates: [...usedRatesMap.values()],
|
||||
appliedModifiers: ruleResult.appliedModifiers,
|
||||
priorityScore: ruleResult.priorityScore,
|
||||
warnings: ruleResult.warnings,
|
||||
hardBlocked: ruleResult.hardBlocked,
|
||||
};
|
||||
}
|
||||
|
||||
pricesMatch(stored: StoredPricingBreakdown, computed: ComputedPriceResult): boolean {
|
||||
if (!stored?.lineItems?.length) return false;
|
||||
if (Number(stored.totalAmount) !== computed.totalAmount) return false;
|
||||
return (
|
||||
this.lineItemsSignature(stored.lineItems) ===
|
||||
this.lineItemsSignature(computed.lineItems)
|
||||
);
|
||||
}
|
||||
|
||||
async createPricingSnapshots(
|
||||
bookingId: string,
|
||||
usedRates: Rate[],
|
||||
appliedModifiers: AppliedCargoModifier[],
|
||||
): Promise<void> {
|
||||
await this.bookingsRepository.clearPricingArtifacts(bookingId);
|
||||
const snapshots = await this.ruleEngineService.snapshotRates(bookingId, usedRates);
|
||||
|
||||
const snapshotByRateId = new Map(snapshots.map((s) => [s.rateId, s.id]));
|
||||
const rows = appliedModifiers
|
||||
.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<typeof r> => r !== null);
|
||||
|
||||
if (rows.length > 0) {
|
||||
await this.bookingsRepository.createCargoModifiers(rows);
|
||||
}
|
||||
}
|
||||
|
||||
async buildEvalInputForBooking(booking: Booking): Promise<BookingEvaluationInput> {
|
||||
const containers = await Promise.all(
|
||||
(booking.bookingContainers ?? []).map(async (bc) => {
|
||||
@@ -115,11 +197,7 @@ export class BookingPricingService {
|
||||
totalAmount: number;
|
||||
currency: string;
|
||||
}> {
|
||||
const stored = booking.pricingBreakdown as {
|
||||
lineItems?: PriceLineItemDto[];
|
||||
totalAmount?: number;
|
||||
currency?: string;
|
||||
} | null;
|
||||
const stored = booking.pricingBreakdown as StoredPricingBreakdown;
|
||||
|
||||
if (stored?.lineItems?.length) {
|
||||
return {
|
||||
@@ -129,41 +207,28 @@ export class BookingPricingService {
|
||||
};
|
||||
}
|
||||
|
||||
const evalInput = await this.buildEvalInputForBooking(booking);
|
||||
const ruleResult = await this.ruleEngineService.evaluate(evalInput);
|
||||
const lineItems: PriceLineItemDto[] = [];
|
||||
let total = 0;
|
||||
const computed = await this.computePriceForBooking(booking);
|
||||
|
||||
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,
|
||||
if (computed.lineItems.length === 0) {
|
||||
const total = Number(booking.totalAmount);
|
||||
return {
|
||||
lineItems: [
|
||||
{
|
||||
code: 'TOTAL',
|
||||
description: 'Contract total',
|
||||
amount: total,
|
||||
currency: booking.paymentCurrency,
|
||||
},
|
||||
],
|
||||
totalAmount: total,
|
||||
currency: booking.paymentCurrency,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
lineItems,
|
||||
totalAmount: total || Number(booking.totalAmount),
|
||||
currency: booking.paymentCurrency,
|
||||
lineItems: computed.lineItems,
|
||||
totalAmount: computed.totalAmount || Number(booking.totalAmount),
|
||||
currency: computed.currency,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -190,14 +255,14 @@ export class BookingPricingService {
|
||||
return score;
|
||||
}
|
||||
|
||||
private async computeBaseRailLines(
|
||||
private async computeBaseRailLinesWithRates(
|
||||
booking: Booking,
|
||||
evalInput: BookingEvaluationInput,
|
||||
): Promise<PriceLineItemDto[]> {
|
||||
): Promise<{ lineItems: PriceLineItemDto[]; usedRates: Rate[] }> {
|
||||
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
|
||||
@@ -209,18 +274,15 @@ console.log('liveRates----', liveRates);
|
||||
: 'CONTAINER_EXPORT'
|
||||
: 'INTERCITY_CONTAINER';
|
||||
|
||||
|
||||
console.log('rateType----', rateType);
|
||||
|
||||
const lines: PriceLineItemDto[] = [];
|
||||
const usedRatesMap = new Map<string, Rate>();
|
||||
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;
|
||||
|
||||
usedRatesMap.set(rate.id, rate);
|
||||
const amount = this.amountForRate(rate, container.quantity, wagonCount);
|
||||
lines.push({
|
||||
code: rateType,
|
||||
@@ -235,6 +297,7 @@ console.log('liveRates----', liveRates);
|
||||
(r) => r.rateType === rateType && r.currency === currency && r.status === 'LIVE',
|
||||
);
|
||||
if (fallback) {
|
||||
usedRatesMap.set(fallback.id, fallback);
|
||||
const amount = this.amountForRate(fallback, 1, wagonCount);
|
||||
lines.push({
|
||||
code: rateType,
|
||||
@@ -245,7 +308,7 @@ console.log('liveRates----', liveRates);
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
return { lineItems: lines, usedRates: [...usedRatesMap.values()] };
|
||||
}
|
||||
|
||||
private pickRate(
|
||||
@@ -281,31 +344,15 @@ console.log('liveRates----', liveRates);
|
||||
}
|
||||
}
|
||||
|
||||
private async persistPriceRun(
|
||||
bookingId: string,
|
||||
modifiers: AppliedCargoModifier[],
|
||||
_total: number,
|
||||
): Promise<void> {
|
||||
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<typeof r> => r !== null);
|
||||
|
||||
if (rows.length > 0) {
|
||||
await this.bookingsRepository.createCargoModifiers(rows);
|
||||
}
|
||||
private lineItemsSignature(items: PriceLineItemDto[]): string {
|
||||
return JSON.stringify(
|
||||
[...items]
|
||||
.map((item) => ({
|
||||
code: item.code,
|
||||
amount: item.amount,
|
||||
currency: item.currency,
|
||||
}))
|
||||
.sort((a, b) => a.code.localeCompare(b.code)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { BookingPricingService } from './booking-pricing.service';
|
||||
import { BookingsRepository } from './bookings.repository';
|
||||
import { assertBookingStatus } from './booking-status.util';
|
||||
import { computeNextStep, type BookingNextStep } from './booking-next-step.util';
|
||||
import { SubmitBookingResponseDto } from './dto/submit-booking-response.dto';
|
||||
import { PriceLineItemDto } from './dto/generate-price-response.dto';
|
||||
import { Booking } from './entities/booking.entity';
|
||||
import { BookingsService } from './bookings.service';
|
||||
|
||||
@@ -22,7 +24,7 @@ export class BookingTransitionService {
|
||||
private readonly bookingsService: BookingsService,
|
||||
) {}
|
||||
|
||||
async submit(bookingId: string): Promise<Booking> {
|
||||
async submit(bookingId: string): Promise<SubmitBookingResponseDto> {
|
||||
const booking = await this.bookingsService.findById(bookingId);
|
||||
assertBookingStatus(booking, ['DRAFT', 'CHANGES_REQUESTED']);
|
||||
|
||||
@@ -32,14 +34,119 @@ export class BookingTransitionService {
|
||||
);
|
||||
}
|
||||
|
||||
const priorityScore = await this.pricingService.computeSubmitPriorityScore(booking);
|
||||
await this.ruleEngineService.snapshotLiveRates(bookingId);
|
||||
const computed = await this.pricingService.computePriceForBooking(booking);
|
||||
this.ruleEngineService.assertNoHardBlocks({
|
||||
priorityScore: computed.priorityScore,
|
||||
appliedModifiers: computed.appliedModifiers,
|
||||
containerWeightResults: [],
|
||||
warnings: computed.warnings,
|
||||
hardBlocked: computed.hardBlocked,
|
||||
requiresDirectorApproval: false,
|
||||
});
|
||||
|
||||
const stored = booking.pricingBreakdown as {
|
||||
lineItems?: PriceLineItemDto[];
|
||||
totalAmount?: number;
|
||||
} | null;
|
||||
const unchanged = this.pricingService.pricesMatch(stored, computed);
|
||||
const priorityScore = await this.pricingService.computeSubmitPriorityScore(booking);
|
||||
|
||||
if (unchanged) {
|
||||
await this.pricingService.createPricingSnapshots(
|
||||
bookingId,
|
||||
computed.usedRates,
|
||||
computed.appliedModifiers,
|
||||
);
|
||||
|
||||
const updated = await this.bookingsRepository.update(bookingId, {
|
||||
status: 'SUBMITTED',
|
||||
priorityScore,
|
||||
} as never);
|
||||
|
||||
const finalBooking = await this.bookingsService.findById(updated!.id);
|
||||
return {
|
||||
bookingId: finalBooking.id,
|
||||
status: finalBooking.status,
|
||||
priceChanged: false,
|
||||
totalAmount: Number(finalBooking.totalAmount),
|
||||
currency: finalBooking.paymentCurrency,
|
||||
lineItems: computed.lineItems,
|
||||
};
|
||||
}
|
||||
|
||||
const previousTotalAmount = Number(booking.totalAmount);
|
||||
await this.bookingsRepository.update(bookingId, {
|
||||
totalAmount: computed.totalAmount,
|
||||
priorityScore: computed.priorityScore,
|
||||
pricingBreakdown: {
|
||||
lineItems: computed.lineItems,
|
||||
totalAmount: computed.totalAmount,
|
||||
currency: computed.currency,
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
status: 'PRICE_CHANGED_PENDING_CONFIRM',
|
||||
} as never);
|
||||
|
||||
const updatedBooking = await this.bookingsService.findById(bookingId);
|
||||
return {
|
||||
bookingId: updatedBooking.id,
|
||||
status: updatedBooking.status,
|
||||
priceChanged: true,
|
||||
previousTotalAmount,
|
||||
totalAmount: computed.totalAmount,
|
||||
currency: computed.currency,
|
||||
lineItems: computed.lineItems,
|
||||
message: 'Price has changed since preview. Confirm to submit with the updated price.',
|
||||
};
|
||||
}
|
||||
|
||||
async confirmSubmit(bookingId: string): Promise<SubmitBookingResponseDto> {
|
||||
const booking = await this.bookingsService.findById(bookingId);
|
||||
assertBookingStatus(booking, ['PRICE_CHANGED_PENDING_CONFIRM']);
|
||||
|
||||
if (Number(booking.totalAmount) <= 0) {
|
||||
throw new BadRequestException('No price to confirm');
|
||||
}
|
||||
|
||||
const computed = await this.pricingService.computePriceForBooking(booking);
|
||||
this.ruleEngineService.assertNoHardBlocks({
|
||||
priorityScore: computed.priorityScore,
|
||||
appliedModifiers: computed.appliedModifiers,
|
||||
containerWeightResults: [],
|
||||
warnings: computed.warnings,
|
||||
hardBlocked: computed.hardBlocked,
|
||||
requiresDirectorApproval: false,
|
||||
});
|
||||
|
||||
await this.pricingService.createPricingSnapshots(
|
||||
bookingId,
|
||||
computed.usedRates,
|
||||
computed.appliedModifiers,
|
||||
);
|
||||
|
||||
const priorityScore = await this.pricingService.computeSubmitPriorityScore(booking);
|
||||
const updated = await this.bookingsRepository.update(bookingId, {
|
||||
status: 'SUBMITTED',
|
||||
priorityScore,
|
||||
totalAmount: computed.totalAmount,
|
||||
pricingBreakdown: {
|
||||
lineItems: computed.lineItems,
|
||||
totalAmount: computed.totalAmount,
|
||||
currency: computed.currency,
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
} as never);
|
||||
return this.bookingsService.findById(updated!.id);
|
||||
|
||||
const finalBooking = await this.bookingsService.findById(updated!.id);
|
||||
return {
|
||||
bookingId: finalBooking.id,
|
||||
status: finalBooking.status,
|
||||
priceChanged: false,
|
||||
totalAmount: Number(finalBooking.totalAmount),
|
||||
currency: finalBooking.paymentCurrency,
|
||||
lineItems: computed.lineItems,
|
||||
message: 'Booking submitted with confirmed price.',
|
||||
};
|
||||
}
|
||||
|
||||
async requestChanges(
|
||||
@@ -279,6 +386,7 @@ export class BookingTransitionService {
|
||||
assertBookingStatus(booking, [
|
||||
'DRAFT',
|
||||
'SUBMITTED',
|
||||
'PRICE_CHANGED_PENDING_CONFIRM',
|
||||
'CHANGES_REQUESTED',
|
||||
'PENDING_APPROVAL',
|
||||
'CONTRACT_READY',
|
||||
@@ -321,4 +429,4 @@ export class BookingTransitionService {
|
||||
nextStep,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import { CreateBookingDto } from './dto/create-booking.dto';
|
||||
import { BookingListSummaryDto } from './dto/booking-list-summary.dto';
|
||||
import { FilterBookingDto } from './dto/filter-booking.dto';
|
||||
import { GeneratePriceResponseDto } from './dto/generate-price-response.dto';
|
||||
import { SubmitBookingResponseDto } from './dto/submit-booking-response.dto';
|
||||
import {
|
||||
ApproveStepDto,
|
||||
CancelBookingDto,
|
||||
@@ -165,17 +166,36 @@ export class BookingsController {
|
||||
}
|
||||
|
||||
@Post(':id/generate-price')
|
||||
@ApiOperation({ summary: 'Generate price preview (DRAFT only)' })
|
||||
@ApiOperation({
|
||||
summary: 'Generate price preview (DRAFT or CHANGES_REQUESTED)',
|
||||
description:
|
||||
'Computes and stores a price preview on the booking. Does not create rate snapshots.',
|
||||
})
|
||||
@ApiOkResponse({ type: GeneratePriceResponseDto })
|
||||
generatePrice(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.pricingService.generatePrice(id);
|
||||
}
|
||||
|
||||
@Post(':id/submit')
|
||||
@ApiOperation({ summary: 'Customer submit booking' })
|
||||
async submit(@Param('id', ParseUUIDPipe) id: string) {
|
||||
const booking = await this.transitionService.submit(id);
|
||||
return this.transitionService.enrichBookingResponse(booking);
|
||||
@ApiOperation({
|
||||
summary: 'Customer submit booking',
|
||||
description:
|
||||
'Recomputes price against live rates. If unchanged, creates rate snapshots and submits. If changed, updates the booking price and returns priceChanged=true for confirmation.',
|
||||
})
|
||||
@ApiOkResponse({ type: SubmitBookingResponseDto })
|
||||
submit(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.transitionService.submit(id);
|
||||
}
|
||||
|
||||
@Post(':id/confirm-submit')
|
||||
@ApiOperation({
|
||||
summary: 'Confirm submit after price change',
|
||||
description:
|
||||
'Creates rate snapshots for the updated booking price and moves the booking to SUBMITTED.',
|
||||
})
|
||||
@ApiOkResponse({ type: SubmitBookingResponseDto })
|
||||
confirmSubmit(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.transitionService.confirmSubmit(id);
|
||||
}
|
||||
|
||||
@Post(':id/staff/request-changes')
|
||||
|
||||
@@ -345,6 +345,26 @@ export class BookingsRepository extends BaseRepository<Booking> {
|
||||
await this.dataSource.getRepository(BookingRateSnapshot).delete({ bookingId });
|
||||
}
|
||||
|
||||
async hasPricingArtifacts(bookingId: string): Promise<boolean> {
|
||||
const snapshotCount = await this.dataSource
|
||||
.getRepository(BookingRateSnapshot)
|
||||
.count({ where: { bookingId } });
|
||||
const modifierCount = await this.dataSource
|
||||
.getRepository(BookingCargoModifier)
|
||||
.count({ where: { bookingId } });
|
||||
return snapshotCount > 0 || modifierCount > 0;
|
||||
}
|
||||
|
||||
async invalidatePricingPreview(bookingId: string): Promise<void> {
|
||||
if (await this.hasPricingArtifacts(bookingId)) {
|
||||
await this.clearPricingArtifacts(bookingId);
|
||||
}
|
||||
await this.update(bookingId, {
|
||||
totalAmount: 0,
|
||||
pricingBreakdown: null,
|
||||
} as never);
|
||||
}
|
||||
|
||||
/** Queue listing with optional bulk exclusion for LINE_STAFF. */
|
||||
async findQueue(options: {
|
||||
status: string | string[];
|
||||
|
||||
@@ -348,6 +348,15 @@ export class BookingsService {
|
||||
this.ruleEngineService.assertNoHardBlocks(ruleResult);
|
||||
warnings.push(...ruleResult.warnings);
|
||||
|
||||
const pricingFieldsChanged = this.pricingRelevantFieldsChanged(
|
||||
existing,
|
||||
dto,
|
||||
freightType,
|
||||
cargoTypeId,
|
||||
allowConsolidation,
|
||||
containers,
|
||||
);
|
||||
|
||||
const updates: Record<string, unknown> = {
|
||||
...dto,
|
||||
freightType,
|
||||
@@ -375,6 +384,10 @@ export class BookingsService {
|
||||
);
|
||||
}
|
||||
|
||||
if (pricingFieldsChanged) {
|
||||
await this.bookingsRepository.invalidatePricingPreview(id);
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
await this.filesService.uploadMany(id, 'bookings', files);
|
||||
}
|
||||
@@ -648,4 +661,57 @@ export class BookingsService {
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private pricingRelevantFieldsChanged(
|
||||
existing: Booking,
|
||||
dto: UpdateBookingDto,
|
||||
freightType: FreightType,
|
||||
cargoTypeId: string | null | undefined,
|
||||
allowConsolidation: boolean,
|
||||
containers: CreateBookingContainerDto[],
|
||||
): boolean {
|
||||
if (dto.freightType !== undefined && dto.freightType !== existing.freightType) {
|
||||
return true;
|
||||
}
|
||||
if (dto.tradeDirection !== undefined && dto.tradeDirection !== existing.tradeDirection) {
|
||||
return true;
|
||||
}
|
||||
if (dto.paymentCurrency !== undefined && dto.paymentCurrency !== existing.paymentCurrency) {
|
||||
return true;
|
||||
}
|
||||
if (dto.isHazardous !== undefined && dto.isHazardous !== existing.isHazardous) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
dto.allowConsolidation !== undefined &&
|
||||
dto.allowConsolidation !== existing.allowConsolidation
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (dto.shippingLineId !== undefined && dto.shippingLineId !== existing.shippingLineId) {
|
||||
return true;
|
||||
}
|
||||
if (dto.cargoTypeId !== undefined && dto.cargoTypeId !== existing.cargoTypeId) {
|
||||
return true;
|
||||
}
|
||||
if (dto.containers !== undefined) {
|
||||
const existingContainers =
|
||||
existing.bookingContainers?.map((bc) => ({
|
||||
containerTypeId: bc.containerTypeId,
|
||||
quantity: bc.quantity,
|
||||
vgmPerUnitTons: Number(bc.vgmPerUnitTons),
|
||||
})) ?? [];
|
||||
if (JSON.stringify(existingContainers) !== JSON.stringify(containers)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (
|
||||
freightType !== existing.freightType ||
|
||||
(cargoTypeId ?? null) !== (existing.cargoTypeId ?? null) ||
|
||||
allowConsolidation !== existing.allowConsolidation
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
import { PriceLineItemDto } from './generate-price-response.dto';
|
||||
|
||||
export class SubmitBookingResponseDto {
|
||||
@ApiProperty()
|
||||
bookingId!: string;
|
||||
|
||||
@ApiProperty()
|
||||
status!: string;
|
||||
|
||||
@ApiProperty()
|
||||
priceChanged!: boolean;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
previousTotalAmount?: number;
|
||||
|
||||
@ApiProperty()
|
||||
totalAmount!: number;
|
||||
|
||||
@ApiProperty()
|
||||
currency!: string;
|
||||
|
||||
@ApiPropertyOptional({ type: [PriceLineItemDto] })
|
||||
lineItems?: PriceLineItemDto[];
|
||||
|
||||
@ApiPropertyOptional()
|
||||
message?: string;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { BookingReviewNote } from './booking-review-note.entity';
|
||||
export const BOOKING_STATUSES = [
|
||||
'DRAFT',
|
||||
'SUBMITTED',
|
||||
'PRICE_CHANGED_PENDING_CONFIRM',
|
||||
'CHANGES_REQUESTED',
|
||||
'PENDING_APPROVAL',
|
||||
'APPROVED_PENDING_SIGNATURE',
|
||||
|
||||
Reference in New Issue
Block a user