fix(self-operated): align turnover sales eligibility and known zero stock
This commit is contained in:
+20
@@ -88,6 +88,26 @@ class DashboardSqlSemanticTest {
|
||||
assertEquals(1,topRows.size());var item=(AnalyticsReadRows.Aggregate)topRows.get(0);
|
||||
assertEquals("RECENT",item.skuCode());assertEquals(0,new BigDecimal("10").compareTo(item.windowQuantity()));assertNull(item.sales30Quantity());
|
||||
}
|
||||
@Test void emptyValidDefectiveOnlySkuDoesNotBlockAvailableTotalButUnknownsStillDo()throws Exception{
|
||||
fixture("NORMAL","120","100",null);fixture("DEFECTIVE-ONLY","0","0",null);
|
||||
try(var s=connection.createStatement()){
|
||||
s.executeUpdate("UPDATE ll_selfop_inventory_snapshot SET product_quality=1 WHERE sku_code='DEFECTIVE-ONLY'");
|
||||
}
|
||||
var stats=report().get(0);
|
||||
assertEquals(0,count(stats,"available_quantity_unknown"));
|
||||
assertEquals(0,new BigDecimal("100").compareTo((BigDecimal)stats.get("available_quantity_known")));
|
||||
var detail=query(AnalyticsReadMapper.class,"inventoryPage",Map.of("p",windowRequest(D.minusDays(30),D,true,"INVENTORY"),"o",options()))
|
||||
.stream().map(AnalyticsReadRows.Inventory.class::cast).filter(r->"DEFECTIVE-ONLY".equals(r.skuCode())).findFirst().orElseThrow();
|
||||
assertEquals(0,BigDecimal.ZERO.compareTo(detail.availableQuantity()));
|
||||
// None of these prove a known empty defective-only SKU.
|
||||
for(String change:List.of("physical_quantity=1","physical_quantity=NULL","quality_status='UNKNOWN',quality_reason='SOURCE_ROW_MISSING'","product_quality=2","product_quality=0,available_quantity=NULL")){
|
||||
try(var s=connection.createStatement()){
|
||||
s.executeUpdate("UPDATE ll_selfop_inventory_snapshot SET product_quality=1,physical_quantity=0,available_quantity=0,quality_status='VALID',quality_reason=NULL WHERE sku_code='DEFECTIVE-ONLY'");
|
||||
s.executeUpdate("UPDATE ll_selfop_inventory_snapshot SET "+change+" WHERE sku_code='DEFECTIVE-ONLY'");
|
||||
}
|
||||
assertEquals(1,count(report().get(0),"available_quantity_unknown"),change);
|
||||
}
|
||||
}
|
||||
@Test void combinedReportKeepsStatisticsWhenAllThreeRankingsAreEmpty()throws Exception{
|
||||
var empty=report();assertEquals(1,empty.size());assertEquals(0,count(empty.get(0),"total_skus"));
|
||||
assertNull(empty.get(0).get("rank_code"));assertNull(empty.get(0).get("physical_quantity_known"));
|
||||
|
||||
+45
-4
@@ -52,6 +52,15 @@ class InventoryTurnoverSqlTest {
|
||||
decimal(Integer.toString(Math.min(30,n)*10),chart.get("w"+n+"_outbound"));
|
||||
}
|
||||
}
|
||||
@Test void emptyDefectiveOnlyAvailableIsZeroButMissingNormalAndStaleStockRemainUnknown() throws Exception {
|
||||
profile(22,"A","EMPTY");stock(22,"A","EMPTY",1,0,0);
|
||||
var q=request("SKU","ALL","PHYSICAL_DESC",null,null,1,20);
|
||||
decimal("0",query("page",input(q,30,0,true)).get(0).get("available"));
|
||||
assertNull(query("page",input(q,30,0,false)).get(0).get("available"));
|
||||
stock(22,"A","EMPTY",0,0,0);
|
||||
execute("UPDATE ll_selfop_inventory_snapshot SET available_quantity=NULL,quality_status='UNKNOWN',quality_reason='FIELD_MISSING' WHERE product_quality=0");
|
||||
assertNull(query("page",input(q,30,0,true)).get(0).get("available"));
|
||||
}
|
||||
@Test void threeWindowsAndDailyQualitiesAreNotMultiplied() throws Exception {
|
||||
seed("A","SKU",30);
|
||||
var p=input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,20),30,30,true);
|
||||
@@ -294,14 +303,14 @@ class InventoryTurnoverSqlTest {
|
||||
decimal("0",complete.get("w30_outbound"));decimal("30",complete.get("w30_complete"));
|
||||
}
|
||||
}
|
||||
@Test void zeroEvidenceUsesExactBeijingMidnightIncludingMilliseconds() throws Exception {
|
||||
@Test void coveredZeroDoesNotDependOnObservationBeforeWindowMidnight() throws Exception {
|
||||
seed("A","SKU",0);execute("DELETE FROM ll_selfop_sku_daily_sales WHERE warehouse_code='W'");
|
||||
for(int n:List.of(7,15,30)) for(int millis:List.of(-1,0,1)) {
|
||||
LocalDate start=END.minusDays(n-1);var utc=start.atStartOfDay().minusHours(8).plusNanos(millis*1_000_000L);
|
||||
execute("UPDATE ll_selfop_sku_profile SET earliest_evidence_at_utc=TIMESTAMP '"+utc.toString().replace('T',' ')+"'");
|
||||
var p=input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,20),30,0,true);
|
||||
var row=reportRows(p).get(0);decimal(String.valueOf(n-(millis>0?1:0)),row.get("w"+n+"_complete"));
|
||||
if(millis>0)assertNull(row.get("w"+n+"_outbound"));else decimal("0",row.get("w"+n+"_outbound"));
|
||||
var row=reportRows(p).get(0);decimal(String.valueOf(n),row.get("w"+n+"_complete"));
|
||||
decimal("0",row.get("w"+n+"_outbound"));
|
||||
execute("INSERT INTO ll_selfop_sku_daily_sales VALUES(11,'demo','W','A','SKU',DATE '"+start+"',0,'PCS')");
|
||||
row=reportRows(p).get(0);decimal(String.valueOf(n),row.get("w"+n+"_complete"));decimal("0",row.get("w"+n+"_outbound"));
|
||||
execute("DELETE FROM ll_selfop_sku_daily_sales WHERE warehouse_code='W'");
|
||||
@@ -312,13 +321,45 @@ class InventoryTurnoverSqlTest {
|
||||
var p=input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,20),1,0,true);
|
||||
var afterLast=END.atStartOfDay().minusHours(8).plusNanos(1_000_000);
|
||||
execute("UPDATE ll_selfop_sku_profile SET earliest_evidence_at_utc=TIMESTAMP '"+afterLast.toString().replace('T',' ')+"'");
|
||||
var row=reportRows(p).get(0);decimal("0",row.get("w7_complete"));assertNull(row.get("w7_observed"));decimal("0",row.get("w7_partial"));
|
||||
var row=reportRows(p).get(0);decimal("1",row.get("w7_complete"));decimal("0",row.get("w7_observed"));decimal("0",row.get("w7_partial"));
|
||||
execute("INSERT INTO ll_selfop_sku_daily_sales VALUES(11,'demo','W','A','SKU',DATE '"+END+"',1.234567,'PCS')");
|
||||
row=reportRows(p).get(0);decimal("1",row.get("w7_complete"));decimal("1.234567",row.get("w7_observed"));
|
||||
execute("UPDATE ll_selfop_sku_profile SET earliest_evidence_at_utc=TIMESTAMP '2026-01-01 00:00:00'");
|
||||
execute("UPDATE ll_selfop_sku_daily_sales SET quantity_unit='UNKNOWN' WHERE warehouse_code='W'");
|
||||
row=reportRows(p).get(0);decimal("0",row.get("w7_complete"));assertNull(row.get("w7_observed"));decimal("0",row.get("w7_partial"));
|
||||
}
|
||||
@Test void noObservationAgeDoesNotHideWindowSalesCoverageOrNoOutboundSummary() throws Exception {
|
||||
seed("A","SOLD",0);seed("A","UNSOLD",0);
|
||||
execute("UPDATE ll_selfop_sku_profile SET evidence_run_id=NULL,evidence_type=NULL,earliest_evidence_at_utc=NULL");
|
||||
execute("DELETE FROM ll_selfop_sku_daily_sales WHERE sku_code='UNSOLD'");
|
||||
var q=request("SKU","ALL","PHYSICAL_DESC",null,30,1,20);
|
||||
var p=input(q,30,0,true);
|
||||
var rows=reportRows(p);
|
||||
var sold=rows.stream().filter(r->"SOLD".equals(r.get("sku_code"))).findFirst().orElseThrow();
|
||||
decimal("300",sold.get("w30_outbound"));decimal("60",sold.get("coverage_days"));decimal("0",sold.get("unassessable"));
|
||||
assertNull(sold.get("turnover"));decimal("0",sold.get("sample_count"));
|
||||
var unsold=rows.stream().filter(r->"UNSOLD".equals(r.get("sku_code"))).findFirst().orElseThrow();
|
||||
decimal("0",unsold.get("w30_outbound"));decimal("1",unsold.get("no_outbound"));decimal("0",unsold.get("unassessable"));
|
||||
assertNull(unsold.get("coverage_days"));
|
||||
var summary=query("overview",p).get(0);
|
||||
decimal("1",summary.get("no_outbound_count"));decimal("0",summary.get("no_outbound_unknown"));
|
||||
var filtered=reportRows(input(request("SKU","NO_OUTBOUND_IN_WINDOW","PHYSICAL_DESC",null,null,1,20),30,0,true));
|
||||
assertEquals(List.of("UNSOLD"),filtered.stream().map(r->r.get("sku_code")).toList());
|
||||
summary=query("overview",input(q,7,0,true)).get(0);
|
||||
decimal("2",summary.get("no_outbound_unknown"));
|
||||
// Page projection preserves null stock types in H2 when freshness is a bound false.
|
||||
var stale=query("page",input(q,30,0,false));
|
||||
assertTrue(stale.stream().allMatch(r->r.get("no_outbound")==null));
|
||||
}
|
||||
@Test void oneUnknownAvailableQuantityKeepsSubtotalWithoutHidingIndependentNoOutboundCount() throws Exception {
|
||||
seed("A","KNOWN",0);seed("A","UNKNOWN",0);
|
||||
execute("DELETE FROM ll_selfop_sku_daily_sales WHERE warehouse_code='W'");
|
||||
execute("UPDATE ll_selfop_inventory_snapshot SET available_quantity=NULL,quality_status='UNKNOWN',quality_reason='FIELD_MISSING' WHERE snapshot_run_id=22 AND sku_code='UNKNOWN' AND product_quality=0");
|
||||
var summary=query("overview",input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,20),30,0,true)).get(0);
|
||||
decimal("1220",summary.get("physical"));assertNull(summary.get("available"));
|
||||
decimal("600",summary.get("available_known"));decimal("1",summary.get("available_unknown"));
|
||||
decimal("2",summary.get("no_outbound_count"));decimal("0",summary.get("no_outbound_unknown"));
|
||||
}
|
||||
@Test void presentInvalidSamplesNeverQualifyAndOwnerZeroNeedsEverySku() throws Exception {
|
||||
seed("A","ONE",30);seed("A","TWO",30);
|
||||
String key=" WHERE snapshot_run_id=100 AND sku_code='TWO' AND product_quality=0";
|
||||
|
||||
Reference in New Issue
Block a user