diff --git a/docs/sprint-6/cannamanage-sprint6-plan.md b/docs/sprint-6/cannamanage-sprint6-plan.md new file mode 100644 index 0000000..6491a01 --- /dev/null +++ b/docs/sprint-6/cannamanage-sprint6-plan.md @@ -0,0 +1,1265 @@ +# CannaManage — Sprint 6 Implementation Plan + +**Date:** 2026-06-12 +**Author:** Patrick Plate / Lumen (Planner) +**Status:** Draft v2 +**Base Branch:** `main` +**Sprint Branch:** `sprint/6-production` +**Sprint Goal:** Production readiness — deploy, DSGVO compliance, Stripe payments, immutable audit log, grow calendar, notifications, launch + +> **Sprint Structure:** Sprint 6 is the "go-live" sprint, split into 7 phases: +> - **Phase 1** — Production deployment (IONOS VPS + Docker Compose + TLS) +> - **Phase 2** — DSGVO consent management +> - **Phase 3** — Stripe integration (SEPA + PayPal + Card) +> - **Phase 4** — Immutable audit log +> - **Phase 5** — Grow calendar (FULL — sensors, photo log, feeding schedule) +> - **Phase 6** — Notifications + PWA +> - **Phase 7** — Launch checklist + documentation +> +> Estimated effort: ~23–24 days (single worker, sequential). Phases are prioritized — Phases 1–4 are hard requirements for launch, Phases 5–7 are value-adds. + +--- + +## 0. Decisions (Confirmed ✅) + +| # | Decision | Detail | Status | +|---|----------|--------|--------| +| D1 | Payment provider | **Stripe** (fresh account for CannaManage) — full-featured API for SEPA, PayPal, Card. Handles mandates, invoicing, dunning (Mahnwesen), billing portal. Cost: €0.35/SEPA tx. | ✅ Confirmed | +| D2 | Primary payment method | **SEPA Lastschrift** (direct debit) — standard for German SaaS recurring billing. PayPal + Card as fallback via Stripe Payment Element. | ✅ Confirmed | +| D3 | Deploy target | **IONOS VPS** (existing plate-software.de server, 8 GB RAM, ~2 cores) — deploy alongside existing services. Docker Compose, Nginx reverse proxy, Let's Encrypt TLS. Already running, no new provisioning needed. | ✅ Confirmed | +| D4 | Domain | **Subdomain on plate-software.de** (e.g., `cannamanage.plate-software.de` or `app.plate-software.de/cannamanage`) — IONOS DNS, no new domain registration needed | ✅ Confirmed | +| D5 | Backup strategy | **PostgreSQL pg_dump** — 7 days daily + 4 weeks weekly retention, stored on IONOS server | ✅ Confirmed | +| D6 | CI/CD | **Gitea Actions** (self-hosted on plate-software.de) → build Docker images → deploy locally | ✅ Confirmed | +| D7 | Monitoring | **Uptime Kuma** (self-hosted) or simple cron + curl health check with Telegram/email alerts | ✅ Confirmed | +| D8 | DSGVO approach | **Consent-first** — users must accept before using the app. Full Art. 15 (data export) + Art. 17 (erasure) compliance. | ✅ Confirmed | +| D9 | Audit log | **Immutable append-only** table with server-generated timestamps (Europe/Berlin). No UPDATE/DELETE on audit records. | ✅ Confirmed | +| D10 | Grow calendar | **FullCalendar** (React) — FULL scope: sensors (temp/humidity), photo log, feeding schedule, stage tracking with batch linking. | ✅ Confirmed | +| D11 | Notifications | **WebSocket (SockJS/STOMP)** for real-time + **Web Push API** for background push (opt-in) | ✅ Confirmed | +| D12 | Pricing model | **3-month free trial** → Tiered: Starter (≤30 members, €19/mo), Pro (≤100 members, €49/mo), Enterprise (custom) | ✅ Confirmed | +| D13 | PWA scope | **Basic in Sprint 6** — manifest + offline page + install prompt. Full offline sync deferred. | ✅ Confirmed | + +--- + +## 1. Sprint 5 Recap (Context) + +| Delivered | Status | +|-----------|--------| +| Full-stack integration (React Query ↔ Spring Boot API) | ✅ | +| Docker Compose (PostgreSQL + backend + frontend, health checks) | ✅ | +| Next.js 15.5.18 upgrade (8+ CVEs resolved) | ✅ | +| Staff management UI (list, invite, edit permissions, revoke) | ✅ | +| System test harness (Docker Compose test profile, Playwright E2E) | ✅ | +| Per-component loading, offline resilience, error boundary | ✅ | +| CORS configuration, seed data strategy (SQL + API-driven) | ✅ | + +**Critical gap from Sprint 5:** App runs locally with Docker Compose but has no production deployment, no payment system, no DSGVO compliance, and no audit trail. These are hard blockers for going live. + +--- + +## 2. Sprint 6 Scope + +### ✅ IN Scope — Hard Requirements (Phases 1–4) + +| # | Feature | Priority | Effort | +|---|---------|----------|--------| +| 1 | **Production deployment** — IONOS VPS (existing plate-software.de), Nginx subdomain, TLS, Docker Compose prod, backups, Gitea Actions CI/CD, monitoring | P0 | 3 days | +| 2 | **DSGVO consent management** — consent entity, banner, audit log, data export (Art. 15), data deletion (Art. 17) | P0 | 3 days | +| 3 | **Stripe integration** — fresh account, SEPA + PayPal + Card, 3-month free trial, tiered subscriptions (Starter €19/Pro €49/Enterprise), billing portal, webhooks | P0 | 4 days | +| 4 | **Immutable audit log** — event entity, AuditService, admin UI, PDF export | P0 | 2 days | + +**Phases 1–4 effort:** ~12 days + +### ✅ IN Scope — Value Adds (Phases 5–7) + +| # | Feature | Priority | Effort | +|---|---------|----------|--------| +| 5 | **Grow calendar (FULL)** — entity, CRUD API, FullCalendar UI, batch linking, sensor integration (temp/humidity), photo uploads, feeding schedules | P1 | 5–6 days | +| 6 | **Notifications + PWA** — WebSocket, bell icon, push notifications, service worker, manifest | P1 | 3 days | +| 7 | **Launch checklist + documentation** — security scan, perf test, ToS/Datenschutz pages, pricing, landing page | P2 | 3 days | + +**Phases 5–7 effort:** ~11–12 days + +**Total estimated effort:** ~23–24 days (single worker, sequential) + +### ❌ OUT of Scope (Sprint 7+) + +- 2FA (TOTP) for members and staff +- Inspector read-only mode (Behörde officials) +- Monthly report auto-sealing (SHA-256 hash) +- Cryptographic hash chain on distributions +- Mobile app (React Native or Flutter) +- Multi-club marketplace +- Advanced analytics / BI dashboard + +--- + +## 3. Architecture Decisions + +### 3.1 Production Infrastructure (Phase 1) + +``` +┌──────────────────────────────────────────────────────┐ +│ IONOS VPS (existing plate-software.de) │ +│ ~2 cores, 8 GB RAM — co-hosted with other services │ +│ │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ Nginx (reverse proxy) │ │ +│ │ - TLS termination (Let's Encrypt / certbot) │ │ +│ │ - HTTP/2, HSTS, security headers │ │ +│ │ - WebSocket upgrade (ws://) │ │ +│ │ - Rate limiting (limit_req_zone) │ │ +│ └───────────┬──────────────────┬──────────────────┘ │ +│ │ │ │ +│ ┌───────────▼──────┐ ┌───────▼──────────────────┐ │ +│ │ Frontend :3000 │ │ Backend :8080 │ │ +│ │ (Next.js SSR) │ │ (Spring Boot + JPA) │ │ +│ └──────────────────┘ └──────────┬───────────────┘ │ +│ │ │ +│ ┌──────────▼───────────────┐ │ +│ │ PostgreSQL 16 :5432 │ │ +│ │ (pgdata volume) │ │ +│ └──────────────────────────┘ │ +│ │ +│ ┌────────────────┐ ┌───────────────────────────┐ │ +│ │ Uptime Kuma │ │ pg_dump cron (daily) │ │ +│ │ :3001 │ │ → /backup/ (7-day rotate)│ │ +│ └────────────────┘ └───────────────────────────┘ │ +└──────────────────────────────────────────────────────┘ +``` + +**Nginx configuration (key directives):** + +```nginx +# /etc/nginx/sites-available/cannamanage.plate-software.de +server { + listen 443 ssl http2; + server_name cannamanage.plate-software.de; + + 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=63072000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff always; + add_header X-Frame-Options DENY always; + add_header Referrer-Policy strict-origin-when-cross-origin always; + + # Rate limiting + limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s; + + # Frontend (Next.js) + location / { + proxy_pass http://127.0.0.1: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; + } + + # Backend API + location /api/ { + limit_req zone=api burst=50 nodelay; + proxy_pass http://127.0.0.1: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; + } + + # WebSocket + location /ws/ { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + } + + # Stripe webhook (no rate limit — Stripe needs reliable delivery) + location /api/v1/webhooks/stripe { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header Stripe-Signature $http_stripe_signature; + } +} + +server { + listen 80; + server_name cannamanage.plate-software.de; + return 301 https://$host$request_uri; +} +``` + +**Docker Compose production profile:** + +```yaml +# docker-compose.prod.yml +version: '3.9' + +services: + db: + image: postgres:16-alpine + container_name: cannamanage-db + restart: always + environment: + POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - pgdata:/var/lib/postgresql/data + - ./backup:/backup + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"] + interval: 10s + timeout: 5s + retries: 5 + + backend: + image: ghcr.io/pplate/cannamanage-backend:${TAG:-latest} + container_name: cannamanage-backend + restart: always + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/${DB_NAME} + - SPRING_DATASOURCE_USERNAME=${DB_USER} + - SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD} + - SPRING_PROFILES_ACTIVE=prod + - JWT_SECRET=${JWT_SECRET} + - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY} + - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} + - STRIPE_PRICE_STARTER=${STRIPE_PRICE_STARTER} + - STRIPE_PRICE_PRO=${STRIPE_PRICE_PRO} + depends_on: + db: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 45s + + frontend: + image: ghcr.io/pplate/cannamanage-frontend:${TAG:-latest} + container_name: cannamanage-frontend + restart: always + environment: + - BACKEND_URL=http://backend:8080 + - NEXTAUTH_URL=https://cannamanage.plate-software.de + - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} + - AUTH_TRUST_HOST=true + - NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY} + depends_on: + backend: + condition: service_healthy + +volumes: + pgdata: +``` + +**Backup cron script:** + +```bash +#!/bin/bash +# /opt/cannamanage/backup.sh — runs daily via cron +BACKUP_DIR="/opt/cannamanage/backup" +DAILY_RETENTION=7 +WEEKLY_RETENTION=28 +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +DAY_OF_WEEK=$(date +%u) + +docker exec cannamanage-db pg_dump -U ${DB_USER} -d ${DB_NAME} \ + | gzip > "${BACKUP_DIR}/cannamanage_daily_${TIMESTAMP}.sql.gz" + +# Weekly backup on Sundays +if [ "$DAY_OF_WEEK" -eq 7 ]; then + cp "${BACKUP_DIR}/cannamanage_daily_${TIMESTAMP}.sql.gz" \ + "${BACKUP_DIR}/cannamanage_weekly_${TIMESTAMP}.sql.gz" +fi + +# Rotate: 7 days daily + 4 weeks weekly +find "${BACKUP_DIR}" -name "*_daily_*.sql.gz" -mtime +${DAILY_RETENTION} -delete +find "${BACKUP_DIR}" -name "*_weekly_*.sql.gz" -mtime +${WEEKLY_RETENTION} -delete +``` + +**Gitea Actions CI/CD (self-hosted on plate-software.de):** + +```yaml +# .gitea/workflows/deploy.yml +name: Deploy to Production +on: + push: + branches: [main] + paths-ignore: ['docs/**', '*.md'] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Build backend + run: mvn -f cannamanage-api/pom.xml package -DskipTests -B + + - name: Build Docker images + run: | + docker build -f Dockerfile.backend -t cannamanage-backend:${{ gitea.sha }} . + docker build -f cannamanage-frontend/Dockerfile -t cannamanage-frontend:${{ gitea.sha }} ./cannamanage-frontend + + - name: Deploy locally + run: | + cd /opt/cannamanage + export TAG=${{ gitea.sha }} + docker compose -f docker-compose.prod.yml up -d --remove-orphans + docker image prune -f +``` + +### 3.2 DSGVO Consent Architecture (Phase 2) + +``` +┌─────────────────────────────────────────────────────────┐ +│ DSGVO Consent Flow │ +│ │ +│ User Login ─→ Check consent_accepted? ─→ YES ─→ App │ +│ │ │ +│ NO │ +│ │ │ +│ ▼ │ +│ Consent Banner (modal) │ +│ - Datenschutzerklärung link │ +│ - AGB link │ +│ - "Akzeptieren" button │ +│ - "Ablehnen" → logout │ +│ │ │ +│ ACCEPT │ +│ │ │ +│ ▼ │ +│ ConsentController.accept() │ +│ → stores Consent entity │ +│ → audit log entry │ +│ → sets consent cookie │ +│ → redirects to app │ +└─────────────────────────────────────────────────────────┘ +``` + +**Entity:** + +```java +@Entity +@Table(name = "consent") +@Data @Builder @NoArgsConstructor @AllArgsConstructor +public class Consent extends AbstractTenantEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "consent_type", nullable = false) + @Enumerated(EnumType.STRING) + private ConsentType consentType; // PRIVACY_POLICY, TERMS_OF_SERVICE, DATA_PROCESSING + + @Column(name = "version", nullable = false) + private String version; // "2026-06-v1" — tracks which version was accepted + + @Column(name = "accepted_at", nullable = false) + private Instant acceptedAt; + + @Column(name = "revoked_at") + private Instant revokedAt; + + @Column(name = "ip_address") + private String ipAddress; + + @Column(name = "user_agent") + private String userAgent; +} +``` + +**Flyway migration (`V6__consent_and_audit.sql`):** + +```sql +-- Consent table +CREATE TABLE consent ( + id BIGSERIAL PRIMARY KEY, + club_id BIGINT NOT NULL REFERENCES club(id), + user_id BIGINT NOT NULL REFERENCES users(id), + consent_type VARCHAR(50) NOT NULL, + version VARCHAR(50) NOT NULL, + accepted_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + revoked_at TIMESTAMP WITH TIME ZONE, + ip_address VARCHAR(45), + user_agent VARCHAR(500), + CONSTRAINT uq_consent_user_type UNIQUE (user_id, consent_type, version) +); + +CREATE INDEX idx_consent_user ON consent(user_id); +CREATE INDEX idx_consent_club ON consent(club_id); +``` + +**Data export endpoint (Art. 15):** + +```java +@GetMapping("/api/v1/dsgvo/export") +@PreAuthorize("isAuthenticated()") +public ResponseEntity exportMyData(@AuthenticationPrincipal UserDetails user) { + // Collects: user profile, all distributions, quota history, consent records, login history + byte[] zipFile = dsgvoService.exportUserData(user.getUsername()); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=meine-daten.zip") + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(zipFile); +} +``` + +**Data deletion endpoint (Art. 17):** + +```java +@DeleteMapping("/api/v1/dsgvo/erasure") +@PreAuthorize("isAuthenticated()") +public ResponseEntity requestErasure(@AuthenticationPrincipal UserDetails user) { + // Soft-delete user data, anonymize distributions (keep aggregates for compliance), + // revoke all consents, invalidate sessions + // NOTE: Some data must be retained for legal compliance (tax records, 10-year retention) + dsgvoService.processErasureRequest(user.getUsername()); + return ResponseEntity.noContent().build(); +} +``` + +### 3.3 Stripe Payment Architecture (Phase 3) + +``` +┌───────────────────────────────────────────────────────────────┐ +│ Stripe Integration │ +│ │ +│ Club Onboarding ─→ Stripe Customer creation │ +│ │ │ +│ ▼ │ +│ Billing Page ─→ Stripe Checkout Session │ +│ (/settings/billing) (Payment Element: SEPA/PayPal/Card) │ +│ │ │ +│ ▼ │ +│ Subscription active │ +│ │ │ +│ ├─→ invoice.paid → extend access │ +│ ├─→ invoice.payment_failed → warn user │ +│ └─→ customer.subscription.deleted → lock │ +│ │ +│ Self-service ─→ Stripe Billing Portal │ +│ (update payment method, cancel, download invoices) │ +└───────────────────────────────────────────────────────────────┘ +``` + +**Pricing tiers:** + +| Tier | Price | Features | Stripe Price ID | +|------|-------|----------|-----------------| +| **Free Trial** | €0 (14 days) | Full features, 1 club, max 7 members | — (trial period on Starter) | +| **Starter** | €29/month | 1 club, up to 50 members, email support | `price_starter_monthly` | +| **Pro** | €79/month | 1 club, unlimited members, priority support, API access | `price_pro_monthly` | + +**Backend — StripeService:** + +```java +@Service +@Slf4j +public class StripeService { + + @Value("${stripe.secret-key}") + private String stripeSecretKey; + + @Value("${stripe.webhook-secret}") + private String webhookSecret; + + @PostConstruct + void init() { + Stripe.apiKey = stripeSecretKey; + } + + public Customer createCustomer(Club club, User owner) { + CustomerCreateParams params = CustomerCreateParams.builder() + .setEmail(owner.getEmail()) + .setName(club.getName()) + .putMetadata("club_id", club.getId().toString()) + .putMetadata("user_id", owner.getId().toString()) + .build(); + return Customer.create(params); + } + + public Session createCheckoutSession(Club club, String priceId, String successUrl, String cancelUrl) { + SessionCreateParams params = SessionCreateParams.builder() + .setCustomer(club.getStripeCustomerId()) + .setMode(SessionCreateParams.Mode.SUBSCRIPTION) + .addPaymentMethodType(SessionCreateParams.PaymentMethodType.SEPA_DEBIT) + .addPaymentMethodType(SessionCreateParams.PaymentMethodType.CARD) + .addPaymentMethodType(SessionCreateParams.PaymentMethodType.PAYPAL) + .addLineItem(SessionCreateParams.LineItem.builder() + .setPrice(priceId) + .setQuantity(1L) + .build()) + .setSuccessUrl(successUrl + "?session_id={CHECKOUT_SESSION_ID}") + .setCancelUrl(cancelUrl) + .setSubscriptionData(SessionCreateParams.SubscriptionData.builder() + .setTrialPeriodDays(14L) + .build()) + .build(); + return Session.create(params); + } + + public Session createBillingPortalSession(Club club, String returnUrl) { + com.stripe.param.billingportal.SessionCreateParams params = + com.stripe.param.billingportal.SessionCreateParams.builder() + .setCustomer(club.getStripeCustomerId()) + .setReturnUrl(returnUrl) + .build(); + return com.stripe.model.billingportal.Session.create(params); + } + + public void handleWebhook(String payload, String sigHeader) { + Event event = Webhook.constructEvent(payload, sigHeader, webhookSecret); + + switch (event.getType()) { + case "invoice.paid" -> handleInvoicePaid(event); + case "invoice.payment_failed" -> handlePaymentFailed(event); + case "customer.subscription.deleted" -> handleSubscriptionCanceled(event); + case "customer.subscription.updated" -> handleSubscriptionUpdated(event); + } + } +} +``` + +**Webhook controller:** + +```java +@RestController +@RequestMapping("/api/v1/webhooks") +public class StripeWebhookController { + + @PostMapping("/stripe") + public ResponseEntity handleStripeWebhook( + @RequestBody String payload, + @RequestHeader("Stripe-Signature") String sigHeader) { + stripeService.handleWebhook(payload, sigHeader); + return ResponseEntity.ok("OK"); + } +} +``` + +**Frontend — billing page (`/settings/billing`):** + +```typescript +// src/app/[locale]/(admin)/settings/billing/page.tsx +export default function BillingPage() { + const { data: subscription } = useSubscription() + const createCheckout = useCreateCheckoutSession() + const openPortal = useOpenBillingPortal() + + return ( +
+

