PHASE 8: CRM, Support & Accounting

Trigger: "Execute Phase 8 as per masterplan."

Goal: CRM with lead pipeline and deal tracking, support ticketing with SLA and knowledge base, and accounting with SKR03 chart of accounts, DATEV export, and period-close workflow. All three modules fully functional with admin UI.

Prerequisite: Phase 7 is completed.

Step 8.0 — Technical Conception

Create doc/developer/conception/phase-8-conception.md.

(reference: Chapter 05 — Feature Catalog (Packages 10, 11, 6.2), Chapter 11 — Data Model, Chapter 12 — Integration (DATEV))

Content: - Scope: membership-crm, membership-support, membership-accounting - CRM Design: Lead lifecycle, configurable pipeline stages, activity tracking, conversion analytics - Support Design: Ticket lifecycle (SLA-aware), auto-routing, knowledge base, CSAT measurement - Accounting Design: Double-entry bookkeeping, SKR03 mapping, DATEV CSV/XML format, period-close - Integration Points: CRM lead -> member conversion, billing event -> accounting entry, support ticket -> CRM activity - Design Chapter References: Ch05 (Packages 6.2, 10, 11), Ch11, Ch12 (DATEV section)

Result: Technical conception for Phase 8.


Step 8.1 — CRM Entities and Service

Create the membership-crm module.

(reference: Chapter 05 — Package 10, Chapter 11 — Data Model, Chapter 10 — API Design)

Approach:

  1. Create JPA entities:

Lead fields:

Field Type Constraints Description
id Long PK, generated Primary key
entityId Long FK NOT NULL Organization
firstName String(100) NOT NULL Contact first name
lastName String(100) NOT NULL Contact last name
email String(200) NOT NULL Contact email
phone String(50) Phone (E.164)
organizationName String(200) Company/club name
source String(30) NOT NULL WEBSITE / REFERRAL / WALK_IN / CAMPAIGN / IMPORT / PARTNER / COLD_OUTREACH
status String(30) default 'NEW' NEW / CONTACTED / QUALIFIED / UNQUALIFIED / CONVERTED / LOST
assignedTo Long FK nullable Assigned sales user
expectedValue BigDecimal(19,4) nullable Expected monthly revenue
expectedTier String(20) nullable STARTER / TEAM / PROFESSIONAL / ENTERPRISE
lostReason String(500) nullable Why lead was lost
notes String(4000) Free-text notes
customFields JSONB default '{}' Extensible fields
lastContactedAt Instant nullable Last outreach timestamp
nextFollowUpAt Instant nullable Scheduled follow-up
createdAt Instant auto
updatedAt Instant auto
version Long @Version

Deal fields:

Field Type Constraints Description
id Long PK, generated
entityId Long FK NOT NULL Organization
leadId Long FK NOT NULL Source lead
title String(200) NOT NULL Deal title (e.g., "Fitnessstudio XY — Team")
pipelineStage String(50) NOT NULL Current pipeline stage (from PipelineConfig)
expectedValue BigDecimal(19,4) Expected monthly revenue
probability Integer default 50 Close probability 0-100%
expectedCloseDate LocalDate Expected close date
actualCloseDate LocalDate nullable Actual close date
wonLostReason String(500) nullable Why deal was won/lost
contractId Long FK nullable Created contract (after conversion)
createdAt Instant auto
updatedAt Instant auto
version Long @Version

Activity fields:

Field Type Constraints Description
id Long PK, generated
entityId Long FK NOT NULL Organization
leadId Long FK nullable Related lead
dealId Long FK nullable Related deal
type String(20) NOT NULL CALL / EMAIL / MEETING / NOTE / TASK / DEMO
description String(4000) NOT NULL Activity content/notes
scheduledAt Instant nullable Planned time
completedAt Instant nullable Completion time
durationMinutes Integer nullable Duration of call/meeting
createdBy Long FK NOT NULL User who created
createdAt Instant auto

