# Architecture **Analysis Date:** 2026-02-13 ## Pattern Overview **Overall:** MVC (Model-View-Controller) with Service Layer for complex business logic **Key Characteristics:** - **Layered architecture**: Controllers → Services → Models → Traits - **Multi-tier approval workflows**: Amount-based approval hierarchy (Secretary → Chair → Board) - **Double-entry bookkeeping**: Automatic accounting entry generation for financial documents - **Trait-based shared behavior**: Cross-cutting concerns (approval workflow, accounting entries) - **Audit logging**: Centralized action tracking via `AuditLogger::log()` - **Role-based access control (RBAC)**: Spatie Laravel Permission with financial roles ## Layers **Presentation Layer:** - Purpose: Render web UI and handle HTTP requests - Location: `resources/views/`, `app/Http/Controllers/` - Contains: Blade templates, controllers, Form Requests, API Resources - Depends on: Models, Services, View Components - Used by: Web browsers, Next.js frontend (via API) **Controller Layer:** - Purpose: HTTP request handling, input validation, orchestration - Location: `app/Http/Controllers/` - Contains: Controllers split by domain (Admin, Auth, Api) - Depends on: Models, Services, Form Requests - Pattern: Validation via Form Request classes (e.g., `StoreFinanceDocumentRequest`), service calls for business logic **Service Layer:** - Purpose: Encapsulate complex business logic - Location: `app/Services/` - Contains: `FinanceDocumentApprovalService`, `MembershipFeeCalculator`, `SettingsService`, `PaymentVerificationService`, `SiteRevalidationService` - Depends on: Models, Audit Logger, Mail - Example: `FinanceDocumentApprovalService` orchestrates multi-tier approval workflow; returns `['success' => bool, 'message' => string, 'complete' => bool]` **Model Layer:** - Purpose: Data access, business rules, relationships - Location: `app/Models/` - Contains: 36 Eloquent models including `Member`, `FinanceDocument`, `User`, `Issue`, `Article`, `Document` - Depends on: Database, Traits - Key models and relationships documented in Finance Workflow section **Trait Layer:** - Purpose: Shared behavior across multiple models - Location: `app/Traits/` - Contains: `HasApprovalWorkflow`, `HasAccountingEntries` - Usage: Models use these traits to inherit approval/accounting behavior without duplication **Support Layer:** - Purpose: Utility functions and helper classes - Location: `app/Support/` - Contains: `AuditLogger` (centralized audit logging), `DownloadFile` (file download handler) - Global helper: `settings('key')` function in `app/helpers.php` — returns cached system settings ## Data Flow ### Member Lifecycle Flow 1. **Registration**: Non-member completes form at `/register/member` → stored in `Members` table with `status = pending` 2. **Profile Activation**: Member logs in, fills profile at `/create-member-profile` 3. **Payment Submission**: Member submits payment proof at `/member/submit-payment` → `MembershipPayments` table 4. **Payment Verification**: Admin verifies payment via multi-tier approval workflow 5. **Activation**: Admin activates member → `status = active`, `membership_expires_at` set 6. **Expiry**: Cron job or admin action → `status = expired` when `membership_expires_at` passes **Key Methods:** - `Member::hasPaidMembership()` — checks if active with future expiry - `Member::canSubmitPayment()` — validates pending status with no pending payments - `Member::getNextFeeType()` — returns entrance_fee or annual_fee ### Finance Document Approval Flow (3-Stage Lifecycle) #### Stage 1: Approval (Amount-Based Multi-Tier) 1. **Submission**: User submits finance document → `FinanceDocument::STATUS_PENDING` - `submitted_by_user_id`, `submitted_at`, `amount`, `title` set - Amount tier auto-determined: `determineAmountTier()` → small/medium/large 2. **Secretary Approval**: Secretary reviews → `approveBySecretary()` - For small amount (<5,000): Approval complete, notifies submitter - For medium/large: Escalates to Chair, notifies next approvers - Status: `STATUS_APPROVED_SECRETARY` 3. **Chair Approval**: Chair reviews (if amount ≥ 5,000) → `approveByChair()` - For medium amount (5,000-50,000): Approval complete - For large amount (>50,000): Escalates to Board, notifies board members - Status: `STATUS_APPROVED_CHAIR` 4. **Board Approval**: Board members review (if amount > 50,000) → `approveByBoard()` - Final approval stage - Status: `STATUS_APPROVED_BOARD` **Rejection Handling**: User can reject at any stage → `STATUS_REJECTED`, triggers email notification **Helper Methods**: ```php $doc->isApprovalComplete() // All required approvals obtained $doc->canBeApprovedBySecretary($user) // Validates secretary can approve $doc->canBeApprovedByChair($user) // Validates chair can approve $doc->getAmountTierLabel() // Returns 小額/中額/大額 ``` #### Stage 2: Disbursement (Dual Confirmation) 1. **Requester Confirmation**: Document requester confirms disbursement details - `DISBURSEMENT_REQUESTER_CONFIRMED` status - Field: `disbursement_requester_confirmed_at` 2. **Cashier Confirmation**: Cashier verifies funds transferred - `DISBURSEMENT_CASHIER_CONFIRMED` status - Field: `disbursement_cashier_confirmed_at` - Creates `PaymentOrder` record 3. **Completion**: Once both confirm → `DISBURSEMENT_COMPLETED` **Helper**: `$doc->isDisbursementComplete()` — both parties confirmed #### Stage 3: Recording (Ledger Entry) 1. **Cashier Ledger Entry**: Cashier records transaction to ledger - Creates `CashierLedgerEntry` with debit/credit amounts - Status: `RECORDING_PENDING` → `RECORDING_COMPLETED` 2. **Accounting Entry**: Automatic double-entry bookkeeping via `HasAccountingEntries` trait - Creates `AccountingEntry` records (debit + credit pair) - Account codes from `config/accounting.php` - Field: `chart_of_account_id` **Helper**: `$doc->isRecordingComplete()` — ledger entry created **Full Completion**: `$doc->isFullyProcessed()` — all three stages complete --- ### Payment Verification Flow Separate approval workflow for `MembershipPayments` with three tiers: 1. **Cashier Approval**: Verifies payment received 2. **Accountant Approval**: Validates accounting entry 3. **Chair Approval**: Final sign-off for large payments **Route**: `/admin/payment-verifications` with status-based filtering --- ### CMS Content Flow (Articles/Pages) 1. **Creation**: Admin creates article at `/admin/articles/create` with: - Title, slug, body (markdown via EasyMDE), featured image, status - Categories and tags (many-to-many relationships) 2. **Publication**: Admin publishes → `status = published`, `published_at` set 3. **API Exposure**: Content available via `/api/v1/articles`, `/api/v1/pages` 4. **Site Revalidation**: On publish/archive, fires webhook to Next.js for static site rebuild - Service: `SiteRevalidationService` - Webhook: POST to `services.nextjs.revalidate_url` with token --- ### Document Library Flow 1. **Upload**: Admin uploads document at `/admin/documents/create` - Creates `Document` with `status = active` - Multiple versions tracked via `DocumentVersion` table 2. **Version Control**: - New version upload creates `DocumentVersion` record - Promote version: sets as current version - Archive: soft delete via `status = archived` 3. **Public Access**: Documents visible at `/documents` if `access_level` allows - Access tracking via `DocumentAccessLog` - QR code generation at `/documents/{uuid}/qrcode` 4. **Permission Check**: - Method: `$doc->canBeViewedBy($user)` validates access - Visibility: public, members-only, or admin-only --- **State Management:** - **Status Fields**: Use class constants, not enums (e.g., `FinanceDocument::STATUS_PENDING`) - **Timestamps**: Created/updated via timestamps; specific approval/rejection times via explicit fields - **Soft Deletes**: Used for documents (`status = archived`), not traditional soft delete - **Transactions**: DB transactions wrap multi-step operations (approval, payment, recording) ## Key Abstractions **HasApprovalWorkflow Trait:** - Purpose: Provides reusable multi-tier approval methods - Files: `app/Traits/HasApprovalWorkflow.php` - Used by: `FinanceDocument`, `MembershipPayment`, `Budget` - Methods: `isSelfApproval()`, `canProceedWithApproval()`, `canBeRejected()`, `getApprovalHistory()` - Pattern: Models define STATUS_* constants; trait provides helper methods; services orchestrate workflow **HasAccountingEntries Trait:** - Purpose: Automatic double-entry bookkeeping - Files: `app/Traits/HasAccountingEntries.php` - Used by: `FinanceDocument`, `Income`, `Transaction` - Methods: `debitEntries()`, `creditEntries()`, `validateBalance()`, `autoGenerateAccountingEntries()` - Pattern: Account IDs from `config/accounting.php`; amount tier determines debit/credit mapping **Audit Logging:** - Purpose: Track all business-critical actions - Class: `app/Support/AuditLogger` - Usage: `AuditLogger::log('finance_document.approved_by_secretary', $document, ['approved_by' => $user->name])` - Storage: `AuditLog` model with JSON metadata - Pattern: Static method callable from anywhere; filters secrets/uploads **Settings Service:** - Purpose: Centralized system configuration - File: `app/Services/SettingsService` - Usage: `settings('membership.entrance_fee')` or `settings('key', 'default')` - Cache: Redis/cache layer to avoid repeated DB queries - Locations: `SystemSetting` model, `config/` files ## Entry Points **Web Routes:** - Location: `routes/web.php` (393 lines covering all web endpoints) - Public: `/`, `/register/member` (if enabled), `/documents` - Authenticated: `/dashboard`, `/my-membership`, `/member/submit-payment` - Admin: `/admin/*` with `admin` middleware and named route prefix `admin.{module}.{action}` **API Routes:** - Location: `routes/api.php` - Version: v1 prefix - Endpoints: `/api/v1/articles`, `/api/v1/pages`, `/api/v1/homepage`, `/api/v1/public-documents` - Authentication: Sanctum (optional for public endpoints) **Artisan Commands:** - Location: `app/Console/Commands/` - Custom commands: `assign:role`, `import:members`, `import:accounting-data`, `import:documents`, `send:membership-expiry-reminders`, `archive:expired-documents` **Auth Routes:** - Location: `routes/auth.php` - Includes: Login, registration (if enabled), password reset, email verification - Middleware: `auth`, `verified` ## Error Handling **Strategy:** Exception-based with Blade error views **Patterns:** - **Form validation errors**: Automatically displayed in Blade templates via `@error` directive - **Authorization errors**: Thrown by `@can` directive or `authorize()` in controllers - **Business logic errors**: Services return `['success' => false, 'message' => 'reason']` or throw exceptions - **Audit logging errors**: Wrapped in try-catch to prevent breaking main flow - **Exception handling**: `app/Exceptions/Handler.php` defines response rendering **File uploads:** - Stored in `storage/app/` (private by default) - Finance documents: `storage/app/finance-documents/` - Article uploads: `storage/app/articles/` - Disability certificates: `storage/app/disability-certificates/` ## Cross-Cutting Concerns **Logging:** - Framework: Laravel's default logging (PSR-3 via Monolog) - Audit trail: `AuditLogger` class for business actions - Config: `config/logging.php` **Validation:** - Location: `app/Http/Requests/` — Form Request classes enforce rules - Pattern: Controller calls `$request->validated()` after implicit validation - Examples: `StoreMemberRequest`, `StoreFinanceDocumentRequest`, `UpdateMemberRequest` **Authentication:** - Framework: Laravel Breeze (session-based) - Models: `User` model with `HasFactory` and relationships to roles/permissions - Guards: `web` (default for sessions), `sanctum` (for API) **Authorization:** - Framework: Spatie Laravel Permission - Roles: `admin`, `finance_requester`, `finance_cashier`, `finance_accountant`, `finance_chair`, `finance_board_member`, `membership_manager` - Permissions: `view_*`, `create_*`, `edit_*`, `delete_*`, `publish_*`, custom permissions for finance tiers - Checking: `$user->can('permission-name')` in controllers, `@can` in Blade **Encryption:** - National IDs: AES-256 encrypted in `national_id_encrypted` column - Hash for searching: SHA256 in `national_id_hash` column - Virtual accessor `national_id`: Auto-decrypts on read - Method: `Member::findByNationalId($id)` uses hash for query --- *Architecture analysis: 2026-02-13*