From d7b5b9818746f13b288e27dd068a2198d5a02ef6 Mon Sep 17 00:00:00 2001 From: Kenso Grimm Date: Wed, 7 Oct 2026 14:02:41 +0000 Subject: [PATCH] Buchungskalender: Verdrahtung absichern (Outbox, Shop-Webhook, Session) Co-Authored-By: Claude Opus 5.5 --- docs/funktionen/unterkunft-api.md | 4 ++++ server/routes/shopWebhook.js | 7 ++++++- server/routes/unterkunft.js | 3 +++ server/services/termine/outbox.js | 3 ++- test/termineOutbox.test.js | 2 +- test/unterkunftWiring.test.js | 30 +++++++++++++++++++++++++++++- 6 files changed, 45 insertions(+), 4 deletions(-) diff --git a/docs/funktionen/unterkunft-api.md b/docs/funktionen/unterkunft-api.md index c82c8ba..578f7bd 100644 --- a/docs/funktionen/unterkunft-api.md +++ b/docs/funktionen/unterkunft-api.md @@ -2,6 +2,10 @@ Verbindlich für Strang 2 (Widget und Buchungsseite, bauen gegen die öffentliche Schnittstelle) und Strang 3 (Portal, baut gegen die Kunden-API). Task 6 bis 8 erfüllen ihn. Fehlerform überall `{ fehler: code, felder?: {feld: text} }`. +## Livegang-Reihenfolge + +Vor dem Anheben von `UNTERKUNFT_ENABLED` über `off` zuerst `scripts/unterkunft-setup.mjs` (legt die Collections samt Index `idx_account` an) und das termine-Setup (Attribut `anhang` der Mail-Warteschlange) auf Produktion laufen lassen. + ## Öffentlich (`/api/public/unterkunft/v1`) - `GET /:siteKey` → `{ aktiv:true, anbieter:{ name, ort, datenschutzUrl }, akzentfarbe, formToken, unterkuenfte:[{ id, name, maxPersonen, mindestNaechte, checkIn, checkOut }] }` oder `{ aktiv:false }`. diff --git a/server/routes/shopWebhook.js b/server/routes/shopWebhook.js index d0a42d5..2db925d 100644 --- a/server/routes/shopWebhook.js +++ b/server/routes/shopWebhook.js @@ -43,7 +43,12 @@ async function verarbeite(event, now) { if (event.type === 'account.updated') { await kontoAktualisiert(accountId || objekt.id, objekt, now) // dasselbe Konto kann auch im Buchungskalender haengen - await kontoAktualisiertUk(accountId || objekt.id, objekt, now) + try { + await kontoAktualisiertUk(accountId || objekt.id, objekt, now) + } catch (err) { + // ein Fehler im Buchungskalender darf die Antwort fuer den Shop nie beeinflussen (ohne Kundendaten im Log) + console.warn(`[shop] Webhook account.updated: Buchungskalender nicht aktualisiert: ${err?.name}`) + } return } if (!accountId) return diff --git a/server/routes/unterkunft.js b/server/routes/unterkunft.js index 020e4a8..34bf60e 100644 --- a/server/routes/unterkunft.js +++ b/server/routes/unterkunft.js @@ -1,4 +1,5 @@ import { Router } from 'express' +import { requireSession } from '../middleware/session.js' /** * Kunden-API des Buchungskalenders: /api/unterkunft (Session, Projekt per ?projectId=; Vertrag: docs/funktionen/unterkunft-api.md). @@ -6,5 +7,7 @@ import { Router } from 'express' * und Stripe; Task 11 die Anfrage-Funktion (/anfrage-funktion). */ const router = Router() +// Jede Route hier braucht eine Sitzung (Task 8 darf das nicht vergessen); Kunden-Pruefung je Route wie in routes/shop.js +router.use(requireSession) export default router diff --git a/server/services/termine/outbox.js b/server/services/termine/outbox.js index e305cfa..e41522a 100644 --- a/server/services/termine/outbox.js +++ b/server/services/termine/outbox.js @@ -225,7 +225,8 @@ export async function einreihen( text: mail.text, ics: ics || '', icsMethod: ics ? icsMethod : '', - anhang: anhang || '', + // nur wenn gesetzt: das Attribut gibt es in Appwrite erst nach erneutem termine-setup, unbekannte Felder wuerden abgelehnt + ...(anhang ? { anhang } : {}), status: 'wartet', versuche: 0, naechsterVersuch: iso(nowMs), diff --git a/test/termineOutbox.test.js b/test/termineOutbox.test.js index 9438147..acd495c 100644 --- a/test/termineOutbox.test.js +++ b/test/termineOutbox.test.js @@ -1283,7 +1283,7 @@ test('einreihen: optionaler Anhang-Verweis wird gespeichert (Senden unverändert const mit = await einreihen(eintrag({ bookingId: 'b_anhang', anhang: 'uk-rechnung:r_123' }), JETZT) assert.equal((await lies(mit.$id)).anhang, 'uk-rechnung:r_123') const ohne = await einreihen(eintrag({ bookingId: 'b_ohne' }), JETZT) - assert.equal((await lies(ohne.$id)).anhang, '') + assert.equal(Object.hasOwn(await lies(ohne.$id), 'anhang'), false, 'ohne Angabe wird das Feld nicht geschrieben') await assert.rejects(() => einreihen(eintrag({ anhang: 'x'.repeat(501) }), JETZT), TypeError) await warteAufVersand() assert.equal(tr.senke.sent.length, 2, 'Versand nicht verändert') diff --git a/test/unterkunftWiring.test.js b/test/unterkunftWiring.test.js index c84f44b..46b24ab 100644 --- a/test/unterkunftWiring.test.js +++ b/test/unterkunftWiring.test.js @@ -29,7 +29,8 @@ const jobs = await import('../server/services/unterkunft/jobs.js') const { default: publicRoutes } = await import('../server/routes/publicUnterkunft.js') const { default: webhookRoutes } = await import('../server/routes/shopWebhook.js') const W = await import('../server/routes/unterkunftWebhook.js') -const app = await startTestApp({ '/api/public/unterkunft/v1': publicRoutes, '/api/shop/stripe-webhook': webhookRoutes }) +const { default: ukRoutes } = await import('../server/routes/unterkunft.js') +const app = await startTestApp({ '/api/unterkunft': ukRoutes, '/api/public/unterkunft/v1': publicRoutes, '/api/shop/stripe-webhook': webhookRoutes }) after(async () => { await app.close() await aw.close() @@ -284,3 +285,30 @@ test('Webhook: account.updated speichert den Status auch in ukEinstellungen mit assert.equal(await W.kontoAktualisiertUk('acct_unbekannt', konto, T0), 0) assert.equal(await W.kontoAktualisiertUk('kein-konto', konto, T0), 0) }) + +test('Kunden-API: ohne Sitzung 401 (Router-weiter Guard)', async () => { + assert.equal((await rawGet('/api/unterkunft/status')).status, 401) +}) + +test('Webhook: Shop-Erstattung ohne Uk-Metadaten geht nicht an Uk', async () => { + config.shop.connectWebhookSecret = SECRET + const gesehen = [] + W.setzeUkEreignisHandlerFuerTests(async (event) => void gesehen.push(event.type)) + const r = await postWebhook(ereignis('charge.refunded', { id: 'ch_9', payment_intent: null, metadata: { webklar: 'shop' } }), { path: PFAD, secret: SECRET }) + assert.equal(r.status, 200) + assert.deepEqual(gesehen, []) +}) + +test('Webhook: ein Fehler im Uk-Teil von account.updated lässt den Shop-Teil gelten und die Antwort 200', async () => { + await leere() + config.shop.connectWebhookSecret = SECRET + config.unterkunft.mode = 'on' + aw.seed('shopConfig', { $id: 'f_shop1', customerId: KUNDE, projectId: PROJEKT, siteKey: SITE_KEY, stripeAccountId: ACCOUNT, stripeStatus: JSON.stringify({ charges_enabled: false }), einstellungen: '{}', rechtstexte: '{}', version: 1 }) + // Uk-Abfrage scheitert: Index fehlt (400) bzw. Collection-Fehler + mock.method(console, 'warn', () => {}) + aw.failNext({ collection: config.collections.ukEinstellungen, method: 'GET', status: 500 }) + const konto = { id: ACCOUNT, charges_enabled: true, payouts_enabled: true, details_submitted: true, requirements: { currently_due: [], past_due: [], disabled_reason: null } } + const r = await postWebhook(ereignis('account.updated', konto), { path: PFAD, secret: SECRET }) + assert.equal(r.status, 200) + assert.equal(JSON.parse(aw.docs('shopConfig').get('f_shop1').stripeStatus).charges_enabled, true) +})