validate($request, [ 'voter_id' => 'required|integer', 'method' => 'nullable|string|in:cash,bank', 'amount_cents' => 'nullable|integer|min:0', 'reference' => 'nullable|string|max:80', 'note' => 'nullable|string|max:2000', 'claim_code' => 'nullable|string|min:4|max:20', ]); $electionId = Util::getCurrentElection(); $method = $request->input('method', 'cash'); $voterId = (int) $request->input('voter_id'); $voter = Voter::where('election_id', $electionId)->where('id', $voterId)->first(); if (!$voter) { return response()->json([ 'status' => 'failed', 'message' => 'Voter not found for current election.', ], 404); } // Enforce method by attendance type (paperless replacement for coupon rules) if ($method === 'cash' && (int) $voter->kehadiran !== 1) { return response()->json([ 'status' => 'failed', 'message' => 'Cash payout only allowed for Fizikal attendance.', ], 422); } if ($method === 'bank' && (int) $voter->kehadiran !== 2) { return response()->json([ 'status' => 'failed', 'message' => 'Bank payout only allowed for Maya attendance.', ], 422); } $hasVoted = Result::where('election_id', $electionId)->where('voter_id', $voterId)->exists(); // Tunai: normally must have voted; exception — fizikal hadir & layak mesyuarat tetapi tidak dibenarkan // mengundi kerana saham < RM500 (same rule as claim code). $kehadiran = (int) $voter->kehadiran; $saham = (float) $voter->saham; $canVoteBySaham = $saham >= 500.0; $canProceedToVote = $kehadiran !== 0 && ($kehadiran !== 1 || $voter->fizikal_registration_verified_at !== null); $eligibleCashPayoutWithoutVote = $method === 'cash' && $kehadiran === 1 && $canProceedToVote && !$canVoteBySaham; if (!$hasVoted && !$eligibleCashPayoutWithoutVote) { return response()->json([ 'status' => 'failed', 'message' => 'Voter has not voted yet.', ], 422); } // Default amounts (can be overridden by passing amount_cents) $defaultAmount = $method === 'cash' ? 30000 : 15000; $amountCents = $request->filled('amount_cents') ? (int) $request->input('amount_cents') : $defaultAmount; $now = Carbon::now(); $adminId = $request->user() ? (int) $request->user()->id : null; $claimCode = $request->input('claim_code'); // For fizikal cash payout, require claim code (paperless coupon). if ($method === 'cash' && (!$claimCode || trim($claimCode) === '')) { return response()->json([ 'status' => 'failed', 'message' => 'Sila masukkan kod tuntutan (claim code) sebelum bayaran tunai dibuat.', ], 422); } $payout = DB::transaction(function () use ($electionId, $voterId, $method, $claimCode, $amountCents, $adminId, $now, $request) { // Consume claim code if provided if ($claimCode && trim($claimCode) !== '') { $row = AllowanceClaimCode::where('election_id', $electionId) ->where('voter_id', $voterId) ->where('method', $method) ->lockForUpdate() ->first(); if (!$row) { return response()->json([ 'status' => 'failed', 'message' => 'Kod tuntutan tidak dijumpai.', ], 404); } if ($row->used_at) { return response()->json([ 'status' => 'failed', 'message' => 'Kod tuntutan telah digunakan.', ], 422); } if ($row->expires_at && Carbon::now()->greaterThan($row->expires_at)) { return response()->json([ 'status' => 'failed', 'message' => 'Kod tuntutan telah tamat tempoh.', ], 422); } if (!Hash::check(trim((string) $claimCode), $row->code_hash)) { return response()->json([ 'status' => 'failed', 'message' => 'Kod tuntutan tidak sah.', ], 422); } $row->used_at = Carbon::now(); $row->used_by_admin_id = $adminId; $row->save(); } // Idempotent: if already exists (unique election_id+voter_id+method) we keep it as paid. return AllowancePayout::updateOrCreate( [ 'election_id' => $electionId, 'voter_id' => $voterId, 'method' => $method, ], [ 'status' => 'paid', 'amount_cents' => $amountCents, 'currency' => 'MYR', 'paid_by_admin_id' => $adminId, 'paid_at' => $now, 'reference' => $request->input('reference'), 'note' => $request->input('note'), ] ); }); // In case the transaction returned a JSON response (validation failure), return it directly. if ($payout instanceof \Illuminate\Http\JsonResponse) { return $payout; } activity() ->performedOn($payout) ->withProperties([ 'election_id' => $electionId, 'voter_id' => $voterId, 'voter_name' => $voter->name, 'attendance' => (int) $voter->kehadiran, 'method' => $method, 'amount_cents' => (int) $payout->amount_cents, 'currency' => $payout->currency, 'paid_by_admin_id' => $adminId, 'reference' => $payout->reference, 'ip' => $request->ip(), 'user_agent' => $request->userAgent(), ]) ->log("allowance payout ({$method}): {$voter->name}"); return response()->json([ 'status' => 'success', 'message' => 'Allowance payout recorded.', 'payout' => $payout, ]); } }