Files
cannamanage/docs/sprint-2/cannamanage-sprint2-plan.md
T
Patrick Plate 2ede872d11 feat: Sprint 2 REST API layer — full implementation
- Fix critical Hibernate @Filter activation bug (TenantFilterAspect)
- Rename UserRole.ROLE_MANAGER → ROLE_STAFF (future-proofing)
- SecurityConfig: ADMIN + MEMBER roles only for Sprint 2
- AuthController: POST /auth/login + POST /auth/refresh with JWT
- AuthService: login, refresh token rotation, hashed refresh storage
- MemberController: CRUD (GET/POST/PUT /members)
- DistributionController: list + record distributions (CanG §26)
- StockController: batch management (GET/POST /stock/batches)
- ComplianceController: quota check (GET /compliance/quota/{id})
- OpenAPI/Swagger config with bearer-jwt security scheme
- GlobalExceptionHandler: full RFC 9457 problem+json coverage
- UserRepository: findByEmail, findByEmailAndTenantId
- Flyway V2: role rename migration + login indexes
- Testcontainers + test profile infrastructure (integration tests deferred)
- Parent POM: Testcontainers BOM, entity scan via properties

Controllers use validated DTOs (Jakarta Bean Validation records).
Compliance checks run before distribution recording.
Tenant filter AOP aspect ensures multi-tenant data isolation.
2026-06-11 12:05:52 +02:00

21 KiB

CannaManage — Sprint 2 Architecture Review & Implementation Plan

Date: 2026-06-11
Author: Patrick Plate / Lumen (Planner)
Status: Draft v1 — awaiting GO
Branch: sprint/2-api
Sprint Goal: REST API layer + Spring Security + OpenAPI + Tenant Filter integration


1. Current State Assessment

1.1 Sprint 1 Deliverables (Complete )

Component Status Notes
8 JPA entities Club, Member, Batch, Distribution, MonthlyQuota, StockMovement, Strain, User
AbstractTenantEntity Hibernate @Filter + @PrePersist sets tenantId from TenantContext
TenantContext ThreadLocal UUID holder
ComplianceService Full CanG §19 enforcement: daily/monthly/batch/THC checks
5 repositories Member, Distribution, Batch, MonthlyQuota, Strain
25 tests, 100% JaCoCo

1.2 Sprint 2 Draft Code (Exists — Needs Revision)

File Status Issues Identified
JwtService.java ⚠️ Functional but needs decisions Token expiry: 1h access + 30d refresh (matches API spec)
JwtAuthFilter.java ⚠️ Good pattern Sets TenantContext correctly, cleans up in finally
SecurityConfig.java ⚠️ Needs role alignment Uses STAFF role not defined in UserRole enum
GlobalExceptionHandler.java Solid RFC 9457 ProblemDetail pattern, covers key exceptions
application.properties ⚠️ Dev secret hardcoded (acceptable for dev), flyway.enabled=false

2. Architecture Conflicts Identified

🔴 CONFLICT 1: Multi-Tenancy Model Mismatch

Architecture Doc says: Schema-per-tenant (CREATE SCHEMA tenant_{id}, SET search_path)
Sprint 1 implemented: Shared-schema with Hibernate @Filter on tenant_id column

Analysis:
The architecture doc (Section 2) explicitly argues for schema-per-tenant with a TenantRoutingDataSource. However, Sprint 1 actually implemented a shared-schema approach using @FilterDef/@Filter on AbstractTenantEntity. Every entity has a tenant_id column and @PrePersist sets it from TenantContext.

Impact: This is not a bug — it's a pragmatic MVP decision. Schema-per-tenant is operationally complex for a solo dev. The shared-schema + Hibernate filter approach works for MVP scale (<50 clubs). The code is internally consistent.

Decision needed:

  • Option A: Keep shared-schema + @Filter for MVP (recommended — less ops complexity)
  • Option B: Refactor to schema-per-tenant as documented (correct long-term, but 2-3 days of plumbing)

Recommendation: Option A. The Hibernate @Filter approach works and the data model already has tenant_id everywhere. We can migrate to schema-per-tenant in v2 when onboarding > 50 clubs. The critical invariant — tenant isolation — is preserved either way.


🟡 CONFLICT 2: Role Model Divergence

