| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- <?php
- namespace Tests\Integration;
- use App\Console\Commands\AgentReleaseManualCredits;
- use App\Http\Controllers\admin\Agent as AdminAgent;
- use App\Models\Agent\Agent;
- use App\Models\Agent\AgentBalanceLog;
- use App\Models\Agent\AgentManualCreditLock;
- use App\Models\Agent\AgentRechargeLock;
- use App\Models\Agent\AgentSetting;
- use App\Models\Agent\AgentWithdrawAccount;
- use App\Services\Agent\AgentEquityService;
- use App\Services\Agent\AgentWalletService;
- use App\Services\Agent\AgentWithdrawalService;
- use Carbon\Carbon;
- use Illuminate\Http\Request;
- use Illuminate\Support\Facades\DB;
- use PHPUnit\Framework\TestCase;
- use Symfony\Component\Console\Tester\CommandTester;
- use Tests\Support\AgentTestEnvironment;
- /** 入股代理充值后 N 天内不能提现,可以立即购买股份(优先消耗锁定中的充值)。 */
- class AgentEquityRechargeLockTest extends TestCase
- {
- use AgentTestEnvironment;
- private AgentWalletService $wallets;
- private function recharge(Agent $agent, string $amount, string $key): void
- {
- $this->wallets->adjust($agent->id, $amount, 'recharge', '充值', $key, 'system', null, 'agent_recharge', 1);
- }
- private function setUpEquity(int $lockDays = 7): void
- {
- $this->wallets = new AgentWalletService();
- AgentSetting::current();
- DB::table('agent_settings')->where('id', 1)->update(['equity_enabled' => 1, 'equity_price_per_percent' => 1000,
- 'equity_min_percent' => 0.1, 'equity_max_percent' => 1, 'equity_recharge_lock_days' => $lockDays]);
- }
- public function test_recharge_is_locked_for_seven_days_then_withdrawable(): void
- {
- $this->setUpEquity();
- $this->paymentChannels();
- $agent = $this->agent(Agent::TYPE_EQUITY);
- $this->recharge($agent, '1000', 'r1');
- $agent = $agent->fresh();
- $this->assertSame('0.0000', $this->wallets->withdrawable($agent));
- $funds = $this->wallets->funds($agent);
- $this->assertSame(['1000.0000', '1000.0000', '0.0000', 7], [$funds['balance'], $funds['locked_amount'], $funds['withdrawable_amount'], $funds['recharge_lock_days']]);
- $locks = $this->wallets->activeLocks($agent);
- $this->assertSame('2026-09-14 12:00:00', $locks[0]['unlock_at']);
- $account = AgentWithdrawAccount::query()->create(['agent_id' => $agent->id, 'type' => 'usdt', 'network' => 'TRC20',
- 'account' => 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE', 'status' => 1]);
- try { (new AgentWithdrawalService($this->wallets))->request($agent, $account->id, '10', 'withdraw-old', 'locked'); $this->fail('locked'); }
- catch (\RuntimeException $e) { $this->assertSame('可提现余额不足(充值未满 7 天的金额暂不可提现)', $e->getMessage()); }
- Carbon::setTestNow(Carbon::parse('2026-09-14 12:00:01'));
- $this->assertSame('1000.0000', $this->wallets->withdrawable($agent->fresh()));
- $this->assertSame([], $this->wallets->activeLocks($agent));
- $this->assertSame('0.0000', $this->wallets->funds($agent->fresh())['locked_amount']);
- }
- public function test_share_purchase_uses_locked_recharge_first(): void
- {
- $this->setUpEquity();
- $agent = $this->agent(Agent::TYPE_EQUITY);
- $this->wallets->adjust($agent->id, '500', 'credit', '初始额度', 'c1', 'admin', 1);
- $this->recharge($agent, '1000', 'r1');
- $this->assertSame('500.0000', $this->wallets->withdrawable($agent->fresh()));
- $order = (new AgentEquityService($this->wallets))->order($agent->fresh(), '0.6'); // 600
- $this->assertSame('completed', $order->status);
- $agent = $agent->fresh();
- $this->assertSame('900.0000', $agent->balance);
- $this->assertSame('400.0000', $this->wallets->lockedAmount($agent));
- $this->assertSame('500.0000', $this->wallets->withdrawable($agent), 'the unlocked funds stay withdrawable');
- }
- public function test_admin_credit_is_frozen_can_buy_shares_and_unlocks_only_the_remainder_at_seven_days(): void
- {
- $this->setUpEquity(0);
- $this->paymentChannels();
- $agent = $this->agent(Agent::TYPE_EQUITY);
- $equity = new AgentEquityService($this->wallets);
- $log = $this->adminCredit($agent, '1000', 'c1');
- $retry = $this->adminCredit($agent, '1000', 'c1');
- $this->assertSame($log['id'], $retry['id']);
- $this->assertSame(AgentManualCreditLock::REFERENCE_TYPE, $log['reference_type']);
- $this->assertSame('0.0000', $log['after_balance']);
- $agent->refresh();
- $this->assertSame('1000.0000', $agent->frozen_balance);
- $this->assertSame('0.0000', $this->wallets->withdrawable($agent));
- $this->assertSame('1000.0000', $equity->overview($agent)['balance']);
- $this->assertSame('completed', $equity->order($agent, '0.6')->status);
- $agent->refresh();
- $this->assertSame('400.0000', $agent->frozen_balance);
- $this->assertSame('0.0000', $agent->balance);
- $command = new AgentReleaseManualCredits();
- $command->setLaravel($this->app);
- $runner = new CommandTester($command);
- Carbon::setTestNow(Carbon::parse('2026-09-14 11:59:59'));
- $this->assertSame(0, $runner->execute([]));
- $this->assertSame('400.0000', $agent->fresh()->frozen_balance);
- Carbon::setTestNow(Carbon::parse('2026-09-14 12:00:00'));
- $this->assertSame(0, $runner->execute([]));
- $this->assertSame(0, $runner->execute([]));
- $agent->refresh();
- $this->assertSame('400.0000', $agent->balance);
- $this->assertSame('400.0000', $this->wallets->withdrawable($agent));
- $this->assertSame('0.0000', $agent->frozen_balance);
- $this->assertSame('400.0000', AgentBalanceLog::query()->where('type', 'credit_unlock')->sole()->amount);
- }
- public function test_admin_credit_can_complete_a_pending_purchase_and_keeps_withdrawal_funds_separate(): void
- {
- $this->setUpEquity();
- $this->paymentChannels();
- $agent = $this->agent(Agent::TYPE_EQUITY);
- $equity = new AgentEquityService($this->wallets);
- $this->recharge($agent, '200', 'r1');
- $order = $equity->order($agent, '0.5');
- $this->assertSame('pending_payment', $order->status);
- $this->adminCredit($agent, '300', 'c1');
- $this->assertSame('completed', $order->fresh()->status);
- $this->assertSame('0.0000', $agent->fresh()->balance);
- $this->assertSame('0.0000', $this->wallets->lockedAmount($agent));
- $this->assertSame('0.0000', $agent->fresh()->frozen_balance);
- $this->adminCredit($agent, '300', 'c2');
- $this->wallets->adjust($agent->id, '1000', 'credit', '初始额度', 'initial', 'admin', 1);
- $withdrawals = new AgentWithdrawalService($this->wallets);
- $account = AgentWithdrawAccount::query()->create(['agent_id' => $agent->id, 'type' => 'usdt', 'network' => 'TRC20',
- 'account' => 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE', 'status' => 1]);
- $withdrawal = $withdrawals->request($agent, $account->id, '20', 'withdraw-old', 'withdraw');
- $this->assertSame($withdrawal->amount, $this->wallets->withdrawalFrozenBalance($agent->fresh()));
- $withdrawals->audit($withdrawal->id, 'reject', 1);
- $this->assertSame('300.0000', $agent->fresh()->frozen_balance);
- $this->assertSame('1000.0000', $agent->fresh()->balance);
- $joint = $this->agent();
- $this->adminCredit($joint, '100', 'c3');
- $this->assertSame('100.0000', $joint->fresh()->balance);
- $this->assertSame('0.0000', $joint->fresh()->frozen_balance);
- }
- private function adminCredit(Agent $agent, string $amount, string $requestId): array
- {
- $request = new class([], ['agent_id' => $agent->id, 'type' => 'credit', 'amount' => $amount,
- 'remark' => '测试加款', 'request_id' => $requestId]) extends Request {
- public object $user;
- };
- $request->setMethod('POST');
- $request->user = (object) ['id' => 1];
- $this->app->instance('request', $request);
- $result = (new AdminAgent())->adjust($request, $this->wallets, new AgentEquityService($this->wallets))->getData(true);
- $this->assertSame(0, $result['code'], $result['msg']);
- return $result['data'];
- }
- public function test_admin_debit_uses_free_balance_first_and_reduces_the_lock_only_for_spent_recharge(): void
- {
- $this->setUpEquity();
- $agent = $this->agent(Agent::TYPE_EQUITY);
- $this->recharge($agent, '1000', 'r1');
- $this->wallets->adjust($agent->id, '200', 'credit', '初始额度', 'c1', 'admin', 1);
- $this->wallets->adjust($agent->id, '100', 'debit', '扣款', 'd1', 'admin', 1);
- $this->assertSame('1000.0000', $this->wallets->lockedAmount($agent));
- $this->assertSame('100.0000', $this->wallets->withdrawable($agent->fresh()));
- $this->wallets->adjust($agent->id, '700', 'debit', '扣款', 'd2', 'admin', 1);
- $this->assertSame('400.0000', $this->wallets->lockedAmount($agent));
- $this->assertSame('0.0000', $this->wallets->withdrawable($agent->fresh()));
- }
- public function test_released_admin_credit_stays_withdrawable_when_a_legacy_lock_exceeds_balance(): void
- {
- $this->setUpEquity();
- $agent = $this->agent(Agent::TYPE_EQUITY);
- $this->adminCredit($agent, '300', 'c1');
- Carbon::setTestNow(Carbon::parse('2026-09-08 12:00:00'));
- $this->recharge($agent, '1000', 'r1');
- // 模拟旧版人工扣款只扣余额、未扣锁定额。
- Agent::query()->whereKey($agent->id)->update(['balance' => 400, 'realtime_balance' => 400]);
- Carbon::setTestNow(Carbon::parse('2026-09-14 12:00:00'));
- $this->assertSame(1, $this->wallets->releaseManualCredits($agent->id, now()));
- $funds = $this->wallets->funds($agent->fresh());
- $this->assertSame('700.0000', $funds['balance']);
- $this->assertSame('0.0000', $funds['frozen_balance']);
- $this->assertSame('400.0000', $funds['locked_amount']);
- $this->assertSame('300.0000', $funds['withdrawable_amount']);
- }
- public function test_joint_agents_and_zero_days_are_not_locked(): void
- {
- $this->setUpEquity(0);
- $equity = $this->agent(Agent::TYPE_EQUITY);
- $this->recharge($equity, '300', 'r-equity');
- $this->assertSame('300.0000', $this->wallets->withdrawable($equity->fresh()));
- DB::table('agent_settings')->where('id', 1)->update(['equity_recharge_lock_days' => 7]);
- $joint = $this->agent(Agent::TYPE_JOINT);
- $this->recharge($joint, '300', 'r-joint');
- $this->assertSame(0, AgentRechargeLock::query()->count(), 'joint recharges are margin, not locked');
- $this->assertArrayNotHasKey('locked_amount', $this->wallets->funds($joint->fresh()));
- }
- }
|