Buchungskalender: Rechnungen, Einstellungen und Funktionen-Kachel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
10fe9a2ae0
commit
efeb99e214
8 files changed
+696
-11
No files matched your search
@@ -467,3 +467,244 @@ test('Kalender: vergangene Tage lassen sich nicht sperren', mit(ukServer(), asyn
|
||||
assert.match(fx.text(fx.finde(panel, '[data-hinweis]')), /Vergangene Tage/)
|
||||
assert.equal(fx.finde(panel, '[data-aktion="sperren-senden"]'), null)
|
||||
}))
|
||||
|
||||
/* ===================================================== Task 14: Rechnungen, Einstellungen, Anfrage ===================================================== */
|
||||
const { render: renderRechnungen } = await import('../public/buchungen-tab-rechnungen.js')
|
||||
const { render: renderEinstellungen, STORNO_MUSTER, STORNO_HINWEIS, AUFTRAG_TEXT } = await import('../public/buchungen-tab-einstellungen.js')
|
||||
const { renderAnfrageFormular, anfrageKarte } = await import('../public/buchungen-anfrage.js')
|
||||
const { initBuchungen, vergissAuswahl } = await import('../public/buchungen.js')
|
||||
|
||||
const zeigeMit = async (fx, modul, ctxExtra = {}) => {
|
||||
const ctx = { ...baueCtx(), ...ctxExtra }
|
||||
const panel = h('div')
|
||||
fx.document.body.append(panel)
|
||||
await modul(panel, ctx)
|
||||
await fx.ruhe()
|
||||
return { panel, ctx }
|
||||
}
|
||||
const ES = { firma: 'Haus am See', inhaber: 'Mimi M.', strasse: 'Seeweg 1', plz: '78532', ort: 'Tuttlingen', email: 'mimi@example.com', telefon: '', steuernummer: '12/345/67890', ustId: '', iban: '', bic: '', bank: '', rechnungsPraefix: 'RE', akzentfarbe: '#336699', onlineZahlung: false, rechnungAuftrag: true, stornobedingungen: '' }
|
||||
const esServer = (o = {}, antwort = {}) => server({ 'GET /api/unterkunft/einstellungen': () => ({ body: { einstellungen: ES, stripe: { status: 'keins', vorhandenesShopKonto: false }, rechnungenVorhanden: false, ...antwort } }), 'PUT /api/unterkunft/einstellungen': () => ({ body: {} }), ...o })
|
||||
|
||||
test('Rechnungen: Liste mit PDF-Link, Art und Betrag; Fremdtext nur als Text', mit(server({
|
||||
'GET /api/unterkunft/rechnungen': () => ({ body: { rechnungen: [
|
||||
{ id: 'r1', nummer: 'RE-2026-0001', datum: '2026-10-07', art: 'rechnung', gastName: 'Anna <b>M</b>', bruttoCent: 40650 },
|
||||
{ id: 'r2', nummer: 'RE-2026-0002', datum: '2026-10-08', art: 'storno', gastName: 'Anna', bruttoCent: -40650 },
|
||||
] } }),
|
||||
}), async (fx) => {
|
||||
const { panel } = await zeigeMit(fx, renderRechnungen)
|
||||
assert.equal(fx.fetchLog[0].url, '/api/unterkunft/rechnungen?projectId=p1')
|
||||
const z = fx.alle(panel, '[data-rechnung]')
|
||||
assert.equal(z.length, 2)
|
||||
assert.match(fx.text(z[0]), /RE-2026-0001/)
|
||||
assert.match(fx.text(z[0]), /07\.10\.2026/)
|
||||
assert.match(fx.text(z[0]), /Anna <b>M<\/b>/)
|
||||
assert.match(fx.text(z[0]), /406,50\s€/)
|
||||
assert.match(fx.text(z[1]), /Storno-Rechnung/)
|
||||
assert.match(fx.text(z[1]), /−406,50\s€/)
|
||||
assert.equal(fx.finde(z[0], '[data-aktion="pdf"]').getAttribute('href'), '/api/unterkunft/rechnungen/r1.pdf?projectId=p1')
|
||||
assert.equal(fx.finde(z[0], 'b'), null)
|
||||
}))
|
||||
|
||||
test('Rechnungen: leer → Hinweis', mit(server({ 'GET /api/unterkunft/rechnungen': () => ({ body: { rechnungen: [] } }) }), async (fx) => {
|
||||
const { panel } = await zeigeMit(fx, renderRechnungen)
|
||||
assert.match(fx.text(panel), /Noch keine Rechnungen/)
|
||||
assert.equal(fx.alle(panel, '[data-rechnung]').length, 0)
|
||||
}))
|
||||
|
||||
test('Einstellungen: Pflichtfelder markiert, leeres Formular sendet nicht und zeigt Feldfehler', mit(esServer({}, { einstellungen: { ...ES, firma: '', steuernummer: '', ustId: '' } }), async (fx) => {
|
||||
const { panel } = await zeigeMit(fx, renderEinstellungen)
|
||||
for (const n of ['firma', 'inhaber', 'strasse', 'plz', 'ort', 'email']) assert.match(fx.text(fx.finde(panel, `[data-feld="${n}"]`).parentNode.querySelector('label')), /\*/, n)
|
||||
await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
|
||||
await fx.ruhe()
|
||||
assert.equal(posts(fx).length, 0)
|
||||
assert.match(fx.text(fx.finde(panel, '[data-fehler="firma"]')), /Firma/)
|
||||
assert.match(fx.text(fx.finde(panel, '[data-fehler="steuernummer"]')), /Steuernummer oder USt-IdNr/)
|
||||
assert.match(fx.text(fx.finde(panel, '[data-fehler="ustId"]')), /Steuernummer oder USt-IdNr/)
|
||||
}))
|
||||
|
||||
test('Einstellungen: speichern sendet die Felder ohne Wrapper; Serverfehler am Feld', mit(esServer({ 'PUT /api/unterkunft/einstellungen': () => ({ status: 400, body: { fehler: 'ungueltig', felder: { iban: 'Bitte gib eine gültige IBAN an.' } } }) }), async (fx) => {
|
||||
const { panel } = await zeigeMit(fx, renderEinstellungen)
|
||||
await fx.setze(fx.finde(panel, '[data-feld="iban"]'), 'xx')
|
||||
await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
|
||||
await fx.ruhe()
|
||||
const put = posts(fx)[0]
|
||||
assert.equal(put.url, '/api/unterkunft/einstellungen?projectId=p1')
|
||||
assert.equal(put.body.firma, 'Haus am See')
|
||||
assert.equal(put.body.rechnungsPraefix, 'RE')
|
||||
assert.equal(put.body.rechnungAuftrag, true)
|
||||
assert.equal(put.body.iban, 'xx')
|
||||
assert.equal(put.body.onlineZahlung, false)
|
||||
assert.match(fx.text(fx.finde(panel, '[data-fehler="iban"]')), /gültige IBAN/)
|
||||
}))
|
||||
|
||||
test('Einstellungen: Präfix prüfen; gesperrt mit Hinweis, sobald Rechnungen existieren', async () => {
|
||||
await mit(esServer(), async (fx) => {
|
||||
const { panel } = await zeigeMit(fx, renderEinstellungen)
|
||||
await fx.setze(fx.finde(panel, '[data-feld="rechnungsPraefix"]'), 'a')
|
||||
await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
|
||||
await fx.ruhe()
|
||||
assert.equal(posts(fx).length, 0)
|
||||
assert.match(fx.text(fx.finde(panel, '[data-fehler="rechnungsPraefix"]')), /2 bis 8/)
|
||||
await fx.setze(fx.finde(panel, '[data-feld="rechnungsPraefix"]'), 'ab1')
|
||||
await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
|
||||
await fx.ruhe()
|
||||
assert.equal(posts(fx)[0].body.rechnungsPraefix, 'AB1')
|
||||
})()
|
||||
await mit(esServer({}, { rechnungenVorhanden: true }), async (fx) => {
|
||||
const { panel } = await zeigeMit(fx, renderEinstellungen)
|
||||
const p = fx.finde(panel, '[data-feld="rechnungsPraefix"]')
|
||||
assert.ok(p.disabled)
|
||||
assert.match(fx.text(p.parentNode), /gesperrt/)
|
||||
await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
|
||||
await fx.ruhe()
|
||||
assert.equal(posts(fx)[0].body.rechnungsPraefix, 'RE')
|
||||
})()
|
||||
})
|
||||
|
||||
test('Einstellungen: Mustertext übernehmen füllt die Stornobedingungen, Hinweis „keine Rechtsberatung“', mit(esServer(), async (fx) => {
|
||||
const { panel } = await zeigeMit(fx, renderEinstellungen)
|
||||
const t = fx.finde(panel, '[data-feld="stornobedingungen"]')
|
||||
assert.equal(t.value, '')
|
||||
assert.match(fx.text(t.parentNode), new RegExp(STORNO_HINWEIS.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
|
||||
await fx.klick(fx.finde(panel, '[data-aktion="storno-muster"]'))
|
||||
assert.equal(t.value, STORNO_MUSTER)
|
||||
assert.match(STORNO_MUSTER, /^Stornierung bis 14 Tage vor Anreise: kostenlos\./)
|
||||
assert.match(STORNO_MUSTER, /Maßgeblich ist der Eingang bei uns\.$/)
|
||||
await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
|
||||
await fx.ruhe()
|
||||
assert.equal(posts(fx)[0].body.stornobedingungen, STORNO_MUSTER)
|
||||
}))
|
||||
|
||||
test('Einstellungen: Auftrag zur Rechnungsstellung – ohne Haken Hinweis, mit Haken weg und rechnungAuftrag im PUT', mit(esServer({}, { einstellungen: { ...ES, rechnungAuftrag: false } }), async (fx) => {
|
||||
const { panel } = await zeigeMit(fx, renderEinstellungen)
|
||||
const box = fx.finde(panel, '[data-feld="rechnungAuftrag"]')
|
||||
assert.match(fx.text(box.parentNode), new RegExp(AUFTRAG_TEXT.slice(0, 40)))
|
||||
const hinweis = fx.finde(panel, '[data-hinweis="auftrag"]')
|
||||
assert.equal(hinweis.hidden, false)
|
||||
assert.match(fx.text(hinweis), /nicht bestätigen/)
|
||||
await fx.klick(box)
|
||||
assert.equal(hinweis.hidden, true)
|
||||
await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
|
||||
await fx.ruhe()
|
||||
assert.equal(posts(fx)[0].body.rechnungAuftrag, true)
|
||||
}))
|
||||
|
||||
test('Einstellungen: Stripe nicht verbunden – Verbinden leitet nur auf https; „Vorhandenes Webshop-Konto“ nur wenn gemeldet', async () => {
|
||||
await mit(esServer({ 'POST /api/unterkunft/stripe/verbinden': () => ({ body: { url: 'https://connect.stripe.com/x' } }) }), async (fx) => {
|
||||
const { panel } = await zeigeMit(fx, renderEinstellungen)
|
||||
assert.equal(fx.finde(panel, '[data-stripe-status]').getAttribute('data-stripe-status'), 'keins')
|
||||
assert.equal(fx.finde(panel, '[data-aktion="stripe-vorhanden"]'), null)
|
||||
assert.equal(fx.finde(panel, '[data-feld="onlineZahlung"]'), null)
|
||||
await fx.klick(fx.finde(panel, '[data-aktion="stripe-verbinden"]'))
|
||||
await fx.ruhe()
|
||||
assert.equal(posts(fx)[0].url, '/api/unterkunft/stripe/verbinden?projectId=p1')
|
||||
assert.equal(fx.window.location.href, 'https://connect.stripe.com/x')
|
||||
})()
|
||||
await mit(esServer({ 'POST /api/unterkunft/stripe/verbinden': () => ({ body: { url: 'javascript:alert(1)' } }) }), async (fx) => {
|
||||
const { panel, ctx } = await zeigeMit(fx, renderEinstellungen)
|
||||
await fx.klick(fx.finde(panel, '[data-aktion="stripe-verbinden"]'))
|
||||
await fx.ruhe()
|
||||
assert.notEqual(fx.window.location.href, 'javascript:alert(1)')
|
||||
assert.equal(ctx.meldungen.at(-1).art, 'fehler')
|
||||
})()
|
||||
await mit(esServer({ 'POST /api/unterkunft/stripe/vorhandenes-konto': () => ({ body: { stripe: { status: 'aktiv' } } }) }, { stripe: { status: 'keins', vorhandenesShopKonto: true } }), async (fx) => {
|
||||
const { panel } = await zeigeMit(fx, renderEinstellungen)
|
||||
await fx.klick(fx.finde(panel, '[data-aktion="stripe-vorhanden"]'))
|
||||
await fx.ruhe()
|
||||
assert.equal(posts(fx)[0].url, '/api/unterkunft/stripe/vorhandenes-konto?projectId=p1')
|
||||
assert.ok(fx.fetchLog.filter((a) => a.method === 'GET' && a.url.startsWith('/api/unterkunft/einstellungen')).length >= 2) // neu geladen
|
||||
})()
|
||||
})
|
||||
|
||||
test('Einstellungen: Stripe Onboarding offen und aktiv; Schalter Online-Zahlung nur bei aktiv', async () => {
|
||||
await mit(esServer({ 'POST /api/unterkunft/stripe/aktualisieren': () => ({ body: { stripe: { status: 'aktiv' } } }) }, { stripe: { status: 'onboarding', vorhandenesShopKonto: true } }), async (fx) => {
|
||||
const { panel } = await zeigeMit(fx, renderEinstellungen)
|
||||
assert.equal(fx.finde(panel, '[data-stripe-status]').getAttribute('data-stripe-status'), 'onboarding')
|
||||
assert.match(fx.text(panel), /Angaben bei Stripe/)
|
||||
assert.ok(fx.finde(panel, '[data-aktion="stripe-aktualisieren"]'))
|
||||
assert.equal(fx.finde(panel, '[data-aktion="stripe-vorhanden"]'), null)
|
||||
assert.equal(fx.finde(panel, '[data-feld="onlineZahlung"]'), null)
|
||||
})()
|
||||
await mit(esServer({}, { einstellungen: { ...ES, onlineZahlung: true }, stripe: { status: 'aktiv', vorhandenesShopKonto: false } }), async (fx) => {
|
||||
const { panel } = await zeigeMit(fx, renderEinstellungen)
|
||||
assert.equal(fx.finde(panel, '[data-stripe-status]').getAttribute('data-stripe-status'), 'aktiv')
|
||||
const schalter = fx.finde(panel, '[data-feld="onlineZahlung"]')
|
||||
assert.ok(schalter.checked)
|
||||
await fx.klick(schalter)
|
||||
await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
|
||||
await fx.ruhe()
|
||||
assert.equal(posts(fx)[0].body.onlineZahlung, false)
|
||||
})()
|
||||
})
|
||||
|
||||
test('Einstellungen: nur lesen sperrt Formular und Stripe-Knöpfe', mit(esServer(), async (fx) => {
|
||||
const { panel } = await zeigeMit(fx, renderEinstellungen, { nurLesen: true })
|
||||
assert.equal(fx.finde(panel, '[data-aktion="stripe-verbinden"]'), null)
|
||||
assert.ok(fx.finde(panel, '[data-feld="firma"]').disabled)
|
||||
assert.equal(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'), null)
|
||||
}))
|
||||
|
||||
test('Anfrageformular sendet { art, einheiten, portale, nachricht }; Feldfehler am Feld', async () => {
|
||||
let antwort = { body: { anfrage: { status: 'offen' } } }
|
||||
await mit((a) => (a.method === 'POST' ? antwort : { body: {} }), async (fx) => {
|
||||
const wurzel = h('div')
|
||||
fx.document.body.append(wurzel)
|
||||
let danach = 0
|
||||
renderAnfrageFormular(wurzel, { projectId: 'p1', nachSenden: async () => { danach += 1 } })
|
||||
antwort = { status: 400, body: { fehler: 'ungueltig', felder: { art: 'Bitte wähle die Art deiner Unterkunft.' } } }
|
||||
await fx.klick(fx.finde(wurzel, '[data-aktion="anfrage-senden"]'))
|
||||
await fx.ruhe()
|
||||
assert.match(fx.text(fx.finde(wurzel, '[data-fehler="art"]')), /Art deiner Unterkunft/)
|
||||
assert.equal(danach, 0)
|
||||
antwort = { body: { anfrage: { status: 'offen' } } }
|
||||
await fx.setze(fx.finde(wurzel, '[data-feld="art"]'), 'ferienwohnung')
|
||||
await fx.setze(fx.finde(wurzel, '[data-feld="einheiten"]'), '3')
|
||||
await fx.klick(fx.finde(wurzel, '[data-feld="portale.airbnb"]'))
|
||||
await fx.setze(fx.finde(wurzel, '[data-feld="nachricht"]'), ' Hallo ')
|
||||
await fx.klick(fx.finde(wurzel, '[data-aktion="anfrage-senden"]'))
|
||||
await fx.ruhe()
|
||||
const p = posts(fx).at(-1)
|
||||
assert.equal(p.url, '/api/unterkunft/anfrage-funktion?projectId=p1')
|
||||
assert.deepEqual(p.body, { art: 'ferienwohnung', einheiten: 3, portale: ['airbnb'], nachricht: 'Hallo' })
|
||||
assert.equal(danach, 1)
|
||||
assert.match(fx.text(wurzel), /199 €/)
|
||||
})()
|
||||
})
|
||||
|
||||
test('Anfragekarte: offen mit Zurückziehen, abgelehnt mit Begründung als Text', async () => {
|
||||
await mit(server(), async (fx) => {
|
||||
let neuGeladen = 0
|
||||
const k = anfrageKarte({ status: 'offen', at: '2026-10-07T10:00:00Z', anfrage: { art: 'monteur', einheiten: 2, portale: ['booking'], nachricht: 'x' } }, { projectId: 'p1', neuLaden: async () => { neuGeladen += 1 } })
|
||||
fx.document.body.append(k)
|
||||
assert.match(fx.text(k), /In Prüfung/)
|
||||
assert.match(fx.text(k), /Monteurwohnung/)
|
||||
assert.match(fx.text(k), /Booking\.com/)
|
||||
await fx.klick(fx.finde(k, '[data-aktion="zurueckziehen"]'))
|
||||
await fx.ruhe()
|
||||
assert.equal(posts(fx)[0].url, '/api/unterkunft/anfrage-funktion/zurueckziehen?projectId=p1')
|
||||
assert.equal(neuGeladen, 1)
|
||||
const b = anfrageKarte({ status: 'abgelehnt', begruendung: 'Passt <i>nicht</i>' }, { projectId: 'p1', neu: () => {} })
|
||||
assert.match(fx.text(b), /Passt <i>nicht<\/i>/)
|
||||
assert.equal(fx.finde(b, 'i'), null)
|
||||
assert.ok(fx.finde(b, '[data-aktion="neue-anfrage"]'))
|
||||
})()
|
||||
})
|
||||
|
||||
test('Ansicht #buchungen: Zustand off zeigt das Anfrageformular, Projekt vom Wunsch-Attribut', async () => {
|
||||
vergissAuswahl()
|
||||
await mit((a) => {
|
||||
if (a.url === '/api/funktionen') return { body: { unterkunft: { verfuegbar: true }, projects: [{ projectId: 'p1', projectName: 'A', features: { unterkunft: { state: 'off' } } }, { projectId: 'p2', projectName: 'B', features: { unterkunft: { state: 'off' } } }] } }
|
||||
if (a.url.startsWith('/api/unterkunft/status')) return { body: { state: 'off' } }
|
||||
return { body: {} }
|
||||
}, async (fx) => {
|
||||
const root = h('div')
|
||||
root.dataset.wunschProjekt = 'p2'
|
||||
fx.document.body.append(root)
|
||||
await initBuchungen(root)
|
||||
await fx.ruhe()
|
||||
assert.equal(fx.fetchLog[1].url, '/api/unterkunft/status?projectId=p2')
|
||||
assert.ok(fx.finde(root, '[data-aktion="anfrage-senden"]'))
|
||||
assert.match(fx.text(root), /Buchungskalender anfragen/)
|
||||
})()
|
||||
vergissAuswahl()
|
||||
})
|
||||
@@ -564,3 +564,20 @@ test('Webshop-Kachel: Projekt-ID maskiert, Klick legt das Wunschprojekt in #webs
|
||||
}
|
||||
assert.equal(body.dataset.wunschProjekt, 'p7')
|
||||
})
|
||||
|
||||
// ---- Buchungskalender-Kachel: nur bei unterkunft.verfuegbar, Preis 199 €, Zustände wie die Webshop-Kachel
|
||||
test('Buchungskalender-Kachel: nur bei unterkunft.verfuegbar; Text, Preis und Zustände', async () => {
|
||||
const mitUk = (state) => project('a', undefined) && { ...project('a'), features: { news, unterkunft: { state } } }
|
||||
const ohne = await renderMit([mitUk('off')], {})
|
||||
assert.doesNotMatch(ohne, /data-fn-tile="unterkunft"/)
|
||||
const zu = await renderMit([mitUk('off')], { unterkunft: { verfuegbar: false } })
|
||||
assert.doesNotMatch(zu, /data-fn-tile="unterkunft"/)
|
||||
const off = tile(await renderMit([mitUk('off')], { unterkunft: { verfuegbar: true } }), 'unterkunft')
|
||||
assert.match(off, /<h3>Buchungskalender<\/h3>/)
|
||||
assert.match(off, /Belegungskalender auf deiner Website, Anfragen freigeben, Rechnung und Online-Zahlung/)
|
||||
assert.match(off, /199 €/)
|
||||
assert.match(off, /Buchungskalender anfragen/)
|
||||
assert.match(off, /data-view="buchungen" data-project="a"/)
|
||||
const faelle = { active: /Aktiv[\s\S]*Buchungskalender öffnen/, setup: /In Einrichtung/, ordered: /Zahlung offen/, disabled: /Abgeschaltet[\s\S]*Buchungskalender ansehen/ }
|
||||
for (const [state, re] of Object.entries(faelle)) assert.match(tile(await renderMit([mitUk(state)], { unterkunft: { verfuegbar: true } }), 'unterkunft'), re, state)
|
||||
})
|
||||
Reference in new issue
Block a user