mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 02:58:11 +00:00
implement intercity booking management and booking window websocket integration
This commit is contained in:
@@ -8,10 +8,14 @@ import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { insertWithGeneratedReference } from '@edr/api-common';
|
||||
|
||||
import { YardCountry } from '@edr/types';
|
||||
|
||||
import { deriveTradeDirection } from '../../common/derive-trade-direction.util';
|
||||
import { CompaniesService } from '../companies/companies.service';
|
||||
import { CompanyProfile, ProfileType } from '../companies/entities/company-profile.entity';
|
||||
import { CompanyStatus } from '../companies/entities/company.entity';
|
||||
import { ServiceType } from '../rule-engine/entities/service-type.entity';
|
||||
import { Yard } from '../rule-engine/entities/yard.entity';
|
||||
import { FilesService } from '../files/files.service';
|
||||
import { MinioService } from '../minio/minio.service';
|
||||
import { ContractsRepository } from './contracts.repository';
|
||||
@@ -110,6 +114,49 @@ export class ContractsService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Every route must match the contract's declared trade direction as derived
|
||||
* from the yard countries (IMPORT = DJ→ET, EXPORT = ET→DJ, DOMESTIC =
|
||||
* intercity). Intercity is Ethiopian-domestic only: both yards must be in
|
||||
* Ethiopia — a Djibouti-internal pair is rejected. Direction mismatches
|
||||
* (e.g. an export lane on an import contract) are rejected for every kind.
|
||||
*/
|
||||
private async assertRoutesMatchDirection(
|
||||
tradeDirection: string,
|
||||
routes: CreateContractDto['routes'],
|
||||
): Promise<void> {
|
||||
const yardIds = [
|
||||
...new Set(routes.flatMap((r) => [r.originYardId, r.destinationYardId])),
|
||||
];
|
||||
const yards = await this.dataSource
|
||||
.getRepository(Yard)
|
||||
.find({ where: yardIds.map((id) => ({ id })) });
|
||||
const yardById = new Map(yards.map((y) => [y.id, y]));
|
||||
|
||||
for (const route of routes) {
|
||||
const origin = yardById.get(route.originYardId);
|
||||
const destination = yardById.get(route.destinationYardId);
|
||||
if (!origin || !destination) {
|
||||
throw new BadRequestException('Route references a yard that does not exist');
|
||||
}
|
||||
const derived = deriveTradeDirection(origin, destination);
|
||||
if (derived !== tradeDirection) {
|
||||
throw new BadRequestException(
|
||||
`Route ${origin.label} → ${destination.label} is ${derived === 'DOMESTIC' ? 'an intercity' : `an ${derived.toLowerCase()}`} lane and does not match the contract's ${tradeDirection === 'DOMESTIC' ? 'intercity' : tradeDirection.toLowerCase()} direction`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
derived === 'DOMESTIC' &&
|
||||
(origin.country !== YardCountry.ETHIOPIA ||
|
||||
destination.country !== YardCountry.ETHIOPIA)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
`Route ${origin.label} → ${destination.label}: intercity service only runs between Ethiopian yards`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Create a new contract (DRAFT) with its routes and cargo-scope rows. */
|
||||
async create(
|
||||
dto: CreateContractDto,
|
||||
@@ -144,6 +191,7 @@ export class ContractsService {
|
||||
|
||||
this.assertCargoScopeShape(dto.freightType, dto.cargoScope);
|
||||
this.assertRouteShape(dto.contractKind, dto.routes);
|
||||
await this.assertRoutesMatchDirection(dto.tradeDirection, dto.routes);
|
||||
|
||||
// Stamp the operational profile (importer/exporter) for portal scoping.
|
||||
let companyProfileId: string | null = null;
|
||||
@@ -175,6 +223,13 @@ export class ContractsService {
|
||||
|
||||
// Customs clearing is owned by the service type, not the customer.
|
||||
const includesCustoms = await this.resolveIncludesCustoms(dto.serviceTypeId);
|
||||
// Intercity never crosses a border, so a customs-including service type is
|
||||
// a contradiction — the wizard hides them, the API enforces it.
|
||||
if (dto.tradeDirection === 'DOMESTIC' && includesCustoms) {
|
||||
throw new BadRequestException(
|
||||
'Intercity contracts cannot use a service type that includes customs clearing',
|
||||
);
|
||||
}
|
||||
|
||||
// An explicit reference is caller-chosen — a collision there is a real
|
||||
// conflict and should surface. Auto-generated references retry past a
|
||||
@@ -360,6 +415,12 @@ export class ContractsService {
|
||||
|
||||
if (dto.cargoScope) this.assertCargoScopeShape(freightType, dto.cargoScope);
|
||||
if (dto.routes) this.assertRouteShape(contractKind, dto.routes);
|
||||
if (dto.routes) {
|
||||
await this.assertRoutesMatchDirection(
|
||||
dto.tradeDirection ?? existing.tradeDirection,
|
||||
dto.routes,
|
||||
);
|
||||
}
|
||||
|
||||
const updates: Record<string, unknown> = {
|
||||
contractKind,
|
||||
@@ -385,6 +446,11 @@ export class ContractsService {
|
||||
const includesCustoms = await this.resolveIncludesCustoms(
|
||||
dto.serviceTypeId ?? existing.serviceTypeId,
|
||||
);
|
||||
if ((dto.tradeDirection ?? existing.tradeDirection) === 'DOMESTIC' && includesCustoms) {
|
||||
throw new BadRequestException(
|
||||
'Intercity contracts cannot use a service type that includes customs clearing',
|
||||
);
|
||||
}
|
||||
updates.customsClearingEnabled = includesCustoms;
|
||||
updates.customsClearingAgent = includesCustoms
|
||||
? null
|
||||
|
||||
Reference in New Issue
Block a user