namespace App\Services; use App\Models\Nominee; use App\Models\Transaction; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class VoteService { /** * Verify a Paystack reference server-side and, if successful, * atomically credit votes + mark the transaction as paid. * Idempotent: replayed webhooks/callbacks are safely ignored. */ public function verifyAndCredit(string $reference): Transaction { $tx = Transaction::where('paystack_reference', $reference)->firstOrFail(); if ($tx->status === 'success') { return $tx; // already processed — no double-crediting } $response = Http::withToken(config('services.paystack.secretKey')) ->acceptJson() ->get(config('services.paystack.paymentUrl') . "/transaction/verify/{$reference}"); $data = $response->json('data') ?? []; $paidAmount = (int) ($data['amount'] ?? 0); // kobo $expected = (int) round($tx->amount_paid * 100); // kobo if ($response->successful() && ($data['status'] ?? '') === 'success' && $paidAmount === $expected) { DB::transaction(function () use ($tx) { // Lock the row to prevent double-crediting races $tx = Transaction::whereKey($tx->id)->lockForUpdate()->first(); if ($tx->status !== 'success') { $tx->update(['status' => 'success', 'paid_at' => now()]); Nominee::whereKey($tx->nominee_id)->increment('votes', $tx->vote_count); } }); $tx->refresh(); } else { $tx->update(['status' => 'failed']); Log::warning('Paystack verification failed', ['reference' => $reference, 'body' => $response->json()]); } return $tx; } }