Skip to content

Kosten-Management — Gesamtkonzept

Status

Konzept — noch nicht implementiert. Diese Seite beschreibt den Zielzustand für ein einheitliches Kosten- und Rechnungs-Management über Server, Module, Add-Ons und Mahn-Workflow.

Warum dieses Konzept?

Aktuell ist Prilogs Kosten- und Rechnungslogik fragmentiert:

  • ServerOrder kennt nur einen monthlyPrice (eine Decimal-Spalte)
  • ServerTier definiert Basispreis + Preis pro Nutzer
  • SynapseModule hat optional einen priceMonthly
  • ServerModule verbindet Order mit aktivierten Modulen — aber niemand summiert das automatisch auf
  • Invoices existieren als API-Endpoint, geben aber bisher leeres Array zurück (return { success: true, invoices: [] })
  • Stripe ist halb angebunden (Customer + Subscription IDs in der DB), aber ohne automatische Synchronisation
  • Mahn-Workflow (overdue → suspended → cancelled) ist nirgendwo implementiert
  • Whisper läuft umsonst — Lee zahlt den Server-Tier ohne pro-Tenant-Tracking

Ergebnis: Lee kennt seinen tatsächlichen Monatsumsatz nur mit manuellem Kopfrechnen. Eine Schule die 6 Monate nicht zahlt, läuft trotzdem weiter. Modul-Aktivierungen führen zu keinem höheren Rechnungsbetrag.

Dieses Konzept löst das in einem zusammenhängenden Modell.

Zielbild in einem Bild

                              ┌─────────────────────────────┐
                              │  AdminPortal (prilog-admin) │
                              │                             │
                              │  • MRR / ARR Übersicht      │
                              │  • Forderungs-Übersicht     │
                              │  • Mahn-Stati pro Schule    │
                              │  • Modul-Umsatz pro Modul   │
                              └──────────────┬──────────────┘


┌──────────────────┐         ┌──────────────────────────────┐         ┌──────────────────┐
│ ServerOrder      │────────▶│  Cost Engine                 │◀────────│  Stripe          │
│ (eine Schule)    │         │                              │         │  (Subscriptions) │
│                  │         │  monthlyTotal = base         │         │                  │
│ + ServerTier     │         │               + perUserCost  │         │  Webhooks:       │
│ + Module[]       │         │               + sumModules   │         │  paid / failed / │
│ + AddOns[]       │         │               + sumAddOns    │         │  past_due        │
└──────────────────┘         │                              │         └──────────────────┘
                             │  generateInvoice() monthly   │
                             └──────────────┬───────────────┘


                            ┌────────────────────────────────┐
                            │  Invoice                       │
                            │                                │
                            │  status: paid|overdue|suspended│         ┌──────────────────┐
                            │  dueDate, amount, items[]      │────────▶│  KundenPortal    │
                            │  pdfUrl                        │         │  (prilog-portal) │
                            └────────────────────────────────┘         │                  │
                                            │                          │  Rechnungs-Liste │
                                            │                          │  ▢ grün ▢ orange │
                                            ▼                          │  ▢ rot           │
                            ┌────────────────────────────────┐         │  Download PDF    │
                            │  Mahn-Workflow                 │         └──────────────────┘
                            │                                │
                            │  +14 Tage: 1. Mahnung (orange) │
                            │  +30 Tage: 2. Mahnung (orange) │
                            │  +45 Tage: Dienst suspend (rot)│
                            │  +60 Tage: Kündigung           │
                            └────────────────────────────────┘

§1 Datenmodell

1.1 Bestand (vorhanden)

ModellZweckBestand
ServerOrder.monthlyPriceMonatspreis pro Schule
ServerTierServer-Stufen mit basePrice, installationPrice, pricePerUser, usersUpTo
SynapseModule.priceMonthlyModul-Preis (optional, null = im Grundpreis)
SynapseModule.stripePriceIdStripe Price ID für recurring
ServerModule.stripeSubscriptionItemIdStripe Subscription Item für aktiviertes Modul
ServerOrder.stripeCustomerIdStripe Customer ID
ServerOrder.stripeSubscriptionIdMaster Subscription

