优化
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
# Admin 测试环境 HTTP 模块
|
||||
|
||||
固定客户编排也将 `customers`、`supplemental-boundaries` 明确记为跳过,不运行建客或边界仓库/库位生命周期。Finance 供应商按启用状态与费用类别查询复用,详情变化或无候选时阻断,不补建;海外入仓费用只允许关联本轮 PDA `generatedPlanIds` 内且客户、批次均匹配的计划,禁止回退历史计划。混合报关柜缺第二固定客户时在准备订单和装柜前阻断。
|
||||
|
||||
固定客户模式下,默认编排排除 `system`、`master-data`、`suppliers`、`notifications`、`pricing`、`phase4-overseas-master-data`、`phase4-overseas-wallet-and-pallet-cost` 的生命周期写入,报告使用 skipped 和 coverage gap,不计模块通过。海外仓模块复用现有仓及只读资源,缺仓直接阻断;不自动创建库位/托盘。业务订单与履约证据仍按批次创建,不能将历史已完成单据当作本轮新证据。其他业务模块内嵌的权限探针账号及专用业务资源仍可能创建,当前模式不代表零新增数据。
|
||||
|
||||
Admin TEST 入口必须通过显式环境文件配置 `ADMIN_TEST_FIXED_CUSTOMER_ID`(`TEN` 编号或数字 ID)及该客户的 `MERCHANT_TEST_USERNAME`、`MERCHANT_TEST_PASSWORD`。每次先核验账号归属、ACTIVE 状态和尺寸初始化,再复用客户造业务数据;校验失败停止后续模块,不自动建客、重提资质或重置密码。客户创建/归档生命周期明确排除,海外仓储费模块只读核验既有规则,不覆盖固定客户配置。需要第二客户的隔离用例在单客户模式下阻断,不用同一客户替代跨客户验证。该限制不代表这些生命周期或跨客户场景验收通过,也不清理历史客户。
|
||||
|
||||
`fleet-return-redispatch-acceptance.mjs` 的原出发证明读取本轮 `LOADING_COMPLETE` 回执与 `departureDeliveries`;重派在 PDA 装车完成后回查自动生成的出库/派送及两箱已发运事实,不再调用手工安排派送或确认发车。原键重放和跨操作员拒绝仍由共享装车验证覆盖,原签收不漂移、源订单完成事件审计及资源分别释放仍须独立核对。离线模拟不能代替匹配部署的真实 HTTP。
|
||||
|
||||
固定托盘造数中,`inbound-measurement.mjs` 只复用 `TP-001`、`YW-TP-002` 指定池中的现有托盘;一只可复用,不要求两只同时存在,两只均缺失才阻断,不自动选择池外托盘。仅明确设置内部 `allowAnyExistingPallet` 的通用场景可读取当前仓库已有资源;两者均不创建托盘。
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import { prepare } from './phase3-container-customs-fixture.mjs'
|
||||
|
||||
test('fixed single customer blocks mixed customs before creating orders or loading fixtures', async () => {
|
||||
await assert.rejects(prepare({ options: { fixedCustomerId: '123' } }), /停止订单及装柜造数/)
|
||||
})
|
||||
import { verifyFormalDeclarationAndExceptions, verifyMixedReceiptIsolation } from './phase3-container-customs.mjs'
|
||||
|
||||
import {
|
||||
|
||||
@@ -236,6 +236,7 @@ async function verifyFrozenSource(context, fixture) {
|
||||
}
|
||||
|
||||
async function createSecondCustomer(context) {
|
||||
if (context.options?.fixedCustomerId) throw new Error('缺少第二个固定客户,停止跨客户财务写入')
|
||||
const originalMarker = context.marker
|
||||
const originalContext = context.context?.merchantQualificationFixture
|
||||
const originalResource = context.createdResources?.merchantQualificationFixture
|
||||
|
||||
@@ -4,7 +4,30 @@ import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { buildContainerCostBody, buildSupplierBody, normalizeFinanceFixtureSources, run,
|
||||
supplierCategoryForFeeItem } from './finance-full-fixtures.mjs'
|
||||
supplierCategoryForFeeItem, loadSupplier } from './finance-full-fixtures.mjs'
|
||||
|
||||
test('fixed customer supplier selection paginates and never creates a fallback', async () => {
|
||||
const calls = [], failures = []
|
||||
const supplier = { id: '123', status: 'ACTIVE', category: 'TRUCKING', name: 'Existing' }
|
||||
const context = {
|
||||
options: { fixedCustomerId: '456' }, marker: 'REUSE',
|
||||
async request(method, path) {
|
||||
calls.push({ method, path })
|
||||
const data = path.includes('/123?') ? supplier : path.includes('page=2')
|
||||
? { records: [supplier], total: 101 }
|
||||
: { records: Array.from({ length: 100 }, () => ({ id: '99', status: 'ACTIVE', category: 'BOOKING' })), total: 101 }
|
||||
return { body: { data } }
|
||||
},
|
||||
apiSuccess(_module, _name, response) { return response.body.data },
|
||||
addStep(...args) { failures.push(args) },
|
||||
}
|
||||
assert.equal((await loadSupplier(context, 'TRUCKING')).id, '123')
|
||||
assert.equal(calls.length, 3)
|
||||
assert.ok(calls.every(call => call.method === 'GET'))
|
||||
context.request = async method => { assert.equal(method, 'GET'); return { body: { data: { records: [], total: 0 } } } }
|
||||
assert.equal(await loadSupplier(context, 'TRUCKING'), null)
|
||||
assert.equal(failures.length, 1)
|
||||
})
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const source = fs.readFileSync(path.join(here, 'finance-full-fixtures.mjs'), 'utf8')
|
||||
|
||||
@@ -258,8 +258,27 @@ async function loadPublishedAutomaticFee(context, resource) {
|
||||
actualWeightKg: responseWeightKg, actualVolumeCbm: responseVolumeCbm }
|
||||
}
|
||||
|
||||
async function loadSupplier(context, category) {
|
||||
export async function loadSupplier(context, category) {
|
||||
if (context.financeFixtureHooks?.loadSupplier) return context.financeFixtureHooks.loadSupplier(context, category)
|
||||
if (context.options?.fixedCustomerId) {
|
||||
for (let page = 1; ; page += 1) {
|
||||
const response = await request(context, 'GET', `/api/platform/v1/suppliers?tenantId=0&status=ACTIVE&page=${page}&size=100`)
|
||||
const data = context.apiSuccess(MODULE, '读取可复用的现有供应商', response, '', OPTIONS)
|
||||
if (!data) return null
|
||||
const records = rows(data)
|
||||
const candidate = records.find(item => id(item?.id) && item.status === 'ACTIVE' && (!category || item.category === category))
|
||||
if (candidate) {
|
||||
const detailResponse = await request(context, 'GET', `/api/platform/v1/suppliers/${encodeURIComponent(candidate.id)}?tenantId=0`)
|
||||
const detail = context.apiSuccess(MODULE, '核对复用供应商状态与类别', detailResponse, '', OPTIONS)
|
||||
if (id(detail?.id) === id(candidate.id) && detail?.status === 'ACTIVE' && (!category || detail.category === category)) return detail
|
||||
blocker(context, '复用供应商已变化', '候选详情不匹配,不创建替代供应商')
|
||||
return null
|
||||
}
|
||||
if (!records.length || page * 100 >= Number(data.total ?? records.length)) break
|
||||
}
|
||||
blocker(context, '缺少可复用供应商', `需要 ACTIVE ${category || '任意类别'} 供应商;禁止自动创建`)
|
||||
return null
|
||||
}
|
||||
const effectiveCategory = category || 'BOOKING'
|
||||
const body = buildSupplierBody(context.marker, effectiveCategory)
|
||||
const response = await request(context, 'POST', '/api/platform/v1/suppliers?tenantId=0', body)
|
||||
|
||||
@@ -122,6 +122,7 @@ async function createCustomerWaitSplit(context, suffix, fixedRoute = null) {
|
||||
}
|
||||
|
||||
async function provisionCrossCustomerPlan(context) {
|
||||
if (context.options?.fixedCustomerId) throw new Error('缺少第二个固定客户,停止跨客户柜费造数')
|
||||
const originalMarker = context.marker
|
||||
const originalUsername = process.env.MERCHANT_TEST_USERNAME
|
||||
const originalPassword = process.env.MERCHANT_TEST_PASSWORD
|
||||
|
||||
@@ -143,7 +143,7 @@ export async function runQuoteSession(context, { marker = 'FLEET-QUOTE-20261003'
|
||||
if (!successful(response) || !login?.accessToken || !login?.refreshToken) throw new Error('Independent TEST login failed')
|
||||
context.token = login.accessToken; context.refreshToken = login.refreshToken
|
||||
const prepare = prepareCustomer || (await import('./merchant-qualification-fixture.mjs')).run
|
||||
await context.executeModule('merchant-qualification-fixture', () => prepare(context, { publishEnvironment: false }))
|
||||
await context.executeModule('merchant-qualification-fixture', () => prepare(context, { publishEnvironment: false, requireDistinctCustomer: false }))
|
||||
if (context.steps.some(step => !step.passed)) throw new Error('Current-run customer preparation failed; quotes not executed')
|
||||
await context.executeModule(MODULE, () => run(context))
|
||||
} catch (error) { fatal = error instanceof Error ? error.message : String(error) }
|
||||
|
||||
@@ -34,7 +34,7 @@ test('real Jackson long counts accept safe decimal strings without changing pall
|
||||
function server(fault = '') {
|
||||
const calls = [], assertions = [], saved = new Map(), loadingActors = new Map()
|
||||
const detail = { dispatchNo: 'DP-A', status: 'ACTIVE', stage: 'BEFORE_LOADING', reviewState: 'DRAFT', recordVersion: '9007199254740998',
|
||||
counts: { packageCount: 3, pickedPackageCount: 0, shippedPackageCount: 0 }, reviews: {},
|
||||
counts: { packageCount: 3, pickedPackageCount: 0, shippedPackageCount: 0 }, reviews: {}, publishLooseboardDemand: false,
|
||||
dispatchInfo: { estimatedArrivalAt: '2026-10-21T10:00:00Z', fbaAppointments: [{ appointmentNo: 'FBA-ORIGINAL' }] }, vehicleInfo: {} }
|
||||
const trip = { tripNo: 'TR-A', status: 'PLANNED', warehouseId: '92', recordVersion: 4, tripPurpose: 'DELIVERY', stops }
|
||||
let driverVersion = 0, uploadComplete = false, driverReviewed = false, nextPlate = '', checkpointError = false, nextTrip, nextDetail
|
||||
@@ -103,6 +103,7 @@ function server(fault = '') {
|
||||
pallets: [pallet()], missingFields: ['PLT-A:palletSpaces'] })
|
||||
}
|
||||
if (method === 'PUT' && path === D) {
|
||||
assert.equal(body.publishLooseboardDemand, detail.publishLooseboardDemand)
|
||||
if (detail.stage !== 'BEFORE_LOADING') {
|
||||
if (fault === 'edit-mutates-on-rejection') detail.counts.pickedPackageCount--
|
||||
return conflict('仅装车前可编辑,请显式退回;已经扫码不得改派')
|
||||
@@ -180,6 +181,15 @@ function server(fault = '') {
|
||||
|
||||
const prepare = s => prepareFleetTrip(s.context, 'native', 'DP-A', '92')
|
||||
|
||||
test('review preparation uses the current confirmed appointment when arrival is unset', async () => {
|
||||
const s = server(), at = new Date(Date.now() + 72 * 3600000).toISOString()
|
||||
s.detail.dispatchInfo.estimatedArrivalAt = null
|
||||
s.detail.dispatchInfo.fbaAppointments = [{ status: 'PENDING', confirmedAt: null }, { status: 'SUCCESS', confirmedAt: at }]
|
||||
const proof = await prepare(s)
|
||||
assert.equal(proof.updateRequest.estimatedArrivalAt, at)
|
||||
assert.ok(Date.parse(proof.expectedDriverEndAt) > Date.parse(at))
|
||||
})
|
||||
|
||||
function jacksonCountWire(s) {
|
||||
const names = new Set(['itemCount', 'palletCount', 'packageCount', 'pickedPackageCount', 'shippedPackageCount', 'scheduledPackages', 'pickedPackages'])
|
||||
const serialize = value => Array.isArray(value) ? value.map(serialize)
|
||||
|
||||
@@ -104,11 +104,14 @@ export async function prepareFleetTrip(context, module, dispatchNo, warehouseId)
|
||||
}, '建立本轮TEST车辆主档及容量')
|
||||
proof.vehicleId = id(vehicle.id)
|
||||
if (!proof.vehicleId) throw new Error('车辆主档没有返回真实资源ID')
|
||||
await prepareQualifiedDriver(context, module, proof)
|
||||
const confirmedTimes = (before.dispatchInfo?.fbaAppointments || []).filter(row => row.status === 'SUCCESS' && row.confirmedAt).map(row => row.confirmedAt).sort()
|
||||
const arrival = before.dispatchInfo?.estimatedArrivalAt ?? confirmedTimes[0] ?? null
|
||||
await prepareQualifiedDriver(context, module, proof, arrival)
|
||||
proof.updateRequest = { recordVersion: before.recordVersion, idempotencyKey: key(proof, 'EDIT'),
|
||||
publishLooseboardDemand: before.publishLooseboardDemand,
|
||||
vehicle: { mode: 'MASTER', vehicleId: proof.vehicleId },
|
||||
driver: { mode: 'MASTER', driverId: proof.driverId, evidence: { expectedTaskEndAt: proof.expectedDriverEndAt } },
|
||||
plannedDepartureAt: new Date(Date.now() + 3600000).toISOString(), estimatedArrivalAt: before.dispatchInfo?.estimatedArrivalAt ?? null,
|
||||
plannedDepartureAt: new Date(Date.now() + 3600000).toISOString(), estimatedArrivalAt: arrival,
|
||||
remark: `${context.marker} TEST合成资料正常履约` }
|
||||
let result = await command(context, module, 'PUT', path, proof.updateRequest, '通过排车主单编辑正式分配车辆司机')
|
||||
for (const action of ['submit-review', 'dispatch-review', 'loading-review']) {
|
||||
@@ -127,8 +130,8 @@ export async function prepareFleetTrip(context, module, dispatchNo, warehouseId)
|
||||
proof.preparation = 'VERIFIED'; await context.checkpoint?.(); return proof
|
||||
}
|
||||
|
||||
async function prepareQualifiedDriver(context, module, proof) {
|
||||
proof.expectedDriverEndAt = new Date(Date.now() + 48 * 3600000).toISOString()
|
||||
async function prepareQualifiedDriver(context, module, proof, arrival) {
|
||||
proof.expectedDriverEndAt = new Date(Math.max(Date.now() + 48 * 3600000, arrival ? Date.parse(arrival) + 6 * 3600000 : 0)).toISOString()
|
||||
let driver = await command(context, module, 'POST', `${BASE}/drivers`, {
|
||||
name: `${proof.marker}验收司机`, phone: '19900000000', ownership: 'OWNED', region: 'OVERSEAS',
|
||||
licenseNumber: proof.marker, licenseType: 'Class A', licenseZone: 'UTC',
|
||||
|
||||
@@ -1,7 +1,96 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import { qualificationCreditCode } from './merchant-qualification-fixture.mjs'
|
||||
import vm from 'node:vm'
|
||||
import { qualificationCreditCode, normalizeFixedCustomerId, run } from './merchant-qualification-fixture.mjs'
|
||||
import { AcceptanceRun } from '../admin-test-env-acceptance.mjs'
|
||||
|
||||
function fixedContext(profileId = '2100000000000000001') {
|
||||
const calls = []
|
||||
return {
|
||||
calls, marker: 'FIXED-CUSTOMER-TEST', context: {}, createdResources: {},
|
||||
options: { fixedCustomerId: 'TEN2100000000000000001', merchantUsername: 'fixture-user', merchantPassword: 'fixture-password' },
|
||||
async request(method, path) {
|
||||
calls.push({ method, path })
|
||||
const data = path.endsWith('/login') ? { accessToken: 'fixture-token', refreshToken: 'fixture-refresh' }
|
||||
: path.endsWith('/profile') ? { id: profileId, status: 'ACTIVE', tenantName: 'Fixture' }
|
||||
: { status: 'READY' }
|
||||
return { http: 200, body: { code: 200, data } }
|
||||
},
|
||||
apiSuccess(_module, _label, response) { return response.body.data },
|
||||
assert() {},
|
||||
}
|
||||
}
|
||||
|
||||
test('fixed customer accepts business number without losing integer precision', () => {
|
||||
assert.equal(normalizeFixedCustomerId('TEN2100000000000000001'), '2100000000000000001')
|
||||
assert.throws(() => normalizeFixedCustomerId('TENinvalid'))
|
||||
})
|
||||
|
||||
test('fixed customer preparation only authenticates and reads existing customer', async () => {
|
||||
const context = fixedContext()
|
||||
await run(context, { publishEnvironment: false, requireDistinctCustomer: false })
|
||||
assert.equal(context.context.merchantQualificationFixture.reused, true)
|
||||
assert.equal(context.context.merchantQualificationFixture.tenantId, '2100000000000000001')
|
||||
assert.equal(context.context.customerId, '2100000000000000001')
|
||||
assert.deepEqual(context.calls.filter(call => call.method !== 'GET').map(call => call.path), ['/api/auth/login', '/api/auth/logout'])
|
||||
assert.equal(context.createdResources.merchantQualificationFixture.password, undefined)
|
||||
})
|
||||
|
||||
test('wrong customer identity stops preparation and logs out', async () => {
|
||||
const context = fixedContext('2100000000000000002')
|
||||
await assert.rejects(run(context, { publishEnvironment: false, requireDistinctCustomer: false }), /归属或状态不匹配/)
|
||||
assert.equal(context.context.merchantQualificationFixture, undefined)
|
||||
assert.equal(context.calls.at(-1).path, '/api/auth/logout')
|
||||
assert.equal(context.calls.length, 3)
|
||||
})
|
||||
|
||||
test('second customer fixture cannot silently reuse the first customer', async () => {
|
||||
const context = fixedContext()
|
||||
await assert.rejects(run(context, { publishEnvironment: false }), /跨客户场景阻断/)
|
||||
assert.equal(context.calls.length, 0)
|
||||
})
|
||||
|
||||
test('TEST fixture without fixed customer fails before any request', async () => {
|
||||
const context = fixedContext()
|
||||
context.options = { environment: 'test' }
|
||||
await assert.rejects(run(context), /必须配置固定客户/)
|
||||
assert.equal(context.calls.length, 0)
|
||||
})
|
||||
|
||||
test('cross-customer financial helpers stop before provisioning or writing', async () => {
|
||||
for (const [file, name, end] of [
|
||||
['finance-direct-paid-isolation.mjs', 'createSecondCustomer', 'async function verifyCustomerIsolation'],
|
||||
['firstleg-container-fee-allocation.mjs', 'provisionCrossCustomerPlan', 'export async function run'],
|
||||
]) {
|
||||
const source = readFileSync(new URL(file, import.meta.url), 'utf8')
|
||||
const helper = source.slice(source.indexOf(`async function ${name}(`), source.indexOf(end))
|
||||
const scope = vm.createContext({})
|
||||
vm.runInContext(helper.replaceAll('import.meta.url', "'file:///unused-test-module.mjs'"), scope)
|
||||
await assert.rejects(scope[name]({ options: { fixedCustomerId: '123' } }), /缺少第二个固定客户/)
|
||||
}
|
||||
})
|
||||
|
||||
test('fixed resource runner excludes lifecycle handlers without marking them passed', async () => {
|
||||
const runner = new AcceptanceRun({ environment: 'test', fixedCustomerId: '123', quiet: true })
|
||||
let calls = 0
|
||||
for (const name of ['customers', 'supplemental-boundaries', 'system', 'master-data', 'suppliers', 'notifications', 'pricing',
|
||||
'phase4-overseas-master-data', 'phase4-overseas-wallet-and-pallet-cost']) {
|
||||
await runner.executeModule(name, async () => { calls++ })
|
||||
}
|
||||
assert.equal(calls, 0)
|
||||
assert.equal(runner.modules.length, 9)
|
||||
assert.ok(runner.modules.every(module => module.skipped && !module.success))
|
||||
})
|
||||
|
||||
test('both request entrypoints reject customer creation before network access', async () => {
|
||||
const runner = new AcceptanceRun({ environment: 'test' })
|
||||
for (const method of ['request', 'requestWithBody']) {
|
||||
for (const path of ['/api/platform/v1/iam/tenants', '/api/platform/v1/iam/tenants/123/archive', '/api/platform/v1/iam/tenants/123/submit-review']) {
|
||||
await assert.rejects(runner[method]('POST', path, {}), /禁止创建、归档客户/)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const acceptance = readFileSync(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8')
|
||||
const fixture = readFileSync(new URL('./merchant-qualification-fixture.mjs', import.meta.url), 'utf8')
|
||||
|
||||
@@ -38,7 +38,46 @@ async function waitForDimensionInitialization(context, tenantId) {
|
||||
if (status !== 'READY') throw new Error(`客户默认尺寸初始化未就绪: ${status}`)
|
||||
}
|
||||
|
||||
export async function run(context, { publishEnvironment = true } = {}) {
|
||||
export function normalizeFixedCustomerId(value) {
|
||||
const text = String(value || '').trim().replace(/^TEN/, '')
|
||||
if (!/^[1-9]\d{0,19}$/.test(text)) throw new Error('固定客户编号必须为 TEN 编号或数字 ID')
|
||||
return text
|
||||
}
|
||||
|
||||
async function reuseFixedCustomer(context, publishEnvironment, requireDistinctCustomer) {
|
||||
if (requireDistinctCustomer) throw new Error('固定客户模式未配置第二个客户,跨客户场景阻断;禁止自动建客或用同一客户替代隔离验证')
|
||||
const tenantId = normalizeFixedCustomerId(context.options.fixedCustomerId)
|
||||
const username = context.options.merchantUsername || process.env.MERCHANT_TEST_USERNAME
|
||||
const password = context.options.merchantPassword || process.env.MERCHANT_TEST_PASSWORD
|
||||
if (!username || !password) throw new Error('请配置固定客户的 MERCHANT_TEST_USERNAME / MERCHANT_TEST_PASSWORD;不会新建客户或重置密码')
|
||||
const loginResponse = await context.request('POST', '/api/auth/login', {loginId:username,password}, '')
|
||||
const login = context.apiSuccess(MODULE, '登录已配置的固定客户账号', loginResponse)
|
||||
if (!login?.accessToken) throw new Error('固定客户账号登录失败')
|
||||
let profile
|
||||
try {
|
||||
const response = await context.request('GET', '/api/merchant/v1/iam/tenant/profile', undefined, login.accessToken)
|
||||
profile = context.apiSuccess(MODULE, '核对固定客户账号实际归属和状态', response)
|
||||
const valid = id(profile?.id) === tenantId && profile?.status === 'ACTIVE'
|
||||
context.assert(MODULE, '固定客户账号属于指定已审核客户', response, valid,
|
||||
`expected=${tenantId}, actual=${id(profile?.id)}, status=${profile?.status}`, {deploymentBlocker:true})
|
||||
if (!valid) throw new Error('固定客户账号归属或状态不匹配,停止造数;不会新建客户')
|
||||
await waitForDimensionInitialization(context, tenantId)
|
||||
} finally {
|
||||
await context.request('POST', '/api/auth/logout', {refreshToken:login.refreshToken}, login.accessToken)
|
||||
}
|
||||
if (publishEnvironment) {
|
||||
process.env.MERCHANT_TEST_USERNAME = username
|
||||
process.env.MERCHANT_TEST_PASSWORD = password
|
||||
}
|
||||
const fixture = {tenantId,tenantName:profile.tenantName,username,marker:context.marker,reused:true}
|
||||
context.context.merchantQualificationFixture = {...fixture,password}
|
||||
context.context.customerId = tenantId
|
||||
context.createdResources.merchantQualificationFixture = fixture
|
||||
}
|
||||
|
||||
export async function run(context, { publishEnvironment = true, requireDistinctCustomer = !publishEnvironment } = {}) {
|
||||
if (context.options?.fixedCustomerId) return reuseFixedCustomer(context, publishEnvironment, requireDistinctCustomer)
|
||||
if (context.options?.environment === 'test') throw new Error('TEST 必须配置固定客户,禁止自动建客')
|
||||
const suffix = context.marker.replace(/[^0-9A-Z]/gi, '').slice(-10).padStart(10, '0')
|
||||
const customerName = String(context.options?.customerName || '').trim()
|
||||
const tenantName = customerName || `全量验收下单客户-${suffix}`
|
||||
|
||||
@@ -140,7 +140,7 @@ export async function createCompletedOrder(context, runFulfillment = async scope
|
||||
return run(scope)
|
||||
}, prepareCustomer = async scope => {
|
||||
const { run } = await import('./merchant-qualification-fixture.mjs')
|
||||
return run(scope, { publishEnvironment: false })
|
||||
return run(scope, { publishEnvironment: false, requireDistinctCustomer: false })
|
||||
}) {
|
||||
const scope = Object.create(context)
|
||||
scope.marker = `${context.marker}-COMPLETED`
|
||||
|
||||
@@ -75,6 +75,16 @@ async function createFixtureCustomer(context) {
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
if (context.options?.fixedCustomerId) {
|
||||
const tenantId = context.context.merchantQualificationFixture?.tenantId
|
||||
if (!tenantId) throw new Error('固定客户前置未完成,禁止创建海外仓费用客户')
|
||||
const response = await context.request('GET', `/api/platform/v1/iam/tenants/${encodeURIComponent(tenantId)}/overseas-storage-fees`)
|
||||
const view = context.apiSuccess(MODULE, '读取固定客户海外仓费用配置', response)
|
||||
context.assert(MODULE, '费用配置属于固定客户', response, id(view?.customerId) === id(tenantId), `customerId=${id(view?.customerId)}`, OPTIONS)
|
||||
context.addCoverageGap(MODULE, SCENARIO, '固定客户模式排除费用规则破坏性生命周期',
|
||||
'只读现有费用配置,不覆盖共享客户费率或归档客户', 'not-implemented', {blocking:false})
|
||||
return
|
||||
}
|
||||
const start = context.steps.length
|
||||
// Always create a module-owned customer. The customers module may archive its
|
||||
// own fixture, and this lifecycle must never depend on that or on history.
|
||||
|
||||
@@ -260,6 +260,7 @@ export async function withPlatformBookingScope(context, runLoading) {
|
||||
}
|
||||
|
||||
export async function prepare(context) {
|
||||
if (context.options?.fixedCustomerId) throw new Error('混合报关柜需要第二个固定客户,停止订单及装柜造数')
|
||||
const outerMarker = context.marker
|
||||
const isolatedResourceKeys = ['phase3Loading', 'phase3LoadingCompletedProducer', 'orchestrationFixtures']
|
||||
const isolatedResourceSnapshot = new Map(isolatedResourceKeys.map((name) => [name, {
|
||||
|
||||
+13
-2
@@ -3,10 +3,21 @@ import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { run } from './phase4-overseas-finance-cross-region.mjs'
|
||||
import { run, selectCurrentInboundPlan } from './phase4-overseas-finance-cross-region.mjs'
|
||||
|
||||
const source = readFileSync(fileURLToPath(new URL('./phase4-overseas-finance-cross-region.mjs', import.meta.url)), 'utf8')
|
||||
|
||||
test('inbound fees cannot select historical plans or another customer under a fixed account', () => {
|
||||
const current = { planId: '4001', planNo: 'CURRENT', customerId: '2001' }
|
||||
const old = { planId: '3999', planNo: 'OLD', customerId: '2001' }
|
||||
const pda = { marker: 'RUN', customerId: '2001', generatedPlanIds: ['4001'] }
|
||||
assert.equal(selectCurrentInboundPlan([old, current], pda, 'RUN'), current)
|
||||
assert.equal(selectCurrentInboundPlan([old], pda, 'RUN'), null)
|
||||
assert.equal(selectCurrentInboundPlan([current], pda, 'OTHER'), null)
|
||||
assert.equal(selectCurrentInboundPlan([{ ...current, customerId: '2002' }], pda, 'RUN'), null)
|
||||
assert.equal(selectCurrentInboundPlan([current], { ...pda, generatedPlanIds: [] }, 'RUN'), null)
|
||||
})
|
||||
|
||||
function response(data, code = '200', http = 200, region) {
|
||||
return {
|
||||
http,
|
||||
@@ -40,7 +51,7 @@ function fixture() {
|
||||
truck: { orderNo: 'TK-1', customerId: '1201' },
|
||||
looseboard: { orderNo: 'LB-1', customerId: '1301' },
|
||||
},
|
||||
phase4OverseasPda: { customerId: '2001', overseasWarehouseId: '9001' },
|
||||
phase4OverseasPda: { marker, generatedPlanIds: ['4001'], customerId: '2001', overseasWarehouseId: '9001' },
|
||||
},
|
||||
async request(...args) {
|
||||
const route = /\/(?:last-leg|overseas-warehouse-resources)(?:\/|\?|$)/.test(String(args[1]));
|
||||
|
||||
@@ -56,6 +56,13 @@ async function resolveListedBusiness(context, base, resource, customerId, label)
|
||||
return longId(row?.id) ? { businessId: id(row.id), businessNo: row.orderNo, customerId } : null
|
||||
}
|
||||
|
||||
export function selectCurrentInboundPlan(plans, pda, marker) {
|
||||
if (!pda || pda.marker !== marker || !longId(pda.customerId)) return null
|
||||
const ids = new Set(rows(pda.generatedPlanIds).map(id))
|
||||
return rows(plans).find(item => ids.has(id(item?.planId || item?.id))
|
||||
&& id(item?.customerId) === id(pda.customerId) && item?.planNo) || null
|
||||
}
|
||||
|
||||
async function resolveFixtures(context) {
|
||||
const business = context.createdResources?.phase4OverseasBusiness
|
||||
const merchantCustomerId = id(context.createdResources?.merchantQualificationFixture?.tenantId
|
||||
@@ -76,7 +83,7 @@ async function resolveFixtures(context) {
|
||||
if (longId(inboundCustomerId)) {
|
||||
const listed = await overseas(context, 'GET', '查询本轮客户海外入仓计划',
|
||||
`/api/platform/v1/last-leg/inbound-plans?page=1&size=200&customerId=${encodeURIComponent(inboundCustomerId)}`)
|
||||
const plan = rows(listed.data).find((item) => longId(item?.planId || item?.id) && item?.planNo)
|
||||
const plan = selectCurrentInboundPlan(listed.data, pda, context.marker)
|
||||
context.assert(MODULE, '海外入仓费用关联真实计划', listed.response,
|
||||
Boolean(plan) && longId(plan?.planId || plan?.id),
|
||||
`planId=${id(plan?.planId || plan?.id) || 'missing'}, planNo=${plan?.planNo || 'missing'}; ${trace(listed.response)}`, OPTIONS)
|
||||
|
||||
@@ -161,5 +161,23 @@ test('合同覆盖库存CSV并明确排除暂缓、离仓、PDA与内部路径',
|
||||
assert.doesNotMatch(source, /创建本轮换标单|verifyRelabelLifecycle/)
|
||||
assert.match(source, /创建本轮公共库位/)
|
||||
assert.match(source, /公共托盘旧版本编辑返回冲突/)
|
||||
assert.doesNotMatch(source, /deploymentBlocker|addCoverageGap|coverageGap/)
|
||||
assert.doesNotMatch(source, /deploymentBlocker/)
|
||||
assert.match(source, /固定资源模式缺少海外仓,禁止自动创建仓库/)
|
||||
})
|
||||
|
||||
test('fixed customer mode reuses warehouse without location or pallet lifecycle writes', async () => {
|
||||
const context = contextFixture()
|
||||
context.options = { fixedCustomerId: '10001' }
|
||||
context.context.merchantQualificationFixture = { tenantId: '10001' }
|
||||
await run(context)
|
||||
assert.equal(context.context.phase4OverseasWarehouse.customerId, '10001')
|
||||
assert.equal(context.context.phase4OverseasWarehouse.warehouseId, '20001')
|
||||
assert.ok(context.calls.filter(call => call.method !== 'GET')
|
||||
.every(call => call.method === 'POST' && call.path === '/api/platform/v1/files/export-tasks'))
|
||||
assert.equal(context.gaps.length, 1)
|
||||
const missing = contextFixture({ warehouseRecords: [] })
|
||||
missing.options = context.options
|
||||
missing.context.merchantQualificationFixture = { tenantId: '10001' }
|
||||
await assert.rejects(run(missing), /禁止自动创建仓库/)
|
||||
assert.equal(missing.calls.some(call => call.method === 'POST' && call.path === '/api/platform/v1/warehouse-admin'), false)
|
||||
})
|
||||
|
||||
@@ -67,7 +67,10 @@ export async function run(context) {
|
||||
context.createdResources.phase4OverseasWarehouse = { marker: context.marker, customerId: null, warehouseId: null, palletId: null, terminalStatus: null }
|
||||
const tenantsResult = await domesticGet(context, '查询可用客户候选', '/api/platform/v1/iam/tenants?page=1&size=100&status=ACTIVE')
|
||||
assertPage(context, '可用客户候选', tenantsResult)
|
||||
const customer = records(tenantsResult.data).find((item) => longId(item?.id) && item?.status === 'ACTIVE')
|
||||
const fixedCustomer = context.context?.merchantQualificationFixture
|
||||
const customer = context.options?.fixedCustomerId
|
||||
? { id: fixedCustomer?.tenantId, status: fixedCustomer?.tenantId ? 'ACTIVE' : null }
|
||||
: records(tenantsResult.data).find((item) => longId(item?.id) && item?.status === 'ACTIVE')
|
||||
const customerId = id(customer?.id)
|
||||
if (!customerId) {
|
||||
context.assert(MODULE, '海外仓验收必须取得ACTIVE客户', tenantsResult.response, false,
|
||||
@@ -151,6 +154,7 @@ export async function run(context) {
|
||||
let warehouse = await findWarehouse(context, records(tenantsResult.data), customerId)
|
||||
let ownedWarehouse = false
|
||||
if (!warehouse) {
|
||||
if (context.options?.fixedCustomerId) throw new Error('固定资源模式缺少海外仓,禁止自动创建仓库')
|
||||
warehouse = await createWarehouseFixture(context, customerId)
|
||||
ownedWarehouse = Boolean(warehouse)
|
||||
}
|
||||
@@ -185,6 +189,12 @@ export async function run(context) {
|
||||
`listId=${id(pallet.id)}, detailId=${id(detail.data?.id) || 'missing'}; ${trace(detail.response)}`, OPTIONS)
|
||||
}
|
||||
|
||||
if (context.options?.fixedCustomerId) {
|
||||
context.context.phase4OverseasWarehouse = { customerId, warehouseId, palletId: null }
|
||||
context.addCoverageGap(MODULE, 'resource-lifecycle', '复用海外仓资源,仅核验列表和详情',
|
||||
'不创建或修改公共库位及托盘;业务分配由后续实际库存流程决定', 'not-implemented', { blocking: false })
|
||||
return
|
||||
}
|
||||
try {
|
||||
await verifyLocationLifecycle(context, warehouseId)
|
||||
await verifyPalletLifecycle(context, warehouseId)
|
||||
|
||||
Reference in New Issue
Block a user