# Sprint 8 Feature Analysis — Vereinsverwaltung Complete

**Date:** 2026-06-13
**Author:** Patrick Plate / Lumen (Architect)
**Status:** Draft v1
**Sprint Goal:** Close the compliance and operational gaps for a German e.V. — Treasury, General Assembly, Documents, Board Management.

---

## Executive Summary

Sprint 8 delivers the "boring but essential" features that every German Verein (e.V.) needs but no cannabis club SaaS currently provides. While Sprint 7 made CannaManage a community platform, Sprint 8 makes it an actual **Vereinsverwaltung** — the digital backbone of club operations beyond cannabis compliance.

Four features, in priority order:

1. **Vereinsfinanzen (Club Treasury)** — Fee schedules, payment tracking, Kassenbuch, receipt PDF generation, annual reports. The Kassenwart's best friend.
2. **Mitgliederversammlung (General Assembly / MV)** — Create MVs, invite members (legal notice periods per §36 BGB), track attendance, conduct votes, generate minutes. The Schriftführer's best friend.
3. **Dokumentenarchiv (Document Storage)** — Organized file storage for Satzung, Protokolle, Verträge, Behördliche Genehmigungen. Lightweight DMS.
4. **Vorstandsverwaltung (Board Management)** — Board positions, term tracking, succession warnings. Small but essential for Vereinsregister compliance.

**Strategic rationale:**
- KCanG §2(4) requires e.V. structure → clubs MUST have a proper Vorstand, hold MVs, keep a Kassenbuch
- Abgabenordnung §§63-68 (Gemeinnützigkeit) requires proper financial documentation if the club claims tax benefits
- BGB §§26-40 defines minimum e.V. governance requirements
- No competitor offers integrated treasury + MV management — they all tell clubs to "use Excel and WhatsApp"
- The Kassenbuch + MV minutes are legally required documents — generating them automatically is a killer feature

**Key design decisions:**

| Decision | Choice | Rationale |
|----------|--------|-----------|
| Payment tracking vs. SEPA collection | Tracking only (MVP) | SEPA Lastschrift requires BaFin registration, IBAN handling, bank API integration — too complex for Sprint 8 |
| File storage backend | Local filesystem `/uploads/` | IONOS VPS has 200GB SSD; S3/MinIO is Sprint 10+ when we go multi-server |
| Voting system | Record results (not live voting) | Real-time voting needs WebSocket conflict resolution, secret ballot crypto — overkill for MVP |
| PDF generation | Reuse existing OpenPDF stack | Already proven in Sprint 5 compliance reports |
| Bookkeeping style | Simple Einnahmen/Ausgaben | Double-entry (Soll/Haben) is overkill for a cannabis club with <€50k/year revenue |
| Financial immutability | Append-only (like audit_events) | Never delete/edit transactions — only void with correction entry |

---

## 1. Vereinsfinanzen (Club Treasury)

### 1.1 Problem Statement

Every German e.V. needs a Kassenbuch (cash book). The Kassenwart (treasurer) must track all income and expenses, issue receipts for member payments, and present an annual financial report (Jahresabschluss) to the elected Kassenprüfer (auditors) at the Mitgliederversammlung.

Today, cannabis clubs use:
- Excel spreadsheets (error-prone, no audit trail, no PDF generation)
- Generic accounting tools (Lexware, sevDesk — not designed for e.V., too complex)
- Paper Kassenbuch (no backup, no search, no automation)

CannaManage can own this because:
- We already have the member database (who owes fees)
- We already have the notification system (payment reminders)
- We already have PDF generation (receipts, reports)
- We already have the audit log (financial compliance trail)

### 1.2 User Stories

| # | As a... | I want to... | So that... | Priority |
|---|---------|-------------|-----------|----------|
| FIN-01 | Kassenwart | Define fee schedules (Beitragsordnung) with name, amount, interval | Different membership types have different fees | P0 |
| FIN-02 | Kassenwart | Assign a fee schedule to each member | I know what everyone should pay | P0 |
| FIN-03 | Kassenwart | Record a payment received (amount, date, method, payer) | I can track who has paid | P0 |
| FIN-04 | Kassenwart | See a dashboard of outstanding balances per member | I know who owes what and since when | P0 |
| FIN-05 | Kassenwart | Generate a PDF receipt (Quittung) for a payment | Members get proof of payment for tax purposes | P0 |
| FIN-06 | Kassenwart | View a Kassenbuch with all income/expenses, running balance | I have the legally required financial overview | P0 |
| FIN-07 | Kassenwart | Categorize expenses (Miete, Strom, Cannabis, Verwaltung, etc.) | The annual report breaks down spending by category | P0 |
| FIN-08 | Kassenwart | Record an expense (amount, date, category, description, receipt) | Outgoing money is tracked | P0 |
| FIN-09 | Kassenwart | Generate an annual financial summary (Jahresabschluss PDF) | The Kassenprüfer can audit the club finances | P1 |
| FIN-10 | Kassenwart | Send payment reminders to overdue members | Members pay on time without me chasing them | P1 |
| FIN-11 | Kassenwart | Export Kassenbuch as CSV | I can import into external tools if needed | P2 |
| FIN-12 | Member (Portal) | See my payment history and outstanding balance | I know if I'm up to date | P0 |
| FIN-13 | Member (Portal) | Download my receipt PDFs | I have proof for my tax return | P1 |
| FIN-14 | Admin | Define expense categories | The Kassenwart has the right buckets | P0 |
| FIN-15 | Kassenprüfer | View the annual report without edit rights | I can audit without accidentally changing data | P1 |