Architecture Doc roles: ROLE_CLUB_ADMIN, ROLE_STAFF, ROLE_MEMBER, ROLE_PREVENTION_OFFICER
API Spec roles: ADMIN, MEMBER (only two roles mentioned in endpoints)
Sprint 1 UserRole enum: ROLE_ADMIN, ROLE_MANAGER, ROLE_MEMBER, ROLE_PREVENTION_OFFICER
SecurityConfig draft: References ADMIN, STAFF, MEMBER

Problems:

  1. ROLE_MANAGER exists in the enum but is not in the architecture doc (which has ROLE_STAFF)
  2. SecurityConfig references STAFF but the enum has MANAGER
  3. The API spec only distinguishes ADMIN and MEMBER — no STAFF endpoints at all
  4. Architecture doc defines a StaffPermission enum with fine-grained permissions — not implemented

Decision needed:

  • Option A: Sprint 2 MVP uses only ADMIN + MEMBER (matches API spec, simplest)
  • Option B: Implement ADMIN + STAFF + MEMBER now (matches architecture doc intent)
  • Option C: Full permission model with StaffPermission JSONB column

Recommendation: Option A for Sprint 2. The API spec only defines ADMIN and MEMBER roles with clear permission boundaries. STAFF with configurable permissions (Option C) is a Sprint 3/4 feature — it requires a staff_accounts table, permission grants, and a custom PermissionEvaluator. The ROLE_PREVENTION_OFFICER is effectively a member with extra read access to under-21 data — can be a flag on the User entity rather than a separate role.


🟡 CONFLICT 3: Token Expiry Discrepancy

Architecture Doc: 8-hour access token
API Spec: 1-hour access token (expiresIn: 3600)
Current Code: 1-hour (matches API spec)

Recommendation: Keep 1-hour. Shorter tokens are more secure for a compliance SaaS handling personal data. The refresh token (30 days) handles UX. The architecture doc value was aspirational; the API spec value is the contract.


🟢 NON-ISSUE: Spring Boot 4.0.6 + Spring Security 7

The current code is already on Boot 4.0.6. Key observations:

  1. SecurityFilterChain pattern — already used correctly (no deprecated WebSecurityConfigurerAdapter)
  2. SessionCreationPolicy.STATELESS — correct for JWT API
  3. JJWT 0.12.6 — latest stable, works with Boot 4
  4. Spring Security 7 modularizationspring-security-access module may be needed if using @PreAuthorize. Boot starter pulls it in transitively.
  5. springdoc-openapi-starter-webmvc-ui — compatible with Boot 4.x (version needs checking in POM)

3. Sprint 2 Scope

IN Scope

# Feature User Stories Priority
1 Auth Controller — login, refresh, logout US-011 Must
2 Member Controller — CRUD + quota endpoint US-002, US-014 Must
3 Distribution Controller — create + list US-003, US-004 Must
4 Stock Controller — strains CRUD, batches CRUD, recall US-005, US-009 Must
5 Compliance Controller — quota check, dry-run check US-004 Must
6 Report Controller — monthly JSON (PDF deferred) US-007 Must (JSON only)
7 Club Controller — /clubs/me, /clubs/me/stats US-006 Must
8 OpenAPI/Swagger UI — full API documentation Must
9 Hibernate Filter activation — enable tenantFilter on every request Must
10 Integration tests — MockMvc + H2 for all controllers Must

OUT of Scope (Sprint 3+)

Feature Reason
STAFF role + StaffPermission model Requires separate table + custom PermissionEvaluator
PDF/CSV report generation Requires iText 7 dependency + report templates
Member portal (session-based auth) Different security chain, Sprint 3
Email notifications Requires Jakarta Mail setup
Stripe integration v2 feature (US-015)
Rate limiting on login Nice-to-have, can add via Spring Cloud Gateway or bucket4j later
CORS configuration Needs frontend domain — add when React app exists
Schema-per-tenant migration v2 operational maturity

4. Architecture Decisions

AD-01: Shared-Schema Multi-Tenancy (Keep Current)

