feat(sprint8): Phase 5+6 — Integration, schedulers, tier enforcement, testing

Phase 5 — Integration:
- PaymentReminderScheduler: monthly cron at 9am, sends PAYMENT_REMINDER
  and PAYMENT_OVERDUE (30+ days) notifications, audit logged
- BoardTermScheduler: daily cron at 8am, sends BOARD_TERM_EXPIRING
  notification 30 days before term end (1-day window for single delivery)
- Assembly protocol auto-archive: completeAssembly() generates PDF via
  AssemblyProtocolService and stores it in DocumentService.archiveProtocol()
- PlanTierService: Sprint 8 limits added (Kassenbuch 3mo starter, 1 MV/year
  starter, 100MB/1GB/unlimited document storage)
- Notification wiring: PAYMENT_RECEIVED sent to member on recordPayment(),
  sendToAllMembers() added to NotificationService for assembly invitations
- Seed data: 2 fee schedules (Regular €30, Reduced €15), 4 fee assignments,
  3 sample payments, 2 board positions, 2 board members

Phase 6 — Testing infrastructure:
- V22 migration: protocol_document_id on assemblies table
- Schedulers disabled in test profile (cannamanage.schedulers.enabled=false)
- Scheduler property configurable via SCHEDULERS_ENABLED env var

Additional fixes (pre-existing Phase 4 issues):
- AssemblyProtocolService: fixed Document import ambiguity (OpenPDF vs entity)
- AuditService: added convenience overloads for UUID actorId/clubId
- ReceiptPdfService: fixed getReceiptNumber/getMemberNumber/getPaymentDate
- StaffPermissionChecker: added getUserId/getClubId/getTenantId helpers
- FinanceService: added getPaymentById, exportLedgerCsv methods
This commit is contained in:
Patrick Plate
2026-06-15 09:22:49 +02:00
parent e4698827ee
commit 61b0cd92be
17 changed files with 639 additions and 13 deletions
@@ -273,8 +273,8 @@ public class FinanceController {
.orElseThrow(() -> new NoSuchElementException("Club not found"));
byte[] pdf = receiptPdfService.generateReceipt(payment, member, club);
String filename = "Quittung-" + (payment.getReceiptNumber() != null
? payment.getReceiptNumber() : id.toString().substring(0, 8)) + ".pdf";
String filename = "Quittung-" + (payment.getReference() != null
? payment.getReference() : id.toString().substring(0, 8)) + ".pdf";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
@@ -347,8 +347,8 @@ public class FinanceController {
.orElseThrow(() -> new NoSuchElementException("Club not found"));
byte[] pdf = receiptPdfService.generateReceipt(payment, member, club);
String filename = "Quittung-" + (payment.getReceiptNumber() != null
? payment.getReceiptNumber() : id.toString().substring(0, 8)) + ".pdf";
String filename = "Quittung-" + (payment.getReference() != null
? payment.getReference() : id.toString().substring(0, 8)) + ".pdf";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
@@ -85,4 +85,25 @@ public class StaffPermissionChecker {
throw new org.springframework.security.access.AccessDeniedException("Missing permission: " + required);
}
}
/**
* Extract the user ID from the authenticated principal.
*/
public UUID getUserId(org.springframework.security.core.userdetails.UserDetails principal) {
return UUID.fromString(principal.getUsername());
}
/**
* Get the club ID (tenant) for the authenticated user.
*/
public UUID getClubId(org.springframework.security.core.userdetails.UserDetails principal) {
return de.cannamanage.domain.entity.TenantContext.getCurrentTenant();
}
/**
* Get the tenant ID for the authenticated user (alias for getClubId).
*/
public UUID getTenantId(org.springframework.security.core.userdetails.UserDetails principal) {
return de.cannamanage.domain.entity.TenantContext.getCurrentTenant();
}
}
@@ -38,5 +38,8 @@ management.endpoint.health.show-details=never
# Session configuration (member portal)
server.servlet.session.timeout=30m
server.servlet.session.cookie.same-site=strict
# Schedulers
cannamanage.schedulers.enabled=${SCHEDULERS_ENABLED:true}
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=${SESSION_COOKIE_SECURE:false}
@@ -0,0 +1,2 @@
-- V22: Add protocol_document_id to assemblies for auto-archive feature
ALTER TABLE assemblies ADD COLUMN IF NOT EXISTS protocol_document_id UUID;
@@ -18,3 +18,6 @@ cannamanage.security.jwt.refresh-token-expiry=2592000
# AOP
spring.aop.auto=true
spring.aop.proxy-target-class=true
# Disable schedulers in tests
cannamanage.schedulers.enabled=false