Funktionen: Bestellung serialisiert, Sitzung nachtragen, Freischaltung einmalig

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-09-30 16:35:59 +00:00
1 parent 0cc35bf1a5
commit 8374a570aa
3 files changed
+120 -34

No files matched your search

+23 -16
View File
@@ -2,6 +2,7 @@ import { Router } from 'express'
import { config } from '../config.js'
import { getDocument } from '../services/appwriteAdmin.js'
import { getSessionCustomerId, requireSession } from '../middleware/session.js'
import { withLocks } from '../services/keyedLock.js'
import { createFeatureCheckout } from '../services/stripeBilling.js'
import { assertOwnProject, getFeatureDoc, listCustomerFeatures, markOrdered, runActivation } from '../services/siteFeatures.js'
@@ -17,7 +18,8 @@ function fail(res, err) {
router.get('/', requireSession, async (req, res) => {
try {
const customerId = getSessionCustomerId(req)
await runActivation({ customerId })
// Ein Fehler beim Freischalten (z. B. Ticketsystem) darf die Liste nicht verhindern
await runActivation({ customerId }).catch((err) => console.error('[funktionen] Freischalten fehlgeschlagen:', err.message))
res.json({ projects: await listCustomerFeatures(customerId), setupEstimate: config.funktionen.setupEstimate })
} catch (err) {
fail(res, err)
@@ -38,21 +40,26 @@ router.post('/news/checkout', requireSession, async (req, res) => {
}
const customerId = getSessionCustomerId(req)
await assertOwnProject(customerId, projectId)
// Zustand vor dem Stripe-Aufruf pruefen: bezahlt/aktiv oder eine noch gueltige offene Bestellung -> 409
const existing = await getFeatureDoc(customerId, projectId, 'news')
const freshOrder = existing?.state === 'ordered' && Date.now() - new Date(existing.orderedAt || 0).getTime() < DAY_MS
if (existing?.state === 'setup' || existing?.state === 'active' || freshOrder) {
return res.status(409).json({ error: 'Die News-Pinnwand ist für dieses Projekt schon bestellt.' })
}
const customer = await getDocument(config.collections.customers, customerId)
const now = new Date().toISOString()
const session = await createFeatureCheckout(customer, { projectId, key: 'news', consentAt: now })
await markOrdered({
customerId,
projectId,
key: 'news',
sessionId: session.id,
consent: { text: config.funktionen.consentText, at: now, ip: req.ip, userAgent: req.get('user-agent') },
// Pruefen, Sitzung anlegen und eintragen als Ganzes je (Kunde, Projekt): zwei schnelle Klicks duerfen keine
// zwei Sitzungen erzeugen, sonst ueberschreibt die zweite die ID der ersten und deren Zahlung findet kein Dokument
const session = await withLocks([`feature-order:${customerId}:${projectId}`], async () => {
// Zustand vor dem Stripe-Aufruf pruefen: bezahlt/aktiv oder eine noch gueltige offene Bestellung -> 409
const existing = await getFeatureDoc(customerId, projectId, 'news')
const freshOrder = existing?.state === 'ordered' && Date.now() - new Date(existing.orderedAt || 0).getTime() < DAY_MS
if (existing?.state === 'setup' || existing?.state === 'active' || freshOrder) {
throw Object.assign(new Error('Die News-Pinnwand ist für dieses Projekt schon bestellt.'), { status: 409 })
}
const customer = await getDocument(config.collections.customers, customerId)
const now = new Date().toISOString()
const created = await createFeatureCheckout(customer, { projectId, key: 'news', consentAt: now })
await markOrdered({
customerId,
projectId,
key: 'news',
sessionId: created.id,
consent: { text: config.funktionen.consentText, at: now, ip: req.ip, userAgent: req.get('user-agent') },
})
return created
})
res.json({ url: session.url })
} catch (err) {