Files
Webklar-Kundenbereich/server/services/vertragsNachweis.js
T

115 lines
5.5 KiB
JavaScript

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<object>} 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 })
}
}