This commit is contained in:
2026-08-11 18:06:28 +08:00
26 changed files with 321 additions and 51 deletions
@@ -43,7 +43,7 @@ public class PlatformContainerController {
}
@PutMapping("/{id}") @PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
@BusinessAudit(event="CONTAINER_UPDATE", resourceType="CONTAINER", resourceId="#id", description="完整编辑货柜基础资料")
@Operation(summary="完整编辑货柜", description="仅无装柜计划或计划为EDITING时允许")
@Operation(summary="完整编辑货柜", description="仅待装柜或预装柜中时允许")
public ApiResponse<ContainerBookingEntity> updateDraft(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long id,
@Valid @RequestBody FullUpdateRequest request) {
@@ -53,7 +53,7 @@ public class PlatformContainerController {
@PatchMapping("/{id}/transport") @PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
@BusinessAudit(event="CONTAINER_TRANSPORT_UPDATE", resourceType="CONTAINER", resourceId="#id",
description="修改货柜集装箱号和航次")
@Operation(summary="修改集装箱号和航次", description="LOADING仅开放此白名单;COMPLETED全部只读")
@Operation(summary="修改集装箱号和航次", description="PDA拣货中仅开放此白名单;装柜完成后全部只读")
public ApiResponse<ContainerBookingEntity> updateTransport(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long id,
@Valid @RequestBody TransportUpdateRequest request) {
@@ -2,9 +2,11 @@ package com.baiyue.firstleg.domain;
/** 智能装柜状态,只允许沿此方向推进。 */
public enum SmartLoadingStatus {
/** 编辑中:可以维护货柜信息并增删未拣货明细。 */
/** 待装柜:货柜已创建但尚未创建装柜计划,仅作为货柜聚合查询的派生状态,不写入装柜计划表。 */
WAIT_LOADING,
/** 预装柜中:可以维护货柜信息并增删未拣货明细。 */
EDITING,
/** 装柜中:已生成 PDA 拣货任务,仍可追加或调整未拣货明细。 */
/** 拣货中:已生成拣货任务,仍可追加或调整未拣货明细。 */
LOADING,
/** 装柜完成:PDA 拣货完成后自动完成,计划、明细和货柜信息只读。 */
COMPLETED;
@@ -12,7 +14,7 @@ public enum SmartLoadingStatus {
/** 装柜完成前可维护集装箱号和航次。 */
public boolean allowsTransportUpdate() { return this != COMPLETED; }
/** 编辑中和装柜中均允许维护尚未拣货的明细。 */
/** 待装柜、预装柜中和拣货中均允许维护尚未拣货的明细。 */
public boolean mutable() { return this != COMPLETED; }
public boolean canAdvanceTo(SmartLoadingStatus next) {
@@ -73,14 +73,14 @@ public final class ContainerDtos {
LocalDateTime actualWarehouseArrivalAt
) {}
@Schema(name = "ContainerFullUpdateRequest", description = "智能装柜 EDITING 阶段完整编辑")
@Schema(name = "ContainerFullUpdateRequest", description = "待装柜或预装柜中阶段完整编辑")
public record FullUpdateRequest(
@NotNull @PositiveOrZero Integer version,
@Valid @NotNull SaveRequest data
) {}
@Schema(name = "ContainerTransportUpdateRequest",
description = "LOADING 阶段只允许修改集装箱号和航次")
description = "拣货中阶段只允许修改集装箱号和航次")
public record TransportUpdateRequest(
@Size(max = 64) String containerNo,
@Size(max = 64) String voyageNo,
@@ -13,7 +13,13 @@ import org.apache.ibatis.annotations.Update;
public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntity> {
@Select("""
<script>
SELECT b.*, p.id AS plan_id, COALESCE(p.plan_status, 'EDITING') AS plan_status
SELECT b.*, p.id AS plan_id,
CASE WHEN p.id IS NULL THEN 'WAIT_LOADING'
WHEN p.plan_status != 'COMPLETED' AND NOT EXISTS (
SELECT 1 FROM fl_loading_plan_item active_item
WHERE active_item.loading_plan_id = p.id AND active_item.deleted = 0
) THEN 'WAIT_LOADING'
ELSE p.plan_status END AS plan_status
FROM fl_container_booking b LEFT JOIN fl_loading_plan p ON p.container_booking_id = b.id
WHERE (#{query.containerNo} IS NULL OR b.container_no = #{query.containerNo})
AND (#{query.cabinetNo} IS NULL OR b.cabinet_no = #{query.cabinetNo})
@@ -24,7 +30,12 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
AND (#{query.departurePortCode} IS NULL OR b.departure_port_code = #{query.departurePortCode})
AND (#{query.destinationPortCode} IS NULL OR b.destination_port_code = #{query.destinationPortCode})
AND (#{query.destinationWarehouseCode} IS NULL OR b.destination_warehouse_code = #{query.destinationWarehouseCode})
AND (#{query.status} IS NULL OR COALESCE(p.plan_status, 'EDITING') = #{query.status})
AND (#{query.status} IS NULL OR CASE WHEN p.id IS NULL THEN 'WAIT_LOADING'
WHEN p.plan_status != 'COMPLETED' AND NOT EXISTS (
SELECT 1 FROM fl_loading_plan_item active_item
WHERE active_item.loading_plan_id = p.id AND active_item.deleted = 0
) THEN 'WAIT_LOADING'
ELSE p.plan_status END = #{query.status})
AND (#{query.estimatedSailingFrom} IS NULL OR b.estimated_sailing_at &gt;= #{query.estimatedSailingFrom})
AND (#{query.estimatedSailingTo} IS NULL OR b.estimated_sailing_at &lt;= #{query.estimatedSailingTo})
AND (#{query.keyword} IS NULL OR b.container_no LIKE CONCAT('%', #{query.keyword}, '%')
@@ -38,7 +49,13 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
Page<ContainerBookingEntity> selectBookingPage(Page<ContainerBookingEntity> page, @Param("query") Query query);
@Select("""
SELECT b.*, p.id AS plan_id, COALESCE(p.plan_status, 'EDITING') AS plan_status
SELECT b.*, p.id AS plan_id,
CASE WHEN p.id IS NULL THEN 'WAIT_LOADING'
WHEN p.plan_status != 'COMPLETED' AND NOT EXISTS (
SELECT 1 FROM fl_loading_plan_item active_item
WHERE active_item.loading_plan_id = p.id AND active_item.deleted = 0
) THEN 'WAIT_LOADING'
ELSE p.plan_status END AS plan_status
FROM fl_container_booking b LEFT JOIN fl_loading_plan p ON p.container_booking_id = b.id
WHERE b.id = #{id}
""")
@@ -45,7 +45,10 @@ public interface LoadingListQueryMapper {
FROM fl_loading_plan p
JOIN fl_container_booking b ON b.id = p.container_booking_id
LEFT JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id AND i.deleted = 0
WHERE p.plan_status = 'LOADING'
WHERE p.preload_pushed_at IS NOT NULL
AND p.plan_status IN ('EDITING', 'LOADING')
AND EXISTS (SELECT 1 FROM fl_loading_plan_item active_item
WHERE active_item.loading_plan_id = p.id AND active_item.deleted = 0)
<if test="query.status != null and query.status != ''">AND p.plan_status = #{query.status}</if>
<if test="query.keyword != null and query.keyword != ''">
AND (p.plan_no LIKE CONCAT('%', #{query.keyword}, '%')
@@ -8,10 +8,28 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstl
""")
int touchEditing(@Param("planId") long planId,@Param("expectedVersion") int expectedVersion,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
@Update("""
UPDATE fl_loading_plan SET plan_status='LOADING',push_idempotency_key=#{key},
UPDATE fl_loading_plan SET push_idempotency_key=#{key},
preload_pushed_by=#{operatorId},preload_pushed_by_name=#{operatorName},preload_pushed_at=CURRENT_TIMESTAMP(3),
updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE id=#{planId} AND plan_status='EDITING' AND version=#{expectedVersion}
WHERE id=#{planId} AND plan_status='EDITING' AND push_idempotency_key IS NULL AND version=#{expectedVersion}
""")
int pushPreload(@Param("planId") long planId,@Param("key") String key,@Param("expectedVersion") int expectedVersion,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
@Update("""
UPDATE fl_loading_plan SET plan_status='LOADING',updated_by=#{operatorId},updated_by_name=#{operatorName},
updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE id=#{planId} AND plan_status='EDITING' AND preload_pushed_at IS NOT NULL
""")
int startPicking(@Param("planId") long planId,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
@Update("""
UPDATE fl_loading_plan plan SET plan_status='EDITING',updated_by=#{operatorId},updated_by_name=#{operatorName},
updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE plan.id=#{planId} AND plan.plan_status='LOADING' AND plan.version=#{expectedVersion}
AND NOT EXISTS (
SELECT 1 FROM fl_loading_pick_item pick_item
JOIN fl_loading_pick_task pick_task ON pick_task.id=pick_item.pick_task_id
WHERE pick_task.loading_plan_id=plan.id AND pick_item.picked_package_count>0
)
""")
int recycleUnpicked(@Param("planId") long planId,@Param("expectedVersion") int expectedVersion,
@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
}
@@ -72,4 +72,10 @@ public class SmartLoadingRepository {
public int push(long planId, String key, int version, long operatorId, String operatorName) {
return plans.pushPreload(planId, key, version, operatorId, operatorName);
}
public int startPicking(long planId, long operatorId, String operatorName) {
return plans.startPicking(planId, operatorId, operatorName);
}
public int recycleUnpicked(long planId, int expectedVersion, long operatorId, String operatorName) {
return plans.recycleUnpicked(planId, expectedVersion, operatorId, operatorName);
}
}
@@ -46,7 +46,7 @@ public class ContainerService {
value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName); value.setVersion(0);
try { repository.insert(value); } catch (DuplicateKeyException ex) { throw duplicate(); }
value.setPlanStatus(SmartLoadingStatus.EDITING.name());
value.setPlanStatus(SmartLoadingStatus.WAIT_LOADING.name());
return value;
}
@@ -55,7 +55,7 @@ public class ContainerService {
FullUpdateRequest request) {
ContainerBookingEntity current = required(id);
requireVersion(current, request.version());
if (status(current) != SmartLoadingStatus.EDITING) {
if (status(current) != SmartLoadingStatus.WAIT_LOADING && status(current) != SmartLoadingStatus.EDITING) {
throw conflict("货柜已进入预装流程,只能修改集装箱号和航次");
}
validateSchedule(request.data());
@@ -32,7 +32,7 @@ import java.util.Set;
@Service
public class LoadingListService {
private static final Set<String> LOADING_STATUSES = Set.of("LOADING");
private static final Set<String> PRELOAD_STATUSES = Set.of("EDITING", "LOADING");
@Resource
private LoadingListRepository repository;
@Resource
@@ -43,8 +43,8 @@ public class LoadingListService {
public PageResult<PreloadSummary> preloadPage(PreloadQuery query) {
validateRange(query.createdFrom(), query.createdTo());
String status = trim(query.status());
if (StringUtils.hasText(status) && !LOADING_STATUSES.contains(status)) {
throw badRequest("装柜清单状态只能是 LOADING");
if (StringUtils.hasText(status) && !PRELOAD_STATUSES.contains(status)) {
throw badRequest("预装清单状态只能是 EDITING 或 LOADING");
}
return repository.preloadPage(new PreloadQuery(trim(query.keyword()), status,
query.warehouseId(), query.createdFrom(), query.createdTo(), query.page(), query.size()));
@@ -59,8 +59,8 @@ public class LoadingListService {
public PreloadDetail preloadDetail(long planId, long warehouseId) {
AggregateRow row = requirePlan(planId);
requireWarehouse(row, warehouseId);
if (!LOADING_STATUSES.contains(row.getPlanStatus())) {
throw notFound("装柜中清单不存在");
if (!PRELOAD_STATUSES.contains(row.getPlanStatus())) {
throw notFound("预装清单不存在");
}
return repository.preloadDetail(row);
}
@@ -160,8 +160,8 @@ public class LoadingListService {
}
private static void requireLoadingMutable(AggregateRow row) {
if (!LOADING_STATUSES.contains(row.getPlanStatus())) {
throw conflict("仅装柜中可修改集装箱号与航次");
if (!PRELOAD_STATUSES.contains(row.getPlanStatus())) {
throw conflict("仅预装柜中或拣货中可修改集装箱号与航次");
}
}
@@ -22,7 +22,9 @@ import com.baiyue.firstleg.entity.LoadingPickOperationEntity;
import com.baiyue.firstleg.entity.LoadingPickReviewEntity;
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity;
import com.baiyue.firstleg.repository.LoadingPickRepository;
import com.baiyue.firstleg.repository.SmartLoadingRepository;
import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
@@ -48,6 +50,8 @@ public class LoadingPickService {
@Resource
private LoadingPickRepository repository;
@Resource
private SmartLoadingRepository smartLoadingRepository;
@Resource
private LoadingListService loadingListService;
@Resource
private PdaPickSettlementService settlementService;
@@ -123,6 +127,7 @@ public class LoadingPickService {
throw conflict("PDA任务分配版本已变化");
}
}
startPickingIfNeeded(task, operatorId, operatorName);
insertOperation(taskId, operationId, requestHash, "CLAIM", operatorId, operatorName);
return view(task, null);
}
@@ -182,6 +187,7 @@ public class LoadingPickService {
history.setOperatorId(operatorId);
history.setOperatorName(operatorName);
repository.insert(history);
startPickingIfNeeded(task, operatorId, operatorName);
touchSettleFinishAndSync(task, operatorId, operatorName);
return view(requiredTask(taskId), request.warehouseId());
}
@@ -207,6 +213,7 @@ public class LoadingPickService {
}
insertHistory(taskId, itemId, operationId, requestHash, "PARTIAL",
request.finalPickedPackageCount(), null, operatorId, operatorName);
startPickingIfNeeded(task, operatorId, operatorName);
touchSettleFinishAndSync(task, operatorId, operatorName);
return view(requiredTask(taskId), request.warehouseId());
}
@@ -271,6 +278,9 @@ public class LoadingPickService {
for (BatchLine line : lines) {
applyBatchLine(taskId, line, operationId, operatorId, operatorName);
}
if (lines.stream().anyMatch(line -> "PICK".equals(line.action()) || "PARTIAL".equals(line.action()))) {
startPickingIfNeeded(task, operatorId, operatorName);
}
if (!repository.allTaskItemsResolved(taskId)) {
throw conflict("整批提交必须处理本任务全部待拣明细");
}
@@ -375,6 +385,25 @@ public class LoadingPickService {
}
}
/** 进入PDA拣货任务即将预装柜计划推进为拣货中;状态推进和历史写入保持幂等。 */
private void startPickingIfNeeded(LoadingPickTaskEntity task, long operatorId, String operatorName) {
if (smartLoadingRepository.startPicking(task.getLoadingPlanId(), operatorId, operatorName) != 1) {
return;
}
LoadingPlanHistoryEntity history = new LoadingPlanHistoryEntity();
history.setLoadingPlanId(task.getLoadingPlanId());
history.setOperationId("PDA-START-" + task.getId());
history.setActionType("START_LOADING");
history.setFromStatus("EDITING");
history.setToStatus("LOADING");
history.setBeforeSnapshot("{\"source\":\"PDA\"}");
history.setAfterSnapshot("{\"source\":\"PDA\",\"status\":\"LOADING\"}");
history.setOperatorId(operatorId);
history.setOperatorName(operatorName);
history.setOperationReason("进入PDA拣货任务");
smartLoadingRepository.insert(history);
}
private LoadingPickTaskEntity requiredTask(long taskId) {
LoadingPickTaskEntity task = repository.task(taskId);
if (task == null) {
@@ -89,7 +89,7 @@ public class SmartLoadingService {
+ '|' + request.expectedVersion());
Plan replay = replay(operation, planId, "ADD_ITEM", null, requestHash);
if (replay != null) return replay;
LoadingPlanEntity plan = editing(planId, request.expectedVersion());
LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName);
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
Candidate candidate = repository.candidate(booking.getWarehouseId(), request.inboundId(), request.inboundDetailId());
if (candidate == null) throw conflict("入库货物不存在或尚未完成入库");
@@ -131,7 +131,7 @@ public class SmartLoadingService {
String requestHash = sha256("RECOMMEND|" + planId + '|' + request.expectedVersion());
Plan replay = replay(operation, planId, "SMART_RECOMMEND", null, requestHash);
if (replay != null) return replay;
LoadingPlanEntity plan = editing(planId, request.expectedVersion());
LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName);
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
for (Candidate candidate : candidates(booking.getWarehouseId())) {
if (!Objects.equals(candidate.warehouseId(), booking.getWarehouseId())) continue;
@@ -163,7 +163,7 @@ public class SmartLoadingService {
+ '|' + request.expectedItemVersion());
Plan replay = replay(operation, planId, "ADJUST_ITEM", itemId, requestHash);
if (replay != null) return replay;
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion());
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion(), operatorId, operatorName);
LoadingPlanItemEntity item = repository.item(planId, itemId);
if (item == null) throw new BizException(ResultCode.NOT_FOUND, "装柜明细不存在");
if (!Objects.equals(item.getVersion(), request.expectedItemVersion())) throw conflict("装柜明细版本冲突");
@@ -197,7 +197,7 @@ public class SmartLoadingService {
+ request.expectedPlanVersion() + '|' + request.expectedItemVersion());
Plan replay = replay(operation, planId, "REMOVE_ITEM", itemId, requestHash);
if (replay != null) return replay;
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion());
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion(), operatorId, operatorName);
LoadingPlanItemEntity item = repository.item(planId, itemId);
if (item == null) throw new BizException(ResultCode.NOT_FOUND, "装柜明细不存在");
loadingPickService.remove(itemId);
@@ -216,7 +216,8 @@ public class SmartLoadingService {
String key = trim(idempotencyKey);
if (!StringUtils.hasText(key) || key.length() > 128) throw new BizException(ResultCode.BAD_REQUEST, "必须提供有效 Idempotency-Key");
LoadingPlanEntity plan = repository.plan(planId); if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "装柜计划不存在");
if ("LOADING".equals(plan.getPlanStatus()) && key.equals(plan.getPushIdempotencyKey())) return view(plan, requireBooking(plan.getContainerBookingId()));
if (key.equals(plan.getPushIdempotencyKey())) return view(plan, requireBooking(plan.getContainerBookingId()));
if (StringUtils.hasText(plan.getPushIdempotencyKey())) throw conflict("预装清单已推送,不能使用新的幂等键重复推送");
if (!"EDITING".equals(plan.getPlanStatus()) || !Objects.equals(plan.getVersion(), request.expectedVersion())) throw conflict("仅编辑中的计划可开始装柜且版本必须匹配");
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
List<LoadingPlanItemEntity> items = repository.items(planId);
@@ -228,7 +229,7 @@ public class SmartLoadingService {
try {
if (repository.push(planId, key, request.expectedVersion(), operatorId, operatorName) != 1) throw conflict("装柜计划已被并发修改");
} catch (DuplicateKeyException e) { throw conflict("幂等键已用于其他推送"); }
history(planId, null, "START_LOADING", "EDITING", "LOADING", key, operatorId, operatorName);
history(planId, null, "PUSH_PRELOAD", "EDITING", "EDITING", key, operatorId, operatorName);
LoadingPlanEntity pushed = repository.plan(planId);
return view(pushed, requireBooking(pushed.getContainerBookingId()));
}
@@ -329,7 +330,19 @@ public class SmartLoadingService {
}
return view(plan, booking);
}
private LoadingPlanEntity editing(long id,int version){LoadingPlanEntity p=repository.plan(id);if(p==null)throw new BizException(ResultCode.NOT_FOUND,"装柜计划不存在");if(!Set.of("EDITING", "LOADING").contains(p.getPlanStatus())||!Objects.equals(p.getVersion(),version))throw conflict("计划已完成或版本冲突");return p;}
private LoadingPlanEntity editing(long id, int version, long operatorId, String operatorName) {
LoadingPlanEntity p = repository.plan(id);
if (p == null) throw new BizException(ResultCode.NOT_FOUND, "装柜计划不存在");
if (!Set.of("EDITING", "LOADING").contains(p.getPlanStatus()) || !Objects.equals(p.getVersion(), version)) {
throw conflict("计划已完成或版本冲突");
}
if ("LOADING".equals(p.getPlanStatus())
&& repository.recycleUnpicked(id, version, operatorId, operatorName) == 1) {
p.setPlanStatus("EDITING");
p.setVersion(version + 1);
}
return p;
}
private ContainerBookingEntity requireBooking(long id){ContainerBookingEntity b=repository.booking(id);if(b==null)throw new BizException(ResultCode.NOT_FOUND,"货柜预订不存在");return b;}
private void touch(LoadingPlanEntity p,long op,String name){if(repository.touch(p.getId(),p.getVersion(),op,name)!=1)throw conflict("计划已被并发修改");}
private Plan replay(String operation, long planId, String action, Long itemId, String requestHash) {
@@ -79,11 +79,18 @@ public class SplitPreloadService {
SplitActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, START_CONFIRMING, key);
if (replay != null) return replay;
InboundSnapshot inbound = requireInbound(tenantId, inboundId);
requireStatus(inbound, "INBOUNDED", "仅待分柜订单可以提交预装清单确认");
boolean loading = "LOADING".equals(inbound.processStatus());
if (loading) {
// Starting a real loading task advances the inbound before its preload
// confirmation is submitted; retain the linked-container hard gate.
loadingModelBridge.requireStarted(tenantId, inboundId);
} else {
requireStatus(inbound, "INBOUNDED", "仅待分柜订单可以提交预装清单确认");
}
PreloadPlanEntity plan = requirePlan(tenantId, inboundId);
if (PreloadStatus.CONFIRMING.equals(plan.getStatus())) {
return complete(tenantId, inboundId, operatorId, START_CONFIRMING, key,
new SplitActionResponse("INBOUNDED", response(plan)));
new SplitActionResponse(loading ? "LOADING" : "INBOUNDED", response(plan)));
}
if (!PreloadStatus.PENDING.equals(plan.getStatus())
|| repository.changePlanStatus(tenantId, inboundId, PreloadStatus.PENDING,
@@ -91,7 +98,8 @@ public class SplitPreloadService {
throw concurrent();
}
return complete(tenantId, inboundId, operatorId, START_CONFIRMING, key,
new SplitActionResponse("INBOUNDED", response(repository.findPlan(tenantId, inboundId))));
new SplitActionResponse(loading ? "LOADING" : "INBOUNDED",
response(repository.findPlan(tenantId, inboundId))));
}
@Transactional