# Plan Review: CannaManage Sprint 3

**Date:** 2026-06-11  
**Reviewer:** Roo (Plan Reviewer)  
**Documents:** Sprint 3 Plan v2 (APPROVED by Planner)  
**Verdict:** ✅ APPROVED

---

## Summary

The Sprint 3 plan is comprehensive, technically sound, and well-ordered. All deferred Sprint 2 features (staff, portal, reports, prevention officer, club settings) are covered. Architecture decisions are consistent with the existing codebase. The plan correctly builds on Sprint 2's shared-schema + TenantFilterAspect pattern rather than the architecture doc's theoretical schema-per-tenant model. 7 non-blocking findings identified — all are suggestions for improvement, none require plan revision.

---

## Review Checklist

### 1. Does the plan cover ALL deferred Sprint 2 items?

| Deferred Item | Plan Coverage | Status |
|---------------|--------------|--------|
| STAFF role | Phase 1 (StaffPermission enum, StaffAccount entity, SpEL checker) | ✅ |
| Member portal | Phase 5 (session-based SecurityFilterChain, PortalController) | ✅ |
| Reports | Phase 4 (ReportController, iText 7 PDF, Commons CSV) | ✅ |
| Prevention Officer | Phase 6 (configurable limit, assignment endpoint, under-21 gate) | ✅ |
| Club settings | Phase 2 (ClubController GET/PUT /clubs/me, stats) | ✅ |
| Integration tests | Phase 7 (Testcontainers PostgreSQL, 6 test classes) | ✅ |

**Result: ✅ PASS** — All deferred items fully addressed.

---

### 2. Architecture consistency with Sprint 2 patterns

| Pattern | Sprint 2 Implementation | Sprint 3 Plan | Consistent? |
|---------|------------------------|---------------|-------------|
| Tenant isolation | `TenantFilterAspect` (shared-schema, Hibernate @Filter) | Continues shared-schema; `staff_accounts` has `tenant_id` | ✅ |
| JWT auth | `JwtService` + `JwtAuthFilter` (stateless, JJWT) | Extends with `jti` + `permissions` claims | ✅ |
| SecurityConfig | `@Order(1)` API + `@Order(2)` Public | Inserts Portal `@Order(2)`, shifts Public to `@Order(3)` | ✅ |
| Role model | `UserRole` enum: `ROLE_ADMIN`, `ROLE_STAFF`, `ROLE_MEMBER` | Uses same enum; STAFF already present in code | ✅ |
| Entity base | `AbstractTenantEntity` with `tenant_id` | New entities (StaffAccount, InviteToken) follow same pattern | ✅ |
| Repositories | Spring Data JPA in `cannamanage-service/.../repository/` | New repos placed in same package | ✅ |

**Result: ✅ PASS** — Fully consistent with established patterns.

---

### 3. Staff permission model well-defined?

| Aspect | Assessment |
|--------|-----------|
| Enum values | 8 permissions covering all current features + 1 future (`MANAGE_GROW_CALENDAR`) | ✅ |
| Storage | JSONB on `staff_accounts` — correct for PostgreSQL, supports flexible permission sets | ✅ |
| Enforcement | SpEL `@PreAuthorize` + custom `StaffPermissionChecker` bean | ✅ |
| JWT embedding | Permissions in JWT for stateless checks; blacklist fallback for revoked tokens | ✅ |
| ADMIN bypass | `StaffPermissionChecker.has()` returns `true` for ADMIN role first | ✅ |
| Templates | 3 role templates (Ausgabe, Lager, Vorstand) — matches architecture doc | ✅ |

**Result: ✅ PASS** — Well-designed, DSGVO-compliant least-privilege model.

---

### 4. Member portal auth design clean?

| Aspect | Assessment |
|--------|-----------|
| Dual SecurityFilterChain | `@Order(2)` for `/portal/**` — correct isolation from API chain | ✅ |
| Session-based | `SessionCreationPolicy.IF_REQUIRED` + 30min timeout | ✅ |
| CSRF | Enabled via `CookieCsrfTokenRepository.withHttpOnlyFalse()` — React SPA can read cookie | ✅ |
| Read-only | All portal endpoints are GET — minimal attack surface | ✅ |
| Data isolation | Member can only see own data (enforced by `memberId` from session principal) | ✅ |
| `SameSite=Strict` | Correct for CSRF prevention | ✅ |

