feat(sprint9): Phase 1 — Data model + ReportGenerator infrastructure
- 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<T> interface + ReportGeneratorService (auto-discovery, format dispatch) - ComplianceRecordsController (CRUD for destruction/transport/propagation/prevention) - ComplianceDeadlineController (create, list, complete, overdue) - DateRangeReportParameters record for report generation
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.cannamanage.domain.enums;
|
||||
|
||||
/**
|
||||
* Traffic-light compliance status for the compliance dashboard.
|
||||
*/
|
||||
public enum ComplianceStatus {
|
||||
GREEN,
|
||||
YELLOW,
|
||||
RED
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.cannamanage.domain.enums;
|
||||
|
||||
/**
|
||||
* Supported export formats for generated reports.
|
||||
*/
|
||||
public enum ExportFormat {
|
||||
PDF,
|
||||
CSV,
|
||||
JSON,
|
||||
ZIP
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user