namespace App\Http\Controllers; use App\Models\Nominee; use App\Models\Transaction; use App\Services\VoteService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; class PaymentController extends Controller { /** * Step 1: validate voter input, create a pending transaction, * initialize Paystack, return the authorization URL. */ public function initialize(Request $request): JsonResponse { $validated = $request->validate([ 'nominee_id' => ['required', 'exists:nominees,id'], 'voter_name' => ['required', 'string', 'max:120'], 'voter_email' => ['required', 'email', 'max:180'], 'voter_phone' => ['nullable', 'string', 'max:20'], 'vote_count' => ['required', 'integer', 'min:1', 'max:1000'], ]); $nominee = Nominee::where('is_active', true)->findOrFail($validated['nominee_id']); $price = (int) config('services.votes.price'); $amount = $validated['vote_count'] * $price; // naira $reference = 'AGRI-' . strtoupper(Str::random(10)) . '-' . time(); $tx = Transaction::create($validated + [ 'paystack_reference' => $reference, 'amount_paid' => $amount, 'status' => 'pending', ]); $response = Http::withToken(config('services.paystack.secretKey')) ->acceptJson() ->post(config('services.paystack.paymentUrl') . '/transaction/initialize', [ 'email' => $tx->voter_email, 'amount' => $amount * 100, // kobo 'reference' => $reference, 'callback_url' => route('payment.callback'), 'metadata' => [ 'nominee_id' => $nominee->id, 'vote_count' => $tx->vote_count, 'voter_name' => $tx->voter_name, 'custom_fields' => [ ['display_name' => 'Nominee', 'variable_name' => 'nominee', 'value' => $nominee->farm_name], ], ], ]); if (! $response->successful()) { $tx->update(['status' => 'failed']); return response()->json(['message' => 'Unable to reach payment gateway. Try again.'], 502); } return response()->json([ 'authorization_url' => $response->json('data.authorization_url'), 'reference' => $reference, ]); } /** * Step 2: browser callback. NEVER trust this alone — verify server-side. */ public function callback(Request $request, VoteService $votes) { $reference = $request->query('reference'); abort_unless($reference, 400, 'Missing payment reference'); $tx = $votes->verifyAndCredit($reference); return redirect() ->route('poll.index') ->with($tx->status === 'success' ? ['success' => "Thank you {$tx->voter_name}! {$tx->vote_count} vote(s) credited."] : ['error' => 'Payment could not be verified. If you were charged, contact support with reference ' . $reference]); } /** * Step 3: Paystack webhook — authoritative channel. */ public function webhook(Request $request, VoteService $votes): JsonResponse { $signature = $request->header('x-paystack-signature'); $computed = hash_hmac('sha512', $request->getContent(), config('services.paystack.secretKey')); abort_unless(hash_equals($computed, (string) $signature), 401, 'Invalid signature'); $event = $request->json()->all(); if (($event['event'] ?? '') === 'charge.success') { $votes->verifyAndCredit($event['data']['reference']); } return response()->json(['status' => 'received']); } }