|
@@ -8,14 +8,17 @@ use Illuminate\Support\Facades\DB;
|
|
|
class ShareSettlementService
|
|
class ShareSettlementService
|
|
|
{
|
|
{
|
|
|
/** A deterministic original-bet-day projection. Reading it never creates a payable balance. */
|
|
/** A deterministic original-bet-day projection. Reading it never creates a payable balance. */
|
|
|
- public function projection(string $date): array
|
|
|
|
|
|
|
+ public function projection(string $date,bool $withDiagnostics=false): array
|
|
|
{
|
|
{
|
|
|
$this->validateDate($date);
|
|
$this->validateDate($date);
|
|
|
$policy = DB::table('share_policies')->where('effective_date','<=',$date)->orderByDesc('effective_date')->first();
|
|
$policy = DB::table('share_policies')->where('effective_date','<=',$date)->orderByDesc('effective_date')->first();
|
|
|
$empty = ['date'=>$date,'policy'=>$policy,'rows'=>[]];
|
|
$empty = ['date'=>$date,'policy'=>$policy,'rows'=>[]];
|
|
|
$activation=DB::table('share_settings')->value('activation_date');
|
|
$activation=DB::table('share_settings')->value('activation_date');
|
|
|
- if (!$activation || $date<$activation || !DB::table('share_system_state')->where('key','initialized_at')->exists()) return $empty;
|
|
|
|
|
|
|
+ $initialized=DB::table('share_system_state')->where('key','initialized_at')->exists();
|
|
|
$depthRates = self::usesDepthRates($date);
|
|
$depthRates = self::usesDepthRates($date);
|
|
|
|
|
+ $diagnostics=$withDiagnostics ? $this->diagnosticContext($date,$policy,$activation,$initialized,$depthRates) : null;
|
|
|
|
|
+ if ($withDiagnostics) $empty['diagnostics']=$diagnostics;
|
|
|
|
|
+ if (!$activation || $date<$activation || !$initialized) return $empty;
|
|
|
if (!$policy || !$policy->benefit_enabled || $policy->valid_member_min_effective_bet === null
|
|
if (!$policy || !$policy->benefit_enabled || $policy->valid_member_min_effective_bet === null
|
|
|
|| (!$depthRates && $policy->indirect_bonus_rate === null)) return $empty;
|
|
|| (!$depthRates && $policy->indirect_bonus_rate === null)) return $empty;
|
|
|
$end = $date . ' 23:59:59';
|
|
$end = $date . ' 23:59:59';
|
|
@@ -41,10 +44,33 @@ class ShareSettlementService
|
|
|
}
|
|
}
|
|
|
$rows = [];
|
|
$rows = [];
|
|
|
foreach ($facts as $fact) {
|
|
foreach ($facts as $fact) {
|
|
|
- if (!isset($qualified[(int)$fact->user_id]) || !array_key_exists($fact->category,ShareDefaults::categories())) continue;
|
|
|
|
|
- foreach ($edges[(int)$fact->user_id] ?? [] as $edge) {
|
|
|
|
|
|
|
+ $bettor=(int)$fact->user_id;
|
|
|
|
|
+ if (!isset($qualified[$bettor])) {
|
|
|
|
|
+ if ($withDiagnostics) {
|
|
|
|
|
+ if (!$users->has($bettor)) ++$diagnostics['excluded']['bettor_unavailable'];
|
|
|
|
|
+ else {
|
|
|
|
|
+ if (bccomp((string)($deposits[$users[$bettor]->member_id] ?? 0),(string)$policy->valid_member_min_deposit,10)<0) ++$diagnostics['excluded']['deposit_below_threshold'];
|
|
|
|
|
+ if (bccomp((string)($effective[$bettor] ?? 0),(string)$policy->valid_member_min_effective_bet,10)<0) ++$diagnostics['excluded']['effective_bet_below_threshold'];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!array_key_exists($fact->category,ShareDefaults::categories())) {
|
|
|
|
|
+ if ($withDiagnostics) ++$diagnostics['excluded']['unsupported_category'];
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $ancestors=$edges[$bettor] ?? [];
|
|
|
|
|
+ if ($withDiagnostics && !$ancestors) ++$diagnostics['excluded']['no_ancestor_relationship'];
|
|
|
|
|
+ foreach ($ancestors as $edge) {
|
|
|
$receiver=(int)$edge->ancestor_user_id;
|
|
$receiver=(int)$edge->ancestor_user_id;
|
|
|
- if (!$users->has($receiver) || $edge->bound_at > $fact->bet_at) continue;
|
|
|
|
|
|
|
+ if (!$users->has($receiver)) {
|
|
|
|
|
+ if ($withDiagnostics) ++$diagnostics['excluded']['receiver_unavailable'];
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($edge->bound_at > $fact->bet_at) {
|
|
|
|
|
+ if ($withDiagnostics) ++$diagnostics['excluded']['relationship_bound_after_bet'];
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
if (!isset($rows[$receiver][$fact->category])) $rows[$receiver][$fact->category]=['direct_performance'=>'0','indirect_performance'=>'0','performance_by_depth'=>[]];
|
|
if (!isset($rows[$receiver][$fact->category])) $rows[$receiver][$fact->category]=['direct_performance'=>'0','indirect_performance'=>'0','performance_by_depth'=>[]];
|
|
|
$field=(int)$edge->depth===1 ? 'direct_performance' : 'indirect_performance';
|
|
$field=(int)$edge->depth===1 ? 'direct_performance' : 'indirect_performance';
|
|
|
$rows[$receiver][$fact->category][$field]=bcadd($rows[$receiver][$fact->category][$field],(string)$fact->effective_amount,10);
|
|
$rows[$receiver][$fact->category][$field]=bcadd($rows[$receiver][$fact->category][$field],(string)$fact->effective_amount,10);
|
|
@@ -74,7 +100,66 @@ class ShareSettlementService
|
|
|
unset($row['performance_by_depth']);
|
|
unset($row['performance_by_depth']);
|
|
|
}
|
|
}
|
|
|
unset($categories,$row);
|
|
unset($categories,$row);
|
|
|
- return ['date'=>$date,'policy'=>$policy,'rows'=>$rows];
|
|
|
|
|
|
|
+ $result=['date'=>$date,'policy'=>$policy,'rows'=>$rows];
|
|
|
|
|
+ if ($withDiagnostics) $result['diagnostics']=$diagnostics;
|
|
|
|
|
+ return $result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Read-only explanation from the same projection and filters used by real settlement. */
|
|
|
|
|
+ public function diagnose(string $date): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $projection=$this->projection($date,true);
|
|
|
|
|
+ $report=['date'=>$date,'timezone'=>ShareDefaults::TIMEZONE,'written'=>false]+$projection['diagnostics'];
|
|
|
|
|
+ $report['receivers']=count($projection['rows']);
|
|
|
|
|
+ $report['positive_receivers']=0;
|
|
|
|
|
+ $report['projected_commission']='0.0000000000';
|
|
|
|
|
+ foreach ($projection['rows'] as $categories) {
|
|
|
|
|
+ $total='0.0000000000';
|
|
|
|
|
+ foreach ($categories as $row) $total=bcadd($total,bcadd($row['direct_commission'],$row['indirect_commission'],10),10);
|
|
|
|
|
+ if (bccomp($total,'0',10)>0) ++$report['positive_receivers'];
|
|
|
|
|
+ $report['projected_commission']=bcadd($report['projected_commission'],$total,10);
|
|
|
|
|
+ }
|
|
|
|
|
+ return $report;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function diagnosticContext(string $date,?object $policy,?string $activation,bool $initialized,bool $depthRates): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $blockers=[];
|
|
|
|
|
+ if (!$activation) $blockers[]=['code'=>'activation_not_configured','message'=>'尚未配置分享返佣首次生效日期'];
|
|
|
|
|
+ elseif ($date<$activation) $blockers[]=['code'=>'before_activation_date','message'=>'目标日期早于首次生效日期;allow-today不会绕过配置生效日'];
|
|
|
|
|
+ if (!$initialized) $blockers[]=['code'=>'relations_not_initialized','message'=>'分享关系尚未初始化'];
|
|
|
|
|
+ if (!$policy) $blockers[]=['code'=>'no_effective_policy','message'=>'目标日期没有已生效的返佣配置版本'];
|
|
|
|
|
+ else {
|
|
|
|
|
+ if (!$policy->benefit_enabled) $blockers[]=['code'=>'benefit_disabled','message'=>'目标日期的计佣配置未启用'];
|
|
|
|
|
+ if ($policy->valid_member_min_effective_bet===null) $blockers[]=['code'=>'missing_effective_bet_threshold','message'=>'目标日期未配置有效投注门槛'];
|
|
|
|
|
+ if (!$depthRates && $policy->indirect_bonus_rate===null) $blockers[]=['code'=>'legacy_policy_incomplete','message'=>'历史计佣版本缺少当时的间接佣金参数'];
|
|
|
|
|
+ }
|
|
|
|
|
+ $currency=(string)config('third_game.currency','CNY');
|
|
|
|
|
+ $stats=DB::table('share_bet_facts')->where('bet_date',$date)->select('source')->selectRaw(
|
|
|
|
|
+ 'COUNT(*) AS total, SUM(CASE WHEN settled = 0 THEN 1 ELSE 0 END) AS unsettled,
|
|
|
|
|
+ SUM(CASE WHEN voided = 1 THEN 1 ELSE 0 END) AS voided,
|
|
|
|
|
+ SUM(CASE WHEN currency IS NULL OR currency <> ? THEN 1 ELSE 0 END) AS currency_mismatch,
|
|
|
|
|
+ SUM(CASE WHEN effective_amount <= 0 THEN 1 ELSE 0 END) AS zero_effective,
|
|
|
|
|
+ SUM(CASE WHEN settled = 1 AND voided = 0 AND currency = ? AND effective_amount > 0 THEN 1 ELSE 0 END) AS candidates',
|
|
|
|
|
+ [$currency,$currency]
|
|
|
|
|
+ )->groupBy('source')->get();
|
|
|
|
|
+ $counts=array_fill_keys(['total','unsettled','voided','currency_mismatch','zero_effective','candidates'],0);
|
|
|
|
|
+ $sources=[];
|
|
|
|
|
+ foreach ($stats as $row) {
|
|
|
|
|
+ $values=[];
|
|
|
|
|
+ foreach ($counts as $field=>$_) { $values[$field]=(int)$row->$field; $counts[$field]+=$values[$field]; }
|
|
|
|
|
+ $sources[$row->source]=$values;
|
|
|
|
|
+ }
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'activation_date'=>$activation,'initialized'=>$initialized,'currency'=>$currency,
|
|
|
|
|
+ 'policy'=>$policy ? ['id'=>(int)$policy->id,'effective_date'=>$policy->effective_date,
|
|
|
|
|
+ 'benefit_enabled'=>(int)$policy->benefit_enabled,'valid_member_min_deposit'=>$policy->valid_member_min_deposit,
|
|
|
|
|
+ 'valid_member_min_effective_bet'=>$policy->valid_member_min_effective_bet] : null,
|
|
|
|
|
+ 'next_policy_effective_date'=>DB::table('share_policies')->where('effective_date','>',$date)->orderBy('effective_date')->value('effective_date'),
|
|
|
|
|
+ 'blockers'=>$blockers,'facts'=>$counts,'facts_by_source'=>$sources,
|
|
|
|
|
+ 'excluded'=>array_fill_keys(['bettor_unavailable','deposit_below_threshold','effective_bet_below_threshold',
|
|
|
|
|
+ 'unsupported_category','no_ancestor_relationship','receiver_unavailable','relationship_bound_after_bet'],0),
|
|
|
|
|
+ ];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public static function rate(array $rules,int $members,string $performance): string
|
|
public static function rate(array $rules,int $members,string $performance): string
|