Files
dogfather-universe/cloudflare-worker/migrations/0012_webdesign_zahlungen.sql
T
DogFatherGitandClaude Opus 5 4b3ec450d5 Webdesign-Bereich: Fundament, oeffentliche Seiten, Zugangsschutz, PayPal
Umsetzung des "Website Masterplan" (16 Seiten) unter /webdesign.

Zugangsschutz mit EIGENER Schranke (server/webdesign-gate.js) statt gate.js:
gate.js laesst seit dem oeffentlichen Start am 21.08.2026 jeden durch, weil
die Pruefung auf SITE_PUBLIC_LAUNCH_AT als allererste Zeile steht. Haette man
/webdesign dahintergehaengt, waere der ausdruecklich nicht-oeffentliche Bereich
inklusive Preisen und spaeteren Kundendaten ab der ersten Sekunde fuer jeden
lesbar gewesen. Eigenes Sitzungs-Cookie, bereich="webdesign" im Token, damit
ein gueltiges Universe-Cookie hier NICHT gilt. 37/37 Tests.

Sieben oeffentliche Seiten in fuenf Sprachen (de, de-CH mit echtem Dialekt, en,
fr, pt). Preise, Zeitrahmen, Paketnamen und die 30-%-Regel stehen an genau
EINER Stelle in wd-core.js -- der Masterplan verlangt "ueberall
widerspruchsfrei", und vier Kopien laufen bei der ersten Preisaenderung
auseinander.

Als App installierbar auf Handy und PC. Der Service Worker speichert bewusst
KEINE HTML-Seite zwischen: nach dem Abmelden wuerden sonst geschuetzte Seiten
weiter ausgeliefert, ohne dass der Server je gefragt wird. 18/18 Tests.

Handy-Abnahme ueber alle Seiten in fuenf Breiten (320-1440) und fuenf Sprachen:
40/40. Der Test fand 35 echte Fehler (Touch-Ziele unter 44px), behoben im
Designsystem statt einzeln pro Seite.

PayPal (Wunsch 22.08.2026 "sofort auf meinem paypal"): Orders API mit
intent=CAPTURE, also sofortiger Einzug statt blosser Reservierung. Gebuehr und
Nettobetrag getrennt gespeichert. Betraege durchgehend als Ganzzahl in Cent.
Gefaelschte Webhooks werden abgewiesen. Fail closed solange Zugangsdaten
fehlen. 28/28 Tests gegen einen nachgebauten PayPal-Server.

Datenbank: 15 Tabellen mit Praefix wd_, fachlich vollstaendig vom Universe
getrennt.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-22 18:06:52 +02:00

118 lines
5.5 KiB
SQL

