fix(self-operated): rank hot SKUs for selected dates
This commit is contained in:
+5
-4
@@ -20,8 +20,8 @@ public final class AnalyticsInventorySql {
|
|||||||
static String dashboardCte(AnalyticsSqlRequest p){return cte(p,false,true);}
|
static String dashboardCte(AnalyticsSqlRequest p){return cte(p,false,true);}
|
||||||
private static String cte(AnalyticsSqlRequest p,boolean stockFirst){return cte(p,stockFirst,false);}
|
private static String cte(AnalyticsSqlRequest p,boolean stockFirst){return cte(p,stockFirst,false);}
|
||||||
private static String cte(AnalyticsSqlRequest p,boolean stockFirst,boolean dashboard){
|
private static String cte(AnalyticsSqlRequest p,boolean stockFirst,boolean dashboard){
|
||||||
String days="INVENTORY".equals(p.kind())?Long.toString(java.time.temporal.ChronoUnit.DAYS.between(p.from(),p.until())):"30";
|
String days=("INVENTORY".equals(p.kind())||"HOT_DASHBOARD".equals(p.kind()))?Long.toString(java.time.temporal.ChronoUnit.DAYS.between(p.from(),p.until())):"30";
|
||||||
String complete="INVENTORY".equals(p.kind())?"#{p.completeWindow}":"#{p.complete30}";
|
String complete=("INVENTORY".equals(p.kind())||"HOT_DASHBOARD".equals(p.kind()))?"#{p.completeWindow}":"#{p.complete30}";
|
||||||
return "WITH candidates AS ("+keys(p)+"), profiles AS (SELECT c.customer_code,c.sku_code,COALESCE(i.product_name,s.product_name) product_name,"
|
return "WITH candidates AS ("+keys(p)+"), profiles AS (SELECT c.customer_code,c.sku_code,COALESCE(i.product_name,s.product_name) product_name,"
|
||||||
+"CASE WHEN i.quantity_unit='PCS' THEN 1 ELSE 0 END stock_unit_valid,"
|
+"CASE WHEN i.quantity_unit='PCS' THEN 1 ELSE 0 END stock_unit_valid,"
|
||||||
+"CASE WHEN s.sku_code IS NULL OR s.quantity_unit='PCS' THEN 1 ELSE 0 END sales_unit_valid,"
|
+"CASE WHEN s.sku_code IS NULL OR s.quantity_unit='PCS' THEN 1 ELSE 0 END sales_unit_valid,"
|
||||||
@@ -36,11 +36,12 @@ public final class AnalyticsInventorySql {
|
|||||||
+"FROM ll_selfop_inventory_snapshot WHERE "+(dashboard&&!p.stockFresh()?"1=0 /* dashboard unused stock */ AND ":"")
|
+"FROM ll_selfop_inventory_snapshot WHERE "+(dashboard&&!p.stockFresh()?"1=0 /* dashboard unused stock */ AND ":"")
|
||||||
+"snapshot_run_id=#{p.inventoryRun} AND"+scope(p)+" GROUP BY customer_code,sku_code),"
|
+"snapshot_run_id=#{p.inventoryRun} AND"+scope(p)+" GROUP BY customer_code,sku_code),"
|
||||||
+(stockFirst?stockPageKeys():"")
|
+(stockFirst?stockPageKeys():"")
|
||||||
+salesCte(p,stockFirst,dashboard&&!p.complete30())
|
+salesCte(p,stockFirst,dashboard&&!"HOT_DASHBOARD".equals(p.kind())&&!p.complete30())
|
||||||
+" values_by_sku AS (SELECT c.customer_code,c.sku_code,c.product_name,c.unit_valid,"
|
+" values_by_sku AS (SELECT c.customer_code,c.sku_code,c.product_name,c.unit_valid,"
|
||||||
+"CASE WHEN c.sales_evidence IS NULL THEN c.stock_evidence WHEN c.stock_evidence IS NULL THEN c.sales_evidence ELSE LEAST(c.sales_evidence,c.stock_evidence) END evidence_at_utc,"
|
+"CASE WHEN c.sales_evidence IS NULL THEN c.stock_evidence WHEN c.stock_evidence IS NULL THEN c.sales_evidence ELSE LEAST(c.sales_evidence,c.stock_evidence) END evidence_at_utc,"
|
||||||
+stockValue("physical_quantity")+","+stockValue("available_quantity")+","+stockValue("frozen_quantity")+","+stockValue("transit_quantity")+","+stockValue("defective_physical_quantity")+",st.observed_at_utc,"
|
+stockValue("physical_quantity")+","+stockValue("available_quantity")+","+stockValue("frozen_quantity")+","+stockValue("transit_quantity")+","+stockValue("defective_physical_quantity")+",st.observed_at_utc,"
|
||||||
+"CASE WHEN c.sales_unit_valid=1 AND COALESCE(s.unit_valid,1)=1 AND "+complete+"=1 THEN COALESCE(s.observed_quantity,0) END owner_outbound,"
|
+("HOT_DASHBOARD".equals(p.kind())?"CASE WHEN c.sales_unit_valid=1 AND COALESCE(s.unit_valid,1)=1 AND #{p.salesKnown}=1 THEN CASE WHEN #{p.completeWindow}=1 THEN COALESCE(s.observed_quantity,0) ELSE s.observed_quantity END END owner_outbound,":
|
||||||
|
"CASE WHEN c.sales_unit_valid=1 AND COALESCE(s.unit_valid,1)=1 AND "+complete+"=1 THEN COALESCE(s.observed_quantity,0) END owner_outbound,")
|
||||||
+"CASE WHEN c.sales_unit_valid=1 AND COALESCE(s.unit_valid,1)=1 AND #{p.salesKnown}=1 THEN s.observed_quantity END observed_outbound,s.order_count "
|
+"CASE WHEN c.sales_unit_valid=1 AND COALESCE(s.unit_valid,1)=1 AND #{p.salesKnown}=1 THEN s.observed_quantity END observed_outbound,s.order_count "
|
||||||
+"FROM profiles c "+(stockFirst?"JOIN stock_paged pk ON pk.customer_code=c.customer_code AND pk.sku_code=c.sku_code ":"")
|
+"FROM profiles c "+(stockFirst?"JOIN stock_paged pk ON pk.customer_code=c.customer_code AND pk.sku_code=c.sku_code ":"")
|
||||||
+"LEFT JOIN stock st ON st.customer_code=c.customer_code AND st.sku_code=c.sku_code LEFT JOIN sales s ON s.customer_code=c.customer_code AND s.sku_code=c.sku_code),"
|
+"LEFT JOIN stock st ON st.customer_code=c.customer_code AND st.sku_code=c.sku_code LEFT JOIN sales s ON s.customer_code=c.customer_code AND s.sku_code=c.sku_code),"
|
||||||
|
|||||||
+2
@@ -30,6 +30,8 @@ public record AnalyticsQuery(String sourceAccount, String warehouseCode, String
|
|||||||
if(chart){
|
if(chart){
|
||||||
if(windowDays!=null)throw new BizException("SKU周期查询不接受windowDays");
|
if(windowDays!=null)throw new BizException("SKU周期查询不接受windowDays");
|
||||||
if(bucket!=null&&!"DAY".equals(bucket))throw new BizException("仅支持DAY日粒度");
|
if(bucket!=null&&!"DAY".equals(bucket))throw new BizException("仅支持DAY日粒度");
|
||||||
|
}else if(hot&&"CUSTOM".equals(rangePreset)){
|
||||||
|
if(startDate==null||endDate==null||windowDays!=null||bucket!=null)throw new BizException("自定义热销窗口须提供起止日期,不接受windowDays或bucket");
|
||||||
}else{
|
}else{
|
||||||
if(rangePreset!=null||startDate!=null||endDate!=null||bucket!=null)throw new BizException("指标不接受图表周期参数");
|
if(rangePreset!=null||startDate!=null||endDate!=null||bucket!=null)throw new BizException("指标不接受图表周期参数");
|
||||||
if(hot?(rankingDays()!=7&&rankingDays()!=30):(windowDays!=null))throw new BizException("热销仅支持7/30天;其他指标固定30天");
|
if(hot?(rankingDays()!=7&&rankingDays()!=30):(windowDays!=null))throw new BizException("热销仅支持7/30天;其他指标固定30天");
|
||||||
|
|||||||
+1
-1
@@ -199,7 +199,7 @@ public class AnalyticsQueryService {
|
|||||||
LocalDate anchor=asOf.atZone(BUSINESS_ZONE).toLocalDate();
|
LocalDate anchor=asOf.atZone(BUSINESS_ZONE).toLocalDate();
|
||||||
SalesRangePolicy.Range range;
|
SalesRangePolicy.Range range;
|
||||||
try{
|
try{
|
||||||
range=inventoryOptions!=null?InventoryWindowPolicy.range(inventoryOptions,anchor):chart?SalesRangePolicy.resolve(q.rangePreset()==null?"TODAY":q.rangePreset(),q.startDate(),q.endDate(),
|
range=inventoryOptions!=null?InventoryWindowPolicy.range(inventoryOptions,anchor):(chart||(hot&&"CUSTOM".equals(q.rangePreset())))?SalesRangePolicy.resolve(q.rangePreset()==null?"TODAY":q.rangePreset(),q.startDate(),q.endDate(),
|
||||||
q.bucket()==null?"DAY":q.bucket(),asOf,BUSINESS_ZONE,maxQueryDays):
|
q.bucket()==null?"DAY":q.bucket(),asOf,BUSINESS_ZONE,maxQueryDays):
|
||||||
new SalesRangePolicy.Range(anchor,anchor.minusDays(hot?q.rankingDays():30),anchor,hot?q.rankingDays():30,
|
new SalesRangePolicy.Range(anchor,anchor.minusDays(hot?q.rankingDays():30),anchor,hot?q.rankingDays():30,
|
||||||
anchor.minusDays(hot?q.rankingDays():30).atStartOfDay(BUSINESS_ZONE).toInstant(),anchor.atStartOfDay(BUSINESS_ZONE).toInstant());
|
anchor.minusDays(hot?q.rankingDays():30).atStartOfDay(BUSINESS_ZONE).toInstant(),anchor.atStartOfDay(BUSINESS_ZONE).toInstant());
|
||||||
|
|||||||
+8
-8
@@ -209,18 +209,18 @@ public final class AnalyticsReadSql {
|
|||||||
return "WITH candidates AS (SELECT customer_code,sku_code FROM ll_selfop_sku_profile WHERE profile_run_id=#{p.salesRun} AND"+KEY_FILTER
|
return "WITH candidates AS (SELECT customer_code,sku_code FROM ll_selfop_sku_profile WHERE profile_run_id=#{p.salesRun} AND"+KEY_FILTER
|
||||||
+" UNION SELECT customer_code,sku_code FROM ll_selfop_sku_profile WHERE profile_run_id=#{p.inventoryRun} AND"+KEY_FILTER+"),"
|
+" UNION SELECT customer_code,sku_code FROM ll_selfop_sku_profile WHERE profile_run_id=#{p.inventoryRun} AND"+KEY_FILTER+"),"
|
||||||
+" metric_sales AS (SELECT customer_code,sku_code,"
|
+" metric_sales AS (SELECT customer_code,sku_code,"
|
||||||
+"SUM(CASE WHEN business_date>=#{p.windowFrom} AND business_date<#{p.anchor} THEN outbound_quantity ELSE 0 END) window_quantity,"
|
+"SUM(CASE WHEN business_date>=#{p.from} AND business_date<#{p.until} THEN outbound_quantity ELSE 0 END) window_quantity,"
|
||||||
+"SUM(CASE WHEN business_date>=#{p.windowFrom} AND business_date<#{p.anchor} THEN outbound_order_count ELSE 0 END) window_orders,"
|
+"SUM(CASE WHEN business_date>=#{p.from} AND business_date<#{p.until} THEN outbound_order_count ELSE 0 END) window_orders,"
|
||||||
+"SUM(CASE WHEN business_date<#{p.anchor} THEN outbound_quantity ELSE 0 END) sales30_quantity,"
|
+"SUM(CASE WHEN business_date>=TIMESTAMPADD(DAY,-30,#{p.anchor}) AND business_date<#{p.anchor} THEN outbound_quantity ELSE 0 END) sales30_quantity,"
|
||||||
+"SUM(CASE WHEN business_date=#{p.anchor} THEN outbound_quantity END) today_quantity,"
|
+"SUM(CASE WHEN business_date=#{p.anchor} THEN outbound_quantity END) today_quantity,"
|
||||||
+"MIN(CASE WHEN business_date>=#{p.windowFrom} AND business_date<#{p.anchor} THEN CASE WHEN quantity_unit='PCS' AND outbound_quantity>=0 THEN 1 ELSE 0 END ELSE 1 END) window_valid,"
|
+"MIN(CASE WHEN business_date>=#{p.from} AND business_date<#{p.until} THEN CASE WHEN quantity_unit='PCS' AND outbound_quantity>=0 THEN 1 ELSE 0 END ELSE 1 END) window_valid,"
|
||||||
+"MIN(CASE WHEN business_date<#{p.anchor} THEN CASE WHEN quantity_unit='PCS' AND outbound_quantity>=0 THEN 1 ELSE 0 END ELSE 1 END) thirty_valid,"
|
+"MIN(CASE WHEN business_date>=TIMESTAMPADD(DAY,-30,#{p.anchor}) AND business_date<#{p.anchor} THEN CASE WHEN quantity_unit='PCS' AND outbound_quantity>=0 THEN 1 ELSE 0 END ELSE 1 END) thirty_valid,"
|
||||||
+"MIN(CASE WHEN business_date=#{p.anchor} THEN CASE WHEN quantity_unit='PCS' AND outbound_quantity>=0 THEN 1 ELSE 0 END ELSE 1 END) today_valid "
|
+"MIN(CASE WHEN business_date=#{p.anchor} THEN CASE WHEN quantity_unit='PCS' AND outbound_quantity>=0 THEN 1 ELSE 0 END ELSE 1 END) today_valid "
|
||||||
+"FROM ll_selfop_sku_daily_sales WHERE sales_run_id=#{p.salesRun} AND"+KEY_FILTER
|
+"FROM ll_selfop_sku_daily_sales WHERE sales_run_id=#{p.salesRun} AND"+KEY_FILTER
|
||||||
+"AND business_date>=TIMESTAMPADD(DAY,-30,#{p.anchor}) AND business_date<TIMESTAMPADD(DAY,1,#{p.anchor}) GROUP BY customer_code,sku_code),"
|
+"AND business_date>=LEAST(#{p.from},TIMESTAMPADD(DAY,-30,#{p.anchor})) AND business_date<GREATEST(#{p.until},TIMESTAMPADD(DAY,1,#{p.anchor})) GROUP BY customer_code,sku_code),"
|
||||||
+" result AS (SELECT c.customer_code,c.sku_code,CASE WHEN "+windowValid+" THEN 1 ELSE 0 END unit_valid,"
|
+" result AS (SELECT c.customer_code,c.sku_code,CASE WHEN "+windowValid+" THEN 1 ELSE 0 END unit_valid,"
|
||||||
+"CASE WHEN "+windowValid+" AND #{p.completeWindow}=1 THEN COALESCE(m.window_quantity,0) END window_quantity,"
|
+"CASE WHEN "+windowValid+" AND #{p.salesKnown}=1 THEN CASE WHEN #{p.completeWindow}=1 THEN COALESCE(m.window_quantity,0) ELSE m.window_quantity END END window_quantity,"
|
||||||
+"CASE WHEN "+windowValid+" AND #{p.completeWindow}=1 THEN COALESCE(m.window_orders,0) END window_sku_order_count,"
|
+"CASE WHEN "+windowValid+" AND #{p.salesKnown}=1 THEN CASE WHEN #{p.completeWindow}=1 THEN COALESCE(m.window_orders,0) ELSE m.window_orders END END window_sku_order_count,"
|
||||||
+"CASE WHEN "+thirtyValid+" AND #{p.complete30}=1 THEN COALESCE(m.sales30_quantity,0) END sales30_quantity,"
|
+"CASE WHEN "+thirtyValid+" AND #{p.complete30}=1 THEN COALESCE(m.sales30_quantity,0) END sales30_quantity,"
|
||||||
+"CASE WHEN "+todayValid+" THEN 1 ELSE 0 END today_unit_valid,"
|
+"CASE WHEN "+todayValid+" THEN 1 ELSE 0 END today_unit_valid,"
|
||||||
+"CASE WHEN "+todayValid+" AND #{p.salesKnown}=1 THEN m.today_quantity END today_quantity "
|
+"CASE WHEN "+todayValid+" AND #{p.salesKnown}=1 THEN m.today_quantity END today_quantity "
|
||||||
|
|||||||
+22
-5
@@ -30,6 +30,13 @@ public class DashboardQueryService {
|
|||||||
var trendPoints=DashboardSalesPolicy.points(c,snapshot.sourceKnown(),c.rangeStartDate(),c.rangeEndDateExclusive(),days);
|
var trendPoints=DashboardSalesPolicy.points(c,snapshot.sourceKnown(),c.rangeStartDate(),c.rangeEndDateExclusive(),days);
|
||||||
var trendSummary=SalesBucketPolicy.aggregate(trendPoints);
|
var trendSummary=SalesBucketPolicy.aggregate(trendPoints);
|
||||||
var fixedPoints=DashboardSalesPolicy.points(c,snapshot.sourceKnown(),fixedFrom,fixedUntil,days);var fixed=SalesBucketPolicy.aggregate(fixedPoints);
|
var fixedPoints=DashboardSalesPolicy.points(c,snapshot.sourceKnown(),fixedFrom,fixedUntil,days);var fixed=SalesBucketPolicy.aggregate(fixedPoints);
|
||||||
|
Map<String,Object> hotStats=stats;List<DashboardReadMapper.Ranked> hotRanked=ranked;
|
||||||
|
if(!c.rangeStartDate().equals(fixedFrom)||!c.rangeEndDateExclusive().equals(fixedUntil)||!base.complete30()){
|
||||||
|
var hotRequest=new AnalyticsSqlRequest(base.account(),base.warehouse(),null,null,base.salesRun(),base.inventoryRun(),c.rangeStartDate(),c.rangeEndDateExclusive(),base.anchor(),fixedFrom,
|
||||||
|
base.completeFrom(),base.completeUntil(),base.coveredFrom(),base.coveredUntil(),base.salesKnown(),base.formalSales(),base.completeWindow(),base.complete30(),base.stockFresh(),0,10,"HOT_DASHBOARD");
|
||||||
|
var hotReport=mapper.report(hotRequest,o).stream().map(DashboardQueryService::normalize).toList();
|
||||||
|
hotStats=hotReport.get(0);hotRanked=hotReport.stream().filter(row->row.get("rank_code")!=null).map(DashboardQueryService::ranked).toList();
|
||||||
|
}
|
||||||
var salesSummary=new AnalyticsResponses.SalesSummary(q.sourceAccount(),q.warehouseCode(),null,null,null,"PCS",trendSummary.outboundQuantity(),trendSummary.observedQuantity(),
|
var salesSummary=new AnalyticsResponses.SalesSummary(q.sourceAccount(),q.warehouseCode(),null,null,null,"PCS",trendSummary.outboundQuantity(),trendSummary.observedQuantity(),
|
||||||
trendSummary.dataStatus(),trendSummary.reasonCodes(),trendSummary.completeDays(),trendSummary.partialDays(),trendSummary.unknownDays());
|
trendSummary.dataStatus(),trendSummary.reasonCodes(),trendSummary.completeDays(),trendSummary.partialDays(),trendSummary.unknownDays());
|
||||||
var points=trendPoints.stream().map(t->new AnalyticsResponses.Point(t.date(),t.outboundQuantity(),t.observedQuantity(),t.dataStatus(),t.reasonCodes(),"PCS",t.provedFrom(),t.provedUntil())).toList();
|
var points=trendPoints.stream().map(t->new AnalyticsResponses.Point(t.date(),t.outboundQuantity(),t.observedQuantity(),t.dataStatus(),t.reasonCodes(),"PCS",t.provedFrom(),t.provedUntil())).toList();
|
||||||
@@ -44,10 +51,18 @@ public class DashboardQueryService {
|
|||||||
Instant freshUntil=contextService.inventoryFreshUntil(snapshot),drilldownUntil=CoverageDistributionPolicy.expiresAt(c,freshUntil);
|
Instant freshUntil=contextService.inventoryFreshUntil(snapshot),drilldownUntil=CoverageDistributionPolicy.expiresAt(c,freshUntil);
|
||||||
return new DashboardResponses.Dashboard(new DashboardResponses.Context(c,fixedFrom,fixedUntil,30,freshUntil,drilldownUntil),kpis,
|
return new DashboardResponses.Dashboard(new DashboardResponses.Context(c,fixedFrom,fixedUntil,30,freshUntil,drilldownUntil),kpis,
|
||||||
new DashboardResponses.SalesTrend(salesSummary,points),distribution(stats,inventoryAvailable,c),
|
new DashboardResponses.SalesTrend(salesSummary,points),distribution(stats,inventoryAvailable,c),
|
||||||
ranking("HOT",stats,ranked,base.salesRun()>0,fixedFrom,fixedUntil,c),
|
hotRanking(ranking("HOT",hotStats,hotRanked,base.salesRun()>0,c.rangeStartDate(),c.rangeEndDateExclusive(),c),trendSummary.dataStatus()),
|
||||||
ranking("LONG",stats,ranked,inventoryAvailable,fixedFrom,fixedUntil,c),
|
ranking("LONG",stats,ranked,inventoryAvailable,fixedFrom,fixedUntil,c),
|
||||||
ranking("NO_OUTBOUND",stats,ranked,inventoryAvailable,fixedFrom,fixedUntil,c));
|
ranking("NO_OUTBOUND",stats,ranked,inventoryAvailable,fixedFrom,fixedUntil,c));
|
||||||
}
|
}
|
||||||
|
private static DashboardResponses.Ranking hotRanking(DashboardResponses.Ranking ranking,String windowStatus){
|
||||||
|
if("COMPLETE".equals(windowStatus))return ranking;
|
||||||
|
var items=ranking.items().stream().map(i->new DashboardResponses.Item(i.customerCode(),i.sku(),i.productName(),i.value(),i.unit(),i.windowOutboundQuantity(),i.skuOutboundOrderCount(),
|
||||||
|
i.physicalQuantity(),i.availableQuantity(),i.frozenQuantity(),i.defectivePhysicalQuantity(),null,null)).toList();
|
||||||
|
var reasons=new LinkedHashSet<>(ranking.reasonCodes());reasons.add("SALES_WINDOW_INCOMPLETE");
|
||||||
|
return new DashboardResponses.Ranking(ranking.windowStartDate(),ranking.windowEndDateExclusive(),ranking.calculationWindowDays(),ranking.eligibleCount(),ranking.unassessableCount(),items,
|
||||||
|
items.isEmpty()&&"UNKNOWN".equals(windowStatus)?"UNKNOWN":"PARTIAL",List.copyOf(reasons));
|
||||||
|
}
|
||||||
private static DashboardResponses.Metric stockMetric(Map<String,Object> s,String knownField,String unknownField,String unit,boolean available,boolean fresh,AnalyticsResponses.Context c){
|
private static DashboardResponses.Metric stockMetric(Map<String,Object> s,String knownField,String unknownField,String unit,boolean available,boolean fresh,AnalyticsResponses.Context c){
|
||||||
if(!available)return new DashboardResponses.Metric(null,null,unit,"UNKNOWN",null,List.of("INVENTORY_UNAVAILABLE"),null,null);
|
if(!available)return new DashboardResponses.Metric(null,null,unit,"UNKNOWN",null,List.of("INVENTORY_UNAVAILABLE"),null,null);
|
||||||
long unknown=count(s,unknownField);BigDecimal known=decimal(s,knownField);
|
long unknown=count(s,unknownField);BigDecimal known=decimal(s,knownField);
|
||||||
@@ -72,6 +87,7 @@ public class DashboardQueryService {
|
|||||||
available?count(s,"excluded_zero_skus"):null,List.copyOf(buckets),status,codes,available?Map.copyOf(reasons):Map.of());
|
available?count(s,"excluded_zero_skus"):null,List.copyOf(buckets),status,codes,available?Map.copyOf(reasons):Map.of());
|
||||||
}
|
}
|
||||||
private static DashboardResponses.Ranking ranking(String kind,Map<String,Object> s,List<DashboardReadMapper.Ranked> rows,boolean available,LocalDate from,LocalDate until,AnalyticsResponses.Context c){
|
private static DashboardResponses.Ranking ranking(String kind,Map<String,Object> s,List<DashboardReadMapper.Ranked> rows,boolean available,LocalDate from,LocalDate until,AnalyticsResponses.Context c){
|
||||||
|
int windowDays=(int)java.time.temporal.ChronoUnit.DAYS.between(from,until);
|
||||||
String prefix=switch(kind){case "HOT"->"hot";case "LONG"->"long";default->"no_outbound";};
|
String prefix=switch(kind){case "HOT"->"hot";case "LONG"->"long";default->"no_outbound";};
|
||||||
List<DashboardReadMapper.Ranked> selected=rows.stream().filter(r->kind.equals(r.rankCode())).sorted(Comparator
|
List<DashboardReadMapper.Ranked> selected=rows.stream().filter(r->kind.equals(r.rankCode())).sorted(Comparator
|
||||||
.comparing(DashboardReadMapper.Ranked::mainValue,Comparator.reverseOrder())
|
.comparing(DashboardReadMapper.Ranked::mainValue,Comparator.reverseOrder())
|
||||||
@@ -79,12 +95,12 @@ public class DashboardQueryService {
|
|||||||
.thenComparing(DashboardReadMapper.Ranked::skuCode).thenComparing(DashboardReadMapper.Ranked::customerCode)).toList();
|
.thenComparing(DashboardReadMapper.Ranked::skuCode).thenComparing(DashboardReadMapper.Ranked::customerCode)).toList();
|
||||||
var items=available?selected.stream().map(r->new DashboardResponses.Item(r.customerCode(),r.skuCode(),r.productName(),r.mainValue(),"LONG".equals(kind)?"DAY":"PCS",
|
var items=available?selected.stream().map(r->new DashboardResponses.Item(r.customerCode(),r.skuCode(),r.productName(),r.mainValue(),"LONG".equals(kind)?"DAY":"PCS",
|
||||||
"HOT".equals(kind)?r.ownerOutbound():r.outboundQuantity30d(),r.orderCount(),r.physicalQuantity(),r.availableQuantity(),r.frozenQuantity(),r.defectivePhysicalQuantity(),
|
"HOT".equals(kind)?r.ownerOutbound():r.outboundQuantity30d(),r.orderCount(),r.physicalQuantity(),r.availableQuantity(),r.frozenQuantity(),r.defectivePhysicalQuantity(),
|
||||||
divide30("HOT".equals(kind)?r.ownerOutbound():r.outboundQuantity30d()),r.coverageDays())).toList():List.<DashboardResponses.Item>of();
|
divide("HOT".equals(kind)?r.ownerOutbound():r.outboundQuantity30d(),windowDays),"HOT".equals(kind)?null:r.coverageDays())).toList():List.<DashboardResponses.Item>of();
|
||||||
long unknown=count(s,prefix+"_unknown");String status=!available?"UNKNOWN":unknown>0?"PARTIAL":"COMPLETE";
|
long unknown=count(s,prefix+"_unknown");String status=!available?"UNKNOWN":unknown>0?"PARTIAL":"COMPLETE";
|
||||||
List<String> reasons=!available?List.of("HOT".equals(kind)?"SALES_UNAVAILABLE":"INVENTORY_UNAVAILABLE"):unknown>0?List.of("UNASSESSABLE_SKUS"):List.of();
|
List<String> reasons=!available?List.of("HOT".equals(kind)?"SALES_UNAVAILABLE":"INVENTORY_UNAVAILABLE"):unknown>0?List.of("UNASSESSABLE_SKUS"):List.of();
|
||||||
if(available&&count(s,prefix+"_eligible")==0&&unknown==0)reasons=List.of("NO_ELIGIBLE_SKUS");
|
if(available&&count(s,prefix+"_eligible")==0&&unknown==0)reasons=List.of("NO_ELIGIBLE_SKUS");
|
||||||
if(available&&"HOT".equals(kind)&&!"VALID".equals(c.salesStatus())){status="UNKNOWN";reasons=List.of("SALES_"+c.salesStatus());}
|
if(available&&"HOT".equals(kind)&&!Set.of("VALID","STALE").contains(c.salesStatus())){status="UNKNOWN";reasons=List.of("SALES_"+c.salesStatus());}
|
||||||
return new DashboardResponses.Ranking(from,until,30,available?count(s,prefix+"_eligible"):null,available?unknown:null,items,status,reasons);
|
return new DashboardResponses.Ranking(from,until,windowDays,available?count(s,prefix+"_eligible"):null,available?unknown:null,items,status,reasons);
|
||||||
}
|
}
|
||||||
private static Map<String,Object> normalize(Map<String,Object> source){Map<String,Object> result=new HashMap<>();if(source!=null)source.forEach((key,value)->result.put(key.toLowerCase(Locale.ROOT),value));return result;}
|
private static Map<String,Object> normalize(Map<String,Object> source){Map<String,Object> result=new HashMap<>();if(source!=null)source.forEach((key,value)->result.put(key.toLowerCase(Locale.ROOT),value));return result;}
|
||||||
static DashboardReadMapper.Ranked ranked(Map<String,Object> row){
|
static DashboardReadMapper.Ranked ranked(Map<String,Object> row){
|
||||||
@@ -95,5 +111,6 @@ public class DashboardQueryService {
|
|||||||
}
|
}
|
||||||
private static BigDecimal decimal(Map<String,Object> source,String key){Object value=source.get(key);return value==null?null:value instanceof BigDecimal decimal?decimal:new BigDecimal(value.toString());}
|
private static BigDecimal decimal(Map<String,Object> source,String key){Object value=source.get(key);return value==null?null:value instanceof BigDecimal decimal?decimal:new BigDecimal(value.toString());}
|
||||||
private static long count(Map<String,Object> source,String key){BigDecimal value=decimal(source,key);return value==null?0:value.longValueExact();}
|
private static long count(Map<String,Object> source,String key){BigDecimal value=decimal(source,key);return value==null?0:value.longValueExact();}
|
||||||
private static BigDecimal divide30(BigDecimal value){return value==null?null:value.divide(BigDecimal.valueOf(30),12,RoundingMode.HALF_UP);}
|
private static BigDecimal divide30(BigDecimal value){return divide(value,30);}
|
||||||
|
private static BigDecimal divide(BigDecimal value,int days){return value==null||days==0?null:value.divide(BigDecimal.valueOf(days),12,RoundingMode.HALF_UP);}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -8,6 +8,10 @@ public final class DashboardReadSql {
|
|||||||
private DashboardReadSql(){}
|
private DashboardReadSql(){}
|
||||||
public static String report(Map<String,Object> args){
|
public static String report(Map<String,Object> args){
|
||||||
var p=(AnalyticsSqlRequest)args.get("p");
|
var p=(AnalyticsSqlRequest)args.get("p");
|
||||||
|
if("HOT_DASHBOARD".equals(p.kind()))return AnalyticsInventorySql.typed(AnalyticsInventorySql.dashboardCte(p)
|
||||||
|
+", hot_statistics AS (SELECT COUNT(CASE WHEN owner_outbound>0 THEN 1 END) hot_eligible,COUNT(CASE WHEN owner_outbound IS NULL THEN 1 END) hot_unknown FROM result)"
|
||||||
|
+", hot_rank AS (SELECT * FROM result WHERE owner_outbound>0 ORDER BY owner_outbound DESC,order_count DESC,sku_code ASC,customer_code ASC LIMIT 10)"
|
||||||
|
+", hot_rows AS ("+select("HOT","owner_outbound","hot_rank")+") SELECT s.*,r.* FROM hot_statistics s LEFT JOIN hot_rows r ON 1=1");
|
||||||
// Only dashboard outputs are considered here. Without complete30 all sales-dependent
|
// Only dashboard outputs are considered here. Without complete30 all sales-dependent
|
||||||
// rankings are empty; observed sales still come from the independent daily query.
|
// rankings are empty; observed sales still come from the independent daily query.
|
||||||
// Stale stock quantities are already masked to NULL. Keep both profile versions so
|
// Stale stock quantities are already masked to NULL. Keep both profile versions so
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ public final class DashboardResponses {
|
|||||||
public record Bucket(String bucketCode,Long skuCount,BigDecimal knownPhysicalQuantity,Long unknownPhysicalSkuCount){}
|
public record Bucket(String bucketCode,Long skuCount,BigDecimal knownPhysicalQuantity,Long unknownPhysicalSkuCount){}
|
||||||
public record Distribution(String bucketRuleVersion,Long candidateSkuCount,Long excludedZeroPhysicalSkuCount,
|
public record Distribution(String bucketRuleVersion,Long candidateSkuCount,Long excludedZeroPhysicalSkuCount,
|
||||||
List<Bucket> buckets,String dataStatus,List<String> reasonCodes,Map<String,Long> unknownReasons){}
|
List<Bucket> buckets,String dataStatus,List<String> reasonCodes,Map<String,Long> unknownReasons){}
|
||||||
public record Item(String customerCode,String sku,String productName,BigDecimal value,String unit,BigDecimal outboundQuantity30d,
|
public record Item(String customerCode,String sku,String productName,BigDecimal value,String unit,BigDecimal windowOutboundQuantity,
|
||||||
Long skuOutboundOrderCount,BigDecimal physicalQuantity,BigDecimal availableQuantity,BigDecimal frozenQuantity,
|
Long skuOutboundOrderCount,BigDecimal physicalQuantity,BigDecimal availableQuantity,BigDecimal frozenQuantity,
|
||||||
BigDecimal defectivePhysicalQuantity,BigDecimal averageDailyOutboundQuantity,BigDecimal inventoryCoverageDays){}
|
BigDecimal defectivePhysicalQuantity,BigDecimal averageDailyOutboundQuantity,BigDecimal inventoryCoverageDays){}
|
||||||
public record Ranking(LocalDate windowStartDate,LocalDate windowEndDateExclusive,int calculationWindowDays,Long eligibleCount,
|
public record Ranking(LocalDate windowStartDate,LocalDate windowEndDateExclusive,int calculationWindowDays,Long eligibleCount,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ info:
|
|||||||
默认maxQueryDays366,粒度DAY。正式outboundQuantity与observedQuantity独立,全部未知仍为空。
|
默认maxQueryDays366,粒度DAY。正式outboundQuantity与observedQuantity独立,全部未知仍为空。
|
||||||
每日COMPLETE仅证明全天覆盖及有效日行或可靠SKU存在;今日/部分日正式量空,观察量可用,未知日期保持空。
|
每日COMPLETE仅证明全天覆盖及有效日行或可靠SKU存在;今日/部分日正式量空,观察量可用,未知日期保持空。
|
||||||
SKU区间正式量仅全部日完整才输出;observedQuantity为可靠点合计,完整/部分/未知日数独立。
|
SKU区间正式量仅全部日完整才输出;observedQuantity为可靠点合计,完整/部分/未知日数独立。
|
||||||
切换图表不改变完整7/30日榜单及30日日均分母;不触发领星请求。
|
热销榜跟随图表所选日期,以已采集有效出库量排序;覆盖不足标记PARTIAL,不改变30日日均分母;不触发领星请求。
|
||||||
运行接口不受经营context限制。动态库存时效、重建和修订待核对独立返回。
|
运行接口不受经营context限制。动态库存时效、重建和修订待核对独立返回。
|
||||||
阶段进度区分ACCOUNT_SHARED/WAREHOUSE及TASK_CUMULATIVE/ATTEMPT_ONLY,不伪造总百分比。
|
阶段进度区分ACCOUNT_SHARED/WAREHOUSE及TASK_CUMULATIVE/ATTEMPT_ONLY,不伪造总百分比。
|
||||||
total未知为空、计数不能跨PAGE/ORDER/INVENTORY_ROW/BUSINESS_KEY/SKU/BATCH单位求和,NONE为非计数阶段且completed/total为空、totalKnown=false;阶段检查点与心跳分开。
|
total未知为空、计数不能跨PAGE/ORDER/INVENTORY_ROW/BUSINESS_KEY/SKU/BATCH单位求和,NONE为非计数阶段且completed/total为空、totalKnown=false;阶段检查点与心跳分开。
|
||||||
@@ -26,7 +26,7 @@ paths:
|
|||||||
summary: 单仓固定版本经营总览
|
summary: 单仓固定版本经营总览
|
||||||
description: >-
|
description: >-
|
||||||
平台独立只读权限platform:self-operated-analytics:list。一次一致快照固定销量和库存版本。
|
平台独立只读权限platform:self-operated-analytics:list。一次一致快照固定销量和库存版本。
|
||||||
默认LAST_15_DAYS仅改变趋势及区间出库,日均、互斥分桶和三个Top10固定至昨日30完整日。
|
默认LAST_15_DAYS同时用于趋势、区间出库和热销Top10;热销按已采集有效量排序并标记覆盖状态。日均、互斥分桶及两个库存Top10固定至昨日30完整日。
|
||||||
不接受货主、SKU、分页、windowDays、topN或其他额外筛选。仓级趋势不套单SKU观察期。
|
不接受货主、SKU、分页、windowDays、topN或其他额外筛选。仓级趋势不套单SKU观察期。
|
||||||
库存严格下钻须携带原版本和drilldownFrom=DASHBOARD;截止时间由服务端根据库存真实观测重算。
|
库存严格下钻须携带原版本和drilldownFrom=DASHBOARD;截止时间由服务端根据库存真实观测重算。
|
||||||
parameters:
|
parameters:
|
||||||
@@ -120,8 +120,8 @@ paths:
|
|||||||
'403': {description: 非平台身份或缺少独立只读权限}
|
'403': {description: 非平台身份或缺少独立只读权限}
|
||||||
/api/platform/v1/last-leg/self-operated-analytics/hot-skus:
|
/api/platform/v1/last-leg/self-operated-analytics/hot-skus:
|
||||||
get:
|
get:
|
||||||
summary: 完整7/30日热销Top100
|
summary: 所选时段已采集热销Top100
|
||||||
description: '独立截至queryAsOf昨日的完整窗口,数量大于零,数量降序、SKU单数降序、SKU及货主升序,先Top100再分页;当前缺货仍可入榜。 权限platform:self-operated-analytics:list。'
|
description: '支持CUSTOM起止日期(含截止日),与总览所选时段一致;未传日期时仍使用windowDays的7/30日窗口。按已采集有效数量排序,覆盖不足由context及coverageStatus提示,不把未知当零。数量大于零,数量降序、SKU单数降序、SKU及货主升序,先Top100再分页;当前缺货仍可入榜。 权限platform:self-operated-analytics:list。'
|
||||||
parameters:
|
parameters:
|
||||||
- {$ref: '#/components/parameters/sourceAccount'}
|
- {$ref: '#/components/parameters/sourceAccount'}
|
||||||
- {$ref: '#/components/parameters/warehouseCode'}
|
- {$ref: '#/components/parameters/warehouseCode'}
|
||||||
@@ -133,6 +133,9 @@ paths:
|
|||||||
- {$ref: '#/components/parameters/inventoryScopeHash'}
|
- {$ref: '#/components/parameters/inventoryScopeHash'}
|
||||||
- {$ref: '#/components/parameters/queryAsOf'}
|
- {$ref: '#/components/parameters/queryAsOf'}
|
||||||
- {$ref: '#/components/parameters/windowDays'}
|
- {$ref: '#/components/parameters/windowDays'}
|
||||||
|
- {$ref: '#/components/parameters/rangePreset'}
|
||||||
|
- {$ref: '#/components/parameters/startDate'}
|
||||||
|
- {$ref: '#/components/parameters/endDate'}
|
||||||
- {$ref: '#/components/parameters/current'}
|
- {$ref: '#/components/parameters/current'}
|
||||||
- {$ref: '#/components/parameters/size'}
|
- {$ref: '#/components/parameters/size'}
|
||||||
responses:
|
responses:
|
||||||
@@ -476,7 +479,7 @@ components:
|
|||||||
productName: {type: string, nullable: true}
|
productName: {type: string, nullable: true}
|
||||||
value: {type: number}
|
value: {type: number}
|
||||||
unit: {type: string, enum: [PCS, DAY]}
|
unit: {type: string, enum: [PCS, DAY]}
|
||||||
outboundQuantity30d: {type: number, nullable: true}
|
windowOutboundQuantity: {type: number, nullable: true, description: 榜单实际窗口出库量;热销在部分覆盖时为已采集量}
|
||||||
skuOutboundOrderCount: {type: string, nullable: true}
|
skuOutboundOrderCount: {type: string, nullable: true}
|
||||||
physicalQuantity: {type: number, nullable: true}
|
physicalQuantity: {type: number, nullable: true}
|
||||||
availableQuantity: {type: number, nullable: true}
|
availableQuantity: {type: number, nullable: true}
|
||||||
@@ -490,7 +493,7 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
windowStartDate: {type: string, format: date}
|
windowStartDate: {type: string, format: date}
|
||||||
windowEndDateExclusive: {type: string, format: date}
|
windowEndDateExclusive: {type: string, format: date}
|
||||||
calculationWindowDays: {type: integer, enum: [30]}
|
calculationWindowDays: {type: integer, minimum: 1, description: 热销为所选日期天数;库存榜固定30}
|
||||||
eligibleCount: {type: string, nullable: true}
|
eligibleCount: {type: string, nullable: true}
|
||||||
unassessableCount: {type: string, nullable: true}
|
unassessableCount: {type: string, nullable: true}
|
||||||
items: {type: array, maxItems: 10, items: {$ref: '#/components/schemas/DashboardRankingItem'}}
|
items: {type: array, maxItems: 10, items: {$ref: '#/components/schemas/DashboardRankingItem'}}
|
||||||
|
|||||||
+8
@@ -35,6 +35,14 @@ class AnalyticsQueryServiceTest {
|
|||||||
when(mapper.profile(22,"demo","W","C","SKU")).thenReturn(profile("PCS","INVENTORY_OBSERVATION",time(D.minusDays(35))));
|
when(mapper.profile(22,"demo","W","C","SKU")).thenReturn(profile("PCS","INVENTORY_OBSERVATION",time(D.minusDays(35))));
|
||||||
when(mapper.daily(anyLong(),anyString(),anyString(),anyString(),anyString(),any(),any())).thenReturn(List.of());
|
when(mapper.daily(anyLong(),anyString(),anyString(),anyString(),anyString(),any(),any())).thenReturn(List.of());
|
||||||
}
|
}
|
||||||
|
@Test void customHotRangeMatchesSelectedDatesIncludingTodayAndLongHistory(){
|
||||||
|
var q=new AnalyticsQuery("demo","W",null,null,"CUSTOM",D.minusDays(44),D,null,null,1,20,null,null,null,null,null);
|
||||||
|
var result=service.hotSkus(q);
|
||||||
|
var cap=ArgumentCaptor.forClass(AnalyticsSqlRequest.class);verify(mapper).page(cap.capture());
|
||||||
|
assertEquals(D.minusDays(44),cap.getValue().from());assertEquals(D.plusDays(1),cap.getValue().until());
|
||||||
|
assertFalse(cap.getValue().completeWindow());assertEquals(D.minusDays(44),result.context().rangeStartDate());
|
||||||
|
assertThrows(com.baiyue.common.core.exception.BizException.class,()->new AnalyticsQuery("demo","W",null,null,"CUSTOM",D.minusDays(2),D,null,7,1,20,null,null,null,null,null).validate(false,false,true));
|
||||||
|
}
|
||||||
@Test void summaryUsesCountAndBoundPageWithoutLoadingProfilesOrHistory(){
|
@Test void summaryUsesCountAndBoundPageWithoutLoadingProfilesOrHistory(){
|
||||||
when(mapper.count(any())).thenReturn(240L);
|
when(mapper.count(any())).thenReturn(240L);
|
||||||
var response=service.salesSummary(chart("LAST_3_DAYS",null,null,null,null));
|
var response=service.salesSummary(chart("LAST_3_DAYS",null,null,null,null));
|
||||||
|
|||||||
+3
-2
@@ -34,7 +34,7 @@ class AnalyticsReadMapperTest {
|
|||||||
}
|
}
|
||||||
var b=bound(c,"page",Map.of("p",request("HOT")));
|
var b=bound(c,"page",Map.of("p",request("HOT")));
|
||||||
var names=b.getParameterMappings().stream().map(org.apache.ibatis.mapping.ParameterMapping::getProperty).toList();
|
var names=b.getParameterMappings().stream().map(org.apache.ibatis.mapping.ParameterMapping::getProperty).toList();
|
||||||
for(String property:List.of("p.account","p.warehouse","p.customer","p.sku","p.salesRun","p.inventoryRun","p.anchor","p.windowFrom","p.size","p.offset"))assertTrue(names.contains(property),property);
|
for(String property:List.of("p.account","p.warehouse","p.customer","p.sku","p.salesRun","p.inventoryRun","p.anchor","p.from","p.until","p.size","p.offset"))assertTrue(names.contains(property),property);
|
||||||
var summaryNames=bound(c,"page",Map.of("p",request("SUMMARY"))).getParameterMappings().stream()
|
var summaryNames=bound(c,"page",Map.of("p",request("SUMMARY"))).getParameterMappings().stream()
|
||||||
.map(org.apache.ibatis.mapping.ParameterMapping::getProperty).toList();
|
.map(org.apache.ibatis.mapping.ParameterMapping::getProperty).toList();
|
||||||
for(String property:List.of("p.from","p.until"))assertTrue(summaryNames.contains(property),property);
|
for(String property:List.of("p.from","p.until"))assertTrue(summaryNames.contains(property),property);
|
||||||
@@ -83,7 +83,8 @@ class AnalyticsReadMapperTest {
|
|||||||
var p=Map.<String,Object>of("p",request(kind)); String sql=bound(c,"page",p).getSql();
|
var p=Map.<String,Object>of("p",request(kind)); String sql=bound(c,"page",p).getSql();
|
||||||
assertTrue(sql.contains("st.snapshot_run_id=?")); assertTrue(sql.contains("st.source_account=?")); assertTrue(sql.contains("st.warehouse_code=?"));
|
assertTrue(sql.contains("st.snapshot_run_id=?")); assertTrue(sql.contains("st.source_account=?")); assertTrue(sql.contains("st.warehouse_code=?"));
|
||||||
assertTrue(sql.contains("AND st.product_quality=0"));
|
assertTrue(sql.contains("AND st.product_quality=0"));
|
||||||
assertTrue(sql.contains("business_date>=TIMESTAMPADD(DAY,-30,CAST(? AS DATE)) AND business_date<TIMESTAMPADD(DAY,1,CAST(? AS DATE))"));
|
if("HOT".equals(kind))assertTrue(sql.contains("business_date>=LEAST(CAST(? AS DATE),TIMESTAMPADD(DAY,-30,CAST(? AS DATE))) AND business_date<GREATEST(CAST(? AS DATE),TIMESTAMPADD(DAY,1,CAST(? AS DATE)))"));
|
||||||
|
else assertTrue(sql.contains("business_date>=TIMESTAMPADD(DAY,-30,CAST(? AS DATE)) AND business_date<TIMESTAMPADD(DAY,1,CAST(? AS DATE))"));
|
||||||
assertTrue(sql.contains("GROUP BY customer_code,sku_code"));
|
assertTrue(sql.contains("GROUP BY customer_code,sku_code"));
|
||||||
assertTrue(sql.indexOf("GROUP BY customer_code,sku_code")<sql.indexOf("LEFT JOIN ll_selfop_inventory_snapshot"));
|
assertTrue(sql.indexOf("GROUP BY customer_code,sku_code")<sql.indexOf("LEFT JOIN ll_selfop_inventory_snapshot"));
|
||||||
if("HOT".equals(kind))assertTrue(sql.indexOf("LIMIT ? OFFSET ?")<sql.indexOf("LEFT JOIN ll_selfop_inventory_snapshot"));
|
if("HOT".equals(kind))assertTrue(sql.indexOf("LIMIT ? OFFSET ?")<sql.indexOf("LEFT JOIN ll_selfop_inventory_snapshot"));
|
||||||
|
|||||||
+2
-1
@@ -28,10 +28,11 @@ class DashboardQueryServiceTest {
|
|||||||
when(mapper.report(any(),any())).thenReturn(List.of(reportRow));
|
when(mapper.report(any(),any())).thenReturn(List.of(reportRow));
|
||||||
var result=service.dashboard(q);
|
var result=service.dashboard(q);
|
||||||
assertEquals(0,BigDecimal.ONE.compareTo(result.kpis().averageDailyOutboundQuantity30d().value()));
|
assertEquals(0,BigDecimal.ONE.compareTo(result.kpis().averageDailyOutboundQuantity30d().value()));
|
||||||
|
assertEquals(15,result.hotSkus().calculationWindowDays());assertEquals("PARTIAL",result.hotSkus().dataStatus());assertNull(result.hotSkus().items().get(0).averageDailyOutboundQuantity());
|
||||||
assertEquals(200L,result.hotSkus().eligibleCount());assertEquals(1,result.hotSkus().items().size());assertEquals("A",result.hotSkus().items().get(0).customerCode());
|
assertEquals(200L,result.hotSkus().eligibleCount());assertEquals(1,result.hotSkus().items().size());assertEquals("A",result.hotSkus().items().get(0).customerCode());
|
||||||
assertNull(result.coverageDistribution().candidateSkuCount());assertTrue(result.coverageDistribution().buckets().stream().allMatch(b->b.skuCount()==null));
|
assertNull(result.coverageDistribution().candidateSkuCount());assertTrue(result.coverageDistribution().buckets().stream().allMatch(b->b.skuCount()==null));
|
||||||
assertNull(result.context().inventoryDrilldownExpiresAt());assertEquals(day.minusDays(30),result.context().fixedWindowStartDate());
|
assertNull(result.context().inventoryDrilldownExpiresAt());assertEquals(day.minusDays(30),result.context().fixedWindowStartDate());
|
||||||
verify(mapper).daily(any(),any(),any());verify(mapper).report(any(),any());verifyNoMoreInteractions(mapper);
|
verify(mapper).daily(any(),any(),any());verify(mapper,times(2)).report(any(),any());verifyNoMoreInteractions(mapper);
|
||||||
var json=new ObjectMapper().registerModule(new JavaTimeModule()).valueToTree(result);assertTrue(json.path("context").has("salesRunId"));assertFalse(json.path("context").has("versionContext"));
|
var json=new ObjectMapper().registerModule(new JavaTimeModule()).valueToTree(result);assertTrue(json.path("context").has("salesRunId"));assertFalse(json.path("context").has("versionContext"));
|
||||||
assertTrue(DashboardQueryService.class.getAnnotation(Transactional.class).readOnly());
|
assertTrue(DashboardQueryService.class.getAnnotation(Transactional.class).readOnly());
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -39,6 +39,21 @@ class DashboardSqlSemanticTest {
|
|||||||
assertTrue(bound.getSql().endsWith("ORDER BY business_date"));
|
assertTrue(bound.getSql().endsWith("ORDER BY business_date"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Test void selectedRangeHotUsesObservedSalesIncludingTodayAndMatchesTop100DespiteIncompleteCoverage()throws Exception{
|
||||||
|
fixture("RECENT","50","50","3");fixture("OLD","50","50",null);
|
||||||
|
daily("C","RECENT",D,"7","PCS");daily("C","OLD",D.minusDays(20),"100","PCS");
|
||||||
|
var b=request(false,true);
|
||||||
|
var p=new AnalyticsSqlRequest(b.account(),b.warehouse(),null,null,b.salesRun(),b.inventoryRun(),D.minusDays(2),D.plusDays(1),D,D.minusDays(2),
|
||||||
|
b.completeFrom(),b.completeUntil(),b.coveredFrom(),b.coveredUntil(),true,true,false,false,true,0,10,"HOT_DASHBOARD");
|
||||||
|
var rows=query(DashboardReadMapper.class,"report",Map.of("p",p,"o",options()));
|
||||||
|
assertEquals(1,rows.size());assertEquals("RECENT",((Map<?,?>)rows.get(0)).get("SKU_CODE"));
|
||||||
|
assertEquals(0,new BigDecimal("10").compareTo((BigDecimal)((Map<?,?>)rows.get(0)).get("MAIN_VALUE")));
|
||||||
|
var top=new AnalyticsSqlRequest(p.account(),p.warehouse(),null,null,p.salesRun(),p.inventoryRun(),p.from(),p.until(),D,p.from(),
|
||||||
|
p.completeFrom(),p.completeUntil(),p.coveredFrom(),p.coveredUntil(),true,true,false,false,true,0,100,"HOT");
|
||||||
|
var topRows=query(AnalyticsReadMapper.class,"page",Map.of("p",top));
|
||||||
|
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 combinedReportKeepsStatisticsWhenAllThreeRankingsAreEmpty()throws Exception{
|
@Test void combinedReportKeepsStatisticsWhenAllThreeRankingsAreEmpty()throws Exception{
|
||||||
var empty=report();assertEquals(1,empty.size());assertEquals(0,count(empty.get(0),"total_skus"));
|
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"));
|
assertNull(empty.get(0).get("rank_code"));assertNull(empty.get(0).get("physical_quantity_known"));
|
||||||
|
|||||||
Reference in New Issue
Block a user