Spezifikation nachgeschärft, Plan-Übersicht mit Verträgen der Stränge
Nachträge aus der Planrunde (Fragment-Link, Teamkopie ohne Link, Schalter AUFTRAEGE_ENABLED, Projekt später zuordnen, Schlussrechnung bis 40 Zeilen, zusammengesetzte Sperrschlüssel, Zuständigkeiten). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e00b4ae572
commit
0bb463eb7d
2 files changed
+434
-16
No files matched your search
@@ -0,0 +1,401 @@
|
|||||||
|
# Aufträge und Rechnungen – Umsetzungsplan, Übersicht und Verträge
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
> Dieser Plan ist auf **parallele Stränge** ausgelegt (Nutzerwunsch 06.10.2026: „so viele Sub-Agents wie möglich, viel nebenbei, zum Schluss die Kontrolle“). Jeder Strang hat eine eigene Plan-Datei, einen eigenen Worktree und **eigene Dateien**. Diese Übersicht ist der Vertrag zwischen den Strängen: Namen, Signaturen, Datenformen und Endpunkte stehen nur hier und gelten für alle.
|
||||||
|
|
||||||
|
**Goal:** Admins legen in WOMS beim Kunden Auftragsbestätigungen (AB) und Rechnungen zu einem Projekt an, sehen je Projekt ein Projektkonto (vereinbart, berechnet, bezahlt, offen, Zeit) und rechnen per Anzahlung, Schlussrechnung, nach Aufwand oder als Festbetrag ab.
|
||||||
|
|
||||||
|
**Architecture:** Der Portal-Server (`kundenbereich.webklar.com`) bekommt einen Baustein `server/services/auftraege/` (Speicher, Projektkonto, Positionsbau, AB-Logik, PDF über pdfkit, Bestätigungs-Token, Mails) und erweitert die bestehende Stripe-Rechnung (`stripeFinance.js`). WOMS (`ticket.webklar.com`) zeigt Projektkonten, AB- und Rechnungsdialoge im neuen weißen Design; das Kundenportal bekommt eine Bestätigungsseite, einen Abschnitt „Aufträge“ und die Vertragsart „Projektauftrag“ auf der Widerrufsseite. Der AB-Teil hängt hinter dem Schalter `AUFTRAEGE_ENABLED` (Vorgabe `off`), damit der Rechnungsteil vor der Rechtsrunde live gehen kann.
|
||||||
|
|
||||||
|
**Tech Stack:** Node 20.19 (Container) und 24 (Host), Express 4, Appwrite-REST über `server/services/appwriteAdmin.js`, Stripe SDK 19 (API 2025-10-29.clover), pdfkit (neu), nodemailer mit HTML-Mailvorlagen (`server/mail-templates`), Tests mit `node --test` und den Nachbauten in `test/support/` (Appwrite, Stripe, Mail-Senke, Test-App); WOMS: React 18, Vite, Tests mit `npm test` (`tests/unit/*.test.mjs`).
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-10-06-auftraege-und-rechnungen-design.md` (Commit e00b4ae). Jeder Implementierer liest Spec **und** diese Übersicht **und** seine Strang-Datei.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ausführungsmodell
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase A L0 Fundament (1 Agent) ───────────────────────────────┐
|
||||||
|
Phase B L1 Projektkonto · L2 Rechnungen · L3 AB-Kern · L4 AB-PDF · L5 Bestätigung+Mails · L6 Kundenportal · L7 Rechtstexte (7 Agents parallel)
|
||||||
|
Phase W [WOMS-Redesign P4+P6+P7 zusammengeführt] → W0 WOMS-Basis (1 Agent) → W1 Rechnungs-UI · W2 AB-UI (2 Agents parallel)
|
||||||
|
Phase Z Z1 Portal-Integration ‖ Z2 WOMS-Integration → Z3 Kontrolle (5 Prüfer parallel → Bestätigung → Korrektur) → Übergabe
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Ein Implementierer je Strang** (Modell `sonnet`), Tasks im Strang der Reihe nach, TDD. **Keine Reviewer je Task** (Nutzerwunsch); jeder Strang endet mit seiner **Selbstprüfung** (letzter Task der Strang-Datei: eigene Tests grün, Vertragsliste abgehakt, `git status` sauber).
|
||||||
|
- **Phase W** startet, sobald die Redesign-Stränge Kunden (P4, P6) und Finanzen (P7) in `feature/design-weiss` zusammengeführt und getaggt sind (Tags `design-p4` … `design-p7`, siehe Memory „woms-theme-weiss“). Fehlen sie beim Start von Phase W, fragt der Orchestrator den Nutzer (warten oder auf `test` bauen und eine Mitnahme-Notiz in `/root/reports/ticketsystem-design-2026-10-04/strang/mitnehmen.md` hinterlegen).
|
||||||
|
- **Phase Z**: Z1 und Z2 dürfen parallel laufen; Z3 erst, wenn beide fertig sind. Kleine Prüfungen in Z3 (Bestätigung einzelner Befunde, Ein-Datei-Korrekturen) mit `haiku`. Kein `opus`/`fable`.
|
||||||
|
- Ausführung wahlweise mit dem Workflow-Werkzeug (Phasen wie oben) oder mit parallelen Agent-Aufrufen; die Plan-Dateien sind für beides gleich.
|
||||||
|
|
||||||
|
### Stränge, Plan-Dateien, Worktrees
|
||||||
|
|
||||||
|
| Strang | Plan-Datei (`docs/superpowers/plans/`) | Worktree | Branch | Start |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| L0 Fundament | `2026-10-06-auftraege-01-fundament.md` | `/home/webklar/apps/kundenbereich.webklar.com/dev-auftraege` | `feature/auftraege` | sofort |
|
||||||
|
| L1 Projektkonto | `…-02-projektkonto.md` | `…/dev-auftraege-l1` | `auftraege/l1-projektkonto` | nach L0 |
|
||||||
|
| L2 Rechnungen | `…-03-rechnungen.md` | `…/dev-auftraege-l2` | `auftraege/l2-rechnungen` | nach L0 |
|
||||||
|
| L3 AB-Kern | `…-04-ab-kern.md` | `…/dev-auftraege-l3` | `auftraege/l3-ab-kern` | nach L0 |
|
||||||
|
| L4 AB-PDF | `…-05-ab-pdf.md` | `…/dev-auftraege-l4` | `auftraege/l4-ab-pdf` | nach L0 |
|
||||||
|
| L5 Bestätigung + Mails | `…-06-bestaetigung.md` | `…/dev-auftraege-l5` | `auftraege/l5-bestaetigung` | nach L0 |
|
||||||
|
| L6 Kundenportal | `…-07-kundenportal.md` | `…/dev-auftraege-l6` | `auftraege/l6-kundenportal` | nach L0 |
|
||||||
|
| L7 Rechtstexte | `…-08-rechtstexte.md` | `…/dev-auftraege-l7` | `auftraege/l7-rechtstexte` | nach L0 |
|
||||||
|
| W0 WOMS-Basis | `…-09-woms-basis.md` | `/home/webklar/apps/ticket.webklar.com/dev-auftraege` | `feature/auftraege` (WOMS) | Redesign P4+P6+P7 da |
|
||||||
|
| W1 Rechnungs-UI | `…-10-woms-rechnungen.md` | `…/ticket.webklar.com/dev-auftraege-w1` | `auftraege/w1-rechnungen` | nach W0 |
|
||||||
|
| W2 AB-UI | `…-11-woms-auftraege.md` | `…/ticket.webklar.com/dev-auftraege-w2` | `auftraege/w2-auftraege` | nach W0 |
|
||||||
|
| Z Integration + Kontrolle | `…-12-integration-kontrolle.md` | Basis-Worktrees | `feature/auftraege` (beide Repos) | nach Phase B bzw. W |
|
||||||
|
|
||||||
|
Lane-Worktrees entstehen aus dem Stand **nach L0**:
|
||||||
|
`git -C /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege worktree add ../dev-auftraege-l1 -b auftraege/l1-projektkonto feature/auftraege`
|
||||||
|
und bekommen `ln -s ../dev-auftraege/node_modules node_modules` (L0 legt dort ein echtes `node_modules` an). Der Symlink ist unversioniert: **nie `git add -A`**, immer Dateien einzeln.
|
||||||
|
|
||||||
|
### Dateizuständigkeit (Konfliktschutz)
|
||||||
|
|
||||||
|
Jede Datei hat genau einen Besitzer-Strang. Ein Strang ändert **nur** seine Dateien; braucht er etwas aus einer fremden Datei, nutzt er den Vertrag unten. Gemeinsame Dateien (`server/config.js`, `server/index.js`, `server/routes/team/index.js`, `package.json`, `package-lock.json`, `test/support/*`, `server/services/auftraege/konstanten.js`) ändert **nur L0**. Ausnahmen stehen in der Tabelle.
|
||||||
|
|
||||||
|
| Datei | Besitzer |
|
||||||
|
|---|---|
|
||||||
|
| `server/services/auftraege/schema.js`, `konstanten.js`, `schalter.js`, `auftragStore.js`, `projektRechnungenStore.js`, `auftragSicht.js`, `arbeitsblaetter.js`, `ablage.js`, `fehler.js` | L0 |
|
||||||
|
| `server/services/auftraege/texte.js` | L0 legt an (vorläufige Wortlaute), **danach nur L7** (nur Wortlaute, Exportnamen bleiben) |
|
||||||
|
| `scripts/auftraege-setup.mjs`, `server/services/finance/settings.js` (Feld `steuernummer`) | L0 |
|
||||||
|
| Router-Gerüste `server/routes/admin/projektkonten.js`, `server/routes/team/arbeitsblaetter.js` | L0 legt an, L1 füllt |
|
||||||
|
| Router-Gerüst `server/routes/admin/auftraege.js` | L0 legt an, L3 füllt |
|
||||||
|
| Router-Gerüste `server/routes/auftrag.js`, `server/routes/auftraegeKunde.js` | L0 legt an, L5 füllt |
|
||||||
|
| `server/services/auftraege/projektkonto.js` | L1 |
|
||||||
|
| `server/services/auftraege/positionen.js`, `rechnungsbau.js`; `server/services/finance/stripeFinance.js`; `server/routes/admin/finanzen.js`; `test/support/fakeStripe.js` (Ausnahme von „nur L0“: nur Ergänzungen für Rechnungsposten mit `amount`, `custom_fields`, `rendering`) | L2 |
|
||||||
|
| `server/services/auftraege/nummern.js`, `auftraege.js` | L3 |
|
||||||
|
| `server/services/auftraege/pdf.js`, `server/assets/fonts/*` | L4 |
|
||||||
|
| `server/services/auftraege/token.js`, `bestaetigung.js`, `mails.js`; `server/mail-templates/K31 …`, `K32 …`, `T22 …`; `server/routes/abo.js` (Widerruf `projekt`); `test/mailTemplates.test.js` (falls er die Vorlagenliste pinnt) | L5 |
|
||||||
|
| `public/auftrag.html`, `public/auftrag.js`, `public/auftrag.css`, `public/dashboard.html`, `public/app.js`, `public/widerrufen.html`, `server/services/customerInvoices.js` | L6 |
|
||||||
|
| `public/rechtliches/*` (neue Dateien), `scripts/build-legal-html.mjs`, `scripts/build-legal-pdfs.sh`, `public/datenschutz.html`, `docs/WEBklar-Datenschutzerklaerung.pdf` (+ `.sha256`), `konstanten.js` **nur** `AGB_FASSUNG` | L7 |
|
||||||
|
| Tests: jede Lane legt eigene Dateien `test/auftraege<Thema>.test.js` an; bestehende Testdateien nur, wenn die Tabelle sie nennt | je Lane |
|
||||||
|
|
||||||
|
|
||||||
|
### Nachträge nach der Planrunde (verbindlich, 06.10.2026)
|
||||||
|
|
||||||
|
Die Strang-Pläne wurden parallel geschrieben; ihre „Vertragsfragen“ sind hier entschieden. Wo ein Strang-Plan davon abweicht, gilt dieser Abschnitt.
|
||||||
|
|
||||||
|
**Zusätzliche Dateien je Strang (Zuständigkeit):**
|
||||||
|
- L0: zusätzlich `.gitignore` (Eintrag `node_modules` **ohne** Schrägstrich, damit auch der Symlink ignoriert wird) und `fontkit` als direkte Abhängigkeit in `package.json` (Version wie die pdfkit-Abhängigkeit).
|
||||||
|
- L2: zusätzlich `test/financeStripeRead.test.js` (erwarteter Schlüsselsatz von `mapInvoice`).
|
||||||
|
- L5: zusätzlich `server/mail-templates/K04 Eingang Widerruf.html` und `T09 Widerruf eingegangen.html` (nur Block `projekt`).
|
||||||
|
- L6: zusätzlich `public/auftrag-ansicht.js`, `public/auftrag-dom.js`, `public/auftrag-portal.js`, `public/auftrag-widerruf.js`, `test/auftraegeKundenportalDaten.js`, `test/ui/auftraege-kundenportal.ui.mjs`, je eine Zeile in `test/customerInvoices.test.js` und `test/publicForms.test.js`.
|
||||||
|
- L7: zusätzlich `docs/rechtliches/agb.md`, `docs/rechtliches/quellen/normen-auftraege-2026-10-06.txt`, Stand-Zeile in `test/legalTexts.test.js`.
|
||||||
|
- Alle: Testhilfen ohne `.test.js` (z. B. `test/helpers/auftraegeAb.js`, `test/auftraege…Hilfe.js`) gehören dem anlegenden Strang.
|
||||||
|
- WOMS: W0 `src/lib/financeApi.js`, `src/lib/financeRequest.js` (Blob), `src/lib/teamApi.js`, `src/lib/auftraegeEndpoints.js`, `src/lib/auftraegeAnsicht.js`, `src/lib/customerTabs.js` (+ Test), Container und vier Startdateien unter `src/components/auftraege/`, `CustomerDetailPage.jsx` (nur Reiterzeile), `CustomerInvoices.jsx` (gelöscht); W1 `ProjektkontoListe.jsx`, `RechnungDialog.jsx`, `projektkonto.css`, `rechnung-dialog.css`, `src/lib/projektkontoView.js`, `src/lib/rechnungDialogView.js` (+ weitere eigene Libs), `InvoiceTable.jsx`, `WorksheetList.jsx`, `WorksheetTimesModal.jsx`, `InvoiceCreateModal.jsx` (gelöscht); W2 `AuftragListe.jsx`, `AuftragDialog.jsx`, `auftrag.css`, `src/lib/auftragView.js`, `src/lib/auftragPdf.js`, `src/lib/kundenFelder.js`, `CustomerDetailPage.jsx` (Feld Kundenart), `SettingsCard.jsx`.
|
||||||
|
|
||||||
|
**Sperren:** siehe V2 (zusammengesetzte Einzel-Schlüssel). Reihenfolge: wer beide braucht, nimmt erst `sperreRechnung`, darin `sperreAuftrag`; innerhalb von `sperreAuftrag` nie `sperreRechnung`. Mails (K32/T22) erst nach Freigabe der Sperre.
|
||||||
|
|
||||||
|
**L0:** `ablage.entfernePdf(pdfDatei)` (räumt eine abgelegte PDF weg, wenn danach etwas scheitert; L3 nutzt genau diesen Namen); Export `AUFTRAEGE_DATETIMES`; `schemaKeys` auch für `financeSettings`; `oeffentlicheAnsicht` liefert `texte.kleinunternehmer`; Steuernummer mit Buchstaben (V4).
|
||||||
|
|
||||||
|
**L1:** Zeitwert = Summe der je Tag gerundeten Werte (wie `aufwandPositionen`); `projektTyp` = Anzeigename der Projektart; `offenCents` bei Stripe-Ausfall `null`; „Ohne Projekt“ = finalisierte Rechnungen mit `source: 'projekt'` ohne aktive Projektrechnung plus verwaiste Projektrechnungen; archivierte Projekte nur mit Geldbezug; bei ausgeschaltetem Schalter werden ABs nicht gelesen.
|
||||||
|
|
||||||
|
**L2:** `baueRechnung` bekommt zusätzlich `createdBy`; `zeilentexte[schluessel]` ersetzt den ganzen Text vor dem letzten „ – “ (einschließlich Datum bzw. „KW …“); **Selbstheilung**: stößt eine Prüfung („bereits abgerechnet“, offener Entwurf zur AB) auf eine Projektrechnung, deren Stripe-Rechnung storniert oder gelöscht ist, wird sie `aktiv: false` und die Prüfung wiederholt; nur finalisierte Rechnungen zählen als „bereits berechnet“, ein Schluss-Entwurf blockiert weitere Anzahlungen; Schlussrechnung bis 40 Zeilen; Aufwand ohne `auftragId` nimmt die jüngste gültige Aufwands-AB nur bei eingeschaltetem Schalter (sonst 100 €); Buchungsfehler nach einem Stripe-Erfolg werden protokolliert, nicht geworfen; Schreibweisen „KW 05/2026“ (Schlüssel `KW-2026-05`), `stundenText` ohne Endnullen, Texte eines Tages mit „; “ verbunden.
|
||||||
|
|
||||||
|
**L3:** Senden: Sperren `SPERRE_NUMMER` + `sperreAuftrag(id)` → **innerhalb** der Sperre Eingabe und Kunde prüfen → Nummer → PDF (`AuftragError` aus `erzeugeAuftragPdf`, z. B. 409 „Entwurf unvollständig …“, wird durchgereicht; andere Fehler 500) → `speicherePdf` → Token → speichern (scheitert danach etwas: `entfernePdf`) → Sperren frei → Mail mit dem **gespeicherten** Dokument. „Erneut senden“ läuft über `/erinnern`: nach einem Mail-Fehler als normale K31, sonst als Erinnerung; `gesendetAn` = aktuelle Kunden-E-Mail. Neu: `ordneProjektZu(id, projektId)` + Route `POST …/auftraege/:id/projekt` (V5). Stripe-Metadaten wandern beim Ersetzen **nicht** mit (nur `projektRechnungen.auftragId`).
|
||||||
|
|
||||||
|
**L4:** statische Figtree-TTFs aus `@expo-google-fonts/figtree` (OFL) unter `server/assets/fonts/`; reicht `privat` an `AB_TEXTE.termine` durch; leere Steuernummer → Zeile entfällt.
|
||||||
|
|
||||||
|
**L5:** K31-Link mit `#t=`; K31 ohne Team-BCC, Teamkopie ohne Link (Block `teamkopie`); T22-Betreff mit Präfix „[Aufträge]“; K32 mit `fruehbeginn_text`; K04/T09 Block `projekt`; `fruehbeginnFassung` bei Privatpersonen immer gespeichert, bei Unternehmen leer; Widerruf unabhängig vom Schalter, AB-Nummer optional.
|
||||||
|
|
||||||
|
**L6:** öffentliche Seite liest `#t=` (Rückfall `?t=`), sendet `credentials: 'omit'`; Abschnitt „Aufträge“ im Portal in Du-Form; Kleinunternehmer-Satz aus `texte.kleinunternehmer`; AB-Nummer auf der Widerrufsseite optional (Hinweis „falls zur Hand“); rechtlich gefärbte Sätze der Seiten stehen im Freigabe-Dokument von L7.
|
||||||
|
|
||||||
|
**L7:** Norm der Frühbeginn-Erklärung § 356 Abs. 5 Nr. 2 und § 357a Abs. 2 BGB; § 649 BGB (Kostenanschlag) ist die richtige Norm für den Überschreitungssatz; Datenschutz-Abschnitt als **letzter** Commit des Strangs (wird erst beim AB-Livegang gemergt); alte AGB-PDF-Fassungen bleiben liegen; Karten in WOMS → Rechtliches laut Freigabe-Dokument (Karte ohne Datei entfällt).
|
||||||
|
|
||||||
|
**Livegang:** Das Setup-Skript läuft **vor** dem Push (Wegwerf-Container mit dem Worktree, siehe Plan 12) oder als Ausweichweg sofort danach per `docker exec`; es legt `projektRechnungen` in jedem Fall an. Der WOMS-Push setzt das Attribut `customers.kundenart` voraus (Setup vorher).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Sprache: alle Texte Deutsch; Mails, PDF und die öffentliche Bestätigungsseite in der **Sie**-Form; im angemeldeten Kundenportal (Dashboard, Abschnitt „Aufträge“) die **Du**-Form wie im übrigen Portal; WOMS wie bestehend.
|
||||||
|
- Schreibweise im Code: nie ein Unicode-Escape mit genau vier Hexziffern (Backslash, `u`, vier Ziffern) schreiben – das Schreibwerkzeug macht daraus das Zeichen selbst (z. B. ein unsichtbares geschütztes Leerzeichen); stattdessen `\u{…}` oder `String.fromCodePoint(…)`.
|
||||||
|
- Kleinunternehmer: keine Umsatzsteuer; jede AB trägt „Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.“
|
||||||
|
- Beträge immer ganze **Cent** (`Number.isSafeInteger`); Anzeige über `euroDe(cents)` aus `server/services/finance/format.js` (bzw. WOMS `euro`).
|
||||||
|
- Datum intern als Berliner Kalendertag `YYYY-MM-DD` (`berlinDate` aus `server/services/finance/dates.js`), Anzeige `TT.MM.JJJJ` (`formatDe`); Zeitpunkte als ISO-UTC (`toISOString()`), Appwrite liefert `+00:00` → immer mit `new Date()` lesen.
|
||||||
|
- Stundensatz Standard **100 € = 10000 Cent**; Minuten je Arbeitsblatt = `worksheetMinutes(ws)` (inkl. Unterstützung), **je Arbeitsblatt auf 15 Minuten aufgerundet**.
|
||||||
|
- **Abo-Zeit** (`worksheets.countsToPlan === true`) und Kommentare (`isComment === true`) werden nie projektbezogen berechnet.
|
||||||
|
- Grenzen (Rechnung und AB): 1–20 Positionen bzw. Zeilen (Minuszeilen zählen mit; Ausnahme: eine Schlussrechnung darf bis 40 Zeilen haben – AB-Positionen ≤ 20, Zusatzpositionen ≤ 10, Abzüge ≤ 10), Beschreibung 1–200 Zeichen, Menge ganzzahlig 1–999, Einzelpreis 1–10.000.000 Cent, Summe ≤ 99.999.999 Cent, Zahlungsziel ganzzahlig 0–60 Tage (Standard 14), Leistungszeitraum ≤ 140 Zeichen.
|
||||||
|
- AB-Nummer `AB-JJJJ-NNNN` (4 Ziffern, je Kalenderjahr ab 0001, Berliner Jahr des Sendens).
|
||||||
|
- Bestätigungs-Token: 32 Zufallsbytes base64url (43 Zeichen), gespeichert **nur** als SHA-256 (hex), gültig **60 Tage** ab dem letzten Versand.
|
||||||
|
- **Keine IP-Adressen** speichern (Ratenbegrenzung nur im Arbeitsspeicher).
|
||||||
|
- Schalter `AUFTRAEGE_ENABLED` = `off` (Vorgabe) | `pilot` (nur Kunden, deren E-Mail in `AUFTRAEGE_PILOT_EMAILS` steht; `*` = alle) | `on`. Er betrifft nur AB-Funktionen und die Rechnungsarten `anzahlung`/`schluss`; Projektkonto und die Arten `frei`, `festbetrag`, `aufwand` laufen immer.
|
||||||
|
- Kein Chrome zur Laufzeit; PDFs der AB über **pdfkit**.
|
||||||
|
- Mail-Kennungen: **K31** Auftragsbestätigung, **K32** Auftrag bestätigt (Kunde), **T22** Auftrag bestätigt (Team).
|
||||||
|
- Tests: `node --test`; nur Nachbauten aus `test/support/` (`startAppwriteMock`, `startFakeStripe`, `useTestEnv`, `mailSink`, `startTestApp`), nie echtes Appwrite, Stripe oder SMTP. Neue Appwrite-Felder nur über `server/services/auftraege/schema.js` (der Appwrite-Nachbau prüft gegen das Schema).
|
||||||
|
- **Ressourcen** (Server mit wenig Arbeitsspeicher, Memory „server-speicher“): Testläufe nur gezielt und begrenzt: `cd <worktree> && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/<eigene Dateien>`. Die volle Suite (`npm test`) läuft **nur in Z1**, und schwere Läufe (volle Suite, Chrome-PDF-Druck, `npm ci`, `vite build`) laufen hinter `flock /tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad/auftraege-schwer.lock`. Vorher `free -m`: unter 1500 MB „available“ warten. Nie `pkill -f` (trifft die eigene Shell), Prozesse nur per PID beenden.
|
||||||
|
- Git: jeder Strang nur in seinem Worktree und Branch; Commits klein, je Task mindestens einer, Nachricht deutsch, Schlusszeile `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`; **kein Push** (der Nutzer pusht), nie `--force`, nie `git add -A`.
|
||||||
|
- Sicherheit der Umgebung (Memory „feedback-destruktive-skripte“): kein `rm -rf`/`rsync --delete` außerhalb des eigenen Worktrees bzw. Scratchpads; keine Live-Daten lesen oder schreiben (kein Prod-Appwrite, kein Live-Stripe, keine echten Mails); `build/` nie anfassen.
|
||||||
|
- Bestehendes Verhalten bleibt: alle vorhandenen Tests bleiben grün; die bisherigen Rechnungsfelder (`mapInvoice`) bleiben, neue kommen dazu.
|
||||||
|
|
||||||
|
## Review Focus
|
||||||
|
|
||||||
|
1. **Gleichzeitige Aufwandsrechnungen** mit denselben Arbeitsblättern (Doppelklick, zwei Admins): die zweite muss mit 409 „Arbeitsblatt bereits abgerechnet“ scheitern. Test in L2 (zwei parallele `createProjectInvoice` mit gleichen `arbeitsblattIds` unter Sperre `['auftraege-rechnung', kundenId]`).
|
||||||
|
2. **Alte Token nach Erinnerung, neuer Fassung, Storno oder Widerruf**: der alte Link darf nie eine AB bestätigen; die Seite erklärt den Grund. Tests in L5 (Erinnerung → alter Token 404; ersetzt/storniert/widerrufen → 410 mit Meldung).
|
||||||
|
3. **Schlussrechnung neben stornierten oder noch nicht gesendeten Rechnungen**: Stornos zählen nicht, ein offener Entwurf zur AB blockiert mit 409. Tests in L2.
|
||||||
|
4. **Unvollständige Kundendaten beim AB-Senden** (keine Kundenart, keine Anschrift, keine E-Mail, Interessent statt Kunde): klare Meldung **vor** Nummernvergabe und PDF, keine Nummer verbraucht. Tests in L3.
|
||||||
|
5. **Umlaute, €, Minuszeichen und lange Texte im PDF** (ä ö ü ß €, „−“, 200 Zeichen ohne Leerzeichen, 20 Positionen über eine Seite): lesbar, kein Glyphenausfall, Seitenumbruch mit Kopf der Tabelle. Tests in L4 (Text aus dem PDF lesen).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verträge
|
||||||
|
|
||||||
|
### V1 Collections und Attribute (`server/services/auftraege/schema.js`, L0)
|
||||||
|
|
||||||
|
Bauart wie `server/services/zeiten/schema.js` (reine Daten, kein Import von `config.js`; Helfer `str(key,size)`, `int(key)`, `bool(key)`, `dt(key)`, `index(key, ...attrs)`; alle Attribute `required: false`).
|
||||||
|
|
||||||
|
```js
|
||||||
|
export const AUFTRAEGE_COLLECTIONS = {
|
||||||
|
auftraege: {
|
||||||
|
attributes: [
|
||||||
|
str('nummer', 20), int('jahr'), int('laufnummer'), str('status', 20),
|
||||||
|
str('kundenId', 64), str('kundeJson', 2000), str('projektId', 64), str('titel', 200),
|
||||||
|
str('zeitquelleJson', 1000), str('art', 20), str('positionenJson', 20000),
|
||||||
|
int('stundensatzCents'), int('schaetzungMinuten'), int('summeCents'),
|
||||||
|
str('zahlungsplanJson', 1000), int('zahlungszielTage'),
|
||||||
|
str('auftragVom', 10), str('auftragWeg', 20), str('startAm', 10), str('fertigBis', 10),
|
||||||
|
str('texteFassung', 20), str('agbFassung', 20), str('pdfDatei', 300), str('pdfSha256', 64),
|
||||||
|
dt('gesendetAm'), str('gesendetAn', 255), str('mailStatus', 20), str('mailFehler', 500),
|
||||||
|
str('tokenHash', 64), dt('tokenGueltigBis'),
|
||||||
|
dt('bestaetigtAm'), str('bestaetigtUeber', 10), str('bestaetigtPdfSha256', 64),
|
||||||
|
bool('fruehbeginn'), str('fruehbeginnFassung', 20),
|
||||||
|
str('ersetztId', 64), str('ersetztDurchId', 64), dt('storniertAm'), dt('widerrufenAm'),
|
||||||
|
str('erstelltVon', 255), dt('erstelltAm'), dt('geaendertAm'),
|
||||||
|
],
|
||||||
|
indexes: [index('idx_kunde', 'kundenId'), index('idx_projekt', 'projektId'), index('idx_jahr', 'jahr'),
|
||||||
|
index('idx_token', 'tokenHash'), index('idx_nummer', 'nummer')], // idx_nummer NICHT unique: Entwürfe haben nummer ''
|
||||||
|
},
|
||||||
|
// Dokument-ID = Stripe-Rechnungs-ID (muss /^in_\w{1,33}$/ erfüllen; der Stripe-Nachbau vergibt in_test_<n>)
|
||||||
|
projektRechnungen: {
|
||||||
|
attributes: [
|
||||||
|
str('invoiceId', 64), str('kundenId', 64), str('projektId', 64), str('auftragId', 64), str('art', 20),
|
||||||
|
int('betragCents'), str('arbeitsblattIdsJson', 20000), str('rechnungsnummer', 64), bool('aktiv'),
|
||||||
|
str('erstelltVon', 255), dt('erstelltAm'),
|
||||||
|
],
|
||||||
|
indexes: [index('idx_kunde', 'kundenId'), index('idx_projekt', 'projektId'), index('idx_auftrag', 'auftragId')],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
export const AUFTRAEGE_ZUSATZ_ATTRIBUTE = {
|
||||||
|
customers: [str('kundenart', 20)], // '' | 'unternehmen' | 'privat'
|
||||||
|
financeSettings: [str('steuernummer', 40)],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`config.collections.auftraege` (`APPWRITE_COLLECTION_AUFTRAEGE`, Vorgabe `'auftraege'`), `config.collections.projektRechnungen` (`APPWRITE_COLLECTION_PROJEKT_RECHNUNGEN`, Vorgabe `'projektRechnungen'`).
|
||||||
|
`config.auftraege = { mode, pilotEmails, uploadDir }` mit `mode` aus `AUFTRAEGE_ENABLED` (wie `config.zeiten.mode`), `pilotEmails = commaList(process.env.AUFTRAEGE_PILOT_EMAILS, [])`, `uploadDir = process.env.AUFTRAEGE_UPLOAD_DIR || path.join(process.cwd(), 'upload-data', 'auftraege')`.
|
||||||
|
|
||||||
|
### V2 Konstanten (`server/services/auftraege/konstanten.js`, L0)
|
||||||
|
|
||||||
|
```js
|
||||||
|
export const RECHNUNGS_ARTEN = ['frei', 'festbetrag', 'aufwand', 'anzahlung', 'schluss']
|
||||||
|
export const AB_RECHNUNGS_ARTEN = ['anzahlung', 'schluss'] // brauchen AB und Schalter
|
||||||
|
export const AUFTRAG_ARTEN = ['festpreis', 'aufwand']
|
||||||
|
export const AUFTRAG_STATUS = ['entwurf', 'versendet', 'bestaetigt', 'abgerechnet', 'storniert', 'ersetzt', 'widerrufen']
|
||||||
|
export const VEREINBARTE_STATUS = ['versendet', 'bestaetigt', 'abgerechnet'] // zählen als „vereinbart“
|
||||||
|
export const AUFTRAG_WEGE = { muendlich: 'mündlich', telefon: 'telefonisch', email: 'per E-Mail', vor_ort: 'vor Ort', chat: 'im Chat' }
|
||||||
|
export const ZAHLUNGSPLAN_ARTEN = ['nach_abnahme', 'anzahlung', 'monatlich', 'frei']
|
||||||
|
export const KUNDENARTEN = ['unternehmen', 'privat']
|
||||||
|
export const STUNDENSATZ_STANDARD_CENTS = 10000
|
||||||
|
export const VIERTELSTUNDE = 15
|
||||||
|
export const TOKEN_GUELTIG_TAGE = 60
|
||||||
|
export const AGB_FASSUNG = '2026-09-30' // L7 darf diese eine Zeile anpassen
|
||||||
|
export const LIMITS = {
|
||||||
|
maxPositionen: 20, maxBeschreibung: 200, maxMenge: 999, maxEinzelpreisCents: 10_000_000,
|
||||||
|
maxSummeCents: 99_999_999, maxTitel: 200, maxZahlungsplanText: 500, maxZahlungszielTage: 60,
|
||||||
|
standardZahlungszielTage: 14, standardAnzahlungProzent: 50, maxZeitquellen: 10,
|
||||||
|
maxSchaetzungMinuten: 99_960, maxLeistungszeitraum: 140,
|
||||||
|
}
|
||||||
|
// withLocks sperrt jedes Listenelement einzeln: deshalb je Sperre GENAU EIN zusammengesetzter Schlüssel
|
||||||
|
export const SPERRE_NUMMER = ['auftraege-nummer']
|
||||||
|
export const sperreRechnung = (kundenId) => [`auftraege-rechnung:${kundenId}`] // Aufwand, Anzahlung, Schluss je Kunde
|
||||||
|
export const sperreAuftrag = (id) => [`auftraege-ab:${id}`] // jede Statusänderung einer AB (L2, L3, L5)
|
||||||
|
export const SCHALTER_AUS_MELDUNG = 'Auftragsbestätigungen sind für diesen Kunden noch nicht freigeschaltet.'
|
||||||
|
```
|
||||||
|
|
||||||
|
Fehlerklasse (`server/services/auftraege/fehler.js`, L0): `export class AuftragError extends Error { constructor(status, message) }` (Eigenschaft `status`) und `export function sendeFehler(res, was, err)`: `AuftragError` und `InvoiceError` (aus `stripeFinance.js`) → `res.status(err.status).json({ error: err.message })`; sonst Protokoll ohne Meldung (wie `logFailure` in `finanzen.js`) und `500 { error: 'Interner Fehler.' }`.
|
||||||
|
|
||||||
|
Statusübergänge der AB (nur diese):
|
||||||
|
`entwurf → versendet` (L3 senden) · `versendet → bestaetigt` (L5) · `versendet|bestaetigt → abgerechnet` (L2, Schlussrechnung finalisiert) · `abgerechnet → bestaetigt|versendet` (L2, Storno der Schlussrechnung; Ziel `bestaetigt`, wenn `bestaetigtAm` gesetzt) · `versendet|bestaetigt → storniert` (L3) · `versendet|bestaetigt|abgerechnet → ersetzt` (L3, beim Senden der neuen Fassung) · `versendet|bestaetigt → widerrufen` (L5, nur Kundenart `privat`). Bestätigen ist auch bei `abgerechnet` ohne `bestaetigtAm` möglich (Status bleibt `abgerechnet`).
|
||||||
|
|
||||||
|
### V3 Datenformen
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Auftrag (intern; auftragStore.docToAuftrag)
|
||||||
|
{
|
||||||
|
id, nummer /* '' im Entwurf */, jahr /* number|null */, laufnummer /* number|null */, status,
|
||||||
|
kundenId, kunde /* KundeSnapshot|null, beim Senden eingefroren */, projektId /* '' ohne Projekt */, titel,
|
||||||
|
zeitquelle /* string[] Ticket-IDs (workorders.$id) */, art /* 'festpreis'|'aufwand' */,
|
||||||
|
positionen /* AbPosition[]; aufwand: [] */, stundensatzCents /* aufwand, sonst null */,
|
||||||
|
schaetzungMinuten /* aufwand optional, sonst null */, summeCents /* festpreis, sonst null */,
|
||||||
|
zahlungsplan /* { art, prozent: number|null, text: string } */, zahlungszielTage,
|
||||||
|
auftragVom /* 'YYYY-MM-DD' */, auftragWeg /* Schlüssel aus AUFTRAG_WEGE */, startAm, fertigBis /* 'YYYY-MM-DD'|'' */,
|
||||||
|
ersetztId, ersetztDurchId, texteFassung, agbFassung, pdfDatei, pdfSha256,
|
||||||
|
gesendetAm /* ISO|null */, gesendetAn, mailStatus /* ''|'gesendet'|'fehler' */, mailFehler,
|
||||||
|
tokenHash, tokenGueltigBis /* ISO|null */,
|
||||||
|
bestaetigtAm /* ISO|null */, bestaetigtUeber /* ''|'link'|'portal' */, bestaetigtPdfSha256,
|
||||||
|
fruehbeginn /* boolean */, fruehbeginnFassung, storniertAm, widerrufenAm /* ISO|null */,
|
||||||
|
erstelltVon, erstelltAm, geaendertAm,
|
||||||
|
}
|
||||||
|
// KundeSnapshot (auftragSicht.kundeSnapshot(customerDoc))
|
||||||
|
{ name, firma, strasse, plz, ort, land, email, kundenart /* ''|'unternehmen'|'privat' */, kundennummer /* customers.code || '' */ }
|
||||||
|
// AbPosition
|
||||||
|
{ beschreibung, menge, einzelpreisCents }
|
||||||
|
// ProjektRechnung (projektRechnungenStore)
|
||||||
|
{ invoiceId, kundenId, projektId, auftragId, art /* 'festbetrag'|'aufwand'|'anzahlung'|'schluss' */, betragCents,
|
||||||
|
arbeitsblattIds /* string[] */, rechnungsnummer, aktiv /* boolean */, erstelltVon, erstelltAm }
|
||||||
|
// Arbeitsblatt (arbeitsblaetter.js; normiert)
|
||||||
|
{ id, ticketId /* worksheets.workorderId */, woid, datum /* 'YYYY-MM-DD' */, minuten /* auf 15 aufgerundet */,
|
||||||
|
text /* kundentauglich, ≤ 200 */, aboZeit /* countsToPlan === true */, kommentar /* isComment === true */ }
|
||||||
|
// Position (Rechnungszeile; positionen.js)
|
||||||
|
{ schluessel /* z. B. '2026-10-06', 'KW-2026-41', 'ab-1', 'zusatz-1', 'abzug-in_123' */, beschreibung,
|
||||||
|
menge, einzelpreisCents, betragCents /* menge × einzelpreis; Minuszeile: menge 1, beide negativ */ }
|
||||||
|
// Projektkonto (projektkonto.js)
|
||||||
|
{ projektId, name, projektTyp, tickets /* [{ id, woid }] */, zeitquelle /* string[] */,
|
||||||
|
vereinbart: { festpreisCents /* number|null */, aufwand /* { stundensatzCents, schaetzungMinuten }|null */ },
|
||||||
|
berechnetCents, bezahltCents, offenCents /* number|null */,
|
||||||
|
zeit: { minuten, aboMinuten, offeneMinuten, satzCents, wertCents, offenerWertCents } }
|
||||||
|
// AuftragTeam (auftragSicht.auftragFuerTeam): Auftrag ohne tokenHash, pdfDatei, bestaetigtPdfSha256, plus
|
||||||
|
{ pdfVorhanden /* boolean */, fruehesterBeginn /* 'YYYY-MM-DD'|null */, zahlungsplanText, aufwandText /* string|null */ }
|
||||||
|
// AuftragOeffentlich (auftragSicht.oeffentlicheAnsicht) – nur für Token- und Kundenrouten
|
||||||
|
{ nummer, datum /* Berliner Tag von gesendetAm */, titel, art,
|
||||||
|
positionen /* [{ beschreibung, menge, einzelpreisCents, summeCents }] */, summeCents, stundensatzCents,
|
||||||
|
schaetzungMinuten, aufwandText, zahlungsplanText, startAm, fertigBis, kundenart, status, bestaetigtAm,
|
||||||
|
fruehbeginn, fruehesterBeginn, agbUrl /* '/rechtliches/agb-<auftrag.agbFassung || AGB_FASSUNG>.pdf' */,
|
||||||
|
texte: { bestaetigung /* BESTAETIGUNG_TEXT */, fruehbeginn /* FRUEHBEGINN_TEXT oder null bei unternehmen */,
|
||||||
|
kleinunternehmer /* AB_TEXTE.kleinunternehmer */ } }
|
||||||
|
// Kundenrouten ergänzen id und kannBestaetigen = (status === 'versendet' || (status === 'abgerechnet' && !bestaetigtAm)) && nicht Kundenansicht
|
||||||
|
```
|
||||||
|
|
||||||
|
Regeln, die mehrere Stränge brauchen (alle in L0, `auftragSicht.js`):
|
||||||
|
- `fruehesterBeginn(auftrag)`: nur bei `kunde.kundenart === 'privat'`, `!fruehbeginn` und gesetztem `gesendetAm`: Tag nach `withdrawalDeadline(new Date(gesendetAm)).lastDay` (aus `server/services/deadlines.js`); sonst `null`.
|
||||||
|
- `zahlungsplanText(auftrag)` und `aufwandText(auftrag)`: aus den Vorlagen in `texte.js` (V7), Beträge mit `euroDe`, bei `anzahlung` Anzahlung = `Math.round(summeCents * prozent / 100)`, Rest = `summeCents − Anzahlung`.
|
||||||
|
|
||||||
|
### V4 Module und Funktionen
|
||||||
|
|
||||||
|
**L0 – Fundament**
|
||||||
|
- `schalter.js`: `auftraegeAktiv(kunde) → boolean` (`kunde`: Kunden-Dokument oder `{ email }`; `on` → true, `pilot` → E-Mail klein geschrieben in `config.auftraege.pilotEmails` oder Liste enthält `*`, sonst false).
|
||||||
|
- `auftragStore.js`: `docToAuftrag(doc)`, `auftragToDoc(auftrag)` (nur Felder aus V1, JSON-Felder serialisiert), `getAuftrag(id) → Auftrag|null`, `listAuftraege({ kundenId, projektId, jahr } = {}) → Auftrag[]` (neueste zuerst), `createAuftrag(daten) → Auftrag` (setzt `erstelltAm`, `geaendertAm`, Status `entwurf`), `updateAuftrag(id, patch) → Auftrag` (setzt `geaendertAm`), `deleteAuftrag(id)`, `findAuftragByTokenHash(hash) → Auftrag|null`, `findAuftragByNummer(nummer) → Auftrag|null`.
|
||||||
|
- `projektRechnungenStore.js`: `getProjektRechnung(invoiceId) → ProjektRechnung|null`, `listProjektRechnungen({ kundenId, projektId, auftragId, nurAktiv = true } = {})`, `createProjektRechnung(invoiceId, daten) → ProjektRechnung`, `updateProjektRechnung(invoiceId, patch) → ProjektRechnung`, `umhaengen(auftragIdAlt, auftragIdNeu) → number` (Anzahl umgehängter Dokumente).
|
||||||
|
- `auftragSicht.js`: `kundeSnapshot(customerDoc)`, `fruehesterBeginn(auftrag)`, `zahlungsplanText(auftrag)`, `aufwandText(auftrag)`, `auftragFuerTeam(auftrag)`, `oeffentlicheAnsicht(auftrag)`.
|
||||||
|
- `arbeitsblaetter.js`: `ladeArbeitsblaetter(ticketIds) → Arbeitsblatt[]` (sortiert nach `datum`, dann `id`; Datum wie `parseSheetDate` in `hourLedger.js`: `TT.MM.JJJJ`, sonst ISO, sonst `$createdAt`, als Berliner Tag; Text gefiltert wie `customerWorkEntries` in `customerProjectView.js`), `ladeArbeitsblaetterNachIds(ids) → Arbeitsblatt[]`, `abrechenbar(blatt) → boolean` (`!aboZeit && !kommentar && minuten > 0`), `abgerechneteArbeitsblaetter(kundenId) → Map<worksheetId, { invoiceId, rechnungsnummer }>` (aus aktiven `projektRechnungen`), `offeneArbeitsblaetter(ticketIds, kundenId) → Arbeitsblatt[]` (abrechenbar und nicht abgerechnet), `zeitquelleFuerProjekt(projekt, auftraege) → string[]` (Vereinigung der `zeitquelle` aller ABs mit Status aus `VEREINBARTE_STATUS`, sonst `[projekt.ticketId]` ohne Leereinträge).
|
||||||
|
- `ablage.js`: `speicherePdf(kundenId, auftragId, buffer) → { pdfDatei: '<kundenId>/<auftragId>.pdf', pdfSha256 }` (unter `config.auftraege.uploadDir`, Verzeichnis 0700, Datei 0600, überschreibt nie: vorhandene Datei → Fehler), `lesePdf(pdfDatei) → Buffer` (nur `^[A-Za-z0-9_-]{1,64}/[A-Za-z0-9_-]{1,64}\.pdf$`, sonst `AuftragError(404, 'PDF nicht gefunden.')`).
|
||||||
|
- `settings.js` (Finanzen): Schlüssel `steuernummer` (Vorgabe `''`, getrimmt, höchstens 40 Zeichen, erlaubt Buchstaben, Ziffern, Leerzeichen, `/`, `-` und `.` (auch eine USt-IdNr. „DE…“); `''` löscht; sonst `SettingsError(400, 'Steuernummer ungültig.')`).
|
||||||
|
|
||||||
|
**L1 – Projektkonto**: `projektkonto.js`: `projektkontenFuerKunde(kundenId) → { auftraegeAktiv, kunde: { id, name, kundenart }, konten: Projektkonto[], ohneProjekt: { berechnetCents, bezahltCents, anzahl }, stripeOk }`; `projektkontoDetail(projektId) → { konto: Projektkonto, auftraege: AuftragTeam[], rechnungen: RechnungKurz[], offeneArbeitsblaetter: Arbeitsblatt[], stripeOk }` mit `RechnungKurz = { id, number, status, createdDate, totalCents, paidCents, art, auftragId }`. Rechenregeln: Spec Abschnitt 4; `berechnetCents`/`bezahltCents` nur aus finalisierten Rechnungen (Status `offen`, `ueberfaellig`, `bezahlt`, `uneinbringlich` laut `listInvoices()`) aktiver `projektRechnungen`; `offenCents = festpreisCents === null ? null : max(0, festpreisCents − Σ total der finalisierten Rechnungen mit auftragId einer Festpreis-AB)`. Scheitert Stripe: Beträge 0, `stripeOk: false`.
|
||||||
|
|
||||||
|
**L2 – Rechnungen**: `positionen.js` (rein): `festePositionen(items) → Position[]` (Eingabe `{ description, quantity, unitAmountCents }` wie heute), `aufwandPositionen(blaetter, { satzCents, zeilentexte = {}, maxZeilen = 20 }) → { positionen, minuten, von, bis }` (eine Zeile je Tag, Text „06.10.2026: <Texte des Tages, mit '; ' verbunden, gekürzt> – 2,25 Std. à 100,00 €“, ohne Text „Arbeitszeit“; zu viele Zeilen → älteste Tage zu ISO-Kalenderwochen „KW 41/2026: …“, Schlüssel `KW-2026-41`; `zeilentexte[schluessel]` ersetzt den Text vor dem Gedankenstrich), `anzahlungPosition(auftrag, { prozent, betragCents, bezeichnung = 'Anzahlung' }, bereitsBerechnetCents) → Position`, `schlussPositionen(auftrag, fruehere, zusatz) → Position[]` (`fruehere: [{ invoiceId, rechnungsnummer, datum, betragCents, art }]`; Minuszeile „abzüglich Anzahlung, Rechnung <Nr> vom <TT.MM.JJJJ>“ bzw. „abzüglich Teilrechnung …“), `stundenText(minuten) → '2,25'`, `leistungszeitraumText(von, bis) → '06.10.2026' | '01.10.–20.10.2026' | '28.12.2026–05.01.2027'`.
|
||||||
|
`rechnungsbau.js`: `baueRechnung(eingabe) → RechnungsEntwurf` mit `eingabe = { customerId, art = 'frei', projektId, auftragId, items, arbeitsblattIds, anzahlung, zeilentexte, leistungszeitraum }` und `RechnungsEntwurf = { kunde, art, projekt /* { id, name, ticketId }|null */, auftrag /* Auftrag|null */, positionen, summeCents, leistungszeitraum, customFields /* [{ name, value }] ≤ 4 */, metadata, projektRechnung /* Daten für createProjektRechnung ohne invoiceId|null */ }`. Zusatzfelder: `Projekt` (Name, auf 140 gekürzt; nicht bei `frei`), `Auftrag` („AB-2026-0002 vom 06.10.2026“, nur mit AB), `Leistungszeitraum` (wenn nicht leer). Metadaten zusätzlich zu den heutigen: `projektId`, `projektName`, `auftragId`, `auftragNummer`, `art`.
|
||||||
|
`stripeFinance.js`: `createProjectInvoice({ customerId, art = 'frei', projektId, auftragId, items, arbeitsblattIds, anzahlung, zeilentexte, leistungszeitraum, daysUntilDue, memo, senden, idempotencyKey, createdBy })` (Positionen über `baueRechnung`; positive Zeile → `quantity` + `unit_amount_decimal`, Minuszeile → `amount`; `custom_fields`; `rendering: { pdf: { page_size: 'a4' } }` für **alle** Rechnungen; `createProjektRechnung` nach dem Entwurf bei `art !== 'frei'`; Aufwand, Anzahlung und Schluss unter `withLocks(sperreRechnung(customerId))`), neue Funktion `rechnungsVorschau(eingabe) → { positionen, summeCents, leistungszeitraum, customFields }` (ohne Stripe-Schreibzugriff), `mapInvoice` liefert zusätzlich `projektId`, `projektName`, `art` (`metadata.art`, sonst `'abo'` bei Abo-Rechnungen bzw. `'frei'`), `auftragId`, `auftragNummer`; Finalisieren (`senden` oder `sendInvoiceNow`) setzt `projektRechnungen.rechnungsnummer` und bei `schluss` die AB auf `abgerechnet`; `voidInvoice`/`deleteDraft` setzen `aktiv: false` und nehmen `abgerechnet` zurück.
|
||||||
|
|
||||||
|
**L3 – AB-Kern**: `nummern.js`: `formatNummer(jahr, laufnummer) → 'AB-2026-0001'`, `vergibNummer(jetzt = new Date()) → { nummer, jahr, laufnummer }` (größte `laufnummer` des Berliner Jahres + 1; Aufrufer hält `SPERRE_NUMMER`). `auftraege.js`: `pruefeEingabe(eingabe, { teilweise = false }) → bereinigte Felder` (AuftragError 400 mit Feldmeldung), `legeEntwurfAn(eingabe, { von })`, `aendereEntwurf(id, eingabe)`, `loescheEntwurf(id)`, `vorschauPdf(id, { deps }) → Buffer`, `sendeAuftrag(id, { von, deps }) → { auftrag, mailFehler: boolean }`, `erinnereAuftrag(id, { deps }) → { auftrag, mailFehler }`, `neueFassung(id, { von }) → Auftrag` (neuer Entwurf mit `ersetztId`), `storniereAuftrag(id) → Auftrag`. `deps = { erzeugeAuftragPdf, neuerToken, sendeAuftragMail }`; Vorgabe per **dynamischem Import** von `./pdf.js` (L4), `./token.js` (L5), `./mails.js` (L5) beim Aufruf, damit L3 ohne L4/L5 testbar ist (Tests übergeben Attrappen). Reihenfolge beim Senden: Eingabe, Kunde (fester Kunde, E-Mail, Anschrift, Kundenart) prüfen → Sperre `SPERRE_NUMMER` → Nummer → PDF erzeugen → `speicherePdf` → Token → Dokument speichern (Status `versendet`) → Sperre frei → Mail → `mailStatus`. Hat die AB `ersetztId`: alte AB `ersetzt` (+ `ersetztDurchId`), `umhaengen(alt, neu)`.
|
||||||
|
|
||||||
|
**L4 – AB-PDF**: `pdf.js`: `auftragPdfDaten(auftrag, { settings, vorschau = false, jetzt, ersetzt }) → AuftragPdfDaten`, `erzeugeAuftragPdf(auftrag, { settings, vorschau = false, jetzt, ersetzt }) → Promise<Buffer>` (`ersetzt = { nummer, datum }` optional, sonst über `getAuftrag(auftrag.ersetztId)`; `AB_TEXTE.termine` bekommt `privat`). `auftrag.kunde` ist immer gesetzt (L3 übergibt bei der Vorschau `kundeSnapshot(liveKunde)` und `nummer: 'Entwurf'`). `settings = { steuernummer, bankDetailsText }`. Inhalt laut Spec Abschnitt 2 „Inhalt des PDFs“, Texte aus `texte.js`. Schrift: statische Figtree-Schnitte (Regular, SemiBold) als Dateien unter `server/assets/fonts/` (OFL, Lizenzdatei dazu); Logo aus `public/` (vorhandene Datei, die die Mailvorlagen nutzen).
|
||||||
|
|
||||||
|
**L5 – Bestätigung und Mails**: `token.js`: `neuerToken(jetzt = new Date()) → { token, hash, gueltigBis /* Date */ }`, `hashToken(token) → string|null` (null bei falschem Format). `bestaetigung.js`: `bestaetigeAuftrag(auftrag, { ueber, fruehbeginn, jetzt = new Date() }) → Auftrag` (Regeln V2; idempotent; `fruehbeginn` nur bei `privat`; schickt danach `sendeBestaetigtMails`, Fehler nur ins Protokoll). `mails.js`: `sendeAuftragMail(auftrag, { pdf, token, erinnerung = false }) → Promise<void>` (wirft bei Fehler), `sendeBestaetigtMails(auftrag) → Promise<void>`. Widerruf in `server/routes/abo.js`: `order = 'projekt'` mit Feld `abNummer`.
|
||||||
|
|
||||||
|
**L6 – Kundenportal**: nur Oberfläche und `customerInvoices.js` (Feld `projekt: raw.metadata?.projektName || ''`).
|
||||||
|
|
||||||
|
**L7 – Rechtstexte**: Wortlaute in `texte.js`, neue Rechtstext-Dateien, Datenschutz, Druckskripte (Spec Abschnitt 8) und das Freigabe-Dokument `/root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde.md`.
|
||||||
|
|
||||||
|
### V5 HTTP-Endpunkte
|
||||||
|
|
||||||
|
Antwortformat bei Fehlern immer `{ error: '<Meldung>' }`. Admin-Routen: Router mit `requireAppwriteStaff` (wie `finanzen.js`); Team-Routen: unter `/api/team` (dort gilt `requireStaffJwt`); Kundenrouten: `requireSession` und eigener Kunde; Token-Routen: ohne Anmeldung, Ratenbegrenzung im Arbeitsspeicher je IP (GET 120/h, POST 10/h → 429 `{ error: 'Zu viele Anfragen. Bitte später erneut versuchen.' }`).
|
||||||
|
|
||||||
|
| Strang | Methode, Pfad | Eingabe | Antwort (200) | Fehler |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| L1 | GET `/api/admin/finanzen/projektkonten?kunde=<id>` | – | `{ auftraegeAktiv, kunde, konten, ohneProjekt, stripeOk }` | 400 ohne Kunde, 404 Kunde unbekannt |
|
||||||
|
| L1 | GET `/api/admin/finanzen/projektkonten/:projektId` | – | `{ konto, auftraege, rechnungen, offeneArbeitsblaetter, stripeOk }` | 404 |
|
||||||
|
| L1 | GET `/api/team/arbeitsblaetter/abgerechnet?ticketId=<id>` | – | `{ abgerechnet: { <worksheetId>: '<Rechnungsnummer>' \| 'Entwurf' } }` (unbekanntes Ticket: `{ abgerechnet: {} }`) | 400 ohne ticketId |
|
||||||
|
| L2 | POST `/api/admin/finanzen/rechnungen` | `{ customerId, art, projektId, auftragId, items, arbeitsblattIds, anzahlung: { prozent, betragCents, bezeichnung }, zeilentexte, leistungszeitraum, daysUntilDue, memo, senden, idempotencyKey }` | `{ rechnung }` (wie heute, `mapInvoice` erweitert) | 400 Eingabe, 404, 409 (Arbeitsblatt bereits abgerechnet, offener Entwurf zur AB, Nichts mehr offen, Adresse), 502 |
|
||||||
|
| L2 | POST `/api/admin/finanzen/rechnungen/vorschau` | wie oben ohne `senden`, `idempotencyKey`, `daysUntilDue`, `memo` | `{ positionen, summeCents, leistungszeitraum, customFields }` | wie oben |
|
||||||
|
| L2 | GET `/api/admin/finanzen/rechnungen` | wie heute | Einträge zusätzlich mit `projektId`, `projektName`, `art`, `auftragId`, `auftragNummer` | – |
|
||||||
|
| L3 | GET `/api/admin/finanzen/auftraege?kunde=<id>` | – | `{ auftraege: AuftragTeam[] }` | 400, 404 Schalter aus |
|
||||||
|
| L3 | POST `/api/admin/finanzen/auftraege` | `{ kundenId, projektId, titel, kundenart, art, positionen, stundensatzCents, schaetzungMinuten, zahlungsplan, zahlungszielTage, auftragVom, auftragWeg, startAm, fertigBis, zeitquelle }` (`kundenart` setzt `customers.kundenart`, wenn dort leer) | `{ auftrag: AuftragTeam }` | 400, 404 |
|
||||||
|
| L3 | GET / PATCH / DELETE `/api/admin/finanzen/auftraege/:id` | PATCH wie POST (teilweise) | `{ auftrag }` bzw. `{ ok: true }` | 404, 409 kein Entwurf |
|
||||||
|
| L3 | GET `/api/admin/finanzen/auftraege/:id/pdf` | – | `application/pdf` (gesendet: gespeicherte Datei; Entwurf: Vorschau mit „Entwurf“) | 404, 409 Entwurf unvollständig |
|
||||||
|
| L3 | POST `/api/admin/finanzen/auftraege/:id/projekt` | `{ projektId }` | `{ auftrag }` (setzt projektId und, wenn leer, zeitquelle = [Ticket des Projekts]; PDF unverändert) | 400, 404, 409 (hat schon ein Projekt; Status storniert/ersetzt/widerrufen) |
|
||||||
|
| L3 | POST `/api/admin/finanzen/auftraege/:id/senden` | – | `{ auftrag, mailFehler }` | 400 Kundenart/E-Mail/Interessent, 409 Adresse, kein Entwurf, Entwurf unvollständig; 500 PDF |
|
||||||
|
| L3 | POST `…/auftraege/:id/erinnern` | – | `{ auftrag, mailFehler }` | 409 nicht `versendet` |
|
||||||
|
| L3 | POST `…/auftraege/:id/neue-fassung` | – | `{ auftrag }` (neuer Entwurf) | 409 |
|
||||||
|
| L3 | POST `…/auftraege/:id/stornieren` | – | `{ auftrag }` | 409 |
|
||||||
|
| L5 | GET `/api/auftrag/:token` | – | `{ auftrag: AuftragOeffentlich }` | 404 `'Dieser Link ist ungültig oder abgelaufen.'`; 410 `'Diese Auftragsbestätigung wurde ersetzt.'` / `'… wurde storniert.'` / `'… wurde widerrufen.'`; 429 |
|
||||||
|
| L5 | GET `/api/auftrag/:token/pdf` | – | `application/pdf` | wie oben |
|
||||||
|
| L5 | POST `/api/auftrag/:token/bestaetigen` | `{ bestaetigt: true, fruehbeginn: boolean }` | `{ auftrag: AuftragOeffentlich }` | 400 „Bitte bestätigen Sie den Auftrag mit dem Häkchen.“, 404/410, 429 |
|
||||||
|
| L5 | GET `/api/auftraege` (Kunde) | – | `{ aktiv, auftraege: (AuftragOeffentlich & { id, kannBestaetigen })[] }` (keine Entwürfe) | 401 |
|
||||||
|
| L5 | GET `/api/auftraege/:id/pdf` (Kunde) | – | `application/pdf` | 404 fremd/unbekannt |
|
||||||
|
| L5 | POST `/api/auftraege/:id/bestaetigen` (Kunde) | `{ bestaetigt: true, fruehbeginn }` | `{ auftrag }` | 400 (Text wie Token-Route), 404, 410 (Meldungen wie Token-Route); in der Kundenansicht sperrt der Wächter (kein Eintrag in `KUNDENANSICHT_ALLOWED`) |
|
||||||
|
| L5 | POST `/api/abo/public/withdraw` | zusätzlich `order: 'projekt'`, `abNummer` (optional; ohne/ungültig nur Mails mit Vermerk „ungeprüft“) | wie heute | wie heute; gilt unabhängig vom Schalter |
|
||||||
|
| L0 | GET/PATCH `/api/admin/finanzen/einstellungen` | zusätzlich `steuernummer` | wie heute | 400 |
|
||||||
|
|
||||||
|
Ist der Schalter für den Kunden aus: Admin-AB-Routen 404 mit `SCHALTER_AUS_MELDUNG`; Rechnungsarten `anzahlung`/`schluss` 400 mit derselben Meldung; Token- und Kundenrouten antworten wie bei unbekannter AB (404) bzw. `GET /api/auftraege` mit `{ aktiv: false, auftraege: [] }`.
|
||||||
|
|
||||||
|
Einbindung (L0): in `server/index.js` **vor** `app.use('/api/admin/finanzen', adminFinanzenRoutes)`: `app.use('/api/admin/finanzen/projektkonten', adminProjektkontenRoutes)`, `app.use('/api/admin/finanzen/auftraege', adminAuftraegeRoutes)`; außerdem `app.use('/api/auftrag', publicAuftragRoutes)` und `app.use('/api/auftraege', kundeAuftraegeRoutes)`; in `server/routes/team/index.js` nach `requireStaffJwt`: `router.use('/arbeitsblaetter', arbeitsblaetterTeamRoutes)`.
|
||||||
|
|
||||||
|
### V6 Mails (L5)
|
||||||
|
|
||||||
|
Vorlagen in `server/mail-templates/` mit Kopfkommentar wie die vorhandenen (`renderMail`):
|
||||||
|
- `K31 Auftragsbestaetigung.html` – Betreff „Ihre Auftragsbestätigung {{nummer}} – {{titel}}“; Platzhalter `name, nummer, titel, datum, betrag, link, gueltig_bis`; Blöcke `erinnerung, privat, festpreis, aufwand, teamkopie`. Anhänge: AB-PDF (`<nummer>.pdf`) und AGB-PDF (`public/rechtliches/agb-<AGB_FASSUNG>.pdf`, Dateiname `WEBklar-AGB-<AGB_FASSUNG>.pdf`). **Kein** BCC an das Team (`bccTeam: false`), weil die Mail den Bestätigungslink trägt; das Team bekommt eine Kopie mit Block `teamkopie` an `CHAT_NOTIFY_EMAILS` (statt Knopf: „Kopie für das Team – der Bestätigungslink steht nur in der Mail an den Kunden.“), auch bei Erinnerungen. Anhang-Fassung: `auftrag.agbFassung` (Rückfall `AGB_FASSUNG`); fehlt die Datei, wirft der Versand (→ `mailStatus: 'fehler'`).
|
||||||
|
- `K32 Auftrag bestaetigt.html` – Betreff „Ihre Bestätigung zu {{nummer}} ist eingegangen“; Platzhalter `name, nummer, titel, zeitpunkt, fruehester_beginn, fruehbeginn_text`; Blöcke `fruehbeginn` (gibt `FRUEHBEGINN_TEXT` wörtlich wieder; kein Satz „Ihr Widerrufsrecht bleibt unberührt“), `ohne_fruehbeginn`.
|
||||||
|
- `T22 Auftrag bestaetigt.html` – Betreff „[Aufträge] Auftrag {{nummer}} bestätigt: {{kunde}}“; Platzhalter `kunde, nummer, titel, zeitpunkt, ueber, woms_link, fruehester_beginn`; Blöcke `privat_fruehbeginn, privat_ohne_fruehbeginn`. `woms_link = <womsBaseUrl>/customers/<kundenId>?tab=invoices`.
|
||||||
|
- Link der K31: `<config.publicBaseUrl>/auftrag.html#t=<token>` (Fragment: landet nicht in Server- und Proxy-Protokollen; die Seite liest `#t=` und als Rückfall `?t=`).
|
||||||
|
- `K04 Eingang Widerruf.html` und `T09 Widerruf eingegangen.html` bekommen je einen Block `projekt` (Vertragsart „Projektauftrag (Auftragsbestätigung <Nr>)“; bei Unternehmen der Hinweis „kein gesetzliches Widerrufsrecht“ im K04-Block).
|
||||||
|
|
||||||
|
### V7 Texte (`server/services/auftraege/texte.js`; L0 legt an, L7 formuliert)
|
||||||
|
|
||||||
|
Exportnamen und Formen (verbindlich):
|
||||||
|
|
||||||
|
```js
|
||||||
|
export const TEXTE_FASSUNG = '2026-10-06'
|
||||||
|
export const ABSENDER = { name: 'WEBklar GbR', strasse: 'Am Schwimmbad 10', plzOrt: '67722 Winnweiler',
|
||||||
|
telefon: '0176 23726355', email: 'support@webklar.com', web: 'webklar.com', vertretenDurch: 'Kenso Grimm und Justin Klein' }
|
||||||
|
export const AB_TEXTE = {
|
||||||
|
titel: 'Auftragsbestätigung',
|
||||||
|
einleitung: ({ datum, weg }) => string, // „Vielen Dank für Ihren Auftrag vom 05.10.2026 (telefonisch). Wir bestätigen ihn wie folgt:“
|
||||||
|
aufwand: ({ satz, schaetzung /* '10' Stunden oder null */ }) => string,
|
||||||
|
kleinunternehmer: 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.',
|
||||||
|
zahlungsplan: {
|
||||||
|
nach_abnahme: ({ ziel }) => string, anzahlung: ({ prozent, anzahlung, rest, ziel }) => string,
|
||||||
|
monatlich: ({ ziel }) => string, frei: ({ text, ziel }) => string,
|
||||||
|
},
|
||||||
|
termine: ({ start /* 'TT.MM.JJJJ'|null */, fertig /* 'TT.MM.JJJJ'|null */, privat /* boolean */ }) => string|null, // privat: „Ohne Ihr ausdrückliches Verlangen beginnen wir erst nach Ablauf der Widerrufsfrist.“
|
||||||
|
mitwirkung: string, // Verweis auf AGB Nr. 7 und 8
|
||||||
|
agb: ({ fassung /* 'TT.MM.JJJJ' */ }) => string, // „Es gelten unsere Allgemeinen Geschäftsbedingungen in der Fassung vom …, die dieser Bestätigung beiliegen.“
|
||||||
|
ersetzt: ({ nummer, datum }) => string, // „Diese Auftragsbestätigung ersetzt AB-… vom ….“
|
||||||
|
gruss: string,
|
||||||
|
}
|
||||||
|
export const BESTAETIGUNG_TEXT = 'Ich bestätige den Auftrag zu diesen Bedingungen.'
|
||||||
|
export const FRUEHBEGINN_FASSUNG = '2026-10-06'
|
||||||
|
export const FRUEHBEGINN_TEXT = string
|
||||||
|
export const WIDERRUFSBELEHRUNG_PROJEKT = { titel: 'Widerrufsbelehrung', abschnitte: [{ ueberschrift: string, absaetze: string[] }] }
|
||||||
|
export const WIDERRUFSFORMULAR = { titel: 'Muster-Widerrufsformular', einleitung: string, zeilen: string[] }
|
||||||
|
```
|
||||||
|
|
||||||
|
### V8 WOMS-Schnittstellen (W0/W1/W2)
|
||||||
|
|
||||||
|
- `src/lib/financeApi.js` (W0 ergänzt): `getProjektkonten(kundeId)`, `getProjektkonto(projektId)`, `previewInvoice(body)`, `createInvoice(body)` (besteht, Körper erweitert), `listAuftraege(kundeId)`, `getAuftrag(id)`, `createAuftrag(body)`, `patchAuftrag(id, body)`, `deleteAuftrag(id)`, `sendAuftrag(id)`, `erinnereAuftrag(id)`, `neueFassungAuftrag(id)`, `storniereAuftrag(id)`, `getAuftragPdf(id) → Blob`.
|
||||||
|
- `src/lib/teamApi.js` (W0 ergänzt): `getAbgerechnet(ticketId) → { abgerechnet }`.
|
||||||
|
- `src/components/auftraege/AuftraegeUndRechnungen.jsx` (W0): Inhalt des Kundenreiters `invoices` (Beschriftung „Aufträge und Rechnungen“), Props `{ customer, onShowOverview }`; lädt `getProjektkonten` und (wenn `auftraegeAktiv`) `listAuftraege`; zeigt `ProjektkontoListe`, `AuftragListe` (nur mit Schalter), Rechnungsliste (bestehende `InvoiceTable`) und öffnet `RechnungDialog` bzw. `AuftragDialog`.
|
||||||
|
- `ProjektkontoListe.jsx` (W1): Props `{ konten, ohneProjekt, auftraegeAktiv, stripeOk, onRechnung(projektId|null), onAuftrag(projektId|null) }`; die Kindkomponenten rendern ihre `Card` selbst.
|
||||||
|
- `RechnungDialog.jsx` (W1): Props `{ open, onClose, onCreated(rechnung, info /* unverändert weiterreichen: senden, notice, tone, finalizedFilter */), kunde /* Kundendokument oder null = Kundenwahl */, projektId /* undefined = keine Vorauswahl, null = „Ohne Projekt“ */, auftraegeAktiv /* nur Startwert; danach gilt getProjektkonten */, onShowOverview /* optional */ }`; lädt `getProjektkonten` selbst; sitzt in `InvoiceTable.jsx` (dort ersetzt W1 das Modal) – `FinancePage.jsx` bleibt unverändert; ersetzt `InvoiceCreateModal` überall (auch `FinancePage`).
|
||||||
|
- `AuftragListe.jsx` (W2): Props `{ auftraege, konten, onChanged(), onBearbeiten(auftrag), onShowOverview }` (W0 reicht `konten` und `onShowOverview` durch); Aktion „Projekt zuordnen“ für ABs ohne Projekt (`ordneAuftragProjektZu(id, projektId)` in financeApi.js).
|
||||||
|
- `AuftragDialog.jsx` (W2): Props `{ open, onClose, onSaved(auftrag), kunde, projektId /* oder null */, auftrag /* Entwurf zum Bearbeiten oder null */, konten /* Projektkonten für die Projektwahl */, onShowOverview }`; Hinweise (nicht blockierend): fehlende Steuernummer im Schritt 4; bei Weg „vor Ort“ und Kundenart privat „zusätzlich ausdrucken und übergeben“ (§ 312f Abs. 1 BGB). PDF öffnen: `openInNewTab(async () => URL.createObjectURL(await getAuftragPdf(id)))`.
|
||||||
|
- Reine Ansichtslogik mit Tests: `src/lib/projektkontoView.js` und `src/lib/rechnungDialogView.js` (W1), `src/lib/auftragView.js` (W2).
|
||||||
|
- Je Strang eigene CSS-Datei: `src/components/auftraege/projektkonto.css`, `rechnung-dialog.css` (W1), `auftrag.css` (W2), Gestaltung nach den Regeln des Redesigns (Komponenten aus `src/components/ui`, `Dialog`, `financeUi.js`).
|
||||||
|
- Weitere Änderungen: W1 `InvoiceTable.jsx` (Spalten Projekt, Art; Modal → `RechnungDialog`; „weitere Tickets“ liest W1 direkt aus Appwrite und schickt `arbeitsblattIds`), `WorksheetList.jsx` (Abzeichen), `WorksheetTimesModal.jsx` (Warnung); W2 `CustomerDetailPage.jsx` (Feld Kundenart in der Übersicht), `SettingsCard.jsx` (Steuernummer); W0 `src/lib/customerTabs.js` (Beschriftung).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration, Kontrolle, Übergabe
|
||||||
|
|
||||||
|
Plan-Datei `2026-10-06-auftraege-12-integration-kontrolle.md`:
|
||||||
|
- **Z1 Portal**: Merge in der Reihenfolge L1, L2, L3, L4, L7 (ohne seinen letzten Commit, den Datenschutz-Abschnitt: `git merge --no-ff auftraege/l7-rechtstexte~1`; der Datenschutz-Commit kommt erst beim AB-Livegang nach der Freigabe), L5, L6 nach `feature/auftraege` (`git merge --no-ff`), nach jedem Merge die Tests des Strangs; dann Durchlauf-Test `test/auftraegeAblauf.test.js` über `startTestApp` (Festbetrag → Projektkonto; Aufwand → abgerechnet → Storno → frei; AB → Senden → Token-Bestätigung → Anzahlung → Schlussrechnung mit Minuszeile → `abgerechnet` → Storno → zurück; Privatperson mit/ohne Frühbeginn; Kundenansicht gesperrt; Schalter aus → 404); volle Suite unter `flock`; Node-20-Lauf der `test/auftraege*.test.js` (`docker run --rm --network host -m 1200m -v <worktree>:<worktree>:ro node:20-alpine`).
|
||||||
|
- **Z2 WOMS**: Merge W1, W2; `npm test`; `vite build`; Bildschirmfotos (Desktop 1440 px, Handy 390 px) aller neuen Ansichten mit nachgebildeten Antworten.
|
||||||
|
- **Z3 Kontrolle**: fünf Prüfer parallel (sonnet) – Geld und Rechenlogik; Sicherheit und Datenschutz (Token, öffentliche Routen, Rechte, Pfade, IDOR, Kundenansicht); Recht und Texte (Spec, § 34a UStDV, § 312f BGB, Widerruf, Frühbeginn, Datenschutz); Oberfläche (Bildschirmfotos); Spec-Abdeckung (jede Zeile der Spec → Code/Test). Jeder Befund wird von einem zweiten Agenten (haiku) gegengeprüft; nur bestätigte Critical/Important werden korrigiert (sonnet), danach betroffene Tests und Z1-Durchlauf erneut.
|
||||||
|
- **Übergabe**: `/root/reports/auftraege-rechnungen-2026-10-06/uebergabe.md` mit Push-Befehlen (Portal `feature/auftraege:main`, WOMS erst nach dem Redesign), Setup-Befehl `docker exec -w /app webklar-kundenbereich-server node scripts/auftraege-setup.mjs`, Compose-Zeilen für `AUFTRAEGE_ENABLED`/`AUFTRAEGE_PILOT_EMAILS` (Vorgabe aus), Steuernummer (Stripe-Dashboard + WOMS-Einstellung), Rechtsrunde (Freigabe-Dokument), Live-Prüfungen.
|
||||||
|
|
||||||
|
## Livegang (Kurzfassung, Details in der Übergabe)
|
||||||
|
|
||||||
|
1. Nutzer: Steuernummer im Stripe-Dashboard hinterlegen.
|
||||||
|
2. Nutzer: Portal `feature/auftraege` → `main` pushen; sofort danach Setup-Skript; Container-Log prüfen. Rechnungsteil (Projektkonto, Festbetrag, Aufwand, A4, Zusatzfelder) ist live, AB-Teil aus.
|
||||||
|
3. Nutzer: WOMS pushen, sobald das Redesign auf `test` ist.
|
||||||
|
4. Rechtsrunde: Nutzer gibt die Texte frei; Claude lädt sie in WOMS → Rechtliches hoch (kurzlebiger Schlüssel).
|
||||||
|
5. `AUFTRAEGE_ENABLED=pilot` mit Testkunde, Probe-AB an den Testkunden, dann `on`.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Aufträge und Rechnungen: Auftragsbestätigung und projektbezogene Rechnungen
|
# Aufträge und Rechnungen: Auftragsbestätigung und projektbezogene Rechnungen
|
||||||
|
|
||||||
Stand: 2026-10-06 · Status: Entwurf zur Freigabe (Abschnitte im Chat einzeln freigegeben)
|
Stand: 2026-10-06 · Status: freigegeben (Abschnitte im Chat einzeln freigegeben; Nachträge aus der Planrunde eingearbeitet, siehe „Nachträge“ am Ende)
|
||||||
|
|
||||||
Grundlage: Wunsch des Nutzers vom 06.10.2026 („Auftragsbestätigung und Rechnungen bei uns, wenn wir auf Kunden gehen;
|
Grundlage: Wunsch des Nutzers vom 06.10.2026 („Auftragsbestätigung und Rechnungen bei uns, wenn wir auf Kunden gehen;
|
||||||
Projekt auswählen, Zusammenfassung sehen, Festbeträge möglich“), Recherche im selben Gespräch (Quellen am Ende),
|
Projekt auswählen, Zusammenfassung sehen, Festbeträge möglich“), Recherche im selben Gespräch (Quellen am Ende),
|
||||||
@@ -117,7 +117,7 @@ Mitarbeiter (ohne Beträge).
|
|||||||
|
|
||||||
### Dialog „Auftragsbestätigung“ (neu)
|
### Dialog „Auftragsbestätigung“ (neu)
|
||||||
|
|
||||||
1. **Projekt** (oder freier Titel, wenn es noch kein Projekt gibt; später zuordenbar) und, falls leer, **Kundenart**.
|
1. **Projekt** (oder freier Titel, wenn es noch kein Projekt gibt; später zuordenbar über „Projekt zuordnen“ in der AB-Liste, ohne das PDF zu ändern) und, falls leer, **Kundenart**.
|
||||||
2. **Auftrag:** Datum der Absprache und Weg (mündlich, telefonisch, per E-Mail, vor Ort, im Chat). Art **Festpreis**
|
2. **Auftrag:** Datum der Absprache und Weg (mündlich, telefonisch, per E-Mail, vor Ort, im Chat). Art **Festpreis**
|
||||||
(Positionen: Beschreibung, Menge, Einzelpreis; Grenzen wie bei Rechnungen) oder **Nach Aufwand** (Stundensatz,
|
(Positionen: Beschreibung, Menge, Einzelpreis; Grenzen wie bei Rechnungen) oder **Nach Aufwand** (Stundensatz,
|
||||||
Standard 100 €; Schätzung in Stunden, optional, in 15-Minuten-Schritten).
|
Standard 100 €; Schätzung in Stunden, optional, in 15-Minuten-Schritten).
|
||||||
@@ -138,8 +138,8 @@ Mitarbeiter (ohne Beträge).
|
|||||||
`storniert` (von Hand, ohne Mail), `ersetzt` (durch eine neue Fassung) und `widerrufen` (Privatperson).
|
`storniert` (von Hand, ohne Mail), `ersetzt` (durch eine neue Fassung) und `widerrufen` (Privatperson).
|
||||||
- **Unveränderlich nach dem Senden.** Änderung = „Neue Fassung“: Kopie als neuer Entwurf mit Vermerk
|
- **Unveränderlich nach dem Senden.** Änderung = „Neue Fassung“: Kopie als neuer Entwurf mit Vermerk
|
||||||
„Ersetzt AB-… vom …“; beim Senden der neuen AB wird die alte `ersetzt`. Rechnungen der alten AB wandern intern mit
|
„Ersetzt AB-… vom …“; beim Senden der neuen AB wird die alte `ersetzt`. Rechnungen der alten AB wandern intern mit
|
||||||
(`projektRechnungen.auftragId` und Stripe-Metadaten auf die neue AB); der auf finalisierten Rechnungen aufgedruckte
|
(`projektRechnungen.auftragId` auf die neue AB); Stripe-Metadaten und der auf finalisierten Rechnungen aufgedruckte
|
||||||
Bezug bleibt unverändert.
|
Bezug bleiben unverändert.
|
||||||
|
|
||||||
### Inhalt des PDFs (A4)
|
### Inhalt des PDFs (A4)
|
||||||
|
|
||||||
@@ -162,8 +162,8 @@ Mitarbeiter (ohne Beträge).
|
|||||||
|
|
||||||
### Erzeugung und Ablage
|
### Erzeugung und Ablage
|
||||||
|
|
||||||
- PDF mit **pdfkit** (reines JavaScript; kein Chrome zur Laufzeit wegen knappem Arbeitsspeicher), Schrift Figtree aus
|
- PDF mit **pdfkit** (reines JavaScript; kein Chrome zur Laufzeit wegen knappem Arbeitsspeicher), statische
|
||||||
`public/fonts`, Logo aus `public/img`.
|
Figtree-Schnitte (OFL) unter `server/assets/fonts/`, Logo `public/webklar-logo.png`.
|
||||||
- Beim Senden einmal erzeugt und unter `upload-data/auftraege/<kundenId>/<abId>.pdf` abgelegt, SHA-256 im Dokument;
|
- Beim Senden einmal erzeugt und unter `upload-data/auftraege/<kundenId>/<abId>.pdf` abgelegt, SHA-256 im Dokument;
|
||||||
danach wird nur noch diese Datei ausgeliefert. Die Datei liegt in der Hetzner-Serversicherung; jede Mail geht zudem
|
danach wird nur noch diese Datei ausgeliefert. Die Datei liegt in der Hetzner-Serversicherung; jede Mail geht zudem
|
||||||
per BCC an das Team-Postfach.
|
per BCC an das Team-Postfach.
|
||||||
@@ -172,9 +172,10 @@ Mitarbeiter (ohne Beträge).
|
|||||||
|
|
||||||
### Versand
|
### Versand
|
||||||
|
|
||||||
- Mail **K31 „Auftragsbestätigung“** (Kunde, Design der Mailvorlagen, BCC Team): kurzer Text, Anhänge AB-PDF und
|
- Mail **K31 „Auftragsbestätigung“** (Kunde, Design der Mailvorlagen, **ohne** BCC ans Team, weil sie den
|
||||||
AGB-PDF, Knopf **„Auftrag bestätigen“** → `https://project.webklar.com/auftrag.html?t=<token>`; bei Privatpersonen
|
Bestätigungslink trägt): kurzer Text, Anhänge AB-PDF und AGB-PDF, Knopf **„Auftrag bestätigen“** →
|
||||||
Hinweis auf Widerrufsbelehrung und Frühbeginn.
|
`https://project.webklar.com/auftrag.html#t=<token>` (Fragment: landet nicht in Server-Protokollen); bei
|
||||||
|
Privatpersonen Hinweis auf Widerrufsbelehrung und Frühbeginn. Das Team bekommt eine Kopie **ohne** Link.
|
||||||
- Token: 32 Zufallsbytes (base64url), gespeichert nur als SHA-256, gültig 60 Tage ab dem letzten Versand.
|
- Token: 32 Zufallsbytes (base64url), gespeichert nur als SHA-256, gültig 60 Tage ab dem letzten Versand.
|
||||||
**„Erinnerung senden“** (WOMS, von Hand) schickt K31 erneut mit Erinnerungs-Hinweis und neuem Token (der alte gilt
|
**„Erinnerung senden“** (WOMS, von Hand) schickt K31 erneut mit Erinnerungs-Hinweis und neuem Token (der alte gilt
|
||||||
nicht mehr).
|
nicht mehr).
|
||||||
@@ -182,7 +183,7 @@ Mitarbeiter (ohne Beträge).
|
|||||||
|
|
||||||
### Bestätigung
|
### Bestätigung
|
||||||
|
|
||||||
- **Öffentliche Seite** `auftrag.html?t=…` (ohne Anmeldung, `noindex`, Ratenbegrenzung je IP im Arbeitsspeicher wie
|
- **Öffentliche Seite** `auftrag.html#t=…` (ohne Anmeldung, `noindex`, Ratenbegrenzung je IP im Arbeitsspeicher wie
|
||||||
bei der Widerrufsseite): Zusammenfassung (Nummer, Datum, Projekt, Positionen bzw. Aufwandsregel, Summe,
|
bei der Widerrufsseite): Zusammenfassung (Nummer, Datum, Projekt, Positionen bzw. Aufwandsregel, Summe,
|
||||||
Zahlungsplan, Termine), Links auf AB-PDF und AGB-PDF, Häkchen „Ich bestätige den Auftrag zu diesen Bedingungen.“,
|
Zahlungsplan, Termine), Links auf AB-PDF und AGB-PDF, Häkchen „Ich bestätige den Auftrag zu diesen Bedingungen.“,
|
||||||
Knopf „Auftrag bestätigen“. Nach der Bestätigung zeigt die Seite „Bestätigt am …“. Ungültiger oder abgelaufener Link:
|
Knopf „Auftrag bestätigen“. Nach der Bestätigung zeigt die Seite „Bestätigt am …“. Ungültiger oder abgelaufener Link:
|
||||||
@@ -196,13 +197,13 @@ Mitarbeiter (ohne Beträge).
|
|||||||
- **Protokoll am AB:** `bestaetigtAm`, `bestaetigtUeber` (`link` | `portal`), `bestaetigtPdfSha256` (muss
|
- **Protokoll am AB:** `bestaetigtAm`, `bestaetigtUeber` (`link` | `portal`), `bestaetigtPdfSha256` (muss
|
||||||
`pdfSha256` entsprechen), `texteFassung`, `fruehbeginn` (ja/nein) und dessen Textfassung. **Keine IP-Adresse.**
|
`pdfSha256` entsprechen), `texteFassung`, `fruehbeginn` (ja/nein) und dessen Textfassung. **Keine IP-Adresse.**
|
||||||
Eine zweite Bestätigung ändert nichts (die erste zählt).
|
Eine zweite Bestätigung ändert nichts (die erste zählt).
|
||||||
- Danach: Mail **K32 „Auftrag bestätigt“** an den Kunden (Eingangsbestätigung mit Datum, Uhrzeit, Frühbeginn ja/nein)
|
- Danach: Mail **K32 „Auftrag bestätigt“** an den Kunden (Eingangsbestätigung mit Datum, Uhrzeit, Frühbeginn ja/nein;
|
||||||
und **T22 „Auftrag bestätigt“** an das Team.
|
eine Frühbeginn-Erklärung wird wörtlich wiedergegeben) und **T22 „Auftrag bestätigt“** an das Team.
|
||||||
|
|
||||||
### Widerruf (Privatperson)
|
### Widerruf (Privatperson)
|
||||||
|
|
||||||
- `widerrufen.html` bekommt die Vertragsart **„Projektauftrag (Auftragsbestätigung)“** mit Feld AB-Nummer. Die
|
- `widerrufen.html` bekommt die Vertragsart **„Projektauftrag (Auftragsbestätigung)“** mit Feld AB-Nummer (optional,
|
||||||
Erklärung läuft über den bestehenden Weg (Eingangsbestätigung an den Kunden, Team-Mail); passt die AB zu Nummer und
|
„falls zur Hand“; der Widerruf ist formfrei). Die Erklärung läuft über den bestehenden Weg (Eingangsbestätigung an den Kunden, Team-Mail); passt die AB zu Nummer und
|
||||||
E-Mail-Adresse, wird sie `widerrufen` und WOMS zeigt es. Erstattung und Wertersatz bleiben Handarbeit.
|
E-Mail-Adresse, wird sie `widerrufen` und WOMS zeigt es. Erstattung und Wertersatz bleiben Handarbeit.
|
||||||
|
|
||||||
## 3. Rechnungen
|
## 3. Rechnungen
|
||||||
@@ -220,7 +221,8 @@ Karte/SEPA, Bankabgleich und Finanzen-Übersicht unverändert).
|
|||||||
| `festbetrag` | Projekt | Freie Positionen wie heute (Frau Wolf: „Erstellung der Website anelia-wolf.com, Pauschalpreis laut Absprache“, 500 €). |
|
| `festbetrag` | Projekt | Freie Positionen wie heute (Frau Wolf: „Erstellung der Website anelia-wolf.com, Pauschalpreis laut Absprache“, 500 €). |
|
||||||
| `frei` | „Ohne Projekt“ gewählt | Freie Positionen wie bisher, ohne Zusatzfelder „Projekt“/„Auftrag“ (aber A4). |
|
| `frei` | „Ohne Projekt“ gewählt | Freie Positionen wie bisher, ohne Zusatzfelder „Projekt“/„Auftrag“ (aber A4). |
|
||||||
|
|
||||||
Grenzen wie heute (1–20 Positionen inkl. Minuszeilen; Beschreibung ≤ 200 Zeichen). Ergäbe eine Aufwandsrechnung mehr
|
Grenzen wie heute (1–20 Positionen inkl. Minuszeilen; Beschreibung ≤ 200 Zeichen); eine Schlussrechnung darf bis
|
||||||
|
40 Zeilen haben (AB-Positionen ≤ 20, Zusatzpositionen ≤ 10, Abzüge ≤ 10). Ergäbe eine Aufwandsrechnung mehr
|
||||||
als 20 Zeilen (zusätzliche Positionen zählen mit), werden die ältesten Tage zu Kalenderwochen zusammengefasst
|
als 20 Zeilen (zusätzliche Positionen zählen mit), werden die ältesten Tage zu Kalenderwochen zusammengefasst
|
||||||
(„KW 41/2026: … – 6,5 Std. à 100,00 €“), bis höchstens 20 Zeilen bleiben.
|
(„KW 41/2026: … – 6,5 Std. à 100,00 €“), bis höchstens 20 Zeilen bleiben.
|
||||||
|
|
||||||
@@ -283,7 +285,8 @@ Neue Collections (Setup-Skript `scripts/auftraege-setup.mjs`, idempotent wie `se
|
|||||||
`agbFassung`, `pdfDatei`, `pdfSha256`, `gesendetAm`, `gesendetAn`, `mailStatus`, `mailFehler`, `tokenHash`,
|
`agbFassung`, `pdfDatei`, `pdfSha256`, `gesendetAm`, `gesendetAn`, `mailStatus`, `mailFehler`, `tokenHash`,
|
||||||
`tokenGueltigBis`, `bestaetigtAm`, `bestaetigtUeber`, `bestaetigtPdfSha256`, `fruehbeginn`, `fruehbeginnFassung`,
|
`tokenGueltigBis`, `bestaetigtAm`, `bestaetigtUeber`, `bestaetigtPdfSha256`, `fruehbeginn`, `fruehbeginnFassung`,
|
||||||
`ersetztId`, `ersetztDurchId`, `storniertAm`, `widerrufenAm`, `erstelltVon`, `erstelltAm`, `geaendertAm`.
|
`ersetztId`, `ersetztDurchId`, `storniertAm`, `widerrufenAm`, `erstelltVon`, `erstelltAm`, `geaendertAm`.
|
||||||
Indizes: `kundenId`, `projektId`, `nummer` (unique), `tokenHash`.
|
Indizes: `kundenId`, `projektId`, `nummer` (normaler Index: Entwürfe haben keine Nummer; eindeutig über die Sperre
|
||||||
|
beim Senden), `tokenHash`.
|
||||||
- **`projektRechnungen`**: Dokument-ID = Stripe-Rechnungs-ID; `kundenId`, `projektId`, `auftragId`, `art`,
|
- **`projektRechnungen`**: Dokument-ID = Stripe-Rechnungs-ID; `kundenId`, `projektId`, `auftragId`, `art`,
|
||||||
`betragCents`, `arbeitsblattIds` (string[]), `aktiv` (bool), `erstelltVon`, `erstelltAm`. Indizes: `kundenId`,
|
`betragCents`, `arbeitsblattIds` (string[]), `aktiv` (bool), `erstelltVon`, `erstelltAm`. Indizes: `kundenId`,
|
||||||
`projektId`, `auftragId`.
|
`projektId`, `auftragId`.
|
||||||
@@ -378,6 +381,10 @@ Der Nutzer gibt frei; danach lädt Claude die Fassungen in WOMS → Rechtliches
|
|||||||
- **Plan 2 – Auftragsbestätigung:** `auftraege`, Kundenart, Steuernummer-Einstellung, PDF, Mails, Bestätigungsseite
|
- **Plan 2 – Auftragsbestätigung:** `auftraege`, Kundenart, Steuernummer-Einstellung, PDF, Mails, Bestätigungsseite
|
||||||
und Portal-Abschnitt, Verbraucher-Variante, Widerruf, Arten `anzahlung` und `schluss` (mit Minuszeilen),
|
und Portal-Abschnitt, Verbraucher-Variante, Widerruf, Arten `anzahlung` und `schluss` (mit Minuszeilen),
|
||||||
WOMS-Dialog und -Liste, Rechtsrunde.
|
WOMS-Dialog und -Liste, Rechtsrunde.
|
||||||
|
- **Parallel gebaut, gestaffelt live:** Auf Wunsch des Nutzers (06.10.2026) werden beide Pläne gleichzeitig in
|
||||||
|
parallelen Strängen gebaut (Umsetzungsplan `2026-10-06-auftraege-00-uebersicht.md`). Der AB-Teil (und die Arten
|
||||||
|
`anzahlung`/`schluss`) hängt hinter dem Schalter `AUFTRAEGE_ENABLED` (`off` | `pilot` | `on`, Vorgabe `off`), so geht
|
||||||
|
der Rechnungsteil ohne Rechtsrunde live; der Datenschutz-Abschnitt kommt erst mit dem AB-Livegang.
|
||||||
- **Reihenfolge je Plan:** Portal-Server und Kundenseiten zuerst; WOMS-Oberfläche im neuen Design auf Basis der
|
- **Reihenfolge je Plan:** Portal-Server und Kundenseiten zuerst; WOMS-Oberfläche im neuen Design auf Basis der
|
||||||
zusammengeführten Redesign-Stränge Kunden (P4) und Finanzen (P7). Sind die Stränge beim Start der WOMS-Aufgaben noch
|
zusammengeführten Redesign-Stränge Kunden (P4) und Finanzen (P7). Sind die Stränge beim Start der WOMS-Aufgaben noch
|
||||||
nicht zusammengeführt, wird mit dem Nutzer entschieden (warten oder auf `test` bauen und eine Mitnahme-Notiz für das
|
nicht zusammengeführt, wird mit dem Nutzer entschieden (warten oder auf `test` bauen und eine Mitnahme-Notiz für das
|
||||||
@@ -400,3 +407,13 @@ Projektmarge und Auswertungen, automatische Erinnerungen.
|
|||||||
- Auftragsbestätigung (Inhalt): https://www.lexware.de/wissen/faktura-warenwirtschaft/auftragsbestaetigung-bedeutung-und-inhalt/
|
- Auftragsbestätigung (Inhalt): https://www.lexware.de/wissen/faktura-warenwirtschaft/auftragsbestaetigung-bedeutung-und-inhalt/
|
||||||
- Stripe Rechnungen anpassen: https://docs.stripe.com/invoicing/customize
|
- Stripe Rechnungen anpassen: https://docs.stripe.com/invoicing/customize
|
||||||
- Agentur-Abrechnung (Festpreis/Aufwand): https://everhour.com/de/invoices/rechnungssoftware-fuer-digitalagenturen
|
- Agentur-Abrechnung (Festpreis/Aufwand): https://everhour.com/de/invoices/rechnungssoftware-fuer-digitalagenturen
|
||||||
|
|
||||||
|
## Nachträge aus der Planrunde (06.10.2026)
|
||||||
|
|
||||||
|
- Kundentexte: Mails, PDF und die öffentliche Bestätigungsseite in Sie-Form; der Abschnitt „Aufträge“ im angemeldeten
|
||||||
|
Portal in Du-Form wie das übrige Portal.
|
||||||
|
- Steuernummer-Feld akzeptiert auch eine USt-IdNr. („DE…“).
|
||||||
|
- Frühbeginn-Erklärung stützt sich auf § 356 Abs. 5 Nr. 2 und § 357a Abs. 2 BGB (Wortlaut in der Rechtsrunde).
|
||||||
|
- Bei Weg „vor Ort“ und Kundenart Privatperson zeigt der AB-Dialog den Hinweis „zusätzlich ausdrucken und übergeben“
|
||||||
|
(§ 312f Abs. 1 BGB), bis die Kanzlei-Frage beantwortet ist.
|
||||||
|
- Team-Mail T22 trägt im Betreff das Präfix „[Aufträge]“ wie die übrigen Team-Mails.
|
||||||
Reference in new issue
Block a user