merge: sync test and stabilize full acceptance
This commit is contained in:
@@ -32,6 +32,7 @@ test('initial fee preparation is reusable without confirming or publishing the f
|
||||
assert.match(helper, /\/fees\/latest/)
|
||||
assert.match(helper, /\/fees\/calculate/)
|
||||
assert.match(helper, /feeStatus === 'CALCULATED'/)
|
||||
assert.match(helper, /\$\{inboundId\}-inbound-fee-initial-calculate/)
|
||||
assert.doesNotMatch(helper, /\/fees\/confirm|\/fees\/send/)
|
||||
})
|
||||
|
||||
@@ -46,6 +47,9 @@ 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.match(source, /context\.context\?\.merchantQualificationFixture/)
|
||||
assert.match(source, /merchantFixture\?\.username/)
|
||||
assert.match(source, /merchantFixture\?\.password/)
|
||||
assert.doesNotMatch(source, /const orderId = id\(context\.createdResources\.orders\?\.orderId\)/)
|
||||
assert.match(source, /Client 读取已推送计费量、对客单价和费用合计/)
|
||||
assert.match(source, /Client 一次确认计费量、对客单价和费用/)
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function ensureInitialCalculated(context, fixture,
|
||||
if (!success(initialResponse) && String(initialResponse?.body?.code) === 'A0404') {
|
||||
initialResponse = await context.request('POST',
|
||||
`/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fees/calculate?tenantId=${encodeURIComponent(tenantId)}`,
|
||||
undefined, undefined, requestHeaders(`${context.marker}-inbound-fee-initial-calculate`))
|
||||
undefined, undefined, requestHeaders(`${context.marker}-${inboundId}-inbound-fee-initial-calculate`))
|
||||
}
|
||||
const initial = context.apiSuccess(MODULE, '读取或补齐审核后初始费用', initialResponse, '', options)
|
||||
context.assert(MODULE, '初始费用保持 WAIT_SPLIT 且尚未发布', initialResponse,
|
||||
@@ -132,8 +132,9 @@ export async function run(context) {
|
||||
`status=${sent?.feeStatus || 'missing'}, process=${sent?.nextProcessStatus || 'missing'}, publication=${published?.publicationStatus || 'missing'}`, OPTIONS)
|
||||
|
||||
const orderId = id(fixture?.orderId)
|
||||
const merchantFixture = context.context?.merchantQualificationFixture
|
||||
const merchantLoginResponse = await context.request('POST', '/api/auth/login', {
|
||||
loginId: context.options.merchantUsername, password: context.options.merchantPassword,
|
||||
loginId: merchantFixture?.username, password: merchantFixture?.password,
|
||||
}, '')
|
||||
const merchantLogin = context.apiSuccess(MODULE, 'Client 登录并准备确认计费量和费用', merchantLoginResponse,
|
||||
`orderId=${orderId || 'missing'}`, OPTIONS)
|
||||
|
||||
@@ -19,6 +19,11 @@ 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 ensureCompletionEvidence(context, chain, label) {
|
||||
const taskId = id(chain.task?.id)
|
||||
const warehouseId = id(chain.source?.warehouseId)
|
||||
|
||||
@@ -47,7 +47,7 @@ function context(resources = {}) {
|
||||
if (token === this.overseasToken) return response(path, null, 403, 'A0403')
|
||||
if (path.includes('/overview')) return response(path, { container: {
|
||||
id: '3001', containerNo: 'MSCU1234567', unloadingCompletedAt: '2026-08-24T10:00:00Z',
|
||||
unloadingPhotoUploadedAt: '2026-08-24T09:00:00Z', estimatedEmptyReportedAt: '2026-08-26T09:00:00Z',
|
||||
unloadingPhotoUploadedAt: '2026-08-24T09:00:00Z', estimatedEmptyReportedAt: '2026-08-27T10:00:00Z',
|
||||
actualEmptyReportedAt: null, emptyReportSourceVersion: 7,
|
||||
} })
|
||||
return response(path, { records: [{ container: { id: '3001', containerNo: 'MSCU1234567' } }] })
|
||||
@@ -103,6 +103,13 @@ test('cross-region producers do not repeat the destructive A0 snapshot mutation'
|
||||
assert.match(fixtureCall, /preparePhase3LoadingFixture\('', false\)/)
|
||||
})
|
||||
|
||||
test('tail controls receive their own completed producer matrix before they consume it', async () => {
|
||||
const entry = await import('node:fs/promises').then((fs) => fs.readFile(
|
||||
new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8'))
|
||||
assert.match(entry, /module === 'phase4-tail-order-controls' \|\| module === 'phase4-overseas-pda'/)
|
||||
assert.match(entry, /await this\.ensurePhase4PdaProducerMatrix\(\)/)
|
||||
})
|
||||
|
||||
test('contract covers dual audience, canonical 403, both event directions and diagnostics', () => {
|
||||
for (const token of [
|
||||
'context.token)', 'context.overseasToken)', 'cn-overseas-event-not-delivered',
|
||||
|
||||
@@ -126,7 +126,7 @@ export async function run(context) {
|
||||
const estimatedAt = Date.parse(snapshot.estimatedEmptyReportedAt)
|
||||
const validTimeline = Number.isFinite(completedAt) && Number.isFinite(uploadedAt)
|
||||
&& Number.isFinite(estimatedAt) && completedAt >= uploadedAt
|
||||
&& estimatedAt - uploadedAt === 2 * 24 * 60 * 60 * 1_000
|
||||
&& estimatedAt - completedAt === 72 * 60 * 60 * 1_000
|
||||
context.assert(MODULE, '海外柜完成经Bridge回写Firstleg时间事实', writtenBack.response,
|
||||
validTimeline && traced(writtenBack.response),
|
||||
evidence(writtenBack.response,
|
||||
|
||||
@@ -33,6 +33,11 @@ test('relabel transfer and discard cover create replay conflict list detail and
|
||||
assert.match(source, /chargeAmount: 0\.01/)
|
||||
assert.match(source, /cartonMark:/)
|
||||
assert.match(source, /fbaCartonMark:/)
|
||||
for (const field of [
|
||||
'operationWarehouseId:', 'sourceInventoryBalanceId:', 'sourceInventoryPositionId:', 'sourceInboundOrderId:',
|
||||
'destinationFbaWarehouseCode:',
|
||||
]) assert.match(source, new RegExp(field))
|
||||
assert.match(source, /query\(`\$\{RELABEL\}\/inventory-candidates`, \{/)
|
||||
for (const token of [
|
||||
'客户创建贴标申请', '客户贴标申请幂等重放', '贴标同幂等键异载荷返回A0409',
|
||||
'客户查询自身贴标申请列表', '客户查询自身贴标申请详情', 'PENDING_PROCESSING',
|
||||
|
||||
@@ -89,13 +89,30 @@ async function verifyRelabel(context, token, order) {
|
||||
`orderId=${id(order.row?.id) || 'missing'}, sku=${cargo?.skuCode || 'missing'}, warehouse=${order.row?.overseasWarehouseId || 'missing'}`)
|
||||
return false
|
||||
}
|
||||
const inventory = await merchant(context, token, 'GET', '客户查询贴标权威库存候选',
|
||||
query(`${RELABEL}/inventory-candidates`, {
|
||||
operationWarehouseId: order.row.overseasWarehouseId,
|
||||
inboundCode: order.row.inboundCode,
|
||||
}))
|
||||
const source = (Array.isArray(inventory.data) ? inventory.data : []).find((item) =>
|
||||
id(item?.inventoryBalanceId) && id(item?.inboundOrderId)
|
||||
&& item?.skuCode === cargo.skuCode
|
||||
&& item?.positions?.some?.((position) => id(position?.positionId) && Number(position?.availablePackages) > 0))
|
||||
const position = source?.positions?.find?.((item) => id(item?.positionId) && Number(item?.availablePackages) > 0)
|
||||
context.assert(MODULE, '贴标申请使用当前客户真实库存余额、位置和来源入库单', inventory.response,
|
||||
Boolean(source && position),
|
||||
`balanceId=${id(source?.inventoryBalanceId) || 'missing'}, positionId=${id(position?.positionId) || 'missing'}, inboundOrderId=${id(source?.inboundOrderId) || 'missing'}`, OPTIONS)
|
||||
if (!source || !position) return false
|
||||
const body = {
|
||||
customerOrderNo: order.row.orderNo,
|
||||
overseasWarehouseId: order.row.overseasWarehouseId,
|
||||
operationWarehouseId: order.row.overseasWarehouseId,
|
||||
labelDeadline: future(7), contactName: '客户贴标验收联系人', contactPhone: '13800000006',
|
||||
chargeAmount: 0.01,
|
||||
remark: context.marker, idempotencyKey: `${context.marker}-CLIENT-RELABEL`.slice(0, 255),
|
||||
items: [{
|
||||
sourceInventoryBalanceId: id(source.inventoryBalanceId),
|
||||
sourceInventoryPositionId: id(position.positionId),
|
||||
sourceInboundOrderId: id(source.inboundOrderId),
|
||||
skuCode: cargo.skuCode, productImageFileId: null, productImageFileName: null, productImageUrl: null,
|
||||
productNameZh: cargo.productNameZh, totalPackageCount: 1, notifiedPackageCount: 1,
|
||||
shipmentNo: cargo.shipmentNo || `${context.marker}-RELABEL-SHIP`, trackingCode: cargo.fbaTrackingNo || null,
|
||||
@@ -103,6 +120,7 @@ async function verifyRelabel(context, token, order) {
|
||||
relabelFileId: null, relabelFileName: null, relabelFileUrl: null,
|
||||
cartonMark: `CM-${String(context.marker).slice(-32)}`,
|
||||
fbaCartonMark: `FBA-${String(context.marker).slice(-32)}`,
|
||||
destinationFbaWarehouseCode: source.destinationFbaWarehouseCode,
|
||||
}],
|
||||
}
|
||||
const created = await merchant(context, token, 'POST', '客户创建贴标申请', RELABEL, body)
|
||||
|
||||
@@ -36,6 +36,7 @@ test('module covers the public tail-order write surface before PDA receiving', (
|
||||
assert.match(source, /feeItemCode/)
|
||||
assert.match(source, /feeItemName/)
|
||||
assert.match(source, /direction: 'RECEIVABLE'/)
|
||||
assert.match(source, /allIds\.length === 1 \|\| nonBlank\(groupRead\.data\?\.order\?\.palletMergeBatchId\)/)
|
||||
assert.match(source, /businessDate:/)
|
||||
assert.match(source, /originalCurrency === 'USD'/)
|
||||
assert.match(source, /currency === 'CNY'/)
|
||||
|
||||
@@ -183,7 +183,9 @@ async function verifyDestinationMergeFeeAndExport(context, details, producer) {
|
||||
const groupRead = await detail(context, representative, '回查批量合托关系')
|
||||
context.assert(MODULE, '批量合托关系包含本轮订单', groupRead.response,
|
||||
groupRead.data?.order?.palletMergeFlag === 'REQUIRED'
|
||||
&& nonBlank(groupRead.data?.order?.palletMergeBatchId), trace(groupRead.response), OPTIONS)
|
||||
&& (allIds.length === 1 || nonBlank(groupRead.data?.order?.palletMergeBatchId)),
|
||||
`orders=${allIds.length}, batchId=${groupRead.data?.order?.palletMergeBatchId || 'single-order'}; ${trace(groupRead.response)}`,
|
||||
OPTIONS)
|
||||
const batchCleared = await api(context, 'PUT', '批量取消PDA合托分组', batchMergePath,
|
||||
{ orderIds: allIds, palletMergeRequired: false })
|
||||
context.assert(MODULE, '批量合托分组取消成功', batchCleared.response, batchCleared.data == null, trace(batchCleared.response), OPTIONS)
|
||||
|
||||
@@ -43,7 +43,7 @@ test('phase-five module proves the client delivery create, list and nested detai
|
||||
assert.match(source, /id\(profile\?\.id\) === customerId/)
|
||||
assert.match(source, /客户创建派送并原子转出库单/)
|
||||
assert.match(source, /deliveryMethod: 'TRUCK_PALLET'/)
|
||||
assert.match(source, /fees: \[\{ feeType: 'DELIVERY_FEE', currency: 'USD', amount: 10/)
|
||||
assert.match(source, /fees: \[\{ feeType: 'DELIVERY_FEE', feeName: '派送费', direction: 'RECEIVABLE',\s*currency: 'USD', amount: 10/)
|
||||
assert.match(source, /客户分页回查自身派送单/)
|
||||
assert.match(source, /客户读取自身派送详情/)
|
||||
assert.match(source, /merchantDetail\.data\?\.order\?\.deliveryNo/)
|
||||
|
||||
@@ -179,7 +179,8 @@ export async function run(context) {
|
||||
vehicleType: 'TRUCK', vehicleSize: 'MEDIUM', driverName: updateBody.driverName,
|
||||
driverPhone: updateBody.driverPhone, vehiclePlate: updateBody.vehiclePlate,
|
||||
appointmentAccount: 'SELF', sellerName: null, remark: `${context.marker}-phase5-transfer`,
|
||||
fees: [{ feeType: 'DELIVERY_FEE', currency: 'USD', amount: 10, remark: `${context.marker}-acceptance` }],
|
||||
fees: [{ feeType: 'DELIVERY_FEE', feeName: '派送费', direction: 'RECEIVABLE',
|
||||
currency: 'USD', amount: 10, remark: `${context.marker}-acceptance` }],
|
||||
}
|
||||
const transferred = await merchant(context, merchantToken, 'POST', '客户创建派送并原子转出库单',
|
||||
query(MERCHANT_DELIVERY_BASE, { dispatchNo }), transferBody)
|
||||
|
||||
@@ -44,6 +44,8 @@ test('four real command payloads mutate contract fields and preserve remarks and
|
||||
const forbidden = index < 2 ? 'remark' : 'reason'
|
||||
assert.equal(Object.hasOwn(body, forbidden), false, `${actionPath} must not send ${forbidden}`)
|
||||
const key = headers['Idempotency-Key']
|
||||
assert.match(key, /^[A-Za-z0-9:_-]+$/)
|
||||
assert.ok(key.endsWith(`terminal-${actionPath}`))
|
||||
const saved = commands.get(key)
|
||||
if (saved) {
|
||||
if (JSON.stringify(saved.body) === JSON.stringify(body)) { replays++; return response(saved.state) }
|
||||
@@ -98,6 +100,8 @@ test('all four commands cover idempotent replay hash conflict and lifecycle sema
|
||||
'SELECT_FLEET', 'SCHEDULE_PICKUP', 'RECORD_PICKUP', 'RECORD_EMPTY_RETURN', 'RESOLVED',
|
||||
]) assert.ok(source.includes(token), `missing ${token}`)
|
||||
assert.match(source, /JSON\.stringify\(replay\.data\) === JSON\.stringify\(first\.data\)/)
|
||||
assert.match(source, /terminal-\$\{path\}.*replace\(\/\[\^A-Za-z0-9:_-\]\//)
|
||||
assert.doesNotMatch(source, /terminal-\$\{action\}/)
|
||||
})
|
||||
|
||||
test('target is strictly current-marker pickup and completed-PDA container', () => {
|
||||
|
||||
@@ -38,7 +38,7 @@ function fixture(context) {
|
||||
return null
|
||||
}
|
||||
async function command(context, target, action, path, body) {
|
||||
const key = `${context.marker}-terminal-${action}`.slice(0, 128)
|
||||
const key = `${context.marker}-terminal-${path}`.replace(/[^A-Za-z0-9:_-]/g, '').slice(0, 128)
|
||||
const first = await api(context, 'POST', `目的港码头拖车${action}`, `${BASE}/${target.containerBookingId}/${path}`,
|
||||
body, { 'Idempotency-Key': key })
|
||||
if (!success(first.response)) return first
|
||||
|
||||
Reference in New Issue
Block a user