271 lines
11 KiB
PHP
271 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Ktm;
|
|
|
|
use App\Mail\KtmCreatedApplicantMail;
|
|
use App\Models\Application;
|
|
use App\Models\Election;
|
|
use App\Models\Position;
|
|
use App\Models\PusatMengundi;
|
|
use App\Models\StaffAssignment;
|
|
use App\Models\User;
|
|
use App\Services\Public\KtmVacancyService;
|
|
use App\Services\RegistrationPeriodService;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class KtmApplicationService
|
|
{
|
|
/**
|
|
* @param array{name: string, ic_number: string, phone_number: string, email: string, address?: string|null} $data
|
|
*/
|
|
public function registerKp(StaffAssignment $ktmAssignment, User $actor, array $data): Application
|
|
{
|
|
return DB::transaction(function () use ($ktmAssignment, $actor, $data): Application {
|
|
$ktmAssignment->loadMissing(['election.settings', 'pusatMengundi', 'user']);
|
|
|
|
if (! app(KtmScopeService::class)->ownsAssignment($actor, $ktmAssignment)) {
|
|
throw ValidationException::withMessages([
|
|
'assignment' => 'Anda tidak dibenarkan mendaftar KP untuk saluran ini.',
|
|
]);
|
|
}
|
|
|
|
$election = $this->assignmentElection($ktmAssignment);
|
|
$pusatMengundi = $this->assignmentPusat($ktmAssignment);
|
|
|
|
if (! app(RegistrationPeriodService::class)->isOpen($election)) {
|
|
throw ValidationException::withMessages([
|
|
'registration' => app(RegistrationPeriodService::class)->closedMessage($election),
|
|
]);
|
|
}
|
|
|
|
$kpPosition = Position::query()->where('code', 'KP')->firstOrFail();
|
|
|
|
if (! app(KtmVacancyService::class)->hasVacancyForAssignment($ktmAssignment->id, $pusatMengundi, 'KP')) {
|
|
throw ValidationException::withMessages([
|
|
'ktm_assignment_id' => 'Kekosongan KP untuk saluran ini telah penuh.',
|
|
]);
|
|
}
|
|
|
|
// SoftDeletes scope excludes soft-deleted records. whereNotIn covers 'cancelled' records
|
|
// (not soft-deleted) and 'deleted_by_ktm' as an explicit redundant safety layer.
|
|
$existing = Application::query()
|
|
->where('election_id', $ktmAssignment->election_id)
|
|
->where('ic_number', $data['ic_number'])
|
|
->whereNotIn('status', ['cancelled', 'deleted_by_ktm'])
|
|
->first();
|
|
|
|
if ($existing instanceof Application) {
|
|
throw ValidationException::withMessages([
|
|
'ic_number' => 'No. kad pengenalan ini telah mempunyai rekod permohonan aktif untuk pilihanraya semasa.',
|
|
]);
|
|
}
|
|
|
|
$application = Application::query()->create([
|
|
'election_id' => $ktmAssignment->election_id,
|
|
'pusat_mengundi_id' => $ktmAssignment->pusat_mengundi_id,
|
|
'selected_ktm_assignment_id' => $ktmAssignment->id,
|
|
'public_uuid' => (string) Str::uuid(),
|
|
'source' => 'created_by_ktm',
|
|
'status' => 'submitted',
|
|
'name' => $data['name'],
|
|
'ic_number' => $data['ic_number'],
|
|
'phone_number' => $data['phone_number'],
|
|
'email' => $data['email'],
|
|
'address' => $data['address'] ?? null,
|
|
'requested_position_id' => $kpPosition->id,
|
|
'created_by_user_id' => $actor->id,
|
|
]);
|
|
|
|
$application->statusHistories()->create([
|
|
'to_status' => 'submitted',
|
|
'changed_by_user_id' => $actor->id,
|
|
'note' => 'KP applicant created by KTM.',
|
|
'metadata' => [
|
|
'source' => 'created_by_ktm',
|
|
'ktm_assignment_id' => $ktmAssignment->id,
|
|
],
|
|
]);
|
|
|
|
Mail::to($application->email)->queue(new KtmCreatedApplicantMail($application->load('pusatMengundi'), $ktmAssignment));
|
|
|
|
activity('ktm_flow')
|
|
->performedOn($application)
|
|
->causedBy($actor)
|
|
->withProperties([
|
|
'ktm_assignment_id' => $ktmAssignment->id,
|
|
'position_code' => 'KP',
|
|
])
|
|
->log('kp_applicant_created_by_ktm');
|
|
|
|
return $application;
|
|
});
|
|
}
|
|
|
|
public function approveKp(Application $application, User $actor): StaffAssignment
|
|
{
|
|
return DB::transaction(function () use ($application, $actor): StaffAssignment {
|
|
$application->loadMissing(['selectedKtmAssignment.pusatMengundi', 'selectedKtmAssignment.election.settings', 'requestedPosition']);
|
|
$ktmAssignment = $application->selectedKtmAssignment;
|
|
|
|
if (! $ktmAssignment instanceof StaffAssignment || ! app(KtmScopeService::class)->ownsAssignment($actor, $ktmAssignment)) {
|
|
throw ValidationException::withMessages([
|
|
'application' => 'Anda tidak dibenarkan meluluskan permohonan ini.',
|
|
]);
|
|
}
|
|
|
|
$election = $this->assignmentElection($ktmAssignment);
|
|
$pusatMengundi = $this->assignmentPusat($ktmAssignment);
|
|
|
|
if (! app(RegistrationPeriodService::class)->isOpen($election)) {
|
|
throw ValidationException::withMessages([
|
|
'registration' => app(RegistrationPeriodService::class)->closedMessage($election),
|
|
]);
|
|
}
|
|
|
|
$kpPosition = Position::query()->where('code', 'KP')->firstOrFail();
|
|
|
|
if ((int) $application->requested_position_id !== (int) $kpPosition->id) {
|
|
throw ValidationException::withMessages([
|
|
'requested_position_id' => 'KTM hanya boleh meluluskan permohonan KP.',
|
|
]);
|
|
}
|
|
|
|
if (! in_array($application->status, ['submitted', 'under_ppm_review', 'approved'], true)) {
|
|
throw ValidationException::withMessages([
|
|
'status' => 'Status permohonan tidak membenarkan kelulusan KTM.',
|
|
]);
|
|
}
|
|
|
|
if (! app(KtmVacancyService::class)->hasVacancyForAssignment($ktmAssignment->id, $pusatMengundi, 'KP', $application->id)) {
|
|
throw ValidationException::withMessages([
|
|
'ktm_assignment_id' => 'Kekosongan KP untuk saluran ini telah penuh.',
|
|
]);
|
|
}
|
|
|
|
$previousStatus = $application->status;
|
|
|
|
$application->forceFill([
|
|
'status' => 'assigned',
|
|
'approved_position_id' => $kpPosition->id,
|
|
'approved_by_user_id' => $actor->id,
|
|
'approved_at' => now(),
|
|
])->save();
|
|
|
|
$assignment = StaffAssignment::query()->updateOrCreate(
|
|
['application_id' => $application->id],
|
|
[
|
|
'election_id' => $application->election_id,
|
|
'user_id' => $application->user_id,
|
|
'position_id' => $kpPosition->id,
|
|
'pusat_mengundi_id' => $application->pusat_mengundi_id,
|
|
'saluran_mengundi_id' => $ktmAssignment->saluran_mengundi_id,
|
|
'reports_to_assignment_id' => $ktmAssignment->id,
|
|
'status' => 'active',
|
|
'assigned_by_user_id' => $actor->id,
|
|
'assigned_at' => now(),
|
|
'source' => 'ktm_approval',
|
|
],
|
|
);
|
|
|
|
$application->statusHistories()->create([
|
|
'from_status' => $previousStatus,
|
|
'to_status' => 'assigned',
|
|
'changed_by_user_id' => $actor->id,
|
|
'note' => 'KP application approved by KTM.',
|
|
'metadata' => [
|
|
'ktm_assignment_id' => $ktmAssignment->id,
|
|
'position_code' => 'KP',
|
|
],
|
|
]);
|
|
|
|
$assignment->histories()->create([
|
|
'to_position_id' => $kpPosition->id,
|
|
'to_pusat_mengundi_id' => $application->pusat_mengundi_id,
|
|
'to_saluran_mengundi_id' => $ktmAssignment->saluran_mengundi_id,
|
|
'to_reports_to_assignment_id' => $ktmAssignment->id,
|
|
'changed_by_user_id' => $actor->id,
|
|
'note' => 'KP assigned by KTM approval.',
|
|
]);
|
|
|
|
activity('ktm_flow')
|
|
->performedOn($application)
|
|
->causedBy($actor)
|
|
->withProperties([
|
|
'assignment_id' => $assignment->id,
|
|
'ktm_assignment_id' => $ktmAssignment->id,
|
|
])
|
|
->log('kp_application_approved_by_ktm');
|
|
|
|
return $assignment;
|
|
});
|
|
}
|
|
|
|
public function deleteCreatedApplication(Application $application, User $actor): Application
|
|
{
|
|
return DB::transaction(function () use ($application, $actor): Application {
|
|
if (! app(KtmScopeService::class)->canAccessApplication($actor, $application)) {
|
|
throw ValidationException::withMessages([
|
|
'application' => 'Anda tidak dibenarkan memadam rekod ini.',
|
|
]);
|
|
}
|
|
|
|
if ($application->source !== 'created_by_ktm') {
|
|
throw ValidationException::withMessages([
|
|
'application' => 'Hanya rekod yang dicipta oleh KTM boleh dipadam oleh KTM.',
|
|
]);
|
|
}
|
|
|
|
if ($application->status === 'assigned') {
|
|
throw ValidationException::withMessages([
|
|
'application' => 'Rekod yang telah diassign tidak boleh dipadam oleh KTM.',
|
|
]);
|
|
}
|
|
|
|
$previousStatus = $application->status;
|
|
|
|
$application->forceFill([
|
|
'status' => 'deleted_by_ktm',
|
|
])->save();
|
|
|
|
$application->statusHistories()->create([
|
|
'from_status' => $previousStatus,
|
|
'to_status' => 'deleted_by_ktm',
|
|
'changed_by_user_id' => $actor->id,
|
|
'note' => 'KTM deleted pre-created applicant record.',
|
|
]);
|
|
|
|
activity('ktm_flow')
|
|
->performedOn($application)
|
|
->causedBy($actor)
|
|
->withProperties([
|
|
'from_status' => $previousStatus,
|
|
'to_status' => 'deleted_by_ktm',
|
|
])
|
|
->log('ktm_created_applicant_deleted');
|
|
|
|
$application->delete();
|
|
|
|
return $application;
|
|
});
|
|
}
|
|
|
|
private function assignmentElection(StaffAssignment $assignment): Election
|
|
{
|
|
/** @var Election $election */
|
|
$election = $assignment->election()->with('settings')->firstOrFail();
|
|
|
|
return $election;
|
|
}
|
|
|
|
private function assignmentPusat(StaffAssignment $assignment): PusatMengundi
|
|
{
|
|
/** @var PusatMengundi $pusatMengundi */
|
|
$pusatMengundi = $assignment->pusatMengundi()->firstOrFail();
|
|
|
|
return $pusatMengundi;
|
|
}
|
|
}
|