# Decision Log This log records architecture decisions, package choices, database design trade-offs, security decisions, and assumptions. ## 2026-05-28 - Phase 0 Workspace Baseline Decision: - Treat current workspace as a clean/new project because no Laravel files or git repository are present. Reason: - Inspection found an empty `C:\Users\User\Aplikasi\prn2026` workspace, no `.git`, and no project files. Impact: - Phase 1 should create a fresh Laravel application. - No existing code removal is required. ## 2026-05-28 - Laravel Version Selection Approach Decision: - Use the latest stable Laravel version supported by Composer and PHP during Phase 1. Reason: - Local PHP CLI is 8.5.1 and Composer is 2.9.4, which should support modern Laravel versions. - Actual package compatibility should be verified at install time. Impact: - Do not lock a specific Laravel major version in Phase 0. - Record final installed Laravel version in this log during Phase 1. ## 2026-05-28 - Authentication Scaffolding Choice Decision: - Plan to use Laravel Breeze with Blade stack for authentication scaffolding. Reason: - It is simple, maintainable, first-party, and suitable for government-style internal systems. - Blade aligns with the requested stack. Impact: - Phase 1 should install Breeze after Laravel scaffolding. - Public Pemohon QR flow should be custom and should not rely on open public account registration unless later required. ## 2026-05-28 - Frontend Stack Choice Decision: - Use Bootstrap 5, Bootstrap Icons, Blade, Vite, and jQuery where suitable. Reason: - Bootstrap 5 provides responsive admin UI quickly and maintainably. - Bootstrap Icons covers common UI icon needs. - jQuery remains useful for simple progressive enhancements with plugins such as Select2/DataTables. Impact: - Avoid heavy SPA architecture. - Keep Blade as the primary rendering model. ## 2026-05-28 - RBAC Package Choice Decision: - Use `spatie/laravel-permission`. Reason: - Mature Laravel package for roles and permissions. - Supports middleware, Blade directives, and permission caching. Impact: - Role and permission seeders are required. - Record-level rules still require Laravel policies and services. ## 2026-05-28 - Audit Package Choice Decision: - Use `spatie/laravel-activitylog` plus domain-specific history tables. Reason: - Activity log is useful for generic before/after auditing. - Domain history tables are better for reporting application status and assignment changes. Impact: - Implement structured histories for application and assignment workflows. - Use activity log for broad audit trail requirements. ## 2026-05-28 - Excel Package Choice Decision: - Use `maatwebsite/excel`. Reason: - It is the common Laravel package for XLSX exports/imports and supports export classes. Impact: - Exports will live in `app/Exports`. - Export query/filter logic will live in services. - Every export must create an `export_logs` row. ## 2026-05-28 - QR Code Package Choice Decision: - Plan to use a Laravel-compatible QR code package, with `simplesoftwareio/simple-qrcode` as the first candidate if compatible. Reason: - The system requires QR codes for Pusat Mengundi public registration links. Impact: - Confirm package compatibility during Phase 1 before installation. - If incompatible, choose another maintained Laravel-compatible QR generator and document the change. ## 2026-05-28 - UI Helper Package Choices Decision: - Plan to use SweetAlert2, Select2 or Tom Select, Flatpickr, and either DataTables or Laravel server-side pagination depending on screen needs. Reason: - SweetAlert2 improves confirmations. - Searchable select controls are useful for Pusat, Saluran, KTM, and role assignment forms. - Flatpickr improves date/time input consistency. - Server-side pagination is safer for large operational data; DataTables may be used selectively. Impact: - Final npm package list must be verified in Phase 1. - Prefer server-rendered pages and pagination for maintainability. ## 2026-05-28 - Optional Debug and PDF Packages Decision: - Do not install Laravel Telescope in the initial Phase 1 production baseline. - Do not install DOMPDF in Phase 1. Reason: - Telescope is useful for local development but should not be part of the production default unless explicitly configured and protected. - DOMPDF is only needed later if printable PDF reports become a confirmed requirement. Impact: - Reconsider Telescope only for local development after core modules exist. - Reconsider DOMPDF only when a concrete PDF report format is provided. ## 2026-05-28 - Static Analysis and Formatting Decision: - Use Laravel Pint. - Use Larastan/PHPStan if compatible with the selected Laravel and PHP versions. Reason: - Pint provides consistent code style. - Static analysis improves reliability, but compatibility with PHP 8.5 and current Laravel must be checked. Impact: - Phase 1 or Phase 11 should configure quality commands. ## 2026-05-28 - Testing Framework Decision: - Use Laravel's built-in PHPUnit baseline. - Pest may be installed if compatible and beneficial after Laravel setup. Reason: - PHPUnit is always available in standard Laravel projects. - Pest improves readability but should not block delivery if compatibility issues arise. Impact: - Tests should be written in a way that can be implemented with PHPUnit even if Pest is skipped. ## 2026-05-28 - Dual-Role Assignment Model Decision: - Model operational roles through `staff_assignments`, not a single role field on users or applications. Reason: - A PPM may also be KTM. - Staff placement is tied to election, Pusat Mengundi, Saluran Mengundi, position, and team structure. Impact: - A person can have more than one active assignment when business rules allow it. - Validation services must enforce invalid duplicates while allowing PPM + KTM. ## 2026-05-28 - Public Identifier Security Decision: - Use UUID or ULID public identifiers for public QR links. Reason: - Public URLs must not expose internal auto-increment IDs. Impact: - Pusat Mengundi requires a public UUID/ULID column. - Public route binding should resolve by public identifier. ## 2026-05-28 - Sensitive Document Storage Decision: - Store IC documents and bank statements on private Laravel storage. Reason: - These are sensitive documents and must not be publicly accessible. Impact: - No public storage symlink for sensitive uploads. - Downloads must go through authorized controller actions. ## 2026-05-28 - Registration Period Assumption Assumption: - Registration period is event-specific and stored in `election_settings`. - `election_settings.is_registration_open` is stored and used together with the configured registration dates. Reason: - The system may need to support future elections with different dates. - The original requirement explicitly asks for a setting indicating whether registration is open. Impact: - Registration checks must use the active election setting. - Admin settings UI must make both the date window and open/closed state clear. ## 2026-05-28 - Generic Public Account Registration Assumption: - Generic public user registration should not be enabled by default. Reason: - Requirement says registration is only for public applicant flow if allowed. - QR applicant flow can collect application records without creating login accounts. Impact: - Breeze registration may be disabled or restricted for internal user creation. - Admin-created users handle internal roles. ## 2026-05-28 - MySQL Availability Assumption: - MySQL server will be provided even though `mysql` CLI is not currently in PATH. Reason: - The requirement specifies MySQL, but local CLI check failed. Impact: - Phase 1 must confirm DB connection through Laravel `.env` and migrations. ## 2026-05-28 - Phase 1 Laravel Version Installed Decision: - Installed `laravel/laravel` v13.8.0. - Installed `laravel/framework` v13.12.0. Reason: - Composer resolved Laravel 13 as the latest stable skeleton/framework compatible with PHP 8.5.1. Impact: - Package compatibility was verified against Laravel 13 and PHP 8.5.1 during installation. ## 2026-05-28 - Phase 1 Authentication and Registration Decision: - Installed Laravel Breeze v2.4.2 for simple Blade authentication scaffolding. - Disabled generic public `/register` routes. - Retained public applicant registration for a later QR-specific module. Reason: - Internal users should be Admin-created. - The original requirement allows public registration only for the applicant flow if enabled. Impact: - `/login`, password reset, logout, profile, and email verification routes exist. - `/register` returns 404. - Public applicant registration will be implemented under `/pohon/{pusat_mengundi_uuid}` in Phase 4. ## 2026-05-28 - Phase 1 Package Compatibility Results Decision: - Installed compatible Composer packages: - `spatie/laravel-permission` v7.4.1 - `spatie/laravel-activitylog` v5.0.0 - `simplesoftwareio/simple-qrcode` v4.2.0 - `laravel/breeze` v2.4.2 - `larastan/larastan` v3.9.6 - `openspout/openspout` v5.7.1 - Laravel Pint was already installed by the Laravel 13 skeleton at v1.29.1. - PHPUnit was installed by the Laravel 13 skeleton at v12.5.28. Reason: - Composer successfully resolved these versions with Laravel 13.12.0 and PHP 8.5.1. Impact: - RBAC, activity log, QR generation, auth scaffolding, static analysis, formatting, and spreadsheet export foundations are available. ## 2026-05-28 - Maatwebsite Excel Compatibility Exception Decision: - Did not install `maatwebsite/excel` in Phase 1. - Installed `openspout/openspout` v5.7.1 as the compatible spreadsheet export foundation. Reason: - Composer rejected `maatwebsite/excel` for this environment. - Latest resolvable `maatwebsite/excel` versions either do not support Laravel 13 or depend on `phpoffice/phpspreadsheet` versions constrained to PHP `<8.5.0`. - The local environment is PHP 8.5.1. - Faking Composer platform PHP was rejected because it would weaken production-readiness. Impact: - Export service design remains unchanged. - Export implementation should use OpenSpout unless `maatwebsite/excel` releases a PHP 8.5 and Laravel 13 compatible version or the runtime target is changed to a compatible PHP version. - This is a documented deviation from the preferred export package in the original requirement due to compatibility. ## 2026-05-28 - Phase 1 Frontend Package Results Decision: - Installed npm packages: - `bootstrap` - `@popperjs/core` - `bootstrap-icons` - `jquery` - `sweetalert2` - `tom-select` - `flatpickr` - `datatables.net-bs5` - `datatables.net-responsive-bs5` Reason: - These packages support the requested Bootstrap 5, jQuery-assisted, responsive admin UI. Impact: - Vite production build is available. - Tailwind dependencies remain from Breeze scaffolding but are not used by the application layout. ## 2026-05-28 - Generated View Replacement Decision: - Replaced generated Laravel/Breeze Tailwind views with Bootstrap 5 Blade views. Reason: - The original requirement specifies Bootstrap 5. Impact: - Auth, guest, app, navigation, profile, welcome, and dashboard views now follow the Bootstrap-based government-style admin UI direction. ## 2026-05-28 - Phase 1 Local Database Choice Decision: - `.env.example` is configured for MySQL. - Local `.env` remains on SQLite for Phase 1 verification. Reason: - The PHP `pdo_mysql` extension is available, but MySQL CLI/server credentials are not available in the current workspace. - SQLite allows auth, RBAC migrations, seeding, and tests to be verified immediately. Impact: - Production and shared environments must configure MySQL in `.env`. - Local development can switch to MySQL once credentials are provided. Superseded by: - `2026-05-28 - Phase 2 MySQL Connection Confirmed`. ## 2026-05-28 - Application Timezone Decision: - Set application timezone to `Asia/Kuala_Lumpur` through `APP_TIMEZONE`. Reason: - The system is for Malaysian election operations and polling-day attendance timestamps must use local time. Impact: - Date/time display, audit timestamps, and attendance check-in defaults will align with Malaysia time when using Laravel timezone helpers. ## 2026-05-28 - Phase 2 MySQL Connection Confirmed Decision: - Use MySQL for local Phase 2 migration and seeding verification. Reason: - Laravel successfully connected to MySQL 8.4.8 through PDO using `.env` database `prn2026`. - This better matches the production database requirement than SQLite. Impact: - Phase 2 schema was verified against MySQL, including identifier length constraints. - MySQL CLI remains unavailable in PATH, but Laravel/PDO connectivity is sufficient for migrations and tests requiring the configured app database. ## 2026-05-28 - Phase 2 Core Domain Migration Strategy Decision: - Implement Phase 2 domain schema in one ordered migration file. Reason: - The tables are tightly coupled by foreign keys and the phase is a single baseline domain cut. - Keeping the initial domain schema in one migration makes fresh local setup easier at this stage. Impact: - Future schema changes should use separate additive migrations. - Down migration disables foreign key checks to safely drop circular application/assignment references. ## 2026-05-28 - Phase 2 Index Name Strategy Decision: - Use explicit short names for long composite indexes. Reason: - MySQL rejected Laravel's generated index name for `position_quotas` because it exceeded the 64-character identifier limit. Impact: - Composite indexes use concise names such as `pq_election_saluran_position_idx` and `sa_election_saluran_position_idx`. ## 2026-05-28 - Phase 2 Seed Data Strategy Decision: - Seed a realistic but small sample election hierarchy and operational assignments. Reason: - Later UI phases need immediate data for dashboards, setup screens, and authorization checks. Impact: - Seed data includes one election, one Bahagian, one Daerah, one Pusat, three Saluran, quotas, sample users, and a PPM who also has a KTM assignment. ## 2026-05-29 - Phase 3 Admin Setup Scope Boundary Decision: - Complete Phase 3 as Admin setup for hierarchy CRUD, Jawatan CRUD, quota setup, PPM assignment, and QR generation. - Keep KKM, JKM, and Police operational CRUD for Phase 7 Admin management. Reason: - The original phase sequence defines Phase 3 around election structure, PPM assignment, jawatan/quota setup, and QR links. - KKM, JKM, and Police records need the broader Admin management controls, post-registration notes, and audit behavior planned in Phase 7. Impact: - The Phase 2 schema already contains representative tables. - Phase 3 UI does not yet manage representative records. ## 2026-05-29 - Phase 3 QR Storage and Route Binding Decision: - Generate QR SVG files into private local storage under `qrcodes/pusat-mengundi/{public_uuid}.svg`. - Serve the QR SVG through an Admin-protected controller route. - Use `/pohon/{pusatMengundi:public_uuid}` as the public registration URL. Reason: - The public URL must not expose internal database IDs. - Keeping generated QR assets outside the public web root avoids accidental direct exposure and keeps access paths explicit. Impact: - Admin can regenerate QR codes per Pusat Mengundi. - Public QR routes currently show a placeholder until Phase 4 implements the full application form. ## 2026-05-29 - Phase 3 PPM Assignment Service Decision: - Implement PPM assignment through `PpmAssignmentService`. - Store the assignment in `staff_assignments`, mark previous active PPM assignment for the same Pusat Mengundi as `replaced`, create a `staff_assignment_histories` row, and assign the RBAC role `PPM` to the user. Reason: - Dual-role support requires assignments to remain separate from RBAC roles. - RBAC controls application access, while `staff_assignments` controls operational placement. Impact: - A user can be PPM and also hold another operational assignment such as KTM when later assignment rules allow it. - PPM changes are auditable from the assignment history and activity log. ## 2026-05-29 - Phase 3 Quota Consistency Validation Decision: - Validate that a selected Saluran Mengundi belongs to the selected Pusat Mengundi when creating or updating a position quota. Reason: - Quotas must not accidentally attach a saluran from another Pusat Mengundi. Impact: - Position quotas can be set at pusat level by leaving saluran blank, or at saluran level with a matching pusat and saluran pair. ## 2026-05-29 - Phase 3 Public Layout Decision: - Add a lightweight public Blade layout for QR-facing pages. Reason: - Public QR routes should not render the authenticated internal navigation when accessed by unauthenticated applicants. Impact: - The Phase 3 QR placeholder is mobile-friendly and independent of internal Admin navigation. ## 2026-05-29 - Phase 4 Public Application Controller Decision: - Replace the Phase 3 QR placeholder route implementation with `PublicApplicationController`. - Keep the public QR URL as `/pohon/{pusatMengundi:public_uuid}`. - Use `/permohonan/{application:public_uuid}/berjaya` for the post-submission confirmation page. Reason: - Existing QR codes and registration URLs must remain valid. - Public UUID route binding avoids exposing internal auto-increment IDs. Impact: - Public applicants can now submit real applications through the QR link. - The old placeholder controller remains unused and can be removed in a later cleanup with a recorded reason. ## 2026-05-29 - Phase 4 Registration Period Service Decision: - Add `RegistrationPeriodService` to centralize open/closed registration checks. - Treat `is_registration_open_override` as an explicit override when it is not null. Reason: - Public, KTM, PPM, and Admin workflows will all need consistent registration-period behavior in later phases. Impact: - Public GET and POST routes block submissions when registration is closed. - Admin settings UI in a later phase should clearly show whether an override is active. ## 2026-05-29 - Phase 4 Public Upload Storage Decision: - Store public IC documents and bank statements on Laravel's private `local` disk under `applications/{application_public_uuid}`. - Record metadata in `application_documents`. Reason: - IC documents and bank statements are sensitive and must not be served from public storage. Impact: - Later document download/view actions must go through authorized controllers. - Public success pages do not expose document paths. ## 2026-05-29 - Phase 4 KTM Vacancy Calculation Decision: - Add `KtmVacancyService`. - For public KP/KPDP applications, remaining vacancy is quota minus active team assignments minus reserved submitted/under-review/approved applications for the same KTM assignment and role. Reason: - Applicants should only choose a KTM when vacancy exists, and pending submissions should reserve capacity to prevent overbooking. Impact: - The public form shows only KTM options with remaining vacancy for KP/KPDP. - POST validation rechecks vacancy to prevent stale form submission. ## 2026-05-29 - Phase 4 Duplicate IC Rule Decision: - Normalize IC numbers to digits and enforce one active application per election. - If the duplicate active record has `source = created_by_ktm`, show a specific message instructing that KTM must delete that pre-created record first. Reason: - The original requirement explicitly distinguishes KTM-created records from public self-registration. Impact: - Public submissions are blocked for duplicate IC values. - Cancelled or `deleted_by_ktm` records do not block future self-registration. ## 2026-05-29 - Phase 4 Bank Verification Initialization Decision: - Create a pending `bank_verifications` row immediately after public application submission. Reason: - This prepares records for the Admin Kewangan workflow and avoids missing finance state later. Impact: - Admin Kewangan module can later query pending bank checks without needing to backfill submitted public applications. ## 2026-05-29 - Phase 5 PPM Record-Level Authorization Decision: - Add `ApplicationPolicy` and `PpmScopeService`. - Scope PPM access by active `staff_assignments` where position is `PPM`, status is `active`, and assignment is at Pusat Mengundi level. Reason: - Role middleware alone cannot enforce "PPM only manages own Pusat Mengundi". Impact: - PPM can list, view, approve, reject, and download documents only for applications under their assigned Pusat Mengundi. - Admin remains able to pass policy checks for future shared application views. ## 2026-05-29 - Phase 5 Approval Creates Assignment Decision: - PPM approval creates or updates an active `staff_assignments` row and sets application status to `assigned`. - Assignment history and application status history are written in the same transaction. Reason: - In this module, approval is coupled with the required placement selection for KTM, KP, KPDP, or PAPM. - A separate `approved` holding state can still be used later if a workflow needs approval without placement. Impact: - Approved Phase 5 applications immediately appear as active staff assignments. - Role changes before approval are recorded through application status metadata, assignment history, and activity log. ## 2026-05-29 - Phase 5 Vacancy Validation Decision: - Add `AssignmentVacancyService` for pusat-level and saluran-level placement checks. - Reuse `KtmVacancyService` for KP/KPDP assignment to KTM, with an option to ignore the application being approved when counting reserved vacancy. Reason: - PPM assignment must prevent overfilling KTM, KP, KPDP, and PAPM quotas. - Pending KP/KPDP public applications reserve vacancy, but the current application should not block its own approval. Impact: - PPM can assign KTM only to an available Saluran. - PPM can assign KP/KPDP only to an active KTM under the same Pusat Mengundi with remaining vacancy. ## 2026-05-29 - Phase 5 Authorized Document Download Decision: - Add PPM document download route under authenticated PPM review routes. - Verify the document belongs to the application before streaming it from private storage. Reason: - PPM needs to review uploaded IC and bank statement documents, but files must remain private. Impact: - Uploaded files are not exposed through public storage URLs. - Document viewing for Admin/Admin Kewangan can be added later using the same private-storage pattern. ## 2026-05-29 - Phase 5 Attendance Boundary Decision: - Do not implement polling-day attendance in Phase 5. - Keep attendance screens, summaries, and exports for Phase 10. Reason: - The phase sequence separates PPM review/assignment from attendance reporting/export design. - Attendance has separate dashboard and export requirements that should be implemented together. Impact: - PPM dashboard still shows attendance as a future module. - Phase 10 remains responsible for polling-day attendance. ## 2026-05-29 - Phase 6 KTM Scope Service Decision: - Add `KtmScopeService` to define KTM ownership from active `staff_assignments` with position `KTM`, a Saluran Mengundi, and status `active`. Reason: - A KTM may only manage KP records under their own assigned Saluran/team. Impact: - KTM listing, registration, approval, and deletion are constrained to the current user's active KTM assignments. - KTM cannot manage another KTM's team. ## 2026-05-29 - Phase 6 KTM-Created Applicant Records Decision: - KTM registration creates `applications` rows with `source = created_by_ktm`, `status = submitted`, and requested position `KP`. - The KTM form does not accept a selectable role; malicious extra role fields are ignored by the service. Reason: - KTM is only allowed to register KP, not KTM, KPDP, PAPM, PPM, Police, KKM, or JKM. - The existing public duplicate-IC rule already blocks public self-registration while an active KTM-created record exists. Impact: - KTM-created applicants can be reviewed/approved by KTM under their own team. - Public self-registration remains blocked until the KTM-created record is deleted or cancelled. ## 2026-05-29 - Phase 6 Email Notification Decision: - Add queued Markdown Mailable `KtmCreatedApplicantMail`. - Queue the email when KTM creates a KP applicant. Reason: - The requirement asks that KTM-created applicants receive email notification with name, role, Pusat Mengundi, KTM name, and correction instructions. Impact: - Queue/mail configuration must be valid in production. - Tests use Laravel mail fake to verify the mailable is queued. ## 2026-05-29 - Phase 6 KTM Approval and Deletion Decision: - KTM approval of KP creates active `staff_assignments` with `reports_to_assignment_id` pointing to the KTM assignment and status `assigned` on the application. - KTM can delete only unassigned `created_by_ktm` records under their own assignment. Reason: - Approval is a placement action, and after registration closes only Admin should be allowed to register/edit/assign. - Deletion is needed to unblock public self-registration for applicants pre-created by KTM. Impact: - KTM approval is blocked after registration closes. - Deletion writes `deleted_by_ktm` status history and soft deletes the application. ## 2026-05-29 - Phase 7 Admin Management Route Boundary Decision: - Add Admin management routes under `/admin/management`, separate from `/admin/setup`. Reason: - Setup screens manage election structure and QR preparation, while Phase 7 manages operational application/staff corrections and representative records. Impact: - Admin dashboard now links to both setup and management. - Admin Kewangan is blocked from management routes because placement changes are Admin-only. ## 2026-05-29 - Phase 7 Manual Entry Source Decision: - Admin manual applications use `source = admin_manual`. - Direct Admin assignments use `source = admin_direct`, while edited assignments use `source = admin_update`. Reason: - Source flags distinguish public, KTM-created, PPM-approved, and Admin-maintained records for audit and later reporting. - Admin-keyed records must not trigger KTM-created applicant bank/detail request email behavior. Impact: - Manual records create pending bank verification state but no email notification. - Reports can filter records by operational source. ## 2026-05-29 - Phase 7 Post-Closing Catatan Enforcement Decision: - Centralize post-closing note enforcement in `AdminPostCloseNoteService`. - Require `catatan` when the election registration period is closed for Admin application edit, manual entry, direct assignment, assignment edit, and representative changes. Reason: - The original requirement makes post-registration Admin changes allowable only with a note. Impact: - Missing catatan returns validation errors. - Accepted post-closing changes create `system_notes` linked to the changed record. ## 2026-05-29 - Phase 7 Representative Management Decision: - Implement Police, KKM, and JKM manual entry in one representative management screen while keeping their existing separate database tables. Reason: - Operationally these records are maintained together by Admin, but the schema separates fields because Police can be saluran-specific and has rank/station fields. Impact: - PPM representative display can continue reading the existing separate tables. - Future edit forms can extend the same service without schema changes. ## 2026-05-29 - Phase 8 Finance Scope Decision: - Implement finance verification under `/kewangan/bank`. - Limit Admin Kewangan write access to `bank_verifications.status` and `finance_note`. Reason: - The finance role must verify bank details without changing placement or assignments. Impact: - Admin Kewangan can filter and update finance records. - Admin Kewangan remains blocked from Admin management assignment routes. ## 2026-05-29 - Phase 8 Finance Export Implementation Decision: - Implement finance XLSX export with OpenSpout in `app/Exports/FinanceVerificationExport`. - Keep export orchestration in `FinanceExportService` and query/filter logic in `FinanceVerificationQuery`. Reason: - The original preferred package `maatwebsite/excel` remains incompatible with this PHP 8.5/Laravel 13 environment. - The project already documented OpenSpout as the compatible export foundation. Impact: - Export files are stored in private local storage under `exports/finance`. - Every finance export writes an `export_logs` row with user, timestamp, report type, filters, file name, disk, and path. ## 2026-05-29 - Phase 8 Sensitive Finance Display Decision: - Add `App\Support\SensitiveData` masking helper and use it on finance screens for IC, phone, and bank account values. Reason: - Finance users need enough information to identify records, but full sensitive values should not be displayed unless necessary. Impact: - Finance list/detail screens mask IC, phone, and account numbers. - Future modules can reuse the same helper for consistent sensitive data handling. ## 2026-05-29 - Phase 9 Wheelchair Management Placement Decision: - Implement wheelchair management under Admin Management at `/admin/management/wheelchairs`. Reason: - Wheelchair allocation and movement are operational Admin functions tied to Pusat Mengundi, not initial election hierarchy setup. Impact: - Admin can access wheelchair management from the Admin management module. - Admin Kewangan and other roles cannot access wheelchair management routes. ## 2026-05-29 - Phase 9 Wheelchair Quantity Rules Decision: - Treat `wheelchair_allocations.allocated_quantity` as the maximum quantity available for a Pusat Mengundi. - Treat `wheelchair_transactions` rows as immutable movement records. - Calculate outstanding quantity as total taken minus total returned. Reason: - Separate movement records are more auditable than overwriting current state fields. Impact: - Taken transactions are blocked when quantity exceeds available allocation. - Returned transactions are blocked when quantity exceeds outstanding quantity. - Allocation cannot be reduced below outstanding quantity. - Every allocation change and movement is written to activity log. ## 2026-05-29 - Phase 10 Attendance Activation Decision: - Block PPM attendance recording unless `election_settings.is_attendance_active` is true. Reason: - Attendance should only be used on polling day or when explicitly activated by Admin settings. Impact: - PPM can see attendance screens but cannot submit records while inactive. - Tests explicitly activate the flag before successful recording. ## 2026-05-29 - Phase 10 Attendance Scope and Storage Decision: - Store attendance per active `staff_assignments` row. - Scope PPM recording by active PPM assignment to Pusat Mengundi. Reason: - Staff assignments already represent final placement and support dual roles. - Attendance belongs to a concrete operational assignment, not only a user. Impact: - A user with dual role can have separate attendance records per assignment. - Admin summaries can group attendance by Pusat, role, and Saluran. ## 2026-05-29 - Phase 10 Attendance Export Decision: - Implement attendance detail export using OpenSpout and `AttendanceExportService`. Reason: - Report format may change later, so export formatting is isolated from controllers. - OpenSpout remains the compatible XLSX writer for this environment. Impact: - Attendance detail exports are stored in private local storage under `exports/attendance`. - Every export writes an `export_logs` row with report type and Pusat filter parameters. ## 2026-05-29 - Phase 11 Document Access Hardening Decision: - Add authorized document download routes for Admin and Admin Kewangan. - Admin can download all application documents. - Admin Kewangan can download only `bank_statement` documents. Reason: - PPM document download existed, but Admin and Finance document access also needs an explicit private-storage controller path. - Finance users should not download IC documents because the finance workflow only requires bank evidence. Impact: - Sensitive files remain on private storage and are served only through authenticated, role-protected routes. - Document download actions are recorded in activity log. ## 2026-05-29 - Phase 11 Final Hardening Scope Decision: - Treat Phase 11 as final quality gate, security review, targeted test-gap closure, and documentation hardening. Reason: - The main feature phases are complete; remaining work should reduce release risk without changing broad behavior. Impact: - Added document authorization tests and sensitive-data masking unit test. - Deployment notes now include final quality gate commands and document-storage warnings. ## 2026-06-02 - Fasa 15-A: Export File Purge Strategy Decision: - Retention period default: 30 days. Configurable via `--days` option to allow shorter retention for audit events (e.g., polling day exports) or longer retention for post-event review. - Purge is triggered by `created_at < now - days` on the `export_logs` table, not `generated_at`, because `created_at` is always set by Eloquent and cannot be null. - Records with files already missing from disk are still marked `purged_at` to prevent repeated processing attempts. - `purged_by_user_id` is nullable and not set by the scheduled command (system-initiated purge). It is reserved for future manual purge UI where an Admin might trigger purge on demand and should be attributed. - The scheduled task uses `withoutOverlapping()` and `runInBackground()` so large purge runs don't block the scheduler. Reason: - Finance verification lists and attendance exports contain bank account numbers and personal data. Indefinite storage of these files increases breach risk. - A 30-day default balances post-export review window (staff may need to re-download for a few weeks) against data minimisation. Impact: - `routes/console.php` now contains a `Schedule::command(...)` entry. - `php artisan schedule:run` must be configured in the operating system cron (per existing deployment notes). - `export_logs` table has `purged_at` and `purged_by_user_id` columns via migration `2026_06_02_000001_add_purged_at_to_export_logs.php`. ## 2026-06-02 - Fasa 14-A: Applicant Status Portal Decision: - Route uses a plain string parameter `{publicUuid}` instead of route model binding `{application:public_uuid}` to prevent Laravel from returning 404 for unknown UUIDs. The controller handles all lookup and returns 200 with a generic message for invalid or internal-only UUIDs. - `deleted_by_ktm` applications and soft-deleted records return the same generic message as "not found", so the portal cannot be used to infer whether a UUID once existed. - Name masking shows only the first 3 characters to confirm identity to the applicant without revealing the full name to observers. - IC number, bank account, and phone are never displayed. - Rejection reason is displayed when status is `rejected` — this is intentional as the applicant has a right to know why their application was rejected. Reason: - Without this feature, applicants could only find out their status by contacting the Admin or PPM, creating unnecessary support burden. - UUID-as-identifier (not requiring login) balances accessibility with security — only the person who received the QR-generated UUID link can access the status. Impact: - Route: `GET /permohonan/{publicUuid}/status` → `public.applications.status`. - No authentication middleware applied to this route. - `SensitiveData::maskName()` added. ## 2026-06-02 - Fasa 12-E: Document Type Allowlists Decision: - Define document type allowlists as constants on the `ApplicationDocument` model (`ALLOWED_FOR_ADMIN`, `ALLOWED_FOR_PPM`, `ALLOWED_FOR_FINANCE`), making the single source of truth co-located with the model they describe. - All download paths reference these constants; adding a new document type requires a deliberate update to the relevant allowlist. Reason: - Admin and PPM paths had no explicit type checks — silently allowed any document type. If a new sensitive type were added, those roles would automatically get access without a conscious decision. - PPM path used 404 for ownership mismatch, which reveals document ID existence to unauthorised callers. Changed to 403. - PPM path had no activity log — inconsistent with Admin and Finance paths. Impact: - `ApplicationDocument::ALLOWED_FOR_ADMIN = ['ic_document', 'bank_statement']` — Admin can download both types. - `ApplicationDocument::ALLOWED_FOR_PPM = ['ic_document', 'bank_statement']` — PPM can download both types (needed to review before approval). - `ApplicationDocument::ALLOWED_FOR_FINANCE = ['bank_statement']` — Finance can only download bank statements. - Any new document type added to the system requires explicit inclusion in each allowlist before the relevant role can download it. ## 2026-06-02 - Fasa 13-A: Composite Database Indexes Decision: - Audit existing indexes before adding new ones. Verified that Phase 2 migration already contains `(election_id, ic_number)` and `(election_id, pusat_mengundi_id, status)` on `applications`, and `(election_id, pusat_mengundi_id, position_id)` and `(election_id, saluran_mengundi_id, position_id)` on `staff_assignments`. - Added only genuinely missing indexes via a separate migration to avoid modifying the baseline Phase 2 migration. - Did not add `(election_id, status)` on `bank_verifications` because that table does not have an `election_id` column. Reason: - `KtmVacancyService` is called on every public KP/KPDP application and on the form for KTM option display. Two queries inside it (`$assignedCount` and `$reservedCount`) had no composite coverage. - The admin application list query does not filter by `election_id`, so `(election_id, status)` on `applications` primarily helps dashboard count queries and future per-election reporting. Impact: - Migration: `2026_06_02_000000_add_performance_indexes.php`. - Index verification steps documented in `docs/08-deployment-notes.md`. - No model or service code changes required. ## 2026-06-02 - Fasa 12-C: Middleware RequireAdminPostCloseNote Decision: - Add `RequireAdminPostCloseNote` middleware and apply it to the entire `admin/management` route group as a safety net on top of the existing service-layer enforcement. - Keep all existing `$this->postCloseNoteService->requireIfClosed(...)` calls in services (they provide the correct election context for each record). - Middleware resolves the election from: `election_id` in the request body (create routes), or the first active election (edit/delete routes where the election comes from the model, not the request). Reason: - The existing service-layer enforcement is correct but convention-based — a new controller that forgets to call `requireIfClosed()` would silently bypass the gate. - Middleware provides an automatic belt-and-suspenders check that applies to ALL write operations in the route group regardless of which service is called. - The middleware approach is safe because: (a) single active election is the normal operating state, and (b) the service still does the precise check with the correct election. Impact: - Alias: `admin.post_close_note` registered in `bootstrap/app.php`. - Applied to `['auth', 'role:Admin', 'admin.post_close_note']` in `routes/web.php` for the management group. - Read operations (GET) pass through without note check. - Missing note returns redirect-back with validation errors on `note` field. ## 2026-06-02 - Fasa 12-A: Admin Settings UI Decision: - Add Admin Settings UI at `GET/PATCH /admin/setup/settings/{election}` for toggling `is_attendance_active` and `is_registration_open_override`. - Place the Settings screen within the existing `admin/setup` route group and include it in the `_nav.blade.php` as a warning-toned button. - Log all settings changes via `activity('admin_settings')` with before and after values. Reason: - Both `is_attendance_active` and `is_registration_open_override` were previously only editable via direct database access, which is unsafe in production. - The risk of accidental misconfiguration (e.g., forgetting to activate attendance on polling day) is high without a UI. Impact: - `ElectionSettingsController`, `ElectionSettingsService`, and `UpdateElectionSettingsRequest` added. - Route names: `admin.setup.settings.edit`, `admin.setup.settings.update`. - Accessibility: Admin role only; all other roles are forbidden. - `is_registration_open_override` exposes three logical states: null (auto), true (force open), false (force closed), mapped via HTML radio buttons with empty string for null. ## 2026-06-02 - Fasa 12-D: Pembetulan SampleElectionSeeder (CALON_TAMBAHAN) Decision: - Fixed `SampleElectionSeeder::seedQuotas()` to query position code `CALON_TAMBAHAN` instead of the old `CALON_SIMPANAN`. - Fixed `tests/Feature/AdminSetupTest.php` to include required operational counts (`saluran_count`, etc.) when posting to `admin.setup.pusat.store`, and to use saluran number '3' (not '1') to avoid conflict with auto-generated salurans from `PusatOperationalSetupService`. Reason: - The 2026-05-31 phase renamed `CALON_SIMPANAN` → `CALON_TAMBAHAN` via migration. The seeder was not updated, causing `KeyError` when accessing `$positions['CALON_TAMBAHAN']`. - The 2026-05-31 phase also made `saluran_count` required on POST for Pusat creation. The test was not updated to match, causing a conflict when it tried to manually create saluran number 1 that already existed. Impact: - `php artisan migrate:fresh --seed` now runs cleanly. - All 90 tests pass. - Pint and PHPStan pass. ## 2026-05-31 - Pendaftaran operasi semasa daftar Pusat Mengundi - Nama Saluran tidak lagi diperlukan pada UI. Saluran dikenal pasti menggunakan nombor di bawah Pusat Mengundi. - Borang daftar Pusat Mengundi menerima bilangan Saluran, KTM, Polis Iring, KP, PPM, KPDP, PAPM, JKM, KKM dan Calon Tambahan. - Saluran bernombor `1..N` dijana secara automatik semasa Pusat Mengundi dicipta. - Kuota `PPM`, `PAPM`, `JKM` dan `KKM` disimpan pada skop Pusat Mengundi. - Kuota `KTM`, `POLIS`, `KP`, `KPDP` dan `CALON_TAMBAHAN` disimpan pada setiap Saluran. Ini mengekalkan model operasi satu pasukan bagi setiap Saluran. - Istilah `CALON_SIMPANAN` ditukar kepada `CALON_TAMBAHAN` melalui migrasi data agar rekod sedia ada kekal boleh digunakan. - Penciptaan Pusat, Saluran dan kuota dilaksanakan dalam transaksi pangkalan data supaya konfigurasi tidak tertinggal separuh jika setup gagal.