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