mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 19:30:57 +00:00
feat: implement shipping line bookings management
- Add ShippingLineBookingsPage for listing and managing shipping line bookings. - Create ShippingLineDocumentsModal for document uploads related to bookings. - Introduce ShippingLineInitiateModal for initiating new shipping line bookings. - Implement booking document state management with booking-doc-state utility. - Add shipping line bookings service for API interactions. - Update index to export new components and services. - Enhance types for freight to include shipping line credits.
This commit is contained in:
@@ -75,6 +75,14 @@ export class CreateRateDto {
|
||||
@IsUUID()
|
||||
destinationYardId?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'FK to shipping_line_companies.id — set to price this rate for one shipping line only. Omitted/null = the standard rate every customer pays. A line rate overrides the standard one for that line\'s bookings.',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
shippingLineCompanyId?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: CURRENCIES })
|
||||
@IsOptional()
|
||||
@IsIn([...CURRENCIES])
|
||||
|
||||
@@ -141,6 +141,22 @@ export class ListRatesQueryDto extends PaginationQueryDto {
|
||||
@IsString()
|
||||
@MaxLength(200)
|
||||
trigger?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Filter to one shipping line\'s rates.',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
shippingLineCompanyId?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'true = only shipping-line rates (any line), false = only standard customer rates. Omitted = both. Powers the Shipping line tab.',
|
||||
})
|
||||
@IsOptional()
|
||||
@Transform(toOptionalBoolean)
|
||||
@IsBoolean()
|
||||
isShippingLineRate?: boolean;
|
||||
}
|
||||
|
||||
export class ListWeightLimitRulesQueryDto extends PaginationQueryDto {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
|
||||
import { ShippingLineCompany } from '../../shipping-lines/entities/shipping-line-company.entity';
|
||||
import { CargoType } from './cargo-type.entity';
|
||||
import { ContainerType } from './container-type.entity';
|
||||
import { Yard } from './yard.entity';
|
||||
@@ -108,6 +109,7 @@ export type RateTrigger = typeof RATE_TRIGGERS[number];
|
||||
@Index(['trigger'])
|
||||
@Index(['originYardId'])
|
||||
@Index(['destinationYardId'])
|
||||
@Index(['shippingLineCompanyId'])
|
||||
export class Rate extends BaseEntity {
|
||||
@Column({ name: 'rate_type', type: 'varchar', length: 50 })
|
||||
rateType!: RateType;
|
||||
@@ -155,6 +157,23 @@ export class Rate extends BaseEntity {
|
||||
@JoinColumn({ name: 'destination_yard_id' })
|
||||
destinationYard?: Yard | null;
|
||||
|
||||
/**
|
||||
* The shipping line this rate belongs to, or NULL for the standard rate every
|
||||
* customer pays. A booking owned by a shipping line prices exclusively off
|
||||
* that line's rates — the standard rate is NOT a fallback, so a missing line
|
||||
* rate hard-blocks the booking rather than quietly billing the customer price.
|
||||
*
|
||||
* Points at `shipping_line_companies` (the portal account that books capacity),
|
||||
* not `shipping_lines` (carrier reference data behind the SHIPPING_LINE
|
||||
* trigger). The two are unrelated despite the similar names.
|
||||
*/
|
||||
@Column({ name: 'shipping_line_company_id', type: 'uuid', nullable: true })
|
||||
shippingLineCompanyId?: string | null;
|
||||
|
||||
@ManyToOne(() => ShippingLineCompany, { nullable: true, eager: false })
|
||||
@JoinColumn({ name: 'shipping_line_company_id' })
|
||||
shippingLineCompany?: ShippingLineCompany | null;
|
||||
|
||||
@Column({ name: 'currency', type: 'varchar', length: 5 })
|
||||
currency!: string;
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface IRatesRepository {
|
||||
rateType: string;
|
||||
/** Omitted for singly-resolved rates — see the repository implementation. */
|
||||
rateUnit?: string;
|
||||
/** Owning shipping line; null/omitted = the standard customer rate. */
|
||||
shippingLineCompanyId?: string | null;
|
||||
containerTypeId?: string | null;
|
||||
cargoTypeId?: string | null;
|
||||
tradeDirection?: string | null;
|
||||
|
||||
@@ -69,6 +69,7 @@ export class RatesRepository implements IRatesRepository {
|
||||
findByPattern(pattern: {
|
||||
rateType: string;
|
||||
rateUnit?: string;
|
||||
shippingLineCompanyId?: string | null;
|
||||
containerTypeId?: string | null;
|
||||
cargoTypeId?: string | null;
|
||||
tradeDirection?: string | null;
|
||||
@@ -85,6 +86,16 @@ export class RatesRepository implements IRatesRepository {
|
||||
qb.andWhere('rate.rate_unit = :rateUnit', { rateUnit: pattern.rateUnit });
|
||||
}
|
||||
|
||||
// The owner is part of the identity: a line's rate for a lane is a
|
||||
// different rate from the standard one, not a duplicate of it.
|
||||
if (pattern.shippingLineCompanyId) {
|
||||
qb.andWhere('rate.shipping_line_company_id = :shippingLineCompanyId', {
|
||||
shippingLineCompanyId: pattern.shippingLineCompanyId,
|
||||
});
|
||||
} else {
|
||||
qb.andWhere('rate.shipping_line_company_id IS NULL');
|
||||
}
|
||||
|
||||
if (pattern.containerTypeId) {
|
||||
qb.andWhere('rate.container_type_id = :containerTypeId', { containerTypeId: pattern.containerTypeId });
|
||||
} else {
|
||||
@@ -139,8 +150,22 @@ export class RatesRepository implements IRatesRepository {
|
||||
// yards joined the route columns have only ids to render.
|
||||
.leftJoinAndSelect('rate.originYard', 'originYard')
|
||||
.leftJoinAndSelect('rate.destinationYard', 'destinationYard')
|
||||
// The shipping-line tab renders the owning line's name, not its id.
|
||||
.leftJoinAndSelect('rate.shippingLineCompany', 'shippingLineCompany')
|
||||
.orderBy('rate.createdAt', query.sortOrder ?? 'DESC');
|
||||
|
||||
if (query.shippingLineCompanyId) {
|
||||
qb.andWhere('rate.shippingLineCompanyId = :shippingLineCompanyId', {
|
||||
shippingLineCompanyId: query.shippingLineCompanyId,
|
||||
});
|
||||
} else if (query.isShippingLineRate !== undefined) {
|
||||
// Tab filter: shipping-line rates (any line) vs standard customer rates.
|
||||
qb.andWhere(
|
||||
query.isShippingLineRate
|
||||
? 'rate.shippingLineCompanyId IS NOT NULL'
|
||||
: 'rate.shippingLineCompanyId IS NULL',
|
||||
);
|
||||
}
|
||||
if (query.status) {
|
||||
qb.andWhere('rate.status = :status', { status: query.status });
|
||||
}
|
||||
@@ -164,7 +189,7 @@ export class RatesRepository implements IRatesRepository {
|
||||
}
|
||||
if (query.search) {
|
||||
qb.andWhere(
|
||||
'(rate.rateType ILIKE :search OR rate.status ILIKE :search OR rate.rateUnit ILIKE :search OR rate.currency ILIKE :search)',
|
||||
'(rate.rateType ILIKE :search OR rate.status ILIKE :search OR rate.rateUnit ILIKE :search OR rate.currency ILIKE :search OR shippingLineCompany.name ILIKE :search)',
|
||||
{ search: `%${query.search}%` },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ import { YardFacilitiesService } from './services/yard-facilities.service';
|
||||
import { RuleEngineService } from './rule-engine.service';
|
||||
|
||||
import { NotificationInboxModule } from '../notification-inbox/notification-inbox.module';
|
||||
import { ShippingLineCompaniesModule } from '../shipping-lines/shipping-line-companies.module';
|
||||
import { WagonTypesModule } from '../wagon-types/wagon-types.module';
|
||||
|
||||
import { BookingCargoModifier } from '../bookings/entities/booking-cargo-modifier.entity';
|
||||
@@ -102,6 +103,9 @@ import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.
|
||||
// Rated wagon capacities — cargo types validate their per-wagon tonnage cap
|
||||
// against them (a cap above the rating is a typo, not a policy).
|
||||
WagonTypesModule,
|
||||
// Rates may be scoped to one shipping line; creating such a rate validates
|
||||
// the line exists and is active.
|
||||
ShippingLineCompaniesModule,
|
||||
],
|
||||
controllers: [
|
||||
CargoTypesController,
|
||||
|
||||
@@ -527,3 +527,145 @@ describe('RuleEngineService — fuel surcharge (per lane + cargo type)', () => {
|
||||
expect(fuelMods(result)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RuleEngineService — shipping-line rates override the standard ones', () => {
|
||||
const LINE = 'slc-msc';
|
||||
|
||||
/** Standard customer container-import rate on the lane. */
|
||||
const standardBase: Rate = {
|
||||
id: 'rate-standard-20',
|
||||
rateType: 'CONTAINER_IMPORT',
|
||||
trigger: 'ALWAYS',
|
||||
rateValue: 1000,
|
||||
rateUnit: 'PER_CONTAINER',
|
||||
currency: 'USD',
|
||||
status: 'LIVE',
|
||||
containerTypeId: 'ct-20',
|
||||
cargoTypeId: null,
|
||||
shippingLineCompanyId: null,
|
||||
originYardId: 'yard-dj',
|
||||
destinationYardId: 'yard-adama',
|
||||
} as Rate;
|
||||
|
||||
/** The same lane, priced for one shipping line. */
|
||||
const lineBase: Rate = {
|
||||
...standardBase,
|
||||
id: 'rate-line-20',
|
||||
rateValue: 1200,
|
||||
shippingLineCompanyId: LINE,
|
||||
} as Rate;
|
||||
|
||||
const standardHazard: Rate = {
|
||||
id: 'rate-hazard-standard',
|
||||
rateType: 'HAZARD_SURCHARGE',
|
||||
trigger: 'HAZARDOUS',
|
||||
rateValue: 50,
|
||||
rateUnit: 'PER_CONTAINER',
|
||||
currency: 'USD',
|
||||
status: 'LIVE',
|
||||
containerTypeId: null,
|
||||
cargoTypeId: null,
|
||||
shippingLineCompanyId: null,
|
||||
} as Rate;
|
||||
|
||||
const lineHazard: Rate = {
|
||||
...standardHazard,
|
||||
id: 'rate-hazard-line',
|
||||
rateValue: 80,
|
||||
shippingLineCompanyId: LINE,
|
||||
} as Rate;
|
||||
|
||||
const buildService = (rates: Rate[]) =>
|
||||
new RuleEngineService(
|
||||
{ findById: jest.fn().mockResolvedValue(null) } as never,
|
||||
{ findById: jest.fn().mockResolvedValue(null) } as never,
|
||||
{
|
||||
findActiveByContainerTypeId: jest
|
||||
.fn()
|
||||
.mockResolvedValue([{ id: 'wlr-20', maxVgmTons: 20, maxCapacityTons: null }]),
|
||||
} as never,
|
||||
{ findAllActive: jest.fn().mockResolvedValue([]) } as never,
|
||||
{ findLiveRates: jest.fn().mockResolvedValue(rates) } as never,
|
||||
{ findById: jest.fn().mockResolvedValue(null) } as never,
|
||||
{} as never,
|
||||
);
|
||||
|
||||
// One 20ft at 25 t against a 20 t limit → 5 t excess.
|
||||
const bookingInput = (
|
||||
overrides: Partial<BookingEvaluationInput> = {},
|
||||
): BookingEvaluationInput => ({
|
||||
serviceTypeId: 'svc-1',
|
||||
paymentCurrency: 'USD',
|
||||
tradeDirection: 'IMPORT',
|
||||
isHazardous: false,
|
||||
totalWagons: 1,
|
||||
originYardId: 'yard-dj',
|
||||
destinationYardId: 'yard-adama',
|
||||
containers: [
|
||||
{ containerTypeId: 'ct-20', quantity: 1, vgmPerUnitTons: 25, totalVgmTons: 25 },
|
||||
],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const overweightOf = (result: { appliedModifiers: Array<{ surchargeCode: string }> }) =>
|
||||
result.appliedModifiers.filter((m) => m.surchargeCode === 'OVERWEIGHT_PER_TON');
|
||||
|
||||
it('derives a line booking\'s overweight from the LINE\'s base rate, not the standard one', async () => {
|
||||
const result = await buildService([standardBase, lineBase]).evaluate(
|
||||
bookingInput({ shippingLineCompanyId: LINE }),
|
||||
);
|
||||
const ow = overweightOf(result);
|
||||
expect(ow).toHaveLength(1);
|
||||
// The line's 1200 / (2 × 20) = 30 USD/t, not the standard 1000 → 25 USD/t.
|
||||
expect(ow[0]).toMatchObject({
|
||||
rateId: lineBase.id,
|
||||
unitPriceUsd: 30,
|
||||
calculatedAmount: 150,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps a customer booking on the standard rate even when a line rate exists', async () => {
|
||||
const result = await buildService([standardBase, lineBase]).evaluate(bookingInput());
|
||||
const ow = overweightOf(result);
|
||||
expect(ow).toHaveLength(1);
|
||||
expect(ow[0]).toMatchObject({
|
||||
rateId: standardBase.id,
|
||||
unitPriceUsd: 25,
|
||||
calculatedAmount: 125,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not fall back to the standard rate when the line has none for the lane', async () => {
|
||||
const result = await buildService([standardBase]).evaluate(
|
||||
bookingInput({ shippingLineCompanyId: LINE }),
|
||||
);
|
||||
// No line rate on the lane → nothing to derive from. Base freight is what
|
||||
// hard-blocks the booking; the standard 1000 must never be borrowed here.
|
||||
expect(overweightOf(result)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('bills the line\'s own surcharge and never the standard one alongside it', async () => {
|
||||
const result = await buildService([
|
||||
standardBase,
|
||||
lineBase,
|
||||
standardHazard,
|
||||
lineHazard,
|
||||
]).evaluate(bookingInput({ shippingLineCompanyId: LINE, isHazardous: true }));
|
||||
|
||||
const hazard = result.appliedModifiers.filter(
|
||||
(m) => m.surchargeCode === 'HAZARD_SURCHARGE',
|
||||
);
|
||||
expect(hazard).toHaveLength(1);
|
||||
expect(hazard[0]).toMatchObject({ rateId: lineHazard.id, calculatedAmount: 80 });
|
||||
});
|
||||
|
||||
it('hard-blocks a requested service the line has no surcharge rate for', async () => {
|
||||
const result = await buildService([standardBase, lineBase, standardHazard]).evaluate(
|
||||
bookingInput({ shippingLineCompanyId: LINE, isHazardous: true }),
|
||||
);
|
||||
// The standard hazard rate exists but belongs to customers, so the line's
|
||||
// hazardous booking must block rather than borrow it.
|
||||
expect(result.hardBlocked).toHaveLength(1);
|
||||
expect(result.hardBlocked[0]).toContain('hazardous');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,6 +74,16 @@ export interface BookingEvaluationInput {
|
||||
isGovernment?: boolean;
|
||||
allowConsolidation?: boolean;
|
||||
shippingLineId?: string | null;
|
||||
/**
|
||||
* The shipping line that OWNS this booking (`bookings.shipping_line_company_id`),
|
||||
* when it is a shipping-line booking rather than a customer one. Such a booking
|
||||
* prices exclusively off that line's own rates — see {@link ratesForOwner}.
|
||||
*
|
||||
* Not to be confused with `shippingLineId` above, which is cargo metadata
|
||||
* naming the carrier that physically moves the goods and only feeds the
|
||||
* SHIPPING_LINE double-handling trigger.
|
||||
*/
|
||||
shippingLineCompanyId?: string | null;
|
||||
/**
|
||||
* The booking's rail leg. Import overweight derives its per-ton price from
|
||||
* this route's own container freight rate, so the engine needs the yards.
|
||||
@@ -282,7 +292,10 @@ export class RuleEngineService {
|
||||
// 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 liveRates = this.ratesForOwner(
|
||||
await this.ratesRepo.findLiveRates(),
|
||||
input.shippingLineCompanyId,
|
||||
);
|
||||
const surchargeRates = this.dedupeRatesBySignature(
|
||||
liveRates.filter((r) => r.trigger && r.trigger !== 'ALWAYS'),
|
||||
);
|
||||
@@ -812,6 +825,28 @@ export class RuleEngineService {
|
||||
return rate.rateType ?? rate.trigger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrow the LIVE rate pool to the ones this booking's owner may price off.
|
||||
*
|
||||
* A customer booking sees only standard rates (no owner) — a shipping line's
|
||||
* negotiated price must never leak into a customer quote. A shipping-line
|
||||
* booking sees only that line's own rates: line rates OVERRIDE the standard
|
||||
* ones rather than stacking on them, and the standard rate is deliberately
|
||||
* NOT a fallback, so a lane the line has no rate for hard-blocks downstream
|
||||
* (base freight already blocks on "no rate for this route") instead of
|
||||
* quietly billing the line at the customer price.
|
||||
*
|
||||
* Filtering once, here, is what makes the override apply uniformly: every
|
||||
* downstream lookup (base freight, derived overweight, empty return, lashing,
|
||||
* fuel, and the additive surcharges) reads from this same pool, so none of
|
||||
* them needs its own owner check.
|
||||
*/
|
||||
private ratesForOwner(rates: Rate[], shippingLineCompanyId?: string | null): Rate[] {
|
||||
return shippingLineCompanyId
|
||||
? rates.filter((r) => r.shippingLineCompanyId === shippingLineCompanyId)
|
||||
: rates.filter((r) => !r.shippingLineCompanyId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse rates that describe the same charge to a single representative.
|
||||
*
|
||||
|
||||
@@ -61,6 +61,8 @@ describe('RatesService — one rate per pattern', () => {
|
||||
})),
|
||||
} as never,
|
||||
{ findById: jest.fn().mockResolvedValue(null) } as never,
|
||||
// Shipping line companies — these rates carry no owner, so it is never hit.
|
||||
{ findById: jest.fn() } as never,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { PaginatedResponse, YardCountry } from '@edr/types';
|
||||
import { IsNull, Not } from 'typeorm';
|
||||
import { ShippingLineStatus } from '../../shipping-lines/entities/shipping-line-company.entity';
|
||||
import { ShippingLineCompaniesService } from '../../shipping-lines/shipping-line-companies.service';
|
||||
import { CreateRateDto } from '../dto/create-rate.dto';
|
||||
import { ListRatesQueryDto } from '../dto/list-rule-engine-query.dto';
|
||||
import { UpdateRateDto } from '../dto/update-rate.dto';
|
||||
@@ -39,6 +41,7 @@ export class RatesService {
|
||||
private readonly yardsRepository: IYardsRepository,
|
||||
@Inject(CARGO_TYPES_REPOSITORY)
|
||||
private readonly cargoTypesRepository: ICargoTypesRepository,
|
||||
private readonly shippingLineCompaniesService: ShippingLineCompaniesService,
|
||||
) {}
|
||||
|
||||
/** List rates — standard paginated envelope with server-side search. */
|
||||
@@ -491,6 +494,7 @@ export class RatesService {
|
||||
rateType: string;
|
||||
/** Passed only for additive surcharges — see {@link resolvesSingleRate}. */
|
||||
rateUnit?: string;
|
||||
shippingLineCompanyId: string | null;
|
||||
containerTypeId: string | null;
|
||||
cargoTypeId: string | null;
|
||||
tradeDirection: string | null;
|
||||
@@ -508,6 +512,35 @@ export class RatesService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the shipping line a rate is scoped to, when any.
|
||||
*
|
||||
* A shipping line only ever ships import — the export leg is sold through the
|
||||
* customer's contract — so a line rate carrying an EXPORT direction is
|
||||
* rejected here as well as by `CK_rates_shipping_line_import_only`.
|
||||
* Returns the owner id to store (null = the standard customer rate).
|
||||
*/
|
||||
private async resolveShippingLineScope(
|
||||
shippingLineCompanyId: string | null | undefined,
|
||||
tradeDirection: string | null,
|
||||
): Promise<string | null> {
|
||||
if (!shippingLineCompanyId) return null;
|
||||
|
||||
// Throws NotFoundException when the line does not exist.
|
||||
const line = await this.shippingLineCompaniesService.findById(shippingLineCompanyId);
|
||||
if (line.status !== ShippingLineStatus.Active) {
|
||||
throw new BadRequestException(
|
||||
`${line.name} is ${line.status} — rates can only be configured for an active shipping line.`,
|
||||
);
|
||||
}
|
||||
if (tradeDirection && tradeDirection !== 'IMPORT') {
|
||||
throw new BadRequestException(
|
||||
'Shipping line rates are import-only — the export leg is priced through the customer contract.',
|
||||
);
|
||||
}
|
||||
return shippingLineCompanyId;
|
||||
}
|
||||
|
||||
/** Create a rate in DRAFT status. */
|
||||
async create(dto: CreateRateDto, proposedByStaffId: string): Promise<Rate> {
|
||||
const appliesTo = dto.appliesTo as Rate['appliesTo'];
|
||||
@@ -568,6 +601,11 @@ export class RatesService {
|
||||
destinationYardId: dto.destinationYardId,
|
||||
});
|
||||
|
||||
const shippingLineCompanyId = await this.resolveShippingLineScope(
|
||||
dto.shippingLineCompanyId,
|
||||
tradeDirection,
|
||||
);
|
||||
|
||||
const rateType = deriveRateType({
|
||||
appliesTo,
|
||||
trigger,
|
||||
@@ -602,6 +640,7 @@ export class RatesService {
|
||||
await this.assertNoDuplicatePattern({
|
||||
rateType,
|
||||
...(this.resolvesSingleRate(appliesTo, trigger) ? {} : { rateUnit }),
|
||||
shippingLineCompanyId,
|
||||
containerTypeId,
|
||||
cargoTypeId,
|
||||
tradeDirection,
|
||||
@@ -614,6 +653,7 @@ export class RatesService {
|
||||
appliesTo,
|
||||
trigger,
|
||||
rateType,
|
||||
shippingLineCompanyId,
|
||||
containerTypeId,
|
||||
cargoTypeId,
|
||||
tradeDirection,
|
||||
@@ -791,6 +831,17 @@ export class RatesService {
|
||||
updates.originYardId = yardScope.originYardId;
|
||||
updates.destinationYardId = yardScope.destinationYardId;
|
||||
|
||||
// The owning line is re-validated on every edit: a patch that flips the
|
||||
// direction to EXPORT has to be refused for a line rate, and a patch that
|
||||
// moves the rate to a suspended line too.
|
||||
const shippingLineCompanyId = await this.resolveShippingLineScope(
|
||||
dto.shippingLineCompanyId !== undefined
|
||||
? dto.shippingLineCompanyId
|
||||
: existing.shippingLineCompanyId,
|
||||
updates.tradeDirection,
|
||||
);
|
||||
updates.shippingLineCompanyId = shippingLineCompanyId;
|
||||
|
||||
// Keep the derived rateType in sync with whatever changed.
|
||||
const rateType = deriveRateType({
|
||||
appliesTo,
|
||||
@@ -839,6 +890,7 @@ export class RatesService {
|
||||
await this.assertNoDuplicatePattern({
|
||||
rateType,
|
||||
...(this.resolvesSingleRate(appliesTo, trigger) ? {} : { rateUnit }),
|
||||
shippingLineCompanyId,
|
||||
containerTypeId: updates.containerTypeId,
|
||||
cargoTypeId: updates.cargoTypeId,
|
||||
tradeDirection: updates.tradeDirection,
|
||||
|
||||
Reference in New Issue
Block a user