DER LEITGEDANKE: SO WENIG TIPPEN WIE MOEGLICH Niemand schreibt hier den Leistungsumfang. Er entsteht aus der Aufgabenvorlage des Pakets -- denselben 92 Punkten, nach denen spaeter gearbeitet wird. Der Nebeneffekt ist wichtiger als die gesparte Tipparbeit: Was im Angebot steht, IST die Arbeitsliste. Ein von Hand geschriebenes Angebot und eine getrennt gepflegte Aufgabenliste laufen unweigerlich auseinander -- und dann steht im Angebot etwas, das niemand abarbeitet. Zu tun bleibt: Paket waehlen, Preis bestaetigen. Beides ist vorbelegt, Laufzeit und Ablaufdatum werden gerechnet. DER WORTLAUT WIRD BEIM ABSENDEN EINGEFROREN Ein angenommenes Angebot ist ein Vertrag. Bei einem Streit zaehlt, WAS dem Kunden gezeigt wurde, als er zusagte. Wuerde der Text bei jeder Ansicht neu aus den Vorlagen erzeugt, staende nach der naechsten Vorlagenaenderung etwas anderes da als damals. Die Zusage wird mit Zeitpunkt und (gehashter) Herkunft belegt -- die Beweislast liegt beim Unternehmer. WAS AUTOMATISCH PASSIERT Angebot raus -> Anfrage steht auf 'angebot' (der Wunsch: 'wenn ich ein angebot rausschicke soll der automatisch das erkennen'). Zusage -> Projekt angelegt, Aufgabenliste eingesetzt, Anzahlung als offene Rechnung erzeugt, Anfrage auf 'angenommen', Benachrichtigung an mich. Die UHR startet dabei NICHT. Sie startet erst mit dem Zahlungseingang -- das ist die Regel aus dem vorigen Schritt, und sie gilt auch dann, wenn der Kunde selbst zugesagt hat. Genau dieser Punkt wird ausdruecklich geprueft: Es fuehlt sich richtig an, mit der Zusage loszulegen. ZWEI FEHLER IN DER GELDANZEIGE GEFUNDEN Beide fielen erst auf, als das erste Angebot ueber tausend Euro entstand -- alle frueheren Testbetraege lagen darunter: - Kein Tausenderpunkt: 1490 Euro erschienen als '1490,00 €'. - Das Minuszeichen ging verloren: Math.trunc(-0.5) ergibt -0, und -0 schreibt sich als '0'. Eine ERSTATTUNG von 50 Cent erschien damit als '0,50 €' -- also wie eine Forderung. Ausgerechnet beim Abbruch mit Rueckzahlung waere das der falsche Ort fuer einen Anzeigefehler. Bewusst von Hand statt ueber Intl.NumberFormat: Die Ausgabe muss auf dem Server und im Browser zeichengleich sein -- ein Betrag, der in der Verwaltung anders aussieht als im Portal, saet Zweifel an der Zahl. Geprueft: 56 zu den Angeboten, 53 zur Automatik (inkl. der neun Geldpruefungen), 70 zur Annahme, 28 zur Suche -- alle gegen eine echte Datenbank. Darunter: zweiter Klick legt nichts doppelt an, abgelaufene Angebote lassen sich nicht mehr annehmen (ein Angebot, das man sieht aber nicht annehmen kann, waere eine Falle), ein fremder Kunde erfaehrt nicht einmal, dass ein Angebot existiert. Co-Authored-By: Claude Opus 5 <[email protected]>
69 lines
2.9 KiB
SQL
69 lines
2.9 KiB
SQL
-- =====================================================================
|
|
-- Angebote mit Zusage im Portal
|
|
--
|
|
-- Ein Angebot, das der Kunde online verbindlich annimmt, ist ein
|
|
-- Vertragsschluss über eine elektronische Benutzeroberfläche. Daraus
|
|
-- folgen zwei Dinge, die diese Tabelle tragen muss:
|
|
--
|
|
-- 1. DER WORTLAUT WIRD FESTGEHALTEN, NICHT NUR DER PREIS.
|
|
-- Bei einem Streit zählt, WAS dem Kunden gezeigt wurde, als er
|
|
-- zusagte. Würde man nur preis_cent und paket speichern und den Text
|
|
-- bei jeder Ansicht neu erzeugen, stünde nach der nächsten Änderung
|
|
-- an den Vorlagen etwas anderes da als damals — und niemand könnte
|
|
-- mehr belegen, worauf man sich geeinigt hat. Deshalb wird der
|
|
-- vollständige Leistungstext beim ABSENDEN eingefroren.
|
|
--
|
|
-- 2. DIE ZUSAGE WIRD MIT ZEITPUNKT UND HERKUNFT BELEGT.
|
|
-- Wer zugesagt hat, wann, und von welcher Verbindung aus. Die
|
|
-- Beweislast liegt beim Unternehmer.
|
|
--
|
|
-- Ein Angebot hängt an einer ANFRAGE, nicht an einem Projekt: Es geht
|
|
-- der Annahme voraus. Erst die Zusage des Kunden macht daraus ein
|
|
-- Projekt.
|
|
-- =====================================================================
|
|
CREATE TABLE IF NOT EXISTS wd_angebote (
|
|
id TEXT PRIMARY KEY,
|
|
nummer TEXT NOT NULL UNIQUE,
|
|
anfrage_id TEXT,
|
|
kunde_id TEXT,
|
|
projekt_id TEXT, -- entsteht erst mit der Zusage
|
|
|
|
paket TEXT NOT NULL,
|
|
titel TEXT NOT NULL,
|
|
preis_cent INTEGER NOT NULL,
|
|
anzahlung_cent INTEGER NOT NULL,
|
|
waehrung TEXT NOT NULL DEFAULT 'EUR',
|
|
laufzeit_werktage INTEGER,
|
|
|
|
-- Der eingefrorene Wortlaut. Siehe Kopfkommentar: Er wird NICHT bei
|
|
-- jeder Ansicht neu erzeugt.
|
|
leistungen_text TEXT NOT NULL,
|
|
hinweis_text TEXT,
|
|
sprache TEXT NOT NULL DEFAULT 'de',
|
|
|
|
-- entwurf | gesendet | angenommen | abgelehnt | abgelaufen | zurueckgezogen
|
|
status TEXT NOT NULL DEFAULT 'entwurf',
|
|
-- Ein Angebot ohne Ablauf bindet unbefristet. Vier Wochen sind lang
|
|
-- genug zum Überlegen und kurz genug, dass ein Preis von damals nicht
|
|
-- ein Jahr später noch gilt.
|
|
gueltig_bis TEXT,
|
|
|
|
gesendet_am TEXT,
|
|
entschieden_am TEXT,
|
|
zusage_ip_hash TEXT,
|
|
ablehnung_grund TEXT,
|
|
|
|
erstellt_am TEXT NOT NULL,
|
|
aktualisiert_am TEXT,
|
|
|
|
-- Bewusst ON DELETE SET NULL statt CASCADE: Wird eine Anfrage
|
|
-- gelöscht, darf der Beleg über ein angenommenes Angebot nicht
|
|
-- mitverschwinden.
|
|
FOREIGN KEY (anfrage_id) REFERENCES wd_anfragen(id) ON DELETE SET NULL,
|
|
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE SET NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_wd_angebote_anfrage ON wd_angebote (anfrage_id);
|
|
CREATE INDEX IF NOT EXISTS idx_wd_angebote_kunde ON wd_angebote (kunde_id, status);
|
|
CREATE INDEX IF NOT EXISTS idx_wd_angebote_status ON wd_angebote (status, gueltig_bis);
|