feat(selfop): support complete inventory sales windows

This commit is contained in:
2026-10-13 12:35:39 +08:00
parent 39f914b3b5
commit 496fc11b0a
16 changed files with 269 additions and 72 deletions
@@ -52,8 +52,8 @@ public class PlatformAnalyticsController {
requirePlatform(user);return ApiResponse.success(service.slowSkus(query));
}
@GetMapping("/inventory-coverage")
@Operation(summary="正品库存、固定30天日均和可售天数")
public ApiResponse<AnalyticsResponses.Page<AnalyticsResponses.Metric>> inventoryCoverage(
@Operation(summary="正品库存、10或30完整日日均和预计可售天数")
public ApiResponse<AnalyticsResponses.Page<AnalyticsResponses.InventoryMetric>> inventoryCoverage(
@AuthenticationPrincipal AuthenticatedUser user,@ModelAttribute AnalyticsQuery query,@ModelAttribute AnalyticsInventoryOptions options){
requirePlatform(user);return ApiResponse.success(service.inventoryCoverage(query,options));
}
@@ -5,12 +5,16 @@ import java.util.Set;
/** Endpoint-specific inventory presentation, separate from immutable version/date identity. */
public record AnalyticsInventoryOptions(String keyword,String attention,String sortBy,Integer coverageDaysThreshold,
String coverageBucket,String bucketRuleVersion,String coverageSelection,String drilldownFrom) {
String coverageBucket,String bucketRuleVersion,String coverageSelection,String drilldownFrom,
Integer salesWindowDays,@org.springframework.format.annotation.DateTimeFormat(iso=org.springframework.format.annotation.DateTimeFormat.ISO.DATE) java.time.LocalDate salesWindowEndDate) {
public int salesDays(){return salesWindowDays==null?30:salesWindowDays;}
public String attentionValue(){return attention==null?"ALL":attention;}
public String sortValue(){return sortBy==null?"AVAILABLE_DESC":sortBy;}
public String keywordPattern(){return keyword==null?null:keyword.replace("!","!!").replace("%","!%").replace("_","!_")+"%";}
public void validate(String endpoint,AnalyticsQuery q){
CoverageDistributionPolicy.validate(this);
if(salesDays()!=10&&salesDays()!=30)throw new BizException("库存销量窗口仅支持10或30天");
if(!"INVENTORY".equals(endpoint)&&(salesWindowDays!=null||salesWindowEndDate!=null))throw new BizException("销量窗口仅用于库存明细");
if(!"INVENTORY".equals(endpoint)&&(coverageBucket!=null||coverageSelection!=null||drilldownFrom!=null))throw new BizException("分布下钻条件仅用于库存明细");
if(coverageDaysThreshold!=null&&coverageDaysThreshold<=0)throw new BizException("可售天数阈值必须为正整数");
if(!Set.of("ALL","NO_OUTBOUND_30D","COVERAGE_GT_THRESHOLD","FROZEN_STOCK","DEFECTIVE_STOCK","UNASSESSABLE").contains(attentionValue()))throw new BizException("库存关注条件无效");
@@ -20,6 +20,8 @@ public final class AnalyticsInventorySql {
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,boolean dashboard){
String days="INVENTORY".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}";
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 s.sku_code IS NULL OR s.quantity_unit='PCS' THEN 1 ELSE 0 END sales_unit_valid,"
@@ -38,16 +40,16 @@ public final class AnalyticsInventorySql {
+" 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,"
+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 #{p.complete30}=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 "+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 "
+"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),"
+" qualified AS (SELECT v.*,CASE WHEN evidence_at_utc<=CONVERT_TZ(CAST(#{p.from} AS DATETIME),'+08:00','+00:00') THEN 1 ELSE 0 END observation_eligible,"
+"CASE WHEN unit_valid=1 AND evidence_at_utc<=CONVERT_TZ(CAST(#{p.from} AS DATETIME),'+08:00','+00:00') THEN owner_outbound END outbound_quantity30d FROM values_by_sku v),"
+" calculated AS (SELECT q.*,CASE WHEN available_quantity=0 THEN 0 WHEN available_quantity>0 AND outbound_quantity30d>0 THEN available_quantity*30/outbound_quantity30d END coverage_days,"
+" calculated AS (SELECT q.*,CASE WHEN available_quantity=0 THEN 0 WHEN available_quantity>0 AND outbound_quantity30d>0 THEN available_quantity*"+days+"/outbound_quantity30d END coverage_days,"
+"CASE WHEN physical_quantity=0 THEN 0 WHEN physical_quantity>0 AND outbound_quantity30d IS NOT NULL THEN CASE WHEN outbound_quantity30d=0 THEN 1 ELSE 0 END END no_outbound,"
+"CASE WHEN #{o.coverageDaysThreshold} IS NULL THEN NULL WHEN physical_quantity=0 THEN 0 WHEN physical_quantity>0 AND available_quantity=0 THEN 0 "
+"WHEN physical_quantity>0 AND available_quantity>0 AND outbound_quantity30d IS NOT NULL THEN CASE WHEN outbound_quantity30d>0 AND available_quantity*30>outbound_quantity30d*#{o.coverageDaysThreshold} THEN 1 ELSE 0 END END above_threshold "
+"WHEN physical_quantity>0 AND available_quantity>0 AND outbound_quantity30d IS NOT NULL THEN CASE WHEN outbound_quantity30d>0 AND available_quantity*"+days+">outbound_quantity30d*#{o.coverageDaysThreshold} THEN 1 ELSE 0 END END above_threshold "
+"FROM qualified q), result AS (SELECT c.*,CASE WHEN physical_quantity IS NULL OR available_quantity IS NULL OR outbound_quantity30d IS NULL THEN 1 ELSE 0 END unassessable FROM calculated c) ";
}
private static String salesCte(AnalyticsSqlRequest p,boolean stockFirst,boolean unused){String d=stockFirst?"d.":"";
@@ -29,13 +29,13 @@ public class AnalyticsQueryService {
LocalDate fullFrom,LocalDate fullUntil,SalesRangePolicy.Range range,AnalyticsResponses.Context context){}
public AnalyticsResponses.Page<AnalyticsResponses.SalesSummary> salesSummary(AnalyticsQuery q){
Snapshot s=context(q,true,false,false); AnalyticsSqlRequest p=request(s,"SUMMARY");
Snapshot s=context(q,true,false,false,null); AnalyticsSqlRequest p=request(s,"SUMMARY");
long count=mapper.count(p);
var records=mapper.page(p).stream().map(r->summary(s,r)).toList();
return page(s,records,count,Map.of());
}
public AnalyticsResponses.Trend dailySales(AnalyticsQuery q){
Snapshot s=context(q,true,true,false);
Snapshot s=context(q,true,true,false,null);
Profile sp=profile(s,s.sales),ip=profile(s,s.inventory);
if(sp==null&&ip==null)throw new BizException("所选固定版本中无该货主和SKU记录");
String unit=sp!=null?sp.quantityUnit():ip==null?null:ip.quantityUnit();
@@ -66,18 +66,18 @@ public class AnalyticsQueryService {
}
public AnalyticsResponses.Page<AnalyticsResponses.Metric> hotSkus(AnalyticsQuery q){return metrics(q,"HOT",true);}
public AnalyticsResponses.Page<AnalyticsResponses.Metric> slowSkus(AnalyticsQuery q){return metrics(q,"SLOW",false);}
public AnalyticsResponses.Page<AnalyticsResponses.Metric> inventoryCoverage(AnalyticsQuery q,AnalyticsInventoryOptions o){
o.validate("INVENTORY",q);Snapshot s=context(q,false,false,false);var p=request(s,"INVENTORY");
public AnalyticsResponses.Page<AnalyticsResponses.InventoryMetric> inventoryCoverage(AnalyticsQuery q,AnalyticsInventoryOptions o){
o.validate("INVENTORY",q);Snapshot s=context(q,false,false,false,o);var p=request(s,"INVENTORY");
CoverageDistributionPolicy.requireDrilldown(q,o,s.context(),inventoryFreshUntil(s),clock.instant());
long total=mapper.inventoryCount(p,o);
return page(s,q.offset()>=total?List.of():mapper.inventoryPage(p,o).stream().map(r->inventoryMetric(s,r)).toList(),total,Map.of());
}
public AnalyticsResponses.Page<AnalyticsResponses.OwnerOption> owners(AnalyticsQuery q,AnalyticsInventoryOptions o){
o.validate("OWNERS",q);Snapshot s=context(q,false,false,false);var p=request(s,"OWNERS");long total=mapper.ownerCount(p,o);
o.validate("OWNERS",q);Snapshot s=context(q,false,false,false,null);var p=request(s,"OWNERS");long total=mapper.ownerCount(p,o);
return page(s,q.offset()>=total?List.of():mapper.owners(p,o).stream().map(code->new AnalyticsResponses.OwnerOption(code,null)).toList(),total,Map.of());
}
public AnalyticsResponses.OwnerOverview ownerOverview(AnalyticsQuery q,AnalyticsInventoryOptions o){
o.validate("OVERVIEW",q);Snapshot s=context(q,false,false,false);var raw=mapper.ownerTotals(request(s,"OVERVIEW"),o);
o.validate("OVERVIEW",q);Snapshot s=context(q,false,false,false,null);var raw=mapper.ownerTotals(request(s,"OVERVIEW"),o);
Map<String,Object> totals=new HashMap<>();if(raw!=null)raw.forEach((key,value)->totals.put(key.toLowerCase(Locale.ROOT),value));
long count=number(totals,"sku_count");if(count==0)return null;
var physical=quantity(totals,"physical_quantity");var outbound=quantity(totals,"owner_outbound");
@@ -90,12 +90,12 @@ public class AnalyticsQueryService {
quantity(totals,"frozen_quantity"),quantity(totals,"defective_physical_quantity"),quantity(totals,"transit_quantity"),outbound,average,
risk(totals,"no_outbound",physical,false),risk(totals,"above_threshold",physical,o.coverageDaysThreshold()==null));
}
private AnalyticsResponses.Metric inventoryMetric(Snapshot s,Inventory r){
private AnalyticsResponses.InventoryMetric inventoryMetric(Snapshot s,Inventory r){
boolean observed=Boolean.TRUE.equals(r.observationEligible());String quality=!"VALID".equals(s.context.inventoryStatus())?s.context.inventoryStatus():r.availableQuantity()==null?"UNKNOWN":"VALID";
String coverage=!Boolean.TRUE.equals(r.unitValid())?"UNIT_UNKNOWN_OR_CONFLICT":r.ownerOutbound()!=null?"COMPLETE":"COVERAGE_INSUFFICIENT";
var calculated=InventoryCoveragePolicy.calculate(quality,r.availableQuantity(),r.outboundQuantity30d(),s.trusted,
complete(s,s.range.startDate(),s.range.endDateExclusive()),observed,Boolean.TRUE.equals(r.unitValid()));
List<String> attention=new ArrayList<>();if(Integer.valueOf(1).equals(r.noOutbound()))attention.add("NO_OUTBOUND_30D");
var calculated=InventoryWindowPolicy.calculate(quality,r.availableQuantity(),r.outboundQuantity30d(),s.trusted,
complete(s,s.range.startDate(),s.range.endDateExclusive()),observed,Boolean.TRUE.equals(r.unitValid()),s.range.dayCount());
List<String> attention=new ArrayList<>();if(Integer.valueOf(1).equals(r.noOutbound()))attention.add("NO_OUTBOUND_IN_WINDOW");
if(Integer.valueOf(1).equals(r.aboveThreshold()))attention.add("COVERAGE_GT_THRESHOLD");
if(positive(r.frozenQuantity()))attention.add("FROZEN_STOCK");if(positive(r.defectivePhysicalQuantity()))attention.add("DEFECTIVE_STOCK");
if(Boolean.TRUE.equals(r.unassessable()))attention.add("UNASSESSABLE");
@@ -106,10 +106,10 @@ public class AnalyticsQueryService {
Map<String,String> statuses=new LinkedHashMap<>();String unknown="STALE".equals(s.context.inventoryStatus())?"STALE":"UNKNOWN";
statuses.put("physicalQuantity",r.physicalQuantity()==null?unknown:"VALID");statuses.put("availableQuantity",r.availableQuantity()==null?unknown:"VALID");
statuses.put("frozenQuantity",r.frozenQuantity()==null?unknown:"VALID");statuses.put("transitQuantity",r.transitQuantity()==null?unknown:"VALID");
statuses.put("defectivePhysicalQuantity",r.defectivePhysicalQuantity()==null?unknown:"VALID");statuses.put("outboundQuantity30d",r.outboundQuantity30d()==null?"UNKNOWN":"VALID");
return new AnalyticsResponses.Metric(s.q.sourceAccount(),r.customerCode(),s.q.warehouseCode(),r.skuCode(),r.productName(),Boolean.TRUE.equals(r.unitValid())?"PCS":null,
r.outboundQuantity30d(),r.orderCount(),null,r.physicalQuantity(),r.availableQuantity(),r.frozenQuantity(),r.transitQuantity(),utc(r.observedAtUtc()),
divide30(r.outboundQuantity30d()),calculated.days(),quality,coverage,observed,utc(r.evidenceAtUtc()),calculated.status(),List.copyOf(new LinkedHashSet<>(reasons)),
statuses.put("defectivePhysicalQuantity",r.defectivePhysicalQuantity()==null?unknown:"VALID");statuses.put("windowOutboundQuantity",r.outboundQuantity30d()==null?"UNKNOWN":"VALID");
return new AnalyticsResponses.InventoryMetric(s.q.sourceAccount(),r.customerCode(),s.q.warehouseCode(),r.skuCode(),r.productName(),Boolean.TRUE.equals(r.unitValid())?"PCS":null,
r.orderCount(),r.physicalQuantity(),r.availableQuantity(),r.frozenQuantity(),r.transitQuantity(),utc(r.observedAtUtc()),
r.outboundQuantity30d()==null?null:r.outboundQuantity30d().divide(BigDecimal.valueOf(s.range.dayCount()),12,java.math.RoundingMode.HALF_UP),calculated.days(),quality,coverage,observed,utc(r.evidenceAtUtc()),calculated.status(),List.copyOf(new LinkedHashSet<>(reasons)),
null,r.defectivePhysicalQuantity(),r.outboundQuantity30d(),r.lastOutbound(),historyStart(s),List.copyOf(attention),Map.copyOf(statuses));
}
private static boolean positive(BigDecimal n){return n!=null&&n.signum()>0;}
@@ -132,7 +132,7 @@ public class AnalyticsQueryService {
unknown,decimal(row,field+"_unknown_physical"),reasons);
}
private AnalyticsResponses.Page<AnalyticsResponses.Metric> metrics(AnalyticsQuery q,String kind,boolean hot){
Snapshot s=context(q,false,false,hot); AnalyticsSqlRequest p=request(s,kind);
Snapshot s=context(q,false,false,hot,null); AnalyticsSqlRequest p=request(s,kind);
List<AnalyticsReadMapper.RankingStat> statistics=mapper.rankingStats(p);
long total=statistics.isEmpty()?0:statistics.get(0).total();
List<AnalyticsResponses.Metric> rows=q.offset()>=total?List.of():mapper.page(p).stream().map(r->metric(s,r,kind)).toList();
@@ -173,7 +173,7 @@ public class AnalyticsQueryService {
stockValid?r.frozenQuantity():null,stockValid?r.transitQuantity():null,utc(r.inventoryObservedAtUtc()),result.average(),result.days(),
r.inventoryQuality(),coverage,observed,evidence,result.status(),List.copyOf(new LinkedHashSet<>(reasons)),null,null,r.sales30Quantity(),null,historyStart(s),List.of(),Map.of());
}
Snapshot context(AnalyticsQuery q,boolean chart,boolean exact,boolean hot){
Snapshot context(AnalyticsQuery q,boolean chart,boolean exact,boolean hot,AnalyticsInventoryOptions inventoryOptions){
q.validate(chart,exact,hot); Instant now=clock.instant();
List<Scope> scopes=mapper.scopes(q.sourceAccount(),q.warehouseCode());
Scope salesScope=scope(scopes,"OUTBOUND"),stockScope=scope(scopes,"INVENTORY");
@@ -199,7 +199,7 @@ public class AnalyticsQueryService {
LocalDate anchor=asOf.atZone(BUSINESS_ZONE).toLocalDate();
SalesRangePolicy.Range range;
try{
range=chart?SalesRangePolicy.resolve(q.rangePreset()==null?"TODAY":q.rangePreset(),q.startDate(),q.endDate(),
range=inventoryOptions!=null?InventoryWindowPolicy.range(inventoryOptions,anchor):chart?SalesRangePolicy.resolve(q.rangePreset()==null?"TODAY":q.rangePreset(),q.startDate(),q.endDate(),
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,
anchor.minusDays(hot?q.rankingDays():30).atStartOfDay(BUSINESS_ZONE).toInstant(),anchor.atStartOfDay(BUSINESS_ZONE).toInstant());
@@ -29,6 +29,14 @@ public final class AnalyticsResponses {
String status,List<String> reasonCodes,String customerName,BigDecimal defectivePhysicalQuantity,
BigDecimal outboundQuantity30d,LocalDate lastObservedOutboundDate,LocalDate observedHistoryStartDate,
List<String> attentionReasons,Map<String,String> quantityStatuses){}
public record InventoryMetric(String sourceAccount,String customerCode,String warehouseCode,String sku,String productName,
String quantityUnit,Long skuOutboundOrderCount,
BigDecimal physicalQuantity,BigDecimal availableQuantity,BigDecimal frozenQuantity,BigDecimal transitQuantity,
Instant inventoryObservedAt,BigDecimal averageDailyOutboundQuantity,BigDecimal inventoryCoverageDays,
String inventoryQuality,String salesCoverage,boolean observationEligible,Instant earliestReliableEvidenceAt,
String status,List<String> reasonCodes,String customerName,BigDecimal defectivePhysicalQuantity,
BigDecimal windowOutboundQuantity,LocalDate lastObservedOutboundDate,LocalDate observedHistoryStartDate,
List<String> attentionReasons,Map<String,String> quantityStatuses){}
public record OwnerOption(String customerCode,String customerName){}
public record QuantityTotal(BigDecimal overallTotal,BigDecimal knownSubtotal,long unknownSkuCount,String dataStatus,List<String> reasonCodes){}
public record RiskShare(Long matchedSkuCount,BigDecimal matchedPhysicalQuantity,BigDecimal physicalQuantityRatio,
@@ -17,13 +17,13 @@ public class DashboardQueryService {
public DashboardQueryService(AnalyticsQueryService contextService,DashboardReadMapper mapper){this.contextService=contextService;this.mapper=mapper;}
public DashboardResponses.Dashboard dashboard(AnalyticsQuery input){
AnalyticsQuery q=DashboardRequest.normalize(input);
var snapshot=contextService.context(q,true,false,false);var c=snapshot.context();
var snapshot=contextService.context(q,true,false,false,null);var c=snapshot.context();
if(snapshot.sales()!=null&&!Boolean.TRUE.equals(snapshot.sales().warehouseDailyReady()))
throw new com.baiyue.common.core.exception.BizException("该销量版本尚未生成仓库每日汇总,请重新发布统计后再查询");
var base=contextService.request(snapshot,"DASHBOARD");LocalDate fixedFrom=c.anchorDate().minusDays(30),fixedUntil=c.anchorDate();
var p=new AnalyticsSqlRequest(base.account(),base.warehouse(),null,null,base.salesRun(),base.inventoryRun(),fixedFrom,fixedUntil,base.anchor(),fixedFrom,
base.completeFrom(),base.completeUntil(),base.coveredFrom(),base.coveredUntil(),base.salesKnown(),base.formalSales(),base.complete30(),base.complete30(),base.stockFresh(),0,10,"DASHBOARD");
var o=new AnalyticsInventoryOptions(null,null,null,null,null,null,null,null);
var o=new AnalyticsInventoryOptions(null,null,null,null,null,null,null,null,null,null);
var days=mapper.daily(p,c.rangeStartDate(),c.rangeEndDateExclusive());
var report=mapper.report(p,o).stream().map(DashboardQueryService::normalize).toList();
Map<String,Object> stats=report.get(0);var ranked=report.stream().filter(row->row.get("rank_code")!=null).map(DashboardQueryService::ranked).toList();
@@ -0,0 +1,43 @@
package com.baiyue.lastleg.selfoperated.analytics.query;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.lastleg.selfoperated.analytics.statistics.SalesRangePolicy;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.ZoneId;
/** Inventory-only complete natural-day window; dashboard and ranking policies remain fixed. */
final class InventoryWindowPolicy {
private InventoryWindowPolicy() {}
static SalesRangePolicy.Range range(AnalyticsInventoryOptions o,LocalDate anchor) {
int days=o.salesDays();
if(days!=10&&days!=30)throw new BizException("库存销量窗口仅支持10或30天");
LocalDate end=o.salesWindowEndDate()==null?anchor.minusDays(1):o.salesWindowEndDate();
if(!end.isBefore(anchor))throw new BizException("库存销量截止日必须早于查询当日");
boolean defaultThirty=days==30&&end.equals(anchor.minusDays(1));
if(!defaultThirty&&(o.coverageBucket()!=null||o.coverageSelection()!=null||o.drilldownFrom()!=null
||"NO_OUTBOUND_30D".equals(o.attentionValue())||"OUTBOUND_30D_ASC".equals(o.sortValue())))
throw new BizException("30日关注、排序及总览下钻仅适用于截至昨日的30日窗口");
LocalDate until=end.plusDays(1),from=until.minusDays(days);var zone=ZoneId.of("Asia/Shanghai");
return new SalesRangePolicy.Range(anchor,from,until,days,from.atStartOfDay(zone).toInstant(),until.atStartOfDay(zone).toInstant());
}
record Result(BigDecimal average,BigDecimal days,String status) {}
static Result calculate(String quality,BigDecimal available,BigDecimal sales,boolean trusted,
boolean complete,boolean observed,boolean unit,int days) {
if(days!=10&&days!=30)throw new IllegalArgumentException("Inventory window must be 10 or 30 days");
if(!"VALID".equals(quality)||available==null)return new Result(null,null,"INVENTORY_"+quality);
if(available.signum()<0)return new Result(null,null,"INVENTORY_INVALID");
BigDecimal divisor=BigDecimal.valueOf(days);
BigDecimal average=unit&&trusted&&complete&&observed&&sales!=null&&sales.signum()>=0
?sales.divide(divisor,12,RoundingMode.HALF_UP):null;
if(available.signum()==0)return new Result(average,BigDecimal.ZERO.setScale(1),"NO_AVAILABLE_INVENTORY");
if(!unit)return new Result(null,null,"UNIT_UNKNOWN_OR_CONFLICT");
if(!trusted)return new Result(null,null,"SALES_UNTRUSTED");
if(!complete)return new Result(null,null,"COVERAGE_INSUFFICIENT");
if(!observed)return new Result(null,null,"OBSERVATION_INSUFFICIENT");
if(sales==null||sales.signum()<0)return new Result(null,null,"SALES_UNKNOWN");
if(sales.signum()==0)return new Result(average,null,"NO_OUTBOUND_IN_WINDOW");
return new Result(average,available.multiply(divisor).divide(sales,1,RoundingMode.HALF_UP),"VALID");
}
}