test: strengthen customs and fulfillment acceptance boundaries
This commit is contained in:
@@ -50,7 +50,8 @@ async function loginMerchant(context) {
|
||||
return token
|
||||
}
|
||||
|
||||
function routes(context, merchantToken) {
|
||||
function routes(context, merchantToken, customerTokens = new Map()) {
|
||||
const tokenFor = (customer) => customerTokens.get(id(customer.customerId)) || merchantToken
|
||||
const binary = (method, path, token = context.token) => raw(context, method, path, undefined, token)
|
||||
const multipart = (path, fields, file, token, idempotencyKey) => {
|
||||
const form = new FormData()
|
||||
@@ -64,27 +65,27 @@ function routes(context, merchantToken) {
|
||||
return {
|
||||
version: 1,
|
||||
platformDetail: (ctx, value) => ctx.request('GET', `${PLATFORM}/${encodeURIComponent(id(value.loadingPlanId))}`),
|
||||
merchantDetail: (ctx, fixture, customer, token = merchantToken) => ctx.request('GET',
|
||||
merchantDetail: (ctx, fixture, customer, token = tokenFor(customer)) => ctx.request('GET',
|
||||
`${MERCHANT}/orders/${encodeURIComponent(id(customer.orderId))}`, undefined, token),
|
||||
merchantPlatformDetail: (ctx, fixture) => ctx.request('GET',
|
||||
`${PLATFORM}/${encodeURIComponent(id(fixture.loadingPlanId))}`, undefined, merchantToken),
|
||||
merchantUploadDocument: (ctx, fixture, customer, file, idempotencyKey) => multipart(
|
||||
`${MERCHANT}/loading-plans/${encodeURIComponent(id(fixture.loadingPlanId))}/documents/upload?${customer.inboundIds.map((value) => `inboundIds=${encodeURIComponent(id(value))}`).join('&')}`,
|
||||
{}, file, customer.customerToken || merchantToken, idempotencyKey),
|
||||
{}, file, tokenFor(customer), idempotencyKey),
|
||||
merchantReplaceDocument: (ctx, fixture, customer, file, idempotencyKey) => multipart(
|
||||
`${MERCHANT}/documents/${encodeURIComponent(id(file.documentId))}/upload?expectedVersion=${Number(file.expectedVersion)}&${customer.inboundIds.map((value) => `inboundIds=${encodeURIComponent(id(value))}`).join('&')}`,
|
||||
{}, file, customer.customerToken || merchantToken, idempotencyKey),
|
||||
{}, file, tokenFor(customer), idempotencyKey),
|
||||
merchantUpdateScope: (ctx, fixture, customer, request, idempotencyKey) => ctx.request('PUT',
|
||||
`${MERCHANT}/documents/${encodeURIComponent(id(request.documentId))}/inbounds`, {
|
||||
inboundIds: request.inboundIds.map(id), expectedVersion: Number(request.expectedVersion),
|
||||
}, customer.customerToken || merchantToken, { 'Idempotency-Key': idempotencyKey }),
|
||||
}, tokenFor(customer), { 'Idempotency-Key': idempotencyKey }),
|
||||
merchantDeleteDocument: (ctx, fixture, customer, request, idempotencyKey) => ctx.request('DELETE',
|
||||
`${MERCHANT}/documents/${encodeURIComponent(id(request.documentId))}?expectedVersion=${Number(request.expectedVersion)}`,
|
||||
undefined, customer.customerToken || merchantToken, { 'Idempotency-Key': idempotencyKey }),
|
||||
undefined, tokenFor(customer), { 'Idempotency-Key': idempotencyKey }),
|
||||
merchantConfirmReceipt: (ctx, fixture, customer, request, idempotencyKey) => ctx.request('POST',
|
||||
`${MERCHANT}/documents/${encodeURIComponent(id(request.documentId))}/receipt/${request.confirm === false ? 'reject' : 'confirm'}`,
|
||||
{ expectedVersion: Number(request.expectedVersion), confirm: request.confirm !== false, remark: request.remark || '' },
|
||||
customer.customerToken || merchantToken, { 'Idempotency-Key': idempotencyKey }),
|
||||
tokenFor(customer), { 'Idempotency-Key': idempotencyKey }),
|
||||
platformStart: (ctx, fixture, request, idempotencyKey) => ctx.request('POST',
|
||||
`${PLATFORM}/${encodeURIComponent(id(fixture.loadingPlanId))}/start`, request, ctx.token,
|
||||
{ 'Idempotency-Key': idempotencyKey }),
|
||||
@@ -97,6 +98,22 @@ function routes(context, merchantToken) {
|
||||
platformSubmitToBroker: (ctx, fixture, request, idempotencyKey) => ctx.request('POST',
|
||||
`${PLATFORM}/${encodeURIComponent(id(fixture.loadingPlanId))}/submit-to-broker`, request, ctx.token,
|
||||
{ 'Idempotency-Key': idempotencyKey }),
|
||||
platformDeclarationBatches: (ctx, fixture) => ctx.request('GET',
|
||||
`${PLATFORM}/${encodeURIComponent(id(fixture.loadingPlanId))}/declaration-batches`),
|
||||
platformCreateDeclaration: (ctx, fixture, request, idempotencyKey) => ctx.request('POST',
|
||||
`${PLATFORM}/${encodeURIComponent(id(fixture.loadingPlanId))}/declaration-batches`, request, ctx.token,
|
||||
{ 'Idempotency-Key': idempotencyKey }),
|
||||
platformSubmitDeclaration: (ctx, fixture, batchId, request, idempotencyKey) => ctx.request('POST',
|
||||
`${PLATFORM}/declaration-batches/${encodeURIComponent(id(batchId))}/submit`, request, ctx.token,
|
||||
{ 'Idempotency-Key': idempotencyKey }),
|
||||
platformClearanceResults: (ctx, fixture, batchId) => ctx.request('GET',
|
||||
`${PLATFORM}/declaration-batches/${encodeURIComponent(id(batchId))}/results`),
|
||||
platformRecordClearance: (ctx, fixture, batchId, request, idempotencyKey) => ctx.request('POST',
|
||||
`${PLATFORM}/declaration-batches/${encodeURIComponent(id(batchId))}/results`, request, ctx.token,
|
||||
{ 'Idempotency-Key': idempotencyKey }),
|
||||
platformResolveClearance: (ctx, fixture, resultId, request, idempotencyKey) => ctx.request('POST',
|
||||
`${PLATFORM}/clearance-results/${encodeURIComponent(id(resultId))}/resolve`, request, ctx.token,
|
||||
{ 'Idempotency-Key': idempotencyKey }),
|
||||
platformComplete: (ctx, fixture, request, idempotencyKey) => ctx.request('POST',
|
||||
`${PLATFORM}/${encodeURIComponent(id(fixture.loadingPlanId))}/complete-all-buy`, request, ctx.token,
|
||||
{ 'Idempotency-Key': idempotencyKey }),
|
||||
@@ -109,10 +126,10 @@ function routes(context, merchantToken) {
|
||||
`${PLATFORM}/documents/${encodeURIComponent(id(request.documentId))}/final/download`),
|
||||
merchantDownloadReceipt: (ctx, fixture, customer, request) => binary('GET',
|
||||
`${MERCHANT}/documents/${encodeURIComponent(id(request.documentId))}/receipt/download`,
|
||||
customer.customerToken || merchantToken),
|
||||
tokenFor(customer)),
|
||||
merchantDownloadFinal: (ctx, fixture, customer, request) => binary('GET',
|
||||
`${MERCHANT}/documents/${encodeURIComponent(id(request.documentId))}/final/download`,
|
||||
customer.customerToken || merchantToken),
|
||||
tokenFor(customer)),
|
||||
buyDeclarationXlsx: async (ctx, request) => {
|
||||
const created = await ctx.request('POST', '/api/platform/v1/files/export-tasks', {
|
||||
exportType: 'LOADING_LIST_BUY_DECLARATION_ITEMS', format: 'XLSX',
|
||||
@@ -150,6 +167,98 @@ function routes(context, merchantToken) {
|
||||
}
|
||||
}
|
||||
|
||||
async function addSecondRefundCustomer(context) {
|
||||
const source = context.createdResources.phase3Loading
|
||||
const secondary = Object.create(context)
|
||||
secondary.marker = `${context.marker}-CUSTOMER-B`
|
||||
secondary.options = { ...context.options, customerName: '' }
|
||||
secondary.context = { ...context.context, orderProfile: {
|
||||
...(context.context.orderProfile || {}), taxMode: 'TAX_REFUND', fulfillmentType: 'STANDARD',
|
||||
}, orderFulfillmentType: 'STANDARD' }
|
||||
secondary.createdResources = {}
|
||||
const qualification = await import('./merchant-qualification-fixture.mjs')
|
||||
await qualification.run(secondary, { publishEnvironment: false })
|
||||
const account = secondary.context.merchantQualificationFixture
|
||||
// orders.mjs captures its credentials at import time. A distinct module instance captures
|
||||
// this customer's credentials without changing the primary customer's cached producer.
|
||||
const saved = ['MERCHANT_TEST_USERNAME', 'MERCHANT_TEST_PASSWORD'].map((name) =>
|
||||
[name, Object.hasOwn(process.env, name), process.env[name]])
|
||||
let orders
|
||||
try {
|
||||
process.env.MERCHANT_TEST_USERNAME = account.username
|
||||
process.env.MERCHANT_TEST_PASSWORD = account.password
|
||||
orders = await import(`./orders.mjs?customsCustomer=${encodeURIComponent(account.tenantId)}`)
|
||||
} finally {
|
||||
for (const [name, present, value] of saved) {
|
||||
if (present) process.env[name] = value
|
||||
else delete process.env[name]
|
||||
}
|
||||
}
|
||||
await secondary.executeModule(`${MODULE}-customer-b-orders`, () => orders.run(secondary))
|
||||
const measurement = await import('./inbound-measurement.mjs')
|
||||
await secondary.executeModule(`${MODULE}-customer-b-measurement`, () => measurement.run(secondary))
|
||||
const inbound = secondary.createdResources.inboundMeasurement
|
||||
if (!inbound?.inboundId || id(inbound.tenantId) !== id(account.tenantId)
|
||||
|| id(inbound.tenantId) === id(source.tenantId)) throw new Error('第二退税客户未生成独立入库单')
|
||||
const login = await secondary.request('POST', '/api/auth/login', {
|
||||
loginId: account.username, password: account.password,
|
||||
}, '')
|
||||
if (!success(login) || !login.body?.data?.accessToken) throw new Error('第二退税客户登录失败')
|
||||
const smart = '/api/platform/v1/first-leg/smart-loading'
|
||||
const candidatesResponse = await context.request('GET', `${smart}/candidates?${new URLSearchParams({
|
||||
bookingId: id(source.containerBookingId), warehouseId: id(source.warehouseId),
|
||||
})}`)
|
||||
const candidates = context.apiSuccess(MODULE, '第二退税客户读取原柜正式可装候选', candidatesResponse, '', OPTIONS)
|
||||
const candidate = (Array.isArray(candidates) ? candidates : candidates?.records || []).find((item) =>
|
||||
id(item.inboundId) === id(inbound.inboundId) && id(item.accountId) === id(account.tenantId)
|
||||
&& item.inboundDetailId && Number(item.availablePackageCount) > 0)
|
||||
if (!candidate) throw new Error('第二退税客户在原柜没有正式可装候选,禁止借历史票')
|
||||
const planResponse = await context.request('GET', `${smart}/plans/${id(source.loadingPlanId)}`)
|
||||
const plan = context.apiSuccess(MODULE, '加入第二客户前回查原柜计划', planResponse, '', OPTIONS)
|
||||
const assignedResponse = await context.request('POST', `${smart}/plans/${id(source.loadingPlanId)}/assignments`, {
|
||||
inboundId: id(inbound.inboundId), inboundDetailId: id(candidate.inboundDetailId), packageCount: 1,
|
||||
expectedVersion: Number(plan.version), operationId: key(context, 'customer-b-assign'),
|
||||
})
|
||||
const assigned = context.apiSuccess(MODULE, '第二退税客户正式分配到同柜', assignedResponse, '', OPTIONS)
|
||||
const item = assigned?.items?.find((value) => id(value.inboundId) === id(inbound.inboundId))
|
||||
const taskPath = `/api/platform/v1/first-leg/pda-loading/tasks/${id(source.canonicalPdaTaskId)}`
|
||||
const taskResponse = await context.request('GET', `${taskPath}?warehouseId=${id(source.warehouseId)}`)
|
||||
const task = context.apiSuccess(MODULE, '同柜新增第二客户后回查原PDA任务', taskResponse, '', OPTIONS)
|
||||
const taskItem = task?.items?.find((value) => id(value.loadingPlanItemId) === id(item?.id))
|
||||
const inventoryResponse = await context.request('GET', `${taskPath}/inventory-snapshot?warehouseId=${id(source.warehouseId)}`)
|
||||
const inventory = context.apiSuccess(MODULE, '第二客户刷新正式拣货库存快照', inventoryResponse, '', OPTIONS)
|
||||
if (!taskItem || !inventory?.snapshotToken) throw new Error('第二客户缺少正式PDA明细或库存快照')
|
||||
const pickedResponse = await context.request('POST', `${taskPath}/batch`, {
|
||||
warehouseId: id(source.warehouseId), loadingPlanId: id(source.loadingPlanId),
|
||||
containerBookingId: id(source.containerBookingId), expectedVersion: Number(task.version),
|
||||
inventorySnapshotToken: inventory.snapshotToken,
|
||||
items: [{ itemId: id(taskItem.id), action: 'PICK', pickedPackageCount: 1, version: Number(taskItem.version) }],
|
||||
}, context.pdaToken || context.token, { 'Idempotency-Key': key(context, 'customer-b-pick') })
|
||||
const picked = context.apiSuccess(MODULE, '同柜第二客户完成正式PDA拣货', pickedResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '第二客户加入后原柜原任务保持且恢复待完成', pickedResponse,
|
||||
id(picked?.id) === id(source.canonicalPdaTaskId) && picked?.workflowStatus === 'AWAITING_REVIEW'
|
||||
&& id(picked?.loadingPlanId) === id(source.loadingPlanId)
|
||||
&& id(picked?.containerBookingId) === id(source.containerBookingId),
|
||||
`workflow=${picked?.workflowStatus || 'missing'}`, OPTIONS)
|
||||
source.pdaTaskVersion = Number(picked?.version)
|
||||
const { stableLoadedCartonMarks } = await import('./phase3-loading.mjs')
|
||||
source.cartonMarks = stableLoadedCartonMarks(picked?.items)
|
||||
return { customerId: id(account.tenantId), sessionToken: login.body.data.accessToken }
|
||||
}
|
||||
|
||||
export async function withPlatformBookingScope(context, runLoading) {
|
||||
const name = 'phase3LoadingBookingScope'
|
||||
const present = Object.hasOwn(context.context, name)
|
||||
const previous = context.context[name]
|
||||
context.context[name] = 'PLATFORM'
|
||||
try {
|
||||
return await runLoading()
|
||||
} finally {
|
||||
if (present) context.context[name] = previous
|
||||
else delete context.context[name]
|
||||
}
|
||||
}
|
||||
|
||||
export async function prepare(context) {
|
||||
const outerMarker = context.marker
|
||||
const isolatedResourceKeys = ['phase3Loading', 'phase3LoadingCompletedProducer', 'orchestrationFixtures']
|
||||
@@ -165,6 +274,7 @@ export async function prepare(context) {
|
||||
const restore = await context.preparePhase3LoadingFixture('-CUSTOMS-MIXED', false, {
|
||||
primary: 'BUY_DECLARATION', incremental: 'TAX_REFUND',
|
||||
})
|
||||
try {
|
||||
const executionMarker = `${outerMarker}-CUSTOMS-MIXED`
|
||||
context.marker = executionMarker
|
||||
if (context.createdResources?.orchestrationFixtures?.phase3Loading) {
|
||||
@@ -174,7 +284,9 @@ export async function prepare(context) {
|
||||
context.createdResources.orchestrationFixtures.phase3LoadingIncremental.marker = executionMarker
|
||||
}
|
||||
const loading = await import('./phase3-loading.mjs')
|
||||
await context.executeModule(`${MODULE}-loading`, () => loading.run(context))
|
||||
await withPlatformBookingScope(context, () =>
|
||||
context.executeModule(`${MODULE}-loading`, () => loading.run(context)))
|
||||
const secondCustomer = await addSecondRefundCustomer(context)
|
||||
const finalize = await import('./phase3-loading-finalize.mjs')
|
||||
await context.executeModule(`${MODULE}-finalize`, () => finalize.run(context))
|
||||
|
||||
@@ -202,10 +314,11 @@ export async function prepare(context) {
|
||||
throw new Error(`正式装柜API未形成混合柜: buy=${buy.length}, tax=${refund.length}`)
|
||||
}
|
||||
const merchantToken = await loginMerchant(context)
|
||||
const customerId = id(refund[0].tenantId)
|
||||
const customer = {
|
||||
customerId, orderId: id(refund[0].orderId),
|
||||
inboundIds: refund.map((item) => id(item.inboundId)),
|
||||
const customers = [...new Set(refund.map((item) => id(item.tenantId)))].map((customerId) => {
|
||||
const own = refund.filter((item) => id(item.tenantId) === customerId)
|
||||
return {
|
||||
customerId, orderId: id(own[0].orderId),
|
||||
inboundIds: own.map((item) => id(item.inboundId)),
|
||||
document: {
|
||||
fileName: `${context.marker}-tax-refund.customblob`, contentType: 'application/octet-stream',
|
||||
content: Uint8Array.from([0x54, 0x41, 0x58]),
|
||||
@@ -214,8 +327,9 @@ export async function prepare(context) {
|
||||
fileName: `${context.marker}-tax-refund-v2.customblob`, contentType: 'application/octet-stream',
|
||||
content: Uint8Array.from([0x54, 0x41, 0x58, 0x32]),
|
||||
},
|
||||
updatedInboundIds: refund.map((item) => id(item.inboundId)),
|
||||
}
|
||||
updatedInboundIds: own.map((item) => id(item.inboundId)),
|
||||
} })
|
||||
if (customers.length < 2) throw new Error('同柜未形成两个实际退税客户')
|
||||
const buyInboundIds = new Set(buy.map((item) => id(item.inboundId)))
|
||||
const planBuyRows = planItems.filter((item) => buyInboundIds.has(id(item.inboundId))).length
|
||||
const expectedBuyRows = planBuyRows > 0 ? planBuyRows : buy.length
|
||||
@@ -230,14 +344,21 @@ export async function prepare(context) {
|
||||
inboundId: id(item.inboundId), orderId: id(item.orderId), customerId: id(item.tenantId),
|
||||
inboundCode: item.inboundCode, taxMode: item.taxMode,
|
||||
})),
|
||||
customers: [customer], expectedBuyRows, buyInboundIds: [...buyInboundIds], customsStatus: customs.status,
|
||||
customers, expectedBuyRows, buyInboundIds: [...buyInboundIds], customsStatus: customs.status,
|
||||
}],
|
||||
}
|
||||
context.context.phase3ContainerCustomsRoutes = routes(context, merchantToken)
|
||||
context.context.phase3ContainerCustomsRoutes = routes(context, merchantToken, new Map([[secondCustomer.customerId, secondCustomer.sessionToken]]))
|
||||
return () => {
|
||||
delete context.context.phase3ContainerCustomsRoutes
|
||||
context.marker = outerMarker
|
||||
restore()
|
||||
restoreIsolatedResources()
|
||||
}
|
||||
} catch (error) {
|
||||
delete context.context.phase3ContainerCustomsRoutes
|
||||
context.marker = outerMarker
|
||||
restore()
|
||||
restoreIsolatedResources()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user