Files

186 lines
7.1 KiB
PHP

<?php
namespace App\Http\Controllers\API\v1\Admin\Allowance;
use App\AllowanceClaimCode;
use App\AllowancePayout;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Util;
use App\Result;
use App\Voter;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Hash;
class PayoutController extends Controller
{
public function __invoke(Request $request)
{
$this->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,
'paid_by_admin_name' => $request->user() ? $request->user()->name : null,
'paid_by_admin_email' => $request->user() ? $request->user()->email : null,
]);
}
}