Add headless CMS API documentation, port 8001 convention, dual role architecture, CMS content models, Next.js integration, and document library sections. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
11 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Taiwan NPO (Non-Profit Organization) management platform built with Laravel 10 (PHP 8.1+). Features member lifecycle management, multi-tier financial approval workflows, issue tracking, CMS with headless API, and document management. UI is in Traditional Chinese. Currency is TWD (NT$).
A companion Next.js frontend (usher-site) consumes the headless CMS API for the public website.
Commands
# Development
php artisan serve --port=8001 && npm run dev # Start both servers (port 8000 often occupied)
# Testing
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 tests
# Database
php artisan migrate:fresh --seed # Reset with all seeders
php artisan db:seed --class=TestDataSeeder # Seed test data only
php artisan db:seed --class=FinancialWorkflowTestDataSeeder # Finance test data
# Code Quality
./vendor/bin/pint # Fix code style (PSR-12)
./vendor/bin/phpstan analyse # Static analysis
Tech Stack
- Backend: Laravel 10, PHP 8.1+, Spatie Laravel Permission
- Frontend: Blade templates, Tailwind CSS 3.1 (
darkMode: 'class'), Alpine.js 3.4, Vite 5 - PDF/Excel: barryvdh/laravel-dompdf, maatwebsite/excel
- QR Codes: simplesoftwareio/simple-qrcode
- DB: SQLite (dev), MySQL (production)
Architecture
Dual Role: Admin App + Headless CMS API
This app serves two purposes:
- Admin app — Blade-based UI at
/admin/*for internal management (members, finance, issues, CMS) - Headless CMS API — JSON API at
/api/v1/*consumed by the Next.js public site
Route Structure
Routes split across routes/web.php, routes/auth.php, and routes/api.php.
Admin routes use group pattern:
Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(...)
- Public:
/register/member(controlled byREGISTRATION_ENABLEDenv),/documents - Member (auth only): dashboard, payment submission
- Admin (auth + admin):
/admin/*withadmin.{module}.{action}named routes
The admin middleware (EnsureUserIsAdmin) allows access if user has admin role OR any permissions at all. A separate CheckPaidMembership middleware gates member-only features.
API routes (/api/v1/*):
GET /articles— paginated, filter by type/category/tag/searchGET /articles/{slug}— detail with related articlesGET /categories— all categories with article countsGET /pages/{slug}— page with hierarchical childrenGET /homepage— aggregated homepage data (featured, latest by type, about, categories)GET /public-documents— document library with search/filterGET /public-documents/{uuid}— document detail by UUID
API response wrapping: Single resources are wrapped in { "data": {...} }. The homepage endpoint is NOT wrapped. Collections use Laravel pagination.
CMS Content Models
Article — 4 content types (news, notice, column, activity), 4 access levels (public, members, board, admin), 3 statuses (draft, published, archived). Supports scheduled publishing (published_at), expiration (expires_at), pinning (display_order), EasyMDE markdown editor, and file attachments.
Page — Hierarchical parent/child pages with custom fields (JSON), optional template field. Used for static content like "About Us".
Document — Versioned document library with auto-generated public_uuid for shareable URLs. Semantic versioning (1.0, 1.1...), SHA256 file integrity, access levels, auto-archive on expiry. QR code generation at /documents/{uuid}/qrcode.
Common query scopes across content models: active(), published(), forAccessLevel(?User), pinned(), byContentType($type).
Next.js Static Site Integration
The public site is fully static after next build. Laravel triggers cache revalidation on content changes:
SiteRevalidationService::revalidateArticle($slug)— called in admin CMS controllers after publish/updateSiteRevalidationService::revalidateDocument($slug)— called viaDocumentObserveron CRUD- Config in
config/services.php→nextjs.revalidate_url,nextjs.revalidate_token SiteAssetSyncServicecopies uploads to the Next.jspublic/directory for static serving- CORS allows
usher.org.tw,localhost:3000,*.vercel.app(seeconfig/cors.php)
Multi-Tier Approval Workflows
Tiered approval based on amount thresholds (configurable in config/accounting.php):
- Small (<5,000): Secretary approval only
- Medium (5,000-50,000): Secretary → Chair
- Large (>50,000): Secretary → Chair → Board
Finance documents follow a 3-stage lifecycle with separate status fields:
- Approval Stage (
status): Multi-tier approval based on amount - Disbursement Stage (
disbursement_status): Dual confirmation (requester + cashier) - Recording Stage (
recording_status): Accountant records to ledger
$doc->isApprovalComplete() // All required approvals obtained
$doc->isDisbursementComplete() // Both parties confirmed
$doc->isRecordingComplete() // Ledger entry created
$doc->isFullyProcessed() // All 3 stages complete
Shared Traits
HasApprovalWorkflow: Multi-tier approval with self-approval prevention (isSelfApproval())HasAccountingEntries: Double-entry bookkeeping, auto-generation, balance validation
Service Layer
Complex business logic in app/Services/:
MembershipFeeCalculator: Fees with disability discount supportSettingsService: System-wide settings with cachingFinanceDocumentApprovalService: Multi-tier approval orchestrationPaymentVerificationService: Payment workflow coordinationSiteRevalidationService: Next.js cache invalidation webhookSiteAssetSyncService: Copy uploads to Next.jspublic/for static serving
Global helper: settings('key') returns cached setting values (defined in app/helpers.php, autoloaded).
RBAC Structure
Uses Spatie Laravel Permission. Core roles:
- Financial:
finance_requester,finance_cashier,finance_accountant,finance_chair,finance_board_member - CMS:
secretary_general(full CMS),membership_manager(article management) adminrole has full access
CMS permissions: view/create/edit/delete/publish_articles, manage_all_articles, plus equivalents for pages and announcements.
Permission checks: $user->can('permission-name') or @can('permission-name') in Blade.
Data Security Patterns
- National ID: AES-256 encrypted (
national_id_encrypted), SHA256 hashed for search (national_id_hash), virtual accessornational_iddecrypts on read - File uploads: Private disk storage, served via authenticated controller.
DownloadFilehelper handles RFC 5987 UTF-8 filenames (Chinese filename support) - Audit logging:
AuditLogger::log($action, $auditable, $metadata)— static class, call in controllers (not automatic via observers)
Member Lifecycle
States: pending → active → expired / suspended
Identity types: patient, parent, social, other (病友/家長分類). Members can have guardian_member_id for parent-child relationships. Members have optional line_id for LINE messaging.
$member->hasPaidMembership() // Active with future expiry
$member->canSubmitPayment() // Pending with no pending payment
$member->getNextFeeType() // entrance_fee or annual_fee
Public registration controlled by REGISTRATION_ENABLED env var — when disabled, existing members can still login but /register/member routes are not registered.
Issue Tracking
Built-in issue tracker with auto-generated numbers (ISS-{year}-{seq}). Statuses: new → assigned → in_progress → review → closed. Types: work_item, project_task, maintenance, member_request. Supports parent/child relationships, time logging (estimated vs actual hours), and member association.
Conventions
- Status values: Defined as class constants (e.g.,
FinanceDocument::STATUS_PENDING), not enums or magic strings - Controller validation: Uses Form Request classes (
StoreMemberRequest, etc.) - DB transactions: Used in controllers for multi-step operations
- UI text: Hardcoded Traditional Chinese strings (no
__()translation layer) - Dark mode: Supported throughout — use
dark:Tailwind prefix for styles - Blade stacks:
@stack('styles')in<head>and@stack('scripts')before</body>for page-specific assets (EasyMDE, charts, etc.)
Custom Artisan Commands
assign:role— Manual role assignmentimport:members/import:accounting-data/import:documents/import:article-documents— Bulk importssend:membership-expiry-reminders— Automated email notificationsdocuments:archive-expired— Auto-archive documents past expiry (respectsauto_archive_on_expiryflag)nextjs:push-assets— Manual trigger for git push of synced assets to Next.js repo
Testing Patterns
Tests use RefreshDatabase trait. Setup commonly includes:
protected function setUp(): void
{
parent::setUp();
$this->artisan('db:seed', ['--class' => 'RoleSeeder']);
}
Test accounts (password: password):
admin@test.com- Full accessrequester@test.com- Submit documentscashier@test.com- Tier 1 approvalaccountant@test.com- Tier 2 approvalchair@test.com- Tier 3 approval
Key Files
routes/web.php— All web routes (admin routes under/adminprefix)routes/api.php— Headless CMS API v1 routesconfig/accounting.php— Account codes, amount tier thresholds, currency settingsconfig/services.php→nextjs— Next.js integration config (revalidation, asset sync)config/cors.php— CORS allowed origins for APIapp/Models/FinanceDocument.php— Core financial workflow logic with 27+ status constantsapp/Models/Member.php— Member lifecycle with encrypted field handlingapp/Models/Article.php— CMS article with content types, access levels, scopesapp/Models/Document.php— Versioned document library with UUID accessapp/Traits/HasApprovalWorkflow.php— Shared multi-tier approval behaviorapp/Traits/HasAccountingEntries.php— Double-entry bookkeeping behaviorapp/Services/SiteRevalidationService.php— Next.js revalidation webhookapp/helpers.php— Globalsettings()helper (autoloaded via composer)database/seeders/— RoleSeeder, ChartOfAccountSeeder, TestDataSeederdocs/SYSTEM_SPECIFICATION.md— Complete system specification