PipelineConfig: id, entityId, stages (JSONB array of {name, order, color, autoActions}) 2. Create CrmService: - Lead management: CRUD, search, assign, bulk operations - Lead source tracking with attribution - Configurable pipeline: stages defined per entity via PipelineConfig: - Default pipeline stages:

   | Order | Stage | Color | Auto-Actions |
   |:-----:|-------|-------|-------------|
   | 1 | NEW | `#90CAF9` (blue) | Auto-assign to sales manager, send welcome email |
   | 2 | CONTACTED | `#CE93D8` (purple) | — |
   | 3 | DEMO_SCHEDULED | `#FFB74D` (orange) | Send calendar invite |
   | 4 | DEMO_COMPLETED | `#FFF176` (yellow) | Create follow-up task (48h) |
   | 5 | TRIAL | `#80CBC4` (teal) | Provision trial tenant, start drip sequence |
   | 6 | PROPOSAL_SENT | `#EF9A9A` (red) | Create follow-up task (7d) |
   | 7 | NEGOTIATION | `#A5D6A7` (green) | — |
   | 8 | WON | `#4CAF50` (dark green) | Convert to member, create contract |
   | 9 | LOST | `#9E9E9E` (grey) | Record reason, schedule win-back (90d) |

 - Admin can add/remove/reorder stages and customize auto-actions per stage
  • Deal tracking: expected value, close date, probability (default 10% at NEW → 90% at PROPOSAL_SENT)
  • Activity log: calls, emails, meetings, notes (linked to lead or deal)
  • Convert lead to member: convertToMember(Long leadId) creates Member + User from Lead data, copies name/email/phone, marks lead as CONVERTED
  • Trial-to-member conversion tracking: funnel metrics per stage
  • Automated follow-up reminders (RabbitMQ: scheduled messages based on nextFollowUpAt) 3. Create tests: CrmServiceTest

Result: CRM entities and business logic.


Step 8.2 — CRM Controller and Admin UI

Approach:

  1. Create controller: - POST /api/crm/leads — Create lead - GET /api/crm/leads — Search leads (paginated, filterable) - GET /api/crm/leads/{id} — Get lead details - PUT /api/crm/leads/{id} — Update lead - POST /api/crm/leads/{id}/convert — Convert to member - POST /api/crm/deals — Create deal - GET /api/crm/deals — Search deals - PUT /api/crm/deals/{id} — Update deal (move pipeline stage) - POST /api/crm/activities — Create activity - GET /api/crm/activities?leadId={id} — Activity timeline - GET /api/crm/pipeline/config — Get pipeline configuration - PUT /api/crm/pipeline/config — Update pipeline configuration - GET /api/crm/reports/conversion — Conversion rates per stage (funnel: % drop-off at each stage) - GET /api/crm/reports/pipeline — Pipeline analytics:
    • Total pipeline value (sum of deal expectedValue * probability)
    • Average deal velocity (days from NEW to WON)
    • Win rate (WON / (WON + LOST) per period)
    • Lead source effectiveness (leads, conversions, revenue per source)
    • GET /api/crm/reports/forecast?months={n} — Revenue forecast based on pipeline probability
    • GET /api/crm/reports/activity?userId={id}&from={}&to={} — Activity report per sales user
  2. Create Flyway: V800__create_crm.sql
  3. Create admin UI: - Lead list: DataTable with columns: name, email, source (icon), status (chip), assigned to, expected value, next follow-up, created date. Filters: status, source, assigned user, date range. Bulk actions: assign, change status, export CSV. - Pipeline board: Kanban-style drag-and-drop (stages as columns, deals as cards). Card shows: title, expected value, probability %, expected close date, days in stage. Drag deal to move stage (auto-updates via API). Color-coded by deal value tier. - Lead/deal detail page:
    • Header: name, status badge, assigned user avatar, source icon
    • Left panel: contact info, organization name, expected value, tier
    • Center panel: Activity timeline (reverse chronological) with type icons (phone, email, meeting, note)
    • Right panel: Quick actions — add note, schedule call, send email, convert to member
    • Bottom: linked deals (for lead), contract info (after conversion)
    • Reports page (4 charts):
    • Funnel chart: leads per stage, conversion % between stages
    • Pipeline value chart: total value per stage (bar chart)
    • Source distribution: pie chart of leads by source
    • Velocity chart: average days in each stage (bar chart)
  4. Tests: controller tests, pipeline stage transition tests

