# CannaManage — Sprint 6 Implementation Plan

**Date:** 2026-06-12
**Author:** Patrick Plate / Lumen (Planner)
**Status:** Draft v3 (post-review)
**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;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' js.stripe.com; frame-src js.stripe.com hooks.stripe.com; img-src 'self' data: *.stripe.com;" 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)

# GPG-encrypted backup (DSGVO-compliant)
docker exec cannamanage-db pg_dump -U ${DB_USER} -d ${DB_NAME} \
  | gpg --encrypt --recipient cannamanage-backup \
  > "${BACKUP_DIR}/cannamanage_daily_${TIMESTAMP}.sql.gpg"

# 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<byte[]> 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<Void> 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 (3 months) | Full features, 1 club, ≤30 members | — (trial period on Starter) |
| **Starter** | €19/month | 1 club, ≤30 members, email support | `price_starter_monthly` |
| **Pro** | €49/month | 1 club, ≤100 members, priority support, API access | `price_pro_monthly` |
| **Enterprise** | Custom | Multiple clubs, unlimited members, dedicated support, SLA | Contact sales |

**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(90L) // 3-month free trial
                .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<String> 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 (
    <div className="space-y-6">
      <h1>Abonnement & Zahlung</h1>

      {/* Current plan */}
      <Card>
        <CardHeader><CardTitle>Aktueller Plan</CardTitle></CardHeader>
        <CardContent>
          <Badge>{subscription?.plan ?? "Free Trial"}</Badge>
          <p>Nächste Zahlung: {subscription?.nextPaymentDate}</p>
          <p>Zahlungsart: {subscription?.paymentMethod}</p>
        </CardContent>
        <CardFooter>
          <Button onClick={() => openPortal.mutate()}>
            Zahlungsmethode verwalten
          </Button>
        </CardFooter>
      </Card>

      {/* Upgrade options (if on trial/starter) */}
      {!subscription?.plan && <PricingCards onSelect={(priceId) => createCheckout.mutate(priceId)} />}

      {/* Invoice history */}
      <InvoiceHistoryTable />
    </div>
  )
}
```

**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 (
    <FullCalendar
      plugins={[dayGridPlugin, interactionPlugin]}
      initialView="dayGridMonth"
      events={events}
      editable={true}
      selectable={true}
      dateClick={handleDateClick}
      eventClick={handleEventClick}
      locale="de"
    />
  )
}
```

### 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 (IONOS VPS)

**Effort:** 3 days

| Step | Task | Files |
|------|------|-------|
| 1.1 | Configure IONOS VPS (existing plate-software.de), 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
- [ ] 3-month 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

**Voraussetzung:** Auftragsverarbeitungsvertrag (AVV) mit Stripe abschließen (DSGVO Art. 28). Stripe bietet standardisiertes DPA unter stripe.com/de/legal/dpa.

---

### 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)

**Database-level protection:**

```sql
REVOKE DELETE ON audit_events FROM cannamanage_app;
```

Only the DBA role can delete audit records — the application user has INSERT + SELECT only.

**Aufbewahrungsfrist:** 10 Jahre (KCanG-konform). Automatische Löschung nach 10 Jahren via Scheduled Task.

---

### 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 | IONOS VPS 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
