feat(firstleg): implement container customs workflow

This commit is contained in:
2026-08-28 16:48:50 +08:00
parent bb9673358d
commit 0ea4aeecc9
92 changed files with 3165 additions and 2754 deletions
@@ -0,0 +1,97 @@
export const CONTAINER_CUSTOMS_MODULE = 'phase3-container-customs'
export const EXACT_50_MB = 50 * 1024 * 1024
export const CONTAINER_CUSTOMS_FIXTURE_KINDS = Object.freeze([
'ALL_BUY_DECLARATION',
'ALL_TAX_REFUND',
'MIXED',
])
export const REQUIRED_ROUTE_OPERATIONS = Object.freeze([
'platformDetail',
'merchantDetail',
'buyDeclarationXlsx',
'merchantUploadDocument',
'merchantReplaceDocument',
'merchantUpdateScope',
'merchantDeleteDocument',
'platformStart',
'platformUploadCompletion',
'platformComplete',
'platformDownload',
'merchantDownload',
'outboundState',
'satisfyOutboundPrerequisites',
])
export const CONTAINER_CUSTOMS_MATRIX = Object.freeze([
{ priority: 'P0', fixture: 'ALL_BUY_DECLARATION', scenario: 'LOADED详情与买单XLSX', expected: 'XLSX只含柜内买单实装行,下载窗口可用' },
{ priority: 'P0', fixture: 'ALL_BUY_DECLARATION', scenario: '开始与完成', expected: 'READY_TO_START→DECLARING→COMPLETED,重放返回同一结果' },
{ priority: 'P0', fixture: 'ALL_TAX_REFUND', scenario: '客户多对多资料', expected: '上传、覆盖、改关联、删除均保留版本与客户范围' },
{ priority: 'P0', fixture: 'ALL_TAX_REFUND', scenario: '冻结与逐份完成文件', expected: '开始冻结来源文件和关联;最后一份完成文件后才允许整柜完成' },
{ priority: 'P0', fixture: 'MIXED', scenario: '买单与退税混合', expected: '买单行进入XLSX,退税票逐票被资料覆盖,整柜统一开始/完成' },
{ priority: 'P0', fixture: 'ALL_TAX_REFUND', scenario: '缺资料禁止开始', expected: 'A0409且聚合、文件、关联、历史零漂移' },
{ priority: 'P0', fixture: 'MIXED', scenario: '自动离仓失败恢复', expected: '报关COMPLETED不回滚;待条件满足后后台重试并单次离仓' },
{ priority: 'P1', fixture: 'ALL_TAX_REFUND', scenario: '开始后锁定', expected: '上传、覆盖、改关联、删除全部A0409零漂移' },
{ priority: 'P1', fixture: 'ALL_TAX_REFUND', scenario: '完成文件继承范围', expected: '完成文件只继承冻结关联,不接收客户端范围' },
{ priority: 'P1', fixture: 'MIXED', scenario: '客户隔离与买单隐藏', expected: '客户只见本企业退税资料;买单XLSX及其他客户文件不可见' },
{ priority: 'P1', fixture: 'ALL_TAX_REFUND', scenario: '文件边界', expected: '任意格式允许;50MiB成功;50MiB+1拒绝且零写入' },
{ priority: 'P1', fixture: 'MIXED', scenario: '幂等与并发', expected: '同键同载荷同结果;异载荷/并发冲突A0409且零漂移' },
{ priority: 'P1', fixture: 'MIXED', scenario: '历史与Outbox', expected: '每个真实动作单条历史;自动离仓意图/结果可追踪且无重复' },
{ priority: 'P1', fixture: 'MIXED', scenario: 'Admin与Client权限', expected: '列表/详情/写权限分离,客户下载仅限自身可见文件' },
])
export function validateContainerCustomsFixtures(value, marker) {
if (value?.marker !== marker || !Array.isArray(value?.containers)) return false
const byKind = new Map(value.containers.map((item) => [item?.kind, item]))
return CONTAINER_CUSTOMS_FIXTURE_KINDS.every((kind) => {
const fixture = byKind.get(kind)
return fixture?.marker === marker && fixture?.status === 'LOADED'
&& /^\d+$/.test(String(fixture?.loadingPlanId ?? ''))
&& /^\d+$/.test(String(fixture?.containerBookingId ?? ''))
&& Array.isArray(fixture?.inbounds) && fixture.inbounds.length > 0
&& fixture.inbounds.every((inbound) => /^\d+$/.test(String(inbound?.inboundId ?? ''))
&& /^\d+$/.test(String(inbound?.customerId ?? ''))
&& ['BUY_DECLARATION', 'TAX_REFUND'].includes(inbound?.taxMode))
})
}
export function validateRouteContract(routes) {
return routes?.version != null
&& REQUIRED_ROUTE_OPERATIONS.every((name) => typeof routes?.[name] === 'function')
}
export function stableSnapshot(value) {
const normalize = (current) => {
if (Array.isArray(current)) return current.map(normalize)
if (current != null && typeof current === 'object') {
return Object.fromEntries(Object.keys(current).sort().map((key) => [key, normalize(current[key])]))
}
return current
}
return JSON.stringify(normalize(value))
}
export function zeroDrift(before, after) {
return stableSnapshot(before) === stableSnapshot(after)
}
export function xlsxMagic(bytes) {
return bytes instanceof Uint8Array && bytes.length >= 4
&& bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04
}
export const READ_ONLY_DB_EVIDENCE = Object.freeze([
'fl_customs_container',
'fl_customs_customer_document',
'fl_customs_customer_document_inbound',
'fl_customs_customer_document_version',
'fl_customs_frozen_document_inbound',
'fl_customs_completion_document',
'fl_customs_completion_document_version',
'fl_customs_command',
'fl_customs_container_history',
'fl_outbound_dispatch',
'fl_outbound_dispatch_history',
'fl_event_outbox',
])