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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
completion: ContainerCustomsFile | null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ContainerCustomsView {
|
|
|
|
|
aggregateId: string
|
|
|
|
|
loadingPlanId: string
|
|
|
|
|
containerBookingId: string
|
|
|
|
|
planNo: string
|
|
|
|
|
cabinetNo: string
|
|
|
|
|
containerNo: string | null
|
|
|
|
|
status: ContainerCustomsStatus
|
2026-09-10 14:02:37 +08:00
|
|
|
hasActualCargo: boolean
|
2026-08-28 16:54:02 +08:00
|
|
|
version: number
|
|
|
|
|
taxRefundCount: number
|
|
|
|
|
coveredTaxRefundCount: number
|
|
|
|
|
inbounds: ContainerCustomsInbound[]
|
|
|
|
|
documents: ContainerCustomsDocument[]
|
|
|
|
|
startedAt: string | null
|
|
|
|
|
completedAt: string | null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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' | 'completion'> & { documentId: string | number; tenantId: string | number; source: ContainerCustomsFile; completion: 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) })),
|
|
|
|
|
documents: value.documents.map((item) => ({ ...item, documentId: String(item.documentId), tenantId: String(item.tenantId), source: file(item.source)!, completion: file(item.completion) })),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getContainerCustoms(loadingPlanId: string) {
|
|
|
|
|
const response = await http.get<ApiEnvelope<RawContainerCustomsView>, ApiEnvelope<RawContainerCustomsView>>(`${basePath}/${encodeURIComponent(loadingPlanId)}`)
|
|
|
|
|
return normalize(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-29 10:19:35 +08:00
|
|
|
export function downloadContainerCustomsDocument(documentId: string) {
|
|
|
|
|
return http.get<Blob, Blob>(`${basePath}/documents/${encodeURIComponent(documentId)}/download`, { responseType: 'blob' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function downloadContainerCustomsCompletion(documentId: string) {
|
|
|
|
|
return http.get<Blob, Blob>(`${basePath}/documents/${encodeURIComponent(documentId)}/completion/download`, { responseType: 'blob' })
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-28 16:54:02 +08:00
|
|
|
async function action(loadingPlanId: string, name: 'start' | 'complete-all-buy', expectedVersion: number, remark?: string) {
|
|
|
|
|
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 async function uploadContainerCustomsCompletion(documentId: string, fileValue: File, expectedVersion: number, finalConfirmation: boolean) {
|
|
|
|
|
const body = new FormData()
|
|
|
|
|
body.append('file', fileValue)
|
|
|
|
|
const response = await http.post<ApiEnvelope<ContainerCustomsDocument>, ApiEnvelope<ContainerCustomsDocument>>(
|
|
|
|
|
`${basePath}/documents/${encodeURIComponent(documentId)}/completion/upload`,
|
|
|
|
|
body,
|
|
|
|
|
{ params: { expectedVersion, finalConfirmation }, headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': operationId('container-customs-completion') } },
|
|
|
|
|
)
|
|
|
|
|
const result = unwrapApiResponse(response)
|
|
|
|
|
return { ...result, documentId: String(result.documentId), tenantId: String(result.tenantId), source: file(result.source)!, completion: file(result.completion) }
|
|
|
|
|
}
|