Kundenansicht neu P2: Übernahmegutschrift im Wortlaut

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 17:11:15 +00:00
1 parent 1b66f6aa81
commit d99c29b6c6
2 files changed
+284 -4

No files matched your search

+221 -4
View File
@@ -1,7 +1,224 @@
/* Kundenansicht neu: Platzhalter, den das nächste Paket füllt (Stub aus P1). */
/* Kundenansicht neu (P2): Übernahmegutschrift beantragen (#gutschrift), Text und Logik 1:1 aus app.js (loadTakeoverCard und Helfer,
Fassung 5, Sie-Form, C14). Antrag stellen, Stand sehen, Rückfragen beantworten. Kundentext (Rückfrage, Entscheidung, Anbieter, Domain)
kommt nur als Text in die Seite. Das Wortlaut-Orakel prüft den sichtbaren Text, der Hash-Pin in legalTexts.test.js den Block der Bedingungen. */
import { h } from './dom.js'
import { api } from './api.js'
import { h, leeren } from './dom.js'
import { K, ersetze, zuAnsicht, ladeAbo, fmtDatum, fmtEuro, meldeZaehlerNeu, ABO_NICHT_GELADEN } from './ansicht-hilfen.js'
export async function zeigeGutschrift(wurzel, { route, me, nav }) {
wurzel.replaceChildren(h('p', { class: 'kn-leer' }, 'Übernahmegutschrift (folgt)'))
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs
const TEXT_ANSICHTSMODUS = 'Im Ansichtsmodus kann kein Antrag gestellt werden.'
const GRENZEN_VORGABE = { maxFiles: 3, maxFileBytes: 8388608, maxTotalBytes: 10485760 }
const TAKEOVER_STATUS = {
offen: ['In Prüfung', 'orange'],
rueckfrage: ['Rückfrage', 'violett'],
genehmigt: ['Genehmigt', 'gruen'],
abgelehnt: ['Nicht genehmigt', 'grau'],
}
// Bei Textänderungen config.takeover.conditionsVersion auf dem Server hochziehen
const TAKEOVER_CONDITIONS = [
'Die Übernahmegutschrift gibt es für Leistungen, die Ihr Abo ersetzt (Domain, Hosting bzw. Betrieb der Website, E-Mail-Postfächer) und die Sie bei Ihrem bisherigen Anbieter bereits im Voraus bezahlt haben.',
'Wir schreiben den Anteil der Rechnung gut, der auf die Zeit ab Leistungsbeginn Ihres Abos bis zum Ende des bezahlten Zeitraums entfällt, tagesgenau berechnet.',
'Nachweis ist die Rechnung des bisherigen Anbieters, ausgestellt auf Sie, mit Leistung, Zeitraum und Betrag. Wir können weitere Nachweise anfordern, etwa die Kündigungsbestätigung. Erstattet Ihr bisheriger Anbieter Beträge für diesen Zeitraum, teilen Sie uns das bitte mit.',
'Die Gutschrift wird mit Ihren nächsten Monatsgebühren verrechnet, auch nach einem Wechsel in ein anderes Abo. Sie wird nicht ausgezahlt und ist nicht übertragbar; endet Ihr Abo ohne einen solchen Wechsel, entfällt ein noch nicht verrechneter Rest.',
'Die Übernahmegutschrift ist eine freiwillige Leistung von WEBklar. Über Ihren Antrag entscheiden wir nach Prüfung und antworten Ihnen per E-Mail.',
]
const TAKEOVER_MONTHS = [[1, '1 Monat'], [3, '3 Monate'], [6, '6 Monate'], [12, '12 Monate (Jahresrechnung)'], [24, '24 Monate'], [36, '36 Monate']]
/** Datei als Base64 lesen (ohne den data:-Anfang). Tests ersetzen IO.lies, im Test gibt es keinen FileReader. */
function leseAlsBase64(datei) {
return new Promise((resolve, reject) => {
const leser = new FileReader()
leser.onload = () => resolve(String(leser.result).split(',')[1] || '')
leser.onerror = () => reject(new Error(`„${datei.name}“ konnte nicht gelesen werden.`))
leser.readAsDataURL(datei)
})
}
export const IO = { lies: leseAlsBase64 }
/** Dateien prüfen und lesen: → [{ name, data }]; wirft die Texte der Prüfungen aus app.js. */
export async function sammleDateien(dateien, grenzen, { erforderlich }, lies = IO.lies) {
const files = [...(dateien || [])]
const mb = (bytes) => Math.round(bytes / 1024 / 1024)
if (erforderlich && !files.length) throw new Error('Bitte laden Sie die Rechnung Ihres bisherigen Anbieters hoch.')
if (files.length > grenzen.maxFiles) throw new Error(`Bitte höchstens ${grenzen.maxFiles} Dateien auf einmal hochladen.`)
const zuGross = files.find((f) => f.size > grenzen.maxFileBytes)
if (zuGross) throw new Error(`„${zuGross.name}“ ist größer als ${mb(grenzen.maxFileBytes)} MB.`)
if (files.reduce((s, f) => s + f.size, 0) > grenzen.maxTotalBytes) throw new Error(`Die Dateien sind zusammen größer als ${mb(grenzen.maxTotalBytes)} MB.`)
return Promise.all(files.map(async (f) => ({ name: f.name, data: await lies(f) })))
}
const feld = (label, steuerung, klein) => h('div', { class: 'kn-tk-feld' }, h('label', { for: steuerung.getAttribute('id') }, label, klein ? [' ', h('small', null, klein)] : null), steuerung)
const leistung = (wert, text) => h('label', null, h('input', { type: 'checkbox', name: 'services', value: wert }), ` ${text}`)
function antragsFormular(grenzen, email) {
const maxMb = Math.round(grenzen.maxTotalBytes / 1024 / 1024)
const bedingungen = h('details', { class: 'kn-tk-bedingungen' }, h('summary', null, 'Bedingungen der Übernahmegutschrift'), h('ol', null, TAKEOVER_CONDITIONS.map((c) => h('li', null, c))))
return h('form', { id: 'tk-form', class: 'kn-tk-form', novalidate: true, hidden: true },
h('fieldset', { class: 'kn-tk-feld' }, h('legend', null, 'Welche Leistung haben Sie bisher woanders bezahlt?'),
h('div', { class: 'kn-tk-leistungen' }, leistung('domain', 'Domain'), leistung('hosting', 'Hosting / Website-Betrieb'), leistung('email', 'E-Mail-Postfächer'), leistung('sonstiges', 'Sonstiges'))),
h('div', { class: 'kn-tk-zeile' },
feld('Bisheriger Anbieter', h('input', { id: 'tk-provider', class: K.input, maxlength: 200, placeholder: 'z. B. IONOS, STRATO' })),
feld('Domain', h('input', { id: 'tk-domain', class: K.input, maxlength: 255, placeholder: 'meine-firma.de', autocapitalize: 'off', spellcheck: 'false' }), '(falls vorhanden)')),
h('div', { class: 'kn-tk-zeile' },
feld('Rechnungsbetrag', h('input', { id: 'tk-amount', class: K.input, type: 'number', min: '0.01', step: '0.01', inputmode: 'decimal', placeholder: '119,88' }), '(brutto, in €)'),
feld('Die Rechnung gilt für', h('select', { id: 'tk-months', class: K.select }, h('option', { value: '' }, 'bitte wählen'), TAKEOVER_MONTHS.map(([v, l]) => h('option', { value: String(v) }, l)))),
feld('Bezahlt bis', h('input', { id: 'tk-paid-until', class: K.input, type: 'date' }), '(letzter Tag)')),
feld('Grund / Nachricht an uns', h('textarea', { id: 'tk-message', class: K.textarea, maxlength: 4000, placeholder: 'z. B.: Domain und Postfach liegen noch bis Ende März bei IONOS, der Vertrag dort ist gekündigt.' })),
feld('Rechnungsnachweis', h('input', { id: 'tk-files', type: 'file', accept: 'application/pdf,image/jpeg,image/png', multiple: true }), `(PDF, JPG oder PNG · bis zu ${grenzen.maxFiles} Dateien · zusammen max. ${maxMb} MB)`),
h('p', { class: 'kn-hinweis' }, 'Tipp: Laden Sie auch die Kündigungsbestätigung Ihres bisherigen Anbieters hoch – daraus sehen wir, wann der Vertrag dort endet.'),
feld('E-Mail-Adresse für unsere Antwort', h('input', { id: 'tk-email', class: K.input, type: 'email', maxlength: 255, autocomplete: 'email', value: email })),
bedingungen,
h('label', { class: 'kn-check' }, h('input', { type: 'checkbox', id: 'tk-consent' }),
h('span', null, 'Ich versichere, dass meine Angaben stimmen und die Rechnung auf mich bzw. mein Unternehmen ausgestellt ist. Die Bedingungen der Übernahmegutschrift habe ich gelesen. Angaben und Nachweise verwendet WEBklar zur Prüfung und Abwicklung des Antrags (',
h('a', { href: '/datenschutz.html#uebernahmegutschrift', target: '_blank', rel: 'noopener' }, 'Datenschutz'), ').')),
h('p', { class: K.fehler, id: 'tk-error', hidden: true }),
h('div', { class: 'kn-aktionen' },
h('button', { type: 'submit', class: K.primaer, id: 'tk-submit' }, 'Antrag absenden'),
h('button', { type: 'button', class: K.knopf, id: 'tk-cancel' }, 'Abbrechen')))
}
function antrag(r, viewAs) {
const fakten = [
`eingereicht am ${fmtDatum(r.createdAt)}`,
`bisher bei ${r.provider}${r.domain ? ` (${r.domain})` : ''}`,
`${fmtEuro(r.invoiceAmountCents / 100)} für ${r.billingMonths === 1 ? '1 Monat' : `${r.billingMonths} Monate`}, bezahlt bis ${fmtDatum(r.paidUntil)}`,
]
const [pilleText, ton] = TAKEOVER_STATUS[r.status] || [r.status, 'grau']
let koerper
if (r.status === 'genehmigt') {
koerper = [h('p', null, 'Gutschrift: ', h('strong', null, fmtEuro(r.amountCents / 100)), ' – wird mit Ihren nächsten Monatsgebühren verrechnet.'),
r.decisionNote ? h('p', { class: 'kn-hinweis kn-umbruch' }, r.decisionNote) : null]
} else if (r.status === 'abgelehnt') {
koerper = [h('p', { class: 'kn-hinweis kn-umbruch' }, r.decisionNote)]
} else if (r.status === 'rueckfrage') {
koerper = [
h('div', { class: 'kn-tk-frage' }, h('strong', null, 'Unsere Rückfrage:'), h('br'), r.question),
h('form', { class: 'kn-tk-antwort', 'data-reply-for': r.id, novalidate: true },
h('textarea', { name: 'message', class: K.textarea, maxlength: 4000, placeholder: 'Ihre Antwort…' }),
h('input', { type: 'file', name: 'files', accept: 'application/pdf,image/jpeg,image/png', multiple: true, 'aria-label': 'Weitere Nachweise' }),
h('p', { class: K.fehler, hidden: true }),
h('div', { class: 'kn-aktionen' }, h('button', { type: 'submit', class: K.primaer, disabled: viewAs }, 'Antwort senden')))]
} else {
koerper = [h('p', { class: 'kn-hinweis' }, 'Wir prüfen Ihre Angaben und antworten Ihnen per E-Mail.')]
}
return h('div', { class: 'kn-tk-antrag' },
h('div', { class: 'kn-tk-kopf' }, h('strong', null, `Antrag ${r.ref} · ${r.services}`), h('span', { class: K.pille[ton] }, pilleText)),
h('div', { class: 'kn-hinweis' }, fakten.join(' · ')),
...koerper)
}
/** Seite Übernahmegutschrift. Lädt Abo und Anträge selbst; nach Absenden oder Antworten lädt sie neu und meldet den Zähler. */
export async function zeigeGutschrift(wurzel, { route, me, nav } = {}) {
const viewAs = me?.viewAs === true
const email = me?.customer?.email || ''
const nr = (laeufe.get(wurzel) ?? 0) + 1
laeufe.set(wurzel, nr)
const aktuell = () => laeufe.get(wurzel) === nr
leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …'))
await lade('')
async function lade(notice, { zaehler = false } = {}) {
const abo = await ladeAbo()
if (!aktuell()) return
if (!abo) { leeren(wurzel).append(h('p', { class: K.fehler }, ABO_NICHT_GELADEN)); return }
const aktiv = Boolean(abo.subscription?.active)
let daten = { requests: [], limits: null }
if (abo.subscription) {
try {
daten = await api('/api/abo/takeover-credits')
} catch (f) {
if (!aktuell()) return
leeren(wurzel).append(h('div', { 'data-karte': 'gutschrift' }, h('div', { class: 'kn-panel kn-leer' }, `Übernahmegutschrift konnte nicht geladen werden: ${f.message}`)))
return
}
if (!aktuell()) return
}
if (zaehler) meldeZaehlerNeu('vertraege')
const anfragen = Array.isArray(daten.requests) ? daten.requests : []
const grenzen = daten.limits || GRENZEN_VORGABE
if (!aktiv && !anfragen.length) {
leeren(wurzel).append(h('div', { 'data-karte': 'gutschrift' }, h('div', { class: 'kn-panel' },
h('p', { class: 'kn-hinweis' }, 'Die Übernahmegutschrift können Sie beantragen, sobald Ihr Abo läuft. ', h('a', zuAnsicht('vertraege'), 'Zum Abo')))))
return
}
const formular = aktiv ? antragsFormular(grenzen, email) : null
const start = aktiv ? h('button', { type: 'button', class: K.knopf, id: 'tk-start', disabled: viewAs }, 'Gutschrift beantragen') : null
ersetze(wurzel, h('div', { 'data-karte': 'gutschrift' }, h('div', { class: 'kn-panel' },
h('p', { class: 'kn-hinweis kn-tk-einleitung' }, 'Sie haben Domain, Hosting oder E-Mail bei Ihrem bisherigen Anbieter schon im Voraus bezahlt? Den Anteil, den Sie ab Beginn Ihres Abos doppelt zahlen, schreiben wir Ihnen gut und verrechnen ihn mit Ihren nächsten Monatsgebühren. Schicken Sie uns dazu einfach die Rechnung.'),
notice ? h('div', { class: 'kn-meldung kn-meldung-ok', role: 'status' }, notice) : null,
aktiv ? h('div', { class: 'kn-aktionen' }, start, viewAs ? h('span', { class: K.meldung }, TEXT_ANSICHTSMODUS) : null) : null,
formular,
anfragen.length ? h('div', { class: 'kn-tk-liste' }, anfragen.map((r) => antrag(r, viewAs))) : null)))
const neuLaden = (text) => lade(text, { zaehler: true })
start?.addEventListener('click', () => {
formular.hidden = false
start.hidden = true
formular.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' })
})
formular?.querySelector('#tk-cancel')?.addEventListener('click', () => { formular.hidden = true; if (start) start.hidden = false })
formular?.addEventListener('submit', (ev) => { ev.preventDefault(); sendeAntrag(formular, grenzen, neuLaden) })
wurzel.querySelectorAll('form[data-reply-for]').forEach((f) => f.addEventListener('submit', (ev) => { ev.preventDefault(); sendeAntwort(f, grenzen, neuLaden, viewAs) }))
}
}
function zeigeFehler(el, text) {
if (!el) return
el.textContent = text
el.hidden = false
}
async function sendeAntrag(form, grenzen, neuLaden) {
const fehler = form.querySelector('#tk-error')
const knopf = form.querySelector('#tk-submit')
fehler.hidden = true
const wert = (id) => form.querySelector(id).value.trim()
const payload = {
services: [...form.querySelectorAll('input[name="services"]:checked')].map((i) => i.value),
provider: wert('#tk-provider'),
domain: wert('#tk-domain'),
invoiceAmount: wert('#tk-amount'),
billingMonths: Number(wert('#tk-months')),
paidUntil: wert('#tk-paid-until'),
message: wert('#tk-message'),
email: wert('#tk-email'),
consent: form.querySelector('#tk-consent').checked,
}
try {
if (!payload.services.length) throw new Error('Bitte wählen Sie, welche Leistung Sie bisher woanders bezahlt haben.')
if (!payload.provider) throw new Error('Bitte nennen Sie Ihren bisherigen Anbieter.')
if (!payload.invoiceAmount || !payload.billingMonths || !payload.paidUntil) throw new Error('Bitte geben Sie Rechnungsbetrag, Zeitraum und „Bezahlt bis“ an.')
if (!payload.consent) throw new Error('Bitte bestätigen Sie Ihre Angaben und die Bedingungen.')
knopf.disabled = true
knopf.textContent = 'Wird gesendet…'
payload.files = await sammleDateien(form.querySelector('#tk-files').files, grenzen, { erforderlich: true })
await api('/api/abo/takeover-credits', { method: 'POST', body: payload })
await neuLaden(`Vielen Dank! Ihr Antrag ist eingegangen. Die Eingangsbestätigung haben wir an ${payload.email} geschickt; unsere Antwort kommt ebenfalls per E-Mail.`)
} catch (f) {
zeigeFehler(fehler, f.message)
knopf.disabled = false
knopf.textContent = 'Antrag absenden'
}
}
async function sendeAntwort(form, grenzen, neuLaden, viewAs) {
if (viewAs) return
const fehler = form.querySelector('p')
const knopf = form.querySelector('button[type="submit"]')
fehler.hidden = true
knopf.disabled = true
try {
const message = form.querySelector('textarea').value.trim()
const files = await sammleDateien(form.querySelector('input[type="file"]').files, grenzen, { erforderlich: false })
if (!message && !files.length) throw new Error('Bitte schreiben Sie eine Antwort oder laden Sie einen Nachweis hoch.')
await api(`/api/abo/takeover-credits/${encodeURIComponent(form.getAttribute('data-reply-for'))}/reply`, { method: 'POST', body: { message, files } })
await neuLaden('Danke für Ihre Antwort! Wir prüfen Ihren Antrag weiter und melden uns per E-Mail.')
} catch (f) {
zeigeFehler(fehler, f.message)
knopf.disabled = false
}
}
+63
View File
@@ -0,0 +1,63 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { zeigeGutschrift, sammleDateien, IO } from '../public/neu/ansicht-gutschrift.js'
import { sichtbarerText, ladeAlteApp } from './support/knWortlautOrakel.js'
import { mit, me, abo, aboAntwort, anfrageGutschrift, neueWurzel, LIMITS } from './support/knDaten.js'
const alt = ladeAlteApp(); const ABO = aboAntwort({ subscription: abo() })
const ANTRAEGE = { requests: [anfrageGutschrift({ id: 't1', status: 'rueckfrage', question: 'Bitte <b>Kündigungsbestätigung</b> hochladen' }), anfrageGutschrift({ id: 't2', status: 'genehmigt', amountCents: 4000, decisionNote: 'Passt.' }), anfrageGutschrift({ id: 't3', status: 'abgelehnt', decisionNote: 'Nicht erstattbar.' })], conditionsVersion: '2026-09-30', limits: LIMITS }
const SRV = { 'GET /api/abo': ABO, 'GET /api/abo/takeover-credits': ANTRAEGE }
const zeigeNeu = async (fx, m = me()) => { const w = neueWurzel(fx); await zeigeGutschrift(w, { route: { bereich: 'vertraege', etage: 'gutschrift', query: '' }, me: m, nav: 'push' }); await fx.ruhe(); return w }
for (const [name, abo_, antwort] of [['mit Anträgen', ABO, ANTRAEGE], ['ohne Abo', aboAntwort(), { requests: [], limits: LIMITS }]]) {
test(`Gutschrift (${name}): Karte wie in app.js`, mit({ 'GET /api/abo': abo_, 'GET /api/abo/takeover-credits': antwort }, async (fx) => {
const w = await zeigeNeu(fx)
assert.equal(sichtbarerText(fx.finde(w, '[data-karte="gutschrift"]')), sichtbarerText(await alt.gutschrift(abo_, antwort)))
}))
}
test('Bedingungen der Gutschrift: Block gleich dem Pin in legalTexts.test.js (Fassung 2026-09-30)', () => {
const q = readFileSync(new URL('../public/neu/ansicht-gutschrift.js', import.meta.url), 'utf8'); const i = q.indexOf('const TAKEOVER_CONDITIONS'); const block = q.slice(i, q.indexOf(']', i))
assert.equal(createHash('sha256').update(block).digest('hex'), '2b7a745cb599c0ab28f20615a745bd86da6faa8d120391ca56beac35a0473e62')
})
test('sammleDateien: Prüftexte wörtlich', async () => {
const g = { maxFiles: 3, maxFileBytes: 8 * 1048576, maxTotalBytes: 10 * 1048576 }; const f = (name, size) => ({ name, size }); const lies = async () => 'QQ=='
await assert.rejects(sammleDateien([], g, { erforderlich: true }, lies), { message: 'Bitte laden Sie die Rechnung Ihres bisherigen Anbieters hoch.' })
await assert.rejects(sammleDateien([1, 2, 3, 4].map((n) => f(`${n}.pdf`, 1)), g, { erforderlich: false }, lies), { message: 'Bitte höchstens 3 Dateien auf einmal hochladen.' })
await assert.rejects(sammleDateien([f('groß.pdf', 9 * 1048576)], g, { erforderlich: false }, lies), { message: '„groß.pdf“ ist größer als 8 MB.' })
await assert.rejects(sammleDateien([f('a.pdf', 6 * 1048576), f('b.pdf', 6 * 1048576)], g, { erforderlich: false }, lies), { message: 'Die Dateien sind zusammen größer als 10 MB.' })
assert.deepEqual(await sammleDateien([f('a.pdf', 5)], g, { erforderlich: true }, lies), [{ name: 'a.pdf', data: 'QQ==' }])
})
test('Antrag: Prüfungen wörtlich, Body mit denselben Schlüsseln wie früher, Meldung nach Erfolg', mit({ ...SRV, 'POST /api/abo/takeover-credits': {} }, async (fx) => {
IO.lies = async () => 'QQ=='; const w = await zeigeNeu(fx); await fx.klick(fx.finde(w, (e) => e.textContent === 'Gutschrift beantragen'))
await fx.klick(fx.finde(w, '#tk-submit')); assert.match(fx.text(w), /Bitte wählen Sie, welche Leistung Sie bisher woanders bezahlt haben\./)
await fx.klick(fx.finde(w, 'input[value="domain"]')); await fx.setze(fx.finde(w, '#tk-provider'), 'IONOS'); await fx.setze(fx.finde(w, '#tk-amount'), '119.88'); await fx.setze(fx.finde(w, '#tk-months'), '12'); await fx.setze(fx.finde(w, '#tk-paid-until'), '2027-03-31')
await fx.klick(fx.finde(w, '#tk-submit')); assert.match(fx.text(w), /Bitte bestätigen Sie Ihre Angaben und die Bedingungen\./)
fx.finde(w, '#tk-files').files = [{ name: 'r.pdf', size: 10 }]; await fx.klick(fx.finde(w, '#tk-consent')); await fx.klick(fx.finde(w, '#tk-submit')); await fx.ruhe()
assert.deepEqual(fx.fetchLog.find((a) => a.method === 'POST').body, { services: ['domain'], provider: 'IONOS', domain: '', invoiceAmount: '119.88', billingMonths: 12, paidUntil: '2027-03-31', message: '', email: 'anna@example.com', consent: true, files: [{ name: 'r.pdf', data: 'QQ==' }] })
assert.match(fx.text(w), /Vielen Dank! Ihr Antrag ist eingegangen\. Die Eingangsbestätigung haben wir an anna@example\.com geschickt; unsere Antwort kommt ebenfalls per E-Mail\./)
}))
test('Rückfrage beantworten; Kundentext bleibt Text; Ansichtsmodus sperrt alles', mit({ ...SRV, 'POST /api/abo/takeover-credits/t1/reply': {} }, async (fx) => {
const w = await zeigeNeu(fx); assert.equal(fx.finde(w, 'b'), null)
await fx.setze(fx.finde(w, 'form[data-reply-for="t1"] textarea'), 'Anbei.'); await fx.klick(fx.finde(w, 'form[data-reply-for="t1"] button[type="submit"]')); await fx.ruhe()
assert.deepEqual(fx.fetchLog.find((a) => a.url.endsWith('/reply')).body, { message: 'Anbei.', files: [] })
const v = await zeigeNeu(fx, me({ viewAs: true })); assert.match(fx.text(v), /Im Ansichtsmodus kann kein Antrag gestellt werden\./)
assert.ok(fx.alle(v, 'button').filter((b) => ['Gutschrift beantragen', 'Antwort senden'].includes(b.textContent)).every((b) => b.hasAttribute('disabled')))
}))
test('Gutschrift ohne Abo: Satz und Link zum Abo über die Brücke', mit({ 'GET /api/abo': aboAntwort(), 'GET /api/abo/takeover-credits': { requests: [], limits: LIMITS } }, async (fx) => {
const w = await zeigeNeu(fx); assert.match(fx.text(w), /Die Übernahmegutschrift können Sie beantragen, sobald Ihr Abo läuft\./)
const a = fx.finde(w, 'a[href="#vertraege"]'); assert.deepEqual([a.textContent, a.getAttribute('data-view')], ['Zum Abo', 'vertraege'])
}))
test('Gutschrift: Antwort ohne Text und Nachweis wird abgewiesen, Abbrechen schließt das Formular, Ausfälle haben eigene Texte', mit({ ...SRV, 'POST /api/abo/takeover-credits/t1/reply': {} }, async (fx) => {
const w = await zeigeNeu(fx)
await fx.klick(fx.finde(w, 'form[data-reply-for="t1"] button[type="submit"]')); await fx.ruhe()
assert.match(fx.text(w), /Bitte schreiben Sie eine Antwort oder laden Sie einen Nachweis hoch\./); assert.ok(!fx.fetchLog.some((a) => a.url.endsWith('/reply')))
assert.equal(fx.finde(w, '#tk-form').hasAttribute('hidden'), true); await fx.klick(fx.finde(w, '#tk-start')); assert.equal(fx.finde(w, '#tk-form').hasAttribute('hidden'), false)
await fx.klick(fx.finde(w, '#tk-cancel')); assert.equal(fx.finde(w, '#tk-form').hasAttribute('hidden'), true)
assert.match(fx.text(w), /Antrag .* · Domain/); assert.match(fx.text(w), /Nicht genehmigt/); assert.match(fx.text(w), /Gutschrift: 40,00 € – wird mit Ihren nächsten Monatsgebühren verrechnet\./)
}))
test('Gutschrift: Ausfall der Anträge und des Abos', mit({ 'GET /api/abo': ABO, 'GET /api/abo/takeover-credits': () => ({ status: 500, body: { error: 'Server kaputt' } }) }, async (fx) => {
assert.match(fx.text(await zeigeNeu(fx)), /Übernahmegutschrift konnte nicht geladen werden: Server kaputt/)
fx.tabelle['GET /api/abo'] = () => ({ status: 500, body: { error: 'x' } })
assert.match(fx.text(await zeigeNeu(fx)), /Deine Verträge konnten gerade nicht geladen werden\./)
}))