fix(firstleg): preserve coload groups in filtered candidates
This commit is contained in:
+20
-16
@@ -113,22 +113,6 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
|||||||
o.process_status IN ('WAIT_MEASURE','MEASURING','MEASURE_PENDING','MEASURE_REJECTED',
|
o.process_status IN ('WAIT_MEASURE','MEASURING','MEASURE_PENDING','MEASURE_REJECTED',
|
||||||
'WAIT_PRICING','FEE_CONFIRMING','INBOUNDING','WAIT_SPLIT','LOADING')
|
'WAIT_PRICING','FEE_CONFIRMING','INBOUNDING','WAIT_SPLIT','LOADING')
|
||||||
)
|
)
|
||||||
<if test="query.orderStatuses != null and !query.orderStatuses.isEmpty()">
|
|
||||||
AND (
|
|
||||||
<foreach collection="query.orderStatuses" item="status" separator=" OR ">
|
|
||||||
<choose>
|
|
||||||
<when test="status.name() == 'WAIT_MEASURE'">o.process_status IN ('WAIT_MEASURE','MEASURING','MEASURE_PENDING','MEASURE_REJECTED','WAIT_PRICING','FEE_CONFIRMING','INBOUNDING')</when>
|
|
||||||
<when test="status.name() == 'WAIT_SPLIT'">o.process_status='WAIT_SPLIT'</when>
|
|
||||||
<when test="status.name() == 'LOADING'">o.process_status='LOADING'</when>
|
|
||||||
<otherwise>1=0</otherwise>
|
|
||||||
</choose>
|
|
||||||
</foreach>
|
|
||||||
)
|
|
||||||
</if>
|
|
||||||
<if test="query.customerId != null">AND o.tenant_id=#{query.customerId}</if>
|
|
||||||
<if test="query.inboundCode != null and query.inboundCode.trim() != ''">AND LOWER(o.inbound_code) LIKE CONCAT('%',LOWER(#{query.inboundCode}),'%')</if>
|
|
||||||
<if test="query.createdStart != null">AND o.created_at>=#{query.createdStart}</if>
|
|
||||||
<if test="query.createdEnd != null">AND o.created_at<DATE_ADD(#{query.createdEnd},INTERVAL 1 DAY)</if>
|
|
||||||
) eligible
|
) eligible
|
||||||
LEFT JOIN fl_coload_group_member current_member
|
LEFT JOIN fl_coload_group_member current_member
|
||||||
ON current_member.tenant_id=eligible.accountId AND current_member.inbound_id=eligible.inboundId
|
ON current_member.tenant_id=eligible.accountId AND current_member.inbound_id=eligible.inboundId
|
||||||
@@ -143,6 +127,26 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
|||||||
COUNT(*)=(SELECT COUNT(*) FROM fl_coload_group_member group_member_count
|
COUNT(*)=(SELECT COUNT(*) FROM fl_coload_group_member group_member_count
|
||||||
WHERE group_member_count.group_id=current_group.id AND group_member_count.active_flag=1)
|
WHERE group_member_count.group_id=current_group.id AND group_member_count.active_flag=1)
|
||||||
))
|
))
|
||||||
|
<if test="query.orderStatuses != null and !query.orderStatuses.isEmpty()">
|
||||||
|
AND MAX(CASE WHEN
|
||||||
|
<foreach collection="query.orderStatuses" item="status" separator=" OR ">
|
||||||
|
<choose>
|
||||||
|
<when test="status.name() == 'WAIT_MEASURE'">eligible.candidateStatus='WAIT_MEASURE'</when>
|
||||||
|
<when test="status.name() == 'WAIT_SPLIT'">eligible.candidateStatus='WAIT_SPLIT'</when>
|
||||||
|
<when test="status.name() == 'LOADING'">eligible.candidateStatus='LOADING'</when>
|
||||||
|
<otherwise>1=0</otherwise>
|
||||||
|
</choose>
|
||||||
|
</foreach>
|
||||||
|
THEN 1 ELSE 0 END)=1
|
||||||
|
</if>
|
||||||
|
<if test="query.customerId != null">
|
||||||
|
AND MAX(CASE WHEN eligible.accountId=#{query.customerId} THEN 1 ELSE 0 END)=1
|
||||||
|
</if>
|
||||||
|
<if test="query.inboundCode != null and query.inboundCode.trim() != ''">
|
||||||
|
AND MAX(CASE WHEN LOWER(eligible.inboundCode) LIKE CONCAT('%',LOWER(#{query.inboundCode}),'%') THEN 1 ELSE 0 END)=1
|
||||||
|
</if>
|
||||||
|
<if test="query.createdStart != null">AND MIN(eligible.createdAt)>=#{query.createdStart}</if>
|
||||||
|
<if test="query.createdEnd != null">AND MIN(eligible.createdAt)<DATE_ADD(#{query.createdEnd},INTERVAL 1 DAY)</if>
|
||||||
ORDER BY sortArrivalAt IS NULL,sortArrivalAt,sortInboundId
|
ORDER BY sortArrivalAt IS NULL,sortArrivalAt,sortInboundId
|
||||||
</script>
|
</script>
|
||||||
""")
|
""")
|
||||||
|
|||||||
+7
-5
@@ -97,8 +97,8 @@ class SmartLoadingCandidateMapperContractTest {
|
|||||||
|
|
||||||
assertThat(sql)
|
assertThat(sql)
|
||||||
.contains("o.created_at createdAt")
|
.contains("o.created_at createdAt")
|
||||||
.contains("o.created_at>=#{query.createdStart}")
|
.contains("MIN(eligible.createdAt)>=#{query.createdStart}")
|
||||||
.contains("o.created_at<DATE_ADD(#{query.createdEnd},INTERVAL 1 DAY)")
|
.contains("MIN(eligible.createdAt)<DATE_ADD(#{query.createdEnd},INTERVAL 1 DAY)")
|
||||||
.doesNotContain("query.expectedArrivalStart", "query.expectedArrivalEnd");
|
.doesNotContain("query.expectedArrivalStart", "query.expectedArrivalEnd");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,9 +136,11 @@ class SmartLoadingCandidateMapperContractTest {
|
|||||||
.getBoundSql(Map.of("query", query, "actualChannels", Set.of("MATSON_CLX_TRUCK"))).getSql();
|
.getBoundSql(Map.of("query", query, "actualChannels", Set.of("MATSON_CLX_TRUCK"))).getSql();
|
||||||
assertThat(renderedUnits)
|
assertThat(renderedUnits)
|
||||||
.contains("o.channel_code IN")
|
.contains("o.channel_code IN")
|
||||||
.contains("o.created_at>=", "o.created_at<DATE_ADD")
|
.contains("MIN(eligible.createdAt)>=", "MIN(eligible.createdAt)<DATE_ADD")
|
||||||
.contains("o.process_status IN ('WAIT_MEASURE'")
|
.contains("eligible.candidateStatus='WAIT_MEASURE'")
|
||||||
.contains("o.process_status='WAIT_SPLIT'");
|
.contains("eligible.candidateStatus='WAIT_SPLIT'")
|
||||||
|
.contains("MAX(CASE WHEN LOWER(eligible.inboundCode)")
|
||||||
|
.contains("MAX(CASE WHEN eligible.accountId=");
|
||||||
|
|
||||||
Method details = SmartLoadingCandidateMapper.class.getDeclaredMethod(
|
Method details = SmartLoadingCandidateMapper.class.getDeclaredMethod(
|
||||||
"globalCandidatesByUnits", List.class);
|
"globalCandidatesByUnits", List.class);
|
||||||
|
|||||||
@@ -10,6 +10,19 @@ import { ensureMatsonCarrierPartner } from './carrier-partner-fixture.mjs'
|
|||||||
import { iso6346ContainerNumber } from './iso6346-container-number.mjs'
|
import { iso6346ContainerNumber } from './iso6346-container-number.mjs'
|
||||||
|
|
||||||
function id(value) { return value == null ? '' : String(value) }
|
function id(value) { return value == null ? '' : String(value) }
|
||||||
|
function channelGroup(channelCode) {
|
||||||
|
const value = String(channelCode || '').toUpperCase()
|
||||||
|
if (value.startsWith('EXX_')) return 'EXX'
|
||||||
|
if (value.startsWith('MATSON_CLX_')) return 'MATSON_CLX'
|
||||||
|
if (value.startsWith('MATSON_MAX_')) return 'MATSON_MAX'
|
||||||
|
if (value.startsWith('HEDE_')) return 'HEDE'
|
||||||
|
if (value.startsWith('OA_LA_') || value === 'OA_PARCEL') return 'OA_LA'
|
||||||
|
if (value.startsWith('OA_CHICAGO_')) return 'OA_CHICAGO'
|
||||||
|
if (value.startsWith('OA_HOUSTON_')) return 'OA_HOUSTON'
|
||||||
|
if (value.startsWith('OA_SAVANNAH_')) return 'OA_SAVANNAH'
|
||||||
|
if (value === 'OA_NEW_YORK_TRUCK') return value
|
||||||
|
throw new Error(`unsupported smart-loading channel group: ${channelCode}`)
|
||||||
|
}
|
||||||
function code(response) { return id(response?.body?.code) || 'missing' }
|
function code(response) { return id(response?.body?.code) || 'missing' }
|
||||||
function evidence(response, extra = '') {
|
function evidence(response, extra = '') {
|
||||||
return `HTTP=${response?.http ?? 'missing'}, code=${code(response)}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}${extra ? `, ${extra}` : ''}`
|
return `HTTP=${response?.http ?? 'missing'}, code=${code(response)}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}${extra ? `, ${extra}` : ''}`
|
||||||
@@ -183,8 +196,24 @@ async function createReleasedBookingAndLink(context, fixture, chains) {
|
|||||||
evidence(createResponse), OPTIONS)
|
evidence(createResponse), OPTIONS)
|
||||||
const oceanBookingId = id(created?.oceanBooking?.id)
|
const oceanBookingId = id(created?.oceanBooking?.id)
|
||||||
if (!oceanBookingId) return null
|
if (!oceanBookingId) return null
|
||||||
|
const bookingDocumentForm = new FormData()
|
||||||
|
bookingDocumentForm.append('expectedVersion', String(Number(created?.oceanBooking?.version)))
|
||||||
|
bookingDocumentForm.append('documentType', 'BOOKING_CONFIRMATION')
|
||||||
|
bookingDocumentForm.append('file', new Blob([`${context.marker}-COLOAD-BOOKING-CONFIRMATION`],
|
||||||
|
{ type: 'text/plain' }), `coload-booking-confirmation-${suffix}.txt`)
|
||||||
|
const bookingDocumentResponse = await context.requestMultipart('POST',
|
||||||
|
`/api/platform/v1/ocean-bookings/${oceanBookingId}/documents/upload`, bookingDocumentForm, context.token,
|
||||||
|
{ 'Idempotency-Key': `${context.marker}-coload-pda-booking-document`.slice(0, 128) })
|
||||||
|
const bookingDocument = context.apiSuccess(MODULE, 'PDA链上传有效订舱确认文件', bookingDocumentResponse,
|
||||||
|
evidence(bookingDocumentResponse), OPTIONS)
|
||||||
|
const releaseVersion = Number(bookingDocument?.bookingVersion)
|
||||||
|
context.assert(MODULE, 'PDA链订舱确认文件已关联且版本递增', bookingDocumentResponse,
|
||||||
|
Boolean(bookingDocument?.document?.id) && Number.isInteger(releaseVersion),
|
||||||
|
`documentId=${id(bookingDocument?.document?.id) || 'missing'}, bookingVersion=${bookingDocument?.bookingVersion ?? 'missing'}`,
|
||||||
|
OPTIONS)
|
||||||
|
if (!Number.isInteger(releaseVersion)) return null
|
||||||
const releaseResponse = await context.request('POST', `/api/platform/v1/ocean-bookings/${oceanBookingId}/release`, {
|
const releaseResponse = await context.request('POST', `/api/platform/v1/ocean-bookings/${oceanBookingId}/release`, {
|
||||||
expectedVersion: Number(created?.oceanBooking?.version), reason: context.marker,
|
expectedVersion: releaseVersion, reason: context.marker,
|
||||||
}, undefined, { 'Idempotency-Key': `${context.marker}-coload-pda-booking-release`.slice(0, 128) })
|
}, undefined, { 'Idempotency-Key': `${context.marker}-coload-pda-booking-release`.slice(0, 128) })
|
||||||
const released = context.apiSuccess(MODULE, 'PDA链放舱并冻结两柜需求', releaseResponse,
|
const released = context.apiSuccess(MODULE, 'PDA链放舱并冻结两柜需求', releaseResponse,
|
||||||
evidence(releaseResponse), OPTIONS)
|
evidence(releaseResponse), OPTIONS)
|
||||||
@@ -265,16 +294,22 @@ async function fulfillmentTrees(context, fixtures, name) {
|
|||||||
return snapshots
|
return snapshots
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadCompletionEvidence(context, fixture, label) {
|
async function uploadCompletionEvidence(context, taskId, warehouseId, label) {
|
||||||
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 path = `/api/platform/v1/files/loading-evidence/${fixture.tenantId}/${fixture.inboundId}?evidenceType=${encodeURIComponent(evidenceType)}`
|
const path = `${PDA}/tasks/${encodeURIComponent(taskId)}/evidence`
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
|
form.append('warehouseId', String(warehouseId))
|
||||||
|
form.append('expectedVersion', '0')
|
||||||
|
form.append('evidenceType', evidenceType)
|
||||||
|
if (evidenceType === 'SEAL') form.append('sealNo', `SEAL-${context.marker}-${label}`.slice(0, 64))
|
||||||
|
form.append('remark', context.marker)
|
||||||
form.append('file', new Blob([png], { type: 'image/png' }), `${context.marker}-${label}-${evidenceType}.png`)
|
form.append('file', new Blob([png], { type: 'image/png' }), `${context.marker}-${label}-${evidenceType}.png`)
|
||||||
let uploadResponse
|
let uploadResponse
|
||||||
try {
|
try {
|
||||||
const raw = await fetch(`${context.options.gateway}${path}`, {
|
const raw = await fetch(`${context.options.gateway}${path}`, {
|
||||||
method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}` },
|
method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}`,
|
||||||
|
'Idempotency-Key': `${context.marker}-${label}-${evidenceType}`.slice(0, 128) },
|
||||||
body: form, signal: AbortSignal.timeout(60_000),
|
body: form, signal: AbortSignal.timeout(60_000),
|
||||||
})
|
})
|
||||||
const body = await raw.json()
|
const body = await raw.json()
|
||||||
@@ -284,19 +319,11 @@ async function uploadCompletionEvidence(context, fixture, label) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
uploadResponse = { method: 'POST', path, http: 0, body: null, raw: String(error) }
|
uploadResponse = { method: 'POST', path, http: 0, body: null, raw: String(error) }
|
||||||
}
|
}
|
||||||
const uploaded = context.apiSuccess(MODULE, `${label} 上传 ${evidenceType} 柜级证据`, uploadResponse,
|
const bound = context.apiSuccess(MODULE, `${label} 上传并绑定 ${evidenceType} 柜级证据`, uploadResponse,
|
||||||
evidence(uploadResponse), OPTIONS)
|
evidence(uploadResponse), OPTIONS)
|
||||||
const fileId = id(uploaded?.fileId || uploaded?.id)
|
const evidenceItem = bound?.evidences?.find?.((item) => item?.evidenceType === evidenceType
|
||||||
if (!fileId) return false
|
&& item?.status !== 'VOID')
|
||||||
const bindResponse = await context.request('POST',
|
if (!evidenceItem?.fileId) return false
|
||||||
`/api/platform/v1/inbounds/${fixture.inboundId}/loading/evidence?tenantId=${encodeURIComponent(fixture.tenantId)}`, {
|
|
||||||
expectedVersion: 0, evidenceType, fileId,
|
|
||||||
sealNo: evidenceType === 'SEAL' ? `SEAL-${context.marker}-${label}`.slice(0, 64) : null,
|
|
||||||
remark: context.marker,
|
|
||||||
}, undefined, { 'Idempotency-Key': `${context.marker}-${label}-${evidenceType}`.slice(0, 128) })
|
|
||||||
const bound = context.apiSuccess(MODULE, `${label} 绑定 ${evidenceType} 柜级证据`, bindResponse,
|
|
||||||
evidence(bindResponse), OPTIONS)
|
|
||||||
if (!bound) return false
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -508,15 +535,34 @@ export async function run(context) {
|
|||||||
let planId = id(plan?.id)
|
let planId = id(plan?.id)
|
||||||
if (!planId) return
|
if (!planId) return
|
||||||
|
|
||||||
|
const candidateStartedAt = Date.now()
|
||||||
const candidateResponse = await context.request('GET',
|
const candidateResponse = await context.request('GET',
|
||||||
`/api/platform/v1/first-leg/smart-loading/inbound-candidates?bookingId=${encodeURIComponent(bookingId)}&warehouseId=${encodeURIComponent(first.warehouseId)}`)
|
`/api/platform/v1/first-leg/smart-loading/inbound-candidates?channelGroupCodes=${encodeURIComponent(channelGroup(first.channelCode))}&warehouseId=${encodeURIComponent(first.warehouseId)}&page=1&size=200`)
|
||||||
const candidates = context.apiSuccess(MODULE, '读取整票智能装柜候选', candidateResponse,
|
const candidateElapsedMs = Date.now() - candidateStartedAt
|
||||||
evidence(candidateResponse), OPTIONS) || []
|
const candidatePage = context.apiSuccess(MODULE, '读取整票智能装柜候选', candidateResponse,
|
||||||
const groupedCandidates = candidates.filter((item) => id(item?.coLoadGroupId) === groupId)
|
evidence(candidateResponse), OPTIONS)
|
||||||
context.assert(MODULE, '每个组成员候选携带相同整组事实', candidateResponse,
|
const candidates = candidatePage?.records || []
|
||||||
groupedCandidates.length === 2 && groupedCandidates.every((item) => Number(item?.memberCount) === 2
|
context.assert(MODULE, '全局候选200条分页查询性能受控', candidateResponse,
|
||||||
&& Number(item?.groupTotalBoxes) === first.packages + second.packages && item?.locked !== true),
|
candidateElapsedMs < 5000 && Number(candidatePage?.page) === 1 && Number(candidatePage?.size) === 200,
|
||||||
`count=${groupedCandidates.length}, groupTotal=${first.packages + second.packages}`, OPTIONS)
|
`elapsedMs=${candidateElapsedMs}, total=${candidatePage?.total ?? 'missing'}`, OPTIONS)
|
||||||
|
|
||||||
|
const today = new Date().toISOString().slice(0, 10)
|
||||||
|
const filteredResponse = await context.request('GET',
|
||||||
|
`${SMART}/inbound-candidates?channelGroupCodes=${encodeURIComponent(channelGroup(first.channelCode))}`
|
||||||
|
+ `&warehouseId=${encodeURIComponent(first.warehouseId)}&inboundCode=${encodeURIComponent(first.inboundCode)}`
|
||||||
|
+ `&createdStart=${today}&createdEnd=${today}&orderStatuses=PENDING_REVIEW&orderStatuses=WAIT_SPLIT&page=1&size=20`)
|
||||||
|
const filteredPage = context.apiSuccess(MODULE, '按创建日期和多状态查询同柜组候选', filteredResponse,
|
||||||
|
evidence(filteredResponse), OPTIONS)
|
||||||
|
const filteredGroup = (filteredPage?.records || []).find((item) => id(item?.coLoadGroupId) === groupId)
|
||||||
|
context.assert(MODULE, '同柜组作为一个分页单位携带全部成员事实', filteredResponse,
|
||||||
|
Number(filteredPage?.records?.length) === 1 && Number(filteredGroup?.memberCount) === 2
|
||||||
|
&& Number(filteredGroup?.groupTotalBoxes) === first.packages + second.packages
|
||||||
|
&& filteredGroup?.locked !== true && filteredGroup?.groupMembers?.length === 2,
|
||||||
|
`units=${filteredPage?.records?.length ?? 0}, members=${filteredGroup?.groupMembers?.length ?? 0}, groupTotal=${first.packages + second.packages}`, OPTIONS)
|
||||||
|
context.assert(MODULE, '多状态仅用于搜索且命中整组不拆页', filteredResponse,
|
||||||
|
filteredGroup?.orderStatusSnapshot === 'WAIT_SPLIT' && filteredGroup?.groupMembers?.length === 2,
|
||||||
|
`date=${today}, status=${filteredGroup?.orderStatusSnapshot || 'missing'}, members=${filteredGroup?.groupMembers?.length ?? 0}`,
|
||||||
|
OPTIONS)
|
||||||
|
|
||||||
let isolatedCandidates = candidates
|
let isolatedCandidates = candidates
|
||||||
const competing = [...new Set(isolatedCandidates.filter((item) => item?.locked !== true)
|
const competing = [...new Set(isolatedCandidates.filter((item) => item?.locked !== true)
|
||||||
@@ -524,14 +570,13 @@ export async function run(context) {
|
|||||||
let isolatedCandidateResponse = candidateResponse
|
let isolatedCandidateResponse = candidateResponse
|
||||||
if (competing.length > 0) {
|
if (competing.length > 0) {
|
||||||
isolatedCandidateResponse = await context.request('GET',
|
isolatedCandidateResponse = await context.request('GET',
|
||||||
`${SMART}/inbound-candidates?bookingId=${encodeURIComponent(bookingId)}&warehouseId=${encodeURIComponent(first.warehouseId)}`)
|
`${SMART}/inbound-candidates?channelGroupCodes=${encodeURIComponent(channelGroup(first.channelCode))}&warehouseId=${encodeURIComponent(first.warehouseId)}&page=1&size=200`)
|
||||||
isolatedCandidates = context.apiSuccess(MODULE, '清理后重新隔离读取目标组候选', isolatedCandidateResponse,
|
const isolatedCandidatePage = context.apiSuccess(MODULE, '清理后重新隔离读取目标组候选', isolatedCandidateResponse,
|
||||||
evidence(isolatedCandidateResponse), OPTIONS) || []
|
evidence(isolatedCandidateResponse), OPTIONS)
|
||||||
|
isolatedCandidates = isolatedCandidatePage?.records || []
|
||||||
}
|
}
|
||||||
const candidateGroupIds = [...new Set(isolatedCandidates.filter((item) => item?.locked !== true)
|
const candidateGroupIds = filteredGroup ? [id(filteredGroup.coLoadGroupId)] : []
|
||||||
.map((item) => id(item?.coLoadGroupId)).filter(Boolean))]
|
const targetCandidateInbounds = (filteredGroup?.groupMembers || []).map((item) => id(item?.inboundId)).sort()
|
||||||
const targetCandidateInbounds = isolatedCandidates.filter((item) => id(item?.coLoadGroupId) === groupId)
|
|
||||||
.map((item) => id(item?.inboundId)).sort()
|
|
||||||
context.assert(MODULE, '推荐候选包含当前目标同柜组且成员身份精确', isolatedCandidateResponse,
|
context.assert(MODULE, '推荐候选包含当前目标同柜组且成员身份精确', isolatedCandidateResponse,
|
||||||
candidateGroupIds.includes(groupId)
|
candidateGroupIds.includes(groupId)
|
||||||
&& JSON.stringify(targetCandidateInbounds) === JSON.stringify([first.inboundId, second.inboundId].sort()),
|
&& JSON.stringify(targetCandidateInbounds) === JSON.stringify([first.inboundId, second.inboundId].sort()),
|
||||||
@@ -539,8 +584,8 @@ export async function run(context) {
|
|||||||
const allCandidateVolume = isolatedCandidates
|
const allCandidateVolume = isolatedCandidates
|
||||||
.reduce((sum, item) => sum + Number(item?.availableVolumeCbm || 0), 0)
|
.reduce((sum, item) => sum + Number(item?.availableVolumeCbm || 0), 0)
|
||||||
context.assert(MODULE, '推荐候选总量形成容量竞争场景', isolatedCandidateResponse,
|
context.assert(MODULE, '推荐候选总量形成容量竞争场景', isolatedCandidateResponse,
|
||||||
allCandidateVolume > 33 * 1.1,
|
allCandidateVolume > 33 * 1.5,
|
||||||
`targetGroupId=${groupId}, candidateVolume=${allCandidateVolume}, limit=${33 * 1.1}`, OPTIONS)
|
`targetGroupId=${groupId}, candidateVolume=${allCandidateVolume}, limit=${33 * 1.5}`, OPTIONS)
|
||||||
const beforeRecommend = plan
|
const beforeRecommend = plan
|
||||||
const recommendResponse = await context.request('POST',
|
const recommendResponse = await context.request('POST',
|
||||||
`/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(planId)}/recommend`, {
|
`/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(planId)}/recommend`, {
|
||||||
@@ -827,7 +872,7 @@ export async function run(context) {
|
|||||||
fullBatch?.workflowStatus === 'AWAITING_REVIEW'
|
fullBatch?.workflowStatus === 'AWAITING_REVIEW'
|
||||||
&& fullBatch?.items?.every((item) => Number(item?.pickedPackageCount) === Number(item?.plannedPackageCount)),
|
&& fullBatch?.items?.every((item) => Number(item?.pickedPackageCount) === Number(item?.plannedPackageCount)),
|
||||||
`workflow=${fullBatch?.workflowStatus || 'missing'}, items=${fullBatch?.items?.length ?? 0}`, OPTIONS)
|
`workflow=${fullBatch?.workflowStatus || 'missing'}, items=${fullBatch?.items?.length ?? 0}`, OPTIONS)
|
||||||
if (!await uploadCompletionEvidence(context, third, 'coload-full')) return
|
if (!await uploadCompletionEvidence(context, fullPda.taskId, third.warehouseId, 'coload-full')) return
|
||||||
const fullCompleteResponse = await context.request('POST', `${PDA}/tasks/${fullPda.taskId}/complete`, {
|
const fullCompleteResponse = await context.request('POST', `${PDA}/tasks/${fullPda.taskId}/complete`, {
|
||||||
expectedTaskVersion: Number(fullBatch?.version),
|
expectedTaskVersion: Number(fullBatch?.version),
|
||||||
operationId: `${context.marker}-coload-full-complete`.slice(0, 128), remark: context.marker,
|
operationId: `${context.marker}-coload-full-complete`.slice(0, 128), remark: context.marker,
|
||||||
@@ -870,7 +915,7 @@ export async function run(context) {
|
|||||||
}, undefined, { 'Idempotency-Key': `${context.marker}-normal-batch`.slice(0, 128) })
|
}, undefined, { 'Idempotency-Key': `${context.marker}-normal-batch`.slice(0, 128) })
|
||||||
const normalBatch = context.apiSuccess(MODULE, '未成组普通单PDA批量拣货不回归', normalBatchResponse,
|
const normalBatch = context.apiSuccess(MODULE, '未成组普通单PDA批量拣货不回归', normalBatchResponse,
|
||||||
evidence(normalBatchResponse), OPTIONS)
|
evidence(normalBatchResponse), OPTIONS)
|
||||||
if (!await uploadCompletionEvidence(context, seventh, 'normal-split')) return
|
if (!await uploadCompletionEvidence(context, normalPda.taskId, seventh.warehouseId, 'normal-split')) return
|
||||||
const normalCompleteResponse = await context.request('POST', `${PDA}/tasks/${normalPda.taskId}/complete`, {
|
const normalCompleteResponse = await context.request('POST', `${PDA}/tasks/${normalPda.taskId}/complete`, {
|
||||||
expectedTaskVersion: Number(normalBatch?.version),
|
expectedTaskVersion: Number(normalBatch?.version),
|
||||||
operationId: `${context.marker}-normal-complete`.slice(0, 128), remark: context.marker,
|
operationId: `${context.marker}-normal-complete`.slice(0, 128), remark: context.marker,
|
||||||
|
|||||||
@@ -415,20 +415,31 @@ export async function run(context) {
|
|||||||
let measuredResponse = await context.request('GET', measurementPath)
|
let measuredResponse = await context.request('GET', measurementPath)
|
||||||
let measured = context.apiSuccess(MODULE, '回查覆盖后的尺寸与体积结果', measuredResponse, '', OPTIONS)
|
let measured = context.apiSuccess(MODULE, '回查覆盖后的尺寸与体积结果', measuredResponse, '', OPTIONS)
|
||||||
let measuredItem = measured?.items?.find((item) => asId(item.id) === asId(firstScan.itemId))
|
let measuredItem = measured?.items?.find((item) => asId(item.id) === asId(firstScan.itemId))
|
||||||
const weightedOuter = measurementProfile?.outer || { length: 64, width: 54, height: 54 }
|
const weightedOuter = {
|
||||||
const unitVolume = halfUp(
|
length: Number(measuredItem?.calculatedOuterLengthCm ?? measuredItem?.outerLengthCm),
|
||||||
weightedOuter.length * weightedOuter.width * weightedOuter.height / 1_000_000, 3)
|
width: Number(measuredItem?.calculatedOuterWidthCm ?? measuredItem?.outerWidthCm),
|
||||||
|
height: Number(measuredItem?.calculatedOuterHeightCm ?? measuredItem?.outerHeightCm),
|
||||||
|
}
|
||||||
|
const calculatedVolume = weightedOuter.length * weightedOuter.width * weightedOuter.height / 1_000_000
|
||||||
|
const usesMatchedAddRule = String(measuredItem?.calculationRuleVersion || '').startsWith('CUSTOMER_DIMENSION_')
|
||||||
|
const unitVolume = usesMatchedAddRule ? halfUp(calculatedVolume, 3) : ceiling(calculatedVolume, 4)
|
||||||
const totalVolume = unitVolume * originalPackageCount
|
const totalVolume = unitVolume * originalPackageCount
|
||||||
const volumeWeight = ceiling(totalVolume * 167, 3)
|
const volumeWeight = ceiling(totalVolume * 167, 3)
|
||||||
|
// Device scans retain the measured girth while the weighted dimensions are
|
||||||
|
// used for volume. The subsequent manual draft explicitly keeps ADD so the
|
||||||
|
// same customer dimension rule remains frozen for review.
|
||||||
const girth = ceiling(replacement.length + 2 * (replacement.width + replacement.height), 3)
|
const girth = ceiling(replacement.length + 2 * (replacement.width + replacement.height), 3)
|
||||||
context.assert(MODULE, '覆盖尺寸、箱数和服务端体积公式正确', measuredResponse,
|
context.assert(MODULE, '覆盖尺寸、箱数和服务端体积公式正确', measuredResponse,
|
||||||
measuredItem && closeTo(measuredItem.lengthCm, replacement.length)
|
measuredItem && closeTo(measuredItem.lengthCm, replacement.length)
|
||||||
&& closeTo(measuredItem.widthCm, replacement.width) && closeTo(measuredItem.heightCm, replacement.height)
|
&& closeTo(measuredItem.widthCm, replacement.width) && closeTo(measuredItem.heightCm, replacement.height)
|
||||||
&& measuredItem.scannedShipmentId === replacementShipmentId
|
&& measuredItem.scannedShipmentId === replacementShipmentId
|
||||||
&& Number(measuredItem.confirmedPackageCount) === originalPackageCount
|
&& Number(measuredItem.confirmedPackageCount) === originalPackageCount
|
||||||
&& closeTo(measuredItem.outerLengthCm, weightedOuter.length)
|
&& closeTo(measuredItem.calculatedOuterLengthCm, weightedOuter.length)
|
||||||
&& closeTo(measuredItem.outerWidthCm, weightedOuter.width)
|
&& closeTo(measuredItem.calculatedOuterWidthCm, weightedOuter.width)
|
||||||
&& closeTo(measuredItem.outerHeightCm, weightedOuter.height)
|
&& closeTo(measuredItem.calculatedOuterHeightCm, weightedOuter.height)
|
||||||
|
&& (measuredItem.coefficientMode == null
|
||||||
|
|| ['ACTUAL', 'ADD', 'MULTIPLY'].includes(measuredItem.coefficientMode))
|
||||||
|
&& Boolean(measuredItem.calculationRuleVersion)
|
||||||
&& closeTo(measuredItem.unitVolumeCbm, unitVolume) && closeTo(measuredItem.totalVolumeCbm, totalVolume)
|
&& closeTo(measuredItem.unitVolumeCbm, unitVolume) && closeTo(measuredItem.totalVolumeCbm, totalVolume)
|
||||||
&& closeTo(measuredItem.volumeWeightKg, volumeWeight) && closeTo(measuredItem.girthCm, girth)
|
&& closeTo(measuredItem.volumeWeightKg, volumeWeight) && closeTo(measuredItem.girthCm, girth)
|
||||||
&& closeTo(measured.actualVolumeCbm, totalVolume) && closeTo(measured.volumeWeightKg, volumeWeight),
|
&& closeTo(measured.actualVolumeCbm, totalVolume) && closeTo(measured.volumeWeightKg, volumeWeight),
|
||||||
@@ -532,7 +543,10 @@ export async function run(context) {
|
|||||||
intelligentImageFileId: item.intelligentImageFileId,
|
intelligentImageFileId: item.intelligentImageFileId,
|
||||||
overLimitWarning: item.overLimitWarning ?? null,
|
overLimitWarning: item.overLimitWarning ?? null,
|
||||||
outerLengthCm: item.outerLengthCm ?? null, outerWidthCm: item.outerWidthCm ?? null,
|
outerLengthCm: item.outerLengthCm ?? null, outerWidthCm: item.outerWidthCm ?? null,
|
||||||
outerHeightCm: item.outerHeightCm ?? null, remark: context.marker,
|
outerHeightCm: item.outerHeightCm ?? null,
|
||||||
|
coefficientMode: String(item.calculationRuleVersion || '').startsWith('CUSTOMER_DIMENSION_')
|
||||||
|
? 'ADD' : item.coefficientMode ?? null,
|
||||||
|
remark: context.marker,
|
||||||
}))
|
}))
|
||||||
: [{
|
: [{
|
||||||
inboundDetailId, itemNo: measuredItem.itemNo,
|
inboundDetailId, itemNo: measuredItem.itemNo,
|
||||||
@@ -541,7 +555,10 @@ export async function run(context) {
|
|||||||
intelligentImageFileId: measuredItem.intelligentImageFileId,
|
intelligentImageFileId: measuredItem.intelligentImageFileId,
|
||||||
overLimitWarning: measuredItem.overLimitWarning ?? null,
|
overLimitWarning: measuredItem.overLimitWarning ?? null,
|
||||||
outerLengthCm: measuredItem.outerLengthCm ?? null, outerWidthCm: measuredItem.outerWidthCm ?? null,
|
outerLengthCm: measuredItem.outerLengthCm ?? null, outerWidthCm: measuredItem.outerWidthCm ?? null,
|
||||||
outerHeightCm: measuredItem.outerHeightCm ?? null, remark: context.marker,
|
outerHeightCm: measuredItem.outerHeightCm ?? null,
|
||||||
|
coefficientMode: String(measuredItem.calculationRuleVersion || '').startsWith('CUSTOMER_DIMENSION_')
|
||||||
|
? 'ADD' : measuredItem.coefficientMode ?? null,
|
||||||
|
remark: context.marker,
|
||||||
}]
|
}]
|
||||||
const draftPayload = {
|
const draftPayload = {
|
||||||
measurementId: measured.measurementId, version: Number(measured.version),
|
measurementId: measured.measurementId, version: Number(measured.version),
|
||||||
|
|||||||
Reference in New Issue
Block a user