Files
usher-manage-stack/.planning/phases/01-database-schema-backend-api/01-02-PLAN.md

243 lines
12 KiB
Markdown

---
phase: 01-database-schema-backend-api
plan: 02
type: execute
wave: 2
depends_on: ["01-01"]
files_modified:
- app/Http/Controllers/Admin/MemberNoteController.php
- app/Http/Requests/StoreNoteRequest.php
- routes/web.php
- app/Http/Controllers/AdminMemberController.php
- tests/Feature/Admin/MemberNoteTest.php
autonomous: true
must_haves:
truths:
- "Admin can create a note for a member via POST /admin/members/{member}/notes with text content"
- "Admin can retrieve all notes for a member via GET /admin/members/{member}/notes with author names and timestamps"
- "Member list at /admin/members shows accurate note count per member without N+1 queries"
- "Note creation is wrapped in DB::transaction with AuditLogger::log call"
- "Non-admin users receive 403 when attempting to create notes"
- "Feature tests pass covering CRUD, authorization, audit logging, and N+1 prevention"
artifacts:
- path: "app/Http/Controllers/Admin/MemberNoteController.php"
provides: "Note store and index endpoints"
exports: ["MemberNoteController"]
min_lines: 30
- path: "app/Http/Requests/StoreNoteRequest.php"
provides: "Validation for note creation with Traditional Chinese error messages"
exports: ["StoreNoteRequest"]
min_lines: 15
- path: "routes/web.php"
provides: "Admin routes for member notes (store + index)"
contains: "members.notes"
- path: "app/Http/Controllers/AdminMemberController.php"
provides: "withCount('notes') added to member list query"
contains: "withCount"
- path: "tests/Feature/Admin/MemberNoteTest.php"
provides: "Feature tests for note creation, retrieval, auth, audit, N+1"
min_lines: 80
key_links:
- from: "app/Http/Controllers/Admin/MemberNoteController.php"
to: "app/Models/Note.php"
via: "Creates notes via Member morphMany relationship"
pattern: "notes\\(\\)->create"
- from: "app/Http/Controllers/Admin/MemberNoteController.php"
to: "app/Support/AuditLogger.php"
via: "Logs note creation in audit trail"
pattern: "AuditLogger::log.*note\\.created"
- from: "app/Http/Controllers/AdminMemberController.php"
to: "app/Models/Note.php"
via: "withCount('notes') for N+1 prevention"
pattern: "withCount.*notes"
- from: "routes/web.php"
to: "app/Http/Controllers/Admin/MemberNoteController.php"
via: "Route registration for store and index"
pattern: "MemberNoteController"
---
<objective>
Create the backend API endpoints for member notes: controller with store/index actions, form request validation, route registration, member list note count integration, and comprehensive feature tests.
Purpose: Provides the backend API that Phase 2 (inline UI) will consume. After this plan, notes can be created and retrieved via HTTP endpoints, and the member list shows note counts.
Output: Working POST/GET endpoints for member notes, StoreNoteRequest validation, note count on member list, feature tests.
</objective>
<execution_context>
@/Users/gbanyan/.claude/get-shit-done/workflows/execute-plan.md
@/Users/gbanyan/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-database-schema-backend-api/01-RESEARCH.md
@.planning/phases/01-database-schema-backend-api/01-01-SUMMARY.md
@app/Http/Controllers/AdminMemberController.php
@app/Http/Controllers/Admin/ArticleController.php
@app/Http/Requests/StoreMemberRequest.php
@app/Support/AuditLogger.php
@routes/web.php
@tests/Feature/MemberRegistrationTest.php
</context>
<tasks>
<task type="auto">
<name>Task 1: Create MemberNoteController, StoreNoteRequest, and register routes</name>
<files>
app/Http/Controllers/Admin/MemberNoteController.php
app/Http/Requests/StoreNoteRequest.php
routes/web.php
</files>
<action>
Create `app/Http/Requests/StoreNoteRequest.php`:
- `authorize()` returns true (authorization is handled by the admin middleware on the route group; this matches the pattern where controllers in the admin route group rely on the middleware, not Form Request authorization)
- `rules()`: content => 'required|string|min:1|max:65535'
- `messages()`: Traditional Chinese error messages — 'content.required' => '備忘錄內容為必填欄位', 'content.min' => '備忘錄內容不可為空白'
Create `app/Http/Controllers/Admin/MemberNoteController.php`:
- Namespace: `App\Http\Controllers\Admin` (matches ArticleController pattern in Admin namespace)
- Extends `App\Http\Controllers\Controller`
**`index(Member $member)` method:**
- Load notes with author: `$member->notes()->with('author')->get()`
- Return JSON response: `response()->json(['notes' => $notes])` — This returns JSON because Phase 2 will consume it via AJAX/Axios from Alpine.js. This is NOT a public API endpoint; it's an admin endpoint returning JSON for inline UI consumption.
- Each note in the response should include: id, content, created_at, author (with name)
**`store(StoreNoteRequest $request, Member $member)` method:**
- Wrap in `DB::transaction()` closure
- Create note: `$member->notes()->create(['content' => $request->content, 'author_user_id' => $request->user()->id])`
- Audit log: `AuditLogger::log('note.created', $note, ['member_id' => $member->id, 'member_name' => $member->full_name, 'author' => $request->user()->name])`
- Return JSON: `response()->json(['note' => $note->load('author'), 'message' => '備忘錄已新增'], 201)` — JSON response for AJAX consumption in Phase 2
In `routes/web.php`:
- Inside the existing `Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(...)` block
- Add these routes near the existing member routes (after line ~139 where member payment routes end):
```php
// Member Notes (會員備忘錄)
Route::get('/members/{member}/notes', [MemberNoteController::class, 'index'])->name('members.notes.index');
Route::post('/members/{member}/notes', [MemberNoteController::class, 'store'])->name('members.notes.store');
```
- Add the import at the top of web.php: `use App\Http\Controllers\Admin\MemberNoteController;`
</action>
<verify>
Run `php artisan route:list --name=members.notes` and confirm both routes appear:
- GET /admin/members/{member}/notes → admin.members.notes.index
- POST /admin/members/{member}/notes → admin.members.notes.store
</verify>
<done>
MemberNoteController exists with store() (JSON response, DB::transaction, AuditLogger) and index() (JSON with notes + author) methods. StoreNoteRequest validates content field with Traditional Chinese messages. Routes registered as admin.members.notes.store and admin.members.notes.index.
</done>
</task>
<task type="auto">
<name>Task 2: Add withCount to member list and create feature tests</name>
<files>
app/Http/Controllers/AdminMemberController.php
tests/Feature/Admin/MemberNoteTest.php
</files>
<action>
In `app/Http/Controllers/AdminMemberController.php`:
- In the `index()` method, change line 18 from `$query = Member::query()->with('user');` to `$query = Member::query()->with('user')->withCount('notes');`
- This adds a `notes_count` attribute to each member via a single subquery (no N+1)
- The Blade view can access `$member->notes_count` — Phase 2 will use this for the badge
Create `tests/Feature/Admin/MemberNoteTest.php`:
- Namespace: `Tests\Feature\Admin`
- Use `RefreshDatabase` trait
- `setUp()`: seed roles via `$this->artisan('db:seed', ['--class' => 'RoleSeeder']);`
- Create admin user helper: `$admin = User::factory()->create(); $admin->assignRole('admin');`
**Test cases (minimum required):**
1. `test_admin_can_create_note_for_member()`:
- Create admin user, create member
- POST to `route('admin.members.notes.store', $member)` with `['content' => 'Test note content']`
- Assert 201 status
- Assert response JSON has note with content and author
- Assert `assertDatabaseHas('notes', ['notable_type' => 'member', 'notable_id' => $member->id, 'content' => 'Test note content', 'author_user_id' => $admin->id])`
2. `test_admin_can_retrieve_notes_for_member()`:
- Create admin, member, and 3 notes using NoteFactory::forMember($member)->byAuthor($admin)
- GET `route('admin.members.notes.index', $member)`
- Assert 200 status
- Assert response JSON has 'notes' array with 3 items
- Assert each note has 'content', 'created_at', and 'author.name' keys
3. `test_note_creation_requires_content()`:
- POST with empty content
- Assert 422 status (validation error)
- Assert response JSON has errors for 'content' field
4. `test_note_creation_logs_audit_trail()`:
- Create note via POST
- Assert `assertDatabaseHas('audit_logs', ['action' => 'note.created'])` with metadata containing member_id
5. `test_non_admin_cannot_create_note()`:
- Create regular user (no admin role, no permissions)
- POST to create note
- Assert 403 status
6. `test_member_list_includes_notes_count()`:
- Create admin, create 2 members
- Create 3 notes for member 1, 0 notes for member 2
- GET `route('admin.members.index')`
- Assert 200 status
- Assert view has 'members' data
- Assert first member has notes_count attribute (verify it's 3 or 0 as expected)
7. `test_notes_returned_newest_first()`:
- Create member with 3 notes at different timestamps (use `created_at` overrides in factory)
- GET index endpoint
- Assert first note in response has the most recent created_at
Follow existing test patterns from MemberRegistrationTest.php and CashierLedgerWorkflowTest.php.
</action>
<verify>
Run `php artisan test --filter=MemberNoteTest` and confirm all 7 tests pass.
Run `php artisan test` to confirm no existing tests are broken by the changes.
</verify>
<done>
AdminMemberController index() includes withCount('notes') for N+1 prevention. MemberNoteTest.php has 7 passing feature tests covering: note creation, retrieval, validation, audit logging, authorization, member list note count, and chronological ordering. All existing tests still pass.
</done>
</task>
</tasks>
<verification>
1. Run `php artisan test --filter=MemberNoteTest` — all 7 tests pass
2. Run `php artisan test` — no regressions in existing tests
3. Run `php artisan route:list --name=members.notes` — both routes (GET index, POST store) visible
4. Manual verification with tinker:
```php
// Create a note via the relationship
$admin = User::where('email', 'admin@test.com')->first();
$member = Member::first();
$note = $member->notes()->create(['content' => 'Manual test', 'author_user_id' => $admin->id]);
echo $note->notable_type; // Should print 'member' (not App\Models\Member)
echo $note->author->name; // Should print admin name
// Verify withCount works
$members = Member::withCount('notes')->get();
echo $members->first()->notes_count; // Should print integer
```
</verification>
<success_criteria>
1. POST /admin/members/{member}/notes creates a note and returns 201 JSON with note + author data
2. GET /admin/members/{member}/notes returns JSON array of notes with author names and timestamps
3. Note creation wrapped in DB::transaction with AuditLogger::log('note.created', ...)
4. StoreNoteRequest validates content (required, string, min:1) with Traditional Chinese messages
5. AdminMemberController index uses withCount('notes') — notes_count available on each member
6. All 7 feature tests pass
7. No regressions in existing test suite
</success_criteria>
<output>
After completion, create `.planning/phases/01-database-schema-backend-api/01-02-SUMMARY.md`
</output>