feat(procurement): implement sample fulfillment and settled fee workflow
This commit is contained in:
@@ -29,11 +29,10 @@ function fixture(options = {}) {
|
||||
const credentials = options.credentials === false ? {} : { username: 'merchant', password: 'secret' }
|
||||
|
||||
function purchaseDetail() {
|
||||
return { summary: state.purchase, productUrl: state.purchase?.productUrl ?? null,
|
||||
domesticCarrier: state.purchase?.domesticCarrier ?? null, domesticTrackingNo: state.purchase?.domesticTrackingNo ?? null,
|
||||
estimatedArrivalAt: state.purchase?.estimatedArrivalAt ?? null, remark: state.purchase?.remark ?? null,
|
||||
histories: state.purchaseHistories }
|
||||
return { summary: state.purchase, remark: state.purchase?.remark ?? null, histories: state.purchaseHistories }
|
||||
}
|
||||
function purchaseCustomer() { return { order: state.purchase, fees: [], progress: [], boxes: [], shipments: [], exceptionSummaries: [] } }
|
||||
function purchasePlatform() { return { customerView: purchaseCustomer(), internalRemark: state.purchase?.remark ?? null, reviewHistory: state.purchaseHistories } }
|
||||
function truckDetail() { return { summary: state.truck, remark: state.truck?.remark ?? null, histories: state.truckHistories } }
|
||||
function looseboardDetail() { return { summary: state.looseboard, remark: state.looseboard?.remark ?? null, histories: state.looseboardHistories } }
|
||||
|
||||
@@ -49,15 +48,15 @@ function fixture(options = {}) {
|
||||
if (requestPath === '/api/merchant/v1/last-leg/overseas-procurements' && method === 'POST') {
|
||||
assert.equal(token, 'merchant-overseas-token')
|
||||
const hash = JSON.stringify(body)
|
||||
if (state.purchase) return hash === state.purchaseHash ? response(purchaseDetail()) : response(null, 'A0409')
|
||||
if (state.purchase) return hash === state.purchaseHash ? response(purchaseCustomer()) : response(null, 'A0409')
|
||||
state.purchaseHash = hash
|
||||
state.purchase = { id: '30001', orderNo: 'OP1', customerId: '10001', customerName: body.customerName,
|
||||
purchaseWarehouseId: null, purchaseWarehouseName: null, purchaseCategory: body.purchaseCategory,
|
||||
productName: body.productName, productSpecification: body.productSpecification, productUrl: body.productUrl,
|
||||
purchaseQuantity: body.purchaseQuantity, inboundQuantity: 0, inventoryQuantity: 0,
|
||||
purchaseQuantity: body.purchaseQuantity,
|
||||
status: 'PENDING_REVIEW', version: 0, remark: body.remark }
|
||||
state.purchaseHistories.push({ operationType: 'CREATE', fromStatus: null, toStatus: 'PENDING_REVIEW', remark: marker })
|
||||
return response(purchaseDetail())
|
||||
return response(purchaseCustomer())
|
||||
}
|
||||
if (requestPath.includes('/customer-options')) return response(state.purchase
|
||||
? [{ customerId: '10001', customerName: '客户甲' }] : [])
|
||||
@@ -71,24 +70,19 @@ function fixture(options = {}) {
|
||||
if (replay) return replay.hash === hash ? response(replay.result) : response(null, 'A0409')
|
||||
if (state.purchase.status !== 'PENDING_REVIEW') return response(null, 'A0409')
|
||||
if (Number(body.version) !== state.purchase.version) return response(null, 'A0409')
|
||||
const next = body.decision === 'REJECT' ? 'REJECTED' : 'APPROVED'
|
||||
const next = body.decision === 'REJECT' ? 'REJECTED' : 'WAIT_PURCHASE'
|
||||
state.purchase = { ...state.purchase, status: next, version: state.purchase.version + 1 }
|
||||
state.purchaseHistories.push({ operationType: 'REVIEW', fromStatus: 'PENDING_REVIEW', toStatus: next, remark: body.remark })
|
||||
const result = structuredClone(purchaseDetail())
|
||||
state.purchaseReviews.set(body.idempotencyKey, { hash, result })
|
||||
return response(result)
|
||||
}
|
||||
if (requestPath.includes('/overseas-procurements/OP1?') && method === 'GET') return response(purchaseDetail())
|
||||
if (requestPath.includes('/overseas-procurements/OP1?') && method === 'GET') return response(purchasePlatform())
|
||||
if (requestPath.includes('/overseas-procurements/OP1?') && method === 'PATCH') {
|
||||
if (Number(body.version) !== state.purchase.version) return response(null, 'A0409')
|
||||
const patch = { ...body }; delete patch.version
|
||||
for (const key of ['productSpecification', 'productUrl', 'domesticCarrier', 'domesticTrackingNo', 'remark']) {
|
||||
if (Object.hasOwn(patch, key)) patch[key] = blankToNull(patch[key])
|
||||
}
|
||||
if (Object.hasOwn(patch, 'purchaseWarehouseId') && blankToNull(patch.purchaseWarehouseId) == null) {
|
||||
patch.purchaseWarehouseId = null; patch.purchaseWarehouseName = null
|
||||
}
|
||||
state.purchase = { ...state.purchase, ...patch, version: state.purchase.version + 1 }
|
||||
if (Object.keys(body).some(key => !['version', 'remark'].includes(key))) return response(null, 'A0400')
|
||||
if (!Object.hasOwn(body, 'remark') || blankToNull(body.remark) === state.purchase.remark) return response(purchaseDetail())
|
||||
state.purchase = { ...state.purchase, remark: blankToNull(body.remark), version: state.purchase.version + 1 }
|
||||
state.purchaseHistories.push({ operationType: 'UPDATE', fromStatus: state.purchase.status,
|
||||
toStatus: state.purchase.status, changeDetail: JSON.stringify({ marker }) })
|
||||
return response(purchaseDetail())
|
||||
@@ -180,7 +174,8 @@ function fixture(options = {}) {
|
||||
} : async function(method,requestPath,body) {
|
||||
calls.push({ channel: 'domestic', method, path: requestPath, body })
|
||||
if (requestPath.includes('/iam/tenants?')) return response(page(activeCustomers))
|
||||
if (requestPath === '/api/auth/login') return response({ accessToken: 'merchant-overseas-token' })
|
||||
if (requestPath === '/api/auth/login') return response({ accessToken: 'merchant-overseas-token', refreshToken: 'merchant-refresh-token' })
|
||||
if (requestPath === '/api/auth/logout') { assert.equal(body.refreshToken, 'merchant-refresh-token'); return response(null) }
|
||||
if (method === 'POST' && requestPath === '/api/platform/v1/warehouse-admin') return response('20001')
|
||||
if (method === 'PATCH' && requestPath.includes('/warehouse-admin/20001/status?')) {
|
||||
state.warehouseStatus = 'INACTIVE'; return response(null)
|
||||
@@ -222,19 +217,21 @@ test('module is context injected, blocks missing prerequisites and excludes forb
|
||||
assert.doesNotMatch(source, /request\('DELETE'/)
|
||||
})
|
||||
|
||||
test('complete procurement truck and looseboard lifecycles pass with legal marker terminals', async () => {
|
||||
test('procurement creation review boundaries and transport cancellation paths pass with legal marker terminals', async () => {
|
||||
const context = fixture()
|
||||
await run(context)
|
||||
assert.equal(context.steps.every((step) => step.passed), true,
|
||||
context.steps.filter((step) => !step.passed).map((step) => `${step.name}: ${step.evidence}`).join('\n'))
|
||||
assert.equal(context.gaps.length, 0)
|
||||
assert.equal(context.blockers.length, 0)
|
||||
assert.equal(context.calls.filter(call => call.path === '/api/auth/login').length, 1)
|
||||
assert.equal(context.calls.filter(call => call.path === '/api/auth/logout').length, 1)
|
||||
|
||||
const procurementWrites = context.calls.filter((call) => call.path.includes('/overseas-procurements') && call.method !== 'GET')
|
||||
assert.deepEqual(procurementWrites.map((call) => call.method),
|
||||
['POST', 'POST', 'POST', 'PATCH', 'PATCH', 'PATCH', 'PATCH', 'POST', 'POST', 'POST', 'POST'])
|
||||
assert.equal(procurementWrites.filter((call) => call.method === 'PATCH').length, 4)
|
||||
assert.ok(procurementWrites.some((call) => call.method === 'PATCH' && call.body.productSpecification === null && call.body.productUrl === ''))
|
||||
['POST', 'POST', 'POST', 'PATCH', 'PATCH', 'PATCH', 'PATCH', 'PATCH', 'POST', 'POST', 'POST', 'POST'])
|
||||
assert.equal(procurementWrites.filter((call) => call.method === 'PATCH').length, 5)
|
||||
assert.ok(procurementWrites.some((call) => call.method === 'PATCH' && call.body.remark === null))
|
||||
const procurementReviews = procurementWrites.filter((call) => call.path.includes('/review?'))
|
||||
assert.equal(procurementReviews.length, 4)
|
||||
assert.ok(procurementReviews.every((call) => Number.isInteger(Number(call.body.version))
|
||||
@@ -326,7 +323,7 @@ test('candidate blocker still rejects partial procurement and retires partner wa
|
||||
test('contract locks candidates three-state idempotency histories legal terminals and trace evidence', () => {
|
||||
for (const token of ['/api/platform/v1/iam/tenants?page=1&size=200&status=ACTIVE',
|
||||
'/overseas-procurements/customer-options?limit=200', '/overseas-procurements/warehouse-options',
|
||||
'/api/merchant/v1/last-leg/overseas-procurements', '显式 null 与空串清空', '字段缺失不修改',
|
||||
'/api/merchant/v1/last-leg/overseas-procurements', '采购备注显式清空', '缺失备注不修改且版本不漂移',
|
||||
'同幂等键同载荷精确重放', '同幂等键异载荷返回冲突', '旧 version 返回 A0409',
|
||||
'状态正向推进', '状态逆向回退返回冲突', '流水覆盖', "status: 'CANCELLED'",
|
||||
'/review', 'truck-review-approve', 'looseboard-status-dispatched',
|
||||
|
||||
Reference in New Issue
Block a user