where('idempotency_key', $idempotencyKey) ->first(); if ($existing !== null) { return $existing; } $payment = PaymentTransaction::query()->create([ 'uuid' => (string) Str::uuid(), 'user_id' => $user->id, 'provider' => config('laverie.payment.default_provider', 'simulated'), 'provider_payment_id' => 'sim-'.Str::random(16), 'amount' => $amount, 'currency' => 'EUR', 'status' => 'initiated', 'idempotency_key' => $idempotencyKey, 'return_url' => $returnUrl, ]); $this->auditService->log($user, 'payment.initiated', $payment, null, $payment->toArray()); return $payment; } public function confirmTopUp(string $paymentUuid): PaymentTransaction { return DB::transaction(function () use ($paymentUuid) { $payment = PaymentTransaction::query() ->where('uuid', $paymentUuid) ->lockForUpdate() ->firstOrFail(); if ($payment->status === 'succeeded') { return $payment; } if (! in_array($payment->status, ['initiated', 'pending'], true)) { throw new \RuntimeException("Paiement non confirmable (statut : {$payment->status})."); } $payment->update(['status' => 'succeeded']); $this->walletService->credit( $payment->user, (float) $payment->amount, PaymentTransaction::class, $payment->id, "payment-credit:{$payment->uuid}", ['provider' => $payment->provider], ); $this->auditService->log( $payment->user, 'payment.confirmed', $payment, null, $payment->fresh()->toArray(), ); return $payment->fresh(); }); } public function handleWebhook(string $provider, array $payload): void { $providerPaymentId = $payload['provider_payment_id'] ?? $payload['payment_id'] ?? null; $idempotencyKey = $payload['idempotency_key'] ?? null; $status = $payload['status'] ?? 'succeeded'; if ($providerPaymentId === null && $idempotencyKey === null) { throw new \InvalidArgumentException('Webhook paiement invalide.'); } DB::transaction(function () use ($provider, $payload, $providerPaymentId, $idempotencyKey, $status) { $query = PaymentTransaction::query()->lockForUpdate(); if ($idempotencyKey !== null) { $payment = $query->where('idempotency_key', $idempotencyKey)->first(); } else { $payment = $query ->where('provider', $provider) ->where('provider_payment_id', $providerPaymentId) ->first(); } if ($payment === null) { return; } if ($payment->status === 'succeeded') { return; } $payment->update([ 'raw_payload' => $payload, ]); if ($status === 'succeeded') { $this->confirmTopUp($payment->uuid); } elseif (in_array($status, ['failed', 'cancelled', 'refunded'], true)) { $payment->update(['status' => $status]); } }); } }