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())); } }