Files
prn2026/docs/03-database-design.md
2026-06-03 08:51:22 +08:00

749 lines
16 KiB
Markdown

# Proposed Database Design
## Phase 2 Implementation Status
Status: Implemented on 2026-05-28.
Implemented in migration:
- `user_profiles`
- `elections`
- `election_settings`
- `bahagian_pilihanrayas`
- `daerah_mengundis`
- `pusat_mengundis`
- `saluran_mengundis`
- `positions`
- `position_quotas`
- `applications`
- `application_documents`
- `application_status_histories`
- `staff_assignments`
- `staff_assignment_histories`
- `police_escorts`
- `kkm_representatives`
- `jkm_representatives`
- `wheelchair_allocations`
- `wheelchair_transactions`
- `attendances`
- `bank_verifications`
- `export_logs`
- `system_notes`
Implemented models and relationships are available under `app/Models`.
Implementation notes:
- Public-facing identifiers use UUID columns.
- MySQL index names are explicitly shortened where Laravel's generated names would exceed MySQL's identifier limit.
- Complex business constraints such as "four KP per KTM" are intentionally enforced by future services rather than only by database constraints.
- A PPM plus KTM dual-role assignment is represented through two active rows in `staff_assignments`.
## Export Log Purge Columns (Fasa 15 — 2026-06-02)
Added via migration `2026_06_02_000001_add_purged_at_to_export_logs.php`.
| Column | Type | Purpose |
|---|---|---|
| `purged_at` | nullable timestamp | Set when the export file is deleted by `exports:purge` command |
| `purged_by_user_id` | nullable FK → users | Reserved for future manual purge UI attribution |
Records with `purged_at IS NOT NULL` are skipped on subsequent purge runs.
## Performance Indexes (Fasa 13 — 2026-06-02)
Added via migration `2026_06_02_000000_add_performance_indexes.php`.
### `applications` table
| Index name | Columns | Purpose |
|---|---|---|
| `app_election_status_idx` | `(election_id, status)` | Dashboard count queries and admin list filtered by election |
| `app_ktm_position_idx` | `(selected_ktm_assignment_id, requested_position_id)` | `KtmVacancyService` reserved-application count (called per KP/KPDP vacancy check) |
### `staff_assignments` table
| Index name | Columns | Purpose |
|---|---|---|
| `sa_reports_to_pos_status_idx` | `(reports_to_assignment_id, position_id, status)` | `KtmVacancyService` active-assignment count under a KTM (called per vacancy check) |
### Existing indexes (Phase 2)
| Table | Columns | Name |
|---|---|---|
| `applications` | `(election_id, ic_number)` | auto |
| `applications` | `(election_id, pusat_mengundi_id, status)` | auto |
| `staff_assignments` | `(election_id, pusat_mengundi_id, position_id)` | `sa_election_pusat_position_idx` |
| `staff_assignments` | `(election_id, saluran_mengundi_id, position_id)` | `sa_election_saluran_position_idx` |
| `attendances` | `(election_id, pusat_mengundi_id, status)` | `att_election_pusat_status_idx` |
| `attendances` | `(election_id, staff_assignment_id)` | `att_election_staff_unique` (unique) |
### Why `bank_verifications` was not indexed with `election_id`
`bank_verifications` does not have a direct `election_id` column — it joins through `application_id` (unique FK). All finance queries that need election filtering use `whereHas('application', ...)` subqueries. The existing `status` index and unique `application_id` index are sufficient for the current query patterns.
## Design Goals
- Support one election or multiple election events over time.
- Keep public identifiers separate from internal database IDs.
- Support dual-role assignment, especially PPM also acting as KTM.
- Keep sensitive documents private and access-controlled.
- Preserve auditability through history tables and activity log.
- Support exports, finance verification, attendance, wheelchair tracking, and post-closing notes.
## Naming Conventions
- Table names use Laravel plural snake_case where practical.
- Existing Malay domain terms are retained for clarity.
- Public UUID/ULID columns use names such as `public_uuid`.
- Status fields use lowercase snake_case values.
- Foreign keys use `foreignId()->constrained()` where possible.
## Core Identity Tables
### users
Laravel default user authentication table with additions as needed.
Key fields:
- `id`
- `name`
- `email`
- `password`
- `email_verified_at`
- `remember_token`
- timestamps
- soft deletes if operationally useful
Notes:
- Roles are assigned through `spatie/laravel-permission`.
- Not every public applicant must become a login user unless later required.
### user_profiles
Stores extended identity details for staff/admin users.
Key fields:
- `id`
- `user_id`
- `ic_number`
- `phone_number`
- `address`
- `bank_name`
- `bank_account_number`
- timestamps
Notes:
- Sensitive fields should be masked in views where full values are not required.
- IC uniqueness at profile level should be evaluated with election-specific application rules.
## Election and Settings
### elections
Represents each election event.
Key fields:
- `id`
- `public_uuid`
- `name`
- `code`
- `status`
- `starts_at`
- `polling_date`
- timestamps
- soft deletes
Suggested statuses:
- `draft`
- `active`
- `closed`
- `archived`
### election_settings
Stores event-specific operational settings.
Key fields:
- `id`
- `election_id`
- `registration_start_date`
- `registration_end_date`
- `polling_date`
- `is_registration_open`
- `is_registration_open_override` nullable
- `is_attendance_active`
- timestamps
Notes:
- Registration open state is computed from dates and `is_registration_open`.
- `is_registration_open_override` can be used later if the system needs to distinguish manual override from normal date-based availability.
- Attendance can be toggled separately for rehearsal or polling day.
## Election Hierarchy
### bahagian_pilihanrayas
Key fields:
- `id`
- `election_id`
- `code`
- `name`
- timestamps
- soft deletes
Relationships:
- belongs to election.
- has many daerah mengundi.
### daerah_mengundis
Key fields:
- `id`
- `election_id`
- `bahagian_pilihanraya_id`
- `code`
- `name`
- timestamps
- soft deletes
Relationships:
- belongs to election.
- belongs to bahagian pilihanraya.
- has many pusat mengundi.
### pusat_mengundis
Key fields:
- `id`
- `election_id`
- `daerah_mengundi_id`
- `public_uuid`
- `code`
- `name`
- `address`
- `registration_url`
- `qr_code_path`
- timestamps
- soft deletes
Relationships:
- belongs to election.
- belongs to daerah mengundi.
- has many saluran mengundi.
- has many position quotas.
- has many staff assignments.
- has one wheelchair allocation.
Notes:
- `public_uuid` is used by `/pohon/{pusat_mengundi_uuid}`.
- `registration_url` can be generated dynamically; storing it is optional. If stored, it must be refreshed if domain changes.
### saluran_mengundis
Key fields:
- `id`
- `election_id`
- `pusat_mengundi_id`
- `number`
- `name`
- `voter_count`
- timestamps
- soft deletes
Relationships:
- belongs to election.
- belongs to pusat mengundi.
- has many staff assignments.
## Roles and Permissions
Managed by `spatie/laravel-permission`:
- `roles`
- `permissions`
- `model_has_roles`
- `model_has_permissions`
- `role_has_permissions`
Application roles:
- Admin
- Admin Kewangan
- PPM
- KTM
- Pemohon
## Positions and Quotas
### positions
Stores operational staff positions.
Key fields:
- `id`
- `code`
- `name`
- `scope`
- `is_public_applyable`
- `is_assignable`
- `allows_dual_role`
- timestamps
- soft deletes
Suggested position codes:
- `PPM`
- `KTM`
- `KP`
- `KPDP`
- `PAPM`
- `POLIS`
- `KKM`
- `JKM`
- `CALON_SIMPANAN`
Suggested scopes:
- `pusat`
- `saluran`
- `external`
### position_quotas
Defines vacancy requirements by election, pusat, saluran, and role.
Key fields:
- `id`
- `election_id`
- `pusat_mengundi_id`
- `saluran_mengundi_id` nullable
- `position_id`
- `quota`
- timestamps
- soft deletes
Notes:
- PPM, PAPM, KKM, JKM, and wheelchair-related staffing are usually Pusat scope.
- KTM, KP, KPDP, Police escort, and calon simpanan are usually Saluran scope.
- Use nullable `saluran_mengundi_id` for Pusat-level quota.
## Applications
### applications
Represents public, KTM-created, or Admin-keyed applicant/staff application record.
Key fields:
- `id`
- `election_id`
- `pusat_mengundi_id`
- `selected_ktm_assignment_id` nullable
- `user_id` nullable
- `public_uuid`
- `source`
- `status`
- `name`
- `ic_number`
- `phone_number`
- `email`
- `address`
- `requested_position_id`
- `approved_position_id` nullable
- `bank_name`
- `bank_account_number`
- `created_by_user_id` nullable
- `approved_by_user_id` nullable
- `approved_at` nullable
- `rejected_by_user_id` nullable
- `rejected_at` nullable
- `rejection_reason` nullable
- timestamps
- soft deletes
Suggested source values:
- `public`
- `created_by_ktm`
- `admin_manual`
Suggested status values are documented in `docs/05-workflow-design.md`.
Rules:
- IC uniqueness is enforced per election among active/non-cancelled records.
- Public self-registration is blocked if an active `created_by_ktm` record exists for the same election and IC.
- Admin may manually enter records after registration closes with required note.
- Phase 4 public submissions create `source = public`, `status = submitted`, and a public UUID reference.
- For KP and KPDP public applications, `selected_ktm_assignment_id` stores the selected active KTM assignment if vacancy remains.
### application_documents
Stores private uploaded documents.
Key fields:
- `id`
- `application_id`
- `document_type`
- `disk`
- `path`
- `original_name`
- `mime_type`
- `size`
- `uploaded_by_user_id` nullable
- timestamps
- soft deletes
Document types:
- `ic_document`
- `bank_statement`
- `other`
Rules:
- IC and bank statement documents are required before PPM approval.
- Documents are stored on private disk.
- Phase 4 public uploads store `ic_document` and `bank_statement` under private local storage using the application public UUID path.
### application_status_histories
Tracks application lifecycle status changes.
Key fields:
- `id`
- `application_id`
- `from_status` nullable
- `to_status`
- `changed_by_user_id` nullable
- `note` nullable
- `metadata` JSON nullable
- timestamps
## Staff Assignments
### staff_assignments
Canonical table for approved staff placement.
Key fields:
- `id`
- `election_id`
- `application_id` nullable
- `user_id` nullable
- `position_id`
- `pusat_mengundi_id`
- `saluran_mengundi_id` nullable
- `reports_to_assignment_id` nullable
- `status`
- `assigned_by_user_id`
- `assigned_at`
- `source`
- timestamps
- soft deletes
Suggested statuses:
- `active`
- `pending`
- `cancelled`
- `replaced`
Notes:
- Allows one person to hold multiple assignments, including PPM and KTM.
- `reports_to_assignment_id` can link KP/KPDP to the approved KTM assignment.
- A KTM assignment to Saluran is created by PPM or Admin, not chosen by KTM.
- Unique constraints should prevent duplicate active position assignment to the same role/scope where business rules require.
### staff_assignment_histories
Tracks assignment changes.
Key fields:
- `id`
- `staff_assignment_id`
- `from_position_id` nullable
- `to_position_id` nullable
- `from_pusat_mengundi_id` nullable
- `to_pusat_mengundi_id` nullable
- `from_saluran_mengundi_id` nullable
- `to_saluran_mengundi_id` nullable
- `from_reports_to_assignment_id` nullable
- `to_reports_to_assignment_id` nullable
- `changed_by_user_id`
- `note` nullable
- timestamps
## Representatives and External Operational Data
### police_escorts
Key fields:
- `id`
- `election_id`
- `pusat_mengundi_id`
- `saluran_mengundi_id` nullable
- `name`
- `ic_number` nullable
- `phone_number` nullable
- `rank` nullable
- `station` nullable
- `notes` nullable
- timestamps
- soft deletes
### kkm_representatives
Key fields:
- `id`
- `election_id`
- `pusat_mengundi_id`
- `name`
- `ic_number` nullable
- `phone_number` nullable
- `agency` nullable
- `notes` nullable
- timestamps
- soft deletes
### jkm_representatives
Key fields:
- `id`
- `election_id`
- `pusat_mengundi_id`
- `name`
- `ic_number` nullable
- `phone_number` nullable
- `agency` nullable
- `notes` nullable
- timestamps
- soft deletes
## Wheelchair Management
### wheelchair_allocations
Key fields:
- `id`
- `election_id`
- `pusat_mengundi_id`
- `allocated_quantity`
- `notes` nullable
- timestamps
### wheelchair_transactions
Key fields:
- `id`
- `wheelchair_allocation_id`
- `transaction_type`
- `quantity`
- `taken_at` nullable
- `taken_by_name` nullable
- `returned_at` nullable
- `return_condition` nullable
- `recorded_by_user_id`
- `notes` nullable
- timestamps
Suggested transaction types:
- `taken`
- `returned`
- `adjustment`
Return condition values:
- `baik`
- `rosak`
## Attendance
### attendances
Key fields:
- `id`
- `election_id`
- `staff_assignment_id`
- `pusat_mengundi_id`
- `saluran_mengundi_id` nullable
- `position_id`
- `status`
- `check_in_time` nullable
- `recorded_by_user_id`
- `note` nullable
- timestamps
Attendance statuses:
- `present`
- `absent`
- `not_recorded`
Notes:
- Store denormalized pusat/saluran/position references for simpler reporting.
- A unique key should prevent multiple active attendance rows for the same election and staff assignment.
## Finance
### bank_verifications
Key fields:
- `id`
- `application_id`
- `status`
- `verified_by_user_id` nullable
- `verified_at` nullable
- `finance_note` nullable
- timestamps
Statuses:
- `pending`
- `verified`
- `rejected`
- `requires_correction`
Notes:
- Admin Kewangan can update this table only.
- Assignment and placement edits are outside Admin Kewangan scope.
- Phase 4 public submissions create an initial `pending` bank verification row.
## Exports and Notes
### export_logs
Key fields:
- `id`
- `election_id` nullable
- `generated_by_user_id`
- `generated_at`
- `report_type`
- `filter_parameters` JSON nullable
- `file_name`
- `disk` nullable
- `path` nullable
- timestamps
Report types:
- `full_staff_list`
- `staff_list_by_pusat`
- `staff_list_by_role`
- `missing_document_list`
- `finance_verification_list`
- `attendance_summary`
- `attendance_detail_by_pusat`
### system_notes
Stores structured operational notes, especially post-registration changes.
Key fields:
- `id`
- `election_id`
- `noteable_type`
- `noteable_id`
- `note_type`
- `note`
- `created_by_user_id`
- timestamps
Suggested note types:
- `post_registration_change`
- `admin_correction`
- `finance_note`
- `general`
## Audit Log
### activity_log
Managed by `spatie/laravel-activitylog`.
Required logged details:
- actor
- subject
- event name
- previous values
- new values
- timestamp
- note or reason where available
## Key Constraints and Indexes
Planned indexes:
- `elections.public_uuid`
- `pusat_mengundis.public_uuid`
- `applications.public_uuid`
- `applications.election_id, ic_number`
- `applications.election_id, pusat_mengundi_id, status`
- `staff_assignments.election_id, pusat_mengundi_id, position_id`
- `staff_assignments.election_id, saluran_mengundi_id, position_id`
- `bank_verifications.status`
- `attendances.election_id, pusat_mengundi_id, status`
- `export_logs.generated_by_user_id, generated_at`
Planned uniqueness:
- Unique active public UUIDs.
- One active PPM per Pusat Mengundi.
- One active KTM per Saluran Mengundi.
- One active KPDP per KTM or Saluran based on final rule implementation.
- Four active KP per KTM/team enforced by quota service, not only DB constraint.
- Application IC uniqueness per election for active public/admin records, with special handling for deleted/cancelled KTM-created records.
## Documented Trade-Offs
- `staff_assignments` is used instead of a single role column because the system must support dual-role assignment.
- Domain-specific history tables are retained in addition to generic activity log because workflows need reportable status/assignment timelines.
- Representative tables are separate from applications because Police, KKM, and JKM records may not follow the public applicant workflow.
- Attendance denormalizes role and location references to make election-day reporting faster and more stable.
## Kemas kini operasi Pusat Mengundi - 2026-05-31
- `saluran_mengundis.name` dikekalkan sebagai nullable untuk keserasian rekod lama, tetapi tidak lagi diminta pada UI.
- Semasa Pusat Mengundi baharu didaftarkan, Saluran bernombor `1..N` dijana secara automatik.
- `position_quotas` menyimpan `PPM`, `PAPM`, `JKM` dan `KKM` pada skop Pusat serta `KTM`, `POLIS`, `KP`, `KPDP` dan `CALON_TAMBAHAN` pada skop Saluran.
- Kod jawatan `CALON_SIMPANAN` dimigrasikan kepada `CALON_TAMBAHAN`.