mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
- Introduced StampUpload component for uploading company stamp images. - Integrated stamp upload in contract signing modal, supporting PNG and JPG formats. - Implemented validation for file type and size (max 5 MB). - Added visual feedback for drag-and-drop functionality. - Updated contract-related pages to handle duplicate contract alerts and pricing notices. - Enhanced contract expiry management with a nightly sweep service. - Added unit tests for new features and updated existing tests for contract handling.
171 lines
5.5 KiB
TypeScript
171 lines
5.5 KiB
TypeScript
import { BaseEntity } from '@edr/api-common';
|
|
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
|
|
import { CargoType } from './cargo-type.entity';
|
|
import { ContainerType } from './container-type.entity';
|
|
import { Yard } from './yard.entity';
|
|
|
|
export const RATE_TYPES = [
|
|
'CONTAINER_IMPORT',
|
|
'CONTAINER_EXPORT',
|
|
'BULK_IMPORT',
|
|
'BULK_EXPORT',
|
|
'INTERCITY_BULK',
|
|
'INTERCITY_CONTAINER',
|
|
'FIRST_MILE',
|
|
'LAST_MILE',
|
|
'DEMURRAGE',
|
|
'LASHING',
|
|
'DOUBLE_HANDLING',
|
|
'CONTAINER_WITH_RETURN',
|
|
'CANCELLATION_FEE',
|
|
'OVERWEIGHT_PER_TON',
|
|
'HAZARD_SURCHARGE',
|
|
'REEFER_SURCHARGE',
|
|
'RETURN_SURCHARGE',
|
|
'PIL_EXTRA_FEE',
|
|
'CUSTOMS_CLEARANCE',
|
|
] as const;
|
|
|
|
export type RateType = typeof RATE_TYPES[number];
|
|
|
|
export const RATE_STATUSES = ['DRAFT', 'PENDING_APPROVAL', 'LIVE', 'SUPERSEDED'] as const;
|
|
export type RateStatus = typeof RATE_STATUSES[number];
|
|
|
|
export const RATE_UNITS = [
|
|
'PER_WAGON',
|
|
'PER_TON',
|
|
// Break-bulk commodities are counted, not weighed (cargo_types.unit_of_measure
|
|
// = PER_ITEM) — their rates bill per item off the same booking quantity field.
|
|
'PER_ITEM',
|
|
'PER_CONTAINER',
|
|
'PER_KM',
|
|
'PER_INVOICE',
|
|
'FLAT',
|
|
] as const;
|
|
export type RateUnit = typeof RATE_UNITS[number];
|
|
|
|
/**
|
|
* Friendly, admin-facing category that determines how the rate is used in
|
|
* pricing and which fields the rate form shows. Replaces the cryptic
|
|
* `rateType` matrix for the configuration UI (rateType is still persisted and
|
|
* derived from `appliesTo` + `tradeDirection` + `trigger` for base-freight
|
|
* lookup and snapshots).
|
|
*
|
|
* - BULK / CONTAINER / INTERCITY : base rail freight (trigger = ALWAYS)
|
|
* - FIRST_MILE / LAST_MILE : pickup / delivery legs
|
|
* - OTHER : trigger-based surcharges (hazard, reefer …)
|
|
*/
|
|
export const RATE_APPLIES_TO = [
|
|
'BULK',
|
|
'CONTAINER',
|
|
'INTERCITY',
|
|
'FIRST_MILE',
|
|
'LAST_MILE',
|
|
'OTHER',
|
|
] as const;
|
|
export type RateAppliesTo = typeof RATE_APPLIES_TO[number];
|
|
|
|
/**
|
|
* What makes a rate apply to a booking. `ALWAYS` is base freight (matched by
|
|
* direction + container/bulk scope). Everything else is a surcharge that the
|
|
* rule engine adds on top, additively, when the booking matches the trigger —
|
|
* so hazard stacks on container/bulk with each line's own unit.
|
|
*/
|
|
export const RATE_TRIGGERS = [
|
|
'ALWAYS',
|
|
'HAZARDOUS',
|
|
'OVERWEIGHT',
|
|
'REEFER',
|
|
// Empty-container return service (container freight only) — fires when the
|
|
// booking ships WITH_RETURN, billed like hazard/reefer (usually PER_CONTAINER).
|
|
'WITH_RETURN',
|
|
'SHIPPING_LINE',
|
|
'CONSOLIDATION',
|
|
// Cargo securing / lashing. Fires when the booking's cargo type has
|
|
// hasLashing = true. Flat fee, billed once per booking.
|
|
'LASHING',
|
|
'CANCELLATION',
|
|
'DEMURRAGE',
|
|
'PIL_EXTRA_FEE',
|
|
// Customs clearance service fee — billed up front via a clearance invoice,
|
|
// never auto-applied to booking pricing (matchesTrigger returns false).
|
|
'CUSTOMS_CLEARANCE',
|
|
] as const;
|
|
export type RateTrigger = typeof RATE_TRIGGERS[number];
|
|
|
|
@Entity({ schema: 'freight', name: 'rates' })
|
|
@Index(['rateType'])
|
|
@Index(['status'])
|
|
@Index(['containerTypeId'])
|
|
@Index(['trigger'])
|
|
@Index(['originYardId'])
|
|
@Index(['destinationYardId'])
|
|
export class Rate extends BaseEntity {
|
|
@Column({ name: 'rate_type', type: 'varchar', length: 50 })
|
|
rateType!: RateType;
|
|
|
|
@Column({ name: 'applies_to', type: 'varchar', length: 20, default: 'OTHER' })
|
|
appliesTo!: RateAppliesTo;
|
|
|
|
@Column({ name: 'trigger', type: 'varchar', length: 20, default: 'ALWAYS' })
|
|
trigger!: RateTrigger;
|
|
|
|
@Column({ name: 'container_type_id', type: 'uuid', nullable: true })
|
|
containerTypeId?: string | null;
|
|
|
|
@ManyToOne(() => ContainerType, { nullable: true, eager: false })
|
|
@JoinColumn({ name: 'container_type_id' })
|
|
containerType?: ContainerType | null;
|
|
|
|
@Column({ name: 'cargo_type_id', type: 'uuid', nullable: true })
|
|
cargoTypeId?: string | null;
|
|
|
|
@ManyToOne(() => CargoType, { nullable: true, eager: false })
|
|
@JoinColumn({ name: 'cargo_type_id' })
|
|
cargoType?: CargoType | null;
|
|
|
|
@Column({ name: 'trade_direction', type: 'varchar', length: 10, nullable: true })
|
|
tradeDirection?: string | null;
|
|
|
|
/**
|
|
* The leg this rate prices. Base freight (trigger = ALWAYS) is quoted per
|
|
* route — "container import, Djibouti → Dire Dawa" — so both yards are
|
|
* required for BULK/CONTAINER/INTERCITY and NULL for everything else. The
|
|
* `CK_rates_yard_scope` DB constraint enforces both halves of that.
|
|
*/
|
|
@Column({ name: 'origin_yard_id', type: 'uuid', nullable: true })
|
|
originYardId?: string | null;
|
|
|
|
@ManyToOne(() => Yard, { nullable: true, eager: false })
|
|
@JoinColumn({ name: 'origin_yard_id' })
|
|
originYard?: Yard | null;
|
|
|
|
@Column({ name: 'destination_yard_id', type: 'uuid', nullable: true })
|
|
destinationYardId?: string | null;
|
|
|
|
@ManyToOne(() => Yard, { nullable: true, eager: false })
|
|
@JoinColumn({ name: 'destination_yard_id' })
|
|
destinationYard?: Yard | null;
|
|
|
|
@Column({ name: 'currency', type: 'varchar', length: 5 })
|
|
currency!: string;
|
|
|
|
@Column({ name: 'rate_value', type: 'numeric', precision: 14, scale: 4 })
|
|
rateValue!: number;
|
|
|
|
@Column({ name: 'rate_unit', type: 'varchar', length: 30 })
|
|
rateUnit!: RateUnit;
|
|
|
|
@Column({ name: 'status', type: 'varchar', length: 20, default: 'DRAFT' })
|
|
status!: RateStatus;
|
|
|
|
@Column({ name: 'proposed_by_staff_id', type: 'uuid' })
|
|
proposedByStaffId!: string;
|
|
|
|
@Column({ name: 'approved_by_ceo_id', type: 'uuid', nullable: true })
|
|
approvedByCeoId?: string | null;
|
|
|
|
@Column({ name: 'approved_at', type: 'timestamptz', nullable: true })
|
|
approvedAt?: Date | null;
|
|
}
|