### 1.3 Data Model

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

-- Fee schedules (Beitragsordnung)
CREATE TABLE fee_schedules (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    name VARCHAR(100) NOT NULL,           -- "Regulär", "Ermäßigt", "Familie", "Ehrenmitglied"
    amount NUMERIC(10,2) NOT NULL,         -- 30.00
    interval VARCHAR(20) NOT NULL DEFAULT 'MONTHLY',  -- MONTHLY, QUARTERLY, YEARLY, ONE_TIME
    description TEXT,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Member fee assignment (which member has which plan)
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,                  -- NULL = indefinite
    notes TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE(member_id, fee_schedule_id, effective_from)
);

-- Payments received (append-only — never delete!)
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,   -- CASH, BANK_TRANSFER, SEPA
    reference VARCHAR(255),                -- Verwendungszweck / transaction reference
    period_from DATE,                      -- covers which period (e.g., 2026-07-01)
    period_to DATE,                        -- to (e.g., 2026-07-31)
    receipt_number VARCHAR(50),            -- auto-generated: CM-2026-000001
    notes TEXT,
    voided BOOLEAN NOT NULL DEFAULT false,
    voided_reason TEXT,
    voided_at TIMESTAMPTZ,
    recorded_by UUID NOT NULL,             -- staff/admin who recorded it
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Expense categories
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),                      -- emoji or icon name for UI
    sort_order INTEGER NOT NULL DEFAULT 0,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Expenses (append-only)
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),             -- uploaded receipt scan
    payment_method VARCHAR(30),            -- CASH, BANK_TRANSFER, CARD
    voided BOOLEAN NOT NULL DEFAULT false,
    voided_reason TEXT,
    voided_at TIMESTAMPTZ,
    recorded_by UUID NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Payment reminders sent (tracking)
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,    -- FIRST, SECOND, FINAL
    sent_via VARCHAR(30) NOT NULL,         -- EMAIL, PUSH, IN_APP
    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_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_expenses_tenant_date ON expenses(tenant_id, expense_date DESC);
