Buchungskalender: Abschluss – Kleinigkeiten Server (Betrag/Waehrung, Erstattungs-Webhook, Obergrenze, Zaehler, Cache, Kommentare, Setup-Check)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6d35674c1e
commit
1d9515823c
13 files changed
+123
-14
No files matched your search
@@ -223,7 +223,7 @@ test('Webhook: unbezahlt, fremdes Konto, unbekannte Anfrage ohne Wirkung; async_
|
||||
const { id, token } = await freigegeben()
|
||||
await post(url(id, token), { rechnungsadresse: ADRESSE, zahlart: 'online' })
|
||||
const sid = (await sp.ladeAnfrage(id)).stripeSessionId
|
||||
const unbezahlt = { ...st.completeSession(ACCOUNT, sid, { payment_status: 'unpaid', payment_intent: 'pi_2' }) }
|
||||
const unbezahlt = { ...st.completeSession(ACCOUNT, sid, { payment_status: 'unpaid', amount_total: 20000, payment_intent: 'pi_2' }) }
|
||||
await W.verarbeiteUkEreignis({ type: 'checkout.session.completed', account: ACCOUNT, data: { object: unbezahlt } }, NOW)
|
||||
assert.equal((await sp.ladeAnfrage(id)).status, 'gebucht')
|
||||
const bezahlt = { ...unbezahlt, payment_status: 'paid' }
|
||||
@@ -257,7 +257,7 @@ test('POST zahlung: neue Sitzung fuer gebuchte Online-Zahlung; vor Ort oder beza
|
||||
assert.match(r.body.url, /^https:\/\/checkout\.stripe\.test\//)
|
||||
assert.equal((await post(url(id, 'falsch', '/zahlung'), {})).status, 404)
|
||||
const sid = (await sp.ladeAnfrage(id)).stripeSessionId
|
||||
st.completeSession(ACCOUNT, sid, { payment_status: 'paid', payment_intent: 'pi_4' })
|
||||
st.completeSession(ACCOUNT, sid, { payment_status: 'paid', amount_total: 20000, payment_intent: 'pi_4' })
|
||||
const nach = await post(url(id, token, '/zahlung'), {})
|
||||
assert.equal(nach.status, 409)
|
||||
assert.equal((await sp.ladeAnfrage(id)).status, 'bezahlt')
|
||||
@@ -329,6 +329,42 @@ test('Erstattung: Stripe ging durch, Speichern scheiterte, KEIN Webhook -> Wiede
|
||||
assert.equal(erstattungen('pi_h2').reduce((s, x) => s + x.amount, 0), 20000)
|
||||
})
|
||||
|
||||
test('Zahlung mit falschem Betrag oder falscher Waehrung gilt nicht als Bezahlung: Warnung, automatisch voll erstattet, Buchung bleibt gebucht (Webhook und Abgleich)', async (t) => {
|
||||
t.mock.method(console, 'warn', () => {})
|
||||
const f1 = await freigegeben('2026-11-10', '2026-11-12')
|
||||
await post(url(f1.id, f1.token), { rechnungsadresse: ADRESSE, zahlart: 'online' })
|
||||
const sid1 = (await sp.ladeAnfrage(f1.id)).stripeSessionId
|
||||
st.setPaymentIntent(ACCOUNT, 'pi_falsch', { amount: 100 })
|
||||
const zuWenig = st.completeSession(ACCOUNT, sid1, { payment_status: 'paid', amount_total: 100, payment_intent: 'pi_falsch' })
|
||||
await W.verarbeiteUkEreignis({ type: 'checkout.session.completed', account: ACCOUNT, data: { object: zuWenig } }, NOW)
|
||||
assert.equal((await sp.ladeAnfrage(f1.id)).status, 'gebucht')
|
||||
assert.equal(erstattungen('pi_falsch').length, 1)
|
||||
assert.equal(erstattungen('pi_falsch')[0].amount, 100)
|
||||
// andere Waehrung
|
||||
const f2 = await freigegeben('2026-12-10', '2026-12-12')
|
||||
await post(url(f2.id, f2.token), { rechnungsadresse: ADRESSE, zahlart: 'online' })
|
||||
const sid2 = (await sp.ladeAnfrage(f2.id)).stripeSessionId
|
||||
st.setPaymentIntent(ACCOUNT, 'pi_usd', { amount: 20000 })
|
||||
const usd = { ...st.completeSession(ACCOUNT, sid2, { payment_status: 'paid', amount_total: 20000, payment_intent: 'pi_usd' }), currency: 'usd' }
|
||||
await W.verarbeiteUkEreignis({ type: 'checkout.session.completed', account: ACCOUNT, data: { object: usd } }, NOW)
|
||||
assert.equal((await sp.ladeAnfrage(f2.id)).status, 'gebucht')
|
||||
assert.equal(erstattungen('pi_usd').length, 1)
|
||||
})
|
||||
|
||||
test('Webhook charge.refunded: ohne gespeicherte Zahlung der Buchung oder mit fremder Zahlung keine Wirkung', async () => {
|
||||
const { id, ctx } = await bezahlt('pi_r1')
|
||||
await ab.storniere(ctx, id, { now: NOW })
|
||||
const charge = (pi, betrag) => ({ type: 'charge.refunded', account: ACCOUNT, data: { object: { id: 'ch_r', payment_intent: pi, amount_refunded: betrag, metadata: { wkUk: '1', webklar: 'unterkunft', anfrageId: id } } } })
|
||||
await W.verarbeiteUkEreignis(charge('pi_fremd', 5000), NOW)
|
||||
assert.equal((await sp.ladeAnfrage(id)).erstattetCent ?? 0, 0)
|
||||
await sp.aktualisiereAnfrage(id, { stripePaymentIntentId: '' })
|
||||
await W.verarbeiteUkEreignis(charge('pi_r1', 5000), NOW)
|
||||
assert.equal((await sp.ladeAnfrage(id)).erstattetCent ?? 0, 0, 'Buchung ohne gespeicherte Zahlung')
|
||||
await sp.aktualisiereAnfrage(id, { stripePaymentIntentId: 'pi_r1' })
|
||||
await W.verarbeiteUkEreignis(charge('pi_r1', 5000), NOW)
|
||||
assert.equal((await sp.ladeAnfrage(id)).erstattetCent, 5000)
|
||||
})
|
||||
|
||||
test('Webhook charge.refunded: erstattetCent = Maximum, nie zurueck', async () => {
|
||||
const { id, ctx } = await bezahlt('pi_8')
|
||||
await ab.storniere(ctx, id, { now: NOW })
|
||||
|
||||
@@ -121,6 +121,8 @@ test('pruefePosten', () => {
|
||||
assert.ok(pruefePosten([{ ...ok, text: 'x'.repeat(201) }], 'regel').fehler.posten)
|
||||
assert.ok(pruefePosten(Array(31).fill(ok), 'regel').fehler.posten)
|
||||
assert.ok(pruefePosten([ok], 'kleinunternehmer').fehler.posten)
|
||||
assert.ok(pruefePosten([{ ...ok, menge: 1, einzelCent: 10_000_000 }, ...Array(10).fill({ ...ok, menge: 1, einzelCent: 10_000_000 })], 'regel').fehler.posten, 'Summe ueber 999.999,99 EUR')
|
||||
assert.ok(pruefePosten([{ ...ok, menge: 1, einzelCent: 9_999_999 }], 'regel').posten)
|
||||
assert.ok(pruefePosten([{ ...ok, ustProzent: 5 }], 'regel').fehler.posten)
|
||||
assert.ok(pruefePosten([{ ...ok, menge: 0 }], 'regel').fehler.posten)
|
||||
assert.ok(pruefePosten([{ ...ok, menge: 1.5 }], 'regel').fehler.posten)
|
||||
|
||||
@@ -209,6 +209,13 @@ test('Anfrage: mehr als anfragenJeSiteStunde je Seite -> 429', async () => {
|
||||
assert.equal(aw.docs('ukAnfragen').size, 2)
|
||||
})
|
||||
|
||||
test('Anfrage: fehlerhafte Felder (Datum, Unterkunft) verbrauchen das Stundenkontingent der Seite nicht', async () => {
|
||||
config.unterkunft.anfragenJeSiteStunde = 1
|
||||
for (const extra of [{ von: 'morgen' }, { bis: '' }, { unterkunft: 'a/b' }]) assert.equal((await post(anfrage(extra))).status, 400)
|
||||
assert.equal((await post(anfrage({ von: '2026-11-10', bis: '2026-11-12' }))).status, 201)
|
||||
assert.equal((await post(anfrage({ von: '2026-12-10', bis: '2026-12-12' }))).status, 429)
|
||||
})
|
||||
|
||||
test('CORS: Preflight erlaubt/fremd, POST von fremder oder fehlender Origin -> 403', async () => {
|
||||
const ok = await send('OPTIONS', `${BASE}/anfrage`, { headers: { origin: ORIGIN, 'access-control-request-method': 'POST' } })
|
||||
assert.equal(ok.status, 204)
|
||||
|
||||
@@ -233,3 +233,22 @@ test('main --check meldet bei vollstaendigem Stand nichts und ein einzelnes Fehl
|
||||
assert.deepEqual(result.missing.map((a) => `${a.collection}.${a.key}`), ['ukAnfragen.anonymisiertAt'])
|
||||
assert.deepEqual(result.missingIndexes.map((i) => `${i.collection}.${i.key}`), ['ukSperren.idx_unterkunft_quelle'])
|
||||
})
|
||||
|
||||
test('main: fehlendes termineOutbox.anhang wird gemeldet (--check, Exit-relevant) und im normalen Lauf angelegt; fehlende Collection nur ein Hinweis', async () => {
|
||||
const client = fakeClient({ [config.collections.termineOutbox]: { attrs: ['an'] } })
|
||||
await main({ client, log: quiet })
|
||||
client.state.get(config.collections.termineOutbox).attrs.delete('anhang')
|
||||
const lines = []
|
||||
const result = await main({ check: true, client, log: (l) => lines.push(l) })
|
||||
assert.deepEqual(result.missing.map((a) => `${a.collection}.${a.key}`), [`${config.collections.termineOutbox}.anhang`])
|
||||
assert.ok(lines.some((l) => l.includes('anhang') && l.includes('FEHLT')))
|
||||
await main({ client, log: quiet })
|
||||
assert.ok(client.state.get(config.collections.termineOutbox).attrs.has('anhang'))
|
||||
assert.deepEqual((await main({ check: true, client, log: quiet })).missing, [])
|
||||
// Collection fehlt: Hinweis, kein Eintrag in missing
|
||||
const leer = fakeClient()
|
||||
const hinweise = []
|
||||
const r = await main({ check: true, client: leer, log: (l) => hinweise.push(l) })
|
||||
assert.ok(hinweise.some((l) => l.includes('termine-setup')))
|
||||
assert.ok(!r.missing.some((a) => a.key === 'anhang'))
|
||||
})
|
||||
@@ -99,6 +99,14 @@ test('Zugriff: ohne Sitzung 401, fremdes Projekt 403, Funktion aus 409, Kundenan
|
||||
assert.equal((await post('/unterkuenfte', { name: 'X', maxPersonen: 2 })).status, 409)
|
||||
})
|
||||
|
||||
test('PUT /einstellungen leert den Speicher der oeffentlichen Schnittstelle: neue Firma sofort sichtbar', async () => {
|
||||
const K = await import('../server/services/unterkunft/kontext.js')
|
||||
K.leereCache()
|
||||
assert.equal((await K.unterkunftPerSiteKey('abcdefghijklmnop')).einstellungen.firma, 'Donau Home')
|
||||
assert.equal((await put('/einstellungen', { ...EINST, firma: 'Neue Firma' })).status, 200)
|
||||
assert.equal((await K.unterkunftPerSiteKey('abcdefghijklmnop')).einstellungen.firma, 'Neue Firma')
|
||||
})
|
||||
|
||||
test('Zugriff: Schalter off sperrt die Verwaltung', async () => {
|
||||
config.unterkunft.mode = 'off'
|
||||
assert.equal((await get('/unterkuenfte')).status, 409)
|
||||
|
||||
Reference in new issue
Block a user