优化
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import { verifyWarehouseTransferReview } from './warehouse-transfer-review.mjs'
|
||||
import { verifyCargoDiscardFulfillment } from './cargo-discard-fulfillment.mjs'
|
||||
import { createPicker, disablePicker, resolvePickerAccess } from './phase4-overseas-pda.mjs'
|
||||
import { fundRelabelWallet } from './phase4-overseas-pda-operations.mjs'
|
||||
import { verifyClientRelabelFulfillment } from './client-relabel-fulfillment.mjs'
|
||||
|
||||
const MODULE = 'phase4-overseas-client-services'
|
||||
const SCENARIO = 'phase4-overseas-client-services-lifecycle'
|
||||
@@ -51,9 +55,9 @@ async function loginMerchant(context) {
|
||||
loginId: fixture.username, password: fixture.password,
|
||||
}, '')
|
||||
const auth = context.apiSuccess(MODULE, '客户工作台账号登录', response, trace(response), OPTIONS)
|
||||
context.assert(MODULE, '客户登录返回海外访问Token', response, nonBlank(auth?.accessToken),
|
||||
context.assert(MODULE, '客户登录返回海外访问Token', response, nonBlank(auth?.accessToken) && nonBlank(auth?.refreshToken),
|
||||
`tokenPresent=${Boolean(auth?.accessToken)}; ${trace(response)}`, OPTIONS)
|
||||
return nonBlank(auth?.accessToken) ? auth.accessToken : null
|
||||
return nonBlank(auth?.accessToken) && nonBlank(auth?.refreshToken) ? auth : null
|
||||
}
|
||||
|
||||
async function currentOrders(context, token) {
|
||||
@@ -98,13 +102,17 @@ async function verifyRelabel(context, token, order) {
|
||||
}))
|
||||
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)
|
||||
&& item?.skuCode === cargo.skuCode && item.inboundCode === order.row.inboundCode
|
||||
&& id(item.overseasWarehouseId) === id(order.row.overseasWarehouseId)
|
||||
&& item?.positions?.some?.((position) => id(position?.positionId) && Number(position?.availablePackages) > 0
|
||||
&& nonBlank(position.palletNo) && position.palletNo.toUpperCase() !== 'UNBOUND'))
|
||||
const position = source?.positions?.find?.((item) => id(item?.positionId) && Number(item?.availablePackages) > 0
|
||||
&& nonBlank(item.palletNo) && item.palletNo.toUpperCase() !== 'UNBOUND')
|
||||
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
|
||||
if (!await fundRelabelWallet(context, id(context.context.merchantQualificationFixture.tenantId))) return false
|
||||
const body = {
|
||||
customerOrderNo: order.row.orderNo,
|
||||
operationWarehouseId: order.row.overseasWarehouseId,
|
||||
@@ -120,9 +128,11 @@ async function verifyRelabel(context, token, order) {
|
||||
productNameZh: cargo.productNameZh, totalPackageCount: 1, notifiedPackageCount: 1,
|
||||
shipmentNo: cargo.shipmentNo || `${context.marker}-RELABEL-SHIP`, trackingCode: cargo.fbaTrackingNo || null,
|
||||
newSkuCode: `${cargo.skuCode}-NEW`.slice(0, 128), labelRequirement: context.marker,
|
||||
operationType: source.destinationFbaWarehouseCode ? 'CARTON_AND_SKU' : 'SKU_LABEL',
|
||||
skuItems: [{ skuCode: cargo.skuCode, newSkuCode: `${cargo.skuCode}-NEW`.slice(0, 128), quantity: 1, remark: context.marker }],
|
||||
relabelFileId: null, relabelFileName: null, relabelFileUrl: null,
|
||||
cartonMark: `CM-${String(context.marker).slice(-32)}`,
|
||||
fbaCartonMark: `FBA-${String(context.marker).slice(-32)}`,
|
||||
fbaCartonMark: source.destinationFbaWarehouseCode ? `FBA-${String(context.marker).slice(-32)}` : null,
|
||||
destinationFbaWarehouseCode: source.destinationFbaWarehouseCode,
|
||||
}],
|
||||
}
|
||||
@@ -150,9 +160,52 @@ async function verifyRelabel(context, token, order) {
|
||||
&& Array.isArray(detail.data?.history) && detail.data.history.some((item) => item?.toStatus === 'PENDING_REVIEW'),
|
||||
`items=${detail.data?.items?.length ?? 'missing'}, histories=${detail.data?.history?.length ?? 'missing'}; ${trace(detail.response)}`, OPTIONS)
|
||||
context.createdResources.phase4OverseasClientServices.relabelNo = relabelNo
|
||||
const proof = context.createdResources.phase4OverseasClientServices.relabelFulfillment = {
|
||||
marker: context.marker, relabelNo, customerId: id(context.context.merchantQualificationFixture.tenantId),
|
||||
sourceOrderNo: order.row.orderNo, warehouseId: id(order.row.overseasWarehouseId),
|
||||
sourcePositionId: id(position.positionId), sourcePalletNo: position.palletNo, status: 'PENDING_REVIEW',
|
||||
}
|
||||
const access = await resolvePickerAccess(context)
|
||||
if (!access) throw new Error('贴标后续缺少有效PDA角色')
|
||||
const picker = await createPicker(context, 94, proof.warehouseId, access)
|
||||
try {
|
||||
if (!picker?.token) throw new Error('贴标后续未取得本仓PDA会话')
|
||||
await verifyClientRelabelFulfillment(context, { proof, merchantToken: token, pdaToken: picker.token })
|
||||
context.addCoverageGap(MODULE, 'client-relabel-delivery', '客户原贴标申请的排车至交付',
|
||||
'原申请已完成PDA贴标和客户确认,后续必须使用本申请targetPalletIds接履约,不能借其他换标单结果',
|
||||
'not-implemented', { blocking: false })
|
||||
} finally {
|
||||
if (picker?.membershipId) await disablePicker(context, picker, 94)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 提交只发送一次,未知结果保留单号并停止,不能用详情回查掩盖失败。
|
||||
export async function submitClientTransfer(context, token, transferNo) {
|
||||
if (!nonBlank(transferNo)) throw new Error('仓储转运提交缺少单号')
|
||||
if (context.createdResources.phase4OverseasClientServices.transferSubmission) throw new Error('本轮仓储转运已有提交证据,禁止重复提交')
|
||||
const proof = { transferNo, marker: context.marker, status: 'SUBMITTING' }
|
||||
context.createdResources.phase4OverseasClientServices.transferSubmission = proof
|
||||
try {
|
||||
const response = await context.request('POST', `${TRANSFER}/${encodeURIComponent(transferNo)}/submit`, undefined, token, {})
|
||||
Object.assign(proof, { http: response?.http, code: codeOf(response), requestId: response?.requestId, traceId: response?.traceId })
|
||||
const data = context.apiSuccess(MODULE, '客户提交仓储转运申请', response, trace(response), OPTIONS)
|
||||
if (!success(response)) {
|
||||
proof.status = !response?.http || response.http >= 500 || !response.body ? 'UNKNOWN_RESULT' : 'REJECTED'
|
||||
throw new Error(`仓储转运提交未成功,停止后续写入: ${transferNo}; ${trace(response)}`)
|
||||
}
|
||||
if (id(data?.transferNo) !== transferNo || data?.status !== 'PENDING_REVIEW') {
|
||||
proof.status = 'UNEXPECTED_RESULT'
|
||||
throw new Error(`仓储转运提交返回单号或状态不符: ${transferNo}`)
|
||||
}
|
||||
proof.status = 'PENDING_REVIEW'
|
||||
return data
|
||||
} catch (error) {
|
||||
if (proof.status === 'SUBMITTING') proof.status = 'UNKNOWN_RESULT'
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyTransfer(context, token, orders) {
|
||||
const order = orders.find((value) => nonBlank(value.row?.inboundCode)
|
||||
&& nonBlank(value.row?.fbaWarehouseCode) && nonBlank(value.row?.overseasWarehouseId) && usableCargo(value))
|
||||
@@ -184,26 +237,7 @@ async function verifyTransfer(context, token, orders) {
|
||||
const created = await merchant(context, token, 'POST', '客户创建仓储转运申请', TRANSFER, body)
|
||||
const transferNo = id(created.data?.transferNo)
|
||||
if (transferNo && created.data?.status === 'DRAFT') {
|
||||
// The overseas gateway occasionally drops a connection while the submit
|
||||
// request is still being processed. Retry transport failures only, then
|
||||
// reconcile from the authoritative detail before asserting the state.
|
||||
let submitted = null
|
||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||
const response = await context.request('POST', `${TRANSFER}/${encodeURIComponent(transferNo)}/submit`, undefined, token, {})
|
||||
if (response?.http === 0 && attempt < 3) {
|
||||
await new Promise(resolve => setTimeout(resolve, 800 * attempt))
|
||||
continue
|
||||
}
|
||||
const data = context.apiSuccess(MODULE, '客户提交仓储转运申请', response, trace(response), OPTIONS)
|
||||
submitted = { response, data }
|
||||
break
|
||||
}
|
||||
if (submitted?.data) created.data = submitted.data
|
||||
if (created.data?.status !== 'PENDING_REVIEW') {
|
||||
const reconciled = await merchant(context, token, 'GET', '提交后回查仓储转运申请',
|
||||
`${TRANSFER}/${encodeURIComponent(transferNo)}`)
|
||||
if (reconciled.data) created.data = reconciled.data
|
||||
}
|
||||
created.data = await submitClientTransfer(context, token, transferNo)
|
||||
}
|
||||
context.assert(MODULE, '仓储转运申请进入合法待审核状态且绑定本轮来源订单', created.response,
|
||||
Boolean(transferNo) && created.data?.status === 'PENDING_REVIEW'
|
||||
@@ -275,7 +309,21 @@ export async function verifyDiscard(context, token, markerOrders) {
|
||||
&& Array.isArray(detail.data?.histories) && detail.data.histories.some((item) => item?.toStatus === 'PENDING_REVIEW'),
|
||||
`items=${detail.data?.items?.length ?? 'missing'}, histories=${detail.data?.histories?.length ?? 'missing'}; ${trace(detail.response)}`, OPTIONS)
|
||||
context.createdResources.phase4OverseasClientServices.discardNo = discardNo
|
||||
try { await verifyDiscardApproval(context, token, body, discardNo, proof) }
|
||||
try {
|
||||
await verifyDiscardApproval(context, token, body, discardNo, proof)
|
||||
const access = await resolvePickerAccess(context)
|
||||
if (!access) throw new Error('废弃执行缺少有效PDA角色')
|
||||
const picker = await createPicker(context, 93, proof.warehouseId, access)
|
||||
try {
|
||||
if (!picker?.token) throw new Error('废弃执行未取得本仓PDA会话')
|
||||
await verifyCargoDiscardFulfillment(context, { proof, merchantToken: token, pdaToken: picker.token,
|
||||
snapshot: () => discardSnapshot(context, token, proof, proof.approvedNo) })
|
||||
context.addCoverageGap(MODULE, 'discard-carton-storage-evidence', '废弃底层箱、SKU和箱级流水独立取证',
|
||||
'已接入PDA执行、客户完成、公共库存总账和终态重放;未执行底层独立取证', 'prerequisite', { blocking: false })
|
||||
} finally {
|
||||
if (picker?.membershipId) await disablePicker(context, picker, 93)
|
||||
}
|
||||
}
|
||||
catch (error) { proof.status = 'FAILED'; proof.failure = error.message; throw error }
|
||||
return true
|
||||
}
|
||||
@@ -363,13 +411,22 @@ export async function verifyDiscardApproval(context, token, body, discardNo, pro
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
const auth = await loginMerchant(context)
|
||||
if (!auth) return
|
||||
try {
|
||||
return await runWithSession(context, auth.accessToken)
|
||||
} finally {
|
||||
const logout = await context.request('POST', '/api/auth/logout', { refreshToken: auth.refreshToken }, auth.accessToken)
|
||||
context.apiSuccess(MODULE, '退出本轮客户服务会话', logout, trace(logout), OPTIONS)
|
||||
}
|
||||
}
|
||||
|
||||
async function runWithSession(context, token) {
|
||||
context.createdResources.phase4OverseasClientServices = {
|
||||
marker: context.marker, relabelNo: null, transferNo: null, discardNo: null,
|
||||
finalStatuses: { relabel: null, transfer: null, discard: null },
|
||||
cleanup: 'Relabel remains pending; transfer review fixtures retained; discard rejection and approval lock checked; actual stock deduction requires separate PDA confirmation; no outbound, dispatch or delivery action is invoked',
|
||||
cleanup: 'Relabel ready for dispatch; transfer review fixtures retained; discard PDA execution and customer confirmation retained; no dispatch or delivery action is invoked',
|
||||
}
|
||||
const token = await loginMerchant(context)
|
||||
if (!token) return
|
||||
|
||||
for (const [name, path] of [['贴标', RELABEL], ['仓储转运', TRANSFER], ['货物废弃', DISCARD]]) {
|
||||
const denied = await context.request('GET', query(path, { page: 1, size: 1 }))
|
||||
@@ -390,8 +447,8 @@ export async function run(context) {
|
||||
const transferDone = await verifyTransfer(context, token, orders)
|
||||
const discardDone = await verifyDiscard(context, token, orders)
|
||||
context.createdResources.phase4OverseasClientServices.finalStatuses = {
|
||||
relabel: relabelDone ? 'PENDING_REVIEW' : null,
|
||||
relabel: relabelDone ? 'READY_FOR_OUTBOUND' : null,
|
||||
transfer: transferDone ? 'APPROVED' : null,
|
||||
discard: discardDone ? 'APPROVED' : null,
|
||||
discard: discardDone ? 'COMPLETED' : null,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user