fix(lastleg): accept cabinet pallet identities
This commit is contained in:
+6
-7
@@ -255,7 +255,7 @@ public class ContainerInboundPlanConversionCoordinator {
|
||||
String fbaCode = trimToNull(order.fbaWarehouseCode());
|
||||
if (fbaCode != null && !"ONE_CLICK_DROPSHIP".equals(order.orderType())) {
|
||||
for (String palletCode : sourcePalletNos) {
|
||||
PalletCodeParts parts = palletCodeParts(preview.containerNo(), palletCode);
|
||||
PalletCodeParts parts = palletCodeParts(palletCode);
|
||||
String previousPrefix = prefixByFba.putIfAbsent(fbaCode, parts.prefix());
|
||||
if (previousPrefix != null && !previousPrefix.equals(parts.prefix())) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "同一 FBA 仓的头程托盘码内部编码不一致");
|
||||
@@ -487,17 +487,16 @@ public class ContainerInboundPlanConversionCoordinator {
|
||||
int localSequenceNo, int globalSequenceNo, String businessKey,
|
||||
String fbaWarehouseCode, String palletCode) { }
|
||||
|
||||
private static PalletCodeParts palletCodeParts(String containerNo, String palletCode) {
|
||||
String stableContainerNo = trimToNull(containerNo);
|
||||
private static PalletCodeParts palletCodeParts(String palletCode) {
|
||||
String stablePalletCode = trimToNull(palletCode);
|
||||
if (stableContainerNo == null || stablePalletCode == null) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "集装箱号或头程托盘码缺失");
|
||||
if (stablePalletCode == null) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "头程托盘码缺失");
|
||||
}
|
||||
Matcher matcher = Pattern.compile("^(" + Pattern.quote(stableContainerNo) + "-.+)-(\\d{3})$")
|
||||
Matcher matcher = Pattern.compile("^(D\\d{4,}-.+)-(\\d{3})$")
|
||||
.matcher(stablePalletCode);
|
||||
if (!matcher.matches()) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
"头程托盘码不符合‘集装箱号-FBA内部编码-下标’规则");
|
||||
"头程托盘码不符合‘柜次号-FBA内部编码-下标’规则");
|
||||
}
|
||||
return new PalletCodeParts(matcher.group(1), Integer.parseInt(matcher.group(2)));
|
||||
}
|
||||
|
||||
+4
-4
@@ -345,7 +345,7 @@ class ContainerInboundPlanConversionCoordinatorTest {
|
||||
when(plans.previewContainer(BOOKING_ID)).thenReturn(preview);
|
||||
when(numbers.next(eq("INBOUND_PLAN"), any(String.class), any(Long.class))).thenReturn("IP-ACY2");
|
||||
when(plans.sourcePalletNos("100", 11L, "FIRST_LEG")).thenReturn(List.of(
|
||||
"MSCU6639870-101-001", "MSCU6639870-101-002"));
|
||||
"D0004-101-001", "D0004-101-002"));
|
||||
|
||||
ContainerPlanConversionCommand command = ReflectionTestUtils.invokeMethod(coordinator, "prepareCommand",
|
||||
1L, 2L, BOOKING_ID, "FIRST-KEY", request,
|
||||
@@ -359,7 +359,7 @@ class ContainerInboundPlanConversionCoordinatorTest {
|
||||
throw new AssertionError(exception);
|
||||
}
|
||||
|
||||
assertEquals(List.of("MSCU6639870-101-001", "MSCU6639870-101-002", "MSCU6639870-101-003"),
|
||||
assertEquals(List.of("D0004-101-001", "D0004-101-002", "D0004-101-003"),
|
||||
preparation.items().stream().map(ContainerInboundPlanConversionCoordinator.PreparedItem::palletCode).toList());
|
||||
}
|
||||
|
||||
@@ -376,7 +376,7 @@ class ContainerInboundPlanConversionCoordinatorTest {
|
||||
1, 1, 3, 2, 1, 3, 3, 3, List.of(order))));
|
||||
when(plans.previewContainer(BOOKING_ID)).thenReturn(preview);
|
||||
when(plans.sourcePalletNos("100", 11L, "FIRST_LEG")).thenReturn(List.of(
|
||||
"MSCU6639870-101-001", "MSCU6639870-101-002"));
|
||||
"D10000-101-001", "D10000-101-002"));
|
||||
when(plans.existingPlanIdentity("100", 11L))
|
||||
.thenReturn(new InboundPlanService.PlanIdentity(21L, "IP-MANUAL", 3));
|
||||
|
||||
@@ -434,7 +434,7 @@ class ContainerInboundPlanConversionCoordinatorTest {
|
||||
private InboundPlanResponse plan(String planNo, List<InboundPlanResponse.Pallet> pallets) {
|
||||
return new InboundPlanResponse(21L, planNo, "100", 11L, "FL-WH-1", "验收客户", "ONE_CLICK_DROPSHIP",
|
||||
"MSCU6639870", "US-LAX-01",
|
||||
"Los Angeles", "PENDING_ASSIGNMENT", 1, 1, BigDecimal.ONE, 10, 0,
|
||||
"Los Angeles", "PENDING_ASSIGNMENT", 1, 1, BigDecimal.TEN, new BigDecimal("8"), BigDecimal.ONE, 10, 0,
|
||||
List.of(), pallets, List.of(), 1, LocalDate.of(2026, 9, 10), "WOOD", false,
|
||||
false, false, "MANUAL", "自动生成", null, null);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function containerFieldGaps(record, { requireContainerNo = false } = {})
|
||||
cabinetNo: isNonBlank,
|
||||
warehouseId: isPositiveId,
|
||||
warehouseName: isNonBlank,
|
||||
channelCode: isNonBlank,
|
||||
channelGroupCode: isNonBlank,
|
||||
containerType: isNonBlank,
|
||||
capacityCbm: (value) => Number(value) > 0,
|
||||
maxWeightKg: (value) => Number(value) > 0,
|
||||
|
||||
@@ -82,7 +82,7 @@ test('正式 DTO/OpenAPI 暴露矩阵使用的稳定字段', () => {
|
||||
|
||||
test('字段校验器接受正式完整投影并精确暴露缺字段', () => {
|
||||
const samples = [
|
||||
[containerFieldGaps, { id: ids.container, cabinetNo: 'CAB-1', containerNo: 'MSCU1234567', warehouseId: ids.id, warehouseName: '宁波仓', channelCode: 'EXX', containerType: '40HQ', capacityCbm: 68, maxWeightKg: 19500, planStatus: 'EDITING' }, { requireContainerNo: true }, 'warehouseName'],
|
||||
[containerFieldGaps, { id: ids.container, cabinetNo: 'CAB-1', containerNo: 'MSCU1234567', warehouseId: ids.id, warehouseName: '宁波仓', channelGroupCode: 'EXX', containerType: '40HQ', capacityCbm: 68, maxWeightKg: 19500, planStatus: 'EDITING' }, { requireContainerNo: true }, 'warehouseName'],
|
||||
[preloadFieldGaps, { planId: ids.plan, containerBookingId: ids.container, preloadNo: 'PL-1', cabinetNo: 'CAB-1', customers: [{ accountId: ids.id, customerName: '测试客户' }], warehouseName: '宁波仓', containerType: '40HQ', status: 'EDITING', skuCount: 1, totalPackageCount: 1, totalVolumeCbm: 1, totalGrossWeightKg: 10, createdAt: at }, undefined, 'customers'],
|
||||
[loadingListFieldGaps, { planId: ids.plan, containerBookingId: ids.container, loadingListNo: 'LL-1', containerNo: 'MSCU1234567', cabinetNo: 'CAB-1', warehouseName: '宁波仓', status: 'COMPLETED', totalPackageCount: 1, totalGrossWeightKg: 10, totalNetWeightKg: 9, totalVolumeCbm: 1, totalQuantity: 2, createdAt: at }, undefined, 'containerNo'],
|
||||
[oceanBookingFieldGaps, { id: ids.id, bookingNo: 'BK-1', status: 'RELEASED', vesselName: 'VESSEL', voyageNo: 'V1', estimatedSailingAt: at, estimatedArrivalAt: at, customerId: ids.id, customerName: '测试客户', shippingCompanyId: null, shippingCompanyName: null, bookingAgentId: ids.id, bookingAgentName: '代理' }, undefined, 'voyageNo'],
|
||||
|
||||
@@ -110,7 +110,7 @@ async function verifyPalletLabels(context, fixture, loadingPlanId) {
|
||||
const index = Number(label?.palletIndex)
|
||||
return id(label?.containerNo) === id(fixture.containerNo)
|
||||
&& id(label?.destinationWarehouseInternalCode)
|
||||
&& id(label?.palletCode) === `${fixture.containerNo}-${label.destinationWarehouseInternalCode}-${String(index).padStart(3, '0')}`
|
||||
&& id(label?.palletCode) === `${fixture.cabinetNo}-${label.destinationWarehouseInternalCode}-${String(index).padStart(3, '0')}`
|
||||
})
|
||||
const indexesValid = (manifest?.groups || []).every((group) =>
|
||||
(group?.labels || []).every((label, index) => Number(label?.palletIndex) === index + 1))
|
||||
@@ -580,6 +580,7 @@ export async function run(context) {
|
||||
destinationWarehouseId: id(fixture.destinationWarehouseId),
|
||||
destinationWarehouseCode: fixture.destinationWarehouseCode || null,
|
||||
palletLabelManifest,
|
||||
cabinetNo: fixture.cabinetNo || null,
|
||||
containerNo: fixture.containerNo || null,
|
||||
sourceStatus: 'LOADED',
|
||||
verification: 'PRODUCED_IN_COMPLETE_TRANSACTION_PENDING_BRIDGE_CONSUMER_HTTP',
|
||||
|
||||
@@ -1715,7 +1715,8 @@ export async function run(context) {
|
||||
missingTypeVoidedEvidenceId: id(missingTypeTarget?.id), replacementEvidenceFileId: replacementFileId,
|
||||
immutableEvidenceId: id(immutablePhoto?.id), immutableEvidenceVersion: Number(immutablePhoto?.version),
|
||||
postLoadedFileId,
|
||||
oceanBookingId: bookingId, loadingPlanId, containerBookingId, containerNo, warehouseId,
|
||||
oceanBookingId: bookingId, loadingPlanId, containerBookingId,
|
||||
cabinetNo: simulation.cabinetNo, containerNo, warehouseId,
|
||||
canonicalPdaTaskId: canonicalTaskId,
|
||||
pickingTaskId: canonicalTaskId, inboundVersion: loadingVersion,
|
||||
atomicBatchVerified: awaitingReview && atomicBatchReplayVerified,
|
||||
|
||||
@@ -81,10 +81,13 @@ test('measurement and loading pass destination identity into the LOADED producer
|
||||
const finalize = read('phase3-loading-finalize.mjs')
|
||||
assert.match(measurement, /destinationType: context\.createdResources\?\.orders\?\.destinationType/)
|
||||
assert.match(loading, /destinationType: initial\?\.summary\?\.destinationType \|\| source\?\.destinationType/)
|
||||
assert.match(loading, /cabinetNo: simulation\.cabinetNo, containerNo/)
|
||||
assert.match(loading, /fulfillmentType: source\?\.fulfillmentType \|\| 'STANDARD'/)
|
||||
assert.match(loading, /Array\.isArray\(incrementalFixture\?\.skuCodes\)/)
|
||||
assert.match(loading, /Array\.isArray\(incrementalFixture\?\.skuLines\)/)
|
||||
assert.match(finalize, /destinationType: fixture\.destinationType \|\| null/)
|
||||
assert.match(finalize, /cabinetNo: fixture\.cabinetNo \|\| null/)
|
||||
assert.match(finalize, /`\$\{fixture\.cabinetNo\}-\$\{label\.destinationWarehouseInternalCode\}-/)
|
||||
assert.match(finalize, /fulfillmentType: fixture\.fulfillmentType \|\| 'STANDARD'/)
|
||||
assert.match(finalize, /skuCodes: Array\.isArray\(fixture\.skuCodes\) \? fixture\.skuCodes : \[\]/)
|
||||
assert.match(finalize, /skuLines: Array\.isArray\(fixture\.skuLines\) \? fixture\.skuLines : \[\]/)
|
||||
|
||||
@@ -103,6 +103,9 @@ test('PDA module covers the container-number receiving contract and excludes out
|
||||
for (const forbidden of ['/outbound', '/dispatch', '/delivery', '/internal/']) assert.equal(source.includes(forbidden), false, forbidden)
|
||||
assert.match(source, /同键同载荷重放/)
|
||||
assert.match(source, /inbound-plans\/container-preview/)
|
||||
assert.match(source, /orders\/container-arrival-times\/query/)
|
||||
assert.match(source, /container-summaries\/\$\{encodeURIComponent\(bookingId\)\}\/expected-arrival/)
|
||||
assert.match(source, /estimatedWarehouseArrivalAt, expectedVersion: Number\(arrivalTime\.version\)/)
|
||||
assert.match(source, /snapshotVersion: Number\(preview\.snapshotVersion\)/)
|
||||
assert.match(source, /snapshotHash: String\(preview\.snapshotHash\)/)
|
||||
assert.match(source, /rememberGeneratedPlans\(context, plans\)/)
|
||||
|
||||
@@ -336,6 +336,33 @@ async function convertReceivingContainers(context, producers) {
|
||||
`sourceContainerBookingId=${id(summary?.sourceContainerBookingId) || 'missing'}, allPlansReady=${summary?.allPlansReady === true}, orders=${totalOrders}, inboundCodes=${inboundCodes}, plans=${existingPlans}, pending=${pendingPlans}; ${trace(summaryResponse)}`, OPTIONS)
|
||||
if (!convertible) return false
|
||||
const bookingId = id(summary.sourceContainerBookingId)
|
||||
const arrivalQueryResponse = await context.requestOverseas('POST',
|
||||
'/api/platform/v1/last-leg/orders/container-arrival-times/query',
|
||||
{ sourceContainerBookingIds: [bookingId] }, {}, context.overseasToken)
|
||||
const arrivalTimes = success(arrivalQueryResponse) && Array.isArray(arrivalQueryResponse.body?.data)
|
||||
? arrivalQueryResponse.body.data : []
|
||||
let arrivalTime = arrivalTimes.find((item) => id(item?.sourceContainerBookingId) === bookingId)
|
||||
context.addStep(MODULE, `读取整柜预计到仓时间 ${containerNo}`, arrivalQueryResponse,
|
||||
Boolean(arrivalTime) && Number.isInteger(Number(arrivalTime?.version)),
|
||||
`sourceContainerBookingId=${bookingId}, estimatedWarehouseArrivalAt=${arrivalTime?.estimatedWarehouseArrivalAt || 'missing'}, version=${arrivalTime?.version ?? 'missing'}; ${trace(arrivalQueryResponse)}`,
|
||||
OPTIONS)
|
||||
if (!arrivalTime) return false
|
||||
if (!nonBlank(arrivalTime.estimatedWarehouseArrivalAt)) {
|
||||
const estimatedWarehouseArrivalAt = new Date(Date.now() + 2 * 86400000).toISOString().slice(0, 19)
|
||||
const arrivalUpdateResponse = await context.requestOverseas('PUT',
|
||||
`/api/platform/v1/last-leg/orders/container-summaries/${encodeURIComponent(bookingId)}/expected-arrival`,
|
||||
{ estimatedWarehouseArrivalAt, expectedVersion: Number(arrivalTime.version),
|
||||
remark: `${context.marker}-overseas-inbound-plan-prerequisite` },
|
||||
{ 'Idempotency-Key': `${context.marker}-expected-arrival-${containerNo}`.slice(0, 128) },
|
||||
context.overseasToken)
|
||||
arrivalTime = success(arrivalUpdateResponse) ? arrivalUpdateResponse.body?.data : null
|
||||
context.addStep(MODULE, `维护整柜预计到仓时间 ${containerNo}`, arrivalUpdateResponse,
|
||||
id(arrivalTime?.sourceContainerBookingId) === bookingId
|
||||
&& nonBlank(arrivalTime?.estimatedWarehouseArrivalAt),
|
||||
`sourceContainerBookingId=${bookingId}, estimatedWarehouseArrivalAt=${arrivalTime?.estimatedWarehouseArrivalAt || 'missing'}, version=${arrivalTime?.version ?? 'missing'}; ${trace(arrivalUpdateResponse)}`,
|
||||
OPTIONS)
|
||||
if (!arrivalTime || !nonBlank(arrivalTime.estimatedWarehouseArrivalAt)) return false
|
||||
}
|
||||
const previewPath = `/api/platform/v1/last-leg/inbound-plans/container-preview?sourceContainerBookingId=${encodeURIComponent(bookingId)}`
|
||||
const previewResponse = await context.requestOverseas('GET', previewPath,
|
||||
undefined, {}, context.overseasToken)
|
||||
|
||||
Reference in New Issue
Block a user