mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 00:10:57 +00:00
enhance contract and train scheduling features
- Added pricing service integration to ContractsService for pre-persistence contract pricing. - Updated LegCapacityPanel to display cargo weight instead of gross weight for better clarity on booked cargo. - Enhanced train scheduling service to include cargo weight in booking details. - Modified ClearanceDocumentsPage to include FULLY_EXECUTED status in booking status options. - Refactored FileUploadSettingsPage to categorize file upload settings into tabs for better organization. - Removed legacy onboarding fields and settings from file upload settings seeder. - Improved type definitions for train scheduling to include cargo weight without wagon tare.
This commit is contained in:
@@ -14,10 +14,10 @@ describe('BookingTransitionService — finalizeClearance gate', () => {
|
||||
serviceType: { includesCustoms: false }, // no output set → only the input gate
|
||||
};
|
||||
|
||||
// Input set has two required docs. Non-customs bookings resolve to the
|
||||
// ONE_TIME self-clearance document set.
|
||||
// Input set has two required docs. Non-customs bookings resolve to their
|
||||
// own without-customs document set.
|
||||
const inputSetting = {
|
||||
code: 'contract_clearance_selfclear_import_container',
|
||||
code: 'clearance_import_container_without_customs',
|
||||
fields: [
|
||||
{ fileKey: 'commercial_invoice', isRequired: true },
|
||||
{ fileKey: 'packing_list', isRequired: true },
|
||||
@@ -201,7 +201,7 @@ describe('BookingTransitionService — finalizeClearance customs output gate', (
|
||||
*/
|
||||
describe('BookingTransitionService — submitClearanceDocuments required-fields gate', () => {
|
||||
const inputSetting = {
|
||||
code: 'contract_clearance_selfclear_import_container',
|
||||
code: 'clearance_import_container_without_customs',
|
||||
fields: [
|
||||
{ fileKey: 'commercial_invoice', fileLabel: 'Commercial invoice', isRequired: true },
|
||||
{ fileKey: 'packing_list', fileLabel: 'Packing list', isRequired: true },
|
||||
|
||||
@@ -11,10 +11,8 @@ describe('clearance.util — clearanceSettingCode', () => {
|
||||
expect(clearanceSettingCode('IMPORT', 'CONTAINER', true)).toBe(
|
||||
'clearance_import_container_with_customs',
|
||||
);
|
||||
// Non-customs bookings self-clear with the same document set a ONE_TIME
|
||||
// self-clear contract uses.
|
||||
expect(clearanceSettingCode('IMPORT', 'CONTAINER', false)).toBe(
|
||||
'contract_clearance_selfclear_import_container',
|
||||
'clearance_import_container_without_customs',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -23,7 +21,7 @@ describe('clearance.util — clearanceSettingCode', () => {
|
||||
'clearance_export_bulk_with_customs',
|
||||
);
|
||||
expect(clearanceSettingCode('EXPORT', 'BULK', false)).toBe(
|
||||
'contract_clearance_selfclear_export_bulk',
|
||||
'clearance_export_bulk_without_customs',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -39,12 +39,11 @@ export function clearanceSettingCode(
|
||||
const op = operationFor(tradeDirection);
|
||||
if (!op) return null;
|
||||
const freight = freightFor(freightType);
|
||||
// Non-customs (Path A) bookings self-clear: the customer proves his own
|
||||
// clearance with the SAME smaller document set a ONE_TIME self-clear
|
||||
// contract uses (customs declaration, release permit, …) — not the
|
||||
// GL-oriented booking sets.
|
||||
// 4 import + 4 export cases (bulk/container × with/without customs) — each
|
||||
// booking resolves to its own clearance_{op}_{freight}_{with|without}_customs
|
||||
// set, independent of any contract-level clearance codes.
|
||||
if (!includesCustoms) {
|
||||
return `contract_clearance_selfclear_${op}_${freight}`;
|
||||
return `clearance_${op}_${freight}_without_customs`;
|
||||
}
|
||||
return `clearance_${op}_${freight}_with_customs`;
|
||||
}
|
||||
|
||||
@@ -26,30 +26,22 @@ function freightFor(freightType: string): Freight {
|
||||
/**
|
||||
* The customer-input clearance setting code, or null when no gate applies.
|
||||
*
|
||||
* - Path B (customs bundled): the customer uploads the documents GL needs to do
|
||||
* the clearance work → `contract_clearance_{op}_{freight}`.
|
||||
* - Path A (no customs): the customer clears the cargo himself and uploads his
|
||||
* own (smaller) clearance proof set → `contract_clearance_selfclear_{op}_{freight}`,
|
||||
* reviewed by Operations rather than GL.
|
||||
* Contract-level IMPORT/EXPORT clearance has been removed — clearance is
|
||||
* collected per booking instead (see bookings/clearance.util.ts), so this
|
||||
* always returns null for IMPORT/EXPORT now.
|
||||
*
|
||||
* DOMESTIC/intercity has no border, but a ONE_TIME intercity contract still
|
||||
* collects the admin-configured intercity document set after both signatures
|
||||
* (ops-reviewed, like Path A). GENERAL intercity contracts skip the contract
|
||||
* gate and collect the same set per booking instead.
|
||||
* (ops-reviewed). GENERAL intercity contracts skip the contract gate and
|
||||
* collect the same set per booking instead.
|
||||
*/
|
||||
export function contractClearanceSettingCode(
|
||||
tradeDirection: string,
|
||||
freightType: string,
|
||||
includesCustoms: boolean,
|
||||
_freightType: string,
|
||||
_includesCustoms: boolean,
|
||||
): string | null {
|
||||
if (tradeDirection === 'DOMESTIC') return INTERCITY_DOCUMENTS_SETTING_CODE;
|
||||
const op = operationFor(tradeDirection);
|
||||
if (!op) return null;
|
||||
const freight = freightFor(freightType);
|
||||
if (!includesCustoms) {
|
||||
return `contract_clearance_selfclear_${op}_${freight}`;
|
||||
}
|
||||
return `contract_clearance_${op}_${freight}`;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The GL-output (customs output) setting code, keyed on op + freight. */
|
||||
|
||||
@@ -42,6 +42,7 @@ describe('ContractsService duplicate guard', () => {
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{ buildBreakdown: async () => ({ lineItems: [] }) } as never,
|
||||
);
|
||||
return (
|
||||
service as unknown as {
|
||||
|
||||
@@ -28,6 +28,7 @@ import { ContractCargoScope } from './entities/contract-cargo-scope.entity';
|
||||
import { isEffectivelyExpired } from './utils/contract-expiry.util';
|
||||
import { diffContractFields } from './contract-document-diff.util';
|
||||
import { ContractDocumentHistoryService } from './contract-document-history.service';
|
||||
import { ContractPricingService } from './contract-pricing.service';
|
||||
import { FileRecord } from '../files/entities/file.entity';
|
||||
|
||||
/** Paginated contract list: flat `total` (backoffice) + `meta` block (portal). */
|
||||
@@ -113,6 +114,7 @@ export class ContractsService {
|
||||
private readonly filesService: FilesService,
|
||||
private readonly minioService: MinioService,
|
||||
private readonly documentHistory: ContractDocumentHistoryService,
|
||||
private readonly pricingService: ContractPricingService,
|
||||
) {}
|
||||
|
||||
/** Generate a unique contract reference number (CTR-YYYY-NNNNN). */
|
||||
@@ -331,6 +333,33 @@ export class ContractsService {
|
||||
);
|
||||
}
|
||||
|
||||
// Price the contract BEFORE anything persists: a lane with no configured
|
||||
// rate 422s here and the wizard shows its blocking modal — with no orphan
|
||||
// DRAFT row left behind for the customer to trip over on retry. The probe
|
||||
// carries exactly the fields buildBreakdown prices from; relation-only
|
||||
// niceties (cargoType labels) are absent, which only affects display
|
||||
// lines, never the missing-rate gates.
|
||||
await this.pricingService.buildBreakdown({
|
||||
tradeDirection: dto.tradeDirection,
|
||||
freightType: dto.freightType,
|
||||
paymentCurrency: 'USD',
|
||||
customsClearingEnabled: includesCustoms,
|
||||
isHazardous: dto.isHazardous ?? false,
|
||||
isReefer: dto.isReefer ?? false,
|
||||
equipmentReturn: dto.equipmentReturn ?? null,
|
||||
firstMilePickupAddress: dto.firstMilePickupAddress ?? null,
|
||||
lastMileDeliveryAddress: dto.lastMileDeliveryAddress ?? null,
|
||||
routes: (dto.routes ?? []).map((r, i) => ({
|
||||
originYardId: r.originYardId,
|
||||
destinationYardId: r.destinationYardId,
|
||||
sortOrder: r.sortOrder ?? i,
|
||||
})),
|
||||
cargoScope: (dto.cargoScope ?? []).map((c) => ({
|
||||
containerSize: c.containerSize ?? null,
|
||||
cargoTypeId: c.cargoTypeId ?? null,
|
||||
})),
|
||||
} as unknown as Contract);
|
||||
|
||||
// An explicit reference is caller-chosen — a collision there is a real
|
||||
// conflict and should surface. Auto-generated references retry past a
|
||||
// concurrent insert that grabbed the same sequence number.
|
||||
|
||||
@@ -7746,6 +7746,9 @@ export class TrainSchedulingService {
|
||||
weightTons: sb.booking
|
||||
? this.grossBookingWeightTons(sb.booking, tareDims)
|
||||
: 0,
|
||||
// Cargo only (VGM / bulk tons) — what the customer actually booked,
|
||||
// without the wagons' tare. The legs tab shows this per booking.
|
||||
cargoWeightTons: sb.booking ? bookingCargoTons(sb.booking) : 0,
|
||||
status: sb.booking?.status ?? null,
|
||||
schedulingStatus: sb.booking?.schedulingStatus ?? null,
|
||||
freightType: sb.booking?.freightType ?? null,
|
||||
|
||||
Reference in New Issue
Block a user