Funktionen: Buchung per Stripe, Webhook, Mails und Freischalt-Job

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-09-30 16:33:48 +00:00
1 parent 770cbe32b7
commit 0cc35bf1a5
12 files changed
+642 -8

No files matched your search

+6
View File
@@ -18,6 +18,7 @@ import {
reconcilePendingSwitch,
switchToWartung,
} from '../services/planSwitch.js'
import { handleFeatureCheckoutEvent, isFeatureCheckoutEvent } from '../services/siteFeatures.js'
import { PRODUCT_KEYS, contractAttachmentFor, describeItems, isTermsV4OrNewer, productLabel, productView, termsDocFor } from '../services/products.js'
import {
ACTIVE_STATUSES,
@@ -518,6 +519,11 @@ async function recordAbschluss(sub, session) {
async function handleStripeEvent(event) {
const obj = event.data.object
// Einmalkauf einer Funktion (News-Pinnwand): eigener Weg, der Abo-Zweig unten bleibt unberuehrt
if (isFeatureCheckoutEvent(event)) {
await handleFeatureCheckoutEvent(event)
return
}
switch (event.type) {
case 'checkout.session.completed': {
if (obj.mode !== 'subscription' || !obj.subscription) return
+63
View File
@@ -0,0 +1,63 @@
import { Router } from 'express'
import { config } from '../config.js'
import { getDocument } from '../services/appwriteAdmin.js'
import { getSessionCustomerId, requireSession } from '../middleware/session.js'
import { createFeatureCheckout } from '../services/stripeBilling.js'
import { assertOwnProject, getFeatureDoc, listCustomerFeatures, markOrdered, runActivation } from '../services/siteFeatures.js'
const router = Router()
const DAY_MS = 24 * 60 * 60 * 1000
function fail(res, err) {
const status = err.status >= 400 && err.status < 600 ? err.status : 500
return res.status(status).json({ error: err.message || 'Interner Serverfehler' })
}
// Zustaende der Funktionen je Projekt; geschlossene Einrichtungs-Tickets dieses Kunden werden vorher freigeschaltet
router.get('/', requireSession, async (req, res) => {
try {
const customerId = getSessionCustomerId(req)
await runActivation({ customerId })
res.json({ projects: await listCustomerFeatures(customerId), setupEstimate: config.funktionen.setupEstimate })
} catch (err) {
fail(res, err)
}
})
router.post('/news/checkout', requireSession, async (req, res) => {
try {
if (req.session.viewAs) {
return res.status(403).json({ error: 'Im "Als Kunde ansehen"-Modus kann nicht bestellt werden.' })
}
const { projectId, consent } = req.body || {}
if (consent !== true) {
return res.status(400).json({ error: 'Bitte stimme der Erklärung zu, um zu bestellen.' })
}
if (typeof projectId !== 'string' || !projectId) {
return res.status(400).json({ error: 'Bitte wähle ein Projekt.' })
}
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') },
})
res.json({ url: session.url })
} catch (err) {
fail(res, err)
}
})
export default router