**Result: ✅ PASS** — Clean separation of concerns.

---

### 5. Dependencies pinned to specific versions?

| Library | Plan Version | Pinned? |
|---------|-------------|---------|
| iText 7 | `8.0.x` | ⚠️ Range |
| Apache Commons CSV | `1.11+` | ⚠️ Range |
| Caffeine | `3.1+` | ⚠️ Range |
| Spring Boot Starter Mail | Boot-managed | ✅ (BOM) |
| Testcontainers | `1.19+` | ⚠️ Range |

**Result: ⚠️ WARNING (non-blocking)** — Versions use ranges instead of exact pins. This is acceptable for a plan document (exact versions determined at implementation time by checking latest stable), but the implementor should pin exact versions in the POM.

---

### 6. Flyway V3 migration complete?

| Schema Change | Covered in V3? |
|---------------|---------------|
| `staff_accounts` table | ✅ Full DDL with JSONB, indexes, unique constraint |
| `revoked_tokens` table | ✅ With jti index + expires index |
| `invite_tokens` table | ✅ With token index, FK to users |
| `users.prevention_officer` column | ✅ `ALTER TABLE ADD COLUMN` |
| Club extension columns (9 columns) | ✅ All `ALTER TABLE ADD COLUMN IF NOT EXISTS` |

**Result: ✅ PASS** — All new tables and columns defined in a single idempotent migration.

---

### 7. Test plan comprehensive?

| Phase | Unit Tests | Integration Tests | Total |
|-------|-----------|-------------------|-------|
| P1 (Staff + Revocation) | T-01 to T-06 | — | 6 |
| P2 (Club) | T-07 to T-10 | — | 4 |
| P3 (Staff CRUD + Invite) | T-11 to T-14 | — | 4 |
| P4 (Reports) | T-15 to T-18 | — | 4 |
| P5 (Portal) | — | T-19, T-20 | 2 |
| P6 (Prevention Officer) | T-21, T-22 | — | 2 |
| P7 (Integration) | — | T-23 to T-26 | 4 |
| **Total** | **18** | **8** | **26** |

Coverage check:
- Every phase has at least one test: ✅
- Edge cases (expired token, invalid regex, permission denial): ✅
- E2E flows (invite → set-password → login → permission check): ✅
- Tenant isolation: ✅
- Token revocation lifecycle: ✅

**Result: ✅ PASS** — Comprehensive coverage matching all implementation phases.

---

### 8. Gaps between API spec and implementation plan?

| API Spec Endpoint | Sprint 3 Plan | Gap? |
|-------------------|---------------|------|
| `GET /clubs/me` (§6.1) | Phase 2 — `ClubController` | ✅ Covered |
| `PUT /clubs/me` (§6.2) | Phase 2 — `ClubController` | ✅ Covered |
| `GET /clubs/me/stats` (§6.3) | Phase 2 — `ClubStatsResponse` DTO | ✅ Covered |
| `POST /auth/logout` (§5.3) | Not explicitly addressed | ⚠️ See finding #3 |
| `GET /reports/monthly` (§10) | Phase 4 — `ReportController` | ✅ Covered |
| `GET /reports/members` (§10) | Phase 4 — member list export | ✅ Covered |
| `GET /reports/recall/{batchId}` (§10) | Phase 4 — recall report | ✅ Covered |
| Staff endpoints (not in spec) | Phase 3 — new endpoints | ℹ️ Plan extends spec |
| Portal endpoints (not in spec) | Phase 5 — new `/portal/**` | ℹ️ Plan extends spec |
| `POST /auth/set-password` (not in spec) | Phase 3 step 3.9 | ℹ️ Plan extends spec |

**Result: ✅ PASS** — One minor gap (logout integration), otherwise plan both implements spec and correctly extends it for Sprint 3 scope.

---

### 9. Token revocation/blacklist approach sound?

