fix(order): project exact customer fulfillment progress in detail
This commit is contained in:
+1
-1
@@ -20,6 +20,6 @@ public class InternalCustomerOrderProgressController {
|
||||
@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
|
||||
@Valid @RequestBody CustomerOrderProgress.Request request) {
|
||||
guard.verify(token);
|
||||
return ApiResponse.success(service.read(request.customerId()));
|
||||
return ApiResponse.success(service.read(request.customerId(), request.orderIds()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import java.util.List;
|
||||
|
||||
public final class CustomerOrderProgress {
|
||||
private CustomerOrderProgress() {}
|
||||
public record Request(@NotNull @Positive Long customerId) {}
|
||||
public record Request(@NotNull @Positive Long customerId,
|
||||
@jakarta.validation.constraints.Size(min=1,max=200) List<@NotNull @Positive Long> orderIds) {}
|
||||
public record Source(String orderId, long packages, long scheduledPackages, long deliveringPackages) {}
|
||||
public record Progress(long orderId, String stage, boolean partial) {}
|
||||
public record Response(List<Progress> progress) {}
|
||||
|
||||
+35
-4
@@ -8,12 +8,17 @@ import java.util.List;
|
||||
@Mapper
|
||||
public interface CustomerOrderProgressMapper {
|
||||
@Select("""
|
||||
<script>
|
||||
WITH scheduled AS (
|
||||
SELECT line.order_id,SUM(a.planned_package_count) AS packages
|
||||
FROM ll_dispatch_order d
|
||||
JOIN ll_dispatch_item i ON i.dispatch_order_id=d.id AND i.customer_id=d.customer_id
|
||||
JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=i.plan_pallet_id
|
||||
JOIN ll_inbound_plan_line line ON line.id=a.line_id AND line.customer_id=d.customer_id
|
||||
<if test="orderIds != null">
|
||||
JOIN ll_order planned_source ON planned_source.id=line.order_id AND planned_source.customer_id=#{customerId}
|
||||
AND planned_source.source_order_id IN <foreach collection="orderIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</if>
|
||||
WHERE d.customer_id=#{customerId} AND d.status IN ('PENDING_DEPARTURE','PICKING','READY_TO_DEPART')
|
||||
AND i.warehouse_pallet_id IS NULL
|
||||
GROUP BY line.order_id
|
||||
@@ -24,14 +29,32 @@ public interface CustomerOrderProgressMapper {
|
||||
JOIN ll_inventory_position p ON p.customer_id=d.customer_id AND p.pallet_id=i.warehouse_pallet_id
|
||||
AND p.overseas_warehouse_id=i.overseas_warehouse_id
|
||||
JOIN ll_inventory_balance b ON b.id=p.balance_id AND b.customer_id=d.customer_id
|
||||
LEFT JOIN (SELECT dispatch_item_id,inventory_position_id,SUM(packages) AS packages
|
||||
FROM ll_dispatch_pda_pick WHERE customer_id=#{customerId}
|
||||
GROUP BY dispatch_item_id,inventory_position_id) pk
|
||||
<if test="orderIds != null">
|
||||
JOIN ll_order stock_source ON stock_source.id=b.inbound_order_id AND stock_source.customer_id=#{customerId}
|
||||
AND stock_source.source_order_id IN <foreach collection="orderIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</if>
|
||||
LEFT JOIN (SELECT pick.dispatch_item_id,pick.inventory_position_id,SUM(pick.packages) AS packages
|
||||
FROM ll_dispatch_pda_pick pick
|
||||
<if test="orderIds != null">
|
||||
JOIN ll_inventory_position scoped_position ON scoped_position.id=pick.inventory_position_id
|
||||
AND scoped_position.customer_id=pick.customer_id
|
||||
JOIN ll_inventory_balance scoped_balance ON scoped_balance.id=scoped_position.balance_id
|
||||
AND scoped_balance.customer_id=pick.customer_id
|
||||
JOIN ll_order scoped_order ON scoped_order.id=scoped_balance.inbound_order_id
|
||||
AND scoped_order.customer_id=pick.customer_id
|
||||
AND scoped_order.source_order_id IN <foreach collection="orderIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</if>
|
||||
WHERE pick.customer_id=#{customerId}
|
||||
GROUP BY pick.dispatch_item_id,pick.inventory_position_id) pk
|
||||
ON pk.dispatch_item_id=i.id AND pk.inventory_position_id=p.id
|
||||
WHERE d.customer_id=#{customerId} AND d.status IN ('PENDING_DEPARTURE','PICKING','READY_TO_DEPART')
|
||||
AND (i.plan_pallet_id IS NULL OR EXISTS (
|
||||
SELECT 1 FROM ll_inbound_plan_pallet_allocation a
|
||||
JOIN ll_inbound_plan_line line ON line.id=a.line_id AND line.customer_id=d.customer_id
|
||||
<if test="orderIds != null">
|
||||
JOIN ll_order planned_source ON planned_source.id=line.order_id AND planned_source.customer_id=#{customerId}
|
||||
AND planned_source.source_order_id IN <foreach collection="orderIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</if>
|
||||
WHERE a.pallet_id=i.plan_pallet_id AND line.order_id=b.inbound_order_id))
|
||||
GROUP BY b.inbound_order_id
|
||||
), departed AS (
|
||||
@@ -41,6 +64,10 @@ public interface CustomerOrderProgressMapper {
|
||||
JOIN ll_dispatch_pda_task t ON t.id=p.task_id AND t.customer_id=a.customer_id
|
||||
JOIN ll_dispatch_order ds ON ds.id=t.dispatch_order_id AND ds.customer_id=a.customer_id
|
||||
JOIN ll_dispatch_item i ON i.id=p.dispatch_item_id AND i.dispatch_order_id=ds.id AND i.customer_id=a.customer_id
|
||||
<if test="orderIds != null">
|
||||
JOIN ll_order departed_source ON departed_source.id=a.order_id AND departed_source.customer_id=#{customerId}
|
||||
AND departed_source.source_order_id IN <foreach collection="orderIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</if>
|
||||
WHERE a.customer_id=#{customerId} AND ds.status='DEPARTED' AND ds.depart_at IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM ll_delivery_order d
|
||||
@@ -62,7 +89,11 @@ public interface CustomerOrderProgressMapper {
|
||||
FROM ll_order o WHERE o.customer_id=#{customerId} AND o.source_region='FIRSTLEG'
|
||||
AND o.source_order_id IS NOT NULL AND o.source_inbound_id IS NOT NULL
|
||||
AND o.order_type NOT IN ('RELABEL_APPLICATION','WAREHOUSE_TRANSFER')
|
||||
<if test="orderIds != null">
|
||||
AND o.source_order_id IN <foreach collection="orderIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</if>
|
||||
ORDER BY o.id
|
||||
</script>
|
||||
""")
|
||||
List<CustomerOrderProgress.Source> sources(@Param("customerId") String customerId);
|
||||
List<CustomerOrderProgress.Source> sources(@Param("customerId") String customerId, @Param("orderIds") List<String> orderIds);
|
||||
}
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@ public class CustomerOrderProgressService {
|
||||
private final CustomerOrderProgressMapper mapper;
|
||||
public CustomerOrderProgressService(CustomerOrderProgressMapper mapper) { this.mapper = mapper; }
|
||||
@Transactional(readOnly=true)
|
||||
public CustomerOrderProgress.Response read(long customerId) {
|
||||
return new CustomerOrderProgress.Response(project(mapper.sources(Long.toString(customerId))));
|
||||
public CustomerOrderProgress.Response read(long customerId, List<Long> orderIds) {
|
||||
return new CustomerOrderProgress.Response(project(mapper.sources(Long.toString(customerId), orderIds == null ? null : orderIds.stream().map(String::valueOf).toList())));
|
||||
}
|
||||
static List<CustomerOrderProgress.Progress> project(List<CustomerOrderProgress.Source> sources) {
|
||||
return sources.stream().collect(Collectors.groupingBy(CustomerOrderProgress.Source::orderId))
|
||||
|
||||
@@ -21,6 +21,13 @@ paths:
|
||||
required: [customerId]
|
||||
properties:
|
||||
customerId: {type: integer, format: int64, minimum: 1}
|
||||
orderIds:
|
||||
type: array
|
||||
nullable: true
|
||||
minItems: 1
|
||||
maxItems: 200
|
||||
items: {type: integer, format: int64, minimum: 1}
|
||||
description: 省略或null查询当前客户全部进度;传入时仅在SQL内查询这些来源Order订单,空数组拒绝,不允许扩大到全客户。
|
||||
responses:
|
||||
'200':
|
||||
description: ApiResponse,只有code为200表示成功;progress为空表示客户当前无有效排车或派送
|
||||
|
||||
+28
-2
@@ -11,10 +11,36 @@ class InternalCustomerOrderProgressControllerTest {
|
||||
@Test void validatesInternalIdentityBeforeReadingCustomerFacts() {
|
||||
var service=mock(CustomerOrderProgressService.class);
|
||||
var controller=new InternalCustomerOrderProgressController(new InternalServiceTokenGuard("test-token"),service);
|
||||
var request=new CustomerOrderProgress.Request(11L);
|
||||
var request=new CustomerOrderProgress.Request(11L, null);
|
||||
assertThatThrownBy(()->controller.read(null,request)).isInstanceOf(org.springframework.web.server.ResponseStatusException.class);
|
||||
assertThatThrownBy(()->controller.read("wrong",request)).isInstanceOf(org.springframework.web.server.ResponseStatusException.class);
|
||||
verifyNoInteractions(service);
|
||||
controller.read("test-token",request);verify(service).read(11);
|
||||
controller.read("test-token",request);verify(service).read(11, 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 CustomerOrderProgress.Request(11L,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 CustomerOrderProgress.Request(11L,ids))).isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@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/lastleg-customer-order-progress-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/lastleg/v1/customer-order-progress")).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");
|
||||
}
|
||||
}
|
||||
|
||||
+23
-10
@@ -38,21 +38,23 @@ class CustomerOrderProgressMapperTest {
|
||||
mapper=new SqlSessionTemplate(factory.getObject()).getMapper(CustomerOrderProgressMapper.class);
|
||||
}
|
||||
@Test void plannedSharedPalletIncludesEveryOwnSourceAndExcludesCancellation() {
|
||||
var sources=mapper.sources("11");assertThat(sources).hasSize(2);
|
||||
var sources=mapper.sources("11", null);assertThat(sources).hasSize(2);
|
||||
assertThat(sources.get(0).scheduledPackages()).isEqualTo(10);
|
||||
assertThat(sources.get(1).scheduledPackages()).isEqualTo(20);
|
||||
assertThat(mapper.sources("11",java.util.List.of("101"))).containsExactly(sources.get(0));
|
||||
assertThat(sources).allMatch(s->s.deliveringPackages()==0);
|
||||
jdbc.update("UPDATE ll_dispatch_order SET status='CANCELLED'");
|
||||
assertThat(mapper.sources("11")).allMatch(s->s.scheduledPackages()==0);
|
||||
assertThat(mapper.sources("22")).hasSize(1).allMatch(s->s.scheduledPackages()==0);
|
||||
assertThat(mapper.sources("11", null)).allMatch(s->s.scheduledPackages()==0);
|
||||
assertThat(mapper.sources("22", null)).hasSize(1).allMatch(s->s.scheduledPackages()==0);
|
||||
}
|
||||
@Test void receivedPalletUsesInventoryAndActualPicksWithoutCountingBothPlanAndStock() {
|
||||
jdbc.update("UPDATE ll_dispatch_item SET warehouse_pallet_id=800");
|
||||
jdbc.update("INSERT INTO ll_inventory_balance VALUES(300,'11',1),(301,'11',2)");
|
||||
jdbc.update("INSERT INTO ll_inventory_position VALUES(400,'11',800,'W1',300,6),(401,'11',800,'W1',301,20)");
|
||||
jdbc.update("INSERT INTO ll_dispatch_pda_pick VALUES(600,'11',100,400,4,700)");
|
||||
var sources=mapper.sources("11");assertThat(sources.get(0).scheduledPackages()).isEqualTo(10);
|
||||
var sources=mapper.sources("11", null);assertThat(sources.get(0).scheduledPackages()).isEqualTo(10);
|
||||
assertThat(sources.get(1).scheduledPackages()).isEqualTo(20);
|
||||
assertThat(mapper.sources("11",java.util.List.of("101"))).containsExactly(sources.get(0));
|
||||
}
|
||||
@Test void deliveryRequiresActualDepartureOutboundAndMatchingDestinationAttribution() {
|
||||
jdbc.update("INSERT INTO ll_dispatch_pda_task VALUES(700,'11',10)");
|
||||
@@ -60,15 +62,26 @@ class CustomerOrderProgressMapperTest {
|
||||
jdbc.update("INSERT INTO ll_dispatch_pda_pick_order VALUES(900,600,'11',1,4)");
|
||||
jdbc.update("INSERT INTO ll_outbound_order VALUES(10000,'11','WAIT_OUTBOUND')");
|
||||
jdbc.update("INSERT INTO ll_delivery_order VALUES(10001,'11',10,10000,'DELIVERING','CODE:FBA1')");
|
||||
assertThat(mapper.sources("11")).allMatch(s->s.deliveringPackages()==0);
|
||||
assertThat(mapper.sources("11", null)).allMatch(s->s.deliveringPackages()==0);
|
||||
jdbc.update("UPDATE ll_dispatch_order SET status='DEPARTED',depart_at=CURRENT_TIMESTAMP");
|
||||
assertThat(mapper.sources("11")).allMatch(s->s.deliveringPackages()==0);
|
||||
assertThat(mapper.sources("11", null)).allMatch(s->s.deliveringPackages()==0);
|
||||
jdbc.update("UPDATE ll_outbound_order SET status='OUTBOUNDED'");
|
||||
assertThat(mapper.sources("11").get(0).deliveringPackages()).isEqualTo(4);
|
||||
assertThat(mapper.sources("11").get(1).deliveringPackages()).isZero();
|
||||
assertThat(mapper.sources("11", null).get(0).deliveringPackages()).isEqualTo(4);
|
||||
assertThat(mapper.sources("11",java.util.List.of("101"))).containsExactly(mapper.sources("11",null).get(0));
|
||||
assertThat(mapper.sources("11", null).get(1).deliveringPackages()).isZero();
|
||||
jdbc.update("UPDATE ll_delivery_order SET delivery_group_key='CODE:FBA2'");
|
||||
assertThat(mapper.sources("11")).allMatch(s->s.deliveringPackages()==0);
|
||||
assertThat(mapper.sources("11", null)).allMatch(s->s.deliveringPackages()==0);
|
||||
jdbc.update("UPDATE ll_delivery_order SET delivery_group_key='CODE:FBA1',status='RETURN_PENDING'");
|
||||
assertThat(mapper.sources("11")).allMatch(s->s.deliveringPackages()==0);
|
||||
assertThat(mapper.sources("11", null)).allMatch(s->s.deliveringPackages()==0);
|
||||
}
|
||||
|
||||
@Test void exactSourceOrderScopePreservesAllOwnInboundFragmentsAndExcludesForeignOrders() {
|
||||
jdbc.update("INSERT INTO ll_order VALUES(4,'11','101','204','FIRSTLEG','FBA',5)");
|
||||
var own=mapper.sources("11",java.util.List.of("101","103"));
|
||||
assertThat(own).extracting(s->s.orderId()).containsExactly("101","101");
|
||||
assertThat(own).extracting(s->s.scheduledPackages()).containsExactly(10L,0L);
|
||||
assertThat(mapper.sources("22",java.util.List.of("101"))).isEmpty();
|
||||
assertThat(mapper.sources("11",java.util.List.of("999"))).isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
@@ -15,4 +15,13 @@ class CustomerOrderProgressServiceTest {
|
||||
assertThat(result.get(1).stage()).isEqualTo("delivering");assertThat(result.get(1).partial()).isFalse();
|
||||
assertThat(result.get(2).partial()).isTrue();assertThat(result.get(3).partial()).isTrue();
|
||||
}
|
||||
|
||||
@Test void boundedSourceIdsReachTheMapperBeforeProjection() {
|
||||
var mapper=org.mockito.Mockito.mock(com.baiyue.lastleg.mapper.CustomerOrderProgressMapper.class);
|
||||
org.mockito.Mockito.when(mapper.sources("11",List.of("101"))).thenReturn(List.of(new Source("101",2,2,0)));
|
||||
assertThat(new CustomerOrderProgressService(mapper).read(11,List.of(101L)).progress()).singleElement()
|
||||
.isEqualTo(new com.baiyue.lastleg.dto.CustomerOrderProgress.Progress(101,"scheduled",false));
|
||||
org.mockito.Mockito.verify(mapper).sources("11",List.of("101"));org.mockito.Mockito.verifyNoMoreInteractions(mapper);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user