Checkout je Funktion: Zustimmungsnachweis vor der Weiterleitung, Zustaende aus dem Register
Rest der Route POST /api/funktionen/:key/checkout unter der Sperre feature-order:<Kunde>:<Projekt>: Zustand aus dem Register (setup/active, disabled, ordered mit offener Sitzung, "Zahlung fortsetzen"), Stripe-Sitzung mit der Zeit des Klicks, Zustimmungsnachweis (Serverfassungen und -texte) vor markOrdered und vor der URL. Standard-Haken belehrung mit BELEHRUNG_PLATZHALTER fuer den Terminplaner. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1f6ae0f7e0
commit
7fcb268644
5 files changed
+534
-43
No files matched your search
+71
-23
@@ -6,14 +6,15 @@ import { getSessionCustomerId, requireSession } from '../middleware/session.js'
|
||||
import { withLocks } from '../services/keyedLock.js'
|
||||
import { createFeatureCheckout, retrieveCheckoutSession } from '../services/stripeBilling.js'
|
||||
import {
|
||||
DISABLED_MESSAGE,
|
||||
assertOwnProject,
|
||||
backfillInvoiceLinks,
|
||||
getFeatureDoc,
|
||||
hakenTexte,
|
||||
listCustomerFeatures,
|
||||
markOrdered,
|
||||
runActivation,
|
||||
} from '../services/siteFeatures.js'
|
||||
import { nachweisTexte, speichereZustimmung, zustimmungFuerSitzung, zustimmungsfassungen } from '../services/featureConsents.js'
|
||||
import { termineFuerKunde } from '../services/termine/schalter.js'
|
||||
import { featureConfig, featureHaken, featureSperre } from '../services/featureRegistry.js'
|
||||
import { bestellFehler, istFortsetzen, pruefeAbo, pruefeZustimmung } from '../services/featureOrder.js'
|
||||
@@ -120,9 +121,10 @@ router.get('/', requireSession, async (req, res) => {
|
||||
* Offene Bestellung: die gespeicherte Stripe-Sitzung entscheidet. open -> dieselbe Zahlungsseite nochmal
|
||||
* (z. B. nach "Zurueck" im Checkout), expired -> null (neue Sitzung), complete -> 409 (Zahlung laeuft
|
||||
* oder ist da, der Webhook richtet ein). Ist Stripe nicht erreichbar, gilt wie frueher: frisch (< 24 h) -> 409.
|
||||
* Die Meldung bei 409 steht im Register der Funktion (orderedConflictMessage).
|
||||
*/
|
||||
async function openOrderSession(doc) {
|
||||
const conflict = () => Object.assign(new Error('Die News-Pinnwand ist für dieses Projekt schon bestellt.'), { status: 409 })
|
||||
async function openOrderSession(doc, feature) {
|
||||
const conflict = () => Object.assign(new Error(feature.orderedConflictMessage), { status: 409 })
|
||||
if (!doc.stripeSessionId) return null
|
||||
let session
|
||||
try {
|
||||
@@ -137,11 +139,45 @@ async function openOrderSession(doc) {
|
||||
throw conflict()
|
||||
}
|
||||
|
||||
const ZUSTIMMUNG_NOETIG = 'Bitte stimme der Erklärung zu, um zu bestellen.'
|
||||
|
||||
/** "Name <E-Mail>" der handelnden Person (oder was davon da ist); das Speichern kuerzt auf die Feldgroesse. */
|
||||
const kontoLabel = (person) => (person?.name && person?.email ? `${person.name} <${person.email}>` : person?.name || person?.email || '')
|
||||
|
||||
/**
|
||||
* Zustimmungsnachweis zu einer Stripe-Sitzung speichern: die Serverfassungen und -texte der Funktion (nie Texte aus dem
|
||||
* Client), dazu die Belehrung aus dem Haken und die Zeit des Klicks. Jeder Fehler hier ist ein Serverfehler: die Route
|
||||
* antwortet mit dem allgemeinen Text (auch bei Pruefungen des Nachweises wie "Texte zu lang"), der Rohtext steht nur im Log.
|
||||
*/
|
||||
async function speichereNachweis({ feature, haken, customerId, projectId, sessionId, req, klickAt }) {
|
||||
const belehrung = (await haken.belehrung?.()) ?? null
|
||||
const texte = nachweisTexte(feature, { belehrung })
|
||||
try {
|
||||
await speichereZustimmung({
|
||||
customerId,
|
||||
projectId,
|
||||
featureKey: feature.key,
|
||||
stripeSessionId: sessionId,
|
||||
fassungen: zustimmungsfassungen(feature),
|
||||
texte,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
konto: kontoLabel(actingPerson(req.session)),
|
||||
at: klickAt,
|
||||
})
|
||||
} catch (err) {
|
||||
throw Object.assign(new Error(`Zustimmungsnachweis nicht gespeichert: ${err?.message}`), { status: 500, cause: err })
|
||||
}
|
||||
}
|
||||
|
||||
// Checkout je Funktion (Register config.features); /news/checkout ist derselbe Pfad mit key = news.
|
||||
// Vorpruefungen in der Reihenfolge der Spezifikation, jede Stufe ohne Seiteneffekt (keine Sperre, keine Stripe-Sitzung,
|
||||
// kein Dokument, kein Nachweis): 1 Kundenansicht, 2 Funktionsschluessel, 3 Zustimmung, 4 Projekt, 5 Schalter,
|
||||
// 5b Unterlagen, 6 Bestellpruefung, 7 Wartungs-Abo.
|
||||
router.post('/:key/checkout', requireSession, async (req, res) => {
|
||||
// Zeit des Klicks: Eingang der Anfrage, vor jeder Sperre und jedem Stripe-Aufruf. Sie ist der eine Zeitpunkt von Nachweis,
|
||||
// Stripe-Metadaten (consentAt) und Dokument und damit der Anker der Widerrufsfrist (UTC mit Z)
|
||||
const klickAt = new Date().toISOString()
|
||||
try {
|
||||
// 1 Kundenansicht
|
||||
if (req.session.viewAs) {
|
||||
@@ -163,7 +199,7 @@ router.post('/:key/checkout', requireSession, async (req, res) => {
|
||||
const customerId = getSessionCustomerId(req)
|
||||
await assertOwnProject(customerId, projectId)
|
||||
if (fortsetzen && (await getFeatureDoc(customerId, projectId, key))?.state !== 'ordered') {
|
||||
throw bestellFehler(400, 'Bitte stimme der Erklärung zu, um zu bestellen.', 'zustimmung_fehlt')
|
||||
throw bestellFehler(400, ZUSTIMMUNG_NOETIG, 'zustimmung_fehlt')
|
||||
}
|
||||
// 5 Schalter (Terminplaner: TERMINE_ENABLED und Pilotliste)
|
||||
const sperre = featureSperre(key, req.session.email)
|
||||
@@ -188,37 +224,49 @@ router.post('/:key/checkout', requireSession, async (req, res) => {
|
||||
// 7 Wartungs-Abo (nur Funktionen mit requiresWartungsAbo); die einzige Quelle von kein_abo | gekuendigt | nicht_aktiv
|
||||
await pruefeAbo(feature, customerId)
|
||||
|
||||
// Task B7 ersetzt den Rest: Sperre, Zustand, Stripe-Sitzung, Nachweis und markOrdered je Funktion aus dem Register.
|
||||
// Bis dahin laeuft nur News; fuer andere Funktionen entsteht weder Sitzung noch Dokument.
|
||||
if (key !== 'news') {
|
||||
throw bestellFehler(503, 'Die Bestellung ist für diese Funktion noch nicht freigeschaltet.', 'bestellung_nicht_freigeschaltet')
|
||||
}
|
||||
// Pruefen, Sitzung anlegen und eintragen als Ganzes je (Kunde, Projekt): zwei schnelle Klicks duerfen keine
|
||||
// zwei Sitzungen erzeugen, sonst ueberschreibt die zweite die ID der ersten und deren Zahlung findet kein Dokument
|
||||
// Zustand, Sitzung, Nachweis und Eintrag als Ganzes je (Kunde, Projekt): zwei schnelle Klicks duerfen keine zwei Sitzungen
|
||||
// erzeugen, sonst ueberschreibt die zweite die ID der ersten und deren Zahlung findet kein Dokument
|
||||
const session = await withLocks([`feature-order:${customerId}:${projectId}`], async () => {
|
||||
// Zustand vor dem Stripe-Aufruf pruefen: bezahlt/aktiv/abgeschaltet -> 409, eine offene Bestellung
|
||||
// richtet sich nach ihrer gespeicherten Stripe-Sitzung
|
||||
const existing = await getFeatureDoc(customerId, projectId, 'news')
|
||||
// a Zustand vor jedem Stripe-Aufruf: bezahlt/aktiv und abgeschaltet sind 409 (Meldungen aus dem Register)
|
||||
const existing = await getFeatureDoc(customerId, projectId, key)
|
||||
if (existing?.state === 'setup' || existing?.state === 'active') {
|
||||
throw Object.assign(new Error('Die News-Pinnwand ist für dieses Projekt schon bestellt.'), { status: 409 })
|
||||
throw Object.assign(new Error(feature.orderedConflictMessage), { status: 409 })
|
||||
}
|
||||
if (existing?.state === 'disabled') throw Object.assign(new Error(DISABLED_MESSAGE), { status: 409 })
|
||||
if (existing?.state === 'ordered') {
|
||||
const open = await openOrderSession(existing)
|
||||
if (open) return open
|
||||
if (existing?.state === 'disabled') throw Object.assign(new Error(feature.disabledMessage), { status: 409 })
|
||||
|
||||
const nachweis = (sessionId) => speichereNachweis({ feature, haken, customerId, projectId, sessionId, req, klickAt })
|
||||
|
||||
// b eine offene Bestellung richtet sich nach ihrer gespeicherten Stripe-Sitzung: offen -> dieselbe Zahlungsseite
|
||||
const open = existing?.state === 'ordered' ? await openOrderSession(existing, feature) : null
|
||||
if (open) {
|
||||
// d Liegt zur Sitzung noch kein Nachweis, wird er vor der URL nachgetragen. "Zahlung fortsetzen" (ohne Zustimmung)
|
||||
// gibt die URL nur her, wenn er schon liegt, sonst stimmt der Kunde im Formular neu zu
|
||||
if (!(await zustimmungFuerSitzung(open.id))) {
|
||||
if (fortsetzen) throw bestellFehler(409, ZUSTIMMUNG_NOETIG, 'zustimmung_fehlt')
|
||||
await nachweis(open.id)
|
||||
}
|
||||
return open
|
||||
}
|
||||
// keine offene Sitzung (abgelaufen, nie angelegt): "Zahlung fortsetzen" legt nie eine neue an, der Kunde stimmt im Formular neu zu
|
||||
if (fortsetzen) throw bestellFehler(409, ZUSTIMMUNG_NOETIG, 'zustimmung_fehlt')
|
||||
|
||||
// c neue Sitzung: die Stripe-Sitzung entsteht zuerst, weil der Nachweis ihre ID traegt
|
||||
const customer = await getDocument(config.collections.customers, customerId)
|
||||
const now = new Date().toISOString()
|
||||
const created = await createFeatureCheckout(customer, { projectId, key: 'news', consentAt: now })
|
||||
const created = await createFeatureCheckout(customer, { projectId, key, consentAt: klickAt })
|
||||
// d Nachweis vor allem anderen: scheitert er, gibt es keine URL und kein ordered (die unerreichbare Stripe-Sitzung laeuft
|
||||
// nach 24 h ab, ein Storno ist nicht noetig)
|
||||
await nachweis(created.id)
|
||||
// e erst nach dem Nachweis wird die Bestellung eingetragen
|
||||
await markOrdered({
|
||||
customerId,
|
||||
projectId,
|
||||
key: 'news',
|
||||
key,
|
||||
sessionId: created.id,
|
||||
consent: { text: config.funktionen.consentText, at: now, ip: req.ip, userAgent: req.get('user-agent') },
|
||||
consent: { text: hakenTexte(feature), at: klickAt, ip: req.ip, userAgent: req.get('user-agent') },
|
||||
})
|
||||
return created
|
||||
})
|
||||
// f die URL geht erst jetzt an den Browser
|
||||
res.json({ url: session.url })
|
||||
} catch (err) {
|
||||
fail(res, err, 'Die Bestellung konnte gerade nicht gestartet werden. Bitte versuch es später noch einmal oder schreib uns im Chat.')
|
||||
|
||||
@@ -25,10 +25,17 @@ async function termineBestellpruefung({ customerId, projectId, feature }) {
|
||||
return bestellpruefung({ einstellung, fragebogen, feature, mitAbo: false })
|
||||
}
|
||||
|
||||
/**
|
||||
* Platzhalter der Widerrufsbelehrung mit Muster-Widerrufsformular, die die Bestellseite vor dem Knopf zeigt und der
|
||||
* Zustimmungsnachweis speichert. Block F (Task F7) ersetzt den Haken belehrung durch widerrufsbelehrungFunktion().
|
||||
*/
|
||||
export const BELEHRUNG_PLATZHALTER = 'ENTWURF (Block F): Widerrufsbelehrung mit Muster-Widerrufsformular.'
|
||||
|
||||
/**
|
||||
* Standard-Haken je Funktion (Funktionen, nicht Daten). B6 fuellt verfuegbar, bestellpruefung und unterlagenBereit
|
||||
* (Standard immer bereit; F8 haengt die Rechtsunterlagen ein), B7 belehrung und B11 freischaltbar. News hat keine
|
||||
* Standard-Haken. Feste Funktionen, damit featureHaken bei jedem Aufruf dieselben liefert.
|
||||
* (Standard immer bereit; F8 haengt die Rechtsunterlagen ein), B7 belehrung (Text der Belehrung, der vor dem Knopf steht
|
||||
* und im Nachweis gespeichert wird) und B11 freischaltbar. News hat keine Standard-Haken, also auch keinen belehrung-Eintrag
|
||||
* im Nachweis. Feste Funktionen, damit featureHaken bei jedem Aufruf dieselben liefert.
|
||||
*/
|
||||
const STANDARD_HAKEN = {
|
||||
news: {},
|
||||
@@ -36,6 +43,7 @@ const STANDARD_HAKEN = {
|
||||
verfuegbar: (email) => termineFuerKunde(email),
|
||||
bestellpruefung: termineBestellpruefung,
|
||||
unterlagenBereit: async () => true,
|
||||
belehrung: async () => BELEHRUNG_PLATZHALTER,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -229,13 +229,24 @@ export async function paidFeatureOrders(customerId) {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Wortlaut der Zustimmung am Dokument (consentText): die Texte der Haken-Items der Funktion, durch eine Leerzeile getrennt.
|
||||
* Eine bequeme Kopie fuer das Team; massgeblich ist der vollstaendige Nachweis in featureConsents.
|
||||
*/
|
||||
export function hakenTexte(feature) {
|
||||
return feature.consent.items
|
||||
.filter((item) => item.haken)
|
||||
.map((item) => item.text)
|
||||
.join('\n\n')
|
||||
}
|
||||
|
||||
/** Bestellung eintragen (Zustand ordered); die Meldungen bei setup/active/disabled stehen im Register der Funktion. */
|
||||
export async function markOrdered({ customerId, projectId, key, sessionId, consent = {} }) {
|
||||
assertKey(key)
|
||||
const feature = featureConfig(key)
|
||||
const doc = await ensureFeatureDoc(customerId, projectId, key)
|
||||
if (doc.state === 'setup' || doc.state === 'active') {
|
||||
throw httpError(409, 'Diese Funktion ist bereits bestellt oder freigeschaltet')
|
||||
}
|
||||
if (doc.state === 'disabled') throw httpError(409, DISABLED_MESSAGE)
|
||||
if (doc.state === 'setup' || doc.state === 'active') throw httpError(409, feature.orderedConflictMessage)
|
||||
if (doc.state === 'disabled') throw httpError(409, feature.disabledMessage)
|
||||
return updateDocument(config.collections.portalFeatures, doc.$id, {
|
||||
featureKey: key,
|
||||
...stateFields('ordered'),
|
||||
|
||||
+401
-13
@@ -1,14 +1,17 @@
|
||||
import { test, after, beforeEach, mock } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||
import { startFakeStripe } from './support/fakeStripe.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
import { schemaKeys } from './support/schema.js'
|
||||
|
||||
// Checkout je Funktion, Teil 1 (Task B6): POST /api/funktionen/:key/checkout mit den Vorpruefungen in der
|
||||
// Reihenfolge der Spezifikation (1 Kundenansicht, 2 Schluessel, 3 Zustimmung, 4 Projekt, 5 Schalter, 5b Unterlagen,
|
||||
// 6 Bestellpruefung, 7 Wartungs-Abo). Jede Ablehnung laesst keine Spur zurueck: keine Stripe-Sitzung, kein
|
||||
// featureConsents- und kein portalFeatures-Dokument. Sperre, Zustandspruefung und Nachweis folgen in Task B7.
|
||||
// Checkout je Funktion (Tasks B6 und B7): POST /api/funktionen/:key/checkout.
|
||||
// Teil 1 (B6): Vorpruefungen in der Reihenfolge der Spezifikation (1 Kundenansicht, 2 Schluessel, 3 Zustimmung, 4 Projekt,
|
||||
// 5 Schalter, 5b Unterlagen, 6 Bestellpruefung, 7 Wartungs-Abo). Jede Ablehnung laesst keine Spur zurueck: keine
|
||||
// Stripe-Sitzung, kein featureConsents- und kein portalFeatures-Dokument.
|
||||
// Teil 2 (B7, ab "Sperre, Zustand, Nachweis"): Zustandspruefung, Stripe-Sitzung, Zustimmungsnachweis vor der Weiterleitung,
|
||||
// markOrdered; Wiederverwendung der offenen Sitzung, "Zahlung fortsetzen", parallele Klicks.
|
||||
const aw = await startAppwriteMock({
|
||||
schema: {
|
||||
portalFeatures: schemaKeys('portalFeatures'),
|
||||
@@ -26,6 +29,8 @@ const { default: funktionenRoutes } = await import('../server/routes/funktionen.
|
||||
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
const reg = await import('../server/services/featureRegistry.js')
|
||||
const order = await import('../server/services/featureOrder.js')
|
||||
const consents = await import('../server/services/featureConsents.js')
|
||||
const sf = await import('../server/services/siteFeatures.js')
|
||||
|
||||
const app = await startTestApp({ '/api/funktionen': funktionenRoutes })
|
||||
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
||||
@@ -514,20 +519,403 @@ test('Abo-Memo: je Anfrage ein aboMemo(), News fragt das Abo nie', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Zwischenstand bis Task B7: der Rest der Route (Sperre, Zustand, Stripe-Sitzung, Nachweis, markOrdered) gilt noch nur fuer News.
|
||||
// Eine Termine-Bestellung, die alle Vorpruefungen besteht, darf bis dahin weder Sitzung noch Dokument noch Nachweis anlegen;
|
||||
// B7 ersetzt diesen Test durch den Erfolgsfall.
|
||||
test('Termine-Bestellung nach bestandenen Vorpruefungen legt bis Task B7 noch nichts an (503)', async () => {
|
||||
// ------------------------------------------------------------------------------------------ Sperre, Zustand, Nachweis (B7) ---
|
||||
// Ab hier der Rest der Route: die Vorpruefungen sind bestanden (Fragebogen-Pruefung per Haken uebersprungen, Block C fuellt sie).
|
||||
const bereit = () => {
|
||||
T.bestellpruefung = async () => []
|
||||
}
|
||||
const featureDoc = (state, extra = {}) =>
|
||||
aw.seed('portalFeatures', { $id: 'f_p1_termine', customerId: 'c1', projectId: 'p1', featureKey: 'termine', state, enabled: state === 'active', siteKey: 'abcdefghijklmnop', ...extra })
|
||||
const sitzungen = () => [...st.checkoutSessions.values()]
|
||||
const nachweise = () => [...aw.docs('featureConsents').values()]
|
||||
const termineDoc = () => aw.docs('portalFeatures').get('f_p1_termine')
|
||||
const sha256 = (text) => createHash('sha256').update(text).digest('hex')
|
||||
const belehrungText = async (key) => (await reg.featureHaken(key).belehrung?.()) ?? null
|
||||
const erwarteteTexte = async (key) => consents.nachweisTexte(reg.featureConfig(key), { belehrung: await belehrungText(key) })
|
||||
const hakenTexte = (feature) => feature.consent.items.filter((item) => item.haken).map((item) => item.text).join('\n\n')
|
||||
|
||||
test('Nachweis steht vor der Weiterleitung: Serverfassungen und -texte in Reihenfolge, Hash, ein Zeitpunkt', async () => {
|
||||
bereit()
|
||||
const vor = new Date().toISOString()
|
||||
const res = await checkout('termine', bodyT())
|
||||
assert.equal(res.status, 503)
|
||||
assert.equal(res.body.code, 'bestellung_nicht_freigeschaltet')
|
||||
keineSpuren()
|
||||
const nach = new Date().toISOString()
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.deepEqual(Object.keys(res.body), ['url'])
|
||||
assert.equal(sitzungen().length, 1)
|
||||
const [session] = sitzungen()
|
||||
assert.equal(res.body.url, session.url)
|
||||
|
||||
assert.equal(nachweise().length, 1, 'genau ein Nachweis')
|
||||
const [n] = nachweise()
|
||||
assert.equal(n.stripeSessionId, session.id)
|
||||
assert.deepEqual([n.customerId, n.projectId, n.featureKey], ['c1', 'p1', 'termine'])
|
||||
assert.deepEqual(JSON.parse(n.texte), await erwarteteTexte('termine'), 'Texte in Anzeigereihenfolge')
|
||||
assert.deepEqual(JSON.parse(n.fassungen), consents.zustimmungsfassungen(T))
|
||||
assert.equal(n.textHash, sha256(n.texte), 'SHA-256 ueber die gespeicherten Texte')
|
||||
assert.equal(n.textHash, consents.texteHash(JSON.parse(n.texte)))
|
||||
assert.ok(n.ip, 'ip')
|
||||
assert.ok(n.userAgent, 'userAgent')
|
||||
assert.equal(n.konto, 'Kunde Eins <k@example.invalid>')
|
||||
|
||||
// ein Zeitpunkt: Nachweis, Stripe-Metadaten und Dokument tragen dieselbe Zeit des Klicks (Eingang der Anfrage)
|
||||
const doc = termineDoc()
|
||||
assert.equal(doc.state, 'ordered')
|
||||
assert.equal(doc.stripeSessionId, session.id)
|
||||
assert.equal(n.acceptedAt, session.metadata.consentAt)
|
||||
assert.equal(n.acceptedAt, doc.consentAt)
|
||||
assert.ok(vor <= n.acceptedAt && n.acceptedAt <= nach, `${vor} <= ${n.acceptedAt} <= ${nach}`)
|
||||
assert.match(n.acceptedAt, /Z$/, 'UTC-Zeit mit Z')
|
||||
assert.equal(doc.consentText, hakenTexte(T).slice(0, 1000), 'consentText aus den Haken-Texten')
|
||||
assert.equal(doc.consentIp, n.ip)
|
||||
assert.equal(doc.consentUserAgent, n.userAgent)
|
||||
})
|
||||
|
||||
test('Belehrung im Nachweis: Eintrag belehrung (haken false) direkt vor dem Knopf, News hat keinen', async () => {
|
||||
bereit()
|
||||
assert.equal(reg.BELEHRUNG_PLATZHALTER, 'ENTWURF (Block F): Widerrufsbelehrung mit Muster-Widerrufsformular.')
|
||||
assert.equal(await reg.featureHaken('termine').belehrung(), reg.BELEHRUNG_PLATZHALTER)
|
||||
assert.equal(reg.featureHaken('news').belehrung, undefined)
|
||||
await checkout('termine', bodyT())
|
||||
const texte = JSON.parse(nachweise()[0].texte)
|
||||
assert.deepEqual(texte.slice(-2), [
|
||||
{ id: 'belehrung', text: reg.BELEHRUNG_PLATZHALTER, haken: false },
|
||||
{ id: 'knopf', text: T.consent.knopf, haken: false },
|
||||
])
|
||||
assert.deepEqual(texte.map((x) => x.id), [...T.consent.items.map((x) => x.id), 'belehrung', 'knopf'])
|
||||
|
||||
const news = await checkout('news', { projectId: 'p3', consent: true }, customerCookie('c3'))
|
||||
assert.equal(news.status, 200)
|
||||
const newsNachweis = nachweise().find((d) => d.featureKey === 'news')
|
||||
assert.deepEqual(JSON.parse(newsNachweis.texte).map((x) => x.id), [...config.features.news.consent.items.map((x) => x.id), 'knopf'])
|
||||
})
|
||||
|
||||
test('Belehrungs-Haken ueberschreibbar: der gelieferte Text steht im Nachweis, null laesst den Eintrag weg', async () => {
|
||||
bereit()
|
||||
T.belehrung = async () => 'Eigene Belehrung.'
|
||||
await checkout('termine', bodyT())
|
||||
assert.deepEqual(JSON.parse(nachweise()[0].texte).at(-2), { id: 'belehrung', text: 'Eigene Belehrung.', haken: false })
|
||||
await deleteDocument('featureConsents', nachweise()[0].$id)
|
||||
await deleteDocument('portalFeatures', 'f_p1_termine')
|
||||
st.checkoutSessions.clear()
|
||||
T.belehrung = async () => null
|
||||
try {
|
||||
assert.equal((await checkout('termine', bodyT())).status, 200)
|
||||
assert.equal(JSON.parse(nachweise()[0].texte).some((x) => x.id === 'belehrung'), false)
|
||||
} finally {
|
||||
delete T.belehrung
|
||||
}
|
||||
})
|
||||
|
||||
test('Client-Texte zaehlen nicht: gespeichert werden die Registertexte und -fassungen', async () => {
|
||||
bereit()
|
||||
const res = await checkout('termine', bodyT({ texte: ['manipuliert'], fassungen: { zustimmung: 'x' }, konto: 'Fremd <f@x.de>', acceptedAt: '2000-01-01T00:00:00Z' }))
|
||||
assert.equal(res.status, 200)
|
||||
const [n] = nachweise()
|
||||
assert.deepEqual(JSON.parse(n.texte), await erwarteteTexte('termine'))
|
||||
assert.deepEqual(JSON.parse(n.fassungen), consents.zustimmungsfassungen(T))
|
||||
assert.equal(n.konto, 'Kunde Eins <k@example.invalid>')
|
||||
assert.ok(n.acceptedAt > '2020')
|
||||
assert.doesNotMatch(JSON.stringify(nachweise()), /manipuliert/)
|
||||
})
|
||||
|
||||
test('Fassungswechsel: alter Body 409, neuer Body 200 mit neuer Fassung im Nachweis', async () => {
|
||||
bereit()
|
||||
const alt = T.consent.version
|
||||
try {
|
||||
T.consent.version = 'ENTWURF-2099-01-01'
|
||||
const veraltet = await checkout('termine', bodyT({ zustimmung: { version: alt, haken: pflichtHaken() } }))
|
||||
assert.equal(veraltet.status, 409)
|
||||
assert.equal(veraltet.body.code, 'fassung_veraltet')
|
||||
keineSpuren()
|
||||
const neu = await checkout('termine', bodyT({ zustimmung: { version: 'ENTWURF-2099-01-01', haken: pflichtHaken() } }))
|
||||
assert.equal(neu.status, 200)
|
||||
assert.equal(JSON.parse(nachweise()[0].fassungen).zustimmung, 'ENTWURF-2099-01-01')
|
||||
} finally {
|
||||
T.consent.version = alt
|
||||
}
|
||||
})
|
||||
|
||||
test('ohne Nachweis keine URL: scheitert das Speichern, gibt es kein ordered, keine Weiterleitung', async () => {
|
||||
bereit()
|
||||
const log = stumm()
|
||||
try {
|
||||
aw.failNext({ collection: 'featureConsents', method: 'POST', status: 500, message: 'GEHEIMER-ROHTEXT interner Fehler' })
|
||||
const res = await checkout('termine', bodyT())
|
||||
assert.equal(res.status, 500)
|
||||
assert.equal(res.body.url, undefined)
|
||||
assert.doesNotMatch(JSON.stringify(res.body), /GEHEIMER-ROHTEXT|https?:/)
|
||||
assert.match(res.body.error, /Bestellung konnte gerade nicht gestartet werden/)
|
||||
assert.equal(nachweise().length, 0)
|
||||
const doc = termineDoc()
|
||||
assert.ok(!doc || doc.state === 'off', 'nicht ordered')
|
||||
assert.ok(!doc?.orderedAt, 'orderedAt leer')
|
||||
// die Stripe-Sitzung ist angelegt, aber nie an den Browser gegangen; sie laeuft nach 24 h ab
|
||||
assert.equal(sitzungen().length, 1)
|
||||
// ein erneuter Klick geht normal durch: neue Sitzung, ein Nachweis zu ihr
|
||||
const noch = await checkout('termine', bodyT())
|
||||
assert.equal(noch.status, 200)
|
||||
assert.equal(sitzungen().length, 2)
|
||||
assert.equal(nachweise().length, 1)
|
||||
assert.equal(nachweise()[0].stripeSessionId, sitzungen()[1].id)
|
||||
} finally {
|
||||
log.mock.restore()
|
||||
}
|
||||
})
|
||||
|
||||
test('Nachweis nicht speicherbar (zu lange Texte): 500 mit allgemeinem Text, kein Rohtext, kein ordered', async () => {
|
||||
bereit()
|
||||
const alt = T.consent.items[0].text
|
||||
const log = stumm()
|
||||
try {
|
||||
T.consent.items[0].text = 'x'.repeat(30000)
|
||||
const res = await checkout('termine', bodyT())
|
||||
assert.equal(res.status, 500)
|
||||
assert.doesNotMatch(JSON.stringify(res.body), /Zustimmungsnachweis|zu lang/)
|
||||
assert.equal(res.body.url, undefined)
|
||||
assert.ok(!termineDoc() || termineDoc().state === 'off')
|
||||
} finally {
|
||||
T.consent.items[0].text = alt
|
||||
log.mock.restore()
|
||||
}
|
||||
})
|
||||
|
||||
test('Datenbankfehler beim markOrdered: 500 ohne Rohtext, der Nachweis steht schon, das Dokument nicht ordered', async () => {
|
||||
bereit()
|
||||
const log = stumm()
|
||||
try {
|
||||
aw.failNext({ collection: 'portalFeatures', method: 'PATCH', status: 400, message: 'Invalid document structure: Attribute "stripeSessionId" has invalid type.' })
|
||||
const res = await checkout('termine', bodyT())
|
||||
assert.equal(res.status, 500, JSON.stringify(res.body))
|
||||
assert.match(res.body.error, /Bestellung konnte gerade nicht gestartet werden/)
|
||||
assert.doesNotMatch(JSON.stringify(res.body), /Invalid document structure|stripeSessionId|https?:/)
|
||||
assert.equal(nachweise().length, 1, 'Nachweis kommt vor markOrdered')
|
||||
assert.equal(termineDoc().state, 'off')
|
||||
} finally {
|
||||
log.mock.restore()
|
||||
}
|
||||
})
|
||||
|
||||
test('Wiederverwendung: zweiter Aufruf bei offener Sitzung gibt dieselbe URL, ohne zweite Sitzung und zweiten Nachweis', async () => {
|
||||
bereit()
|
||||
const erste = await checkout('termine', bodyT())
|
||||
const zweite = await checkout('termine', bodyT())
|
||||
assert.equal(zweite.status, 200)
|
||||
assert.equal(zweite.body.url, erste.body.url)
|
||||
assert.equal(sitzungen().length, 1)
|
||||
assert.equal(nachweise().length, 1)
|
||||
// die Wiederverwendung fasst das Dokument nicht an (orderedAt bleibt der Zeitpunkt der Bestellung)
|
||||
aw.seed('portalFeatures', { ...termineDoc(), orderedAt: '2026-01-01T00:00:00.000Z' })
|
||||
await checkout('termine', bodyT())
|
||||
assert.equal(termineDoc().orderedAt, '2026-01-01T00:00:00.000Z')
|
||||
assert.equal(termineDoc().state, 'ordered')
|
||||
})
|
||||
|
||||
test('Wiederverwendung ohne Nachweis: der Nachweis wird nachgetragen, bevor die URL zurueckkommt', async () => {
|
||||
bereit()
|
||||
const erste = await checkout('termine', bodyT())
|
||||
await deleteDocument('featureConsents', nachweise()[0].$id)
|
||||
assert.equal(nachweise().length, 0)
|
||||
const vor = new Date().toISOString()
|
||||
const zweite = await checkout('termine', bodyT())
|
||||
assert.equal(zweite.status, 200)
|
||||
assert.equal(zweite.body.url, erste.body.url)
|
||||
assert.equal(sitzungen().length, 1, 'keine zweite Stripe-Sitzung')
|
||||
assert.equal(nachweise().length, 1)
|
||||
assert.equal(nachweise()[0].stripeSessionId, sitzungen()[0].id)
|
||||
assert.ok(nachweise()[0].acceptedAt >= vor, 'Zeit des neuen Klicks')
|
||||
assert.deepEqual(JSON.parse(nachweise()[0].texte), await erwarteteTexte('termine'))
|
||||
|
||||
// scheitert das Nachtragen, kommt keine URL zurueck
|
||||
await deleteDocument('featureConsents', nachweise()[0].$id)
|
||||
const log = stumm()
|
||||
try {
|
||||
aw.failNext({ collection: 'featureConsents', method: 'POST', status: 500 })
|
||||
const kaputt = await checkout('termine', bodyT())
|
||||
assert.equal(kaputt.status, 500)
|
||||
assert.equal(kaputt.body.url, undefined)
|
||||
assert.equal(nachweise().length, 0)
|
||||
} finally {
|
||||
log.mock.restore()
|
||||
}
|
||||
})
|
||||
|
||||
test('abgelaufene Sitzung: neue Sitzung und neuer Nachweis (zwei Dokumente, zwei Sitzungs-IDs)', async () => {
|
||||
bereit()
|
||||
const erste = await checkout('termine', bodyT())
|
||||
sitzungen()[0].status = 'expired'
|
||||
const zweite = await checkout('termine', bodyT())
|
||||
assert.equal(zweite.status, 200)
|
||||
assert.notEqual(zweite.body.url, erste.body.url)
|
||||
assert.equal(sitzungen().length, 2)
|
||||
assert.equal(nachweise().length, 2)
|
||||
assert.equal(new Set(nachweise().map((n) => n.stripeSessionId)).size, 2)
|
||||
assert.deepEqual(nachweise().map((n) => n.stripeSessionId).sort(), sitzungen().map((s) => s.id).sort())
|
||||
assert.equal(termineDoc().stripeSessionId, sitzungen()[1].id)
|
||||
assert.equal(termineDoc().consentAt, sitzungen()[1].metadata.consentAt)
|
||||
})
|
||||
|
||||
test('parallele Klicks: eine Sitzung, ein Nachweis, beide 200 mit gleicher URL', async () => {
|
||||
bereit()
|
||||
const [a, b] = await Promise.all([checkout('termine', bodyT()), checkout('termine', bodyT())])
|
||||
assert.deepEqual([a.status, b.status], [200, 200])
|
||||
assert.equal(a.body.url, b.body.url)
|
||||
assert.equal(sitzungen().length, 1)
|
||||
assert.equal(nachweise().length, 1)
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------------------------------ Zustaende ---
|
||||
test('Zustaende: setup und active sind 409 orderedConflictMessage, disabled 409 disabledMessage, nirgends eine neue Sitzung', async () => {
|
||||
bereit()
|
||||
for (const [state, text] of [
|
||||
['setup', T.orderedConflictMessage],
|
||||
['active', T.orderedConflictMessage],
|
||||
['disabled', T.disabledMessage],
|
||||
]) {
|
||||
featureDoc(state)
|
||||
const vorher = spuren()
|
||||
const res = await checkout('termine', bodyT())
|
||||
assert.equal(res.status, 409, state)
|
||||
assert.equal(res.body.error, text, state)
|
||||
assert.equal(res.body.url, undefined, state)
|
||||
assert.deepEqual(spuren(), vorher, `${state}: nichts angelegt`)
|
||||
assert.equal(termineDoc().state, state, `${state}: unveraendert`)
|
||||
}
|
||||
// News: Meldungen aus dem Register der News
|
||||
const news = config.features.news
|
||||
aw.seed('portalFeatures', { $id: 'f_p3_news', customerId: 'c3', projectId: 'p3', featureKey: 'news', state: 'active', enabled: true, siteKey: 'abcdefghijklmnop' })
|
||||
const nCookie = customerCookie('c3')
|
||||
assert.equal((await checkout('news', { projectId: 'p3', consent: true }, nCookie)).body.error, news.orderedConflictMessage)
|
||||
aw.seed('portalFeatures', { $id: 'f_p3_news', customerId: 'c3', projectId: 'p3', featureKey: 'news', state: 'disabled', enabled: false, siteKey: 'abcdefghijklmnop' })
|
||||
assert.equal((await checkout('news', { projectId: 'p3', consent: true }, nCookie)).body.error, news.disabledMessage)
|
||||
assert.equal(sitzungen().length, 0)
|
||||
})
|
||||
|
||||
test('Zustand complete: 409, keine neue Sitzung, kein neuer Nachweis', async () => {
|
||||
bereit()
|
||||
await checkout('termine', bodyT())
|
||||
sitzungen()[0].status = 'complete'
|
||||
const res = await checkout('termine', bodyT())
|
||||
assert.equal(res.status, 409)
|
||||
assert.equal(res.body.error, T.orderedConflictMessage)
|
||||
assert.equal(sitzungen().length, 1)
|
||||
assert.equal(nachweise().length, 1)
|
||||
// auch "Zahlung fortsetzen" bekommt keine URL
|
||||
const fort = await checkout('termine', { projectId: 'p1', fortsetzen: true })
|
||||
assert.equal(fort.status, 409)
|
||||
assert.equal(fort.body.url, undefined)
|
||||
})
|
||||
|
||||
test('Stripe nicht erreichbar: frische Bestellung (< 24 h) ist 409, aeltere bekommt eine neue Sitzung samt Nachweis', async () => {
|
||||
bereit()
|
||||
const log = mock.method(console, 'warn', () => {})
|
||||
try {
|
||||
featureDoc('ordered', { stripeSessionId: 'cs_weg', orderedAt: new Date().toISOString() })
|
||||
const frisch = await checkout('termine', bodyT())
|
||||
assert.equal(frisch.status, 409)
|
||||
assert.equal(frisch.body.error, T.orderedConflictMessage)
|
||||
assert.deepEqual(spuren(), { sessions: 0, consents: 0, features: 1 }, 'nichts angelegt')
|
||||
featureDoc('ordered', { stripeSessionId: 'cs_weg', orderedAt: new Date(Date.now() - 2 * DAY_MS).toISOString() })
|
||||
const alt = await checkout('termine', bodyT())
|
||||
assert.equal(alt.status, 200)
|
||||
assert.equal(sitzungen().length, 1)
|
||||
assert.equal(nachweise().length, 1)
|
||||
assert.equal(termineDoc().stripeSessionId, sitzungen()[0].id)
|
||||
// ordered ohne gespeicherte Sitzungs-ID: ebenfalls eine neue Sitzung
|
||||
featureDoc('ordered', { stripeSessionId: '' })
|
||||
assert.equal((await checkout('termine', bodyT())).status, 200)
|
||||
assert.equal(sitzungen().length, 2)
|
||||
assert.equal(nachweise().length, 2)
|
||||
} finally {
|
||||
log.mock.restore()
|
||||
}
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------------------------------ Zahlung fortsetzen ---
|
||||
test('Zahlung fortsetzen: Dokument ordered mit Nachweis gibt dieselbe URL, ohne zweite Sitzung und zweiten Nachweis', async () => {
|
||||
bereit()
|
||||
const erste = await checkout('termine', bodyT())
|
||||
const fort = await checkout('termine', { projectId: 'p1', fortsetzen: true })
|
||||
assert.equal(fort.status, 200, JSON.stringify(fort.body))
|
||||
assert.equal(fort.body.url, erste.body.url)
|
||||
assert.equal(sitzungen().length, 1)
|
||||
assert.equal(nachweise().length, 1)
|
||||
})
|
||||
|
||||
test('Zahlung fortsetzen ohne Nachweis: 409 zustimmung_fehlt ohne URL, danach mit zustimmung 200 und der Nachweis ist wieder da', async () => {
|
||||
bereit()
|
||||
const erste = await checkout('termine', bodyT())
|
||||
await deleteDocument('featureConsents', nachweise()[0].$id)
|
||||
const fort = await checkout('termine', { projectId: 'p1', fortsetzen: true })
|
||||
assert.equal(fort.status, 409)
|
||||
assert.equal(fort.body.code, 'zustimmung_fehlt')
|
||||
assert.equal(fort.body.url, undefined)
|
||||
assert.doesNotMatch(JSON.stringify(fort.body), /https?:/)
|
||||
assert.equal(nachweise().length, 0, 'ohne Zustimmung entsteht kein Nachweis')
|
||||
assert.equal(sitzungen().length, 1)
|
||||
// der Kunde stimmt im Formular erneut zu
|
||||
const neu = await checkout('termine', bodyT())
|
||||
assert.equal(neu.status, 200)
|
||||
assert.equal(neu.body.url, erste.body.url)
|
||||
assert.equal(nachweise().length, 1)
|
||||
assert.equal(nachweise()[0].stripeSessionId, sitzungen()[0].id)
|
||||
assert.equal(sitzungen().length, 1)
|
||||
})
|
||||
|
||||
test('Zahlung fortsetzen bei abgelaufener Sitzung: 409 zustimmung_fehlt, mit zustimmung eine neue Sitzung', async () => {
|
||||
bereit()
|
||||
await checkout('termine', bodyT())
|
||||
sitzungen()[0].status = 'expired'
|
||||
const fort = await checkout('termine', { projectId: 'p1', fortsetzen: true })
|
||||
assert.equal(fort.status, 409)
|
||||
assert.equal(fort.body.code, 'zustimmung_fehlt')
|
||||
assert.equal(fort.body.url, undefined)
|
||||
assert.equal(sitzungen().length, 1, 'fortsetzen legt nie eine Sitzung an')
|
||||
assert.equal(nachweise().length, 1)
|
||||
const neu = await checkout('termine', bodyT())
|
||||
assert.equal(neu.status, 200)
|
||||
assert.equal(sitzungen().length, 2)
|
||||
assert.equal(nachweise().length, 2)
|
||||
})
|
||||
|
||||
test('Zahlung fortsetzen: ein Lesefehler beim Nachweis ist 500 ohne URL (im Zweifel keine Weiterleitung)', async () => {
|
||||
bereit()
|
||||
await checkout('termine', bodyT())
|
||||
const log = stumm()
|
||||
try {
|
||||
aw.failNext({ collection: 'featureConsents', method: 'GET', status: 500, message: 'GEHEIMER-ROHTEXT' })
|
||||
const fort = await checkout('termine', { projectId: 'p1', fortsetzen: true })
|
||||
assert.equal(fort.status, 500)
|
||||
assert.equal(fort.body.url, undefined)
|
||||
assert.doesNotMatch(JSON.stringify(fort.body), /GEHEIMER-ROHTEXT/)
|
||||
} finally {
|
||||
log.mock.restore()
|
||||
}
|
||||
})
|
||||
|
||||
test('Zahlung fortsetzen bei News: gleiche Regeln (Nachweis da, dieselbe URL)', async () => {
|
||||
const erste = await checkout('news', { projectId: 'p3', consent: true }, customerCookie('c3'))
|
||||
const fort = await checkout('news', { projectId: 'p3', fortsetzen: true }, customerCookie('c3'))
|
||||
assert.equal(fort.status, 200)
|
||||
assert.equal(fort.body.url, erste.body.url)
|
||||
assert.equal(nachweise().length, 1)
|
||||
assert.equal(sitzungen().length, 1)
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------------------------------ Meldungen aus dem Register ---
|
||||
test('markOrdered: Meldungen kommen aus dem Register der Funktion', async () => {
|
||||
for (const [key, feature] of [['news', config.features.news], ['termine', T]]) {
|
||||
for (const [state, text] of [['setup', feature.orderedConflictMessage], ['active', feature.orderedConflictMessage], ['disabled', feature.disabledMessage]]) {
|
||||
aw.seed('portalFeatures', { $id: `f_p1_${key}`, customerId: 'c1', projectId: 'p1', featureKey: key, state, enabled: state === 'active', siteKey: 'abcdefghijklmnop' })
|
||||
await assert.rejects(
|
||||
() => sf.markOrdered({ customerId: 'c1', projectId: 'p1', key, sessionId: 'cs_x', consent: {} }),
|
||||
(err) => err.status === 409 && err.message === text,
|
||||
`${key} ${state}`
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------------------------------ fortsetzen ---
|
||||
const featureDoc = (state, extra = {}) =>
|
||||
aw.seed('portalFeatures', { $id: 'f_p1_termine', customerId: 'c1', projectId: 'p1', featureKey: 'termine', state, enabled: state === 'active', siteKey: 'abcdefghijklmnop', ...extra })
|
||||
|
||||
test('fortsetzen: ohne Dokument oder ausser ordered ist 400 zustimmung_fehlt, es entsteht nichts', async () => {
|
||||
const fall = { projectId: 'p1', fortsetzen: true }
|
||||
|
||||
@@ -8,7 +8,7 @@ import { schemaKeys } from './support/schema.js'
|
||||
|
||||
// websiteProjects, workorders und customers legt das Setup-Skript nicht an: ohne Schema im Nachbau
|
||||
const aw = await startAppwriteMock({
|
||||
schema: { portalFeatures: schemaKeys('portalFeatures'), subscriptions: schemaKeys('subscriptions') },
|
||||
schema: { portalFeatures: schemaKeys('portalFeatures'), subscriptions: schemaKeys('subscriptions'), featureConsents: schemaKeys('featureConsents') },
|
||||
})
|
||||
const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
@@ -410,3 +410,39 @@ test('checkout_datenbankfehler_ohne_rohe_meldung', async () => {
|
||||
assert.doesNotMatch(res.body.error, /Invalid document structure|stripeSessionId/)
|
||||
assert.match(res.body.error, /Bestellung konnte gerade nicht gestartet werden/)
|
||||
})
|
||||
|
||||
// Task B7: auch die News-Bestellung schreibt vor der Weiterleitung einen Zustimmungsnachweis (Texte und Fassungen aus dem
|
||||
// Register config.features.news statt Literale: Block F2 hebt die Fassung ohne Testbruch an)
|
||||
test('news_schreibt_auch_einen_nachweis', async () => {
|
||||
aw.seed('websiteProjects', { $id: 'pn1', customerId: 'c1', projectName: 'Nachweis-Laden' })
|
||||
const news = config.features.news
|
||||
const vor = new Date().toISOString()
|
||||
const res = await checkout('pn1')
|
||||
const nach = new Date().toISOString()
|
||||
assert.equal(res.status, 200)
|
||||
const session = sessions().at(-1)
|
||||
const nachweise = [...aw.docs('featureConsents').values()].filter((d) => d.projectId === 'pn1')
|
||||
assert.equal(nachweise.length, 1)
|
||||
const [n] = nachweise
|
||||
assert.equal(n.stripeSessionId, session.id)
|
||||
assert.equal(n.featureKey, 'news')
|
||||
assert.deepEqual(JSON.parse(n.texte), [
|
||||
...news.consent.items.map((item) => ({ id: item.id, text: item.text, haken: Boolean(item.haken) })),
|
||||
{ id: 'knopf', text: news.consent.knopf, haken: false },
|
||||
])
|
||||
assert.deepEqual(JSON.parse(n.fassungen), { zustimmung: news.consent.version, leistungsbeschreibung: news.leistungsbeschreibung.version })
|
||||
assert.ok(n.ip && n.userAgent)
|
||||
assert.equal(n.konto, 'Kunde Eins <k@example.invalid>')
|
||||
// ein Zeitpunkt: Nachweis, Stripe-Metadaten und Dokument; das Dokument behaelt den Wortlaut wie bisher
|
||||
const doc = await sf.getFeatureDoc('c1', 'pn1', 'news')
|
||||
assert.equal(doc.consentText, config.funktionen.consentText)
|
||||
assert.equal(n.acceptedAt, doc.consentAt)
|
||||
assert.equal(n.acceptedAt, session.metadata.consentAt)
|
||||
assert.ok(vor <= n.acceptedAt && n.acceptedAt <= nach)
|
||||
// Widerrufs-Anker: nach der Zahlung zaehlt die Zeit des Klicks aus dem Nachweis, nicht die Zeit des Dokuments
|
||||
aw.seed('portalFeatures', { ...aw.docs('portalFeatures').get(doc.$id), orderedAt: '2026-01-01T00:00:00.000Z', consentAt: '2026-01-02T00:00:00.000Z' })
|
||||
assert.equal((await postWebhook(event('checkout.session.completed', paidSession(session.id, 'pn1')))).status, 200)
|
||||
const order = (await sf.paidFeatureOrders('c1')).find((o) => o.projekt === 'Nachweis-Laden')
|
||||
assert.equal(order.featureKey, 'news')
|
||||
assert.equal(order.vertragAt, n.acceptedAt)
|
||||
})
|
||||
Reference in new issue
Block a user