fix(order): project exact customer fulfillment progress in detail
This commit is contained in:
@@ -10,7 +10,8 @@ public final class MerchantWorkbenchFilters {
|
||||
private MerchantWorkbenchFilters() {}
|
||||
|
||||
public record Request(@NotNull @Positive Long tenantId,
|
||||
@Pattern(regexp = "WAIT_MEASURE|WAIT_PALLETIZE|WAIT_SPLIT|LOADING|LOADED|DELIVERED") String mainStatus) {}
|
||||
@Pattern(regexp = "WAIT_MEASURE|WAIT_PALLETIZE|WAIT_SPLIT|LOADING|LOADED|DELIVERED") String mainStatus,
|
||||
@jakarta.validation.constraints.Size(min=1,max=200) List<@NotNull @Positive Long> orderIds) {}
|
||||
|
||||
public record Response(List<Long> matchingStatusOrderIds, List<Long> feeOrderIds,
|
||||
List<Long> customsOrderIds, List<Progress> progress) {}
|
||||
|
||||
+12
-2
@@ -65,6 +65,7 @@ public interface MerchantWorkbenchFilterMapper {
|
||||
List<Long> pendingCustoms(@Param("tenantId") long tenantId);
|
||||
/** Only physically loaded fragments can expose container milestones to the customer. */
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT o.order_id, CASE WHEN owned.container_booking_id IS NULL THEN 0
|
||||
WHEN EXISTS (SELECT 1 FROM fl_carrier_schedule_node_fact n
|
||||
WHERE n.container_booking_id=owned.container_booking_id
|
||||
@@ -76,6 +77,11 @@ public interface MerchantWorkbenchFilterMapper {
|
||||
LEFT JOIN (
|
||||
SELECT DISTINCT i.inbound_id, i.account_id, p.id AS loading_plan_id, p.container_booking_id
|
||||
FROM fl_loading_plan_item i
|
||||
<if test="orderIds != null">
|
||||
JOIN fl_inbound_order selected ON selected.id=i.inbound_id AND selected.tenant_id=i.account_id
|
||||
AND selected.deleted=0 AND selected.order_id IN
|
||||
<foreach collection="orderIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</if>
|
||||
JOIN fl_loading_plan p ON p.id=i.loading_plan_id AND p.plan_status='COMPLETED'
|
||||
JOIN fl_loading_pick_task t ON t.loading_plan_id=p.id AND t.task_status='COMPLETED'
|
||||
JOIN fl_loading_pick_item picked ON picked.pick_task_id=t.id AND picked.loading_plan_item_id=i.id
|
||||
@@ -84,8 +90,12 @@ public interface MerchantWorkbenchFilterMapper {
|
||||
AND picked.item_status IN ('PICKED','PARTIAL') AND picked.picked_package_count>0
|
||||
) owned ON owned.inbound_id=o.id AND owned.account_id=o.tenant_id
|
||||
WHERE o.tenant_id=#{tenantId} AND o.deleted=0 AND o.order_id IS NOT NULL
|
||||
AND o.process_status <> 'CANCELLED'
|
||||
AND o.process_status <> 'CANCELLED'
|
||||
<if test="orderIds != null">
|
||||
AND o.order_id IN <foreach collection="orderIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</if>
|
||||
ORDER BY o.order_id
|
||||
</script>
|
||||
""")
|
||||
List<com.baiyue.firstleg.dto.MerchantWorkbenchFilters.Fragment> progressFragments(@Param("tenantId") long tenantId);
|
||||
List<com.baiyue.firstleg.dto.MerchantWorkbenchFilters.Fragment> progressFragments(@Param("tenantId") long tenantId, @Param("orderIds") List<Long> orderIds);
|
||||
}
|
||||
|
||||
+5
-1
@@ -12,11 +12,15 @@ public class MerchantWorkbenchFilterService {
|
||||
|
||||
@org.springframework.transaction.annotation.Transactional(readOnly=true)
|
||||
public MerchantWorkbenchFilters.Response read(MerchantWorkbenchFilters.Request request) {
|
||||
if (request.orderIds() != null) {
|
||||
return new MerchantWorkbenchFilters.Response(null, java.util.List.of(), java.util.List.of(),
|
||||
progress(mapper.progressFragments(request.tenantId(), request.orderIds())));
|
||||
}
|
||||
return new MerchantWorkbenchFilters.Response(
|
||||
request.mainStatus() == null ? null : mapper.byProcessStatuses(request.tenantId(),
|
||||
unified.internalStatuses(request.mainStatus())),
|
||||
mapper.pendingFees(request.tenantId()), mapper.pendingCustoms(request.tenantId()),
|
||||
progress(mapper.progressFragments(request.tenantId())));
|
||||
progress(mapper.progressFragments(request.tenantId(), null)));
|
||||
}
|
||||
|
||||
static java.util.List<MerchantWorkbenchFilters.Progress> progress(java.util.List<MerchantWorkbenchFilters.Fragment> facts) {
|
||||
|
||||
@@ -355,7 +355,7 @@ paths:
|
||||
/internal/firstleg/v1/inbounds/unified-orders/workbench-filters:
|
||||
post:
|
||||
summary: 查询单一客户工作台履约筛选标识(Order内部)
|
||||
description: 只读返回命中状态、最新待确认费用及待上传资料的订单标识。客户必须明确且为正数;不创建报关聚合,不跨Schema读取。
|
||||
description: 只读返回命中状态、最新待确认费用及待上传资料的订单标识。客户必须明确且为正数;不创建报关聚合,不跨Schema读取。传入orderIds为精确进度模式,仅计算progress,matchingStatusOrderIds为null,feeOrderIds和customsOrderIds为空;mainStatus在该模式不参与查询。
|
||||
security: [{internalServiceToken: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -367,6 +367,13 @@ paths:
|
||||
properties:
|
||||
tenantId: {type: integer, format: int64, minimum: 1}
|
||||
mainStatus: {type: string, nullable: true, enum: [WAIT_MEASURE, WAIT_PALLETIZE, WAIT_SPLIT, LOADING, LOADED, DELIVERED]}
|
||||
orderIds:
|
||||
type: array
|
||||
nullable: true
|
||||
minItems: 1
|
||||
maxItems: 200
|
||||
items: {type: integer, format: int64, minimum: 1}
|
||||
description: 省略或null查询当前客户全部进度;传入时仅在SQL内查询这些来源Order订单,空数组拒绝,不允许扩大到全客户。
|
||||
responses:
|
||||
'200':
|
||||
description: 按响应code判断,标识只供Order在自身客户范围内过滤、计数和分页
|
||||
|
||||
+15
-1
@@ -45,7 +45,7 @@ class InternalInboundOrderReferenceControllerTest {
|
||||
private InternalInboundOrderController controller;
|
||||
|
||||
@Test void workbenchFiltersRejectMissingAndWrongInternalIdentity() {
|
||||
var request = new com.baiyue.firstleg.dto.MerchantWorkbenchFilters.Request(11L, "LOADED");
|
||||
var request = new com.baiyue.firstleg.dto.MerchantWorkbenchFilters.Request(11L, "LOADED", null);
|
||||
assertThatThrownBy(() -> controller.workbenchFilters(null, request)).isInstanceOf(BizException.class);
|
||||
assertThatThrownBy(() -> controller.workbenchFilters("wrong", request)).isInstanceOf(BizException.class);
|
||||
org.mockito.Mockito.verifyNoInteractions(workbenchFilters);
|
||||
@@ -143,4 +143,18 @@ class InternalInboundOrderReferenceControllerTest {
|
||||
carrierId == null ? null : "承运商", null, null, null, null,
|
||||
null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Test void optionalExactScopeRejectsEmptyOversizeNullOrNonPositiveItems() {
|
||||
try(var factory=jakarta.validation.Validation.buildDefaultValidatorFactory()) {
|
||||
var validator=factory.getValidator();
|
||||
for(java.util.List<Long> ids: java.util.List.of(java.util.List.<Long>of(),java.util.List.of(0L),
|
||||
java.util.Arrays.asList(1L,null),java.util.stream.LongStream.rangeClosed(1,201).boxed().toList())) {
|
||||
assertThat(validator.validate(new com.baiyue.firstleg.dto.MerchantWorkbenchFilters.Request(11L,null,ids))).isNotEmpty();
|
||||
}
|
||||
for(java.util.List<Long> ids: java.util.Arrays.asList(null,java.util.List.of(1L),java.util.stream.LongStream.rangeClosed(1,200).boxed().toList())) {
|
||||
assertThat(validator.validate(new com.baiyue.firstleg.dto.MerchantWorkbenchFilters.Request(11L,null,ids))).isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+12
@@ -74,4 +74,16 @@ class UnifiedInboundOpenApiContractTest {
|
||||
.containsOnlyKeys("standardNode","nodeSequence","providerCode","estimatedAt","actualAt","locationCode","locationName");
|
||||
}
|
||||
|
||||
|
||||
@Test @SuppressWarnings("unchecked") void documentsBoundedOptionalExactOrderScope() throws Exception {
|
||||
var document=(java.util.Map<String,Object>)new org.yaml.snakeyaml.Yaml().load(
|
||||
new org.springframework.core.io.ClassPathResource("openapi/firstleg-v1.yaml").getContentAsString(java.nio.charset.StandardCharsets.UTF_8));
|
||||
var paths=(java.util.Map<String,Object>)document.get("paths");
|
||||
var operation=(java.util.Map<String,Object>)((java.util.Map<String,Object>)paths.get("/internal/firstleg/v1/inbounds/unified-orders/workbench-filters")).get("post");
|
||||
var content=(java.util.Map<String,Object>)((java.util.Map<String,Object>)operation.get("requestBody")).get("content");
|
||||
var schema=(java.util.Map<String,Object>)((java.util.Map<String,Object>)content.get("application/json")).get("schema");
|
||||
var ids=(java.util.Map<String,Object>)((java.util.Map<String,Object>)schema.get("properties")).get("orderIds");
|
||||
assertThat(ids).containsEntry("minItems",1).containsEntry("maxItems",200).containsEntry("nullable",true);
|
||||
assertThat((java.util.List<String>)schema.get("required")).doesNotContain("orderIds");
|
||||
}
|
||||
}
|
||||
|
||||
+16
-7
@@ -83,19 +83,28 @@ class MerchantWorkbenchFilterMapperTest {
|
||||
}
|
||||
|
||||
@Test void onlyActualLoadedOwnedFragmentsAndActualClearanceAdvanceProgress() {
|
||||
assertThat(mapper.progressFragments(11)).allMatch(f->f.stage()==0);
|
||||
assertThat(mapper.progressFragments(11, null)).allMatch(f->f.stage()==0);
|
||||
jdbc.update("INSERT INTO fl_loading_pick_task VALUES(900,100,'COMPLETED')");
|
||||
jdbc.update("INSERT INTO fl_loading_pick_item SELECT 900,id,'PICKED',2 FROM fl_loading_plan_item");
|
||||
assertThat(mapper.progressFragments(11)).allMatch(f->f.stage()==1).noneMatch(f->f.orderId()==103||f.orderId()==104);
|
||||
assertThat(mapper.progressFragments(11, null)).allMatch(f->f.stage()==1).noneMatch(f->f.orderId()==103||f.orderId()==104);
|
||||
jdbc.update("INSERT INTO fl_customs_container VALUES(200,100,'DECLARING')");
|
||||
assertThat(mapper.progressFragments(11)).allMatch(f->f.stage()==1);
|
||||
assertThat(mapper.progressFragments(11, null)).allMatch(f->f.stage()==1);
|
||||
jdbc.update("UPDATE fl_customs_container SET status='COMPLETED'");
|
||||
assertThat(mapper.progressFragments(11)).allMatch(f->f.stage()==2);
|
||||
assertThat(mapper.progressFragments(11, null)).allMatch(f->f.stage()==2);
|
||||
jdbc.update("INSERT INTO fl_carrier_schedule_node_fact VALUES(100,'PICKUP_READY',NULL)");
|
||||
assertThat(mapper.progressFragments(11)).allMatch(f->f.stage()==2);
|
||||
assertThat(mapper.progressFragments(11, null)).allMatch(f->f.stage()==2);
|
||||
jdbc.update("UPDATE fl_carrier_schedule_node_fact SET actual_at=CURRENT_TIMESTAMP");
|
||||
assertThat(mapper.progressFragments(11)).allMatch(f->f.stage()==3);
|
||||
assertThat(mapper.progressFragments(11, null)).allMatch(f->f.stage()==3);
|
||||
assertThat(mapper.progressFragments(11,List.of(101L))).singleElement().satisfies(f->{assertThat(f.orderId()).isEqualTo(101);assertThat(f.stage()).isEqualTo(3);});
|
||||
jdbc.update("UPDATE fl_container_booking SET record_status='CANCELLED'");
|
||||
assertThat(mapper.progressFragments(11)).allMatch(f->f.stage()==0);
|
||||
assertThat(mapper.progressFragments(11, null)).allMatch(f->f.stage()==0);
|
||||
}
|
||||
|
||||
@Test void exactOrderScopeKeepsAllItsFragmentsAndCannotReadForeignOrDeletedOrders() {
|
||||
jdbc.update("INSERT INTO fl_inbound_order(id,tenant_id,order_id,deleted,tax_mode,process_status) VALUES(50,11,101,0,'TAX_REFUND','LOADED')");
|
||||
assertThat(mapper.progressFragments(11,List.of(101L,103L,104L))).extracting(f->f.orderId()).containsExactly(101L,101L);
|
||||
assertThat(mapper.progressFragments(11,List.of(999L))).isEmpty();
|
||||
assertThat(mapper.progressFragments(22,List.of(101L))).isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.firstleg.dto.MerchantWorkbenchFilters;
|
||||
import com.baiyue.firstleg.mapper.MerchantWorkbenchFilterMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class MerchantWorkbenchFilterServiceTest {
|
||||
@Test void exactDetailProgressSkipsUnrelatedCustomerQueues() {
|
||||
var mapper=mock(MerchantWorkbenchFilterMapper.class);
|
||||
var unified=mock(UnifiedInboundQueryService.class);
|
||||
var service=new MerchantWorkbenchFilterService();
|
||||
ReflectionTestUtils.setField(service,"mapper",mapper);
|
||||
ReflectionTestUtils.setField(service,"unified",unified);
|
||||
when(mapper.progressFragments(11,List.of(101L))).thenReturn(List.of(
|
||||
new MerchantWorkbenchFilters.Fragment(101,3),new MerchantWorkbenchFilters.Fragment(101,1)));
|
||||
var response=service.read(new MerchantWorkbenchFilters.Request(11L,null,List.of(101L)));
|
||||
assertThat(response.progress()).containsExactly(new MerchantWorkbenchFilters.Progress(101,"cleared",true));
|
||||
assertThat(response.feeOrderIds()).isEmpty();assertThat(response.customsOrderIds()).isEmpty();
|
||||
assertThat(response.matchingStatusOrderIds()).isNull();
|
||||
verify(mapper).progressFragments(11,List.of(101L));verifyNoMoreInteractions(mapper);verifyNoInteractions(unified);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user