mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(rates): multi-tier distance band entry in last-mile rate form
- tierList field type in rule-engine form dialog (add/remove rows, overlap + open-ended validation, From km auto-continues) - create submits one rate row per tier sequentially - editing a band row keeps the single From/To/value form
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Last-mile contract fields on freight.last_mile_requests: the customer-chosen
|
||||
* delivery date, the chief-approved advance amount (held until the invoice is
|
||||
* generated at signing time), the rate snapshot rendered into the contract,
|
||||
* and the customer signature bookkeeping. The signed PDF and signature image
|
||||
* live in freight.files (resource 'last_mile_requests').
|
||||
*/
|
||||
export class LastMileRequestContract3290000000000 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.last_mile_requests
|
||||
ADD COLUMN IF NOT EXISTS requested_delivery_date date,
|
||||
ADD COLUMN IF NOT EXISTS approved_advance_amount numeric(14,2),
|
||||
ADD COLUMN IF NOT EXISTS contract_summary jsonb,
|
||||
ADD COLUMN IF NOT EXISTS contract_generated_at timestamptz,
|
||||
ADD COLUMN IF NOT EXISTS customer_signed_at timestamptz,
|
||||
ADD COLUMN IF NOT EXISTS signer_display_name varchar(160),
|
||||
ADD COLUMN IF NOT EXISTS consent_text text
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.last_mile_requests
|
||||
DROP COLUMN IF EXISTS requested_delivery_date,
|
||||
DROP COLUMN IF EXISTS approved_advance_amount,
|
||||
DROP COLUMN IF EXISTS contract_summary,
|
||||
DROP COLUMN IF EXISTS contract_generated_at,
|
||||
DROP COLUMN IF EXISTS customer_signed_at,
|
||||
DROP COLUMN IF EXISTS signer_display_name,
|
||||
DROP COLUMN IF EXISTS consent_text
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ArrayNotEmpty, ArrayUnique, IsArray, IsString } from 'class-validator';
|
||||
import { ArrayNotEmpty, ArrayUnique, IsArray, IsDateString, IsString } from 'class-validator';
|
||||
|
||||
export class SubmitLastMileRequestDto {
|
||||
@ApiProperty({
|
||||
@@ -12,4 +12,12 @@ export class SubmitLastMileRequestDto {
|
||||
@ArrayUnique()
|
||||
@IsString({ each: true })
|
||||
containerNumbers!: string[];
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Requested last-mile delivery date (ISO date), chosen by the customer against the train departure from Djibouti.',
|
||||
example: '2026-08-15',
|
||||
})
|
||||
@IsDateString()
|
||||
deliveryDate!: string;
|
||||
}
|
||||
|
||||
@@ -65,6 +65,47 @@ export class LastMileRequest extends BaseEntity {
|
||||
@Column({ name: 'resulting_last_mile_id', type: 'uuid', nullable: true })
|
||||
resultingLastMileId?: string | null;
|
||||
|
||||
/** Customer-chosen last-mile delivery date (guided by the train's Djibouti departure). */
|
||||
@Column({ name: 'requested_delivery_date', type: 'date', nullable: true })
|
||||
requestedDeliveryDate?: string | null;
|
||||
|
||||
/** Chief-approved advance — invoiced only after the customer signs the LM contract. */
|
||||
@Column({
|
||||
name: 'approved_advance_amount',
|
||||
type: 'numeric',
|
||||
precision: 14,
|
||||
scale: 2,
|
||||
nullable: true,
|
||||
transformer: {
|
||||
to: (v?: number | null) => v,
|
||||
from: (v?: string | null) => (v == null ? null : Number(v)),
|
||||
},
|
||||
})
|
||||
approvedAdvanceAmount?: number | null;
|
||||
|
||||
/** Rate snapshot taken at approval, rendered into the contract document. */
|
||||
@Column({ name: 'contract_summary', type: 'jsonb', nullable: true })
|
||||
contractSummary?: {
|
||||
estimatedKm: number | null;
|
||||
mode: string | null;
|
||||
currency: string | null;
|
||||
total: number | null;
|
||||
lines: Array<{ description: string; amount: number }>;
|
||||
advanceAmount: number;
|
||||
} | null;
|
||||
|
||||
@Column({ name: 'contract_generated_at', type: 'timestamptz', nullable: true })
|
||||
contractGeneratedAt?: Date | null;
|
||||
|
||||
@Column({ name: 'customer_signed_at', type: 'timestamptz', nullable: true })
|
||||
customerSignedAt?: Date | null;
|
||||
|
||||
@Column({ name: 'signer_display_name', type: 'varchar', length: 160, nullable: true })
|
||||
signerDisplayName?: string | null;
|
||||
|
||||
@Column({ name: 'consent_text', type: 'text', nullable: true })
|
||||
consentText?: string | null;
|
||||
|
||||
@ManyToOne(() => LastMile, { nullable: true, eager: false })
|
||||
@JoinColumn({ name: 'resulting_last_mile_id' })
|
||||
resultingLastMile?: LastMile | null;
|
||||
|
||||
@@ -252,7 +252,12 @@ export class LastMileRequestsService {
|
||||
});
|
||||
}
|
||||
|
||||
async submit(id: string, userId: string | null, containerNumbers: string[]): Promise<LastMileRequest> {
|
||||
async submit(
|
||||
id: string,
|
||||
userId: string | null,
|
||||
containerNumbers: string[],
|
||||
deliveryDate: string,
|
||||
): Promise<LastMileRequest> {
|
||||
const request = await this.findById(id);
|
||||
if (request.status !== LastMileRequestStatus.AwaitingConfirmation) {
|
||||
throw new BadRequestException(`Request is already ${request.status.toLowerCase()}`);
|
||||
@@ -274,6 +279,7 @@ export class LastMileRequestsService {
|
||||
|
||||
await this.requestsRepository.update(id, {
|
||||
requestedContainerNumbers: selected,
|
||||
requestedDeliveryDate: deliveryDate,
|
||||
status: LastMileRequestStatus.Submitted,
|
||||
submittedByUserId: userId,
|
||||
submittedAt: new Date(),
|
||||
|
||||
Reference in New Issue
Block a user