贴标申请,仓储转运

This commit is contained in:
2026-09-28 11:59:08 +08:00
parent 0e62b2f45b
commit 2bb98d2047
14 changed files with 278 additions and 38 deletions
+1 -1
View File
@@ -69,7 +69,7 @@
| CN / `baiyue_iam` | `migrations/cn/baiyue_iam/V1__align_schema_baseline.sql` | 收敛客户编号、昵称和资质原文件名定义;收紧前检查存量数据 | | CN / `baiyue_iam` | `migrations/cn/baiyue_iam/V1__align_schema_baseline.sql` | 收敛客户编号、昵称和资质原文件名定义;收紧前检查存量数据 |
| CN / `baiyue_firstleg` | `migrations/cn/baiyue_firstleg/V1__align_schema_baseline.sql` | 收敛一件代发托盘码非空定义;收紧前检查存量数据 | | CN / `baiyue_firstleg` | `migrations/cn/baiyue_firstleg/V1__align_schema_baseline.sql` | 收敛一件代发托盘码非空定义;收紧前检查存量数据 |
| CN / `baiyue_file` | `migrations/cn/baiyue_file/V1__align_schema_baseline.sql`、`V2__widen_business_type.sql` | 补齐 PDA 发布表和固定渠道,并支持正式长业务类型 | | CN / `baiyue_file` | `migrations/cn/baiyue_file/V1__align_schema_baseline.sql`、`V2__widen_business_type.sql` | 补齐 PDA 发布表和固定渠道,并支持正式长业务类型 |
| OVERSEAS / `baiyue_lastleg` | `migrations/overseas/baiyue_lastleg/V1__align_lastleg_production_schema.sql`、`V2__align_schema_baseline.sql`、`V3__relax_valid_nullable_fields.sql` | 补齐尾程字段并收敛字段定义;放宽退货出库和预入仓批量 SKU 换标合法空值 | | OVERSEAS / `baiyue_lastleg` | `migrations/overseas/baiyue_lastleg/V1__align_lastleg_production_schema.sql`、`V2__align_schema_baseline.sql`、`V3__relax_valid_nullable_fields.sql`、`V4__cargo_discard_return_inventory.sql` | 补齐尾程字段并收敛字段定义;放宽退货出库和预入仓批量 SKU 换标合法空值;货物废弃支持退货库存箱 |
上述新增候选当前只完成编写和离线检查,尚未在测试或生产执行。测试通过后必须复用同一份 SQL 和 manifest 晋级生产,不得重新生成或修改候选。`scripts/prod-database-schema-acceptance.mjs` 仍只负责历史尾程 V1 的专用只读核对/受控执行,不代表本批跨 schema 发布入口;本批按迁移 manifest 管理,实际执行须等待环境执行器接入或当前轮明确授权后的受控人工执行。 上述新增候选当前只完成编写和离线检查,尚未在测试或生产执行。测试通过后必须复用同一份 SQL 和 manifest 晋级生产,不得重新生成或修改候选。`scripts/prod-database-schema-acceptance.mjs` 仍只负责历史尾程 V1 的专用只读核对/受控执行,不代表本批跨 schema 发布入口;本批按迁移 manifest 管理,实际执行须等待环境执行器接入或当前轮明确授权后的受控人工执行。
@@ -0,0 +1,24 @@
ALTER TABLE ll_cargo_discard_item
MODIFY COLUMN order_id BIGINT NULL COMMENT '一件代发订单ID',
MODIFY COLUMN profile_id BIGINT NULL COMMENT '托盘档案ID',
MODIFY COLUMN sku_inventory_id BIGINT NULL COMMENT 'SKU库存记录ID';
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND constraint_name='chk_ll_return_inventory_carton_quantity'), 'ALTER TABLE ll_return_inventory_carton DROP CHECK chk_ll_return_inventory_carton_quantity', 'SELECT 1'));
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK((status IN('AVAILABLE','RELABEL_PENDING','OUTBOUNDED','DISCARD_PENDING') AND sku_types>0 AND total_quantity>0) OR (status IN('MERGED','DISCARDED') AND sku_types=0 AND total_quantity=0));
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND constraint_name='chk_ll_return_inventory_carton_status'), 'ALTER TABLE ll_return_inventory_carton DROP CHECK chk_ll_return_inventory_carton_status', 'SELECT 1'));
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','RELABEL_PENDING','MERGED','OUTBOUNDED','DISCARD_PENDING','DISCARDED'));
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='source_type'),
'ALTER TABLE ll_cargo_discard_item ADD COLUMN source_type VARCHAR(32) NOT NULL DEFAULT ''DROPSHIP'' COMMENT ''库存来源:一件代发DROPSHIP、退货箱RETURN_CARTON'' AFTER order_id', 'SELECT 1');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='return_carton_id'),
'ALTER TABLE ll_cargo_discard_item ADD COLUMN return_carton_id BIGINT NULL COMMENT ''退货库存箱ID'' AFTER source_type', 'SELECT 1');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND index_name='idx_ll_cargo_discard_return_carton'),
'ALTER TABLE ll_cargo_discard_item ADD KEY idx_ll_cargo_discard_return_carton(customer_id,return_carton_id)', 'SELECT 1');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
@@ -140,10 +140,12 @@ CREATE TABLE ll_cargo_discard_item (
id bigint NOT NULL COMMENT '主键ID', id bigint NOT NULL COMMENT '主键ID',
application_id bigint NOT NULL COMMENT '申请单ID', application_id bigint NOT NULL COMMENT '申请单ID',
customer_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户租户ID', customer_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户租户ID',
order_id bigint NOT NULL COMMENT '订单ID', order_id bigint DEFAULT NULL COMMENT '一件代发订单ID',
source_type varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'DROPSHIP' COMMENT '库存来源:一件代发DROPSHIP、退货箱RETURN_CARTON',
return_carton_id bigint DEFAULT NULL COMMENT '退货库存箱ID',
inbound_code varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '入仓单号快照', inbound_code varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '入仓单号快照',
profile_id bigint NOT NULL COMMENT '托盘档案ID', profile_id bigint DEFAULT NULL COMMENT '托盘档案ID',
sku_inventory_id bigint NOT NULL COMMENT 'SKU库存记录ID', sku_inventory_id bigint DEFAULT NULL COMMENT 'SKU库存记录ID',
sku_code varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'SKU编码', sku_code varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'SKU编码',
product_name varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '产品名称', product_name varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '产品名称',
available_packages_snapshot int NOT NULL COMMENT '可用箱数快照', available_packages_snapshot int NOT NULL COMMENT '可用箱数快照',
@@ -157,6 +159,7 @@ CREATE TABLE ll_cargo_discard_item (
UNIQUE KEY uk_ll_cargo_discard_item (application_id,profile_id,sku_code), UNIQUE KEY uk_ll_cargo_discard_item (application_id,profile_id,sku_code),
KEY idx_ll_cargo_discard_item_list (customer_id,application_id), KEY idx_ll_cargo_discard_item_list (customer_id,application_id),
KEY idx_ll_cargo_discard_item_inbound (customer_id,inbound_code,sku_code) KEY idx_ll_cargo_discard_item_inbound (customer_id,inbound_code,sku_code)
,KEY idx_ll_cargo_discard_return_carton (customer_id,return_carton_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='货物废弃SKU明细'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='货物废弃SKU明细';
CREATE TABLE ll_cargo_discard_status_history ( CREATE TABLE ll_cargo_discard_status_history (
@@ -2384,8 +2387,8 @@ CREATE TABLE ll_return_inventory_carton (
UNIQUE KEY uk_ll_return_inventory_carton_mark (carton_mark), UNIQUE KEY uk_ll_return_inventory_carton_mark (carton_mark),
UNIQUE KEY uk_ll_return_inventory_carton_receipt (source_receipt_id), UNIQUE KEY uk_ll_return_inventory_carton_receipt (source_receipt_id),
KEY idx_ll_return_inventory_carton_customer (customer_id,overseas_warehouse_id,status,created_at), KEY idx_ll_return_inventory_carton_customer (customer_id,overseas_warehouse_id,status,created_at),
CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK ((((status in (_utf8mb4'AVAILABLE',_utf8mb4'RELABEL_PENDING',_utf8mb4'OUTBOUNDED')) and (sku_types > 0) and (total_quantity > 0)) or ((status = _utf8mb4'MERGED') and (sku_types = 0) and (total_quantity = 0)))), CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK ((((status in (_utf8mb4'AVAILABLE',_utf8mb4'RELABEL_PENDING',_utf8mb4'OUTBOUNDED',_utf8mb4'DISCARD_PENDING')) and (sku_types > 0) and (total_quantity > 0)) or ((status in (_utf8mb4'MERGED',_utf8mb4'DISCARDED')) and (sku_types = 0) and (total_quantity = 0)))),
CONSTRAINT chk_ll_return_inventory_carton_status CHECK ((status in (_utf8mb4'AVAILABLE',_utf8mb4'RELABEL_PENDING',_utf8mb4'MERGED',_utf8mb4'OUTBOUNDED'))) CONSTRAINT chk_ll_return_inventory_carton_status CHECK ((status in (_utf8mb4'AVAILABLE',_utf8mb4'RELABEL_PENDING',_utf8mb4'MERGED',_utf8mb4'OUTBOUNDED',_utf8mb4'DISCARD_PENDING',_utf8mb4'DISCARDED')))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='退货实物库存箱及箱唛'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='退货实物库存箱及箱唛';
CREATE TABLE ll_return_inventory_carton_item ( CREATE TABLE ll_return_inventory_carton_item (
@@ -19,7 +19,7 @@ public final class CargoDiscardRequests {
@NotBlank @Size(max = 255) String idempotencyKey, @NotBlank @Size(max = 255) String idempotencyKey,
@Size(max = 1000) String remark) {} @Size(max = 1000) String remark) {}
public record Item(@NotNull Long orderId, @NotBlank @Size(max = 128) String skuCode, public record Item(Long orderId, Long returnCartonId, @NotBlank @Size(max = 128) String skuCode,
@Positive int discardPackages) {} @Positive int discardPackages) {}
public record Review(@NotBlank String decision, @Size(max = 1000) String remark) {} public record Review(@NotBlank String decision, @Size(max = 1000) String remark) {}
@@ -7,7 +7,8 @@ import java.util.List;
public final class CargoDiscardResponses { public final class CargoDiscardResponses {
private CargoDiscardResponses() {} private CargoDiscardResponses() {}
public record Candidate(Long orderId, String inboundCode, String inboundOrderNo, public record Candidate(Long orderId, String sourceType, Long returnCartonId, String cartonMark,
String inboundCode, String inboundOrderNo,
String overseasWarehouseId, String overseasWarehouseName, String overseasWarehouseId, String overseasWarehouseName,
LocalDateTime inboundCreatedAt, long availablePackages, LocalDateTime inboundCreatedAt, long availablePackages,
List<CandidateSku> skus) {} List<CandidateSku> skus) {}
@@ -14,6 +14,8 @@ public class CargoDiscardItem {
private Long applicationId; private Long applicationId;
private String customerId; private String customerId;
private Long orderId; private Long orderId;
private String sourceType;
private Long returnCartonId;
private String inboundCode; private String inboundCode;
private Long profileId; private Long profileId;
private Long skuInventoryId; private Long skuInventoryId;
@@ -10,6 +10,8 @@ import com.baiyue.lastleg.discard.service.CargoDiscardService;
import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile; import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper; import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity; import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -27,6 +29,7 @@ public class PdaCargoDiscardService {
@Resource private DropshipOrderProfileMapper profiles; @Resource private DropshipOrderProfileMapper profiles;
@Resource private CargoDiscardInventoryAdjustmentService adjustments; @Resource private CargoDiscardInventoryAdjustmentService adjustments;
@Resource private CargoDiscardService discardService; @Resource private CargoDiscardService discardService;
@Resource private ReturnInventoryCartonMapper returnCartons;
public List<PdaCargoDiscardDtos.PalletTask> tasks(PdaWarehouseIdentity identity) { public List<PdaCargoDiscardDtos.PalletTask> tasks(PdaWarehouseIdentity identity) {
return applications.selectList(new LambdaQueryWrapper<CargoDiscardApplication>() return applications.selectList(new LambdaQueryWrapper<CargoDiscardApplication>()
@@ -69,6 +72,9 @@ public class PdaCargoDiscardService {
item.setActualDiscardPackages(actuals.get(item.getId())); item.setPdaStatus("COMPLETED"); item.setActualDiscardPackages(actuals.get(item.getId())); item.setPdaStatus("COMPLETED");
item.setPdaOperatorId(identity.operatorKey()); item.setPdaCompletedAt(now); items.updateById(item); item.setPdaOperatorId(identity.operatorKey()); item.setPdaCompletedAt(now); items.updateById(item);
} }
task.skus().stream().map(PdaCargoDiscardDtos.Sku::itemId).map(items::selectById)
.filter(this::isReturnItem).map(CargoDiscardItem::getReturnCartonId).filter(Objects::nonNull).distinct()
.forEach(cartonId -> adjustments.finishReturnCarton(application.getCustomerId(), cartonId));
if (items.listApplication(application.getCustomerId(), application.getId()).stream() if (items.listApplication(application.getCustomerId(), application.getId()).stream()
.allMatch(i -> "COMPLETED".equals(i.getPdaStatus()))) { .allMatch(i -> "COMPLETED".equals(i.getPdaStatus()))) {
int actualTotal = items.listApplication(application.getCustomerId(), application.getId()).stream() int actualTotal = items.listApplication(application.getCustomerId(), application.getId()).stream()
@@ -83,17 +89,31 @@ public class PdaCargoDiscardService {
private List<PdaCargoDiscardDtos.PalletTask> palletTasks(CargoDiscardApplication application) { private List<PdaCargoDiscardDtos.PalletTask> palletTasks(CargoDiscardApplication application) {
return items.listApplication(application.getCustomerId(), application.getId()).stream() return items.listApplication(application.getCustomerId(), application.getId()).stream()
.collect(Collectors.groupingBy(CargoDiscardItem::getProfileId, LinkedHashMap::new, Collectors.toList())) .collect(Collectors.groupingBy(this::physicalGroupKey, LinkedHashMap::new, Collectors.toList()))
.entrySet().stream().map(entry -> { .entrySet().stream().map(entry -> {
DropshipOrderProfile profile = profiles.selectById(entry.getKey()); CargoDiscardItem first = entry.getValue().get(0);
if (profile == null || !application.getCustomerId().equals(profile.getCustomerId())) throw conflict("废弃明细关联托盘不存在"); boolean returnSource = isReturnItem(first);
DropshipOrderProfile profile = returnSource ? null : profiles.selectById(first.getProfileId());
ReturnInventoryCarton returnCarton = returnSource ? returnCartons.selectById(first.getReturnCartonId()) : null;
if (!returnSource && (profile == null || !application.getCustomerId().equals(profile.getCustomerId()))) throw conflict("废弃明细关联托盘不存在");
if (returnSource && (returnCarton == null || !application.getCustomerId().equals(returnCarton.getCustomerId()))) throw conflict("废弃明细关联退货箱不存在");
List<PdaCargoDiscardDtos.Sku> skus = entry.getValue().stream().map(i -> new PdaCargoDiscardDtos.Sku( List<PdaCargoDiscardDtos.Sku> skus = entry.getValue().stream().map(i -> new PdaCargoDiscardDtos.Sku(
i.getId(), i.getSkuCode(), i.getProductName(), i.getDiscardPackages(), i.getActualDiscardPackages(), i.getId(), i.getSkuCode(), i.getProductName(), i.getDiscardPackages(), i.getActualDiscardPackages(),
i.getPdaStatus() == null ? "PENDING" : i.getPdaStatus())).toList(); i.getPdaStatus() == null ? "PENDING" : i.getPdaStatus())).toList();
String status = skus.stream().allMatch(s -> "COMPLETED".equals(s.status())) ? "COMPLETED" : "PENDING"; String status = skus.stream().allMatch(s -> "COMPLETED".equals(s.status())) ? "COMPLETED" : "PENDING";
return new PdaCargoDiscardDtos.PalletTask(application.getDiscardNo(), application.getCustomerId(), return new PdaCargoDiscardDtos.PalletTask(application.getDiscardNo(), application.getCustomerId(),
application.getOverseasWarehouseId(), profile.getPalletNo(), profile.getId(), status, skus); application.getOverseasWarehouseId(), returnSource ? firstNonBlank(returnCarton.getPalletCode(), returnCarton.getCartonMark()) : profile.getPalletNo(),
returnSource ? returnCarton.getId() : profile.getId(), status, skus);
}).toList(); }).toList();
} }
private String physicalGroupKey(CargoDiscardItem item) {
return isReturnItem(item) ? "RETURN:" + item.getReturnCartonId() : "DROPSHIP:" + item.getProfileId();
}
private boolean isReturnItem(CargoDiscardItem item) {
return "RETURN_CARTON".equals(item.getSourceType()) || item.getReturnCartonId() != null;
}
private static String firstNonBlank(String first, String second) {
return first != null && !first.isBlank() ? first : second;
}
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); } private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
} }
@@ -15,6 +15,9 @@ import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
import com.baiyue.lastleg.dropship.mapper.DropshipSkuInventoryMapper; import com.baiyue.lastleg.dropship.mapper.DropshipSkuInventoryMapper;
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper; import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper; import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -37,11 +40,14 @@ public class CargoDiscardInventoryAdjustmentService {
@Resource private DropshipOrderProfileMapper profiles; @Resource private DropshipOrderProfileMapper profiles;
@Resource private InventoryPositionMapper positions; @Resource private InventoryPositionMapper positions;
@Resource private InventoryBalanceMapper balances; @Resource private InventoryBalanceMapper balances;
@Resource private ReturnInventoryCartonMapper returnCartons;
@Resource private ReturnInventoryCartonItemMapper returnCartonItems;
@Transactional(propagation = Propagation.MANDATORY) @Transactional(propagation = Propagation.MANDATORY)
public AdjustmentResult discardPalletItem(String customerId, CargoDiscardApplication application, public AdjustmentResult discardPalletItem(String customerId, CargoDiscardApplication application,
CargoDiscardItem item, List<CargoDiscardCarton> links, CargoDiscardItem item, List<CargoDiscardCarton> links,
int actualPackages, String operatorId, LocalDateTime occurredAt) { int actualPackages, String operatorId, LocalDateTime occurredAt) {
if (isReturnItem(item)) return discardReturnItem(customerId, item, actualPackages);
if (actualPackages < 0 || actualPackages > links.size()) throw conflict("实际废弃数量超出该托盘申请数量"); if (actualPackages < 0 || actualPackages > links.size()) throw conflict("实际废弃数量超出该托盘申请数量");
DropshipSkuInventory sku = inventory.lockSkuInventory(customerId, item.getSkuInventoryId()); DropshipSkuInventory sku = inventory.lockSkuInventory(customerId, item.getSkuInventoryId());
if (sku == null) throw conflict("托盘 SKU 库存不存在: " + item.getSkuCode()); if (sku == null) throw conflict("托盘 SKU 库存不存在: " + item.getSkuCode());
@@ -88,6 +94,36 @@ public class CargoDiscardInventoryAdjustmentService {
return new AdjustmentResult(before, actualPackages, after); return new AdjustmentResult(before, actualPackages, after);
} }
private AdjustmentResult discardReturnItem(String customerId, CargoDiscardItem item, int actualPackages) {
if (actualPackages < 0 || actualPackages > item.getDiscardPackages()) {
throw conflict("实际废弃数量超出退货库存申请数量");
}
ReturnInventoryCartonItem sku = returnCartonItems.lockSku(item.getReturnCartonId(), item.getSkuCode());
if (sku == null || !customerId.equals(sku.getCustomerId())) throw conflict("退货库存 SKU 不存在");
long before = value(sku.getQuantity());
if (before < actualPackages || (actualPackages > 0 && returnCartonItems.deduct(sku.getId(), actualPackages) != 1)) {
throw conflict("退货库存数量不足");
}
return new AdjustmentResult(before, actualPackages, before - actualPackages);
}
@Transactional(propagation = Propagation.MANDATORY)
public void finishReturnCarton(String customerId, long cartonId) {
var carton = returnCartons.lockId(cartonId);
if (carton == null || !customerId.equals(carton.getCustomerId())) throw conflict("退货库存箱不存在");
if ("AVAILABLE".equals(carton.getStatus()) || "DISCARDED".equals(carton.getStatus())) return;
long remaining = returnCartonItems.totalQuantity(cartonId);
int skuTypes = returnCartonItems.skuTypes(cartonId);
String status = remaining == 0 ? "DISCARDED" : "AVAILABLE";
if (returnCartons.finishDiscard(cartonId, customerId, status, remaining, skuTypes) != 1) {
throw conflict("退货库存箱状态已变化");
}
}
private static boolean isReturnItem(CargoDiscardItem item) {
return "RETURN_CARTON".equals(item.getSourceType()) || item.getReturnCartonId() != null;
}
@Transactional(propagation = Propagation.MANDATORY) @Transactional(propagation = Propagation.MANDATORY)
public AdjustmentResult discard(String customerId, CargoDiscardApplication application, public AdjustmentResult discard(String customerId, CargoDiscardApplication application,
List<CargoDiscardItem> items, List<CargoDiscardItem> items,
@@ -22,6 +22,10 @@ import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory; import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
import com.baiyue.lastleg.mapper.LastlegOrderMapper; import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper; import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@@ -64,6 +68,8 @@ public class CargoDiscardService {
@Resource private CargoDiscardInventoryAdjustmentService adjustments; @Resource private CargoDiscardInventoryAdjustmentService adjustments;
@Resource private LastlegOrderMapper orders; @Resource private LastlegOrderMapper orders;
@Resource private LastlegOrderStatusHistoryMapper orderHistories; @Resource private LastlegOrderStatusHistoryMapper orderHistories;
@Resource private ReturnInventoryCartonMapper returnCartons;
@Resource private ReturnInventoryCartonItemMapper returnCartonItems;
@Resource private ObjectMapper objectMapper; @Resource private ObjectMapper objectMapper;
@Value("${baiyue.lastleg.cargo-discard.fee-per-package-usd:5.00}") @Value("${baiyue.lastleg.cargo-discard.fee-per-package-usd:5.00}")
private BigDecimal feePerPackage = new BigDecimal("5.00"); private BigDecimal feePerPackage = new BigDecimal("5.00");
@@ -75,9 +81,25 @@ public class CargoDiscardService {
CandidateAccumulator value = grouped.computeIfAbsent(row.getOrderId(), ignored -> new CandidateAccumulator(row)); CandidateAccumulator value = grouped.computeIfAbsent(row.getOrderId(), ignored -> new CandidateAccumulator(row));
value.add(row); value.add(row);
} }
return grouped.values().stream() List<CargoDiscardResponses.Candidate> result = grouped.values().stream()
.map(value -> value.response(inventory.countAvailableOrder(customerId, value.first.getOrderId()))) .map(value -> value.response(inventory.countAvailableOrder(customerId, value.first.getOrderId())))
.toList(); .collect(java.util.stream.Collectors.toCollection(java.util.ArrayList::new));
String keyword = trim(inboundCode);
if (returnCartons == null || returnCartonItems == null) return result;
for (ReturnInventoryCarton carton : returnCartons.findAvailable(customerId, null)) {
if (StringUtils.hasText(keyword) && !containsIgnoreCase(carton.getSourceReturnNo(), keyword)
&& !containsIgnoreCase(carton.getCartonMark(), keyword)) continue;
List<CargoDiscardResponses.CandidateSku> skus = returnCartonItems.listCarton(carton.getId()).stream()
.filter(item -> value(item.getQuantity()) > 0)
.map(item -> new CargoDiscardResponses.CandidateSku(item.getSkuCode(), item.getProductNameZh(), value(item.getQuantity())))
.toList();
long available = skus.stream().mapToLong(CargoDiscardResponses.CandidateSku::availablePackages).sum();
if (available <= 0) continue;
result.add(new CargoDiscardResponses.Candidate(null, "RETURN_CARTON", carton.getId(), carton.getCartonMark(),
carton.getSourceReturnNo(), carton.getSourceReturnNo(), carton.getOverseasWarehouseId(), null,
null, available, skus));
}
return result;
} }
@Transactional @Transactional
@@ -94,18 +116,39 @@ public class CargoDiscardService {
} }
Set<String> requestedDimensions = new LinkedHashSet<>(); Set<String> requestedDimensions = new LinkedHashSet<>();
Map<Long, LastlegOrder> orderCache = new HashMap<>(); Map<Long, LastlegOrder> orderCache = new HashMap<>();
Map<Long, ReturnInventoryCarton> returnCartonCache = new HashMap<>();
String warehouseId = null; String warehouseId = null;
String warehouseName = null; String warehouseName = null;
int total = 0; int total = 0;
for (CargoDiscardRequests.Item input : request.items()) { for (CargoDiscardRequests.Item input : request.items()) {
String dimension = input.orderId() + ":" + input.skuCode().trim(); boolean returnSource = input.returnCartonId() != null;
if (returnSource == (input.orderId() != null)) {
throw badRequest("Each discard item must identify exactly one inventory source");
}
String dimension = (returnSource ? "RETURN:" + input.returnCartonId() : "ORDER:" + input.orderId())
+ ":" + input.skuCode().trim();
if (!requestedDimensions.add(dimension)) throw badRequest("Duplicate order/SKU discard item: " + dimension); if (!requestedDimensions.add(dimension)) throw badRequest("Duplicate order/SKU discard item: " + dimension);
LastlegOrder order = requireDropshipOrder(customerId, input.orderId()); String itemWarehouseId;
orderCache.put(order.getId(), order); String itemWarehouseName;
if (returnSource) {
ReturnInventoryCarton carton = requireReturnCarton(customerId, input.returnCartonId());
ReturnInventoryCartonItem sku = returnCartonItems.lockSku(carton.getId(), input.skuCode().trim());
if (sku == null || value(sku.getQuantity()) < input.discardPackages()) {
throw conflict("Insufficient return inventory for SKU " + input.skuCode().trim());
}
returnCartonCache.put(carton.getId(), carton);
itemWarehouseId = carton.getOverseasWarehouseId();
itemWarehouseName = null;
} else {
LastlegOrder order = requireDropshipOrder(customerId, input.orderId());
orderCache.put(order.getId(), order);
itemWarehouseId = order.getOverseasWarehouseId();
itemWarehouseName = order.getOverseasWarehouseName();
}
if (warehouseId == null) { if (warehouseId == null) {
warehouseId = required(order.getOverseasWarehouseId(), "Order has no operating overseas warehouse"); warehouseId = required(itemWarehouseId, "Inventory has no operating overseas warehouse");
warehouseName = trim(order.getOverseasWarehouseName()); warehouseName = trim(itemWarehouseName);
} else if (!warehouseId.equals(order.getOverseasWarehouseId())) { } else if (!warehouseId.equals(itemWarehouseId)) {
throw conflict("One cargo discard application cannot span operating warehouses"); throw conflict("One cargo discard application cannot span operating warehouses");
} }
total = Math.addExact(total, input.discardPackages()); total = Math.addExact(total, input.discardPackages());
@@ -116,7 +159,9 @@ public class CargoDiscardService {
application.setId(IdWorker.getId()); application.setCustomerId(customerId); application.setId(IdWorker.getId()); application.setCustomerId(customerId);
application.setDiscardNo(orderNoGenerator.next("DISCARD", warehouseId, application.getId())); application.setDiscardNo(orderNoGenerator.next("DISCARD", warehouseId, application.getId()));
application.setOverseasWarehouseId(warehouseId); application.setOverseasWarehouseName(warehouseName); application.setOverseasWarehouseId(warehouseId); application.setOverseasWarehouseName(warehouseName);
application.setInboundCodes(orderCache.values().stream().map(LastlegOrder::getInboundCode) application.setInboundCodes(java.util.stream.Stream.concat(
orderCache.values().stream().map(LastlegOrder::getInboundCode),
returnCartonCache.values().stream().map(ReturnInventoryCarton::getSourceReturnNo))
.distinct().sorted().collect(java.util.stream.Collectors.joining(","))); .distinct().sorted().collect(java.util.stream.Collectors.joining(",")));
application.setTotalPackages(total); application.setFeePerPackage(feePerPackage); application.setTotalPackages(total); application.setFeePerPackage(feePerPackage);
application.setTotalFee(feePerPackage.multiply(BigDecimal.valueOf(total))); application.setTotalFee(feePerPackage.multiply(BigDecimal.valueOf(total)));
@@ -157,8 +202,14 @@ public class CargoDiscardService {
initialOrderHistory.setEventId("CARGO_DISCARD_CREATE:" + application.getId()); initialOrderHistory.setRemark(trim(request.remark())); initialOrderHistory.setCreatedAt(now); initialOrderHistory.setEventId("CARGO_DISCARD_CREATE:" + application.getId()); initialOrderHistory.setRemark(trim(request.remark())); initialOrderHistory.setCreatedAt(now);
orderHistories.insert(initialOrderHistory); orderHistories.insert(initialOrderHistory);
Set<Long> reservedReturnCartons = new LinkedHashSet<>();
for (CargoDiscardRequests.Item input : request.items()) { for (CargoDiscardRequests.Item input : request.items()) {
reserve(customerId, application, orderCache.get(input.orderId()), input, now); if (input.returnCartonId() != null) {
reserveReturn(customerId, application, returnCartonCache.get(input.returnCartonId()), input, now,
reservedReturnCartons);
} else {
reserve(customerId, application, orderCache.get(input.orderId()), input, now);
}
} }
history(customerId, application.getId(), null, PENDING_REVIEW, operatorId, request.remark(), now); history(customerId, application.getId(), null, PENDING_REVIEW, operatorId, request.remark(), now);
return detail(customerId, application.getDiscardNo()); return detail(customerId, application.getDiscardNo());
@@ -215,7 +266,7 @@ public class CargoDiscardService {
.collect(java.util.stream.Collectors.groupingBy(CargoDiscardCarton::getItemId)); .collect(java.util.stream.Collectors.groupingBy(CargoDiscardCarton::getItemId));
String historyRemark = request.remark(); String historyRemark = request.remark();
if (REJECTED.equals(next)) { if (REJECTED.equals(next)) {
release(customerId, links); release(customerId, links, applicationItems);
} }
LocalDateTime reviewedAt = now(); LocalDateTime reviewedAt = now();
@@ -275,7 +326,7 @@ public class CargoDiscardService {
throw conflict("客户只能取消待审核的废弃申请"); throw conflict("客户只能取消待审核的废弃申请");
} }
List<CargoDiscardCarton> links = discardCartons.listApplication(customerId, application.getId()); List<CargoDiscardCarton> links = discardCartons.listApplication(customerId, application.getId());
release(customerId, links); release(customerId, links, items.listApplication(customerId, application.getId()));
LocalDateTime at = now(); LocalDateTime at = now();
String from = application.getStatus(); String from = application.getStatus();
application.setStatus(CANCELLED); application.setReviewedBy(platform ? operatorId : application.getReviewedBy()); application.setStatus(CANCELLED); application.setReviewedBy(platform ? operatorId : application.getReviewedBy());
@@ -309,6 +360,7 @@ public class CargoDiscardService {
if (sku == null) throw conflict("SKU inventory disappeared while reserving cargo"); if (sku == null) throw conflict("SKU inventory disappeared while reserving cargo");
CargoDiscardItem item = new CargoDiscardItem(); CargoDiscardItem item = new CargoDiscardItem();
item.setId(IdWorker.getId()); item.setApplicationId(application.getId()); item.setCustomerId(customerId); item.setId(IdWorker.getId()); item.setApplicationId(application.getId()); item.setCustomerId(customerId);
item.setSourceType("DROPSHIP");
item.setOrderId(order.getId()); item.setInboundCode(order.getInboundCode()); item.setProfileId(entry.getKey()); item.setOrderId(order.getId()); item.setInboundCode(order.getInboundCode()); item.setProfileId(entry.getKey());
item.setSkuInventoryId(sku.getId()); item.setSkuCode(skuCode); item.setProductName(sku.getProductName()); item.setSkuInventoryId(sku.getId()); item.setSkuCode(skuCode); item.setProductName(sku.getProductName());
item.setAvailablePackagesSnapshot(Math.toIntExact(inventory.countAvailableProfile( item.setAvailablePackagesSnapshot(Math.toIntExact(inventory.countAvailableProfile(
@@ -326,14 +378,50 @@ public class CargoDiscardService {
} }
} }
private void release(String customerId, List<CargoDiscardCarton> links) { private void release(String customerId, List<CargoDiscardCarton> links, List<CargoDiscardItem> applicationItems) {
for (CargoDiscardCarton link : links) { for (CargoDiscardCarton link : links) {
if (inventory.changeCartonStatus(customerId, link.getCartonId(), "DISCARD_PENDING", "AVAILABLE") != 1) { if (inventory.changeCartonStatus(customerId, link.getCartonId(), "DISCARD_PENDING", "AVAILABLE") != 1) {
throw conflict("Reserved cargo status changed before rejection"); throw conflict("Reserved cargo status changed before rejection");
} }
} }
applicationItems.stream().filter(this::isReturnItem).map(CargoDiscardItem::getReturnCartonId)
.filter(java.util.Objects::nonNull).distinct().forEach(cartonId -> {
if (returnCartons.releaseDiscard(cartonId, customerId) != 1) {
throw conflict("Reserved return inventory status changed before rejection");
}
});
} }
private void reserveReturn(String customerId, CargoDiscardApplication application, ReturnInventoryCarton carton,
CargoDiscardRequests.Item input, LocalDateTime now, Set<Long> reservedCartons) {
ReturnInventoryCartonItem sku = returnCartonItems.lockSku(carton.getId(), input.skuCode().trim());
if (sku == null || value(sku.getQuantity()) < input.discardPackages()) {
throw conflict("Available return inventory changed; retry the discard request");
}
if (reservedCartons.add(carton.getId()) && returnCartons.markDiscardPending(carton.getId(), customerId) != 1) {
throw conflict("Return inventory was reserved concurrently; retry the discard request");
}
CargoDiscardItem item = new CargoDiscardItem();
item.setId(IdWorker.getId()); item.setApplicationId(application.getId()); item.setCustomerId(customerId);
item.setSourceType("RETURN_CARTON"); item.setReturnCartonId(carton.getId()); item.setInboundCode(carton.getSourceReturnNo());
item.setSkuCode(sku.getSkuCode()); item.setProductName(sku.getProductNameZh());
item.setAvailablePackagesSnapshot(Math.toIntExact(value(sku.getQuantity())));
item.setDiscardPackages(input.discardPackages()); item.setCreatedAt(now); items.insert(item);
}
private ReturnInventoryCarton requireReturnCarton(String customerId, Long cartonId) {
ReturnInventoryCarton carton = returnCartons.lockId(cartonId);
if (carton == null || !customerId.equals(carton.getCustomerId())) throw notFound("Return inventory carton not found");
if (!"AVAILABLE".equals(carton.getStatus())) throw conflict("Return inventory carton is not available");
return carton;
}
private boolean isReturnItem(CargoDiscardItem item) {
return "RETURN_CARTON".equals(item.getSourceType()) || item.getReturnCartonId() != null;
}
private static long value(Long value) { return value == null ? 0L : value; }
private LastlegOrder requireDropshipOrder(String customerId, Long orderId) { private LastlegOrder requireDropshipOrder(String customerId, Long orderId) {
LastlegOrder order = orders.selectById(orderId); LastlegOrder order = orders.selectById(orderId);
if (order == null || !customerId.equals(order.getCustomerId())) throw notFound("Inbound order not found"); if (order == null || !customerId.equals(order.getCustomerId())) throw notFound("Inbound order not found");
@@ -385,6 +473,9 @@ public class CargoDiscardService {
private static LocalDateTime now() { return LocalDateTime.now(); } private static LocalDateTime now() { return LocalDateTime.now(); }
private static String trim(String value) { return value == null || value.isBlank() ? null : value.trim(); } private static String trim(String value) { return value == null || value.isBlank() ? null : value.trim(); }
private static boolean containsIgnoreCase(String value, String keyword) {
return value != null && keyword != null && value.toLowerCase(java.util.Locale.ROOT).contains(keyword.toLowerCase(java.util.Locale.ROOT));
}
private static String required(String value, String message) { String result = trim(value); if (result == null) throw conflict(message); return result; } private static String required(String value, String message) { String result = trim(value); if (result == null) throw conflict(message); return result; }
private static ResponseStatusException badRequest(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, message); } private static ResponseStatusException badRequest(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, message); }
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); } private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
@@ -401,7 +492,7 @@ public class CargoDiscardService {
private CargoDiscardResponses.Candidate response(long physicalPackages) { private CargoDiscardResponses.Candidate response(long physicalPackages) {
List<CargoDiscardResponses.CandidateSku> skuViews = skus.values().stream() List<CargoDiscardResponses.CandidateSku> skuViews = skus.values().stream()
.map(value -> new CargoDiscardResponses.CandidateSku(value.skuCode, value.productName, value.packages)).toList(); .map(value -> new CargoDiscardResponses.CandidateSku(value.skuCode, value.productName, value.packages)).toList();
return new CargoDiscardResponses.Candidate(first.getOrderId(), first.getInboundCode(), first.getInboundOrderNo(), return new CargoDiscardResponses.Candidate(first.getOrderId(), "DROPSHIP", null, null, first.getInboundCode(), first.getInboundOrderNo(),
first.getOverseasWarehouseId(), first.getOverseasWarehouseName(), first.getCreatedAt(), physicalPackages, skuViews); first.getOverseasWarehouseId(), first.getOverseasWarehouseName(), first.getCreatedAt(), physicalPackages, skuViews);
} }
} }
@@ -5,6 +5,7 @@ import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List; import java.util.List;
@@ -14,4 +15,16 @@ public interface ReturnInventoryCartonItemMapper extends BaseMapper<ReturnInvent
List<ReturnInventoryCartonItem> listCarton(@Param("cartonId") long cartonId); List<ReturnInventoryCartonItem> listCarton(@Param("cartonId") long cartonId);
@Select("SELECT * FROM ll_return_inventory_carton_item WHERE carton_id=#{cartonId} ORDER BY sku_code FOR UPDATE") @Select("SELECT * FROM ll_return_inventory_carton_item WHERE carton_id=#{cartonId} ORDER BY sku_code FOR UPDATE")
List<ReturnInventoryCartonItem> lockCarton(@Param("cartonId") long cartonId); List<ReturnInventoryCartonItem> lockCarton(@Param("cartonId") long cartonId);
@Select("SELECT * FROM ll_return_inventory_carton_item WHERE carton_id=#{cartonId} AND sku_code=#{skuCode} LIMIT 1 FOR UPDATE")
ReturnInventoryCartonItem lockSku(@Param("cartonId") long cartonId, @Param("skuCode") String skuCode);
@Update("UPDATE ll_return_inventory_carton_item SET quantity=quantity-#{quantity} WHERE id=#{id} AND quantity>=#{quantity}")
int deduct(@Param("id") long id, @Param("quantity") long quantity);
@Select("SELECT COALESCE(SUM(quantity),0) FROM ll_return_inventory_carton_item WHERE carton_id=#{cartonId}")
long totalQuantity(@Param("cartonId") long cartonId);
@Select("SELECT COUNT(*) FROM ll_return_inventory_carton_item WHERE carton_id=#{cartonId} AND quantity>0")
int skuTypes(@Param("cartonId") long cartonId);
} }
@@ -25,7 +25,9 @@ public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryC
List<ReturnInventoryCarton> merchantPage(@Param("customerId") String customerId, List<ReturnInventoryCarton> merchantPage(@Param("customerId") String customerId,
@Param("query") com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests.Query query, @Param("offset") long offset); @Param("query") com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests.Query query, @Param("offset") long offset);
@Select("SELECT * FROM ll_return_inventory_carton WHERE customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} AND status='AVAILABLE' ORDER BY created_at DESC,id DESC LIMIT 500") @Select("<script>SELECT * FROM ll_return_inventory_carton WHERE customer_id=#{customerId} AND status='AVAILABLE' " +
"<if test='warehouseId != null and warehouseId != \"\"'>AND overseas_warehouse_id=#{warehouseId} </if>" +
"ORDER BY created_at DESC,id DESC LIMIT 500</script>")
List<ReturnInventoryCarton> findAvailable(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId); List<ReturnInventoryCarton> findAvailable(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId);
@Select("SELECT * FROM ll_return_inventory_carton WHERE carton_mark=#{cartonMark} LIMIT 1") @Select("SELECT * FROM ll_return_inventory_carton WHERE carton_mark=#{cartonMark} LIMIT 1")
ReturnInventoryCarton findMark(@Param("cartonMark") String cartonMark); ReturnInventoryCarton findMark(@Param("cartonMark") String cartonMark);
@@ -36,6 +38,17 @@ public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryC
@Select("SELECT * FROM ll_return_inventory_carton WHERE id=#{id} FOR UPDATE") @Select("SELECT * FROM ll_return_inventory_carton WHERE id=#{id} FOR UPDATE")
ReturnInventoryCarton lockId(@Param("id") long id); ReturnInventoryCarton lockId(@Param("id") long id);
@Update("UPDATE ll_return_inventory_carton SET status='DISCARD_PENDING',updated_at=NOW(3) WHERE id=#{id} AND customer_id=#{customerId} AND status='AVAILABLE'")
int markDiscardPending(@Param("id") long id, @Param("customerId") String customerId);
@Update("UPDATE ll_return_inventory_carton SET status=#{status},total_quantity=#{totalQuantity},sku_types=#{skuTypes},updated_at=NOW(3) WHERE id=#{id} AND customer_id=#{customerId} AND status='DISCARD_PENDING'")
int finishDiscard(@Param("id") long id, @Param("customerId") String customerId,
@Param("status") String status, @Param("totalQuantity") long totalQuantity,
@Param("skuTypes") int skuTypes);
@Update("UPDATE ll_return_inventory_carton SET status='AVAILABLE',updated_at=NOW(3) WHERE id=#{id} AND customer_id=#{customerId} AND status='DISCARD_PENDING'")
int releaseDiscard(@Param("id") long id, @Param("customerId") String customerId);
@Select("SELECT COALESCE(SUM(ri.notified_package_count),0) FROM ll_relabel_item ri " + @Select("SELECT COALESCE(SUM(ri.notified_package_count),0) FROM ll_relabel_item ri " +
"JOIN ll_relabel_application ra ON ra.id=ri.application_id " + "JOIN ll_relabel_application ra ON ra.id=ri.application_id " +
"WHERE ri.return_carton_id=#{cartonId} AND ri.sku_code=#{skuCode} " + "WHERE ri.return_carton_id=#{cartonId} AND ri.sku_code=#{skuCode} " +
@@ -2074,8 +2074,9 @@ CREATE TABLE IF NOT EXISTS ll_cargo_discard_application (
CREATE TABLE IF NOT EXISTS ll_cargo_discard_item ( CREATE TABLE IF NOT EXISTS ll_cargo_discard_item (
id BIGINT NOT NULL COMMENT '主键ID', application_id BIGINT NOT NULL COMMENT '申请单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', id BIGINT NOT NULL COMMENT '主键ID', application_id BIGINT NOT NULL COMMENT '申请单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
order_id BIGINT NOT NULL COMMENT '订单ID', inbound_code VARCHAR(128) NOT NULL COMMENT '入仓单号快照', profile_id BIGINT NOT NULL COMMENT '托盘档案ID', order_id BIGINT NULL COMMENT '一件代发订单ID', source_type VARCHAR(32) NOT NULL DEFAULT 'DROPSHIP' COMMENT '库存来源:一件代发DROPSHIP、退货箱RETURN_CARTON',
sku_inventory_id BIGINT NOT NULL COMMENT 'SKU库存记录ID', sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', product_name VARCHAR(255) NULL COMMENT '产品名称', return_carton_id BIGINT NULL COMMENT '退货库存箱ID', inbound_code VARCHAR(128) NOT NULL COMMENT '入仓单号快照', profile_id BIGINT NULL COMMENT '托盘档案ID',
sku_inventory_id BIGINT NULL COMMENT 'SKU库存记录ID', sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', product_name VARCHAR(255) NULL COMMENT '产品名称',
available_packages_snapshot INT NOT NULL COMMENT '可用箱数快照', discard_packages INT NOT NULL COMMENT '申请废弃箱数', available_packages_snapshot INT NOT NULL COMMENT '可用箱数快照', discard_packages INT NOT NULL COMMENT '申请废弃箱数',
actual_discard_packages INT NULL COMMENT 'PDA确认实际废弃箱数', pda_status VARCHAR(32) NOT NULL DEFAULT 'PENDING' COMMENT 'PDA处理状态:待处理PENDING、已完成COMPLETED', actual_discard_packages INT NULL COMMENT 'PDA确认实际废弃箱数', pda_status VARCHAR(32) NOT NULL DEFAULT 'PENDING' COMMENT 'PDA处理状态:待处理PENDING、已完成COMPLETED',
pda_operator_id VARCHAR(128) NULL COMMENT 'PDA操作人', pda_completed_at DATETIME(3) NULL COMMENT 'PDA完成时间', pda_operator_id VARCHAR(128) NULL COMMENT 'PDA操作人', pda_completed_at DATETIME(3) NULL COMMENT 'PDA完成时间',
@@ -2636,17 +2637,17 @@ PREPARE return_receipt_carton_mark_stmt FROM @ddl; EXECUTE return_receipt_carton
CREATE TABLE IF NOT EXISTS ll_return_inventory_carton ( CREATE TABLE IF NOT EXISTS ll_return_inventory_carton (
new_carton_mark VARCHAR(255) NULL COMMENT '换标后新箱唛', new_carton_mark VARCHAR(255) NULL COMMENT '换标后新箱唛',
id BIGINT NOT NULL COMMENT '退货库存箱ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID', source_receipt_id BIGINT NULL COMMENT '来源PDA签收单ID', source_batch_no VARCHAR(128) NULL COMMENT '来源签收批次号', carton_mark VARCHAR(128) NOT NULL COMMENT '退货箱号和箱唛编码', status VARCHAR(32) NOT NULL COMMENT 'AVAILABLE可用或MERGED已合箱', sku_types INT NOT NULL COMMENT '箱内SKU种类数', total_quantity BIGINT NOT NULL COMMENT '箱内总件数', length DECIMAL(12,3) NULL COMMENT '箱长CM', width DECIMAL(12,3) NULL COMMENT '箱宽CM', height DECIMAL(12,3) NULL COMMENT '箱高CM', weight DECIMAL(12,3) NULL COMMENT '箱重KG', created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间', merged_at DATETIME(3) NULL COMMENT '合箱完成时间', id BIGINT NOT NULL COMMENT '退货库存箱ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID', source_receipt_id BIGINT NULL COMMENT '来源PDA签收单ID', source_batch_no VARCHAR(128) NULL COMMENT '来源签收批次号', carton_mark VARCHAR(128) NOT NULL COMMENT '退货箱号和箱唛编码', status VARCHAR(32) NOT NULL COMMENT 'AVAILABLE可用或MERGED已合箱', sku_types INT NOT NULL COMMENT '箱内SKU种类数', total_quantity BIGINT NOT NULL COMMENT '箱内总件数', length DECIMAL(12,3) NULL COMMENT '箱长CM', width DECIMAL(12,3) NULL COMMENT '箱宽CM', height DECIMAL(12,3) NULL COMMENT '箱高CM', weight DECIMAL(12,3) NULL COMMENT '箱重KG', created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间', merged_at DATETIME(3) NULL COMMENT '合箱完成时间',
PRIMARY KEY(id), UNIQUE KEY uk_ll_return_inventory_carton_mark(carton_mark), UNIQUE KEY uk_ll_return_inventory_carton_receipt(source_receipt_id), KEY idx_ll_return_inventory_carton_customer(customer_id,overseas_warehouse_id,status,created_at), CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK((status IN('AVAILABLE','RELABEL_PENDING','OUTBOUNDED') AND sku_types>0 AND total_quantity>0) OR (status='MERGED' AND sku_types=0 AND total_quantity=0)), CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','RELABEL_PENDING','MERGED','OUTBOUNDED')) PRIMARY KEY(id), UNIQUE KEY uk_ll_return_inventory_carton_mark(carton_mark), UNIQUE KEY uk_ll_return_inventory_carton_receipt(source_receipt_id), KEY idx_ll_return_inventory_carton_customer(customer_id,overseas_warehouse_id,status,created_at), CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK((status IN('AVAILABLE','RELABEL_PENDING','OUTBOUNDED','DISCARD_PENDING') AND sku_types>0 AND total_quantity>0) OR (status IN('MERGED','DISCARDED') AND sku_types=0 AND total_quantity=0)), CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','RELABEL_PENDING','MERGED','OUTBOUNDED','DISCARD_PENDING','DISCARDED'))
) ENGINE=InnoDB COMMENT='退货实物库存箱及箱唛'; ) ENGINE=InnoDB COMMENT='退货实物库存箱及箱唛';
SET @ll_return_carton_add_pallet = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='pallet_code'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN pallet_code VARCHAR(128) NULL COMMENT ''签收时绑定的托盘码'' AFTER source_return_no')); SET @ll_return_carton_add_pallet = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='pallet_code'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN pallet_code VARCHAR(128) NULL COMMENT ''签收时绑定的托盘码'' AFTER source_return_no'));
PREPARE ll_return_carton_add_pallet_stmt FROM @ll_return_carton_add_pallet; EXECUTE ll_return_carton_add_pallet_stmt; DEALLOCATE PREPARE ll_return_carton_add_pallet_stmt; PREPARE ll_return_carton_add_pallet_stmt FROM @ll_return_carton_add_pallet; EXECUTE ll_return_carton_add_pallet_stmt; DEALLOCATE PREPARE ll_return_carton_add_pallet_stmt;
-- 已合箱的来源箱保留审计记录,但其箱内库存必须归零;动态 SQL 兼容 Spring 的数据库初始化器。 -- 已合箱的来源箱保留审计记录,但其箱内库存必须归零;动态 SQL 兼容 Spring 的数据库初始化器。
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND constraint_name='chk_ll_return_inventory_carton_quantity'), 'ALTER TABLE ll_return_inventory_carton DROP CHECK chk_ll_return_inventory_carton_quantity', 'SELECT 1')); SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND constraint_name='chk_ll_return_inventory_carton_quantity'), 'ALTER TABLE ll_return_inventory_carton DROP CHECK chk_ll_return_inventory_carton_quantity', 'SELECT 1'));
PREPARE return_inventory_carton_check_drop_stmt FROM @ddl; EXECUTE return_inventory_carton_check_drop_stmt; DEALLOCATE PREPARE return_inventory_carton_check_drop_stmt; PREPARE return_inventory_carton_check_drop_stmt FROM @ddl; EXECUTE return_inventory_carton_check_drop_stmt; DEALLOCATE PREPARE return_inventory_carton_check_drop_stmt;
ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK((status IN('AVAILABLE','RELABEL_PENDING','OUTBOUNDED') AND sku_types>0 AND total_quantity>0) OR (status='MERGED' AND sku_types=0 AND total_quantity=0)); ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK((status IN('AVAILABLE','RELABEL_PENDING','OUTBOUNDED','DISCARD_PENDING') AND sku_types>0 AND total_quantity>0) OR (status IN('MERGED','DISCARDED') AND sku_types=0 AND total_quantity=0));
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND constraint_name='chk_ll_return_inventory_carton_status'), 'ALTER TABLE ll_return_inventory_carton DROP CHECK chk_ll_return_inventory_carton_status', 'SELECT 1')); SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND constraint_name='chk_ll_return_inventory_carton_status'), 'ALTER TABLE ll_return_inventory_carton DROP CHECK chk_ll_return_inventory_carton_status', 'SELECT 1'));
PREPARE return_inventory_carton_status_drop_stmt FROM @ddl; EXECUTE return_inventory_carton_status_drop_stmt; DEALLOCATE PREPARE return_inventory_carton_status_drop_stmt; PREPARE return_inventory_carton_status_drop_stmt FROM @ddl; EXECUTE return_inventory_carton_status_drop_stmt; DEALLOCATE PREPARE return_inventory_carton_status_drop_stmt;
ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','RELABEL_PENDING','MERGED','OUTBOUNDED')); ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','RELABEL_PENDING','MERGED','OUTBOUNDED','DISCARD_PENDING','DISCARDED'));
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='source_return_no'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN source_return_no VARCHAR(2000) NULL COMMENT ''来源退货单号'' AFTER source_batch_no')); SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='source_return_no'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN source_return_no VARCHAR(2000) NULL COMMENT ''来源退货单号'' AFTER source_batch_no'));
PREPARE return_inventory_carton_return_no_stmt FROM @ddl; EXECUTE return_inventory_carton_return_no_stmt; DEALLOCATE PREPARE return_inventory_carton_return_no_stmt; PREPARE return_inventory_carton_return_no_stmt FROM @ddl; EXECUTE return_inventory_carton_return_no_stmt; DEALLOCATE PREPARE return_inventory_carton_return_no_stmt;
ALTER TABLE ll_return_inventory_carton MODIFY COLUMN source_batch_no TEXT NULL COMMENT '实际消耗来源签收批次号,多个批次逗号分隔', MODIFY COLUMN source_return_no TEXT NULL COMMENT '实际消耗来源退货单号,多个单号逗号分隔'; ALTER TABLE ll_return_inventory_carton MODIFY COLUMN source_batch_no TEXT NULL COMMENT '实际消耗来源签收批次号,多个批次逗号分隔', MODIFY COLUMN source_return_no TEXT NULL COMMENT '实际消耗来源退货单号,多个单号逗号分隔';
@@ -64,8 +64,8 @@ class CargoDiscardMysqlAcceptanceTest {
seedCartons(990001003L, 990001103L, 990001601L, "PALLET-C", "SKU-MAT-BLUE", "MAT", 7); seedCartons(990001003L, 990001103L, 990001601L, "PALLET-C", "SKU-MAT-BLUE", "MAT", 7);
var approveRequest = new CargoDiscardRequests.Create(List.of( var approveRequest = new CargoDiscardRequests.Create(List.of(
new CargoDiscardRequests.Item(990001001L, "SKU-BOX-RED", 3), new CargoDiscardRequests.Item(990001001L, null, "SKU-BOX-RED", 3),
new CargoDiscardRequests.Item(990001001L, "SKU-RACK-KITCHEN", 2)), new CargoDiscardRequests.Item(990001001L, null, "SKU-RACK-KITCHEN", 2)),
"ACCEPT-DISCARD-APPROVE", "验收:破损货物,同意废弃"); "ACCEPT-DISCARD-APPROVE", "验收:破损货物,同意废弃");
var approved = service.create(CUSTOMER, "acceptance-creator", approveRequest); var approved = service.create(CUSTOMER, "acceptance-creator", approveRequest);
assertEquals(20L, skuPackages(990001001L)); assertEquals(20L, skuPackages(990001001L));
@@ -75,7 +75,7 @@ class CargoDiscardMysqlAcceptanceTest {
assertEquals(15L, skuPackages(990001001L)); assertEquals(15L, skuPackages(990001001L));
var rejected = service.create(CUSTOMER, "acceptance-creator", new CargoDiscardRequests.Create( var rejected = service.create(CUSTOMER, "acceptance-creator", new CargoDiscardRequests.Create(
List.of(new CargoDiscardRequests.Item(990001002L, "SKU-LAMP-WHITE", 4)), List.of(new CargoDiscardRequests.Item(990001002L, null, "SKU-LAMP-WHITE", 4)),
"ACCEPT-DISCARD-REJECT", "验收:外箱可修复")); "ACCEPT-DISCARD-REJECT", "验收:外箱可修复"));
assertEquals(10L, skuPackages(990001002L)); assertEquals(10L, skuPackages(990001002L));
rejected = service.review(CUSTOMER, rejected.summary().discardNo(), "acceptance-reviewer", rejected = service.review(CUSTOMER, rejected.summary().discardNo(), "acceptance-reviewer",
@@ -84,14 +84,14 @@ class CargoDiscardMysqlAcceptanceTest {
assertEquals(10L, skuPackages(990001002L)); assertEquals(10L, skuPackages(990001002L));
var pending = service.create(CUSTOMER, "acceptance-creator", new CargoDiscardRequests.Create( var pending = service.create(CUSTOMER, "acceptance-creator", new CargoDiscardRequests.Create(
List.of(new CargoDiscardRequests.Item(990001003L, "SKU-MAT-BLUE", 2)), List.of(new CargoDiscardRequests.Item(990001003L, null, "SKU-MAT-BLUE", 2)),
"ACCEPT-DISCARD-PENDING", "验收:等待仓库审核")); "ACCEPT-DISCARD-PENDING", "验收:等待仓库审核"));
assertEquals(CargoDiscardService.PENDING_REVIEW, pending.summary().status()); assertEquals(CargoDiscardService.PENDING_REVIEW, pending.summary().status());
assertEquals(7L, skuPackages(990001003L)); assertEquals(7L, skuPackages(990001003L));
assertThrows(ResponseStatusException.class, () -> service.create(CUSTOMER, "acceptance-creator", assertThrows(ResponseStatusException.class, () -> service.create(CUSTOMER, "acceptance-creator",
new CargoDiscardRequests.Create(List.of( new CargoDiscardRequests.Create(List.of(
new CargoDiscardRequests.Item(990001003L, "SKU-MAT-BLUE", 99)), new CargoDiscardRequests.Item(990001003L, null, "SKU-MAT-BLUE", 99)),
"ACCEPT-DISCARD-INSUFFICIENT", "验收:库存不足应失败"))); "ACCEPT-DISCARD-INSUFFICIENT", "验收:库存不足应失败")));
assertEquals(1, count("SELECT COUNT(*) FROM ll_cargo_discard_application WHERE customer_id=? AND status='APPROVED'")); assertEquals(1, count("SELECT COUNT(*) FROM ll_cargo_discard_application WHERE customer_id=? AND status='APPROVED'"));
@@ -17,6 +17,10 @@ import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.mapper.LastlegOrderMapper; import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper; import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator; import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -51,6 +55,8 @@ class CargoDiscardServiceTest {
@Mock CargoDiscardInventoryAdjustmentService adjustments; @Mock CargoDiscardInventoryAdjustmentService adjustments;
@Mock LastlegOrderMapper orders; @Mock LastlegOrderMapper orders;
@Mock LastlegOrderStatusHistoryMapper orderHistories; @Mock LastlegOrderStatusHistoryMapper orderHistories;
@Mock ReturnInventoryCartonMapper returnCartons;
@Mock ReturnInventoryCartonItemMapper returnCartonItems;
private CargoDiscardService service; private CargoDiscardService service;
private final AtomicReference<CargoDiscardApplication> storedApplication = new AtomicReference<>(); private final AtomicReference<CargoDiscardApplication> storedApplication = new AtomicReference<>();
@@ -70,6 +76,8 @@ class CargoDiscardServiceTest {
ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator()); ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator());
ReflectionTestUtils.setField(service, "orders", orders); ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "orderHistories", orderHistories); ReflectionTestUtils.setField(service, "orderHistories", orderHistories);
ReflectionTestUtils.setField(service, "returnCartons", returnCartons);
ReflectionTestUtils.setField(service, "returnCartonItems", returnCartonItems);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper()); ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
ReflectionTestUtils.setField(service, "feePerPackage", new BigDecimal("5.00")); ReflectionTestUtils.setField(service, "feePerPackage", new BigDecimal("5.00"));
lenient().when(applications.insert(any(CargoDiscardApplication.class))).thenAnswer(invocation -> { storedApplication.set(invocation.getArgument(0)); return 1; }); lenient().when(applications.insert(any(CargoDiscardApplication.class))).thenAnswer(invocation -> { storedApplication.set(invocation.getArgument(0)); return 1; });
@@ -94,7 +102,7 @@ class CargoDiscardServiceTest {
when(inventory.changeCartonStatus(eq("100"), anyLong(), eq("AVAILABLE"), eq("DISCARD_PENDING"))).thenReturn(1); when(inventory.changeCartonStatus(eq("100"), anyLong(), eq("AVAILABLE"), eq("DISCARD_PENDING"))).thenReturn(1);
var detail = service.create("100", "7", new CargoDiscardRequests.Create( var detail = service.create("100", "7", new CargoDiscardRequests.Create(
List.of(new CargoDiscardRequests.Item(101L, "SKU-1", 2)), "discard-create-1", "damaged")); List.of(new CargoDiscardRequests.Item(101L, null, "SKU-1", 2)), "discard-create-1", "damaged"));
assertEquals(CargoDiscardService.PENDING_REVIEW, detail.summary().status()); assertEquals(CargoDiscardService.PENDING_REVIEW, detail.summary().status());
assertEquals(2, detail.summary().totalPackages()); assertEquals(2, detail.summary().totalPackages());
@@ -117,6 +125,34 @@ class CargoDiscardServiceTest {
assertEquals(2, result.get(0).skus().size()); assertEquals(2, result.get(0).skus().size());
} }
@Test
void candidatesIncludeAvailableReturnCartonWithSourceIdentity() {
ReturnInventoryCarton carton = new ReturnInventoryCarton();
carton.setId(901L);
carton.setCustomerId("100");
carton.setOverseasWarehouseId("WH-1");
carton.setSourceReturnNo("RT-20260928-001");
carton.setCartonMark("RC-LAX-001");
carton.setStatus("AVAILABLE");
carton.setTotalQuantity(4L);
ReturnInventoryCartonItem sku = new ReturnInventoryCartonItem();
sku.setCartonId(901L);
sku.setSkuCode("SKU-RETURN");
sku.setProductNameZh("退货商品");
sku.setQuantity(4L);
when(returnCartons.findAvailable("100", null)).thenReturn(List.of(carton));
when(returnCartonItems.listCarton(901L)).thenReturn(List.of(sku));
var result = service.candidates("100", "RT-20260928-001");
assertEquals(1, result.size());
assertEquals("RETURN_CARTON", result.get(0).sourceType());
assertEquals(901L, result.get(0).returnCartonId());
assertEquals("RC-LAX-001", result.get(0).cartonMark());
assertEquals(4, result.get(0).availablePackages());
assertEquals("SKU-RETURN", result.get(0).skus().get(0).skuCode());
}
@Test @Test
void approvalPushesToPdaWithoutDeductingInventory() { void approvalPushesToPdaWithoutDeductingInventory() {
CargoDiscardApplication application = pendingApplication(); CargoDiscardApplication application = pendingApplication();