-- ===================================================================== -- 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);