1.2 Neu (zu bauen)

Modell Invoice (heute leerer Stub, neu definieren)

prisma
model Invoice {
  id              String   @id @default(cuid())
  orderId         Int      @map("order_id")
  invoiceNumber   String   @unique @map("invoice_number") @db.VarChar(20)  // z.B. "2026-04-0042"

  // Zeitraum
  periodStart     DateTime @map("period_start")
  periodEnd       DateTime @map("period_end")
  issuedAt        DateTime @default(now()) @map("issued_at")
  dueAt           DateTime @map("due_at")           // typisch issuedAt + 14 Tage

  // Beträge (alle in Euro, Decimal(10,2))
  subtotalNet     Decimal  @map("subtotal_net") @db.Decimal(10, 2)
  vatRate         Decimal  @default(19.0) @map("vat_rate") @db.Decimal(5, 2)
  vatAmount       Decimal  @map("vat_amount") @db.Decimal(10, 2)
  totalGross      Decimal  @map("total_gross") @db.Decimal(10, 2)

  // Status (siehe §3 Mahn-Workflow)
  status          String   @default("pending") @db.VarChar(20)
  // pending → paid → ok
  // pending → overdue (>dueAt) → reminded_1 → reminded_2 → suspended → cancelled

  // Zahlungs-Tracking
  paidAt          DateTime? @map("paid_at")
  paidAmount      Decimal?  @map("paid_amount") @db.Decimal(10, 2)
  paymentMethod   String?   @map("payment_method") @db.VarChar(50)  // stripe | sepa | bank | manual
  stripeInvoiceId String?   @unique @map("stripe_invoice_id") @db.VarChar(100)

  // PDF
  pdfUrl          String?   @map("pdf_url") @db.Text
  pdfGeneratedAt  DateTime? @map("pdf_generated_at")

  // Mahnstufe (Anzahl bereits versendeter Mahnungen)
  reminderLevel   Int       @default(0) @map("reminder_level")
  lastReminderAt  DateTime? @map("last_reminder_at")
  suspendedAt     DateTime? @map("suspended_at")

  // Beziehungen
  order           ServerOrder @relation(fields: [orderId], references: [id])
  items           InvoiceItem[]

  createdAt       DateTime @default(now()) @map("created_at")
  updatedAt       DateTime @updatedAt @map("updated_at")

  @@index([orderId, status])
  @@index([dueAt, status])  // für Mahn-Cron
  @@map("invoices")
}

model InvoiceItem {
  id           String  @id @default(cuid())
  invoiceId    String  @map("invoice_id")

  // Was wird abgerechnet?
  itemType     String  @db.VarChar(30)  // server_base | per_user | module | addon | one_time
  itemRefId    String? @db.VarChar(50)  // z.B. SynapseModule.id für Module
  description  String  @db.VarChar(255) // "Server Tier 'Standard' (Apr 2026)" oder "Krisenmanagement-Modul (Apr 2026)"

  quantity     Decimal @default(1) @db.Decimal(10, 2)  // z.B. Anzahl Nutzer bei per_user
  unitPriceNet Decimal @map("unit_price_net") @db.Decimal(10, 4)
  totalNet     Decimal @map("total_net") @db.Decimal(10, 2)

  invoice      Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)

  @@index([invoiceId])
  @@map("invoice_items")
}

Erweiterung ServerOrder

prisma
model ServerOrder {
  // ...bestehende Felder...

  // Mahn-Status auf Tenant-Ebene (aggregiert über Invoices)
  paymentHealthStatus String  @default("ok") @map("payment_health_status") @db.VarChar(20)
  // ok | overdue | suspended | cancelled
  serviceSuspendedAt  DateTime? @map("service_suspended_at")

  // Beziehungen
  invoices            Invoice[]
}

