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]>
118 lines
5.5 KiB
SQL
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);
|