mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 07:51:02 +00:00
Merge pull request #771 from Tria-plc/freight_feature/usermanagement
fix
This commit is contained in:
@@ -475,7 +475,14 @@ export class BookingPricingService {
|
||||
const wagonCount = await this.resolveWagonCount(booking);
|
||||
|
||||
for (const container of evalInput.containers) {
|
||||
const rate = this.pickRate(liveRates, rateType, container.containerTypeId, 'USD');
|
||||
const rate = this.pickRate(
|
||||
liveRates,
|
||||
rateType,
|
||||
container.containerTypeId,
|
||||
'USD',
|
||||
booking.originYardId,
|
||||
booking.destinationYardId,
|
||||
);
|
||||
if (!rate) continue;
|
||||
|
||||
usedRatesMap.set(rate.id, rate);
|
||||
@@ -515,8 +522,15 @@ export class BookingPricingService {
|
||||
}
|
||||
|
||||
if (lines.length === 0) {
|
||||
// Bulk (and any booking with no container lines) still has to price off a
|
||||
// rate configured for this leg — never one belonging to another route.
|
||||
const fallback = liveRates.find(
|
||||
(r) => r.rateType === rateType && r.currency === 'USD' && r.status === 'LIVE',
|
||||
(r) =>
|
||||
r.rateType === rateType &&
|
||||
r.currency === 'USD' &&
|
||||
r.status === 'LIVE' &&
|
||||
r.originYardId === booking.originYardId &&
|
||||
r.destinationYardId === booking.destinationYardId,
|
||||
);
|
||||
if (fallback) {
|
||||
usedRatesMap.set(fallback.id, fallback);
|
||||
@@ -711,20 +725,32 @@ export class BookingPricingService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base freight is quoted per leg, so a rate only applies to a booking running
|
||||
* the exact origin → destination it was configured for. There is deliberately
|
||||
* no route-agnostic fallback: charging a Dire Dawa price for a Mojo shipment
|
||||
* because nobody configured Mojo yet is worse than surfacing no line at all.
|
||||
* Within the leg, a rate scoped to the container type wins over one that
|
||||
* covers every type.
|
||||
*/
|
||||
private pickRate(
|
||||
rates: Rate[],
|
||||
rateType: string,
|
||||
containerTypeId: string,
|
||||
currency: string,
|
||||
originYardId: string,
|
||||
destinationYardId: string,
|
||||
): Rate | undefined {
|
||||
const onLeg = rates.filter(
|
||||
(r) =>
|
||||
r.rateType === rateType &&
|
||||
r.currency === currency &&
|
||||
r.originYardId === originYardId &&
|
||||
r.destinationYardId === destinationYardId,
|
||||
);
|
||||
return (
|
||||
rates.find(
|
||||
(r) =>
|
||||
r.rateType === rateType &&
|
||||
r.currency === currency &&
|
||||
r.containerTypeId === containerTypeId,
|
||||
) ??
|
||||
rates.find((r) => r.rateType === rateType && r.currency === currency && !r.containerTypeId)
|
||||
onLeg.find((r) => r.containerTypeId === containerTypeId) ??
|
||||
onLeg.find((r) => !r.containerTypeId)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
|
||||
const TRADE_DIRECTIONS = ['IMPORT', 'EXPORT', 'BOTH'] as const;
|
||||
const CURRENCIES = ['USD'] as const;
|
||||
export const INTERCITY_KINDS = ['CONTAINER', 'BULK'] as const;
|
||||
|
||||
export class CreateRateDto {
|
||||
@ApiProperty({ enum: RATE_APPLIES_TO, description: 'Friendly category the rate applies to' })
|
||||
@@ -37,6 +38,31 @@ export class CreateRateDto {
|
||||
@IsIn([...TRADE_DIRECTIONS])
|
||||
tradeDirection?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
enum: INTERCITY_KINDS,
|
||||
description:
|
||||
'Whether an intercity rate covers containers or bulk. Required when appliesTo = INTERCITY; ignored otherwise. Not stored — it selects the INTERCITY_CONTAINER / INTERCITY_BULK rate type.',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsIn([...INTERCITY_KINDS])
|
||||
intercityKind?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'FK to yards.id — origin of the leg this rate prices. Required for base freight (bulk/container/intercity), rejected for surcharges and first/last mile.',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
originYardId?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'FK to yards.id — destination of the leg this rate prices. Required for base freight (bulk/container/intercity), rejected for surcharges and first/last mile.',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
destinationYardId?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: CURRENCIES })
|
||||
@IsOptional()
|
||||
@IsIn([...CURRENCIES])
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
|
||||
import { CargoType } from './cargo-type.entity';
|
||||
import { ContainerType } from './container-type.entity';
|
||||
import { Yard } from './yard.entity';
|
||||
|
||||
export const RATE_TYPES = [
|
||||
'CONTAINER_IMPORT',
|
||||
@@ -91,6 +92,8 @@ export type RateTrigger = typeof RATE_TRIGGERS[number];
|
||||
@Index(['status'])
|
||||
@Index(['containerTypeId'])
|
||||
@Index(['trigger'])
|
||||
@Index(['originYardId'])
|
||||
@Index(['destinationYardId'])
|
||||
export class Rate extends BaseEntity {
|
||||
@Column({ name: 'rate_type', type: 'varchar', length: 50 })
|
||||
rateType!: RateType;
|
||||
@@ -118,6 +121,26 @@ export class Rate extends BaseEntity {
|
||||
@Column({ name: 'trade_direction', type: 'varchar', length: 10, nullable: true })
|
||||
tradeDirection?: string | null;
|
||||
|
||||
/**
|
||||
* The leg this rate prices. Base freight (trigger = ALWAYS) is quoted per
|
||||
* route — "container import, Djibouti → Dire Dawa" — so both yards are
|
||||
* required for BULK/CONTAINER/INTERCITY and NULL for everything else. The
|
||||
* `CK_rates_yard_scope` DB constraint enforces both halves of that.
|
||||
*/
|
||||
@Column({ name: 'origin_yard_id', type: 'uuid', nullable: true })
|
||||
originYardId?: string | null;
|
||||
|
||||
@ManyToOne(() => Yard, { nullable: true, eager: false })
|
||||
@JoinColumn({ name: 'origin_yard_id' })
|
||||
originYard?: Yard | null;
|
||||
|
||||
@Column({ name: 'destination_yard_id', type: 'uuid', nullable: true })
|
||||
destinationYardId?: string | null;
|
||||
|
||||
@ManyToOne(() => Yard, { nullable: true, eager: false })
|
||||
@JoinColumn({ name: 'destination_yard_id' })
|
||||
destinationYard?: Yard | null;
|
||||
|
||||
@Column({ name: 'currency', type: 'varchar', length: 5 })
|
||||
currency!: string;
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface IRatesRepository {
|
||||
containerTypeId?: string | null;
|
||||
cargoTypeId?: string | null;
|
||||
tradeDirection?: string | null;
|
||||
originYardId?: string | null;
|
||||
destinationYardId?: string | null;
|
||||
}): Promise<Rate | null>;
|
||||
findAll(options?: FindManyOptions<Rate>): Promise<Rate[]>;
|
||||
findAndCount(options?: FindManyOptions<Rate>): Promise<[Rate[], number]>;
|
||||
|
||||
@@ -37,6 +37,8 @@ export class RatesRepository implements IRatesRepository {
|
||||
containerTypeId?: string | null;
|
||||
cargoTypeId?: string | null;
|
||||
tradeDirection?: string | null;
|
||||
originYardId?: string | null;
|
||||
destinationYardId?: string | null;
|
||||
}): Promise<Rate | null> {
|
||||
const qb = this.repo
|
||||
.createQueryBuilder('rate')
|
||||
@@ -59,6 +61,18 @@ export class RatesRepository implements IRatesRepository {
|
||||
} else {
|
||||
qb.andWhere('rate.trade_direction IS NULL');
|
||||
}
|
||||
if (pattern.originYardId) {
|
||||
qb.andWhere('rate.origin_yard_id = :originYardId', { originYardId: pattern.originYardId });
|
||||
} else {
|
||||
qb.andWhere('rate.origin_yard_id IS NULL');
|
||||
}
|
||||
if (pattern.destinationYardId) {
|
||||
qb.andWhere('rate.destination_yard_id = :destinationYardId', {
|
||||
destinationYardId: pattern.destinationYardId,
|
||||
});
|
||||
} else {
|
||||
qb.andWhere('rate.destination_yard_id IS NULL');
|
||||
}
|
||||
|
||||
return qb.getOne();
|
||||
}
|
||||
@@ -75,6 +89,10 @@ export class RatesRepository implements IRatesRepository {
|
||||
findPaged(query: ListRatesQueryDto): Promise<PaginatedResponse<Rate>> {
|
||||
const qb = this.repo
|
||||
.createQueryBuilder('rate')
|
||||
// The admin table shows the leg a base-freight rate prices — without the
|
||||
// yards joined the route columns have only ids to render.
|
||||
.leftJoinAndSelect('rate.originYard', 'originYard')
|
||||
.leftJoinAndSelect('rate.destinationYard', 'destinationYard')
|
||||
.orderBy('rate.createdAt', query.sortOrder ?? 'DESC');
|
||||
|
||||
if (query.status) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { PaginatedResponse } from '@edr/types';
|
||||
import { PaginatedResponse, YardCountry } from '@edr/types';
|
||||
import { CreateRateDto } from '../dto/create-rate.dto';
|
||||
import { ListRatesQueryDto } from '../dto/list-rule-engine-query.dto';
|
||||
import { UpdateRateDto } from '../dto/update-rate.dto';
|
||||
@@ -14,12 +14,24 @@ import { Rate } from '../entities/rate.entity';
|
||||
import { deriveRateType } from '../entities/rate-type.util';
|
||||
import { allowedRateUnits, isRateUnitAllowed } from '../entities/rate-unit.util';
|
||||
import { IRatesRepository, RATES_REPOSITORY } from '../interfaces/rates.repository.interface';
|
||||
import { IYardsRepository, YARDS_REPOSITORY } from '../interfaces/yards.repository.interface';
|
||||
|
||||
/** Categories priced per rail leg — they carry an origin → destination yard pair. */
|
||||
const BASE_FREIGHT_CATEGORIES: readonly Rate['appliesTo'][] = ['BULK', 'CONTAINER', 'INTERCITY'];
|
||||
|
||||
/** The yard pair a rate scopes to, already validated against its direction. */
|
||||
interface YardScope {
|
||||
originYardId: string | null;
|
||||
destinationYardId: string | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RatesService {
|
||||
constructor(
|
||||
@Inject(RATES_REPOSITORY)
|
||||
private readonly repository: IRatesRepository,
|
||||
@Inject(YARDS_REPOSITORY)
|
||||
private readonly yardsRepository: IYardsRepository,
|
||||
) {}
|
||||
|
||||
/** List rates — standard paginated envelope with server-side search. */
|
||||
@@ -62,6 +74,136 @@ export class RatesService {
|
||||
return requestedUnit;
|
||||
}
|
||||
|
||||
/** Base rail freight is priced per leg; surcharges and truck legs are not. */
|
||||
private isBaseFreight(appliesTo: Rate['appliesTo'], trigger: Rate['trigger']): boolean {
|
||||
return trigger === 'ALWAYS' && BASE_FREIGHT_CATEGORIES.includes(appliesTo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Which country each end of the leg must sit in, given what the rate is for.
|
||||
* The railway only sells three shapes: import lands at the Djibouti ports and
|
||||
* rails inland, export is the reverse, and intercity stays inside Ethiopia.
|
||||
*/
|
||||
private expectedYardCountries(
|
||||
appliesTo: Rate['appliesTo'],
|
||||
tradeDirection: string | null,
|
||||
): { origin: YardCountry; destination: YardCountry } {
|
||||
if (appliesTo === 'INTERCITY') {
|
||||
return { origin: YardCountry.ETHIOPIA, destination: YardCountry.ETHIOPIA };
|
||||
}
|
||||
return tradeDirection === 'EXPORT'
|
||||
? { origin: YardCountry.ETHIOPIA, destination: YardCountry.DJIBOUTI }
|
||||
: { origin: YardCountry.DJIBOUTI, destination: YardCountry.ETHIOPIA };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and normalise the leg a rate prices.
|
||||
*
|
||||
* Base freight must name both yards and they must match the direction, so a
|
||||
* "container import" rate cannot be quoted Ethiopia → Ethiopia. Everything
|
||||
* else (surcharges, first/last mile) is route-agnostic and has its yards
|
||||
* cleared, mirroring how container/cargo scope is cleared for surcharges.
|
||||
*/
|
||||
private async resolveYardScope(input: {
|
||||
appliesTo: Rate['appliesTo'];
|
||||
trigger: Rate['trigger'];
|
||||
tradeDirection: string | null;
|
||||
originYardId?: string | null;
|
||||
destinationYardId?: string | null;
|
||||
}): Promise<YardScope> {
|
||||
const { appliesTo, trigger, tradeDirection } = input;
|
||||
if (!this.isBaseFreight(appliesTo, trigger)) {
|
||||
return { originYardId: null, destinationYardId: null };
|
||||
}
|
||||
|
||||
const originYardId = input.originYardId ?? null;
|
||||
const destinationYardId = input.destinationYardId ?? null;
|
||||
if (!originYardId || !destinationYardId) {
|
||||
throw new BadRequestException(
|
||||
'Base freight rates are priced per leg — pick both an origin and a destination yard.',
|
||||
);
|
||||
}
|
||||
if (originYardId === destinationYardId) {
|
||||
throw new BadRequestException('Origin and destination yard must be different.');
|
||||
}
|
||||
|
||||
const [origin, destination] = await Promise.all([
|
||||
this.yardsRepository.findById(originYardId),
|
||||
this.yardsRepository.findById(destinationYardId),
|
||||
]);
|
||||
if (!origin) throw new BadRequestException(`Origin yard ${originYardId} not found`);
|
||||
if (!destination) {
|
||||
throw new BadRequestException(`Destination yard ${destinationYardId} not found`);
|
||||
}
|
||||
|
||||
const expected = this.expectedYardCountries(appliesTo, tradeDirection);
|
||||
if (origin.country !== expected.origin || destination.country !== expected.destination) {
|
||||
const shape =
|
||||
appliesTo === 'INTERCITY' ? 'Intercity' : `${tradeDirection ?? 'Import'} freight`;
|
||||
throw new BadRequestException(
|
||||
`${shape} runs ${expected.origin} → ${expected.destination}, but ${origin.label} is in ` +
|
||||
`${origin.country} and ${destination.label} is in ${destination.country}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return { originYardId, destinationYardId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Guard the scope fields a base-freight category needs before we derive its
|
||||
* rateType: import/export must say which, and intercity must say whether it
|
||||
* carries containers or bulk (the two price differently and an unstated kind
|
||||
* would silently file the rate as one of them).
|
||||
*/
|
||||
private assertScopeCoherent(input: {
|
||||
appliesTo: Rate['appliesTo'];
|
||||
trigger: Rate['trigger'];
|
||||
tradeDirection: string | null;
|
||||
intercityKind: string | null;
|
||||
containerTypeId: string | null;
|
||||
cargoTypeId: string | null;
|
||||
}): void {
|
||||
const { appliesTo, trigger, tradeDirection, intercityKind } = input;
|
||||
const { containerTypeId, cargoTypeId } = input;
|
||||
if (!this.isBaseFreight(appliesTo, trigger)) return;
|
||||
|
||||
if (appliesTo === 'INTERCITY') {
|
||||
if (intercityKind !== 'CONTAINER' && intercityKind !== 'BULK') {
|
||||
throw new BadRequestException(
|
||||
'An intercity rate must say whether it covers containers or bulk.',
|
||||
);
|
||||
}
|
||||
// The scope field has to agree with the kind, or the rate would advertise
|
||||
// one cargo kind and narrow by the other.
|
||||
if (intercityKind === 'CONTAINER' && cargoTypeId) {
|
||||
throw new BadRequestException(
|
||||
'An intercity container rate cannot be scoped to a bulk cargo type.',
|
||||
);
|
||||
}
|
||||
if (intercityKind === 'BULK' && containerTypeId) {
|
||||
throw new BadRequestException(
|
||||
'An intercity bulk rate cannot be scoped to a container type.',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (tradeDirection !== 'IMPORT' && tradeDirection !== 'EXPORT') {
|
||||
throw new BadRequestException(
|
||||
`${appliesTo === 'BULK' ? 'Bulk' : 'Container'} freight must be either IMPORT or EXPORT.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a rate covers bulk cargo — the flag `deriveRateType` splits
|
||||
* INTERCITY_BULK from INTERCITY_CONTAINER on. Intercity states its kind
|
||||
* explicitly; for BULK/CONTAINER the category already says it.
|
||||
*/
|
||||
private resolvesToBulk(appliesTo: Rate['appliesTo'], intercityKind: string | null): boolean {
|
||||
return appliesTo === 'INTERCITY' ? intercityKind === 'BULK' : appliesTo === 'BULK';
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a second rate with the same identity pattern (rateType + scope). With
|
||||
* effective-date windows gone, two LIVE/DRAFT rates for the same pattern would
|
||||
@@ -73,12 +215,14 @@ export class RatesService {
|
||||
containerTypeId: string | null;
|
||||
cargoTypeId: string | null;
|
||||
tradeDirection: string | null;
|
||||
originYardId: string | null;
|
||||
destinationYardId: string | null;
|
||||
ignoreId?: string;
|
||||
}): Promise<void> {
|
||||
const existing = await this.repository.findByPattern(pattern);
|
||||
if (existing && existing.id !== pattern.ignoreId) {
|
||||
throw new ConflictException(
|
||||
'A rate for this exact combination already exists. Edit or delete the existing rate instead of creating a duplicate.',
|
||||
'A rate for this exact combination already exists on this route. Edit or delete the existing rate instead of creating a duplicate.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -92,17 +236,45 @@ export class RatesService {
|
||||
const isSurcharge = trigger !== 'ALWAYS';
|
||||
const containerTypeId = isSurcharge ? null : (dto.containerTypeId ?? null);
|
||||
const cargoTypeId = isSurcharge ? null : (dto.cargoTypeId ?? null);
|
||||
const tradeDirection = isSurcharge ? null : (dto.tradeDirection ?? null);
|
||||
// Intercity never leaves Ethiopia, so it has no trade direction to store —
|
||||
// its yard pair already says where it runs.
|
||||
const tradeDirection =
|
||||
isSurcharge || appliesTo === 'INTERCITY' ? null : (dto.tradeDirection ?? null);
|
||||
|
||||
const intercityKind = dto.intercityKind ?? null;
|
||||
this.assertScopeCoherent({
|
||||
appliesTo,
|
||||
trigger,
|
||||
tradeDirection,
|
||||
intercityKind,
|
||||
containerTypeId,
|
||||
cargoTypeId,
|
||||
});
|
||||
const { originYardId, destinationYardId } = await this.resolveYardScope({
|
||||
appliesTo,
|
||||
trigger,
|
||||
tradeDirection,
|
||||
originYardId: dto.originYardId,
|
||||
destinationYardId: dto.destinationYardId,
|
||||
});
|
||||
|
||||
const rateType = deriveRateType({
|
||||
appliesTo,
|
||||
trigger,
|
||||
tradeDirection,
|
||||
isBulk: Boolean(cargoTypeId),
|
||||
isBulk: this.resolvesToBulk(appliesTo, intercityKind),
|
||||
});
|
||||
const rateUnit = this.resolveRateUnit(appliesTo, trigger, dto.rateUnit as Rate['rateUnit']);
|
||||
|
||||
await this.assertNoDuplicatePattern({ rateType, rateUnit, containerTypeId, cargoTypeId, tradeDirection });
|
||||
await this.assertNoDuplicatePattern({
|
||||
rateType,
|
||||
rateUnit,
|
||||
containerTypeId,
|
||||
cargoTypeId,
|
||||
tradeDirection,
|
||||
originYardId,
|
||||
destinationYardId,
|
||||
});
|
||||
|
||||
return this.repository.create({
|
||||
appliesTo,
|
||||
@@ -111,6 +283,8 @@ export class RatesService {
|
||||
containerTypeId,
|
||||
cargoTypeId,
|
||||
tradeDirection,
|
||||
originYardId,
|
||||
destinationYardId,
|
||||
currency: dto.currency ?? 'USD',
|
||||
rateValue: dto.rateValue,
|
||||
rateUnit,
|
||||
@@ -194,21 +368,52 @@ export class RatesService {
|
||||
: dto.cargoTypeId !== undefined
|
||||
? dto.cargoTypeId
|
||||
: existing.cargoTypeId;
|
||||
const tradeDirection = isSurcharge
|
||||
? null
|
||||
: dto.tradeDirection !== undefined
|
||||
? dto.tradeDirection
|
||||
: existing.tradeDirection;
|
||||
const tradeDirection =
|
||||
isSurcharge || appliesTo === 'INTERCITY'
|
||||
? null
|
||||
: dto.tradeDirection !== undefined
|
||||
? dto.tradeDirection
|
||||
: existing.tradeDirection;
|
||||
|
||||
updates.containerTypeId = containerTypeId ?? null;
|
||||
updates.cargoTypeId = cargoTypeId ?? null;
|
||||
updates.tradeDirection = tradeDirection ?? null;
|
||||
|
||||
// A patch that leaves the cargo kind unsaid keeps the one the rate already
|
||||
// has — read back off its rateType, the only place it is recorded.
|
||||
const intercityKind =
|
||||
dto.intercityKind ?? (existing.rateType === 'INTERCITY_BULK' ? 'BULK' : 'CONTAINER');
|
||||
|
||||
this.assertScopeCoherent({
|
||||
appliesTo,
|
||||
trigger,
|
||||
tradeDirection: updates.tradeDirection,
|
||||
intercityKind,
|
||||
containerTypeId: updates.containerTypeId,
|
||||
cargoTypeId: updates.cargoTypeId,
|
||||
});
|
||||
// Re-validate the leg: changing direction can invalidate a yard pair that
|
||||
// was legal under the old one (an import route is not an export route).
|
||||
const yardScope = await this.resolveYardScope({
|
||||
appliesTo,
|
||||
trigger,
|
||||
tradeDirection: updates.tradeDirection,
|
||||
originYardId:
|
||||
dto.originYardId !== undefined ? dto.originYardId : existing.originYardId,
|
||||
destinationYardId:
|
||||
dto.destinationYardId !== undefined
|
||||
? dto.destinationYardId
|
||||
: existing.destinationYardId,
|
||||
});
|
||||
updates.originYardId = yardScope.originYardId;
|
||||
updates.destinationYardId = yardScope.destinationYardId;
|
||||
|
||||
// Keep the derived rateType in sync with whatever changed.
|
||||
const rateType = deriveRateType({
|
||||
appliesTo,
|
||||
trigger,
|
||||
tradeDirection,
|
||||
isBulk: Boolean(cargoTypeId),
|
||||
isBulk: this.resolvesToBulk(appliesTo, intercityKind),
|
||||
});
|
||||
updates.rateType = rateType;
|
||||
|
||||
@@ -224,6 +429,8 @@ export class RatesService {
|
||||
containerTypeId: updates.containerTypeId,
|
||||
cargoTypeId: updates.cargoTypeId,
|
||||
tradeDirection: updates.tradeDirection,
|
||||
originYardId: updates.originYardId,
|
||||
destinationYardId: updates.destinationYardId,
|
||||
ignoreId: id,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user