98 lines
4.9 KiB
JavaScript
98 lines
4.9 KiB
JavaScript
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',
|
||
|
|
])
|