Files
usher-manage-stack/CLAUDE.md
gbanyan f6295b759e docs: update CLAUDE.md with CMS, API, and architecture details
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>
2026-02-13 15:08:41 +08:00

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:

  1. Admin app — Blade-based UI at /admin/* for internal management (members, finance, issues, CMS)
  2. 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 by REGISTRATION_ENABLED env), /documents
  • Member (auth only): dashboard, payment submission
  • Admin (auth + admin): /admin/* with admin.{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/search
  • GET /articles/{slug} — detail with related articles
  • GET /categories — all categories with article counts
  • GET /pages/{slug} — page with hierarchical children
  • GET /homepage — aggregated homepage data (featured, latest by type, about, categories)
  • GET /public-documents — document library with search/filter
  • GET /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/update
  • SiteRevalidationService::revalidateDocument($slug) — called via DocumentObserver on CRUD
  • Config in config/services.phpnextjs.revalidate_url, nextjs.revalidate_token
  • SiteAssetSyncService copies uploads to the Next.js public/ directory for static serving
  • CORS allows usher.org.tw, localhost:3000, *.vercel.app (see config/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:

  1. Approval Stage (status): Multi-tier approval based on amount
  2. Disbursement Stage (disbursement_status): Dual confirmation (requester + cashier)
  3. 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 support
  • SettingsService: System-wide settings with caching
  • FinanceDocumentApprovalService: Multi-tier approval orchestration
  • PaymentVerificationService: Payment workflow coordination
  • SiteRevalidationService: Next.js cache invalidation webhook
  • SiteAssetSyncService: Copy uploads to Next.js public/ 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)
  • admin role 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 accessor national_id decrypts on read
  • File uploads: Private disk storage, served via authenticated controller. DownloadFile helper 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: pendingactiveexpired / 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: newassignedin_progressreviewclosed. 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 assignment
  • import:members / import:accounting-data / import:documents / import:article-documents — Bulk imports
  • send:membership-expiry-reminders — Automated email notifications
  • documents:archive-expired — Auto-archive documents past expiry (respects auto_archive_on_expiry flag)
  • 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 access
  • requester@test.com - Submit documents
  • cashier@test.com - Tier 1 approval
  • accountant@test.com - Tier 2 approval
  • chair@test.com - Tier 3 approval

Key Files

  • routes/web.php — All web routes (admin routes under /admin prefix)
  • routes/api.php — Headless CMS API v1 routes
  • config/accounting.php — Account codes, amount tier thresholds, currency settings
  • config/services.phpnextjs — Next.js integration config (revalidation, asset sync)
  • config/cors.php — CORS allowed origins for API
  • app/Models/FinanceDocument.php — Core financial workflow logic with 27+ status constants
  • app/Models/Member.php — Member lifecycle with encrypted field handling
  • app/Models/Article.php — CMS article with content types, access levels, scopes
  • app/Models/Document.php — Versioned document library with UUID access
  • app/Traits/HasApprovalWorkflow.php — Shared multi-tier approval behavior
  • app/Traits/HasAccountingEntries.php — Double-entry bookkeeping behavior
  • app/Services/SiteRevalidationService.php — Next.js revalidation webhook
  • app/helpers.php — Global settings() helper (autoloaded via composer)
  • database/seeders/ — RoleSeeder, ChartOfAccountSeeder, TestDataSeeder
  • docs/SYSTEM_SPECIFICATION.md — Complete system specification