Buchungskalender: Buchungsseite – Ablauf, bezahlt und Wiederholung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0b0d8da335
commit
51a36be36a
4 files changed
+71
-14
No files matched your search
@@ -30,6 +30,7 @@
|
||||
kleinunternehmer: 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.',
|
||||
laden: 'Die Buchung konnte nicht geladen werden. Bitte versuchen Sie es später erneut.',
|
||||
senden: 'Die Buchung konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut.',
|
||||
belegt: 'Der Zeitraum ist leider inzwischen belegt. Bitte fragen Sie einen anderen Zeitraum an.',
|
||||
knopf: 'Zahlungspflichtig buchen',
|
||||
}
|
||||
|
||||
@@ -91,6 +92,11 @@
|
||||
return [a.firma, a.inhaber].filter(Boolean).join(', ') + (zeile ? ', ' + zeile : '')
|
||||
}
|
||||
|
||||
function zahlartenText(d, wahl) {
|
||||
var z = (d.zahlarten || []).map(function (x) { return x === 'online' ? 'online (Karte, über Stripe)' : 'vor Ort' })
|
||||
return 'Zahlarten: ' + z.join(' oder ') + (wahl ? ' (gewählt: ' + (wahl === 'online' ? 'online' : 'vor Ort') + ')' : '')
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------- Pflichtangaben --- */
|
||||
function pflichtangaben(d, wahl) {
|
||||
var s = d.summen || {}
|
||||
@@ -99,14 +105,14 @@
|
||||
var kontakt = [a.email, a.telefon].filter(Boolean).join(', ')
|
||||
var u = d.unterkunft || {}
|
||||
var posten = h('ul', { 'data-tag': 'posten' }, (d.posten || []).map(function (x) {
|
||||
return h('li', {}, [h('span', { text: x.text }), h('span', { text: euro(x.menge * x.einzelCent) })])
|
||||
var bez = x.menge !== 1 ? x.text + ' (' + x.menge + ' × ' + euro(x.einzelCent) + ')' : x.text
|
||||
return h('li', {}, [h('span', { text: bez }), h('span', { text: euro(x.menge * x.einzelCent) })])
|
||||
}))
|
||||
var gesamt = 'Gesamtpreis: ' + euro(s.bruttoCent || 0)
|
||||
var ust = []
|
||||
if (d.steuerart === 'kleinunternehmer') ust.push(TXT.kleinunternehmer)
|
||||
else (s.ust || []).filter(function (z) { return z.satz > 0 }).forEach(function (z) { ust.push(z.satz + ' % USt ' + euro(z.steuerCent)) })
|
||||
var preisText = d.steuerart === 'kleinunternehmer' ? gesamt + '. ' + TXT.kleinunternehmer : gesamt + (ust.length ? ' inkl. ' + ust.join(' und ') : ' inkl. USt')
|
||||
var zahlarten = (d.zahlarten || []).map(function (z) { return z === 'online' ? 'online (Karte, über Stripe)' : 'vor Ort' })
|
||||
var ds = a.datenschutzUrl && /^https:\/\//.test(a.datenschutzUrl) ? a.datenschutzUrl : ''
|
||||
var datenschutz = [p('Datenschutz: ' + (a.firma || 'Der Anbieter') + ' verarbeitet Ihre Angaben zur Durchführung der Buchung und für die Rechnung (Art. 6 Abs. 1 lit. b und c DSGVO). Näheres steht in der Datenschutzerklärung des Anbieters.')]
|
||||
if (ds) datenschutz.push(h('a', { class: 'uk-link', href: ds, target: '_blank', rel: 'noopener noreferrer', text: 'Datenschutzerklärung des Anbieters' }))
|
||||
@@ -117,7 +123,7 @@
|
||||
p('Personen: ' + d.personen, 'uk-personen'),
|
||||
posten,
|
||||
p(preisText, 'uk-gesamt'),
|
||||
p('Zahlarten: ' + zahlarten.join(' oder ') + (wahl ? ' (gewählt: ' + (wahl === 'online' ? 'online' : 'vor Ort') + ')' : ''), 'uk-zahlarten'),
|
||||
p(zahlartenText(d, wahl), 'uk-zahlarten'),
|
||||
p('Stornobedingungen: ' + (d.stornobedingungen || ''), 'uk-storno'),
|
||||
p(TXT.widerruf, 'uk-widerruf'),
|
||||
h('div', { class: 'uk-datenschutz' }, datenschutz),
|
||||
@@ -131,7 +137,7 @@
|
||||
var g = d.gast || {}
|
||||
var felder = {}
|
||||
function feld(name, bez, wert, extra) {
|
||||
var input = h('input', Object.assign({ type: 'text', name: name, autocomplete: (extra && extra.ac) || 'off', maxlength: 120 }, {}))
|
||||
var input = h('input', { type: 'text', name: name, autocomplete: (extra && extra.ac) || 'off', maxlength: 120 })
|
||||
input.value = wert || ''
|
||||
felder[name] = input
|
||||
var fehler = h('span', { class: 'uk-feldfehler', 'data-fehler': name, hidden: true })
|
||||
@@ -161,10 +167,16 @@
|
||||
fLand.wurzel,
|
||||
h('h2', { text: 'Zahlart' }),
|
||||
h('div', { 'data-tag': 'zahlart' }, radios),
|
||||
pflichtangaben(d),
|
||||
pflichtangaben(d, gewaehlt),
|
||||
knopf,
|
||||
meldung,
|
||||
])
|
||||
radios.forEach(function (l) {
|
||||
l.querySelector('input').addEventListener('change', function () {
|
||||
var z = form.querySelector('.uk-zahlarten')
|
||||
if (z) z.textContent = zahlartenText(d, waehl())
|
||||
})
|
||||
})
|
||||
function waehl() { var r = radios.map(function (l) { return l.querySelector('input') }).find(function (i) { return i.checked }); return r ? r.value : gewaehlt }
|
||||
var sendet = false
|
||||
function fehlerAnzeigen(felderFehler, text) {
|
||||
@@ -196,10 +208,12 @@
|
||||
bestaetigt(Object.assign({}, d, { status: 'gebucht', zahlart: zahlart, bezahlt: false, rechnungNummer: r.body.rechnungNummer }))
|
||||
return
|
||||
}
|
||||
if (r.status === 404) return nurText('Buchung', TXT.ungueltig)
|
||||
if (r.status === 410 || r.body.fehler === 'frist_abgelaufen' || r.body.fehler === 'abgelaufen') return nurText('Buchung', TXT.abgelaufen)
|
||||
if (r.body.fehler === 'zeitraum_belegt') return nurText('Buchung', TXT.belegt)
|
||||
if (r.status === 409 || r.ok) { laden(); return }
|
||||
sendet = false
|
||||
knopf.removeAttribute('disabled')
|
||||
if (r.status === 404) return nurText('Buchung', TXT.ungueltig)
|
||||
if (r.body.fehler === 'abgelaufen') return nurText('Buchung', TXT.abgelaufen)
|
||||
fehlerAnzeigen(r.body.felder, r.status === 400 && r.body.felder ? 'Bitte prüfen Sie Ihre Angaben.' : TXT.senden)
|
||||
}, function () {
|
||||
sendet = false
|
||||
@@ -217,7 +231,7 @@
|
||||
if (d.rechnungNummer) kinder.push(p('Rechnung ' + d.rechnungNummer))
|
||||
kinder.push(h('a', { class: 'uk-link', 'data-tag': 'rechnung', href: basis + '/rechnung.pdf' + tokenQuery + '&art=rechnung', text: 'Rechnung herunterladen (PDF)' }))
|
||||
var online = (d.zahlarten || []).indexOf('online') >= 0
|
||||
if (d.bezahlt) kinder.push(p('Die Zahlung ist eingegangen. Vielen Dank!'))
|
||||
if (d.bezahlt || d.status === 'bezahlt') kinder.push(p('Bezahlt – vielen Dank.'))
|
||||
else if (d.zahlart === 'online' && online) {
|
||||
var meldung = h('div', { class: 'uk-fehler', role: 'alert', hidden: true })
|
||||
var knopf = h('button', { type: 'button', class: 'uk-knopf', 'data-tag': 'online-zahlen', text: 'Jetzt online bezahlen' })
|
||||
@@ -241,8 +255,9 @@
|
||||
/* ----------------------------------------------------------------------- Start --- */
|
||||
function darstellen(d) {
|
||||
akzent(d)
|
||||
if (d.unterkunft && d.unterkunft.name) document.title = 'Buchung – ' + d.unterkunft.name
|
||||
if (d.status === 'freigegeben') zeige(formular(d), hinweisAbbruch())
|
||||
else if (d.status === 'gebucht') bestaetigt(d)
|
||||
else if (d.status === 'gebucht' || d.status === 'bezahlt') bestaetigt(d)
|
||||
else if (d.status === 'abgelaufen') nurText('Buchung', TXT.abgelaufen)
|
||||
else if (d.status === 'abgelehnt') nurText('Buchung', 'Diese Anfrage wurde leider abgelehnt.')
|
||||
else if (d.status === 'storniert') nurText('Buchung', 'Diese Buchung wurde storniert.')
|
||||
@@ -250,9 +265,12 @@
|
||||
}
|
||||
|
||||
if (!token || !id) { nurText('Buchung', TXT.ungueltig); return }
|
||||
api('', 'GET').then(function (r) {
|
||||
function laden() {
|
||||
return api('', 'GET').then(function (r) {
|
||||
if (r.status === 404 || r.status === 401 || r.status === 403) return nurText('Buchung', TXT.ungueltig)
|
||||
if (!r.ok) return nurText('Buchung', TXT.laden)
|
||||
darstellen(r.body)
|
||||
}, function () { nurText('Buchung', TXT.laden) })
|
||||
}
|
||||
laden()
|
||||
})()
|
||||
@@ -4,7 +4,8 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Unterkunft</title>
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>Buchung</title>
|
||||
<link rel="stylesheet" href="/unterkunft-buchung.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -45,7 +45,7 @@ function seite(res, next, siteKey, ansicht) {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
||||
'Referrer-Policy': 'no-referrer',
|
||||
'Content-Security-Policy': CSP,
|
||||
'X-Robots-Tag': 'noindex',
|
||||
})
|
||||
|
||||
@@ -32,6 +32,8 @@ test('Widget-Seite und Buchungsseite: 200, Schluessel escaped, Schutz-Header', a
|
||||
assert.match(r.text, /<script src="\/unterkunft-buchung\.js" defer><\/script>/)
|
||||
assert.equal(r.headers.get('cache-control'), 'no-store')
|
||||
assert.equal(r.headers.get('x-robots-tag'), 'noindex')
|
||||
assert.equal(r.headers.get('referrer-policy'), 'no-referrer')
|
||||
assert.match(r.text, /<meta name="referrer" content="no-referrer">/)
|
||||
assert.equal(r.headers.get('x-content-type-options'), 'nosniff')
|
||||
const csp = r.headers.get('content-security-policy')
|
||||
for (const d of ["default-src 'self'", "script-src 'self'", "connect-src 'self'", "frame-ancestors 'none'", "base-uri 'none'", "form-action 'self'"]) assert.ok(csp.includes(d), d)
|
||||
@@ -186,7 +188,7 @@ test('gebucht, online unbezahlt: „Jetzt online bezahlen“ holt /zahlung und l
|
||||
test('gebucht und bezahlt: kein Zahlknopf', async () => {
|
||||
await mitFx([{ status: 200, body: { ...DATEN, status: 'gebucht', zahlart: 'online', bezahlt: true } }], undefined, (fx, el) => {
|
||||
assert.equal(fx.finde(el, '[data-tag="online-zahlen"]'), null)
|
||||
assert.match(fx.text(el), /Zahlung ist eingegangen/)
|
||||
assert.match(fx.text(el), /Bezahlt – vielen Dank\./)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -207,11 +209,16 @@ test('abgelaufen, 404 und Netzfehler: feste Texte; ohne Token kein Abruf', async
|
||||
})
|
||||
|
||||
test('Fehler beim Buchen: abgelaufen, Feldfehler, Server-Ausfall (Knopf wieder frei)', async () => {
|
||||
await mitFx([{ status: 200, body: DATEN }, { status: 409, body: { fehler: 'abgelaufen' } }], undefined, async (fx, el) => {
|
||||
await mitFx([{ status: 200, body: DATEN }, { status: 410, body: { fehler: 'frist_abgelaufen' } }], undefined, async (fx, el) => {
|
||||
await fuelle(fx, el)
|
||||
await fx.klick(fx.finde(el, 'button[type="submit"]'))
|
||||
assert.match(fx.text(el), /Die Reservierung ist abgelaufen\./)
|
||||
})
|
||||
await mitFx([{ status: 200, body: DATEN }, { status: 409, body: { fehler: 'zeitraum_belegt' } }], undefined, async (fx, el) => {
|
||||
await fuelle(fx, el)
|
||||
await fx.klick(fx.finde(el, 'button[type="submit"]'))
|
||||
assert.match(fx.text(el), /Der Zeitraum ist leider inzwischen belegt\. Bitte fragen Sie einen anderen Zeitraum an\./)
|
||||
})
|
||||
await mitFx([{ status: 200, body: DATEN }, { status: 400, body: { fehler: 'ungueltig', felder: { plz: 'PLZ ungültig' } } }, { status: 500, body: {} }], undefined, async (fx, el) => {
|
||||
await fuelle(fx, el)
|
||||
await fx.klick(fx.finde(el, 'button[type="submit"]'))
|
||||
@@ -234,3 +241,34 @@ test('Token verlaesst die Seite nur in den Schnittstellen-Aufrufen', async () =>
|
||||
for (const a of fx.fetchLog) assert.ok(a.url.startsWith('/api/public/unterkunft/v1/'), a.url)
|
||||
})
|
||||
})
|
||||
|
||||
test('Status bezahlt wie gebucht: Bestaetigung, kein Zahlknopf', async () => {
|
||||
await mitFx([{ status: 200, body: { ...DATEN, status: 'bezahlt', zahlart: 'online', bezahlt: true } }], undefined, (fx, el) => {
|
||||
assert.match(fx.text(el), /Ihre Buchung ist bestätigt/)
|
||||
assert.match(fx.text(el), /Bezahlt – vielen Dank\./)
|
||||
assert.equal(fx.finde(el, '[data-tag="online-zahlen"]'), null)
|
||||
})
|
||||
})
|
||||
|
||||
test('Wiederholung: POST 409 oder ok ohne gebucht laedt neu und zeigt den aktuellen Stand', async () => {
|
||||
const gebucht = { ...DATEN, status: 'gebucht', zahlart: 'vorort', bezahlt: false, rechnungNummer: 'R-1' }
|
||||
for (const post of [{ status: 409, body: { fehler: 'bereits_gebucht' } }, { status: 200, body: {} }]) {
|
||||
await mitFx([{ status: 200, body: DATEN }, post, { status: 200, body: gebucht }], undefined, async (fx, el) => {
|
||||
await fuelle(fx, el)
|
||||
await fx.klick(fx.finde(el, 'button[type="submit"]'))
|
||||
assert.equal(fx.fetchLog.length, 3)
|
||||
assert.equal(fx.fetchLog[2].method, 'GET')
|
||||
assert.match(fx.text(el), /Ihre Buchung ist bestätigt/)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test('Uebersicht: Menge × Einzelpreis, gewaehlte Zahlart, Titel', async () => {
|
||||
await mitFx([{ status: 200, body: DATEN }], undefined, async (fx, el) => {
|
||||
assert.match(fx.text(fx.finde(el, '[data-tag="posten"]')), /Übernachtung, 4 Nächte \(4 × 100,00\s€\)400,00\s€/)
|
||||
assert.match(fx.text(fx.finde(el, '.uk-zahlarten')), /\(gewählt: vor Ort\)/)
|
||||
await fx.klick(fx.finde(el, 'input[value="online"]'))
|
||||
assert.match(fx.text(fx.finde(el, '.uk-zahlarten')), /\(gewählt: online\)/)
|
||||
assert.equal(fx.document.title, 'Buchung – Ferienwohnung')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user