// Testdaten und Rahmen für die Ansichten von P2 (Kundenansicht neu). Formen laut server/routes/abo.js (GET /api/abo), // routes/customerData.js mit services/customerInvoices.js (Rechnungen), routes/projects.js, routes/chat.js und services/takeoverCredits.js. import { installFakeDom } from './fakeDom.js' const ANTWORTEN = 400 // so viele Antwortfunktionen legt der Fake-fetch bereit; danach antwortet fakeDom mit 200 {} // fakeDom schreibt target und ergebnisse in das Ereignis; ein echtes CustomEvent hat nur lesbare Felder. class EreignisAttrappe { constructor(typ, init = {}) { this.type = typ this.detail = init.detail ?? null this.bubbles = init.bubbles ?? false this.cancelable = init.cancelable ?? false this.target = null this.currentTarget = null this.defaultPrevented = false this.stopped = false this.ergebnisse = [] } preventDefault() { this.defaultPrevented = true } stopPropagation() { this.stopped = true } } // fakeDom kennt auf dem Dokument keine Ereignisse; Tests der Zähler-Meldung brauchen sie. function ereignisseFuerDokument(document) { const hoerer = new Map() document.addEventListener = (typ, fn) => { if (!hoerer.has(typ)) hoerer.set(typ, []); hoerer.get(typ).push(fn) } document.removeEventListener = (typ, fn) => { const l = hoerer.get(typ); if (l?.includes(fn)) l.splice(l.indexOf(fn), 1) } document.dispatchEvent = (ev) => { ev.target = document ev.currentTarget = document for (const fn of [...(hoerer.get(ev.type) ?? [])]) ev.ergebnisse.push(fn.call(document, ev)) return !ev.defaultPrevented } } function mitAttrappen(fx) { const vorher = Object.getOwnPropertyDescriptor(globalThis, 'CustomEvent') Object.defineProperty(globalThis, 'CustomEvent', { value: EreignisAttrappe, writable: true, configurable: true, enumerable: true }) ereignisseFuerDokument(fx.document) return () => { if (vorher) Object.defineProperty(globalThis, 'CustomEvent', vorher) else delete globalThis.CustomEvent } } const ohneQuery = (url) => String(url).split('?')[0] /** Antwort für einen Aufruf nachschlagen: 'METHOD url', url, 'METHOD pfad', pfad; unbekannt → 404. */ function beantworte(tabelle, aufruf) { const pfad = ohneQuery(aufruf.url) const schluessel = [`${aufruf.method} ${aufruf.url}`, aufruf.url, `${aufruf.method} ${pfad}`, pfad].find((k) => Object.hasOwn(tabelle, k)) if (!schluessel) return { status: 404, body: { error: 'unbekannt' } } const wert = tabelle[schluessel] return typeof wert === 'function' ? wert(aufruf) : { status: 200, body: wert } } /** Test mit Fake-DOM und Fake-fetch. tabelle: { 'GET /api/abo': Objekt | (aufruf) => ({ status, body }) }; der Test bekommt fx (siehe fakeDom.js) mit fx.tabelle. */ export const mit = (tabelle, fn) => async () => { const antworten = Array.from({ length: ANTWORTEN }, () => (aufruf) => beantworte(tabelle, aufruf)) const fx = installFakeDom({ antworten }) fx.tabelle = tabelle const zurueck = mitAttrappen(fx) try { return await fn(fx) } finally { zurueck() fx.restore() } } /** Wie mit() ohne Antworten und ohne Warten: fn(fx) läuft sofort (für synchrone Tests). */ export const mitDom = (fn) => { const fx = installFakeDom({ antworten: [] }) const zurueck = mitAttrappen(fx) try { return fn(fx) } finally { zurueck() fx.restore() } } /** Leere Wurzel (div) im Dokument. */ export const neueWurzel = (fx) => { const wurzel = fx.document.createElement('div') fx.document.body.append(wurzel) return wurzel } export const VORSCHAU = { prorationDate: 1760000000, amountDueCents: 3000, creditCents: 1500, monthlyCents: 5000, nextChargeAt: '2026-11-16T00:00:00.000Z', minTermEnd: '2027-10-16T00:00:00.000Z' } export const VORSCHAU_SPEICHER = { direction: 'down', units: 1, totalGb: 10, monthlyCents: 100, deltaMonthlyCents: -100, prorationCents: 0, nextChargeAt: '2026-11-16T00:00:00.000Z', effectiveAt: '2026-11-01T00:00:00.000Z' } export const LIMITS = { maxFiles: 3, maxFileBytes: 8388608, maxTotalBytes: 10485760 } /** Antwort von GET /api/auth/me für einen Kunden; over.customer ergänzt den Kunden, alles andere (viewAs, viewId …) die Antwort. */ export const me = ({ customer = {}, ...over } = {}) => ({ authenticated: true, role: 'customer', customer: { id: 'c1', code: 'K-1001', name: 'Anna Beispiel', companyName: '', email: 'anna@example.com', phone: '', location: '', customerStatus: 'customer', portalAccessEnabled: true, ...customer }, ...over, }) /** Eintrag aus GET /api/projects. */ export const projekt = (over = {}) => ({ id: 'p1', projectName: 'Bäckerei Muster', subdomain: 'baeckerei', previewUrl: 'https://baeckerei.project.webklar.com', liveDomain: '', status: 'ready', provisioningStatus: '', updatedAt: '2026-10-14T09:00:00.000Z', ...over, }) /** Eintrag aus GET /api/invoices (Stripe-Form, Beträge in Euro). */ export const rechnung = (over = {}) => ({ id: 'in_1', source: 'stripe', number: 'RE-2026-001', date: '2026-10-01', dueDate: '2026-10-15', amount: 50, balance: 0, status: 'Bezahlt', paid: true, cancelled: false, overdue: false, portalUrl: '', hasPdf: true, projekt: '', ...over, }) /** Nachricht aus GET /api/chat/messages. */ export const nachricht = (over = {}) => ({ id: 'm1', sender: 'customer', senderName: 'Anna Beispiel', text: 'Hallo, ich hätte eine Frage.', attachments: [], createdAt: '2026-10-14T08:30:00.000Z', ...over, }) /** Angebot (offerView) aus GET /api/abo → offers. */ export const angebot = (over = {}) => ({ id: 'o1', ref: 'A-1', createdAt: '2026-10-01T10:00:00.000Z', status: 'offen', scope: 'abo', productKey: 'wartung', months: 2, monthlyPriceCents: 2500, regularPriceCents: 5000, savingsCents: 5000, text: 'Erste 2 Monate je 25,00 € statt 50,00 €', validUntil: '2026-11-30T00:00:00.000Z', note: '', ...over, }) /** subscription aus GET /api/abo (publicSubscription). produkt: 'wartung' (Vorgabe) oder 'hosting'. */ export const abo = ({ produkt = 'wartung', ...over } = {}) => ({ status: 'active', active: true, paymentProblem: false, startedAt: '2026-09-01T10:00:00.000Z', currentPeriodEnd: '2026-11-01T00:00:00.000Z', cancelAtPeriodEnd: false, cancelAt: null, minTermEnd: produkt === 'hosting' ? '2027-09-01T10:00:00.000Z' : '2027-09-01T10:00:00.000Z', endedAt: null, priceAmount: produkt === 'hosting' ? 2000 : 5000, startChoice: null, serviceStartsAt: null, offerText: null, productKey: produkt, termsVersion: '2026-09-30-v5', termStartAt: null, termsUrl: '/abo-bedingungen.html', termsPdfUrl: '/WEBklar-Abo-Vertragsunterlagen.pdf', storage: { units: 0, pendingSetup: false, pendingReduction: null }, canChangeStorage: true, canSwitch: produkt === 'hosting', switchPending: null, withdrawals: [], withdrawalOpen: false, withdrawalUntil: null, ...over, }) /** Stundenkonto (computeLedger). */ export const ledger = (over = {}) => ({ balanceMinutes: 150, grantedMinutes: 240, usedMinutes: 90, overageMinutes: 0, expiredMinutes: 0, nextExpiry: { minutes: 150, at: '2027-01-01T00:00:00.000Z' }, credits: [{ grantedAt: '2026-10-01T00:00:00.000Z', expiresAt: '2027-01-01T00:00:00.000Z', minutes: 240, remaining: 150, expired: false }], usage: [{ date: '2026-10-10T00:00:00.000Z', minutes: 90, coveredMinutes: 90, overageMinutes: 0, description: 'Textänderungen' }], ...over, }) const PRODUKTE = [ { key: 'hosting', name: 'WEBklar Hosting-Abo', amountCents: 2000, minTermMonths: 12, minutesPerMonth: 0, features: [['Hosting', 'Deine Website online']] }, { key: 'wartung', name: 'WEBklar Wartungs-Abo', amountCents: 5000, minTermMonths: 12, minutesPerMonth: 60, features: [['Wartung', 'Änderungen und Pflege']] }, ] /** Ganze Antwort von GET /api/abo. over.subscription ersetzt das Abo (null = kein Abo). */ export const aboAntwort = (over = {}) => ({ available: true, testAccount: false, termsVersion: '2026-09-30-v5', lateStartAt: '2026-10-30T00:00:00.000Z', products: PRODUKTE.map((p) => ({ ...p, features: p.features.map((f) => [...f]) })), mailStorage: { includedGb: 5, gbPerUnit: 5, unitPriceCents: 100, maxUnits: 10 }, offers: { hosting: null, wartung: null }, ledger: null, subscription: null, ...over, }) /** Antrag aus GET /api/abo/takeover-credits → requests (customerView). */ export const anfrageGutschrift = (over = {}) => ({ id: 'g1', ref: 'G-1', createdAt: '2026-10-05T10:00:00.000Z', services: 'Domain', provider: 'Anbieter GmbH', domain: 'beispiel.de', invoiceAmountCents: 1200, billingMonths: 12, paidUntil: '2027-03-01T00:00:00.000Z', status: 'offen', question: '', amountCents: 0, decisionNote: '', ...over, })