Abonnement & Zahlung

+ + {/* Current plan */} + + Aktueller Plan + + {subscription?.plan ?? "Free Trial"} +

Nächste Zahlung: {subscription?.nextPaymentDate}

+

Zahlungsart: {subscription?.paymentMethod}

+
+ + + +
+ + {/* Upgrade options (if on trial/starter) */} + {!subscription?.plan && createCheckout.mutate(priceId)} />} + + {/* Invoice history */} + +
+ ) +} +``` + +**Database changes (Club entity):** + +```java +// Add to Club entity +@Column(name = "stripe_customer_id") +private String stripeCustomerId; + +@Column(name = "subscription_status") +@Enumerated(EnumType.STRING) +private SubscriptionStatus subscriptionStatus; // TRIALING, ACTIVE, PAST_DUE, CANCELED, NONE + +@Column(name = "subscription_plan") +private String subscriptionPlan; // "starter", "pro" + +@Column(name = "trial_ends_at") +private Instant trialEndsAt; +``` + +### 3.4 Immutable Audit Log Architecture (Phase 4) + +```java +@Entity +@Table(name = "audit_event") +@Data @Builder @NoArgsConstructor @AllArgsConstructor +@Immutable // Hibernate: prevents UPDATE/DELETE +public class AuditEvent { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "club_id", nullable = false) + private Long clubId; + + @Column(name = "actor_id") + private Long actorId; // null for system events + + @Column(name = "actor_name", nullable = false) + private String actorName; // "Patrick Plate" or "SYSTEM" + + @Column(name = "event_type", nullable = false) + @Enumerated(EnumType.STRING) + private AuditEventType eventType; + + @Column(name = "entity_type") + private String entityType; // "Distribution", "Member", "Batch", etc. + + @Column(name = "entity_id") + private Long entityId; + + @Column(name = "description", nullable = false) + private String description; // Human-readable: "25g Northern Lights an Max Mustermann ausgegeben" + + @Column(name = "details", columnDefinition = "JSONB") + private String details; // JSON with before/after values + + @Column(name = "ip_address") + private String ipAddress; + + @Column(name = "created_at", nullable = false, updatable = false) + private Instant createdAt; // Server-generated, Europe/Berlin + + @PrePersist + void prePersist() { + this.createdAt = Instant.now(); + } +} +``` + +**AuditEventType enum:** + +```java +public enum AuditEventType { + // Distributions + DISTRIBUTION_CREATED, + DISTRIBUTION_VOIDED, + + // Members + MEMBER_CREATED, + MEMBER_UPDATED, + MEMBER_DEACTIVATED, + MEMBER_DELETED, + + // Batches + BATCH_CREATED, + BATCH_RECALLED, + BATCH_DESTROYED, + + // Stock + STOCK_ADJUSTED, + + // Auth + USER_LOGIN, + USER_LOGOUT, + USER_LOGIN_FAILED, + + // Staff + STAFF_INVITED, + STAFF_PERMISSIONS_CHANGED, + STAFF_REVOKED, + + // DSGVO + CONSENT_ACCEPTED, + CONSENT_REVOKED, + DATA_EXPORT_REQUESTED, + DATA_ERASURE_REQUESTED, + + // Billing + SUBSCRIPTION_CREATED, + SUBSCRIPTION_CANCELED, + PAYMENT_RECEIVED, + PAYMENT_FAILED, + + // Grow + GROW_ENTRY_CREATED, + GROW_ENTRY_UPDATED, + HARVEST_LINKED_TO_BATCH +} +``` + +**AuditService (aspect-style):** + +```java +@Service +@Slf4j +public class AuditService { + + private final AuditEventRepository repository; + + public void log(AuditEventType type, Long clubId, Long actorId, String actorName, + String entityType, Long entityId, String description, Object details) { + AuditEvent event = AuditEvent.builder() + .clubId(clubId) + .actorId(actorId) + .actorName(actorName) + .eventType(type) + .entityType(entityType) + .entityId(entityId) + .description(description) + .details(details != null ? objectMapper.writeValueAsString(details) : null) + .build(); + repository.save(event); + log.debug("Audit: {} by {} — {}", type, actorName, description); + } +} +``` + +### 3.5 Grow Calendar Architecture (Phase 5) + +**Entity:** + +```java +@Entity +@Table(name = "grow_entry") +@Data @Builder @NoArgsConstructor @AllArgsConstructor +public class GrowEntry extends AbstractTenantEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "strain_id", nullable = false) + private Strain strain; + + @Column(name = "stage", nullable = false) + @Enumerated(EnumType.STRING) + private GrowStage stage; // SEEDLING, VEGETATIVE, FLOWERING, HARVEST, DRYING, CURING + + @Column(name = "start_date", nullable = false) + private LocalDate startDate; + + @Column(name = "end_date") + private LocalDate endDate; + + @Column(name = "plant_count") + private Integer plantCount; + + @Column(name = "notes") + private String notes; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "batch_id") + private Batch linkedBatch; // null until harvest → batch linking + + @Column(name = "expected_yield_grams") + private BigDecimal expectedYieldGrams; + + @Column(name = "actual_yield_grams") + private BigDecimal actualYieldGrams; +} +``` + +**GrowStage enum with colors:** + +```java +public enum GrowStage { + SEEDLING("#4ade80"), // green-400 + VEGETATIVE("#22c55e"), // green-500 + FLOWERING("#a855f7"), // purple-500 + HARVEST("#f59e0b"), // amber-500 + DRYING("#78716c"), // stone-500 + CURING("#d97706"); // amber-600 + + private final String color; + GrowStage(String color) { this.color = color; } + public String getColor() { return color; } +} +``` + +**FullCalendar integration:** + +```typescript +// src/app/[locale]/(admin)/grow/page.tsx +import FullCalendar from "@fullcalendar/react" +import dayGridPlugin from "@fullcalendar/daygrid" +import interactionPlugin from "@fullcalendar/interaction" + +export default function GrowPage() { + const { data: entries } = useGrowEntries() + + const events = entries?.map(entry => ({ + id: entry.id.toString(), + title: `${entry.strain.name} — ${entry.stage}`, + start: entry.startDate, + end: entry.endDate, + backgroundColor: STAGE_COLORS[entry.stage], + extendedProps: { entry } + })) + + return ( + + ) +} +``` + +### 3.6 WebSocket + Notifications Architecture (Phase 6) + +**Backend (Spring WebSocket + STOMP):** + +```java +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + config.enableSimpleBroker("/topic", "/queue"); + config.setApplicationDestinationPrefixes("/app"); + config.setUserDestinationPrefix("/user"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws") + .setAllowedOriginPatterns("*") + .withSockJS(); + } +} +``` + +**Notification events:** + +```java +@Service +public class NotificationService { + + private final SimpMessagingTemplate messagingTemplate; + + public void notifyQuotaWarning(Long userId, int percentUsed) { + messagingTemplate.convertAndSendToUser( + userId.toString(), + "/queue/notifications", + new NotificationEvent("QUOTA_WARNING", + String.format("Kontingent zu %d%% ausgeschöpft", percentUsed)) + ); + } + + public void notifyBatchRecall(Long clubId, String batchName) { + messagingTemplate.convertAndSend( + "/topic/club/" + clubId + "/notifications", + new NotificationEvent("BATCH_RECALL", + String.format("Charge '%s' wurde zurückgerufen!", batchName)) + ); + } + + public void notifyNewDistribution(Long memberId, String strainName, BigDecimal amount) { + messagingTemplate.convertAndSendToUser( + memberId.toString(), + "/queue/notifications", + new NotificationEvent("DISTRIBUTION", + String.format("%.1fg %s wurde für dich ausgegeben", amount, strainName)) + ); + } +} +``` + +**PWA manifest (`public/manifest.json`):** + +```json +{ + "name": "CannaManage", + "short_name": "CannaManage", + "description": "Cannabis-Anbauvereinigung Management", + "start_url": "/", + "display": "standalone", + "background_color": "#0a0a0a", + "theme_color": "#16a34a", + "icons": [ + { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" } + ] +} +``` + +--- + +## 4. Phase Details + +### Phase 1: Production Deployment (Hetzner VPS) + +**Effort:** 3 days + +| Step | Task | Files | +|------|------|-------| +| 1.1 | Provision Hetzner VPS (CX31), install Docker + Docker Compose + Nginx + Certbot | Server setup (manual) | +| 1.2 | Register domain, configure DNS A/AAAA records | DNS config (manual) | +| 1.3 | Create `docker-compose.prod.yml` with env vars, volumes, restart policies | `docker-compose.prod.yml` | +| 1.4 | Create `.env.prod.example` with all required env vars documented | `.env.prod.example` | +| 1.5 | Configure Nginx reverse proxy (TLS, HTTP/2, WebSocket, security headers, rate limiting) | Server: `/etc/nginx/sites-available/cannamanage.de` | +| 1.6 | Set up Let's Encrypt auto-renewal (certbot timer) | Server cron | +| 1.7 | Create backup script + cron job (daily pg_dump, 7-day rotation) | `scripts/backup.sh` | +| 1.8 | Set up GitHub Actions CI/CD (build → push GHCR → SSH deploy) | `.github/workflows/deploy.yml` | +| 1.9 | Install Uptime Kuma for health monitoring + alerting | Docker container on VPS | +| 1.10 | Add Spring Actuator production config (metrics, health, no info exposure) | `application-prod.properties` | +| 1.11 | Create `application-prod.properties` with production-safe defaults | `cannamanage-api/src/main/resources/application-prod.properties` | + +**Acceptance Criteria:** + +- [ ] `https://cannamanage.de` serves the frontend over HTTPS (A+ SSL Labs rating) +- [ ] Backend API reachable at `https://cannamanage.de/api/v1/actuator/health` +- [ ] `docker compose -f docker-compose.prod.yml up -d` starts all services cleanly +- [ ] Push to `main` triggers automatic deployment via GitHub Actions +- [ ] Daily backup cron creates compressed pg_dump, old backups rotated +- [ ] Uptime Kuma shows green status, alerts on downtime within 60s +- [ ] Security headers present (HSTS, X-Frame-Options, CSP basic) +- [ ] Rate limiting active (>30 req/s returns 429) + +--- + +### Phase 2: DSGVO Consent Management + +**Effort:** 3 days + +| Step | Task | Files | +|------|------|-------| +| 2.1 | Create Flyway migration `V6__consent_and_audit.sql` (consent table + audit_event table) | `db/migration/V6__consent_and_audit.sql` | +| 2.2 | Create `Consent` entity + `ConsentType` enum + `ConsentRepository` | `cannamanage-domain/` | +| 2.3 | Create `ConsentService` (accept, revoke, check, list) | `cannamanage-service/` | +| 2.4 | Create `ConsentController` REST endpoints | `cannamanage-api/` | +| 2.5 | Create `DsgvoService` (data export ZIP, data erasure with anonymization) | `cannamanage-service/` | +| 2.6 | Create `DsgvoController` (GET /dsgvo/export, DELETE /dsgvo/erasure) | `cannamanage-api/` | +| 2.7 | Frontend: Consent modal/banner (shown on first login if not accepted) | `cannamanage-frontend/` | +| 2.8 | Frontend: /settings/privacy page (revoke consent, download data, request deletion) | `cannamanage-frontend/` | +| 2.9 | Create `Datenschutzerklärung` (Privacy Policy) static page | `cannamanage-frontend/` | +| 2.10 | Create `AGB` (Terms of Service) static page | `cannamanage-frontend/` | +| 2.11 | Integration test: consent flow (accept → access granted, revoke → access blocked) | Tests | + +**Acceptance Criteria:** + +- [ ] First login shows consent modal — cannot proceed without accepting +- [ ] "Ablehnen" logs out the user immediately +- [ ] Consent entity stores IP address, user agent, timestamp, version +- [ ] Consent audit log entry created on accept/revoke +- [ ] GET `/api/v1/dsgvo/export` returns ZIP with all user data (JSON format) +- [ ] DELETE `/api/v1/dsgvo/erasure` anonymizes user data, retains aggregates for legal compliance +- [ ] Privacy policy and ToS pages accessible without authentication +- [ ] Revoking consent blocks further app access until re-accepted +- [ ] Consent version tracked — new policy version re-prompts acceptance + +--- + +### Phase 3: Stripe Integration (SEPA + PayPal + Card) + +**Effort:** 4 days + +| Step | Task | Files | +|------|------|-------| +| 3.1 | Add Stripe Java SDK dependency to `pom.xml` | `cannamanage-service/pom.xml` | +| 3.2 | Create `StripeService` (customer creation, checkout session, billing portal, webhook handling) | `cannamanage-service/` | +| 3.3 | Create `SubscriptionController` (create checkout, get status, open portal) | `cannamanage-api/` | +| 3.4 | Create `StripeWebhookController` (signature verification + event dispatch) | `cannamanage-api/` | +| 3.5 | Flyway migration: add Stripe columns to `club` table | `db/migration/V7__stripe_columns.sql` | +| 3.6 | Create Stripe Customer on club creation (onboarding hook) | `ClubService.java` modification | +| 3.7 | Implement subscription status enforcement (middleware: block API if subscription expired) | `SubscriptionFilter.java` | +| 3.8 | Frontend: `/settings/billing` page (plan display, payment method, invoices) | `cannamanage-frontend/` | +| 3.9 | Frontend: pricing cards component (Starter vs Pro comparison) | `cannamanage-frontend/` | +| 3.10 | Frontend: checkout redirect + success/cancel handling | `cannamanage-frontend/` | +| 3.11 | Configure Stripe webhook endpoint in Stripe Dashboard (test + live modes) | Manual (Stripe Dashboard) | +| 3.12 | Test mode E2E: create subscription, trigger webhook, verify access | Integration test | + +**Acceptance Criteria:** + +- [ ] Club owner can subscribe via SEPA Lastschrift (direct debit) +- [ ] Club owner can subscribe via PayPal or Card as alternatives +- [ ] 14-day free trial starts automatically for new clubs +- [ ] After trial expiry without payment → club access locked with "Please subscribe" message +- [ ] `invoice.paid` webhook extends access, stores payment in audit log +- [ ] `invoice.payment_failed` webhook shows warning banner to club owner +- [ ] `customer.subscription.deleted` webhook locks club access +- [ ] Billing portal link opens Stripe-hosted management (update card, cancel, invoices) +- [ ] `/settings/billing` shows current plan, next payment date, payment method +- [ ] Webhook endpoint validates Stripe signature (rejects spoofed events) +- [ ] All payment events logged in audit log + +--- + +### Phase 4: Immutable Audit Log + +**Effort:** 2 days + +| Step | Task | Files | +|------|------|-------| +| 4.1 | Create `AuditEvent` entity (with `@Immutable`) + `AuditEventType` enum | `cannamanage-domain/` | +| 4.2 | Create `AuditEventRepository` (read-only, no delete methods) | `cannamanage-service/` | +| 4.3 | Create `AuditService` (log method, used by all other services) | `cannamanage-service/` | +| 4.4 | Integrate AuditService into existing services (Distribution, Member, Batch, Stock, Auth, Staff) | Multiple service files | +| 4.5 | Create `AuditController` (GET paginated, filter by type/date/actor) | `cannamanage-api/` | +| 4.6 | Create audit PDF export endpoint (for Behörde inspections) | `cannamanage-api/` | +| 4.7 | Frontend: `/admin/audit-log` page (read-only table, filterable, searchable) | `cannamanage-frontend/` | +| 4.8 | Frontend: PDF export button on audit log page | `cannamanage-frontend/` | +| 4.9 | Ensure timestamps are server-generated (Europe/Berlin) and audit records cannot be modified | Entity + DB constraints | +| 4.10 | Unit tests for AuditService, integration test for immutability | Tests | + +**Acceptance Criteria:** + +- [ ] Every distribution, member change, batch action, login, and staff change creates an audit entry +- [ ] Audit entries are immutable (no UPDATE/DELETE — enforced by Hibernate `@Immutable` + DB trigger) +- [ ] `/admin/audit-log` page shows chronological event list with filters (type, date range, actor) +- [ ] PDF export generates a printable report for Behörde inspection +- [ ] Timestamps are server-generated in Europe/Berlin timezone (not client-submitted) +- [ ] Audit log includes DSGVO events (consent accept/revoke, data export, erasure) +- [ ] Audit log includes payment events (subscription created, payment received/failed) +- [ ] Search works across description field (full-text) +- [ ] Pagination works (default 50 per page) + +--- + +### Phase 5: Grow Calendar (FULL) + +**Effort:** 5–6 days + +| Step | Task | Files | +|------|------|-------| +| 5.1 | Flyway migration: `V8__grow_calendar.sql` (grow_entry + sensor_reading + photo_log + feeding_schedule tables) | `db/migration/V8__grow_calendar.sql` | +| 5.2 | Create `GrowEntry` entity + `GrowStage` enum | `cannamanage-domain/` | +| 5.3 | Create `SensorReading` entity (temp, humidity, timestamp, grow_entry_id) | `cannamanage-domain/` | +| 5.4 | Create `GrowPhoto` entity (url, caption, timestamp, grow_entry_id) | `cannamanage-domain/` | +| 5.5 | Create `FeedingSchedule` entity (nutrient, amount, frequency, grow_entry_id) | `cannamanage-domain/` | +| 5.6 | Create repositories for all grow entities | `cannamanage-service/` | +| 5.7 | Create `GrowCalendarService` (CRUD, stage transitions, batch linking) | `cannamanage-service/` | +| 5.8 | Create `SensorService` (record readings, aggregation, alerts on threshold breach) | `cannamanage-service/` | +| 5.9 | Create `GrowCalendarController` (GET/POST/PUT/DELETE + sensor + photo + feeding endpoints) | `cannamanage-api/` | +| 5.10 | Frontend: `/grow` page with FullCalendar (month view, color-coded stages) | `cannamanage-frontend/` | +| 5.11 | Frontend: create/edit grow entry dialog (strain, stage, dates, plant count, notes) | `cannamanage-frontend/` | +| 5.12 | Frontend: harvest → batch linking dialog (connect grow entry to existing batch) | `cannamanage-frontend/` | +| 5.13 | Frontend: grow entry detail panel (show full lifecycle of a grow) | `cannamanage-frontend/` | +| 5.14 | Frontend: sensor dashboard (temp/humidity charts, last 7d/30d, threshold alerts) | `cannamanage-frontend/` | +| 5.15 | Frontend: photo log gallery (upload, timeline view, captions) | `cannamanage-frontend/` | +| 5.16 | Frontend: feeding schedule editor (add nutrients, amounts, frequency, calendar overlay) | `cannamanage-frontend/` | +| 5.17 | Unit + integration tests for grow services | Tests | + +**Acceptance Criteria:** + +- [ ] FullCalendar renders grow entries as color-coded blocks (one color per stage) +- [ ] Click date → create new grow entry (select strain, stage, plant count) +- [ ] Drag to resize → adjusts end date +- [ ] Click entry → opens detail panel with edit/delete options +- [ ] Stage progression: Seedling → Vegetative → Flowering → Harvest → Drying → Curing +- [ ] Harvest stage allows linking to an existing batch (traceability: grow → batch → distribution) +- [ ] Calendar shows de locale (German month names, Monday start) +- [ ] Entries filtered by club (multi-tenant) +- [ ] Audit log entry for create/update/link operations +- [ ] **Sensors:** Record temp/humidity readings (manual entry or API endpoint for IoT devices) +- [ ] **Sensors:** Chart visualization (line chart, last 7d/30d toggle) +- [ ] **Sensors:** Configurable threshold alerts (e.g., temp > 30°C → notification) +- [ ] **Photos:** Upload photos per grow entry (max 10MB, JPEG/PNG) +- [ ] **Photos:** Gallery view with timeline and captions +- [ ] **Feeding:** Define feeding schedule (nutrient name, amount in ml/g, frequency) +- [ ] **Feeding:** Calendar overlay showing feeding days + +--- + +### Phase 6: Notifications + PWA + +**Effort:** 3 days + +| Step | Task | Files | +|------|------|-------| +| 6.1 | Add Spring WebSocket + STOMP dependencies | `cannamanage-api/pom.xml` | +| 6.2 | Create `WebSocketConfig` (STOMP broker, SockJS fallback) | `cannamanage-api/` | +| 6.3 | Create `NotificationService` (quota warning, batch recall, distribution, payment) | `cannamanage-service/` | +| 6.4 | Integrate notifications into existing services (trigger on events) | Multiple services | +| 6.5 | Create `NotificationController` (GET unread count, mark as read) | `cannamanage-api/` | +| 6.6 | Frontend: WebSocket connection hook (SockJS client, auto-reconnect) | `cannamanage-frontend/` | +| 6.7 | Frontend: notification bell in header (unread badge count) | `cannamanage-frontend/` | +| 6.8 | Frontend: notification dropdown/panel (list of recent notifications) | `cannamanage-frontend/` | +| 6.9 | PWA: create `manifest.json` + icons (192px, 512px) | `cannamanage-frontend/public/` | +| 6.10 | PWA: create service worker (offline page, asset caching) | `cannamanage-frontend/public/sw.js` | +| 6.11 | PWA: install prompt (iOS/Android home-screen) | `cannamanage-frontend/` | +| 6.12 | Web Push: VAPID key generation, push subscription endpoint, opt-in toggle | Backend + Frontend | + +**Acceptance Criteria:** + +- [ ] Real-time notification when quota reaches 80% (appears in bell without page refresh) +- [ ] Real-time notification on batch recall (all affected members notified) +- [ ] Real-time notification on new distribution (member portal bell) +- [ ] Notification bell shows unread count badge +- [ ] Click bell → dropdown with notification list (mark as read) +- [ ] PWA installable on mobile (home screen icon, standalone mode) +- [ ] Offline page shown when network unavailable (instead of browser error) +- [ ] Web Push notifications (opt-in) delivered even when app is closed +- [ ] WebSocket reconnects automatically after network drop +- [ ] Notification events logged in audit log + +--- + +### Phase 7: Launch Checklist + Documentation + +**Effort:** 3 days + +| Step | Task | Files | +|------|------|-------| +| 7.1 | Security scan (Snyk Code + SCA + SonarQube) — fix any Critical/High findings | Scan results | +| 7.2 | Performance test (k6 or artillery) — target: 100 concurrent users, <500ms p95 | `scripts/load-test.js` | +| 7.3 | Update wiki with Sprint 6 documentation | Wiki | +| 7.4 | Create landing page (`/`) with product description, features, screenshots | `cannamanage-frontend/` | +| 7.5 | Create pricing page (`/pricing`) with tier comparison + CTA buttons | `cannamanage-frontend/` | +| 7.6 | Create `Datenschutzerklärung` legal page (DSGVO compliant) | `cannamanage-frontend/` | +| 7.7 | Create `Impressum` legal page (required by German law) | `cannamanage-frontend/` | +| 7.8 | Create `AGB` (Terms of Service) legal page | `cannamanage-frontend/` | +| 7.9 | Create `robots.txt` + `sitemap.xml` for SEO | `cannamanage-frontend/public/` | +| 7.10 | Final smoke test on production (full user journey: register → subscribe → distribute → audit) | Manual | +| 7.11 | Set up error tracking (Sentry or similar — optional, can use Uptime Kuma logs) | Config | + +**Acceptance Criteria:** + +- [ ] Zero Critical/High security findings (Snyk + SonarQube clean) +- [ ] p95 latency < 500ms under 100 concurrent users +- [ ] Landing page renders with product description, screenshots, and CTA +- [ ] Pricing page shows Free Trial / Starter / Pro with feature comparison +- [ ] All legal pages present: Datenschutzerklärung, Impressum, AGB +- [ ] Full user journey works on production: signup → consent → subscribe → add member → distribute → audit log +- [ ] Wiki updated with Sprint 6 architecture and deployment docs +- [ ] `robots.txt` + `sitemap.xml` present for search engine indexing +- [ ] HTTPS everywhere (no mixed content) + +--- + +## 5. Risk Assessment + +| # | Risk | Probability | Impact | Mitigation | +|---|------|-------------|--------|------------| +| R1 | Stripe integration complexity (SEPA mandates, webhook reliability) | Medium | High | Start with test mode, use Stripe CLI for local webhook testing, extensive logging | +| R2 | DSGVO compliance gaps (incomplete data export, retention conflicts) | Medium | High | Legal review of Datenschutzerklärung before launch, document what data is retained and why | +| R3 | Production deployment issues (SSL, DNS propagation, Docker networking) | Low | Medium | Test with staging subdomain first (`staging.cannamanage.de`), blue-green deploy | +| R4 | WebSocket scalability on single VPS | Low | Low | Hetzner CX31 handles 1000+ concurrent WS connections easily for MVP; scale later if needed | +| R5 | Stripe webhook delivery failures (lost events) | Low | High | Implement idempotent webhook handler + periodic reconciliation job (check subscription status every 6h) | +| R6 | Backup corruption / restore failure | Low | Critical | Test restore procedure monthly, verify backup integrity with checksum | +| R7 | FullCalendar bundle size (large JS library) | Low | Low | Dynamic import, lazy-load only on `/grow` route | +| R8 | German legal requirements (Impressum, Datenschutz) | Medium | High | Consult template generators (e.g., e-recht24.de), have Patrick review before launch | + +--- + +## 6. Database Migrations Summary + +| Migration | Phase | Description | +|-----------|-------|-------------| +| `V6__consent_and_audit.sql` | 2 + 4 | `consent` table + `audit_event` table | +| `V7__stripe_columns.sql` | 3 | Add `stripe_customer_id`, `subscription_status`, `subscription_plan`, `trial_ends_at` to `club` | +| `V8__grow_calendar.sql` | 5 | `grow_entry` table with strain/stage/dates/batch FK | +| `V9__notifications.sql` | 6 | `notification` table (persisted notifications for history) | + +--- + +## 7. New Dependencies + +### Backend (Maven) + +| Dependency | Version | Purpose | Phase | +|------------|---------|---------|-------| +| `com.stripe:stripe-java` | 26.x | Stripe API SDK | 3 | +| `org.springframework.boot:spring-boot-starter-websocket` | (managed) | WebSocket + STOMP | 6 | + +### Frontend (npm) + +| Dependency | Version | Purpose | Phase | +|------------|---------|---------|-------| +| `@stripe/stripe-js` | ^4.x | Stripe.js for Payment Element | 3 | +| `@fullcalendar/react` | ^6.x | Calendar component | 5 | +| `@fullcalendar/daygrid` | ^6.x | Month/week grid view | 5 | +| `@fullcalendar/interaction` | ^6.x | Drag/drop, click events | 5 | +| `@stomp/stompjs` | ^7.x | STOMP over WebSocket | 6 | +| `sockjs-client` | ^1.x | SockJS fallback transport | 6 | +| `next-pwa` | ^5.x | PWA plugin for Next.js (or `@ducanh2912/next-pwa`) | 6 | +| `web-push` | ^3.x | VAPID push (if server-side push needed) | 6 | + +--- + +## 8. Open Questions (for Patrick's return) + +| # | Question | Answer | Status | +|---|----------|--------|--------| +| Q1 | Domain? | Subdomain on existing `plate-software.de` (e.g., `cannamanage.plate-software.de`) — IONOS DNS | ✅ Resolved | +| Q2 | Server? | IONOS existing VPS (8 GB RAM, ~2 cores) — already running, deploy alongside existing services | ✅ Resolved | +| Q3 | Stripe account? | Fresh Stripe account for CannaManage (test mode first, then live) | ✅ Resolved | +| Q4 | Pricing model? | 3-month free trial → Tiered: Starter (≤30 members, €19/mo), Pro (≤100 members, €49/mo), Enterprise (custom) | ✅ Resolved | +| Q5 | CI/CD? | Gitea Actions (self-hosted on plate-software.de) | ✅ Resolved | +| Q6 | Backup strategy? | 7 days daily + 4 weeks weekly retention | ✅ Resolved | +| Q7 | Grow calendar scope? | FULL — sensors (temp/humidity), photo log, feeding schedule, not just date tracking | ✅ Resolved | +| Q8 | PWA scope? | Basic in Sprint 6 — manifest + offline page + install prompt | ✅ Resolved | + +--- + +## 9. Sprint 5 Backlog Items Addressed + +| Sprint 5 Backlog Item | Sprint 6 Phase | Status | +|-----------------------|----------------|--------| +| Notification System (email + in-app + push) | Phase 6 | ✅ Planned | +| PWA Manifest + Service Worker | Phase 6 | ✅ Planned | +| Club Settings UI | — | ❌ Deferred (Sprint 7) | +| Inspector Read-Only Mode | — | ❌ Deferred (Sprint 7) | +| Monthly Report Auto-Sealing | — | ❌ Deferred (Sprint 7) | +| Cryptographic Hash Chain | — | ❌ Deferred (Sprint 7) | +| 2FA (TOTP) | — | ❌ Deferred (Sprint 7) | +| Micro-interactions (Framer Motion) | — | ❌ Deferred (Sprint 7) | + +--- + +## 10. Success Criteria (Sprint 6 Complete) + +The sprint is complete when: + +1. **Production live** — `cannamanage.plate-software.de` serves the full app over HTTPS with automated Gitea Actions CI/CD +2. **DSGVO compliant** — consent management, data export, data deletion all functional +3. **Payments working** — clubs can subscribe via SEPA/PayPal/Card, billing portal accessible +4. **Audit trail** — every significant action logged immutably, exportable as PDF +5. **Grow tracking** — full grow calendar with stage management, batch linking, sensor data, photo log, and feeding schedules +6. **Real-time notifications** — WebSocket bell icon + PWA installable +7. **Launch-ready** — legal pages present, security scanned, performance tested + +--- + +## References + +- Sprint 5 Plan: `docs/sprint-5/cannamanage-sprint5-plan.md` +- Sprint 5 Backlog: `docs/sprint-5/cannamanage-sprint5-backlog.md` +- Sprint 4 Plan: `docs/sprint-4/cannamanage-sprint4-plan.md` +- Stripe SEPA Docs: https://stripe.com/docs/payments/sepa-debit +- Stripe Billing Portal: https://stripe.com/docs/billing/subscriptions/integrating-customer-portal +- FullCalendar React: https://fullcalendar.io/docs/react +- Spring WebSocket: https://docs.spring.io/spring-framework/reference/web/websocket.html +- Next.js PWA: https://github.com/nicolo-ribaudo/next-pwa