Files
admin-web/src/modules/container-loading/api/platformContainerCustoms.ts
T

217 lines
11 KiB
TypeScript
Raw Normal View History

2026-08-28 16:54:02 +08:00
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
export type ContainerCustomsStatus = 'WAITING_CUSTOMER_DOCUMENTS' | 'READY_TO_START' | 'DECLARING' | 'COMPLETED'
export type ContainerCustomsTaxMode = 'TAX_REFUND' | 'BUY_DECLARATION'
export interface ContainerCustomsFile {
fileId: string
fileName: string
fileHash: string
sizeBytes: number
version: number
uploadedAt: string
}
2026-09-28 09:49:53 +08:00
export interface ContainerCustomsReceiptVersion {
version: number
file: ContainerCustomsFile
operatorName?: string | null
createdAt: string
}
2026-08-28 16:54:02 +08:00
export interface ContainerCustomsInbound {
inboundId: string
orderId: string
orderNo: string
inboundCode: string
tenantId: string
customerName: string
taxMode: ContainerCustomsTaxMode
covered: boolean
}
export interface ContainerCustomsDocument {
documentId: string
tenantId: string
source: ContainerCustomsFile
inboundIds: string[]
inboundCodes: string[]
frozen: boolean
receipt: ContainerCustomsFile | null
2026-09-24 16:29:31 +08:00
receiptStatus?: 'PENDING' | 'CONFIRMED' | 'REJECTED' | null
receiptRejectReason?: string | null
finalDocument: ContainerCustomsFile | null
2026-09-28 09:49:53 +08:00
receiptHistory?: ContainerCustomsReceiptVersion[]
2026-08-28 16:54:02 +08:00
}
export interface ContainerCustomsView {
aggregateId: string
loadingPlanId: string
containerBookingId: string
planNo: string
cabinetNo: string
containerNo: string | null
status: ContainerCustomsStatus
hasActualCargo: boolean
2026-08-28 16:54:02 +08:00
version: number
taxRefundCount: number
coveredTaxRefundCount: number
2026-09-24 16:29:31 +08:00
actualBoxCount: number
2026-08-28 16:54:02 +08:00
inbounds: ContainerCustomsInbound[]
documents: ContainerCustomsDocument[]
startedAt: string | null
completedAt: string | null
brokerSubmittedAt?: string | null
2026-08-28 16:54:02 +08:00
}
export type DeclarationBatchStatus = 'READY' | 'SUBMITTED' | 'ACKNOWLEDGED' | 'RESULT_PENDING' | 'CLOSED' | 'CANCELLED'
export type ClearanceResultType = 'INSPECTION' | 'SUPPLEMENT_REQUIRED' | 'RELEASED' | 'DETAINED' | 'OTHER'
export type ClearanceResultStatus = 'OPEN' | 'RESOLVED' | 'CANCELLED'
export interface ClearanceResult {
id: string
declarationBatchId: string
customsContainerId: string
resultType: ClearanceResultType
resultStatus: ClearanceResultStatus
customsEntryNo: string | null
occurredAt: string
resolution: string | null
evidenceFileId: string | null
recordedByName: string
recordedAt: string
previousResultId: string | null
}
export interface DeclarationBatch {
id: string
customsContainerId: string
loadingPlanId: string
batchNo: string
status: DeclarationBatchStatus
sourceVersion: number
customsEntryNo: string | null
version: number
submittedAt: string | null
submittedByName: string | null
acknowledgedAt: string | null
acknowledgedByName: string | null
remark: string | null
results: ClearanceResult[]
}
2026-08-28 16:54:02 +08:00
interface RawContainerCustomsView extends Omit<ContainerCustomsView, 'aggregateId' | 'loadingPlanId' | 'containerBookingId' | 'version' | 'inbounds' | 'documents'> {
aggregateId: string | number
loadingPlanId: string | number
containerBookingId: string | number
version: string | number
inbounds: Array<Omit<ContainerCustomsInbound, 'inboundId' | 'orderId' | 'tenantId'> & { inboundId: string | number; orderId: string | number; tenantId: string | number }>
documents: Array<Omit<ContainerCustomsDocument, 'documentId' | 'tenantId' | 'source' | 'receipt' | 'finalDocument'> & { documentId: string | number; tenantId: string | number; source: ContainerCustomsFile; receipt: ContainerCustomsFile | null; finalDocument: ContainerCustomsFile | null }>
2026-08-28 16:54:02 +08:00
}
const basePath = '/platform/v1/customs/loading-plans'
function operationId(prefix: string) { return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}` }
function file(value: ContainerCustomsFile | null): ContainerCustomsFile | null {
return value ? { ...value, fileId: String(value.fileId), sizeBytes: Number(value.sizeBytes), version: Number(value.version) } : null
}
function normalize(value: RawContainerCustomsView): ContainerCustomsView {
return {
...value,
aggregateId: String(value.aggregateId), loadingPlanId: String(value.loadingPlanId), containerBookingId: String(value.containerBookingId), version: Number(value.version),
inbounds: value.inbounds.map((item) => ({ ...item, inboundId: String(item.inboundId), orderId: String(item.orderId), tenantId: String(item.tenantId) })),
2026-09-28 09:49:53 +08:00
documents: value.documents.map((item) => ({ ...item, documentId: String(item.documentId), tenantId: String(item.tenantId), source: file(item.source)!, receipt: file(item.receipt), finalDocument: file(item.finalDocument), receiptHistory: (item.receiptHistory || []).map((version) => ({ ...version, version: Number(version.version), file: file(version.file)! })) })),
2026-08-28 16:54:02 +08:00
}
}
function normalizeClearanceResult(value: ClearanceResult): ClearanceResult {
return { ...value, id: String(value.id), declarationBatchId: String(value.declarationBatchId), customsContainerId: String(value.customsContainerId), evidenceFileId: value.evidenceFileId == null ? null : String(value.evidenceFileId), previousResultId: value.previousResultId == null ? null : String(value.previousResultId) }
}
function normalizeDeclarationBatch(value: DeclarationBatch): DeclarationBatch {
return { ...value, id: String(value.id), customsContainerId: String(value.customsContainerId), loadingPlanId: String(value.loadingPlanId), sourceVersion: Number(value.sourceVersion), version: Number(value.version), results: (value.results || []).map(normalizeClearanceResult) }
}
export async function getDeclarationBatches(loadingPlanId: string) {
const response = await http.get<ApiEnvelope<DeclarationBatch[]>, ApiEnvelope<DeclarationBatch[]>>(`${basePath}/${encodeURIComponent(loadingPlanId)}/declaration-batches`)
return (unwrapApiResponse(response) || []).map(normalizeDeclarationBatch)
}
export async function createDeclarationBatch(loadingPlanId: string, expectedVersion: number, batchNo?: string) {
const response = await http.post<ApiEnvelope<DeclarationBatch>, ApiEnvelope<DeclarationBatch>>(`${basePath}/${encodeURIComponent(loadingPlanId)}/declaration-batches`, { expectedVersion, batchNo: batchNo?.trim() || null, remark: null }, { headers: { 'Idempotency-Key': operationId('customs-declaration-batch-create') } })
return normalizeDeclarationBatch(unwrapApiResponse(response))
}
export async function submitDeclarationBatch(batch: DeclarationBatch, customsEntryNo: string) {
const response = await http.post<ApiEnvelope<DeclarationBatch>, ApiEnvelope<DeclarationBatch>>(`${basePath}/declaration-batches/${encodeURIComponent(batch.id)}/submit`, { expectedVersion: batch.version, batchNo: batch.batchNo, customsEntryNo: customsEntryNo.trim(), remark: null }, { headers: { 'Idempotency-Key': operationId('customs-declaration-batch-submit') } })
return normalizeDeclarationBatch(unwrapApiResponse(response))
}
export async function recordClearanceResult(batch: DeclarationBatch, resultType: ClearanceResultType, resolution?: string) {
const response = await http.post<ApiEnvelope<ClearanceResult>, ApiEnvelope<ClearanceResult>>(`${basePath}/declaration-batches/${encodeURIComponent(batch.id)}/results`, { expectedVersion: batch.version, resultType, occurredAt: new Date().toISOString(), resolution: resolution?.trim() || null, evidenceFileId: null, resolved: false }, { headers: { 'Idempotency-Key': operationId('customs-clearance-result-record') } })
return normalizeClearanceResult(unwrapApiResponse(response))
}
export async function resolveClearanceResult(result: ClearanceResult, batchVersion: number, resolution: string) {
const response = await http.post<ApiEnvelope<ClearanceResult>, ApiEnvelope<ClearanceResult>>(`${basePath}/clearance-results/${encodeURIComponent(result.id)}/resolve`, { expectedVersion: batchVersion, resolution: resolution.trim() }, { headers: { 'Idempotency-Key': operationId('customs-clearance-result-resolve') } })
return normalizeClearanceResult(unwrapApiResponse(response))
}
2026-08-28 16:54:02 +08:00
export async function getContainerCustoms(loadingPlanId: string) {
2026-09-10 14:15:44 +08:00
const response = await http.get<ApiEnvelope<RawContainerCustomsView | null>, ApiEnvelope<RawContainerCustomsView | null>>(`${basePath}/${encodeURIComponent(loadingPlanId)}`)
const value = unwrapApiResponse(response)
return value ? normalize(value) : null
2026-08-28 16:54:02 +08:00
}
export function downloadContainerCustomsDocument(documentId: string) {
return http.get<Blob, Blob>(`${basePath}/documents/${encodeURIComponent(documentId)}/download`, { responseType: 'blob' })
}
export function downloadContainerCustomsReceipt(documentId: string) {
return http.get<Blob, Blob>(`${basePath}/documents/${encodeURIComponent(documentId)}/receipt/download`, { responseType: 'blob' })
}
export function downloadContainerCustomsFinal(documentId: string) {
return http.get<Blob, Blob>(`${basePath}/documents/${encodeURIComponent(documentId)}/final/download`, { responseType: 'blob' })
}
async function action(loadingPlanId: string, name: 'start' | 'submit-to-broker' | 'complete-all-buy', expectedVersion: number, remark?: string) {
2026-08-28 16:54:02 +08:00
const response = await http.post<ApiEnvelope<RawContainerCustomsView>, ApiEnvelope<RawContainerCustomsView>>(
`${basePath}/${encodeURIComponent(loadingPlanId)}/${name}`,
{ expectedVersion, confirm: true, remark: remark?.trim() || null },
{ headers: { 'Idempotency-Key': operationId(`container-customs-${name}`) } },
)
return normalize(unwrapApiResponse(response))
}
export function startContainerCustoms(loadingPlanId: string, expectedVersion: number) {
return action(loadingPlanId, 'start', expectedVersion)
}
export function completeAllBuyContainerCustoms(loadingPlanId: string, expectedVersion: number) {
return action(loadingPlanId, 'complete-all-buy', expectedVersion)
}
export function submitContainerCustomsToBroker(loadingPlanId: string, expectedVersion: number) {
return action(loadingPlanId, 'submit-to-broker', expectedVersion, '已将客户确认回执发送给报关行')
}
async function uploadFile(documentId: string, fileValue: File, expectedVersion: number, kind: 'receipt' | 'final') {
2026-08-28 16:54:02 +08:00
const body = new FormData()
body.append('file', fileValue)
const response = await http.post<ApiEnvelope<ContainerCustomsDocument>, ApiEnvelope<ContainerCustomsDocument>>(
`${basePath}/documents/${encodeURIComponent(documentId)}/${kind}/upload`,
2026-08-28 16:54:02 +08:00
body,
{ params: { expectedVersion }, headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': operationId(`container-customs-${kind}`) } },
2026-08-28 16:54:02 +08:00
)
const result = unwrapApiResponse(response)
return { ...result, documentId: String(result.documentId), tenantId: String(result.tenantId), source: file(result.source)!, receipt: file(result.receipt), finalDocument: file(result.finalDocument) }
}
export function uploadContainerCustomsReceipt(documentId: string, fileValue: File, expectedVersion: number) {
return uploadFile(documentId, fileValue, expectedVersion, 'receipt')
}
export function uploadContainerCustomsFinal(documentId: string, fileValue: File, expectedVersion: number) {
return uploadFile(documentId, fileValue, expectedVersion, 'final')
2026-08-28 16:54:02 +08:00
}