fix: complete overseas fulfillment acceptance

This commit is contained in:
2026-09-07 14:03:00 +08:00
parent fcf1771967
commit 2c5fda33e7
8 changed files with 44 additions and 4 deletions
@@ -128,7 +128,7 @@ public class DeliveryTransferService {
outbound.setOutboundNo(orderNoGenerator.next("OUTBOUND", dispatch.getOverseasWarehouseId(), outbound.getId()));
outbound.setDispatchOrderId(dispatch.getId()); outbound.setDispatchNo(dispatch.getDispatchNo());
outbound.setOverseasWarehouseId(dispatch.getOverseasWarehouseId()); outbound.setStatus("WAIT_OUTBOUND"); outbound.setOutboundType("DISPATCH_DELIVERY");
outbound.setLxCodes(items.stream().map(DispatchItem::getInboundCode).filter(StringUtils::hasText).map(code -> tailOrders.selectOne(new LambdaQueryWrapper<LastlegOrder>().eq(LastlegOrder::getCustomerId, customerId).eq(LastlegOrder::getInboundCode, code))).filter(Objects::nonNull).map(LastlegOrder::getLxCode).filter(StringUtils::hasText).distinct().collect(java.util.stream.Collectors.joining(",")));
outbound.setLxCodes(resolveLxCodes(customerId, items));
outbound.setPackageCount(items.stream().map(DispatchItem::getPackageCount).filter(Objects::nonNull).mapToInt(Integer::intValue).sum());
outbound.setPalletCount(items.size()); outbound.setIdempotencyKey(request.idempotencyKey().trim());
outbound.setCreatedBy(operatorId); outbound.setCreatedAt(now); outbound.setUpdatedAt(now);
@@ -562,6 +562,16 @@ public class DeliveryTransferService {
private DeliveryResponses.DeliveryOperationRow operationRow(DeliveryOperation o) { return new DeliveryResponses.DeliveryOperationRow(o.getId(), o.getCreatedAt(), o.getOperationNode(), firstText(o.getOperatorName(), o.getOperatorId()), o.getResult()); }
private void recordOperation(String customerId, Long deliveryId, String operatorId, String node, String result, LocalDateTime now) { DeliveryOperation operation = new DeliveryOperation(); operation.setCustomerId(customerId); operation.setDeliveryOrderId(deliveryId); operation.setOperationNode(node); operation.setOperatorId(operatorId); operation.setOperatorName(operatorId); operation.setResult(result); operation.setCreatedAt(now); deliveryOperations.insert(operation); }
private LastlegOrder firstTailOrder(String customerId, List<DispatchItem> items) { return items.stream().map(DispatchItem::getOrderId).filter(Objects::nonNull).map(tailOrders::selectById).filter(Objects::nonNull).filter(order -> customerId.equals(order.getCustomerId())).findFirst().orElse(null); }
private String resolveLxCodes(String customerId, List<DispatchItem> items) {
List<String> inboundCodes = items.stream().map(DispatchItem::getInboundCode)
.filter(StringUtils::hasText).map(String::trim).distinct().toList();
if (inboundCodes.isEmpty()) return "";
return tailOrders.selectList(new LambdaQueryWrapper<LastlegOrder>()
.eq(LastlegOrder::getCustomerId, customerId)
.in(LastlegOrder::getInboundCode, inboundCodes))
.stream().map(LastlegOrder::getLxCode).filter(StringUtils::hasText)
.map(String::trim).distinct().collect(java.util.stream.Collectors.joining(","));
}
private BigDecimal totalWeight(String customerId, List<DispatchItem> items) { List<Long> ids = items.stream().map(DispatchItem::getOrderId).filter(Objects::nonNull).distinct().toList(); if (ids.isEmpty()) return BigDecimal.ZERO; return tailOrderDetails.selectList(new LambdaQueryWrapper<LastlegOrderDetail>().eq(LastlegOrderDetail::getCustomerId, customerId).in(LastlegOrderDetail::getOrderId, ids)).stream().map(LastlegOrderDetail::getGrossWeightKg).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); }
private static String trim(String value) { return StringUtils.hasText(value) ? value.trim() : null; }
private static String required(String value, String message) {
@@ -140,6 +140,25 @@ class DeliveryTransferServiceTest {
assertTrue(!result.replayed());
}
@Test
void transferCollectsLxCodesWhenAnInboundCodeBelongsToMultipleTailOrders() {
DispatchOrder dispatch = dispatch(101L, "DSP-101", "TENANT-A");
when(dispatchOrders.lockGlobalByDispatchNo("DSP-101")).thenReturn(dispatch);
when(dispatchItems.findByDispatchOrder("TENANT-A", 101L)).thenReturn(List.of(
item(201L, 501L, "PLT-501", "ONT8", "Ontario", "IN-A", 12)));
LastlegOrder primary = new LastlegOrder();
primary.setCustomerId("TENANT-A"); primary.setInboundCode("IN-A"); primary.setLxCode("LX-1");
LastlegOrder derived = new LastlegOrder();
derived.setCustomerId("TENANT-A"); derived.setInboundCode("IN-A"); derived.setLxCode(null);
when(tailOrders.selectList(any())).thenReturn(List.of(primary, derived), List.of());
service.platformTransfer("DSP-101", "U-1", transferRequest("transfer-duplicate-inbound", null));
ArgumentCaptor<OutboundOrder> outbound = ArgumentCaptor.forClass(OutboundOrder.class);
verify(outboundOrders).insert(outbound.capture());
assertEquals("LX-1", outbound.getValue().getLxCodes());
}
@Test
void sameIdempotencyKeyReturnsExistingDocumentsWithoutWrites() {
DispatchOrder dispatch = dispatch(101L, "DSP-101", "TENANT-A");
@@ -30,6 +30,8 @@ test('module never borrows historical order or inventory candidates', () => {
test('relabel transfer and discard cover create replay conflict list detail and legal initial states', () => {
assert.match(source, /chargeAmount: 0\.01/)
assert.match(source, /cartonMark:/)
assert.match(source, /fbaCartonMark:/)
for (const token of [
'客户创建贴标申请', '客户贴标申请幂等重放', '贴标同幂等键异载荷返回A0409',
'客户查询自身贴标申请列表', '客户查询自身贴标申请详情', 'PENDING_PROCESSING',
@@ -101,6 +101,8 @@ async function verifyRelabel(context, token, order) {
shipmentNo: cargo.shipmentNo || `${context.marker}-RELABEL-SHIP`, trackingCode: cargo.fbaTrackingNo || null,
newSkuCode: `${cargo.skuCode}-NEW`.slice(0, 128), labelRequirement: context.marker,
relabelFileId: null, relabelFileName: null, relabelFileUrl: null,
cartonMark: `CM-${String(context.marker).slice(-32)}`,
fbaCartonMark: `FBA-${String(context.marker).slice(-32)}`,
}],
}
const created = await merchant(context, token, 'POST', '客户创建贴标申请', RELABEL, body)
@@ -46,7 +46,8 @@ test('P2 to P5 cover all frozen endpoints, replays and conflict boundaries witho
]) assert.ok(source.includes(endpoint), endpoint)
for (const marker of ['按原数计数', '同键重放', '同键异载荷A0409', '旧版本A0409',
'SUBMITTED', '移回原库位', 'WAIT_REVIEW', 'REWORK', 'APPROVE', 'COMPLETED',
'chargeAmount: 0.01', 'P5换标费用冻结后开放PDA作业', 'P5换标完成后钱包结算收敛']) {
'chargeAmount: 0.01', 'cartonMark:', 'fbaCartonMark:',
'P5换标费用冻结后开放PDA作业', 'P5换标完成后钱包结算收敛']) {
assert.ok(source.includes(marker), marker)
}
for (const forbidden of ['/outbound', '/returns/outbound', '/dispatch', '/delivery', '/internal/']) {
@@ -333,7 +333,9 @@ async function verifyRelabel(context, token, fixture) {
idempotencyKey: key(context, 'RELABEL-CREATE'),
items: [{ skuCode: inventory.skuCode, productNameZh: inventory.productName || '本轮库存商品',
totalPackageCount: 3, notifiedPackageCount: 3, shipmentNo: `SHIP-${context.marker}`.slice(0, 128),
newSkuCode: newSku, labelRequirement: context.marker }],
newSkuCode: newSku, labelRequirement: context.marker,
cartonMark: `CM-${String(context.marker).slice(-32)}`,
fbaCartonMark: `FBA-${String(context.marker).slice(-32)}` }],
}
const createdResponse = await context.requestOverseas('POST',
query(relabelBase, { customerId }), createBody)
@@ -30,6 +30,8 @@ test('钱包闭环覆盖充值、授信、采购冻结结算和换标结算释
"terminalStatus === 'COMPLETED'",
"'CANCELLED'",
'relabelPayload(context, warehouseId, suffix, 0.01)',
'cartonMark:',
'fbaCartonMark:',
]) assert.ok(source.includes(token), token)
assert.match(source, /actualBalance[\s\S]*frozenBalance[\s\S]*availableBalance[\s\S]*creditLimit[\s\S]*creditUsed[\s\S]*availableCredit/)
assert.match(source, /voucherFileId: id\(files\.find\(\(item\) => item\.category === 'VOUCHER'\)\?\.fileId\)/)
@@ -178,7 +178,9 @@ function relabelPayload(context, warehouseId, suffix, chargeAmount) {
remark: context.marker, idempotencyKey: `${context.marker}-${suffix}`.slice(0, 255),
items: [{ skuCode: `OLD-${value}-${suffix}`.slice(0, 128), productNameZh: `${context.marker} 换标商品`,
totalPackageCount: 1, notifiedPackageCount: 1, shipmentNo: `SHIP-${value}-${suffix}`.slice(0, 128),
newSkuCode: `NEW-${value}-${suffix}`.slice(0, 128), labelRequirement: context.marker }],
newSkuCode: `NEW-${value}-${suffix}`.slice(0, 128), labelRequirement: context.marker,
cartonMark: `CM-${value}-${suffix}`.slice(0, 255),
fbaCartonMark: `FBA-${value}-${suffix}`.slice(0, 255) }],
}
}