Result: CRM with pipeline board, lead management, and analytics.


Step 8.3 — Support Entities and Service

Create the membership-support module.

(reference: Chapter 05 — Package 11, Chapter 11 — Data Model)

Approach:

  1. Create JPA entities:

Ticket fields:

Field Type Constraints Description
id Long PK, generated
entityId Long FK NOT NULL Organization
ticketNumber String(20) UNIQUE per entity Auto-generated: T-{5-digit-seq}
memberId Long FK nullable Reporting member (null if internal)
subject String(300) NOT NULL Ticket title
description String(10000) NOT NULL Detailed description
category String(30) NOT NULL BUG_REPORT / FEATURE_REQUEST / BILLING_QUESTION / ONBOARDING_HELP / GENERAL / ACCESS_ISSUE
priority String(20) default 'P3_MEDIUM' P1_CRITICAL / P2_HIGH / P3_MEDIUM / P4_LOW
status String(30) default 'OPEN' OPEN / IN_PROGRESS / WAITING_CUSTOMER / RESOLVED / CLOSED
assignedTo Long FK nullable Assigned support agent
slaDeadline Instant nullable Calculated from priority + tier SLA config
slaBreach boolean default false True if slaDeadline passed while not resolved
resolvedAt Instant nullable Resolution timestamp
closedAt Instant nullable Close timestamp
csatScore Integer nullable Customer satisfaction 1-5 (after resolution)
csatComment String(2000) nullable CSAT free-text feedback
tags JSONB default '[]' Searchable tags
createdAt Instant auto
updatedAt Instant auto
version Long @Version

TicketComment fields:

Field Type Constraints Description
id Long PK, generated
ticketId Long FK NOT NULL Parent ticket
authorId Long FK NOT NULL Comment author
content String(10000) NOT NULL Comment text (Markdown)
isInternal boolean default false True = visible to agents only, hidden from member
createdAt Instant auto

KnowledgeBaseArticle fields:

Field Type Constraints Description
id Long PK, generated
entityId Long FK nullable Null = global (Membership One), set = per-entity
title String(300) NOT NULL Article title
slug String(200) UNIQUE URL-safe identifier
content String NOT NULL Full article (Markdown)
category String(50) NOT NULL GETTING_STARTED / BILLING / MEMBERS / CHECK_IN / COURSES / INTEGRATIONS / TROUBLESHOOTING
tags JSONB default '[]' Searchable tags
status String(20) default 'DRAFT' DRAFT / PUBLISHED / ARCHIVED
publishedAt Instant nullable Publication timestamp
viewCount Long default 0 View counter (incremented on read)
helpfulCount Long default 0 "Was this helpful?" yes counter
createdAt Instant auto
updatedAt Instant auto
version Long @Version
2. Create SupportService:
- Ticket lifecycle: create, assign, comment, resolve, close, reopen
- SLA configuration per pricing tier:
 | Tier | P1 (Critical) | P2 (High) | P3 (Medium) | P4 (Low) | Business Hours |
 |------|:-------------:|:---------:|:-----------:|:--------:|:--------------|
 | Starter | — | — | — | — | Community only (KB self-service) |
 | Team | 24h | 48h | 72h | 5 days | Mon-Fri 09:00-17:00 CET |
 | Professional | 4h | 24h | 48h | 72h | Mon-Fri 08:00-20:00 CET |
 | Enterprise | 1h | 4h | 24h | 48h | Mon-Sun 07:00-22:00 CET |
  • SLA deadline calculation (business hours, timezone-aware, excludes German public holidays)
  • SLA breach detection: scheduled job every 15 min checks tickets approaching deadline, sets slaBreach = true and sends alert to assigned agent + team lead
  • Auto-routing: assign by category and entity location
  • Canned responses / macros (stored as CommunicationTemplate with type SUPPORT_MACRO)
  • Internal notes (visible to agents only)
  • CSAT survey: auto-send on resolution, 1-5 scale 3. Create KnowledgeBaseService:
  • Article CRUD, Markdown rendering, full-text search
  • Categories and tags, view count tracking
  • Public access (no auth required for published articles) 4. Create tests: SupportServiceTest, KnowledgeBaseServiceTest

