优化
This commit is contained in:
@@ -23,7 +23,7 @@ test('IT 测试客户初始化后具备已审核的必填资质和下单资格',
|
||||
test('统一初始化 SQL 提供人工验收所需公共仓储主数据', () => {
|
||||
for (const marker of [
|
||||
"'TP-001'", "'TP-002'", "'A1 普通库位'", "'A0 共享溢出位'",
|
||||
"'CUS-001'", "'CUSTOMER'", "'SHIPPER'", "'EXP-TEST'", "'IT 测试快递'",
|
||||
"'CAR-001'", "'CARRIER'", "'SHIPPER'", "'EXP-TEST'", "'IT 测试快递'",
|
||||
]) assert.ok(sql.includes(marker), `missing initialization marker: ${marker}`)
|
||||
assert.match(sql, /'EXP-TEST', 'IT 测试快递'[\s\S]*?'ACTIVE'/)
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const source = readFileSync(new URL('./master-data.mjs', import.meta.url), 'utf8')
|
||||
|
||||
test('master-data uses only current partner types for API and import fixtures', () => {
|
||||
assert.match(source, /partnerType: 'CARRIER', types: \['CARRIER'\]/)
|
||||
assert.match(source, /types: \['CARRIER'\]/)
|
||||
assert.match(source, /`IMP\$\{suffix\}`, `导入船公司-\$\{suffix\}`, 'CARRIER'/)
|
||||
assert.doesNotMatch(source, /partnerType: 'SUPPLIER'|types: \['SUPPLIER'|SHIPPING_COMPANY/)
|
||||
})
|
||||
@@ -158,7 +158,7 @@ export async function run(context) {
|
||||
expressPage?.records?.some((item) => id(item.id) === expressId && item.status === 'INACTIVE'), `records=${expressPage?.records?.length ?? 'n/a'}`)
|
||||
|
||||
const partnerPayload = {
|
||||
partnerCode: `PT${suffix}`, partnerName: `Admin验收往来单位-${suffix}`, partnerType: 'SUPPLIER', types: ['SUPPLIER'],
|
||||
partnerCode: `PT${suffix}`, partnerName: `Admin验收船公司-${suffix}`, partnerType: 'CARRIER', types: ['CARRIER'],
|
||||
contactName: '往来单位验收员', contactPhone: '13800000002', contactEmail: `partner-${suffix}@example.com`,
|
||||
countryCode: 'CN', province: '广东省', city: '深圳市', district: '南山区', addressLine: 'Admin API 往来单位地址',
|
||||
postalCode: '518000', status: 'ACTIVE', remark: context.marker,
|
||||
@@ -171,8 +171,8 @@ export async function run(context) {
|
||||
const partnerDetailResponse = await context.request('GET', partnerPath)
|
||||
const partner = context.apiSuccess(module, '查询往来单位详情', partnerDetailResponse)
|
||||
context.assert(module, '往来单位详情正确', partnerDetailResponse,
|
||||
id(partner?.id) === partnerId && partner?.types?.includes('SUPPLIER'), `partnerId=${partnerId}`)
|
||||
const updatePartnerResponse = await context.request('PUT', partnerPath, { ...partnerPayload, partnerName: `${partnerPayload.partnerName}-已编辑`, types: ['SUPPLIER', 'CARRIER'], remark: `${context.marker}-EDITED` })
|
||||
id(partner?.id) === partnerId && partner?.types?.includes('CARRIER'), `partnerId=${partnerId}`)
|
||||
const updatePartnerResponse = await context.request('PUT', partnerPath, { ...partnerPayload, partnerName: `${partnerPayload.partnerName}-已编辑`, types: ['CARRIER'], remark: `${context.marker}-EDITED` })
|
||||
context.apiSuccess(module, '编辑往来单位', updatePartnerResponse)
|
||||
const disablePartnerResponse = await context.request('PATCH', `/api/platform/v1/partners/${partnerId}/status?tenantId=0`, { status: 'INACTIVE' })
|
||||
context.apiSuccess(module, '停用往来单位', disablePartnerResponse)
|
||||
@@ -190,7 +190,7 @@ export async function run(context) {
|
||||
const importFixtures = {
|
||||
仓库: { headers: ['仓库编码*', '仓库名称', '仓库类型', '国家二字码', '省/州', '城市*', '区/县', '详细地址*', '邮政编码', '时区', '状态', '联系人', '联系电话', '联系邮箱', '备注'], values: [`IMW${suffix}`, `导入验收仓-${suffix}`, 'DOMESTIC', 'CN', '广东省', '深圳市', '南山区', `导入地址-${context.marker}`, '518000', 'Asia/Shanghai', 'ACTIVE', '导入联系人', '13800000011', `im-${suffix}@example.com`, context.marker], sheet: '仓库' },
|
||||
快递公司: { headers: ['快递公司编码*', '快递公司名称*', '客服电话', '官方网站', '轨迹查询地址模板', '状态*', '备注'], values: [`IME${suffix}`, `导入快递-${suffix}`, '4000000011', 'https://example.com', 'https://example.com/{trackingNo}', 'ACTIVE', context.marker], sheet: '快递公司' },
|
||||
往来单位: { headers: ['单位编码*', '单位名称*', '单位类型*', '联系人*', '联系电话*', '联系邮箱', '国家二字码*', '省/州', '城市*', '区/县', '详细地址*', '邮政编码', '状态*', '备注'], values: [`IMP${suffix}`, `导入单位-${suffix}`, 'SUPPLIER', '导入联系人', '13800000012', `ip-${suffix}@example.com`, 'CN', '广东省', '深圳市', '南山区', `导入地址-${context.marker}`, '518000', 'ACTIVE', context.marker], sheet: '往来单位' },
|
||||
往来单位: { headers: ['单位编码*', '单位名称*', '单位类型*', '联系人*', '联系电话*', '联系邮箱', '国家二字码*', '省/州', '城市*', '区/县', '详细地址*', '邮政编码', '状态*', '备注'], values: [`IMP${suffix}`, `导入船公司-${suffix}`, 'CARRIER', '导入联系人', '13800000012', `ip-${suffix}@example.com`, 'CN', '广东省', '深圳市', '南山区', `导入地址-${context.marker}`, '518000', 'ACTIVE', context.marker], sheet: '往来单位' },
|
||||
}
|
||||
for (const [name, path] of templates) {
|
||||
const response = await context.requestBinary('GET', path)
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
import { findPricedOrderRoute } from './orders.mjs'
|
||||
import { findPricedOrderRoute, findPricedOrderRouteAcrossOrigins } from './orders.mjs'
|
||||
|
||||
const source = readFileSync(new URL('orders.mjs', import.meta.url), 'utf8')
|
||||
|
||||
@@ -13,7 +13,30 @@ test('orders fixture verifies the complete pricing tuple before selecting an FBA
|
||||
assert.match(source, new RegExp(`${field}:`))
|
||||
}
|
||||
assert.match(source, /estimateMatches\(response, candidate\)/)
|
||||
assert.doesNotMatch(source, /const destination = records\(fbaPage\)\[0\]/)
|
||||
assert.doesNotMatch(source, /records\(domesticPage\)\[0\]/)
|
||||
assert.match(source, /findPricedOrderRouteAcrossOrigins/)
|
||||
assert.match(source, /warehouse\?\.status === 'ACTIVE'/)
|
||||
})
|
||||
|
||||
test('pricing selection skips an ACTIVE domestic warehouse with an empty channel catalog', async () => {
|
||||
const channelCalls = []
|
||||
const result = await findPricedOrderRouteAcrossOrigins({
|
||||
receivings: [{ id: 'unpriced', status: 'ACTIVE' }, { id: 'priced', status: 'ACTIVE' }],
|
||||
destinations: [{ id: 'abe3', warehouseCode: 'ABE3', warehouseName: 'ABE3' }],
|
||||
loadChannels: async (receiving) => {
|
||||
channelCalls.push(receiving.id)
|
||||
return receiving.id === 'unpriced'
|
||||
? { http: 200, body: { code: '200', data: [] }, requestId: 'channels-empty', traceId: 'trace-empty' }
|
||||
: { http: 200, body: { code: '200', data: [{ priceBookVersionId: '19', channelCode: 'EXX_RAIL_NY', pricingTaxModes: ['TAX_INCLUDED'] }] } }
|
||||
},
|
||||
estimate: async (request) => ({ http: 200, body: { code: '200', data: {
|
||||
priceBookVersionId: '19', channelCode: request.channelCode, pricingTaxMode: request.pricingTaxMode,
|
||||
} } }),
|
||||
})
|
||||
assert.deepEqual(channelCalls, ['unpriced', 'priced'])
|
||||
assert.equal(result.receiving.id, 'priced')
|
||||
assert.equal(result.priced.destination.warehouseCode, 'ABE3')
|
||||
assert.equal(result.originFailures[0].requestId, 'channels-empty')
|
||||
})
|
||||
|
||||
test('order payload reuses the tax mode from the successful estimate', () => {
|
||||
|
||||
@@ -97,6 +97,34 @@ export async function findPricedOrderRoute({ receiving, destinations, channels,
|
||||
return { priced: null, response: lastResponse, attempts, failures }
|
||||
}
|
||||
|
||||
export async function findPricedOrderRouteAcrossOrigins({ receivings, destinations, loadChannels, estimate,
|
||||
concurrency = PRICING_PROBE_CONCURRENCY }) {
|
||||
const originFailures = []
|
||||
let attempts = 0
|
||||
let response = null
|
||||
for (const receiving of receivings) {
|
||||
if (!receiving?.id) continue
|
||||
const channelResponse = await loadChannels(receiving)
|
||||
response = channelResponse || response
|
||||
const channels = success(channelResponse) && Array.isArray(channelResponse.body?.data)
|
||||
? channelResponse.body.data : []
|
||||
const priceableChannels = channels.filter((item) => Array.isArray(item.pricingTaxModes) && item.pricingTaxModes.length > 0)
|
||||
if (priceableChannels.length === 0) {
|
||||
originFailures.push({ originWarehouseId: id(receiving.id), stage: 'channels', http: channelResponse?.http ?? 'missing',
|
||||
code: channelResponse?.body?.code ?? 'missing', requestId: channelResponse?.requestId ?? 'missing',
|
||||
traceId: channelResponse?.traceId ?? 'missing', count: channels.length })
|
||||
continue
|
||||
}
|
||||
const result = await findPricedOrderRoute({ receiving, destinations, channels: priceableChannels, estimate, concurrency })
|
||||
attempts += result.attempts
|
||||
response = result.response || response
|
||||
if (result.priced) return { receiving, ...result, originFailures }
|
||||
originFailures.push({ originWarehouseId: id(receiving.id), stage: 'estimate', attempts: result.attempts,
|
||||
evidence: pricingFailureEvidence(result) })
|
||||
}
|
||||
return { receiving: null, priced: null, response, attempts, originFailures }
|
||||
}
|
||||
|
||||
function pricingFailureEvidence(result) {
|
||||
const describe = (failure) => failure
|
||||
? `${failure.tuple}[HTTP=${failure.http},code=${failure.code},requestId=${failure.requestId},traceId=${failure.traceId}${failure.mismatch ? `,response=${failure.mismatch}` : ''}]`
|
||||
@@ -104,6 +132,14 @@ function pricingFailureEvidence(result) {
|
||||
return `没有可计价组合;attempts=${result.attempts}, first=${describe(result.failures[0])}, last=${describe(result.failures.at(-1))}`
|
||||
}
|
||||
|
||||
function originPricingFailureEvidence(result) {
|
||||
const describe = (failure) => !failure ? 'missing'
|
||||
: failure.stage === 'channels'
|
||||
? `origin=${failure.originWarehouseId},channels[HTTP=${failure.http},code=${failure.code},count=${failure.count},requestId=${failure.requestId},traceId=${failure.traceId}]`
|
||||
: `origin=${failure.originWarehouseId},estimate[attempts=${failure.attempts},${failure.evidence}]`
|
||||
return `没有 ACTIVE 国内仓可计价组合;origins=${result.originFailures?.length ?? 0}, attempts=${result.attempts}, first=${describe(result.originFailures?.[0])}, last=${describe(result.originFailures?.at(-1))}`
|
||||
}
|
||||
|
||||
async function request(context, method, path, body, token, extraHeaders = {}) {
|
||||
const headers = { Accept: 'application/json', Authorization: `Bearer ${token}`, ...extraHeaders }
|
||||
let payload = body
|
||||
@@ -171,32 +207,25 @@ async function loadMasterData(context, merchantToken) {
|
||||
const fbaPage = context.apiSuccess(MODULE, 'Client 查询 FBA 目的仓', fbaResponse)
|
||||
context.assert(MODULE, '存在可用 FBA 目的仓', fbaResponse, records(fbaPage).length > 0,
|
||||
`count=${records(fbaPage).length}`)
|
||||
const receiving = records(domesticPage)[0]
|
||||
const receivings = records(domesticPage).filter((warehouse) => warehouse?.status === 'ACTIVE')
|
||||
const destinations = records(fbaPage)
|
||||
if (!receiving || destinations.length === 0) throw new Error('缺少创建测试订单所需的收货仓或目的仓')
|
||||
if (receivings.length === 0 || destinations.length === 0) throw new Error('缺少创建测试订单所需的收货仓或目的仓')
|
||||
|
||||
const channelResponse = await request(context, 'GET',
|
||||
`/api/merchant/v1/first-leg/pricing/channels?originWarehouseId=${encodeURIComponent(receiving.id)}`,
|
||||
undefined, merchantToken)
|
||||
const channels = context.apiSuccess(MODULE, 'Client 查询可用计费渠道', channelResponse)
|
||||
const priceableChannels = Array.isArray(channels)
|
||||
? channels.filter((item) => Array.isArray(item.pricingTaxModes) && item.pricingTaxModes.length > 0)
|
||||
: []
|
||||
context.assert(MODULE, '存在带计价税制的可用渠道', channelResponse, priceableChannels.length > 0,
|
||||
`count=${Array.isArray(channels) ? channels.length : 'n/a'}`)
|
||||
if (priceableChannels.length === 0) throw new Error('缺少创建测试订单所需的可用渠道')
|
||||
|
||||
const pricingResult = await findPricedOrderRoute({
|
||||
receiving, destinations, channels: priceableChannels,
|
||||
const pricingResult = await findPricedOrderRouteAcrossOrigins({
|
||||
receivings, destinations,
|
||||
loadChannels: (receiving) => request(context, 'GET',
|
||||
`/api/merchant/v1/first-leg/pricing/channels?originWarehouseId=${encodeURIComponent(receiving.id)}`,
|
||||
undefined, merchantToken),
|
||||
estimate: (body) => request(context, 'POST', '/api/merchant/v1/first-leg/pricing/estimate', body, merchantToken),
|
||||
})
|
||||
const priced = pricingResult.priced
|
||||
context.addStep(MODULE, 'Client 选择具有真实价格覆盖的渠道和目的仓', pricingResult.response, Boolean(priced),
|
||||
const receiving = pricingResult.receiving
|
||||
context.addStep(MODULE, 'Client 从 ACTIVE 国内仓选择具有真实价格覆盖的渠道和目的仓', pricingResult.response, Boolean(priced),
|
||||
priced
|
||||
? `origin=${receiving.id}, priceBookVersionId=${priced.channel.priceBookVersionId}, channel=${priced.channel.channelCode}, pricingTaxMode=${priced.pricingTaxMode}, destination=${priced.destination.warehouseCode}, attempts=${pricingResult.attempts}`
|
||||
: pricingFailureEvidence(pricingResult),
|
||||
: originPricingFailureEvidence(pricingResult),
|
||||
CONTRACT_OPTIONS)
|
||||
if (!priced) throw new Error('当前起运仓、渠道、报价税制和FBA目的仓没有可计价组合')
|
||||
if (!priced) throw new Error('当前 ACTIVE 国内收货仓、渠道、报价税制和FBA目的仓没有可计价组合')
|
||||
|
||||
const partnerResponse = await request(context, 'GET',
|
||||
'/api/merchant/v1/partners/candidates?type=SHIPPER', undefined, merchantToken)
|
||||
|
||||
@@ -2,6 +2,7 @@ const MODULE = 'truck-appointments'
|
||||
const OPTIONS = { scenario: 'truck-appointment-lifecycle', deploymentBlocker: true }
|
||||
const BASE = '/api/platform/v1/truck-appointments'
|
||||
const OCEAN_BOOKING_BASE = '/api/platform/v1/ocean-bookings'
|
||||
const SUPPLIER_BASE = '/api/platform/v1/suppliers?tenantId=0'
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
|
||||
@@ -67,10 +68,11 @@ export function inspectOceanBookingIdentity(marker, resource, detail = null) {
|
||||
}
|
||||
|
||||
export function truckAppointmentPayload(context, suffix, identity, version = 0) {
|
||||
const supplier = context.createdResources?.truckSupplier || {}
|
||||
return {
|
||||
outboundOrderId: null, oceanBookingId: identity.oceanBookingId, containerBookingId: null, loadingPlanId: null,
|
||||
warehouseId: identity.warehouseId, customerId: identity.customerId, customerName: identity.customerName,
|
||||
carrierId: null, carrierName: `承运商-${suffix}`, fleetContact: '车队联系人', fleetPhone: '13800000004',
|
||||
carrierId: supplier.supplierId || null, carrierName: supplier.supplierName || `承运商-${suffix}`, fleetContact: '车队联系人', fleetPhone: '13800000004',
|
||||
driverName: '司机验收', driverPhone: '13800000005', driverIdCard: null, vehicleNo: `粤B-${suffix.slice(-6)}`,
|
||||
storeName: `门店-${suffix}`, storeContact: '门店联系人', storePhone: '13800000006', storeAddress: '深圳市南山区验收地址',
|
||||
storeCity: '深圳市', storeZipcode: '518000', packageCount: 10, grossWeightKg: 100, volumeCbm: 2.5,
|
||||
@@ -79,6 +81,30 @@ export function truckAppointmentPayload(context, suffix, identity, version = 0)
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureTruckSupplier(context, suffix) {
|
||||
const response = await request(context, 'POST', SUPPLIER_BASE, {
|
||||
name: `拖车供应商-${suffix}`.slice(0, 128), category: 'TRUCKING', scope: null,
|
||||
region: '深圳', legalRepresentative: '验收法人', contactName: '拖车联系人',
|
||||
phone: '13800000000', email: 'trucking-acceptance@example.com', address: '深圳市验收地址',
|
||||
businessLicenseNo: `TRUCK${suffix}`.slice(0, 64), licenseFileId: null,
|
||||
status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: context.marker,
|
||||
})
|
||||
const supplierValue = data(response)
|
||||
const supplierId = id(typeof supplierValue === 'object' ? supplierValue?.id : supplierValue)
|
||||
let supplier = typeof supplierValue === 'object' ? supplierValue : null
|
||||
if (supplierId) {
|
||||
const detailResponse = await request(context, 'GET', `/api/platform/v1/suppliers/${encodeURIComponent(supplierId)}?tenantId=0`)
|
||||
supplier = data(detailResponse)
|
||||
}
|
||||
context.addStep(MODULE, '创建本轮有效拖车供应商', response,
|
||||
Boolean(supplierId) && supplier?.category === 'TRUCKING' && supplier?.status === 'ACTIVE',
|
||||
`supplierId=${supplierId || 'missing'}, category=${supplier?.category || 'missing'}, status=${supplier?.status || 'missing'}`, OPTIONS)
|
||||
if (supplierId) context.createdResources.truckSupplier = {
|
||||
marker: context.marker, supplierId, supplierName: supplier.name, finalStatus: 'ACTIVE', retained: true,
|
||||
}
|
||||
return context.createdResources.truckSupplier || null
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
const suffix = context.marker.replace(/\D/g, '').slice(-10)
|
||||
const list = await request(context, 'GET', `${BASE}?page=1&size=5&keyword=${encodeURIComponent(context.marker)}`)
|
||||
@@ -122,6 +148,9 @@ export async function run(context) {
|
||||
}
|
||||
const bookingIdentity = Object.freeze({ ...identityResult.identity })
|
||||
|
||||
const truckSupplier = await ensureTruckSupplier(context, suffix)
|
||||
if (!truckSupplier) return
|
||||
|
||||
const createResponse = await request(context, 'POST', BASE, truckAppointmentPayload(context, suffix, bookingIdentity)); const appointment = context.apiSuccess(MODULE, '创建隔离拖车预约', createResponse, '', OPTIONS)
|
||||
const appointmentId = id(appointment?.id)
|
||||
context.assert(MODULE, '拖车预约费用、司机车辆和地址字段完整', createResponse, Boolean(appointmentId) && appointment?.status === 'DRAFT' && appointment?.payableAmount != null && appointment?.receivableAmount != null, `id=${appointmentId || 'missing'}, status=${appointment?.status || 'missing'}`, OPTIONS)
|
||||
|
||||
Reference in New Issue
Block a user