Files
cannamanage/docs/sprint-8/cannamanage-sprint8-analysis.md
Patrick Plate b22702317a feat(sprint8): Phase 3 — Mitgliederversammlung (assemblies, voting, protocol PDF)
Backend:
- V19 migration: assemblies, assembly_agenda_items, assembly_attendees, assembly_votes, assembly_vote_records
- Enums: AssemblyType, AssemblyStatus, AgendaItemType, VoteType, VoteDecision, VoteResult
- Entities: Assembly, AssemblyAgendaItem, AssemblyAttendee, AssemblyVote, AssemblyVoteRecord
- Repositories: Assembly, AgendaItem, Attendee, Vote, VoteRecord
- AssemblyService: full lifecycle (create, invite, start, attend, vote, quorum, complete)
- AssemblyProtocolService: OpenPDF protocol generation (§147 AO compliant)
- AssemblyController: admin + portal endpoints
- Extended: AuditEventType, NotificationType, StaffPermission

Frontend:
- Assembly service with full API client and TypeScript types
- Admin assemblies list page with create dialog (agenda builder)
- Admin assembly detail page (quorum, agenda, votes, attendees)
- Navigation: Versammlungen with Gavel icon (after Finanzen)

Legal basis: §32-§40 BGB (Mitgliederversammlung), §147 AO (retention)
2026-06-15 08:39:10 +02:00

43 KiB

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

-- 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

-- 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)
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

-- 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

-- 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

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

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

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

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

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