Result: Support ticketing with SLA and knowledge base.


Step 8.4 — Support Controller and Admin UI

Approach:

  1. Create controller: - POST /api/support/tickets — Create ticket - GET /api/support/tickets — Search tickets (paginated) - GET /api/support/tickets/{id} — Get ticket with comments - PUT /api/support/tickets/{id} — Update ticket (status, assignment, priority) - POST /api/support/tickets/{id}/comments — Add comment - POST /api/support/tickets/{id}/resolve — Resolve ticket - POST /api/support/tickets/{id}/csat — Submit CSAT score - GET /api/support/kb/articles — Search knowledge base - POST /api/support/kb/articles — Create article - PUT /api/support/kb/articles/{id} — Update article - GET /api/support/sla/config — Get SLA configuration - PUT /api/support/sla/config — Update SLA configuration - GET /api/support/reports/sla — SLA compliance report - GET /api/support/reports/csat — CSAT report
  2. Create Flyway: V801__create_support.sql
  3. Create admin UI: - Ticket queue: DataTable with columns: ticket#, subject, priority (color-coded: P1=red, P2=orange, P3=yellow, P4=grey), status, member name, assigned to, SLA countdown timer (green→yellow→red), created date. Filters: status, priority, assigned to, category. Sort by: priority (default), SLA deadline, created date. Real-time update via polling (30s). - Ticket detail page:
    • Header: ticket#, subject, priority badge, status badge, SLA countdown
    • Left panel: member info (name, email, tier, last login), ticket metadata (category, created, assigned)
    • Center panel: conversation thread (alternating left/right bubbles for member/agent). Internal notes: toggle "Show internal" → yellow background notes visible to agents only
    • Right panel: Quick actions — assign to me, change priority, add canned response (dropdown), link to KB article, resolve, close
    • Bottom: related tickets (by same member), similar KB articles (keyword match)
    • KB editor: Markdown editor with live preview (side-by-side), category picker, tag input, SEO slug auto-generation, publish/draft toggle. Article list with view count, helpful count, last updated.
    • Reports (4 charts):
    • SLA compliance: % of tickets resolved within SLA, per priority, per month (target: >95%)
    • CSAT trend: average score per month (line chart), score distribution (1-5 histogram)
    • Ticket volume: by category (pie), by month (bar), by day of week (heatmap)
    • Resolution time: average time to resolve per priority, per agent
  4. Member app: create ticket (from profile → "Help" button), view own tickets with status, read knowledge base (searchable, categorized)

Result: Support system with ticketing, SLA, knowledge base, and CSAT.


Step 8.5 — Accounting Entities and Service

Create the membership-accounting module.

(reference: Chapter 05 — Package 6.2, Chapter 11 — Data Model, Chapter 12 — DATEV)

Approach:

  1. Create JPA entities:

AccountingEntry fields:

Field Type Constraints Description
id Long PK, generated
entityId Long FK NOT NULL Organization
entryNumber String(20) UNIQUE per entity Sequential: B-{YYYY}-{5-digit-seq}
entryDate LocalDate NOT NULL Booking date
accountCode String(10) FK NOT NULL Debit account (from ChartOfAccount)
counterAccountCode String(10) FK NOT NULL Credit account
amount BigDecimal(19,4) NOT NULL Posting amount (always positive)
currency String(3) default 'EUR' ISO 4217
taxRate BigDecimal(5,2) nullable VAT rate if applicable
taxAmount BigDecimal(19,4) nullable VAT amount
description String(500) NOT NULL Booking text (Buchungstext)
reference String(100) nullable Source reference (e.g., transaction ID, invoice number)
referenceType String(30) nullable TRANSACTION / INVOICE / MANUAL / STORNO
costCenterId Long FK nullable Cost center assignment
periodYear Integer NOT NULL Booking year (for period close)
periodMonth Integer NOT NULL Booking month (1-12)
periodClosed boolean default false True if period is closed (immutable)
createdBy Long FK NOT NULL User who created entry
createdAt Instant auto
version Long @Version

