mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 07:51:02 +00:00
Merge branch 'freight_feature/profile' of github.com:Tria-plc/edr-platform into freight_feature/profile
This commit is contained in:
@@ -242,12 +242,23 @@ export class BookingPricingService {
|
||||
)
|
||||
: 0;
|
||||
|
||||
// Consolidation is system-managed: the CONSOLIDATION_ENABLED surcharge fires
|
||||
// whenever any container line leaves a wagon partially filled. Derived from
|
||||
// the container quantities — there is no persisted opt-in flag.
|
||||
// Consolidation is system-managed: the CONSOLIDATION surcharge fires whenever
|
||||
// a container type leaves a wagon partially filled. Aggregate by type first —
|
||||
// two lines of the same type share wagons, so 2× 20FT (= one full wagon) must
|
||||
// NOT count as a partial wagon. Mirrors ConsolidationService.slotsFromContainerLines.
|
||||
const remainderByType = new Map<string, { quantity: number; perWagon: number }>();
|
||||
for (const l of lines) {
|
||||
const prev = remainderByType.get(l.container.containerTypeId);
|
||||
remainderByType.set(l.container.containerTypeId, {
|
||||
quantity: (prev?.quantity ?? 0) + Number(l.quantity || 0),
|
||||
perWagon: l.perWagon,
|
||||
});
|
||||
}
|
||||
const allowConsolidation =
|
||||
booking.freightType === 'CONTAINER' &&
|
||||
lines.some((l) => wagonRemainder(l.quantity, l.perWagon) > 0);
|
||||
[...remainderByType.values()].some(
|
||||
(t) => wagonRemainder(t.quantity, t.perWagon) > 0,
|
||||
);
|
||||
|
||||
return {
|
||||
freightType: booking.freightType as 'CONTAINER' | 'BULK',
|
||||
|
||||
@@ -57,16 +57,29 @@ export class ConsolidationService {
|
||||
async slotsFromContainerLines(
|
||||
lines: Array<{ containerTypeId: string; quantity: number }>,
|
||||
): Promise<ConsolidationSlot[]> {
|
||||
const slots: ConsolidationSlot[] = [];
|
||||
// Aggregate by container type first: two lines of the same type on one
|
||||
// booking share the same wagons. Counting them separately would flag a
|
||||
// self-complete booking (e.g. 2× 20FT = exactly one wagon) as a partial
|
||||
// wagon and wrongly park it in PENDING_CONSOLIDATION.
|
||||
const quantityByType = new Map<string, number>();
|
||||
for (const line of lines) {
|
||||
const ct = await this.containerTypesService.findById(line.containerTypeId);
|
||||
if (!line.containerTypeId) continue;
|
||||
quantityByType.set(
|
||||
line.containerTypeId,
|
||||
(quantityByType.get(line.containerTypeId) ?? 0) + Number(line.quantity || 0),
|
||||
);
|
||||
}
|
||||
|
||||
const slots: ConsolidationSlot[] = [];
|
||||
for (const [containerTypeId, quantity] of quantityByType) {
|
||||
const ct = await this.containerTypesService.findById(containerTypeId);
|
||||
const perWagon = containersPerWagon(Number(ct.wagonsPerUnit));
|
||||
const remainder = wagonRemainder(line.quantity, perWagon);
|
||||
const remainder = wagonRemainder(quantity, perWagon);
|
||||
if (remainder === 0) continue;
|
||||
slots.push({
|
||||
containerTypeId: line.containerTypeId,
|
||||
containerTypeId,
|
||||
containerTypeCode: ct.code,
|
||||
quantity: line.quantity,
|
||||
quantity,
|
||||
containersPerWagon: perWagon,
|
||||
remainder,
|
||||
slotsNeeded: perWagon - remainder,
|
||||
|
||||
@@ -201,8 +201,15 @@ export class RuleEngineService {
|
||||
// Surcharges are now self-describing rates: any LIVE rate whose `trigger`
|
||||
// is not ALWAYS. Each fires independently and stacks on top of base freight
|
||||
// — hazard + reefer + overweight all add together, each with its own unit.
|
||||
//
|
||||
// A given surcharge identity (same trigger + rateType + unit + value +
|
||||
// scope) must contribute exactly ONE line. Duplicate LIVE rate rows — e.g.
|
||||
// from a non-idempotent seeder — would otherwise repeat the same surcharge
|
||||
// many times and inflate the total, so we collapse them to one row each.
|
||||
const liveRates = await this.ratesRepo.findLiveRates();
|
||||
const surchargeRates = liveRates.filter((r) => r.trigger && r.trigger !== 'ALWAYS');
|
||||
const surchargeRates = this.dedupeRatesBySignature(
|
||||
liveRates.filter((r) => r.trigger && r.trigger !== 'ALWAYS'),
|
||||
);
|
||||
|
||||
for (const rate of surchargeRates) {
|
||||
const triggered = this.matchesTrigger(rate.trigger, {
|
||||
@@ -404,4 +411,34 @@ export class RuleEngineService {
|
||||
private surchargeCode(rate: Rate): string {
|
||||
return rate.rateType ?? rate.trigger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse rates that describe the same charge to a single representative.
|
||||
*
|
||||
* Two rates are "the same" when they would produce an identical price line:
|
||||
* same trigger, rateType, unit, value, currency, and scoping (container /
|
||||
* cargo type). Duplicate rows (e.g. a seeder run more than once) therefore
|
||||
* stack into one line instead of repeating — keeping the breakdown clean and
|
||||
* the total correct. The first row of each signature is kept so an existing
|
||||
* rateId is preserved for snapshotting.
|
||||
*/
|
||||
private dedupeRatesBySignature(rates: Rate[]): Rate[] {
|
||||
const seen = new Set<string>();
|
||||
const result: Rate[] = [];
|
||||
for (const rate of rates) {
|
||||
const signature = [
|
||||
rate.trigger,
|
||||
rate.rateType,
|
||||
rate.rateUnit,
|
||||
Number(rate.rateValue),
|
||||
rate.currency,
|
||||
rate.containerTypeId ?? '',
|
||||
rate.cargoTypeId ?? '',
|
||||
].join('|');
|
||||
if (seen.has(signature)) continue;
|
||||
seen.add(signature);
|
||||
result.push(rate);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user