
---

## S06-004: Member create form (P-161)
*Date: 2026-05-08*

### A1 — Task prompt AC copy-paste error
The task prompt lists MEM-TA-03 (share contributions) under "Acceptance Criteria". The correct story is **MEM-TA-01** (member registration). Implementation follows MEM-TA-01 and BUSINESS_FLOWS.md §I1.

### A2 — member_no generation
No explicit generation rule in planning docs. Factory uses `M-NNNN` pattern. Implemented as: `max(member_no) + 1` per tenant inside DB transaction, formatted as `M-NNNN`. The unique constraint `uq_members_tenant_no` acts as the race-condition safety net.

### A3 — Branch assignment deferred
Task title mentions "branch assignment" but no branch model exists in Sprint 6. Field omitted from form; documented here so it can be added when Branch domain is built.

### A4 — Duplicate IC UX
AC says "blocks with message and shows existing record for confirmation". Confirmation flow is out of scope. Implementation returns a validation error that includes the existing member's `member_no` and name. Full record-confirmation UX deferred to a future task.

### A5 — ic_no_hash computation
Hash = `sha256(tenant_id + ic_no_with_hyphens)`. This matches the MemberFactory convention. The `CreateMemberRequest` validation rule and the `CreateMember` action both use the same hash function.

### A6 — files_touched incomplete
Sprint backlog lists `FormUiController.php` and `FormUi.tsx` only. Actual files required: `CreateMember` action, `CreateMemberRequest`, `StoreMemberController`, test file, route additions. All created in this task.

### A7 — Explicit audit log for registration
`LogsActivity` fires on dirty model changes. To satisfy AC #5 (capture registration with TA user_id), an explicit `activity()->causedBy($actor)->performedOn($member)->log('member.registered')` call is added to `CreateMember` action.

---

## S06-005: Member detail page (P-162)
*Date: 2026-05-08*

### A1 — Task prompt AC copy-paste error
The task prompt lists MEM-TA-04 (subscription payments) under "Acceptance Criteria". The correct stories are MEM-TA-02 through MEM-TA-11 (full member relationship view). Implementation follows P-162 screen brief.

### A2 — Edit-in-place deferred
Task title says "edit-in-place" but the hi-fi screen shows `[Edit]` linking to a separate edit form. True inline editing is complex and deferred. The `[Edit]` button links to `/members/{id}/edit` (to be built in a future task). Noted so the route can be added cleanly.

### A3 — Shares/Loans/Subscriptions/Documents tabs are stubs
These tabs require domain tables (shares, loans, subscriptions) not yet built in Sprint 6. Each tab renders a "Coming Soon" placeholder. The tab structure and URL hash routing are fully wired so content can be added without changing the layout.

### A4 — IC display is masked
The member's IC number is shown as `••••••-••-XXXX` (last 4 digits only) in the profile card, protecting PII in the UI. Full IC is only stored encrypted server-side.

### A5 — Activity log query
Uses `Spatie\Activitylog\Models\Activity::query()->where('subject_type', Member::class)->where('subject_id', $id)` (v5 API). Only last 10 events shown in the Overview tab; full history in the Activity tab (paginated).

---

## S06-006: Member status transition actions
*Date: 2026-05-08*

### A1 — MemberStateMachine already exists
`MemberStateMachine` (S06-002) handles state transitions, date column updates, and audit logging. `StatusActions` is a thin business-rule wrapper that enforces per-transition requirements (board_reference for approvals) before delegating.

### A2 — Board reference stored as reason
AC requires "board meeting reference (date + resolution number)" for approvals. Implementation stores this in the `reason` parameter passed to MemberStateMachine, which records it in the activity log properties. A separate column is not added (no schema migration needed this sprint).

### A3 — Welcome/rejection emails deferred
AC mentions "welcome email if email on file; rejection email if appropriate". Email sending requires a Notification/Mailable class and Mail driver config — deferred to a future task. Noted in assumptions.

### A4 — HTTP controller added despite not in files_touched
The task lists only an Action + test file, but without an HTTP endpoint the transition can't be invoked. A `TransitionMemberController` and route are added for completeness. The frontend confirmation UX (modal) is wired via this endpoint.

---

## S06-007: Member NRIC encryption
*Date: 2026-05-08*

