Files
craftvia/docs/craftvia/lanes/abrechnung.md
T
msolarczekandClaude Opus 5 2df785dd14 L14 Abrechnungsübersicht: Tests, Backfill, Smoke und Lane-Bericht
test-abrechnung-service (118 Prüfungen inkl. Mandantentrennung und PDF-Render-Smoke), test-abrechnung-sync (21), Fixture-Zeilen für die neuen Tenant-Modelle, Rollen-Matrix billing:*, smoke-auth um /billing, Detail, Druckansicht, Auftrags-Tab und mobile Meilensteine erweitert, scripts/billing-backfill.ts, docs/craftvia/lanes/abrechnung.md, ABNAHME §3 „Abrechnungsübersicht“ (keine Buchhaltung). Gate 69/69, RLS 69/69.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:36:10 +02:00

18 KiB
Raw Blame History

Lane L14 – Abrechnungsübersicht (lane/abrechnung)

Stand: 2026-09-15 · Basis 5ea23a1 (feature/craftvia-mvp, L1–L12 integriert) · Spec §10.3, §13, §16/§17, §35

Ziel: Abgeschlossene Aufträge und erreichte Meilensteine laufen in einer Abrechnungsübersicht auf, damit die Buchhaltung außerhalb von Craftvia die Rechnungen stellt. Keine Buchhaltung: keine Preise, Beträge, Steuern, Rechnungserstellung, Zahlungen, kein Export in Buchhaltungsformate (kein CSV).

1. Umfang

