test(selfop): verify every coverage bucket identity and pagination

This commit is contained in:
2026-10-13 11:18:40 +08:00
parent c13bf96cb9
commit ed8c9b1543
@@ -85,20 +85,55 @@ class DashboardSqlSemanticTest {
assertTrue(rows.stream().filter(row->"LONG".equals(row.get("rank_code"))).allMatch(row->new BigDecimal(row.get("main_value").toString()).compareTo(new BigDecimal("30.04"))==0));
}
@Test void sevenExclusiveBucketsConserveCandidatesAndEveryInventoryDrilldownMatches()throws Exception{
fixture("NOOUT","100","100",null);fixture("FROZEN","50","0",null);fixture("UNKNOWN",null,null,null);
fixture("THIRTY","30","30","30");fixture("THIRTY04","30.04","30.04","30");fixture("ZERO","0","0",null);
var stats=stats();assertEquals(5,count(stats,"candidate_skus"));assertEquals(1,count(stats,"excluded_zero_skus"));
assertEquals(1,count(stats,"bucket_days_0_30"));assertEquals(1,count(stats,"bucket_days_30_60"));assertEquals(1,count(stats,"bucket_no_outbound_30d"));
assertEquals(1,count(stats,"bucket_no_available"));assertEquals(1,count(stats,"bucket_unknown"));
long sum=0;for(String bucket:CoverageDistributionPolicy.BUCKETS){long n=count(stats,"bucket_"+bucket.toLowerCase(Locale.ROOT));sum+=n;
Map<String,Set<String>> expected=new LinkedHashMap<>();
for(String bucket:CoverageDistributionPolicy.BUCKETS)expected.put(bucket,new HashSet<>());
// Quantities make coverage exactly available days. +/- 0.000001 must not be display-rounded across boundaries.
String[][] finite={
{"THIRTY-BELOW","29.999999","DAYS_0_30"},{"THIRTY","30","DAYS_0_30"},{"ONE","1","DAYS_0_30"},
{"THIRTY-ABOVE","30.000001","DAYS_30_60"},{"SIXTY-BELOW","59.999999","DAYS_30_60"},{"SIXTY","60","DAYS_30_60"},
{"SIXTY-ABOVE","60.000001","DAYS_60_90"},{"NINETY-BELOW","89.999999","DAYS_60_90"},{"NINETY","90","DAYS_60_90"},
{"NINETY-ABOVE","90.000001","DAYS_GT_90"},{"LONG-A","100","DAYS_GT_90"},{"LONG-B","100","DAYS_GT_90"}};
for(var row:finite){fixture(row[0],row[1],row[1],"30");expected.get(row[2]).add("C/"+row[0]);}
for(int i=0;i<3;i++){
fixture("NOOUT-"+i,"100","100",null);expected.get("NO_OUTBOUND_30D").add("C/NOOUT-"+i);
fixture("FROZEN-"+i,"50","0",null);expected.get("NO_AVAILABLE").add("C/FROZEN-"+i);
fixture("UNKNOWN-"+i,null,null,null);expected.get("UNKNOWN").add("C/UNKNOWN-"+i);
}
fixture("ZERO","0","0",null);
// Same SKU, different owner and bucket: identities must never collapse to SKU alone.
profile(11,"B","THIRTY","PCS");profile(22,"B","THIRTY","PCS");stock("B","THIRTY","75","75");daily("B","THIRTY",D.minusDays(1),"30","PCS");
expected.get("DAYS_60_90").add("B/THIRTY");
var stats=stats();assertEquals(1,count(stats,"excluded_zero_skus"));
var classified=query(DashboardReadMapper.class,"report",Map.of("p",request(),"o",options()),false,true);
Map<String,Set<String>> dashboardIdentities=new HashMap<>();
for(Object value:classified){Map<?,?> row=(Map<?,?>)value;Map<String,Object> normalized=new HashMap<>();row.forEach((k,v)->normalized.put(k.toString().toLowerCase(Locale.ROOT),v));
String bucket=(String)normalized.get("bucket_code");String key=normalized.get("customer_code")+"/"+normalized.get("sku_code");
assertTrue(dashboardIdentities.computeIfAbsent(bucket,ignored->new HashSet<>()).add(key),"duplicate dashboard identity "+key);
}
assertEquals(expected,dashboardIdentities);
long sum=0;Set<String> union=new HashSet<>();
for(String bucket:CoverageDistributionPolicy.BUCKETS){
long n=count(stats,"bucket_"+bucket.toLowerCase(Locale.ROOT));assertTrue(n>0,bucket+" must be populated");sum+=n;
assertEquals(expected.get(bucket).size(),n,bucket);
var o=new AnalyticsInventoryOptions(null,null,null,null,bucket,CoverageDistributionPolicy.RULE_VERSION,null,null);
assertEquals(n,((Number)query(AnalyticsReadMapper.class,"inventoryCount",Map.of("p",request(),"o",o)).get(0)).longValue(),bucket);
}assertEquals(5,sum);
var finite=new AnalyticsInventoryOptions(null,null,"COVERAGE_DAYS_DESC",null,null,null,"FINITE_POSITIVE",null);
assertEquals(count(stats,"long_eligible"),((Number)query(AnalyticsReadMapper.class,"inventoryCount",Map.of("p",request(),"o",finite)).get(0)).longValue());
assertEquals(2,count(stats,"no_outbound_eligible"));
var ranked=ranks();assertEquals("THIRTY04",ranked.stream().filter(r->r.rankCode().equals("LONG")).max(Comparator.comparing(DashboardReadMapper.Ranked::mainValue)).orElseThrow().skuCode());
assertTrue(ranked.stream().anyMatch(r->r.rankCode().equals("NO_OUTBOUND")&&r.skuCode().equals("FROZEN")));
var all=query(AnalyticsReadMapper.class,"inventoryPage",Map.of("p",request(),"o",o)).stream().map(AnalyticsReadRows.Inventory.class::cast).map(row->row.customerCode()+"/"+row.skuCode()).toList();
assertEquals(dashboardIdentities.get(bucket),new HashSet<>(all),bucket+" drilldown identities");assertEquals(n,all.size());
List<String> paged=new ArrayList<>();Set<String> seen=new HashSet<>();
for(long offset=0;offset<=n+1;offset+=2){
var page=query(AnalyticsReadMapper.class,"inventoryPage",Map.of("p",pageRequest(offset,2),"o",o)).stream().map(AnalyticsReadRows.Inventory.class::cast).map(row->row.customerCode()+"/"+row.skuCode()).toList();
int start=(int)Math.min(offset,n),end=(int)Math.min(offset+2,n);assertEquals(all.subList(start,end),page,bucket+" offset="+offset);
for(String key:page)assertTrue(seen.add(key),bucket+" repeated identity "+key);paged.addAll(page);
}
assertEquals(all,paged,bucket+" pages omit or reorder identities");assertEquals(dashboardIdentities.get(bucket),seen);
for(String key:seen)assertTrue(union.add(key),"identity belongs to multiple buckets: "+key);
}
assertEquals(count(stats,"candidate_skus"),sum);assertEquals(sum,union.size());assertFalse(union.contains("C/ZERO"));
var finiteSelection=new AnalyticsInventoryOptions(null,null,"COVERAGE_DAYS_DESC",null,null,null,"FINITE_POSITIVE",null);
assertEquals(13,count(stats,"long_eligible"));
assertEquals(count(stats,"long_eligible"),((Number)query(AnalyticsReadMapper.class,"inventoryCount",Map.of("p",request(),"o",finiteSelection)).get(0)).longValue());
assertEquals(6,count(stats,"no_outbound_eligible"));
assertTrue(ranks().stream().anyMatch(row->row.rankCode().equals("NO_OUTBOUND")&&row.skuCode().equals("FROZEN-0")));
}
@Test void exactNinetyBoundaryAndUnknownInventoryRemainDistinct()throws Exception{
fixture("NINETY","90","90","30");fixture("NINETY04","90.04","90.04","30");fixture("INFINITE","999","999",null);
@@ -185,9 +220,11 @@ class DashboardSqlSemanticTest {
p.setLong(1,run);p.setString(2,"demo");p.setString(3,"W");p.setString(4,owner);p.setString(5,sku);p.setString(6,sku);p.setString(7,unit);p.setString(8,run==11?"VALID_OUTBOUND":"INVENTORY_OBSERVATION");p.setLong(9,run);p.setObject(10,D.minusDays(40).atStartOfDay().minusHours(8));p.executeUpdate();}}
private void daily(String owner,String sku,LocalDate day,String value,String unit)throws Exception{try(var p=connection.prepareStatement("INSERT INTO ll_selfop_sku_daily_sales VALUES(?,?,?,?,?,?,?,?,?,?)")){
p.setLong(1,++sequence);p.setLong(2,11);p.setString(3,"demo");p.setString(4,"W");p.setString(5,owner);p.setString(6,sku);p.setObject(7,day);p.setBigDecimal(8,value==null?null:new BigDecimal(value));p.setLong(9,1);p.setString(10,unit);p.executeUpdate();}}
private void stock(String sku,String physical,String available)throws Exception{try(var p=connection.prepareStatement("INSERT INTO ll_selfop_inventory_snapshot VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)")){
p.setLong(1,22);p.setString(2,"demo");p.setString(3,"W");p.setString(4,"C");p.setString(5,sku);p.setInt(6,0);p.setBigDecimal(7,physical==null?null:new BigDecimal(physical));p.setBigDecimal(8,available==null?null:new BigDecimal(available));p.setBigDecimal(9,BigDecimal.ZERO);p.setBigDecimal(10,BigDecimal.ZERO);p.setObject(11,D.atStartOfDay());p.setString(12,physical==null?"UNKNOWN":"VALID");p.setString(13,physical==null?"FIELD_MISSING":null);p.executeUpdate();}}
private void stock(String sku,String physical,String available)throws Exception{stock("C",sku,physical,available);}
private void stock(String owner,String sku,String physical,String available)throws Exception{try(var p=connection.prepareStatement("INSERT INTO ll_selfop_inventory_snapshot VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)")){
p.setLong(1,22);p.setString(2,"demo");p.setString(3,"W");p.setString(4,owner);p.setString(5,sku);p.setInt(6,0);p.setBigDecimal(7,physical==null?null:new BigDecimal(physical));p.setBigDecimal(8,available==null?null:new BigDecimal(available));p.setBigDecimal(9,BigDecimal.ZERO);p.setBigDecimal(10,BigDecimal.ZERO);p.setObject(11,D.atStartOfDay());p.setString(12,physical==null?"UNKNOWN":"VALID");p.setString(13,physical==null?"FIELD_MISSING":null);p.executeUpdate();}}
private AnalyticsSqlRequest request(){return new AnalyticsSqlRequest("demo","W",null,null,11,22,D.minusDays(30),D,D,D.minusDays(30),D.minusDays(30),D,D.minusDays(30).atStartOfDay().minusHours(8),D.atStartOfDay(),true,true,true,true,true,0,20,"DASHBOARD");}
private AnalyticsSqlRequest pageRequest(long offset,int size){var p=request();return new AnalyticsSqlRequest(p.account(),p.warehouse(),p.customer(),p.sku(),p.salesRun(),p.inventoryRun(),p.from(),p.until(),p.anchor(),p.windowFrom(),p.completeFrom(),p.completeUntil(),p.coveredFrom(),p.coveredUntil(),p.salesKnown(),p.formalSales(),p.completeWindow(),p.complete30(),p.stockFresh(),offset,size,"INVENTORY");}
private AnalyticsSqlRequest request(boolean complete,boolean fresh){var p=request();return new AnalyticsSqlRequest(p.account(),p.warehouse(),null,null,p.salesRun(),p.inventoryRun(),p.from(),p.until(),p.anchor(),p.windowFrom(),p.completeFrom(),p.completeUntil(),p.coveredFrom(),p.coveredUntil(),p.salesKnown(),p.formalSales(),complete,complete,fresh,p.offset(),p.size(),p.kind());}
private static AnalyticsInventoryOptions options(){return new AnalyticsInventoryOptions(null,null,null,null,null,null,null,null);}
@SuppressWarnings("unchecked") private List<Map<String,Object>> report(){
@@ -201,7 +238,8 @@ class DashboardSqlSemanticTest {
private List<?> query(Class<?> mapper,String method,Map<String,Object> args){
return query(mapper,method,args,false);
}
private List<?> query(Class<?> mapper,String method,Map<String,Object> args,boolean originalAggregates){
private List<?> query(Class<?> mapper,String method,Map<String,Object> args,boolean originalAggregates){return query(mapper,method,args,originalAggregates,false);}
private List<?> query(Class<?> mapper,String method,Map<String,Object> args,boolean originalAggregates,boolean classifiedIdentities){
var original=config.getMappedStatement(mapper.getName()+"."+method);var bound=original.getBoundSql(args);var meta=org.apache.ibatis.reflection.SystemMetaObject.forObject(args);
StringBuilder sql=new StringBuilder();int index=0;boolean quoted=false;
for(int i=0;i<bound.getSql().length();i++){char ch=bound.getSql().charAt(i);if(ch=='\'')quoted=!quoted;
@@ -215,6 +253,12 @@ class DashboardSqlSemanticTest {
for(String field:List.of("physical_quantity","available_quantity","frozen_quantity","transit_quantity","defective_physical_quantity"))
rendered=rendered.replace("CASE WHEN c.stock_unit_valid=1 AND 0=1 THEN st."+field+" END","CAST(NULL AS DECIMAL(20,6))");
if(originalAggregates)rendered=rendered.replace("1=0 /* dashboard unused sales */ AND ","").replace("1=0 /* dashboard unused stock */ AND ","");
if(classifiedIdentities){
// Expose identities from the actual dashboard classification CTE; never reimplement its bucket formula in the test.
String finalSelect="SELECT s.*,r.* FROM statistics s LEFT JOIN rankings r ON 1=1";
assertTrue(rendered.endsWith(finalSelect));rendered=rendered.substring(0,rendered.length()-finalSelect.length())
+"SELECT customer_code,sku_code,bucket_code FROM classified WHERE bucket_code IS NOT NULL";
}
String id="dashboard.literal."+(++sequence);config.addMappedStatement(new MappedStatement.Builder(config,id,new org.apache.ibatis.builder.StaticSqlSource(config,rendered),SqlCommandType.SELECT).resultMaps(original.getResultMaps()).build());return session.selectList(id);
}
}