BarSystem v2 - member-account POS for club bars: balances & top-ups, SumUp card and voucher checkout, barcode-scanned vouchers, sales reports, encrypted backups (local/VPS/S3)
  • JavaScript 96.3%
  • Python 2%
  • CSS 0.8%
  • HTML 0.4%
  • Shell 0.3%
  • Other 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
korpo fbdb3d6972
All checks were successful
CI / typecheck (push) Successful in 5m39s
CI / check-and-test (push) Successful in 6m17s
CI / image (push) Has been skipped
CI / release (push) Has been skipped
CI / backup-suites (push) Successful in 10m46s
chore(hygiene): tote Exporte, tote i18n-Keys, totes CSS + Doku-Drift bereinigt
Repo-Hygiene-Runde: erst suchen und belegen, dann löschen. Vier parallele
Prüfungen (tote Exporte, i18n-Katalog, Frontend/CSS, Doku-Drift), jede Löschung
mit Referenznachweis — nichts geraten.

Gelöscht (Code)
- 30 tote Exporte in 18 src-Dateien (module.exports-Zeilen; die internen
  Definitionen bleiben). Drei Kandidaten wurden als NICHT tot belegt und
  behalten: winAnsi und safeEqual (echte Nutzung — der Alt-Report war hier
  veraltet), pushToAll (in API.md dokumentiert).
- 33 tote i18n-Keys (37 Zeilen) — der advisory Report ist jetzt bei 0;
  inkl. der 6 unauffaelligen Kandidaten, die nur als JS-Identifier/API-Feld
  existieren (roleLabel, running, sessionActive, sold, tipGeneral, todaySales).
- 1 totes Global (doGuestCardHosted, seit 109c5fd von der Guarded-Variante
  abgeloest) und 3 tote CSS-Regeln (.session-banner, .topup-row,
  .free-drink-stop — letzteres ein Rename-Rest ohne Wirkung, Inline-Styles
  decken den Button ab).

Doku-Drift (falscher Inhalt, nicht Geschmack)
- README: FREIBIER-Limit stand als "unbegrenzt" — tatsaechlich 0 (Floor-Kette
  D50/v104/v105), inkl. vollstaendiger Liste der saldosenkenden Pfade.
- README: SW-Cache-Version stand als Handzahl v44 — tatsaechlich v57; die Zahl
  ist dreimal gedriftet und steht jetzt nicht mehr im README.
- DEPLOYMENT: PyPI-Proxy galt als "NICHT nutzbar (10/09)" — seit 25/09
  repariert und aktiv; Block auf Ist-Stand.
- TRUSTED_PROXY_IP: README/.env.example/DEPLOYMENT nannten `auto` als Default —
  Code-Default ist `loopback` (R-06); .env.example kommentiert das Opt-in jetzt aus.
- DEPLOYMENT: --maxWorkers=16 -> 8; "drei Typecheck-Jobs" -> einer; toter
  Verweis references/setup-wizard-and-bootstrap.md entfernt; ci-deploy-Reste und
  Typo; Messwerte/Runner-Config nur noch als Verweis auf INFRASTRUCTURE.md
  (die Zahlen waren auseinandergelaufen).
- API.md: Cookie-Session + X-CSRF-Token als Primaervertrag dokumentiert (Bearer
  nur noch als Uebergangs-Fallback) — sonst laufen API-Clients in 403;
  /receipts jetzt pos|reports statt "auth".
- package.json/-lock: 2.2.0 -> 2.63.0 (60 Releases Drift; npm install --dry-run
  bestaetigt ein stabiles Lockfile).
- .env.example: PRE_MIGRATION_BACKUP als Notausgang dokumentiert.
- CHANGELOG: Nachtrag am 12.x-Eintrag (heute v13.0.3).

Nicht geloescht (bewusst, mit Begruendung im Bericht): deploy.sh (gitignored,
Weg 1), Anleitungs-PDFs, vendor/jsqr.js (dynamisch geladen), sumup/types.js
(type-only), bankImport/index.js (Barrel), .dockerignore-Eintraege (von
deploy.sh begruendet), scripts/dev-set-portal-pin.js (manueller Dev-Helfer).

Gate: npm run check gruen, npm test 2888/2888 (223 Suiten).
2026-10-01 03:19:03 +02:00
.forgejo Review 30/09/2026 — Wellen D–F, Welle E und Wiederholungs-Review 2026-10-01 01:19:17 +02:00
docs chore(hygiene): tote Exporte, tote i18n-Keys, totes CSS + Doku-Drift bereinigt 2026-10-01 03:19:03 +02:00
public chore(hygiene): tote Exporte, tote i18n-Keys, totes CSS + Doku-Drift bereinigt 2026-10-01 03:19:03 +02:00
scripts Review 30/09/2026 — Wellen D–F, Welle E und Wiederholungs-Review 2026-10-01 01:19:17 +02:00
src chore(hygiene): tote Exporte, tote i18n-Keys, totes CSS + Doku-Drift bereinigt 2026-10-01 03:19:03 +02:00
tests docs(known-issues): auf lebende Liste reduziert (537 → 151 Zeilen) 2026-10-01 02:54:52 +02:00
types fix(types): TransactionType-Union um TRANSFER_OUT/TRANSFER_IN/PENALTY ergänzt 2026-09-05 19:04:22 +03:00
.dockerignore Review 30/09/2026 — Wellen D–F, Welle E und Wiederholungs-Review 2026-10-01 01:19:17 +02:00
.env.example chore(hygiene): tote Exporte, tote i18n-Keys, totes CSS + Doku-Drift bereinigt 2026-10-01 03:19:03 +02:00
.gitignore Adversariale Verifikation der Wellen A–C: acht unvollständige Fixes nachgezogen 2026-10-01 01:54:05 +02:00
.prettierrc feat: Deckel-Bar-Begleichen mit vollem Trinkgeld-Screen (wie Gast-Barzahlung) 2026-08-23 17:36:07 +02:00
AGENTS.md chore(hygiene): tote Exporte, tote i18n-Keys, totes CSS + Doku-Drift bereinigt 2026-10-01 03:19:03 +02:00
API.md chore(hygiene): tote Exporte, tote i18n-Keys, totes CSS + Doku-Drift bereinigt 2026-10-01 03:19:03 +02:00
backup_helper.py Review 30/09/2026 — Wellen D–F, Welle E und Wiederholungs-Review 2026-10-01 01:19:17 +02:00
CHANGELOG.md chore(hygiene): tote Exporte, tote i18n-Keys, totes CSS + Doku-Drift bereinigt 2026-10-01 03:19:03 +02:00
compose.build.yaml ci: Runner-Image mit Python-Deps + 3 parallele Jobs; Compose-Split, Bootstrap-Secrets, Doku-Audit 2026-09-25 15:27:48 +02:00
compose.yaml feat(security): Proxy-Trust automatisch ableiten (TRUSTED_PROXY_IP=auto) 2026-09-25 22:34:05 +02:00
docker-entrypoint.sh fix(setup): Panel-Sentinels 'null' abwehren + 426 trotz TLS diagnostizierbar 2026-09-27 21:58:52 +02:00
Dockerfile fix(ci): Image-Build ohne Nexus-PyPI-Proxy (Repo liefert 404) + korrekte pip-Proxy-Logik 2026-09-10 15:49:53 +02:00
eslint.config.js fix(gutscheine): Kamera-Scanner in Firefox/Safari via jsQR-Fallback 2026-09-28 01:50:45 +02:00
KNOWN_ISSUES.md chore(hygiene): tote Exporte, tote i18n-Keys, totes CSS + Doku-Drift bereinigt 2026-10-01 03:19:03 +02:00
LICENSE feat: Deckel-Bar-Begleichen mit vollem Trinkgeld-Screen (wie Gast-Barzahlung) 2026-08-23 17:36:07 +02:00
package-lock.json chore(hygiene): tote Exporte, tote i18n-Keys, totes CSS + Doku-Drift bereinigt 2026-10-01 03:19:03 +02:00
package.json chore(hygiene): tote Exporte, tote i18n-Keys, totes CSS + Doku-Drift bereinigt 2026-10-01 03:19:03 +02:00
README.md chore(hygiene): tote Exporte, tote i18n-Keys, totes CSS + Doku-Drift bereinigt 2026-10-01 03:19:03 +02:00
server.js fix(setup): Panel-Sentinels 'null' abwehren + 426 trotz TLS diagnostizierbar 2026-09-27 21:58:52 +02:00
start.bat feat: Deckel-Bar-Begleichen mit vollem Trinkgeld-Screen (wie Gast-Barzahlung) 2026-08-23 17:36:07 +02:00
start.sh feat: Deckel-Bar-Begleichen mit vollem Trinkgeld-Screen (wie Gast-Barzahlung) 2026-08-23 17:36:07 +02:00
tsconfig.checkjs.backend.json feat: Deckel-Bar-Begleichen mit vollem Trinkgeld-Screen (wie Gast-Barzahlung) 2026-08-23 17:36:07 +02:00
tsconfig.checkjs.base.json ci: Forgejo CI pipeline + Secrets cleanup + branch protection 2026-08-31 17:47:54 +03:00
tsconfig.checkjs.browser-base.json feat: Deckel-Bar-Begleichen mit vollem Trinkgeld-Screen (wie Gast-Barzahlung) 2026-08-23 17:36:07 +02:00
tsconfig.checkjs.frontend-portal.json KNOWN_ISSUES-Fixes + Feature-Paket 28/08/2026 (KI-2/4/5/6, NF-2/4/5/6/7/10) 2026-08-28 21:48:51 +02:00
tsconfig.checkjs.frontend-pos.json Review 30/09/2026 — Wellen D–F, Welle E und Wiederholungs-Review 2026-10-01 01:19:17 +02:00
tsconfig.checkjs.frontend-setup.json feat: Deckel-Bar-Begleichen mit vollem Trinkgeld-Screen (wie Gast-Barzahlung) 2026-08-23 17:36:07 +02:00
tsconfig.checkjs.frontend-voucher.json feat(gutscheine): öffentliche Gutschein-Seite — Saldo/Laufzeit-Selbstauskunft per QR 2026-09-28 03:39:02 +02:00
tsconfig.checkjs.tests.json feat: Deckel-Bar-Begleichen mit vollem Trinkgeld-Screen (wie Gast-Barzahlung) 2026-08-23 17:36:07 +02:00

