mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 03:10:54 +00:00
[200~feat: add CustomsPaymentsCard and PaymentsTab components for handling customs payments and payment summaries
This commit is contained in:
@@ -376,12 +376,21 @@ export class ContractPricingService {
|
||||
// own container-type rate), bulk contracts freeze the route's bulk fee.
|
||||
// A customs contract may not proceed without the fee(s) configured.
|
||||
if (contract.customsClearingEnabled) {
|
||||
// An Ethiopian-side-only customs service prices off its own rate; the
|
||||
// snapshot codes carry the same prefix so booking pricing finds them.
|
||||
const customsType = contract.serviceType?.includesEthiopianCustomsOnly
|
||||
? 'ETHIOPIAN_CUSTOMS_CLEARANCE'
|
||||
: 'CUSTOMS_CLEARANCE';
|
||||
const customsLabel =
|
||||
customsType === 'ETHIOPIAN_CUSTOMS_CLEARANCE'
|
||||
? 'Ethiopian customs clearance service'
|
||||
: 'Customs clearance service';
|
||||
// Strict, no route-less fallback.
|
||||
// ponytail: multi-route contracts bill the first lane's fee; per-lane fees need per-route snapshots.
|
||||
const onLeg = route
|
||||
? liveRates.filter(
|
||||
(r) =>
|
||||
r.rateType === 'CUSTOMS_CLEARANCE' &&
|
||||
r.rateType === customsType &&
|
||||
r.currency === 'USD' &&
|
||||
r.tradeDirection === contract.tradeDirection &&
|
||||
r.originYardId === route.originYardId &&
|
||||
@@ -406,14 +415,14 @@ export class ContractPricingService {
|
||||
);
|
||||
if (!rate || Number(rate.rateValue) <= 0) {
|
||||
throw new UnprocessableEntityException(
|
||||
`No customs clearance service fee is configured for ${size} containers on this direction and route. Ask the rates team to set a live CUSTOMS_CLEARANCE rate for this container type and origin → destination.`,
|
||||
`No customs clearance service fee is configured for ${size} containers on this direction and route. Ask the rates team to set a live ${customsType} rate for this container type and origin → destination.`,
|
||||
);
|
||||
}
|
||||
lineItems.push({
|
||||
// Distinct code per size so the frozen snapshots don't collide —
|
||||
// booking pricing looks each size up by CUSTOMS_CLEARANCE_<FT>FT.
|
||||
code: `CUSTOMS_CLEARANCE_${sizeFt}FT`,
|
||||
label: `Customs clearance service (${size})`,
|
||||
// booking pricing looks each size up by <customsType>_<FT>FT.
|
||||
code: `${customsType}_${sizeFt}FT`,
|
||||
label: `${customsLabel} (${size})`,
|
||||
unit: toContractUnit(rate.rateUnit),
|
||||
unitPrice: convert(Number(rate.rateValue)),
|
||||
containerSize: size,
|
||||
@@ -432,12 +441,12 @@ export class ContractPricingService {
|
||||
: undefined) ?? onLeg.find((r) => !r.containerTypeId && !r.cargoTypeId);
|
||||
if (!rate || Number(rate.rateValue) <= 0) {
|
||||
throw new UnprocessableEntityException(
|
||||
'No bulk customs clearance service fee is configured for this cargo type on this direction and route. Ask the rates team to set a live bulk CUSTOMS_CLEARANCE rate for this commodity and origin → destination.',
|
||||
`No bulk customs clearance service fee is configured for this cargo type on this direction and route. Ask the rates team to set a live bulk ${customsType} rate for this commodity and origin → destination.`,
|
||||
);
|
||||
}
|
||||
lineItems.push({
|
||||
code: 'CUSTOMS_CLEARANCE',
|
||||
label: `Customs clearance service (${scope?.cargoType?.cargoTypeName ?? 'bulk'})`,
|
||||
code: customsType,
|
||||
label: `${customsLabel} (${scope?.cargoType?.cargoTypeName ?? 'bulk'})`,
|
||||
unit: toContractUnit(rate.rateUnit),
|
||||
unitPrice: convert(Number(rate.rateValue)),
|
||||
cargoTypeCode: scope?.cargoType?.code ?? null,
|
||||
|
||||
@@ -124,11 +124,14 @@ export class ContractsService {
|
||||
return `CTR-${year}-${String(seq + 1).padStart(5, '0')}`;
|
||||
}
|
||||
|
||||
/** The service type a contract is sold under (null when the id is unknown). */
|
||||
private resolveServiceType(serviceTypeId: string): Promise<ServiceType | null> {
|
||||
return this.dataSource.getRepository(ServiceType).findOne({ where: { id: serviceTypeId } });
|
||||
}
|
||||
|
||||
/** Whether a service type bundles customs clearance. */
|
||||
private async resolveIncludesCustoms(serviceTypeId: string): Promise<boolean> {
|
||||
const serviceType = await this.dataSource
|
||||
.getRepository(ServiceType)
|
||||
.findOne({ where: { id: serviceTypeId } });
|
||||
const serviceType = await this.resolveServiceType(serviceTypeId);
|
||||
return serviceType?.includesCustoms ?? false;
|
||||
}
|
||||
|
||||
@@ -324,7 +327,8 @@ export class ContractsService {
|
||||
}
|
||||
|
||||
// Customs clearing is owned by the service type, not the customer.
|
||||
const includesCustoms = await this.resolveIncludesCustoms(dto.serviceTypeId);
|
||||
const serviceType = await this.resolveServiceType(dto.serviceTypeId);
|
||||
const includesCustoms = serviceType?.includesCustoms ?? false;
|
||||
// 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) {
|
||||
@@ -344,6 +348,8 @@ export class ContractsService {
|
||||
freightType: dto.freightType,
|
||||
paymentCurrency: 'USD',
|
||||
customsClearingEnabled: includesCustoms,
|
||||
// Decides which customs fee the probe looks up (Ethiopian-only vs full).
|
||||
serviceType,
|
||||
isHazardous: dto.isHazardous ?? false,
|
||||
isReefer: dto.isReefer ?? false,
|
||||
equipmentReturn: dto.equipmentReturn ?? null,
|
||||
|
||||
Reference in New Issue
Block a user