This commit is contained in:
2026-08-20 18:02:38 +08:00
parent b55b049e77
commit 1cd8b6a473
210 changed files with 6974 additions and 825 deletions
@@ -1,20 +1,25 @@
package com.baiyue.lastleg.inboundplan.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.inboundplan.dto.ConvertInboundPlanRequest;
import com.baiyue.lastleg.inboundplan.dto.BatchFbaAppointmentRequest;
import com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanSummary;
import com.baiyue.lastleg.inboundplan.dto.SupplementPalletRequest;
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
import com.baiyue.lastleg.security.OverseasPermissions;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
@@ -25,15 +30,27 @@ public class InboundPlanController {
@Resource private InboundPlanService service;
@Resource private InboundFbaAppointmentService appointmentService;
@GetMapping
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "')")
@Operation(summary = "Page inbound plans without PDA, internal-event or outbound workflow fields", description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_LIST)
public ApiResponse<PageResult<InboundPlanSummary>> page(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@Valid InboundPlanRequests.Query query) {
return ApiResponse.success(service.page(scope(user, customerId), query));
}
@PostMapping("/convert")
@Operation(summary = "Convert one inbound order to one inbound plan")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_CREATE + "')")
@Operation(summary = "Convert one inbound order to one inbound plan", description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_CREATE)
public ApiResponse<InboundPlanResponse> convert(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody ConvertInboundPlanRequest request) {
return ApiResponse.success(service.convert(scope(user, request.customerId()), request.orderId()));
}
@GetMapping("/{planNo}")
@Operation(summary = "Get an inbound plan with lines, pallets and allocations")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "')")
@Operation(summary = "Get an inbound plan with lines, pallets and allocations", description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_LIST)
public ApiResponse<InboundPlanResponse> detail(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String planNo,
@RequestParam(required = false) String customerId) {
@@ -41,7 +58,8 @@ public class InboundPlanController {
}
@PostMapping("/{planNo}/pallets/supplement")
@Operation(summary = "Add one printable supplemental pallet for a plan destination warehouse")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_EDIT + "')")
@Operation(summary = "Add one printable supplemental pallet for a plan destination warehouse", description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_EDIT)
public ApiResponse<InboundPlanResponse.Pallet> supplementPallet(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String planNo,
@Valid @RequestBody SupplementPalletRequest request) {
@@ -49,7 +67,8 @@ public class InboundPlanController {
}
@PostMapping("/fba-appointments/batch")
@Operation(summary = "Create one FBA appointment for selected inbound lines")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_EDIT + "')")
@Operation(summary = "Create one FBA appointment for selected inbound lines", description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_EDIT)
public ApiResponse<FbaAppointmentResponse> batchFbaAppointment(
@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody BatchFbaAppointmentRequest request) {
@@ -59,7 +78,8 @@ public class InboundPlanController {
}
@GetMapping("/fba-appointments/{appointmentNo}")
@Operation(summary = "Get an FBA appointment and its selected inbound lines")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "')")
@Operation(summary = "Get an FBA appointment and its selected inbound lines", description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_LIST)
public ApiResponse<FbaAppointmentResponse> fbaAppointmentDetail(
@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String appointmentNo,
@@ -0,0 +1,30 @@
package com.baiyue.lastleg.inboundplan.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import java.time.LocalDateTime;
public final class InboundPlanRequests {
private InboundPlanRequests() {
}
public record Query(
@Min(1) long page,
@Min(1) @Max(200) long size,
String planNo,
String inboundCode,
String status,
String overseasWarehouseId,
String destinationWarehouseCode,
String fbaWarehouseCode,
String appointmentStatus,
LocalDateTime createdFrom,
LocalDateTime createdTo
) {
public Query {
if (page == 0) page = 1;
if (size == 0) size = 20;
}
}
}
@@ -0,0 +1,32 @@
package com.baiyue.lastleg.inboundplan.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "Inbound plan row for the platform browser API; outbound workflow fields are intentionally excluded")
public record InboundPlanSummary(
Long id,
String planNo,
Long sourceOrderId,
String customerId,
String customerName,
String orderType,
String inboundCode,
String containerNo,
String overseasWarehouseId,
String overseasWarehouseName,
String destinationWarehouseId,
String destinationWarehouseCode,
String destinationWarehouseName,
String fbaWarehouseCode,
Integer packageCount,
BigDecimal totalVolumeCbm,
Integer palletCount,
String status,
String appointmentStatus,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
}
@@ -1,13 +1,50 @@
package com.baiyue.lastleg.inboundplan.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanSummary;
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface InboundPlanMapper extends BaseMapper<InboundPlan> {
String PAGE_FROM = " FROM ll_inbound_plan p JOIN ll_order o ON o.id=p.source_order_id AND o.customer_id=p.customer_id " +
"LEFT JOIN ll_inbound_plan_line l ON l.plan_id=p.id AND l.order_id=p.source_order_id " +
"WHERE p.customer_id=#{customerId}" +
"<if test='q.planNo != null and q.planNo != \"\"'> AND p.plan_no LIKE CONCAT('%',#{q.planNo},'%')</if>" +
"<if test='q.inboundCode != null and q.inboundCode != \"\"'> AND l.inbound_code LIKE CONCAT('%',#{q.inboundCode},'%')</if>" +
"<if test='q.status != null and q.status != \"\"'> AND p.status=#{q.status}</if>" +
"<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND o.overseas_warehouse_id=#{q.overseasWarehouseId}</if>" +
"<if test='q.destinationWarehouseCode != null and q.destinationWarehouseCode != \"\"'> AND l.destination_warehouse_code=#{q.destinationWarehouseCode}</if>" +
"<if test='q.fbaWarehouseCode != null and q.fbaWarehouseCode != \"\"'> AND l.fba_warehouse_code=#{q.fbaWarehouseCode}</if>" +
"<if test='q.appointmentStatus != null and q.appointmentStatus != \"\" and q.appointmentStatus != \"NOT_APPOINTED\"'> " +
"AND EXISTS (SELECT 1 FROM ll_inbound_fba_appointment_item ai JOIN ll_inbound_fba_appointment a ON a.id=ai.appointment_id " +
"WHERE ai.plan_id=p.id AND a.customer_id=#{customerId} AND a.appointment_status=#{q.appointmentStatus})</if>" +
"<if test='q.appointmentStatus == \"NOT_APPOINTED\"'> AND NOT EXISTS (SELECT 1 FROM ll_inbound_fba_appointment_item ai " +
"WHERE ai.plan_id=p.id)</if>" +
"<if test='q.createdFrom != null'> AND p.created_at &gt;=#{q.createdFrom}</if>" +
"<if test='q.createdTo != null'> AND p.created_at &lt;=#{q.createdTo}</if>";
@Select("<script>SELECT p.id,p.plan_no,p.source_order_id,p.customer_id,o.customer_name,o.order_type," +
"l.inbound_code,p.container_no,o.overseas_warehouse_id,o.overseas_warehouse_name," +
"l.destination_warehouse_id,l.destination_warehouse_code,l.destination_warehouse_name,l.fba_warehouse_code," +
"COALESCE(l.required_package_count,o.package_count,0) package_count,p.total_volume_cbm,p.pallet_count,p.status," +
"COALESCE((SELECT a.appointment_status FROM ll_inbound_fba_appointment_item ai " +
"JOIN ll_inbound_fba_appointment a ON a.id=ai.appointment_id WHERE ai.plan_id=p.id " +
"ORDER BY a.created_at DESC,a.id DESC LIMIT 1),'NOT_APPOINTED') appointment_status,p.created_at,p.updated_at" +
PAGE_FROM + " ORDER BY p.created_at DESC,p.id DESC LIMIT #{limit} OFFSET #{offset}</script>")
List<InboundPlanSummary> pageRows(@Param("customerId") String customerId,
@Param("q") InboundPlanRequests.Query query,
@Param("offset") long offset,
@Param("limit") long limit);
@Select("<script>SELECT COUNT(*)" + PAGE_FROM + "</script>")
long countRows(@Param("customerId") String customerId, @Param("q") InboundPlanRequests.Query query);
@Select("SELECT * FROM ll_inbound_plan WHERE customer_id=#{customerId} AND source_order_id=#{sourceOrderId} LIMIT 1")
InboundPlan findBySourceOrder(@Param("customerId") String customerId, @Param("sourceOrderId") Long sourceOrderId);
}
@@ -17,7 +17,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
@RestController
@RequestMapping("/api/platform/v1/last-leg/inbound-plans/{planNo}/pallets")
@RequestMapping("/api/pda/v1/last-leg/inbound-plans/{planNo}/pallets")
public class InboundPalletAssignmentController {
@Resource private InboundPdaWorkflowService service;
@@ -21,7 +21,7 @@ import org.springframework.web.server.ResponseStatusException;
import java.util.List;
@RestController
@RequestMapping("/api/platform/v1/last-leg/inbound-plans/{planNo}/pda")
@RequestMapping("/api/pda/v1/last-leg/inbound-plans/{planNo}")
public class InboundPdaController {
public static final String OPERATE_PERMISSION = "lastleg:pda:operate";
@Resource private InboundPdaWorkflowService service;
@@ -20,6 +20,9 @@ public class InboundStockReceipt {
private Long lineId;
private Long orderId;
private String inboundCode;
private String skuCode;
private String productNameZh;
private String productNameEn;
private Long warehousePalletId;
private String palletNo;
private Long warehouseLocationId;
@@ -2,6 +2,7 @@ package com.baiyue.lastleg.inboundplan.pda.service;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.entity.LastlegOrderDetail;
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
@@ -13,6 +14,7 @@ import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse;
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
@@ -34,6 +36,7 @@ import java.util.stream.Collectors;
@Service
public class InboundStockReceiptService {
@Resource private LastlegOrderMapper orders;
@Resource private LastlegOrderDetailMapper orderDetails;
@Resource private InboundStockReceiptMapper receipts;
@Resource private InventoryCommandService inventory;
@Resource private DropshipOrderService dropshipOrders;
@@ -79,9 +82,22 @@ public class InboundStockReceiptService {
if (pickedPackages != value(line.getRequiredPackageCount())) {
throw conflict("PDA picked packages must equal the inbound order package total");
}
List<SkuRemainder> skuRemainders = "ONE_CLICK_DROPSHIP".equals(order.getOrderType())
? List.of() : skuRemainders(order.getId(), pickedPackages);
for (InboundPlanPalletAllocation allocation : lineAllocations) {
created.add(persist(customerId, plan, task, line, allocation,
requirePallet(palletById, allocation), order, receivedAt));
InboundPlanPallet pallet = requirePallet(palletById, allocation);
if ("ONE_CLICK_DROPSHIP".equals(order.getOrderType())) {
created.add(persist(customerId, plan, task, line, allocation, pallet, order,
SkuSnapshot.unspecified(value(allocation.getPickedPackageCount())), receivedAt));
} else {
long allocationPackages = value(allocation.getPickedPackageCount());
for (SkuSnapshot sku : consume(skuRemainders, allocationPackages)) {
created.add(persist(customerId, plan, task, line, allocation, pallet, order, sku, receivedAt));
}
}
}
if (skuRemainders.stream().anyMatch(value -> value.remaining > 0)) {
throw conflict("Inbound SKU package snapshots were not fully received");
}
}
activateDropship(customerId, task.getId(), orderById, created);
@@ -112,20 +128,21 @@ public class InboundStockReceiptService {
private InboundStockReceipt persist(String customerId, InboundPlan plan, InboundPdaTask task, InboundPlanLine line,
InboundPlanPalletAllocation allocation, InboundPlanPallet pallet,
LastlegOrder order, LocalDateTime receivedAt) {
long packages = value(allocation.getPickedPackageCount());
LastlegOrder order, SkuSnapshot sku, LocalDateTime receivedAt) {
long packages = sku.packages();
if (packages <= 0 || pallet.getWarehousePalletId() == null) {
throw conflict("Stock-in requires picked packages and an assigned warehouse pallet");
}
String destinationCode = required(first(line.getDestinationWarehouseCode(), order.getFbaWarehouseCode()),
"Destination warehouse code is required for stock-in");
String operationKey = "PDA_STOCK_IN:" + task.getId() + ":" + allocation.getId();
String operationKey = "PDA_STOCK_IN:" + task.getId() + ":" + allocation.getId() + ":" + sku.skuCode();
String overseasWarehouseId = required(order.getOverseasWarehouseId(),
"Actual overseas warehouse is required for stock-in");
InventoryCommandResponse result = inventory.stockIn(customerId, task.getOperatorId(),
new InventoryCommandRequests.StockIn(null, order.getCustomerName(), overseasWarehouseId,
null, order.getOverseasWarehouseName(), line.getDestinationWarehouseId(), destinationCode,
line.getDestinationWarehouseName(), order.getId(), plan.getPlanNo(), line.getInboundCode(),
sku.skuCode(), sku.productNameZh(), sku.productNameEn(),
pallet.getWarehousePalletId(), pallet.getPalletNo(), plan.getPlanNo(),
pallet.getWarehouseLocationId(), null, packages, order.getOrderNo(),
order.getSourceEventId(), operationKey,
@@ -140,6 +157,9 @@ public class InboundStockReceiptService {
receipt.setLineId(line.getId());
receipt.setOrderId(order.getId());
receipt.setInboundCode(line.getInboundCode());
receipt.setSkuCode(sku.skuCode());
receipt.setProductNameZh(sku.productNameZh());
receipt.setProductNameEn(sku.productNameEn());
receipt.setWarehousePalletId(pallet.getWarehousePalletId());
receipt.setPalletNo(pallet.getPalletNo());
receipt.setWarehouseLocationId(pallet.getWarehouseLocationId());
@@ -154,6 +174,46 @@ public class InboundStockReceiptService {
return receipt;
}
private List<SkuRemainder> skuRemainders(Long orderId, long expectedPackages) {
List<LastlegOrderDetail> details = orderDetails.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<LastlegOrderDetail>()
.eq(LastlegOrderDetail::getOrderId, orderId).orderByAsc(LastlegOrderDetail::getId));
Map<String, SkuRemainder> grouped = new java.util.LinkedHashMap<>();
for (LastlegOrderDetail detail : details) {
String skuCode = trim(detail.getSkuCode());
long packages = value(detail.getPackageCount());
if (skuCode == null || packages <= 0) continue;
SkuRemainder existing = grouped.get(skuCode);
if (existing == null) {
grouped.put(skuCode, new SkuRemainder(skuCode, trim(detail.getProductNameZh()),
trim(detail.getProductNameEn()), packages));
} else {
existing.remaining += packages;
}
}
long actualPackages = grouped.values().stream().mapToLong(value -> value.remaining).sum();
if (grouped.isEmpty() || actualPackages != expectedPackages) {
throw conflict("Inbound SKU package snapshots must exactly match the received package total");
}
return new java.util.ArrayList<>(grouped.values());
}
private static List<SkuSnapshot> consume(List<SkuRemainder> remainders, long packages) {
List<SkuSnapshot> result = new java.util.ArrayList<>();
long pending = packages;
for (SkuRemainder remainder : remainders) {
if (pending == 0) break;
if (remainder.remaining == 0) continue;
long accepted = Math.min(pending, remainder.remaining);
result.add(new SkuSnapshot(remainder.skuCode, remainder.productNameZh,
remainder.productNameEn, accepted));
remainder.remaining -= accepted;
pending -= accepted;
}
if (pending != 0) throw conflict("PDA received packages exceed the frozen SKU package snapshots");
return result;
}
private void activateDropship(String customerId, Long taskId, Map<Long, LastlegOrder> orderById,
List<InboundStockReceipt> stockReceipts) {
Map<Long, List<InboundStockReceipt>> receiptsByOrder = stockReceipts.stream()
@@ -190,4 +250,24 @@ public class InboundStockReceiptService {
private static ResponseStatusException conflict(String message) {
return new ResponseStatusException(HttpStatus.CONFLICT, message);
}
private static final class SkuRemainder {
private final String skuCode;
private final String productNameZh;
private final String productNameEn;
private long remaining;
private SkuRemainder(String skuCode, String productNameZh, String productNameEn, long remaining) {
this.skuCode = skuCode;
this.productNameZh = productNameZh;
this.productNameEn = productNameEn;
this.remaining = remaining;
}
}
private record SkuSnapshot(String skuCode, String productNameZh, String productNameEn, long packages) {
private static SkuSnapshot unspecified(long packages) {
return new SkuSnapshot("UNSPECIFIED", null, null, packages);
}
}
}
@@ -0,0 +1,54 @@
package com.baiyue.lastleg.inboundplan.pda.service;
import com.baiyue.common.event.EventEnvelope;
import com.baiyue.common.event.EventEnvelopeValidator;
import com.baiyue.lastleg.client.EventBridgeClient;
import com.baiyue.lastleg.entity.LastlegEventOutbox;
import com.baiyue.lastleg.repository.UnloadCompletedOutboxRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.UUID;
/** 至少一次投递海外卸柜完成事件;国内事件桥按 eventId 幂等接收。 */
@Component
@Slf4j
public class InboundUnloadCompletedOutboxRelay {
@Resource private UnloadCompletedOutboxRepository repository;
@Resource private EventBridgeClient client;
@Resource private ObjectMapper objectMapper;
@Value("${baiyue.security.internal-service-token}") private String internalToken;
@Value("${baiyue.firstleg.outbox-relay.lease-seconds:60}") private long leaseSeconds;
@Value("${baiyue.firstleg.outbox-relay.retry-seconds:30}") private long retrySeconds;
private final String workerId = UUID.randomUUID().toString();
@Scheduled(fixedDelayString = "${baiyue.firstleg.outbox-relay.fixed-delay-ms:5000}")
public void relay() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime expiredLeaseBefore = now.minusSeconds(Math.max(leaseSeconds, 10));
for (LastlegEventOutbox candidate : repository.pending(50, now, expiredLeaseBefore)) {
if (!repository.claim(candidate.getId(), workerId, now, expiredLeaseBefore)) continue;
LastlegEventOutbox row = repository.claimed(candidate.getId(), workerId);
if (row == null) continue;
try {
EventEnvelope event = objectMapper.readValue(row.getPayloadJson(), EventEnvelope.class);
EventEnvelopeValidator.requireValid(event);
var response = client.receive(internalToken, event);
if (response == null || !response.successful() || response.data() == null
|| !event.eventId().equals(response.data().eventId())) {
throw new IllegalStateException("国内事件桥未确认海外卸柜完成事件");
}
repository.published(row.getId(), workerId, LocalDateTime.now());
} catch (Exception exception) {
repository.retry(row.getId(), workerId, LocalDateTime.now().plusSeconds(Math.max(retrySeconds, 1)),
exception.getMessage());
log.warn("海外卸柜完成事件投递失败: eventKey={}, reason={}", row.getEventKey(), exception.getMessage());
}
}
}
}
@@ -2,7 +2,10 @@ package com.baiyue.lastleg.inboundplan.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanSummary;
import com.baiyue.lastleg.inboundplan.dto.SupplementPalletRequest;
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
@@ -36,6 +39,19 @@ public class InboundPlanService {
@Resource private InboundPlanPalletAllocationMapper allocationMapper;
@Resource private InboundPlanOrderMapper orderMapper;
@Transactional(readOnly = true)
public PageResult<InboundPlanSummary> page(String customerId, InboundPlanRequests.Query query) {
if (query.createdFrom() != null && query.createdTo() != null
&& query.createdFrom().isAfter(query.createdTo())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "createdFrom must not be after createdTo");
}
long total = planMapper.countRows(customerId, query);
List<InboundPlanSummary> records = total == 0 ? List.of()
: planMapper.pageRows(customerId, query, Math.multiplyExact(query.page() - 1, query.size()), query.size());
long pages = total == 0 ? 0 : (total + query.size() - 1) / query.size();
return new PageResult<>(records, query.page(), query.size(), total, pages);
}
@Transactional
public InboundPlanResponse convert(String customerId, Long selectedOrderId) {
List<SourceOrderSnapshot> orders = orderMapper.lockOrder(customerId, selectedOrderId);