Context: Architecture doc prescribes schema-per-tenant, Sprint 1 uses Hibernate @Filter.
Decision: Keep @Filter approach for MVP.
Rationale: Reduces ops complexity for solo dev. Hibernate filters are reliable when properly activated. The data model is already correct — migration to schema-per-tenant later only requires infrastructure changes, not entity redesign.
Risk: If the filter is not enabled on a session, queries return cross-tenant data. Mitigation: the TenantFilterAspect (new component, see Step 4 below) will guarantee activation.

AD-02: Two-Role Model for Sprint 2

Context: Architecture doc defines 4 roles + configurable staff permissions.
Decision: Sprint 2 uses ADMIN and MEMBER only.
Rationale: API spec only documents these two. SecurityConfig URL rules are sufficient. @PreAuthorize annotations on controller methods provide method-level granularity.
Consequence: Rename UserRole.ROLE_MANAGER → remove it. Keep ROLE_PREVENTION_OFFICER as a marker for later but don't enforce it in Sprint 2 security rules.

AD-03: JwtService as Single Source of Auth Truth

Context: No UserDetailsService loading from DB on every request.
Decision: JWT claims are sufficient for authorization. No DB hit per request.
Rationale: Stateless JWT. The token contains sub (userId), tenant_id, and role. These are all SecurityConfig needs. If we need to check user.active in real-time, we'll add a refresh-token-revocation mechanism (invalidate refresh token → user is forced to re-login within 1 hour).
Tradeoff: A deactivated user remains authenticated until their access token expires (max 1 hour). Acceptable for MVP.

AD-04: ProblemDetail (RFC 9457) for All Errors

Context: API spec defines a custom error format with status, error, code, message, timestamp, path.
Decision: Use Spring's native ProblemDetail + custom properties.
Rationale: Already implemented in GlobalExceptionHandler. Native Spring Boot support since 3.x. The custom error codes (QUOTA_EXCEEDED_MONTHLY, etc.) map to ProblemDetail.setProperty("code", ...).

AD-05: No Custom UserDetailsService

Context: Spring Security typically expects a UserDetailsService bean.
Decision: AuthController handles login directly via repository lookup + BCrypt verification. No Spring Security UserDetailsService.
Rationale: For JWT-only auth, the authentication manager is only needed at login time. The JwtAuthFilter bypasses Spring's standard auth flow entirely — it sets the SecurityContext directly from JWT claims. This is cleaner for stateless APIs.


5. Implementation Steps (Ordered)

Phase 1: Foundation Fixes (Day 1)

Step What Files
1.1 Fix UserRole enum — rename MANAGER→STAFF, keep PREVENTION_OFFICER as future placeholder cannamanage-domain/.../enums/UserRole.java
1.2 Add UserRepository to service module cannamanage-service/.../repository/UserRepository.java
1.3 Create TenantFilterAspect — AOP aspect that enables Hibernate filter on EntityManager before every @Transactional service call cannamanage-service/.../config/TenantFilterAspect.java
1.4 Add updatedAt field to AbstractTenantEntity (currently missing vs. architecture doc) cannamanage-domain/.../entity/AbstractTenantEntity.java
1.5 Create H2 dev profile with spring.flyway.enabled=true + initial migration cannamanage-api/src/main/resources/application-dev.properties, db/migration/V1__init.sql

Phase 2: Auth Controller (Day 1-2)

Step What Files
2.1 Create AuthController with POST /login, POST /refresh, POST /logout cannamanage-api/.../controller/AuthController.java
2.2 Create LoginRequest, LoginResponse, RefreshRequest, RefreshResponse DTOs cannamanage-api/.../dto/auth/
2.3 Create AuthService — handles login logic, password verification, refresh token rotation cannamanage-service/.../AuthService.java
2.4 Add domain exception handlers for QuotaExceededException, MemberNotFoundException, BatchNotFoundException to GlobalExceptionHandler cannamanage-api/.../exception/GlobalExceptionHandler.java

Phase 3: Member Controller (Day 2-3)

Step What Files
3.1 Create MemberController — GET list (paginated), POST create, GET by id, PUT update, DELETE soft-delete cannamanage-api/.../controller/MemberController.java
3.2 Create MemberService — business logic bridge between controller and repositories cannamanage-service/.../MemberService.java
3.3 Create DTOs: CreateMemberRequest, UpdateMemberRequest, MemberResponse, MemberSummaryResponse cannamanage-api/.../dto/member/
3.4 Add GET /members/{id}/quota endpoint (delegates to ComplianceService) MemberController.java
3.5 Add GET /members/me for MEMBER-role self-view MemberController.java

