Buchungskalender: Verdrahtung absichern (Outbox, Shop-Webhook, Session)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-07 14:02:41 +00:00
1 parent a68baf10d9
commit d7b5b98187
6 files changed
+45 -4

No files matched your search

+4
View File
@@ -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 }`.
+6 -1
View File
@@ -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
+3
View File
@@ -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
+2 -1
View File
@@ -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),
+1 -1
View File
@@ -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')
+29 -1
View File
@@ -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)
})