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