Eine Gutschrift/ein Lieferschein braucht eine Empfaengeradresse aufs
Dokument. Rechnungsadresse hat Vorrang, sonst Lieferadresse. Ist keine
von beiden hinterlegt -> Anlegen blockiert.
- Frontend: Klick auf 'Gutschrift anlegen' prueft defaults.hasRecipient-
Address; wenn false -> Modal-OK-Meldung statt Formular.
- Backend Defense-in-Depth: createCreditNote wirft 400, wenn weder
billingAddressId noch addressId gesetzt. getCreditNoteDefaults liefert
hasRecipientAddress.
Verifiziert: ohne Adresse -> hasRecipientAddress false + create 400;
mit Adresse -> ok.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Beleg-Upload jetzt fuer beide Arten: bei Geld die Ueberweisungs-
bestaetigung, bei Sachwert das unterschriebene Dokument. ReceiptControls
in der Liste fuer Geld UND Sachwert (Label je nach Typ). Endpoint war
schon typ-agnostisch.
- PDF-Unterschriftsblock nur noch bei Sachwert - eine Ueberweisung wird
nicht unterschrieben (Beleg = hochgeladene Ueberweisungsbestaetigung).
Bei Geld entfaellt der Unterschrift/Ort-Block; im Formular sind Ort +
'Unterschrift am' bei Geld ausgeblendet.
Verifiziert: beide PDFs erzeugen sauber (je 1 Seite).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pentester R142: Uebergang Geld->betragsloser Sachwert setzte die schon
vergebene Gutschriftsnummer auf null -> Luecke in der GS-Serie.
Loesung: betragsloser Sachwert = Lieferschein mit eigener
Lieferscheinnummer aus separatem Nummernkreis.
- Schema: CreditNote.deliveryNoteNumber (nullbar, unique) + neues Model
DeliveryNoteNumberRange (Default-Praefix 'LS-') + Migration.
- deliveryNoteNumberRange.service (mirror, eigener Zaehler, FOR UPDATE).
- Nummern lazy pro Serie, NIE freigeben: Uebergaenge behalten die
jeweils vergebene Nummer der anderen Serie reserviert -> kein
Doppelverbrauch, keine Luecke. effectiveNumber() liefert je nach Typ
die passende (LS/GS) fuer Anzeige/PDF/Audit.
- Endpunkte GET/PUT /credit-notes/delivery-note-number-range; Settings-
Seite verwaltet jetzt beide Nummernkreise. PDF-Titel 'Sachwert-
Uebergabe', Dateiname lieferschein-...
- Frontend: Typ + displayNumber in Liste/Modal.
Verifiziert: Sachwert 0 -> LS-Nr, GS-Zaehler unberuehrt; Geld -> GS-Nr;
Uebergaenge behalten beide Nummern (kein Neuverbrauch, keine Luecke).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ein betragsloser Sachwert ist eher ein Lieferschein als eine Gutschrift
-> er soll KEINE Gutschriftsnummer aus dem Nummernkreis verbrauchen.
- Schema: CreditNote.number nullbar (Migration MODIFY ... NULL, UNIQUE
bleibt - MySQL erlaubt mehrere NULLs).
- createCreditNote: betragsloser Sachwert -> number=null, assignNextNumber
wird NICHT aufgerufen (Zaehler unangetastet).
- updateCreditNote: Uebergaenge - wird betragslos -> Nummer entfernen;
bekommt nachtraeglich Betrag & hatte keine -> jetzt Nummer vergeben.
- PDF/Liste/Modal/Audit: Fallback 'Sachwert-Uebergabe'/'Beleg #id' wenn
keine Nummer; PDF-Titel 'Sachwert-Uebergabe', kein ZUGFeRD (schon vorher).
Verifiziert: Sachwert 0 -> number null + Zaehler bleibt; Geld -> Nummer
+ Zaehler +1; Sachwert nachtraeglich mit Betrag -> Nummer vergeben.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Eine Sachwert-Gutschrift darf betragslos sein (Betrag leer/0): dann
findet keine Rechnungsstellung statt - der Kunde hat den Gegenstand
einfach als Subvention erhalten.
- Backend: leeres amount -> 0; Sachwert erlaubt 0, Geld verlangt > 0
(400 sonst). vatRelevant bei 0 erzwungen-false.
- PDF: betragsloser Sachwert -> Titel 'Sachwert-Uebergabe', kein
Betrags-/USt-Block (Hinweis keine Rechnungsstellung), KEIN ZUGFeRD-
Embedding. Mit Betrag -> unveraendert ZUGFeRD.
- Frontend: Wert-Feld bei Sachwert optional; USt-Block ausgeblendet ohne
Betrag; Liste zeigt 'Sachwert ohne Betrag (keine Rechnung)'.
Verifiziert: Sachwert 0 -> kein factur-x.xml; Geld 0 -> 400;
Sachwert 200 -> ZUGFeRD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1) Folgevertrag-Bug: Backend legt den Folgevertrag korrekt mit
previousContractId an. Der Verlust passierte im Frontend - das
Vorgaenger-Dropdown holt Vertraege ueber getAll, das DEACTIVATED
standardmaessig ausblendet. Beim Bearbeiten des Folgevertrags war der
deaktivierte Vorgaenger nicht als Option da -> Verknuepfung ging beim
Speichern verloren.
Fix: getAllContracts + Controller + contractApi.getAll um
includeDeactivated erweitert; Vorgaenger-Dropdown nutzt es und markiert
deaktivierte Vertraege mit '· deaktiviert'. Verifiziert (Flag inkludiert
deaktivierte; Folgevertrag setzt previousContractId).
2) Kundendaten-Modal: zeigt jetzt zusaetzlich Lieferadresse + (falls
abweichend) Rechnungsadresse des geoeffneten Vertrags, die Stressfrei-
Adresse des Vertrags einzeln und darunter alle weiteren Stressfrei-
Adressen des Kunden. CustomerInfoModal nimmt optionale Vertragskontext-
Props; ContractDetail + ContractForm uebergeben sie.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bei Geld-Gutschriften kann die Auszahlung auf ein anderes Konto gehen
als das Vertrags-Abbuchkonto:
- CreditNote.payoutBankCardId + Migration (FK ON DELETE SET NULL, damit
Loeschen einer Bankkarte die Gutschrift nicht mitreisst).
- Formular: Dropdown mit allen Bankkonten des Kunden (Default =
Vertrags-Abbuchkonto). getCreditNoteDefaults liefert bankCards +
contractBankCardId.
- Server prueft, dass die gewaehlte Bankkarte dem Kunden des Vertrags
gehoert (kein Fremdkonto unterschieben).
- PDF: bei Ueberweisung 'Unsere Bankverbindung' (Absender) + darunter
'an Bankkonto: <Kunden-IBAN> (<Inhaber>)'. Section-Zeile zeigt das
Auszahlungskonto.
Lokal verifiziert (Anlage + PDF).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PDF-Erzeugung fuer Gutschriften mit Firmenstammdaten als Absender,
Empfaenger (Kunde+Adresse), Netto/USt/Brutto (bzw. ohne USt-Ausweis),
Sachwert/'Ware erhalten' bzw. Ueberweisungs-Bankverbindung,
Unterschriftsblock, Firmen-Fusszeile.
Endpoint POST /credit-notes/:id/pdf; 'PDF'-Button in CreditNotesSection
(erzeugen + im Tab oeffnen bzw. vorhandenes ansehen). PDF-Pfad wird bei
inhaltlicher Aenderung geleert -> Neu-Erzeugung. Download ueber
fileDownload (credit-notes subDir, Vertrags-Ownership).
Lokal verifiziert (valides %PDF, korrekte Betraege).
ZUGFeRD-XML-Embedding (PDF/A-3, EN 16931) folgt als Teil 2 mit
Validator-Pruefung.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enum-Wert ONGOING neu, MariaDB-Migration (ans Ende gehängt, damit
keine Rows umgeschrieben werden). Prisma Client neu generiert.
Cockpit-Logik: ONGOING wird in die Query mit reingeholt (damit
DSGVO-/Consent-Warnungen weiter greifen), aber alle Fristen-
Blöcke pro Vertrag geskippt:
- Kündigungsfrist
- Vertragsende
- Zwischenrechnung-Frist
Kein Skip für Daten-Qualität (fehlende Adresse, Bank, Portal-
Zugang etc.) – die sind auch für unbefristete Verträge relevant.
Frontend: statusLabels/statusVariants/statusDescriptions in
ContractDetail, ContractList, CustomerDetail und
ContractDetailModal um ONGOING ergänzt. Status-Select im
ContractForm bekommt die neue Option zwischen ACTIVE und
CANCELLED.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Die "Anbieter & Tarif"-Card war nur sichtbar, wenn provider oder
tariff gesetzt waren. Bei Entwürfen ohne Anbieter wurden dadurch
auch customerNumberAtProvider + contractNumberAtProvider versteckt,
obwohl sie pflegbar sind und für den Wechsel-Workflow wichtig sind.
Fix: Card-Sichtbarkeitsbedingung um die beiden Felder erweitert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Erwähnte Vertragsnummern (Pattern PREFIX-RANDOM) in Title und
Description werden gegen previousContract + followUpContract des
aktuellen Vertrags aufgelöst und als Link mit target="_blank"
gerendert. Nicht aufgelöste Nummern bleiben als Text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Folge-Symptom des Pen-30.13-Fixes: alle file-downloads liefen mit
Content-Disposition: attachment – das ist gegen Stored-XSS richtig,
hat aber die "Anzeigen"-Buttons (Bankkarten / Ausweise /
Verträge / etc.) kaputtgemacht, weil der Browser jetzt
herunterlud statt im Tab zu öffnen.
Magic-Byte-basierter Whitelist-Pfad eingebaut: optional ?disposition=
inline am Download-Endpoint, ABER nur wenn die ersten Bytes der
Datei das Magic eines safe Typs zeigen (PDF, PNG, JPEG, GIF, WebP).
Bei Mismatch fällt's auf attachment zurück – Stored-XSS bleibt
weiterhin unmöglich, falls jemand HTML als .pdf hochlädt.
Frontend: neuer viewUrl(path)-Alias = fileUrl(path, {inline: true}).
Alle Stellen mit `<a href={fileUrl(...)} target="_blank">` oder
`window.open(fileUrl(...), '_blank')` (13 Stellen über CustomerDetail,
ContractDetail, PdfTemplates, GDPRDashboard, InvoicesSection)
nutzen jetzt viewUrl. Download-Stellen bleiben fileUrl
(= attachment, byte-genaues File-Save).
Live-verifiziert auf dev:
- ohne Param: attachment (default, Stored-XSS-Schutz)
- ?disposition=inline + echte PDF: inline + application/pdf
- ?disposition=inline + HTML als .pdf: attachment (Magic-Mismatch
→ Browser lädt herunter statt zu rendern)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Feature 1 – Kunden-Schnellansicht:
Info-Icon neben dem Kundenlink im Vertragsdetail oeffnet ein Modal
mit den wichtigsten Kundendaten (Firma, Name, Geburtsdatum/-ort,
Gruendungsdatum, Adresse, Telefon, Mobil, E-Mail, Portal-E-Mail,
Steuer-/Handelsregisternr). Jedes Feld hat einen Copy-Button.
Lazy-Fetch via customerApi.getById, staleTime 30s.
Feature 2 – Cent/Euro-Doppel-Input:
Neben dem €/kWh-Arbeitspreis-Feld jetzt ein zweites ct/kWh-Feld.
Bidirektional gekoppelt – Tippen in € aktualisiert ct (×100),
Tippen in ct aktualisiert € (÷100). Backend speichert weiterhin
nur den Euro-Wert; Cent ist reine UI-Hilfe. Float-Rausch-Schutz
verhindert "0.25 → 25.0000000000004". Greift fuer unitPrice und
unitPriceNt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Live-Pentest gegen Dev-Server + 3 parallele Audit-Agents.
🚨 CRITICAL: /api/uploads/* war ohne Auth erreichbar
- express.static('/api/uploads', ...) → jeder konnte mit ratbarer URL
sensible PDFs (Kündigungsbestätigungen, Ausweise, Bankkarten,
Vollmachten) ziehen. Live-verifiziert: 23-KB-PDF eines echten Kunden
ohne Login geladen.
- Fix: authenticate-Middleware vor static-Handler (req.query.token
unterstützung war schon da, jetzt aktiv genutzt).
- Frontend: utils/fileUrl.ts hängt JWT als ?token=... an. 24 direkte
/api${...Path}-URLs in 5 Dateien per Skript migriert (CustomerDetail,
ContractDetail, InvoicesSection, PdfTemplates, GDPRDashboard).
🚨 HIGH: Login-Timing User-Enumeration
- bcrypt.compare wurde nur bei existierenden Usern ausgeführt → 110ms
vs 10ms Differenz, Email-Enumeration trivial messbar.
- Fix: Dummy-bcrypt-compare bei invalid user (Cost 12). Plus Lazy-
Rehash bei erfolgreichem Login: alte Cost-10-Hashes (z.B. admin aus
Installation) werden auf BCRYPT_COST upgraded, damit Dummy- und
Echt-Hash-Cost zusammenpassen.
- Live-verifiziert nach Admin-Rehash: 422ms (invalid) vs 423ms (valid)
– Side-Channel dicht.
🚨 HIGH: XSS via Privacy-Policy/Imprint-HTML
- 4 Frontend-Seiten renderten Backend-HTML ohne DOMPurify
(PortalPrivacy, ConsentPage, PortalWebsitePrivacy, PortalImprint).
Admin-eingegebene <script>-Tags wären bei jedem Portal-Kunden-
Besuch ausgeführt worden – auch auf der öffentlichen Consent-Seite.
- Fix: DOMPurify.sanitize mit strikter FORBID_TAGS/ATTR Config.
🛡 HIGH: IDOR-Härtung an Upload-/Document-Endpoints
- canAccessContract jetzt in: uploadContractDocument,
deleteContractDocument, handleContractDocumentUpload (Kündigungs-
Letter+Confirmation), handleContractDocumentDelete,
saveAttachmentAsContractDocument.
- Defense-in-Depth: aktuell durch requirePermission abgesichert,
schützt auch gegen künftige Staff-Scoping-Rollen.
Offen für v1.1:
- Per-File-Ownership-Check für /api/uploads (Kontroll-Lookup
welche Ressource zur Datei gehört)
- TipTap-Link-Tool javascript:-Protokoll blockieren
- Prisma-Error-Messages in Admin-Endpoints generisch sanitisieren
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>