297 lines
12 KiB
Markdown
297 lines
12 KiB
Markdown
# 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*
|