Files
BetiX/app/Services/WalletService.php
Dolo 0280278978
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
Initialer Laravel Commit für BetiX
2026-04-04 18:01:50 +02:00

221 lines
8.4 KiB
PHP

<?php
namespace App\Services;
use App\Models\User;
use App\Models\WalletTransfer;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
class WalletService
{
/**
* Verify and handle Vault PIN
*/
public function verifyVaultPin(User $user, string $pin): ?\Illuminate\Http\JsonResponse
{
// Locked?
if (!empty($user->vault_pin_locked_until) && now()->lessThan($user->vault_pin_locked_until)) {
return response()->json([
'message' => 'Vault PIN locked. Try again later.',
'locked_until' => optional($user->vault_pin_locked_until)->toIso8601String(),
], 423);
}
if (empty($user->vault_pin_hash)) {
return response()->json(['message' => 'Vault PIN not set'], 400);
}
if (!Hash::check($pin, $user->vault_pin_hash)) {
// increment attempts and maybe lock
$attempts = (int) ($user->vault_pin_attempts ?? 0) + 1;
$user->vault_pin_attempts = $attempts;
if ($attempts >= 5) {
$user->vault_pin_locked_until = now()->addMinutes(15);
$user->vault_pin_attempts = 0; // reset after lock
}
$user->save();
return response()->json(['message' => 'Invalid PIN'], 423);
}
// success → reset attempts
if (!empty($user->vault_pin_attempts)) {
$user->vault_pin_attempts = 0;
$user->save();
}
return null;
}
/**
* Get the vault balance for a given currency.
*/
public function getVaultBalance(User $user, string $currency): float
{
if ($currency === 'BTX') {
return (float) ($user->vault_balance ?? 0);
}
$map = $user->vault_balances ?? [];
return (float) ($map[$currency] ?? 0);
}
/**
* Get the wallet balance for a given currency.
*/
public function getWalletBalance(User $user, string $currency): float
{
if ($currency === 'BTX') {
return (float) ($user->balance ?? 0);
}
// For other currencies, check wallets relationship or JSON field if present
$map = $user->vault_balances ?? []; // re-use structure — wallet balance not stored here
// Non-BTX wallet balances are not tracked in the main user table yet
return 0.0;
}
/**
* Deposit from main balance to vault.
*/
public function depositToVault(User $user, string $amountStr, ?string $idempotencyKey = null, string $currency = 'BTX'): array
{
$amount = (float) $amountStr;
if ($amount <= 0) {
abort(422, 'Amount must be greater than zero');
}
if ($idempotencyKey) {
$existing = WalletTransfer::where('user_id', $user->id)
->where('idempotency_key', $idempotencyKey)
->first();
if ($existing) {
return $this->buildBalanceResponse($user, $currency);
}
}
return DB::transaction(function () use ($user, $amount, $idempotencyKey, $currency) {
$u = User::where('id', $user->id)->lockForUpdate()->first();
if ($currency === 'BTX') {
$balanceBefore = (float) $u->balance;
$vaultBefore = (float) $u->vault_balance;
if ($balanceBefore < $amount) abort(400, 'Insufficient balance');
$u->balance = $balanceBefore - $amount;
$u->vault_balance = $vaultBefore + $amount;
$u->save();
WalletTransfer::create([
'user_id' => $u->id,
'type' => 'deposit',
'amount' => $amount,
'balance_before' => $balanceBefore,
'balance_after' => (float) $u->balance,
'vault_before' => $vaultBefore,
'vault_after' => (float) $u->vault_balance,
'currency' => $currency,
'idempotency_key' => $idempotencyKey,
'meta' => null,
]);
} else {
// Non-BTX: use vault_balances JSON map
$map = $u->vault_balances ?? [];
$vaultBefore = (float) ($map[$currency] ?? 0);
// Wallet balance for non-BTX is not in DB yet — block if 0
abort(400, "Deposits for {$currency} are not yet supported.");
}
return $this->buildBalanceResponse($u, $currency);
});
}
/**
* Withdraw from vault to main balance.
*/
public function withdrawFromVault(User $user, string $amountStr, ?string $idempotencyKey = null, string $currency = 'BTX'): array
{
$amount = (float) $amountStr;
if ($amount <= 0) {
abort(422, 'Amount must be greater than zero');
}
if (!empty($user->withdraw_cooldown_until) && now()->lessThan($user->withdraw_cooldown_until)) {
abort(429, 'Withdraw cooldown active. Try again at ' . $user->withdraw_cooldown_until->toIso8601String());
}
if ($idempotencyKey) {
$existing = WalletTransfer::where('user_id', $user->id)
->where('idempotency_key', $idempotencyKey)
->first();
if ($existing) {
return $this->buildBalanceResponse($user, $currency);
}
}
return DB::transaction(function () use ($user, $amount, $idempotencyKey, $currency) {
$u = User::where('id', $user->id)->lockForUpdate()->first();
if ($currency === 'BTX') {
$balanceBefore = (float) $u->balance;
$vaultBefore = (float) $u->vault_balance;
if ($vaultBefore < $amount) abort(400, 'Insufficient vault balance');
$u->vault_balance = $vaultBefore - $amount;
$u->balance = $balanceBefore + $amount;
$u->withdraw_cooldown_until = now()->addMinutes(30);
$u->save();
WalletTransfer::create([
'user_id' => $u->id,
'type' => 'withdraw',
'amount' => $amount,
'balance_before' => $balanceBefore,
'balance_after' => (float) $u->balance,
'vault_before' => $vaultBefore,
'vault_after' => (float) $u->vault_balance,
'currency' => $currency,
'idempotency_key' => $idempotencyKey,
'meta' => null,
]);
} else {
$map = $u->vault_balances ?? [];
$vaultBefore = (float) ($map[$currency] ?? 0);
if ($vaultBefore < $amount) abort(400, 'Insufficient vault balance');
$map[$currency] = number_format($vaultBefore - $amount, 8, '.', '');
$u->vault_balances = $map;
$u->withdraw_cooldown_until = now()->addMinutes(30);
$u->save();
WalletTransfer::create([
'user_id' => $u->id,
'type' => 'withdraw',
'amount' => $amount,
'balance_before' => 0,
'balance_after' => 0,
'vault_before' => $vaultBefore,
'vault_after' => (float) $map[$currency],
'currency' => $currency,
'idempotency_key' => $idempotencyKey,
'meta' => null,
]);
}
return $this->buildBalanceResponse($u, $currency);
});
}
/**
* Build the balance response for a given currency.
*/
private function buildBalanceResponse(User $user, string $currency): array
{
$map = $user->vault_balances ?? [];
return [
'balance' => $currency === 'BTX' ? (string) $user->balance : '0',
'vault_balance' => $currency === 'BTX'
? (string) $user->vault_balance
: (string) ($map[$currency] ?? '0'),
'vault_balances' => array_merge(['BTX' => (string) ($user->vault_balance ?? '0')], $map),
];
}
}