SharingSettlementTest.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. <?php
  2. namespace Tests\Integration;
  3. use App\Services\Sharing\ShareDefaults;
  4. use App\Services\Sharing\ShareRelations;
  5. use App\Services\Sharing\ShareSettlementService;
  6. use Carbon\Carbon;
  7. use Illuminate\Config\Repository;
  8. use Illuminate\Database\Capsule\Manager;
  9. use Illuminate\Database\Schema\Blueprint;
  10. use Illuminate\Foundation\Application;
  11. use Illuminate\Support\Facades\DB;
  12. use Illuminate\Support\Facades\Facade;
  13. use PHPUnit\Framework\TestCase;
  14. class SharingSettlementTest extends TestCase
  15. {
  16. private ShareSettlementService $service;
  17. private ShareRelations $relations;
  18. protected function setUp(): void
  19. {
  20. Facade::clearResolvedInstances();
  21. Carbon::setTestNow(Carbon::parse('2026-10-25 12:00:00','Asia/Shanghai'));
  22. $app=new Application(dirname(__DIR__,2));
  23. $app->instance('config',new Repository(['app'=>['timezone'=>'Asia/Shanghai'],'third_game'=>['currency'=>'CNY']]));
  24. Facade::setFacadeApplication($app);
  25. $db=new Manager($app); $db->addConnection(['driver'=>'sqlite','database'=>':memory:','prefix'=>'bot_']);
  26. $db->setAsGlobal(); $db->bootEloquent(); $app->instance('db',$db->getDatabaseManager()); $app->bind('db.schema',fn()=>$db->schema());
  27. $db->schema()->create('users',function(Blueprint $t){
  28. $t->id(); $t->string('member_id'); $t->string('user_code'); $t->string('agent_user_code')->nullable(); $t->integer('from')->default(1); $t->timestamps();
  29. $t->string('user_id')->nullable(); $t->string('first_name')->nullable(); $t->string('username')->nullable(); $t->string('language')->nullable();
  30. $t->integer('recharge_channel_group_id')->default(0);
  31. });
  32. $db->schema()->create('share_settings',function(Blueprint $t){
  33. $t->id(); $t->boolean('enabled')->default(1); $t->boolean('benefit_enabled')->default(0); $t->string('share_domain')->default('https://pc.example');
  34. $t->decimal('valid_member_min_deposit',24,10)->default(20); $t->timestamps();
  35. });
  36. $db->schema()->create('balance_logs',function(Blueprint $t){
  37. $t->id(); $t->string('member_id'); $t->integer('type')->default(1); $t->string('change_type'); $t->decimal('amount',24,10); $t->timestamps();
  38. });
  39. $db->schema()->create('wallets',function(Blueprint $t){$t->id();$t->string('member_id');$t->decimal('available_balance',24,10)->default(50);$t->decimal('frozen_balance',24,10)->default(0);});
  40. (require dirname(__DIR__,2).'/database/migrations/2026_10_24_120000_create_member_sharing_system.php')->up();
  41. foreach ([[1,'A',null],[2,'B','A'],[3,'C','B'],[4,'D','C'],[5,'E','A'],[6,'X',null],[7,'DEMO',null]] as [$id,$code,$parent]) {
  42. DB::table('users')->insert(['id'=>$id,'member_id'=>'M'.$id,'user_code'=>$code,'agent_user_code'=>$parent,'from'=>$id===7?2:1,'created_at'=>'2026-10-01 00:00:00']);
  43. DB::table('balance_logs')->insert(['member_id'=>'M'.$id,'change_type'=>'充值','amount'=>20,'created_at'=>'2026-10-20 00:00:00']);
  44. DB::table('wallets')->insert(['member_id'=>'M'.$id]);
  45. }
  46. DB::table('share_settings')->insert(['activation_date'=>'2026-10-24','enabled'=>1,'benefit_enabled'=>1,'valid_member_min_effective_bet'=>100]);
  47. DB::table('share_policies')->insert(['effective_date'=>'2026-10-24','enabled'=>1,'benefit_enabled'=>1,
  48. 'valid_member_min_deposit'=>20,'valid_member_min_effective_bet'=>100,'rules'=>json_encode(ShareDefaults::rules())]);
  49. $this->relations=new ShareRelations(); $this->relations->initialize(true); $this->service=new ShareSettlementService();
  50. }
  51. protected function tearDown(): void
  52. {
  53. Carbon::setTestNow(); DB::disconnect(); Facade::clearResolvedInstances(); parent::tearDown();
  54. }
  55. private function fact(int $user,string $amount,string $date='2026-10-24',array $extra=[]): int
  56. {
  57. return DB::table('share_bet_facts')->insertGetId($extra+['source'=>'fixture','source_id'=>uniqid('',true),'user_id'=>$user,'member_id'=>'M'.$user,
  58. 'category'=>'third:2','bet_at'=>$date.' 12:00:00','bet_date'=>$date,'settled_at'=>'2026-10-25 10:00:00',
  59. 'bet_amount'=>$amount,'net_profit'=>'-'.$amount,'effective_amount'=>$amount,'settled'=>1,'voided'=>0,'currency'=>'CNY','source_version'=>str_repeat('a',64)]);
  60. }
  61. private function amount(int $user,string $field='earned_amount'): string { return bcadd((string)DB::table('share_accounts')->where('user_id',$user)->value($field),'0',10); }
  62. public function test_nine_categories_have_the_exact_twelve_tiers_and_both_qualifiers_are_required(): void
  63. {
  64. $this->assertCount(9,ShareDefaults::rules());
  65. $rules=ShareDefaults::rules()['third:2']; $this->assertCount(12,$rules);
  66. $this->assertSame('0.25',ShareSettlementService::rate($rules,1,'1000000'));
  67. $this->assertSame('0',ShareSettlementService::rate($rules,0,'99.99'));
  68. $this->assertSame('0.10',ShareSettlementService::rate($rules,0,'100'));
  69. $this->assertSame('0.70',ShareSettlementService::rate($rules,5,'1700000'));
  70. }
  71. public function test_receiver_own_rate_is_compounded_by_depth_and_retries_are_idempotent(): void
  72. {
  73. $this->fact(2,'200'); $this->fact(3,'20000');
  74. $projection=$this->service->projection('2026-10-24');
  75. $a=$projection['rows'][1]['third:2']; $b=$projection['rows'][2]['third:2'];
  76. $this->assertEquals(0.1,$a['rate']); $this->assertEquals(0.2,$b['rate']);
  77. $this->assertSame('0.0200000000',$a['indirect_commission']);
  78. $this->assertSame('40.0000000000',$b['direct_commission']);
  79. $before=DB::table('wallets')->get()->toJson();
  80. $first=$this->service->settle('2026-10-24');
  81. $this->assertSame(2,$first['receivers']); $this->assertSame('0.2200000000',$this->amount(1));
  82. $this->assertSame('40.0000000000',$this->amount(2));
  83. $entries=DB::table('share_ledger')->count();
  84. $this->assertSame(0,$this->service->settle('2026-10-24')['changed']);
  85. $this->assertSame($entries,DB::table('share_ledger')->count());
  86. $this->assertSame($before,DB::table('wallets')->get()->toJson());
  87. }
  88. public function test_third_depth_adds_another_receiver_rate_factor(): void
  89. {
  90. $this->fact(2,'200'); $this->fact(3,'20000'); $this->fact(4,'1000');
  91. $row=$this->service->projection('2026-10-24')['rows'][1]['third:2'];
  92. $this->assertSame('21000.0000000000',$row['indirect_performance']);
  93. $this->assertSame('0.0200010000',$row['indirect_commission']);
  94. $this->assertSame(3,DB::table('share_tree')->where('ancestor_user_id',1)->where('descendant_user_id',4)->value('depth'));
  95. }
  96. public function test_ten_percent_compounds_through_four_levels_using_only_the_receiver_rate(): void
  97. {
  98. DB::table('users')->insert(['id'=>8,'member_id'=>'M8','user_code'=>'F','agent_user_code'=>'D','created_at'=>'2026-10-01 00:00:00']);
  99. DB::table('balance_logs')->insert(['member_id'=>'M8','change_type'=>'充值','amount'=>20,'created_at'=>'2026-10-20 00:00:00']);
  100. $this->relations->initialize(true);
  101. $rules=ShareDefaults::rules();
  102. $rules['third:2']=[
  103. ['min_valid_members'=>0,'min_performance'=>'0','rate'=>'10'],
  104. ['min_valid_members'=>0,'min_performance'=>'2000','rate'=>'20'],
  105. ];
  106. DB::table('share_policies')->where('effective_date','2026-10-24')->update(['rules'=>json_encode($rules)]);
  107. $this->fact(2,'1000'); $this->fact(3,'2000'); $this->fact(4,'1000'); $this->fact(8,'1000');
  108. $rows=$this->service->projection('2026-10-24')['rows'];
  109. $a=$rows[1]['third:2'];
  110. $this->assertSame('10',$a['rate']);
  111. $this->assertSame('20',$rows[2]['third:2']['rate']);
  112. $this->assertSame('100.0000000000',$a['direct_commission']);
  113. $this->assertSame('4000.0000000000',$a['indirect_performance']);
  114. // 2000*10%*10% + 1000*10%^3 + 1000*10%^4 = 20+1+0.1.
  115. $this->assertSame('21.1000000000',$a['indirect_commission']);
  116. $this->service->settle('2026-10-24');
  117. $this->assertSame('121.1000000000',$this->amount(1));
  118. $this->assertSame(0,$this->service->settle('2026-10-24')['changed']);
  119. }
  120. public function test_depth_buckets_keep_precision_until_the_final_aggregate(): void
  121. {
  122. $second=ShareSettlementService::depthCommission('0.000000006','10',2);
  123. $third=ShareSettlementService::depthCommission('0.00000006','10',3);
  124. $this->assertSame('0.000000000060000000000000000000',$second);
  125. $this->assertSame('0.000000000060000000000000000000',$third);
  126. $this->assertSame('0.0000000001',bcadd(bcadd($second,$third,30),'0',10));
  127. $this->assertSame('0.000000000000000000000000000000',ShareSettlementService::depthCommission('1000','0',99));
  128. $this->assertSame('1000.000000000000000000000000000000',ShareSettlementService::depthCommission('1000','100',999));
  129. $this->assertSame('0.000000000000000000000000000000',ShareSettlementService::depthCommission('1000','0.1',999));
  130. }
  131. public function test_formula_cutover_preserves_booked_history_and_changes_only_following_days(): void
  132. {
  133. $this->fact(2,'200'); $this->fact(3,'20000');
  134. DB::table('share_policies')->where('effective_date','2026-10-24')->update(['indirect_bonus_rate'=>30]);
  135. DB::table('share_system_state')->insert(['key'=>'commission_depth_rate_start','value'=>'2099-01-01']);
  136. $this->service->settle('2026-10-24');
  137. $this->assertSame('6.2000000000',$this->amount(1));
  138. DB::table('share_system_state')->where('key','commission_depth_rate_start')->delete();
  139. $before=DB::table('share_accounts')->get()->toJson();
  140. $ledger=DB::table('share_ledger')->get()->toJson();
  141. $wallets=DB::table('wallets')->get()->toJson();
  142. $migration=require dirname(__DIR__,2).'/database/migrations/2026_10_24_150000_use_depth_rates_for_member_sharing.php';
  143. $migration->up();
  144. $this->assertSame('2026-10-26',DB::table('share_system_state')->where('key','commission_depth_rate_start')->value('value'));
  145. $this->assertSame($before,DB::table('share_accounts')->get()->toJson());
  146. $this->assertSame($ledger,DB::table('share_ledger')->get()->toJson());
  147. $this->assertSame($wallets,DB::table('wallets')->get()->toJson());
  148. $this->assertSame(0,$this->service->settle('2026-10-24')['changed']);
  149. $this->fact(2,'200','2026-10-26'); $this->fact(3,'20000','2026-10-26');
  150. $this->assertSame('0.0200000000',$this->service->projection('2026-10-26')['rows'][1]['third:2']['indirect_commission']);
  151. Carbon::setTestNow(Carbon::parse('2026-10-27 12:00:00','Asia/Shanghai'));
  152. $migration->up();
  153. $this->assertSame('2026-10-26',DB::table('share_system_state')->where('key','commission_depth_rate_start')->value('value'));
  154. }
  155. public function test_fresh_installation_requires_no_legacy_bonus_or_waiting_day(): void
  156. {
  157. DB::table('share_settings')->update(['activation_date'=>null]);
  158. $migration=require dirname(__DIR__,2).'/database/migrations/2026_10_24_150000_use_depth_rates_for_member_sharing.php';
  159. $migration->up();
  160. $this->assertSame('1970-01-01',DB::table('share_system_state')->where('key','commission_depth_rate_start')->value('value'));
  161. $migration->down();
  162. $this->assertFalse(DB::table('share_system_state')->where('key','commission_depth_rate_start')->exists());
  163. }
  164. public function test_qualification_excludes_performance_and_later_deposits_do_not_requalify_previous_days(): void
  165. {
  166. $this->fact(2,'200'); $this->fact(3,'20000');
  167. DB::table('balance_logs')->where('member_id','M3')->update(['created_at'=>'2026-10-25 00:01:00']);
  168. $projection=$this->service->projection('2026-10-24');
  169. $this->assertArrayNotHasKey(2,$projection['rows']);
  170. $this->assertSame('0',$projection['rows'][1]['third:2']['indirect_performance']);
  171. $this->assertSame(1,$projection['rows'][1]['third:2']['valid_direct_members']);
  172. $this->fact(2,'10','2026-10-25'); $this->fact(3,'20000','2026-10-25');
  173. $this->assertArrayHasKey(2,$this->service->projection('2026-10-25')['rows']);
  174. }
  175. public function test_all_game_cumulative_qualification_but_category_tiers_stay_independent(): void
  176. {
  177. $this->fact(2,'50'); $this->fact(2,'60','2026-10-23',['category'=>'self:sport']);
  178. $this->fact(5,'20000','2026-10-24',['category'=>'self:lottery']);
  179. $rows=$this->service->projection('2026-10-24')['rows'][1];
  180. $this->assertSame('50.0000000000',$rows['third:2']['direct_performance']);
  181. $this->assertSame('0',$rows['third:2']['rate']);
  182. $this->assertSame('0.20',$rows['self:lottery']['rate']);
  183. $this->assertSame(2,$rows['self:lottery']['valid_direct_members']);
  184. }
  185. public function test_late_result_reconciles_original_day_and_later_cumulative_qualifications(): void
  186. {
  187. $this->fact(2,'200'); $this->fact(3,'99');
  188. $this->service->settle('2026-10-24'); $this->assertSame('0.2000000000',$this->amount(1));
  189. $this->fact(3,'1','2026-10-23');
  190. DB::table('share_system_state')->insert(['key'=>'facts_dirty_dates','value'=>'["2026-10-23"]']);
  191. $result=$this->service->reconcile();
  192. $this->assertSame(1,$result['dates']); $this->assertSame('0.2000990000',$this->amount(1));
  193. $this->assertSame('[]',DB::table('share_system_state')->where('key','facts_dirty_dates')->value('value'));
  194. }
  195. public function test_paid_reversal_offsets_future_earnings_without_touching_main_wallet_or_reserved_claim(): void
  196. {
  197. $this->fact(2,'200'); $c=$this->fact(3,'20000'); $this->service->settle('2026-10-24');
  198. DB::table('share_accounts')->where('user_id',1)->update(['paid_amount'=>'0.22']);
  199. $wallet=DB::table('wallets')->get()->toJson();
  200. DB::table('share_bet_facts')->where('id',$c)->update(['voided'=>1,'effective_amount'=>0,'source_version'=>'canceled']);
  201. $this->service->settle('2026-10-24');
  202. $this->assertSame('0.2000000000',$this->amount(1)); $this->assertSame('0.2200000000',$this->amount(1,'paid_amount'));
  203. $this->assertSame('0.0000000000',$this->amount(2));
  204. Carbon::setTestNow(Carbon::parse('2026-10-26 12:00:00','Asia/Shanghai'));
  205. $this->fact(2,'10000','2026-10-25'); $this->service->settle('2026-10-25');
  206. $this->assertSame('10.2000000000',$this->amount(1));
  207. $this->assertSame('9.9800000000',bcsub($this->amount(1),$this->amount(1,'paid_amount'),10));
  208. $this->assertSame($wallet,DB::table('wallets')->get()->toJson());
  209. }
  210. public function test_policy_snapshot_is_used_for_replay_and_preview_is_read_only(): void
  211. {
  212. $fact=$this->fact(2,'200'); $this->service->settle('2026-10-24');
  213. $rules=ShareDefaults::rules(); $rules['third:2']=[['min_valid_members'=>0,'min_performance'=>'0','rate'=>'99']];
  214. DB::table('share_policies')->insert(['effective_date'=>'2026-10-25','enabled'=>1,'benefit_enabled'=>1,'valid_member_min_deposit'=>20,
  215. 'valid_member_min_effective_bet'=>100,'rules'=>json_encode($rules)]);
  216. DB::table('share_bet_facts')->where('id',$fact)->update(['effective_amount'=>'300']);
  217. $before=DB::table('share_accounts')->get()->toJson();
  218. $this->assertSame('0.1000000000',$this->service->settle('2026-10-24',false)['delta']);
  219. $this->assertSame($before,DB::table('share_accounts')->get()->toJson());
  220. $this->service->settle('2026-10-24'); $this->assertSame('0.3000000000',$this->amount(1));
  221. }
  222. public function test_grades_use_incremental_thresholds_and_never_downgrade_on_correction(): void
  223. {
  224. DB::table('share_levels')->insert([['level'=>2,'name'=>'二级','incremental_commission'=>100],['level'=>3,'name'=>'三级','incremental_commission'=>300]]);
  225. $fact=$this->fact(2,'50000'); $this->service->settle('2026-10-24');
  226. $this->assertSame(2,DB::table('share_members')->where('user_id',1)->value('level'));
  227. DB::table('share_bet_facts')->where('id',$fact)->update(['effective_amount'=>160000]);
  228. $this->service->settle('2026-10-24');
  229. $this->assertSame('400.0000000000',$this->amount(1));
  230. $this->assertSame(3,DB::table('share_members')->where('user_id',1)->value('level'));
  231. DB::table('share_bet_facts')->where('id',$fact)->update(['effective_amount'=>200]); $this->service->settle('2026-10-24');
  232. $this->assertSame(3,DB::table('share_members')->where('user_id',1)->value('level'));
  233. }
  234. public function test_later_first_binding_keeps_subtree_but_does_not_give_old_bets_to_new_ancestor(): void
  235. {
  236. $this->fact(2,'200'); $this->fact(3,'20000');
  237. $this->relations->bind(1,6,'A');
  238. $this->assertSame(3,DB::table('share_tree')->where('ancestor_user_id',6)->where('descendant_user_id',3)->value('depth'));
  239. $this->assertArrayNotHasKey(6,$this->service->projection('2026-10-24')['rows']);
  240. $this->expectException(\RuntimeException::class); $this->relations->bind(6,3,'X');
  241. }
  242. public function test_invalid_legacy_codes_and_cycles_are_isolated_without_guessing_or_mutating_users(): void
  243. {
  244. foreach ([[8,'P','Q'],[9,'Q','P'],[10,'R','MISSING'],[11,'DUP',null],[12,'DUP',null],[13,'CHILD','DUP']] as [$id,$code,$parent]) {
  245. DB::table('users')->insert(['id'=>$id,'member_id'=>'M'.$id,'user_code'=>$code,'agent_user_code'=>$parent,'created_at'=>'2026-10-01 00:00:00']);
  246. }
  247. $users=DB::table('users')->get()->toJson();
  248. $preview=$this->relations->initialize(); $this->assertSame(6,$preview['created']);
  249. $this->assertSame(7,DB::table('share_members')->count());
  250. $this->relations->initialize(true);
  251. $this->assertSame('cycle',DB::table('share_members')->where('user_id',8)->value('binding_status'));
  252. $this->assertNull(DB::table('share_members')->where('user_id',9)->value('parent_user_id'));
  253. $this->assertSame('missing',DB::table('share_members')->where('user_id',10)->value('binding_status'));
  254. $this->assertSame('ambiguous',DB::table('share_members')->where('user_id',13)->value('binding_status'));
  255. $this->assertSame($users,DB::table('users')->get()->toJson());
  256. $this->assertSame(0,$this->relations->initialize(true)['created']);
  257. }
  258. public function test_fault_rolls_back_every_ledger_account_and_period_write(): void
  259. {
  260. $this->fact(2,'200');
  261. DB::statement("CREATE TRIGGER reject_share_ledger BEFORE INSERT ON bot_share_ledger BEGIN SELECT RAISE(ABORT,'injected failure'); END");
  262. try { $this->service->settle('2026-10-24'); $this->fail('ledger failure expected'); } catch (\Illuminate\Database\QueryException $e) { $this->assertStringContainsString('injected failure',$e->getMessage()); }
  263. $this->assertSame('0.0000000000',$this->amount(1)); $this->assertSame(0,DB::table('share_settlements')->count());
  264. $this->assertSame(0,DB::table('share_ledger')->count()); $this->assertSame(0,DB::transactionLevel());
  265. DB::statement('DROP TRIGGER reject_share_ledger');
  266. $this->service->settle('2026-10-24'); $this->assertSame('0.2000000000',$this->amount(1));
  267. }
  268. public function test_today_never_becomes_claimable_and_foreign_currency_is_not_combined(): void
  269. {
  270. $this->fact(2,'200'); $this->fact(3,'20000','2026-10-24',['currency'=>'USD']);
  271. $this->assertSame('0',$this->service->projection('2026-10-24')['rows'][1]['third:2']['indirect_performance']);
  272. $this->expectException(\InvalidArgumentException::class); $this->service->settle('2026-10-25');
  273. }
  274. public function test_open_day_stays_queued_and_a_missed_midnight_run_is_automatically_recovered(): void
  275. {
  276. Carbon::setTestNow(Carbon::parse('2026-10-24 12:00:00','Asia/Shanghai'));
  277. $this->fact(2,'200');
  278. DB::table('share_system_state')->insert(['key'=>'facts_dirty_dates','value'=>'["2026-10-24"]']);
  279. $this->assertSame(0,$this->service->reconcile()['dates']);
  280. $this->assertSame(['2026-10-24'],json_decode(DB::table('share_system_state')->where('key','facts_dirty_dates')->value('value'),true));
  281. Carbon::setTestNow(Carbon::parse('2026-10-26 12:00:00','Asia/Shanghai'));
  282. $this->service->settle('2026-10-25');
  283. $this->assertSame(1,$this->service->reconcile()['dates']);
  284. $this->assertSame('0.2000000000',$this->amount(1));
  285. }
  286. public function test_real_members_without_an_invitation_code_receive_one_once_without_rebinding(): void
  287. {
  288. DB::table('users')->insert(['id'=>8,'member_id'=>'M8','user_code'=>'','agent_user_code'=>null,'from'=>-1,'created_at'=>'2026-10-01 00:00:00']);
  289. $this->assertSame(1,$this->relations->initialize()['invitation_codes_assigned']);
  290. $this->assertSame('',DB::table('users')->where('id',8)->value('user_code'));
  291. $this->relations->initialize(true);
  292. $code=DB::table('users')->where('id',8)->value('user_code');
  293. $this->assertNotSame('',$code); $this->assertNotSame('0',$code);
  294. $this->assertSame($code,DB::table('share_members')->where('user_id',8)->value('invitation_code'));
  295. $this->assertNull(DB::table('share_members')->where('user_id',8)->value('parent_user_id'));
  296. $this->relations->initialize(true);
  297. $this->assertSame($code,DB::table('users')->where('id',8)->value('user_code'));
  298. }
  299. public function test_entry_visibility_does_not_control_monetary_policy_and_pre_activation_bets_never_pay(): void
  300. {
  301. $this->fact(2,'200');
  302. DB::table('share_policies')->where('effective_date','2026-10-24')->update(['enabled'=>0]);
  303. $this->service->settle('2026-10-24');
  304. $this->assertSame('0.2000000000',$this->amount(1));
  305. $this->assertSame([],$this->service->projection('2026-10-23')['rows']);
  306. $this->assertSame(0,$this->service->settle('2026-10-23')['changed']);
  307. }
  308. public function test_new_telegram_member_after_initialization_gets_a_root_and_account_without_reinitializing_others(): void
  309. {
  310. if (!function_exists('removeZero')) require_once dirname(__DIR__,2).'/app/Helpers/helpers.php';
  311. DB::table('wallets')->insert(['member_id'=>'M99']);
  312. $user=\App\Services\PublicService::index('M99','telegram-new','新会员');
  313. $id=(int)$user->id;
  314. $this->assertSame(-1,(int)$user->from);
  315. $this->assertSame(1,(int)$user->recharge_channel_group_id);
  316. $this->assertSame('root',DB::table('share_members')->where('user_id',$id)->value('binding_status'));
  317. $this->assertSame((string)$user->user_code,DB::table('share_members')->where('user_id',$id)->value('invitation_code'));
  318. $this->assertSame(0,DB::table('share_tree')->where('ancestor_user_id',$id)->where('descendant_user_id',$id)->value('depth'));
  319. $this->assertSame('0.0000000000',$this->amount($id));
  320. $this->assertSame(8,DB::table('share_members')->count());
  321. }
  322. }