CREATE INDEX idx_expenses_category ON expenses(category_id);
CREATE INDEX idx_payment_reminders_member ON payment_reminders(member_id);
```

**Key design decisions:**
- Payments and expenses are **append-only** — voiding creates a flag, never deletes the record
- Receipt numbers are auto-generated sequential per club per year (`CM-{year}-{seq}`)
- `period_from`/`period_to` on payments allows tracking which month a payment covers
- Default expense categories seeded on club creation: Miete, Strom/Nebenkosten, Cannabis-Einkauf, Anbaumaterial, Versicherung, Verwaltung, Sonstiges
- File uploads for expense receipts reuse the document storage from Feature 3

### 1.4 API Endpoints

| Method | Path | Permission | Description |
|--------|------|-----------|-------------|
| GET | `/api/finance/fee-schedules` | MANAGE_FINANCES | List fee schedules |
| POST | `/api/finance/fee-schedules` | MANAGE_FINANCES | Create fee schedule |
| PUT | `/api/finance/fee-schedules/{id}` | MANAGE_FINANCES | Update fee schedule |
| POST | `/api/finance/fee-assignments` | MANAGE_FINANCES | Assign fee to member |
| GET | `/api/finance/fee-assignments/{memberId}` | MANAGE_FINANCES | Get member's assignment |
| GET | `/api/finance/payments` | MANAGE_FINANCES | List payments (paginated, filterable) |
| POST | `/api/finance/payments` | MANAGE_FINANCES | Record payment |
| POST | `/api/finance/payments/{id}/void` | MANAGE_FINANCES | Void payment |
| GET | `/api/finance/payments/{id}/receipt` | MANAGE_FINANCES | Download receipt PDF |
| GET | `/api/finance/balances` | MANAGE_FINANCES | Outstanding balance per member |
| GET | `/api/finance/balances/{memberId}` | MANAGE_FINANCES | Single member balance |
| GET | `/api/finance/expenses` | MANAGE_FINANCES | List expenses |
| POST | `/api/finance/expenses` | MANAGE_FINANCES | Record expense |
| POST | `/api/finance/expenses/{id}/void` | MANAGE_FINANCES | Void expense |
| GET | `/api/finance/categories` | MANAGE_FINANCES | List expense categories |
| POST | `/api/finance/categories` | MANAGE_FINANCES | Create category |
| GET | `/api/finance/kassenbuch` | MANAGE_FINANCES | Kassenbuch view (all txns chronological) |
| GET | `/api/finance/kassenbuch/export` | MANAGE_FINANCES | CSV export |
| GET | `/api/finance/reports/annual/{year}` | MANAGE_FINANCES | Annual summary |
| GET | `/api/finance/reports/annual/{year}/pdf` | MANAGE_FINANCES | Jahresabschluss PDF |
| POST | `/api/finance/reminders/send` | MANAGE_FINANCES | Send reminders to overdue |
| GET | `/api/portal/finance/my-payments` | Member | My payment history |
| GET | `/api/portal/finance/my-balance` | Member | My outstanding balance |
| GET | `/api/portal/finance/receipts/{paymentId}` | Member | Download my receipt |

### 1.5 Frontend Pages

**Admin dashboard:**
- `/settings/finance/fee-schedules` — CRUD fee schedules
- `/finance` — Kassenbuch main view (tabbed: Payments / Expenses / Balance / Reports)
- `/finance/payments` — Payment list with record new, void, receipt download
- `/finance/expenses` — Expense list with record new, void, receipt upload
- `/finance/balances` — Member balance overview (grid: name, due, paid, outstanding, last payment, overdue since)
- `/finance/reports` — Annual report generation

**Portal (member-facing):**
- `/portal/finance` — My payment history + balance card

### 1.6 Integration Points

| Integration | How |
|-------------|-----|
| Notification system (Sprint 7) | Payment reminders sent via NotificationDispatchService (email + push + in-app) |
| Audit log (Sprint 5) | All financial CRUD operations logged immutably |
| PDF generation (Sprint 5) | Receipt + Jahresabschluss PDF via OpenPDF |
| Member entity | Fee assignment references Member, payment references Member |
| Document archive (Feature 3) | Expense receipt uploads stored in document archive |
| Tier enforcement | Starter: manual fee tracking only; Pro: full Kassenbuch + reminders; Enterprise: custom receipt templates |

### 1.7 Tier Mapping

| Feature | Starter | Pro | Enterprise |
|---------|---------|-----|-----------|
| Fee schedules | 2 max | Unlimited | Unlimited |
| Payment recording | Manual only | Full + reminders | Full + reminders + custom templates |
| Kassenbuch | View only (last 3 months) | Full history + CSV export | Full + scheduled reports |
| Annual report | Basic summary | Full PDF | Branded PDF + scheduled delivery |
| Payment reminders | ❌ | ✅ Auto (email + in-app) | ✅ Auto + custom schedule |
| Expense tracking | 50 entries/year | Unlimited | Unlimited |
| Receipt PDF | Basic template | Standard template | Custom-branded template |

---

## 2. Mitgliederversammlung (General Assembly / MV)

### 2.1 Problem Statement

Every German e.V. must hold at least one ordentliche Mitgliederversammlung per year (BGB §32). The MV is where:
- The Vorstand reports to members (Rechenschaftsbericht)
- The Kassenprüfer presents the audit result
- Members vote on changes (Satzungsänderungen, Beitragserhöhungen)
- Board elections happen (Vorstandswahl)
- Important decisions get a democratic mandate

Legal requirements (BGB §36 + typical Satzung):
- Written invitation to ALL members with complete Tagesordnung (agenda)
- Notice period: typically 2-4 weeks (defined in Satzung)
- Attendance list (Anwesenheitsliste) — required for quorum
- Quorum: typically 50% of members present (or whatever the Satzung says)
- Minutes (Protokoll) signed by Versammlungsleiter + Schriftführer
- Minutes must be stored permanently (Vereinsregister may request them)

Today clubs manage this via:
- WhatsApp group: "Hey MV am 15. März 19 Uhr" (not legally compliant — no written invitation)
- Paper sign-in sheet at the door (lost, illegible, no quorum calculation)
- Hand-written minutes (delays, disputes about what was decided)
- No permanent archive of past MV decisions

### 2.2 User Stories

| # | As a... | I want to... | So that... | Priority |
|---|---------|-------------|-----------|----------|
| MV-01 | Vorstand | Create an MV with date, time, location, and agenda items | Members know when and what's being discussed | P0 |
| MV-02 | Vorstand | Send official invitations to all members (via notification system) | The legal notice requirement (§36 BGB) is met | P0 |
| MV-03 | Vorstand | Set the notice period (days before MV) | System validates invites are sent on time | P1 |
| MV-04 | Vorstand | Define quorum percentage | System calculates if quorum is reached | P0 |
| MV-05 | Schriftführer | Record attendance (check members in) | The Anwesenheitsliste is digital and accurate | P0 |
| MV-06 | Schriftführer | See live quorum status | I know if we can legally hold votes | P0 |
| MV-07 | Schriftführer | Create voting items (Anträge/Beschlüsse) | Votes are structured and results clear | P0 |
| MV-08 | Schriftführer | Record vote results (yes/no/abstain counts) | Decisions are documented with exact numbers | P0 |
| MV-09 | Schriftführer | Generate Protokoll (minutes) as PDF | Official document for the Vereinsregister | P0 |
| MV-10 | Schriftführer | Upload signed protocol PDF back to system | The official signed version is archived | P1 |
| MV-11 | Vorstand | Record board election results | New Vorstand members are documented | P1 |
| MV-12 | Member (Portal) | See upcoming MV with agenda | I can prepare and decide whether to attend | P0 |
| MV-13 | Member (Portal) | RSVP to the MV | The club knows approximate attendance | P1 |
| MV-14 | Member (Portal) | View past MV minutes (if published) | I can look up what was decided | P1 |
| MV-15 | Admin | View history of all past MVs with their protocols | Institutional memory is preserved | P0 |

### 2.3 Data Model

```sql
-- V19: Mitgliederversammlung (General Assembly)

