diff --git a/scripts/admin-test-env-acceptance.mjs b/scripts/admin-test-env-acceptance.mjs index fea49054c..5b55ff983 100644 --- a/scripts/admin-test-env-acceptance.mjs +++ b/scripts/admin-test-env-acceptance.mjs @@ -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 diff --git a/scripts/admin-test-env-modules/inbound-fees-contract.test.mjs b/scripts/admin-test-env-modules/inbound-fees-contract.test.mjs index 7c4bc44a5..d73d32dc8 100644 --- a/scripts/admin-test-env-modules/inbound-fees-contract.test.mjs +++ b/scripts/admin-test-env-modules/inbound-fees-contract.test.mjs @@ -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'/) diff --git a/scripts/admin-test-env-modules/inbound-fees.mjs b/scripts/admin-test-env-modules/inbound-fees.mjs index 7b81403ee..be615f844 100644 --- a/scripts/admin-test-env-modules/inbound-fees.mjs +++ b/scripts/admin-test-env-modules/inbound-fees.mjs @@ -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 } diff --git a/scripts/admin-test-env-modules/inbound-measurement.mjs b/scripts/admin-test-env-modules/inbound-measurement.mjs index 403c03a5e..1326096c1 100644 --- a/scripts/admin-test-env-modules/inbound-measurement.mjs +++ b/scripts/admin-test-env-modules/inbound-measurement.mjs @@ -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, diff --git a/scripts/admin-test-env-modules/manual-state-loading-pda-contract.test.mjs b/scripts/admin-test-env-modules/manual-state-loading-pda-contract.test.mjs index 8443cfcd4..788d65410 100644 --- a/scripts/admin-test-env-modules/manual-state-loading-pda-contract.test.mjs +++ b/scripts/admin-test-env-modules/manual-state-loading-pda-contract.test.mjs @@ -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/) }) diff --git a/scripts/admin-test-env-modules/manual-state-loading-pda.mjs b/scripts/admin-test-env-modules/manual-state-loading-pda.mjs index 6a2a139dc..1255cc298 100644 --- a/scripts/admin-test-env-modules/manual-state-loading-pda.mjs +++ b/scripts/admin-test-env-modules/manual-state-loading-pda.mjs @@ -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' diff --git a/scripts/admin-test-env-modules/manual-state-matrix-contract.test.mjs b/scripts/admin-test-env-modules/manual-state-matrix-contract.test.mjs index 29201174a..94b20a224 100644 --- a/scripts/admin-test-env-modules/manual-state-matrix-contract.test.mjs +++ b/scripts/admin-test-env-modules/manual-state-matrix-contract.test.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([ diff --git a/scripts/admin-test-env-modules/master-data-version-contract.test.mjs b/scripts/admin-test-env-modules/master-data-version-contract.test.mjs index 6b2f0d5f6..637964831 100644 --- a/scripts/admin-test-env-modules/master-data-version-contract.test.mjs +++ b/scripts/admin-test-env-modules/master-data-version-contract.test.mjs @@ -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\)/) +}) diff --git a/scripts/admin-test-env-modules/master-data.mjs b/scripts/admin-test-env-modules/master-data.mjs index bfe308b76..8a2760e92 100644 --- a/scripts/admin-test-env-modules/master-data.mjs +++ b/scripts/admin-test-env-modules/master-data.mjs @@ -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, ''') } -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.map((value, columnIndex) => `${xmlEscape(value)}`).join('')}`).join('') const files = { '[Content_Types].xml': '', @@ -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 diff --git a/scripts/admin-test-env-modules/phase3-container-overview.mjs b/scripts/admin-test-env-modules/phase3-container-overview.mjs index 3baabdd5d..503ddfe26 100644 --- a/scripts/admin-test-env-modules/phase3-container-overview.mjs +++ b/scripts/admin-test-env-modules/phase3-container-overview.mjs @@ -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) diff --git a/scripts/admin-test-env-modules/phase3-container-regression-contract.test.mjs b/scripts/admin-test-env-modules/phase3-container-regression-contract.test.mjs index 160033774..ff083903f 100644 --- a/scripts/admin-test-env-modules/phase3-container-regression-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-container-regression-contract.test.mjs @@ -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/) diff --git a/scripts/admin-test-env-modules/phase3-order-splitting-contract.test.mjs b/scripts/admin-test-env-modules/phase3-order-splitting-contract.test.mjs index 96a8a44ec..14dc2b700 100644 --- a/scripts/admin-test-env-modules/phase3-order-splitting-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-order-splitting-contract.test.mjs @@ -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=') diff --git a/scripts/admin-test-env-modules/phase3-order-splitting.mjs b/scripts/admin-test-env-modules/phase3-order-splitting.mjs index b4249f523..b57902ad2 100644 --- a/scripts/admin-test-env-modules/phase3-order-splitting.mjs +++ b/scripts/admin-test-env-modules/phase3-order-splitting.mjs @@ -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) diff --git a/scripts/admin-test-env-modules/phase3-phase4-producer-fixture-contract.test.mjs b/scripts/admin-test-env-modules/phase3-phase4-producer-fixture-contract.test.mjs index e7129fd93..83d107f6c 100644 --- a/scripts/admin-test-env-modules/phase3-phase4-producer-fixture-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-phase4-producer-fixture-contract.test.mjs @@ -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/) }) diff --git a/scripts/admin-test-env-modules/phase3-warehouse-rbac.mjs b/scripts/admin-test-env-modules/phase3-warehouse-rbac.mjs index dd09e2b4c..e112f93b4 100644 --- a/scripts/admin-test-env-modules/phase3-warehouse-rbac.mjs +++ b/scripts/admin-test-env-modules/phase3-warehouse-rbac.mjs @@ -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') diff --git a/scripts/admin-test-env-modules/phase4-canonical-warehouse-rbac-contract.test.mjs b/scripts/admin-test-env-modules/phase4-canonical-warehouse-rbac-contract.test.mjs index d3997a36c..8bdd57e31 100644 --- a/scripts/admin-test-env-modules/phase4-canonical-warehouse-rbac-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase4-canonical-warehouse-rbac-contract.test.mjs @@ -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' ]) {