Funktionen: Bestellung serialisiert, Sitzung nachtragen, Freischaltung einmalig
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0cc35bf1a5
commit
8374a570aa
3 files changed
+120
-34
No files matched your search
+23
-16
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user