docs(01): create phase plan — 2 plans for database schema and backend API
This commit is contained in:
@@ -32,10 +32,11 @@ Decimal phases appear between their surrounding integers in numeric order.
|
|||||||
4. Member list shows accurate note count for each member without N+1 queries
|
4. Member list shows accurate note count for each member without N+1 queries
|
||||||
5. Note creation events are logged in audit trail with action and metadata
|
5. Note creation events are logged in audit trail with action and metadata
|
||||||
|
|
||||||
**Plans**: TBD
|
**Plans:** 2 plans
|
||||||
|
|
||||||
Plans:
|
Plans:
|
||||||
- [ ] TBD (will be defined in plan-phase)
|
- [ ] 01-01-PLAN.md — Database schema, Note model, Member relationship, morph map, factory
|
||||||
|
- [ ] 01-02-PLAN.md — MemberNoteController, routes, member list withCount, feature tests
|
||||||
|
|
||||||
### Phase 2: Inline Quick-Add UI
|
### Phase 2: Inline Quick-Add UI
|
||||||
**Goal**: Deliver core value — admins can annotate members inline without page navigation
|
**Goal**: Deliver core value — admins can annotate members inline without page navigation
|
||||||
@@ -84,7 +85,7 @@ Phases execute in numeric order: 1 → 2 → 3
|
|||||||
|
|
||||||
| Phase | Plans Complete | Status | Completed |
|
| Phase | Plans Complete | Status | Completed |
|
||||||
|-------|----------------|--------|-----------|
|
|-------|----------------|--------|-----------|
|
||||||
| 1. Database Schema & Backend API | 0/TBD | Not started | - |
|
| 1. Database Schema & Backend API | 0/2 | Planning complete | - |
|
||||||
| 2. Inline Quick-Add UI | 0/TBD | Not started | - |
|
| 2. Inline Quick-Add UI | 0/TBD | Not started | - |
|
||||||
| 3. Note History & Display | 0/TBD | Not started | - |
|
| 3. Note History & Display | 0/TBD | Not started | - |
|
||||||
|
|
||||||
|
|||||||
181
.planning/phases/01-database-schema-backend-api/01-01-PLAN.md
Normal file
181
.planning/phases/01-database-schema-backend-api/01-01-PLAN.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
---
|
||||||
|
phase: 01-database-schema-backend-api
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- database/migrations/YYYY_MM_DD_HHMMSS_create_notes_table.php
|
||||||
|
- app/Models/Note.php
|
||||||
|
- app/Models/Member.php
|
||||||
|
- app/Providers/AppServiceProvider.php
|
||||||
|
- database/factories/NoteFactory.php
|
||||||
|
autonomous: true
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Notes table exists with polymorphic columns (notable_type, notable_id) and composite index"
|
||||||
|
- "Note model has morphTo relationship to notable and belongsTo relationship to author (User)"
|
||||||
|
- "Member model has morphMany relationship to notes ordered by created_at desc"
|
||||||
|
- "Morph map registered in AppServiceProvider maps 'member' to Member::class"
|
||||||
|
- "NoteFactory can generate test notes with forMember() state method"
|
||||||
|
artifacts:
|
||||||
|
- path: "database/migrations/*_create_notes_table.php"
|
||||||
|
provides: "Notes table schema with polymorphic columns and indexes"
|
||||||
|
contains: "morphs('notable')"
|
||||||
|
- path: "app/Models/Note.php"
|
||||||
|
provides: "Note model with relationships"
|
||||||
|
exports: ["Note"]
|
||||||
|
min_lines: 25
|
||||||
|
- path: "app/Models/Member.php"
|
||||||
|
provides: "notes() morphMany relationship added to existing model"
|
||||||
|
contains: "morphMany(Note::class"
|
||||||
|
- path: "app/Providers/AppServiceProvider.php"
|
||||||
|
provides: "Morph map registration for namespace safety"
|
||||||
|
contains: "enforceMorphMap"
|
||||||
|
- path: "database/factories/NoteFactory.php"
|
||||||
|
provides: "Factory for test note creation"
|
||||||
|
contains: "forMember"
|
||||||
|
key_links:
|
||||||
|
- from: "app/Models/Note.php"
|
||||||
|
to: "app/Models/Member.php"
|
||||||
|
via: "morphTo/morphMany polymorphic relationship"
|
||||||
|
pattern: "morphTo|morphMany"
|
||||||
|
- from: "app/Models/Note.php"
|
||||||
|
to: "app/Models/User.php"
|
||||||
|
via: "belongsTo author relationship"
|
||||||
|
pattern: "belongsTo.*User::class.*author_user_id"
|
||||||
|
- from: "app/Providers/AppServiceProvider.php"
|
||||||
|
to: "app/Models/Member.php"
|
||||||
|
via: "Morph map registration"
|
||||||
|
pattern: "enforceMorphMap.*member.*Member::class"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create the database foundation for the member notes system: migration, Note model with polymorphic relationships, Member model relationship addition, morph map registration, and test factory.
|
||||||
|
|
||||||
|
Purpose: Establishes the data layer that all subsequent note features depend on. Without this, no notes can be stored or queried.
|
||||||
|
Output: Notes table in database, Note model, Member->notes relationship, NoteFactory for testing.
|
||||||
|
</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
|
||||||
|
|
||||||
|
@app/Models/Member.php
|
||||||
|
@app/Models/CustomFieldValue.php
|
||||||
|
@app/Providers/AppServiceProvider.php
|
||||||
|
@database/factories/MemberFactory.php
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create notes migration and Note model with polymorphic relationships</name>
|
||||||
|
<files>
|
||||||
|
database/migrations/YYYY_MM_DD_HHMMSS_create_notes_table.php
|
||||||
|
app/Models/Note.php
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Create migration using `php artisan make:migration create_notes_table`. In the migration:
|
||||||
|
- `$table->id()`
|
||||||
|
- `$table->morphs('notable')` — creates notable_type (string), notable_id (unsignedBigInteger), and composite index on [notable_type, notable_id] automatically
|
||||||
|
- `$table->longText('content')` — note text content
|
||||||
|
- `$table->foreignId('author_user_id')->constrained('users')->cascadeOnDelete()` — links to User who wrote the note
|
||||||
|
- `$table->timestamps()`
|
||||||
|
- `$table->index('created_at')` — for chronological sorting queries
|
||||||
|
|
||||||
|
Create `app/Models/Note.php`:
|
||||||
|
- Use `HasFactory` trait
|
||||||
|
- `$fillable`: notable_type, notable_id, content, author_user_id
|
||||||
|
- `notable()` method returning `$this->morphTo()` (MorphTo relationship)
|
||||||
|
- `author()` method returning `$this->belongsTo(User::class, 'author_user_id')` (BelongsTo relationship)
|
||||||
|
- Follow existing model patterns from CustomFieldValue.php (same polymorphic pattern)
|
||||||
|
|
||||||
|
Run `php artisan migrate` to apply the migration.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
Run `php artisan migrate:status` and confirm the create_notes_table migration shows as "Ran".
|
||||||
|
Run `php artisan tinker --execute="Schema::hasTable('notes')"` and confirm it returns true.
|
||||||
|
Run `php artisan tinker --execute="Schema::getColumnListing('notes')"` and confirm columns: id, notable_type, notable_id, content, author_user_id, created_at, updated_at.
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
Notes table exists in database with all columns (id, notable_type, notable_id, content, author_user_id, created_at, updated_at), composite index on [notable_type, notable_id], and index on created_at. Note model exists with notable() morphTo and author() belongsTo relationships.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Add Member relationship, morph map, and test factory</name>
|
||||||
|
<files>
|
||||||
|
app/Models/Member.php
|
||||||
|
app/Providers/AppServiceProvider.php
|
||||||
|
database/factories/NoteFactory.php
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
In `app/Models/Member.php`:
|
||||||
|
- Add `use Illuminate\Database\Eloquent\Relations\MorphMany;` import
|
||||||
|
- Add `notes()` method returning `$this->morphMany(Note::class, 'notable')->orderBy('created_at', 'desc')` — default ordering newest first for display
|
||||||
|
- Add `use App\Models\Note;` import
|
||||||
|
- Place the method near existing relationship methods (after payments, user, etc.)
|
||||||
|
|
||||||
|
In `app/Providers/AppServiceProvider.php`:
|
||||||
|
- Add `use Illuminate\Database\Eloquent\Relations\Relation;` import
|
||||||
|
- Add `use App\Models\Member;` import
|
||||||
|
- In `boot()` method, add: `Relation::enforceMorphMap(['member' => Member::class]);`
|
||||||
|
- This ensures 'member' is stored in notable_type column instead of the full class name 'App\Models\Member', protecting against future namespace refactoring
|
||||||
|
|
||||||
|
Create `database/factories/NoteFactory.php`:
|
||||||
|
- Follow existing MemberFactory pattern
|
||||||
|
- `definition()` returns: notable_type => 'member' (uses morph map alias, NOT Member::class), notable_id => Member::factory(), content => $this->faker->paragraph(), author_user_id => User::factory()
|
||||||
|
- Add `forMember(Member $member)` state method that sets notable_type => 'member', notable_id => $member->id
|
||||||
|
- Add `byAuthor(User $user)` state method that sets author_user_id => $user->id
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
Run `php artisan tinker --execute="use App\Models\Member; use App\Models\Note; echo (new Member)->notes() instanceof \Illuminate\Database\Eloquent\Relations\MorphMany ? 'OK' : 'FAIL';"` and confirm "OK".
|
||||||
|
Run `php artisan tinker --execute="use Illuminate\Database\Eloquent\Relations\Relation; echo json_encode(Relation::morphMap());"` and confirm output contains "member" key mapping to Member class.
|
||||||
|
Run `php artisan tinker --execute="use App\Models\Note; echo class_exists(\Database\Factories\NoteFactory::class) ? 'OK' : 'FAIL';"` and confirm "OK".
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
Member model has notes() morphMany relationship returning notes ordered by created_at desc. AppServiceProvider registers morph map with 'member' => Member::class. NoteFactory exists with definition(), forMember(), and byAuthor() state methods.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Run `php artisan migrate:fresh --seed` to confirm migration works cleanly with existing seeders.
|
||||||
|
Run `php artisan tinker` and execute:
|
||||||
|
```php
|
||||||
|
use App\Models\Member;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Note;
|
||||||
|
|
||||||
|
$user = User::first();
|
||||||
|
$member = Member::first();
|
||||||
|
$note = $member->notes()->create(['content' => 'Test note', 'author_user_id' => $user->id]);
|
||||||
|
echo $note->id . ' - ' . $note->notable_type . ' - ' . $note->content;
|
||||||
|
echo $note->author->name;
|
||||||
|
echo $member->notes()->count();
|
||||||
|
```
|
||||||
|
All commands should execute without errors. The notable_type should show 'member' (not 'App\Models\Member') due to the morph map.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
1. Notes table exists with all required columns and indexes
|
||||||
|
2. Note model has working morphTo and belongsTo relationships
|
||||||
|
3. Member model has working morphMany notes relationship (ordered by created_at desc)
|
||||||
|
4. Morph map stores 'member' string (not full class name) in notable_type
|
||||||
|
5. NoteFactory can create notes with forMember() and byAuthor() state methods
|
||||||
|
6. `php artisan migrate:fresh --seed` runs without errors
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/01-database-schema-backend-api/01-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
242
.planning/phases/01-database-schema-backend-api/01-02-PLAN.md
Normal file
242
.planning/phases/01-database-schema-backend-api/01-02-PLAN.md
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
---
|
||||||
|
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>
|
||||||
Reference in New Issue
Block a user