Buchungskalender: Verdrahtung absichern (Outbox, Shop-Webhook, Session)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a68baf10d9
commit
d7b5b98187
6 files changed
+45
-4
No files matched your search
@@ -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} }`.
|
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`)
|
## Ö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 }`.
|
- `GET /:siteKey` → `{ aktiv:true, anbieter:{ name, ort, datenschutzUrl }, akzentfarbe, formToken, unterkuenfte:[{ id, name, maxPersonen, mindestNaechte, checkIn, checkOut }] }` oder `{ aktiv:false }`.
|
||||||
|
|||||||
@@ -43,7 +43,12 @@ async function verarbeite(event, now) {
|
|||||||
if (event.type === 'account.updated') {
|
if (event.type === 'account.updated') {
|
||||||
await kontoAktualisiert(accountId || objekt.id, objekt, now)
|
await kontoAktualisiert(accountId || objekt.id, objekt, now)
|
||||||
// dasselbe Konto kann auch im Buchungskalender haengen
|
// 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
|
return
|
||||||
}
|
}
|
||||||
if (!accountId) return
|
if (!accountId) return
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Router } from 'express'
|
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).
|
* 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).
|
* und Stripe; Task 11 die Anfrage-Funktion (/anfrage-funktion).
|
||||||
*/
|
*/
|
||||||
const router = Router()
|
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
|
export default router
|
||||||
@@ -225,7 +225,8 @@ export async function einreihen(
|
|||||||
text: mail.text,
|
text: mail.text,
|
||||||
ics: ics || '',
|
ics: ics || '',
|
||||||
icsMethod: ics ? icsMethod : '',
|
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',
|
status: 'wartet',
|
||||||
versuche: 0,
|
versuche: 0,
|
||||||
naechsterVersuch: iso(nowMs),
|
naechsterVersuch: iso(nowMs),
|
||||||
|
|||||||
@@ -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)
|
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')
|
assert.equal((await lies(mit.$id)).anhang, 'uk-rechnung:r_123')
|
||||||
const ohne = await einreihen(eintrag({ bookingId: 'b_ohne' }), JETZT)
|
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 assert.rejects(() => einreihen(eintrag({ anhang: 'x'.repeat(501) }), JETZT), TypeError)
|
||||||
await warteAufVersand()
|
await warteAufVersand()
|
||||||
assert.equal(tr.senke.sent.length, 2, 'Versand nicht verändert')
|
assert.equal(tr.senke.sent.length, 2, 'Versand nicht verändert')
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ const jobs = await import('../server/services/unterkunft/jobs.js')
|
|||||||
const { default: publicRoutes } = await import('../server/routes/publicUnterkunft.js')
|
const { default: publicRoutes } = await import('../server/routes/publicUnterkunft.js')
|
||||||
const { default: webhookRoutes } = await import('../server/routes/shopWebhook.js')
|
const { default: webhookRoutes } = await import('../server/routes/shopWebhook.js')
|
||||||
const W = await import('../server/routes/unterkunftWebhook.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 () => {
|
after(async () => {
|
||||||
await app.close()
|
await app.close()
|
||||||
await aw.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('acct_unbekannt', konto, T0), 0)
|
||||||
assert.equal(await W.kontoAktualisiertUk('kein-konto', 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)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user