mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
fix
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
/**
|
||||
* Add paid column to first_mile and last_mile tables to track invoice payment status.
|
||||
*/
|
||||
export class AddPaidToFirstAndLastMile1860000000000
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = "AddPaidToFirstAndLastMile1860000000000";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.first_mile
|
||||
ADD COLUMN IF NOT EXISTS paid boolean NOT NULL DEFAULT false;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.last_mile
|
||||
ADD COLUMN IF NOT EXISTS paid boolean NOT NULL DEFAULT false;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.first_mile
|
||||
DROP COLUMN IF EXISTS paid;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.last_mile
|
||||
DROP COLUMN IF EXISTS paid;
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsIn, IsNumber, IsOptional, IsUUID, Min } from 'class-validator';
|
||||
import { IsBoolean, IsIn, IsNumber, IsOptional, IsUUID, Min } from 'class-validator';
|
||||
|
||||
import { FIRST_MILE_STATUSES, FirstMileStatus } from '../entities/first-mile.entity';
|
||||
|
||||
@@ -58,4 +58,9 @@ export class CreateFirstMileDto {
|
||||
@Transform(({ value }) => (value === '' ? undefined : value))
|
||||
@IsUUID()
|
||||
vehicleId?: string | null;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Invoice payment status', default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
paid?: boolean;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ export class FirstMile extends BaseEntity {
|
||||
@Column({ name: 'remaining_payment', type: 'numeric', precision: 14, scale: 2, default: 0 })
|
||||
remainingPayment!: number;
|
||||
|
||||
@Column({ name: 'paid', type: 'boolean', default: false })
|
||||
paid!: boolean;
|
||||
|
||||
// TODO: uncomment after migration creates column
|
||||
// @Column({ type: 'boolean', default: false })
|
||||
// isPostPaymentCompleted!: boolean;
|
||||
|
||||
@@ -12,6 +12,8 @@ import { UpdateFirstMileDto } from './dto/update-first-mile.dto';
|
||||
import { FirstMile, FirstMileStatus } from './entities/first-mile.entity';
|
||||
import { FirstMileContainerAllocation } from './entities/first-mile-container-allocation.entity';
|
||||
import { FirstMileRepository } from './first-mile.repository';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { InvoiceEventPayload } from '../billing/billing.service';
|
||||
|
||||
type FirstMileListFilter = {
|
||||
status?: FirstMileStatus;
|
||||
@@ -146,6 +148,18 @@ export class FirstMileService {
|
||||
};
|
||||
}
|
||||
|
||||
@OnEvent("firstmile.invoice.paid")
|
||||
async onBookingInvoicePaid(payload: InvoiceEventPayload): Promise<void> {
|
||||
try {
|
||||
await this.firstMileRepository.update(payload.sourceId, { paid: true } as any);
|
||||
this.logger.log(`Marked first-mile record ${payload.sourceId} as paid (invoice ${payload.invoiceId})`);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Failed to update first-mile payment status for record ${payload.sourceId}: ${String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<FirstMile> {
|
||||
const record = await this.firstMileRepository.findById(id, {
|
||||
relations: {
|
||||
@@ -175,6 +189,7 @@ export class FirstMileService {
|
||||
estimatedKm: dto.estimatedKm ?? null,
|
||||
exactKm: dto.exactKm ?? null,
|
||||
vehicleId: dto.vehicleId ?? null,
|
||||
paid: (dto as any).paid ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -207,6 +222,7 @@ export class FirstMileService {
|
||||
async update(id: string, dto: UpdateFirstMileDto): Promise<FirstMile> {
|
||||
const existing = await this.findById(id);
|
||||
|
||||
const dtoAny = dto as any;
|
||||
const updated = await this.firstMileRepository.update(id, {
|
||||
...(dto.bookingId !== undefined ? { bookingId: dto.bookingId } : {}),
|
||||
...(dto.status !== undefined ? { status: dto.status } : {}),
|
||||
@@ -215,7 +231,8 @@ export class FirstMileService {
|
||||
...(dto.estimatedKm !== undefined ? { estimatedKm: dto.estimatedKm } : {}),
|
||||
...(dto.exactKm !== undefined ? { exactKm: dto.exactKm } : {}),
|
||||
...(dto.vehicleId !== undefined ? { vehicleId: dto.vehicleId } : {}),
|
||||
});
|
||||
...(dtoAny.paid !== undefined ? { paid: dtoAny.paid } : {}),
|
||||
} as any);
|
||||
|
||||
if (!updated) {
|
||||
throw new NotFoundException(`First-mile record ${id} not found`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsIn, IsNumber, IsOptional, IsUUID, Min } from 'class-validator';
|
||||
import { IsBoolean, IsIn, IsNumber, IsOptional, IsUUID, Min } from 'class-validator';
|
||||
|
||||
import { LAST_MILE_STATUSES, LastMileStatus } from '../entities/last-mile.entity';
|
||||
|
||||
@@ -58,4 +58,9 @@ export class CreateLastMileDto {
|
||||
@Transform(({ value }) => (value === '' ? undefined : value))
|
||||
@IsUUID()
|
||||
vehicleId?: string | null;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Invoice payment status', default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
paid?: boolean;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ export class LastMile extends BaseEntity {
|
||||
@Column({ name: 'remaining_payment', type: 'numeric', precision: 14, scale: 2, default: 0 })
|
||||
remainingPayment!: number;
|
||||
|
||||
@Column({ name: 'paid', type: 'boolean', default: false })
|
||||
paid!: boolean;
|
||||
|
||||
// TODO: uncomment after migration creates column
|
||||
// @Column({ type: 'boolean', default: false })
|
||||
// isPostPaymentCompleted!: boolean;
|
||||
|
||||
@@ -88,17 +88,17 @@ export class LastMileController {
|
||||
const booking = await this.bookingsService.findById(record.bookingId);
|
||||
if (dto.exactKm !== undefined || dto.exactKm != record.exactKm || dto.remainingPayment !== undefined || dto.remainingPayment !== record.remainingPayment) {
|
||||
await this.billingService.generateInvoice({
|
||||
source: Freight.InvoiceSource.FirstMile,
|
||||
source: Freight.InvoiceSource.LastMile,
|
||||
sourceId: record.id,
|
||||
type: "FIRST_MILE",
|
||||
type: "LAST_MILE",
|
||||
companyId: booking.companyId,
|
||||
companyProfileId: booking.companyProfileId,
|
||||
currency: "ETB",
|
||||
|
||||
lines: [
|
||||
{
|
||||
chargeType: "FIRST_MILE",
|
||||
description: "First Mile Transportation Service",
|
||||
chargeType: "LAST_MILE",
|
||||
description: "Last Mile Transportation Service",
|
||||
quantity: 1,
|
||||
unitRate: record.remainingPayment,
|
||||
amount: record.remainingPayment,
|
||||
|
||||
@@ -10,6 +10,8 @@ import { UpdateLastMileDto } from './dto/update-last-mile.dto';
|
||||
import { LastMile, LastMileStatus } from './entities/last-mile.entity';
|
||||
import { LastMileContainerAllocation } from './entities/last-mile-container-allocation.entity';
|
||||
import { LastMileRepository } from './last-mile.repository';
|
||||
import { InvoiceEventPayload } from '../billing/billing.service';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
type LastMileListFilter = {
|
||||
status?: LastMileStatus;
|
||||
@@ -137,12 +139,26 @@ export class LastMileService {
|
||||
estimatedKm: dto.estimatedKm ?? null,
|
||||
exactKm: dto.exactKm ?? null,
|
||||
vehicleId: dto.vehicleId ?? null,
|
||||
paid: (dto as any).paid ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent("lastmile.invoice.paid")
|
||||
async onBookingInvoicePaid(payload: InvoiceEventPayload): Promise<void> {
|
||||
try {
|
||||
await this.lastMileRepository.update(payload.sourceId, { paid: true } as any);
|
||||
this.logger.log(`Marked last-mile record ${payload.sourceId} as paid (invoice ${payload.invoiceId})`);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Failed to update last-mile payment status for record ${payload.sourceId}: ${String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateLastMileDto): Promise<LastMile> {
|
||||
const existing = await this.findById(id);
|
||||
|
||||
const dtoAny = dto as any;
|
||||
const updated = await this.lastMileRepository.update(id, {
|
||||
...(dto.bookingId !== undefined ? { bookingId: dto.bookingId } : {}),
|
||||
...(dto.status !== undefined ? { status: dto.status } : {}),
|
||||
@@ -151,7 +167,8 @@ export class LastMileService {
|
||||
...(dto.estimatedKm !== undefined ? { estimatedKm: dto.estimatedKm } : {}),
|
||||
...(dto.exactKm !== undefined ? { exactKm: dto.exactKm } : {}),
|
||||
...(dto.vehicleId !== undefined ? { vehicleId: dto.vehicleId } : {}),
|
||||
});
|
||||
...(dtoAny.paid !== undefined ? { paid: dtoAny.paid } : {}),
|
||||
} as any);
|
||||
|
||||
if (!updated) {
|
||||
throw new NotFoundException(`Last-mile record ${id} not found`);
|
||||
|
||||
@@ -771,9 +771,25 @@ const FirstMilePage = () => {
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => {
|
||||
const hasDistance = row.original.exactKm != null && row.original.exactKm > 0;
|
||||
const isPaid = (row.original as any).paid;
|
||||
if (!hasDistance) {
|
||||
return <Text c="dimmed">—</Text>;
|
||||
}
|
||||
if (isPaid) {
|
||||
return (
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<UnstyledButton
|
||||
onClick={() => openInvoice(row.original)}
|
||||
c="blue"
|
||||
fw={500}
|
||||
style={{ textDecoration: "underline", cursor: "pointer" }}
|
||||
>
|
||||
#345
|
||||
</UnstyledButton>
|
||||
<Badge color="green" variant="light" size="sm">Paid</Badge>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<UnstyledButton
|
||||
onClick={() => openInvoice(row.original)}
|
||||
|
||||
@@ -751,9 +751,25 @@ const LastMilePage = () => {
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => {
|
||||
const hasDistance = row.original.exactKm != null && row.original.exactKm > 0;
|
||||
const isPaid = (row.original as any).paid;
|
||||
if (!hasDistance) {
|
||||
return <Text c="dimmed">—</Text>;
|
||||
}
|
||||
if (isPaid) {
|
||||
return (
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<UnstyledButton
|
||||
onClick={() => openInvoice(row.original)}
|
||||
c="blue"
|
||||
fw={500}
|
||||
style={{ textDecoration: "underline", cursor: "pointer" }}
|
||||
>
|
||||
#345
|
||||
</UnstyledButton>
|
||||
<Badge color="green" variant="light" size="sm">Paid</Badge>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<UnstyledButton
|
||||
onClick={() => openInvoice(row.original)}
|
||||
|
||||
@@ -149,7 +149,8 @@ export enum InvoiceSource {
|
||||
Booking = "booking",
|
||||
Warehouse = "warehouse",
|
||||
Demurrage = "demurrage",
|
||||
FirstMile = "firstmile"
|
||||
FirstMile = "firstmile",
|
||||
LastMile = "lastmile"
|
||||
}
|
||||
|
||||
export enum SchedulingStatus {
|
||||
|
||||
Reference in New Issue
Block a user