| Aspect | Assessment |
|--------|-----------|
| DB-backed (no Redis) | Correct for MVP scale — simple, durable, no infrastructure dependency | ✅ |
| Caffeine cache (60s TTL, 10K max) | Appropriate tradeoff — worst case 60s window after revocation | ✅ |
| `jti` claim in JWT | Required for per-token revocation — currently missing, plan adds it | ✅ |
| `revokeAllForUser()` | Called on permission change + staff deactivation — correct triggers | ✅ |
| Cleanup scheduler (daily 3 AM) | Removes expired tokens to prevent table bloat | ✅ |
| Index on `jti` | Fast lookup for blacklist check | ✅ |
| Index on `expires_at` | Fast cleanup queries | ✅ |

**Result: ✅ PASS** — Sound, pragmatic approach for a club-scale application.

---

### 10. Risks not addressed?

| Potential Risk | Addressed? | Notes |
|----------------|-----------|-------|
| iText AGPL license | ✅ | Risk table mentions it, mitigation: switch to OpenPDF before go-live |
| Boot 4 `@EntityScan` issue | ✅ | Phase 7 step 7.1 explicitly addresses it |
| JSONB + Hibernate 6 | ✅ | `@JdbcTypeCode(SqlTypes.JSON)` mentioned |
| SMTP delivery | ✅ | Mailpit for dev, transactional email service for prod |
| Cache staleness | ✅ | Accepted as non-critical for club app |
| Portal CSRF + SPA | ✅ | `CookieCsrfTokenRepository.withHttpOnlyFalse()` |
| `JwtAuthFilter` portal path exclusion | ⚠️ | See finding #5 |
| Email domain regex DoS | ⚠️ | See finding #6 |

**Result: ✅ PASS** — All major risks addressed. Two minor items noted below.

---

## Findings

### ⚠️ Warnings (non-blocking — implement-time improvements)

#### 1. Dependency versions not pinned

**Plan §9** uses version ranges (`8.0.x`, `1.11+`, `3.1+`).

**Recommendation:** At implementation time, pin exact versions in the POM:
- iText 7: `8.0.5`
- Commons CSV: `1.12.0`
- Caffeine: `3.1.8`
- Testcontainers: `1.20.1`

---

#### 2. `revoked_tokens` table has no `tenant_id` column

The token blacklist is global across all tenants. This works correctly (tokens are unique by `jti` regardless of tenant), but it means:
- `revokeAllForUser(userId)` queries ALL tenants' revoked tokens
- No ability to purge one tenant's revoked tokens independently

**Recommendation:** Consider adding `tenant_id` for operational convenience. Not blocking — the current design is functionally correct.

---

#### 3. `POST /auth/logout` not integrated with token revocation

API spec §5.3 defines a logout endpoint that invalidates the refresh token. Sprint 3's `TokenRevocationService` adds access token revocation. The plan doesn't explicitly describe how these interact.

**Recommendation:** On `POST /auth/logout`:
1. Revoke refresh token (existing behavior)
2. Also add current access token's `jti` to `revoked_tokens` (new behavior)

This ensures immediate invalidation rather than waiting for natural token expiry. Implementor should handle this in Phase 1.

---

#### 4. Portal `formLogin` with JSON API may need `AuthenticationSuccessHandler`

The plan describes portal as "JSON API" (Decision D2: no Thymeleaf) but configures `formLogin()` with `defaultSuccessUrl`. Standard `formLogin` returns HTTP redirects (302), not JSON responses.

**Recommendation:** Implement a custom `AuthenticationSuccessHandler` that returns `200 OK` with session info as JSON, and a custom `AuthenticationFailureHandler` returning `401` JSON error. This aligns with the SPA architecture.

---

#### 5. `JwtAuthFilter.shouldNotFilter()` needs `/portal/**` exclusion

Current `shouldNotFilter()` skips `/api/v1/auth/`, `/swagger-ui`, `/v3/api-docs`. The plan adds a portal with session-based auth, but the JWT filter will still process `/portal/**` requests (finding no Bearer header → passes through).

**Recommendation:** Add `/portal/` to `shouldNotFilter()` for clarity and to avoid unnecessary filter processing:
```java
return path.startsWith("/api/v1/auth/")
    || path.startsWith("/portal/")  // ← add this
    || path.startsWith("/swagger-ui")
    || path.startsWith("/v3/api-docs");
```

