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]>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
-- =====================================================================
|
||||
-- 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);
|
||||
Reference in New Issue
Block a user