This commit is contained in:
2026-09-18 16:48:30 +08:00
parent 7d6ca17c02
commit fd27a0ed21
4 changed files with 177 additions and 100 deletions
@@ -24,6 +24,7 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
@@ -58,17 +59,20 @@ public class ReceivingContainerController {
@GetMapping("/{containerNo}")
@Operation(summary = "Get one receiving container without exposing plan or allocation IDs")
public ApiResponse<ReceivingContainerResponses.Session> detail(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String containerNo) {
@PathVariable String containerNo,
@RequestParam String sourceContainerBookingId) {
PdaWarehouseIdentity identity = identity(user);
return ApiResponse.success(service.detail(identity.operatorKey(), identity.overseasWarehouseId(), containerNo));
return ApiResponse.success(service.detail(identity.operatorKey(), identity.overseasWarehouseId(),
containerNo, sourceContainerBookingId));
}
@PostMapping("/{containerNo}/prepare")
public ApiResponse<ReceivingContainerResponses.Session> prepare(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String containerNo, @Valid @RequestBody ReceivingContainerRequests.Action request) {
@PathVariable String containerNo, @RequestParam String sourceContainerBookingId,
@Valid @RequestBody ReceivingContainerRequests.Action request) {
PdaWarehouseIdentity identity = identity(user);
return ApiResponse.success(service.prepare(identity.operatorKey(), identity.operatorName(), identity.overseasWarehouseId(),
containerNo, request));
containerNo, sourceContainerBookingId, request));
}
@PostMapping(value = "/current/unload-photos/files", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@@ -62,6 +62,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
@@ -142,14 +143,16 @@ public class ReceivingContainerService {
.map(value -> candidate(value, current)).toList();
}
public ReceivingContainerResponses.Session detail(String operatorKey, long overseasWarehouseId, String containerNo) {
requireReadyContainer(containerNo, overseasWarehouseId, false);
List<InboundPlan> containerPlans = requirePlans(containerNo);
public ReceivingContainerResponses.Session detail(String operatorKey, long overseasWarehouseId, String containerNo,
String sourceContainerBookingId) {
requireReadyContainer(containerNo, sourceContainerBookingId, overseasWarehouseId, false);
List<InboundPlan> containerPlans = requirePlans(sourceContainerBookingId, containerNo);
requireMaterializedPlans(containerPlans);
if (!warehouseEligible(containerPlans, overseasWarehouseId)) throw conflict("当前柜已分配至其他海外接柜仓,无法在本仓操作");
ReceivingContainerSession current = sessions.findActiveByOperator(activeOperatorKey(operatorKey));
requireSessionWarehouse(current, overseasWarehouseId);
if (current != null && !containerNo.equals(current.getContainerNo())) {
if (current != null && (!containerNo.equals(current.getContainerNo())
|| !sourceContainerBookingId.equals(current.getSourceContainerBookingId()))) {
throw conflict("Current account is locked to another selected container");
}
ReceivingContainerSession reserved = sessions.findActiveByContainer(activeContainerKey(containerNo));
@@ -158,15 +161,17 @@ public class ReceivingContainerService {
}
WarehouseResourceGateway.WarehouseReference receivingWarehouse =
warehouse.requireActiveSharedWarehouse(overseasWarehouseId);
return snapshot(current != null && containerNo.equals(current.getContainerNo()) ? current : waitingView(
operatorKey, receivingWarehouse, containerNo), containerPlans);
return snapshot(current != null && containerNo.equals(current.getContainerNo())
&& sourceContainerBookingId.equals(current.getSourceContainerBookingId()) ? current : waitingView(
operatorKey, receivingWarehouse, sourceContainerBookingId, containerNo), containerPlans);
}
@Transactional
public ReceivingContainerResponses.Session prepare(String operatorKey, String operatorName, long overseasWarehouseId, String containerNo,
String sourceContainerBookingId,
ReceivingContainerRequests.Action request) {
PrepareCommand command = new PrepareCommand(containerNo, request.idempotencyKey());
ContainerReadiness readiness = requireReadyContainer(containerNo, overseasWarehouseId, true);
PrepareCommand command = new PrepareCommand(sourceContainerBookingId, containerNo, request.idempotencyKey());
ContainerReadiness readiness = requireReadyContainer(containerNo, sourceContainerBookingId, overseasWarehouseId, true);
ReceivingContainerOperation replay = replay(operatorKey, "PREPARE", request.idempotencyKey(), command);
if (replay != null) {
ReceivingContainerResponses.Session result = read(replay, ReceivingContainerResponses.Session.class);
@@ -175,13 +180,14 @@ public class ReceivingContainerService {
}
return result;
}
List<InboundPlan> containerPlans = requirePlans(containerNo);
List<InboundPlan> containerPlans = requirePlans(sourceContainerBookingId, containerNo);
requireMaterializedPlans(containerPlans);
if (!warehouseEligible(containerPlans, overseasWarehouseId)) throw conflict("当前柜已分配至其他海外接柜仓,无法在本仓操作");
String activeOperatorKey = activeOperatorKey(operatorKey);
ReceivingContainerSession active = sessions.lockActiveByOperator(activeOperatorKey);
requireSessionWarehouse(active, overseasWarehouseId);
if (active != null && !containerNo.equals(active.getContainerNo())) {
if (active != null && (!containerNo.equals(active.getContainerNo())
|| !sourceContainerBookingId.equals(active.getSourceContainerBookingId()))) {
throw conflict("Finish the current container before selecting another one");
}
if (active == null && sessions.lockActiveByContainer(activeContainerKey(containerNo)) != null) {
@@ -229,7 +235,7 @@ public class ReceivingContainerService {
List<ReceivingContainerResponses.SignedPhoto> photos = validateSignPhotos(request);
List<Long> photoFileIds = photos.stream().map(photo -> positiveFileId(photo.fileId())).toList();
photoFileIds.forEach(fileId -> unloadPhotos.validate(PLATFORM_SCOPE, session.getId(), fileId));
List<InboundPlan> containerPlans = requirePlans(session.getContainerNo());
List<InboundPlan> containerPlans = requirePlans(session.getSourceContainerBookingId(), session.getContainerNo());
requireMaterializedPlans(containerPlans);
LocalDateTime now = LocalDateTime.now();
session.setSignPhotos(write(photos)); session.setStatus("SIGNED"); session.setSignedAt(now);
@@ -243,7 +249,7 @@ public class ReceivingContainerService {
public ReceivingContainerResponses.Session current(String operatorKey) {
ReceivingContainerSession session = requireCurrent(operatorKey, false);
return snapshot(session, requirePlans(session.getContainerNo()));
return snapshot(session, requirePlans(session.getSourceContainerBookingId(), session.getContainerNo()));
}
public UnloadPhotoFileGateway.DownloadedFile downloadCurrentPhoto(String operatorKey, String fileId) {
@@ -276,10 +282,11 @@ public class ReceivingContainerService {
public ReceivingContainerResponses.CartonResolved resolveCarton(String operatorKey,
ReceivingContainerRequests.CartonResolve request) {
ReceivingContainerSession session = requireSigned(operatorKey);
List<InboundRef> currentRefs = refs(session.getContainerNo());
List<InboundRef> currentRefs = refs(session.getSourceContainerBookingId(), session.getContainerNo());
String sourcePalletNo = dropshipSourcePallet(currentRefs, session.getContainerNo(), request.cartonMark());
if (sourcePalletNo != null) {
List<DropshipPalletRef> palletRefs = dropshipPalletRefs(session.getContainerNo(), sourcePalletNo);
List<DropshipPalletRef> palletRefs = dropshipPalletRefs(
session.getSourceContainerBookingId(), session.getContainerNo(), sourcePalletNo);
requireCompleteManifest(session.getContainerNo(), sourcePalletNo);
List<InboundRef> group = palletRefs.stream().map(DropshipPalletRef::ref).distinct().toList();
InboundRef first = group.get(0);
@@ -306,14 +313,16 @@ public class ReceivingContainerService {
}
carton = new CartonRef(matches.get(0));
} else if (matches.size() == 1) {
carton = requireCartonRef(matches.get(0).order().getCustomerId(), session.getContainerNo(), request.cartonMark());
carton = requireCartonRef(matches.get(0).order().getCustomerId(),
session.getSourceContainerBookingId(), session.getContainerNo(), request.cartonMark());
} else {
carton = requireUnmatchedFbaCartonPrefix(inboundCode);
}
InboundRef ref = carton.ref();
if ("FBA".equals(destinationType(ref.line(), ref.order()))
&& !"ONE_CLICK_DROPSHIP".equals(ref.order().getOrderType())) {
List<InboundRef> group = requireFbaPlanRefs(session.getContainerNo(), ref);
List<InboundRef> group = requireFbaPlanRefs(
session.getSourceContainerBookingId(), session.getContainerNo(), ref);
List<String> inboundCodes = group.stream().map(value -> value.line().getInboundCode()).distinct().sorted().toList();
int total = group.stream().mapToInt(value -> value(value.line().getRequiredPackageCount())).sum();
int received = group.stream().mapToInt(this::receivedLinePackages).sum();
@@ -339,10 +348,11 @@ public class ReceivingContainerService {
public ReceivingContainerResponses.PalletResolved resolvePallet(String operatorKey,
ReceivingContainerRequests.PalletResolve request) {
ReceivingContainerSession session = requireSigned(operatorKey);
InboundRef ref = requireInbound(session.getContainerNo(), request.inboundCode());
InboundRef ref = requireInbound(session.getSourceContainerBookingId(),
session.getContainerNo(), request.inboundCode());
List<InboundRef> group = "FBA".equals(destinationType(ref.line(), ref.order()))
&& !"ONE_CLICK_DROPSHIP".equals(ref.order().getOrderType())
? requireFbaPlanRefs(session.getContainerNo(), ref) : List.of(ref);
? requireFbaPlanRefs(session.getSourceContainerBookingId(), session.getContainerNo(), ref) : List.of(ref);
PalletSelection selection = group.size() == 1 && !"FBA".equals(destinationType(ref.line(), ref.order()))
? resolveReceivingPallet(ref, request.palletCode(), false) : resolveFbaGroupPallet(group, request.palletCode());
WarehouseResourceGateway.WarehousePalletReference pallet = selection.resource();
@@ -369,14 +379,16 @@ public class ReceivingContainerService {
if (replay != null) return read(replay, ReceivingContainerResponses.Receipt.class);
requirePalletCapacity(request.packageCount());
ReceivingContainerSession session = requireSigned(operatorKey);
InboundRef ref = requireInbound(session.getContainerNo(), request.inboundCode());
InboundRef ref = requireInbound(session.getSourceContainerBookingId(),
session.getContainerNo(), request.inboundCode());
if ("ONE_CLICK_DROPSHIP".equals(ref.order().getOrderType())) {
throw conflict("One-click dropship inbound must be operated by complete source pallet");
}
if (!"FBA".equals(destinationType(ref.line(), ref.order()))) {
throw conflict("OVERSEAS destination must use carton-by-carton scanning; bulk count is forbidden");
}
List<InboundRef> fbaPlanRefs = requireFbaPlanRefs(session.getContainerNo(), ref);
List<InboundRef> fbaPlanRefs = requireFbaPlanRefs(
session.getSourceContainerBookingId(), session.getContainerNo(), ref);
PalletSelection selection = resolveFbaGroupPallet(fbaPlanRefs, request.palletCode());
WarehouseResourceGateway.WarehousePalletReference resource = selection.resource();
InboundPlanPallet pallet = selection.pallet();
@@ -451,15 +463,16 @@ public class ReceivingContainerService {
ReceivingContainerOperation replay = replay(operatorKey, "CARTON_RECEIVE", request.idempotencyKey(), request);
if (replay != null) return read(replay, ReceivingContainerResponses.Receipt.class);
ReceivingContainerSession session = requireSigned(operatorKey);
List<InboundRef> currentRefs = refs(session.getContainerNo());
List<InboundRef> currentRefs = refs(session.getSourceContainerBookingId(), session.getContainerNo());
if (currentRefs.stream().noneMatch(value ->
"OVERSEAS".equals(destinationType(value.line(), value.order())))) {
throw conflict("FBA destination must use bulk count");
}
String inboundCode = cartonInboundCode(request.cartonMark());
InboundRef inboundRef = requireInbound(session.getContainerNo(), inboundCode);
InboundRef inboundRef = requireInbound(session.getSourceContainerBookingId(), session.getContainerNo(), inboundCode);
String customerId = inboundRef.order().getCustomerId();
CartonRef carton = requireCartonRef(customerId, session.getContainerNo(), request.cartonMark());
CartonRef carton = requireCartonRef(customerId, session.getSourceContainerBookingId(),
session.getContainerNo(), request.cartonMark());
InboundRef ref = carton.ref();
if (!"OVERSEAS".equals(destinationType(ref.line(), ref.order()))) {
throw conflict("FBA destination must use bulk count");
@@ -496,10 +509,12 @@ public class ReceivingContainerService {
ReceivingContainerOperation replay = replay(operatorKey, "PALLET_RECEIVE", request.idempotencyKey(), request);
if (replay != null) return read(replay, ReceivingContainerResponses.Receipt.class);
ReceivingContainerSession session = requireSigned(operatorKey);
String sourcePalletNo = dropshipSourcePallet(session.getContainerNo(), request.sourcePalletNo());
String sourcePalletNo = dropshipSourcePallet(
session.getSourceContainerBookingId(), session.getContainerNo(), request.sourcePalletNo());
if (sourcePalletNo == null) throw notFound("DP or carton is not part of the current container");
requireCompleteManifest(session.getContainerNo(), sourcePalletNo);
List<DropshipPalletRef> matches = dropshipPalletRefs(session.getContainerNo(), sourcePalletNo);
List<DropshipPalletRef> matches = dropshipPalletRefs(
session.getSourceContainerBookingId(), session.getContainerNo(), sourcePalletNo);
int total = matches.stream().mapToInt(value -> value(value.allocation().getPlannedPackageCount())).sum();
requirePalletCapacity(total);
boolean alreadyReceived = matches.stream().allMatch(match ->
@@ -538,17 +553,17 @@ public class ReceivingContainerService {
ReceivingContainerOperation replay = replay(operatorKey, "COMPLETE", request.idempotencyKey(), request);
if (replay != null) return read(replay, ReceivingContainerResponses.Session.class);
ReceivingContainerSession session = requireSigned(operatorKey);
List<InboundPlan> containerPlans = requirePlans(session.getContainerNo());
Totals totals = totals(containerPlans);
List<InboundPlan> containerPlans = requirePlans(session.getSourceContainerBookingId(), session.getContainerNo());
Totals totals = totals(session, containerPlans);
boolean hasDifference = totals.total() != totals.received() || hasFbaOverReceipt(session, containerPlans);
if (hasDifference && !request.allowDifference()) {
throw conflict("Container has a receiving difference; confirm onsite completion with difference");
}
if (!hasDifference) {
validateFbaSkuBalance(session.getContainerNo());
validateFbaSkuBalance(session.getSourceContainerBookingId(), session.getContainerNo());
workflow.completeContainer(containerPlans);
} else {
workflow.completeContainerWithDifferences(containerPlans, reconciledPlans(containerPlans));
workflow.completeContainerWithDifferences(containerPlans, reconciledPlans(session, containerPlans));
}
LocalDateTime now = LocalDateTime.now();
session.setStatus(hasDifference ? "COMPLETED_WITH_DIFFERENCE" : "COMPLETED");
@@ -558,23 +573,24 @@ public class ReceivingContainerService {
save(session, "COMPLETE", request.idempotencyKey(), request, result); return result;
}
private List<InboundPlan> reconciledPlans(List<InboundPlan> containerPlans) {
Set<Long> complete = refs(containerPlans.get(0).getContainerNo()).stream()
private List<InboundPlan> reconciledPlans(ReceivingContainerSession session, List<InboundPlan> containerPlans) {
List<InboundRef> currentRefs = refs(session.getSourceContainerBookingId(), session.getContainerNo());
Set<Long> complete = currentRefs.stream()
.collect(Collectors.groupingBy(value -> value.plan().getId(), LinkedHashMap::new, Collectors.toList()))
.entrySet().stream().filter(entry -> entry.getValue().stream().allMatch(ref ->
receivedLinePackages(ref) == value(ref.line().getRequiredPackageCount())))
.map(Map.Entry::getKey).collect(Collectors.toSet());
Set<String> incompleteFbaGroups = refs(containerPlans.get(0).getContainerNo()).stream()
Set<String> incompleteFbaGroups = currentRefs.stream()
.filter(ref -> !"ONE_CLICK_DROPSHIP".equals(ref.order().getOrderType()))
.filter(ref -> "FBA".equals(destinationType(ref.line(), ref.order())))
.filter(ref -> !complete.contains(ref.plan().getId()))
.map(ref -> normalizedFba(ref.line().getFbaWarehouseCode())).collect(Collectors.toSet());
refs(containerPlans.get(0).getContainerNo()).stream()
currentRefs.stream()
.filter(ref -> "FBA".equals(destinationType(ref.line(), ref.order())))
.filter(ref -> fbaGroupHasOverReceipt(findSession(containerPlans.get(0).getContainerNo()), ref))
.map(ref -> normalizedFba(ref.line().getFbaWarehouseCode())).forEach(incompleteFbaGroups::add);
return containerPlans.stream().filter(plan -> complete.contains(plan.getId()))
.filter(plan -> refs(plan.getContainerNo()).stream().filter(ref -> Objects.equals(ref.plan().getId(), plan.getId()))
.filter(plan -> currentRefs.stream().filter(ref -> Objects.equals(ref.plan().getId(), plan.getId()))
.noneMatch(ref -> "FBA".equals(destinationType(ref.line(), ref.order()))
&& incompleteFbaGroups.contains(normalizedFba(ref.line().getFbaWarehouseCode()))))
.toList();
@@ -589,22 +605,23 @@ public class ReceivingContainerService {
return session;
}
private ContainerReadiness requireReadyContainer(String containerNo, long overseasWarehouseId, boolean lock) {
private ContainerReadiness requireReadyContainer(String containerNo, String sourceContainerBookingId,
long overseasWarehouseId, boolean lock) {
String bookingId = trim(sourceContainerBookingId);
if (bookingId == null) throw conflict("sourceContainerBookingId is required");
LambdaQueryWrapper<LastlegOrder> query = new LambdaQueryWrapper<LastlegOrder>()
.eq(LastlegOrder::getContainerNo, containerNo).orderByAsc(LastlegOrder::getId);
.eq(LastlegOrder::getContainerNo, containerNo)
.eq(LastlegOrder::getSourceContainerBookingId, bookingId)
.orderByAsc(LastlegOrder::getId);
if (lock) query.last("FOR UPDATE");
List<LastlegOrder> orderFacts = orders.selectList(query);
Set<String> bookingIds = orderFacts.stream().map(LastlegOrder::getSourceContainerBookingId)
.map(ReceivingContainerService::trim).filter(Objects::nonNull).collect(Collectors.toSet());
if (orderFacts.isEmpty() || bookingIds.size() != 1) {
throw conflict("Container must resolve exactly one sourceContainerBookingId");
}
if (orderFacts.isEmpty()) throw notFound("当前柜次来源与集装箱号不匹配");
List<InboundPlan> planFacts = plans.selectList(new LambdaQueryWrapper<InboundPlan>()
.eq(InboundPlan::getContainerNo, containerNo).orderByAsc(InboundPlan::getId));
List<InboundPlanLine> lineFacts = lines.selectList(new LambdaQueryWrapper<InboundPlanLine>()
.in(InboundPlanLine::getOrderId, orderFacts.stream().map(LastlegOrder::getId).toList())
.orderByAsc(InboundPlanLine::getId));
ContainerReadiness value = readiness(bookingIds.iterator().next(), orderFacts, planFacts, lineFacts,
ContainerReadiness value = readiness(bookingId, orderFacts, planFacts, lineFacts,
overseasWarehouseId);
if (value == null || !value.operable()) {
throw conflict(value == null ? "当前柜已分配至其他海外接柜仓,无法在本仓操作" : value.blockReason());
@@ -614,7 +631,8 @@ public class ReceivingContainerService {
private void requireStableSessionFacts(ReceivingContainerSession session, boolean lock) {
if (trim(session.getContainerFactHash()) == null) return;
ContainerReadiness current = requireReadyContainer(session.getContainerNo(), session.getOverseasWarehouseId(), lock);
ContainerReadiness current = requireReadyContainer(session.getContainerNo(),
session.getSourceContainerBookingId(), session.getOverseasWarehouseId(), lock);
if (!Objects.equals(session.getSourceContainerBookingId(), current.sourceContainerBookingId())
|| !Objects.equals(session.getContainerFactHash(), current.factHash())) {
throw conflict("Container orders or inbound plans changed after prepare; restart the receiving session");
@@ -657,10 +675,8 @@ public class ReceivingContainerService {
int planCount = matchedPlans.size();
boolean allPlansReady = totalOrders > 0 && inboundCodes == totalOrders
&& plannedOrders == totalOrders && planCount == totalOrders;
List<InboundPlan> containerPlans = planFacts.stream()
.filter(plan -> containerNo.equals(plan.getContainerNo())).toList();
boolean allPlansCompleted = !containerPlans.isEmpty()
&& containerPlans.stream().allMatch(plan -> List.of("COMPLETED", "PENDING_RECONCILIATION")
boolean allPlansCompleted = !matchedPlans.isEmpty()
&& matchedPlans.stream().allMatch(plan -> List.of("COMPLETED", "PENDING_RECONCILIATION")
.contains(plan.getStatus()));
boolean shippingReady = shippingGates.receivingAvailable(sourceContainerBookingId, containerNo) == 1;
boolean operable = allPlansReady && shippingReady && warehouseSnapshotConsistent;
@@ -716,9 +732,12 @@ public class ReceivingContainerService {
return session;
}
private List<InboundPlan> requirePlans(String containerNo) {
List<InboundPlan> values = plans.selectList(new LambdaQueryWrapper<InboundPlan>()
.eq(InboundPlan::getContainerNo, containerNo).orderByAsc(InboundPlan::getId));
private List<InboundPlan> requirePlans(String sourceContainerBookingId, String containerNo) {
List<InboundPlan> values = refs(sourceContainerBookingId, containerNo).stream()
.map(InboundRef::plan)
.collect(Collectors.toMap(InboundPlan::getId, Function.identity(), (left, right) -> left,
LinkedHashMap::new))
.values().stream().toList();
if (values.isEmpty()) throw notFound("Receiving container was not found");
if (values.stream().anyMatch(value -> trim(value.getCustomerId()) == null)) {
throw conflict("Every inbound plan in the receiving container must have an owning customer");
@@ -730,32 +749,31 @@ public class ReceivingContainerService {
return values;
}
private List<InboundPlan> plansForContainer(String containerNo) {
List<InboundPlan> values = plans.selectList(new LambdaQueryWrapper<InboundPlan>()
.eq(InboundPlan::getContainerNo, containerNo)
.orderByAsc(InboundPlan::getId));
if (values.isEmpty()) throw notFound("Receiving container was not found");
return values;
}
private List<InboundRef> refs(String containerNo) {
private List<InboundRef> refs(String sourceContainerBookingId, String containerNo) {
List<InboundRef> result = new ArrayList<>();
for (InboundPlan plan : plansForContainer(containerNo)) {
List<InboundPlan> containerPlans = plans.selectList(new LambdaQueryWrapper<InboundPlan>()
.eq(InboundPlan::getContainerNo, containerNo).orderByAsc(InboundPlan::getId));
for (InboundPlan plan : containerPlans) {
String customerId = trim(plan.getCustomerId());
if (customerId == null) throw conflict("Inbound plan has no owning customer");
for (InboundPlanLine line : lines.selectList(new LambdaQueryWrapper<InboundPlanLine>()
.eq(InboundPlanLine::getCustomerId, customerId).eq(InboundPlanLine::getPlanId, plan.getId())
.orderByAsc(InboundPlanLine::getId))) {
LastlegOrder order = orders.selectById(line.getOrderId());
if (order == null || !customerId.equals(order.getCustomerId())) throw conflict("Inbound line order is missing");
if (order == null || !customerId.equals(order.getCustomerId())
|| !Objects.equals(containerNo, trim(order.getContainerNo()))) {
throw conflict("Inbound line order is missing or belongs to another cabinet occurrence");
}
if (!Objects.equals(sourceContainerBookingId, trim(order.getSourceContainerBookingId()))) continue;
result.add(new InboundRef(plan, line, order));
}
}
if (result.isEmpty()) throw notFound("Receiving container was not found");
return result;
}
private InboundRef requireInbound(String containerNo, String inboundCode) {
List<InboundRef> matches = refs(containerNo).stream()
private InboundRef requireInbound(String sourceContainerBookingId, String containerNo, String inboundCode) {
List<InboundRef> matches = refs(sourceContainerBookingId, containerNo).stream()
.filter(value -> inboundCode.equals(value.line().getInboundCode())).toList();
if (matches.isEmpty()) throw notFound("Inbound code is not part of the current container");
Set<Long> planIds = matches.stream().map(value -> value.plan().getId()).collect(Collectors.toSet());
@@ -763,11 +781,12 @@ public class ReceivingContainerService {
return matches.stream().min(Comparator.comparing(value -> value.line().getId())).orElseThrow();
}
private CartonRef requireCartonRef(String customerId, String containerNo, String cartonMark) {
private CartonRef requireCartonRef(String customerId, String sourceContainerBookingId,
String containerNo, String cartonMark) {
InboundPlanCarton carton = cartons.selectOne(new LambdaQueryWrapper<InboundPlanCarton>()
.eq(InboundPlanCarton::getCustomerId, customerId).eq(InboundPlanCarton::getCartonMark, cartonMark));
if (carton == null) throw notFound("Carton mark was not found");
List<InboundRef> matches = refs(containerNo).stream()
List<InboundRef> matches = refs(sourceContainerBookingId, containerNo).stream()
.filter(value -> customerId.equals(value.order().getCustomerId()))
.filter(value -> carton.getLineId() == null || Objects.equals(value.line().getId(), carton.getLineId()))
.filter(value -> carton.getPlanId() == null || Objects.equals(value.plan().getId(), carton.getPlanId()))
@@ -791,8 +810,9 @@ public class ReceivingContainerService {
throw notFound("Carton mark was not found");
}
private List<InboundRef> requireFbaPlanRefs(String containerNo, InboundRef selected) {
List<InboundRef> values = refs(containerNo).stream()
private List<InboundRef> requireFbaPlanRefs(String sourceContainerBookingId,
String containerNo, InboundRef selected) {
List<InboundRef> values = refs(sourceContainerBookingId, containerNo).stream()
.filter(value -> !"ONE_CLICK_DROPSHIP".equals(value.order().getOrderType()))
.filter(value -> "FBA".equals(destinationType(value.line(), value.order())))
.filter(value -> Objects.equals(normalizedFba(value.line().getFbaWarehouseCode()),
@@ -940,8 +960,8 @@ public class ReceivingContainerService {
return Objects.equals(line.getDestinationWarehouseCode(), pallet.getDestinationWarehouseCode());
}
private String dropshipSourcePallet(String containerNo, String scanCode) {
return dropshipSourcePallet(refs(containerNo), containerNo, scanCode);
private String dropshipSourcePallet(String sourceContainerBookingId, String containerNo, String scanCode) {
return dropshipSourcePallet(refs(sourceContainerBookingId, containerNo), containerNo, scanCode);
}
private String dropshipSourcePallet(List<InboundRef> currentRefs, String containerNo, String scanCode) {
@@ -960,9 +980,10 @@ public class ReceivingContainerService {
return resolved.isEmpty() ? null : resolved.get(0);
}
private List<DropshipPalletRef> dropshipPalletRefs(String containerNo, String sourcePalletNo) {
private List<DropshipPalletRef> dropshipPalletRefs(String sourceContainerBookingId,
String containerNo, String sourcePalletNo) {
List<DropshipPalletRef> result = new ArrayList<>();
for (InboundRef ref : refs(containerNo)) {
for (InboundRef ref : refs(sourceContainerBookingId, containerNo)) {
if (!"ONE_CLICK_DROPSHIP".equals(ref.order().getOrderType())) continue;
List<InboundPlanPallet> sourcePallets = pallets.selectList(new LambdaQueryWrapper<InboundPlanPallet>()
.eq(InboundPlanPallet::getCustomerId, ref.order().getCustomerId())
@@ -1009,7 +1030,8 @@ public class ReceivingContainerService {
InboundPlanPalletAllocation allocation) { }
private ReceivingContainerResponses.Session snapshot(ReceivingContainerSession session, List<InboundPlan> containerPlans) {
List<ReceivingContainerResponses.Inbound> inbounds = refs(session.getContainerNo()).stream()
List<ReceivingContainerResponses.Inbound> inbounds = refs(
session.getSourceContainerBookingId(), session.getContainerNo()).stream()
.map(this::inbound).toList();
int total = inbounds.stream().mapToInt(ReceivingContainerResponses.Inbound::totalPackages).sum();
int received = inbounds.stream().mapToInt(ReceivingContainerResponses.Inbound::receivedPackages).sum();
@@ -1065,7 +1087,7 @@ public class ReceivingContainerService {
private boolean hasFbaOverReceipt(ReceivingContainerSession session, List<InboundPlan> containerPlans) {
if (session == null || containerPlans == null || containerPlans.isEmpty()) return false;
return refs(session.getContainerNo()).stream()
return refs(session.getSourceContainerBookingId(), session.getContainerNo()).stream()
.filter(ref -> !"ONE_CLICK_DROPSHIP".equals(ref.order().getOrderType()))
.filter(ref -> "FBA".equals(destinationType(ref.line(), ref.order())))
.anyMatch(ref -> fbaGroupHasOverReceipt(session, ref));
@@ -1073,7 +1095,7 @@ public class ReceivingContainerService {
private boolean fbaGroupHasOverReceipt(ReceivingContainerSession session, InboundRef ref) {
if (session == null || session.getId() == null) return false;
int planned = requireFbaPlanRefs(session.getContainerNo(), ref).stream()
int planned = requireFbaPlanRefs(session.getSourceContainerBookingId(), session.getContainerNo(), ref).stream()
.mapToInt(value -> value(value.line().getRequiredPackageCount())).sum();
return fbaPalletCounts.sumActual(session.getId(), fbaGroupKey(session.getContainerNo(), ref)) > planned;
}
@@ -1115,8 +1137,9 @@ public class ReceivingContainerService {
return mergeAllowed(plan) ? PALLET_MERGE_HINT : null;
}
private Totals totals(List<InboundPlan> containerPlans) {
List<ReceivingContainerResponses.Inbound> values = refs(containerPlans.get(0).getContainerNo())
private Totals totals(ReceivingContainerSession session, List<InboundPlan> containerPlans) {
List<ReceivingContainerResponses.Inbound> values = refs(
session.getSourceContainerBookingId(), session.getContainerNo())
.stream().map(this::inbound).toList();
List<String> destinationCodes = values.stream().map(this::destinationCode)
.filter(Objects::nonNull).distinct().sorted().toList();
@@ -1246,8 +1269,8 @@ public class ReceivingContainerService {
}
}
private void validateFbaSkuBalance(String containerNo) {
for (InboundRef ref : refs(containerNo)) {
private void validateFbaSkuBalance(String sourceContainerBookingId, String containerNo) {
for (InboundRef ref : refs(sourceContainerBookingId, containerNo)) {
if (!"FBA".equals(destinationType(ref.line(), ref.order()))) continue;
if ("ONE_CLICK_DROPSHIP".equals(ref.order().getOrderType())) continue;
String customerId = ref.order().getCustomerId();
@@ -1296,12 +1319,14 @@ public class ReceivingContainerService {
private ReceivingContainerSession waitingView(String operatorKey,
WarehouseResourceGateway.WarehouseReference warehouse,
String sourceContainerBookingId,
String containerNo) {
ReceivingContainerSession value = new ReceivingContainerSession();
value.setOverseasWarehouseId(warehouse.warehouseId());
value.setOverseasWarehouseCode(warehouse.warehouseCode());
value.setOverseasWarehouseName(warehouse.warehouseName());
value.setOperatorKey(operatorKey); value.setContainerNo(containerNo); value.setStatus("WAIT_SIGN"); value.setVersion(0);
value.setOperatorKey(operatorKey); value.setSourceContainerBookingId(sourceContainerBookingId);
value.setContainerNo(containerNo); value.setStatus("WAIT_SIGN"); value.setVersion(0);
return value;
}
@@ -1411,5 +1436,5 @@ public class ReceivingContainerService {
int planCount, int plannedInboundCodeCount, int pendingPlanCount,
boolean allPlansReady, boolean allPlansCompleted, boolean shippingReady, boolean operable, String blockReason,
LocalDateTime plannedArrivalAt, String factHash, Totals totals) { }
private record PrepareCommand(String containerNo, String idempotencyKey) { }
private record PrepareCommand(String sourceContainerBookingId, String containerNo, String idempotencyKey) { }
}