Phase 4: Distribution Controller (Day 3-4)

Step What Files
4.1 Create DistributionController — POST create, GET list (paginated + filtered), GET by id, POST /{id}/notes cannamanage-api/.../controller/DistributionController.java
4.2 Create DistributionService — orchestrates ComplianceService + persists Distribution + updates batch stock cannamanage-service/.../DistributionService.java
4.3 Create DTOs: CreateDistributionRequest, DistributionResponse, AddCorrectionNoteRequest cannamanage-api/.../dto/distribution/
4.4 Implement batch stock decrement on distribution creation (atomic with compliance check) DistributionService.java

Phase 5: Stock Controller (Day 4-5)

Step What Files
5.1 Create StockController — strains CRUD + batches CRUD + recall cannamanage-api/.../controller/StockController.java
5.2 Create StockService — batch management, recall workflow cannamanage-service/.../StockService.java
5.3 Create DTOs: CreateStrainRequest, StrainResponse, CreateBatchRequest, BatchResponse, RecallBatchRequest cannamanage-api/.../dto/stock/
5.4 Implement recall — flags batch, returns affected members StockService.java

Phase 6: Compliance + Club + Reports (Day 5-6)

Step What Files
6.1 Create ComplianceControllerGET /compliance/quota/{memberId}, GET /compliance/check (dry-run) cannamanage-api/.../controller/ComplianceController.java
6.2 Create ClubControllerGET /clubs/me, PUT /clubs/me, GET /clubs/me/stats cannamanage-api/.../controller/ClubController.java
6.3 Create ClubService — club profile + dashboard stats aggregation cannamanage-service/.../ClubService.java
6.4 Create ReportControllerGET /reports/monthly?format=json (JSON only for Sprint 2) cannamanage-api/.../controller/ReportController.java
6.5 Create DTOs for all above cannamanage-api/.../dto/club/, .../dto/compliance/, .../dto/report/

Phase 7: OpenAPI + Integration Tests (Day 6-7)

Step What Files
7.1 Add @Tag, @Operation, @ApiResponse annotations to all controllers All controllers
7.2 Configure OpenApiConfig — info, security scheme, server URL cannamanage-api/.../config/OpenApiConfig.java
7.3 Write AuthControllerTest — MockMvc tests for login/refresh/logout cannamanage-api/src/test/...
7.4 Write MemberControllerTest — CRUD + auth + tenant isolation cannamanage-api/src/test/...
7.5 Write DistributionControllerTest — compliance rejection scenarios cannamanage-api/src/test/...
7.6 Write StockControllerTest — batch lifecycle + recall cannamanage-api/src/test/...
7.7 Write ComplianceControllerTest — dry-run check cannamanage-api/src/test/...
7.8 Ensure JaCoCo stays ≥ 80% on service module Build config

6. Key Technical Patterns

6.1 Tenant Filter Activation (New — Critical)

The Hibernate @Filter requires explicit activation on each Session. Currently nothing activates it. We need:

@Aspect
@Component
@RequiredArgsConstructor
public class TenantFilterAspect {

    private final EntityManager entityManager;

    @Before("@within(org.springframework.stereotype.Service)")
    public void enableTenantFilter() {
        UUID tenantId = TenantContext.getCurrentTenant();
        if (tenantId != null) {
            Session session = entityManager.unwrap(Session.class);
            session.enableFilter("tenantFilter")
                   .setParameter("tenantId", tenantId);
        }
    }
}

Alternative: A HandlerInterceptor that runs after the JwtAuthFilter and activates the filter. Both work — AOP is more robust as it catches service-to-service calls too.

6.2 Controller DTO Mapping Pattern

@RestController
@RequestMapping("/api/v1/members")
@RequiredArgsConstructor
@Tag(name = "Members", description = "Member management")
public class MemberController {

    private final MemberService memberService;

    @PostMapping
    @PreAuthorize("hasRole('ADMIN')")
    @ResponseStatus(HttpStatus.CREATED)
    public MemberResponse create(@Valid @RequestBody CreateMemberRequest request) {
        return memberService.createMember(request);
    }
}

6.3 Pagination Response Pattern

