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), ]; } }