BarSystem v2 — Mitgliederkonto POS

Point-of-Sale-System für Vereinsbars mit Mitgliederkonten, Guthabenverwaltung und Kartenzahlung via SumUp (Hosted Checkout + Solo-Terminal). Deutsche UI, kein Build-Schritt, ein Prozess, eine SQLite-Datei.

Auf einen Blick:

  • 🍺 Mitglieder-Guthaben am POS abbuchen, aufladen (Bar/Karte/IBAN), Limits je Rolle
  • 💳 Gastkauf: Bar, SumUp-Karte oder Gutschein — auch gemischt (Gutschein + Rest)
  • 🎫 Digitale Gutscheine mit Barcode-Scan am POS und Saldo-Check im Portal
  • 📊 Berichte: Umsatz-Charts, Z-Bericht, Trinkgeld/Fuxenkassa, Marge, Inventur
  • 🔒 Verschlüsselte Backups — lokal + optional VPS (SCP) und/oder S3
  • 🚀 Start: ./start.sh oder docker compose -f compose.yaml -f compose.build.yaml up -d --build → Setup-Wizard

Neuerungen je Datum: CHANGELOG.md


Inhaltsverzeichnis

  1. Features
  2. Architektur & Projektstruktur
  3. Technologie-Stack
  4. Schnellstart
  5. Konfiguration
  6. Rollen & Berechtigungen
  7. Feature-Flags
  8. SumUp Kartenzahlung
  9. Offline-Modus (PWA)
  10. Sicherheitsmodell
  11. Design-Notizen
  12. Betrieb
  13. Umgebungsvariablen
  14. Docker
  15. API
  16. Entwicklung
  17. Bekannte offene Punkte
  18. Changelog

Features

Kasse (POS)

  • Produktraster nach Kategorien, Drag-and-Drop-Sortierung, Keg-/Fass-Anzeige (Fass-Zustand direkt am Produkt), Sucher + Mitgliedersuche parallel.
  • Warenkorb mit Mengen-Eingabe, Artikel entfernen, Wechselgeld-Rechner (abschaltbar), Gesamtsumme.
  • Tastaturkürzel: 1–9 = Produkt der aktiven Kategorie, Enter = Kaufen, Esc = Warenkorb leeren, Backspace = letztes Produkt entfernen.
  • Gastkauf getrennt vom Mitgliederkauf — parallele Bedienung ohne Mitgliedsauswahl möglich.
  • Preisprofile: Standard / Happy Hour / Party — ein Klick schaltet alle Preise um; pro Produkt sind Overrides (Multiplikator oder Fixpreis) möglich, inkl. „Alle"-Override pro Profil.
  • Gutschein-Zahlung: Wert-Gutschein als Zahlart — deckt der Gutschein den Warenkorb nicht vollständig, wird der Rest bar oder per SumUp-Karte in einer Buchung bezahlt (Split-Zahlung). Saldo prüfen zeigt Restwert/Status auch ohne Warenkorb; Barcode-Scanner (USB/Laser) lesen den Code aus dem Gutschein-PDF oder vom Bildschirm (Code-128).
  • Mitglieder-Topup direkt am POS (Bar / Karte / IBAN-Überweisung).

Mitgliederkonten

  • Guthabenverwaltung mit Kaufabzug und Untergrenzen (Floor-Kette, D50/v104/v105):
    • ALTER_HERR (Alter Herr): unbegrenzt (kein Limit)
    • AKTIVER (Aktiver): bis −50 € (vorab erlaubt, wird beim Aufladen ausgeglichen)
    • FREIBIER (Funktionskonto): 0 € — ein Budget, kein Kredit
    • Über der Rollentabelle stehen: Konto-Untergrenze (members.balance_floor) > Gruppen-Kreditlimit (member_groups.credit_floor) > globales function_account_floor (nur FREIBIER) > Rolle. Maßgeblich ist immer effectiveBalanceFloor(member) (src/db/pricing.js).
  • Die Untergrenze wird auf jedem saldosenkenden Pfad erzwungen (Kauf, Retro, Vorbestellung, Gast-Deckel-Umwandlung, Topup-Storno, Überweisung, Freibier-Modus) — serverseitig, nicht nur in der UI. Ausnahme: Kneipen-Strafen (erzwungene Abbuchung, D63).
  • Low-Balance-Alert: Mitglieder unterhalb des Limits werden gekennzeichnet; E-Mail-Benachrichtigung (einmalig pro Aufladung, Claim-vor-Send gegen Duplikate).
  • NFC-Karten: Karte an den Leser halten → Mitglied wird ausgewählt (Karten-UID wird gehasht gespeichert, Kartenzuweisung ist Eindeutigkeits- geschützt: 409 bei bereits vergebener Karte).
  • P2P-Überweisung + Geldanfragen (Flags p2pTransfer/paymentRequests): Mitglieder überweisen Guthaben an andere Mitglieder oder bitten sie um einen Betrag. Die Anfrage selbst bewegt kein Geld — bezahlt wird über denselben Transfer-Pfad (identische Limits, identischer Ledger). Spam-Schutz serverseitig: 20 Anfragen/Tag, 3/Tag an dieselbe Person, max. 5 offene bzw. 100 € offen, 12 h Ruhe nach einer Ablehnung, Verfall nach 7 Tagen.

Aufladung

Weg Wo Mechanik
Bar POS Direkte Gutschrift; 5s-Idempotenzfenster (gleicher Betrag pro Mitglied → 409) gegen Doppel-Tippen
Karte (SumUp) POS + Portal Hosted-Checkout-URL (Browser/Terminal) oder Cloud-API-Push ans Solo-Terminal; Verifikation serverseitig mit Betrag + Referenz
IBAN POS + Portal EPC-QR-Code (Konto/BIC/Name aus Env), Pending-Eintrag mit atomarem Claim; Admin bestätigt nach Geldeingang
  • Jede Kartenaufladung wird über pending_topups geführt und erst nach serverseitiger SumUp-Verifikation gutgeschrieben (Betrag + checkout_ref müssen stimmen). Replay-Schutz: UNIQUE-Index auf sumup_checkout_id → eine bezahlte checkout_id kann nie doppelt gebucht werden.

