This commit is contained in:
2026-08-18 11:39:40 +08:00
parent ac6c0d0395
commit d2c532f807
165 changed files with 2547 additions and 1060 deletions
+11 -2
View File
@@ -88,7 +88,6 @@ PDA 部分拣货会生成本柜实装 `LOADED` 分片以及可继续分柜的 `R
"loadingPlanNo": "LP202608130001", "loadingPlanNo": "LP202608130001",
"containerBookingId": "208500001", "containerBookingId": "208500001",
"containerNo": "MSCU1234567", "containerNo": "MSCU1234567",
"cabinetNo": "CAB20260813001",
"containerType": "40HQ", "containerType": "40HQ",
"sealNo": "SEAL001", "sealNo": "SEAL001",
"voyageNo": "V001", "voyageNo": "V001",
@@ -142,12 +141,20 @@ PDA 部分拣货会生成本柜实装 `LOADED` 分片以及可继续分柜的 `R
- 文件只传不可变文件引用、区域和摘要,不传二进制内容。 - 文件只传不可变文件引用、区域和摘要,不传二进制内容。
- V1 消费者忽略未知字段;已有字段不改变含义或类型。 - V1 消费者忽略未知字段;已有字段不改变含义或类型。
### 5.1 订单类型分支
`orderType`、`pallets`、`palletNo`、`skuPackages` 和 `cartonMarks` 必须正式加入头程公共事件契约及发送端,不能长期依赖尾程读取原始 JSON 扩展字段。三个服务升级到同一 `common-event-contract` 版本后再启用生产流量。
`ONE_CLICK_DROPSHIP` 的一笔最终实装入仓单可包含一个或多个来源托盘快照。消费时必须满足:所有托盘的 SKU 箱数之和等于订单箱数、SKU 数量之和等于订单数量、每个 SKU 的箱唛数等于其箱数、同一托盘内 SKU 不重复、同一订单内来源托盘号和箱唛不重复。通过后按来源托盘创建一件代发档案及托盘级 SKU 库存,并创建箱唛记录和不可变箱唛流水。`source_pallet_no` 是头程冻结快照;`warehouse_pallet_id/pallet_no` 是 Warehouse API 分配的海外仓实物托盘,两者不得混用。
非 `ONE_CLICK_DROPSHIP` 类型只创建 `ll_order`、冻结明细和初始状态历史,来源 `palletCount` 按 0 入库,不创建一件代发 SKU/箱唛库存。后续入仓 PDA 分配的托盘属于海外仓作业资源。
## 6. 尾程建单幂等 ## 6. 尾程建单幂等
尾程订单表必须有以下唯一约束: 尾程订单表必须有以下唯一约束:
```text ```text
UNIQUE (tenant_id, source_region, source_inbound_id) UNIQUE (customer_id, source_region, source_inbound_id)
``` ```
建议同时保存: 建议同时保存:
@@ -170,6 +177,8 @@ UNIQUE (tenant_id, source_region, source_inbound_id)
事件内任意一笔订单建单失败时,整个事件消费事务回滚,不允许出现同一客户同一柜次只生成部分尾程订单。 事件内任意一笔订单建单失败时,整个事件消费事务回滚,不允许出现同一客户同一柜次只生成部分尾程订单。
同一实际集装箱的订单共享 `order_no`、`container_no` 和 `source_loading_plan_id`;`ll_order.id` 仍是每条订单独立的雪花 ID。整柜查询、整柜操作和转入库计划统一以 `containerNo` 聚合,不使用虚拟柜号 `cabinetNo`。数据库必须保留 `KEY idx_ll_order_container (customer_id, container_no)`。
## 7. 尾程订单初始状态 ## 7. 尾程订单初始状态
事件自动生成的订单建议使用: 事件自动生成的订单建议使用:
@@ -20,7 +20,7 @@ public class BridgeInbox {
private Integer eventVersion; private Integer eventVersion;
private String sourceRegion; private String sourceRegion;
private String targetRegion; private String targetRegion;
private String tenantId; private String customerId;
private String aggregateType; private String aggregateType;
private String aggregateId; private String aggregateId;
private Long aggregateVersion; private Long aggregateVersion;
@@ -69,7 +69,7 @@ public class EventInboxService {
inbox.setEventVersion(envelope.eventVersion()); inbox.setEventVersion(envelope.eventVersion());
inbox.setSourceRegion(envelope.sourceRegion().name()); inbox.setSourceRegion(envelope.sourceRegion().name());
inbox.setTargetRegion(envelope.targetRegion().name()); inbox.setTargetRegion(envelope.targetRegion().name());
inbox.setTenantId(envelope.tenantId()); inbox.setCustomerId(envelope.tenantId());
inbox.setAggregateType(envelope.aggregateType()); inbox.setAggregateType(envelope.aggregateType());
inbox.setAggregateId(envelope.aggregateId()); inbox.setAggregateId(envelope.aggregateId());
inbox.setAggregateVersion(envelope.aggregateVersion()); inbox.setAggregateVersion(envelope.aggregateVersion());
@@ -34,7 +34,7 @@ public class HttpLastlegEventClient implements LastlegEventClient {
public String deliver(BridgeInbox row) throws Exception { public String deliver(BridgeInbox row) throws Exception {
EventEnvelope envelope = new EventEnvelope(row.getEventId(), row.getEventType(), row.getEventVersion(), EventEnvelope envelope = new EventEnvelope(row.getEventId(), row.getEventType(), row.getEventVersion(),
row.getOccurredAt(), RegionCode.valueOf(row.getSourceRegion()), RegionCode.valueOf(row.getTargetRegion()), row.getOccurredAt(), RegionCode.valueOf(row.getSourceRegion()), RegionCode.valueOf(row.getTargetRegion()),
row.getTenantId(), row.getAggregateType(), row.getAggregateId(), row.getAggregateVersion(), row.getCustomerId(), row.getAggregateType(), row.getAggregateId(), row.getAggregateVersion(),
row.getHandoverVersion(), row.getTraceId(), row.getCorrelationId(), row.getCausationId(), row.getHandoverVersion(), row.getTraceId(), row.getCorrelationId(), row.getCausationId(),
objectMapper.readTree(row.getPayloadJson()), row.getPayloadHash()); objectMapper.readTree(row.getPayloadJson()), row.getPayloadHash());
String body = objectMapper.writeValueAsString(envelope); String body = objectMapper.writeValueAsString(envelope);
@@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS bridge_outbox (
event_version INT NOT NULL COMMENT '事件契约版本', event_version INT NOT NULL COMMENT '事件契约版本',
source_region VARCHAR(32) NOT NULL COMMENT '来源区域', source_region VARCHAR(32) NOT NULL COMMENT '来源区域',
target_region VARCHAR(32) NOT NULL COMMENT '目标区域', target_region VARCHAR(32) NOT NULL COMMENT '目标区域',
tenant_id VARCHAR(128) NOT NULL COMMENT '租户标识', customer_id VARCHAR(128) NOT NULL COMMENT '租户标识',
aggregate_type VARCHAR(64) NOT NULL COMMENT '聚合类型', aggregate_type VARCHAR(64) NOT NULL COMMENT '聚合类型',
aggregate_id VARCHAR(128) NOT NULL COMMENT '聚合标识', aggregate_id VARCHAR(128) NOT NULL COMMENT '聚合标识',
aggregate_version BIGINT NOT NULL COMMENT '聚合版本', aggregate_version BIGINT NOT NULL COMMENT '聚合版本',
@@ -39,7 +39,7 @@ CREATE TABLE IF NOT EXISTS bridge_inbox (
event_version INT NOT NULL COMMENT '事件契约版本', event_version INT NOT NULL COMMENT '事件契约版本',
source_region VARCHAR(32) NOT NULL COMMENT '来源区域', source_region VARCHAR(32) NOT NULL COMMENT '来源区域',
target_region VARCHAR(32) NOT NULL COMMENT '目标区域', target_region VARCHAR(32) NOT NULL COMMENT '目标区域',
tenant_id VARCHAR(128) NOT NULL COMMENT '租户标识', customer_id VARCHAR(128) NOT NULL COMMENT '租户标识',
aggregate_type VARCHAR(64) NOT NULL COMMENT '聚合类型', aggregate_type VARCHAR(64) NOT NULL COMMENT '聚合类型',
aggregate_id VARCHAR(128) NOT NULL COMMENT '聚合标识', aggregate_id VARCHAR(128) NOT NULL COMMENT '聚合标识',
aggregate_version BIGINT NOT NULL COMMENT '聚合版本', aggregate_version BIGINT NOT NULL COMMENT '聚合版本',
+5 -2
View File
@@ -21,13 +21,17 @@
| 托盘分配算法 | `inboundplan/service/PalletAllocator.java` | | 托盘分配算法 | `inboundplan/service/PalletAllocator.java` |
| PDA 入仓、实收、上架、合托、完成 | `inboundplan/pda/controller/`、`inboundplan/pda/service/InboundPdaWorkflowService.java` | | PDA 入仓、实收、上架、合托、完成 | `inboundplan/pda/controller/`、`inboundplan/pda/service/InboundPdaWorkflowService.java` |
| 仓储资源调用 | `inboundplan/pda/gateway/WarehouseResourceGateway.java`、`HttpWarehouseResourceGateway.java` | | 仓储资源调用 | `inboundplan/pda/gateway/WarehouseResourceGateway.java`、`HttpWarehouseResourceGateway.java` |
| 仓储转运、历史订单/入仓绑定、可选换标联动 | `transfer/controller/WarehouseTransferController.java`、`transfer/service/WarehouseTransferService.java` |
| 一件代发接收、SKU/箱唛库存 | `dropship/service/DropshipOrderService.java`、`dropship/entity/`、`dropship/mapper/` |
| 数据库 | `src/main/resources/db/lastleg.sql` | | 数据库 | `src/main/resources/db/lastleg.sql` |
| 测试 | `src/test/java/com/baiyue/lastleg/` | | 测试 | `src/test/java/com/baiyue/lastleg/` |
## 事件建单规则 ## 事件建单规则
- 只接受冻结契约支持的事件类型和版本;校验事件 ID、租户、区域、Payload 哈希及必要字段。 - 只接受冻结契约支持的事件类型和版本;校验事件 ID、租户、区域、Payload 哈希及必要字段。
- 同一事件/loading plan 生成一个共享尾程单号;`payload.orders` 每项按 `sourceInboundId` 独立落一条 `ll_order`,并保存对应明细和初始状态历史。 - 同一实际集装箱 `containerNo` 生成一个共享业务尾程单号;`payload.orders` 每项按 `sourceInboundId` 独立落一条雪花 ID `ll_order`,并保存对应明细和初始状态历史。整柜操作不使用 `cabinetNo`。
- `ONE_CLICK_DROPSHIP` 每个来源入仓单必须携带一个或多个不可变来源托盘快照;每托按 SKU 携带箱数、数量和箱唛。校验订单汇总一致、托盘号/SKU/箱唛唯一后,按托建立档案及 SKU 库存,并建立箱唛和流水。仓库实物托盘由 Warehouse API 分配,不得覆盖来源托盘快照。
- 其他订单类型只建立入仓单维度的尾程订单与冻结明细,来源 `palletCount` 强制为 0;后续 PDA 托盘属于海外仓作业资源。
- 整个事件消费在一个事务内完成;任意订单失败时不得留下部分订单。 - 整个事件消费在一个事务内完成;任意订单失败时不得留下部分订单。
- 相同事件重复投递返回既有结果,不重新生成单号或插入明细。来源入仓单唯一约束是最终防线。 - 相同事件重复投递返回既有结果,不重新生成单号或插入明细。来源入仓单唯一约束是最终防线。
- 内部接口先执行常量时间令牌校验;缺失或错误令牌不得进入消费事务。 - 内部接口先执行常量时间令牌校验;缺失或错误令牌不得进入消费事务。
@@ -44,4 +48,3 @@
- 事件契约变化同时检查 `common-event-contract` 版本、Event Bridge 消费方和旧 Payload 哈希兼容。 - 事件契约变化同时检查 `common-event-contract` 版本、Event Bridge 消费方和旧 Payload 哈希兼容。
- 定向验证:`mvn -pl lastleg -am test`。 - 定向验证:`mvn -pl lastleg -am test`。
- 事件链路变更至少覆盖重复事件、相同来源入仓单、事务回滚、并发单号、内部令牌和租户越权测试。 - 事件链路变更至少覆盖重复事件、相同来源入仓单、事务回滚、并发单号、内部令牌和租户越权测试。
@@ -51,27 +51,27 @@ public class LastlegOrderController {
@Operation(summary = "Page tail-leg order rows") @Operation(summary = "Page tail-leg order rows")
public ApiResponse<PageResult<LastlegOrderSummary>> list( public ApiResponse<PageResult<LastlegOrderSummary>> list(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid LastlegOrderRequests.Query query) { @Valid LastlegOrderRequests.Query query) {
return ApiResponse.success(orderService.page(scope(user, tenantId), query)); return ApiResponse.success(orderService.page(scope(user, customerId), query));
} }
@GetMapping("/api/platform/v1/last-leg/orders/{id}") @GetMapping("/api/platform/v1/last-leg/orders/{id}")
@Operation(summary = "Get one inbound-code-level tail-leg order") @Operation(summary = "Get one inbound-code-level tail-leg order")
public ApiResponse<LastlegOrderDetailResponse> detail( public ApiResponse<LastlegOrderDetailResponse> detail(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@PathVariable long id) { @PathVariable long id) {
return ApiResponse.success(orderService.detail(scope(user, tenantId), id)); return ApiResponse.success(orderService.detail(scope(user, customerId), id));
} }
@GetMapping(value = "/api/platform/v1/last-leg/orders/export", produces = "text/csv") @GetMapping(value = "/api/platform/v1/last-leg/orders/export", produces = "text/csv")
@Operation(summary = "Export filtered tail-leg orders as UTF-8 CSV") @Operation(summary = "Export filtered tail-leg orders as UTF-8 CSV")
public ResponseEntity<byte[]> export( public ResponseEntity<byte[]> export(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid LastlegOrderRequests.Query query) { @Valid LastlegOrderRequests.Query query) {
byte[] body = orderService.exportCsv(scope(user, tenantId), query); byte[] body = orderService.exportCsv(scope(user, customerId), query);
return ResponseEntity.ok() return ResponseEntity.ok()
.contentType(new MediaType("text", "csv", StandardCharsets.UTF_8)) .contentType(new MediaType("text", "csv", StandardCharsets.UTF_8))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=lastleg-orders.csv") .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=lastleg-orders.csv")
@@ -82,31 +82,31 @@ public class LastlegOrderController {
@Operation(summary = "Transfer one order to a destination warehouse") @Operation(summary = "Transfer one order to a destination warehouse")
public ApiResponse<Integer> transferOne( public ApiResponse<Integer> transferOne(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@PathVariable long id, @PathVariable long id,
@Valid @RequestBody LastlegOrderRequests.SingleTransferDestination request) { @Valid @RequestBody LastlegOrderRequests.SingleTransferDestination request) {
return ApiResponse.success(orderService.transferDestination(scope(user, tenantId), request.withOrderId(id))); return ApiResponse.success(orderService.transferDestination(scope(user, customerId), request.withOrderId(id)));
} }
@PutMapping("/api/platform/v1/last-leg/orders/destination") @PutMapping("/api/platform/v1/last-leg/orders/destination")
@Operation(summary = "Transfer multiple orders to a destination warehouse") @Operation(summary = "Transfer multiple orders to a destination warehouse")
public ApiResponse<Integer> transferBatch( public ApiResponse<Integer> transferBatch(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid @RequestBody LastlegOrderRequests.TransferDestination request) { @Valid @RequestBody LastlegOrderRequests.TransferDestination request) {
return ApiResponse.success(orderService.transferDestination(scope(user, tenantId), request)); return ApiResponse.success(orderService.transferDestination(scope(user, customerId), request));
} }
private String scope(AuthenticatedUser user, String requestedTenant) { private String scope(AuthenticatedUser user, String requestedCustomer) {
if (user == null || user.tenantId() == null) { if (user == null || user.tenantId() == null) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
} }
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) { if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
if (requestedTenant == null || requestedTenant.isBlank() || "0".equals(requestedTenant)) { if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Platform queries must specify a customer tenant"); "Platform queries must specify a customer");
} }
return requestedTenant; return requestedCustomer;
} }
return String.valueOf(user.tenantId()); return String.valueOf(user.tenantId());
} }
@@ -25,46 +25,46 @@ public class DeliveryTransferController {
@Operation(summary = "Create one pending outbound order and one pending delivery order") @Operation(summary = "Create one pending outbound order and one pending delivery order")
public ApiResponse<DeliveryResponses.TransferResult> transfer( public ApiResponse<DeliveryResponses.TransferResult> transfer(
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String dispatchNo, @AuthenticationPrincipal AuthenticatedUser user, @PathVariable String dispatchNo,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid @RequestBody DeliveryRequests.Transfer request) { @Valid @RequestBody DeliveryRequests.Transfer request) {
return ApiResponse.success(service.transfer(scope(user, tenantId), dispatchNo, operator(user), request)); return ApiResponse.success(service.transfer(scope(user, customerId), dispatchNo, operator(user), request));
} }
@GetMapping("/outbound-orders") @GetMapping("/outbound-orders")
public ApiResponse<PageResult<DeliveryResponses.OutboundRow>> outboundPage( public ApiResponse<PageResult<DeliveryResponses.OutboundRow>> outboundPage(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String tenantId, @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
@Valid DeliveryRequests.Query query) { @Valid DeliveryRequests.Query query) {
return ApiResponse.success(service.outboundPage(scope(user, tenantId), query)); return ApiResponse.success(service.outboundPage(scope(user, customerId), query));
} }
@GetMapping("/outbound-orders/{outboundNo}") @GetMapping("/outbound-orders/{outboundNo}")
public ApiResponse<DeliveryResponses.OutboundDetail> outboundDetail( public ApiResponse<DeliveryResponses.OutboundDetail> outboundDetail(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String tenantId, @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
@PathVariable String outboundNo, @Valid DeliveryRequests.PalletQuery query) { @PathVariable String outboundNo, @Valid DeliveryRequests.PalletQuery query) {
return ApiResponse.success(service.outboundDetail(scope(user, tenantId), outboundNo, query)); return ApiResponse.success(service.outboundDetail(scope(user, customerId), outboundNo, query));
} }
@GetMapping("/delivery-orders") @GetMapping("/delivery-orders")
public ApiResponse<PageResult<DeliveryResponses.DeliveryRow>> deliveryPage( public ApiResponse<PageResult<DeliveryResponses.DeliveryRow>> deliveryPage(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String tenantId, @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
@Valid DeliveryRequests.Query query) { @Valid DeliveryRequests.Query query) {
return ApiResponse.success(service.deliveryPage(scope(user, tenantId), query)); return ApiResponse.success(service.deliveryPage(scope(user, customerId), query));
} }
@GetMapping("/delivery-orders/{deliveryNo}") @GetMapping("/delivery-orders/{deliveryNo}")
public ApiResponse<DeliveryResponses.OutboundDetail> deliveryDetail( public ApiResponse<DeliveryResponses.OutboundDetail> deliveryDetail(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String tenantId, @AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
@PathVariable String deliveryNo, @Valid DeliveryRequests.PalletQuery query) { @PathVariable String deliveryNo, @Valid DeliveryRequests.PalletQuery query) {
return ApiResponse.success(service.deliveryDetail(scope(user, tenantId), deliveryNo, query)); return ApiResponse.success(service.deliveryDetail(scope(user, customerId), deliveryNo, query));
} }
private String scope(AuthenticatedUser user, String requestedTenant) { private String scope(AuthenticatedUser user, String requestedCustomer) {
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) { if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
if (requestedTenant == null || requestedTenant.isBlank() || "0".equals(requestedTenant)) { if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target tenant"); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target tenant");
} }
return requestedTenant.trim(); return requestedCustomer.trim();
} }
return String.valueOf(user.tenantId()); return String.valueOf(user.tenantId());
} }
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
public class DeliveryOrder { public class DeliveryOrder {
@TableId(type = IdType.ASSIGN_ID) @TableId(type = IdType.ASSIGN_ID)
private Long id; private Long id;
private String tenantId; private String customerId;
private String deliveryNo; private String deliveryNo;
private Long outboundOrderId; private Long outboundOrderId;
private String outboundNo; private String outboundNo;
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
public class OutboundOrder { public class OutboundOrder {
@TableId(type = IdType.ASSIGN_ID) @TableId(type = IdType.ASSIGN_ID)
private Long id; private Long id;
private String tenantId; private String customerId;
private String outboundNo; private String outboundNo;
private Long dispatchOrderId; private Long dispatchOrderId;
private String dispatchNo; private String dispatchNo;
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
public class OutboundPalletItem { public class OutboundPalletItem {
@TableId(type = IdType.ASSIGN_ID) @TableId(type = IdType.ASSIGN_ID)
private Long id; private Long id;
private String tenantId; private String customerId;
private Long outboundOrderId; private Long outboundOrderId;
private Long warehousePalletId; private Long warehousePalletId;
private String palletNo; private String palletNo;
@@ -8,10 +8,10 @@ import org.apache.ibatis.annotations.Select;
@Mapper @Mapper
public interface DeliveryOrderMapper extends BaseMapper<DeliveryOrder> { public interface DeliveryOrderMapper extends BaseMapper<DeliveryOrder> {
@Select("SELECT * FROM ll_delivery_order WHERE tenant_id=#{tenantId} AND delivery_no=#{deliveryNo} LIMIT 1") @Select("SELECT * FROM ll_delivery_order WHERE customer_id=#{customerId} AND delivery_no=#{deliveryNo} LIMIT 1")
DeliveryOrder findByNo(@Param("tenantId") String tenantId, @Param("deliveryNo") String deliveryNo); DeliveryOrder findByNo(@Param("customerId") String customerId, @Param("deliveryNo") String deliveryNo);
@Select("SELECT * FROM ll_delivery_order WHERE tenant_id=#{tenantId} AND idempotency_key=#{key} LIMIT 1") @Select("SELECT * FROM ll_delivery_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
DeliveryOrder findByIdempotency(@Param("tenantId") String tenantId, @Param("key") String key); DeliveryOrder findByIdempotency(@Param("customerId") String customerId, @Param("key") String key);
@Select("SELECT * FROM ll_delivery_order WHERE tenant_id=#{tenantId} AND outbound_order_id=#{outboundOrderId} LIMIT 1 FOR UPDATE") @Select("SELECT * FROM ll_delivery_order WHERE customer_id=#{customerId} AND outbound_order_id=#{outboundOrderId} LIMIT 1 FOR UPDATE")
DeliveryOrder lockByOutbound(@Param("tenantId") String tenantId, @Param("outboundOrderId") Long outboundOrderId); DeliveryOrder lockByOutbound(@Param("customerId") String customerId, @Param("outboundOrderId") Long outboundOrderId);
} }
@@ -8,10 +8,10 @@ import org.apache.ibatis.annotations.Select;
@Mapper @Mapper
public interface OutboundOrderMapper extends BaseMapper<OutboundOrder> { public interface OutboundOrderMapper extends BaseMapper<OutboundOrder> {
@Select("SELECT * FROM ll_outbound_order WHERE tenant_id=#{tenantId} AND outbound_no=#{outboundNo} LIMIT 1") @Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND outbound_no=#{outboundNo} LIMIT 1")
OutboundOrder findByNo(@Param("tenantId") String tenantId, @Param("outboundNo") String outboundNo); OutboundOrder findByNo(@Param("customerId") String customerId, @Param("outboundNo") String outboundNo);
@Select("SELECT * FROM ll_outbound_order WHERE tenant_id=#{tenantId} AND idempotency_key=#{key} LIMIT 1") @Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
OutboundOrder findByIdempotency(@Param("tenantId") String tenantId, @Param("key") String key); OutboundOrder findByIdempotency(@Param("customerId") String customerId, @Param("key") String key);
@Select("SELECT * FROM ll_outbound_order WHERE tenant_id=#{tenantId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1 FOR UPDATE") @Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1 FOR UPDATE")
OutboundOrder lockByDispatch(@Param("tenantId") String tenantId, @Param("dispatchOrderId") Long dispatchOrderId); OutboundOrder lockByDispatch(@Param("customerId") String customerId, @Param("dispatchOrderId") Long dispatchOrderId);
} }
@@ -9,6 +9,6 @@ import java.util.List;
@Mapper @Mapper
public interface OutboundPalletItemMapper extends BaseMapper<OutboundPalletItem> { public interface OutboundPalletItemMapper extends BaseMapper<OutboundPalletItem> {
@Select("SELECT * FROM ll_outbound_pallet_item WHERE tenant_id=#{tenantId} AND outbound_order_id=#{outboundOrderId} ORDER BY id") @Select("SELECT * FROM ll_outbound_pallet_item WHERE customer_id=#{customerId} AND outbound_order_id=#{outboundOrderId} ORDER BY id")
List<OutboundPalletItem> findByOutbound(@Param("tenantId") String tenantId, @Param("outboundOrderId") Long outboundOrderId); List<OutboundPalletItem> findByOutbound(@Param("customerId") String customerId, @Param("outboundOrderId") Long outboundOrderId);
} }
@@ -49,22 +49,22 @@ public class DeliveryTransferService {
@Resource private LastlegOrderStatusHistoryMapper histories; @Resource private LastlegOrderStatusHistoryMapper histories;
@Transactional @Transactional
public DeliveryResponses.TransferResult transfer(String tenantId, String dispatchNo, String operatorId, public DeliveryResponses.TransferResult transfer(String customerId, String dispatchNo, String operatorId,
DeliveryRequests.Transfer request) { DeliveryRequests.Transfer request) {
DispatchOrder found = dispatchOrders.findByDispatchNo(tenantId, dispatchNo); DispatchOrder found = dispatchOrders.findByDispatchNo(customerId, dispatchNo);
if (found == null) throw notFound("Dispatch order not found"); if (found == null) throw notFound("Dispatch order not found");
DispatchOrder dispatch = dispatchOrders.lockById(tenantId, found.getId()); DispatchOrder dispatch = dispatchOrders.lockById(customerId, found.getId());
OutboundOrder replay = outboundOrders.findByIdempotency(tenantId, request.idempotencyKey().trim()); OutboundOrder replay = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
if (replay != null) { if (replay != null) {
if (!dispatch.getId().equals(replay.getDispatchOrderId())) throw conflict("Idempotency key belongs to another dispatch"); if (!dispatch.getId().equals(replay.getDispatchOrderId())) throw conflict("Idempotency key belongs to another dispatch");
return result(replay, requireDelivery(tenantId, replay.getId()), true); return result(replay, requireDelivery(customerId, replay.getId()), true);
} }
OutboundOrder existing = outboundOrders.lockByDispatch(tenantId, dispatch.getId()); OutboundOrder existing = outboundOrders.lockByDispatch(customerId, dispatch.getId());
if (existing != null) return result(existing, requireDelivery(tenantId, existing.getId()), true); if (existing != null) return result(existing, requireDelivery(customerId, existing.getId()), true);
if (Set.of("CANCELLED", "DEPARTED", "PARTIAL_DEPARTED", "COMPLETED").contains(dispatch.getStatus())) { if (Set.of("CANCELLED", "DEPARTED", "PARTIAL_DEPARTED", "COMPLETED").contains(dispatch.getStatus())) {
throw conflict("Dispatch cannot be transferred to delivery in its current status"); throw conflict("Dispatch cannot be transferred to delivery in its current status");
} }
List<DispatchItem> items = dispatchItems.findByDispatchOrder(tenantId, dispatch.getId()); List<DispatchItem> items = dispatchItems.findByDispatchOrder(customerId, dispatch.getId());
if (items.isEmpty()) throw conflict("Dispatch has no pallets"); if (items.isEmpty()) throw conflict("Dispatch has no pallets");
if (items.stream().anyMatch(item -> item.getWarehousePalletId() == null)) { if (items.stream().anyMatch(item -> item.getWarehousePalletId() == null)) {
throw conflict("All dispatch pallets must be received before transfer to delivery"); throw conflict("All dispatch pallets must be received before transfer to delivery");
@@ -73,7 +73,7 @@ public class DeliveryTransferService {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
OutboundOrder outbound = new OutboundOrder(); OutboundOrder outbound = new OutboundOrder();
outbound.setId(IdWorker.getId()); outbound.setTenantId(tenantId); outbound.setId(IdWorker.getId()); outbound.setCustomerId(customerId);
outbound.setOutboundNo("OUT" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + outbound.getId()); outbound.setOutboundNo("OUT" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + outbound.getId());
outbound.setDispatchOrderId(dispatch.getId()); outbound.setDispatchNo(dispatch.getDispatchNo()); outbound.setDispatchOrderId(dispatch.getId()); outbound.setDispatchNo(dispatch.getDispatchNo());
outbound.setOverseasWarehouseId(dispatch.getOverseasWarehouseId()); outbound.setStatus("WAIT_OUTBOUND"); outbound.setOverseasWarehouseId(dispatch.getOverseasWarehouseId()); outbound.setStatus("WAIT_OUTBOUND");
@@ -85,20 +85,20 @@ public class DeliveryTransferService {
} catch (DuplicateKeyException e) { } catch (DuplicateKeyException e) {
// The unique keys are the final concurrent guard. A client retry must observe // The unique keys are the final concurrent guard. A client retry must observe
// the document created by the winning transaction rather than receive a false failure. // the document created by the winning transaction rather than receive a false failure.
OutboundOrder concurrent = outboundOrders.findByIdempotency(tenantId, request.idempotencyKey().trim()); OutboundOrder concurrent = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
if (concurrent != null && dispatch.getId().equals(concurrent.getDispatchOrderId())) { if (concurrent != null && dispatch.getId().equals(concurrent.getDispatchOrderId())) {
return result(concurrent, requireDelivery(tenantId, concurrent.getId()), true); return result(concurrent, requireDelivery(customerId, concurrent.getId()), true);
} }
concurrent = outboundOrders.lockByDispatch(tenantId, dispatch.getId()); concurrent = outboundOrders.lockByDispatch(customerId, dispatch.getId());
if (concurrent != null) { if (concurrent != null) {
return result(concurrent, requireDelivery(tenantId, concurrent.getId()), true); return result(concurrent, requireDelivery(customerId, concurrent.getId()), true);
} }
throw conflict("Dispatch was concurrently transferred to delivery"); throw conflict("Dispatch was concurrently transferred to delivery");
} }
for (DispatchItem source : items) { for (DispatchItem source : items) {
OutboundPalletItem item = new OutboundPalletItem(); OutboundPalletItem item = new OutboundPalletItem();
item.setId(IdWorker.getId()); item.setTenantId(tenantId); item.setOutboundOrderId(outbound.getId()); item.setId(IdWorker.getId()); item.setCustomerId(customerId); item.setOutboundOrderId(outbound.getId());
item.setWarehousePalletId(source.getWarehousePalletId()); item.setPalletNo(source.getPalletNo()); item.setWarehousePalletId(source.getWarehousePalletId()); item.setPalletNo(source.getPalletNo());
item.setDestinationWarehouseId(source.getDestinationWarehouseId()); item.setDestinationWarehouseId(source.getDestinationWarehouseId());
item.setDestinationWarehouseCode(source.getDestinationWarehouseCode()); item.setDestinationWarehouseCode(source.getDestinationWarehouseCode());
@@ -109,7 +109,7 @@ public class DeliveryTransferService {
} }
DeliveryOrder delivery = new DeliveryOrder(); DeliveryOrder delivery = new DeliveryOrder();
delivery.setId(IdWorker.getId()); delivery.setTenantId(tenantId); delivery.setId(IdWorker.getId()); delivery.setCustomerId(customerId);
delivery.setDeliveryNo("DEL" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + delivery.getId()); delivery.setDeliveryNo("DEL" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + delivery.getId());
delivery.setOutboundOrderId(outbound.getId()); delivery.setOutboundNo(outbound.getOutboundNo()); delivery.setOutboundOrderId(outbound.getId()); delivery.setOutboundNo(outbound.getOutboundNo());
delivery.setDispatchOrderId(dispatch.getId()); delivery.setDispatchNo(dispatch.getDispatchNo()); delivery.setDispatchOrderId(dispatch.getId()); delivery.setDispatchNo(dispatch.getDispatchNo());
@@ -117,13 +117,13 @@ public class DeliveryTransferService {
delivery.setPackageCount(outbound.getPackageCount()); delivery.setPalletCount(outbound.getPalletCount()); delivery.setPackageCount(outbound.getPackageCount()); delivery.setPalletCount(outbound.getPalletCount());
delivery.setIdempotencyKey(request.idempotencyKey().trim()); delivery.setCreatedBy(operatorId); delivery.setIdempotencyKey(request.idempotencyKey().trim()); delivery.setCreatedBy(operatorId);
delivery.setCreatedAt(now); delivery.setUpdatedAt(now); deliveryOrders.insert(delivery); delivery.setCreatedAt(now); delivery.setUpdatedAt(now); deliveryOrders.insert(delivery);
bindTailOrders(tenantId, items, outbound.getOutboundNo(), delivery.getDeliveryNo(), request.remark(), now); bindTailOrders(customerId, items, outbound.getOutboundNo(), delivery.getDeliveryNo(), request.remark(), now);
return result(outbound, delivery, false); return result(outbound, delivery, false);
} }
public PageResult<DeliveryResponses.OutboundRow> outboundPage(String tenantId, DeliveryRequests.Query query) { public PageResult<DeliveryResponses.OutboundRow> outboundPage(String customerId, DeliveryRequests.Query query) {
Page<OutboundOrder> page = outboundOrders.selectPage(new Page<>(query.page(), query.size()), Page<OutboundOrder> page = outboundOrders.selectPage(new Page<>(query.page(), query.size()),
new LambdaQueryWrapper<OutboundOrder>().eq(OutboundOrder::getTenantId, tenantId) new LambdaQueryWrapper<OutboundOrder>().eq(OutboundOrder::getCustomerId, customerId)
.like(StringUtils.hasText(query.documentNo()), OutboundOrder::getOutboundNo, query.documentNo()) .like(StringUtils.hasText(query.documentNo()), OutboundOrder::getOutboundNo, query.documentNo())
.eq(StringUtils.hasText(query.warehouseId()), OutboundOrder::getOverseasWarehouseId, query.warehouseId()) .eq(StringUtils.hasText(query.warehouseId()), OutboundOrder::getOverseasWarehouseId, query.warehouseId())
.eq(StringUtils.hasText(query.status()), OutboundOrder::getStatus, query.status()) .eq(StringUtils.hasText(query.status()), OutboundOrder::getStatus, query.status())
@@ -131,13 +131,13 @@ public class DeliveryTransferService {
return page(page, page.getRecords().stream().map(this::outboundRow).toList()); return page(page, page.getRecords().stream().map(this::outboundRow).toList());
} }
public DeliveryResponses.OutboundDetail outboundDetail(String tenantId, String outboundNo, public DeliveryResponses.OutboundDetail outboundDetail(String customerId, String outboundNo,
DeliveryRequests.PalletQuery query) { DeliveryRequests.PalletQuery query) {
OutboundOrder outbound = outboundOrders.findByNo(tenantId, outboundNo); OutboundOrder outbound = outboundOrders.findByNo(customerId, outboundNo);
if (outbound == null) throw notFound("Outbound order not found"); if (outbound == null) throw notFound("Outbound order not found");
DeliveryOrder delivery = requireDelivery(tenantId, outbound.getId()); DeliveryOrder delivery = requireDelivery(customerId, outbound.getId());
Page<OutboundPalletItem> page = outboundPallets.selectPage(new Page<>(query.page(), query.size()), Page<OutboundPalletItem> page = outboundPallets.selectPage(new Page<>(query.page(), query.size()),
new LambdaQueryWrapper<OutboundPalletItem>().eq(OutboundPalletItem::getTenantId, tenantId) new LambdaQueryWrapper<OutboundPalletItem>().eq(OutboundPalletItem::getCustomerId, customerId)
.eq(OutboundPalletItem::getOutboundOrderId, outbound.getId()) .eq(OutboundPalletItem::getOutboundOrderId, outbound.getId())
.eq(StringUtils.hasText(query.destinationWarehouseCode()), .eq(StringUtils.hasText(query.destinationWarehouseCode()),
OutboundPalletItem::getDestinationWarehouseCode, query.destinationWarehouseCode()) OutboundPalletItem::getDestinationWarehouseCode, query.destinationWarehouseCode())
@@ -146,9 +146,9 @@ public class DeliveryTransferService {
page(page, page.getRecords().stream().map(this::palletRow).toList())); page(page, page.getRecords().stream().map(this::palletRow).toList()));
} }
public PageResult<DeliveryResponses.DeliveryRow> deliveryPage(String tenantId, DeliveryRequests.Query query) { public PageResult<DeliveryResponses.DeliveryRow> deliveryPage(String customerId, DeliveryRequests.Query query) {
Page<DeliveryOrder> page = deliveryOrders.selectPage(new Page<>(query.page(), query.size()), Page<DeliveryOrder> page = deliveryOrders.selectPage(new Page<>(query.page(), query.size()),
new LambdaQueryWrapper<DeliveryOrder>().eq(DeliveryOrder::getTenantId, tenantId) new LambdaQueryWrapper<DeliveryOrder>().eq(DeliveryOrder::getCustomerId, customerId)
.like(StringUtils.hasText(query.documentNo()), DeliveryOrder::getDeliveryNo, query.documentNo()) .like(StringUtils.hasText(query.documentNo()), DeliveryOrder::getDeliveryNo, query.documentNo())
.eq(StringUtils.hasText(query.warehouseId()), DeliveryOrder::getOverseasWarehouseId, query.warehouseId()) .eq(StringUtils.hasText(query.warehouseId()), DeliveryOrder::getOverseasWarehouseId, query.warehouseId())
.eq(StringUtils.hasText(query.status()), DeliveryOrder::getStatus, query.status()) .eq(StringUtils.hasText(query.status()), DeliveryOrder::getStatus, query.status())
@@ -156,16 +156,16 @@ public class DeliveryTransferService {
return page(page, page.getRecords().stream().map(this::deliveryRow).toList()); return page(page, page.getRecords().stream().map(this::deliveryRow).toList());
} }
public DeliveryResponses.OutboundDetail deliveryDetail(String tenantId, String deliveryNo, public DeliveryResponses.OutboundDetail deliveryDetail(String customerId, String deliveryNo,
DeliveryRequests.PalletQuery query) { DeliveryRequests.PalletQuery query) {
DeliveryOrder delivery = deliveryOrders.findByNo(tenantId, deliveryNo); DeliveryOrder delivery = deliveryOrders.findByNo(customerId, deliveryNo);
if (delivery == null) throw notFound("Delivery order not found"); if (delivery == null) throw notFound("Delivery order not found");
OutboundOrder outbound = outboundOrders.selectById(delivery.getOutboundOrderId()); OutboundOrder outbound = outboundOrders.selectById(delivery.getOutboundOrderId());
if (outbound == null || !tenantId.equals(outbound.getTenantId())) throw notFound("Outbound order not found"); if (outbound == null || !customerId.equals(outbound.getCustomerId())) throw notFound("Outbound order not found");
return outboundDetail(tenantId, outbound.getOutboundNo(), query); return outboundDetail(customerId, outbound.getOutboundNo(), query);
} }
private void bindTailOrders(String tenantId, List<DispatchItem> items, String outboundNo, String deliveryNo, private void bindTailOrders(String customerId, List<DispatchItem> items, String outboundNo, String deliveryNo,
String remark, LocalDateTime now) { String remark, LocalDateTime now) {
Set<Long> orderIds = new LinkedHashSet<>(); Set<Long> orderIds = new LinkedHashSet<>();
items.stream().map(DispatchItem::getOrderId).filter(Objects::nonNull).forEach(orderIds::add); items.stream().map(DispatchItem::getOrderId).filter(Objects::nonNull).forEach(orderIds::add);
@@ -182,7 +182,7 @@ public class DeliveryTransferService {
} }
if (orderIds.isEmpty()) return; if (orderIds.isEmpty()) return;
for (LastlegOrder order : tailOrders.selectList(new LambdaQueryWrapper<LastlegOrder>() for (LastlegOrder order : tailOrders.selectList(new LambdaQueryWrapper<LastlegOrder>()
.eq(LastlegOrder::getTenantId, tenantId).in(LastlegOrder::getId, orderIds))) { .eq(LastlegOrder::getCustomerId, customerId).in(LastlegOrder::getId, orderIds))) {
String from = order.getCurrentStatus(); order.setOutboundOrderNo(outboundNo); String from = order.getCurrentStatus(); order.setOutboundOrderNo(outboundNo);
order.setDeliveryOrderNo(deliveryNo); order.setCurrentStatus("WAIT_OUTBOUND"); order.setUpdatedAt(now); order.setDeliveryOrderNo(deliveryNo); order.setCurrentStatus("WAIT_OUTBOUND"); order.setUpdatedAt(now);
tailOrders.updateById(order); tailOrders.updateById(order);
@@ -194,8 +194,8 @@ public class DeliveryTransferService {
} }
} }
private DeliveryOrder requireDelivery(String tenantId, Long outboundId) { private DeliveryOrder requireDelivery(String customerId, Long outboundId) {
DeliveryOrder value = deliveryOrders.lockByOutbound(tenantId, outboundId); DeliveryOrder value = deliveryOrders.lockByOutbound(customerId, outboundId);
if (value == null) throw new IllegalStateException("Delivery document is missing for outbound order"); if (value == null) throw new IllegalStateException("Delivery document is missing for outbound order");
return value; return value;
} }
@@ -40,20 +40,20 @@ public class DispatchController {
@Operation(summary = "List unlocked planned or received pallets for intelligent pre-dispatch") @Operation(summary = "List unlocked planned or received pallets for intelligent pre-dispatch")
public ApiResponse<List<DispatchPalletCandidate>> candidates( public ApiResponse<List<DispatchPalletCandidate>> candidates(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@RequestParam(required = false) String orderNo, @RequestParam(required = false) String orderNo,
@RequestParam(required = false) String fbaWarehouseCode, @RequestParam(required = false) String fbaWarehouseCode,
@RequestParam(required = false) String sourceStage) { @RequestParam(required = false) String sourceStage) {
return ApiResponse.success(service.candidates(scope(user, tenantId), orderNo, fbaWarehouseCode, sourceStage)); return ApiResponse.success(service.candidates(scope(user, customerId), orderNo, fbaWarehouseCode, sourceStage));
} }
@PostMapping("/pre-arrange") @PostMapping("/pre-arrange")
@Operation(summary = "Create a pending-departure dispatch and lock all selected pallets") @Operation(summary = "Create a pending-departure dispatch and lock all selected pallets")
public ApiResponse<DispatchResponse> preArrange( public ApiResponse<DispatchResponse> preArrange(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid @RequestBody DispatchRequests.PreArrange request) { @Valid @RequestBody DispatchRequests.PreArrange request) {
String scopedTenant = scope(user, tenantId); String scopedTenant = scope(user, customerId);
String operatorId = user.userId() == null ? user.username() : String.valueOf(user.userId()); String operatorId = user.userId() == null ? user.username() : String.valueOf(user.userId());
return ApiResponse.success(service.preArrange(scopedTenant, operatorId, request)); return ApiResponse.success(service.preArrange(scopedTenant, operatorId, request));
} }
@@ -62,37 +62,37 @@ public class DispatchController {
@Operation(summary = "Page dispatch orders") @Operation(summary = "Page dispatch orders")
public ApiResponse<PageResult<DispatchResponse>> page( public ApiResponse<PageResult<DispatchResponse>> page(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid DispatchRequests.Query query) { @Valid DispatchRequests.Query query) {
return ApiResponse.success(service.page(scope(user, tenantId), query)); return ApiResponse.success(service.page(scope(user, customerId), query));
} }
@GetMapping("/{dispatchNo}") @GetMapping("/{dispatchNo}")
@Operation(summary = "Get dispatch details and all pallet destinations") @Operation(summary = "Get dispatch details and all pallet destinations")
public ApiResponse<DispatchResponse> detail( public ApiResponse<DispatchResponse> detail(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@PathVariable String dispatchNo) { @PathVariable String dispatchNo) {
return ApiResponse.success(service.detail(scope(user, tenantId), dispatchNo)); return ApiResponse.success(service.detail(scope(user, customerId), dispatchNo));
} }
@PutMapping("/{dispatchNo}") @PutMapping("/{dispatchNo}")
@Operation(summary = "Edit vehicle, driver, appointment, route and departure data") @Operation(summary = "Edit vehicle, driver, appointment, route and departure data")
public ApiResponse<DispatchResponse> update( public ApiResponse<DispatchResponse> update(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@PathVariable String dispatchNo, @PathVariable String dispatchNo,
@Valid @RequestBody DispatchRequests.Update request) { @Valid @RequestBody DispatchRequests.Update request) {
return ApiResponse.success(service.update(scope(user, tenantId), dispatchNo, request)); return ApiResponse.success(service.update(scope(user, customerId), dispatchNo, request));
} }
@DeleteMapping("/{dispatchNo}") @DeleteMapping("/{dispatchNo}")
@Operation(summary = "Cancel a pending dispatch and release its pallet locks") @Operation(summary = "Cancel a pending dispatch and release its pallet locks")
public ApiResponse<Void> cancel( public ApiResponse<Void> cancel(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@PathVariable String dispatchNo) { @PathVariable String dispatchNo) {
service.cancel(scope(user, tenantId), dispatchNo); service.cancel(scope(user, customerId), dispatchNo);
return ApiResponse.success(null); return ApiResponse.success(null);
} }
@@ -100,24 +100,24 @@ public class DispatchController {
@Operation(summary = "Ship all PDA-picked pallets and mark the dispatch departed") @Operation(summary = "Ship all PDA-picked pallets and mark the dispatch departed")
public ApiResponse<DispatchPdaResponses.Departed> depart( public ApiResponse<DispatchPdaResponses.Departed> depart(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@PathVariable String dispatchNo, @PathVariable String dispatchNo,
@Valid @RequestBody DispatchPdaRequests.Depart request) { @Valid @RequestBody DispatchPdaRequests.Depart request) {
String scopedTenant = scope(user, tenantId); String scopedTenant = scope(user, customerId);
String operatorId = user.userId() == null ? user.username() : String.valueOf(user.userId()); String operatorId = user.userId() == null ? user.username() : String.valueOf(user.userId());
return ApiResponse.success(pdaWorkflow.depart(scopedTenant, dispatchNo, operatorId, request)); return ApiResponse.success(pdaWorkflow.depart(scopedTenant, dispatchNo, operatorId, request));
} }
private String scope(AuthenticatedUser user, String requestedTenant) { private String scope(AuthenticatedUser user, String requestedCustomer) {
if (user == null || user.tenantId() == null) { if (user == null || user.tenantId() == null) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
} }
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) { if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
if (requestedTenant == null || requestedTenant.isBlank() || "0".equals(requestedTenant)) { if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Platform request must specify target tenant"); "Platform request must specify target tenant");
} }
return requestedTenant; return requestedCustomer;
} }
return String.valueOf(user.tenantId()); return String.valueOf(user.tenantId());
} }
@@ -15,7 +15,7 @@ public class DispatchItem {
@TableId(type = IdType.INPUT) @TableId(type = IdType.INPUT)
private Long id; private Long id;
private Long dispatchOrderId; private Long dispatchOrderId;
private String tenantId; private String customerId;
private Long planId; private Long planId;
private Long planPalletId; private Long planPalletId;
private Long warehousePalletId; private Long warehousePalletId;
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
public class DispatchOrder { public class DispatchOrder {
@TableId(type = IdType.INPUT) @TableId(type = IdType.INPUT)
private Long id; private Long id;
private String tenantId; private String customerId;
private String dispatchNo; private String dispatchNo;
private String overseasWarehouseId; private String overseasWarehouseId;
private String overseasWarehouseName; private String overseasWarehouseName;
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
public class DispatchPalletLock { public class DispatchPalletLock {
@TableId(type = IdType.INPUT) @TableId(type = IdType.INPUT)
private Long id; private Long id;
private String tenantId; private String customerId;
private String sourcePalletKey; private String sourcePalletKey;
/** Equals sourcePalletKey while ACTIVE and is null after release; unique index enforces exclusivity. */ /** Equals sourcePalletKey while ACTIVE and is null after release; unique index enforces exclusivity. */
private String activeKey; private String activeKey;
@@ -17,7 +17,7 @@ public interface DispatchCandidateMapper {
"p.destination_warehouse_code AS destinationWarehouseCode,p.destination_warehouse_name AS destinationWarehouseName," + "p.destination_warehouse_code AS destinationWarehouseCode,p.destination_warehouse_name AS destinationWarehouseName," +
"MIN(l.fba_warehouse_code) AS fbaWarehouseCode," + "MIN(l.fba_warehouse_code) AS fbaWarehouseCode," +
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN COALESCE((SELECT SUM(pos.available_packages) " + "CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN COALESCE((SELECT SUM(pos.available_packages) " +
"FROM ll_inventory_position pos WHERE pos.tenant_id=#{tenantId} AND pos.pallet_id=p.warehouse_pallet_id),0) " + "FROM ll_inventory_position pos WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id),0) " +
"ELSE COALESCE(SUM(CASE WHEN a.picked_package_count &gt; 0 THEN a.picked_package_count " + "ELSE COALESCE(SUM(CASE WHEN a.picked_package_count &gt; 0 THEN a.picked_package_count " +
"ELSE a.planned_package_count END),0) END AS packageCount,p.allocated_volume_cbm AS volumeCbm," + "ELSE a.planned_package_count END),0) END AS packageCount,p.allocated_volume_cbm AS volumeCbm," +
"CASE WHEN p.warehouse_pallet_id IS NULL THEN 'PLAN' ELSE 'RECEIVED' END AS sourceStage " + "CASE WHEN p.warehouse_pallet_id IS NULL THEN 'PLAN' ELSE 'RECEIVED' END AS sourceStage " +
@@ -25,22 +25,22 @@ public interface DispatchCandidateMapper {
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " + "LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " +
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " + "LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " +
"LEFT JOIN ll_order o ON o.id=l.order_id " + "LEFT JOIN ll_order o ON o.id=l.order_id " +
"WHERE ip.tenant_id=#{tenantId} " + "WHERE ip.customer_id=#{customerId} " +
"<if test='orderNo != null and orderNo != \"\"'> AND o.order_no LIKE CONCAT('%',#{orderNo},'%')</if> " + "<if test='orderNo != null and orderNo != \"\"'> AND o.order_no LIKE CONCAT('%',#{orderNo},'%')</if> " +
"<if test='fbaWarehouseCode != null and fbaWarehouseCode != \"\"'> AND l.fba_warehouse_code=#{fbaWarehouseCode}</if> " + "<if test='fbaWarehouseCode != null and fbaWarehouseCode != \"\"'> AND l.fba_warehouse_code=#{fbaWarehouseCode}</if> " +
"<if test='sourceStage != null and sourceStage != \"\"'> AND " + "<if test='sourceStage != null and sourceStage != \"\"'> AND " +
"CASE WHEN p.warehouse_pallet_id IS NULL THEN 'PLAN' ELSE 'RECEIVED' END=#{sourceStage}</if> " + "CASE WHEN p.warehouse_pallet_id IS NULL THEN 'PLAN' ELSE 'RECEIVED' END=#{sourceStage}</if> " +
"AND (p.warehouse_pallet_id IS NULL OR EXISTS (SELECT 1 FROM ll_inventory_position pos " + "AND (p.warehouse_pallet_id IS NULL OR EXISTS (SELECT 1 FROM ll_inventory_position pos " +
"WHERE pos.tenant_id=#{tenantId} AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages &gt; 0)) " + "WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages &gt; 0)) " +
"AND NOT EXISTS (SELECT 1 FROM ll_dispatch_pallet_lock d " + "AND NOT EXISTS (SELECT 1 FROM ll_dispatch_pallet_lock d " +
"WHERE d.tenant_id=ip.tenant_id AND d.status='ACTIVE' AND (" + "WHERE d.customer_id=ip.customer_id AND d.status='ACTIVE' AND (" +
"d.source_pallet_key=CONCAT('PLAN:',p.id) OR (p.warehouse_pallet_id IS NOT NULL AND " + "d.source_pallet_key=CONCAT('PLAN:',p.id) OR (p.warehouse_pallet_id IS NOT NULL AND " +
"d.source_pallet_key=CONCAT('RECEIVED:',p.warehouse_pallet_id)))) " + "d.source_pallet_key=CONCAT('RECEIVED:',p.warehouse_pallet_id)))) " +
"GROUP BY p.plan_id,p.id,p.warehouse_pallet_id,p.pallet_no,p.destination_warehouse_id," + "GROUP BY p.plan_id,p.id,p.warehouse_pallet_id,p.pallet_no,p.destination_warehouse_id," +
"p.destination_warehouse_code,p.destination_warehouse_name,p.allocated_volume_cbm " + "p.destination_warehouse_code,p.destination_warehouse_name,p.allocated_volume_cbm " +
"ORDER BY p.plan_id,p.sequence_no" + "ORDER BY p.plan_id,p.sequence_no" +
"</script>") "</script>")
List<DispatchPalletCandidate> available(@Param("tenantId") String tenantId, List<DispatchPalletCandidate> available(@Param("customerId") String customerId,
@Param("orderNo") String orderNo, @Param("orderNo") String orderNo,
@Param("fbaWarehouseCode") String fbaWarehouseCode, @Param("fbaWarehouseCode") String fbaWarehouseCode,
@Param("sourceStage") String sourceStage); @Param("sourceStage") String sourceStage);
@@ -53,7 +53,7 @@ public interface DispatchCandidateMapper {
"p.destination_warehouse_code AS destinationWarehouseCode,p.destination_warehouse_name AS destinationWarehouseName," + "p.destination_warehouse_code AS destinationWarehouseCode,p.destination_warehouse_name AS destinationWarehouseName," +
"MIN(l.fba_warehouse_code) AS fbaWarehouseCode," + "MIN(l.fba_warehouse_code) AS fbaWarehouseCode," +
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN COALESCE((SELECT SUM(pos.available_packages) " + "CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN COALESCE((SELECT SUM(pos.available_packages) " +
"FROM ll_inventory_position pos WHERE pos.tenant_id=#{tenantId} AND pos.pallet_id=p.warehouse_pallet_id),0) " + "FROM ll_inventory_position pos WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id),0) " +
"ELSE COALESCE(SUM(CASE WHEN a.picked_package_count &gt; 0 THEN a.picked_package_count " + "ELSE COALESCE(SUM(CASE WHEN a.picked_package_count &gt; 0 THEN a.picked_package_count " +
"ELSE a.planned_package_count END),0) END AS packageCount,p.allocated_volume_cbm AS volumeCbm," + "ELSE a.planned_package_count END),0) END AS packageCount,p.allocated_volume_cbm AS volumeCbm," +
"CASE WHEN p.warehouse_pallet_id IS NULL THEN 'PLAN' ELSE 'RECEIVED' END AS sourceStage " + "CASE WHEN p.warehouse_pallet_id IS NULL THEN 'PLAN' ELSE 'RECEIVED' END AS sourceStage " +
@@ -61,12 +61,12 @@ public interface DispatchCandidateMapper {
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " + "LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " +
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " + "LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " +
"LEFT JOIN ll_order o ON o.id=l.order_id " + "LEFT JOIN ll_order o ON o.id=l.order_id " +
"WHERE ip.tenant_id=#{tenantId} AND p.id IN " + "WHERE ip.customer_id=#{customerId} AND p.id IN " +
"<foreach collection='planPalletIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " + "<foreach collection='planPalletIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
"GROUP BY p.plan_id,p.id,p.warehouse_pallet_id,p.pallet_no,p.destination_warehouse_id," + "GROUP BY p.plan_id,p.id,p.warehouse_pallet_id,p.pallet_no,p.destination_warehouse_id," +
"p.destination_warehouse_code,p.destination_warehouse_name,p.allocated_volume_cbm " + "p.destination_warehouse_code,p.destination_warehouse_name,p.allocated_volume_cbm " +
"ORDER BY p.id FOR UPDATE" + "ORDER BY p.id FOR UPDATE" +
"</script>") "</script>")
List<DispatchPalletCandidate> lockSelected(@Param("tenantId") String tenantId, List<DispatchPalletCandidate> lockSelected(@Param("customerId") String customerId,
@Param("planPalletIds") List<Long> planPalletIds); @Param("planPalletIds") List<Long> planPalletIds);
} }
@@ -11,15 +11,15 @@ import java.util.List;
@Mapper @Mapper
public interface DispatchItemMapper extends BaseMapper<DispatchItem> { public interface DispatchItemMapper extends BaseMapper<DispatchItem> {
@Select("SELECT * FROM ll_dispatch_item WHERE tenant_id=#{tenantId} AND dispatch_order_id=#{dispatchOrderId} ORDER BY id") @Select("SELECT * FROM ll_dispatch_item WHERE customer_id=#{customerId} AND dispatch_order_id=#{dispatchOrderId} ORDER BY id")
List<DispatchItem> findByDispatchOrder(@Param("tenantId") String tenantId, List<DispatchItem> findByDispatchOrder(@Param("customerId") String customerId,
@Param("dispatchOrderId") Long dispatchOrderId); @Param("dispatchOrderId") Long dispatchOrderId);
@Update("UPDATE ll_dispatch_item i JOIN ll_inbound_plan_pallet p ON p.id=i.plan_pallet_id " + @Update("UPDATE ll_dispatch_item i JOIN ll_inbound_plan_pallet p ON p.id=i.plan_pallet_id " +
"JOIN ll_inbound_plan ip ON ip.id=p.plan_id AND ip.tenant_id=i.tenant_id " + "JOIN ll_inbound_plan ip ON ip.id=p.plan_id AND ip.customer_id=i.customer_id " +
"SET i.warehouse_pallet_id=p.warehouse_pallet_id,i.source_stage='RECEIVED' " + "SET i.warehouse_pallet_id=p.warehouse_pallet_id,i.source_stage='RECEIVED' " +
"WHERE i.tenant_id=#{tenantId} AND i.dispatch_order_id=#{dispatchOrderId} " + "WHERE i.customer_id=#{customerId} AND i.dispatch_order_id=#{dispatchOrderId} " +
"AND i.warehouse_pallet_id IS NULL AND p.warehouse_pallet_id IS NOT NULL") "AND i.warehouse_pallet_id IS NULL AND p.warehouse_pallet_id IS NOT NULL")
int bindReceivedPallets(@Param("tenantId") String tenantId, int bindReceivedPallets(@Param("customerId") String customerId,
@Param("dispatchOrderId") Long dispatchOrderId); @Param("dispatchOrderId") Long dispatchOrderId);
} }
@@ -8,9 +8,9 @@ import org.apache.ibatis.annotations.Select;
@Mapper @Mapper
public interface DispatchOrderMapper extends BaseMapper<DispatchOrder> { public interface DispatchOrderMapper extends BaseMapper<DispatchOrder> {
@Select("SELECT * FROM ll_dispatch_order WHERE tenant_id=#{tenantId} AND dispatch_no=#{dispatchNo} LIMIT 1") @Select("SELECT * FROM ll_dispatch_order WHERE customer_id=#{customerId} AND dispatch_no=#{dispatchNo} LIMIT 1")
DispatchOrder findByDispatchNo(@Param("tenantId") String tenantId, @Param("dispatchNo") String dispatchNo); DispatchOrder findByDispatchNo(@Param("customerId") String customerId, @Param("dispatchNo") String dispatchNo);
@Select("SELECT * FROM ll_dispatch_order WHERE tenant_id=#{tenantId} AND id=#{id} LIMIT 1 FOR UPDATE") @Select("SELECT * FROM ll_dispatch_order WHERE customer_id=#{customerId} AND id=#{id} LIMIT 1 FOR UPDATE")
DispatchOrder lockById(@Param("tenantId") String tenantId, @Param("id") Long id); DispatchOrder lockById(@Param("customerId") String customerId, @Param("id") Long id);
} }
@@ -8,8 +8,8 @@ import org.apache.ibatis.annotations.Select;
@Mapper @Mapper
public interface DispatchPalletLockMapper extends BaseMapper<DispatchPalletLock> { public interface DispatchPalletLockMapper extends BaseMapper<DispatchPalletLock> {
@Select("SELECT * FROM ll_dispatch_pallet_lock WHERE tenant_id=#{tenantId} AND source_pallet_key=#{sourcePalletKey} " + @Select("SELECT * FROM ll_dispatch_pallet_lock WHERE customer_id=#{customerId} AND source_pallet_key=#{sourcePalletKey} " +
"AND status='ACTIVE' LIMIT 1 FOR UPDATE") "AND status='ACTIVE' LIMIT 1 FOR UPDATE")
DispatchPalletLock lockActive(@Param("tenantId") String tenantId, DispatchPalletLock lockActive(@Param("customerId") String customerId,
@Param("sourcePalletKey") String sourcePalletKey); @Param("sourcePalletKey") String sourcePalletKey);
} }
@@ -20,38 +20,38 @@ public class DispatchPdaController {
@PostMapping("/push") @PostMapping("/push")
public ApiResponse<DispatchPdaResponses.Task> push(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<DispatchPdaResponses.Task> push(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String dispatchNo, @PathVariable String dispatchNo,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid @RequestBody DispatchPdaRequests.Push request) { @Valid @RequestBody DispatchPdaRequests.Push request) {
return ApiResponse.success(service.push(scope(user, tenantId), dispatchNo, request)); return ApiResponse.success(service.push(scope(user, customerId), dispatchNo, request));
} }
@GetMapping @GetMapping
public ApiResponse<DispatchPdaResponses.Task> task(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<DispatchPdaResponses.Task> task(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String dispatchNo, @PathVariable String dispatchNo,
@RequestParam(required = false) String tenantId) { @RequestParam(required = false) String customerId) {
return ApiResponse.success(service.task(scope(user, tenantId), dispatchNo)); return ApiResponse.success(service.task(scope(user, customerId), dispatchNo));
} }
@PostMapping("/items/{itemId}/pick") @PostMapping("/items/{itemId}/pick")
public ApiResponse<DispatchPdaResponses.Picked> pick(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<DispatchPdaResponses.Picked> pick(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String dispatchNo, @PathVariable long itemId, @PathVariable String dispatchNo, @PathVariable long itemId,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid @RequestBody DispatchPdaRequests.Pick request) { @Valid @RequestBody DispatchPdaRequests.Pick request) {
return ApiResponse.success(service.pick(scope(user, tenantId), dispatchNo, itemId, operator(user), request)); return ApiResponse.success(service.pick(scope(user, customerId), dispatchNo, itemId, operator(user), request));
} }
@PostMapping("/complete") @PostMapping("/complete")
public ApiResponse<DispatchPdaResponses.PickingCompleted> completePicking( public ApiResponse<DispatchPdaResponses.PickingCompleted> completePicking(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String dispatchNo, @PathVariable String dispatchNo,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid @RequestBody DispatchPdaRequests.CompletePicking request) { @Valid @RequestBody DispatchPdaRequests.CompletePicking request) {
return ApiResponse.success(service.completePicking(scope(user, tenantId), dispatchNo, request)); return ApiResponse.success(service.completePicking(scope(user, customerId), dispatchNo, request));
} }
private String scope(AuthenticatedUser user, String requestedTenant) { private String scope(AuthenticatedUser user, String requestedCustomer) {
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) { if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
if (requestedTenant == null || requestedTenant.isBlank() || "0".equals(requestedTenant)) { if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target tenant"); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target tenant");
} }
return requestedTenant.trim(); return requestedCustomer.trim();
} }
return String.valueOf(user.tenantId()); return String.valueOf(user.tenantId());
} }
@@ -11,7 +11,7 @@ import java.time.LocalDateTime;
@TableName("ll_dispatch_pda_operation") @TableName("ll_dispatch_pda_operation")
public class DispatchPdaOperation { public class DispatchPdaOperation {
@TableId(type = IdType.INPUT) private Long id; @TableId(type = IdType.INPUT) private Long id;
private String tenantId; private String customerId;
private Long taskId; private Long taskId;
private String operationType; private String operationType;
private String idempotencyKey; private String idempotencyKey;
@@ -11,7 +11,7 @@ import java.time.LocalDateTime;
@TableName("ll_dispatch_pda_pick") @TableName("ll_dispatch_pda_pick")
public class DispatchPdaPick { public class DispatchPdaPick {
@TableId(type = IdType.INPUT) private Long id; @TableId(type = IdType.INPUT) private Long id;
private String tenantId; private String customerId;
private Long taskId; private Long taskId;
private Long dispatchItemId; private Long dispatchItemId;
private Long inventoryPositionId; private Long inventoryPositionId;
@@ -12,7 +12,7 @@ import java.time.LocalDateTime;
@TableName("ll_dispatch_pda_pick_order") @TableName("ll_dispatch_pda_pick_order")
public class DispatchPdaPickOrder { public class DispatchPdaPickOrder {
@TableId(type = IdType.INPUT) private Long id; @TableId(type = IdType.INPUT) private Long id;
private String tenantId; private String customerId;
private Long pickId; private Long pickId;
private Long receiptId; private Long receiptId;
private Long orderId; private Long orderId;
@@ -11,7 +11,7 @@ import java.time.LocalDateTime;
@TableName("ll_dispatch_pda_task") @TableName("ll_dispatch_pda_task")
public class DispatchPdaTask { public class DispatchPdaTask {
@TableId(type = IdType.INPUT) private Long id; @TableId(type = IdType.INPUT) private Long id;
private String tenantId; private String customerId;
private Long dispatchOrderId; private Long dispatchOrderId;
private String taskNo; private String taskNo;
private String status; private String status;
@@ -6,6 +6,6 @@ import org.apache.ibatis.annotations.*;
@Mapper @Mapper
public interface DispatchPdaOperationMapper extends BaseMapper<DispatchPdaOperation> { public interface DispatchPdaOperationMapper extends BaseMapper<DispatchPdaOperation> {
@Select("SELECT * FROM ll_dispatch_pda_operation WHERE tenant_id=#{tenantId} AND idempotency_key=#{key} LIMIT 1") @Select("SELECT * FROM ll_dispatch_pda_operation WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
DispatchPdaOperation findKey(@Param("tenantId") String tenantId, @Param("key") String key); DispatchPdaOperation findKey(@Param("customerId") String customerId, @Param("key") String key);
} }
@@ -7,8 +7,8 @@ import java.util.List;
@Mapper @Mapper
public interface DispatchPdaPickMapper extends BaseMapper<DispatchPdaPick> { public interface DispatchPdaPickMapper extends BaseMapper<DispatchPdaPick> {
@Select("SELECT * FROM ll_dispatch_pda_pick WHERE tenant_id=#{tenantId} AND task_id=#{taskId} ORDER BY id") @Select("SELECT * FROM ll_dispatch_pda_pick WHERE customer_id=#{customerId} AND task_id=#{taskId} ORDER BY id")
List<DispatchPdaPick> listTask(@Param("tenantId") String tenantId, @Param("taskId") Long taskId); List<DispatchPdaPick> listTask(@Param("customerId") String customerId, @Param("taskId") Long taskId);
@Select("SELECT COALESCE(SUM(packages),0) FROM ll_dispatch_pda_pick WHERE tenant_id=#{tenantId} AND task_id=#{taskId} AND dispatch_item_id=#{itemId}") @Select("SELECT COALESCE(SUM(packages),0) FROM ll_dispatch_pda_pick WHERE customer_id=#{customerId} AND task_id=#{taskId} AND dispatch_item_id=#{itemId}")
long totalItem(@Param("tenantId") String tenantId, @Param("taskId") Long taskId, @Param("itemId") Long itemId); long totalItem(@Param("customerId") String customerId, @Param("taskId") Long taskId, @Param("itemId") Long itemId);
} }
@@ -12,20 +12,20 @@ import java.util.List;
@Mapper @Mapper
public interface DispatchPdaPickOrderMapper extends BaseMapper<DispatchPdaPickOrder> { public interface DispatchPdaPickOrderMapper extends BaseMapper<DispatchPdaPickOrder> {
@Select("SELECT r.* FROM ll_inbound_stock_receipt r " + @Select("SELECT r.* FROM ll_inbound_stock_receipt r " +
"WHERE r.tenant_id=#{tenantId} AND r.inventory_position_id=#{positionId} " + "WHERE r.customer_id=#{customerId} AND r.inventory_position_id=#{positionId} " +
"AND r.received_packages > (SELECT COALESCE(SUM(a.packages),0) FROM ll_dispatch_pda_pick_order a " + "AND r.received_packages > (SELECT COALESCE(SUM(a.packages),0) FROM ll_dispatch_pda_pick_order a " +
"WHERE a.tenant_id=r.tenant_id AND a.receipt_id=r.id) ORDER BY r.received_at,r.id") "WHERE a.customer_id=r.customer_id AND a.receipt_id=r.id) ORDER BY r.received_at,r.id")
List<InboundStockReceipt> availableReceipts(@Param("tenantId") String tenantId, List<InboundStockReceipt> availableReceipts(@Param("customerId") String customerId,
@Param("positionId") Long positionId); @Param("positionId") Long positionId);
@Select("SELECT COALESCE(SUM(packages),0) FROM ll_dispatch_pda_pick_order " + @Select("SELECT COALESCE(SUM(packages),0) FROM ll_dispatch_pda_pick_order " +
"WHERE tenant_id=#{tenantId} AND receipt_id=#{receiptId}") "WHERE customer_id=#{customerId} AND receipt_id=#{receiptId}")
long attributedPackages(@Param("tenantId") String tenantId, @Param("receiptId") Long receiptId); long attributedPackages(@Param("customerId") String customerId, @Param("receiptId") Long receiptId);
@Select("SELECT COALESCE(SUM(product_quantity),0) FROM ll_dispatch_pda_pick_order " + @Select("SELECT COALESCE(SUM(product_quantity),0) FROM ll_dispatch_pda_pick_order " +
"WHERE tenant_id=#{tenantId} AND receipt_id=#{receiptId}") "WHERE customer_id=#{customerId} AND receipt_id=#{receiptId}")
long attributedQuantity(@Param("tenantId") String tenantId, @Param("receiptId") Long receiptId); long attributedQuantity(@Param("customerId") String customerId, @Param("receiptId") Long receiptId);
@Select("SELECT * FROM ll_dispatch_pda_pick_order WHERE tenant_id=#{tenantId} AND pick_id=#{pickId} ORDER BY id") @Select("SELECT * FROM ll_dispatch_pda_pick_order WHERE customer_id=#{customerId} AND pick_id=#{pickId} ORDER BY id")
List<DispatchPdaPickOrder> listPick(@Param("tenantId") String tenantId, @Param("pickId") Long pickId); List<DispatchPdaPickOrder> listPick(@Param("customerId") String customerId, @Param("pickId") Long pickId);
} }
@@ -6,9 +6,9 @@ import org.apache.ibatis.annotations.*;
@Mapper @Mapper
public interface DispatchPdaTaskMapper extends BaseMapper<DispatchPdaTask> { public interface DispatchPdaTaskMapper extends BaseMapper<DispatchPdaTask> {
@Select("SELECT * FROM ll_dispatch_pda_task WHERE tenant_id=#{tenantId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1 FOR UPDATE") @Select("SELECT * FROM ll_dispatch_pda_task WHERE customer_id=#{customerId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1 FOR UPDATE")
DispatchPdaTask lockByDispatch(@Param("tenantId") String tenantId, @Param("dispatchOrderId") Long dispatchOrderId); DispatchPdaTask lockByDispatch(@Param("customerId") String customerId, @Param("dispatchOrderId") Long dispatchOrderId);
@Select("SELECT * FROM ll_dispatch_pda_task WHERE tenant_id=#{tenantId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1") @Select("SELECT * FROM ll_dispatch_pda_task WHERE customer_id=#{customerId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1")
DispatchPdaTask findByDispatch(@Param("tenantId") String tenantId, @Param("dispatchOrderId") Long dispatchOrderId); DispatchPdaTask findByDispatch(@Param("customerId") String customerId, @Param("dispatchOrderId") Long dispatchOrderId);
} }
@@ -45,25 +45,25 @@ public class DispatchPdaWorkflowService {
@Resource private ObjectMapper objectMapper; @Resource private ObjectMapper objectMapper;
@Transactional @Transactional
public DispatchPdaResponses.Task push(String tenantId, String dispatchNo, DispatchPdaRequests.Push request) { public DispatchPdaResponses.Task push(String customerId, String dispatchNo, DispatchPdaRequests.Push request) {
DispatchOrder order = requireOrder(tenantId, dispatchNo); DispatchOrder order = requireOrder(customerId, dispatchNo);
requirePdaStatus(order); requirePdaStatus(order);
requireVehicle(order); requireVehicle(order);
String warehouseId = warehouse(request.warehouseId()); String warehouseId = warehouse(request.warehouseId());
requireDispatchWarehouse(order, warehouseId); requireDispatchWarehouse(order, warehouseId);
DispatchPdaOperation replay = replay(tenantId, "PUSH", request.idempotencyKey(), request); DispatchPdaOperation replay = replay(customerId, "PUSH", request.idempotencyKey(), request);
if (replay != null) return read(replay, DispatchPdaResponses.Task.class); if (replay != null) return read(replay, DispatchPdaResponses.Task.class);
items.bindReceivedPallets(tenantId, order.getId()); items.bindReceivedPallets(customerId, order.getId());
List<DispatchItem> dispatchItems = items.findByDispatchOrder(tenantId, order.getId()); List<DispatchItem> dispatchItems = items.findByDispatchOrder(customerId, order.getId());
if (dispatchItems.isEmpty()) throw conflict("Dispatch has no pallets to push"); if (dispatchItems.isEmpty()) throw conflict("Dispatch has no pallets to push");
if (dispatchItems.stream().anyMatch(item -> item.getWarehousePalletId() == null)) { if (dispatchItems.stream().anyMatch(item -> item.getWarehousePalletId() == null)) {
throw conflict("All pre-arranged pallets must be received before PDA push"); throw conflict("All pre-arranged pallets must be received before PDA push");
} }
DispatchPdaTask task = tasks.lockByDispatch(tenantId, order.getId()); DispatchPdaTask task = tasks.lockByDispatch(customerId, order.getId());
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
if (task == null) { if (task == null) {
task = new DispatchPdaTask(); task = new DispatchPdaTask();
task.setId(IdWorker.getId()); task.setTenantId(tenantId); task.setDispatchOrderId(order.getId()); task.setId(IdWorker.getId()); task.setCustomerId(customerId); task.setDispatchOrderId(order.getId());
task.setTaskNo("DPDA" + task.getId()); task.setStatus("PDA_PENDING"); task.setTaskNo("DPDA" + task.getId()); task.setStatus("PDA_PENDING");
task.setWarehouseId(warehouseId); task.setWarehouseId(warehouseId);
task.setOperatorId(trim(request.operatorId())); task.setOperatorName(trim(request.operatorName())); task.setOperatorId(trim(request.operatorId())); task.setOperatorName(trim(request.operatorName()));
@@ -71,91 +71,91 @@ public class DispatchPdaWorkflowService {
} }
if ("PENDING_DEPARTURE".equals(order.getStatus())) { order.setStatus("PICKING"); order.setUpdatedAt(now); orders.updateById(order); } if ("PENDING_DEPARTURE".equals(order.getStatus())) { order.setStatus("PICKING"); order.setUpdatedAt(now); orders.updateById(order); }
if (!warehouseId.equals(task.getWarehouseId())) throw conflict("PDA task warehouse does not match selected warehouse"); if (!warehouseId.equals(task.getWarehouseId())) throw conflict("PDA task warehouse does not match selected warehouse");
DispatchPdaResponses.Task result = task(tenantId, order, task, dispatchItems); DispatchPdaResponses.Task result = task(customerId, order, task, dispatchItems);
save(tenantId, task.getId(), "PUSH", request.idempotencyKey(), request, result); save(customerId, task.getId(), "PUSH", request.idempotencyKey(), request, result);
return result; return result;
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
public DispatchPdaResponses.Task task(String tenantId, String dispatchNo) { public DispatchPdaResponses.Task task(String customerId, String dispatchNo) {
DispatchOrder order = requireOrder(tenantId, dispatchNo); DispatchOrder order = requireOrder(customerId, dispatchNo);
DispatchPdaTask task = tasks.findByDispatch(tenantId, order.getId()); DispatchPdaTask task = tasks.findByDispatch(customerId, order.getId());
if (task == null) throw notFound("Dispatch PDA task not found"); if (task == null) throw notFound("Dispatch PDA task not found");
return task(tenantId, order, task, items.findByDispatchOrder(tenantId, order.getId())); return task(customerId, order, task, items.findByDispatchOrder(customerId, order.getId()));
} }
@Transactional @Transactional
public DispatchPdaResponses.Picked pick(String tenantId, String dispatchNo, long itemId, public DispatchPdaResponses.Picked pick(String customerId, String dispatchNo, long itemId,
String operatorId, DispatchPdaRequests.Pick request) { String operatorId, DispatchPdaRequests.Pick request) {
DispatchOrder order = requireOrder(tenantId, dispatchNo); DispatchOrder order = requireOrder(customerId, dispatchNo);
requirePdaStatus(order); requirePdaStatus(order);
requireVehicle(order); requireVehicle(order);
String warehouseId = warehouse(request.warehouseId()); String warehouseId = warehouse(request.warehouseId());
requireDispatchWarehouse(order, warehouseId); requireDispatchWarehouse(order, warehouseId);
DispatchPdaTask task = tasks.lockByDispatch(tenantId, order.getId()); DispatchPdaTask task = tasks.lockByDispatch(customerId, order.getId());
if (task == null || !("PDA_PENDING".equals(task.getStatus()) || "PICKING".equals(task.getStatus()))) { if (task == null || !("PDA_PENDING".equals(task.getStatus()) || "PICKING".equals(task.getStatus()))) {
throw conflict("Dispatch PDA task is not available for picking"); throw conflict("Dispatch PDA task is not available for picking");
} }
if (!warehouseId.equals(task.getWarehouseId())) throw conflict("PDA pick warehouse does not match task warehouse"); if (!warehouseId.equals(task.getWarehouseId())) throw conflict("PDA pick warehouse does not match task warehouse");
DispatchItem item = items.selectById(itemId); DispatchItem item = items.selectById(itemId);
if (item == null || !tenantId.equals(item.getTenantId()) || !order.getId().equals(item.getDispatchOrderId())) { if (item == null || !customerId.equals(item.getCustomerId()) || !order.getId().equals(item.getDispatchOrderId())) {
throw notFound("Dispatch pallet item not found"); throw notFound("Dispatch pallet item not found");
} }
if (!warehouseId.equals(item.getOverseasWarehouseId())) throw conflict("Pallet does not belong to selected warehouse"); if (!warehouseId.equals(item.getOverseasWarehouseId())) throw conflict("Pallet does not belong to selected warehouse");
DispatchPdaOperation replay = replay(tenantId, "PICK", request.idempotencyKey(), request); DispatchPdaOperation replay = replay(customerId, "PICK", request.idempotencyKey(), request);
if (replay != null) return read(replay, DispatchPdaResponses.Picked.class); if (replay != null) return read(replay, DispatchPdaResponses.Picked.class);
InventoryPosition position = positions.selectById(request.positionId()); InventoryPosition position = positions.selectById(request.positionId());
if (position == null || !tenantId.equals(position.getTenantId()) if (position == null || !customerId.equals(position.getCustomerId())
|| item.getWarehousePalletId() == null || !item.getWarehousePalletId().equals(position.getPalletId())) { || item.getWarehousePalletId() == null || !item.getWarehousePalletId().equals(position.getPalletId())) {
throw conflict("Inventory position does not belong to this dispatch pallet"); throw conflict("Inventory position does not belong to this dispatch pallet");
} }
long picked = picks.totalItem(tenantId, task.getId(), itemId); long picked = picks.totalItem(customerId, task.getId(), itemId);
if (picked + request.packages() > item.getPackageCount()) throw conflict("Picked packages exceed dispatch pallet packages"); if (picked + request.packages() > item.getPackageCount()) throw conflict("Picked packages exceed dispatch pallet packages");
String keyBase = "DISPATCH:" + order.getDispatchNo() + ":" + itemId + ":" + request.idempotencyKey(); String keyBase = "DISPATCH:" + order.getDispatchNo() + ":" + itemId + ":" + request.idempotencyKey();
InventoryCommandRequests.Move move = new InventoryCommandRequests.Move(tenantId, request.positionId(), InventoryCommandRequests.Move move = new InventoryCommandRequests.Move(customerId, request.positionId(),
request.packages(), order.getDispatchNo(), order.getDispatchNo(), null, request.packages(), order.getDispatchNo(), order.getDispatchNo(), null,
keyBase + ":ALLOCATE", request.remark()); keyBase + ":ALLOCATE", request.remark());
inventory.allocate(tenantId, operatorId, move); inventory.allocate(customerId, operatorId, move);
inventory.pick(tenantId, operatorId, new InventoryCommandRequests.Move(tenantId, request.positionId(), inventory.pick(customerId, operatorId, new InventoryCommandRequests.Move(customerId, request.positionId(),
request.packages(), order.getDispatchNo(), order.getDispatchNo(), null, request.packages(), order.getDispatchNo(), order.getDispatchNo(), null,
keyBase + ":PICK", request.remark())); keyBase + ":PICK", request.remark()));
DispatchPdaPick pick = new DispatchPdaPick(); DispatchPdaPick pick = new DispatchPdaPick();
pick.setId(IdWorker.getId()); pick.setTenantId(tenantId); pick.setTaskId(task.getId()); pick.setId(IdWorker.getId()); pick.setCustomerId(customerId); pick.setTaskId(task.getId());
pick.setDispatchItemId(itemId); pick.setInventoryPositionId(request.positionId()); pick.setPackages(request.packages()); pick.setDispatchItemId(itemId); pick.setInventoryPositionId(request.positionId()); pick.setPackages(request.packages());
pick.setProductQuantity(request.productQuantity()); pick.setIdempotencyKey(request.idempotencyKey()); pick.setProductQuantity(request.productQuantity()); pick.setIdempotencyKey(request.idempotencyKey());
pick.setPickedAt(LocalDateTime.now()); pick.setCreatedAt(pick.getPickedAt()); picks.insert(pick); pick.setPickedAt(LocalDateTime.now()); pick.setCreatedAt(pick.getPickedAt()); picks.insert(pick);
pickAttribution.attribute(tenantId, pick); pickAttribution.attribute(customerId, pick);
long total = picked + request.packages(); long total = picked + request.packages();
order.setPickedPackageCount((int) (n(order.getPickedPackageCount()) + request.packages())); order.setPickedPackageCount((int) (n(order.getPickedPackageCount()) + request.packages()));
task.setStatus("PICKING"); task.setUpdatedAt(LocalDateTime.now()); task.setStatus("PICKING"); task.setUpdatedAt(LocalDateTime.now());
if (allPicked(tenantId, task.getId(), items.findByDispatchOrder(tenantId, order.getId()))) { if (allPicked(customerId, task.getId(), items.findByDispatchOrder(customerId, order.getId()))) {
task.setStatus("COMPLETED"); task.setCompletedAt(LocalDateTime.now()); order.setStatus("READY_TO_DEPART"); task.setStatus("COMPLETED"); task.setCompletedAt(LocalDateTime.now()); order.setStatus("READY_TO_DEPART");
} }
order.setUpdatedAt(LocalDateTime.now()); order.setUpdatedAt(LocalDateTime.now());
orders.updateById(order); orders.updateById(order);
tasks.updateById(task); tasks.updateById(task);
DispatchPdaResponses.Picked result = new DispatchPdaResponses.Picked(itemId, request.positionId(), request.packages(), total, false, false); DispatchPdaResponses.Picked result = new DispatchPdaResponses.Picked(itemId, request.positionId(), request.packages(), total, false, false);
save(tenantId, task.getId(), "PICK", request.idempotencyKey(), request, result); save(customerId, task.getId(), "PICK", request.idempotencyKey(), request, result);
return result; return result;
} }
@Transactional @Transactional
public DispatchPdaResponses.PickingCompleted completePicking(String tenantId, String dispatchNo, public DispatchPdaResponses.PickingCompleted completePicking(String customerId, String dispatchNo,
DispatchPdaRequests.CompletePicking request) { DispatchPdaRequests.CompletePicking request) {
DispatchOrder found = requireOrder(tenantId, dispatchNo); DispatchOrder found = requireOrder(customerId, dispatchNo);
DispatchOrder order = orders.lockById(tenantId, found.getId()); DispatchOrder order = orders.lockById(customerId, found.getId());
if (order == null) throw notFound("Dispatch order not found"); if (order == null) throw notFound("Dispatch order not found");
DispatchPdaOperation replay = replay(tenantId, "COMPLETE_PICKING", request.idempotencyKey(), request); DispatchPdaOperation replay = replay(customerId, "COMPLETE_PICKING", request.idempotencyKey(), request);
if (replay != null) return read(replay, DispatchPdaResponses.PickingCompleted.class).withReplay(); if (replay != null) return read(replay, DispatchPdaResponses.PickingCompleted.class).withReplay();
requireVehicle(order); requireVehicle(order);
DispatchPdaTask task = tasks.lockByDispatch(tenantId, order.getId()); DispatchPdaTask task = tasks.lockByDispatch(customerId, order.getId());
if (task == null || !("PDA_PENDING".equals(task.getStatus()) || "PICKING".equals(task.getStatus()))) { if (task == null || !("PDA_PENDING".equals(task.getStatus()) || "PICKING".equals(task.getStatus()))) {
throw conflict("Dispatch PDA task is not available for completion"); throw conflict("Dispatch PDA task is not available for completion");
} }
List<DispatchItem> source = items.findByDispatchOrder(tenantId, order.getId()); List<DispatchItem> source = items.findByDispatchOrder(customerId, order.getId());
long planned = source.stream().map(DispatchItem::getPackageCount).filter(Objects::nonNull) long planned = source.stream().map(DispatchItem::getPackageCount).filter(Objects::nonNull)
.mapToLong(Integer::longValue).sum(); .mapToLong(Integer::longValue).sum();
long picked = picks.listTask(tenantId, task.getId()).stream().mapToLong(p -> n(p.getPackages())).sum(); long picked = picks.listTask(customerId, task.getId()).stream().mapToLong(p -> n(p.getPackages())).sum();
if (picked <= 0 || picked > planned) throw conflict("PDA completion requires a valid picked package count"); if (picked <= 0 || picked > planned) throw conflict("PDA completion requires a valid picked package count");
boolean partial = picked < planned; boolean partial = picked < planned;
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
@@ -166,47 +166,47 @@ public class DispatchPdaWorkflowService {
order.setUpdatedAt(now); orders.updateById(order); order.setUpdatedAt(now); orders.updateById(order);
DispatchPdaResponses.PickingCompleted result = new DispatchPdaResponses.PickingCompleted(order.getDispatchNo(), DispatchPdaResponses.PickingCompleted result = new DispatchPdaResponses.PickingCompleted(order.getDispatchNo(),
order.getStatus(), planned, picked, planned - picked, partial, false); order.getStatus(), planned, picked, planned - picked, partial, false);
save(tenantId, task.getId(), "COMPLETE_PICKING", request.idempotencyKey(), request, result); save(customerId, task.getId(), "COMPLETE_PICKING", request.idempotencyKey(), request, result);
return result; return result;
} }
/** Final warehouse exit: only fully PDA-picked dispatches can consume picked inventory. */ /** Final warehouse exit: only fully PDA-picked dispatches can consume picked inventory. */
@Transactional @Transactional
public DispatchPdaResponses.Departed depart(String tenantId, String dispatchNo, String operatorId, public DispatchPdaResponses.Departed depart(String customerId, String dispatchNo, String operatorId,
DispatchPdaRequests.Depart request) { DispatchPdaRequests.Depart request) {
DispatchOrder found = requireOrder(tenantId, dispatchNo); DispatchOrder found = requireOrder(customerId, dispatchNo);
DispatchOrder order = orders.lockById(tenantId, found.getId()); DispatchOrder order = orders.lockById(customerId, found.getId());
if (order == null) throw notFound("Dispatch order not found"); if (order == null) throw notFound("Dispatch order not found");
DispatchPdaOperation replay = replay(tenantId, "DEPART", request.idempotencyKey(), request); DispatchPdaOperation replay = replay(customerId, "DEPART", request.idempotencyKey(), request);
if (replay != null) return read(replay, DispatchPdaResponses.Departed.class).withReplay(); if (replay != null) return read(replay, DispatchPdaResponses.Departed.class).withReplay();
requireVehicle(order); requireVehicle(order);
OutboundOrder outbound = outboundOrders.lockByDispatch(tenantId, order.getId()); OutboundOrder outbound = outboundOrders.lockByDispatch(customerId, order.getId());
if (outbound == null || !"WAIT_OUTBOUND".equals(outbound.getStatus())) { if (outbound == null || !"WAIT_OUTBOUND".equals(outbound.getStatus())) {
throw conflict("Dispatch must be transferred to a pending outbound order before departure"); throw conflict("Dispatch must be transferred to a pending outbound order before departure");
} }
DispatchPdaTask task = tasks.lockByDispatch(tenantId, order.getId()); DispatchPdaTask task = tasks.lockByDispatch(customerId, order.getId());
if (task == null || !"COMPLETED".equals(task.getStatus()) if (task == null || !"COMPLETED".equals(task.getStatus())
|| !("READY_TO_DEPART".equals(order.getStatus()) || "PARTIAL_READY_TO_DEPART".equals(order.getStatus()))) { || !("READY_TO_DEPART".equals(order.getStatus()) || "PARTIAL_READY_TO_DEPART".equals(order.getStatus()))) {
throw conflict("Dispatch must complete PDA picking before departure"); throw conflict("Dispatch must complete PDA picking before departure");
} }
List<DispatchItem> dispatchItems = items.findByDispatchOrder(tenantId, order.getId()); List<DispatchItem> dispatchItems = items.findByDispatchOrder(customerId, order.getId());
boolean partial = "PARTIAL_READY_TO_DEPART".equals(order.getStatus()); boolean partial = "PARTIAL_READY_TO_DEPART".equals(order.getStatus());
if (!partial && !allPicked(tenantId, task.getId(), dispatchItems)) { if (!partial && !allPicked(customerId, task.getId(), dispatchItems)) {
throw conflict("Every dispatch pallet must be fully picked before departure"); throw conflict("Every dispatch pallet must be fully picked before departure");
} }
List<DispatchPdaPick> dispatchPicks = picks.listTask(tenantId, task.getId()); List<DispatchPdaPick> dispatchPicks = picks.listTask(customerId, task.getId());
if (dispatchPicks.isEmpty()) throw conflict("Dispatch has no PDA picks to ship"); if (dispatchPicks.isEmpty()) throw conflict("Dispatch has no PDA picks to ship");
long shipped = 0; long shipped = 0;
for (DispatchPdaPick pick : dispatchPicks) { for (DispatchPdaPick pick : dispatchPicks) {
String key = "DISPATCH:" + order.getDispatchNo() + ":PICK:" + pick.getId() + ":SHIP"; String key = "DISPATCH:" + order.getDispatchNo() + ":PICK:" + pick.getId() + ":SHIP";
inventory.ship(tenantId, operatorId, new InventoryCommandRequests.Move(tenantId, inventory.ship(customerId, operatorId, new InventoryCommandRequests.Move(customerId,
pick.getInventoryPositionId(), n(pick.getPackages()), pick.getInventoryPositionId(), n(pick.getPackages()),
order.getDispatchNo(), order.getDispatchNo(), null, key, request.remark())); order.getDispatchNo(), order.getDispatchNo(), null, key, request.remark()));
shipped += n(pick.getPackages()); shipped += n(pick.getPackages());
} }
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
pickAttribution.depart(tenantId, outbound.getOutboundNo(), order.getDispatchNo(), dispatchPicks, now); pickAttribution.depart(customerId, outbound.getOutboundNo(), order.getDispatchNo(), dispatchPicks, now);
outbound.setStatus("OUTBOUNDED"); outbound.setStatus("OUTBOUNDED");
outbound.setUpdatedAt(now); outbound.setUpdatedAt(now);
outboundOrders.updateById(outbound); outboundOrders.updateById(outbound);
@@ -214,33 +214,33 @@ public class DispatchPdaWorkflowService {
order.setStatus(partial ? "PARTIAL_DEPARTED" : "DEPARTED"); order.setStatus(partial ? "PARTIAL_DEPARTED" : "DEPARTED");
order.setUpdatedAt(now); order.setUpdatedAt(now);
orders.updateById(order); orders.updateById(order);
releasePalletLocks(tenantId, order.getId(), now); releasePalletLocks(customerId, order.getId(), now);
DispatchPdaResponses.Departed result = new DispatchPdaResponses.Departed(order.getDispatchNo(), DispatchPdaResponses.Departed result = new DispatchPdaResponses.Departed(order.getDispatchNo(),
order.getStatus(), shipped, false); order.getStatus(), shipped, false);
save(tenantId, task.getId(), "DEPART", request.idempotencyKey(), request, result); save(customerId, task.getId(), "DEPART", request.idempotencyKey(), request, result);
return result; return result;
} }
private DispatchPdaResponses.Task task(String tenantId, DispatchOrder order, DispatchPdaTask task, List<DispatchItem> source) { private DispatchPdaResponses.Task task(String customerId, DispatchOrder order, DispatchPdaTask task, List<DispatchItem> source) {
List<DispatchPdaResponses.Item> values = new ArrayList<>(); List<DispatchPdaResponses.Item> values = new ArrayList<>();
for (DispatchItem item : source) { for (DispatchItem item : source) {
List<InventoryPosition> palletPositions = item.getWarehousePalletId() == null ? List.of() List<InventoryPosition> palletPositions = item.getWarehousePalletId() == null ? List.of()
: positions.listPallet(tenantId, item.getWarehousePalletId()); : positions.listPallet(customerId, item.getWarehousePalletId());
values.add(new DispatchPdaResponses.Item(item.getId(), item.getPalletNo(), item.getDestinationWarehouseCode(), values.add(new DispatchPdaResponses.Item(item.getId(), item.getPalletNo(), item.getDestinationWarehouseCode(),
item.getDestinationWarehouseName(), item.getFbaWarehouseCode(), item.getPackageCount(), item.getDestinationWarehouseName(), item.getFbaWarehouseCode(), item.getPackageCount(),
picks.totalItem(tenantId, task.getId(), item.getId()), palletPositions.stream().map(p -> picks.totalItem(customerId, task.getId(), item.getId()), palletPositions.stream().map(p ->
new DispatchPdaResponses.Position(p.getId(), p.getBalanceId(), n(p.getAvailablePackages()), new DispatchPdaResponses.Position(p.getId(), p.getBalanceId(), n(p.getAvailablePackages()),
n(p.getAllocatedPackages()), n(p.getPickedPackages()))).toList())); n(p.getAllocatedPackages()), n(p.getPickedPackages()))).toList()));
} }
return new DispatchPdaResponses.Task(task.getId(), task.getTaskNo(), order.getDispatchNo(), order.getVehiclePlate(), task.getWarehouseId(), task.getStatus(), values); return new DispatchPdaResponses.Task(task.getId(), task.getTaskNo(), order.getDispatchNo(), order.getVehiclePlate(), task.getWarehouseId(), task.getStatus(), values);
} }
private boolean allPicked(String tenantId, Long taskId, List<DispatchItem> source) { private boolean allPicked(String customerId, Long taskId, List<DispatchItem> source) {
return source.stream().allMatch(item -> picks.totalItem(tenantId, taskId, item.getId()) == item.getPackageCount()); return source.stream().allMatch(item -> picks.totalItem(customerId, taskId, item.getId()) == item.getPackageCount());
} }
private void releasePalletLocks(String tenantId, Long dispatchOrderId, LocalDateTime now) { private void releasePalletLocks(String customerId, Long dispatchOrderId, LocalDateTime now) {
List<DispatchPalletLock> activeLocks = palletLocks.selectList(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<DispatchPalletLock>() List<DispatchPalletLock> activeLocks = palletLocks.selectList(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<DispatchPalletLock>()
.eq(DispatchPalletLock::getTenantId, tenantId) .eq(DispatchPalletLock::getCustomerId, customerId)
.eq(DispatchPalletLock::getDispatchOrderId, dispatchOrderId) .eq(DispatchPalletLock::getDispatchOrderId, dispatchOrderId)
.eq(DispatchPalletLock::getStatus, "ACTIVE")); .eq(DispatchPalletLock::getStatus, "ACTIVE"));
for (DispatchPalletLock lock : activeLocks) { for (DispatchPalletLock lock : activeLocks) {
@@ -251,7 +251,7 @@ public class DispatchPdaWorkflowService {
palletLocks.updateById(lock); palletLocks.updateById(lock);
} }
} }
private DispatchOrder requireOrder(String tenantId, String dispatchNo) { DispatchOrder order = orders.findByDispatchNo(tenantId, dispatchNo); if (order == null) throw notFound("Dispatch order not found"); return order; } private DispatchOrder requireOrder(String customerId, String dispatchNo) { DispatchOrder order = orders.findByDispatchNo(customerId, dispatchNo); if (order == null) throw notFound("Dispatch order not found"); return order; }
private void requirePdaStatus(DispatchOrder order) { private void requirePdaStatus(DispatchOrder order) {
if (!("PENDING_DEPARTURE".equals(order.getStatus()) || "PICKING".equals(order.getStatus()) if (!("PENDING_DEPARTURE".equals(order.getStatus()) || "PICKING".equals(order.getStatus())
|| "READY_TO_DEPART".equals(order.getStatus()))) throw conflict("Dispatch has already departed and cannot be changed by PDA"); || "READY_TO_DEPART".equals(order.getStatus()))) throw conflict("Dispatch has already departed and cannot be changed by PDA");
@@ -263,8 +263,8 @@ public class DispatchPdaWorkflowService {
throw conflict("Selected warehouse does not match dispatch warehouse"); throw conflict("Selected warehouse does not match dispatch warehouse");
} }
} }
private DispatchPdaOperation replay(String tenantId, String type, String key, Object request) { DispatchPdaOperation op = operations.findKey(tenantId, required(key, "Idempotency key is required")); if (op == null) return null; if (!type.equals(op.getOperationType()) || !hash(request).equals(op.getRequestHash())) throw conflict("Idempotency key was reused with a different request"); if (!"COMPLETED".equals(op.getStatus())) throw conflict("PDA operation is still processing"); return op; } private DispatchPdaOperation replay(String customerId, String type, String key, Object request) { DispatchPdaOperation op = operations.findKey(customerId, required(key, "Idempotency key is required")); if (op == null) return null; if (!type.equals(op.getOperationType()) || !hash(request).equals(op.getRequestHash())) throw conflict("Idempotency key was reused with a different request"); if (!"COMPLETED".equals(op.getStatus())) throw conflict("PDA operation is still processing"); return op; }
private void save(String tenantId, Long taskId, String type, String key, Object request, Object result) { DispatchPdaOperation op = new DispatchPdaOperation(); op.setId(IdWorker.getId()); op.setTenantId(tenantId); op.setTaskId(taskId); op.setOperationType(type); op.setIdempotencyKey(required(key,"Idempotency key is required")); op.setRequestHash(hash(request)); op.setResultJson(json(result)); op.setStatus("COMPLETED"); op.setCreatedAt(LocalDateTime.now()); op.setUpdatedAt(op.getCreatedAt()); operations.insert(op); } private void save(String customerId, Long taskId, String type, String key, Object request, Object result) { DispatchPdaOperation op = new DispatchPdaOperation(); op.setId(IdWorker.getId()); op.setCustomerId(customerId); op.setTaskId(taskId); op.setOperationType(type); op.setIdempotencyKey(required(key,"Idempotency key is required")); op.setRequestHash(hash(request)); op.setResultJson(json(result)); op.setStatus("COMPLETED"); op.setCreatedAt(LocalDateTime.now()); op.setUpdatedAt(op.getCreatedAt()); operations.insert(op); }
private <T> T read(DispatchPdaOperation op, Class<T> type) { try { return objectMapper.readValue(op.getResultJson(), type); } catch (Exception e) { throw new IllegalStateException("Cannot read PDA replay", e); } } private <T> T read(DispatchPdaOperation op, Class<T> type) { try { return objectMapper.readValue(op.getResultJson(), type); } catch (Exception e) { throw new IllegalStateException("Cannot read PDA replay", e); } }
private String hash(Object value) { try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(json(value).getBytes(StandardCharsets.UTF_8))); } catch (Exception e) { throw new IllegalStateException(e); } } private String hash(Object value) { try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(json(value).getBytes(StandardCharsets.UTF_8))); } catch (Exception e) { throw new IllegalStateException(e); } }
private String json(Object value) { try { return objectMapper.writeValueAsString(value); } catch (Exception e) { throw new IllegalStateException(e); } } private String json(Object value) { try { return objectMapper.writeValueAsString(value); } catch (Exception e) { throw new IllegalStateException(e); } }
@@ -26,18 +26,18 @@ public class DispatchPickOrderAttributionService {
@Resource private LastlegOrderStatusHistoryMapper history; @Resource private LastlegOrderStatusHistoryMapper history;
/** Call after the inventory position has been locked/moved and the PDA pick row has been inserted. */ /** Call after the inventory position has been locked/moved and the PDA pick row has been inserted. */
public List<OrderPackages> attribute(String tenantId, DispatchPdaPick pick) { public List<OrderPackages> attribute(String customerId, DispatchPdaPick pick) {
long remainingPackages = value(pick.getPackages()); long remainingPackages = value(pick.getPackages());
long totalPackages = remainingPackages; long totalPackages = remainingPackages;
long totalQuantity = value(pick.getProductQuantity()); long totalQuantity = value(pick.getProductQuantity());
long allocatedPackages = 0; long allocatedPackages = 0;
long allocatedQuantity = 0; long allocatedQuantity = 0;
Map<Long, Long> pickedByOrder = new LinkedHashMap<>(); Map<Long, Long> pickedByOrder = new LinkedHashMap<>();
for (InboundStockReceipt receipt : attributions.availableReceipts(tenantId, for (InboundStockReceipt receipt : attributions.availableReceipts(customerId,
pick.getInventoryPositionId())) { pick.getInventoryPositionId())) {
if (remainingPackages == 0) break; if (remainingPackages == 0) break;
long receiptPackages = value(receipt.getReceivedPackages()) long receiptPackages = value(receipt.getReceivedPackages())
- attributions.attributedPackages(tenantId, receipt.getId()); - attributions.attributedPackages(customerId, receipt.getId());
if (receiptPackages <= 0) continue; if (receiptPackages <= 0) continue;
long packages = Math.min(remainingPackages, receiptPackages); long packages = Math.min(remainingPackages, receiptPackages);
long nextPackages = allocatedPackages + packages; long nextPackages = allocatedPackages + packages;
@@ -45,7 +45,7 @@ public class DispatchPickOrderAttributionService {
long quantity = nextQuantity - allocatedQuantity; long quantity = nextQuantity - allocatedQuantity;
DispatchPdaPickOrder row = new DispatchPdaPickOrder(); DispatchPdaPickOrder row = new DispatchPdaPickOrder();
row.setId(IdWorker.getId()); row.setId(IdWorker.getId());
row.setTenantId(tenantId); row.setCustomerId(customerId);
row.setPickId(pick.getId()); row.setPickId(pick.getId());
row.setReceiptId(receipt.getId()); row.setReceiptId(receipt.getId());
row.setOrderId(receipt.getOrderId()); row.setOrderId(receipt.getOrderId());
@@ -60,7 +60,7 @@ public class DispatchPickOrderAttributionService {
} }
if (remainingPackages != 0) throw conflict("PDA pick cannot be attributed to inbound receipt orders"); if (remainingPackages != 0) throw conflict("PDA pick cannot be attributed to inbound receipt orders");
for (Map.Entry<Long, Long> entry : pickedByOrder.entrySet()) { for (Map.Entry<Long, Long> entry : pickedByOrder.entrySet()) {
if (orders.increasePicked(tenantId, entry.getKey(), entry.getValue()) != 1) { if (orders.increasePicked(customerId, entry.getKey(), entry.getValue()) != 1) {
throw conflict("Picked packages exceed tail-leg order remaining packages"); throw conflict("Picked packages exceed tail-leg order remaining packages");
} }
} }
@@ -68,17 +68,17 @@ public class DispatchPickOrderAttributionService {
} }
/** Call once for each departing PDA pick; repeated dispatch departure is protected by its outer idempotency key. */ /** Call once for each departing PDA pick; repeated dispatch departure is protected by its outer idempotency key. */
public List<OrderPackages> depart(String tenantId, String outboundNo, String dispatchNo, List<DispatchPdaPick> picks, public List<OrderPackages> depart(String customerId, String outboundNo, String dispatchNo, List<DispatchPdaPick> picks,
LocalDateTime departedAt) { LocalDateTime departedAt) {
Map<Long, Long> shippedByOrder = new LinkedHashMap<>(); Map<Long, Long> shippedByOrder = new LinkedHashMap<>();
for (DispatchPdaPick pick : picks) { for (DispatchPdaPick pick : picks) {
for (DispatchPdaPickOrder row : attributions.listPick(tenantId, pick.getId())) { for (DispatchPdaPickOrder row : attributions.listPick(customerId, pick.getId())) {
shippedByOrder.merge(row.getOrderId(), value(row.getPackages()), Long::sum); shippedByOrder.merge(row.getOrderId(), value(row.getPackages()), Long::sum);
} }
} }
for (Map.Entry<Long, Long> entry : shippedByOrder.entrySet()) { for (Map.Entry<Long, Long> entry : shippedByOrder.entrySet()) {
LastlegOrder before = orders.lockById(tenantId, entry.getKey()); LastlegOrder before = orders.lockById(customerId, entry.getKey());
if (before == null || orders.transferPickedToShipped(tenantId, entry.getKey(), entry.getValue(), outboundNo) != 1) { if (before == null || orders.transferPickedToShipped(customerId, entry.getKey(), entry.getValue(), outboundNo) != 1) {
throw conflict("Tail-leg order picked packages are inconsistent"); throw conflict("Tail-leg order picked packages are inconsistent");
} }
long shipped = value(before.getShippedPackageCount()) + entry.getValue(); long shipped = value(before.getShippedPackageCount()) + entry.getValue();
@@ -43,21 +43,21 @@ public class DispatchService {
@Resource private DispatchPalletLockMapper locks; @Resource private DispatchPalletLockMapper locks;
@Resource private DispatchCandidateMapper candidates; @Resource private DispatchCandidateMapper candidates;
public List<DispatchPalletCandidate> candidates(String tenantId, String orderNo, public List<DispatchPalletCandidate> candidates(String customerId, String orderNo,
String fbaWarehouseCode, String sourceStage) { String fbaWarehouseCode, String sourceStage) {
if (StringUtils.hasText(sourceStage) && !Set.of("PLAN", "RECEIVED").contains(sourceStage)) { if (StringUtils.hasText(sourceStage) && !Set.of("PLAN", "RECEIVED").contains(sourceStage)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "sourceStage must be PLAN or RECEIVED"); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "sourceStage must be PLAN or RECEIVED");
} }
return candidates.available(tenantId, trimToNull(orderNo), trimToNull(fbaWarehouseCode), trimToNull(sourceStage)); return candidates.available(customerId, trimToNull(orderNo), trimToNull(fbaWarehouseCode), trimToNull(sourceStage));
} }
@Transactional @Transactional
public DispatchResponse preArrange(String tenantId, String operatorId, DispatchRequests.PreArrange request) { public DispatchResponse preArrange(String customerId, String operatorId, DispatchRequests.PreArrange request) {
List<Long> palletIds = request.planPalletIds().stream().filter(Objects::nonNull).distinct().sorted().toList(); List<Long> palletIds = request.planPalletIds().stream().filter(Objects::nonNull).distinct().sorted().toList();
if (palletIds.isEmpty() || palletIds.size() != request.planPalletIds().size()) { if (palletIds.isEmpty() || palletIds.size() != request.planPalletIds().size()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Plan pallet IDs must be unique and non-empty"); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Plan pallet IDs must be unique and non-empty");
} }
List<DispatchPalletCandidate> selected = candidates.lockSelected(tenantId, palletIds); List<DispatchPalletCandidate> selected = candidates.lockSelected(customerId, palletIds);
if (selected.size() != palletIds.size()) { if (selected.size() != palletIds.size()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "One or more selected pallets are not in this tenant"); throw new ResponseStatusException(HttpStatus.NOT_FOUND, "One or more selected pallets are not in this tenant");
} }
@@ -71,9 +71,9 @@ public class DispatchService {
} }
for (DispatchPalletCandidate candidate : selected) { for (DispatchPalletCandidate candidate : selected) {
boolean planLocked = candidate.planPalletId() != null boolean planLocked = candidate.planPalletId() != null
&& locks.lockActive(tenantId, "PLAN:" + candidate.planPalletId()) != null; && locks.lockActive(customerId, "PLAN:" + candidate.planPalletId()) != null;
boolean receivedLocked = candidate.warehousePalletId() != null boolean receivedLocked = candidate.warehousePalletId() != null
&& locks.lockActive(tenantId, "RECEIVED:" + candidate.warehousePalletId()) != null; && locks.lockActive(customerId, "RECEIVED:" + candidate.warehousePalletId()) != null;
if (planLocked || receivedLocked) { if (planLocked || receivedLocked) {
throw new ResponseStatusException(HttpStatus.CONFLICT, throw new ResponseStatusException(HttpStatus.CONFLICT,
"Pallet is already locked by another active dispatch: " + candidate.palletNo()); "Pallet is already locked by another active dispatch: " + candidate.palletNo());
@@ -83,7 +83,7 @@ public class DispatchService {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
DispatchOrder dispatch = new DispatchOrder(); DispatchOrder dispatch = new DispatchOrder();
dispatch.setId(IdWorker.getId()); dispatch.setId(IdWorker.getId());
dispatch.setTenantId(tenantId); dispatch.setCustomerId(customerId);
dispatch.setDispatchNo("PC" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + dispatch.getId()); dispatch.setDispatchNo("PC" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + dispatch.getId());
dispatch.setOverseasWarehouseId(selected.get(0).overseasWarehouseId()); dispatch.setOverseasWarehouseId(selected.get(0).overseasWarehouseId());
dispatch.setOverseasWarehouseName(selected.get(0).overseasWarehouseName()); dispatch.setOverseasWarehouseName(selected.get(0).overseasWarehouseName());
@@ -106,7 +106,7 @@ public class DispatchService {
for (DispatchPalletCandidate candidate : selected) { for (DispatchPalletCandidate candidate : selected) {
DispatchPalletLock lock = new DispatchPalletLock(); DispatchPalletLock lock = new DispatchPalletLock();
lock.setId(IdWorker.getId()); lock.setId(IdWorker.getId());
lock.setTenantId(tenantId); lock.setCustomerId(customerId);
lock.setSourcePalletKey(candidate.sourcePalletKey()); lock.setSourcePalletKey(candidate.sourcePalletKey());
lock.setActiveKey(candidate.sourcePalletKey()); lock.setActiveKey(candidate.sourcePalletKey());
lock.setPlanId(candidate.planId()); lock.setPlanId(candidate.planId());
@@ -126,7 +126,7 @@ public class DispatchService {
DispatchItem item = new DispatchItem(); DispatchItem item = new DispatchItem();
item.setId(IdWorker.getId()); item.setId(IdWorker.getId());
item.setDispatchOrderId(dispatch.getId()); item.setDispatchOrderId(dispatch.getId());
item.setTenantId(tenantId); item.setCustomerId(customerId);
item.setPlanId(candidate.planId()); item.setPlanId(candidate.planId());
item.setPlanPalletId(candidate.planPalletId()); item.setPlanPalletId(candidate.planPalletId());
item.setWarehousePalletId(candidate.warehousePalletId()); item.setWarehousePalletId(candidate.warehousePalletId());
@@ -147,12 +147,12 @@ public class DispatchService {
item.setCreatedAt(now); item.setCreatedAt(now);
items.insert(item); items.insert(item);
} }
return response(dispatch, items.findByDispatchOrder(tenantId, dispatch.getId())); return response(dispatch, items.findByDispatchOrder(customerId, dispatch.getId()));
} }
public PageResult<DispatchResponse> page(String tenantId, DispatchRequests.Query query) { public PageResult<DispatchResponse> page(String customerId, DispatchRequests.Query query) {
Page<DispatchOrder> page = orders.selectPage(new Page<>(query.page(), query.size()), Page<DispatchOrder> page = orders.selectPage(new Page<>(query.page(), query.size()),
new LambdaQueryWrapper<DispatchOrder>().eq(DispatchOrder::getTenantId, tenantId) new LambdaQueryWrapper<DispatchOrder>().eq(DispatchOrder::getCustomerId, customerId)
.like(StringUtils.hasText(query.dispatchNo()), DispatchOrder::getDispatchNo, query.dispatchNo()) .like(StringUtils.hasText(query.dispatchNo()), DispatchOrder::getDispatchNo, query.dispatchNo())
.like(StringUtils.hasText(query.vehiclePlate()), DispatchOrder::getVehiclePlate, query.vehiclePlate()) .like(StringUtils.hasText(query.vehiclePlate()), DispatchOrder::getVehiclePlate, query.vehiclePlate())
.eq(StringUtils.hasText(query.status()), DispatchOrder::getStatus, query.status()) .eq(StringUtils.hasText(query.status()), DispatchOrder::getStatus, query.status())
@@ -160,18 +160,18 @@ public class DispatchService {
query.fbaAppointmentNo()) query.fbaAppointmentNo())
.orderByDesc(DispatchOrder::getCreatedAt)); .orderByDesc(DispatchOrder::getCreatedAt));
List<DispatchResponse> records = page.getRecords().stream() List<DispatchResponse> records = page.getRecords().stream()
.map(order -> response(order, items.findByDispatchOrder(tenantId, order.getId()))).toList(); .map(order -> response(order, items.findByDispatchOrder(customerId, order.getId()))).toList();
return new PageResult<>(records, page.getCurrent(), page.getSize(), page.getTotal(), page.getPages()); return new PageResult<>(records, page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
} }
public DispatchResponse detail(String tenantId, String dispatchNo) { public DispatchResponse detail(String customerId, String dispatchNo) {
DispatchOrder dispatch = find(tenantId, dispatchNo); DispatchOrder dispatch = find(customerId, dispatchNo);
return response(dispatch, items.findByDispatchOrder(tenantId, dispatch.getId())); return response(dispatch, items.findByDispatchOrder(customerId, dispatch.getId()));
} }
@Transactional @Transactional
public DispatchResponse update(String tenantId, String dispatchNo, DispatchRequests.Update request) { public DispatchResponse update(String customerId, String dispatchNo, DispatchRequests.Update request) {
DispatchOrder dispatch = orders.lockById(tenantId, find(tenantId, dispatchNo).getId()); DispatchOrder dispatch = orders.lockById(customerId, find(customerId, dispatchNo).getId());
if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found"); if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found");
// Vehicle plate is intentionally editable through all dispatch states. // Vehicle plate is intentionally editable through all dispatch states.
dispatch.setVehiclePlate(trimToNull(request.vehiclePlate())); dispatch.setVehiclePlate(trimToNull(request.vehiclePlate()));
@@ -193,12 +193,12 @@ public class DispatchService {
} }
dispatch.setUpdatedAt(LocalDateTime.now()); dispatch.setUpdatedAt(LocalDateTime.now());
orders.updateById(dispatch); orders.updateById(dispatch);
return response(dispatch, items.findByDispatchOrder(tenantId, dispatch.getId())); return response(dispatch, items.findByDispatchOrder(customerId, dispatch.getId()));
} }
@Transactional @Transactional
public void cancel(String tenantId, String dispatchNo) { public void cancel(String customerId, String dispatchNo) {
DispatchOrder dispatch = orders.lockById(tenantId, find(tenantId, dispatchNo).getId()); DispatchOrder dispatch = orders.lockById(customerId, find(customerId, dispatchNo).getId());
if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found"); if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found");
if (!(PENDING_DEPARTURE.equals(dispatch.getStatus()))) { if (!(PENDING_DEPARTURE.equals(dispatch.getStatus()))) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "Dispatched order cannot be cancelled"); throw new ResponseStatusException(HttpStatus.CONFLICT, "Dispatched order cannot be cancelled");
@@ -208,7 +208,7 @@ public class DispatchService {
dispatch.setUpdatedAt(now); dispatch.setUpdatedAt(now);
orders.updateById(dispatch); orders.updateById(dispatch);
List<DispatchPalletLock> activeLocks = locks.selectList(new LambdaQueryWrapper<DispatchPalletLock>() List<DispatchPalletLock> activeLocks = locks.selectList(new LambdaQueryWrapper<DispatchPalletLock>()
.eq(DispatchPalletLock::getTenantId, tenantId) .eq(DispatchPalletLock::getCustomerId, customerId)
.eq(DispatchPalletLock::getDispatchOrderId, dispatch.getId()) .eq(DispatchPalletLock::getDispatchOrderId, dispatch.getId())
.eq(DispatchPalletLock::getStatus, "ACTIVE")); .eq(DispatchPalletLock::getStatus, "ACTIVE"));
for (DispatchPalletLock lock : activeLocks) { for (DispatchPalletLock lock : activeLocks) {
@@ -220,8 +220,8 @@ public class DispatchService {
} }
} }
private DispatchOrder find(String tenantId, String dispatchNo) { private DispatchOrder find(String customerId, String dispatchNo) {
DispatchOrder dispatch = orders.findByDispatchNo(tenantId, dispatchNo); DispatchOrder dispatch = orders.findByDispatchNo(customerId, dispatchNo);
if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found"); if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found");
return dispatch; return dispatch;
} }
@@ -0,0 +1,23 @@
package com.baiyue.lastleg.dropship.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.lastleg.dropship.dto.DropshipRequests;
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
import com.baiyue.lastleg.security.InternalServiceTokenGuard;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/internal/v1/lastleg/dropship")
public class DropshipOrderController {
@Resource private DropshipOrderService service;
@Resource private InternalServiceTokenGuard tokenGuard;
@PostMapping("/receive")
public ApiResponse<Integer> receive(@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@Valid @RequestBody DropshipRequests.Receive request) {
tokenGuard.verify(token);
return ApiResponse.success(service.receive(request));
}
}
@@ -0,0 +1,15 @@
package com.baiyue.lastleg.dropship.dto;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import java.util.List;
public final class DropshipRequests {
private DropshipRequests() {}
public record Receive(@NotBlank String customerId, @NotNull Long orderId, @NotNull Long warehouseId,
@NotBlank String idempotencyKey, @NotEmpty List<@Valid Pallet> pallets) {}
public record Pallet(@NotBlank String palletNo, @NotEmpty List<@Valid SkuPackages> skuPackages) {}
public record SkuPackages(@NotBlank String skuCode, String productName, @Positive long packageCount,
@Positive long productQuantity,
@NotEmpty List<@NotBlank String> cartonMarks) {}
}
@@ -0,0 +1,23 @@
package com.baiyue.lastleg.dropship.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data @TableName("ll_dropship_carton")
public class DropshipCarton {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private String customerId;
private Long orderId;
private Long profileId;
private Long warehousePalletId;
private String palletNo;
private String skuCode;
private String cartonMark;
private Integer cartonSequence;
private String status;
private String outboundOrderNo;
private Long version;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,24 @@
package com.baiyue.lastleg.dropship.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data @TableName("ll_dropship_carton_ledger")
public class DropshipCartonLedger {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private String customerId;
private Long orderId;
private Long cartonId;
private String cartonMark;
private String skuCode;
private String operationType;
private Long beforePackages;
private Long deltaPackages;
private Long afterPackages;
private String businessOrderNo;
private String idempotencyKey;
private String remark;
private LocalDateTime occurredAt;
private LocalDateTime createdAt;
}
@@ -0,0 +1,22 @@
package com.baiyue.lastleg.dropship.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data @TableName("ll_dropship_order_profile")
public class DropshipOrderProfile {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private String customerId;
private Long orderId;
private Long warehousePalletId;
private String palletNo;
/** Immutable pallet number snapshot supplied by the first-leg handover event. */
private String sourcePalletNo;
private Long inventoryPositionId;
private String status;
private String idempotencyKey;
private Long version;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,19 @@
package com.baiyue.lastleg.dropship.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data @TableName("ll_dropship_sku_inventory")
public class DropshipSkuInventory {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private String customerId;
private Long orderId;
private Long profileId;
private String skuCode;
private String productName;
private Long packageCount;
private Long productQuantity;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,8 @@
package com.baiyue.lastleg.dropship.mapper;
import com.baiyue.lastleg.dropship.entity.DropshipCartonLedger;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.*;
public interface DropshipCartonLedgerMapper extends BaseMapper<DropshipCartonLedger> {
@Select("SELECT * FROM ll_dropship_carton_ledger WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
DropshipCartonLedger findIdempotency(@Param("customerId") String customerId, @Param("key") String key);
}
@@ -0,0 +1,8 @@
package com.baiyue.lastleg.dropship.mapper;
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.*;
public interface DropshipCartonMapper extends BaseMapper<DropshipCarton> {
@Select("SELECT * FROM ll_dropship_carton WHERE customer_id=#{customerId} AND order_id=#{orderId} AND carton_mark=#{cartonMark} LIMIT 1")
DropshipCarton find(@Param("customerId") String customerId, @Param("orderId") Long orderId, @Param("cartonMark") String cartonMark);
}
@@ -0,0 +1,9 @@
package com.baiyue.lastleg.dropship.mapper;
import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.*;
import java.util.List;
public interface DropshipOrderProfileMapper extends BaseMapper<DropshipOrderProfile> {
@Select("SELECT * FROM ll_dropship_order_profile WHERE customer_id=#{customerId} AND order_id=#{orderId} ORDER BY source_pallet_no")
List<DropshipOrderProfile> listOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId);
}
@@ -0,0 +1,6 @@
package com.baiyue.lastleg.dropship.mapper;
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface DropshipSkuInventoryMapper extends BaseMapper<DropshipSkuInventory> {}
@@ -0,0 +1,130 @@
package com.baiyue.lastleg.dropship.service;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.lastleg.dropship.dto.DropshipRequests;
import com.baiyue.lastleg.dropship.entity.*;
import com.baiyue.lastleg.dropship.mapper.*;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Service
public class DropshipOrderService {
@Resource private LastlegOrderMapper orders;
@Resource private DropshipOrderProfileMapper profiles;
@Resource private DropshipSkuInventoryMapper skuInventory;
@Resource private DropshipCartonMapper cartons;
@Resource private DropshipCartonLedgerMapper ledger;
@Resource private WarehouseResourceGateway warehouse;
@Transactional
public int receive(DropshipRequests.Receive request) {
LastlegOrder order = orders.selectById(request.orderId());
if (order == null || !request.customerId().equals(order.getCustomerId())) throw conflict("Tail order not found");
if (!"ONE_CLICK_DROPSHIP".equals(order.getOrderType())) throw conflict("Order is not a one-click dropship order");
long totalPackages = request.pallets().stream().flatMap(value -> value.skuPackages().stream())
.mapToLong(DropshipRequests.SkuPackages::packageCount).sum();
long totalQuantity = request.pallets().stream().flatMap(value -> value.skuPackages().stream())
.mapToLong(DropshipRequests.SkuPackages::productQuantity).sum();
if (order.getPackageCount() != null && order.getPackageCount().longValue() != totalPackages) {
throw conflict("SKU package total does not equal order package count");
}
if (order.getProductQuantity() != null && order.getProductQuantity().longValue() != totalQuantity) {
throw conflict("SKU product quantity total does not equal order product quantity");
}
List<DropshipOrderProfile> existing = profiles.listOrder(request.customerId(), request.orderId());
if (!existing.isEmpty()) {
Set<String> existingPallets = existing.stream().map(DropshipOrderProfile::getSourcePalletNo)
.collect(java.util.stream.Collectors.toSet());
Set<String> requestedPallets = request.pallets().stream().map(value -> value.palletNo().trim())
.collect(java.util.stream.Collectors.toSet());
if (!existingPallets.equals(requestedPallets)) throw conflict("Dropship order pallet snapshot conflicts with existing data");
return Math.toIntExact(totalPackages);
}
validateMarks(request);
LocalDateTime now = LocalDateTime.now();
for (DropshipRequests.Pallet inputPallet : request.pallets()) {
WarehouseResourceGateway.WarehousePalletReference pallet = warehouse.ensureDropshipPallet(
request.customerId(), request.warehouseId(), request.orderId() + ":" + inputPallet.palletNo().trim());
DropshipOrderProfile profile = profile(request, order, inputPallet, pallet, now);
profiles.insert(profile);
int sequence = 0;
for (DropshipRequests.SkuPackages input : inputPallet.skuPackages()) {
DropshipSkuInventory inventory = new DropshipSkuInventory();
inventory.setId(IdWorker.getId()); inventory.setCustomerId(request.customerId()); inventory.setOrderId(order.getId());
inventory.setProfileId(profile.getId()); inventory.setSkuCode(input.skuCode()); inventory.setProductName(input.productName());
inventory.setPackageCount(input.packageCount()); inventory.setProductQuantity(input.productQuantity());
inventory.setCreatedAt(now); inventory.setUpdatedAt(now);
skuInventory.insert(inventory);
for (String mark : input.cartonMarks()) {
DropshipCarton carton = new DropshipCarton();
carton.setId(IdWorker.getId()); carton.setCustomerId(request.customerId()); carton.setOrderId(order.getId());
carton.setProfileId(profile.getId()); carton.setWarehousePalletId(pallet.palletId()); carton.setPalletNo(pallet.palletNo());
carton.setSkuCode(input.skuCode()); carton.setCartonMark(mark.trim()); carton.setCartonSequence(++sequence);
carton.setStatus("AVAILABLE"); carton.setVersion(0L); carton.setCreatedAt(now); carton.setUpdatedAt(now);
cartons.insert(carton);
ledger.insert(stockInFact(request, order, carton, now));
}
}
}
return Math.toIntExact(totalPackages);
}
private void validateMarks(DropshipRequests.Receive request) {
Set<String> marks = new HashSet<>();
Set<String> palletNos = new HashSet<>();
for (DropshipRequests.Pallet pallet : request.pallets()) {
if (!palletNos.add(pallet.palletNo().trim())) throw conflict("Duplicate pallet number: " + pallet.palletNo());
Set<String> skus = new HashSet<>();
for (DropshipRequests.SkuPackages item : pallet.skuPackages()) {
if (item.packageCount() != item.cartonMarks().size()) {
throw conflict("SKU packageCount must equal cartonMarks count: " + item.skuCode());
}
if (!skus.add(item.skuCode().trim())) throw conflict("Duplicate SKU on pallet: " + item.skuCode());
for (String mark : item.cartonMarks()) {
if (!marks.add(mark.trim())) throw conflict("Duplicate carton mark: " + mark);
}
}
}
}
private DropshipOrderProfile profile(DropshipRequests.Receive request, LastlegOrder order,
DropshipRequests.Pallet input,
WarehouseResourceGateway.WarehousePalletReference pallet, LocalDateTime now) {
DropshipOrderProfile profile = new DropshipOrderProfile();
profile.setId(IdWorker.getId()); profile.setCustomerId(request.customerId()); profile.setOrderId(order.getId());
profile.setWarehousePalletId(pallet.palletId()); profile.setPalletNo(pallet.palletNo());
profile.setSourcePalletNo(input.palletNo().trim());
profile.setStatus("RECEIVED"); profile.setVersion(0L);
profile.setIdempotencyKey(request.idempotencyKey() + ":" + input.palletNo().trim());
profile.setCreatedAt(now); profile.setUpdatedAt(now);
return profile;
}
private DropshipCartonLedger stockInFact(DropshipRequests.Receive request, LastlegOrder order,
DropshipCarton carton, LocalDateTime now) {
DropshipCartonLedger fact = new DropshipCartonLedger();
fact.setId(IdWorker.getId()); fact.setCustomerId(request.customerId()); fact.setOrderId(order.getId());
fact.setCartonId(carton.getId()); fact.setCartonMark(carton.getCartonMark()); fact.setSkuCode(carton.getSkuCode());
fact.setOperationType("STOCK_IN"); fact.setBeforePackages(0L); fact.setDeltaPackages(1L); fact.setAfterPackages(1L);
fact.setBusinessOrderNo(order.getOrderNo());
fact.setIdempotencyKey(request.idempotencyKey() + ":" + carton.getCartonMark());
fact.setOccurredAt(now); fact.setCreatedAt(now);
return fact;
}
private static ResponseStatusException conflict(String message) {
return new ResponseStatusException(HttpStatus.CONFLICT, message);
}
}
@@ -1,3 +1,3 @@
package com.baiyue.lastleg.entity; package com.baiyue.lastleg.entity;
import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime;
@Data @TableName("ll_loading_event_inbox") public class LastlegEventInbox { @TableId private String eventId; private String tenantId; private String eventType; private Integer eventVersion; private String payloadHash; private String payloadJson; private String status; private LocalDateTime receivedAt; private LocalDateTime processedAt; private String lastError; } @Data @TableName("ll_loading_event_inbox") public class LastlegEventInbox { @TableId private String eventId; private String customerId; private String eventType; private Integer eventVersion; private String payloadHash; private String payloadJson; private String status; private LocalDateTime receivedAt; private LocalDateTime processedAt; private String lastError; }
@@ -13,7 +13,7 @@ public class LastlegOrder {
@TableId(type = IdType.ASSIGN_ID) @TableId(type = IdType.ASSIGN_ID)
private Long id; private Long id;
private String orderNo; private String orderNo;
private String tenantId; private String customerId;
private String orderType; private String orderType;
private String inboundCode; private String inboundCode;
private String sourceRegion; private String sourceRegion;
@@ -28,15 +28,15 @@ public class InboundPlanController {
@Operation(summary = "Convert the entire container to one inbound plan") @Operation(summary = "Convert the entire container to one inbound plan")
public ApiResponse<InboundPlanResponse> convert(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<InboundPlanResponse> convert(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody ConvertInboundPlanRequest request) { @Valid @RequestBody ConvertInboundPlanRequest request) {
return ApiResponse.success(service.convert(scope(user, request.tenantId()), request.orderId())); return ApiResponse.success(service.convert(scope(user, request.customerId()), request.orderId()));
} }
@GetMapping("/{planNo}") @GetMapping("/{planNo}")
@Operation(summary = "Get an inbound plan with lines, pallets and allocations") @Operation(summary = "Get an inbound plan with lines, pallets and allocations")
public ApiResponse<InboundPlanResponse> detail(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<InboundPlanResponse> detail(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String planNo, @PathVariable String planNo,
@RequestParam(required = false) String tenantId) { @RequestParam(required = false) String customerId) {
return ApiResponse.success(service.detailByPlanNo(scope(user, tenantId), planNo)); return ApiResponse.success(service.detailByPlanNo(scope(user, customerId), planNo));
} }
@PostMapping("/fba-appointments/batch") @PostMapping("/fba-appointments/batch")
@@ -44,9 +44,9 @@ public class InboundPlanController {
public ApiResponse<FbaAppointmentResponse> batchFbaAppointment( public ApiResponse<FbaAppointmentResponse> batchFbaAppointment(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody BatchFbaAppointmentRequest request) { @Valid @RequestBody BatchFbaAppointmentRequest request) {
String tenantId = scope(user, request.tenantId()); String customerId = scope(user, request.customerId());
String operatorId = user.userId() == null ? user.username() : String.valueOf(user.userId()); String operatorId = user.userId() == null ? user.username() : String.valueOf(user.userId());
return ApiResponse.success(appointmentService.batchCreate(tenantId, operatorId, request)); return ApiResponse.success(appointmentService.batchCreate(customerId, operatorId, request));
} }
@GetMapping("/fba-appointments/{appointmentNo}") @GetMapping("/fba-appointments/{appointmentNo}")
@@ -54,20 +54,20 @@ public class InboundPlanController {
public ApiResponse<FbaAppointmentResponse> fbaAppointmentDetail( public ApiResponse<FbaAppointmentResponse> fbaAppointmentDetail(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String appointmentNo, @PathVariable String appointmentNo,
@RequestParam(required = false) String tenantId) { @RequestParam(required = false) String customerId) {
return ApiResponse.success(appointmentService.detail(scope(user, tenantId), appointmentNo)); return ApiResponse.success(appointmentService.detail(scope(user, customerId), appointmentNo));
} }
private String scope(AuthenticatedUser user, String requestedTenant) { private String scope(AuthenticatedUser user, String requestedCustomer) {
if (user == null || user.tenantId() == null) { if (user == null || user.tenantId() == null) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
} }
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) { if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
if (requestedTenant == null || requestedTenant.isBlank() || "0".equals(requestedTenant)) { if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Platform request must specify target tenant"); "Platform request must specify target tenant");
} }
return requestedTenant; return requestedCustomer;
} }
return String.valueOf(user.tenantId()); return String.valueOf(user.tenantId());
} }
@@ -9,7 +9,7 @@ import java.time.LocalDateTime;
import java.util.List; import java.util.List;
public record BatchFbaAppointmentRequest( public record BatchFbaAppointmentRequest(
String tenantId, String customerId,
@NotEmpty @Size(max = 500) List<@NotNull Long> lineIds, @NotEmpty @Size(max = 500) List<@NotNull Long> lineIds,
@NotBlank @Size(max = 64) String appointmentNo, @NotBlank @Size(max = 64) String appointmentNo,
@NotNull LocalDateTime appointmentAt, @NotNull LocalDateTime appointmentAt,
@@ -4,6 +4,6 @@ import jakarta.validation.constraints.NotNull;
public record ConvertInboundPlanRequest( public record ConvertInboundPlanRequest(
@NotNull Long orderId, @NotNull Long orderId,
String tenantId String customerId
) { ) {
} }
@@ -5,7 +5,7 @@ import java.util.List;
public record FbaAppointmentResponse( public record FbaAppointmentResponse(
Long id, Long id,
String tenantId, String customerId,
String appointmentNo, String appointmentNo,
LocalDateTime appointmentAt, LocalDateTime appointmentAt,
String appointmentStatus, String appointmentStatus,
@@ -7,7 +7,7 @@ import java.util.List;
public record InboundPlanResponse( public record InboundPlanResponse(
Long id, Long id,
String planNo, String planNo,
String tenantId, String customerId,
String containerNo, String containerNo,
String status, String status,
int lineCount, int lineCount,
@@ -12,7 +12,7 @@ import java.time.LocalDateTime;
public class InboundFbaAppointment { public class InboundFbaAppointment {
@TableId(type = IdType.INPUT) @TableId(type = IdType.INPUT)
private Long id; private Long id;
private String tenantId; private String customerId;
private String appointmentNo; private String appointmentNo;
private LocalDateTime appointmentAt; private LocalDateTime appointmentAt;
private String appointmentStatus; private String appointmentStatus;
@@ -14,7 +14,7 @@ public class InboundPlan {
@TableId(type = IdType.INPUT) @TableId(type = IdType.INPUT)
private Long id; private Long id;
private String planNo; private String planNo;
private String tenantId; private String customerId;
private String containerNo; private String containerNo;
private String status; private String status;
private Integer lineCount; private Integer lineCount;
@@ -13,10 +13,10 @@ public interface InboundFbaAppointmentQueryMapper {
"SELECT l.id AS lineId,l.plan_id AS planId,l.order_id AS orderId,l.inbound_code AS inboundCode," + "SELECT l.id AS lineId,l.plan_id AS planId,l.order_id AS orderId,l.inbound_code AS inboundCode," +
"l.fba_warehouse_code AS fbaWarehouseCode " + "l.fba_warehouse_code AS fbaWarehouseCode " +
"FROM ll_inbound_plan_line l JOIN ll_inbound_plan p ON p.id=l.plan_id " + "FROM ll_inbound_plan_line l JOIN ll_inbound_plan p ON p.id=l.plan_id " +
"WHERE p.tenant_id=#{tenantId} AND l.id IN " + "WHERE p.customer_id=#{customerId} AND l.id IN " +
"<foreach collection='lineIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " + "<foreach collection='lineIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
"ORDER BY l.id FOR UPDATE" + "ORDER BY l.id FOR UPDATE" +
"</script>") "</script>")
List<FbaAppointmentLineSnapshot> lockLines(@Param("tenantId") String tenantId, List<FbaAppointmentLineSnapshot> lockLines(@Param("customerId") String customerId,
@Param("lineIds") List<Long> lineIds); @Param("lineIds") List<Long> lineIds);
} }
@@ -8,6 +8,6 @@ import org.apache.ibatis.annotations.Select;
@Mapper @Mapper
public interface InboundPlanMapper extends BaseMapper<InboundPlan> { public interface InboundPlanMapper extends BaseMapper<InboundPlan> {
@Select("SELECT * FROM ll_inbound_plan WHERE tenant_id=#{tenantId} AND container_no=#{containerNo} LIMIT 1") @Select("SELECT * FROM ll_inbound_plan WHERE customer_id=#{customerId} AND container_no=#{containerNo} LIMIT 1")
InboundPlan findByContainer(@Param("tenantId") String tenantId, @Param("containerNo") String containerNo); InboundPlan findByContainer(@Param("customerId") String customerId, @Param("containerNo") String containerNo);
} }
@@ -12,10 +12,10 @@ import java.util.List;
@Mapper @Mapper
public interface InboundPlanOrderMapper { public interface InboundPlanOrderMapper {
@Select("SELECT container_no FROM ll_order WHERE tenant_id=#{tenantId} AND id=#{orderId} LIMIT 1") @Select("SELECT container_no FROM ll_order WHERE customer_id=#{customerId} AND id=#{orderId} LIMIT 1")
String findContainerNo(@Param("tenantId") String tenantId, @Param("orderId") Long orderId); String findContainerNo(@Param("customerId") String customerId, @Param("orderId") Long orderId);
@Select("SELECT o.id AS orderId,o.tenant_id AS tenantId,o.container_no AS containerNo," + @Select("SELECT o.id AS orderId,o.customer_id AS customerId,o.container_no AS containerNo," +
"o.inbound_code AS inboundCode,o.current_status AS currentStatus,o.inbound_order_no AS inboundOrderNo," + "o.inbound_code AS inboundCode,o.current_status AS currentStatus,o.inbound_order_no AS inboundOrderNo," +
"COALESCE(o.volume_cbm,0) AS volumeCbm,COALESCE(o.package_count,0) AS packageCount," + "COALESCE(o.volume_cbm,0) AS volumeCbm,COALESCE(o.package_count,0) AS packageCount," +
"NULLIF(o.fba_warehouse_code,'') AS fbaWarehouseCode," + "NULLIF(o.fba_warehouse_code,'') AS fbaWarehouseCode," +
@@ -29,17 +29,17 @@ public interface InboundPlanOrderMapper {
"<> COALESCE(NULLIF(o.transfer_warehouse_id,''),NULLIF(o.transfer_warehouse_code,'')) THEN 1 ELSE 0 END) " + "<> COALESCE(NULLIF(o.transfer_warehouse_id,''),NULLIF(o.transfer_warehouse_code,'')) THEN 1 ELSE 0 END) " +
"AS destinationMismatchCount " + "AS destinationMismatchCount " +
"FROM ll_order o LEFT JOIN ll_order_detail d ON d.order_id=o.id " + "FROM ll_order o LEFT JOIN ll_order_detail d ON d.order_id=o.id " +
"WHERE o.tenant_id=#{tenantId} AND o.container_no=#{containerNo} " + "WHERE o.customer_id=#{customerId} AND o.container_no=#{containerNo} " +
"GROUP BY o.id,o.tenant_id,o.container_no,o.inbound_code,o.current_status,o.inbound_order_no,o.volume_cbm,o.package_count,o.fba_warehouse_code," + "GROUP BY o.id,o.customer_id,o.container_no,o.inbound_code,o.current_status,o.inbound_order_no,o.volume_cbm,o.package_count,o.fba_warehouse_code," +
"o.transfer_warehouse_id,o.transfer_warehouse_code,o.transfer_warehouse_name " + "o.transfer_warehouse_id,o.transfer_warehouse_code,o.transfer_warehouse_name " +
"ORDER BY o.id FOR UPDATE") "ORDER BY o.id FOR UPDATE")
List<SourceOrderSnapshot> lockContainerOrders(@Param("tenantId") String tenantId, List<SourceOrderSnapshot> lockContainerOrders(@Param("customerId") String customerId,
@Param("containerNo") String containerNo); @Param("containerNo") String containerNo);
@Update("UPDATE ll_order SET inbound_order_no=#{planNo},current_status='INBOUND_PLANNED'," + @Update("UPDATE ll_order SET inbound_order_no=#{planNo},current_status='INBOUND_PLANNED'," +
"pallet_merge_flag='MERGED',pallet_count=#{palletCount},updated_at=#{updatedAt} " + "pallet_merge_flag='MERGED',pallet_count=#{palletCount},updated_at=#{updatedAt} " +
"WHERE tenant_id=#{tenantId} AND id=#{orderId}") "WHERE customer_id=#{customerId} AND id=#{orderId}")
int markPlanned(@Param("tenantId") String tenantId, @Param("orderId") Long orderId, int markPlanned(@Param("customerId") String customerId, @Param("orderId") Long orderId,
@Param("planNo") String planNo, @Param("palletCount") int palletCount, @Param("planNo") String planNo, @Param("palletCount") int palletCount,
@Param("updatedAt") LocalDateTime updatedAt); @Param("updatedAt") LocalDateTime updatedAt);
@@ -4,7 +4,7 @@ import java.math.BigDecimal;
public record SourceOrderSnapshot( public record SourceOrderSnapshot(
Long orderId, Long orderId,
String tenantId, String customerId,
String containerNo, String containerNo,
String inboundCode, String inboundCode,
String currentStatus, String currentStatus,
@@ -18,12 +18,12 @@ public record SourceOrderSnapshot(
int destinationCount, int destinationCount,
int destinationMismatchCount int destinationMismatchCount
) { ) {
public SourceOrderSnapshot(Long orderId, String tenantId, String containerNo, String inboundCode, public SourceOrderSnapshot(Long orderId, String customerId, String containerNo, String inboundCode,
String currentStatus, String inboundOrderNo, BigDecimal volumeCbm, String currentStatus, String inboundOrderNo, BigDecimal volumeCbm,
int packageCount, String destinationWarehouseId, int packageCount, String destinationWarehouseId,
String destinationWarehouseCode, String destinationWarehouseName, String destinationWarehouseCode, String destinationWarehouseName,
int destinationCount, int destinationMismatchCount) { int destinationCount, int destinationMismatchCount) {
this(orderId, tenantId, containerNo, inboundCode, currentStatus, inboundOrderNo, volumeCbm, this(orderId, customerId, containerNo, inboundCode, currentStatus, inboundOrderNo, volumeCbm,
packageCount, null, destinationWarehouseId, destinationWarehouseCode, packageCount, null, destinationWarehouseId, destinationWarehouseCode,
destinationWarehouseName, destinationCount, destinationMismatchCount); destinationWarehouseName, destinationCount, destinationMismatchCount);
} }
@@ -24,19 +24,19 @@ public class InboundPalletAssignmentController {
@PostMapping("/{planPalletId}/assign") @PostMapping("/{planPalletId}/assign")
public ApiResponse<Void> assign(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<Void> assign(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String planNo, @PathVariable long planPalletId, @PathVariable String planNo, @PathVariable long planPalletId,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid @RequestBody PdaRequests.Assign request) { @Valid @RequestBody PdaRequests.Assign request) {
service.assign(scope(user, tenantId), planNo, planPalletId, request); service.assign(scope(user, customerId), planNo, planPalletId, request);
return ApiResponse.success(null); return ApiResponse.success(null);
} }
private String scope(AuthenticatedUser user, String requestedTenant) { private String scope(AuthenticatedUser user, String requestedCustomer) {
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) { if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
if (requestedTenant == null || requestedTenant.isBlank() || "0".equals(requestedTenant)) { if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target tenant"); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target tenant");
} }
return requestedTenant; return requestedCustomer;
} }
return String.valueOf(user.tenantId()); return String.valueOf(user.tenantId());
} }
@@ -28,66 +28,66 @@ public class InboundPdaController {
@PostMapping("/push") @PostMapping("/push")
public ApiResponse<PdaResponses.Task> push(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<PdaResponses.Task> push(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String planNo, @PathVariable String planNo,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid @RequestBody PdaRequests.Push request) { @Valid @RequestBody PdaRequests.Push request) {
return ApiResponse.success(service.push(scope(user, tenantId), planNo, request)); return ApiResponse.success(service.push(scope(user, customerId), planNo, request));
} }
@GetMapping @GetMapping
public ApiResponse<PdaResponses.Task> task(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<PdaResponses.Task> task(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String planNo, @PathVariable String planNo,
@RequestParam(required = false) String tenantId) { @RequestParam(required = false) String customerId) {
return ApiResponse.success(service.task(scope(user, tenantId), planNo)); return ApiResponse.success(service.task(scope(user, customerId), planNo));
} }
@PostMapping("/allocations/{allocationId}/count") @PostMapping("/allocations/{allocationId}/count")
public ApiResponse<PdaResponses.Operation> count(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<PdaResponses.Operation> count(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String planNo, @PathVariable String planNo,
@PathVariable long allocationId, @PathVariable long allocationId,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid @RequestBody PdaRequests.PackageOperation request) { @Valid @RequestBody PdaRequests.PackageOperation request) {
return ApiResponse.success(service.count(scope(user, tenantId), planNo, allocationId, request)); return ApiResponse.success(service.count(scope(user, customerId), planNo, allocationId, request));
} }
@PostMapping("/allocations/{allocationId}/pick") @PostMapping("/allocations/{allocationId}/pick")
public ApiResponse<PdaResponses.Operation> pick(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<PdaResponses.Operation> pick(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String planNo, @PathVariable String planNo,
@PathVariable long allocationId, @PathVariable long allocationId,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid @RequestBody PdaRequests.PackageOperation request) { @Valid @RequestBody PdaRequests.PackageOperation request) {
return ApiResponse.success(service.pick(scope(user, tenantId), planNo, allocationId, request)); return ApiResponse.success(service.pick(scope(user, customerId), planNo, allocationId, request));
} }
@GetMapping("/allocations/{allocationId}/merge-candidates") @GetMapping("/allocations/{allocationId}/merge-candidates")
public ApiResponse<List<PdaResponses.MergeCandidate>> candidates( public ApiResponse<List<PdaResponses.MergeCandidate>> candidates(
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String planNo, @AuthenticationPrincipal AuthenticatedUser user, @PathVariable String planNo,
@PathVariable long allocationId, @RequestParam(required = false) String tenantId) { @PathVariable long allocationId, @RequestParam(required = false) String customerId) {
return ApiResponse.success(service.mergeCandidates(scope(user, tenantId), planNo, allocationId)); return ApiResponse.success(service.mergeCandidates(scope(user, customerId), planNo, allocationId));
} }
@PostMapping("/allocations/{allocationId}/merge") @PostMapping("/allocations/{allocationId}/merge")
public ApiResponse<PdaResponses.Merge> merge(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<PdaResponses.Merge> merge(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String planNo, @PathVariable String planNo,
@PathVariable long allocationId, @PathVariable long allocationId,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid @RequestBody PdaRequests.ConfirmMerge request) { @Valid @RequestBody PdaRequests.ConfirmMerge request) {
return ApiResponse.success(service.merge(scope(user, tenantId), planNo, allocationId, request)); return ApiResponse.success(service.merge(scope(user, customerId), planNo, allocationId, request));
} }
@PostMapping("/complete") @PostMapping("/complete")
public ApiResponse<PdaResponses.Task> complete(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<PdaResponses.Task> complete(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String planNo, @PathVariable String planNo,
@RequestParam(required = false) String tenantId) { @RequestParam(required = false) String customerId) {
return ApiResponse.success(service.complete(scope(user, tenantId), planNo)); return ApiResponse.success(service.complete(scope(user, customerId), planNo));
} }
private String scope(AuthenticatedUser user, String requestedTenant) { private String scope(AuthenticatedUser user, String requestedCustomer) {
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) { if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
if (requestedTenant == null || requestedTenant.isBlank() || "0".equals(requestedTenant)) { if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target tenant"); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target tenant");
} }
return requestedTenant; return requestedCustomer;
} }
return String.valueOf(user.tenantId()); return String.valueOf(user.tenantId());
} }
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
public class InboundPalletMerge { public class InboundPalletMerge {
@TableId(type = IdType.INPUT) @TableId(type = IdType.INPUT)
private Long id; private Long id;
private String tenantId; private String customerId;
private Long taskId; private Long taskId;
private String mergeGroupNo; private String mergeGroupNo;
private Long sourcePalletId; private Long sourcePalletId;
@@ -12,7 +12,7 @@ import java.time.LocalDateTime;
public class InboundPdaOperation { public class InboundPdaOperation {
@TableId(type = IdType.INPUT) @TableId(type = IdType.INPUT)
private Long id; private Long id;
private String tenantId; private String customerId;
private Long taskId; private Long taskId;
private Long allocationId; private Long allocationId;
private String action; private String action;
@@ -13,7 +13,7 @@ public class InboundPdaTask {
@TableId(type = IdType.INPUT) @TableId(type = IdType.INPUT)
private Long id; private Long id;
private Long planId; private Long planId;
private String tenantId; private String customerId;
private String taskNo; private String taskNo;
private String status; private String status;
private String operatorId; private String operatorId;
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
public class InboundStockReceipt { public class InboundStockReceipt {
@TableId(type = IdType.ASSIGN_ID) @TableId(type = IdType.ASSIGN_ID)
private Long id; private Long id;
private String tenantId; private String customerId;
private Long planId; private Long planId;
private Long taskId; private Long taskId;
private Long allocationId; private Long allocationId;
@@ -27,12 +27,24 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway {
} }
@Override @Override
public long reserve(String tenantId, long warehouseId, long palletId, long locationId, public WarehousePalletReference ensureDropshipPallet(String customerId, long warehouseId, String businessId) {
WarehousePalletResponse response = client.post().uri("/internal/v1/warehouse-pallets/ensure-dropship")
.header("X-Internal-Service-Token", internalToken)
.body(Map.of("customerId", Long.parseLong(customerId), "warehouseId", warehouseId, "businessId", businessId))
.retrieve().onStatus(HttpStatusCode::isError, (request, result) -> {
throw new IllegalStateException("Warehouse rejected dropship pallet: HTTP " + result.getStatusCode());
}).body(WarehousePalletResponse.class);
if (response == null || response.data() == null) throw new IllegalStateException("Warehouse returned an empty pallet response");
return new WarehousePalletReference(response.data().id(), response.data().palletCode(), response.data().locationId());
}
@Override
public long reserve(String customerId, long warehouseId, long palletId, long locationId,
String businessType, String businessId, String reservationKey) { String businessType, String businessId, String reservationKey) {
ReserveEnvelope response = client.post() ReserveEnvelope response = client.post()
.uri("/internal/v1/warehouse-resource-locks") .uri("/internal/v1/warehouse-resource-locks")
.header("X-Internal-Service-Token", internalToken) .header("X-Internal-Service-Token", internalToken)
.body(Map.of("tenantId", Long.parseLong(tenantId), "warehouseId", warehouseId, .body(Map.of("customerId", Long.parseLong(customerId), "warehouseId", warehouseId,
"palletId", palletId, "locationId", locationId, "businessType", businessType, "palletId", palletId, "locationId", locationId, "businessType", businessType,
"businessId", businessId, "reservationKey", reservationKey)) "businessId", businessId, "reservationKey", reservationKey))
.retrieve() .retrieve()
@@ -45,11 +57,11 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway {
} }
@Override @Override
public void scrap(String tenantId, long resourceLockId) { public void scrap(String customerId, long resourceLockId) {
client.post() client.post()
.uri("/internal/v1/warehouse-resource-locks/{lockId}/scrap", resourceLockId) .uri("/internal/v1/warehouse-resource-locks/{lockId}/scrap", resourceLockId)
.header("X-Internal-Service-Token", internalToken) .header("X-Internal-Service-Token", internalToken)
.body(Map.of("tenantId", Long.parseLong(tenantId))) .body(Map.of("customerId", Long.parseLong(customerId)))
.retrieve() .retrieve()
.onStatus(HttpStatusCode::isError, (request, response) -> { .onStatus(HttpStatusCode::isError, (request, response) -> {
throw new IllegalStateException("Warehouse rejected pallet scrap: HTTP " + response.getStatusCode()); throw new IllegalStateException("Warehouse rejected pallet scrap: HTTP " + response.getStatusCode());
@@ -58,11 +70,11 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway {
} }
@Override @Override
public void scrapPallet(String tenantId, long palletId) { public void scrapPallet(String customerId, long palletId) {
client.post() client.post()
.uri("/internal/v1/warehouse-resource-locks/pallets/{palletId}/scrap", palletId) .uri("/internal/v1/warehouse-resource-locks/pallets/{palletId}/scrap", palletId)
.header("X-Internal-Service-Token", internalToken) .header("X-Internal-Service-Token", internalToken)
.body(Map.of("tenantId", Long.parseLong(tenantId))) .body(Map.of("customerId", Long.parseLong(customerId)))
.retrieve() .retrieve()
.onStatus(HttpStatusCode::isError, (request, response) -> { .onStatus(HttpStatusCode::isError, (request, response) -> {
throw new IllegalStateException("Warehouse rejected pallet scrap: HTTP " + response.getStatusCode()); throw new IllegalStateException("Warehouse rejected pallet scrap: HTTP " + response.getStatusCode());
@@ -70,6 +82,8 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway {
.toBodilessEntity(); .toBodilessEntity();
} }
private record WarehousePalletResponse(WarehousePalletData data) {}
private record WarehousePalletData(long id, String palletCode, long locationId) {}
private record ReserveEnvelope(ReserveData data) {} private record ReserveEnvelope(ReserveData data) {}
private record ReserveData(long lockId) {} private record ReserveData(long lockId) {}
} }
@@ -1,8 +1,11 @@
package com.baiyue.lastleg.inboundplan.pda.gateway; package com.baiyue.lastleg.inboundplan.pda.gateway;
public interface WarehouseResourceGateway { public interface WarehouseResourceGateway {
long reserve(String tenantId, long warehouseId, long palletId, long locationId, WarehousePalletReference ensureDropshipPallet(String customerId, long warehouseId, String businessId);
long reserve(String customerId, long warehouseId, long palletId, long locationId,
String businessType, String businessId, String reservationKey); String businessType, String businessId, String reservationKey);
void scrap(String tenantId, long resourceLockId); void scrap(String customerId, long resourceLockId);
void scrapPallet(String tenantId, long palletId); void scrapPallet(String customerId, long palletId);
record WarehousePalletReference(long palletId, String palletNo, long locationId) {}
} }
@@ -8,6 +8,6 @@ import org.apache.ibatis.annotations.Select;
@Mapper @Mapper
public interface InboundPdaOperationMapper extends BaseMapper<InboundPdaOperation> { public interface InboundPdaOperationMapper extends BaseMapper<InboundPdaOperation> {
@Select("SELECT * FROM ll_inbound_pda_operation WHERE tenant_id=#{tenantId} AND idempotency_key=#{key} LIMIT 1") @Select("SELECT * FROM ll_inbound_pda_operation WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
InboundPdaOperation findByKey(@Param("tenantId") String tenantId, @Param("key") String key); InboundPdaOperation findByKey(@Param("customerId") String customerId, @Param("key") String key);
} }
@@ -8,9 +8,9 @@ import org.apache.ibatis.annotations.Select;
@Mapper @Mapper
public interface InboundPdaTaskMapper extends BaseMapper<InboundPdaTask> { public interface InboundPdaTaskMapper extends BaseMapper<InboundPdaTask> {
@Select("SELECT * FROM ll_inbound_pda_task WHERE tenant_id=#{tenantId} AND plan_id=#{planId} LIMIT 1 FOR UPDATE") @Select("SELECT * FROM ll_inbound_pda_task WHERE customer_id=#{customerId} AND plan_id=#{planId} LIMIT 1 FOR UPDATE")
InboundPdaTask lockByPlan(@Param("tenantId") String tenantId, @Param("planId") Long planId); InboundPdaTask lockByPlan(@Param("customerId") String customerId, @Param("planId") Long planId);
@Select("SELECT * FROM ll_inbound_pda_task WHERE tenant_id=#{tenantId} AND plan_id=#{planId} LIMIT 1") @Select("SELECT * FROM ll_inbound_pda_task WHERE customer_id=#{customerId} AND plan_id=#{planId} LIMIT 1")
InboundPdaTask findByPlan(@Param("tenantId") String tenantId, @Param("planId") Long planId); InboundPdaTask findByPlan(@Param("customerId") String customerId, @Param("planId") Long planId);
} }
@@ -8,7 +8,7 @@ import org.apache.ibatis.annotations.Select;
import java.util.List; import java.util.List;
public interface InboundStockReceiptMapper extends BaseMapper<InboundStockReceipt> { public interface InboundStockReceiptMapper extends BaseMapper<InboundStockReceipt> {
@Select("SELECT * FROM ll_inbound_stock_receipt WHERE tenant_id=#{tenantId} AND task_id=#{taskId} " + @Select("SELECT * FROM ll_inbound_stock_receipt WHERE customer_id=#{customerId} AND task_id=#{taskId} " +
"ORDER BY allocation_id") "ORDER BY allocation_id")
List<InboundStockReceipt> listTask(@Param("tenantId") String tenantId, @Param("taskId") Long taskId); List<InboundStockReceipt> listTask(@Param("customerId") String customerId, @Param("taskId") Long taskId);
} }
@@ -30,14 +30,14 @@ public class PdaWorkflowRepository {
@Resource private InboundPdaOperationMapper operations; @Resource private InboundPdaOperationMapper operations;
@Resource private InboundPalletMergeMapper merges; @Resource private InboundPalletMergeMapper merges;
public InboundPlan lockPlan(String tenantId, String planNo) { public InboundPlan lockPlan(String customerId, String planNo) {
return plans.selectOne(new LambdaQueryWrapper<InboundPlan>() return plans.selectOne(new LambdaQueryWrapper<InboundPlan>()
.eq(InboundPlan::getTenantId, tenantId).eq(InboundPlan::getPlanNo, planNo).last("FOR UPDATE")); .eq(InboundPlan::getCustomerId, customerId).eq(InboundPlan::getPlanNo, planNo).last("FOR UPDATE"));
} }
public InboundPdaTask lockTask(String tenantId, Long planId) { return tasks.lockByPlan(tenantId, planId); } public InboundPdaTask lockTask(String customerId, Long planId) { return tasks.lockByPlan(customerId, planId); }
public InboundPdaTask task(String tenantId, Long planId) { return tasks.findByPlan(tenantId, planId); } public InboundPdaTask task(String customerId, Long planId) { return tasks.findByPlan(customerId, planId); }
public InboundPdaOperation operation(String tenantId, String key) { return operations.findByKey(tenantId, key); } public InboundPdaOperation operation(String customerId, String key) { return operations.findByKey(customerId, key); }
public InboundPlanPalletAllocation lockAllocation(Long planId, Long allocationId) { public InboundPlanPalletAllocation lockAllocation(Long planId, Long allocationId) {
return allocations.selectOne(new LambdaQueryWrapper<InboundPlanPalletAllocation>() return allocations.selectOne(new LambdaQueryWrapper<InboundPlanPalletAllocation>()
@@ -55,22 +55,22 @@ public class InboundPdaWorkflowService {
} }
@Transactional @Transactional
public PdaResponses.Task push(String tenantId, String planNo, PdaRequests.Push request) { public PdaResponses.Task push(String customerId, String planNo, PdaRequests.Push request) {
InboundPlan plan = requirePlan(tenantId, planNo); InboundPlan plan = requirePlan(customerId, planNo);
InboundPdaOperation replay = replay(tenantId, "PUSH", request.idempotencyKey(), request); InboundPdaOperation replay = replay(customerId, "PUSH", request.idempotencyKey(), request);
if (replay != null) return read(replay, PdaResponses.Task.class); if (replay != null) return read(replay, PdaResponses.Task.class);
List<InboundPlanPallet> pallets = repository.pallets(plan.getId()); List<InboundPlanPallet> pallets = repository.pallets(plan.getId());
if (pallets.isEmpty() || pallets.stream().anyMatch(this::isNotAssigned)) { if (pallets.isEmpty() || pallets.stream().anyMatch(this::isNotAssigned)) {
throw conflict("Every planned pallet must have an assigned and locked warehouse pallet/location"); throw conflict("Every planned pallet must have an assigned and locked warehouse pallet/location");
} }
InboundPdaTask task = repository.lockTask(tenantId, plan.getId()); InboundPdaTask task = repository.lockTask(customerId, plan.getId());
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
if (task == null) { if (task == null) {
task = new InboundPdaTask(); task = new InboundPdaTask();
task.setId(IdWorker.getId()); task.setId(IdWorker.getId());
task.setPlanId(plan.getId()); task.setPlanId(plan.getId());
task.setTenantId(tenantId); task.setCustomerId(customerId);
task.setTaskNo("PDA" + task.getId()); task.setTaskNo("PDA" + task.getId());
task.setStatus("PDA_PENDING"); task.setStatus("PDA_PENDING");
task.setOperatorId(request.operatorId()); task.setOperatorId(request.operatorId());
@@ -85,13 +85,13 @@ public class InboundPdaWorkflowService {
plan.setUpdatedAt(now); plan.setUpdatedAt(now);
repository.update(plan); repository.update(plan);
PdaResponses.Task result = task(task, planNo); PdaResponses.Task result = task(task, planNo);
saveOperation(tenantId, task.getId(), null, "PUSH", request.idempotencyKey(), request, result); saveOperation(customerId, task.getId(), null, "PUSH", request.idempotencyKey(), request, result);
return result; return result;
} }
@Transactional @Transactional
public void assign(String tenantId, String planNo, long planPalletId, PdaRequests.Assign request) { public void assign(String customerId, String planNo, long planPalletId, PdaRequests.Assign request) {
InboundPlan plan = requirePlan(tenantId, planNo); InboundPlan plan = requirePlan(customerId, planNo);
InboundPlanPallet pallet = requirePallet(plan.getId(), planPalletId); InboundPlanPallet pallet = requirePallet(plan.getId(), planPalletId);
validateDestinationWarehouse(pallet, request.warehouseId()); validateDestinationWarehouse(pallet, request.warehouseId());
if (pallet.getWarehouseResourceLockId() != null) { if (pallet.getWarehouseResourceLockId() != null) {
@@ -100,7 +100,7 @@ public class InboundPdaWorkflowService {
throw conflict("Planned pallet is already assigned to another warehouse resource"); throw conflict("Planned pallet is already assigned to another warehouse resource");
} }
String businessId = planNo + ":" + planPalletId; String businessId = planNo + ":" + planPalletId;
long lockId = warehouse.reserve(tenantId, request.warehouseId(), request.palletId(), request.locationId(), long lockId = warehouse.reserve(customerId, request.warehouseId(), request.palletId(), request.locationId(),
"INBOUND_PLAN_PALLET", businessId, request.idempotencyKey()); "INBOUND_PLAN_PALLET", businessId, request.idempotencyKey());
pallet.setWarehousePalletId(request.palletId()); pallet.setWarehousePalletId(request.palletId());
pallet.setWarehouseLocationId(request.locationId()); pallet.setWarehouseLocationId(request.locationId());
@@ -117,20 +117,20 @@ public class InboundPdaWorkflowService {
} }
@Transactional @Transactional
public PdaResponses.Task task(String tenantId, String planNo) { public PdaResponses.Task task(String customerId, String planNo) {
InboundPlan plan = requirePlan(tenantId, planNo); InboundPlan plan = requirePlan(customerId, planNo);
InboundPdaTask task = repository.task(tenantId, plan.getId()); InboundPdaTask task = repository.task(customerId, plan.getId());
if (task == null) throw notFound("PDA task not found"); if (task == null) throw notFound("PDA task not found");
return task(task, planNo); return task(task, planNo);
} }
@Transactional @Transactional
public PdaResponses.Operation count(String tenantId, String planNo, long allocationId, public PdaResponses.Operation count(String customerId, String planNo, long allocationId,
PdaRequests.PackageOperation request) { PdaRequests.PackageOperation request) {
InboundPlan plan = requirePlan(tenantId, planNo); InboundPlan plan = requirePlan(customerId, planNo);
InboundPdaOperation replay = replay(tenantId, "COUNT", request.idempotencyKey(), request); InboundPdaOperation replay = replay(customerId, "COUNT", request.idempotencyKey(), request);
if (replay != null) return read(replay, PdaResponses.Operation.class); if (replay != null) return read(replay, PdaResponses.Operation.class);
InboundPdaTask task = requireActiveTask(tenantId, plan); InboundPdaTask task = requireActiveTask(customerId, plan);
InboundPlanPalletAllocation allocation = requireAllocation(plan.getId(), allocationId); InboundPlanPalletAllocation allocation = requireAllocation(plan.getId(), allocationId);
int picked = value(allocation.getPickedPackageCount()); int picked = value(allocation.getPickedPackageCount());
if (request.packageCount() < picked) { if (request.packageCount() < picked) {
@@ -147,17 +147,17 @@ public class InboundPdaWorkflowService {
startPicking(task, plan, request.operatorId(), request.operatorName()); startPicking(task, plan, request.operatorId(), request.operatorName());
InboundPlanLine line = requireLine(plan.getId(), allocation.getLineId()); InboundPlanLine line = requireLine(plan.getId(), allocation.getLineId());
PdaResponses.Operation result = operation("COUNTED", allocation, line, List.of(), null, null, null); PdaResponses.Operation result = operation("COUNTED", allocation, line, List.of(), null, null, null);
saveOperation(tenantId, task.getId(), allocationId, "COUNT", request.idempotencyKey(), request, result); saveOperation(customerId, task.getId(), allocationId, "COUNT", request.idempotencyKey(), request, result);
return result; return result;
} }
@Transactional @Transactional
public PdaResponses.Operation pick(String tenantId, String planNo, long allocationId, public PdaResponses.Operation pick(String customerId, String planNo, long allocationId,
PdaRequests.PackageOperation request) { PdaRequests.PackageOperation request) {
InboundPlan plan = requirePlan(tenantId, planNo); InboundPlan plan = requirePlan(customerId, planNo);
InboundPdaOperation replay = replay(tenantId, "PICK", request.idempotencyKey(), request); InboundPdaOperation replay = replay(customerId, "PICK", request.idempotencyKey(), request);
if (replay != null) return read(replay, PdaResponses.Operation.class); if (replay != null) return read(replay, PdaResponses.Operation.class);
InboundPdaTask task = requireActiveTask(tenantId, plan); InboundPdaTask task = requireActiveTask(customerId, plan);
InboundPlanPalletAllocation allocation = requireAllocation(plan.getId(), allocationId); InboundPlanPalletAllocation allocation = requireAllocation(plan.getId(), allocationId);
InboundPlanLine line = requireLine(plan.getId(), allocation.getLineId()); InboundPlanLine line = requireLine(plan.getId(), allocation.getLineId());
int picked = value(allocation.getPickedPackageCount()); int picked = value(allocation.getPickedPackageCount());
@@ -169,7 +169,7 @@ public class InboundPdaWorkflowService {
List<PdaResponses.MergeCandidate> candidates = candidates(plan, allocation, mergeCount); List<PdaResponses.MergeCandidate> candidates = candidates(plan, allocation, mergeCount);
PdaResponses.Operation result = operation("MERGE_CONFIRMATION_REQUIRED", allocation, line, PdaResponses.Operation result = operation("MERGE_CONFIRMATION_REQUIRED", allocation, line,
candidates, plannedRemaining, request.packageCount(), mergeCount); candidates, plannedRemaining, request.packageCount(), mergeCount);
saveOperation(tenantId, task.getId(), allocationId, "PICK", request.idempotencyKey(), request, result); saveOperation(customerId, task.getId(), allocationId, "PICK", request.idempotencyKey(), request, result);
return result; return result;
} }
if (request.packageCount() > countedRemaining) { if (request.packageCount() > countedRemaining) {
@@ -189,25 +189,25 @@ public class InboundPdaWorkflowService {
repository.update(pallet); repository.update(pallet);
startPicking(task, plan, request.operatorId(), request.operatorName()); startPicking(task, plan, request.operatorId(), request.operatorName());
PdaResponses.Operation result = operation("PICKED", allocation, line, List.of(), null, null, null); PdaResponses.Operation result = operation("PICKED", allocation, line, List.of(), null, null, null);
saveOperation(tenantId, task.getId(), allocationId, "PICK", request.idempotencyKey(), request, result); saveOperation(customerId, task.getId(), allocationId, "PICK", request.idempotencyKey(), request, result);
return result; return result;
} }
@Transactional @Transactional
public List<PdaResponses.MergeCandidate> mergeCandidates(String tenantId, String planNo, public List<PdaResponses.MergeCandidate> mergeCandidates(String customerId, String planNo,
long allocationId) { long allocationId) {
InboundPlan plan = requirePlan(tenantId, planNo); InboundPlan plan = requirePlan(customerId, planNo);
InboundPlanPalletAllocation allocation = requireAllocation(plan.getId(), allocationId); InboundPlanPalletAllocation allocation = requireAllocation(plan.getId(), allocationId);
return candidates(plan, allocation, 1); return candidates(plan, allocation, 1);
} }
@Transactional @Transactional
public PdaResponses.Merge merge(String tenantId, String planNo, long targetAllocationId, public PdaResponses.Merge merge(String customerId, String planNo, long targetAllocationId,
PdaRequests.ConfirmMerge request) { PdaRequests.ConfirmMerge request) {
InboundPlan plan = requirePlan(tenantId, planNo); InboundPlan plan = requirePlan(customerId, planNo);
InboundPdaOperation replay = replay(tenantId, "MERGE", request.idempotencyKey(), request); InboundPdaOperation replay = replay(customerId, "MERGE", request.idempotencyKey(), request);
if (replay != null) return read(replay, PdaResponses.Merge.class); if (replay != null) return read(replay, PdaResponses.Merge.class);
InboundPdaTask task = requireActiveTask(tenantId, plan); InboundPdaTask task = requireActiveTask(customerId, plan);
if (request.sourceAllocationId() == targetAllocationId) throw conflict("Source and target allocations differ"); if (request.sourceAllocationId() == targetAllocationId) throw conflict("Source and target allocations differ");
InboundPlanPalletAllocation target = requireAllocation(plan.getId(), targetAllocationId); InboundPlanPalletAllocation target = requireAllocation(plan.getId(), targetAllocationId);
InboundPlanPalletAllocation source = requireAllocation(plan.getId(), request.sourceAllocationId()); InboundPlanPalletAllocation source = requireAllocation(plan.getId(), request.sourceAllocationId());
@@ -240,7 +240,7 @@ public class InboundPdaWorkflowService {
if (sourcePallet.getWarehouseResourceLockId() == null) { if (sourcePallet.getWarehouseResourceLockId() == null) {
throw conflict("Source pallet has no warehouse resource lock"); throw conflict("Source pallet has no warehouse resource lock");
} }
warehouse.scrap(tenantId, sourcePallet.getWarehouseResourceLockId()); warehouse.scrap(customerId, sourcePallet.getWarehouseResourceLockId());
sourcePallet.setAssignmentStatus("SCRAPPED"); sourcePallet.setAssignmentStatus("SCRAPPED");
sourcePallet.setWarehouseLocationId(null); sourcePallet.setWarehouseLocationId(null);
sourcePallet.setWarehouseResourceLockId(null); sourcePallet.setWarehouseResourceLockId(null);
@@ -251,7 +251,7 @@ public class InboundPdaWorkflowService {
String groupNo = "PMG" + IdWorker.getId(); String groupNo = "PMG" + IdWorker.getId();
InboundPalletMerge merge = new InboundPalletMerge(); InboundPalletMerge merge = new InboundPalletMerge();
merge.setId(IdWorker.getId()); merge.setId(IdWorker.getId());
merge.setTenantId(tenantId); merge.setCustomerId(customerId);
merge.setTaskId(task.getId()); merge.setTaskId(task.getId());
merge.setMergeGroupNo(groupNo); merge.setMergeGroupNo(groupNo);
merge.setSourcePalletId(sourcePallet.getId()); merge.setSourcePalletId(sourcePallet.getId());
@@ -270,14 +270,14 @@ public class InboundPdaWorkflowService {
repository.insert(merge); repository.insert(merge);
PdaResponses.Merge result = new PdaResponses.Merge("MERGED", groupNo, sourceLine.getOrderId(), PdaResponses.Merge result = new PdaResponses.Merge("MERGED", groupNo, sourceLine.getOrderId(),
targetLine.getOrderId(), request.packageCount(), movedVolume, scrapped); targetLine.getOrderId(), request.packageCount(), movedVolume, scrapped);
saveOperation(tenantId, task.getId(), targetAllocationId, "MERGE", request.idempotencyKey(), request, result); saveOperation(customerId, task.getId(), targetAllocationId, "MERGE", request.idempotencyKey(), request, result);
return result; return result;
} }
@Transactional @Transactional
public PdaResponses.Task complete(String tenantId, String planNo) { public PdaResponses.Task complete(String customerId, String planNo) {
InboundPlan plan = requirePlan(tenantId, planNo); InboundPlan plan = requirePlan(customerId, planNo);
InboundPdaTask task = repository.lockTask(tenantId, plan.getId()); InboundPdaTask task = repository.lockTask(customerId, plan.getId());
if (task == null) throw conflict("Push the inbound plan to PDA before operating"); if (task == null) throw conflict("Push the inbound plan to PDA before operating");
if ("COMPLETED".equals(task.getStatus())) return task(task, planNo); if ("COMPLETED".equals(task.getStatus())) return task(task, planNo);
Map<Long, Integer> pickedByLine = new HashMap<>(); Map<Long, Integer> pickedByLine = new HashMap<>();
@@ -293,7 +293,7 @@ public class InboundPdaWorkflowService {
} }
} }
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
stockReceipts.stockIn(tenantId, plan, task, lines, allocations, repository.pallets(plan.getId()), now); stockReceipts.stockIn(customerId, plan, task, lines, allocations, repository.pallets(plan.getId()), now);
task.setStatus("COMPLETED"); task.setStatus("COMPLETED");
task.setCompletedAt(now); task.setCompletedAt(now);
task.setUpdatedAt(now); task.setUpdatedAt(now);
@@ -419,14 +419,14 @@ public class InboundPdaWorkflowService {
|| !("ASSIGNED".equals(pallet.getAssignmentStatus()) || "LOCKED".equals(pallet.getAssignmentStatus())); || !("ASSIGNED".equals(pallet.getAssignmentStatus()) || "LOCKED".equals(pallet.getAssignmentStatus()));
} }
private InboundPlan requirePlan(String tenantId, String planNo) { private InboundPlan requirePlan(String customerId, String planNo) {
InboundPlan plan = repository.lockPlan(tenantId, planNo); InboundPlan plan = repository.lockPlan(customerId, planNo);
if (plan == null) throw notFound("Inbound plan not found"); if (plan == null) throw notFound("Inbound plan not found");
return plan; return plan;
} }
private InboundPdaTask requireActiveTask(String tenantId, InboundPlan plan) { private InboundPdaTask requireActiveTask(String customerId, InboundPlan plan) {
InboundPdaTask task = repository.lockTask(tenantId, plan.getId()); InboundPdaTask task = repository.lockTask(customerId, plan.getId());
if (task == null) throw conflict("Push the inbound plan to PDA before operating"); if (task == null) throw conflict("Push the inbound plan to PDA before operating");
if ("COMPLETED".equals(task.getStatus())) throw conflict("PDA task is already completed"); if ("COMPLETED".equals(task.getStatus())) throw conflict("PDA task is already completed");
return task; return task;
@@ -470,8 +470,8 @@ public class InboundPdaWorkflowService {
return volume(line.getVolumeCbm()).divide(BigDecimal.valueOf(required), 8, RoundingMode.HALF_UP); return volume(line.getVolumeCbm()).divide(BigDecimal.valueOf(required), 8, RoundingMode.HALF_UP);
} }
private InboundPdaOperation replay(String tenantId, String action, String key, Object request) { private InboundPdaOperation replay(String customerId, String action, String key, Object request) {
InboundPdaOperation operation = repository.operation(tenantId, key); InboundPdaOperation operation = repository.operation(customerId, key);
if (operation == null) return null; if (operation == null) return null;
if (!action.equals(operation.getAction()) || !hash(request).equals(operation.getRequestHash())) { if (!action.equals(operation.getAction()) || !hash(request).equals(operation.getRequestHash())) {
throw conflict("Idempotency key was already used with a different request"); throw conflict("Idempotency key was already used with a different request");
@@ -479,11 +479,11 @@ public class InboundPdaWorkflowService {
return operation; return operation;
} }
private void saveOperation(String tenantId, Long taskId, Long allocationId, String action, private void saveOperation(String customerId, Long taskId, Long allocationId, String action,
String key, Object request, Object result) { String key, Object request, Object result) {
InboundPdaOperation operation = new InboundPdaOperation(); InboundPdaOperation operation = new InboundPdaOperation();
operation.setId(IdWorker.getId()); operation.setId(IdWorker.getId());
operation.setTenantId(tenantId); operation.setCustomerId(customerId);
operation.setTaskId(taskId); operation.setTaskId(taskId);
operation.setAllocationId(allocationId); operation.setAllocationId(allocationId);
operation.setAction(action); operation.setAction(action);
@@ -32,14 +32,14 @@ public class InboundStockReceiptService {
@Resource private InboundStockReceiptMapper receipts; @Resource private InboundStockReceiptMapper receipts;
@Resource private InventoryCommandService inventory; @Resource private InventoryCommandService inventory;
public void stockIn(String tenantId, InboundPlan plan, InboundPdaTask task, public void stockIn(String customerId, InboundPlan plan, InboundPdaTask task,
List<InboundPlanLine> lines, List<InboundPlanPalletAllocation> allocations, List<InboundPlanLine> lines, List<InboundPlanPalletAllocation> allocations,
List<InboundPlanPallet> pallets, LocalDateTime receivedAt) { List<InboundPlanPallet> pallets, LocalDateTime receivedAt) {
if (!receipts.listTask(tenantId, task.getId()).isEmpty()) return; if (!receipts.listTask(customerId, task.getId()).isEmpty()) return;
Map<Long, LastlegOrder> orderById = orders.selectBatchIds(lines.stream() Map<Long, LastlegOrder> orderById = orders.selectBatchIds(lines.stream()
.map(InboundPlanLine::getOrderId).collect(Collectors.toSet())).stream() .map(InboundPlanLine::getOrderId).collect(Collectors.toSet())).stream()
.filter(order -> tenantId.equals(order.getTenantId())) .filter(order -> customerId.equals(order.getCustomerId()))
.collect(Collectors.toMap(LastlegOrder::getId, order -> order)); .collect(Collectors.toMap(LastlegOrder::getId, order -> order));
Map<Long, InboundPlanPallet> palletById = pallets.stream() Map<Long, InboundPlanPallet> palletById = pallets.stream()
.collect(Collectors.toMap(InboundPlanPallet::getId, pallet -> pallet)); .collect(Collectors.toMap(InboundPlanPallet::getId, pallet -> pallet));
@@ -59,13 +59,13 @@ public class InboundStockReceiptService {
throw conflict("PDA picked packages must equal the inbound order package total"); throw conflict("PDA picked packages must equal the inbound order package total");
} }
for (InboundPlanPalletAllocation allocation : lineAllocations) { for (InboundPlanPalletAllocation allocation : lineAllocations) {
persist(tenantId, plan, task, line, allocation, persist(customerId, plan, task, line, allocation,
requirePallet(palletById, allocation), order, receivedAt); requirePallet(palletById, allocation), order, receivedAt);
} }
} }
} }
private void persist(String tenantId, InboundPlan plan, InboundPdaTask task, InboundPlanLine line, private void persist(String customerId, InboundPlan plan, InboundPdaTask task, InboundPlanLine line,
InboundPlanPalletAllocation allocation, InboundPlanPallet pallet, InboundPlanPalletAllocation allocation, InboundPlanPallet pallet,
LastlegOrder order, LocalDateTime receivedAt) { LastlegOrder order, LocalDateTime receivedAt) {
long packages = value(allocation.getPickedPackageCount()); long packages = value(allocation.getPickedPackageCount());
@@ -77,7 +77,7 @@ public class InboundStockReceiptService {
String operationKey = "PDA_STOCK_IN:" + task.getId() + ":" + allocation.getId(); String operationKey = "PDA_STOCK_IN:" + task.getId() + ":" + allocation.getId();
String overseasWarehouseId = required(order.getOverseasWarehouseId(), String overseasWarehouseId = required(order.getOverseasWarehouseId(),
"Actual overseas warehouse is required for stock-in"); "Actual overseas warehouse is required for stock-in");
InventoryCommandResponse result = inventory.stockIn(tenantId, task.getOperatorId(), InventoryCommandResponse result = inventory.stockIn(customerId, task.getOperatorId(),
new InventoryCommandRequests.StockIn(null, order.getCustomerName(), overseasWarehouseId, new InventoryCommandRequests.StockIn(null, order.getCustomerName(), overseasWarehouseId,
null, order.getOverseasWarehouseName(), line.getDestinationWarehouseId(), destinationCode, null, order.getOverseasWarehouseName(), line.getDestinationWarehouseId(), destinationCode,
line.getDestinationWarehouseName(), order.getId(), plan.getPlanNo(), line.getInboundCode(), line.getDestinationWarehouseName(), order.getId(), plan.getPlanNo(), line.getInboundCode(),
@@ -88,7 +88,7 @@ public class InboundStockReceiptService {
InboundStockReceipt receipt = new InboundStockReceipt(); InboundStockReceipt receipt = new InboundStockReceipt();
receipt.setId(IdWorker.getId()); receipt.setId(IdWorker.getId());
receipt.setTenantId(tenantId); receipt.setCustomerId(customerId);
receipt.setPlanId(plan.getId()); receipt.setPlanId(plan.getId());
receipt.setTaskId(task.getId()); receipt.setTaskId(task.getId());
receipt.setAllocationId(allocation.getId()); receipt.setAllocationId(allocation.getId());
@@ -32,7 +32,7 @@ public class InboundFbaAppointmentService {
@Resource private InboundFbaAppointmentQueryMapper queryMapper; @Resource private InboundFbaAppointmentQueryMapper queryMapper;
@Transactional @Transactional
public FbaAppointmentResponse batchCreate(String tenantId, String operatorId, public FbaAppointmentResponse batchCreate(String customerId, String operatorId,
BatchFbaAppointmentRequest request) { BatchFbaAppointmentRequest request) {
List<Long> lineIds = List.copyOf(request.lineIds()); List<Long> lineIds = List.copyOf(request.lineIds());
if (new HashSet<>(lineIds).size() != lineIds.size()) { if (new HashSet<>(lineIds).size() != lineIds.size()) {
@@ -42,12 +42,12 @@ public class InboundFbaAppointmentService {
String status = normalizeStatus(request.appointmentStatus()); String status = normalizeStatus(request.appointmentStatus());
String remark = trimToNull(request.appointmentRemark()); String remark = trimToNull(request.appointmentRemark());
InboundFbaAppointment existing = find(tenantId, appointmentNo); InboundFbaAppointment existing = find(customerId, appointmentNo);
if (existing != null) { if (existing != null) {
return verifyReplay(existing, lineIds, request.appointmentAt(), status, remark); return verifyReplay(existing, lineIds, request.appointmentAt(), status, remark);
} }
List<FbaAppointmentLineSnapshot> lines = queryMapper.lockLines(tenantId, lineIds); List<FbaAppointmentLineSnapshot> lines = queryMapper.lockLines(customerId, lineIds);
if (lines.size() != lineIds.size()) { if (lines.size() != lineIds.size()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, throw new ResponseStatusException(HttpStatus.NOT_FOUND,
"One or more selected inbound lines do not exist in this tenant"); "One or more selected inbound lines do not exist in this tenant");
@@ -55,7 +55,7 @@ public class InboundFbaAppointmentService {
String fbaWarehouseCode = commonFbaWarehouse(lines); String fbaWarehouseCode = commonFbaWarehouse(lines);
// The row locks serialize concurrent submissions selecting the same inbound lines. // The row locks serialize concurrent submissions selecting the same inbound lines.
existing = find(tenantId, appointmentNo); existing = find(customerId, appointmentNo);
if (existing != null) { if (existing != null) {
return verifyReplay(existing, lineIds, request.appointmentAt(), status, remark); return verifyReplay(existing, lineIds, request.appointmentAt(), status, remark);
} }
@@ -63,7 +63,7 @@ public class InboundFbaAppointmentService {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
InboundFbaAppointment appointment = new InboundFbaAppointment(); InboundFbaAppointment appointment = new InboundFbaAppointment();
appointment.setId(IdWorker.getId()); appointment.setId(IdWorker.getId());
appointment.setTenantId(tenantId); appointment.setCustomerId(customerId);
appointment.setAppointmentNo(appointmentNo); appointment.setAppointmentNo(appointmentNo);
appointment.setAppointmentAt(request.appointmentAt()); appointment.setAppointmentAt(request.appointmentAt());
appointment.setAppointmentStatus(status); appointment.setAppointmentStatus(status);
@@ -76,7 +76,7 @@ public class InboundFbaAppointmentService {
try { try {
appointmentMapper.insert(appointment); appointmentMapper.insert(appointment);
} catch (DuplicateKeyException exception) { } catch (DuplicateKeyException exception) {
InboundFbaAppointment concurrent = find(tenantId, appointmentNo); InboundFbaAppointment concurrent = find(customerId, appointmentNo);
if (concurrent != null) { if (concurrent != null) {
return verifyReplay(concurrent, lineIds, request.appointmentAt(), status, remark); return verifyReplay(concurrent, lineIds, request.appointmentAt(), status, remark);
} }
@@ -99,8 +99,8 @@ public class InboundFbaAppointmentService {
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
public FbaAppointmentResponse detail(String tenantId, String appointmentNo) { public FbaAppointmentResponse detail(String customerId, String appointmentNo) {
InboundFbaAppointment appointment = find(tenantId, InboundFbaAppointment appointment = find(customerId,
requiredTrimmed(appointmentNo, "Appointment number is required")); requiredTrimmed(appointmentNo, "Appointment number is required"));
if (appointment == null) { if (appointment == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "FBA appointment not found"); throw new ResponseStatusException(HttpStatus.NOT_FOUND, "FBA appointment not found");
@@ -134,9 +134,9 @@ public class InboundFbaAppointmentService {
return warehouses.get(0); return warehouses.get(0);
} }
private InboundFbaAppointment find(String tenantId, String appointmentNo) { private InboundFbaAppointment find(String customerId, String appointmentNo) {
return appointmentMapper.selectOne(new LambdaQueryWrapper<InboundFbaAppointment>() return appointmentMapper.selectOne(new LambdaQueryWrapper<InboundFbaAppointment>()
.eq(InboundFbaAppointment::getTenantId, tenantId) .eq(InboundFbaAppointment::getCustomerId, customerId)
.eq(InboundFbaAppointment::getAppointmentNo, appointmentNo)); .eq(InboundFbaAppointment::getAppointmentNo, appointmentNo));
} }
@@ -148,7 +148,7 @@ public class InboundFbaAppointmentService {
private FbaAppointmentResponse response(InboundFbaAppointment appointment, private FbaAppointmentResponse response(InboundFbaAppointment appointment,
List<InboundFbaAppointmentItem> items) { List<InboundFbaAppointmentItem> items) {
return new FbaAppointmentResponse(appointment.getId(), appointment.getTenantId(), return new FbaAppointmentResponse(appointment.getId(), appointment.getCustomerId(),
appointment.getAppointmentNo(), appointment.getAppointmentAt(), appointment.getAppointmentStatus(), appointment.getAppointmentNo(), appointment.getAppointmentAt(), appointment.getAppointmentStatus(),
appointment.getAppointmentRemark(), appointment.getFbaWarehouseCode(), appointment.getSelectedCount(), appointment.getAppointmentRemark(), appointment.getFbaWarehouseCode(), appointment.getSelectedCount(),
appointment.getCreatedBy(), appointment.getCreatedAt(), appointment.getUpdatedAt(), appointment.getCreatedBy(), appointment.getCreatedAt(), appointment.getUpdatedAt(),
@@ -36,20 +36,20 @@ public class InboundPlanService {
@Resource private InboundPlanOrderMapper orderMapper; @Resource private InboundPlanOrderMapper orderMapper;
@Transactional @Transactional
public InboundPlanResponse convert(String tenantId, Long selectedOrderId) { public InboundPlanResponse convert(String customerId, Long selectedOrderId) {
String containerNo = trimToNull(orderMapper.findContainerNo(tenantId, selectedOrderId)); String containerNo = trimToNull(orderMapper.findContainerNo(customerId, selectedOrderId));
if (containerNo == null) { if (containerNo == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Order does not exist in this tenant or container number is empty"); "Order does not exist in this tenant or container number is empty");
} }
List<SourceOrderSnapshot> orders = orderMapper.lockContainerOrders(tenantId, containerNo); List<SourceOrderSnapshot> orders = orderMapper.lockContainerOrders(customerId, containerNo);
if (orders.isEmpty()) { if (orders.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Container orders not found"); throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Container orders not found");
} }
InboundPlan existing = planMapper.findByContainer(tenantId, containerNo); InboundPlan existing = planMapper.findByContainer(customerId, containerNo);
if (existing != null) { if (existing != null) {
return detail(tenantId, existing); return detail(customerId, existing);
} }
validateOrders(orders); validateOrders(orders);
@@ -57,7 +57,7 @@ public class InboundPlanService {
InboundPlan plan = new InboundPlan(); InboundPlan plan = new InboundPlan();
plan.setId(IdWorker.getId()); plan.setId(IdWorker.getId());
plan.setPlanNo("LIP" + plan.getId()); plan.setPlanNo("LIP" + plan.getId());
plan.setTenantId(tenantId); plan.setCustomerId(customerId);
plan.setContainerNo(containerNo); plan.setContainerNo(containerNo);
plan.setStatus("PENDING_ASSIGNMENT"); plan.setStatus("PENDING_ASSIGNMENT");
plan.setLineCount(orders.size()); plan.setLineCount(orders.size());
@@ -88,23 +88,23 @@ public class InboundPlanService {
InboundPlanLine line = lineByOrder.get(order.orderId()); InboundPlanLine line = lineByOrder.get(order.orderId());
line.setPlannedPalletCount(count); line.setPlannedPalletCount(count);
lineMapper.updateById(line); lineMapper.updateById(line);
if (orderMapper.markPlanned(tenantId, order.orderId(), plan.getPlanNo(), count, now) != 1) { if (orderMapper.markPlanned(customerId, order.orderId(), plan.getPlanNo(), count, now) != 1) {
throw new IllegalStateException("Failed to update order " + order.orderId()); throw new IllegalStateException("Failed to update order " + order.orderId());
} }
orderMapper.insertHistory(order.orderId(), order.currentStatus(), orderMapper.insertHistory(order.orderId(), order.currentStatus(),
"Container converted to inbound plan " + plan.getPlanNo(), now); "Container converted to inbound plan " + plan.getPlanNo(), now);
} }
return detail(tenantId, plan); return detail(customerId, plan);
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
public InboundPlanResponse detailByPlanNo(String tenantId, String planNo) { public InboundPlanResponse detailByPlanNo(String customerId, String planNo) {
InboundPlan plan = planMapper.selectOne(new LambdaQueryWrapper<InboundPlan>() InboundPlan plan = planMapper.selectOne(new LambdaQueryWrapper<InboundPlan>()
.eq(InboundPlan::getTenantId, tenantId).eq(InboundPlan::getPlanNo, planNo)); .eq(InboundPlan::getCustomerId, customerId).eq(InboundPlan::getPlanNo, planNo));
if (plan == null) { if (plan == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound plan not found"); throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound plan not found");
} }
return detail(tenantId, plan); return detail(customerId, plan);
} }
private Map<Long, InboundPlanLine> createLines(InboundPlan plan, List<SourceOrderSnapshot> orders) { private Map<Long, InboundPlanLine> createLines(InboundPlan plan, List<SourceOrderSnapshot> orders) {
@@ -195,8 +195,8 @@ public class InboundPlanService {
} }
} }
private InboundPlanResponse detail(String tenantId, InboundPlan plan) { private InboundPlanResponse detail(String customerId, InboundPlan plan) {
if (!tenantId.equals(plan.getTenantId())) { if (!customerId.equals(plan.getCustomerId())) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound plan not found"); throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound plan not found");
} }
List<InboundPlanLine> lines = lineMapper.selectList(new LambdaQueryWrapper<InboundPlanLine>() List<InboundPlanLine> lines = lineMapper.selectList(new LambdaQueryWrapper<InboundPlanLine>()
@@ -207,7 +207,7 @@ public class InboundPlanService {
new LambdaQueryWrapper<InboundPlanPalletAllocation>() new LambdaQueryWrapper<InboundPlanPalletAllocation>()
.eq(InboundPlanPalletAllocation::getPlanId, plan.getId()) .eq(InboundPlanPalletAllocation::getPlanId, plan.getId())
.orderByAsc(InboundPlanPalletAllocation::getId)); .orderByAsc(InboundPlanPalletAllocation::getId));
return new InboundPlanResponse(plan.getId(), plan.getPlanNo(), plan.getTenantId(), plan.getContainerNo(), return new InboundPlanResponse(plan.getId(), plan.getPlanNo(), plan.getCustomerId(), plan.getContainerNo(),
plan.getStatus(), plan.getLineCount(), plan.getPalletCount(), plan.getTotalVolumeCbm(), plan.getStatus(), plan.getLineCount(), plan.getPalletCount(), plan.getTotalVolumeCbm(),
lines.stream().map(line -> new InboundPlanResponse.Line(line.getId(), line.getOrderId(), line.getInboundCode(), line.getFbaWarehouseCode(), lines.stream().map(line -> new InboundPlanResponse.Line(line.getId(), line.getOrderId(), line.getInboundCode(), line.getFbaWarehouseCode(),
line.getDestinationWarehouseId(), line.getDestinationWarehouseCode(), line.getDestinationWarehouseName(), line.getDestinationWarehouseId(), line.getDestinationWarehouseCode(), line.getDestinationWarehouseName(),
@@ -27,66 +27,66 @@ public class InventoryCommandController {
@Operation(summary = "Put inbound-order packages into an overseas warehouse pallet") @Operation(summary = "Put inbound-order packages into an overseas warehouse pallet")
public ApiResponse<InventoryCommandResponse> stockIn(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<InventoryCommandResponse> stockIn(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody InventoryCommandRequests.StockIn request) { @Valid @RequestBody InventoryCommandRequests.StockIn request) {
return ApiResponse.success(service.stockIn(scope(user, request.tenantId()), operator(user), request)); return ApiResponse.success(service.stockIn(scope(user, request.customerId()), operator(user), request));
} }
@PostMapping("/hold") @PostMapping("/hold")
@Operation(summary = "Move available inbound-order packages to hold") @Operation(summary = "Move available inbound-order packages to hold")
public ApiResponse<InventoryCommandResponse> hold(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<InventoryCommandResponse> hold(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody InventoryCommandRequests.Move request) { @Valid @RequestBody InventoryCommandRequests.Move request) {
return ApiResponse.success(service.hold(scope(user, request.tenantId()), operator(user), request)); return ApiResponse.success(service.hold(scope(user, request.customerId()), operator(user), request));
} }
@PostMapping("/release") @PostMapping("/release")
@Operation(summary = "Release held inbound-order packages") @Operation(summary = "Release held inbound-order packages")
public ApiResponse<InventoryCommandResponse> release(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<InventoryCommandResponse> release(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody InventoryCommandRequests.Move request) { @Valid @RequestBody InventoryCommandRequests.Move request) {
return ApiResponse.success(service.release(scope(user, request.tenantId()), operator(user), request)); return ApiResponse.success(service.release(scope(user, request.customerId()), operator(user), request));
} }
@PostMapping("/allocate") @PostMapping("/allocate")
@Operation(summary = "Allocate available inbound-order packages to an outbound order") @Operation(summary = "Allocate available inbound-order packages to an outbound order")
public ApiResponse<InventoryCommandResponse> allocate(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<InventoryCommandResponse> allocate(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody InventoryCommandRequests.Move request) { @Valid @RequestBody InventoryCommandRequests.Move request) {
return ApiResponse.success(service.allocate(scope(user, request.tenantId()), operator(user), request)); return ApiResponse.success(service.allocate(scope(user, request.customerId()), operator(user), request));
} }
@PostMapping("/unallocate") @PostMapping("/unallocate")
@Operation(summary = "Cancel an outbound allocation") @Operation(summary = "Cancel an outbound allocation")
public ApiResponse<InventoryCommandResponse> unallocate(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<InventoryCommandResponse> unallocate(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody InventoryCommandRequests.Move request) { @Valid @RequestBody InventoryCommandRequests.Move request) {
return ApiResponse.success(service.unallocate(scope(user, request.tenantId()), operator(user), request)); return ApiResponse.success(service.unallocate(scope(user, request.customerId()), operator(user), request));
} }
@PostMapping("/pick") @PostMapping("/pick")
@Operation(summary = "Confirm picked inbound-order packages") @Operation(summary = "Confirm picked inbound-order packages")
public ApiResponse<InventoryCommandResponse> pick(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<InventoryCommandResponse> pick(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody InventoryCommandRequests.Move request) { @Valid @RequestBody InventoryCommandRequests.Move request) {
return ApiResponse.success(service.pick(scope(user, request.tenantId()), operator(user), request)); return ApiResponse.success(service.pick(scope(user, request.customerId()), operator(user), request));
} }
@PostMapping("/ship") @PostMapping("/ship")
@Operation(summary = "Ship picked inbound-order packages and report whether the pallet is empty") @Operation(summary = "Ship picked inbound-order packages and report whether the pallet is empty")
public ApiResponse<InventoryCommandResponse> ship(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<InventoryCommandResponse> ship(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody InventoryCommandRequests.Move request) { @Valid @RequestBody InventoryCommandRequests.Move request) {
return ApiResponse.success(service.ship(scope(user, request.tenantId()), operator(user), request)); return ApiResponse.success(service.ship(scope(user, request.customerId()), operator(user), request));
} }
@PostMapping("/adjust") @PostMapping("/adjust")
@Operation(summary = "Adjust inbound-order package inventory after a physical count") @Operation(summary = "Adjust inbound-order package inventory after a physical count")
public ApiResponse<InventoryCommandResponse> adjust(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<InventoryCommandResponse> adjust(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody InventoryCommandRequests.Adjustment request) { @Valid @RequestBody InventoryCommandRequests.Adjustment request) {
return ApiResponse.success(service.adjust(scope(user, request.tenantId()), operator(user), request)); return ApiResponse.success(service.adjust(scope(user, request.customerId()), operator(user), request));
} }
private String scope(AuthenticatedUser user, String requestedTenant) { private String scope(AuthenticatedUser user, String requestedCustomer) {
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) { if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
if (requestedTenant == null || requestedTenant.isBlank() || "0".equals(requestedTenant)) { if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Platform request must specify target tenant"); "Platform request must specify target tenant");
} }
return requestedTenant.trim(); return requestedCustomer.trim();
} }
return String.valueOf(user.tenantId()); return String.valueOf(user.tenantId());
} }
@@ -10,7 +10,7 @@ public final class InventoryCommandRequests {
} }
public record StockIn( public record StockIn(
String tenantId, String customerId,
String customerName, String customerName,
@NotBlank String overseasWarehouseId, @NotBlank String overseasWarehouseId,
String overseasWarehouseCode, String overseasWarehouseCode,
@@ -35,7 +35,7 @@ public final class InventoryCommandRequests {
} }
public record Move( public record Move(
String tenantId, String customerId,
@NotNull Long positionId, @NotNull Long positionId,
@Positive long packages, @Positive long packages,
String businessOrderNo, String businessOrderNo,
@@ -47,7 +47,7 @@ public final class InventoryCommandRequests {
} }
public record Adjustment( public record Adjustment(
String tenantId, String customerId,
@NotNull Long positionId, @NotNull Long positionId,
long packageDelta, long packageDelta,
@NotBlank String reason, @NotBlank String reason,
@@ -8,15 +8,15 @@ import org.apache.ibatis.annotations.Select;
@Mapper @Mapper
public interface InventoryCommandMapper { public interface InventoryCommandMapper {
@Select("SELECT * FROM ll_inventory_position WHERE tenant_id=#{tenantId} AND id=#{positionId} LIMIT 1 FOR UPDATE") @Select("SELECT * FROM ll_inventory_position WHERE customer_id=#{customerId} AND id=#{positionId} LIMIT 1 FOR UPDATE")
InventoryPosition lockPosition(@Param("tenantId") String tenantId, InventoryPosition lockPosition(@Param("customerId") String customerId,
@Param("positionId") Long positionId); @Param("positionId") Long positionId);
@Select("SELECT * FROM ll_inventory_balance WHERE tenant_id=#{tenantId} AND id=#{balanceId} LIMIT 1 FOR UPDATE") @Select("SELECT * FROM ll_inventory_balance WHERE customer_id=#{customerId} AND id=#{balanceId} LIMIT 1 FOR UPDATE")
InventoryBalance lockBalance(@Param("tenantId") String tenantId, InventoryBalance lockBalance(@Param("customerId") String customerId,
@Param("balanceId") Long balanceId); @Param("balanceId") Long balanceId);
@Select("SELECT COALESCE(SUM(available_packages+hold_packages+allocated_packages+picked_packages+receiving_packages),0) " + @Select("SELECT COALESCE(SUM(available_packages+hold_packages+allocated_packages+picked_packages+receiving_packages),0) " +
"FROM ll_inventory_position WHERE tenant_id=#{tenantId} AND pallet_id=#{palletId}") "FROM ll_inventory_position WHERE customer_id=#{customerId} AND pallet_id=#{palletId}")
long palletPackages(@Param("tenantId") String tenantId, @Param("palletId") Long palletId); long palletPackages(@Param("customerId") String customerId, @Param("palletId") Long palletId);
} }
@@ -39,11 +39,11 @@ public class InventoryCommandService {
@Resource private ObjectMapper objectMapper; @Resource private ObjectMapper objectMapper;
@Transactional @Transactional
public InventoryCommandResponse stockIn(String tenantId, String operatorId, public InventoryCommandResponse stockIn(String customerId, String operatorId,
InventoryCommandRequests.StockIn request) { InventoryCommandRequests.StockIn request) {
return execute(tenantId, operatorId, "STOCK_IN", request.idempotencyKey(), request, operation -> { return execute(customerId, operatorId, "STOCK_IN", request.idempotencyKey(), request, operation -> {
InventoryBalance balance = balance(tenantId, request); InventoryBalance balance = balance(customerId, request);
InventoryPosition position = position(tenantId, balance, request); InventoryPosition position = position(customerId, balance, request);
long before = value(position.getAvailablePackages()); long before = value(position.getAvailablePackages());
position.setAvailablePackages(before + request.packages()); position.setAvailablePackages(before + request.packages());
position.setStatus("OCCUPIED"); position.setStatus("OCCUPIED");
@@ -54,7 +54,7 @@ public class InventoryCommandService {
balance.setAvailablePackages(value(balance.getAvailablePackages()) + request.packages()); balance.setAvailablePackages(value(balance.getAvailablePackages()) + request.packages());
touch(balance); touch(balance);
balances.updateById(balance); balances.updateById(balance);
ledger(tenantId, operatorId, operation.getId(), balance, position, "STOCK_IN", ledger(customerId, operatorId, operation.getId(), balance, position, "STOCK_IN",
"OUTSIDE", "AVAILABLE", before, request.packages(), before + request.packages(), "OUTSIDE", "AVAILABLE", before, request.packages(), before + request.packages(),
request.businessOrderNo(), request.inboundOrderNo(), null, request.sourceEventId(), request.businessOrderNo(), request.inboundOrderNo(), null, request.sourceEventId(),
request.idempotencyKey(), request.remark()); request.idempotencyKey(), request.remark());
@@ -63,34 +63,34 @@ public class InventoryCommandService {
} }
@Transactional @Transactional
public InventoryCommandResponse hold(String tenantId, String operatorId, InventoryCommandRequests.Move request) { public InventoryCommandResponse hold(String customerId, String operatorId, InventoryCommandRequests.Move request) {
return move(tenantId, operatorId, "HOLD", Bucket.AVAILABLE, Bucket.HOLD, request); return move(customerId, operatorId, "HOLD", Bucket.AVAILABLE, Bucket.HOLD, request);
} }
@Transactional @Transactional
public InventoryCommandResponse release(String tenantId, String operatorId, InventoryCommandRequests.Move request) { public InventoryCommandResponse release(String customerId, String operatorId, InventoryCommandRequests.Move request) {
return move(tenantId, operatorId, "RELEASE", Bucket.HOLD, Bucket.AVAILABLE, request); return move(customerId, operatorId, "RELEASE", Bucket.HOLD, Bucket.AVAILABLE, request);
} }
@Transactional @Transactional
public InventoryCommandResponse allocate(String tenantId, String operatorId, InventoryCommandRequests.Move request) { public InventoryCommandResponse allocate(String customerId, String operatorId, InventoryCommandRequests.Move request) {
return move(tenantId, operatorId, "ALLOCATE", Bucket.AVAILABLE, Bucket.ALLOCATED, request); return move(customerId, operatorId, "ALLOCATE", Bucket.AVAILABLE, Bucket.ALLOCATED, request);
} }
@Transactional @Transactional
public InventoryCommandResponse unallocate(String tenantId, String operatorId, InventoryCommandRequests.Move request) { public InventoryCommandResponse unallocate(String customerId, String operatorId, InventoryCommandRequests.Move request) {
return move(tenantId, operatorId, "UNALLOCATE", Bucket.ALLOCATED, Bucket.AVAILABLE, request); return move(customerId, operatorId, "UNALLOCATE", Bucket.ALLOCATED, Bucket.AVAILABLE, request);
} }
@Transactional @Transactional
public InventoryCommandResponse pick(String tenantId, String operatorId, InventoryCommandRequests.Move request) { public InventoryCommandResponse pick(String customerId, String operatorId, InventoryCommandRequests.Move request) {
return move(tenantId, operatorId, "PICK", Bucket.ALLOCATED, Bucket.PICKED, request); return move(customerId, operatorId, "PICK", Bucket.ALLOCATED, Bucket.PICKED, request);
} }
@Transactional @Transactional
public InventoryCommandResponse ship(String tenantId, String operatorId, InventoryCommandRequests.Move request) { public InventoryCommandResponse ship(String customerId, String operatorId, InventoryCommandRequests.Move request) {
return execute(tenantId, operatorId, "SHIP", request.idempotencyKey(), request, operation -> { return execute(customerId, operatorId, "SHIP", request.idempotencyKey(), request, operation -> {
Locked locked = locked(tenantId, request.positionId()); Locked locked = locked(customerId, request.positionId());
InventoryPosition position = locked.position(); InventoryPosition position = locked.position();
InventoryBalance balance = locked.balance(); InventoryBalance balance = locked.balance();
requireAtLeast(value(position.getPickedPackages()), request.packages(), "picked"); requireAtLeast(value(position.getPickedPackages()), request.packages(), "picked");
@@ -105,9 +105,9 @@ public class InventoryCommandService {
balance.setPickedPackages(value(balance.getPickedPackages()) - request.packages()); balance.setPickedPackages(value(balance.getPickedPackages()) - request.packages());
touch(balance); touch(balance);
balances.updateById(balance); balances.updateById(balance);
boolean palletEmpty = commands.palletPackages(tenantId, position.getPalletId()) == 0; boolean palletEmpty = commands.palletPackages(customerId, position.getPalletId()) == 0;
if (palletEmpty) warehouseGateway.scrapPallet(tenantId, position.getPalletId()); if (palletEmpty) warehouseGateway.scrapPallet(customerId, position.getPalletId());
ledger(tenantId, operatorId, operation.getId(), balance, position, "SHIP", ledger(customerId, operatorId, operation.getId(), balance, position, "SHIP",
"PICKED", "SHIPPED", before, -request.packages(), before - request.packages(), "PICKED", "SHIPPED", before, -request.packages(), before - request.packages(),
request.businessOrderNo(), null, request.outboundOrderNo(), request.sourceEventId(), request.businessOrderNo(), null, request.outboundOrderNo(), request.sourceEventId(),
request.idempotencyKey(), request.remark()); request.idempotencyKey(), request.remark());
@@ -116,13 +116,13 @@ public class InventoryCommandService {
} }
@Transactional @Transactional
public InventoryCommandResponse adjust(String tenantId, String operatorId, public InventoryCommandResponse adjust(String customerId, String operatorId,
InventoryCommandRequests.Adjustment request) { InventoryCommandRequests.Adjustment request) {
if (request.packageDelta() == 0) { if (request.packageDelta() == 0) {
throw badRequest("Adjustment package delta cannot be zero"); throw badRequest("Adjustment package delta cannot be zero");
} }
return execute(tenantId, operatorId, "ADJUST", request.idempotencyKey(), request, operation -> { return execute(customerId, operatorId, "ADJUST", request.idempotencyKey(), request, operation -> {
Locked locked = locked(tenantId, request.positionId()); Locked locked = locked(customerId, request.positionId());
InventoryPosition position = locked.position(); InventoryPosition position = locked.position();
InventoryBalance balance = locked.balance(); InventoryBalance balance = locked.balance();
long before = value(position.getAvailablePackages()); long before = value(position.getAvailablePackages());
@@ -139,7 +139,7 @@ public class InventoryCommandService {
balance.setAvailablePackages(balanceAfter); balance.setAvailablePackages(balanceAfter);
touch(balance); touch(balance);
balances.updateById(balance); balances.updateById(balance);
ledger(tenantId, operatorId, operation.getId(), balance, position, "ADJUST", ledger(customerId, operatorId, operation.getId(), balance, position, "ADJUST",
"AVAILABLE", "AVAILABLE", before, request.packageDelta(), after, "AVAILABLE", "AVAILABLE", before, request.packageDelta(), after,
request.businessOrderNo(), null, null, null, request.idempotencyKey(), request.businessOrderNo(), null, null, null, request.idempotencyKey(),
request.reason() + remarkSuffix(request.remark())); request.reason() + remarkSuffix(request.remark()));
@@ -147,10 +147,10 @@ public class InventoryCommandService {
}); });
} }
private InventoryCommandResponse move(String tenantId, String operatorId, String operationType, private InventoryCommandResponse move(String customerId, String operatorId, String operationType,
Bucket from, Bucket to, InventoryCommandRequests.Move request) { Bucket from, Bucket to, InventoryCommandRequests.Move request) {
return execute(tenantId, operatorId, operationType, request.idempotencyKey(), request, operation -> { return execute(customerId, operatorId, operationType, request.idempotencyKey(), request, operation -> {
Locked locked = locked(tenantId, request.positionId()); Locked locked = locked(customerId, request.positionId());
InventoryPosition position = locked.position(); InventoryPosition position = locked.position();
InventoryBalance balance = locked.balance(); InventoryBalance balance = locked.balance();
requireAtLeast(packages(position, from), request.packages(), from.name().toLowerCase()); requireAtLeast(packages(position, from), request.packages(), from.name().toLowerCase());
@@ -166,7 +166,7 @@ public class InventoryCommandService {
packages(balance, to, packages(balance, to) + request.packages()); packages(balance, to, packages(balance, to) + request.packages());
touch(balance); touch(balance);
balances.updateById(balance); balances.updateById(balance);
ledger(tenantId, operatorId, operation.getId(), balance, position, operationType, ledger(customerId, operatorId, operation.getId(), balance, position, operationType,
from.name(), to.name(), beforeTarget, request.packages(), beforeTarget + request.packages(), from.name(), to.name(), beforeTarget, request.packages(), beforeTarget + request.packages(),
request.businessOrderNo(), null, request.outboundOrderNo(), request.sourceEventId(), request.businessOrderNo(), null, request.outboundOrderNo(), request.sourceEventId(),
request.idempotencyKey(), request.remark()); request.idempotencyKey(), request.remark());
@@ -174,13 +174,13 @@ public class InventoryCommandService {
}); });
} }
private InventoryBalance balance(String tenantId, InventoryCommandRequests.StockIn request) { private InventoryBalance balance(String customerId, InventoryCommandRequests.StockIn request) {
InventoryBalance balance = balances.lockDimension(tenantId, request.overseasWarehouseId(), InventoryBalance balance = balances.lockDimension(customerId, request.overseasWarehouseId(),
request.destinationWarehouseCode(), request.inboundOrderId(), request.inboundCode()); request.destinationWarehouseCode(), request.inboundOrderId(), request.inboundCode());
if (balance != null) return balance; if (balance != null) return balance;
balance = new InventoryBalance(); balance = new InventoryBalance();
balance.setId(IdWorker.getId()); balance.setId(IdWorker.getId());
balance.setTenantId(tenantId); balance.setCustomerId(customerId);
balance.setCustomerName(trim(request.customerName())); balance.setCustomerName(trim(request.customerName()));
balance.setOverseasWarehouseId(request.overseasWarehouseId().trim()); balance.setOverseasWarehouseId(request.overseasWarehouseId().trim());
balance.setOverseasWarehouseCode(trim(request.overseasWarehouseCode())); balance.setOverseasWarehouseCode(trim(request.overseasWarehouseCode()));
@@ -199,21 +199,21 @@ public class InventoryCommandService {
balances.insert(balance); balances.insert(balance);
return balance; return balance;
} catch (DuplicateKeyException exception) { } catch (DuplicateKeyException exception) {
InventoryBalance concurrent = balances.lockDimension(tenantId, request.overseasWarehouseId(), InventoryBalance concurrent = balances.lockDimension(customerId, request.overseasWarehouseId(),
request.destinationWarehouseCode(), request.inboundOrderId(), request.inboundCode()); request.destinationWarehouseCode(), request.inboundOrderId(), request.inboundCode());
if (concurrent != null) return concurrent; if (concurrent != null) return concurrent;
throw exception; throw exception;
} }
} }
private InventoryPosition position(String tenantId, InventoryBalance balance, private InventoryPosition position(String customerId, InventoryBalance balance,
InventoryCommandRequests.StockIn request) { InventoryCommandRequests.StockIn request) {
String batchNo = trim(request.batchNo()) == null ? "DEFAULT" : request.batchNo().trim(); String batchNo = trim(request.batchNo()) == null ? "DEFAULT" : request.batchNo().trim();
InventoryPosition position = positions.lockPosition(tenantId, balance.getId(), request.palletId(), batchNo); InventoryPosition position = positions.lockPosition(customerId, balance.getId(), request.palletId(), batchNo);
if (position != null) return position; if (position != null) return position;
position = new InventoryPosition(); position = new InventoryPosition();
position.setId(IdWorker.getId()); position.setId(IdWorker.getId());
position.setTenantId(tenantId); position.setCustomerId(customerId);
position.setBalanceId(balance.getId()); position.setBalanceId(balance.getId());
position.setOverseasWarehouseId(balance.getOverseasWarehouseId()); position.setOverseasWarehouseId(balance.getOverseasWarehouseId());
position.setDestinationWarehouseCode(balance.getDestinationWarehouseCode()); position.setDestinationWarehouseCode(balance.getDestinationWarehouseCode());
@@ -237,30 +237,30 @@ public class InventoryCommandService {
positions.insert(position); positions.insert(position);
return position; return position;
} catch (DuplicateKeyException exception) { } catch (DuplicateKeyException exception) {
InventoryPosition concurrent = positions.lockPosition(tenantId, balance.getId(), request.palletId(), batchNo); InventoryPosition concurrent = positions.lockPosition(customerId, balance.getId(), request.palletId(), batchNo);
if (concurrent != null) return concurrent; if (concurrent != null) return concurrent;
throw exception; throw exception;
} }
} }
private Locked locked(String tenantId, Long positionId) { private Locked locked(String customerId, Long positionId) {
InventoryPosition position = commands.lockPosition(tenantId, positionId); InventoryPosition position = commands.lockPosition(customerId, positionId);
if (position == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inventory position not found"); if (position == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inventory position not found");
InventoryBalance balance = commands.lockBalance(tenantId, position.getBalanceId()); InventoryBalance balance = commands.lockBalance(customerId, position.getBalanceId());
if (balance == null) throw new IllegalStateException("Inventory position balance is missing"); if (balance == null) throw new IllegalStateException("Inventory position balance is missing");
return new Locked(balance, position); return new Locked(balance, position);
} }
private InventoryCommandResponse execute(String tenantId, String operatorId, String operationType, private InventoryCommandResponse execute(String customerId, String operatorId, String operationType,
String key, Object request, Work work) { String key, Object request, Work work) {
String normalizedKey = required(key, "Idempotency key is required"); String normalizedKey = required(key, "Idempotency key is required");
String requestHash = hash(request); String requestHash = hash(request);
InventoryOperation existing = operations.findIdempotency(tenantId, normalizedKey); InventoryOperation existing = operations.findIdempotency(customerId, normalizedKey);
if (existing != null) return replay(existing, operationType, requestHash); if (existing != null) return replay(existing, operationType, requestHash);
InventoryOperation operation = new InventoryOperation(); InventoryOperation operation = new InventoryOperation();
operation.setId(IdWorker.getId()); operation.setId(IdWorker.getId());
operation.setTenantId(tenantId); operation.setCustomerId(customerId);
operation.setIdempotencyKey(normalizedKey); operation.setIdempotencyKey(normalizedKey);
operation.setOperationType(operationType); operation.setOperationType(operationType);
operation.setRequestHash(requestHash); operation.setRequestHash(requestHash);
@@ -270,7 +270,7 @@ public class InventoryCommandService {
try { try {
operations.insert(operation); operations.insert(operation);
} catch (DuplicateKeyException exception) { } catch (DuplicateKeyException exception) {
InventoryOperation concurrent = operations.findIdempotency(tenantId, normalizedKey); InventoryOperation concurrent = operations.findIdempotency(customerId, normalizedKey);
if (concurrent != null) return replay(concurrent, operationType, requestHash); if (concurrent != null) return replay(concurrent, operationType, requestHash);
throw exception; throw exception;
} }
@@ -297,14 +297,14 @@ public class InventoryCommandService {
} }
} }
private void ledger(String tenantId, String operatorId, Long operationId, InventoryBalance balance, private void ledger(String customerId, String operatorId, Long operationId, InventoryBalance balance,
InventoryPosition position, String businessType, String from, String to, InventoryPosition position, String businessType, String from, String to,
long beforePackages, long deltaPackages, long afterPackages, long beforePackages, long deltaPackages, long afterPackages,
String businessOrderNo, String inboundOrderNo, String outboundOrderNo, String businessOrderNo, String inboundOrderNo, String outboundOrderNo,
String sourceEventId, String idempotencyKey, String remark) { String sourceEventId, String idempotencyKey, String remark) {
InventoryLedger ledger = new InventoryLedger(); InventoryLedger ledger = new InventoryLedger();
ledger.setId(IdWorker.getId()); ledger.setId(IdWorker.getId());
ledger.setTenantId(tenantId); ledger.setCustomerId(customerId);
ledger.setBalanceId(balance.getId()); ledger.setBalanceId(balance.getId());
ledger.setPositionId(position.getId()); ledger.setPositionId(position.getId());
ledger.setOverseasWarehouseId(balance.getOverseasWarehouseId()); ledger.setOverseasWarehouseId(balance.getOverseasWarehouseId());
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
public class InventoryBalance { public class InventoryBalance {
@TableId(type = IdType.ASSIGN_ID) @TableId(type = IdType.ASSIGN_ID)
private Long id; private Long id;
private String tenantId; private String customerId;
private String customerName; private String customerName;
private String overseasWarehouseId; private String overseasWarehouseId;
private String overseasWarehouseCode; private String overseasWarehouseCode;
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
public class InventoryLedger { public class InventoryLedger {
@TableId(type = IdType.ASSIGN_ID) @TableId(type = IdType.ASSIGN_ID)
private Long id; private Long id;
private String tenantId; private String customerId;
private Long balanceId; private Long balanceId;
private Long positionId; private Long positionId;
private String overseasWarehouseId; private String overseasWarehouseId;
@@ -22,6 +22,9 @@ public class InventoryLedger {
private Long palletId; private Long palletId;
private String palletNo; private String palletNo;
private String batchNo; private String batchNo;
private String skuCode;
private String cartonMark;
private Long cartonPackages;
private String warehouseLocationCode; private String warehouseLocationCode;
private String businessType; private String businessType;
private Long businessOrderId; private Long businessOrderId;
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
public class InventoryOperation { public class InventoryOperation {
@TableId(type = IdType.ASSIGN_ID) @TableId(type = IdType.ASSIGN_ID)
private Long id; private Long id;
private String tenantId; private String customerId;
private String idempotencyKey; private String idempotencyKey;
private String operationType; private String operationType;
private String requestHash; private String requestHash;
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
public class InventoryPosition { public class InventoryPosition {
@TableId(type = IdType.ASSIGN_ID) @TableId(type = IdType.ASSIGN_ID)
private Long id; private Long id;
private String tenantId; private String customerId;
private Long balanceId; private Long balanceId;
private String overseasWarehouseId; private String overseasWarehouseId;
private String destinationWarehouseCode; private String destinationWarehouseCode;
@@ -9,32 +9,32 @@ import org.apache.ibatis.annotations.Select;
import java.util.List; import java.util.List;
public interface InventoryBalanceMapper extends BaseMapper<InventoryBalance> { public interface InventoryBalanceMapper extends BaseMapper<InventoryBalance> {
@Select("SELECT * FROM ll_inventory_balance WHERE tenant_id=#{tenantId} " + @Select("SELECT * FROM ll_inventory_balance WHERE customer_id=#{customerId} " +
"AND overseas_warehouse_id=#{overseasWarehouseId} " + "AND overseas_warehouse_id=#{overseasWarehouseId} " +
"AND destination_warehouse_code=#{destinationWarehouseCode} " + "AND destination_warehouse_code=#{destinationWarehouseCode} " +
"AND inbound_order_id=#{inboundOrderId} AND inbound_code=#{inboundCode} LIMIT 1") "AND inbound_order_id=#{inboundOrderId} AND inbound_code=#{inboundCode} LIMIT 1")
InventoryBalance findDimension(@Param("tenantId") String tenantId, InventoryBalance findDimension(@Param("customerId") String customerId,
@Param("overseasWarehouseId") String overseasWarehouseId, @Param("overseasWarehouseId") String overseasWarehouseId,
@Param("destinationWarehouseCode") String destinationWarehouseCode, @Param("destinationWarehouseCode") String destinationWarehouseCode,
@Param("inboundOrderId") Long inboundOrderId, @Param("inboundOrderId") Long inboundOrderId,
@Param("inboundCode") String inboundCode); @Param("inboundCode") String inboundCode);
@Select("SELECT * FROM ll_inventory_balance WHERE tenant_id=#{tenantId} " + @Select("SELECT * FROM ll_inventory_balance WHERE customer_id=#{customerId} " +
"AND overseas_warehouse_id=#{overseasWarehouseId} " + "AND overseas_warehouse_id=#{overseasWarehouseId} " +
"AND destination_warehouse_code=#{destinationWarehouseCode} " + "AND destination_warehouse_code=#{destinationWarehouseCode} " +
"AND inbound_order_id=#{inboundOrderId} AND inbound_code=#{inboundCode} LIMIT 1 FOR UPDATE") "AND inbound_order_id=#{inboundOrderId} AND inbound_code=#{inboundCode} LIMIT 1 FOR UPDATE")
InventoryBalance lockDimension(@Param("tenantId") String tenantId, InventoryBalance lockDimension(@Param("customerId") String customerId,
@Param("overseasWarehouseId") String overseasWarehouseId, @Param("overseasWarehouseId") String overseasWarehouseId,
@Param("destinationWarehouseCode") String destinationWarehouseCode, @Param("destinationWarehouseCode") String destinationWarehouseCode,
@Param("inboundOrderId") Long inboundOrderId, @Param("inboundOrderId") Long inboundOrderId,
@Param("inboundCode") String inboundCode); @Param("inboundCode") String inboundCode);
@Select("<script>SELECT * FROM ll_inventory_balance WHERE tenant_id=#{tenantId}" + @Select("<script>SELECT * FROM ll_inventory_balance WHERE customer_id=#{customerId}" +
"<if test='overseasWarehouseId != null and overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{overseasWarehouseId}</if>" + "<if test='overseasWarehouseId != null and overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{overseasWarehouseId}</if>" +
"<if test='destinationWarehouseCode != null and destinationWarehouseCode != \"\"'> AND destination_warehouse_code=#{destinationWarehouseCode}</if>" + "<if test='destinationWarehouseCode != null and destinationWarehouseCode != \"\"'> AND destination_warehouse_code=#{destinationWarehouseCode}</if>" +
"<if test='inboundCode != null and inboundCode != \"\"'> AND inbound_code=#{inboundCode}</if>" + "<if test='inboundCode != null and inboundCode != \"\"'> AND inbound_code=#{inboundCode}</if>" +
" ORDER BY updated_at DESC,id DESC LIMIT #{limit} OFFSET #{offset}</script>") " ORDER BY updated_at DESC,id DESC LIMIT #{limit} OFFSET #{offset}</script>")
List<InventoryBalance> listTenant(@Param("tenantId") String tenantId, List<InventoryBalance> listTenant(@Param("customerId") String customerId,
@Param("overseasWarehouseId") String overseasWarehouseId, @Param("overseasWarehouseId") String overseasWarehouseId,
@Param("destinationWarehouseCode") String destinationWarehouseCode, @Param("destinationWarehouseCode") String destinationWarehouseCode,
@Param("inboundCode") String inboundCode, @Param("inboundCode") String inboundCode,
@@ -45,8 +45,8 @@ public interface InventoryBalanceMapper extends BaseMapper<InventoryBalance> {
"COALESCE(SUM(available_packages),0) available_packages,COALESCE(SUM(hold_packages),0) hold_packages," + "COALESCE(SUM(available_packages),0) available_packages,COALESCE(SUM(hold_packages),0) hold_packages," +
"COALESCE(SUM(allocated_packages),0) allocated_packages,COALESCE(SUM(picked_packages),0) picked_packages," + "COALESCE(SUM(allocated_packages),0) allocated_packages,COALESCE(SUM(picked_packages),0) picked_packages," +
"COALESCE(SUM(in_transit_packages),0) in_transit_packages,COALESCE(SUM(receiving_packages),0) receiving_packages," + "COALESCE(SUM(in_transit_packages),0) in_transit_packages,COALESCE(SUM(receiving_packages),0) receiving_packages," +
"COALESCE(SUM(procurement_packages),0) procurement_packages FROM ll_inventory_balance WHERE tenant_id=#{tenantId}" + "COALESCE(SUM(procurement_packages),0) procurement_packages FROM ll_inventory_balance WHERE customer_id=#{customerId}" +
"<if test='overseasWarehouseId != null and overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{overseasWarehouseId}</if></script>") "<if test='overseasWarehouseId != null and overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{overseasWarehouseId}</if></script>")
InventorySummary summarize(@Param("tenantId") String tenantId, InventorySummary summarize(@Param("customerId") String customerId,
@Param("overseasWarehouseId") String overseasWarehouseId); @Param("overseasWarehouseId") String overseasWarehouseId);
} }
@@ -9,11 +9,11 @@ import java.time.LocalDateTime;
import java.util.List; import java.util.List;
public interface InventoryLedgerMapper extends BaseMapper<InventoryLedger> { public interface InventoryLedgerMapper extends BaseMapper<InventoryLedger> {
@Select("SELECT * FROM ll_inventory_ledger WHERE tenant_id=#{tenantId} AND idempotency_key=#{idempotencyKey} LIMIT 1") @Select("SELECT * FROM ll_inventory_ledger WHERE customer_id=#{customerId} AND idempotency_key=#{idempotencyKey} LIMIT 1")
InventoryLedger findIdempotency(@Param("tenantId") String tenantId, InventoryLedger findIdempotency(@Param("customerId") String customerId,
@Param("idempotencyKey") String idempotencyKey); @Param("idempotencyKey") String idempotencyKey);
@Select("<script>SELECT * FROM ll_inventory_ledger WHERE tenant_id=#{tenantId}" + @Select("<script>SELECT * FROM ll_inventory_ledger WHERE customer_id=#{customerId}" +
"<if test='overseasWarehouseId != null and overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{overseasWarehouseId}</if>" + "<if test='overseasWarehouseId != null and overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{overseasWarehouseId}</if>" +
"<if test='inboundCode != null and inboundCode != \"\"'> AND inbound_code=#{inboundCode}</if>" + "<if test='inboundCode != null and inboundCode != \"\"'> AND inbound_code=#{inboundCode}</if>" +
"<if test='businessType != null and businessType != \"\"'> AND business_type=#{businessType}</if>" + "<if test='businessType != null and businessType != \"\"'> AND business_type=#{businessType}</if>" +
@@ -21,7 +21,7 @@ public interface InventoryLedgerMapper extends BaseMapper<InventoryLedger> {
"<if test='startedAt != null'> AND occurred_at &gt;=#{startedAt}</if>" + "<if test='startedAt != null'> AND occurred_at &gt;=#{startedAt}</if>" +
"<if test='endedAt != null'> AND occurred_at &lt;#{endedAt}</if>" + "<if test='endedAt != null'> AND occurred_at &lt;#{endedAt}</if>" +
" ORDER BY occurred_at DESC,id DESC LIMIT #{limit} OFFSET #{offset}</script>") " ORDER BY occurred_at DESC,id DESC LIMIT #{limit} OFFSET #{offset}</script>")
List<InventoryLedger> listTenant(@Param("tenantId") String tenantId, List<InventoryLedger> listTenant(@Param("customerId") String customerId,
@Param("overseasWarehouseId") String overseasWarehouseId, @Param("overseasWarehouseId") String overseasWarehouseId,
@Param("inboundCode") String inboundCode, @Param("inboundCode") String inboundCode,
@Param("businessType") String businessType, @Param("businessType") String businessType,
@@ -6,11 +6,11 @@ import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Select;
public interface InventoryOperationMapper extends BaseMapper<InventoryOperation> { public interface InventoryOperationMapper extends BaseMapper<InventoryOperation> {
@Select("SELECT * FROM ll_inventory_operation WHERE tenant_id=#{tenantId} AND idempotency_key=#{idempotencyKey} LIMIT 1") @Select("SELECT * FROM ll_inventory_operation WHERE customer_id=#{customerId} AND idempotency_key=#{idempotencyKey} LIMIT 1")
InventoryOperation findIdempotency(@Param("tenantId") String tenantId, InventoryOperation findIdempotency(@Param("customerId") String customerId,
@Param("idempotencyKey") String idempotencyKey); @Param("idempotencyKey") String idempotencyKey);
@Select("SELECT * FROM ll_inventory_operation WHERE tenant_id=#{tenantId} AND idempotency_key=#{idempotencyKey} LIMIT 1 FOR UPDATE") @Select("SELECT * FROM ll_inventory_operation WHERE customer_id=#{customerId} AND idempotency_key=#{idempotencyKey} LIMIT 1 FOR UPDATE")
InventoryOperation lockIdempotency(@Param("tenantId") String tenantId, InventoryOperation lockIdempotency(@Param("customerId") String customerId,
@Param("idempotencyKey") String idempotencyKey); @Param("idempotencyKey") String idempotencyKey);
} }
@@ -9,18 +9,18 @@ import java.util.List;
public interface InventoryPositionMapper extends BaseMapper<InventoryPosition> { public interface InventoryPositionMapper extends BaseMapper<InventoryPosition> {
@Select("SELECT p.* FROM ll_inventory_position p JOIN ll_inventory_balance b ON b.id=p.balance_id " + @Select("SELECT p.* FROM ll_inventory_position p JOIN ll_inventory_balance b ON b.id=p.balance_id " +
"WHERE b.tenant_id=#{tenantId} AND p.balance_id=#{balanceId} ORDER BY p.pallet_no,p.batch_no") "WHERE b.customer_id=#{customerId} AND p.balance_id=#{balanceId} ORDER BY p.pallet_no,p.batch_no")
List<InventoryPosition> listBalance(@Param("tenantId") String tenantId, List<InventoryPosition> listBalance(@Param("customerId") String customerId,
@Param("balanceId") Long balanceId); @Param("balanceId") Long balanceId);
@Select("SELECT * FROM ll_inventory_position WHERE tenant_id=#{tenantId} AND pallet_id=#{palletId} " + @Select("SELECT * FROM ll_inventory_position WHERE customer_id=#{customerId} AND pallet_id=#{palletId} " +
"ORDER BY balance_id,batch_no") "ORDER BY balance_id,batch_no")
List<InventoryPosition> listPallet(@Param("tenantId") String tenantId, List<InventoryPosition> listPallet(@Param("customerId") String customerId,
@Param("palletId") Long palletId); @Param("palletId") Long palletId);
@Select("SELECT * FROM ll_inventory_position WHERE tenant_id=#{tenantId} AND balance_id=#{balanceId} " + @Select("SELECT * FROM ll_inventory_position WHERE customer_id=#{customerId} AND balance_id=#{balanceId} " +
"AND pallet_id=#{palletId} AND batch_no=#{batchNo} LIMIT 1 FOR UPDATE") "AND pallet_id=#{palletId} AND batch_no=#{batchNo} LIMIT 1 FOR UPDATE")
InventoryPosition lockPosition(@Param("tenantId") String tenantId, InventoryPosition lockPosition(@Param("customerId") String customerId,
@Param("balanceId") Long balanceId, @Param("balanceId") Long balanceId,
@Param("palletId") Long palletId, @Param("palletId") Long palletId,
@Param("batchNo") String batchNo); @Param("batchNo") String batchNo);
@@ -38,55 +38,55 @@ public class InventoryQueryController {
@Operation(summary = "Get inbound-order package inventory totals") @Operation(summary = "Get inbound-order package inventory totals")
public ApiResponse<InventoryDashboardResponse> summary( public ApiResponse<InventoryDashboardResponse> summary(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid InventoryQueryRequests.BalanceQuery query) { @Valid InventoryQueryRequests.BalanceQuery query) {
return ApiResponse.success(service.dashboard(scope(user, tenantId), query)); return ApiResponse.success(service.dashboard(scope(user, customerId), query));
} }
@GetMapping(BASE_PATH + "/balances") @GetMapping(BASE_PATH + "/balances")
@Operation(summary = "Page inventory by customer, warehouse and inbound order") @Operation(summary = "Page inventory by customer, warehouse and inbound order")
public ApiResponse<PageResult<InventoryBalanceView>> balances( public ApiResponse<PageResult<InventoryBalanceView>> balances(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid InventoryQueryRequests.BalanceQuery query) { @Valid InventoryQueryRequests.BalanceQuery query) {
return ApiResponse.success(service.balances(scope(user, tenantId), query)); return ApiResponse.success(service.balances(scope(user, customerId), query));
} }
@GetMapping(BASE_PATH + "/balances/{balanceId}/positions") @GetMapping(BASE_PATH + "/balances/{balanceId}/positions")
@Operation(summary = "List pallet and batch positions for one inbound-order balance") @Operation(summary = "List pallet and batch positions for one inbound-order balance")
public ApiResponse<List<InventoryPositionView>> positions( public ApiResponse<List<InventoryPositionView>> positions(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@PathVariable long balanceId, @PathVariable long balanceId,
@Valid InventoryQueryRequests.PositionQuery query) { @Valid InventoryQueryRequests.PositionQuery query) {
return ApiResponse.success(service.positions(scope(user, tenantId), balanceId, query)); return ApiResponse.success(service.positions(scope(user, customerId), balanceId, query));
} }
@GetMapping(BASE_PATH + "/ledger") @GetMapping(BASE_PATH + "/ledger")
@Operation(summary = "Page append-only inventory movements") @Operation(summary = "Page append-only inventory movements")
public ApiResponse<PageResult<InventoryLedgerView>> ledgers( public ApiResponse<PageResult<InventoryLedgerView>> ledgers(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid InventoryQueryRequests.LedgerQuery query) { @Valid InventoryQueryRequests.LedgerQuery query) {
return ApiResponse.success(service.ledgers(scope(user, tenantId), query)); return ApiResponse.success(service.ledgers(scope(user, customerId), query));
} }
@GetMapping(value = BASE_PATH + "/balances/export", produces = "text/csv") @GetMapping(value = BASE_PATH + "/balances/export", produces = "text/csv")
@Operation(summary = "Export filtered inbound-order inventory as UTF-8 CSV") @Operation(summary = "Export filtered inbound-order inventory as UTF-8 CSV")
public ResponseEntity<byte[]> exportBalances( public ResponseEntity<byte[]> exportBalances(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid InventoryQueryRequests.BalanceQuery query) { @Valid InventoryQueryRequests.BalanceQuery query) {
return csv(service.exportBalances(scope(user, tenantId), query), "inventory-balances.csv"); return csv(service.exportBalances(scope(user, customerId), query), "inventory-balances.csv");
} }
@GetMapping(value = BASE_PATH + "/ledger/export", produces = "text/csv") @GetMapping(value = BASE_PATH + "/ledger/export", produces = "text/csv")
@Operation(summary = "Export filtered inventory movements as UTF-8 CSV") @Operation(summary = "Export filtered inventory movements as UTF-8 CSV")
public ResponseEntity<byte[]> exportLedgers( public ResponseEntity<byte[]> exportLedgers(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @RequestParam(required = false) String customerId,
@Valid InventoryQueryRequests.LedgerQuery query) { @Valid InventoryQueryRequests.LedgerQuery query) {
return csv(service.exportLedgers(scope(user, tenantId), query), "inventory-ledger.csv"); return csv(service.exportLedgers(scope(user, customerId), query), "inventory-ledger.csv");
} }
private static ResponseEntity<byte[]> csv(byte[] body, String filename) { private static ResponseEntity<byte[]> csv(byte[] body, String filename) {
@@ -96,16 +96,16 @@ public class InventoryQueryController {
.body(body); .body(body);
} }
private String scope(AuthenticatedUser user, String requestedTenant) { private String scope(AuthenticatedUser user, String requestedCustomer) {
if (user == null || user.tenantId() == null) { if (user == null || user.tenantId() == null) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
} }
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) { if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
if (requestedTenant == null || requestedTenant.isBlank() || "0".equals(requestedTenant)) { if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Platform queries must specify a customer tenant"); "Platform queries must specify a customer");
} }
return requestedTenant; return requestedCustomer;
} }
return String.valueOf(user.tenantId()); return String.valueOf(user.tenantId());
} }
@@ -11,14 +11,14 @@ import org.apache.ibatis.annotations.Select;
import java.util.List; import java.util.List;
public interface InventoryQueryMapper { public interface InventoryQueryMapper {
String BALANCE_FILTER = " WHERE tenant_id=#{tenantId}" + String BALANCE_FILTER = " WHERE customer_id=#{customerId}" +
"<if test='q.customerName != null and q.customerName != \"\"'> AND customer_name LIKE CONCAT('%',#{q.customerName},'%')</if>" + "<if test='q.customerName != null and q.customerName != \"\"'> AND customer_name LIKE CONCAT('%',#{q.customerName},'%')</if>" +
"<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{q.overseasWarehouseId}</if>" + "<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{q.overseasWarehouseId}</if>" +
"<if test='q.overseasWarehouseCode != null and q.overseasWarehouseCode != \"\"'> AND overseas_warehouse_code=#{q.overseasWarehouseCode}</if>" + "<if test='q.overseasWarehouseCode != null and q.overseasWarehouseCode != \"\"'> AND overseas_warehouse_code=#{q.overseasWarehouseCode}</if>" +
"<if test='q.destinationWarehouseCode != null and q.destinationWarehouseCode != \"\"'> AND destination_warehouse_code=#{q.destinationWarehouseCode}</if>" + "<if test='q.destinationWarehouseCode != null and q.destinationWarehouseCode != \"\"'> AND destination_warehouse_code=#{q.destinationWarehouseCode}</if>" +
"<if test='q.inboundOrderNo != null and q.inboundOrderNo != \"\"'> AND inbound_order_no LIKE CONCAT('%',#{q.inboundOrderNo},'%')</if>" + "<if test='q.inboundOrderNo != null and q.inboundOrderNo != \"\"'> AND inbound_order_no LIKE CONCAT('%',#{q.inboundOrderNo},'%')</if>" +
"<if test='q.inboundCode != null and q.inboundCode != \"\"'> AND inbound_code LIKE CONCAT('%',#{q.inboundCode},'%')</if>" + "<if test='q.inboundCode != null and q.inboundCode != \"\"'> AND inbound_code LIKE CONCAT('%',#{q.inboundCode},'%')</if>" +
"<if test='q.businessOrderNo != null and q.businessOrderNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_ledger l WHERE l.balance_id=ll_inventory_balance.id AND l.tenant_id=#{tenantId} AND l.business_order_no LIKE CONCAT('%',#{q.businessOrderNo},'%'))</if>" + "<if test='q.businessOrderNo != null and q.businessOrderNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_ledger l WHERE l.balance_id=ll_inventory_balance.id AND l.customer_id=#{customerId} AND l.business_order_no LIKE CONCAT('%',#{q.businessOrderNo},'%'))</if>" +
"<choose>" + "<choose>" +
"<when test='q.inventoryStatus == \"AVAILABLE\"'> AND available_packages &gt; 0</when>" + "<when test='q.inventoryStatus == \"AVAILABLE\"'> AND available_packages &gt; 0</when>" +
"<when test='q.inventoryStatus == \"HOLD\"'> AND hold_packages &gt; 0</when>" + "<when test='q.inventoryStatus == \"HOLD\"'> AND hold_packages &gt; 0</when>" +
@@ -29,14 +29,14 @@ public interface InventoryQueryMapper {
"<when test='q.inventoryStatus == \"PROCUREMENT\"'> AND procurement_packages &gt; 0</when>" + "<when test='q.inventoryStatus == \"PROCUREMENT\"'> AND procurement_packages &gt; 0</when>" +
"</choose>"; "</choose>";
String JOINED_BALANCE_FILTER = " WHERE b.tenant_id=#{tenantId}" + String JOINED_BALANCE_FILTER = " WHERE b.customer_id=#{customerId}" +
"<if test='q.customerName != null and q.customerName != \"\"'> AND b.customer_name LIKE CONCAT('%',#{q.customerName},'%')</if>" + "<if test='q.customerName != null and q.customerName != \"\"'> AND b.customer_name LIKE CONCAT('%',#{q.customerName},'%')</if>" +
"<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND b.overseas_warehouse_id=#{q.overseasWarehouseId}</if>" + "<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND b.overseas_warehouse_id=#{q.overseasWarehouseId}</if>" +
"<if test='q.overseasWarehouseCode != null and q.overseasWarehouseCode != \"\"'> AND b.overseas_warehouse_code=#{q.overseasWarehouseCode}</if>" + "<if test='q.overseasWarehouseCode != null and q.overseasWarehouseCode != \"\"'> AND b.overseas_warehouse_code=#{q.overseasWarehouseCode}</if>" +
"<if test='q.destinationWarehouseCode != null and q.destinationWarehouseCode != \"\"'> AND b.destination_warehouse_code=#{q.destinationWarehouseCode}</if>" + "<if test='q.destinationWarehouseCode != null and q.destinationWarehouseCode != \"\"'> AND b.destination_warehouse_code=#{q.destinationWarehouseCode}</if>" +
"<if test='q.inboundOrderNo != null and q.inboundOrderNo != \"\"'> AND b.inbound_order_no LIKE CONCAT('%',#{q.inboundOrderNo},'%')</if>" + "<if test='q.inboundOrderNo != null and q.inboundOrderNo != \"\"'> AND b.inbound_order_no LIKE CONCAT('%',#{q.inboundOrderNo},'%')</if>" +
"<if test='q.inboundCode != null and q.inboundCode != \"\"'> AND b.inbound_code LIKE CONCAT('%',#{q.inboundCode},'%')</if>" + "<if test='q.inboundCode != null and q.inboundCode != \"\"'> AND b.inbound_code LIKE CONCAT('%',#{q.inboundCode},'%')</if>" +
"<if test='q.businessOrderNo != null and q.businessOrderNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_ledger l WHERE l.balance_id=b.id AND l.tenant_id=#{tenantId} AND l.business_order_no LIKE CONCAT('%',#{q.businessOrderNo},'%'))</if>" + "<if test='q.businessOrderNo != null and q.businessOrderNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_ledger l WHERE l.balance_id=b.id AND l.customer_id=#{customerId} AND l.business_order_no LIKE CONCAT('%',#{q.businessOrderNo},'%'))</if>" +
"<choose>" + "<choose>" +
"<when test='q.inventoryStatus == \"AVAILABLE\"'> AND b.available_packages &gt; 0</when>" + "<when test='q.inventoryStatus == \"AVAILABLE\"'> AND b.available_packages &gt; 0</when>" +
"<when test='q.inventoryStatus == \"HOLD\"'> AND b.hold_packages &gt; 0</when>" + "<when test='q.inventoryStatus == \"HOLD\"'> AND b.hold_packages &gt; 0</when>" +
@@ -49,13 +49,13 @@ public interface InventoryQueryMapper {
@Select("<script>SELECT * FROM ll_inventory_balance" + BALANCE_FILTER + @Select("<script>SELECT * FROM ll_inventory_balance" + BALANCE_FILTER +
" ORDER BY updated_at DESC,id DESC LIMIT #{limit} OFFSET #{offset}</script>") " ORDER BY updated_at DESC,id DESC LIMIT #{limit} OFFSET #{offset}</script>")
List<InventoryBalance> balances(@Param("tenantId") String tenantId, List<InventoryBalance> balances(@Param("customerId") String customerId,
@Param("q") InventoryQueryRequests.BalanceQuery query, @Param("q") InventoryQueryRequests.BalanceQuery query,
@Param("offset") long offset, @Param("offset") long offset,
@Param("limit") long limit); @Param("limit") long limit);
@Select("<script>SELECT COUNT(*) FROM ll_inventory_balance" + BALANCE_FILTER + "</script>") @Select("<script>SELECT COUNT(*) FROM ll_inventory_balance" + BALANCE_FILTER + "</script>")
long countBalances(@Param("tenantId") String tenantId, long countBalances(@Param("customerId") String customerId,
@Param("q") InventoryQueryRequests.BalanceQuery query); @Param("q") InventoryQueryRequests.BalanceQuery query);
@Select("<script>SELECT COUNT(*) dimension_count," + @Select("<script>SELECT COUNT(*) dimension_count," +
@@ -67,16 +67,16 @@ public interface InventoryQueryMapper {
"COALESCE(SUM(receiving_packages),0) receiving_packages," + "COALESCE(SUM(receiving_packages),0) receiving_packages," +
"COALESCE(SUM(procurement_packages),0) procurement_packages " + "COALESCE(SUM(procurement_packages),0) procurement_packages " +
"FROM ll_inventory_balance" + BALANCE_FILTER + "</script>") "FROM ll_inventory_balance" + BALANCE_FILTER + "</script>")
InventoryAggregateProjection aggregate(@Param("tenantId") String tenantId, InventoryAggregateProjection aggregate(@Param("customerId") String customerId,
@Param("q") InventoryQueryRequests.BalanceQuery query); @Param("q") InventoryQueryRequests.BalanceQuery query);
@Select("<script>SELECT COUNT(DISTINCT p.pallet_id) FROM ll_inventory_position p " + @Select("<script>SELECT COUNT(DISTINCT p.pallet_id) FROM ll_inventory_position p " +
"JOIN ll_inventory_balance b ON b.id=p.balance_id" + "JOIN ll_inventory_balance b ON b.id=p.balance_id" +
JOINED_BALANCE_FILTER + "</script>") JOINED_BALANCE_FILTER + "</script>")
long countPallets(@Param("tenantId") String tenantId, long countPallets(@Param("customerId") String customerId,
@Param("q") InventoryQueryRequests.BalanceQuery query); @Param("q") InventoryQueryRequests.BalanceQuery query);
@Select("<script>SELECT * FROM ll_inventory_position WHERE tenant_id=#{tenantId} AND balance_id=#{balanceId}" + @Select("<script>SELECT * FROM ll_inventory_position WHERE customer_id=#{customerId} AND balance_id=#{balanceId}" +
"<if test='q.palletNo != null and q.palletNo != \"\"'> AND pallet_no LIKE CONCAT('%',#{q.palletNo},'%')</if>" + "<if test='q.palletNo != null and q.palletNo != \"\"'> AND pallet_no LIKE CONCAT('%',#{q.palletNo},'%')</if>" +
"<if test='q.batchNo != null and q.batchNo != \"\"'> AND batch_no LIKE CONCAT('%',#{q.batchNo},'%')</if>" + "<if test='q.batchNo != null and q.batchNo != \"\"'> AND batch_no LIKE CONCAT('%',#{q.batchNo},'%')</if>" +
"<choose>" + "<choose>" +
@@ -86,14 +86,14 @@ public interface InventoryQueryMapper {
"<when test='q.inventoryStatus == \"PICKED\"'> AND picked_packages &gt; 0</when>" + "<when test='q.inventoryStatus == \"PICKED\"'> AND picked_packages &gt; 0</when>" +
"<when test='q.inventoryStatus == \"RECEIVING\"'> AND receiving_packages &gt; 0</when>" + "<when test='q.inventoryStatus == \"RECEIVING\"'> AND receiving_packages &gt; 0</when>" +
"</choose> ORDER BY pallet_no,batch_no,id</script>") "</choose> ORDER BY pallet_no,batch_no,id</script>")
List<InventoryPosition> positions(@Param("tenantId") String tenantId, List<InventoryPosition> positions(@Param("customerId") String customerId,
@Param("balanceId") long balanceId, @Param("balanceId") long balanceId,
@Param("q") InventoryQueryRequests.PositionQuery query); @Param("q") InventoryQueryRequests.PositionQuery query);
@Select("SELECT * FROM ll_inventory_balance WHERE tenant_id=#{tenantId} AND id=#{balanceId} LIMIT 1") @Select("SELECT * FROM ll_inventory_balance WHERE customer_id=#{customerId} AND id=#{balanceId} LIMIT 1")
InventoryBalance ownedBalance(@Param("tenantId") String tenantId, @Param("balanceId") long balanceId); InventoryBalance ownedBalance(@Param("customerId") String customerId, @Param("balanceId") long balanceId);
String LEDGER_FILTER = " WHERE tenant_id=#{tenantId}" + String LEDGER_FILTER = " WHERE customer_id=#{customerId}" +
"<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{q.overseasWarehouseId}</if>" + "<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{q.overseasWarehouseId}</if>" +
"<if test='q.destinationWarehouseCode != null and q.destinationWarehouseCode != \"\"'> AND destination_warehouse_code=#{q.destinationWarehouseCode}</if>" + "<if test='q.destinationWarehouseCode != null and q.destinationWarehouseCode != \"\"'> AND destination_warehouse_code=#{q.destinationWarehouseCode}</if>" +
"<if test='q.businessType != null and q.businessType != \"\"'> AND business_type=#{q.businessType}</if>" + "<if test='q.businessType != null and q.businessType != \"\"'> AND business_type=#{q.businessType}</if>" +
@@ -108,12 +108,12 @@ public interface InventoryQueryMapper {
@Select("<script>SELECT * FROM ll_inventory_ledger" + LEDGER_FILTER + @Select("<script>SELECT * FROM ll_inventory_ledger" + LEDGER_FILTER +
" ORDER BY occurred_at DESC,id DESC LIMIT #{limit} OFFSET #{offset}</script>") " ORDER BY occurred_at DESC,id DESC LIMIT #{limit} OFFSET #{offset}</script>")
List<InventoryLedger> ledgers(@Param("tenantId") String tenantId, List<InventoryLedger> ledgers(@Param("customerId") String customerId,
@Param("q") InventoryQueryRequests.LedgerQuery query, @Param("q") InventoryQueryRequests.LedgerQuery query,
@Param("offset") long offset, @Param("offset") long offset,
@Param("limit") long limit); @Param("limit") long limit);
@Select("<script>SELECT COUNT(*) FROM ll_inventory_ledger" + LEDGER_FILTER + "</script>") @Select("<script>SELECT COUNT(*) FROM ll_inventory_ledger" + LEDGER_FILTER + "</script>")
long countLedgers(@Param("tenantId") String tenantId, long countLedgers(@Param("customerId") String customerId,
@Param("q") InventoryQueryRequests.LedgerQuery query); @Param("q") InventoryQueryRequests.LedgerQuery query);
} }
@@ -31,9 +31,9 @@ public class InventoryQueryService {
@Resource private InventoryQueryMapper mapper; @Resource private InventoryQueryMapper mapper;
public InventoryDashboardResponse dashboard(String tenantId, InventoryQueryRequests.BalanceQuery query) { public InventoryDashboardResponse dashboard(String customerId, InventoryQueryRequests.BalanceQuery query) {
validateStatus(query.inventoryStatus(), BALANCE_STATUSES); validateStatus(query.inventoryStatus(), BALANCE_STATUSES);
InventoryAggregateProjection value = mapper.aggregate(tenantId, query); InventoryAggregateProjection value = mapper.aggregate(customerId, query);
long availablePackages = number(value.getAvailablePackages()); long availablePackages = number(value.getAvailablePackages());
long holdPackages = number(value.getHoldPackages()); long holdPackages = number(value.getHoldPackages());
long allocatedPackages = number(value.getAllocatedPackages()); long allocatedPackages = number(value.getAllocatedPackages());
@@ -41,46 +41,46 @@ public class InventoryQueryService {
long inTransitPackages = number(value.getInTransitPackages()); long inTransitPackages = number(value.getInTransitPackages());
long receivingPackages = number(value.getReceivingPackages()); long receivingPackages = number(value.getReceivingPackages());
long procurementPackages = number(value.getProcurementPackages()); long procurementPackages = number(value.getProcurementPackages());
return new InventoryDashboardResponse(number(value.getDimensionCount()), mapper.countPallets(tenantId, query), return new InventoryDashboardResponse(number(value.getDimensionCount()), mapper.countPallets(customerId, query),
availablePackages + holdPackages + allocatedPackages + pickedPackages + inTransitPackages availablePackages + holdPackages + allocatedPackages + pickedPackages + inTransitPackages
+ receivingPackages + procurementPackages, + receivingPackages + procurementPackages,
availablePackages, holdPackages, allocatedPackages, pickedPackages, inTransitPackages, availablePackages, holdPackages, allocatedPackages, pickedPackages, inTransitPackages,
receivingPackages, procurementPackages); receivingPackages, procurementPackages);
} }
public PageResult<InventoryBalanceView> balances(String tenantId, InventoryQueryRequests.BalanceQuery query) { public PageResult<InventoryBalanceView> balances(String customerId, InventoryQueryRequests.BalanceQuery query) {
validateStatus(query.inventoryStatus(), BALANCE_STATUSES); validateStatus(query.inventoryStatus(), BALANCE_STATUSES);
long total = mapper.countBalances(tenantId, query); long total = mapper.countBalances(customerId, query);
List<InventoryBalanceView> records = total == 0 ? List.of() : mapper.balances( List<InventoryBalanceView> records = total == 0 ? List.of() : mapper.balances(
tenantId, query, offset(query.page(), query.size()), query.size()).stream().map(this::balanceView).toList(); customerId, query, offset(query.page(), query.size()), query.size()).stream().map(this::balanceView).toList();
return page(records, query.page(), query.size(), total); return page(records, query.page(), query.size(), total);
} }
public List<InventoryPositionView> positions(String tenantId, long balanceId, public List<InventoryPositionView> positions(String customerId, long balanceId,
InventoryQueryRequests.PositionQuery query) { InventoryQueryRequests.PositionQuery query) {
validateStatus(query.inventoryStatus(), POSITION_STATUSES); validateStatus(query.inventoryStatus(), POSITION_STATUSES);
InventoryBalance balance = mapper.ownedBalance(tenantId, balanceId); InventoryBalance balance = mapper.ownedBalance(customerId, balanceId);
if (balance == null) { if (balance == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inventory balance not found"); throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inventory balance not found");
} }
return mapper.positions(tenantId, balanceId, query).stream().map(value -> positionView(value, balance)).toList(); return mapper.positions(customerId, balanceId, query).stream().map(value -> positionView(value, balance)).toList();
} }
public PageResult<InventoryLedgerView> ledgers(String tenantId, InventoryQueryRequests.LedgerQuery query) { public PageResult<InventoryLedgerView> ledgers(String customerId, InventoryQueryRequests.LedgerQuery query) {
validateDateRange(query); validateDateRange(query);
long total = mapper.countLedgers(tenantId, query); long total = mapper.countLedgers(customerId, query);
List<InventoryLedgerView> records = total == 0 ? List.of() : mapper.ledgers( List<InventoryLedgerView> records = total == 0 ? List.of() : mapper.ledgers(
tenantId, query, offset(query.page(), query.size()), query.size()).stream().map(this::ledgerView).toList(); customerId, query, offset(query.page(), query.size()), query.size()).stream().map(this::ledgerView).toList();
return page(records, query.page(), query.size(), total); return page(records, query.page(), query.size(), total);
} }
public byte[] exportBalances(String tenantId, InventoryQueryRequests.BalanceQuery query) { public byte[] exportBalances(String customerId, InventoryQueryRequests.BalanceQuery query) {
validateStatus(query.inventoryStatus(), BALANCE_STATUSES); validateStatus(query.inventoryStatus(), BALANCE_STATUSES);
List<String> rows = new ArrayList<>(); List<String> rows = new ArrayList<>();
rows.add("customerId,customerName,overseasWarehouseCode,overseasWarehouseName,destinationWarehouseCode," + rows.add("customerId,customerName,overseasWarehouseCode,overseasWarehouseName,destinationWarehouseCode," +
"destinationWarehouseName,inboundOrderId,inboundOrderNo,inboundCode,totalPackages,availablePackages," + "destinationWarehouseName,inboundOrderId,inboundOrderNo,inboundCode,totalPackages,availablePackages," +
"holdPackages,allocatedPackages,pickedPackages,inTransitPackages,receivingPackages,procurementPackages"); "holdPackages,allocatedPackages,pickedPackages,inTransitPackages,receivingPackages,procurementPackages");
mapper.balances(tenantId, query, 0, EXPORT_LIMIT).stream().map(this::balanceView).forEach(v -> rows.add( mapper.balances(customerId, query, 0, EXPORT_LIMIT).stream().map(this::balanceView).forEach(v -> rows.add(
String.join(",", csv(v.customerId()), csv(v.customerName()), csv(v.overseasWarehouseCode()), csv(v.overseasWarehouseName()), String.join(",", csv(v.customerId()), csv(v.customerName()), csv(v.overseasWarehouseCode()), csv(v.overseasWarehouseName()),
csv(v.destinationWarehouseCode()), csv(v.destinationWarehouseName()), csv(v.inboundOrderId()), csv(v.destinationWarehouseCode()), csv(v.destinationWarehouseName()), csv(v.inboundOrderId()),
csv(v.inboundOrderNo()), csv(v.inboundCode()), csv(v.inboundOrderNo()), csv(v.inboundCode()),
@@ -90,13 +90,13 @@ public class InventoryQueryService {
return ("\uFEFF" + String.join("\r\n", rows)).getBytes(StandardCharsets.UTF_8); return ("\uFEFF" + String.join("\r\n", rows)).getBytes(StandardCharsets.UTF_8);
} }
public byte[] exportLedgers(String tenantId, InventoryQueryRequests.LedgerQuery query) { public byte[] exportLedgers(String customerId, InventoryQueryRequests.LedgerQuery query) {
validateDateRange(query); validateDateRange(query);
List<String> rows = new ArrayList<>(); List<String> rows = new ArrayList<>();
rows.add("occurredAt,customerId,overseasWarehouseId,destinationWarehouseCode,inboundOrderId,inboundOrderNo," + rows.add("occurredAt,customerId,overseasWarehouseId,destinationWarehouseCode,inboundOrderId,inboundOrderNo," +
"inboundCode,businessType,businessOrderNo,outboundOrderNo,palletNo,batchNo,statusFrom,statusTo," + "inboundCode,businessType,businessOrderNo,outboundOrderNo,palletNo,batchNo,statusFrom,statusTo," +
"beforePackages,deltaPackages,afterPackages,operatorId,remark"); "beforePackages,deltaPackages,afterPackages,operatorId,remark");
mapper.ledgers(tenantId, query, 0, EXPORT_LIMIT).stream().map(this::ledgerView).forEach(v -> rows.add( mapper.ledgers(customerId, query, 0, EXPORT_LIMIT).stream().map(this::ledgerView).forEach(v -> rows.add(
String.join(",", csv(v.occurredAt()), csv(v.customerId()), csv(v.overseasWarehouseId()), String.join(",", csv(v.occurredAt()), csv(v.customerId()), csv(v.overseasWarehouseId()),
csv(v.destinationWarehouseCode()), csv(v.inboundOrderId()), csv(v.inboundOrderNo()), csv(v.inboundCode()), csv(v.destinationWarehouseCode()), csv(v.inboundOrderId()), csv(v.inboundOrderNo()), csv(v.inboundCode()),
csv(v.businessType()), csv(v.businessOrderNo()), csv(v.outboundOrderNo()), csv(v.palletNo()), csv(v.batchNo()), csv(v.businessType()), csv(v.businessOrderNo()), csv(v.outboundOrderNo()), csv(v.palletNo()), csv(v.batchNo()),
@@ -113,7 +113,7 @@ public class InventoryQueryService {
long inTransitPackages = number(value.getInTransitPackages()); long inTransitPackages = number(value.getInTransitPackages());
long receivingPackages = number(value.getReceivingPackages()); long receivingPackages = number(value.getReceivingPackages());
long procurementPackages = number(value.getProcurementPackages()); long procurementPackages = number(value.getProcurementPackages());
return new InventoryBalanceView(value.getId(), value.getTenantId(), value.getCustomerName(), value.getOverseasWarehouseId(), return new InventoryBalanceView(value.getId(), value.getCustomerId(), value.getCustomerName(), value.getOverseasWarehouseId(),
value.getOverseasWarehouseCode(), value.getOverseasWarehouseName(), value.getDestinationWarehouseId(), value.getOverseasWarehouseCode(), value.getOverseasWarehouseName(), value.getDestinationWarehouseId(),
value.getDestinationWarehouseCode(), value.getDestinationWarehouseName(), value.getInboundOrderId(), value.getDestinationWarehouseCode(), value.getDestinationWarehouseName(), value.getInboundOrderId(),
value.getInboundOrderNo(), value.getInboundCode(), availablePackages + holdPackages + allocatedPackages + pickedPackages value.getInboundOrderNo(), value.getInboundCode(), availablePackages + holdPackages + allocatedPackages + pickedPackages
@@ -128,7 +128,7 @@ public class InventoryQueryService {
long allocated = number(value.getAllocatedPackages()); long allocated = number(value.getAllocatedPackages());
long picked = number(value.getPickedPackages()); long picked = number(value.getPickedPackages());
long receiving = number(value.getReceivingPackages()); long receiving = number(value.getReceivingPackages());
return new InventoryPositionView(value.getId(), value.getBalanceId(), value.getTenantId(), return new InventoryPositionView(value.getId(), value.getBalanceId(), value.getCustomerId(),
value.getOverseasWarehouseId(), value.getDestinationWarehouseCode(), balance.getInboundOrderId(), value.getOverseasWarehouseId(), value.getDestinationWarehouseCode(), balance.getInboundOrderId(),
balance.getInboundOrderNo(), balance.getInboundCode(), balance.getInboundOrderNo(), balance.getInboundCode(),
value.getPalletId(), value.getPalletNo(), value.getBatchNo(), value.getWarehouseLocationId(), value.getPalletId(), value.getPalletNo(), value.getBatchNo(), value.getWarehouseLocationId(),
@@ -138,7 +138,7 @@ public class InventoryQueryService {
} }
private InventoryLedgerView ledgerView(InventoryLedger value) { private InventoryLedgerView ledgerView(InventoryLedger value) {
return new InventoryLedgerView(value.getId(), value.getTenantId(), value.getOverseasWarehouseId(), return new InventoryLedgerView(value.getId(), value.getCustomerId(), value.getOverseasWarehouseId(),
value.getDestinationWarehouseCode(), value.getInboundOrderId(), value.getInboundOrderNo(), value.getInboundCode(), value.getDestinationWarehouseCode(), value.getInboundOrderId(), value.getInboundOrderNo(), value.getInboundCode(),
value.getPalletNo(), value.getBatchNo(), value.getPalletNo(), value.getBatchNo(),
value.getWarehouseLocationCode(), value.getBusinessType(), value.getBusinessOrderNo(), value.getWarehouseLocationCode(), value.getBusinessType(), value.getBusinessOrderNo(),
@@ -9,30 +9,30 @@ import org.apache.ibatis.annotations.Update;
@Mapper @Mapper
public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> { public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
@Select("SELECT * FROM ll_order WHERE tenant_id=#{tenantId} AND source_region=#{sourceRegion} " + @Select("SELECT * FROM ll_order WHERE customer_id=#{customerId} AND source_region=#{sourceRegion} " +
"AND source_inbound_id=#{sourceInboundId} LIMIT 1") "AND source_inbound_id=#{sourceInboundId} LIMIT 1")
LastlegOrder findSource(@Param("tenantId") String tenantId, LastlegOrder findSource(@Param("customerId") String customerId,
@Param("sourceRegion") String sourceRegion, @Param("sourceRegion") String sourceRegion,
@Param("sourceInboundId") String sourceInboundId); @Param("sourceInboundId") String sourceInboundId);
@Select("SELECT COUNT(*) FROM ll_order WHERE tenant_id=#{tenantId} AND container_no=#{containerNo}") @Select("SELECT COUNT(*) FROM ll_order WHERE customer_id=#{customerId} AND container_no=#{containerNo}")
long countContainer(@Param("tenantId") String tenantId, @Param("containerNo") String containerNo); long countContainer(@Param("customerId") String customerId, @Param("containerNo") String containerNo);
@Select("SELECT * FROM ll_order WHERE tenant_id=#{tenantId} AND id=#{id} FOR UPDATE") @Select("SELECT * FROM ll_order WHERE customer_id=#{customerId} AND id=#{id} FOR UPDATE")
LastlegOrder lockById(@Param("tenantId") String tenantId, @Param("id") Long id); LastlegOrder lockById(@Param("customerId") String customerId, @Param("id") Long id);
@Update("UPDATE ll_order SET picked_package_count=picked_package_count+#{packages},updated_at=NOW() " + @Update("UPDATE ll_order SET picked_package_count=picked_package_count+#{packages},updated_at=NOW() " +
"WHERE tenant_id=#{tenantId} AND id=#{id} " + "WHERE customer_id=#{customerId} AND id=#{id} " +
"AND package_count-shipped_package_count-picked_package_count >= #{packages}") "AND package_count-shipped_package_count-picked_package_count >= #{packages}")
int increasePicked(@Param("tenantId") String tenantId, @Param("id") Long id, int increasePicked(@Param("customerId") String customerId, @Param("id") Long id,
@Param("packages") long packages); @Param("packages") long packages);
@Update("UPDATE ll_order SET picked_package_count=picked_package_count-#{packages}," + @Update("UPDATE ll_order SET picked_package_count=picked_package_count-#{packages}," +
"shipped_package_count=shipped_package_count+#{packages},outbound_order_no=#{outboundNo}," + "shipped_package_count=shipped_package_count+#{packages},outbound_order_no=#{outboundNo}," +
"current_status=CASE WHEN shipped_package_count+#{packages}=package_count " + "current_status=CASE WHEN shipped_package_count+#{packages}=package_count " +
"THEN 'COMPLETED' ELSE 'PARTIALLY_SHIPPED' END,updated_at=NOW() " + "THEN 'COMPLETED' ELSE 'PARTIALLY_SHIPPED' END,updated_at=NOW() " +
"WHERE tenant_id=#{tenantId} AND id=#{id} AND picked_package_count >= #{packages} " + "WHERE customer_id=#{customerId} AND id=#{id} AND picked_package_count >= #{packages} " +
"AND shipped_package_count+#{packages} <= package_count") "AND shipped_package_count+#{packages} <= package_count")
int transferPickedToShipped(@Param("tenantId") String tenantId, @Param("id") Long id, int transferPickedToShipped(@Param("customerId") String customerId, @Param("id") Long id,
@Param("packages") long packages, @Param("outboundNo") String outboundNo); @Param("packages") long packages, @Param("outboundNo") String outboundNo);
} }
@@ -20,24 +20,24 @@ public class RelabelApplicationController {
@PostMapping @PostMapping
public ApiResponse<RelabelResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<RelabelResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @Valid @RequestBody RelabelRequests.Create request) { @RequestParam(required = false) String customerId, @Valid @RequestBody RelabelRequests.Create request) {
return ApiResponse.success(service.create(scope(user, tenantId), operator(user), request)); return ApiResponse.success(service.create(scope(user, customerId), operator(user), request));
} }
@GetMapping @GetMapping
public ApiResponse<PageResult<RelabelResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<PageResult<RelabelResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @Valid RelabelRequests.Query query) { @RequestParam(required = false) String customerId, @Valid RelabelRequests.Query query) {
return ApiResponse.success(service.page(scope(user, tenantId), query)); return ApiResponse.success(service.page(scope(user, customerId), query));
} }
@GetMapping("/{relabelNo}") @GetMapping("/{relabelNo}")
public ApiResponse<RelabelResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<RelabelResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @PathVariable String relabelNo) { @RequestParam(required = false) String customerId, @PathVariable String relabelNo) {
return ApiResponse.success(service.detail(scope(user, tenantId), relabelNo)); return ApiResponse.success(service.detail(scope(user, customerId), relabelNo));
} }
@PutMapping("/{relabelNo}/status") @PutMapping("/{relabelNo}/status")
public ApiResponse<RelabelResponses.Detail> status(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<RelabelResponses.Detail> status(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String tenantId, @PathVariable String relabelNo, @RequestParam(required = false) String customerId, @PathVariable String relabelNo,
@Valid @RequestBody RelabelRequests.ChangeStatus request) { @Valid @RequestBody RelabelRequests.ChangeStatus request) {
return ApiResponse.success(service.changeStatus(scope(user, tenantId), relabelNo, operator(user), request)); return ApiResponse.success(service.changeStatus(scope(user, customerId), relabelNo, operator(user), request));
} }
private String scope(AuthenticatedUser user, String requested) { private String scope(AuthenticatedUser user, String requested) {
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
@@ -11,7 +11,7 @@ import java.time.LocalDateTime;
@TableName("ll_relabel_application") @TableName("ll_relabel_application")
public class RelabelApplication { public class RelabelApplication {
@TableId(type = IdType.INPUT) private Long id; @TableId(type = IdType.INPUT) private Long id;
private String tenantId; private String customerId;
private String relabelNo; private String relabelNo;
private Long lastlegOrderId; private Long lastlegOrderId;
private String customerOrderNo; private String customerOrderNo;
@@ -12,7 +12,7 @@ import java.time.LocalDateTime;
public class RelabelItem { public class RelabelItem {
@TableId(type = IdType.INPUT) private Long id; @TableId(type = IdType.INPUT) private Long id;
private Long applicationId; private Long applicationId;
private String tenantId; private String customerId;
private String skuCode; private String skuCode;
private String productImageFileId; private String productImageFileId;
private String productImageFileName; private String productImageFileName;
@@ -11,7 +11,7 @@ import java.time.LocalDateTime;
@TableName("ll_relabel_status_history") @TableName("ll_relabel_status_history")
public class RelabelStatusHistory { public class RelabelStatusHistory {
@TableId(type = IdType.INPUT) private Long id; @TableId(type = IdType.INPUT) private Long id;
private String tenantId; private String customerId;
private Long applicationId; private Long applicationId;
private String fromStatus; private String fromStatus;
private String toStatus; private String toStatus;
@@ -8,12 +8,12 @@ import org.apache.ibatis.annotations.Select;
@Mapper @Mapper
public interface RelabelApplicationMapper extends BaseMapper<RelabelApplication> { public interface RelabelApplicationMapper extends BaseMapper<RelabelApplication> {
@Select("SELECT * FROM ll_relabel_application WHERE tenant_id=#{tenantId} AND relabel_no=#{relabelNo} LIMIT 1") @Select("SELECT * FROM ll_relabel_application WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo} LIMIT 1")
RelabelApplication findNo(@Param("tenantId") String tenantId, @Param("relabelNo") String relabelNo); RelabelApplication findNo(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo);
@Select("SELECT * FROM ll_relabel_application WHERE tenant_id=#{tenantId} AND idempotency_key=#{key} LIMIT 1") @Select("SELECT * FROM ll_relabel_application WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
RelabelApplication findKey(@Param("tenantId") String tenantId, @Param("key") String key); RelabelApplication findKey(@Param("customerId") String customerId, @Param("key") String key);
@Select("SELECT * FROM ll_relabel_application WHERE tenant_id=#{tenantId} AND relabel_no=#{relabelNo} LIMIT 1 FOR UPDATE") @Select("SELECT * FROM ll_relabel_application WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo} LIMIT 1 FOR UPDATE")
RelabelApplication lockNo(@Param("tenantId") String tenantId, @Param("relabelNo") String relabelNo); RelabelApplication lockNo(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo);
} }
@@ -49,7 +49,7 @@ public class RelabelApplicationService {
@Resource private ObjectMapper objectMapper; @Resource private ObjectMapper objectMapper;
@Transactional @Transactional
public RelabelResponses.Detail create(String tenantId, String operatorId, RelabelRequests.Create request) { public RelabelResponses.Detail create(String customerId, String operatorId, RelabelRequests.Create request) {
int total = 0; int total = 0;
int notified = 0; int notified = 0;
for (RelabelRequests.Item item : request.items()) { for (RelabelRequests.Item item : request.items()) {
@@ -60,10 +60,10 @@ public class RelabelApplicationService {
notified = Math.addExact(notified, item.notifiedPackageCount()); notified = Math.addExact(notified, item.notifiedPackageCount());
} }
String requestHash = hash(request); String requestHash = hash(request);
RelabelApplication replay = applications.findKey(tenantId, request.idempotencyKey().trim()); RelabelApplication replay = applications.findKey(customerId, request.idempotencyKey().trim());
if (replay != null) { if (replay != null) {
if (!requestHash.equals(replay.getRequestHash())) throw conflict("Idempotency key was reused with different relabel data"); if (!requestHash.equals(replay.getRequestHash())) throw conflict("Idempotency key was reused with different relabel data");
return detail(tenantId, replay.getRelabelNo()); return detail(customerId, replay.getRelabelNo());
} }
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
long applicationId = IdWorker.getId(); long applicationId = IdWorker.getId();
@@ -71,7 +71,7 @@ public class RelabelApplicationService {
LastlegOrder order = new LastlegOrder(); LastlegOrder order = new LastlegOrder();
order.setId(IdWorker.getId()); order.setId(IdWorker.getId());
order.setOrderNo(orderNoGenerator.next()); order.setOrderNo(orderNoGenerator.next());
order.setTenantId(tenantId); order.setCustomerId(customerId);
order.setOrderType("RELABEL_APPLICATION"); order.setOrderType("RELABEL_APPLICATION");
order.setInboundCode(relabelNo); order.setInboundCode(relabelNo);
order.setSourceRegion("CUSTOMER"); order.setSourceRegion("CUSTOMER");
@@ -95,7 +95,7 @@ public class RelabelApplicationService {
RelabelApplication application = new RelabelApplication(); RelabelApplication application = new RelabelApplication();
application.setId(applicationId); application.setId(applicationId);
application.setTenantId(tenantId); application.setCustomerId(customerId);
application.setRelabelNo(relabelNo); application.setRelabelNo(relabelNo);
application.setLastlegOrderId(order.getId()); application.setLastlegOrderId(order.getId());
application.setCustomerOrderNo(request.customerOrderNo().trim()); application.setCustomerOrderNo(request.customerOrderNo().trim());
@@ -115,19 +115,19 @@ public class RelabelApplicationService {
application.setUpdatedAt(now); application.setUpdatedAt(now);
applications.insert(application); applications.insert(application);
for (RelabelRequests.Item source : request.items()) insertItem(tenantId, applicationId, source, now); for (RelabelRequests.Item source : request.items()) insertItem(customerId, applicationId, source, now);
insertHistory(tenantId, applicationId, null, PENDING_PROCESSING, operatorId, "Relabel application submitted", now); insertHistory(customerId, applicationId, null, PENDING_PROCESSING, operatorId, "Relabel application submitted", now);
insertOrderHistory(order.getId(), null, PENDING_PROCESSING, "Relabel application submitted", now); insertOrderHistory(order.getId(), null, PENDING_PROCESSING, "Relabel application submitted", now);
return detail(tenantId, relabelNo); return detail(customerId, relabelNo);
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
public PageResult<RelabelResponses.Summary> page(String tenantId, RelabelRequests.Query query) { public PageResult<RelabelResponses.Summary> page(String customerId, RelabelRequests.Query query) {
if (query.createdFrom() != null && query.createdTo() != null && query.createdFrom().isAfter(query.createdTo())) { if (query.createdFrom() != null && query.createdTo() != null && query.createdFrom().isAfter(query.createdTo())) {
throw badRequest("createdFrom must not be after createdTo"); throw badRequest("createdFrom must not be after createdTo");
} }
LambdaQueryWrapper<RelabelApplication> wrapper = new LambdaQueryWrapper<RelabelApplication>() LambdaQueryWrapper<RelabelApplication> wrapper = new LambdaQueryWrapper<RelabelApplication>()
.eq(RelabelApplication::getTenantId, tenantId) .eq(RelabelApplication::getCustomerId, customerId)
.like(StringUtils.hasText(query.relabelNo()), RelabelApplication::getRelabelNo, query.relabelNo()) .like(StringUtils.hasText(query.relabelNo()), RelabelApplication::getRelabelNo, query.relabelNo())
.like(StringUtils.hasText(query.customerOrderNo()), RelabelApplication::getCustomerOrderNo, query.customerOrderNo()) .like(StringUtils.hasText(query.customerOrderNo()), RelabelApplication::getCustomerOrderNo, query.customerOrderNo())
.eq(StringUtils.hasText(query.status()), RelabelApplication::getStatus, query.status()) .eq(StringUtils.hasText(query.status()), RelabelApplication::getStatus, query.status())
@@ -135,7 +135,7 @@ public class RelabelApplicationService {
.ge(query.createdFrom() != null, RelabelApplication::getCreatedAt, query.createdFrom()) .ge(query.createdFrom() != null, RelabelApplication::getCreatedAt, query.createdFrom())
.le(query.createdTo() != null, RelabelApplication::getCreatedAt, query.createdTo()) .le(query.createdTo() != null, RelabelApplication::getCreatedAt, query.createdTo())
.apply(StringUtils.hasText(query.skuCode()), .apply(StringUtils.hasText(query.skuCode()),
"EXISTS (SELECT 1 FROM ll_relabel_item ri WHERE ri.application_id=ll_relabel_application.id AND ri.tenant_id=ll_relabel_application.tenant_id AND ri.sku_code LIKE CONCAT('%',{0},'%'))", "EXISTS (SELECT 1 FROM ll_relabel_item ri WHERE ri.application_id=ll_relabel_application.id AND ri.customer_id=ll_relabel_application.customer_id AND ri.sku_code LIKE CONCAT('%',{0},'%'))",
query.skuCode()) query.skuCode())
.orderByDesc(RelabelApplication::getCreatedAt); .orderByDesc(RelabelApplication::getCreatedAt);
Page<RelabelApplication> page = applications.selectPage(new Page<>(query.page(), query.size()), wrapper); Page<RelabelApplication> page = applications.selectPage(new Page<>(query.page(), query.size()), wrapper);
@@ -144,11 +144,11 @@ public class RelabelApplicationService {
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
public RelabelResponses.Detail detail(String tenantId, String relabelNo) { public RelabelResponses.Detail detail(String customerId, String relabelNo) {
RelabelApplication application = require(tenantId, relabelNo); RelabelApplication application = require(customerId, relabelNo);
List<RelabelItem> detailItems = itemList(application.getId()); List<RelabelItem> detailItems = itemList(application.getId());
List<RelabelStatusHistory> statusHistory = histories.selectList(new LambdaQueryWrapper<RelabelStatusHistory>() List<RelabelStatusHistory> statusHistory = histories.selectList(new LambdaQueryWrapper<RelabelStatusHistory>()
.eq(RelabelStatusHistory::getTenantId, tenantId) .eq(RelabelStatusHistory::getCustomerId, customerId)
.eq(RelabelStatusHistory::getApplicationId, application.getId()) .eq(RelabelStatusHistory::getApplicationId, application.getId())
.orderByAsc(RelabelStatusHistory::getCreatedAt)); .orderByAsc(RelabelStatusHistory::getCreatedAt));
return new RelabelResponses.Detail(summary(application, detailItems), application.getRemark(), return new RelabelResponses.Detail(summary(application, detailItems), application.getRemark(),
@@ -158,13 +158,13 @@ public class RelabelApplicationService {
} }
@Transactional @Transactional
public RelabelResponses.Detail changeStatus(String tenantId, String relabelNo, String operatorId, public RelabelResponses.Detail changeStatus(String customerId, String relabelNo, String operatorId,
RelabelRequests.ChangeStatus request) { RelabelRequests.ChangeStatus request) {
RelabelApplication application = applications.lockNo(tenantId, relabelNo); RelabelApplication application = applications.lockNo(customerId, relabelNo);
if (application == null) throw notFound("Relabel application not found"); if (application == null) throw notFound("Relabel application not found");
String next = request.status().trim().toUpperCase(); String next = request.status().trim().toUpperCase();
if (!STATUSES.contains(next)) throw badRequest("Unsupported relabel status"); if (!STATUSES.contains(next)) throw badRequest("Unsupported relabel status");
if (next.equals(application.getStatus())) return detail(tenantId, relabelNo); if (next.equals(application.getStatus())) return detail(customerId, relabelNo);
validateTransition(application.getStatus(), next); validateTransition(application.getStatus(), next);
String before = application.getStatus(); String before = application.getStatus();
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
@@ -172,20 +172,20 @@ public class RelabelApplicationService {
application.setUpdatedAt(now); application.setUpdatedAt(now);
applications.updateById(application); applications.updateById(application);
LastlegOrder order = orders.selectById(application.getLastlegOrderId()); LastlegOrder order = orders.selectById(application.getLastlegOrderId());
if (order == null || !tenantId.equals(order.getTenantId())) throw conflict("Linked tail-leg order is missing"); if (order == null || !customerId.equals(order.getCustomerId())) throw conflict("Linked tail-leg order is missing");
String orderBefore = order.getCurrentStatus(); String orderBefore = order.getCurrentStatus();
order.setCurrentStatus(next); order.setCurrentStatus(next);
order.setRelabelStatus(next); order.setRelabelStatus(next);
order.setUpdatedAt(now); order.setUpdatedAt(now);
orders.updateById(order); orders.updateById(order);
insertHistory(tenantId, application.getId(), before, next, operatorId, request.remark(), now); insertHistory(customerId, application.getId(), before, next, operatorId, request.remark(), now);
insertOrderHistory(order.getId(), orderBefore, next, request.remark(), now); insertOrderHistory(order.getId(), orderBefore, next, request.remark(), now);
return detail(tenantId, relabelNo); return detail(customerId, relabelNo);
} }
private void insertItem(String tenantId, long applicationId, RelabelRequests.Item source, LocalDateTime now) { private void insertItem(String customerId, long applicationId, RelabelRequests.Item source, LocalDateTime now) {
RelabelItem item = new RelabelItem(); RelabelItem item = new RelabelItem();
item.setId(IdWorker.getId()); item.setApplicationId(applicationId); item.setTenantId(tenantId); item.setId(IdWorker.getId()); item.setApplicationId(applicationId); item.setCustomerId(customerId);
item.setSkuCode(source.skuCode().trim()); item.setProductImageFileId(trim(source.productImageFileId())); item.setSkuCode(source.skuCode().trim()); item.setProductImageFileId(trim(source.productImageFileId()));
item.setProductImageFileName(trim(source.productImageFileName())); item.setProductImageUrl(trim(source.productImageUrl())); item.setProductImageFileName(trim(source.productImageFileName())); item.setProductImageUrl(trim(source.productImageUrl()));
item.setProductNameZh(source.productNameZh().trim()); item.setTotalPackageCount(source.totalPackageCount()); item.setProductNameZh(source.productNameZh().trim()); item.setTotalPackageCount(source.totalPackageCount());
@@ -219,14 +219,14 @@ public class RelabelApplicationService {
return items.selectList(new LambdaQueryWrapper<RelabelItem>().eq(RelabelItem::getApplicationId, applicationId) return items.selectList(new LambdaQueryWrapper<RelabelItem>().eq(RelabelItem::getApplicationId, applicationId)
.orderByAsc(RelabelItem::getId)); .orderByAsc(RelabelItem::getId));
} }
private RelabelApplication require(String tenantId, String relabelNo) { private RelabelApplication require(String customerId, String relabelNo) {
RelabelApplication value = applications.findNo(tenantId, relabelNo); RelabelApplication value = applications.findNo(customerId, relabelNo);
if (value == null) throw notFound("Relabel application not found"); if (value == null) throw notFound("Relabel application not found");
return value; return value;
} }
private void insertHistory(String tenantId, Long id, String from, String to, String operator, private void insertHistory(String customerId, Long id, String from, String to, String operator,
String remark, LocalDateTime now) { String remark, LocalDateTime now) {
RelabelStatusHistory value = new RelabelStatusHistory(); value.setId(IdWorker.getId()); value.setTenantId(tenantId); RelabelStatusHistory value = new RelabelStatusHistory(); value.setId(IdWorker.getId()); value.setCustomerId(customerId);
value.setApplicationId(id); value.setFromStatus(from); value.setToStatus(to); value.setOperatorId(trim(operator)); value.setApplicationId(id); value.setFromStatus(from); value.setToStatus(to); value.setOperatorId(trim(operator));
value.setRemark(trim(remark)); value.setCreatedAt(now); histories.insert(value); value.setRemark(trim(remark)); value.setCreatedAt(now); histories.insert(value);
} }
@@ -35,15 +35,15 @@ public class LastlegOrderService {
@Resource private LastlegOrderDetailMapper detailMapper; @Resource private LastlegOrderDetailMapper detailMapper;
@Resource private LastlegOrderStatusHistoryMapper historyMapper; @Resource private LastlegOrderStatusHistoryMapper historyMapper;
public PageResult<LastlegOrderSummary> page(String tenantId, LastlegOrderRequests.Query query) { public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) {
Page<LastlegOrder> page = orderMapper.selectPage( Page<LastlegOrder> page = orderMapper.selectPage(
new Page<>(query.page(), query.size()), filter(tenantId, query).orderByDesc(LastlegOrder::getCreatedAt)); new Page<>(query.page(), query.size()), filter(customerId, query).orderByDesc(LastlegOrder::getCreatedAt));
return new PageResult<>(page.getRecords().stream().map(this::summary).toList(), return new PageResult<>(page.getRecords().stream().map(this::summary).toList(),
page.getCurrent(), page.getSize(), page.getTotal(), page.getPages()); page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
} }
public LastlegOrderDetailResponse detail(String tenantId, long orderId) { public LastlegOrderDetailResponse detail(String customerId, long orderId) {
LastlegOrder order = findOwned(tenantId, orderId); LastlegOrder order = findOwned(customerId, orderId);
List<LastlegOrderDetail> cargo = detailMapper.selectList(new LambdaQueryWrapper<LastlegOrderDetail>() List<LastlegOrderDetail> cargo = detailMapper.selectList(new LambdaQueryWrapper<LastlegOrderDetail>()
.eq(LastlegOrderDetail::getOrderId, orderId).orderByAsc(LastlegOrderDetail::getId)); .eq(LastlegOrderDetail::getOrderId, orderId).orderByAsc(LastlegOrderDetail::getId));
List<LastlegOrderStatusHistory> history = historyMapper.selectList( List<LastlegOrderStatusHistory> history = historyMapper.selectList(
@@ -54,13 +54,13 @@ public class LastlegOrderService {
} }
@Transactional @Transactional
public int transferDestination(String tenantId, LastlegOrderRequests.TransferDestination request) { public int transferDestination(String customerId, LastlegOrderRequests.TransferDestination request) {
List<Long> ids = request.orderIds().stream().filter(Objects::nonNull).distinct().toList(); List<Long> ids = request.orderIds().stream().filter(Objects::nonNull).distinct().toList();
if (ids.isEmpty()) { if (ids.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "At least one order is required"); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "At least one order is required");
} }
List<LastlegOrder> orders = orderMapper.selectList(new LambdaQueryWrapper<LastlegOrder>() List<LastlegOrder> orders = orderMapper.selectList(new LambdaQueryWrapper<LastlegOrder>()
.eq(LastlegOrder::getTenantId, tenantId).in(LastlegOrder::getId, ids).last("FOR UPDATE")); .eq(LastlegOrder::getCustomerId, customerId).in(LastlegOrder::getId, ids).last("FOR UPDATE"));
if (orders.size() != ids.size()) { if (orders.size() != ids.size()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "One or more tail-leg orders were not found"); throw new ResponseStatusException(HttpStatus.NOT_FOUND, "One or more tail-leg orders were not found");
} }
@@ -96,8 +96,8 @@ public class LastlegOrderService {
return orders.size(); return orders.size();
} }
public byte[] exportCsv(String tenantId, LastlegOrderRequests.Query query) { public byte[] exportCsv(String customerId, LastlegOrderRequests.Query query) {
List<LastlegOrder> orders = orderMapper.selectList(filter(tenantId, query) List<LastlegOrder> orders = orderMapper.selectList(filter(customerId, query)
.orderByDesc(LastlegOrder::getCreatedAt).last("LIMIT 10000")); .orderByDesc(LastlegOrder::getCreatedAt).last("LIMIT 10000"));
List<String> lines = new ArrayList<>(); List<String> lines = new ArrayList<>();
lines.add("ID,尾程单号,订单类型,入仓码,头程单号,货主,海外仓,转仓,转仓编码,FBA仓库,集装箱号,货柜状态,是否异常,是否暂扣,托数,箱数,已拣箱数,已出库箱数,剩余箱数,体积(m3),数量,体积重量,预计到港时间,实际到港时间,预计到仓,入库单号,出库单号,派送单号,换标单号,换标状态,合托标记,当前状态,备注(仅自己),备注(客户),备注(仓库)"); lines.add("ID,尾程单号,订单类型,入仓码,头程单号,货主,海外仓,转仓,转仓编码,FBA仓库,集装箱号,货柜状态,是否异常,是否暂扣,托数,箱数,已拣箱数,已出库箱数,剩余箱数,体积(m3),数量,体积重量,预计到港时间,实际到港时间,预计到仓,入库单号,出库单号,派送单号,换标单号,换标状态,合托标记,当前状态,备注(仅自己),备注(客户),备注(仓库)");
@@ -120,12 +120,12 @@ public class LastlegOrderService {
return ("\uFEFF" + String.join("\r\n", lines)).getBytes(StandardCharsets.UTF_8); return ("\uFEFF" + String.join("\r\n", lines)).getBytes(StandardCharsets.UTF_8);
} }
private LambdaQueryWrapper<LastlegOrder> filter(String tenantId, LastlegOrderRequests.Query query) { private LambdaQueryWrapper<LastlegOrder> filter(String customerId, LastlegOrderRequests.Query query) {
if (query.createdFrom() != null && query.createdTo() != null && query.createdFrom().isAfter(query.createdTo())) { if (query.createdFrom() != null && query.createdTo() != null && query.createdFrom().isAfter(query.createdTo())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "createdFrom must not be after createdTo"); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "createdFrom must not be after createdTo");
} }
return new LambdaQueryWrapper<LastlegOrder>() return new LambdaQueryWrapper<LastlegOrder>()
.eq(LastlegOrder::getTenantId, tenantId) .eq(LastlegOrder::getCustomerId, customerId)
.like(StringUtils.hasText(query.orderNo()), LastlegOrder::getOrderNo, query.orderNo()) .like(StringUtils.hasText(query.orderNo()), LastlegOrder::getOrderNo, query.orderNo())
.like(StringUtils.hasText(query.inboundCode()), LastlegOrder::getInboundCode, query.inboundCode()) .like(StringUtils.hasText(query.inboundCode()), LastlegOrder::getInboundCode, query.inboundCode())
.like(StringUtils.hasText(query.sourceOrderNo()), LastlegOrder::getSourceOrderNo, query.sourceOrderNo()) .like(StringUtils.hasText(query.sourceOrderNo()), LastlegOrder::getSourceOrderNo, query.sourceOrderNo())
@@ -139,9 +139,9 @@ public class LastlegOrderService {
.le(query.createdTo() != null, LastlegOrder::getCreatedAt, query.createdTo()); .le(query.createdTo() != null, LastlegOrder::getCreatedAt, query.createdTo());
} }
private LastlegOrder findOwned(String tenantId, long orderId) { private LastlegOrder findOwned(String customerId, long orderId) {
LastlegOrder order = orderMapper.selectOne(new LambdaQueryWrapper<LastlegOrder>() LastlegOrder order = orderMapper.selectOne(new LambdaQueryWrapper<LastlegOrder>()
.eq(LastlegOrder::getTenantId, tenantId).eq(LastlegOrder::getId, orderId)); .eq(LastlegOrder::getCustomerId, customerId).eq(LastlegOrder::getId, orderId));
if (order == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Tail-leg order not found"); if (order == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Tail-leg order not found");
return order; return order;
} }
@@ -3,7 +3,10 @@ package com.baiyue.lastleg.service;
import com.baiyue.common.event.*; import com.baiyue.common.event.*;
import com.baiyue.lastleg.entity.*; import com.baiyue.lastleg.entity.*;
import com.baiyue.lastleg.mapper.*; import com.baiyue.lastleg.mapper.*;
import com.baiyue.lastleg.dropship.dto.DropshipRequests;
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.DeserializationFeature;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -12,6 +15,9 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Objects; import java.util.Objects;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
@Service @Service
public class LoadingCompletedConsumer { public class LoadingCompletedConsumer {
@@ -21,6 +27,7 @@ public class LoadingCompletedConsumer {
@Resource private LastlegOrderStatusHistoryMapper historyMapper; @Resource private LastlegOrderStatusHistoryMapper historyMapper;
@Resource private ObjectMapper objectMapper; @Resource private ObjectMapper objectMapper;
@Resource private LastlegOrderNoGenerator orderNoGenerator; @Resource private LastlegOrderNoGenerator orderNoGenerator;
@Resource private DropshipOrderService dropshipOrders;
@Transactional @Transactional
public int consume(EventEnvelope envelope) { public int consume(EventEnvelope envelope) {
@@ -42,7 +49,7 @@ public class LoadingCompletedConsumer {
} }
LastlegEventInbox inbox=new LastlegEventInbox(); LastlegEventInbox inbox=new LastlegEventInbox();
inbox.setEventId(envelope.eventId()); inbox.setTenantId(envelope.tenantId()); inbox.setEventType(envelope.eventType()); inbox.setEventId(envelope.eventId()); inbox.setCustomerId(envelope.tenantId()); inbox.setEventType(envelope.eventType());
inbox.setEventVersion(envelope.eventVersion()); inbox.setPayloadHash(envelope.payloadHash()); inbox.setEventVersion(envelope.eventVersion()); inbox.setPayloadHash(envelope.payloadHash());
inbox.setPayloadJson(envelope.payload().toString()); inbox.setStatus("RECEIVED"); inbox.setReceivedAt(LocalDateTime.now()); inbox.setPayloadJson(envelope.payload().toString()); inbox.setStatus("RECEIVED"); inbox.setReceivedAt(LocalDateTime.now());
try { inboxMapper.insert(inbox); } catch(DuplicateKeyException exception) { try { inboxMapper.insert(inbox); } catch(DuplicateKeyException exception) {
@@ -50,13 +57,22 @@ public class LoadingCompletedConsumer {
} }
String orderNo = orderNoGenerator.forContainerNo(payload.containerNo()); String orderNo = orderNoGenerator.forContainerNo(payload.containerNo());
for(var source:payload.orders()) createOrder(envelope,payload,source,orderNo); JsonNode rawOrders = envelope.payload().path("orders");
for (int index = 0; index < payload.orders().size(); index++) {
var source = payload.orders().get(index);
JsonNode rawSource = rawOrders.isArray() && index < rawOrders.size() ? rawOrders.get(index) : objectMapper.createObjectNode();
createOrder(envelope, payload, source, orderNo, rawSource);
}
inbox.setStatus("PROCESSED"); inbox.setProcessedAt(LocalDateTime.now()); inboxMapper.updateById(inbox); inbox.setStatus("PROCESSED"); inbox.setProcessedAt(LocalDateTime.now()); inboxMapper.updateById(inbox);
return payload.orders().size(); return payload.orders().size();
} }
private FirstLegLoadingPlanCompletedV1 parsePayload(EventEnvelope envelope) { private FirstLegLoadingPlanCompletedV1 parsePayload(EventEnvelope envelope) {
try { return objectMapper.treeToValue(envelope.payload(),FirstLegLoadingPlanCompletedV1.class); } try {
ObjectMapper compatibleMapper = objectMapper.copy()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return compatibleMapper.treeToValue(envelope.payload(), FirstLegLoadingPlanCompletedV1.class);
}
catch(Exception exception) { throw new IllegalArgumentException("尾程事件负载无效",exception); } catch(Exception exception) { throw new IllegalArgumentException("尾程事件负载无效",exception); }
} }
@@ -65,22 +81,33 @@ public class LoadingCompletedConsumer {
} }
private void createOrder(EventEnvelope event,FirstLegLoadingPlanCompletedV1 payload, private void createOrder(EventEnvelope event,FirstLegLoadingPlanCompletedV1 payload,
FirstLegLoadingPlanCompletedV1.Order source,String orderNo) { FirstLegLoadingPlanCompletedV1.Order source,String orderNo, JsonNode rawSource) {
String orderHash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(objectMapper.valueToTree(source))); String orderHash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(rawSource));
LastlegOrder existing=orderMapper.findSource(event.tenantId(),event.sourceRegion().name(),source.sourceInboundId()); LastlegOrder existing=orderMapper.findSource(event.tenantId(),event.sourceRegion().name(),source.sourceInboundId());
if(existing!=null) { if(existing!=null) {
if(!Objects.equals(existing.getSourcePayloadHash(),orderHash)) throw new IllegalStateException("来源入仓单快照冲突: "+source.sourceInboundId()); if(!Objects.equals(existing.getSourcePayloadHash(),orderHash)) throw new IllegalStateException("来源入仓单快照冲突: "+source.sourceInboundId());
return; return;
} }
LastlegOrder order=new LastlegOrder(); LastlegOrder order=new LastlegOrder();
order.setId(IdWorker.getId()); order.setOrderNo(orderNo); order.setTenantId(event.tenantId()); order.setOrderType("FIRST_LEG"); String orderType = rawSource.path("orderType").asText(event.payload().path("orderType").asText("FIRST_LEG"));
String customerId = rawSource.path("customerId")
.asText(event.payload().path("customerId").asText(event.tenantId()));
if (!event.tenantId().equals(customerId)) {
throw new IllegalArgumentException("Order customerId does not match the trusted event customer");
}
List<DropshipRequests.Pallet> dropshipPallets = "ONE_CLICK_DROPSHIP".equals(orderType)
? pallets(rawSource) : List.of();
if ("ONE_CLICK_DROPSHIP".equals(orderType) && dropshipPallets.isEmpty()) {
throw new IllegalArgumentException("One-click dropship event has no pallet snapshot");
}
order.setId(IdWorker.getId()); order.setOrderNo(orderNo); order.setCustomerId(customerId); order.setOrderType(orderType);
order.setInboundCode(source.inboundCode()); order.setSourceRegion(event.sourceRegion().name()); order.setInboundCode(source.inboundCode()); order.setSourceRegion(event.sourceRegion().name());
order.setSourceInboundId(source.sourceInboundId()); order.setRootInboundId(source.rootInboundId()); order.setSourceInboundId(source.sourceInboundId()); order.setRootInboundId(source.rootInboundId());
order.setSourceOrderId(source.sourceOrderId()); order.setSourceOrderNo(source.sourceOrderNo()); order.setRootOrderId(source.rootOrderId()); order.setSourceOrderId(source.sourceOrderId()); order.setSourceOrderNo(source.sourceOrderNo()); order.setRootOrderId(source.rootOrderId());
order.setOverseasWarehouseId(payload.warehouseId()); order.setOverseasWarehouseName(payload.warehouseName()); order.setOverseasWarehouseId(payload.warehouseId()); order.setOverseasWarehouseName(payload.warehouseName());
order.setFbaWarehouseCode(source.destinationWarehouseCode()); order.setContainerNo(payload.containerNo()); order.setFbaWarehouseCode(source.destinationWarehouseCode()); order.setContainerNo(payload.containerNo());
order.setContainerStatus("待转柜"); order.setAbnormal(false); order.setDetained(false); order.setContainerStatus("待转柜"); order.setAbnormal(false); order.setDetained(false);
order.setPackageCount(source.packageCount()); order.setPalletCount(Objects.requireNonNullElse(source.palletCount(),0)); order.setPackageCount(source.packageCount()); order.setPalletCount(dropshipPallets.size());
order.setProductQuantity(source.productQuantity()); order.setVolumeCbm(source.volumeCbm()); order.setProductQuantity(source.productQuantity()); order.setVolumeCbm(source.volumeCbm());
order.setVolumeWeightKg(source.items().stream().map(FirstLegLoadingPlanCompletedV1.Item::dimensionalWeightKg) order.setVolumeWeightKg(source.items().stream().map(FirstLegLoadingPlanCompletedV1.Item::dimensionalWeightKg)
.filter(Objects::nonNull).reduce(BigDecimal.ZERO,BigDecimal::add)); .filter(Objects::nonNull).reduce(BigDecimal.ZERO,BigDecimal::add));
@@ -110,5 +137,57 @@ public class LoadingCompletedConsumer {
LastlegOrderStatusHistory history=new LastlegOrderStatusHistory(); LastlegOrderStatusHistory history=new LastlegOrderStatusHistory();
history.setOrderId(order.getId()); history.setToStatus("PENDING_ASSIGNMENT"); history.setEventId(event.eventId()); history.setOrderId(order.getId()); history.setToStatus("PENDING_ASSIGNMENT"); history.setEventId(event.eventId());
history.setRemark("头程装柜完成自动生成"); history.setCreatedAt(LocalDateTime.now()); historyMapper.insert(history); history.setRemark("头程装柜完成自动生成"); history.setCreatedAt(LocalDateTime.now()); historyMapper.insert(history);
if ("ONE_CLICK_DROPSHIP".equals(orderType)) {
long warehouseId;
try { warehouseId = Long.parseLong(payload.warehouseId()); }
catch (Exception exception) { throw new IllegalArgumentException("One-click dropship warehouseId is invalid", exception); }
dropshipOrders.receive(new DropshipRequests.Receive(customerId, order.getId(), warehouseId,
event.eventId() + ":DROPSHIP", dropshipPallets));
}
}
private List<DropshipRequests.Pallet> pallets(JsonNode source) {
List<DropshipRequests.Pallet> result = new ArrayList<>();
JsonNode nodes = source.path("pallets");
if (nodes.isArray()) {
for (JsonNode pallet : nodes) {
String palletNo = pallet.path("palletNo").asText(pallet.path("sourcePalletNo").asText(null));
List<DropshipRequests.SkuPackages> skuRows = skuPackages(pallet);
if (palletNo != null && !palletNo.isBlank() && !skuRows.isEmpty()) {
result.add(new DropshipRequests.Pallet(palletNo, skuRows));
}
}
return result;
}
String palletNo = source.path("palletNo").asText(source.path("sourcePalletNo").asText(null));
List<DropshipRequests.SkuPackages> skuRows = skuPackages(source);
if (palletNo != null && !palletNo.isBlank() && !skuRows.isEmpty()) {
result.add(new DropshipRequests.Pallet(palletNo, skuRows));
}
return result;
}
private List<DropshipRequests.SkuPackages> skuPackages(JsonNode source) {
List<DropshipRequests.SkuPackages> result = new ArrayList<>();
JsonNode nodes = source.path("skuPackages");
if (!nodes.isArray()) nodes = source.path("items");
if (!nodes.isArray()) return result;
for (JsonNode item : nodes) {
String sku = item.path("skuCode").asText(null);
long packageCount = item.path("packageCount").asLong(0);
long productQuantity = item.path("productQuantity").asLong(0);
JsonNode markNodes = item.path("cartonMarks");
if (!markNodes.isArray()) markNodes = item.path("boxMarks");
if (!markNodes.isArray()) markNodes = item.path("cartonNos");
List<String> marks = new ArrayList<>();
if (markNodes.isArray()) {
for (JsonNode mark : markNodes) if (!mark.asText().isBlank()) marks.add(mark.asText());
}
if (sku != null && !sku.isBlank() && packageCount > 0 && productQuantity > 0 && !marks.isEmpty()) {
String productName = item.path("productName").asText(item.path("productNameZh").asText(null));
result.add(new DropshipRequests.SkuPackages(sku, productName, packageCount, productQuantity, marks));
}
}
return result;
} }
} }
@@ -0,0 +1,4 @@
package com.baiyue.lastleg.transfer;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration @MapperScan("com.baiyue.lastleg.transfer.mapper") public class TransferMapperConfiguration {}

Some files were not shown because too many files have changed in this diff Show More