# Sprint 8 Implementation Plan — Vereinsverwaltung Complete

**Date:** 2026-06-13
**Author:** Patrick Plate / Lumen (Architect)
**Status:** Draft v2 (with legal citations)
**Based on:** `cannamanage-sprint8-analysis.md`
**Sprint Goal:** Club Treasury + General Assembly + Document Archive + Board Management

---

## Legal Framework

This sprint implements core Vereinsverwaltung features. Each feature is grounded in specific German law:

### Vereinsrecht (BGB)

| Section | Topic | Relevance to Sprint 8 |
|---------|-------|----------------------|
| §21 BGB | Nicht wirtschaftlicher Verein | Cannabis Anbauvereinigung operates as e.V. — all features must respect this legal form |
| §25 BGB | Satzung regelt Verfassung | Fee schedules + assembly rules must be configurable per Satzung |
| §26 BGB | Vorstand | Board management feature — legal representation |
| §27 BGB | Bestellung/Abberufung des Vorstands | Board term tracking + elections at MV |
| §30 BGB | Besonderer Vertreter | Board positions beyond §26 Vorstand (Kassenwart, Schriftführer) |
| §32 BGB | Mitgliederversammlung | Assembly feature — decision-making organ |
| §33 BGB | Satzungsänderung | 75% majority for Satzungsänderung, einstimmig for Zweckänderung — vote type modeling |
| §34 BGB | Ausschluss vom Stimmrecht | Member cannot vote on matters concerning themselves |
| §36 BGB | Einberufung der MV | Notice period validation (minimum per Satzung, typically 14 days) |
| §37 BGB | Minderheitsverlangen | 10% of members can force extraordinary MV — future feature hook |
| §38 BGB | Mitgliedschaft | Not transferable, not inheritable — relevant for fee assignment logic |
| §39 BGB | Austritt | Withdrawal with max 2-year notice — affects fee obligation end date |
| §40 BGB | Abdingbarkeit | Which rules can be overridden by Satzung (configurable notice periods, quorum) |
| §56 BGB | Mindestmitgliederzahl | 7 members for founding — validation hook |
| §58 BGB | Satzungsinhalt | Required content including Beitragsregelung |
| §58 Nr. 2 BGB | Beitragsregelung | **Satzung must define member contribution rules** — legal basis for fee schedule feature |
| §67 BGB | Vereinsregister | Mitgliederverzeichnis requirements; Registergericht requires MV protocols for Vorstandsänderungen |
| §259 BGB | Rechenschaftspflicht | **Duty to render accounts** — legal basis for Kassenbuch + financial reports |
| §666 BGB | Auskunftspflicht | **Duty of information to members** — members can demand financial transparency |
| §670 BGB | Aufwendungsersatz | Expense reimbursement — basis for expense tracking feature |

### Abgabenordnung (AO) — Tax Law

| Section | Topic | Relevance |
|---------|-------|-----------|
| §141 AO | Buchführungspflicht | Bookkeeping obligation if revenue > €600,000 or profit > €60,000 (unlikely for most clubs, but system must support it) |
| §143 AO | Aufzeichnungspflicht | Recording obligation for VAT — Kassenbuch must be accurate |
| §147 AO | Aufbewahrungspflichten | **Retention periods: 6 years Handelsbriefe, 10 years Buchungsbelege** — drives document retention policy |

*Note: Cannabis clubs are likely NOT gemeinnützig (§51 AO), so §§51-68 AO typically don't apply. They operate as wirtschaftlicher Verein or ideeller Verein with Nebenzweckprivileg.*

### KCanG (Konsumcannabisgesetz)

| Section | Topic | Relevance |
|---------|-------|-----------|
| §2 KCanG | Definition Anbauvereinigung | Legal definition — validates club model |
| §16 KCanG | Max 500 Mitglieder | Member cap — existing enforcement |
| §17 KCanG | Voraussetzungen der Mitgliedschaft | Only one club per person — existing enforcement |
| §19 KCanG | Weitergabe | Distribution limits (25g/day, 50g/month) — existing enforcement |
| §20 KCanG | Schutz von Kindern und Jugendlichen | Age verification, U21 rules — existing enforcement |
| §22 KCanG | Dokumentationspflichten | **Documentation requirements** — financial records + protocols must be auditable |
| §23 KCanG | Präventionsbeauftragter | Board must include Präventionsbeauftragter — board position feature |
| §24 KCanG | Aufbewahrungsfristen | **5 years after member leaves** — cannabis-specific data retention |
| §26 KCanG | Kontrolle und Überwachung | Regulatory oversight — all records must be producible on demand |

### DSGVO (Data Protection)

| Article | Topic | Relevance |
|---------|-------|-----------|
| Art. 6(1)(b) | Vertragserfüllung | Processing member payment data for contract performance (membership agreement) |
| Art. 6(1)(c) | Rechtliche Verpflichtung | Processing for legal obligation (KCanG §22 documentation, §147 AO retention) |
| Art. 6(1)(f) | Berechtigtes Interesse | Fee collection + reminder sending as legitimate interest |
| Art. 15 | Auskunftsrecht | Members can request all their payment data — portal "Meine Zahlungen" satisfies this |
| Art. 17 | Recht auf Löschung | Right to erasure — BUT overridden by §24 KCanG (5-year retention) and §147 AO (10-year retention) for financial data |

### Retention Policy Matrix (derived from law)

| Data Type | Retention | Legal Basis | Implementation |
|-----------|-----------|-------------|----------------|
| Payment records (Buchungsbelege) | 10 years | §147 Abs. 1 Nr. 4 AO | `payments.created_at` + 10 years |
| Receipts (Quittungen) | 10 years | §147 Abs. 1 Nr. 4 AO | Receipt PDFs retained |
| Financial reports | 10 years | §147 Abs. 1 Nr. 1 AO | Annual report PDFs retained |
| Geschäftsbriefe (payment reminders) | 6 years | §147 Abs. 1 Nr. 2 AO | `payment_reminders` table |
| MV Protocols | 10 years (practical) | §67 BGB (Vereinsregister needs them) | Protocol PDFs in document archive |
| Cannabis-specific member data | 5 years after leaving | §24 KCanG | Soft-delete + retention check |
| Board composition history | Permanent | §67 BGB (Vereinsregister) | Never deleted |

---

## Overview

This plan implements four features in six phases:

```mermaid
graph TD
    P1[Phase 1: Treasury Backend] --> P2[Phase 2: Treasury Frontend + PDF]
    P2 --> P3[Phase 3: Mitgliederversammlung]
    P3 --> P4[Phase 4: Dokumente + Vorstand]
    P4 --> P5[Phase 5: Integration and Polish]
    P5 --> P6[Phase 6: Testing and QA]

    subgraph Existing
        NS[NotificationDispatchService]
        AS[AuditService]
        PDF[OpenPDF - PdfReportGenerator]
        EV[EventService + ClubEvent]
        PT[PlanTierService]
    end

    NS --> P2
    NS --> P3
    AS --> P1
    AS --> P3
    AS --> P4
    PDF --> P2
    PDF --> P3
    EV --> P3
    PT --> P5
```

---

## Phase 1: Treasury Backend

**Legal basis:** §58 Nr. 2 BGB (Satzung must regulate Beiträge), §259 BGB (Rechenschaftspflicht), §666 BGB (Auskunftspflicht), §670 BGB (Aufwendungsersatz), §147 AO (10-year retention for Buchungsbelege), Art. 6(1)(b) DSGVO (contract performance for payment processing), Art. 6(1)(c) DSGVO (legal obligation for financial record-keeping).

Implements fee schedules, payment/expense recording, Kassenbuch calculation, and balance tracking.

### Step 1.1 — Extend Enums