Storno / Undo

  • Voll-Storno: Original-Transaktion → REFUNDED, UNDO-Gegenbuchung wird erzeugt, Saldo korrigiert.
  • Teil-Storno (einzelne Position): Originalbetrag und ursprünglicher Saldo bleiben unverändert. Die Position erhält einen refunded_at-Marker und eine datierte UNDO-Ledger-Zeile dokumentiert den Teilbetrag.
  • Kartenzahlungen: Void refundet IMMER zuerst bei SumUp (fail-closed — ohne gespeicherte sumup_transaction_id wird abgelehnt), Teil-Stornos nutzen den SumUp-Teilrefund (POST /transactions/{id}/refund mit Betrag).

Sessions & Schichten

  • Sessions (Veranstaltungen): Titel + verantwortliche Mitglieder (Approvers), Start/Ende, Session-Zeitplan.
  • Schichten (Shifts): Öffnen/Schließen mit optionalem Kassenbestand (Feature-Flag requireCashCount, siehe unten), Kassen-Erwartungswert (Anfangsbestand + Verkäufe − Ausgaben), Über/Unter-Zählung, Übergabe-Notiz. Auto-Close (Session-Ende ohne Zählung) persistiert den Erwartungswert und lässt die Zählung für eine spätere Abrechnung offen (Reconciliation).

