12 KiB
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:
FinanceDocumentApprovalServiceorchestrates 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 inapp/helpers.php— returns cached system settings
Data Flow
Member Lifecycle Flow
- Registration: Non-member completes form at
/register/member→ stored inMemberstable withstatus = pending - Profile Activation: Member logs in, fills profile at
/create-member-profile - Payment Submission: Member submits payment proof at
/member/submit-payment→MembershipPaymentstable - Payment Verification: Admin verifies payment via multi-tier approval workflow
- Activation: Admin activates member →
status = active,membership_expires_atset - Expiry: Cron job or admin action →
status = expiredwhenmembership_expires_atpasses
Key Methods:
Member::hasPaidMembership()— checks if active with future expiryMember::canSubmitPayment()— validates pending status with no pending paymentsMember::getNextFeeType()— returns entrance_fee or annual_fee
Finance Document Approval Flow (3-Stage Lifecycle)
Stage 1: Approval (Amount-Based Multi-Tier)
-
Submission: User submits finance document →
FinanceDocument::STATUS_PENDINGsubmitted_by_user_id,submitted_at,amount,titleset- Amount tier auto-determined:
determineAmountTier()→ small/medium/large
-
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
-
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
-
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:
$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)
-
Requester Confirmation: Document requester confirms disbursement details
DISBURSEMENT_REQUESTER_CONFIRMEDstatus- Field:
disbursement_requester_confirmed_at
-
Cashier Confirmation: Cashier verifies funds transferred
DISBURSEMENT_CASHIER_CONFIRMEDstatus- Field:
disbursement_cashier_confirmed_at - Creates
PaymentOrderrecord
-
Completion: Once both confirm →
DISBURSEMENT_COMPLETED
Helper: $doc->isDisbursementComplete() — both parties confirmed
Stage 3: Recording (Ledger Entry)
-
Cashier Ledger Entry: Cashier records transaction to ledger
- Creates
CashierLedgerEntrywith debit/credit amounts - Status:
RECORDING_PENDING→RECORDING_COMPLETED
- Creates
-
Accounting Entry: Automatic double-entry bookkeeping via
HasAccountingEntriestrait- Creates
AccountingEntryrecords (debit + credit pair) - Account codes from
config/accounting.php - Field:
chart_of_account_id
- Creates
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:
- Cashier Approval: Verifies payment received
- Accountant Approval: Validates accounting entry
- Chair Approval: Final sign-off for large payments
Route: /admin/payment-verifications with status-based filtering
CMS Content Flow (Articles/Pages)
-
Creation: Admin creates article at
/admin/articles/createwith:- Title, slug, body (markdown via EasyMDE), featured image, status
- Categories and tags (many-to-many relationships)
-
Publication: Admin publishes →
status = published,published_atset -
API Exposure: Content available via
/api/v1/articles,/api/v1/pages -
Site Revalidation: On publish/archive, fires webhook to Next.js for static site rebuild
- Service:
SiteRevalidationService - Webhook: POST to
services.nextjs.revalidate_urlwith token
- Service:
Document Library Flow
-
Upload: Admin uploads document at
/admin/documents/create- Creates
Documentwithstatus = active - Multiple versions tracked via
DocumentVersiontable
- Creates
-
Version Control:
- New version upload creates
DocumentVersionrecord - Promote version: sets as current version
- Archive: soft delete via
status = archived
- New version upload creates
-
Public Access: Documents visible at
/documentsifaccess_levelallows- Access tracking via
DocumentAccessLog - QR code generation at
/documents/{uuid}/qrcode
- Access tracking via
-
Permission Check:
- Method:
$doc->canBeViewedBy($user)validates access - Visibility: public, members-only, or admin-only
- Method:
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:
AuditLogmodel 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')orsettings('key', 'default') - Cache: Redis/cache layer to avoid repeated DB queries
- Locations:
SystemSettingmodel,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/*withadminmiddleware and named route prefixadmin.{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
@errordirective - Authorization errors: Thrown by
@candirective orauthorize()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.phpdefines 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:
AuditLoggerclass 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:
Usermodel withHasFactoryand 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,@canin Blade
Encryption:
- National IDs: AES-256 encrypted in
national_id_encryptedcolumn - Hash for searching: SHA256 in
national_id_hashcolumn - Virtual accessor
national_id: Auto-decrypts on read - Method:
Member::findByNationalId($id)uses hash for query
Architecture analysis: 2026-02-13