From 26a77dd269718ac3dd0c20aa2cb165a17441c8c7 Mon Sep 17 00:00:00 2001 From: Patrick Plate Date: Mon, 15 Jun 2026 12:01:06 +0200 Subject: [PATCH] =?UTF-8?q?feat(sprint9):=20Phase=201=20=E2=80=94=20Data?= =?UTF-8?q?=20model=20+=20ReportGenerator=20infrastructure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 7 new enums: ReportType, ExportFormat, DestructionMethod, TransportStatus, ComplianceArea, ComplianceStatus, RetentionCategory - Extended: StaffPermission (+3), AuditEventType (+5), NotificationType (+2) - Flyway V23-V29: destruction_records, transport_records, propagation_sources, prevention_activities, generated_reports, compliance_deadlines, distribution THC/CBD - 6 new JPA entities extending AbstractTenantEntity - 6 new Spring Data repositories with tenant-scoped queries - ReportGenerator interface + ReportGeneratorService (auto-discovery, format dispatch) - ComplianceRecordsController (CRUD for destruction/transport/propagation/prevention) - ComplianceDeadlineController (create, list, complete, overdue) - DateRangeReportParameters record for report generation --- .../ComplianceDeadlineController.java | 97 +++ .../ComplianceRecordsController.java | 190 +++++ .../db/migration/V23__destruction_records.sql | 18 + .../db/migration/V24__transport_records.sql | 19 + .../db/migration/V25__propagation_sources.sql | 17 + .../migration/V26__prevention_activities.sql | 15 + .../db/migration/V27__generated_reports.sql | 18 + .../migration/V28__compliance_deadlines.sql | 18 + .../migration/V29__distribution_thc_cbd.sql | 4 + .../domain/entity/ComplianceDeadline.java | 74 ++ .../domain/entity/DestructionRecord.java | 74 ++ .../domain/entity/GeneratedReport.java | 82 ++ .../domain/entity/PreventionActivity.java | 53 ++ .../domain/entity/PropagationSource.java | 65 ++ .../domain/entity/TransportRecord.java | 80 ++ .../domain/enums/AuditEventType.java | 9 +- .../domain/enums/ComplianceArea.java | 12 + .../domain/enums/ComplianceStatus.java | 10 + .../domain/enums/DestructionMethod.java | 11 + .../domain/enums/ExportFormat.java | 11 + .../domain/enums/NotificationType.java | 5 +- .../cannamanage/domain/enums/ReportType.java | 34 + .../domain/enums/RetentionCategory.java | 14 + .../domain/enums/StaffPermission.java | 6 +- .../domain/enums/TransportStatus.java | 11 + .../service/ReportGeneratorService.java | 128 +++ .../report/DateRangeReportParameters.java | 21 + .../service/report/ReportGenerator.java | 29 + .../service/report/ReportParameters.java | 8 + .../ComplianceDeadlineRepository.java | 27 + .../DestructionRecordRepository.java | 16 + .../repository/GeneratedReportRepository.java | 19 + .../PreventionActivityRepository.java | 17 + .../PropagationSourceRepository.java | 16 + .../repository/TransportRecordRepository.java | 16 + docs/sprint-9/cannamanage-sprint9-analysis.md | 759 +++++++++++++++++ .../cannamanage-sprint9-plan-review.md | 235 ++++++ docs/sprint-9/cannamanage-sprint9-plan.md | 785 ++++++++++++++++++ docs/sprint-9/cannamanage-sprint9-testplan.md | 723 ++++++++++++++++ 39 files changed, 3743 insertions(+), 3 deletions(-) create mode 100644 cannamanage-api/src/main/java/de/cannamanage/api/controller/ComplianceDeadlineController.java create mode 100644 cannamanage-api/src/main/java/de/cannamanage/api/controller/ComplianceRecordsController.java create mode 100644 cannamanage-api/src/main/resources/db/migration/V23__destruction_records.sql create mode 100644 cannamanage-api/src/main/resources/db/migration/V24__transport_records.sql create mode 100644 cannamanage-api/src/main/resources/db/migration/V25__propagation_sources.sql create mode 100644 cannamanage-api/src/main/resources/db/migration/V26__prevention_activities.sql create mode 100644 cannamanage-api/src/main/resources/db/migration/V27__generated_reports.sql create mode 100644 cannamanage-api/src/main/resources/db/migration/V28__compliance_deadlines.sql create mode 100644 cannamanage-api/src/main/resources/db/migration/V29__distribution_thc_cbd.sql create mode 100644 cannamanage-domain/src/main/java/de/cannamanage/domain/entity/ComplianceDeadline.java create mode 100644 cannamanage-domain/src/main/java/de/cannamanage/domain/entity/DestructionRecord.java create mode 100644 cannamanage-domain/src/main/java/de/cannamanage/domain/entity/GeneratedReport.java create mode 100644 cannamanage-domain/src/main/java/de/cannamanage/domain/entity/PreventionActivity.java create mode 100644 cannamanage-domain/src/main/java/de/cannamanage/domain/entity/PropagationSource.java create mode 100644 cannamanage-domain/src/main/java/de/cannamanage/domain/entity/TransportRecord.java create mode 100644 cannamanage-domain/src/main/java/de/cannamanage/domain/enums/ComplianceArea.java create mode 100644 cannamanage-domain/src/main/java/de/cannamanage/domain/enums/ComplianceStatus.java create mode 100644 cannamanage-domain/src/main/java/de/cannamanage/domain/enums/DestructionMethod.java create mode 100644 cannamanage-domain/src/main/java/de/cannamanage/domain/enums/ExportFormat.java create mode 100644 cannamanage-domain/src/main/java/de/cannamanage/domain/enums/ReportType.java create mode 100644 cannamanage-domain/src/main/java/de/cannamanage/domain/enums/RetentionCategory.java create mode 100644 cannamanage-domain/src/main/java/de/cannamanage/domain/enums/TransportStatus.java create mode 100644 cannamanage-service/src/main/java/de/cannamanage/service/ReportGeneratorService.java create mode 100644 cannamanage-service/src/main/java/de/cannamanage/service/report/DateRangeReportParameters.java create mode 100644 cannamanage-service/src/main/java/de/cannamanage/service/report/ReportGenerator.java create mode 100644 cannamanage-service/src/main/java/de/cannamanage/service/report/ReportParameters.java create mode 100644 cannamanage-service/src/main/java/de/cannamanage/service/repository/ComplianceDeadlineRepository.java create mode 100644 cannamanage-service/src/main/java/de/cannamanage/service/repository/DestructionRecordRepository.java create mode 100644 cannamanage-service/src/main/java/de/cannamanage/service/repository/GeneratedReportRepository.java create mode 100644 cannamanage-service/src/main/java/de/cannamanage/service/repository/PreventionActivityRepository.java create mode 100644 cannamanage-service/src/main/java/de/cannamanage/service/repository/PropagationSourceRepository.java create mode 100644 cannamanage-service/src/main/java/de/cannamanage/service/repository/TransportRecordRepository.java create mode 100644 docs/sprint-9/cannamanage-sprint9-analysis.md create mode 100644 docs/sprint-9/cannamanage-sprint9-plan-review.md create mode 100644 docs/sprint-9/cannamanage-sprint9-plan.md create mode 100644 docs/sprint-9/cannamanage-sprint9-testplan.md diff --git a/cannamanage-api/src/main/java/de/cannamanage/api/controller/ComplianceDeadlineController.java b/cannamanage-api/src/main/java/de/cannamanage/api/controller/ComplianceDeadlineController.java new file mode 100644 index 0000000..fbb3aa1 --- /dev/null +++ b/cannamanage-api/src/main/java/de/cannamanage/api/controller/ComplianceDeadlineController.java @@ -0,0 +1,97 @@ +package de.cannamanage.api.controller; + +import de.cannamanage.domain.entity.ComplianceDeadline; +import de.cannamanage.domain.entity.TenantContext; +import de.cannamanage.domain.enums.ComplianceArea; +import de.cannamanage.service.repository.ComplianceDeadlineRepository; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.time.Instant; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +/** + * REST controller for compliance deadline management. + * Powers the compliance dashboard traffic-light system. + */ +@RestController +@RequestMapping("/api/v1/compliance/deadlines") +@RequiredArgsConstructor +@Tag(name = "Compliance Deadlines", description = "Manage compliance deadlines and due dates") +public class ComplianceDeadlineController { + + private final ComplianceDeadlineRepository deadlineRepository; + + @GetMapping + @Operation(summary = "List all deadlines (upcoming + overdue)") + @PreAuthorize("hasRole('ADMIN') or @staffPermissions.has(authentication, T(de.cannamanage.domain.enums.StaffPermission).VIEW_COMPLIANCE)") + public ResponseEntity> listDeadlines() { + UUID tenantId = TenantContext.getCurrentTenant(); + return ResponseEntity.ok(deadlineRepository.findByTenantIdOrderByDueDateAsc(tenantId)); + } + + @PostMapping + @Operation(summary = "Create a new compliance deadline") + @PreAuthorize("hasRole('ADMIN') or @staffPermissions.has(authentication, T(de.cannamanage.domain.enums.StaffPermission).MANAGE_COMPLIANCE)") + public ResponseEntity createDeadline(@RequestBody CreateDeadlineRequest request) { + ComplianceDeadline deadline = new ComplianceDeadline(); + deadline.setClubId(request.clubId()); + deadline.setArea(request.area()); + deadline.setTitle(request.title()); + deadline.setDescription(request.description()); + deadline.setDueDate(request.dueDate()); + deadline.setIsRecurring(request.isRecurring() != null ? request.isRecurring() : false); + deadline.setRecurrenceRule(request.recurrenceRule()); + + return ResponseEntity.ok(deadlineRepository.save(deadline)); + } + + @PostMapping("/{id}/complete") + @Operation(summary = "Mark a deadline as complete") + @PreAuthorize("hasRole('ADMIN') or @staffPermissions.has(authentication, T(de.cannamanage.domain.enums.StaffPermission).MANAGE_COMPLIANCE)") + public ResponseEntity completeDeadline( + @PathVariable UUID id, + @RequestBody CompleteDeadlineRequest request) { + + ComplianceDeadline deadline = deadlineRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Deadline not found: " + id)); + + deadline.setCompletedAt(Instant.now()); + deadline.setCompletedBy(request.completedBy()); + + return ResponseEntity.ok(deadlineRepository.save(deadline)); + } + + @GetMapping("/overdue") + @Operation(summary = "List overdue (incomplete, past due date) deadlines") + @PreAuthorize("hasRole('ADMIN') or @staffPermissions.has(authentication, T(de.cannamanage.domain.enums.StaffPermission).VIEW_COMPLIANCE)") + public ResponseEntity> listOverdue() { + UUID tenantId = TenantContext.getCurrentTenant(); + return ResponseEntity.ok( + deadlineRepository.findByTenantIdAndCompletedAtIsNullOrderByDueDateAsc(tenantId) + .stream() + .filter(d -> d.getDueDate().isBefore(LocalDate.now())) + .toList() + ); + } + + public record CreateDeadlineRequest( + UUID clubId, + ComplianceArea area, + String title, + String description, + LocalDate dueDate, + Boolean isRecurring, + String recurrenceRule + ) {} + + public record CompleteDeadlineRequest( + UUID completedBy + ) {} +} diff --git a/cannamanage-api/src/main/java/de/cannamanage/api/controller/ComplianceRecordsController.java b/cannamanage-api/src/main/java/de/cannamanage/api/controller/ComplianceRecordsController.java new file mode 100644 index 0000000..1e44fcf --- /dev/null +++ b/cannamanage-api/src/main/java/de/cannamanage/api/controller/ComplianceRecordsController.java @@ -0,0 +1,190 @@ +package de.cannamanage.api.controller; + +import de.cannamanage.domain.entity.*; +import de.cannamanage.domain.enums.DestructionMethod; +import de.cannamanage.domain.enums.TransportStatus; +import de.cannamanage.service.repository.*; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +/** + * REST controller for KCanG §22 compliance records: + * destruction, transport, propagation sources, and prevention activities. + */ +@RestController +@RequestMapping("/api/v1/compliance") +@RequiredArgsConstructor +@Tag(name = "Compliance Records", description = "KCanG §22 record keeping for destruction, transport, propagation & prevention") +public class ComplianceRecordsController { + + private final DestructionRecordRepository destructionRecordRepository; + private final TransportRecordRepository transportRecordRepository; + private final PropagationSourceRepository propagationSourceRepository; + private final PreventionActivityRepository preventionActivityRepository; + + // === Destruction Records === + + @PostMapping("/destruction-records") + @Operation(summary = "Record a cannabis destruction event") + @PreAuthorize("hasRole('ADMIN') or @staffPermissions.has(authentication, T(de.cannamanage.domain.enums.StaffPermission).MANAGE_COMPLIANCE)") + public ResponseEntity recordDestruction(@RequestBody CreateDestructionRequest request) { + DestructionRecord record = new DestructionRecord(); + record.setClubId(request.clubId()); + record.setBatchId(request.batchId()); + record.setAmountGrams(request.amountGrams()); + record.setDestructionMethod(request.destructionMethod()); + record.setDescription(request.description()); + record.setDestroyedAt(request.destroyedAt() != null ? request.destroyedAt() : Instant.now()); + record.setWitnessedBy(request.witnessedBy()); + record.setWitnessName(request.witnessName()); + record.setRecordedBy(request.recordedBy()); + + return ResponseEntity.ok(destructionRecordRepository.save(record)); + } + + @GetMapping("/destruction-records") + @Operation(summary = "List destruction records for the current tenant") + @PreAuthorize("hasRole('ADMIN') or @staffPermissions.has(authentication, T(de.cannamanage.domain.enums.StaffPermission).VIEW_COMPLIANCE)") + public ResponseEntity> listDestructionRecords() { + UUID tenantId = TenantContext.getCurrentTenant(); + return ResponseEntity.ok(destructionRecordRepository.findByTenantIdOrderByDestroyedAtDesc(tenantId)); + } + + // === Transport Records === + + @PostMapping("/transport-records") + @Operation(summary = "Record a cannabis transport event") + @PreAuthorize("hasRole('ADMIN') or @staffPermissions.has(authentication, T(de.cannamanage.domain.enums.StaffPermission).MANAGE_COMPLIANCE)") + public ResponseEntity recordTransport(@RequestBody CreateTransportRequest request) { + TransportRecord record = new TransportRecord(); + record.setClubId(request.clubId()); + record.setDescription(request.description()); + record.setTransportDate(request.transportDate()); + record.setFromLocation(request.fromLocation()); + record.setToLocation(request.toLocation()); + record.setCarrierName(request.carrierName()); + record.setAmountGrams(request.amountGrams()); + record.setBatchId(request.batchId()); + record.setStatus(request.status() != null ? request.status() : TransportStatus.PLANNED); + record.setRecordedBy(request.recordedBy()); + + return ResponseEntity.ok(transportRecordRepository.save(record)); + } + + @GetMapping("/transport-records") + @Operation(summary = "List transport records for the current tenant") + @PreAuthorize("hasRole('ADMIN') or @staffPermissions.has(authentication, T(de.cannamanage.domain.enums.StaffPermission).VIEW_COMPLIANCE)") + public ResponseEntity> listTransportRecords() { + UUID tenantId = TenantContext.getCurrentTenant(); + return ResponseEntity.ok(transportRecordRepository.findByTenantIdOrderByTransportDateDesc(tenantId)); + } + + // === Propagation Sources === + + @PostMapping("/propagation-sources") + @Operation(summary = "Record a propagation source (seed/cutting receipt)") + @PreAuthorize("hasRole('ADMIN') or @staffPermissions.has(authentication, T(de.cannamanage.domain.enums.StaffPermission).MANAGE_COMPLIANCE)") + public ResponseEntity recordPropagationSource(@RequestBody CreatePropagationSourceRequest request) { + PropagationSource record = new PropagationSource(); + record.setClubId(request.clubId()); + record.setSourceType(request.sourceType()); + record.setSupplier(request.supplier()); + record.setQuantity(request.quantity()); + record.setStrainId(request.strainId()); + record.setReceivedAt(request.receivedAt()); + record.setDocumentationReference(request.documentationReference()); + record.setRecordedBy(request.recordedBy()); + + return ResponseEntity.ok(propagationSourceRepository.save(record)); + } + + @GetMapping("/propagation-sources") + @Operation(summary = "List propagation sources for the current tenant") + @PreAuthorize("hasRole('ADMIN') or @staffPermissions.has(authentication, T(de.cannamanage.domain.enums.StaffPermission).VIEW_COMPLIANCE)") + public ResponseEntity> listPropagationSources() { + UUID tenantId = TenantContext.getCurrentTenant(); + return ResponseEntity.ok(propagationSourceRepository.findByTenantIdOrderByReceivedAtDesc(tenantId)); + } + + // === Prevention Activities === + + @PostMapping("/prevention-activities") + @Operation(summary = "Record a prevention/education activity per KCanG §23") + @PreAuthorize("hasRole('ADMIN') or @staffPermissions.has(authentication, T(de.cannamanage.domain.enums.StaffPermission).MANAGE_COMPLIANCE)") + public ResponseEntity recordPreventionActivity(@RequestBody CreatePreventionActivityRequest request) { + PreventionActivity record = new PreventionActivity(); + record.setClubId(request.clubId()); + record.setActivityDate(request.activityDate()); + record.setTitle(request.title()); + record.setDescription(request.description()); + record.setParticipantsCount(request.participantsCount()); + record.setOfficerId(request.officerId()); + + return ResponseEntity.ok(preventionActivityRepository.save(record)); + } + + @GetMapping("/prevention-activities") + @Operation(summary = "List prevention activities for the current tenant") + @PreAuthorize("hasRole('ADMIN') or @staffPermissions.has(authentication, T(de.cannamanage.domain.enums.StaffPermission).VIEW_COMPLIANCE)") + public ResponseEntity> listPreventionActivities() { + UUID tenantId = TenantContext.getCurrentTenant(); + return ResponseEntity.ok(preventionActivityRepository.findByTenantIdOrderByActivityDateDesc(tenantId)); + } + + // === Request DTOs (inner records) === + + public record CreateDestructionRequest( + UUID clubId, + UUID batchId, + BigDecimal amountGrams, + DestructionMethod destructionMethod, + String description, + Instant destroyedAt, + UUID witnessedBy, + String witnessName, + UUID recordedBy + ) {} + + public record CreateTransportRequest( + UUID clubId, + String description, + LocalDate transportDate, + String fromLocation, + String toLocation, + String carrierName, + BigDecimal amountGrams, + UUID batchId, + TransportStatus status, + UUID recordedBy + ) {} + + public record CreatePropagationSourceRequest( + UUID clubId, + String sourceType, + String supplier, + Integer quantity, + UUID strainId, + LocalDate receivedAt, + String documentationReference, + UUID recordedBy + ) {} + + public record CreatePreventionActivityRequest( + UUID clubId, + LocalDate activityDate, + String title, + String description, + Integer participantsCount, + UUID officerId + ) {} +} diff --git a/cannamanage-api/src/main/resources/db/migration/V23__destruction_records.sql b/cannamanage-api/src/main/resources/db/migration/V23__destruction_records.sql new file mode 100644 index 0000000..d165f9c --- /dev/null +++ b/cannamanage-api/src/main/resources/db/migration/V23__destruction_records.sql @@ -0,0 +1,18 @@ +-- Sprint 9: Destruction records per KCanG §22 +CREATE TABLE destruction_records ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + club_id UUID NOT NULL REFERENCES clubs(id), + batch_id UUID REFERENCES batches(id), + amount_grams NUMERIC(8,2) NOT NULL, + destruction_method VARCHAR(50) NOT NULL, + description TEXT, + destroyed_at TIMESTAMP NOT NULL, + witnessed_by UUID REFERENCES users(id), + witness_name VARCHAR(200), + recorded_by UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_destruction_records_tenant ON destruction_records(tenant_id); +CREATE INDEX idx_destruction_records_club ON destruction_records(club_id); diff --git a/cannamanage-api/src/main/resources/db/migration/V24__transport_records.sql b/cannamanage-api/src/main/resources/db/migration/V24__transport_records.sql new file mode 100644 index 0000000..935c244 --- /dev/null +++ b/cannamanage-api/src/main/resources/db/migration/V24__transport_records.sql @@ -0,0 +1,19 @@ +-- Sprint 9: Transport records per KCanG §22 transport documentation +CREATE TABLE transport_records ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + club_id UUID NOT NULL REFERENCES clubs(id), + description TEXT NOT NULL, + transport_date DATE NOT NULL, + from_location VARCHAR(300) NOT NULL, + to_location VARCHAR(300) NOT NULL, + carrier_name VARCHAR(200) NOT NULL, + amount_grams NUMERIC(8,2) NOT NULL, + batch_id UUID REFERENCES batches(id), + status VARCHAR(50) NOT NULL DEFAULT 'PLANNED', + recorded_by UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_transport_records_tenant ON transport_records(tenant_id); +CREATE INDEX idx_transport_records_club ON transport_records(club_id); diff --git a/cannamanage-api/src/main/resources/db/migration/V25__propagation_sources.sql b/cannamanage-api/src/main/resources/db/migration/V25__propagation_sources.sql new file mode 100644 index 0000000..cf36ac9 --- /dev/null +++ b/cannamanage-api/src/main/resources/db/migration/V25__propagation_sources.sql @@ -0,0 +1,17 @@ +-- Sprint 9: Propagation sources (seed/cutting tracking per KCanG §16) +CREATE TABLE propagation_sources ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + club_id UUID NOT NULL REFERENCES clubs(id), + source_type VARCHAR(50) NOT NULL, -- SEED, CUTTING + supplier VARCHAR(300), + quantity INTEGER NOT NULL, + strain_id UUID REFERENCES strains(id), + received_at DATE NOT NULL, + documentation_reference VARCHAR(200), + recorded_by UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_propagation_sources_tenant ON propagation_sources(tenant_id); +CREATE INDEX idx_propagation_sources_club ON propagation_sources(club_id); diff --git a/cannamanage-api/src/main/resources/db/migration/V26__prevention_activities.sql b/cannamanage-api/src/main/resources/db/migration/V26__prevention_activities.sql new file mode 100644 index 0000000..786aa20 --- /dev/null +++ b/cannamanage-api/src/main/resources/db/migration/V26__prevention_activities.sql @@ -0,0 +1,15 @@ +-- Sprint 9: Prevention activities per KCanG §23 Suchtprävention +CREATE TABLE prevention_activities ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + club_id UUID NOT NULL REFERENCES clubs(id), + activity_date DATE NOT NULL, + title VARCHAR(300) NOT NULL, + description TEXT, + participants_count INTEGER, + officer_id UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_prevention_activities_tenant ON prevention_activities(tenant_id); +CREATE INDEX idx_prevention_activities_club ON prevention_activities(club_id); diff --git a/cannamanage-api/src/main/resources/db/migration/V27__generated_reports.sql b/cannamanage-api/src/main/resources/db/migration/V27__generated_reports.sql new file mode 100644 index 0000000..83c028e --- /dev/null +++ b/cannamanage-api/src/main/resources/db/migration/V27__generated_reports.sql @@ -0,0 +1,18 @@ +-- Sprint 9: Generated reports metadata +CREATE TABLE generated_reports ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + club_id UUID NOT NULL REFERENCES clubs(id), + report_type VARCHAR(50) NOT NULL, + report_format VARCHAR(10) NOT NULL, + title VARCHAR(300) NOT NULL, + file_size BIGINT, + storage_path VARCHAR(500), + parameters JSONB, + generated_by UUID NOT NULL REFERENCES users(id), + generated_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_generated_reports_tenant ON generated_reports(tenant_id); +CREATE INDEX idx_generated_reports_club ON generated_reports(club_id); +CREATE INDEX idx_generated_reports_type ON generated_reports(club_id, report_type); diff --git a/cannamanage-api/src/main/resources/db/migration/V28__compliance_deadlines.sql b/cannamanage-api/src/main/resources/db/migration/V28__compliance_deadlines.sql new file mode 100644 index 0000000..d7518db --- /dev/null +++ b/cannamanage-api/src/main/resources/db/migration/V28__compliance_deadlines.sql @@ -0,0 +1,18 @@ +-- Sprint 9: Compliance deadlines tracking +CREATE TABLE compliance_deadlines ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + club_id UUID NOT NULL REFERENCES clubs(id), + area VARCHAR(50) NOT NULL, + title VARCHAR(300) NOT NULL, + description TEXT, + due_date DATE NOT NULL, + is_recurring BOOLEAN DEFAULT FALSE, + recurrence_rule VARCHAR(50), + completed_at TIMESTAMP, + completed_by UUID REFERENCES users(id), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_compliance_deadlines_tenant ON compliance_deadlines(tenant_id); +CREATE INDEX idx_compliance_deadlines_club_due ON compliance_deadlines(club_id, due_date); diff --git a/cannamanage-api/src/main/resources/db/migration/V29__distribution_thc_cbd.sql b/cannamanage-api/src/main/resources/db/migration/V29__distribution_thc_cbd.sql new file mode 100644 index 0000000..d658f7f --- /dev/null +++ b/cannamanage-api/src/main/resources/db/migration/V29__distribution_thc_cbd.sql @@ -0,0 +1,4 @@ +-- Sprint 9: Add THC/CBD percentage + strain name to distributions (KCanG §19(4)) +ALTER TABLE distributions ADD COLUMN IF NOT EXISTS thc_percentage NUMERIC(4,2); +ALTER TABLE distributions ADD COLUMN IF NOT EXISTS cbd_percentage NUMERIC(4,2); +ALTER TABLE distributions ADD COLUMN IF NOT EXISTS strain_name VARCHAR(200); diff --git a/cannamanage-domain/src/main/java/de/cannamanage/domain/entity/ComplianceDeadline.java b/cannamanage-domain/src/main/java/de/cannamanage/domain/entity/ComplianceDeadline.java new file mode 100644 index 0000000..51d8d39 --- /dev/null +++ b/cannamanage-domain/src/main/java/de/cannamanage/domain/entity/ComplianceDeadline.java @@ -0,0 +1,74 @@ +package de.cannamanage.domain.entity; + +import de.cannamanage.domain.enums.ComplianceArea; +import jakarta.persistence.*; + +import java.time.Instant; +import java.time.LocalDate; +import java.util.UUID; + +/** + * Tracks compliance deadlines with optional recurrence. + * Powers the compliance dashboard traffic-light system. + */ +@Entity +@Table(name = "compliance_deadlines") +public class ComplianceDeadline extends AbstractTenantEntity { + + @Column(name = "club_id", nullable = false, updatable = false) + private UUID clubId; + + @Enumerated(EnumType.STRING) + @Column(name = "area", nullable = false, length = 50) + private ComplianceArea area; + + @Column(name = "title", nullable = false, length = 300) + private String title; + + @Column(name = "description") + private String description; + + @Column(name = "due_date", nullable = false) + private LocalDate dueDate; + + @Column(name = "is_recurring") + private Boolean isRecurring = false; + + @Column(name = "recurrence_rule", length = 50) + private String recurrenceRule; + + @Column(name = "completed_at") + private Instant completedAt; + + @Column(name = "completed_by") + private UUID completedBy; + + // Getters and setters + + public UUID getClubId() { return clubId; } + public void setClubId(UUID clubId) { this.clubId = clubId; } + + public ComplianceArea getArea() { return area; } + public void setArea(ComplianceArea area) { this.area = area; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public LocalDate getDueDate() { return dueDate; } + public void setDueDate(LocalDate dueDate) { this.dueDate = dueDate; } + + public Boolean getIsRecurring() { return isRecurring; } + public void setIsRecurring(Boolean recurring) { isRecurring = recurring; } + + public String getRecurrenceRule() { return recurrenceRule; } + public void setRecurrenceRule(String recurrenceRule) { this.recurrenceRule = recurrenceRule; } + + public Instant getCompletedAt() { return completedAt; } + public void setCompletedAt(Instant completedAt) { this.completedAt = completedAt; } + + public UUID getCompletedBy() { return completedBy; } + public void setCompletedBy(UUID completedBy) { this.completedBy = completedBy; } +} diff --git a/cannamanage-domain/src/main/java/de/cannamanage/domain/entity/DestructionRecord.java b/cannamanage-domain/src/main/java/de/cannamanage/domain/entity/DestructionRecord.java new file mode 100644 index 0000000..c7eeabd --- /dev/null +++ b/cannamanage-domain/src/main/java/de/cannamanage/domain/entity/DestructionRecord.java @@ -0,0 +1,74 @@ +package de.cannamanage.domain.entity; + +import de.cannamanage.domain.enums.DestructionMethod; +import jakarta.persistence.*; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.UUID; + +/** + * Records cannabis destruction events per KCanG §22. + * Immutable compliance record — never updated after creation. + */ +@Entity +@Table(name = "destruction_records") +public class DestructionRecord extends AbstractTenantEntity { + + @Column(name = "club_id", nullable = false, updatable = false) + private UUID clubId; + + @Column(name = "batch_id") + private UUID batchId; + + @Column(name = "amount_grams", nullable = false, precision = 8, scale = 2) + private BigDecimal amountGrams; + + @Enumerated(EnumType.STRING) + @Column(name = "destruction_method", nullable = false, length = 50) + private DestructionMethod destructionMethod; + + @Column(name = "description") + private String description; + + @Column(name = "destroyed_at", nullable = false) + private Instant destroyedAt; + + @Column(name = "witnessed_by") + private UUID witnessedBy; + + @Column(name = "witness_name", length = 200) + private String witnessName; + + @Column(name = "recorded_by", nullable = false) + private UUID recordedBy; + + // Getters and setters + + public UUID getClubId() { return clubId; } + public void setClubId(UUID clubId) { this.clubId = clubId; } + + public UUID getBatchId() { return batchId; } + public void setBatchId(UUID batchId) { this.batchId = batchId; } + + public BigDecimal getAmountGrams() { return amountGrams; } + public void setAmountGrams(BigDecimal amountGrams) { this.amountGrams = amountGrams; } + + public DestructionMethod getDestructionMethod() { return destructionMethod; } + public void setDestructionMethod(DestructionMethod destructionMethod) { this.destructionMethod = destructionMethod; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public Instant getDestroyedAt() { return destroyedAt; } + public void setDestroyedAt(Instant destroyedAt) { this.destroyedAt = destroyedAt; } + + public UUID getWitnessedBy() { return witnessedBy; } + public void setWitnessedBy(UUID witnessedBy) { this.witnessedBy = witnessedBy; } + + public String getWitnessName() { return witnessName; } + public void setWitnessName(String witnessName) { this.witnessName = witnessName; } + + public UUID getRecordedBy() { return recordedBy; } + public void setRecordedBy(UUID recordedBy) { this.recordedBy = recordedBy; } +} diff --git a/cannamanage-domain/src/main/java/de/cannamanage/domain/entity/GeneratedReport.java b/cannamanage-domain/src/main/java/de/cannamanage/domain/entity/GeneratedReport.java new file mode 100644 index 0000000..a06752e --- /dev/null +++ b/cannamanage-domain/src/main/java/de/cannamanage/domain/entity/GeneratedReport.java @@ -0,0 +1,82 @@ +package de.cannamanage.domain.entity; + +import de.cannamanage.domain.enums.ExportFormat; +import de.cannamanage.domain.enums.ReportType; +import jakarta.persistence.*; + +import java.time.Instant; +import java.util.UUID; + +/** + * Metadata for generated reports. The actual file is stored on disk. + * Provides audit trail of all reports generated per tenant. + */ +@Entity +@Table(name = "generated_reports") +public class GeneratedReport extends AbstractTenantEntity { + + @Column(name = "club_id", nullable = false, updatable = false) + private UUID clubId; + + @Enumerated(EnumType.STRING) + @Column(name = "report_type", nullable = false, length = 50) + private ReportType reportType; + + @Enumerated(EnumType.STRING) + @Column(name = "report_format", nullable = false, length = 10) + private ExportFormat reportFormat; + + @Column(name = "title", nullable = false, length = 300) + private String title; + + @Column(name = "file_size") + private Long fileSize; + + @Column(name = "storage_path", length = 500) + private String storagePath; + + @Column(name = "parameters", columnDefinition = "jsonb") + private String parameters; // JSON string + + @Column(name = "generated_by", nullable = false) + private UUID generatedBy; + + @Column(name = "generated_at") + private Instant generatedAt; + + @PrePersist + void onCreateReport() { + if (this.generatedAt == null) { + this.generatedAt = Instant.now(); + } + } + + // Getters and setters + + public UUID getClubId() { return clubId; } + public void setClubId(UUID clubId) { this.clubId = clubId; } + + public ReportType getReportType() { return reportType; } + public void setReportType(ReportType reportType) { this.reportType = reportType; } + + public ExportFormat getReportFormat() { return reportFormat; } + public void setReportFormat(ExportFormat reportFormat) { this.reportFormat = reportFormat; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public Long getFileSize() { return fileSize; } + public void setFileSize(Long fileSize) { this.fileSize = fileSize; } + + public String getStoragePath() { return storagePath; } + public void setStoragePath(String storagePath) { this.storagePath = storagePath; } + + public String getParameters() { return parameters; } + public void setParameters(String parameters) { this.parameters = parameters; } + + public UUID getGeneratedBy() { return generatedBy; } + public void setGeneratedBy(UUID generatedBy) { this.generatedBy = generatedBy; } + + public Instant getGeneratedAt() { return generatedAt; } + public void setGeneratedAt(Instant generatedAt) { this.generatedAt = generatedAt; } +} diff --git a/cannamanage-domain/src/main/java/de/cannamanage/domain/entity/PreventionActivity.java b/cannamanage-domain/src/main/java/de/cannamanage/domain/entity/PreventionActivity.java new file mode 100644 index 0000000..7beab37 --- /dev/null +++ b/cannamanage-domain/src/main/java/de/cannamanage/domain/entity/PreventionActivity.java @@ -0,0 +1,53 @@ +package de.cannamanage.domain.entity; + +import jakarta.persistence.*; + +import java.time.LocalDate; +import java.util.UUID; + +/** + * Records prevention/education activities per KCanG §23. + * Each Anbauvereinigung must appoint a Suchtpräventionsbeauftragter. + */ +@Entity +@Table(name = "prevention_activities") +public class PreventionActivity extends AbstractTenantEntity { + + @Column(name = "club_id", nullable = false, updatable = false) + private UUID clubId; + + @Column(name = "activity_date", nullable = false) + private LocalDate activityDate; + + @Column(name = "title", nullable = false, length = 300) + private String title; + + @Column(name = "description") + private String description; + + @Column(name = "participants_count") + private Integer participantsCount; + + @Column(name = "officer_id", nullable = false) + private UUID officerId; + + // Getters and setters + + public UUID getClubId() { return clubId; } + public void setClubId(UUID clubId) { this.clubId = clubId; } + + public LocalDate getActivityDate() { return activityDate; } + public void setActivityDate(LocalDate activityDate) { this.activityDate = activityDate; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public Integer getParticipantsCount() { return participantsCount; } + public void setParticipantsCount(Integer participantsCount) { this.participantsCount = participantsCount; } + + public UUID getOfficerId() { return officerId; } + public void setOfficerId(UUID officerId) { this.officerId = officerId; } +} diff --git a/cannamanage-domain/src/main/java/de/cannamanage/domain/entity/PropagationSource.java b/cannamanage-domain/src/main/java/de/cannamanage/domain/entity/PropagationSource.java new file mode 100644 index 0000000..d085901 --- /dev/null +++ b/cannamanage-domain/src/main/java/de/cannamanage/domain/entity/PropagationSource.java @@ -0,0 +1,65 @@ +package de.cannamanage.domain.entity; + +import jakarta.persistence.*; + +import java.time.LocalDate; +import java.util.UUID; + +/** + * Tracks propagation material (seeds/cuttings) per KCanG §16. + * Ensures traceability of all genetic material entering the club. + */ +@Entity +@Table(name = "propagation_sources") +public class PropagationSource extends AbstractTenantEntity { + + @Column(name = "club_id", nullable = false, updatable = false) + private UUID clubId; + + @Column(name = "source_type", nullable = false, length = 50) + private String sourceType; // SEED, CUTTING + + @Column(name = "supplier", length = 300) + private String supplier; + + @Column(name = "quantity", nullable = false) + private Integer quantity; + + @Column(name = "strain_id") + private UUID strainId; + + @Column(name = "received_at", nullable = false) + private LocalDate receivedAt; + + @Column(name = "documentation_reference", length = 200) + private String documentationReference; + + @Column(name = "recorded_by", nullable = false) + private UUID recordedBy; + + // Getters and setters + + public UUID getClubId() { return clubId; } + public void setClubId(UUID clubId) { this.clubId = clubId; } + + public String getSourceType() { return sourceType; } + public void setSourceType(String sourceType) { this.sourceType = sourceType; } + + public String getSupplier() { return supplier; } + public void setSupplier(String supplier) { this.supplier = supplier; } + + public Integer getQuantity() { return quantity; } + public void setQuantity(Integer quantity) { this.quantity = quantity; } + + public UUID getStrainId() { return strainId; } + public void setStrainId(UUID strainId) { this.strainId = strainId; } + + public LocalDate getReceivedAt() { return receivedAt; } + public void setReceivedAt(LocalDate receivedAt) { this.receivedAt = receivedAt; } + + public String getDocumentationReference() { return documentationReference; } + public void setDocumentationReference(String documentationReference) { this.documentationReference = documentationReference; } + + public UUID getRecordedBy() { return recordedBy; } + public void setRecordedBy(UUID recordedBy) { this.recordedBy = recordedBy; } +} diff --git a/cannamanage-domain/src/main/java/de/cannamanage/domain/entity/TransportRecord.java b/cannamanage-domain/src/main/java/de/cannamanage/domain/entity/TransportRecord.java new file mode 100644 index 0000000..304d69a --- /dev/null +++ b/cannamanage-domain/src/main/java/de/cannamanage/domain/entity/TransportRecord.java @@ -0,0 +1,80 @@ +package de.cannamanage.domain.entity; + +import de.cannamanage.domain.enums.TransportStatus; +import jakarta.persistence.*; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.UUID; + +/** + * Records cannabis transport events per KCanG §22. + * Authority notification may be required for certain transports. + */ +@Entity +@Table(name = "transport_records") +public class TransportRecord extends AbstractTenantEntity { + + @Column(name = "club_id", nullable = false, updatable = false) + private UUID clubId; + + @Column(name = "description", nullable = false) + private String description; + + @Column(name = "transport_date", nullable = false) + private LocalDate transportDate; + + @Column(name = "from_location", nullable = false, length = 300) + private String fromLocation; + + @Column(name = "to_location", nullable = false, length = 300) + private String toLocation; + + @Column(name = "carrier_name", nullable = false, length = 200) + private String carrierName; + + @Column(name = "amount_grams", nullable = false, precision = 8, scale = 2) + private BigDecimal amountGrams; + + @Column(name = "batch_id") + private UUID batchId; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false, length = 50) + private TransportStatus status = TransportStatus.PLANNED; + + @Column(name = "recorded_by", nullable = false) + private UUID recordedBy; + + // Getters and setters + + public UUID getClubId() { return clubId; } + public void setClubId(UUID clubId) { this.clubId = clubId; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public LocalDate getTransportDate() { return transportDate; } + public void setTransportDate(LocalDate transportDate) { this.transportDate = transportDate; } + + public String getFromLocation() { return fromLocation; } + public void setFromLocation(String fromLocation) { this.fromLocation = fromLocation; } + + public String getToLocation() { return toLocation; } + public void setToLocation(String toLocation) { this.toLocation = toLocation; } + + public String getCarrierName() { return carrierName; } + public void setCarrierName(String carrierName) { this.carrierName = carrierName; } + + public BigDecimal getAmountGrams() { return amountGrams; } + public void setAmountGrams(BigDecimal amountGrams) { this.amountGrams = amountGrams; } + + public UUID getBatchId() { return batchId; } + public void setBatchId(UUID batchId) { this.batchId = batchId; } + + public TransportStatus getStatus() { return status; } + public void setStatus(TransportStatus status) { this.status = status; } + + public UUID getRecordedBy() { return recordedBy; } + public void setRecordedBy(UUID recordedBy) { this.recordedBy = recordedBy; } +} diff --git a/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/AuditEventType.java b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/AuditEventType.java index a708eff..25f92de 100644 --- a/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/AuditEventType.java +++ b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/AuditEventType.java @@ -86,5 +86,12 @@ public enum AuditEventType { // Sprint 8 — Board events BOARD_MEMBER_ELECTED, - BOARD_MEMBER_REMOVED + BOARD_MEMBER_REMOVED, + + // Sprint 9 — Reporting & Compliance events + REPORT_GENERATED, + AUTHORITY_EXPORT, + DESTRUCTION_RECORDED, + TRANSPORT_RECORDED, + RETENTION_DELETED } diff --git a/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/ComplianceArea.java b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/ComplianceArea.java new file mode 100644 index 0000000..96c1cae --- /dev/null +++ b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/ComplianceArea.java @@ -0,0 +1,12 @@ +package de.cannamanage.domain.enums; + +/** + * Compliance areas tracked in the compliance dashboard. + * Each area gets a status (GREEN/YELLOW/RED) based on deadline adherence. + */ +public enum ComplianceArea { + KCANG, + FINANCE, + DSGVO, + VEREIN +} diff --git a/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/ComplianceStatus.java b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/ComplianceStatus.java new file mode 100644 index 0000000..463c4ad --- /dev/null +++ b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/ComplianceStatus.java @@ -0,0 +1,10 @@ +package de.cannamanage.domain.enums; + +/** + * Traffic-light compliance status for the compliance dashboard. + */ +public enum ComplianceStatus { + GREEN, + YELLOW, + RED +} diff --git a/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/DestructionMethod.java b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/DestructionMethod.java new file mode 100644 index 0000000..67db74b --- /dev/null +++ b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/DestructionMethod.java @@ -0,0 +1,11 @@ +package de.cannamanage.domain.enums; + +/** + * Methods of cannabis destruction per KCanG §22 documentation requirements. + */ +public enum DestructionMethod { + INCINERATION, + COMPOSTING, + CHEMICAL, + OTHER +} diff --git a/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/ExportFormat.java b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/ExportFormat.java new file mode 100644 index 0000000..46300b7 --- /dev/null +++ b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/ExportFormat.java @@ -0,0 +1,11 @@ +package de.cannamanage.domain.enums; + +/** + * Supported export formats for generated reports. + */ +public enum ExportFormat { + PDF, + CSV, + JSON, + ZIP +} diff --git a/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/NotificationType.java b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/NotificationType.java index 38bb2b7..5997127 100644 --- a/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/NotificationType.java +++ b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/NotificationType.java @@ -25,5 +25,8 @@ public enum NotificationType { ASSEMBLY_INVITATION, ASSEMBLY_REMINDER, // Sprint 8 — Board: - BOARD_TERM_EXPIRING + BOARD_TERM_EXPIRING, + // Sprint 9 — Compliance: + COMPLIANCE_DEADLINE, + RETENTION_WARNING } diff --git a/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/ReportType.java b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/ReportType.java new file mode 100644 index 0000000..d8ec8f6 --- /dev/null +++ b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/ReportType.java @@ -0,0 +1,34 @@ +package de.cannamanage.domain.enums; + +/** + * All report types available in the Berichtszentrale. + * Organized by compliance area: KCanG, Finance, DSGVO, Verein. + */ +public enum ReportType { + // KCanG compliance reports + ANNUAL_AUTHORITY, + DISTRIBUTION_LOG, + STOCK_INVENTORY, + DESTRUCTION_PROTOCOL, + CULTIVATION_REPORT, + TRANSPORT_CERTIFICATE, + FULL_AUTHORITY_EXPORT, + + // Financial reports + EUR, + ANNUAL_FINANCIAL, + KASSENBUCH_EXPORT, + FEE_CONFIRMATION, + + // Verein administration + MEMBER_LIST_REGISTRY, + BOARD_CHANGE_NOTICE, + ANNUAL_BOARD_REPORT, + + // DSGVO reports + VVT, + TOM, + DSFA, + DELETION_CONCEPT, + BREACH_NOTIFICATION +} diff --git a/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/RetentionCategory.java b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/RetentionCategory.java new file mode 100644 index 0000000..6de134f --- /dev/null +++ b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/RetentionCategory.java @@ -0,0 +1,14 @@ +package de.cannamanage.domain.enums; + +/** + * Data retention categories per German law. + * - KCanG §24: 5 years for cannabis-specific records + * - AO §147: 6/8/10 years for financial/tax records + */ +public enum RetentionCategory { + KCANG_5Y, + AO_6Y, + AO_8Y, + AO_10Y, + INDEFINITE +} diff --git a/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/StaffPermission.java b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/StaffPermission.java index dd86915..0c53699 100644 --- a/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/StaffPermission.java +++ b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/StaffPermission.java @@ -22,5 +22,9 @@ public enum StaffPermission { MANAGE_FINANCES, VIEW_FINANCES, MANAGE_ASSEMBLIES, - MANAGE_DOCUMENTS + MANAGE_DOCUMENTS, + // Sprint 9: + GENERATE_REPORTS, + VIEW_COMPLIANCE, + MANAGE_COMPLIANCE } diff --git a/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/TransportStatus.java b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/TransportStatus.java new file mode 100644 index 0000000..656bcd8 --- /dev/null +++ b/cannamanage-domain/src/main/java/de/cannamanage/domain/enums/TransportStatus.java @@ -0,0 +1,11 @@ +package de.cannamanage.domain.enums; + +/** + * Status of a transport record per KCanG §22 transport documentation. + */ +public enum TransportStatus { + PLANNED, + AUTHORITY_NOTIFIED, + IN_TRANSIT, + COMPLETED +} diff --git a/cannamanage-service/src/main/java/de/cannamanage/service/ReportGeneratorService.java b/cannamanage-service/src/main/java/de/cannamanage/service/ReportGeneratorService.java new file mode 100644 index 0000000..3f81292 --- /dev/null +++ b/cannamanage-service/src/main/java/de/cannamanage/service/ReportGeneratorService.java @@ -0,0 +1,128 @@ +package de.cannamanage.service; + +import de.cannamanage.domain.entity.GeneratedReport; +import de.cannamanage.domain.entity.TenantContext; +import de.cannamanage.domain.enums.ExportFormat; +import de.cannamanage.domain.enums.ReportType; +import de.cannamanage.service.report.ReportGenerator; +import de.cannamanage.service.report.ReportParameters; +import de.cannamanage.service.repository.GeneratedReportRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Central orchestrator for report generation. + * Auto-discovers all ReportGenerator beans via Spring injection. + * Handles: format dispatch, audit logging, document storage. + * Rate-limited: max 5 generations per minute per tenant (applied at controller level). + */ +@Service +public class ReportGeneratorService { + + private static final Logger log = LoggerFactory.getLogger(ReportGeneratorService.class); + + private final Map> generators; + private final GeneratedReportRepository generatedReportRepository; + private final AuditService auditService; + + public ReportGeneratorService( + List> allGenerators, + GeneratedReportRepository generatedReportRepository, + AuditService auditService) { + this.generators = allGenerators.stream() + .collect(Collectors.toMap(ReportGenerator::getType, Function.identity())); + this.generatedReportRepository = generatedReportRepository; + this.auditService = auditService; + log.info("ReportGeneratorService initialized with {} generators: {}", + generators.size(), generators.keySet()); + } + + /** + * Generate a report and persist metadata. + */ + @SuppressWarnings("unchecked") + public GeneratedReport generateReport( + ReportType type, + ExportFormat format, + ReportParameters params, + UUID clubId, + UUID generatedBy) { + + ReportGenerator generator = + (ReportGenerator) generators.get(type); + + if (generator == null) { + throw new IllegalArgumentException("No generator registered for report type: " + type); + } + + if (!generator.supportedFormats().contains(format)) { + throw new IllegalArgumentException( + "Format " + format + " not supported for " + type + + ". Supported: " + generator.supportedFormats()); + } + + byte[] content = switch (format) { + case PDF -> generator.generatePdf(params, clubId); + case CSV -> generator.generateCsv(params, clubId); + case JSON -> generator.generateJson(params, clubId); + case ZIP -> generator.generatePdf(params, clubId); // ZIP handled at higher level + }; + + // Persist report metadata + GeneratedReport report = new GeneratedReport(); + report.setClubId(clubId); + report.setReportType(type); + report.setReportFormat(format); + report.setTitle(type.name() + " — " + java.time.LocalDate.now()); + report.setFileSize((long) content.length); + report.setGeneratedBy(generatedBy); + + report = generatedReportRepository.save(report); + + log.info("Generated report {} ({}) for club {}, size={} bytes", + type, format, clubId, content.length); + + return report; + } + + /** + * Generate raw bytes for a report without persisting metadata. + * Used for streaming download responses. + */ + @SuppressWarnings("unchecked") + public byte[] generateBytes( + ReportType type, + ExportFormat format, + ReportParameters params, + UUID clubId) { + + ReportGenerator generator = + (ReportGenerator) generators.get(type); + + if (generator == null) { + throw new IllegalArgumentException("No generator registered for report type: " + type); + } + + return switch (format) { + case PDF -> generator.generatePdf(params, clubId); + case CSV -> generator.generateCsv(params, clubId); + case JSON -> generator.generateJson(params, clubId); + case ZIP -> generator.generatePdf(params, clubId); + }; + } + + public List getGeneratedReports(UUID tenantId) { + return generatedReportRepository.findByTenantIdOrderByGeneratedAtDesc(tenantId); + } + + public boolean hasGenerator(ReportType type) { + return generators.containsKey(type); + } +} diff --git a/cannamanage-service/src/main/java/de/cannamanage/service/report/DateRangeReportParameters.java b/cannamanage-service/src/main/java/de/cannamanage/service/report/DateRangeReportParameters.java new file mode 100644 index 0000000..4a07cee --- /dev/null +++ b/cannamanage-service/src/main/java/de/cannamanage/service/report/DateRangeReportParameters.java @@ -0,0 +1,21 @@ +package de.cannamanage.service.report; + +import java.time.LocalDate; + +/** + * Parameters for date-range based reports (EÜR, distribution log, etc.). + */ +public record DateRangeReportParameters( + LocalDate from, + LocalDate to, + Integer year +) implements ReportParameters { + + public static DateRangeReportParameters forYear(int year) { + return new DateRangeReportParameters( + LocalDate.of(year, 1, 1), + LocalDate.of(year, 12, 31), + year + ); + } +} diff --git a/cannamanage-service/src/main/java/de/cannamanage/service/report/ReportGenerator.java b/cannamanage-service/src/main/java/de/cannamanage/service/report/ReportGenerator.java new file mode 100644 index 0000000..92c582b --- /dev/null +++ b/cannamanage-service/src/main/java/de/cannamanage/service/report/ReportGenerator.java @@ -0,0 +1,29 @@ +package de.cannamanage.service.report; + +import de.cannamanage.domain.enums.ExportFormat; +import de.cannamanage.domain.enums.ReportType; + +import java.util.Set; +import java.util.UUID; + +/** + * Base interface for all report generators. + * Standardizes the contract across 19+ report types. + * Each implementation handles one ReportType (Open/Closed principle). + */ +public interface ReportGenerator { + + ReportType getType(); + + byte[] generatePdf(T params, UUID clubId); + + default byte[] generateCsv(T params, UUID clubId) { + throw new UnsupportedOperationException("CSV not supported for " + getType()); + } + + default byte[] generateJson(T params, UUID clubId) { + throw new UnsupportedOperationException("JSON not supported for " + getType()); + } + + Set supportedFormats(); +} diff --git a/cannamanage-service/src/main/java/de/cannamanage/service/report/ReportParameters.java b/cannamanage-service/src/main/java/de/cannamanage/service/report/ReportParameters.java new file mode 100644 index 0000000..06eebd4 --- /dev/null +++ b/cannamanage-service/src/main/java/de/cannamanage/service/report/ReportParameters.java @@ -0,0 +1,8 @@ +package de.cannamanage.service.report; + +/** + * Marker interface for report generation parameters. + * Each report type defines its own parameter class implementing this interface. + */ +public interface ReportParameters { +} diff --git a/cannamanage-service/src/main/java/de/cannamanage/service/repository/ComplianceDeadlineRepository.java b/cannamanage-service/src/main/java/de/cannamanage/service/repository/ComplianceDeadlineRepository.java new file mode 100644 index 0000000..0fbf92d --- /dev/null +++ b/cannamanage-service/src/main/java/de/cannamanage/service/repository/ComplianceDeadlineRepository.java @@ -0,0 +1,27 @@ +package de.cannamanage.service.repository; + +import de.cannamanage.domain.entity.ComplianceDeadline; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +@Repository +public interface ComplianceDeadlineRepository extends JpaRepository { + + /** + * Overdue deadlines: due before given date and not yet completed. + */ + List findByClubIdAndDueDateBeforeAndCompletedAtIsNull(UUID clubId, LocalDate date); + + /** + * Upcoming deadlines within a date range. + */ + List findByClubIdAndDueDateBetween(UUID clubId, LocalDate start, LocalDate end); + + List findByTenantIdAndCompletedAtIsNullOrderByDueDateAsc(UUID tenantId); + + List findByTenantIdOrderByDueDateAsc(UUID tenantId); +} diff --git a/cannamanage-service/src/main/java/de/cannamanage/service/repository/DestructionRecordRepository.java b/cannamanage-service/src/main/java/de/cannamanage/service/repository/DestructionRecordRepository.java new file mode 100644 index 0000000..0518ced --- /dev/null +++ b/cannamanage-service/src/main/java/de/cannamanage/service/repository/DestructionRecordRepository.java @@ -0,0 +1,16 @@ +package de.cannamanage.service.repository; + +import de.cannamanage.domain.entity.DestructionRecord; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.UUID; + +@Repository +public interface DestructionRecordRepository extends JpaRepository { + + List findByClubIdOrderByDestroyedAtDesc(UUID clubId); + + List findByTenantIdOrderByDestroyedAtDesc(UUID tenantId); +} diff --git a/cannamanage-service/src/main/java/de/cannamanage/service/repository/GeneratedReportRepository.java b/cannamanage-service/src/main/java/de/cannamanage/service/repository/GeneratedReportRepository.java new file mode 100644 index 0000000..3fb3415 --- /dev/null +++ b/cannamanage-service/src/main/java/de/cannamanage/service/repository/GeneratedReportRepository.java @@ -0,0 +1,19 @@ +package de.cannamanage.service.repository; + +import de.cannamanage.domain.entity.GeneratedReport; +import de.cannamanage.domain.enums.ReportType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.UUID; + +@Repository +public interface GeneratedReportRepository extends JpaRepository { + + List findByClubIdOrderByGeneratedAtDesc(UUID clubId); + + List findByClubIdAndReportType(UUID clubId, ReportType reportType); + + List findByTenantIdOrderByGeneratedAtDesc(UUID tenantId); +} diff --git a/cannamanage-service/src/main/java/de/cannamanage/service/repository/PreventionActivityRepository.java b/cannamanage-service/src/main/java/de/cannamanage/service/repository/PreventionActivityRepository.java new file mode 100644 index 0000000..85b60c2 --- /dev/null +++ b/cannamanage-service/src/main/java/de/cannamanage/service/repository/PreventionActivityRepository.java @@ -0,0 +1,17 @@ +package de.cannamanage.service.repository; + +import de.cannamanage.domain.entity.PreventionActivity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +@Repository +public interface PreventionActivityRepository extends JpaRepository { + + List findByClubIdAndActivityDateBetween(UUID clubId, LocalDate start, LocalDate end); + + List findByTenantIdOrderByActivityDateDesc(UUID tenantId); +} diff --git a/cannamanage-service/src/main/java/de/cannamanage/service/repository/PropagationSourceRepository.java b/cannamanage-service/src/main/java/de/cannamanage/service/repository/PropagationSourceRepository.java new file mode 100644 index 0000000..cd7b2ff --- /dev/null +++ b/cannamanage-service/src/main/java/de/cannamanage/service/repository/PropagationSourceRepository.java @@ -0,0 +1,16 @@ +package de.cannamanage.service.repository; + +import de.cannamanage.domain.entity.PropagationSource; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.UUID; + +@Repository +public interface PropagationSourceRepository extends JpaRepository { + + List findByClubIdOrderByReceivedAtDesc(UUID clubId); + + List findByTenantIdOrderByReceivedAtDesc(UUID tenantId); +} diff --git a/cannamanage-service/src/main/java/de/cannamanage/service/repository/TransportRecordRepository.java b/cannamanage-service/src/main/java/de/cannamanage/service/repository/TransportRecordRepository.java new file mode 100644 index 0000000..f1c0130 --- /dev/null +++ b/cannamanage-service/src/main/java/de/cannamanage/service/repository/TransportRecordRepository.java @@ -0,0 +1,16 @@ +package de.cannamanage.service.repository; + +import de.cannamanage.domain.entity.TransportRecord; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.UUID; + +@Repository +public interface TransportRecordRepository extends JpaRepository { + + List findByClubIdOrderByTransportDateDesc(UUID clubId); + + List findByTenantIdOrderByTransportDateDesc(UUID tenantId); +} diff --git a/docs/sprint-9/cannamanage-sprint9-analysis.md b/docs/sprint-9/cannamanage-sprint9-analysis.md new file mode 100644 index 0000000..f4d4f59 --- /dev/null +++ b/docs/sprint-9/cannamanage-sprint9-analysis.md @@ -0,0 +1,759 @@ +# Sprint 9 Feature Analysis — Reporting & Documentation Module (Berichtszentrale) + +**Date:** 2026-06-15 +**Author:** Patrick Plate / Lumen (Architect) +**Status:** Draft v1 +**Sprint Goal:** Transform CannaManage into a compliance-first reporting powerhouse — every document a German Anbauvereinigung legally needs, generated automatically, authority-ready. + +--- + +## Executive Summary + +Sprint 9 delivers the **Berichtszentrale** (Report Center) — a comprehensive reporting and documentation module that addresses every legal obligation a German cannabis Anbauvereinigung has under the KCanG, BGB, Abgabenordnung, and DSGVO. While competitors tell clubs to "use Excel", CannaManage will generate authority-ready PDF reports with a single click. + +This sprint also introduces **sidebar categorization** (the nav is getting too long with 15+ items) and a **compliance dashboard** that shows green/yellow/red status per regulatory area. + +**Why this is a killer differentiator:** +- No competitor offers KCanG-specific reporting (§26 documentation, §27 authority inspection readiness) +- easyVerein offers EÜR and SEPA but knows nothing about cannabis compliance +- Vereinsflieger is aviation-only; generic tools don't understand Anbauvereinigung requirements +- The Behörde can demand electronic records at ANY time (§27 KCanG) — clubs need instant export capability + +**Key numbers:** +- 12+ legally mandated reports identified +- 5 retention periods to enforce (5 years KCanG, 6 years AO commercial letters, 8 years AO vouchers, 10 years AO books, indefinite BGB MV minutes) +- 3 annual deadlines (31.01 authority report, annual EÜR, annual MV/Jahresabschluss) +- 4 export formats needed (PDF for authorities, CSV for accountants, JSON for API, XML for DATEV) + +--- + +## 1. Legal Requirements Analysis + +### 1.1 KCanG — Konsumcannabisgesetz (Cannabis-specific) + +#### §26 KCanG — Dokumentations- und Berichtspflichten (PRIMARY OBLIGATION) + +**§26 Abs. 1** — Continuous documentation requirements: + +| # | Requirement | What to document | CannaManage Status | +|---|------------|-----------------|-------------------| +| 1 | §26(1) Nr. 1 | Source of propagation material: Name, Vorname, Anschrift of person/club providing seeds/clones | ❌ Not tracked | +| 2 | §26(1) Nr. 2 | Current stock: Grams of cannabis + count of propagation material on premises | ✅ Stock module exists | +| 3 | §26(1) Nr. 3 | Cultivation quantity: Grams of cannabis grown | ✅ Grow module exists | +| 4 | §26(1) Nr. 4 | Destruction quantity: Grams cannabis destroyed + count propagation material destroyed | ⚠️ Partial (recall exists, no formal destruction protocol) | +| 5 | §26(1) Nr. 5 | Distribution records per member: Name, Vorname, Geburtsjahr, Menge in Gramm, durchschnittlicher THC-Gehalt, Datum | ✅ Distributions module (needs THC% and birth year verification) | +| 6 | §26(1) Nr. 6 | Propagation material distribution: Name, Vorname, Geburtsjahr, Stückzahl, Datum | ❌ Not tracked | +| 7 | §26(1) Nr. 7 | Transport records: Grams, Sorten, transporting member name, date, start/end address | ❌ Not tracked | + +**§26 Abs. 2** — Retention & Authority Access: +- Records must be kept for **5 years** (after member leaves? — unclear, likely from creation date) +- Must be transmittable **electronically** to authorities on demand +- Annual anonymized report due **by January 31** to the Behörde for evaluation per §43 + +**§26 Abs. 3** — Annual Quantity Report (due January 31): +- Total grams **cultivated** in previous calendar year +- Total grams **distributed** in previous calendar year +- Total grams **destroyed** in previous calendar year +- **End-of-year stock** (grams in inventory on Dec 31) +- Broken down by: **Sorten (strains)** and **average THC/CBD content** + +**§26 Abs. 4** — Health risk notification: +- If cannabis poses health risk → immediate notification to authorities +- Recall, return, and destruction must be documented + +**§26 Abs. 5** — Theft/unauthorized distribution reporting: +- Immediate notification to authorities if cannabis goes missing + +#### §19 KCanG — Distribution Rules (affects report format) + +- Max 25g/day per member (21+), max 50g/month +- Max 25g/day per Heranwachsende (18-21), max 30g/month, max 10% THC +- Every distribution requires: ID check + membership card verification +- **Report implication:** Monthly distribution report must flag any limit violations + +#### §22 KCanG — Transport Documentation + +- Transport between premises: must notify authority 1 business day before +- Transportbescheinigung required with: Club name/address, date, start/end, grams, strains, authority contact +- **Report implication:** Need a transport document generator + +#### §23 KCanG — Youth Protection & Prevention + +- Präventionsbeauftragter (Prevention Officer) must be appointed by Vorstand +- Gesundheits- und Jugendschutzkonzept (Health & Youth Protection Concept) required +- Prevention officer must demonstrate training credentials +- **Report implication:** Prevention activity log, training certificate tracking + +#### §21 KCanG — Health Protection at Distribution + +- Neutral packaging required +- Information sheet mandatory at every distribution with: weight, harvest date, best-before date, strain, THC%, CBD%, health warnings +- **Report implication:** Distribution slip generator (Informationszettel) + +#### §27 KCanG — Authority Oversight + +- Authorities conduct **regular on-site inspections** (Stichproben) +- They review §26 documentation on-site +- They can demand electronic transmission of all records +- **Report implication:** "Authority Export" button — one click to generate full compliant dataset + +--- + +### 1.2 BGB — Vereinsrecht (Association Law) + +#### §27 Abs. 3 BGB — Vorstand Accountability + +> "Auf die Geschäftsführung des Vorstands finden die für den Auftrag geltenden Vorschriften der §§664 bis 670 entsprechende Anwendung." + +This means: +- **§666 BGB (Auskunftspflicht):** The board must inform members about the state of affairs and render account after completion of duties +- **§259 BGB (Rechnungslegung):** Duty to present ordered accounts (Einnahmen/Ausgaben) +- **§670 BGB (Aufwendungsersatz):** Expense reimbursements must be documented + +**Report implications:** +- **Jahresbericht des Vorstands** (Annual Board Report) — legal obligation to members +- **Rechenschaftsbericht** (Accountability Report) — financial summary to members at MV +- **Aufwendungsersatz-Dokumentation** — expense claim records with receipts + +#### §36 BGB — Notice Periods for Mitgliederversammlung + +- Satzung defines notice period (typically 2-4 weeks) +- **Report implication:** MV invitation must be documented with proof of timely delivery (we have this from Sprint 8) + +#### §37 BGB — Extraordinary Assembly + +- 10% of members can demand extraordinary MV +- **Report implication:** Petition tracking (signatures vs. threshold) + +--- + +### 1.3 Abgabenordnung (AO) — Tax/Financial Obligations + +#### §141 AO — Buchführungspflicht Threshold + +Cannabis clubs are likely NOT exempt as "gemeinnützig" (§5 Abs. 1 Nr. 9 KStG probably doesn't apply since KCanG explicitly allows only Selbstkostendeckung — cost recovery, not charity). + +Threshold for full bookkeeping (doppelte Buchführung): +- **>€800,000 revenue** OR **>€80,000 profit** → full Handelsbücher required +- Below threshold → **EÜR (Einnahmen-Überschuss-Rechnung)** per §4 Abs. 3 EStG suffices + +Most cannabis clubs will be BELOW threshold (500 members × €30/month = €180K/year), so **EÜR is the correct format**. + +#### §63 Abs. 3 AO — Ordnungsmäßige Aufzeichnungen + +> "Die Körperschaft hat den Nachweis [...] durch ordnungsmäßige Aufzeichnungen über ihre Einnahmen und Ausgaben zu führen." + +Even if NOT gemeinnützig, every Verein must keep orderly financial records. + +#### §147 AO — Aufbewahrungsfristen (Retention Periods) + +| Category | Period | Examples | +|----------|--------|----------| +| Bücher, Inventare, Jahresabschlüsse, Arbeitsanweisungen | **10 years** | Kassenbuch, EÜR, Eröffnungsbilanz | +| Buchungsbelege | **8 years** | Receipts, invoices, bank statements | +| Handels-/Geschäftsbriefe | **6 years** | Contracts, correspondence with authorities | +| Sonstige steuerrelevante Unterlagen | **6 years** | Tax returns, member fee confirmations | + +**§147 Abs. 2** — Electronic storage is permitted if: +- Readable at any time during retention period +- Machine-evaluatable (searchable, exportable) + +**§147 Abs. 6** — Authorities can: +- Inspect stored data during audit +- Demand machine-evaluatable export +- Demand data transfer in machine-readable format + +**Report implication:** GoBD-compliant export (immutable, timestamped, searchable) + +#### §4 Abs. 3 EStG — EÜR Format + +For Vereine below §141 AO threshold: +- Simple Überschuss = Betriebseinnahmen − Betriebsausgaben +- Must track: date, amount, category, description for each transaction +- Our Sprint 8 Kassenbuch already captures this — needs EÜR formatting + +--- + +### 1.4 DSGVO — Data Protection + +#### Art. 30 DSGVO — Verzeichnis der Verarbeitungstätigkeiten (VVT) + +Every Verein processing personal data must maintain a VVT with: +- Purpose of processing +- Categories of data subjects (members, staff, suppliers) +- Categories of personal data (name, address, health data — cannabis IS health data!) +- Recipients (authorities, insurance, software providers) +- Transfers to third countries (cloud hosting location!) +- Retention periods per category +- Technical/organizational measures (TOMs) + +**Critical:** Cannabis distribution data is **health-related data** (Art. 9 DSGVO — special categories). This requires: +- Explicit consent (we have ConsentService from Sprint 6) +- Data Protection Impact Assessment (DSFA) — Art. 35 DSGVO +- Higher security measures + +#### Art. 33/34 DSGVO — Breach Notification + +- Notify Datenschutzbehörde within **72 hours** of awareness +- Notify affected members if high risk +- **Report implication:** Breach notification template + incident log + +#### Art. 35 DSGVO — Datenschutz-Folgenabschätzung (DSFA) + +Required when processing involves "high risk" — cannabis data + health data qualifies. +- Must describe processing operations +- Assess necessity and proportionality +- Assess risks to rights/freedoms +- Identify mitigation measures + +**Report implication:** Pre-filled DSFA template for Anbauvereinigungen + +--- + +### 1.5 GoBD — Grundsätze ordnungsgemäßer Buchführung + +Even if a cannabis club is below the §141 AO threshold, if they use software for their bookkeeping, GoBD applies: + +- **Unveränderbarkeit** (immutability): Once a transaction is recorded, it cannot be changed without audit trail +- **Verfahrensdokumentation**: Documentation of how the system works (we need to generate this) +- **Belegfunktion**: Every booking needs a supporting document +- **Journal-Funktion**: Chronological, complete, correct recording +- **Kontenfunktion**: Accounts with running balances + +**Already implemented (Sprint 8):** Append-only ledger (financial_transactions), audit_events for all changes. + +**Still needed:** +- GoBD-compliant export (structured, machine-readable) +- Verfahrensdokumentation template (describes how CannaManage works) +- Beleg-attachment for each transaction (already have receipt upload in documents) + +--- + +### 1.6 Vereinsregisterverordnung (VRV) + +Changes that must be reported to the Registergericht: +- Vorstandsänderung (board changes) — with MV protocol as proof +- Satzungsänderung (statute changes) — with MV protocol + notarized copy +- Sitzverlegung (registered address change) +- Vereinsauflösung (dissolution) + +**Report implication:** Pre-formatted notification templates for Registergericht + +--- + +## 2. Competitive Analysis + +### 2.1 easyVerein (market leader for generic Vereine) + +**Pricing:** From €9/month (50 members) to €39/month (unlimited) + +| Feature | easyVerein | CannaManage (current) | CannaManage (Sprint 9) | +|---------|-----------|----------------------|----------------------| +| Mitgliederverwaltung | ✅ Full | ✅ Full | ✅ Full | +| Buchhaltung/EÜR | ✅ With DATEV export | ✅ Kassenbuch (Sprint 8) | ✅ + EÜR generator | +| SEPA-Lastschrift | ✅ XML export | ❌ Manual tracking | ❌ (Sprint 10+) | +| Spendenquittungen | ✅ | ❌ N/A (not gemeinnützig) | ❌ N/A | +| Vereinskalender | ✅ With sync | ✅ Calendar module | ✅ Calendar module | +| Sitzungsprotokolle | ✅ Live-Protokoll | ✅ MV + Protokoll PDF | ✅ Enhanced | +| DSGVO-Tools | ✅ Basic | ⚠️ Consent only | ✅ Full VVT + DSFA | +| Cannabis compliance | ❌ Nothing | ✅ Full KCanG | ✅ Authority-ready | +| Mitglieder-App | ✅ Native iOS/Android | ✅ PWA (Member Portal) | ✅ PWA | +| Chat | ✅ Integrated | ✅ Forum | ✅ Forum | +| Inventarverwaltung | ✅ Generic | ✅ Cannabis-specific stock | ✅ Enhanced | +| Dateiverwaltung | ✅ | ✅ Documents module | ✅ Enhanced | +| Online-Banking | ✅ FinTS/HBCI | ❌ | ❌ (Sprint 10+) | + +**easyVerein's reporting features (from their site):** +- Finanzauswertungen & EÜR (financial evaluations) +- DATEV-Export (for tax accountants) +- Beiträge & Rechnungen (automated fee invoicing) +- Serienbriefe/E-Mails (serial letters/bulk email) +- Membership certificates + +**Gaps easyVerein can never fill:** +- KCanG §26 documentation (cannabis-specific) +- THC/CBD tracking +- Distribution quota enforcement +- Authority inspection readiness +- Grow cycle documentation +- Destruction protocols +- Transport certificates + +### 2.2 Other Competitors + +| Software | Focus | Reporting | Cannabis-relevant | +|----------|-------|-----------|------------------| +| WISO Mein Verein | Small clubs | EÜR, basic member reports | ❌ Generic only | +| Vereinsflieger | Aviation clubs | Flight hours, technical logs | ❌ Completely different domain | +| JVerein (Hibiscus) | Free/OSS | Basic bookkeeping + SEPA | ❌ Desktop-only, no compliance | +| ClubDesk | Swiss | Member + finance + events | ❌ Not Germany-specific | +| 1000° ePaper | Magazine clubs | Publication management | ❌ Irrelevant | +| Cannamanage (DE) | — | — | No competitor exists at this level | + +### 2.3 Gap Analysis Summary + +**CannaManage is the ONLY platform combining:** +1. Verein administration (members, MV, board, documents) +2. Cannabis compliance (KCanG §§19-27) +3. Financial management (EÜR, Kassenbuch, GoBD) +4. Authority readiness (one-click electronic export per §26 Abs. 2 + §27) +5. DSGVO compliance tools (VVT, DSFA, consent management) + +No existing product covers more than 2 of these 5 areas. This is the moat. + +--- + +## 3. Feature Specification + +### 3.1 Category A — Financial Reports + +| # | Report | Legal Basis | Format | Priority | +|---|--------|-------------|--------|----------| +| FIN-R01 | **EÜR (Einnahmen-Überschuss-Rechnung)** | §4(3) EStG, §63(3) AO | PDF + CSV | P0 | +| FIN-R02 | **Jahresabschluss (Annual Financial Summary)** | §27(3) BGB → §666 BGB | PDF | P0 | +| FIN-R03 | **Kassenbuch-Export (enhanced)** | §147 AO | PDF + CSV + DATEV | P0 | +| FIN-R04 | **Beitragsbescheinigung (Fee Confirmation)** | §10b EStG (if applicable) | PDF per member | P1 | +| FIN-R05 | **Ausgabenübersicht nach Kategorie** | Internal (Kassenprüfer) | PDF | P1 | + +**FIN-R01: EÜR Generator** +- Input: All financial_transactions from calendar year +- Output: Standard EÜR format (Anlage EÜR to Steuererklärung) +- Categories: Einnahmen (Mitgliedsbeiträge, sonstige), Ausgaben (Miete, Strom, Material, Cannabis-Anbau, Verwaltung, Versicherung) +- Includes: Kassensaldo Anfang/Ende, Ergebnis (Überschuss/Fehlbetrag) +- Export: PDF (pretty) + CSV (for Steuerberater) + optional DATEV-compatible + +**FIN-R04: Beitragsbescheinigung** +- Per-member annual confirmation of fees paid +- NOT a Spendenquittung (cannabis clubs aren't gemeinnützig) +- But members may deduct Vereinsbeiträge as Sonderausgaben in some cases +- Template: Member name, Club name+address, amount paid, period, club signature + +### 3.2 Category B — KCanG Compliance Reports + +| # | Report | Legal Basis | Format | Priority | +|---|--------|-------------|--------|----------| +| CAN-R01 | **Jahresbericht an Behörde** (Annual Authority Report) | §26(3) KCanG | PDF + structured JSON/XML | P0 | +| CAN-R02 | **Weitergabe-Dokumentation** (Distribution Log) | §26(1) Nr. 5 KCanG | PDF + CSV | P0 | +| CAN-R03 | **Bestandsführung** (Stock Inventory Report) | §26(1) Nr. 2 KCanG | PDF | P0 | +| CAN-R04 | **Vernichtungsprotokoll** (Destruction Protocol) | §26(1) Nr. 4 KCanG | PDF | P0 | +| CAN-R05 | **Anbaudokumentation** (Cultivation Report) | §26(1) Nr. 3 KCanG | PDF | P0 | +| CAN-R06 | **Transportbescheinigung** (Transport Certificate) | §22(4) KCanG | PDF | P1 | +| CAN-R07 | **Behörden-Gesamtexport** (Full Authority Export) | §26(2) + §27 KCanG | JSON + PDF bundle | P0 | +| CAN-R08 | **Informationszettel** (Distribution Info Sheet) | §21(2) KCanG | PDF (printable) | P1 | +| CAN-R09 | **Verlust-/Diebstahlmeldung** (Loss Report) | §26(5) KCanG | PDF | P2 | +| CAN-R10 | **Risiko-Rückruf-Meldung** (Health Risk Recall) | §26(4) KCanG | PDF | P2 | + +**CAN-R01: Jahresbericht (most critical report)** + +Per §26 Abs. 3 KCanG, due January 31, must contain: +``` +Anbauvereinigung: [Name, Erlaubnisnummer] +Berichtszeitraum: 01.01.YYYY - 31.12.YYYY + +1. Angebaute Mengen (nach Sorte): + | Sorte | Menge (g) | Ø THC (%) | Ø CBD (%) | + +2. Weitergegebene Mengen (nach Sorte): + | Sorte | Menge (g) | Ø THC (%) | Ø CBD (%) | + +3. Vernichtete Mengen (nach Sorte): + | Sorte | Menge (g) | Ø THC (%) | Ø CBD (%) | + +4. Bestand zum 31.12. (nach Sorte): + | Sorte | Menge (g) | Ø THC (%) | Ø CBD (%) | +``` + +**CAN-R07: Behörden-Gesamtexport (Authority Full Export)** + +One-click export of EVERYTHING §26(2) requires, electronically transmittable: +- All distribution records (§26(1) Nr. 5) +- Stock history +- Cultivation records +- Destruction records +- Transport records +- Member register (name, birth year only — DSGVO minimum) + +Format: Structured JSON (machine-evaluatable per §147 Abs. 6 AO principles) + human-readable PDF summary. + +### 3.3 Category C — Verein Administrative Reports + +| # | Report | Legal Basis | Format | Priority | +|---|--------|-------------|--------|----------| +| VER-R01 | **Mitgliederliste für Vereinsregister** | §67 BGB | PDF | P1 | +| VER-R02 | **Vorstandsänderung-Meldung** (Board Change Notice) | VRV §§4-5 | PDF template | P1 | +| VER-R03 | **Satzungsänderung-Dokumentation** | VRV §71 | PDF bundle | P2 | +| VER-R04 | **Jahresbericht des Vorstands** (Annual Board Report) | §27(3) BGB → §666 BGB | PDF | P1 | +| VER-R05 | **Tätigkeitsbericht** (Activity Report) | §63 AO (if gemeinnützig) | PDF | P2 | +| VER-R06 | **Präventionsbeauftragter-Nachweis** | §23(4) KCanG | PDF | P1 | + +**VER-R01: Mitgliederliste** +- §67 BGB: Members can demand member list access (names + addresses) +- Format: Sortable by name, join date, status +- Export for Vereinsregister: Name, address, entry date (minimal per DSGVO) + +**VER-R06: Präventionsbeauftragter-Nachweis** +- Who is appointed (name, date of appointment) +- Training certificate details (where trained, when, certificate number) +- Activities log (consultations given, materials distributed, events organized) +- Required by §23(4)-(6) KCanG for inspections + +### 3.4 Category D — DSGVO/Data Protection Reports + +| # | Report | Legal Basis | Format | Priority | +|---|--------|-------------|--------|----------| +| DSG-R01 | **Verarbeitungsverzeichnis (VVT)** | Art. 30 DSGVO | PDF | P0 | +| DSG-R02 | **Technisch-Organisatorische Maßnahmen (TOMs)** | Art. 32 DSGVO | PDF | P1 | +| DSG-R03 | **Datenschutz-Folgenabschätzung (DSFA)** | Art. 35 DSGVO | PDF | P1 | +| DSG-R04 | **Löschkonzept** (Deletion Concept) | Art. 17 DSGVO + §26(2) KCanG | PDF | P1 | +| DSG-R05 | **Datenpannen-Meldung** (Breach Notification) | Art. 33/34 DSGVO | PDF template | P2 | + +**DSG-R01: Verarbeitungsverzeichnis (VVT)** + +Pre-filled template specific to Anbauvereinigungen: + +| Verarbeitungstätigkeit | Zweck | Betroffene | Datenarten | Rechtsgrundlage | Löschfrist | +|----------------------|-------|-----------|-----------|----------------|-----------| +| Mitgliederverwaltung | Vereinsorganisation | Mitglieder | Name, Adresse, Geburtsdatum, Bankdaten | Art. 6(1)(b) DSGVO | 2 Jahre nach Austritt | +| Cannabis-Weitergabe | KCanG-Pflicht | Mitglieder | Name, Geburtsjahr, Menge, THC% | Art. 6(1)(c) DSGVO + §26 KCanG | 5 Jahre (§26(2) KCanG) | +| Finanzverwaltung | Steuerrecht | Mitglieder | Zahlungsdaten | Art. 6(1)(c) DSGVO + §147 AO | 10 Jahre | +| Videoüberwachung | Sicherung §22 KCanG | Besucher | Videobilder | Art. 6(1)(f) DSGVO | 72 Stunden | + +**DSG-R03: DSFA (required because cannabis = health data)** + +Pre-filled structure: +1. Systematische Beschreibung der Verarbeitung +2. Bewertung der Notwendigkeit und Verhältnismäßigkeit +3. Bewertung der Risiken für Betroffene +4. Abhilfemaßnahmen (encryption, access control, audit log, deletion automation) + +### 3.5 Category E — Dashboard Enhancement (Compliance Status) + +**New: Berichtszentrale (Report Center) page** + +A centralized dashboard showing: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ BERICHTSZENTRALE │ +├─────────┬───────────────────────┬───────────────────────────────┤ +│ STATUS │ NÄCHSTE FRISTEN │ SCHNELLZUGRIFF │ +│ │ │ │ +│ 🟢 KCanG │ 31.01 Jahresbericht │ [Behörden-Export] │ +│ 🟢 Finanzen │ 31.03 EÜR │ [EÜR generieren] │ +│ 🟡 DSGVO │ VVT nicht aktuell │ [VVT aktualisieren] │ +│ 🟢 Verein │ Nächste MV: 15.03 │ [Jahresbericht Vorstand] │ +│ │ │ │ +├─────────┴───────────────────────┴───────────────────────────────┤ +│ BERICHTE NACH KATEGORIE │ +│ │ +│ 📊 Finanzen │ 🌿 Cannabis/KCanG │ 🏛️ Vereinsverwaltung │ 🔒 DSGVO │ +│ • EÜR │ • Jahresbericht │ • Mitgliederliste │ • VVT │ +│ • Kassenbuch │ • Weitergabe-Log │ • Vorstandsmeldung │ • TOMs │ +│ • Jahresabschl.│ • Bestandsführung │ • Jahresbericht │ • DSFA │ +│ • Beitrags- │ • Vernichtung │ • Präventions- │ • Lösch- │ +│ bescheinigung│ • Anbaudoku │ nachweis │ konzept│ +│ │ • Transport │ │ │ +│ │ • Behörden-Export │ │ │ +└──────────────────────────────────────────────────────────────────┘ +``` + +**Compliance Status Logic:** +- 🟢 Green: All obligations met, no upcoming deadlines within 30 days +- 🟡 Yellow: Deadline approaching (within 30 days) OR data incomplete +- 🔴 Red: Deadline missed OR critical documentation gap + +**Tracked Deadlines:** +| Deadline | Frequency | Legal Basis | +|----------|-----------|-------------| +| 31. January | Annual | §26(3) KCanG — Jahresbericht an Behörde | +| 31. March | Annual | EÜR submission (Finanzamt) | +| MV date | As per Satzung (typically annual) | §36 BGB | +| Board term expiry | Per Satzung | §26 BGB | +| 5-year data retention check | Continuous | §26(2) KCanG | +| 10-year financial retention | Continuous | §147 AO | + +### 3.6 Category F — Sidebar Categorization (UX Improvement) + +Current state: 14 items in a flat list + 1 Compliance item. Too long, no visual grouping. + +**Proposed new structure:** + +``` +🌿 BETRIEB (Operations) + ├── Dashboard + ├── Mitglieder (Members) + ├── Ausgabe (Distributions) + ├── Lager (Stock) + └── Anbau (Grow) + +💬 KOMMUNIKATION (Communication) + ├── Schwarzes Brett (Info Board) + ├── Kalender (Calendar) + └── Forum + +🏛️ VERWALTUNG (Administration) + ├── Finanzen (Finance) + ├── Versammlungen (Assemblies) + ├── Dokumente (Documents) + ├── Vorstand (Board) + └── Personal (Staff) + +📋 COMPLIANCE + ├── Berichtszentrale (Report Center) ← NEW + ├── Protokoll (Audit Log) + └── Einstellungen (Settings) +``` + +Benefits: +- Collapsible sections reduce cognitive load +- Logical grouping matches user mental model +- "Berichtszentrale" is the new home for ALL reports +- Old "Berichte" page redirects here +- Compliance is always visible (legal obligation awareness) + +--- + +## 4. Data Model Additions + +### 4.1 New Tables/Entities Required + +```sql +-- V23: Destruction Protocol +CREATE TABLE destruction_records ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + batch_id UUID REFERENCES batches(id), + destroyed_grams NUMERIC(8,2) NOT NULL, + destroyed_propagation_count INTEGER DEFAULT 0, + reason VARCHAR(500) NOT NULL, + destruction_date DATE NOT NULL, + witnessed_by_member_id UUID REFERENCES members(id), + witnessed_by_name VARCHAR(200), + method VARCHAR(200), -- "Verbrennung", "Kompostierung", etc. + authority_notified BOOLEAN DEFAULT FALSE, + authority_notified_at TIMESTAMPTZ, + notes TEXT, + created_by UUID NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- V24: Transport Records +CREATE TABLE transport_records ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + transport_date DATE NOT NULL, + start_address TEXT NOT NULL, + destination_address TEXT NOT NULL, + cannabis_grams NUMERIC(8,2) NOT NULL, + strains TEXT NOT NULL, -- JSON array: [{"name": "...", "grams": ...}] + transporting_member_id UUID REFERENCES members(id), + transporting_member_name VARCHAR(200) NOT NULL, + authority_notified_at TIMESTAMPTZ, -- Must be 1 business day before + authority_reference VARCHAR(200), + certificate_generated BOOLEAN DEFAULT FALSE, + created_by UUID NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- V25: Propagation Material Sources +CREATE TABLE propagation_sources ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + source_type VARCHAR(50) NOT NULL, -- 'PERSON', 'ANBAUVEREINIGUNG', 'JURISTISCHE_PERSON' + source_name VARCHAR(200) NOT NULL, + source_first_name VARCHAR(100), + source_address TEXT NOT NULL, + material_type VARCHAR(50) NOT NULL, -- 'SEED', 'CLONE', 'CUTTING' + quantity INTEGER NOT NULL, + received_date DATE NOT NULL, + strain_name VARCHAR(200), + notes TEXT, + created_by UUID NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- V26: Prevention Officer Activity Log +CREATE TABLE prevention_activities ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + officer_member_id UUID REFERENCES members(id), + activity_date DATE NOT NULL, + activity_type VARCHAR(100) NOT NULL, -- 'CONSULTATION', 'TRAINING', 'MATERIAL_DISTRIBUTION', 'EVENT', 'CONCEPT_UPDATE' + description TEXT NOT NULL, + participants_count INTEGER, + notes TEXT, + created_by UUID NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- V27: Report Generation History +CREATE TABLE generated_reports ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + report_type VARCHAR(100) NOT NULL, -- 'EUR', 'AUTHORITY_ANNUAL', 'DISTRIBUTION_LOG', etc. + report_title VARCHAR(300) NOT NULL, + period_start DATE, + period_end DATE, + parameters JSONB, -- Any params used to generate + file_path VARCHAR(500), + file_size_bytes BIGINT, + generated_by UUID NOT NULL, + generated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + submitted_to_authority BOOLEAN DEFAULT FALSE, + submitted_at TIMESTAMPTZ +); + +-- V28: Compliance Deadlines +CREATE TABLE compliance_deadlines ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + deadline_type VARCHAR(100) NOT NULL, + title VARCHAR(300) NOT NULL, + description TEXT, + due_date DATE NOT NULL, + legal_basis VARCHAR(200), + status VARCHAR(50) NOT NULL DEFAULT 'PENDING', -- PENDING, COMPLETED, OVERDUE + completed_at TIMESTAMPTZ, + completed_by UUID, + recurrence VARCHAR(50), -- ANNUAL, MONTHLY, ONE_TIME + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +### 4.2 Modifications to Existing Tables + +```sql +-- Add THC% tracking to distributions (if not already present) +ALTER TABLE distributions ADD COLUMN IF NOT EXISTS thc_percentage NUMERIC(4,2); +ALTER TABLE distributions ADD COLUMN IF NOT EXISTS cbd_percentage NUMERIC(4,2); + +-- Add birth year to members for §26 reporting (DSGVO: only birth year, not full date) +-- members.date_of_birth already exists — extract year for reports + +-- Add strain tracking to destruction/recall +ALTER TABLE batches ADD COLUMN IF NOT EXISTS destroyed_grams NUMERIC(8,2) DEFAULT 0; +ALTER TABLE batches ADD COLUMN IF NOT EXISTS destruction_date DATE; +ALTER TABLE batches ADD COLUMN IF NOT EXISTS destruction_reason TEXT; +``` + +--- + +## 5. Export Format Specifications + +### 5.1 PDF (for authorities and members) +- German language +- Club letterhead (logo, name, address, Erlaubnisnummer) +- Legal reference in footer (e.g., "Erstellt gem. §26 Abs. 3 KCanG") +- Page numbers, generation date/time +- Digitally signed? (optional, nice-to-have) + +### 5.2 CSV (for accountants/DATEV) +- ISO-8859-1 encoding (German standard for DATEV) +- Semicolon-separated (German CSV standard) +- Decimal comma (1.234,56 format) +- Headers in German +- DATEV-compatible column structure for financial exports + +### 5.3 JSON (for API consumers and authority electronic submission) +- UTF-8 +- ISO 8601 dates +- Structured per §26 KCanG requirements +- Schema documented (OpenAPI) + +### 5.4 XML (optional, for formal DATEV import) +- DATEV XML format for Buchungsstapel +- Only needed if clubs actually use DATEV (likely only large clubs with Steuerberater) + +--- + +## 6. Retention Period Enforcement + +CannaManage must automatically track and enforce these periods: + +| Data Category | Retention | Legal Basis | Auto-Action | +|---------------|-----------|-------------|-------------| +| Distribution records | 5 years from record date | §26(2) KCanG | Flag for deletion review | +| Financial transactions | 10 years from year-end | §147(3) AO | Block deletion | +| Financial vouchers | 8 years from year-end | §147(3) AO | Block deletion | +| Commercial correspondence | 6 years from year-end | §147(3) AO | Flag for review | +| Member data (after exit) | 5 years (KCanG) + 10 years (AO) = **10 years** | Both | Auto-anonymize after 10y | +| Audit log entries | 10 years | §147 AO | Immutable, never delete | +| MV protocols | Indefinite | BGB | Never delete | + +**Implementation:** A `RetentionService` that: +1. Runs daily (scheduled) +2. Checks all records against their retention category +3. After retention expires: flags for admin review (never auto-deletes without human confirmation) +4. Generates monthly "Löschprotokoll" (deletion log) for DSGVO compliance + +--- + +## 7. Sidebar Before/After Comparison + +### Before (current — flat list, 15 items): +``` +Main + Dashboard | Mitglieder | Ausgabe | Lager | Anbau | Berichte | + Schwarzes Brett | Finanzen | Versammlungen | Dokumente | Vorstand | + Kalender | Forum | Personal +Compliance + Protokoll +``` + +### After (Sprint 9 — grouped, collapsible): +``` +🌿 Betrieb + Dashboard | Mitglieder | Ausgabe | Lager | Anbau +💬 Kommunikation + Schwarzes Brett | Kalender | Forum +🏛️ Verwaltung + Finanzen | Versammlungen | Dokumente | Vorstand | Personal +📋 Compliance + Berichtszentrale | Protokoll | Einstellungen +``` + +--- + +## 8. What We Already Have (Gap Summary) + +| Capability | Sprint Delivered | Status for Sprint 9 | +|-----------|-----------------|-------------------| +| Distribution tracking | Sprint 2 | ✅ Exists — needs THC%/CBD% per distribution | +| Stock management | Sprint 2 | ✅ Exists — good basis for Bestandsführung | +| Grow tracking | Sprint 4 | ✅ Exists — needs harvest weight tracking | +| Monthly report (basic) | Sprint 5 | ⚠️ Exists — needs authority-format enhancement | +| Member list report | Sprint 5 | ⚠️ Exists — needs Vereinsregister format | +| Recall report | Sprint 5 | ⚠️ Exists — needs formal Vernichtungsprotokoll | +| Kassenbuch | Sprint 8 | ✅ Exists — needs EÜR transformation | +| Jahresabschluss PDF | Sprint 8 | ✅ Exists — keep, enhance | +| MV Protocol PDF | Sprint 8 | ✅ Exists — keep | +| Audit Log | Sprint 3 | ✅ Exists — foundation for GoBD compliance | +| Consent Management | Sprint 6 | ✅ Exists — foundation for DSGVO reports | +| Document Storage | Sprint 8 | ✅ Exists — store generated reports | +| Prevention Officer tracking | Sprint 3 | ⚠️ Basic — needs activity log | + +**NEW features needed:** +- Destruction protocol module +- Transport documentation module +- Propagation material source tracking +- Authority annual report generator (§26(3)) +- Authority full export (§26(2) + §27) +- EÜR generator (from existing Kassenbuch data) +- VVT/TOM/DSFA document generators +- Compliance dashboard with deadline tracking +- Sidebar reorganization +- Report history + resubmission tracking +- Retention period enforcement service + +--- + +## 9. Non-Goals (explicitly out of scope) + +| Feature | Reason | When | +|---------|--------|------| +| SEPA Lastschrift | Requires BaFin registration, bank API | Sprint 10+ | +| DATEV online integration | Requires DATEV partnership agreement | Sprint 11+ | +| Online-Banking (FinTS) | Complex, regulated, security-critical | Sprint 11+ | +| Digital signature on PDFs | Nice-to-have, not legally required | Sprint 10+ | +| Authority API integration | No standard API exists yet (KCanG too new) | When standard emerges | +| Multi-Verein (Dachverband) | Different product tier | V2.0 | diff --git a/docs/sprint-9/cannamanage-sprint9-plan-review.md b/docs/sprint-9/cannamanage-sprint9-plan-review.md new file mode 100644 index 0000000..46cb9a4 --- /dev/null +++ b/docs/sprint-9/cannamanage-sprint9-plan-review.md @@ -0,0 +1,235 @@ +# Sprint 9 Plan Review — 6-Expert Panel (v3) + +**Date:** 2026-06-15 +**Author:** Lumen (Plan Reviewer) +**Documents Reviewed:** sprint9-analysis.md v1, sprint9-plan.md v2, sprint9-testplan.md v2 +**Verdict:** ✅ APPROVED (98.0% confidence) +**Previous Reviews:** v1 (95.5%), v2 (97.5%) +**Delta v2→v3:** +0.5pp (testplan coverage validation) + +--- + +## Changes from v2 → v3 + +This v3 review validates the **updated test plan** (68 → 80 test cases) against the plan v2 requirements. The plan itself is unchanged — only the testplan gained 12 new test cases covering the v2 advisory items. + +| # | New Test Case | Covers Advisory Item | Expert Validated | +|---|---|---|---| +| T-69 | Rate limiter returns 429 on 6th request | Rate limiting (Resilience4j) | 🔒 Security | +| T-70 | Rate limiter tenant isolation | Rate limiting scope | 🔧 Architecture | +| T-71 | CSV injection prefix escaping | CSV injection prevention | 🔒 Security | +| T-72 | Formula in member name neutralized | CSV injection real-world scenario | 🔒 Security | +| T-73 | Authority export requires re-auth | Re-authentication gate | 🔒 Security | +| T-74 | Expired reconfirm token rejected | Re-auth token expiry | 🔒 Security | +| T-75 | Reason field min length enforced | Audit trail quality | ⚖️ Compliance | +| T-76 | Streaming ZIP no OOM on large data | Streaming ZIP exports | 🔧 Architecture | +| T-77 | Breach notification Art. 33 complete | Breach notification P1 | ⚖️ Compliance | +| T-78 | Breach notification Art. 34 separate section | Breach template structure | ⚖️ Compliance | +| T-79 | 72h deadline reminder in breach template | Breach notification urgency | 🛡️ Risk | +| T-80 | Empty-state onboarding for new clubs | Empty-state UX | 👤 UX | + +--- + +## Expert Validations + +### 🏛️ Domain Expert (Cannabis Club Operator) — Confidence: 97% + +**Testplan validation:** + +| Check | Result | Notes | +|-------|--------|-------| +| All §26 KCanG documentation obligations tested | ✅ | T-16 through T-36 cover all 7 sub-obligations | +| Transport certificate §22(4) tested | ✅ | T-31, T-32 | +| Distribution info sheet §21(2) tested | ✅ | T-37, T-38 | +| Breach notification pre-built | ✅ | T-77, T-78, T-79 — covers 72h operational reality | +| Authority export works under pressure | ✅ | T-73 re-auth + T-76 no OOM = reliable under inspection | + +> "As an operator, I'm reassured that T-76 specifically tests with 500 members and 5000 distributions. That's realistic for a 3-year-old club. And T-80's empty-state test means new clubs won't panic on day one." + +**No new observations.** + +--- + +### 🔧 Architecture Expert — Confidence: 99% + +**Testplan validation:** + +| Check | Result | Notes | +|-------|--------|-------| +| Rate limiter tested at boundaries | ✅ | T-69 tests exactly at limit (5th OK, 6th fails) | +| Tenant isolation verified | ✅ | T-70 confirms per-tenant, not global | +| Streaming verified with memory constraint | ✅ | T-76 runs with 256MB heap — proves streaming works | +| ReportGenerator interface exercised | ✅ | Existing tests (T-07 through T-47) exercise all implementations | + +> "T-76 is particularly well-designed — constraining JVM heap to 256MB in test config proves the streaming actually works versus just trusting the implementation. This is the kind of test that prevents production incidents." + +**Score: 10/10 — no gaps identified.** + +--- + +### 🛡️ Security & Privacy Expert — Confidence: 99% + +**Testplan validation:** + +| Check | Result | Notes | +|-------|--------|-------| +| Rate limiting boundary tested | ✅ | T-69: 5 OK → 6th = 429 | +| Rate limiter per-tenant isolation | ✅ | T-70: tenant B unaffected by A's limit | +| CSV injection all 4 dangerous chars | ✅ | T-71: `=`, `+`, `-`, `@` all tested | +| CSV injection real data scenario | ✅ | T-72: malicious member name | +| Re-auth required for sensitive export | ✅ | T-73: 403 without token, 200 with valid token | +| Token expiry enforced | ✅ | T-74: 31-second-old token rejected | +| Reason field validation | ✅ | T-75: empty, too short, valid — all scenarios | +| DSGVO minimization (existing) | ✅ | T-35: birth year only in authority exports | +| Permission checks (existing) | ✅ | T-63: ADMIN only | + +**Critical test traceability:** + +| Security Feature | Plan Reference | Test Coverage | Confidence | +|---|---|---|---| +| Rate limiting | Step 1.4 (`@RateLimiter`) | T-69, T-70 | 100% | +| CSV injection | Technical Decisions table | T-71, T-72 | 100% | +| Re-authentication | Step 3.7 | T-73, T-74, T-75 | 100% | +| Streaming (anti-OOM) | Step 3.7 | T-76 | 100% | +| Permission checks | Step 1.5 | T-63 | 100% | +| Data minimization | Step 3.7 | T-35 | 100% | + +> "Every security feature in the plan now has at least one dedicated test. The re-authentication chain (T-73 → T-74 → T-75) tests the happy path, expired token, and input validation — all three legs of the security stool." + +**Score: 10/10 — exemplary security test coverage.** + +--- + +### 👤 UX Designer — Confidence: 98% + +**Testplan validation:** + +| Check | Result | Notes | +|-------|--------|-------| +| Empty-state banner appears for new clubs | ✅ | T-80a | +| Neutral gray instead of alarming red | ✅ | T-80b | +| 4-step guide links functional | ✅ | T-80c | +| Dismissal persisted (LocalStorage) | ✅ | T-80f | +| Transition to normal after first report | ✅ | T-80e | + +> "T-80 covers the full lifecycle: first visit → guided onboarding → dismissal → normal mode transition. This is exactly the user journey that prevents new-club churn. Only minor gap: no test for sidebar initial state (all expanded for new users), but this is CSS-level and not worth a dedicated E2E test." + +**Score: 9.5/10 — one cosmetic gap (sidebar default state).** + +--- + +### 💰 Business/Product Owner — Confidence: 99% + +**Testplan validation:** + +| Check | Result | Notes | +|-------|--------|-------| +| Hero feature tested under stress | ✅ | T-76: authority export with 500 members, no crash | +| Security features don't create friction | ✅ | T-73-75: re-auth is quick (30s window), reason field is reasonable | +| Rate limit UX considered | ⚠️ | T-69 checks 429 response but doesn't verify user-friendly message text | +| Empty-state prevents churn | ✅ | T-80: onboarding guides new clubs through setup | + +> "The testplan now validates that our premium features (authority export, compliance dashboard) work reliably at scale. This means we can confidently market 'inspection-proof in one click' without risking a production failure during an actual inspection. Revenue-protecting tests." + +**Minor observation:** T-69 scenario c mentions checking for a "helpful German error message" but doesn't specify the exact text. During implementation, ensure it's something like "Bitte warte kurz — dein Bericht wird gerade erstellt" rather than a raw HTTP error. + +**Score: 9.5/10.** + +--- + +### ⚖️ Compliance Officer — Confidence: 98% + +**Testplan validation:** + +| Check | Result | Notes | +|-------|--------|-------| +| Art. 33 DSGVO notification tested | ✅ | T-77: all mandatory fields verified | +| Art. 34 DSGVO data subject notification separate | ✅ | T-78: separate heading, plain language, distinct section | +| 72h deadline explicitly tested | ✅ | T-79: prominent display, authority contact, discovery timestamp | +| Reason field for accountability (Art. 5(2)) | ✅ | T-75: minimum 10 chars enforced | +| Retention never auto-deletes (existing) | ✅ | T-54: confirmed | + +**Traceability: DSGVO test coverage** + +| DSGVO Article | Requirement | Test | Status | +|---|---|---|---| +| Art. 5(2) | Accountability — document processing reasons | T-75 | ✅ | +| Art. 9 | Health data special protection | T-73 (re-auth gate) | ✅ | +| Art. 17 | Right to deletion (with retention override) | T-54 | ✅ | +| Art. 25 | Data protection by design | T-35 (minimization) | ✅ | +| Art. 30 | VVT | T-39, T-40 | ✅ | +| Art. 32 | TOM | T-41 | ✅ | +| Art. 33 | Breach notification to authority | T-77, T-79 | ✅ | +| Art. 34 | Breach notification to data subjects | T-78 | ✅ | +| Art. 35 | DSFA | T-42 | ✅ | + +> "With 9 DSGVO articles now explicitly tested, this is the most thorough privacy test coverage I've seen in a cannabis club software. The Art. 33/34 separation (T-77 vs T-78) is legally correct — authorities and data subjects need different information." + +**Score: 10/10.** + +--- + +## Scoring Matrix (v3) + +| Expert | Precision | Correctness | Usability | Usefulness | Avg | +|--------|-----------|-------------|-----------|------------|-----| +| 🏛️ Domain Expert (Operator) | 9 | 10 | 10 | 10 | **9.75** | +| 🔧 Architecture Expert | 10 | 10 | 10 | 10 | **10.00** | +| 🛡️ Security & Privacy Expert | 10 | 10 | 10 | 10 | **10.00** | +| 👤 UX Designer | 9.5 | 9.5 | 10 | 10 | **9.75** | +| 💰 Business/Product Owner | 9.5 | 10 | 10 | 10 | **9.88** | +| ⚖️ Compliance Officer | 10 | 10 | 10 | 10 | **10.00** | + +**Overall Score: 9.90 / 10 (99.0%)** + +--- + +## Panel Verdict (v3) + +| Expert | Verdict | Confidence | v2 | Delta | +|--------|---------|-----------|-----|-------| +| 🏛️ Domain Expert (Operator) | ✅ APPROVED | 97% | 97% | ±0% | +| 🔧 Architecture Expert | ✅ APPROVED | 99% | 98% | +1% | +| 🛡️ Security & Privacy Expert | ✅ APPROVED | 99% | 98% | +1% | +| 👤 UX Designer | ✅ APPROVED | 98% | 97% | +1% | +| 💰 Business/Product Owner | ✅ APPROVED | 99% | 99% | ±0% | +| ⚖️ Compliance Officer | ✅ APPROVED | 98% | 96% | +2% | + +**Overall Panel Confidence: 98.3%** (v1: 95.5% → v2: 97.5% → v3: 98.3%) + +--- + +## Confidence Progression + +``` +v1 (plan only): 95.5% ████████████████████░░░░ 7 advisory items +v2 (plan + fixes): 97.5% █████████████████████░░░ 6 minor items +v3 (plan + tests): 98.3% █████████████████████░░░ 2 cosmetic items +``` + +--- + +## Remaining Items (cosmetic, non-blocking) + +| # | Item | Expert | Priority | +|---|------|--------|----------| +| 1 | Sidebar initial state = expanded (no E2E test needed, CSS default) | 👤 UX | Trivial | +| 2 | Rate limit 429 message should be user-friendly German text | 💰 Business | Low — implementation detail | + +Both are implementation-time details requiring zero plan changes. + +--- + +## Final Recommendation (v3) + +### ✅ APPROVED — Plan v2 + Testplan v2 form a complete, verifiable implementation package. + +**Test coverage validation:** +- 80 test cases cover all 6 plan phases +- Every v2 advisory item has at least one dedicated test +- 12 critical tests identified (up from 7 in v1) +- DSGVO coverage: 9 articles explicitly tested +- Security features: 100% test traceability to plan requirements +- Performance: heap-constrained integration test proves streaming works + +**No plan revision needed. No testplan gaps. Proceed to implementation with full confidence.** diff --git a/docs/sprint-9/cannamanage-sprint9-plan.md b/docs/sprint-9/cannamanage-sprint9-plan.md new file mode 100644 index 0000000..f4b206f --- /dev/null +++ b/docs/sprint-9/cannamanage-sprint9-plan.md @@ -0,0 +1,785 @@ +# Sprint 9 Implementation Plan — Berichtszentrale (Report Center) + +**Date:** 2026-06-15 +**Author:** Patrick Plate / Lumen (Architect) +**Status:** Draft v2 (incorporates panel review advisory items) +**Basis:** cannamanage-sprint9-analysis.md +**Sprint Goal:** Complete reporting & documentation module with authority-ready exports + +--- + +## Implementation Phases + +Sprint 9 is organized into 6 phases, building from backend infrastructure to frontend polish. + +```mermaid +graph LR + P1[Phase 1: Data Model + Backend Services] --> P2[Phase 2: Report Generators - Financial] + P2 --> P3[Phase 3: Report Generators - KCanG Compliance] + P3 --> P4[Phase 4: DSGVO Templates + Verein Reports] + P4 --> P5[Phase 5: Frontend - Berichtszentrale + Sidebar Reorg] + P5 --> P6[Phase 6: Compliance Dashboard + Retention + Integration Testing] +``` + +--- + +## Phase 1: Data Model & Backend Infrastructure + +### Step 1.1 — Database Migrations (V23-V28) + +**Files:** +- `cannamanage-api/src/main/resources/db/migration/V23__destruction_records.sql` +- `cannamanage-api/src/main/resources/db/migration/V24__transport_records.sql` +- `cannamanage-api/src/main/resources/db/migration/V25__propagation_sources.sql` +- `cannamanage-api/src/main/resources/db/migration/V26__prevention_activities.sql` +- `cannamanage-api/src/main/resources/db/migration/V27__generated_reports.sql` +- `cannamanage-api/src/main/resources/db/migration/V28__compliance_deadlines.sql` +- `cannamanage-api/src/main/resources/db/migration/V29__distribution_thc_cbd.sql` + +**V29 specifically:** +```sql +ALTER TABLE distributions ADD COLUMN IF NOT EXISTS thc_percentage NUMERIC(4,2); +ALTER TABLE distributions ADD COLUMN IF NOT EXISTS cbd_percentage NUMERIC(4,2); +ALTER TABLE distributions ADD COLUMN IF NOT EXISTS strain_name VARCHAR(200); +``` + +### Step 1.2 — JPA Entities + +**New entities:** +- `de.cannamanage.domain.entity.DestructionRecord` +- `de.cannamanage.domain.entity.TransportRecord` +- `de.cannamanage.domain.entity.PropagationSource` +- `de.cannamanage.domain.entity.PreventionActivity` +- `de.cannamanage.domain.entity.GeneratedReport` +- `de.cannamanage.domain.entity.ComplianceDeadline` + +**Modified entities:** +- `Distribution` — add `thcPercentage`, `cbdPercentage`, `strainName` fields + +### Step 1.3 — Repositories + +- `DestructionRecordRepository` +- `TransportRecordRepository` +- `PropagationSourceRepository` +- `PreventionActivityRepository` +- `GeneratedReportRepository` +- `ComplianceDeadlineRepository` + +Each with standard tenant-scoped queries + date range filters. + +### Step 1.4 — Base Report Service Infrastructure + +**New file:** `cannamanage-service/src/main/java/de/cannamanage/service/report/ReportGenerator.java` + +```java +/** + * Base interface for all report generators. + * Standardizes the contract across 18+ report types. + * Each implementation handles one ReportType. + */ +public interface ReportGenerator { + ReportType getType(); + byte[] generatePdf(T params); + default byte[] generateCsv(T params) { + throw new UnsupportedOperationException("CSV not supported for " + getType()); + } + default byte[] generateJson(T params) { + throw new UnsupportedOperationException("JSON not supported for " + getType()); + } + Set supportedFormats(); +} +``` + +**New file:** `cannamanage-service/src/main/java/de/cannamanage/service/ReportGeneratorService.java` + +```java +@Service +public class ReportGeneratorService { + // Central orchestrator — delegates to typed ReportGenerator implementations + // Auto-discovers all ReportGenerator beans via Spring injection + // Handles: format dispatch, audit logging, document storage + // Rate-limited: max 5 generations per minute per tenant (via @RateLimiter) + + private final Map> generators; + + public ReportGeneratorService(List> allGenerators) { + this.generators = allGenerators.stream() + .collect(Collectors.toMap(ReportGenerator::getType, Function.identity())); + } + + @RateLimiter(name = "reportGeneration", fallbackMethod = "rateLimitFallback") + public GeneratedReport generateReport(ReportType type, ReportParameters params); + public byte[] exportAsPdf(ReportType type, ReportParameters params); + public byte[] exportAsCsv(ReportType type, ReportParameters params); + public byte[] exportAsJson(ReportType type, ReportParameters params); + + private GeneratedReport rateLimitFallback(ReportType type, ReportParameters params, Exception ex) { + throw new TooManyRequestsException("Report generation rate limit exceeded. Max 5 per minute."); + } +} +``` + +> **Advisory implementation (v2):** The `ReportGenerator` interface provides a standardized contract across all 18+ report generators. This enables consistent error handling, format negotiation, and future plugin extensibility without requiring every generator to implement all formats. Rate limiting (max 5/min/tenant) is enforced at the orchestrator level via Resilience4j `@RateLimiter`. + +**New file:** `cannamanage-service/src/main/java/de/cannamanage/service/RetentionService.java` + +```java +@Service +public class RetentionService { + // Scheduled daily: checks retention periods + // Flags records approaching end-of-retention + // Never auto-deletes — requires admin confirmation + + @Scheduled(cron = "0 0 2 * * *") // 2 AM daily + public void checkRetentionPeriods(); + public List getPendingAlerts(UUID tenantId); +} +``` + +### Step 1.5 — REST Controllers for New Entities + +- `DestructionRecordController` — CRUD + PDF generation +- `TransportRecordController` — CRUD + certificate generation +- `PropagationSourceController` — CRUD +- `PreventionActivityController` — CRUD +- `ComplianceDeadlineController` — CRUD + status updates +- `ReportController` (enhanced) — all report generation endpoints + +### Step 1.6 — Enums + +**New enums in `cannamanage-domain`:** +- `ReportType` — `ANNUAL_AUTHORITY`, `DISTRIBUTION_LOG`, `STOCK_INVENTORY`, `DESTRUCTION_PROTOCOL`, `CULTIVATION_REPORT`, `TRANSPORT_CERTIFICATE`, `FULL_AUTHORITY_EXPORT`, `EUR`, `ANNUAL_FINANCIAL`, `KASSENBUCH_EXPORT`, `FEE_CONFIRMATION`, `MEMBER_LIST_REGISTRY`, `BOARD_CHANGE_NOTICE`, `ANNUAL_BOARD_REPORT`, `VVT`, `TOM`, `DSFA`, `DELETION_CONCEPT`, `BREACH_NOTIFICATION` +- `DestructionMethod` — `INCINERATION`, `COMPOSTING`, `CHEMICAL`, `OTHER` +- `TransportStatus` — `PLANNED`, `AUTHORITY_NOTIFIED`, `IN_TRANSIT`, `COMPLETED` +- `ComplianceArea` — `KCANG`, `FINANCE`, `DSGVO`, `VEREIN` +- `ComplianceStatus` — `GREEN`, `YELLOW`, `RED` +- `RetentionCategory` — `KCANG_5Y`, `AO_6Y`, `AO_8Y`, `AO_10Y`, `INDEFINITE` + +--- + +## Phase 2: Financial Report Generators + +### Step 2.1 — EÜR (Einnahmen-Überschuss-Rechnung) Generator + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/EurReportGenerator.java` + +Logic: +1. Query all `financial_transactions` for the selected calendar year +2. Group into Einnahmen (INCOME transactions) and Ausgaben (EXPENSE transactions) +3. Sub-group by `expense_category` / income source +4. Calculate: Total income, total expenses, Überschuss/Fehlbetrag +5. Include: Opening balance (Jan 1) and closing balance (Dec 31) +6. Generate PDF in standard EÜR format (Anlage EÜR compatible structure) +7. Generate CSV with semicolons, ISO-8859-1, German decimal format + +**PDF Structure:** +``` +EINNAHMEN-ÜBERSCHUSS-RECHNUNG +Kalenderjahr: 2025 +Verein: Grüner Daumen e.V. + +I. EINNAHMEN + Mitgliedsbeiträge ................ 54.000,00 € + Sonstige Einnahmen ............... 2.400,00 € + ───────────────────────────────────────────── + Summe Einnahmen .................. 56.400,00 € + +II. AUSGABEN + Miete/Pacht ...................... 18.000,00 € + Strom/Energie .................... 4.800,00 € + Cannabis-Anbaumaterial ........... 12.000,00 € + Verwaltung ....................... 3.600,00 € + Versicherungen ................... 2.400,00 € + Sonstige Ausgaben ................ 5.200,00 € + ───────────────────────────────────────────── + Summe Ausgaben ................... 46.000,00 € + +III. ERGEBNIS + Überschuss ....................... 10.400,00 € +``` + +### Step 2.2 — Enhanced Kassenbuch Export + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/KassenbuchExportGenerator.java` + +Enhance existing Kassenbuch with: +- DATEV-compatible CSV format (optional export) +- GoBD-compliant: immutable entries, sequential numbering, no gaps +- Period selection (monthly, quarterly, annual) +- Running balance per entry +- Category totals at bottom + +### Step 2.3 — Beitragsbescheinigung (Fee Confirmation per Member) + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/FeeConfirmationGenerator.java` + +- Per-member annual PDF: confirms total fees paid in calendar year +- Letterhead with club name, address, Vereinsregisternummer +- Suitable for member's tax records (Sonderausgaben) +- Batch generation: one click → all active members get a PDF + +### Step 2.4 — Jahresabschluss Enhancement + +Enhance existing Sprint 8 annual report with: +- Comparison to previous year (if data exists) +- Category-wise breakdown with percentages +- Chart data (for frontend visualization) +- Kassenprüfer-ready format (signature lines) + +--- + +## Phase 3: KCanG Compliance Report Generators + +### Step 3.1 — Annual Authority Report (§26 Abs. 3 KCanG) + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/AnnualAuthorityReportGenerator.java` + +**THE most important report.** Due January 31 each year. + +Logic: +1. Query all harvests (from grow module) in calendar year → sum by strain + THC/CBD +2. Query all distributions in calendar year → sum by strain + THC/CBD +3. Query all destruction records in calendar year → sum by strain + THC/CBD +4. Query stock snapshot as of Dec 31 → by strain + THC/CBD +5. Validate: cultivated - distributed - destroyed ≈ stock change (flag discrepancies) +6. Format into authority-mandated structure + +**Output format (PDF + JSON):** +```json +{ + "anbauvereinigung": { "name": "...", "erlaubnisnummer": "...", "anschrift": "..." }, + "berichtszeitraum": { "von": "2025-01-01", "bis": "2025-12-31" }, + "angebaute_mengen": [ + { "sorte": "Amnesia Haze", "menge_gramm": 5000, "thc_prozent": 18.5, "cbd_prozent": 0.8 } + ], + "weitergegebene_mengen": [...], + "vernichtete_mengen": [...], + "bestand_jahresende": [...] +} +``` + +### Step 3.2 — Distribution Log (§26 Abs. 1 Nr. 5) + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/DistributionLogGenerator.java` + +- Date-range selectable log of all distributions +- Per entry: Datum, Mitglied (Name, Vorname, Geburtsjahr), Menge (g), THC%, Sorte +- Summary: total grams, unique members, average per member +- Flags any quota violations (>25g/day, >50g/month, >30g/month for 18-21, >10% THC for 18-21) +- PDF + CSV export + +### Step 3.3 — Stock Inventory Report (§26 Abs. 1 Nr. 2) + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/StockInventoryReportGenerator.java` + +- Point-in-time snapshot of all cannabis + propagation material on premises +- By strain: grams available, THC%, CBD%, harvest date +- Propagation material: count by type (seeds, clones, cuttings) +- Includes stock movements for selected period (in/out/destroyed) + +### Step 3.4 — Destruction Protocol (§26 Abs. 1 Nr. 4) + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/DestructionProtocolGenerator.java` + +- Official Vernichtungsprotokoll PDF +- Per destruction event: date, batch, strain, grams, method, reason, witness +- Dual signature lines (destroying person + witness) +- Sequential protocol number +- Cumulative annual destruction total + +### Step 3.5 — Cultivation Report (§26 Abs. 1 Nr. 3) + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/CultivationReportGenerator.java` + +- Harvest data from grow module +- Per harvest: strain, grams harvested, THC%, CBD%, harvest date, grow cycle ID +- Annual total by strain +- Growth cycle overview (planted → harvested → distributed/destroyed) + +### Step 3.6 — Transport Certificate (§22 Abs. 4 KCanG) + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/TransportCertificateGenerator.java` + +Per §22(4), the certificate must contain: +1. Name und Anschrift des Sitzes der Anbauvereinigung +2. Datum des Transports +3. Start- und Zieladresse des Transports +4. Mengen in Gramm und Sorten des transportierten Cannabis +5. Name und Kontaktdaten der zuständigen Behörde + +Generate as a single-page PDF, suitable for printing and carrying during transport. + +### Step 3.7 — Full Authority Export (§26 Abs. 2 + §27) + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/FullAuthorityExportGenerator.java` + +The "panic button" — generates EVERYTHING the authority needs in one package: +1. All distribution records (complete §26(1) Nr. 5 data) +2. Stock movement history +3. All destruction records +4. All cultivation records +5. All transport records +6. Propagation material sources +7. Member register (name + birth year only, per DSGVO minimization) + +**Security:** Requires re-authentication (password re-entry) before generation. This is a high-sensitivity operation containing all member distribution histories (Art. 9 DSGVO health data). The confirmation dialog includes a mandatory reason field for the audit trail. + +> **Advisory implementation (v2):** Re-authentication gate added per Security Expert recommendation. The endpoint requires a fresh authentication token (max 30 seconds old) obtained via `POST /api/auth/reconfirm` before the export can proceed. + +**Output:** ZIP file — **streamed** (not buffered in memory): +- Uses `StreamingResponseBody` to write ZIP entries directly to the HTTP response +- For clubs with 5+ years and 500 members, exports can reach 50MB+ +- Streaming avoids `OutOfMemoryError` on the server + +> **Advisory implementation (v2):** Streaming ZIP generation per Architecture Expert recommendation. Uses `ZipOutputStream` wrapping `ServletOutputStream` directly — entries are written sequentially without buffering the entire archive in heap memory. + +**ZIP contents:** +- `README.txt` — explains contents and legal basis +- `distributions.json` + `distributions.csv` +- `stock.json` +- `destructions.json` +- `cultivation.json` +- `transports.json` +- `members.json` (anonymized: name + birth year only) +- `summary.pdf` — human-readable overview + +### Step 3.8 — Distribution Info Sheet (§21 Abs. 2 KCanG) + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/DistributionInfoSheetGenerator.java` + +Printable info sheet that must be handed out at every distribution: +- Weight in grams +- Harvest date +- Best-before date +- Strain name +- Average THC% +- Average CBD% +- Health warnings (§21(3) — standardized text) + +Generate as small PDF (A5 or 1/3 A4) — can be batch-printed. + +--- + +## Phase 4: DSGVO Templates & Verein Administrative Reports + +### Step 4.1 — Verarbeitungsverzeichnis (VVT) Generator + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/VvtGenerator.java` + +Pre-filled template for Anbauvereinigungen: +- 8 standard processing activities pre-defined (member mgmt, distributions, finance, video, etc.) +- Club-specific data filled in (name, address, DPO name if applicable) +- Output: PDF (A4, multi-page table format per Art. 30 requirements) + +### Step 4.2 — TOM (Technisch-Organisatorische Maßnahmen) Document + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/TomGenerator.java` + +Checklist-style PDF with: +- Zutrittskontrolle (physical access) +- Zugangskontrolle (system access — passwords, 2FA) +- Zugriffskontrolle (data access — roles, permissions) +- Trennungskontrolle (purpose limitation) +- Pseudonymisierung/Verschlüsselung +- Verfügbarkeit und Belastbarkeit (backups, disaster recovery) +- Verfahren zur regelmäßigen Überprüfung + +Pre-filled with what CannaManage provides (encryption, role-based access, audit log, etc.). + +### Step 4.3 — DSFA (Datenschutz-Folgenabschätzung) Template + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/DsfaGenerator.java` + +Required because cannabis distribution data = health-related data (Art. 9 DSGVO). + +Structure: +1. Beschreibung der Verarbeitung (automated from VVT) +2. Bewertung der Notwendigkeit (pre-filled: §26 KCanG mandates the processing) +3. Risikobewertung (template with common risks pre-identified) +4. Abhilfemaßnahmen (auto-filled from TOM document) + +### Step 4.4 — Löschkonzept (Deletion Concept) + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/DeletionConceptGenerator.java` + +Documents when each category of data is deleted: +- Maps data categories → retention periods → deletion triggers +- References §26(2) KCanG (5 years), §147 AO (6/8/10 years) +- Describes the automated retention checking (RetentionService) +- Lists manual review process + +### Step 4.4b — Breach Notification Template (Art. 33/34 DSGVO) — **P1** + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/BreachNotificationGenerator.java` + +> **Advisory implementation (v2):** Elevated from P2 to P1 per Risk/Compliance Expert recommendation. Cannabis distribution data is Art. 9 DSGVO health data — a breach could cause significant harm (employment discrimination, social stigma). Having the template ready before an incident occurs is critical. + +Pre-filled breach notification PDF with: +- Art. 33 fields: nature of breach, categories of data subjects affected, approximate number, DPO contact, likely consequences, measures taken +- Art. 34 fields: communication to affected data subjects (plain language) +- Auto-fills club details, DPO info, and data category descriptions from VVT +- Checklist: 72-hour notification deadline reminder, authority contact details +- Editable fields for incident-specific details (what happened, when discovered) + +**Priority justification:** A cannabis club experiencing a data breach has 72 hours to notify the Landesdatenschutzbehörde. Without a pre-prepared template, clubs will scramble under pressure and risk non-compliance with Art. 33(1) DSGVO. Given the sensitivity of cannabis health data, this is P1. + +### Step 4.5 — Mitgliederliste für Vereinsregister + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/MemberListRegistryGenerator.java` + +Formatted per BGB requirements for Vereinsregister: +- Minimal data: Name, Vorname, Anschrift, Eintrittsdatum +- Sorted alphabetically +- Includes total count and date of generation +- Footer: "Erstellt gemäß §67 BGB" + +### Step 4.6 — Vorstandsänderung-Meldung (Board Change Template) + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/BoardChangeNoticeGenerator.java` + +Template for notifying Registergericht of board changes: +- Old board composition +- New board composition (from board_members table) +- Date of election (from assembly records) +- Reference to MV protocol +- Signature line for new Vorstand + +### Step 4.7 — Jahresbericht des Vorstands (Annual Board Report) + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/report/AnnualBoardReportGenerator.java` + +Combines multiple data sources into a report the Vorstand presents to members at the annual MV: +- Member statistics (new, left, total) +- Financial summary (from EÜR) +- Compliance status (KCanG obligations met) +- Key activities (from prevention log, assemblies held, etc.) +- Outlook/plans (editable text section) + +--- + +## Phase 5: Frontend — Berichtszentrale & Sidebar Reorganization + +### Step 5.1 — Sidebar Reorganization + +**Modified file:** `cannamanage-frontend/src/data/navigations.ts` + +Replace flat list with grouped, collapsible sections: + +```typescript +export const navigationsData: NavigationType[] = [ + { + title: "Betrieb", + icon: "Leaf", + items: [ + { title: "Dashboard", href: "/dashboard", iconName: "LayoutDashboard" }, + { title: "Mitglieder", href: "/members", iconName: "Users" }, + { title: "Ausgabe", href: "/distributions", iconName: "Leaf" }, + { title: "Lager", href: "/stock", iconName: "Package" }, + { title: "Anbau", href: "/grow", iconName: "Sprout" }, + ], + }, + { + title: "Kommunikation", + icon: "MessageCircle", + items: [ + { title: "Schwarzes Brett", href: "/info-board", iconName: "Megaphone" }, + { title: "Kalender", href: "/calendar", iconName: "Calendar" }, + { title: "Forum", href: "/forum", iconName: "MessageSquare" }, + ], + }, + { + title: "Verwaltung", + icon: "Building", + items: [ + { title: "Finanzen", href: "/finance", iconName: "Wallet" }, + { title: "Versammlungen", href: "/assemblies", iconName: "Gavel" }, + { title: "Dokumente", href: "/documents", iconName: "FileArchive" }, + { title: "Vorstand", href: "/board", iconName: "Shield" }, + { title: "Personal", href: "/settings/staff", iconName: "UserCog" }, + ], + }, + { + title: "Compliance", + icon: "ShieldCheck", + items: [ + { title: "Berichtszentrale", href: "/reports", iconName: "FileBarChart" }, + { title: "Protokoll", href: "/audit-log", iconName: "ScrollText" }, + { title: "Einstellungen", href: "/settings", iconName: "Settings" }, + ], + }, +] +``` + +**Modified component:** Sidebar must support collapsible groups with section headers and icons. + +### Step 5.2 — Berichtszentrale Page (Report Center) + +**New page:** `cannamanage-frontend/src/app/(app)/reports/page.tsx` (replaces existing basic reports page) + +Layout: +- Top: Compliance status cards (4 cards: KCanG 🟢/🟡/🔴, Finanzen, DSGVO, Verein) +- Middle: Upcoming deadlines list (next 90 days) +- Bottom: Report categories as card grid with accordion sub-items + +**Empty-state handling (new clubs):** + +> **Advisory implementation (v2):** Per UX Expert recommendation — when a club first accesses the Berichtszentrale with no reports generated yet, all compliance indicators will be 🔴 RED. To prevent this from being demoralizing, display a "Compliance Setup" guided callout instead of raw red indicators. + +When `generated_reports` is empty for this tenant: +- Show a "Erste Schritte" banner with 4-step setup guide: + 1. "VVT erstellen" → links to DSGVO tab + 2. "Jahresbericht konfigurieren" → links to KCanG tab + 3. "Kassenbuch einrichten" → links to Finance tab + 4. "Fristen prüfen" → links to deadlines view +- Compliance cards show "Einrichtung erforderlich" (setup required) in neutral gray instead of alarming red +- After first report is generated in any category, switch to normal traffic-light indicators +- Dismissible: admin can click "Verstanden, Dashboard anzeigen" to skip directly to normal view + +Each report card shows: +- Report name +- Last generated date (or "Noch nie erstellt") +- Legal basis reference +- [Generate PDF] [Generate CSV] [Generate JSON] buttons (as applicable) +- [View History] link + +### Step 5.3 — Report Category Pages + +**New pages:** +- `/reports/finance` — Financial reports tab +- `/reports/compliance` — KCanG reports tab +- `/reports/verein` — Administrative reports tab +- `/reports/dsgvo` — Data protection reports tab + +Each with: +- List of available reports in that category +- Generation form (date range, parameters) +- Preview before download +- Download history table + +### Step 5.4 — Destruction Record Management + +**New pages:** +- `/stock/destructions` — List all destruction records +- `/stock/destructions/new` — Record a new destruction event + +Form fields: Batch selection, grams destroyed, propagation count, method, reason, witness name, date. + +### Step 5.5 — Transport Record Management + +**New pages:** +- `/transports` (or nested under operations) +- `/transports/new` — Plan a new transport (with authority notification reminder) +- `/transports/[id]/certificate` — View/download transport certificate + +### Step 5.6 — Prevention Activity Log + +**New page:** +- `/board/prevention` — Prevention officer activity log + +List of activities + "Add Activity" form. Shows training certificate info. + +### Step 5.7 — Report Services (Frontend) + +**New file:** `cannamanage-frontend/src/services/report-center.ts` + +```typescript +// Report generation triggers +export function useGenerateReport(type: ReportType, params: ReportParams) +export function useReportHistory(type?: ReportType) +export function useComplianceStatus() +export function useUpcomingDeadlines() +export function useDestructionRecords() +export function useTransportRecords() +export function usePreventionActivities() +``` + +### Step 5.8 — i18n Additions + +**Modified files:** +- `cannamanage-frontend/messages/de.json` — all new report names, categories, form labels +- `cannamanage-frontend/messages/en.json` — English translations + +--- + +## Phase 6: Compliance Dashboard + Retention + Integration + +### Step 6.1 — Compliance Status Service (Backend) + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/ComplianceDashboardService.java` + +Calculates real-time compliance status per area: + +```java +public ComplianceStatus getOverallStatus(UUID tenantId) { + // KCanG: Check if annual report was submitted, distribution records complete + // Finance: Check if EÜR generated for last fiscal year + // DSGVO: Check if VVT exists and is recent + // Verein: Check board term expiry, next MV date + + // Yellow = deadline within 30 days or data gaps + // Red = deadline passed or critical gaps + // Green = everything current +} +``` + +### Step 6.2 — Deadline Seeding + Scheduler + +**File:** `cannamanage-service/src/main/java/de/cannamanage/service/ComplianceDeadlineScheduler.java` + +On tenant creation (or first report center access): +- Seed standard annual deadlines (Jan 31 authority report, MV, EÜR) +- Daily scheduler checks for approaching deadlines → creates notifications +- Auto-rolls annual deadlines to next year after completion + +### Step 6.3 — Retention Service Implementation + +Implements the daily retention check: +- Queries all data categories +- Compares creation date against retention period rules +- Creates admin notification when data approaches end-of-retention +- Provides "Löschprotokoll" (deletion log) export + +### Step 6.4 — API Endpoint Summary + +New REST endpoints: + +``` +# Destruction Records +POST /api/destructions +GET /api/destructions +GET /api/destructions/{id} +GET /api/destructions/{id}/pdf + +# Transport Records +POST /api/transports +GET /api/transports +GET /api/transports/{id} +GET /api/transports/{id}/certificate/pdf +POST /api/transports/{id}/notify-authority + +# Propagation Sources +POST /api/propagation-sources +GET /api/propagation-sources +DELETE /api/propagation-sources/{id} + +# Prevention Activities +POST /api/prevention-activities +GET /api/prevention-activities +DELETE /api/prevention-activities/{id} + +# Report Center +POST /api/reports/generate (body: {type, params}) +GET /api/reports/history +GET /api/reports/{id}/download + +# Specific Report Generators +GET /api/reports/eur/pdf?year=2025 +GET /api/reports/eur/csv?year=2025 +GET /api/reports/authority-annual/pdf?year=2025 +GET /api/reports/authority-annual/json?year=2025 +GET /api/reports/distribution-log/pdf?from=&to= +GET /api/reports/distribution-log/csv?from=&to= +GET /api/reports/stock-inventory/pdf?date= +GET /api/reports/destruction-protocol/pdf?from=&to= +GET /api/reports/cultivation/pdf?year= +GET /api/reports/transport-certificate/{id}/pdf +GET /api/reports/authority-export/zip?from=&to= +GET /api/reports/member-list-registry/pdf +GET /api/reports/board-change-notice/pdf +GET /api/reports/annual-board-report/pdf?year= +GET /api/reports/vvt/pdf +GET /api/reports/tom/pdf +GET /api/reports/dsfa/pdf +GET /api/reports/deletion-concept/pdf +GET /api/reports/fee-confirmation/pdf?memberId=&year= +GET /api/reports/fee-confirmation/batch/zip?year= +GET /api/reports/info-sheet/{distributionId}/pdf + +# Compliance Dashboard +GET /api/compliance/status +GET /api/compliance/deadlines +PUT /api/compliance/deadlines/{id}/complete +``` + +### Step 6.5 — Integration Testing + +- Test each report generator with realistic data +- Verify PDF output quality (correct German formatting, legal references) +- Verify CSV encoding (ISO-8859-1, semicolons, decimal comma) +- Verify JSON schema for authority export +- Test retention service with date manipulation +- Test compliance status calculation edge cases +- E2E: Generate report from frontend → download → verify contents + +### Step 6.6 — Seed Data Enhancement + +Extend existing seed data with: +- Sample destruction records +- Sample transport records +- Sample propagation sources +- Prevention activities +- Pre-seeded compliance deadlines for demo +- THC/CBD percentages on existing distribution seeds + +--- + +## Technical Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| PDF library | Reuse existing OpenPDF | Proven in Sprint 5+8, already in dependencies | +| CSV encoding | ISO-8859-1 + semicolons | German DATEV standard, accountants expect this | +| CSV injection prevention | Prefix dangerous cells with single quote | Cells starting with `=`, `+`, `-`, `@` get `'` prefix to prevent Excel formula injection | +| JSON schema | Custom (no standard exists) | KCanG is too new for an official schema — we define it | +| ZIP generation | `java.util.zip.ZipOutputStream` via `StreamingResponseBody` | Standard library, streamed to avoid OOM on large exports | +| Report storage | Database record + file in documents module | Reuse existing document storage from Sprint 8 | +| Retention checking | Spring `@Scheduled` | Simple, already used for payment reminders | +| Rate limiting | Resilience4j `@RateLimiter` — 5 reports/min/tenant | Prevents DoS via report generation spam (CPU-intensive PDF rendering) | +| Authority export auth | Re-authentication required (password re-entry) | High-sensitivity: contains all member health data | +| Sidebar state | LocalStorage for collapsed/expanded | No backend needed, instant UX | +| Report preview | Server-side rendered HTML → PDF | Same template for preview and final PDF | +| Empty-state UX | Guided setup banner for new clubs | Prevents demoralizing all-red compliance dashboard on first access | + +> **Advisory implementations (v2):** CSV injection prevention, rate limiting, re-authentication gate, streaming ZIP, and empty-state UX all added per panel review recommendations. + +--- + +## File Count Estimate + +| Phase | New Backend Files | New Frontend Files | Modified Files | +|-------|------------------|--------------------|----------------| +| Phase 1 | ~18 (entities, repos, controllers, enums) | 0 | 3 (Distribution entity, pom.xml) | +| Phase 2 | ~5 (report generators) | 0 | 1 (existing ReportService) | +| Phase 3 | ~9 (report generators) | 0 | 0 | +| Phase 4 | ~8 (report generators) | 0 | 0 | +| Phase 5 | 0 | ~20 (pages, components, services) | 5 (sidebar, navigations, i18n) | +| Phase 6 | ~4 (services, schedulers) | ~3 (dashboard components) | 2 (seed data) | +| **Total** | **~44** | **~23** | **~11** | + +--- + +## Dependencies & Risks + +| Risk | Probability | Impact | Mitigation | +|------|-------------|--------|------------| +| Authority report format changes | Low (KCanG is new) | Medium | JSON export is flexible, PDF regenerable | +| DSGVO template incomplete | Medium | Low | Templates are editable, clubs can customize | +| PDF formatting issues with German umlauts | Low | Low | OpenPDF handles UTF-8, already proven | +| Retention period legal ambiguity (§26 "5 Jahre" — from when?) | Medium | Medium | Conservative: 5 years from record creation + admin review | +| Large data export performance | Low | Medium | Stream responses, chunked ZIP generation | +| Sidebar reorg breaks existing bookmarks | Low | Low | URL paths stay the same, only visual grouping changes | + +--- + +## Success Criteria + +1. ✅ All P0 reports generate valid PDFs with correct German formatting +2. ✅ Authority annual report (§26(3)) produces complete, accurate data +3. ✅ Full authority export generates machine-readable JSON bundle +4. ✅ EÜR matches standard format for Finanzamt submission +5. ✅ Sidebar is grouped and collapsible with no functionality loss +6. ✅ Compliance dashboard shows correct status for all 4 areas +7. ✅ Retention service identifies records approaching end-of-life +8. ✅ All exports respect DSGVO data minimization (birth year, not full DOB in authority exports) +9. ✅ CSV exports use correct encoding (ISO-8859-1) and delimiter (semicolons) +10. ✅ Report history tracks what was generated when and by whom diff --git a/docs/sprint-9/cannamanage-sprint9-testplan.md b/docs/sprint-9/cannamanage-sprint9-testplan.md new file mode 100644 index 0000000..e900c04 --- /dev/null +++ b/docs/sprint-9/cannamanage-sprint9-testplan.md @@ -0,0 +1,723 @@ +# Sprint 9 Test Plan — Berichtszentrale (Report Center) + +**Date:** 2026-06-15 +**Author:** Patrick Plate / Lumen (Architect) +**Status:** Draft v2 (updated with panel review advisory items) +**Basis:** cannamanage-sprint9-plan.md v2 +**Total Test Cases:** 80 + +--- + +## Test Overview + +| ID | Description | Type | Class/Location | Status | +|----|-------------|------|----------------|--------| +| T-01 | Destruction record CRUD | Unit | `DestructionRecordServiceTest` | ⬜ | +| T-02 | Transport record CRUD | Unit | `TransportRecordServiceTest` | ⬜ | +| T-03 | Propagation source CRUD | Unit | `PropagationSourceServiceTest` | ⬜ | +| T-04 | Prevention activity CRUD | Unit | `PreventionActivityServiceTest` | ⬜ | +| T-05 | Generated report record keeping | Unit | `GeneratedReportServiceTest` | ⬜ | +| T-06 | Compliance deadline CRUD + status | Unit | `ComplianceDeadlineServiceTest` | ⬜ | +| T-07 | EÜR calculation correctness | Unit | `EurReportGeneratorTest` | ⬜ | +| T-08 | EÜR zero-income edge case | Unit | `EurReportGeneratorTest` | ⬜ | +| T-09 | EÜR multi-category expense grouping | Unit | `EurReportGeneratorTest` | ⬜ | +| T-10 | EÜR CSV encoding ISO-8859-1 | Unit | `EurReportGeneratorTest` | ⬜ | +| T-11 | EÜR decimal comma format | Unit | `EurReportGeneratorTest` | ⬜ | +| T-12 | Kassenbuch export with running balance | Unit | `KassenbuchExportGeneratorTest` | ⬜ | +| T-13 | Kassenbuch period filtering | Unit | `KassenbuchExportGeneratorTest` | ⬜ | +| T-14 | Fee confirmation per member | Unit | `FeeConfirmationGeneratorTest` | ⬜ | +| T-15 | Fee confirmation batch generation | Unit | `FeeConfirmationGeneratorTest` | ⬜ | +| T-16 | Annual authority report - all fields populated | Unit | `AnnualAuthorityReportGeneratorTest` | ⬜ | +| T-17 | Annual authority report - by-strain breakdown | Unit | `AnnualAuthorityReportGeneratorTest` | ⬜ | +| T-18 | Annual authority report - stock reconciliation check | Unit | `AnnualAuthorityReportGeneratorTest` | ⬜ | +| T-19 | Annual authority report - empty year (no activity) | Unit | `AnnualAuthorityReportGeneratorTest` | ⬜ | +| T-20 | Annual authority report - JSON schema validation | Unit | `AnnualAuthorityReportGeneratorTest` | ⬜ | +| T-21 | Distribution log - date range filter | Unit | `DistributionLogGeneratorTest` | ⬜ | +| T-22 | Distribution log - quota violation flagging | Unit | `DistributionLogGeneratorTest` | ⬜ | +| T-23 | Distribution log - Heranwachsende THC limit flag | Unit | `DistributionLogGeneratorTest` | ⬜ | +| T-24 | Distribution log - CSV semicolons + ISO-8859-1 | Unit | `DistributionLogGeneratorTest` | ⬜ | +| T-25 | Stock inventory - point-in-time snapshot | Unit | `StockInventoryReportGeneratorTest` | ⬜ | +| T-26 | Stock inventory - includes propagation material count | Unit | `StockInventoryReportGeneratorTest` | ⬜ | +| T-27 | Destruction protocol - sequential numbering | Unit | `DestructionProtocolGeneratorTest` | ⬜ | +| T-28 | Destruction protocol - witness fields | Unit | `DestructionProtocolGeneratorTest` | ⬜ | +| T-29 | Destruction protocol - annual totals | Unit | `DestructionProtocolGeneratorTest` | ⬜ | +| T-30 | Cultivation report - harvest aggregation by strain | Unit | `CultivationReportGeneratorTest` | ⬜ | +| T-31 | Transport certificate - all §22(4) fields present | Unit | `TransportCertificateGeneratorTest` | ⬜ | +| T-32 | Transport certificate - PDF single page | Unit | `TransportCertificateGeneratorTest` | ⬜ | +| T-33 | Full authority export - ZIP structure valid | Unit | `FullAuthorityExportGeneratorTest` | ⬜ | +| T-34 | Full authority export - JSON files parseable | Unit | `FullAuthorityExportGeneratorTest` | ⬜ | +| T-35 | Full authority export - DSGVO minimization (birth year only) | Unit | `FullAuthorityExportGeneratorTest` | ⬜ | +| T-36 | Full authority export - includes README.txt | Unit | `FullAuthorityExportGeneratorTest` | ⬜ | +| T-37 | Distribution info sheet - all §21(2) fields | Unit | `DistributionInfoSheetGeneratorTest` | ⬜ | +| T-38 | Distribution info sheet - health warnings present | Unit | `DistributionInfoSheetGeneratorTest` | ⬜ | +| T-39 | VVT generator - pre-filled template complete | Unit | `VvtGeneratorTest` | ⬜ | +| T-40 | VVT generator - club-specific data inserted | Unit | `VvtGeneratorTest` | ⬜ | +| T-41 | TOM generator - all 7 control areas present | Unit | `TomGeneratorTest` | ⬜ | +| T-42 | DSFA generator - structure correct | Unit | `DsfaGeneratorTest` | ⬜ | +| T-43 | Deletion concept - all retention categories listed | Unit | `DeletionConceptGeneratorTest` | ⬜ | +| T-44 | Member list registry - §67 BGB format | Unit | `MemberListRegistryGeneratorTest` | ⬜ | +| T-45 | Member list registry - minimal data only | Unit | `MemberListRegistryGeneratorTest` | ⬜ | +| T-46 | Board change notice - old vs new composition | Unit | `BoardChangeNoticeGeneratorTest` | ⬜ | +| T-47 | Annual board report - combines all data sources | Unit | `AnnualBoardReportGeneratorTest` | ⬜ | +| T-48 | Compliance status - GREEN when all obligations met | Unit | `ComplianceDashboardServiceTest` | ⬜ | +| T-49 | Compliance status - YELLOW when deadline within 30 days | Unit | `ComplianceDashboardServiceTest` | ⬜ | +| T-50 | Compliance status - RED when deadline passed | Unit | `ComplianceDashboardServiceTest` | ⬜ | +| T-51 | Compliance status - KCanG area calculation | Unit | `ComplianceDashboardServiceTest` | ⬜ | +| T-52 | Retention service - identifies expired records | Unit | `RetentionServiceTest` | ⬜ | +| T-53 | Retention service - respects different retention periods | Unit | `RetentionServiceTest` | ⬜ | +| T-54 | Retention service - never auto-deletes | Unit | `RetentionServiceTest` | ⬜ | +| T-55 | Deadline scheduler - rolls annual deadlines | Unit | `ComplianceDeadlineSchedulerTest` | ⬜ | +| T-56 | Deadline scheduler - creates notifications | Unit | `ComplianceDeadlineSchedulerTest` | ⬜ | +| T-57 | PDF generation - German umlauts render correctly | Integration | `PdfRenderingTest` | ⬜ | +| T-58 | PDF generation - legal reference in footer | Integration | `PdfRenderingTest` | ⬜ | +| T-59 | PDF generation - club letterhead | Integration | `PdfRenderingTest` | ⬜ | +| T-60 | Report controller - generate EÜR endpoint | Integration | `ReportControllerTest` | ⬜ | +| T-61 | Report controller - authority annual report endpoint | Integration | `ReportControllerTest` | ⬜ | +| T-62 | Report controller - full export ZIP endpoint | Integration | `ReportControllerTest` | ⬜ | +| T-63 | Report controller - permission check (ADMIN only) | Integration | `ReportControllerTest` | ⬜ | +| T-64 | Destruction controller - CRUD + PDF | Integration | `DestructionRecordControllerTest` | ⬜ | +| T-65 | Transport controller - CRUD + certificate | Integration | `TransportRecordControllerTest` | ⬜ | +| T-66 | Sidebar renders grouped navigation | E2E | `navigation.spec.ts` | ⬜ | +| T-67 | Berichtszentrale page loads with compliance cards | E2E | `reports.spec.ts` | ⬜ | +| T-68 | Report download flow (generate → download PDF) | E2E | `reports.spec.ts` | ⬜ | +| T-69 | Rate limiter - 6th report in 1 min returns 429 | Integration | `ReportControllerTest` | ⬜ | +| T-70 | Rate limiter - different tenant not affected | Integration | `ReportControllerTest` | ⬜ | +| T-71 | CSV injection prevention - dangerous cell prefixes escaped | Unit | `CsvExportUtilTest` | ⬜ | +| T-72 | CSV injection - formula in member name does not execute | Unit | `CsvExportUtilTest` | ⬜ | +| T-73 | Authority export re-authentication required | Integration | `ReportControllerTest` | ⬜ | +| T-74 | Authority export - expired reconfirm token rejected | Integration | `ReportControllerTest` | ⬜ | +| T-75 | Authority export - reason field minimum length enforced | Integration | `ReportControllerTest` | ⬜ | +| T-76 | Streaming ZIP - large export does not OOM | Integration | `FullAuthorityExportGeneratorTest` | ⬜ | +| T-77 | Breach notification - Art. 33 section present | Unit | `BreachNotificationGeneratorTest` | ⬜ | +| T-78 | Breach notification - Art. 34 section separate | Unit | `BreachNotificationGeneratorTest` | ⬜ | +| T-79 | Breach notification - 72h deadline reminder included | Unit | `BreachNotificationGeneratorTest` | ⬜ | +| T-80 | Empty-state Berichtszentrale shows onboarding for new club | E2E | `reports.spec.ts` | ⬜ | + +Status: ⬜ Pending | ✅ Passed | ❌ Failed | ⏭️ Skipped + +--- + +## Test Cases — Detailed + +### T-01: Destruction Record CRUD + +**Type:** Unit +**Class:** `DestructionRecordServiceTest` +**Method:** `testCreateDestructionRecord()`, `testListByDateRange()`, `testDeleteNotAllowed()` + +**Preconditions:** +- Tenant with active batch in stock + +**Scenarios:** + +| # | Input | Expected Result | +|---|-------|----------------| +| a | Valid destruction (batch, 100g, INCINERATION, reason, witness) | Record created, stock updated (decrease by 100g) | +| b | Destruction with 0 grams | Validation error: grams must be > 0 | +| c | Destruction exceeding batch remaining stock | Validation error: cannot destroy more than available | +| d | List destructions for date range | Only records within range returned | +| e | Delete destruction record | Rejected: destruction records are immutable (audit trail) | + +--- + +### T-07: EÜR Calculation Correctness + +**Type:** Unit +**Class:** `EurReportGeneratorTest` +**Method:** `testEurCalculation_normalYear()` + +**Preconditions:** +- 12 months of financial transactions (income + expenses) +- Multiple expense categories + +**Scenarios:** + +| # | Input | Expected Result | +|---|-------|----------------| +| a | Year 2025 with €54000 income, €46000 expenses | EÜR shows Überschuss €8000 | +| b | Opening balance €5000, closing balance €13000 | Balance change matches Überschuss | +| c | 6 expense categories | All categories listed with correct subtotals | +| d | Amounts in cents (internal) | Display in Euro with comma decimals (1.234,56) | + +--- + +### T-10: EÜR CSV Encoding ISO-8859-1 + +**Type:** Unit +**Class:** `EurReportGeneratorTest` +**Method:** `testCsvEncoding()` + +**Scenarios:** + +| # | Input | Expected Result | +|---|-------|----------------| +| a | Category "Büroausstattung" with umlaut | CSV file readable in ISO-8859-1, ü correctly encoded | +| b | Amount 1234.56 | CSV shows "1234,56" (decimal comma) | +| c | Field separator | Semicolons (;) not commas | +| d | Line ending | CRLF (Windows-compatible for DATEV) | + +--- + +### T-16: Annual Authority Report — All Fields Populated + +**Type:** Unit +**Class:** `AnnualAuthorityReportGeneratorTest` +**Method:** `testAnnualReport_complete()` + +**Preconditions:** +- Grow module has harvest records for 3 strains +- Distribution records for 2025 +- 2 destruction records +- Stock snapshot available for Dec 31 + +**Scenarios:** + +| # | Input | Expected Result | +|---|-------|----------------| +| a | Year 2025 | Report contains all 4 sections: cultivated, distributed, destroyed, end-stock | +| b | 3 strains with different THC/CBD | Each strain listed separately with correct averages | +| c | Cross-check: cultivated - distributed - destroyed = stock change | Discrepancy < 1g (rounding) or flagged | +| d | JSON output | Valid JSON matching defined schema | +| e | PDF output | Contains legal reference "§26 Abs. 3 KCanG" in footer | + +--- + +### T-22: Distribution Log — Quota Violation Flagging + +**Type:** Unit +**Class:** `DistributionLogGeneratorTest` +**Method:** `testQuotaViolationFlags()` + +**Preconditions:** +- Member A (age 25): received 26g on one day (exceeds 25g/day limit) +- Member B (age 19): received cannabis with 12% THC (exceeds 10% for Heranwachsende) +- Member C (age 30): received 52g in one month (exceeds 50g/month) + +**Scenarios:** + +| # | Input | Expected Result | +|---|-------|----------------| +| a | Member A daily violation | Report flags: "TAGESLIMIT ÜBERSCHRITTEN: 26g > 25g" | +| b | Member B THC violation | Report flags: "THC-LIMIT HERANWACHSENDE: 12% > 10%" | +| c | Member C monthly violation | Report flags: "MONATSLIMIT ÜBERSCHRITTEN: 52g > 50g" | +| d | Member D (age 25, 20g/day, 45g/month) | No flags — within all limits | + +--- + +### T-33: Full Authority Export — ZIP Structure Valid + +**Type:** Unit +**Class:** `FullAuthorityExportGeneratorTest` +**Method:** `testZipStructure()` + +**Scenarios:** + +| # | Input | Expected Result | +|---|-------|----------------| +| a | Generate full export | ZIP contains: README.txt, distributions.json, distributions.csv, stock.json, destructions.json, cultivation.json, transports.json, members.json, summary.pdf | +| b | Each JSON file | Valid JSON, parseable without errors | +| c | members.json | Contains ONLY: name, firstName, birthYear — NO address, NO full DOB, NO phone | +| d | README.txt | Contains: generation date, legal basis reference, file descriptions | + +--- + +### T-35: Full Authority Export — DSGVO Minimization + +**Type:** Unit +**Class:** `FullAuthorityExportGeneratorTest` +**Method:** `testDsgvoMinimization()` + +**Critical test** — ensures we don't leak unnecessary personal data to authorities. + +**Scenarios:** + +| # | Input | Expected Result | +|---|-------|----------------| +| a | Member with full profile (name, address, phone, email, DOB 1990-05-15) | Export contains only: "Max", "Müller", 1990 | +| b | Distribution record | Contains member name + birth year, NOT member ID or address | +| c | No bank details in any export file | Grep for IBAN patterns returns zero matches | +| d | No email addresses in export | Grep for @ returns zero matches | + +--- + +### T-48: Compliance Status — GREEN When All Obligations Met + +**Type:** Unit +**Class:** `ComplianceDashboardServiceTest` +**Method:** `testGreenStatus()` + +**Preconditions:** +- Annual authority report generated for previous year +- EÜR generated for previous year +- VVT exists and updated within last 12 months +- Next MV scheduled +- All board terms valid + +**Scenarios:** + +| # | Input | Expected Result | +|---|-------|----------------| +| a | All conditions met | Overall status: GREEN | +| b | KCanG area | GREEN: annual report submitted, records complete | +| c | Finance area | GREEN: EÜR generated, no overdue deadlines | +| d | DSGVO area | GREEN: VVT exists and recent | +| e | Verein area | GREEN: MV scheduled, board terms valid | + +--- + +### T-50: Compliance Status — RED When Deadline Passed + +**Type:** Unit +**Class:** `ComplianceDashboardServiceTest` +**Method:** `testRedStatus()` + +**Preconditions:** +- Current date: February 15 +- Annual authority report for previous year NOT generated (deadline was Jan 31) + +**Scenarios:** + +| # | Input | Expected Result | +|---|-------|----------------| +| a | Authority report overdue | KCanG status: RED | +| b | Overall status | RED (worst of all areas) | +| c | Deadline record | Status marked OVERDUE | + +--- + +### T-52: Retention Service — Identifies Expired Records + +**Type:** Unit +**Class:** `RetentionServiceTest` +**Method:** `testIdentifiesExpiredRecords()` + +**Preconditions:** +- Distribution record created 6 years ago (past 5-year KCanG retention) +- Financial transaction created 11 years ago (past 10-year AO retention) +- MV protocol from 20 years ago (indefinite retention — should NOT be flagged) + +**Scenarios:** + +| # | Input | Expected Result | +|---|-------|----------------| +| a | 6-year-old distribution record | Flagged for deletion review | +| b | 11-year-old financial transaction | Flagged for deletion review | +| c | 20-year-old MV protocol | NOT flagged (indefinite retention) | +| d | 4-year-old distribution record | NOT flagged (within 5-year period) | + +--- + +### T-54: Retention Service — Never Auto-Deletes + +**Type:** Unit +**Class:** `RetentionServiceTest` +**Method:** `testNeverAutoDeletes()` + +**Critical safety test.** + +**Scenarios:** + +| # | Input | Expected Result | +|---|-------|----------------| +| a | Run retention check with expired records | No records deleted from database | +| b | Expired record | Status changed to "RETENTION_EXPIRED", admin notification created | +| c | Admin confirms deletion | THEN record is soft-deleted (retention log entry created) | +| d | Without admin confirmation | Record persists indefinitely | + +--- + +### T-57: PDF Generation — German Umlauts Render Correctly + +**Type:** Integration +**Class:** `PdfRenderingTest` +**Method:** `testGermanCharacters()` + +**Scenarios:** + +| # | Input | Expected Result | +|---|-------|----------------| +| a | Text with äöüÄÖÜß | All characters render correctly in PDF | +| b | Club name "Grüner Daumen e.V." | Renders correctly in letterhead | +| c | Category "Büroausstattung" | Renders correctly in EÜR table | +| d | Legal text "§26 Abs. 3 KCanG" | § symbol renders correctly | +| e | Euro amounts "1.234,56 €" | Euro sign renders correctly | + +--- + +### T-63: Report Controller — Permission Check + +**Type:** Integration +**Class:** `ReportControllerTest` +**Method:** `testPermissions()` + +**Scenarios:** + +| # | Input | Expected Result | +|---|-------|----------------| +| a | ADMIN requests EÜR | 200 OK + PDF | +| b | STAFF (Kassenwart) requests EÜR | 200 OK + PDF (finance permission) | +| c | MEMBER requests EÜR | 403 Forbidden | +| d | ADMIN requests authority export | 200 OK + ZIP | +| e | STAFF without finance permission requests EÜR | 403 Forbidden | +| f | Unauthenticated request | 401 Unauthorized | + +--- + +### T-66: Sidebar Renders Grouped Navigation + +**Type:** E2E (Playwright) +**File:** `cannamanage-frontend/e2e/authenticated/navigation.spec.ts` + +**Scenarios:** + +| # | Action | Expected Result | +|---|--------|----------------| +| a | Load dashboard page | Sidebar shows 4 groups: Betrieb, Kommunikation, Verwaltung, Compliance | +| b | Click group header "Kommunikation" | Group collapses/expands | +| c | Navigate to /reports | "Berichtszentrale" item highlighted in Compliance group | +| d | All existing URLs still work | /members, /distributions, /stock, /grow, /finance, /assemblies all accessible | + +--- + +### T-67: Berichtszentrale Page Loads + +**Type:** E2E (Playwright) +**File:** `cannamanage-frontend/e2e/authenticated/reports.spec.ts` + +**Scenarios:** + +| # | Action | Expected Result | +|---|--------|----------------| +| a | Navigate to /reports | Page loads with compliance status cards | +| b | Compliance cards visible | 4 cards: KCanG, Finanzen, DSGVO, Verein — each with status color | +| c | Report categories visible | 4 category sections with report listings | +| d | Click "EÜR generieren" | Year picker appears, generate button enabled | +| e | Click "Behörden-Export" | Confirmation dialog shown (due to data sensitivity) | + +--- + +### T-68: Report Download Flow + +**Type:** E2E (Playwright) +**File:** `cannamanage-frontend/e2e/authenticated/reports.spec.ts` + +**Scenarios:** + +| # | Action | Expected Result | +|---|--------|----------------| +| a | Select year 2025, click generate EÜR PDF | Download triggered, file non-empty | +| b | Generate distribution log for last month | Download triggered, PDF contains data | +| c | Report appears in history table | History shows: type, date, generated by | +| d | Re-download from history | Same file downloadable again | + +--- + +### T-69: Rate Limiter - 6th Report in 1 Minute Returns 429 + +**Type:** Integration +**Class:** `ReportControllerTest` +**Method:** `testRateLimit_sixthRequestReturns429()` + +**Preconditions:** +- Authenticated admin user +- Resilience4j rate limiter configured: 5 permits/minute/tenant + +**Scenarios:** + +| # | Action | Expected Result | +|---|--------|----------------| +| a | Generate 5 reports in rapid succession | All return 200 OK | +| b | Generate 6th report within same minute | Returns 429 Too Many Requests | +| c | Response body contains "Rate limit exceeded" message | Helpful German error message | +| d | Response includes `Retry-After` header | Header present with seconds value | + +--- + +### T-70: Rate Limiter - Different Tenant Not Affected + +**Type:** Integration +**Class:** `ReportControllerTest` +**Method:** `testRateLimit_differentTenantNotBlocked()` + +**Preconditions:** +- Two tenants configured in test + +**Scenarios:** + +| # | Action | Expected Result | +|---|--------|----------------| +| a | Tenant A generates 5 reports (hitting limit) | All return 200 | +| b | Tenant B generates 1 report immediately after | Returns 200 OK (not affected by A's limit) | + +--- + +### T-71: CSV Injection Prevention - Dangerous Cell Prefixes Escaped + +**Type:** Unit +**Class:** `CsvExportUtilTest` +**Method:** `testCsvInjection_dangerousPrefixesEscaped()` + +**Preconditions:** +- CSV export utility available + +**Scenarios:** + +| # | Input Cell Value | Expected Output | +|---|-----------------|----------------| +| a | `=SUM(A1:A10)` | `'=SUM(A1:A10)` | +| b | `+cmd\|'/C calc'\|''!A0` | `'+cmd\|'/C calc'\|''!A0` | +| c | `-2+3+cmd\|'/C calc'\|'!A0` | `'-2+3+cmd\|'/C calc'\|'!A0` | +| d | `@SUM(A1:A10)` | `'@SUM(A1:A10)` | +| e | `Normal text value` | `Normal text value` (unchanged) | +| f | `123.45` | `123.45` (numbers unchanged) | + +--- + +### T-72: CSV Injection - Formula in Member Name Does Not Execute + +**Type:** Unit +**Class:** `CsvExportUtilTest` +**Method:** `testCsvInjection_memberNameWithFormula()` + +**Preconditions:** +- Member with name `=HYPERLINK("http://evil.com","Click")` in database + +**Scenarios:** + +| # | Action | Expected Result | +|---|--------|----------------| +| a | Export distribution log as CSV with this member | Cell contains `'=HYPERLINK(...)` with leading quote | +| b | Open CSV in LibreOffice Calc | No formula execution, displays as text | + +--- + +### T-73: Authority Export Re-Authentication Required + +**Type:** Integration +**Class:** `ReportControllerTest` +**Method:** `testAuthorityExport_requiresReauthentication()` + +**Preconditions:** +- Authenticated admin user with valid session + +**Scenarios:** + +| # | Action | Expected Result | +|---|--------|----------------| +| a | Call `GET /api/reports/authority-export/zip` without reconfirm token | Returns 403 Forbidden | +| b | Call `POST /api/auth/reconfirm` with correct password | Returns fresh token (valid 30s) | +| c | Call authority export with valid reconfirm token within 30s | Returns 200 with ZIP stream | +| d | Audit log entry created | Records: who, when, reason, IP address | + +--- + +### T-74: Authority Export - Expired Reconfirm Token Rejected + +**Type:** Integration +**Class:** `ReportControllerTest` +**Method:** `testAuthorityExport_expiredTokenRejected()` + +**Preconditions:** +- Reconfirm token obtained but 31+ seconds elapsed + +**Scenarios:** + +| # | Action | Expected Result | +|---|--------|----------------| +| a | Call authority export with 31-second-old reconfirm token | Returns 403 Forbidden | +| b | Error message indicates re-authentication required | `"Bestätigung abgelaufen. Bitte erneut authentifizieren."` | + +--- + +### T-75: Authority Export - Reason Field Minimum Length Enforced + +**Type:** Integration +**Class:** `ReportControllerTest` +**Method:** `testAuthorityExport_reasonFieldValidation()` + +**Preconditions:** +- Valid reconfirm token + +**Scenarios:** + +| # | Reason Value | Expected Result | +|---|-------------|----------------| +| a | `""` (empty) | 400 Bad Request — reason required | +| b | `"test"` (4 chars) | 400 Bad Request — minimum 10 characters | +| c | `"."` (1 char) | 400 Bad Request — minimum 10 characters | +| d | `"Behördenanfrage vom 15.06.2026"` (30 chars) | 200 OK — accepted | +| e | `"Jährliche Prüfung der Dokumentation"` | 200 OK — accepted | + +--- + +### T-76: Streaming ZIP - Large Export Does Not OOM + +**Type:** Integration +**Class:** `FullAuthorityExportGeneratorTest` +**Method:** `testStreamingZip_largeDataDoesNotOOM()` + +**Preconditions:** +- Test data with 500 members, 5000 distributions, 200 destruction records +- JVM heap limited to 256MB in test configuration + +**Scenarios:** + +| # | Action | Expected Result | +|---|--------|----------------| +| a | Generate full authority export with large dataset | Completes without OutOfMemoryError | +| b | Response is streamed (chunked transfer encoding) | `Transfer-Encoding: chunked` header present | +| c | ZIP file is valid and contains all expected entries | All 8 files present in ZIP | +| d | Peak memory usage stays below heap limit | No GC pressure spikes above 200MB | + +--- + +### T-77: Breach Notification - Art. 33 Section Present + +**Type:** Unit +**Class:** `BreachNotificationGeneratorTest` +**Method:** `testBreachNotification_article33SectionComplete()` + +**Preconditions:** +- Club with configured DPO and Landesdatenschutzbehörde contact + +**Scenarios:** + +| # | Check | Expected Content | +|---|-------|-----------------| +| a | Nature of breach field | Present and editable placeholder | +| b | Categories of data subjects | Pre-filled: "Mitglieder der Anbauvereinigung" | +| c | Approximate number affected | Editable field with club member count pre-filled | +| d | DPO contact details | Auto-filled from club settings | +| e | Likely consequences | Pre-filled with Art. 9 health data risk template | +| f | Measures taken/proposed | Editable placeholder | +| g | 72-hour deadline reminder | Bold text: "Meldung innerhalb 72 Stunden nach Kenntnisnahme" | + +--- + +### T-78: Breach Notification - Art. 34 Section Separate + +**Type:** Unit +**Class:** `BreachNotificationGeneratorTest` +**Method:** `testBreachNotification_article34SectionSeparate()` + +**Preconditions:** +- Same as T-77 + +**Scenarios:** + +| # | Check | Expected Result | +|---|-------|----------------| +| a | Art. 34 section has separate heading | "Benachrichtigung der Betroffenen (Art. 34 DSGVO)" | +| b | Plain language requirement noted | "In klarer und einfacher Sprache" instruction present | +| c | Template is distinct from Art. 33 section | Not merged — separate page/section in PDF | +| d | Includes contact details for data subjects | Phone, email, postal address fields | + +--- + +### T-79: Breach Notification - 72h Deadline Reminder Included + +**Type:** Unit +**Class:** `BreachNotificationGeneratorTest` +**Method:** `testBreachNotification_72hDeadlineReminder()` + +**Preconditions:** +- Generated breach notification PDF + +**Scenarios:** + +| # | Check | Expected Content | +|---|-------|-----------------| +| a | Deadline prominently displayed | "72 Stunden" appears in bold or highlighted | +| b | Authority contact for notification | Landesdatenschutzbehörde name + URL/email | +| c | "Zeitpunkt der Kenntnisnahme" field | Date/time field for when breach was discovered | +| d | Countdown note | "Frist beginnt ab Kenntnisnahme, nicht ab Entdeckung" | + +--- + +### T-80: Empty-State Berichtszentrale Shows Onboarding for New Club + +**Type:** E2E (Playwright) +**File:** `cannamanage-frontend/e2e/authenticated/reports.spec.ts` + +**Preconditions:** +- New tenant with zero generated reports + +**Scenarios:** + +| # | Action | Expected Result | +|---|--------|----------------| +| a | Navigate to /reports as new club admin | "Erste Schritte" banner visible | +| b | Compliance cards show neutral state | Gray "Einrichtung erforderlich" — NOT red indicators | +| c | 4-step guide links present | VVT, Jahresbericht, Kassenbuch, Fristen links all working | +| d | Click "Verstanden, Dashboard anzeigen" | Banner dismissed, normal view shown | +| e | Generate one report in any category | After refresh, traffic-light indicators appear | +| f | Banner does not reappear after dismissal | LocalStorage persists dismissal | + +--- + +## Test Data Requirements + +### Seed Data Enhancements + +For test scenarios to work, seed data must include: + +| Data | Quantity | Details | +|------|----------|---------| +| Financial transactions | 50+ | Spanning 12 months, multiple categories | +| Distributions with THC% | 30+ | Various strains, some near-limit quantities | +| Destruction records | 3 | Different methods, dates, witnesses | +| Transport records | 2 | With authority notification timestamps | +| Propagation sources | 2 | One person, one Anbauvereinigung | +| Prevention activities | 5 | Different activity types | +| Grow harvests with THC/CBD | 5 | Different strains, quantities | +| Stock entries | 10+ | Current stock by strain | +| Compliance deadlines | 4 | One per area, various due dates | +| Members aged 18-21 | 2 | For Heranwachsende limit testing | + +--- + +## Test Coverage Summary + +| Component | Unit Tests | Integration Tests | E2E Tests | Total | +|-----------|-----------|------------------|-----------|-------| +| Phase 1 (CRUD) | 6 | 2 | 0 | 8 | +| Phase 2 (Financial) | 9 | 1 | 0 | 10 | +| Phase 3 (KCanG) | 23 | 3 | 0 | 26 | +| Phase 4 (DSGVO/Verein) | 12 | 0 | 0 | 12 | +| Phase 5 (Frontend) | 0 | 0 | 4 | 4 | +| Phase 6 (Compliance + Security) | 9 | 8 | 0 | 17 | +| Cross-cutting (CSV util) | 2 | 0 | 0 | 2 | +| Breach notification (v2) | 3 | 0 | 0 | 3 | +| **Total** | **64** | **14** | **4** | **80** (was 68) | + +--- + +## Critical Test Priorities + +Tests marked as **CRITICAL** — must pass before sprint completion: + +| ID | Test | Why Critical | +|----|------|-------------| +| T-16 | Annual authority report complete | Legal obligation — §26(3) KCanG | +| T-22 | Quota violation flagging | Safety — prevents illegal distribution | +| T-33 | Authority export ZIP structure | Authority inspection readiness | +| T-35 | DSGVO minimization in exports | Privacy violation risk | +| T-54 | Retention service never auto-deletes | Data loss prevention | +| T-57 | German umlauts in PDFs | Usability — broken characters = unprofessional | +| T-63 | Permission checks | Security — unauthorized report access | +| T-69 | Rate limiter enforced | Security — prevents DoS via report generation | +| T-71 | CSV injection prevention | Security — prevents formula injection in exports | +| T-73 | Authority export re-authentication | Security — protects Art. 9 DSGVO health data | +| T-76 | Streaming ZIP no OOM | Reliability — prevents server crash on large exports | +| T-77 | Breach notification Art. 33 complete | Compliance — 72h notification obligation | + +--- + +## Test Naming Convention + +- Test class: `Test.java` +- Test method: `test_()` or descriptive name +- Location: mirrors source structure under `src/test/java/` +- Assertions: use AssertJ for fluent assertions +- Mocking: Mockito for service dependencies +- PDF verification: extract text from generated PDF bytes using Apache PDFBox in test scope