implement Global Logistics booking process for customs contracts and enhance contract document handling

This commit is contained in:
Marshal
2026-06-29 08:08:10 +00:00
parent 4be4cc9054
commit aeb5e0046e
13 changed files with 436 additions and 104 deletions

View File

@@ -1,5 +1,6 @@
import {
BadRequestException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
@@ -191,15 +192,20 @@ export class ContractBookingService {
*/
private async assertGate(contract: Contract, isGlActor: boolean): Promise<string> {
if (contract.customsClearingEnabled) {
// Path B — the customer creates the booking once GL has finalized the
// pre-booking clearance (GL "create booking" was removed; clearance ends
// at finalize and hands the booking back to the customer).
// Path B — Global Logistics creates the booking ON BEHALF OF the customer
// once GL has finalized the pre-booking clearance. The customer never
// books a customs contract himself.
if (!isGlActor) {
throw new ForbiddenException(
'Customs-clearance contracts are booked by Global Logistics on behalf of the customer.',
);
}
if (contract.clearanceStatus !== 'CLEARANCE_READY_FOR_BOOKING') {
throw new BadRequestException(
'Contract clearance is not ready for booking yet.',
);
}
return isGlActor ? 'GL_ET' : 'CUSTOMER';
return 'GL_ET';
}
// Path A — customer (or staff) once the contract is executed.

View File

@@ -8,7 +8,7 @@ import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { CompaniesService } from '../companies/companies.service';
import { ProfileType } from '../companies/entities/company-profile.entity';
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 { FilesService } from '../files/files.service';
@@ -220,9 +220,55 @@ export class ContractsService {
}
}
// Attach the company profile's onboarding / business-license documents to the
// contract by reference. The separate "Documents" intake step was removed —
// the profile documents are simply carried onto every contract automatically.
await this.attachProfileDocuments(contract.id, companyProfileId);
return { contract: await this.findById(contract.id), warnings };
}
/**
* Copy a company profile's stored business-license / onboarding documents onto
* a contract by reference (no byte re-upload). Codes are slugged from each
* document name so they group under "Profile documents" on the contract detail
* page. No-op when the contract has no profile or the profile has no documents.
*/
private async attachProfileDocuments(
contractId: string,
companyProfileId: string | null,
): Promise<void> {
if (!companyProfileId) return;
const profile = await this.dataSource
.getRepository(CompanyProfile)
.findOne({ where: { id: companyProfileId } });
const docs = profile?.businessLicenseFiles ?? [];
if (docs.length === 0) return;
const slug = (name: string) =>
name
.toLowerCase()
.replace(/\.[a-z0-9]+$/, '')
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '') || 'profile_document';
try {
await this.filesService.attachExistingFiles(
contractId,
'contracts',
docs.map((d, i) => ({
code: `${slug(d.name)}_${i + 1}`,
name: d.name,
url: d.url,
size: d.size,
mimeType: d.mimeType,
})),
);
} catch {
// Non-fatal — the contract is still valid without the carried documents.
}
}
private async persistRoutes(
contractId: string,
routes: CreateContractDto['routes'],

View File

@@ -77,6 +77,13 @@ export class WagonsService {
async update(id: string, dto: UpdateWagonDto): Promise<Wagon> {
const wagon = await this.findById(id);
Object.assign(wagon, dto);
// `findById` eager-loads `currentYard`; when the DTO changes the scalar FK
// TypeORM otherwise re-derives `current_yard_id` from the STALE relation
// object (the old yard) on save and silently reverts the change. Drop the
// relation so the scalar `currentYardId` wins.
if (dto.currentYardId !== undefined) {
wagon.currentYard = null;
}
await this.wagonRepo.save(wagon);
// Re-read with the relation so the response reflects the new yard label
// instead of the stale relation object loaded before the assign.