### A1 — Encryption already implemented via model cast
The `ic_no_encrypted` column uses `'encrypted'` cast (S06-001), which uses Laravel's built-in AES-256-CBC encryption with the app key. The `NricEncryption` class centralises the hash computation and format validation that were previously duplicated across `CreateMemberRequest` and `CreateMember` action.

### A2 — Log redaction confirmed
`Member::getActivitylogOptions()` uses `logOnly(['member_no', 'name', 'state', 'joining_date', 'contact_email', 'contact_phone'])` — `ic_no_encrypted` and `ic_no_hash` are excluded. `DetailUiController` only returns `ic_masked` (last 4 digits). No plaintext IC escapes to any external system.

### A3 — Hash format unchanged
Hash remains `sha256(tenant_id + ic_with_hyphens)` — consistent with S06-001 factory and S06-004 action. Changing the hash function would invalidate existing records.

---

## S06-009: Member factory + seeded data
*Date: 2026-05-08*

### A1 — Seeding added to DemoDataSeeder, not a new FactorySeedSeeder
Task files_touched lists `FactorySeedSeeder.php` but a new seeder file would be isolated and not called by `DatabaseSeeder`. Members seed is added to `DemoDataSeeder` (the existing demo data seeder for Koperasi Wawasan) to keep all demo data in one place.

### A2 — member_no sequencing in seed
Factory uses `M-NNNN` format via `unique()->numberBetween()`. When seeding, member_no is generated sequentially within the seeder to guarantee uniqueness and realistic numbering.

### A3 — ic_no_hash computation
Each seeded member's `ic_no_hash` = `sha256(tenant_id + ic_no)` using `NricEncryption::hash()`. This is consistent with the production code path.

---

## S06-010: Member search
*Date: 2026-05-08*

### A1 — ic_no_last4 is acceptable non-sensitive data
The last 4 digits of a Malaysian NRIC are a sequential registry number (e.g., "5678"). They do not encode a birthdate, gender, or home state — those are in the first 6 digits. Storing last4 in plaintext is acceptable under PDPA because it has no standalone identifying value. If legal determines otherwise, the ic_no_last4 column must be removed and NRIC partial search disabled.

### A2 — Backfill for ic_no_last4
The migration adds a nullable column. Records created before this migration will have `ic_no_last4 = NULL` and won't match IC-last4 searches. For production, a one-time backfill is required. Demo seeder is updated to populate the column.

### A3 — member_no uses GIN trigram for substring search
`member_no ILIKE '%0042%'` enables finding "M-0042" by typing just "0042". A B-tree `text_pattern_ops` index would only support prefix matching. GIN trigram allows mid-string matches which is more useful.

### A4 — Search class reuses BelongsToTenant global scope
`Search::execute()` calls `Member::query()` which automatically applies the `TenantScope` global scope — no manual `where('tenant_id', ...)` required. Cross-tenant isolation is structural, not manual.

### A5 — No frontend changes needed for search expansion
The existing search input in `Index.tsx` already sends `?search=` to the backend. The expansion (phone, IC last4, full NRIC) is transparent to the UI.

---

## S06-011: Member export
*Date: 2026-05-08*

### A1 — CSV only (not Excel)
Task says "CSV/Excel". Excel export via PhpSpreadsheet would add a ~5MB dependency. CSV with a UTF-8 BOM opens correctly in Excel, Numbers, and LibreOffice Calc. Assumption: CSV-with-BOM satisfies the Excel requirement for the tender demo. If the founder requires .xlsx format, PhpSpreadsheet integration is a separate task.

### A2 — TA sees full decrypted IC; non-TA sees masked
Laravel's `encrypted` cast decrypts `ic_no_encrypted` on model load. The `Export` class passes `showFullIc = $user->hasRole('tenant_admin')`. Non-TA roles (including TU, who currently has no member list access at all) receive the masked format. If a future role gets read-only member access, the masking is already in place.

### A3 — Export respects the same search/status filters as the list page
`ExportMembersController` reuses `Search::execute()` — the export button passes `?search=&status=` params from the current URL. "What you see is what you export" UX matches P-160 spec.

### A4 — No streaming size limit implemented
`lazy()` + streamed response means memory usage is O(batch_size), not O(total_records). No artificial record cap is applied. For extremely large tenants (>100k members), the HTTP response timeout may become an issue — but this is not a concern for the tender pilot scope.

---

## S06-008: Member-Party polymorphism (SKIPPED)
*Date: 2026-05-08*

