Files
cannamanage/docs/sprint-6/cannamanage-sprint6-plan.md

1282 lines
54 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: ~2324 days (single worker, sequential). Phases are prioritized — Phases 14 are hard requirements for launch, Phases 57 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 14)
| # | 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 14 effort:** ~12 days
### ✅ IN Scope — Value Adds (Phases 57)
| # | Feature | Priority | Effort |
|---|---------|----------|--------|
| 5 | **Grow calendar (FULL)** — entity, CRUD API, FullCalendar UI, batch linking, sensor integration (temp/humidity), photo uploads, feeding schedules | P1 | 56 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 57 effort:** ~1112 days
**Total estimated effort:** ~2324 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:** 56 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