Files
Webklar-Kundenbereich/server/routes/funktionen.js
T
knsoandClaude Opus 5.5 b45108c1de Funktionen: Stripe-Sitzungs-ID bis 128 Zeichen, keine Rohmeldungen beim Bestellen
Live-Fehler: cs_live-Sitzungs-IDs (~66 Zeichen) passten nicht in das Feld
portalFeatures.stripeSessionId (64), die Bestellung brach nach dem Anlegen
der Stripe-Sitzung ab und der Kunde sah die Appwrite-Meldung.
- Setup-Skript: Feld 128 Zeichen, bestehendes Feld wird per ensureStringSize vergroessert
- Fehler von Appwrite oder unerwartete Fehler: Log + verstaendlicher Hinweis

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:58:44 +00:00

114 lines
5.1 KiB
JavaScript

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, retrieveCheckoutSession } from '../services/stripeBilling.js'
import {
DISABLED_MESSAGE,
assertOwnProject,
backfillInvoiceLinks,
getFeatureDoc,
listCustomerFeatures,
markOrdered,
runActivation,
} from '../services/siteFeatures.js'
const router = Router()
const DAY_MS = 24 * 60 * 60 * 1000
// Eigene Fehler (httpError) tragen einen Text fuer den Kunden. Fehler von Appwrite (err.type) oder
// unerwartete Fehler werden nur geloggt; der Kunde sieht einen verstaendlichen Hinweis statt der Rohmeldung.
function fail(res, err, fallback) {
const own = err.status >= 400 && err.status < 500 && !err.type
if (own) return res.status(err.status).json({ error: err.message })
console.error('[funktionen]', err.status || '', err.type || '', err.message)
return res.status(500).json({ error: fallback })
}
// Zustaende der Funktionen je Projekt; geschlossene Einrichtungs-Tickets dieses Kunden werden vorher freigeschaltet
router.get('/', requireSession, async (req, res) => {
try {
const customerId = getSessionCustomerId(req)
// Ein Fehler beim Freischalten (z. B. Ticketsystem) darf die Liste nicht verhindern
await runActivation({ customerId }).catch((err) => console.error('[funktionen] Freischalten fehlgeschlagen:', err.message))
await backfillInvoiceLinks(customerId)
res.json({
projects: await listCustomerFeatures(customerId),
setupEstimate: config.funktionen.setupEstimate,
consentText: config.funktionen.consentText,
})
} catch (err) {
fail(res, err, 'Die Funktionen konnten gerade nicht geladen werden. Bitte lade die Seite gleich noch einmal.')
}
})
/**
* Offene Bestellung: die gespeicherte Stripe-Sitzung entscheidet. open -> dieselbe Zahlungsseite nochmal
* (z. B. nach "Zurueck" im Checkout), expired -> null (neue Sitzung), complete -> 409 (Zahlung laeuft
* oder ist da, der Webhook richtet ein). Ist Stripe nicht erreichbar, gilt wie frueher: frisch (< 24 h) -> 409.
*/
async function openOrderSession(doc) {
const conflict = () => Object.assign(new Error('Die News-Pinnwand ist für dieses Projekt schon bestellt.'), { status: 409 })
if (!doc.stripeSessionId) return null
let session
try {
session = await retrieveCheckoutSession(doc.stripeSessionId)
} catch (err) {
console.warn('[funktionen] Sitzung nicht abrufbar:', err.message)
if (Date.now() - new Date(doc.orderedAt || 0).getTime() < DAY_MS) throw conflict()
return null
}
if (session.status === 'open' && session.url) return { id: session.id, url: session.url }
if (session.status === 'expired') return null
throw conflict()
}
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)
// 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/abgeschaltet -> 409, eine offene Bestellung
// richtet sich nach ihrer gespeicherten Stripe-Sitzung
const existing = await getFeatureDoc(customerId, projectId, 'news')
if (existing?.state === 'setup' || existing?.state === 'active') {
throw Object.assign(new Error('Die News-Pinnwand ist für dieses Projekt schon bestellt.'), { status: 409 })
}
if (existing?.state === 'disabled') throw Object.assign(new Error(DISABLED_MESSAGE), { status: 409 })
if (existing?.state === 'ordered') {
const open = await openOrderSession(existing)
if (open) return open
}
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) {
fail(res, err, 'Die Bestellung konnte gerade nicht gestartet werden. Bitte versuch es später noch einmal oder schreib uns im Chat.')
}
})
export default router