192 lines
11 KiB
JavaScript
192 lines
11 KiB
JavaScript
import { createPicker, disablePicker, resolvePickerAccess } from './phase4-overseas-pda.mjs'
|
|
|
|
const MODULE = 'phase4-overseas-return-followups'
|
|
const SCENARIO = 'phase4-overseas-return-followups'
|
|
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
|
|
const RETURN_ORDERS = '/api/merchant/v1/last-leg/return-orders'
|
|
const PDA_RECEIPTS = '/api/pda/v1/last-leg/return-receipts'
|
|
const RETURN_INVENTORY = '/api/merchant/v1/last-leg/return-inventory'
|
|
|
|
function id(value) { return value == null ? '' : String(value) }
|
|
function nonBlank(value) { return typeof value === 'string' && value.trim() !== '' }
|
|
function code(response) { return String(response?.body?.code ?? '') }
|
|
function success(response) { return response?.http === 200 && code(response) === '200' }
|
|
function conflict(response) {
|
|
return (response?.http === 409 || (response?.http === 200 && ['409', 'A0409'].includes(code(response))))
|
|
&& nonBlank(response?.requestId) && nonBlank(response?.traceId)
|
|
}
|
|
function notFound(response) {
|
|
return (response?.http === 404 || (response?.http === 200 && ['404', 'A0404'].includes(code(response))))
|
|
&& nonBlank(response?.requestId) && nonBlank(response?.traceId)
|
|
}
|
|
function records(value) { return Array.isArray(value?.records) ? value.records : Array.isArray(value) ? value : [] }
|
|
function trace(response) {
|
|
return `HTTP=${response?.http ?? 'missing'}, code=${code(response) || 'missing'}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`
|
|
}
|
|
function query(path, values) {
|
|
const params = new URLSearchParams()
|
|
for (const [name, value] of Object.entries(values || {})) {
|
|
if (value !== undefined && value !== null && String(value) !== '') params.set(name, String(value))
|
|
}
|
|
return params.size ? `${path}?${params}` : path
|
|
}
|
|
function key(context, action) {
|
|
return `${context.marker}-RETURN-${action}`.replace(/[^A-Za-z0-9:_-]/g, '').slice(0, 255)
|
|
}
|
|
function markerSuffix(context) {
|
|
return String(context.marker).replace(/[^A-Za-z0-9]/g, '').slice(-18).toUpperCase()
|
|
}
|
|
|
|
function add(context, name, response, passed, evidence = '') {
|
|
context.addStep(MODULE, name, response, passed, `${evidence}${evidence ? '; ' : ''}${trace(response)}`, OPTIONS)
|
|
}
|
|
|
|
async function overseas(context, token, method, name, path, body = undefined) {
|
|
const response = await context.requestOverseas(method, path, body, {}, token)
|
|
add(context, name, response, success(response))
|
|
return { response, data: success(response) ? response.body?.data : null }
|
|
}
|
|
|
|
async function resolveWarehouse(context) {
|
|
const existing = id(context.createdResources?.phase4OverseasPda?.overseasWarehouseId)
|
|
const response = await context.request('GET',
|
|
'/api/platform/v1/warehouse-admin?tenantId=0&page=1&size=200&capabilityCode=RECEIVING&warehouseType=OVERSEAS&status=ACTIVE')
|
|
const rows = records(response?.body?.data)
|
|
const fixedIds = new Set(['193000000000200003', '193000000000200004'])
|
|
const warehouse = rows.find((item) => id(item?.id) === existing)
|
|
|| rows.find((item) => fixedIds.has(id(item?.id)))
|
|
|| rows[0]
|
|
add(context, '读取可执行退货签收的海外仓', response,
|
|
success(response) && nonBlank(id(warehouse?.id)), `warehouseId=${id(warehouse?.id) || 'missing'}`)
|
|
return success(response) && warehouse ? { id: id(warehouse.id), name: warehouse.warehouseName || warehouse.name } : null
|
|
}
|
|
|
|
async function loginMerchant(context) {
|
|
const fixture = context.context?.merchantQualificationFixture
|
|
if (fixture?.marker !== context.marker || !fixture?.username || !fixture?.password) {
|
|
add(context, '取得本轮客户账号', null, false, `marker=${fixture?.marker || 'missing'}`)
|
|
return null
|
|
}
|
|
const response = await context.request('POST', '/api/auth/login', {
|
|
loginId: fixture.username, password: fixture.password,
|
|
}, '')
|
|
const token = success(response) ? response.body?.data?.overseasAccessToken : null
|
|
add(context, '客户登录并取得海外访问令牌', response, nonBlank(token), `tokenPresent=${nonBlank(token)}`)
|
|
return nonBlank(token) ? token : null
|
|
}
|
|
|
|
function itemMap(items) {
|
|
return new Map((Array.isArray(items) ? items : []).map((item) => [item.skuCode, item]))
|
|
}
|
|
|
|
export async function run(context) {
|
|
const source = context.createdResources.phase4OverseasReturnReceipt
|
|
if (source?.marker !== context.marker || !source?.returnNo) {
|
|
add(context, '后续关联要求本轮退货签收合箱前置', null, false, '缺少本轮退货前置')
|
|
return
|
|
}
|
|
const merchantToken = await loginMerchant(context)
|
|
const warehouse = await resolveWarehouse(context)
|
|
const access = await resolvePickerAccess(context)
|
|
if (!merchantToken || !warehouse || !access) return
|
|
const picker = await createPicker(context, 92, warehouse.id, access)
|
|
if (!picker?.token) {
|
|
add(context, '创建后续关联PDA操作员', null, false, '未取得绑定仓PDA令牌')
|
|
if (picker?.membershipId) await disablePicker(context, picker, 92)
|
|
return
|
|
}
|
|
const returnNo = source.returnNo
|
|
const customerId = id(context.context?.merchantQualificationFixture?.tenantId)
|
|
const suffix = markerSuffix(context)
|
|
try {
|
|
const relabelCandidates = await overseas(context, merchantToken, 'GET', '客户查询本轮退货换标候选',
|
|
`${RETURN_ORDERS}/${encodeURIComponent(returnNo)}/relabel-candidates`)
|
|
const relabelCandidate = (Array.isArray(relabelCandidates.data) ? relabelCandidates.data : [])
|
|
.find(row => nonBlank(row?.cartonMark) && row?.status === 'AVAILABLE')
|
|
context.assert(MODULE, '退货换标候选来自本轮已签收可用箱', relabelCandidates.response,
|
|
Boolean(relabelCandidate) && relabelCandidate.sourceReturnNo === returnNo,
|
|
`cartonMark=${relabelCandidate?.cartonMark || 'missing'}`, OPTIONS)
|
|
if (relabelCandidate) {
|
|
const relabelBody = {
|
|
cartonMarks: [relabelCandidate.cartonMark], idempotencyKey: key(context, 'RELABEL'),
|
|
contactName: '退货换标验收', contactPhone: '13800000008', deliveryMethod: 'TRUCK', remark: context.marker,
|
|
}
|
|
const relabel = await overseas(context, merchantToken, 'POST', '客户从退货箱创建换标单',
|
|
`${RETURN_ORDERS}/${encodeURIComponent(returnNo)}/relabel`, relabelBody)
|
|
const relabelNo = id(relabel.data?.relabelNo)
|
|
context.assert(MODULE, '退货换标单绑定原退货单与选中箱', relabel.response,
|
|
nonBlank(relabelNo) && relabel.data?.returnNo === returnNo
|
|
&& relabel.data?.status === 'PENDING_PROCESSING'
|
|
&& relabel.data?.cartonMarks?.includes?.(relabelCandidate.cartonMark),
|
|
`relabelNo=${relabelNo || 'missing'}`, OPTIONS)
|
|
if (relabelNo) {
|
|
const form = new FormData()
|
|
form.append('file', new Blob([new TextEncoder().encode(`%PDF-1.4\n${context.marker}\n%%EOF`)],
|
|
{ type: 'application/pdf' }), `${suffix}-return-relabel.pdf`)
|
|
const uploaded = await context.requestOverseasMultipart('POST',
|
|
`/api/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/files?scope=CARTON_TOTAL`,
|
|
form, {}, merchantToken)
|
|
add(context, '客户编辑退货换标总资料', uploaded, success(uploaded))
|
|
const relabelDetail = await overseas(context, merchantToken, 'GET', '客户回查退货换标资料修改',
|
|
`/api/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}`)
|
|
context.assert(MODULE, '退货换标详情持久化总资料且业务状态未漂移', relabelDetail.response,
|
|
relabelDetail.data?.application?.relabelNo === relabelNo
|
|
&& relabelDetail.data?.application?.status === 'PENDING_PROCESSING'
|
|
&& nonBlank(relabelDetail.data?.application?.totalFileId), trace(relabelDetail.response), OPTIONS)
|
|
context.createdResources.phase4OverseasReturnReceipt.relabelNo = relabelNo
|
|
}
|
|
}
|
|
|
|
const outboundCandidates = await overseas(context, context.overseasToken, 'GET', '平台查询本轮退货出库候选',
|
|
query('/api/platform/v1/last-leg/outbound-orders/return/candidates', {
|
|
customerId, overseasWarehouseId: warehouse.id,
|
|
}))
|
|
const outboundCandidate = (Array.isArray(outboundCandidates.data) ? outboundCandidates.data : [])
|
|
.find(row => row?.returnNo === returnNo && nonBlank(row?.cartonMark)
|
|
&& row.cartonMark !== relabelCandidate?.cartonMark && row.status === 'AVAILABLE')
|
|
context.assert(MODULE, '退货出库候选排除已冻结换标箱', outboundCandidates.response,
|
|
Boolean(outboundCandidate), `cartonMark=${outboundCandidate?.cartonMark || 'missing'}`, OPTIONS)
|
|
if (outboundCandidate) {
|
|
const outboundBody = {
|
|
recipient: `${context.marker} RETURN OUTBOUND`, overseasWarehouseId: warehouse.id,
|
|
returnNo, cartonMarks: [outboundCandidate.cartonMark], remark: context.marker,
|
|
idempotencyKey: key(context, 'OUTBOUND'),
|
|
}
|
|
const outbound = await overseas(context, context.overseasToken, 'POST', '平台创建独立退货出库单',
|
|
query('/api/platform/v1/last-leg/outbound-orders/return', { customerId }), outboundBody)
|
|
const taskNo = id(outbound.data?.taskNo)
|
|
const outboundNo = id(outbound.data?.outboundNo)
|
|
context.assert(MODULE, 'RETURN_OUTBOUND创建PDA待拣任务', outbound.response,
|
|
nonBlank(taskNo) && nonBlank(outboundNo) && outbound.data?.status === 'PENDING',
|
|
`outboundNo=${outboundNo || 'missing'}, taskNo=${taskNo || 'missing'}`, OPTIONS)
|
|
if (taskNo) {
|
|
const task = await overseas(context, picker.token, 'GET', 'PDA读取退货出库任务明细',
|
|
`/api/pda/v1/last-leg/outbound-tasks/${encodeURIComponent(taskNo)}`)
|
|
const taskItem = task.data?.items?.find?.(row => String(row?.cartonMarks || '').includes(outboundCandidate.cartonMark))
|
|
|| task.data?.items?.[0]
|
|
const pickBody = { outboundPalletItemId: taskItem?.outboundPalletItemId,
|
|
inventoryPositionId: null, packages: 1, cartonMarks: [outboundCandidate.cartonMark],
|
|
idempotencyKey: key(context, 'OUTBOUND-PICK') }
|
|
const picked = await overseas(context, picker.token, 'POST', 'PDA一次扫描退货箱完成拣货',
|
|
`/api/pda/v1/last-leg/outbound-tasks/${encodeURIComponent(taskNo)}/pick`, pickBody)
|
|
context.assert(MODULE, '退货出库拣货推进为PICKED且任务进行中', picked.response,
|
|
picked.data?.outboundNo === outboundNo && picked.data?.status === 'IN_PROGRESS', trace(picked.response), OPTIONS)
|
|
const completed = await overseas(context, picker.token, 'POST', 'PDA完成退货出库',
|
|
`/api/pda/v1/last-leg/outbound-tasks/${encodeURIComponent(taskNo)}/complete`,
|
|
{ idempotencyKey: key(context, 'OUTBOUND-COMPLETE') })
|
|
const outboundDetail = await overseas(context, context.overseasToken, 'GET', '平台回查退货出库最终状态',
|
|
query(`/api/platform/v1/last-leg/outbound-orders/${encodeURIComponent(outboundNo)}`, { customerId }))
|
|
context.assert(MODULE, 'RETURN_OUTBOUND真实完成且箱唛只出库一次', outboundDetail.response,
|
|
completed.data?.status === 'COMPLETED'
|
|
&& outboundDetail.data?.outbound?.outboundType === 'RETURN_OUTBOUND'
|
|
&& outboundDetail.data?.outbound?.status === 'OUTBOUNDED'
|
|
&& outboundDetail.data?.pallets?.some?.(row => row.cartonMarks === outboundCandidate.cartonMark
|
|
&& Number(row.pickedPackageCount) === 1), trace(outboundDetail.response), OPTIONS)
|
|
context.createdResources.phase4OverseasReturnReceipt.returnOutboundNo = outboundNo
|
|
}
|
|
}
|
|
} finally {
|
|
await disablePicker(context, picker, 92)
|
|
}
|
|
}
|