docs: map existing codebase

This commit is contained in:
2026-02-13 10:34:18 +08:00
parent 296a70010d
commit 47218c1874
7 changed files with 2083 additions and 0 deletions

View File

@@ -0,0 +1,296 @@
# 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*