优化
This commit is contained in:
@@ -13,5 +13,8 @@ public final class MerchantWorkbenchFilters {
|
|||||||
@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) {}
|
||||||
|
|
||||||
public record Response(List<Long> matchingStatusOrderIds, List<Long> feeOrderIds,
|
public record Response(List<Long> matchingStatusOrderIds, List<Long> feeOrderIds,
|
||||||
List<Long> customsOrderIds) {}
|
List<Long> customsOrderIds, List<Progress> progress) {}
|
||||||
|
|
||||||
|
public record Progress(long orderId, String stage, boolean partial) {}
|
||||||
|
public record Fragment(long orderId, int stage) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,4 +63,29 @@ public interface MerchantWorkbenchFilterMapper {
|
|||||||
ORDER BY i.order_id
|
ORDER BY i.order_id
|
||||||
""")
|
""")
|
||||||
List<Long> pendingCustoms(@Param("tenantId") long tenantId);
|
List<Long> pendingCustoms(@Param("tenantId") long tenantId);
|
||||||
|
/** Only physically loaded fragments can expose container milestones to the customer. */
|
||||||
|
@Select("""
|
||||||
|
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
|
||||||
|
AND n.standard_node='PICKUP_READY' AND n.actual_at IS NOT NULL) THEN 3
|
||||||
|
WHEN EXISTS (SELECT 1 FROM fl_customs_container c
|
||||||
|
WHERE c.loading_plan_id=owned.loading_plan_id AND c.status='COMPLETED') THEN 2
|
||||||
|
ELSE 1 END AS stage
|
||||||
|
FROM fl_inbound_order o
|
||||||
|
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
|
||||||
|
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
|
||||||
|
JOIN fl_container_booking b ON b.id=p.container_booking_id AND b.record_status='ACTIVE'
|
||||||
|
WHERE i.account_id=#{tenantId} AND i.deleted=0
|
||||||
|
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'
|
||||||
|
ORDER BY o.order_id
|
||||||
|
""")
|
||||||
|
List<com.baiyue.firstleg.dto.MerchantWorkbenchFilters.Fragment> progressFragments(@Param("tenantId") long tenantId);
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -10,10 +10,22 @@ public class MerchantWorkbenchFilterService {
|
|||||||
@Resource private MerchantWorkbenchFilterMapper mapper;
|
@Resource private MerchantWorkbenchFilterMapper mapper;
|
||||||
@Resource private UnifiedInboundQueryService unified;
|
@Resource private UnifiedInboundQueryService unified;
|
||||||
|
|
||||||
|
@org.springframework.transaction.annotation.Transactional(readOnly=true)
|
||||||
public MerchantWorkbenchFilters.Response read(MerchantWorkbenchFilters.Request request) {
|
public MerchantWorkbenchFilters.Response read(MerchantWorkbenchFilters.Request request) {
|
||||||
return new MerchantWorkbenchFilters.Response(
|
return new MerchantWorkbenchFilters.Response(
|
||||||
request.mainStatus() == null ? null : mapper.byProcessStatuses(request.tenantId(),
|
request.mainStatus() == null ? null : mapper.byProcessStatuses(request.tenantId(),
|
||||||
unified.internalStatuses(request.mainStatus())),
|
unified.internalStatuses(request.mainStatus())),
|
||||||
mapper.pendingFees(request.tenantId()), mapper.pendingCustoms(request.tenantId()));
|
mapper.pendingFees(request.tenantId()), mapper.pendingCustoms(request.tenantId()),
|
||||||
|
progress(mapper.progressFragments(request.tenantId())));
|
||||||
|
}
|
||||||
|
|
||||||
|
static java.util.List<MerchantWorkbenchFilters.Progress> progress(java.util.List<MerchantWorkbenchFilters.Fragment> facts) {
|
||||||
|
var grouped = facts.stream().collect(java.util.stream.Collectors.groupingBy(MerchantWorkbenchFilters.Fragment::orderId));
|
||||||
|
return grouped.entrySet().stream().sorted(java.util.Map.Entry.comparingByKey()).map(entry -> {
|
||||||
|
int max = entry.getValue().stream().mapToInt(MerchantWorkbenchFilters.Fragment::stage).max().orElse(0);
|
||||||
|
boolean partial = entry.getValue().stream().anyMatch(fragment -> fragment.stage() < max);
|
||||||
|
return new MerchantWorkbenchFilters.Progress(entry.getKey(),
|
||||||
|
switch(max) { case 1 -> "loaded"; case 2 -> "declared"; case 3 -> "cleared"; default -> "none"; }, partial);
|
||||||
|
}).filter(fact -> !"none".equals(fact.stage())).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -378,11 +378,22 @@ paths:
|
|||||||
code: {type: string}
|
code: {type: string}
|
||||||
data:
|
data:
|
||||||
type: object
|
type: object
|
||||||
required: [feeOrderIds, customsOrderIds]
|
required: [feeOrderIds, customsOrderIds, progress]
|
||||||
properties:
|
properties:
|
||||||
matchingStatusOrderIds: {type: array, nullable: true, items: {type: integer, format: int64}}
|
matchingStatusOrderIds: {type: array, nullable: true, items: {type: integer, format: int64}}
|
||||||
feeOrderIds: {type: array, items: {type: integer, format: int64}}
|
feeOrderIds: {type: array, items: {type: integer, format: int64}}
|
||||||
customsOrderIds: {type: array, items: {type: integer, format: int64}}
|
customsOrderIds: {type: array, items: {type: integer, format: int64}}
|
||||||
|
progress:
|
||||||
|
type: array
|
||||||
|
description: 实际装载片段投影;报关要求COMPLETED,目的港清关要求PICKUP_READY实际时间,缺失节点不推测。
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
required: [orderId, stage, partial]
|
||||||
|
properties:
|
||||||
|
orderId: {type: integer, format: int64}
|
||||||
|
stage: {type: string, enum: [loaded, declared, cleared]}
|
||||||
|
partial: {type: boolean}
|
||||||
|
|
||||||
/internal/firstleg/v1/inbounds/unified-orders/query:
|
/internal/firstleg/v1/inbounds/unified-orders/query:
|
||||||
post:
|
post:
|
||||||
summary: 按统一订单条件分页查询履约摘要(Order 内部)
|
summary: 按统一订单条件分页查询履约摘要(Order 内部)
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@ class InternalInboundOrderReferenceControllerTest {
|
|||||||
assertThatThrownBy(() -> controller.workbenchFilters(null, request)).isInstanceOf(BizException.class);
|
assertThatThrownBy(() -> controller.workbenchFilters(null, request)).isInstanceOf(BizException.class);
|
||||||
assertThatThrownBy(() -> controller.workbenchFilters("wrong", request)).isInstanceOf(BizException.class);
|
assertThatThrownBy(() -> controller.workbenchFilters("wrong", request)).isInstanceOf(BizException.class);
|
||||||
org.mockito.Mockito.verifyNoInteractions(workbenchFilters);
|
org.mockito.Mockito.verifyNoInteractions(workbenchFilters);
|
||||||
var response = new com.baiyue.firstleg.dto.MerchantWorkbenchFilters.Response(List.of(1L), List.of(), List.of());
|
var response = new com.baiyue.firstleg.dto.MerchantWorkbenchFilters.Response(List.of(1L), List.of(), List.of(), List.of());
|
||||||
when(workbenchFilters.read(request)).thenReturn(response);
|
when(workbenchFilters.read(request)).thenReturn(response);
|
||||||
assertThat(controller.workbenchFilters("internal-token", request).data()).isEqualTo(response);
|
assertThat(controller.workbenchFilters("internal-token", request).data()).isEqualTo(response);
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-2
@@ -26,13 +26,17 @@ class MerchantWorkbenchFilterMapperTest {
|
|||||||
jdbc.execute("CREATE TABLE fl_customs_container(id BIGINT PRIMARY KEY,loading_plan_id BIGINT,status VARCHAR(40))");
|
jdbc.execute("CREATE TABLE fl_customs_container(id BIGINT PRIMARY KEY,loading_plan_id BIGINT,status VARCHAR(40))");
|
||||||
jdbc.execute("CREATE TABLE fl_customs_customer_document(id BIGINT PRIMARY KEY,customs_container_id BIGINT,tenant_id BIGINT,status VARCHAR(32))");
|
jdbc.execute("CREATE TABLE fl_customs_customer_document(id BIGINT PRIMARY KEY,customs_container_id BIGINT,tenant_id BIGINT,status VARCHAR(32))");
|
||||||
jdbc.execute("CREATE TABLE fl_customs_customer_document_inbound(document_id BIGINT,inbound_id BIGINT,tenant_id BIGINT)");
|
jdbc.execute("CREATE TABLE fl_customs_customer_document_inbound(document_id BIGINT,inbound_id BIGINT,tenant_id BIGINT)");
|
||||||
|
jdbc.execute("ALTER TABLE fl_loading_plan_item ADD id BIGINT GENERATED BY DEFAULT AS IDENTITY");
|
||||||
|
jdbc.execute("CREATE TABLE fl_loading_pick_task(id BIGINT,loading_plan_id BIGINT,task_status VARCHAR(32))");
|
||||||
|
jdbc.execute("CREATE TABLE fl_loading_pick_item(pick_task_id BIGINT,loading_plan_item_id BIGINT,item_status VARCHAR(32),picked_package_count INT)");
|
||||||
|
jdbc.execute("CREATE TABLE fl_carrier_schedule_node_fact(container_booking_id BIGINT,standard_node VARCHAR(64),actual_at TIMESTAMP)");
|
||||||
var config=new MybatisConfiguration();config.setMapUnderscoreToCamelCase(true);config.addMapper(MerchantWorkbenchFilterMapper.class);
|
var config=new MybatisConfiguration();config.setMapUnderscoreToCamelCase(true);config.addMapper(MerchantWorkbenchFilterMapper.class);
|
||||||
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(ds);factory.setConfiguration(config);
|
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(ds);factory.setConfiguration(config);
|
||||||
mapper=new SqlSessionTemplate(factory.getObject()).getMapper(MerchantWorkbenchFilterMapper.class);
|
mapper=new SqlSessionTemplate(factory.getObject()).getMapper(MerchantWorkbenchFilterMapper.class);
|
||||||
jdbc.update("INSERT INTO fl_inbound_order VALUES(1,11,101,0,'TAX_REFUND','LOADED'),(2,11,102,0,'TAX_REFUND','DELIVERED'),(3,22,103,0,'TAX_REFUND','LOADED'),(4,11,104,1,'TAX_REFUND','LOADED'),(5,11,105,0,'BUY_DECLARATION','LOADED')");
|
jdbc.update("INSERT INTO fl_inbound_order VALUES(1,11,101,0,'TAX_REFUND','LOADED'),(2,11,102,0,'TAX_REFUND','DELIVERED'),(3,22,103,0,'TAX_REFUND','LOADED'),(4,11,104,1,'TAX_REFUND','LOADED'),(5,11,105,0,'BUY_DECLARATION','LOADED')");
|
||||||
jdbc.update("INSERT INTO fl_container_booking VALUES(100,'ACTIVE')");
|
jdbc.update("INSERT INTO fl_container_booking VALUES(100,'ACTIVE')");
|
||||||
jdbc.update("INSERT INTO fl_loading_plan VALUES(100,100,'COMPLETED',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)");
|
jdbc.update("INSERT INTO fl_loading_plan VALUES(100,100,'COMPLETED',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)");
|
||||||
jdbc.update("INSERT INTO fl_loading_plan_item VALUES(100,1,11,0),(100,1,11,0),(100,2,11,0),(100,3,22,0),(100,4,11,0),(100,5,11,0)");
|
jdbc.update("INSERT INTO fl_loading_plan_item(loading_plan_id,inbound_id,account_id,deleted) VALUES(100,1,11,0),(100,1,11,0),(100,2,11,0),(100,3,22,0),(100,4,11,0),(100,5,11,0)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test void latestFeeVersionAndCustomerScopeDetermineConfirmationQueue() {
|
@Test void latestFeeVersionAndCustomerScopeDetermineConfirmationQueue() {
|
||||||
@@ -71,10 +75,27 @@ class MerchantWorkbenchFilterMapperTest {
|
|||||||
jdbc.update("INSERT INTO fl_customs_container VALUES(200,100,'WAITING_CUSTOMER_DOCUMENTS')");
|
jdbc.update("INSERT INTO fl_customs_container VALUES(200,100,'WAITING_CUSTOMER_DOCUMENTS')");
|
||||||
jdbc.update("INSERT INTO fl_container_booking VALUES(101,'ACTIVE')");
|
jdbc.update("INSERT INTO fl_container_booking VALUES(101,'ACTIVE')");
|
||||||
jdbc.update("INSERT INTO fl_loading_plan VALUES(101,101,'COMPLETED',TIMESTAMP '2099-01-01 00:00:00',CURRENT_TIMESTAMP)");
|
jdbc.update("INSERT INTO fl_loading_plan VALUES(101,101,'COMPLETED',TIMESTAMP '2099-01-01 00:00:00',CURRENT_TIMESTAMP)");
|
||||||
jdbc.update("INSERT INTO fl_loading_plan_item VALUES(101,1,11,0)");
|
jdbc.update("INSERT INTO fl_loading_plan_item(loading_plan_id,inbound_id,account_id,deleted) VALUES(101,1,11,0)");
|
||||||
jdbc.update("INSERT INTO fl_customs_container VALUES(201,101,'COMPLETED')");
|
jdbc.update("INSERT INTO fl_customs_container VALUES(201,101,'COMPLETED')");
|
||||||
assertThat(mapper.pendingCustoms(11)).containsExactly(102L);
|
assertThat(mapper.pendingCustoms(11)).containsExactly(102L);
|
||||||
jdbc.update("UPDATE fl_container_booking SET record_status='CANCELLED' WHERE id=100");
|
jdbc.update("UPDATE fl_container_booking SET record_status='CANCELLED' WHERE id=100");
|
||||||
assertThat(mapper.pendingCustoms(11)).isEmpty();
|
assertThat(mapper.pendingCustoms(11)).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test void onlyActualLoadedOwnedFragmentsAndActualClearanceAdvanceProgress() {
|
||||||
|
assertThat(mapper.progressFragments(11)).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);
|
||||||
|
jdbc.update("INSERT INTO fl_customs_container VALUES(200,100,'DECLARING')");
|
||||||
|
assertThat(mapper.progressFragments(11)).allMatch(f->f.stage()==1);
|
||||||
|
jdbc.update("UPDATE fl_customs_container SET status='COMPLETED'");
|
||||||
|
assertThat(mapper.progressFragments(11)).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);
|
||||||
|
jdbc.update("UPDATE fl_carrier_schedule_node_fact SET actual_at=CURRENT_TIMESTAMP");
|
||||||
|
assertThat(mapper.progressFragments(11)).allMatch(f->f.stage()==3);
|
||||||
|
jdbc.update("UPDATE fl_container_booking SET record_status='CANCELLED'");
|
||||||
|
assertThat(mapper.progressFragments(11)).allMatch(f->f.stage()==0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.baiyue.firstleg.service;
|
||||||
|
|
||||||
|
import com.baiyue.firstleg.dto.MerchantWorkbenchFilters.Fragment;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import java.util.List;
|
||||||
|
import static org.assertj.core.api.Assertions.*;
|
||||||
|
|
||||||
|
class MerchantWorkbenchProgressTest {
|
||||||
|
@Test void unstartedFragmentsPreventWholeOrderProgressAndLatestFactsWinWithoutInventingHistory() {
|
||||||
|
var facts=List.of(new Fragment(1,0),new Fragment(1,1),new Fragment(2,2),new Fragment(2,3),new Fragment(3,3),new Fragment(4,0));
|
||||||
|
var projected=MerchantWorkbenchFilterService.progress(facts);
|
||||||
|
assertThat(projected).hasSize(3);
|
||||||
|
assertThat(projected.get(0).stage()).isEqualTo("loaded");assertThat(projected.get(0).partial()).isTrue();
|
||||||
|
assertThat(projected.get(1).stage()).isEqualTo("cleared");assertThat(projected.get(1).partial()).isTrue();
|
||||||
|
assertThat(projected.get(2).partial()).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package com.baiyue.lastleg.controller;
|
||||||
|
|
||||||
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
|
import com.baiyue.lastleg.dto.CustomerOrderProgress;
|
||||||
|
import com.baiyue.lastleg.security.InternalServiceTokenGuard;
|
||||||
|
import com.baiyue.lastleg.service.CustomerOrderProgressService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/internal/lastleg/v1/customer-order-progress")
|
||||||
|
public class InternalCustomerOrderProgressController {
|
||||||
|
private final InternalServiceTokenGuard guard;
|
||||||
|
private final CustomerOrderProgressService service;
|
||||||
|
public InternalCustomerOrderProgressController(InternalServiceTokenGuard guard, CustomerOrderProgressService service) {
|
||||||
|
this.guard = guard; this.service = service;
|
||||||
|
}
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<CustomerOrderProgress.Response> read(
|
||||||
|
@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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.baiyue.lastleg.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Positive;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class CustomerOrderProgress {
|
||||||
|
private CustomerOrderProgress() {}
|
||||||
|
public record Request(@NotNull @Positive Long customerId) {}
|
||||||
|
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) {}
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
package com.baiyue.lastleg.mapper;
|
||||||
|
|
||||||
|
import com.baiyue.lastleg.dto.CustomerOrderProgress;
|
||||||
|
import org.apache.ibatis.annotations.*;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Batch read of every actual source, never the representative MIN(order_id) on a shared pallet. */
|
||||||
|
@Mapper
|
||||||
|
public interface CustomerOrderProgressMapper {
|
||||||
|
@Select("""
|
||||||
|
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
|
||||||
|
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
|
||||||
|
UNION ALL
|
||||||
|
SELECT b.inbound_order_id AS order_id,SUM(p.available_packages+COALESCE(pk.packages,0)) 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_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
|
||||||
|
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
|
||||||
|
WHERE a.pallet_id=i.plan_pallet_id AND line.order_id=b.inbound_order_id))
|
||||||
|
GROUP BY b.inbound_order_id
|
||||||
|
), departed AS (
|
||||||
|
SELECT a.order_id,SUM(a.packages) AS packages
|
||||||
|
FROM ll_dispatch_pda_pick_order a
|
||||||
|
JOIN ll_dispatch_pda_pick p ON p.id=a.pick_id AND p.customer_id=a.customer_id
|
||||||
|
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
|
||||||
|
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
|
||||||
|
JOIN ll_outbound_order outb ON outb.id=d.outbound_order_id AND outb.customer_id=d.customer_id
|
||||||
|
WHERE d.customer_id=a.customer_id AND d.dispatch_order_id=ds.id
|
||||||
|
AND outb.status='OUTBOUNDED' AND d.status IN ('WAIT_DELIVERY','DELIVERING')
|
||||||
|
AND d.delivery_group_key=CASE
|
||||||
|
WHEN i.destination_type='PRIVATE' AND NULLIF(TRIM(i.delivery_address),'') IS NOT NULL
|
||||||
|
THEN CONCAT('PRIVATE:',TRIM(i.delivery_address),':',TRIM(COALESCE(i.postal_code,'')))
|
||||||
|
WHEN COALESCE(NULLIF(TRIM(i.destination_warehouse_code),''),NULLIF(TRIM(i.fba_warehouse_code),'')) IS NOT NULL
|
||||||
|
THEN CONCAT('CODE:',UPPER(COALESCE(NULLIF(TRIM(i.destination_warehouse_code),''),NULLIF(TRIM(i.fba_warehouse_code),''))))
|
||||||
|
WHEN NULLIF(TRIM(i.destination_warehouse_id),'') IS NOT NULL THEN CONCAT('ID:',TRIM(i.destination_warehouse_id))
|
||||||
|
ELSE NULL END)
|
||||||
|
GROUP BY a.order_id
|
||||||
|
)
|
||||||
|
SELECT o.source_order_id AS order_id,COALESCE(o.package_count,0) AS packages,
|
||||||
|
COALESCE((SELECT SUM(s.packages) FROM scheduled s WHERE s.order_id=o.id),0) AS scheduled_packages,
|
||||||
|
COALESCE((SELECT SUM(d.packages) FROM departed d WHERE d.order_id=o.id),0) AS delivering_packages
|
||||||
|
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')
|
||||||
|
ORDER BY o.id
|
||||||
|
""")
|
||||||
|
List<CustomerOrderProgress.Source> sources(@Param("customerId") String customerId);
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package com.baiyue.lastleg.service;
|
||||||
|
|
||||||
|
import com.baiyue.lastleg.dto.CustomerOrderProgress;
|
||||||
|
import com.baiyue.lastleg.mapper.CustomerOrderProgressMapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
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))));
|
||||||
|
}
|
||||||
|
static List<CustomerOrderProgress.Progress> project(List<CustomerOrderProgress.Source> sources) {
|
||||||
|
return sources.stream().collect(Collectors.groupingBy(CustomerOrderProgress.Source::orderId))
|
||||||
|
.entrySet().stream().map(entry -> {
|
||||||
|
long total = entry.getValue().stream().mapToLong(CustomerOrderProgress.Source::packages).sum();
|
||||||
|
long delivering = entry.getValue().stream().mapToLong(CustomerOrderProgress.Source::deliveringPackages).sum();
|
||||||
|
long scheduled = entry.getValue().stream().mapToLong(CustomerOrderProgress.Source::scheduledPackages).sum();
|
||||||
|
String stage = delivering > 0 ? "delivering" : scheduled > 0 ? "scheduled" : "none";
|
||||||
|
long covered = delivering > 0 ? delivering : scheduled;
|
||||||
|
// Equality is deliberate: duplicate or incomplete attribution must not claim full coverage.
|
||||||
|
boolean partial = total <= 0 || covered != total || entry.getValue().stream().anyMatch(s -> s.packages() <= 0);
|
||||||
|
return new CustomerOrderProgress.Progress(Long.parseLong(entry.getKey()), stage, partial);
|
||||||
|
}).filter(p -> !"none".equals(p.stage())).sorted(java.util.Comparator.comparingLong(CustomerOrderProgress.Progress::orderId)).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: 客户来源头程订单尾程进度内部查询
|
||||||
|
version: 1.0.0
|
||||||
|
paths:
|
||||||
|
/internal/lastleg/v1/customer-order-progress:
|
||||||
|
post:
|
||||||
|
summary: Order按认证客户读取排车和实际派送进度
|
||||||
|
description: >-
|
||||||
|
只读内部接口,无公网路由。customerId由Order认证上下文提供,所有业务表在Lastleg自身Schema内按客户读取。
|
||||||
|
正式排车单PENDING_DEPARTURE/PICKING/READY_TO_DEPART按计划或库存来源计算,预排草稿和取消单不计入。
|
||||||
|
派送必须已实际DEPARTED、出库OUTBOUNDED且对应目的地派送单仍WAIT_DELIVERY/DELIVERING。
|
||||||
|
混托使用所有库存及不可变拣货来源,不使用代表orderId;数量覆盖不完整或异常时标记partial,不签发派送完成事实。
|
||||||
|
security: [{internalServiceToken: []}]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [customerId]
|
||||||
|
properties:
|
||||||
|
customerId: {type: integer, format: int64, minimum: 1}
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ApiResponse,只有code为200表示成功;progress为空表示客户当前无有效排车或派送
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
code: {type: string}
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
|
required: [progress]
|
||||||
|
properties:
|
||||||
|
progress:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
required: [orderId, stage, partial]
|
||||||
|
properties:
|
||||||
|
orderId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||||
|
stage: {type: string, enum: [scheduled, delivering]}
|
||||||
|
partial: {type: boolean}
|
||||||
|
'400': {description: 客户参数无效}
|
||||||
|
'401': {description: 内部服务令牌缺失或无效}
|
||||||
|
'503': {description: 内部令牌未配置}
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
internalServiceToken:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: X-Internal-Service-Token
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package com.baiyue.lastleg.controller;
|
||||||
|
|
||||||
|
import com.baiyue.lastleg.dto.CustomerOrderProgress;
|
||||||
|
import com.baiyue.lastleg.security.InternalServiceTokenGuard;
|
||||||
|
import com.baiyue.lastleg.service.CustomerOrderProgressService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.assertj.core.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
package com.baiyue.lastleg.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||||
|
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||||
|
import org.h2.jdbcx.JdbcDataSource;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mybatis.spring.SqlSessionTemplate;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import java.util.UUID;
|
||||||
|
import static org.assertj.core.api.Assertions.*;
|
||||||
|
|
||||||
|
class CustomerOrderProgressMapperTest {
|
||||||
|
private JdbcTemplate jdbc;
|
||||||
|
private CustomerOrderProgressMapper mapper;
|
||||||
|
@BeforeEach void setup() throws Exception {
|
||||||
|
var ds=new JdbcDataSource();ds.setURL("jdbc:h2:mem:tail-progress-"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1");
|
||||||
|
jdbc=new JdbcTemplate(ds);
|
||||||
|
jdbc.execute("CREATE TABLE ll_order(id BIGINT,customer_id VARCHAR,source_order_id VARCHAR,source_inbound_id VARCHAR,source_region VARCHAR,order_type VARCHAR,package_count INT)");
|
||||||
|
jdbc.execute("CREATE TABLE ll_dispatch_order(id BIGINT,customer_id VARCHAR,status VARCHAR,depart_at TIMESTAMP)");
|
||||||
|
jdbc.execute("CREATE TABLE ll_dispatch_item(id BIGINT,dispatch_order_id BIGINT,customer_id VARCHAR,plan_pallet_id BIGINT,warehouse_pallet_id BIGINT,overseas_warehouse_id VARCHAR,destination_type VARCHAR,delivery_address VARCHAR,postal_code VARCHAR,destination_warehouse_code VARCHAR,fba_warehouse_code VARCHAR,destination_warehouse_id VARCHAR)");
|
||||||
|
jdbc.execute("CREATE TABLE ll_inbound_plan_pallet_allocation(pallet_id BIGINT,line_id BIGINT,planned_package_count INT)");
|
||||||
|
jdbc.execute("CREATE TABLE ll_inbound_plan_line(id BIGINT,customer_id VARCHAR,order_id BIGINT)");
|
||||||
|
jdbc.execute("CREATE TABLE ll_inventory_position(id BIGINT,customer_id VARCHAR,pallet_id BIGINT,overseas_warehouse_id VARCHAR,balance_id BIGINT,available_packages INT)");
|
||||||
|
jdbc.execute("CREATE TABLE ll_inventory_balance(id BIGINT,customer_id VARCHAR,inbound_order_id BIGINT)");
|
||||||
|
jdbc.execute("CREATE TABLE ll_dispatch_pda_pick(id BIGINT,customer_id VARCHAR,dispatch_item_id BIGINT,inventory_position_id BIGINT,packages INT,task_id BIGINT)");
|
||||||
|
jdbc.execute("CREATE TABLE ll_dispatch_pda_pick_order(id BIGINT,pick_id BIGINT,customer_id VARCHAR,order_id BIGINT,packages INT)");
|
||||||
|
jdbc.execute("CREATE TABLE ll_dispatch_pda_task(id BIGINT,customer_id VARCHAR,dispatch_order_id BIGINT)");
|
||||||
|
jdbc.execute("CREATE TABLE ll_delivery_order(id BIGINT,customer_id VARCHAR,dispatch_order_id BIGINT,outbound_order_id BIGINT,status VARCHAR,delivery_group_key VARCHAR)");
|
||||||
|
jdbc.execute("CREATE TABLE ll_outbound_order(id BIGINT,customer_id VARCHAR,status VARCHAR)");
|
||||||
|
jdbc.update("INSERT INTO ll_order VALUES(1,'11','101','201','FIRSTLEG','FBA',10),(2,'11','102','202','FIRSTLEG','FBA',20),(3,'22','103','203','FIRSTLEG','FBA',30)");
|
||||||
|
jdbc.update("INSERT INTO ll_dispatch_order VALUES(10,'11','PENDING_DEPARTURE',NULL)");
|
||||||
|
jdbc.update("INSERT INTO ll_dispatch_item(id,dispatch_order_id,customer_id,plan_pallet_id,overseas_warehouse_id,destination_warehouse_code) VALUES(100,10,'11',500,'W1','FBA1')");
|
||||||
|
jdbc.update("INSERT INTO ll_inbound_plan_line VALUES(1000,'11',1),(1001,'11',2),(1002,'22',3)");
|
||||||
|
jdbc.update("INSERT INTO ll_inbound_plan_pallet_allocation VALUES(500,1000,10),(500,1001,20),(500,1002,30)");
|
||||||
|
var config=new MybatisConfiguration();config.setMapUnderscoreToCamelCase(true);config.addMapper(CustomerOrderProgressMapper.class);
|
||||||
|
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(ds);factory.setConfiguration(config);
|
||||||
|
mapper=new SqlSessionTemplate(factory.getObject()).getMapper(CustomerOrderProgressMapper.class);
|
||||||
|
}
|
||||||
|
@Test void plannedSharedPalletIncludesEveryOwnSourceAndExcludesCancellation() {
|
||||||
|
var sources=mapper.sources("11");assertThat(sources).hasSize(2);
|
||||||
|
assertThat(sources.get(0).scheduledPackages()).isEqualTo(10);
|
||||||
|
assertThat(sources.get(1).scheduledPackages()).isEqualTo(20);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
@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);
|
||||||
|
assertThat(sources.get(1).scheduledPackages()).isEqualTo(20);
|
||||||
|
}
|
||||||
|
@Test void deliveryRequiresActualDepartureOutboundAndMatchingDestinationAttribution() {
|
||||||
|
jdbc.update("INSERT INTO ll_dispatch_pda_task VALUES(700,'11',10)");
|
||||||
|
jdbc.update("INSERT INTO ll_dispatch_pda_pick VALUES(600,'11',100,400,4,700)");
|
||||||
|
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);
|
||||||
|
jdbc.update("UPDATE ll_dispatch_order SET status='DEPARTED',depart_at=CURRENT_TIMESTAMP");
|
||||||
|
assertThat(mapper.sources("11")).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();
|
||||||
|
jdbc.update("UPDATE ll_delivery_order SET delivery_group_key='CODE:FBA2'");
|
||||||
|
assertThat(mapper.sources("11")).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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package com.baiyue.lastleg.service;
|
||||||
|
|
||||||
|
import com.baiyue.lastleg.dto.CustomerOrderProgress.Source;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import java.util.List;
|
||||||
|
import static org.assertj.core.api.Assertions.*;
|
||||||
|
|
||||||
|
class CustomerOrderProgressServiceTest {
|
||||||
|
@Test void aggregatesAllInboundFragmentsAndNeverTreatsDuplicateOrUnknownCoverageAsComplete() {
|
||||||
|
var result=CustomerOrderProgressService.project(List.of(
|
||||||
|
new Source("1",10,10,0),new Source("1",10,0,0),
|
||||||
|
new Source("2",10,0,10),new Source("3",10,0,20),new Source("4",0,5,0),new Source("5",10,0,0)));
|
||||||
|
assertThat(result).hasSize(4);
|
||||||
|
assertThat(result.get(0).stage()).isEqualTo("scheduled");assertThat(result.get(0).partial()).isTrue();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ public interface FirstlegUnifiedOrderClient {
|
|||||||
|
|
||||||
record WorkbenchFilterRequest(long tenantId, String mainStatus) {}
|
record WorkbenchFilterRequest(long tenantId, String mainStatus) {}
|
||||||
record WorkbenchFilters(List<Long> matchingStatusOrderIds, List<Long> feeOrderIds,
|
record WorkbenchFilters(List<Long> matchingStatusOrderIds, List<Long> feeOrderIds,
|
||||||
List<Long> customsOrderIds) {}
|
List<Long> customsOrderIds, List<com.baiyue.order.dto.MerchantOrderProgress> progress) {}
|
||||||
|
|
||||||
@PostMapping("/query")
|
@PostMapping("/query")
|
||||||
ApiResponse<PageResult<Summary>> page(@RequestHeader("X-Internal-Service-Token") String token,
|
ApiResponse<PageResult<Summary>> page(@RequestHeader("X-Internal-Service-Token") String token,
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.baiyue.order.client;
|
||||||
|
|
||||||
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
|
import com.baiyue.order.dto.MerchantOrderProgress;
|
||||||
|
import org.springframework.cloud.openfeign.FeignClient;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@FeignClient(name="lastleg-service", contextId="orderLastlegProgressClient",
|
||||||
|
url="${baiyue.clients.lastleg.url:}", path="/internal/lastleg/v1/customer-order-progress")
|
||||||
|
public interface LastlegOrderProgressClient {
|
||||||
|
@PostMapping
|
||||||
|
ApiResponse<Response> read(@RequestHeader("X-Internal-Service-Token") String token, @RequestBody Request request);
|
||||||
|
record Request(long customerId) {}
|
||||||
|
record Response(List<MerchantOrderProgress> progress) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.baiyue.order.dto;
|
||||||
|
|
||||||
|
/** Customer progress is a read projection, not another writable order state machine. */
|
||||||
|
public record MerchantOrderProgress(long orderId, String stage, boolean partial) {
|
||||||
|
public static final java.util.List<String> STAGES = java.util.List.of("none", "loaded", "declared", "cleared", "scheduled", "delivering", "delivered");
|
||||||
|
public int rank() {
|
||||||
|
int rank = STAGES.indexOf(stage);
|
||||||
|
if (rank < 0 || orderId <= 0) throw new IllegalArgumentException("Invalid order progress");
|
||||||
|
return rank;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ public record MerchantOrderWorkbenchQuery(
|
|||||||
@Size(max=128) String trackingNumber,
|
@Size(max=128) String trackingNumber,
|
||||||
@Pattern(regexp="DRAFT|PENDING_REVIEW|WAIT_MEASURE|WAIT_PALLETIZE|WAIT_SPLIT|LOADING|LOADED|DELIVERED|COMPLETED|CANCELLED") String mainStatus,
|
@Pattern(regexp="DRAFT|PENDING_REVIEW|WAIT_MEASURE|WAIT_PALLETIZE|WAIT_SPLIT|LOADING|LOADED|DELIVERED|COMPLETED|CANCELLED") String mainStatus,
|
||||||
LocalDateTime createdFrom, LocalDateTime createdTo,
|
LocalDateTime createdFrom, LocalDateTime createdTo,
|
||||||
@Pattern(regexp="all|fee|customs|draft") String queue,
|
@Pattern(regexp="all|fee|customs|draft|loaded|declared|cleared|scheduled|delivering|delivered") String queue,
|
||||||
@Min(1) long page, @Min(1) @Max(200) long size) {
|
@Min(1) long page, @Min(1) @Max(200) long size) {
|
||||||
public MerchantOrderWorkbenchQuery {
|
public MerchantOrderWorkbenchQuery {
|
||||||
if (page==0) page=1;
|
if (page==0) page=1;
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ import com.baiyue.common.core.api.PageResult;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public record MerchantOrderWorkbenchResponse(PageResult<UnifiedOrderResponse.Row> page,
|
public record MerchantOrderWorkbenchResponse(PageResult<UnifiedOrderResponse.Row> page,
|
||||||
Counts queues, List<String> channelNames) {
|
Counts queues, List<String> channelNames, List<MerchantOrderProgress> progress) {
|
||||||
public record Counts(long all, long fee, long customs, long draft) {}
|
public record Counts(long all, long fee, long customs, long draft, long loaded, long declared,
|
||||||
|
long cleared, long scheduled, long delivering, long delivered) {
|
||||||
|
public long forQueue(String queue) {
|
||||||
|
return switch(queue) { case "fee" -> fee; case "customs" -> customs; case "draft" -> draft;
|
||||||
|
case "loaded" -> loaded; case "declared" -> declared; case "cleared" -> cleared;
|
||||||
|
case "scheduled" -> scheduled; case "delivering" -> delivering; case "delivered" -> delivered;
|
||||||
|
case "all" -> all; default -> throw new IllegalArgumentException("Invalid queue"); };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-3
@@ -21,14 +21,18 @@ public class MerchantOrderWorkbenchRepository {
|
|||||||
|
|
||||||
@Transactional(readOnly=true)
|
@Transactional(readOnly=true)
|
||||||
public Result read(long customerId, MerchantOrderWorkbenchQuery q, List<Long> statusIds,
|
public Result read(long customerId, MerchantOrderWorkbenchQuery q, List<Long> statusIds,
|
||||||
List<Long> feeIds, List<Long> customsIds) {
|
List<Long> feeIds, List<Long> customsIds, java.util.Map<String,List<Long>> progressIds) {
|
||||||
var base=base(customerId,q,statusIds);
|
var base=base(customerId,q,statusIds);
|
||||||
long all=mapper.selectCount(base.clone());
|
long all=mapper.selectCount(base.clone());
|
||||||
long fee=mapper.selectCount(ids(base.clone(),feeIds));
|
long fee=mapper.selectCount(ids(base.clone(),feeIds));
|
||||||
long customs=mapper.selectCount(ids(base.clone(),customsIds));
|
long customs=mapper.selectCount(ids(base.clone(),customsIds));
|
||||||
long draft=mapper.selectCount(base.clone().eq("status","DRAFT"));
|
long draft=mapper.selectCount(base.clone().eq("status","DRAFT"));
|
||||||
var counts=new Counts(all,fee,customs,draft);
|
var counts=new Counts(all,fee,customs,draft,
|
||||||
long total=switch(q.queue()) {case "fee"->fee;case "customs"->customs;case "draft"->draft;default->all;};
|
progressCount(base,progressIds,"loaded"), progressCount(base,progressIds,"declared"),
|
||||||
|
progressCount(base,progressIds,"cleared"), progressCount(base,progressIds,"scheduled"),
|
||||||
|
progressCount(base,progressIds,"delivering"), progressCount(base,progressIds,"delivered"));
|
||||||
|
long total=counts.forQueue(q.queue());
|
||||||
|
if (progressIds.containsKey(q.queue())) ids(base,progressIds.get(q.queue()));
|
||||||
if ("fee".equals(q.queue())) ids(base,feeIds);
|
if ("fee".equals(q.queue())) ids(base,feeIds);
|
||||||
if ("customs".equals(q.queue())) ids(base,customsIds);
|
if ("customs".equals(q.queue())) ids(base,customsIds);
|
||||||
if ("draft".equals(q.queue())) base.eq("status","DRAFT");
|
if ("draft".equals(q.queue())) base.eq("status","DRAFT");
|
||||||
@@ -43,6 +47,15 @@ public class MerchantOrderWorkbenchRepository {
|
|||||||
total/q.size()+(total%q.size()==0?0:1)),counts,channels);
|
total/q.size()+(total%q.size()==0?0:1)),counts,channels);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<OrderEntity> progressScope(long customerId) {
|
||||||
|
return mapper.selectList(new QueryWrapper<OrderEntity>().select("id","root_order_id","status")
|
||||||
|
.eq("tenant_id",customerId).ne("status","CANCELLED"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private long progressCount(QueryWrapper<OrderEntity> base,java.util.Map<String,List<Long>> progressIds,String stage) {
|
||||||
|
return mapper.selectCount(ids(base.clone(),progressIds.get(stage)));
|
||||||
|
}
|
||||||
|
|
||||||
private QueryWrapper<OrderEntity> base(long customerId, MerchantOrderWorkbenchQuery q, List<Long> statusIds) {
|
private QueryWrapper<OrderEntity> base(long customerId, MerchantOrderWorkbenchQuery q, List<Long> statusIds) {
|
||||||
var w=new QueryWrapper<OrderEntity>().eq("tenant_id",customerId);
|
var w=new QueryWrapper<OrderEntity>().eq("tenant_id",customerId);
|
||||||
String direct=switch(q.mainStatus()==null?"":q.mainStatus()) {
|
String direct=switch(q.mainStatus()==null?"":q.mainStatus()) {
|
||||||
|
|||||||
@@ -4,40 +4,109 @@ import com.baiyue.common.core.api.PageResult;
|
|||||||
import com.baiyue.common.core.api.ResultCode;
|
import com.baiyue.common.core.api.ResultCode;
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
import com.baiyue.order.client.FirstlegUnifiedOrderClient;
|
import com.baiyue.order.client.FirstlegUnifiedOrderClient;
|
||||||
|
import com.baiyue.order.client.LastlegOrderProgressClient;
|
||||||
|
import com.baiyue.order.dto.MerchantOrderProgress;
|
||||||
import com.baiyue.order.dto.MerchantOrderWorkbenchQuery;
|
import com.baiyue.order.dto.MerchantOrderWorkbenchQuery;
|
||||||
import com.baiyue.order.dto.MerchantOrderWorkbenchResponse;
|
import com.baiyue.order.dto.MerchantOrderWorkbenchResponse;
|
||||||
|
import com.baiyue.order.entity.OrderEntity;
|
||||||
|
import com.baiyue.order.entity.OrderStatus;
|
||||||
import com.baiyue.order.repository.MerchantOrderWorkbenchRepository;
|
import com.baiyue.order.repository.MerchantOrderWorkbenchRepository;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import java.util.Set;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class MerchantOrderWorkbenchService {
|
public class MerchantOrderWorkbenchService {
|
||||||
@Resource private MerchantOrderWorkbenchRepository repository;
|
@Resource private MerchantOrderWorkbenchRepository repository;
|
||||||
@Resource private FirstlegUnifiedOrderClient firstleg;
|
@Resource private FirstlegUnifiedOrderClient firstleg;
|
||||||
@Resource private UnifiedOrderQueryService unified;
|
@Resource private UnifiedOrderQueryService unified;
|
||||||
|
@Resource private LastlegOrderProgressClient lastleg;
|
||||||
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
||||||
private static final Set<String> DIRECT=Set.of("DRAFT","PENDING_REVIEW","COMPLETED","CANCELLED");
|
private static final Set<String> DIRECT=Set.of("DRAFT","PENDING_REVIEW","COMPLETED","CANCELLED");
|
||||||
|
|
||||||
|
@Transactional(readOnly=true)
|
||||||
public MerchantOrderWorkbenchResponse page(long customerId,MerchantOrderWorkbenchQuery query) {
|
public MerchantOrderWorkbenchResponse page(long customerId,MerchantOrderWorkbenchQuery query) {
|
||||||
if(query.createdFrom()!=null&&query.createdTo()!=null&&query.createdFrom().isAfter(query.createdTo()))
|
if(query.createdFrom()!=null&&query.createdTo()!=null&&query.createdFrom().isAfter(query.createdTo()))
|
||||||
throw new BizException(ResultCode.BAD_REQUEST,"创建时间起止范围不合法");
|
throw new BizException(ResultCode.BAD_REQUEST,"创建时间起止范围不合法");
|
||||||
String status=query.mainStatus()==null||DIRECT.contains(query.mainStatus())?null:query.mainStatus();
|
String status=query.mainStatus()==null||DIRECT.contains(query.mainStatus())?null:query.mainStatus();
|
||||||
FirstlegUnifiedOrderClient.WorkbenchFilters filters;
|
FirstlegUnifiedOrderClient.WorkbenchFilters filters;
|
||||||
|
List<MerchantOrderProgress> tailProgress;
|
||||||
try {
|
try {
|
||||||
var response=firstleg.workbenchFilters(internalToken,
|
var response=firstleg.workbenchFilters(internalToken,
|
||||||
new FirstlegUnifiedOrderClient.WorkbenchFilterRequest(customerId,status));
|
new FirstlegUnifiedOrderClient.WorkbenchFilterRequest(customerId,status));
|
||||||
if(response==null||!response.successful()||response.data()==null) throw new IllegalStateException();
|
if(response==null||!response.successful()||response.data()==null) throw new IllegalStateException();
|
||||||
filters=response.data();
|
filters=response.data();
|
||||||
if(filters.feeOrderIds()==null||filters.customsOrderIds()==null
|
if(filters.feeOrderIds()==null||filters.customsOrderIds()==null||filters.progress()==null
|
||||||
||status!=null&&filters.matchingStatusOrderIds()==null) throw new IllegalStateException();
|
||status!=null&&filters.matchingStatusOrderIds()==null) throw new IllegalStateException();
|
||||||
|
var tail=lastleg.read(internalToken,new LastlegOrderProgressClient.Request(customerId));
|
||||||
|
if(tail==null||!tail.successful()||tail.data()==null||tail.data().progress()==null) throw new IllegalStateException();
|
||||||
|
tailProgress=tail.data().progress();
|
||||||
|
validateFacts(filters.progress(),1,3);
|
||||||
|
validateFacts(tailProgress,4,5);
|
||||||
} catch(RuntimeException failure) {
|
} catch(RuntimeException failure) {
|
||||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE,"订单待办查询暂时不可用,请重试");
|
throw new BizException(ResultCode.SERVICE_UNAVAILABLE,"订单待办查询暂时不可用,请重试");
|
||||||
}
|
}
|
||||||
var result=repository.read(customerId,query,filters.matchingStatusOrderIds(),filters.feeOrderIds(),filters.customsOrderIds());
|
var progress=project(repository.progressScope(customerId),filters.progress(),tailProgress);
|
||||||
|
var progressIds=new LinkedHashMap<String,List<Long>>();
|
||||||
|
for(String stage:MerchantOrderProgress.STAGES.subList(1,7))
|
||||||
|
progressIds.put(stage,progress.values().stream().filter(p->stage.equals(p.stage())).map(MerchantOrderProgress::orderId).toList());
|
||||||
|
var result=repository.read(customerId,query,filters.matchingStatusOrderIds(),filters.feeOrderIds(),filters.customsOrderIds(),progressIds);
|
||||||
var source=result.page();
|
var source=result.page();
|
||||||
return new MerchantOrderWorkbenchResponse(new PageResult<>(unified.merchantWorkbenchRows(customerId,source.records()),
|
return new MerchantOrderWorkbenchResponse(new PageResult<>(unified.merchantWorkbenchRows(customerId,source.records()),
|
||||||
source.page(),source.size(),source.total(),source.pages()),result.queues(),result.channelNames());
|
source.page(),source.size(),source.total(),source.pages()),result.queues(),result.channelNames(),
|
||||||
|
source.records().stream().map(o->progress.getOrDefault(o.getId(),new MerchantOrderProgress(o.getId(),"none",false))).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateFacts(List<MerchantOrderProgress> facts, int min, int max) {
|
||||||
|
var seen=new java.util.HashSet<Long>();
|
||||||
|
for(var fact:facts) if(fact==null||fact.rank()<min||fact.rank()>max||!seen.add(fact.orderId()))
|
||||||
|
throw new IllegalStateException("Invalid progress response");
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<Long,MerchantOrderProgress> project(
|
||||||
|
List<OrderEntity> scope,
|
||||||
|
List<MerchantOrderProgress> firstleg,
|
||||||
|
List<MerchantOrderProgress> lastleg) {
|
||||||
|
var scopeById=scope.stream().collect(Collectors.toMap(OrderEntity::getId,o->o));
|
||||||
|
var facts=new HashMap<Long,MerchantOrderProgress>();
|
||||||
|
Stream.concat(firstleg.stream(),lastleg.stream()).forEach(p->
|
||||||
|
facts.merge(p.orderId(),p,(a,b)->a.rank()>b.rank()?a:b));
|
||||||
|
var result=new LinkedHashMap<Long,MerchantOrderProgress>();
|
||||||
|
for(var order:scope) {
|
||||||
|
var status=order.getStatus();
|
||||||
|
var fact=status==OrderStatus.COMPLETED
|
||||||
|
? new MerchantOrderProgress(order.getId(),"delivered",false)
|
||||||
|
: status==OrderStatus.DRAFT||status==OrderStatus.SUBMITTED
|
||||||
|
? new MerchantOrderProgress(order.getId(),"none",false)
|
||||||
|
: facts.getOrDefault(order.getId(),new MerchantOrderProgress(order.getId(),"none",false));
|
||||||
|
result.put(order.getId(),fact);
|
||||||
|
}
|
||||||
|
var children=scope.stream().filter(o->o.getRootOrderId()!=null && !o.getRootOrderId().equals(o.getId()))
|
||||||
|
.collect(Collectors.groupingBy(OrderEntity::getRootOrderId));
|
||||||
|
children.forEach((root,orders)->{
|
||||||
|
var own=result.get(root);
|
||||||
|
if(own==null||"delivered".equals(own.stage())) return;
|
||||||
|
var rootOrder=scopeById.get(root);
|
||||||
|
if(rootOrder.getStatus()==OrderStatus.DRAFT||rootOrder.getStatus()==OrderStatus.SUBMITTED) return;
|
||||||
|
var fragments=new ArrayList<MerchantOrderProgress>();
|
||||||
|
fragments.add(own); orders.forEach(o->fragments.add(result.get(o.getId())));
|
||||||
|
// Completion remains exclusively owned by Order's trusted root completion gate.
|
||||||
|
int max=fragments.stream().mapToInt(MerchantOrderProgress::rank).max().orElse(0);
|
||||||
|
if(max==6) max=5;
|
||||||
|
final int rank=max;
|
||||||
|
boolean partial=fragments.stream().anyMatch(p->p.rank()<rank||p.partial());
|
||||||
|
result.put(root,new MerchantOrderProgress(root,
|
||||||
|
MerchantOrderProgress.STAGES.get(rank),partial));
|
||||||
|
});
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ paths:
|
|||||||
summary: 客户订单工作台分页和筛选后待办数量
|
summary: 客户订单工作台分页和筛选后待办数量
|
||||||
description: >-
|
description: >-
|
||||||
仅按登录客户查询,权限merchant:order:list。草稿优先,再按创建时间和订单ID降序稳定分页。
|
仅按登录客户查询,权限merchant:order:list。草稿优先,再按创建时间和订单ID降序稳定分页。
|
||||||
入仓码/SKU采用或匹配;所有条件先筛选再计数。四类数量共用当前筛选,不从当前页行数计算。
|
入仓码/SKU采用或匹配;所有条件先筛选再计数。十类数量共用当前筛选,不从当前页行数计算。
|
||||||
默认包含草稿和有效正式单;履约子状态以Firstleg为准,待审核和已完成以Order为准。
|
默认包含草稿和有效正式单;履约子状态以Firstleg为准,待审核和已完成以Order为准。
|
||||||
security: [{bearerAuth: []}]
|
security: [{bearerAuth: []}]
|
||||||
parameters:
|
parameters:
|
||||||
@@ -23,7 +23,7 @@ paths:
|
|||||||
- {name: mainStatus, in: query, schema: {type: string, enum: [DRAFT, PENDING_REVIEW, WAIT_MEASURE, WAIT_PALLETIZE, WAIT_SPLIT, LOADING, LOADED, DELIVERED, COMPLETED, CANCELLED]}}
|
- {name: mainStatus, in: query, schema: {type: string, enum: [DRAFT, PENDING_REVIEW, WAIT_MEASURE, WAIT_PALLETIZE, WAIT_SPLIT, LOADING, LOADED, DELIVERED, COMPLETED, CANCELLED]}}
|
||||||
- {name: createdFrom, in: query, schema: {type: string, format: date-time}}
|
- {name: createdFrom, in: query, schema: {type: string, format: date-time}}
|
||||||
- {name: createdTo, in: query, schema: {type: string, format: date-time}}
|
- {name: createdTo, in: query, schema: {type: string, format: date-time}}
|
||||||
- {name: queue, in: query, schema: {type: string, enum: [all, fee, customs, draft], default: all}}
|
- {name: queue, in: query, schema: {type: string, enum: [all, fee, customs, draft, loaded, declared, cleared, scheduled, delivering, delivered], default: all}}
|
||||||
- {name: page, in: query, schema: {type: integer, minimum: 1, default: 1}}
|
- {name: page, in: query, schema: {type: integer, minimum: 1, default: 1}}
|
||||||
- {name: size, in: query, schema: {type: integer, minimum: 1, maximum: 200, default: 10}}
|
- {name: size, in: query, schema: {type: integer, minimum: 1, maximum: 200, default: 10}}
|
||||||
responses:
|
responses:
|
||||||
@@ -37,18 +37,35 @@ paths:
|
|||||||
code: {type: string}
|
code: {type: string}
|
||||||
data:
|
data:
|
||||||
type: object
|
type: object
|
||||||
required: [page, queues, channelNames]
|
required: [page, queues, channelNames, progress]
|
||||||
properties:
|
properties:
|
||||||
page: {$ref: '#/components/schemas/UnifiedOrderPage'}
|
page: {$ref: '#/components/schemas/UnifiedOrderPage'}
|
||||||
queues:
|
queues:
|
||||||
type: object
|
type: object
|
||||||
required: [all, fee, customs, draft]
|
required: [all, fee, customs, draft, loaded, declared, cleared, scheduled, delivering, delivered]
|
||||||
properties:
|
properties:
|
||||||
all: {type: integer, format: int64, minimum: 0}
|
all: {type: integer, format: int64, minimum: 0}
|
||||||
fee: {type: integer, format: int64, minimum: 0}
|
fee: {type: integer, format: int64, minimum: 0}
|
||||||
customs: {type: integer, format: int64, minimum: 0}
|
customs: {type: integer, format: int64, minimum: 0}
|
||||||
draft: {type: integer, format: int64, minimum: 0}
|
draft: {type: integer, format: int64, minimum: 0}
|
||||||
|
loaded: {type: integer, format: int64, minimum: 0}
|
||||||
|
declared: {type: integer, format: int64, minimum: 0}
|
||||||
|
cleared: {type: integer, format: int64, minimum: 0}
|
||||||
|
scheduled: {type: integer, format: int64, minimum: 0}
|
||||||
|
delivering: {type: integer, format: int64, minimum: 0}
|
||||||
|
delivered: {type: integer, format: int64, minimum: 0}
|
||||||
channelNames: {type: array, items: {type: string}, description: 当前客户完整渠道筛选项,不限于当前页}
|
channelNames: {type: array, items: {type: string}, description: 当前客户完整渠道筛选项,不限于当前页}
|
||||||
|
progress:
|
||||||
|
type: array
|
||||||
|
description: 与当前页订单一一对应的客户履约进度;分类互斥,待办可重叠。分片未齐时partial为true,完成只认Order可信回写。
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
required: [orderId, stage, partial]
|
||||||
|
properties:
|
||||||
|
orderId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||||
|
stage: {type: string, enum: [none, loaded, declared, cleared, scheduled, delivering, delivered]}
|
||||||
|
partial: {type: boolean}
|
||||||
|
|
||||||
/internal/order/v1/fulfillment-completions:
|
/internal/order/v1/fulfillment-completions:
|
||||||
post:
|
post:
|
||||||
security: []
|
security: []
|
||||||
|
|||||||
+30
-12
@@ -68,11 +68,16 @@ class MerchantOrderWorkbenchRepositoryTest {
|
|||||||
private MerchantOrderWorkbenchQuery query(String keyword,String queue,long page) {
|
private MerchantOrderWorkbenchQuery query(String keyword,String queue,long page) {
|
||||||
return new MerchantOrderWorkbenchQuery(keyword,null,null,null,null,null,null,null,queue,page,10);
|
return new MerchantOrderWorkbenchQuery(keyword,null,null,null,null,null,null,null,queue,page,10);
|
||||||
}
|
}
|
||||||
|
private java.util.Map<String,List<Long>> progressIds() {
|
||||||
|
var map=new java.util.LinkedHashMap<String,List<Long>>();
|
||||||
|
for(String key:List.of("loaded","declared","cleared","scheduled","delivering","delivered")) map.put(key,List.of());
|
||||||
|
return map;
|
||||||
|
}
|
||||||
@Test void readsBeyondTheOldTwoHundredLimitWithStableDraftPrefixAndCorrectTotals() {
|
@Test void readsBeyondTheOldTwoHundredLimitWithStableDraftPrefixAndCorrectTotals() {
|
||||||
var first=repository.read(11,query(null,"all",1),null,List.of(210L),List.of(211L));
|
var first=repository.read(11,query(null,"all",1),null,List.of(210L),List.of(211L), progressIds());
|
||||||
assertThat(first.page().total()).isEqualTo(212);
|
assertThat(first.page().total()).isEqualTo(212);
|
||||||
assertThat(first.page().records().subList(0,3)).extracting(OrderEntity::getId).containsExactly(3L,2L,1L);
|
assertThat(first.page().records().subList(0,3)).extracting(OrderEntity::getId).containsExactly(3L,2L,1L);
|
||||||
var last=repository.read(11,query(null,"all",22),null,List.of(210L),List.of(211L));
|
var last=repository.read(11,query(null,"all",22),null,List.of(210L),List.of(211L), progressIds());
|
||||||
assertThat(last.page().records()).extracting(OrderEntity::getId).containsExactly(5L,4L);
|
assertThat(last.page().records()).extracting(OrderEntity::getId).containsExactly(5L,4L);
|
||||||
assertThat(last.page().pages()).isEqualTo(22);
|
assertThat(last.page().pages()).isEqualTo(22);
|
||||||
assertThat(last.queues().all()).isEqualTo(212);
|
assertThat(last.queues().all()).isEqualTo(212);
|
||||||
@@ -80,31 +85,44 @@ class MerchantOrderWorkbenchRepositoryTest {
|
|||||||
assertThat(last.queues().fee()).isEqualTo(1);
|
assertThat(last.queues().fee()).isEqualTo(1);
|
||||||
assertThat(last.queues().customs()).isEqualTo(1);
|
assertThat(last.queues().customs()).isEqualTo(1);
|
||||||
assertThat(last.channelNames()).containsExactly("AIR","SEA");
|
assertThat(last.channelNames()).containsExactly("AIR","SEA");
|
||||||
assertThat(repository.read(11,query(null,"all",23),null,List.of(),List.of()).page().records()).isEmpty();
|
assertThat(repository.read(11,query(null,"all",23),null,List.of(),List.of(), progressIds()).page().records()).isEmpty();
|
||||||
}
|
}
|
||||||
@Test void filtersBeforeCountingAndPagingAndNeverUsesForeignSkuOrQueueIds() {
|
@Test void filtersBeforeCountingAndPagingAndNeverUsesForeignSkuOrQueueIds() {
|
||||||
var sku=repository.read(11,query("SPECIAL","all",1),null,List.of(210L,214L),List.of(215L));
|
var sku=repository.read(11,query("SPECIAL","all",1),null,List.of(210L,214L),List.of(215L), progressIds());
|
||||||
assertThat(sku.page().total()).isEqualTo(1);
|
assertThat(sku.page().total()).isEqualTo(1);
|
||||||
assertThat(sku.page().records()).extracting(OrderEntity::getId).containsExactly(210L);
|
assertThat(sku.page().records()).extracting(OrderEntity::getId).containsExactly(210L);
|
||||||
assertThat(sku.queues().fee()).isEqualTo(1);assertThat(sku.queues().customs()).isZero();
|
assertThat(sku.queues().fee()).isEqualTo(1);assertThat(sku.queues().customs()).isZero();
|
||||||
var fee=repository.read(11,query(null,"fee",1),null,List.of(210L,214L),List.of());
|
var fee=repository.read(11,query(null,"fee",1),null,List.of(210L,214L),List.of(), progressIds());
|
||||||
assertThat(fee.page().records()).extracting(OrderEntity::getId).containsExactly(210L);
|
assertThat(fee.page().records()).extracting(OrderEntity::getId).containsExactly(210L);
|
||||||
assertThat(repository.read(11,query(null,"fee",1),null,List.of(),List.of()).page().records()).isEmpty();
|
assertThat(repository.read(11,query(null,"fee",1),null,List.of(),List.of(), progressIds()).page().records()).isEmpty();
|
||||||
var drafts=repository.read(11,query(null,"draft",1),null,List.of(),List.of());
|
var drafts=repository.read(11,query(null,"draft",1),null,List.of(),List.of(), progressIds());
|
||||||
assertThat(drafts.page().total()).isEqualTo(3);
|
assertThat(drafts.page().total()).isEqualTo(3);
|
||||||
}
|
}
|
||||||
@Test void combinesWarehouseChannelDestinationTrackingDatesAndStatusBeforePaging() {
|
@Test void combinesWarehouseChannelDestinationTrackingDatesAndStatusBeforePaging() {
|
||||||
var from=java.time.LocalDateTime.of(2026,10,2,0,0);
|
var from=java.time.LocalDateTime.of(2026,10,2,0,0);
|
||||||
var to=from.plusDays(1).minusNanos(1);
|
var to=from.plusDays(1).minusNanos(1);
|
||||||
var q=new MerchantOrderWorkbenchQuery("IN-210",10L,"SEA","FBA","TRACK-210","LOADED",from,to,"all",1,10);
|
var q=new MerchantOrderWorkbenchQuery("IN-210",10L,"SEA","FBA","TRACK-210","LOADED",from,to,"all",1,10);
|
||||||
assertThat(repository.read(11,q,List.of(210L,214L),List.of(),List.of()).page().records())
|
assertThat(repository.read(11,q,List.of(210L,214L),List.of(),List.of(), progressIds()).page().records())
|
||||||
.extracting(OrderEntity::getId).containsExactly(210L);
|
.extracting(OrderEntity::getId).containsExactly(210L);
|
||||||
assertThat(repository.read(11,q,List.of(),List.of(),List.of()).page().total()).isZero();
|
assertThat(repository.read(11,q,List.of(),List.of(),List.of(), progressIds()).page().total()).isZero();
|
||||||
jdbc.update("UPDATE ord_order SET status='SUBMITTED' WHERE id=210");
|
jdbc.update("UPDATE ord_order SET status='SUBMITTED' WHERE id=210");
|
||||||
assertThat(repository.read(11,q,List.of(210L),List.of(),List.of()).page().total()).isZero();
|
assertThat(repository.read(11,q,List.of(210L),List.of(),List.of(), progressIds()).page().total()).isZero();
|
||||||
var pending=new MerchantOrderWorkbenchQuery(null,null,null,null,null,"PENDING_REVIEW",null,null,"all",1,10);
|
var pending=new MerchantOrderWorkbenchQuery(null,null,null,null,null,"PENDING_REVIEW",null,null,"all",1,10);
|
||||||
assertThat(repository.read(11,pending,null,List.of(),List.of()).page().records()).extracting(OrderEntity::getId).containsExactly(210L);
|
assertThat(repository.read(11,pending,null,List.of(),List.of(), progressIds()).page().records()).extracting(OrderEntity::getId).containsExactly(210L);
|
||||||
var outside=new MerchantOrderWorkbenchQuery(null,null,null,null,null,null,to.plusDays(1),null,"all",1,10);
|
var outside=new MerchantOrderWorkbenchQuery(null,null,null,null,null,null,to.plusDays(1),null,"all",1,10);
|
||||||
assertThat(repository.read(11,outside,null,List.of(),List.of()).page().total()).isZero();
|
assertThat(repository.read(11,outside,null,List.of(),List.of(), progressIds()).page().total()).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void progressQueuesShareFiltersAndCountBeforePagination() {
|
||||||
|
var ids=progressIds();ids.put("cleared",List.of(210L,211L,214L));ids.put("delivering",List.of(212L));
|
||||||
|
var result=repository.read(11,query(null,"cleared",1),null,List.of(210L),List.of(),ids);
|
||||||
|
assertThat(result.page().total()).isEqualTo(2);
|
||||||
|
assertThat(result.queues().cleared()).isEqualTo(2);
|
||||||
|
assertThat(result.queues().delivering()).isEqualTo(1);
|
||||||
|
assertThat(result.page().records()).extracting(OrderEntity::getId).containsExactly(211L,210L);
|
||||||
|
var filtered=repository.read(11,query("SPECIAL","cleared",1),null,List.of(210L),List.of(),ids);
|
||||||
|
assertThat(filtered.page().total()).isEqualTo(1);
|
||||||
|
assertThat(filtered.queues().fee()).isEqualTo(1);
|
||||||
|
assertThat(repository.progressScope(11)).hasSize(212);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-6
@@ -19,11 +19,14 @@ import static org.mockito.ArgumentMatchers.*;
|
|||||||
class MerchantOrderWorkbenchServiceTest {
|
class MerchantOrderWorkbenchServiceTest {
|
||||||
private final FirstlegUnifiedOrderClient firstleg = mock(FirstlegUnifiedOrderClient.class);
|
private final FirstlegUnifiedOrderClient firstleg = mock(FirstlegUnifiedOrderClient.class);
|
||||||
private final MerchantOrderWorkbenchRepository repository = mock(MerchantOrderWorkbenchRepository.class);
|
private final MerchantOrderWorkbenchRepository repository = mock(MerchantOrderWorkbenchRepository.class);
|
||||||
|
private final com.baiyue.order.client.LastlegOrderProgressClient lastleg = mock(com.baiyue.order.client.LastlegOrderProgressClient.class);
|
||||||
private final UnifiedOrderQueryService unified = mock(UnifiedOrderQueryService.class);
|
private final UnifiedOrderQueryService unified = mock(UnifiedOrderQueryService.class);
|
||||||
private final MerchantOrderWorkbenchService service = new MerchantOrderWorkbenchService();
|
private final MerchantOrderWorkbenchService service = new MerchantOrderWorkbenchService();
|
||||||
|
|
||||||
@BeforeEach void setup() {
|
@BeforeEach void setup() {
|
||||||
ReflectionTestUtils.setField(service, "firstleg", firstleg);
|
ReflectionTestUtils.setField(service, "firstleg", firstleg);
|
||||||
|
ReflectionTestUtils.setField(service, "lastleg", lastleg);
|
||||||
|
when(lastleg.read(anyString(),any())).thenReturn(ApiResponse.success(new com.baiyue.order.client.LastlegOrderProgressClient.Response(List.of())));
|
||||||
ReflectionTestUtils.setField(service, "repository", repository);
|
ReflectionTestUtils.setField(service, "repository", repository);
|
||||||
ReflectionTestUtils.setField(service, "unified", unified);
|
ReflectionTestUtils.setField(service, "unified", unified);
|
||||||
ReflectionTestUtils.setField(service, "internalToken", "test-service-token");
|
ReflectionTestUtils.setField(service, "internalToken", "test-service-token");
|
||||||
@@ -33,10 +36,10 @@ class MerchantOrderWorkbenchServiceTest {
|
|||||||
}
|
}
|
||||||
@Test void combinesScopedFiltersWithDatabasePageWithoutReplacingTotals() {
|
@Test void combinesScopedFiltersWithDatabasePageWithoutReplacingTotals() {
|
||||||
var q = query("LOADED");
|
var q = query("LOADED");
|
||||||
var filters = new FirstlegUnifiedOrderClient.WorkbenchFilters(List.of(1L),List.of(2L),List.of(3L));
|
var filters = new FirstlegUnifiedOrderClient.WorkbenchFilters(List.of(1L),List.of(2L),List.of(3L), List.of());
|
||||||
when(firstleg.workbenchFilters(anyString(),any())).thenReturn(ApiResponse.success(filters));
|
when(firstleg.workbenchFilters(anyString(),any())).thenReturn(ApiResponse.success(filters));
|
||||||
when(repository.read(11,q,filters.matchingStatusOrderIds(),filters.feeOrderIds(),filters.customsOrderIds()))
|
when(repository.read(eq(11L),eq(q),eq(filters.matchingStatusOrderIds()),eq(filters.feeOrderIds()),eq(filters.customsOrderIds()),anyMap()))
|
||||||
.thenReturn(new MerchantOrderWorkbenchRepository.Result(new PageResult<>(List.of(),22,10,210,21),new Counts(210,2,1,0),List.of("SEA")));
|
.thenReturn(new MerchantOrderWorkbenchRepository.Result(new PageResult<>(List.of(),22,10,210,21),new Counts(210,2,1,0,0,0,0,0,0,0),List.of("SEA")));
|
||||||
when(unified.merchantWorkbenchRows(11,List.of())).thenReturn(List.of());
|
when(unified.merchantWorkbenchRows(11,List.of())).thenReturn(List.of());
|
||||||
var result = service.page(11,q);
|
var result = service.page(11,q);
|
||||||
assertThat(result.page().total()).isEqualTo(210);
|
assertThat(result.page().total()).isEqualTo(210);
|
||||||
@@ -47,9 +50,10 @@ class MerchantOrderWorkbenchServiceTest {
|
|||||||
}
|
}
|
||||||
@Test void unavailableOrIncompleteFulfillmentFiltersCannotProduceMisleadingEmptyQueues() {
|
@Test void unavailableOrIncompleteFulfillmentFiltersCannotProduceMisleadingEmptyQueues() {
|
||||||
for(var filters : List.of(
|
for(var filters : List.of(
|
||||||
new FirstlegUnifiedOrderClient.WorkbenchFilters(null,List.of(),List.of()),
|
new FirstlegUnifiedOrderClient.WorkbenchFilters(null,List.of(),List.of(), List.of()),
|
||||||
new FirstlegUnifiedOrderClient.WorkbenchFilters(List.of(),null,List.of()),
|
new FirstlegUnifiedOrderClient.WorkbenchFilters(List.of(),null,List.of(), List.of()),
|
||||||
new FirstlegUnifiedOrderClient.WorkbenchFilters(List.of(),List.of(),null))) {
|
new FirstlegUnifiedOrderClient.WorkbenchFilters(List.of(),List.of(),null, List.of()),
|
||||||
|
new FirstlegUnifiedOrderClient.WorkbenchFilters(List.of(),List.of(),List.of(),null))) {
|
||||||
when(firstleg.workbenchFilters(anyString(),any())).thenReturn(ApiResponse.success(filters));
|
when(firstleg.workbenchFilters(anyString(),any())).thenReturn(ApiResponse.success(filters));
|
||||||
assertThatThrownBy(()->service.page(11,query("LOADED"))).isInstanceOf(BizException.class).hasMessageContaining("暂时不可用");
|
assertThatThrownBy(()->service.page(11,query("LOADED"))).isInstanceOf(BizException.class).hasMessageContaining("暂时不可用");
|
||||||
}
|
}
|
||||||
@@ -63,4 +67,29 @@ class MerchantOrderWorkbenchServiceTest {
|
|||||||
assertThatThrownBy(()->service.page(11,q)).isInstanceOf(BizException.class).hasMessageContaining("时间");
|
assertThatThrownBy(()->service.page(11,q)).isInstanceOf(BizException.class).hasMessageContaining("时间");
|
||||||
verifyNoInteractions(firstleg,repository,unified);
|
verifyNoInteractions(firstleg,repository,unified);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test void tailFailureDoesNotReturnPartialQueueCounts() {
|
||||||
|
when(firstleg.workbenchFilters(anyString(),any())).thenReturn(ApiResponse.success(
|
||||||
|
new FirstlegUnifiedOrderClient.WorkbenchFilters(null,List.of(),List.of(),List.of())));
|
||||||
|
when(lastleg.read(anyString(),any())).thenThrow(new IllegalStateException());
|
||||||
|
assertThatThrownBy(()->service.page(11,query(null))).isInstanceOf(BizException.class);
|
||||||
|
verifyNoInteractions(repository,unified);
|
||||||
|
}
|
||||||
|
private com.baiyue.order.entity.OrderEntity order(long id,Long root,com.baiyue.order.entity.OrderStatus status) {
|
||||||
|
var o=new com.baiyue.order.entity.OrderEntity();o.setId(id);o.setRootOrderId(root);o.setStatus(status);return o;
|
||||||
|
}
|
||||||
|
@Test void currentStageIsExclusiveAndRootRequiresEveryFragmentWithTrustedCompletion() {
|
||||||
|
var status=com.baiyue.order.entity.OrderStatus.APPROVED;
|
||||||
|
var scope=List.of(order(1L,1L,status),order(2L,1L,status),order(3L,null,com.baiyue.order.entity.OrderStatus.COMPLETED),
|
||||||
|
order(4L,null,com.baiyue.order.entity.OrderStatus.SUBMITTED));
|
||||||
|
var first=List.of(new com.baiyue.order.dto.MerchantOrderProgress(1L,"cleared",false),
|
||||||
|
new com.baiyue.order.dto.MerchantOrderProgress(2L,"loaded",false));
|
||||||
|
var tail=List.of(new com.baiyue.order.dto.MerchantOrderProgress(2L,"delivering",false),
|
||||||
|
new com.baiyue.order.dto.MerchantOrderProgress(4L,"scheduled",false),
|
||||||
|
new com.baiyue.order.dto.MerchantOrderProgress(999L,"delivering",false));
|
||||||
|
var projected=MerchantOrderWorkbenchService.project(scope,first,tail);
|
||||||
|
assertThat(projected.get(1L).stage()).isEqualTo("delivering");assertThat(projected.get(1L).partial()).isTrue();
|
||||||
|
assertThat(projected.get(2L).partial()).isFalse();assertThat(projected.get(3L).stage()).isEqualTo("delivered");
|
||||||
|
assertThat(projected.get(4L).stage()).isEqualTo("none");assertThat(projected).doesNotContainKey(999L);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export async function verifyWorkbench(read, customerId) {
|
|||||||
const checks = []
|
const checks = []
|
||||||
const check = (name, ok) => { requireFact(ok, name); checks.push({ name, passed: true }) }
|
const check = (name, ok) => { requireFact(ok, name); checks.push({ name, passed: true }) }
|
||||||
async function all(path, workbench = false, params = {}) {
|
async function all(path, workbench = false, params = {}) {
|
||||||
let rows = [], expected = null, first
|
let rows = [], progress = [], expected = null, first
|
||||||
for (let number = 1; ; number++) {
|
for (let number = 1; ; number++) {
|
||||||
const query = new URLSearchParams({ ...params, page: String(number), size: '200' })
|
const query = new URLSearchParams({ ...params, page: String(number), size: '200' })
|
||||||
const response = await read(`${path}?${query}`)
|
const response = await read(`${path}?${query}`)
|
||||||
@@ -49,31 +49,44 @@ export async function verifyWorkbench(read, customerId) {
|
|||||||
if (number === 1) { expected = Number(page.total); first = response }
|
if (number === 1) { expected = Number(page.total); first = response }
|
||||||
requireFact(Number(page.total) === expected, 'Dataset changed while paging; rerun in a stable window')
|
requireFact(Number(page.total) === expected, 'Dataset changed while paging; rerun in a stable window')
|
||||||
requireFact(page.records.every(row => String(row.tenantId) === String(customerId)), 'Customer scope mismatch')
|
requireFact(page.records.every(row => String(row.tenantId) === String(customerId)), 'Customer scope mismatch')
|
||||||
|
if (workbench) {
|
||||||
|
requireFact(Array.isArray(response.progress) && response.progress.length === page.records.length
|
||||||
|
&& new Set(response.progress.map(p => String(p.orderId))).size === page.records.length
|
||||||
|
&& response.progress.every(p => page.records.some(r => String(r.orderId) === String(p.orderId))
|
||||||
|
&& ['none','loaded','declared','cleared','scheduled','delivering','delivered'].includes(p.stage)
|
||||||
|
&& typeof p.partial === 'boolean'), 'Invalid progress coverage')
|
||||||
|
progress.push(...response.progress)
|
||||||
|
}
|
||||||
rows.push(...page.records)
|
rows.push(...page.records)
|
||||||
if (number >= Number(page.pages)) break
|
if (number >= Number(page.pages)) break
|
||||||
requireFact(number < 1000, 'Pagination safety bound exceeded')
|
requireFact(number < 1000, 'Pagination safety bound exceeded')
|
||||||
}
|
}
|
||||||
requireFact(rows.length === expected && new Set(ids(rows)).size === expected, 'Duplicate or missing orders across pages')
|
requireFact(rows.length === expected && new Set(ids(rows)).size === expected, 'Duplicate or missing orders across pages')
|
||||||
return { rows, first }
|
return { rows, first, progress }
|
||||||
}
|
}
|
||||||
const workbench = await all(endpoint, true)
|
const workbench = await all(endpoint, true)
|
||||||
check('sample actually exceeds the former 200-order limit', workbench.rows.length > 200)
|
check('sample actually exceeds the former 200-order limit', workbench.rows.length > 200)
|
||||||
const formal = await all('/merchant/v1/orders')
|
const formal = await all('/merchant/v1/orders')
|
||||||
check('all formal orders match the independent unified query', equalIds(workbench.rows.filter(r => r.mainStatus !== 'DRAFT'), formal.rows))
|
check('all formal orders match the independent unified query', equalIds(workbench.rows.filter(r => r.mainStatus !== 'DRAFT'), formal.rows))
|
||||||
const queues = workbench.first.queues
|
const queues = workbench.first.queues
|
||||||
for (const name of ['all', 'fee', 'customs', 'draft']) {
|
for (const name of ['all', 'fee', 'customs', 'draft', 'loaded', 'declared', 'cleared', 'scheduled', 'delivering', 'delivered']) {
|
||||||
check(`queue ${name} has a valid count`, Number.isSafeInteger(Number(queues?.[name])) && Number(queues[name]) >= 0)
|
check(`queue ${name} has a valid count`, Number.isSafeInteger(Number(queues?.[name])) && Number(queues[name]) >= 0)
|
||||||
}
|
}
|
||||||
check('all queue count matches complete rows', Number(queues.all) === workbench.rows.length)
|
check('all queue count matches complete rows', Number(queues.all) === workbench.rows.length)
|
||||||
check('fee count matches authoritative confirmation flags', Number(queues.fee) === workbench.rows.filter(r => r.customerFeeConfirmationRequired === true).length)
|
check('fee count matches authoritative confirmation flags', Number(queues.fee) === workbench.rows.filter(r => r.customerFeeConfirmationRequired === true).length)
|
||||||
check('draft count matches draft rows', Number(queues.draft) === workbench.rows.filter(r => r.mainStatus === 'DRAFT').length)
|
check('draft count matches draft rows', Number(queues.draft) === workbench.rows.filter(r => r.mainStatus === 'DRAFT').length)
|
||||||
for (const queue of ['fee', 'customs', 'draft']) {
|
for (const queue of ['fee', 'customs', 'draft', 'loaded', 'declared', 'cleared', 'scheduled', 'delivering', 'delivered']) {
|
||||||
const selected = await all(endpoint, true, { queue })
|
const selected = await all(endpoint, true, { queue })
|
||||||
check(`${queue} complete page count`, selected.rows.length === Number(queues[queue]))
|
check(`${queue} complete page count`, selected.rows.length === Number(queues[queue]))
|
||||||
const own = new Set(ids(workbench.rows))
|
const own = new Set(ids(workbench.rows))
|
||||||
check(`${queue} contains only listed customer orders`, ids(selected.rows).every(id => own.has(id)))
|
check(`${queue} contains only listed customer orders`, ids(selected.rows).every(id => own.has(id)))
|
||||||
if (queue === 'fee') check('fee queue exact membership', equalIds(selected.rows, workbench.rows.filter(r => r.customerFeeConfirmationRequired === true)))
|
if (queue === 'fee') check('fee queue exact membership', equalIds(selected.rows, workbench.rows.filter(r => r.customerFeeConfirmationRequired === true)))
|
||||||
if (queue === 'draft') check('draft queue exact membership', equalIds(selected.rows, workbench.rows.filter(r => r.mainStatus === 'DRAFT')))
|
if (queue === 'draft') check('draft queue exact membership', equalIds(selected.rows, workbench.rows.filter(r => r.mainStatus === 'DRAFT')))
|
||||||
|
if (['loaded','declared','cleared','scheduled','delivering','delivered'].includes(queue)) {
|
||||||
|
const expectedIds = new Set(workbench.progress.filter(p => p.stage === queue).map(p => String(p.orderId)))
|
||||||
|
check(`${queue} progress exact membership`, selected.rows.length === expectedIds.size && ids(selected.rows).every(id => expectedIds.has(id)))
|
||||||
|
check(`${queue} selected progress agrees`, selected.progress.every(p => p.stage === queue))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const [page, size] of [[21, 10], [2, 50]]) {
|
for (const [page, size] of [[21, 10], [2, 50]]) {
|
||||||
const result = await read(`${endpoint}?page=${page}&size=${size}`)
|
const result = await read(`${endpoint}?page=${page}&size=${size}`)
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ function fixture() {
|
|||||||
mainStatus: i < 3 ? 'DRAFT' : 'LOADED', customerFeeConfirmationRequired: i >= 10 && i < 12,
|
mainStatus: i < 3 ? 'DRAFT' : 'LOADED', customerFeeConfirmationRequired: i >= 10 && i < 12,
|
||||||
channelName: i % 2 ? 'SEA' : 'AIR' }))
|
channelName: i % 2 ? 'SEA' : 'AIR' }))
|
||||||
const calls = []
|
const calls = []
|
||||||
|
const stages = ['loaded','declared','cleared','scheduled','delivering','delivered']
|
||||||
|
const stage = row => row.mainStatus === 'DRAFT' ? 'none' : stages[Number(row.orderId) % stages.length]
|
||||||
const queues = list => ({ all: list.length, fee: list.filter(r => r.customerFeeConfirmationRequired).length,
|
const queues = list => ({ all: list.length, fee: list.filter(r => r.customerFeeConfirmationRequired).length,
|
||||||
customs: list.filter(r => r.orderId === '1012').length, draft: list.filter(r => r.mainStatus === 'DRAFT').length })
|
customs: list.filter(r => r.orderId === '1012').length, draft: list.filter(r => r.mainStatus === 'DRAFT').length, ...Object.fromEntries(stages.map(key => [key, list.filter(r => stage(r) === key).length])) })
|
||||||
function read(path) {
|
function read(path) {
|
||||||
calls.push(path)
|
calls.push(path)
|
||||||
const url = new URL(path, 'https://test.invalid'), q = url.searchParams
|
const url = new URL(path, 'https://test.invalid'), q = url.searchParams
|
||||||
@@ -36,9 +38,10 @@ function fixture() {
|
|||||||
if (q.get('queue') === 'fee') selected = selected.filter(r => r.customerFeeConfirmationRequired)
|
if (q.get('queue') === 'fee') selected = selected.filter(r => r.customerFeeConfirmationRequired)
|
||||||
if (q.get('queue') === 'customs') selected = selected.filter(r => r.orderId === '1012')
|
if (q.get('queue') === 'customs') selected = selected.filter(r => r.orderId === '1012')
|
||||||
if (q.get('queue') === 'draft') selected = selected.filter(r => r.mainStatus === 'DRAFT')
|
if (q.get('queue') === 'draft') selected = selected.filter(r => r.mainStatus === 'DRAFT')
|
||||||
|
if (stages.includes(q.get('queue'))) selected = selected.filter(r => stage(r) === q.get('queue'))
|
||||||
const number = Number(q.get('page')), size = Number(q.get('size'))
|
const number = Number(q.get('page')), size = Number(q.get('size'))
|
||||||
const page = { page: number, size, total: selected.length, pages: Math.ceil(selected.length / size), records: selected.slice((number - 1) * size, number * size).map(r => ({ ...r })) }
|
const page = { page: number, size, total: selected.length, pages: Math.ceil(selected.length / size), records: selected.slice((number - 1) * size, number * size).map(r => ({ ...r })) }
|
||||||
return url.pathname.endsWith('/workbench') ? { page, queues: counts, channelNames: ['AIR', 'SEA'] } : page
|
return url.pathname.endsWith('/workbench') ? { page, queues: counts, channelNames: ['AIR', 'SEA'], progress: page.records.map(row => ({ orderId: row.orderId, stage: stage(row), partial: false })) } : page
|
||||||
}
|
}
|
||||||
return { read, calls, rows }
|
return { read, calls, rows }
|
||||||
}
|
}
|
||||||
@@ -55,6 +58,8 @@ for (const [name, mutate, expected] of [
|
|||||||
['customer leakage', r => { (r.page ?? r).records[0].tenantId = '22' }, /Customer scope/],
|
['customer leakage', r => { (r.page ?? r).records[0].tenantId = '22' }, /Customer scope/],
|
||||||
['duplicate identity', r => { const p = r.page ?? r; if (p.records.length > 1) p.records[1] = p.records[0] }, /duplicate/],
|
['duplicate identity', r => { const p = r.page ?? r; if (p.records.length > 1) p.records[1] = p.records[0] }, /duplicate/],
|
||||||
['partial response', r => { (r.page ?? r).records.pop() }, /Truncated/],
|
['partial response', r => { (r.page ?? r).records.pop() }, /Truncated/],
|
||||||
|
['missing progress', r => { if (r.queues) delete r.progress }, /progress coverage/],
|
||||||
|
['wrong clearing count', r => { if (r.queues) r.queues.cleared = 99 }, /cleared complete page count/],
|
||||||
['wrong fee count', r => { if (r.queues) r.queues.fee = 99 }, /fee count/],
|
['wrong fee count', r => { if (r.queues) r.queues.fee = 99 }, /fee count/],
|
||||||
['wrong draft count', r => { if (r.queues) r.queues.draft = 0 }, /draft count/],
|
['wrong draft count', r => { if (r.queues) r.queues.draft = 0 }, /draft count/],
|
||||||
['missing channel option', r => { if (r.queues) r.channelNames = ['SEA'] }, /channel options/],
|
['missing channel option', r => { if (r.queues) r.channelNames = ['SEA'] }, /channel options/],
|
||||||
|
|||||||
Reference in New Issue
Block a user