test(acceptance): align retained state fixtures
This commit is contained in:
@@ -37,7 +37,7 @@ test('retains all clickable container, loading-list and PDA workflow states thro
|
|||||||
assert.doesNotMatch(source, /\/preload\/confirming|\/preload\/confirm\?tenantId=/)
|
assert.doesNotMatch(source, /\/preload\/confirming|\/preload\/confirm\?tenantId=/)
|
||||||
assert.match(source, /linked\?\.cabinetNo !== simulation\.cabinetNo/)
|
assert.match(source, /linked\?\.cabinetNo !== simulation\.cabinetNo/)
|
||||||
assert.match(source, /iso6346ContainerNumber/)
|
assert.match(source, /iso6346ContainerNumber/)
|
||||||
assert.match(source, /shippingCompanyId: id\(agent\.id\), shippingCompanyName: agent\.name/)
|
assert.match(source, /shippingCompanyId: id\(carrier\.id\), shippingCompanyName: carrier\.name/)
|
||||||
assert.match(source, /bookingAgentId: id\(agent\.id\), bookingAgentName: agent\.name/)
|
assert.match(source, /bookingAgentId: id\(agent\.id\), bookingAgentName: agent\.name/)
|
||||||
assert.match(source, /billOfLadingNo: `BL-\$\{value\}`/)
|
assert.match(source, /billOfLadingNo: `BL-\$\{value\}`/)
|
||||||
const batch = source.slice(source.indexOf('async function pickAll'), source.indexOf('async function complete'))
|
const batch = source.slice(source.indexOf('async function pickAll'), source.indexOf('async function complete'))
|
||||||
@@ -57,7 +57,7 @@ test('fails closed when invoked outside the manual matrix wrapper', () => {
|
|||||||
assert.match(source, /missing=manualStateMatrix\.pages/)
|
assert.match(source, /missing=manualStateMatrix\.pages/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('covers simplified clearance and customs actions through public multipart upload and complete APIs', () => {
|
test('covers clearance through its public API and keeps customs on the dedicated customs-v1 flow', () => {
|
||||||
assert.match(source, /new FormData\(\)/)
|
assert.match(source, /new FormData\(\)/)
|
||||||
assert.match(source, /form\.append\('file'/)
|
assert.match(source, /form\.append\('file'/)
|
||||||
assert.match(source, /form\.append\('expectedVersion'/)
|
assert.match(source, /form\.append\('expectedVersion'/)
|
||||||
@@ -65,10 +65,9 @@ test('covers simplified clearance and customs actions through public multipart u
|
|||||||
assert.match(source, /'Idempotency-Key'/)
|
assert.match(source, /'Idempotency-Key'/)
|
||||||
assert.match(source, /clearancePath\}\/document\/upload/)
|
assert.match(source, /clearancePath\}\/document\/upload/)
|
||||||
assert.match(source, /clearancePath\}\/complete/)
|
assert.match(source, /clearancePath\}\/complete/)
|
||||||
assert.match(source, /customsPath\}\/document\/upload\?\$\{warehouse\}/)
|
|
||||||
assert.match(source, /customsPath\}\/complete\?\$\{warehouse\}/)
|
|
||||||
assert.match(source, /matrix\.pages\.containerLoading\.actions/)
|
assert.match(source, /matrix\.pages\.containerLoading\.actions/)
|
||||||
assert.match(source, /matrix\.pages\.loadingLists\.actions/)
|
assert.doesNotMatch(source, /CUSTOMS_DECLARATION|loading-customs|customsPath/)
|
||||||
|
assert.match(source, /tasks\/\$\{taskId\}\/evidence/)
|
||||||
assert.doesNotMatch(source, /internal\/file|business-files/)
|
assert.doesNotMatch(source, /internal\/file|business-files/)
|
||||||
assert.doesNotMatch(source, /\{\s*fileId\s*:/)
|
assert.doesNotMatch(source, /\{\s*fileId\s*:/)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -49,31 +49,31 @@ async function uploadDocument(context, path, expectedVersion, operationReason, l
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensureCompletionEvidence(context, chain, label) {
|
async function ensureCompletionEvidence(context, chain, label) {
|
||||||
const tenantId = chain.source.tenantId
|
const taskId = id(chain.task?.id)
|
||||||
const inboundId = chain.source.inboundId
|
const warehouseId = id(chain.source?.warehouseId)
|
||||||
const png = Uint8Array.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,6,0,0,0,31,21,196,137,0,0,0,13,73,68,65,84,8,215,99,248,207,192,240,31,0,5,0,1,255,137,153,61,29,0,0,0,0,73,69,78,68,174,66,96,130])
|
const png = Uint8Array.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,6,0,0,0,31,21,196,137,0,0,0,13,73,68,65,84,8,215,99,248,207,192,240,31,0,5,0,1,255,137,153,61,29,0,0,0,0,73,69,78,68,174,66,96,130])
|
||||||
for (const evidenceType of ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL']) {
|
for (const evidenceType of ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL']) {
|
||||||
const uploadPath = `/api/platform/v1/files/loading-evidence/${tenantId}/${inboundId}?evidenceType=${encodeURIComponent(evidenceType)}`
|
const query = new URLSearchParams({ warehouseId, expectedVersion: '0', evidenceType,
|
||||||
|
remark: context.marker })
|
||||||
|
if (evidenceType === 'SEAL') query.set('sealNo', `SEAL-${suffix(context, label)}`.slice(0, 64))
|
||||||
|
const uploadPath = `${PDA}/tasks/${taskId}/evidence?${query}`
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
form.append('file', new Blob([png], { type: 'image/png' }), `${label}-${evidenceType}.png`)
|
form.append('file', new Blob([png], { type: 'image/png' }), `${label}-${evidenceType}.png`)
|
||||||
const rawUpload = await fetch(`${context.options.gateway}${uploadPath}`, {
|
const rawUpload = await fetch(`${context.options.gateway}${uploadPath}`, {
|
||||||
method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}` },
|
method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}`,
|
||||||
|
'Idempotency-Key': operationId(context, `${label}-evidence-${evidenceType}`) },
|
||||||
body: form, signal: AbortSignal.timeout(60_000),
|
body: form, signal: AbortSignal.timeout(60_000),
|
||||||
})
|
})
|
||||||
const uploadBody = await rawUpload.json()
|
const uploadBody = await rawUpload.json()
|
||||||
const upload = { method: 'POST', path: uploadPath, http: rawUpload.status, body: uploadBody,
|
const upload = { method: 'POST', path: uploadPath, http: rawUpload.status, body: uploadBody,
|
||||||
requestId: uploadBody?.requestId || rawUpload.headers.get('x-request-id'),
|
requestId: uploadBody?.requestId || rawUpload.headers.get('x-request-id'),
|
||||||
traceId: uploadBody?.traceId || rawUpload.headers.get('x-trace-id') }
|
traceId: uploadBody?.traceId || rawUpload.headers.get('x-trace-id') }
|
||||||
const file = context.apiSuccess(MODULE, `${label} 上传 ${evidenceType} 装柜证据`, upload, '', OPTIONS)
|
const evidence = context.apiSuccess(MODULE, `${label} 上传并绑定 ${evidenceType} 装柜证据`, upload, '', OPTIONS)
|
||||||
const fileId = id(file?.id || file?.fileId)
|
const active = evidence?.evidences?.find?.((item) => item.evidenceType === evidenceType
|
||||||
if (!fileId) throw new Error(`${label} ${evidenceType} 未返回文件ID`)
|
&& item.status !== 'VOID')
|
||||||
const bind = await post(context,
|
if (!active?.evidenceId || id(evidence?.taskId) !== taskId) {
|
||||||
`/api/platform/v1/inbounds/${inboundId}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`, {
|
throw new Error(`${label} ${evidenceType} 未返回任务证据ID`)
|
||||||
expectedVersion: 0, evidenceType, fileId,
|
}
|
||||||
sealNo: evidenceType === 'SEAL' ? `SEAL-${suffix(context, label)}`.slice(0, 64) : null,
|
|
||||||
remark: context.marker,
|
|
||||||
}, `${label}-evidence-${evidenceType}`)
|
|
||||||
context.apiSuccess(MODULE, `${label} 绑定 ${evidenceType} 装柜证据`, bind, '', OPTIONS)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,11 +133,14 @@ async function createConfirmedContainer(context, source, label) {
|
|||||||
const agentPage = context.apiSuccess(MODULE, `${label} 读取 ACTIVE 订舱代理`, agentResponse, '', OPTIONS)
|
const agentPage = context.apiSuccess(MODULE, `${label} 读取 ACTIVE 订舱代理`, agentResponse, '', OPTIONS)
|
||||||
const agent = agentPage?.records?.find?.((item) => item?.id && item?.name)
|
const agent = agentPage?.records?.find?.((item) => item?.id && item?.name)
|
||||||
if (!agent) throw new Error(`${label} 未取得 ACTIVE 订舱代理`)
|
if (!agent) throw new Error(`${label} 未取得 ACTIVE 订舱代理`)
|
||||||
|
const { ensureMatsonCarrierPartner } = await import('./carrier-partner-fixture.mjs')
|
||||||
|
const carrier = await ensureMatsonCarrierPartner(context, MODULE, OPTIONS)
|
||||||
|
if (!carrier?.id) throw new Error(`${label} 未取得 ACTIVE 船公司`)
|
||||||
const draft = {
|
const draft = {
|
||||||
customerId: source.tenantId, customerName: summary.customerName || 'IT 测试客户',
|
customerId: source.tenantId, customerName: summary.customerName || 'IT 测试客户',
|
||||||
channelCode: summary.channelCode, warehouseId: source.warehouseId,
|
channelCode: summary.channelCode, warehouseId: source.warehouseId,
|
||||||
billOfLadingNo: `BL-${value}`.slice(0, 128),
|
billOfLadingNo: `BL-${value}`.slice(0, 128),
|
||||||
shippingCompanyId: id(agent.id), shippingCompanyName: agent.name, carrierCode: 'OTHER',
|
shippingCompanyId: id(carrier.id), shippingCompanyName: carrier.name, carrierCode: carrier.carrierCode,
|
||||||
bookingAgentId: id(agent.id), bookingAgentName: agent.name, departurePortCode: 'CNSZX',
|
bookingAgentId: id(agent.id), bookingAgentName: agent.name, departurePortCode: 'CNSZX',
|
||||||
vesselName: `人工船-${value}`.slice(0, 128), voyageNo: `V-${value}`.slice(0, 64),
|
vesselName: `人工船-${value}`.slice(0, 128), voyageNo: `V-${value}`.slice(0, 64),
|
||||||
departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
|
departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
|
||||||
@@ -161,8 +164,20 @@ async function createConfirmedContainer(context, source, label) {
|
|||||||
{ expectedVersion: 0, data: draft }, `${label}-booking-create`)
|
{ expectedVersion: 0, data: draft }, `${label}-booking-create`)
|
||||||
const created = context.apiSuccess(MODULE, `${label} 创建订舱`, createdResponse, '', OPTIONS)
|
const created = context.apiSuccess(MODULE, `${label} 创建订舱`, createdResponse, '', OPTIONS)
|
||||||
const bookingId = id(created?.oceanBooking?.id)
|
const bookingId = id(created?.oceanBooking?.id)
|
||||||
|
if (!bookingId) throw new Error(`${label} 创建订舱未返回订舱ID`)
|
||||||
|
const documentForm = new FormData()
|
||||||
|
documentForm.append('expectedVersion', String(Number(created?.oceanBooking?.version)))
|
||||||
|
documentForm.append('documentType', 'BOOKING_CONFIRMATION')
|
||||||
|
documentForm.append('file', new Blob([`${context.marker}-${label}-BOOKING_CONFIRMATION`],
|
||||||
|
{ type: 'text/plain' }), `booking-confirmation-${value}.txt`)
|
||||||
|
const documentResponse = await context.requestMultipart('POST',
|
||||||
|
`/api/platform/v1/ocean-bookings/${bookingId}/documents/upload`, documentForm, context.token,
|
||||||
|
{ 'Idempotency-Key': operationId(context, `${label}-booking-document`) })
|
||||||
|
const document = context.apiSuccess(MODULE, `${label} 上传订舱确认文件`, documentResponse, '', OPTIONS)
|
||||||
|
const releaseVersion = Number(document?.bookingVersion)
|
||||||
|
if (!Number.isInteger(releaseVersion)) throw new Error(`${label} 订舱确认文件未返回有效版本`)
|
||||||
const releasedResponse = await post(context, `/api/platform/v1/ocean-bookings/${bookingId}/release`, {
|
const releasedResponse = await post(context, `/api/platform/v1/ocean-bookings/${bookingId}/release`, {
|
||||||
expectedVersion: Number(created?.oceanBooking?.version), reason: context.marker,
|
expectedVersion: releaseVersion, reason: context.marker,
|
||||||
}, `${label}-booking-release`)
|
}, `${label}-booking-release`)
|
||||||
const confirmed = context.apiSuccess(MODULE, `${label} 放舱订舱`, releasedResponse, '', OPTIONS)
|
const confirmed = context.apiSuccess(MODULE, `${label} 放舱订舱`, releasedResponse, '', OPTIONS)
|
||||||
context.assert(MODULE, `${label} 订舱为 RELEASED 且不预生成真实柜`, releasedResponse,
|
context.assert(MODULE, `${label} 订舱为 RELEASED 且不预生成真实柜`, releasedResponse,
|
||||||
@@ -179,12 +194,13 @@ async function createConfirmedContainer(context, source, label) {
|
|||||||
throw new Error(`${label} 关联真实订舱后内部柜次身份漂移`)
|
throw new Error(`${label} 关联真实订舱后内部柜次身份漂移`)
|
||||||
}
|
}
|
||||||
context.apiSuccess(MODULE, `${label} 放舱后关联真实柜号`, linkedResponse, '', OPTIONS)
|
context.apiSuccess(MODULE, `${label} 放舱后关联真实柜号`, linkedResponse, '', OPTIONS)
|
||||||
return containerId
|
return { containerId, containerNo }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createPlanTask(context, label) {
|
async function createPlanTask(context, label) {
|
||||||
const source = await readInbound(context, await prepareInbound(context, label))
|
const source = await readInbound(context, await prepareInbound(context, label))
|
||||||
const containerId = await createConfirmedContainer(context, source, label)
|
const container = await createConfirmedContainer(context, source, label)
|
||||||
|
const { containerId, containerNo } = container
|
||||||
const candidateResponse = await context.request('GET',
|
const candidateResponse = await context.request('GET',
|
||||||
`${SMART}/candidates?bookingId=${containerId}&warehouseId=${source.warehouseId}`)
|
`${SMART}/candidates?bookingId=${containerId}&warehouseId=${source.warehouseId}`)
|
||||||
const candidates = context.apiSuccess(MODULE, `${label} 查询同渠道候选货物`, candidateResponse, '', OPTIONS)
|
const candidates = context.apiSuccess(MODULE, `${label} 查询同渠道候选货物`, candidateResponse, '', OPTIONS)
|
||||||
@@ -288,6 +304,15 @@ async function complete(context, chain, label) {
|
|||||||
remark: `${context.marker} 人工完成状态样本`,
|
remark: `${context.marker} 人工完成状态样本`,
|
||||||
})
|
})
|
||||||
chain.task = context.apiSuccess(MODULE, `${label} 人工确认装柜完成`, response, '', OPTIONS)
|
chain.task = context.apiSuccess(MODULE, `${label} 人工确认装柜完成`, response, '', OPTIONS)
|
||||||
|
for (let attempt = 0; attempt < 90 && chain.task?.taskStatus !== 'COMPLETED'
|
||||||
|
&& chain.task?.taskStatus !== 'COMPLETION_FAILED'; attempt += 1) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1_000))
|
||||||
|
const detailResponse = await context.request('GET',
|
||||||
|
`${PDA}/tasks/${chain.task.id}?warehouseId=${encodeURIComponent(chain.source.warehouseId)}`)
|
||||||
|
if (detailResponse.http === 200 && String(detailResponse?.body?.code) === '200') {
|
||||||
|
chain.task = detailResponse.body?.data
|
||||||
|
}
|
||||||
|
}
|
||||||
return chain
|
return chain
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,39 +376,11 @@ async function completeSimpleDocuments(context, chain, matrix) {
|
|||||||
&& Number(clearanceCompleted?.version) === Number(clearanceDocument?.version) + 1,
|
&& Number(clearanceCompleted?.version) === Number(clearanceDocument?.version) + 1,
|
||||||
`documentId=${id(clearanceCompleted?.id) || 'missing'}, completedAt=${clearanceCompleted?.completedAt || 'missing'}`, OPTIONS)
|
`documentId=${id(clearanceCompleted?.id) || 'missing'}, completedAt=${clearanceCompleted?.completedAt || 'missing'}`, OPTIONS)
|
||||||
|
|
||||||
const customsPath = `${LOADING}/loading-lists/${chain.plan.id}/customs`
|
|
||||||
const warehouse = `warehouseId=${encodeURIComponent(chain.source.warehouseId)}`
|
|
||||||
const customsUpload = await uploadDocument(context, `${customsPath}/document/upload?${warehouse}`, 0,
|
|
||||||
reason, 'loading-customs')
|
|
||||||
const customsDocument = context.apiSuccess(MODULE, '装柜清单公开接口上传出口报关文档',
|
|
||||||
customsUpload, '', OPTIONS)
|
|
||||||
context.assert(MODULE, '装柜清单报关上传返回真实文件事实', customsUpload,
|
|
||||||
Boolean(customsDocument?.id) && Boolean(customsDocument?.fileId)
|
|
||||||
&& customsDocument?.documentType === 'CUSTOMS_DECLARATION'
|
|
||||||
&& /^[0-9a-f]{64}$/i.test(String(customsDocument?.fileHash || ''))
|
|
||||||
&& !customsDocument?.completedAt,
|
|
||||||
`documentId=${id(customsDocument?.id) || 'missing'}, version=${customsDocument?.version ?? 'missing'}`, OPTIONS)
|
|
||||||
if (!customsDocument?.id || !customsDocument?.fileId || !Number.isInteger(Number(customsDocument?.version))) return
|
|
||||||
const customsCompleteResponse = await post(context, `${customsPath}/complete?${warehouse}`, {
|
|
||||||
expectedVersion: Number(customsDocument?.version), operationReason: reason,
|
|
||||||
}, 'loading-customs-complete')
|
|
||||||
const customsCompleted = context.apiSuccess(MODULE, '装柜清单点击完成出口报关',
|
|
||||||
customsCompleteResponse, '', OPTIONS)
|
|
||||||
context.assert(MODULE, '装柜清单报关完成事实可回读', customsCompleteResponse,
|
|
||||||
id(customsCompleted?.id) === id(customsDocument?.id) && Boolean(customsCompleted?.completedAt)
|
|
||||||
&& Number(customsCompleted?.version) === Number(customsDocument?.version) + 1,
|
|
||||||
`documentId=${id(customsCompleted?.id) || 'missing'}, completedAt=${customsCompleted?.completedAt || 'missing'}`, OPTIONS)
|
|
||||||
|
|
||||||
matrix.pages.containerLoading.actions = { clearance: fixture(chain.containerId, 'COMPLETED',
|
matrix.pages.containerLoading.actions = { clearance: fixture(chain.containerId, 'COMPLETED',
|
||||||
`/container-loading/${chain.containerId}/smart-loading`, {
|
`/container-loading/${chain.containerId}/smart-loading`, {
|
||||||
documentId: id(clearanceCompleted?.id), documentVersion: Number(clearanceCompleted?.version),
|
documentId: id(clearanceCompleted?.id), documentVersion: Number(clearanceCompleted?.version),
|
||||||
completedAt: clearanceCompleted?.completedAt || null,
|
completedAt: clearanceCompleted?.completedAt || null,
|
||||||
}) }
|
}) }
|
||||||
matrix.pages.loadingLists.actions = { customsDeclaration: fixture(chain.plan.id, 'COMPLETED',
|
|
||||||
`/loading-lists?planId=${chain.plan.id}&warehouseId=${chain.source.warehouseId}`, {
|
|
||||||
warehouseId: chain.source.warehouseId, documentId: id(customsCompleted?.id),
|
|
||||||
documentVersion: Number(customsCompleted?.version), completedAt: customsCompleted?.completedAt || null,
|
|
||||||
}) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function run(context, matrix = context.createdResources.manualStateMatrix) {
|
export async function run(context, matrix = context.createdResources.manualStateMatrix) {
|
||||||
@@ -396,13 +393,13 @@ export async function run(context, matrix = context.createdResources.manualState
|
|||||||
requiredActions: ['clearance'], fixtures: {}, actions: {} }
|
requiredActions: ['clearance'], fixtures: {}, actions: {} }
|
||||||
matrix.pages.preloadPlans = { requiredStatuses: PRELOAD_STATUSES, fixtures: {} }
|
matrix.pages.preloadPlans = { requiredStatuses: PRELOAD_STATUSES, fixtures: {} }
|
||||||
matrix.pages.loadingLists = { requiredStatuses: ['COMPLETED'],
|
matrix.pages.loadingLists = { requiredStatuses: ['COMPLETED'],
|
||||||
requiredActions: ['customsDeclaration'], fixtures: {}, actions: {} }
|
requiredActions: [], fixtures: {}, actions: {} }
|
||||||
matrix.pages.pdaTasks = { requiredStatuses: PDA_STATUSES, fixtures: {} }
|
matrix.pages.pdaTasks = { requiredStatuses: PDA_STATUSES, fixtures: {} }
|
||||||
|
|
||||||
const waitingSource = await readInbound(context, await prepareInbound(context, 'WAIT'))
|
const waitingSource = await readInbound(context, await prepareInbound(context, 'WAIT'))
|
||||||
const waitingContainerId = await createConfirmedContainer(context, waitingSource, 'WAIT')
|
const waitingContainer = await createConfirmedContainer(context, waitingSource, 'WAIT')
|
||||||
matrix.pages.containerLoading.fixtures.WAIT_LOADING = fixture(waitingContainerId, 'WAIT_LOADING',
|
matrix.pages.containerLoading.fixtures.WAIT_LOADING = fixture(waitingContainer.containerId, 'WAIT_LOADING',
|
||||||
`/container-loading?containerId=${waitingContainerId}`)
|
`/container-loading?containerId=${waitingContainer.containerId}`)
|
||||||
|
|
||||||
const editing = await createPlanTask(context, 'EDIT')
|
const editing = await createPlanTask(context, 'EDIT')
|
||||||
const inProgress = await claim(context, await createPlanTask(context, 'PROGRESS'), 'PROGRESS')
|
const inProgress = await claim(context, await createPlanTask(context, 'PROGRESS'), 'PROGRESS')
|
||||||
|
|||||||
@@ -39,9 +39,11 @@ async function raw(context, method, path, body, token, headers = {}) {
|
|||||||
|
|
||||||
async function loginMerchant(context) {
|
async function loginMerchant(context) {
|
||||||
const account = context.context?.merchantQualificationFixture
|
const account = context.context?.merchantQualificationFixture
|
||||||
if (!account?.username || !account?.password) throw new Error('缺少本轮客户登录凭据')
|
const username = account?.username || process.env.MERCHANT_TEST_USERNAME || ''
|
||||||
|
const password = account?.password || process.env.MERCHANT_TEST_PASSWORD || ''
|
||||||
|
if (!username || !password) throw new Error('缺少客户登录凭据')
|
||||||
const response = await context.request('POST', '/api/auth/login', {
|
const response = await context.request('POST', '/api/auth/login', {
|
||||||
username: account.username, password: account.password,
|
username, password,
|
||||||
}, '')
|
}, '')
|
||||||
const token = success(response) ? response.body?.data?.accessToken : null
|
const token = success(response) ? response.body?.data?.accessToken : null
|
||||||
if (!token) throw new Error(`本轮客户登录失败: requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`)
|
if (!token) throw new Error(`本轮客户登录失败: requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`)
|
||||||
|
|||||||
@@ -69,31 +69,28 @@ async function request(context, method, path, body, headers = {}, token = contex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureCompletionEvidence(context, tenantId, inboundId, keyBase) {
|
async function ensureCompletionEvidence(context, taskId, warehouseId, keyBase) {
|
||||||
const png = Uint8Array.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,6,0,0,0,31,21,196,137,0,0,0,13,73,68,65,84,8,215,99,248,207,192,240,31,0,5,0,1,255,137,153,61,29,0,0,0,0,73,69,78,68,174,66,96,130])
|
const png = Uint8Array.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,6,0,0,0,31,21,196,137,0,0,0,13,73,68,65,84,8,215,99,248,207,192,240,31,0,5,0,1,255,137,153,61,29,0,0,0,0,73,69,78,68,174,66,96,130])
|
||||||
for (const evidenceType of ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL']) {
|
for (const evidenceType of ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL']) {
|
||||||
const uploadPath = `/api/platform/v1/files/loading-evidence/${tenantId}/${inboundId}?evidenceType=${encodeURIComponent(evidenceType)}`
|
const query = new URLSearchParams({ warehouseId, expectedVersion: '0', evidenceType,
|
||||||
|
remark: context.marker })
|
||||||
|
if (evidenceType === 'SEAL') query.set('sealNo', `SEAL-${keyBase}`.slice(0, 64))
|
||||||
|
const uploadPath = `${PDA}/tasks/${taskId}/evidence?${query}`
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
form.append('file', new Blob([png], { type: 'image/png' }), `${keyBase}-${evidenceType}.png`)
|
form.append('file', new Blob([png], { type: 'image/png' }), `${keyBase}-${evidenceType}.png`)
|
||||||
const rawUpload = await fetch(`${context.options.gateway}${uploadPath}`, {
|
const rawUpload = await fetch(`${context.options.gateway}${uploadPath}`, {
|
||||||
method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}` },
|
method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}`,
|
||||||
|
'Idempotency-Key': `${keyBase}-evidence-${evidenceType}`.slice(0, 128) },
|
||||||
body: form, signal: AbortSignal.timeout(60_000),
|
body: form, signal: AbortSignal.timeout(60_000),
|
||||||
})
|
})
|
||||||
const uploadBody = await rawUpload.json()
|
const uploadBody = await rawUpload.json()
|
||||||
const upload = { method: 'POST', path: uploadPath, http: rawUpload.status, body: uploadBody,
|
const upload = { method: 'POST', path: uploadPath, http: rawUpload.status, body: uploadBody,
|
||||||
requestId: uploadBody?.requestId || rawUpload.headers.get('x-request-id'),
|
requestId: uploadBody?.requestId || rawUpload.headers.get('x-request-id'),
|
||||||
traceId: uploadBody?.traceId || rawUpload.headers.get('x-trace-id') }
|
traceId: uploadBody?.traceId || rawUpload.headers.get('x-trace-id') }
|
||||||
const file = context.apiSuccess(MODULE, `${keyBase} 上传 ${evidenceType} 装柜证据`, upload, '', OPTIONS)
|
const evidence = context.apiSuccess(MODULE, `${keyBase} 上传并绑定 ${evidenceType} 装柜证据`, upload, '', OPTIONS)
|
||||||
const fileId = id(file?.id || file?.fileId)
|
const active = evidence?.evidences?.find?.((item) => item.evidenceType === evidenceType
|
||||||
if (!fileId) return false
|
&& item.status !== 'VOID')
|
||||||
const bind = await request(context, 'POST',
|
if (!active?.evidenceId || id(evidence?.taskId) !== taskId) return false
|
||||||
`/api/platform/v1/inbounds/${inboundId}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`, {
|
|
||||||
expectedVersion: 0, evidenceType, fileId,
|
|
||||||
sealNo: evidenceType === 'SEAL' ? `SEAL-${keyBase}`.slice(0, 64) : null,
|
|
||||||
remark: context.marker,
|
|
||||||
}, { 'Idempotency-Key': `${keyBase}-evidence-${evidenceType}`.slice(0, 128) })
|
|
||||||
const bound = context.apiSuccess(MODULE, `${keyBase} 绑定 ${evidenceType} 装柜证据`, bind, '', OPTIONS)
|
|
||||||
if (!bound) return false
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -201,8 +198,20 @@ async function loadOneFragment(context, state) {
|
|||||||
const oceanBookingId = id(bookingCreated?.oceanBooking?.id)
|
const oceanBookingId = id(bookingCreated?.oceanBooking?.id)
|
||||||
if (!oceanBookingId) return null
|
if (!oceanBookingId) return null
|
||||||
context.createdResources.phase3OrderSplitting.retainedAuditRecordIds.push(oceanBookingId)
|
context.createdResources.phase3OrderSplitting.retainedAuditRecordIds.push(oceanBookingId)
|
||||||
|
const bookingDocumentForm = new FormData()
|
||||||
|
bookingDocumentForm.append('expectedVersion', String(Number(bookingCreated?.oceanBooking?.version)))
|
||||||
|
bookingDocumentForm.append('documentType', 'BOOKING_CONFIRMATION')
|
||||||
|
bookingDocumentForm.append('file', new Blob([`${context.marker}-${sourceOrderNo}-BOOKING_CONFIRMATION`],
|
||||||
|
{ type: 'text/plain' }), `booking-confirmation-${operationBase.replace(/[^A-Za-z0-9-]/g, '')}.txt`)
|
||||||
|
const bookingDocumentResponse = await context.requestMultipart('POST',
|
||||||
|
`/api/platform/v1/ocean-bookings/${oceanBookingId}/documents/upload`, bookingDocumentForm, context.token,
|
||||||
|
{ 'Idempotency-Key': `${operationBase}-booking-document`.slice(0, 128) })
|
||||||
|
const bookingDocument = context.apiSuccess(MODULE, `上传 ${sourceOrderNo} 的订舱确认文件`,
|
||||||
|
bookingDocumentResponse, '', OPTIONS)
|
||||||
|
const releaseVersion = Number(bookingDocument?.bookingVersion)
|
||||||
|
if (!Number.isInteger(releaseVersion)) return null
|
||||||
const releaseResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${oceanBookingId}/release`, {
|
const releaseResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${oceanBookingId}/release`, {
|
||||||
expectedVersion: Number(bookingCreated?.oceanBooking?.version), reason: context.marker,
|
expectedVersion: releaseVersion, reason: context.marker,
|
||||||
}, { 'Idempotency-Key': `${operationBase}-booking-release`.slice(0, 128) })
|
}, { 'Idempotency-Key': `${operationBase}-booking-release`.slice(0, 128) })
|
||||||
const bookingConfirmed = context.apiSuccess(MODULE, `放舱 ${sourceOrderNo} 的订舱`, releaseResponse, '', OPTIONS)
|
const bookingConfirmed = context.apiSuccess(MODULE, `放舱 ${sourceOrderNo} 的订舱`, releaseResponse, '', OPTIONS)
|
||||||
context.assert(MODULE, `${sourceOrderNo} 订舱为 RELEASED 且不预生成真实柜`, releaseResponse,
|
context.assert(MODULE, `${sourceOrderNo} 订舱为 RELEASED 且不预生成真实柜`, releaseResponse,
|
||||||
@@ -419,7 +428,7 @@ async function loadOneFragment(context, state) {
|
|||||||
`HTTP=${differentReplay.http}, code=${codeOf(differentReplay)}, requestId=${differentReplay.requestId || 'missing'}, traceId=${differentReplay.traceId || 'missing'}`, OPTIONS)
|
`HTTP=${differentReplay.http}, code=${codeOf(differentReplay)}, requestId=${differentReplay.requestId || 'missing'}, traceId=${differentReplay.traceId || 'missing'}`, OPTIONS)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await ensureCompletionEvidence(context, tenantId, id(candidate.inboundId), operationBase)) return null
|
if (!await ensureCompletionEvidence(context, taskId, warehouseId, operationBase)) return null
|
||||||
|
|
||||||
const completePath = `${PDA}/tasks/${taskId}/complete`
|
const completePath = `${PDA}/tasks/${taskId}/complete`
|
||||||
const completePayload = {
|
const completePayload = {
|
||||||
@@ -427,8 +436,18 @@ async function loadOneFragment(context, state) {
|
|||||||
remark: context.marker,
|
remark: context.marker,
|
||||||
}
|
}
|
||||||
const complete = await request(context, 'POST', completePath, completePayload)
|
const complete = await request(context, 'POST', completePath, completePayload)
|
||||||
const completed = context.apiSuccess(MODULE, `人工确认 ${sourceOrderNo} 装柜完成并结算余量`, complete, '', OPTIONS)
|
let completed = context.apiSuccess(MODULE, `人工确认 ${sourceOrderNo} 装柜完成并结算余量`, complete, '', OPTIONS)
|
||||||
context.assert(MODULE, `${sourceOrderNo} 只在 complete 后进入完成态`, complete,
|
let completedResponse = complete
|
||||||
|
for (let attempt = 0; attempt < 90 && completed?.taskStatus !== 'COMPLETED'
|
||||||
|
&& completed?.taskStatus !== 'COMPLETION_FAILED'; attempt += 1) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1_000))
|
||||||
|
completedResponse = await context.request('GET',
|
||||||
|
`${PDA}/tasks/${taskId}?warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||||
|
if (completedResponse.http === 200 && codeOf(completedResponse) === '200') {
|
||||||
|
completed = completedResponse.body?.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.assert(MODULE, `${sourceOrderNo} 只在 complete 后进入完成态`, completedResponse,
|
||||||
completed?.taskStatus === 'COMPLETED' && completed?.workflowStatus === 'COMPLETED'
|
completed?.taskStatus === 'COMPLETED' && completed?.workflowStatus === 'COMPLETED'
|
||||||
&& completed?.items?.some?.((row) => id(row.id) === id(claimedItem.id)
|
&& completed?.items?.some?.((row) => id(row.id) === id(claimedItem.id)
|
||||||
&& Number(row.pickedPackageCount) === 1 && Number(row.plannedPackageCount) === 1),
|
&& Number(row.pickedPackageCount) === 1 && Number(row.plannedPackageCount) === 1),
|
||||||
|
|||||||
Reference in New Issue
Block a user