This commit is contained in:
2026-09-01 10:00:13 +08:00
parent 249b182e7f
commit 477971dda1
11 changed files with 145 additions and 24 deletions
@@ -46,6 +46,7 @@ test('PAYABLE fee selection uses only the explicit nullable supplierCategory fie
test('Finance PAYABLE fixture creates an isolated ACTIVE supplier through the formal API', () => {
const body = buildSupplierBody('ADMIN-API-20260815-FINANCE', 'BOOKING')
assert.equal(body.category, 'BOOKING')
assert.deepEqual(body.carrierCodes, ['MATSON'])
assert.equal(Object.hasOwn(body, 'scope'), false)
assert.equal(body.status, 'ACTIVE')
assert.match(body.name, /^财务验收供应商-/)
@@ -53,6 +54,10 @@ test('Finance PAYABLE fixture creates an isolated ACTIVE supplier through the fo
assert.match(source, /detail\?\.status === 'ACTIVE' && \(!category \|\| detail\?\.category === category\)/)
})
test('Finance measurement can reuse real pallets from the selected pricing warehouse', () => {
assert.match(source, /context\.context\.allowAnyExistingPallet = true/)
})
test('fee detail writes preserve 19-digit fee item IDs as strings', () => {
assert.doesNotMatch(source, /feeItemId: Number\(/)
assert.match(source, /feeItemId: String\(feeItem\.id\)/)
@@ -37,6 +37,7 @@ export function buildSupplierBody(marker, category) {
return {
name: `财务验收供应商-${suffix}`,
category,
...(category === 'BOOKING' ? { carrierCodes: ['MATSON'] } : {}),
region: '宁波', legalRepresentative: '财务验收法人', contactName: '财务验收联系人',
phone: '13800000000', email: `finance-supplier-${suffix || 'fixture'}@example.com`,
address: '财务验收地址', businessLicenseNo: `FIN${suffix}`.slice(0, 64),
@@ -98,7 +99,7 @@ async function measureOrder(context, resource) {
if (context.financeFixtureHooks?.measureOrder) return context.financeFixtureHooks.measureOrder(context, resource)
const originalMarker = context.marker
const createdKeys = ['orders', 'inboundMeasurement', 'ocrInbound', 'testInbound']
const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound']
const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound', 'allowAnyExistingPallet']
const createdSnapshot = new Map(createdKeys.map((key) => [key, { present: Object.hasOwn(context.createdResources, key), value: context.createdResources[key] }]))
const contextSnapshot = new Map(contextKeys.map((key) => [key, { present: Object.hasOwn(context.context, key), value: context.context[key] }]))
context.marker = resource.marker
@@ -109,6 +110,10 @@ async function measureOrder(context, resource) {
context.context.ocrInbound = resource
context.context.testInbound = resource
context.context.createdInbound = resource
// Finance pricing profiles can point at any configured domestic warehouse.
// Reuse that warehouse's real pallet master instead of requiring the bulk-data
// generator's two fixed Yiwu pallet codes.
context.context.allowAnyExistingPallet = true
try {
const measurement = await import(new URL('./inbound-measurement.mjs', import.meta.url))
await measurement.run(context)
@@ -30,9 +30,25 @@ function validPdf(response) {
async function reusablePallets(context, warehouseId) {
const cacheKey = asId(warehouseId)
if (reusablePalletsByWarehouse.has(cacheKey)) return reusablePalletsByWarehouse.get(cacheKey)
const allowAnyExistingPallet = context.context?.allowAnyExistingPallet === true
const scopedCacheKey = `${cacheKey}:${allowAnyExistingPallet ? 'any' : 'fixed'}`
if (reusablePalletsByWarehouse.has(scopedCacheKey)) return reusablePalletsByWarehouse.get(scopedCacheKey)
const pallets = []
if (allowAnyExistingPallet) {
const response = await context.request('GET',
`/api/platform/v1/pallets?page=1&size=100&tenantId=0&warehouseId=${encodeURIComponent(warehouseId)}`)
const page = context.apiSuccess(MODULE, '查询当前仓库已有可复用托盘', response,
`warehouseId=${warehouseId}`, OPTIONS)
const candidates = (page?.records || []).filter((item) => asId(item?.warehouseId) === cacheKey
&& asId(item?.id) && String(item?.palletCode || '').trim())
context.assert(MODULE, '当前仓库至少存在一个可复用托盘', response, candidates.length > 0,
`warehouseId=${warehouseId}, matched=${candidates.map((item) => item.palletCode).join('|') || 'missing'}`, OPTIONS)
if (candidates.length === 0) return null
const selected = candidates.slice(0, 2).map((item) => ({ id: asId(item.id), code: String(item.palletCode) }))
reusablePalletsByWarehouse.set(scopedCacheKey, selected)
return selected
}
for (const palletCode of REUSABLE_PALLET_CODES) {
const response = await context.request('GET',
`/api/platform/v1/pallets?page=1&size=20&tenantId=0&warehouseId=${encodeURIComponent(warehouseId)}&palletCode=${encodeURIComponent(palletCode)}`)
@@ -45,7 +61,7 @@ async function reusablePallets(context, warehouseId) {
if (!exact) return null
pallets.push({ id: asId(exact.id), code: palletCode })
}
reusablePalletsByWarehouse.set(cacheKey, pallets)
reusablePalletsByWarehouse.set(scopedCacheKey, pallets)
return pallets
}
@@ -79,7 +95,7 @@ async function provisionWarehouseResources(context, { inboundId, warehouseId, pa
for (let index = 1; index <= palletCount; index += 1) {
const palletId = palletIds[index - 1]
context.assert(MODULE, `随机复用原有称重托盘 ${index}/${palletCount}`, null,
REUSABLE_PALLET_CODES.includes(candidates.find((item) => item.id === palletId)?.code),
Boolean(candidates.find((item) => item.id === palletId)?.code),
`warehouseId=${warehouseId}, palletId=${palletId}`, OPTIONS)
// 库位由 Warehouse 在测量审核时按正式规则分配;普通位不足会复用唯一的 A0 共享溢出位。
// 验收脚本不得为了制造容量而新增普通库位。
@@ -391,12 +391,27 @@ export async function run(context) {
{ expectedVersion: 0, data: bookingDraft }, { 'Idempotency-Key': `${context.marker}-loading-booking-create` })
const bookingCreatedDraft = context.apiSuccess(MODULE, '登记含渠道仓库和运输快照的 DRAFT 订舱', bookingCreate, '', OPTIONS)
const bookingId = id(bookingCreatedDraft?.oceanBooking?.id)
if (!bookingId) return
const bookingDocumentForm = new FormData()
bookingDocumentForm.append('expectedVersion', String(Number(bookingCreatedDraft?.oceanBooking?.version)))
bookingDocumentForm.append('documentType', 'BOOKING_CONFIRMATION')
bookingDocumentForm.append('file', new Blob([`${context.marker}-BOOKING_CONFIRMATION`], { type: 'text/plain' }),
`booking-confirmation-${context.marker}.txt`)
const bookingDocumentResponse = await context.requestMultipart('POST',
`/api/platform/v1/ocean-bookings/${bookingId}/documents/upload`, bookingDocumentForm, context.token,
{ 'Idempotency-Key': `${context.marker}-loading-booking-document`.slice(0, 128) })
const bookingDocument = context.apiSuccess(MODULE, '上传有效订舱确认文件', bookingDocumentResponse, '', OPTIONS)
const releaseVersion = Number(bookingDocument?.bookingVersion)
context.assert(MODULE, '订舱确认文件已关联且版本递增', bookingDocumentResponse,
Boolean(bookingDocument?.document?.id) && bookingDocument?.document?.documentType === 'BOOKING_CONFIRMATION'
&& Number.isInteger(releaseVersion),
`documentId=${id(bookingDocument?.document?.id) || 'missing'}, bookingVersion=${bookingDocument?.bookingVersion ?? 'missing'}`, OPTIONS)
if (!Number.isInteger(releaseVersion)) return
const bookingRelease = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/release`, {
expectedVersion: Number(bookingCreatedDraft?.oceanBooking?.version), reason: context.marker,
expectedVersion: releaseVersion, reason: context.marker,
}, { 'Idempotency-Key': `${context.marker}-loading-booking-release`.slice(0, 128) })
const bookingCreated = context.apiSuccess(MODULE, '放舱订舱并冻结运输事实', bookingRelease, '', OPTIONS)
const bookingNo = id(bookingCreated?.oceanBooking?.bookingNo)
if (!bookingId) return
const confirmedBooking = bookingCreated
const transportFields = ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']
const frozenTransport = Object.fromEntries(transportFields.map((field) => [field, confirmedBooking?.oceanBooking?.[field]]))
@@ -92,6 +92,8 @@ test('PDA task opens only after no-number push, release, four-field link and pla
assert.match(containerLoadingAcceptance, /oceanBookingId: bookingId, requirementId: id\(requirement\.id\), containerNo: realContainerNo/)
assert.match(containerLoadingAcceptance, /关联后列表仅保留模拟柜原地补全的一条 ACTIVE 操作身份/)
assert.match(containerLoadingAcceptance, /ocean-bookings\/\$\{bookingId\}\/release/)
assert.match(containerLoadingAcceptance, /ocean-bookings\/\$\{bookingId\}\/documents\/upload/)
assert.match(containerLoadingAcceptance, /documentType', 'BOOKING_CONFIRMATION'/)
assert.match(containerLoadingAcceptance, /订舱放舱为 RELEASED 且不预生成真实货柜/)
assert.match(containerLoadingAcceptance, /订舱 RELEASED 但预装未 CONFIRMED 不生成 PDA 任务/)
assert.match(containerLoadingAcceptance, /loading-plans\/\$\{planId\}\/confirm/)