validate($request, [ 'method' => 'nullable|string|in:cash,bank', ]); $electionId = Util::getCurrentElection(); $method = $request->input('method', 'cash'); // voter auth guard; Auth::id() should be voter id under voterAPI $voterId = Auth::id(); $voter = $request->user(); if (!$voter || (int) $voter->id !== (int) $voterId) { return response()->json([ 'status' => 'failed', 'message' => 'Unauthenticated.', ], 401); } // Enforce method by attendance if ($method === 'cash' && (int) $voter->kehadiran !== 1) { return response()->json([ 'status' => 'failed', 'message' => 'Kod tuntutan tunai hanya untuk kehadiran Fizikal.', ], 422); } if ($method === 'bank' && (int) $voter->kehadiran !== 2) { return response()->json([ 'status' => 'failed', 'message' => 'Kod tuntutan bank hanya untuk kehadiran Maya.', ], 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 voter UI). $kehadiran = (int) $voter->kehadiran; $saham = (float) $voter->saham; $canVoteBySaham = $saham >= 500.0; $canProceedToVote = $kehadiran !== 0 && ($kehadiran !== 1 || $voter->fizikal_registration_verified_at !== null); $eligibleClaimCashWithoutVote = $method === 'cash' && $kehadiran === 1 && $canProceedToVote && !$canVoteBySaham; if (!$hasVoted && !$eligibleClaimCashWithoutVote) { return response()->json([ 'status' => 'failed', 'message' => 'Sila undi terlebih dahulu sebelum menuntut elaun.', ], 422); } // If already paid, don't generate codes $alreadyPaid = AllowancePayout::where('election_id', $electionId) ->where('voter_id', $voterId) ->where('method', $method) ->where('status', 'paid') ->exists(); if ($alreadyPaid) { return response()->json([ 'status' => 'failed', 'message' => 'Elaun anda telah direkodkan sebagai sudah dibayar.', ], 422); } // Generate a 6-digit numeric code $codeInt = random_int(0, 999999); $code = str_pad((string) $codeInt, 6, '0', STR_PAD_LEFT); $expiresAt = Carbon::now()->addHours(6); $row = AllowanceClaimCode::updateOrCreate( [ 'election_id' => $electionId, 'voter_id' => $voterId, 'method' => $method, ], [ 'code_hash' => Hash::make($code), 'code_last4' => substr($code, -4), 'expires_at' => $expiresAt, 'used_at' => null, 'used_by_admin_id' => null, ] ); activity() ->performedOn($row) ->withProperties([ 'election_id' => $electionId, 'voter_id' => (int) $voterId, 'voter_name' => $voter->name, 'attendance' => (int) $voter->kehadiran, 'method' => $method, 'expires_at' => $expiresAt->toDateTimeString(), 'code_last4' => $row->code_last4, 'ip' => $request->ip(), 'user_agent' => $request->userAgent(), ]) ->log("generate claim code ({$method}): {$voter->name}"); return response()->json([ 'status' => 'success', 'message' => 'Kod tuntutan berjaya dijana.', 'code' => $code, // return plaintext ONCE to voter 'expires_at' => $expiresAt, 'method' => $method, ]); } }