Widerruf: Terminplaner als eigene Auswahl, Frist und Mails je Funktion
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
34384c7a39
commit
01bf05cc35
6 files changed
+258
-32
No files matched your search
@@ -9,7 +9,7 @@
|
||||
Empfänger: E-Mail aus dem Formular · BCC intern
|
||||
Platzhalter: {{name}}, {{produkt}}, {{bestellung}}, {{eingang_zeitpunkt}}, {{email}}, {{kundennummer}}
|
||||
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): kundennummer, abschluss, wechsel, zusatzspeicher, funktion, pruefen
|
||||
produkt = Katalogname ohne „WEBklar “ („Abo“ ohne gefundenes Abo). Folgen je Bestellung wie in den Hinweisen zum Widerruf (Fassung 4): abschluss (Wertersatz bei Sofortbeginn, Zusatzspeicher voll erstattet), wechsel (Hosting-Abo läuft weiter, Mehrbetrag abzüglich Wertersatz), zusatzspeicher (volle Erstattung, kein Wertersatz), pruefen (keine offene Frist).
|
||||
produkt = Katalogname ohne „WEBklar “ („Abo“ ohne gefundenes Abo); beim Widerruf einer Funktion ihr Name aus dem Register (News-Pinnwand, Terminplaner). Folgen je Bestellung wie in den Hinweisen zum Widerruf (Fassung 4): abschluss (Wertersatz bei Sofortbeginn, Zusatzspeicher voll erstattet), wechsel (Hosting-Abo läuft weiter, Mehrbetrag abzüglich Wertersatz), zusatzspeicher (volle Erstattung, kein Wertersatz), pruefen (keine offene Frist).
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
@@ -51,7 +51,7 @@ a{color:#111111;}
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Wie es weitergeht</div><div class="mut" style="color:#4a4843;">Wir setzen Ihren E-Mail-Zusatzspeicher auf die Menge vor der Bestellung zurück. Was Sie dafür gezahlt haben, erstatten wir vollständig innerhalb von 14 Tagen über das ursprüngliche Zahlungsmittel; Wertersatz verlangen wir nicht. Passen die gespeicherten E-Mails danach nicht mehr in den verbleibenden Speicher, nimmt das betroffene Postfach keine neuen E-Mails mehr an, bis wieder Platz ist.</div></td></tr></table></td></tr>
|
||||
<!-- ENDE zusatzspeicher -->
|
||||
<!-- WENN funktion -->
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Wie es weitergeht</div><div class="mut" style="color:#4a4843;">Wir prüfen Ihren Widerruf und melden uns umgehend bei Ihnen. Ist er wirksam, erstatten wir bereits gezahlte Beträge innerhalb von 14 Tagen über das ursprüngliche Zahlungsmittel und schalten die Funktion ab; war die Einrichtung auf Ihren Wunsch schon vor Ablauf der Widerrufsfrist abgeschlossen, verrechnen wir gegebenenfalls einen Wertersatz (§ 357a Abs. 2 BGB). Sie erhalten dazu eine gesonderte Abrechnung.</div></td></tr></table></td></tr>
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Wie es weitergeht</div><div class="mut" style="color:#4a4843;">Wir prüfen Ihren Widerruf und melden uns umgehend bei Ihnen. Ist er wirksam, erstatten wir bereits gezahlte Beträge innerhalb von 14 Tagen über das ursprüngliche Zahlungsmittel und schalten die Funktion ab; hat die Einrichtung auf Ihren ausdrücklichen Wunsch schon vor Ablauf der Widerrufsfrist begonnen, verrechnen wir gegebenenfalls einen Wertersatz für die bis zu Ihrem Widerruf erbrachte Leistung (§ 357a Abs. 2 BGB). Sie erhalten dazu eine gesonderte Abrechnung.</div></td></tr></table></td></tr>
|
||||
<!-- ENDE funktion -->
|
||||
<!-- WENN pruefen -->
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Wie es weitergeht</div><div class="mut" style="color:#4a4843;">Wir prüfen, ob für diese Bestellung eine Widerrufsfrist läuft, und melden uns umgehend bei Ihnen.</div></td></tr></table></td></tr>
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
Betreff: [Abo] WIDERRUF eingegangen: {{email}}
|
||||
Absender: „WEBklar Kundenportal“ <Portal-Postfach>
|
||||
Empfänger: kenso@webklar.com, andrej@webklar.com
|
||||
Platzhalter: {{email}}, {{name}}, {{produkt}}, {{kundennummer}}, {{bestellung}}, {{stripe_abo_id}}, {{abo_beginn}}, {{fristende}}, {{rechtzeitig}}, {{ereignisse}}, {{pruefhinweis}}
|
||||
Platzhalter: {{email}}, {{name}}, {{produkt}}, {{preis}}, {{kundennummer}}, {{bestellung}}, {{stripe_abo_id}}, {{abo_beginn}}, {{fristende}}, {{rechtzeitig}}, {{ereignisse}}, {{pruefhinweis}}
|
||||
preis = Preis der widerrufenen Funktion aus dem Register (nur im Block funktion, sonst leer).
|
||||
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): kundennummer, abo_gefunden, auf_kuendigung_gesetzt, kein_abo_gefunden, kuendigung_fehlgeschlagen, abschluss, wechsel, zusatzspeicher, funktion, keine_frist, nachweis_fehlt, wechsel_offen, abschluss_verspaetet, nicht_pruefbar, bereits_widerrufen
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@@ -61,7 +62,7 @@ a{color:#111111;}
|
||||
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#fdf0ef;border:1px solid #f1c9c5;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Zu tun</div><div class="mut" style="color:#4a4843;">Zusatzspeicher-Bestellung widerrufen: in Stripe die Menge zurück auf den Stand vor der Bestellung setzen, konsoleH anpassen und den Betrag innerhalb von 14 Tagen vollständig erstatten (kein Wertersatz, Hinweise zum Widerruf Nr. 4). Widerrufen:<br>{{ereignisse}}</div></td></tr></table></td></tr>
|
||||
<!-- ENDE zusatzspeicher -->
|
||||
<!-- WENN funktion -->
|
||||
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#fdf0ef;border:1px solid #f1c9c5;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Zu tun</div><div class="mut" style="color:#4a4843;">Widerruf einer Funktion (z. B. News-Pinnwand, 30 € einmalig). Widerrufsfrist bis {{fristende}}, Eingang: <strong>{{rechtzeitig}}</strong>. Es wurde NICHTS automatisch geändert: kein Abo, kein Vertragsereignis. Bitte von Hand prüfen, die Zahlung in Stripe innerhalb von 14 Tagen erstatten (gegebenenfalls abzüglich Wertersatz, wenn der Kunde den Einbau vor Fristende ausdrücklich verlangt hat) und die Funktion im Ticketsystem abschalten (Kunde → Reiter „Funktionen“). Der Kunde hat „wir prüfen“ bekommen. Bezahlte Funktionen des Kunden:<br>{{ereignisse}}</div></td></tr></table></td></tr>
|
||||
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#fdf0ef;border:1px solid #f1c9c5;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Zu tun</div><div class="mut" style="color:#4a4843;">Widerruf einer Funktion ({{produkt}}, {{preis}} einmalig). Widerrufsfrist bis {{fristende}}, Eingang: <strong>{{rechtzeitig}}</strong>. Es wurde NICHTS automatisch geändert: kein Abo, kein Vertragsereignis. Bitte von Hand prüfen, die Zahlung in Stripe innerhalb von 14 Tagen erstatten, gegebenenfalls abzüglich Wertersatz für den begonnenen Einbau (Aufwand im Arbeitsblatt, ⚖ Q5), sofern der Kunde den Beginn vor Fristende ausdrücklich verlangt hat, und die Funktion im Ticketsystem abschalten (Kunde → Reiter „Funktionen“). Beim Terminplaner bleibt das Wartungs-Abo unberührt. Der Kunde hat „wir prüfen“ bekommen. Bezahlte Bestellungen dieser Funktion (je Projekt mit eigenem Fristende):<br>{{ereignisse}}</div></td></tr></table></td></tr>
|
||||
<!-- ENDE funktion -->
|
||||
<!-- WENN keine_frist -->
|
||||
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#fdf0ef;border:1px solid #f1c9c5;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Achtung</div><div class="mut" style="color:#4a4843;">Nichts automatisch geändert – keine offene Frist gefunden: zu dieser Bestellung läuft keine Widerrufsfrist (mehr) oder es gibt keine Bestellung dieser Art. Bitte prüfen, bevor der Widerruf abgelehnt wird; der Kunde hat „wir prüfen“ bekommen.</div></td></tr></table></td></tr>
|
||||
|
||||
+33
-16
@@ -19,6 +19,7 @@ import {
|
||||
switchToWartung,
|
||||
} from '../services/planSwitch.js'
|
||||
import { handleFeatureCheckoutEvent, isFeatureCheckoutEvent, paidFeatureOrders } from '../services/siteFeatures.js'
|
||||
import { featureConfig } from '../services/featureRegistry.js'
|
||||
import { PRODUCT_KEYS, contractAttachmentFor, describeItems, isTermsV4OrNewer, productLabel, productView, termsDocFor } from '../services/products.js'
|
||||
import {
|
||||
ACTIVE_STATUSES,
|
||||
@@ -705,13 +706,15 @@ export function publicRateLimitState() {
|
||||
|
||||
// Auswahl im Formular: Vertrag (Kuendigung) und Bestellung (Widerruf), Standard jeweils der erste Wert
|
||||
const CONTRACTS = ['abo', 'zusatzspeicher']
|
||||
const ORDERS = ['abschluss', 'wechsel', 'zusatzspeicher', 'funktion']
|
||||
const ORDERS = ['abschluss', 'wechsel', 'zusatzspeicher', 'funktion', 'termine']
|
||||
// Bestellung einer Funktion -> Funktionsschluessel im Register ('funktion' meint die News-Pinnwand; jede Funktion hat ihre eigene Frist)
|
||||
const FEATURE_OF_ORDER = { funktion: 'news', termine: 'termine' }
|
||||
// Bestellung -> Ereignistyp mit Widerrufsfrist (F03); beim Zusatzspeicher zaehlt nur ein Plus (openWithdrawals)
|
||||
const ORDER_EVENT_TYPE = { abschluss: 'abschluss', wechsel: 'wechsel', zusatzspeicher: 'speicher' }
|
||||
|
||||
/**
|
||||
* Erklaerung aus dem Formular: null ohne Name oder gueltige E-Mail; { error } bei einer unzulaessigen Paketzahl.
|
||||
* contract 'abo' | 'zusatzspeicher', order 'abschluss' | 'wechsel' | 'zusatzspeicher' | 'funktion'; packages (nur beim
|
||||
* contract 'abo' | 'zusatzspeicher', order 'abschluss' | 'wechsel' | 'zusatzspeicher' | 'funktion' | 'termine'; packages (nur beim
|
||||
* Zusatzspeicher) ganze Zahl von 1 bis maxUnits, null heisst alle gebuchten (Rechtsrunde 28.09.2026).
|
||||
*/
|
||||
function validDeclaration(body) {
|
||||
@@ -784,6 +787,7 @@ function orderLabel(order, sub, { customer = false } = {}) {
|
||||
if (order === 'wechsel') return 'Wechsel ins Wartungs-Abo'
|
||||
if (order === 'zusatzspeicher') return 'E-Mail-Zusatzspeicher'
|
||||
if (order === 'funktion') return 'Funktion (z. B. News-Pinnwand)'
|
||||
if (order === 'termine') return 'Terminplaner (Einrichtung)'
|
||||
const abo = productNameOf(sub)
|
||||
return `${customer ? 'Abschluss Ihres Abos' : 'Abschluss des Abos'}${abo ? ` (${abo})` : ''}`
|
||||
}
|
||||
@@ -1092,19 +1096,29 @@ async function withdrawOrder(sub, d, receivedAt) {
|
||||
const FEATURE_STATE_LABEL = { setup: 'in Einrichtung', active: 'aktiv', disabled: 'abgeschaltet' }
|
||||
|
||||
/**
|
||||
* Widerruf einer Funktion (z. B. News-Pinnwand, 30 € einmalig): gehoert zu keinem Abo und hat kein Vertrags-
|
||||
* ereignis, darum aendert sich nichts automatisch. Gesucht wird der Kunde ueber die E-Mail-Adresse; das Team
|
||||
* bekommt in T09 die bezahlten Funktionen mit Fristende und erstattet/schaltet ab von Hand.
|
||||
* Widerruf einer Funktion (News-Pinnwand 30 € oder Terminplaner-Einrichtung 149 €, je einmalig): gehoert zu keinem Abo
|
||||
* und hat kein Vertragsereignis, darum aendert sich nichts automatisch. Gesucht wird der Kunde ueber die E-Mail-Adresse;
|
||||
* das Team bekommt in T09 die bezahlten Bestellungen der gewaehlten Funktion mit Fristende und erstattet/schaltet ab von
|
||||
* Hand. Jede Funktion hat ihre eigene Widerrufsfrist: gefiltert wird auf featureKey, der Anker ist deren vertragAt
|
||||
* (Zeit des Klicks der bezahlten Sitzung, ohne Nachweis orderedAt); das spaeteste Fristende entscheidet ueber "rechtzeitig".
|
||||
* Dokumente ohne featureKey stammen aus der Zeit, als es nur die News gab.
|
||||
*/
|
||||
async function withdrawFeature(d, receivedAt) {
|
||||
async function withdrawFeature(d, receivedAt, featureKey) {
|
||||
const customer =
|
||||
(await getCustomerByEmail(d.email).catch(() => null)) || (await getCustomerByEmail(d.email.toLowerCase()).catch(() => null))
|
||||
const orders = customer ? await paidFeatureOrders(customer.$id) : []
|
||||
// Anker der Frist ist die Zeit des Klicks der bezahlten Sitzung (vertragAt), ohne Nachweis orderedAt
|
||||
const ends = orders.filter((o) => o.vertragAt || o.orderedAt).map((o) => withdrawalDeadline(o.vertragAt || o.orderedAt).endsAt)
|
||||
const deadline = ends.length ? new Date(Math.max(...ends.map((e) => new Date(e).getTime()))) : null
|
||||
const alle = customer ? await paidFeatureOrders(customer.$id) : []
|
||||
const orders = alle.filter((o) => (o.featureKey || 'news') === featureKey)
|
||||
const endOf = (o) => {
|
||||
const anker = o.vertragAt || o.orderedAt
|
||||
return anker ? new Date(withdrawalDeadline(anker).endsAt) : null
|
||||
}
|
||||
const ends = orders.map(endOf).filter(Boolean)
|
||||
const deadline = ends.length ? new Date(Math.max(...ends.map((e) => e.getTime()))) : null
|
||||
const lines = orders
|
||||
.map((o) => `${o.projekt || 'Projekt'}: ${FEATURE_STATE_LABEL[o.state] || o.state}, bestellt am ${fmtDate(o.vertragAt || o.orderedAt)}${o.ticketWoid ? `, Ticket #${o.ticketWoid}` : ''}`)
|
||||
.map((o) => {
|
||||
const ende = endOf(o)
|
||||
return `${o.projekt || 'Projekt'}: ${FEATURE_STATE_LABEL[o.state] || o.state}, bestellt am ${fmtDate(o.vertragAt || o.orderedAt)}${ende ? `, Fristende ${fmtDate(ende)}` : ''}${o.ticketWoid ? `, Ticket #${o.ticketWoid}` : ''}`
|
||||
})
|
||||
.join('\n')
|
||||
return { customer, orders, deadline, lines, inTime: Boolean(deadline) && receivedAt <= deadline }
|
||||
}
|
||||
@@ -1127,12 +1141,14 @@ router.post('/public/withdraw', async (req, res) => {
|
||||
|
||||
let sub = null
|
||||
let result = { inTime: false, deadline: null, withdrawn: [], canceled: false, cancelFailed: false, event: null, unreadable: false, startMissing: false, alreadyWithdrawn: false, late: false, openSwitchWithdrawn: false }
|
||||
// Funktion: kein Abo, keine Vertragsereignisse; nur Nachschlagen fuer das Team (withdrawFeature)
|
||||
const feature = d.order === 'funktion'
|
||||
// Funktion (News oder Terminplaner): kein Abo, keine Vertragsereignisse; nur Nachschlagen fuer das Team (withdrawFeature)
|
||||
const featureKey = FEATURE_OF_ORDER[d.order] ?? null
|
||||
const feature = featureKey !== null
|
||||
const funktion = feature ? featureConfig(featureKey) : null
|
||||
let fn = { customer: null, orders: [], deadline: null, lines: '', inTime: false }
|
||||
try {
|
||||
if (feature) {
|
||||
fn = await withdrawFeature(d, receivedAt)
|
||||
fn = await withdrawFeature(d, receivedAt, featureKey)
|
||||
result = { ...result, deadline: fn.deadline, inTime: fn.inTime, late: !fn.inTime && Boolean(fn.deadline) }
|
||||
} else {
|
||||
sub = await findActiveSubscriptionByEmail(d.email)
|
||||
@@ -1153,7 +1169,7 @@ router.post('/public/withdraw', async (req, res) => {
|
||||
values: {
|
||||
name: d.name,
|
||||
bestellung: orderLabel(d.order, sub, { customer: true }),
|
||||
produkt: feature ? 'News-Pinnwand' : productLabelOf(sub),
|
||||
produkt: feature ? funktion.label.name : productLabelOf(sub),
|
||||
eingang_zeitpunkt: fmtDateTime(receivedAt),
|
||||
email: d.email,
|
||||
kundennummer: d.customerNumber,
|
||||
@@ -1176,7 +1192,8 @@ router.post('/public/withdraw', async (req, res) => {
|
||||
name: d.name,
|
||||
kundennummer: d.customerNumber,
|
||||
bestellung: orderLabel(d.order, sub),
|
||||
produkt: feature ? 'News-Pinnwand' : productLabelOf(sub),
|
||||
produkt: feature ? funktion.label.name : productLabelOf(sub),
|
||||
preis: feature ? funktion.priceLabel : '',
|
||||
stripe_abo_id: sub?.stripeSubscriptionId || '',
|
||||
abo_beginn: fmtDate(sub?.startedAt),
|
||||
fristende: result.deadline ? `${fmtDate(result.deadline)}, 24 Uhr` : 'keine Frist gefunden',
|
||||
|
||||
Reference in new issue
Block a user