mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 00:38:11 +00:00
Import operation gate pass
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsArray, IsDateString, IsIn, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
import { DJIBOUTI_INCIDENT_TYPES, type DjiboutiIncidentType } from '../entities/djibouti-incident.entity';
|
||||
import {
|
||||
EMPTY_CONTAINER_RETURN_STATUSES,
|
||||
type EmptyContainerReturnStatus,
|
||||
} from '../entities/empty-container-return.entity';
|
||||
import {
|
||||
IMPORT_CUSTOMS_RISK_LEVELS,
|
||||
type ImportCustomsDocumentType,
|
||||
type ImportCustomsRiskLevel,
|
||||
} from '../entities/import-customs-finalization.entity';
|
||||
|
||||
export const IMPORT_CUSTOMS_DOCUMENT_TYPES = [
|
||||
'IM4',
|
||||
'IM5',
|
||||
'T1_CLOSURE_PROOF',
|
||||
'TRANSIT_PERMIT_SCREENSHOT',
|
||||
'CUSTOMER_PAYMENT_SLIP',
|
||||
'IMPORT_RELEASE_PERMIT',
|
||||
] as const;
|
||||
|
||||
export class CreateDjiboutiIncidentDto {
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
@IsUUID()
|
||||
bookingId!: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
containerNumber?: string;
|
||||
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
cargoId?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
facility?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
station?: string;
|
||||
|
||||
@ApiProperty({ enum: DJIBOUTI_INCIDENT_TYPES })
|
||||
@IsIn(DJIBOUTI_INCIDENT_TYPES)
|
||||
incidentType!: DjiboutiIncidentType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
description!: string;
|
||||
|
||||
@ApiPropertyOptional({ type: [String] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
photos?: string[];
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
reportedBy?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
reportedAt?: string;
|
||||
}
|
||||
|
||||
export class UploadImportCustomsDocumentDto {
|
||||
@ApiProperty({ enum: IMPORT_CUSTOMS_DOCUMENT_TYPES })
|
||||
@IsIn(IMPORT_CUSTOMS_DOCUMENT_TYPES)
|
||||
documentType!: ImportCustomsDocumentType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
fileId!: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
performedBy?: string;
|
||||
}
|
||||
|
||||
export class RecordDeclarationDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
declarationSerialNumber!: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
performedBy?: string;
|
||||
}
|
||||
|
||||
export class AssignCustomsRiskDto {
|
||||
@ApiProperty({ enum: IMPORT_CUSTOMS_RISK_LEVELS })
|
||||
@IsIn(IMPORT_CUSTOMS_RISK_LEVELS)
|
||||
risk!: ImportCustomsRiskLevel;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
performedBy?: string;
|
||||
}
|
||||
|
||||
export class ImportOperationActionDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
performedBy?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class CreateEmptyContainerReturnDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
containerNumber!: string;
|
||||
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
bookingId?: string;
|
||||
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
customerId?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
returnDate?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
facility?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
yard?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
zone?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
condition?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
handoverNote?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
performedBy?: string;
|
||||
}
|
||||
|
||||
export class UpdateEmptyContainerReturnStatusDto extends ImportOperationActionDto {
|
||||
@ApiProperty({ enum: EMPTY_CONTAINER_RETURN_STATUSES })
|
||||
@IsIn(EMPTY_CONTAINER_RETURN_STATUSES)
|
||||
status!: EmptyContainerReturnStatus;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
wagonAllocationReference?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
handoverNote?: string;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
|
||||
export const DJIBOUTI_INCIDENT_TYPES = [
|
||||
'SEAL_BROKEN',
|
||||
'CONTAINER_OPENED',
|
||||
'CONTAINER_DAMAGED',
|
||||
'FLUID_LEAKING',
|
||||
'QUANTITY_MISMATCH',
|
||||
'WEIGHT_MISMATCH',
|
||||
'OTHER',
|
||||
] as const;
|
||||
|
||||
export type DjiboutiIncidentType = (typeof DJIBOUTI_INCIDENT_TYPES)[number];
|
||||
|
||||
@Entity({ schema: 'freight', name: 'djibouti_import_incidents' })
|
||||
@Index(['bookingId'])
|
||||
@Index(['containerNumber'])
|
||||
@Index(['incidentType'])
|
||||
export class DjiboutiIncident extends BaseEntity {
|
||||
@Column({ name: 'booking_id', type: 'uuid' })
|
||||
bookingId!: string;
|
||||
|
||||
@Column({ name: 'container_number', type: 'varchar', length: 80, nullable: true })
|
||||
containerNumber?: string | null;
|
||||
|
||||
@Column({ name: 'cargo_id', type: 'uuid', nullable: true })
|
||||
cargoId?: string | null;
|
||||
|
||||
@Column({ name: 'facility', type: 'varchar', length: 120, nullable: true })
|
||||
facility?: string | null;
|
||||
|
||||
@Column({ name: 'station', type: 'varchar', length: 120, nullable: true })
|
||||
station?: string | null;
|
||||
|
||||
@Column({ name: 'incident_type', type: 'varchar', length: 40 })
|
||||
incidentType!: DjiboutiIncidentType;
|
||||
|
||||
@Column({ name: 'description', type: 'text' })
|
||||
description!: string;
|
||||
|
||||
@Column({ name: 'photos', type: 'jsonb', default: () => "'[]'::jsonb" })
|
||||
photos!: string[];
|
||||
|
||||
@Column({ name: 'reported_by', type: 'varchar', length: 120, nullable: true })
|
||||
reportedBy?: string | null;
|
||||
|
||||
@Column({ name: 'reported_at', type: 'timestamptz' })
|
||||
reportedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
|
||||
export const EMPTY_CONTAINER_RETURN_STATUSES = [
|
||||
'RETURNED',
|
||||
'ASSIGNED_STORAGE',
|
||||
'DOCUMENTATION_CLEARED',
|
||||
'WAGON_ALLOCATED',
|
||||
'TRANSPORTED_TO_DJIBOUTI',
|
||||
'HANDOVER_ISSUED',
|
||||
'COMPLETED',
|
||||
] as const;
|
||||
|
||||
export type EmptyContainerReturnStatus = (typeof EMPTY_CONTAINER_RETURN_STATUSES)[number];
|
||||
|
||||
@Entity({ schema: 'freight', name: 'empty_container_returns' })
|
||||
@Index(['containerNumber'])
|
||||
@Index(['bookingId'])
|
||||
@Index(['status'])
|
||||
export class EmptyContainerReturn extends BaseEntity {
|
||||
@Column({ name: 'container_number', type: 'varchar', length: 80 })
|
||||
containerNumber!: string;
|
||||
|
||||
@Column({ name: 'booking_id', type: 'uuid', nullable: true })
|
||||
bookingId?: string | null;
|
||||
|
||||
@Column({ name: 'customer_id', type: 'uuid', nullable: true })
|
||||
customerId?: string | null;
|
||||
|
||||
@Column({ name: 'return_date', type: 'timestamptz' })
|
||||
returnDate!: Date;
|
||||
|
||||
@Column({ name: 'facility', type: 'varchar', length: 120, nullable: true })
|
||||
facility?: string | null;
|
||||
|
||||
@Column({ name: 'yard', type: 'varchar', length: 120, nullable: true })
|
||||
yard?: string | null;
|
||||
|
||||
@Column({ name: 'zone', type: 'varchar', length: 120, nullable: true })
|
||||
zone?: string | null;
|
||||
|
||||
@Column({ name: 'condition', type: 'text', nullable: true })
|
||||
condition?: string | null;
|
||||
|
||||
@Column({ name: 'handover_note', type: 'text', nullable: true })
|
||||
handoverNote?: string | null;
|
||||
|
||||
@Column({ name: 'status', type: 'varchar', length: 40, default: 'RETURNED' })
|
||||
status!: EmptyContainerReturnStatus;
|
||||
|
||||
@Column({ name: 'wagon_allocation_reference', type: 'varchar', length: 120, nullable: true })
|
||||
wagonAllocationReference?: string | null;
|
||||
|
||||
@Column({ name: 'performed_by', type: 'varchar', length: 120, nullable: true })
|
||||
performedBy?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
|
||||
export const IMPORT_CUSTOMS_RISK_LEVELS = ['GREEN', 'YELLOW', 'BLUE', 'RED'] as const;
|
||||
export type ImportCustomsRiskLevel = (typeof IMPORT_CUSTOMS_RISK_LEVELS)[number];
|
||||
|
||||
export type ImportCustomsDocumentType =
|
||||
| 'IM4'
|
||||
| 'IM5'
|
||||
| 'T1_CLOSURE_PROOF'
|
||||
| 'TRANSIT_PERMIT_SCREENSHOT'
|
||||
| 'CUSTOMER_PAYMENT_SLIP'
|
||||
| 'IMPORT_RELEASE_PERMIT';
|
||||
|
||||
@Entity({ schema: 'freight', name: 'import_customs_finalizations' })
|
||||
@Index(['bookingId'], { unique: true })
|
||||
@Index(['customsRisk'])
|
||||
export class ImportCustomsFinalization extends BaseEntity {
|
||||
@Column({ name: 'booking_id', type: 'uuid' })
|
||||
bookingId!: string;
|
||||
|
||||
@Column({ name: 'documents', type: 'jsonb', default: () => "'{}'::jsonb" })
|
||||
documents!: Partial<Record<ImportCustomsDocumentType, string>>;
|
||||
|
||||
@Column({ name: 'declaration_serial_number', type: 'varchar', length: 120, nullable: true })
|
||||
declarationSerialNumber?: string | null;
|
||||
|
||||
@Column({ name: 'duties_taxes_notified_at', type: 'timestamptz', nullable: true })
|
||||
dutiesTaxesNotifiedAt?: Date | null;
|
||||
|
||||
@Column({ name: 'duties_taxes_paid_at', type: 'timestamptz', nullable: true })
|
||||
dutiesTaxesPaidAt?: Date | null;
|
||||
|
||||
@Column({ name: 'customs_risk', type: 'varchar', length: 12, nullable: true })
|
||||
customsRisk?: ImportCustomsRiskLevel | null;
|
||||
|
||||
@Column({ name: 'import_release_permitted_at', type: 'timestamptz', nullable: true })
|
||||
importReleasePermittedAt?: Date | null;
|
||||
|
||||
@Column({ name: 'completed_at', type: 'timestamptz', nullable: true })
|
||||
completedAt?: Date | null;
|
||||
|
||||
@Column({ name: 'performed_by', type: 'varchar', length: 120, nullable: true })
|
||||
performedBy?: string | null;
|
||||
|
||||
@Column({ name: 'notes', type: 'text', nullable: true })
|
||||
notes?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Body, Controller, Get, Param, ParseUUIDPipe, Post, Query } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import {
|
||||
AssignCustomsRiskDto,
|
||||
CreateDjiboutiIncidentDto,
|
||||
CreateEmptyContainerReturnDto,
|
||||
ImportOperationActionDto,
|
||||
RecordDeclarationDto,
|
||||
UpdateEmptyContainerReturnStatusDto,
|
||||
UploadImportCustomsDocumentDto,
|
||||
} from './dto/import-operations.dto';
|
||||
import { ImportOperationsService } from './import-operations.service';
|
||||
|
||||
@ApiTags('import-operations')
|
||||
@ApiBearerAuth()
|
||||
@Controller('import-operations')
|
||||
export class ImportOperationsController {
|
||||
constructor(private readonly service: ImportOperationsService) {}
|
||||
|
||||
@Get('djibouti-incidents')
|
||||
@ApiOperation({ summary: 'Batch 8: list Djibouti import incidents' })
|
||||
listIncidents(@Query('bookingId') bookingId?: string) {
|
||||
return this.service.listIncidents(bookingId);
|
||||
}
|
||||
|
||||
@Post('djibouti-incidents')
|
||||
@ApiOperation({ summary: 'Batch 8: report a Djibouti import incident / exception' })
|
||||
createIncident(@Body() dto: CreateDjiboutiIncidentDto) {
|
||||
return this.service.createIncident(dto);
|
||||
}
|
||||
|
||||
@Get('customs/:bookingId')
|
||||
@ApiOperation({ summary: 'Batch 12: import customs finalization state' })
|
||||
getCustoms(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
||||
return this.service.getCustoms(bookingId);
|
||||
}
|
||||
|
||||
@Post('customs/:bookingId/documents')
|
||||
@ApiOperation({ summary: 'Batch 12: upload IM4/IM5/T1/permit/payment-slip documents' })
|
||||
uploadCustomsDocument(
|
||||
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
||||
@Body() dto: UploadImportCustomsDocumentDto,
|
||||
) {
|
||||
return this.service.uploadCustomsDocument(bookingId, dto);
|
||||
}
|
||||
|
||||
@Post('customs/:bookingId/declaration')
|
||||
@ApiOperation({ summary: 'Batch 12: record declaration serial number' })
|
||||
recordDeclaration(
|
||||
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
||||
@Body() dto: RecordDeclarationDto,
|
||||
) {
|
||||
return this.service.recordDeclaration(bookingId, dto);
|
||||
}
|
||||
|
||||
@Post('customs/:bookingId/notify-duties-taxes')
|
||||
@ApiOperation({ summary: 'Batch 12: notify duties and taxes' })
|
||||
notifyDutiesTaxes(
|
||||
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
||||
@Body() dto: ImportOperationActionDto,
|
||||
) {
|
||||
return this.service.notifyDutiesTaxes(bookingId, dto);
|
||||
}
|
||||
|
||||
@Post('customs/:bookingId/duties-taxes-paid')
|
||||
@ApiOperation({ summary: 'Batch 12: mark duties and taxes paid' })
|
||||
markDutiesTaxesPaid(
|
||||
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
||||
@Body() dto: ImportOperationActionDto,
|
||||
) {
|
||||
return this.service.markDutiesTaxesPaid(bookingId, dto);
|
||||
}
|
||||
|
||||
@Post('customs/:bookingId/risk')
|
||||
@ApiOperation({ summary: 'Batch 12: assign customs risk' })
|
||||
assignRisk(@Param('bookingId', ParseUUIDPipe) bookingId: string, @Body() dto: AssignCustomsRiskDto) {
|
||||
return this.service.assignRisk(bookingId, dto);
|
||||
}
|
||||
|
||||
@Post('customs/:bookingId/release-permitted')
|
||||
@ApiOperation({ summary: 'Batch 12: mark import release permitted' })
|
||||
markReleasePermitted(
|
||||
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
||||
@Body() dto: ImportOperationActionDto,
|
||||
) {
|
||||
return this.service.markReleasePermitted(bookingId, dto);
|
||||
}
|
||||
|
||||
@Get('empty-container-returns')
|
||||
@ApiOperation({ summary: 'Batch 16: list empty container returns' })
|
||||
listEmptyReturns() {
|
||||
return this.service.listEmptyReturns();
|
||||
}
|
||||
|
||||
@Post('empty-container-returns')
|
||||
@ApiOperation({ summary: 'Batch 16: create an empty container return record' })
|
||||
createEmptyReturn(@Body() dto: CreateEmptyContainerReturnDto) {
|
||||
return this.service.createEmptyReturn(dto);
|
||||
}
|
||||
|
||||
@Post('empty-container-returns/:id/status')
|
||||
@ApiOperation({ summary: 'Batch 16: advance empty container return workflow' })
|
||||
updateEmptyReturnStatus(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() dto: UpdateEmptyContainerReturnStatusDto,
|
||||
) {
|
||||
return this.service.updateEmptyReturnStatus(id, dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { DjiboutiIncident } from './entities/djibouti-incident.entity';
|
||||
import { EmptyContainerReturn } from './entities/empty-container-return.entity';
|
||||
import { ImportCustomsFinalization } from './entities/import-customs-finalization.entity';
|
||||
import { ImportOperationsController } from './import-operations.controller';
|
||||
import { ImportOperationsService } from './import-operations.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
DjiboutiIncident,
|
||||
ImportCustomsFinalization,
|
||||
EmptyContainerReturn,
|
||||
]),
|
||||
],
|
||||
controllers: [ImportOperationsController],
|
||||
providers: [ImportOperationsService],
|
||||
exports: [ImportOperationsService],
|
||||
})
|
||||
export class ImportOperationsModule {}
|
||||
@@ -0,0 +1,211 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
CreateDjiboutiIncidentDto,
|
||||
CreateEmptyContainerReturnDto,
|
||||
ImportOperationActionDto,
|
||||
RecordDeclarationDto,
|
||||
AssignCustomsRiskDto,
|
||||
UpdateEmptyContainerReturnStatusDto,
|
||||
UploadImportCustomsDocumentDto,
|
||||
} from './dto/import-operations.dto';
|
||||
import {
|
||||
DjiboutiIncident,
|
||||
type DjiboutiIncidentType,
|
||||
} from './entities/djibouti-incident.entity';
|
||||
import { EmptyContainerReturn } from './entities/empty-container-return.entity';
|
||||
import {
|
||||
ImportCustomsFinalization,
|
||||
type ImportCustomsDocumentType,
|
||||
} from './entities/import-customs-finalization.entity';
|
||||
|
||||
const DAMAGE_INCIDENTS: DjiboutiIncidentType[] = [
|
||||
'SEAL_BROKEN',
|
||||
'CONTAINER_OPENED',
|
||||
'CONTAINER_DAMAGED',
|
||||
'FLUID_LEAKING',
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class ImportOperationsService {
|
||||
constructor(
|
||||
@InjectRepository(DjiboutiIncident)
|
||||
private readonly incidents: Repository<DjiboutiIncident>,
|
||||
@InjectRepository(ImportCustomsFinalization)
|
||||
private readonly customs: Repository<ImportCustomsFinalization>,
|
||||
@InjectRepository(EmptyContainerReturn)
|
||||
private readonly emptyReturns: Repository<EmptyContainerReturn>,
|
||||
) {}
|
||||
|
||||
listIncidents(bookingId?: string) {
|
||||
return this.incidents.find({
|
||||
where: bookingId ? { bookingId } : {},
|
||||
order: { reportedAt: 'DESC', createdAt: 'DESC' } as never,
|
||||
});
|
||||
}
|
||||
|
||||
async createIncident(dto: CreateDjiboutiIncidentDto) {
|
||||
const photos = dto.photos ?? [];
|
||||
if (DAMAGE_INCIDENTS.includes(dto.incidentType) && photos.length === 0) {
|
||||
throw new BadRequestException('Photos are required for damage-related Djibouti incidents');
|
||||
}
|
||||
|
||||
const incident = await this.incidents.save(
|
||||
this.incidents.create({
|
||||
bookingId: dto.bookingId,
|
||||
containerNumber: dto.containerNumber ?? null,
|
||||
cargoId: dto.cargoId ?? null,
|
||||
facility: dto.facility ?? null,
|
||||
station: dto.station ?? null,
|
||||
incidentType: dto.incidentType,
|
||||
description: dto.description,
|
||||
photos,
|
||||
reportedBy: dto.reportedBy ?? null,
|
||||
reportedAt: dto.reportedAt ? new Date(dto.reportedAt) : new Date(),
|
||||
}),
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[NOTIFY] Djibouti incident ${incident.incidentType} for booking ${incident.bookingId}; notify Global Logistics Ethiopia and customer.`,
|
||||
);
|
||||
console.log(
|
||||
`[MOVEMENT] Attach incident ${incident.id} to booking ${incident.bookingId} movement history.`,
|
||||
);
|
||||
return incident;
|
||||
}
|
||||
|
||||
async getCustoms(bookingId: string) {
|
||||
return this.getOrCreateCustoms(bookingId);
|
||||
}
|
||||
|
||||
async uploadCustomsDocument(bookingId: string, dto: UploadImportCustomsDocumentDto) {
|
||||
const row = await this.getOrCreateCustoms(bookingId);
|
||||
const documents = { ...(row.documents ?? {}), [dto.documentType]: dto.fileId };
|
||||
await this.customs.update(row.id, {
|
||||
documents,
|
||||
performedBy: dto.performedBy ?? row.performedBy ?? null,
|
||||
});
|
||||
return this.getCustoms(bookingId);
|
||||
}
|
||||
|
||||
async recordDeclaration(bookingId: string, dto: RecordDeclarationDto) {
|
||||
const row = await this.getOrCreateCustoms(bookingId);
|
||||
await this.customs.update(row.id, {
|
||||
declarationSerialNumber: dto.declarationSerialNumber,
|
||||
performedBy: dto.performedBy ?? row.performedBy ?? null,
|
||||
});
|
||||
return this.getCustoms(bookingId);
|
||||
}
|
||||
|
||||
async notifyDutiesTaxes(bookingId: string, dto: ImportOperationActionDto = {}) {
|
||||
const row = await this.getOrCreateCustoms(bookingId);
|
||||
await this.customs.update(row.id, {
|
||||
dutiesTaxesNotifiedAt: row.dutiesTaxesNotifiedAt ?? new Date(),
|
||||
performedBy: dto.performedBy ?? row.performedBy ?? null,
|
||||
notes: dto.notes ?? row.notes ?? null,
|
||||
});
|
||||
console.log(`[NOTIFY] Duties and taxes notification sent for booking ${bookingId}.`);
|
||||
return this.getCustoms(bookingId);
|
||||
}
|
||||
|
||||
async markDutiesTaxesPaid(bookingId: string, dto: ImportOperationActionDto = {}) {
|
||||
const row = await this.getOrCreateCustoms(bookingId);
|
||||
this.assertDocument(row, 'CUSTOMER_PAYMENT_SLIP', 'Customer payment slip is required before marking duties and taxes paid');
|
||||
await this.customs.update(row.id, {
|
||||
dutiesTaxesPaidAt: row.dutiesTaxesPaidAt ?? new Date(),
|
||||
performedBy: dto.performedBy ?? row.performedBy ?? null,
|
||||
notes: dto.notes ?? row.notes ?? null,
|
||||
});
|
||||
return this.getCustoms(bookingId);
|
||||
}
|
||||
|
||||
async assignRisk(bookingId: string, dto: AssignCustomsRiskDto) {
|
||||
const row = await this.getOrCreateCustoms(bookingId);
|
||||
await this.customs.update(row.id, {
|
||||
customsRisk: dto.risk,
|
||||
performedBy: dto.performedBy ?? row.performedBy ?? null,
|
||||
});
|
||||
console.log(`[NOTIFY] Customs risk ${dto.risk} assigned for booking ${bookingId}; notify customer.`);
|
||||
return this.getCustoms(bookingId);
|
||||
}
|
||||
|
||||
async markReleasePermitted(bookingId: string, dto: ImportOperationActionDto = {}) {
|
||||
const row = await this.getOrCreateCustoms(bookingId);
|
||||
this.assertReleaseReady(row);
|
||||
await this.customs.update(row.id, {
|
||||
importReleasePermittedAt: row.importReleasePermittedAt ?? new Date(),
|
||||
completedAt: row.completedAt ?? new Date(),
|
||||
performedBy: dto.performedBy ?? row.performedBy ?? null,
|
||||
notes: dto.notes ?? row.notes ?? null,
|
||||
});
|
||||
console.log(`[NOTIFY] Import release permitted for booking ${bookingId}; notify customer.`);
|
||||
return this.getCustoms(bookingId);
|
||||
}
|
||||
|
||||
listEmptyReturns() {
|
||||
return this.emptyReturns.find({ order: { createdAt: 'DESC' } as never });
|
||||
}
|
||||
|
||||
async createEmptyReturn(dto: CreateEmptyContainerReturnDto) {
|
||||
return this.emptyReturns.save(
|
||||
this.emptyReturns.create({
|
||||
containerNumber: dto.containerNumber,
|
||||
bookingId: dto.bookingId ?? null,
|
||||
customerId: dto.customerId ?? null,
|
||||
returnDate: dto.returnDate ? new Date(dto.returnDate) : new Date(),
|
||||
facility: dto.facility ?? null,
|
||||
yard: dto.yard ?? null,
|
||||
zone: dto.zone ?? null,
|
||||
condition: dto.condition ?? null,
|
||||
handoverNote: dto.handoverNote ?? null,
|
||||
performedBy: dto.performedBy ?? null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async updateEmptyReturnStatus(id: string, dto: UpdateEmptyContainerReturnStatusDto) {
|
||||
const row = await this.emptyReturns.findOne({ where: { id } });
|
||||
if (!row) {
|
||||
throw new NotFoundException(`Empty container return ${id} not found`);
|
||||
}
|
||||
await this.emptyReturns.update(id, {
|
||||
status: dto.status,
|
||||
wagonAllocationReference: dto.wagonAllocationReference ?? row.wagonAllocationReference ?? null,
|
||||
handoverNote: dto.handoverNote ?? row.handoverNote ?? null,
|
||||
performedBy: dto.performedBy ?? row.performedBy ?? null,
|
||||
});
|
||||
return this.emptyReturns.findOneOrFail({ where: { id } });
|
||||
}
|
||||
|
||||
private async getOrCreateCustoms(bookingId: string) {
|
||||
const existing = await this.customs.findOne({ where: { bookingId } });
|
||||
if (existing) return existing;
|
||||
return this.customs.save(this.customs.create({ bookingId, documents: {} }));
|
||||
}
|
||||
|
||||
private assertDocument(
|
||||
row: ImportCustomsFinalization,
|
||||
type: ImportCustomsDocumentType,
|
||||
message: string,
|
||||
) {
|
||||
if (!row.documents?.[type]) {
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private assertReleaseReady(row: ImportCustomsFinalization) {
|
||||
this.assertDocument(row, 'T1_CLOSURE_PROOF', 'T1 closure proof is required before import release');
|
||||
this.assertDocument(row, 'IMPORT_RELEASE_PERMIT', 'Import release permit upload is required before release is permitted');
|
||||
if (!row.declarationSerialNumber?.trim()) {
|
||||
throw new BadRequestException('Declaration serial number is required before import release');
|
||||
}
|
||||
if (!row.customsRisk) {
|
||||
throw new BadRequestException('Customs risk must be assigned before import release');
|
||||
}
|
||||
if (!row.dutiesTaxesPaidAt) {
|
||||
throw new BadRequestException('Duties and taxes must be paid before import release');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user