Punkt Umsetzung
Datenmodell Migration 20260915150000_abrechnung_uebersicht: Tabellen work_order_milestones (WorkOrderMilestone: Titel, Beschreibung, Reihenfolge, Status open/reached/confirmed/billed, gemeldet/bestätigt/abgelehnt von–am, Notiz, Ablehnungsgrund, billingRecordId, Soft Delete) und billing_records (BillingRecord: Art order_completion/milestone/daily_report, Quelle milestoneId/reportId, Zeitraum, Status open/billed/voided, snapshot Json, PDF-Dokument + Prüfsumme, Rechnungsnummer, abgerechnet/storniert von–am, Stornogrund). Beide mit enable_tenant_rls, in beiden TENANT_MODELS-Listen, Personenfelder in pii-fields.ts. Partial-Unique-Indizes (SQL): höchstens ein nicht stornierter Eintrag je Auftragsabschluss / Meilenstein / Tagesbericht. L12-Modelle: je eine nullable Spalte billing_record_id + Index an time_entries und material_usages (keine FK).
Entstehung (services/billing/candidates.ts#syncBillingCandidates) Idempotent, INSERT … ON CONFLICT DO NOTHING (hält auch eine umgebende Transaktion intakt). Event-Weg: emitEvent ruft nach der Benachrichtigung einen Billing-Hook für work_order.released_for_billing und report.approved auf – Abrechnungsfreigabe (L2) und Berichtsfreigabe (L5) bleiben unverändert. Meilenstein-Bestätigung ruft den Sync in derselben Transaktion. scripts/billing-backfill.ts für Bestandsdaten (abgerechnete Aufträge bekommen bewusst keinen offenen Eintrag).
Zeitraum-Regeln Auftragsabschluss: vom Ende des letzten abgerechneten Abschnitts (sonst erste dokumentierte Tätigkeit/Auftragsanlage) bis zur Freigabe; enthält alles noch nicht Abgerechnete des Auftrags. Meilenstein: gleicher Beginn bis Bestätigungszeitpunkt; enthält alle noch nicht abgerechneten Positionen, die vor der Bestätigung erfasst wurden. Tagesbericht (nur Aufträge ohne definierte Meilensteine): Kalendertag des Berichts in der Mandanten-Zeitzone; eine neue Berichtsversion ersetzt den Bericht eines noch offenen Eintrags. Positionen = Zeiteinträge (nach startedAt) und Materialerfassungen (nach createdAt) mit billingRecordId = null.
Aufstellung (services/billing/statement.ts) Kopf: Mandant (Name/Adresse wie Bericht; Logo wie im Bericht noch ohne Dokument), Kunde (Nr., Name, Rechnungsadresse = Kundenadresse, Abrechnungshinweise billingNotes), Objekt, Auftrag (Nr., externe Nr., Angebotsnr., Titel, Auftragsart, Abrechnungsart, Team), Abschnitt (Art, Meilenstein-Titel/Berichtsdatum, Zeitraum, Einsatztage). Zeiten je Person: Arbeit, Fahrzeit (Anfahrt + Rückfahrt), Materialbeschaffung, Summe; Pausen und Unterbrechungen nur informativ, nicht summiert; Kennzeichen „manuell nachgetragen (freigegeben)“. Nur approved + beendete Einträge; offene Nachträge als Hinweis „x h offen, nicht enthalten“, laufende Einträge als Hinweis. Anfahrten: Anzahl + Datumsliste. Material: verwendet, zusätzlich (hervorgehoben), nicht verwendete Planpositionen (informativ), Abweichungsgründe. Texte Zusatzleistungen/Abweichungen/offene Restarbeiten aus freigegebenen Berichten des Abschnitts; Berichtsreferenzen (Nr./Version/Datum/Freigabe) mit Unterschrift.
Anfahrten-Regel lib/billing/statement.ts#countTrips: je Kalendertag bilden Anfahrts-Segmente (travel), die sich zeitlich überschneiden oder innerhalb von 15 min beginnen, eine Fahrt (Kolonne fährt gemeinsam); eine spätere, getrennte Anfahrt am selben Tag zählt erneut. Rückfahrten zählen nicht als Anfahrt. Keine km.
Aktionen (services/billing/records.ts) markBilled (billing:write, Transaktion): Aufstellung neu berechnen → offene Zeiten ohne confirmPendingExcluded → blocked pending_time_entries; Snapshot eingefroren, Rechnungsnummer optional, Positionen per bedingtem updateMany zugeordnet (Mengenabgleich → sonst conflict positions_already_billed), Meilenstein → billed, Auftragsabschluss → Auftrag billed über applyTransition; Audit; danach PDF-Job. voidBilling (Grund Pflicht, nur billed): voided, Positionen frei, Meilenstein zurück auf confirmed, neuer offener Eintrag derselben Quelle, Auftrag billed → released_for_billing. updateInvoiceNumber (nur billed, Audit), requestBillingPdf.
Statusmodell status.ts unverändert (billed bleibt Endstatus in der Übergangstabelle). transition.ts#applyTransition hat die interne Option billingVoid: nur damit ist billed → released_for_billing erlaubt (Recht work_order:release_billing, Grund Pflicht, Guards der Freigabe, Statushistorie, Audit, Event). Nicht über transitionSchema/API/UI erreichbar.
PDF Job billing-pdf (Queue in queues.ts, Processor-Einzeiler mit turbopackIgnore), services/billing/pdf.ts#generateBillingPdf: aus dem Snapshot, HTML (server/pdf/templates/billing.tsx, gleiche Komponente wie Detail/Druck) → pdf/render.ts → storeFile Kategorie other, Titel „Abrechnungsblatt A-… – Art“, Sichtbarkeit backoffice_only, am Auftrag/Kunde/Objekt; Prüfsumme am Eintrag; nie ersetzt. Kopf mit Mandant, Fuß mit Eintrag-ID, Erstellt-Datum, Seite x von y.
Meilensteine (services/billing/milestones.ts) createMilestone/updateMilestone/deleteMilestone (Soft Delete)/moveMilestone (work_order:write, Auftrag nicht abgerechnet/storniert; ändern/löschen nur open), markMilestoneReached (field:execute + Auftrag im Scope oder work_order:write; idempotent), confirmMilestone / rejectMilestone (Grund Pflicht) mit billing:write. Events milestone.reached → Nutzer mit billing:write (In-App), milestone.confirmed/milestone.rejected → meldende Person (In-App, Grund im Text); Links Backoffice /work-orders/[id]?tab=billing, mobil /m/orders/[id].
Rechte / Modul billing:read, billing:write → Backoffice (über den bestehenden Rollenfilter) und Mandantenadmin; keine Feldrolle. Modul billing in lib/modules.ts (Layout requireModule, Actions server/actions/billing/* mit moduleGuard("billing")). scripts/sync-role-permissions.ts zieht Bestandsmandanten nach (lokal +8 Zuordnungen).
Sync Op milestone.reach (Envelope, ops.ts passthrough, Registry external-ops.ts → services/billing/sync-ops.ts, Zod lib/billing/schemas.ts#milestoneReachPayload), additiv/konfliktfrei, idempotent über clientOpId und fachlich.
API GET /api/v1/billing, GET /api/v1/billing/{id}, POST /api/v1/billing/{id}/billed, POST /api/v1/billing/{id}/void, GET/POST /api/v1/work-orders/{id}/milestones, `POST /api/v1/milestones/{id}/reach

2. Screens / Routen

Route Inhalt
/billing Nav „Abrechnung“ (Modul billing, billing:read). Tabs Offen (Standard) · Abgerechnet · Storniert mit Zählern; Filter Zeitraum, Kunde, Team, Art, Suche Auftragsnr./Kunde; Tabelle Auftrag, Kunde/Objekt, Art + Abschnitt, Zeitraum, Arbeitszeit, Fahrzeit, Anfahrten, Materialpositionen, Warnbadge „x h offene Zeiten“ (Text + Icon), „Bereit seit“ (bzw. abgerechnet am + Rechnungsnr. / storniert am + Grund); Mehrfachauswahl → „Abrechnungsblätter drucken“; Paginierung. Ohne Recht: Hinweis.
/billing/[id] Kopf mit Status/Art, Aktionen „Als abgerechnet markieren“ (Popup: Rechnungsnummer optional, bei offenen Zeiten Warnung + Checkbox), „PDF“ (sobald erzeugt, /files/<id>), „Druckansicht“, „Rechnungsnummer ändern“, „Stornieren“ (Popup, Grund Pflicht); Links Auftrag, Fotos, Berichte; Aufstellung als Abrechnungsblatt.
/billing/print?ids=… Druckansicht (bis 50 Blätter, Seitenumbruch je Blatt, Navigation/Buttons per Print-CSS ausgeblendet), Button „Drucken“. Kein Export.
/work-orders/[id]?tab=billing Tab „Meilensteine & Abrechnung“: Meilensteine anlegen, nach oben/unten, löschen (offen), Status (Text + Icon), gemeldet/bestätigt von–am, Notiz, letzter Ablehnungsgrund; Bestätigen/Ablehnen (Grund); Liste der Abrechnungseinträge mit Status und Link.
/m/orders/[id] Abschnitt „Meilensteine“ (nur wenn vorhanden): Status-Badges, großer Button „Erreicht melden“ (optionale Notiz, 48 px), offline über die Outbox („wird übertragen“), Ablehnungsgrund, „wartet auf Bestätigung“.
/dashboard Kachel „Bereit zur Abrechnung“ (offene Einträge, nur mit billing:read) → /billing.

3. Dateien

Neu

  • prisma/migrations/20260915150000_abrechnung_uebersicht/migration.sql
  • src/lib/billing/{schemas,statement}.ts
  • src/server/services/billing/{candidates,statement,records,milestones,queries,pdf,events,sync-ops,common}.ts
  • src/server/actions/billing/{billing,milestones}.ts
  • src/server/jobs/processors/billing-pdf.ts, src/server/pdf/templates/billing.tsx
  • src/app/(app)/billing/{layout,page}.tsx, src/app/(app)/billing/[id]/page.tsx, src/app/(app)/billing/print/page.tsx
  • src/app/api/v1/billing/route.ts, billing/[id]/route.ts, billing/[id]/billed/route.ts, billing/[id]/void/route.ts, work-orders/[id]/milestones/route.ts, milestones/[id]/{reach,confirm,reject}/route.ts
  • src/components/billing/{statement-document,work-order-billing-tab,action-form,print-button,ui}.tsx, src/components/field/{milestones,milestone-reach-button}.tsx
  • messages/{de,en}/billing.json
  • scripts/test-abrechnung-service.ts, scripts/test-abrechnung-sync.ts, scripts/billing-backfill.ts

Eingriffe außerhalb von billing/ (alle additiv, klein)

  • prisma/schema.prisma (2 Enums-Blöcke + 2 Modelle am Ende, je 1 Spalte + Index an TimeEntry/MaterialUsage, 2 Relationslisten am Ende von WorkOrder – plannedDurationMinutes/L13-Felder unberührt)
  • src/server/db.ts, src/server/backup/topology.ts (TENANT_MODELS), src/server/dsgvo/pii-fields.ts, src/server/rbac.ts (2 Rechte), src/lib/modules.ts (1 Modul), src/lib/nav.ts (1 Eintrag), src/components/audit-trail.tsx (2 Labels)
  • src/lib/events.ts (3 Events, entityType milestone), src/server/events.ts (Billing-Hook nach handleEvent, eigener try/catch), src/server/services/notifications/recipients.ts (1 Case), handle-event.ts (Link milestone)
  • src/server/services/work-orders/transition.ts (Option billingVoid, s. o.), src/server/services/work-orders/dashboard.ts (Zähler billingReady), src/app/(app)/dashboard/page.tsx (Kachel), src/app/(app)/work-orders/[id]/page.tsx (Tab billing)
  • src/app/(field)/m/(core)/orders/[id]/page.tsx (1 Import + 1 Zeile MilestonesSection)
  • src/lib/sync/envelope.ts (Op-Typ), src/lib/sync/ops.ts (passthrough), src/server/services/sync/external-ops.ts (Registry), src/server/jobs/queues.ts (Queue billing-pdf), src/server/jobs/processors/index.ts (1 Zeile)
  • src/lib/api/openapi.ts (Tag + 8 Pfade)
  • messages/{de,en}/{nav,modules,dashboard,workOrders,notifications}.json (additive Schlüssel)
  • Tests: scripts/test-e2e-tenant-isolation.ts (Fixture-Zeilen für die 2 neuen Tenant-Modelle), scripts/test-security-roles.ts (Proben billing:read/billing:write), scripts/smoke-auth.ts (Abrechnung, Tab, mobil, Mandant 2)
  • scripts/lib/demo-seed.ts: Seed-Fix – auf frischer DB brach prisma db seed seit L12 ab (other_session_running: DEMO-07/DEMO-06 hatten laufende Uhren, spätere vergangene Einsätze derselben Monteure kollidierten). DEMO-06/DEMO-07 werden jetzt nach DEMO-19 angelegt (Inhalt unverändert).

Keine neuen npm-Abhängigkeiten, keine Stubs.

4. Tests

  • scripts/test-abrechnung-service.ts – 118 Prüfungen: Kandidaten je Art (Auftragsabschluss idempotent, über das echte releaseForBilling-Event, Partial-Unique-Index, Tagesbericht über report.approved ohne Doppel, Zeitraum = Kalendertag, neue Berichtsversion ersetzt, Abschlussbericht erzeugt keinen Abschnitt, Auftrag mit Meilensteinen ohne Tagesbericht-Abschnitt); Anfahrten-Zählung (Kolonne 2 Personen gleichzeitig = 1, zwei Tage = 2, getrennt am selben Tag = 2); Aufstellung (Arbeit/Fahrzeit/Materialbeschaffung je Person, Pausen nicht summiert, manuell-Kennzeichen, Summen, offener Nachtrag nur als Hinweis, abgelehnte/laufende nicht enthalten, Material verwendet/zusätzlich/nicht verwendet, Abrechnungshinweise, Nummern, Berichtstexte, keine Preis-/Betragsfelder); markBilled (Teamleiter/Monteur forbidden, offene Zeiten blocked, Rechnungsnummer, PDF-Job, Auftrag → billed, Positionen zugeordnet, Audit, doppelt → conflict, Snapshot eingefroren trotz späterer Änderung, zweiter Abschnitt enthält abgerechnete Positionen nicht, Rechnungsnummer ändern); voidBilling (Grund Pflicht, Rechte, nur billed, Positionen frei, neuer offener Eintrag, Auftrag zurück + Statushistorie, genau ein offener Eintrag, zweites Storno conflict); Meilenstein-Flow (Anlegen nur Backoffice, Sortieren, Monteur ohne Scope not_found, melden + Event an Backoffice, idempotent, gemeldet nicht änder-/löschbar, Monteur/Teamleiter können nicht bestätigen, Ablehnen mit Grund + Event/Link an Meldenden, Bestätigen → Eintrag + Event, Zeiten nach Bestätigung nicht enthalten, abgerechnet → Meilenstein billed, nächster Meilenstein ohne abgerechnete Zeiten, Soft Delete, Sichtbarkeit mobil); Liste/Filter/Sortierung/Summen, Detail, billing:read für Monteur/Teamleiter forbidden, Dashboard-Kachel; Mandantentrennung (Liste, Detail, Aufstellung, abrechnen, stornieren, Rechnungsnummer, Meilenstein anlegen/melden/bestätigen, PDF, Kandidaten-Sync); PDF-Render-Smoke (Dokument other/backoffice_only/Titel, Prüfsumme, Bytes %PDF, nicht ersetzt; Skip ohne Chromium – lokal über Google Chrome gerendert).
  • scripts/test-abrechnung-sync.ts – 21 Prüfungen: Registry/Envelope, Payload-Schema, milestone.reach applied + Event, gleiche clientOpId → duplicate, neue clientOpId für gemeldeten Meilenstein → applied ohne Änderung, ungültige Payload → rejected invalid, Meilenstein eines anderen Auftrags → not_found, Monteur ohne Zuweisung / Mandant B → not_found ohne Wirkung, Teamleiter des Teams darf melden; alle 8 API-Operationen dokumentiert und als Route vorhanden.
  • Angepasst: test-e2e-tenant-isolation.ts (163 ✓, inkl. RLS je neuer Tabelle), test-security-roles.ts (129 ✓, Matrix-Abdeckung der neuen Rechte).

Gate npm run gate (Vordergrund): prisma generate, tsc, lint (0 Fehler, 3 bestehende Warnungen außerhalb L14), build inkl. Modul-Guard-Check (35 Action-Dateien), 69/69 Testskripte grün. RLS-Lauf RLS_ENFORCED=true npm run test (RLS_DATABASE_URL auf craftvia_abrechnung): 69/69 grün.

HTTP-Smoke (scripts/smoke-auth.ts, Dev-Server :3115, frischer Demo-Seed, sync-role-permissions, billing-backfill): 120 Prüfungen, alle grün (eine Monteur-Prüfung war zunächst falsch formuliert – Seitentexte stehen über die serialisierten Messages immer im HTML – und wurde auf den datengebundenen Eintrag-Link umgestellt, Monteur-Lauf danach 31/31). Neu: Backoffice /billing, ?status=billed, Suche, /billing/[id] (Abrechnungsblatt, Anfahrten, Aktion), /billing/print, Dashboard-Kachel, Auftrags-Tab mit Meilenstein; Monteur mobil „Meilensteine / Erreicht melden“, /billing ohne Recht (Hinweis, kein Eintrag), Detail 404; Mandant 2: Liste ohne Demo-Einträge, Detail 404, Druckansicht leer, Auftrags-Tab 404. Das Abrechnungsblatt wurde zusätzlich als PDF/PNG gerendert und visuell geprüft. Browser-Prüfung mit Anmeldung nicht durchgeführt (keine Passwort-/Token-Eingabe durch den Agenten).

5. Bekannte Lücken / Hinweise

  1. Bestehender Button „Abgerechnet“ (L2) im Auftragsdetail (released_for_billing → billed) bleibt erhalten; wird er statt der Abrechnungsübersicht benutzt, bleibt der offene Auftragsabschluss-Eintrag stehen (kein Snapshot, keine Positionszuordnung). Empfehlung: Button ausblenden, sobald das Modul billing aktiv ist (L2-Datei, hier nicht geändert).
  2. Events in Transaktionen: Der Billing-Hook läuft wie handleEvent im Kontext des Aufrufers (bei inTransaction innerhalb der Transaktion); Anlage per ON CONFLICT DO NOTHING, Fehler werden geloggt. Ein unerwarteter SQL-Fehler im Hook würde eine umgebende Postgres-Transaktion dennoch abbrechen.
  3. Partial-Unique-Indizes stehen nur in der Migration (Prisma kennt sie nicht); ein späteres prisma migrate diff zeigt sie nicht an – bei Schema-Squash erhalten.
  4. Rechnungsnummer nachträglich: Spalte + Anzeige aktualisiert; ein bereits erzeugtes PDF bleibt unverändert (unveränderliches Dokument).
  5. Recht für Auftragsstatus: markBilled/voidBilling eines Auftragsabschlusses ändern den Auftragsstatus und brauchen zusätzlich work_order:release_billing (Standardrollen mit billing:write haben es; eigene Rollen ohne das Recht erhalten forbidden, nichts wird gespeichert).
  6. Sync-Op milestone.reach prüft das Modul billing nicht (läuft über /api/v1/sync, Modul field); die mobile Anzeige erscheint nur, wenn Meilensteine existieren.
  7. Zeitpunkt der Positionen: Material wird nach Erfassungszeitpunkt (createdAt), Zeiten nach Beginn (startedAt) einem Tages-/Meilenstein-Abschnitt zugeordnet; Nachträge nach der Bestätigung eines Meilensteins fallen in den nächsten Abschnitt bzw. den Auftragsabschluss.
  8. Mandantenlogo im Blatt wie im Bericht noch nicht (kein Logo-Dokument).
  9. Umgebung: Demo-Seed auf frischer DB brauchte den Seed-Fix (s. §3). docs/craftvia/API.md nicht ergänzt (OpenAPI ist maßgeblich).

6. Migration / Deploy

20260915150000_abrechnung_uebersicht – additiv (3 Enums, 2 Tabellen mit RLS, 2 nullable Spalten + Indizes, 3 Partial-Unique-Indizes, 2 FKs mit ON DELETE CASCADE auf work_orders). Deploy: prisma migrate deploy → npx tsx scripts/sync-role-permissions.ts (Nutzer neu anmelden) → npx tsx scripts/billing-backfill.ts → Worker neu starten (Queue billing-pdf).