127 lines
3.9 KiB
PHP
127 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Exceptions\WalletInsufficientFundsException;
|
|
use App\Models\User;
|
|
use App\Models\Wallet;
|
|
use App\Models\WalletTransaction;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
|
|
class WalletService
|
|
{
|
|
public function getBalance(User $user): float
|
|
{
|
|
$wallet = $this->findOrCreateWallet($user);
|
|
|
|
return (float) $wallet->current_balance;
|
|
}
|
|
|
|
public function credit(
|
|
User $user,
|
|
float $amount,
|
|
string $sourceType,
|
|
?int $sourceId = null,
|
|
?string $idempotencyKey = null,
|
|
?array $metadata = null,
|
|
): WalletTransaction {
|
|
return $this->applyMovement($user, 'credit', $amount, $sourceType, $sourceId, $idempotencyKey, $metadata);
|
|
}
|
|
|
|
public function debit(
|
|
User $user,
|
|
float $amount,
|
|
string $sourceType,
|
|
?int $sourceId = null,
|
|
?string $idempotencyKey = null,
|
|
?array $metadata = null,
|
|
): WalletTransaction {
|
|
return $this->applyMovement($user, 'debit', $amount, $sourceType, $sourceId, $idempotencyKey, $metadata);
|
|
}
|
|
|
|
private function applyMovement(
|
|
User $user,
|
|
string $type,
|
|
float $amount,
|
|
string $sourceType,
|
|
?int $sourceId,
|
|
?string $idempotencyKey,
|
|
?array $metadata,
|
|
): WalletTransaction {
|
|
if ($amount <= 0) {
|
|
throw new \InvalidArgumentException('Le montant doit être strictement positif.');
|
|
}
|
|
|
|
return DB::transaction(function () use ($user, $type, $amount, $sourceType, $sourceId, $idempotencyKey, $metadata) {
|
|
if ($idempotencyKey !== null) {
|
|
$existing = WalletTransaction::query()
|
|
->where('idempotency_key', $idempotencyKey)
|
|
->first();
|
|
|
|
if ($existing !== null) {
|
|
return $existing;
|
|
}
|
|
}
|
|
|
|
$wallet = Wallet::query()
|
|
->where('user_id', $user->id)
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if ($wallet === null) {
|
|
$wallet = $this->createWallet($user);
|
|
$wallet = Wallet::query()->whereKey($wallet->id)->lockForUpdate()->firstOrFail();
|
|
}
|
|
|
|
if (! $wallet->isActive()) {
|
|
throw new \RuntimeException('Le porte-monnaie n\'est pas actif.');
|
|
}
|
|
|
|
$balanceBefore = (float) $wallet->current_balance;
|
|
|
|
if ($type === 'debit' && $balanceBefore < $amount) {
|
|
throw new WalletInsufficientFundsException($amount, $balanceBefore);
|
|
}
|
|
|
|
$balanceAfter = $type === 'credit'
|
|
? round($balanceBefore + $amount, 2)
|
|
: round($balanceBefore - $amount, 2);
|
|
|
|
$wallet->update(['current_balance' => $balanceAfter]);
|
|
|
|
return WalletTransaction::query()->create([
|
|
'uuid' => (string) Str::uuid(),
|
|
'wallet_id' => $wallet->id,
|
|
'type' => $type,
|
|
'amount' => $amount,
|
|
'balance_before' => $balanceBefore,
|
|
'balance_after' => $balanceAfter,
|
|
'source_type' => $sourceType,
|
|
'source_id' => $sourceId,
|
|
'idempotency_key' => $idempotencyKey,
|
|
'metadata' => $metadata,
|
|
'created_at' => now(),
|
|
]);
|
|
});
|
|
}
|
|
|
|
private function findOrCreateWallet(User $user): Wallet
|
|
{
|
|
return Wallet::query()->firstOrCreate(
|
|
['user_id' => $user->id],
|
|
['currency' => 'EUR', 'current_balance' => 0, 'status' => 'active'],
|
|
);
|
|
}
|
|
|
|
private function createWallet(User $user): Wallet
|
|
{
|
|
return Wallet::query()->create([
|
|
'user_id' => $user->id,
|
|
'currency' => 'EUR',
|
|
'current_balance' => 0,
|
|
'status' => 'active',
|
|
]);
|
|
}
|
|
}
|