贴标申请,仓储转运
This commit is contained in:
+16
@@ -17,6 +17,9 @@ import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import com.baiyue.lastleg.service.LastlegOrderService;
|
||||
import com.baiyue.lastleg.service.LastlegOrderCommandService;
|
||||
import com.baiyue.lastleg.service.LoadingCompletedConsumer;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelResponses;
|
||||
import com.baiyue.lastleg.relabel.service.RelabelApplicationService;
|
||||
import com.baiyue.lastleg.shipping.service.ContainerShippingNodeConsumer;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.lastleg.wallet.WalletCommandCompletedConsumer;
|
||||
@@ -52,6 +55,7 @@ public class LastlegOrderController {
|
||||
@Resource private LastlegOrderCommandService commandService;
|
||||
@Resource private InternalServiceTokenGuard tokenGuard;
|
||||
@Resource private WalletCommandCompletedConsumer walletCommandCompletedConsumer;
|
||||
@Resource private RelabelApplicationService relabelApplicationService;
|
||||
|
||||
@PostMapping("/internal/lastleg/events")
|
||||
@Operation(summary = "Receive a first-leg loading or actual shipping-node event",
|
||||
@@ -163,6 +167,18 @@ public class LastlegOrderController {
|
||||
return ApiResponse.success(orderService.detail(platformScope(user, customerId), id));
|
||||
}
|
||||
|
||||
@PutMapping("/api/platform/v1/last-leg/orders/{id}/relabel-review")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EDIT + "')")
|
||||
@Operation(summary = "Review a directly-created labeling order in the tail-leg order workbench")
|
||||
public ApiResponse<RelabelResponses.Detail> reviewDirectRelabel(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable long id,
|
||||
@Valid @RequestBody RelabelRequests.ChangeStatus request) {
|
||||
return ApiResponse.success(relabelApplicationService.reviewDirectTailOrder(
|
||||
platformScope(user, customerId), id, operatorId(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/api/platform/v1/last-leg/orders/{id}/fees")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EDIT + "')")
|
||||
@Operation(summary = "Add a fee to one tail-leg order", description = "Required authority: " + OverseasPermissions.LAST_MILE_ORDER_EDIT)
|
||||
|
||||
+19
-14
@@ -430,33 +430,38 @@ public class DeliveryTransferService {
|
||||
new LambdaQueryWrapper<WarehouseTransferOrder>()
|
||||
.eq(StringUtils.hasText(customerId), WarehouseTransferOrder::getCustomerId, trim(customerId))
|
||||
.eq(StringUtils.hasText(warehouseId), WarehouseTransferOrder::getOperatingWarehouseId, trim(warehouseId))
|
||||
.eq(WarehouseTransferOrder::getStatus, "WAIT_FBA_DESTINATION")
|
||||
// 已确定目的仓的转运单在完成入库/换标后进入 READY_FOR_DISPATCH;
|
||||
// 未确定目的仓的历史单据仍保留 WAIT_FBA_DESTINATION,均应进入出库候选。
|
||||
.in(WarehouseTransferOrder::getStatus, "WAIT_FBA_DESTINATION", "READY_FOR_DISPATCH")
|
||||
.orderByDesc(WarehouseTransferOrder::getUpdatedAt));
|
||||
List<DeliveryResponses.WarehouseTransferOutboundCandidate> result = new ArrayList<>();
|
||||
for (WarehouseTransferOrder transfer : transfers) {
|
||||
boolean destinationBound = "FBA".equals(transfer.getDestinationType())
|
||||
? StringUtils.hasText(transfer.getDestinationWarehouseCode())
|
||||
: "PRIVATE".equals(transfer.getDestinationType())
|
||||
&& StringUtils.hasText(transfer.getPrivateDestinationAddress());
|
||||
// This screen is the destination-completion queue. Once a real
|
||||
// destination is bound, the remaining inventory belongs in smart
|
||||
// dispatch and must disappear from this list.
|
||||
if (destinationBound) continue;
|
||||
LastlegOrder order = tailOrders.selectById(transfer.getLastlegOrderId());
|
||||
List<WarehouseTransferPallet> completedPallets = warehouseTransferPallets.listOrder(
|
||||
transfer.getCustomerId(), transfer.getId()).stream()
|
||||
.filter(pallet -> "COMPLETED".equals(pallet.getStatus()) && pallet.getWarehousePalletId() != null)
|
||||
.toList();
|
||||
long available = 0;
|
||||
for (WarehouseTransferPallet pallet : completedPallets) {
|
||||
long palletAvailable = inventoryPositions.listPallet(transfer.getCustomerId(), pallet.getWarehousePalletId())
|
||||
.stream().filter(position -> transfer.getOperatingWarehouseId().equals(position.getOverseasWarehouseId()))
|
||||
.mapToLong(position -> value(position.getAvailablePackages())).sum();
|
||||
available += palletAvailable;
|
||||
if (palletAvailable <= 0) continue;
|
||||
result.add(new DeliveryResponses.WarehouseTransferOutboundCandidate(
|
||||
transfer.getCustomerId(), order == null ? null : order.getCustomerName(), transfer.getTransferNo(),
|
||||
transfer.getOperatingWarehouseId(), transfer.getStatus(), Boolean.TRUE.equals(transfer.getNeedRelabel()),
|
||||
transfer.getRelabelNo(), transfer.getDestinationType(), transfer.getDestinationWarehouseCode(),
|
||||
transfer.getPrivateDestinationAddress(), transfer.getPrivateDestinationPostalCode(),
|
||||
pallet.getId(), pallet.getWarehousePalletId(), pallet.getPalletNo(), Math.toIntExact(palletAvailable),
|
||||
pallet.getGrossWeightKg(), pallet.getVolumeCbm()));
|
||||
}
|
||||
if (available <= 0 || completedPallets.isEmpty()) continue;
|
||||
result.add(new DeliveryResponses.WarehouseTransferOutboundCandidate(
|
||||
transfer.getCustomerId(), order == null ? null : order.getCustomerName(), transfer.getTransferNo(),
|
||||
transfer.getOperatingWarehouseId(), transfer.getStatus(), Boolean.TRUE.equals(transfer.getNeedRelabel()),
|
||||
transfer.getRelabelNo(), transfer.getDestinationType(), transfer.getDestinationWarehouseCode(),
|
||||
transfer.getPrivateDestinationAddress(), transfer.getPrivateDestinationPostalCode(),
|
||||
null, null, null, Math.toIntExact(available),
|
||||
completedPallets.stream().map(WarehouseTransferPallet::getGrossWeightKg).filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add),
|
||||
completedPallets.stream().map(WarehouseTransferPallet::getVolumeCbm).filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+32
-16
@@ -60,7 +60,7 @@ public interface DispatchCandidateMapper {
|
||||
"GROUP_CONCAT(DISTINCT o.order_no ORDER BY o.order_no SEPARATOR ',') AS orderRefs,MIN(l.inbound_code) AS inboundCode," +
|
||||
"p.pallet_no AS palletNo,p.destination_warehouse_id AS destinationWarehouseId," +
|
||||
"p.destination_warehouse_code AS destinationWarehouseCode,p.destination_warehouse_name AS destinationWarehouseName," +
|
||||
"MIN(l.fba_warehouse_code) AS fbaWarehouseCode," +
|
||||
"COALESCE(NULLIF(MIN(l.fba_warehouse_code),''),NULLIF(p.destination_warehouse_code,'')) AS fbaWarehouseCode," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN COALESCE((SELECT SUM(pos.available_packages) " +
|
||||
"FROM ll_inventory_position pos WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id),0) " +
|
||||
"ELSE COALESCE(SUM(CASE WHEN a.picked_package_count > 0 THEN a.picked_package_count " +
|
||||
@@ -79,15 +79,20 @@ public interface DispatchCandidateMapper {
|
||||
"WHERE ri.customer_id=ip.customer_id AND rp.pallet_id=p.warehouse_pallet_id " +
|
||||
"AND ri.fba_carton_mark IS NOT NULL AND ri.fba_carton_mark != '' " +
|
||||
"AND ra.status NOT IN ('REJECTED','CANCELLED')) AS relabelCartonMarks," +
|
||||
"MIN(l.destination_type) AS destinationType,MIN(l.delivery_address) AS deliveryAddress," +
|
||||
"COALESCE(NULLIF(MIN(l.destination_type),''),NULLIF(p.destination_type,'')) AS destinationType,COALESCE(MIN(l.delivery_address),p.delivery_address) AS deliveryAddress," +
|
||||
"MIN(l.postal_code) AS postalCode,MIN(l.contact_name) AS contactName,MIN(l.contact_phone) AS contactPhone " +
|
||||
"FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id " +
|
||||
"LEFT JOIN ll_warehouse_transfer_order wt ON wt.customer_id=ip.customer_id AND wt.lastleg_order_id=ip.source_order_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_order o ON o.id=l.order_id " +
|
||||
"WHERE ((l.destination_type='FBA' AND NULLIF(TRIM(l.fba_warehouse_code),'') IS NOT NULL) OR " +
|
||||
"WHERE ((ip.creation_mode='WAREHOUSE_TRANSFER' AND ((wt.destination_type='FBA' AND NULLIF(TRIM(wt.destination_warehouse_code),'') IS NOT NULL) OR " +
|
||||
"(wt.destination_type='PRIVATE' AND NULLIF(TRIM(wt.private_destination_address),'') IS NOT NULL) OR " +
|
||||
"(p.destination_type='FBA' AND NULLIF(TRIM(p.destination_warehouse_code),'') IS NOT NULL) OR " +
|
||||
"(p.destination_type='PRIVATE' AND NULLIF(TRIM(p.delivery_address),'') IS NOT NULL))) OR " +
|
||||
"(ip.creation_mode!='WAREHOUSE_TRANSFER' AND ((l.destination_type='FBA' AND NULLIF(TRIM(l.fba_warehouse_code),'') IS NOT NULL) OR " +
|
||||
"(l.destination_type='PRIVATE' AND NULLIF(TRIM(l.delivery_address),'') IS NOT NULL AND NULLIF(TRIM(l.postal_code),'') IS NOT NULL " +
|
||||
"AND NULLIF(TRIM(l.contact_name),'') IS NOT NULL AND NULLIF(TRIM(l.contact_phone),'') IS NOT NULL)) " +
|
||||
"AND NULLIF(TRIM(l.contact_name),'') IS NOT NULL AND NULLIF(TRIM(l.contact_phone),'') IS NOT NULL)))) " +
|
||||
"<if test='customerId != null and customerId != \"\"'> AND ip.customer_id=#{customerId}</if> " +
|
||||
"<if test='planIds != null and planIds.size() > 0'> AND p.plan_id IN " +
|
||||
"<foreach collection='planIds' item='id' open='(' separator=',' close=')'>#{id}</foreach></if> " +
|
||||
@@ -107,12 +112,18 @@ public interface DispatchCandidateMapper {
|
||||
"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))) " +
|
||||
"AND (#{draftToken} IS NULL OR d.draft_token IS NULL OR d.draft_token != #{draftToken})) " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
||||
// Warehouse-transfer inventory is intentionally reserved before dispatch. It must still be
|
||||
// eligible for the normal smart-dispatch flow once its destination is bound; other order
|
||||
// types keep the reservation guard to prevent duplicate dispatching.
|
||||
"AND (ip.creation_mode='WAREHOUSE_TRANSFER' OR NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
||||
"JOIN ll_inventory_position tp ON tp.id=ta.inventory_position_id " +
|
||||
"WHERE ta.customer_id=ip.customer_id AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id) " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_relabel_item rli JOIN ll_relabel_application rla ON rla.id=rli.application_id AND rla.customer_id=rli.customer_id " +
|
||||
"WHERE ta.customer_id=ip.customer_id AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id)) " +
|
||||
// A warehouse-transfer pallet may contain both relabel and non-relabel inventory.
|
||||
// Do not hide the whole pallet because one carton/SKU has an active relabel item;
|
||||
// the remaining available inventory must still be dispatchable.
|
||||
"AND (ip.creation_mode!='WAREHOUSE_TRANSFER' OR NOT EXISTS (SELECT 1 FROM ll_relabel_item rli JOIN ll_relabel_application rla ON rla.id=rli.application_id AND rla.customer_id=rli.customer_id " +
|
||||
"WHERE rli.customer_id=ip.customer_id AND rli.source_inbound_order_id IS NOT NULL " +
|
||||
"AND rli.source_inbound_order_id=o.id AND rla.status NOT IN ('REJECTED','CANCELLED','COMPLETED')) " +
|
||||
"AND rli.source_inbound_order_id=o.id AND rla.status NOT IN ('REJECTED','CANCELLED','COMPLETED'))) " +
|
||||
"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 " +
|
||||
"ORDER BY p.plan_id,p.sequence_no" +
|
||||
@@ -160,7 +171,7 @@ public interface DispatchCandidateMapper {
|
||||
"GROUP_CONCAT(DISTINCT o.order_no ORDER BY o.order_no SEPARATOR ',') AS orderRefs,MIN(l.inbound_code) AS inboundCode," +
|
||||
"p.pallet_no AS palletNo,p.destination_warehouse_id AS destinationWarehouseId," +
|
||||
"p.destination_warehouse_code AS destinationWarehouseCode,p.destination_warehouse_name AS destinationWarehouseName," +
|
||||
"MIN(l.fba_warehouse_code) AS fbaWarehouseCode," +
|
||||
"COALESCE(NULLIF(MIN(l.fba_warehouse_code),''),NULLIF(p.destination_warehouse_code,'')) AS fbaWarehouseCode," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN COALESCE((SELECT SUM(pos.available_packages) " +
|
||||
"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 > 0 THEN a.picked_package_count " +
|
||||
@@ -179,31 +190,36 @@ public interface DispatchCandidateMapper {
|
||||
"WHERE ri.customer_id=ip.customer_id AND rp.pallet_id=p.warehouse_pallet_id " +
|
||||
"AND ri.fba_carton_mark IS NOT NULL AND ri.fba_carton_mark != '' " +
|
||||
"AND ra.status NOT IN ('REJECTED','CANCELLED')) AS relabelCartonMarks," +
|
||||
"MIN(l.destination_type) AS destinationType,MIN(l.delivery_address) AS deliveryAddress," +
|
||||
"COALESCE(NULLIF(MIN(l.destination_type),''),NULLIF(p.destination_type,'')) AS destinationType,COALESCE(MIN(l.delivery_address),p.delivery_address) AS deliveryAddress," +
|
||||
"MIN(l.postal_code) AS postalCode,MIN(l.contact_name) AS contactName,MIN(l.contact_phone) AS contactPhone " +
|
||||
"FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id " +
|
||||
"LEFT JOIN ll_warehouse_transfer_order wt ON wt.customer_id=ip.customer_id AND wt.lastleg_order_id=ip.source_order_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_order o ON o.id=l.order_id " +
|
||||
"WHERE ip.customer_id=#{customerId} AND p.id IN " +
|
||||
"<foreach collection='planPalletIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||
"AND ((l.destination_type='FBA' AND NULLIF(TRIM(l.fba_warehouse_code),'') IS NOT NULL) OR " +
|
||||
"AND ((ip.creation_mode='WAREHOUSE_TRANSFER' AND ((wt.destination_type='FBA' AND NULLIF(TRIM(wt.destination_warehouse_code),'') IS NOT NULL) OR " +
|
||||
"(wt.destination_type='PRIVATE' AND NULLIF(TRIM(wt.private_destination_address),'') IS NOT NULL) OR " +
|
||||
"(p.destination_type='FBA' AND NULLIF(TRIM(p.destination_warehouse_code),'') IS NOT NULL) OR " +
|
||||
"(p.destination_type='PRIVATE' AND NULLIF(TRIM(p.delivery_address),'') IS NOT NULL))) OR " +
|
||||
"(ip.creation_mode!='WAREHOUSE_TRANSFER' AND ((l.destination_type='FBA' AND NULLIF(TRIM(l.fba_warehouse_code),'') IS NOT NULL) OR " +
|
||||
"(l.destination_type='PRIVATE' AND NULLIF(TRIM(l.delivery_address),'') IS NOT NULL AND NULLIF(TRIM(l.postal_code),'') IS NOT NULL " +
|
||||
"AND NULLIF(TRIM(l.contact_name),'') IS NOT NULL AND NULLIF(TRIM(l.contact_phone),'') IS NOT NULL)) " +
|
||||
"AND NULLIF(TRIM(l.contact_name),'') IS NOT NULL AND NULLIF(TRIM(l.contact_phone),'') IS NOT NULL)))) " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_inbound_plan_pallet_allocation ha " +
|
||||
"JOIN ll_inbound_plan_line hl ON hl.id=ha.line_id " +
|
||||
"JOIN ll_order ho ON ho.id=hl.order_id AND ho.customer_id=ip.customer_id " +
|
||||
"WHERE ha.pallet_id=p.id AND ho.detained=1) " +
|
||||
"AND (p.warehouse_pallet_id IS NULL OR EXISTS (SELECT 1 FROM ll_inventory_position pos " +
|
||||
"WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0)) " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
||||
"AND (ip.creation_mode='WAREHOUSE_TRANSFER' OR NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
||||
"JOIN ll_inventory_position tp ON tp.id=ta.inventory_position_id " +
|
||||
"WHERE ta.customer_id=#{customerId} AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id) " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_relabel_item rli JOIN ll_relabel_application rla ON rla.id=rli.application_id AND rla.customer_id=rli.customer_id " +
|
||||
"WHERE ta.customer_id=#{customerId} AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id)) " +
|
||||
"AND (ip.creation_mode='WAREHOUSE_TRANSFER' OR NOT EXISTS (SELECT 1 FROM ll_relabel_item rli JOIN ll_relabel_application rla ON rla.id=rli.application_id AND rla.customer_id=rli.customer_id " +
|
||||
"JOIN ll_inbound_plan_line rpl ON rpl.order_id=rli.source_inbound_order_id " +
|
||||
"WHERE rli.customer_id=#{customerId} AND rli.source_inbound_order_id IS NOT NULL AND rpl.id IN " +
|
||||
"(SELECT pa.line_id FROM ll_inbound_plan_pallet_allocation pa WHERE pa.pallet_id=p.id) " +
|
||||
"AND rla.status NOT IN ('REJECTED','CANCELLED','COMPLETED')) " +
|
||||
"AND rla.status NOT IN ('REJECTED','CANCELLED','COMPLETED'))) " +
|
||||
"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 " +
|
||||
"ORDER BY p.id FOR UPDATE" +
|
||||
|
||||
+6
-1
@@ -241,7 +241,12 @@ public class DispatchPdaWorkflowService {
|
||||
}
|
||||
}
|
||||
for (DispatchItem item : dispatchItems) {
|
||||
if (item.getOrderId() != null) warehouseTransfers.markOutboundByOrder(customerId, item.getOrderId(), now);
|
||||
// A relabel dispatch may consume only part of a warehouse-transfer
|
||||
// pallet. Do not close the whole transfer by its parent order; the
|
||||
// remaining cartons still need destination binding/outbound.
|
||||
if (item.getOrderId() != null && !"RELABEL_ORDER".equals(item.getSourceBusinessType())) {
|
||||
warehouseTransfers.markOutboundByOrder(customerId, item.getOrderId(), now);
|
||||
}
|
||||
if ("RELABEL_ORDER".equals(item.getSourceBusinessType()) && item.getSourceBusinessNo() != null) {
|
||||
warehouseTransfers.markOutboundByRelabel(customerId, item.getSourceBusinessNo(), now);
|
||||
}
|
||||
|
||||
+6
@@ -53,6 +53,12 @@ public class MerchantRelabelApplicationController {
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RELABEL_LIST + "')")
|
||||
public ApiResponse<RelabelResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo) { return ApiResponse.success(service.detail(customer(user), relabelNo)); }
|
||||
@PutMapping("/{relabelNo}/resubmit")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RELABEL_CREATE + "')")
|
||||
public ApiResponse<RelabelResponses.Detail> resubmit(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo, @Valid @RequestBody RelabelRequests.Resubmit request) {
|
||||
return ApiResponse.success(service.resubmit(customer(user), relabelNo, operator(user), request));
|
||||
}
|
||||
@PostMapping(value = "/{relabelNo}/shipping-label", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RELABEL_FILES + "')")
|
||||
public ApiResponse<RelabelResponses.Detail> uploadShippingLabel(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
@@ -129,6 +129,12 @@ public final class RelabelRequests {
|
||||
public record ChangeStatus(@NotBlank @Size(max = 32) String status,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
public record Resubmit(@NotNull LocalDateTime labelDeadline,
|
||||
@NotBlank @Size(max = 128) String contactName,
|
||||
@NotBlank @Size(max = 64) String contactPhone,
|
||||
@Size(max = 1000) String remark,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) {}
|
||||
|
||||
public record CustomerConfirm(@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ public final class RelabelResponses {
|
||||
String walletErrorMessage, String deliveryMethod, boolean printShippingLabel,
|
||||
String shippingLabelFileId, String shippingLabelFileName, String shippingLabelFileUrl,
|
||||
String fulfillmentStatus, String outboundOrderNo, LocalDateTime customerConfirmedAt,
|
||||
LocalDateTime createdAt) {}
|
||||
String reviewChannel, String reviewRemark, LocalDateTime createdAt) {}
|
||||
public record Item(Long id, String skuCode, String productImageFileId, String productImageFileName,
|
||||
String productImageUrl, String productNameZh, int totalPackageCount,
|
||||
int notifiedPackageCount, String shipmentNo, String trackingCode,
|
||||
|
||||
+2
-1
@@ -9,7 +9,8 @@ public final class PdaRelabelResponses {
|
||||
public record Candidate(String relabelNo, String customerOrderNo, LocalDateTime labelDeadline,
|
||||
String applicationStatus, String taskStatus, String operationType, int itemCount,
|
||||
int expectedPackages, int relabelledPackages, int skippedPackages,
|
||||
int exceptionPackages, int version, boolean claimedByCurrentOperator) { }
|
||||
int exceptionPackages, int version, boolean claimedByCurrentOperator,
|
||||
String reviewDecision, String reviewRemark, LocalDateTime reviewedAt) { }
|
||||
|
||||
public record Detail(String relabelNo, String customerOrderNo, LocalDateTime labelDeadline,
|
||||
String contactName, String contactPhone, String applicationStatus, String operationType,
|
||||
|
||||
+2
-1
@@ -35,7 +35,8 @@ public interface PdaRelabelTaskMapper extends BaseMapper<PdaRelabelTask> {
|
||||
relabelled_packages=#{task.relabelledPackages}, skipped_packages=#{task.skippedPackages},
|
||||
exception_packages=#{task.exceptionPackages}, completion_photo_file_ids=#{task.completionPhotoFileIds},
|
||||
completion_remark=#{task.completionRemark}, review_decision=#{task.reviewDecision},
|
||||
review_remark=#{task.reviewRemark}, submitted_at=#{task.submittedAt}, reviewed_at=#{task.reviewedAt},
|
||||
review_remark=#{task.reviewRemark}, claimed_at=#{task.claimedAt},
|
||||
submitted_at=#{task.submittedAt}, reviewed_at=#{task.reviewedAt},
|
||||
version=version+1, updated_at=#{task.updatedAt}
|
||||
WHERE id=#{task.id} AND customer_id=#{task.customerId} AND version=#{expectedVersion}
|
||||
""")
|
||||
|
||||
+16
-4
@@ -53,6 +53,7 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class PdaRelabelService {
|
||||
@Resource private com.baiyue.lastleg.relabel.service.RelabelBindingGuard bindingGuard;
|
||||
public static final String AVAILABLE = "AVAILABLE";
|
||||
public static final String PROCESSING = "PROCESSING";
|
||||
public static final String WAIT_REVIEW = "WAIT_REVIEW";
|
||||
public static final String COMPLETED = "COMPLETED";
|
||||
@@ -116,7 +117,16 @@ public class PdaRelabelService {
|
||||
throw conflict("当前账号正在执行换标单 " + activeTask.getRelabelNo() + ",请先完成后再操作其他换标单");
|
||||
}
|
||||
if (task != null) {
|
||||
if (!operatorKey.equals(task.getOperatorKey()) || !PROCESSING.equals(task.getStatus())) {
|
||||
if (AVAILABLE.equals(task.getStatus()) && "REWORK".equals(task.getReviewDecision())) {
|
||||
int expectedVersion = task.getVersion();
|
||||
task.setStatus(PROCESSING);
|
||||
task.setOperatorKey(operatorKey);
|
||||
task.setOperatorName(operatorName);
|
||||
task.setClaimedAt(LocalDateTime.now());
|
||||
updateTask(task, expectedVersion);
|
||||
insertHistory(task, "REWORK_CLAIM", AVAILABLE, PROCESSING, operatorKey, operatorName,
|
||||
Map.of("relabelNo", relabelNo));
|
||||
} else if (!operatorKey.equals(task.getOperatorKey()) || !PROCESSING.equals(task.getStatus())) {
|
||||
throw conflict("Relabel task is already claimed or awaiting review");
|
||||
}
|
||||
} else {
|
||||
@@ -488,7 +498,7 @@ public class PdaRelabelService {
|
||||
requireVersion(task, request.expectedVersion());
|
||||
// Preserve the previous round before freeing the active-label uniqueness constraint.
|
||||
List<PdaRelabelUnitFact> previousFacts = factList(task);
|
||||
insertHistory(task, "REWORK_ARCHIVE", WAIT_REVIEW, PROCESSING, operatorKey, operatorName,
|
||||
insertHistory(task, "REWORK_ARCHIVE", WAIT_REVIEW, AVAILABLE, operatorKey, operatorName,
|
||||
Map.of("facts", previousFacts, "photoFileIds", Objects.toString(task.getCompletionPhotoFileIds(), "[]"),
|
||||
"completionRemark", Objects.toString(task.getCompletionRemark(), ""), "remark", request.remark()));
|
||||
for (PdaRelabelUnitFact fact : previousFacts) {
|
||||
@@ -501,7 +511,7 @@ public class PdaRelabelService {
|
||||
applicationService.resetForRework(application.getCustomerId(), relabelNo, operatorKey, request.remark());
|
||||
application.setStatus(PROCESSING);
|
||||
String before = task.getStatus();
|
||||
String after = PROCESSING;
|
||||
String after = AVAILABLE;
|
||||
task.setStatus(after); task.setReviewerKey(operatorKey); task.setReviewerName(operatorName);
|
||||
task.setReviewDecision(decision); task.setReviewRemark(trim(request.remark()));
|
||||
task.setReviewedAt(LocalDateTime.now());
|
||||
@@ -694,7 +704,9 @@ public class PdaRelabelService {
|
||||
task == null ? value(application.getNotifiedPackageCount()) : value(task.getExpectedPackages()),
|
||||
task == null ? 0 : value(task.getRelabelledPackages()), task == null ? 0 : value(task.getSkippedPackages()),
|
||||
task == null ? 0 : value(task.getExceptionPackages()), task == null ? 0 : value(task.getVersion()),
|
||||
task != null && operatorKey.equals(task.getOperatorKey()));
|
||||
task != null && PROCESSING.equals(task.getStatus()) && operatorKey.equals(task.getOperatorKey()),
|
||||
task == null ? null : task.getReviewDecision(), task == null ? null : task.getReviewRemark(),
|
||||
task == null ? null : task.getReviewedAt());
|
||||
}
|
||||
|
||||
private PdaRelabelResponses.Detail detail(RelabelApplication application, PdaRelabelTask task) {
|
||||
|
||||
+106
-19
@@ -332,7 +332,11 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
private RelabelResponses.Detail create(String customerId, String operatorId, RelabelRequests.Create request,
|
||||
Long existingTailOrderId) {
|
||||
if (!StringUtils.hasText(request.operationWarehouseId())) throw badRequest("Operation warehouse is required");
|
||||
boolean requiresOutbound = request.items().stream().anyMatch(item -> !"SKU_LABEL".equals(operationType(item)));
|
||||
LastlegOrder linkedSourceOrder = existingTailOrderId == null ? null : orders.selectById(existingTailOrderId);
|
||||
boolean warehouseTransferRelabel = linkedSourceOrder != null
|
||||
&& "WAREHOUSE_TRANSFER".equals(linkedSourceOrder.getOrderType());
|
||||
boolean requiresOutbound = warehouseTransferRelabel
|
||||
|| request.items().stream().anyMatch(item -> !"SKU_LABEL".equals(operationType(item)));
|
||||
String deliveryMethod = trim(request.deliveryMethod());
|
||||
if (requiresOutbound && (!StringUtils.hasText(deliveryMethod) || !DELIVERY_METHODS.contains(deliveryMethod))) {
|
||||
throw badRequest("Delivery method must be TRUCK, PICKUP or EXPRESS when carton labels are changed");
|
||||
@@ -446,7 +450,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
application.setPrintShippingLabel(Boolean.TRUE.equals(request.printShippingLabel()));
|
||||
application.setFulfillmentStatus(requiresOutbound ? "WAIT_RELABEL" : "NOT_REQUIRED");
|
||||
application.setRemark(trim(request.remark()));
|
||||
application.setStatus(PENDING_PROCESSING);
|
||||
application.setStatus(PENDING_REVIEW);
|
||||
application.setItemCount(request.items().size());
|
||||
application.setTotalPackageCount(total);
|
||||
application.setNotifiedPackageCount(notified);
|
||||
@@ -476,7 +480,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
for (SourceInventory source : sources) {
|
||||
insertItem(customerId, applicationId, source, now);
|
||||
}
|
||||
insertHistory(customerId, applicationId, null, PENDING_PROCESSING, operatorId, "Relabel application submitted", now);
|
||||
insertHistory(customerId, applicationId, null, PENDING_REVIEW, operatorId, "Relabel application submitted for review", now);
|
||||
if (existingTailOrderId == null) {
|
||||
tailOrders.forEach(order -> insertOrderHistory(customerId, order.getId(), null,
|
||||
order.getCurrentStatus(), "Relabel application submitted", now));
|
||||
@@ -541,6 +545,24 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
@Transactional
|
||||
public RelabelResponses.Detail changeStatus(String customerId, String relabelNo, String operatorId,
|
||||
RelabelRequests.ChangeStatus request) {
|
||||
return changeStatus(customerId, relabelNo, operatorId, request, false);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail reviewDirectTailOrder(String customerId, long orderId, String operatorId,
|
||||
RelabelRequests.ChangeStatus request) {
|
||||
LastlegOrder order = orders.selectById(orderId);
|
||||
if (order == null || !customerId.equals(order.getCustomerId())
|
||||
|| !"RELABEL_APPLICATION".equals(order.getOrderType())
|
||||
|| !StringUtils.hasText(order.getRelabelOrderNo())) {
|
||||
throw notFound("Direct relabel tail order not found");
|
||||
}
|
||||
return changeStatus(customerId, order.getRelabelOrderNo(), operatorId, request, true);
|
||||
}
|
||||
|
||||
private RelabelResponses.Detail changeStatus(String customerId, String relabelNo, String operatorId,
|
||||
RelabelRequests.ChangeStatus request,
|
||||
boolean directTailOrderReview) {
|
||||
RelabelApplication application = StringUtils.hasText(customerId)
|
||||
? applications.lockNo(customerId, relabelNo)
|
||||
: applications.lockAnyByRelabelNo(relabelNo);
|
||||
@@ -548,6 +570,16 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
customerId = application.getCustomerId();
|
||||
String next = request.status().trim().toUpperCase();
|
||||
if (!STATUSES.contains(next)) throw badRequest("Unsupported relabel status");
|
||||
if ("REJECTED".equals(next) && !StringUtils.hasText(request.remark())) {
|
||||
throw badRequest("审核驳回必须填写原因");
|
||||
}
|
||||
LastlegOrder primaryOrder = orders.selectById(application.getLastlegOrderId());
|
||||
boolean directApplication = primaryOrder != null && "RELABEL_APPLICATION".equals(primaryOrder.getOrderType());
|
||||
if (PENDING_REVIEW.equals(application.getStatus()) && directApplication != directTailOrderReview) {
|
||||
throw conflict(directApplication
|
||||
? "直接创建的贴标单请在尾程订单审核"
|
||||
: "关联业务产生的换标单请在换标单审核");
|
||||
}
|
||||
if (next.equals(application.getStatus())) return detail(customerId, relabelNo);
|
||||
validateTransition(application.getStatus(), next);
|
||||
if (PENDING_REVIEW.equals(application.getStatus()) && PENDING_PROCESSING.equals(next)
|
||||
@@ -614,7 +646,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
case CUSTOMER_CONFIRM_PENDING -> mainStatus;
|
||||
case "COMPLETED" -> "RETURN_RELABEL".equals(order.getOrderType())
|
||||
? "PENDING_ASSIGNMENT" : "COMPLETED";
|
||||
case "REJECTED", "CANCELLED" -> "CANCELLED";
|
||||
case "REJECTED" -> directApplication ? "REJECTED" : "CANCELLED";
|
||||
case "CANCELLED" -> "CANCELLED";
|
||||
default -> mainStatus;
|
||||
};
|
||||
// Reviewing/processing a return relabel must not move the already
|
||||
@@ -700,6 +733,51 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
return detail(customerId, relabelNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail resubmit(String customerId, String relabelNo, String operatorId,
|
||||
RelabelRequests.Resubmit request) {
|
||||
RelabelApplication application = applications.lockNo(customerId, relabelNo);
|
||||
if (application == null) throw notFound("Relabel application not found");
|
||||
if (!"REJECTED".equals(application.getStatus())) {
|
||||
throw conflict("Only a rejected relabel application can be resubmitted");
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int holdIndex = 0;
|
||||
for (RelabelItem item : itemList(application.getId())) {
|
||||
if (item.getReturnCartonId() != null) {
|
||||
ReturnInventoryCarton carton = returnCartons.lockId(item.getReturnCartonId());
|
||||
if (carton == null || !customerId.equals(carton.getCustomerId())
|
||||
|| !"AVAILABLE".equals(carton.getStatus())
|
||||
|| returnCartons.markRelabelPending(carton.getId(), application.getId(), relabelNo, now) != 1) {
|
||||
throw conflict("退货箱已被其他作业占用,无法重新提交");
|
||||
}
|
||||
} else if (item.getSourceInventoryPositionId() != null
|
||||
&& Objects.requireNonNullElse(item.getNotifiedPackageCount(), 0) > 0) {
|
||||
inventoryCommands.hold(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||
item.getSourceInventoryPositionId(), item.getNotifiedPackageCount(), relabelNo, null,
|
||||
"RELABEL:" + relabelNo, request.idempotencyKey().trim() + ":RESUBMIT-HOLD:" + holdIndex++,
|
||||
"换标驳回修改后重新冻结库存"));
|
||||
}
|
||||
}
|
||||
application.setLabelDeadline(request.labelDeadline());
|
||||
application.setContactName(request.contactName().trim());
|
||||
application.setContactPhone(request.contactPhone().trim());
|
||||
application.setRemark(trim(request.remark()));
|
||||
application.setStatus(PENDING_REVIEW);
|
||||
application.setFulfillmentStatus("WAIT_REVIEW");
|
||||
application.setUpdatedAt(now);
|
||||
applications.updateById(application);
|
||||
for (LastlegOrder order : tailOrders(application)) {
|
||||
order.setRelabelStatus(PENDING_REVIEW);
|
||||
order.setSubOrderStatus("RELABEL:" + PENDING_REVIEW);
|
||||
order.setUpdatedAt(now);
|
||||
orders.updateById(order);
|
||||
}
|
||||
insertHistory(customerId, application.getId(), "REJECTED", PENDING_REVIEW, operatorId,
|
||||
request.remark(), now);
|
||||
return detail(customerId, relabelNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void resetForRework(String customerId, String relabelNo, String operatorId, String remark) {
|
||||
RelabelApplication application = applications.lockNo(customerId, relabelNo);
|
||||
@@ -801,19 +879,22 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
if (linkedTransfer != null) {
|
||||
WarehouseTransferOrder transfer = linkedTransfer;
|
||||
if (transfer != null && "WAIT_RELABEL".equals(transfer.getStatus())) {
|
||||
Set<String> destinations = relabelItems.stream()
|
||||
.map(RelabelItem::getDestinationFbaWarehouseCode)
|
||||
.filter(StringUtils::hasText).map(String::trim).collect(Collectors.toSet());
|
||||
if (!StringUtils.hasText(transfer.getFbaWarehouseCode()) && destinations.size() == 1) {
|
||||
transfer.setFbaWarehouseCode(destinations.iterator().next());
|
||||
}
|
||||
String transferStatus = StringUtils.hasText(transfer.getFbaWarehouseCode())
|
||||
// Item-level relabel destinations apply only to those cartons.
|
||||
// They must never bind the whole transfer, because its remaining
|
||||
// cartons may still require a separate destination selection.
|
||||
boolean transferHasDestination = "FBA".equals(transfer.getDestinationType())
|
||||
? StringUtils.hasText(transfer.getDestinationWarehouseCode())
|
||||
: "PRIVATE".equals(transfer.getDestinationType())
|
||||
&& StringUtils.hasText(transfer.getPrivateDestinationAddress());
|
||||
String transferStatus = transferHasDestination
|
||||
? "READY_FOR_DISPATCH" : "WAIT_FBA_DESTINATION";
|
||||
transfer.setStatus(transferStatus);
|
||||
transfer.setRelabelCompletedAt(now);
|
||||
transfer.setUpdatedAt(now);
|
||||
warehouseTransfers.updateById(transfer);
|
||||
application.setFulfillmentStatus("READY_FOR_DISPATCH".equals(transferStatus)
|
||||
boolean relabelHasDestination = !relabelItems.isEmpty() && relabelItems.stream()
|
||||
.allMatch(item -> StringUtils.hasText(item.getDestinationFbaWarehouseCode()));
|
||||
application.setFulfillmentStatus("READY_FOR_DISPATCH".equals(transferStatus) || relabelHasDestination
|
||||
? "WAIT_PRE_DISPATCH" : transferStatus);
|
||||
applications.updateById(application);
|
||||
// 转运单自身的尾程订单与换标申请尾程订单是两条记录,需同步释放原转运单参与排车。
|
||||
@@ -945,6 +1026,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
value.getDeliveryMethod(), Boolean.TRUE.equals(value.getPrintShippingLabel()),
|
||||
value.getShippingLabelFileId(), value.getShippingLabelFileName(), value.getShippingLabelFileUrl(),
|
||||
value.getFulfillmentStatus(), value.getOutboundOrderNo(), value.getCustomerConfirmedAt(),
|
||||
order != null && "RELABEL_APPLICATION".equals(order.getOrderType()) ? "TAIL_ORDER" : "RELABEL_ORDER",
|
||||
null,
|
||||
value.getCreatedAt());
|
||||
}
|
||||
|
||||
@@ -996,8 +1079,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
.and(w -> w.eq(LastlegOrder::getSourceOrderNo, customerOrderNo).or().eq(LastlegOrder::getOrderNo, customerOrderNo))
|
||||
.last("LIMIT 1"));
|
||||
if (existingReturn != null) {
|
||||
existingReturn.setRelabelOrderNo(relabelNo); existingReturn.setRelabelStatus(PENDING_PROCESSING);
|
||||
existingReturn.setSubOrderStatus("RETURN_RELABEL:" + PENDING_PROCESSING); existingReturn.setPackageCount(packageCount);
|
||||
existingReturn.setRelabelOrderNo(relabelNo); existingReturn.setRelabelStatus(PENDING_REVIEW);
|
||||
existingReturn.setSubOrderStatus("RETURN_RELABEL:" + PENDING_REVIEW); existingReturn.setPackageCount(packageCount);
|
||||
existingReturn.setUpdatedAt(now); orders.updateById(existingReturn); return existingReturn;
|
||||
}
|
||||
LastlegOrder order = new LastlegOrder();
|
||||
@@ -1008,9 +1091,9 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
order.setSourceInboundId(relabelNo + ":" + warehouseId);
|
||||
order.setSourceOrderNo(request.customerOrderNo().trim()); order.setOverseasWarehouseId(warehouseId);
|
||||
order.setPalletCount(0); order.setPackageCount(packageCount); order.setPickedPackageCount(0);
|
||||
order.setShippedPackageCount(0); order.setCurrentStatus("WAIT_RELABEL");
|
||||
order.setSubOrderStatus("RELABEL:" + PENDING_PROCESSING); order.setRelabelOrderNo(relabelNo);
|
||||
order.setRelabelStatus(PENDING_PROCESSING); order.setCustomerRemark(trim(request.remark()));
|
||||
order.setShippedPackageCount(0); order.setCurrentStatus(PENDING_REVIEW);
|
||||
order.setSubOrderStatus("RELABEL:" + PENDING_REVIEW); order.setRelabelOrderNo(relabelNo);
|
||||
order.setRelabelStatus(PENDING_REVIEW); order.setCustomerRemark(trim(request.remark()));
|
||||
order.setSourceEventId("CUSTOMER_RELABEL:" + relabelNo + ":" + warehouseId);
|
||||
order.setSourceLoadingPlanId("RELABEL_APPLICATION"); order.setSourcePayloadHash("sha256:" + requestHash);
|
||||
order.setCreatedAt(now); order.setUpdatedAt(now); orders.insert(order);
|
||||
@@ -1023,8 +1106,12 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
if (order == null || !customerId.equals(order.getCustomerId())) {
|
||||
throw conflict("Existing tail order does not belong to this customer");
|
||||
}
|
||||
order.setRelabelOrderNo(relabelNo); order.setRelabelStatus(PENDING_PROCESSING);
|
||||
order.setPackageCount(packageCount); order.setUpdatedAt(now); orders.updateById(order);
|
||||
order.setRelabelOrderNo(relabelNo); order.setRelabelStatus(PENDING_REVIEW);
|
||||
// A linked warehouse-transfer relabel can cover only part of the
|
||||
// transfer. Keep the authoritative whole-order package count so later
|
||||
// dispatches can attribute the remaining cartons correctly.
|
||||
if (!"WAREHOUSE_TRANSFER".equals(order.getOrderType())) order.setPackageCount(packageCount);
|
||||
order.setUpdatedAt(now); orders.updateById(order);
|
||||
return order;
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -30,6 +30,7 @@ public final class WarehouseTransferResponses {
|
||||
String lastlegOrderNo, String operatingWarehouseId, String fbaWarehouseCode,
|
||||
String destinationType, String destinationWarehouseCode, String destinationWarehouseName,
|
||||
int packageCount, String deliveryMethod, boolean needRelabel, String relabelNo,
|
||||
String relabelApplicationStatus, String relabelFulfillmentStatus,
|
||||
String status, LocalDateTime estimatedArrivalAt, LocalDateTime createdAt) {}
|
||||
public record Detail(Long id, String transferNo, Long inboundOrderId, String inboundCode,
|
||||
Long lastlegOrderId, String lastlegOrderNo, String orderType, String orderStatus,
|
||||
|
||||
+5
-1
@@ -23,7 +23,11 @@ import org.apache.ibatis.annotations.Update;
|
||||
@Update("UPDATE ll_warehouse_transfer_order SET status='OUTBOUNDED',outbound_at=#{outboundAt},updated_at=#{outboundAt} WHERE customer_id=#{customerId} AND lastleg_order_id=#{orderId} AND outbound_at IS NULL")
|
||||
int markOutboundByOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
||||
@Param("outboundAt") java.time.LocalDateTime outboundAt);
|
||||
@Update("UPDATE ll_warehouse_transfer_order SET status='OUTBOUNDED',outbound_at=#{outboundAt},updated_at=#{outboundAt} WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo} AND outbound_at IS NULL")
|
||||
@Update("UPDATE ll_warehouse_transfer_order wto SET wto.status='OUTBOUNDED',wto.outbound_at=#{outboundAt},wto.updated_at=#{outboundAt} " +
|
||||
"WHERE wto.customer_id=#{customerId} AND wto.relabel_no=#{relabelNo} AND wto.outbound_at IS NULL " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_pallet wtp " +
|
||||
"JOIN ll_inventory_position ip ON ip.customer_id=wtp.customer_id AND ip.pallet_id=wtp.warehouse_pallet_id " +
|
||||
"WHERE wtp.customer_id=wto.customer_id AND wtp.transfer_order_id=wto.id AND ip.available_packages>0)")
|
||||
int markOutboundByRelabel(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo,
|
||||
@Param("outboundAt") java.time.LocalDateTime outboundAt);
|
||||
}
|
||||
|
||||
+33
-19
@@ -147,8 +147,10 @@ public class WarehouseTransferService {
|
||||
validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(),
|
||||
request.privateDestinationAddress(), request.privateDestinationPostalCode(),
|
||||
request.privateDestinationContactName(), request.privateDestinationContactPhone());
|
||||
validateTransferDestinationRelabel(request.destinationType(), request.fbaWarehouseCode(), request.destinationWarehouseCode(),
|
||||
request.needRelabel());
|
||||
// A split relabel transfer may intentionally defer the whole-order
|
||||
// destination: relabeled cartons go through relabel dispatch while the
|
||||
// remaining cartons are bound later from warehouse-transfer outbound.
|
||||
// All other transfer types must still select FBA or PRIVATE up front.
|
||||
String key = request.idempotencyKey().trim();
|
||||
String requestHash = hash(request);
|
||||
WarehouseTransferOrder replay = transfers.findKey(customerId, key);
|
||||
@@ -279,8 +281,6 @@ public class WarehouseTransferService {
|
||||
validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(),
|
||||
request.privateDestinationAddress(), request.privateDestinationPostalCode(),
|
||||
request.privateDestinationContactName(), request.privateDestinationContactPhone());
|
||||
validateTransferDestinationRelabel(request.destinationType(), request.fbaWarehouseCode(), request.destinationWarehouseCode(),
|
||||
request.needRelabel());
|
||||
|
||||
LastlegOrder order = requireTailOrder(customerId, transfer);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -422,12 +422,16 @@ public class WarehouseTransferService {
|
||||
.le(query.createdTo() != null, WarehouseTransferOrder::getCreatedAt, query.createdTo())
|
||||
.orderByDesc(WarehouseTransferOrder::getCreatedAt).orderByDesc(WarehouseTransferOrder::getId);
|
||||
Page<WarehouseTransferOrder> result = transfers.selectPage(Page.of(query.page(), query.size()), wrapper);
|
||||
Map<String, RelabelApplication> relabelApplicationsByNo = relabelApplications(result.getRecords());
|
||||
List<WarehouseTransferResponses.Summary> records = result.getRecords().stream().map(value ->
|
||||
new WarehouseTransferResponses.Summary(value.getId(), value.getTransferNo(), value.getInboundCode(),
|
||||
value.getLastlegOrderId(), value.getLastlegOrderNo(), value.getOperatingWarehouseId(),
|
||||
value.getFbaWarehouseCode(), value.getDestinationType(), value.getDestinationWarehouseCode(),
|
||||
value.getDestinationWarehouseName(), value.getPackageCount(), value.getDeliveryMethod(),
|
||||
Boolean.TRUE.equals(value.getNeedRelabel()), value.getRelabelNo(), value.getStatus(),
|
||||
Boolean.TRUE.equals(value.getNeedRelabel()), value.getRelabelNo(),
|
||||
relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getStatus(),
|
||||
relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getFulfillmentStatus(),
|
||||
value.getStatus(),
|
||||
value.getEstimatedArrivalAt(), value.getCreatedAt())).toList();
|
||||
return new PageResult<>(records, result.getCurrent(), result.getSize(), result.getTotal(), result.getPages());
|
||||
}
|
||||
@@ -911,7 +915,7 @@ public class WarehouseTransferService {
|
||||
transfers.updateById(transfer);
|
||||
LastlegOrder order = requireTailOrder(customerId, transfer); String before = order.getCurrentStatus();
|
||||
order.setCurrentStatus(INBOUND_COMPLETED);
|
||||
order.setRelabelStatus(Boolean.TRUE.equals(transfer.getNeedRelabel()) ? "PENDING_PROCESSING" : "NOT_REQUIRED");
|
||||
order.setRelabelStatus(Boolean.TRUE.equals(transfer.getNeedRelabel()) ? "PENDING_REVIEW" : "NOT_REQUIRED");
|
||||
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + transfer.getStatus() + ";RELABEL:" + order.getRelabelStatus());
|
||||
order.setUpdatedAt(now); orders.updateById(order);
|
||||
if (!java.util.Objects.equals(before, order.getCurrentStatus())) {
|
||||
@@ -1214,18 +1218,34 @@ public class WarehouseTransferService {
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<WarehouseTransferResponses.Summary> pdaCandidates(long overseasWarehouseId) {
|
||||
return transfers.selectList(new LambdaQueryWrapper<WarehouseTransferOrder>()
|
||||
List<WarehouseTransferOrder> candidates = transfers.selectList(new LambdaQueryWrapper<WarehouseTransferOrder>()
|
||||
.eq(WarehouseTransferOrder::getOperatingWarehouseId, String.valueOf(overseasWarehouseId))
|
||||
.in(WarehouseTransferOrder::getStatus, List.of(WAIT_PDA_RECEIVING, APPROVED, ALLOCATED, WAIT_RELABEL, READY_FOR_DISPATCH))
|
||||
.orderByDesc(WarehouseTransferOrder::getUpdatedAt)).stream().map(value ->
|
||||
.orderByDesc(WarehouseTransferOrder::getUpdatedAt));
|
||||
Map<String, RelabelApplication> relabelApplicationsByNo = relabelApplications(candidates);
|
||||
return candidates.stream().map(value ->
|
||||
new WarehouseTransferResponses.Summary(value.getId(), value.getTransferNo(), value.getInboundCode(),
|
||||
value.getLastlegOrderId(), value.getLastlegOrderNo(), value.getOperatingWarehouseId(),
|
||||
value.getFbaWarehouseCode(), value.getDestinationType(), value.getDestinationWarehouseCode(),
|
||||
value.getDestinationWarehouseName(), value.getPackageCount(), value.getDeliveryMethod(),
|
||||
Boolean.TRUE.equals(value.getNeedRelabel()), value.getRelabelNo(), value.getStatus(),
|
||||
Boolean.TRUE.equals(value.getNeedRelabel()), value.getRelabelNo(),
|
||||
relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getStatus(),
|
||||
relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getFulfillmentStatus(),
|
||||
value.getStatus(),
|
||||
value.getEstimatedArrivalAt(), value.getCreatedAt())).toList();
|
||||
}
|
||||
|
||||
private Map<String, RelabelApplication> relabelApplications(List<WarehouseTransferOrder> transferOrders) {
|
||||
Set<String> relabelNos = transferOrders.stream().map(WarehouseTransferOrder::getRelabelNo)
|
||||
.filter(StringUtils::hasText).collect(Collectors.toSet());
|
||||
if (relabelNos.isEmpty()) return Map.of();
|
||||
List<RelabelApplication> applications = relabelApplications.selectList(
|
||||
new LambdaQueryWrapper<RelabelApplication>().in(RelabelApplication::getRelabelNo, relabelNos));
|
||||
if (applications == null || applications.isEmpty()) return Map.of();
|
||||
return applications.stream().collect(Collectors.toMap(RelabelApplication::getRelabelNo,
|
||||
Function.identity(), (left, right) -> right));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public WarehouseTransferResponses.Detail pdaDetail(long overseasWarehouseId, String transferNo) {
|
||||
WarehouseTransferOrder transfer = transfers.findNoByWarehouse(transferNo, String.valueOf(overseasWarehouseId));
|
||||
@@ -1391,19 +1411,13 @@ public class WarehouseTransferService {
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateTransferDestinationRelabel(String destinationType, String fbaWarehouseCode,
|
||||
String destinationWarehouseCode, boolean needRelabel) {
|
||||
private static void validateTransferDestinationRequired(String destinationType, String fbaWarehouseCode,
|
||||
boolean allowDeferredForRelabel) {
|
||||
String type = trim(destinationType);
|
||||
if (type == null && StringUtils.hasText(fbaWarehouseCode)) type = "FBA";
|
||||
if (type == null || "NONE".equalsIgnoreCase(type)) {
|
||||
if ((type == null || "NONE".equalsIgnoreCase(type)) && !allowDeferredForRelabel) {
|
||||
throw badRequest("仓储转运必须选择 FBA 仓或私人仓");
|
||||
}
|
||||
if ("FBA".equalsIgnoreCase(type) && !needRelabel) {
|
||||
throw badRequest("转运到 FBA 仓必须同时创建贴标单");
|
||||
}
|
||||
if ("PRIVATE".equalsIgnoreCase(type) && needRelabel) {
|
||||
throw badRequest("不需要贴标时请选择私人仓作为目的地");
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyDestination(WarehouseTransferOrder transfer, WarehouseTransferRequests.Create request) {
|
||||
@@ -1598,7 +1612,7 @@ public class WarehouseTransferService {
|
||||
transfer.getOperatingWarehouseId(), transfer.getLabelDeadline(), transfer.getRelabelContactName(),
|
||||
transfer.getRelabelContactPhone(), transfer.getRelabelChargeAmount(), "仓储转运确认后自动创建换标单",
|
||||
"WAREHOUSE_TRANSFER:" + transfer.getTransferNo() + ":RELABEL", items,
|
||||
trim(transfer.getRelabelDeliveryMethod()), false);
|
||||
firstText(transfer.getRelabelDeliveryMethod(), "TRUCK"), false);
|
||||
return relabelService.createForExistingTailOrder(customerId, operatorId,
|
||||
transfer.getLastlegOrderId(), request);
|
||||
}
|
||||
|
||||
@@ -240,6 +240,16 @@ paths:
|
||||
responses:
|
||||
'200': { description: 统一 ApiResponse 包装的尾程订单详情;order.relabelApplications 返回所有换标子单的独立状态和本次数量 }
|
||||
'404': { description: 尾程订单不存在或不属于查询客户 }
|
||||
/api/platform/v1/last-leg/orders/{id}/relabel-review:
|
||||
put:
|
||||
summary: 在尾程订单工作台审核客户直接创建的贴标单
|
||||
description: 权限 platform:overseas-warehouse:last-mile-order:edit;仅 RELABEL_APPLICATION + PENDING_REVIEW 可审核。通过进入 WAIT_RELABEL,驳回必须填写原因并保持 REJECTED 供客户修改后重提。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: integer, format: int64 } }
|
||||
- { name: customerId, in: query, required: false, schema: { type: string, pattern: '^[1-9][0-9]*$' } }
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelStatusChange' } } } }
|
||||
responses: { '200': { description: 审核后的贴标单详情 }, '400': { description: 驳回原因缺失或决定非法 }, '409': { description: 非直接贴标单、审核入口错误或状态冲突 } }
|
||||
/api/platform/v1/last-leg/orders/{id}/fees:
|
||||
post:
|
||||
summary: 新增尾程订单费用
|
||||
@@ -1001,7 +1011,7 @@ paths:
|
||||
/api/merchant/v1/last-leg/warehouse-transfers:
|
||||
get:
|
||||
summary: 客户分页查询自己的仓储转运单
|
||||
description: 权限 merchant:overseas-warehouse:transfer:list;列表权限包含详情只读。
|
||||
description: 权限 merchant:overseas-warehouse:transfer:list;列表权限包含详情只读;每行同时返回关联换标单号 relabelNo、换标申请流程状态 relabelStatus 与履约状态 relabelFulfillmentStatus。
|
||||
security: [{ bearerAuth: [] }]
|
||||
responses: { '200': { description: 当前客户仓储转运单分页 }, '403': { description: 缺少仓储转运查看权限 } }
|
||||
post:
|
||||
@@ -1355,7 +1365,7 @@ paths:
|
||||
/api/platform/v1/last-leg/relabel-applications/{relabelNo}/status:
|
||||
put:
|
||||
summary: 变更海外换标单状态
|
||||
description: 权限 platform:overseas-warehouse:relabel-order:status;PDA复核通过后进入CUSTOMER_CONFIRM_PENDING,客户确认后进入READY_FOR_OUTBOUND,再由排车或直接出库履约。
|
||||
description: 权限 platform:overseas-warehouse:relabel-order:status;待审核阶段只处理转运、退货等关联业务产生的换标单,客户直接创建的贴标单必须在尾程订单工作台审核。PDA复核通过后进入CUSTOMER_CONFIRM_PENDING,客户确认后进入READY_FOR_OUTBOUND。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: relabelNo, in: path, required: true, schema: { type: string, maxLength: 64 } }
|
||||
@@ -1373,7 +1383,7 @@ paths:
|
||||
/api/platform/v1/last-leg/relabel-applications/{relabelNo}/files:
|
||||
post:
|
||||
summary: 上传总换标PDF或明细换标PDF
|
||||
description: 权限 platform:overseas-warehouse:relabel-order:create。仅PENDING_PROCESSING/PROCESSING可替换,PDF内容且不超过10MiB。客户从申请单解析,ITEM必须属于该申请单同客户。File类别分别为OVERSEAS_RELABEL_TOTAL_DOCUMENT(application.id)及OVERSEAS_RELABEL_ITEM_DOCUMENT(item.id)。成功保存File返回的资料元数据,旧资料不再允许通过当前绑定下载。
|
||||
description: 权限 platform:overseas-warehouse:relabel-order:create。PENDING_REVIEW/PENDING_PROCESSING/PROCESSING/REJECTED 可替换,PDF内容且不超过10MiB。客户从申请单解析,ITEM必须属于该申请单同客户。成功保存File返回的资料元数据,旧资料不再允许通过当前绑定下载。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: relabelNo, in: path, required: true, schema: { type: string } }
|
||||
@@ -1458,10 +1468,18 @@ paths:
|
||||
responses:
|
||||
'200': { description: 换标申请详情, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelMerchantDetailResponse' } } } }
|
||||
'404': { description: 申请不存在或不属于当前客户 }
|
||||
/api/merchant/v1/last-leg/relabel-applications/{relabelNo}/resubmit:
|
||||
put:
|
||||
summary: 修改并重新提交已驳回贴标单
|
||||
description: 权限 merchant:overseas-warehouse:relabel:create;客户可先替换贴标资料,再修改截止时间、联系人、电话和备注并重新提交审核。重新冻结原库存和费用,直接贴标回到尾程订单审核,关联换标回到换标单审核。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string, maxLength: 64 } }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelResubmit' } } } }
|
||||
responses: { '200': { description: 重新提交后的贴标单详情 }, '409': { description: 非驳回状态、库存已变化或费用尚未释放 } }
|
||||
/api/merchant/v1/last-leg/relabel-applications/{relabelNo}/shipping-label:
|
||||
post:
|
||||
summary: 上传换标快递面单
|
||||
description: 权限 merchant:overseas-warehouse:relabel:files;仅 EXPRESS 且申请尚未客户确认时允许上传,文件由服务端绑定到申请单。
|
||||
description: 权限 merchant:overseas-warehouse:relabel:files;仅 EXPRESS 且申请尚未客户确认时允许上传;审核驳回后可替换文件再重新提交,文件由服务端绑定到申请单。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string, maxLength: 64 } }]
|
||||
requestBody: { required: true, content: { multipart/form-data: { schema: { type: object, required: [file], properties: { file: { type: string, format: binary } } } } } }
|
||||
@@ -2589,7 +2607,7 @@ components:
|
||||
fbaShipmentId: { type: string, nullable: true, description: 来源冻结货件编号;多值以逗号分隔 }
|
||||
poReferenceNo: { type: string, nullable: true, description: 来源冻结FBA跟踪/参考号;多值以逗号分隔 }
|
||||
packageCount: { type: integer, minimum: 0 }
|
||||
RelabelStatus: { type: string, enum: [PENDING_PROCESSING, PROCESSING, CUSTOMER_CONFIRM_PENDING, READY_FOR_OUTBOUND, COMPLETED, REJECTED, CANCELLED] }
|
||||
RelabelStatus: { type: string, enum: [PENDING_REVIEW, PENDING_PROCESSING, PROCESSING, CUSTOMER_CONFIRM_PENDING, READY_FOR_OUTBOUND, COMPLETED, REJECTED, CANCELLED] }
|
||||
RelabelMerchantPageResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/MerchantApiResponse' }
|
||||
@@ -2626,6 +2644,8 @@ components:
|
||||
fulfillmentStatus: { type: string, nullable: true, enum: [WAIT_RELABEL, WAIT_PRE_DISPATCH, READY_FOR_OUTBOUND, OUTBOUND_CREATED, COMPLETED] }
|
||||
outboundOrderNo: { type: string, nullable: true }
|
||||
customerConfirmedAt: { type: string, format: date-time, nullable: true }
|
||||
reviewChannel: { type: string, enum: [TAIL_ORDER, RELABEL_ORDER], description: TAIL_ORDER 表示客户直接贴标,RELABEL_ORDER 表示其他业务关联换标 }
|
||||
reviewRemark: { type: string, nullable: true, description: 当前驳回状态最近一次审核驳回原因 }
|
||||
createdAt: { type: string, format: date-time }
|
||||
RelabelDetail:
|
||||
type: object
|
||||
@@ -2764,6 +2784,15 @@ components:
|
||||
properties:
|
||||
status: { $ref: '#/components/schemas/RelabelStatus' }
|
||||
remark: { type: string, nullable: true, maxLength: 1000 }
|
||||
RelabelResubmit:
|
||||
type: object
|
||||
required: [labelDeadline, contactName, contactPhone, idempotencyKey]
|
||||
properties:
|
||||
labelDeadline: { type: string, format: date-time }
|
||||
contactName: { type: string, minLength: 1, maxLength: 128 }
|
||||
contactPhone: { type: string, minLength: 1, maxLength: 64 }
|
||||
remark: { type: string, nullable: true, maxLength: 1000 }
|
||||
idempotencyKey: { type: string, minLength: 1, maxLength: 255 }
|
||||
WalletFreezeRetry:
|
||||
type: object
|
||||
required: [idempotencyKey]
|
||||
|
||||
+28
-1
@@ -113,6 +113,31 @@ class PdaRelabelServiceTest {
|
||||
verify(tasks, never()).insert(any(PdaRelabelTask.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void claimReworkTaskOneAtATimeAndAssignsItToCurrentOperator() {
|
||||
application.setStatus("PROCESSING");
|
||||
PdaRelabelTask rework = task("AVAILABLE", 3);
|
||||
rework.setOperatorKey(null);
|
||||
rework.setOperatorName(null);
|
||||
rework.setReviewDecision("REWORK");
|
||||
rework.setReviewRemark("label is crooked");
|
||||
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
|
||||
when(tasks.lock("100", 1L)).thenReturn(rework);
|
||||
|
||||
var result = service.claim("7", "picker", 900L, "FBR-1",
|
||||
new PdaRelabelRequests.Claim("claim-rework"));
|
||||
|
||||
assertEquals("PROCESSING", result.taskStatus());
|
||||
assertEquals("REWORK", result.reviewDecision());
|
||||
assertEquals("label is crooked", result.reviewRemark());
|
||||
assertEquals("7", rework.getOperatorKey());
|
||||
assertEquals("picker", rework.getOperatorName());
|
||||
assertEquals(4, rework.getVersion());
|
||||
verify(tasks).lockProcessingByOperator("7");
|
||||
verify(tasks).updateWithVersion(rework, 3);
|
||||
verify(tasks, never()).insert(any(PdaRelabelTask.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void workPhotoDownloadValidatesTaskOwnershipAndFileScope() {
|
||||
application.setStatus("PROCESSING");
|
||||
@@ -369,10 +394,12 @@ class PdaRelabelServiceTest {
|
||||
var result = service.returnForRework("8", "reviewer", "FBR-1",
|
||||
new PdaRelabelRequests.Review("REWORK", 2, "review-rework", "wrong label"));
|
||||
|
||||
assertEquals("PROCESSING", result.taskStatus());
|
||||
assertEquals("AVAILABLE", result.taskStatus());
|
||||
assertEquals("PROCESSING", result.applicationStatus());
|
||||
assertEquals(0, result.relabelledPackages());
|
||||
assertEquals("wrong label", result.reviewRemark());
|
||||
assertEquals("7", task.getOperatorKey());
|
||||
assertEquals("picker", task.getOperatorName());
|
||||
assertTrue(result.completionPhotoFileIds().isEmpty());
|
||||
verify(applicationService).resetForRework("100", "FBR-1", "8", "wrong label");
|
||||
verify(applicationService, never()).changeStatus(anyString(), anyString(), anyString(), any());
|
||||
|
||||
+86
-7
@@ -126,7 +126,7 @@ class RelabelApplicationServiceTest {
|
||||
void createBuildsPendingRelabelTailOrderItemsAndBothHistories() {
|
||||
RelabelRequests.Create request = request("key-1", 10, 6);
|
||||
RelabelApplication persisted = application(11L, "FBR-1", "10001", "key-1", null,
|
||||
RelabelApplicationService.PENDING_PROCESSING);
|
||||
RelabelApplicationService.PENDING_REVIEW);
|
||||
persisted.setLastlegOrderId(21L);
|
||||
persisted.setItemCount(1); persisted.setTotalPackageCount(10); persisted.setNotifiedPackageCount(6);
|
||||
when(applications.findNo(eq("10001"), any())).thenReturn(persisted);
|
||||
@@ -149,8 +149,8 @@ class RelabelApplicationServiceTest {
|
||||
verify(orderHistories).insert(tailHistory.capture());
|
||||
|
||||
assertEquals("RELABEL_APPLICATION", tailOrder.getValue().getOrderType());
|
||||
assertEquals("WAIT_RELABEL", tailOrder.getValue().getCurrentStatus());
|
||||
assertEquals("RELABEL:PENDING_PROCESSING", tailOrder.getValue().getSubOrderStatus());
|
||||
assertEquals("PENDING_REVIEW", tailOrder.getValue().getCurrentStatus());
|
||||
assertEquals("RELABEL:PENDING_REVIEW", tailOrder.getValue().getSubOrderStatus());
|
||||
assertEquals("CUSTOMER", tailOrder.getValue().getSourceRegion());
|
||||
assertTrue(tailOrder.getValue().getOrderNo().matches("RL-WH-\\d{8}-\\d{6}"));
|
||||
assertNull(tailOrder.getValue().getInboundCode());
|
||||
@@ -159,8 +159,8 @@ class RelabelApplicationServiceTest {
|
||||
assertEquals(10, tailOrder.getValue().getPackageCount());
|
||||
assertEquals(6, application.getValue().getNotifiedPackageCount());
|
||||
assertEquals("SKU-A", relabelItem.getValue().getSkuCode());
|
||||
assertEquals("PENDING_PROCESSING", relabelHistory.getValue().getToStatus());
|
||||
assertEquals("WAIT_RELABEL", tailHistory.getValue().getToStatus());
|
||||
assertEquals("PENDING_REVIEW", relabelHistory.getValue().getToStatus());
|
||||
assertEquals("PENDING_REVIEW", tailHistory.getValue().getToStatus());
|
||||
assertEquals("FBR-1", result.application().relabelNo());
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ class RelabelApplicationServiceTest {
|
||||
void warehouseTransferRelabelReusesExistingTailOrder() {
|
||||
RelabelRequests.Create request = request("key-transfer", 10, 6);
|
||||
RelabelApplication persisted = application(11L, "FBR-TRANSFER", "10001", "key-transfer", null,
|
||||
RelabelApplicationService.PENDING_PROCESSING);
|
||||
RelabelApplicationService.PENDING_REVIEW);
|
||||
persisted.setLastlegOrderId(21L);
|
||||
LastlegOrder transferOrder = order(21L, "10001", "WT-WH-20260916-560449", "INBOUND_COMPLETED");
|
||||
transferOrder.setOrderType("WAREHOUSE_TRANSFER");
|
||||
@@ -185,9 +185,38 @@ class RelabelApplicationServiceTest {
|
||||
verify(orderHistories, never()).insert(any(LastlegOrderStatusHistory.class));
|
||||
assertEquals(21L, application.getValue().getLastlegOrderId());
|
||||
assertEquals(application.getValue().getRelabelNo(), transferOrder.getRelabelOrderNo());
|
||||
assertEquals(RelabelApplicationService.PENDING_PROCESSING, transferOrder.getRelabelStatus());
|
||||
assertEquals(RelabelApplicationService.PENDING_REVIEW, transferOrder.getRelabelStatus());
|
||||
assertEquals("WAREHOUSE_TRANSFER", transferOrder.getOrderType());
|
||||
assertEquals("INBOUND_COMPLETED", transferOrder.getCurrentStatus());
|
||||
assertEquals("WAIT_RELABEL", application.getValue().getFulfillmentStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void warehouseTransferSkuOnlyRelabelStillRequiresOutboundFulfillment() {
|
||||
RelabelRequests.Item skuOnly = new RelabelRequests.Item(101L, 201L, 301L,
|
||||
"SKU-A", null, null, null, "Chinese name", 1, 1,
|
||||
"WT-WH-1", null, "SKU-NEW", "SKU relabel", null,
|
||||
null, null, null, "CARTON-1", null, "XLX1", "SKU_LABEL",
|
||||
List.of(new RelabelRequests.SkuItem("SKU-A", "SKU-NEW", 1, null)));
|
||||
RelabelRequests.Create request = new RelabelRequests.Create("WT-WH-1", "WH-1",
|
||||
LocalDateTime.of(2026, 9, 25, 18, 0), "Li", "13800000000", null,
|
||||
"warehouse transfer", "key-transfer-sku", List.of(skuOnly), "TRUCK", false);
|
||||
RelabelApplication persisted = application(11L, "FBR-TRANSFER-SKU", "10001",
|
||||
"key-transfer-sku", null, RelabelApplicationService.PENDING_REVIEW);
|
||||
persisted.setLastlegOrderId(21L);
|
||||
LastlegOrder transferOrder = order(21L, "10001", "WT-WH-1", "INBOUND_COMPLETED");
|
||||
transferOrder.setOrderType("WAREHOUSE_TRANSFER");
|
||||
when(applications.findNo(eq("10001"), any())).thenReturn(persisted);
|
||||
when(items.selectList(any())).thenReturn(List.of(item(31L, 11L, "SKU-A", 1, 1)));
|
||||
when(histories.selectList(any())).thenReturn(List.of());
|
||||
when(orders.selectById(21L)).thenReturn(transferOrder);
|
||||
|
||||
service.createForExistingTailOrder("10001", "operator-1", 21L, request);
|
||||
|
||||
ArgumentCaptor<RelabelApplication> application = ArgumentCaptor.forClass(RelabelApplication.class);
|
||||
verify(applications).insert(application.capture());
|
||||
assertEquals("TRUCK", application.getValue().getDeliveryMethod());
|
||||
assertEquals("WAIT_RELABEL", application.getValue().getFulfillmentStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -465,6 +494,56 @@ class RelabelApplicationServiceTest {
|
||||
verify(walletCommands, never()).publish(any(), any(Long.class), any(), any(), any(), any(), any(Long.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void directLabelingOrderCanOnlyBeApprovedFromTailOrderReview() {
|
||||
RelabelApplication application = application(11L, "FBR-DIRECT", "10001", "key-direct", "hash",
|
||||
RelabelApplicationService.PENDING_REVIEW);
|
||||
application.setLastlegOrderId(21L);
|
||||
LastlegOrder order = order(21L, "10001", "LMO-DIRECT", RelabelApplicationService.PENDING_REVIEW);
|
||||
order.setOrderType("RELABEL_APPLICATION"); order.setRelabelOrderNo("FBR-DIRECT");
|
||||
stubDetail(application);
|
||||
when(applications.lockNo("10001", "FBR-DIRECT")).thenReturn(application);
|
||||
when(orders.selectById(21L)).thenReturn(order);
|
||||
|
||||
ResponseStatusException wrongEntry = assertThrows(ResponseStatusException.class, () ->
|
||||
service.changeStatus("10001", "FBR-DIRECT", "operator-2",
|
||||
new RelabelRequests.ChangeStatus("PENDING_PROCESSING", null)));
|
||||
assertEquals(409, wrongEntry.getStatusCode().value());
|
||||
|
||||
service.reviewDirectTailOrder("10001", 21L, "operator-2",
|
||||
new RelabelRequests.ChangeStatus("PENDING_PROCESSING", null));
|
||||
|
||||
assertEquals(RelabelApplicationService.PENDING_PROCESSING, application.getStatus());
|
||||
assertEquals("WAIT_RELABEL", order.getCurrentStatus());
|
||||
assertEquals("RELABEL:PENDING_PROCESSING", order.getSubOrderStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void directLabelingRejectionRequiresAndKeepsReviewRemark() {
|
||||
RelabelApplication application = application(11L, "FBR-DIRECT", "10001", "key-direct", "hash",
|
||||
RelabelApplicationService.PENDING_REVIEW);
|
||||
application.setLastlegOrderId(21L);
|
||||
LastlegOrder order = order(21L, "10001", "LMO-DIRECT", RelabelApplicationService.PENDING_REVIEW);
|
||||
order.setOrderType("RELABEL_APPLICATION"); order.setRelabelOrderNo("FBR-DIRECT");
|
||||
stubDetail(application);
|
||||
when(applications.lockNo("10001", "FBR-DIRECT")).thenReturn(application);
|
||||
when(orders.selectById(21L)).thenReturn(order);
|
||||
|
||||
ResponseStatusException missingReason = assertThrows(ResponseStatusException.class, () ->
|
||||
service.reviewDirectTailOrder("10001", 21L, "operator-2",
|
||||
new RelabelRequests.ChangeStatus("REJECTED", " ")));
|
||||
assertEquals(400, missingReason.getStatusCode().value());
|
||||
|
||||
service.reviewDirectTailOrder("10001", 21L, "operator-2",
|
||||
new RelabelRequests.ChangeStatus("REJECTED", "请替换模糊的箱唛文件"));
|
||||
|
||||
assertEquals("REJECTED", application.getStatus());
|
||||
assertEquals("REJECTED", order.getCurrentStatus());
|
||||
ArgumentCaptor<RelabelStatusHistory> history = ArgumentCaptor.forClass(RelabelStatusHistory.class);
|
||||
verify(histories).insert(history.capture());
|
||||
assertEquals("请替换模糊的箱唛文件", history.getValue().getRemark());
|
||||
}
|
||||
|
||||
@Test
|
||||
void completedReturnRelabelKeepsParentInboundCompleted() {
|
||||
RelabelApplication application = application(11L, "FBR-1", "10001", "key-1", "hash", RelabelApplicationService.READY_FOR_OUTBOUND);
|
||||
|
||||
+28
-5
@@ -44,6 +44,7 @@ import com.baiyue.lastleg.transfer.gateway.TransferMeasurementPhotoFileGateway;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferReceiptMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferSourceOrderMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -90,6 +91,28 @@ class WarehouseTransferServiceTest {
|
||||
assertNull(WarehouseTransferService.customerMainStatuses(WarehouseTransferService.DRAFT));
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferPageReturnsRelabelApplicationAndFulfillmentStatuses() {
|
||||
WarehouseTransferOrder transfer = transfer(WarehouseTransferService.WAIT_RELABEL);
|
||||
transfer.setNeedRelabel(true);
|
||||
transfer.setRelabelNo("RL-20260923-000001");
|
||||
Page<WarehouseTransferOrder> resultPage = new Page<>(1, 20, 1);
|
||||
resultPage.setRecords(List.of(transfer));
|
||||
RelabelApplication relabel = new RelabelApplication();
|
||||
relabel.setRelabelNo("RL-20260923-000001");
|
||||
relabel.setStatus("COMPLETED");
|
||||
relabel.setFulfillmentStatus("READY_FOR_DISPATCH");
|
||||
when(transfers.selectPage(any(), any())).thenReturn(resultPage);
|
||||
when(relabelApplications.selectList(any())).thenReturn(List.of(relabel));
|
||||
|
||||
var result = service.page("TENANT-A", new WarehouseTransferRequests.Query(
|
||||
1, 20, null, null, null, null, null, null, null, null));
|
||||
|
||||
assertEquals("RL-20260923-000001", result.records().get(0).relabelNo());
|
||||
assertEquals("COMPLETED", result.records().get(0).relabelStatus());
|
||||
assertEquals("READY_FOR_DISPATCH", result.records().get(0).relabelFulfillmentStatus());
|
||||
}
|
||||
|
||||
@Mock WarehouseTransferOrderMapper transfers;
|
||||
@Mock WarehouseTransferItemMapper transferItems;
|
||||
@Mock WarehouseTransferCartonMapper transferCartons;
|
||||
@@ -328,9 +351,9 @@ class WarehouseTransferServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferCanEnterTheTransitWarehouseBeforeFbaDestinationIsKnown() {
|
||||
void transferCanEnterTheTransitWarehouseWithFbaDestinationAndNoRelabel() {
|
||||
WarehouseTransferRequests.Create request = new WarehouseTransferRequests.Create(
|
||||
"IN-1", List.of(11L), "WH-1", null, "123 Pickup Road", 5,
|
||||
"IN-1", List.of(11L), "WH-1", "ONT8", "123 Pickup Road", 5,
|
||||
new BigDecimal("0.2500"), "WAREHOUSE_PICKUP", LocalDateTime.of(2026, 9, 12, 10, 0),
|
||||
null, null, null, "FBA destination will be supplied later", false, "key-no-fba",
|
||||
List.of(new WarehouseTransferRequests.Item("SKU-1", "Product", 5, "MARK-1")));
|
||||
@@ -346,9 +369,9 @@ class WarehouseTransferServiceTest {
|
||||
ArgumentCaptor<LastlegOrder> tailOrder = ArgumentCaptor.forClass(LastlegOrder.class);
|
||||
verify(transfers).insert(transfer.capture());
|
||||
verify(orders).insert(tailOrder.capture());
|
||||
assertNull(transfer.getValue().getFbaWarehouseCode());
|
||||
assertEquals("ONT8", transfer.getValue().getFbaWarehouseCode());
|
||||
assertNull(transfer.getValue().getFbaShipmentNo());
|
||||
assertNull(tailOrder.getValue().getFbaWarehouseCode());
|
||||
assertEquals("ONT8", tailOrder.getValue().getFbaWarehouseCode());
|
||||
assertEquals("WAREHOUSE_PICKUP", transfer.getValue().getDeliveryMethod());
|
||||
}
|
||||
|
||||
@@ -391,7 +414,7 @@ class WarehouseTransferServiceTest {
|
||||
@Test
|
||||
void newTransitInboundCreatesSnapshotAndImmutableCartonMarksWithoutHistoricalInventory() {
|
||||
WarehouseTransferRequests.Create request = new WarehouseTransferRequests.Create(
|
||||
null, null, "WH-1", null, "123 Pickup Road", 5,
|
||||
null, null, "WH-1", "ONT8", "123 Pickup Road", 5,
|
||||
new BigDecimal("0.2500"), "WAREHOUSE_PICKUP", LocalDateTime.of(2026, 9, 12, 10, 0),
|
||||
null, "TRACK-MUST-NOT-BE-SAVED", null, "Incoming transit cargo", true, "key-incoming",
|
||||
List.of(new WarehouseTransferRequests.Item("SKU-1", "Product A", 2, null),
|
||||
|
||||
Reference in New Issue
Block a user