From 809e19e975b696a306ed14ca459bc02ea38a3227 Mon Sep 17 00:00:00 2001 From: Patrick Plate Date: Fri, 19 Jun 2026 16:43:56 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20comprehensive=20wiki=20overhaul=20?= =?UTF-8?q?=E2=80=94=20Sprint=2014=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 03-Architecture.md | 812 +++++++++++------------------ 08-TestPlan.md | 749 +++++++++++--------------- 09-Deployment.md | 895 ++++++++++---------------------- 10-Retrospective.md | 480 +++++++++-------- 11-Features.md | 275 ++++++++++ CannaManage-03-Architecture.md | 812 +++++++++++------------------ CannaManage-08-TestPlan.md | 749 +++++++++++--------------- CannaManage-09-Deployment.md | 895 ++++++++++---------------------- CannaManage-10-Retrospective.md | 480 +++++++++-------- CannaManage-11-Features.md | 275 ++++++++++ CannaManage-Home.md | 160 +++--- Home.md | 160 +++--- _Sidebar.md | 39 +- 13 files changed, 3049 insertions(+), 3732 deletions(-) create mode 100644 11-Features.md create mode 100644 CannaManage-11-Features.md diff --git a/03-Architecture.md b/03-Architecture.md index 75bb7e6..0420f46 100644 --- a/03-Architecture.md +++ b/03-Architecture.md @@ -1,9 +1,9 @@ # 03 — System Architecture **Project:** CannaManage — B2B SaaS for German Cannabis Social Clubs (Anbauvereinigungen) -**Phase:** 3 of 5 — Staff, Portal & Compliance Reports -**Stack:** Spring Boot 4.0.6 (Java 21) · JPA/Hibernate 7 · PostgreSQL 16 · React/Vite (Sprint 4) -**Last updated:** 2026-06-12 +**Phase:** Sprint 14 — Marketing & Monetization +**Stack:** Spring Boot 4.0.6 (Java 21) · JPA/Hibernate 7 · PostgreSQL 16 · Next.js 15 (React 19) +**Last updated:** 2026-06-19 --- @@ -11,66 +11,70 @@ ```mermaid graph TD - AdminBrowser["🖥️ Browser — Admin Portal"] - MemberBrowser["🖥️ Browser — Member Portal"] + Internet["🌍 Internet"] - Frontend["React/Vite Frontend\n(SPA — served by Nginx)"] - - AdminBrowser -->|HTTPS| Frontend - MemberBrowser -->|HTTPS| Frontend - - Frontend -->|REST/JSON| Backend - - subgraph Backend ["☕ Spring Boot 4.0.6 Application (Java 21)"] - REST["REST API Layer\n/api/v1/"] - Service["Service Layer\n(ComplianceService, ReportService,\nStaffService, PortalService,\nTokenRevocationService…)"] - JPA["JPA / Hibernate 7\nRepositories"] - Security["Dual SecurityFilterChain\nJWT (admin/staff) + Session (portal)"] - Cache["Caffeine Cache\n(token revocation)"] - - REST --> Service - Service --> JPA - Service --> Cache - Security --> REST - end - - JPA -->|JDBC| PG[("🐘 PostgreSQL 16\nmulti-tenant via tenant_id")] - Backend -->|Stripe Java SDK| Stripe["💳 Stripe\n(payment processing)"] - Backend -->|Spring Mail / SMTP| Mail["📧 Spring Mail\n(staff invite emails)"] - Backend -->|OpenPDF| PDF["📄 OpenPDF\n(PDF compliance reports)"] - - Flyway["🔄 Flyway\n(DB migrations)"] -->|applies migrations| PG - - subgraph Hetzner ["🖧 Hetzner VPS — Docker Compose"] - Backend - PG + subgraph TrueNAS ["🖧 TrueNAS Docker — Production"] Nginx["🔒 Nginx\n(reverse proxy + TLS)"] + + subgraph Frontend ["⚛️ Next.js 15 Application"] + Marketing["Marketing Pages\n(Landing, Pricing, Login)"] + AdminUI["Admin Dashboard\n(18 sections)"] + PortalUI["Member Portal\n(self-service)"] + end + + subgraph Backend ["☕ Spring Boot 4.0.6 (Java 21)"] + REST["REST API Layer\n33 controllers · /api/v1/"] + Service["Service Layer\n40+ services"] + JPA["JPA / Hibernate 7\nRepositories"] + Security["Dual SecurityFilterChain\nJWT (admin/staff) + Session (portal)"] + Audit["Audit Log\n(immutable trail)"] + Cache["Caffeine Cache\n(token revocation)"] + end + + PG[("🐘 PostgreSQL 16\nmulti-tenant via schema")] end - Frontend --> Nginx - Nginx --> Backend + Internet -->|"HTTPS :443"| Nginx + Nginx --> Frontend + Nginx -->|"proxy_pass :8080"| Backend + Frontend -->|"REST/JSON"| Backend + + REST --> Service + Service --> JPA + Service --> Cache + Service --> Audit + Security --> REST + JPA -->|"JDBC"| PG + + Backend -->|"Stripe SDK"| Stripe["💳 Stripe\n(SEPA, PayPal, Card)"] + Backend -->|"SMTP"| Mail["📧 Mail\n(notifications, invites)"] + Backend -->|"OpenPDF"| PDF["📄 PDF Reports"] ``` ### Component Responsibilities | Component | Technology | Role | |---|---|---| -| Admin Portal | React/Vite SPA (Sprint 4) | Club management UI | -| Member Portal | Session-based auth (Spring Security) | Member self-service: quota, history, dashboard | -| REST API | Spring Boot 4.0.6 / Spring MVC | All business logic endpoints (9 controllers) | +| Marketing Pages | Next.js 15 (SSR) | Public landing, pricing, login | +| Admin Dashboard | Next.js 15 (CSR) | 18-section club management UI | +| Member Portal | Next.js 15 (CSR) | Member self-service: quota, history, events | +| REST API | Spring Boot 4.0.6 / Spring MVC | 33 controllers, 100+ endpoints | | Auth (Admin/Staff) | Spring Security 7 + JJWT | Stateless JWT authentication | | Auth (Portal) | Spring Security 7 + HttpSession | Session-based member authentication | | Token Revocation | Caffeine cache + DB backing | In-memory revocation check with automatic cleanup | | Staff Permissions | JSONB + annotation checker | 8 granular permissions, role templates | | ORM | JPA / Hibernate 7 | Entity persistence, tenant filtering | -| Database | PostgreSQL 16 | Primary data store (multi-tenant) | -| Migrations | Flyway 10 (V1–V5) | Versioned schema management | -| Payments | Stripe Java SDK | Club subscription billing (Sprint 4+) | -| Email | Spring Mail (SMTP) | Staff invite emails, recall alerts | -| PDF | OpenPDF (iText fork, LGPL) | Compliance report generation (monthly, member-list, recall) | -| CSV | Apache Commons CSV–style | Semicolon-delimited reports, ISO-8859-1 | -| Testing | Testcontainers (PostgreSQL 16) | Real-DB integration tests in Docker | -| Hosting | Hetzner CX21 VPS + Docker Compose | Production deployment | +| Database | PostgreSQL 16 | Primary data store (schema-per-tenant) | +| Migrations | Flyway 10 (V1–V36) | Versioned schema management | +| Payments | Stripe Java SDK | SEPA, PayPal, Credit Card subscriptions | +| Bank Import | Custom parsers | MT940, CAMT053, CSV statement parsing + auto-matching | +| Email | Spring Mail (SMTP) | Notifications, invites, alerts | +| PDF | OpenPDF (iText fork, LGPL) | Compliance report generation | +| CSV | Apache Commons CSV | Semicolon-delimited reports, ISO-8859-1 | +| Audit | Custom audit service | Immutable trail for compliance actions | +| Testing | Testcontainers + JaCoCo | Real-DB integration tests, 80% coverage gate | +| CI/CD | Gitea Actions | PostgreSQL service container, automated pipeline | +| Hosting | TrueNAS Docker + Nginx | Production at cannamanage.plate-software.de | --- @@ -82,29 +86,24 @@ Each club gets its own PostgreSQL schema (e.g. `tenant_abc123`). A platform-leve **Why schema-per-tenant, not shared schema?** -A shared-schema approach (single table with `tenant_id` on every row) is operationally convenient in the short term but creates serious problems at scale: - | Concern | Shared Schema | Schema-Per-Tenant | |---|---|---| | Data isolation | Application-layer only — one missing filter = data leak | Enforced at DB level — schemas are hard boundaries | | DSGVO compliance | Harder to prove isolation; one backup contains all clubs' data | Per-tenant pg_dump; each club's data is cleanly separable | | Deletion / right to erasure | Must `DELETE WHERE tenant_id = ?` across every table | `DROP SCHEMA tenant_abc123 CASCADE` — clean and auditable | -| Migrations | One migration path for all | Per-schema migration via Flyway `schemas` config; adds ~100ms per onboard | +| Migrations | One migration path for all | Per-schema migration via Flyway `schemas` config | | Query performance | Cross-tenant index bloat on large shared tables | Smaller per-tenant tables; no cross-tenant contention | | Future per-club DB isolation | Requires full re-architecture | Trivial: move schema to dedicated DB server | -| Operational overhead | Lower — one connection pool | Slightly higher — one pool per tenant (managed by HikariCP with pool-per-schema) | -**Conclusion:** The shared-schema "MVP convenience" argument only holds for throwaway prototypes. For a compliance SaaS handling personal health-adjacent data (cannabis consumption records), schema-per-tenant is the correct design from Day 1. The migration complexity is manageable; the data isolation benefit is permanent. +**Conclusion:** For a compliance SaaS handling personal health-adjacent data (cannabis consumption records), schema-per-tenant is the correct design from Day 1. The migration complexity is manageable; the data isolation benefit is permanent. ### Tenant Provisioning -When a new club onboards: - ``` POST /api/v1/admin/bootstrap → TenantProvisioningService.provisionTenant(tenantId) → CREATE SCHEMA tenant_{tenantId} - → Flyway.migrate(schema=tenant_{tenantId}) // applies all V*.sql + → Flyway.migrate(schema=tenant_{tenantId}) // applies all V1–V36 → INSERT INTO public.tenants (id, schema_name, onboarded_at, status) ``` @@ -118,75 +117,48 @@ HTTP Request └─ All queries execute in tenant's private schema ``` -### Tenant Resolution - -``` -HTTP Request - └─ Spring Security Filter: extract JWT → resolve tenant_id - └─ TenantContext.setCurrentTenant(tenantId) // ThreadLocal - └─ JPA @Where filter applied on every entity query -``` - -### Code Pattern — Schema Routing DataSource - -```java -// TenantRoutingDataSource.java (pseudocode) -public class TenantRoutingDataSource extends AbstractRoutingDataSource { - - @Override - protected Object determineCurrentLookupKey() { - return TenantContext.getCurrentTenant(); // returns tenant schema name - } -} -``` - -```java -// TenantInterceptor.java (pseudocode) -@Component -public class TenantInterceptor implements HandlerInterceptor { - - @Override - public boolean preHandle(HttpServletRequest req, ...) { - String tenantId = JwtUtils.extractTenantId(req); - TenantContext.setCurrentTenant("tenant_" + tenantId); - return true; - } -} -``` - -**Invariants enforced:** -- Every incoming request resolves its schema before any query runs -- No entity has a `tenant_id` column — schema isolation replaces row-level filtering -- Raw JDBC queries must be avoided; all access goes through JPA repositories with schema routing -- The `public` schema contains only the tenants registry and platform-level config - --- ## 3. Authentication & Authorization -### JWT Token Flow +### Dual SecurityFilterChain -- **Access token:** 8-hour expiry, signed HS256, contains `sub` (userId), `role`, `tenantId` -- **Refresh token:** 30-day expiry, stored in `users.refresh_token_hash` (hashed) -- **Stateless:** No server-side session. JWT is verified on every request by `JwtAuthFilter` +```mermaid +graph LR + subgraph Chain1 ["JWT Chain (Order 1) — /api/v1/**"] + JF[JwtAuthFilter] --> JD[JWT Decode] + JD --> Role[Role Check] + Role --> Perm[Permission Check] + end + + subgraph Chain2 ["Session Chain (Order 2) — /portal/**"] + SF[SessionAuthFilter] --> SC[Session Cookie] + SC --> MR[Member Role] + end +``` + +| Property | JWT Chain | Session Chain | +|----------|-----------|--------------| +| Path | `/api/v1/**` | `/portal/**` | +| Token type | Bearer JWT (Authorization header) | HttpSession cookie | +| Users | Admin, Staff | Members | +| CSRF | Disabled (stateless) | Enabled | +| Expiry | Access: 8h, Refresh: 30d | Session: 24h | ### Roles | Role | Description | Access | |---|---|---| -| `ROLE_CLUB_ADMIN` | Club administrator | Full club management, all members, reports, distributions, staff management | -| `ROLE_STAFF` | Club staff member | Configurable subset of admin permissions — defined per staff account by the admin | -| `ROLE_MEMBER` | Club member | Own quota, own distribution history (read-only) | +| `ROLE_CLUB_ADMIN` | Club administrator | Full club management, all members, reports, distributions, staff, finance | +| `ROLE_STAFF` | Club staff member | Configurable subset of admin permissions | +| `ROLE_MEMBER` | Club member | Own quota, own distribution history, events, forum | | `ROLE_PREVENTION_OFFICER` | Designated prevention officer | Member under-21 reports, prevention data | -> **Staff is a core feature, not an add-on.** Real clubs have multiple staff members (front desk, cultivation responsible, prevention officer designate) with different operational responsibilities. DSGVO requires that each staff member can only access data they need for their specific role. The `ROLE_STAFF` with configurable permission grants from the admin is designed from Phase 0 — retrofitting it later would require schema and API changes. - ### Staff Permission Model -Admins configure staff permissions at account creation. Permissions are stored as a `JSONB` column `granted_permissions` on the `staff_accounts` table within the tenant schema. +8 granular permissions stored as JSONB on `staff_accounts`: ```java -// Configurable staff permissions (granted by admin per staff account) public enum StaffPermission { RECORD_DISTRIBUTION, // can record distributions VIEW_MEMBER_LIST, // can view member roster @@ -199,436 +171,240 @@ public enum StaffPermission { } ``` -Pre-created role templates (configurable by admin): -- **Ausgabe** (Distribution desk): `RECORD_DISTRIBUTION`, `VIEW_MEMBER_LIST`, `VIEW_MEMBER_QUOTA` -- **Lager** (Stock/cultivation): `VIEW_STOCK`, `RECORD_STOCK_IN`, `MANAGE_GROW_CALENDAR` -- **Vorstand** (Board member): all permissions except staff management - -### Service-Layer Authorization Example - -```java -@Service -public class DistributionService { - - @PreAuthorize("hasRole('CLUB_ADMIN')") - public Distribution recordDistribution(RecordDistributionRequest req) { ... } - - @PreAuthorize("hasRole('MEMBER') and #memberId == authentication.principal.memberId") - public QuotaStatus getMyQuota(UUID memberId) { ... } - - @PreAuthorize("hasAnyRole('CLUB_ADMIN', 'PREVENTION_OFFICER')") - public List getUnder21Members() { ... } -} -``` - -### Member Login Sequence - -```mermaid -sequenceDiagram - participant B as Browser - participant API as Spring Boot /api/v1/auth/login - participant DB as PostgreSQL (users table) - participant JWT as JwtService - - B->>API: POST /api/v1/auth/login {email, password} - API->>DB: SELECT * FROM users WHERE email = ? AND active = true - DB-->>API: UserEntity (password_hash, role, tenant_id, member_id) - API->>API: BCrypt.verify(password, password_hash) - alt Invalid credentials - API-->>B: 401 Unauthorized - else Valid - API->>JWT: generateAccessToken(userId, role, tenantId) → 8h - API->>JWT: generateRefreshToken(userId) → 30d - API->>DB: UPDATE users SET refresh_token_hash = ?, last_login = NOW() - DB-->>API: OK - JWT-->>API: accessToken, refreshToken - API-->>B: 200 { accessToken, refreshToken, expiresIn: 28800 } - end -``` - --- -## 4. Data Model (JPA Entities) +## 4. Data Model (57 Entities) -### Entity-Relationship Diagram - -```mermaid -erDiagram - Club { - UUID id PK - UUID tenant_id - string name - string address - string license_number - int max_members - timestamp created_at - enum status - } - - Member { - UUID id PK - UUID tenant_id - UUID club_id FK - string first_name - string last_name - string email - date date_of_birth - date membership_date - string membership_number - enum status - boolean is_under_21 - boolean prevention_officer - } - - Strain { - UUID id PK - UUID tenant_id - string name - decimal thc_percentage - decimal cbd_percentage - string description - } - - Batch { - UUID id PK - UUID tenant_id - UUID strain_id FK - decimal quantity_grams - date harvest_date - string batch_code - enum status - boolean contamination_flag - } - - Distribution { - UUID id PK - UUID tenant_id - UUID member_id FK - UUID batch_id FK - decimal quantity_grams - timestamp distributed_at - UUID recorded_by FK - string notes - boolean immutable - } - - MonthlyQuota { - UUID id PK - UUID tenant_id - UUID member_id FK - int year - int month - decimal total_distributed - decimal max_allowed - } - - StockMovement { - UUID id PK - UUID tenant_id - UUID batch_id FK - enum movement_type - decimal quantity_grams - string reason - timestamp created_at - } - - User { - UUID id PK - UUID tenant_id - UUID member_id FK - string email - string password_hash - enum role - timestamp last_login - boolean active - } - - StaffAccount { - UUID id PK - UUID tenant_id - UUID user_id FK - string display_name - jsonb granted_permissions - boolean active - timestamp created_at - } - - RevokedToken { - UUID id PK - string token_hash - timestamp expires_at - timestamp revoked_at - } - - InviteToken { - UUID id PK - UUID tenant_id - string token - string email - enum target_role - timestamp expires_at - boolean used - } - - Club ||--o{ Member : "has members" - Member ||--o{ Distribution : "receives" - Member ||--o{ MonthlyQuota : "has quota per month" - Member ||--o| User : "may have login" - User ||--o| StaffAccount : "has staff profile" - Strain ||--o{ Batch : "cultivated as" - Batch ||--o{ Distribution : "distributed via" - Batch ||--o{ StockMovement : "tracked in" - Member ||--o{ Distribution : "recorded_by (admin)" -``` - -### Relationship Notes - -| Relationship | Cardinality | Notes | -|---|---|---| -| Club → Member | 1:N | `member.club_id` FK; max enforced by `Club.max_members` | -| Member → Distribution | 1:N | Each distribution targets one member | -| Member → MonthlyQuota | 1:N | One row per `(member_id, year, month)` — UNIQUE constraint | -| Member → User | 1:0..1 | A member may have a portal login; admins may have no `member_id` | -| Strain → Batch | 1:N | Each batch is one strain; a strain can have many batches | -| Batch → Distribution | 1:N | A batch can supply many distributions | -| Batch → StockMovement | 1:N | Every IN/OUT/RECALL against a batch is journaled | -| Distribution.recorded_by → Member | N:1 | The admin who recorded it (audit trail) | - -### Key Constraints - -- `Distribution.immutable = true` by default — records are append-only; no UPDATE/DELETE allowed via API -- `MonthlyQuota` has `UNIQUE(member_id, year, month)` — enforced at DB level -- `Batch.contamination_flag` triggers recall workflow; `Batch.status = RECALLED` is the final state -- All `tenant_id` columns: `NOT NULL`, `updatable = false`, set via `@PrePersist` -- `Member.is_under_21` is derived from `date_of_birth` at registration and re-evaluated on birthday (scheduled job) - ---- - -## 5. API Layer Design - -### Base Path: `/api/v1/` - -All endpoints are RESTful. JSON request/response bodies. JWT Bearer token required on all except `/auth/**`. - -| Controller | Base Path | Auth | Key Endpoints | -|---|---|---|---| -| `AuthController` | `/api/v1/auth` | Public / JWT | `POST /login`, `POST /refresh`, `POST /logout` | -| `ClubController` | `/api/v1/clubs` | JWT (ADMIN) | `GET /me`, `PUT /me`, `GET /me/stats` | -| `MemberController` | `/api/v1/members` | JWT (ADMIN) | `GET /`, `POST /`, `GET /{id}`, `PUT /{id}/status` | -| `DistributionController` | `/api/v1/distributions` | JWT (ADMIN/STAFF) | `POST /`, `GET /?memberId=&month=&year=` | -| `StockController` | `/api/v1/stock` | JWT (ADMIN/STAFF) | `GET /batches`, `POST /batches`, `POST /batches/{id}/recall` | -| `ReportController` | `/api/v1/reports` | JWT (ADMIN/STAFF) | `GET /monthly` (PDF/CSV/JSON), `GET /member-list`, `GET /recall/{batchId}` | -| `ComplianceController` | `/api/v1/compliance` | JWT (ADMIN) | `GET /quota/{memberId}` | -| `StaffController` | `/api/v1/staff` | JWT (ADMIN) | `POST /`, `GET /`, `PUT /{id}`, `DELETE /{id}`, `POST /invite` | -| `PortalController` | `/api/v1/portal` | Session (MEMBER) | `GET /dashboard`, `GET /quota`, `GET /history` | -| `PreventionController` | `/api/v1/prevention` | JWT (ADMIN/PREVENTION) | `POST /officers`, `DELETE /officers/{id}`, `GET /under21` | - -### Standard HTTP conventions -- `201 Created` + `Location` header on resource creation -- `400 Bad Request` with `{ error, message, field? }` on validation failure -- `403 Forbidden` when role/tenant check fails -- `422 Unprocessable Entity` when compliance limits are breached (quota exceeded) -- Pagination: `?page=0&size=20&sort=field,asc` - ---- - -## 6. Compliance Engine - -The `ComplianceService` is the heart of regulatory enforcement. It is called synchronously before every distribution is persisted. All operations run in a single `@Transactional` block with optimistic locking to prevent race conditions under concurrent distribution recording. - -```java -@Service -@Transactional -public class ComplianceService { - - /** - * Validates whether a distribution is legally permitted. - * - * Checks: - * 1. Member is ACTIVE (not SUSPENDED or EXPELLED) - * 2. Daily limit: total distributed today + requestedGrams ≤ 25g - * 3. Monthly limit: MonthlyQuota.total_distributed + requestedGrams ≤ max_allowed - * where max_allowed = 30g (under-21) or 50g (adult) - * 4. Batch is AVAILABLE (not RECALLED or EXHAUSTED) - * 5. Batch has sufficient stock - * - * @throws ComplianceLimitExceededException with remaining quota details - * @throws MemberIneligibleException if member is not ACTIVE - * @throws BatchUnavailableException if batch is recalled or exhausted - */ - public ComplianceCheckResult checkDistributionAllowed( - UUID memberId, UUID batchId, BigDecimal quantityGrams) { ... } - - /** - * Returns remaining quota for the current calendar month. - * Creates a MonthlyQuota row if none exists (lazy initialization). - * - * @return QuotaStatus { totalAllowed, totalUsed, remaining, isUnder21 } - */ - public QuotaStatus getMonthlyRemaining(UUID memberId) { ... } - - /** - * Flags a batch as RECALLED. - * Returns all members who received distributions from this batch - * so the caller can trigger notifications. - * Writes a StockMovement(RECALL) entry. - * - * @return List { memberId, name, email, totalReceived } - */ - public List recallBatch(UUID batchId) { ... } -} -``` - -### Race Condition Prevention - -`MonthlyQuota` rows carry a JPA `@Version` column (optimistic locking). If two concurrent requests attempt to increment `total_distributed` simultaneously, one will receive an `OptimisticLockException` and must retry. The retry logic is handled by a `@Retryable` annotation (Spring Retry, max 3 attempts, 50ms backoff). - -```java -@Entity -public class MonthlyQuota extends AbstractTenantEntity { - - @Version - private Long version; // optimistic lock - - // ... other fields -} -``` - ---- - -## 7. Infrastructure (Hetzner) +### Entity Groups ```mermaid graph TD - Dev["👨‍💻 Developer (Fedora Workstation)"] - Gitea["🏠 Gitea (homelab)\n192.168.188.119:30008"] - Hetzner["☁️ Hetzner VPS CX21\n~€5.88/month"] - - Dev -->|git push| Gitea - Gitea -->|SSH deploy script\n(webhook → deploy.sh)| Hetzner - - subgraph Hetzner - Nginx["🔒 Nginx Container\n(reverse proxy + TLS/Let's Encrypt)"] - App["☕ cannamanage-app\n(Spring Boot JAR)"] - DB[("🐘 cannamanage-db\nPostgreSQL 16")] - - Nginx -->|proxy_pass :8080| App - App -->|JDBC :5432| DB + subgraph Core ["Core Domain"] + Member + Distribution + MonthlyQuota + Batch + Strain + StockMovement end - Internet["🌍 Internet\nHTTPS :443"] -->|HTTPS| Nginx + subgraph Auth ["Authentication"] + User + StaffAccount + RevokedToken + InviteToken + Consent + DeviceRegistration + end + + subgraph Club ["Club Management"] + Club + ClubSettings + BoardMember + end + + subgraph Finance ["Finance"] + Transaction + Expense + MembershipFee + ImportSession + ImportedTransaction + PaymentMatch + Subscription + end + + subgraph Communication ["Communication"] + InfoPost + Event + EventRsvp + ForumThread + ForumPost + Notification + NotificationPreference + end + + subgraph Governance ["Governance"] + Assembly + AgendaItem + Vote + VoteBallot + Document + end + + subgraph Grow ["Cultivation"] + GrowCycle + GrowEntry + SensorReading + PropagationSource + end + + subgraph Compliance ["Compliance"] + ComplianceRecord + ComplianceDeadline + DestructionRecord + TransportRecord + PreventionActivity + Report + AuditLog + end ``` -### Docker Compose Services +### Key Entity Counts by Domain -```yaml -# docker-compose.yml (abbreviated) -services: - cannamanage-app: - image: cannamanage:latest - environment: - SPRING_DATASOURCE_URL: jdbc:postgresql://cannamanage-db:5432/cannamanage - JWT_SECRET: ${JWT_SECRET} - STRIPE_API_KEY: ${STRIPE_API_KEY} - depends_on: [cannamanage-db] - ports: ["127.0.0.1:8080:8080"] - - cannamanage-db: - image: postgres:16-alpine - volumes: [pgdata:/var/lib/postgresql/data] - environment: - POSTGRES_DB: cannamanage - POSTGRES_PASSWORD: ${DB_PASSWORD} - - cannamanage-nginx: - image: nginx:alpine - ports: ["443:443", "80:80"] - volumes: - - ./nginx.conf:/etc/nginx/conf.d/default.conf - - /etc/letsencrypt:/etc/letsencrypt:ro -``` - -### Hetzner Sizing - -| Resource | Spec | Rationale | -|---|---|---| -| Server | CX21 (2 vCPU, 4GB RAM) | Sufficient for < 200 concurrent clubs at MVP | -| Storage | 40GB SSD (included) | PostgreSQL + logs; Hetzner Volumes for backups | -| Backups | Hetzner automated backups (20% surcharge) | Daily snapshot retention 7 days | -| Location | Falkenstein, Germany (FSN1) | DSGVO: data remains within Germany | -| TLS | Let's Encrypt via Certbot | Auto-renew via cron | - -### Deployment Workflow - -``` -git push origin main - → Gitea webhook fires - → deploy.sh on Hetzner: - docker pull cannamanage:latest - docker compose up -d --no-deps cannamanage-app - # zero-downtime: Nginx buffers requests during restart -``` - -Flyway migrations run automatically on application startup (`spring.flyway.enabled=true`). Migration files live at `src/main/resources/db/migration/V*.sql`. +| Domain | Entities | Key Tables | +|--------|----------|-----------| +| Core Operations | 6 | Member, Distribution, MonthlyQuota, Batch, Strain, StockMovement | +| Authentication | 6 | User, StaffAccount, RevokedToken, InviteToken, Consent, DeviceRegistration | +| Club Management | 3 | Club, ClubSettings, BoardMember | +| Finance | 6 | Transaction, Expense, MembershipFee, ImportSession, ImportedTransaction, PaymentMatch | +| Communication | 7 | InfoPost, Event, EventRsvp, ForumThread, ForumPost, Notification, NotificationPreference | +| Governance | 5 | Assembly, AgendaItem, Vote, VoteBallot, Document | +| Cultivation | 4 | GrowCycle, GrowEntry, SensorReading, PropagationSource | +| Compliance | 6 | ComplianceRecord, ComplianceDeadline, DestructionRecord, TransportRecord, PreventionActivity, Report | +| Audit & Billing | 4 | AuditLog, Subscription, StorageQuota, ... | +| **Total** | **~57** | | --- -## 8. Key Design Decisions +## 5. API Layer (33 Controllers) -| Decision | Choice | Rationale | -|---|---|---| -| Multi-tenancy | Schema-per-tenant | Hard data isolation, DSGVO-clean deletion, no cross-tenant query risk | -| Frontend MVP | React/Vite SPA (Sprint 4) | Modern stack; no JSF/PrimeFaces lock-in; easier to hire for; mobile-friendly from day 1 | -| Frontend v2 | Next.js | SSR/ISR for SEO on marketing pages; same React codebase | -| Auth (Admin/Staff) | JWT (stateless) | No sticky sessions needed; horizontal scale ready | -| Auth (Portal) | Session-based (HttpSession) | Simpler for members; no token management needed on member side | -| Dual SecurityFilterChain | Separate filter chains for JWT and session | Clean separation of concerns; each auth mechanism has its own rules | -| PDF generation | OpenPDF (iText fork, LGPL) | No license cost (unlike iText 7 AGPL); mature; sufficient for compliance reports | -| Token revocation | Caffeine in-memory cache + DB | Fast O(1) lookup without Redis dependency; DB for persistence across restarts | -| Staff permissions | JSONB column on `staff_accounts` | Flexible permission model; no join tables needed; easy to query and update | -| Compliance enforcement | Service layer + DB constraint | Belt-and-suspenders: service validates, DB `UNIQUE` prevents duplicates | -| Distribution immutability | `immutable = true`, no DELETE API | Audit trail integrity for regulatory compliance | -| Hosting | Hetzner (Germany) | DSGVO compliance; low cost; German DC | -| Staff roles | Core feature from Phase 0 | DSGVO requires least-privilege access; retrofitting post-MVP too costly | -| Integration testing | Testcontainers (PostgreSQL 16) | Real DB behavior in tests; catches Flyway/JPA issues CI cannot | +### Controller Inventory + +| Group | Controllers | Endpoints | +|-------|-------------|-----------| +| **Auth** | AuthController, ConsentController, DsgvoController | Login, refresh, register, consent management | +| **Members** | MemberController, PortalController | CRUD, quota, self-service | +| **Operations** | DistributionController, StockController, GrowCalendarController | Record distributions, manage inventory, cultivation | +| **Communication** | InfoBoardController, EventController, ForumController | Posts, events, threads | +| **Notifications** | NotificationController, NotificationPreferenceController, NotificationComposeController, DeviceRegistrationController | Push, email, in-app | +| **Finance** | FinanceController, BankImportController, SubscriptionController, StripeWebhookController | Treasury, bank import, billing | +| **Governance** | AssemblyController, BoardController, DocumentController | Assemblies, votes, documents | +| **Compliance** | ComplianceController, ComplianceDashboardController, ComplianceDeadlineController, ComplianceRecordsController, ReportController | Status, deadlines, records, reports | +| **Admin** | StaffController, ClubController, MailSettingsController, StorageController, AuditController | Staff, club config, mail, storage, audit | +| **System** | TestResetController | Test environment reset (non-prod only) | --- -## 9. Dual SecurityFilterChain Pattern (Sprint 3) +## 6. Frontend Architecture -Sprint 3 introduced a dual-chain security architecture to support both stateless API access and stateful member portal sessions: +### Technology Stack -```java -@Configuration -@EnableWebSecurity -public class SecurityConfig { +| Layer | Technology | Purpose | +|-------|-----------|---------| +| Framework | Next.js 15 (App Router) | SSR for marketing, CSR for dashboard | +| UI Library | React 19 | Component rendering | +| Components | shadcn/ui + Radix | Accessible, composable UI primitives | +| Styling | Tailwind CSS 4 | Utility-first styling with dark mode | +| Data | @tanstack/react-query | Server state management, caching | +| Tables | TanStack Table v8 | Sortable, filterable data tables | +| Charts | Recharts | KPI dashboards, quota visualization | +| Forms | React Hook Form + Zod | Type-safe form validation | +| Auth | NextAuth v5 | Session management, JWT relay | +| i18n | next-intl | German / English localization | +| Testing | Vitest + MSW + Playwright | Unit, mock, E2E | - // Chain 1: JWT-based — admin dashboard, staff operations, API - @Bean - @Order(1) - public SecurityFilterChain jwtFilterChain(HttpSecurity http) { - return http - .securityMatcher("/api/v1/**") - .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) - .sessionManagement(s -> s.sessionCreationPolicy(STATELESS)) - .build(); - } +### Route Groups - // Chain 2: Session-based — member portal (form login, CSRF enabled) - @Bean - @Order(2) - public SecurityFilterChain portalFilterChain(HttpSecurity http) { - return http - .securityMatcher("/portal/**") - .formLogin(f -> f.loginPage("/portal/login")) - .sessionManagement(s -> s.maximumSessions(1)) - .build(); - } -} +``` +app/ +├── (marketing)/ → Public: landing, pricing, login +├── (dashboard-layout)/ → Admin: 18 sections with sidebar +│ ├── dashboard/ +│ ├── members/ +│ ├── distributions/ +│ ├── stock/ +│ ├── grow/ +│ ├── info-board/ +│ ├── calendar/ +│ ├── forum/ +│ ├── finance/ +│ │ └── import/ +│ ├── assemblies/ +│ ├── documents/ +│ ├── board/ +│ ├── settings/staff/ +│ ├── compliance/ +│ ├── reports-center/ +│ ├── audit-log/ +│ └── reports/ +└── (portal)/ → Member: self-service with top nav + ├── portal/dashboard/ + ├── portal/history/ + ├── portal/profile/ + └── portal/events/ ``` -**Why two chains?** -- Admin/staff API consumers (React dashboard, mobile apps) expect stateless JWT — no cookies, no CSRF -- Member portal users expect traditional web behavior — login form, sessions, redirects -- Mixing both in one chain leads to config conflicts (CSRF on/off, session policy contradictions) -- Each chain has independent authorization rules and authentication mechanisms +--- + +## 7. Database Migrations (Flyway V1–V36) + +| Range | Sprint | Domain | +|-------|--------|--------| +| V1–V5 | 1–3 | Core schema, members, distributions, stock, staff, club settings | +| V6–V10 | 6 | DSGVO consent, Stripe subscriptions, grow calendar, notifications, PWA | +| V11–V14 | 7 | Info board, events, forum | +| V15–V19 | 8 | Finance (treasury), assemblies, documents, board members | +| V20–V22 | 9 | Reports, compliance records, compliance deadlines | +| V23–V26 | 9 | Destruction records, transport records, propagation sources, prevention activities | +| V27–V29 | 9–10 | Compliance dashboard, bank import, distribution THC/CBD tracking | +| V30–V33 | 10–11 | Import sessions, payment matching, test coverage support | +| V34–V36 | 12–14 | Document integration, storage quotas, marketing/subscription tiers | + +--- + +## 8. Integration Points + +```mermaid +graph LR + CM[CannaManage Backend] + + CM -->|"Stripe SDK"| Stripe["Stripe API\n(subscriptions, webhooks)"] + CM -->|"SMTP"| SMTP["Mail Server\n(notifications, invites)"] + CM -->|"OpenPDF"| PDF["PDF Generation\n(compliance reports)"] + CM -->|"MT940/CAMT053"| Bank["Bank Statement Files\n(uploaded by user)"] + CM -->|"Push API"| Push["Web Push\n(PWA notifications)"] +``` + +| Integration | Protocol | Direction | Purpose | +|-------------|----------|-----------|---------| +| Stripe | REST/SDK | Bidirectional | Subscription billing, webhooks for payment events | +| SMTP | SMTP/TLS | Outbound | Email notifications, staff invites, alerts | +| OpenPDF | Library | Internal | Generate PDF compliance reports | +| Bank Import | File upload | Inbound | MT940, CAMT053, CSV bank statement parsing | +| Web Push | Push API | Outbound | PWA push notifications to registered devices | +| Swagger UI | HTTP | Inbound | API documentation at `/swagger-ui.html` | + +--- + +## 9. Deployment Architecture + +```mermaid +graph TB + Dev["👨‍💻 Dev Workstation\n(macOS)"] + Gitea["🏠 Gitea\n(TrueNAS :30008)"] + Runner["⚙️ Gitea Actions Runner\n(TrueNAS Docker)"] + + Dev -->|"git push"| Gitea + Gitea -->|"triggers"| Runner + Runner -->|"mvn + docker build"| Deploy + + subgraph TrueNAS ["🖧 TrueNAS — Production Docker"] + Deploy["Docker Compose"] + Nginx["Nginx :443"] + App["cannamanage-app :8080"] + FE["cannamanage-frontend :3000"] + DB["PostgreSQL 16 :5432"] + + Deploy --> Nginx + Deploy --> App + Deploy --> FE + Deploy --> DB + Nginx --> App + Nginx --> FE + App --> DB + end + + Internet["🌍 Internet"] -->|"HTTPS"| Nginx +``` + +See [Deployment Guide](CannaManage-09-Deployment) for full production setup details. diff --git a/08-TestPlan.md b/08-TestPlan.md index e82e70d..1d970fd 100644 --- a/08-TestPlan.md +++ b/08-TestPlan.md @@ -1,9 +1,9 @@ # 08 — Test Plan **Project:** CannaManage — B2B SaaS for German Cannabis Social Clubs -**Version:** 3.0 (Sprint 3) -**Date:** 2026-06-12 -**Status:** Active — 67+ automated tests passing +**Version:** 14.0 (Sprint 14) +**Date:** 2026-06-19 +**Status:** Active — 500+ automated tests passing, JaCoCo 80% coverage gate --- @@ -12,16 +12,19 @@ ### 1.1 Testing Pyramid ``` - ┌─────────────────┐ - │ E2E Tests │ 10% — Playwright (deferred to v2) - │ (10%) │ - ├─────────────────┤ - │ Integration │ 20% — Spring Boot Test + Testcontainers - │ Tests (20%) │ - ├─────────────────┤ - │ Unit Tests │ 70% — JUnit 5 + Mockito - │ (70%) │ - └─────────────────┘ + ┌───────────────────┐ + │ E2E / System │ 5% — Playwright (browser automation) + │ Tests │ + ├───────────────────┤ + │ Integration │ 25% — Spring Boot Test + Testcontainers + │ Tests │ + ├───────────────────┤ + │ Frontend Unit │ 20% — Vitest + MSW (React components) + │ Tests │ + ├───────────────────┤ + │ Backend Unit │ 50% — JUnit 5 + Mockito + │ Tests │ + └───────────────────┘ ``` The compliance-critical path (`ComplianceService`) requires **100% line coverage** — no exceptions. Every quota rule is a legal obligation under CanG §§19–22. @@ -30,504 +33,342 @@ The compliance-critical path (`ComplianceService`) requires **100% line coverage | Layer | Tool | Purpose | |-------|------|---------| -| Unit | JUnit 5 (`junit-jupiter`) | Test runner | -| Unit | Mockito 5 | Mock dependencies | -| Unit | AssertJ | Fluent assertions | -| Integration | Spring Boot Test (`@SpringBootTest`) | Full application context | -| Integration | Testcontainers (PostgreSQL module) | Real DB in Docker | -| Integration | MockMvc / RestAssured | HTTP layer testing | -| Coverage | JaCoCo | Line/branch coverage reporting | -| E2E | Playwright (Java) | Browser automation — **deferred to v2** | +| **Backend Unit** | JUnit 5 (`junit-jupiter`) | Test runner | +| **Backend Unit** | Mockito 5 | Mock dependencies | +| **Backend Unit** | AssertJ | Fluent assertions | +| **Backend Integration** | Spring Boot Test (`@SpringBootTest`) | Full application context | +| **Backend Integration** | Testcontainers (PostgreSQL 16) | Real DB in Docker | +| **Backend Integration** | MockMvc / RestAssured | HTTP layer testing | +| **Backend Coverage** | JaCoCo | Line/branch coverage — **80% gate** | +| **Frontend Unit** | Vitest | Fast ESM-native test runner | +| **Frontend Unit** | MSW (Mock Service Worker) | API mocking at network level | +| **Frontend Unit** | Testing Library | Component rendering + queries | +| **Frontend E2E** | Playwright | Browser automation, multi-browser | +| **Frontend E2E** | Playwright Test | Test runner with fixtures + assertions | +| **System Tests** | Playwright + SQL seed | Full-stack with seeded database | -### 1.3 CI Trigger Policy +### 1.3 CI Pipeline Test Flow + +```mermaid +graph LR + Push["git push"] --> CI["Gitea Actions"] + CI --> Backend["Maven Build\n+ Unit Tests\n+ Integration Tests"] + CI --> Frontend["pnpm install\n+ Vitest"] + Backend --> JaCoCo["JaCoCo Report\n80% gate"] + JaCoCo -->|"pass"| Deploy["Deploy"] + JaCoCo -->|"fail"| Block["❌ Block Merge"] + Frontend --> E2E["Playwright E2E\n(on main only)"] + E2E --> Deploy +``` | Branch pattern | Tests run | |---------------|-----------| -| `feature/*` | Unit tests only (`./mvnw test`) | -| `develop` | Unit + Integration (`./mvnw verify -P integration-tests`) | -| `main` | Unit + Integration + coverage gate | - -Coverage gate blocks merge to `main` if `ComplianceService` drops below 100%. +| Feature PR | Backend unit + integration, Frontend Vitest | +| `main` | All above + JaCoCo gate + Playwright E2E | --- -## 2. Unit Test Cases — ComplianceService - -**Class under test:** `de.cannamanage.service.ComplianceService` -**Dependencies mocked:** `DistributionRepository`, `MemberRepository`, `StrainRepository` - ---- - -**TC-001** | `checkDistributionAllowed_givenAdultAtMonthlyLimit_shouldThrowQuotaExceededMonthly` -- **Given:** Adult member (age 35) with exactly 50.0g distributed this calendar month, requesting 1.0g more -- **When:** `complianceService.checkDistributionAllowed(memberId, 1.0)` -- **Then:** Throws `QuotaExceededException` with code `QUOTA_EXCEEDED_MONTHLY` -- **Compliance ref:** CanG §19(2) — 50g/month limit for adults - ---- - -**TC-002** | `checkDistributionAllowed_givenUnder21AtMonthlyLimit_shouldThrowQuotaExceededMonthly` -- **Given:** Under-21 member (age 18) with exactly 30.0g distributed this calendar month, requesting 1.0g more -- **When:** `complianceService.checkDistributionAllowed(memberId, 1.0)` -- **Then:** Throws `QuotaExceededException` with code `QUOTA_EXCEEDED_MONTHLY` -- **Compliance ref:** CanG §19(3) — 30g/month limit for under-21 members - ---- - -**TC-003** | `checkDistributionAllowed_givenAdultAtDailyLimit_shouldThrowQuotaExceededDaily` -- **Given:** Adult member with exactly 25.0g distributed today, requesting 0.5g more -- **When:** `complianceService.checkDistributionAllowed(memberId, 0.5)` -- **Then:** Throws `QuotaExceededException` with code `QUOTA_EXCEEDED_DAILY` -- **Compliance ref:** CanG §19(2) — 25g/day limit - ---- - -**TC-004** | `checkDistributionAllowed_givenUnder21RequestingHighThcStrain_shouldThrowHighThcRestricted` -- **Given:** Under-21 member (age 20), requesting a strain with THC content 12.5% (exceeds 10% threshold) -- **When:** `complianceService.checkDistributionAllowed(memberId, 5.0, strainId)` -- **Then:** Throws `QuotaExceededException` with code `HIGH_THC_RESTRICTED_UNDER_21` -- **Compliance ref:** CanG §19(4) — under-21 members restricted to ≤10% THC strains - ---- - -**TC-005** | `checkDistributionAllowed_givenAdultAt49gMonthlyRequesting2g_shouldThrowQuotaExceededMonthly` -- **Given:** Adult member with 49.0g distributed this month, requesting 2.0g (would total 51.0g) -- **When:** `complianceService.checkDistributionAllowed(memberId, 2.0)` -- **Then:** Throws `QuotaExceededException` with code `QUOTA_EXCEEDED_MONTHLY` -- **Note:** Even partial over-quota requests must be rejected in full; no partial fulfillment - ---- - -**TC-006** | `checkDistributionAllowed_givenAdultAt0gRequesting25g_shouldReturnAllowed` -- **Given:** Adult member with 0.0g distributed this month and today, requesting 25.0g -- **When:** `complianceService.checkDistributionAllowed(memberId, 25.0)` -- **Then:** Returns `DistributionAllowedResult` with `allowed = true`, `remainingDaily = 0.0`, `remainingMonthly = 25.0` -- **Note:** Exactly at daily limit — allowed - ---- - -**TC-007** | `checkDistributionAllowed_givenAdultAt24_9gDailyRequesting0_1g_shouldReturnAllowed` -- **Given:** Adult member with 24.9g distributed today, requesting 0.1g (would reach exactly 25.0g) -- **When:** `complianceService.checkDistributionAllowed(memberId, 0.1)` -- **Then:** Returns `allowed = true`, `remainingDaily = 0.0` -- **Note:** Boundary — exactly at limit is allowed - ---- - -**TC-008** | `checkDistributionAllowed_givenAdultAt24_9gDailyRequesting0_2g_shouldThrowQuotaExceededDaily` -- **Given:** Adult member with 24.9g distributed today, requesting 0.2g (would total 25.1g) -- **When:** `complianceService.checkDistributionAllowed(memberId, 0.2)` -- **Then:** Throws `QuotaExceededException` with code `QUOTA_EXCEEDED_DAILY` -- **Note:** Boundary + 1 — must be blocked - ---- - -**TC-009** | `checkDistributionAllowed_givenSuspendedMember_shouldThrowMemberInactive` -- **Given:** Member with `status = MemberStatus.SUSPENDED`, requesting any amount -- **When:** `complianceService.checkDistributionAllowed(memberId, 1.0)` -- **Then:** Throws `QuotaExceededException` with code `MEMBER_INACTIVE` -- **Note:** Status check must occur before any quota calculation - ---- - -**TC-010** | `checkDistributionAllowed_givenExpelledMember_shouldThrowMemberInactive` -- **Given:** Member with `status = MemberStatus.EXPELLED`, requesting any amount -- **When:** `complianceService.checkDistributionAllowed(memberId, 5.0)` -- **Then:** Throws `QuotaExceededException` with code `MEMBER_INACTIVE` -- **Note:** Expelled members are permanently blocked, no quota check performed - ---- - -## 3. Unit Test Cases — MemberService - -**Class under test:** `de.cannamanage.service.MemberService` -**Dependencies mocked:** `MemberRepository`, `ClubRepository`, `PasswordEncoder` - ---- - -**TC-011** | `createMember_givenAge17_shouldThrowUnderageException` -- **Given:** `CreateMemberRequest` with DOB resulting in age 17 at time of registration -- **When:** `memberService.createMember(request, tenantId)` -- **Then:** Throws `UnderageException` with message containing minimum age (18) -- **Compliance ref:** CanG §6(1) — membership requires minimum age 18 - ---- - -**TC-012** | ~~`createMember_givenAge18_shouldSucceedAndSetIsUnder21False`~~ — *this case is incorrect* - -> **Note:** Age 18 IS under 21, therefore `isUnder21 = true`. See TC-013. - ---- - -**TC-013** | `createMember_givenAge18_shouldSucceedAndSetIsUnder21True` -- **Given:** `CreateMemberRequest` with DOB resulting in age 18 at time of registration -- **When:** `memberService.createMember(request, tenantId)` -- **Then:** Returns created `Member` with `isUnder21 = true`, `status = ACTIVE` -- **Note:** The 18-year-old flag matters for quota enforcement (30g/month, ≤10% THC) - ---- - -**TC-014** | `createMember_givenAge21_shouldSucceedAndSetIsUnder21False` -- **Given:** `CreateMemberRequest` with DOB resulting in age exactly 21 at time of registration -- **When:** `memberService.createMember(request, tenantId)` -- **Then:** Returns created `Member` with `isUnder21 = false`, `status = ACTIVE` -- **Note:** Age 21 is the boundary — exactly 21 qualifies as adult for quota purposes - ---- - -**TC-015** | `createMember_givenDuplicateEmail_shouldThrowDuplicateMemberException` -- **Given:** `MemberRepository.existsByEmailAndTenantId(email, tenantId)` returns `true` -- **When:** `memberService.createMember(request, tenantId)` -- **Then:** Throws `DuplicateMemberException` with code `DUPLICATE_EMAIL` -- **Note:** Email uniqueness is per-tenant, not global - ---- - -## 4. Unit Test Cases — Tenant Isolation - -**Class under test:** JPA repositories with `@TenantAware` filter active -**Setup:** Thread-local `TenantContext` populated via `TenantContextHolder.setTenantId()` - ---- - -**TC-016** | `distributionRepository_givenTenantAContext_shouldNotReturnTenantBData` -- **Given:** Two tenants (Tenant A: UUID-A, Tenant B: UUID-B); 5 distributions for each; `TenantContextHolder` set to UUID-A -- **When:** `distributionRepository.findAll()` -- **Then:** Returns exactly 5 records, all with `tenantId = UUID-A`; zero records from Tenant B -- **Note:** Hibernate filter `tenantFilter` must be enabled in `TenantAwareInterceptor` - ---- - -**TC-017** | `memberRepository_givenTenantAContext_shouldNotSeeClubBMembers` -- **Given:** 10 members in Club A, 8 members in Club B; context set to Club A's tenant -- **When:** `memberRepository.findAll()` -- **Then:** Returns exactly 10 records; no member from Club B present -- **Note:** Cross-tenant data leakage is a GDPR violation, not just a business bug - ---- - -## 5. Integration Test Cases (Testcontainers) - -**Setup:** `@SpringBootTest(webEnvironment = RANDOM_PORT)` with `@Testcontainers`; real PostgreSQL 16 container; Flyway migrations applied before each test class. - ---- - -**TC-018** | `POST /api/v1/distributions — successful distribution recording` -- **Given:** Active adult member with 0g distributed; valid `DistributionRequest` for 10.0g; authenticated as `ROLE_ADMIN` -- **When:** `POST /api/v1/distributions` with valid JWT -- **Then:** HTTP 201; response body contains `distributionId`, `amount = 10.0`, `recordedAt`; DB contains one `distribution` row with `is_recalled = false` - ---- - -**TC-019** | `POST /api/v1/distributions — quota exceeded returns 422` -- **Given:** Adult member with 50.0g already distributed this month; requesting 1.0g more -- **When:** `POST /api/v1/distributions` -- **Then:** HTTP 422; response body `{"errorCode": "QUOTA_EXCEEDED_MONTHLY", "remainingMonthly": 0.0}` - ---- - -**TC-020** | `POST /api/v1/distributions — concurrent race condition (last gram)` -- **Given:** Adult member with 24.9g distributed today; two concurrent requests each for 0.2g (both would individually exceed 25g/day) -- **When:** Both requests fired simultaneously via two threads -- **Then:** Exactly one succeeds (HTTP 201); exactly one fails (HTTP 422 `QUOTA_EXCEEDED_DAILY`); DB total does not exceed 25.0g -- **Mechanism:** `SELECT ... FOR UPDATE` on quota aggregation query prevents double-spend - ---- - -**TC-021** | `POST /api/v1/auth/login — valid credentials return JWT` -- **Given:** Admin user with email `admin@test-club.de`, correct password -- **When:** `POST /api/v1/auth/login` with `{"email": "admin@test-club.de", "password": "..."}` -- **Then:** HTTP 200; response contains `accessToken` (JWT), `tokenType = "Bearer"`, `expiresIn = 3600` - ---- - -**TC-022** | `POST /api/v1/auth/login — invalid credentials return 401` -- **Given:** Admin user exists; wrong password provided -- **When:** `POST /api/v1/auth/login` with wrong password -- **Then:** HTTP 401; response `{"errorCode": "INVALID_CREDENTIALS"}`; no token issued - ---- - -**TC-023** | `GET /api/v1/members — ROLE_MEMBER accessing admin endpoint returns 403` -- **Given:** Authenticated user with `ROLE_MEMBER` JWT (not ROLE_ADMIN) -- **When:** `GET /api/v1/members` (admin-only endpoint) -- **Then:** HTTP 403; response `{"errorCode": "FORBIDDEN"}` - ---- - -**TC-024** | `GET /api/v1/members/{id}/quota — member accessing own quota returns 200` -- **Given:** Authenticated member with JWT; requesting their own `memberId` -- **When:** `GET /api/v1/members/{ownId}/quota` -- **Then:** HTTP 200; response contains `dailyUsed`, `dailyRemaining`, `monthlyUsed`, `monthlyRemaining`, `isUnder21` - ---- - -**TC-025** | `GET /api/v1/members/{otherMemberId}/quota — member accessing other member returns 403` -- **Given:** Authenticated member requesting quota of a *different* member (same club) -- **When:** `GET /api/v1/members/{otherMemberId}/quota` -- **Then:** HTTP 403; GDPR principle: members must not see each other's consumption data - ---- - -**TC-026** | `POST /api/v1/stock/batches/{id}/recall — verify cascade` -- **Given:** Batch `BATCH-TEST-001` with 3 distributions recorded against it; `isRecalled = false` -- **When:** `POST /api/v1/stock/batches/BATCH-TEST-001/recall` with `{"reason": "Contamination detected"}` -- **Then:** HTTP 200; batch `isRecalled = true`; all 3 distribution records have `isRecalled = true`; response body contains list of 3 affected member IDs for notification - ---- - -## 6. Test Data Fixtures - -Define these constants in `src/test/java/de/cannamanage/fixtures/TestFixtures.java`: +## 2. Backend Testing + +### 2.1 Unit Tests (JUnit 5 + Mockito) + +**Target:** All service classes, especially compliance-critical code. + +#### ComplianceService — Critical Path (100% coverage required) + +| TC | Method Under Test | Scenario | Expected | +|----|------------------|----------|----------| +| TC-001 | `checkDistributionAllowed` | Adult at 50g monthly limit + 1g request | `QuotaExceededException(MONTHLY)` | +| TC-002 | `checkDistributionAllowed` | Under-21 at 30g monthly limit + 1g request | `QuotaExceededException(MONTHLY)` | +| TC-003 | `checkDistributionAllowed` | Adult at 25g daily limit + 0.5g request | `QuotaExceededException(DAILY)` | +| TC-004 | `checkDistributionAllowed` | THC >10% for under-21 | `THCLimitExceededException` | +| TC-005 | `checkDistributionAllowed` | Suspended member | `MemberSuspendedException` | +| TC-006 | `checkDistributionAllowed` | Valid request within limits | No exception | +| TC-007 | `checkDistributionAllowed` | New month resets quota | No exception | +| TC-008 | `calculateRemainingQuota` | Mid-month with partial usage | Correct remaining | + +#### Other Service Tests + +| Service | Key Test Cases | +|---------|---------------| +| `DistributionService` | Create, validate batch availability, stock deduction | +| `MemberService` | CRUD, status transitions, age calculation | +| `StockService` | Batch management, movement tracking, low-stock alerts | +| `ReportService` | Monthly report generation, CSV/PDF export | +| `FinanceService` | Transaction recording, balance calculation | +| `BankImportService` | MT940/CAMT053 parsing, auto-matching | +| `AssemblyService` | Vote creation, ballot counting, quorum check | +| `AuditService` | Event recording, immutability verification | +| `NotificationService` | Dispatch, preference filtering, rate limiting | + +### 2.2 Integration Tests (Testcontainers) + +Real PostgreSQL 16 in Docker — catches issues that H2/mocks hide: ```java -public final class TestFixtures { +@SpringBootTest +@Testcontainers +class DistributionIntegrationTest { - // Tenant - public static final UUID TENANT_ID = - UUID.fromString("00000000-0000-0000-0000-000000000001"); - public static final String CLUB_NAME = "Test Cannabis Club e.V."; + @Container + static PostgreSQLContainer pg = new PostgreSQLContainer<>("postgres:16-alpine"); - // Adult member - public static final UUID ADULT_MEMBER_ID = - UUID.fromString("00000000-0000-0000-0000-000000000010"); - public static final String ADULT_MEMBER_NAME = "Klaus Mueller"; - public static final LocalDate ADULT_MEMBER_DOB = - LocalDate.of(1990, 1, 1); // age 36 as of 2026 - - // Under-21 member - public static final UUID UNDER21_MEMBER_ID = - UUID.fromString("00000000-0000-0000-0000-000000000011"); - public static final String UNDER21_MEMBER_NAME = "Lisa Mayer"; - public static final LocalDate UNDER21_MEMBER_DOB = - LocalDate.of(2007, 1, 1); // age 19 as of 2026, isUnder21=true - - // Strain - public static final UUID STRAIN_ID = - UUID.fromString("00000000-0000-0000-0000-000000000020"); - public static final String STRAIN_NAME = "Test OG"; - public static final double STRAIN_THC_PERCENT = 20.0; - public static final double STRAIN_CBD_PERCENT = 1.0; - - // Batch - public static final String BATCH_NUMBER = "BATCH-TEST-001"; - public static final double BATCH_INITIAL_WEIGHT_G = 500.0; - - // Compliance constants (mirror ComplianceConstants.java) - public static final double ADULT_MONTHLY_LIMIT_G = 50.0; - public static final double UNDER21_MONTHLY_LIMIT_G = 30.0; - public static final double DAILY_LIMIT_G = 25.0; - public static final double UNDER21_MAX_THC_PERCENT = 10.0; + @Test + void createDistribution_validRequest_persistsAndUpdatesQuota() { + // Given: member with remaining quota, batch with stock + // When: POST /api/v1/distributions + // Then: distribution persisted, quota updated, stock decremented + } } ``` ---- +**Key integration test scenarios:** -## 7. Coverage Requirements +| Area | Test Cases | +|------|-----------| +| Auth flow | Login → JWT → protected endpoint → 200 | +| Token revocation | Revoke → subsequent request → 401 | +| Tenant isolation | Tenant A data invisible to Tenant B | +| Flyway migrations | All V1–V36 apply cleanly to fresh DB | +| Stripe webhooks | Payment success → subscription activated | +| Bank import | Upload MT940 → parse → auto-match → transactions created | +| Report generation | Generate monthly report → PDF valid | -| Module | Test Type | Minimum Coverage | Enforcement | -|--------|-----------|-----------------|-------------| -| `cannamanage-service` | Unit | 80% line | JaCoCo CI gate | -| `cannamanage-api` | Integration | 70% endpoint coverage | Manual checklist | -| `cannamanage-domain` | Unit | 60% line (entities/enums) | JaCoCo CI gate | -| `ComplianceService` | Unit | **100% line + branch** | JaCoCo CI gate — hard fail | -| `TenantIsolationFilter` | Unit + Integration | 90% line | JaCoCo CI gate | - -> **Rationale for 100% on ComplianceService:** Each uncovered line is a potential legal violation. A missed branch in quota calculation could result in over-distribution — a criminal offence under CanG §34. This is not negotiable. - -### JaCoCo Configuration (`pom.xml`) +### 2.3 Coverage — JaCoCo Configuration ```xml + org.jacoco jacoco-maven-plugin - 0.8.12 - - - jacoco-check - check - - - - CLASS - - de.cannamanage.service.ComplianceService - - - - LINE - COVEREDRATIO - 1.00 - - - BRANCH - COVEREDRATIO - 1.00 - - - - - PACKAGE - - de.cannamanage.service.* - - - - LINE - COVEREDRATIO - 0.80 - - - - - - - + + + + BUNDLE + + + LINE + COVEREDRATIO + 0.80 + + + + + CLASS + + de.cannamanage.service.ComplianceService + + + + LINE + COVEREDRATIO + 1.00 + + + + + ``` +| Rule | Target | Threshold | +|------|--------|-----------| +| Bundle (overall) | All classes | 80% line coverage | +| ComplianceService | Single class | 100% line coverage | +| Controllers | All controllers | 70% (integration tests cover the rest) | + --- -## 8. Test Execution +## 3. Frontend Testing -```bash -# Run all unit tests -./mvnw test -pl cannamanage-service +### 3.1 Unit Tests — Vitest + MSW -# Run integration tests (requires Docker for Testcontainers) -./mvnw verify -P integration-tests +**Config:** `vitest.config.ts` with `jsdom` environment. -# Run specific test class -./mvnw test -pl cannamanage-service -Dtest=ComplianceServiceTest +```typescript +// Example: MemberList component test +import { render, screen } from '@testing-library/react'; +import { server } from '@/mocks/server'; +import { http, HttpResponse } from 'msw'; -# Coverage report (output: target/site/jacoco/index.html) -./mvnw verify jacoco:report +test('renders member list from API', async () => { + server.use( + http.get('/api/v1/members', () => + HttpResponse.json([{ id: 1, firstName: 'Max', lastName: 'Mustermann' }]) + ) + ); -# Coverage report for single module -./mvnw verify jacoco:report -pl cannamanage-service - -# Run compliance tests only (tagged) -./mvnw test -pl cannamanage-service -Dgroups=compliance - -# Check coverage gate (will fail build if thresholds not met) -./mvnw verify -P coverage-check + render(); + expect(await screen.findByText('Max Mustermann')).toBeInTheDocument(); +}); ``` -### Testcontainers Docker requirement +**MSW handler pattern:** Mirrors the real API contract from `src/types/api.ts` — tests validate against the actual interface. -Integration tests require Docker available on the host. The PostgreSQL container is pulled automatically by Testcontainers on first run. Ensure: -- Docker daemon running: `systemctl start docker` (or `docker info`) -- User in `docker` group: `sudo usermod -aG docker $USER` +| Area | Key Test Cases | +|------|---------------| +| Components | Render, user interaction, conditional display | +| Hooks | React Query hooks with MSW responses | +| Forms | Validation, submission, error display | +| Auth | Protected route redirection, token refresh | +| i18n | German + English text rendering | -### Test annotation conventions +### 3.2 E2E Tests — Playwright -```java -// Unit test — no Spring context -@ExtendWith(MockitoExtension.class) -class ComplianceServiceTest { ... } +**Config:** `playwright.config.ts` — Chromium, Firefox, WebKit. -// Integration test — full context + Testcontainers -@SpringBootTest -@Testcontainers -@ActiveProfiles("test") -class DistributionIntegrationTest { ... } +```typescript +// Example: Distribution recording flow +test('admin records a distribution', async ({ page }) => { + await page.goto('/dashboard/distributions'); + await page.click('[data-testid="new-distribution"]'); + + // Step 1: Select member + await page.fill('[data-testid="member-search"]', 'Max'); + await page.click('[data-testid="member-option-1"]'); + + // Step 2: Quota check shown + await expect(page.locator('[data-testid="remaining-quota"]')).toBeVisible(); + + // Step 3: Select batch + amount + await page.selectOption('[data-testid="batch-select"]', 'batch-001'); + await page.fill('[data-testid="amount"]', '5.0'); + + // Step 4: Confirm + await page.click('[data-testid="confirm-distribution"]'); + await expect(page.locator('[data-testid="success-toast"]')).toBeVisible(); +}); +``` -// Tag compliance tests for selective execution -@Tag("compliance") -@Test -void checkDistributionAllowed_givenAdultAtMonthlyLimit_shouldThrowQuotaExceededMonthly() { ... } +**E2E test coverage:** + +| Flow | What's tested | +|------|--------------| +| Login (admin) | JWT auth, redirect to dashboard | +| Login (member) | Session auth, redirect to portal | +| Distribution recording | 4-step form, quota enforcement | +| Member management | CRUD, search, filter | +| Stock management | Add batch, view movements | +| Report generation | Select type, generate, download | +| Staff invite | Create invite, permission editor | +| Payment import | Upload file, review matches, confirm | + +### 3.3 System Tests (SQL Seed + Playwright) + +Full-stack tests that seed the database via SQL, then drive the application through the browser: + +```typescript +test.describe('System: Distribution Compliance', () => { + test.beforeAll(async () => { + // Seed database with member at 49g/month + await seedDatabase('test-data/member-near-limit.sql'); + }); + + test('blocks distribution exceeding monthly quota', async ({ page }) => { + await loginAsAdmin(page); + await page.goto('/dashboard/distributions'); + // Try to distribute 2g (would exceed 50g limit) + // ... + await expect(page.locator('[data-testid="quota-error"]')).toContainText('50g'); + }); +}); ``` --- -## 7. Sprint 3 Integration Tests (Testcontainers) +## 4. Test Data Strategy -Sprint 3 added 30+ integration tests using **Testcontainers** with a real PostgreSQL 16 instance in Docker. These tests exercise the full Spring Boot context including Flyway migrations, JPA repositories, security filters, and HTTP endpoints. +| Environment | Data Source | Lifecycle | +|-------------|-----------|-----------| +| Unit tests | Mocked (Mockito / MSW) | Per-test | +| Integration tests | Testcontainers (fresh DB each run) | Per-class | +| E2E tests | SQL seed files | Per-suite | +| System tests | SQL seed + API-driven setup | Per-suite | -### 7.1 Base Class Pattern +### Test Data Files -```java -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Testcontainers -@ActiveProfiles("integration") -public abstract class AbstractIntegrationTest { - - @Container - static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16-alpine"); - - @DynamicPropertySource - static void configureProperties(DynamicPropertyRegistry registry) { - registry.add("spring.datasource.url", postgres::getJdbcUrl); - registry.add("spring.datasource.username", postgres::getUsername); - registry.add("spring.datasource.password", postgres::getPassword); - } - - @Autowired - protected TestRestTemplate restTemplate; - - protected HttpHeaders adminHeaders() { /* JWT for ADMIN role */ } - protected HttpHeaders staffHeaders(String... permissions) { /* JWT for STAFF role */ } -} +``` +cannamanage-api/src/test/resources/ +├── test-data/ +│ ├── member-near-limit.sql +│ ├── full-club-setup.sql +│ ├── bank-import-mt940.txt +│ └── bank-import-camt053.xml ``` -### 7.2 Sprint 3 Test Classes +--- -| Test Class | Tests | What It Verifies | -|-----------|-------|-----------------| -| `AuthIntegrationTest` | 8 | Login, refresh, logout, token revocation, password change invalidation | -| `PortalIntegrationTest` | 5 | Member session auth, dashboard, quota, history, session expiry | -| `ReportIntegrationTest` | 5 | Monthly PDF/CSV/JSON generation, member-list export, recall report | -| `StaffPermissionIntegrationTest` | 6 | Permission enforcement, role templates, CRUD lifecycle, invite flow | -| `TenantIsolationTest` | 3 | Cross-tenant data cannot be accessed via any endpoint | -| `TokenRevocationIntegrationTest` | 4 | Logout revokes token, password change revokes all, cache + DB sync | +## 5. Quality Gates -### 7.3 Sprint 3 Unit Tests +| Gate | Threshold | Blocks | +|------|-----------|--------| +| JaCoCo overall | ≥ 80% line | PR merge | +| JaCoCo ComplianceService | 100% line | PR merge | +| Backend tests | All pass | PR merge | +| Frontend Vitest | All pass | PR merge | +| Playwright E2E | All pass | Deploy to prod | +| Build success | mvn clean verify | Everything | -| Test Class | Tests | What It Verifies | -|-----------|-------|-----------------| -| `StaffServiceTest` | 5 | Permission assignment, role templates, CRUD operations | -| `TokenRevocationServiceTest` | 4 | Cache behavior, DB persistence, cleanup scheduler | -| `PortalServiceTest` | 4 | Dashboard aggregation, quota calculation, history pagination | -| `PreventionOfficerServiceTest` | 3 | Designation, revocation, limit enforcement | -| `EmailServiceTest` | 2 | Invite email formatting, SMTP integration | -| `ReportServiceTest` | 4 | Report aggregation logic for all 3 report types | -| `PdfReportGeneratorTest` | 3 | PDF generation, page numbering, content correctness | -| `StaffPermissionCheckerTest` | 3 | Annotation-based permission enforcement | +--- -### 7.4 Combined Test Count +## 6. Running Tests Locally -| Category | Sprint 1 | Sprint 2 | Sprint 3 | Total | -|----------|---------|---------|---------|-------| -| Unit tests (Mockito) | 25 | 0 | 28 | 53 | -| Controller tests (MockMvc) | 0 | 12 | 0 | 12 | -| Integration tests (Testcontainers) | 0 | 0 | 31 | 31 | -| **Total** | **25** | **12** | **59** | **67+** | - -### 7.5 Running Integration Tests +### Backend ```bash -# Run all tests (unit + integration) -./mvnw verify -P integration-tests +# All tests +mvn clean verify -# Run only integration tests -./mvnw test -pl cannamanage-api -Dtest="*IntegrationTest" +# Unit only (fast) +mvn test -# Run specific integration test class -./mvnw test -pl cannamanage-api -Dtest="TokenRevocationIntegrationTest" +# Integration only +mvn verify -DskipUnitTests + +# Single test class +mvn test -Dtest=ComplianceServiceTest + +# With coverage report +mvn verify -Pjacoco +open target/site/jacoco/index.html ``` -**Prerequisites:** -- Docker daemon running (Testcontainers requirement) -- At least 2GB free RAM (PostgreSQL container + Spring Boot context) -- Port 5432 does NOT need to be free — Testcontainers uses random ports +### Frontend -### 7.6 Coverage Goals (Updated) +```bash +cd cannamanage-frontend -| Component | Target | Current | Notes | -|-----------|--------|---------|-------| -| `ComplianceService` | 100% | 100% | Legal requirement — no exceptions | -| `TokenRevocationService` | 95% | 95% | Security-critical path | -| `StaffPermissionChecker` | 90% | 92% | Permission enforcement | -| `ReportService` | 85% | 87% | All 3 report types covered | -| `PortalService` | 80% | 83% | Member self-service logic | -| Overall project | 70% | 74% | Balanced coverage across modules | +# Unit tests (Vitest) +pnpm test + +# Watch mode +pnpm test:watch + +# Coverage +pnpm test:coverage + +# E2E (Playwright) +pnpm exec playwright test + +# E2E with UI +pnpm exec playwright test --ui + +# Specific test file +pnpm exec playwright test tests/distribution.spec.ts +``` + +--- + +## 7. Test Metrics (Current) + +| Metric | Value | +|--------|-------| +| Total automated tests | 500+ | +| Backend unit tests | ~250 | +| Backend integration tests | ~100 | +| Frontend Vitest tests | ~100 | +| Playwright E2E tests | ~50 | +| JaCoCo overall coverage | ~82% | +| ComplianceService coverage | 100% | +| CI pipeline duration | ~4 minutes | +| Flaky test rate | < 1% | diff --git a/09-Deployment.md b/09-Deployment.md index 960e5a1..a5dfd05 100644 --- a/09-Deployment.md +++ b/09-Deployment.md @@ -1,96 +1,85 @@ # 09 — Deployment Guide **Project:** CannaManage — B2B SaaS for German Cannabis Social Clubs -**Version:** 3.0 (Sprint 3) -**Date:** 2026-06-12 -**Target environment:** Hetzner VPS — Ubuntu 22.04 LTS — Docker Compose (Release) | TrueNAS.local — Docker (Build/CI) +**Version:** 14.0 (Sprint 14) +**Date:** 2026-06-19 +**Target environment:** TrueNAS Docker — Docker Compose (Production) | Gitea Actions (CI/CD) --- -## 1. Prerequisites - -### Hetzner VPS Specification - -| Resource | Value | Monthly Cost | -|----------|-------|-------------| -| Server type | CX21 | ~€5.88/month | -| vCPU | 2 | — | -| RAM | 4 GB | — | -| SSD | 40 GB | — | -| Network | 20 TB transfer | — | -| OS | Ubuntu 22.04 LTS | — | - -> **Scale-up trigger:** Upgrade to CX31 (8GB RAM) when concurrent active clubs exceeds 20. PostgreSQL is the memory consumer — headroom is consumed by connection pools, not application heap. - -### DNS Setup - -| Record | Type | Value | -|--------|------|-------| -| `cannamanage.de` | A | `` | -| `app.cannamanage.de` | A | `` | -| `*.cannamanage.de` | A | `` | - -Wildcard A record enables future per-club subdomains (`clubname.cannamanage.de`) without additional DNS changes. - -### Required Software - -- Docker Engine 24+ (`docker.io` or Docker CE) -- Docker Compose v2 (`docker compose` — not `docker-compose`) -- Certbot with Nginx plugin (`python3-certbot-nginx`) -- OpenSSH server (enabled by default on Ubuntu) - ---- - -## 2. Infrastructure Architecture +## 1. Infrastructure Overview ```mermaid graph TB - Dev["👨‍💻 Dev Workstation\n(Fedora, 192.168.188.x)"] - Gitea["🏠 Gitea\n(truenas.local:30008)"] - TrueNAS["🖧 TrueNAS.local Docker\n(192.168.188.119)\nBuild + Staging"] - Hetzner["☁️ Hetzner VPS CX21\nProduction Release"] + Dev["👨‍💻 Dev Workstation\n(macOS)"] + Gitea["🏠 Gitea\n(TrueNAS :30008)"] + Runner["⚙️ Gitea Actions Runner\n(TrueNAS Docker)"] Dev -->|"git push"| Gitea - Gitea -->|"Gitea Actions runner\n(on TrueNAS.local)"| TrueNAS - TrueNAS -->|"mvn package + docker build"| TrueNAS - TrueNAS -->|"docker save | scp\n(on merge to main)"| Hetzner + Gitea -->|"triggers CI"| Runner + Runner -->|"build + test + deploy"| Prod - subgraph TrueNAS ["TrueNAS.local — CI/CD Build Environment"] - GiteaRunner["Gitea Actions Runner"] - BuildCache["Maven .m2 cache\n(persistent volume)"] - StagingDB["PostgreSQL staging\n(ephemeral)"] + subgraph Prod ["🖧 TrueNAS — Production (Docker Compose)"] + Nginx["🔒 Nginx\nreverse proxy + TLS\n:443 → :8080/:3000"] + Backend["☕ cannamanage-app\nSpring Boot 4.0.6\n:8080"] + Frontend["⚛️ cannamanage-frontend\nNext.js 15\n:3000"] + DB["🐘 PostgreSQL 16\n:5432\n(persistent volume)"] + + Nginx --> Backend + Nginx --> Frontend + Backend --> DB end - subgraph Hetzner ["Hetzner VPS — Production Release Environment"] - Nginx["Nginx (reverse proxy + TLS)"] - App["cannamanage-app\n(Spring Boot 3.x)"] - DB["PostgreSQL 16\n(persistent pgdata volume)"] - Nginx -->|"proxy_pass :8080"| App - App -->|"JDBC :5432"| DB - end - - Internet["🌍 Internet HTTPS"] -->|"port 443"| Nginx + Internet["🌍 Internet\nhttps://cannamanage.plate-software.de"] -->|":443"| Nginx ``` -### Environment Roles +### Environment Summary | Environment | Host | Purpose | |---|---|---| -| **Development** | Dev workstation (Fedora) | Local feature development, unit tests | -| **Build / CI** | TrueNAS.local Docker | Gitea Actions runner; Maven build; integration tests (Testcontainers); Docker image build | -| **Production / Release** | Hetzner VPS CX21 | Live clubs, real data; Hetzner = our release environment | +| **Development** | macOS (local) | Feature development, unit tests, `docker-compose.yml` | +| **CI/CD** | TrueNAS Gitea Actions Runner | Automated build, test (PostgreSQL service container), deploy | +| **Production** | TrueNAS Docker Compose | Live application at `cannamanage.plate-software.de` | -All three services on Hetzner run on an internal Docker bridge network (`cannamanage_net`). Only Nginx is exposed to the public internet. PostgreSQL has no external port binding. +> **Note:** The project previously planned Hetzner VPS hosting but migrated to TrueNAS Docker for cost savings and network locality. The production URL is forwarded to the TrueNAS instance via port forwarding. --- -## 3. Docker Compose Setup +## 2. Prerequisites -**File:** `/opt/cannamanage/docker-compose.yml` +### TrueNAS Docker Host + +| Resource | Value | +|----------|-------| +| Platform | TrueNAS Scale (Docker) | +| Docker Engine | 24+ | +| Docker Compose | v2 (compose plugin) | +| RAM allocated | 4 GB+ for all containers | +| Storage | Persistent volumes on ZFS pools | + +### DNS / Networking + +| Record | Value | +|--------|-------| +| Domain | `cannamanage.plate-software.de` | +| TLS | Let's Encrypt via Nginx (certbot) | +| Port forwarding | Router :443 → TrueNAS :443 | + +### Required Software (on TrueNAS) + +- Docker Engine 24+ +- Docker Compose v2 +- Gitea (self-hosted, port 30008) +- Gitea Actions Runner (registered) +- Certbot / Nginx for TLS + +--- + +## 3. Docker Compose — Production + +**File:** `docker-compose.truenas.yml` ```yaml -version: '3.9' - networks: cannamanage_net: driver: bridge @@ -98,349 +87,171 @@ networks: volumes: pgdata: driver: local - nginx_certs: - driver: local services: - nginx: - image: nginx:1.25-alpine - container_name: cannamanage-nginx - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - - ./nginx/conf.d:/etc/nginx/conf.d:ro - - nginx_certs:/etc/letsencrypt:ro - - /var/log/nginx:/var/log/nginx - depends_on: - app: - condition: service_healthy - networks: - - cannamanage_net - restart: unless-stopped - - app: - image: cannamanage:${VERSION:-latest} - container_name: cannamanage-app - environment: - - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/cannamanage - - SPRING_DATASOURCE_USERNAME=${DB_USERNAME} - - SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD} - - APP_JWT_SECRET=${JWT_SECRET} - - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY} - - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} - - SPRING_MAIL_HOST=${MAIL_HOST} - - SPRING_MAIL_USERNAME=${MAIL_USERNAME} - - SPRING_MAIL_PASSWORD=${MAIL_PASSWORD} - - SENTRY_DSN=${SENTRY_DSN} - - SPRING_PROFILES_ACTIVE=production - depends_on: - db: - condition: service_healthy - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 60s - networks: - - cannamanage_net - restart: unless-stopped - db: image: postgres:16-alpine container_name: cannamanage-db environment: - - POSTGRES_DB=cannamanage - - POSTGRES_USER=${DB_USERNAME} - - POSTGRES_PASSWORD=${DB_PASSWORD} + POSTGRES_DB: cannamanage + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - pgdata:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d cannamanage"] - interval: 10s - timeout: 5s - retries: 5 networks: - cannamanage_net restart: unless-stopped - # PostgreSQL port intentionally NOT exposed externally + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"] + interval: 10s + timeout: 5s + retries: 5 + + backend: + build: + context: . + dockerfile: Dockerfile.backend + container_name: cannamanage-app + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/cannamanage + SPRING_DATASOURCE_USERNAME: ${DB_USER} + SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD} + SPRING_PROFILES_ACTIVE: production + JWT_SECRET: ${JWT_SECRET} + STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY} + STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET} + MAIL_HOST: ${MAIL_HOST} + MAIL_USERNAME: ${MAIL_USERNAME} + MAIL_PASSWORD: ${MAIL_PASSWORD} + ports: + - "8080:8080" + depends_on: + db: + condition: service_healthy + networks: + - cannamanage_net + restart: unless-stopped + + frontend: + build: + context: ./cannamanage-frontend + dockerfile: Dockerfile + container_name: cannamanage-frontend + environment: + NEXTAUTH_URL: https://cannamanage.plate-software.de + NEXTAUTH_SECRET: ${NEXTAUTH_SECRET} + NEXT_PUBLIC_API_URL: https://cannamanage.plate-software.de/api + ports: + - "3000:3000" + depends_on: + - backend + networks: + - cannamanage_net + restart: unless-stopped + + nginx: + image: nginx:alpine + container_name: cannamanage-nginx + ports: + - "443:443" + - "80:80" + volumes: + - ./deploy/nginx/cannamanage.conf:/etc/nginx/conf.d/default.conf:ro + - /etc/letsencrypt:/etc/letsencrypt:ro + depends_on: + - backend + - frontend + networks: + - cannamanage_net + restart: unless-stopped ``` -**Nginx site config** (`/opt/cannamanage/nginx/conf.d/cannamanage.conf`): +--- + +## 4. Nginx Configuration + +**File:** `deploy/nginx/cannamanage.conf` ```nginx server { listen 80; - server_name app.cannamanage.de; - return 301 https://$host$request_uri; + server_name cannamanage.plate-software.de; + return 301 https://$server_name$request_uri; } server { - listen 443 ssl http2; - server_name app.cannamanage.de; + listen 443 ssl; + server_name cannamanage.plate-software.de; - ssl_certificate /etc/letsencrypt/live/app.cannamanage.de/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/app.cannamanage.de/privkey.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; + ssl_certificate /etc/letsencrypt/live/cannamanage.plate-software.de/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/cannamanage.plate-software.de/privkey.pem; - # Security headers - add_header Strict-Transport-Security "max-age=31536000" always; - add_header X-Frame-Options DENY; - add_header X-Content-Type-Options nosniff; - - location / { - proxy_pass http://app:8080; + # Backend API + location /api/ { + proxy_pass http://backend:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 60s; } - # Stripe webhook — allow larger body - location /api/v1/billing/webhook { - proxy_pass http://app:8080; + # Swagger UI + location /swagger-ui/ { + proxy_pass http://backend:8080; proxy_set_header Host $host; - client_max_body_size 1m; + } + + location /v3/api-docs { + proxy_pass http://backend:8080; + proxy_set_header Host $host; + } + + # Frontend (everything else) + location / { + proxy_pass http://frontend:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; } } ``` --- -## 4. Environment Variables +## 5. CI/CD — Gitea Actions -**File:** `/opt/cannamanage/.env` (never committed to git — add to `.gitignore`) +**File:** `.gitea/workflows/ci.yml` -```bash -# Database -DB_USERNAME=cannamanage_user -DB_PASSWORD= - -# JWT signing key (256-bit minimum — generate with: openssl rand -hex 32) -JWT_SECRET=<256-bit-random-hex> - -# Stripe (use sk_live_ for production, sk_test_ for staging) -STRIPE_SECRET_KEY=sk_live_... -STRIPE_WEBHOOK_SECRET=whsec_... - -# Email (SMTP) -MAIL_HOST=smtp.example.com -MAIL_PORT=587 -MAIL_USERNAME=noreply@cannamanage.de -MAIL_PASSWORD= -MAIL_FROM=CannaManage - -# Error tracking -SENTRY_DSN=https://@.ingest.sentry.io/ - -# Application version (set by CI during deploy) -VERSION=latest -``` - -> **Security:** Never store `.env` in version control. Use Gitea repository secrets for CI and inject at deploy time. On the VPS, set file permissions: `chmod 600 /opt/cannamanage/.env`. - -### 4.1 SMTP Configuration (Sprint 3 — Staff Invite Flow) - -The `EmailService` uses Spring Mail to send staff invite emails. Configure the following environment variables: - -| Variable | Required | Example | Notes | -|----------|----------|---------|-------| -| `MAIL_HOST` | ✅ | `smtp.mailgun.org` | SMTP server hostname | -| `MAIL_PORT` | ✅ | `587` | Use 587 for STARTTLS, 465 for implicit TLS | -| `MAIL_USERNAME` | ✅ | `noreply@cannamanage.de` | SMTP auth username | -| `MAIL_PASSWORD` | ✅ | `` | SMTP auth password | -| `MAIL_FROM` | ✅ | `CannaManage ` | From address shown to recipients | - -**Spring Boot properties mapping:** - -```properties -spring.mail.host=${MAIL_HOST} -spring.mail.port=${MAIL_PORT:587} -spring.mail.username=${MAIL_USERNAME} -spring.mail.password=${MAIL_PASSWORD} -spring.mail.properties.mail.smtp.auth=true -spring.mail.properties.mail.smtp.starttls.enable=true -``` - -**Deliverability recommendations:** -- Set up SPF record: `v=spf1 include:mailgun.org ~all` -- Set up DKIM signing via your mail provider -- Use a dedicated subdomain for transactional email: `mail.cannamanage.de` -- Fallback: if SMTP is unavailable, invite tokens can still be generated and the URL shared manually by the admin - -### 4.2 Caffeine Cache (Token Revocation — No External Dependencies) - -Sprint 3 introduced token revocation using **Caffeine** (in-process Java cache). This requires **no external infrastructure** — no Redis, no Memcached, no additional Docker containers. - -**How it works:** -- Revoked tokens are stored in both the `revoked_tokens` PostgreSQL table and an in-memory Caffeine cache -- Each entry has a TTL matching the token's original expiry time (so cache entries auto-evict when they'd be expired anyway) -- On application restart, the cache is cold but the DB is the source of truth — first check hits DB, subsequent checks hit cache -- `TokenCleanupScheduler` runs daily at 03:00 to delete expired entries from the DB - -**Memory impact:** Negligible. Each cache entry is ~200 bytes (token hash + expiry timestamp). Even with 10,000 revoked tokens, total memory usage is < 2MB. - -**Configuration (application.properties):** - -```properties -# Token revocation cache settings -app.token-revocation.cache-max-size=10000 -app.token-revocation.cleanup-cron=0 0 3 * * * -``` - -**No Redis needed:** For a single-instance deployment (which CannaManage will be for the foreseeable future), Caffeine is the correct choice. Redis would be needed only if we scale to multiple application instances that need shared revocation state. That's a Sprint 6+ concern at earliest. - ---- - -## 5. First-Time Deployment - -### Step 1 — Create Hetzner VPS - -1. Log into [console.hetzner.cloud](https://console.hetzner.cloud) -2. Create server: **CX21**, Ubuntu 22.04, Nuremberg or Frankfurt datacenter -3. Add your SSH public key during setup (`cat ~/.ssh/id_ed25519.pub`) -4. Note the assigned IPv4 address — update DNS A records - -### Step 2 — Install Docker + Docker Compose - -```bash -ssh root@ - -# Update system -apt update && apt upgrade -y - -# Install Docker -curl -fsSL https://get.docker.com | sh - -# Add deploy user (never run production as root) -adduser deploy -usermod -aG docker deploy -usermod -aG sudo deploy - -# Install Certbot -apt install -y python3-certbot-nginx certbot -``` - -### Step 3 — Clone Repository - -```bash -su - deploy -mkdir -p /opt/cannamanage -cd /opt/cannamanage -git clone http://192.168.188.119:30008/pplate/cannamanage.git . -# Or from public mirror when available -``` - -### Step 4 — Create Production `.env` - -```bash -cd /opt/cannamanage -cp .env.example .env -nano .env # Fill in all production secrets -chmod 600 .env -``` - -### Step 5 — Obtain SSL Certificate - -```bash -# Stop anything on port 80 first (nothing should be running yet) -certbot certonly --standalone \ - -d app.cannamanage.de \ - --non-interactive \ - --agree-tos \ - -m ssl@cannamanage.de - -# Symlink certs into nginx_certs volume location -# Certbot places certs at /etc/letsencrypt/live/app.cannamanage.de/ -``` - -### Step 6 — Build Docker Image - -```bash -# On the VPS (or build locally and push to registry) -./mvnw package -DskipTests -P production -docker build -t cannamanage:latest . -``` - -### Step 7 — Start Services - -```bash -cd /opt/cannamanage -docker compose up -d -``` - -### Step 8 — Verify Health - -```bash -# All containers should be 'healthy' or 'running' -docker compose ps - -# Check application logs -docker compose logs -f app --tail=100 - -# Test health endpoint -curl -f http://localhost:8080/actuator/health -# Expected: {"status":"UP","components":{"db":{"status":"UP"},"diskSpace":{"status":"UP"}}} -``` - -### Step 9 — Flyway Migrations - -Flyway runs automatically on Spring Boot startup. Verify migration log: - -```bash -docker compose logs app | grep -i flyway -# Expected: Successfully applied N migrations to schema "public" -``` - -### Step 10 — Create First Admin User - -```bash -# Option A: via REST API (recommended) -curl -X POST https://app.cannamanage.de/api/v1/admin/bootstrap \ - -H "Content-Type: application/json" \ - -d '{ - "adminEmail": "admin@yourclub.de", - "adminPassword": "", - "clubName": "Your Club e.V.", - "clubRegistrationNumber": "VR 12345" - }' - -# The bootstrap endpoint is disabled after first use (one-time setup flag in DB) -``` - -### Step 11 — Verify Production Access - -```bash -# Web UI -open https://app.cannamanage.de - -# API health check -curl https://app.cannamanage.de/actuator/health -``` - ---- - -## 6. CI/CD Pipeline (Gitea Actions on TrueNAS.local) - -The Gitea Actions runner runs **on TrueNAS.local** — this is our homelab build machine. It has Docker, a persistent Maven `.m2` cache volume, and direct SSH access to the Hetzner VPS. Builds happen locally; only the final artifact (Docker image tarball) is shipped to Hetzner. - -**File:** `.gitea/workflows/deploy.yml` +The CI pipeline runs on every push to `main`: ```yaml -name: Build and Deploy to Production +name: CI/CD Pipeline on: push: - branches: - - main + branches: [main] + pull_request: + branches: [main] jobs: - test: - runs-on: self-hosted # <-- TrueNAS.local Gitea runner + build-and-test: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_DB: cannamanage_test + POSTGRES_USER: test + POSTGRES_PASSWORD: test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U test" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: - uses: actions/checkout@v4 @@ -449,279 +260,141 @@ jobs: with: java-version: '21' distribution: 'temurin' - cache: maven - - name: Run unit tests - run: ./mvnw test -pl cannamanage-service - - - name: Run integration tests - run: ./mvnw verify -P integration-tests - # Testcontainers starts PostgreSQL via Docker on the TrueNAS runner - - - name: Coverage gate check - run: ./mvnw verify -P coverage-check - - build-and-deploy: - needs: test - runs-on: self-hosted # <-- TrueNAS.local Gitea runner - steps: - - uses: actions/checkout@v4 - - - name: Build JAR (production profile) - run: ./mvnw package -DskipTests -P production - - - name: Build Docker image + - name: Build & Test Backend run: | - docker build \ - -t cannamanage:${{ github.sha }} \ - -t cannamanage:latest \ - . + mvn clean verify -B \ + -Dspring.datasource.url=jdbc:postgresql://localhost:5432/cannamanage_test \ + -Dspring.datasource.username=test \ + -Dspring.datasource.password=test + env: + SPRING_PROFILES_ACTIVE: test - - name: Save Docker image - run: docker save cannamanage:${{ github.sha }} | gzip > /tmp/cannamanage.tar.gz - - - name: Copy image to Hetzner VPS + - name: Frontend Tests + working-directory: cannamanage-frontend run: | - scp -o StrictHostKeyChecking=no \ - /tmp/cannamanage.tar.gz \ - deploy@${{ secrets.HETZNER_IP }}:/tmp/cannamanage.tar.gz - - - name: Deploy via SSH to Hetzner (Production Release) - run: | - ssh -o StrictHostKeyChecking=no deploy@${{ secrets.HETZNER_IP }} " - set -e - cd /opt/cannamanage - - # Load new image - docker load < /tmp/cannamanage.tar.gz - rm /tmp/cannamanage.tar.gz - - # Rolling restart app only (DB stays up) - VERSION=${{ github.sha }} docker compose up -d app - - # Wait for health - sleep 10 - docker compose ps app | grep 'healthy' || (docker compose logs app --tail=50 && exit 1) - - # Prune old images (keep last 3 SHAs) - docker image prune -f - " - - - name: Cleanup local build artifact - run: rm -f /tmp/cannamanage.tar.gz + npm install -g pnpm + pnpm install --frozen-lockfile + pnpm test ``` -### Gitea Actions Runner on TrueNAS.local +### Pipeline Stages -The self-hosted runner is a Docker container on TrueNAS.local: - -```bash -# On TrueNAS.local — install Gitea Actions runner -docker run -d \ - --name gitea-runner-cannamanage \ - --restart unless-stopped \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -v /opt/gitea-runner/cannamanage:/data \ - -v /opt/gitea-runner/.m2:/root/.m2 \ # Maven cache persisted across builds - -e GITEA_INSTANCE_URL=http://192.168.188.119:30008 \ - -e GITEA_RUNNER_REGISTRATION_TOKEN= \ - gitea/act_runner:latest -``` - -### Required Gitea Repository Secrets - -| Secret | Where set | Value | -|--------|-----------|-------| -| `HETZNER_IP` | Gitea repo secrets | Hetzner VPS IPv4 address | -| `SSH_PRIVATE_KEY` | Gitea repo secrets | Private key for `deploy` user on Hetzner | - -```bash -# On Hetzner VPS — add TrueNAS runner's public key -# (generate keypair on TrueNAS.local: ssh-keygen -t ed25519 -f ~/.ssh/gitea_runner_deploy) -mkdir -p ~/.ssh && chmod 700 ~/.ssh -echo "" >> ~/.ssh/authorized_keys -chmod 600 ~/.ssh/authorized_keys -``` +| Stage | What | Fails on | +|-------|------|----------| +| **Checkout** | Clone repo | — | +| **Java Setup** | JDK 21 Temurin | — | +| **Maven Build** | Compile + unit tests + integration tests | Test failure, compilation error | +| **JaCoCo** | Coverage report (80% gate) | Coverage below threshold | +| **Frontend** | pnpm install + Vitest | Test failure | +| **Deploy** | docker-compose up (on main merge) | Build failure | --- -## 7. Database Backup +## 6. Environment Variables -### Automated Daily Backup +**File:** `.env.production` (on TrueNAS host, NOT committed to git) -Add to root crontab (`crontab -e`): - -```bash -# Daily backup at 03:00 UTC — keep 14 days -0 3 * * * docker exec cannamanage-db pg_dump \ - -U cannamanage_user \ - --format=custom \ - cannamanage | gzip > /opt/backups/cannamanage_$(date +\%Y\%m\%d).sql.gz - -# Cleanup backups older than 14 days -5 3 * * * find /opt/backups -name "cannamanage_*.sql.gz" -mtime +14 -delete -``` - -Create backup directory: -```bash -mkdir -p /opt/backups -chown deploy:deploy /opt/backups -``` - -### Restore from Backup - -```bash -# Restore (caution: this overwrites existing data) -gunzip -c /opt/backups/cannamanage_20260406.sql.gz | \ - docker exec -i cannamanage-db pg_restore \ - -U cannamanage_user \ - --clean \ - --dbname=cannamanage - -# Verify restore -docker exec cannamanage-db psql \ - -U cannamanage_user \ - -d cannamanage \ - -c "SELECT COUNT(*) FROM clubs;" -``` - -### Offsite Backup (Optional) - -For additional redundancy, sync backups to Hetzner Object Storage: - -```bash -# Install s3cmd and configure with Hetzner S3-compatible endpoint -s3cmd sync /opt/backups/ s3://cannamanage-backups/ -``` +| Variable | Purpose | Example | +|----------|---------|---------| +| `DB_USER` | PostgreSQL username | `cannamanage` | +| `DB_PASSWORD` | PostgreSQL password | (generated) | +| `JWT_SECRET` | JWT signing key (256-bit) | (generated) | +| `NEXTAUTH_SECRET` | NextAuth session encryption | (generated) | +| `STRIPE_SECRET_KEY` | Stripe API secret | `sk_live_...` | +| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing | `whsec_...` | +| `MAIL_HOST` | SMTP server | `smtp.example.com` | +| `MAIL_USERNAME` | SMTP user | `noreply@cannamanage.de` | +| `MAIL_PASSWORD` | SMTP password | (secret) | --- -## 8. Monitoring & Health Checks - -### Spring Boot Actuator - -The application exposes health endpoints via `spring-boot-actuator`: - -```bash -# Full health detail (requires ROLE_ADMIN JWT) -GET /actuator/health - -# Example response -{ - "status": "UP", - "components": { - "db": { "status": "UP", "details": { "database": "PostgreSQL", "validationQuery": "isValid()" } }, - "diskSpace": { "status": "UP", "details": { "total": 42GB, "free": 30GB } }, - "ping": { "status": "UP" } - } -} -``` - -Expose only `health` and `info` publicly in `application-production.yml`: -```yaml -management: - endpoints: - web: - exposure: - include: health,info - endpoint: - health: - show-details: when-authorized -``` - -### Log Locations - -| Source | Location | -|--------|----------| -| Application logs | `docker compose logs -f app` | -| Nginx access logs | `/var/log/nginx/access.log` | -| Nginx error logs | `/var/log/nginx/error.log` | -| PostgreSQL logs | `docker compose logs db` | -| Sentry (errors) | `https://sentry.io/organizations//` | - -### Alerting - -Configure Sentry to email on new errors: -1. Set `SENTRY_DSN` in `.env` -2. Add `io.sentry:sentry-spring-boot-starter-jakarta:7.x` to POM -3. Sentry auto-captures all unhandled exceptions with full stack trace - -Simple uptime check via `cron` + email: - -```bash -# Health check every 5 minutes — email on 3 consecutive failures -*/5 * * * * /opt/cannamanage/scripts/health_check.sh -``` +## 7. Backup Strategy ```bash #!/bin/bash -# /opt/cannamanage/scripts/health_check.sh -HEALTH_URL="https://app.cannamanage.de/actuator/health" -FAIL_COUNT_FILE="/tmp/cannamanage_health_fails" +# deploy/backup.sh — runs daily via cron +DATE=$(date +%Y-%m-%d_%H%M) +BACKUP_DIR="/mnt/pool/backups/cannamanage" -HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_URL") -if [ "$HTTP_STATUS" != "200" ]; then - FAILS=$(cat "$FAIL_COUNT_FILE" 2>/dev/null || echo 0) - FAILS=$((FAILS + 1)) - echo "$FAILS" > "$FAIL_COUNT_FILE" - if [ "$FAILS" -ge 3 ]; then - echo "CannaManage health check failed $FAILS times" | \ - mail -s "ALERT: CannaManage DOWN" admin@cannamanage.de - fi -else - echo 0 > "$FAIL_COUNT_FILE" -fi +# PostgreSQL dump +docker exec cannamanage-db pg_dumpall -U ${DB_USER} | gzip > "${BACKUP_DIR}/db_${DATE}.sql.gz" + +# Retain last 30 days +find "${BACKUP_DIR}" -name "db_*.sql.gz" -mtime +30 -delete ``` +| What | Frequency | Retention | +|------|-----------|-----------| +| PostgreSQL full dump | Daily (cron) | 30 days | +| ZFS snapshots | Hourly (TrueNAS) | 7 days | +| Git repository | Every push (Gitea) | Permanent | + --- -## 9. SSL Certificate Renewal +## 8. Deployment Commands -Let's Encrypt certificates expire after 90 days. Certbot handles renewal automatically: +### Initial Setup ```bash -# Test renewal (dry run — no actual renewal) -certbot renew --dry-run - -# Manual renewal -certbot renew --nginx - -# Reload Nginx after renewal -docker exec cannamanage-nginx nginx -s reload -``` - -### Auto-Renewal via Cron - -```bash -# Renew at 02:00 UTC on the 1st and 15th of each month -0 2 1,15 * * certbot renew --quiet --nginx && \ - docker exec cannamanage-nginx nginx -s reload -``` - -Certbot only renews when the certificate is less than 30 days from expiry — safe to run frequently. - ---- - -## 10. Rollback Procedure - -If a deployment causes issues: - -```bash -# On VPS — list recent images -docker images cannamanage --format "table {{.Tag}}\t{{.CreatedAt}}" - -# Roll back to previous SHA +# Clone on TrueNAS +git clone http://truenas.local:30008/pplate/cannamanage.git /opt/cannamanage cd /opt/cannamanage -VERSION= docker compose up -d app -# Verify health after rollback -docker compose ps app -curl https://app.cannamanage.de/actuator/health +# Create .env.production +cp .env.example .env.production +# Edit with real values... + +# Start all services +docker compose -f docker-compose.truenas.yml --env-file .env.production up -d + +# Verify +docker compose -f docker-compose.truenas.yml ps +curl -k https://cannamanage.plate-software.de/api/v1/health ``` -If database migrations were applied and rollback is needed: -1. Restore from last backup (see Section 7) -2. Redeploy the previous image version -3. Flyway baseline the schema at previous version +### Redeploy After Push -> **Note:** Flyway migrations are append-only and forward-only. Design migrations to be reversible where possible (add columns before removing old ones, etc.). +```bash +cd /opt/cannamanage +git pull origin main +docker compose -f docker-compose.truenas.yml --env-file .env.production up -d --build +``` + +### Logs + +```bash +# All services +docker compose -f docker-compose.truenas.yml logs -f + +# Backend only +docker logs -f cannamanage-app + +# Database +docker logs -f cannamanage-db +``` + +--- + +## 9. Monitoring + +| Check | Method | Alert | +|-------|--------|-------| +| Application health | `GET /api/v1/health` (Spring Actuator) | HTTP 200 expected | +| Database connectivity | Docker healthcheck (pg_isready) | Container restart | +| Disk usage | ZFS pool monitoring (TrueNAS) | >80% alert | +| TLS certificate | Certbot auto-renewal (cron) | 30-day warning | +| Container status | `docker compose ps` | Any "unhealthy" or "exited" | + +--- + +## 10. Troubleshooting + +| Issue | Diagnosis | Fix | +|-------|-----------|-----| +| 502 Bad Gateway | Backend not started | `docker compose logs backend` — check for OOM or startup errors | +| Database connection refused | DB container unhealthy | `docker compose restart db` — check pgdata volume | +| TLS cert expired | Certbot renewal failed | `certbot renew --nginx` | +| Port 443 not reachable | Router forwarding lost | Re-add port forward rule on router | +| Out of disk | ZFS pool full | `docker system prune -a` + check backup retention | +| Frontend 500 | NEXTAUTH_SECRET mismatch | Verify `.env.production` matches container env | diff --git a/10-Retrospective.md b/10-Retrospective.md index 0ec0623..434e464 100644 --- a/10-Retrospective.md +++ b/10-Retrospective.md @@ -1,7 +1,231 @@ # 10 — Sprint Retrospectives **Project:** CannaManage — B2B SaaS for German Cannabis Social Clubs -**Last updated:** 2026-06-12 +**Last updated:** 2026-06-19 + +--- + +## Sprint 14 Retrospective — Marketing & Monetization + +**Sprint:** 14 — Landing Page, Login Redesign, Pricing Page, Storage Quotas +**Period:** 2026-06-18 (AI-assisted sprint) +**Mode:** Solo development, AI-assisted (Claude Opus via Roo Orchestrator) +**Outcome:** ✅ Complete — Marketing landing page, pricing tiers, login UX redesign, storage quotas + +### What Went Well ✅ + +- **Landing page with feature showcase** creates professional first impression for potential club customers +- **Pricing page with tier comparison** enables self-service sign-up without sales calls +- **Login redesign** improves onboarding UX — clearer CTAs, better error states +- **Storage quotas per subscription tier** — clean enforcement without breaking existing users + +### Key Decisions Made 📋 + +| Decision | Rationale | +|----------|-----------| +| Marketing pages as SSR (not static) | SEO benefits, dynamic pricing from backend | +| Three subscription tiers | Starter (small clubs), Professional (medium), Enterprise (large) | +| Storage quotas as soft limits | Warn at 80%, block at 100%, admin can override | +| Login page as marketing entry point | First thing users see — must look professional | + +--- + +## Sprint 13 Retrospective — Production Hardening + +**Sprint:** 13 — Security Fixes, CI/CD Quality Gates, Repo Cleanup +**Period:** 2026-06-17 (AI-assisted sprint) +**Outcome:** ✅ Complete — Security fixes, CI quality gates, repository cleanup + +### What Went Well ✅ + +- **Gitea Actions CI pipeline** now runs full test suite with PostgreSQL 16 service container +- **Security audit** identified and fixed several issues (XSS in forum, CSRF token handling) +- **Repository cleanup** removed dead code, unused dependencies, and test artifacts +- **Quality gates** prevent merging code below 80% coverage + +### Key Decisions Made 📋 + +| Decision | Rationale | +|----------|-----------| +| PostgreSQL service container (not Testcontainers in CI) | More reliable in Gitea Actions environment | +| Branch protection on main | Require passing CI before merge | +| Snyk integration | Automated dependency vulnerability scanning | + +--- + +## Sprint 12 Retrospective — Golden Test Standard + +**Sprint:** 12 — Documents Page Integration, UX Improvements, Integration Test Hardening +**Period:** 2026-06-16 (AI-assisted sprint) +**Outcome:** ✅ Complete — Documents page fully integrated, UX polish, test infrastructure hardened + +### What Went Well ✅ + +- **Documents page** now supports upload, download, categorization, and retention policies +- **UX improvements** across all pages: better loading states, consistent error handling +- **Integration test hardening** — eliminated flaky tests, added retry logic for async operations +- **Panel review process** caught edge cases in document permissions + +### Key Decisions Made 📋 + +| Decision | Rationale | +|----------|-----------| +| File storage on local volume (not S3) | Simpler for self-hosted, DSGVO-compliant data locality | +| Retention categories per document | Legal requirement: different documents have different retention periods | +| Soft-delete for documents | Allow recovery within retention period | + +--- + +## Sprint 11 Retrospective — Backend Test Coverage + +**Sprint:** 11 — JaCoCo, ~250 New Tests, 80% Coverage Target +**Period:** 2026-06-15 (AI-assisted sprint) +**Outcome:** ✅ Complete — Coverage raised from ~45% to ~82%, quality gates established + +### What Went Well ✅ + +- **JaCoCo 80% gate** now blocks any PR that drops below threshold +- **~250 new tests** across all service classes — not just happy paths, edge cases too +- **ComplianceService 100%** — every legal rule has a test backing it +- **Testcontainers adoption** eliminated all H2-specific test issues +- **Test naming convention** established: `method_scenario_expected()` + +### What Was Challenging ⚠️ + +- **Writing tests for legacy service code** required some refactoring for testability +- **Testcontainers startup time** adds ~15s per test class — mitigated with `@Testcontainers` shared instances +- **Mocking multi-tenant context** required custom test utilities for `TenantContext` + +### Key Decisions Made 📋 + +| Decision | Rationale | +|----------|-----------| +| 80% overall, 100% ComplianceService | Compliance is legal obligation; rest follows best practice | +| Testcontainers over H2 | Real PostgreSQL catches real issues | +| No test coverage for DTOs/entities | Boilerplate coverage inflates numbers without value | + +--- + +## Sprint 10 Retrospective — Smart Payment Import + +**Sprint:** 10 — Bank Statement Import (MT940/CAMT053/CSV), Auto-Matching +**Period:** 2026-06-14 (AI-assisted sprint) +**Outcome:** ✅ Complete — Bank import pipeline, auto-matching, manual review UI + +### What Went Well ✅ + +- **Multi-format bank import** (MT940, CAMT053, CSV) handles all common German bank export formats +- **Auto-matching algorithm** correctly matches ~85% of incoming payments to member fees +- **Import session workflow** (upload → preview → confirm) prevents accidental data corruption +- **Unmatched payment review UI** lets treasurer manually assign remaining 15% + +### Key Decisions Made 📋 + +| Decision | Rationale | +|----------|-----------| +| Client-side parsing (not backend) | Keeps sensitive bank data in browser until confirmed | +| Fuzzy matching by amount + reference | German bank transfers often have garbled reference text | +| Import session as state machine | PENDING → REVIEWING → CONFIRMED → COMPLETED prevents partial imports | +| Batch processing with flush/clear | Large statements (1000+ transactions) need memory management | + +--- + +## Sprint 9 Retrospective — Berichtszentrale (Report Center) + +**Sprint:** 9 — Report Center, Authority-Ready Exports, Generated Reports +**Period:** 2026-06-13 (AI-assisted sprint) +**Outcome:** ✅ Complete — 8 report types, PDF/CSV export, compliance dashboard + +### What Went Well ✅ + +- **8 report types** covering all CanG compliance obligations (monthly, annual, member-list, destruction, transport, propagation, prevention, compliance-status) +- **Authority-ready PDF format** matches what German authorities expect to see +- **Compliance dashboard** gives club admins a single view of their compliance status +- **Deadline tracking** alerts clubs before compliance deadlines + +### Key Decisions Made 📋 + +| Decision | Rationale | +|----------|-----------| +| OpenPDF for all reports | LGPL, no license cost, good table support | +| Report generation async (background) | Large reports can take 10-30s | +| Pre-built templates per report type | Authorities expect specific formats | +| Compliance deadlines as entity | Track, alert, and mark as completed | + +--- + +## Sprint 8 Retrospective — Vereinsverwaltung (Club Governance) + +**Sprint:** 8 — Club Treasury, General Assembly, Document Archive, Board Management +**Period:** 2026-06-12 (AI-assisted sprint) +**Outcome:** ✅ Complete — Finance module, assembly voting, document management, board member tracking + +### What Went Well ✅ + +- **Club Treasury** with income/expense tracking, categorization, and balance reports +- **General Assembly** module with agenda items, voting (secret + open), quorum validation +- **Document Archive** with upload, categorization, and retention period enforcement +- **Board Management** tracks current board composition with term dates + +### Key Decisions Made 📋 + +| Decision | Rationale | +|----------|-----------| +| Secret ballot as default | German Vereinsrecht requires secret votes for board elections | +| Treasury separate from payment import | Different concerns: treasury = overview, import = automation | +| Document retention per CanG | Cannabis-specific documents: 5-year retention minimum | +| Board terms as date ranges | Enables historical board composition queries | + +--- + +## Sprint 7 Retrospective — Communication & Community + +**Sprint:** 7 — Info Board, Club Events Calendar, Club-Internal Forum, Notifications +**Period:** 2026-06-12 (AI-assisted sprint) +**Outcome:** ✅ Complete — Full community communication stack + +### What Went Well ✅ + +- **Info Board** (Schwarzes Brett) provides a WhatsApp-alternative for club announcements +- **Events Calendar** with RSVP tracking and recurring events +- **Forum** with threads, posts, and moderation — clubs don't need external Discord/Telegram +- **Notification system** unifies all alerts (push, email, in-app) with per-user preferences + +### Key Decisions Made 📋 + +| Decision | Rationale | +|----------|-----------| +| Forum over external chat | DSGVO compliance requires data stays in our system | +| Notifications as unified system | One preference center for all notification types | +| Event RSVP with capacity limits | Clubs have physical space constraints | +| Info Board moderation by admin/staff | Prevent misuse, keep content relevant | + +--- + +## Sprint 6 Retrospective — Production Readiness + +**Sprint:** 6 — DSGVO Consent, Stripe Payments, Audit Log, Grow Calendar, Notifications, PWA +**Period:** 2026-06-12 (AI-assisted sprint) +**Outcome:** ✅ Complete — All launch-critical features delivered + +### What Went Well ✅ + +- **DSGVO consent management** with granular consent types, revocation, and data export +- **Stripe integration** supporting SEPA, PayPal, and Credit Card — covers all common German payment methods +- **Audit log** provides immutable trail for all compliance-relevant actions +- **Grow calendar** with cycle tracking, sensor readings, harvest projections +- **PWA** with service worker enables offline access to key data +- **TrueNAS deployment** works — simpler and cheaper than Hetzner VPS + +### Key Decisions Made 📋 + +| Decision | Rationale | +|----------|-----------| +| TrueNAS over Hetzner VPS | Cost savings, local network, ZFS backups included | +| Stripe for all payment types | Single integration for SEPA + PayPal + Card | +| Audit log as append-only | Legal requirement: compliance trail must be immutable | +| PWA over native app | Cross-platform, no app store approval, faster iteration | +| Consent per data category | DSGVO requires granular consent (not just one checkbox) | --- @@ -9,123 +233,48 @@ **Sprint:** 5 — React Query Integration, Docker Compose Full-Stack, Staff CRUD, System Tests **Period:** 2026-06-12 (single-day intensive sprint, AI-assisted) -**Mode:** Solo development, AI-assisted (Claude Opus via Roo Orchestrator) **Outcome:** ✅ Complete — React Query mock fallback, Docker Compose stack, Staff UI, 190+ automated tests -**Key tech:** @tanstack/react-query, Vitest, MSW, Docker Compose, Next.js 15.5.18 ### What Went Well ✅ -**React Query mock fallback pattern works without backend.** The `stale-while-revalidate` strategy with automatic fallback to mock data when the API is unreachable means frontend development never blocks on backend availability. Developers can work offline, tests run without external dependencies, and the transition to real API calls is a configuration change — not a rewrite. - -**Multi-persona review process is now mature (90% first-pass approval).** After 4 sprints of iteration, the review pipeline (Planner → Plan Reviewer → Security Reviewer → Code Reviewer) now catches issues early enough that 9 out of 10 implementations pass on the first review cycle. The review checklist has been refined to eliminate false positives while catching real issues. - -**Vitest + MSW setup was smooth.** The combination of Vitest (fast unit test runner with native ESM support) and MSW (Mock Service Worker for API mocking) provides sub-second test feedback during development. The MSW handlers mirror the real API contract from `src/types/api.ts`, so tests validate against the actual interface. - -**Full staff CRUD UI delivered.** The staff management page with invite flow, permission editor (8 granular permissions), role assignment, and status management — all integrated with React Query for optimistic updates and automatic cache invalidation. - -**SQL seed + API-driven system tests provide end-to-end confidence.** The Playwright system tests seed the database via SQL, then drive the full application through the browser — login, navigate, create records, verify persistence. This catches integration issues that unit tests cannot. - -### What Was Challenging ⚠️ - -**Docker Compose backend build — Alpine DNS issues.** The multi-stage Docker build using Alpine-based images hit DNS resolution failures when fetching Maven dependencies. The Alpine musl libc DNS resolver behaves differently from glibc, causing intermittent `UnknownHostException` during `mvn package`. Fix: switched to Debian-slim base image for the build stage. - -**Next.js 15.5.18 had breaking peer deps for ESLint plugins.** Upgrading to Next.js 15.5.18 introduced peer dependency conflicts with `eslint-config-next` and several Tailwind CSS plugins. The resolution required pinning specific versions in `package.json` and adding `pnpm.overrides` for transitive dependencies. Cost ~45 minutes of debugging. - -**Per-component loading states required careful UX thought.** With React Query managing each resource independently, the page could show 3-4 different loading spinners simultaneously. The solution was per-component skeleton states (not spinners) with staggered appearance delays to avoid "flash of loading." +- **React Query mock fallback pattern** — frontend works without backend via stale-while-revalidate + automatic mock fallback +- **Multi-persona review process** now mature (90% first-pass approval) +- **Vitest + MSW setup** provides sub-second test feedback +- **Full staff CRUD UI** with invite flow, permission editor, role assignment +- **SQL seed + API-driven system tests** provide end-to-end confidence ### Key Decisions Made 📋 -| Decision | Rationale | Alternatives Rejected | -|----------|-----------|----------------------| -| @tanstack/react-query over SWR | Better devtools, more granular cache control, optimistic updates built-in, larger community | SWR (simpler but less powerful), plain fetch (no caching) | -| Per-component loading (not page-level) | Each data source loads independently — faster perceived performance | Page-level spinner (blocks everything), React Suspense boundaries (still experimental with App Router) | -| Stale-while-revalidate + offline mock fallback | Works without backend running; graceful degradation; instant dev startup | Strict online-only (blocks offline dev), no caching (slow UX) | -| Full staff CRUD in Sprint 5 | Staff management was the #1 user request from Sprint 4 testing; natural extension of existing permission model | Defer to Sprint 6 (users waiting too long), partial staff (confusing UX) | -| SQL seed + API-driven system tests | True end-to-end confidence; catches integration bugs that mocked tests miss | Only unit tests (insufficient), manual testing (not repeatable) | -| Vitest over Jest | Native ESM, faster execution, better TypeScript support, compatible with MSW v2 | Jest (slow transform, CJS-first), node:test (no ecosystem) | - -### Risks Going Forward ⚠️ - -| Risk | Likelihood | Impact | Mitigation | -|------|-----------|--------|------------| -| React Query cache invalidation bugs in production | Medium | Medium — stale data shown to users | Aggressive `staleTime` defaults (30s); manual invalidation on mutations | -| Docker Compose resource usage on small VPS | Medium | Low — can tune container limits | Set memory limits per service; consider removing dev containers from production compose | -| 190+ tests slow CI pipeline | Low | Low — currently <3 minutes total | Parallelize Vitest; cache Playwright browsers; split E2E from unit in CI | -| Next.js 15.x upgrade churn | Medium | Medium — each minor may break plugins | Pin exact versions; only upgrade when needed; test thoroughly before merging | - -### Sprint 6 Goals (Planned) - -- [ ] DSGVO consent management UI (cookie banner + data processing agreement) -- [ ] Stripe payment integration (subscription billing for clubs) -- [ ] Grow calendar (cultivation tracking with harvest projections) -- [ ] PWA / offline support (service worker, cached pages) -- [ ] Deploy to Hetzner VPS (Docker Compose production stack) +| Decision | Rationale | +|----------|-----------| +| @tanstack/react-query over SWR | Better devtools, granular cache control, optimistic updates | +| Per-component loading (not page-level) | Each data source loads independently — faster perceived performance | +| Vitest over Jest | Native ESM, faster execution, better TypeScript support | --- -## Sprint 4 Retrospective — Frontend MVP (Admin Dashboard + Member Portal) +## Sprint 4 Retrospective — Frontend MVP -**Sprint:** 4 — Frontend MVP with Shadboard, Next.js 15, React 19, shadcn/ui, Tailwind 4 +**Sprint:** 4 — Admin Dashboard + Member Portal (Next.js 15, React 19, shadcn/ui) **Period:** 2026-06-12 (single-day intensive sprint, AI-assisted) -**Mode:** Solo development, AI-assisted (Claude Opus via Roo Orchestrator) -**Outcome:** ✅ Complete — 143 files, ~23,568 LoC, 14 routes, 6 Playwright E2E tests passing -**Commit:** `fe6e96d` +**Outcome:** ✅ Complete — 143 files, ~23,568 LoC, 14 routes, 6 Playwright E2E tests ### What Went Well ✅ -**Shadboard starter kit saved weeks of boilerplate.** Using the MIT-licensed Shadboard template (Next.js 15 + shadcn/ui + Tailwind 4) as a foundation meant sidebar layout, theme system, command palette, and responsive behavior came pre-built. Estimated 2-3 weeks of work avoided. The structure was clean enough to extend without fighting it. - -**Persona review caught compliance gaps early.** Before coding the distribution form, reviewing the user stories against CanG §19 requirements identified the need for under-21 reduced limits (30g/month), suspension/expulsion blocking, and immutable audit trail indicators in the UI — all of which were built into the form from the start rather than retrofitted. - -**Playwright E2E caught the NextAuth deadlock immediately.** The very first E2E run revealed that NextAuth v5's middleware was blocking indefinitely when the backend wasn't reachable. Without E2E tests, this would have been a production-discovery bug. The fix (AbortController with 3-second timeout in the auth fetch) took 10 minutes once diagnosed. - -**Dark + light mode from Day 1 was low-effort, high-value.** Since Shadboard already had next-themes integrated and Tailwind 4's `dark:` variant is zero-config, supporting both themes required only choosing color variables — no structural changes. The radial quota visualization in the portal looks excellent in both modes. - -**i18n architecture (next-intl) scales cleanly.** All 200+ UI strings live in `messages/de.json` and `messages/en.json`. No hardcoded text in components. Adding a language is just a JSON file. The translation structure mirrors the route structure (dashboard.*, members.*, portal.*) making keys predictable. - -**Separate route groups for admin vs. portal.** Using Next.js route groups `(dashboard-layout)` and `(portal)` with independent layouts means the admin sidebar never leaks into the member portal (which uses a top nav). Clean separation without code duplication. - -### What Was Challenging ⚠️ - -**NextAuth v5 middleware deadlocked without backend.** The default NextAuth behavior waits indefinitely for the backend auth endpoint. During frontend-only development (backend not running), this caused all protected routes to hang. Fix: AbortController timeout wrapper around the fetch in the `authorize` callback + graceful error handling in middleware. - -**Tailwind 4 breaking changes from v3 documentation.** Most online examples and Stack Overflow answers reference Tailwind v3 syntax. Tailwind 4 changes include: new CSS-first configuration (`@theme` in CSS instead of `tailwind.config.js`), `@apply` deprecated in favor of direct utility classes, and some color utilities renamed. Required reading the v4 migration guide carefully. - -**Mock data architecture decisions.** Without the real backend running, all pages use mock data from `src/data/mock/`. The interface contracts needed to be designed carefully so the migration to real API calls (Sprint 5) is a drop-in replacement. TypeScript interfaces in `src/types/api.ts` define the shared contract. - -**Multi-step distribution form state management.** The 4-step distribution form (member selection → quota check → batch+amount → confirmation) required careful state threading across steps without a state management library. Solved with React `useState` + prop drilling since the form is a single page component. This is fine for 4 steps but wouldn't scale to 10+. +- **Shadboard starter kit** saved weeks of boilerplate (MIT-licensed) +- **Persona review** caught compliance gaps early +- **Dark + light mode from Day 1** was low-effort, high-value +- **i18n architecture (next-intl)** scales cleanly +- **Separate route groups** for admin vs. portal ### Key Decisions Made 📋 -| Decision | Rationale | Alternatives Rejected | -|----------|-----------|----------------------| -| Shadboard (MIT) as starter kit | Pre-built layout, theme, sidebar, command palette. Saves 2-3 weeks. MIT license = no restrictions. | Custom scaffold (slow), Vercel templates (too generic), paid templates (license restrictions) | -| Node 22 LTS | Long-term support, stable for production. Required by some Next.js 15 features. | Node 20 (older LTS), Node 24 (too new, not LTS yet) | -| i18n from Day 1 (next-intl) | Cheaper to add from start than retrofit. German clubs may have English-speaking members. EU expansion possible. | Hardcoded German (faster short-term, expensive later), react-i18next (heavier, server components don't play well) | -| Dark mode default + light toggle | Cannabis club aesthetic suits dark mode. Outdoor mobile use needs light mode. Both supported via next-themes. | Dark-only (excludes bright-environment use), Light-only (boring, doesn't match brand) | -| Separate portal route group | Members get a simplified top-nav layout. Admins get a full sidebar. No layout collision. | Single layout with conditional rendering (messy), separate Next.js app (overkill) | -| pnpm over npm/yarn | Faster installs, strict dependency resolution, disk-efficient via hard links. Shadboard already configured for it. | npm (slower, phantom deps), yarn (Berry complexity) | -| Mock data in typed files | TypeScript interfaces enforce API contract. Easy to swap with real fetch calls. No backend dependency for UI work. | MSW (Mock Service Worker — adds complexity), json-server (external process) | - -### Risks Going Forward ⚠️ - -| Risk | Likelihood | Impact | Mitigation | -|------|-----------|--------|------------| -| Mock-to-real API migration breaks assumptions | Medium | Medium — pages may need refactoring | TypeScript interfaces as contract; API response types match backend DTOs | -| NextAuth session expiry UX | Low | Low — users see login page | Implement token refresh in `jwt` callback; show toast before expiry | -| Tailwind 4 ecosystem immaturity | Low | Low — workarounds exist | Pin Tailwind version; avoid bleeding-edge plugins | -| No loading/error states in UI | Medium | Medium — poor UX on slow connections | Sprint 5: add React Suspense boundaries + error.tsx per route | -| Frontend tests don't cover real auth flow | High | Medium — false confidence | Sprint 5: E2E tests with real backend running in Docker Compose | - -### Sprint 5 Goals (Delivered ✅) - -- [x] Wire frontend to real backend API (React Query with mock fallback) -- [x] Staff management UI (full CRUD: invite, permissions editor, role assignment) -- [x] Full E2E + system test suite with backend running in Docker Compose -- [x] Loading states (per-component skeletons), error boundaries, toast notifications -- [x] Vitest + MSW unit testing infrastructure -- [ ] ~~DSGVO consent management flow~~ → deferred to Sprint 6 -- [ ] ~~WebSocket notifications~~ → deferred to Sprint 6 +| Decision | Rationale | +|----------|-----------| +| Shadboard (MIT) as starter kit | Pre-built layout, theme, sidebar. Saves 2-3 weeks | +| i18n from Day 1 (next-intl) | Cheaper to add from start than retrofit | +| Dark mode default + light toggle | Cannabis club aesthetic suits dark mode | +| pnpm over npm/yarn | Faster installs, strict dependency resolution | --- @@ -133,134 +282,25 @@ **Sprint:** 3 — Staff Permissions, Token Revocation, Member Portal, Reports, Prevention Officer **Period:** 2026-05-15 to 2026-06-12 -**Mode:** Solo development, AI-assisted (Claude Opus via Roo Orchestrator) -**Outcome:** ✅ Complete — 7 phases delivered, ~80 files, ~8,500 LoC, 67+ tests passing +**Outcome:** ✅ Complete — 7 phases delivered, ~80 files, ~8,500 LoC, 67+ tests ### What Went Well ✅ -**Seven-phase delivery cadence worked.** Breaking Sprint 3 into 7 discrete phases (Staff → Token Revocation → Club Settings/Invite → Reports → Portal → Prevention → Integration Tests) created natural milestones. Each phase was independently reviewable and testable. No phase took longer than 2 days. - -**OpenPDF over iText 7 was the right call.** The LGPL license means no license cost, no AGPL compliance headaches. The API is nearly identical to iText 5 (which most online examples reference). PDF reports with headers, footers, page numbers, and tables — all working correctly on the first integration test run. - -**Caffeine cache for token revocation is elegant.** O(1) lookup, no external dependency (no Redis needed), TTL matches token expiry so memory is bounded. The `TokenCleanupScheduler` handles DB garbage collection daily. Total complexity: ~80 lines of code for a production-ready revocation system. - -**Dual SecurityFilterChain pattern is clean.** Separating JWT (admin/staff API) from session (member portal) into two ordered filter chains eliminated all the config conflicts we'd have from mixing them. Each chain has independent CSRF, session, and auth rules. - -**Testcontainers proved their value immediately.** The first integration test run caught a Flyway migration issue (V4 column default not compatible with PostgreSQL strict mode) that H2 would have silently accepted. Real DB in tests = real confidence. - -**JSONB for staff permissions is flexible.** No join tables, no migration needed when adding a new permission enum value. A single `SET` field maps cleanly to a JSONB array column. Querying is still efficient for our scale. - -### What Was Challenging ⚠️ - -**Session auth and JWT in the same app requires careful ordering.** The `@Order(1)` / `@Order(2)` chain priority, plus correct `securityMatcher()` scoping, took two iterations to get right. The first attempt had the portal chain catching API requests. - -**OpenPDF font handling.** Default Helvetica works fine for ASCII/Latin-1, but German umlauts (ä, ö, ü) required explicitly using CP1252 encoding in the `BaseFont.createFont()` call. Caught in `PdfReportGeneratorTest`. - -**Invite flow email testing.** GreenMail (embedded SMTP for tests) was considered but ultimately we used Mockito to mock `JavaMailSender` in unit tests. The actual SMTP integration is tested manually against a local Mailhog instance. - -**30+ integration tests take ~45 seconds.** Testcontainers PostgreSQL startup adds ~8 seconds, and Spring Boot context load adds another ~12 seconds. The remaining time is actual test execution. Acceptable for CI, but not instant enough for TDD flow. Unit tests (< 3 seconds) remain the primary feedback loop. - -### Key Decisions Made 📋 - -| Decision | Rationale | Alternatives Rejected | -|----------|-----------|----------------------| -| OpenPDF over iText 7 | LGPL license — no cost, no AGPL compliance risk | iText 7 (AGPL/commercial), Apache PDFBox (lower-level API, more code) | -| Caffeine over Redis for revocation cache | No external infra needed; bounded memory via TTL; single-instance app (for now) | Redis (overkill for MVP), simple `ConcurrentHashMap` (no TTL eviction) | -| Dual SecurityFilterChain | Clean separation of JWT and session auth; no config conflicts | Single chain with conditional logic (messy), separate Spring Boot apps (over-engineering) | -| JSONB for permissions | Flexible, no joins, no migration for new permissions | Join table `staff_permissions` (normalized but more complex), bitmask (not human-readable) | -| Testcontainers over H2 for integration tests | Catches real PostgreSQL-specific behavior; Flyway migrations tested against real dialect | H2 in PostgreSQL mode (doesn't catch all dialect differences), embedded PostgreSQL (deprecated) | -| Session auth for member portal | Simpler UX for members (no token management); natural session expiry | JWT for portal too (adds complexity for non-technical users) | - -### Risks Going Forward ⚠️ - -| Risk | Likelihood | Impact | Mitigation | -|------|-----------|--------|------------| -| Frontend skill gap (React/Vite) | High | Medium — delays Sprint 4 | Start with admin dashboard only; use component libraries (shadcn/ui) | -| SMTP deliverability for invites | Medium | Low — alternative is manual password handoff | Use proper SPF/DKIM on `cannamanage.de`; Mailgun as fallback | -| Token revocation cache grows unbounded if no cleanup | Low | Low — scheduler runs daily | `TokenCleanupScheduler` + Caffeine TTL eviction | -| 45-second integration test suite slows CI | Medium | Low — annoying but not blocking | Parallel test execution (`maven-surefire-plugin` forks); test categorization | +- **OpenPDF over iText 7** — LGPL, no license cost, API identical to iText 5 +- **Caffeine cache for token revocation** — O(1) lookup, no Redis needed +- **Dual SecurityFilterChain** — clean separation of JWT (admin) and session (member) +- **Testcontainers** caught a Flyway migration issue that H2 would have hidden --- -## Sprint 0 Planning Retrospective +## Sprint 2 Retrospective — REST API -**Sprint:** 0 — Planning & Documentation -**Period:** 2026-04-04 to 2026-04-06 -**Mode:** Solo planning, AI-assisted documentation (Claude Sonnet 4.6 via Roo Orchestrator + Doc Writer modes) -**Outcome:** ✅ Complete — 10-document suite written, architecture locked +**Sprint:** 2 — 5 Controllers, JWT Auth, Spring Security 7, OpenAPI +**Outcome:** ✅ Complete — Full REST API with auth, docs, and tenant isolation --- -## What Went Well ✅ +## Sprint 1 Retrospective — Domain Foundation -**AI-assisted documentation at scale.** The complete documentation suite (10 documents, ~25,000 words total) was created in a single focused session using the Roo Orchestrator mode to coordinate multi-document generation. This would have taken 2–3 days manually. The quality is high enough to serve as actual implementation guidance — not placeholder text. - -**Legal analysis confirmed viability early.** The CanG compliance review (Phase 1) identified the key constraints (no public directory, no consumer-facing advertising, B2B-only) before any code was written. These became hard architectural constraints rather than late surprises. No "oh wait, we can't do that" moments during technical design. - -**Architecture decisions locked before code.** The shared-schema multi-tenancy decision, immutable distribution records design, and `ComplianceConstants` pattern were all decided and documented before a single line of production code was written. This is the correct order. Rework from late architectural pivots is far more expensive than planning time. - -**Compliance constants centralized from day zero.** Designing `ComplianceConstants.java` as the single source of truth for all CanG quota values (25g/day, 50g/month, etc.) prevents the most dangerous class of compliance bug: magic numbers scattered across the codebase that diverge when the law changes. - -**ComfyUI mockup images in minutes.** Generating 5 realistic UI mockup images with FLUX.1-schnell took approximately 8 minutes of wall-clock time. This provides a visual reference for the UI that would otherwise require a designer or Figma skills. The images are good enough for stakeholder presentations and early user research. - -**Test plan written before code.** TC-001 through TC-026 were defined against specifications, not against existing implementation. This forces clarity on what the code must do before writing it — the test cases are essentially executable requirements. - ---- - -## What Was Challenging ⚠️ - -**ComfyUI manual startup friction.** The ComfyUI image generation server does not auto-start with the system. This required manual service start and a retry cycle before image generation could proceed. The fix (systemd user service + auto-start lifespan check in `mcp-image-gen`) was implemented during this planning sprint but added unexpected overhead. - -**Solo developer timeline is ambitious.** The 18–24 month estimate for a production-ready SaaS while employed full-time at ADP Germany is tight. Sprint 1 goals are achievable; the risk accumulates in Sprints 3–6 when frontend work, billing integration, and PDF generation converge. The PrimeFaces JSF choice for MVP was deliberate to reduce this risk — existing Java frontend skills transfer directly. - -**Spring Boot 3 is not yet a "home" stack.** ADP work uses Jakarta EE (JBoss, CDI, JAX-RS). Spring Boot 3 shares the JPA/Hibernate mental model but diverges on dependency injection, auto-configuration, and application packaging. The learning curve is real but bounded — the `mss-failsafe` and `wellmann-shop` projects in `pi_mcps` demonstrate that the transition is manageable. - -**Next.js/React remains a significant gap.** The v2 frontend pivot to Next.js 15 + React 19 is the highest-skill-gap risk in the project. PrimeFaces buys time, but the clock starts ticking on React learning from Sprint 1. Deferring is correct; ignoring it is not. - -**No real user validation yet.** The entire architecture and pricing model is based on market research and regulatory reading, not on conversations with actual club administrators. The product may be solving the right problem in the wrong way. This is the most important open risk. - ---- - -## Key Decisions Made 📋 - -| Decision | Rationale | Alternatives rejected | -|----------|-----------|----------------------| -| Shared-schema multi-tenancy (single DB, `tenant_id` columns) | Lowest ops overhead for MVP; one DB to backup/restore; simpler Flyway migrations | Schema-per-tenant (complex provisioning), DB-per-tenant (expensive at scale) | -| Immutable distribution records (`@Column(updatable = false)`) | Legal integrity — audit logs must be tamper-proof; corrections via `RecallEvent`, not `UPDATE` | Mutable records (simpler but legally risky under CanG §26 record-keeping) | -| PrimeFaces JSF for MVP frontend | Leverages existing Jakarta EE skills; fastest path to working product; no JS build tooling required | React/Next.js (faster modern dev, but higher skill gap), Thymeleaf (less interactive) | -| No public club discovery — permanent architectural exclusion | CanG §§6–7 prohibit advertising cannabis to the general public; club lookup tool would likely constitute advertising | N/A — this is a legal constraint, not a design choice | -| `ComplianceConstants.java` single source of truth | Prevents magic number scatter; single change point when law evolves | Constants in each service (fragile), DB-configurable limits (dangerous — allows disabling compliance) | -| Hetzner VPS over AWS/GCP | Cost (€5.88/month vs €20+); EU data residency (GDPR); simpler ops for solo developer | AWS (expensive, complex), Fly.io (less EU clarity), Railway (vendor lock-in) | - ---- - -## Risks Going Forward ⚠️ - -| Risk | Likelihood | Impact | Mitigation | -|------|-----------|--------|------------| -| New German government tightens CanG (e.g. lower quota limits) | Medium | High — requires rapid compliance updates | `ComplianceConstants.java` centralizes all limits; update is a 1-file change + test re-run | -| Stripe flags account as cannabis-adjacent | Medium | Critical — billing becomes unusable | Use category "Vereinsverwaltung" (club management) in Stripe onboarding; prepare Mollie as fallback | -| Solo dev burnout / timeline slip | High | Medium — delayed launch, not cancellation | Strict MVP scope; PrimeFaces reduces frontend effort; no scope creep before first paying customer | -| Market timing risk — clubs adopt ad-hoc Excel/WhatsApp solutions | Medium | High — low willingness to pay for formal software | User research with 3+ clubs in Sprint 1 is mandatory before writing production code | -| Legal risk: CanG compliance interpretation | Low | High — criminal liability for club officers | Specialist cannabis law opinion (€300–500) before launch; not optional | -| Under-21 age calculation edge cases | Low | Medium — compliance bug | Birthday-based age calculation uses `Period.between()`, not year subtraction; tested in TC-013/014 | - ---- - -## Metrics - -| Metric | Value | -|--------|-------| -| Planning duration | 3 days (2026-04-04 to 2026-04-06) | -| Documents created | 10 (01-PROJECT-CHARTER through 10-RETROSPECTIVE) | -| Estimated total words | ~25,000 | -| Test cases defined | 26 | -| API endpoints specified | 30+ | -| JPA entities designed | 8 | -| UI screens wireframed | 6 | -| UI mockup images generated | 5 | -| Lines of production code written | **0** | -| Architecture decisions logged | 6 major | -| Open risks identified | 6 | - -The ratio of planning output to production code written is intentional. Phase 0 exists to eliminate avoidable rework — the most expensive kind. +**Sprint:** 1 — 8 Entities, ComplianceService, Flyway V1 +**Outcome:** ✅ Complete — Core domain model with compliance enforcement from Day 1 diff --git a/11-Features.md b/11-Features.md new file mode 100644 index 0000000..2204d02 --- /dev/null +++ b/11-Features.md @@ -0,0 +1,275 @@ +# 11 — Features + +**Project:** CannaManage — B2B SaaS for German Cannabis Social Clubs +**Version:** 14.0 (Sprint 14) +**Date:** 2026-06-19 + +--- + +## Feature Overview by Navigation Group + +CannaManage provides a comprehensive management platform organized into four main groups plus member self-service and public marketing pages. + +--- + +## 🟢 Betrieb (Operations) + +### Dashboard +- **KPI Overview** — Real-time cards showing active members, monthly distributions, stock levels, compliance status +- **Activity Feed** — Recent actions across all modules +- **Quick Actions** — Common tasks (new distribution, add member, view reports) +- **Charts** — Distribution trends, stock levels over time (Recharts) + +### Mitglieder (Members) +- **Member Registry** — Full CRUD with search, filter, sort (TanStack Table) +- **Status Management** — Active, Suspended, Expelled, Waiting List +- **Age Verification** — Automatic under-21 detection for reduced limits +- **Membership Dates** — Join date, probation period tracking +- **DSGVO Consent** — Per-member consent status and history +- **Member Import** — Bulk CSV import for existing clubs +- **Quota Dashboard** — Per-member remaining daily/monthly quota + +### Ausgabe (Distributions) +- **4-Step Distribution Form** — Member selection → Quota check → Batch + amount → Confirmation +- **Real-time Quota Enforcement** — CanG §19: 50g/month adults, 30g/month under-21, 25g/day all +- **THC Limit Enforcement** — CanG §19: max 10% THC for under-21 members +- **Batch Selection** — Only shows batches with sufficient stock +- **Distribution History** — Full audit trail with timestamps +- **Receipt Generation** — Printable distribution receipt +- **Suspension Block** — Prevents distributions to suspended members + +### Lager (Stock / Inventory) +- **Batch Management** — Create batches with strain, quantity, THC/CBD content, harvest date +- **Strain Library** — Reusable strain definitions with genetics, type (Indica/Sativa/Hybrid) +- **Stock Movements** — In (harvest/purchase), Out (distribution), Loss (destruction) +- **Low-Stock Alerts** — Configurable threshold notifications +- **Expiry Tracking** — Batches with shelf-life monitoring +- **Stock Reports** — Current inventory, movement history, valuation + +### Anbau (Grow Calendar) +- **Grow Cycles** — Track cultivation from seed to harvest +- **Growth Stages** — Seedling, Vegetative, Flowering, Harvest, Drying, Curing +- **Sensor Readings** — Temperature, humidity, CO2, pH logging +- **Harvest Projections** — Estimated yield based on cycle history +- **Propagation Sources** — Track seeds, clones, mother plants +- **Environmental Alerts** — Threshold-based sensor notifications + +--- + +## 💬 Kommunikation (Communication) + +### Schwarzes Brett (Info Board) +- **Club Announcements** — Admin/staff post updates for all members +- **Pinned Posts** — Important announcements stay at top +- **Categories** — Organize posts by topic +- **Read Tracking** — Know which members saw important announcements +- **Rich Text** — Formatted posts with links and basic styling + +### Kalender (Events Calendar) +- **Club Events** — Create events with date, time, location, description +- **RSVP Tracking** — Members confirm attendance with capacity limits +- **Recurring Events** — Weekly/monthly/yearly recurrence rules +- **Event Categories** — Social, Meeting, Harvest, Maintenance, etc. +- **Calendar View** — Month/week/day views with event cards +- **Email Reminders** — Automatic reminder before events + +### Forum +- **Discussion Threads** — Members start topics, reply with threaded conversations +- **Categories/Boards** — Organized by topic (General, Growing Tips, Strain Reviews, etc.) +- **Moderation** — Admin/staff can pin, lock, or delete threads +- **Mentions** — @mention other members in posts +- **DSGVO Compliant** — All forum data stays within the platform (no external services) + +--- + +## 📋 Verwaltung (Administration) + +### Finanzen — Übersicht (Finance Overview) +- **Club Treasury** — Income and expense tracking with categories +- **Transaction Log** — All financial movements with receipts +- **Expense Categories** — Rent, Utilities, Seeds, Equipment, Insurance, etc. +- **Balance Reports** — Monthly/quarterly/annual financial summaries +- **Membership Fee Tracking** — Which members have paid, who's overdue +- **Export** — CSV export for tax advisor + +### Finanzen — Import (Bank Statement Import) +- **Multi-Format Import** — MT940, CAMT053, CSV bank statement upload +- **Auto-Matching** — Automatically matches payments to member fees (~85% hit rate) +- **Import Sessions** — Upload → Preview → Confirm workflow prevents accidents +- **Manual Matching** — Review and assign unmatched transactions +- **Duplicate Detection** — Prevents importing the same statement twice +- **Match Rules** — Configurable matching by amount, reference text, IBAN + +### Versammlungen (General Assemblies) +- **Assembly Planning** — Date, time, location, agenda items +- **Agenda Management** — Add, reorder, assign presenters to agenda items +- **Voting** — Secret ballot and open voting with real-time results +- **Vote Types** — Yes/No, Multiple Choice, Board Election +- **Quorum Validation** — Checks minimum attendance before votes count +- **Minutes (Protokoll)** — Generate assembly minutes from votes and attendance +- **Ballot Counting** — Automatic tallying with result determination + +### Dokumente (Document Archive) +- **Document Upload** — Any file type with metadata +- **Categorization** — Contracts, Licenses, Protocols, Compliance, Insurance, etc. +- **Retention Policies** — Automatic retention period enforcement per category +- **Version History** — Track document revisions +- **Access Control** — Admin/staff only for sensitive documents +- **Search** — Full-text search across document metadata +- **Download** — Individual and bulk download + +### Vorstand (Board Management) +- **Board Composition** — Current board members with roles (Vorsitzender, Schatzmeister, etc.) +- **Term Tracking** — Start/end dates, election history +- **Historical View** — Past board compositions +- **Role Definitions** — Responsibilities per board position +- **Contact Information** — Board member contact details for authorities + +### Personal (Staff Management) +- **Staff Accounts** — Separate login credentials for club employees +- **Permission Editor** — 8 granular permissions configurable per staff member +- **Role Templates** — Pre-configured permission sets (Front Desk, Grow Manager, etc.) +- **Invite Flow** — Email invite → password setup → active account +- **Activity Log** — What each staff member did and when +- **Account Lifecycle** — Create, suspend, reactivate, delete + +--- + +## ✅ Compliance + +### Compliance-Status (Dashboard) +- **Overall Score** — Club-wide compliance health indicator +- **Area Breakdown** — Per-area compliance status (Distribution, Storage, Reporting, etc.) +- **Deadline Alerts** — Upcoming compliance deadlines with countdown +- **Issue Tracking** — Open compliance issues requiring attention +- **Recommendations** — Actionable steps to improve compliance + +### Berichtszentrale (Report Center) +- **8 Report Types:** + 1. **Monthly Report** — Monthly distribution summary per member + 2. **Annual Report** — Yearly overview for authorities + 3. **Member List** — Current membership roster (authority format) + 4. **Destruction Record** — Cannabis destruction documentation + 5. **Transport Record** — Cannabis transport documentation + 6. **Propagation Sources** — Seed/clone origin documentation + 7. **Prevention Activities** — Youth prevention measures documentation + 8. **Compliance Status** — Overall compliance snapshot +- **Export Formats** — PDF (authority-ready), CSV (data processing) +- **Scheduled Generation** — Auto-generate monthly/annual reports +- **Report History** — Archive of all generated reports + +### Protokoll (Audit Log) +- **Immutable Trail** — Every compliance-relevant action logged +- **Actor Tracking** — Who did what, when, from which IP +- **Action Types** — Distribution, Stock Change, Member Status Change, Report Generation, etc. +- **Filtering** — By date, actor, action type, entity +- **Export** — CSV export for external audit +- **Retention** — 10-year retention per CanG requirements + +### Berichte (Generated Reports) +- **Report Viewer** — View generated reports inline +- **Download** — PDF download for submission to authorities +- **Report Queue** — Status of pending report generation +- **Regeneration** — Re-generate reports with updated data + +--- + +## 👤 Member Portal (Self-Service) + +### Portal Dashboard +- **Quota Visualization** — Radial chart showing remaining daily/monthly quota +- **Recent Distributions** — Last 5 distributions with details +- **Upcoming Events** — Next club events with RSVP +- **Announcements** — Latest info board posts + +### Distribution History +- **Full History** — All distributions received with date, amount, strain +- **Monthly Summary** — Aggregated monthly consumption +- **Download** — Export personal history as CSV + +### Profile Management +- **Personal Data** — Update contact information +- **Consent Management** — View and revoke DSGVO consents +- **Data Export** — Request personal data export (DSGVO Art. 15) +- **Account Deletion** — Request account deletion (DSGVO Art. 17) + +### Events +- **Upcoming Events** — Browse club events +- **RSVP** — Confirm or decline attendance +- **My Events** — View events where RSVP was submitted + +--- + +## 🌐 Marketing (Public Pages) + +### Landing Page +- **Hero Section** — Value proposition with CTA +- **Feature Showcase** — Key features with icons and descriptions +- **Social Proof** — Club testimonials / statistics +- **Pricing Teaser** — Link to pricing page +- **Legal Compliance** — CanG compliance messaging + +### Pricing Page +- **Tier Comparison** — Starter / Professional / Enterprise +- **Feature Matrix** — What's included in each tier +- **FAQ** — Common pricing questions +- **CTA** — Sign-up buttons per tier +- **Annual Discount** — Monthly vs. annual pricing + +### Login / Registration +- **Admin Login** — JWT-based authentication +- **Member Login** — Session-based (separate form) +- **Registration** — New club sign-up flow +- **Password Reset** — Email-based recovery +- **Professional Design** — First impression for new users + +--- + +## 🔧 Platform Features (Cross-Cutting) + +### Multi-Tenancy +- Schema-per-tenant isolation (PostgreSQL) +- Automatic tenant resolution from JWT +- Per-tenant Flyway migrations +- Clean tenant deletion (`DROP SCHEMA CASCADE`) + +### Internationalization (i18n) +- German (primary) + English +- All UI strings externalized in `messages/*.json` +- Date/number formatting per locale +- Easy to add new languages + +### Dark Mode + Light Mode +- System preference detection +- Manual toggle in UI +- All components styled for both modes +- next-themes integration + +### PWA (Progressive Web App) +- Service worker for offline access +- App manifest for "Add to Home Screen" +- Push notifications (Web Push API) +- Cached critical pages + +### Notifications +- **Channels:** In-app, Email, Push (PWA) +- **Preferences:** Per-user, per-notification-type +- **Compose:** Admin can send custom notifications to all/selected members +- **Rate Limiting:** Prevents notification spam + +### DSGVO Compliance +- Granular consent management (per data category) +- Data export (Art. 15 DSGVO) +- Right to erasure (Art. 17 DSGVO) — schema drop +- Consent revocation with immediate effect +- Cookie consent banner +- Privacy policy integration + +### Payments (Stripe) +- SEPA Direct Debit +- PayPal +- Credit/Debit Card +- Subscription management +- Invoice generation +- Webhook handling for payment events +- Storage quotas per subscription tier diff --git a/CannaManage-03-Architecture.md b/CannaManage-03-Architecture.md index 75bb7e6..0420f46 100644 --- a/CannaManage-03-Architecture.md +++ b/CannaManage-03-Architecture.md @@ -1,9 +1,9 @@ # 03 — System Architecture **Project:** CannaManage — B2B SaaS for German Cannabis Social Clubs (Anbauvereinigungen) -**Phase:** 3 of 5 — Staff, Portal & Compliance Reports -**Stack:** Spring Boot 4.0.6 (Java 21) · JPA/Hibernate 7 · PostgreSQL 16 · React/Vite (Sprint 4) -**Last updated:** 2026-06-12 +**Phase:** Sprint 14 — Marketing & Monetization +**Stack:** Spring Boot 4.0.6 (Java 21) · JPA/Hibernate 7 · PostgreSQL 16 · Next.js 15 (React 19) +**Last updated:** 2026-06-19 --- @@ -11,66 +11,70 @@ ```mermaid graph TD - AdminBrowser["🖥️ Browser — Admin Portal"] - MemberBrowser["🖥️ Browser — Member Portal"] + Internet["🌍 Internet"] - Frontend["React/Vite Frontend\n(SPA — served by Nginx)"] - - AdminBrowser -->|HTTPS| Frontend - MemberBrowser -->|HTTPS| Frontend - - Frontend -->|REST/JSON| Backend - - subgraph Backend ["☕ Spring Boot 4.0.6 Application (Java 21)"] - REST["REST API Layer\n/api/v1/"] - Service["Service Layer\n(ComplianceService, ReportService,\nStaffService, PortalService,\nTokenRevocationService…)"] - JPA["JPA / Hibernate 7\nRepositories"] - Security["Dual SecurityFilterChain\nJWT (admin/staff) + Session (portal)"] - Cache["Caffeine Cache\n(token revocation)"] - - REST --> Service - Service --> JPA - Service --> Cache - Security --> REST - end - - JPA -->|JDBC| PG[("🐘 PostgreSQL 16\nmulti-tenant via tenant_id")] - Backend -->|Stripe Java SDK| Stripe["💳 Stripe\n(payment processing)"] - Backend -->|Spring Mail / SMTP| Mail["📧 Spring Mail\n(staff invite emails)"] - Backend -->|OpenPDF| PDF["📄 OpenPDF\n(PDF compliance reports)"] - - Flyway["🔄 Flyway\n(DB migrations)"] -->|applies migrations| PG - - subgraph Hetzner ["🖧 Hetzner VPS — Docker Compose"] - Backend - PG + subgraph TrueNAS ["🖧 TrueNAS Docker — Production"] Nginx["🔒 Nginx\n(reverse proxy + TLS)"] + + subgraph Frontend ["⚛️ Next.js 15 Application"] + Marketing["Marketing Pages\n(Landing, Pricing, Login)"] + AdminUI["Admin Dashboard\n(18 sections)"] + PortalUI["Member Portal\n(self-service)"] + end + + subgraph Backend ["☕ Spring Boot 4.0.6 (Java 21)"] + REST["REST API Layer\n33 controllers · /api/v1/"] + Service["Service Layer\n40+ services"] + JPA["JPA / Hibernate 7\nRepositories"] + Security["Dual SecurityFilterChain\nJWT (admin/staff) + Session (portal)"] + Audit["Audit Log\n(immutable trail)"] + Cache["Caffeine Cache\n(token revocation)"] + end + + PG[("🐘 PostgreSQL 16\nmulti-tenant via schema")] end - Frontend --> Nginx - Nginx --> Backend + Internet -->|"HTTPS :443"| Nginx + Nginx --> Frontend + Nginx -->|"proxy_pass :8080"| Backend + Frontend -->|"REST/JSON"| Backend + + REST --> Service + Service --> JPA + Service --> Cache + Service --> Audit + Security --> REST + JPA -->|"JDBC"| PG + + Backend -->|"Stripe SDK"| Stripe["💳 Stripe\n(SEPA, PayPal, Card)"] + Backend -->|"SMTP"| Mail["📧 Mail\n(notifications, invites)"] + Backend -->|"OpenPDF"| PDF["📄 PDF Reports"] ``` ### Component Responsibilities | Component | Technology | Role | |---|---|---| -| Admin Portal | React/Vite SPA (Sprint 4) | Club management UI | -| Member Portal | Session-based auth (Spring Security) | Member self-service: quota, history, dashboard | -| REST API | Spring Boot 4.0.6 / Spring MVC | All business logic endpoints (9 controllers) | +| Marketing Pages | Next.js 15 (SSR) | Public landing, pricing, login | +| Admin Dashboard | Next.js 15 (CSR) | 18-section club management UI | +| Member Portal | Next.js 15 (CSR) | Member self-service: quota, history, events | +| REST API | Spring Boot 4.0.6 / Spring MVC | 33 controllers, 100+ endpoints | | Auth (Admin/Staff) | Spring Security 7 + JJWT | Stateless JWT authentication | | Auth (Portal) | Spring Security 7 + HttpSession | Session-based member authentication | | Token Revocation | Caffeine cache + DB backing | In-memory revocation check with automatic cleanup | | Staff Permissions | JSONB + annotation checker | 8 granular permissions, role templates | | ORM | JPA / Hibernate 7 | Entity persistence, tenant filtering | -| Database | PostgreSQL 16 | Primary data store (multi-tenant) | -| Migrations | Flyway 10 (V1–V5) | Versioned schema management | -| Payments | Stripe Java SDK | Club subscription billing (Sprint 4+) | -| Email | Spring Mail (SMTP) | Staff invite emails, recall alerts | -| PDF | OpenPDF (iText fork, LGPL) | Compliance report generation (monthly, member-list, recall) | -| CSV | Apache Commons CSV–style | Semicolon-delimited reports, ISO-8859-1 | -| Testing | Testcontainers (PostgreSQL 16) | Real-DB integration tests in Docker | -| Hosting | Hetzner CX21 VPS + Docker Compose | Production deployment | +| Database | PostgreSQL 16 | Primary data store (schema-per-tenant) | +| Migrations | Flyway 10 (V1–V36) | Versioned schema management | +| Payments | Stripe Java SDK | SEPA, PayPal, Credit Card subscriptions | +| Bank Import | Custom parsers | MT940, CAMT053, CSV statement parsing + auto-matching | +| Email | Spring Mail (SMTP) | Notifications, invites, alerts | +| PDF | OpenPDF (iText fork, LGPL) | Compliance report generation | +| CSV | Apache Commons CSV | Semicolon-delimited reports, ISO-8859-1 | +| Audit | Custom audit service | Immutable trail for compliance actions | +| Testing | Testcontainers + JaCoCo | Real-DB integration tests, 80% coverage gate | +| CI/CD | Gitea Actions | PostgreSQL service container, automated pipeline | +| Hosting | TrueNAS Docker + Nginx | Production at cannamanage.plate-software.de | --- @@ -82,29 +86,24 @@ Each club gets its own PostgreSQL schema (e.g. `tenant_abc123`). A platform-leve **Why schema-per-tenant, not shared schema?** -A shared-schema approach (single table with `tenant_id` on every row) is operationally convenient in the short term but creates serious problems at scale: - | Concern | Shared Schema | Schema-Per-Tenant | |---|---|---| | Data isolation | Application-layer only — one missing filter = data leak | Enforced at DB level — schemas are hard boundaries | | DSGVO compliance | Harder to prove isolation; one backup contains all clubs' data | Per-tenant pg_dump; each club's data is cleanly separable | | Deletion / right to erasure | Must `DELETE WHERE tenant_id = ?` across every table | `DROP SCHEMA tenant_abc123 CASCADE` — clean and auditable | -| Migrations | One migration path for all | Per-schema migration via Flyway `schemas` config; adds ~100ms per onboard | +| Migrations | One migration path for all | Per-schema migration via Flyway `schemas` config | | Query performance | Cross-tenant index bloat on large shared tables | Smaller per-tenant tables; no cross-tenant contention | | Future per-club DB isolation | Requires full re-architecture | Trivial: move schema to dedicated DB server | -| Operational overhead | Lower — one connection pool | Slightly higher — one pool per tenant (managed by HikariCP with pool-per-schema) | -**Conclusion:** The shared-schema "MVP convenience" argument only holds for throwaway prototypes. For a compliance SaaS handling personal health-adjacent data (cannabis consumption records), schema-per-tenant is the correct design from Day 1. The migration complexity is manageable; the data isolation benefit is permanent. +**Conclusion:** For a compliance SaaS handling personal health-adjacent data (cannabis consumption records), schema-per-tenant is the correct design from Day 1. The migration complexity is manageable; the data isolation benefit is permanent. ### Tenant Provisioning -When a new club onboards: - ``` POST /api/v1/admin/bootstrap → TenantProvisioningService.provisionTenant(tenantId) → CREATE SCHEMA tenant_{tenantId} - → Flyway.migrate(schema=tenant_{tenantId}) // applies all V*.sql + → Flyway.migrate(schema=tenant_{tenantId}) // applies all V1–V36 → INSERT INTO public.tenants (id, schema_name, onboarded_at, status) ``` @@ -118,75 +117,48 @@ HTTP Request └─ All queries execute in tenant's private schema ``` -### Tenant Resolution - -``` -HTTP Request - └─ Spring Security Filter: extract JWT → resolve tenant_id - └─ TenantContext.setCurrentTenant(tenantId) // ThreadLocal - └─ JPA @Where filter applied on every entity query -``` - -### Code Pattern — Schema Routing DataSource - -```java -// TenantRoutingDataSource.java (pseudocode) -public class TenantRoutingDataSource extends AbstractRoutingDataSource { - - @Override - protected Object determineCurrentLookupKey() { - return TenantContext.getCurrentTenant(); // returns tenant schema name - } -} -``` - -```java -// TenantInterceptor.java (pseudocode) -@Component -public class TenantInterceptor implements HandlerInterceptor { - - @Override - public boolean preHandle(HttpServletRequest req, ...) { - String tenantId = JwtUtils.extractTenantId(req); - TenantContext.setCurrentTenant("tenant_" + tenantId); - return true; - } -} -``` - -**Invariants enforced:** -- Every incoming request resolves its schema before any query runs -- No entity has a `tenant_id` column — schema isolation replaces row-level filtering -- Raw JDBC queries must be avoided; all access goes through JPA repositories with schema routing -- The `public` schema contains only the tenants registry and platform-level config - --- ## 3. Authentication & Authorization -### JWT Token Flow +### Dual SecurityFilterChain -- **Access token:** 8-hour expiry, signed HS256, contains `sub` (userId), `role`, `tenantId` -- **Refresh token:** 30-day expiry, stored in `users.refresh_token_hash` (hashed) -- **Stateless:** No server-side session. JWT is verified on every request by `JwtAuthFilter` +```mermaid +graph LR + subgraph Chain1 ["JWT Chain (Order 1) — /api/v1/**"] + JF[JwtAuthFilter] --> JD[JWT Decode] + JD --> Role[Role Check] + Role --> Perm[Permission Check] + end + + subgraph Chain2 ["Session Chain (Order 2) — /portal/**"] + SF[SessionAuthFilter] --> SC[Session Cookie] + SC --> MR[Member Role] + end +``` + +| Property | JWT Chain | Session Chain | +|----------|-----------|--------------| +| Path | `/api/v1/**` | `/portal/**` | +| Token type | Bearer JWT (Authorization header) | HttpSession cookie | +| Users | Admin, Staff | Members | +| CSRF | Disabled (stateless) | Enabled | +| Expiry | Access: 8h, Refresh: 30d | Session: 24h | ### Roles | Role | Description | Access | |---|---|---| -| `ROLE_CLUB_ADMIN` | Club administrator | Full club management, all members, reports, distributions, staff management | -| `ROLE_STAFF` | Club staff member | Configurable subset of admin permissions — defined per staff account by the admin | -| `ROLE_MEMBER` | Club member | Own quota, own distribution history (read-only) | +| `ROLE_CLUB_ADMIN` | Club administrator | Full club management, all members, reports, distributions, staff, finance | +| `ROLE_STAFF` | Club staff member | Configurable subset of admin permissions | +| `ROLE_MEMBER` | Club member | Own quota, own distribution history, events, forum | | `ROLE_PREVENTION_OFFICER` | Designated prevention officer | Member under-21 reports, prevention data | -> **Staff is a core feature, not an add-on.** Real clubs have multiple staff members (front desk, cultivation responsible, prevention officer designate) with different operational responsibilities. DSGVO requires that each staff member can only access data they need for their specific role. The `ROLE_STAFF` with configurable permission grants from the admin is designed from Phase 0 — retrofitting it later would require schema and API changes. - ### Staff Permission Model -Admins configure staff permissions at account creation. Permissions are stored as a `JSONB` column `granted_permissions` on the `staff_accounts` table within the tenant schema. +8 granular permissions stored as JSONB on `staff_accounts`: ```java -// Configurable staff permissions (granted by admin per staff account) public enum StaffPermission { RECORD_DISTRIBUTION, // can record distributions VIEW_MEMBER_LIST, // can view member roster @@ -199,436 +171,240 @@ public enum StaffPermission { } ``` -Pre-created role templates (configurable by admin): -- **Ausgabe** (Distribution desk): `RECORD_DISTRIBUTION`, `VIEW_MEMBER_LIST`, `VIEW_MEMBER_QUOTA` -- **Lager** (Stock/cultivation): `VIEW_STOCK`, `RECORD_STOCK_IN`, `MANAGE_GROW_CALENDAR` -- **Vorstand** (Board member): all permissions except staff management - -### Service-Layer Authorization Example - -```java -@Service -public class DistributionService { - - @PreAuthorize("hasRole('CLUB_ADMIN')") - public Distribution recordDistribution(RecordDistributionRequest req) { ... } - - @PreAuthorize("hasRole('MEMBER') and #memberId == authentication.principal.memberId") - public QuotaStatus getMyQuota(UUID memberId) { ... } - - @PreAuthorize("hasAnyRole('CLUB_ADMIN', 'PREVENTION_OFFICER')") - public List getUnder21Members() { ... } -} -``` - -### Member Login Sequence - -```mermaid -sequenceDiagram - participant B as Browser - participant API as Spring Boot /api/v1/auth/login - participant DB as PostgreSQL (users table) - participant JWT as JwtService - - B->>API: POST /api/v1/auth/login {email, password} - API->>DB: SELECT * FROM users WHERE email = ? AND active = true - DB-->>API: UserEntity (password_hash, role, tenant_id, member_id) - API->>API: BCrypt.verify(password, password_hash) - alt Invalid credentials - API-->>B: 401 Unauthorized - else Valid - API->>JWT: generateAccessToken(userId, role, tenantId) → 8h - API->>JWT: generateRefreshToken(userId) → 30d - API->>DB: UPDATE users SET refresh_token_hash = ?, last_login = NOW() - DB-->>API: OK - JWT-->>API: accessToken, refreshToken - API-->>B: 200 { accessToken, refreshToken, expiresIn: 28800 } - end -``` - --- -## 4. Data Model (JPA Entities) +## 4. Data Model (57 Entities) -### Entity-Relationship Diagram - -```mermaid -erDiagram - Club { - UUID id PK - UUID tenant_id - string name - string address - string license_number - int max_members - timestamp created_at - enum status - } - - Member { - UUID id PK - UUID tenant_id - UUID club_id FK - string first_name - string last_name - string email - date date_of_birth - date membership_date - string membership_number - enum status - boolean is_under_21 - boolean prevention_officer - } - - Strain { - UUID id PK - UUID tenant_id - string name - decimal thc_percentage - decimal cbd_percentage - string description - } - - Batch { - UUID id PK - UUID tenant_id - UUID strain_id FK - decimal quantity_grams - date harvest_date - string batch_code - enum status - boolean contamination_flag - } - - Distribution { - UUID id PK - UUID tenant_id - UUID member_id FK - UUID batch_id FK - decimal quantity_grams - timestamp distributed_at - UUID recorded_by FK - string notes - boolean immutable - } - - MonthlyQuota { - UUID id PK - UUID tenant_id - UUID member_id FK - int year - int month - decimal total_distributed - decimal max_allowed - } - - StockMovement { - UUID id PK - UUID tenant_id - UUID batch_id FK - enum movement_type - decimal quantity_grams - string reason - timestamp created_at - } - - User { - UUID id PK - UUID tenant_id - UUID member_id FK - string email - string password_hash - enum role - timestamp last_login - boolean active - } - - StaffAccount { - UUID id PK - UUID tenant_id - UUID user_id FK - string display_name - jsonb granted_permissions - boolean active - timestamp created_at - } - - RevokedToken { - UUID id PK - string token_hash - timestamp expires_at - timestamp revoked_at - } - - InviteToken { - UUID id PK - UUID tenant_id - string token - string email - enum target_role - timestamp expires_at - boolean used - } - - Club ||--o{ Member : "has members" - Member ||--o{ Distribution : "receives" - Member ||--o{ MonthlyQuota : "has quota per month" - Member ||--o| User : "may have login" - User ||--o| StaffAccount : "has staff profile" - Strain ||--o{ Batch : "cultivated as" - Batch ||--o{ Distribution : "distributed via" - Batch ||--o{ StockMovement : "tracked in" - Member ||--o{ Distribution : "recorded_by (admin)" -``` - -### Relationship Notes - -| Relationship | Cardinality | Notes | -|---|---|---| -| Club → Member | 1:N | `member.club_id` FK; max enforced by `Club.max_members` | -| Member → Distribution | 1:N | Each distribution targets one member | -| Member → MonthlyQuota | 1:N | One row per `(member_id, year, month)` — UNIQUE constraint | -| Member → User | 1:0..1 | A member may have a portal login; admins may have no `member_id` | -| Strain → Batch | 1:N | Each batch is one strain; a strain can have many batches | -| Batch → Distribution | 1:N | A batch can supply many distributions | -| Batch → StockMovement | 1:N | Every IN/OUT/RECALL against a batch is journaled | -| Distribution.recorded_by → Member | N:1 | The admin who recorded it (audit trail) | - -### Key Constraints - -- `Distribution.immutable = true` by default — records are append-only; no UPDATE/DELETE allowed via API -- `MonthlyQuota` has `UNIQUE(member_id, year, month)` — enforced at DB level -- `Batch.contamination_flag` triggers recall workflow; `Batch.status = RECALLED` is the final state -- All `tenant_id` columns: `NOT NULL`, `updatable = false`, set via `@PrePersist` -- `Member.is_under_21` is derived from `date_of_birth` at registration and re-evaluated on birthday (scheduled job) - ---- - -## 5. API Layer Design - -### Base Path: `/api/v1/` - -All endpoints are RESTful. JSON request/response bodies. JWT Bearer token required on all except `/auth/**`. - -| Controller | Base Path | Auth | Key Endpoints | -|---|---|---|---| -| `AuthController` | `/api/v1/auth` | Public / JWT | `POST /login`, `POST /refresh`, `POST /logout` | -| `ClubController` | `/api/v1/clubs` | JWT (ADMIN) | `GET /me`, `PUT /me`, `GET /me/stats` | -| `MemberController` | `/api/v1/members` | JWT (ADMIN) | `GET /`, `POST /`, `GET /{id}`, `PUT /{id}/status` | -| `DistributionController` | `/api/v1/distributions` | JWT (ADMIN/STAFF) | `POST /`, `GET /?memberId=&month=&year=` | -| `StockController` | `/api/v1/stock` | JWT (ADMIN/STAFF) | `GET /batches`, `POST /batches`, `POST /batches/{id}/recall` | -| `ReportController` | `/api/v1/reports` | JWT (ADMIN/STAFF) | `GET /monthly` (PDF/CSV/JSON), `GET /member-list`, `GET /recall/{batchId}` | -| `ComplianceController` | `/api/v1/compliance` | JWT (ADMIN) | `GET /quota/{memberId}` | -| `StaffController` | `/api/v1/staff` | JWT (ADMIN) | `POST /`, `GET /`, `PUT /{id}`, `DELETE /{id}`, `POST /invite` | -| `PortalController` | `/api/v1/portal` | Session (MEMBER) | `GET /dashboard`, `GET /quota`, `GET /history` | -| `PreventionController` | `/api/v1/prevention` | JWT (ADMIN/PREVENTION) | `POST /officers`, `DELETE /officers/{id}`, `GET /under21` | - -### Standard HTTP conventions -- `201 Created` + `Location` header on resource creation -- `400 Bad Request` with `{ error, message, field? }` on validation failure -- `403 Forbidden` when role/tenant check fails -- `422 Unprocessable Entity` when compliance limits are breached (quota exceeded) -- Pagination: `?page=0&size=20&sort=field,asc` - ---- - -## 6. Compliance Engine - -The `ComplianceService` is the heart of regulatory enforcement. It is called synchronously before every distribution is persisted. All operations run in a single `@Transactional` block with optimistic locking to prevent race conditions under concurrent distribution recording. - -```java -@Service -@Transactional -public class ComplianceService { - - /** - * Validates whether a distribution is legally permitted. - * - * Checks: - * 1. Member is ACTIVE (not SUSPENDED or EXPELLED) - * 2. Daily limit: total distributed today + requestedGrams ≤ 25g - * 3. Monthly limit: MonthlyQuota.total_distributed + requestedGrams ≤ max_allowed - * where max_allowed = 30g (under-21) or 50g (adult) - * 4. Batch is AVAILABLE (not RECALLED or EXHAUSTED) - * 5. Batch has sufficient stock - * - * @throws ComplianceLimitExceededException with remaining quota details - * @throws MemberIneligibleException if member is not ACTIVE - * @throws BatchUnavailableException if batch is recalled or exhausted - */ - public ComplianceCheckResult checkDistributionAllowed( - UUID memberId, UUID batchId, BigDecimal quantityGrams) { ... } - - /** - * Returns remaining quota for the current calendar month. - * Creates a MonthlyQuota row if none exists (lazy initialization). - * - * @return QuotaStatus { totalAllowed, totalUsed, remaining, isUnder21 } - */ - public QuotaStatus getMonthlyRemaining(UUID memberId) { ... } - - /** - * Flags a batch as RECALLED. - * Returns all members who received distributions from this batch - * so the caller can trigger notifications. - * Writes a StockMovement(RECALL) entry. - * - * @return List { memberId, name, email, totalReceived } - */ - public List recallBatch(UUID batchId) { ... } -} -``` - -### Race Condition Prevention - -`MonthlyQuota` rows carry a JPA `@Version` column (optimistic locking). If two concurrent requests attempt to increment `total_distributed` simultaneously, one will receive an `OptimisticLockException` and must retry. The retry logic is handled by a `@Retryable` annotation (Spring Retry, max 3 attempts, 50ms backoff). - -```java -@Entity -public class MonthlyQuota extends AbstractTenantEntity { - - @Version - private Long version; // optimistic lock - - // ... other fields -} -``` - ---- - -## 7. Infrastructure (Hetzner) +### Entity Groups ```mermaid graph TD - Dev["👨‍💻 Developer (Fedora Workstation)"] - Gitea["🏠 Gitea (homelab)\n192.168.188.119:30008"] - Hetzner["☁️ Hetzner VPS CX21\n~€5.88/month"] - - Dev -->|git push| Gitea - Gitea -->|SSH deploy script\n(webhook → deploy.sh)| Hetzner - - subgraph Hetzner - Nginx["🔒 Nginx Container\n(reverse proxy + TLS/Let's Encrypt)"] - App["☕ cannamanage-app\n(Spring Boot JAR)"] - DB[("🐘 cannamanage-db\nPostgreSQL 16")] - - Nginx -->|proxy_pass :8080| App - App -->|JDBC :5432| DB + subgraph Core ["Core Domain"] + Member + Distribution + MonthlyQuota + Batch + Strain + StockMovement end - Internet["🌍 Internet\nHTTPS :443"] -->|HTTPS| Nginx + subgraph Auth ["Authentication"] + User + StaffAccount + RevokedToken + InviteToken + Consent + DeviceRegistration + end + + subgraph Club ["Club Management"] + Club + ClubSettings + BoardMember + end + + subgraph Finance ["Finance"] + Transaction + Expense + MembershipFee + ImportSession + ImportedTransaction + PaymentMatch + Subscription + end + + subgraph Communication ["Communication"] + InfoPost + Event + EventRsvp + ForumThread + ForumPost + Notification + NotificationPreference + end + + subgraph Governance ["Governance"] + Assembly + AgendaItem + Vote + VoteBallot + Document + end + + subgraph Grow ["Cultivation"] + GrowCycle + GrowEntry + SensorReading + PropagationSource + end + + subgraph Compliance ["Compliance"] + ComplianceRecord + ComplianceDeadline + DestructionRecord + TransportRecord + PreventionActivity + Report + AuditLog + end ``` -### Docker Compose Services +### Key Entity Counts by Domain -```yaml -# docker-compose.yml (abbreviated) -services: - cannamanage-app: - image: cannamanage:latest - environment: - SPRING_DATASOURCE_URL: jdbc:postgresql://cannamanage-db:5432/cannamanage - JWT_SECRET: ${JWT_SECRET} - STRIPE_API_KEY: ${STRIPE_API_KEY} - depends_on: [cannamanage-db] - ports: ["127.0.0.1:8080:8080"] - - cannamanage-db: - image: postgres:16-alpine - volumes: [pgdata:/var/lib/postgresql/data] - environment: - POSTGRES_DB: cannamanage - POSTGRES_PASSWORD: ${DB_PASSWORD} - - cannamanage-nginx: - image: nginx:alpine - ports: ["443:443", "80:80"] - volumes: - - ./nginx.conf:/etc/nginx/conf.d/default.conf - - /etc/letsencrypt:/etc/letsencrypt:ro -``` - -### Hetzner Sizing - -| Resource | Spec | Rationale | -|---|---|---| -| Server | CX21 (2 vCPU, 4GB RAM) | Sufficient for < 200 concurrent clubs at MVP | -| Storage | 40GB SSD (included) | PostgreSQL + logs; Hetzner Volumes for backups | -| Backups | Hetzner automated backups (20% surcharge) | Daily snapshot retention 7 days | -| Location | Falkenstein, Germany (FSN1) | DSGVO: data remains within Germany | -| TLS | Let's Encrypt via Certbot | Auto-renew via cron | - -### Deployment Workflow - -``` -git push origin main - → Gitea webhook fires - → deploy.sh on Hetzner: - docker pull cannamanage:latest - docker compose up -d --no-deps cannamanage-app - # zero-downtime: Nginx buffers requests during restart -``` - -Flyway migrations run automatically on application startup (`spring.flyway.enabled=true`). Migration files live at `src/main/resources/db/migration/V*.sql`. +| Domain | Entities | Key Tables | +|--------|----------|-----------| +| Core Operations | 6 | Member, Distribution, MonthlyQuota, Batch, Strain, StockMovement | +| Authentication | 6 | User, StaffAccount, RevokedToken, InviteToken, Consent, DeviceRegistration | +| Club Management | 3 | Club, ClubSettings, BoardMember | +| Finance | 6 | Transaction, Expense, MembershipFee, ImportSession, ImportedTransaction, PaymentMatch | +| Communication | 7 | InfoPost, Event, EventRsvp, ForumThread, ForumPost, Notification, NotificationPreference | +| Governance | 5 | Assembly, AgendaItem, Vote, VoteBallot, Document | +| Cultivation | 4 | GrowCycle, GrowEntry, SensorReading, PropagationSource | +| Compliance | 6 | ComplianceRecord, ComplianceDeadline, DestructionRecord, TransportRecord, PreventionActivity, Report | +| Audit & Billing | 4 | AuditLog, Subscription, StorageQuota, ... | +| **Total** | **~57** | | --- -## 8. Key Design Decisions +## 5. API Layer (33 Controllers) -| Decision | Choice | Rationale | -|---|---|---| -| Multi-tenancy | Schema-per-tenant | Hard data isolation, DSGVO-clean deletion, no cross-tenant query risk | -| Frontend MVP | React/Vite SPA (Sprint 4) | Modern stack; no JSF/PrimeFaces lock-in; easier to hire for; mobile-friendly from day 1 | -| Frontend v2 | Next.js | SSR/ISR for SEO on marketing pages; same React codebase | -| Auth (Admin/Staff) | JWT (stateless) | No sticky sessions needed; horizontal scale ready | -| Auth (Portal) | Session-based (HttpSession) | Simpler for members; no token management needed on member side | -| Dual SecurityFilterChain | Separate filter chains for JWT and session | Clean separation of concerns; each auth mechanism has its own rules | -| PDF generation | OpenPDF (iText fork, LGPL) | No license cost (unlike iText 7 AGPL); mature; sufficient for compliance reports | -| Token revocation | Caffeine in-memory cache + DB | Fast O(1) lookup without Redis dependency; DB for persistence across restarts | -| Staff permissions | JSONB column on `staff_accounts` | Flexible permission model; no join tables needed; easy to query and update | -| Compliance enforcement | Service layer + DB constraint | Belt-and-suspenders: service validates, DB `UNIQUE` prevents duplicates | -| Distribution immutability | `immutable = true`, no DELETE API | Audit trail integrity for regulatory compliance | -| Hosting | Hetzner (Germany) | DSGVO compliance; low cost; German DC | -| Staff roles | Core feature from Phase 0 | DSGVO requires least-privilege access; retrofitting post-MVP too costly | -| Integration testing | Testcontainers (PostgreSQL 16) | Real DB behavior in tests; catches Flyway/JPA issues CI cannot | +### Controller Inventory + +| Group | Controllers | Endpoints | +|-------|-------------|-----------| +| **Auth** | AuthController, ConsentController, DsgvoController | Login, refresh, register, consent management | +| **Members** | MemberController, PortalController | CRUD, quota, self-service | +| **Operations** | DistributionController, StockController, GrowCalendarController | Record distributions, manage inventory, cultivation | +| **Communication** | InfoBoardController, EventController, ForumController | Posts, events, threads | +| **Notifications** | NotificationController, NotificationPreferenceController, NotificationComposeController, DeviceRegistrationController | Push, email, in-app | +| **Finance** | FinanceController, BankImportController, SubscriptionController, StripeWebhookController | Treasury, bank import, billing | +| **Governance** | AssemblyController, BoardController, DocumentController | Assemblies, votes, documents | +| **Compliance** | ComplianceController, ComplianceDashboardController, ComplianceDeadlineController, ComplianceRecordsController, ReportController | Status, deadlines, records, reports | +| **Admin** | StaffController, ClubController, MailSettingsController, StorageController, AuditController | Staff, club config, mail, storage, audit | +| **System** | TestResetController | Test environment reset (non-prod only) | --- -## 9. Dual SecurityFilterChain Pattern (Sprint 3) +## 6. Frontend Architecture -Sprint 3 introduced a dual-chain security architecture to support both stateless API access and stateful member portal sessions: +### Technology Stack -```java -@Configuration -@EnableWebSecurity -public class SecurityConfig { +| Layer | Technology | Purpose | +|-------|-----------|---------| +| Framework | Next.js 15 (App Router) | SSR for marketing, CSR for dashboard | +| UI Library | React 19 | Component rendering | +| Components | shadcn/ui + Radix | Accessible, composable UI primitives | +| Styling | Tailwind CSS 4 | Utility-first styling with dark mode | +| Data | @tanstack/react-query | Server state management, caching | +| Tables | TanStack Table v8 | Sortable, filterable data tables | +| Charts | Recharts | KPI dashboards, quota visualization | +| Forms | React Hook Form + Zod | Type-safe form validation | +| Auth | NextAuth v5 | Session management, JWT relay | +| i18n | next-intl | German / English localization | +| Testing | Vitest + MSW + Playwright | Unit, mock, E2E | - // Chain 1: JWT-based — admin dashboard, staff operations, API - @Bean - @Order(1) - public SecurityFilterChain jwtFilterChain(HttpSecurity http) { - return http - .securityMatcher("/api/v1/**") - .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) - .sessionManagement(s -> s.sessionCreationPolicy(STATELESS)) - .build(); - } +### Route Groups - // Chain 2: Session-based — member portal (form login, CSRF enabled) - @Bean - @Order(2) - public SecurityFilterChain portalFilterChain(HttpSecurity http) { - return http - .securityMatcher("/portal/**") - .formLogin(f -> f.loginPage("/portal/login")) - .sessionManagement(s -> s.maximumSessions(1)) - .build(); - } -} +``` +app/ +├── (marketing)/ → Public: landing, pricing, login +├── (dashboard-layout)/ → Admin: 18 sections with sidebar +│ ├── dashboard/ +│ ├── members/ +│ ├── distributions/ +│ ├── stock/ +│ ├── grow/ +│ ├── info-board/ +│ ├── calendar/ +│ ├── forum/ +│ ├── finance/ +│ │ └── import/ +│ ├── assemblies/ +│ ├── documents/ +│ ├── board/ +│ ├── settings/staff/ +│ ├── compliance/ +│ ├── reports-center/ +│ ├── audit-log/ +│ └── reports/ +└── (portal)/ → Member: self-service with top nav + ├── portal/dashboard/ + ├── portal/history/ + ├── portal/profile/ + └── portal/events/ ``` -**Why two chains?** -- Admin/staff API consumers (React dashboard, mobile apps) expect stateless JWT — no cookies, no CSRF -- Member portal users expect traditional web behavior — login form, sessions, redirects -- Mixing both in one chain leads to config conflicts (CSRF on/off, session policy contradictions) -- Each chain has independent authorization rules and authentication mechanisms +--- + +## 7. Database Migrations (Flyway V1–V36) + +| Range | Sprint | Domain | +|-------|--------|--------| +| V1–V5 | 1–3 | Core schema, members, distributions, stock, staff, club settings | +| V6–V10 | 6 | DSGVO consent, Stripe subscriptions, grow calendar, notifications, PWA | +| V11–V14 | 7 | Info board, events, forum | +| V15–V19 | 8 | Finance (treasury), assemblies, documents, board members | +| V20–V22 | 9 | Reports, compliance records, compliance deadlines | +| V23–V26 | 9 | Destruction records, transport records, propagation sources, prevention activities | +| V27–V29 | 9–10 | Compliance dashboard, bank import, distribution THC/CBD tracking | +| V30–V33 | 10–11 | Import sessions, payment matching, test coverage support | +| V34–V36 | 12–14 | Document integration, storage quotas, marketing/subscription tiers | + +--- + +## 8. Integration Points + +```mermaid +graph LR + CM[CannaManage Backend] + + CM -->|"Stripe SDK"| Stripe["Stripe API\n(subscriptions, webhooks)"] + CM -->|"SMTP"| SMTP["Mail Server\n(notifications, invites)"] + CM -->|"OpenPDF"| PDF["PDF Generation\n(compliance reports)"] + CM -->|"MT940/CAMT053"| Bank["Bank Statement Files\n(uploaded by user)"] + CM -->|"Push API"| Push["Web Push\n(PWA notifications)"] +``` + +| Integration | Protocol | Direction | Purpose | +|-------------|----------|-----------|---------| +| Stripe | REST/SDK | Bidirectional | Subscription billing, webhooks for payment events | +| SMTP | SMTP/TLS | Outbound | Email notifications, staff invites, alerts | +| OpenPDF | Library | Internal | Generate PDF compliance reports | +| Bank Import | File upload | Inbound | MT940, CAMT053, CSV bank statement parsing | +| Web Push | Push API | Outbound | PWA push notifications to registered devices | +| Swagger UI | HTTP | Inbound | API documentation at `/swagger-ui.html` | + +--- + +## 9. Deployment Architecture + +```mermaid +graph TB + Dev["👨‍💻 Dev Workstation\n(macOS)"] + Gitea["🏠 Gitea\n(TrueNAS :30008)"] + Runner["⚙️ Gitea Actions Runner\n(TrueNAS Docker)"] + + Dev -->|"git push"| Gitea + Gitea -->|"triggers"| Runner + Runner -->|"mvn + docker build"| Deploy + + subgraph TrueNAS ["🖧 TrueNAS — Production Docker"] + Deploy["Docker Compose"] + Nginx["Nginx :443"] + App["cannamanage-app :8080"] + FE["cannamanage-frontend :3000"] + DB["PostgreSQL 16 :5432"] + + Deploy --> Nginx + Deploy --> App + Deploy --> FE + Deploy --> DB + Nginx --> App + Nginx --> FE + App --> DB + end + + Internet["🌍 Internet"] -->|"HTTPS"| Nginx +``` + +See [Deployment Guide](CannaManage-09-Deployment) for full production setup details. diff --git a/CannaManage-08-TestPlan.md b/CannaManage-08-TestPlan.md index e82e70d..1d970fd 100644 --- a/CannaManage-08-TestPlan.md +++ b/CannaManage-08-TestPlan.md @@ -1,9 +1,9 @@ # 08 — Test Plan **Project:** CannaManage — B2B SaaS for German Cannabis Social Clubs -**Version:** 3.0 (Sprint 3) -**Date:** 2026-06-12 -**Status:** Active — 67+ automated tests passing +**Version:** 14.0 (Sprint 14) +**Date:** 2026-06-19 +**Status:** Active — 500+ automated tests passing, JaCoCo 80% coverage gate --- @@ -12,16 +12,19 @@ ### 1.1 Testing Pyramid ``` - ┌─────────────────┐ - │ E2E Tests │ 10% — Playwright (deferred to v2) - │ (10%) │ - ├─────────────────┤ - │ Integration │ 20% — Spring Boot Test + Testcontainers - │ Tests (20%) │ - ├─────────────────┤ - │ Unit Tests │ 70% — JUnit 5 + Mockito - │ (70%) │ - └─────────────────┘ + ┌───────────────────┐ + │ E2E / System │ 5% — Playwright (browser automation) + │ Tests │ + ├───────────────────┤ + │ Integration │ 25% — Spring Boot Test + Testcontainers + │ Tests │ + ├───────────────────┤ + │ Frontend Unit │ 20% — Vitest + MSW (React components) + │ Tests │ + ├───────────────────┤ + │ Backend Unit │ 50% — JUnit 5 + Mockito + │ Tests │ + └───────────────────┘ ``` The compliance-critical path (`ComplianceService`) requires **100% line coverage** — no exceptions. Every quota rule is a legal obligation under CanG §§19–22. @@ -30,504 +33,342 @@ The compliance-critical path (`ComplianceService`) requires **100% line coverage | Layer | Tool | Purpose | |-------|------|---------| -| Unit | JUnit 5 (`junit-jupiter`) | Test runner | -| Unit | Mockito 5 | Mock dependencies | -| Unit | AssertJ | Fluent assertions | -| Integration | Spring Boot Test (`@SpringBootTest`) | Full application context | -| Integration | Testcontainers (PostgreSQL module) | Real DB in Docker | -| Integration | MockMvc / RestAssured | HTTP layer testing | -| Coverage | JaCoCo | Line/branch coverage reporting | -| E2E | Playwright (Java) | Browser automation — **deferred to v2** | +| **Backend Unit** | JUnit 5 (`junit-jupiter`) | Test runner | +| **Backend Unit** | Mockito 5 | Mock dependencies | +| **Backend Unit** | AssertJ | Fluent assertions | +| **Backend Integration** | Spring Boot Test (`@SpringBootTest`) | Full application context | +| **Backend Integration** | Testcontainers (PostgreSQL 16) | Real DB in Docker | +| **Backend Integration** | MockMvc / RestAssured | HTTP layer testing | +| **Backend Coverage** | JaCoCo | Line/branch coverage — **80% gate** | +| **Frontend Unit** | Vitest | Fast ESM-native test runner | +| **Frontend Unit** | MSW (Mock Service Worker) | API mocking at network level | +| **Frontend Unit** | Testing Library | Component rendering + queries | +| **Frontend E2E** | Playwright | Browser automation, multi-browser | +| **Frontend E2E** | Playwright Test | Test runner with fixtures + assertions | +| **System Tests** | Playwright + SQL seed | Full-stack with seeded database | -### 1.3 CI Trigger Policy +### 1.3 CI Pipeline Test Flow + +```mermaid +graph LR + Push["git push"] --> CI["Gitea Actions"] + CI --> Backend["Maven Build\n+ Unit Tests\n+ Integration Tests"] + CI --> Frontend["pnpm install\n+ Vitest"] + Backend --> JaCoCo["JaCoCo Report\n80% gate"] + JaCoCo -->|"pass"| Deploy["Deploy"] + JaCoCo -->|"fail"| Block["❌ Block Merge"] + Frontend --> E2E["Playwright E2E\n(on main only)"] + E2E --> Deploy +``` | Branch pattern | Tests run | |---------------|-----------| -| `feature/*` | Unit tests only (`./mvnw test`) | -| `develop` | Unit + Integration (`./mvnw verify -P integration-tests`) | -| `main` | Unit + Integration + coverage gate | - -Coverage gate blocks merge to `main` if `ComplianceService` drops below 100%. +| Feature PR | Backend unit + integration, Frontend Vitest | +| `main` | All above + JaCoCo gate + Playwright E2E | --- -## 2. Unit Test Cases — ComplianceService - -**Class under test:** `de.cannamanage.service.ComplianceService` -**Dependencies mocked:** `DistributionRepository`, `MemberRepository`, `StrainRepository` - ---- - -**TC-001** | `checkDistributionAllowed_givenAdultAtMonthlyLimit_shouldThrowQuotaExceededMonthly` -- **Given:** Adult member (age 35) with exactly 50.0g distributed this calendar month, requesting 1.0g more -- **When:** `complianceService.checkDistributionAllowed(memberId, 1.0)` -- **Then:** Throws `QuotaExceededException` with code `QUOTA_EXCEEDED_MONTHLY` -- **Compliance ref:** CanG §19(2) — 50g/month limit for adults - ---- - -**TC-002** | `checkDistributionAllowed_givenUnder21AtMonthlyLimit_shouldThrowQuotaExceededMonthly` -- **Given:** Under-21 member (age 18) with exactly 30.0g distributed this calendar month, requesting 1.0g more -- **When:** `complianceService.checkDistributionAllowed(memberId, 1.0)` -- **Then:** Throws `QuotaExceededException` with code `QUOTA_EXCEEDED_MONTHLY` -- **Compliance ref:** CanG §19(3) — 30g/month limit for under-21 members - ---- - -**TC-003** | `checkDistributionAllowed_givenAdultAtDailyLimit_shouldThrowQuotaExceededDaily` -- **Given:** Adult member with exactly 25.0g distributed today, requesting 0.5g more -- **When:** `complianceService.checkDistributionAllowed(memberId, 0.5)` -- **Then:** Throws `QuotaExceededException` with code `QUOTA_EXCEEDED_DAILY` -- **Compliance ref:** CanG §19(2) — 25g/day limit - ---- - -**TC-004** | `checkDistributionAllowed_givenUnder21RequestingHighThcStrain_shouldThrowHighThcRestricted` -- **Given:** Under-21 member (age 20), requesting a strain with THC content 12.5% (exceeds 10% threshold) -- **When:** `complianceService.checkDistributionAllowed(memberId, 5.0, strainId)` -- **Then:** Throws `QuotaExceededException` with code `HIGH_THC_RESTRICTED_UNDER_21` -- **Compliance ref:** CanG §19(4) — under-21 members restricted to ≤10% THC strains - ---- - -**TC-005** | `checkDistributionAllowed_givenAdultAt49gMonthlyRequesting2g_shouldThrowQuotaExceededMonthly` -- **Given:** Adult member with 49.0g distributed this month, requesting 2.0g (would total 51.0g) -- **When:** `complianceService.checkDistributionAllowed(memberId, 2.0)` -- **Then:** Throws `QuotaExceededException` with code `QUOTA_EXCEEDED_MONTHLY` -- **Note:** Even partial over-quota requests must be rejected in full; no partial fulfillment - ---- - -**TC-006** | `checkDistributionAllowed_givenAdultAt0gRequesting25g_shouldReturnAllowed` -- **Given:** Adult member with 0.0g distributed this month and today, requesting 25.0g -- **When:** `complianceService.checkDistributionAllowed(memberId, 25.0)` -- **Then:** Returns `DistributionAllowedResult` with `allowed = true`, `remainingDaily = 0.0`, `remainingMonthly = 25.0` -- **Note:** Exactly at daily limit — allowed - ---- - -**TC-007** | `checkDistributionAllowed_givenAdultAt24_9gDailyRequesting0_1g_shouldReturnAllowed` -- **Given:** Adult member with 24.9g distributed today, requesting 0.1g (would reach exactly 25.0g) -- **When:** `complianceService.checkDistributionAllowed(memberId, 0.1)` -- **Then:** Returns `allowed = true`, `remainingDaily = 0.0` -- **Note:** Boundary — exactly at limit is allowed - ---- - -**TC-008** | `checkDistributionAllowed_givenAdultAt24_9gDailyRequesting0_2g_shouldThrowQuotaExceededDaily` -- **Given:** Adult member with 24.9g distributed today, requesting 0.2g (would total 25.1g) -- **When:** `complianceService.checkDistributionAllowed(memberId, 0.2)` -- **Then:** Throws `QuotaExceededException` with code `QUOTA_EXCEEDED_DAILY` -- **Note:** Boundary + 1 — must be blocked - ---- - -**TC-009** | `checkDistributionAllowed_givenSuspendedMember_shouldThrowMemberInactive` -- **Given:** Member with `status = MemberStatus.SUSPENDED`, requesting any amount -- **When:** `complianceService.checkDistributionAllowed(memberId, 1.0)` -- **Then:** Throws `QuotaExceededException` with code `MEMBER_INACTIVE` -- **Note:** Status check must occur before any quota calculation - ---- - -**TC-010** | `checkDistributionAllowed_givenExpelledMember_shouldThrowMemberInactive` -- **Given:** Member with `status = MemberStatus.EXPELLED`, requesting any amount -- **When:** `complianceService.checkDistributionAllowed(memberId, 5.0)` -- **Then:** Throws `QuotaExceededException` with code `MEMBER_INACTIVE` -- **Note:** Expelled members are permanently blocked, no quota check performed - ---- - -## 3. Unit Test Cases — MemberService - -**Class under test:** `de.cannamanage.service.MemberService` -**Dependencies mocked:** `MemberRepository`, `ClubRepository`, `PasswordEncoder` - ---- - -**TC-011** | `createMember_givenAge17_shouldThrowUnderageException` -- **Given:** `CreateMemberRequest` with DOB resulting in age 17 at time of registration -- **When:** `memberService.createMember(request, tenantId)` -- **Then:** Throws `UnderageException` with message containing minimum age (18) -- **Compliance ref:** CanG §6(1) — membership requires minimum age 18 - ---- - -**TC-012** | ~~`createMember_givenAge18_shouldSucceedAndSetIsUnder21False`~~ — *this case is incorrect* - -> **Note:** Age 18 IS under 21, therefore `isUnder21 = true`. See TC-013. - ---- - -**TC-013** | `createMember_givenAge18_shouldSucceedAndSetIsUnder21True` -- **Given:** `CreateMemberRequest` with DOB resulting in age 18 at time of registration -- **When:** `memberService.createMember(request, tenantId)` -- **Then:** Returns created `Member` with `isUnder21 = true`, `status = ACTIVE` -- **Note:** The 18-year-old flag matters for quota enforcement (30g/month, ≤10% THC) - ---- - -**TC-014** | `createMember_givenAge21_shouldSucceedAndSetIsUnder21False` -- **Given:** `CreateMemberRequest` with DOB resulting in age exactly 21 at time of registration -- **When:** `memberService.createMember(request, tenantId)` -- **Then:** Returns created `Member` with `isUnder21 = false`, `status = ACTIVE` -- **Note:** Age 21 is the boundary — exactly 21 qualifies as adult for quota purposes - ---- - -**TC-015** | `createMember_givenDuplicateEmail_shouldThrowDuplicateMemberException` -- **Given:** `MemberRepository.existsByEmailAndTenantId(email, tenantId)` returns `true` -- **When:** `memberService.createMember(request, tenantId)` -- **Then:** Throws `DuplicateMemberException` with code `DUPLICATE_EMAIL` -- **Note:** Email uniqueness is per-tenant, not global - ---- - -## 4. Unit Test Cases — Tenant Isolation - -**Class under test:** JPA repositories with `@TenantAware` filter active -**Setup:** Thread-local `TenantContext` populated via `TenantContextHolder.setTenantId()` - ---- - -**TC-016** | `distributionRepository_givenTenantAContext_shouldNotReturnTenantBData` -- **Given:** Two tenants (Tenant A: UUID-A, Tenant B: UUID-B); 5 distributions for each; `TenantContextHolder` set to UUID-A -- **When:** `distributionRepository.findAll()` -- **Then:** Returns exactly 5 records, all with `tenantId = UUID-A`; zero records from Tenant B -- **Note:** Hibernate filter `tenantFilter` must be enabled in `TenantAwareInterceptor` - ---- - -**TC-017** | `memberRepository_givenTenantAContext_shouldNotSeeClubBMembers` -- **Given:** 10 members in Club A, 8 members in Club B; context set to Club A's tenant -- **When:** `memberRepository.findAll()` -- **Then:** Returns exactly 10 records; no member from Club B present -- **Note:** Cross-tenant data leakage is a GDPR violation, not just a business bug - ---- - -## 5. Integration Test Cases (Testcontainers) - -**Setup:** `@SpringBootTest(webEnvironment = RANDOM_PORT)` with `@Testcontainers`; real PostgreSQL 16 container; Flyway migrations applied before each test class. - ---- - -**TC-018** | `POST /api/v1/distributions — successful distribution recording` -- **Given:** Active adult member with 0g distributed; valid `DistributionRequest` for 10.0g; authenticated as `ROLE_ADMIN` -- **When:** `POST /api/v1/distributions` with valid JWT -- **Then:** HTTP 201; response body contains `distributionId`, `amount = 10.0`, `recordedAt`; DB contains one `distribution` row with `is_recalled = false` - ---- - -**TC-019** | `POST /api/v1/distributions — quota exceeded returns 422` -- **Given:** Adult member with 50.0g already distributed this month; requesting 1.0g more -- **When:** `POST /api/v1/distributions` -- **Then:** HTTP 422; response body `{"errorCode": "QUOTA_EXCEEDED_MONTHLY", "remainingMonthly": 0.0}` - ---- - -**TC-020** | `POST /api/v1/distributions — concurrent race condition (last gram)` -- **Given:** Adult member with 24.9g distributed today; two concurrent requests each for 0.2g (both would individually exceed 25g/day) -- **When:** Both requests fired simultaneously via two threads -- **Then:** Exactly one succeeds (HTTP 201); exactly one fails (HTTP 422 `QUOTA_EXCEEDED_DAILY`); DB total does not exceed 25.0g -- **Mechanism:** `SELECT ... FOR UPDATE` on quota aggregation query prevents double-spend - ---- - -**TC-021** | `POST /api/v1/auth/login — valid credentials return JWT` -- **Given:** Admin user with email `admin@test-club.de`, correct password -- **When:** `POST /api/v1/auth/login` with `{"email": "admin@test-club.de", "password": "..."}` -- **Then:** HTTP 200; response contains `accessToken` (JWT), `tokenType = "Bearer"`, `expiresIn = 3600` - ---- - -**TC-022** | `POST /api/v1/auth/login — invalid credentials return 401` -- **Given:** Admin user exists; wrong password provided -- **When:** `POST /api/v1/auth/login` with wrong password -- **Then:** HTTP 401; response `{"errorCode": "INVALID_CREDENTIALS"}`; no token issued - ---- - -**TC-023** | `GET /api/v1/members — ROLE_MEMBER accessing admin endpoint returns 403` -- **Given:** Authenticated user with `ROLE_MEMBER` JWT (not ROLE_ADMIN) -- **When:** `GET /api/v1/members` (admin-only endpoint) -- **Then:** HTTP 403; response `{"errorCode": "FORBIDDEN"}` - ---- - -**TC-024** | `GET /api/v1/members/{id}/quota — member accessing own quota returns 200` -- **Given:** Authenticated member with JWT; requesting their own `memberId` -- **When:** `GET /api/v1/members/{ownId}/quota` -- **Then:** HTTP 200; response contains `dailyUsed`, `dailyRemaining`, `monthlyUsed`, `monthlyRemaining`, `isUnder21` - ---- - -**TC-025** | `GET /api/v1/members/{otherMemberId}/quota — member accessing other member returns 403` -- **Given:** Authenticated member requesting quota of a *different* member (same club) -- **When:** `GET /api/v1/members/{otherMemberId}/quota` -- **Then:** HTTP 403; GDPR principle: members must not see each other's consumption data - ---- - -**TC-026** | `POST /api/v1/stock/batches/{id}/recall — verify cascade` -- **Given:** Batch `BATCH-TEST-001` with 3 distributions recorded against it; `isRecalled = false` -- **When:** `POST /api/v1/stock/batches/BATCH-TEST-001/recall` with `{"reason": "Contamination detected"}` -- **Then:** HTTP 200; batch `isRecalled = true`; all 3 distribution records have `isRecalled = true`; response body contains list of 3 affected member IDs for notification - ---- - -## 6. Test Data Fixtures - -Define these constants in `src/test/java/de/cannamanage/fixtures/TestFixtures.java`: +## 2. Backend Testing + +### 2.1 Unit Tests (JUnit 5 + Mockito) + +**Target:** All service classes, especially compliance-critical code. + +#### ComplianceService — Critical Path (100% coverage required) + +| TC | Method Under Test | Scenario | Expected | +|----|------------------|----------|----------| +| TC-001 | `checkDistributionAllowed` | Adult at 50g monthly limit + 1g request | `QuotaExceededException(MONTHLY)` | +| TC-002 | `checkDistributionAllowed` | Under-21 at 30g monthly limit + 1g request | `QuotaExceededException(MONTHLY)` | +| TC-003 | `checkDistributionAllowed` | Adult at 25g daily limit + 0.5g request | `QuotaExceededException(DAILY)` | +| TC-004 | `checkDistributionAllowed` | THC >10% for under-21 | `THCLimitExceededException` | +| TC-005 | `checkDistributionAllowed` | Suspended member | `MemberSuspendedException` | +| TC-006 | `checkDistributionAllowed` | Valid request within limits | No exception | +| TC-007 | `checkDistributionAllowed` | New month resets quota | No exception | +| TC-008 | `calculateRemainingQuota` | Mid-month with partial usage | Correct remaining | + +#### Other Service Tests + +| Service | Key Test Cases | +|---------|---------------| +| `DistributionService` | Create, validate batch availability, stock deduction | +| `MemberService` | CRUD, status transitions, age calculation | +| `StockService` | Batch management, movement tracking, low-stock alerts | +| `ReportService` | Monthly report generation, CSV/PDF export | +| `FinanceService` | Transaction recording, balance calculation | +| `BankImportService` | MT940/CAMT053 parsing, auto-matching | +| `AssemblyService` | Vote creation, ballot counting, quorum check | +| `AuditService` | Event recording, immutability verification | +| `NotificationService` | Dispatch, preference filtering, rate limiting | + +### 2.2 Integration Tests (Testcontainers) + +Real PostgreSQL 16 in Docker — catches issues that H2/mocks hide: ```java -public final class TestFixtures { +@SpringBootTest +@Testcontainers +class DistributionIntegrationTest { - // Tenant - public static final UUID TENANT_ID = - UUID.fromString("00000000-0000-0000-0000-000000000001"); - public static final String CLUB_NAME = "Test Cannabis Club e.V."; + @Container + static PostgreSQLContainer pg = new PostgreSQLContainer<>("postgres:16-alpine"); - // Adult member - public static final UUID ADULT_MEMBER_ID = - UUID.fromString("00000000-0000-0000-0000-000000000010"); - public static final String ADULT_MEMBER_NAME = "Klaus Mueller"; - public static final LocalDate ADULT_MEMBER_DOB = - LocalDate.of(1990, 1, 1); // age 36 as of 2026 - - // Under-21 member - public static final UUID UNDER21_MEMBER_ID = - UUID.fromString("00000000-0000-0000-0000-000000000011"); - public static final String UNDER21_MEMBER_NAME = "Lisa Mayer"; - public static final LocalDate UNDER21_MEMBER_DOB = - LocalDate.of(2007, 1, 1); // age 19 as of 2026, isUnder21=true - - // Strain - public static final UUID STRAIN_ID = - UUID.fromString("00000000-0000-0000-0000-000000000020"); - public static final String STRAIN_NAME = "Test OG"; - public static final double STRAIN_THC_PERCENT = 20.0; - public static final double STRAIN_CBD_PERCENT = 1.0; - - // Batch - public static final String BATCH_NUMBER = "BATCH-TEST-001"; - public static final double BATCH_INITIAL_WEIGHT_G = 500.0; - - // Compliance constants (mirror ComplianceConstants.java) - public static final double ADULT_MONTHLY_LIMIT_G = 50.0; - public static final double UNDER21_MONTHLY_LIMIT_G = 30.0; - public static final double DAILY_LIMIT_G = 25.0; - public static final double UNDER21_MAX_THC_PERCENT = 10.0; + @Test + void createDistribution_validRequest_persistsAndUpdatesQuota() { + // Given: member with remaining quota, batch with stock + // When: POST /api/v1/distributions + // Then: distribution persisted, quota updated, stock decremented + } } ``` ---- +**Key integration test scenarios:** -## 7. Coverage Requirements +| Area | Test Cases | +|------|-----------| +| Auth flow | Login → JWT → protected endpoint → 200 | +| Token revocation | Revoke → subsequent request → 401 | +| Tenant isolation | Tenant A data invisible to Tenant B | +| Flyway migrations | All V1–V36 apply cleanly to fresh DB | +| Stripe webhooks | Payment success → subscription activated | +| Bank import | Upload MT940 → parse → auto-match → transactions created | +| Report generation | Generate monthly report → PDF valid | -| Module | Test Type | Minimum Coverage | Enforcement | -|--------|-----------|-----------------|-------------| -| `cannamanage-service` | Unit | 80% line | JaCoCo CI gate | -| `cannamanage-api` | Integration | 70% endpoint coverage | Manual checklist | -| `cannamanage-domain` | Unit | 60% line (entities/enums) | JaCoCo CI gate | -| `ComplianceService` | Unit | **100% line + branch** | JaCoCo CI gate — hard fail | -| `TenantIsolationFilter` | Unit + Integration | 90% line | JaCoCo CI gate | - -> **Rationale for 100% on ComplianceService:** Each uncovered line is a potential legal violation. A missed branch in quota calculation could result in over-distribution — a criminal offence under CanG §34. This is not negotiable. - -### JaCoCo Configuration (`pom.xml`) +### 2.3 Coverage — JaCoCo Configuration ```xml + org.jacoco jacoco-maven-plugin - 0.8.12 - - - jacoco-check - check - - - - CLASS - - de.cannamanage.service.ComplianceService - - - - LINE - COVEREDRATIO - 1.00 - - - BRANCH - COVEREDRATIO - 1.00 - - - - - PACKAGE - - de.cannamanage.service.* - - - - LINE - COVEREDRATIO - 0.80 - - - - - - - + + + + BUNDLE + + + LINE + COVEREDRATIO + 0.80 + + + + + CLASS + + de.cannamanage.service.ComplianceService + + + + LINE + COVEREDRATIO + 1.00 + + + + + ``` +| Rule | Target | Threshold | +|------|--------|-----------| +| Bundle (overall) | All classes | 80% line coverage | +| ComplianceService | Single class | 100% line coverage | +| Controllers | All controllers | 70% (integration tests cover the rest) | + --- -## 8. Test Execution +## 3. Frontend Testing -```bash -# Run all unit tests -./mvnw test -pl cannamanage-service +### 3.1 Unit Tests — Vitest + MSW -# Run integration tests (requires Docker for Testcontainers) -./mvnw verify -P integration-tests +**Config:** `vitest.config.ts` with `jsdom` environment. -# Run specific test class -./mvnw test -pl cannamanage-service -Dtest=ComplianceServiceTest +```typescript +// Example: MemberList component test +import { render, screen } from '@testing-library/react'; +import { server } from '@/mocks/server'; +import { http, HttpResponse } from 'msw'; -# Coverage report (output: target/site/jacoco/index.html) -./mvnw verify jacoco:report +test('renders member list from API', async () => { + server.use( + http.get('/api/v1/members', () => + HttpResponse.json([{ id: 1, firstName: 'Max', lastName: 'Mustermann' }]) + ) + ); -# Coverage report for single module -./mvnw verify jacoco:report -pl cannamanage-service - -# Run compliance tests only (tagged) -./mvnw test -pl cannamanage-service -Dgroups=compliance - -# Check coverage gate (will fail build if thresholds not met) -./mvnw verify -P coverage-check + render(); + expect(await screen.findByText('Max Mustermann')).toBeInTheDocument(); +}); ``` -### Testcontainers Docker requirement +**MSW handler pattern:** Mirrors the real API contract from `src/types/api.ts` — tests validate against the actual interface. -Integration tests require Docker available on the host. The PostgreSQL container is pulled automatically by Testcontainers on first run. Ensure: -- Docker daemon running: `systemctl start docker` (or `docker info`) -- User in `docker` group: `sudo usermod -aG docker $USER` +| Area | Key Test Cases | +|------|---------------| +| Components | Render, user interaction, conditional display | +| Hooks | React Query hooks with MSW responses | +| Forms | Validation, submission, error display | +| Auth | Protected route redirection, token refresh | +| i18n | German + English text rendering | -### Test annotation conventions +### 3.2 E2E Tests — Playwright -```java -// Unit test — no Spring context -@ExtendWith(MockitoExtension.class) -class ComplianceServiceTest { ... } +**Config:** `playwright.config.ts` — Chromium, Firefox, WebKit. -// Integration test — full context + Testcontainers -@SpringBootTest -@Testcontainers -@ActiveProfiles("test") -class DistributionIntegrationTest { ... } +```typescript +// Example: Distribution recording flow +test('admin records a distribution', async ({ page }) => { + await page.goto('/dashboard/distributions'); + await page.click('[data-testid="new-distribution"]'); + + // Step 1: Select member + await page.fill('[data-testid="member-search"]', 'Max'); + await page.click('[data-testid="member-option-1"]'); + + // Step 2: Quota check shown + await expect(page.locator('[data-testid="remaining-quota"]')).toBeVisible(); + + // Step 3: Select batch + amount + await page.selectOption('[data-testid="batch-select"]', 'batch-001'); + await page.fill('[data-testid="amount"]', '5.0'); + + // Step 4: Confirm + await page.click('[data-testid="confirm-distribution"]'); + await expect(page.locator('[data-testid="success-toast"]')).toBeVisible(); +}); +``` -// Tag compliance tests for selective execution -@Tag("compliance") -@Test -void checkDistributionAllowed_givenAdultAtMonthlyLimit_shouldThrowQuotaExceededMonthly() { ... } +**E2E test coverage:** + +| Flow | What's tested | +|------|--------------| +| Login (admin) | JWT auth, redirect to dashboard | +| Login (member) | Session auth, redirect to portal | +| Distribution recording | 4-step form, quota enforcement | +| Member management | CRUD, search, filter | +| Stock management | Add batch, view movements | +| Report generation | Select type, generate, download | +| Staff invite | Create invite, permission editor | +| Payment import | Upload file, review matches, confirm | + +### 3.3 System Tests (SQL Seed + Playwright) + +Full-stack tests that seed the database via SQL, then drive the application through the browser: + +```typescript +test.describe('System: Distribution Compliance', () => { + test.beforeAll(async () => { + // Seed database with member at 49g/month + await seedDatabase('test-data/member-near-limit.sql'); + }); + + test('blocks distribution exceeding monthly quota', async ({ page }) => { + await loginAsAdmin(page); + await page.goto('/dashboard/distributions'); + // Try to distribute 2g (would exceed 50g limit) + // ... + await expect(page.locator('[data-testid="quota-error"]')).toContainText('50g'); + }); +}); ``` --- -## 7. Sprint 3 Integration Tests (Testcontainers) +## 4. Test Data Strategy -Sprint 3 added 30+ integration tests using **Testcontainers** with a real PostgreSQL 16 instance in Docker. These tests exercise the full Spring Boot context including Flyway migrations, JPA repositories, security filters, and HTTP endpoints. +| Environment | Data Source | Lifecycle | +|-------------|-----------|-----------| +| Unit tests | Mocked (Mockito / MSW) | Per-test | +| Integration tests | Testcontainers (fresh DB each run) | Per-class | +| E2E tests | SQL seed files | Per-suite | +| System tests | SQL seed + API-driven setup | Per-suite | -### 7.1 Base Class Pattern +### Test Data Files -```java -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Testcontainers -@ActiveProfiles("integration") -public abstract class AbstractIntegrationTest { - - @Container - static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16-alpine"); - - @DynamicPropertySource - static void configureProperties(DynamicPropertyRegistry registry) { - registry.add("spring.datasource.url", postgres::getJdbcUrl); - registry.add("spring.datasource.username", postgres::getUsername); - registry.add("spring.datasource.password", postgres::getPassword); - } - - @Autowired - protected TestRestTemplate restTemplate; - - protected HttpHeaders adminHeaders() { /* JWT for ADMIN role */ } - protected HttpHeaders staffHeaders(String... permissions) { /* JWT for STAFF role */ } -} +``` +cannamanage-api/src/test/resources/ +├── test-data/ +│ ├── member-near-limit.sql +│ ├── full-club-setup.sql +│ ├── bank-import-mt940.txt +│ └── bank-import-camt053.xml ``` -### 7.2 Sprint 3 Test Classes +--- -| Test Class | Tests | What It Verifies | -|-----------|-------|-----------------| -| `AuthIntegrationTest` | 8 | Login, refresh, logout, token revocation, password change invalidation | -| `PortalIntegrationTest` | 5 | Member session auth, dashboard, quota, history, session expiry | -| `ReportIntegrationTest` | 5 | Monthly PDF/CSV/JSON generation, member-list export, recall report | -| `StaffPermissionIntegrationTest` | 6 | Permission enforcement, role templates, CRUD lifecycle, invite flow | -| `TenantIsolationTest` | 3 | Cross-tenant data cannot be accessed via any endpoint | -| `TokenRevocationIntegrationTest` | 4 | Logout revokes token, password change revokes all, cache + DB sync | +## 5. Quality Gates -### 7.3 Sprint 3 Unit Tests +| Gate | Threshold | Blocks | +|------|-----------|--------| +| JaCoCo overall | ≥ 80% line | PR merge | +| JaCoCo ComplianceService | 100% line | PR merge | +| Backend tests | All pass | PR merge | +| Frontend Vitest | All pass | PR merge | +| Playwright E2E | All pass | Deploy to prod | +| Build success | mvn clean verify | Everything | -| Test Class | Tests | What It Verifies | -|-----------|-------|-----------------| -| `StaffServiceTest` | 5 | Permission assignment, role templates, CRUD operations | -| `TokenRevocationServiceTest` | 4 | Cache behavior, DB persistence, cleanup scheduler | -| `PortalServiceTest` | 4 | Dashboard aggregation, quota calculation, history pagination | -| `PreventionOfficerServiceTest` | 3 | Designation, revocation, limit enforcement | -| `EmailServiceTest` | 2 | Invite email formatting, SMTP integration | -| `ReportServiceTest` | 4 | Report aggregation logic for all 3 report types | -| `PdfReportGeneratorTest` | 3 | PDF generation, page numbering, content correctness | -| `StaffPermissionCheckerTest` | 3 | Annotation-based permission enforcement | +--- -### 7.4 Combined Test Count +## 6. Running Tests Locally -| Category | Sprint 1 | Sprint 2 | Sprint 3 | Total | -|----------|---------|---------|---------|-------| -| Unit tests (Mockito) | 25 | 0 | 28 | 53 | -| Controller tests (MockMvc) | 0 | 12 | 0 | 12 | -| Integration tests (Testcontainers) | 0 | 0 | 31 | 31 | -| **Total** | **25** | **12** | **59** | **67+** | - -### 7.5 Running Integration Tests +### Backend ```bash -# Run all tests (unit + integration) -./mvnw verify -P integration-tests +# All tests +mvn clean verify -# Run only integration tests -./mvnw test -pl cannamanage-api -Dtest="*IntegrationTest" +# Unit only (fast) +mvn test -# Run specific integration test class -./mvnw test -pl cannamanage-api -Dtest="TokenRevocationIntegrationTest" +# Integration only +mvn verify -DskipUnitTests + +# Single test class +mvn test -Dtest=ComplianceServiceTest + +# With coverage report +mvn verify -Pjacoco +open target/site/jacoco/index.html ``` -**Prerequisites:** -- Docker daemon running (Testcontainers requirement) -- At least 2GB free RAM (PostgreSQL container + Spring Boot context) -- Port 5432 does NOT need to be free — Testcontainers uses random ports +### Frontend -### 7.6 Coverage Goals (Updated) +```bash +cd cannamanage-frontend -| Component | Target | Current | Notes | -|-----------|--------|---------|-------| -| `ComplianceService` | 100% | 100% | Legal requirement — no exceptions | -| `TokenRevocationService` | 95% | 95% | Security-critical path | -| `StaffPermissionChecker` | 90% | 92% | Permission enforcement | -| `ReportService` | 85% | 87% | All 3 report types covered | -| `PortalService` | 80% | 83% | Member self-service logic | -| Overall project | 70% | 74% | Balanced coverage across modules | +# Unit tests (Vitest) +pnpm test + +# Watch mode +pnpm test:watch + +# Coverage +pnpm test:coverage + +# E2E (Playwright) +pnpm exec playwright test + +# E2E with UI +pnpm exec playwright test --ui + +# Specific test file +pnpm exec playwright test tests/distribution.spec.ts +``` + +--- + +## 7. Test Metrics (Current) + +| Metric | Value | +|--------|-------| +| Total automated tests | 500+ | +| Backend unit tests | ~250 | +| Backend integration tests | ~100 | +| Frontend Vitest tests | ~100 | +| Playwright E2E tests | ~50 | +| JaCoCo overall coverage | ~82% | +| ComplianceService coverage | 100% | +| CI pipeline duration | ~4 minutes | +| Flaky test rate | < 1% | diff --git a/CannaManage-09-Deployment.md b/CannaManage-09-Deployment.md index 960e5a1..a5dfd05 100644 --- a/CannaManage-09-Deployment.md +++ b/CannaManage-09-Deployment.md @@ -1,96 +1,85 @@ # 09 — Deployment Guide **Project:** CannaManage — B2B SaaS for German Cannabis Social Clubs -**Version:** 3.0 (Sprint 3) -**Date:** 2026-06-12 -**Target environment:** Hetzner VPS — Ubuntu 22.04 LTS — Docker Compose (Release) | TrueNAS.local — Docker (Build/CI) +**Version:** 14.0 (Sprint 14) +**Date:** 2026-06-19 +**Target environment:** TrueNAS Docker — Docker Compose (Production) | Gitea Actions (CI/CD) --- -## 1. Prerequisites - -### Hetzner VPS Specification - -| Resource | Value | Monthly Cost | -|----------|-------|-------------| -| Server type | CX21 | ~€5.88/month | -| vCPU | 2 | — | -| RAM | 4 GB | — | -| SSD | 40 GB | — | -| Network | 20 TB transfer | — | -| OS | Ubuntu 22.04 LTS | — | - -> **Scale-up trigger:** Upgrade to CX31 (8GB RAM) when concurrent active clubs exceeds 20. PostgreSQL is the memory consumer — headroom is consumed by connection pools, not application heap. - -### DNS Setup - -| Record | Type | Value | -|--------|------|-------| -| `cannamanage.de` | A | `` | -| `app.cannamanage.de` | A | `` | -| `*.cannamanage.de` | A | `` | - -Wildcard A record enables future per-club subdomains (`clubname.cannamanage.de`) without additional DNS changes. - -### Required Software - -- Docker Engine 24+ (`docker.io` or Docker CE) -- Docker Compose v2 (`docker compose` — not `docker-compose`) -- Certbot with Nginx plugin (`python3-certbot-nginx`) -- OpenSSH server (enabled by default on Ubuntu) - ---- - -## 2. Infrastructure Architecture +## 1. Infrastructure Overview ```mermaid graph TB - Dev["👨‍💻 Dev Workstation\n(Fedora, 192.168.188.x)"] - Gitea["🏠 Gitea\n(truenas.local:30008)"] - TrueNAS["🖧 TrueNAS.local Docker\n(192.168.188.119)\nBuild + Staging"] - Hetzner["☁️ Hetzner VPS CX21\nProduction Release"] + Dev["👨‍💻 Dev Workstation\n(macOS)"] + Gitea["🏠 Gitea\n(TrueNAS :30008)"] + Runner["⚙️ Gitea Actions Runner\n(TrueNAS Docker)"] Dev -->|"git push"| Gitea - Gitea -->|"Gitea Actions runner\n(on TrueNAS.local)"| TrueNAS - TrueNAS -->|"mvn package + docker build"| TrueNAS - TrueNAS -->|"docker save | scp\n(on merge to main)"| Hetzner + Gitea -->|"triggers CI"| Runner + Runner -->|"build + test + deploy"| Prod - subgraph TrueNAS ["TrueNAS.local — CI/CD Build Environment"] - GiteaRunner["Gitea Actions Runner"] - BuildCache["Maven .m2 cache\n(persistent volume)"] - StagingDB["PostgreSQL staging\n(ephemeral)"] + subgraph Prod ["🖧 TrueNAS — Production (Docker Compose)"] + Nginx["🔒 Nginx\nreverse proxy + TLS\n:443 → :8080/:3000"] + Backend["☕ cannamanage-app\nSpring Boot 4.0.6\n:8080"] + Frontend["⚛️ cannamanage-frontend\nNext.js 15\n:3000"] + DB["🐘 PostgreSQL 16\n:5432\n(persistent volume)"] + + Nginx --> Backend + Nginx --> Frontend + Backend --> DB end - subgraph Hetzner ["Hetzner VPS — Production Release Environment"] - Nginx["Nginx (reverse proxy + TLS)"] - App["cannamanage-app\n(Spring Boot 3.x)"] - DB["PostgreSQL 16\n(persistent pgdata volume)"] - Nginx -->|"proxy_pass :8080"| App - App -->|"JDBC :5432"| DB - end - - Internet["🌍 Internet HTTPS"] -->|"port 443"| Nginx + Internet["🌍 Internet\nhttps://cannamanage.plate-software.de"] -->|":443"| Nginx ``` -### Environment Roles +### Environment Summary | Environment | Host | Purpose | |---|---|---| -| **Development** | Dev workstation (Fedora) | Local feature development, unit tests | -| **Build / CI** | TrueNAS.local Docker | Gitea Actions runner; Maven build; integration tests (Testcontainers); Docker image build | -| **Production / Release** | Hetzner VPS CX21 | Live clubs, real data; Hetzner = our release environment | +| **Development** | macOS (local) | Feature development, unit tests, `docker-compose.yml` | +| **CI/CD** | TrueNAS Gitea Actions Runner | Automated build, test (PostgreSQL service container), deploy | +| **Production** | TrueNAS Docker Compose | Live application at `cannamanage.plate-software.de` | -All three services on Hetzner run on an internal Docker bridge network (`cannamanage_net`). Only Nginx is exposed to the public internet. PostgreSQL has no external port binding. +> **Note:** The project previously planned Hetzner VPS hosting but migrated to TrueNAS Docker for cost savings and network locality. The production URL is forwarded to the TrueNAS instance via port forwarding. --- -## 3. Docker Compose Setup +## 2. Prerequisites -**File:** `/opt/cannamanage/docker-compose.yml` +### TrueNAS Docker Host + +| Resource | Value | +|----------|-------| +| Platform | TrueNAS Scale (Docker) | +| Docker Engine | 24+ | +| Docker Compose | v2 (compose plugin) | +| RAM allocated | 4 GB+ for all containers | +| Storage | Persistent volumes on ZFS pools | + +### DNS / Networking + +| Record | Value | +|--------|-------| +| Domain | `cannamanage.plate-software.de` | +| TLS | Let's Encrypt via Nginx (certbot) | +| Port forwarding | Router :443 → TrueNAS :443 | + +### Required Software (on TrueNAS) + +- Docker Engine 24+ +- Docker Compose v2 +- Gitea (self-hosted, port 30008) +- Gitea Actions Runner (registered) +- Certbot / Nginx for TLS + +--- + +## 3. Docker Compose — Production + +**File:** `docker-compose.truenas.yml` ```yaml -version: '3.9' - networks: cannamanage_net: driver: bridge @@ -98,349 +87,171 @@ networks: volumes: pgdata: driver: local - nginx_certs: - driver: local services: - nginx: - image: nginx:1.25-alpine - container_name: cannamanage-nginx - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - - ./nginx/conf.d:/etc/nginx/conf.d:ro - - nginx_certs:/etc/letsencrypt:ro - - /var/log/nginx:/var/log/nginx - depends_on: - app: - condition: service_healthy - networks: - - cannamanage_net - restart: unless-stopped - - app: - image: cannamanage:${VERSION:-latest} - container_name: cannamanage-app - environment: - - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/cannamanage - - SPRING_DATASOURCE_USERNAME=${DB_USERNAME} - - SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD} - - APP_JWT_SECRET=${JWT_SECRET} - - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY} - - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} - - SPRING_MAIL_HOST=${MAIL_HOST} - - SPRING_MAIL_USERNAME=${MAIL_USERNAME} - - SPRING_MAIL_PASSWORD=${MAIL_PASSWORD} - - SENTRY_DSN=${SENTRY_DSN} - - SPRING_PROFILES_ACTIVE=production - depends_on: - db: - condition: service_healthy - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 60s - networks: - - cannamanage_net - restart: unless-stopped - db: image: postgres:16-alpine container_name: cannamanage-db environment: - - POSTGRES_DB=cannamanage - - POSTGRES_USER=${DB_USERNAME} - - POSTGRES_PASSWORD=${DB_PASSWORD} + POSTGRES_DB: cannamanage + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - pgdata:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d cannamanage"] - interval: 10s - timeout: 5s - retries: 5 networks: - cannamanage_net restart: unless-stopped - # PostgreSQL port intentionally NOT exposed externally + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"] + interval: 10s + timeout: 5s + retries: 5 + + backend: + build: + context: . + dockerfile: Dockerfile.backend + container_name: cannamanage-app + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/cannamanage + SPRING_DATASOURCE_USERNAME: ${DB_USER} + SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD} + SPRING_PROFILES_ACTIVE: production + JWT_SECRET: ${JWT_SECRET} + STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY} + STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET} + MAIL_HOST: ${MAIL_HOST} + MAIL_USERNAME: ${MAIL_USERNAME} + MAIL_PASSWORD: ${MAIL_PASSWORD} + ports: + - "8080:8080" + depends_on: + db: + condition: service_healthy + networks: + - cannamanage_net + restart: unless-stopped + + frontend: + build: + context: ./cannamanage-frontend + dockerfile: Dockerfile + container_name: cannamanage-frontend + environment: + NEXTAUTH_URL: https://cannamanage.plate-software.de + NEXTAUTH_SECRET: ${NEXTAUTH_SECRET} + NEXT_PUBLIC_API_URL: https://cannamanage.plate-software.de/api + ports: + - "3000:3000" + depends_on: + - backend + networks: + - cannamanage_net + restart: unless-stopped + + nginx: + image: nginx:alpine + container_name: cannamanage-nginx + ports: + - "443:443" + - "80:80" + volumes: + - ./deploy/nginx/cannamanage.conf:/etc/nginx/conf.d/default.conf:ro + - /etc/letsencrypt:/etc/letsencrypt:ro + depends_on: + - backend + - frontend + networks: + - cannamanage_net + restart: unless-stopped ``` -**Nginx site config** (`/opt/cannamanage/nginx/conf.d/cannamanage.conf`): +--- + +## 4. Nginx Configuration + +**File:** `deploy/nginx/cannamanage.conf` ```nginx server { listen 80; - server_name app.cannamanage.de; - return 301 https://$host$request_uri; + server_name cannamanage.plate-software.de; + return 301 https://$server_name$request_uri; } server { - listen 443 ssl http2; - server_name app.cannamanage.de; + listen 443 ssl; + server_name cannamanage.plate-software.de; - ssl_certificate /etc/letsencrypt/live/app.cannamanage.de/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/app.cannamanage.de/privkey.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; + ssl_certificate /etc/letsencrypt/live/cannamanage.plate-software.de/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/cannamanage.plate-software.de/privkey.pem; - # Security headers - add_header Strict-Transport-Security "max-age=31536000" always; - add_header X-Frame-Options DENY; - add_header X-Content-Type-Options nosniff; - - location / { - proxy_pass http://app:8080; + # Backend API + location /api/ { + proxy_pass http://backend:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 60s; } - # Stripe webhook — allow larger body - location /api/v1/billing/webhook { - proxy_pass http://app:8080; + # Swagger UI + location /swagger-ui/ { + proxy_pass http://backend:8080; proxy_set_header Host $host; - client_max_body_size 1m; + } + + location /v3/api-docs { + proxy_pass http://backend:8080; + proxy_set_header Host $host; + } + + # Frontend (everything else) + location / { + proxy_pass http://frontend:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; } } ``` --- -## 4. Environment Variables +## 5. CI/CD — Gitea Actions -**File:** `/opt/cannamanage/.env` (never committed to git — add to `.gitignore`) +**File:** `.gitea/workflows/ci.yml` -```bash -# Database -DB_USERNAME=cannamanage_user -DB_PASSWORD= - -# JWT signing key (256-bit minimum — generate with: openssl rand -hex 32) -JWT_SECRET=<256-bit-random-hex> - -# Stripe (use sk_live_ for production, sk_test_ for staging) -STRIPE_SECRET_KEY=sk_live_... -STRIPE_WEBHOOK_SECRET=whsec_... - -# Email (SMTP) -MAIL_HOST=smtp.example.com -MAIL_PORT=587 -MAIL_USERNAME=noreply@cannamanage.de -MAIL_PASSWORD= -MAIL_FROM=CannaManage - -# Error tracking -SENTRY_DSN=https://@.ingest.sentry.io/ - -# Application version (set by CI during deploy) -VERSION=latest -``` - -> **Security:** Never store `.env` in version control. Use Gitea repository secrets for CI and inject at deploy time. On the VPS, set file permissions: `chmod 600 /opt/cannamanage/.env`. - -### 4.1 SMTP Configuration (Sprint 3 — Staff Invite Flow) - -The `EmailService` uses Spring Mail to send staff invite emails. Configure the following environment variables: - -| Variable | Required | Example | Notes | -|----------|----------|---------|-------| -| `MAIL_HOST` | ✅ | `smtp.mailgun.org` | SMTP server hostname | -| `MAIL_PORT` | ✅ | `587` | Use 587 for STARTTLS, 465 for implicit TLS | -| `MAIL_USERNAME` | ✅ | `noreply@cannamanage.de` | SMTP auth username | -| `MAIL_PASSWORD` | ✅ | `` | SMTP auth password | -| `MAIL_FROM` | ✅ | `CannaManage ` | From address shown to recipients | - -**Spring Boot properties mapping:** - -```properties -spring.mail.host=${MAIL_HOST} -spring.mail.port=${MAIL_PORT:587} -spring.mail.username=${MAIL_USERNAME} -spring.mail.password=${MAIL_PASSWORD} -spring.mail.properties.mail.smtp.auth=true -spring.mail.properties.mail.smtp.starttls.enable=true -``` - -**Deliverability recommendations:** -- Set up SPF record: `v=spf1 include:mailgun.org ~all` -- Set up DKIM signing via your mail provider -- Use a dedicated subdomain for transactional email: `mail.cannamanage.de` -- Fallback: if SMTP is unavailable, invite tokens can still be generated and the URL shared manually by the admin - -### 4.2 Caffeine Cache (Token Revocation — No External Dependencies) - -Sprint 3 introduced token revocation using **Caffeine** (in-process Java cache). This requires **no external infrastructure** — no Redis, no Memcached, no additional Docker containers. - -**How it works:** -- Revoked tokens are stored in both the `revoked_tokens` PostgreSQL table and an in-memory Caffeine cache -- Each entry has a TTL matching the token's original expiry time (so cache entries auto-evict when they'd be expired anyway) -- On application restart, the cache is cold but the DB is the source of truth — first check hits DB, subsequent checks hit cache -- `TokenCleanupScheduler` runs daily at 03:00 to delete expired entries from the DB - -**Memory impact:** Negligible. Each cache entry is ~200 bytes (token hash + expiry timestamp). Even with 10,000 revoked tokens, total memory usage is < 2MB. - -**Configuration (application.properties):** - -```properties -# Token revocation cache settings -app.token-revocation.cache-max-size=10000 -app.token-revocation.cleanup-cron=0 0 3 * * * -``` - -**No Redis needed:** For a single-instance deployment (which CannaManage will be for the foreseeable future), Caffeine is the correct choice. Redis would be needed only if we scale to multiple application instances that need shared revocation state. That's a Sprint 6+ concern at earliest. - ---- - -## 5. First-Time Deployment - -### Step 1 — Create Hetzner VPS - -1. Log into [console.hetzner.cloud](https://console.hetzner.cloud) -2. Create server: **CX21**, Ubuntu 22.04, Nuremberg or Frankfurt datacenter -3. Add your SSH public key during setup (`cat ~/.ssh/id_ed25519.pub`) -4. Note the assigned IPv4 address — update DNS A records - -### Step 2 — Install Docker + Docker Compose - -```bash -ssh root@ - -# Update system -apt update && apt upgrade -y - -# Install Docker -curl -fsSL https://get.docker.com | sh - -# Add deploy user (never run production as root) -adduser deploy -usermod -aG docker deploy -usermod -aG sudo deploy - -# Install Certbot -apt install -y python3-certbot-nginx certbot -``` - -### Step 3 — Clone Repository - -```bash -su - deploy -mkdir -p /opt/cannamanage -cd /opt/cannamanage -git clone http://192.168.188.119:30008/pplate/cannamanage.git . -# Or from public mirror when available -``` - -### Step 4 — Create Production `.env` - -```bash -cd /opt/cannamanage -cp .env.example .env -nano .env # Fill in all production secrets -chmod 600 .env -``` - -### Step 5 — Obtain SSL Certificate - -```bash -# Stop anything on port 80 first (nothing should be running yet) -certbot certonly --standalone \ - -d app.cannamanage.de \ - --non-interactive \ - --agree-tos \ - -m ssl@cannamanage.de - -# Symlink certs into nginx_certs volume location -# Certbot places certs at /etc/letsencrypt/live/app.cannamanage.de/ -``` - -### Step 6 — Build Docker Image - -```bash -# On the VPS (or build locally and push to registry) -./mvnw package -DskipTests -P production -docker build -t cannamanage:latest . -``` - -### Step 7 — Start Services - -```bash -cd /opt/cannamanage -docker compose up -d -``` - -### Step 8 — Verify Health - -```bash -# All containers should be 'healthy' or 'running' -docker compose ps - -# Check application logs -docker compose logs -f app --tail=100 - -# Test health endpoint -curl -f http://localhost:8080/actuator/health -# Expected: {"status":"UP","components":{"db":{"status":"UP"},"diskSpace":{"status":"UP"}}} -``` - -### Step 9 — Flyway Migrations - -Flyway runs automatically on Spring Boot startup. Verify migration log: - -```bash -docker compose logs app | grep -i flyway -# Expected: Successfully applied N migrations to schema "public" -``` - -### Step 10 — Create First Admin User - -```bash -# Option A: via REST API (recommended) -curl -X POST https://app.cannamanage.de/api/v1/admin/bootstrap \ - -H "Content-Type: application/json" \ - -d '{ - "adminEmail": "admin@yourclub.de", - "adminPassword": "", - "clubName": "Your Club e.V.", - "clubRegistrationNumber": "VR 12345" - }' - -# The bootstrap endpoint is disabled after first use (one-time setup flag in DB) -``` - -### Step 11 — Verify Production Access - -```bash -# Web UI -open https://app.cannamanage.de - -# API health check -curl https://app.cannamanage.de/actuator/health -``` - ---- - -## 6. CI/CD Pipeline (Gitea Actions on TrueNAS.local) - -The Gitea Actions runner runs **on TrueNAS.local** — this is our homelab build machine. It has Docker, a persistent Maven `.m2` cache volume, and direct SSH access to the Hetzner VPS. Builds happen locally; only the final artifact (Docker image tarball) is shipped to Hetzner. - -**File:** `.gitea/workflows/deploy.yml` +The CI pipeline runs on every push to `main`: ```yaml -name: Build and Deploy to Production +name: CI/CD Pipeline on: push: - branches: - - main + branches: [main] + pull_request: + branches: [main] jobs: - test: - runs-on: self-hosted # <-- TrueNAS.local Gitea runner + build-and-test: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_DB: cannamanage_test + POSTGRES_USER: test + POSTGRES_PASSWORD: test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U test" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: - uses: actions/checkout@v4 @@ -449,279 +260,141 @@ jobs: with: java-version: '21' distribution: 'temurin' - cache: maven - - name: Run unit tests - run: ./mvnw test -pl cannamanage-service - - - name: Run integration tests - run: ./mvnw verify -P integration-tests - # Testcontainers starts PostgreSQL via Docker on the TrueNAS runner - - - name: Coverage gate check - run: ./mvnw verify -P coverage-check - - build-and-deploy: - needs: test - runs-on: self-hosted # <-- TrueNAS.local Gitea runner - steps: - - uses: actions/checkout@v4 - - - name: Build JAR (production profile) - run: ./mvnw package -DskipTests -P production - - - name: Build Docker image + - name: Build & Test Backend run: | - docker build \ - -t cannamanage:${{ github.sha }} \ - -t cannamanage:latest \ - . + mvn clean verify -B \ + -Dspring.datasource.url=jdbc:postgresql://localhost:5432/cannamanage_test \ + -Dspring.datasource.username=test \ + -Dspring.datasource.password=test + env: + SPRING_PROFILES_ACTIVE: test - - name: Save Docker image - run: docker save cannamanage:${{ github.sha }} | gzip > /tmp/cannamanage.tar.gz - - - name: Copy image to Hetzner VPS + - name: Frontend Tests + working-directory: cannamanage-frontend run: | - scp -o StrictHostKeyChecking=no \ - /tmp/cannamanage.tar.gz \ - deploy@${{ secrets.HETZNER_IP }}:/tmp/cannamanage.tar.gz - - - name: Deploy via SSH to Hetzner (Production Release) - run: | - ssh -o StrictHostKeyChecking=no deploy@${{ secrets.HETZNER_IP }} " - set -e - cd /opt/cannamanage - - # Load new image - docker load < /tmp/cannamanage.tar.gz - rm /tmp/cannamanage.tar.gz - - # Rolling restart app only (DB stays up) - VERSION=${{ github.sha }} docker compose up -d app - - # Wait for health - sleep 10 - docker compose ps app | grep 'healthy' || (docker compose logs app --tail=50 && exit 1) - - # Prune old images (keep last 3 SHAs) - docker image prune -f - " - - - name: Cleanup local build artifact - run: rm -f /tmp/cannamanage.tar.gz + npm install -g pnpm + pnpm install --frozen-lockfile + pnpm test ``` -### Gitea Actions Runner on TrueNAS.local +### Pipeline Stages -The self-hosted runner is a Docker container on TrueNAS.local: - -```bash -# On TrueNAS.local — install Gitea Actions runner -docker run -d \ - --name gitea-runner-cannamanage \ - --restart unless-stopped \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -v /opt/gitea-runner/cannamanage:/data \ - -v /opt/gitea-runner/.m2:/root/.m2 \ # Maven cache persisted across builds - -e GITEA_INSTANCE_URL=http://192.168.188.119:30008 \ - -e GITEA_RUNNER_REGISTRATION_TOKEN= \ - gitea/act_runner:latest -``` - -### Required Gitea Repository Secrets - -| Secret | Where set | Value | -|--------|-----------|-------| -| `HETZNER_IP` | Gitea repo secrets | Hetzner VPS IPv4 address | -| `SSH_PRIVATE_KEY` | Gitea repo secrets | Private key for `deploy` user on Hetzner | - -```bash -# On Hetzner VPS — add TrueNAS runner's public key -# (generate keypair on TrueNAS.local: ssh-keygen -t ed25519 -f ~/.ssh/gitea_runner_deploy) -mkdir -p ~/.ssh && chmod 700 ~/.ssh -echo "" >> ~/.ssh/authorized_keys -chmod 600 ~/.ssh/authorized_keys -``` +| Stage | What | Fails on | +|-------|------|----------| +| **Checkout** | Clone repo | — | +| **Java Setup** | JDK 21 Temurin | — | +| **Maven Build** | Compile + unit tests + integration tests | Test failure, compilation error | +| **JaCoCo** | Coverage report (80% gate) | Coverage below threshold | +| **Frontend** | pnpm install + Vitest | Test failure | +| **Deploy** | docker-compose up (on main merge) | Build failure | --- -## 7. Database Backup +## 6. Environment Variables -### Automated Daily Backup +**File:** `.env.production` (on TrueNAS host, NOT committed to git) -Add to root crontab (`crontab -e`): - -```bash -# Daily backup at 03:00 UTC — keep 14 days -0 3 * * * docker exec cannamanage-db pg_dump \ - -U cannamanage_user \ - --format=custom \ - cannamanage | gzip > /opt/backups/cannamanage_$(date +\%Y\%m\%d).sql.gz - -# Cleanup backups older than 14 days -5 3 * * * find /opt/backups -name "cannamanage_*.sql.gz" -mtime +14 -delete -``` - -Create backup directory: -```bash -mkdir -p /opt/backups -chown deploy:deploy /opt/backups -``` - -### Restore from Backup - -```bash -# Restore (caution: this overwrites existing data) -gunzip -c /opt/backups/cannamanage_20260406.sql.gz | \ - docker exec -i cannamanage-db pg_restore \ - -U cannamanage_user \ - --clean \ - --dbname=cannamanage - -# Verify restore -docker exec cannamanage-db psql \ - -U cannamanage_user \ - -d cannamanage \ - -c "SELECT COUNT(*) FROM clubs;" -``` - -### Offsite Backup (Optional) - -For additional redundancy, sync backups to Hetzner Object Storage: - -```bash -# Install s3cmd and configure with Hetzner S3-compatible endpoint -s3cmd sync /opt/backups/ s3://cannamanage-backups/ -``` +| Variable | Purpose | Example | +|----------|---------|---------| +| `DB_USER` | PostgreSQL username | `cannamanage` | +| `DB_PASSWORD` | PostgreSQL password | (generated) | +| `JWT_SECRET` | JWT signing key (256-bit) | (generated) | +| `NEXTAUTH_SECRET` | NextAuth session encryption | (generated) | +| `STRIPE_SECRET_KEY` | Stripe API secret | `sk_live_...` | +| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing | `whsec_...` | +| `MAIL_HOST` | SMTP server | `smtp.example.com` | +| `MAIL_USERNAME` | SMTP user | `noreply@cannamanage.de` | +| `MAIL_PASSWORD` | SMTP password | (secret) | --- -## 8. Monitoring & Health Checks - -### Spring Boot Actuator - -The application exposes health endpoints via `spring-boot-actuator`: - -```bash -# Full health detail (requires ROLE_ADMIN JWT) -GET /actuator/health - -# Example response -{ - "status": "UP", - "components": { - "db": { "status": "UP", "details": { "database": "PostgreSQL", "validationQuery": "isValid()" } }, - "diskSpace": { "status": "UP", "details": { "total": 42GB, "free": 30GB } }, - "ping": { "status": "UP" } - } -} -``` - -Expose only `health` and `info` publicly in `application-production.yml`: -```yaml -management: - endpoints: - web: - exposure: - include: health,info - endpoint: - health: - show-details: when-authorized -``` - -### Log Locations - -| Source | Location | -|--------|----------| -| Application logs | `docker compose logs -f app` | -| Nginx access logs | `/var/log/nginx/access.log` | -| Nginx error logs | `/var/log/nginx/error.log` | -| PostgreSQL logs | `docker compose logs db` | -| Sentry (errors) | `https://sentry.io/organizations//` | - -### Alerting - -Configure Sentry to email on new errors: -1. Set `SENTRY_DSN` in `.env` -2. Add `io.sentry:sentry-spring-boot-starter-jakarta:7.x` to POM -3. Sentry auto-captures all unhandled exceptions with full stack trace - -Simple uptime check via `cron` + email: - -```bash -# Health check every 5 minutes — email on 3 consecutive failures -*/5 * * * * /opt/cannamanage/scripts/health_check.sh -``` +## 7. Backup Strategy ```bash #!/bin/bash -# /opt/cannamanage/scripts/health_check.sh -HEALTH_URL="https://app.cannamanage.de/actuator/health" -FAIL_COUNT_FILE="/tmp/cannamanage_health_fails" +# deploy/backup.sh — runs daily via cron +DATE=$(date +%Y-%m-%d_%H%M) +BACKUP_DIR="/mnt/pool/backups/cannamanage" -HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_URL") -if [ "$HTTP_STATUS" != "200" ]; then - FAILS=$(cat "$FAIL_COUNT_FILE" 2>/dev/null || echo 0) - FAILS=$((FAILS + 1)) - echo "$FAILS" > "$FAIL_COUNT_FILE" - if [ "$FAILS" -ge 3 ]; then - echo "CannaManage health check failed $FAILS times" | \ - mail -s "ALERT: CannaManage DOWN" admin@cannamanage.de - fi -else - echo 0 > "$FAIL_COUNT_FILE" -fi +# PostgreSQL dump +docker exec cannamanage-db pg_dumpall -U ${DB_USER} | gzip > "${BACKUP_DIR}/db_${DATE}.sql.gz" + +# Retain last 30 days +find "${BACKUP_DIR}" -name "db_*.sql.gz" -mtime +30 -delete ``` +| What | Frequency | Retention | +|------|-----------|-----------| +| PostgreSQL full dump | Daily (cron) | 30 days | +| ZFS snapshots | Hourly (TrueNAS) | 7 days | +| Git repository | Every push (Gitea) | Permanent | + --- -## 9. SSL Certificate Renewal +## 8. Deployment Commands -Let's Encrypt certificates expire after 90 days. Certbot handles renewal automatically: +### Initial Setup ```bash -# Test renewal (dry run — no actual renewal) -certbot renew --dry-run - -# Manual renewal -certbot renew --nginx - -# Reload Nginx after renewal -docker exec cannamanage-nginx nginx -s reload -``` - -### Auto-Renewal via Cron - -```bash -# Renew at 02:00 UTC on the 1st and 15th of each month -0 2 1,15 * * certbot renew --quiet --nginx && \ - docker exec cannamanage-nginx nginx -s reload -``` - -Certbot only renews when the certificate is less than 30 days from expiry — safe to run frequently. - ---- - -## 10. Rollback Procedure - -If a deployment causes issues: - -```bash -# On VPS — list recent images -docker images cannamanage --format "table {{.Tag}}\t{{.CreatedAt}}" - -# Roll back to previous SHA +# Clone on TrueNAS +git clone http://truenas.local:30008/pplate/cannamanage.git /opt/cannamanage cd /opt/cannamanage -VERSION= docker compose up -d app -# Verify health after rollback -docker compose ps app -curl https://app.cannamanage.de/actuator/health +# Create .env.production +cp .env.example .env.production +# Edit with real values... + +# Start all services +docker compose -f docker-compose.truenas.yml --env-file .env.production up -d + +# Verify +docker compose -f docker-compose.truenas.yml ps +curl -k https://cannamanage.plate-software.de/api/v1/health ``` -If database migrations were applied and rollback is needed: -1. Restore from last backup (see Section 7) -2. Redeploy the previous image version -3. Flyway baseline the schema at previous version +### Redeploy After Push -> **Note:** Flyway migrations are append-only and forward-only. Design migrations to be reversible where possible (add columns before removing old ones, etc.). +```bash +cd /opt/cannamanage +git pull origin main +docker compose -f docker-compose.truenas.yml --env-file .env.production up -d --build +``` + +### Logs + +```bash +# All services +docker compose -f docker-compose.truenas.yml logs -f + +# Backend only +docker logs -f cannamanage-app + +# Database +docker logs -f cannamanage-db +``` + +--- + +## 9. Monitoring + +| Check | Method | Alert | +|-------|--------|-------| +| Application health | `GET /api/v1/health` (Spring Actuator) | HTTP 200 expected | +| Database connectivity | Docker healthcheck (pg_isready) | Container restart | +| Disk usage | ZFS pool monitoring (TrueNAS) | >80% alert | +| TLS certificate | Certbot auto-renewal (cron) | 30-day warning | +| Container status | `docker compose ps` | Any "unhealthy" or "exited" | + +--- + +## 10. Troubleshooting + +| Issue | Diagnosis | Fix | +|-------|-----------|-----| +| 502 Bad Gateway | Backend not started | `docker compose logs backend` — check for OOM or startup errors | +| Database connection refused | DB container unhealthy | `docker compose restart db` — check pgdata volume | +| TLS cert expired | Certbot renewal failed | `certbot renew --nginx` | +| Port 443 not reachable | Router forwarding lost | Re-add port forward rule on router | +| Out of disk | ZFS pool full | `docker system prune -a` + check backup retention | +| Frontend 500 | NEXTAUTH_SECRET mismatch | Verify `.env.production` matches container env | diff --git a/CannaManage-10-Retrospective.md b/CannaManage-10-Retrospective.md index 0ec0623..434e464 100644 --- a/CannaManage-10-Retrospective.md +++ b/CannaManage-10-Retrospective.md @@ -1,7 +1,231 @@ # 10 — Sprint Retrospectives **Project:** CannaManage — B2B SaaS for German Cannabis Social Clubs -**Last updated:** 2026-06-12 +**Last updated:** 2026-06-19 + +--- + +## Sprint 14 Retrospective — Marketing & Monetization + +**Sprint:** 14 — Landing Page, Login Redesign, Pricing Page, Storage Quotas +**Period:** 2026-06-18 (AI-assisted sprint) +**Mode:** Solo development, AI-assisted (Claude Opus via Roo Orchestrator) +**Outcome:** ✅ Complete — Marketing landing page, pricing tiers, login UX redesign, storage quotas + +### What Went Well ✅ + +- **Landing page with feature showcase** creates professional first impression for potential club customers +- **Pricing page with tier comparison** enables self-service sign-up without sales calls +- **Login redesign** improves onboarding UX — clearer CTAs, better error states +- **Storage quotas per subscription tier** — clean enforcement without breaking existing users + +### Key Decisions Made 📋 + +| Decision | Rationale | +|----------|-----------| +| Marketing pages as SSR (not static) | SEO benefits, dynamic pricing from backend | +| Three subscription tiers | Starter (small clubs), Professional (medium), Enterprise (large) | +| Storage quotas as soft limits | Warn at 80%, block at 100%, admin can override | +| Login page as marketing entry point | First thing users see — must look professional | + +--- + +## Sprint 13 Retrospective — Production Hardening + +**Sprint:** 13 — Security Fixes, CI/CD Quality Gates, Repo Cleanup +**Period:** 2026-06-17 (AI-assisted sprint) +**Outcome:** ✅ Complete — Security fixes, CI quality gates, repository cleanup + +### What Went Well ✅ + +- **Gitea Actions CI pipeline** now runs full test suite with PostgreSQL 16 service container +- **Security audit** identified and fixed several issues (XSS in forum, CSRF token handling) +- **Repository cleanup** removed dead code, unused dependencies, and test artifacts +- **Quality gates** prevent merging code below 80% coverage + +### Key Decisions Made 📋 + +| Decision | Rationale | +|----------|-----------| +| PostgreSQL service container (not Testcontainers in CI) | More reliable in Gitea Actions environment | +| Branch protection on main | Require passing CI before merge | +| Snyk integration | Automated dependency vulnerability scanning | + +--- + +## Sprint 12 Retrospective — Golden Test Standard + +**Sprint:** 12 — Documents Page Integration, UX Improvements, Integration Test Hardening +**Period:** 2026-06-16 (AI-assisted sprint) +**Outcome:** ✅ Complete — Documents page fully integrated, UX polish, test infrastructure hardened + +### What Went Well ✅ + +- **Documents page** now supports upload, download, categorization, and retention policies +- **UX improvements** across all pages: better loading states, consistent error handling +- **Integration test hardening** — eliminated flaky tests, added retry logic for async operations +- **Panel review process** caught edge cases in document permissions + +### Key Decisions Made 📋 + +| Decision | Rationale | +|----------|-----------| +| File storage on local volume (not S3) | Simpler for self-hosted, DSGVO-compliant data locality | +| Retention categories per document | Legal requirement: different documents have different retention periods | +| Soft-delete for documents | Allow recovery within retention period | + +--- + +## Sprint 11 Retrospective — Backend Test Coverage + +**Sprint:** 11 — JaCoCo, ~250 New Tests, 80% Coverage Target +**Period:** 2026-06-15 (AI-assisted sprint) +**Outcome:** ✅ Complete — Coverage raised from ~45% to ~82%, quality gates established + +### What Went Well ✅ + +- **JaCoCo 80% gate** now blocks any PR that drops below threshold +- **~250 new tests** across all service classes — not just happy paths, edge cases too +- **ComplianceService 100%** — every legal rule has a test backing it +- **Testcontainers adoption** eliminated all H2-specific test issues +- **Test naming convention** established: `method_scenario_expected()` + +### What Was Challenging ⚠️ + +- **Writing tests for legacy service code** required some refactoring for testability +- **Testcontainers startup time** adds ~15s per test class — mitigated with `@Testcontainers` shared instances +- **Mocking multi-tenant context** required custom test utilities for `TenantContext` + +### Key Decisions Made 📋 + +| Decision | Rationale | +|----------|-----------| +| 80% overall, 100% ComplianceService | Compliance is legal obligation; rest follows best practice | +| Testcontainers over H2 | Real PostgreSQL catches real issues | +| No test coverage for DTOs/entities | Boilerplate coverage inflates numbers without value | + +--- + +## Sprint 10 Retrospective — Smart Payment Import + +**Sprint:** 10 — Bank Statement Import (MT940/CAMT053/CSV), Auto-Matching +**Period:** 2026-06-14 (AI-assisted sprint) +**Outcome:** ✅ Complete — Bank import pipeline, auto-matching, manual review UI + +### What Went Well ✅ + +- **Multi-format bank import** (MT940, CAMT053, CSV) handles all common German bank export formats +- **Auto-matching algorithm** correctly matches ~85% of incoming payments to member fees +- **Import session workflow** (upload → preview → confirm) prevents accidental data corruption +- **Unmatched payment review UI** lets treasurer manually assign remaining 15% + +### Key Decisions Made 📋 + +| Decision | Rationale | +|----------|-----------| +| Client-side parsing (not backend) | Keeps sensitive bank data in browser until confirmed | +| Fuzzy matching by amount + reference | German bank transfers often have garbled reference text | +| Import session as state machine | PENDING → REVIEWING → CONFIRMED → COMPLETED prevents partial imports | +| Batch processing with flush/clear | Large statements (1000+ transactions) need memory management | + +--- + +## Sprint 9 Retrospective — Berichtszentrale (Report Center) + +**Sprint:** 9 — Report Center, Authority-Ready Exports, Generated Reports +**Period:** 2026-06-13 (AI-assisted sprint) +**Outcome:** ✅ Complete — 8 report types, PDF/CSV export, compliance dashboard + +### What Went Well ✅ + +- **8 report types** covering all CanG compliance obligations (monthly, annual, member-list, destruction, transport, propagation, prevention, compliance-status) +- **Authority-ready PDF format** matches what German authorities expect to see +- **Compliance dashboard** gives club admins a single view of their compliance status +- **Deadline tracking** alerts clubs before compliance deadlines + +### Key Decisions Made 📋 + +| Decision | Rationale | +|----------|-----------| +| OpenPDF for all reports | LGPL, no license cost, good table support | +| Report generation async (background) | Large reports can take 10-30s | +| Pre-built templates per report type | Authorities expect specific formats | +| Compliance deadlines as entity | Track, alert, and mark as completed | + +--- + +## Sprint 8 Retrospective — Vereinsverwaltung (Club Governance) + +**Sprint:** 8 — Club Treasury, General Assembly, Document Archive, Board Management +**Period:** 2026-06-12 (AI-assisted sprint) +**Outcome:** ✅ Complete — Finance module, assembly voting, document management, board member tracking + +### What Went Well ✅ + +- **Club Treasury** with income/expense tracking, categorization, and balance reports +- **General Assembly** module with agenda items, voting (secret + open), quorum validation +- **Document Archive** with upload, categorization, and retention period enforcement +- **Board Management** tracks current board composition with term dates + +### Key Decisions Made 📋 + +| Decision | Rationale | +|----------|-----------| +| Secret ballot as default | German Vereinsrecht requires secret votes for board elections | +| Treasury separate from payment import | Different concerns: treasury = overview, import = automation | +| Document retention per CanG | Cannabis-specific documents: 5-year retention minimum | +| Board terms as date ranges | Enables historical board composition queries | + +--- + +## Sprint 7 Retrospective — Communication & Community + +**Sprint:** 7 — Info Board, Club Events Calendar, Club-Internal Forum, Notifications +**Period:** 2026-06-12 (AI-assisted sprint) +**Outcome:** ✅ Complete — Full community communication stack + +### What Went Well ✅ + +- **Info Board** (Schwarzes Brett) provides a WhatsApp-alternative for club announcements +- **Events Calendar** with RSVP tracking and recurring events +- **Forum** with threads, posts, and moderation — clubs don't need external Discord/Telegram +- **Notification system** unifies all alerts (push, email, in-app) with per-user preferences + +### Key Decisions Made 📋 + +| Decision | Rationale | +|----------|-----------| +| Forum over external chat | DSGVO compliance requires data stays in our system | +| Notifications as unified system | One preference center for all notification types | +| Event RSVP with capacity limits | Clubs have physical space constraints | +| Info Board moderation by admin/staff | Prevent misuse, keep content relevant | + +--- + +## Sprint 6 Retrospective — Production Readiness + +**Sprint:** 6 — DSGVO Consent, Stripe Payments, Audit Log, Grow Calendar, Notifications, PWA +**Period:** 2026-06-12 (AI-assisted sprint) +**Outcome:** ✅ Complete — All launch-critical features delivered + +### What Went Well ✅ + +- **DSGVO consent management** with granular consent types, revocation, and data export +- **Stripe integration** supporting SEPA, PayPal, and Credit Card — covers all common German payment methods +- **Audit log** provides immutable trail for all compliance-relevant actions +- **Grow calendar** with cycle tracking, sensor readings, harvest projections +- **PWA** with service worker enables offline access to key data +- **TrueNAS deployment** works — simpler and cheaper than Hetzner VPS + +### Key Decisions Made 📋 + +| Decision | Rationale | +|----------|-----------| +| TrueNAS over Hetzner VPS | Cost savings, local network, ZFS backups included | +| Stripe for all payment types | Single integration for SEPA + PayPal + Card | +| Audit log as append-only | Legal requirement: compliance trail must be immutable | +| PWA over native app | Cross-platform, no app store approval, faster iteration | +| Consent per data category | DSGVO requires granular consent (not just one checkbox) | --- @@ -9,123 +233,48 @@ **Sprint:** 5 — React Query Integration, Docker Compose Full-Stack, Staff CRUD, System Tests **Period:** 2026-06-12 (single-day intensive sprint, AI-assisted) -**Mode:** Solo development, AI-assisted (Claude Opus via Roo Orchestrator) **Outcome:** ✅ Complete — React Query mock fallback, Docker Compose stack, Staff UI, 190+ automated tests -**Key tech:** @tanstack/react-query, Vitest, MSW, Docker Compose, Next.js 15.5.18 ### What Went Well ✅ -**React Query mock fallback pattern works without backend.** The `stale-while-revalidate` strategy with automatic fallback to mock data when the API is unreachable means frontend development never blocks on backend availability. Developers can work offline, tests run without external dependencies, and the transition to real API calls is a configuration change — not a rewrite. - -**Multi-persona review process is now mature (90% first-pass approval).** After 4 sprints of iteration, the review pipeline (Planner → Plan Reviewer → Security Reviewer → Code Reviewer) now catches issues early enough that 9 out of 10 implementations pass on the first review cycle. The review checklist has been refined to eliminate false positives while catching real issues. - -**Vitest + MSW setup was smooth.** The combination of Vitest (fast unit test runner with native ESM support) and MSW (Mock Service Worker for API mocking) provides sub-second test feedback during development. The MSW handlers mirror the real API contract from `src/types/api.ts`, so tests validate against the actual interface. - -**Full staff CRUD UI delivered.** The staff management page with invite flow, permission editor (8 granular permissions), role assignment, and status management — all integrated with React Query for optimistic updates and automatic cache invalidation. - -**SQL seed + API-driven system tests provide end-to-end confidence.** The Playwright system tests seed the database via SQL, then drive the full application through the browser — login, navigate, create records, verify persistence. This catches integration issues that unit tests cannot. - -### What Was Challenging ⚠️ - -**Docker Compose backend build — Alpine DNS issues.** The multi-stage Docker build using Alpine-based images hit DNS resolution failures when fetching Maven dependencies. The Alpine musl libc DNS resolver behaves differently from glibc, causing intermittent `UnknownHostException` during `mvn package`. Fix: switched to Debian-slim base image for the build stage. - -**Next.js 15.5.18 had breaking peer deps for ESLint plugins.** Upgrading to Next.js 15.5.18 introduced peer dependency conflicts with `eslint-config-next` and several Tailwind CSS plugins. The resolution required pinning specific versions in `package.json` and adding `pnpm.overrides` for transitive dependencies. Cost ~45 minutes of debugging. - -**Per-component loading states required careful UX thought.** With React Query managing each resource independently, the page could show 3-4 different loading spinners simultaneously. The solution was per-component skeleton states (not spinners) with staggered appearance delays to avoid "flash of loading." +- **React Query mock fallback pattern** — frontend works without backend via stale-while-revalidate + automatic mock fallback +- **Multi-persona review process** now mature (90% first-pass approval) +- **Vitest + MSW setup** provides sub-second test feedback +- **Full staff CRUD UI** with invite flow, permission editor, role assignment +- **SQL seed + API-driven system tests** provide end-to-end confidence ### Key Decisions Made 📋 -| Decision | Rationale | Alternatives Rejected | -|----------|-----------|----------------------| -| @tanstack/react-query over SWR | Better devtools, more granular cache control, optimistic updates built-in, larger community | SWR (simpler but less powerful), plain fetch (no caching) | -| Per-component loading (not page-level) | Each data source loads independently — faster perceived performance | Page-level spinner (blocks everything), React Suspense boundaries (still experimental with App Router) | -| Stale-while-revalidate + offline mock fallback | Works without backend running; graceful degradation; instant dev startup | Strict online-only (blocks offline dev), no caching (slow UX) | -| Full staff CRUD in Sprint 5 | Staff management was the #1 user request from Sprint 4 testing; natural extension of existing permission model | Defer to Sprint 6 (users waiting too long), partial staff (confusing UX) | -| SQL seed + API-driven system tests | True end-to-end confidence; catches integration bugs that mocked tests miss | Only unit tests (insufficient), manual testing (not repeatable) | -| Vitest over Jest | Native ESM, faster execution, better TypeScript support, compatible with MSW v2 | Jest (slow transform, CJS-first), node:test (no ecosystem) | - -### Risks Going Forward ⚠️ - -| Risk | Likelihood | Impact | Mitigation | -|------|-----------|--------|------------| -| React Query cache invalidation bugs in production | Medium | Medium — stale data shown to users | Aggressive `staleTime` defaults (30s); manual invalidation on mutations | -| Docker Compose resource usage on small VPS | Medium | Low — can tune container limits | Set memory limits per service; consider removing dev containers from production compose | -| 190+ tests slow CI pipeline | Low | Low — currently <3 minutes total | Parallelize Vitest; cache Playwright browsers; split E2E from unit in CI | -| Next.js 15.x upgrade churn | Medium | Medium — each minor may break plugins | Pin exact versions; only upgrade when needed; test thoroughly before merging | - -### Sprint 6 Goals (Planned) - -- [ ] DSGVO consent management UI (cookie banner + data processing agreement) -- [ ] Stripe payment integration (subscription billing for clubs) -- [ ] Grow calendar (cultivation tracking with harvest projections) -- [ ] PWA / offline support (service worker, cached pages) -- [ ] Deploy to Hetzner VPS (Docker Compose production stack) +| Decision | Rationale | +|----------|-----------| +| @tanstack/react-query over SWR | Better devtools, granular cache control, optimistic updates | +| Per-component loading (not page-level) | Each data source loads independently — faster perceived performance | +| Vitest over Jest | Native ESM, faster execution, better TypeScript support | --- -## Sprint 4 Retrospective — Frontend MVP (Admin Dashboard + Member Portal) +## Sprint 4 Retrospective — Frontend MVP -**Sprint:** 4 — Frontend MVP with Shadboard, Next.js 15, React 19, shadcn/ui, Tailwind 4 +**Sprint:** 4 — Admin Dashboard + Member Portal (Next.js 15, React 19, shadcn/ui) **Period:** 2026-06-12 (single-day intensive sprint, AI-assisted) -**Mode:** Solo development, AI-assisted (Claude Opus via Roo Orchestrator) -**Outcome:** ✅ Complete — 143 files, ~23,568 LoC, 14 routes, 6 Playwright E2E tests passing -**Commit:** `fe6e96d` +**Outcome:** ✅ Complete — 143 files, ~23,568 LoC, 14 routes, 6 Playwright E2E tests ### What Went Well ✅ -**Shadboard starter kit saved weeks of boilerplate.** Using the MIT-licensed Shadboard template (Next.js 15 + shadcn/ui + Tailwind 4) as a foundation meant sidebar layout, theme system, command palette, and responsive behavior came pre-built. Estimated 2-3 weeks of work avoided. The structure was clean enough to extend without fighting it. - -**Persona review caught compliance gaps early.** Before coding the distribution form, reviewing the user stories against CanG §19 requirements identified the need for under-21 reduced limits (30g/month), suspension/expulsion blocking, and immutable audit trail indicators in the UI — all of which were built into the form from the start rather than retrofitted. - -**Playwright E2E caught the NextAuth deadlock immediately.** The very first E2E run revealed that NextAuth v5's middleware was blocking indefinitely when the backend wasn't reachable. Without E2E tests, this would have been a production-discovery bug. The fix (AbortController with 3-second timeout in the auth fetch) took 10 minutes once diagnosed. - -**Dark + light mode from Day 1 was low-effort, high-value.** Since Shadboard already had next-themes integrated and Tailwind 4's `dark:` variant is zero-config, supporting both themes required only choosing color variables — no structural changes. The radial quota visualization in the portal looks excellent in both modes. - -**i18n architecture (next-intl) scales cleanly.** All 200+ UI strings live in `messages/de.json` and `messages/en.json`. No hardcoded text in components. Adding a language is just a JSON file. The translation structure mirrors the route structure (dashboard.*, members.*, portal.*) making keys predictable. - -**Separate route groups for admin vs. portal.** Using Next.js route groups `(dashboard-layout)` and `(portal)` with independent layouts means the admin sidebar never leaks into the member portal (which uses a top nav). Clean separation without code duplication. - -### What Was Challenging ⚠️ - -**NextAuth v5 middleware deadlocked without backend.** The default NextAuth behavior waits indefinitely for the backend auth endpoint. During frontend-only development (backend not running), this caused all protected routes to hang. Fix: AbortController timeout wrapper around the fetch in the `authorize` callback + graceful error handling in middleware. - -**Tailwind 4 breaking changes from v3 documentation.** Most online examples and Stack Overflow answers reference Tailwind v3 syntax. Tailwind 4 changes include: new CSS-first configuration (`@theme` in CSS instead of `tailwind.config.js`), `@apply` deprecated in favor of direct utility classes, and some color utilities renamed. Required reading the v4 migration guide carefully. - -**Mock data architecture decisions.** Without the real backend running, all pages use mock data from `src/data/mock/`. The interface contracts needed to be designed carefully so the migration to real API calls (Sprint 5) is a drop-in replacement. TypeScript interfaces in `src/types/api.ts` define the shared contract. - -**Multi-step distribution form state management.** The 4-step distribution form (member selection → quota check → batch+amount → confirmation) required careful state threading across steps without a state management library. Solved with React `useState` + prop drilling since the form is a single page component. This is fine for 4 steps but wouldn't scale to 10+. +- **Shadboard starter kit** saved weeks of boilerplate (MIT-licensed) +- **Persona review** caught compliance gaps early +- **Dark + light mode from Day 1** was low-effort, high-value +- **i18n architecture (next-intl)** scales cleanly +- **Separate route groups** for admin vs. portal ### Key Decisions Made 📋 -| Decision | Rationale | Alternatives Rejected | -|----------|-----------|----------------------| -| Shadboard (MIT) as starter kit | Pre-built layout, theme, sidebar, command palette. Saves 2-3 weeks. MIT license = no restrictions. | Custom scaffold (slow), Vercel templates (too generic), paid templates (license restrictions) | -| Node 22 LTS | Long-term support, stable for production. Required by some Next.js 15 features. | Node 20 (older LTS), Node 24 (too new, not LTS yet) | -| i18n from Day 1 (next-intl) | Cheaper to add from start than retrofit. German clubs may have English-speaking members. EU expansion possible. | Hardcoded German (faster short-term, expensive later), react-i18next (heavier, server components don't play well) | -| Dark mode default + light toggle | Cannabis club aesthetic suits dark mode. Outdoor mobile use needs light mode. Both supported via next-themes. | Dark-only (excludes bright-environment use), Light-only (boring, doesn't match brand) | -| Separate portal route group | Members get a simplified top-nav layout. Admins get a full sidebar. No layout collision. | Single layout with conditional rendering (messy), separate Next.js app (overkill) | -| pnpm over npm/yarn | Faster installs, strict dependency resolution, disk-efficient via hard links. Shadboard already configured for it. | npm (slower, phantom deps), yarn (Berry complexity) | -| Mock data in typed files | TypeScript interfaces enforce API contract. Easy to swap with real fetch calls. No backend dependency for UI work. | MSW (Mock Service Worker — adds complexity), json-server (external process) | - -### Risks Going Forward ⚠️ - -| Risk | Likelihood | Impact | Mitigation | -|------|-----------|--------|------------| -| Mock-to-real API migration breaks assumptions | Medium | Medium — pages may need refactoring | TypeScript interfaces as contract; API response types match backend DTOs | -| NextAuth session expiry UX | Low | Low — users see login page | Implement token refresh in `jwt` callback; show toast before expiry | -| Tailwind 4 ecosystem immaturity | Low | Low — workarounds exist | Pin Tailwind version; avoid bleeding-edge plugins | -| No loading/error states in UI | Medium | Medium — poor UX on slow connections | Sprint 5: add React Suspense boundaries + error.tsx per route | -| Frontend tests don't cover real auth flow | High | Medium — false confidence | Sprint 5: E2E tests with real backend running in Docker Compose | - -### Sprint 5 Goals (Delivered ✅) - -- [x] Wire frontend to real backend API (React Query with mock fallback) -- [x] Staff management UI (full CRUD: invite, permissions editor, role assignment) -- [x] Full E2E + system test suite with backend running in Docker Compose -- [x] Loading states (per-component skeletons), error boundaries, toast notifications -- [x] Vitest + MSW unit testing infrastructure -- [ ] ~~DSGVO consent management flow~~ → deferred to Sprint 6 -- [ ] ~~WebSocket notifications~~ → deferred to Sprint 6 +| Decision | Rationale | +|----------|-----------| +| Shadboard (MIT) as starter kit | Pre-built layout, theme, sidebar. Saves 2-3 weeks | +| i18n from Day 1 (next-intl) | Cheaper to add from start than retrofit | +| Dark mode default + light toggle | Cannabis club aesthetic suits dark mode | +| pnpm over npm/yarn | Faster installs, strict dependency resolution | --- @@ -133,134 +282,25 @@ **Sprint:** 3 — Staff Permissions, Token Revocation, Member Portal, Reports, Prevention Officer **Period:** 2026-05-15 to 2026-06-12 -**Mode:** Solo development, AI-assisted (Claude Opus via Roo Orchestrator) -**Outcome:** ✅ Complete — 7 phases delivered, ~80 files, ~8,500 LoC, 67+ tests passing +**Outcome:** ✅ Complete — 7 phases delivered, ~80 files, ~8,500 LoC, 67+ tests ### What Went Well ✅ -**Seven-phase delivery cadence worked.** Breaking Sprint 3 into 7 discrete phases (Staff → Token Revocation → Club Settings/Invite → Reports → Portal → Prevention → Integration Tests) created natural milestones. Each phase was independently reviewable and testable. No phase took longer than 2 days. - -**OpenPDF over iText 7 was the right call.** The LGPL license means no license cost, no AGPL compliance headaches. The API is nearly identical to iText 5 (which most online examples reference). PDF reports with headers, footers, page numbers, and tables — all working correctly on the first integration test run. - -**Caffeine cache for token revocation is elegant.** O(1) lookup, no external dependency (no Redis needed), TTL matches token expiry so memory is bounded. The `TokenCleanupScheduler` handles DB garbage collection daily. Total complexity: ~80 lines of code for a production-ready revocation system. - -**Dual SecurityFilterChain pattern is clean.** Separating JWT (admin/staff API) from session (member portal) into two ordered filter chains eliminated all the config conflicts we'd have from mixing them. Each chain has independent CSRF, session, and auth rules. - -**Testcontainers proved their value immediately.** The first integration test run caught a Flyway migration issue (V4 column default not compatible with PostgreSQL strict mode) that H2 would have silently accepted. Real DB in tests = real confidence. - -**JSONB for staff permissions is flexible.** No join tables, no migration needed when adding a new permission enum value. A single `SET` field maps cleanly to a JSONB array column. Querying is still efficient for our scale. - -### What Was Challenging ⚠️ - -**Session auth and JWT in the same app requires careful ordering.** The `@Order(1)` / `@Order(2)` chain priority, plus correct `securityMatcher()` scoping, took two iterations to get right. The first attempt had the portal chain catching API requests. - -**OpenPDF font handling.** Default Helvetica works fine for ASCII/Latin-1, but German umlauts (ä, ö, ü) required explicitly using CP1252 encoding in the `BaseFont.createFont()` call. Caught in `PdfReportGeneratorTest`. - -**Invite flow email testing.** GreenMail (embedded SMTP for tests) was considered but ultimately we used Mockito to mock `JavaMailSender` in unit tests. The actual SMTP integration is tested manually against a local Mailhog instance. - -**30+ integration tests take ~45 seconds.** Testcontainers PostgreSQL startup adds ~8 seconds, and Spring Boot context load adds another ~12 seconds. The remaining time is actual test execution. Acceptable for CI, but not instant enough for TDD flow. Unit tests (< 3 seconds) remain the primary feedback loop. - -### Key Decisions Made 📋 - -| Decision | Rationale | Alternatives Rejected | -|----------|-----------|----------------------| -| OpenPDF over iText 7 | LGPL license — no cost, no AGPL compliance risk | iText 7 (AGPL/commercial), Apache PDFBox (lower-level API, more code) | -| Caffeine over Redis for revocation cache | No external infra needed; bounded memory via TTL; single-instance app (for now) | Redis (overkill for MVP), simple `ConcurrentHashMap` (no TTL eviction) | -| Dual SecurityFilterChain | Clean separation of JWT and session auth; no config conflicts | Single chain with conditional logic (messy), separate Spring Boot apps (over-engineering) | -| JSONB for permissions | Flexible, no joins, no migration for new permissions | Join table `staff_permissions` (normalized but more complex), bitmask (not human-readable) | -| Testcontainers over H2 for integration tests | Catches real PostgreSQL-specific behavior; Flyway migrations tested against real dialect | H2 in PostgreSQL mode (doesn't catch all dialect differences), embedded PostgreSQL (deprecated) | -| Session auth for member portal | Simpler UX for members (no token management); natural session expiry | JWT for portal too (adds complexity for non-technical users) | - -### Risks Going Forward ⚠️ - -| Risk | Likelihood | Impact | Mitigation | -|------|-----------|--------|------------| -| Frontend skill gap (React/Vite) | High | Medium — delays Sprint 4 | Start with admin dashboard only; use component libraries (shadcn/ui) | -| SMTP deliverability for invites | Medium | Low — alternative is manual password handoff | Use proper SPF/DKIM on `cannamanage.de`; Mailgun as fallback | -| Token revocation cache grows unbounded if no cleanup | Low | Low — scheduler runs daily | `TokenCleanupScheduler` + Caffeine TTL eviction | -| 45-second integration test suite slows CI | Medium | Low — annoying but not blocking | Parallel test execution (`maven-surefire-plugin` forks); test categorization | +- **OpenPDF over iText 7** — LGPL, no license cost, API identical to iText 5 +- **Caffeine cache for token revocation** — O(1) lookup, no Redis needed +- **Dual SecurityFilterChain** — clean separation of JWT (admin) and session (member) +- **Testcontainers** caught a Flyway migration issue that H2 would have hidden --- -## Sprint 0 Planning Retrospective +## Sprint 2 Retrospective — REST API -**Sprint:** 0 — Planning & Documentation -**Period:** 2026-04-04 to 2026-04-06 -**Mode:** Solo planning, AI-assisted documentation (Claude Sonnet 4.6 via Roo Orchestrator + Doc Writer modes) -**Outcome:** ✅ Complete — 10-document suite written, architecture locked +**Sprint:** 2 — 5 Controllers, JWT Auth, Spring Security 7, OpenAPI +**Outcome:** ✅ Complete — Full REST API with auth, docs, and tenant isolation --- -## What Went Well ✅ +## Sprint 1 Retrospective — Domain Foundation -**AI-assisted documentation at scale.** The complete documentation suite (10 documents, ~25,000 words total) was created in a single focused session using the Roo Orchestrator mode to coordinate multi-document generation. This would have taken 2–3 days manually. The quality is high enough to serve as actual implementation guidance — not placeholder text. - -**Legal analysis confirmed viability early.** The CanG compliance review (Phase 1) identified the key constraints (no public directory, no consumer-facing advertising, B2B-only) before any code was written. These became hard architectural constraints rather than late surprises. No "oh wait, we can't do that" moments during technical design. - -**Architecture decisions locked before code.** The shared-schema multi-tenancy decision, immutable distribution records design, and `ComplianceConstants` pattern were all decided and documented before a single line of production code was written. This is the correct order. Rework from late architectural pivots is far more expensive than planning time. - -**Compliance constants centralized from day zero.** Designing `ComplianceConstants.java` as the single source of truth for all CanG quota values (25g/day, 50g/month, etc.) prevents the most dangerous class of compliance bug: magic numbers scattered across the codebase that diverge when the law changes. - -**ComfyUI mockup images in minutes.** Generating 5 realistic UI mockup images with FLUX.1-schnell took approximately 8 minutes of wall-clock time. This provides a visual reference for the UI that would otherwise require a designer or Figma skills. The images are good enough for stakeholder presentations and early user research. - -**Test plan written before code.** TC-001 through TC-026 were defined against specifications, not against existing implementation. This forces clarity on what the code must do before writing it — the test cases are essentially executable requirements. - ---- - -## What Was Challenging ⚠️ - -**ComfyUI manual startup friction.** The ComfyUI image generation server does not auto-start with the system. This required manual service start and a retry cycle before image generation could proceed. The fix (systemd user service + auto-start lifespan check in `mcp-image-gen`) was implemented during this planning sprint but added unexpected overhead. - -**Solo developer timeline is ambitious.** The 18–24 month estimate for a production-ready SaaS while employed full-time at ADP Germany is tight. Sprint 1 goals are achievable; the risk accumulates in Sprints 3–6 when frontend work, billing integration, and PDF generation converge. The PrimeFaces JSF choice for MVP was deliberate to reduce this risk — existing Java frontend skills transfer directly. - -**Spring Boot 3 is not yet a "home" stack.** ADP work uses Jakarta EE (JBoss, CDI, JAX-RS). Spring Boot 3 shares the JPA/Hibernate mental model but diverges on dependency injection, auto-configuration, and application packaging. The learning curve is real but bounded — the `mss-failsafe` and `wellmann-shop` projects in `pi_mcps` demonstrate that the transition is manageable. - -**Next.js/React remains a significant gap.** The v2 frontend pivot to Next.js 15 + React 19 is the highest-skill-gap risk in the project. PrimeFaces buys time, but the clock starts ticking on React learning from Sprint 1. Deferring is correct; ignoring it is not. - -**No real user validation yet.** The entire architecture and pricing model is based on market research and regulatory reading, not on conversations with actual club administrators. The product may be solving the right problem in the wrong way. This is the most important open risk. - ---- - -## Key Decisions Made 📋 - -| Decision | Rationale | Alternatives rejected | -|----------|-----------|----------------------| -| Shared-schema multi-tenancy (single DB, `tenant_id` columns) | Lowest ops overhead for MVP; one DB to backup/restore; simpler Flyway migrations | Schema-per-tenant (complex provisioning), DB-per-tenant (expensive at scale) | -| Immutable distribution records (`@Column(updatable = false)`) | Legal integrity — audit logs must be tamper-proof; corrections via `RecallEvent`, not `UPDATE` | Mutable records (simpler but legally risky under CanG §26 record-keeping) | -| PrimeFaces JSF for MVP frontend | Leverages existing Jakarta EE skills; fastest path to working product; no JS build tooling required | React/Next.js (faster modern dev, but higher skill gap), Thymeleaf (less interactive) | -| No public club discovery — permanent architectural exclusion | CanG §§6–7 prohibit advertising cannabis to the general public; club lookup tool would likely constitute advertising | N/A — this is a legal constraint, not a design choice | -| `ComplianceConstants.java` single source of truth | Prevents magic number scatter; single change point when law evolves | Constants in each service (fragile), DB-configurable limits (dangerous — allows disabling compliance) | -| Hetzner VPS over AWS/GCP | Cost (€5.88/month vs €20+); EU data residency (GDPR); simpler ops for solo developer | AWS (expensive, complex), Fly.io (less EU clarity), Railway (vendor lock-in) | - ---- - -## Risks Going Forward ⚠️ - -| Risk | Likelihood | Impact | Mitigation | -|------|-----------|--------|------------| -| New German government tightens CanG (e.g. lower quota limits) | Medium | High — requires rapid compliance updates | `ComplianceConstants.java` centralizes all limits; update is a 1-file change + test re-run | -| Stripe flags account as cannabis-adjacent | Medium | Critical — billing becomes unusable | Use category "Vereinsverwaltung" (club management) in Stripe onboarding; prepare Mollie as fallback | -| Solo dev burnout / timeline slip | High | Medium — delayed launch, not cancellation | Strict MVP scope; PrimeFaces reduces frontend effort; no scope creep before first paying customer | -| Market timing risk — clubs adopt ad-hoc Excel/WhatsApp solutions | Medium | High — low willingness to pay for formal software | User research with 3+ clubs in Sprint 1 is mandatory before writing production code | -| Legal risk: CanG compliance interpretation | Low | High — criminal liability for club officers | Specialist cannabis law opinion (€300–500) before launch; not optional | -| Under-21 age calculation edge cases | Low | Medium — compliance bug | Birthday-based age calculation uses `Period.between()`, not year subtraction; tested in TC-013/014 | - ---- - -## Metrics - -| Metric | Value | -|--------|-------| -| Planning duration | 3 days (2026-04-04 to 2026-04-06) | -| Documents created | 10 (01-PROJECT-CHARTER through 10-RETROSPECTIVE) | -| Estimated total words | ~25,000 | -| Test cases defined | 26 | -| API endpoints specified | 30+ | -| JPA entities designed | 8 | -| UI screens wireframed | 6 | -| UI mockup images generated | 5 | -| Lines of production code written | **0** | -| Architecture decisions logged | 6 major | -| Open risks identified | 6 | - -The ratio of planning output to production code written is intentional. Phase 0 exists to eliminate avoidable rework — the most expensive kind. +**Sprint:** 1 — 8 Entities, ComplianceService, Flyway V1 +**Outcome:** ✅ Complete — Core domain model with compliance enforcement from Day 1 diff --git a/CannaManage-11-Features.md b/CannaManage-11-Features.md new file mode 100644 index 0000000..2204d02 --- /dev/null +++ b/CannaManage-11-Features.md @@ -0,0 +1,275 @@ +# 11 — Features + +**Project:** CannaManage — B2B SaaS for German Cannabis Social Clubs +**Version:** 14.0 (Sprint 14) +**Date:** 2026-06-19 + +--- + +## Feature Overview by Navigation Group + +CannaManage provides a comprehensive management platform organized into four main groups plus member self-service and public marketing pages. + +--- + +## 🟢 Betrieb (Operations) + +### Dashboard +- **KPI Overview** — Real-time cards showing active members, monthly distributions, stock levels, compliance status +- **Activity Feed** — Recent actions across all modules +- **Quick Actions** — Common tasks (new distribution, add member, view reports) +- **Charts** — Distribution trends, stock levels over time (Recharts) + +### Mitglieder (Members) +- **Member Registry** — Full CRUD with search, filter, sort (TanStack Table) +- **Status Management** — Active, Suspended, Expelled, Waiting List +- **Age Verification** — Automatic under-21 detection for reduced limits +- **Membership Dates** — Join date, probation period tracking +- **DSGVO Consent** — Per-member consent status and history +- **Member Import** — Bulk CSV import for existing clubs +- **Quota Dashboard** — Per-member remaining daily/monthly quota + +### Ausgabe (Distributions) +- **4-Step Distribution Form** — Member selection → Quota check → Batch + amount → Confirmation +- **Real-time Quota Enforcement** — CanG §19: 50g/month adults, 30g/month under-21, 25g/day all +- **THC Limit Enforcement** — CanG §19: max 10% THC for under-21 members +- **Batch Selection** — Only shows batches with sufficient stock +- **Distribution History** — Full audit trail with timestamps +- **Receipt Generation** — Printable distribution receipt +- **Suspension Block** — Prevents distributions to suspended members + +### Lager (Stock / Inventory) +- **Batch Management** — Create batches with strain, quantity, THC/CBD content, harvest date +- **Strain Library** — Reusable strain definitions with genetics, type (Indica/Sativa/Hybrid) +- **Stock Movements** — In (harvest/purchase), Out (distribution), Loss (destruction) +- **Low-Stock Alerts** — Configurable threshold notifications +- **Expiry Tracking** — Batches with shelf-life monitoring +- **Stock Reports** — Current inventory, movement history, valuation + +### Anbau (Grow Calendar) +- **Grow Cycles** — Track cultivation from seed to harvest +- **Growth Stages** — Seedling, Vegetative, Flowering, Harvest, Drying, Curing +- **Sensor Readings** — Temperature, humidity, CO2, pH logging +- **Harvest Projections** — Estimated yield based on cycle history +- **Propagation Sources** — Track seeds, clones, mother plants +- **Environmental Alerts** — Threshold-based sensor notifications + +--- + +## 💬 Kommunikation (Communication) + +### Schwarzes Brett (Info Board) +- **Club Announcements** — Admin/staff post updates for all members +- **Pinned Posts** — Important announcements stay at top +- **Categories** — Organize posts by topic +- **Read Tracking** — Know which members saw important announcements +- **Rich Text** — Formatted posts with links and basic styling + +### Kalender (Events Calendar) +- **Club Events** — Create events with date, time, location, description +- **RSVP Tracking** — Members confirm attendance with capacity limits +- **Recurring Events** — Weekly/monthly/yearly recurrence rules +- **Event Categories** — Social, Meeting, Harvest, Maintenance, etc. +- **Calendar View** — Month/week/day views with event cards +- **Email Reminders** — Automatic reminder before events + +### Forum +- **Discussion Threads** — Members start topics, reply with threaded conversations +- **Categories/Boards** — Organized by topic (General, Growing Tips, Strain Reviews, etc.) +- **Moderation** — Admin/staff can pin, lock, or delete threads +- **Mentions** — @mention other members in posts +- **DSGVO Compliant** — All forum data stays within the platform (no external services) + +--- + +## 📋 Verwaltung (Administration) + +### Finanzen — Übersicht (Finance Overview) +- **Club Treasury** — Income and expense tracking with categories +- **Transaction Log** — All financial movements with receipts +- **Expense Categories** — Rent, Utilities, Seeds, Equipment, Insurance, etc. +- **Balance Reports** — Monthly/quarterly/annual financial summaries +- **Membership Fee Tracking** — Which members have paid, who's overdue +- **Export** — CSV export for tax advisor + +### Finanzen — Import (Bank Statement Import) +- **Multi-Format Import** — MT940, CAMT053, CSV bank statement upload +- **Auto-Matching** — Automatically matches payments to member fees (~85% hit rate) +- **Import Sessions** — Upload → Preview → Confirm workflow prevents accidents +- **Manual Matching** — Review and assign unmatched transactions +- **Duplicate Detection** — Prevents importing the same statement twice +- **Match Rules** — Configurable matching by amount, reference text, IBAN + +### Versammlungen (General Assemblies) +- **Assembly Planning** — Date, time, location, agenda items +- **Agenda Management** — Add, reorder, assign presenters to agenda items +- **Voting** — Secret ballot and open voting with real-time results +- **Vote Types** — Yes/No, Multiple Choice, Board Election +- **Quorum Validation** — Checks minimum attendance before votes count +- **Minutes (Protokoll)** — Generate assembly minutes from votes and attendance +- **Ballot Counting** — Automatic tallying with result determination + +### Dokumente (Document Archive) +- **Document Upload** — Any file type with metadata +- **Categorization** — Contracts, Licenses, Protocols, Compliance, Insurance, etc. +- **Retention Policies** — Automatic retention period enforcement per category +- **Version History** — Track document revisions +- **Access Control** — Admin/staff only for sensitive documents +- **Search** — Full-text search across document metadata +- **Download** — Individual and bulk download + +### Vorstand (Board Management) +- **Board Composition** — Current board members with roles (Vorsitzender, Schatzmeister, etc.) +- **Term Tracking** — Start/end dates, election history +- **Historical View** — Past board compositions +- **Role Definitions** — Responsibilities per board position +- **Contact Information** — Board member contact details for authorities + +### Personal (Staff Management) +- **Staff Accounts** — Separate login credentials for club employees +- **Permission Editor** — 8 granular permissions configurable per staff member +- **Role Templates** — Pre-configured permission sets (Front Desk, Grow Manager, etc.) +- **Invite Flow** — Email invite → password setup → active account +- **Activity Log** — What each staff member did and when +- **Account Lifecycle** — Create, suspend, reactivate, delete + +--- + +## ✅ Compliance + +### Compliance-Status (Dashboard) +- **Overall Score** — Club-wide compliance health indicator +- **Area Breakdown** — Per-area compliance status (Distribution, Storage, Reporting, etc.) +- **Deadline Alerts** — Upcoming compliance deadlines with countdown +- **Issue Tracking** — Open compliance issues requiring attention +- **Recommendations** — Actionable steps to improve compliance + +### Berichtszentrale (Report Center) +- **8 Report Types:** + 1. **Monthly Report** — Monthly distribution summary per member + 2. **Annual Report** — Yearly overview for authorities + 3. **Member List** — Current membership roster (authority format) + 4. **Destruction Record** — Cannabis destruction documentation + 5. **Transport Record** — Cannabis transport documentation + 6. **Propagation Sources** — Seed/clone origin documentation + 7. **Prevention Activities** — Youth prevention measures documentation + 8. **Compliance Status** — Overall compliance snapshot +- **Export Formats** — PDF (authority-ready), CSV (data processing) +- **Scheduled Generation** — Auto-generate monthly/annual reports +- **Report History** — Archive of all generated reports + +### Protokoll (Audit Log) +- **Immutable Trail** — Every compliance-relevant action logged +- **Actor Tracking** — Who did what, when, from which IP +- **Action Types** — Distribution, Stock Change, Member Status Change, Report Generation, etc. +- **Filtering** — By date, actor, action type, entity +- **Export** — CSV export for external audit +- **Retention** — 10-year retention per CanG requirements + +### Berichte (Generated Reports) +- **Report Viewer** — View generated reports inline +- **Download** — PDF download for submission to authorities +- **Report Queue** — Status of pending report generation +- **Regeneration** — Re-generate reports with updated data + +--- + +## 👤 Member Portal (Self-Service) + +### Portal Dashboard +- **Quota Visualization** — Radial chart showing remaining daily/monthly quota +- **Recent Distributions** — Last 5 distributions with details +- **Upcoming Events** — Next club events with RSVP +- **Announcements** — Latest info board posts + +### Distribution History +- **Full History** — All distributions received with date, amount, strain +- **Monthly Summary** — Aggregated monthly consumption +- **Download** — Export personal history as CSV + +### Profile Management +- **Personal Data** — Update contact information +- **Consent Management** — View and revoke DSGVO consents +- **Data Export** — Request personal data export (DSGVO Art. 15) +- **Account Deletion** — Request account deletion (DSGVO Art. 17) + +### Events +- **Upcoming Events** — Browse club events +- **RSVP** — Confirm or decline attendance +- **My Events** — View events where RSVP was submitted + +--- + +## 🌐 Marketing (Public Pages) + +### Landing Page +- **Hero Section** — Value proposition with CTA +- **Feature Showcase** — Key features with icons and descriptions +- **Social Proof** — Club testimonials / statistics +- **Pricing Teaser** — Link to pricing page +- **Legal Compliance** — CanG compliance messaging + +### Pricing Page +- **Tier Comparison** — Starter / Professional / Enterprise +- **Feature Matrix** — What's included in each tier +- **FAQ** — Common pricing questions +- **CTA** — Sign-up buttons per tier +- **Annual Discount** — Monthly vs. annual pricing + +### Login / Registration +- **Admin Login** — JWT-based authentication +- **Member Login** — Session-based (separate form) +- **Registration** — New club sign-up flow +- **Password Reset** — Email-based recovery +- **Professional Design** — First impression for new users + +--- + +## 🔧 Platform Features (Cross-Cutting) + +### Multi-Tenancy +- Schema-per-tenant isolation (PostgreSQL) +- Automatic tenant resolution from JWT +- Per-tenant Flyway migrations +- Clean tenant deletion (`DROP SCHEMA CASCADE`) + +### Internationalization (i18n) +- German (primary) + English +- All UI strings externalized in `messages/*.json` +- Date/number formatting per locale +- Easy to add new languages + +### Dark Mode + Light Mode +- System preference detection +- Manual toggle in UI +- All components styled for both modes +- next-themes integration + +### PWA (Progressive Web App) +- Service worker for offline access +- App manifest for "Add to Home Screen" +- Push notifications (Web Push API) +- Cached critical pages + +### Notifications +- **Channels:** In-app, Email, Push (PWA) +- **Preferences:** Per-user, per-notification-type +- **Compose:** Admin can send custom notifications to all/selected members +- **Rate Limiting:** Prevents notification spam + +### DSGVO Compliance +- Granular consent management (per data category) +- Data export (Art. 15 DSGVO) +- Right to erasure (Art. 17 DSGVO) — schema drop +- Consent revocation with immediate effect +- Cookie consent banner +- Privacy policy integration + +### Payments (Stripe) +- SEPA Direct Debit +- PayPal +- Credit/Debit Card +- Subscription management +- Invoice generation +- Webhook handling for payment events +- Storage quotas per subscription tier diff --git a/CannaManage-Home.md b/CannaManage-Home.md index cc60f5c..594bae9 100644 --- a/CannaManage-Home.md +++ b/CannaManage-Home.md @@ -2,75 +2,95 @@ **Multi-tenant compliance platform for German Cannabis Social Clubs (Anbauvereinigungen)** -> **Status:** Sprint 5 ✅ Complete | **Stack:** Java 21 + Spring Boot 4.0.6 + Next.js 15.5.18 | **Tests:** 190+ automated tests | **Legal:** CanG §19 compliant +> **Status:** Sprint 14 ✅ Complete | **Stack:** Java 21 + Spring Boot 4.0.6 + Next.js 15 | **Tests:** 500+ automated tests | **Legal:** CanG §19 compliant | **Live:** [cannamanage.plate-software.de](https://cannamanage.plate-software.de) --- -## 🚀 Current State +## 🚀 Sprint History -| Milestone | What's Done | Tests | -|-----------|-------------|-------| -| **Sprint 1** — Foundation | 8 JPA entities, ComplianceService (CanG §19 enforcement), Flyway V1 | 25 unit tests | -| **Sprint 2** — REST API | 5 controllers, JWT auth, Spring Security 7, OpenAPI, TenantFilterAspect | 12 integration tests | -| **Sprint 3** — Staff, Portal & Compliance | Staff permissions (JSONB), token revocation, invite flow, PDF/CSV reports, member portal (session auth), prevention officer, 30+ integration tests | 30+ integration tests | -| **Sprint 4** — Frontend MVP | Admin Dashboard + Member Portal (Shadboard, Next.js 15, React 19, 143 files, 23K lines). Login, KPI dashboard, member management (TanStack Table), multi-step distribution form with quota check, batch/stock management with charts, compliance reports. Member portal with radial quota visualization, distribution history, profile. i18n (de/en), dark+light mode, Playwright E2E, Docker multi-stage. | 6 E2E tests | -| **Sprint 5** — API Integration | React Query integration (mock fallback pattern), Docker Compose full-stack, Staff CRUD UI, system tests with SQL seed + API-driven flows, Vitest + MSW unit testing | 190+ tests (unit + integration + E2E + system) | -| **Sprint 6** — DSGVO & Payments | _Planned:_ DSGVO consent UI, Stripe payments, grow calendar, PWA/offline, Hetzner VPS deploy | — | +| Sprint | Theme | Key Deliverables | +|--------|-------|-----------------| +| **1** — Domain Foundation | Backend core | 8 JPA entities, ComplianceService (CanG §19), Flyway V1 | +| **2** — REST API | API layer | 5 controllers, JWT auth, Spring Security 7, OpenAPI | +| **3** — Staff & Portal | Auth & reports | Staff permissions (JSONB), token revocation, PDF/CSV reports, member portal | +| **4** — Frontend MVP | UI foundation | Next.js 15, React 19, admin dashboard + member portal, shadcn/ui, i18n (de/en) | +| **5** — API Integration | Full-stack wiring | React Query, Docker Compose full-stack, Staff CRUD, system tests | +| **6** — Production Readiness | Launch features | DSGVO consent, Stripe (SEPA/PayPal/Card), audit log, grow calendar, notifications, PWA | +| **7** — Communication | Community | Info Board, Club Events Calendar, Club-Internal Forum, Notification system | +| **8** — Vereinsverwaltung | Club governance | Club Treasury, General Assembly (votes), Document Archive, Board Management | +| **9** — Berichtszentrale | Reporting | Report Center with authority-ready exports, generated compliance reports | +| **10** — Smart Payment Import | Finance automation | Bank statement import (MT940/CAMT053/CSV), auto-matching for member payments | +| **11** — Test Coverage | Quality | JaCoCo 80% target, ~250 new tests, Testcontainers, coverage gates | +| **12** — Golden Test Standard | Polish | Documents page integration, UX improvements, integration test hardening | +| **13** — Production Hardening | Security & CI | Security fixes, CI/CD quality gates, repo cleanup, Gitea Actions | +| **14** — Marketing & Monetization | Growth | Landing page, login redesign, pricing page, storage quotas | + +--- ## 📋 Documentation | # | Document | Description | |---|----------|-------------| -| 01 | [Project Charter](CannaManage-01-Charter) | Vision, scope, legal framework, risk register, Gantt timeline | -| 02 | [User Stories](CannaManage-02-UserStories) | 25+ stories with MoSCoW priorities + acceptance criteria | -| 03 | [Architecture](CannaManage-03-Architecture) | System diagram, ERD, multi-tenancy design, dual SecurityFilterChain | +| 01 | [Project Charter](CannaManage-01-Charter) | Vision, scope, legal framework (CanG), risk register | +| 02 | [User Stories](CannaManage-02-UserStories) | 60+ stories with MoSCoW priorities + acceptance criteria | +| 03 | [Architecture](CannaManage-03-Architecture) | System diagram, ERD (57 entities), multi-tenancy, dual SecurityFilterChain | | 04 | [Flow Charts](CannaManage-04-Flowcharts) | Business logic flows: distribution, recall, compliance check | -| 05 | [API Spec](CannaManage-05-API) | REST API: 9 controllers, JWT + session auth, role-based access | +| 05 | [API Spec](CannaManage-05-API) | REST API: 33 controllers, JWT + session auth, role-based access | | 06 | [Wireframes & Mockups](CannaManage-06-Wireframes) | UI mockups for admin dashboard, distribution, quota views | | 07 | [Coding Standards](CannaManage-07-CodingStandards) | Java 21 standards, compliance patterns, Git strategy | -| 08 | [Test Plan](CannaManage-08-TestPlan) | Test strategy, 67+ automated tests, Testcontainers integration | -| 09 | [Deployment Guide](CannaManage-09-Deployment) | Hetzner VPS, Docker Compose, PostgreSQL, SMTP, Gitea CI/CD | -| 10 | [Retrospective](CannaManage-10-Retrospective) | Sprint retrospectives and decisions log | +| 08 | [Test Plan](CannaManage-08-TestPlan) | Test strategy, 500+ automated tests, JaCoCo 80% target | +| 09 | [Deployment Guide](CannaManage-09-Deployment) | TrueNAS Docker, Gitea Actions CI/CD, Nginx reverse proxy | +| 10 | [Retrospective](CannaManage-10-Retrospective) | Sprint retrospectives and decisions log (sprints 1–14) | +| 11 | [Features](CannaManage-11-Features) | Comprehensive feature catalog by navigation group | + +--- ## 🏗️ Tech Stack | Layer | Technology | |-------|-----------| -| Language | Java 21 (Temurin) | -| Framework | Spring Boot 4.0.6 | -| Security | Spring Security 7.0 + JWT (stateless) + Session (portal) · JJWT 0.12.6 | -| ORM | Hibernate 7 / JPA | -| Database | PostgreSQL 16 (prod) · Testcontainers (integration tests) | -| Migrations | Flyway 10 (V1–V5) | -| Multi-tenancy | Hibernate `@Filter` + `TenantFilterAspect` (AOP) | -| PDF Generation | OpenPDF (iText fork — LGPL, no license cost) | -| Caching | Caffeine (in-memory token revocation cache) | -| Email | Spring Mail (SMTP — staff invite flow) | -| Testing (Backend) | JUnit 5 + Mockito + Testcontainers (PostgreSQL 16) | -| Frontend | Next.js 15 + React 19 + TypeScript | -| UI Components | shadcn/ui (Radix primitives) + Tailwind CSS 4 | -| Data Fetching | @tanstack/react-query (stale-while-revalidate, mock fallback) | -| Charts | Recharts | -| Tables | TanStack Table v8 | -| Frontend Auth | NextAuth v5 (Auth.js) | -| Frontend i18n | next-intl | -| Frontend Testing | Vitest + MSW (unit) · Playwright (E2E + system) | -| Container Stack | Docker Compose (backend + frontend + PostgreSQL) | -| API Docs | SpringDoc OpenAPI 2.8.6 · Swagger UI | -| Build | Maven multi-module (backend) · pnpm (frontend) | -| Hosting | Hetzner VPS (German DC) · Docker Compose | +| **Language** | Java 21 (Temurin) | +| **Framework** | Spring Boot 4.0.6 | +| **Security** | Spring Security 7.0 + JWT (stateless) + Session (portal) · JJWT 0.12.6 | +| **ORM** | Hibernate 7 / JPA | +| **Database** | PostgreSQL 16 (prod) · Testcontainers (integration tests) | +| **Migrations** | Flyway 10 (V1–V36) | +| **Multi-tenancy** | Hibernate `@Filter` + `TenantFilterAspect` (AOP) | +| **PDF Generation** | OpenPDF (iText fork — LGPL, no license cost) | +| **Payments** | Stripe (SEPA, PayPal, Credit Card) | +| **Caching** | Caffeine (in-memory token revocation cache) | +| **Email** | Spring Mail (SMTP — notifications, invites) | +| **Testing (Backend)** | JUnit 5 + Mockito + Testcontainers + JaCoCo (80% gate) | +| **Frontend** | Next.js 15 + React 19 + TypeScript | +| **UI Components** | shadcn/ui (Radix primitives) + Tailwind CSS 4 | +| **Data Fetching** | @tanstack/react-query (stale-while-revalidate) | +| **Charts** | Recharts | +| **Tables** | TanStack Table v8 | +| **Frontend Auth** | NextAuth v5 (Auth.js) | +| **Frontend i18n** | next-intl (de/en) | +| **Frontend Testing** | Vitest + MSW (unit) · Playwright (E2E + system) | +| **Container Stack** | Docker Compose (backend + frontend + PostgreSQL + Nginx) | +| **API Docs** | SpringDoc OpenAPI 2.8.6 · Swagger UI | +| **Build** | Maven multi-module (backend) · pnpm (frontend) | +| **CI/CD** | Gitea Actions (PostgreSQL 16 service container) | +| **Hosting** | TrueNAS Docker → https://cannamanage.plate-software.de | + +--- ## 📦 Module Layout ``` cannamanage/ -├── cannamanage-domain/ → 11 JPA entities, enums, TenantContext -├── cannamanage-service/ → Business logic, repositories, ComplianceService, ReportService, TokenRevocationService -├── cannamanage-api/ → Spring Boot app, 9 controllers, dual security config, DTOs -├── cannamanage-frontend/ → Next.js 15 app, admin dashboard + member portal, 143 files +├── cannamanage-domain/ → 57 JPA entities, 30+ enums, TenantContext +├── cannamanage-service/ → 40+ services, repositories, business logic +├── cannamanage-api/ → Spring Boot app, 33 controllers, security config, DTOs, 36 Flyway migrations +├── cannamanage-frontend/ → Next.js 15 app, 18 dashboard sections + portal + marketing +├── deploy/ → Production Docker configs, Nginx, deploy scripts └── docs/ → Sprint plans, security reviews, design docs ``` +--- + ## 🔒 Security Model - **Dual SecurityFilterChain** — JWT chain for admin/staff API + session-based chain for member portal @@ -79,8 +99,11 @@ cannamanage/ - **Token revocation:** Caffeine in-memory cache with DB backing (`revoked_tokens` table), automatic cleanup scheduler - **Multi-tenant isolation:** Hibernate @Filter activated per-request via AOP - **Token rotation:** refresh tokens SHA-256 hashed, rotated on each use -- **Invite flow:** Admin creates invite → email sent via SMTP → staff sets password via 72-hour token -- **Frontend auth:** NextAuth v5 with CredentialsProvider → backend JWT (server-side only, no token exposure to client) +- **DSGVO compliance:** Full consent management, data export, right-to-erasure via schema drop +- **Audit logging:** Immutable audit trail for all compliance-relevant actions +- **Frontend auth:** NextAuth v5 with CredentialsProvider → backend JWT (server-side only) + +--- ## 📊 Quick Facts @@ -88,19 +111,38 @@ cannamanage/ |--------|-------| | Target Market | 500–3,000 German Anbauvereinigungen | | Legal Basis | Konsumcannabisgesetz (CanG) §§2, 15–26 | -| Revenue Model | B2B SaaS subscription | -| Entities | 11 (Member, Distribution, MonthlyQuota, Batch, Strain, StockMovement, Club, User, StaffAccount, RevokedToken, InviteToken) | -| API Endpoints | 25+ across 9 controllers | -| Total Files | 200+ (backend + frontend) | -| Test Coverage | 190+ automated tests (unit + integration + E2E + system) | -| Flyway Migrations | V1–V5 (initial schema → staff/portal → club settings → invite tokens) | -| Frontend Pages | 14 routes (10 admin + 4 portal) | -| Frontend Version | Next.js 15.5.18 | +| Revenue Model | B2B SaaS subscription (Stripe) | +| Entities | 57 (Members, Distributions, Stock, Grow, Finance, Assemblies, Documents, Forum, Events, Reports, Compliance, …) | +| API Endpoints | 100+ across 33 controllers | +| Flyway Migrations | V1–V36 | +| Frontend Sections | 18 dashboard + portal + marketing | +| Test Coverage | 500+ automated tests (unit + integration + E2E + system) | +| JaCoCo Target | 80% line coverage | +| Frontend Pages | 25+ routes (dashboard, portal, marketing) | | Security Scan | SAST + SCA clean (Snyk Code, SonarQube) | +| Deployment | TrueNAS Docker → cannamanage.plate-software.de | +| CI/CD | Gitea Actions with PostgreSQL service container | -## 🔗 Links +--- -- **Repository:** [git.plate-software.de/pplate/cannamanage](https://git.plate-software.de/pplate/cannamanage) -- **Swagger UI:** `http://localhost:8080/swagger-ui.html` (local dev) -- **Frontend Dev:** `http://localhost:3000` (Next.js dev server) -- **Branch:** `main` (current release) +## 🖥️ Frontend Navigation + +### Admin Dashboard (18 sections) + +| Group | Sections | +|-------|----------| +| **Betrieb** (Operations) | Dashboard, Mitglieder, Ausgabe, Lager, Anbau | +| **Kommunikation** (Communication) | Schwarzes Brett, Kalender, Forum | +| **Verwaltung** (Administration) | Finanzen (Übersicht + Import), Versammlungen, Dokumente, Vorstand, Personal | +| **Compliance** | Compliance-Status, Berichtszentrale, Protokoll, Berichte | + +### Member Portal +- Self-service dashboard with quota visualization +- Distribution history +- Profile management +- Event RSVP + +### Marketing (Public) +- Landing page with feature showcase +- Pricing page with tier comparison +- Login / registration diff --git a/Home.md b/Home.md index cc60f5c..594bae9 100644 --- a/Home.md +++ b/Home.md @@ -2,75 +2,95 @@ **Multi-tenant compliance platform for German Cannabis Social Clubs (Anbauvereinigungen)** -> **Status:** Sprint 5 ✅ Complete | **Stack:** Java 21 + Spring Boot 4.0.6 + Next.js 15.5.18 | **Tests:** 190+ automated tests | **Legal:** CanG §19 compliant +> **Status:** Sprint 14 ✅ Complete | **Stack:** Java 21 + Spring Boot 4.0.6 + Next.js 15 | **Tests:** 500+ automated tests | **Legal:** CanG §19 compliant | **Live:** [cannamanage.plate-software.de](https://cannamanage.plate-software.de) --- -## 🚀 Current State +## 🚀 Sprint History -| Milestone | What's Done | Tests | -|-----------|-------------|-------| -| **Sprint 1** — Foundation | 8 JPA entities, ComplianceService (CanG §19 enforcement), Flyway V1 | 25 unit tests | -| **Sprint 2** — REST API | 5 controllers, JWT auth, Spring Security 7, OpenAPI, TenantFilterAspect | 12 integration tests | -| **Sprint 3** — Staff, Portal & Compliance | Staff permissions (JSONB), token revocation, invite flow, PDF/CSV reports, member portal (session auth), prevention officer, 30+ integration tests | 30+ integration tests | -| **Sprint 4** — Frontend MVP | Admin Dashboard + Member Portal (Shadboard, Next.js 15, React 19, 143 files, 23K lines). Login, KPI dashboard, member management (TanStack Table), multi-step distribution form with quota check, batch/stock management with charts, compliance reports. Member portal with radial quota visualization, distribution history, profile. i18n (de/en), dark+light mode, Playwright E2E, Docker multi-stage. | 6 E2E tests | -| **Sprint 5** — API Integration | React Query integration (mock fallback pattern), Docker Compose full-stack, Staff CRUD UI, system tests with SQL seed + API-driven flows, Vitest + MSW unit testing | 190+ tests (unit + integration + E2E + system) | -| **Sprint 6** — DSGVO & Payments | _Planned:_ DSGVO consent UI, Stripe payments, grow calendar, PWA/offline, Hetzner VPS deploy | — | +| Sprint | Theme | Key Deliverables | +|--------|-------|-----------------| +| **1** — Domain Foundation | Backend core | 8 JPA entities, ComplianceService (CanG §19), Flyway V1 | +| **2** — REST API | API layer | 5 controllers, JWT auth, Spring Security 7, OpenAPI | +| **3** — Staff & Portal | Auth & reports | Staff permissions (JSONB), token revocation, PDF/CSV reports, member portal | +| **4** — Frontend MVP | UI foundation | Next.js 15, React 19, admin dashboard + member portal, shadcn/ui, i18n (de/en) | +| **5** — API Integration | Full-stack wiring | React Query, Docker Compose full-stack, Staff CRUD, system tests | +| **6** — Production Readiness | Launch features | DSGVO consent, Stripe (SEPA/PayPal/Card), audit log, grow calendar, notifications, PWA | +| **7** — Communication | Community | Info Board, Club Events Calendar, Club-Internal Forum, Notification system | +| **8** — Vereinsverwaltung | Club governance | Club Treasury, General Assembly (votes), Document Archive, Board Management | +| **9** — Berichtszentrale | Reporting | Report Center with authority-ready exports, generated compliance reports | +| **10** — Smart Payment Import | Finance automation | Bank statement import (MT940/CAMT053/CSV), auto-matching for member payments | +| **11** — Test Coverage | Quality | JaCoCo 80% target, ~250 new tests, Testcontainers, coverage gates | +| **12** — Golden Test Standard | Polish | Documents page integration, UX improvements, integration test hardening | +| **13** — Production Hardening | Security & CI | Security fixes, CI/CD quality gates, repo cleanup, Gitea Actions | +| **14** — Marketing & Monetization | Growth | Landing page, login redesign, pricing page, storage quotas | + +--- ## 📋 Documentation | # | Document | Description | |---|----------|-------------| -| 01 | [Project Charter](CannaManage-01-Charter) | Vision, scope, legal framework, risk register, Gantt timeline | -| 02 | [User Stories](CannaManage-02-UserStories) | 25+ stories with MoSCoW priorities + acceptance criteria | -| 03 | [Architecture](CannaManage-03-Architecture) | System diagram, ERD, multi-tenancy design, dual SecurityFilterChain | +| 01 | [Project Charter](CannaManage-01-Charter) | Vision, scope, legal framework (CanG), risk register | +| 02 | [User Stories](CannaManage-02-UserStories) | 60+ stories with MoSCoW priorities + acceptance criteria | +| 03 | [Architecture](CannaManage-03-Architecture) | System diagram, ERD (57 entities), multi-tenancy, dual SecurityFilterChain | | 04 | [Flow Charts](CannaManage-04-Flowcharts) | Business logic flows: distribution, recall, compliance check | -| 05 | [API Spec](CannaManage-05-API) | REST API: 9 controllers, JWT + session auth, role-based access | +| 05 | [API Spec](CannaManage-05-API) | REST API: 33 controllers, JWT + session auth, role-based access | | 06 | [Wireframes & Mockups](CannaManage-06-Wireframes) | UI mockups for admin dashboard, distribution, quota views | | 07 | [Coding Standards](CannaManage-07-CodingStandards) | Java 21 standards, compliance patterns, Git strategy | -| 08 | [Test Plan](CannaManage-08-TestPlan) | Test strategy, 67+ automated tests, Testcontainers integration | -| 09 | [Deployment Guide](CannaManage-09-Deployment) | Hetzner VPS, Docker Compose, PostgreSQL, SMTP, Gitea CI/CD | -| 10 | [Retrospective](CannaManage-10-Retrospective) | Sprint retrospectives and decisions log | +| 08 | [Test Plan](CannaManage-08-TestPlan) | Test strategy, 500+ automated tests, JaCoCo 80% target | +| 09 | [Deployment Guide](CannaManage-09-Deployment) | TrueNAS Docker, Gitea Actions CI/CD, Nginx reverse proxy | +| 10 | [Retrospective](CannaManage-10-Retrospective) | Sprint retrospectives and decisions log (sprints 1–14) | +| 11 | [Features](CannaManage-11-Features) | Comprehensive feature catalog by navigation group | + +--- ## 🏗️ Tech Stack | Layer | Technology | |-------|-----------| -| Language | Java 21 (Temurin) | -| Framework | Spring Boot 4.0.6 | -| Security | Spring Security 7.0 + JWT (stateless) + Session (portal) · JJWT 0.12.6 | -| ORM | Hibernate 7 / JPA | -| Database | PostgreSQL 16 (prod) · Testcontainers (integration tests) | -| Migrations | Flyway 10 (V1–V5) | -| Multi-tenancy | Hibernate `@Filter` + `TenantFilterAspect` (AOP) | -| PDF Generation | OpenPDF (iText fork — LGPL, no license cost) | -| Caching | Caffeine (in-memory token revocation cache) | -| Email | Spring Mail (SMTP — staff invite flow) | -| Testing (Backend) | JUnit 5 + Mockito + Testcontainers (PostgreSQL 16) | -| Frontend | Next.js 15 + React 19 + TypeScript | -| UI Components | shadcn/ui (Radix primitives) + Tailwind CSS 4 | -| Data Fetching | @tanstack/react-query (stale-while-revalidate, mock fallback) | -| Charts | Recharts | -| Tables | TanStack Table v8 | -| Frontend Auth | NextAuth v5 (Auth.js) | -| Frontend i18n | next-intl | -| Frontend Testing | Vitest + MSW (unit) · Playwright (E2E + system) | -| Container Stack | Docker Compose (backend + frontend + PostgreSQL) | -| API Docs | SpringDoc OpenAPI 2.8.6 · Swagger UI | -| Build | Maven multi-module (backend) · pnpm (frontend) | -| Hosting | Hetzner VPS (German DC) · Docker Compose | +| **Language** | Java 21 (Temurin) | +| **Framework** | Spring Boot 4.0.6 | +| **Security** | Spring Security 7.0 + JWT (stateless) + Session (portal) · JJWT 0.12.6 | +| **ORM** | Hibernate 7 / JPA | +| **Database** | PostgreSQL 16 (prod) · Testcontainers (integration tests) | +| **Migrations** | Flyway 10 (V1–V36) | +| **Multi-tenancy** | Hibernate `@Filter` + `TenantFilterAspect` (AOP) | +| **PDF Generation** | OpenPDF (iText fork — LGPL, no license cost) | +| **Payments** | Stripe (SEPA, PayPal, Credit Card) | +| **Caching** | Caffeine (in-memory token revocation cache) | +| **Email** | Spring Mail (SMTP — notifications, invites) | +| **Testing (Backend)** | JUnit 5 + Mockito + Testcontainers + JaCoCo (80% gate) | +| **Frontend** | Next.js 15 + React 19 + TypeScript | +| **UI Components** | shadcn/ui (Radix primitives) + Tailwind CSS 4 | +| **Data Fetching** | @tanstack/react-query (stale-while-revalidate) | +| **Charts** | Recharts | +| **Tables** | TanStack Table v8 | +| **Frontend Auth** | NextAuth v5 (Auth.js) | +| **Frontend i18n** | next-intl (de/en) | +| **Frontend Testing** | Vitest + MSW (unit) · Playwright (E2E + system) | +| **Container Stack** | Docker Compose (backend + frontend + PostgreSQL + Nginx) | +| **API Docs** | SpringDoc OpenAPI 2.8.6 · Swagger UI | +| **Build** | Maven multi-module (backend) · pnpm (frontend) | +| **CI/CD** | Gitea Actions (PostgreSQL 16 service container) | +| **Hosting** | TrueNAS Docker → https://cannamanage.plate-software.de | + +--- ## 📦 Module Layout ``` cannamanage/ -├── cannamanage-domain/ → 11 JPA entities, enums, TenantContext -├── cannamanage-service/ → Business logic, repositories, ComplianceService, ReportService, TokenRevocationService -├── cannamanage-api/ → Spring Boot app, 9 controllers, dual security config, DTOs -├── cannamanage-frontend/ → Next.js 15 app, admin dashboard + member portal, 143 files +├── cannamanage-domain/ → 57 JPA entities, 30+ enums, TenantContext +├── cannamanage-service/ → 40+ services, repositories, business logic +├── cannamanage-api/ → Spring Boot app, 33 controllers, security config, DTOs, 36 Flyway migrations +├── cannamanage-frontend/ → Next.js 15 app, 18 dashboard sections + portal + marketing +├── deploy/ → Production Docker configs, Nginx, deploy scripts └── docs/ → Sprint plans, security reviews, design docs ``` +--- + ## 🔒 Security Model - **Dual SecurityFilterChain** — JWT chain for admin/staff API + session-based chain for member portal @@ -79,8 +99,11 @@ cannamanage/ - **Token revocation:** Caffeine in-memory cache with DB backing (`revoked_tokens` table), automatic cleanup scheduler - **Multi-tenant isolation:** Hibernate @Filter activated per-request via AOP - **Token rotation:** refresh tokens SHA-256 hashed, rotated on each use -- **Invite flow:** Admin creates invite → email sent via SMTP → staff sets password via 72-hour token -- **Frontend auth:** NextAuth v5 with CredentialsProvider → backend JWT (server-side only, no token exposure to client) +- **DSGVO compliance:** Full consent management, data export, right-to-erasure via schema drop +- **Audit logging:** Immutable audit trail for all compliance-relevant actions +- **Frontend auth:** NextAuth v5 with CredentialsProvider → backend JWT (server-side only) + +--- ## 📊 Quick Facts @@ -88,19 +111,38 @@ cannamanage/ |--------|-------| | Target Market | 500–3,000 German Anbauvereinigungen | | Legal Basis | Konsumcannabisgesetz (CanG) §§2, 15–26 | -| Revenue Model | B2B SaaS subscription | -| Entities | 11 (Member, Distribution, MonthlyQuota, Batch, Strain, StockMovement, Club, User, StaffAccount, RevokedToken, InviteToken) | -| API Endpoints | 25+ across 9 controllers | -| Total Files | 200+ (backend + frontend) | -| Test Coverage | 190+ automated tests (unit + integration + E2E + system) | -| Flyway Migrations | V1–V5 (initial schema → staff/portal → club settings → invite tokens) | -| Frontend Pages | 14 routes (10 admin + 4 portal) | -| Frontend Version | Next.js 15.5.18 | +| Revenue Model | B2B SaaS subscription (Stripe) | +| Entities | 57 (Members, Distributions, Stock, Grow, Finance, Assemblies, Documents, Forum, Events, Reports, Compliance, …) | +| API Endpoints | 100+ across 33 controllers | +| Flyway Migrations | V1–V36 | +| Frontend Sections | 18 dashboard + portal + marketing | +| Test Coverage | 500+ automated tests (unit + integration + E2E + system) | +| JaCoCo Target | 80% line coverage | +| Frontend Pages | 25+ routes (dashboard, portal, marketing) | | Security Scan | SAST + SCA clean (Snyk Code, SonarQube) | +| Deployment | TrueNAS Docker → cannamanage.plate-software.de | +| CI/CD | Gitea Actions with PostgreSQL service container | -## 🔗 Links +--- -- **Repository:** [git.plate-software.de/pplate/cannamanage](https://git.plate-software.de/pplate/cannamanage) -- **Swagger UI:** `http://localhost:8080/swagger-ui.html` (local dev) -- **Frontend Dev:** `http://localhost:3000` (Next.js dev server) -- **Branch:** `main` (current release) +## 🖥️ Frontend Navigation + +### Admin Dashboard (18 sections) + +| Group | Sections | +|-------|----------| +| **Betrieb** (Operations) | Dashboard, Mitglieder, Ausgabe, Lager, Anbau | +| **Kommunikation** (Communication) | Schwarzes Brett, Kalender, Forum | +| **Verwaltung** (Administration) | Finanzen (Übersicht + Import), Versammlungen, Dokumente, Vorstand, Personal | +| **Compliance** | Compliance-Status, Berichtszentrale, Protokoll, Berichte | + +### Member Portal +- Self-service dashboard with quota visualization +- Distribution history +- Profile management +- Event RSVP + +### Marketing (Public) +- Landing page with feature showcase +- Pricing page with tier comparison +- Login / registration diff --git a/_Sidebar.md b/_Sidebar.md index 9f7b751..08abf35 100644 --- a/_Sidebar.md +++ b/_Sidebar.md @@ -20,15 +20,38 @@ - [Test Plan](CannaManage-08-TestPlan) - [Deployment Guide](CannaManage-09-Deployment) +**🌟 Product** +- [Features](CannaManage-11-Features) + --- **📊 Sprint Status** -| Sprint | Status | -|--------|--------| -| 1 — Domain | ✅ | -| 2 — API | ✅ | -| 3 — Staff & Portal | ✅ | -| 4 — Frontend MVP | ✅ | -| 5 — API Integration | ✅ | -| 6 — DSGVO & Payments | 📋 | +| Sprint | Theme | Status | +|--------|-------|--------| +| 1 | Domain Foundation | ✅ | +| 2 | REST API | ✅ | +| 3 | Staff & Portal | ✅ | +| 4 | Frontend MVP | ✅ | +| 5 | API Integration | ✅ | +| 6 | Production Readiness | ✅ | +| 7 | Communication | ✅ | +| 8 | Vereinsverwaltung | ✅ | +| 9 | Berichtszentrale | ✅ | +| 10 | Payment Import | ✅ | +| 11 | Test Coverage | ✅ | +| 12 | Golden Tests | ✅ | +| 13 | Prod Hardening | ✅ | +| 14 | Marketing | ✅ | + +--- + +**📈 Metrics** + +| Metric | Value | +|--------|-------| +| Entities | 57 | +| Controllers | 33 | +| Migrations | V1–V36 | +| Tests | 500+ | +| Coverage | 80% |