-- =====================================================================
-- 0012_webdesign_zahlungen.sql — Zahlungen im Webdesign-Bereich
--
-- Vier Zahlungswege (Entscheidung 22.08.2026):
-- anzahlung 30 % bei Angebotsannahme -> PayPal Orders (einmalig)
-- restbetrag nach der Abnahme -> PayPal Orders (einmalig)
-- zusatz angenommenes Zusatzangebot -> PayPal Orders (einmalig)
-- betreuung monatlich ab 49 € -> PayPal Subscriptions
--
-- Grundsätze:
-- * Beträge IMMER in Cent als Ganzzahl. Gleitkomma ist bei Geld falsch.
-- * Eine Zahlung wird NIE gelöscht und NIE überschrieben — auch eine
-- fehlgeschlagene ist ein Beleg dafür, was passiert ist.
-- * Der Zustand kommt am Ende immer von PayPal, nie vom Browser des
-- Kunden. Deshalb gibt es zusätzlich zur Rückleitung einen Webhook.
-- =====================================================================
CREATE TABLE IF NOT EXISTS wd_zahlungen (
id TEXT PRIMARY KEY,
nummer TEXT NOT NULL UNIQUE, -- "Z-2608-0001"
kunde_id TEXT NOT NULL,
projekt_id TEXT,
aenderung_id TEXT, -- bei Zusatzangeboten
art TEXT NOT NULL, -- anzahlung | restbetrag | zusatz | betreuung
zweck_text TEXT NOT NULL, -- was der Kunde bei PayPal liest
betrag_cent INTEGER NOT NULL,
waehrung TEXT NOT NULL DEFAULT 'EUR',
-- offen = angelegt, Kunde war noch nicht bei PayPal
-- freigegeben= Kunde hat bei PayPal bestätigt, Einzug läuft
-- bezahlt = Geld ist eingezogen und auf dem Konto
-- fehlge = Einzug abgelehnt (Deckung, Abbruch, Zeitablauf)
-- erstattet = ganz oder teilweise zurückgezahlt
status TEXT NOT NULL DEFAULT 'offen',
-- PayPal-Kennungen. order_id bei Einmalzahlung, subscription_id beim Abo.
paypal_order_id TEXT,
paypal_capture_id TEXT,
paypal_subscription_id TEXT,
-- Was PayPal tatsächlich gutgeschrieben hat, nach Abzug der Gebühr.
-- Bewusst getrennt vom Rechnungsbetrag: die Gebühr ist Aufwand, kein
-- Rabatt, und für die Buchhaltung müssen beide Zahlen erhalten bleiben.
netto_cent INTEGER,
gebuehr_cent INTEGER,
-- Verbraucherschutz (gemischte Privat-/Firmenkunden, Entscheidung
-- 22.08.2026). Bei einer Dienstleistung, die vor Ablauf der 14 Tage
-- beginnt, erlischt das Widerrufsrecht NUR, wenn der Kunde vorher
-- ausdrücklich zugestimmt hat. Ohne Zeitstempel ist das im Streitfall
-- nicht beweisbar — genau derselbe Nachweis wie beim Supporter-Abo.
widerruf_zugestimmt INTEGER NOT NULL DEFAULT 0,
widerruf_zugestimmt_am TEXT,
agb_zugestimmt_am TEXT,
kundenart TEXT, -- privat | firma
erstellt_am TEXT NOT NULL,
freigegeben_am TEXT,
bezahlt_am TEXT,
aktualisiert_am TEXT,
fehler_text TEXT,
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE,
FOREIGN KEY (projekt_id) REFERENCES wd_projekte(id) ON DELETE SET NULL,
FOREIGN KEY (aenderung_id) REFERENCES wd_aenderungen(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_wd_zahlungen_kunde ON wd_zahlungen(kunde_id, status);
CREATE INDEX IF NOT EXISTS idx_wd_zahlungen_projekt ON wd_zahlungen(projekt_id, art);
CREATE INDEX IF NOT EXISTS idx_wd_zahlungen_order ON wd_zahlungen(paypal_order_id);
CREATE INDEX IF NOT EXISTS idx_wd_zahlungen_abo ON wd_zahlungen(paypal_subscription_id);
CREATE INDEX IF NOT EXISTS idx_wd_zahlungen_zeit ON wd_zahlungen(erstellt_am DESC);
-- ---------------------------------------------------------------------
-- Eingegangene PayPal-Webhooks.
--
-- Zwei Gründe für diese Tabelle:
-- 1. Doppelte Zustellung. PayPal sendet dasselbe Ereignis mehrfach, wenn
-- unsere Antwort nicht ankommt. Ohne Merkliste würde eine Zahlung
-- mehrfach verbucht. Die event_id als Primärschlüssel macht das
-- unmöglich.
-- 2. Nachvollziehbarkeit. Wenn Geld fehlt, muss man sehen können, was
-- PayPal wann gemeldet hat — nicht nur, was wir daraus gemacht haben.
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS wd_paypal_ereignisse (
event_id TEXT PRIMARY KEY,
typ TEXT NOT NULL,
zahlung_id TEXT,
signatur_ok INTEGER NOT NULL DEFAULT 0,
verarbeitet INTEGER NOT NULL DEFAULT 0,
rohdaten TEXT,
empfangen_am TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_wd_paypal_zeit ON wd_paypal_ereignisse(empfangen_am DESC);
-- Betreuungs-Abos. Eigene Tabelle statt einer Spalte am Kunden, weil ein
-- Kunde mehrere Websites mit je eigener Betreuung haben kann.
CREATE TABLE IF NOT EXISTS wd_betreuung_abos (
id TEXT PRIMARY KEY,
kunde_id TEXT NOT NULL,
projekt_id TEXT,
paket TEXT NOT NULL,
preis_cent INTEGER NOT NULL,
paypal_subscription_id TEXT UNIQUE,
-- angelegt | aktiv | pausiert | gekuendigt | abgelaufen
status TEXT NOT NULL DEFAULT 'angelegt',
start_am TEXT,
naechste_zahlung TEXT,
gekuendigt_am TEXT,
laeuft_bis TEXT, -- Zugang bleibt bis dahin
erstellt_am TEXT NOT NULL,
aktualisiert_am TEXT,
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE,
FOREIGN KEY (projekt_id) REFERENCES wd_projekte(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_wd_abos_kunde ON wd_betreuung_abos(kunde_id, status);