Rechtstexte und K15: Prüfbefunde F5–F8 (AVV genauer, PDF-Prüfsumme, Unterlagen der vereinbarten Fassung)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-05 03:42:27 +00:00
1 parent 2c059cb3d6
commit 90098726b9
13 files changed
+345 -63

No files matched your search

+16 -6
View File
@@ -89,11 +89,21 @@ function funktionSatz(featureKey) {
/**
* Anhaenge der Bestellmail einer Funktion in der aktuellen Fassung: termine => Satz 'termine', news => []. Wirft bei
* unbekannter Funktion und bei fehlender Datei (der Aufrufer entscheidet, was dann geschieht).
* unbekannter Funktion und bei fehlender Datei (der Aufrufer entscheidet, was dann geschieht). opts.fassung waehlt eine andere
* (ausgelieferte) Fassung, etwa die, der ein Kunde bei der Bestellung zugestimmt hat; sonst gilt die aktuelle.
*/
export async function featureLegalAttachments(featureKey, opts) {
export async function featureLegalAttachments(featureKey, { fassung, ...opts } = {}) {
const satz = funktionSatz(featureKey)
return satz ? legalPdfAttachments(satz, aktuelleFassung(satz), opts) : []
return satz ? legalPdfAttachments(satz, fassung ?? aktuelleFassung(satz), opts) : []
}
/**
* Adressen der HTML-Seiten der Funktion ({ lb, avv, widerruf }, versioniert) in der aktuellen oder der gewaehlten Fassung
* (opts.fassung); news => null. Wirft bei unbekannter Funktion oder ungueltiger Fassung.
*/
export function featureLegalPaths(featureKey, { fassung } = {}) {
const satz = funktionSatz(featureKey)
return satz ? legalPaths(satz, fassung ?? aktuelleFassung(satz)) : null
}
/**
@@ -116,13 +126,13 @@ async function istLesbaresPdf(pfad) {
/**
* true, wenn alle Rechtsdokumente der Funktion in der aktuellen Fassung vorliegen: HTML-Seiten (die die Bestellseite
* verlinkt) und PDFs (die Mail anhaengt; je PDF stat und die ersten fuenf Bytes, nicht der ganze Inhalt). Unbekannte
* Funktion und jeder Lesefehler: false (fail-closed), nie ein Wurf.
* Funktion und jeder Lesefehler: false (fail-closed), nie ein Wurf. opts.fassung prueft eine andere Fassung als die aktuelle.
*/
export async function featureLegalReady(featureKey) {
export async function featureLegalReady(featureKey, { fassung } = {}) {
try {
const satz = funktionSatz(featureKey)
if (!satz) return true
const v = aktuelleFassung(satz)
const v = fassung ?? aktuelleFassung(satz)
await Promise.all(Object.values(legalPaths(satz, v)).map((adresse) => access(join(basisDir, adresse))))
const lesbar = await Promise.all(Object.values(legalPdfFiles(satz, v)).map((f) => istLesbaresPdf(f.path)))
return lesbar.every(Boolean)
+46 -28
View File
@@ -1178,41 +1178,58 @@ async function meldeFallZurZahlung(doc, fall, session) {
}
/** K15 einer Funktion ohne Rechtsdokumente (News): keine Anhaenge, keine Bloecke anlagen und zustimmungen. */
const OHNE_RECHTSUNTERLAGEN = Object.freeze({ attachments: Object.freeze([]), anlagen: false, anhang: false, zustimmungen: '', fehlt: false })
const OHNE_RECHTSUNTERLAGEN = Object.freeze({ attachments: Object.freeze([]), unterlagen: null, anhang: false, zustimmungen: '', fehlt: false })
/**
* Rechtsunterlagen der K15 einer Funktion mit Rechtsdokumenten: die PDFs der aktuellen Fassung (legalDocs.featureLegalAttachments,
* erst beim Aufruf geladen; kein Registerhaken) und der Wortlaut der Zustimmungen aus dem Nachweis zur bezahlten Sitzung. Gehoert der
* Nachweis zu einer anderen Fassung als der aktuellen (die Fassung wurde zwischen Klick und Zahlung angehoben), gehen weder PDFs noch
* Links raus: es waeren nicht die Unterlagen, denen der Kunde zugestimmt hat; das Team sendet sie von Hand (fehlt). Nicht lesbare PDFs:
* K15 ohne Anhang, fehlt true, Fehler im Log. Wirft nie.
* @returns {Promise<{ attachments: object[], anlagen: boolean, anhang: boolean, zustimmungen: string, fehlt: boolean }>}
* Rechtsunterlagen der K15 einer Funktion mit Rechtsdokumenten: die PDFs und Adressen der Fassung, der der Kunde bei der Bestellung
* zugestimmt hat (Fassung des Nachweises zur bezahlten Sitzung), und der Wortlaut seiner Zustimmungen. Das ist meist die aktuelle
* Fassung (legalDocs.featureLegalAttachments, erst beim Aufruf geladen; kein Registerhaken). Wurde die Fassung zwischen Klick und
* Zahlung angehoben, gehen die Unterlagen der vereinbarten Fassung raus, wenn es sie gibt; fehlen sie, geht K15 ohne PDFs und Links
* raus (bei der aktuellen Fassung bleiben die Links, nur die PDFs fehlen). Ist der Nachweis nicht lesbar oder seine Fassung nicht bestimmbar, ist die vereinbarte Fassung unbekannt: nie die aktuelle
* ungeprueft anhaengen, K15 geht ohne PDFs und Links raus. Ohne Nachweis (Altbestand) gilt die aktuelle Fassung. In allen Faellen ohne
* Unterlagen ist fehlt true (T16 sagt "von Hand nachsenden") und der Grund steht im Log. Wirft nie.
* @returns {Promise<{ attachments: object[], unterlagen: { fassung: string, lb: string, avv: string, widerruf: string } | null, anhang: boolean, zustimmungen: string, fehlt: boolean }>}
*/
async function k15Rechtsunterlagen(doc, feature, session) {
const ohne = (zustimmungen) => ({ attachments: [], unterlagen: null, anhang: false, zustimmungen, fehlt: true })
let nachweis = null
try {
nachweis = await zustimmungFuerSitzung(session.id)
} catch (err) {
console.error(`[funktionen] Zustimmungsnachweis zu Sitzung ${session.id} fuer K15 nicht lesbar, der Block zustimmungen entfaellt:`, err.message)
console.error(`[funktionen] K15 (Funktion ${doc.$id}): Zustimmungsnachweis zu Sitzung ${session.id} nicht lesbar, die vereinbarte Fassung ist unbekannt - ohne Unterlagen, das Team sendet sie von Hand:`, err.message)
return ohne('')
}
const zustimmungen = zustimmungenText(nachweis)
let fassungNachweis = ''
try {
fassungNachweis = nachweis ? JSON.parse(nachweis.fassungen)?.zustimmung ?? '' : ''
} catch {
fassungNachweis = ''
}
if (fassungNachweis && fassungNachweis !== feature.consent.version) {
console.error(`[funktionen] K15 (Funktion ${doc.$id}): Nachweis gehoert zur Fassung ${fassungNachweis}, aktuell ist ${feature.consent.version} - ohne Unterlagen, das Team sendet sie von Hand`)
return { attachments: [], anlagen: false, anhang: false, zustimmungen, fehlt: true }
const aktuell = feature.consent.version
let vereinbart = aktuell
if (nachweis) {
try {
vereinbart = JSON.parse(nachweis.fassungen)?.zustimmung
} catch {
vereinbart = undefined
}
if (typeof vereinbart !== 'string' || vereinbart === '') {
console.error(`[funktionen] K15 (Funktion ${doc.$id}): Fassung im Zustimmungsnachweis nicht bestimmbar - ohne Unterlagen, das Team sendet sie von Hand`)
return ohne(zustimmungen)
}
}
try {
const { featureLegalAttachments } = await import('./legalDocs.js')
const attachments = await featureLegalAttachments(doc.featureKey)
return { attachments, anlagen: true, anhang: attachments.length > 0, zustimmungen, fehlt: attachments.length === 0 }
const legal = await import('./legalDocs.js')
const eigene = vereinbart === aktuell ? {} : { fassung: vereinbart }
if (vereinbart !== aktuell) {
console.error(`[funktionen] K15 (Funktion ${doc.$id}): Der Kunde hat der Fassung ${vereinbart} zugestimmt, aktuell ist ${aktuell} - die Unterlagen der vereinbarten Fassung gehen raus, wenn es sie gibt`)
if (!(await legal.featureLegalReady(doc.featureKey, eigene))) throw new Error(`Unterlagen der Fassung ${vereinbart} fehlen`)
}
const attachments = await legal.featureLegalAttachments(doc.featureKey, eigene)
if (attachments.length === 0) throw new Error('keine Anhaenge fuer diese Funktion')
const adressen = legal.featureLegalPaths(doc.featureKey, eigene)
const absolut = (pfad) => `${config.publicBaseUrl}${pfad}`
const unterlagen = { fassung: vereinbart, lb: absolut(adressen.lb), avv: absolut(adressen.avv), widerruf: absolut(adressen.widerruf) }
return { attachments, unterlagen, anhang: true, zustimmungen, fehlt: false }
} catch (err) {
console.error(`[funktionen] K15-Anhaenge fuer Funktion ${doc.$id} nicht lesbar, K15 geht ohne Anhang raus:`, err.message)
return { attachments: [], anlagen: true, anhang: false, zustimmungen, fehlt: true }
console.error(`[funktionen] K15 (Funktion ${doc.$id}): Unterlagen nicht lesbar, K15 geht ohne PDFs raus:`, err.message)
// die Seiten der aktuellen (= vereinbarten) Fassung bleiben verlinkt; Links auf eine andere Fassung gibt es nur, wenn deren Dateien da sind
return { ...ohne(zustimmungen), unterlagen: vereinbart === aktuell ? unterlagenAdressen(feature) : null }
}
}
@@ -1237,29 +1254,30 @@ async function onFeaturePaid(session) {
// K15 mit den Vertragsunterlagen (Terminplaner: drei PDFs als dauerhafter Datentraeger, § 312f BGB). Die Bestaetigung geht vor dem
// Anhang: sind die PDFs nicht lesbar oder kommt die Mail nicht an, bleibt die Zahlung nicht ohne Spur, T16 sagt dem Team
// "Unterlagen von Hand nachsenden" (Block anhang_fehlt).
const unterlagen = feature ? unterlagenAdressen(feature) : null
const mitRechtsdokumenten = feature ? unterlagenAdressen(feature) !== null : false
let anhangFehlt = false
if (customer?.email) {
try {
if (!feature) throw new Error(`Funktion ${doc.featureKey} nicht im Register`)
const rechtlich = unterlagen ? await k15Rechtsunterlagen(doc, feature, session) : OHNE_RECHTSUNTERLAGEN
const rechtlich = mitRechtsdokumenten ? await k15Rechtsunterlagen(doc, feature, session) : OHNE_RECHTSUNTERLAGEN
anhangFehlt = rechtlich.fehlt
await sendCustomerMail(customer.email, {
...bestellMail(feature, {
kundeName: customer.name || '',
projekt: await projectName(doc.projectId),
invoiceUrl,
unterlagen: rechtlich.anlagen ? unterlagen : null,
unterlagen: rechtlich.unterlagen,
anhang: rechtlich.anhang,
zustimmungenText: rechtlich.zustimmungen,
}),
attachments: rechtlich.attachments,
attachments: [...rechtlich.attachments],
})
} catch (err) {
console.error('[funktionen] K15 fehlgeschlagen:', err.message)
if (unterlagen) anhangFehlt = true
if (mitRechtsdokumenten) anhangFehlt = true
}
} else if (unterlagen) {
} else if (mitRechtsdokumenten) {
console.error(`[funktionen] K15 (Funktion ${doc.$id}) nicht gesendet: beim Kunden ist keine E-Mail-Adresse hinterlegt - die Vertragsunterlagen muessen von Hand nachgesendet werden`)
anhangFehlt = true
}
// Funktionen, die ein laufendes Wartungs-Abo voraussetzen: endete es vor der Zahlung, sagt T16 es dem Team (Block kein_abo).