mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 06:35:42 +00:00
truck type detantion
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truck-detention rules can be scoped by vehicle type (TRUCK / VAN / TRAILER /
|
||||||
|
* TANKER / FLATBED / …), so different truck types carry different detention
|
||||||
|
* rates. Null = applies to any truck type.
|
||||||
|
*/
|
||||||
|
export class AddFeeRuleVehicleType2010000000000 implements MigrationInterface {
|
||||||
|
name = 'AddFeeRuleVehicleType2010000000000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE freight.warehouse_fee_rules ADD COLUMN IF NOT EXISTS vehicle_type varchar(20)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE freight.warehouse_fee_rules DROP COLUMN IF EXISTS vehicle_type`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,6 +93,11 @@ export class CreateFeeRuleDto {
|
|||||||
@Min(0)
|
@Min(0)
|
||||||
freeHours?: number;
|
freeHours?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Truck detention only: scope by vehicle type (TRUCK | VAN | TRAILER | …). Null = any.' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
vehicleType?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
enum: FEE_RULE_BASES,
|
enum: FEE_RULE_BASES,
|
||||||
description: 'Double-handling charge basis: PER_CONTAINER | PER_TON | PER_ITEM.',
|
description: 'Double-handling charge basis: PER_CONTAINER | PER_TON | PER_ITEM.',
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ export class WarehouseFeeRule extends BaseEntity {
|
|||||||
@Column({ name: 'container_type', type: 'varchar', length: 40, nullable: true })
|
@Column({ name: 'container_type', type: 'varchar', length: 40, nullable: true })
|
||||||
containerType?: string | null;
|
containerType?: string | null;
|
||||||
|
|
||||||
|
// Truck detention only: scope by vehicle type (TRUCK | VAN | TRAILER | TANKER
|
||||||
|
// | FLATBED | …). Null = any truck type.
|
||||||
|
@Column({ name: 'vehicle_type', type: 'varchar', length: 20, nullable: true })
|
||||||
|
vehicleType?: string | null;
|
||||||
|
|
||||||
@Column({ name: 'facility_id', type: 'uuid', nullable: true })
|
@Column({ name: 'facility_id', type: 'uuid', nullable: true })
|
||||||
facilityId?: string | null;
|
facilityId?: string | null;
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ interface ItemAttributes {
|
|||||||
tradeDirection: string | null;
|
tradeDirection: string | null;
|
||||||
cargoTypeCode: string | null;
|
cargoTypeCode: string | null;
|
||||||
containerTypeCode: string | null;
|
containerTypeCode: string | null;
|
||||||
|
/** Vehicle type of the truck (truck detention scoping); null otherwise. */
|
||||||
|
vehicleType: string | null;
|
||||||
inventoryQuantity: number;
|
inventoryQuantity: number;
|
||||||
bookingContainerCount: number;
|
bookingContainerCount: number;
|
||||||
/** Booking cargo total in the cargo's unit of measure: tonnes (PER_TON) or item count (PER_ITEM). */
|
/** Booking cargo total in the cargo's unit of measure: tonnes (PER_TON) or item count (PER_ITEM). */
|
||||||
@@ -52,6 +54,16 @@ export interface FeePreview {
|
|||||||
ratePerDay: number;
|
ratePerDay: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
}>;
|
}>;
|
||||||
|
/** Truck detention: per-vehicle-type breakdown — each truck-type group billed by its own matching rule. */
|
||||||
|
groups?: Array<{
|
||||||
|
vehicleType: string | null;
|
||||||
|
truckCount: number;
|
||||||
|
chargeableDays: number;
|
||||||
|
ratePerDay: number;
|
||||||
|
amount: number;
|
||||||
|
ruleId: string | null;
|
||||||
|
ruleName: string | null;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||||
@@ -194,6 +206,7 @@ export class WarehouseFeeService {
|
|||||||
if (!check(rule.tradeDirection, item.tradeDirection, { allowBoth: true })) return null;
|
if (!check(rule.tradeDirection, item.tradeDirection, { allowBoth: true })) return null;
|
||||||
if (!check(rule.cargoTypeCode, item.cargoTypeCode)) return null;
|
if (!check(rule.cargoTypeCode, item.cargoTypeCode)) return null;
|
||||||
if (!check(rule.containerType, item.containerTypeCode)) return null;
|
if (!check(rule.containerType, item.containerTypeCode)) return null;
|
||||||
|
if (!check(rule.vehicleType, item.vehicleType)) return null;
|
||||||
if (!check(rule.facilityId, item.facilityId)) return null;
|
if (!check(rule.facilityId, item.facilityId)) return null;
|
||||||
if (!check(rule.warehouseId, item.warehouseId)) return null;
|
if (!check(rule.warehouseId, item.warehouseId)) return null;
|
||||||
if (!check(rule.yardId, item.yardId)) return null;
|
if (!check(rule.yardId, item.yardId)) return null;
|
||||||
@@ -430,42 +443,102 @@ export class WarehouseFeeService {
|
|||||||
* tiers by detention day) until it is delivered/returned (or now, if open).
|
* tiers by detention day) until it is delivered/returned (or now, if open).
|
||||||
*/
|
*/
|
||||||
async previewTruckDetention(lastMileId: string, billingCurrency = 'USD'): Promise<FeePreview> {
|
async previewTruckDetention(lastMileId: string, billingCurrency = 'USD'): Promise<FeePreview> {
|
||||||
const [row] = await this.dataSource.query(
|
const [leg] = await this.dataSource.query(
|
||||||
`SELECT lm.arrived_at AS "arrivedAt",
|
`SELECT lm.arrived_at AS "arrivedAt",
|
||||||
lm.delivered_at AS "deliveredAt",
|
lm.delivered_at AS "deliveredAt",
|
||||||
b.freight_type AS "freightType",
|
b.freight_type AS "freightType",
|
||||||
b.trade_direction AS "tradeDirection",
|
b.trade_direction AS "tradeDirection"
|
||||||
(SELECT count(*) FROM freight.last_mile_vehicle_assignments va
|
|
||||||
WHERE va.last_mile_id = lm.id AND va.deleted_at IS NULL) AS "truckCount"
|
|
||||||
FROM freight.last_mile lm
|
FROM freight.last_mile lm
|
||||||
LEFT JOIN freight.bookings b ON b.id = lm.booking_id
|
LEFT JOIN freight.bookings b ON b.id = lm.booking_id
|
||||||
WHERE lm.id = $1 AND lm.deleted_at IS NULL`,
|
WHERE lm.id = $1 AND lm.deleted_at IS NULL`,
|
||||||
[lastMileId],
|
[lastMileId],
|
||||||
);
|
);
|
||||||
if (!row) throw new NotFoundException(`Last-mile record ${lastMileId} not found`);
|
if (!leg) throw new NotFoundException(`Last-mile record ${lastMileId} not found`);
|
||||||
|
|
||||||
|
// Group the leg's vehicles by type so each truck type is billed by its own
|
||||||
|
// matching rule (rates differ by truck type). Falls back to one untyped group.
|
||||||
|
const groupRows: Array<{ vehicleType: string | null; truckCount: number | string }> =
|
||||||
|
await this.dataSource.query(
|
||||||
|
`SELECT v.vehicle_type AS "vehicleType", count(*)::int AS "truckCount"
|
||||||
|
FROM freight.last_mile_vehicle_assignments va
|
||||||
|
JOIN freight.vehicles v ON v.id = va.vehicle_id AND v.deleted_at IS NULL
|
||||||
|
WHERE va.last_mile_id = $1 AND va.deleted_at IS NULL
|
||||||
|
GROUP BY v.vehicle_type`,
|
||||||
|
[lastMileId],
|
||||||
|
);
|
||||||
|
const groups = groupRows.length ? groupRows : [{ vehicleType: null, truckCount: 1 }];
|
||||||
|
|
||||||
const item: ItemAttributes = {
|
|
||||||
arrivedAt: null,
|
|
||||||
gateClearedAt: null,
|
|
||||||
releaseDate: null,
|
|
||||||
freightType: row.freightType ?? null,
|
|
||||||
tradeDirection: row.tradeDirection ?? null,
|
|
||||||
cargoTypeCode: null,
|
|
||||||
containerTypeCode: null,
|
|
||||||
inventoryQuantity: 1,
|
|
||||||
bookingContainerCount: 1,
|
|
||||||
cargoQuantity: 0,
|
|
||||||
facilityId: null,
|
|
||||||
warehouseId: null,
|
|
||||||
yardId: null,
|
|
||||||
zoneId: null,
|
|
||||||
};
|
|
||||||
const rules = await this.feeRuleRepository.findAll({ where: { isActive: true } });
|
const rules = await this.feeRuleRepository.findAll({ where: { isActive: true } });
|
||||||
const rule = this.bestRule(
|
const detentionRules = rules.filter((r) => r.ruleType === 'TRUCK_DETENTION_FEE');
|
||||||
rules.filter((r) => r.ruleType === 'TRUCK_DETENTION_FEE'),
|
const now = new Date();
|
||||||
item,
|
const targetCurrency = this.normalizeCurrency(billingCurrency);
|
||||||
|
|
||||||
|
const computed = await Promise.all(
|
||||||
|
groups.map(async (g) => {
|
||||||
|
const item: ItemAttributes = {
|
||||||
|
arrivedAt: null,
|
||||||
|
gateClearedAt: null,
|
||||||
|
releaseDate: null,
|
||||||
|
freightType: leg.freightType ?? null,
|
||||||
|
tradeDirection: leg.tradeDirection ?? null,
|
||||||
|
cargoTypeCode: null,
|
||||||
|
containerTypeCode: null,
|
||||||
|
vehicleType: g.vehicleType ?? null,
|
||||||
|
inventoryQuantity: 1,
|
||||||
|
bookingContainerCount: 1,
|
||||||
|
cargoQuantity: 0,
|
||||||
|
facilityId: null,
|
||||||
|
warehouseId: null,
|
||||||
|
yardId: null,
|
||||||
|
zoneId: null,
|
||||||
|
};
|
||||||
|
const rule = this.bestRule(detentionRules, item);
|
||||||
|
const c = await this.computeTruckDetention(
|
||||||
|
rule,
|
||||||
|
{ arrivedAt: leg.arrivedAt, deliveredAt: leg.deliveredAt, truckCount: g.truckCount },
|
||||||
|
now,
|
||||||
|
billingCurrency,
|
||||||
|
);
|
||||||
|
return { vehicleType: g.vehicleType ?? null, truckCount: Math.max(1, Math.round(Number(g.truckCount) || 1)), c };
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
return this.computeTruckDetention(rule, row, new Date(), billingCurrency);
|
|
||||||
|
const totalAmount = Math.round(computed.reduce((s, x) => s + x.c.amount, 0) * 100) / 100;
|
||||||
|
const totalTrucks = computed.reduce((s, x) => s + x.truckCount, 0);
|
||||||
|
const totalBillable = computed.reduce((s, x) => s + x.c.billableUnits, 0);
|
||||||
|
const chargeableDays = computed[0]?.c.chargeableDays ?? 0;
|
||||||
|
const single = computed.length === 1 ? computed[0].c : null;
|
||||||
|
const anyRuleName = computed.find((x) => x.c.ruleId)?.c.ruleName ?? null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
ruleType: 'TRUCK_DETENTION_FEE',
|
||||||
|
basis: null,
|
||||||
|
ruleId: single?.ruleId ?? null,
|
||||||
|
ruleName: single ? single.ruleName : computed.length > 1 && anyRuleName ? 'Per truck-type rules' : anyRuleName,
|
||||||
|
freeDays: 0,
|
||||||
|
ratePerDay: single?.ratePerDay ?? 0,
|
||||||
|
currency: targetCurrency,
|
||||||
|
ruleCurrency: single?.ruleCurrency ?? null,
|
||||||
|
billingCurrency: targetCurrency,
|
||||||
|
startDate: leg.arrivedAt ? new Date(leg.arrivedAt).toISOString() : null,
|
||||||
|
endDate: (leg.deliveredAt ? new Date(leg.deliveredAt) : now).toISOString(),
|
||||||
|
endIsOpen: !leg.deliveredAt,
|
||||||
|
elapsedDays: chargeableDays,
|
||||||
|
chargeableDays,
|
||||||
|
containerCount: totalTrucks,
|
||||||
|
billableUnits: totalBillable,
|
||||||
|
amount: totalAmount,
|
||||||
|
tiers: single ? single.tiers : [],
|
||||||
|
groups: computed.map((x) => ({
|
||||||
|
vehicleType: x.vehicleType,
|
||||||
|
truckCount: x.truckCount,
|
||||||
|
chargeableDays: x.c.chargeableDays,
|
||||||
|
ratePerDay: x.c.ratePerDay,
|
||||||
|
amount: x.c.amount,
|
||||||
|
ruleId: x.c.ruleId,
|
||||||
|
ruleName: x.c.ruleName,
|
||||||
|
})),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async computeTruckDetention(
|
private async computeTruckDetention(
|
||||||
|
|||||||
@@ -316,19 +316,44 @@ export class WarehouseInvoiceService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const truckCount = preview.containerCount; // reused as the per-truck count
|
// One line per truck-type group (each billed by its own matching rule). Groups
|
||||||
const line: InvoiceLineInput = {
|
// with no matching rule bill 0 and are dropped. Falls back to a single line.
|
||||||
chargeType: "TRUCK_DETENTION",
|
const groups = preview.groups && preview.groups.length ? preview.groups : null;
|
||||||
description: `Truck detention - ${preview.chargeableDays} day(s) x ${truckCount} truck(s)${preview.tiers.length ? " using tiered tariff" : ""}`,
|
const lines: InvoiceLineInput[] = groups
|
||||||
quantity: preview.billableUnits,
|
? groups
|
||||||
unitRate: preview.ratePerDay,
|
.filter((g) => g.amount > 0)
|
||||||
amount: preview.amount,
|
.map((g) => ({
|
||||||
currency: preview.currency,
|
chargeType: "TRUCK_DETENTION",
|
||||||
metadata: {
|
description: `Truck detention${g.vehicleType ? ` (${g.vehicleType})` : ""} - ${g.chargeableDays} day(s) x ${g.truckCount} truck(s)`,
|
||||||
feeRuleId: preview.ruleId ?? null,
|
quantity: g.truckCount * g.chargeableDays,
|
||||||
chargeableDays: preview.chargeableDays ?? null,
|
unitRate: g.ratePerDay,
|
||||||
},
|
amount: g.amount,
|
||||||
};
|
currency: preview.currency,
|
||||||
|
metadata: {
|
||||||
|
feeRuleId: g.ruleId ?? null,
|
||||||
|
chargeableDays: g.chargeableDays,
|
||||||
|
vehicleType: g.vehicleType ?? null,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
chargeType: "TRUCK_DETENTION",
|
||||||
|
description: `Truck detention - ${preview.chargeableDays} day(s) x ${preview.containerCount} truck(s)`,
|
||||||
|
quantity: preview.billableUnits,
|
||||||
|
unitRate: preview.ratePerDay,
|
||||||
|
amount: preview.amount,
|
||||||
|
currency: preview.currency,
|
||||||
|
metadata: {
|
||||||
|
feeRuleId: preview.ruleId ?? null,
|
||||||
|
chargeableDays: preview.chargeableDays ?? null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (lines.length === 0) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"No truck detention is currently payable for this last-mile leg.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return this.billing.generateInvoice({
|
return this.billing.generateInvoice({
|
||||||
source: "last_mile" as Freight.InvoiceSource,
|
source: "last_mile" as Freight.InvoiceSource,
|
||||||
@@ -337,7 +362,7 @@ export class WarehouseInvoiceService {
|
|||||||
companyId: lm.companyId,
|
companyId: lm.companyId,
|
||||||
companyProfileId: lm.companyProfileId || "",
|
companyProfileId: lm.companyProfileId || "",
|
||||||
currency: billingCurrency,
|
currency: billingCurrency,
|
||||||
lines: [line],
|
lines,
|
||||||
status: Freight.InvoiceStatus.Issued,
|
status: Freight.InvoiceStatus.Issued,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,37 @@ export function TruckDetentionModal({ opened, onClose, record }: TruckDetentionM
|
|||||||
Still accruing — no delivery/return time yet. The amount grows until the vehicle is returned.
|
Still accruing — no delivery/return time yet. The amount grows until the vehicle is returned.
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{preview.tiers && preview.tiers.length > 0 ? (
|
{preview.groups && preview.groups.length > 1 ? (
|
||||||
|
<Table withTableBorder withColumnBorders verticalSpacing="xs" fz="xs">
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>Truck type</Table.Th>
|
||||||
|
<Table.Th>Trucks</Table.Th>
|
||||||
|
<Table.Th>Days</Table.Th>
|
||||||
|
<Table.Th ta="right">Rate / truck / day</Table.Th>
|
||||||
|
<Table.Th ta="right">Amount</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{preview.groups.map((g, i) => (
|
||||||
|
<Table.Tr key={i}>
|
||||||
|
<Table.Td>
|
||||||
|
{g.vehicleType ?? 'Unknown'}
|
||||||
|
{!g.ruleId && (
|
||||||
|
<Text span size="xs" c="red">
|
||||||
|
{' '}· no rule
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>{g.truckCount}</Table.Td>
|
||||||
|
<Table.Td>{g.chargeableDays}</Table.Td>
|
||||||
|
<Table.Td ta="right">{money(g.ratePerDay, preview.currency)}</Table.Td>
|
||||||
|
<Table.Td ta="right">{money(g.amount, preview.currency)}</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
) : preview.tiers && preview.tiers.length > 0 ? (
|
||||||
<Table withTableBorder withColumnBorders verticalSpacing="xs" fz="xs">
|
<Table withTableBorder withColumnBorders verticalSpacing="xs" fz="xs">
|
||||||
<Table.Thead>
|
<Table.Thead>
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
FEE_RULE_BASIS_LABELS,
|
FEE_RULE_BASIS_LABELS,
|
||||||
FEE_RULE_TYPES,
|
FEE_RULE_TYPES,
|
||||||
FEE_RULE_TYPE_LABELS,
|
FEE_RULE_TYPE_LABELS,
|
||||||
|
VEHICLE_TYPES,
|
||||||
type FeeRuleBasis,
|
type FeeRuleBasis,
|
||||||
type FeeRuleType,
|
type FeeRuleType,
|
||||||
} from '@/types/warehouse';
|
} from '@/types/warehouse';
|
||||||
@@ -372,6 +373,7 @@ function FeeRules() {
|
|||||||
tradeDirection: '',
|
tradeDirection: '',
|
||||||
cargoTypeCode: '',
|
cargoTypeCode: '',
|
||||||
containerType: '',
|
containerType: '',
|
||||||
|
vehicleType: '',
|
||||||
freeDays: 3,
|
freeDays: 3,
|
||||||
freeHours: 3,
|
freeHours: 3,
|
||||||
ratePerDay: 0,
|
ratePerDay: 0,
|
||||||
@@ -399,6 +401,7 @@ function FeeRules() {
|
|||||||
tradeDirection: '',
|
tradeDirection: '',
|
||||||
cargoTypeCode: '',
|
cargoTypeCode: '',
|
||||||
containerType: '',
|
containerType: '',
|
||||||
|
vehicleType: '',
|
||||||
freeDays: 3,
|
freeDays: 3,
|
||||||
freeHours: 3,
|
freeHours: 3,
|
||||||
ratePerDay: 0,
|
ratePerDay: 0,
|
||||||
@@ -469,7 +472,7 @@ function FeeRules() {
|
|||||||
ratePerDay: form.ratePerDay,
|
ratePerDay: form.ratePerDay,
|
||||||
currency: form.currency || 'USD',
|
currency: form.currency || 'USD',
|
||||||
...(isDoubleHandling ? { basis: form.basis } : {}),
|
...(isDoubleHandling ? { basis: form.basis } : {}),
|
||||||
...(isTruckDetention ? { freeHours: form.freeHours } : {}),
|
...(isTruckDetention ? { freeHours: form.freeHours, vehicleType: clean(form.vehicleType) ?? null } : {}),
|
||||||
...(!isDoubleHandling && tiers.length ? { tiers } : {}),
|
...(!isDoubleHandling && tiers.length ? { tiers } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -638,6 +641,16 @@ function FeeRules() {
|
|||||||
onChange={(value) => setForm((f) => ({ ...f, tradeDirection: selectValue(value) }))}
|
onChange={(value) => setForm((f) => ({ ...f, tradeDirection: selectValue(value) }))}
|
||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
|
{isTruckDetention && (
|
||||||
|
<Select
|
||||||
|
label="Truck type"
|
||||||
|
placeholder="Any truck type"
|
||||||
|
data={VEHICLE_TYPES.map((v) => ({ value: v, label: v.charAt(0) + v.slice(1).toLowerCase() }))}
|
||||||
|
value={form.vehicleType || null}
|
||||||
|
onChange={(value) => setForm((f) => ({ ...f, vehicleType: selectValue(value) }))}
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{isBulkRule && (
|
{isBulkRule && (
|
||||||
<Select
|
<Select
|
||||||
label="Cargo type"
|
label="Cargo type"
|
||||||
|
|||||||
@@ -763,6 +763,10 @@ export const FEE_RULE_BASIS_LABELS: Record<FeeRuleBasis, string> = {
|
|||||||
PER_ITEM: 'Per Item',
|
PER_ITEM: 'Per Item',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Vehicle types a Truck Detention rule can be scoped to (rates differ by truck type). */
|
||||||
|
export const VEHICLE_TYPES = ['TRUCK', 'VAN', 'CAR', 'BUS', 'TRAILER', 'TANKER', 'FLATBED'] as const;
|
||||||
|
export type VehicleTypeCode = (typeof VEHICLE_TYPES)[number];
|
||||||
|
|
||||||
export interface FeeRule {
|
export interface FeeRule {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -774,6 +778,8 @@ export interface FeeRule {
|
|||||||
tradeDirection?: string | null;
|
tradeDirection?: string | null;
|
||||||
cargoTypeCode?: string | null;
|
cargoTypeCode?: string | null;
|
||||||
containerType?: string | null;
|
containerType?: string | null;
|
||||||
|
/** Truck detention only: scope by vehicle type (null = any). */
|
||||||
|
vehicleType?: string | null;
|
||||||
facilityId?: string | null;
|
facilityId?: string | null;
|
||||||
warehouseId?: string | null;
|
warehouseId?: string | null;
|
||||||
yardId?: string | null;
|
yardId?: string | null;
|
||||||
@@ -820,6 +826,16 @@ export interface FeePreview {
|
|||||||
billableUnits: number;
|
billableUnits: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
tiers?: FeePreviewTier[];
|
tiers?: FeePreviewTier[];
|
||||||
|
/** Truck detention: per-vehicle-type breakdown. */
|
||||||
|
groups?: Array<{
|
||||||
|
vehicleType: string | null;
|
||||||
|
truckCount: number;
|
||||||
|
chargeableDays: number;
|
||||||
|
ratePerDay: number;
|
||||||
|
amount: number;
|
||||||
|
ruleId: string | null;
|
||||||
|
ruleName: string | null;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AllocationPreviewResult {
|
export interface AllocationPreviewResult {
|
||||||
|
|||||||
Reference in New Issue
Block a user