Terminplaner TP2: UI-Testserver Entwurf und Playwright Fragebogen bis Bestellklick

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 23:34:38 +00:00
1 parent 87d267e51a
commit 1ac142b92f
2 files changed
+876 -5

No files matched your search

+137 -5
View File
@@ -21,6 +21,14 @@
// startet ohne Einstellung (Vorlagenauswahl); der Server stellt auch dashboard.html und /api/funktionen bereit, die uebrigen
// Portal-Schnittstellen (/api/auth/me usw.) mockt das Skript per page.route.
//
// Entwurf und Bestellung (TP2, Fragebogen bis Bestellklick): srv.zuruecksetzen({ entwurf: true }) startet wie ein Kunde vor der ersten Branchenwahl:
// kein Funktionsdokument, keine Einstellung, aber ein laufendes Wartungs-Abo (wartungDoc) fuer c1 und ein Konto mit Firma und Anschrift
// (Vorschlag fuer die Betriebsangaben). srv.abo(zustand) tauscht das Abo ('wartung' | 'hosting' | 'keins' | 'gekuendigt' | 'nicht_aktiv'),
// srv.entwurf({ eingabe, fragebogen }) legt Funktionsdokument (off), Einstellung und Fragebogen an (Abkuerzung statt aller Klicks),
// srv.bestellt({ mitZustimmung }) macht daraus eine Bestellung im Zustand ordered mit offener Stripe-Sitzung (und dem Nachweis dieser Sitzung),
// srv.kundenansichtCookie() ist die Sitzung eines Mitarbeiters in der Kundenansicht (der Waechter haengt hier nicht: geprueft wird die
// Absicherung der Router selbst). srv.fragebogen() / srv.version() lesen den gespeicherten Stand.
//
// Seitenherkunft: srv.url ist http://127.0.0.1:PORT, srv.pageOrigin ist http://localhost:PORT (derselbe Server, anderer Host).
// Mit { crossOrigin: true } gilt pageOrigin als Herkunft des Portals (config.publicBaseUrl): Seiten von pageOrigin mit
// data-api=srv.url sind dann echte Cross-Origin-Anfragen (Preflight fuer POST), die der Server erlaubt.
@@ -118,6 +126,8 @@ export async function startTermineUiServer({ jetzt = JETZT, crossOrigin = false,
termineClaims: schemaKeys('termineClaims'),
termineOutbox: schemaKeys('termineOutbox'),
customerViewLog: schemaKeys('customerViewLog'),
subscriptions: schemaKeys('subscriptions'),
featureConsents: schemaKeys('featureConsents'),
},
datetimes: TERMINE_DATETIMES,
defaultLimit: 25,
@@ -128,14 +138,19 @@ export async function startTermineUiServer({ jetzt = JETZT, crossOrigin = false,
const { config } = await import('../../server/config.js')
const { startTestApp } = await import('../support/testApp.js')
const { setMailTransportForTests } = await import('../../server/services/mailer.js')
const { deleteDocument } = await import('../../server/services/appwriteAdmin.js')
const { deleteDocument, updateDocument } = await import('../../server/services/appwriteAdmin.js')
const { default: publicTermineRoutes, setzeUhrFuerTests: setzeUhrPublic, resetPublicTermineForTests } = await import('../../server/routes/publicTermine.js')
const { default: termineRoutes, setzeUhrFuerTests: setzeUhrKunde, resetTermineRoutesForTests } = await import('../../server/routes/termine.js')
const { default: funktionenRoutes } = await import('../../server/routes/funktionen.js')
const { normalisiereEinstellung } = await import('../../server/services/termine/einstellungen.js')
const { speichereEinstellung, resetKontextForTests } = await import('../../server/services/termine/kontext.js')
const { speichereEinstellung, speichereFragebogen, resetKontextForTests } = await import('../../server/services/termine/kontext.js')
const { resetOutboxForTests, warteAufVersand } = await import('../../server/services/termine/outbox.js')
const { resetTermineLimitsForTests } = await import('../../server/services/termine/limits.js')
const { SCHRITTE, leerFragebogen } = await import('../../server/services/termine/fragebogen.js')
const { featureConfig, featureHaken } = await import('../../server/services/featureRegistry.js')
const { hakenTexte, markOrdered } = await import('../../server/services/siteFeatures.js')
const { nachweisTexte, speichereZustimmung, zustimmungsfassungen } = await import('../../server/services/featureConsents.js')
const { hostingDoc, wartungDoc } = await import('../support/fixtures.js')
// Testseiten (HTML aus dem Skript) kommen vom Server selbst: Chromium lehnt Anfragen von Seiten, die Playwright per
// route.fulfill liefert, an 127.0.0.1 ab (Local Network Access); Seiten desselben Servers duerfen.
@@ -233,15 +248,123 @@ export async function startTermineUiServer({ jetzt = JETZT, crossOrigin = false,
return { name: roh.slice(0, i), value: roh.slice(i + 1), url: app.url }
},
/**
* Wartungs-Abo des Kunden c1 tauschen: 'wartung' (laeuft), 'hosting' (nur Hosting-Abo, Grund kein_abo), 'keins' (gar kein Abo, kein_abo),
* 'gekuendigt' (Kuendigung vorgemerkt, Grund gekuendigt) oder 'nicht_aktiv' (Zahlung offen, Grund nicht_aktiv).
*/
async abo(zustand = 'wartung') {
for (const id of [...aw.docs('subscriptions').keys()]) await deleteDocument('subscriptions', id)
const docs = {
wartung: [wartungDoc],
hosting: [hostingDoc],
keins: [],
gekuendigt: [{ ...wartungDoc, cancelAtPeriodEnd: true }],
nicht_aktiv: [{ ...wartungDoc, status: 'past_due' }],
}[zustand]
if (!docs) throw new Error(`Unbekanntes Abo: ${zustand}`)
for (const doc of docs) aw.seed('subscriptions', doc)
},
/** Cookie einer Kundenansicht (Mitarbeiter Andrej sieht das Portal von c1) fuer page.context().addCookies([...]): { name, value, url }. */
async kundenansichtCookie({ customerId = KUNDE } = {}) {
const { kundenansichtCookie } = await import('../support/fixtures.js')
const roh = kundenansichtCookie({ customerId })
const i = roh.indexOf('=')
return { name: roh.slice(0, i), value: roh.slice(i + 1), url: app.url }
},
/**
* Entwurf wie nach der Branchenwahl, ohne alle Klicks: Funktionsdokument im Zustand off (mit siteKey), Einstellung (Vorgabe: salonEingabe,
* gespeichert als Entwurf, also auch ohne Pflichtangaben fuer Gaeste) und Fragebogen (Vorgabe: Branche salon, nichts bestaetigt; fragebogen
* ueberschreibt einzelne Felder, z. B. { bestaetigt: srv.SCHRITTE, bundeslandGewaehlt: true }).
*/
async entwurf({ eingabe = salonEingabe(), fragebogen = {} } = {}) {
const erg = normalisiereEinstellung(eingabe, { now: uhr(), entwurf: true })
if (erg.fehler) throw new Error(`Einstellung ungueltig: ${JSON.stringify(erg.fehler)}`)
resetKontextForTests()
aw.seed('portalFeatures', srv.featureDoc('off'))
const doc = aw.docs('termineConfig').get(FEATURE)
await speichereEinstellung(srv.featureDoc('off'), erg.einstellung, { version: doc ? doc.version : 0, now: uhr() })
await speichereFragebogen(srv.featureDoc('off'), () => ({ ...leerFragebogen(), branche: 'salon', ...fragebogen }), { by: `kunde:${KUNDE}`, now: uhr() })
},
/** Gespeicherter Fragebogen (Objekt) oder null; Version der gespeicherten Einstellung (0 ohne Dokument). */
fragebogen() {
const doc = aw.docs('termineConfig').get(FEATURE)
return doc?.fragebogen ? JSON.parse(doc.fragebogen) : null
},
version() { return aw.docs('termineConfig').get(FEATURE)?.version ?? 0 },
SCHRITTE,
/**
* Bestellung im Zustand ordered, wie sie der Checkout hinterlaesst: eine offene Stripe-Sitzung im Nachbau, das Funktionsdokument ordered
* (markOrdered) und, mit mitZustimmung, der Zustimmungsnachweis dieser Sitzung (Texte des Registers samt Belehrung, Fassungen, Zeit).
* Setzt einen Entwurf voraus (srv.entwurf). Liefert die ID der Sitzung.
*/
async bestellt({ sitzungId = 'cs_test_ui_bestellt', mitZustimmung = true } = {}) {
const register = featureConfig('termine')
st.checkoutSessions.set(sitzungId, {
id: sitzungId,
object: 'checkout.session',
url: `https://checkout.stripe.test/c/pay/${sitzungId}`,
mode: 'payment',
customer: null,
client_reference_id: KUNDE,
subscription: null,
metadata: { customerId: KUNDE, projectId: PROJEKT, featureKey: 'termine' },
status: 'open',
payment_status: 'unpaid',
invoice: null,
})
const klickAt = uhr().toISOString()
await markOrdered({ customerId: KUNDE, projectId: PROJEKT, key: 'termine', sessionId: sitzungId, consent: { text: hakenTexte(register), at: klickAt, ip: '127.0.0.1', userAgent: 'ui-test' } })
if (mitZustimmung) {
await speichereZustimmung({
customerId: KUNDE,
projectId: PROJEKT,
featureKey: 'termine',
stripeSessionId: sitzungId,
fassungen: zustimmungsfassungen(register),
texte: nachweisTexte(register, { belehrung: (await featureHaken('termine').belehrung?.()) ?? null }),
ip: '127.0.0.1',
userAgent: 'ui-test',
konto: 'Silke Müller <silke@salon-mueller.example>',
at: klickAt,
})
}
resetKontextForTests()
return sitzungId
},
/**
* Bestellung bezahlt, Einrichtungs-Ticket angelegt: das Funktionsdokument im Zustand setup mit Ticketnummer (Ticket im Nachbau, noch nicht
* geschlossen, sonst schaltete GET /api/funktionen frei) und Rechnungslink. Setzt ein Funktionsdokument voraus (srv.entwurf, srv.bestellt).
*/
async einrichtung({ woid = 100342, invoiceUrl = 'https://invoice.stripe.test/i/in_ui1' } = {}) {
aw.seed('workorders', { $id: 't_ui1', woid, status: 'In Progress' })
await updateDocument('portalFeatures', FEATURE, { state: 'setup', enabled: false, purchaseStatus: 'paid', ticketId: 't_ui1', invoiceUrl })
resetKontextForTests()
},
/** Zustimmungsnachweise loeschen (ein ordered ohne Nachweis: der Zustand nach einer abgelaufenen Sitzung oder einem Fehler). */
async loescheZustimmungen() {
for (const id of [...aw.docs('featureConsents').keys()]) await deleteDocument('featureConsents', id)
},
/**
* Alles auf Anfang: Dokumente loeschen, Zaehler und Zwischenspeicher leeren, neue Senke, Einstellung und Funktion neu anlegen.
* ohneEinstellung: die Funktion ist aktiv, aber es gibt noch keine Einstellung (erstes Oeffnen im Portal: Vorlage waehlen).
* entwurf: ein Kunde vor der ersten Branchenwahl (TP2): kein Funktionsdokument, keine Einstellung, ein laufendes Wartungs-Abo fuer c1 und ein
* Konto mit Firma und Anschrift; die Stripe-Sitzungen und Aufrufe des Nachbaus sind leer.
*/
async zuruecksetzen({ eingabe, uhrNeu = true, ohneEinstellung = false } = {}) {
async zuruecksetzen({ eingabe, uhrNeu = true, ohneEinstellung = false, entwurf = false } = {}) {
await warteAufVersand()
for (const collection of ['portalFeatures', 'termineConfig', 'termineBookings', 'termineClaims', 'termineOutbox', 'customers', 'websiteProjects']) {
const sammlungen = ['portalFeatures', 'termineConfig', 'termineBookings', 'termineClaims', 'termineOutbox', 'customers', 'websiteProjects', 'subscriptions', 'featureConsents', 'workorders']
for (const collection of sammlungen) {
for (const id of [...aw.docs(collection).keys()]) await deleteDocument(collection, id)
}
st.checkoutSessions.clear()
st.calls.length = 0
resetKontextForTests()
resetOutboxForTests()
resetTermineLimitsForTests()
@@ -259,7 +382,16 @@ export async function startTermineUiServer({ jetzt = JETZT, crossOrigin = false,
srv.sink = mailSink()
setMailTransportForTests(srv.sink)
aw.seed('websiteProjects', { $id: PROJEKT, customerId: KUNDE, projectName: 'Salon Müller', subdomain: 'salon', liveDomain: 'salon-mueller.example' })
aw.seed('customers', { $id: KUNDE, name: 'Silke Müller', email: 'silke@salon-mueller.example' })
aw.seed('customers', {
$id: KUNDE,
name: 'Silke Müller',
email: 'silke@salon-mueller.example',
...(entwurf ? { companyName: 'Salon Müller', street: 'Hauptstr. 1', postalCode: '67722', city: 'Winnweiler' } : {}),
})
if (entwurf) {
await srv.abo('wartung')
return
}
aw.seed('portalFeatures', srv.featureDoc())
if (!ohneEinstellung) await srv.speichere(anpassen(eingabe ?? salonEingabe()))
},