diff --git a/apps/backend.zip b/apps/backend.zip new file mode 100644 index 00000000..d269b162 Binary files /dev/null and b/apps/backend.zip differ diff --git a/apps/backend/app/Enums/AssignedRole.php b/apps/backend/app/Enums/AssignedRole.php index f1415516..a4fd9e94 100644 --- a/apps/backend/app/Enums/AssignedRole.php +++ b/apps/backend/app/Enums/AssignedRole.php @@ -4,12 +4,14 @@ enum AssignedRole: string { - case Rw = 'rw'; + case KasiPelayanan = 'kasi_pelayanan'; + case KaurTuUmum = 'kaur_tu_umum'; public function label(): string { return match ($this) { - self::Rw => 'RW', + self::KasiPelayanan => 'Kasi Pelayanan', + self::KaurTuUmum => 'Kaur TU & Umum', }; } } diff --git a/apps/backend/app/Enums/LetterStatus.php b/apps/backend/app/Enums/LetterStatus.php index 7a19a81d..03822141 100644 --- a/apps/backend/app/Enums/LetterStatus.php +++ b/apps/backend/app/Enums/LetterStatus.php @@ -11,8 +11,6 @@ enum LetterStatus: string case RtRejected = 'rt_rejected'; case RwApproved = 'rw_approved'; case RwRejected = 'rw_rejected'; - case WaitingRevisionWarga = 'waiting_revision_warga'; - case RejectedRevision = 'rejected_revision'; case KadusApproved = 'kadus_approved'; case KadusRejected = 'kadus_rejected'; case KasiApproved = 'kasi_approved'; @@ -26,8 +24,6 @@ public function label(): string self::RtRejected => 'Ditolak RT', self::RwApproved => 'Disetujui RW', self::RwRejected => 'Ditolak RW', - self::WaitingRevisionWarga => 'Menunggu Revisi Warga', - self::RejectedRevision => 'Ditolak Revisi', self::KadusApproved => 'Disetujui Kadus', self::KadusRejected => 'Ditolak Kadus', self::KasiApproved => 'Disetujui Kasi Pelayanan', @@ -40,7 +36,6 @@ public function isRejected(): bool return in_array($this, [ self::RtRejected, self::RwRejected, - self::RejectedRevision, self::KadusRejected, self::KasiRejected, ], true); diff --git a/apps/backend/app/Http/Controllers/Api/LetterController.php b/apps/backend/app/Http/Controllers/Api/LetterController.php index 408bcb4f..7be6784c 100644 --- a/apps/backend/app/Http/Controllers/Api/LetterController.php +++ b/apps/backend/app/Http/Controllers/Api/LetterController.php @@ -2,14 +2,11 @@ namespace App\Http\Controllers\Api; -use App\Enums\LetterStatus; use App\Http\Controllers\Controller; use App\Http\Requests\LetterIndexRequest; use App\Http\Requests\StoreLetterRequest; use App\Models\Letter; use App\Services\LetterService; -use Illuminate\Http\Request; -use Illuminate\Support\Facades\DB; class LetterController extends Controller { @@ -57,57 +54,6 @@ public function show($id) ]); } - public function resubmit(Request $request, Letter $letter) - { - $user = auth()->user(); - - if ($letter->submitted_by !== $user->id) { - abort(403, 'Anda tidak berwenang mengubah surat ini.'); - } - - if ($letter->status !== LetterStatus::WaitingRevisionWarga) { - abort(422, 'Surat tidak dalam status revisi warga.'); - } - - if ($letter->revision_count >= 2) { - abort(422, 'Batas revisi telah tercapai (maksimal 2x). Silakan ajukan surat baru.'); - } - - $data = $request->validate([ - 'letter_type_id' => ['nullable', 'exists:letter_types,id'], - 'purpose' => ['required', 'string', 'max:500'], - 'payload' => ['nullable', 'array'], - 'notes' => ['nullable', 'string', 'max:255'], - ]); - - DB::transaction(function () use ($letter, $data) { - $oldStatus = $letter->status->value; - - $letter->update([ - 'letter_type_id' => $data['letter_type_id'] ?? $letter->letter_type_id, - 'purpose' => $data['purpose'], - 'payload' => $data['payload'] ?? $letter->payload, - 'notes' => $data['notes'] ?? $letter->notes, - 'status' => LetterStatus::RwApproved->value, - 'letter_number' => null, - 'expires_at' => null, - 'processed_at' => now(), - ]); - - $letter->statusLogs()->create([ - 'actor_id' => auth()->id(), - 'old_status' => $oldStatus, - 'new_status' => LetterStatus::RwApproved->value, - 'reason' => $data['notes'] ?? 'Warga mengirim ulang surat hasil revisi.', - ]); - }); - - return response()->json([ - 'message' => 'Surat berhasil dikirim ulang untuk verifikasi.', - 'data' => $letter->fresh(), - ]); - } - public function destroy(Letter $letter) { $user = auth()->user(); diff --git a/apps/backend/app/Services/KasiApprovalService.php b/apps/backend/app/Services/KasiApprovalService.php index 21ede001..f3184be6 100644 --- a/apps/backend/app/Services/KasiApprovalService.php +++ b/apps/backend/app/Services/KasiApprovalService.php @@ -6,6 +6,7 @@ use App\Models\Letter; use App\Models\User; use App\Notifications\LetterStatusNotification; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\DB; class KasiApprovalService @@ -14,13 +15,12 @@ public function __construct( protected OfficialService $officialService ) {} - public function getPendingLetters(User $user) + public function getPendingLetters(User $user): Collection { return Letter::query() ->whereIn('status', [ LetterStatus::RwApproved, - LetterStatus::WaitingRevisionWarga, LetterStatus::KasiApproved, LetterStatus::KasiRejected, @@ -97,47 +97,32 @@ public function approve( $oldStatus = $letter->status->value; - if ($data['status'] === 'needs_revision') { - $newRevisionCount = $letter->revision_count + 1; + $newStatus = $data['status'] === 'approved' + ? LetterStatus::KasiApproved->value + : LetterStatus::KasiRejected->value; - // Revisi ke-3+ → auto-reject (maks 2x revisi) - if ($newRevisionCount > 2) { - $newStatus = LetterStatus::RejectedRevision->value; - } else { - $newStatus = LetterStatus::WaitingRevisionWarga->value; - } + $letterNumber = null; + $expiresAt = null; - $letter->revision_count = $newRevisionCount; - $letterNumber = null; - $expiresAt = null; - } else { - $newStatus = $data['status'] === 'approved' - ? LetterStatus::KasiApproved->value - : LetterStatus::KasiRejected->value; - - $letterNumber = null; - $expiresAt = null; - - if ($data['status'] === 'approved') { - - $letter->approvals() - ->where('approval_level', 'kasi') - ->update([ - 'approved_by' => $user->id, - ]); - - $letterNumber = sprintf( - '%03d/%s/%d', - $letter->id, - strtoupper($letter->letterType->code), - now()->year - ); + if ($data['status'] === 'approved') { - if ($letter->letterType->validity_days) { - $expiresAt = now()->addDays( - $letter->letterType->validity_days - ); - } + $letter->approvals() + ->where('approval_level', 'kasi') + ->update([ + 'approved_by' => $user->id, + ]); + + $letterNumber = sprintf( + '%03d/%s/%d', + $letter->id, + strtoupper($letter->letterType->code), + now()->year + ); + + if ($letter->letterType->validity_days) { + $expiresAt = now()->addDays( + $letter->letterType->validity_days + ); } } @@ -157,21 +142,6 @@ public function approve( $citizenUser = $letter->citizen->user; - if ($data['status'] === 'needs_revision') { - if ($citizenUser) { - $citizenUser->notify( - new LetterStatusNotification( - $letter, - 'Perlu Revisi Data', - 'Permohonan surat Anda perlu diperbaiki sebelum verifikasi final.', - 'waiting_revision_warga' - ) - ); - } - - return; - } - if ($data['status'] === 'approved') { if ($citizenUser) { diff --git a/apps/backend/composer.lock b/apps/backend/composer.lock index 19496ea7..7634ba4b 100644 --- a/apps/backend/composer.lock +++ b/apps/backend/composer.lock @@ -1202,16 +1202,16 @@ }, { "name": "guzzlehttp/uri-template", - "version": "v2.0.0", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "516c3bf2af176c532d5b59b3430292f7e9ecccb1" + "reference": "7a466ad606491eb6528c717482f7cca77f1851f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/516c3bf2af176c532d5b59b3430292f7e9ecccb1", - "reference": "516c3bf2af176c532d5b59b3430292f7e9ecccb1", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/7a466ad606491eb6528c717482f7cca77f1851f3", + "reference": "7a466ad606491eb6528c717482f7cca77f1851f3", "shasum": "" }, "require": { @@ -1268,7 +1268,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v2.0.0" + "source": "https://github.com/guzzle/uri-template/tree/v2.0.1" }, "funding": [ { @@ -1284,20 +1284,20 @@ "type": "tidelift" } ], - "time": "2026-07-20T13:40:43+00:00" + "time": "2026-08-24T17:13:02+00:00" }, { "name": "laravel/framework", - "version": "v13.26.1", + "version": "v13.30.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "e4a1bc52ef551d52e60244bb004256d6861da7ab" + "reference": "718d17db56861e0a49f644217c8853dab1bff8ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/e4a1bc52ef551d52e60244bb004256d6861da7ab", - "reference": "e4a1bc52ef551d52e60244bb004256d6861da7ab", + "url": "https://api.github.com/repos/laravel/framework/zipball/718d17db56861e0a49f644217c8853dab1bff8ce", + "reference": "718d17db56861e0a49f644217c8853dab1bff8ce", "shasum": "" }, "require": { @@ -1419,7 +1419,7 @@ "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^11.5.50 || ^12.5.8 || ^13.0.3", "predis/predis": "^2.3 || ^3.0", - "rector/rector": "^2.3", + "rector/rector": "2.6.3", "resend/resend-php": "^1.0", "symfony/cache": "^7.4.0 || ^8.0.0", "symfony/http-client": "^7.4.0 || ^8.0.0", @@ -1511,20 +1511,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-08-18T20:31:28+00:00" + "time": "2026-09-01T21:42:30+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.23", + "version": "v0.3.24", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "b7b4c35e5bc47450f6b6238c6cc9c47ba19b2221" + "reference": "5d3cdef29e93ca3b62b1871359db3078cd99908b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/b7b4c35e5bc47450f6b6238c6cc9c47ba19b2221", - "reference": "b7b4c35e5bc47450f6b6238c6cc9c47ba19b2221", + "url": "https://api.github.com/repos/laravel/prompts/zipball/5d3cdef29e93ca3b62b1871359db3078cd99908b", + "reference": "5d3cdef29e93ca3b62b1871359db3078cd99908b", "shasum": "" }, "require": { @@ -1568,9 +1568,9 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.23" + "source": "https://github.com/laravel/prompts/tree/v0.3.24" }, - "time": "2026-08-11T18:58:24+00:00" + "time": "2026-08-20T12:55:36+00:00" }, { "name": "laravel/sanctum", @@ -1637,16 +1637,16 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.0.15", + "version": "v2.0.16", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661" + "reference": "7cfc24e4fa2cca045fb8dd2a797a2b2b13b655ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/dccd8bcb851bb03fcc005df650b708b57cc52661", - "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7cfc24e4fa2cca045fb8dd2a797a2b2b13b655ed", + "reference": "7cfc24e4fa2cca045fb8dd2a797a2b2b13b655ed", "shasum": "" }, "require": { @@ -1694,7 +1694,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-07-21T16:49:22+00:00" + "time": "2026-08-18T20:28:54+00:00" }, { "name": "laravel/tinker", @@ -1956,16 +1956,16 @@ }, { "name": "league/flysystem", - "version": "3.35.3", + "version": "3.36.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "5fc8404762179ae514678487b23494fd69b2309c" + "reference": "f7fb152932f30072d573510cbd4dd657d6475b25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/5fc8404762179ae514678487b23494fd69b2309c", - "reference": "5fc8404762179ae514678487b23494fd69b2309c", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f7fb152932f30072d573510cbd4dd657d6475b25", + "reference": "f7fb152932f30072d573510cbd4dd657d6475b25", "shasum": "" }, "require": { @@ -2033,9 +2033,9 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.35.3" + "source": "https://github.com/thephpleague/flysystem/tree/3.36.0" }, - "time": "2026-08-22T12:55:54+00:00" + "time": "2026-09-02T08:00:27+00:00" }, { "name": "league/flysystem-local", @@ -2393,16 +2393,16 @@ }, { "name": "monolog/monolog", - "version": "3.10.0", + "version": "3.11.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + "reference": "147f303310f06334f03f409e49d7ad1e275ff05a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", - "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/147f303310f06334f03f409e49d7ad1e275ff05a", + "reference": "147f303310f06334f03f409e49d7ad1e275ff05a", "shasum": "" }, "require": { @@ -2480,7 +2480,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + "source": "https://github.com/Seldaek/monolog/tree/3.11.0" }, "funding": [ { @@ -2492,7 +2492,7 @@ "type": "tidelift" } ], - "time": "2026-01-02T08:56:05+00:00" + "time": "2026-09-02T12:39:56+00:00" }, { "name": "nesbot/carbon", @@ -3781,16 +3781,16 @@ }, { "name": "symfony/console", - "version": "v7.4.17", + "version": "v7.4.18", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "962e18f09ebe68a49039b4c82fc0ea4871824fca" + "reference": "23d6f88a29f6d0eac45bd77d70307adf83ba7ab0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/962e18f09ebe68a49039b4c82fc0ea4871824fca", - "reference": "962e18f09ebe68a49039b4c82fc0ea4871824fca", + "url": "https://api.github.com/repos/symfony/console/zipball/23d6f88a29f6d0eac45bd77d70307adf83ba7ab0", + "reference": "23d6f88a29f6d0eac45bd77d70307adf83ba7ab0", "shasum": "" }, "require": { @@ -3855,7 +3855,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.17" + "source": "https://github.com/symfony/console/tree/v7.4.18" }, "funding": [ { @@ -3875,20 +3875,20 @@ "type": "tidelift" } ], - "time": "2026-08-21T12:09:28+00:00" + "time": "2026-08-25T14:18:37+00:00" }, { "name": "symfony/css-selector", - "version": "v7.4.17", + "version": "v7.4.18", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "e3822e1cb7013a0a99b3c578130458927ec4eb89" + "reference": "fecf40067fc8d8880ea87b8ac227600b9aadd1d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/e3822e1cb7013a0a99b3c578130458927ec4eb89", - "reference": "e3822e1cb7013a0a99b3c578130458927ec4eb89", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/fecf40067fc8d8880ea87b8ac227600b9aadd1d0", + "reference": "fecf40067fc8d8880ea87b8ac227600b9aadd1d0", "shasum": "" }, "require": { @@ -3924,7 +3924,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.4.17" + "source": "https://github.com/symfony/css-selector/tree/v7.4.18" }, "funding": [ { @@ -3944,7 +3944,7 @@ "type": "tidelift" } ], - "time": "2026-08-21T17:40:08+00:00" + "time": "2026-08-23T10:03:40+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4334,16 +4334,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.4.17", + "version": "v7.4.18", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "2ebe78c083501dfb9509b31a7aedcae4d60a391f" + "reference": "d070b716a32fbe3bf04204db0f58ace73b86d133" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/2ebe78c083501dfb9509b31a7aedcae4d60a391f", - "reference": "2ebe78c083501dfb9509b31a7aedcae4d60a391f", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/d070b716a32fbe3bf04204db0f58ace73b86d133", + "reference": "d070b716a32fbe3bf04204db0f58ace73b86d133", "shasum": "" }, "require": { @@ -4392,7 +4392,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.17" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.18" }, "funding": [ { @@ -4412,20 +4412,20 @@ "type": "tidelift" } ], - "time": "2026-08-20T09:55:18+00:00" + "time": "2026-08-30T20:10:52+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.17", + "version": "v7.4.18", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "aa160388d444210e3d01bbb4c5c53af4cd763df4" + "reference": "275d2d2d24530f2a0eaf17704a3a93860a036351" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/aa160388d444210e3d01bbb4c5c53af4cd763df4", - "reference": "aa160388d444210e3d01bbb4c5c53af4cd763df4", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/275d2d2d24530f2a0eaf17704a3a93860a036351", + "reference": "275d2d2d24530f2a0eaf17704a3a93860a036351", "shasum": "" }, "require": { @@ -4511,7 +4511,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.17" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.18" }, "funding": [ { @@ -4531,7 +4531,7 @@ "type": "tidelift" } ], - "time": "2026-08-22T13:41:33+00:00" + "time": "2026-08-30T21:24:29+00:00" }, { "name": "symfony/mailer", @@ -4619,7 +4619,7 @@ }, { "name": "symfony/mime", - "version": "v7.4.17", + "version": "v7.4.18", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", @@ -4684,7 +4684,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.17" + "source": "https://github.com/symfony/mime/tree/v7.4.18" }, "funding": [ { @@ -5697,7 +5697,7 @@ }, { "name": "symfony/process", - "version": "v7.4.17", + "version": "v7.4.18", "source": { "type": "git", "url": "https://github.com/symfony/process.git", @@ -5738,7 +5738,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.17" + "source": "https://github.com/symfony/process/tree/v7.4.18" }, "funding": [ { @@ -5762,7 +5762,7 @@ }, { "name": "symfony/routing", - "version": "v7.4.17", + "version": "v7.4.18", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", @@ -5823,7 +5823,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.17" + "source": "https://github.com/symfony/routing/tree/v7.4.18" }, "funding": [ { @@ -5847,16 +5847,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.7.1", + "version": "v3.7.3", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", - "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", "shasum": "" }, "require": { @@ -5910,7 +5910,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" }, "funding": [ { @@ -5930,7 +5930,7 @@ "type": "tidelift" } ], - "time": "2026-06-16T09:55:08+00:00" + "time": "2026-07-27T15:39:01+00:00" }, { "name": "symfony/string", @@ -6285,16 +6285,16 @@ }, { "name": "symfony/var-dumper", - "version": "v7.4.17", + "version": "v7.4.18", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "53712df8727da1744490202eeb9cb50d4b95419d" + "reference": "e088da50b813f32473a76871616cbb8fa54653a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/53712df8727da1744490202eeb9cb50d4b95419d", - "reference": "53712df8727da1744490202eeb9cb50d4b95419d", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/e088da50b813f32473a76871616cbb8fa54653a8", + "reference": "e088da50b813f32473a76871616cbb8fa54653a8", "shasum": "" }, "require": { @@ -6348,7 +6348,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.17" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.18" }, "funding": [ { @@ -6368,7 +6368,7 @@ "type": "tidelift" } ], - "time": "2026-08-21T12:09:28+00:00" + "time": "2026-08-30T20:10:52+00:00" }, { "name": "thecodingmachine/safe", @@ -6570,23 +6570,23 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.6.4", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "416df702837983f8d5ff48c9c3fee4f5f57b980b" + "reference": "301c07936b16d88628b126b01d082ba153cf4c40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/416df702837983f8d5ff48c9c3fee4f5f57b980b", - "reference": "416df702837983f8d5ff48c9c3fee4f5f57b980b", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/301c07936b16d88628b126b01d082ba153cf4c40", + "reference": "301c07936b16d88628b126b01d082ba153cf4c40", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.1.4", + "graham-campbell/result-type": "^1.2", "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.5", + "phpoption/phpoption": "^1.10", "symfony/polyfill-ctype": "^1.26", "symfony/polyfill-mbstring": "^1.26", "symfony/polyfill-php80": "^1.26" @@ -6630,7 +6630,7 @@ "homepage": "https://github.com/vlucas" } ], - "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "description": "Loads environment variables from `.env` to `$_ENV` and `$_SERVER` automagically, and optionally to `getenv()`.", "keywords": [ "dotenv", "env", @@ -6638,7 +6638,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.4" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.7.0" }, "funding": [ { @@ -6650,7 +6650,7 @@ "type": "tidelift" } ], - "time": "2026-07-06T19:11:50+00:00" + "time": "2026-08-24T18:07:49+00:00" }, { "name": "voku/portable-ascii", @@ -7721,23 +7721,23 @@ }, { "name": "phpunit/php-file-iterator", - "version": "6.0.1", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + "reference": "a248d1640ab059b075f53a2ef0f9856e864e06b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", - "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a248d1640ab059b075f53a2ef0f9856e864e06b5", + "reference": "a248d1640ab059b075f53a2ef0f9856e864e06b5", "shasum": "" }, "require": { "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^12.5.33" }, "type": "library", "extra": { @@ -7770,7 +7770,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.2" }, "funding": [ { @@ -7790,7 +7790,7 @@ "type": "tidelift" } ], - "time": "2026-02-02T14:04:18+00:00" + "time": "2026-08-25T14:40:53+00:00" }, { "name": "phpunit/php-invoker", @@ -7978,16 +7978,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.5.33", + "version": "12.5.34", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b98e028a26c5c5ba7e4a54be96ccf35f2914d184" + "reference": "6cbff63d670de92cb1cb3d2ff9f40327e9da9c7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b98e028a26c5c5ba7e4a54be96ccf35f2914d184", - "reference": "b98e028a26c5c5ba7e4a54be96ccf35f2914d184", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6cbff63d670de92cb1cb3d2ff9f40327e9da9c7f", + "reference": "6cbff63d670de92cb1cb3d2ff9f40327e9da9c7f", "shasum": "" }, "require": { @@ -7997,18 +7997,18 @@ "ext-libxml": "*", "ext-mbstring": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.4", + "myclabs/deep-copy": "^1.14.0", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.3", "phpunit/php-code-coverage": "^12.5.7", - "phpunit/php-file-iterator": "^6.0.1", + "phpunit/php-file-iterator": "^6.0.2", "phpunit/php-invoker": "^6.0.0", "phpunit/php-text-template": "^5.0.0", "phpunit/php-timer": "^8.0.0", "sebastian/cli-parser": "^4.2.1", "sebastian/comparator": "^7.1.8", - "sebastian/diff": "^7.0.0", + "sebastian/diff": "^7.0.1", "sebastian/environment": "^8.1.2", "sebastian/exporter": "^7.0.3", "sebastian/global-state": "^8.0.3", @@ -8056,7 +8056,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.33" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.34" }, "funding": [ { @@ -8064,7 +8064,7 @@ "type": "other" } ], - "time": "2026-07-28T13:58:09+00:00" + "time": "2026-08-27T08:38:28+00:00" }, { "name": "sebastian/cli-parser", @@ -8287,24 +8287,24 @@ }, { "name": "sebastian/diff", - "version": "7.0.0", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + "reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", - "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6", + "reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6", "shasum": "" }, "require": { "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^12.0", - "symfony/process": "^7.2" + "phpunit/phpunit": "^12.5.33", + "symfony/process": "^7.4.17" }, "type": "library", "extra": { @@ -8342,15 +8342,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", + "type": "tidelift" } ], - "time": "2025-02-07T04:55:46+00:00" + "time": "2026-08-25T15:35:54+00:00" }, { "name": "sebastian/environment", diff --git a/apps/backend/database/migrations/2026_07_12_204135_create_letter_types_table.php b/apps/backend/database/migrations/2026_07_12_204135_create_letter_types_table.php index b51bbe46..cc3fda23 100644 --- a/apps/backend/database/migrations/2026_07_12_204135_create_letter_types_table.php +++ b/apps/backend/database/migrations/2026_07_12_204135_create_letter_types_table.php @@ -19,7 +19,7 @@ public function up(): void $table->text('template')->nullable(); $table->enum('verification_type', ['manual', 'auto', 'document']); $table->text('requirement_info'); - $table->enum('assigned_role', ['rw'])->nullable(); + $table->enum('assigned_role', ['kasi_pelayanan', 'kaur_tu_umum'])->nullable(); $table->unsignedInteger('validity_days')->nullable(); $table->boolean('is_active'); $table->timestamps(); diff --git a/apps/backend/database/seeders/LetterTypeSeeder.php b/apps/backend/database/seeders/LetterTypeSeeder.php index 77697a40..dde8d84b 100644 --- a/apps/backend/database/seeders/LetterTypeSeeder.php +++ b/apps/backend/database/seeders/LetterTypeSeeder.php @@ -305,7 +305,7 @@ private function letterTypes(): array 'description' => 'Keterangan kepemilikan/menjalankan usaha di wilayah desa.', 'verification_type' => 'manual', 'requirement_info' => 'KTP, KK, jenis & lokasi usaha.', - 'assigned_role' => 'rw', + 'assigned_role' => 'kasi_pelayanan', 'validity_days' => 365, 'body' => 'adalah benar memiliki/menjalankan usaha berupa {{ jenis_usaha }} yang berlokasi di {{ lokasi_usaha }}, digunakan untuk keperluan {{ purpose }}.', ], @@ -315,7 +315,7 @@ private function letterTypes(): array 'description' => 'Keterangan status belum menikah untuk keperluan administratif.', 'verification_type' => 'manual', 'requirement_info' => 'KTP, KK.', - 'assigned_role' => 'rw', + 'assigned_role' => 'kasi_pelayanan', 'validity_days' => 90, 'body' => 'berdasarkan data kependudukan yang ada, benar berstatus belum menikah, digunakan untuk keperluan {{ purpose }}.', ], @@ -325,7 +325,7 @@ private function letterTypes(): array 'description' => 'Keterangan domisili/tempat tinggal warga di wilayah desa.', 'verification_type' => 'manual', 'requirement_info' => 'KTP, KK. Untuk pemohon non-warga terdaftar: catatan keterangan RT/RW/Kadus.', - 'assigned_role' => 'rw', + 'assigned_role' => 'kasi_pelayanan', 'validity_days' => 90, 'body' => 'adalah benar berdomisili di wilayah desa, digunakan untuk keperluan {{ purpose }}.', ], @@ -335,7 +335,7 @@ private function letterTypes(): array 'description' => 'Keterangan status ekonomi tidak mampu untuk keperluan bantuan sosial/pendidikan.', 'verification_type' => 'manual', 'requirement_info' => 'KTP, KK.', - 'assigned_role' => 'rw', + 'assigned_role' => 'kasi_pelayanan', 'validity_days' => 180, 'body' => 'adalah benar termasuk warga dengan kondisi ekonomi tidak mampu, digunakan untuk keperluan {{ keperluan_persyaratan }}.', ], @@ -345,7 +345,7 @@ private function letterTypes(): array 'description' => 'Keterangan bahwa warga tidak memiliki rumah untuk keperluan bantuan atau administrasi.', 'verification_type' => 'manual', 'requirement_info' => 'KTP, KK, surat pengantar RT/RW.', - 'assigned_role' => 'rw', + 'assigned_role' => 'kasi_pelayanan', 'validity_days' => 90, 'body' => 'adalah benar tidak memiliki rumah dan membutuhkan keterangan ini untuk keperluan {{ purpose }}.', ], @@ -355,7 +355,7 @@ private function letterTypes(): array 'description' => 'Keterangan penghasilan untuk keperluan administrasi atau bantuan sosial.', 'verification_type' => 'manual', 'requirement_info' => 'KTP, KK, bukti penghasilan.', - 'assigned_role' => 'rw', + 'assigned_role' => 'kasi_pelayanan', 'validity_days' => 90, 'body' => 'berdasarkan data yang ada, penghasilan orang tersebut sebesar {{ penghasilan_perbulan }} per bulan dari pekerjaan {{ jenis_usaha }}, digunakan untuk keperluan {{ purpose }}.', ], @@ -365,7 +365,7 @@ private function letterTypes(): array 'description' => 'Keterangan perubahan atau perbedaan nama berdasarkan dokumen pendukung.', 'verification_type' => 'manual', 'requirement_info' => 'KTP, KK, akta atau dokumen pendukung nama.', - 'assigned_role' => 'rw', + 'assigned_role' => 'kasi_pelayanan', 'validity_days' => 90, 'body' => 'bahwa yang bersangkutan menggunakan nama {{ nama_lama }}, dan nama yang benar menurut dokumen adalah {{ nama_benar }}, digunakan untuk keperluan {{ purpose }}.', ], @@ -375,7 +375,7 @@ private function letterTypes(): array 'description' => 'Keterangan kematian dan penguburan untuk keperluan administrasi keluarga.', 'verification_type' => 'manual', 'requirement_info' => 'KTP, KK, surat keterangan kematian.', - 'assigned_role' => 'rw', + 'assigned_role' => 'kasi_pelayanan', 'validity_days' => 90, 'body' => 'bahwa almarhum {{ nama_almarhum }} meninggal dunia pada tanggal {{ tanggal_meninggal }} di {{ tempat_meninggal }} dan dimakamkan di {{ tempat_pemakaman }}, digunakan untuk keperluan {{ purpose }}.', ], @@ -385,7 +385,7 @@ private function letterTypes(): array 'description' => 'Keterangan kelahiran untuk keperluan administrasi kartu keluarga atau akta kelahiran.', 'verification_type' => 'manual', 'requirement_info' => 'KTP, KK, akta kelahiran atau keterangan bidan.', - 'assigned_role' => 'rw', + 'assigned_role' => 'kasi_pelayanan', 'validity_days' => 90, 'body' => 'bahwa anak bernama {{ nama_anak }} lahir pada tanggal {{ tanggal_lahir_anak }} di {{ tempat_lahir_anak }}, dari pasangan {{ nama_ayah }} dan {{ nama_ibu }}, digunakan untuk keperluan {{ purpose }}.', ], @@ -395,7 +395,7 @@ private function letterTypes(): array 'description' => 'Keterangan bahwa warga pertama kali memiliki rumah di alamat yang dinyatakan.', 'verification_type' => 'manual', 'requirement_info' => 'KTP, KK, bukti kepemilikan rumah.', - 'assigned_role' => 'rw', + 'assigned_role' => 'kasi_pelayanan', 'validity_days' => 90, 'body' => 'bahwa yang bersangkutan adalah warga kami yang pertama kali memiliki rumah di alamat {{ alamat_rumah }}, digunakan untuk keperluan {{ purpose }}.', ], diff --git a/apps/backend/routes/api.php b/apps/backend/routes/api.php index 777a082b..1e624560 100644 --- a/apps/backend/routes/api.php +++ b/apps/backend/routes/api.php @@ -21,17 +21,6 @@ // use App\Http\Controllers\Api\OfficialController; -/* -|-------------------------------------------------------------------------- -| Public Routes -|-------------------------------------------------------------------------- -*/ - -Route::post( - '/login', - [AuthenticatedSessionController::class, 'store'] -); - /* |-------------------------------------------------------------------------- | Protected Routes diff --git a/apps/backend/tests/Feature/LetterStatusTest.php b/apps/backend/tests/Feature/LetterStatusTest.php new file mode 100644 index 00000000..bfa7fbba --- /dev/null +++ b/apps/backend/tests/Feature/LetterStatusTest.php @@ -0,0 +1,82 @@ + $c->name, LetterStatus::cases()); + + $this->assertNotContains('WaitingRevisionWarga', $caseNames); + } + + #[Test] + public function it_does_not_have_rejected_revision_case(): void + { + $caseNames = array_map(fn (LetterStatus $c) => $c->name, LetterStatus::cases()); + + $this->assertNotContains('RejectedRevision', $caseNames); + } + + #[DataProvider('legacyGranularCasesThatMustStillExist')] + #[Test] + public function granular_v4_cases_still_exist_because_downstream_services_are_not_migrated_yet(string $caseName): void + { + // EV5-0-S1 TIDAK boleh menghapus case ini — RwApprovalService, + // KasiApprovalService, PdfService, dan cast model Letter/ + // LetterStatusLog masih memakainya. Penghapusan case granular + // adalah scope EV5-5 (setelah EV5-2/EV5-4 selesai rewrite + // service-nya), bukan hari ini. + $caseNames = array_map(fn (LetterStatus $c) => $c->name, LetterStatus::cases()); + + $this->assertContains($caseName, $caseNames); + } + + public static function legacyGranularCasesThatMustStillExist(): array + { + return [ + 'Pending' => ['Pending'], + 'RtApproved' => ['RtApproved'], + 'RtRejected' => ['RtRejected'], + 'RwApproved' => ['RwApproved'], + 'RwRejected' => ['RwRejected'], + 'KadusApproved' => ['KadusApproved'], + 'KadusRejected' => ['KadusRejected'], + 'KasiApproved' => ['KasiApproved'], + 'KasiRejected' => ['KasiRejected'], + ]; + } + + #[Test] + public function it_has_exactly_nine_cases_after_removing_only_the_two_revision_cases(): void + { + // 11 case lama - 2 case revisi (WaitingRevisionWarga, RejectedRevision) = 9. + $this->assertCount(9, LetterStatus::cases()); + } + + #[Test] + public function is_approved_and_is_rejected_no_longer_reference_removed_revision_cases(): void + { + // isRejected()/isApproved() harus tetap konsisten setelah 2 case + // dihapus — tidak boleh melempar error saat dipanggil di case + // manapun yang tersisa. + foreach (LetterStatus::cases() as $case) { + $this->assertIsBool($case->isApproved()); + $this->assertIsBool($case->isRejected()); + } + } + + #[Test] + public function kasi_approved_is_still_the_only_final_approval(): void + { + $this->assertTrue(LetterStatus::KasiApproved->isFinalApproval()); + $this->assertTrue(LetterStatus::KasiApproved->isTerminal()); + } +} diff --git a/apps/backend/tests/Feature/RevisionFlowTest.php b/apps/backend/tests/Feature/RevisionFlowTest.php index 89f40460..7e88d4d9 100644 --- a/apps/backend/tests/Feature/RevisionFlowTest.php +++ b/apps/backend/tests/Feature/RevisionFlowTest.php @@ -2,7 +2,6 @@ namespace Tests\Feature; -use App\Enums\LetterStatus; use App\Http\Requests\KasiApprovalRequest; use Illuminate\Support\Facades\Validator; use Tests\TestCase; @@ -23,10 +22,4 @@ public function test_kasi_approval_request_accepts_needs_revision_status(): void $this->assertFalse($validator->fails(), $validator->errors()->toJson()); } - - public function test_waiting_revision_warga_status_is_defined_for_revision_flow(): void - { - $this->assertSame('waiting_revision_warga', LetterStatus::WaitingRevisionWarga->value); - $this->assertSame('rw_approved', LetterStatus::RwApproved->value); - } } diff --git a/apps/backend/tests/Unit/AssignedRoleEnumTest.php b/apps/backend/tests/Unit/AssignedRoleEnumTest.php new file mode 100644 index 00000000..9e39fc86 --- /dev/null +++ b/apps/backend/tests/Unit/AssignedRoleEnumTest.php @@ -0,0 +1,58 @@ + $c->name, AssignedRole::cases()); + + $this->assertNotContains('Rw', $caseNames); + } + + #[Test] + public function it_has_exactly_two_cases_matching_final_step_approvers(): void + { + $this->assertCount(2, AssignedRole::cases()); + } + + #[Test] + public function it_has_kasi_pelayanan_case_with_correct_value(): void + { + $this->assertSame('kasi_pelayanan', AssignedRole::KasiPelayanan->value); + } + + #[Test] + public function it_has_kaur_tu_umum_case_with_correct_value(): void + { + $this->assertSame('kaur_tu_umum', AssignedRole::KaurTuUmum->value); + } + + #[Test] + public function it_can_be_constructed_from_string_value(): void + { + $this->assertSame(AssignedRole::KasiPelayanan, AssignedRole::from('kasi_pelayanan')); + $this->assertSame(AssignedRole::KaurTuUmum, AssignedRole::from('kaur_tu_umum')); + } + + #[Test] + public function constructing_from_rw_throws(): void + { + $this->expectException(\ValueError::class); + AssignedRole::from('rw'); + } + + #[Test] + public function every_case_has_a_non_empty_label(): void + { + foreach (AssignedRole::cases() as $case) { + $this->assertNotEmpty($case->label()); + } + } +}