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:
marshalyordanos
2026-08-13 15:54:40 +03:00
parent 9aae132dd4
commit 9fff469ffa
50 changed files with 4485 additions and 77 deletions

View File

@@ -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,
);
});

View File

@@ -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,