fix: preserve complete historical dashboard days

This commit is contained in:
2026-10-13 13:43:13 +08:00
parent 496fc11b0a
commit cebcdfb66e
2 changed files with 33 additions and 1 deletions
@@ -17,7 +17,9 @@ public final class DashboardSalesPolicy {
Instant upper=c.salesCoveredUntil()==null?null:(c.salesCoveredUntil().isBefore(end)?c.salesCoveredUntil():end); Instant upper=c.salesCoveredUntil()==null?null:(c.salesCoveredUntil().isBefore(end)?c.salesCoveredUntil():end);
boolean intersects=lower!=null&&upper!=null&&lower.isBefore(upper); boolean intersects=lower!=null&&upper!=null&&lower.isBefore(upper);
boolean invalid=actual!=null&&actual.invalidRowCount()!=null&&actual.invalidRowCount()>0; boolean invalid=actual!=null&&actual.invalidRowCount()!=null&&actual.invalidRowCount()>0;
boolean trusted="VALID".equals(c.salesStatus())&&!c.rebuildRequired(); // Global recency describes how far collection has progressed, not whether an
// already published historical day has complete coverage. Keep STALE on context.
boolean trusted=("VALID".equals(c.salesStatus())||"STALE".equals(c.salesStatus()))&&!c.rebuildRequired();
boolean complete=sourceKnown&&trusted&&!invalid&&day.isBefore(c.anchorDate())&&c.completeDayFrom()!=null&&c.completeDayUntil()!=null boolean complete=sourceKnown&&trusted&&!invalid&&day.isBefore(c.anchorDate())&&c.completeDayFrom()!=null&&c.completeDayUntil()!=null
&&!day.isBefore(c.completeDayFrom())&&day.isBefore(c.completeDayUntil())&&intersects&&lower.equals(start)&&upper.equals(end); &&!day.isBefore(c.completeDayFrom())&&day.isBefore(c.completeDayUntil())&&intersects&&lower.equals(start)&&upper.equals(end);
BigDecimal known=sourceKnown&&actual!=null?actual.observedQuantity():null; BigDecimal known=sourceKnown&&actual!=null?actual.observedQuantity():null;
@@ -24,6 +24,36 @@ class DashboardPolicyTest {
assertEquals("PARTIAL",points.get(1).dataStatus());assertEquals(BigDecimal.ZERO,points.get(1).observedQuantity());assertNull(points.get(1).outboundQuantity()); assertEquals("PARTIAL",points.get(1).dataStatus());assertEquals(BigDecimal.ZERO,points.get(1).observedQuantity());assertNull(points.get(1).outboundQuantity());
var unknown=SalesBucketPolicy.aggregate(DashboardSalesPolicy.points(context(null,null,false),false,DAY.minusDays(1),DAY.plusDays(1),List.of()));assertNull(unknown.observedQuantity()); var unknown=SalesBucketPolicy.aggregate(DashboardSalesPolicy.points(context(null,null,false),false,DAY.minusDays(1),DAY.plusDays(1),List.of()));assertNull(unknown.observedQuantity());
} }
@Test void staleCollectionDoesNotDowngradePublishedHistoricalDayOrInventOtherFourteenDays(){
LocalDate published=DAY.minusDays(3);var c=publishedDayContext("STALE",false);
for(BigDecimal amount:List.of(BigDecimal.ZERO,new BigDecimal("2251"))) {
var rows=amount.signum()==0?List.<DashboardReadMapper.Day>of():List.of(new DashboardReadMapper.Day(published,amount,0L));
var points=DashboardSalesPolicy.points(c,true,DAY.minusDays(14),DAY.plusDays(1),rows);
var point=points.stream().filter(p->p.date().equals(published)).findFirst().orElseThrow();
assertEquals("COMPLETE",point.dataStatus());assertEquals(amount,point.outboundQuantity());
assertFalse(point.reasonCodes().contains("SALES_DAY_INCOMPLETE"));assertFalse(point.reasonCodes().contains("SALES_STALE"));
var summary=SalesBucketPolicy.aggregate(points);
assertEquals(1,summary.completeDays());assertEquals(14,summary.unknownDays());assertEquals(0,summary.partialDays());
assertEquals("PARTIAL",summary.dataStatus());assertNull(summary.outboundQuantity());assertEquals(amount,summary.observedQuantity());
var single=SalesBucketPolicy.aggregate(DashboardSalesPolicy.points(c,true,published,published.plusDays(1),rows));
assertEquals("COMPLETE",single.dataStatus());assertEquals(amount,single.outboundQuantity());
assertEquals("STALE",c.salesStatus());assertTrue(c.reasonCodes().contains("SALES_STALE"));
}
}
@Test void historicalRecencyFixDoesNotRelaxInvalidQuantitySourceOrRebuildGuards(){
LocalDate day=DAY.minusDays(3);var rows=List.of(new DashboardReadMapper.Day(day,new BigDecimal("2251"),0L));
assertEquals("UNKNOWN",DashboardSalesPolicy.points(publishedDayContext("STALE",false),false,day,day.plusDays(1),rows).get(0).dataStatus());
var invalid=DashboardSalesPolicy.points(publishedDayContext("STALE",false),true,day,day.plusDays(1),List.of(new DashboardReadMapper.Day(day,new BigDecimal("2"),1L))).get(0);
assertEquals("PARTIAL",invalid.dataStatus());assertNull(invalid.outboundQuantity());assertTrue(invalid.reasonCodes().contains("SALES_QUANTITY_OR_UNIT_INVALID"));
var rebuilding=DashboardSalesPolicy.points(publishedDayContext("REBUILD_REQUIRED",true),true,day,day.plusDays(1),rows).get(0);
assertEquals("PARTIAL",rebuilding.dataStatus());assertNull(rebuilding.outboundQuantity());
}
private static AnalyticsResponses.Context publishedDayContext(String status,boolean rebuild){
LocalDate day=DAY.minusDays(3);var zone=ZoneId.of("Asia/Shanghai");
return new AnalyticsResponses.Context(11L,22L,"a".repeat(64),"b".repeat(64),NOW,NOW.plusSeconds(1800),"Asia/Shanghai",DAY,
DAY.minusDays(14),DAY.plusDays(1),null,day,day.plusDays(1),day.atStartOfDay(zone).toInstant(),day.plusDays(1).atStartOfDay(zone).toInstant(),
NOW.minusSeconds(60),NOW.minusSeconds(60),NOW.minusSeconds(30),status,"VALID",rebuild,"CURRENT",List.of("SALES_"+status));
}
@Test void strictInventoryDrilldownExpiresBeforeContextWhileOrdinaryInventoryRemainsAllowed(){ @Test void strictInventoryDrilldownExpiresBeforeContextWhileOrdinaryInventoryRemainsAllowed(){
var c=context(NOW.minusSeconds(86400),NOW,true);var q=query(true);var strict=new AnalyticsInventoryOptions(null,null,null,null,"UNKNOWN",CoverageDistributionPolicy.RULE_VERSION,null,"DASHBOARD",null,null); var c=context(NOW.minusSeconds(86400),NOW,true);var q=query(true);var strict=new AnalyticsInventoryOptions(null,null,null,null,"UNKNOWN",CoverageDistributionPolicy.RULE_VERSION,null,"DASHBOARD",null,null);
Instant fresh=NOW.plusSeconds(60);assertEquals(fresh,CoverageDistributionPolicy.expiresAt(c,fresh)); Instant fresh=NOW.plusSeconds(60);assertEquals(fresh,CoverageDistributionPolicy.expiresAt(c,fresh));