test(acceptance): reuse fixed customers and align native fulfillment flows
This commit is contained in:
@@ -6,6 +6,7 @@ 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'
|
||||
import { readCurrentBoxedInventory } from './current-boxed-inventory.mjs'
|
||||
|
||||
const MODULE = 'phase4-overseas-client-services'
|
||||
const SCENARIO = 'phase4-overseas-client-services-lifecycle'
|
||||
@@ -63,14 +64,28 @@ async function loginMerchant(context) {
|
||||
return nonBlank(auth?.accessToken) && nonBlank(auth?.refreshToken) ? auth : null
|
||||
}
|
||||
|
||||
async function currentOrders(context, token) {
|
||||
export async function currentOrders(context, token) {
|
||||
const pda = context.createdResources.phase4OverseasPda
|
||||
const containerNos = [...new Set([...(pda.completedContainers || []), pda.containerNo].map(id).filter(Boolean))]
|
||||
const result = []
|
||||
for (const containerNo of containerNos) {
|
||||
const page = await merchant(context, token, 'GET', `查询本轮完成柜${containerNo}的客户尾程订单`,
|
||||
query(ORDERS, { page: 1, size: 200, containerNo }))
|
||||
for (const row of records(page.data)) {
|
||||
const all = [], seen = new Set(); let total
|
||||
for (let pageNo = 1; pageNo <= 100; pageNo++) {
|
||||
const page = await merchant(context, token, 'GET', `查询本轮完成柜${containerNo}的客户尾程订单`,
|
||||
query(ORDERS, { page: pageNo, size: 200, containerNo }))
|
||||
const count = Number(page.data?.total)
|
||||
if (page.data?.total == null || !Number.isSafeInteger(count) || count < 0 || !Array.isArray(page.data?.records)
|
||||
|| (total !== undefined && count !== total) || (!page.data.records.length && all.length < count))
|
||||
throw Error('客户尾程订单分页不完整或总数变化')
|
||||
total ??= count
|
||||
for (const row of page.data.records) {
|
||||
if (!id(row.id) || seen.has(id(row.id))) throw Error('客户尾程订单分页身份重复或缺失')
|
||||
seen.add(id(row.id)); all.push(row)
|
||||
}
|
||||
if (all.length >= total) break
|
||||
}
|
||||
if (all.length !== total) throw Error('客户尾程订单分页未完整读取')
|
||||
for (const row of all) {
|
||||
if (id(row?.containerNo) !== containerNo || !id(row?.id)) continue
|
||||
const detail = await merchant(context, token, 'GET', `读取本轮尾程订单${id(row.id)}详情`,
|
||||
`${ORDERS}/${encodeURIComponent(id(row.id))}`)
|
||||
@@ -91,27 +106,21 @@ function usableCargo(order) {
|
||||
&& nonBlank(item?.productNameZh) && Number(item?.packageCount) > 0)
|
||||
}
|
||||
|
||||
async function verifyRelabel(context, token, order) {
|
||||
async function verifyRelabel(context, token, orders) {
|
||||
const boxed = await readCurrentBoxedInventory(context, token,
|
||||
context.createdResources.phase4OverseasPda.overseasWarehouseId, MODULE)
|
||||
const source = boxed.find(row => orders.some(order => order.row.inboundCode === row.inboundCode && usableCargo(order)?.skuCode === row.skuCode))
|
||||
const order = source && orders.find(order => order.row.inboundCode === source.inboundCode && usableCargo(order)?.skuCode === source.skuCode)
|
||||
if (!order) throw Error('客户换标缺少本轮未占用真实箱库存')
|
||||
const carton = source.cartons[0]
|
||||
const cargo = usableCargo(order)
|
||||
if (!cargo || !nonBlank(order.row?.overseasWarehouseId)) {
|
||||
blocker(context, '本轮贴标申请候选',
|
||||
`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.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,
|
||||
const position = source.positions.find(item => id(item.positionId) === carton.positionId)
|
||||
context.assert(MODULE, '贴标申请使用当前客户真实库存余额、位置和来源入库单', null,
|
||||
Boolean(source && position),
|
||||
`balanceId=${id(source?.inventoryBalanceId) || 'missing'}, positionId=${id(position?.positionId) || 'missing'}, inboundOrderId=${id(source?.inboundOrderId) || 'missing'}`, OPTIONS)
|
||||
if (!source || !position) return false
|
||||
@@ -132,9 +141,9 @@ async function verifyRelabel(context, token, order) {
|
||||
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 }],
|
||||
skuItems: carton.items.map(item => ({ skuCode: item.skuCode, newSkuCode: `${item.skuCode}-NEW`.slice(0, 128), quantity: Number(item.quantity), remark: context.marker })),
|
||||
relabelFileId: null, relabelFileName: null, relabelFileUrl: null,
|
||||
cartonMark: `CM-${String(context.marker).slice(-32)}`,
|
||||
cartonMark: carton.cartonMark,
|
||||
fbaCartonMark: source.destinationFbaWarehouseCode ? `FBA-${String(context.marker).slice(-32)}` : null,
|
||||
destinationFbaWarehouseCode: source.destinationFbaWarehouseCode,
|
||||
}],
|
||||
@@ -210,8 +219,12 @@ export async function submitClientTransfer(context, token, transferNo) {
|
||||
}
|
||||
|
||||
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))
|
||||
const boxed = await readCurrentBoxedInventory(context, token,
|
||||
context.createdResources.phase4OverseasPda.overseasWarehouseId, MODULE)
|
||||
const source = boxed.find(row => row.destinationFbaWarehouseCode && orders.some(order => order.row.inboundCode === row.inboundCode
|
||||
&& usableCargo(order)?.skuCode === row.skuCode))
|
||||
const order = source && orders.find(value => value.row.inboundCode === source.inboundCode && usableCargo(value)?.skuCode === source.skuCode)
|
||||
const carton = source?.cartons[0]
|
||||
const cargo = order ? usableCargo(order) : null
|
||||
if (!order || !cargo) {
|
||||
blocker(context, '本轮仓储转运申请候选',
|
||||
@@ -226,14 +239,14 @@ async function verifyTransfer(context, token, orders) {
|
||||
deliveryAddress: `${order.row.fbaWarehouseCode} ${context.marker}`.slice(0, 1000),
|
||||
packageCount: packages, volumeCbm: volume, deliveryMethod: 'TRUCK', estimatedArrivalAt: future(10),
|
||||
fbaShipmentNo: cargo.shipmentNo || `${context.marker}-TRANSFER-SHIP`, trackingNo: cargo.fbaTrackingNo || null,
|
||||
fbaBoxNos: cargo.shippingMark || null, remark: context.marker, needRelabel: true,
|
||||
fbaBoxNos: carton.cartonMark, remark: context.marker, needRelabel: true,
|
||||
labelDeadline: future(7), relabelContactName: '客户贴标验收联系人', relabelContactPhone: '13800000006',
|
||||
relabelChargeAmount: 0.01, relabelDeliveryMethod: 'TRUCK',
|
||||
idempotencyKey: `${context.marker}-CLIENT-TRANSFER`.slice(0, 255),
|
||||
items: [{ skuCode: cargo.skuCode, productNameZh: cargo.productNameZh,
|
||||
packageCount: packages, shippingMark: cargo.shippingMark || null }],
|
||||
relabelItems: [{ operationType: 'CARTON_AND_SKU', customerCartonMarks: [cargo.shippingMark || `CM-${context.marker}`],
|
||||
originalSku: cargo.skuCode, newSku: `${cargo.skuCode}-NEW`.slice(0, 128), quantity: 1,
|
||||
packageCount: packages, shippingMark: carton.cartonMark }],
|
||||
relabelItems: [{ operationType: 'CARTON_AND_SKU', customerCartonMarks: [carton.cartonMark],
|
||||
originalSku: cargo.skuCode, newSku: `${cargo.skuCode}-NEW`.slice(0, 128), quantity: Number(carton.items[0].quantity),
|
||||
fbaCartonMark: `FBA-${context.marker}`.slice(0, 255), destinationWarehouseCode: order.row.fbaWarehouseCode,
|
||||
remark: context.marker }],
|
||||
}
|
||||
@@ -267,6 +280,7 @@ async function verifyTransfer(context, token, orders) {
|
||||
const transferProof = context.createdResources.phase4OverseasClientServices.transferFulfillment = {
|
||||
marker: context.marker, customerId: id(order.row.customerId), warehouseId: id(order.row.overseasWarehouseId),
|
||||
transferNo, inboundCode: order.row.inboundCode, skuCode: cargo.skuCode, status: 'APPROVED',
|
||||
requiredSourcePositionId: carton.positionId, sourceCartonMark: carton.cartonMark,
|
||||
}
|
||||
await allocateWarehouseTransfer(context, { proof: transferProof, merchantToken: token })
|
||||
const relabelProof = transferProof.relabelFulfillment = {
|
||||
@@ -294,15 +308,18 @@ export async function verifyDiscard(context, token, markerOrders) {
|
||||
const candidates = context.apiSuccess(MODULE, '客户查询货物废弃库存候选', response, trace(response), OPTIONS)
|
||||
const markerOrderIds = new Set(markerOrders.map((value) => id(value.row?.id)))
|
||||
const candidate = Array.isArray(candidates) ? candidates.find((value) => markerOrderIds.has(id(value?.orderId))
|
||||
&& Array.isArray(value?.skus) && value.skus.some((sku) => nonBlank(sku?.skuCode) && Number(sku?.availablePackages) > 0)) : null
|
||||
&& value.cartonSelectionSupported === true && value.cartons?.some(box => id(box.cartonId)
|
||||
&& box.skus?.some(sku => nonBlank(sku.skuCode) && Number(sku.availablePackages) > 0))) : null
|
||||
if (!candidate) {
|
||||
blocker(context, '本轮货物废弃库存候选',
|
||||
`candidateOrders=${Array.isArray(candidates) ? candidates.map((value) => id(value?.orderId)).join(',') : 'missing'}, markerOrders=${[...markerOrderIds].join(',') || 'missing'}; 禁止借历史一件代发库存`, response)
|
||||
return false
|
||||
}
|
||||
const sku = candidate.skus.find((value) => nonBlank(value?.skuCode) && Number(value?.availablePackages) > 0)
|
||||
const carton = candidate.cartons.find(box => id(box.cartonId)
|
||||
&& box.skus?.some(sku => nonBlank(sku.skuCode) && Number(sku.availablePackages) > 0))
|
||||
const sku = carton.skus.find(value => nonBlank(value.skuCode) && Number(value.availablePackages) > 0)
|
||||
const body = {
|
||||
items: [{ orderId: id(candidate.orderId), skuCode: sku.skuCode, discardPackages: 1 }],
|
||||
items: [{ orderId: id(candidate.orderId), skuCode: sku.skuCode, discardPackages: 1, cartonIds: [id(carton.cartonId)] }],
|
||||
idempotencyKey: `${context.marker}-CLIENT-DISCARD`.slice(0, 255), remark: context.marker,
|
||||
}
|
||||
const proof = context.createdResources.phase4OverseasClientServices.discardApproval = {
|
||||
@@ -369,7 +386,7 @@ async function discardCommand(context, token, method, name, path, body) {
|
||||
if (!success(result.response)) throw new Error(`${name}: ${trace(result.response)}`)
|
||||
return result.data
|
||||
}
|
||||
async function discardSnapshot(context, token, proof, discardNo) {
|
||||
export async function discardSnapshot(context, token, proof, discardNo) {
|
||||
const all = async (kind) => {
|
||||
const rows = []
|
||||
let total
|
||||
@@ -388,6 +405,19 @@ async function discardSnapshot(context, token, proof, discardNo) {
|
||||
throw new Error('废弃库存分页超过上限')
|
||||
}
|
||||
const balances = await all('balances'), ledger = await all('ledger')
|
||||
// 列表按客户、仓、目的仓和SKU聚合,不携带入仓码;用代表余额ID回查真实来源并核对汇总数,不能给聚合行补造来源。
|
||||
for (let index = 0; index < balances.length; index++) {
|
||||
const aggregate = balances[index]
|
||||
if (aggregate.skuCode !== proof.skuCode) continue
|
||||
const raw = await discardCommand(context, context.token, 'GET', '废弃核对实际来源余额详情',
|
||||
query(`/api/platform/v1/last-leg/inventory/balances/${encodeURIComponent(id(aggregate.id))}`, { customerId: proof.customerId }))
|
||||
discardRequire(context, '废弃来源余额身份与聚合数量一致', id(raw?.id) === id(aggregate.id)
|
||||
&& id(raw.customerId) === proof.customerId && raw.overseasWarehouseId === proof.warehouseId
|
||||
&& raw.inboundCode === proof.inboundCode && raw.skuCode === proof.skuCode
|
||||
&& ['totalPackages', 'availablePackages', 'holdPackages', 'allocatedPackages', 'pickedPackages', 'inTransitPackages', 'receivingPackages', 'procurementPackages']
|
||||
.every(field => /^\d+$/.test(String(raw[field])) && Number(raw[field]) === Number(aggregate[field])))
|
||||
balances[index] = raw
|
||||
}
|
||||
const candidates = await discardCommand(context, token, 'GET', '废弃回查本轮客户真实候选', `${DISCARD}/inventory-candidates`)
|
||||
const source = candidates.find(row => id(row.orderId) === proof.sourceOrderId)
|
||||
const detail = discardNo ? await discardCommand(context, token, 'GET', '废弃回查申请真实状态', `${DISCARD}/${encodeURIComponent(discardNo)}`) : null
|
||||
@@ -469,7 +499,7 @@ async function runWithSession(context, token) {
|
||||
blocker(context, '本轮贴标订单明细', `orders=${orders.length}; 缺少真实SKU/品名/箱数或实际海外仓`)
|
||||
return
|
||||
}
|
||||
const relabelDone = await verifyRelabel(context, token, relabelOrder)
|
||||
const relabelDone = await verifyRelabel(context, token, orders)
|
||||
const transferDone = await verifyTransfer(context, token, orders)
|
||||
const discardDone = await verifyDiscard(context, token, orders)
|
||||
context.createdResources.phase4OverseasClientServices.finalStatuses = {
|
||||
|
||||
Reference in New Issue
Block a user