221 lines
8.4 KiB
PHP
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),
|
|
];
|
|
}
|
|
}
|