first
This commit is contained in:
270
app/Services/Ktm/KtmApplicationService.php
Normal file
270
app/Services/Ktm/KtmApplicationService.php
Normal file
@@ -0,0 +1,270 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user