优化
This commit is contained in:
@@ -5,8 +5,8 @@ const BASE = '/api/platform/v1/fee-items'
|
||||
const BASELINE_COUNT = 111
|
||||
const DOMESTIC_COUNT = 84
|
||||
const INTERNATIONAL_COUNT = 27
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin'
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!'
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
function records(value) { return Array.isArray(value?.records) ? value.records : [] }
|
||||
|
||||
@@ -7,10 +7,12 @@ import { fileURLToPath } from 'node:url'
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const source = fs.readFileSync(path.join(here, 'finance-demo-fixtures.mjs'), 'utf8')
|
||||
|
||||
test('演示数据只消费本轮固定 marker 和客户 10001', () => {
|
||||
test('演示数据只消费本轮固定 marker 和动态正式客户 fixture', () => {
|
||||
assert.match(source, /const MARKER = 'FIN-DEMO-20260812'/)
|
||||
assert.match(source, /fixture\.marker !== runMarker/)
|
||||
assert.match(source, /fixture\.customerId !== '10001'/)
|
||||
assert.match(source, /customerId !== id\(qualificationFixture\?\.tenantId\)/)
|
||||
assert.match(source, /context\.context\.merchantQualificationFixture/)
|
||||
assert.doesNotMatch(source, /customerId:\s*['"]?10001|accountId=10001/)
|
||||
assert.match(source, /const runMarker = `\$\{MARKER\}-/)
|
||||
assert.match(source, /禁止借历史来源/)
|
||||
assert.doesNotMatch(source, /DELETE|cleanup|remove/i)
|
||||
@@ -41,8 +43,8 @@ test('所有财务写操作使用 marker 幂等键并上传真实凭证', () =>
|
||||
})
|
||||
|
||||
test('额外应付审核路径先追加 feeId 再追加 accountId 查询参数', () => {
|
||||
assert.match(source, /`\$\{basePath\}\/\$\{created\.id\}\/approve\?accountId=10001`/)
|
||||
assert.doesNotMatch(source, /fee-details\?accountId=10001\/\$\{created\.id\}/)
|
||||
assert.match(source, /`\$\{basePath\}\/\$\{created\.id\}\/approve\?accountId=\$\{encodeURIComponent\(customerId\)\}`/)
|
||||
assert.doesNotMatch(source, /fee-details\?accountId=.*\/\$\{created\.id\}/)
|
||||
})
|
||||
|
||||
test('费用来源使用 operationVersion,应付供应商长整型保持字符串精度', () => {
|
||||
|
||||
@@ -30,10 +30,10 @@ function blocker(context, name, response, evidence, category) {
|
||||
})
|
||||
}
|
||||
|
||||
async function waitForSources(context, sourceFeeIds) {
|
||||
async function waitForSources(context, customerId, sourceFeeIds) {
|
||||
let pool = []
|
||||
for (let attempt = 0; attempt < 30; attempt += 1) {
|
||||
const response = await context.request('GET', `${BASE}/fee-pool?customerId=10001&direction=ALL`)
|
||||
const response = await context.request('GET', `${BASE}/fee-pool?customerId=${encodeURIComponent(customerId)}&direction=ALL`)
|
||||
pool = rows(data(response)).concat(rows(data(response)?.items))
|
||||
const visible = new Set(pool.map((item) => id(item.sourceId)))
|
||||
if (sourceFeeIds.every((sourceId) => visible.has(sourceId))) return pool
|
||||
@@ -80,10 +80,11 @@ async function createExtraPayable(context, fixture, index) {
|
||||
unitPrice: 90 + index * 10, amount: 90 + index * 10, currency: 'CNY',
|
||||
supplierId, remark: `${MARKER}-AP-${index}` }
|
||||
const basePath = `/api/platform/v1/inbounds/${inboundId}/fee-details`
|
||||
const createdResponse = await write(context, 'POST', `${basePath}?accountId=10001`, body, `extra-payable-${index}`)
|
||||
const customerId = id(fixture.customerId)
|
||||
const createdResponse = await write(context, 'POST', `${basePath}?accountId=${encodeURIComponent(customerId)}`, body, `extra-payable-${index}`)
|
||||
const created = data(createdResponse)
|
||||
if (!ok(createdResponse) || !created?.id) return null
|
||||
const approveResponse = await write(context, 'POST', `${basePath}/${created.id}/approve?accountId=10001`, undefined, `extra-payable-${index}-approve`)
|
||||
const approveResponse = await write(context, 'POST', `${basePath}/${created.id}/approve?accountId=${encodeURIComponent(customerId)}`, undefined, `extra-payable-${index}-approve`)
|
||||
record(context, `创建第 ${index} 条额外应付来源`, approveResponse, ok(approveResponse),
|
||||
`sourceFeeId=${created.id}, inboundId=${inboundId}, supplierId=${supplierId}`)
|
||||
return { sourceFeeId: id(created.id), direction: 'PAYABLE', businessDate: body.businessDate,
|
||||
@@ -91,7 +92,7 @@ async function createExtraPayable(context, fixture, index) {
|
||||
}
|
||||
|
||||
async function createStatement(context, source, role, targetStatus) {
|
||||
const body = { customerId: '10001', currency: 'CNY', generationMode: 'SELECTED',
|
||||
const body = { customerId: id(source.customerId), currency: 'CNY', generationMode: 'SELECTED',
|
||||
feeSourceIds: [source.feeSourceId], periodStart: source.businessDate, periodEnd: source.businessDate,
|
||||
remark: `${MARKER}-${role}` }
|
||||
const response = await write(context, 'POST', `${BASE}/statements`, body, `statement-${role}-create`)
|
||||
@@ -120,7 +121,7 @@ async function createAr(context, statement, role) {
|
||||
|
||||
async function receive(context, fixture, settlement, amount, role) {
|
||||
const response = await write(context, 'POST', `${BASE}/receipts`, {
|
||||
customerId: 10001, receivedDate: new Date().toISOString().slice(0, 10), amount, currency: 'CNY',
|
||||
customerId: id(fixture.customerId), receivedDate: new Date().toISOString().slice(0, 10), amount, currency: 'CNY',
|
||||
paymentMethod: 'BANK_TRANSFER', voucherFileIds: [fixture.voucherFileId], remark: `${MARKER}-${role}`,
|
||||
allocations: [{ billId: Number(settlement.reconciliationStatementId), amount }],
|
||||
}, `receipt-${role}`)
|
||||
@@ -130,7 +131,7 @@ async function receive(context, fixture, settlement, amount, role) {
|
||||
|
||||
async function createAp(context, fixture, source, role) {
|
||||
const response = await write(context, 'POST', `${BASE}/payable-settlements`, {
|
||||
customerId: 10001, supplierId: id(source.supplierId), feeSourceIds: [Number(source.feeSourceId)],
|
||||
customerId: id(fixture.customerId), supplierId: id(source.supplierId), feeSourceIds: [Number(source.feeSourceId)],
|
||||
dueDate: new Date().toISOString().slice(0, 10), currency: 'CNY',
|
||||
}, `ap-${role}-${source.feeSourceId}-${source.supplierId}-create`)
|
||||
const value = data(response)
|
||||
@@ -165,8 +166,9 @@ async function recoverExistingDemo(context, reportPath) {
|
||||
const report = JSON.parse(await readFile(reportPath, 'utf8'))
|
||||
const fixture = report?.createdResources?.financeFullFixtures
|
||||
const matrix = structuredClone(report?.createdResources?.financeDemo ?? {})
|
||||
if (fixture?.marker !== matrix.runMarker || matrix.marker !== MARKER || matrix.customerId !== '10001') {
|
||||
throw new Error('恢复报告不是 FIN-DEMO-20260812 / 客户 10001 的同一轮 fixture')
|
||||
if (fixture?.marker !== matrix.runMarker || matrix.marker !== MARKER
|
||||
|| !id(matrix.customerId) || id(matrix.customerId) !== id(fixture.customerId)) {
|
||||
throw new Error('恢复报告不是 FIN-DEMO-20260812 同一轮动态客户 fixture')
|
||||
}
|
||||
context.marker = matrix.runMarker
|
||||
await verifyUnfilteredLists(context)
|
||||
@@ -189,7 +191,7 @@ async function recoverExistingDemo(context, reportPath) {
|
||||
const role = roles[index]
|
||||
const detail = await sourceDetail(context, payableIds[index])
|
||||
const source = detail.value
|
||||
if (!ok(detail.response) || source?.direction !== 'PAYABLE' || source?.customerId !== '10001'
|
||||
if (!ok(detail.response) || source?.direction !== 'PAYABLE' || id(source?.customerId) !== id(matrix.customerId)
|
||||
|| !id(source?.sourceFeeId)) {
|
||||
blocker(context, `${role} 恢复来源属于本轮 fixture`, detail.response,
|
||||
`feeSourceId=${payableIds[index]}, sourceFeeId=${source?.sourceFeeId ?? 'missing'}`,
|
||||
@@ -276,9 +278,12 @@ export async function run(context) {
|
||||
const producer = await import('./finance-full-fixtures.mjs')
|
||||
await producer.run(context)
|
||||
const fixture = context.createdResources.financeFullFixtures
|
||||
if (!fixture || fixture.marker !== runMarker || fixture.customerId !== '10001') {
|
||||
const qualificationFixture = context.context.merchantQualificationFixture
|
||||
const customerId = id(fixture?.customerId)
|
||||
if (!fixture || fixture.marker !== runMarker || !customerId
|
||||
|| customerId !== id(qualificationFixture?.tenantId)) {
|
||||
blocker(context, '取得演示企业真实 Finance fixture', null,
|
||||
'finance-full-fixtures 未返回 marker=FIN-DEMO-20260812、customerId=10001 的独立资源,禁止借历史来源',
|
||||
'finance-full-fixtures 未返回与 merchant-qualification-fixture 同客户的本轮独立资源,禁止借历史来源',
|
||||
'missing-finance-demo-fixture')
|
||||
return
|
||||
}
|
||||
@@ -289,10 +294,10 @@ export async function run(context) {
|
||||
if (value) extraPayables.push(value)
|
||||
}
|
||||
const produced = [...fixture.feeSources.map((item) => ({ ...item, sourceFeeId: id(item.feeSourceId) })), ...extraPayables]
|
||||
const pool = await waitForSources(context, produced.map((item) => item.sourceFeeId).filter(Boolean))
|
||||
const pool = await waitForSources(context, customerId, produced.map((item) => item.sourceFeeId).filter(Boolean))
|
||||
const poolBySource = new Map(pool.map((item) => [id(item.sourceId), item]))
|
||||
const sources = produced.map((source) => ({ ...source,
|
||||
feeSourceId: financeId(poolBySource.get(source.sourceFeeId)), marker: runMarker,
|
||||
feeSourceId: financeId(poolBySource.get(source.sourceFeeId)), marker: runMarker, customerId,
|
||||
supplierId: id(source.supplierId ?? fixture.supplier?.supplierId) }))
|
||||
.filter((source) => source.feeSourceId)
|
||||
const receivables = sources.filter((source) => source.direction === 'RECEIVABLE')
|
||||
@@ -304,7 +309,8 @@ export async function run(context) {
|
||||
return
|
||||
}
|
||||
|
||||
const matrix = { marker: MARKER, runMarker, customerId: '10001', customerLabel: '一期演示企业 10001',
|
||||
const matrix = { marker: MARKER, runMarker, customerId,
|
||||
customerLabel: qualificationFixture?.tenantName || `动态验收客户 ${customerId}`,
|
||||
feeSources: {}, statements: {}, receivableSettlements: {}, payableSettlements: {},
|
||||
voucherFileId: id(fixture.voucherFileId), container: fixture.container, allocations: fixture.allocations }
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@ test('measurement submission confirms weight evidence before submit', () => {
|
||||
})
|
||||
|
||||
test('measurement fixture derives pallet resources from confirmed image count', () => {
|
||||
assert.match(source, /palletCount: WEIGHING_IMAGE_COUNT/)
|
||||
assert.match(source, /waitSplitVariant\?\.palletCount \|\| 1/)
|
||||
assert.match(source, /palletIds: warehouseResourcesReady\.palletIds/)
|
||||
assert.match(source, /confirmedRows\.length === palletIds\.length/)
|
||||
assert.doesNotMatch(source, /Math\.ceil\(Number\(totalVolumeCbm\)/)
|
||||
assert.doesNotMatch(source, /Math\.ceil\(totalVolume \/ DEFAULT_PALLET_CAPACITY_CBM\)/)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const MODULE = 'inbound-measurement'
|
||||
const SCENARIO = 'mojia-carton-measurement'
|
||||
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
|
||||
const WEIGHING_IMAGE_COUNT = 1
|
||||
const FIXTURE_PALLET_CAPACITY_CBM = 1.2
|
||||
const WEIGHT_IMAGE = Uint8Array.from([137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82,
|
||||
0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 13, 73, 68, 65, 84,
|
||||
@@ -44,16 +43,17 @@ async function provisionWarehouseResources(context, { inboundId, warehouseId, pa
|
||||
if (!warehouseId || !Number.isInteger(palletCount) || palletCount < 1) return null
|
||||
|
||||
const suffix = `${String(context.marker).replace(/\D/g, '').slice(-8)}${String(inboundId).slice(-6)}`
|
||||
const palletResponse = await context.request('POST', '/api/platform/v1/pallets?tenantId=0', {
|
||||
warehouseId, palletType: 'STANDARD', capacityCbm: FIXTURE_PALLET_CAPACITY_CBM,
|
||||
weightKg: 8, color: '蓝色', remark: `${context.marker}-INBOUND-${inboundId}`,
|
||||
})
|
||||
const palletId = asId(context.apiSuccess(MODULE, '创建称重托盘主数据', palletResponse,
|
||||
`warehouseId=${warehouseId}`, OPTIONS))
|
||||
if (!palletId) return null
|
||||
|
||||
const resources = []
|
||||
const palletIds = []
|
||||
for (let index = 1; index <= palletCount; index += 1) {
|
||||
const palletResponse = await context.request('POST', '/api/platform/v1/pallets?tenantId=0', {
|
||||
warehouseId, palletType: 'STANDARD', capacityCbm: FIXTURE_PALLET_CAPACITY_CBM,
|
||||
weightKg: 8, color: '蓝色', remark: `${context.marker}-INBOUND-${inboundId}-PALLET-${index}`,
|
||||
})
|
||||
const palletId = asId(context.apiSuccess(MODULE, `创建称重托盘主数据 ${index}/${palletCount}`,
|
||||
palletResponse, `warehouseId=${warehouseId}`, OPTIONS))
|
||||
if (!palletId) return null
|
||||
palletIds.push(palletId)
|
||||
const resourceSuffix = `${suffix}${String(index).padStart(2, '0')}`
|
||||
let locationId = null
|
||||
if (context.context?.phase3LoadingRequireSharedOverflow !== true) {
|
||||
@@ -75,54 +75,65 @@ async function provisionWarehouseResources(context, { inboundId, warehouseId, pa
|
||||
marker: context.marker, tenantId: '0', warehouseId: asId(warehouseId), palletCount,
|
||||
palletCapacityCbm: FIXTURE_PALLET_CAPACITY_CBM, resources,
|
||||
}
|
||||
return { palletId, palletCount, resources }
|
||||
return { palletId: palletIds[0], palletIds, palletCount, resources }
|
||||
}
|
||||
|
||||
async function createConfirmedWeightEvidence(context, { inboundId, tenantId, palletId }) {
|
||||
const uploadResponse = await context.requestMultipart('POST',
|
||||
`/api/platform/v1/files/measurements/${tenantId}/${inboundId}`, {
|
||||
content: WEIGHT_IMAGE, type: 'image/png', name: `weight-${inboundId}.png`,
|
||||
})
|
||||
const upload = context.apiSuccess(MODULE, '上传称重图片并执行 OCR', uploadResponse,
|
||||
`inboundId=${inboundId}`, OPTIONS)
|
||||
const fileId = asId(upload?.fileId)
|
||||
if (!fileId) return null
|
||||
async function createConfirmedWeightEvidence(context, {
|
||||
inboundId, tenantId, palletIds, grossWeightPerPalletKg,
|
||||
}) {
|
||||
if (!Array.isArray(palletIds) || palletIds.length < 1) return null
|
||||
const confirmedResults = []
|
||||
for (let index = 0; index < palletIds.length; index += 1) {
|
||||
const palletId = palletIds[index]
|
||||
const uploadResponse = await context.requestMultipart('POST',
|
||||
`/api/platform/v1/files/measurements/${tenantId}/${inboundId}`, {
|
||||
content: WEIGHT_IMAGE, type: 'image/png', name: `weight-${inboundId}-${index + 1}.png`,
|
||||
})
|
||||
const upload = context.apiSuccess(MODULE, `上传称重图片并执行 OCR ${index + 1}/${palletIds.length}`,
|
||||
uploadResponse, `inboundId=${inboundId}`, OPTIONS)
|
||||
const fileId = asId(upload?.fileId)
|
||||
if (!fileId) return null
|
||||
|
||||
const createResponse = await context.request('POST',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results?${query(tenantId)}`, {
|
||||
fileId, palletId, ocrProvider: 'ADMIN_ACCEPTANCE', ocrRawText: null,
|
||||
ocrWeightValue: null, ocrWeightUnit: null, confidence: null,
|
||||
})
|
||||
const created = context.apiSuccess(MODULE, '登记称重图片与托盘快照', createResponse,
|
||||
`fileId=${fileId}, palletId=${palletId}`, OPTIONS)
|
||||
if (!created?.id) return null
|
||||
const createResponse = await context.request('POST',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results?${query(tenantId)}`, {
|
||||
fileId, palletId, ocrProvider: 'ADMIN_ACCEPTANCE', ocrRawText: null,
|
||||
ocrWeightValue: null, ocrWeightUnit: null, confidence: null,
|
||||
})
|
||||
const created = context.apiSuccess(MODULE, `登记称重图片与托盘快照 ${index + 1}/${palletIds.length}`,
|
||||
createResponse, `fileId=${fileId}, palletId=${palletId}`, OPTIONS)
|
||||
if (!created?.id) return null
|
||||
|
||||
const confirmResponse = await context.request('POST',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results/${created.id}/confirm?${query(tenantId)}`, {
|
||||
version: Number(created.version), adjustedWeightValue: 100,
|
||||
adjustedWeightUnit: 'KG', adjustmentReason: `${context.marker} 自动化人工确认称重`,
|
||||
})
|
||||
const confirmed = context.apiSuccess(MODULE, '人工确认称重图片净重', confirmResponse,
|
||||
`resultId=${created.id}, palletId=${palletId}`, OPTIONS)
|
||||
const netWeightKg = Number(confirmed?.netWeightKg)
|
||||
const grossWeightKg = Number(confirmed?.grossWeightKg)
|
||||
if (confirmed?.recognitionStatus !== 'CONFIRMED' || !Number.isFinite(netWeightKg) || netWeightKg <= 0) {
|
||||
return null
|
||||
const grossWeight = Number(grossWeightPerPalletKg) + index * 5
|
||||
const confirmResponse = await context.request('POST',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results/${created.id}/confirm?${query(tenantId)}`, {
|
||||
version: Number(created.version), adjustedWeightValue: grossWeight,
|
||||
adjustedWeightUnit: 'KG', adjustmentReason: `${context.marker} 自动化人工确认称重`,
|
||||
})
|
||||
const confirmed = context.apiSuccess(MODULE, `人工确认称重图片净重 ${index + 1}/${palletIds.length}`,
|
||||
confirmResponse, `resultId=${created.id}, palletId=${palletId}`, OPTIONS)
|
||||
const netWeightKg = Number(confirmed?.netWeightKg)
|
||||
const grossWeightKg = Number(confirmed?.grossWeightKg)
|
||||
if (confirmed?.recognitionStatus !== 'CONFIRMED' || !Number.isFinite(netWeightKg) || netWeightKg <= 0) {
|
||||
return null
|
||||
}
|
||||
confirmedResults.push({ id: asId(created.id), palletId, netWeightKg, grossWeightKg })
|
||||
}
|
||||
|
||||
const workspaceResponse = await context.request('GET',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/measurement?${query(tenantId)}`)
|
||||
const workspace = context.apiSuccess(MODULE, '回查已确认称重图片和货物总重量', workspaceResponse,
|
||||
`resultId=${created.id}`, OPTIONS)
|
||||
`resultIds=${confirmedResults.map((result) => result.id).join(',')}`, OPTIONS)
|
||||
const confirmedRows = (workspace?.weightResults || [])
|
||||
.filter((result) => result?.recognitionStatus === 'CONFIRMED')
|
||||
const persisted = confirmedRows.length === WEIGHING_IMAGE_COUNT
|
||||
&& asId(confirmedRows[0]?.id) === asId(created.id)
|
||||
&& asId(confirmedRows[0]?.palletId) === palletId
|
||||
const grossWeightKg = confirmedResults.reduce((sum, result) => sum + result.grossWeightKg, 0)
|
||||
const netWeightKg = confirmedResults.reduce((sum, result) => sum + result.netWeightKg, 0)
|
||||
const persisted = confirmedRows.length === palletIds.length
|
||||
&& confirmedResults.every((expected) => confirmedRows.some((row) =>
|
||||
asId(row?.id) === expected.id && asId(row?.palletId) === expected.palletId))
|
||||
&& closeTo(workspace?.actualGrossWeightKg, grossWeightKg, 0.001)
|
||||
&& closeTo(workspace?.actualNetWeightKg, netWeightKg, 0.001)
|
||||
context.assert(MODULE, '图片张数决定托数且净重汇总已落地', workspaceResponse, persisted,
|
||||
`confirmedImages=${confirmedRows.length}, expectedPallets=${WEIGHING_IMAGE_COUNT}, grossWeight=${grossWeightKg}, netWeight=${netWeightKg}, actualGross=${workspace?.actualGrossWeightKg ?? 'missing'}, actualNet=${workspace?.actualNetWeightKg ?? 'missing'}`,
|
||||
`confirmedImages=${confirmedRows.length}, expectedPallets=${palletIds.length}, grossWeight=${grossWeightKg}, netWeight=${netWeightKg}, actualGross=${workspace?.actualGrossWeightKg ?? 'missing'}, actualNet=${workspace?.actualNetWeightKg ?? 'missing'}`,
|
||||
OPTIONS)
|
||||
return persisted ? { count: confirmedRows.length, grossWeightKg, netWeightKg } : null
|
||||
}
|
||||
@@ -280,8 +291,8 @@ export async function run(context) {
|
||||
}
|
||||
|
||||
const scanPath = `/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/carton-scans?${query(tenantId)}`
|
||||
// Keep the isolated sample inside the customer's largest configured inner box (60x50x50cm).
|
||||
const firstDimensions = { length: 59.1, width: 40.2, height: 30.3 }
|
||||
const measurementProfile = context.context?.waitSplitVariant?.measurementProfile
|
||||
const firstDimensions = measurementProfile?.first || { length: 59.1, width: 40.2, height: 30.3 }
|
||||
const firstShipmentId = `FBA-${context.marker.slice(-12)}-01`
|
||||
const firstScanResponse = await multipartRequest(context, scanPath, {
|
||||
boxCode, ...firstDimensions, scannedShipmentId: firstShipmentId,
|
||||
@@ -325,7 +336,7 @@ export async function run(context) {
|
||||
&& Number(countResult?.measurementVersion) === Number(firstScan.measurementVersion) + 1,
|
||||
`version=${countResult?.measurementVersion ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const replacement = { length: 59.5, width: 41.2, height: 31.3 }
|
||||
const replacement = measurementProfile?.replacement || { length: 59.5, width: 41.2, height: 31.3 }
|
||||
const replacementShipmentId = `FBA-${context.marker.slice(-12)}-02`
|
||||
const rescanResponse = await multipartRequest(context, scanPath, {
|
||||
boxCode, ...replacement, scannedShipmentId: replacementShipmentId,
|
||||
@@ -342,8 +353,7 @@ export async function run(context) {
|
||||
let measuredResponse = await context.request('GET', measurementPath)
|
||||
let measured = context.apiSuccess(MODULE, '回查覆盖后的尺寸与体积结果', measuredResponse, '', OPTIONS)
|
||||
let measuredItem = measured?.items?.find((item) => asId(item.id) === asId(firstScan.itemId))
|
||||
// The verified customer rule maps this scan to EXTRA_LARGE 60x50x50cm and adds 2cm per edge.
|
||||
const weightedOuter = { length: 64, width: 54, height: 54 }
|
||||
const weightedOuter = measurementProfile?.outer || { length: 64, width: 54, height: 54 }
|
||||
const unitVolume = halfUp(
|
||||
weightedOuter.length * weightedOuter.width * weightedOuter.height / 1_000_000, 3)
|
||||
const totalVolume = unitVolume * originalPackageCount
|
||||
@@ -374,7 +384,7 @@ export async function run(context) {
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/items/${firstScan.itemId}/confirmed-package-count?${query(tenantId)}`,
|
||||
{ measurementVersion: Number(measured?.version), confirmedPackageCount: packagesPerDetail })
|
||||
const resetFirstCount = context.apiSuccess(MODULE,
|
||||
'增量 fixture 将首条货物确认箱数恢复为自身两箱', resetFirstCountResponse,
|
||||
'增量 fixture 将首条货物确认箱数恢复为自身箱数', resetFirstCountResponse,
|
||||
`itemId=${asId(firstScan.itemId)}, confirmedPackageCount=${packagesPerDetail}`, OPTIONS)
|
||||
context.assert(MODULE, '增量首条货物箱数和版本更新正确', resetFirstCountResponse,
|
||||
Number(resetFirstCount?.confirmedPackageCount) === packagesPerDetail
|
||||
@@ -409,10 +419,10 @@ export async function run(context) {
|
||||
`measurementVersion=${countDetail?.measurementVersion ?? 'missing'}`, OPTIONS)
|
||||
}
|
||||
measuredResponse = await context.request('GET', measurementPath)
|
||||
measured = context.apiSuccess(MODULE, '回查增量 fixture 四条测量明细', measuredResponse, '', OPTIONS)
|
||||
measured = context.apiSuccess(MODULE, '回查增量 fixture 动态测量明细', measuredResponse, '', OPTIONS)
|
||||
measuredItem = measured?.items?.find((item) => asId(item.id) === asId(firstScan.itemId))
|
||||
const detailIds = new Set((measured?.items || []).map((item) => asId(item?.inboundDetailId)).filter(Boolean))
|
||||
context.assert(MODULE, '增量 fixture 形成四条真实独立货物测量明细', measuredResponse,
|
||||
context.assert(MODULE, '增量 fixture 形成动态真实独立货物测量明细', measuredResponse,
|
||||
detailIds.size >= incrementalDetailCount
|
||||
&& measured.items.filter((item) => detailIds.has(asId(item?.inboundDetailId))).length >= incrementalDetailCount,
|
||||
`measurementItems=${measured?.items?.length ?? 'missing'}, distinctInboundDetails=${detailIds.size}`, OPTIONS)
|
||||
@@ -482,12 +492,13 @@ export async function run(context) {
|
||||
return
|
||||
}
|
||||
|
||||
const palletCount = Math.max(1, Number(context.context?.waitSplitVariant?.palletCount || 1))
|
||||
const warehouseResourcesReady = await provisionWarehouseResources(context, {
|
||||
inboundId, warehouseId: asId(summary?.warehouseId), palletCount: WEIGHING_IMAGE_COUNT,
|
||||
inboundId, warehouseId: asId(summary?.warehouseId), palletCount,
|
||||
})
|
||||
context.assert(MODULE, '测量提交前按称重图片数准备真实库位', draftResponse,
|
||||
Boolean(warehouseResourcesReady),
|
||||
`warehouseId=${asId(summary?.warehouseId) || 'missing'}, palletCount=${WEIGHING_IMAGE_COUNT}`,
|
||||
`warehouseId=${asId(summary?.warehouseId) || 'missing'}, palletCount=${palletCount}`,
|
||||
OPTIONS)
|
||||
if (!warehouseResourcesReady) {
|
||||
finishScenario(context, start)
|
||||
@@ -495,11 +506,12 @@ export async function run(context) {
|
||||
}
|
||||
|
||||
const weightEvidence = await createConfirmedWeightEvidence(context, {
|
||||
inboundId, tenantId, palletId: warehouseResourcesReady.palletId,
|
||||
inboundId, tenantId, palletIds: warehouseResourcesReady.palletIds,
|
||||
grossWeightPerPalletKg: Number(context.context?.waitSplitVariant?.grossWeightPerPalletKg || 100),
|
||||
})
|
||||
context.assert(MODULE, '测量提交前已通过正式接口确认称重图片', draftResponse,
|
||||
Boolean(weightEvidence),
|
||||
`confirmedImages=${weightEvidence?.count ?? 0}, expected=${WEIGHING_IMAGE_COUNT}`,
|
||||
`confirmedImages=${weightEvidence?.count ?? 0}, expected=${palletCount}`,
|
||||
OPTIONS)
|
||||
if (!weightEvidence) {
|
||||
finishScenario(context, start)
|
||||
|
||||
@@ -7,23 +7,23 @@ const iamSeed = readFileSync(new URL('../../iam/src/main/resources/db/phase1-adm
|
||||
const oceanBookingSchema = readFileSync(new URL('../../firstleg/src/main/resources/db/phase3-ocean-booking-schema.sql', import.meta.url), 'utf8')
|
||||
const truckAppointmentSchema = readFileSync(new URL('../../firstleg/src/main/resources/db/phase3-truck-appointment-schema.sql', import.meta.url), 'utf8')
|
||||
|
||||
test('统一初始化 SQL 保留最小登录与客户主体', () => {
|
||||
assert.match(sql, /一个平台系统管理员和一个 IT 测试客户管理员/)
|
||||
test('统一初始化 SQL 只保留平台管理员且不创建固定客户', () => {
|
||||
assert.match(sql, /最小登录基线只保留平台系统管理员/)
|
||||
assert.equal((sql.match(/'platform_admin'/g) || []).length, 1)
|
||||
assert.equal((sql.match(/'merchant_admin'/g) || []).length, 1)
|
||||
assert.match(sql, /\(10001, 'IT_TEST', 'IT 测试客户'/)
|
||||
assert.doesNotMatch(sql, /'merchant_admin'|'IT_TEST'|'IT 测试客户'/)
|
||||
assert.doesNotMatch(iamSeed, /'IT_TEST'|'IT 测试客户'|\(10001,/)
|
||||
const accountSeed = sql.match(/INSERT INTO auth_account \([\s\S]*?deleted = 0;/)?.[0]
|
||||
assert.ok(accountSeed, 'missing auth account seed')
|
||||
assert.doesNotMatch(accountSeed, /ON DUPLICATE KEY UPDATE[\s\S]*?password_hash\s*=/,
|
||||
're-running the unified SQL must not reset an existing account password')
|
||||
})
|
||||
|
||||
test('IT 测试客户初始化后具备已审核的必填资质和下单资格', () => {
|
||||
for (const source of [sql, iamSeed]) {
|
||||
assert.match(source, /\(2100001, 10001, 'BUSINESS_LICENSE', '91310000ITTEST0001', NULL,[\s\S]*?'APPROVED'/)
|
||||
assert.match(source, /\(2100002, 10001, 'LEGAL_PERSON_ID', NULL, NULL,[\s\S]*?'APPROVED'/)
|
||||
assert.match(source, /status = 'APPROVED'/)
|
||||
}
|
||||
test('统一初始化直接提供tenant0商户角色与权限模板', () => {
|
||||
assert.match(sql, /4000100, 0, 'MERCHANT', 'MERCHANT_OWNER'/)
|
||||
assert.match(sql, /5300000, 0, 'MERCHANT', 'MENU',[\s\S]*?'merchant:enterprise'/)
|
||||
assert.match(sql, /5301024, 0, 'MERCHANT', 'BUTTON',[\s\S]*?'merchant:order:quick-create'/)
|
||||
assert.match(sql, /5305000, 0, 'MERCHANT', 'MENU',[\s\S]*?'merchant:finance:statement'/)
|
||||
assert.match(sql, /r\.id=4000100[\s\S]*?客户全局管理员 Finance 模板授权不完整/)
|
||||
})
|
||||
|
||||
test('统一初始化 SQL 提供人工验收所需公共仓储主数据', () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import test from 'node:test'
|
||||
|
||||
const acceptance = readFileSync(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8')
|
||||
const fixture = readFileSync(new URL('./merchant-qualification-fixture.mjs', import.meta.url), 'utf8')
|
||||
const orders = readFileSync(new URL('./orders.mjs', import.meta.url), 'utf8')
|
||||
|
||||
test('默认全量验收先准备已审核商户再运行订单链路', () => {
|
||||
const modules = acceptance.match(/const CONFIGURED_MODULES = \[([^\n]+)\]/)?.[1] || ''
|
||||
@@ -22,5 +23,38 @@ test('商户夹具通过正式 File 和 IAM 审核流程建立下单资格', ()
|
||||
assert.match(fixture, /item\.status === 'APPROVED'/)
|
||||
assert.match(fixture, /MERCHANT_TEST_USERNAME/)
|
||||
assert.match(fixture, /MERCHANT_TEST_PASSWORD/)
|
||||
assert.match(fixture, /context\.options\?\.customerName/)
|
||||
assert.doesNotMatch(fixture, /internal\/iam|UPDATE\s+iam_|INSERT\s+INTO\s+iam_/i)
|
||||
})
|
||||
|
||||
test('批量待分柜参数固定同一客户并限制为订单测量链', () => {
|
||||
assert.match(acceptance, /--customer-name <name>/)
|
||||
assert.match(acceptance, /--merchant-username <name>/)
|
||||
assert.match(acceptance, /--origin-warehouse-name <name>/)
|
||||
assert.match(acceptance, /--channel-name <name>/)
|
||||
assert.match(acceptance, /--wait-split-count <n>/)
|
||||
assert.match(acceptance, /--order-profile-mode <mode>/)
|
||||
assert.match(acceptance, /merchant-username和merchant-password必须同时提供/)
|
||||
assert.match(acceptance, /modules\.splice\(fixtureIndex, 1\)/)
|
||||
assert.match(acceptance, /wait-split-count必须是1-100的整数/)
|
||||
assert.match(acceptance, /批量WAIT_SPLIT只允许执行merchant-qualification-fixture、orders、inbound-measurement/)
|
||||
assert.match(acceptance, /createAdditionalWaitSplitFixtures/)
|
||||
assert.match(acceptance, /measurement\.processStatus === 'WAIT_SPLIT'/)
|
||||
assert.match(acceptance, /measurement\.finalStatus === 'APPROVED'/)
|
||||
assert.match(acceptance, /waitSplitVariant\(sequence\)/)
|
||||
assert.match(acceptance, /packageCountPerSku/)
|
||||
assert.match(acceptance, /grossWeightPerPalletKg/)
|
||||
assert.match(acceptance, /randomizedOrderProfile\(sequence\)/)
|
||||
assert.match(acceptance, /\['PRIVATE', 'STANDARD'\]/)
|
||||
assert.match(acceptance, /ONE_CLICK_DROPSHIP/)
|
||||
assert.match(acceptance, /destinationWarehouseCode: order\.destinationWarehouseCode/)
|
||||
assert.match(acceptance, /channelName: order\.channelName/)
|
||||
assert.match(acceptance, /pricingTaxMode: order\.pricingTaxMode/)
|
||||
assert.match(orders, /customerId: profile\.id/)
|
||||
assert.match(orders, /customerName: companyName/)
|
||||
assert.match(orders, /\['FBA', 'OVERSEAS', 'PRIVATE'\]\.includes/)
|
||||
assert.match(orders, /destinationType === 'PRIVATE'/)
|
||||
assert.match(orders, /PRIVATE_ADDRESS/)
|
||||
assert.match(orders, /eligible\.length < 1/)
|
||||
assert.doesNotMatch(orders, /customerName: master\.partner\?\.partnerName/)
|
||||
})
|
||||
|
||||
@@ -28,10 +28,11 @@ async function waitForDimensionInitialization(context, tenantId) {
|
||||
|
||||
export async function run(context) {
|
||||
const suffix = context.marker.replace(/[^0-9A-Z]/gi, '').slice(-10).padStart(10, '0')
|
||||
const customerName = String(context.options?.customerName || '').trim()
|
||||
const phone = await context.findAvailablePhone(MODULE)
|
||||
const createResponse = await context.request('POST', '/api/platform/v1/iam/tenants', {
|
||||
tenantName: `全量验收下单客户-${suffix}`,
|
||||
legalName: `全量验收下单有限公司-${suffix}`,
|
||||
tenantName: customerName || `全量验收下单客户-${suffix}`,
|
||||
legalName: customerName ? `${customerName}有限公司` : `全量验收下单有限公司-${suffix}`,
|
||||
legalRepresentative: '全量验收法人',
|
||||
contactName: '全量验收管理员',
|
||||
contactPhone: phone,
|
||||
|
||||
@@ -2,8 +2,8 @@ import { findPricedOrderRouteAcrossOrigins } from './orders.mjs'
|
||||
|
||||
const MODULE = 'order-inbound-status-fixtures'
|
||||
const OPTIONS = { scenario: 'manual-order-inbound-status-matrix', deploymentBlocker: true }
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin'
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!'
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
|
||||
|
||||
const ORDER_TARGETS = [
|
||||
['DRAFT', []],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const MODULE = 'orders'
|
||||
const CONTRACT_OPTIONS = { scenario: 'isolated-order-chain', deploymentBlocker: true }
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin'
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!'
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
|
||||
const BOXES = [
|
||||
{ boxType: 'SMALL', lengthCm: 30, widthCm: 20, heightCm: 20 },
|
||||
{ boxType: 'MEDIUM', lengthCm: 40, widthCm: 30, heightCm: 30 },
|
||||
@@ -21,8 +21,8 @@ function success(response) { return Boolean(response) && response.http === 200 &
|
||||
function sleep(milliseconds) { return new Promise((resolve) => setTimeout(resolve, milliseconds)) }
|
||||
function key(context, operation) { return `${context.marker}-${operation}`.slice(0, 128) }
|
||||
export function requestedOrderDestinationType(context) {
|
||||
return String(context?.context?.orderDestinationType || 'FBA').trim().toUpperCase() === 'OVERSEAS'
|
||||
? 'OVERSEAS' : 'FBA'
|
||||
const value = String(context?.context?.orderDestinationType || 'FBA').trim().toUpperCase()
|
||||
return ['FBA', 'OVERSEAS', 'PRIVATE'].includes(value) ? value : 'FBA'
|
||||
}
|
||||
export function approvedInboundReady(orderDetail, taskResponse, tenantId, orderId) {
|
||||
const summary = orderDetail?.summary
|
||||
@@ -202,35 +202,88 @@ async function assertProductImageDownload(context, merchantToken, fileId) {
|
||||
|
||||
async function loadMasterData(context, merchantToken) {
|
||||
const destinationType = requestedOrderDestinationType(context)
|
||||
const fixedRoute = context.context?.waitSplitRoute || {}
|
||||
const domesticResponse = await request(context, 'GET',
|
||||
'/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=DOMESTIC', undefined, merchantToken)
|
||||
const domesticPage = context.apiSuccess(MODULE, 'Client 查询国内收货仓', domesticResponse)
|
||||
context.assert(MODULE, '存在可用国内收货仓', domesticResponse, records(domesticPage).length > 0,
|
||||
`count=${records(domesticPage).length}`)
|
||||
|
||||
const destinationResponse = await request(context, 'GET',
|
||||
`/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=${encodeURIComponent(destinationType)}`,
|
||||
undefined, merchantToken)
|
||||
const destinationPage = context.apiSuccess(MODULE, `Client 查询 ${destinationType} 目的仓`, destinationResponse)
|
||||
context.assert(MODULE, `存在可用 ${destinationType} 目的仓`, destinationResponse,
|
||||
records(destinationPage).length > 0, `count=${records(destinationPage).length}`)
|
||||
let destinationResponse = null
|
||||
let destinationPage = null
|
||||
if (destinationType !== 'PRIVATE') {
|
||||
destinationResponse = await request(context, 'GET',
|
||||
`/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=${encodeURIComponent(destinationType)}`,
|
||||
undefined, merchantToken)
|
||||
destinationPage = context.apiSuccess(MODULE, `Client 查询 ${destinationType} 目的仓`, destinationResponse)
|
||||
context.assert(MODULE, `存在可用 ${destinationType} 目的仓`, destinationResponse,
|
||||
records(destinationPage).length > 0, `count=${records(destinationPage).length}`)
|
||||
}
|
||||
const receivings = records(domesticPage).filter((warehouse) => warehouse?.status === 'ACTIVE')
|
||||
const destinations = records(destinationPage).filter((warehouse) => warehouse?.status === 'ACTIVE')
|
||||
if (receivings.length === 0 || destinations.length === 0) throw new Error('缺少创建测试订单所需的收货仓或目的仓')
|
||||
let destinations = records(destinationPage).filter((warehouse) => warehouse?.status === 'ACTIVE')
|
||||
if (Number(destinationPage?.total) > destinations.length) {
|
||||
const nextResponse = await request(context, 'GET',
|
||||
`/api/merchant/v1/warehouses?page=2&size=200&status=ACTIVE&warehouseType=${encodeURIComponent(destinationType)}`,
|
||||
undefined, merchantToken)
|
||||
const nextPage = context.apiSuccess(MODULE, `Client 查询 ${destinationType} 目的仓第二页`, nextResponse)
|
||||
destinations = [...destinations, ...records(nextPage).filter((warehouse) => warehouse?.status === 'ACTIVE')]
|
||||
}
|
||||
if (receivings.length === 0 || (destinationType !== 'PRIVATE' && destinations.length === 0)) {
|
||||
throw new Error('缺少创建测试订单所需的收货仓或目的仓')
|
||||
}
|
||||
|
||||
const pricingResult = await findPricedOrderRouteAcrossOrigins({
|
||||
receivings, destinations,
|
||||
loadChannels: (receiving) => request(context, 'GET',
|
||||
let pricingResult
|
||||
if (fixedRoute.originWarehouseName && (fixedRoute.channelCode || fixedRoute.channelName)) {
|
||||
const receiving = receivings.find((warehouse) => warehouse.warehouseName === fixedRoute.originWarehouseName)
|
||||
if (!receiving) throw new Error(`未找到固定起运仓: ${fixedRoute.originWarehouseName}`)
|
||||
const channelsResponse = await request(context, 'GET',
|
||||
`/api/merchant/v1/first-leg/pricing/channels?originWarehouseId=${encodeURIComponent(receiving.id)}`,
|
||||
undefined, merchantToken),
|
||||
estimate: (body) => request(context, 'POST', '/api/merchant/v1/first-leg/pricing/estimate', body, merchantToken),
|
||||
destinationType,
|
||||
})
|
||||
undefined, merchantToken)
|
||||
const channels = context.apiSuccess(MODULE, 'Client 查询固定起运仓物流渠道', channelsResponse)
|
||||
const channel = (Array.isArray(channels) ? channels : []).find((item) =>
|
||||
(!fixedRoute.channelCode || item.channelCode === fixedRoute.channelCode)
|
||||
&& (!fixedRoute.channelName || item.channelName === fixedRoute.channelName))
|
||||
if (!channel) throw new Error(`未找到固定物流渠道: ${fixedRoute.channelCode || fixedRoute.channelName}`)
|
||||
const modes = Array.isArray(channel.pricingTaxModes) ? [...channel.pricingTaxModes] : []
|
||||
if (modes.length === 0) throw new Error('固定物流渠道没有可用报价税制')
|
||||
const shift = modes.length ? Number(fixedRoute.taxModeIndex || 0) % modes.length : 0
|
||||
const selectedChannel = { ...channel, pricingTaxModes: [...modes.slice(shift), ...modes.slice(0, shift)] }
|
||||
if (destinationType === 'PRIVATE') {
|
||||
pricingResult = { receiving, priced: { channel: selectedChannel,
|
||||
pricingTaxMode: selectedChannel.pricingTaxModes[0], destination: null },
|
||||
response: channelsResponse, attempts: 0, failures: [], originFailures: [] }
|
||||
} else {
|
||||
const detailResponse = await context.request('GET',
|
||||
`/api/platform/v1/fba-pricing/carrier-rates/price-books/${encodeURIComponent(channel.priceBookVersionId)}`)
|
||||
const detail = context.apiSuccess(MODULE, 'Admin 查询固定渠道当前价格明细', detailResponse)
|
||||
const pricedCodes = new Set((detail?.rates || [])
|
||||
.filter((rate) => rate.channelCode === channel.channelCode && rate.destinationType === destinationType)
|
||||
.map((rate) => String(rate.destinationCode || '').trim()).filter(Boolean))
|
||||
const eligible = destinations.filter((destination) => pricedCodes.has(String(destination.warehouseCode)))
|
||||
if (eligible.length < 1) throw new Error(`固定渠道没有可计价目的仓: ${destinationType}`)
|
||||
const destination = eligible[Number(fixedRoute.destinationIndex || 0) % eligible.length]
|
||||
const result = await findPricedOrderRoute({
|
||||
receiving, destinations: [destination], channels: [selectedChannel],
|
||||
estimate: (body) => request(context, 'POST', '/api/merchant/v1/first-leg/pricing/estimate', body, merchantToken),
|
||||
destinationType,
|
||||
})
|
||||
pricingResult = { receiving, ...result, originFailures: [] }
|
||||
}
|
||||
} else {
|
||||
pricingResult = await findPricedOrderRouteAcrossOrigins({
|
||||
receivings, destinations,
|
||||
loadChannels: (receiving) => request(context, 'GET',
|
||||
`/api/merchant/v1/first-leg/pricing/channels?originWarehouseId=${encodeURIComponent(receiving.id)}`,
|
||||
undefined, merchantToken),
|
||||
estimate: (body) => request(context, 'POST', '/api/merchant/v1/first-leg/pricing/estimate', body, merchantToken),
|
||||
destinationType,
|
||||
})
|
||||
}
|
||||
const priced = pricingResult.priced
|
||||
const receiving = pricingResult.receiving
|
||||
context.addStep(MODULE, 'Client 从 ACTIVE 国内仓选择具有真实价格覆盖的渠道和目的仓', pricingResult.response, Boolean(priced),
|
||||
priced
|
||||
? `origin=${receiving.id}, priceBookVersionId=${priced.channel.priceBookVersionId}, channel=${priced.channel.channelCode}, pricingTaxMode=${priced.pricingTaxMode}, destination=${priced.destination.warehouseCode}, attempts=${pricingResult.attempts}`
|
||||
? `origin=${receiving.id}, priceBookVersionId=${priced.channel.priceBookVersionId}, channel=${priced.channel.channelCode}, pricingTaxMode=${priced.pricingTaxMode}, destination=${priced.destination?.warehouseCode || 'PRIVATE_ADDRESS'}, attempts=${pricingResult.attempts}`
|
||||
: originPricingFailureEvidence(pricingResult),
|
||||
CONTRACT_OPTIONS)
|
||||
if (!priced) throw new Error(`当前 ACTIVE 国内收货仓、渠道、报价税制和 ${destinationType} 目的仓没有可计价组合`)
|
||||
@@ -249,22 +302,39 @@ function buildPayload(context, profile, master, productImageFileId) {
|
||||
const shipmentId = `SHIP-${context.marker}`.slice(0, 64)
|
||||
const referenceId = `REF-${context.marker}`.slice(0, 64)
|
||||
const companyName = profile.tenantName || `订单验收客户-${context.marker}`
|
||||
const requestedSkuCount = Number(context.context?.waitSplitVariant?.skuCount || BOXES.length)
|
||||
const skuCount = Math.max(1, Math.min(BOXES.length, requestedSkuCount))
|
||||
const packageCountPerSku = Math.max(1,
|
||||
Number(context.context?.waitSplitVariant?.packageCountPerSku
|
||||
|| context.context?.phase3LoadingIncrementalPackageCount || 1))
|
||||
const selectedBoxes = BOXES.slice(0, skuCount)
|
||||
const orderProfile = context.context?.orderProfile || {}
|
||||
const isPrivate = master.destinationType === 'PRIVATE'
|
||||
const pickupMethod = orderProfile.pickupMethod || 'PICKUP'
|
||||
const receiver = isPrivate ? {
|
||||
companyName: `${companyName} 私人收件人`, contactName: `收件人-${context.marker}`.slice(0, 64),
|
||||
phone: '12025550188', email: 'private-receiver@example.com', countryCode: 'US',
|
||||
province: 'New York', city: 'New York', district: 'Queens',
|
||||
addressLine: `${context.marker} 88 Randomized Test Ave`, postalCode: '11354',
|
||||
} : null
|
||||
return {
|
||||
fulfillmentType: String(context.context?.orderFulfillmentType || 'STANDARD').trim().toUpperCase(),
|
||||
customerId: master.partner?.id ?? null,
|
||||
customerName: master.partner?.partnerName ?? companyName,
|
||||
customerId: profile.id,
|
||||
customerName: companyName,
|
||||
transportMode: 'SEA', tradeTerm: 'FOB',
|
||||
destinationCountryCode: master.destination.countryCode || 'US',
|
||||
destinationWarehouseId: master.destination.id,
|
||||
destinationWarehouseName: master.destination.warehouseName,
|
||||
destinationCountryCode: master.destination?.countryCode || receiver?.countryCode || 'US',
|
||||
destinationWarehouseId: master.destination?.id || null,
|
||||
destinationWarehouseName: master.destination?.warehouseName || (isPrivate ? '私人地址' : null),
|
||||
expectedShipDate: tomorrow(), customerRemark: `${context.marker} Client 创建隔离订单`,
|
||||
pickupMethod: 'PICKUP', receivingWarehouseId: master.receiving.id,
|
||||
pickupMethod, requestedPickupAt: pickupMethod === 'PICKUP'
|
||||
? new Date(Date.now() + 7 * 86_400_000).toISOString().slice(0, 19) : null,
|
||||
receivingWarehouseId: master.receiving.id,
|
||||
receivingWarehouseName: master.receiving.warehouseName,
|
||||
channelCode: master.channel.channelCode, channelName: master.channel.channelName,
|
||||
carrierId: null, carrierName: null, taxMode: 'BUY_DECLARATION',
|
||||
pricingTaxMode: master.pricingTaxMode, containerMode: 'LCL',
|
||||
customsInspection: false, destinationType: master.destinationType,
|
||||
amazonWarehouseCode: master.destinationType === 'FBA' ? master.destination.warehouseCode : null,
|
||||
carrierId: null, carrierName: null, taxMode: orderProfile.taxMode || 'BUY_DECLARATION',
|
||||
pricingTaxMode: master.pricingTaxMode, containerMode: orderProfile.containerMode || 'LCL',
|
||||
customsInspection: orderProfile.customsInspection ?? false, destinationType: master.destinationType,
|
||||
amazonWarehouseCode: master.destinationType === 'FBA' ? master.destination?.warehouseCode : null,
|
||||
shipmentId: master.destinationType === 'FBA' ? shipmentId : null,
|
||||
referenceId: master.destinationType === 'FBA' ? referenceId : null,
|
||||
sender: {
|
||||
@@ -272,11 +342,11 @@ function buildPayload(context, profile, master, productImageFileId) {
|
||||
email: 'order-acceptance@example.com', countryCode: 'CN', province: '广东省',
|
||||
city: '深圳市', district: '南山区', addressLine: `${context.marker} 测试地址`, postalCode: '518000',
|
||||
},
|
||||
receiver: null,
|
||||
cargoes: BOXES.map((box, index) => {
|
||||
receiver,
|
||||
cargoes: selectedBoxes.map((box, index) => {
|
||||
const volumeCbm = box.lengthCm * box.widthCm * box.heightCm / 1_000_000
|
||||
const unitsPerPackage = 10 + index
|
||||
const packageCount = Number(context.context?.phase3LoadingIncrementalPackageCount) === 2 ? 2 : 1
|
||||
const packageCount = packageCountPerSku
|
||||
return {
|
||||
skuNo: `SKU-${box.boxType}-${context.marker}`.slice(0, 64),
|
||||
cargoName: `${box.boxType} 测试产品`, cargoNameEn: `${box.boxType} test product`,
|
||||
@@ -300,11 +370,12 @@ function buildPayload(context, profile, master, productImageFileId) {
|
||||
}
|
||||
|
||||
function assertCargoContract(context, response, cargoes, payload) {
|
||||
const complete = Array.isArray(cargoes) && cargoes.length === BOXES.length
|
||||
&& BOXES.every((box, index) => {
|
||||
const actual = cargoes[index]; const expected = payload.cargoes[index]
|
||||
return Number(actual?.lengthCm) === box.lengthCm && Number(actual?.widthCm) === box.widthCm
|
||||
&& Number(actual?.heightCm) === box.heightCm && id(actual?.productImageFileId) === id(expected.productImageFileId)
|
||||
const expectedCargoes = payload.cargoes || []
|
||||
const complete = Array.isArray(cargoes) && cargoes.length === expectedCargoes.length
|
||||
&& expectedCargoes.every((expected, index) => {
|
||||
const actual = cargoes[index]
|
||||
return Number(actual?.lengthCm) === expected.lengthCm && Number(actual?.widthCm) === expected.widthCm
|
||||
&& Number(actual?.heightCm) === expected.heightCm && id(actual?.productImageFileId) === id(expected.productImageFileId)
|
||||
&& actual?.skuNo === expected.skuNo && actual?.cargoName === expected.cargoName
|
||||
&& actual?.cargoNameEn === expected.cargoNameEn && actual?.customsHsCode === expected.customsHsCode
|
||||
&& Number(actual?.unitsPerPackage) === expected.unitsPerPackage
|
||||
@@ -320,7 +391,7 @@ function assertCargoContract(context, response, cargoes, payload) {
|
||||
&& (actual?.referenceId ?? null) === (expected.referenceId ?? null)
|
||||
&& actual?.upcNo === expected.upcNo && actual?.cargoRemark.includes(context.marker)
|
||||
})
|
||||
context.assert(MODULE, '四箱型产品字段及图片契约完整回显', response, complete,
|
||||
context.assert(MODULE, '动态SKU产品字段及图片契约完整回显', response, complete,
|
||||
`cargoes=${Array.isArray(cargoes) ? cargoes.length : 'n/a'}, imageFileId=${payload.cargoes[0].productImageFileId}`,
|
||||
CONTRACT_OPTIONS)
|
||||
}
|
||||
@@ -405,9 +476,18 @@ export async function run(context) {
|
||||
})),
|
||||
boxTypes: BOXES.map((box) => box.boxType), inboundId: null,
|
||||
receivingWarehouseId: id(master.receiving.id),
|
||||
receivingWarehouseName: master.receiving.warehouseName,
|
||||
destinationType: master.destinationType,
|
||||
destinationWarehouseId: id(master.destination.id),
|
||||
destinationWarehouseCode: master.destination.warehouseCode,
|
||||
destinationWarehouseId: id(master.destination?.id),
|
||||
destinationWarehouseCode: master.destination?.warehouseCode || null,
|
||||
destinationWarehouseName: master.destination?.warehouseName || (master.destinationType === 'PRIVATE' ? '私人地址' : null),
|
||||
channelCode: master.channel.channelCode,
|
||||
channelName: master.channel.channelName,
|
||||
pricingTaxMode: master.pricingTaxMode,
|
||||
pickupMethod: payload.pickupMethod,
|
||||
taxMode: payload.taxMode,
|
||||
containerMode: payload.containerMode,
|
||||
customsInspection: payload.customsInspection,
|
||||
}
|
||||
|
||||
const replayResponse = await request(context, 'POST', createPath, payload, merchantToken,
|
||||
|
||||
@@ -4,8 +4,8 @@ const MODULE = 'phase3-customs'
|
||||
const OPTIONS = { scenario: 'phase3-customs-collaboration', deploymentBlocker: true }
|
||||
const PLATFORM = '/api/platform/v1/customs'
|
||||
const MERCHANT = '/api/merchant/v1/customs'
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin'
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!'
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
|
||||
|
||||
@@ -3,8 +3,8 @@ import { createHash } from 'node:crypto'
|
||||
|
||||
const MODULE = 'phase3-loading'
|
||||
const OPTIONS = { scenario: 'phase3-split-preload-loading', deploymentBlocker: true }
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin'
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!'
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
|
||||
const MATSON_SUPPLIER_NAME = 'MATSON 开发联调船司'
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const MODULE = 'phase3-order-splitting'
|
||||
const SCENARIO = 'source-loaded-single-remainder-chain'
|
||||
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin'
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!'
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
|
||||
const CONTAINERS = '/api/platform/v1/containers'
|
||||
const SMART = '/api/platform/v1/first-leg/smart-loading'
|
||||
const PDA = '/api/platform/v1/first-leg/pda-loading'
|
||||
|
||||
@@ -46,6 +46,10 @@ export async function run(context) {
|
||||
const module = 'supplemental'
|
||||
const scenario = 'service-health-and-auth-boundary'
|
||||
const blocker = { scenario, deploymentBlocker: true }
|
||||
const customerId = String(context.context.merchantQualificationFixture?.tenantId || '')
|
||||
context.assert(module, '客户作用域负向检查使用本轮正式客户', null, Boolean(customerId),
|
||||
`customerId=${customerId || 'missing'}`, blocker)
|
||||
if (!customerId) return
|
||||
|
||||
// Health endpoints are part of the deployed service contract. Warehouse is
|
||||
// special: a schema/readiness failure is intentionally HTTP 503, not a fake
|
||||
@@ -102,7 +106,7 @@ export async function run(context) {
|
||||
}
|
||||
for (const [name, path] of [
|
||||
['仓库非法状态', '/api/platform/v1/warehouse-admin?page=1&size=5&status=NOT_A_STATUS'],
|
||||
['仓库客户作用域', '/api/platform/v1/warehouse-admin?page=1&size=5&tenantId=10001'],
|
||||
['仓库客户作用域', `/api/platform/v1/warehouse-admin?page=1&size=5&tenantId=${encodeURIComponent(customerId)}`],
|
||||
]) {
|
||||
const response = await context.request('GET', path)
|
||||
context.addStep(module, `${name}安全失败`, response, isBusinessFailure(response),
|
||||
@@ -118,8 +122,8 @@ export async function run(context) {
|
||||
await checkPage(context, module, '托盘仓库筛选', `${palletBase}&warehouseId=${warehouseId}`, { scenario: 'warehouse-resources' })
|
||||
}
|
||||
const resourceScopeChecks = [
|
||||
['库位客户作用域', '/api/platform/v1/locations?page=1&size=5&tenantId=10001'],
|
||||
['托盘客户作用域', '/api/platform/v1/pallets?page=1&size=5&tenantId=10001'],
|
||||
['库位客户作用域', `/api/platform/v1/locations?page=1&size=5&tenantId=${encodeURIComponent(customerId)}`],
|
||||
['托盘客户作用域', `/api/platform/v1/pallets?page=1&size=5&tenantId=${encodeURIComponent(customerId)}`],
|
||||
['库位非法状态', `${locationBase}&status=ALLOCATED`],
|
||||
]
|
||||
for (const [name, path] of resourceScopeChecks) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const MODULE = 'suppliers'
|
||||
const OPTIONS = { scenario: 'platform-public-suppliers', deploymentBlocker: true }
|
||||
const BASE = '/api/platform/v1/suppliers'
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin'
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!'
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
|
||||
|
||||
function ok(response) { return response?.http === 200 && String(response?.body?.code) === '200' }
|
||||
function code(response, expected) { return response?.http === 200 && String(response?.body?.code) === expected }
|
||||
|
||||
Reference in New Issue
Block a user