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