136 lines
12 KiB
JavaScript
136 lines
12 KiB
JavaScript
const MODULE = 'procurement-sample-fulfillment'
|
|
// Peak cash requirement: 1.00 purchase settled plus 2.00 freight frozen.
|
|
export const PROCUREMENT_REQUIRED_AVAILABLE_BALANCE = '3.00'
|
|
const PLATFORM = '/api/platform/v1/last-leg/overseas-procurements'
|
|
const MERCHANT = '/api/merchant/v1/last-leg/overseas-procurements'
|
|
const clone = value => value == null ? value : structuredClone(value)
|
|
const cents = value => {
|
|
const match = /^(\d+)(?:\.(\d{1,2}))?$/.exec(String(value))
|
|
if (!match) throw new Error('钱包金额缺失或精度异常')
|
|
return BigInt(match[1]) * 100n + BigInt((match[2] || '').padEnd(2, '0'))
|
|
}
|
|
|
|
/** Internal scenario; the coordinator supplies the configured customer, existing balance, session and upload. */
|
|
export async function verifyProcurementSample(context, inputs) {
|
|
if (!context.marker || typeof context.checkpoint !== 'function' || typeof context.request !== 'function'
|
|
|| inputs?.marker !== context.marker || !/^[1-9]\d*$/.test(String(inputs.customerId))
|
|
|| !/^[1-9]\d*$/.test(String(inputs.warehouseId)) || !inputs.customerToken
|
|
|| typeof inputs.upload !== 'function' || typeof inputs.readWallet !== 'function')
|
|
throw new Error('缺少本轮独立客户、仓库、会话、上传或持久化验收前置')
|
|
if (context.createdResources.procurementSample) throw new Error('已有采购验收证据,不自动重跑或覆盖')
|
|
const proof = context.createdResources.procurementSample = { marker: context.marker, status: 'RUNNING', requests: [], checks: [], orderNo: null }
|
|
const check = async (label, accepted) => {
|
|
proof.checks.push({ label, passed: Boolean(accepted) }); await context.checkpoint()
|
|
if (!accepted) throw new Error(`${MODULE}: ${label}`)
|
|
}
|
|
const customerId = String(inputs.customerId)
|
|
let detail, sequence = 0
|
|
const at = () => {
|
|
const value = detail?.customerView?.order?.updatedAt
|
|
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}T/.test(value)) throw new Error('服务端业务时间缺失')
|
|
return value
|
|
}
|
|
const date = () => at().slice(0, 10)
|
|
const key = label => `${context.marker}-${++sequence}-${label}`.slice(0, 128)
|
|
const path = (operation = '') => `${PLATFORM}/${encodeURIComponent(proof.orderNo)}${operation ? '/' + operation : ''}?customerId=${encodeURIComponent(customerId)}`
|
|
async function request(method, url, body, label, token = context.token, expectedCode = '200') {
|
|
const entry = { method, path: url, body: clone(body), label, status: 'SENDING' }
|
|
proof.requests.push(entry); await context.checkpoint()
|
|
let response
|
|
try { response = await context.request(method, url, body, token) }
|
|
catch (error) { entry.status = 'UNKNOWN_RESULT'; await context.checkpoint(); throw error }
|
|
Object.assign(entry, { status: response?.http === 0 || response?.http >= 500 || !response?.body ? 'UNKNOWN_RESULT' : 'RESPONDED', http: response?.http, code: String(response?.body?.code), requestId: response?.requestId, traceId: response?.traceId })
|
|
await context.checkpoint()
|
|
await check(label, response?.http === 200 && String(response?.body?.code) === expectedCode)
|
|
return response.body.data
|
|
}
|
|
async function read() {
|
|
detail = await request('GET', path(), undefined, '回查采购聚合详情')
|
|
await check('采购聚合仍属于原客户原单且版本有效', detail?.customerView?.order?.orderNo === proof.orderNo
|
|
&& detail.customerView.order.customerId === customerId
|
|
&& Number.isSafeInteger(detail.customerView.order.version) && detail.customerView.order.version >= 0)
|
|
return detail
|
|
}
|
|
async function command(operation, values = {}, replay = false) {
|
|
const body = { version: detail.customerView.order.version, idempotencyKey: key(operation.replaceAll('/', '-')), reason: context.marker, ...values }
|
|
await request('POST', path(operation), body, operation)
|
|
if (replay) await request('POST', path(operation), body, `${operation} 原请求重放`)
|
|
await read(); return body
|
|
}
|
|
async function waitFor(label, predicate) {
|
|
for (let attempt = 0; attempt < 31; attempt++) {
|
|
await read()
|
|
if (predicate(detail)) return
|
|
if (attempt < 30) await new Promise(resolve => setTimeout(resolve, 2000))
|
|
}
|
|
await check(`${label}在限定时间内完成`, false)
|
|
}
|
|
const fee = purpose => detail.customerView.fees.find(row => row.active && row.purpose === purpose)
|
|
async function wallet(label, spent, frozen) {
|
|
const current = await inputs.readWallet()
|
|
proof.walletChecks ??= []; proof.walletChecks.push({ label, account: clone(current) })
|
|
await check(label, cents(current.actualBalance) === cents(proof.walletBefore.actualBalance) - spent
|
|
&& cents(current.frozenBalance) === cents(proof.walletBefore.frozenBalance) + frozen
|
|
&& cents(current.creditUsed) === cents(proof.walletBefore.creditUsed))
|
|
}
|
|
try {
|
|
proof.walletBefore = clone(await inputs.readWallet())
|
|
await check('客户现金余额足以完成独立样本', cents(proof.walletBefore.availableBalance) >= cents(PROCUREMENT_REQUIRED_AVAILABLE_BALANCE))
|
|
const recipient = { name: '采购样品验收', phone: '13800000002', address: '广东省深圳市南山区科技园测试路1号' }
|
|
const create = { customerName: inputs.customerName, purchaseCategory: 'SAMPLE', productName: `${context.marker} sample`, productSpecification: 'one-spec', purchaseQuantity: 2, budgetUnitPrice: '1.00', currency: 'CNY', receivingAddress: recipient.address, contactName: recipient.name, contactPhone: recipient.phone, idempotencyKey: key('create') }
|
|
const created = await request('POST', MERCHANT, create, '客户创建样品订单', inputs.customerToken)
|
|
proof.orderNo = created?.order?.orderNo; await context.checkpoint()
|
|
await check('客户创建响应包含本轮订单', typeof proof.orderNo === 'string' && created.order.customerId === customerId)
|
|
const repeated = await request('POST', MERCHANT, create, '客户同键重放', inputs.customerToken)
|
|
await check('重放没有新建采购单', repeated?.order?.orderNo === proof.orderNo && repeated.order.customerId === customerId)
|
|
await read()
|
|
await request('POST', path('review'), { version: detail.customerView.order.version, idempotencyKey: key('review'), decision: 'APPROVE', purchaseWarehouseId: String(inputs.warehouseId), remark: context.marker }, '审核通过并确定收货仓')
|
|
await read()
|
|
const purchaseFile = await inputs.upload(proof.orderNo, 'PURCHASE')
|
|
const voucher = await inputs.upload(proof.orderNo, 'VOUCHER')
|
|
const labelFile = await inputs.upload(proof.orderNo, 'LABEL')
|
|
await check('凭证上传返回正式文件引用', [purchaseFile, voucher, labelFile].every(file => /^[1-9]\d*$/.test(String(file.fileId))))
|
|
await command('purchase', { productName: create.productName, specification: create.productSpecification, quantity: 2, amount: '1.00', currency: 'CNY', purchasedAt: at(), estimatedArrivalAt: new Date(Date.now() + 86400000).toISOString().slice(0,19), voucherFileId: String(purchaseFile.fileId) }, true)
|
|
await waitFor('样品货款实扣及来源关联', () => fee('SAMPLE_PAYMENT')?.walletStatus === 'SETTLED' && fee('SAMPLE_PAYMENT')?.sourceLinked)
|
|
await wallet('购买完成扣货款1元', 100n, 0n)
|
|
for (let count = 0; count < 2; count++) await command('overseas-receipts', { leg: 'OVERSEAS', warehouseId: String(inputs.warehouseId), goodQuantity: 1, damagedQuantity: 0, extraQuantity: 0, receivedAt: at() }, true)
|
|
await check('分次收货重放不增加数量', detail.physical.receipts.filter(row => row.leg === 'OVERSEAS' && row.active).reduce((sum,row) => sum + Number(row.goodQuantity),0) === 2)
|
|
await command('overseas-receipts/complete', { leg: 'OVERSEAS' })
|
|
const receipts = detail.physical.receipts.filter(row => row.leg === 'OVERSEAS' && row.active)
|
|
await command('packing', { cartons: receipts.map((row,index) => ({ cartonNo: `${context.marker}-C${index+1}`, lengthCm: '10.00', widthCm: '10.00', heightCm: '10.00', grossWeightKg: '1.000', sources: [{ receiptId: String(row.id), quantity: 1 }] })) })
|
|
await command('packing/confirm', { packingVersion: detail.physical.packingVersion })
|
|
const cartonIds = detail.physical.cartons.map(row => String(row.id))
|
|
async function labels(leg) {
|
|
await command('labels', { leg, packingVersion: detail.physical.packingVersion, receiver: recipient, labels: cartonIds.map((cartonId,index) => ({ cartonId, fileId: String(labelFile.fileId), page: 1, carrier: 'TEST-EXPRESS', trackingNo: `${context.marker}-${leg}-${index+1}` })) })
|
|
return detail.transport[leg === 'OVERSEAS' ? 'overseasLabels' : 'domesticLabels'].version
|
|
}
|
|
const internationalLabels = await labels('OVERSEAS')
|
|
await command('international-estimate', { labelVersion: internationalLabels, amount: '2.00', businessDate: date() }, true)
|
|
await waitFor('预估冻结', () => fee('INTERNATIONAL_ESTIMATE')?.walletStatus === 'FROZEN')
|
|
await wallet('预估2元只冻结不消费', 100n, 200n)
|
|
await command('labels/apply', { labelVersion: internationalLabels, cartonIds, appliedAt: at() })
|
|
await command('overseas-shipment', { leg: 'OVERSEAS', labelVersion: internationalLabels, cartonIds, shippedAt: at() }, true)
|
|
await check('国际段只有一次实际交寄', detail.transport.shipments.filter(row => row.leg === 'OVERSEAS').length === 1)
|
|
for (const sourceCartonId of cartonIds) await command('domestic-receipts', { leg: 'DOMESTIC', sourceCartonId, goodQuantity: 1, damagedQuantity: 0, extraQuantity: 0, receivedAt: at() }, true)
|
|
await command('domestic-receipts/complete', { leg: 'DOMESTIC' })
|
|
const actual = amount => ({ value: { determination: amount === '0.00' ? 'ZERO' : 'POSITIVE', amount, currency: 'CNY', businessDate: date(), voucherFileId: String(voucher.fileId), reason: context.marker } })
|
|
await command('charges/confirm', { internationalFreight: actual('1.50'), duty: actual('0.00'), domesticFreight: actual('0.00'), otherDetermination: 'NOT_APPLICABLE', otherFees: [] }, true)
|
|
await waitFor('实际费用实扣及来源关联', () => fee('INTERNATIONAL_FREIGHT')?.walletStatus === 'SETTLED' && fee('INTERNATIONAL_FREIGHT')?.sourceLinked)
|
|
await wallet('实际国际运费扣1.5元并释放剩余冻结', 250n, 0n)
|
|
const domesticLabels = await labels('DOMESTIC')
|
|
await command('customer-shipment', { leg: 'DOMESTIC', labelVersion: domesticLabels, cartonIds, shippedAt: at() }, true)
|
|
await check('寄客户正常终态及单次交寄', detail.customerView.order.status === 'SHIPPED_TO_CUSTOMER' && detail.transport.shipments.filter(row => row.leg === 'DOMESTIC').length === 1)
|
|
const customer = await request('GET', `${MERCHANT}/${encodeURIComponent(proof.orderNo)}`, undefined, '客户最终详情', inputs.customerToken)
|
|
await check('客户可见终态且没有平台内部数据', customer?.order?.orderNo === proof.orderNo
|
|
&& customer.order.customerId === customerId && customer.order.status === 'SHIPPED_TO_CUSTOMER'
|
|
&& !['files','purchaseFacts','controls','internalRemark','operations'].some(field => Object.hasOwn(customer,field)))
|
|
await wallet('重复交寄不再次扣款', 250n, 0n)
|
|
proof.walletBusinessNos = [...new Set(detail.fees.filter(row => row.walletStatus === 'SETTLED').map(row => row.walletBusinessNo))]
|
|
await check('每笔成功消费保留权威钱包业务引用', proof.walletBusinessNos.length === 2 && proof.walletBusinessNos.every(value => typeof value === 'string' && value.length > 0))
|
|
proof.settledAmountCny = '2.50'
|
|
proof.status = 'PASSED'; await context.checkpoint(); return proof
|
|
} catch (error) {
|
|
proof.status = 'FAILED'; proof.failure = error.message; await context.checkpoint(); throw error
|
|
}
|
|
}
|