Merge remote-tracking branch 'origin/test' into dev
# Conflicts: # firstleg/src/main/java/com/baiyue/firstleg/loading/service/CabinetLoadingEvidenceService.java # firstleg/src/test/java/com/baiyue/firstleg/loading/service/CabinetLoadingEvidenceServiceTest.java # scripts/admin-test-env-modules/ocean-bookings.mjs # scripts/admin-test-env-modules/phase3-container-customs-fixture.mjs # scripts/admin-test-env-modules/phase3-container-documents-contract.test.mjs # scripts/admin-test-env-modules/phase3-container-loading.mjs # scripts/admin-test-env-modules/phase3-customs.mjs # scripts/admin-test-env-modules/phase3-loading-finalize.mjs # scripts/admin-test-env-modules/phase3-loading.mjs # scripts/admin-test-env-modules/phase3-port-schedules.mjs # scripts/admin-test-env-modules/service-center-flow.mjs
This commit is contained in:
@@ -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])
|
||||
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()
|
||||
form.append('file', new Blob([png], { type: 'image/png' }), `${keyBase}-${evidenceType}.png`)
|
||||
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),
|
||||
})
|
||||
const uploadBody = await rawUpload.json()
|
||||
const upload = { method: 'POST', path: uploadPath, http: rawUpload.status, body: uploadBody,
|
||||
requestId: uploadBody?.requestId || rawUpload.headers.get('x-request-id'),
|
||||
traceId: uploadBody?.traceId || rawUpload.headers.get('x-trace-id') }
|
||||
const file = context.apiSuccess(MODULE, `${keyBase} 上传 ${evidenceType} 装柜证据`, upload, '', OPTIONS)
|
||||
const fileId = id(file?.id || file?.fileId)
|
||||
if (!fileId) return false
|
||||
const bind = await request(context, 'POST',
|
||||
`/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
|
||||
const evidence = context.apiSuccess(MODULE, `${keyBase} 上传并绑定 ${evidenceType} 装柜证据`, upload, '', OPTIONS)
|
||||
const active = evidence?.evidences?.find?.((item) => item.evidenceType === evidenceType
|
||||
&& item.status !== 'VOID')
|
||||
if (!active?.evidenceId || id(evidence?.taskId) !== taskId) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -165,13 +162,13 @@ function snapshotMatches(tree, original, loaded, remaining, activeSuffix) {
|
||||
&& activeMatches && quantityConserved(tree)
|
||||
}
|
||||
|
||||
function bookingPayload(marker, sequence, source, agent) {
|
||||
function bookingPayload(marker, sequence, source, agent, carrier) {
|
||||
const suffix = `${marker}-${sequence}`.replace(/[^A-Za-z0-9]/g, '').slice(-18)
|
||||
return oceanBookingDraft({ marker }, suffix, {
|
||||
customerId: source.tenantId, customerName: source.customerName, channelCode: source.channelCode,
|
||||
warehouseId: source.warehouseId, warehouseName: source.warehouseName,
|
||||
}, { vesselName: `SPLIT-VESSEL-${suffix}`.slice(0, 128), voyageNo: `SV-${sequence}`.slice(0, 64),
|
||||
shippingCompanyId: id(agent.id), shippingCompanyName: agent.name,
|
||||
shippingCompanyId: id(carrier.id), shippingCompanyName: carrier.name, carrierCode: carrier.carrierCode,
|
||||
bookingAgentId: id(agent.id), bookingAgentName: agent.name,
|
||||
estimatedSailingAt: '2026-09-16T08:00:00', estimatedArrivalAt: '2026-10-16T08:00:00' })
|
||||
}
|
||||
@@ -184,6 +181,9 @@ async function loadOneFragment(context, state) {
|
||||
const agentPage = context.apiSuccess(MODULE, `为 ${sourceOrderNo} 读取 ACTIVE 订舱代理`, agentResponse, '', OPTIONS)
|
||||
const agent = agentPage?.records?.find?.((item) => item?.id && item?.name)
|
||||
if (!agent) return null
|
||||
const { ensureMatsonCarrierPartner } = await import('./carrier-partner-fixture.mjs')
|
||||
const carrier = await ensureMatsonCarrierPartner(context, MODULE, OPTIONS)
|
||||
if (!carrier?.id) return null
|
||||
const simulationResponse = await request(context, 'POST', CONTAINERS, {
|
||||
channelCode: state.channelCode, warehouseId, containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000,
|
||||
})
|
||||
@@ -192,14 +192,26 @@ async function loadOneFragment(context, state) {
|
||||
if (!containerId) return null
|
||||
context.createdResources.phase3OrderSplitting.retainedAuditRecordIds.push(containerId)
|
||||
const bookingCreate = await request(context, 'POST', '/api/platform/v1/ocean-bookings', {
|
||||
expectedVersion: 0, data: bookingPayload(context.marker, sequence, state, agent),
|
||||
expectedVersion: 0, data: bookingPayload(context.marker, sequence, state, agent, carrier),
|
||||
}, { 'Idempotency-Key': `${operationBase}-booking-create` })
|
||||
const bookingCreated = context.apiSuccess(MODULE, `为 ${sourceOrderNo} 创建渠道仓库一致的订舱`, bookingCreate, '', OPTIONS)
|
||||
const oceanBookingId = id(bookingCreated?.oceanBooking?.id)
|
||||
if (!oceanBookingId) return null
|
||||
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`, {
|
||||
expectedVersion: Number(bookingCreated?.oceanBooking?.version), reason: context.marker,
|
||||
expectedVersion: releaseVersion, reason: context.marker,
|
||||
}, { 'Idempotency-Key': `${operationBase}-booking-release`.slice(0, 128) })
|
||||
const bookingConfirmed = context.apiSuccess(MODULE, `放舱 ${sourceOrderNo} 的订舱`, releaseResponse, '', OPTIONS)
|
||||
context.assert(MODULE, `${sourceOrderNo} 订舱为 RELEASED 且不预生成真实柜`, releaseResponse,
|
||||
@@ -230,8 +242,8 @@ async function loadOneFragment(context, state) {
|
||||
`orderId=${sourceOrderId}, available=${candidate?.availablePackageCount ?? 'missing'}`, OPTIONS)
|
||||
if (!candidate?.inboundId || !candidate?.inboundDetailId) return null
|
||||
|
||||
const createPlan = await request(context, 'POST', `${SMART}/plans`, { bookingId: containerId, remark: context.marker })
|
||||
const plan = context.apiSuccess(MODULE, `为 ${sourceOrderNo} 创建装柜计划`, createPlan, '', OPTIONS)
|
||||
const createPlan = await request(context, 'GET', `${SMART}/plans/by-booking/${containerId}`)
|
||||
const plan = context.apiSuccess(MODULE, `读取 ${sourceOrderNo} 模拟柜自动创建的装柜计划`, createPlan, '', OPTIONS)
|
||||
const planId = id(plan?.id)
|
||||
if (!planId) return null
|
||||
context.createdResources.phase3OrderSplitting.retainedAuditRecordIds.push(planId)
|
||||
@@ -289,11 +301,11 @@ async function loadOneFragment(context, state) {
|
||||
`status=${bookingDetail?.oceanBooking?.status || 'missing'}, containerBookingId=${containerId}`, OPTIONS)
|
||||
if (bookingDetail?.oceanBooking?.status !== 'RELEASED' || !releasedAllocationMatches) return null
|
||||
|
||||
const push = await request(context, 'POST', `${SMART}/plans/${planId}/push-preload`,
|
||||
{ expectedVersion: Number(assigned.version) }, { 'Idempotency-Key': `${operationBase}-push` })
|
||||
const pushed = context.apiSuccess(MODULE, `推送 ${sourceOrderNo} 的预装计划`, push, '', OPTIONS)
|
||||
context.assert(MODULE, `${sourceOrderNo} 补真实柜号后计划保持 EDITING`, push,
|
||||
pushed?.status === 'EDITING', `status=${pushed?.status || 'missing'}`, OPTIONS)
|
||||
const push = await request(context, 'GET', `${SMART}/plans/${planId}`)
|
||||
const pushed = context.apiSuccess(MODULE, `读取 ${sourceOrderNo} 自动关联的预装计划`, push, '', OPTIONS)
|
||||
context.assert(MODULE, `${sourceOrderNo} 补真实柜号后自动预装计划保持 EDITING`, push,
|
||||
pushed?.status === 'EDITING' && Number(pushed?.version) === Number(assigned.version),
|
||||
`status=${pushed?.status || 'missing'}, version=${pushed?.version ?? 'missing'}`, OPTIONS)
|
||||
if (pushed?.status !== 'EDITING') return null
|
||||
|
||||
const unconfirmedTaskResponse = await context.request('GET',
|
||||
@@ -416,7 +428,7 @@ async function loadOneFragment(context, state) {
|
||||
`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 completePayload = {
|
||||
@@ -424,8 +436,18 @@ async function loadOneFragment(context, state) {
|
||||
remark: context.marker,
|
||||
}
|
||||
const complete = await request(context, 'POST', completePath, completePayload)
|
||||
const completed = context.apiSuccess(MODULE, `人工确认 ${sourceOrderNo} 装柜完成并结算余量`, complete, '', OPTIONS)
|
||||
context.assert(MODULE, `${sourceOrderNo} 只在 complete 后进入完成态`, complete,
|
||||
let completed = context.apiSuccess(MODULE, `人工确认 ${sourceOrderNo} 装柜完成并结算余量`, complete, '', OPTIONS)
|
||||
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?.items?.some?.((row) => id(row.id) === id(claimedItem.id)
|
||||
&& Number(row.pickedPackageCount) === 1 && Number(row.plannedPackageCount) === 1),
|
||||
|
||||
Reference in New Issue
Block a user