229 lines
9.3 KiB
PHP
229 lines
9.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Ppm;
|
|
|
|
use App\Models\Application;
|
|
use App\Models\Position;
|
|
use App\Models\PusatMengundi;
|
|
use App\Models\StaffAssignment;
|
|
use App\Models\User;
|
|
use App\Services\Public\KtmVacancyService;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class PpmApplicationReviewService
|
|
{
|
|
/**
|
|
* @param array{approved_position_code: string, saluran_mengundi_id?: int|null, ktm_assignment_id?: int|null, note?: string|null} $data
|
|
*/
|
|
public function approve(Application $application, User $actor, array $data): StaffAssignment
|
|
{
|
|
return DB::transaction(function () use ($application, $actor, $data): StaffAssignment {
|
|
$application->loadMissing(['documents', 'pusatMengundi', 'requestedPosition']);
|
|
|
|
if (! $this->hasRequiredDocuments($application)) {
|
|
throw ValidationException::withMessages([
|
|
'documents' => 'Permohonan hanya boleh diluluskan jika dokumen IC dan penyata bank telah dimuat naik.',
|
|
]);
|
|
}
|
|
|
|
if (! in_array($application->status, ['submitted', 'under_ppm_review', 'approved'], true)) {
|
|
throw ValidationException::withMessages([
|
|
'status' => 'Status permohonan tidak membenarkan kelulusan.',
|
|
]);
|
|
}
|
|
|
|
$position = Position::query()
|
|
->where('code', $data['approved_position_code'])
|
|
->whereIn('code', ['KTM', 'KP', 'KPDP', 'PAPM'])
|
|
->firstOrFail();
|
|
|
|
$assignmentContext = $this->assignmentContext($application, $position, $data);
|
|
$previousStatus = $application->status;
|
|
$previousPositionId = $application->approved_position_id ?: $application->requested_position_id;
|
|
|
|
$application->forceFill([
|
|
'status' => 'assigned',
|
|
'approved_position_id' => $position->id,
|
|
'approved_by_user_id' => $actor->id,
|
|
'approved_at' => now(),
|
|
'selected_ktm_assignment_id' => $assignmentContext['reports_to_assignment_id'],
|
|
])->save();
|
|
|
|
$assignment = StaffAssignment::query()->updateOrCreate(
|
|
['application_id' => $application->id],
|
|
[
|
|
'election_id' => $application->election_id,
|
|
'user_id' => $application->user_id,
|
|
'position_id' => $position->id,
|
|
'pusat_mengundi_id' => $application->pusat_mengundi_id,
|
|
'saluran_mengundi_id' => $assignmentContext['saluran_mengundi_id'],
|
|
'reports_to_assignment_id' => $assignmentContext['reports_to_assignment_id'],
|
|
'status' => 'active',
|
|
'assigned_by_user_id' => $actor->id,
|
|
'assigned_at' => now(),
|
|
'source' => 'ppm_approval',
|
|
],
|
|
);
|
|
|
|
$application->statusHistories()->create([
|
|
'from_status' => $previousStatus,
|
|
'to_status' => 'assigned',
|
|
'changed_by_user_id' => $actor->id,
|
|
'note' => $data['note'] ?? null,
|
|
'metadata' => [
|
|
'approved_position_code' => $position->code,
|
|
'previous_position_id' => $previousPositionId,
|
|
'saluran_mengundi_id' => $assignmentContext['saluran_mengundi_id'],
|
|
'reports_to_assignment_id' => $assignmentContext['reports_to_assignment_id'],
|
|
],
|
|
]);
|
|
|
|
$assignment->histories()->create([
|
|
'from_position_id' => $previousPositionId,
|
|
'to_position_id' => $position->id,
|
|
'to_pusat_mengundi_id' => $application->pusat_mengundi_id,
|
|
'to_saluran_mengundi_id' => $assignmentContext['saluran_mengundi_id'],
|
|
'to_reports_to_assignment_id' => $assignmentContext['reports_to_assignment_id'],
|
|
'changed_by_user_id' => $actor->id,
|
|
'note' => $data['note'] ?? 'Application approved by PPM.',
|
|
]);
|
|
|
|
if ($previousPositionId !== $position->id) {
|
|
activity('ppm_review')
|
|
->performedOn($application)
|
|
->causedBy($actor)
|
|
->withProperties([
|
|
'from_position_id' => $previousPositionId,
|
|
'to_position_id' => $position->id,
|
|
])
|
|
->log('application_role_changed_by_ppm');
|
|
}
|
|
|
|
activity('ppm_review')
|
|
->performedOn($application)
|
|
->causedBy($actor)
|
|
->withProperties([
|
|
'assignment_id' => $assignment->id,
|
|
'approved_position_code' => $position->code,
|
|
'saluran_mengundi_id' => $assignmentContext['saluran_mengundi_id'],
|
|
'reports_to_assignment_id' => $assignmentContext['reports_to_assignment_id'],
|
|
])
|
|
->log('application_approved_assigned_by_ppm');
|
|
|
|
return $assignment;
|
|
});
|
|
}
|
|
|
|
public function reject(Application $application, User $actor, string $reason): Application
|
|
{
|
|
return DB::transaction(function () use ($application, $actor, $reason): Application {
|
|
if (! in_array($application->status, ['submitted', 'under_ppm_review', 'approved'], true)) {
|
|
throw ValidationException::withMessages([
|
|
'status' => 'Status permohonan tidak membenarkan penolakan.',
|
|
]);
|
|
}
|
|
|
|
$previousStatus = $application->status;
|
|
|
|
$application->forceFill([
|
|
'status' => 'rejected',
|
|
'rejected_by_user_id' => $actor->id,
|
|
'rejected_at' => now(),
|
|
'rejection_reason' => $reason,
|
|
])->save();
|
|
|
|
$application->statusHistories()->create([
|
|
'from_status' => $previousStatus,
|
|
'to_status' => 'rejected',
|
|
'changed_by_user_id' => $actor->id,
|
|
'note' => $reason,
|
|
]);
|
|
|
|
activity('ppm_review')
|
|
->performedOn($application)
|
|
->causedBy($actor)
|
|
->withProperties([
|
|
'reason' => $reason,
|
|
])
|
|
->log('application_rejected_by_ppm');
|
|
|
|
return $application;
|
|
});
|
|
}
|
|
|
|
private function hasRequiredDocuments(Application $application): bool
|
|
{
|
|
$types = $application->documents()
|
|
->whereIn('document_type', ['ic_document', 'bank_statement'])
|
|
->pluck('document_type')
|
|
->unique();
|
|
|
|
return $types->contains('ic_document') && $types->contains('bank_statement');
|
|
}
|
|
|
|
/**
|
|
* @param array{approved_position_code: string, saluran_mengundi_id?: int|null, ktm_assignment_id?: int|null, note?: string|null} $data
|
|
* @return array{saluran_mengundi_id: int|null, reports_to_assignment_id: int|null}
|
|
*/
|
|
private function assignmentContext(Application $application, Position $position, array $data): array
|
|
{
|
|
$pusatMengundi = $application->pusatMengundi;
|
|
|
|
if (! $pusatMengundi instanceof PusatMengundi) {
|
|
throw ValidationException::withMessages([
|
|
'pusat_mengundi_id' => 'Pusat Mengundi permohonan tidak sah.',
|
|
]);
|
|
}
|
|
|
|
if ($position->code === 'PAPM') {
|
|
if (! app(AssignmentVacancyService::class)->hasPusatVacancy($pusatMengundi, 'PAPM')) {
|
|
throw ValidationException::withMessages([
|
|
'approved_position_code' => 'Kekosongan PAPM untuk Pusat Mengundi ini telah penuh.',
|
|
]);
|
|
}
|
|
|
|
return [
|
|
'saluran_mengundi_id' => null,
|
|
'reports_to_assignment_id' => null,
|
|
];
|
|
}
|
|
|
|
if ($position->code === 'KTM') {
|
|
$saluranId = (int) ($data['saluran_mengundi_id'] ?? 0);
|
|
|
|
if (! app(AssignmentVacancyService::class)->hasSaluranVacancy($pusatMengundi, $saluranId, 'KTM')) {
|
|
throw ValidationException::withMessages([
|
|
'saluran_mengundi_id' => 'Saluran Mengundi yang dipilih tidak sah atau kekosongan KTM telah penuh.',
|
|
]);
|
|
}
|
|
|
|
return [
|
|
'saluran_mengundi_id' => $saluranId,
|
|
'reports_to_assignment_id' => null,
|
|
];
|
|
}
|
|
|
|
if (in_array($position->code, ['KP', 'KPDP'], true)) {
|
|
$ktmAssignmentId = (int) ($data['ktm_assignment_id'] ?? 0);
|
|
|
|
if (! app(KtmVacancyService::class)->hasVacancyForAssignment($ktmAssignmentId, $pusatMengundi, $position->code, $application->id)) {
|
|
throw ValidationException::withMessages([
|
|
'ktm_assignment_id' => 'KTM yang dipilih tidak sah atau kekosongan jawatan ini telah penuh.',
|
|
]);
|
|
}
|
|
|
|
$ktmAssignment = StaffAssignment::query()->findOrFail($ktmAssignmentId);
|
|
|
|
return [
|
|
'saluran_mengundi_id' => $ktmAssignment->saluran_mengundi_id,
|
|
'reports_to_assignment_id' => $ktmAssignment->id,
|
|
];
|
|
}
|
|
|
|
throw ValidationException::withMessages([
|
|
'approved_position_code' => 'Jawatan yang dipilih tidak sah untuk kelulusan PPM.',
|
|
]);
|
|
}
|
|
}
|