Transaktionen & Berichte

  • Transaktionsliste: durchsuchbar (Name, Vulgo, E-Mail), filterbar (Typ, Zeitraum), Einzelpositionen aufklappbar, paginiert („Seite X von Y (Z Einträge)"), Export-freundlich.
  • Umsatz-Bericht: Gast/Mitglied-Trennung, Balkendiagramme (responsive, Dark/Light, Hover-Tooltip mit Menge + Umsatz), Mitglieder-Vergleich, Zeitraum-Filter.
  • Z-Bericht (Tagesabschluss): Bar-/Karten-Umsatz, Gastanzahl, Aufladungen, Session-/Shift-Zuordnung — inkl. Drawer-Soll/Ist.
  • Trinkgeld & Fuxenkassa (Feature tipScreen): Rückgeld als Trinkgeld verbuchen — Topf Fuxenkassa (Füxe) oder Bar; Saldo, Auszahlung, eigene Z-Bericht-Zeilen; Auszahlungen belasten die erwartete Kasse (Z).
  • Marge-Report: Umsatz vs. Wareneinsatz (COGS) je Produkt aus dem Inventar-Snapshot (historisch korrekt), Mitglieder/Gäste getrennt, Schwund-Kosten separat.
  • Bon als PDF/QR: digitaler Kassenbon (80 mm) mit QR-Code — Drucken (window.print + Druck-CSS) oder QR-Scan für den Gast (Token-URL, HMAC-geschützt, rate-limited).
  • Inventur-Timeline (wenn Inventar aktiv): Fass-Entnahme-Verlauf als Chart + Ein-/Ausblenden einzelner Produkte.

Verwaltung (Admin-UI)

  • Mitglieder: CRUD, CSV-Import, CSV-Export (RFC 4180, Excel-tauglich, inkl. DSGVO-Einzelauskunft und Filter „nur negative Salden"), kombinierbare Stammdatenfilter für Rolle, Saldo, Karte, E-Mail und eigenen Krug sowie Sortierung „Schulden zuerst", Soft-Delete (nur bei Saldo 0), E-Mail-Suche nur mit entsprechender Permission (kein E-Mail-Orakel fürs Personal), NFC-Karten zuweisen/entfernen, PIN-Reset (mit E-Mail-Versand).
  • Kategorien / Produkte / Preise: Inline-Bearbeitung, Drag-and-Drop-Reihenfolge.
  • Preisprofile: Multiplikator-/Fixpreis-Overrides je Produkt, aktives Profil — plus Zeitfenster-Autoaktivierung („Happy Hour": von/bis, Wochentage; kleinste id gewinnt bei Konflikt; manuelle Wechsel werden beim nächsten Takt überschrieben).
  • Inventar: Bestand pro Produkt, Fass-/Einheiten-Tracking (/inventory/:id/tap öffnet neue Einheit), automatische Bestandsreduktion beim Kauf, Keg-Wechsel- Logik (leere Einheit → nächste öffnen), Warnschwellen + E-Mail-Alert (gedrosselt: nur bei Erfolg verbraucht).
  • Mitgliedsbeiträge (Fees): Status Offen/Bezahlt, Mahnung, Liste mit letzter Mahnungs-E-Mail.
  • Retro-Buchung: Nachträgliche Käufe/Topups für eine vergangene Session (benötigt Feature retroBooking).
  • Backups: verschlüsselt (AES-256-GCM, BACKUP_ENCRYPTION_KEY), manuell + automatisch (Serverstart, nach jedem Kauf, bei Session-Ende sowie 5-min-Snapshots), Download (verschlüsselte PC-Kopie), Upload, Transfer auf VPS via SFTP/paramiko (strikte known_hosts-Prüfung) und/oder S3-Objekt-Storage (AWS/MinIO/Wasabi/B2) — lokal bleibt immer erhalten; die Backup-Liste zeigt, wo jede Kopie liegt.
  • E-Mail-Texte: alle automatischen Mails als editierbare Templates mit Platzhalter-Übersicht; Namensformat für {name} wählbar (vollständig / Vulgo / Vorname / Nachname) — gilt auch für die Überweisungs-Referenz.
  • Portal: Erstzugang-Flow — Mitglieder ohne PIN erhalten eine Willkommens-Mail („Erstzugang: Noch keinen PIN?"), alle anderen den Reset-Flow; Antwort bleibt generisch (kein E-Mail-Enumeration-Orakel). Eigene E-Mail-Adresse änderbar (PIN + Bestätigungs-Code, Info-Mail an die alte Adresse).
  • Kontoimporte (Admin): Kontoauszüge als camt.053/MT940/CSV hochladen, automatische Matching-Vorschläge (QR-Referenz/Namensnormalisierung — nie automatisch buchen), Gutschriften als TOPUP_IBAN buchen oder ablehnen; Re-Importe werden erkannt (idempotent).
  • Gutscheine: digitale, übertragbare Gutscheine (Wert- oder Produkt-Typ, Ablaufdatum) — Code = Geheimnis, nur Hash gespeichert; Erstellen am POS (Code einmalig + PDF/QR/Barcode), Saldo-/Status-Check überall; Einlösung am POS (Produkt direkt oder als Zahlart, inkl. Split-Zahlung), Admin-Tab mit Liste + Ungültig-machen (Admin-Ausweg, nicht erstattbar); Z-Bericht mit eigener Zeile.
  • Audit-Log: wer hat wann was getan (90-Tage-Rotation), filterbar, „Alle anzeigen"-Reset.
  • Einstellungen: Feature-Flags, Primärfarbe, Tastenkürzel-Zuordnung, Daten-Reset (transactions / audit-log / members / events / alles).

Mitgliederportal (public/portal.html)

  • Login mit PIN, Guthabenanzeige, Aufladen per Karte (SumUp Hosted) und IBAN (QR), Transaktionsverlauf, Pending-Topups-Status, Gutschein-Saldo per Code abfragen, Passwort/PIN-Reset (E-Mail-Code, brute-force-geschützt, 5 Versuche).
  • Überweisen (Flag p2pTransfer) und Geldanfragen (Flag paymentRequests): Anfragen stellen, Posteingang mit Bezahlen/Ablehnen, Ausgang mit Zurückziehen, Badge am Tab, Push-Benachrichtigung (sofern VAPID konfiguriert). Details zur Richtung und zum Spam-Schutz: docs/DECISIONS.md (D47).

Bedienkomfort

  • Komplett deutsche UI, Dark- + Light-Mode, primäre Akzentfarbe einstellbar.
  • Digitale Kassenbons (Feature-Flag) nach dem Kauf.
  • Kein Frontend-Build, kein Framework — Vanilla-JS-SPA, sofort ladbar.
  • Barcode-Scanner (optional, beliebiger USB-HID-Scanner): Der Scanner tippt die EAN wie eine Tastatur — einmalig im Admin (Verwaltung → Produkte → Produkt bearbeiten → Feld „Barcode/EAN") auf die Flasche scannen, ab dann landet das Produkt beim Scan direkt im POS-Warenkorb (preisprofil-gerecht). Kein Flag, keine externe Datenbank: Die products-Tabelle IST die Zuordnung. Doppelte Zuweisung wird vom Server abgelehnt (409). Funktioniert neben NFC-Karten- und Gutschein-Scans (reine Ziffern-Bursts werden als EAN erkannt).

Architektur & Projektstruktur

barsystem-v2/
├── server.js                 # Express-Launcher: Helmet/CSP, Rate-Limits, Routen, Statics
├── src/
│   ├── config.js             # Umgebungsvariablen (SumUp, SMTP, Limits, …)
│   ├── db.js                 # Re-Exporte: db, getActiveProfile, getDisplayPrice,
│   │                         #   BALANCE_LIMITS, getOr404, getActiveEvent
│   ├── middleware.js         # Auth, Permissions, Rate-Limiter (Staff vs Portal getrennt),
│   │                         #   requireFeature, requireSumupEnabled, catchAsync
│   ├── permissions.js        # Einzige Quelle: ALL_PERMISSIONS (10), Rollen-Defaults
│   ├── money.js              # parseMoney / cents (JEDER Betrag geht hier durch)
│   ├── utils.js              # assertString, assertPositiveAmount, escapeLike,
│   │                         #   parseCSV, generateEpcQr, genToken
│   ├── dateUtils.js          # lokale Tagesgrenzen (BUSINESS_TIMEZONE), UTC-Format
│   ├── utils_crypto.js       # PBKDF2-Hashing, AES-256-GCM-Verschlüsselung
│   ├── db/
│   │   ├── schema.js         # Kanonisches FINAL-Schema
│   │   ├── migrations.js     # Runner: lädt Module aus db/migrations/, v45+
│   │   ├── seed.js           # Erst-Bootstrap (Admin/Manager/Staff, Basisprodukte)
│   │   ├── pricing.js        # BALANCE_LIMITS je Rolle
│   │   └── connection.js
│   ├── routes/               # HTTP-Schicht: Validierung, Permissions, JSON
│   │   ├── auth.js, users.js, members.js (+ members/crud.js), catalog/ (products,
│   │   │   categories, inventory, bundles, validation), checkout.js  # /checkout,
│   │   │   #   /checkout/guest(+/init), /checkout/undo-item
│   │   ├── topups.js         # cash/card(+mode=terminal)/iban, pending-bestätigen
│   │   ├── terminal.js       # SumUp-Cloud-API: list/pay/status/cancel/pair + Webhook
│   │   ├── portal/           # Portal-Realm: security.js, topups.js, transfers.js,
│   │   │                     #   requests.js (Geldanfragen v100) + recipients.js
│   │   │                     #   (geteilte Empfänger-Auflösung), preorders.js,
│   │   │                     #   schedule.js, vouchers.js,
│   │   │                     #   emailChange.js, orders.js …
│   │   │                     #   (penalties.js liegt unter members/ — s. u.)
│   │   ├── vouchers.js       # Gutschein-CRUD + PDF/QR/Barcode (WP8)
│   │   ├── bankImport.js     # camt.053/MT940/CSV-Import (Review-Queue)
│   │   ├── stream.js         # SSE-Live-Events (WP-O3)
│   │   ├── guestTabs.js      # Gast-Deckel/Kredit-Tabs (v79)
│   │   ├── posOrders.js      # Portal-Bestellungen POS-Management (v92)
│   │   ├── apiKeys.js        # X-API-Key-Auth (NF-5)
│   │   ├── suppliers.js, purchaseOrders.js  # Lieferanten + Bestellungen (NF-7)
│   │   ├── events.js, shifts.js, reports.js, zReport.js, features.js,
│   │   ├── settings.js, backups.js, auditLog.js, reset.js, fees.js,
│   │   ├── profiles.js, health.js, maintenance.js, push.js, exports.js
│   ├── services/             # Geschäftslogik + ALLE DB-Schreibvorgänge in transactions
│   │   ├── checkoutService.js# Rest-Datei: reconcilePendingRefundOperations (kein Barrel mehr, B3)
│   │   ├── purchaseService.js, undoService.js, retroService.js, checkoutHelpers.js
│   │   ├── memberService.js (+ members/: topups, transfers, moneyRequests, penalties),
│   │   ├── sumupService.js (+ sumup/), mailService.js,
│   │   ├── stockAlertService.js, featureFlags.js, shiftService.js, auditLog.js, logger.js,
│   │   ├── guestTabService.js, bundleService.js, orderService.js, preorderService.js,
│   │   ├── feeService.js (+ fees/), reportService.js, eventService.js,
│   │   ├── backupWatchdog.js, profileScheduler.js, csvExport.js, pdfReceipt.js,
│   │   ├── purchaseOrderService.js, supplierService.js, pushService.js,
│   │   ├── voucherService.js, crypto.js, liveEvents.js
│   └── db.js
├── public/
│   ├── index.html            # Kassen- + Admin-SPA (lädt alle Module)
│   ├── portal.html           # Mitgliederportal
│   ├── manifest.json, sw.js   # POS-PWA
│   ├── portal-manifest.json, portal-sw.js # Mitgliederportal-PWA
│   ├── css/style.css
│   └── js/                   # Vanilla-Module (prettier-formatiert)
│       ├── escape.js         # esc()/dataArgs() — XSS-kritisch, lädt zuerst
│       ├── i18n.js           # Deutscher UI-Copy-Katalog T + t() (dependency-free)
│       ├── error-catcher.js  # globaler JS-Fehler-Toast (M4: externalisiert)
│       ├── api.js            # API-Client (Cookie/CSRF; Bearer-Fallback)
│       ├── offline-db.js, offline-queue.js  # Offline-Modus: IndexedDB + Sync (WP-O1)
│       ├── live.js           # SSE-Client (WP-O3)
│       ├── app.js            # geteilte Helfer (localSqliteDate, isPOSRoute,
│       │                     #   balanceClass/roleClass …)
│       ├── app-actions.js    # Action-Dispatcher (invokeAction + Delegation, M5-Split)
│       ├── app-shell.js, app-auth.js, app-router.js  # Shell, Login, Routing (M5-Splits)
│       ├── toast.js          # Toast-API (lädt früh)
│       ├── pos.js            # Kasse: State, Rendering, Mitgliederauswahl
│       ├── pos-catalog.js    # Kategorie-/Produkt-Grid + Profilwahl (aus pos.js)
│       ├── pos-freedrink.js  # Freibier-Modus (freeDrinkState)
│       ├── pos-guest-tabs.js, pos-guest-tabs-settle.js  # Gast-Deckel + Begleichen
│       ├── pos-orders.js     # Portal-Bestellungen im POS (v92)
│       ├── scanner-camera.js # Kamera-Barcode-Scan (NF-4)
│       ├── push-client.js    # Web-Push-Subscription (NF-2, POS + Portal)
│       ├── pos-session.js    # Sessions/Shifts/Kassenbestand/Verantwortliche
│       ├── pos-cart.js       # Warenkorb (addToCart/changeQty/drawCart …)
│       ├── pos-payment-core.js  # Payment-State, Contexts, Cancellation, Terminal-Scheduler
│       ├── pos-payment-card.js  # Terminal-Checkout auf dem Core
│       ├── pos-tips.js       # Trinkgeld/Auszahlungen (WP1); pos-vouchers.js: Gutscheine (WP8)
│       ├── pos-payment.js    # Bezahlung (Guest Cash/Card, Member-Checkout)
│       ├── pos-topup.js      # Aufladen (Cash/Card/IBAN, Topup-Terminal-Polling)
│       ├── pos-barcode.js    # Barcode/EAN-Eingabe (WP-O4)
│       ├── pos-operations.js # Inventar-Tap, Undo-Polling, NFC, Tastatur
│       ├── admin*.js         # Admin-Tabs (je Domain eine Datei: members/categories/
│       │                     #   products/profiles/users/events/transactions/…)
│       ├── reports-chart.js, reports-sales.js, reports-z.js, reports-inventory.js,
│       │                     #   reports-margin.js, reports-tips.js, reports-hook.js
│       ├── admin-settings-sections.js  # Einstellungen: SumUp/Server/VPS/Audit
│       ├── admin-maintenance.js  # Wartung: Migrationszustand + Neustart
│       ├── setup.js          # First-Setup-Wizard (setup.html)
│       ├── portal-core.js, portal-auth.js, portal-tabs.js, portal-pwa.js
│       │                     # Mitgliederportal (M5-Split, portal.html)
│       └── date.js, escape.js s.o.
├── tests/                    # Jest: Unit- + Integrationstests (2888)
├── Dockerfile, compose.yaml (Produktion: Registry-Pull), compose.build.yaml
├── AGENTS.md                 # Agent-Guide (Architektur, Invarianten, Pitfalls)
├── API.md                    # API-Referenz
└── KNOWN_ISSUES.md           # Einzige lebende Befundliste

Schichtung (wichtig für Änderungen): Routen validieren und antworten, Services tragen die Geschäftslogik und schreiben die DB ausschließlich in db.transaction(). Routen importieren die Services direkt aus ihrem Heimatmodul (kein checkoutService-Barrel mehr, B3).


Technologie-Stack

  • Backend: Node.js 22.14+ oder 24+ (better-sqlite3 v13 segfaultet auf 22.13), Express, better-sqlite3 v13 (synchron — keine Await-Races innerhalb von Transaktionen)
  • Frontend: Vanilla JS SPA, kein React/Vue, kein TypeScript, kein Build
  • Auth: httpOnly-Cookie-Sessions (session / portal_session, SameSite=Strict, Secure hinter HTTPS) + realm-getrennte Double-Submit-CSRF-Cookies (csrf / portal_csrf); PBKDF2-SHA-512 (600k Iterationen). Bearer-Header nur noch als Übergangs-Fallback (Token-Fixtures, Alt-Clients)
  • Tests: Jest (in-memory SQLite, Savepoint-Isolation pro Test, KEIN resetModules() pro Test)
  • Deployment: ein Prozess, eine SQLite-Datei, optional Docker

Schnellstart

Voraussetzungen

  • Node.js 22.14+ oder 24+ (better-sqlite3 v13 liefert NAPI-Prebuilds im npm-Tarball; 22.13 crasht beim require)

Docker (empfohlen)

cp .env.example .env
# Im Container optional: fehlende Secrets (Backup-Key, Setup-Token, VAPID)
# erzeugt der Start selbst und schreibt sie in die Logs (data/secrets.env).
# Für headless-Deployments ohne Wizard stattdessen:
#   INITIAL_ADMIN_PASSWORD=<starkes Passwort>
#   BACKUP_ENCRYPTION_KEY=<32-Byte-Schlüssel>
# compose.yaml ist pull-only (Produktion zieht das CI-Image aus der Registry);
# für den lokalen Build den Override mitgeben:
docker compose -f compose.yaml -f compose.build.yaml up -d --build

Im Container erzeugt der Start fehlende Secrets (BACKUP_ENCRYPTION_KEY, SETUP_TOKEN, VAPID-Paar) selbst, legt sie unter data/secrets.env (0600) ab und schreibt sie in die Logs — docker compose logs app | grep SETUP_TOKEN liefert das Token für /setup.html. Diese Datei nie löschen: ohne BACKUP_ENCRYPTION_KEY sind alte Backups unlesbar.

Der Datenordner ./data/ wird als Bind-Mount gemountet (./data:/app/data). Compose bindet standardmäßig nur an 127.0.0.1; BIND_ADDRESS=0.0.0.0 nur hinter einem TLS-Reverse-Proxy verwenden.

Linux (manuell)

cd barsystem-v2
npm install
REQUIRE_HTTPS=false PORT=3000 node server.js   # erster Start: /setup.html im Browser

Einfacher: ./start.sh (installiert Dependencies + Python-venv mit paramiko/boto3 bei Bedarf, prüft ausstehende Migrationen, startet mit REQUIRE_HTTPS=false für lokale Entwicklung). Windows: start.bat. Beim ersten Start (leere DB) erscheint automatisch /setup.html — kein INITIAL_ADMIN_PASSWORD nötig.

REQUIRE_HTTPS=false ist ausschließlich für lokale Entwicklung gedacht (Default: true → HTTP-Anfragen werden mit 426 abgelehnt).

Erster Start — Setup-Wizard

  1. Die DB wird automatisch angelegt (Migrationen v1–v97).
  2. Ohne INITIAL_*_PASSWORD-env bleibt die Benutzer-Tabelle leer — beim Öffnen der App im Browser erscheint automatisch /setup.html (der First-Setup-Wizard): dort Admin-Benutzername + Passwort wählen, optional SMTP/Bank/Zeitzone eintragen. Der Wizard legt den Admin-User an und setzt seed_done.
  3. Mit dem neuen Admin einloggen, Feature-Flags prüfen (Admin → Einstellungen), SumUp-Credentials setzen (siehe SumUp).

INITIAL_ADMIN_PASSWORD in .env ist seit dem Setup-Wizard optional — nur noch für headless/Docker-Bootstrap nötig (dann wird der Login wie gehabt mit erzwungenem Passwortwechsel gebootstrapt).


Konfiguration

Es gibt drei Konfigurations-Ebenen — die meisten Werte liegen in .env, einige werden in der Admin-UI gesetzt, SumUp-Credentials entstehen im SumUp-Merchant-Portal:

Was Wo Wann nötig
Backup-Key (env), optionale Bootstrap-Passwörter, SumUp, SMTP, IBAN, Ports .env (Kopie von .env.example) im Container optional — Backup-Key/Setup-Token erzeugt der Start selbst (data/secrets.env + Log); Admin-Passwort kommt aus dem Wizard
Feature-Flags, Primärfarbe, Tastenkürzel, Bankdaten, Mitgliedsbeitrag, Reader-Pairing, SumUp/VPS/Server-Settings Admin-UI → Einstellungen nach dem ersten Login
SumUp API-Key / Merchant Code / Affiliate Key me.sumup.com → Developer Settings / Toolkit nur bei Kartenzahlung
Solo-Reader-Pairing-Code am SumUp-Solo-Gerät (Connections → API) nur bei Terminalzahlung

Schritt 1 — .env anlegen

cp .env.example .env

Zwingend in .env (ohne diesen Wert verweigert der Server Backups):

Variable Wert Erzeugen
BACKUP_ENCRYPTION_KEY 32 Byte openssl rand -hex 32

Im Container nicht mehr nötig: Docker-Start erzeugt fehlende Secrets (BACKUP_ENCRYPTION_KEY, SETUP_TOKEN, VAPID-Paar) selbst, legt sie unter data/secrets.env (0600) ab und schreibt sie in die Logs (docker compose logs app) — siehe docs/DEPLOYMENT.md → Weg 3. AUTO_SECRETS=0 schaltet das ab.

Alles andere geht über den Setup-Wizard bzw. die Admin-UI — das Admin-Passwort wird beim ersten Start im Browser gewählt (/setup.html), SumUp/SMTP/Bank/Zeitzone/VPS lassen sich danach in den Einstellungen setzen (DB-Werte haben Vorrang vor env). INITIAL_ADMIN_PASSWORD/INITIAL_MANAGER_*/ INITIAL_STAFF_* sind nur noch für headless/Docker-Bootstrap relevant.

Erster Start mit minimalem .env (Wizard übernimmt den Rest):

PORT=3000
DB_PATH=data/bar.db
REQUIRE_HTTPS=false            # nur lokal; produktiv true (Default)
BACKUP_ENCRYPTION_KEY=$(openssl rand -hex 32)
# KEIN INITIAL_ADMIN_PASSWORD nötig — /setup.html erscheint automatisch

Schritt 2 — Setup-Wizard (erster Start)

  1. Server starten (Docker oder node server.js).
  2. App im Browser öffnen → /setup.html erscheint automatisch (leere DB): Admin-Benutzername + Passwort wählen, optional SMTP/Bank/Zeitzone.
  3. Danach einloggen; optional INITIAL_MANAGER_PASSWORD / INITIAL_STAFF_PASSWORD in .env setzen (oder Benutzer in der UI anlegen).

Schritt 3 — Admin-UI (Einstellungen)

Nach dem Login in der Admin-Ansicht:

  • Funktionen (Feature-Flags): SumUp, Gastkauf, Karten-Aufladung, QR-Codes, NFC-Karten, digitaler Bon, Tastaturkürzel, Wechselgeld-Rechner, Kassenbestand- Pflicht (requireCashCount), Inventar-Tracking, Retro-Buchung, Z-Bericht — hier an/aus schalten (siehe Tabelle im Abschnitt Feature-Flags).
  • Einstellungen: SMTP-Zugang + „Testmail senden", Admin-E-Mail, Bankdaten (IBAN/BIC/Kontoinhaber — alternativ zu den .env-Werten), Mitgliedsbeitrag-Betrag und -Periode.
  • Tastenkürzel: freie Zuordnung von Tasten auf Produkte.
  • Darstellung: Primärfarbe, Dark/Light-Mode.
  • Daten: Reset-Bereiche (transactions / audit-log / members / events / alles).

Schritt 4 — SumUp (nur bei Kartenzahlung)

  1. API-Key: me.sumup.com → Settings → Developer Settings (für Tests: eigene Sandbox anlegen) → API-Key → .env: SUMUP_API_KEY.
  2. Merchant Code → .env: SUMUP_MERCHANT_CODE.
  3. Affiliate Key: Toolkit → Affiliate Keys, Application identifier z. B. at.bar.vereinsbar → .env: SUMUP_AFFILIATE_KEY + SUMUP_APP_ID.
  4. PUBLIC_URL setzen (Redirect-Ziel nach Hosted-Zahlung).
  5. Solo-Terminal (optional): Pairing-Code am Gerät → POST /api/terminal/pair (Benutzer mit settings-Permission). Details + Virtual-Solo-Test: SumUp Kartenzahlung.

Typische Szenarien

Vereinsbetrieb (Vertrauensbasis, keine Karten):

BACKUP_ENCRYPTION_KEY=$(openssl rand -hex 32)
# Admin-Passwort kommt aus dem Setup-Wizard (erster Start)
# SumUp-Zeilen auskommentiert lassen

→ Beim ersten Start /setup.html durchspielen; in der UI: requireCashCount AUS (kein Kassenbestand nötig), Rest default.

Party (Kasse + Karten):

BACKUP_ENCRYPTION_KEY=$(openssl rand -hex 32)
SUMUP_API_KEY=sk_live_…
SUMUP_MERCHANT_CODE=…
SUMUP_AFFILIATE_KEY=ak_…
SUMUP_APP_ID=at.bar.vereinsbar
PUBLIC_URL=https://barsystem.example.com

→ Beim ersten Start /setup.html; in der UI: requireCashCount AN (Anfangs-/Endbestand Pflicht, Backend lehnt 0 ab), sumupEnabled AN, ggf. Happy-Hour-/Party-Preisprofil anlegen und aktivieren, Personal-Account anlegen (Rolle STAFF = „Personal").

E-Mail-Benachrichtigungen (Low-Balance-Alerts, PIN-Reset, Fee-Mahnungen):

SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=user@gmail.com
SMTP_PASS=<App-Passwort>
SMTP_FROM=user@gmail.com
ADMIN_EMAIL=admin@example.com

Backup auf VPS (automatisch bei Start, nach Käufen und Sessions — lokal max. 1/5 min, Upload zu VPS/S3 1× täglich via UPLOAD_INTERVAL_HOURS):

VPS_HOST=your-vps.example.com
VPS_USER=root
VPS_PASS=…
REMOTE_DIR=/root/barsystem-backups/
VPS_KNOWN_HOSTS=/pfad/known_hosts   # strikte Prüfung immer aktiv

Ohne VPS_HOST bleiben Backups lokal unter data/backups/.

Alle Variablen im Detail: Umgebungsvariablen.


Rollen & Berechtigungen

Rolle Beschreibung
ADMIN Alles (inkl. Rollenvergabe, Einstellungen, Backups)
MANAGER Betrieb + Berichte + Mitglieder-/Produktverwaltung
STAFF Nur Kasse (POS) — kein Admin, keine Berichte

Es gibt 10 Berechtigungen (src/permissions.js ist die einzige Quelle): pos, members, products, inventory, reports, users, settings, manageFeatures, backups, features-Zugriff u. ä. — nur ADMIN darf die ADMIN-Rolle oder elevate Permissions vergeben (serverseitig erzwungen). UI-Anzeige: Rolle STAFF erscheint als „Personal", interne Werte bleiben englisch.

➡️ Anleitungen zum Ausdrucken (deutsch, mit Screenshots und roten Markierungen):

Anleitung Vollversion A4-Aushang (1 Seite, zum Aufhängen)
Bardienst / Kasse Bardienst_Schnellstart.pdf Bardienst_Aushang_A4.pdf
Mitgliederportal Mitgliederportal_Anleitung.pdf Mitgliederportal_Aushang_A4.pdf
Verwaltung (Admin) Verwaltung_Anleitung.pdf Verwaltung_Aushang_A4.pdf

Dazu die Textfassung für Bardienst: docs/BARDIENST_QUICKSTART.md. Neu bauen (nach UI-Änderungen): siehe scripts/docs/README.md.


Feature-Flags

Verwaltung: Admin → Einstellungen (Tab „Funktionen"). Aktuelle Flags (Auszug — Quelle: FEATURE_FLAG_DEFINITIONS in src/constants.js, geseedet in src/db/migrations.js; die UI-Keys dazu führt public/js/admin-features.js):

Flag Effekt
sumupEnabled SumUp-Kartenzahlung aktiv (Routen sonst 503)
guestCheckout Gastkauf erlaubt
cardTopup Karten-Aufladung am POS durch Personal
portalCardTopup Karten-Aufladung im Portal (Mitglieder, ohne Terminal)
qrCodes IBAN-QR-Codes
nfcCards NFC-Kartenleser
digitalReceipt Digitaler Bon nach Kauf
keyboardShortcuts Tastaturkürzel im POS
disableChangeScreen Wechselgeld-Rechner ausblenden
requireCashCount Anfangs-/Endbestand bei Session/Shift PFLICHT (Backend lehnt 0 ab) — für Vereine auf Vertrauensbasis aus, für Partys an
inventoryTracking Bestand/Fass-Tracking
retroBooking Retro-Buchung
zReport Z-Bericht
offlineMode Offline-Modus (PWA & Offline-Warteschlange) — siehe Offline-Modus (PWA)
p2pTransfer P2P-Guthabenübertrag im Portal (Mitglied zu Mitglied)
paymentRequests Geldanfragen im Portal (bitten statt überweisen; eigener Kill-Switch gegen Spam, v101)
memberFees Mitgliedsbeiträge (Tab Verwaltung → Beiträge + alle /api/fees-Routen; aus = 503, v102)
memberFeeAutoSend Beitrags-Automatik: Versand am Stichtag + Admin-Erinnerung 14 Tage vorher (aus = nur manueller Versand, v102)
memberFeeEmails Beitrags-Mails überhaupt (Fälligkeits-Mail + Mahnung)
cashCarryOver Kassenstand-Modus: Anfangsbestand wird automatisch vom letzten gezählten Endbestand übernommen (v103; Feld „Anfangsbestand" entfällt)
guestPricing Zweiter Preis (Gastpreis) je Produkt für Gast-Verkäufe (v97)

SumUp Kartenzahlung

Zwei Zahlungswege, beide über die SumUp-API (api.sumup.com/v0.1):

  1. Hosted Checkout (Portal + POS-Fallback): Der Server erzeugt einen Checkout mit hosted_checkout.enabled, die Zahlungsseite öffnet sich im Browser (hosted_checkout_url). Keine Hardware nötig.
  2. Cloud API / Solo-Terminal (POS, physisches Terminal): Die Zahlung wird an einen verbundenen SumUp-Solo-Reader gepusht, der POS pollt den Status (alle 2 s) und bucht nach Zahlung über den verifizierten Pfad. Ohne Reader: automatischer Hosted-Fallback.

Purpose-Binding (Sicherheit)

Jeder server-seitig erzeugte Checkout trägt eine Referenz mit Zweck-Präfix: guest- (Gastkauf), topup- (POS-Aufladung), portal-topup- (Portal). Der Gastkauf-Pfad lehnt Topup-Referenzen ab (409) und umgekehrt — eine bezahlte Aufladung kann nie als Gastkauf verbucht werden. Zusätzlich: UNIQUE-Index auf sumup_checkout_id → Replay-Schutz (409 „bereits gebucht").

Einmalige Einrichtung (Solo-Terminal)

  1. Credentials: me.sumup.com → Settings → Developer Settings (Sandbox: eigene Sandbox anlegen) → API-Key (sk_live_…) → .env: SUMUP_API_KEY.
  2. Merchant Code → .env: SUMUP_MERCHANT_CODE.
  3. Affiliate Key: Toolkit → Affiliate Keys, Application identifier z. B. at.bar.vereinsbar → .env: SUMUP_AFFILIATE_KEY + SUMUP_APP_ID.
  4. Solo einschalten → Connections → API → Connect → Pairing-Code.
  5. Pairing einmalig: POST /api/terminal/pair mit { "pairing_code": "…" } (Benutzer mit settings-Permission). Der Reader wird ab dann automatisch über GET /api/terminal/list gefunden (erster Reader wird genutzt).

⚠️ Solo Lite unterstützt die Cloud API NICHT (nur Bluetooth-SDKs). Für den Terminalweg ist das volle Solo nötig (~150 €). Vor dem Kauf mit Virtual Solo (virtual-solo.sumup.com) + Sandbox testen.

Zahlungsabläufe

  • POS Gast-Kauf: Karte-Button → Reader vorhanden? → Terminal-Push + Polling → PAID → Buchung (/checkout/guest). Kein Reader → Hosted-Fallback (window.open + „Zahlung abschließen").
  • POS Mitglieder-Aufladung: Karte-Button → Terminal-Push (mode=terminal) → Polling → Gutschrift.
  • Portal (remote): Hosted Checkout per URL/QR → „Zahlung prüfen".
  • Webhook /webhooks/sumup: verifiziert IMMER per API (nie dem Payload vertrauen) und bucht Topups automatisch gut. Gast-Checkouts bucht er nicht (kein Warenkorb-Kontext) — das macht der POS.
  • Storno: Void refundet zuerst bei SumUp (Voll- oder Teilrefund), dann lokal (fail-closed).
  • Reconcile: Bezahlte Checkouts sind per Referenz-Präfix über die SumUp-API abfragbar.

Offline-Modus (PWA)

Der POS ist als PWA installierbar und kann ohne Internetverkauf weiterarbeiten (Feature-Flag offlineMode, Default aus). Verkäufe werden lokal in einer IndexedDB-Queue gepuffert und nach dem Wiederverbinden automatisch synchronisiert.

Auch das Mitgliederportal ist als eigene, separate PWA installierbar. Es nutzt portal-manifest.json, startet bei /portal.html und hat einen eigenen Service Worker mit getrenntem Cache. Der Portal-Service-Worker hält nur die Portal-Shell bereit; Login, Guthaben, Aufladungen und alle übrigen Mitgliederdaten bleiben aus Sicherheitsgründen online-only und werden nie im Cache gespeichert.

Installation (Android/Chrome)

  • Die PWA benötigt einen sicheren Kontext — die App muss also über HTTPS (oder localhost) ausgeliefert werden, sonst registriert sich der Service Worker nicht. REQUIRE_HTTPS=false reicht dafür nicht.
  • Menü → „Zum Startbildschirm hinzufügen" / „App installieren" — der Service Worker cached die App-Shell, danach startet das Tablet auch im Garten ohne Netz (Start fällt offline auf die gecachte Shell zurück).
  • Portal: /portal.html öffnen → Menü → „Zum Startbildschirm hinzufügen" / „App installieren". Die installierte App öffnet direkt das Mitgliederportal.

Wartung: Cache-Version

  • public/sw.js hat eine VERSION-Konstante (const VERSION in der Datei selbst — die frühere Handzahl hier ist dreimal gedriftet, deshalb steht sie nicht mehr im README). Bei JEDEM Deployment mit JS/CSS-Änderungen hochzählen — das ist der manuelle Cache-Buster, sonst liefert der Service Worker alte Dateien aus. Alternativ auf dem Tablet in den App-Infos den Cache leeren.
  • public/portal-sw.js hat einen eigenen Versionszähler (aktuell bs-portal-v11) und Cache-Namespace. POS- und Portal-PWA löschen beim Aktivieren nicht mehr die jeweils andere Cache-Version.

Offline-Queue

  • Enqueue: Mitgliederkauf oder Gast-Barzahlung offline → Buchung landet in der Queue (clientCapturedAt = Kaufzeitpunkt, offlineBooking: true, request_id idempotent, Offline-Epoche für Debug/Transparenz). Warenkorb wird geleert, der Bon trägt den Hinweis „OFFLINE — noch nicht verbucht".
  • Sync: FIFO nach dem Wiederverbinden (online-Event, App-Start, 60-s-Takt, manuell). 2xx → gebucht, 4xx → Fehlerliste (Banner), Netzwerkfehler/5xx → Abbruch mit Wiederholung (Reihenfolge bleibt erhalten). Jede Sync-Buchung trägt den Header X-Offline-Booking: 1.
  • Anzeige: Banner rechts oben zeigt Queue-Status/Fehler; die Fehlerliste erlaubt „Erneut versuchen" und „Verwerfen".

Limit-Override (Layer-2-Politik)

  • Das Tablet prüft das Rollen-Limit lokal gegen die projizierte Balance (Cache-Stand − offene Queue). Unter dem Limit wird offline verweigert (Gast-Barzahlung bleibt möglich).
  • Weicht der Cache-Stand ab (zweites Terminal hat zwischenzeitlich belastet), kann eine Offline-Buchung beim Sync unter das Limit fallen: Der Server bucht sie trotzdem — das Bier ist verkauft, die Buchung darf nicht verloren gehen — markiert sie aber mit offline_limit_override (sichtbar in Protokoll/Z-Bericht und Audit). Einlösen/Aufladen gleicht das Konto aus.

Sicherheitsmodell

  • Authentifizierung: httpOnly-Cookie-Sessions (session für Staff, portal_session für das Mitgliederportal; SHA-256-gehashte Token in der DB, niemals Klartext). Token-Invalidierung bei Passwortwechsel/Logout. Der Bearer-Header bleibt als Übergangs-Fallback erhalten (Token-Fixtures, Alt-Clients), wird vom Frontend aber nicht mehr gespeichert.
  • CSRF: Double-Submit-Cookies, realm-getrennt (csrf/portal_csrf), erzwungen per csrfGuard auf allen State-Changing-Pfaden, sobald eine Session-Cookie vorhanden ist (Bearer-only-Clients ohne Cookie sind ausgenommen); SameSite=Strict + Secure hinter HTTPS. Staff- und Portal-Cookies werden getrennt gesetzt/gelöscht — Logout in einem Realm lässt den anderen unberührt.
  • Content Security Policy (Helmet): script-src 'self', script-src-attr 'none', object-src 'none', frame-ancestors 'none', base-uri 'self', upgrade-insecure-requests. Seit dem M4-Refactor existieren keinerlei Inline-JS-Handler und keine Inline-<script>-Blöcke mehr — sämtliche Events laufen über data-action-Delegation (eine Funktion in app-actions.js, Argumente als JSON in data-args, kein eval). Tests erzwingen beides.
  • Rate-Limiting: getrennte Maps für Staff-API und Portal (IP + Account), Fenster 15 min; PIN-Reset zusätzlich DB-gestützt (5 Versuche).
  • Geld: jeder Betrag durch parseMoney; Rollen-Limits auf jedem saldosenkenden Pfad; Preisänderungen zwischen Verifikation und Buchung brechen den Kauf (M7).
  • Backups: AES-256-GCM-verschlüsselt; automatische Trigger (Serverstart, Kauf, Session-Ende, 5-min-Snapshots); SFTP-Transfer (paramiko) mit strikter known_hosts-Prüfung.
  • HTTPS: REQUIRE_HTTPS lehnt HTTP ab (426); TRUSTED_PROXY vertraut X-Forwarded-* nur von den erkannten Proxys (Default loopback; auto = Loopback + eigene private Netze ist ein explizites Opt-in und wird von compose.yaml gesetzt — beim Start loggen trust proxy-Zeilen den Satz).

Design-Notizen

  • Zeitstempel werden in UTC gespeichert (datetime('now'), Format YYYY-MM-DD HH:MM:SS). Tagesberichte rechnen in BUSINESS_TIMEZONE (Default Europe/Berlin). Nie ISO-T-Strings in SQL-Vergleichen mit datetime('now') mischen (Leerzeichen < T → Same-Day-Brüche).
  • Partial-Undo: Teil-Stornos schreiben eine eigene UNDO-Ledger-Zeile („Storno … von Transaktion #x (Teil)"). Die Drawer-Berechnung berücksichtigt Original und Gegenbuchung; der ursprüngliche Umsatz bleibt als Bruttowert erhalten.
  • Drawer (Kassen-Soll): expected = opening_cash + Verkäufe − Ausgaben; Auto-Close ohne Zählung lässt closing_cash/cash_over_short NULL bis zur gezählten Abrechnung.
  • Migrationen: v1–v44 gesquasht, v45+ als Module in src/db/migrations/ mit up: (db) => … (Runner übergibt db!). Neue Migrationen IMMER anhängen.
  • LLM-Freundlichkeit: AGENTS.md (Architektur, Invarianten, Pitfalls), prettier-formatierter Code (npm run format), Tests formatierungs-agnostisch.

Betrieb

  • Graceful Shutdown: SIGTERM → offene Verbindungen werden beendet, laufende Requests abgeschlossen.
  • Backups: verschlüsselt, automatisch bei Serverstart, nach jedem Kauf und bei Session-Ende (+ 5-min-Snapshots) + manuell in der Admin-UI.
  • API-Reset: POST /api/reset (Permission manageFeatures) setzt ausgewählte Bereiche zurück (transactions / audit-log / members / events / alles) — mit Absicherung gegen versehentliches Auslösen.
  • Beenden: manuell Ctrl+C (SIGTERM), Docker docker compose down.

Umgebungsvariablen

Variable Beschreibung Default
PORT Server-Port 3000
DB_PATH SQLite-Dateipfad data/bar.db (Docker: /app/data/bar.db)
BUSINESS_TIMEZONE Zeitzone für Tagesberichte (Zeiten in UTC gespeichert) Europe/Berlin
TRUSTED_PROXY Boolean: X-Forwarded-Proto/Host von erkannten Proxys vertrauen true
TRUSTED_PROXY_IP Peers für X-Forwarded-*: loopback (Default), auto (Loopback + eigene private Netze, Opt-in), explizite Liste (kommagetrennt, CIDR erlaubt), * = jeder Hop loopback (compose.yaml setzt auto)
REQUIRE_HTTPS HTTP-Anfragen ablehnen (426) true
PUBLIC_URL Öffentliche URL (redirect_url für SumUp-Hosted-Checkout) —
SUMUP_API_KEY SumUp API Key (me.sumup.com → Developer Settings) —
SUMUP_MERCHANT_CODE SumUp Merchant Code (Pflicht für Cloud API) —
SUMUP_CURRENCY Währung EUR
SUMUP_AFFILIATE_KEY Affiliate Key (Cloud API; Toolkit → Affiliate Keys) —
SUMUP_APP_ID App-ID, z. B. at.bar.vereinsbar —
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS Mail-Versand (Alerts, PIN-Reset, Mahnungen) — / 587 / — / —
SMTP_FROM Absender-Adresse noreply@barsystem.local
IBAN / BIC / ACCOUNT_HOLDER Konto für EPC-QR-Codes —
API_ONLY Nur API, kein Frontend false
CORS_ORIGIN Explizite CORS-Origin (leer = same-origin) —
RATE_LIMIT_WINDOW / RATE_LIMIT_MAX Rate-Limit-Fenster/Requests 15 / 200
RATE_LIMIT_DUMP Rate-Limit-Zustand über Restart speichern (0 = aus) 1
RATE_LIMIT_DUMP_PATH Pfad der Rate-Limit-Dump-Datei data/rate-limit-dump.json
AUDIT_LOG_DAYS Audit-Log-Rotation 90
ADMIN_EMAIL Admin-E-Mail für Alerts —
PORTAL_RESET_MAX_ATTEMPTS Max Fehlversuche PIN-Reset 5
PBKDF2_ITERATIONS Passwort-Hash-Iterationen (Tests: 1000) 600000
PASSWORD_MAX_LENGTH Max Passwort-/PIN-Länge 256
PASSWORD_MIN_LENGTH Min Passwort-/PIN-Länge 8
INITIAL_ADMIN_PASSWORD Optional: Bootstrap-Admin-Passwort (sonst Setup-Wizard); kein Default —
INITIAL_MANAGER_PASSWORD / INITIAL_STAFF_PASSWORD Optionale Bootstrap-Zugänge —
BACKUP_ENCRYPTION_KEY 32-Byte-Schlüssel AES-256-GCM — (Docker: automatisch in data/secrets.env)
SETUP_TOKEN Setup-Wizard-Token (HTTPS/Prod) — (Docker: automatisch + Log)
AUTO_SECRETS Fehlende Secrets beim Container-Start erzeugen (1/0) 1
VPS_KNOWN_HOSTS known_hosts für strikte SCP-Prüfung Systemdatei
APP_UID / APP_GID Container-User (Docker) 1001

Docker (optional)

cp .env.example .env
# compose.yaml allein zieht nur aus der Registry (Produktion) —
# gebaut wird lokal/auf dem VPS über den Override:
docker compose -f compose.yaml -f compose.build.yaml up -d --build

compose.yaml bindet standardmäßig an 127.0.0.1; Daten unter ./data/.

Bind-Mount & Dateirechte: Der Entrypoint chown-t ./data beim Start auf den Container-User (APP_UID/APP_GID, Default 1001). Läuft der Host-User unter einer anderen UID (z. B. 1000), verliert er den Schreibzugriff auf data/bar.db. Fix: APP_UID/APP_GID in .env auf die Host-UID setzen (oder ein Named Volume statt Bind-Mount verwenden).

Legacy-Backups (unverschlüsselt): Vor der Verschlüsselung erzeugte Backups (data/backups/barsystem_backup_*.db.gz, ggf. baseline_pre_features_*.db) enthalten Mitglieder-PII im Klartext und sind seit dem Update über die Backup-UI sichtbar/löschbar. Aufräumen: rm data/backups/barsystem_backup_*.db.gz.


API

Alle REST-Endpunkte sind direkt im Source Code definiert — eine Route pro Datei unter src/routes/ (auth.js, members.js, products.js, checkout.js, topups.js, terminal.js, portal.js, events.js, shifts.js, reports.js, zReport.js, settings.js, backups.js, auditLog.js, fees.js, maintenance.js, setup.js …). Detaillierte Endpoint-Referenz: API.md.


Entwicklung

npm test             # Jest parallel (--maxWorkers=50%), Savepoint-Isolation
npm run test:serial  # deterministischer serieller Lauf (--runInBand)
npm run test:frontend # jsdom-Frontend-Suiten
npm run lint      # eslint src/ server.js tests/ public/js/ (0 Fehler Pflicht)
npm run format    # prettier über Backend + public/js + scripts (printWidth 100, singleQuote)
node scripts/check-migrations.js   # Migrations-Dry-Run (readonly)
  • Neue Routen: in server.js UND im Integrationstest-App-Setup registrieren.
  • Neue Service-Funktionen: direkt aus dem Heimatmodul importieren (kein Barrel mehr, B3).
  • Migrationen: nur neue Datei in src/db/migrations/ anlegen (v45+), Signatur { version, name, up(db) }.
  • Frontend: KEINE Inline-onclick=/onchange=-Handler mehr — data-action
    • data-args='${JSON.stringify([...])}' verwenden (siehe AGENTS.md).
  • Test-Anpassung: Tests, die buggy Verhalten asserten, aktualisieren statt löschen; global.fetch-Mocks in afterEach wiederherstellen.

Bekannte offene Punkte

Siehe KNOWN_ISSUES.md — lebende Liste der offenen Punkte (Operator-Aufgabe R-20, SumUp-Sandbox + Solo-Hardware, L12-UX-Rest, Idempotenz-/Härtungs-Restpunkte, Struktur-Rest). Abgeschlossenes steht dort nicht mehr: Historie in CHANGELOG.md und git log, Betrieb in docs/INFRASTRUCTURE.md.