feat(selfop): filter turnover by multiple owners across all aggregates

This commit is contained in:
2026-10-15 11:12:44 +08:00
parent 72d7ed6962
commit e19d303a76
6 changed files with 51 additions and 15 deletions
@@ -43,10 +43,12 @@ class PlatformInventoryTurnoverControllerTest {
mvc.perform(get("/api/platform/v1/last-leg/self-operated-analytics/inventory-turnover")
.param("sourceAccount","demo").param("warehouseCode","W").param("endDate","2026-10-12")
.param("baseWindowDays","15").param("groupBy","OWNER").param("focus","COVERAGE_GT")
.param("customerCodes","A","B")
.param("coverageDaysThreshold","60").param("current","2").param("size","20")
.param("hideZeroInventory","false").param("snapshotContext","fixed-token").param("includeOverview","false")).andExpect(status().isOk());
var capture=ArgumentCaptor.forClass(InventoryTurnoverRequest.class);verify(service).query(capture.capture());
var q=capture.getValue();assertEquals(LocalDate.of(2026,10,12),q.endDate());assertEquals(15,q.days());
assertEquals(List.of("A","B"),q.customerCodes());
assertEquals("OWNER",q.group());assertEquals(60,q.coverageDaysThreshold());assertEquals(20,q.offset());assertFalse(q.overview());assertFalse(q.hideZero());assertEquals("fixed-token",q.snapshotContext());
}
}
@@ -20,6 +20,13 @@ class InventoryTurnoverRequestTest {
assertFalse(request(Map.of("hideZeroInventory",false)).hideZero());
assertTrue(request(Map.of("hideZeroInventory",true)).hideZero());
}
@Test void validatesMultipleOwnersAndRejectsTheRemovedSingleOwnerParameter() {
request(Map.of("customerCodes",List.of("A","B"))).validate(TODAY);
request(Map.of("customerCodes",List.of())).validate(TODAY);
assertThrows(BizException.class,()->request(Map.of("customerCodes",List.of(""))).validate(TODAY));
assertThrows(BizException.class,()->request(Map.of("customerCodes",Collections.nCopies(101,"A"))).validate(TODAY));
assertThrows(BizException.class,()->InventoryTurnoverRequest.validateParameters(Set.of("customerCode")));
}
@Test void rejectsIncompleteDatesFiltersSortsAndUnpinnedPagination() {
for(var invalid:List.of(Map.<String,Object>of("endDate","2026-10-13"),Map.<String,Object>of("baseWindowDays",367),
Map.<String,Object>of("groupBy","OWNER","sku","S"),Map.<String,Object>of("groupBy","OWNER","sortBy","COVERAGE_DESC"),
@@ -56,6 +56,26 @@ class InventoryTurnoverSqlTest {
var exact=new InventoryTurnoverRequest("demo","W",END,30,"SKU",null,"ZERO","ALL",null,null,"PHYSICAL_DESC",1,20,true,"fixed",true);
assertTrue(reportRows(input(exact,30,0,true)).isEmpty());
}
@Test void filtersMultipleOwnersBeforeAggregationAndPaginationWithoutMergingSameSku() throws Exception {
seed("A","SAME",30);seed("B","SAME",30);seed("C","SAME",30);
for(String group:List.of("SKU","OWNER")) {
var selected=new InventoryTurnoverRequest("demo","W",END,30,group,List.of("B","A","A"),null,"ALL",null,null,"PHYSICAL_DESC",1,20,true,"fixed",false);
var p=input(selected,30,30,true);
var rows=reportRows(p);
assertEquals(List.of("A","B"),rows.stream().map(r->r.get("customer_code")).toList());
var all=input(new InventoryTurnoverRequest("demo","W",END,30,group,null,null,"ALL",null,null,"PHYSICAL_DESC",1,20,true,"fixed",false),30,30,true);
assertEquals(3,reportRows(all).size());
for(int page=1;page<=2;page++) {
var q=new InventoryTurnoverRequest("demo","W",END,30,group,List.of("A","B"),null,"ALL",null,null,"PHYSICAL_DESC",page,1,false,"fixed",false);
assertEquals(rows.get(page-1).get("customer_code"),reportRows(input(q,30,30,true)).get(0).get("customer_code"));
}
var overview=query("overview",p);
assertFalse(overview.isEmpty());
assertNotEquals(query("overview",all),overview);
var unknown=new InventoryTurnoverRequest("demo","W",END,30,group,List.of("' OR 1=1 --"),null,"ALL",null,null,"PHYSICAL_DESC",1,20,true,"fixed",false);
assertTrue(reportRows(input(unknown,30,30,true)).isEmpty());
}
}
@Test void ownerVolumeChartRanksFullTotalsExcludesUnknownsAndIgnoresListFilters() throws Exception {
master();
for(int i=0;i<12;i++) {