Kundenansicht P5: Kundenart im Zustimmungsnachweis
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4bf1b67e67
commit
b000dba698
2 files changed
+64
-3
No files matched your search
@@ -5,6 +5,7 @@
|
|||||||
import { createHash } from 'node:crypto'
|
import { createHash } from 'node:crypto'
|
||||||
import { config } from '../config.js'
|
import { config } from '../config.js'
|
||||||
import { createDocument, listDocuments, Query } from './appwriteAdmin.js'
|
import { createDocument, listDocuments, Query } from './appwriteAdmin.js'
|
||||||
|
import { KUNDENARTEN } from './kundenart.js'
|
||||||
|
|
||||||
// Grenzen der Collection (scripts/feature-setup.mjs, FEATURE_CONSENT_ATTRIBUTES)
|
// Grenzen der Collection (scripts/feature-setup.mjs, FEATURE_CONSENT_ATTRIBUTES)
|
||||||
const MAX_ID = 36
|
const MAX_ID = 36
|
||||||
@@ -74,9 +75,11 @@ export function texteHash(texte) {
|
|||||||
* Nachweis anfuegen. fassungen: Name => Fassung (z. B. zustimmung, leistungsbeschreibung, avv); texte: [{ id, text, haken }]
|
* Nachweis anfuegen. fassungen: Name => Fassung (z. B. zustimmung, leistungsbeschreibung, avv); texte: [{ id, text, haken }]
|
||||||
* genau in Anzeigereihenfolge. Erlaubt leer: stripeSessionId, ip, userAgent (Nachweis der Aktivierung durch das Team) und
|
* genau in Anzeigereihenfolge. Erlaubt leer: stripeSessionId, ip, userAgent (Nachweis der Aktivierung durch das Team) und
|
||||||
* haken je Text. Wirft 400 (und schreibt nichts), wenn etwas fehlt, ungueltig oder zu lang ist.
|
* haken je Text. Wirft 400 (und schreibt nichts), wenn etwas fehlt, ungueltig oder zu lang ist.
|
||||||
|
* kundenart (optional, 'unternehmen' | 'privat'): Kundenart zum Klickzeitpunkt (V4); das Feld wird nur geschrieben, wenn es
|
||||||
|
* uebergeben wurde (ohne Setup-Skript kennt Appwrite es nicht), jeder andere uebergebene Wert ist ein 400.
|
||||||
* @returns {Promise<object>} das angelegte Dokument
|
* @returns {Promise<object>} das angelegte Dokument
|
||||||
*/
|
*/
|
||||||
export async function speichereZustimmung({ customerId, projectId, featureKey, stripeSessionId, fassungen, texte, ip, userAgent, konto, at } = {}) {
|
export async function speichereZustimmung({ customerId, projectId, featureKey, stripeSessionId, fassungen, texte, ip, userAgent, konto, at, kundenart } = {}) {
|
||||||
const daten = {
|
const daten = {
|
||||||
customerId: kennung(customerId, MAX_ID, 'Kunde'),
|
customerId: kennung(customerId, MAX_ID, 'Kunde'),
|
||||||
projectId: kennung(projectId, MAX_ID, 'Projekt'),
|
projectId: kennung(projectId, MAX_ID, 'Projekt'),
|
||||||
@@ -90,8 +93,10 @@ export async function speichereZustimmung({ customerId, projectId, featureKey, s
|
|||||||
const texteJson = JSON.stringify(texte)
|
const texteJson = JSON.stringify(texte)
|
||||||
if (fassungenJson.length > MAX_FASSUNGEN) throw httpError(400, 'Zustimmungsnachweis: Fassungen zu lang')
|
if (fassungenJson.length > MAX_FASSUNGEN) throw httpError(400, 'Zustimmungsnachweis: Fassungen zu lang')
|
||||||
if (texteJson.length > MAX_TEXTE) throw httpError(400, 'Zustimmungsnachweis: Texte zu lang')
|
if (texteJson.length > MAX_TEXTE) throw httpError(400, 'Zustimmungsnachweis: Texte zu lang')
|
||||||
|
if (kundenart !== undefined && !KUNDENARTEN.includes(kundenart)) throw httpError(400, 'Zustimmungsnachweis: Kundenart ungültig')
|
||||||
return createDocument(config.collections.featureConsents, {
|
return createDocument(config.collections.featureConsents, {
|
||||||
...daten,
|
...daten,
|
||||||
|
...(kundenart !== undefined ? { kundenart } : {}),
|
||||||
fassungen: fassungenJson,
|
fassungen: fassungenJson,
|
||||||
texte: texteJson,
|
texte: texteJson,
|
||||||
textHash: sha256(texteJson),
|
textHash: sha256(texteJson),
|
||||||
@@ -180,9 +185,10 @@ const kontoLabel = (person) => (person?.name && person?.email ? `${person.name}
|
|||||||
* steht nur im Log. Ohne sessionId (Gratisweg) traegt der Nachweis keine Stripe-Sitzung.
|
* steht nur im Log. Ohne sessionId (Gratisweg) traegt der Nachweis keine Stripe-Sitzung.
|
||||||
* @param {object} req Express-Anfrage (ip, User-Agent, Sitzung)
|
* @param {object} req Express-Anfrage (ip, User-Agent, Sitzung)
|
||||||
* @param {{ feature: object, haken?: object, customerId: string, projectId: string, sessionId?: string, klickAt: string }} p
|
* @param {{ feature: object, haken?: object, customerId: string, projectId: string, sessionId?: string, klickAt: string }} p
|
||||||
* feature: Registereintrag, beim Gratisweg freiweg(feature) (Zustimmung consentFrei)
|
* feature: Registereintrag, beim Gratisweg freiweg(feature) (Zustimmung consentFrei), im Abo-Modell aboweg(feature)
|
||||||
|
* kundenart (optional): Kundenart zum Klickzeitpunkt, wird nur durchgereicht, wenn sie uebergeben wurde
|
||||||
*/
|
*/
|
||||||
export async function speichereNachweisAusAnfrage(req, { feature, haken = {}, customerId, projectId, sessionId, klickAt }) {
|
export async function speichereNachweisAusAnfrage(req, { feature, haken = {}, customerId, projectId, sessionId, klickAt, kundenart }) {
|
||||||
const belehrung = (await haken.belehrung?.()) ?? null
|
const belehrung = (await haken.belehrung?.()) ?? null
|
||||||
const texte = nachweisTexte(feature, { belehrung })
|
const texte = nachweisTexte(feature, { belehrung })
|
||||||
try {
|
try {
|
||||||
@@ -197,6 +203,7 @@ export async function speichereNachweisAusAnfrage(req, { feature, haken = {}, cu
|
|||||||
userAgent: req.get('user-agent'),
|
userAgent: req.get('user-agent'),
|
||||||
konto: kontoLabel(actingPerson(req.session)),
|
konto: kontoLabel(actingPerson(req.session)),
|
||||||
at: klickAt,
|
at: klickAt,
|
||||||
|
...(kundenart !== undefined ? { kundenart } : {}),
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw Object.assign(new Error(`Zustimmungsnachweis nicht gespeichert: ${err?.message}`), { status: 500, cause: err })
|
throw Object.assign(new Error(`Zustimmungsnachweis nicht gespeichert: ${err?.message}`), { status: 500, cause: err })
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { test, after } from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||||
|
import { startFakeStripe } from './support/fakeStripe.js'
|
||||||
|
import { useTestEnv } from './support/testEnv.js'
|
||||||
|
import { schemaKeys } from './support/schema.js'
|
||||||
|
|
||||||
|
// Kundenart im Zustimmungsnachweis (V4): nur geschrieben, wenn uebergeben; ungueltig ist 400 ohne Schreibzugriff
|
||||||
|
const schema = { featureConsents: schemaKeys('featureConsents') } // bleibt aenderbar wie in featureSetup.test.js
|
||||||
|
const aw = await startAppwriteMock({ schema })
|
||||||
|
const st = await startFakeStripe()
|
||||||
|
after(() => Promise.all([aw.close(), st.close()]))
|
||||||
|
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||||
|
const reg = await import('../server/services/featureRegistry.js')
|
||||||
|
const { speichereZustimmung, speichereNachweisAusAnfrage } = await import('../server/services/featureConsents.js')
|
||||||
|
|
||||||
|
const base = { customerId: 'c1', projectId: 'p1', featureKey: 'termine', stripeSessionId: '', fassungen: { zustimmung: 'x' }, texte: [{ id: 'knopf', text: 'Einschalten', haken: false }], ip: '203.0.113.9', userAgent: 'UA/1', konto: 'k', at: '2026-10-17T08:00:00.000Z' }
|
||||||
|
|
||||||
|
test('kundenart wird gespeichert; ohne Angabe fehlt der Schlüssel, auch bei altem Schema', async () => {
|
||||||
|
assert.equal((await speichereZustimmung({ ...base, kundenart: 'privat' })).kundenart, 'privat')
|
||||||
|
assert.equal((await speichereZustimmung({ ...base, kundenart: 'unternehmen' })).kundenart, 'unternehmen')
|
||||||
|
schema.featureConsents = schemaKeys('featureConsents').filter((k) => k !== 'kundenart') // Setup-Skript noch nicht gelaufen
|
||||||
|
assert.equal(Object.hasOwn(await speichereZustimmung(base), 'kundenart'), false)
|
||||||
|
await assert.rejects(() => speichereZustimmung({ ...base, kundenart: 'privat' }), /Unknown attribute/)
|
||||||
|
schema.featureConsents = schemaKeys('featureConsents')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ungültige Kundenart: 400, nichts geschrieben', async () => {
|
||||||
|
const vorher = aw.docs('featureConsents').size
|
||||||
|
for (const kundenart of ['firma', '', 'Privat', 3, null]) {
|
||||||
|
await assert.rejects(() => speichereZustimmung({ ...base, kundenart }), (e) => e.status === 400 && /Kundenart ungültig/.test(e.message), String(kundenart))
|
||||||
|
}
|
||||||
|
assert.equal(aw.docs('featureConsents').size, vorher)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Nachweis aus der Anfrage im Abo-Modell: Zeile, Hinweis, optional Häkchen, Knopf; Fassungen ohne widerruf', async () => {
|
||||||
|
const req = { ip: '203.0.113.9', get: (h) => (h === 'user-agent' ? 'UA/1' : ''), session: { name: 'Kunde Eins', email: 'k@example.invalid' } }
|
||||||
|
const feature = reg.featureConfig('termine')
|
||||||
|
await speichereNachweisAusAnfrage(req, { feature: reg.aboweg(feature), customerId: 'c1', projectId: 'p1', klickAt: '2026-10-17T08:00:00.000Z', kundenart: 'unternehmen' })
|
||||||
|
await speichereNachweisAusAnfrage(req, { feature: reg.aboweg(feature, { beginn: true }), customerId: 'c1', projectId: 'p2', klickAt: '2026-10-17T08:00:00.000Z', kundenart: 'privat' })
|
||||||
|
const [a, b] = [...aw.docs('featureConsents').values()].filter((d) => d.featureKey === 'termine' && d.projectId.startsWith('p')).slice(-2)
|
||||||
|
assert.deepEqual(JSON.parse(a.texte).map((t) => t.id), ['einschalten-zeile', 'hinweis', 'knopf']) // termine trägt den Hinweis (Pflichten der alten Häkchen)
|
||||||
|
assert.equal(JSON.parse(a.texte)[1].text, reg.consentAbo('termine').items[1].text)
|
||||||
|
assert.deepEqual(JSON.parse(b.texte).map((t) => [t.id, t.haken]), [['einschalten-zeile', false], ['hinweis', false], ['beginn', true], ['knopf', false]])
|
||||||
|
assert.deepEqual(Object.keys(JSON.parse(a.fassungen)).sort(), ['avv', 'leistungsbeschreibung', 'zustimmung'])
|
||||||
|
assert.deepEqual([a.kundenart, b.kundenart, a.konto, a.stripeSessionId], ['unternehmen', 'privat', 'Kunde Eins <k@example.invalid>', ''])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Nachweis aus der Anfrage ohne kundenart: Schlüssel fehlt (alter Weg bleibt)', async () => {
|
||||||
|
const req = { ip: '203.0.113.9', get: () => '', session: { name: 'Kunde Eins', email: 'k@example.invalid' } }
|
||||||
|
await speichereNachweisAusAnfrage(req, { feature: reg.featureConfig('news'), customerId: 'c1', projectId: 'p9', klickAt: '2026-10-17T08:00:00.000Z' })
|
||||||
|
const doc = [...aw.docs('featureConsents').values()].find((d) => d.projectId === 'p9')
|
||||||
|
assert.equal(Object.hasOwn(doc, 'kundenart'), false)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user