Shop Review B I-8: Aktivierung des Webshops durch das Team nur mit Zustimmung in Textform

AVV Webshop, Vorbemerkung Abs. 2: Das Team darf den Webshop ohne Online-Bestellung
nur einschalten, wenn der Kunde vorher in Textform zugestimmt hat. Die Textform-Aktivierung
gilt jetzt wie beim Terminplaner auch fuer 'shop' (Modus on, Pilotadressen ausgenommen,
shopTextformNoetig): Nachweis in featureConsents mit Fassung (config.shop.fassung), Datum,
Weg, Mitarbeiter und dem Wortlaut der Erklaerungen der Bestelltexte, K16 mit
Leistungsbeschreibung und AVV als PDF. GET /api/team/funktionen/:kunde liefert zusaetzlich
shop { textformNoetig, avvVersion, links }.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 09:44:11 +00:00
1 parent a129bdb0ec
commit 1b7aa9ebe3
5 files changed
+390 -24

No files matched your search

+33 -14
View File
@@ -9,6 +9,7 @@ import { featureLegalAttachments, featureLegalPaths } from '../../services/legal
import {
FEATURE_KEYS,
FEATURE_LABELS,
TEXTFORM_KEYS,
adminActivate,
assertOwnProject,
customerOf,
@@ -17,11 +18,13 @@ import {
getFeatureDoc,
listCustomerFeatureDocs,
listCustomerFeatures,
textformFassung,
ticketNachlegen,
} from '../../services/siteFeatures.js'
import { driftWarnung, einbauanleitung, ticketStand } from '../../services/termine/einbauanleitung.js'
import { freischaltHindernis, meldeFreigeschaltet } from '../../services/termine/freischaltung.js'
import { ladeStand } from '../../services/termine/kontext.js'
import { shopTextformNoetig } from '../../services/shop/schalter.js'
import { termineTextformNoetig } from '../../services/termine/schalter.js'
/**
@@ -89,29 +92,34 @@ async function notiereK16Fehlt(projectId, doc) {
const ticketExists = async (ticketId) => (await readTicket(ticketId)) !== null
/** Verlangt die Aktivierung dieser Funktion fuer diese Adresse die Zustimmung in Textform? Nur Terminplaner und Webshop, je nach Modus und Pilotliste der Funktion. */
const textformVerlangt = { termine: termineTextformNoetig, shop: shopTextformNoetig }
/**
* Zustimmung des Kunden in Textform fuer die Aktivierung des Terminplaners durch das Team (F13a). null, wenn sie entfaellt: andere
* Funktion, Modus off oder pilot, Pilotadresse (termineTextformNoetig) oder ein Dokument, das nichts mehr aktiviert und dem nichts
* fehlt (schon aktiv, oder durch Kauf freigeschaltet, dann liegt die Zustimmung des Checkouts vor). Sonst { date, channel }; fehlt
* oder ist eine Angabe ungueltig (Datum vor der Fassung oder in der Zukunft, kein Weg), 400 - ohne dass etwas angelegt wird.
* Zustimmung des Kunden in Textform fuer die Aktivierung des Terminplaners oder des Webshops durch das Team (F13a; AVV Webshop,
* Vorbemerkung Abs. 2). null, wenn sie entfaellt: andere Funktion, Modus off oder pilot, Pilotadresse (termineTextformNoetig,
* shopTextformNoetig) oder ein Dokument, das nichts mehr aktiviert und dem nichts fehlt (schon aktiv, oder durch Kauf freigeschaltet,
* dann liegt die Zustimmung des Checkouts vor). Sonst { date, channel }; fehlt oder ist eine Angabe ungueltig (Datum vor der Fassung
* oder in der Zukunft, kein Weg), 400 - ohne dass etwas angelegt wird.
*/
async function textformFuerAktivierung({ body, customerId, key, doc }) {
if (key !== 'termine') return null
if (!TEXTFORM_KEYS.includes(key)) return null
if (doc?.state === 'active' || doc?.unlockedByPurchase === true) return null
const kunde = await customerOf(customerId)
if (!termineTextformNoetig(kunde?.email)) return null
if (!textformVerlangt[key](kunde?.email)) return null
// nur Texte: validateTextform macht aus jedem Wert einen Text (aus ['chat'] wuerde 'chat')
const text = (wert) => (typeof wert === 'string' ? wert : '')
return validateTextform({ date: text(body?.textformDate), channel: text(body?.textformChannel) }, { minDate: config.termine.avvVersion })
return validateTextform({ date: text(body?.textformDate), channel: text(body?.textformChannel) }, { minDate: textformFassung(key) })
}
/** Leistungsbeschreibung und AVV als PDF fuer die K16 der Textform-Aktivierung; nicht lesbar => 503, bevor etwas geaendert wird. */
async function unterlagenAlsAnhang() {
/** Leistungsbeschreibung und AVV der Funktion als PDF fuer die K16 der Textform-Aktivierung; nicht lesbar => 503, bevor etwas geaendert wird. */
async function unterlagenAlsAnhang(key) {
try {
return await featureLegalAttachments('termine', { ohne: ['widerruf'] })
return await featureLegalAttachments(key, { ohne: ['widerruf'] })
} catch (err) {
console.error('[funktionen-admin] Rechtsunterlagen (PDF) fuer die Freischaltung nicht lesbar:', err.message)
throw Object.assign(new Error('Die Rechtsunterlagen (PDF) sind nicht lesbar. Der Terminplaner wurde nicht freigeschaltet. Bitte später erneut versuchen.'), { status: 503 })
const funktion = key === 'shop' ? 'Der Webshop' : 'Der Terminplaner'
throw Object.assign(new Error(`Die Rechtsunterlagen (PDF) sind nicht lesbar. ${funktion} wurde nicht freigeschaltet. Bitte später erneut versuchen.`), { status: 503 })
}
}
@@ -150,10 +158,21 @@ async function termineFeld(customerId) {
}
}
/** Dasselbe fuer den Webshop (AVV Webshop, Vorbemerkung Abs. 2): Fassung = config.shop.fassung, Adressen der versionierten Unterlagen des Webshops. */
async function shopFeld(customerId) {
const kunde = await customerOf(customerId)
const { lb, avv } = featureLegalPaths('shop')
return {
textformNoetig: shopTextformNoetig(kunde?.email),
avvVersion: config.shop.fassung,
links: { lb: `${config.publicBaseUrl}${lb}`, avv: `${config.publicBaseUrl}${avv}` },
}
}
funktionenAdminRouter.get('/:customerId', async (req, res) => {
try {
const projects = await adminView(req.params.customerId)
res.json({ projects, termine: await termineFeld(req.params.customerId) })
res.json({ projects, termine: await termineFeld(req.params.customerId), shop: await shopFeld(req.params.customerId) })
} catch (err) {
sendError(res, err)
}
@@ -231,13 +250,13 @@ funktionenAdminRouter.post('/:customerId/:key', async (req, res) => {
}
await assertOwnProject(customerId, projectId)
const by = adminName(req)
let freigeschaltet = null // Terminplaner mit Zustimmung in Textform: K16 mit den Unterlagen nach der Sperre
let freigeschaltet = null // Terminplaner oder Webshop mit Zustimmung in Textform: K16 mit den Unterlagen nach der Sperre
await withLocks([`feature-admin:${projectId}`], async () => {
const doc = await getFeatureDoc(customerId, projectId, key)
const adminNote = composeAdminNote(doc?.adminNote, { by, action, note })
if (action === 'activate') {
const textform = await textformFuerAktivierung({ body: req.body, customerId, key, doc })
const attachments = textform ? await unterlagenAlsAnhang() : []
const attachments = textform ? await unterlagenAlsAnhang(key) : []
const aktiviert = await adminActivate(customerId, projectId, key, { by, adminNote, textform })
if (textform) freigeschaltet = { doc: aktiviert, attachments }
} else if (action === 'abnahme') {