Öffnungszeiten: Einschalten im Wartungs-Abo

POST /api/funktionen/zeiten/einschalten (Gratisweg, Spec 3.2): Abo frisch
prüfen, Zustimmung (consentFrei) und Nachweis ohne Stripe-Sitzung, Zustand
setup mit unlockedBy abo, dann Neubau, Einbau-Ticket und K24; ein zweiter
Klick holt Fehlendes nach (holeZeitenNach, eigene Sperre). GET /api/funktionen
liefert das Angebot (zeiten), features.zeiten mit unlockedBy, aboEndeAt, einbau,
loeschungAm sowie zeitenStatus und zeitenLeer je Projekt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 02:03:49 +00:00
1 parent 8d670ca5dc
commit 66793d06d5
9 files changed
+1124 -58

No files matched your search

+44 -3
View File
@@ -1,6 +1,6 @@
import { config } from '../config.js'
import { aboMemo } from './aboStatus.js'
import { oeffentlicheAnsicht } from './featureRegistry.js'
import { aboMemo, bestellbar } from './aboStatus.js'
import { featureConfig } from './featureRegistry.js'
import { istObjekt } from './termine/ablauf.js'
import { holeOderNull, ladeEinstellung } from './termine/kontext.js'
@@ -31,6 +31,16 @@ const ZUSTIMMUNG_FEHLT = 'Bitte stimme der Erklärung zu, um zu bestellen.'
const FASSUNG_VERALTET = 'Die Erklärungen wurden inzwischen geändert. Bitte lies sie noch einmal und stimme neu zu.'
const MAX_HAKEN = 50
/**
* Sicht der Zustimmung fuer den Browser (Bestellseite, Antwort bei veralteter Fassung): Fassung, Knopf und alle Texte; ob ein Punkt
* einen Haken braucht, steht je Punkt. Fuer feature.consent und, beim Gratisweg, freiweg(feature).consent.
*/
export const oeffentlicheConsent = (consent) => ({
version: consent.version,
knopf: consent.knopf,
items: consent.items.map((item) => ({ id: item.id, text: item.text, haken: Boolean(item.haken) })),
})
const fehlt = () => bestellFehler(400, ZUSTIMMUNG_FEHLT, 'zustimmung_fehlt')
const hatWert = (wert) => wert !== undefined && wert !== null
@@ -63,7 +73,8 @@ export function pruefeZustimmung(feature, eingabe) {
const { version, haken } = eingabe.zustimmung
if (typeof version !== 'string' || !Array.isArray(haken) || haken.length > MAX_HAKEN) throw fehlt()
if (version !== feature.consent.version) {
throw bestellFehler(409, FASSUNG_VERALTET, 'fassung_veraltet', { consent: oeffentlicheAnsicht(feature.key).consent })
// die Fassung des geprueften Eintrags (beim Gratisweg die Gratis-Zustimmung), nicht die des Registers
throw bestellFehler(409, FASSUNG_VERALTET, 'fassung_veraltet', { consent: oeffentlicheConsent(feature.consent) })
}
if (!haken.every((id) => typeof id === 'string' && ids.includes(id))) throw fehlt()
const gesetzt = new Set(haken)
@@ -132,3 +143,33 @@ export async function pruefeAbo(feature, customerId) {
if (!Object.hasOwn(ABO_TEXTE, grund) || grund === 'unbekannt') throw new Error('Abo-Prüfung ohne gültiges Ergebnis')
throw bestellFehler(409, aboMeldung(feature, grund), grund, { grund })
}
const GRATIS_ABO_TEXT = {
kein_abo_gekuendigt: (name, preis) => `Ohne laufendes Wartungs-Abo kannst Du ${name} für einmalig ${preis} bestellen.`,
nicht_aktiv: () => 'Einschaltbar, sobald Dein Abo aktiv ist.',
}
const GERADE_NICHT_PRUEFBAR = 'Gerade nicht prüfbar – bitte versuch es gleich noch einmal.'
/**
* Abo-Pruefung des Gratisweges "Einschalten" von Oeffnungszeiten live (Spec 3.2 Schritt 1): immer frisch gelesen (bestellbar direkt,
* nie aboMemo), gedacht fuer den Aufruf unter der Sperre feature-order. Laeuft das Wartungs-Abo und ist es aktiv, geschieht nichts.
* kein_abo und gekuendigt: 409 (code und body.grund wie der Grund) mit dem Hinweis auf den 30-Euro-Weg; nicht_aktiv: 409 "Einschaltbar,
* sobald Dein Abo aktiv ist." Ein Lesefehler ist 503 (fail-closed, nie stillschweigend erlaubt); im Log stehen nur Art und Status.
*/
export async function pruefeAboFuerEinschalten(customerId) {
const feature = featureConfig('zeiten')
let ergebnis
try {
ergebnis = await bestellbar(customerId)
} catch (err) {
console.error(`[funktionen] Abo für das Einschalten nicht lesbar: ${err?.name ?? 'Fehler'} ${err?.status ?? ''}`.trim())
throw Object.assign(new Error(GERADE_NICHT_PRUEFBAR), { status: 503 })
}
if (ergebnis?.ok === true) return
const grund = ergebnis?.grund
if (grund === 'nicht_aktiv') throw bestellFehler(409, GRATIS_ABO_TEXT.nicht_aktiv(), grund, { grund })
if (grund === 'kein_abo' || grund === 'gekuendigt') {
throw bestellFehler(409, GRATIS_ABO_TEXT.kein_abo_gekuendigt(feature.label.name, feature.priceLabel), grund, { grund })
}
throw new Error('Abo-Prüfung ohne gültiges Ergebnis')
}