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 } export interface ContainerCustomsReceiptVersion { version: number file: ContainerCustomsFile operatorName?: string | null createdAt: string } export type ContainerCustomsConfirmationAction = 'CONFIRM' | 'REJECT' export interface ContainerCustomsConfirmationHistory { id: string receiptVersion: number action: ContainerCustomsConfirmationAction reason: string | null operatorName: string createdAt: string } export interface ContainerCustomsBrokerSubmission { id: string customsContainerId: string loadingPlanId: string batchNo: string status: 'SENT' sourceVersion: number sentByName: string sentAt: string version: number } 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 receiptStatus?: 'PENDING' | 'CONFIRMED' | 'REJECTED' | null receiptRejectReason?: string | null finalDocument: ContainerCustomsFile | null receiptHistory?: ContainerCustomsReceiptVersion[] confirmationHistory: ContainerCustomsConfirmationHistory[] } export interface ContainerCustomsView { aggregateId: string loadingPlanId: string containerBookingId: string planNo: string cabinetNo: string containerNo: string | null status: ContainerCustomsStatus hasActualCargo: boolean version: number taxRefundCount: number coveredTaxRefundCount: number actualBoxCount: number inbounds: ContainerCustomsInbound[] documents: ContainerCustomsDocument[] brokerSubmission: ContainerCustomsBrokerSubmission | null startedAt: string | null completedAt: string | null brokerSubmittedAt?: string | null } 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[] } interface RawContainerCustomsView extends Omit { aggregateId: string | number loadingPlanId: string | number containerBookingId: string | number version: string | number inbounds: Array & { inboundId: string | number; orderId: string | number; tenantId: string | number }> documents: Array & { documentId: string | number; tenantId: string | number; source: ContainerCustomsFile; receipt: ContainerCustomsFile | null; finalDocument: ContainerCustomsFile | null }> } 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) })), brokerSubmission: value.brokerSubmission ? { ...value.brokerSubmission, id: String(value.brokerSubmission.id), customsContainerId: String(value.brokerSubmission.customsContainerId), loadingPlanId: String(value.brokerSubmission.loadingPlanId), sourceVersion: Number(value.brokerSubmission.sourceVersion), version: Number(value.brokerSubmission.version), } : null, 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)! })), confirmationHistory: (item.confirmationHistory || []).map((history) => ({ ...history, id: String(history.id), receiptVersion: Number(history.receiptVersion), reason: history.reason ?? null })), })), } } 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>(`${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>(`${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>(`${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>(`${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>(`${basePath}/clearance-results/${encodeURIComponent(result.id)}/resolve`, { expectedVersion: batchVersion, resolution: resolution.trim() }, { headers: { 'Idempotency-Key': operationId('customs-clearance-result-resolve') } }) return normalizeClearanceResult(unwrapApiResponse(response)) } export async function getContainerCustoms(loadingPlanId: string) { const response = await http.get, ApiEnvelope>(`${basePath}/${encodeURIComponent(loadingPlanId)}`) const value = unwrapApiResponse(response) return value ? normalize(value) : null } export function downloadContainerCustomsDocument(documentId: string) { return http.get(`${basePath}/documents/${encodeURIComponent(documentId)}/download`, { responseType: 'blob' }) } export function downloadContainerCustomsReceipt(documentId: string) { return http.get(`${basePath}/documents/${encodeURIComponent(documentId)}/receipt/download`, { responseType: 'blob' }) } export function downloadContainerCustomsFinal(documentId: string) { return http.get(`${basePath}/documents/${encodeURIComponent(documentId)}/final/download`, { responseType: 'blob' }) } async function action(loadingPlanId: string, name: 'start' | 'send-to-broker' | 'submit-to-broker' | 'complete-all-buy', expectedVersion: number, remark?: string) { const response = await http.post, ApiEnvelope>( `${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 sendContainerCustomsToBroker(loadingPlanId: string, expectedVersion: number) { return action(loadingPlanId, 'send-to-broker', 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') { const body = new FormData() body.append('file', fileValue) const response = await http.post, ApiEnvelope>( `${basePath}/documents/${encodeURIComponent(documentId)}/${kind}/upload`, body, { params: { expectedVersion }, headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': operationId(`container-customs-${kind}`) } }, ) 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') }