Merge remote-tracking branch 'origin/main' into feature/oeffnungszeiten

Zusammenführung von TP2-Abschluss und Webshop (origin/main cab9c33) mit Öffnungszeiten live. Beide
Funktionen bleiben vollständig: jeder Konflikt ist als Vereinigung gelöst, Reihenfolge der Funktionsschlüssel
überall news, termine, shop, zeiten.

Konfliktdateien (28): public/app.js, dashboard.html, funktionen.js, widerrufen.html; scripts/stripe-products.mjs;
server/config.js, index.js, middleware/kundenansichtGuard.js, routes/abo.js, routes/admin/funktionenAdmin.js,
routes/funktionen.js, services/customerAdmin.js, customerViewLog.js, featureRegistry.js, retention.js,
siteFeatures.js, stripeBilling.js; test/customerViewLog, featureFreischaltung, featureRegistry, funktionenRoutes,
funktionenTermine, kundenansichtGuard, mailTemplates, publicForms, siteFeatures, stripeProductsScript (je
.test.js) und test/support/schema.js.

Entscheidungen: Mailvorlagen G01–G15, K01–K30, T01–T21 lückenlos (K24–K26 Webshop, K27–K30 Öffnungszeiten);
Stripe-Katalog mit shopSetup und zeitenSetup; Widerruf kennt shop und zeiten; ABO_FUNKTIONEN bleibt analyse
und termine (kein zeiten); activate() schreibt die Zeiten-Felder nur über extra; kein Halter von
feature-activate:<id> ruft disable() auf; die Aufbewahrungs-Löschung löscht Zeiten-Daten vor dem Abschalten und
hält den Kunden bei einem Fehler (held); die Admin-Antwort trägt ersatzTicket und warnung; Hash-Routen webshop
und zeiten, Ansichten view-webshop und view-zeiten; assertOwnProject behält die Meldung von main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-06 06:12:23 +00:00
commit e87140e5ba
245 files changed
+38799 -1050

No files matched your search

+8 -2
View File
@@ -19,6 +19,7 @@ import {
} from '../services/siteFeatures.js'
import { actingPerson, speichereNachweisAusAnfrage, zustimmungFuerSitzung } from '../services/featureConsents.js'
import { termineFuerKunde } from '../services/termine/schalter.js'
import { shopFuerKunde } from '../services/shop/schalter.js'
import { zeitenFuerKunde } from '../services/zeiten/schalter.js'
import { einschaltenDeps, einschaltenZeiten, zeitenProjektAnsicht } from '../services/zeiten/einschalten.js'
import { starteBehalten } from '../services/zeiten/behalten.js'
@@ -75,6 +76,7 @@ async function analyseView(customerId, projectId) {
avvVersion: config.analyse.avvVersion,
legal: legalPaths(config.analyse.avvVersion),
controller,
activatedAt: doc?.state === 'active' ? doc.activatedAt || null : null,
}
}
@@ -233,6 +235,9 @@ router.get('/', requireSession, async (req, res) => {
// Terminplaner: Schalter TERMINE_ENABLED (off | pilot | on); nur wenn verfuegbar zeigt das Portal die Kachel, dann auch
// mit dem Abo des Kunden (bestellbar, grund) und dem Preis des Registers
termine: await termineAngebot(req.session.email, customerId, abos),
// Webshop: Schalter SHOP_ENABLED (off | pilot | on); nur wenn verfuegbar zeigt das Portal die Kachel; Anfrage und Zustand je Projekt
// kommen aus projects[].features.shop und GET /api/shop/status
shop: { verfuegbar: shopFuerKunde(req.session.email) },
// Oeffnungszeiten live: Schalter ZEITEN_ENABLED (off | pilot | on); nur wenn verfuegbar zeigt das Portal die Kachel, dann mit
// dem Abo-Stand (inklusive, grund), dem Preis und den Texten des Gratisweges "Einschalten"
zeiten: await zeitenAngebot(req.session.email, customerId, abos),
@@ -304,7 +309,7 @@ router.post('/:key/checkout', requireSession, async (req, res) => {
return res.status(400).json({ error: 'Bitte wähle ein Projekt.' })
}
const customerId = getSessionCustomerId(req)
await assertOwnProject(customerId, projectId)
await assertOwnProject(customerId, projectId, { forNewOrder: true })
if (fortsetzen && (await getFeatureDoc(customerId, projectId, key))?.state !== 'ordered') {
throw bestellFehler(400, ZUSTIMMUNG_NOETIG, 'zustimmung_fehlt')
}
@@ -365,7 +370,7 @@ router.post('/:key/checkout', requireSession, async (req, res) => {
projectId,
key,
sessionId: created.id,
consent: { text: hakenTexte(feature), at: klickAt, ip: req.ip, userAgent: req.get('user-agent') },
consent: { text: hakenTexte(feature), at: klickAt },
})
return created
})
@@ -468,6 +473,7 @@ router.post('/analyse/activate', analyseGuard, async (req, res) => {
return res.status(403).json({ error: 'In der Kundenansicht kann nichts geändert werden.' })
}
const { projectId, accentColor, consent } = req.body || {}
await assertOwnProject(getSessionCustomerId(req), analyseProjectId(projectId), { forNewOrder: true })
const { doc } = await activateAnalyse({
customerId: getSessionCustomerId(req),
projectId: analyseProjectId(projectId),