CostCenter fields:

Field Type Constraints Description
id Long PK, generated
entityId Long FK NOT NULL Organization
code String(20) UNIQUE per entity Cost center code (e.g., "KST-100")
name String(200) NOT NULL Display name (e.g., "Fitnessstudio Hauptgebäude")
description String(500) Purpose description
active boolean default true Active flag
createdAt Instant auto
version Long @Version

(ChartOfAccount already in seed data from Phase 0) 2. Create AccountingService: - General ledger: double-entry bookkeeping (every entry creates debit + credit pair) - Automated posting from billing events (via RabbitMQ listener on billing.* events):

 | Business Event | Debit Account | Credit Account | Description |
 |----------------|:------------:|:--------------:|-------------|
 | Invoice created (membership) | 1410 Forderungen Mitgliedsbeiträge | 4400 Erlöse Mitgliedsbeiträge | "Mitgliedsbeitrag {month} {memberName}" |
 | Invoice created (course) | 1450 Forderungen Kurse | 4401 Erlöse Kurse/Events | "Kurs: {courseName}" |
 | Invoice created (shop) | 1460 Forderungen Shop | 4402 Erlöse Shop | "Shop-Bestellung {orderNumber}" |
 | SEPA payment received | 1200 Bank | 1410/1450/1460 Forderungen | "SEPA-Einzug {transactionRef}" |
 | Storno (full) | 4410 Erlösschmälerungen | 1410 Forderungen | "Storno: {invoiceNumber}" |
 | Storno (partial) | 4410 Erlösschmälerungen | 1410 Forderungen | "Teilstorno: {invoiceNumber} ({amount})" |
 | SEPA rejection (Rücklastschrift) | 1510 Forderungen Rücklastschriften | 1200 Bank | "Rücklastschrift: {memberName}" |
 | Rücklastschrift fee | 6820 Rücklastschrift-Kosten | 1200 Bank | "Gebühr Rücklastschrift" |
 | Payment provider fee | 6815 Provider-Gebühren | 1200 Bank | "Transaktionsgebühr {provider}" |
 | Fremdbeträge pass-through | 1710 Verbindlichkeiten Fremdbeträge | 4440 Durchlaufposten | "Fremdbeträge {entityName}" |

 Each posting creates exactly 2 `AccountingEntry` rows (debit + credit, same amount) — enforced by `AccountingService.createDoubleEntry()`.
  • Cost center management: CRUD, assign to entries, default cost center per organization
  • Period close: lock all entries for a month (periodClosed = true), prevent further modifications, require all entries posted
  • Financial reports:
    • P&L (GuV): Revenue accounts (4xxx) minus expense accounts (6xxx/7xxx), grouped by account
    • Balance by account: Opening + movements + closing per account
    • Cash flow: Bank account movements, grouped by week/month
    • Receivables aging: Unpaid invoices grouped by age (0-30d, 30-60d, 60-90d, >90d) 3. Create tests: AccountingServiceTest

Result: Accounting with double-entry bookkeeping and SKR03.


Step 8.6 — DATEV Export and Reconciliation

(reference: Chapter 12 — Integration (DATEV section))

