94 lines
5.9 KiB
JavaScript
94 lines
5.9 KiB
JavaScript
import { resolve } from 'node:path'
|
|
import { fingerprint, validateOrderInput } from './prod-release-order-stage.mjs'
|
|
import { normalizeOrderInput } from './prod-release-measurement-plan.mjs'
|
|
import { executeOrder } from './prod-release-acceptance.mjs'
|
|
import { executeDelivery } from './prod-release-delivery-stage.mjs'
|
|
import { createFlowContext, withFlowJournal, requireSuccess } from './prod-release-http.mjs'
|
|
import { runDomesticStage } from './prod-release-domestic-stage.mjs'
|
|
import { runOverseasStage, finishSingleOrderTrip } from './prod-release-overseas-stage.mjs'
|
|
|
|
export async function executeFullFlow(settings, rawInput, journalPath, resume = false, fetcher = fetch) {
|
|
const input = { ...normalizeOrderInput(rawInput), customer: settings.customer,
|
|
environment: { mode: settings.environment, apiBase: settings.apiBase },
|
|
operator: { merchant: settings.credentials.MERCHANT.loginId, admin: settings.credentials.ADMIN.loginId } }
|
|
validateOrderInput(input)
|
|
const binding = fingerprint({ input, credentials: Object.fromEntries(Object.entries(settings.credentials).map(([key,value])=>[key,value.loginId])) })
|
|
return withFlowJournal(journalPath, binding, async (state, save) => {
|
|
if (state.status !== 'NEW' && !resume) throw Error('已有全流程检查点,须保持原输入并显式 --resume')
|
|
const ctx = createFlowContext({ settings, state, save, fetcher })
|
|
ctx.marker = input.batch
|
|
const checkpoint = name => {
|
|
const local = state.stages[name] || {}
|
|
return { state: local, save: async patch => {
|
|
const next = { ...local, ...patch }
|
|
await save({ stages: { ...state.stages, [name]: next } }); Object.assign(local, next)
|
|
} }
|
|
}
|
|
const bridgeFetch = async (url, options) => {
|
|
if (!url.startsWith(settings.apiBase + '/')) throw Error('全流程请求偏离统一Gateway')
|
|
const path = '/api' + url.slice(settings.apiBase.length)
|
|
const token = options.headers?.Authorization?.replace(/^Bearer /, '')
|
|
const body = options.body instanceof FormData ? options.body : options.body ? JSON.parse(options.body) : undefined
|
|
const result = await ctx.requestWithBody(options.method, path, body, token ?? null,
|
|
options.headers?.['Idempotency-Key'] ? { 'Idempotency-Key': options.headers['Idempotency-Key'] } : {})
|
|
if (path === '/api/auth/login' && result.body?.code === '200') ctx.registerSession(result.body.data)
|
|
return { ok: result.http >= 200 && result.http < 300, status: result.http, json: async () => result.body }
|
|
}
|
|
try {
|
|
await save({ status: 'RUNNING' })
|
|
ctx.token = (await ctx.login('ADMIN')).accessToken
|
|
ctx.merchantToken = (await ctx.login('MERCHANT')).accessToken
|
|
ctx.pdaToken = (await ctx.login('CN_PDA')).accessToken
|
|
ctx.overseasPdaToken = (await ctx.login('OVERSEAS_LAX_PDA')).accessToken
|
|
const binding = requireSuccess(await ctx.request('GET', '/api/pda/v1/last-leg/receiving-containers/warehouse-profile', undefined, ctx.overseasPdaToken))
|
|
if (binding.warehouseCode !== 'US-LAX-01') throw Error('海外PDA不是洛杉矶接柜仓')
|
|
const orderCp = checkpoint('order')
|
|
let order = orderCp.state.result
|
|
if (!order) {
|
|
order = await executeOrder(settings, input, resolve(`${journalPath}.order.json`), resume, bridgeFetch)
|
|
await orderCp.save({ result: order })
|
|
}
|
|
console.log('单票阶段:ORDER_APPROVED')
|
|
const domesticCp = checkpoint('domestic')
|
|
let domestic = domesticCp.state.result
|
|
if (!domestic) {
|
|
domestic = await runDomesticStage({ input, order, context: ctx, checkpoint: domesticCp })
|
|
await domesticCp.save({ result: domestic })
|
|
}
|
|
console.log(`单票阶段:${domestic.status}`)
|
|
const overseasCp = checkpoint('overseas')
|
|
let overseas = overseasCp.state.result
|
|
if (!overseas) {
|
|
overseas = await runOverseasStage({ input: { ...input, customerId: settings.customer.id,
|
|
packageCount: input.payload.cargoes[0].packageCount, warehouseId: binding.warehouseId },
|
|
domestic: { ...domestic, sourceInboundId: domestic.inboundId, orderId: order.orderId, orderNo: order.orderNo, inboundCode: order.inboundCode },
|
|
context: ctx, checkpoint: overseasCp })
|
|
await overseasCp.save({ result: overseas })
|
|
}
|
|
console.log(`单票阶段:${overseas.status}`)
|
|
const delivery = await executeDelivery(settings, { batch: input.batch, ...overseas,
|
|
orderId: order.orderId, orderNo: order.orderNo, inboundCode: order.inboundCode,
|
|
packageCount: input.payload.cargoes[0].packageCount, warehouseId: binding.warehouseId },
|
|
resolve(`${journalPath}.delivery.json`), bridgeFetch)
|
|
if (delivery.blockers.length) throw Error(`签收/回传未闭环:${delivery.blockers.join(',')}`)
|
|
const tripClosure = await finishSingleOrderTrip({ input: { ...input, customerId: settings.customer.id }, overseas, context: ctx, checkpoint: checkpoint('tripClosure') })
|
|
const result = { status: 'SINGLE_ORDER_HTTP_COMPLETED', fullFlowVerified: true,
|
|
scope: 'ONE_ORDER_NORMAL_HTTP_PATH', orderNo: order.orderNo, inboundCode: order.inboundCode,
|
|
containerNo: domestic.containerNo, dispatchNo: overseas.dispatchNo, deliveryNo: overseas.deliveryNo,
|
|
domesticOrderStatus: delivery.domesticOrderStatus, domesticInboundStatus: delivery.domesticInboundStatus,
|
|
tripClosureStatus: tripClosure.status,
|
|
syntheticEvidence: true, excluded: ['真实货物及设备', '浏览器视觉', '故障注入', '多客户/拆单/异常分支', '真实付款/第三方下单/邮件'],
|
|
completedAt: new Date().toISOString() }
|
|
await save({ status: 'COMPLETED', result })
|
|
return result
|
|
} catch (error) {
|
|
await save({ status: 'BLOCKED', failure: error.message })
|
|
throw error
|
|
} finally {
|
|
const cleanup = await ctx.close()
|
|
if (state.result) state.result.sessionCleanup = cleanup.length ? 'FAILED' : 'COMPLETED'
|
|
await save({ cleanup })
|
|
}
|
|
})
|
|
}
|