Checkout je Funktion: Vorpruefungen in der Reihenfolge der Spezifikation
POST /api/funktionen/:key/checkout ersetzt /news/checkout (der News-Pfad bleibt als key = news erhalten). Stufen ohne Seiteneffekt: 1 Kundenansicht (403), 2 Schluessel gegen das Register (404), 3 Zustimmung (400 zustimmung_fehlt, 409 fassung_veraltet mit der Serverfassung; News weiter mit consent: true), 4 Projekt (400/403; bei fortsetzen muss das Dokument ordered sein), 5 Schalter (409 nicht_verfuegbar), 5b Unterlagen (503 unterlagen_fehlen), 6 Bestellpruefung (409 bestellung_offen mit offen), 7 Wartungs-Abo (409 kein_abo | gekuendigt | nicht_aktiv, Lesefehler 500). fail() gibt err.body weiter. featureOrder.js: pruefeZustimmung, istFortsetzen, aboMeldung (einzige Quelle der Abo-Hinweistexte), ladeEntwurf, pruefeAbo, orderDeps. featureRegistry.js: featureSperre und die Standard-Haken verfuegbar, bestellpruefung (mitAbo: false, ohne abo) und unterlagenBereit des Terminplaners. Der Rest der Route (Sperre, Zustand, Sitzung, Nachweis) bleibt bis Task B7 nur fuer News; andere Funktionen enden mit 503. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d568a3798d
commit
ded6bb2a77
5 files changed
+960
-11
No files matched your search
@@ -15,6 +15,8 @@ import {
|
||||
runActivation,
|
||||
} from '../services/siteFeatures.js'
|
||||
import { termineFuerKunde } from '../services/termine/schalter.js'
|
||||
import { featureConfig, featureHaken, featureSperre } from '../services/featureRegistry.js'
|
||||
import { bestellFehler, istFortsetzen, pruefeAbo, pruefeZustimmung } from '../services/featureOrder.js'
|
||||
|
||||
import {
|
||||
ELIGIBILITY_MESSAGES,
|
||||
@@ -32,9 +34,10 @@ 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.
|
||||
// err.body (Checkout-Vorpruefungen: code, consent, offen, grund) geht mit in die Antwort, error bleibt der Text.
|
||||
function fail(res, err, fallback) {
|
||||
const own = ((err.status >= 400 && err.status < 500) || err.status === 503) && !err.type
|
||||
if (own) return res.status(err.status).json({ error: err.message })
|
||||
if (own) return res.status(err.status).json({ ...err.body, error: err.message })
|
||||
console.error('[funktionen]', err.status || '', err.type || '', err.message)
|
||||
return res.status(500).json({ error: fallback })
|
||||
}
|
||||
@@ -134,20 +137,62 @@ async function openOrderSession(doc) {
|
||||
throw conflict()
|
||||
}
|
||||
|
||||
router.post('/news/checkout', requireSession, async (req, res) => {
|
||||
// Checkout je Funktion (Register config.features); /news/checkout ist derselbe Pfad mit key = news.
|
||||
// Vorpruefungen in der Reihenfolge der Spezifikation, jede Stufe ohne Seiteneffekt (keine Sperre, keine Stripe-Sitzung,
|
||||
// kein Dokument, kein Nachweis): 1 Kundenansicht, 2 Funktionsschluessel, 3 Zustimmung, 4 Projekt, 5 Schalter,
|
||||
// 5b Unterlagen, 6 Bestellpruefung, 7 Wartungs-Abo.
|
||||
router.post('/:key/checkout', requireSession, async (req, res) => {
|
||||
try {
|
||||
// 1 Kundenansicht
|
||||
if (req.session.viewAs) {
|
||||
return res.status(403).json({ error: 'In der Kundenansicht 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.' })
|
||||
}
|
||||
// 2 Funktionsschluessel (analyse hat einen eigenen Weg und steht nicht im Register)
|
||||
const key = req.params.key
|
||||
const feature = featureConfig(key)
|
||||
if (!feature) return res.status(404).json({ error: 'Unbekannte Funktion' })
|
||||
const body = req.body || {}
|
||||
// 3 Zustimmung: alle Pflicht-Haken und die aktuelle Fassung; bei "Zahlung fortsetzen" entfaellt sie (Stufe 4 prueft dafuer ordered)
|
||||
const fortsetzen = istFortsetzen(body)
|
||||
if (!fortsetzen) pruefeZustimmung(feature, body)
|
||||
// 4 Projekt
|
||||
const { projectId } = body
|
||||
if (typeof projectId !== 'string' || !projectId) {
|
||||
return res.status(400).json({ error: 'Bitte wähle ein Projekt.' })
|
||||
}
|
||||
const customerId = getSessionCustomerId(req)
|
||||
await assertOwnProject(customerId, projectId)
|
||||
if (fortsetzen && (await getFeatureDoc(customerId, projectId, key))?.state !== 'ordered') {
|
||||
throw bestellFehler(400, 'Bitte stimme der Erklärung zu, um zu bestellen.', 'zustimmung_fehlt')
|
||||
}
|
||||
// 5 Schalter (Terminplaner: TERMINE_ENABLED und Pilotliste)
|
||||
const sperre = featureSperre(key, req.session.email)
|
||||
if (sperre) throw bestellFehler(409, sperre, 'nicht_verfuegbar')
|
||||
// 5b Vertragsunterlagen abrufbar (vor jeder Stripe-Sitzung)
|
||||
const haken = featureHaken(key)
|
||||
if (haken.unterlagenBereit && !(await haken.unterlagenBereit())) {
|
||||
throw bestellFehler(
|
||||
503,
|
||||
'Die Vertragsunterlagen sind gerade nicht abrufbar. Bitte versuch es später noch einmal oder schreib uns im Chat.',
|
||||
'unterlagen_fehlen'
|
||||
)
|
||||
}
|
||||
// 6 Bestellpruefung: offene Punkte des Entwurfs. Ohne abo: den Abo-Punkt (h) wendet der Checkout nicht an, das tut Stufe 7
|
||||
if (haken.bestellpruefung) {
|
||||
const offen = await haken.bestellpruefung({ customerId, projectId, feature })
|
||||
if (!Array.isArray(offen)) throw new TypeError('bestellpruefung muss eine Liste liefern')
|
||||
if (offen.length > 0) {
|
||||
throw bestellFehler(409, 'Die Bestellung ist noch nicht vollständig. Bitte erledige zuerst die offenen Punkte.', 'bestellung_offen', { offen })
|
||||
}
|
||||
}
|
||||
// 7 Wartungs-Abo (nur Funktionen mit requiresWartungsAbo); die einzige Quelle von kein_abo | gekuendigt | nicht_aktiv
|
||||
await pruefeAbo(feature, customerId)
|
||||
|
||||
// Task B7 ersetzt den Rest: Sperre, Zustand, Stripe-Sitzung, Nachweis und markOrdered je Funktion aus dem Register.
|
||||
// Bis dahin laeuft nur News; fuer andere Funktionen entsteht weder Sitzung noch Dokument.
|
||||
if (key !== 'news') {
|
||||
throw bestellFehler(503, 'Die Bestellung ist für diese Funktion noch nicht freigeschaltet.', 'bestellung_nicht_freigeschaltet')
|
||||
}
|
||||
// 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 () => {
|
||||
|
||||
Reference in new issue
Block a user