Angebote: der Kunde sagt mit einem Klick zu

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]>
This commit is contained in:
2026-08-24 12:14:27 +02:00
co-authored by Claude Opus 5
parent 658febb6c5
commit cea265eb8c
8 changed files with 823 additions and 4 deletions
+18
View File
@@ -221,6 +221,24 @@ pruefe("danach sind es null", Auto.meldungenOffen() === 0);
pruefe("aber geloescht wurde nichts",
db.prepare(`SELECT COUNT(*) AS n FROM wd_meldungen`).get().n >= offenVorher);
L("\n12. GELDBETRAEGE — zwei Fehler, die man nur bei bestimmten Zahlen sieht");
const { centZuText } = await import("./lib/webdesign-helfer.js");
/* Beide fielen erst auf, als das erste Angebot über tausend Euro
entstand — alle früheren Testbeträge lagen darunter, und da sieht man
weder den fehlenden Tausenderpunkt noch das verlorene Minuszeichen. */
pruefe("Tausender bekommen einen Punkt", centZuText(149000) === "1.490,00 €", centZuText(149000));
pruefe("Millionen auch", centZuText(100000000) === "1.000.000,00 €", centZuText(100000000));
pruefe("darunter bleibt es unverändert", centZuText(99900) === "999,00 €", centZuText(99900));
/* Math.trunc(-0.5) ergibt -0, und -0 schreibt sich als "0": Eine
Erstattung von 50 Cent erschien damit als Forderung. */
pruefe("kleine NEGATIVE Beträge behalten ihr Minus", centZuText(-50) === "-0,50 €", centZuText(-50));
pruefe("größere negative auch", centZuText(-149000) === "-1.490,00 €", centZuText(-149000));
pruefe("null bleibt null", centZuText(0) === "0,00 €", centZuText(0));
pruefe("nichts bleibt nichts", centZuText(null) === null);
pruefe("die Nachkommastellen stimmen", centZuText(1490) === "14,90 €", centZuText(1490));
pruefe("eine andere Währung wird angehängt",
centZuText(1000, "CHF") === "10,00 CHF", centZuText(1000, "CHF"));
L(`\n===== ${ok} bestanden, ${fehler} fehlgeschlagen =====`);
process.exitCode = fehler ? 1 : 0;
rmSync(tmp, { recursive: true, force: true });