paymentHealthStatus ist die Single Source of Truth für die Kunden-Portal- Anzeige (grün/orange/rot) und für das Backend (darf der Tenant noch Module nutzen?).

1.3 Add-On-Modell

Neben Modulen brauchen wir auch Add-Ons (z.B. „Whisper Premium", „mehr Storage", „SMS-Versand-Kontingent"). Diese sind nicht im Modul-Katalog (SynapseModule) sondern eigenes Modell:

prisma
model AddOnCatalog {
  id            Int     @id @default(autoincrement())
  key           String  @unique @db.VarChar(50)        // "extra_storage_50gb"
  name          String  @db.VarChar(100)               // "Zusätzlicher Storage 50 GB"
  description   String? @db.Text
  category      String  @db.VarChar(50)                // storage | transcription | sms | other
  priceMonthly  Decimal @map("price_monthly") @db.Decimal(10, 2)
  unit          String  @default("flatrate") @db.VarChar(30)  // flatrate | per_unit
  active        Boolean @default(true)

  orderAddOns   ServerOrderAddOn[]
  @@map("addon_catalog")
}

model ServerOrderAddOn {
  id           Int      @id @default(autoincrement())
  orderId      Int      @map("order_id")
  addOnId      Int      @map("addon_id")
  quantity     Int      @default(1)
  enabledAt    DateTime @default(now()) @map("enabled_at")
  disabledAt   DateTime? @map("disabled_at")

  order   ServerOrder  @relation(fields: [orderId], references: [id])
  addOn   AddOnCatalog @relation(fields: [addOnId], references: [id])

  @@unique([orderId, addOnId])
  @@map("server_order_addons")
}

§2 Cost-Engine

Eine zentrale Service-Funktion calculateMonthlyTotal(orderId) berechnet den aktuellen Monatspreis einer Schule:

typescript
async function calculateMonthlyTotal(orderId: number): Promise<MonthlyBreakdown> {
  const order = await prisma.serverOrder.findUnique({
    where: { id: orderId },
    include: {
      serverTier: true,
      modules: { include: { module: true } },
      addOns: { include: { addOn: true } }
    }
  });

  // 1. Server-Basispreis
  const base = order.customPricePerUser
    ? Decimal(0)              // Custom-Mode: nur per-User
    : order.serverTier.basePrice;

  // 2. Pro-Nutzer-Preis (bei Custom-Mode override, sonst Tier-Default)
  const perUserPrice = order.customPricePerUser ?? order.serverTier.pricePerUser;
  const userCount = order.staffCount + order.studentsCount + order.parentsCount;
  const perUserTotal = perUserPrice.mul(userCount);

  // 3. Modul-Aufschläge
  const moduleTotal = order.modules
    .filter(m => m.enabled && m.module.priceMonthly)
    .reduce((sum, m) => sum.add(m.module.priceMonthly), Decimal(0));

  // 4. Add-On-Aufschläge
  const addOnTotal = order.addOns
    .filter(a => !a.disabledAt)
    .reduce((sum, a) => sum.add(a.addOn.priceMonthly.mul(a.quantity)), Decimal(0));

  const subtotal = base.add(perUserTotal).add(moduleTotal).add(addOnTotal);

  return {
    items: [
      { type: 'server_base', label: order.serverTier.nameExternal, amount: base },
      { type: 'per_user', label: `${userCount} Nutzer × ${perUserPrice}€`, amount: perUserTotal },
      ...modulesItems,
      ...addOnItems,
    ],
    subtotalNet: subtotal,
    vatRate: 19,
    vatAmount: subtotal.mul(0.19),
    totalGross: subtotal.mul(1.19)
  };
}

Diese Funktion ist die einzige Stelle an der ein Monatspreis berechnet wird. Wird verwendet von:

  • Admin-Dashboard (MRR/ARR-Berechnung)
  • Cron-Job „generateInvoices" am 1. eines Monats
  • Portal-Ansicht „Mein Server" — zeigt aktuelle Preis-Aufschlüsselung
  • Onboarding-Wizard — bevor die Schule den Vertrag abschliesst
  • Stripe-Sync — synct ServerOrder.monthlyPrice mit Stripe-Subscription

§3 Mahn-Workflow

Status-Übergänge einer Rechnung:

                    ┌──────────────┐
                    │   pending    │  (Rechnung ausgestellt, dueAt in 14 Tagen)
                    └──────┬───────┘

              ┌────────────┼────────────┐
              │            │            │
              ▼            ▼            ▼
        ┌─────────┐  ┌──────────┐  ┌──────────┐
        │  paid   │  │ overdue  │  │ disputed │  (manueller Override)
        └─────────┘  └────┬─────┘  └──────────┘
                          │  +1 Tag nach dueAt

                    ┌──────────────┐
                    │ reminded_1   │  +14 Tage nach dueAt → Email "1. Erinnerung"
                    └──────┬───────┘


                    ┌──────────────┐
                    │ reminded_2   │  +28 Tage nach dueAt → Email "2. Mahnung"
                    └──────┬───────┘     + persönlicher Anruf empfohlen


                    ┌──────────────┐
                    │  suspended   │  +45 Tage nach dueAt
                    └──────┬───────┘  → ServerOrder.paymentHealthStatus = 'suspended'
                           │           → Synapse → read-only Mode
                           │           → Web-Client zeigt Banner


                    ┌──────────────┐
                    │  cancelled   │  +60 Tage nach dueAt
                    └──────────────┘  → Vertragskündigung, Daten-Export

3.1 Mahn-Cron

Ein Cron-Job läuft täglich um 06:00 Uhr und macht Folgendes:

typescript
// src/core/cron/jobs.ts
registerCronJob({
  key: 'invoice-dunning',
  name: 'Mahnstufen aktualisieren',
  schedule: '0 6 * * *',
  enabled: true,
  async handler() {
    const now = new Date();
    const overdueInvoices = await prisma.invoice.findMany({
      where: { status: { in: ['pending', 'reminded_1', 'reminded_2'] } }
    });

    for (const inv of overdueInvoices) {
      const daysOverdue = differenceInDays(now, inv.dueAt);

      if (daysOverdue >= 45 && inv.status !== 'suspended') {
        await suspendService(inv.orderId);
        await prisma.invoice.update({ where: { id: inv.id }, data: { status: 'suspended', suspendedAt: now } });
      } else if (daysOverdue >= 28 && inv.status !== 'reminded_2') {
        await sendReminderEmail(inv, 2);
        await prisma.invoice.update({ where: { id: inv.id }, data: { status: 'reminded_2', reminderLevel: 2, lastReminderAt: now } });
      } else if (daysOverdue >= 14 && inv.status !== 'reminded_1') {
        await sendReminderEmail(inv, 1);
        await prisma.invoice.update({ where: { id: inv.id }, data: { status: 'reminded_1', reminderLevel: 1, lastReminderAt: now } });
      } else if (daysOverdue > 0 && inv.status === 'pending') {
        await prisma.invoice.update({ where: { id: inv.id }, data: { status: 'overdue' } });
      }
    }

    // Aggregiere paymentHealthStatus auf ServerOrder
    await refreshPaymentHealthStatus();
  }
});

3.2 Service-Unterbrechung

suspendService(orderId) macht Folgendes:

  1. Setzt ServerOrder.paymentHealthStatus = 'suspended', serviceSuspendedAt = now
  2. Sendet Synapse-Admin-Befehl → alle Räume des Tenants in „read only" (m.room.history_visibility bleibt, neue Messages werden via Modul-Hook geblockt)
  3. Web-Client zeigt fullscreen Banner: „Dieser Prilog-Server ist wegen ausstehender Zahlung pausiert. Bitte kontaktiere [Kontakt-Email]"
  4. Sendet Email an Schul-Verwaltung + Prilog-Operator
  5. Schreibt Audit-Log-Eintrag

resumeService(orderId) (manuell oder nach Zahlungseingang):

  1. Setzt paymentHealthStatus = 'ok', serviceSuspendedAt = null
  2. Synapse → write-Mode wieder offen
  3. Web-Client-Banner verschwindet
  4. Email „Dienst wieder aktiv"

§4 UI-Spezifikation

4.1 Admin-Portal (prilog-admin)

Neue Seite /finance/dashboard:

┌───────────────────────────────────────────────────────────────┐
│  Finanzen — Übersicht                                         │
│                                                               │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│  │ MRR     │ │ ARR     │ │ Aktive  │ │ Overdue │ │ Susp.   │ │
│  │ 2.480 € │ │ 29.760€ │ │ 18      │ │ 3       │ │ 1       │ │
│  └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│                                                               │
│  Modul-Umsatz aufgeschlüsselt                                 │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │ Krisenmanagement       12 Schulen × 12 €  =  144 €/Mo  │ │
│  │ DMS Premium             4 Schulen × 19 €  =   76 €/Mo  │ │
│  │ Whisper-Transkription   8 Schulen ×  9 €  =   72 €/Mo  │ │
│  └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘

Neue Seite /finance/invoices:

Tabelle aller Rechnungen aller Schulen, filterbar nach Status. Klick auf eine Rechnung → Detail-Seite mit Items, Zahlungs-Verlauf, manueller „als bezahlt markieren"-Knopf.

Neue Seite /finance/dunning:

Liste aller offenen Forderungen, sortiert nach Verzugsdauer. Markiert Schulen die kurz vor Service-Unterbrechung stehen, mit Button „Persönlich kontaktiert"

  • Notiz-Feld.

Erweiterung /orders/[orderId]:

Im Tab „Finanzen" eine Aufschlüsselung des aktuellen Monatspreises (calculateMonthlyTotal)

  • Liste der bisherigen Rechnungen + manueller Override-Knopf für customPricePerUser.

4.2 Kunden-Portal (prilog-portal)

Erweiterung /invoices:

┌─────────────────────────────────────────────────────────┐
│  Rechnungen                                             │
│                                                         │
│  Status: ● Bezahlt  ◐ Im Verzug  ● Dienst pausiert     │
│                                                         │
│  ┌─────────────────────────────────────────────────┐   │
│  │ ● 2026-04-0042   01.04.2026   95,20 €  [⬇ PDF] │   │
│  │   Status: bezahlt am 03.04.2026                 │   │
│  └─────────────────────────────────────────────────┘   │
│                                                         │
│  ┌─────────────────────────────────────────────────┐   │
│  │ ◐ 2026-03-0041   01.03.2026   95,20 €  [⬇ PDF] │   │
│  │   Fällig: 15.03.2026 — 27 Tage im Verzug        │   │
│  │   2. Mahnung versandt am 12.04.2026             │   │
│  │   ⚠ Bei weiterer Verzögerung wird der Dienst    │   │
│  │     am 30.04.2026 vorübergehend pausiert.       │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

Erweiterung /server (Seite „Mein Server"):

Zeigt eine Aufschlüsselung des aktuellen Monatspreises (vollständig nachvollziehbar):

Aktueller Monatspreis           95,20 € brutto
─────────────────────────────────────────
Server "Standard" Basis          7,00 €
+ 92 Nutzer × 0,40 €            36,80 €
+ Modul Krisenmanagement        12,00 €
+ Modul Whisper-Transkription    9,00 €
+ Add-On 50 GB Storage          15,00 €
─────────────────────────────────────────
Netto                           79,80 €
+ MwSt 19%                      15,16 €
─────────────────────────────────────────
Brutto                          94,96 €

(Differenz zur Gesamtsumme = Rundung auf vollen Cent)

4.3 Web-Client-Banner bei Suspension

Wenn paymentHealthStatus === 'suspended', zeigt das Web-Client einen vollflächigen Banner über dem Chat:

┌─────────────────────────────────────────────────────────┐
│ ⚠ Dieser Prilog-Server ist wegen ausstehender Zahlung   │
│   pausiert. Lesen ist möglich, neue Nachrichten         │
│   können nicht versendet werden.                         │
│                                                         │
│   Bitte kontaktiere die Schul-Verwaltung oder           │
│   leander@prilog.chat.                                  │
└─────────────────────────────────────────────────────────┘

§5 Stripe-Integration

Stripe ist die Quelle der Wahrheit für Zahlungseingänge. Die DB-Invoices sind Spiegel für interne Mahn-Logik und PDF-Generierung.

Webhook-Listener, der diese Stripe-Events behandelt:

Stripe-EventWas wir tun
invoice.createdDB-Invoice anlegen mit stripeInvoiceId, status pending
invoice.payment_succeededDB-Invoice auf paid, paidAt, paidAmount setzen, paymentHealthStatus refresh
invoice.payment_failedDB-Invoice bleibt pending (Mahn-Cron übernimmt)
invoice.finalizedPDF-URL aus Stripe übernehmen
customer.subscription.updatedModul-Aktivierungen syncen (Subscription Items ↔ ServerModule)
customer.subscription.deletedVertragskündigung verarbeiten

Webhook-Endpoint: POST /api/admin/stripe/webhook (existiert bereits, muss erweitert werden).

§6 Whisper-Cost-Tracking

Optional, für später wenn Whisper monetarisiert werden soll.

Ansatz: pro Tenant ein Counter transcribedSecondsThisMonth in der DB. Wird bei jeder erfolgreichen Transkription incrementiert (in transcribe.service.ts nach transcribeAudio()-Erfolg).

Modell:

prisma
model TenantUsageStats {
  id                      String   @id @default(cuid())
  tenantId                String   @map("tenant_id")
  yearMonth               String   @db.VarChar(7)         // "2026-04"
  transcribedSeconds      Int      @default(0) @map("transcribed_seconds")
  transcribedRequests     Int      @default(0) @map("transcribed_requests")

  tenant Tenant @relation(fields: [tenantId], references: [id])

  @@unique([tenantId, yearMonth])
  @@map("tenant_usage_stats")
}

Pricing-Optionen:

  • A: flatrate — feste 9 €/Monat unabhängig von Nutzung (einfach, vorhersagbar für Schule)
  • B: kontingent + überzogen — z.B. 9 € für 1000 s/Monat, danach 0,01 €/s
  • C: rein verbrauchsabhängig — 0,01 €/s

Empfehlung: A (Flatrate) für die ersten 50 Schulen. Sobald wir > 100 Schulen oder einzelne Power-User haben, auf B umstellen weil wir sonst draufzahlen.

Damit das funktioniert müssen wir erstmal die tatsächlichen Whisper-Kosten pro Schule kennen — daher das Tracking auch ohne Monetarisierung jetzt schon sinnvoll, als interne Kostenstellen-Erfassung.

§7 PDF-Generierung

Wir haben aktuell keine PDF-Engine. Optionen:

OptionVorteilNachteil
Stripe Hosted Invoice PDFsKostenlos, automatisch, professionellWir kontrollieren Layout nicht, branding ist Stripe-typisch
PDFKit / pdfmake (node)Volle Kontrolle, eigenes LayoutAufwand: ~2 Tage, Wartung
HTML → PDF via PuppeteerWir nutzen unser bestehendes CSS, sehr flexibelPuppeteer-Dependency, Chromium auf Server

Empfehlung: Phase 1 → Stripe Hosted PDFs (Stripe liefert URL, wir verlinken). Phase 2 → eigenes Template mit Puppeteer wenn Branding wichtig wird.

§8 Phasen-Plan

PhaseInhaltAufwandAbhängigkeit
0Schema-Erweiterung (Invoice, InvoiceItem, AddOn-Modelle, paymentHealthStatus)2 h
1calculateMonthlyTotal() Service + Tests4 h0
2Cron generateInvoices (1. eines Monats, alle aktiven Schulen)3 h1
3Stripe-Webhook erweitern (invoice.* Events) + Sync4 h0
4Mahn-Cron invoice-dunning (täglich 06:00) + Email-Templates6 h2, 3
5suspendService / resumeService + Web-Client-Banner4 h4
6Admin-UI: Finance-Dashboard + Invoices-Tabelle + Dunning-Liste8 h1, 2
7Portal-UI: erweiterte /invoices Seite + /server Preis-Aufschlüsselung4 h1, 2
8Whisper-Cost-Tracking (TenantUsageStats, Increment in transcribe.service)3 h– (parallel)
9Stripe-Test-Modus → Production-Switch + erste echte Rechnung2 h1–7

Gesamt-Aufwand: ca. 40 Stunden = 1 fokussierte Arbeitswoche

§9 KPIs für Lee

Sobald das System läuft, beantwortet das Admin-Dashboard diese Fragen ohne manuelles Rechnen:

KPIWie wird es berechnet?
MRR (Monthly Recurring Revenue)Σ aller calculateMonthlyTotal() für aktive Schulen
ARR (Annual Recurring Revenue)MRR × 12
Churn-RateAnteil cancelled Schulen / Total Schulen letzte 12 Monate
Average Revenue Per Account (ARPA)MRR / aktive Schulen
Outstanding ReceivablesΣ aller pending + overdue + reminded_* Invoices
Service Suspension RateAnteil suspended Schulen
Module PenetrationPro Modul: % der Schulen die es aktiviert haben
Module RevenuePro Modul: Σ moduleItems aus laufenden Invoices

§10 Offene Fragen / Entscheidungen

  1. Vorauszahlung oder nachträglich? Aktuell impliziert das Konzept Vorauskasse (Rechnung am 1. des Monats für DEN Monat). Alternative: nachträglich (1. Mai für April). Vorauskasse ist üblich bei SaaS, vereinfacht Mahnungen. → Lee entscheidet.

  2. Jahresrechnung-Option? Manche Schulen wollen lieber 1 × pro Jahr zahlen (mit Rabatt z.B. 10%). Sollte als optionale billingInterval: monthly | yearly auf ServerOrder umgesetzt werden. Phase 10+.

  3. Mehrwertsteuer: aktuell hardcoded 19%. Bei Schulen die als Körperschaft öffentlichen Rechts geführt sind, gelten teilweise Befreiungen. Sollte als Feld auf ServerOrder konfigurierbar sein (vatExempt: boolean + vatExemptReason).

  4. Mahn-Gebühren: in Deutschland zulässig, aber das Konzept sieht keine vor. Bei Bedarf einfach in Phase 4 ergänzen (InvoiceItem.itemType = 'reminder_fee').

  5. Sepa-Lastschrift: aktuell impliziert das Konzept Stripe-Card. SEPA-Direct-Debit ist über Stripe verfügbar, wäre aber zusätzlich zu konfigurieren. Wichtig für deutsche Schulen weil Karten unüblich.

  6. Service-Suspension UX: wie hart wollen wir suspendieren?

    • Soft: read-only, keine neuen Nachrichten
    • Hart: kompletter Login-Block Empfehlung: soft für die ersten 14 Tage Suspension, dann hart wenn weiter keine Reaktion.

§11 Verwandte Themen