import { config } from '../config.js' import { createDocument } from './appwriteAdmin.js' import { actingPerson, texteHash } from './featureConsents.js' import { neueAnsicht } from './kundenansicht/schalter.js' import { KUNDENARTEN } from './kundenart.js' import { FEHLER_NACHWEIS, WORTLAUT_STATUS } from './bestelltexte.js' /** * Weiche der Fassungen und Nachweis der Bestellseiten (Kundenansicht neu, P7, V12). * * fassungDerAnfrage entscheidet, ob eine Anfrage die Fassung 6 (neue Ansicht) oder die bisherige Fassung meint; alle Routen * verzweigen nur bei 'neu'. speichereVertragsNachweis schreibt den angezeigten Wortlaut als Zeile in featureConsents (ohne Projekt), * bevor die unumkehrbare Handlung geschieht (Stripe-Sitzung, Wechsel, Speicheraenderung, Antrag). */ const ARTEN = ['abo', 'wechsel', 'speicher', 'gutschrift'] // Grenzen der Collection featureConsents (wie featureConsents.js) const MAX_ID = 36 const MAX_FASSUNGEN = 1000 const MAX_TEXTE = 20000 const MAX_IP = 64 const MAX_USER_AGENT = 300 const MAX_KONTO = 255 const nichtLeer = (wert) => typeof wert === 'string' && wert.trim() !== '' /** * 'alt' | 'neu' | null. 'neu': termsVersion ist nichtleer, gleich config.abo.termsVersionNeu UND die Anfrage sieht die neue Ansicht. * 'alt': termsVersion ist gleich config.abo.termsVersion. Alles andere (auch eine fehlende Neu-Fassung) ist null. Die Neu-Fassung * wird zuerst geprueft: bei gleichen Werten gilt neueAnsicht(req) ? 'neu' : null (P10 aendert hier nichts). */ export function fassungDerAnfrage(req, termsVersion) { if (!nichtLeer(termsVersion)) return null if (istNeueFassung(termsVersion)) return neueAnsicht(req) ? 'neu' : null return termsVersion === config.abo.termsVersion ? 'alt' : null } /** Nichtleer und gleich config.abo.termsVersionNeu (ohne Sitzungsbezug; fuer Mails und Ereignisse). */ export function istNeueFassung(termsVersion) { return nichtLeer(termsVersion) && nichtLeer(config.abo.termsVersionNeu) && termsVersion === config.abo.termsVersionNeu } function httpError(status, message) { const err = new Error(message) err.status = status return err } const kurz = (wert, max) => String(wert ?? '').slice(0, max) function zeitpunkt(at) { const ms = at instanceof Date ? at.getTime() : typeof at === 'string' && /^\d{4}-\d\d-\d\dT/.test(at) ? Date.parse(at) : Number.NaN if (Number.isNaN(ms)) throw httpError(400, 'Vertragsnachweis: Zeitpunkt fehlt oder ist ungültig') return new Date(ms).toISOString() } function pruefeTexte(texte) { if (!Array.isArray(texte) || texte.length === 0) throw httpError(400, 'Vertragsnachweis: Texte fehlen') for (const eintrag of texte) { const ok = eintrag !== null && typeof eintrag === 'object' && !Array.isArray(eintrag) && nichtLeer(eintrag.id) && nichtLeer(eintrag.text) && (eintrag.haken === undefined || typeof eintrag.haken === 'boolean') if (!ok) throw httpError(400, 'Vertragsnachweis: Text ungültig') } } /** * Nachweis der Bestellseiten anlegen. featureKey ist die Art, projectId und stripeSessionId bleiben leer (der Nachweis entsteht vor der * Handlung; die Zuordnung laeuft ueber Zeit, Konto und bezug). fassungen bekommt bezug und den Status des Wortlauts dazu. * Pruefungen vor dem Schreiben (400, nichts geschrieben); jeder Fehler beim Schreiben wird zu einem 500 mit FEHLER_NACHWEIS, die Ursache * steht nur im Log. * @param {{ customerId: string, art: 'abo'|'wechsel'|'speicher'|'gutschrift', bezug?: string, texte: Array<{ id: string, text: string, haken?: boolean }>, * fassungen: object, kundenart: 'privat'|'unternehmen', req: object, at: string|Date }} p * @returns {Promise} das angelegte Dokument */ export async function speichereVertragsNachweis({ customerId, art, bezug = '', texte, fassungen, kundenart, req, at } = {}) { if (!ARTEN.includes(art)) throw httpError(400, 'Vertragsnachweis: Art ungültig') if (!nichtLeer(customerId) || customerId.length > MAX_ID) throw httpError(400, 'Vertragsnachweis: Kunde fehlt oder ist ungültig') if (!KUNDENARTEN.includes(kundenart)) throw httpError(400, 'Vertragsnachweis: Kundenart ungültig') if (typeof bezug !== 'string') throw httpError(400, 'Vertragsnachweis: Bezug ungültig') if (fassungen === null || typeof fassungen !== 'object' || Array.isArray(fassungen)) throw httpError(400, 'Vertragsnachweis: Fassungen fehlen') pruefeTexte(texte) const acceptedAt = zeitpunkt(at) const fassungenJson = JSON.stringify({ ...fassungen, bezug, wortlaut: WORTLAUT_STATUS }) const texteJson = JSON.stringify(texte) if (fassungenJson.length > MAX_FASSUNGEN) throw httpError(400, 'Vertragsnachweis: Fassungen zu lang') if (texteJson.length > MAX_TEXTE) throw httpError(400, 'Vertragsnachweis: Texte zu lang') try { const person = actingPerson(req.session) const konto = person?.name && person?.email ? `${person.name} <${person.email}>` : person?.name || person?.email || '' return await createDocument(config.collections.featureConsents, { customerId, projectId: '', featureKey: art, stripeSessionId: '', acceptedAt, kundenart, fassungen: fassungenJson, texte: texteJson, textHash: texteHash(texte), ip: kurz(req.ip, MAX_IP), userAgent: kurz(req.get('user-agent'), MAX_USER_AGENT), konto: kurz(konto, MAX_KONTO), }) } catch (err) { console.error(`[vertragsnachweis] ${art} nicht gespeichert: ${err?.name ?? 'Fehler'} ${err?.status ?? ''} ${err?.message ?? ''}`.trim()) throw Object.assign(new Error(FEHLER_NACHWEIS), { status: 500 }) } } /** * Wie speichereVertragsNachweis, fuer die Routen: jeder Fehler (auch eine Pruefung der Eingaben) wird zu einem 500 mit FEHLER_NACHWEIS, * damit der Kunde nie einen internen Text sieht und die Handlung (Stripe-Sitzung, Wechsel, Speicheraenderung, Antrag) unterbleibt. */ export async function speichereNachweisFuerRoute(args) { try { return await speichereVertragsNachweis(args) } catch (err) { if (err?.status === 500 && err.message === FEHLER_NACHWEIS) throw err console.error(`[vertragsnachweis] Nachweis abgelehnt: ${err?.name ?? 'Fehler'} ${err?.status ?? ''} ${err?.message ?? ''}`.trim()) throw Object.assign(new Error(FEHLER_NACHWEIS), { status: 500 }) } }