### A1 — Task is COMMERCIAL_FORK
S06-008 has `scope_tag: "COMMERCIAL_FORK"` and `sprint: 106`. Per the framework rules, COMMERCIAL_FORK tasks are deferred to the post-tender fork and must not be implemented during the tender stage. Task was identified and skipped.

---

## S06-012: Sprint retro
*Date: 2026-05-08*

### Sprint S06 — Test Suite Results
- Feature/Domain tests: **251 passing**
- Unit tests: **104 passing**
- Total: **355 tests, 0 failures**
- PHPStan: **0 errors** across all member domain files
- Pint: **clean** across all member domain files

### State machine edge cases discovered
1. `active → rejected` is blocked by `StatusActions::transition()` — must use `reject()`. This is correct behavior but the error message was initially non-descriptive. Fixed to reference the method name.
2. The `MemberStateMachine` does not validate that `approve()` is only called from `pending_approval`. The state machine allows the `active → active` no-op transition as an edge case. Documented; not a blocking issue for tender.
3. Board reference requirement is enforced in `StatusActions::approve()` but the HTTP controller initially passed `$boardReference ?: $reason` which bypassed the check when reason was provided. Fixed before PR merge.

### Party polymorphism status
S06-008 (Member-Party polymorphism) was identified as COMMERCIAL_FORK and skipped. The `is_party_id` column exists on `members` and is nullable. Members are not yet auto-registered as parties on approval. This integration is deferred to post-tender Sprint 106.

### Assumptions that need founder sign-off
- **A1 (S06-010/S06-011)**: `ic_no_last4` as non-sensitive plaintext — needs legal confirmation before production.
- **A1 (S06-011)**: CSV-with-BOM accepted as "Excel export" — confirm with founder before tender demo.
- **A2 (S06-004)**: Branch assignment deferred — no branch domain yet.

### Files introduced by S06 (summary)
| File | Purpose |
|---|---|
| `app/Domain/Members/Models/Member.php` | Core member model |
| `app/Domain/Members/Enums/MemberStatus.php` | Status backed enum |
| `app/Domain/Members/Actions/CreateMember.php` | Member registration action |
| `app/Domain/Members/Actions/MemberStateMachine.php` | Transition enforcement |
| `app/Domain/Members/Actions/StatusActions.php` | Business-rule wrapper |
| `app/Domain/Members/NricEncryption.php` | IC hash, mask, last4 |
| `app/Domain/Members/Search.php` | Search domain class |
| `app/Domain/Members/Export.php` | CSV export domain class |
| `app/Http/Controllers/Members/*` | 5 controllers |
| `resources/js/pages/Members/*.tsx` | Create, Index, Show pages |
| `database/migrations/*_create_members_table.php` | Members schema |
| `database/migrations/*_add_member_search_columns.php` | Search indexes + ic_no_last4 |
| `database/factories/Domain/Members/MemberFactory.php` | Test factory |
| `database/seeders/DemoDataSeeder.php` (modified) | 50 demo members |
| `tests/Feature/Domain/Members/*.php` | 6 feature test files |
| `tests/Unit/Domain/Members/*.php` | 3 unit test files |

---

## S15-001: GP23 section taxonomy and coa_accounts migration
*Date: 2026-05-08*

### A1 — Legacy coarse codes fully removed
The old `coa_accounts.statement_section` CHECK constraint allowed `('balance_sheet','profit_and_loss','equity','off_balance_sheet')`. These 4 codes are replaced entirely by 11 granular GP23 codes. The data migration maps existing demo rows to their most common granular equivalent (balance_sheet asset→current_assets, liability→current_liabilities; profit_and_loss income→operating_income, expense→operating_expenses). Any account with `equity` or `off_balance_sheet` section stays as-is.

### A2 — Taxonomy lives in Reporting domain
`StatementSection` enum and `SectionTaxonomy` class placed in `app/Domain/Reporting/` (not `Accounting/`). Taxonomy is a financial reporting concern — it governs how accounts appear on Penyata Kewangan, not how accounting entries are posted.

### A3 — Data migration is best-effort (no production data yet)
The migration assumes demo-only data. Accounts with legacy section codes that do not cleanly map (e.g., a balance_sheet equity account stored as `account_type=equity`) are left as-is since `equity` remains a valid code. No production data to protect.

### A4 — Cherry-picks from S15-001 and S15-003 carried
PRs #151, #153 not yet merged to dev.