Use Spring's Page<T> mapped to a custom envelope matching the API spec:

public record PageResponse<T>(
    List<T> content,
    int page,
    int size,
    long totalElements,
    int totalPages
) {
    public static <T> PageResponse<T> from(Page<T> page) {
        return new PageResponse<>(
            page.getContent(),
            page.getNumber(),
            page.getSize(),
            page.getTotalElements(),
            page.getTotalPages()
        );
    }
}

7. Risk Assessment

Risk Probability Impact Mitigation
Hibernate @Filter not activated → cross-tenant data leak High (if forgotten) Critical TenantFilterAspect AOP guarantee + integration tests verifying isolation
Spring Security 7 API changes break SecurityConfig Low Medium Code is already using Boot 4.0.6 patterns — verified compatible
springdoc-openapi incompatibility with Boot 4 Medium Low Check version in POM, use 2.8.x which supports Boot 4
JaCoCo coverage drops below 80% with new controllers Medium Low Write MockMvc tests for every endpoint, assert status codes
@PreAuthorize not working without @EnableMethodSecurity Low High Already enabled in SecurityConfig
JWT secret hardcoded in properties committed to Git Medium (already there) Medium Replace with ${JWT_SECRET} env var pattern before any production use

8. Open Questions for Patrick

  1. Tenancy model: Architecture doc says schema-per-tenant, Sprint 1 uses shared-schema + Hibernate @Filter. I recommend keeping @Filter for MVP. Agree?

  2. Role model for Sprint 2: Keep it to ADMIN + MEMBER only? Or do you want STAFF (with basic URL-level permissions, not the full StaffPermission JSONB model) included now?

  3. UserRole cleanup: The current enum has ROLE_MANAGER which doesn't exist in any doc. Replace with ROLE_STAFF? Or just remove and keep ADMIN + MEMBER + PREVENTION_OFFICER for now?

  4. Test database: Use H2 in-memory for Sprint 2 tests, or set up Testcontainers PostgreSQL? H2 is simpler for dev speed; Testcontainers is more realistic but slower.

  5. Flyway migrations: Should I create the full schema migration now (all 8 entities → DDL), or keep ddl-auto=update for rapid iteration and create migrations when stable?

  6. Member portal auth (Sprint 3 preview): The architecture doc and SecurityConfig both hint at a session-based /portal/** chain. Confirm this is deferred to Sprint 3?

  7. Prevention Officer: The architecture doc says this is a separate role with access to under-21 member data. Should this be:

    • A separate UserRole value with specific @PreAuthorize rules?
    • Or a boolean flag on the User/Member that grants additional read access via service-layer checks?

9. Definition of Done — Sprint 2

  • All 7 controllers implemented with full CRUD per API spec
  • JWT auth flow working: login → access token → authenticated requests → refresh → logout
  • Tenant isolation verified: integration tests proving cross-tenant queries return 0 results
  • Compliance checks enforced on distribution creation (fail with 422 + custom error codes)
  • OpenAPI Swagger UI accessible at /swagger-ui.html with all endpoints documented
  • @PreAuthorize annotations on all endpoints with correct role requirements
  • GlobalExceptionHandler covers all domain exceptions with RFC 9457 ProblemDetail
  • MockMvc integration tests for all controllers (≥ 2 tests per endpoint: happy path + auth failure)
  • JaCoCo ≥ 80% on cannamanage-service, ≥ 70% on cannamanage-api
  • No hardcoded secrets in committed code (move JWT secret to env var pattern)
  • Build passes: mvn clean verify

10. Estimated Effort

Phase Effort Cumulative
Phase 1: Foundation Fixes 3-4 hours Day 1
Phase 2: Auth Controller 4-5 hours Day 1-2
Phase 3: Member Controller 4-5 hours Day 2-3
Phase 4: Distribution Controller 4-5 hours Day 3-4
Phase 5: Stock Controller 4-5 hours Day 4-5
Phase 6: Compliance + Club + Reports 4-5 hours Day 5-6
Phase 7: OpenAPI + Tests 6-8 hours Day 6-7
Total ~30-37 hours ~7 working days

This plan covers the REST API backbone. Sprint 3 will address: member portal (session auth), PDF/CSV reports (iText 7), email notifications, and the STAFF permission model.