**Files to create/modify:**
- [`PaymentMethod.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/enums/PaymentMethod.java) (new)
- [`FeeInterval.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/enums/FeeInterval.java) (new)
- [`ReminderType.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/enums/ReminderType.java) (new)
- [`StaffPermission.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/enums/StaffPermission.java) (modify)
- [`AuditEventType.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/enums/AuditEventType.java) (modify)

**Approach:**

```java
// PaymentMethod.java
public enum PaymentMethod {
    CASH, BANK_TRANSFER, SEPA, CARD
}

// FeeInterval.java
public enum FeeInterval {
    MONTHLY, QUARTERLY, YEARLY, ONE_TIME
}

// ReminderType.java
public enum ReminderType {
    FIRST, SECOND, FINAL
}
```

Add to `StaffPermission.java`:
```java
// Sprint 8:
MANAGE_FINANCES,       // Record payments/expenses, view balances, generate reports
VIEW_FINANCES,         // Read-only financial access (Kassenprüfer)
MANAGE_ASSEMBLY,       // Create/manage MVs, record attendance/votes
MANAGE_DOCUMENTS       // Upload/manage documents
```

Add to `AuditEventType.java`:
```java
// Sprint 8 — Finance events
PAYMENT_RECORDED,
PAYMENT_VOIDED,
EXPENSE_RECORDED,
EXPENSE_VOIDED,
FEE_SCHEDULE_CREATED,
FEE_SCHEDULE_UPDATED,
FEE_ASSIGNMENT_CHANGED,
PAYMENT_REMINDER_SENT,
FINANCIAL_REPORT_GENERATED,

// Sprint 8 — Assembly events
ASSEMBLY_CREATED,
ASSEMBLY_INVITATIONS_SENT,
ASSEMBLY_STARTED,
ASSEMBLY_COMPLETED,
ASSEMBLY_CANCELLED,
ASSEMBLY_ATTENDANCE_RECORDED,
ASSEMBLY_VOTE_RECORDED,
ASSEMBLY_ELECTION_RECORDED,
ASSEMBLY_PROTOCOL_GENERATED,
ASSEMBLY_PROTOCOL_UPLOADED,

// Sprint 8 — Document events
DOCUMENT_UPLOADED,
DOCUMENT_UPDATED,
DOCUMENT_DELETED,
DOCUMENT_VERSION_CREATED,

// Sprint 8 — Board events
BOARD_MEMBER_APPOINTED,
BOARD_MEMBER_TERM_ENDED
```

**Dependencies:** None (leaf changes)
**Acceptance criteria:** All enums compile, existing functionality unaffected.

---

### Step 1.2 — Flyway Migration V18: Treasury Tables

**Files to create:**
- [`V18__vereinsfinanzen.sql`](cannamanage-api/src/main/resources/db/migration/V18__vereinsfinanzen.sql)

**Content:**

```sql
-- V18: Vereinsfinanzen (Club Treasury)

CREATE TABLE fee_schedules (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    name VARCHAR(100) NOT NULL,
    amount NUMERIC(10,2) NOT NULL,
    interval VARCHAR(20) NOT NULL DEFAULT 'MONTHLY',
    description TEXT,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE member_fee_assignments (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    member_id UUID NOT NULL REFERENCES members(id),
    fee_schedule_id UUID NOT NULL REFERENCES fee_schedules(id),
    effective_from DATE NOT NULL DEFAULT CURRENT_DATE,
    effective_until DATE,
    notes TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE(member_id, fee_schedule_id, effective_from)
);

CREATE TABLE payments (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    member_id UUID NOT NULL REFERENCES members(id),
    amount NUMERIC(10,2) NOT NULL,
    payment_date DATE NOT NULL,
    payment_method VARCHAR(30) NOT NULL,
    reference VARCHAR(255),
    period_from DATE,
    period_to DATE,
    receipt_number VARCHAR(50),
    notes TEXT,
    voided BOOLEAN NOT NULL DEFAULT false,
    voided_reason TEXT,
    voided_at TIMESTAMPTZ,
    recorded_by UUID NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE expense_categories (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    name VARCHAR(100) NOT NULL,
    icon VARCHAR(50),
    sort_order INTEGER NOT NULL DEFAULT 0,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE expenses (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    category_id UUID NOT NULL REFERENCES expense_categories(id),
    amount NUMERIC(10,2) NOT NULL,
    expense_date DATE NOT NULL,
    description VARCHAR(500) NOT NULL,
    receipt_path VARCHAR(500),
    payment_method VARCHAR(30),
    voided BOOLEAN NOT NULL DEFAULT false,
    voided_reason TEXT,
    voided_at TIMESTAMPTZ,
    recorded_by UUID NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE payment_reminders (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    member_id UUID NOT NULL REFERENCES members(id),
    amount_due NUMERIC(10,2) NOT NULL,
    reminder_date DATE NOT NULL DEFAULT CURRENT_DATE,
    reminder_type VARCHAR(30) NOT NULL,
    sent_via VARCHAR(30) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_fee_schedules_tenant ON fee_schedules(tenant_id, is_active);
CREATE INDEX idx_member_fee_assign_member ON member_fee_assignments(member_id);
CREATE INDEX idx_member_fee_assign_tenant ON member_fee_assignments(tenant_id);
CREATE INDEX idx_payments_tenant_date ON payments(tenant_id, payment_date DESC);
CREATE INDEX idx_payments_member ON payments(member_id, payment_date DESC);
CREATE INDEX idx_payments_receipt ON payments(tenant_id, receipt_number);
CREATE INDEX idx_expenses_tenant_date ON expenses(tenant_id, expense_date DESC);
CREATE INDEX idx_expenses_category ON expenses(category_id);
CREATE INDEX idx_expense_categories_tenant ON expense_categories(tenant_id, sort_order);
CREATE INDEX idx_payment_reminders_member ON payment_reminders(member_id);
```

**Dependencies:** V17 must be applied first
**Acceptance criteria:** Migration runs cleanly on PostgreSQL, all indexes created.

---

### Step 1.3 — Domain Entities for Treasury

**Files to create:**
- [`FeeSchedule.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/entity/FeeSchedule.java)
- [`MemberFeeAssignment.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/entity/MemberFeeAssignment.java)
- [`Payment.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/entity/Payment.java)
- [`ExpenseCategory.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/entity/ExpenseCategory.java)
- [`Expense.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/entity/Expense.java)
- [`PaymentReminder.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/entity/PaymentReminder.java)

**Approach:** Follow existing patterns — extend `AbstractTenantEntity`, use JPA annotations, manual getters/setters (matching project style — no Lombok on entities).

```java
@Entity
@Table(name = "fee_schedules")
public class FeeSchedule extends AbstractTenantEntity {

    @Column(name = "name", nullable = false, length = 100)
    private String name;

    @Column(name = "amount", nullable = false, precision = 10, scale = 2)
    private BigDecimal amount;

    @Enumerated(EnumType.STRING)
    @Column(name = "interval", nullable = false, length = 20)
    private FeeInterval interval = FeeInterval.MONTHLY;

    @Column(name = "description", columnDefinition = "TEXT")
    private String description;

    @Column(name = "is_active", nullable = false)
    private boolean active = true;

    @Column(name = "updated_at")
    private Instant updatedAt;

    // getters/setters...
}
```

```java
@Entity
@Table(name = "payments")
public class Payment extends AbstractTenantEntity {

    @Column(name = "member_id", nullable = false)
    private UUID memberId;

    @Column(name = "amount", nullable = false, precision = 10, scale = 2)
    private BigDecimal amount;

    @Column(name = "payment_date", nullable = false)
    private LocalDate paymentDate;

    @Enumerated(EnumType.STRING)
    @Column(name = "payment_method", nullable = false, length = 30)
    private PaymentMethod paymentMethod;

    @Column(name = "reference", length = 255)
    private String reference;

    @Column(name = "period_from")
    private LocalDate periodFrom;

    @Column(name = "period_to")
    private LocalDate periodTo;

    @Column(name = "receipt_number", length = 50)
    private String receiptNumber;

    @Column(name = "notes", columnDefinition = "TEXT")
    private String notes;

    @Column(name = "voided", nullable = false)
    private boolean voided = false;

    @Column(name = "voided_reason", columnDefinition = "TEXT")
    private String voidedReason;

    @Column(name = "voided_at")
    private Instant voidedAt;

    @Column(name = "recorded_by", nullable = false)
    private UUID recordedBy;

    // getters/setters...
}
```

**Dependencies:** Step 1.1 (enums), Step 1.2 (migration)
**Acceptance criteria:** All entities compile, JPA mappings valid.

---

### Step 1.4 — Repositories for Treasury

**Files to create:**
- [`FeeScheduleRepository.java`](cannamanage-service/src/main/java/de/cannamanage/service/repository/FeeScheduleRepository.java)
- [`MemberFeeAssignmentRepository.java`](cannamanage-service/src/main/java/de/cannamanage/service/repository/MemberFeeAssignmentRepository.java)
- [`PaymentRepository.java`](cannamanage-service/src/main/java/de/cannamanage/service/repository/PaymentRepository.java)
- [`ExpenseCategoryRepository.java`](cannamanage-service/src/main/java/de/cannamanage/service/repository/ExpenseCategoryRepository.java)
- [`ExpenseRepository.java`](cannamanage-service/src/main/java/de/cannamanage/service/repository/ExpenseRepository.java)
- [`PaymentReminderRepository.java`](cannamanage-service/src/main/java/de/cannamanage/service/repository/PaymentReminderRepository.java)

**Approach:** Spring Data JPA repositories with custom queries for financial logic.

```java
public interface PaymentRepository extends JpaRepository<Payment, UUID> {

    List<Payment> findByTenantIdAndVoidedFalseOrderByPaymentDateDesc(UUID tenantId);

    List<Payment> findByMemberIdAndVoidedFalseOrderByPaymentDateDesc(UUID memberId);

    @Query("SELECT SUM(p.amount) FROM Payment p WHERE p.memberId = :memberId AND p.voided = false " +
           "AND p.periodFrom >= :from AND p.periodTo <= :to")
    Optional<BigDecimal> sumPaymentsForPeriod(@Param("memberId") UUID memberId,
                                              @Param("from") LocalDate from,
                                              @Param("to") LocalDate to);

    @Query("SELECT p FROM Payment p WHERE p.tenantId = :tenantId AND p.voided = false " +
           "AND p.paymentDate BETWEEN :from AND :to ORDER BY p.paymentDate")
    List<Payment> findByTenantAndDateRange(@Param("tenantId") UUID tenantId,
                                           @Param("from") LocalDate from,
                                           @Param("to") LocalDate to);

    Optional<Payment> findTopByTenantIdAndReceiptNumberStartsWithOrderByReceiptNumberDesc(
            UUID tenantId, String prefix);
}
```

```java
public interface ExpenseRepository extends JpaRepository<Expense, UUID> {

    List<Expense> findByTenantIdAndVoidedFalseOrderByExpenseDateDesc(UUID tenantId);

    @Query("SELECT e FROM Expense e WHERE e.tenantId = :tenantId AND e.voided = false " +
           "AND e.expenseDate BETWEEN :from AND :to ORDER BY e.expenseDate")
    List<Expense> findByTenantAndDateRange(@Param("tenantId") UUID tenantId,
                                            @Param("from") LocalDate from,
                                            @Param("to") LocalDate to);

    @Query("SELECT e.categoryId, SUM(e.amount) FROM Expense e " +
           "WHERE e.tenantId = :tenantId AND e.voided = false " +
           "AND e.expenseDate BETWEEN :from AND :to GROUP BY e.categoryId")
    List<Object[]> sumByCategoryForPeriod(@Param("tenantId") UUID tenantId,
                                          @Param("from") LocalDate from,
                                          @Param("to") LocalDate to);
}
```

**Dependencies:** Step 1.3 (entities)
**Acceptance criteria:** Repositories compile, queries valid JPQL.

---

### Step 1.5 — FinanceService

**Files to create:**
- [`FinanceService.java`](cannamanage-service/src/main/java/de/cannamanage/service/FinanceService.java)

**Approach:** Central service managing fee schedules, payments, expenses, and balance calculations.

```java
@Slf4j
@Service
public class FinanceService {

    private final FeeScheduleRepository feeScheduleRepo;
    private final MemberFeeAssignmentRepository feeAssignmentRepo;
    private final PaymentRepository paymentRepo;
    private final ExpenseRepository expenseRepo;
    private final ExpenseCategoryRepository categoryRepo;
    private final AuditService auditService;

    // Fee schedule CRUD
    public FeeSchedule createFeeSchedule(UUID tenantId, String name, BigDecimal amount,
                                          FeeInterval interval, String description) { ... }
    public FeeSchedule updateFeeSchedule(UUID id, ...) { ... }
    public List<FeeSchedule> listFeeSchedules(UUID tenantId) { ... }

    // Fee assignments
    public MemberFeeAssignment assignFeeToMember(UUID memberId, UUID feeScheduleId,
                                                  LocalDate effectiveFrom) { ... }

    // Payments
    public Payment recordPayment(UUID tenantId, UUID memberId, BigDecimal amount,
                                  LocalDate date, PaymentMethod method, String reference,
                                  LocalDate periodFrom, LocalDate periodTo,
                                  String notes, UUID recordedBy) { ... }
    public void voidPayment(UUID paymentId, String reason, UUID actorId) { ... }

    // Expenses
    public Expense recordExpense(UUID tenantId, UUID categoryId, BigDecimal amount,
                                  LocalDate date, String description, String receiptPath,
                                  PaymentMethod method, UUID recordedBy) { ... }
    public void voidExpense(UUID expenseId, String reason, UUID actorId) { ... }

    // Balance calculation
    public MemberBalance calculateMemberBalance(UUID memberId) { ... }
    public List<MemberBalance> calculateAllBalances(UUID tenantId) { ... }

    // Receipt number generation: CM-{year}-{seq:06d}
    private String generateReceiptNumber(UUID tenantId) { ... }
}
```

Key logic for `calculateMemberBalance`:
```java
public MemberBalance calculateMemberBalance(UUID memberId) {
    MemberFeeAssignment assignment = feeAssignmentRepo.findActiveFeeAssignment(memberId)
            .orElse(null);
    if (assignment == null) return MemberBalance.noFeeAssigned(memberId);

    FeeSchedule schedule = feeScheduleRepo.findById(assignment.getFeeScheduleId()).orElseThrow();

    // Calculate total due from effective_from to today
    LocalDate from = assignment.getEffectiveFrom();
    LocalDate to = LocalDate.now();
    BigDecimal totalDue = calculateTotalDue(schedule, from, to);

    // Calculate total paid
    BigDecimal totalPaid = paymentRepo.sumPaymentsForMember(memberId).orElse(BigDecimal.ZERO);

    BigDecimal outstanding = totalDue.subtract(totalPaid);
    LocalDate lastPaymentDate = paymentRepo.findLastPaymentDate(memberId).orElse(null);

    return new MemberBalance(memberId, schedule.getName(), totalDue, totalPaid,
                             outstanding, lastPaymentDate);
}
```

**Dependencies:** Step 1.4 (repositories), existing `AuditService`
**Acceptance criteria:** All CRUD operations work, balance calculation accurate, receipt numbers sequential.

---

### Step 1.6 — KassenbuchService

**Files to create:**
- [`KassenbuchService.java`](cannamanage-service/src/main/java/de/cannamanage/service/KassenbuchService.java)

**Approach:** Generates the Kassenbuch view — chronological list of all transactions with running balance.

```java
@Slf4j
@Service
public class KassenbuchService {

    public KassenbuchView generateKassenbuch(UUID tenantId, LocalDate from, LocalDate to) {
        List<Payment> payments = paymentRepo.findByTenantAndDateRange(tenantId, from, to);
        List<Expense> expenses = expenseRepo.findByTenantAndDateRange(tenantId, from, to);

        // Merge into single timeline, calculate running balance
        List<KassenbuchEntry> entries = mergeAndSort(payments, expenses);
        BigDecimal runningBalance = calculateOpeningBalance(tenantId, from);

        for (KassenbuchEntry entry : entries) {
            if (entry.isIncome()) {
                runningBalance = runningBalance.add(entry.getAmount());
            } else {
                runningBalance = runningBalance.subtract(entry.getAmount());
            }
            entry.setRunningBalance(runningBalance);
        }

        return new KassenbuchView(entries, runningBalance);
    }

    public String exportCsv(UUID tenantId, LocalDate from, LocalDate to) {
        // Generate CSV with columns: Datum;Typ;Beschreibung;Einnahme;Ausgabe;Saldo
        ...
    }
}
```

**Dependencies:** Step 1.5 (FinanceService)
**Acceptance criteria:** Kassenbuch shows correct running balance, CSV export produces valid file.

---

### Step 1.7 — Finance REST Controller

**Files to create:**
- [`FinanceController.java`](cannamanage-api/src/main/java/de/cannamanage/api/controller/FinanceController.java)

**Approach:** REST endpoints for all treasury operations. Protected by `MANAGE_FINANCES` permission.

```java
@RestController
@RequestMapping("/api/finance")
@RequiredArgsConstructor
public class FinanceController {

    @GetMapping("/fee-schedules")
    public ResponseEntity<List<FeeScheduleDto>> listFeeSchedules() { ... }

    @PostMapping("/fee-schedules")
    public ResponseEntity<FeeScheduleDto> createFeeSchedule(@Valid @RequestBody CreateFeeScheduleRequest req) { ... }

    @PostMapping("/payments")
    public ResponseEntity<PaymentDto> recordPayment(@Valid @RequestBody RecordPaymentRequest req) { ... }

    @PostMapping("/payments/{id}/void")
    public ResponseEntity<Void> voidPayment(@PathVariable UUID id, @RequestBody VoidRequest req) { ... }

    @GetMapping("/balances")
    public ResponseEntity<List<MemberBalanceDto>> getAllBalances() { ... }

    @GetMapping("/kassenbuch")
    public ResponseEntity<KassenbuchViewDto> getKassenbuch(
            @RequestParam @DateTimeFormat(iso = ISO.DATE) LocalDate from,
            @RequestParam @DateTimeFormat(iso = ISO.DATE) LocalDate to) { ... }

    @GetMapping("/kassenbuch/export")
    public ResponseEntity<byte[]> exportCsv(
            @RequestParam @DateTimeFormat(iso = ISO.DATE) LocalDate from,
            @RequestParam @DateTimeFormat(iso = ISO.DATE) LocalDate to) { ... }

    @GetMapping("/reports/annual/{year}")
    public ResponseEntity<AnnualReportDto> getAnnualReport(@PathVariable int year) { ... }
}
```

**Dependencies:** Step 1.5, Step 1.6
**Acceptance criteria:** All endpoints return correct data, permission checks enforced.

---

### Step 1.8 — Portal Finance Endpoints

**Files to modify:**
- [`PortalController.java`](cannamanage-api/src/main/java/de/cannamanage/api/controller/PortalController.java) (or new `PortalFinanceController.java`)

**Approach:** Member-facing endpoints showing their own payment history and balance.

```java
@RestController
@RequestMapping("/api/portal/finance")
public class PortalFinanceController {

    @GetMapping("/my-payments")
    public ResponseEntity<List<PaymentDto>> myPayments() {
        UUID memberId = getCurrentMemberId();
        return ResponseEntity.ok(financeService.getPaymentsForMember(memberId));
    }

    @GetMapping("/my-balance")
    public ResponseEntity<MemberBalanceDto> myBalance() {
        UUID memberId = getCurrentMemberId();
        return ResponseEntity.ok(financeService.calculateMemberBalance(memberId).toDto());
    }

    @GetMapping("/receipts/{paymentId}")
    public ResponseEntity<byte[]> downloadReceipt(@PathVariable UUID paymentId) {
        // Verify payment belongs to current member
        ...
    }
}
```

**Dependencies:** Step 1.7
**Acceptance criteria:** Members see only their own data, cannot access other members' payments.

---

## Phase 2: Treasury Frontend + PDF

**Legal basis:** §259 BGB (Rechenschaftspflicht — members have right to financial transparency), §666 BGB (Auskunftspflicht — board must provide information on request), §147 AO (Quittungen must be retained 10 years), Art. 15 DSGVO (Auskunftsrecht — portal "Meine Zahlungen" satisfies member's right of access to their payment data), standard Vereinspraxis (member has Anspruch auf Beitragsquittung).

### Step 2.1 — Receipt PDF Generation

**Files to create:**
- [`ReceiptPdfService.java`](cannamanage-service/src/main/java/de/cannamanage/service/ReceiptPdfService.java)

**Approach:** Generate Quittung PDF using OpenPDF (same pattern as existing `PdfReportGenerator`).

```java
@Slf4j
@Service
public class ReceiptPdfService {

    public byte[] generateReceipt(Payment payment, Member member, Club club) {
        Document document = new Document(PageSize.A4, 50, 50, 50, 50);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PdfWriter.getInstance(document, baos);
        document.open();

        // Header: Club name + address
        // Title: "Quittung Nr. CM-2026-000001"
        // Body: Member name, amount, date, payment method, period covered
        // Footer: Signature line, date, club stamp placeholder

        document.close();
        return baos.toByteArray();
    }
}
```

Receipt PDF layout:
```
┌─────────────────────────────────────────┐
│  [Club Name]                            │
│  [Club Address]                         │
│                                         │
│  QUITTUNG                               │
│  Nr. CM-2026-000001                     │
│                                         │
│  Erhalten von:  [Member Name]           │
│  Betrag:        30,00 €                 │
│  Datum:         01.07.2026              │
│  Zahlungsart:   Überweisung             │
│  Zeitraum:      Juli 2026               │
│  Verwendung:    Mitgliedsbeitrag        │
│                                         │
│  _______________    _______________     │
│  Datum              Unterschrift         │
│                     Kassenwart           │
└─────────────────────────────────────────┘
```

**Dependencies:** Step 1.5, existing OpenPDF library
**Acceptance criteria:** Valid PDF generated, correct German locale formatting (Euro, dates).

---

### Step 2.2 — Annual Financial Report PDF

**Files to create:**
- [`FinancialReportService.java`](cannamanage-service/src/main/java/de/cannamanage/service/FinancialReportService.java)

**Approach:** Generate Jahresabschluss PDF with summary tables.

Content:
- Title: "Jahresabschluss {year} — {Club Name}"
- Summary: Total income, total expenses, net balance
- Income breakdown: by month (table)
- Expense breakdown: by category (table)
- Running balance chart (month by month)
- Member payment summary: number of members, total received, outstanding
- Footer: Generated date, "Erstellt mit CannaManage"

**Dependencies:** Step 1.6 (KassenbuchService), OpenPDF
**Acceptance criteria:** Annual report PDF matches expected format, all numbers correct.

---

### Step 2.3 — Frontend: Finance Admin Pages

**Files to create:**
- [`cannamanage-frontend/src/app/(dashboard-layout)/finance/page.tsx`](cannamanage-frontend/src/app/(dashboard-layout)/finance/page.tsx) — Main finance dashboard
- [`cannamanage-frontend/src/app/(dashboard-layout)/finance/payments/page.tsx`](cannamanage-frontend/src/app/(dashboard-layout)/finance/payments/page.tsx) — Payment list
- [`cannamanage-frontend/src/app/(dashboard-layout)/finance/expenses/page.tsx`](cannamanage-frontend/src/app/(dashboard-layout)/finance/expenses/page.tsx) — Expense list
- [`cannamanage-frontend/src/app/(dashboard-layout)/finance/balances/page.tsx`](cannamanage-frontend/src/app/(dashboard-layout)/finance/balances/page.tsx) — Balance overview
- [`cannamanage-frontend/src/app/(dashboard-layout)/finance/reports/page.tsx`](cannamanage-frontend/src/app/(dashboard-layout)/finance/reports/page.tsx) — Report generation
- [`cannamanage-frontend/src/app/(dashboard-layout)/settings/finance/page.tsx`](cannamanage-frontend/src/app/(dashboard-layout)/settings/finance/page.tsx) — Fee schedules + categories config
- [`cannamanage-frontend/src/services/finance.ts`](cannamanage-frontend/src/services/finance.ts) — API client
- [`cannamanage-frontend/src/data/mock/finance.ts`](cannamanage-frontend/src/data/mock/finance.ts) — Mock data

**Finance Dashboard layout:**
```
┌──────────────────────────────────────────────────┐
│  Vereinsfinanzen                                  │
│                                                   │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐          │
│  │ Einnahmen│  │ Ausgaben │  │  Saldo  │          │
│  │ 4.230€  │  │ 2.180€  │  │ 2.050€  │          │
│  │ diesen M.│  │ diesen M.│  │         │          │
│  └─────────┘  └─────────┘  └─────────┘          │
│                                                   │
│  Tabs: [Kassenbuch] [Zahlungen] [Ausgaben]       │
│         [Offene Beiträge] [Berichte]             │
│                                                   │
│  ┌───────────────────────────────────────┐       │
│  │ Kassenbuch                             │       │
│  │ Datum    | Typ      | Beschreibung | +/- | Saldo│
│  │ 01.07   | Einnahme | Beitrag M.X  | +30 | 2050│
│  │ 28.06   | Ausgabe  | Strom Juni   | -85 | 2020│
│  │ ...                                    │       │
│  └───────────────────────────────────────┘       │
└──────────────────────────────────────────────────┘
```

**Dependencies:** Step 1.7 (API endpoints)
**Acceptance criteria:** All finance pages render, data fetched from API, responsive design.

---

### Step 2.4 — Frontend: Portal Finance View

**Files to create:**
- [`cannamanage-frontend/src/app/(portal-layout)/portal/finance/page.tsx`](cannamanage-frontend/src/app/(portal-layout)/portal/finance/page.tsx)

**Layout:**
```
┌────────────────────────────────────────┐
│  Meine Zahlungen                        │
│                                         │
│  ┌────────────┐  ┌────────────┐        │
│  │ Saldo      │  │ Nächster   │        │
│  │ 0,00 €     │  │ Beitrag    │        │
│  │ ✅ Aktuell │  │ 01.08.2026 │        │
│  └────────────┘  └────────────┘        │
│                                         │
│  Zahlungshistorie:                      │
│  ┌─────────────────────────────┐       │
│  │ 01.07.2026 | 30€ | ✅ | 📄  │       │
│  │ 01.06.2026 | 30€ | ✅ | 📄  │       │
│  │ 01.05.2026 | 30€ | ✅ | 📄  │       │
│  └─────────────────────────────┘       │
│  📄 = Download Quittung                 │
└────────────────────────────────────────┘
```

**Dependencies:** Step 1.8 (portal API), Step 2.1 (receipt PDF)
**Acceptance criteria:** Members see their balance and payment history, can download receipts.

---

### Step 2.5 — Navigation Updates

**Files to modify:**
- [`navigations.ts`](cannamanage-frontend/src/data/navigations.ts)

**Approach:** Add finance section to admin nav and portal nav.

Admin nav additions:
```typescript
{
  title: 'Finanzen',
  icon: 'wallet',
  path: '/finance',
  permission: 'MANAGE_FINANCES',
  children: [
    { title: 'Kassenbuch', path: '/finance' },
    { title: 'Zahlungen', path: '/finance/payments' },
    { title: 'Ausgaben', path: '/finance/expenses' },
    { title: 'Offene Beiträge', path: '/finance/balances' },
    { title: 'Berichte', path: '/finance/reports' },
  ]
}
```

Portal nav additions:
```typescript
{ title: 'Meine Zahlungen', icon: 'receipt', path: '/portal/finance' }
```

**Dependencies:** All Phase 2 pages
**Acceptance criteria:** Navigation renders correctly, permission-gated.

---

## Phase 3: Mitgliederversammlung (General Assembly)

**Legal basis:** §32 BGB (MV as decision-making organ — Beschlussfassung), §33 BGB (75% majority for Satzungsänderung, unanimous for Zweckänderung — modeled as VoteType), §34 BGB (exclusion from voting on own matters), §36 BGB (Einberufung — notice period enforcement), §37 BGB (Minderheitsverlangen — 10% can force extraordinary MV), §40 BGB (Satzung can override quorum/notice defaults), §67 BGB (Registergericht requires MV protocols for Vorstandsänderungen — protocol generation + archival), §27 BGB (Bestellung/Abberufung des Vorstands at MV — election feature), §22 KCanG (Dokumentationspflichten — all decisions auditable).

### Step 3.1 — Flyway Migration V19: Assembly Tables

**Files to create:**
- [`V19__mitgliederversammlung.sql`](cannamanage-api/src/main/resources/db/migration/V19__mitgliederversammlung.sql)

**Content:** As defined in analysis document — `general_assemblies`, `assembly_agenda_items`, `assembly_attendance`, `assembly_votes`, `assembly_elections` tables with all indexes.

**Dependencies:** V18
**Acceptance criteria:** Migration runs cleanly.

---

### Step 3.2 — Assembly Enums + Entities

**Files to create:**
- [`AssemblyType.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/enums/AssemblyType.java) — `ORDINARY`, `EXTRAORDINARY`
- [`AssemblyStatus.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/enums/AssemblyStatus.java) — `DRAFT`, `INVITED`, `IN_PROGRESS`, `COMPLETED`, `CANCELLED`
- [`AgendaItemType.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/enums/AgendaItemType.java) — `DISCUSSION`, `VOTE`, `ELECTION`, `REPORT`, `OTHER`
- [`VoteType.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/enums/VoteType.java) — `SIMPLE_MAJORITY`, `TWO_THIRDS`, `UNANIMOUS`
- [`VoteResult.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/enums/VoteResult.java) — `ACCEPTED`, `REJECTED`, `TABLED`
- [`GeneralAssembly.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/entity/GeneralAssembly.java)
- [`AssemblyAgendaItem.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/entity/AssemblyAgendaItem.java)
- [`AssemblyAttendance.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/entity/AssemblyAttendance.java)
- [`AssemblyVote.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/entity/AssemblyVote.java)
- [`AssemblyElection.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/entity/AssemblyElection.java)

**Approach:** Standard JPA entities, following existing patterns.

```java
@Entity
@Table(name = "general_assemblies", indexes = {
    @Index(name = "idx_assemblies_tenant", columnList = "tenant_id, scheduled_at"),
    @Index(name = "idx_assemblies_status", columnList = "tenant_id, status")
})
public class GeneralAssembly extends AbstractTenantEntity {

    @Column(name = "club_id", nullable = false)
    private UUID clubId;

    @Column(name = "title", nullable = false, length = 200)
    private String title;

    @Enumerated(EnumType.STRING)
    @Column(name = "assembly_type", nullable = false, length = 30)
    private AssemblyType assemblyType = AssemblyType.ORDINARY;

    @Column(name = "scheduled_at", nullable = false)
    private Instant scheduledAt;

    @Column(name = "location", nullable = false, length = 300)
    private String location;

    @Column(name = "notice_period_days", nullable = false)
    private int noticePeriodDays = 14;

    @Column(name = "quorum_percentage", nullable = false)
    private int quorumPercentage = 50;

    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false, length = 30)
    private AssemblyStatus status = AssemblyStatus.DRAFT;

    @Column(name = "invitation_sent_at")
    private Instant invitationSentAt;

    @Column(name = "started_at")
    private Instant startedAt;

    @Column(name = "ended_at")
    private Instant endedAt;

    @Column(name = "notes", columnDefinition = "TEXT")
    private String notes;

    @Column(name = "protocol_document_id")
    private UUID protocolDocumentId;

    @Column(name = "event_id")
    private UUID eventId;

    @Column(name = "created_by", nullable = false)
    private UUID createdBy;

    @OneToMany(mappedBy = "assemblyId", cascade = CascadeType.ALL)
    @OrderBy("sortOrder ASC")
    private List<AssemblyAgendaItem> agendaItems = new ArrayList<>();

    // getters/setters...
}
```

**Dependencies:** Step 3.1
**Acceptance criteria:** All entities compile, relationships mapped correctly.

---

### Step 3.3 — AssemblyService

**Files to create:**
- [`AssemblyService.java`](cannamanage-service/src/main/java/de/cannamanage/service/AssemblyService.java)

**Approach:** Manages the MV lifecycle: create → invite → start → complete.

Key methods:
```java
@Slf4j
@Service
@Transactional
public class AssemblyService {

    // Lifecycle
    public GeneralAssembly createAssembly(CreateAssemblyRequest req) { ... }
    public void sendInvitations(UUID assemblyId) { ... }  // transitions DRAFT → INVITED
    public void startAssembly(UUID assemblyId) { ... }    // transitions INVITED → IN_PROGRESS
    public void completeAssembly(UUID assemblyId) { ... } // transitions IN_PROGRESS → COMPLETED
    public void cancelAssembly(UUID assemblyId) { ... }

    // Agenda
    public AssemblyAgendaItem addAgendaItem(UUID assemblyId, ...) { ... }
    public void reorderAgendaItems(UUID assemblyId, List<UUID> orderedIds) { ... }

    // Attendance
    public void checkInMember(UUID assemblyId, UUID memberId, UUID checkedInBy) { ... }
    public void removeCheckIn(UUID assemblyId, UUID memberId) { ... }
    public QuorumStatus getQuorumStatus(UUID assemblyId) { ... }

    // Voting
    public AssemblyVote createVote(UUID assemblyId, ...) { ... }
    public void recordVoteResult(UUID voteId, int yes, int no, int abstain) { ... }

    // Elections
    public void recordElection(UUID assemblyId, String position, UUID electedMemberId,
                               Integer voteCount, LocalDate termStart, LocalDate termEnd) { ... }
}
```

`sendInvitations` logic:
```java
public void sendInvitations(UUID assemblyId) {
    GeneralAssembly assembly = assemblyRepo.findById(assemblyId).orElseThrow();
    if (assembly.getStatus() != AssemblyStatus.DRAFT) {
        throw new IllegalStateException("Can only invite from DRAFT status");
    }

    // Validate notice period
    long daysUntil = ChronoUnit.DAYS.between(Instant.now(), assembly.getScheduledAt());
    if (daysUntil < assembly.getNoticePeriodDays()) {
        log.warn("Notice period not met: {} days until MV, {} required",
                 daysUntil, assembly.getNoticePeriodDays());
        // Still allow — just warn (club can override)
    }

    // Create corresponding ClubEvent (integrates with Sprint 7 event calendar)
    ClubEvent event = eventService.createEvent(assembly.getClubId(), assembly.getTitle(),
            assembly.formatDescription(), EventType.GENERAL_ASSEMBLY,
            assembly.getScheduledAt(), null, assembly.getLocation(), null,
            assembly.getCreatedBy());
    assembly.setEventId(event.getId());

    // Send notification to all active members
    notificationDispatchService.broadcast(assembly.getTenantId(),
            NotificationType.ASSEMBLY_INVITATION,
            "Einladung zur " + assembly.getTitle(),
            formatInvitationBody(assembly));

    assembly.setStatus(AssemblyStatus.INVITED);
    assembly.setInvitationSentAt(Instant.now());
    assemblyRepo.save(assembly);

    auditService.log(AuditEventType.ASSEMBLY_INVITATIONS_SENT, ...);
}
```

`getQuorumStatus` logic:
```java
public QuorumStatus getQuorumStatus(UUID assemblyId) {
    GeneralAssembly assembly = assemblyRepo.findById(assemblyId).orElseThrow();
    long attendeeCount = attendanceRepo.countByAssemblyId(assemblyId);
    long totalActiveMembers = memberRepo.countByTenantIdAndStatus(
            assembly.getTenantId(), MemberStatus.ACTIVE);

    int percentage = (int) (attendeeCount * 100 / totalActiveMembers);
    boolean quorumReached = percentage >= assembly.getQuorumPercentage();

    return new QuorumStatus(attendeeCount, totalActiveMembers,
                            percentage, assembly.getQuorumPercentage(), quorumReached);
}
```

**Dependencies:** Step 3.2, existing `EventService`, `NotificationDispatchService`, `AuditService`
**Acceptance criteria:** Full MV lifecycle works, quorum calculation correct, invitations sent via notification system.

---

### Step 3.4 — Protocol PDF Generation

**Files to create:**
- [`ProtocolPdfService.java`](cannamanage-service/src/main/java/de/cannamanage/service/ProtocolPdfService.java)

**Approach:** Generate official MV protocol (Protokoll) as PDF.

Protocol structure:
```
PROTOKOLL
der ordentlichen Mitgliederversammlung
des [Club Name] e.V.

Datum:    [date]
Ort:      [location]
Beginn:   [start time]
Ende:     [end time]

Anwesende Mitglieder: [count] von [total] ([percentage]%)
Beschlussfähigkeit:   ✅ Erreicht / ❌ Nicht erreicht

TAGESORDNUNG:
1. [item 1]
2. [item 2]
...

BESCHLÜSSE:
TOP 5: [vote title]
  Ergebnis: Angenommen (Ja: 42, Nein: 3, Enthaltungen: 5)

WAHLEN:
1. Vorsitzende/r: [Name] (gewählt mit [X] Stimmen)
Kassenwart/in: [Name] (gewählt mit [X] Stimmen)

ANWESENHEITSLISTE:
[numbered list of all attendees]

___________________          ___________________
Versammlungsleiter/in        Schriftführer/in
```

**Dependencies:** Step 3.3, OpenPDF
**Acceptance criteria:** PDF generated with all required sections, German formatting.

---

### Step 3.5 — Assembly REST Controller

**Files to create:**
- [`AssemblyController.java`](cannamanage-api/src/main/java/de/cannamanage/api/controller/AssemblyController.java)

**Approach:** Full CRUD + lifecycle endpoints as defined in analysis. Protected by `MANAGE_ASSEMBLY` permission.

**Dependencies:** Step 3.3, Step 3.4
**Acceptance criteria:** All endpoints functional, proper status transitions enforced.

---

### Step 3.6 — Frontend: Assembly Pages

**Files to create:**
- [`cannamanage-frontend/src/app/(dashboard-layout)/assemblies/page.tsx`](cannamanage-frontend/src/app/(dashboard-layout)/assemblies/page.tsx) — MV list
- [`cannamanage-frontend/src/app/(dashboard-layout)/assemblies/new/page.tsx`](cannamanage-frontend/src/app/(dashboard-layout)/assemblies/new/page.tsx) — Create MV
- [`cannamanage-frontend/src/app/(dashboard-layout)/assemblies/[id]/page.tsx`](cannamanage-frontend/src/app/(dashboard-layout)/assemblies/[id]/page.tsx) — MV detail (tabs)
- [`cannamanage-frontend/src/app/(dashboard-layout)/assemblies/[id]/live/page.tsx`](cannamanage-frontend/src/app/(dashboard-layout)/assemblies/[id]/live/page.tsx) — Live management
- [`cannamanage-frontend/src/app/(portal-layout)/portal/assemblies/page.tsx`](cannamanage-frontend/src/app/(portal-layout)/portal/assemblies/page.tsx) — Portal: upcoming + past MVs
- [`cannamanage-frontend/src/services/assemblies.ts`](cannamanage-frontend/src/services/assemblies.ts) — API client
- [`cannamanage-frontend/src/data/mock/assemblies.ts`](cannamanage-frontend/src/data/mock/assemblies.ts) — Mock data

**Live management page layout:**
```
┌─────────────────────────────────────────────────────┐
│  🟢 MV Aktiv — Ordentliche MV 2026                  │
│                                                      │
│  ┌──────────────┐  ┌──────────────┐                 │
│  │ Anwesend     │  │ Quorum       │                 │
│  │ 38 / 72      │  │ 52% ✅       │                 │
│  │ Mitglieder   │  │ von 50% nötig│                 │
│  └──────────────┘  └──────────────┘                 │
│                                                      │
│  Tabs: [Anwesenheit] [Abstimmungen] [Wahlen]        │
│                                                      │
│  Anwesenheit:                                        │
│  🔍 [Mitglied suchen...]                            │
│  ┌─────────────────────────────┐                    │
│  │ ☑ Max Mustermann  | 19:02  │                    │
│  │ ☑ Erika Muster    | 19:05  │                    │
│  │ ☐ Hans Dampf      |        │                    │
│  └─────────────────────────────┘                    │
│                                                      │
│  [📄 Protokoll generieren]  [✅ MV beenden]          │
└─────────────────────────────────────────────────────┘
```

**Dependencies:** Step 3.5 (API)
**Acceptance criteria:** Full MV flow works from creation to protocol generation.

---

## Phase 4: Dokumentenarchiv + Vorstandsverwaltung

**Legal basis (Documents):** §147 AO (Aufbewahrungspflichten — 6/10 year retention), §24 KCanG (5-year retention for cannabis-specific data after member leaves), §26 KCanG (Kontrolle — records producible on regulatory demand), §22 KCanG (Dokumentationspflichten), Art. 6(1)(c) DSGVO (processing for legal obligation — retention requirements override Art. 17 erasure right).

**Legal basis (Board):** §26 BGB (Vorstand — legal representation, minimum 1 person), §27 BGB (Bestellung und Abberufung — appointment/dismissal by MV), §30 BGB (Besonderer Vertreter — additional positions like Kassenwart, Schriftführer), §67 BGB (Vereinsregister — board composition changes must be registered), §23 KCanG (Präventionsbeauftragter — mandatory board position for cannabis clubs).

### Step 4.1 — Flyway Migrations V20 + V21

**Files to create:**
- [`V20__dokumentenarchiv.sql`](cannamanage-api/src/main/resources/db/migration/V20__dokumentenarchiv.sql)
- [`V21__vorstandsverwaltung.sql`](cannamanage-api/src/main/resources/db/migration/V21__vorstandsverwaltung.sql)

**Content:** As defined in analysis document.

**Dependencies:** V19
**Acceptance criteria:** Migrations run cleanly.

---

### Step 4.2 — Document Entities + Enums

**Files to create:**
- [`DocumentAccessLevel.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/enums/DocumentAccessLevel.java) — `ALL_MEMBERS`, `BOARD_ONLY`
- [`DocumentSource.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/enums/DocumentSource.java) — `MANUAL`, `SYSTEM_MV_PROTOCOL`, `SYSTEM_FINANCIAL_REPORT`
- [`Document.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/entity/Document.java)
- [`DocumentCategory.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/entity/DocumentCategory.java)

**Dependencies:** Step 4.1
**Acceptance criteria:** Entities compile.

---

### Step 4.3 — DocumentStorageService (Filesystem)

**Files to create:**
- [`DocumentStorageService.java`](cannamanage-service/src/main/java/de/cannamanage/service/DocumentStorageService.java)

**Approach:** Handles the actual filesystem operations — store, retrieve, delete files.

```java
@Slf4j
@Service
public class DocumentStorageService {

    @Value("${cannamanage.storage.base-path:/uploads}")
    private String basePath;

    private static final long MAX_FILE_SIZE = 20 * 1024 * 1024; // 20MB
    private static final Set<String> ALLOWED_TYPES = Set.of(
        "application/pdf", "application/msword",
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        "application/vnd.ms-excel",
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        "image/png", "image/jpeg"
    );

    public StoredFile store(UUID tenantId, UUID documentId, MultipartFile file) {
        validateFile(file);
        Path targetDir = Path.of(basePath, "documents", tenantId.toString(), documentId.toString());
        Files.createDirectories(targetDir);
        Path targetFile = targetDir.resolve(file.getOriginalFilename());
        file.transferTo(targetFile);
        return new StoredFile(targetFile.toString(), file.getSize());
    }

    public byte[] retrieve(String storagePath) {
        return Files.readAllBytes(Path.of(storagePath));
    }

    public void delete(String storagePath) {
        Files.deleteIfExists(Path.of(storagePath));
    }

    private void validateFile(MultipartFile file) {
        if (file.getSize() > MAX_FILE_SIZE) throw new FileTooLargeException(...);
        if (!ALLOWED_TYPES.contains(file.getContentType())) throw new UnsupportedFileTypeException(...);
    }
}
```

**Dependencies:** None (uses Spring `@Value` for path configuration)
**Acceptance criteria:** Files stored in correct directory structure, validation works.

---

### Step 4.4 — DocumentService + Controller

**Files to create:**
- [`DocumentService.java`](cannamanage-service/src/main/java/de/cannamanage/service/DocumentService.java)
- [`DocumentController.java`](cannamanage-api/src/main/java/de/cannamanage/api/controller/DocumentController.java)

**Approach:** CRUD for document metadata + multipart upload handling.

```java
@RestController
@RequestMapping("/api/documents")
public class DocumentController {

    @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<DocumentDto> uploadDocument(
            @RequestPart("file") MultipartFile file,
            @RequestPart("metadata") CreateDocumentRequest metadata) {
        Document doc = documentService.uploadDocument(file, metadata);
        return ResponseEntity.status(HttpStatus.CREATED).body(doc.toDto());
    }

    @GetMapping("/{id}/download")
    public ResponseEntity<Resource> downloadDocument(@PathVariable UUID id) {
        Document doc = documentService.getDocument(id);
        byte[] content = storageService.retrieve(doc.getStoragePath());
        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(doc.getContentType()))
                .header(HttpHeaders.CONTENT_DISPOSITION,
                        "attachment; filename=\"" + doc.getFilename() + "\"")
                .body(new ByteArrayResource(content));
    }
}
```

**Dependencies:** Step 4.2, Step 4.3
**Acceptance criteria:** Upload, download, delete all work. Tenant isolation enforced.

---

### Step 4.5 — Board Entities + Service

**Files to create:**
- [`BoardPosition.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/entity/BoardPosition.java)
- [`BoardMember.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/entity/BoardMember.java)
- [`BoardService.java`](cannamanage-service/src/main/java/de/cannamanage/service/BoardService.java)
- [`BoardController.java`](cannamanage-api/src/main/java/de/cannamanage/api/controller/BoardController.java)

**BoardService key methods:**
```java
@Slf4j
@Service
public class BoardService {

    public List<BoardPosition> getPositions(UUID tenantId) { ... }
    public BoardPosition createPosition(UUID tenantId, String name, boolean required, int maxHolders) { ... }

    public BoardMember appointMember(UUID positionId, UUID memberId,
                                      LocalDate termStart, LocalDate termEnd,
                                      UUID assemblyId) { ... }

    public void endTerm(UUID boardMemberId, String reason) { ... }

    public List<BoardMember> getCurrentBoard(UUID tenantId) { ... }

    public List<BoardMember> getBoardHistory(UUID tenantId) { ... }

    // Called by MV elections
    public void applyElectionResult(AssemblyElection election) {
        // End current holder's term
        // Appoint new member
    }
}
```

**Dependencies:** Step 4.1 (V21 migration), Step 3.3 (linked from elections)
**Acceptance criteria:** Board composition trackable, history preserved.

---

### Step 4.6 — Frontend: Documents + Board Pages

**Files to create:**
- [`cannamanage-frontend/src/app/(dashboard-layout)/documents/page.tsx`](cannamanage-frontend/src/app/(dashboard-layout)/documents/page.tsx) — Document archive
- [`cannamanage-frontend/src/app/(dashboard-layout)/board/page.tsx`](cannamanage-frontend/src/app/(dashboard-layout)/board/page.tsx) — Board management
- [`cannamanage-frontend/src/app/(portal-layout)/portal/documents/page.tsx`](cannamanage-frontend/src/app/(portal-layout)/portal/documents/page.tsx) — Portal doc access
- [`cannamanage-frontend/src/app/(portal-layout)/portal/board/page.tsx`](cannamanage-frontend/src/app/(portal-layout)/portal/board/page.tsx) — Portal board view
- [`cannamanage-frontend/src/services/documents.ts`](cannamanage-frontend/src/services/documents.ts)
- [`cannamanage-frontend/src/services/board.ts`](cannamanage-frontend/src/services/board.ts)

**Dependencies:** Step 4.4, Step 4.5
**Acceptance criteria:** Upload/download works, board displayed correctly.

---

## Phase 5: Integration & Polish

**Legal basis:** §259 BGB + §666 BGB (payment reminders serve the Verein's legitimate interest in collecting dues owed per Satzung — Art. 6(1)(f) DSGVO), §27 BGB (term expiry notifications ensure continuity of legal representation per §26 BGB), §147 AO (auto-archive ensures 10-year retention compliance for system-generated financial documents), §22 KCanG + §26 KCanG (all auto-archived protocols satisfy documentation and oversight requirements).

### Step 5.1 — Payment Reminder Scheduler

**Files to create:**
- [`PaymentReminderScheduler.java`](cannamanage-service/src/main/java/de/cannamanage/service/PaymentReminderScheduler.java)

**Approach:** Scheduled job that checks for overdue members and sends reminders via the notification system.

```java
@Slf4j
@Service
public class PaymentReminderScheduler {

    @Scheduled(cron = "0 0 9 * * MON")  // Every Monday at 9:00 AM
    public void checkOverduePayments() {
        List<Club> clubs = clubRepo.findAllActive();
        for (Club club : clubs) {
            PlanTier tier = planTierService.getClubTier(club.getId());
            if (tier == PlanTier.STARTER || tier == PlanTier.TRIAL) continue; // Pro+ only

            List<MemberBalance> overdue = financeService.getOverdueMembers(club.getTenantId());
            for (MemberBalance balance : overdue) {
                ReminderType type = determineReminderType(balance);
                sendReminder(balance, type);
            }
        }
    }

    private ReminderType determineReminderType(MemberBalance balance) {
        long daysOverdue = ChronoUnit.DAYS.between(balance.getOverdueSince(), LocalDate.now());
        if (daysOverdue > 60) return ReminderType.FINAL;
        if (daysOverdue > 30) return ReminderType.SECOND;
        return ReminderType.FIRST;
    }
}
```

**Dependencies:** Step 1.5, Sprint 7 `NotificationDispatchService`, `PlanTierService`
**Acceptance criteria:** Reminders sent on schedule, only to Pro+ clubs, correct escalation.

---

### Step 5.2 — Board Term Expiry Scheduler

**Files to create:**
- [`BoardTermScheduler.java`](cannamanage-service/src/main/java/de/cannamanage/service/BoardTermScheduler.java)

**Approach:** Checks for board terms expiring within 30 days, notifies admins.

```java
@Slf4j
@Service
public class BoardTermScheduler {

    @Scheduled(cron = "0 0 8 1 * *")  // 1st of every month at 8:00 AM
    public void checkExpiringTerms() {
        LocalDate threshold = LocalDate.now().plusDays(30);
        List<BoardMember> expiring = boardMemberRepo.findByTermEndBeforeAndIsActiveTrue(threshold);

        for (BoardMember bm : expiring) {
            notificationDispatchService.sendToAdmins(bm.getTenantId(),
                    NotificationType.BOARD_TERM_EXPIRING,
                    "Amtszeit endet bald",
                    String.format("Die Amtszeit von %s als %s endet am %s.",
                            memberName, positionName, bm.getTermEnd()));
        }
    }
}
```

**Dependencies:** Step 4.5
**Acceptance criteria:** Notifications sent 30 days before term expiry.

---

### Step 5.3 — Auto-Archive Integration

**Files to modify:**
- [`AssemblyService.java`](cannamanage-service/src/main/java/de/cannamanage/service/AssemblyService.java) (add protocol auto-store)
- [`FinancialReportService.java`](cannamanage-service/src/main/java/de/cannamanage/service/FinancialReportService.java) (add report auto-store)

**Approach:** When MV protocol or annual report PDF is generated, automatically create a `Document` record with `source = SYSTEM_*`.

```java
// In AssemblyService.completeAssembly()
byte[] protocolPdf = protocolPdfService.generateProtocol(assembly);
Document doc = documentService.storeSystemDocument(
        assembly.getTenantId(),
        "Protokoll " + assembly.getTitle(),
        "protokoll-" + assembly.getId() + ".pdf",
        protocolPdf,
        DocumentSource.SYSTEM_MV_PROTOCOL,
        categoryRepo.findByTenantIdAndName(assembly.getTenantId(), "Protokolle")
);
assembly.setProtocolDocumentId(doc.getId());
```

**Dependencies:** Step 3.3, Step 4.4
**Acceptance criteria:** Protocols and reports auto-archived in document system.

---

### Step 5.4 — Tier Enforcement Updates

**Files to modify:**
- [`PlanTierService.java`](cannamanage-service/src/main/java/de/cannamanage/service/PlanTierService.java)

**Add tier rules:**
```java
// Finance tier rules
private static final int STARTER_FEE_SCHEDULES_LIMIT = 2;
private static final int STARTER_EXPENSES_YEARLY_LIMIT = 50;
private static final int STARTER_DOCUMENT_STORAGE_MB = 100;
private static final int PRO_DOCUMENT_STORAGE_MB = 1024;

public void requireFinanceAccess(UUID clubId) { ... }
public void enforceExpenseLimit(UUID clubId) { ... }
public void enforceDocumentStorageLimit(UUID clubId, long currentUsageBytes) { ... }
public void requireReminderAccess(UUID clubId) { ... }
```

**Dependencies:** All feature services
**Acceptance criteria:** Tier limits correctly enforced, appropriate error messages.

---

### Step 5.5 — Seed Default Data on Club Creation

**Files to modify:**
- [`ClubService.java`](cannamanage-service/src/main/java/de/cannamanage/service/ClubService.java) (or create `ClubSetupService`)

**Approach:** When a new club is created, seed default expense categories, document categories, and board positions.

Default expense categories:
- Miete, Strom/Nebenkosten, Cannabis-Einkauf, Anbaumaterial, Versicherung, Verwaltung, Sonstiges

Default document categories:
- Satzung, Protokolle, Verträge, Versicherungen, Behördliche Genehmigungen, Sonstiges

Default board positions:
- 1. Vorsitzende/r (required), 2. Vorsitzende/r, Kassenwart/in (required), Schriftführer/in, Beisitzer/in (max_holders: 3)

**Dependencies:** Step 1.3, Step 4.2, Step 4.5
**Acceptance criteria:** New clubs start with sensible defaults.

---

### Step 5.6 — NotificationType Extensions

**Files to modify:**
- [`NotificationType.java`](cannamanage-domain/src/main/java/de/cannamanage/domain/enums/NotificationType.java)

**Add:**
```java
// Sprint 8:
PAYMENT_REMINDER,
ASSEMBLY_INVITATION,
BOARD_TERM_EXPIRING
```

**Dependencies:** None
**Acceptance criteria:** New notification types available for dispatching.

---

## Phase 6: Testing & QA

### Step 6.1 — Unit Tests

**Files to create:**
- `cannamanage-service/src/test/java/de/cannamanage/service/FinanceServiceTest.java`
- `cannamanage-service/src/test/java/de/cannamanage/service/KassenbuchServiceTest.java`
- `cannamanage-service/src/test/java/de/cannamanage/service/ReceiptPdfServiceTest.java`
- `cannamanage-service/src/test/java/de/cannamanage/service/FinancialReportServiceTest.java`
- `cannamanage-service/src/test/java/de/cannamanage/service/PaymentReminderSchedulerTest.java`
- `cannamanage-service/src/test/java/de/cannamanage/service/AssemblyServiceTest.java`
- `cannamanage-service/src/test/java/de/cannamanage/service/ProtocolPdfServiceTest.java`
- `cannamanage-service/src/test/java/de/cannamanage/service/DocumentStorageServiceTest.java`
- `cannamanage-service/src/test/java/de/cannamanage/service/DocumentServiceTest.java`
- `cannamanage-service/src/test/java/de/cannamanage/service/BoardServiceTest.java`
- `cannamanage-service/src/test/java/de/cannamanage/service/BoardTermSchedulerTest.java`

**Dependencies:** All Phase 1-5 services
**Acceptance criteria:** 90%+ line coverage on business logic.

---

### Step 6.2 — Integration Tests

**Files to create:**
- `cannamanage-api/src/test/java/de/cannamanage/api/FinanceControllerIntegrationTest.java`
- `cannamanage-api/src/test/java/de/cannamanage/api/AssemblyControllerIntegrationTest.java`
- `cannamanage-api/src/test/java/de/cannamanage/api/DocumentControllerIntegrationTest.java`
- `cannamanage-api/src/test/java/de/cannamanage/api/BoardControllerIntegrationTest.java`
- `cannamanage-api/src/test/java/de/cannamanage/api/PortalFinanceIntegrationTest.java`

**Dependencies:** Step 6.1
**Acceptance criteria:** All endpoints tested with realistic scenarios, permission checks verified.

---

### Step 6.3 — E2E Playwright Tests

**Files to create:**
- `cannamanage-frontend/e2e/finance.spec.ts` — Full finance workflow
- `cannamanage-frontend/e2e/assemblies.spec.ts` — MV creation → completion
- `cannamanage-frontend/e2e/documents.spec.ts` — Upload/download/categorize
- `cannamanage-frontend/e2e/board.spec.ts` — Board management
- `cannamanage-frontend/e2e/portal-finance.spec.ts` — Member payment view

**Key test scenarios:**

```typescript
// finance.spec.ts
test('Admin records payment and member sees it in portal', async ({ page }) => {
    // 1. Admin navigates to finance
    // 2. Records €30 payment for Member X
    // 3. Verifies receipt number generated
    // 4. Downloads receipt PDF
    // 5. Switches to portal as Member X
    // 6. Verifies payment appears in history
    // 7. Verifies balance shows €0 outstanding
});

// assemblies.spec.ts
test('Full MV lifecycle: create → invite → attend → vote → complete', async ({ page }) => {
    // 1. Admin creates MV with agenda
    // 2. Sends invitations
    // 3. Starts MV
    // 4. Checks in 3 members
    // 5. Records vote (2 yes, 1 no)
    // 6. Completes MV
    // 7. Verifies protocol PDF generated
    // 8. Verifies protocol auto-archived in documents
});
```

**Dependencies:** Step 6.2
**Acceptance criteria:** All E2E tests pass in CI.

---

## File Summary

### New Files (by module)

**cannamanage-domain (enums):** 10 new enum files
**cannamanage-domain (entities):** 15 new entity files
**cannamanage-service:** 12 new service files + 11 test files
**cannamanage-api:** 5 new controller files + 4 migration files + 5 integration test files
**cannamanage-frontend:** ~15 new pages + 5 service files + 5 mock files + 5 E2E test files

**Total: ~90 new files**

### Modified Files

| File | Change |
|------|--------|
| `StaffPermission.java` | +4 enum values |
| `AuditEventType.java` | +26 enum values |
| `NotificationType.java` | +3 enum values |
| `PlanTierService.java` | +4 tier enforcement methods |
| `ClubService.java` | +seed default data logic |
| `navigations.ts` | +finance, assembly, documents, board nav items |
| `EventType.java` | +`GENERAL_ASSEMBLY` value |

---

## Dependency Graph

```mermaid
graph TD
    S1_1[1.1 Enums] --> S1_3[1.3 Entities]
    S1_2[1.2 V18 Migration] --> S1_3
    S1_3 --> S1_4[1.4 Repositories]
    S1_4 --> S1_5[1.5 FinanceService]
    S1_5 --> S1_6[1.6 KassenbuchService]
    S1_5 --> S1_7[1.7 FinanceController]
    S1_6 --> S1_7
    S1_7 --> S1_8[1.8 Portal Endpoints]
    S1_5 --> S2_1[2.1 Receipt PDF]
    S1_6 --> S2_2[2.2 Annual Report PDF]
    S1_7 --> S2_3[2.3 Finance Frontend]
    S1_8 --> S2_4[2.4 Portal Finance]
    S2_3 --> S2_5[2.5 Navigation]
    S2_4 --> S2_5

    S3_1[3.1 V19 Migration] --> S3_2[3.2 Assembly Entities]
    S3_2 --> S3_3[3.3 AssemblyService]
    S3_3 --> S3_4[3.4 Protocol PDF]
    S3_3 --> S3_5[3.5 AssemblyController]
    S3_5 --> S3_6[3.6 Assembly Frontend]

    S4_1[4.1 V20+V21 Migrations] --> S4_2[4.2 Document Entities]
    S4_2 --> S4_3[4.3 DocumentStorageService]
    S4_3 --> S4_4[4.4 DocumentService + Controller]
    S4_1 --> S4_5[4.5 Board Entities + Service]
    S4_4 --> S4_6[4.6 Doc + Board Frontend]
    S4_5 --> S4_6

    S1_5 --> S5_1[5.1 Payment Reminder Scheduler]
    S4_5 --> S5_2[5.2 Board Term Scheduler]
    S3_3 --> S5_3[5.3 Auto-Archive]
    S4_4 --> S5_3
    S5_1 --> S5_4[5.4 Tier Enforcement]
    S5_3 --> S5_5[5.5 Seed Defaults]
    S1_1 --> S5_6[5.6 NotificationType]

    S5_4 --> S6_1[6.1 Unit Tests]
    S5_5 --> S6_1
    S6_1 --> S6_2[6.2 Integration Tests]
    S6_2 --> S6_3[6.3 E2E Tests]
```