Not strictly blocking (filter already passes through when no Bearer header present), but cleaner.

---

#### 6. Regex pattern validation — potential ReDoS

`validateEmailDomain()` uses `email.matches(club.getAllowedEmailPattern())` with admin-supplied regex. Malicious or poorly written regex could cause catastrophic backtracking (ReDoS).

**Recommendation:** Add a timeout or use `Pattern.compile()` with a simple validation check:
```java
try {
    Pattern.compile(pattern);  // validates syntax
    // Additionally: reject patterns with known dangerous constructs
    // Or use a timeout: java.util.concurrent.CompletableFuture with timeout
} catch (PatternSyntaxException e) {
    throw new InvalidRegexException(pattern);
}
```

The plan does validate invalid regex (step 2.7, test T-10) but doesn't mention ReDoS protection specifically.

---

#### 7. Architecture doc deviation should be noted

The architecture doc (03-Architecture.md) describes:
- Schema-per-tenant (Sprint 2 implemented shared-schema with `tenant_id`)
- `ROLE_CLUB_ADMIN` / `ROLE_PREVENTION_OFFICER` (code uses `ROLE_ADMIN` / boolean flag)
- 8-hour access token (code uses 1-hour)

These are expected evolutionary deviations — the architecture doc reflects initial design, and Sprint 2 made pragmatic choices. The architecture doc should be updated to reflect reality, but this doesn't block Sprint 3.

**Recommendation:** Add a backlog item to sync the wiki architecture doc with actual implementation post-Sprint 3.

---

## Traceability Matrix

| Requirement Source | Plan Step | Test Case | Status |
|-------------------|-----------|-----------|--------|
| Deferred: STAFF role | Phase 1 (1.1–1.15) | T-01 to T-06, T-25 | ✅ |
| Deferred: Club settings | Phase 2 (2.1–2.8) | T-07 to T-10 | ✅ |
| Deferred: Staff CRUD + invite | Phase 3 (3.1–3.15) | T-11 to T-14, T-25 | ✅ |
| Deferred: Reports (US-007, US-008) | Phase 4 (4.1–4.9) | T-15 to T-18 | ✅ |
| Deferred: Member portal | Phase 5 (5.1–5.7) | T-19, T-20 | ✅ |
| Deferred: Prevention Officer (US-010) | Phase 6 (6.1–6.8) | T-21, T-22 | ✅ |
| Deferred: Integration tests | Phase 7 (7.1–7.8) | T-23 to T-26 | ✅ |
| Decision D1: Token revocation | Phase 1 (1.5, 1.6, 1.10, 1.14) | T-05, T-06, T-26 | ✅ |
| Decision D2: Portal as JSON API | Phase 5 | T-19, T-20 | ✅ |
| Decision D3: Multiple prevention officers | Phase 6 (6.2, 6.7) | T-21 | ✅ |
| Decision D4: Minimal PDF branding | Phase 4 (4.4, 4.5) | T-16 | ✅ |
| Decision D5: Testcontainers | Phase 7 (7.2) | T-23 to T-26 | ✅ |
| Decision D6: Invite flow | Phase 3 (3.2–3.9) | T-11, T-12, T-25 | ✅ |
| Decision D7: Email domain whitelist | Phase 2 (2.7), Phase 3 (3.5) | T-09, T-10 | ✅ |

---

## Verdict

### ✅ APPROVED

The Sprint 3 plan is complete, technically sound, and ready for implementation. All 10 review checklist items pass. 7 non-blocking warnings are noted as implementation-time improvements — none require plan revision.

**Recommendation:** Proceed to implementation. The implementor should address warnings #3 (logout integration), #4 (portal auth handlers), and #5 (JwtAuthFilter exclusion) during Phase 1 and Phase 5 respectively.

---

## Review Metadata

| Field | Value |
|-------|-------|
| Review iteration | 1 of 3 (max) |
| Plan version reviewed | v2 |
| Time spent | ~15 minutes |
| Confidence | 92% |
| Blocking findings | 0 |
| Non-blocking findings | 7 |
