table('account_balances') ->where('account_id', $accountId) ->value('balance'); return $this->fromCents($this->toCents((string) ($value ?? '0'))); } /** @return Collection */ public function recentTransactions(int $accountId, int $limit = 10): Collection { return BalanceTransaction::query() ->where('account_id', $accountId) ->latest('id') ->limit($limit) ->get(); } public function spend(int $accountId, string $amount, string $reference, ?string $description = null): BalanceTransaction { $cents = $this->toCents($amount); if ($cents <= 0) { throw new InvalidArgumentException('Сумма списания должна быть больше нуля.'); } return DB::connection('azerothcore_auth')->transaction(function () use ($accountId, $cents, $reference, $description): BalanceTransaction { $existing = BalanceTransaction::query()->where('reference', $reference)->first(); if ($existing) { if ($existing->account_id !== $accountId || $this->toCents((string) $existing->amount) !== -$cents) { throw new InvalidArgumentException('Ключ операции уже использован с другими параметрами.'); } return $existing; } $balance = DB::connection('azerothcore_auth')->table('account_balances') ->where('account_id', $accountId) ->lockForUpdate() ->first(); $current = $this->toCents((string) ($balance->balance ?? '0')); if ($current < $cents) { throw new InvalidArgumentException('Недостаточно средств.'); } $after = $this->fromCents($current - $cents); DB::connection('azerothcore_auth')->table('account_balances') ->where('account_id', $accountId) ->update(['balance' => $after, 'updated_at' => now()]); return BalanceTransaction::query()->create([ 'account_id' => $accountId, 'type' => BalanceTransaction::TYPE_SPEND, 'amount' => $this->fromCents(-$cents), 'balance_after' => $after, 'reference' => $reference, 'metadata' => array_filter(['description' => $description]), ]); }, 3); } public function adjustByAdmin( int $accountId, string $amount, string $direction, int $adminAccountId, string $adminUsername, string $reason, ): BalanceTransaction { $cents = $this->toCents($amount); if ($cents <= 0 || ! in_array($direction, ['credit', 'debit'], true)) { throw new InvalidArgumentException('Некорректная корректировка баланса.'); } return DB::connection('azerothcore_auth')->transaction(function () use ($accountId, $cents, $direction, $adminAccountId, $adminUsername, $reason): BalanceTransaction { DB::connection('azerothcore_auth')->table('account_balances')->insertOrIgnore([ 'account_id' => $accountId, 'balance' => '0.00', 'created_at' => now(), 'updated_at' => now(), ]); $balance = DB::connection('azerothcore_auth')->table('account_balances') ->where('account_id', $accountId) ->lockForUpdate() ->firstOrFail(); $current = $this->toCents((string) $balance->balance); $delta = $direction === 'credit' ? $cents : -$cents; if ($current + $delta < 0) { throw new InvalidArgumentException('Недостаточно средств для ручного списания.'); } $after = $this->fromCents($current + $delta); DB::connection('azerothcore_auth')->table('account_balances') ->where('account_id', $accountId) ->update(['balance' => $after, 'updated_at' => now()]); return BalanceTransaction::query()->create([ 'account_id' => $accountId, 'type' => $direction === 'credit' ? BalanceTransaction::TYPE_ADMIN_CREDIT : BalanceTransaction::TYPE_ADMIN_DEBIT, 'amount' => $this->fromCents($delta), 'balance_after' => $after, 'reference' => 'admin:'.Str::uuid(), 'metadata' => [ 'admin_account_id' => $adminAccountId, 'admin_username' => $adminUsername, 'reason' => $reason, ], ]); }, 3); } /** @return Collection */ public function adminTransactions(?string $search = null, int $limit = 100): Collection { return BalanceTransaction::query() ->leftJoin('account', 'account.id', '=', 'balance_transactions.account_id') ->select('balance_transactions.*', 'account.username') ->when($search, function ($query, string $search): void { $query->where(function ($query) use ($search): void { $query->where('account.username', 'like', '%'.$search.'%'); if (ctype_digit($search)) { $query->orWhere('balance_transactions.account_id', (int) $search); } }); }) ->latest('balance_transactions.id') ->limit($limit) ->get(); } private function toCents(string $amount): int { $normalized = str_replace(',', '.', trim($amount)); if (! preg_match('/^-?\d+(?:\.\d{1,6})?$/', $normalized)) { throw new InvalidArgumentException('Некорректная сумма.'); } $negative = str_starts_with($normalized, '-'); $normalized = ltrim($normalized, '-'); [$whole, $fraction] = array_pad(explode('.', $normalized, 2), 2, ''); $cents = ((int) $whole * 100) + (int) str_pad(substr($fraction, 0, 2), 2, '0'); return $negative ? -$cents : $cents; } private function fromCents(int $cents): string { return number_format($cents / 100, 2, '.', ''); } }