Approach:

  1. Create DatevExportService: - DATEV CSV format (Buchungsstapel, per DATEV specification):

    • Header record (row 1): "EXTF";700;21;"Buchungsstapel";13;...
    • Format version: 700
    • Data category: 21 (Buchungsstapel)
    • Format name: "Buchungsstapel"
    • Berater-Nr (tax consultant number, configurable)
    • Mandanten-Nr (client number = entity ID)
    • WJ-Beginn (fiscal year start, default 01/01)
    • Sachkontenlange (4 for SKR03)
    • Datum-von / Datum-bis (export period)
    • Data rows (row 2+):
    Column DATEV Field Source Example
    1 Umsatz (S/H) amount 29.00
    2 S/H-Kennzeichen derived S (Soll/debit) or H (Haben/credit)
    3 WKZ Umsatz currency EUR
    7 Konto accountCode 1400
    8 Gegenkonto counterAccountCode 4400
    9 BU-Schlüssel derived from taxRate 3 (19% USt) or 2 (7% USt)
    10 Belegdatum entryDate 2202 (DDMM)
    11 Belegfeld 1 entryNumber B-2026-00042
    13 Buchungstext description "Mitgliedsbeitrag Jan 2026"
    14 Kostenstelle 1 costCenter.code KST-100
  • DATEV XML format (optional, for Unternehmen Online): implement only if customer demand exists
  • Export filter: date range, cost center, account range
  • Validation: all entries in period must be posted, no gaps in numbering
  • File naming: EXTF_Buchungsstapel_{entityId}_{YYYY-MM}.csv 2. Create ReconciliationService:
  • Compare accounting entries with Cash360 transaction data
  • Identify discrepancies: missing entries, amount mismatches
  • Reconciliation algorithm (per month):

    1. Fetch all Cash360 transactions for entity + period from Cash360 API
    2. Fetch all AccountingEntry records for entity + period from local DB
    3. Match by: transactionId (reference field), amount, date (±1 day tolerance)
    4. Categorize results:

      Category Description Action
      MATCHED Transaction + entry exist, amounts equal No action
      AMOUNT_MISMATCH Both exist, amounts differ Flag for manual review
      MISSING_ENTRY Transaction exists, no accounting entry Auto-create suggested entry
      ORPHAN_ENTRY Accounting entry exists, no transaction Flag for investigation
      TIMING_DIFFERENCE Transaction in period N, entry in period N+1 Informational warning
    5. Generate reconciliation report with summary totals per category - Reconciliation report: matched count, unmatched count, suggested corrections, total variance

    6. Create controller: - GET /api/accounting/entries — Search accounting entries - POST /api/accounting/entries — Create manual entry - GET /api/accounting/cost-centers — List cost centers - POST /api/accounting/cost-centers — Create cost center - GET /api/accounting/chart-of-accounts — List accounts - POST /api/accounting/datev/export?from={date}&to={date} — Generate DATEV export - GET /api/accounting/datev/export/{id}/download — Download DATEV file - POST /api/accounting/period/close?month={YYYY-MM} — Close accounting period - GET /api/accounting/reconciliation?month={YYYY-MM} — Run reconciliation - GET /api/accounting/reports/pnl?from={date}&to={date} — P&L report (GuV) - GET /api/accounting/reports/balance?date={date} — Balance by account
    7. Create Flyway: V802__create_accounting.sql
    8. Create admin UI: - Ledger view: DataTable with account, date, amount, description, cost center - DATEV export: Date range picker, download button, export history - Period close: Month selector, close button, status indicator - Reports: P&L chart, balance table, reconciliation differences

Result: Accounting with DATEV export, reconciliation, and period-close.


Step 8.7 — Phase 8 Documentation

  1. Update doc/enduser/admin-manual.md (CRM, support, accounting sections)
  2. Update doc/enduser/consumer-manual.md (support tickets, knowledge base)
  3. Update doc/developer/backend-guide.md (CRM, support, accounting architecture)
  4. Update doc/developer/api-reference.md (new endpoints)

Step 8.8 — Update Intranet

Run python doc/intranet/build.py.


Phase 8 — Quality Gate

# Check Target
1 Conception exists
2 Compilation + tests 0 errors, 0 failures
3 Coverage >= 70%
4 CRM create lead -> advance pipeline -> convert to member
5 Pipeline board Kanban drag-and-drop works
6 Support ticket create -> assign -> comment -> resolve -> CSAT survey
7 SLA deadline calculated correctly per tier
8 Knowledge base create article -> search -> view count
9 Accounting billing event creates ledger entries (double-entry)
10 DATEV export produces valid CSV with correct header
11 Period close locks entries, prevents modification
12 Reconciliation identifies discrepancies
13 Documentation + intranet updated
14 CLAUDE.md updated

Report: "Phase 8 completed."

---