mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
feat(permissions): add granular permissions for train management actions
feat(train-builder): update permissions checks for train actions in UI feat(contracts): enhance contract view and shipment request pages with new features test(shipment-preview): add tests for customs clearing logic in booking preview style(contract-sign-bar): create CSS for fixed sign bar layout
This commit is contained in:
@@ -1974,6 +1974,11 @@ export class ContractBookingService {
|
||||
isReefer: this.resolveShipmentHandlingFlag(contract, dto, 'reeferQuantity'),
|
||||
equipmentReturn: this.resolveShipmentEquipmentReturn(contract, dto),
|
||||
isGovernment: contract.isGovernment,
|
||||
// The clearance fee is gated on this flag in BookingPricingService, and
|
||||
// createUnderContract copies it off the contract. Omitting it here priced
|
||||
// the preview WITHOUT the customs line the created booking is then billed
|
||||
// — the customer confirmed one total and got invoiced a larger one.
|
||||
customsClearingEnabled: contract.customsClearingEnabled,
|
||||
shippingLineId: null,
|
||||
contractRouteId: route?.id ?? null,
|
||||
originYardId: route?.originYardId ?? null,
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { ContractBookingService } from './contract-booking.service';
|
||||
import type { Contract } from './entities/contract.entity';
|
||||
import type { Booking } from '../bookings/entities/booking.entity';
|
||||
import type { CreateBookingUnderContractDto } from './dto/create-booking-under-contract.dto';
|
||||
|
||||
/**
|
||||
* The price the customer confirms in the modal comes from validateShipment,
|
||||
* which prices an UNSAVED twin of the booking createUnderContract will write.
|
||||
* Any contract field that reaches the pricing service must be copied onto that
|
||||
* twin — a field left off doesn't fail loudly, it silently drops whole charge
|
||||
* lines from the quote while the created booking is still billed for them.
|
||||
*
|
||||
* The regression this locks: `customsClearingEnabled` was missing, so
|
||||
* BookingPricingService's `if (booking.customsClearingEnabled)` gate never
|
||||
* opened in the preview. Container bookings quoted rail freight alone, then
|
||||
* invoiced rail + customs clearance.
|
||||
*/
|
||||
describe('shipment preview / created booking parity', () => {
|
||||
const contract = (over: Partial<Contract> = {}): Contract =>
|
||||
({
|
||||
id: 'c1',
|
||||
freightType: 'CONTAINER',
|
||||
tradeDirection: 'EXPORT',
|
||||
paymentCurrency: 'ETB',
|
||||
serviceTypeId: 'svc1',
|
||||
customsClearingEnabled: true,
|
||||
equipmentReturn: 'NO_RETURN',
|
||||
isHazardous: false,
|
||||
isReefer: false,
|
||||
isGovernment: false,
|
||||
cargoScope: [],
|
||||
firstMilePickupAddress: null,
|
||||
lastMileDeliveryAddress: null,
|
||||
...over,
|
||||
}) as Contract;
|
||||
|
||||
/**
|
||||
* Run validateShipment against stubbed collaborators and hand back the
|
||||
* booking the pricing service was actually asked to price.
|
||||
*/
|
||||
const previewBookingFor = async (c: Contract): Promise<Booking> => {
|
||||
let priced: Booking | null = null;
|
||||
|
||||
const svc = {
|
||||
contractsRepository: { findByIdWithRelations: async () => c },
|
||||
bookingPricingService: {
|
||||
computePriceForBooking: async (b: Booking) => {
|
||||
priced = b;
|
||||
return {
|
||||
lineItems: [],
|
||||
totalAmount: 0,
|
||||
currency: 'ETB',
|
||||
overweightLines: [],
|
||||
hardBlocked: [],
|
||||
};
|
||||
},
|
||||
},
|
||||
ruleEngineService: { capacityViolations: async () => [] },
|
||||
resolveRoute: async () => null,
|
||||
resolveShipmentCurrency: () => 'ETB',
|
||||
resolveCargoTypeId: () => null,
|
||||
resolveShipmentHandlingFlag: () => false,
|
||||
resolveShipmentEquipmentReturn: () => c.equipmentReturn,
|
||||
resolveBulkTons: () => 0,
|
||||
resolveBulkWeightTons: () => 0,
|
||||
resolveContainerTypeForSize: async () => ({ id: 'ct40', sizeFt: 40 }),
|
||||
handlingCounts: () => ({
|
||||
hazardousQuantity: 0,
|
||||
reeferQuantity: 0,
|
||||
returnQuantity: 0,
|
||||
}),
|
||||
max20ftPairDiffTons: async () => 2,
|
||||
findContainerClashesOnTrain: async () => [],
|
||||
};
|
||||
|
||||
const dto = {
|
||||
containers: [
|
||||
{
|
||||
containerSize: '40ft',
|
||||
quantity: 2,
|
||||
units: [{ vgmTons: 10 }, { vgmTons: 10 }],
|
||||
},
|
||||
],
|
||||
} as unknown as CreateBookingUnderContractDto;
|
||||
|
||||
await (
|
||||
ContractBookingService.prototype as unknown as {
|
||||
validateShipment: (
|
||||
this: unknown,
|
||||
id: string,
|
||||
dto: CreateBookingUnderContractDto,
|
||||
) => Promise<unknown>;
|
||||
}
|
||||
).validateShipment.call(svc, 'c1', dto);
|
||||
|
||||
if (!priced) throw new Error('pricing service was never called');
|
||||
return priced;
|
||||
};
|
||||
|
||||
it('prices the preview with customs clearing on when the contract clears', async () => {
|
||||
// Without this the clearance fee is quoted as 0 and billed in full later.
|
||||
const booking = await previewBookingFor(contract());
|
||||
expect(booking.customsClearingEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('leaves customs clearing off when the contract does not clear', async () => {
|
||||
const booking = await previewBookingFor(
|
||||
contract({ customsClearingEnabled: false }),
|
||||
);
|
||||
expect(booking.customsClearingEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('carries the contract mile legs so trucking is quoted too', async () => {
|
||||
const booking = await previewBookingFor(
|
||||
contract({
|
||||
firstMilePickupAddress: 'Modjo Dry Port',
|
||||
lastMileDeliveryAddress: 'Djibouti Port',
|
||||
}),
|
||||
);
|
||||
expect(booking.firstMilePickupAddress).toBe('Modjo Dry Port');
|
||||
expect(booking.lastMileDeliveryAddress).toBe('Djibouti Port');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user