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,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<ReportType, ReportGenerator<?>> generators;
|
||||
private final GeneratedReportRepository generatedReportRepository;
|
||||
private final AuditService auditService;
|
||||
|
||||
public ReportGeneratorService(
|
||||
List<ReportGenerator<?>> 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<ReportParameters> generator =
|
||||
(ReportGenerator<ReportParameters>) 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<ReportParameters> generator =
|
||||
(ReportGenerator<ReportParameters>) 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<GeneratedReport> getGeneratedReports(UUID tenantId) {
|
||||
return generatedReportRepository.findByTenantIdOrderByGeneratedAtDesc(tenantId);
|
||||
}
|
||||
|
||||
public boolean hasGenerator(ReportType type) {
|
||||
return generators.containsKey(type);
|
||||
}
|
||||
}
|
||||
+21
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<T extends ReportParameters> {
|
||||
|
||||
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<ExportFormat> supportedFormats();
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
+27
@@ -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<ComplianceDeadline, UUID> {
|
||||
|
||||
/**
|
||||
* Overdue deadlines: due before given date and not yet completed.
|
||||
*/
|
||||
List<ComplianceDeadline> findByClubIdAndDueDateBeforeAndCompletedAtIsNull(UUID clubId, LocalDate date);
|
||||
|
||||
/**
|
||||
* Upcoming deadlines within a date range.
|
||||
*/
|
||||
List<ComplianceDeadline> findByClubIdAndDueDateBetween(UUID clubId, LocalDate start, LocalDate end);
|
||||
|
||||
List<ComplianceDeadline> findByTenantIdAndCompletedAtIsNullOrderByDueDateAsc(UUID tenantId);
|
||||
|
||||
List<ComplianceDeadline> findByTenantIdOrderByDueDateAsc(UUID tenantId);
|
||||
}
|
||||
+16
@@ -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<DestructionRecord, UUID> {
|
||||
|
||||
List<DestructionRecord> findByClubIdOrderByDestroyedAtDesc(UUID clubId);
|
||||
|
||||
List<DestructionRecord> findByTenantIdOrderByDestroyedAtDesc(UUID tenantId);
|
||||
}
|
||||
+19
@@ -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<GeneratedReport, UUID> {
|
||||
|
||||
List<GeneratedReport> findByClubIdOrderByGeneratedAtDesc(UUID clubId);
|
||||
|
||||
List<GeneratedReport> findByClubIdAndReportType(UUID clubId, ReportType reportType);
|
||||
|
||||
List<GeneratedReport> findByTenantIdOrderByGeneratedAtDesc(UUID tenantId);
|
||||
}
|
||||
+17
@@ -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<PreventionActivity, UUID> {
|
||||
|
||||
List<PreventionActivity> findByClubIdAndActivityDateBetween(UUID clubId, LocalDate start, LocalDate end);
|
||||
|
||||
List<PreventionActivity> findByTenantIdOrderByActivityDateDesc(UUID tenantId);
|
||||
}
|
||||
+16
@@ -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<PropagationSource, UUID> {
|
||||
|
||||
List<PropagationSource> findByClubIdOrderByReceivedAtDesc(UUID clubId);
|
||||
|
||||
List<PropagationSource> findByTenantIdOrderByReceivedAtDesc(UUID tenantId);
|
||||
}
|
||||
+16
@@ -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<TransportRecord, UUID> {
|
||||
|
||||
List<TransportRecord> findByClubIdOrderByTransportDateDesc(UUID clubId);
|
||||
|
||||
List<TransportRecord> findByTenantIdOrderByTransportDateDesc(UUID tenantId);
|
||||
}
|
||||
Reference in New Issue
Block a user