Terminplaner: Staff-Aktivierung nur mit Zustimmung in Textform (Modus on), Portal
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5b5b4b90eb
commit
bade4edb74
5 files changed
+381
-7
No files matched your search
@@ -1,9 +1,11 @@
|
||||
import { Router } from 'express'
|
||||
import { config } from '../../config.js'
|
||||
import { aboMemo } from '../../services/aboStatus.js'
|
||||
import { validateTextform } from '../../services/analyse.js'
|
||||
import { getDocument } from '../../services/appwriteAdmin.js'
|
||||
import { featureConfig } from '../../services/featureRegistry.js'
|
||||
import { withLocks } from '../../services/keyedLock.js'
|
||||
import { featureLegalAttachments, featureLegalPaths } from '../../services/legalDocs.js'
|
||||
import {
|
||||
FEATURE_KEYS,
|
||||
FEATURE_LABELS,
|
||||
@@ -18,8 +20,9 @@ import {
|
||||
ticketNachlegen,
|
||||
} from '../../services/siteFeatures.js'
|
||||
import { driftWarnung, einbauanleitung, ticketStand } from '../../services/termine/einbauanleitung.js'
|
||||
import { freischaltHindernis } from '../../services/termine/freischaltung.js'
|
||||
import { freischaltHindernis, meldeFreigeschaltet } from '../../services/termine/freischaltung.js'
|
||||
import { ladeStand } from '../../services/termine/kontext.js'
|
||||
import { termineTextformNoetig } from '../../services/termine/schalter.js'
|
||||
|
||||
/**
|
||||
* Funktionen (News-Pinnwand, Terminplaner) je Kundenprojekt im Ticketsystem (Kunden-Reiter "Funktionen").
|
||||
@@ -66,6 +69,32 @@ async function customerName(customerId) {
|
||||
|
||||
const ticketExists = async (ticketId) => (await readTicket(ticketId)) !== null
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
async function textformFuerAktivierung({ body, customerId, key, doc }) {
|
||||
if (key !== 'termine') return null
|
||||
if (doc?.state === 'active' || doc?.unlockedByPurchase === true) return null
|
||||
const kunde = await customerOf(customerId)
|
||||
if (!termineTextformNoetig(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 })
|
||||
}
|
||||
|
||||
/** Leistungsbeschreibung und AVV als PDF fuer die K16 der Textform-Aktivierung; nicht lesbar => 503, bevor etwas geaendert wird. */
|
||||
async function unterlagenAlsAnhang() {
|
||||
try {
|
||||
return await featureLegalAttachments('termine', { 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 })
|
||||
}
|
||||
}
|
||||
|
||||
// Die Funktionsdokumente des Kunden werden einmal gelesen und fuer alle Projekte und Funktionen verwendet.
|
||||
// hindernis (freischaltHindernis, termine/freischaltung.js): was die Freischaltung eines Terminplaners in setup noch hindert
|
||||
// ('kein_abo' | 'keine_einstellung' | 'abo_unbekannt'), null = nichts; ausserhalb von setup und bei News immer null. Das Abo wird
|
||||
@@ -86,9 +115,25 @@ async function adminView(customerId) {
|
||||
return projects
|
||||
}
|
||||
|
||||
/**
|
||||
* Angaben des Terminplaners fuer die Karte im WOMS (F13a): textformNoetig = verlangt die Aktivierung dieses Kunden die Zustimmung in
|
||||
* Textform (Modus on und Adresse nicht in der Pilotliste), avvVersion = Fassung der Unterlagen, auf die sich die Zustimmung bezieht,
|
||||
* links = absolute, versionierte Adressen von Leistungsbeschreibung und AVV fuer die Chat-Vorlage (nie Adressen ohne Fassung).
|
||||
*/
|
||||
async function termineFeld(customerId) {
|
||||
const kunde = await customerOf(customerId)
|
||||
const { lb, avv } = featureLegalPaths('termine')
|
||||
return {
|
||||
textformNoetig: termineTextformNoetig(kunde?.email),
|
||||
avvVersion: config.termine.avvVersion,
|
||||
links: { lb: `${config.publicBaseUrl}${lb}`, avv: `${config.publicBaseUrl}${avv}` },
|
||||
}
|
||||
}
|
||||
|
||||
funktionenAdminRouter.get('/:customerId', async (req, res) => {
|
||||
try {
|
||||
res.json({ projects: await adminView(req.params.customerId) })
|
||||
const projects = await adminView(req.params.customerId)
|
||||
res.json({ projects, termine: await termineFeld(req.params.customerId) })
|
||||
} catch (err) {
|
||||
sendError(res, err)
|
||||
}
|
||||
@@ -166,11 +211,15 @@ 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
|
||||
await withLocks([`feature-admin:${projectId}`], async () => {
|
||||
const doc = await getFeatureDoc(customerId, projectId, key)
|
||||
const adminNote = composeAdminNote(doc?.adminNote, { by, action, note })
|
||||
if (action === 'activate') {
|
||||
await adminActivate(customerId, projectId, key, { by, adminNote })
|
||||
const textform = await textformFuerAktivierung({ body: req.body, customerId, key, doc })
|
||||
const attachments = textform ? await unterlagenAlsAnhang() : []
|
||||
const aktiviert = await adminActivate(customerId, projectId, key, { by, adminNote, textform })
|
||||
if (textform) freigeschaltet = { doc: aktiviert, attachments }
|
||||
} else if (action === 'abnahme') {
|
||||
await freischaltenNachAbnahme({ customerId, projectId, key, by, adminNote })
|
||||
} else if (action === 'ticket') {
|
||||
@@ -182,6 +231,8 @@ funktionenAdminRouter.post('/:customerId/:key', async (req, res) => {
|
||||
await disable(doc.$id, { by, adminNote })
|
||||
}
|
||||
})
|
||||
// Der Kunde bekommt die Unterlagen auf einem dauerhaften Datentraeger (Anhang); ein Versandfehler wird dort geloggt, nie geworfen
|
||||
if (freigeschaltet) await meldeFreigeschaltet(freigeschaltet.doc, { attachments: freigeschaltet.attachments })
|
||||
res.json({ projects: (await adminView(customerId)).filter((p) => p.projectId === projectId) })
|
||||
} catch (err) {
|
||||
sendError(res, err)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { withLocks } from './keyedLock.js'
|
||||
import { sendAdminNotice, sendCustomerMail } from './mailer.js'
|
||||
import { getStripe, retrieveCheckoutSession } from './stripeBilling.js'
|
||||
import { anleitungsLink, featureConfig, featureHaken, featureKeys, kuerze, ticketFreischaltungKeys, ticketTexte } from './featureRegistry.js'
|
||||
import { zustimmungFuerSitzung } from './featureConsents.js'
|
||||
import { speichereZustimmung, zustimmungFuerSitzung } from './featureConsents.js'
|
||||
import { aboLage, laeuft } from './aboStatus.js'
|
||||
import { berlinDateTime } from './berlinTime.js'
|
||||
import { loeschTag } from './termine/frist.js'
|
||||
@@ -511,16 +511,46 @@ async function assertAboLaeuftFuerAktivierung(customerId) {
|
||||
if (!laeuftJetzt) throw httpError(409, KEIN_ABO_AKTIVIERUNG)
|
||||
}
|
||||
|
||||
const TEXTFORM_WEG = { chat: 'Chat im Kundenportal', email: 'E-Mail' }
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export function textformNachweisText({ date, channel }, fassung) {
|
||||
const [jahr, monat, tag] = String(date).split('-')
|
||||
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.`
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
export async function adminActivate(customerId, projectId, key, { by, adminNote } = {}) {
|
||||
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')
|
||||
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
|
||||
await speichereZustimmung({
|
||||
customerId,
|
||||
projectId,
|
||||
featureKey: key,
|
||||
stripeSessionId: '',
|
||||
fassungen: { leistungsbeschreibung: fassung, avv: fassung },
|
||||
texte: [{ id: 'textform', text: textformNachweisText(textform, fassung) }],
|
||||
ip: '',
|
||||
userAgent: '',
|
||||
konto: `Mitarbeiter: ${by || 'Mitarbeiter'}`,
|
||||
at: now,
|
||||
})
|
||||
}
|
||||
return activate(doc.$id, { by, adminNote })
|
||||
}
|
||||
|
||||
|
||||
@@ -28,3 +28,15 @@ export function termineFuerKunde(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? Nur im Modus on und nur,
|
||||
* wenn die Adresse des Kunden nicht in TERMINE_PILOT_EMAILS steht. '*' ist keine Pilotadresse und befreit niemanden. In off und
|
||||
* pilot bleibt die Aktivierung wie in TP1 (Pilotbetrieb, Mitarbeiter und Kunde sprechen direkt miteinander).
|
||||
*/
|
||||
export function termineTextformNoetig(email) {
|
||||
if (termineModus() !== 'on') return false
|
||||
const adresse = String(email || '').trim().toLowerCase()
|
||||
if (adresse === '' || adresse === '*') return true
|
||||
return !config.termine.pilotEmails.includes(adresse)
|
||||
}
|
||||
Reference in new issue
Block a user