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

+13
View File
@@ -24,3 +24,16 @@ export function shopFuerKunde(email) {
const adresse = String(email || '').trim().toLowerCase()
return adresse !== '' && erlaubt.includes(adresse)
}
/**
* Verlangt die Aktivierung durch das Team (WOMS, ohne Bestellung) die Zustimmung des Kunden in Textform (AVV Webshop,
* Vorbemerkung Abs. 2)? Wie beim Terminplaner (termineTextformNoetig): nur im Modus on und nur, wenn die Adresse des Kunden nicht
* in SHOP_PILOT_EMAILS steht. '*' ist keine Pilotadresse und befreit niemanden. In off und pilot sprechen Mitarbeiter und Kunde
* direkt miteinander (Pilotbetrieb).
*/
export function shopTextformNoetig(email) {
if (shopModus() !== 'on') return false
const adresse = String(email || '').trim().toLowerCase()
if (adresse === '' || adresse === '*') return true
return !config.shop.pilotEmails.includes(adresse)
}
+28 -9
View File
@@ -543,38 +543,57 @@ async function assertAboLaeuftFuerAktivierung(customerId) {
const TEXTFORM_WEG = { chat: 'Chat im Kundenportal', email: 'E-Mail' }
/** Funktionen, die das Team mit Zustimmung des Kunden in Textform aktivieren kann (Modus on), und die Fassung der Unterlagen, auf die sie sich bezieht. */
export const TEXTFORM_KEYS = ['termine', 'shop']
export const textformFassung = (key) => (key === 'shop' ? config.shop.fassung : config.termine.avvVersion)
/**
* Text des Nachweises, wenn der Kunde dem Terminplaner in Textform zugestimmt hat (Aktivierung durch das Team im Modus on, F13a).
* textform = { date: 'JJJJ-MM-TT', channel: 'chat' | 'email' } (validateTextform); fassung = config.termine.avvVersion.
* Text des Nachweises, wenn der Kunde dem Terminplaner oder dem Webshop in Textform zugestimmt hat (Aktivierung durch das Team im
* Modus on, F13a). textform = { date: 'JJJJ-MM-TT', channel: 'chat' | 'email' } (validateTextform); fassung = textformFassung(key).
*/
export function textformNachweisText({ date, channel }, fassung) {
export function textformNachweisText({ date, channel }, fassung, key = 'termine') {
const [jahr, monat, tag] = String(date).split('-')
if (key === 'shop') {
return `Der Kunde hat am ${tag}.${monat}.${jahr} per ${TEXTFORM_WEG[channel]} in Textform erklärt, dass er der Leistungsbeschreibung Webshop und dem Auftragsverarbeitungsvertrag Webshop (Fassung ${fassung}) zustimmt und die folgenden Erklärungen der Bestelltexte der Fassung ${fassung} abgibt.`
}
return `Der Kunde hat der Leistungsbeschreibung Terminplaner und dem Auftragsverarbeitungsvertrag Terminplaner (Fassung ${fassung}) am ${tag}.${monat}.${jahr} per ${TEXTFORM_WEG[channel]} in Textform zugestimmt und bestätigt, dass sein Angebot nicht im Gesundheitsbereich liegt.`
}
// Erklaerungen der Webshop-Bestelltexte, die auch bei der Aktivierung in Textform gelten (ohne Widerrufsverlangen und Pflichtinformationen:
// die gehoeren zur kostenpflichtigen Online-Bestellung)
const SHOP_TEXTFORM_ERKLAERUNGEN = ['unterlagen', 'verantwortung', 'stripe']
/** Texte des Nachweises in Speicherreihenfolge: Termine = ein Satz; Webshop = Satz mit Datum und Weg, danach der Wortlaut der Erklaerungen der Fassung. */
function textformNachweisTexte(textform, fassung, key) {
const satz = { id: 'textform', text: textformNachweisText(textform, fassung, key) }
if (key !== 'shop') return [satz]
const items = featureConfig('shop').consent.items.filter((i) => SHOP_TEXTFORM_ERKLAERUNGEN.includes(i.id))
return [satz, ...items.map(({ id, text }) => ({ id, text }))]
}
/**
* Freischaltung ohne Zahlung und ohne Ticket (unlockedByPurchase bleibt false). Ausnahme: ein Terminplaner, den ein Kauf
* freigeschaltet hat (unlockedByPurchase true), wird nur bei laufendem Wartungs-Abo aktiviert (409 sonst, Lesefehler => Fehler).
* News und Dokumente ohne Kauf (Pilot, Kulanz) sind nicht betroffen.
* textform (nur Terminplaner, schon geprueft: { date, channel }): Zustimmung des Kunden in Textform. Der Nachweis (featureConsents, ohne
* Stripe-Sitzung, IP und Browser; Konto = Mitarbeiter) wird VOR der Freischaltung gespeichert: scheitert er, bleibt der Terminplaner aus.
* Der Aufrufer sendet danach K16 mit den Unterlagen (meldeFreigeschaltet).
* textform (nur Terminplaner und Webshop, schon geprueft: { date, channel }): Zustimmung des Kunden in Textform. Der Nachweis
* (featureConsents, ohne Stripe-Sitzung, IP und Browser; Konto = Mitarbeiter; Fassung = textformFassung(key)) wird VOR der Freischaltung
* gespeichert: scheitert er, bleibt die Funktion aus. Der Aufrufer sendet danach K16 mit den Unterlagen (meldeFreigeschaltet).
*/
export async function adminActivate(customerId, projectId, key, { by, adminNote, textform = null, now = new Date() } = {}) {
assertKey(key)
if (textform && key !== 'termine') throw httpError(400, 'Zustimmung in Textform gibt es nur für den Terminplaner')
if (textform && !TEXTFORM_KEYS.includes(key)) throw httpError(400, 'Zustimmung in Textform gibt es nur für den Terminplaner und den Webshop')
await assertOwnProject(customerId, projectId)
const doc = await ensureFeatureDoc(customerId, projectId, key)
if (key === 'termine' && doc.unlockedByPurchase === true) await assertAboLaeuftFuerAktivierung(customerId)
if (textform) {
const fassung = config.termine.avvVersion
const fassung = textformFassung(key)
await speichereZustimmung({
customerId,
projectId,
featureKey: key,
stripeSessionId: '',
fassungen: { leistungsbeschreibung: fassung, avv: fassung },
texte: [{ id: 'textform', text: textformNachweisText(textform, fassung) }],
texte: textformNachweisTexte(textform, fassung, key),
ip: '',
userAgent: '',
konto: `Mitarbeiter: ${by || 'Mitarbeiter'}`,