docs: map existing codebase
This commit is contained in:
296
.planning/codebase/ARCHITECTURE.md
Normal file
296
.planning/codebase/ARCHITECTURE.md
Normal 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*
|
||||||
293
.planning/codebase/CONCERNS.md
Normal file
293
.planning/codebase/CONCERNS.md
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
# Codebase Concerns
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-02-13
|
||||||
|
|
||||||
|
## Tech Debt
|
||||||
|
|
||||||
|
### FinanceDocument Model Complexity
|
||||||
|
- **Issue:** `app/Models/FinanceDocument.php` is 882 lines with 27+ status constants and multiple workflows (approval, disbursement, recording, payment). Contains both old workflow (cashier→accountant) and new workflow (secretary→chair→board) fields and methods coexisting.
|
||||||
|
- **Files:** `app/Models/FinanceDocument.php` (lines 1-882)
|
||||||
|
- **Impact:** Hard to maintain, difficult to extend, high risk of introducing bugs in approval logic. Multiple overlapping status fields make state transitions error-prone.
|
||||||
|
- **Fix approach:**
|
||||||
|
- Extract new workflow logic into separate `FinanceDocumentApprovalService` class (already partially exists in codebase)
|
||||||
|
- Remove deprecated cashier/accountant approval workflow entirely once old code migrated
|
||||||
|
- Consider using state machine package (e.g., `thiagoprz/eloquent-state-machine`) to manage state transitions
|
||||||
|
|
||||||
|
### Large Model Files
|
||||||
|
- **Issue:** Multiple models exceed 400+ lines: `Income` (467), `Document` (466), `Article` (460), `Member` (437), `Announcement` (427), `Issue` (363)
|
||||||
|
- **Files:** `app/Models/{Income,Document,Article,Member,Announcement,Issue}.php`
|
||||||
|
- **Impact:** Models are bloated with business logic; difficult to test and maintain
|
||||||
|
- **Fix approach:** Extract domain logic to service classes, keep models for data relationships only
|
||||||
|
|
||||||
|
### Backward Compatibility Overhead
|
||||||
|
- **Issue:** FinanceDocument maintains both old (cashier/accountant) and new (secretary/chair/board) approval workflows with deprecated methods marked `@deprecated`
|
||||||
|
- **Files:** `app/Models/FinanceDocument.php` (lines 551-568, 757+)
|
||||||
|
- **Impact:** Code duplication, confusing for developers, increases cognitive load
|
||||||
|
- **Fix approach:** Set migration deadline (e.g., March 2026), remove all deprecated methods and legacy status constants
|
||||||
|
|
||||||
|
## Known Bugs
|
||||||
|
|
||||||
|
### Site Revalidation Webhook Silent Failures
|
||||||
|
- **Symptoms:** Next.js static site may not update when articles/pages/documents are modified in Laravel admin
|
||||||
|
- **Files:** `app/Services/SiteRevalidationService.php` (lines 34-59)
|
||||||
|
- **Trigger:** Log warnings only at WARNING level when webhook fails; developers may not notice missed revalidations
|
||||||
|
- **Current state:** Catches exceptions but logs only warnings; webhook timeout is 5 seconds
|
||||||
|
- **Workaround:** Check Laravel logs manually for `"Site revalidation failed"` warnings
|
||||||
|
- **Fix approach:**
|
||||||
|
- Queue webhook calls with retry logic (e.g., 3 retries with exponential backoff)
|
||||||
|
- Log errors at ERROR level with alert notification
|
||||||
|
- Add monitoring for failed revalidations
|
||||||
|
|
||||||
|
### Email Notifications May Fail Silently
|
||||||
|
- **Symptoms:** Users don't receive approval/rejection notifications (e.g., `FinanceDocumentFullyApproved`, `PaymentSubmittedMail`)
|
||||||
|
- **Files:** `app/Http/Controllers/FinanceDocumentController.php` (lines 105-108, 174, 184, etc.)
|
||||||
|
- **Trigger:** Uses `Mail::queue()` without error handling or confirmation
|
||||||
|
- **Current state:** Emails queued but no verification of delivery
|
||||||
|
- **Fix approach:**
|
||||||
|
- Add event listeners to track mail failures
|
||||||
|
- Implement fallback notification channel (SMS via existing SMS service or in-app notifications)
|
||||||
|
- Log all mail queue operations with request ID for audit trail
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### National ID Decryption Exception Not Fully Handled
|
||||||
|
- **Risk:** Corrupted or mismatched encryption keys could cause decryption to fail; exception logged but member object may be in inconsistent state
|
||||||
|
- **Files:** `app/Models/Member.php` (lines 177-190)
|
||||||
|
- **Current mitigation:** Try-catch block logs error to Laravel log
|
||||||
|
- **Recommendations:**
|
||||||
|
- Rotate encryption keys with migration strategy for existing encrypted data
|
||||||
|
- Add health check command to verify all encrypted data can be decrypted
|
||||||
|
- Implement decryption retry with fallback (query original source if available)
|
||||||
|
- Document key rotation procedure in SECURITY.md
|
||||||
|
|
||||||
|
### Password Generation from Phone Number
|
||||||
|
- **Risk:** Password derived from last 4 digits of phone (`substr($phone, -4)`) is not cryptographically secure for initial user setup
|
||||||
|
- **Files:** `app/Console/Commands/ImportMembersCommand.php` (lines 135-146)
|
||||||
|
- **Current mitigation:** Used only during bulk import; users should change password after first login
|
||||||
|
- **Recommendations:**
|
||||||
|
- Force password reset on first login (add `force_password_reset` flag to User model)
|
||||||
|
- Use random password generation instead of phone-derived passwords
|
||||||
|
- Send temporary password via email (encrypted) or secure channel, never via phone number
|
||||||
|
|
||||||
|
### Mail Headers Not Validated
|
||||||
|
- **Risk:** Site revalidation webhook uses unvalidated `x-revalidate-token` header; no CSRF protection
|
||||||
|
- **Files:** `app/Services/SiteRevalidationService.php` (line 54)
|
||||||
|
- **Current mitigation:** Token stored in `.env` file
|
||||||
|
- **Recommendations:**
|
||||||
|
- Document token rotation process
|
||||||
|
- Add rate limiting to revalidation endpoint
|
||||||
|
- Consider using HMAC-SHA256 signed requests instead of bearer tokens
|
||||||
|
|
||||||
|
### Public Document UUID Leakage
|
||||||
|
- **Risk:** Documents have `public_uuid` exposed in API; uuid may be guessable if predictable
|
||||||
|
- **Files:** `app/Models/Document.php` (revalidation calls public_uuid)
|
||||||
|
- **Current mitigation:** Appears to use Laravel UUIDs (non-sequential)
|
||||||
|
- **Recommendations:**
|
||||||
|
- Verify UUID generation uses `Illuminate\Support\Str::uuid()` (v4, cryptographically random)
|
||||||
|
- Audit Document access control in `app/Http/Controllers/PublicDocumentController.php`
|
||||||
|
|
||||||
|
## Performance Bottlenecks
|
||||||
|
|
||||||
|
### Large Load in FinanceDocument Show Page
|
||||||
|
- **Problem:** `FinanceDocumentController::show()` eager loads 17 relationships, many of which may be unused in view
|
||||||
|
- **Files:** `app/Http/Controllers/FinanceDocumentController.php` (lines 116-141)
|
||||||
|
- **Cause:** Over-eager loading; should load relationships based on view requirements
|
||||||
|
- **Improvement path:**
|
||||||
|
- Split load into view-specific queries
|
||||||
|
- Use `with()` conditionally based on user role
|
||||||
|
- Measure N+1 queries with Laravel Debugbar
|
||||||
|
|
||||||
|
### N+1 Queries on Finance Index Listing
|
||||||
|
- **Problem:** FinanceDocument index loads 15 results with full relationships but filters happen in PHP, not database
|
||||||
|
- **Files:** `app/Http/Controllers/FinanceDocumentController.php` (lines 20-56)
|
||||||
|
- **Cause:** Workflow stage filtering happens after query, not in WHERE clause
|
||||||
|
- **Improvement path:**
|
||||||
|
- Move workflow_stage logic into Eloquent scopes
|
||||||
|
- Use `whereRaw()` for date-based stage detection if necessary
|
||||||
|
- Index database columns: `payment_order_created_at`, `payment_executed_at`, `cashier_ledger_entry_id`, `accounting_transaction_id`
|
||||||
|
|
||||||
|
### Import Commands Process Large Files In Memory
|
||||||
|
- **Problem:** `ImportMembersCommand`, `ImportAccountingData`, `ImportHugoContent` load entire Excel files into memory with PhpSpreadsheet
|
||||||
|
- **Files:** `app/Console/Commands/{ImportMembersCommand,ImportAccountingData,ImportHugoContent}.php`
|
||||||
|
- **Cause:** Using `IOFactory::load()` without streaming
|
||||||
|
- **Improvement path:**
|
||||||
|
- Use chunked reading for large files (PhpSpreadsheet supports `ChunkReadFilter`)
|
||||||
|
- Batch insert rows in groups of 100-500
|
||||||
|
- Add progress bar using Artisan command
|
||||||
|
|
||||||
|
### Mail Queue Without Batch Processing
|
||||||
|
- **Problem:** Finance document approval sends individual emails in loop (e.g., line 105-108) without batching
|
||||||
|
- **Files:** `app/Http/Controllers/FinanceDocumentController.php` (lines 105-108, 182-185, 215-218)
|
||||||
|
- **Cause:** Loop creates separate Mail::queue() call for each cashier/chair/board member
|
||||||
|
- **Improvement path:**
|
||||||
|
- Collect all recipients and send single batch notification
|
||||||
|
- Use Mailable::withSymfonyMessage() to set BCC instead of TO:recipient loop
|
||||||
|
- Consider notification channels for role-based sending
|
||||||
|
|
||||||
|
## Fragile Areas
|
||||||
|
|
||||||
|
### Multi-Tier Approval Workflow State Machine
|
||||||
|
- **Files:** `app/Models/FinanceDocument.php`, `app/Http/Controllers/FinanceDocumentController.php`
|
||||||
|
- **Why fragile:**
|
||||||
|
- Complex conditional logic determining next approver (amount tier + status = next tier)
|
||||||
|
- No atomic state transitions; multiple `update()` calls without transactions
|
||||||
|
- Amount tier can change after approval (potential issue if amount modified)
|
||||||
|
- Self-approval prevention logic repeated in multiple methods
|
||||||
|
- **Safe modification:**
|
||||||
|
- Add database constraint: `UNIQUE(finance_documents, status, approved_by_secretary_id)` to prevent duplicate approvals
|
||||||
|
- Wrap approval logic in explicit DB::transaction() in controller
|
||||||
|
- Add test for each tier (small, medium, large) with all roles
|
||||||
|
- **Test coverage:** Good coverage exists (`tests/Unit/FinanceDocumentTest.php`) but missing edge cases
|
||||||
|
|
||||||
|
### Member Encryption/Decryption
|
||||||
|
- **Files:** `app/Models/Member.php` (lines 175-207)
|
||||||
|
- **Why fragile:**
|
||||||
|
- Decryption failure returns null without distinction from empty field
|
||||||
|
- No validation that decrypted data is valid national ID format
|
||||||
|
- Hash function (SHA256) hardcoded; no version indicator if hashing algorithm changes
|
||||||
|
- **Safe modification:**
|
||||||
|
- Create `MemberEncryption` helper class with version support
|
||||||
|
- Add `$member->national_id_decryption_failed` flag to track failures
|
||||||
|
- Validate decrypted ID format matches Taiwan ID requirements
|
||||||
|
- **Test coverage:** Missing - no tests for encryption/decryption failure scenarios
|
||||||
|
|
||||||
|
### Article Slug Generation with Chinese Characters
|
||||||
|
- **Files:** `app/Models/Article.php` (lines 84-109)
|
||||||
|
- **Why fragile:**
|
||||||
|
- Slug fallback uses `Str::ascii()` which may produce empty string for pure Chinese titles
|
||||||
|
- Final fallback `'article-'.time()` creates non-semantic slugs
|
||||||
|
- No uniqueness guarantee if two articles created in same second
|
||||||
|
- **Safe modification:**
|
||||||
|
- Use pinyin conversion library for Chinese titles
|
||||||
|
- Always append counter to ensure uniqueness (current code does this, but starting from base slug not timestamp)
|
||||||
|
- **Test coverage:** Missing - no tests for Chinese title slug generation
|
||||||
|
|
||||||
|
### Document Access Control
|
||||||
|
- **Files:** `app/Http/Controllers/PublicDocumentController.php`, `app/Http/Controllers/Api/PublicDocumentController.php`
|
||||||
|
- **Why fragile:**
|
||||||
|
- Document access depends on category and user role
|
||||||
|
- Multiple access control points (controller + model) make audit trail difficult
|
||||||
|
- No consistent logging of document access
|
||||||
|
- **Safe modification:**
|
||||||
|
- Centralize access checks in `Document::canBeViewedBy()` method
|
||||||
|
- Add audit logging to `DocumentAccessLog` table on every access
|
||||||
|
- Use policy classes for clearer authorization
|
||||||
|
- **Test coverage:** Missing - no tests for document access control by category
|
||||||
|
|
||||||
|
## Scaling Limits
|
||||||
|
|
||||||
|
### SQLite in Development, MySQL in Production
|
||||||
|
- **Current capacity:** SQLite limited by single process/file locks
|
||||||
|
- **Limit:** Development environment may not catch MySQL-specific issues (JSON functions, full text search, etc.)
|
||||||
|
- **Scaling path:**
|
||||||
|
- Add CI step running tests against MySQL
|
||||||
|
- Use Docker Compose with MySQL for local development
|
||||||
|
- Document differences in CLAUDE.md
|
||||||
|
|
||||||
|
### Audit Logging Unbounded Growth
|
||||||
|
- **Current capacity:** `AuditLog` table grows with every action (`AuditLogger::log()` called throughout codebase)
|
||||||
|
- **Limit:** No retention policy; table will grow indefinitely
|
||||||
|
- **Scaling path:**
|
||||||
|
- Add scheduled cleanup command (`php artisan audit-logs:prune --days=90`)
|
||||||
|
- Archive old logs to separate table or external storage
|
||||||
|
- Add indexes on `auditable_id`, `auditable_type`, `created_at` for queries
|
||||||
|
|
||||||
|
### File Upload Storage
|
||||||
|
- **Current capacity:** Attachments stored in `storage/app/local` and `storage/app/public` without quota
|
||||||
|
- **Limit:** Disk space will fill with finance documents, articles, disability certificates
|
||||||
|
- **Scaling path:**
|
||||||
|
- Migrate uploads to S3 or cloud storage
|
||||||
|
- Add cleanup for soft-deleted documents
|
||||||
|
- Implement virus scanning for uploaded files
|
||||||
|
|
||||||
|
## Dependencies at Risk
|
||||||
|
|
||||||
|
### barryvdh/laravel-dompdf (PDF Generation)
|
||||||
|
- **Risk:** Package maintained but based on Dompdf which has history of security issues in HTML/CSS parsing
|
||||||
|
- **Impact:** User-submitted content (article attachments, issue descriptions) rendered to PDF could be exploited
|
||||||
|
- **Migration plan:**
|
||||||
|
- Sanitize HTML before PDF generation
|
||||||
|
- Consider mPDF or TCPDF as alternatives
|
||||||
|
- Test with OWASP XSS payloads in PDF generation
|
||||||
|
|
||||||
|
### maatwebsite/excel (Excel Import/Export)
|
||||||
|
- **Risk:** PhpSpreadsheet (underlying library) may have formula injection vulnerabilities
|
||||||
|
- **Impact:** Imported Excel files with formulas could execute on user machines
|
||||||
|
- **Migration plan:**
|
||||||
|
- Strip formulas during import (`setReadDataOnly(true)`)
|
||||||
|
- Validate cell content is not formula before importing
|
||||||
|
- Document in user guide: "Do not import untrusted Excel files"
|
||||||
|
|
||||||
|
### simplesoftwareio/simple-qrcode
|
||||||
|
- **Risk:** Package has been archived; no active maintenance
|
||||||
|
- **Impact:** Potential security vulnerabilities won't be patched
|
||||||
|
- **Migration plan:**
|
||||||
|
- Consider switch to `chillerlan/php-qrcode` (actively maintained)
|
||||||
|
- Audit current QR generation code for security issues
|
||||||
|
- Test QR code generation with large payloads
|
||||||
|
|
||||||
|
### spatie/laravel-permission (Role/Permission)
|
||||||
|
- **Risk:** Complex permission system; incorrect configuration could expose restricted data
|
||||||
|
- **Impact:** Member lifecycle, finance approvals depend entirely on permission configuration
|
||||||
|
- **Migration plan:**
|
||||||
|
- Document all role/permission requirements in RBAC_SPECIFICATION.md
|
||||||
|
- Add smoke test verifying each role can/cannot access correct routes
|
||||||
|
- Audit permission checks with debugbar on each protected route
|
||||||
|
|
||||||
|
## Missing Critical Features
|
||||||
|
|
||||||
|
### No Rate Limiting on Finance Approval
|
||||||
|
- **Problem:** No rate limit prevents rapid approval/rejection spam
|
||||||
|
- **Blocks:** Audit trail integrity; potential for accidental approvals
|
||||||
|
- **Recommendation:** Add throttle middleware on `FinanceDocumentController::approve()` (1 approval per 2 seconds per user)
|
||||||
|
|
||||||
|
### No Email Delivery Confirmation
|
||||||
|
- **Problem:** System queues emails but provides no confirmation users received them
|
||||||
|
- **Blocks:** Important notifications (payment rejection, expense approval) may go unseen
|
||||||
|
- **Recommendation:** Implement email open tracking or delivery webhook with SendGrid/Mailgun
|
||||||
|
|
||||||
|
### No Database Transaction on Multi-Step Operations
|
||||||
|
- **Problem:** Finance document approval updates multiple fields in separate queries (lines 160-164)
|
||||||
|
- **Blocks:** Partial failures could leave document in inconsistent state
|
||||||
|
- **Recommendation:** Wrap all multi-step operations in explicit `DB::transaction()`
|
||||||
|
|
||||||
|
### No Async Job Retry Logic
|
||||||
|
- **Problem:** Site revalidation webhook has only catch block, no retry queue
|
||||||
|
- **Blocks:** Temporary network issues cause permanent cache inconsistency
|
||||||
|
- **Recommendation:** Use queued jobs with `ShouldBeEncrypted` and exponential backoff
|
||||||
|
|
||||||
|
## Test Coverage Gaps
|
||||||
|
|
||||||
|
### Finance Approval Multi-Tier Logic
|
||||||
|
- **What's not tested:** All tier combinations with role changes (e.g., chair approves small amount, medium amount requires board)
|
||||||
|
- **Files:** `app/Models/FinanceDocument.php` (determineAmountTier, canBeApprovedByChair, etc.)
|
||||||
|
- **Risk:** Tier logic changes could silently break approval chain
|
||||||
|
- **Priority:** High - this is financial workflow core
|
||||||
|
|
||||||
|
### Member Encryption Edge Cases
|
||||||
|
- **What's not tested:** Decryption failures, key rotation, migration from plaintext
|
||||||
|
- **Files:** `app/Models/Member.php` (getNationalIdAttribute, setNationalIdAttribute)
|
||||||
|
- **Risk:** Corrupted encryption keys could cause data loss
|
||||||
|
- **Priority:** High - financial impact
|
||||||
|
|
||||||
|
### Article Slug Generation for Chinese Titles
|
||||||
|
- **What's not tested:** Pure Chinese titles, mixed Chinese/English, special characters
|
||||||
|
- **Files:** `app/Models/Article.php` (generateUniqueSlug)
|
||||||
|
- **Risk:** Frontend routing breaks if slug generation fails
|
||||||
|
- **Priority:** Medium - affects CMS functionality
|
||||||
|
|
||||||
|
### Document Access Control by Category and Role
|
||||||
|
- **What's not tested:** Board members accessing member-only documents, public documents accessible without auth
|
||||||
|
- **Files:** `app/Http/Controllers/PublicDocumentController.php`, `app/Models/Document.php`
|
||||||
|
- **Risk:** Private documents exposed to wrong users
|
||||||
|
- **Priority:** High - security critical
|
||||||
|
|
||||||
|
### Email Notification Delivery
|
||||||
|
- **What's not tested:** Actual email sends, template rendering, missing email addresses
|
||||||
|
- **Files:** All Mail classes in `app/Mail/`
|
||||||
|
- **Risk:** Users don't know about approvals/rejections
|
||||||
|
- **Priority:** Medium - business critical but hard to test
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Concerns audit: 2026-02-13*
|
||||||
199
.planning/codebase/CONVENTIONS.md
Normal file
199
.planning/codebase/CONVENTIONS.md
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
# Coding Conventions
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-02-13
|
||||||
|
|
||||||
|
## Naming Patterns
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Classes: PascalCase (e.g., `FinanceDocument.php`, `MembershipFeeCalculator.php`)
|
||||||
|
- Controllers: PascalCase with `Controller` suffix (e.g., `MemberPaymentController.php`)
|
||||||
|
- Traits: PascalCase with descriptive names (e.g., `HasApprovalWorkflow.php`, `HasAccountingEntries.php`)
|
||||||
|
- Models: PascalCase singular (e.g., `Member.php`, `FinanceDocument.php`)
|
||||||
|
- Services: PascalCase with `Service` suffix (e.g., `MembershipFeeCalculator.php`, `SettingsService.php`)
|
||||||
|
- Requests: PascalCase with action + `Request` suffix (e.g., `StoreMemberRequest.php`, `UpdateIssueRequest.php`)
|
||||||
|
- Database: Migration files use timestamp prefix with snake_case (Laravel convention)
|
||||||
|
|
||||||
|
**Functions:**
|
||||||
|
- Methods: camelCase (e.g., `createMember()`, `getBaseAmount()`, `isSelfApproval()`)
|
||||||
|
- Helper functions: camelCase, wrapped in function_exists check (e.g., `settings()` in `app/helpers.php`)
|
||||||
|
- Test methods: camelCase, prefixed with `test_` or use `/** @test */` doc comment (see `tests/Unit/MembershipPaymentTest.php`)
|
||||||
|
- Model query scopes: camelCase (Laravel convention)
|
||||||
|
|
||||||
|
**Variables:**
|
||||||
|
- Local variables: camelCase (e.g., `$baseAmount`, `$feeDetails`, `$discountRate`)
|
||||||
|
- Model attributes: snake_case in database, accessed as camelCase properties (Laravel convention)
|
||||||
|
- Constants: UPPER_SNAKE_CASE (e.g., `STATUS_PENDING`, `AMOUNT_TIER_SMALL`, `IDENTITY_PATIENT`)
|
||||||
|
- Protected/private properties: camelCase with prefix (e.g., `$settings`, `$feeCalculator`)
|
||||||
|
|
||||||
|
**Types:**
|
||||||
|
- Model classes: Use `User`, `Member`, `FinanceDocument` (singular)
|
||||||
|
- Collections: Arrays or `Collection` type hints
|
||||||
|
- Enums: Not used; status values stored as class constants (e.g., in `FinanceDocument.php` lines 15-59)
|
||||||
|
- Nullable types: Use `?Type` (e.g., `?User $user`)
|
||||||
|
- Return type hints: Always specified (e.g., `public function calculate(...): array`)
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
**Formatting:**
|
||||||
|
- Tool: Laravel Pint (PHP code style formatter)
|
||||||
|
- Run command: `./vendor/bin/pint`
|
||||||
|
- Indentation: 4 spaces (configured in `.editorconfig`)
|
||||||
|
- Line endings: LF (configured in `.editorconfig`)
|
||||||
|
- Final newline: Required (configured in `.editorconfig`)
|
||||||
|
- Trailing whitespace: Trimmed (configured in `.editorconfig`)
|
||||||
|
|
||||||
|
**Language versions:**
|
||||||
|
- PHP: 8.1+ (required in `composer.json`)
|
||||||
|
- Laravel: 10.10+ (required in `composer.json`)
|
||||||
|
|
||||||
|
**Linting:**
|
||||||
|
- Tool: PHPStan (static analysis)
|
||||||
|
- Run command: `./vendor/bin/phpstan analyse`
|
||||||
|
- No ESLint/Prettier for frontend (basic Tailwind + Alpine.js)
|
||||||
|
|
||||||
|
## Import Organization
|
||||||
|
|
||||||
|
**Order:**
|
||||||
|
1. PHP core/standard library classes (`use Illuminate\*`, `use App\*`)
|
||||||
|
2. Third-party library classes (`use Spatie\*`, `use Barryvdh\*`)
|
||||||
|
3. Application namespaces (`use App\Models\*`, `use App\Services\*`, `use App\Traits\*`)
|
||||||
|
4. Facades and static imports last (`use Illuminate\Support\Facades\*`)
|
||||||
|
|
||||||
|
**Example from `MemberPaymentController.php`:**
|
||||||
|
```php
|
||||||
|
use App\Mail\PaymentSubmittedMail;
|
||||||
|
use App\Models\Member;
|
||||||
|
use App\Models\MembershipPayment;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\MembershipFeeCalculator;
|
||||||
|
use App\Support\AuditLogger;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Path Aliases:**
|
||||||
|
- No custom aliases configured; use standard PSR-4 autoloading
|
||||||
|
- Namespace hierarchy: `App\Models\`, `App\Services\`, `App\Http\Controllers\`, `App\Http\Requests\`, `App\Traits\`, `App\Support\`
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
**Patterns:**
|
||||||
|
- Use try-catch blocks for complex operations with side effects (see `BankReconciliationController.php` lines 75-81)
|
||||||
|
- Catch generic `\Exception` when handling database/file operations
|
||||||
|
- Use DB transactions for multi-step operations requiring rollback
|
||||||
|
- Authorization: Use `$this->authorize('permission-name')` in controllers (Gate authorization)
|
||||||
|
- Validation: Use Form Request classes (`StoreMemberRequest`, `UpdateMemberRequest`) in controller methods
|
||||||
|
- Permission checks: Use Spatie Laravel Permission with roles and permissions (see `SeedsRolesAndPermissions` trait)
|
||||||
|
|
||||||
|
**Example from `BankReconciliationController.php`:**
|
||||||
|
```php
|
||||||
|
DB::beginTransaction();
|
||||||
|
try {
|
||||||
|
// Handle bank statement file upload
|
||||||
|
$statementPath = null;
|
||||||
|
if ($request->hasFile('bank_statement_file')) {
|
||||||
|
$statementPath = $request->file('bank_statement_file')->store('bank-statements', 'local');
|
||||||
|
}
|
||||||
|
// ... process data
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return redirect()->back()->with('error', 'Error message');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
**Framework:** None configured; uses Laravel's default logging or direct logging via `Storage` facades.
|
||||||
|
|
||||||
|
**Patterns:**
|
||||||
|
- Use `AuditLogger` static class for business logic audit trail (see `app/Support/AuditLogger.php`)
|
||||||
|
- Call pattern: `AuditLogger::log($action, $auditable, $metadata)`
|
||||||
|
- Example usage in `MemberPaymentController.php` and other controllers
|
||||||
|
- All member/finance/permission changes should be logged via AuditLogger
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
**When to Comment:**
|
||||||
|
- Trait usage instructions (see `HasApprovalWorkflow.php` lines 7-14)
|
||||||
|
- Complex business logic requiring explanation (e.g., multi-tier approval, fee calculations)
|
||||||
|
- Prevent: Obvious comments describing what code does (let code be self-documenting)
|
||||||
|
- Include: Comments explaining WHY, not WHAT
|
||||||
|
|
||||||
|
**JSDoc/TSDoc:**
|
||||||
|
- Use PHPDoc for public methods (see `MembershipFeeCalculator.php` lines 17-23)
|
||||||
|
- Parameter types documented with `@param Type $variable`
|
||||||
|
- Return types documented with `@return Type`
|
||||||
|
- Example from `calculate()` method:
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* 計算會費金額
|
||||||
|
*
|
||||||
|
* @param Member $member 會員
|
||||||
|
* @param string $feeType 會費類型 (entrance_fee | annual_fee)
|
||||||
|
* @return array{base_amount: float, discount_amount: float, final_amount: float, disability_discount: bool, fee_type: string}
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Chinese Comments:**
|
||||||
|
- UI-facing text and business logic comments may use Traditional Chinese
|
||||||
|
- Code comments default to English for consistency
|
||||||
|
- No specific rule enforced; follow existing pattern in related files
|
||||||
|
|
||||||
|
## Function Design
|
||||||
|
|
||||||
|
**Size:** Keep under 30-40 lines per method
|
||||||
|
- Controllers: Each action method typically 20-30 lines
|
||||||
|
- Services: Business logic methods 15-25 lines
|
||||||
|
- Traits: Utility methods 5-15 lines
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- Maximum 5 parameters per method
|
||||||
|
- Use dependency injection for services (constructor or method parameter)
|
||||||
|
- Use Form Request classes for validation parameters (not raw `Request`)
|
||||||
|
- Optional parameters use `?Type` or default values
|
||||||
|
|
||||||
|
**Return Values:**
|
||||||
|
- Always include return type hint (e.g., `: array`, `: bool`, `: void`)
|
||||||
|
- Use arrays for multiple values needing structure (see `MembershipFeeCalculator.php` line 24)
|
||||||
|
- Use model instances for Eloquent operations
|
||||||
|
- Return `null` explicitly when appropriate (use `?Type` hint)
|
||||||
|
|
||||||
|
## Module Design
|
||||||
|
|
||||||
|
**Exports:**
|
||||||
|
- Laravel models are auto-discovered via namespace
|
||||||
|
- Controllers are auto-discovered via `App\Http\Controllers` namespace
|
||||||
|
- Requests are auto-discovered via `App\Http\Requests` namespace
|
||||||
|
- Traits are explicitly `use`d in consuming classes
|
||||||
|
|
||||||
|
**Barrel Files:**
|
||||||
|
- Not used; each file has single responsibility
|
||||||
|
- Use explicit imports rather than wildcard imports
|
||||||
|
|
||||||
|
**Status Constants Pattern:**
|
||||||
|
- All statuses defined as class constants, never magic strings or enums
|
||||||
|
- Grouped by category (approval status, disbursement status, recording status)
|
||||||
|
- Examples from `FinanceDocument.php`:
|
||||||
|
```php
|
||||||
|
public const STATUS_PENDING = 'pending';
|
||||||
|
public const STATUS_APPROVED_SECRETARY = 'approved_secretary';
|
||||||
|
public const DISBURSEMENT_PENDING = 'pending';
|
||||||
|
public const DISBURSEMENT_REQUESTER_CONFIRMED = 'requester_confirmed';
|
||||||
|
```
|
||||||
|
|
||||||
|
**Service Injection Pattern:**
|
||||||
|
- Constructor injection for services (see `MemberPaymentController.__construct()`)
|
||||||
|
- Store in protected property for method access
|
||||||
|
- Use type hints for IDE support
|
||||||
|
|
||||||
|
**Trait Usage Pattern:**
|
||||||
|
- Mixed concern traits (e.g., `HasApprovalWorkflow`, `HasAccountingEntries`) used in models
|
||||||
|
- Test helper traits (e.g., `SeedsRolesAndPermissions`, `CreatesFinanceData`) used in test classes
|
||||||
|
- Traits documented at top with usage instructions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Convention analysis: 2026-02-13*
|
||||||
281
.planning/codebase/INTEGRATIONS.md
Normal file
281
.planning/codebase/INTEGRATIONS.md
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
# External Integrations
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-02-13
|
||||||
|
|
||||||
|
## APIs & External Services
|
||||||
|
|
||||||
|
**Next.js Frontend Site Revalidation:**
|
||||||
|
- Service: Custom webhook to Next.js `/api/revalidate`
|
||||||
|
- What it's used for: Trigger static site regeneration when articles, pages, or documents change
|
||||||
|
- SDK/Client: Guzzle HTTP client (`guzzlehttp/guzzle`)
|
||||||
|
- Implementation: `app/Services/SiteRevalidationService.php`
|
||||||
|
- Methods:
|
||||||
|
- `SiteRevalidationService::revalidateArticle(?slug)` - Trigger article cache invalidation
|
||||||
|
- `SiteRevalidatService::revalidatePage(?slug)` - Trigger page cache invalidation
|
||||||
|
- `SiteRevalidationService::revalidateDocument(?slug)` - Trigger document cache invalidation
|
||||||
|
- Triggered by: `app/Observers/DocumentObserver.php` on article/page create/update/delete
|
||||||
|
- Timeout: 5 seconds
|
||||||
|
- Error handling: Logged as warning, does not fail request
|
||||||
|
|
||||||
|
**Email Service Provider (Configurable):**
|
||||||
|
- Services supported: SMTP, Mailgun, Postmark, AWS SES
|
||||||
|
- SDK/Client: Laravel Mail facades
|
||||||
|
- Implementation: Controllers use `Mail::to($email)->queue(MailClass::class)`
|
||||||
|
- Auth: `MAIL_HOST`, `MAIL_PORT`, `MAIL_USERNAME`, `MAIL_PASSWORD`, or API keys
|
||||||
|
- Dev/Test: Mailpit on `localhost:1025` (configured in `.env.example`)
|
||||||
|
- Queue integration: All mail uses async queue for better performance
|
||||||
|
|
||||||
|
## Data Storage
|
||||||
|
|
||||||
|
**Primary Database:**
|
||||||
|
- Type/Provider: MySQL (production) or SQLite (development)
|
||||||
|
- Connection config: `config/database.php`
|
||||||
|
- Connection env vars:
|
||||||
|
- `DB_CONNECTION` - Type: `mysql` or `sqlite`
|
||||||
|
- `DB_HOST` - Database server (e.g., `127.0.0.1`)
|
||||||
|
- `DB_PORT` - Port (default 3306 for MySQL)
|
||||||
|
- `DB_DATABASE` - Database name
|
||||||
|
- `DB_USERNAME` - Username
|
||||||
|
- `DB_PASSWORD` - Password
|
||||||
|
- ORM/Client: Laravel Eloquent (built-in)
|
||||||
|
- Encryption: AES-256-CBC via `config/app.php` cipher
|
||||||
|
- Notable tables:
|
||||||
|
- `users` - System users (staff, admin)
|
||||||
|
- `members` - Member profiles (encrypted national IDs)
|
||||||
|
- `finance_documents` - Finance approvals with multi-tier workflows
|
||||||
|
- `payments` - Member fee payments
|
||||||
|
- `articles`, `pages`, `categories`, `tags` - CMS content
|
||||||
|
- `documents` - Document library entries
|
||||||
|
- `roles`, `permissions`, `model_has_roles` - Spatie permission tables
|
||||||
|
- `settings` - System-wide configuration (cached)
|
||||||
|
|
||||||
|
**File Storage:**
|
||||||
|
- Databases: Local disk and S3 configurable
|
||||||
|
- Private files: `storage/app/` (served via controller)
|
||||||
|
- Public files: `storage/app/public/` (served via `/storage/...` URL)
|
||||||
|
- S3 Config: `config/filesystems.php` → `disks.s3`
|
||||||
|
- S3 env vars:
|
||||||
|
- `AWS_ACCESS_KEY_ID`
|
||||||
|
- `AWS_SECRET_ACCESS_KEY`
|
||||||
|
- `AWS_DEFAULT_REGION`
|
||||||
|
- `AWS_BUCKET`
|
||||||
|
- `AWS_URL` (optional CDN)
|
||||||
|
- `AWS_ENDPOINT` (optional for S3-compatible services)
|
||||||
|
|
||||||
|
**Caching:**
|
||||||
|
- Providers: File (default), Redis, Memcached, DynamoDB
|
||||||
|
- Config: `config/cache.php`
|
||||||
|
- Default driver: `file` (suitable for single-server, can switch to Redis)
|
||||||
|
- Used for: `settings()` helper caching, query result caching
|
||||||
|
- Cache prefix: `laravel_cache_` (configurable via `CACHE_PREFIX` env)
|
||||||
|
- Settings cache: Automatically cleared on `SystemSetting` model mutation
|
||||||
|
|
||||||
|
**Session Storage:**
|
||||||
|
- Providers: File (default), Database, Redis
|
||||||
|
- Config: `config/session.php`
|
||||||
|
- Default driver: `file`
|
||||||
|
- Lifetime: 120 minutes (configurable via `SESSION_LIFETIME` env)
|
||||||
|
- CSRF tokens: Protected via Laravel middleware
|
||||||
|
|
||||||
|
## Authentication & Identity
|
||||||
|
|
||||||
|
**Auth Provider:**
|
||||||
|
- Type: Custom (Session-based)
|
||||||
|
- Implementation: Built-in Laravel authentication with `User` model
|
||||||
|
- Guard: `web` (session-based)
|
||||||
|
- Config: `config/auth.php`
|
||||||
|
- Provider: Eloquent user provider (`App\Models\User`)
|
||||||
|
|
||||||
|
**API Token Authentication:**
|
||||||
|
- Service: Laravel Sanctum
|
||||||
|
- Implementation: `config/sanctum.php`
|
||||||
|
- For: Public API endpoints (`/api/v1/*`)
|
||||||
|
- Stateful domains: `localhost`, `127.0.0.1`, custom domain (via `SANCTUM_STATEFUL_DOMAINS` env)
|
||||||
|
- Token prefix: Customizable via `SANCTUM_TOKEN_PREFIX` env
|
||||||
|
- Expiration: No default expiration (tokens live indefinitely unless custom set)
|
||||||
|
|
||||||
|
**Role-Based Access Control (RBAC):**
|
||||||
|
- Service: Spatie Laravel Permission
|
||||||
|
- Package: `spatie/laravel-permission@^6.23`
|
||||||
|
- Config: `config/permission.php`
|
||||||
|
- Models: `Spatie\Permission\Models\Role`, `Spatie\Permission\Models\Permission`
|
||||||
|
- Tables:
|
||||||
|
- `roles` - Available roles
|
||||||
|
- `permissions` - Available permissions
|
||||||
|
- `model_has_roles` - User-to-role assignments
|
||||||
|
- `model_has_permissions` - User-to-permission direct assignments
|
||||||
|
- `role_has_permissions` - Role-to-permission assignments
|
||||||
|
- Core roles (seeded in `database/seeders/`):
|
||||||
|
- `admin` - Full system access
|
||||||
|
- `finance_requester` - Submit finance documents
|
||||||
|
- `finance_cashier` - Tier 1 approvals (small amounts)
|
||||||
|
- `finance_accountant` - Tier 2 approvals (medium amounts) and ledger recording
|
||||||
|
- `finance_chair` - Tier 2 approvals (medium amounts)
|
||||||
|
- `finance_board_member` - Tier 3 approvals (large amounts)
|
||||||
|
- `secretary_general` - CMS management, member management
|
||||||
|
- `membership_manager` - Member lifecycle management
|
||||||
|
|
||||||
|
**Identity/National ID Encryption:**
|
||||||
|
- Encryption: AES-256 via Laravel's encryption
|
||||||
|
- Fields in `members` table:
|
||||||
|
- `national_id_encrypted` - Stores encrypted national ID
|
||||||
|
- `national_id_hash` - SHA256 hash for searching (indexed)
|
||||||
|
- `national_id` - Virtual accessor (decrypts on read)
|
||||||
|
- Location: `app/Models/Member.php`
|
||||||
|
|
||||||
|
## Webhooks & Callbacks
|
||||||
|
|
||||||
|
**Incoming Webhooks:**
|
||||||
|
- Site revalidation endpoint: `/api/revalidate` (public, requires valid token)
|
||||||
|
- Token: `NEXTJS_REVALIDATE_TOKEN` env var
|
||||||
|
- Method: `POST`
|
||||||
|
- Payload: `{ type: 'article'|'page'|'document', slug?: string }`
|
||||||
|
|
||||||
|
**Outgoing Webhooks:**
|
||||||
|
- Next.js site revalidation: `POST {NEXTJS_REVALIDATE_URL}`
|
||||||
|
- URL: `NEXTJS_REVALIDATE_TOKEN` env var
|
||||||
|
- Token header: `x-revalidate-token`
|
||||||
|
- Payload: `{ type: 'article'|'page'|'document', slug?: string }`
|
||||||
|
- Triggered on: Article/Page/Document create/update/delete
|
||||||
|
- Implementation: `app/Services/SiteRevalidationService.php`
|
||||||
|
- Timeout: 5 seconds, failures logged but do not block request
|
||||||
|
|
||||||
|
## Monitoring & Observability
|
||||||
|
|
||||||
|
**Error Tracking:**
|
||||||
|
- Service: Spatie Ignition (error page companion)
|
||||||
|
- Package: `spatie/laravel-ignition@^2.0`
|
||||||
|
- Used in: Development and error page debugging
|
||||||
|
- Config: `config/app.php` → providers
|
||||||
|
|
||||||
|
**Logging:**
|
||||||
|
- Framework: Monolog (via Laravel)
|
||||||
|
- Config: `config/logging.php`
|
||||||
|
- Default channel: `stack` (multi-handler)
|
||||||
|
- Channels available:
|
||||||
|
- `single` - Single log file: `storage/logs/laravel.log`
|
||||||
|
- `daily` - Rotate daily, keep 14 days
|
||||||
|
- `slack` - Send logs to Slack webhook (env: `LOG_SLACK_WEBHOOK_URL`)
|
||||||
|
- `papertrail` - Syslog UDP to Papertrail (env: `PAPERTRAIL_URL`, `PAPERTRAIL_PORT`)
|
||||||
|
- `stderr` - Output to stderr
|
||||||
|
- `syslog` - System syslog
|
||||||
|
- `errorlog` - PHP error_log
|
||||||
|
- `log` - File channel with custom naming
|
||||||
|
- `array` - In-memory (testing only)
|
||||||
|
- Log level: `debug` (default, configurable via `LOG_LEVEL` env)
|
||||||
|
- Deprecations channel: Null by default (can redirect via `LOG_DEPRECATIONS_CHANNEL` env)
|
||||||
|
|
||||||
|
**Audit Logging:**
|
||||||
|
- Service: Custom audit logger
|
||||||
|
- Implementation: `app/Support/AuditLogger.php`
|
||||||
|
- Usage: `AuditLogger::log($action, $auditable, $metadata)`
|
||||||
|
- Tracked: Model mutations (create/update/delete) for compliance
|
||||||
|
- Storage: Appended to audit log entries
|
||||||
|
|
||||||
|
## CI/CD & Deployment
|
||||||
|
|
||||||
|
**Hosting:**
|
||||||
|
- Platform: Dedicated servers, Docker (Laravel Sail), or cloud (AWS, etc.)
|
||||||
|
- Port: API typically runs on port 8001 (not 8000 - that's often occupied)
|
||||||
|
|
||||||
|
**Build Process:**
|
||||||
|
- Frontend assets: `npm run build` (Vite compilation)
|
||||||
|
- Backend: `composer install` (or `composer install --no-dev` for production)
|
||||||
|
- Migrations: `php artisan migrate --force`
|
||||||
|
- Cache: `php artisan config:cache`, `php artisan view:cache`
|
||||||
|
|
||||||
|
**Code Quality (Pre-commit):**
|
||||||
|
- Linter: Laravel Pint (PSR-12)
|
||||||
|
- Run: `./vendor/bin/pint`
|
||||||
|
- Fixes style issues automatically
|
||||||
|
- Static analysis: PHPStan
|
||||||
|
- Run: `./vendor/bin/phpstan analyse`
|
||||||
|
- Config: `phpstan.neon` (if present)
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
- Unit & Feature Tests: PHPUnit
|
||||||
|
- Run: `php artisan test`
|
||||||
|
- Run specific: `php artisan test --filter=ClassName`
|
||||||
|
- Config: `phpunit.xml`
|
||||||
|
- Browser/E2E Tests: Laravel Dusk
|
||||||
|
- Run: `php artisan dusk`
|
||||||
|
- Uses Chrome/Chromium driver
|
||||||
|
|
||||||
|
## Environment Configuration
|
||||||
|
|
||||||
|
**Required Environment Variables:**
|
||||||
|
|
||||||
|
Core:
|
||||||
|
- `APP_NAME` - Application name
|
||||||
|
- `APP_ENV` - Environment: `local`, `production`
|
||||||
|
- `APP_DEBUG` - Boolean: enable/disable debug mode
|
||||||
|
- `APP_KEY` - Base64 encryption key (auto-generated)
|
||||||
|
- `APP_URL` - Full URL: `https://member.usher.org.tw` (production)
|
||||||
|
|
||||||
|
Database:
|
||||||
|
- `DB_CONNECTION` - `mysql` (default) or `sqlite`
|
||||||
|
- `DB_HOST` - Database server
|
||||||
|
- `DB_PORT` - Port (3306 for MySQL)
|
||||||
|
- `DB_DATABASE` - Database name
|
||||||
|
- `DB_USERNAME` - Username
|
||||||
|
- `DB_PASSWORD` - Password
|
||||||
|
|
||||||
|
Mail:
|
||||||
|
- `MAIL_MAILER` - `smtp` (default), `mailgun`, `postmark`, `ses`, `log`
|
||||||
|
- `MAIL_HOST` - SMTP host
|
||||||
|
- `MAIL_PORT` - SMTP port (587 typical)
|
||||||
|
- `MAIL_USERNAME` - SMTP username
|
||||||
|
- `MAIL_PASSWORD` - SMTP password
|
||||||
|
- `MAIL_ENCRYPTION` - `tls`, `ssl`, or null
|
||||||
|
- `MAIL_FROM_ADDRESS` - From email address
|
||||||
|
- `MAIL_FROM_NAME` - From name
|
||||||
|
|
||||||
|
Caching & Queuing:
|
||||||
|
- `CACHE_DRIVER` - `file` (default), `redis`, `memcached`, `dynamodb`
|
||||||
|
- `QUEUE_CONNECTION` - `sync` (default), `database`, `redis`, `sqs`
|
||||||
|
- `SESSION_DRIVER` - `file` (default), `database`, `redis`
|
||||||
|
|
||||||
|
Feature Toggles:
|
||||||
|
- `REGISTRATION_ENABLED` - Boolean: allow public registration (`false` by default)
|
||||||
|
|
||||||
|
Frontend Integration:
|
||||||
|
- `NEXTJS_REVALIDATE_URL` - Full URL to Next.js revalidation endpoint
|
||||||
|
- `NEXTJS_REVALIDATE_TOKEN` - Bearer token for revalidation requests
|
||||||
|
- `NEXTJS_PUBLIC_PATH` - Optional: path to Next.js `public/` directory (for local asset sync)
|
||||||
|
|
||||||
|
**Secrets Location:**
|
||||||
|
- Method: Environment variables (`.env` file, never committed)
|
||||||
|
- Production: Use hosted secrets manager or CI/CD environment variables
|
||||||
|
- Never commit: Passwords, API keys, encryption keys, tokens
|
||||||
|
|
||||||
|
## Optional: Redis
|
||||||
|
|
||||||
|
**If using Redis for caching/sessions/queue:**
|
||||||
|
- Config: `config/database.php` → `redis`
|
||||||
|
- Env vars:
|
||||||
|
- `REDIS_HOST` - `127.0.0.1` (default)
|
||||||
|
- `REDIS_PORT` - `6379` (default)
|
||||||
|
- `REDIS_PASSWORD` - Password (null by default)
|
||||||
|
- `REDIS_CLIENT` - `phpredis` (default)
|
||||||
|
- `REDIS_CLUSTER` - `redis` (default)
|
||||||
|
- Databases:
|
||||||
|
- Default: 0 (primary connection)
|
||||||
|
- Cache: 1 (separate database for cache keys)
|
||||||
|
|
||||||
|
## Optional: AWS Integration
|
||||||
|
|
||||||
|
**If using AWS S3 for file storage:**
|
||||||
|
- Config: `config/filesystems.php` → `disks.s3`
|
||||||
|
- Env vars: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_DEFAULT_REGION`, `AWS_BUCKET`
|
||||||
|
|
||||||
|
**If using AWS SES for email:**
|
||||||
|
- Config: `config/mail.php` → `mailers.ses`
|
||||||
|
- Env vars: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_DEFAULT_REGION`
|
||||||
|
|
||||||
|
**If using AWS SQS for queue:**
|
||||||
|
- Config: `config/queue.php` → `connections.sqs`
|
||||||
|
- Env vars: AWS credentials + `SQS_PREFIX`, `SQS_QUEUE`, `SQS_SUFFIX`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Integration audit: 2026-02-13*
|
||||||
223
.planning/codebase/STACK.md
Normal file
223
.planning/codebase/STACK.md
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
# Technology Stack
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-02-13
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
|
||||||
|
**Primary:**
|
||||||
|
- PHP 8.1+ - Backend framework, ORM, business logic
|
||||||
|
|
||||||
|
**Secondary:**
|
||||||
|
- TypeScript - Not used (JavaScript tooling only)
|
||||||
|
- JavaScript - Frontend interactivity via Alpine.js and Vite build
|
||||||
|
|
||||||
|
## Runtime
|
||||||
|
|
||||||
|
**Environment:**
|
||||||
|
- Laravel 10 (minimum 10.10)
|
||||||
|
- PHP 8.1+ (configured in `composer.json`)
|
||||||
|
|
||||||
|
**Package Managers:**
|
||||||
|
- Composer (PHP) - Dependency management for backend
|
||||||
|
- Lockfile: `composer.lock` (present)
|
||||||
|
- npm (Node.js) - Frontend asset builds and development
|
||||||
|
- Lockfile: `package-lock.json` (present)
|
||||||
|
|
||||||
|
## Frameworks
|
||||||
|
|
||||||
|
**Core Backend:**
|
||||||
|
- Laravel 10 - Full-stack web framework for routing, models, migrations, services
|
||||||
|
- Location: `app/`, `routes/`, `config/`, `database/`
|
||||||
|
|
||||||
|
**Frontend Templating:**
|
||||||
|
- Blade (Laravel templating engine) - Server-side template rendering
|
||||||
|
- Location: `resources/views/`
|
||||||
|
|
||||||
|
**Frontend Interactivity:**
|
||||||
|
- Alpine.js 3.4 - Lightweight reactive JavaScript for form handling and DOM manipulation
|
||||||
|
- Package: `alpinejs@^3.4.2` in `package.json`
|
||||||
|
|
||||||
|
**Build & Asset Pipeline:**
|
||||||
|
- Vite 5 - Fast build tool for compiling CSS/JS
|
||||||
|
- Package: `vite@^5.0.0`
|
||||||
|
- Config: `vite.config.js` (Laravel Vite plugin configured)
|
||||||
|
- Laravel Vite Plugin - Integration between Laravel and Vite
|
||||||
|
- Package: `laravel-vite-plugin@^1.0.0`
|
||||||
|
|
||||||
|
**Styling:**
|
||||||
|
- Tailwind CSS 3.1 - Utility-first CSS framework
|
||||||
|
- Package: `tailwindcss@^3.1.0`
|
||||||
|
- Dark mode: `darkMode: 'class'` configured in tailwind config
|
||||||
|
- Forms plugin: `@tailwindcss/forms@^0.5.2` for styled form elements
|
||||||
|
- PostCSS 8.4.31 - CSS processing
|
||||||
|
- Autoprefixer 10.4.2 - Browser vendor prefixes
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
- PHPUnit 10.1 - PHP unit testing framework
|
||||||
|
- Config: `phpunit.xml`
|
||||||
|
- Test directory: `tests/`
|
||||||
|
- Laravel Dusk 8.3 - Browser testing framework for end-to-end tests
|
||||||
|
- Run: `php artisan dusk`
|
||||||
|
- Mockery 1.4.4 - PHP mocking library for tests
|
||||||
|
|
||||||
|
**Development Quality:**
|
||||||
|
- Laravel Pint 1.0 - Code style formatter (PSR-12)
|
||||||
|
- Laravel Tinker 2.8 - Interactive PHP REPL
|
||||||
|
- Laravel Breeze 1.29 - Lightweight authentication scaffolding
|
||||||
|
- FakerPHP 1.9.1 - Fake data generation for seeders
|
||||||
|
- Spatie Ignition 2.0 - Error page debugging companion
|
||||||
|
- Collision 7.0 - Error page styling
|
||||||
|
|
||||||
|
**DevOps/Deployment:**
|
||||||
|
- Laravel Sail 1.18 - Docker-based local development environment (optional)
|
||||||
|
|
||||||
|
## Key Dependencies
|
||||||
|
|
||||||
|
**Critical Business Logic:**
|
||||||
|
- `spatie/laravel-permission` 6.23 - RBAC role and permission system
|
||||||
|
- Used for: Finance approvals, user role management
|
||||||
|
- Config: `config/permission.php`
|
||||||
|
- Tables: `roles`, `permissions`, `model_has_roles`, `model_has_permissions`, `role_has_permissions`
|
||||||
|
|
||||||
|
- `barryvdh/laravel-dompdf` 3.1 - PDF generation for finance reports
|
||||||
|
- Used for: Document export, compliance reports
|
||||||
|
- Provider: `Barryvdh\DomPDF\ServiceProvider`
|
||||||
|
|
||||||
|
- `maatwebsite/excel` 3.1 - Excel file import/export
|
||||||
|
- Used for: Member data imports, financial report exports
|
||||||
|
- Laravel 10 compatible version
|
||||||
|
|
||||||
|
**HTTP & API Integration:**
|
||||||
|
- `guzzlehttp/guzzle` 7.2 - HTTP client for external requests
|
||||||
|
- Used in: `SiteRevalidationService` for Next.js webhook calls
|
||||||
|
- Location: `app/Services/SiteRevalidationService.php`
|
||||||
|
|
||||||
|
**QR Code Generation:**
|
||||||
|
- `simplesoftwareio/simple-qrcode` 4.2 - QR code generation library
|
||||||
|
- Used for: Member ID verification, document tracking
|
||||||
|
|
||||||
|
**Authentication:**
|
||||||
|
- `laravel/sanctum` 3.3 - API token authentication and CSRF protection
|
||||||
|
- Used for: Public API endpoints (`/api/v1/*`)
|
||||||
|
- Config: `config/sanctum.php`
|
||||||
|
- Stateful domains: `localhost`, `127.0.0.1` (customizable via env)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
**Environment Configuration:**
|
||||||
|
- `.env.example` - Template for environment variables
|
||||||
|
- Required variables (non-secret):
|
||||||
|
- `APP_NAME`, `APP_ENV`, `APP_DEBUG`, `APP_URL` - Application identity
|
||||||
|
- `DB_CONNECTION`, `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD` - Database
|
||||||
|
- `MAIL_MAILER`, `MAIL_HOST`, `MAIL_PORT`, `MAIL_FROM_ADDRESS` - Email
|
||||||
|
- `CACHE_DRIVER`, `QUEUE_CONNECTION`, `SESSION_DRIVER` - Caching/queueing
|
||||||
|
- `REGISTRATION_ENABLED` - Toggle public registration on/off
|
||||||
|
- `NEXTJS_REVALIDATE_URL`, `NEXTJS_REVALIDATE_TOKEN` - Webhook to frontend
|
||||||
|
- `NEXTJS_PUBLIC_PATH` - Optional: Local Next.js repo for asset syncing
|
||||||
|
|
||||||
|
**Core Configs:**
|
||||||
|
- `config/app.php` - Timezone: `Asia/Taipei`, Locale: `zh_TW`, Cipher: `AES-256-CBC`
|
||||||
|
- `config/database.php` - Database connection options (MySQL primary, SQLite dev)
|
||||||
|
- `config/auth.php` - Authentication guards (session-based), public registration toggle
|
||||||
|
- `config/mail.php` - SMTP, Mailgun, Postmark, SES support
|
||||||
|
- `config/accounting.php` - Account codes, amount tier thresholds, currency (TWD)
|
||||||
|
- `config/filesystems.php` - Local, public, private, and S3 disk definitions
|
||||||
|
- `config/cache.php` - File (default), Redis, Memcached, DynamoDB support
|
||||||
|
- `config/queue.php` - Sync (default), database, Redis, SQS, Beanstalkd support
|
||||||
|
- `config/services.php` - Third-party service credentials (Mailgun, Postmark, AWS SES, Next.js webhooks)
|
||||||
|
- `config/logging.php` - Monolog channels (stack, single, daily, Slack, Papertrail)
|
||||||
|
- `config/cors.php` - CORS policy for API endpoints
|
||||||
|
- `config/session.php` - Session driver and lifetime (120 minutes default)
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
**Primary (Production):**
|
||||||
|
- MySQL 5.7+ (configured in `config/database.php`)
|
||||||
|
- Character set: `utf8mb4`, Collation: `utf8mb4_unicode_ci`
|
||||||
|
- Strict mode enabled
|
||||||
|
|
||||||
|
**Development:**
|
||||||
|
- SQLite supported as alternative (`DB_CONNECTION=sqlite`)
|
||||||
|
- Location: `database/database.sqlite`
|
||||||
|
|
||||||
|
**Migrations:**
|
||||||
|
- Directory: `database/migrations/`
|
||||||
|
- Run: `php artisan migrate`
|
||||||
|
- Fresh reset: `php artisan migrate:fresh --seed`
|
||||||
|
|
||||||
|
## Caching & Session
|
||||||
|
|
||||||
|
**Cache (Default: File):**
|
||||||
|
- Driver: `file` (can be switched to Redis, Memcached, DynamoDB)
|
||||||
|
- Location: `storage/framework/cache/data/`
|
||||||
|
- Used for: Settings caching (`settings()` helper), query result caching
|
||||||
|
|
||||||
|
**Sessions (Default: File):**
|
||||||
|
- Driver: `file` (can be switched to database, Redis, etc.)
|
||||||
|
- Lifetime: 120 minutes
|
||||||
|
- Storage: `storage/framework/sessions/`
|
||||||
|
|
||||||
|
**Queue (Default: Sync):**
|
||||||
|
- Driver: `sync` (processes jobs immediately in production, can use `database`/`redis`)
|
||||||
|
- Used for: Email sending, bulk operations
|
||||||
|
- Failed jobs table: `failed_jobs`
|
||||||
|
|
||||||
|
## Email
|
||||||
|
|
||||||
|
**Mailers Supported:**
|
||||||
|
- SMTP (default) - Mailgun, custom SMTP servers
|
||||||
|
- Mailgun API
|
||||||
|
- Postmark API
|
||||||
|
- AWS SES
|
||||||
|
- Log (development)
|
||||||
|
- Failover chains (SMTP → Log)
|
||||||
|
|
||||||
|
**Dev Default:**
|
||||||
|
- Mailpit (mock SMTP server) on `localhost:1025`
|
||||||
|
- Configured in `.env.example` for local testing
|
||||||
|
|
||||||
|
**Mail Queue:**
|
||||||
|
- All transactional emails use `Mail::...->queue()` for async sending
|
||||||
|
- Location: Email classes in `app/Mail/`
|
||||||
|
|
||||||
|
## File Storage
|
||||||
|
|
||||||
|
**Private Files:**
|
||||||
|
- Disk: `private` (stores in `storage/app/`)
|
||||||
|
- Access: Served via controller responses only (no direct URL)
|
||||||
|
- Used for: Encrypted documents, sensitive uploads
|
||||||
|
|
||||||
|
**Public Files:**
|
||||||
|
- Disk: `public` (stores in `storage/app/public/`)
|
||||||
|
- URL: Accessible via `/storage/...` after running `php artisan storage:link`
|
||||||
|
- Used for: Admin-uploaded images, article attachments
|
||||||
|
|
||||||
|
**Optional S3:**
|
||||||
|
- Disk: `s3` (configured for AWS S3)
|
||||||
|
- Requires: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_DEFAULT_REGION`, `AWS_BUCKET`
|
||||||
|
|
||||||
|
**Next.js Asset Sync (Optional):**
|
||||||
|
- Location: `config/services.php` → `nextjs.public_path`
|
||||||
|
- Feature: Auto-copy uploaded images to Next.js `public/` directory
|
||||||
|
- Git integration: Optional auto-commit and push of assets
|
||||||
|
- Used for: Serving static images from static Next.js build
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
**Hosting Targets:**
|
||||||
|
- Dedicated servers (traditional deployment with PHP-FPM + Nginx)
|
||||||
|
- Docker (via Laravel Sail)
|
||||||
|
- Vercel/Netlify (headless API mode)
|
||||||
|
|
||||||
|
**Production Considerations:**
|
||||||
|
- Database: Migrate to MySQL 8.0+
|
||||||
|
- Cache: Switch from `file` to Redis for multi-server setups
|
||||||
|
- Queue: Switch from `sync` to `database` or `redis`
|
||||||
|
- Session: Switch from `file` to `database` or `redis`
|
||||||
|
- Mail: Configure Mailgun/SES for production email
|
||||||
|
- Storage: Use S3 for file uploads instead of local disk
|
||||||
|
- Assets: Compile with `npm run build` before deployment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Stack analysis: 2026-02-13*
|
||||||
362
.planning/codebase/STRUCTURE.md
Normal file
362
.planning/codebase/STRUCTURE.md
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
# Codebase Structure
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-02-13
|
||||||
|
|
||||||
|
## Directory Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
usher-manage-stack/
|
||||||
|
├── app/ # Application code
|
||||||
|
│ ├── Console/Commands/ # Custom Artisan commands
|
||||||
|
│ ├── Exceptions/ # Exception handlers
|
||||||
|
│ ├── Http/
|
||||||
|
│ │ ├── Controllers/ # Web controllers split by domain
|
||||||
|
│ │ │ ├── Admin/ # Admin panel controllers (10 controllers)
|
||||||
|
│ │ │ ├── Api/ # API controllers (4 controllers)
|
||||||
|
│ │ │ ├── Auth/ # Auth controllers
|
||||||
|
│ │ │ ├── *Controller.php # Main route controllers
|
||||||
|
│ │ ├── Middleware/ # HTTP middleware (11 total)
|
||||||
|
│ │ ├── Requests/ # Form Request validation classes
|
||||||
|
│ │ └── Resources/ # API response resources
|
||||||
|
│ ├── Jobs/ # Queued jobs
|
||||||
|
│ ├── Mail/ # Mail classes (notifications)
|
||||||
|
│ ├── Models/ # Eloquent models (36 models)
|
||||||
|
│ ├── Observers/ # Model observers
|
||||||
|
│ ├── Providers/ # Service providers
|
||||||
|
│ ├── Services/ # Business logic services (7 services)
|
||||||
|
│ ├── Support/ # Helper classes
|
||||||
|
│ ├── Traits/ # Shared model traits (2 traits)
|
||||||
|
│ ├── View/Components/ # Blade components
|
||||||
|
│ ├── helpers.php # Global helper functions
|
||||||
|
│
|
||||||
|
├── bootstrap/ # Bootstrap files
|
||||||
|
├── config/ # Configuration files
|
||||||
|
├── database/
|
||||||
|
│ ├── migrations/ # Database migrations
|
||||||
|
│ ├── seeders/ # Database seeders
|
||||||
|
│ ├── factories/ # Model factories for testing
|
||||||
|
│ └── schema/ # Schema dump
|
||||||
|
│
|
||||||
|
├── resources/
|
||||||
|
│ ├── css/ # Tailwind CSS
|
||||||
|
│ ├── views/ # Blade templates
|
||||||
|
│ │ ├── admin/ # Admin panel views (20+ subdirectories)
|
||||||
|
│ │ ├── auth/ # Auth views (login, register, etc.)
|
||||||
|
│ │ ├── components/ # Reusable components
|
||||||
|
│ │ ├── emails/ # Email templates
|
||||||
|
│ │ ├── layouts/ # Layout files
|
||||||
|
│ │ ├── member/ # Member-specific views
|
||||||
|
│ │ ├── public/ # Public-facing views
|
||||||
|
│ │ └── profile/ # Profile views
|
||||||
|
│
|
||||||
|
├── routes/
|
||||||
|
│ ├── web.php # Web routes (393 lines, 200+ named routes)
|
||||||
|
│ ├── api.php # API routes (v1)
|
||||||
|
│ ├── auth.php # Auth routes
|
||||||
|
│ ├── console.php # Console commands
|
||||||
|
│ └── channels.php # Broadcasting channels
|
||||||
|
│
|
||||||
|
├── storage/
|
||||||
|
│ ├── app/ # File uploads (private)
|
||||||
|
│ ├── logs/ # Application logs
|
||||||
|
│ └── framework/ # Framework files
|
||||||
|
│
|
||||||
|
├── tests/ # Test suite
|
||||||
|
├── .env.example # Environment template
|
||||||
|
├── CLAUDE.md # Project-specific instructions
|
||||||
|
├── composer.json # PHP dependencies
|
||||||
|
└── artisan # Artisan CLI entry point
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Purposes
|
||||||
|
|
||||||
|
**app/Models:**
|
||||||
|
- Purpose: Eloquent models representing database tables
|
||||||
|
- Contains: 36 models with relationships, scopes, accessors
|
||||||
|
- Key files:
|
||||||
|
- `Member.php` — Member lifecycle (status, disability, identity type)
|
||||||
|
- `FinanceDocument.php` — Finance approval workflow (27+ status constants)
|
||||||
|
- `User.php` — User authentication and roles
|
||||||
|
- `MembershipPayment.php` — Payment verification workflow
|
||||||
|
- `Document.php` — Document library with versioning
|
||||||
|
- `Article.php`, `Page.php` — CMS content
|
||||||
|
- `Issue.php` — Issue tracker with comments, attachments, time logs
|
||||||
|
- `AccountingEntry.php` — Double-entry bookkeeping records
|
||||||
|
|
||||||
|
**app/Http/Controllers:**
|
||||||
|
- Purpose: Handle HTTP requests and coordinate responses
|
||||||
|
- Contains: 25+ controller classes
|
||||||
|
- Structure:
|
||||||
|
- `Admin/` — 10 controllers for admin features (articles, documents, pages, etc.)
|
||||||
|
- `Api/` — 4 controllers for REST API endpoints
|
||||||
|
- Root controllers for public and authenticated routes
|
||||||
|
- Pattern: Inject dependencies, call services, return view/response
|
||||||
|
|
||||||
|
**app/Http/Controllers/Admin:**
|
||||||
|
- `DocumentController.php` — Document CRUD with version control
|
||||||
|
- `ArticleController.php` — Article CRUD with publish/archive
|
||||||
|
- `PageController.php` — Page CRUD with publish
|
||||||
|
- `AnnouncementController.php` — Announcements with pinning
|
||||||
|
- `SystemSettingsController.php` — System settings UI
|
||||||
|
- `GeneralLedgerController.php`, `TrialBalanceController.php` — Accounting reports
|
||||||
|
|
||||||
|
**app/Services:**
|
||||||
|
- Purpose: Encapsulate complex business logic
|
||||||
|
- Files:
|
||||||
|
- `FinanceDocumentApprovalService.php` — Multi-tier approval orchestration
|
||||||
|
- `MembershipFeeCalculator.php` — Fee calculation with disability discount
|
||||||
|
- `SettingsService.php` — System settings caching/retrieval
|
||||||
|
- `PaymentVerificationService.php` — Payment workflow coordination
|
||||||
|
- `SiteRevalidationService.php` — Next.js static site rebuild webhook
|
||||||
|
- `SiteAssetSyncService.php`, `NextjsRepoSyncService.php` — Frontend sync
|
||||||
|
|
||||||
|
**app/Traits:**
|
||||||
|
- Purpose: Shared behavior across multiple models
|
||||||
|
- `HasApprovalWorkflow.php` — Multi-tier approval methods (6 methods)
|
||||||
|
- `HasAccountingEntries.php` — Double-entry bookkeeping (8 methods)
|
||||||
|
- Usage: Models add `use TraitName;` to inherit behavior
|
||||||
|
|
||||||
|
**app/Http/Requests:**
|
||||||
|
- Purpose: Form request validation classes (implicit validation in controllers)
|
||||||
|
- Files:
|
||||||
|
- `StoreMemberRequest.php` — Member creation rules
|
||||||
|
- `UpdateMemberRequest.php` — Member update rules
|
||||||
|
- `StoreFinanceDocumentRequest.php` — Finance document rules
|
||||||
|
- `StoreIssueRequest.php` — Issue creation rules
|
||||||
|
- `ProfileUpdateRequest.php` — User profile rules
|
||||||
|
- Pattern: Define `rules()` and `messages()` methods; controller receives validated data
|
||||||
|
|
||||||
|
**app/Http/Middleware:**
|
||||||
|
- `EnsureUserIsAdmin.php` — Admin access check (admin role OR any permission)
|
||||||
|
- `Authenticate.php` — Session authentication
|
||||||
|
- `CheckPaidMembership.php` — Membership status validation
|
||||||
|
- Standard middleware: CSRF, encryption, trusted proxies
|
||||||
|
|
||||||
|
**app/Support:**
|
||||||
|
- `AuditLogger.php` — Static class for centralized audit logging
|
||||||
|
- `DownloadFile.php` — File download handler with cleanup
|
||||||
|
- Usage: `AuditLogger::log('action_name', $model, $metadata)`
|
||||||
|
|
||||||
|
**resources/views:**
|
||||||
|
- Purpose: Blade templates rendering HTML
|
||||||
|
- Structure:
|
||||||
|
- `admin/` — 20+ directories for admin features
|
||||||
|
- `admin/finance/` — Finance documents
|
||||||
|
- `admin/members/` — Member management
|
||||||
|
- `admin/issues/` — Issue tracker
|
||||||
|
- `admin/articles/`, `admin/pages/` — CMS
|
||||||
|
- `admin/announcements/`, `admin/documents/` — Announcements and document library
|
||||||
|
- `auth/` — Login, register, password reset
|
||||||
|
- `member/` — Member-facing views
|
||||||
|
- `public/` — Public-facing views
|
||||||
|
- `layouts/` — Layout templates (app.blade.php, guest.blade.php)
|
||||||
|
- `components/` — Reusable Blade components
|
||||||
|
|
||||||
|
**config/:**
|
||||||
|
- `accounting.php` — Account codes, amount tiers, chart of accounts
|
||||||
|
- `auth.php` — Authentication guards, registration enabled flag
|
||||||
|
- `permission.php` — Spatie permission defaults
|
||||||
|
- `services.php` — External service configuration (Nextjs revalidate URL)
|
||||||
|
- `app.php`, `database.php`, `cache.php`, etc. — Standard Laravel configs
|
||||||
|
|
||||||
|
**routes/:**
|
||||||
|
- `web.php` — All 200+ web routes
|
||||||
|
- Public: `/`, `/documents`, `/register/member` (if enabled), `/beta/bug-report`
|
||||||
|
- Auth: `/dashboard`, `/my-membership`, `/member/submit-payment`, `/profile`
|
||||||
|
- Admin: `/admin/*` with group prefix and `admin` middleware
|
||||||
|
- `api.php` — API v1 routes
|
||||||
|
- `/api/v1/articles`, `/api/v1/pages`, `/api/v1/homepage`, `/api/v1/public-documents`
|
||||||
|
- Authentication: Sanctum (optional)
|
||||||
|
- `auth.php` — Login, register, password reset routes
|
||||||
|
|
||||||
|
## Key File Locations
|
||||||
|
|
||||||
|
**Entry Points:**
|
||||||
|
- `public/index.php` — Application entry point
|
||||||
|
- `app/Providers/RouteServiceProvider.php` — Route registration
|
||||||
|
- `routes/web.php` — All web routes
|
||||||
|
- `routes/api.php` — API routes
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
- `config/accounting.php` — Finance tier thresholds, account codes
|
||||||
|
- `config/auth.php` — Registration enabled flag, authentication settings
|
||||||
|
- `config/services.php` — External service URLs and tokens
|
||||||
|
- `.env` file (not committed) — Environment variables
|
||||||
|
|
||||||
|
**Core Logic:**
|
||||||
|
- `app/Models/FinanceDocument.php` — Finance workflow logic (approval, disbursement, recording)
|
||||||
|
- `app/Models/Member.php` — Member lifecycle logic (status checks, fees)
|
||||||
|
- `app/Services/FinanceDocumentApprovalService.php` — Approval orchestration
|
||||||
|
- `app/Traits/HasApprovalWorkflow.php` — Reusable approval behavior
|
||||||
|
- `app/Traits/HasAccountingEntries.php` — Reusable accounting behavior
|
||||||
|
|
||||||
|
**Business Rules:**
|
||||||
|
- `app/Services/MembershipFeeCalculator.php` — Fee calculation
|
||||||
|
- `app/Services/PaymentVerificationService.php` — Payment workflow
|
||||||
|
- `app/Http/Controllers/FinanceDocumentController.php` — Finance CRUD and workflow actions
|
||||||
|
- `app/Http/Controllers/AdminMemberController.php` — Member CRUD and lifecycle
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
- `tests/` — Test suite (PHPUnit)
|
||||||
|
- Database tests use `RefreshDatabase` trait
|
||||||
|
- Test accounts in seeders: `admin@test.com`, `requester@test.com`, `cashier@test.com`, etc.
|
||||||
|
|
||||||
|
**Database:**
|
||||||
|
- `database/migrations/` — 20+ migrations
|
||||||
|
- `database/seeders/` — RoleSeeder, ChartOfAccountSeeder, TestDataSeeder, FinancialWorkflowTestDataSeeder
|
||||||
|
- `database/factories/` — Model factories for testing
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Controllers: `PluralCamelCase + Controller.php` (e.g., `FinanceDocumentController.php`)
|
||||||
|
- Models: `SingularCamelCase.php` (e.g., `FinanceDocument.php`)
|
||||||
|
- Requests: `Action + ModelName + Request.php` (e.g., `StoreFinanceDocumentRequest.php`)
|
||||||
|
- Migrations: `YYYY_MM_DD_HHMMSS_action_table_name.php`
|
||||||
|
- Views: `snake_case.blade.php` (e.g., `create.blade.php`, `edit.blade.php`)
|
||||||
|
|
||||||
|
**Directories:**
|
||||||
|
- Controllers: Group by feature/domain (Admin/, Api/, Auth/)
|
||||||
|
- Views: Mirror controller structure (admin/, auth/, member/, public/)
|
||||||
|
- Models: Flat in app/Models/
|
||||||
|
- Services: Flat in app/Services/
|
||||||
|
- Migrations: Flat in database/migrations/
|
||||||
|
|
||||||
|
**Routes:**
|
||||||
|
- Named routes: `domain.action` (e.g., `admin.finance.index`, `member.payments.store`)
|
||||||
|
- Admin routes: Prefix `admin.` and grouped under `/admin` URL prefix
|
||||||
|
- Public routes: No prefix (e.g., `documents.index`, `register.member`)
|
||||||
|
|
||||||
|
**Database:**
|
||||||
|
- Tables: `snake_case` plural (e.g., `finance_documents`, `membership_payments`)
|
||||||
|
- Columns: `snake_case` (e.g., `submitted_by_user_id`, `approved_at`)
|
||||||
|
- Foreign keys: `model_id` singular (e.g., `member_id`, `user_id`)
|
||||||
|
- Timestamps: `created_at`, `updated_at` (auto-managed)
|
||||||
|
- Encrypted fields: `field_encrypted` and `field_hash` for search (e.g., `national_id_encrypted`, `national_id_hash`)
|
||||||
|
|
||||||
|
**Constants:**
|
||||||
|
- Status values: `ModelName::STATUS_STATE` all caps (e.g., `FinanceDocument::STATUS_PENDING`)
|
||||||
|
- Type values: `ModelName::TYPE_NAME` all caps (e.g., `Member::TYPE_INDIVIDUAL`)
|
||||||
|
- Amount tiers: `ModelName::AMOUNT_TIER_LEVEL` all caps (e.g., `FinanceDocument::AMOUNT_TIER_SMALL`)
|
||||||
|
|
||||||
|
## Where to Add New Code
|
||||||
|
|
||||||
|
**New Feature (CRUD):**
|
||||||
|
- Primary code:
|
||||||
|
- Model: `app/Models/NewModel.php` — Define relationships, scopes, accessors
|
||||||
|
- Controller: `app/Http/Controllers/Admin/NewModelController.php` or root controller
|
||||||
|
- Requests: `app/Http/Requests/Store/UpdateNewModelRequest.php`
|
||||||
|
- Views: `resources/views/admin/new-models/` with index/create/edit/show
|
||||||
|
- Routes: Add to `routes/web.php` within appropriate middleware group
|
||||||
|
|
||||||
|
- Example structure:
|
||||||
|
```php
|
||||||
|
// Model relationships
|
||||||
|
public function relationships()
|
||||||
|
|
||||||
|
// Form validation in FormRequest
|
||||||
|
public function rules()
|
||||||
|
|
||||||
|
// Controller action
|
||||||
|
public function store(StoreNewModelRequest $request)
|
||||||
|
|
||||||
|
// View rendering
|
||||||
|
return view('admin.new-models.create', ['data' => ...]);
|
||||||
|
```
|
||||||
|
|
||||||
|
**New Component/Module (Multi-Model Feature):**
|
||||||
|
- Implementation:
|
||||||
|
- Models: Multiple related models in `app/Models/`
|
||||||
|
- Service: `app/Services/NewModuleService.php` for complex logic
|
||||||
|
- Controller: `app/Http/Controllers/NewModuleController.php` or `Admin/NewModuleController.php`
|
||||||
|
- Views: `resources/views/admin/new-module/` or `resources/views/new-module/`
|
||||||
|
- Routes: Add feature routes to `routes/web.php`
|
||||||
|
- Migrations: Database tables in `database/migrations/`
|
||||||
|
|
||||||
|
- Example: Finance Documents (FinanceDocument → PaymentOrder → CashierLedgerEntry → AccountingEntry)
|
||||||
|
```php
|
||||||
|
// Service orchestrates workflow
|
||||||
|
$service->approveBySecretary($document, $user);
|
||||||
|
|
||||||
|
// Models track state
|
||||||
|
$document->status, $document->disbursement_status, $document->recording_status
|
||||||
|
```
|
||||||
|
|
||||||
|
**Utilities/Helpers:**
|
||||||
|
- Shared helpers: `app/Support/HelperClass.php` or method in `app/helpers.php`
|
||||||
|
- Example: `AuditLogger::log()`, `DownloadFile::download()`
|
||||||
|
- Global function: Add to `app/helpers.php` if used in multiple places
|
||||||
|
- Config values: Add to appropriate `config/` file
|
||||||
|
|
||||||
|
**New API Endpoint:**
|
||||||
|
- Controller: `app/Http/Controllers/Api/ResourceController.php`
|
||||||
|
- Resource: `app/Http/Resources/ResourceResource.php` for response formatting
|
||||||
|
- Routes: Add to `routes/api.php` under `prefix('v1')`
|
||||||
|
- Example:
|
||||||
|
```php
|
||||||
|
// routes/api.php
|
||||||
|
Route::get('/articles', [ArticleController::class, 'index']);
|
||||||
|
|
||||||
|
// app/Http/Controllers/Api/ArticleController.php
|
||||||
|
public function index() {
|
||||||
|
return ArticleResource::collection(Article::active()->get());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**New Blade Component:**
|
||||||
|
- File: `resources/views/components/component-name.blade.php`
|
||||||
|
- Class (if needed): `app/View/Components/ComponentName.php`
|
||||||
|
- Usage: `<x-component-name :data="$data" />`
|
||||||
|
|
||||||
|
**New Service:**
|
||||||
|
- File: `app/Services/NewService.php`
|
||||||
|
- Usage: Dependency injection in controllers or other services
|
||||||
|
- Pattern:
|
||||||
|
```php
|
||||||
|
// In controller
|
||||||
|
public function action(NewService $service) {
|
||||||
|
$result = $service->doSomething();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**New Trait (Shared Behavior):**
|
||||||
|
- File: `app/Traits/HasNewBehavior.php`
|
||||||
|
- Usage: `use HasNewBehavior;` in models
|
||||||
|
- Pattern: Define methods that multiple models need
|
||||||
|
|
||||||
|
## Special Directories
|
||||||
|
|
||||||
|
**storage/app/:**
|
||||||
|
- Purpose: Private file uploads
|
||||||
|
- Subdirectories: `finance-documents/`, `articles/`, `disability-certificates/`
|
||||||
|
- Generated: Yes
|
||||||
|
- Committed: No (in .gitignore)
|
||||||
|
- Access: `Storage::disk('local')->get($path)` or via authenticated controller
|
||||||
|
|
||||||
|
**database/schema/:**
|
||||||
|
- Purpose: Schema dump for reference
|
||||||
|
- Generated: By `php artisan schema:dump`
|
||||||
|
- Committed: Yes (read-only reference)
|
||||||
|
- Use: Check table structure without opening migrations
|
||||||
|
|
||||||
|
**resources/css/:**
|
||||||
|
- Purpose: Tailwind CSS source
|
||||||
|
- Style: Uses `@apply` for utility classes
|
||||||
|
- Config: `tailwind.config.js` with dark mode class strategy
|
||||||
|
- Build: `npm run build` compiles to `public/css/app.css`
|
||||||
|
|
||||||
|
**config/:**
|
||||||
|
- Purpose: Application configuration
|
||||||
|
- Committed: Yes (except `.env`)
|
||||||
|
- Environment-specific: Values read from `.env` via `env('KEY', 'default')`
|
||||||
|
- Access: `config('key.subkey')` in code
|
||||||
|
|
||||||
|
**tests/:**
|
||||||
|
- Purpose: PHPUnit test suite
|
||||||
|
- Structure: Mirror app structure (Unit/, Feature/)
|
||||||
|
- Pattern: `RefreshDatabase` trait for database reset, use test factories
|
||||||
|
- Run: `php artisan test`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Structure analysis: 2026-02-13*
|
||||||
429
.planning/codebase/TESTING.md
Normal file
429
.planning/codebase/TESTING.md
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
# Testing Patterns
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-02-13
|
||||||
|
|
||||||
|
## Test Framework
|
||||||
|
|
||||||
|
**Runner:**
|
||||||
|
- PHPUnit 10.1+ (configured in `composer.json`)
|
||||||
|
- Config file: `phpunit.xml`
|
||||||
|
|
||||||
|
**Assertion Library:**
|
||||||
|
- PHPUnit assertions (built-in)
|
||||||
|
- Laravel testing traits: `RefreshDatabase`, `WithFaker`, `DatabaseMigrations`
|
||||||
|
|
||||||
|
**Run Commands:**
|
||||||
|
```bash
|
||||||
|
php artisan test # Run all tests
|
||||||
|
php artisan test --filter=ClassName # Run specific test class
|
||||||
|
php artisan test --filter=test_method_name # Run specific test method
|
||||||
|
php artisan dusk # Run browser (Dusk) tests
|
||||||
|
```
|
||||||
|
|
||||||
|
**Coverage:**
|
||||||
|
```bash
|
||||||
|
php artisan test --coverage # Generate coverage report
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test File Organization
|
||||||
|
|
||||||
|
**Location:**
|
||||||
|
- Unit tests: `tests/Unit/`
|
||||||
|
- Feature tests: `tests/Feature/`
|
||||||
|
- Browser tests: `tests/Browser/`
|
||||||
|
- Shared test utilities: `tests/Traits/`
|
||||||
|
- Test base classes: `tests/TestCase.php`, `tests/DuskTestCase.php`
|
||||||
|
|
||||||
|
**Naming:**
|
||||||
|
- Test files: PascalCase + `Test.php` suffix (e.g., `FinanceDocumentTest.php`, `CashierLedgerWorkflowTest.php`)
|
||||||
|
- Test methods: `test_` prefix (e.g., `test_payment_belongs_to_member()`) OR `/** @test */` annotation
|
||||||
|
- Test trait files: PascalCase with context (e.g., `CreatesFinanceData.php`, `SeedsRolesAndPermissions.php`)
|
||||||
|
|
||||||
|
**Structure:**
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── Unit/ # Model logic, calculations, state checks
|
||||||
|
│ ├── FinanceDocumentTest.php
|
||||||
|
│ ├── MemberTest.php
|
||||||
|
│ ├── BudgetTest.php
|
||||||
|
│ └── MembershipPaymentTest.php
|
||||||
|
├── Feature/ # HTTP requests, workflows, integrations
|
||||||
|
│ ├── CashierLedgerWorkflowTest.php
|
||||||
|
│ ├── Auth/
|
||||||
|
│ ├── BankReconciliation/
|
||||||
|
│ └── ProfileTest.php
|
||||||
|
├── Browser/ # Full page interactions with Dusk
|
||||||
|
│ ├── MemberDashboardBrowserTest.php
|
||||||
|
│ ├── FinanceWorkflowBrowserTest.php
|
||||||
|
│ └── Pages/
|
||||||
|
├── Traits/ # Reusable test setup helpers
|
||||||
|
│ ├── SeedsRolesAndPermissions.php
|
||||||
|
│ ├── CreatesFinanceData.php
|
||||||
|
│ ├── CreatesMemberData.php
|
||||||
|
│ └── CreatesApplication.php
|
||||||
|
├── TestCase.php # Base test class with setup
|
||||||
|
└── DuskTestCase.php # Base for browser tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
**Suite Organization:**
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use App\Models\FinanceDocument;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class FinanceDocumentTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_determines_small_amount_tier_correctly()
|
||||||
|
{
|
||||||
|
$document = new FinanceDocument(['amount' => 4999]);
|
||||||
|
$this->assertEquals('small', $document->determineAmountTier());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Patterns:**
|
||||||
|
|
||||||
|
1. **Setup Method:**
|
||||||
|
```php
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// Seed roles/permissions once per test class
|
||||||
|
$this->artisan('db:seed', ['--class' => 'FinancialWorkflowPermissionsSeeder']);
|
||||||
|
|
||||||
|
// Fake storage if needed
|
||||||
|
Storage::fake('private');
|
||||||
|
|
||||||
|
// Create test users
|
||||||
|
$this->admin = User::factory()->create();
|
||||||
|
$this->admin->assignRole('admin');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Teardown:** Not typically needed; `RefreshDatabase` trait handles rollback automatically.
|
||||||
|
|
||||||
|
3. **Assertion Pattern:**
|
||||||
|
Use specific assertions for clarity:
|
||||||
|
```php
|
||||||
|
// Model relationships
|
||||||
|
$this->assertInstanceOf(Member::class, $payment->member);
|
||||||
|
$this->assertEquals($cashier->id, $payment->verifiedByCashier->id);
|
||||||
|
|
||||||
|
// Status checks
|
||||||
|
$this->assertTrue($payment->isPending());
|
||||||
|
$this->assertFalse($document->needsBoardMeetingApproval());
|
||||||
|
|
||||||
|
// Database state
|
||||||
|
$this->assertDatabaseHas('cashier_ledger_entries', [
|
||||||
|
'entry_type' => 'receipt',
|
||||||
|
'amount' => 5000,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// HTTP responses
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertRedirect();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mocking
|
||||||
|
|
||||||
|
**Framework:** Mockery (configured in `composer.json`)
|
||||||
|
|
||||||
|
**Patterns:**
|
||||||
|
Minimal mocking in this codebase. When needed:
|
||||||
|
|
||||||
|
1. **File Storage Mocking:**
|
||||||
|
```php
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Storage::fake('private'); // All storage operations use fake disk
|
||||||
|
}
|
||||||
|
|
||||||
|
// In test
|
||||||
|
$file = UploadedFile::fake()->create('document.pdf', 100, 'application/pdf');
|
||||||
|
$path = $file->store('payment-receipts', 'private');
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Database Factories:** Preferred over mocking for model creation
|
||||||
|
```php
|
||||||
|
$member = Member::factory()->create(['membership_status' => 'active']);
|
||||||
|
$payment = MembershipPayment::factory()->create(['member_id' => $member->id]);
|
||||||
|
```
|
||||||
|
|
||||||
|
**What to Mock:**
|
||||||
|
- External APIs (not used in current codebase)
|
||||||
|
- File uploads (use `Storage::fake()`)
|
||||||
|
- Email sending (Laravel's test mailers or `Mail::fake()`)
|
||||||
|
|
||||||
|
**What NOT to Mock:**
|
||||||
|
- Database models (use factories instead)
|
||||||
|
- Business logic methods (test them directly)
|
||||||
|
- Service classes (inject real instances)
|
||||||
|
- Permission/role system (use real Spatie setup)
|
||||||
|
|
||||||
|
## Fixtures and Factories
|
||||||
|
|
||||||
|
**Test Data:**
|
||||||
|
Models use Laravel factories (`database/factories/`):
|
||||||
|
```php
|
||||||
|
// In test
|
||||||
|
$member = Member::factory()->create([
|
||||||
|
'membership_status' => 'active',
|
||||||
|
'membership_expires_at' => now()->addYear(),
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Helper Traits:**
|
||||||
|
Reusable test setup in `tests/Traits/`:
|
||||||
|
|
||||||
|
1. **`SeedsRolesAndPermissions`** (`tests/Traits/SeedsRolesAndPermissions.php`):
|
||||||
|
- `seedRolesAndPermissions()`: Seed all financial roles
|
||||||
|
- `createUserWithRole(string $role)`: Create user + assign role
|
||||||
|
- `createAdmin()`, `createSecretary()`, `createChair()`: Convenience methods
|
||||||
|
- `createFinanceApprovalTeam()`: Create all approval users at once
|
||||||
|
|
||||||
|
2. **`CreatesFinanceData`** (`tests/Traits/CreatesFinanceData.php`):
|
||||||
|
- `createFinanceDocument(array $attributes)`: Basic document
|
||||||
|
- `createSmallAmountDocument()`: Auto-set amount < 5000
|
||||||
|
- `createMediumAmountDocument()`: Auto-set 5000-50000
|
||||||
|
- `createLargeAmountDocument()`: Auto-set > 50000
|
||||||
|
- `createDocumentAtStage(string $stage)`: Pre-set approval status
|
||||||
|
- `createPaymentOrder()`, `createBankReconciliation()`: Domain-specific
|
||||||
|
- `createFakeAttachment()`: PDF file for testing
|
||||||
|
- `getValidFinanceDocumentData()`: Reusable form data
|
||||||
|
|
||||||
|
3. **`CreatesMemberData`** (`tests/Traits/CreatesMemberData.php`):
|
||||||
|
- `createMember()`: Create member + associated user
|
||||||
|
- `createPendingMember()`, `createActiveMember()`, `createExpiredMember()`: Status helpers
|
||||||
|
- `createMemberWithPendingPayment()`: Pre-populated workflow state
|
||||||
|
|
||||||
|
**Location:**
|
||||||
|
- Traits: `tests/Traits/`
|
||||||
|
- Factories: `database/factories/`
|
||||||
|
- Database seeders: `database/seeders/`
|
||||||
|
|
||||||
|
**Usage Example:**
|
||||||
|
```php
|
||||||
|
class FinanceWorkflowTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase, SeedsRolesAndPermissions, CreatesFinanceData;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->seedRolesAndPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_approval_workflow()
|
||||||
|
{
|
||||||
|
$secretary = $this->createSecretary();
|
||||||
|
$document = $this->createSmallAmountDocument();
|
||||||
|
|
||||||
|
$this->actingAs($secretary)
|
||||||
|
->post(route('admin.finance-document.approve', $document), [
|
||||||
|
'approval_notes' => 'Approved'
|
||||||
|
])
|
||||||
|
->assertRedirect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
|
||||||
|
**Requirements:** No minimum coverage enforced (not configured in `phpunit.xml`).
|
||||||
|
|
||||||
|
**View Coverage:**
|
||||||
|
```bash
|
||||||
|
php artisan test --coverage # Text report
|
||||||
|
php artisan test --coverage --html # HTML report in coverage/ directory
|
||||||
|
```
|
||||||
|
|
||||||
|
**Target areas (recommended):**
|
||||||
|
- Model methods: 80%+
|
||||||
|
- Service classes: 80%+
|
||||||
|
- Controllers: 70%+ (integration tests)
|
||||||
|
- Traits: 80%+
|
||||||
|
|
||||||
|
## Test Types
|
||||||
|
|
||||||
|
**Unit Tests** (`tests/Unit/`):
|
||||||
|
- Scope: Single method/model logic
|
||||||
|
- Database: Yes, but minimal (models with factories)
|
||||||
|
- Approach: Direct method calls, assertions on return values
|
||||||
|
- Example (`FinanceDocumentTest.php`):
|
||||||
|
```php
|
||||||
|
public function test_it_determines_small_amount_tier_correctly()
|
||||||
|
{
|
||||||
|
$document = new FinanceDocument(['amount' => 4999]);
|
||||||
|
$this->assertEquals('small', $document->determineAmountTier());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Feature Tests** (`tests/Feature/`):
|
||||||
|
- Scope: Full request → response cycle
|
||||||
|
- Database: Yes (full workflow)
|
||||||
|
- Approach: HTTP methods (`$this->get()`, `$this->post()`), database assertions
|
||||||
|
- Example (`CashierLedgerWorkflowTest.php` lines 41-64):
|
||||||
|
```php
|
||||||
|
public function cashier_can_create_receipt_entry()
|
||||||
|
{
|
||||||
|
$this->actingAs($this->cashier);
|
||||||
|
|
||||||
|
$response = $this->post(route('admin.cashier-ledger.store'), [
|
||||||
|
'entry_type' => 'receipt',
|
||||||
|
'amount' => 5000,
|
||||||
|
// ...
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertRedirect();
|
||||||
|
$this->assertDatabaseHas('cashier_ledger_entries', [
|
||||||
|
'entry_type' => 'receipt',
|
||||||
|
'amount' => 5000,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Browser Tests** (`tests/Browser/` with Laravel Dusk):
|
||||||
|
- Scope: Full page interactions (JavaScript, dynamic content)
|
||||||
|
- Database: Yes
|
||||||
|
- Approach: Browser automation, user journey testing
|
||||||
|
- Example (`ExampleTest.php`):
|
||||||
|
```php
|
||||||
|
public function testBasicExample(): void
|
||||||
|
{
|
||||||
|
$this->browse(function (Browser $browser) {
|
||||||
|
$browser->visit('/')
|
||||||
|
->assertSee('Laravel');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
**Async Testing:**
|
||||||
|
Use `RefreshDatabase` trait for atomic transactions:
|
||||||
|
```php
|
||||||
|
public function test_concurrent_payment_submissions()
|
||||||
|
{
|
||||||
|
$member = $this->createActiveMember();
|
||||||
|
|
||||||
|
$payment1 = MembershipPayment::factory()->create([
|
||||||
|
'member_id' => $member->id,
|
||||||
|
'status' => MembershipPayment::STATUS_PENDING
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Multiple operations automatically rolled back after test
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Testing:**
|
||||||
|
```php
|
||||||
|
public function test_invalid_amount_rejected()
|
||||||
|
{
|
||||||
|
$validated = $this->validate([
|
||||||
|
'amount' => 'required|numeric|min:0'
|
||||||
|
], ['amount' => -1000]);
|
||||||
|
|
||||||
|
$this->assertFalse($validated); // Validation fails
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Permission Testing:**
|
||||||
|
```php
|
||||||
|
public function test_non_admin_cannot_approve_large_document()
|
||||||
|
{
|
||||||
|
$user = $this->createMembershipManager();
|
||||||
|
$document = $this->createLargeAmountDocument();
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->post(route('admin.finance-document.approve', $document))
|
||||||
|
->assertForbidden(); // 403 Forbidden
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**State Transitions:**
|
||||||
|
```php
|
||||||
|
public function test_document_approval_workflow()
|
||||||
|
{
|
||||||
|
$secretary = $this->createSecretary();
|
||||||
|
$document = $this->createDocumentAtStage('pending');
|
||||||
|
|
||||||
|
// Step 1: Secretary approves
|
||||||
|
$this->actingAs($secretary)
|
||||||
|
->post(route('admin.finance-document.approve', $document));
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
FinanceDocument::STATUS_APPROVED_SECRETARY,
|
||||||
|
$document->fresh()->status
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 2: Chair approves
|
||||||
|
$chair = $this->createChair();
|
||||||
|
$this->actingAs($chair)
|
||||||
|
->post(route('admin.finance-document.approve', $document->fresh()));
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
FinanceDocument::STATUS_APPROVED_CHAIR,
|
||||||
|
$document->fresh()->status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database Assertions:**
|
||||||
|
```php
|
||||||
|
// Check record exists with specific values
|
||||||
|
$this->assertDatabaseHas('finance_documents', [
|
||||||
|
'id' => $document->id,
|
||||||
|
'status' => FinanceDocument::STATUS_APPROVED_SECRETARY,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check record count
|
||||||
|
$this->assertCount(3, MembershipPayment::all());
|
||||||
|
|
||||||
|
// Count with condition
|
||||||
|
$this->assertEquals(
|
||||||
|
2,
|
||||||
|
MembershipPayment::where('status', 'pending')->count()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment & Configuration
|
||||||
|
|
||||||
|
**Test Environment:**
|
||||||
|
- Set via `phpunit.xml` `<env>` elements
|
||||||
|
- `APP_ENV=testing`
|
||||||
|
- `DB_CONNECTION=sqlite`
|
||||||
|
- `DB_DATABASE=:memory:` (transient in-memory database)
|
||||||
|
- `MAIL_MAILER=array` (no real emails sent)
|
||||||
|
- `QUEUE_CONNECTION=sync` (synchronous job processing)
|
||||||
|
|
||||||
|
**Database State:**
|
||||||
|
- `RefreshDatabase` trait: Runs migrations before each test, rolls back after
|
||||||
|
- `:memory:` SQLite: Fresh database per test, extremely fast
|
||||||
|
- No fixtures needed; use factories instead
|
||||||
|
|
||||||
|
**Disabling Middleware (when needed):**
|
||||||
|
```php
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->withoutMiddleware([
|
||||||
|
\App\Http\Middleware\EnsureUserIsAdmin::class,
|
||||||
|
\App\Http\Middleware\VerifyCsrfToken::class
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Testing analysis: 2026-02-13*
|
||||||
Reference in New Issue
Block a user