-- Main MV record
CREATE TABLE general_assemblies (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    club_id UUID NOT NULL REFERENCES clubs(id),
    title VARCHAR(200) NOT NULL,           -- "Ordentliche MV 2026", "Außerordentliche MV Juni 2026"
    assembly_type VARCHAR(30) NOT NULL DEFAULT 'ORDINARY',  -- ORDINARY, EXTRAORDINARY
    scheduled_at TIMESTAMPTZ NOT NULL,
    location VARCHAR(300) NOT NULL,
    notice_period_days INTEGER NOT NULL DEFAULT 14,
    quorum_percentage INTEGER NOT NULL DEFAULT 50,   -- minimum attendance %
    status VARCHAR(30) NOT NULL DEFAULT 'DRAFT',     -- DRAFT, INVITED, IN_PROGRESS, COMPLETED, CANCELLED
    invitation_sent_at TIMESTAMPTZ,
    started_at TIMESTAMPTZ,
    ended_at TIMESTAMPTZ,
    notes TEXT,
    protocol_document_id UUID,             -- FK to documents table (Feature 3)
    event_id UUID,                         -- FK to club_events (MV as special event)
    created_by UUID NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Agenda items (Tagesordnung)
CREATE TABLE assembly_agenda_items (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    assembly_id UUID NOT NULL REFERENCES general_assemblies(id) ON DELETE CASCADE,
    sort_order INTEGER NOT NULL,
    title VARCHAR(300) NOT NULL,           -- "TOP 1: Begrüßung", "TOP 5: Vorstandswahl"
    description TEXT,
    item_type VARCHAR(30) NOT NULL DEFAULT 'DISCUSSION',  -- DISCUSSION, VOTE, ELECTION, REPORT, OTHER
    duration_minutes INTEGER,              -- estimated duration
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Attendance tracking
CREATE TABLE assembly_attendance (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    assembly_id UUID NOT NULL REFERENCES general_assemblies(id) ON DELETE CASCADE,
    member_id UUID NOT NULL REFERENCES members(id),
    checked_in_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    checked_in_by UUID NOT NULL,           -- who marked them present
    proxy_for UUID,                        -- if representing another member (Vollmacht)
    UNIQUE(assembly_id, member_id)
);

-- Voting items (Anträge / Beschlüsse)
CREATE TABLE assembly_votes (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    assembly_id UUID NOT NULL REFERENCES general_assemblies(id) ON DELETE CASCADE,
    agenda_item_id UUID REFERENCES assembly_agenda_items(id),
    title VARCHAR(300) NOT NULL,           -- "Antrag: Beitragserhöhung auf 35€"
    description TEXT,
    vote_type VARCHAR(30) NOT NULL DEFAULT 'SIMPLE_MAJORITY',  -- SIMPLE_MAJORITY, TWO_THIRDS, UNANIMOUS
    required_majority INTEGER NOT NULL DEFAULT 50,  -- percentage needed to pass
    yes_count INTEGER NOT NULL DEFAULT 0,
    no_count INTEGER NOT NULL DEFAULT 0,
    abstain_count INTEGER NOT NULL DEFAULT 0,
    result VARCHAR(20),                    -- ACCEPTED, REJECTED, TABLED
    voted_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Board elections recorded at MV
CREATE TABLE assembly_elections (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    assembly_id UUID NOT NULL REFERENCES general_assemblies(id) ON DELETE CASCADE,
    position VARCHAR(100) NOT NULL,        -- "1. Vorsitzender", "Kassenwart"
    elected_member_id UUID REFERENCES members(id),
    vote_count INTEGER,
    term_start DATE NOT NULL,
    term_end DATE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_assemblies_tenant ON general_assemblies(tenant_id, scheduled_at DESC);
CREATE INDEX idx_assemblies_status ON general_assemblies(tenant_id, status);
CREATE INDEX idx_assembly_agenda_order ON assembly_agenda_items(assembly_id, sort_order);
CREATE INDEX idx_assembly_attendance_assembly ON assembly_attendance(assembly_id);
CREATE INDEX idx_assembly_votes_assembly ON assembly_votes(assembly_id);
```

**Key design decisions:**
- MV is linked to `club_events` — an MV is a special event type, leveraging Sprint 7's RSVP infrastructure
- Attendance is recorded at the MV (not the RSVP — RSVP is intent, attendance is fact)
- Votes are recorded post-factum (not live polling) — the Schriftführer enters results
- Proxy voting (Vollmacht) supported via `proxy_for` field
- Protocol is stored in the Document Archive (Feature 3)
- Status flow: DRAFT → INVITED → IN_PROGRESS → COMPLETED

### 2.4 API Endpoints

| Method | Path | Permission | Description |
|--------|------|-----------|-------------|
| GET | `/api/assemblies` | MANAGE_ASSEMBLY | List all MVs |
| POST | `/api/assemblies` | MANAGE_ASSEMBLY | Create MV |
| GET | `/api/assemblies/{id}` | MANAGE_ASSEMBLY | MV detail |
| PUT | `/api/assemblies/{id}` | MANAGE_ASSEMBLY | Update MV |
| POST | `/api/assemblies/{id}/invite` | MANAGE_ASSEMBLY | Send invitations (transitions to INVITED) |
| POST | `/api/assemblies/{id}/start` | MANAGE_ASSEMBLY | Start MV (transitions to IN_PROGRESS) |
| POST | `/api/assemblies/{id}/complete` | MANAGE_ASSEMBLY | End MV (transitions to COMPLETED) |
| POST | `/api/assemblies/{id}/cancel` | MANAGE_ASSEMBLY | Cancel MV |
| GET | `/api/assemblies/{id}/agenda` | Any auth | Get agenda items |
| POST | `/api/assemblies/{id}/agenda` | MANAGE_ASSEMBLY | Add agenda item |
| PUT | `/api/assemblies/{id}/agenda/{itemId}` | MANAGE_ASSEMBLY | Edit agenda item |
| DELETE | `/api/assemblies/{id}/agenda/{itemId}` | MANAGE_ASSEMBLY | Remove agenda item |
| GET | `/api/assemblies/{id}/attendance` | MANAGE_ASSEMBLY | Attendance list |
| POST | `/api/assemblies/{id}/attendance` | MANAGE_ASSEMBLY | Check in member |
| DELETE | `/api/assemblies/{id}/attendance/{memberId}` | MANAGE_ASSEMBLY | Remove check-in (correction) |
| GET | `/api/assemblies/{id}/quorum` | MANAGE_ASSEMBLY | Quorum status |
| GET | `/api/assemblies/{id}/votes` | MANAGE_ASSEMBLY | List vote items |
| POST | `/api/assemblies/{id}/votes` | MANAGE_ASSEMBLY | Create vote item |
| PUT | `/api/assemblies/{id}/votes/{voteId}` | MANAGE_ASSEMBLY | Record vote result |
| POST | `/api/assemblies/{id}/elections` | MANAGE_ASSEMBLY | Record election result |
| GET | `/api/assemblies/{id}/protocol/pdf` | MANAGE_ASSEMBLY | Generate protocol PDF |
| POST | `/api/assemblies/{id}/protocol/upload` | MANAGE_ASSEMBLY | Upload signed protocol |
| GET | `/api/portal/assemblies` | Member | Upcoming + past MVs |
| GET | `/api/portal/assemblies/{id}` | Member | MV detail with agenda |
| POST | `/api/portal/assemblies/{id}/rsvp` | Member | RSVP (reuses event RSVP) |

### 2.5 Frontend Pages

**Admin dashboard:**
- `/assemblies` — List of all MVs (upcoming + past)
- `/assemblies/new` — Create MV form (date, location, quorum, agenda builder)
- `/assemblies/{id}` — MV detail page (tabbed: Overview / Agenda / Attendance / Votes / Protocol)
- `/assemblies/{id}/live` — Live management during MV (attendance check-in, quorum display, vote recording)

**Portal (member-facing):**
- `/portal/assemblies` — Upcoming MVs (with RSVP) + past MVs (with protocol download)

### 2.6 Integration Points

| Integration | How |
|-------------|-----|
| Club Events (Sprint 7) | MV creates a ClubEvent with type `GENERAL_ASSEMBLY` — inherits RSVP, calendar, reminders |
| Notification system (Sprint 7) | MV invitations sent via NotificationDispatchService (email + push + in-app) — legally compliant written notice |
| Document Archive (Feature 3) | Protocol PDF auto-stored in documents with category "Protokolle" |
| Board Management (Feature 4) | Election results create/update board positions |
| Audit log | All MV operations logged (creation, invitation, attendance, votes, completion) |

### 2.7 Legal References

| Requirement | BGB Reference | Implementation |
|-------------|---------------|----------------|
| MV invitation with agenda | §36 BGB | `invite` endpoint sends full agenda via notification system |
| Notice period | Satzung (typically 2-4 weeks) | `notice_period_days` field, system warns if invite sent too late |
| Quorum | §32(1) BGB | Live quorum calculation: `attendance_count / active_members * 100 >= quorum_percentage` |
| Simple majority | §32(1) BGB | `vote_type = SIMPLE_MAJORITY`, `yes > no` (abstentions don't count) |
| 2/3 majority for Satzungsänderung | §33(1) BGB | `vote_type = TWO_THIRDS`, `yes >= 2/3 of votes cast` |
| Minutes (Protokoll) | §58 Nr. 4 BGB | Auto-generated PDF with decisions, vote counts, attendance |
| Board election | §26-27 BGB | Election results recorded, linked to board management |

---

## 3. Dokumentenarchiv (Document Storage)

### 3.1 Problem Statement

Every club accumulates documents: the Satzung, MV protocols, insurance policies, the KCanG license, lease contracts, board resolutions. Today these live in someone's Google Drive or Dropbox — not accessible to the full board, no version tracking, no categorization, and critically: if the Vorstand changes, the new board may not even know where the documents are.

This is a lightweight DMS — not SharePoint. Simple: upload, categorize, find, download. With access control (some docs board-only, some visible to all members).

### 3.2 User Stories

| # | As a... | I want to... | So that... | Priority |
|---|---------|-------------|-----------|----------|
| DOC-01 | Admin | Upload a document with title, category, and description | Club docs are organized digitally | P0 |
| DOC-02 | Admin | Define access level per document (all members / board only) | Sensitive contracts aren't visible to everyone | P0 |
| DOC-03 | Admin | Organize documents by category | Finding docs is easy | P0 |
| DOC-04 | Admin | Download any document | I can access uploaded files | P0 |
| DOC-05 | Admin | Delete a document | Outdated files are removed | P1 |
| DOC-06 | Admin | Replace/update a document (new version) | Current version is always available, old version archived | P1 |
| DOC-07 | Member (Portal) | Browse documents marked "visible to all" | I can access the Satzung, published protocols | P0 |
| DOC-08 | Member (Portal) | Download documents I have access to | I don't need to ask the Vorstand for basic documents | P0 |
| DOC-09 | System | Auto-store MV protocol PDFs | Protocols are automatically archived | P0 |
| DOC-10 | System | Auto-store annual financial reports | Jahresabschluss PDFs are automatically archived | P0 |
| DOC-11 | Admin | Search documents by title/description | I can find what I need quickly | P2 |

### 3.3 Data Model

```sql
-- V20: Dokumentenarchiv (Document Storage)

CREATE TABLE document_categories (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    name VARCHAR(100) NOT NULL,            -- "Satzung", "Protokolle", "Verträge", etc.
    description TEXT,
    sort_order INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE documents (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    category_id UUID REFERENCES document_categories(id),
    title VARCHAR(255) NOT NULL,
    description TEXT,
    filename VARCHAR(255) NOT NULL,
    content_type VARCHAR(100) NOT NULL,
    size_bytes BIGINT NOT NULL,
    storage_path VARCHAR(500) NOT NULL,    -- relative path in /uploads/documents/{tenant}/{uuid}
    access_level VARCHAR(30) NOT NULL DEFAULT 'BOARD_ONLY',  -- ALL_MEMBERS, BOARD_ONLY
    version INTEGER NOT NULL DEFAULT 1,
    previous_version_id UUID REFERENCES documents(id),
    uploaded_by UUID NOT NULL,
    source VARCHAR(50) NOT NULL DEFAULT 'MANUAL',  -- MANUAL, SYSTEM_MV_PROTOCOL, SYSTEM_FINANCIAL_REPORT
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_documents_tenant ON documents(tenant_id, category_id);
CREATE INDEX idx_documents_access ON documents(tenant_id, access_level);
CREATE INDEX idx_document_categories_tenant ON document_categories(tenant_id, sort_order);
```

**Key design decisions:**
- Storage path: `/uploads/documents/{tenant_id}/{document_id}/{filename}` — tenant-isolated on filesystem
- No full-text indexing (Sprint 8 MVP) — search by title/description only
- Version tracking via `previous_version_id` linked list — simple, no complex version trees
- System-generated documents tagged with `source` to distinguish from manual uploads
- Default categories seeded on club creation: Satzung, Protokolle, Verträge, Versicherungen, Behördliche Genehmigungen, Sonstiges
- Max file size: 20MB per file (sufficient for scans, PDFs, common docs)
- Allowed content types: PDF, DOCX, DOC, XLSX, XLS, PNG, JPG, JPEG

### 3.4 API Endpoints

| Method | Path | Permission | Description |
|--------|------|-----------|-------------|
| GET | `/api/documents` | MANAGE_DOCUMENTS | List all documents (filterable by category) |
| POST | `/api/documents` | MANAGE_DOCUMENTS | Upload document |
| GET | `/api/documents/{id}` | MANAGE_DOCUMENTS | Document metadata |
| GET | `/api/documents/{id}/download` | MANAGE_DOCUMENTS | Download file |
| PUT | `/api/documents/{id}` | MANAGE_DOCUMENTS | Update metadata |
| POST | `/api/documents/{id}/new-version` | MANAGE_DOCUMENTS | Upload new version |
| DELETE | `/api/documents/{id}` | MANAGE_DOCUMENTS | Delete document |
| GET | `/api/documents/categories` | Any auth | List categories |
| POST | `/api/documents/categories` | MANAGE_DOCUMENTS | Create category |
| GET | `/api/portal/documents` | Member | List documents with `access_level = ALL_MEMBERS` |
| GET | `/api/portal/documents/{id}/download` | Member | Download (if access_level allows) |

### 3.5 Storage Architecture

```
/uploads/
├── documents/
│   ├── {tenant_id_1}/
│   │   ├── {doc_id_1}/
│   │   │   └── satzung-2026.pdf
│   │   ├── {doc_id_2}/
│   │   │   └── mietvertrag.pdf
│   │   └── ...
│   └── {tenant_id_2}/
│       └── ...
├── expense-receipts/
│   └── {tenant_id}/
│       └── {expense_id}/
│           └── receipt.jpg
└── post-attachments/
    └── {tenant_id}/
        └── {post_id}/
            └── flyer.png
```

### 3.6 Tier Mapping

| Feature | Starter | Pro | Enterprise |
|---------|---------|-----|-----------|
| Document storage | 100MB total | 1GB total | 10GB total |
| Categories | Default only | Custom categories | Custom + nested |
| Version history | ❌ | ✅ | ✅ |
| System auto-archive | ❌ | ✅ (MV protocols) | ✅ (MV + financial) |

---

## 4. Vorstandsverwaltung (Board Management)

### 4.1 Problem Statement

A German e.V. must have a Vorstand (board). The Vereinsregister requires knowing who holds which position. Board positions have terms (Amtszeit), and when a term expires, an election must happen at the next MV. Currently, clubs track board membership in their heads or a spreadsheet — when the 2. Vorsitzender steps down, nobody remembers when the term started.

### 4.2 User Stories

| # | As a... | I want to... | So that... | Priority |
|---|---------|-------------|-----------|----------|
| BRD-01 | Admin | Define board positions (Vorsitzende/r, Kassenwart, Schriftführer, etc.) | The club's organizational structure is documented | P0 |
| BRD-02 | Admin | Assign a member to a board position with start/end dates | Everyone knows who holds which role | P0 |
| BRD-03 | Admin | See current board composition | Quick overview of who's responsible for what | P0 |
| BRD-04 | Admin | See board history (who held which position when) | Institutional memory is preserved | P1 |
| BRD-05 | Admin | Get notified 30 days before a term expires | I can plan the next election at the MV | P1 |
| BRD-06 | Member (Portal) | See current board members and their roles | I know who to contact | P0 |
| BRD-07 | System | Display board on club info/profile | Transparency to members | P1 |

### 4.3 Data Model

```sql
-- V21: Vorstandsverwaltung (Board Management)

CREATE TABLE board_positions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    name VARCHAR(100) NOT NULL,            -- "1. Vorsitzende/r", "Kassenwart", etc.
    description TEXT,
    sort_order INTEGER NOT NULL DEFAULT 0,
    is_required BOOLEAN NOT NULL DEFAULT false,  -- legally required positions
    max_holders INTEGER NOT NULL DEFAULT 1,      -- typically 1, but Beisitzer can be multiple
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE board_members (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    position_id UUID NOT NULL REFERENCES board_positions(id),
    member_id UUID NOT NULL REFERENCES members(id),
    term_start DATE NOT NULL,
    term_end DATE,                         -- NULL = indefinite (until next MV)
    elected_at_assembly_id UUID REFERENCES general_assemblies(id),
    is_active BOOLEAN NOT NULL DEFAULT true,
    ended_reason VARCHAR(50),              -- TERM_EXPIRED, RESIGNED, VOTED_OUT, OTHER
    ended_at DATE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_board_positions_tenant ON board_positions(tenant_id, sort_order);
CREATE INDEX idx_board_members_active ON board_members(tenant_id, is_active, position_id);
CREATE INDEX idx_board_members_member ON board_members(member_id);
```

**Key design decisions:**
- Positions are customizable (clubs can add Beisitzer, Jugendwart, etc.)
- `is_required` marks the legally mandatory positions (1. Vorsitzende/r, Kassenwart/in)
- History preserved: `is_active = false` + `ended_reason` tracks past holders
- Linked to MV elections via `elected_at_assembly_id`
- Default positions seeded: 1. Vorsitzende/r, 2. Vorsitzende/r, Kassenwart/in, Schriftführer/in, Beisitzer/in
- Term expiry notification via scheduled job (30 days before `term_end`)

### 4.4 API Endpoints

| Method | Path | Permission | Description |
|--------|------|-----------|-------------|
| GET | `/api/board/positions` | Any auth | List positions |
| POST | `/api/board/positions` | Admin only | Create position |
| PUT | `/api/board/positions/{id}` | Admin only | Update position |
| GET | `/api/board/members` | Any auth | Current board composition |
| POST | `/api/board/members` | Admin only | Appoint member to position |
| PUT | `/api/board/members/{id}/end` | Admin only | End appointment |
| GET | `/api/board/history` | Any auth | Board history |
| GET | `/api/portal/board` | Member | Current board (public view) |

---

## 5. Prioritization & Sprint Plan

### 5.1 Priority Matrix

```mermaid
graph LR
    subgraph P0 - Must Have
        FIN[Vereinsfinanzen Core]
        MV[MV Core]
        DOC[Dokumentenarchiv]
        BRD[Vorstandsverwaltung]
    end

    subgraph P1 - Should Have
        FINR[Payment Reminders]
        FINPDF[Jahresabschluss PDF]
        MVEL[Board Elections]
        DOCV[Document Versioning]
    end

    subgraph P2 - Nice to Have
        FINCSV[CSV Export]
        DOCSRCH[Document Search]
    end
```

### 5.2 Phased Delivery

| Phase | Focus | Features |
|-------|-------|----------|
| Phase 1 | Treasury Backend | Fee schedules, payments, expenses, Kassenbuch, balance calculation |
| Phase 2 | Treasury Frontend + PDF | Admin finance pages, receipt PDF, portal payment view |
| Phase 3 | Mitgliederversammlung | MV creation, invitations, attendance, voting, protocol PDF |
| Phase 4 | Dokumentenarchiv + Board | File upload/download, categories, board positions/members |
| Phase 5 | Integration & Polish | Notifications, auto-archive, tier enforcement, term expiry alerts |
| Phase 6 | Testing & QA | Unit tests, integration tests, E2E Playwright tests |

---

## 6. Technical Architecture

### 6.1 New Entities

| Entity | Package | Purpose |
|--------|---------|---------|
| `FeeSchedule` | `domain.entity` | Fee plan definition |
| `MemberFeeAssignment` | `domain.entity` | Links member to fee schedule |
| `Payment` | `domain.entity` | Incoming payment record |
| `Expense` | `domain.entity` | Outgoing expense record |
| `ExpenseCategory` | `domain.entity` | Expense categorization |
| `PaymentReminder` | `domain.entity` | Reminder tracking |
| `GeneralAssembly` | `domain.entity` | MV master record |
| `AssemblyAgendaItem` | `domain.entity` | Tagesordnung item |
| `AssemblyAttendance` | `domain.entity` | MV attendance |
| `AssemblyVote` | `domain.entity` | Vote item + result |
| `AssemblyElection` | `domain.entity` | Election result |
| `Document` | `domain.entity` | Stored file metadata |
| `DocumentCategory` | `domain.entity` | Document categorization |
| `BoardPosition` | `domain.entity` | Board position definition |
| `BoardMember` | `domain.entity` | Member-position assignment |

### 6.2 New Enums

| Enum | Values |
|------|--------|
| `PaymentMethod` | `CASH`, `BANK_TRANSFER`, `SEPA`, `CARD` |
| `FeeInterval` | `MONTHLY`, `QUARTERLY`, `YEARLY`, `ONE_TIME` |
| `ReminderType` | `FIRST`, `SECOND`, `FINAL` |
| `AssemblyType` | `ORDINARY`, `EXTRAORDINARY` |
| `AssemblyStatus` | `DRAFT`, `INVITED`, `IN_PROGRESS`, `COMPLETED`, `CANCELLED` |
| `AgendaItemType` | `DISCUSSION`, `VOTE`, `ELECTION`, `REPORT`, `OTHER` |
| `VoteType` | `SIMPLE_MAJORITY`, `TWO_THIRDS`, `UNANIMOUS` |
| `VoteResult` | `ACCEPTED`, `REJECTED`, `TABLED` |
| `DocumentAccessLevel` | `ALL_MEMBERS`, `BOARD_ONLY` |
| `DocumentSource` | `MANUAL`, `SYSTEM_MV_PROTOCOL`, `SYSTEM_FINANCIAL_REPORT` |

### 6.3 New Services

| Service | Responsibility |
|---------|---------------|
| `FinanceService` | Fee schedule CRUD, payment/expense recording, balance calculation |
| `KassenbuchService` | Kassenbuch view generation, running balance, CSV export |
| `ReceiptPdfService` | Payment receipt PDF generation |
| `FinancialReportService` | Annual report generation (data + PDF) |
| `PaymentReminderService` | Overdue detection, reminder sending (uses NotificationDispatchService) |
| `AssemblyService` | MV lifecycle management (create → invite → start → complete) |
| `AssemblyVoteService` | Vote management and result recording |
| `ProtocolPdfService` | MV protocol PDF generation |
| `DocumentStorageService` | File upload/download, filesystem operations |
| `DocumentService` | Document metadata CRUD, access control |
| `BoardService` | Board position/member management |
| `BoardTermScheduler` | Scheduled job: checks for expiring terms, sends notifications |

### 6.4 New Staff Permissions

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

### 6.5 New Audit Event Types

```java
// Finance
PAYMENT_RECORDED,
PAYMENT_VOIDED,
EXPENSE_RECORDED,
EXPENSE_VOIDED,
FEE_SCHEDULE_CREATED,
FEE_SCHEDULE_UPDATED,
FEE_ASSIGNMENT_CHANGED,
PAYMENT_REMINDER_SENT,
FINANCIAL_REPORT_GENERATED,

// Assembly
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,

// Documents
DOCUMENT_UPLOADED,
DOCUMENT_UPDATED,
DOCUMENT_DELETED,
DOCUMENT_VERSION_CREATED,

// Board
BOARD_MEMBER_APPOINTED,
BOARD_MEMBER_TERM_ENDED
```

### 6.6 Flyway Migrations

| Version | Content |
|---------|---------|
| V18 | Vereinsfinanzen tables (fee_schedules, payments, expenses, etc.) |
| V19 | Mitgliederversammlung tables (general_assemblies, agenda, attendance, votes, elections) |
| V20 | Dokumentenarchiv tables (documents, document_categories) |
| V21 | Vorstandsverwaltung tables (board_positions, board_members) |

### 6.7 Mermaid: Entity Relationships

```mermaid
erDiagram
    MEMBER ||--o{ PAYMENT : pays
    MEMBER ||--o{ MEMBER_FEE_ASSIGNMENT : has
    FEE_SCHEDULE ||--o{ MEMBER_FEE_ASSIGNMENT : defines
    EXPENSE_CATEGORY ||--o{ EXPENSE : categorizes
    MEMBER ||--o{ ASSEMBLY_ATTENDANCE : attends
    GENERAL_ASSEMBLY ||--o{ ASSEMBLY_AGENDA_ITEM : contains
    GENERAL_ASSEMBLY ||--o{ ASSEMBLY_ATTENDANCE : tracks
    GENERAL_ASSEMBLY ||--o{ ASSEMBLY_VOTE : holds
    GENERAL_ASSEMBLY ||--o{ ASSEMBLY_ELECTION : records
    DOCUMENT_CATEGORY ||--o{ DOCUMENT : organizes
    BOARD_POSITION ||--o{ BOARD_MEMBER : filled_by
    MEMBER ||--o{ BOARD_MEMBER : serves_as
    GENERAL_ASSEMBLY ||--o{ ASSEMBLY_ELECTION : determines
    ASSEMBLY_ELECTION }|--|| BOARD_MEMBER : creates
```

---

## 7. Legal References

| Law | Section | Requirement | Feature |
|-----|---------|-------------|---------|
| BGB | §26 | e.V. must have a Vorstand | Board Management |
| BGB | §27 | Vorstand appointed by MV | MV Elections |
| BGB | §32(1) | Decisions by majority of attending members | MV Voting |
| BGB | §33(1) | Satzungsänderung requires 3/4 majority | MV Vote Types |
| BGB | §36 | MV invitation with agenda in writing | MV Notifications |
| BGB | §58 Nr. 4 | Minutes of resolutions must be kept | MV Protocol |
| KCanG | §2(4) | Cannabis clubs must be organized as e.V. | All features |
| KCanG | §19 | Detailed record-keeping obligations | Financial audit trail |
| AO | §63 | Actual management (tatsächliche Geschäftsführung) | Kassenbuch, Board Management |
| AO | §66 | Proper bookkeeping for tax-exempt orgs | Financial reports |

---

## 8. Mobile Considerations

All features are designed mobile-first for the upcoming native app (Sprint 9+):

| Feature | Mobile UX |
|---------|-----------|
| Payment recording | Quick-entry form with amount + member selector |
| Balance check | Simple card showing outstanding amount |
| MV attendance | QR code check-in at the door (future) |
| Document view | PDF viewer embedded, download to device |
| Board view | Contact card style for board members |
| Voting | Large yes/no/abstain buttons during MV |

---

## 9. Risk Assessment

| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|------------|
| Financial data loss | Low | Critical | Append-only design, daily DB backups, no delete operations |
| Incorrect balance calculation | Medium | High | Extensive unit tests, manual verification workflow |
| MV legal non-compliance | Low | High | Notice period validation, quorum calculation, protocol template vetted |
| File storage disk full | Low | Medium | Tier-based quotas, monitoring alert at 80% |
| Permission confusion | Medium | Medium | Clear UI showing who can do what, role-based defaults |
| PDF generation failure | Low | Medium | Fallback: show data in UI, allow retry |
