AgentEquityRechargeLockTest.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. <?php
  2. namespace Tests\Integration;
  3. use App\Console\Commands\AgentReleaseManualCredits;
  4. use App\Http\Controllers\admin\Agent as AdminAgent;
  5. use App\Models\Agent\Agent;
  6. use App\Models\Agent\AgentBalanceLog;
  7. use App\Models\Agent\AgentManualCreditLock;
  8. use App\Models\Agent\AgentRechargeLock;
  9. use App\Models\Agent\AgentSetting;
  10. use App\Models\Agent\AgentWithdrawAccount;
  11. use App\Services\Agent\AgentEquityService;
  12. use App\Services\Agent\AgentWalletService;
  13. use App\Services\Agent\AgentWithdrawalService;
  14. use Carbon\Carbon;
  15. use Illuminate\Http\Request;
  16. use Illuminate\Support\Facades\DB;
  17. use PHPUnit\Framework\TestCase;
  18. use Symfony\Component\Console\Tester\CommandTester;
  19. use Tests\Support\AgentTestEnvironment;
  20. /** 入股代理充值后 N 天内不能提现,可以立即购买股份(优先消耗锁定中的充值)。 */
  21. class AgentEquityRechargeLockTest extends TestCase
  22. {
  23. use AgentTestEnvironment;
  24. private AgentWalletService $wallets;
  25. private function recharge(Agent $agent, string $amount, string $key): void
  26. {
  27. $this->wallets->adjust($agent->id, $amount, 'recharge', '充值', $key, 'system', null, 'agent_recharge', 1);
  28. }
  29. private function setUpEquity(int $lockDays = 7): void
  30. {
  31. $this->wallets = new AgentWalletService();
  32. AgentSetting::current();
  33. DB::table('agent_settings')->where('id', 1)->update(['equity_enabled' => 1, 'equity_price_per_percent' => 1000,
  34. 'equity_min_percent' => 0.1, 'equity_max_percent' => 1, 'equity_recharge_lock_days' => $lockDays]);
  35. }
  36. public function test_recharge_is_locked_for_seven_days_then_withdrawable(): void
  37. {
  38. $this->setUpEquity();
  39. $this->paymentChannels();
  40. $agent = $this->agent(Agent::TYPE_EQUITY);
  41. $this->recharge($agent, '1000', 'r1');
  42. $agent = $agent->fresh();
  43. $this->assertSame('0.0000', $this->wallets->withdrawable($agent));
  44. $funds = $this->wallets->funds($agent);
  45. $this->assertSame(['1000.0000', '1000.0000', '0.0000', 7], [$funds['balance'], $funds['locked_amount'], $funds['withdrawable_amount'], $funds['recharge_lock_days']]);
  46. $locks = $this->wallets->activeLocks($agent);
  47. $this->assertSame('2026-09-14 12:00:00', $locks[0]['unlock_at']);
  48. $account = AgentWithdrawAccount::query()->create(['agent_id' => $agent->id, 'type' => 'usdt', 'network' => 'TRC20',
  49. 'account' => 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE', 'status' => 1]);
  50. try { (new AgentWithdrawalService($this->wallets))->request($agent, $account->id, '10', 'withdraw-old', 'locked'); $this->fail('locked'); }
  51. catch (\RuntimeException $e) { $this->assertSame('可提现余额不足(充值未满 7 天的金额暂不可提现)', $e->getMessage()); }
  52. Carbon::setTestNow(Carbon::parse('2026-09-14 12:00:01'));
  53. $this->assertSame('1000.0000', $this->wallets->withdrawable($agent->fresh()));
  54. $this->assertSame([], $this->wallets->activeLocks($agent));
  55. $this->assertSame('0.0000', $this->wallets->funds($agent->fresh())['locked_amount']);
  56. }
  57. public function test_share_purchase_uses_locked_recharge_first(): void
  58. {
  59. $this->setUpEquity();
  60. $agent = $this->agent(Agent::TYPE_EQUITY);
  61. $this->wallets->adjust($agent->id, '500', 'credit', '初始额度', 'c1', 'admin', 1);
  62. $this->recharge($agent, '1000', 'r1');
  63. $this->assertSame('500.0000', $this->wallets->withdrawable($agent->fresh()));
  64. $order = (new AgentEquityService($this->wallets))->order($agent->fresh(), '0.6'); // 600
  65. $this->assertSame('completed', $order->status);
  66. $agent = $agent->fresh();
  67. $this->assertSame('900.0000', $agent->balance);
  68. $this->assertSame('400.0000', $this->wallets->lockedAmount($agent));
  69. $this->assertSame('500.0000', $this->wallets->withdrawable($agent), 'the unlocked funds stay withdrawable');
  70. }
  71. public function test_admin_credit_is_frozen_can_buy_shares_and_unlocks_only_the_remainder_at_seven_days(): void
  72. {
  73. $this->setUpEquity(0);
  74. $this->paymentChannels();
  75. $agent = $this->agent(Agent::TYPE_EQUITY);
  76. $equity = new AgentEquityService($this->wallets);
  77. $log = $this->adminCredit($agent, '1000', 'c1');
  78. $retry = $this->adminCredit($agent, '1000', 'c1');
  79. $this->assertSame($log['id'], $retry['id']);
  80. $this->assertSame(AgentManualCreditLock::REFERENCE_TYPE, $log['reference_type']);
  81. $this->assertSame('0.0000', $log['after_balance']);
  82. $agent->refresh();
  83. $this->assertSame('1000.0000', $agent->frozen_balance);
  84. $this->assertSame('0.0000', $this->wallets->withdrawable($agent));
  85. $this->assertSame('1000.0000', $equity->overview($agent)['balance']);
  86. $this->assertSame('completed', $equity->order($agent, '0.6')->status);
  87. $agent->refresh();
  88. $this->assertSame('400.0000', $agent->frozen_balance);
  89. $this->assertSame('0.0000', $agent->balance);
  90. $command = new AgentReleaseManualCredits();
  91. $command->setLaravel($this->app);
  92. $runner = new CommandTester($command);
  93. Carbon::setTestNow(Carbon::parse('2026-09-14 11:59:59'));
  94. $this->assertSame(0, $runner->execute([]));
  95. $this->assertSame('400.0000', $agent->fresh()->frozen_balance);
  96. Carbon::setTestNow(Carbon::parse('2026-09-14 12:00:00'));
  97. $this->assertSame(0, $runner->execute([]));
  98. $this->assertSame(0, $runner->execute([]));
  99. $agent->refresh();
  100. $this->assertSame('400.0000', $agent->balance);
  101. $this->assertSame('400.0000', $this->wallets->withdrawable($agent));
  102. $this->assertSame('0.0000', $agent->frozen_balance);
  103. $this->assertSame('400.0000', AgentBalanceLog::query()->where('type', 'credit_unlock')->sole()->amount);
  104. }
  105. public function test_admin_credit_can_complete_a_pending_purchase_and_keeps_withdrawal_funds_separate(): void
  106. {
  107. $this->setUpEquity();
  108. $this->paymentChannels();
  109. $agent = $this->agent(Agent::TYPE_EQUITY);
  110. $equity = new AgentEquityService($this->wallets);
  111. $this->recharge($agent, '200', 'r1');
  112. $order = $equity->order($agent, '0.5');
  113. $this->assertSame('pending_payment', $order->status);
  114. $this->adminCredit($agent, '300', 'c1');
  115. $this->assertSame('completed', $order->fresh()->status);
  116. $this->assertSame('0.0000', $agent->fresh()->balance);
  117. $this->assertSame('0.0000', $this->wallets->lockedAmount($agent));
  118. $this->assertSame('0.0000', $agent->fresh()->frozen_balance);
  119. $this->adminCredit($agent, '300', 'c2');
  120. $this->wallets->adjust($agent->id, '1000', 'credit', '初始额度', 'initial', 'admin', 1);
  121. $withdrawals = new AgentWithdrawalService($this->wallets);
  122. $account = AgentWithdrawAccount::query()->create(['agent_id' => $agent->id, 'type' => 'usdt', 'network' => 'TRC20',
  123. 'account' => 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE', 'status' => 1]);
  124. $withdrawal = $withdrawals->request($agent, $account->id, '20', 'withdraw-old', 'withdraw');
  125. $this->assertSame($withdrawal->amount, $this->wallets->withdrawalFrozenBalance($agent->fresh()));
  126. $withdrawals->audit($withdrawal->id, 'reject', 1);
  127. $this->assertSame('300.0000', $agent->fresh()->frozen_balance);
  128. $this->assertSame('1000.0000', $agent->fresh()->balance);
  129. $joint = $this->agent();
  130. $this->adminCredit($joint, '100', 'c3');
  131. $this->assertSame('100.0000', $joint->fresh()->balance);
  132. $this->assertSame('0.0000', $joint->fresh()->frozen_balance);
  133. }
  134. private function adminCredit(Agent $agent, string $amount, string $requestId): array
  135. {
  136. $request = new class([], ['agent_id' => $agent->id, 'type' => 'credit', 'amount' => $amount,
  137. 'remark' => '测试加款', 'request_id' => $requestId]) extends Request {
  138. public object $user;
  139. };
  140. $request->setMethod('POST');
  141. $request->user = (object) ['id' => 1];
  142. $this->app->instance('request', $request);
  143. $result = (new AdminAgent())->adjust($request, $this->wallets, new AgentEquityService($this->wallets))->getData(true);
  144. $this->assertSame(0, $result['code'], $result['msg']);
  145. return $result['data'];
  146. }
  147. public function test_admin_debit_uses_free_balance_first_and_reduces_the_lock_only_for_spent_recharge(): void
  148. {
  149. $this->setUpEquity();
  150. $agent = $this->agent(Agent::TYPE_EQUITY);
  151. $this->recharge($agent, '1000', 'r1');
  152. $this->wallets->adjust($agent->id, '200', 'credit', '初始额度', 'c1', 'admin', 1);
  153. $this->wallets->adjust($agent->id, '100', 'debit', '扣款', 'd1', 'admin', 1);
  154. $this->assertSame('1000.0000', $this->wallets->lockedAmount($agent));
  155. $this->assertSame('100.0000', $this->wallets->withdrawable($agent->fresh()));
  156. $this->wallets->adjust($agent->id, '700', 'debit', '扣款', 'd2', 'admin', 1);
  157. $this->assertSame('400.0000', $this->wallets->lockedAmount($agent));
  158. $this->assertSame('0.0000', $this->wallets->withdrawable($agent->fresh()));
  159. }
  160. public function test_released_admin_credit_stays_withdrawable_when_a_legacy_lock_exceeds_balance(): void
  161. {
  162. $this->setUpEquity();
  163. $agent = $this->agent(Agent::TYPE_EQUITY);
  164. $this->adminCredit($agent, '300', 'c1');
  165. Carbon::setTestNow(Carbon::parse('2026-09-08 12:00:00'));
  166. $this->recharge($agent, '1000', 'r1');
  167. // 模拟旧版人工扣款只扣余额、未扣锁定额。
  168. Agent::query()->whereKey($agent->id)->update(['balance' => 400, 'realtime_balance' => 400]);
  169. Carbon::setTestNow(Carbon::parse('2026-09-14 12:00:00'));
  170. $this->assertSame(1, $this->wallets->releaseManualCredits($agent->id, now()));
  171. $funds = $this->wallets->funds($agent->fresh());
  172. $this->assertSame('700.0000', $funds['balance']);
  173. $this->assertSame('0.0000', $funds['frozen_balance']);
  174. $this->assertSame('400.0000', $funds['locked_amount']);
  175. $this->assertSame('300.0000', $funds['withdrawable_amount']);
  176. }
  177. public function test_joint_agents_and_zero_days_are_not_locked(): void
  178. {
  179. $this->setUpEquity(0);
  180. $equity = $this->agent(Agent::TYPE_EQUITY);
  181. $this->recharge($equity, '300', 'r-equity');
  182. $this->assertSame('300.0000', $this->wallets->withdrawable($equity->fresh()));
  183. DB::table('agent_settings')->where('id', 1)->update(['equity_recharge_lock_days' => 7]);
  184. $joint = $this->agent(Agent::TYPE_JOINT);
  185. $this->recharge($joint, '300', 'r-joint');
  186. $this->assertSame(0, AgentRechargeLock::query()->count(), 'joint recharges are margin, not locked');
  187. $this->assertArrayNotHasKey('locked_amount', $this->wallets->funds($joint->fresh()));
  188. }
  189. }