提交
This commit is contained in:
+8
@@ -0,0 +1,8 @@
|
|||||||
|
package com.baiyue.lastleg.dropship.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record DropshipPalletSnapshot(String sourcePalletNo, long packageCount,
|
||||||
|
List<Sku> skus) {
|
||||||
|
public record Sku(String skuCode, String productName, long packageCount, long productQuantity) {}
|
||||||
|
}
|
||||||
+24
-10
@@ -7,6 +7,7 @@ import com.baiyue.lastleg.dropship.mapper.*;
|
|||||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||||
import com.baiyue.lastleg.inboundplan.pda.entity.InboundStockReceipt;
|
import com.baiyue.lastleg.inboundplan.pda.entity.InboundStockReceipt;
|
||||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||||
|
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -109,6 +110,21 @@ public class DropshipOrderService {
|
|||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<DropshipPalletSnapshot> palletSnapshots(String customerId, Long orderId) {
|
||||||
|
List<DropshipOrderProfile> orderProfiles = profiles.listOrder(customerId, orderId);
|
||||||
|
Map<Long, List<DropshipSkuInventory>> skusByProfile = skuInventory.listOrder(customerId, orderId).stream()
|
||||||
|
.collect(Collectors.groupingBy(DropshipSkuInventory::getProfileId));
|
||||||
|
return orderProfiles.stream().map(profile -> {
|
||||||
|
List<DropshipPalletSnapshot.Sku> skus = skusByProfile.getOrDefault(profile.getId(), List.of()).stream()
|
||||||
|
.map(sku -> new DropshipPalletSnapshot.Sku(sku.getSkuCode(), sku.getProductName(),
|
||||||
|
value(sku.getPackageCount()), value(sku.getProductQuantity())))
|
||||||
|
.toList();
|
||||||
|
return new DropshipPalletSnapshot(profile.getSourcePalletNo(),
|
||||||
|
skus.stream().mapToLong(DropshipPalletSnapshot.Sku::packageCount).sum(), skus);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void activateAfterStockIn(String customerId, Long orderId, Long taskId,
|
public void activateAfterStockIn(String customerId, Long orderId, Long taskId,
|
||||||
List<InboundStockReceipt> receipts) {
|
List<InboundStockReceipt> receipts) {
|
||||||
@@ -118,20 +134,18 @@ public class DropshipOrderService {
|
|||||||
if (profilesForOrder.stream().anyMatch(profile -> "ACTIVE".equals(profile.getStatus()))) {
|
if (profilesForOrder.stream().anyMatch(profile -> "ACTIVE".equals(profile.getStatus()))) {
|
||||||
throw conflict("Dropship pallet activation is incomplete; repair profiles before retrying");
|
throw conflict("Dropship pallet activation is incomplete; repair profiles before retrying");
|
||||||
}
|
}
|
||||||
Map<Long, List<InboundStockReceipt>> receiptsByPallet = receipts.stream()
|
Map<String, List<InboundStockReceipt>> receiptsByPallet = receipts.stream()
|
||||||
.filter(receipt -> receipt.getWarehousePalletId() != null)
|
.filter(receipt -> receipt.getPalletNo() != null)
|
||||||
.collect(Collectors.groupingBy(InboundStockReceipt::getWarehousePalletId));
|
.collect(Collectors.groupingBy(InboundStockReceipt::getPalletNo));
|
||||||
if (receiptsByPallet.size() != profilesForOrder.size()) {
|
if (receiptsByPallet.size() != profilesForOrder.size()) {
|
||||||
throw conflict("PDA pallet count does not match dropship source pallet count");
|
throw conflict("PDA pallet count does not match dropship source pallet count");
|
||||||
}
|
}
|
||||||
List<DropshipOrderProfile> orderedProfiles = profilesForOrder.stream()
|
|
||||||
.sorted(java.util.Comparator.comparing(DropshipOrderProfile::getSourcePalletNo)).toList();
|
|
||||||
List<List<InboundStockReceipt>> orderedReceipts = receiptsByPallet.values().stream()
|
|
||||||
.sorted(java.util.Comparator.comparing(value -> value.get(0).getWarehousePalletId())).toList();
|
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
for (int index = 0; index < orderedProfiles.size(); index++) {
|
for (DropshipOrderProfile profile : profilesForOrder) {
|
||||||
DropshipOrderProfile profile = orderedProfiles.get(index);
|
List<InboundStockReceipt> actual = receiptsByPallet.get(profile.getSourcePalletNo());
|
||||||
List<InboundStockReceipt> actual = orderedReceipts.get(index);
|
if (actual == null || actual.isEmpty()) {
|
||||||
|
throw conflict("PDA receipt is missing for dropship source pallet " + profile.getSourcePalletNo());
|
||||||
|
}
|
||||||
List<DropshipCarton> profileCartons = cartons.listProfile(customerId, profile.getId());
|
List<DropshipCarton> profileCartons = cartons.listProfile(customerId, profile.getId());
|
||||||
long expected = profileCartons.size();
|
long expected = profileCartons.size();
|
||||||
long received = actual.stream().mapToLong(value -> value.getReceivedPackages() == null ? 0 : value.getReceivedPackages()).sum();
|
long received = actual.stream().mapToLong(value -> value.getReceivedPackages() == null ? 0 : value.getReceivedPackages()).sum();
|
||||||
|
|||||||
+10
@@ -65,6 +65,16 @@ public class InboundPdaController {
|
|||||||
return ApiResponse.success(service.pick(scope(user, customerId), planNo, allocationId, request));
|
return ApiResponse.success(service.pick(scope(user, customerId), planNo, allocationId, request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/allocations/{allocationId}/pick-whole-pallet")
|
||||||
|
public ApiResponse<PdaResponses.Operation> pickWholePallet(
|
||||||
|
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String planNo,
|
||||||
|
@PathVariable long allocationId, @RequestParam(required = false) String customerId,
|
||||||
|
@Valid @RequestBody PdaRequests.WholePalletOperation request) {
|
||||||
|
requireOperate(user);
|
||||||
|
return ApiResponse.success(service.pickWholePallet(
|
||||||
|
scope(user, customerId), planNo, allocationId, request));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/cartons/scan")
|
@PostMapping("/cartons/scan")
|
||||||
public ApiResponse<PdaResponses.Carton> scanCarton(@AuthenticationPrincipal AuthenticatedUser user,
|
public ApiResponse<PdaResponses.Carton> scanCarton(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@PathVariable String planNo,
|
@PathVariable String planNo,
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ public final class PdaRequests {
|
|||||||
public record PackageOperation(@Positive int packageCount, @NotBlank String idempotencyKey,
|
public record PackageOperation(@Positive int packageCount, @NotBlank String idempotencyKey,
|
||||||
String operatorId, String operatorName) {}
|
String operatorId, String operatorName) {}
|
||||||
|
|
||||||
|
public record WholePalletOperation(@NotBlank String idempotencyKey,
|
||||||
|
String operatorId, String operatorName) {}
|
||||||
|
|
||||||
public record ConfirmMerge(@Positive long sourceAllocationId, @Positive int packageCount,
|
public record ConfirmMerge(@Positive long sourceAllocationId, @Positive int packageCount,
|
||||||
@NotBlank String idempotencyKey, String operatorId, String operatorName) {}
|
@NotBlank String idempotencyKey, String operatorId, String operatorName) {}
|
||||||
|
|
||||||
|
|||||||
+16
-1
@@ -11,7 +11,18 @@ public final class PdaResponses {
|
|||||||
LocalDateTime pushedAt, LocalDateTime startedAt, LocalDateTime completedAt,
|
LocalDateTime pushedAt, LocalDateTime startedAt, LocalDateTime completedAt,
|
||||||
List<Long> unloadPhotoFileIds, LocalDateTime unloadPhotoUploadedAt,
|
List<Long> unloadPhotoFileIds, LocalDateTime unloadPhotoUploadedAt,
|
||||||
LocalDateTime estimatedEmptyReturnAt, int expectedPackageCount,
|
LocalDateTime estimatedEmptyReturnAt, int expectedPackageCount,
|
||||||
int pickedPackageCount) {
|
int pickedPackageCount, String orderType, boolean wholePalletInbound,
|
||||||
|
List<DropshipPallet> dropshipPallets) {
|
||||||
|
public Task(long taskId, String taskNo, String planNo, String status,
|
||||||
|
LocalDateTime pushedAt, LocalDateTime startedAt, LocalDateTime completedAt,
|
||||||
|
List<Long> unloadPhotoFileIds, LocalDateTime unloadPhotoUploadedAt,
|
||||||
|
LocalDateTime estimatedEmptyReturnAt, int expectedPackageCount,
|
||||||
|
int pickedPackageCount) {
|
||||||
|
this(taskId, taskNo, planNo, status, pushedAt, startedAt, completedAt,
|
||||||
|
unloadPhotoFileIds, unloadPhotoUploadedAt, estimatedEmptyReturnAt,
|
||||||
|
expectedPackageCount, pickedPackageCount, null, false, List.of());
|
||||||
|
}
|
||||||
|
|
||||||
public Task(long taskId, String taskNo, String planNo, String status,
|
public Task(long taskId, String taskNo, String planNo, String status,
|
||||||
LocalDateTime pushedAt, LocalDateTime startedAt, LocalDateTime completedAt,
|
LocalDateTime pushedAt, LocalDateTime startedAt, LocalDateTime completedAt,
|
||||||
List<Long> unloadPhotoFileIds, LocalDateTime unloadPhotoUploadedAt,
|
List<Long> unloadPhotoFileIds, LocalDateTime unloadPhotoUploadedAt,
|
||||||
@@ -21,6 +32,10 @@ public final class PdaResponses {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record DropshipPallet(String sourcePalletNo, long planPalletId, long allocationId,
|
||||||
|
long packageCount, List<DropshipSku> skus) {}
|
||||||
|
public record DropshipSku(String skuCode, String productName, long packageCount, long productQuantity) {}
|
||||||
|
|
||||||
public record UnloadPhotoFile(long fileId, String fileName, String contentType, long sizeBytes) {}
|
public record UnloadPhotoFile(long fileId, String fileName, String contentType, long sizeBytes) {}
|
||||||
|
|
||||||
public record Operation(String result, long allocationId, int requiredPackageCount,
|
public record Operation(String result, long allocationId, int requiredPackageCount,
|
||||||
|
|||||||
+5
@@ -17,6 +17,8 @@ import com.baiyue.lastleg.inboundplan.pda.entity.InboundPdaTask;
|
|||||||
import com.baiyue.lastleg.inboundplan.pda.mapper.InboundPalletMergeMapper;
|
import com.baiyue.lastleg.inboundplan.pda.mapper.InboundPalletMergeMapper;
|
||||||
import com.baiyue.lastleg.inboundplan.pda.mapper.InboundPdaOperationMapper;
|
import com.baiyue.lastleg.inboundplan.pda.mapper.InboundPdaOperationMapper;
|
||||||
import com.baiyue.lastleg.inboundplan.pda.mapper.InboundPdaTaskMapper;
|
import com.baiyue.lastleg.inboundplan.pda.mapper.InboundPdaTaskMapper;
|
||||||
|
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||||
|
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
|
||||||
@@ -32,6 +34,7 @@ public class PdaWorkflowRepository {
|
|||||||
@Resource private InboundPdaOperationMapper operations;
|
@Resource private InboundPdaOperationMapper operations;
|
||||||
@Resource private InboundPalletMergeMapper merges;
|
@Resource private InboundPalletMergeMapper merges;
|
||||||
@Resource private InboundPlanCartonMapper cartons;
|
@Resource private InboundPlanCartonMapper cartons;
|
||||||
|
@Resource private LastlegOrderMapper orders;
|
||||||
|
|
||||||
public InboundPlan lockPlan(String customerId, String planNo) {
|
public InboundPlan lockPlan(String customerId, String planNo) {
|
||||||
return plans.selectOne(new LambdaQueryWrapper<InboundPlan>()
|
return plans.selectOne(new LambdaQueryWrapper<InboundPlan>()
|
||||||
@@ -92,6 +95,8 @@ public class PdaWorkflowRepository {
|
|||||||
.eq(InboundPlanPalletAllocation::getPlanId, planId));
|
.eq(InboundPlanPalletAllocation::getPlanId, planId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LastlegOrder order(Long orderId) { return orderId == null ? null : orders.selectById(orderId); }
|
||||||
|
|
||||||
public void insert(InboundPdaTask value) { tasks.insert(value); }
|
public void insert(InboundPdaTask value) { tasks.insert(value); }
|
||||||
public void insert(InboundPdaOperation value) { operations.insert(value); }
|
public void insert(InboundPdaOperation value) { operations.insert(value); }
|
||||||
public void insert(InboundPalletMerge value) { merges.insert(value); }
|
public void insert(InboundPalletMerge value) { merges.insert(value); }
|
||||||
|
|||||||
+88
-11
@@ -14,6 +14,9 @@ import com.baiyue.lastleg.inboundplan.pda.entity.InboundPdaTask;
|
|||||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||||
import com.baiyue.lastleg.inboundplan.pda.gateway.UnloadPhotoFileGateway;
|
import com.baiyue.lastleg.inboundplan.pda.gateway.UnloadPhotoFileGateway;
|
||||||
import com.baiyue.lastleg.inboundplan.pda.repository.PdaWorkflowRepository;
|
import com.baiyue.lastleg.inboundplan.pda.repository.PdaWorkflowRepository;
|
||||||
|
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
|
||||||
|
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||||
|
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.MapperFeature;
|
import com.fasterxml.jackson.databind.MapperFeature;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@@ -50,6 +53,7 @@ public class InboundPdaWorkflowService {
|
|||||||
@Resource private InboundStockReceiptService stockReceipts;
|
@Resource private InboundStockReceiptService stockReceipts;
|
||||||
@Resource private InboundUnloadCompletedOutboxService unloadCompletedOutbox;
|
@Resource private InboundUnloadCompletedOutboxService unloadCompletedOutbox;
|
||||||
@Resource private UnloadPhotoFileGateway unloadPhotoFiles;
|
@Resource private UnloadPhotoFileGateway unloadPhotoFiles;
|
||||||
|
@Resource private DropshipOrderService dropshipOrders;
|
||||||
@Resource private ObjectMapper sourceObjectMapper;
|
@Resource private ObjectMapper sourceObjectMapper;
|
||||||
|
|
||||||
private ObjectMapper objectMapper;
|
private ObjectMapper objectMapper;
|
||||||
@@ -209,6 +213,39 @@ public class InboundPdaWorkflowService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public PdaResponses.Operation pickWholePallet(String customerId, String planNo, long allocationId,
|
||||||
|
PdaRequests.WholePalletOperation request) {
|
||||||
|
InboundPlan plan = requirePlan(customerId, planNo);
|
||||||
|
requireDropship(plan);
|
||||||
|
InboundPdaOperation replay = replay(customerId, "PICK_WHOLE_PALLET", request.idempotencyKey(), request);
|
||||||
|
if (replay != null) return read(replay, PdaResponses.Operation.class);
|
||||||
|
InboundPdaTask task = requireActiveTask(customerId, plan);
|
||||||
|
InboundPlanPalletAllocation allocation = requireAllocation(plan.getId(), allocationId);
|
||||||
|
InboundPlanLine line = requireLine(plan.getId(), allocation.getLineId());
|
||||||
|
int planned = value(allocation.getPlannedPackageCount());
|
||||||
|
if (planned <= 0) throw conflict("One-click dropship pallet has no planned packages");
|
||||||
|
if (value(allocation.getPickedPackageCount()) != 0) {
|
||||||
|
throw conflict("One-click dropship pallet has already been partially operated");
|
||||||
|
}
|
||||||
|
allocation.setCountedPackageCount(planned);
|
||||||
|
allocation.setPickedPackageCount(planned);
|
||||||
|
allocation.setActualVolumeCbm(volume(allocation.getAllocatedVolumeCbm()));
|
||||||
|
allocation.setStatus("PICKED");
|
||||||
|
bump(allocation);
|
||||||
|
repository.update(allocation);
|
||||||
|
InboundPlanPallet pallet = requirePallet(plan.getId(), allocation.getPalletId());
|
||||||
|
pallet.setActualPackageCount(planned);
|
||||||
|
pallet.setActualVolumeCbm(volume(allocation.getAllocatedVolumeCbm()));
|
||||||
|
repository.update(pallet);
|
||||||
|
startPicking(task, plan, request.operatorId(), request.operatorName());
|
||||||
|
PdaResponses.Operation result = operation("WHOLE_PALLET_PICKED", allocation, line,
|
||||||
|
List.of(), 0, planned, 0);
|
||||||
|
saveOperation(customerId, task.getId(), allocationId, "PICK_WHOLE_PALLET",
|
||||||
|
request.idempotencyKey(), request, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/** 海外首次扫描箱唛:仅接受本柜国内冻结且已归属本计划的箱。 */
|
/** 海外首次扫描箱唛:仅接受本柜国内冻结且已归属本计划的箱。 */
|
||||||
@Transactional
|
@Transactional
|
||||||
public PdaResponses.Carton scanCarton(String customerId, String planNo, PdaRequests.ScanCarton request) {
|
public PdaResponses.Carton scanCarton(String customerId, String planNo, PdaRequests.ScanCarton request) {
|
||||||
@@ -423,7 +460,8 @@ public class InboundPdaWorkflowService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
List<InboundPlanCarton> cartons = repository.cartons(plan.getId());
|
List<InboundPlanCarton> cartons = repository.cartons(plan.getId());
|
||||||
if (!cartons.isEmpty() && cartons.stream().anyMatch(carton -> !"ON_PALLET".equals(carton.getStatus()))) {
|
if (!isDropship(plan) && !cartons.isEmpty()
|
||||||
|
&& cartons.stream().anyMatch(carton -> !"ON_PALLET".equals(carton.getStatus()))) {
|
||||||
throw conflict("All expected carton marks must be scanned onto a pallet before stock-in");
|
throw conflict("All expected carton marks must be scanned onto a pallet before stock-in");
|
||||||
}
|
}
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
@@ -639,6 +677,9 @@ public class InboundPdaWorkflowService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void requireLegacyCounting(InboundPlan plan) {
|
private void requireLegacyCounting(InboundPlan plan) {
|
||||||
|
if (isDropship(plan)) {
|
||||||
|
throw conflict("One-click dropship inbound must be picked by complete source pallet");
|
||||||
|
}
|
||||||
if (repository.hasCartonLabelPool(plan.getId())) {
|
if (repository.hasCartonLabelPool(plan.getId())) {
|
||||||
throw conflict("This inbound plan has a carton manifest and must be operated by carton scanning");
|
throw conflict("This inbound plan has a carton manifest and must be operated by carton scanning");
|
||||||
}
|
}
|
||||||
@@ -708,20 +749,56 @@ public class InboundPdaWorkflowService {
|
|||||||
volume(allocation.getActualVolumeCbm()), remaining, requested, merge, candidates);
|
volume(allocation.getActualVolumeCbm()), remaining, requested, merge, candidates);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PdaResponses.Task task(InboundPdaTask task, String planNo) {
|
|
||||||
return task(task, planNo, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private PdaResponses.Task taskResponse(InboundPdaTask task, InboundPlan plan, String planNo) {
|
private PdaResponses.Task taskResponse(InboundPdaTask task, InboundPlan plan, String planNo) {
|
||||||
int expected = repository.lines(plan.getId()).stream().mapToInt(line -> value(line.getRequiredPackageCount())).sum();
|
int expected = repository.lines(plan.getId()).stream().mapToInt(line -> value(line.getRequiredPackageCount())).sum();
|
||||||
int picked = repository.allocations(plan.getId()).stream().mapToInt(allocation -> value(allocation.getPickedPackageCount())).sum();
|
List<InboundPlanPalletAllocation> allocations = repository.allocations(plan.getId());
|
||||||
return task(task, planNo, expected, picked);
|
int picked = allocations.stream().mapToInt(allocation -> value(allocation.getPickedPackageCount())).sum();
|
||||||
}
|
LastlegOrder order = repository.order(plan.getSourceOrderId());
|
||||||
|
String orderType = order == null ? null : order.getOrderType();
|
||||||
private PdaResponses.Task task(InboundPdaTask task, String planNo, int expected, int picked) {
|
boolean wholePalletInbound = "ONE_CLICK_DROPSHIP".equals(orderType);
|
||||||
|
Map<String, InboundPlanPallet> palletsByNo = wholePalletInbound
|
||||||
|
? repository.pallets(plan.getId()).stream().collect(java.util.stream.Collectors.toMap(
|
||||||
|
InboundPlanPallet::getPalletNo, value -> value)) : Map.of();
|
||||||
|
Map<Long, InboundPlanPalletAllocation> allocationsByPallet = wholePalletInbound
|
||||||
|
? allocations.stream().collect(java.util.stream.Collectors.toMap(
|
||||||
|
InboundPlanPalletAllocation::getPalletId, value -> value)) : Map.of();
|
||||||
|
List<PdaResponses.DropshipPallet> dropshipPallets = wholePalletInbound
|
||||||
|
? dropshipOrders.palletSnapshots(task.getCustomerId(), order.getId()).stream()
|
||||||
|
.map(pallet -> dropshipPallet(pallet, palletsByNo, allocationsByPallet)).toList() : List.of();
|
||||||
return new PdaResponses.Task(task.getId(), task.getTaskNo(), planNo, task.getStatus(),
|
return new PdaResponses.Task(task.getId(), task.getTaskNo(), planNo, task.getStatus(),
|
||||||
task.getPushedAt(), task.getStartedAt(), task.getCompletedAt(), unloadPhotoFileIds(task),
|
task.getPushedAt(), task.getStartedAt(), task.getCompletedAt(), unloadPhotoFileIds(task),
|
||||||
task.getUnloadPhotoUploadedAt(), task.getEstimatedEmptyReturnAt(), expected, picked);
|
task.getUnloadPhotoUploadedAt(), task.getEstimatedEmptyReturnAt(), expected, picked,
|
||||||
|
orderType, wholePalletInbound, dropshipPallets);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PdaResponses.Task task(InboundPdaTask task, String planNo) {
|
||||||
|
return new PdaResponses.Task(task.getId(), task.getTaskNo(), planNo, task.getStatus(),
|
||||||
|
task.getPushedAt(), task.getStartedAt(), task.getCompletedAt(), unloadPhotoFileIds(task),
|
||||||
|
task.getUnloadPhotoUploadedAt(), task.getEstimatedEmptyReturnAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
private PdaResponses.DropshipPallet dropshipPallet(DropshipPalletSnapshot pallet,
|
||||||
|
Map<String, InboundPlanPallet> palletsByNo,
|
||||||
|
Map<Long, InboundPlanPalletAllocation> allocationsByPallet) {
|
||||||
|
InboundPlanPallet planPallet = palletsByNo.get(pallet.sourcePalletNo());
|
||||||
|
if (planPallet == null) throw conflict("Dropship source pallet is missing from the inbound plan");
|
||||||
|
InboundPlanPalletAllocation allocation = allocationsByPallet.get(planPallet.getId());
|
||||||
|
if (allocation == null) throw conflict("Dropship source pallet has no inbound allocation");
|
||||||
|
return new PdaResponses.DropshipPallet(pallet.sourcePalletNo(), planPallet.getId(), allocation.getId(),
|
||||||
|
pallet.packageCount(), pallet.skus().stream()
|
||||||
|
.map(sku -> new PdaResponses.DropshipSku(sku.skuCode(), sku.productName(),
|
||||||
|
sku.packageCount(), sku.productQuantity())).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireDropship(InboundPlan plan) {
|
||||||
|
if (!isDropship(plan)) {
|
||||||
|
throw conflict("Whole-pallet picking is only available for one-click dropship inbound plans");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDropship(InboundPlan plan) {
|
||||||
|
LastlegOrder order = repository.order(plan.getSourceOrderId());
|
||||||
|
return order != null && "ONE_CLICK_DROPSHIP".equals(order.getOrderType());
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Long> unloadPhotoFileIds(InboundPdaTask task) {
|
private List<Long> unloadPhotoFileIds(InboundPdaTask task) {
|
||||||
|
|||||||
+17
-7
@@ -18,6 +18,7 @@ import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
|
|||||||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||||
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||||
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||||
|
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -93,11 +94,24 @@ public class InboundStockReceiptService {
|
|||||||
}
|
}
|
||||||
List<SkuRemainder> skuRemainders = "ONE_CLICK_DROPSHIP".equals(order.getOrderType())
|
List<SkuRemainder> skuRemainders = "ONE_CLICK_DROPSHIP".equals(order.getOrderType())
|
||||||
? List.of() : skuRemainders(order.getId(), value(line.getRequiredPackageCount()));
|
? List.of() : skuRemainders(order.getId(), value(line.getRequiredPackageCount()));
|
||||||
|
Map<String, DropshipPalletSnapshot> dropshipPallets = "ONE_CLICK_DROPSHIP".equals(order.getOrderType())
|
||||||
|
? dropshipOrders.palletSnapshots(customerId, order.getId()).stream()
|
||||||
|
.collect(Collectors.toMap(DropshipPalletSnapshot::sourcePalletNo, value -> value))
|
||||||
|
: Map.of();
|
||||||
for (InboundPlanPalletAllocation allocation : lineAllocations) {
|
for (InboundPlanPalletAllocation allocation : lineAllocations) {
|
||||||
InboundPlanPallet pallet = requirePallet(palletById, allocation);
|
InboundPlanPallet pallet = requirePallet(palletById, allocation);
|
||||||
if ("ONE_CLICK_DROPSHIP".equals(order.getOrderType())) {
|
if ("ONE_CLICK_DROPSHIP".equals(order.getOrderType())) {
|
||||||
created.add(persist(customerId, plan, task, line, allocation, pallet, order,
|
if (value(allocation.getPickedPackageCount()) != value(allocation.getPlannedPackageCount())) {
|
||||||
SkuSnapshot.unspecified(value(allocation.getPickedPackageCount())), receivedAt));
|
throw conflict("One-click dropship inbound must receive a complete source pallet");
|
||||||
|
}
|
||||||
|
DropshipPalletSnapshot sourcePallet = dropshipPallets.get(pallet.getPalletNo());
|
||||||
|
if (sourcePallet == null || sourcePallet.packageCount() != value(allocation.getPickedPackageCount())) {
|
||||||
|
throw conflict("One-click dropship source pallet SKU package total does not match PDA receipt");
|
||||||
|
}
|
||||||
|
for (DropshipPalletSnapshot.Sku sku : sourcePallet.skus()) {
|
||||||
|
created.add(persist(customerId, plan, task, line, allocation, pallet, order,
|
||||||
|
new SkuSnapshot(sku.skuCode(), sku.productName(), null, sku.packageCount()), receivedAt));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
long allocationPackages = value(allocation.getPickedPackageCount());
|
long allocationPackages = value(allocation.getPickedPackageCount());
|
||||||
for (SkuSnapshot sku : consume(skuRemainders, allocationPackages)) {
|
for (SkuSnapshot sku : consume(skuRemainders, allocationPackages)) {
|
||||||
@@ -279,9 +293,5 @@ public class InboundStockReceiptService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record SkuSnapshot(String skuCode, String productNameZh, String productNameEn, long packages) {
|
private record SkuSnapshot(String skuCode, String productNameZh, String productNameEn, long packages) {}
|
||||||
private static SkuSnapshot unspecified(long packages) {
|
|
||||||
return new SkuSnapshot("UNSPECIFIED", null, null, packages);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-11
@@ -30,6 +30,7 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -62,6 +63,32 @@ public class InboundPlanService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public InboundPlanResponse convert(String customerId, Long selectedOrderId, List<String> sourcePalletNos) {
|
public InboundPlanResponse convert(String customerId, Long selectedOrderId, List<String> sourcePalletNos) {
|
||||||
|
return convert(customerId, selectedOrderId, sourcePalletNos, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public InboundPlanResponse convertDropship(String customerId, Long selectedOrderId,
|
||||||
|
List<WholePalletInput> sourcePallets) {
|
||||||
|
if (sourcePallets == null || sourcePallets.isEmpty()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||||
|
"One-click dropship inbound requires source pallets");
|
||||||
|
}
|
||||||
|
HashSet<String> palletNos = new HashSet<>();
|
||||||
|
for (WholePalletInput pallet : sourcePallets) {
|
||||||
|
if (pallet == null || trimToNull(pallet.palletNo()) == null || pallet.packageCount() <= 0) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||||
|
"One-click dropship source pallet number and package count are required");
|
||||||
|
}
|
||||||
|
if (!palletNos.add(pallet.palletNo().trim())) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||||
|
"Duplicate one-click dropship source pallet number: " + pallet.palletNo());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return convert(customerId, selectedOrderId, List.of(), List.copyOf(sourcePallets));
|
||||||
|
}
|
||||||
|
|
||||||
|
private InboundPlanResponse convert(String customerId, Long selectedOrderId, List<String> sourcePalletNos,
|
||||||
|
List<WholePalletInput> wholePallets) {
|
||||||
List<SourceOrderSnapshot> orders = orderMapper.lockOrder(customerId, selectedOrderId);
|
List<SourceOrderSnapshot> orders = orderMapper.lockOrder(customerId, selectedOrderId);
|
||||||
if (orders == null || orders.isEmpty()) {
|
if (orders == null || orders.isEmpty()) {
|
||||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound order not found");
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound order not found");
|
||||||
@@ -91,21 +118,27 @@ public class InboundPlanService {
|
|||||||
Map<Long, SourceOrderSnapshot> orderById = orders.stream()
|
Map<Long, SourceOrderSnapshot> orderById = orders.stream()
|
||||||
.collect(Collectors.toMap(SourceOrderSnapshot::orderId, value -> value));
|
.collect(Collectors.toMap(SourceOrderSnapshot::orderId, value -> value));
|
||||||
Map<Long, InboundPlanLine> lineByOrder = createLines(plan, orders);
|
Map<Long, InboundPlanLine> lineByOrder = createLines(plan, orders);
|
||||||
PalletAllocator.Result allocation = PalletAllocator.allocate(orders.stream()
|
PalletAllocator.Result allocation = wholePallets == null ? PalletAllocator.allocate(orders.stream()
|
||||||
.map(order -> new PalletAllocator.LineInput(order.orderId(), warehouseKey(order), volume(order),
|
.map(order -> new PalletAllocator.LineInput(order.orderId(), warehouseKey(order), volume(order),
|
||||||
order.packageCount(), order.recommendedPalletCount()))
|
order.packageCount(), order.recommendedPalletCount())).toList()) : null;
|
||||||
.toList());
|
if (wholePallets != null) validateWholePallets(orders, wholePallets);
|
||||||
plan.setPalletCount(allocation.pallets().size());
|
plan.setPalletCount(wholePallets == null ? allocation.pallets().size() : wholePallets.size());
|
||||||
planMapper.insert(plan);
|
planMapper.insert(plan);
|
||||||
|
|
||||||
lineByOrder.values().forEach(lineMapper::insert);
|
lineByOrder.values().forEach(lineMapper::insert);
|
||||||
Map<PalletAllocator.PalletDraft, InboundPlanPallet> persistedPallets = createPallets(plan, allocation, orderById,
|
Map<Long, Integer> palletCounts;
|
||||||
sourcePalletNos == null ? List.of() : sourcePalletNos);
|
if (wholePallets == null) {
|
||||||
createAllocations(plan, allocation, lineByOrder, persistedPallets);
|
Map<PalletAllocator.PalletDraft, InboundPlanPallet> persistedPallets = createPallets(plan, allocation,
|
||||||
|
orderById, sourcePalletNos == null ? List.of() : sourcePalletNos);
|
||||||
Map<Long, Integer> palletCounts = allocation.allocations().stream().collect(Collectors.groupingBy(
|
createAllocations(plan, allocation, lineByOrder, persistedPallets);
|
||||||
PalletAllocator.AllocationDraft::orderId,
|
palletCounts = allocation.allocations().stream().collect(Collectors.groupingBy(
|
||||||
Collectors.collectingAndThen(Collectors.mapping(item -> item.pallet().sequenceNo(), Collectors.toSet()), java.util.Set::size)));
|
PalletAllocator.AllocationDraft::orderId, Collectors.collectingAndThen(Collectors.mapping(
|
||||||
|
item -> item.pallet().sequenceNo(), Collectors.toSet()), java.util.Set::size)));
|
||||||
|
} else {
|
||||||
|
SourceOrderSnapshot order = orders.get(0);
|
||||||
|
createWholePallets(plan, order, lineByOrder.get(order.orderId()), wholePallets);
|
||||||
|
palletCounts = Map.of(order.orderId(), wholePallets.size());
|
||||||
|
}
|
||||||
for (SourceOrderSnapshot order : orders) {
|
for (SourceOrderSnapshot order : orders) {
|
||||||
int count = palletCounts.getOrDefault(order.orderId(), 0);
|
int count = palletCounts.getOrDefault(order.orderId(), 0);
|
||||||
InboundPlanLine line = lineByOrder.get(order.orderId());
|
InboundPlanLine line = lineByOrder.get(order.orderId());
|
||||||
@@ -120,6 +153,54 @@ public class InboundPlanService {
|
|||||||
return detail(customerId, plan);
|
return detail(customerId, plan);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateWholePallets(List<SourceOrderSnapshot> orders, List<WholePalletInput> pallets) {
|
||||||
|
if (orders.size() != 1) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||||
|
"One-click dropship inbound plan must belong to one inbound order");
|
||||||
|
}
|
||||||
|
int packages = pallets.stream().mapToInt(WholePalletInput::packageCount).sum();
|
||||||
|
if (packages != orders.get(0).packageCount()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||||
|
"One-click dropship pallet package total must equal inbound order package count");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createWholePallets(InboundPlan plan, SourceOrderSnapshot order, InboundPlanLine line,
|
||||||
|
List<WholePalletInput> sourcePallets) {
|
||||||
|
BigDecimal remainingVolume = volume(order);
|
||||||
|
int remainingPackages = order.packageCount();
|
||||||
|
for (int index = 0; index < sourcePallets.size(); index++) {
|
||||||
|
WholePalletInput source = sourcePallets.get(index);
|
||||||
|
BigDecimal palletVolume = index == sourcePallets.size() - 1 ? remainingVolume
|
||||||
|
: volume(order).multiply(BigDecimal.valueOf(source.packageCount()))
|
||||||
|
.divide(BigDecimal.valueOf(order.packageCount()), 4, java.math.RoundingMode.HALF_UP);
|
||||||
|
if (palletVolume.compareTo(remainingVolume) > 0) palletVolume = remainingVolume;
|
||||||
|
InboundPlanPallet pallet = new InboundPlanPallet();
|
||||||
|
pallet.setId(IdWorker.getId()); pallet.setCustomerId(plan.getCustomerId()); pallet.setPlanId(plan.getId());
|
||||||
|
pallet.setPalletNo(source.palletNo().trim()); pallet.setDestinationWarehouseId(order.destinationWarehouseId());
|
||||||
|
pallet.setDestinationWarehouseCode(order.destinationWarehouseCode());
|
||||||
|
pallet.setDestinationWarehouseName(order.destinationWarehouseName());
|
||||||
|
pallet.setCapacityCbm(PalletAllocator.PALLET_CAPACITY_CBM); pallet.setAllocatedVolumeCbm(palletVolume);
|
||||||
|
pallet.setSequenceNo(index + 1); pallet.setAssignmentStatus("UNASSIGNED");
|
||||||
|
pallet.setActualVolumeCbm(BigDecimal.ZERO); pallet.setActualPackageCount(0); palletMapper.insert(pallet);
|
||||||
|
|
||||||
|
InboundPlanPalletAllocation allocation = new InboundPlanPalletAllocation();
|
||||||
|
allocation.setId(IdWorker.getId()); allocation.setCustomerId(plan.getCustomerId()); allocation.setPlanId(plan.getId());
|
||||||
|
allocation.setLineId(line.getId()); allocation.setPalletId(pallet.getId());
|
||||||
|
allocation.setAllocatedVolumeCbm(palletVolume); allocation.setPlannedPackageCount(source.packageCount());
|
||||||
|
allocation.setCountedPackageCount(0); allocation.setPickedPackageCount(0);
|
||||||
|
allocation.setActualVolumeCbm(BigDecimal.ZERO); allocation.setStatus("PENDING_COUNT"); allocation.setVersion(0);
|
||||||
|
allocationMapper.insert(allocation);
|
||||||
|
remainingVolume = remainingVolume.subtract(palletVolume);
|
||||||
|
remainingPackages -= source.packageCount();
|
||||||
|
}
|
||||||
|
if (remainingPackages != 0 || remainingVolume.signum() != 0) {
|
||||||
|
throw new IllegalStateException("Failed to conserve one-click dropship pallet totals");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record WholePalletInput(String palletNo, int packageCount) {}
|
||||||
|
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public InboundPlanResponse detailByPlanNo(String customerId, String planNo) {
|
public InboundPlanResponse detailByPlanNo(String customerId, String planNo) {
|
||||||
|
|||||||
@@ -207,6 +207,11 @@ public class LoadingCompletedConsumer {
|
|||||||
catch (Exception exception) { throw new IllegalArgumentException("One-click dropship warehouseId is invalid", exception); }
|
catch (Exception exception) { throw new IllegalArgumentException("One-click dropship warehouseId is invalid", exception); }
|
||||||
dropshipOrders.receive(new DropshipRequests.Receive(customerId, order.getId(), warehouseId,
|
dropshipOrders.receive(new DropshipRequests.Receive(customerId, order.getId(), warehouseId,
|
||||||
event.eventId() + ":DROPSHIP", dropshipPallets));
|
event.eventId() + ":DROPSHIP", dropshipPallets));
|
||||||
|
InboundPlanResponse plan = inboundPlans.convertDropship(customerId, order.getId(), dropshipPallets.stream()
|
||||||
|
.map(pallet -> new InboundPlanService.WholePalletInput(pallet.palletNo(), pallet.skuPackages().stream()
|
||||||
|
.mapToInt(sku -> Math.toIntExact(sku.packageCount())).sum()))
|
||||||
|
.toList());
|
||||||
|
registerInTransit(event, payload, source, order, plan.planNo());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -97,7 +97,7 @@ class DropshipOrderServiceTest {
|
|||||||
when(cartons.listProfile("C-1", 2L)).thenReturn(List.of(secondCarton));
|
when(cartons.listProfile("C-1", 2L)).thenReturn(List.of(secondCarton));
|
||||||
|
|
||||||
service.activateAfterStockIn("C-1", 10L, 50L, List.of(
|
service.activateAfterStockIn("C-1", 10L, 50L, List.of(
|
||||||
receipt(101L, "WH-P-1", 201L, 1), receipt(102L, "WH-P-2", 202L, 1)));
|
receipt(101L, "SOURCE-P-1", 201L, 1), receipt(102L, "SOURCE-P-2", 202L, 1)));
|
||||||
|
|
||||||
assertThat(first.getStatus()).isEqualTo("ACTIVE");
|
assertThat(first.getStatus()).isEqualTo("ACTIVE");
|
||||||
assertThat(first.getWarehousePalletId()).isEqualTo(101L);
|
assertThat(first.getWarehousePalletId()).isEqualTo(101L);
|
||||||
|
|||||||
+52
@@ -12,6 +12,9 @@ import com.baiyue.lastleg.inboundplan.pda.entity.InboundPdaTask;
|
|||||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||||
import com.baiyue.lastleg.inboundplan.pda.gateway.UnloadPhotoFileGateway;
|
import com.baiyue.lastleg.inboundplan.pda.gateway.UnloadPhotoFileGateway;
|
||||||
import com.baiyue.lastleg.inboundplan.pda.repository.PdaWorkflowRepository;
|
import com.baiyue.lastleg.inboundplan.pda.repository.PdaWorkflowRepository;
|
||||||
|
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||||
|
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
|
||||||
|
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -48,6 +51,7 @@ class InboundPdaWorkflowServiceTest {
|
|||||||
@Mock InboundStockReceiptService stockReceipts;
|
@Mock InboundStockReceiptService stockReceipts;
|
||||||
@Mock InboundUnloadCompletedOutboxService unloadCompletedOutbox;
|
@Mock InboundUnloadCompletedOutboxService unloadCompletedOutbox;
|
||||||
@Mock UnloadPhotoFileGateway unloadPhotoFiles;
|
@Mock UnloadPhotoFileGateway unloadPhotoFiles;
|
||||||
|
@Mock DropshipOrderService dropshipOrders;
|
||||||
private InboundPdaWorkflowService service;
|
private InboundPdaWorkflowService service;
|
||||||
|
|
||||||
private InboundPlan plan;
|
private InboundPlan plan;
|
||||||
@@ -69,6 +73,7 @@ class InboundPdaWorkflowServiceTest {
|
|||||||
ReflectionTestUtils.setField(service, "stockReceipts", stockReceipts);
|
ReflectionTestUtils.setField(service, "stockReceipts", stockReceipts);
|
||||||
ReflectionTestUtils.setField(service, "unloadCompletedOutbox", unloadCompletedOutbox);
|
ReflectionTestUtils.setField(service, "unloadCompletedOutbox", unloadCompletedOutbox);
|
||||||
ReflectionTestUtils.setField(service, "unloadPhotoFiles", unloadPhotoFiles);
|
ReflectionTestUtils.setField(service, "unloadPhotoFiles", unloadPhotoFiles);
|
||||||
|
ReflectionTestUtils.setField(service, "dropshipOrders", dropshipOrders);
|
||||||
ReflectionTestUtils.setField(service, "sourceObjectMapper", new ObjectMapper().findAndRegisterModules());
|
ReflectionTestUtils.setField(service, "sourceObjectMapper", new ObjectMapper().findAndRegisterModules());
|
||||||
service.initializeObjectMapper();
|
service.initializeObjectMapper();
|
||||||
plan = plan();
|
plan = plan();
|
||||||
@@ -492,6 +497,53 @@ class InboundPdaWorkflowServiceTest {
|
|||||||
verify(unloadCompletedOutbox, never()).publish(any(), any(), any());
|
verify(unloadCompletedOutbox, never()).publish(any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dropshipTaskIsMarkedAndWholePalletPickReceivesThePlannedPalletAtOnce() {
|
||||||
|
plan.setSourceOrderId(101L);
|
||||||
|
LastlegOrder order = new LastlegOrder();
|
||||||
|
order.setId(101L); order.setOrderType("ONE_CLICK_DROPSHIP");
|
||||||
|
when(repository.order(101L)).thenReturn(order);
|
||||||
|
when(dropshipOrders.palletSnapshots("100", 101L)).thenReturn(List.of(
|
||||||
|
new DropshipPalletSnapshot("PLT-21", 3,
|
||||||
|
List.of(new DropshipPalletSnapshot.Sku("SKU-A", "Product A", 3, 30)))));
|
||||||
|
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
|
||||||
|
when(repository.allocations(plan.getId())).thenReturn(List.of(target));
|
||||||
|
when(repository.pallets(plan.getId())).thenReturn(List.of(targetPallet));
|
||||||
|
when(repository.task("100", plan.getId())).thenReturn(task);
|
||||||
|
activeTask();
|
||||||
|
when(repository.lockAllocation(plan.getId(), target.getId())).thenReturn(target);
|
||||||
|
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
|
||||||
|
when(repository.lockPallet(plan.getId(), targetPallet.getId())).thenReturn(targetPallet);
|
||||||
|
|
||||||
|
var taskResult = service.task("100", "LIP1");
|
||||||
|
var pickResult = service.pickWholePallet("100", "LIP1", target.getId(),
|
||||||
|
new PdaRequests.WholePalletOperation("whole-1", "7", "worker"));
|
||||||
|
|
||||||
|
assertEquals("ONE_CLICK_DROPSHIP", taskResult.orderType());
|
||||||
|
assertTrue(taskResult.wholePalletInbound());
|
||||||
|
assertEquals("SKU-A", taskResult.dropshipPallets().get(0).skus().get(0).skuCode());
|
||||||
|
assertEquals(targetPallet.getId(), taskResult.dropshipPallets().get(0).planPalletId());
|
||||||
|
assertEquals(target.getId(), taskResult.dropshipPallets().get(0).allocationId());
|
||||||
|
assertEquals("WHOLE_PALLET_PICKED", pickResult.result());
|
||||||
|
assertEquals(3, target.getPickedPackageCount());
|
||||||
|
assertEquals(3, targetPallet.getActualPackageCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dropshipPlanRejectsOrdinaryPackagePicking() {
|
||||||
|
plan.setSourceOrderId(101L);
|
||||||
|
LastlegOrder order = new LastlegOrder();
|
||||||
|
order.setId(101L); order.setOrderType("ONE_CLICK_DROPSHIP");
|
||||||
|
when(repository.order(101L)).thenReturn(order);
|
||||||
|
|
||||||
|
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||||
|
() -> service.pick("100", "LIP1", target.getId(),
|
||||||
|
new PdaRequests.PackageOperation(1, "wrong-mode", "7", "worker")));
|
||||||
|
|
||||||
|
assertEquals(409, exception.getStatusCode().value());
|
||||||
|
verify(repository, never()).lockAllocation(plan.getId(), target.getId());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsReuseOfIdempotencyKeyWithDifferentActionOrPayload() {
|
void rejectsReuseOfIdempotencyKeyWithDifferentActionOrPayload() {
|
||||||
InboundPdaOperation existing = new InboundPdaOperation();
|
InboundPdaOperation existing = new InboundPdaOperation();
|
||||||
|
|||||||
+23
@@ -17,6 +17,7 @@ import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
|
|||||||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||||
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||||
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||||
|
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
@@ -166,6 +167,7 @@ class InboundStockReceiptServiceTest {
|
|||||||
void activatesDropshipAfterGenericStockInReceiptIsCreated() {
|
void activatesDropshipAfterGenericStockInReceiptIsCreated() {
|
||||||
Fixture fixture = fixture(5, 5, 5, 10);
|
Fixture fixture = fixture(5, 5, 5, 10);
|
||||||
fixture.order.setOrderType("ONE_CLICK_DROPSHIP");
|
fixture.order.setOrderType("ONE_CLICK_DROPSHIP");
|
||||||
|
when(dropshipOrders.palletSnapshots("100", 101L)).thenReturn(List.of(dropshipPallet("PLT-21", 5)));
|
||||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||||
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
|
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
|
||||||
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 5, 0, 0, 0, false, false));
|
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 5, 0, 0, 0, false, false));
|
||||||
@@ -183,6 +185,7 @@ class InboundStockReceiptServiceTest {
|
|||||||
void partialStockInSynchronizesOrderWithoutActivatingDropship() {
|
void partialStockInSynchronizesOrderWithoutActivatingDropship() {
|
||||||
Fixture fixture = fixture(5, 2, 2, 10);
|
Fixture fixture = fixture(5, 2, 2, 10);
|
||||||
fixture.order.setOrderType("ONE_CLICK_DROPSHIP");
|
fixture.order.setOrderType("ONE_CLICK_DROPSHIP");
|
||||||
|
when(dropshipOrders.palletSnapshots("100", 101L)).thenReturn(List.of(dropshipPallet("PLT-21", 2)));
|
||||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||||
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
|
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
|
||||||
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 2, 0, 0, 0, false, false));
|
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 2, 0, 0, 0, false, false));
|
||||||
@@ -197,6 +200,26 @@ class InboundStockReceiptServiceTest {
|
|||||||
verify(dropshipOrders, never()).activateAfterStockIn(any(), any(), any(), any());
|
verify(dropshipOrders, never()).activateAfterStockIn(any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsPartiallyPickedDropshipSourcePallet() {
|
||||||
|
Fixture fixture = fixture(5, 5, 2, 10);
|
||||||
|
fixture.order.setOrderType("ONE_CLICK_DROPSHIP");
|
||||||
|
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||||
|
when(dropshipOrders.palletSnapshots("100", 101L)).thenReturn(List.of(dropshipPallet("PLT-21", 5)));
|
||||||
|
|
||||||
|
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||||
|
() -> service.stockIn("100", fixture.plan, fixture.task, List.of(fixture.line),
|
||||||
|
List.of(fixture.allocation), List.of(fixture.pallet), LocalDateTime.now(), true));
|
||||||
|
|
||||||
|
assertEquals(409, exception.getStatusCode().value());
|
||||||
|
verify(inventory, never()).stockIn(any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
private DropshipPalletSnapshot dropshipPallet(String palletNo, long packages) {
|
||||||
|
return new DropshipPalletSnapshot(palletNo, packages,
|
||||||
|
List.of(new DropshipPalletSnapshot.Sku("SKU-1", "Product", packages, packages * 10)));
|
||||||
|
}
|
||||||
|
|
||||||
private Fixture fixture(int required, int planned, int picked, int quantity) {
|
private Fixture fixture(int required, int planned, int picked, int quantity) {
|
||||||
InboundPlan plan = new InboundPlan(); plan.setId(1L); plan.setPlanNo("IP-1");
|
InboundPlan plan = new InboundPlan(); plan.setId(1L); plan.setPlanNo("IP-1");
|
||||||
InboundPdaTask task = new InboundPdaTask(); task.setId(2L); task.setOperatorId("operator");
|
InboundPdaTask task = new InboundPdaTask(); task.setId(2L); task.setOperatorId("operator");
|
||||||
|
|||||||
+21
@@ -146,6 +146,27 @@ class InboundPlanServiceTest {
|
|||||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void convertsDropshipSourcePalletsWithoutRedistributingTheirPackages() {
|
||||||
|
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(order(2L, "RK-2", "1.0000", "WH-A")));
|
||||||
|
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
|
||||||
|
.thenReturn(1);
|
||||||
|
|
||||||
|
service.convertDropship("100", 2L, List.of(
|
||||||
|
new InboundPlanService.WholePalletInput("SOURCE-P-1", 3),
|
||||||
|
new InboundPlanService.WholePalletInput("SOURCE-P-2", 7)));
|
||||||
|
|
||||||
|
ArgumentCaptor<InboundPlanPallet> pallets = ArgumentCaptor.forClass(InboundPlanPallet.class);
|
||||||
|
verify(palletMapper, times(2)).insert(pallets.capture());
|
||||||
|
assertEquals(List.of("SOURCE-P-1", "SOURCE-P-2"),
|
||||||
|
pallets.getAllValues().stream().map(InboundPlanPallet::getPalletNo).toList());
|
||||||
|
ArgumentCaptor<InboundPlanPalletAllocation> allocations =
|
||||||
|
ArgumentCaptor.forClass(InboundPlanPalletAllocation.class);
|
||||||
|
verify(allocationMapper, times(2)).insert(allocations.capture());
|
||||||
|
assertEquals(List.of(3, 7), allocations.getAllValues().stream()
|
||||||
|
.map(InboundPlanPalletAllocation::getPlannedPackageCount).toList());
|
||||||
|
}
|
||||||
|
|
||||||
private SourceOrderSnapshot order(Long id, String inboundCode, String volume, String warehouse) {
|
private SourceOrderSnapshot order(Long id, String inboundCode, String volume, String warehouse) {
|
||||||
return new SourceOrderSnapshot(id, "100", "CONT-1", inboundCode, "PENDING_ASSIGNMENT", null,
|
return new SourceOrderSnapshot(id, "100", "CONT-1", inboundCode, "PENDING_ASSIGNMENT", null,
|
||||||
new BigDecimal(volume), new BigDecimal(volume).multiply(BigDecimal.TEN).intValue(),
|
new BigDecimal(volume), new BigDecimal(volume).multiply(BigDecimal.TEN).intValue(),
|
||||||
|
|||||||
+2
-2
@@ -3,12 +3,12 @@ import com.baiyue.common.event.*; import com.baiyue.lastleg.entity.*; import com
|
|||||||
@ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class)
|
@ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class)
|
||||||
class LoadingCompletedConsumerTest {
|
class LoadingCompletedConsumerTest {
|
||||||
@Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegInboundOrderMapper inboundOrders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock InboundPlanCartonMapper cartons; @Mock DropshipOrderService dropshipOrders; @Mock InboundPlanService inboundPlans; @Mock InventoryCommandService inventory; ObjectMapper json=new ObjectMapper().findAndRegisterModules(); LoadingCompletedConsumer service;
|
@Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegInboundOrderMapper inboundOrders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock InboundPlanCartonMapper cartons; @Mock DropshipOrderService dropshipOrders; @Mock InboundPlanService inboundPlans; @Mock InventoryCommandService inventory; ObjectMapper json=new ObjectMapper().findAndRegisterModules(); LoadingCompletedConsumer service;
|
||||||
@BeforeEach void setup(){ service=new LoadingCompletedConsumer(); ReflectionTestUtils.setField(service,"inboxMapper",inbox); ReflectionTestUtils.setField(service,"orderMapper",orders); ReflectionTestUtils.setField(service,"inboundOrderMapper",inboundOrders); ReflectionTestUtils.setField(service,"detailMapper",details); ReflectionTestUtils.setField(service,"historyMapper",histories); ReflectionTestUtils.setField(service,"cartonMapper",cartons); ReflectionTestUtils.setField(service,"dropshipOrders",dropshipOrders); ReflectionTestUtils.setField(service,"inboundPlans",inboundPlans); ReflectionTestUtils.setField(service,"inventory",inventory); ReflectionTestUtils.setField(service,"objectMapper",json); ReflectionTestUtils.setField(service,"orderNoGenerator",new LastlegOrderNoGenerator()); lenient().doAnswer(i->{ LastlegOrder o=i.getArgument(0); o.setId(Math.abs(o.getSourceInboundId().hashCode())+1L); return 1; }).when(orders).insert(any(LastlegOrder.class)); lenient().when(inboundPlans.convert(anyString(),anyLong())).thenReturn(new InboundPlanResponse(1L,"LIP-1","100",1L,"MSCU1","PENDING_ASSIGNMENT",1,1,BigDecimal.ONE,List.of(),List.of(),List.of())); }
|
@BeforeEach void setup(){ service=new LoadingCompletedConsumer(); ReflectionTestUtils.setField(service,"inboxMapper",inbox); ReflectionTestUtils.setField(service,"orderMapper",orders); ReflectionTestUtils.setField(service,"inboundOrderMapper",inboundOrders); ReflectionTestUtils.setField(service,"detailMapper",details); ReflectionTestUtils.setField(service,"historyMapper",histories); ReflectionTestUtils.setField(service,"cartonMapper",cartons); ReflectionTestUtils.setField(service,"dropshipOrders",dropshipOrders); ReflectionTestUtils.setField(service,"inboundPlans",inboundPlans); ReflectionTestUtils.setField(service,"inventory",inventory); ReflectionTestUtils.setField(service,"objectMapper",json); ReflectionTestUtils.setField(service,"orderNoGenerator",new LastlegOrderNoGenerator()); lenient().doAnswer(i->{ LastlegOrder o=i.getArgument(0); o.setId(Math.abs(o.getSourceInboundId().hashCode())+1L); return 1; }).when(orders).insert(any(LastlegOrder.class)); var plan=new InboundPlanResponse(1L,"LIP-1","100",1L,"MSCU1","PENDING_ASSIGNMENT",1,1,BigDecimal.ONE,List.of(),List.of(),List.of()); lenient().when(inboundPlans.convert(anyString(),anyLong())).thenReturn(plan); lenient().when(inboundPlans.convertDropship(anyString(),anyLong(),anyList())).thenReturn(plan); }
|
||||||
@Test void createsOneOrderAndOneInboundPlanPerInbound(){ EventEnvelope event=event(); assertEquals(2,service.consume(event)); verify(orders,times(2)).insert(any(LastlegOrder.class)); verify(inboundPlans,times(2)).convert(eq("100"),anyLong()); verify(inventory,times(2)).registerInTransit(eq("100"),eq("SYSTEM"),any(InventoryCommandRequests.InTransit.class)); verify(details,times(2)).insert(any(LastlegOrderDetail.class)); verify(histories,times(2)).insert(any(LastlegOrderStatusHistory.class)); ArgumentCaptor<LastlegOrder> c=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(c.capture()); assertEquals(Set.of("in-1","in-2"),c.getAllValues().stream().map(LastlegOrder::getSourceInboundId).collect(java.util.stream.Collectors.toSet())); assertTrue(c.getAllValues().stream().allMatch(o->"PENDING_ASSIGNMENT".equals(o.getCurrentStatus()))); assertTrue(c.getAllValues().stream().allMatch(o->o.getId()!=null)); assertTrue(c.getAllValues().stream().allMatch(o->"MSCU1".equals(o.getContainerNo()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertEquals(new LastlegOrderNoGenerator().forContainerNo("MSCU1"),c.getAllValues().get(0).getOrderNo()); }
|
@Test void createsOneOrderAndOneInboundPlanPerInbound(){ EventEnvelope event=event(); assertEquals(2,service.consume(event)); verify(orders,times(2)).insert(any(LastlegOrder.class)); verify(inboundPlans,times(2)).convert(eq("100"),anyLong()); verify(inventory,times(2)).registerInTransit(eq("100"),eq("SYSTEM"),any(InventoryCommandRequests.InTransit.class)); verify(details,times(2)).insert(any(LastlegOrderDetail.class)); verify(histories,times(2)).insert(any(LastlegOrderStatusHistory.class)); ArgumentCaptor<LastlegOrder> c=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(c.capture()); assertEquals(Set.of("in-1","in-2"),c.getAllValues().stream().map(LastlegOrder::getSourceInboundId).collect(java.util.stream.Collectors.toSet())); assertTrue(c.getAllValues().stream().allMatch(o->"PENDING_ASSIGNMENT".equals(o.getCurrentStatus()))); assertTrue(c.getAllValues().stream().allMatch(o->o.getId()!=null)); assertTrue(c.getAllValues().stream().allMatch(o->"MSCU1".equals(o.getContainerNo()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertEquals(new LastlegOrderNoGenerator().forContainerNo("MSCU1"),c.getAllValues().get(0).getOrderNo()); }
|
||||||
@Test void repeatedEventReturnsExistingEffect(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash(event.payloadHash()); when(inbox.selectById(event.eventId())).thenReturn(row); when(orders.countContainer("100","MSCU1")).thenReturn(2L); assertEquals(2,service.consume(event)); verify(orders).countContainer("100","MSCU1"); verify(orders,never()).insert(any(LastlegOrder.class)); verify(details,never()).insert(any(LastlegOrderDetail.class)); }
|
@Test void repeatedEventReturnsExistingEffect(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash(event.payloadHash()); when(inbox.selectById(event.eventId())).thenReturn(row); when(orders.countContainer("100","MSCU1")).thenReturn(2L); assertEquals(2,service.consume(event)); verify(orders).countContainer("100","MSCU1"); verify(orders,never()).insert(any(LastlegOrder.class)); verify(details,never()).insert(any(LastlegOrderDetail.class)); }
|
||||||
@Test void sameEventWithDifferentHashIsRejected(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash("sha256:"+"0".repeat(64)); when(inbox.selectById(event.eventId())).thenReturn(row); assertThrows(IllegalArgumentException.class,()->service.consume(event)); }
|
@Test void sameEventWithDifferentHashIsRejected(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash("sha256:"+"0".repeat(64)); when(inbox.selectById(event.eventId())).thenReturn(row); assertThrows(IllegalArgumentException.class,()->service.consume(event)); }
|
||||||
@Test void rejectsEventFromWrongRegion(){ EventEnvelope source=event(); EventEnvelope invalid=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),RegionCode.OVERSEAS,RegionCode.CN,source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),source.payload(),source.payloadHash()); assertThrows(IllegalArgumentException.class,()->service.consume(invalid)); }
|
@Test void rejectsEventFromWrongRegion(){ EventEnvelope source=event(); EventEnvelope invalid=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),RegionCode.OVERSEAS,RegionCode.CN,source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),source.payload(),source.payloadHash()); assertThrows(IllegalArgumentException.class,()->service.consume(invalid)); }
|
||||||
@Test void consumesFormalDropshipPalletSnapshots(){ var sku=new FirstLegLoadingPlanCompletedV1.SkuPackageSnapshot("SKU-A","Product A",2,20,List.of("BOX-1","BOX-2")); var pallet=new FirstLegLoadingPlanCompletedV1.PalletSnapshot("PLT-1",List.of(sku)); var item=new FirstLegLoadingPlanCompletedV1.Item("d-1","SKU-A","Product A","Product A",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,null,"MARK",null,List.of()); var order=new FirstLegLoadingPlanCompletedV1.Order("in-d","root-d","ord-d","FL-D","root-od","RK-D","wh-1","ONT8","ONT8",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,List.of(item),"100","ONE_CLICK_DROPSHIP",List.of(pallet)); var payload=new FirstLegLoadingPlanCompletedV1("plan-d","LP-D","book-d","CONT-D","CAB-D","92","Warehouse",List.of(order)); JsonNode node=json.valueToTree(payload); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); var event=new EventEnvelope("123e4567-e89b-12d3-a456-426614174010",EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,1,Instant.now(),RegionCode.CN,RegionCode.OVERSEAS,"100","LOADING_PLAN","plan-d",2,1L,"123e4567-e89b-12d3-a456-426614174011","FIRSTLEG_LOADING_COMPLETED:plan-d:100:1",null,node,hash); assertEquals(1,service.consume(event)); ArgumentCaptor<DropshipRequests.Receive> captor=ArgumentCaptor.forClass(DropshipRequests.Receive.class); verify(dropshipOrders).receive(captor.capture()); assertEquals("100",captor.getValue().customerId()); assertEquals(1,captor.getValue().pallets().size()); assertEquals("PLT-1",captor.getValue().pallets().get(0).palletNo()); assertEquals(List.of("BOX-1","BOX-2"),captor.getValue().pallets().get(0).skuPackages().get(0).cartonMarks()); }
|
@Test void consumesFormalDropshipPalletSnapshots(){ var sku=new FirstLegLoadingPlanCompletedV1.SkuPackageSnapshot("SKU-A","Product A",2,20,List.of("BOX-1","BOX-2")); var pallet=new FirstLegLoadingPlanCompletedV1.PalletSnapshot("PLT-1",List.of(sku)); var item=new FirstLegLoadingPlanCompletedV1.Item("d-1","SKU-A","Product A","Product A",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,null,"MARK",null,List.of()); var order=new FirstLegLoadingPlanCompletedV1.Order("in-d","root-d","ord-d","FL-D","root-od","RK-D","wh-1","ONT8","ONT8",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,List.of(item),"100","ONE_CLICK_DROPSHIP",List.of(pallet)); var payload=new FirstLegLoadingPlanCompletedV1("plan-d","LP-D","book-d","CONT-D","CAB-D","92","Warehouse",List.of(order)); JsonNode node=json.valueToTree(payload); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); var event=new EventEnvelope("123e4567-e89b-12d3-a456-426614174010",EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,1,Instant.now(),RegionCode.CN,RegionCode.OVERSEAS,"100","LOADING_PLAN","plan-d",2,1L,"123e4567-e89b-12d3-a456-426614174011","FIRSTLEG_LOADING_COMPLETED:plan-d:100:1",null,node,hash); assertEquals(1,service.consume(event)); ArgumentCaptor<DropshipRequests.Receive> captor=ArgumentCaptor.forClass(DropshipRequests.Receive.class); verify(dropshipOrders).receive(captor.capture()); assertEquals("100",captor.getValue().customerId()); assertEquals(1,captor.getValue().pallets().size()); assertEquals("PLT-1",captor.getValue().pallets().get(0).palletNo()); assertEquals(List.of("BOX-1","BOX-2"),captor.getValue().pallets().get(0).skuPackages().get(0).cartonMarks()); ArgumentCaptor<List<InboundPlanService.WholePalletInput>> plans=ArgumentCaptor.forClass(List.class); verify(inboundPlans).convertDropship(eq("100"),anyLong(),plans.capture()); assertEquals(List.of(new InboundPlanService.WholePalletInput("PLT-1",2)),plans.getValue()); verify(inventory).registerInTransit(eq("100"),eq("SYSTEM"),any()); }
|
||||||
@Test void oneDropshipInboundWithMultipleSkusCreatesOneOrderAndMultipleSkuDetails(){
|
@Test void oneDropshipInboundWithMultipleSkusCreatesOneOrderAndMultipleSkuDetails(){
|
||||||
var itemA=new FirstLegLoadingPlanCompletedV1.Item("d-a","SKU-A","产品A","Product A",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,"SHIP-A","MARK-A",null,List.of("BOX-A1","BOX-A2"));
|
var itemA=new FirstLegLoadingPlanCompletedV1.Item("d-a","SKU-A","产品A","Product A",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,"SHIP-A","MARK-A",null,List.of("BOX-A1","BOX-A2"));
|
||||||
var itemB=new FirstLegLoadingPlanCompletedV1.Item("d-b","SKU-B","产品B","Product B",3,15,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,"SHIP-B","MARK-B",null,List.of("BOX-B1","BOX-B2","BOX-B3"));
|
var itemB=new FirstLegLoadingPlanCompletedV1.Item("d-b","SKU-B","产品B","Product B",3,15,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,"SHIP-B","MARK-B",null,List.of("BOX-B1","BOX-B2","BOX-B3"));
|
||||||
|
|||||||
Reference in New Issue
Block a user