245 lines
8.6 KiB
JavaScript
245 lines
8.6 KiB
JavaScript
// 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,
|
|
})
|