test: stabilize full dev acceptance orchestration
This commit is contained in:
@@ -69,7 +69,7 @@ const MODULE_DEPENDENCIES = new Map([
|
||||
['phase3-waiting-for-loading', ['phase3-outbound-dispatch']],
|
||||
['phase3-carrier-schedule-queries', ['phase3-outbound-dispatch']],
|
||||
['phase3-order-splitting', ['phase3-loading-finalize']],
|
||||
['manual-state-matrix', ['phase3-loading-finalize', 'phase3-customs', 'ocean-bookings']],
|
||||
['manual-state-matrix', ['phase3-loading-finalize', 'phase3-customs', 'ocean-bookings', 'phase3-container-documents']],
|
||||
['phase4-carrier-pickup-ready', ['phase3-carrier-schedule-queries', 'phase3-container-documents']],
|
||||
['phase4-tail-order-controls', ['phase4-carrier-pickup-ready']],
|
||||
['phase4-overseas-pda', ['phase4-tail-order-controls']],
|
||||
@@ -1572,7 +1572,7 @@ export class AcceptanceRun {
|
||||
await this.runPhase4ProducerVariant('DROPSHIP-PRIMARY', 'FBA', 'ONE_CLICK_DROPSHIP', true)
|
||||
}
|
||||
if (Number(readiness().fbaContainerCount || 0) < 2) {
|
||||
await this.runPhase4ProducerVariant('FBA-STANDARD', 'FBA', 'STANDARD', false)
|
||||
await this.runPhase4ProducerVariant('FBA-STANDARD', 'FBA', 'STANDARD', true)
|
||||
}
|
||||
if (Number(readiness().overseasContainerCount || 0) < 1) {
|
||||
await this.runPhase4ProducerVariant('OVERSEAS-PRIMARY', 'OVERSEAS', 'ONE_CLICK_DROPSHIP', true)
|
||||
@@ -1583,11 +1583,15 @@ export class AcceptanceRun {
|
||||
const matrix = this.createdResources.phase4InboundProducerFixtures
|
||||
const containers = Array.isArray(matrix?.containers) ? matrix.containers : []
|
||||
const dropshipReady = containers.find((item) => item?.fulfillmentType === 'ONE_CLICK_DROPSHIP')
|
||||
const standardFbaReady = containers.find((item) => item?.destinationType === 'FBA'
|
||||
&& item?.fulfillmentType !== 'ONE_CLICK_DROPSHIP'
|
||||
&& String(item?.resourceKey || '').includes('-P4-FBA-STANDARD:'))
|
||||
const overseasReady = containers.find((item) => item?.destinationType === 'OVERSEAS')
|
||||
const awaitingVariants = containers.filter((item) => ['FBA-STANDARD', 'SECONDARY']
|
||||
const awaitingVariants = containers.filter((item) => ['SECONDARY']
|
||||
.some((label) => String(item?.resourceKey || '').includes(`-P4-${label}:`)))
|
||||
const shippingMatrixReady = dropshipReady?.shippingReady === true
|
||||
&& Boolean(dropshipReady?.carrierScheduleQueryId)
|
||||
&& standardFbaReady?.shippingReady === true && Boolean(standardFbaReady?.carrierScheduleQueryId)
|
||||
&& overseasReady?.shippingReady === true && Boolean(overseasReady?.carrierScheduleQueryId)
|
||||
&& awaitingVariants.length >= 1
|
||||
&& awaitingVariants.every((item) => item.shippingReady === false
|
||||
|
||||
@@ -45,6 +45,8 @@ test('automatic fee adjustment confirms the ten percent tolerance boundary', asy
|
||||
|
||||
test('customer confirms chargeable quantity and manual unit price in one locked version', async () => {
|
||||
const source = await readFile(moduleFile, 'utf8')
|
||||
assert.match(source, /const orderId = id\(fixture\?\.orderId\)/)
|
||||
assert.doesNotMatch(source, /const orderId = id\(context\.createdResources\.orders\?\.orderId\)/)
|
||||
assert.match(source, /Client 读取已推送计费量、对客单价和费用合计/)
|
||||
assert.match(source, /Client 一次确认计费量、对客单价和费用/)
|
||||
assert.match(source, /customerConfirmationStatus === 'CONFIRMED'/)
|
||||
|
||||
@@ -131,7 +131,7 @@ export async function run(context) {
|
||||
&& money(published?.amount) === money(sent?.totalAmount),
|
||||
`status=${sent?.feeStatus || 'missing'}, process=${sent?.nextProcessStatus || 'missing'}, publication=${published?.publicationStatus || 'missing'}`, OPTIONS)
|
||||
|
||||
const orderId = id(context.createdResources.orders?.orderId)
|
||||
const orderId = id(fixture?.orderId)
|
||||
const merchantLoginResponse = await context.request('POST', '/api/auth/login', {
|
||||
loginId: context.options.merchantUsername, password: context.options.merchantPassword,
|
||||
}, '')
|
||||
@@ -260,6 +260,7 @@ export async function run(context) {
|
||||
receivableTotal >= money(published?.amount) + money(manualV2?.amount) && payableTotal >= 0,
|
||||
`receivableTotal=${receivableTotal.toFixed(2)}, freight=${money(published?.amount).toFixed(2)}, manual=${money(manualV2?.amount).toFixed(2)}, payableTotal=${payableTotal.toFixed(2)}`, OPTIONS)
|
||||
|
||||
if (!customerConfirmed?.customerConfirmationStatus) { finish(context, start); return }
|
||||
context.createdResources.inboundFees = { marker: context.marker, tenantId, inboundId,
|
||||
freightFeeId: id(sent.feeId), freightVersion: sent.feeVersion, manualDetailId: id(manualV2.id),
|
||||
manualVersion: manualV2.sourceFeeVersion, customerConfirmationStatus: customerConfirmed.customerConfirmationStatus }
|
||||
|
||||
@@ -1097,7 +1097,9 @@ export async function run(context) {
|
||||
|
||||
context.createdResources.inboundMeasurement = {
|
||||
sealedCartonMarks, sealedSourcePalletNos,
|
||||
marker: context.marker, tenantId, inboundId, measurementId: asId(draft.measurementId),
|
||||
marker: context.marker, tenantId, inboundId,
|
||||
orderId: asId(summary?.orderId || context.createdResources?.orders?.orderId),
|
||||
measurementId: asId(draft.measurementId),
|
||||
boxCode, finalStatus: finalMeasurement?.measurementStatus || null,
|
||||
processStatus: finalMeasurement?.processStatus || null,
|
||||
palletCount: weightEvidence.count, actualGrossWeightKg: weightEvidence.grossWeightKg,
|
||||
|
||||
@@ -57,17 +57,16 @@ test('fails closed when invoked outside the manual matrix wrapper', () => {
|
||||
assert.match(source, /missing=manualStateMatrix\.pages/)
|
||||
})
|
||||
|
||||
test('covers clearance through its public API and keeps customs on the dedicated customs-v1 flow', () => {
|
||||
assert.match(source, /new FormData\(\)/)
|
||||
assert.match(source, /form\.append\('file'/)
|
||||
assert.match(source, /form\.append\('expectedVersion'/)
|
||||
assert.match(source, /form\.append\('operationReason'/)
|
||||
assert.match(source, /'Idempotency-Key'/)
|
||||
assert.match(source, /clearancePath\}\/document\/upload/)
|
||||
assert.match(source, /clearancePath\}\/complete/)
|
||||
test('reuses the departed-container clearance action verified by the dedicated module', () => {
|
||||
assert.match(source, /createdResources\?\.phase3ContainerDocuments/)
|
||||
assert.match(source, /documentIds\?\.DESTINATION_CLEARANCE/)
|
||||
assert.match(source, /本轮离仓后已完成的目的港清关动作/)
|
||||
assert.match(source, /GET', `\$\{CONTAINERS\}\/\$\{containerId\}\/clearance`/)
|
||||
assert.match(source, /completed\?\.documentType === 'DESTINATION_CLEARANCE'/)
|
||||
assert.match(source, /Boolean\(completed\?\.completedAt\)/)
|
||||
assert.match(source, /matrix\.pages\.containerLoading\.actions/)
|
||||
assert.doesNotMatch(source, /CUSTOMS_DECLARATION|loading-customs|customsPath/)
|
||||
assert.match(source, /tasks\/\$\{taskId\}\/evidence/)
|
||||
assert.doesNotMatch(source, /internal\/file|business-files/)
|
||||
assert.doesNotMatch(source, /\{\s*fileId\s*:/)
|
||||
assert.doesNotMatch(source, /clearance\/document\/upload/)
|
||||
})
|
||||
|
||||
@@ -19,37 +19,6 @@ function operationId(context, label) {
|
||||
return `${context.marker}-manual-loading-${label}-${crypto.randomUUID()}`.slice(0, 128)
|
||||
}
|
||||
|
||||
async function post(context, path, body, label) {
|
||||
return context.request('POST', path, body, context.token,
|
||||
{ 'Idempotency-Key': operationId(context, label) })
|
||||
}
|
||||
|
||||
async function uploadDocument(context, path, expectedVersion, operationReason, label) {
|
||||
const form = new FormData()
|
||||
form.append('file', new Blob([`${context.marker}-${label}`], { type: 'text/plain' }), `${label}.txt`)
|
||||
form.append('expectedVersion', String(expectedVersion))
|
||||
if (operationReason) form.append('operationReason', operationReason)
|
||||
try {
|
||||
const response = await fetch(`${context.options.gateway}${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json', Authorization: `Bearer ${context.token}`,
|
||||
'Idempotency-Key': operationId(context, `${label}-upload`),
|
||||
},
|
||||
body: form,
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
})
|
||||
const raw = await response.text()
|
||||
let body = null
|
||||
try { body = raw ? JSON.parse(raw) : null } catch { /* bounded raw evidence */ }
|
||||
return { method: 'POST', path, http: response.status, body, raw: body ? null : raw.slice(0, 500),
|
||||
requestId: body?.requestId || response.headers.get('x-request-id'),
|
||||
traceId: body?.traceId || response.headers.get('x-trace-id') }
|
||||
} catch (error) {
|
||||
return { method: 'POST', path, http: 0, body: null, raw: String(error), requestId: null, traceId: null }
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureCompletionEvidence(context, chain, label) {
|
||||
const taskId = id(chain.task?.id)
|
||||
const warehouseId = id(chain.source?.warehouseId)
|
||||
@@ -351,34 +320,26 @@ function fixture(value, status, detailPath, extra = {}) {
|
||||
return { id: id(value), status, detailPath, ...extra }
|
||||
}
|
||||
|
||||
async function completeSimpleDocuments(context, chain, matrix) {
|
||||
const reason = `${context.marker} 人工页面动作验收`
|
||||
const clearancePath = `${CONTAINERS}/${chain.containerId}/clearance`
|
||||
const clearanceUpload = await uploadDocument(context, `${clearancePath}/document/upload`, 0,
|
||||
reason, 'container-clearance')
|
||||
const clearanceDocument = context.apiSuccess(MODULE, '货柜公开接口上传目的港清关资料',
|
||||
clearanceUpload, '', OPTIONS)
|
||||
context.assert(MODULE, '货柜清关上传返回真实文件事实', clearanceUpload,
|
||||
Boolean(clearanceDocument?.id) && Boolean(clearanceDocument?.fileId)
|
||||
&& clearanceDocument?.documentType === 'DESTINATION_CLEARANCE'
|
||||
&& /^[0-9a-f]{64}$/i.test(String(clearanceDocument?.fileHash || ''))
|
||||
&& !clearanceDocument?.completedAt,
|
||||
`documentId=${id(clearanceDocument?.id) || 'missing'}, version=${clearanceDocument?.version ?? 'missing'}`, OPTIONS)
|
||||
if (!clearanceDocument?.id || !clearanceDocument?.fileId || !Number.isInteger(Number(clearanceDocument?.version))) return
|
||||
const clearanceCompleteResponse = await post(context, `${clearancePath}/complete`, {
|
||||
expectedVersion: Number(clearanceDocument?.version), operationReason: reason,
|
||||
}, 'container-clearance-complete')
|
||||
const clearanceCompleted = context.apiSuccess(MODULE, '货柜点击完成目的港清关',
|
||||
clearanceCompleteResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '货柜清关完成事实可回读', clearanceCompleteResponse,
|
||||
id(clearanceCompleted?.id) === id(clearanceDocument?.id) && Boolean(clearanceCompleted?.completedAt)
|
||||
&& Number(clearanceCompleted?.version) === Number(clearanceDocument?.version) + 1,
|
||||
`documentId=${id(clearanceCompleted?.id) || 'missing'}, completedAt=${clearanceCompleted?.completedAt || 'missing'}`, OPTIONS)
|
||||
async function registerClearanceAction(context, matrix) {
|
||||
const documentsFixture = context.createdResources?.phase3ContainerDocuments
|
||||
const containerId = id(documentsFixture?.containerBookingId)
|
||||
const expectedDocumentId = id(documentsFixture?.documentIds?.DESTINATION_CLEARANCE)
|
||||
context.assert(MODULE, '复用本轮离仓后已完成的目的港清关动作', null,
|
||||
documentsFixture?.marker === context.marker && Boolean(containerId && expectedDocumentId),
|
||||
`containerBookingId=${containerId || 'missing'}, documentId=${expectedDocumentId || 'missing'}`, OPTIONS)
|
||||
if (!containerId || !expectedDocumentId || documentsFixture?.marker !== context.marker) return
|
||||
|
||||
matrix.pages.containerLoading.actions = { clearance: fixture(chain.containerId, 'COMPLETED',
|
||||
`/container-loading/${chain.containerId}/smart-loading`, {
|
||||
documentId: id(clearanceCompleted?.id), documentVersion: Number(clearanceCompleted?.version),
|
||||
completedAt: clearanceCompleted?.completedAt || null,
|
||||
const response = await context.request('GET', `${CONTAINERS}/${containerId}/clearance`)
|
||||
const completed = context.apiSuccess(MODULE, '回读目的港清关动作验收事实', response, '', OPTIONS)
|
||||
context.assert(MODULE, '目的港清关动作保持完成和版本事实', response,
|
||||
id(completed?.id) === expectedDocumentId && completed?.documentType === 'DESTINATION_CLEARANCE'
|
||||
&& Boolean(completed?.completedAt) && Number.isInteger(Number(completed?.version)),
|
||||
`documentId=${id(completed?.id) || 'missing'}, completedAt=${completed?.completedAt || 'missing'}, version=${completed?.version ?? 'missing'}`, OPTIONS)
|
||||
|
||||
matrix.pages.containerLoading.actions = { clearance: fixture(containerId, 'COMPLETED',
|
||||
`/container-loading/${containerId}/smart-loading`, {
|
||||
documentId: id(completed?.id), documentVersion: Number(completed?.version),
|
||||
completedAt: completed?.completedAt || null,
|
||||
}) }
|
||||
}
|
||||
|
||||
@@ -464,6 +425,6 @@ export async function run(context, matrix = context.createdResources.manualState
|
||||
id(listDetail?.summary?.planId) === listFixture.id && listDetail?.summary?.status === 'COMPLETED',
|
||||
`planId=${listFixture.id}, actual=${listDetail?.summary?.status || 'missing'}`, OPTIONS)
|
||||
|
||||
await completeSimpleDocuments(context, completed, matrix)
|
||||
await registerClearanceAction(context, matrix)
|
||||
}
|
||||
import { iso6346ContainerNumber } from './iso6346-container-number.mjs'
|
||||
|
||||
@@ -26,6 +26,11 @@ test('focused matrix execution injects real container lifecycle prerequisites an
|
||||
'phase3-evidence-gate',
|
||||
'phase3-loading-finalize',
|
||||
'phase3-customs',
|
||||
'phase3-port-schedules',
|
||||
'truck-appointments',
|
||||
'service-center-flow',
|
||||
'phase3-outbound-dispatch',
|
||||
'phase3-container-documents',
|
||||
'manual-state-matrix',
|
||||
]
|
||||
assert.deepEqual(normalizeModuleOrder(['order-inbound-status-fixtures', 'manual-state-matrix']), expected)
|
||||
@@ -47,6 +52,11 @@ test('focused splitting and manual groups resolve producers before consumers wit
|
||||
'phase3-loading-finalize',
|
||||
'phase3-order-splitting',
|
||||
'phase3-customs',
|
||||
'phase3-port-schedules',
|
||||
'truck-appointments',
|
||||
'service-center-flow',
|
||||
'phase3-outbound-dispatch',
|
||||
'phase3-container-documents',
|
||||
'manual-state-matrix',
|
||||
]
|
||||
assert.deepEqual(normalizeModuleOrder([
|
||||
|
||||
@@ -10,3 +10,9 @@ test('non-versioned master data stays explicit without making the full gate perm
|
||||
assert.match(source, /非版本化主数据不伪造旧version请求/)
|
||||
assert.match(source, /phase4-overseas-master-data/)
|
||||
})
|
||||
|
||||
test('warehouse import fixture tracks the current version two template and internal code column', () => {
|
||||
assert.match(source, /仓库编码\*', '内部编号', '仓库名称/)
|
||||
assert.match(source, /sheet: '仓库', templateVersion: '2'/)
|
||||
assert.match(source, /xlsxBytes\(fixture\.headers, fixture\.values, fixture\.sheet, fixture\.templateVersion\)/)
|
||||
})
|
||||
|
||||
@@ -31,8 +31,8 @@ function u16(value) { return Uint8Array.from([value & 255, (value >>> 8) & 255])
|
||||
function u32(value) { return Uint8Array.from([value & 255, (value >>> 8) & 255, (value >>> 16) & 255, (value >>> 24) & 255]) }
|
||||
function concat(...parts) { const result = new Uint8Array(parts.reduce((sum, item) => sum + item.length, 0)); let offset = 0; for (const part of parts) { result.set(part, offset); offset += part.length } return result }
|
||||
function xmlEscape(value) { return String(value ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''') }
|
||||
function xlsxBytes(headers, values, sheetName) {
|
||||
const rows = [['模板版本', '1'], ['说明', 'Admin HTTP acceptance fixture'], headers, values]
|
||||
function xlsxBytes(headers, values, sheetName, templateVersion = '1') {
|
||||
const rows = [['模板版本', templateVersion], ['说明', 'Admin HTTP acceptance fixture'], headers, values]
|
||||
const cells = rows.map((row, rowIndex) => `<row r="${rowIndex + 1}">${row.map((value, columnIndex) => `<c r="${String.fromCharCode(65 + columnIndex)}${rowIndex + 1}" t="inlineStr"><is><t>${xmlEscape(value)}</t></is></c>`).join('')}</row>`).join('')
|
||||
const files = {
|
||||
'[Content_Types].xml': '<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/></Types>',
|
||||
@@ -189,7 +189,7 @@ export async function run(context) {
|
||||
['往来单位', '/api/platform/v1/partners/import-template'],
|
||||
]
|
||||
const importFixtures = {
|
||||
仓库: { headers: ['仓库编码*', '仓库名称', '仓库类型', '国家二字码', '省/州', '城市*', '区/县', '详细地址*', '邮政编码', '时区', '状态', '联系人', '联系电话', '联系邮箱', '备注'], values: [`IMW${suffix}`, `导入验收仓-${suffix}`, 'DOMESTIC', 'CN', '广东省', '深圳市', '南山区', `导入地址-${context.marker}`, '518000', 'Asia/Shanghai', 'ACTIVE', '导入联系人', '13800000011', `im-${suffix}@example.com`, context.marker], sheet: '仓库' },
|
||||
仓库: { headers: ['仓库编码*', '内部编号', '仓库名称', '仓库类型', '国家二字码', '省/州', '城市*', '区/县', '详细地址*', '邮政编码', '时区', '状态', '联系人', '联系电话', '联系邮箱', '备注'], values: [`IMW${suffix}`, '', `导入验收仓-${suffix}`, 'DOMESTIC', 'CN', '广东省', '深圳市', '南山区', `导入地址-${context.marker}`, '518000', 'Asia/Shanghai', 'ACTIVE', '导入联系人', '13800000011', `im-${suffix}@example.com`, context.marker], sheet: '仓库', templateVersion: '2' },
|
||||
快递公司: { headers: ['快递公司编码*', '快递公司名称*', '客服电话', '官方网站', '轨迹查询地址模板', '状态*', '备注'], values: [`IME${suffix}`, `导入快递-${suffix}`, '4000000011', 'https://example.com', 'https://example.com/{trackingNo}', 'ACTIVE', context.marker], sheet: '快递公司' },
|
||||
往来单位: { headers: ['单位编码*', '单位名称*', '单位类型*', '联系人*', '联系电话*', '联系邮箱', '船司标准编码', '国家二字码*', '省/州', '城市*', '区/县', '详细地址*', '邮政编码', '状态*', '备注'], values: [`IMP${suffix}`, `导入船公司-${suffix}`, 'CARRIER', '导入联系人', '13800000012', `ip-${suffix}@example.com`, 'MATSON', 'CN', '广东省', '深圳市', '南山区', `导入地址-${context.marker}`, '518000', 'ACTIVE', context.marker], sheet: '往来单位' },
|
||||
}
|
||||
@@ -199,7 +199,7 @@ export async function run(context) {
|
||||
`bytes=${response.bytes?.length ?? 0}, contentType=${response.contentType || 'missing'}`)
|
||||
if (xlsxOk(response)) {
|
||||
const fixture = importFixtures[name]
|
||||
const fixtureBytes = xlsxBytes(fixture.headers, fixture.values, fixture.sheet)
|
||||
const fixtureBytes = xlsxBytes(fixture.headers, fixture.values, fixture.sheet, fixture.templateVersion)
|
||||
const importPath = `${path.replace('/import-template', '/import')}?tenantId=0`
|
||||
const first = await uploadTemplate(context, importPath, fixtureBytes, `${context.marker}-${name}.xlsx`)
|
||||
const firstData = first.body?.data
|
||||
|
||||
@@ -94,14 +94,17 @@ export async function run(context) {
|
||||
|
||||
for (const documentType of ['DELIVERY_ORDER']) {
|
||||
const response = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(containerId)}/documents/${documentType}`)
|
||||
const data = context.apiSuccess(MODULE, `查询 ${documentType} 单证`, response, `documentType=${documentType}`, OPTIONS)
|
||||
context.assert(MODULE, `${documentType} 单证归属货柜`, response,
|
||||
data == null || id(data.containerId || containerId) === containerId,
|
||||
`containerBookingId=${containerId}`, OPTIONS)
|
||||
context.assert(MODULE, `国内未出仓时 ${documentType} 不生成国外工作台数据`, response,
|
||||
response.http === 200 && String(response?.body?.code) === 'A0404'
|
||||
&& String(response?.body?.message || '').includes('国外工作台货柜不存在'),
|
||||
`HTTP=${response.http}, code=${response?.body?.code ?? 'missing'}, documentType=${documentType}`, OPTIONS)
|
||||
}
|
||||
|
||||
const clearance = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(containerId)}/clearance`)
|
||||
context.apiSuccess(MODULE, '查询目的港清关资料', clearance, `containerBookingId=${containerId}`, OPTIONS)
|
||||
context.assert(MODULE, '国内未出仓时目的港清关不生成国外工作台数据', clearance,
|
||||
clearance.http === 200 && String(clearance?.body?.code) === 'A0404'
|
||||
&& String(clearance?.body?.message || '').includes('国外工作台货柜不存在'),
|
||||
`HTTP=${clearance.http}, code=${clearance?.body?.code ?? 'missing'}, containerBookingId=${containerId}`, OPTIONS)
|
||||
const customs = await context.request('GET', `/api/platform/v1/first-leg/loading-lists/${encodeURIComponent(planId)}/customs?warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
if (fixture?.finalStatus === 'LOADED') {
|
||||
context.apiSuccess(MODULE, '查询出口报关资料', customs, `loadingPlanId=${planId}, warehouseId=${warehouseId}`, OPTIONS)
|
||||
|
||||
@@ -52,6 +52,12 @@ test('overview regression requires workflowStatus and distinguishes 100 percent
|
||||
assert.doesNotMatch(matrix, /PDA overview `workflowStatus` 缺失[\s\S]*BLOCKED_CONTRACT/)
|
||||
})
|
||||
|
||||
test('pre-departure overview rejects destination workbench documents explicitly', () => {
|
||||
assert.match(overviewAcceptance, /国内未出仓时 \$\{documentType\} 不生成国外工作台数据/)
|
||||
assert.match(overviewAcceptance, /国内未出仓时目的港清关不生成国外工作台数据/)
|
||||
assert.match(overviewAcceptance, /国外工作台货柜不存在/)
|
||||
})
|
||||
|
||||
test('active overview photos download through controlled paths while void history has no download entry', () => {
|
||||
assert.match(overviewOpenApi, /\/api\/platform\/v1\/containers\/\{id\}\/pda\/photos\/\{fileId\}\/download/)
|
||||
assert.match(overviewAcceptance, /\['evidenceId', 'fileId', 'status'\]\.every/)
|
||||
|
||||
@@ -64,6 +64,17 @@ test('PDA opens only after linking a released requirement and uses stable plan i
|
||||
assert.doesNotMatch(source, /push-preload/)
|
||||
})
|
||||
|
||||
test('plan confirmation prepares the remainder initial fee first', () => {
|
||||
const feeIndex = source.indexOf('ensureInitialCalculated(context')
|
||||
const confirmIndex = source.indexOf("`/api/platform/v1/first-leg/loading-plans/${encodeURIComponent(planId)}/confirm`")
|
||||
assert.match(source, /import \{ ensureInitialCalculated \} from '\.\/inbound-fees\.mjs'/)
|
||||
assert.ok(feeIndex >= 0 && confirmIndex > feeIndex)
|
||||
assert.match(source, /inboundId: id\(candidate\.inboundId\)/)
|
||||
assert.match(source, /finalStatus: 'APPROVED'/)
|
||||
assert.match(source, /processStatus: 'WAIT_SPLIT'/)
|
||||
assert.match(source, /if \(!initialFee\) return null/)
|
||||
})
|
||||
|
||||
test('each remainder settles only through inventory snapshot, atomic batch and explicit complete', () => {
|
||||
const claimIndex = source.indexOf("`${PDA}/tasks/${taskId}/claim`")
|
||||
const snapshotIndex = source.indexOf('inventory-snapshot?warehouseId=')
|
||||
|
||||
@@ -9,6 +9,7 @@ const PDA = '/api/platform/v1/first-leg/pda-loading'
|
||||
|
||||
import { oceanBookingDraft } from './ocean-booking-fixture.mjs'
|
||||
import { iso6346ContainerNumber } from './iso6346-container-number.mjs'
|
||||
import { ensureInitialCalculated } from './inbound-fees.mjs'
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
|
||||
@@ -242,6 +243,14 @@ async function loadOneFragment(context, state) {
|
||||
`orderId=${sourceOrderId}, available=${candidate?.availablePackageCount ?? 'missing'}`, OPTIONS)
|
||||
if (!candidate?.inboundId || !candidate?.inboundDetailId) return null
|
||||
|
||||
const initialFee = await ensureInitialCalculated(context, {
|
||||
tenantId,
|
||||
inboundId: id(candidate.inboundId),
|
||||
finalStatus: 'APPROVED',
|
||||
processStatus: 'WAIT_SPLIT',
|
||||
}, OPTIONS)
|
||||
if (!initialFee) return null
|
||||
|
||||
const createPlan = await request(context, 'GET', `${SMART}/plans/by-booking/${containerId}`)
|
||||
const plan = context.apiSuccess(MODULE, `读取 ${sourceOrderNo} 模拟柜自动创建的装柜计划`, createPlan, '', OPTIONS)
|
||||
const planId = id(plan?.id)
|
||||
|
||||
@@ -118,7 +118,7 @@ test('measurement and loading pass destination identity into the LOADED producer
|
||||
test('only PDA-operated variants traverse real outbound and carrier nodes before overseas receiving', () => {
|
||||
const entry = read('../admin-test-env-acceptance.mjs')
|
||||
assert.match(entry, /runPhase4ProducerVariant\('OVERSEAS-PRIMARY', 'OVERSEAS', 'ONE_CLICK_DROPSHIP', true\)/)
|
||||
assert.match(entry, /runPhase4ProducerVariant\('FBA-STANDARD', 'FBA', 'STANDARD', false\)/)
|
||||
assert.match(entry, /runPhase4ProducerVariant\('FBA-STANDARD', 'FBA', 'STANDARD', true\)/)
|
||||
assert.match(entry, /runPhase4ProducerVariant\('SECONDARY', 'FBA', 'STANDARD', false\)/)
|
||||
const start = entry.indexOf('if (pickupRequired && producerKey)')
|
||||
const end = entry.indexOf('} finally {', start)
|
||||
@@ -147,6 +147,7 @@ test('only PDA-operated variants traverse real outbound and carrier nodes before
|
||||
assert.match(entry, /overseasReady\?\.shippingReady === true/)
|
||||
assert.match(entry, /awaitingVariants\.length >= 1/)
|
||||
assert.doesNotMatch(entry, /awaitingVariants\.length === 2/)
|
||||
assert.match(entry, /standardFbaReady\?\.shippingReady === true/)
|
||||
assert.match(entry, /awaitingVariants\.every\(\(item\) => item\.shippingReady === false/)
|
||||
})
|
||||
|
||||
|
||||
@@ -24,8 +24,10 @@ const PHASE5_OPERATOR_CODES = [
|
||||
'platform:overseas-warehouse:delivery:list',
|
||||
'platform:overseas-warehouse:exception-order:list',
|
||||
'platform:overseas-warehouse:exception-order:create',
|
||||
'platform:overseas-warehouse:exception-order:process',
|
||||
'platform:overseas-warehouse:delivery:return',
|
||||
'platform:overseas-warehouse:delivery:status',
|
||||
'platform:overseas-warehouse:delivery:documents',
|
||||
'platform:overseas-warehouse:customer-visit:list',
|
||||
'platform:overseas-warehouse:customer-visit:edit',
|
||||
]
|
||||
@@ -88,7 +90,7 @@ export async function run(context) {
|
||||
context.assert(MODULE, '操作员具备国内入仓权限、四期只读权限与五期履约权限', rolesResponse,
|
||||
operatorCodes.has('platform:firstleg:inbound:loading:view')
|
||||
&& operatorCodes.has('platform:firstleg:inbound:loading:edit')
|
||||
&& operatorCodes.size === 29
|
||||
&& operatorCodes.size === 31
|
||||
&& PHASE4_READ_CODES.every((code) => operatorCodes.has(code))
|
||||
&& PHASE5_OPERATOR_CODES.every((code) => operatorCodes.has(code))
|
||||
&& !operatorCodes.has('platform:firstleg:inbound:loading:complete')
|
||||
|
||||
@@ -26,7 +26,7 @@ test('warehouse operator and tally use the exact phase-four and phase-five permi
|
||||
assert.match(iamSql, new RegExp(`'WAREHOUSE_OPERATOR','${code}'`))
|
||||
assert.match(iamSql, new RegExp(`'WAREHOUSE_TALLY','${code}'`))
|
||||
}
|
||||
assert.match(acceptance, /operatorCodes\.size === 29/)
|
||||
assert.match(acceptance, /operatorCodes\.size === 31/)
|
||||
assert.match(acceptance, /tallyCodes\.size === 27/)
|
||||
assert.match(acceptance, /PHASE4_WRITE_SUFFIXES/)
|
||||
assert.match(iamSql, /assert_phase4_warehouse_role_minimum/)
|
||||
@@ -47,8 +47,10 @@ test('warehouse roles include the exact phase-five fulfillment responsibilities'
|
||||
'platform:overseas-warehouse:delivery:list',
|
||||
'platform:overseas-warehouse:exception-order:list',
|
||||
'platform:overseas-warehouse:exception-order:create',
|
||||
'platform:overseas-warehouse:exception-order:process',
|
||||
'platform:overseas-warehouse:delivery:return',
|
||||
'platform:overseas-warehouse:delivery:status',
|
||||
'platform:overseas-warehouse:delivery:documents',
|
||||
'platform:overseas-warehouse:customer-visit:list',
|
||||
'platform:overseas-warehouse:customer-visit:edit'
|
||||
]) {
|
||||
|
||||
Reference in New Issue
Block a user