fix: complete October 22 workflow feedback and request failure recovery
This commit is contained in:
@@ -226,6 +226,10 @@ PDA 待复核通过后,`manual-loading-complete-fixture.mjs` 冻结无证据
|
||||
|
||||
`fee-items` 需要同一 marker 的客户订单和入库单。执行 `--modules fee-items` 时,入口自动扩展为 `merchant-qualification-fixture,orders,fee-items`,无需手工补齐前置模块。
|
||||
|
||||
固定客户模式下 `fee-items` 只查询现有费用目录、核对112条正式基线及客户权限,不新增、编辑或删除费用主档,增删改生命周期单独记录为未覆盖。所有模式均不自动删除历史 `ACPT_` 项目;非固定模式的隔离写入仅清理本轮返回的项目和明细 ID。
|
||||
|
||||
`finance-full-fixtures` 在固定客户模式下只复用已有不包税计费规则:唯一规则或唯一默认规则必须属于当前客户并匹配所需计费方式;缺失、不匹配或默认规则有歧义即停止,不自动创建规则版本或切换默认规则。供应商同样复用既有启用记录;下述创建隔离供应商的说明仅适用于非固定客户模式。
|
||||
|
||||
模块只需导出:
|
||||
|
||||
```js
|
||||
|
||||
@@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
import { normalizeModuleOrder, parseArguments } from '../admin-test-env-acceptance.mjs'
|
||||
import { cleanupSucceeded, isAcceptanceFeeItem, formalFeeItems } from './fee-items.mjs'
|
||||
import { cleanupSucceeded, formalFeeItems, run } from './fee-items.mjs'
|
||||
|
||||
const source = readFileSync(new URL('./fee-items.mjs', import.meta.url), 'utf8')
|
||||
|
||||
@@ -23,15 +23,44 @@ test('cleanup treats successful deletion and an already absent resource as clean
|
||||
assert.equal(cleanupSucceeded({ http: 200, body: { code: 'A0422' } }), false)
|
||||
})
|
||||
|
||||
test('stale cleanup only recognizes the acceptance-owned code and name pair', () => {
|
||||
assert.equal(isAcceptanceFeeItem({ feeCode: 'ACPT_813083345572', feeTypeName: '全量验收费用-813083345572-已编辑' }), true)
|
||||
assert.equal(isAcceptanceFeeItem({ feeCode: 'FEE_TYPE_001', feeTypeName: '全量验收费用-001' }), false)
|
||||
assert.equal(isAcceptanceFeeItem({ feeCode: 'ACPT_813083345572', feeTypeName: '正式费用项目' }), false)
|
||||
test('no automatic historical fee-item cleanup remains', () => {
|
||||
assert.doesNotMatch(source, /cleanupStaleAcceptanceItems|isAcceptanceFeeItem/)
|
||||
})
|
||||
|
||||
test('baseline checks run only after stale acceptance cleanup', () => {
|
||||
assert.ok(source.indexOf('cleanupStaleAcceptanceItems(context)')
|
||||
< source.indexOf("'全部目录默认20条分页与完整列表一致'"))
|
||||
for (const missingBaseline of [false, true]) test(`fixed fee catalogue preserves old test items and performs no master writes; missing=${missingBaseline}`, async () => {
|
||||
const codes = ['FBA_HEAD_FREIGHT', 'PRIVATE_LAST_MILE_DELIVERY', ...Array.from({ length: 110 }, (_, i) => `FEE_TYPE_${String(i + 1).padStart(3, '0')}`)]
|
||||
const catalogue = codes.map((feeCode, i) => ({ id: String(i + 1), feeCode, tenantId: '0', status: 'ACTIVE',
|
||||
tradeScope: i < 84 ? 'DOMESTIC' : 'INTERNATIONAL', category: '标准', feeTypeName: `费用${i}` }))
|
||||
catalogue.push({ id: '999', feeCode: 'ACPT_123456', feeTypeName: '全量验收费用-123456', tenantId: '0', status: 'DISABLED', category: '临时', tradeScope: 'DOMESTIC' })
|
||||
if (missingBaseline) catalogue.splice(3, 1)
|
||||
const calls = [], gaps = []
|
||||
const context = { options: { fixedCustomerId: '101' }, marker: 'TEST', createdResources: {},
|
||||
async request(method, path, body, token) {
|
||||
calls.push({ method, path })
|
||||
const response = data => ({ http: 200, path, body: { code: '200', data } })
|
||||
if (path === '/api/auth/login') return response({ accessToken: 'client', refreshToken: 'refresh' })
|
||||
if (path === '/api/auth/logout') return response({})
|
||||
assert.equal(method, 'GET')
|
||||
if (token === 'client') return { http: 403, body: { code: 'A0403' } }
|
||||
const params = new URL(path, 'https://test.invalid').searchParams
|
||||
const selected = catalogue.filter(row => ['category', 'tradeScope', 'status'].every(key => !params.has(key) || row[key] === params.get(key))
|
||||
&& (!params.has('keyword') || row.feeTypeName.includes(params.get('keyword'))))
|
||||
const size = Number(params.get('size')), page = Number(params.get('page'))
|
||||
return response({ records: selected.slice((page - 1) * size, page * size), total: selected.length, page, size, pages: Math.ceil(selected.length / size) })
|
||||
}, apiSuccess(_m, _n, response) { return response.body.data },
|
||||
assert(_m, name, _r, passed) { assert.ok(passed, name) },
|
||||
addCoverageGap(...args) { gaps.push(args) },
|
||||
}
|
||||
const before = structuredClone(catalogue)
|
||||
if (missingBaseline) await assert.rejects(run(context), /112条/)
|
||||
else {
|
||||
await run(context)
|
||||
assert.equal(context.createdResources.feeItems.mode, 'READ_ONLY')
|
||||
assert.equal(gaps.length, 1)
|
||||
assert.equal(calls.filter(row => row.path === '/api/auth/logout').length, 1)
|
||||
}
|
||||
assert.deepEqual(catalogue, before)
|
||||
assert.ok(calls.every(row => row.method === 'GET' || row.path.startsWith('/api/auth/')))
|
||||
})
|
||||
|
||||
test('正式费用按编码识别,扩展停用项目保留且不掩盖缺失正式编码', () => {
|
||||
|
||||
@@ -56,10 +56,6 @@ export function formalFeeItems(rows) {
|
||||
const codes = new Set(expectedCodes())
|
||||
return rows.filter(row => codes.has(row.feeCode))
|
||||
}
|
||||
export function isAcceptanceFeeItem(row) {
|
||||
return /^ACPT_\d+$/.test(String(row?.feeCode || ''))
|
||||
&& String(row?.feeTypeName || '').startsWith('全量验收费用-')
|
||||
}
|
||||
function fixture(context) {
|
||||
const candidates = [context.createdResources?.orders, context.createdResources?.testInbound,
|
||||
context.createdResources?.inboundMeasurement, context.context?.testInbound]
|
||||
@@ -73,26 +69,7 @@ async function listAll(context) {
|
||||
return { response, page, rows: records(page) }
|
||||
}
|
||||
|
||||
async function cleanupStaleAcceptanceItems(context) {
|
||||
const listed = await listAll(context)
|
||||
const stale = listed.rows.filter(isAcceptanceFeeItem)
|
||||
const failed = []
|
||||
for (const row of stale) {
|
||||
const response = await context.request('DELETE', `${BASE}/${encodeURIComponent(id(row.id))}`)
|
||||
if (!cleanupSucceeded(response)) failed.push({ row, response })
|
||||
}
|
||||
const response = failed[0]?.response || listed.response
|
||||
context.assert(MODULE, '前置清理历史验收临时费用项目', response, failed.length === 0,
|
||||
failed.length === 0
|
||||
? `stale=${stale.length}, cleaned=${stale.length}`
|
||||
: `failed=${failed.map(({ row, response: failure }) => `${row.feeCode}:${failure.body?.code || failure.http}`).join(',')}`,
|
||||
OPTIONS)
|
||||
return failed.length === 0
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
if (!await cleanupStaleAcceptanceItems(context)) return
|
||||
|
||||
const initialPageResponse = await context.request('GET', query({ page: 1, size: 20 }))
|
||||
const initialPage = context.apiSuccess(MODULE, '费用项目查看权限与分页查询', initialPageResponse,
|
||||
'page=1,size=20', OPTIONS)
|
||||
@@ -158,12 +135,26 @@ export async function run(context) {
|
||||
}, '')
|
||||
const merchantLogin = success(merchantLoginResponse) ? merchantLoginResponse.body?.data : null
|
||||
const merchantToken = merchantLogin?.accessToken
|
||||
try {
|
||||
const forbiddenResponse = merchantToken
|
||||
? await context.request('GET', query({ page: 1, size: 20 }), undefined, merchantToken)
|
||||
: merchantLoginResponse
|
||||
context.assert(MODULE, '无费用项目平台权限的客户账号被拒绝', forbiddenResponse,
|
||||
Boolean(merchantToken) && forbidden(forbiddenResponse),
|
||||
`HTTP=${forbiddenResponse.http}, code=${forbiddenResponse.body?.code ?? 'missing'}`, OPTIONS)
|
||||
} finally {
|
||||
if (merchantToken && merchantLogin.refreshToken) {
|
||||
const logout = await context.request('POST', '/api/auth/logout', { refreshToken: merchantLogin.refreshToken }, merchantToken)
|
||||
context.assert(MODULE, '退出费用目录权限核验会话', logout, success(logout), '', OPTIONS)
|
||||
}
|
||||
}
|
||||
|
||||
if (context.options?.fixedCustomerId) {
|
||||
context.createdResources.feeItems = { marker: context.marker, mode: 'READ_ONLY', baselineCount: initialRows.length }
|
||||
context.addCoverageGap(MODULE, 'fixed-fee-item-lifecycle', '费用项目增删改生命周期',
|
||||
'固定客户模式只读核对现有费用目录,不创建、修改或删除公共主档及历史测试项目', 'not-implemented', { blocking: false })
|
||||
return
|
||||
}
|
||||
|
||||
const suffix = context.marker.replace(/\D/g, '').slice(-12)
|
||||
const createPayload = payload(suffix)
|
||||
|
||||
@@ -4,7 +4,33 @@ import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { buildContainerCostBody, buildSupplierBody, normalizeFinanceFixtureSources, run,
|
||||
supplierCategoryForFeeItem, loadSupplier } from './finance-full-fixtures.mjs'
|
||||
supplierCategoryForFeeItem, loadSupplier, ensureCustomerChargeRule } from './finance-full-fixtures.mjs'
|
||||
|
||||
for (const mode of ['single', 'default', 'missing', 'wrong mode', 'foreign customer', 'ambiguous', 'two defaults']) {
|
||||
test(`fixed customer charge rules are read-only and fail closed: ${mode}`, async () => {
|
||||
const good = { id: '701', customerId: '456', status: 'ACTIVE', businessType: 'FBA', taxMode: 'TAX_EXCLUDED', chargeMode: 'CBM', defaultRule: false }
|
||||
let rules = [good]
|
||||
if (mode === 'default') rules = [{ ...good, defaultRule: true }, { ...good, id: '702', chargeMode: 'KG' }]
|
||||
if (mode === 'missing') rules = []
|
||||
if (mode === 'wrong mode') rules = [{ ...good, chargeMode: 'KG' }]
|
||||
if (mode === 'foreign customer') rules = [{ ...good, customerId: '999' }]
|
||||
if (mode === 'ambiguous') rules = [good, { ...good, id: '702', chargeMode: 'KG' }]
|
||||
if (mode === 'two defaults') rules = [{ ...good, defaultRule: true }, { ...good, id: '702', defaultRule: true }]
|
||||
const calls = [], checks = [], before = structuredClone(rules)
|
||||
const context = { options: { fixedCustomerId: '456' },
|
||||
async request(method, path) { calls.push({ method, path }); assert.equal(method, 'GET');
|
||||
return { body: { data: path.includes('/inbounds/') ? { summary: { businessType: 'FBA' } } : rules } } },
|
||||
apiSuccess(_m, _n, response) { return response.body.data },
|
||||
assert(_m, _n, _r, passed) { checks.push(passed) },
|
||||
}
|
||||
const invoke = () => ensureCustomerChargeRule(context, { tenantId: '456', inboundId: '100' }, { pricingTaxMode: 'TAX_EXCLUDED', chargeUnit: 'CBM' })
|
||||
if (['single', 'default'].includes(mode)) assert.equal((await invoke()).id, '701')
|
||||
else await assert.rejects(invoke(), /禁止自动创建或切换/)
|
||||
assert.deepEqual(rules, before)
|
||||
assert.equal(calls.length, 2)
|
||||
assert.equal(checks.at(-1), ['single', 'default'].includes(mode))
|
||||
})
|
||||
}
|
||||
|
||||
test('fixed customer supplier selection paginates and never creates a fallback', async () => {
|
||||
const calls = [], failures = []
|
||||
|
||||
@@ -366,7 +366,7 @@ async function loadPricingEvidence(context) {
|
||||
return { activeVersionIds: active.map((item) => id(item.id)).filter(Boolean), profiles }
|
||||
}
|
||||
|
||||
async function ensureCustomerChargeRule(context, resource, profile) {
|
||||
export async function ensureCustomerChargeRule(context, resource, profile) {
|
||||
if (context.financeFixtureHooks?.ensureCustomerChargeRule) {
|
||||
return context.financeFixtureHooks.ensureCustomerChargeRule(context, resource, profile)
|
||||
}
|
||||
@@ -389,6 +389,15 @@ async function ensureCustomerChargeRule(context, resource, profile) {
|
||||
const applicable = rows(listed).filter((rule) => rule?.status === 'ACTIVE'
|
||||
&& rule?.businessType === businessType && rule?.taxMode === 'TAX_EXCLUDED')
|
||||
let desired = applicable.find((rule) => rule?.chargeMode === profile.chargeUnit)
|
||||
if (context.options?.fixedCustomerId) {
|
||||
const defaults = applicable.filter(rule => rule.defaultRule === true)
|
||||
const selected = applicable.length === 1 ? applicable[0] : defaults.length === 1 ? defaults[0] : null
|
||||
const valid = selected?.id && id(selected.customerId) === tenantId && selected.chargeMode === profile.chargeUnit
|
||||
context.assert(MODULE, '固定客户复用现有有效默认计费方式', listResponse, Boolean(valid),
|
||||
`customerId=${tenantId}, chargeMode=${profile.chargeUnit}`, OPTIONS)
|
||||
if (!valid) throw new Error('固定客户缺少匹配且无歧义的计费规则,禁止自动创建或切换默认规则')
|
||||
return selected
|
||||
}
|
||||
if (!desired) {
|
||||
const effectiveFrom = new Date(Date.now() - 86_400_000).toISOString().slice(0, 19)
|
||||
const body = {
|
||||
|
||||
@@ -11,11 +11,12 @@ const acceptanceUrl = new URL('../admin-test-env-acceptance.mjs', import.meta.ur
|
||||
|
||||
test('both pallet receipt batches supply SKU box counts and measured pallet fields', () => {
|
||||
for (const [sku, quantity, startedBy] of [['A', 3, 'RETURN_NO'], ['B', 2, 'SKU']]) {
|
||||
const body = palletReceiptBody('RT-1', `P-${sku}`, sku, quantity, `KEY-${sku}`, startedBy)
|
||||
const body = palletReceiptBody('RT-1', `P-${sku}`, sku, quantity, `KEY-${sku}`, startedBy, sku === 'A' ? 0 : 3)
|
||||
assert.equal(body.unitType, 'PALLET')
|
||||
assert.equal(body.unitCode, `P-${sku}`)
|
||||
assert.equal(body.palletCode, body.unitCode)
|
||||
assert.equal(body.startedBy, startedBy)
|
||||
assert.equal(body.expectedReceivedQuantity, sku === 'A' ? 0 : 3)
|
||||
assert.deepEqual(body.items, [{ skuCode: sku, receivedQuantity: quantity, boxCount: 1 }])
|
||||
for (const field of ['length', 'width', 'height', 'weight']) assert.ok(body[field] > 0)
|
||||
assert.equal(body.totalBoxCount, undefined, 'server derives the total from SKU box counts')
|
||||
|
||||
@@ -263,7 +263,7 @@ async function runWithSession(context, merchantToken) {
|
||||
|| !success(palletB.response) || !nonBlank(palletB.data?.palletCode)) throw new Error('签收托盘准备失败')
|
||||
const palletCodeA = palletA.data.palletCode
|
||||
const firstBody = {
|
||||
...palletReceiptBody(returnNo, palletCodeA, skuA, 3, key(context, 'RECEIPT-1'), 'RETURN_NO'),
|
||||
...palletReceiptBody(returnNo, palletCodeA, skuA, 3, key(context, 'RECEIPT-1'), 'RETURN_NO', 0),
|
||||
trackingNo: `TRACK-${suffix}`,
|
||||
}
|
||||
const first = await overseas(context, picker.token, 'POST', 'PDA首次单SKU签收甲3件', `${PDA_RECEIPTS}/confirm`, firstBody)
|
||||
@@ -291,7 +291,7 @@ async function runWithSession(context, merchantToken) {
|
||||
|
||||
await reject('超收被拒绝且不得生成费用批次', conflict, async () => {
|
||||
const body = {
|
||||
...palletReceiptBody(returnNo, palletCodeA, skuA, 2, key(context, 'OVER-RECEIPT'), 'SKU'),
|
||||
...palletReceiptBody(returnNo, palletCodeA, skuA, 2, key(context, 'OVER-RECEIPT'), 'SKU', 3),
|
||||
}
|
||||
let response = null
|
||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||
@@ -310,7 +310,7 @@ async function runWithSession(context, merchantToken) {
|
||||
['非法签收方式', { startedBy: 'INVALID' }, validationFailure],
|
||||
['未预报SKU', { items: [{ skuCode: `UNKNOWN-${suffix}`, receivedQuantity: 1, boxCount: 1 }] }, conflict],
|
||||
]) {
|
||||
await reject(`拒绝${name}`, expected, () => context.request('POST', `${PDA_RECEIPTS}/confirm`, { ...firstBody, ...changes, idempotencyKey: key(context, `INVALID-RECEIPT-${encodeURIComponent(name)}`) }, picker.token, {}))
|
||||
await reject(`拒绝${name}`, expected, () => context.request('POST', `${PDA_RECEIPTS}/confirm`, { ...firstBody, expectedReceivedQuantity: 3, ...changes, idempotencyKey: key(context, `INVALID-RECEIPT-${encodeURIComponent(name)}`) }, picker.token, {}))
|
||||
}
|
||||
await reject('部分签收退货单禁止合箱', conflict, () => context.request('POST', '/api/pda/v1/last-leg/return-inventory/cartons/merge', {
|
||||
returnNos: [returnNo], items: [{ skuCode: skuA, quantity: 1 }], length: 30, width: 20, height: 10,
|
||||
@@ -327,7 +327,7 @@ async function runWithSession(context, merchantToken) {
|
||||
`receipts=${afterPartial.data?.receipts?.length ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const secondBody = {
|
||||
...palletReceiptBody(returnNo, palletB.data.palletCode, skuB, 2, key(context, 'RECEIPT-2'), 'SKU'),
|
||||
...palletReceiptBody(returnNo, palletB.data.palletCode, skuB, 2, key(context, 'RECEIPT-2'), 'SKU', 3),
|
||||
}
|
||||
const second = await overseas(context, picker.token, 'POST', 'PDA无照片按单SKU完成乙2件签收', `${PDA_RECEIPTS}/confirm`, secondBody)
|
||||
if (!success(second.response)) throw new Error('最终签收失败,停止合箱和换标后续')
|
||||
@@ -419,6 +419,7 @@ async function runWithSession(context, merchantToken) {
|
||||
|
||||
const mergeBody = {
|
||||
sourceCartonMarks: [cartonA, cartonB],
|
||||
expectedSourceQuantities: [{ cartonMark: cartonA, skuCode: skuA, quantity: 3 }, { cartonMark: cartonB, skuCode: skuB, quantity: 2 }],
|
||||
sourceDeductions: [{ cartonMark: cartonA, skuCode: skuA, quantity: 1 }, { cartonMark: cartonB, skuCode: skuB, quantity: 1 }],
|
||||
length: 30, width: 20, height: 10, weight: 2, idempotencyKey: key(context, 'MERGE'),
|
||||
}
|
||||
@@ -469,7 +470,7 @@ async function runWithSession(context, merchantToken) {
|
||||
['空来源箱', { sourceCartonMarks: [] }, validationFailure],
|
||||
['零合箱数量', { sourceDeductions: [{ cartonMark: cartonA, skuCode: skuA, quantity: 0 }] }, validationFailure],
|
||||
['超可用库存', { sourceDeductions: [{ cartonMark: cartonA, skuCode: skuA, quantity: 999 }] }, conflict],
|
||||
['不存在来源箱', { sourceCartonMarks: [`MISSING-${suffix}`] }, conflict],
|
||||
['不存在来源箱', { sourceCartonMarks: [`MISSING-${suffix}`], expectedSourceQuantities: [{ cartonMark: `MISSING-${suffix}`, skuCode: skuA, quantity: 1 }] }, conflict],
|
||||
]) {
|
||||
await reject(`合箱拒绝${name}`, expected, () => context.request('POST', '/api/pda/v1/last-leg/return-inventory/cartons/merge-boxes', { ...mergeBody, ...changes, idempotencyKey: key(context, `INVALID-MERGE-${encodeURIComponent(name)}`) }, picker.token, {}))
|
||||
}
|
||||
|
||||
@@ -47,9 +47,9 @@ export function exactReceiptLedger(data, expected) {
|
||||
&& JSON.stringify(stableState(rows(data).map(project))) === JSON.stringify(stableState(expected.map(project)))
|
||||
}
|
||||
|
||||
export function palletReceiptBody(returnNo, palletCode, skuCode, receivedQuantity, idempotencyKey, startedBy) {
|
||||
export function palletReceiptBody(returnNo, palletCode, skuCode, receivedQuantity, idempotencyKey, startedBy, expectedReceivedQuantity) {
|
||||
return {
|
||||
returnNo, palletCode, unitType: 'PALLET', unitCode: palletCode, startedBy,
|
||||
returnNo, palletCode, unitType: 'PALLET', unitCode: palletCode, startedBy, expectedReceivedQuantity,
|
||||
length: 120, width: 100, height: 80, weight: 10,
|
||||
items: [{ skuCode, receivedQuantity, boxCount: 1 }], idempotencyKey,
|
||||
}
|
||||
@@ -109,7 +109,7 @@ export async function runReturnBoundaries(context, { merchantToken, warehouse, p
|
||||
}
|
||||
function receiptBody(order, quantity = order.quantity, idempotencyKey = key('RECEIPT')) {
|
||||
// 合成包裹明确原包装选择与实测输入,使边界场景进入待验证的库存/并发规则。
|
||||
return { returnNo: order.returnNo, startedBy: 'RETURN_NO', unitType: 'PARCEL', unitCode: order.unitCode,
|
||||
return { returnNo: order.returnNo, expectedReceivedQuantity: 0, startedBy: 'RETURN_NO', unitType: 'PARCEL', unitCode: order.unitCode,
|
||||
originalPackaging: true, length: 30, width: 20, height: 10, weight: 2,
|
||||
items: [{ skuCode: order.sku, receivedQuantity: quantity }], idempotencyKey }
|
||||
}
|
||||
@@ -399,9 +399,9 @@ export async function runReturnBoundaries(context, { merchantToken, warehouse, p
|
||||
const withLedger = async () => ({ inventory: await snapshot([order]), ledger: await ledger() })
|
||||
await verifyNoMutation({ snapshot: withLedger, action: () => request(picker.token, 'POST', `${RECEIPTS}/confirm`, body),
|
||||
check, name: '签收重放不重复产生流水', expected: result => ok(result) && result.body.data?.receiptNo === signed.receiptNo })
|
||||
await verifyNoMutation({ snapshot: withLedger, action: () => request(picker.token, 'POST', `${RECEIPTS}/confirm`, receiptBody(order, 2)),
|
||||
await verifyNoMutation({ snapshot: withLedger, action: () => request(picker.token, 'POST', `${RECEIPTS}/confirm`, { ...receiptBody(order, 2), expectedReceivedQuantity: 2 }),
|
||||
check, name: '超收失败不留下流水', expected: result => rejected(result, [409]) })
|
||||
const final = await good(picker.token, 'POST', '流水场景第二批实收一件', `${RECEIPTS}/confirm`, { ...receiptBody(order, 1), unitCode: `${order.unitCode}-SECOND` })
|
||||
const final = await good(picker.token, 'POST', '流水场景第二批实收一件', `${RECEIPTS}/confirm`, { ...receiptBody(order, 1), expectedReceivedQuantity: 2, unitCode: `${order.unitCode}-SECOND` })
|
||||
const data = await ledger()
|
||||
check('两批签收流水业务引用唯一且分别两件一件', null, exactReceiptLedger(data,
|
||||
[entry, { ...entry, referenceNo: final.receiptNo, totalDelta: 1, availableDelta: 1 }]) && signed.receiptNo !== final.receiptNo)
|
||||
|
||||
@@ -1296,7 +1296,7 @@ $criticalPaths = @(
|
||||
'apps/pda-uniapp/tests/app-update.test.mjs',
|
||||
'apps/pda-uniapp/tests/page-contract.test.mjs',
|
||||
'apps/pda-uniapp/tests/pda-loading-api.test.mjs',
|
||||
'apps/pda-uniapp/utils/evidence-upload-queue.js',
|
||||
'apps/pda-uniapp/tests/picking-failure-recovery.test.mjs',
|
||||
'apps/pda-overseas-uniapp/package.json',
|
||||
'apps/pda-overseas-uniapp/src/pages.json',
|
||||
'apps/pda-overseas-uniapp/src/data/overseas-workbench.js',
|
||||
|
||||
@@ -86,7 +86,7 @@ try {
|
||||
const pallet = await command('os', picker.token, 'POST', `${receipts}/pallets`, { returnNo, skuCode })
|
||||
const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aWQAAAABJRU5ErkJggg==', 'base64')
|
||||
await upload(picker.token, `${receipts}/${returnNo}/photos/files`, png, 'synthetic-receipt.png', 'image/png')
|
||||
const receiptBody = { returnNo, startedBy: 'RETURN_NO', palletCode: pallet.palletCode, items: [{ skuCode, receivedQuantity: receiptQuantity }], idempotencyKey: `${context.marker}-RECEIPT` }
|
||||
const receiptBody = { returnNo, expectedReceivedQuantity: 0, startedBy: 'RETURN_NO', palletCode: pallet.palletCode, items: [{ skuCode, receivedQuantity: receiptQuantity }], idempotencyKey: `${context.marker}-RECEIPT` }
|
||||
const received = await command('os', picker.token, 'POST', `${receipts}/confirm`, receiptBody)
|
||||
const replay = await command('os', picker.token, 'POST', `${receipts}/confirm`, receiptBody)
|
||||
check('Receipt completed once with one stock and fee batch', received.orderStatus === 'COMPLETED' && Number(received.receivedQuantity) === receiptQuantity && received.receiptNo === replay.receiptNo && received.receiptBatchNo === replay.receiptBatchNo)
|
||||
|
||||
Reference in New Issue
Block a user