AIQ BILLING MOCK
Demo-Abrechnungssystem eines deutschen Energieversorgers (Strom & Gas) — zentrale Backend-Datenquelle für Kundenportal, Bestellstrecke und Vertriebspartnerportal, inklusive Admin-Frontend zur Simulation realer Abrechnungs- und Vertriebsvorgänge.
Überblick
Das aiq-billing-mock bildet ein echtes Billing-System für Produktdemos nach. Es zeigt, wie Daten in einem Abrechnungssystem ankommen bzw. abgerufen werden, und stellt dafür bereit:
- REST-APIs je Modul (Bestellstrecke, Vertrieb, Kundenportal) mit Swagger/OpenAPI-3.1-Doku.
- ein CONUTI-gebrandetes Admin-Frontend zum Ansehen und Simulieren von Vorgängen (Storno, Netzbetreiber-Ablehnung, Lieferbeginn, Kündigung, Sperrung, Schlussrechnung).
- einen idempotenten Demo-Datensatz (7 Personas) mit gültigen MaLo-/MeLo-/IBAN-IDs.
Architektur
Ein Next.js-16-Monolith stellt sowohl die Modul-APIs als auch das Admin-Frontend bereit und
spricht eine PostgreSQL-Datenbank über Prisma an. Die drei Konsumenten-Frontends sind externe
Module; das Output-Management (Dokumente/E-Mails) ist das externe System output.rocks.
Tech-Stack
| Bereich | Technologie | Hinweis |
|---|---|---|
| Framework | Next.js 16 (App Router) | APIs + Admin-UI in einem Projekt; dynamische params sind ein Promise |
| Datenbank | PostgreSQL 17 | postgres:17-alpine |
| ORM | Prisma 7 | Driver-Adapter PrismaPg (Pflicht), Client in lib/generated/prisma |
| Validierung + OpenAPI | Zod 4 + zod-openapi | Single Source: ein Schema validiert und erzeugt das Spec |
| Swagger-UI | swagger-ui-react | unter /api/docs |
| Auth | Auth.js v5 + API-Key-Bearer | Admin-Session (Rolle ADMIN) + Modul-Keys + Customer-JWT |
| Styling | Tailwind v4 + CONUTI-Tokens | Raleway, teal/lime, Pills/Kreise, Lucide-Icons |
| Betrieb | Docker Compose | migrate + seed + start automatisch beim Container-Start |
Datenmodell
Kern ist die SAP-IS-U-nahe Hierarchie Geschäftspartner → Vertragskonto → Vertrag →
Lieferstelle → Zähler → Zählerstand. Strom + Gas eines Kunden bündeln sich auf einem
Vertragskonto (eine Bankverbindung / ein SEPA-Mandat). Geld als Decimal, IDs als
cuid(), Zeitstempel als Timestamptz.
- Geschaeftspartner (Kunde)
- Vertragskonto (bündelt Rechnung/Zahlung)
- Bankverbindung, SepaMandat
- MarketPartner (Lieferant/VNB/MSB, MP-ID)
- Netzgebiet, PlzNetzgebiet, GasParameter
- Vertrag (Sparte, Status, Laufzeit, Prognose)
- Lieferstelle (MaLo/MeLo, Adresse)
- Zaehler (Typ, HT/NT, Einspeisung)
- Zaehlerstand (OBIS, Plausibilität)
- Tarif, TarifPreis (je Netzgebiet, Provision)
- Abschlagsplan
- Rechnung (Turnus/Schluss, Saldo)
- Dokument (Metadaten + output.rocks-Ref)
- Partner, PartnerUser
- Order (Vertrieb + Bestellstrecke)
- OrderStatusHistorie
- Commission, Clawback, ImportBatch
- Vorgang (Querschnitts-Servicefall)
- VorgangStatusHistorie, Nachricht
- EreignisLog (append-only Audit über alles)
- ApiClient (Modul-Keys), AdminUser
State Machines & Simulation
Drei verknüpfte Zustandsautomaten als Transition-Tabellen. Zustandswechsel erfolgen nur über
Service-Funktionen; jeder Wechsel schreibt einen EreignisLog-Eintrag. Ein ungültiger
Übergang liefert 409 INVALID_TRANSITION. Das Admin-Frontend zeigt pro Status nur
die gültigen Aktions-Buttons.
Order (Auftrag)
Vertrag
Admin-Simulationen & Seiteneffekte
| Aktion (Endpunkt) | Wirkung |
|---|---|
bonitaet_setzen | setzt Bonität; OK → Übermittlung, ABGELEHNT → BONITAET_ABGELEHNT |
bestaetigen | Order → BESTAETIGT; bucht Commission (RESERVIERT) bei Partner-Order |
lieferbeginn_bestaetigen | Order → IN_VERTRAG_UEBERFUEHRT; Vertrag → AKTIV; Commission → VERDIENT |
netzbetreiber_ablehnung (+ Grund) | Order → ABGELEHNT; löst Clawback aus (Grund-Enum) |
storno_simulieren / widerruf | Order → STORNIERT/WIDERRUFEN; Clawback |
kuendigung_bestaetigen | Vertrag → GEKUENDIGT; Lieferende + Kündigungsbestätigung (Dokument) |
schlussrechnung_erzeugen | erzeugt SCHLUSSRECHNUNG + Dokument |
sperrung / entsperrung / mahnung | Vertragsstatus + Sperr-Flag |
time/advance | spult fristabhängige Felder eines Auftrags vor (Demo) |
Validierung
Zentrale Utilities (lib/validation/) werden von der API und vom Seed genutzt —
dadurch sind alle Demo-IDs garantiert gültig.
| Prüfung | Regel |
|---|---|
| MaLo-ID | 11-stellig, Prüfziffer nach Lok-/Waggon-Verfahren (Mod-10) |
| MP-ID / Codenummer | 13-stellig, gleiches Prüfziffer-Verfahren |
| MeLo-ID | 33 Zeichen (DE + 6 Codenummer + 5 PLZ + 20 Zählpunkt) |
| IBAN | ISO-7064 Mod-97 |
| Zählerstand | Rückläufer = Hard-Fail (außer Zählerwechsel/Einspeisung); Verbrauch außerhalb ~50–200 % der Prognose = Warnung mit Bestätigungspflicht; Datum nicht in Zukunft/vor letztem Stand |
| Abschlag | 50–200 % vom Soll |
| Gas | kWh = m³ × Zustandszahl × Brennwert |
Auth-Konzept
| Endpunktgruppe | Authentifizierung |
|---|---|
/api/order/** | Modul-Bearer (aiq_order_demo_key) |
/api/sales/** | Modul-Bearer + Header X-Partner-Id (Mandant) + Scopes |
/api/customer/auth/login | Modul-Bearer (aiq_customer_demo_key) |
übrige /api/customer/** | Modul-Bearer + X-Customer-Token (JWT, kundengescoped) |
/api/admin/** + /admin | Auth.js-Session, Rolle ADMIN (proxy.ts) |
/api/docs/** | öffentlich (lokale Demo) |
API · Bestellstrecke Bearer: aiq_order_demo_key
| Methode | Pfad | Zweck |
|---|---|---|
| GET | /api/order/verfuegbarkeit | Belieferbarkeit + Netzbetreiber zur Adresse |
| GET | /api/order/tarife | Tarife nach PLZ/Sparte/Verbrauch (inkl. Jahreskosten, Monatsabschlag) |
| POST | /api/order/tarife/berechnung | Exakte Preisberechnung inkl. USt |
| POST | /api/order/orders | Auftrag abschließen (legt Order + Vorgang + Widerrufsfrist an) |
| GET | /api/order/orders/{nr}/status | Auftragsstatus |
| POST | /api/order/orders/{nr}/widerruf | Widerruf (14-Tage-Frist) |
| POST | /api/order/orders/{nr}/bonitaet | Bonitätsprüfung (Mock) |
API · Vertrieb Bearer: aiq_sales_demo_key · X-Partner-Id
| Methode | Pfad | Zweck |
|---|---|---|
| POST | /api/sales/auth/login | Partner-Login → liefert partnerId/Kennung |
| GET | /api/sales/tariffs | Tarif- + Provisions-Feed |
| POST | /api/sales/orders | Auftrag/Lead übermitteln (qualifiziert/unqualifiziert) |
| GET | /api/sales/orders | eigene Aufträge (mandantengetrennt) |
| GET | /api/sales/orders/{nr} · /history | Status + Statushistorie |
| POST | /api/sales/orders/import | Listen-Import (Leads) → ImportBatch + Fehlerprotokoll |
| GET | /api/sales/commissions | Provisionsübersicht inkl. Stornoreserve |
| GET | /api/sales/reporting/performance · /forecast | Abschluss-/Stornoquote, Provisions-Forecast |
API · Kundenportal Bearer: aiq_customer_demo_key · X-Customer-Token
| Methode | Pfad | Zweck |
|---|---|---|
| POST | /api/customer/auth/login | Login (Vertragsnr + Nachname/Geburtsdatum) → JWT |
| GET | /api/customer/contracts · /{nr} · /{nr}/meters | Verträge, Vertragsdetails, Zähler |
| POST/GET | /api/customer/contracts/{nr}/meter-readings | Zählerstand melden (Plausibilität) / Historie |
| PUT | /api/customer/contracts/{nr}/installment | Abschlag ändern (Min/Max-Korridor) |
| PUT | /api/customer/bank-account | Bankdaten/SEPA ändern (IBAN-Prüfung, neues Mandat) |
| POST | /api/customer/contracts/{nr}/move · /termination | Umzug melden, Kündigung |
| GET | /api/customer/invoices · /documents · /cases | Rechnungen, Dokumentenarchiv, Vorgänge |
| POST | /api/customer/cases/{nr}/messages | Rückfrage/Nachricht zu einem Vorgang |
| PUT | /api/customer/profile | Kontaktdaten ändern |
Jede schreibende Aktion legt einen Vorgang mit Statushistorie an.
API · Admin Auth.js-Session
| Methode | Pfad | Zweck |
|---|---|---|
| POST | /api/admin/orders/{id}/actions | Auftrags-Simulationen (s. State Machines) |
| POST | /api/admin/contracts/{id}/actions | Vertrags-Aktionen (Kündigung, Sperrung, Schlussrechnung …) |
| POST | /api/admin/meter-readings/{id}/release | unplausiblen Zählerstand freigeben |
| POST | /api/admin/time/advance | Demo-Fristen vorspulen |
| GET/POST | /api/admin/api-clients | Modul-API-Keys verwalten (Klartext einmalig) |
Admin-Frontend
Server Components unter /admin lesen Prisma direkt; Mutationen laufen als Server
Actions über die Services. Seiten: Dashboard (KPIs + EreignisLog-Timeline), Kunden,
Verträge (mit Aktions-Rail), Zählerstände (Freigabe), Tarife, Aufträge
(Status-Flow + Simulations-Rail mit Grund-Dropdowns), Vorgänge (Nachrichten-Thread),
Dokumente, API-Clients, Swagger. CONUTI-Stil: teal Sidebar, lime Akzent,
Pills, Karten mit weicher Schattierung, Raleway, Lucide-Icons.
Seed & Personas
Der Seed ist idempotent (clear-then-insert, Zeitbezüge relativ zu now()). Stammdaten:
4 Regionen/Netzgebiete (Stuttgart, München, Berlin, Köln), je Sparte 3–4 Tarife mit Preisen je
Netzgebiet, GasParameter, Marktrollen.
| # | Persona | Demonstriert |
|---|---|---|
| 1 | Anna Berger | Strom Eintarif AKTIV (Standardfall: Stände, Jahresabrechnung, Vorgang mit Rückfrage) |
| 2 | Max Sonnberg | Strom Zweitarif + PV (HT/NT + Einspeisung, OBIS) |
| 3 | Petra Gasmann | Gas mit laufendem Lieferantenwechsel (Admin: Lieferbeginn bestätigen) |
| 4 | Klaus Stein | Gas-Auftrag vom Netzbetreiber abgelehnt |
| 5 | Familie Kombi | Strom + Gas auf einem Vertragskonto (gebündelt) |
| 6 | Sandra Wechsler | Check24-Auftrag (bestätigt, Provision reserviert) |
| 7 | Tom Neukunde | Storno/Widerruf-Kandidat (Widerrufsfrist offen) |
Setup & Betrieb
# Aus dem Repo-Root:
docker compose up --build # Postgres + App: migrate + seed + start
# Admin: http://localhost:3000/admin (admin / admin)
# Swagger: http://localhost:3000/api/docs
docker compose down -v # inkl. DB-Volume zurücksetzen
# Lokale Entwicklung (aiq-billing-mock/):
docker compose up -d postgres
cp .env.example .env && npm install
npm run db:migrate && npm run db:seed && npm run dev
Tests
Ein End-to-End-Smoke-Test (scripts/smoke-test.sh) prüft alle Module gegen die
laufende App — 49 Checks, 0 Fehler: Status-Codes (200/201/401/403/422/409),
Auth-Gates, Tarifberechnung, Auftrags- & Simulationsketten (Order → Vertrag → Commission),
Zählerstand-Plausibilität, IBAN-Prüfung, OpenAPI-Specs, Swagger-UI.
bash aiq-billing-mock/scripts/smoke-test.sh
# ==== RESULT: 49 passed, 0 failed ====
Scope & Grenzen
- Keine Marktkommunikation (EDIFACT/UTILMD/APERAK, Bilanzierung, MaStR) — bewusst ausgeklammert.
- Dokumente sind Metadaten/Referenzen auf output.rocks, kein PDF-Rendering.
- Kein persistenter Bestellstrecken-Warenkorb (direkter Auftragsabschluss).
- Fristen/Werktage berücksichtigen nur bundesweite Feiertage (vereinfacht).
- EreignisLog/Historien sind per Konvention append-only (nicht DB-erzwungen).
- Ausschließlich für Produktdemos — kein Echtbetrieb.