Kundenansicht neu P7: Wechsel Fassung 6, Betrieb ohne Sofortbeginn-Zeitstempel

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 20:03:46 +00:00
1 parent 99e1987bc4
commit 4e0e42f633
3 files changed
+242 -12

No files matched your search

+52 -8
View File
@@ -26,7 +26,7 @@ import { featureConfig } from '../services/featureRegistry.js'
import { istAboModell } from '../services/aboModell.js'
import { ENTHALTEN_IN, aboModellFuerKunde } from '../services/aboFunktionen.js'
import { neueAnsicht } from '../services/kundenansicht/schalter.js'
import { FEHLER_BEDINGUNGEN, FEHLER_NACHWEIS, FEHLER_UNVOLLSTAENDIG, bestelltexteFuerClient, bestellZeile, nachweisTexte } from '../services/bestelltexte.js'
import { FEHLER_BEDINGUNGEN, FEHLER_NACHWEIS, FEHLER_UNVOLLSTAENDIG, WECHSEL_GESPERRT, bestelltexteFuerClient, bestellZeile, nachweisTexte } from '../services/bestelltexte.js'
import { kundenartWirksam } from '../services/kundenart.js'
import { fassungDerAnfrage, istNeueFassung, speichereNachweisFuerRoute } from '../services/vertragsNachweis.js'
import { PRODUCT_KEYS, contractAttachmentFor, describeItems, isTermsV4OrNewer, productLabel, productView, termsDocFor } from '../services/products.js'
@@ -480,25 +480,69 @@ router.post('/switch', requireSession, async (req, res) => {
return res.status(403).json({ error: 'In der Kundenansicht kann nicht gewechselt werden.' })
}
const { acceptTerms, earlyStart, termsVersion, prorationDate, amountDueCents } = req.body || {}
const fassung = fassungDerAnfrage(req, termsVersion)
const sechs = fassung === 'neu'
if (acceptTerms !== true) {
return res.status(400).json({ error: 'Bitte stimmen Sie den Vertragsbedingungen zu, um zu wechseln.' })
return res.status(400).json({ error: sechs ? FEHLER_UNVOLLSTAENDIG : 'Bitte stimmen Sie den Vertragsbedingungen zu, um zu wechseln.' })
}
if (earlyStart !== true) {
return res.status(400).json({ error: 'Bitte bestätigen Sie, dass wir sofort mit dem Wartungs-Abo beginnen sollen.' })
if (!sechs) {
// Fassung 6 aus der alten Ansicht oder fremde Version aus der neuen Ansicht: gleich der Hinweis auf die geaenderten Bedingungen
if (fassung === null && (neueAnsicht(req) || istNeueFassung(termsVersion))) {
return res.status(409).json({ error: neueAnsicht(req) ? FEHLER_BEDINGUNGEN : 'Die Bedingungen wurden aktualisiert. Bitte Seite neu laden.' })
}
if (earlyStart !== true) {
return res.status(400).json({ error: 'Bitte bestätigen Sie, dass wir sofort mit dem Wartungs-Abo beginnen sollen.' })
}
}
if (!termsVersion || !Number.isInteger(prorationDate) || !Number.isInteger(amountDueCents)) {
return res.status(400).json({ error: 'Die Bestellung ist unvollständig. Bitte laden Sie die Seite neu.' })
if (!termsVersion || !Number.isInteger(prorationDate) || !Number.isInteger(amountDueCents) || (sechs && amountDueCents < 0)) {
return res.status(400).json({ error: sechs ? FEHLER_UNVOLLSTAENDIG : 'Die Bestellung ist unvollständig. Bitte laden Sie die Seite neu.' })
}
if (termsVersion !== config.abo.termsVersion) {
if (termsVersion !== config.abo.termsVersion && !sechs) {
return res.status(409).json({ error: 'Die Bedingungen wurden aktualisiert. Bitte Seite neu laden.' })
}
const customerId = getSessionCustomerId(req)
const now = new Date().toISOString()
// Fassung 6: Kundenart allein aus dem Kundendokument (ein Lesefehler ist ein Serverfehler, nie ein Rueckfall); Privatkunden brauchen das Haekchen
let kundenart = null
if (sechs) {
try {
kundenart = kundenartWirksam(await getDocument(config.collections.customers, customerId))
} catch (err) {
console.error('[abo] Kunde für den Wechsel nicht lesbar:', err.message)
return res.status(500).json({ error: FEHLER_NACHWEIS })
}
if (kundenart !== 'unternehmen' && earlyStart !== true) return res.status(400).json({ error: WECHSEL_GESPERRT })
}
// Gewuenschte Funktion (nur neue Ansicht): geprueft oder verworfen, der Wechsel laeuft in jedem Fall
const funktion = neueAnsicht(req) ? await wechselFunktionAusBody(customerId, req.body?.funktion) : null
if (sechs) {
// Der gezeigte Wortlaut wird gespeichert, bevor Stripe etwas aendert; scheitert das, passiert nichts
await speichereNachweisFuerRoute({
customerId,
art: 'wechsel',
bezug: 'hosting>wartung',
texte: nachweisTexte('wechsel', {
kundenart,
fassung: termsVersion,
beginnGehakt: kundenart !== 'unternehmen',
bestellung: bestellZeile('wechsel', { heuteCents: amountDueCents, monatCents: config.products.wartung.amountCents, mindestMonate: config.abo.minTermMonths }),
}),
fassungen: { zustimmung: termsVersion },
kundenart,
req,
at: now,
})
}
const { subscription, paymentUrl } = await switchToWartung(
customerId,
{ termsVersion, consentAt: now, earlyStartConsentAt: now, consentIp: req.ip, consentUserAgent: req.get('user-agent') || '' },
{
termsVersion,
consentAt: now,
earlyStartConsentAt: sechs && kundenart === 'unternehmen' ? '' : now,
...(sechs ? { kundenart } : {}),
consentIp: req.ip,
consentUserAgent: req.get('user-agent') || '',
},
prorationDate,
// Fix 1 (8): der in der Bestelluebersicht gezeigte Betrag; weicht die neue Vorschau ab, 409
{ expectedAmountCents: amountDueCents, funktion }
+9 -4
View File
@@ -341,7 +341,8 @@ const DETAILS_MAX = 2000
/**
* Wechsel ins Wartungs-Abo (Button "Zahlungspflichtig bestellen"). consent: { termsVersion, consentAt,
* consentIp, consentUserAgent, earlyStartConsentAt } - ohne Zustimmung zum Sofortbeginn kein Wechsel (400, F16).
* consentIp, consentUserAgent, earlyStartConsentAt, kundenart? } - ohne Zustimmung zum Sofortbeginn kein Wechsel (400, F16); einzige Ausnahme
* kundenart 'unternehmen' (Fassung 6, Betrieb): dann ohne Zeitstempel, der Grund steht als details.kundenart im Ereignis.
* prorationDate: Zeitpunkt der Vorschau (previewProblem). Voraussetzungen wie previewSwitch (409). Mit
* options.expectedAmountCents (Route: angezeigter Betrag) rechnet der Server die Vorschau nach; weicht der Betrag
* ab, 409 ohne Aenderung bei Stripe.
@@ -355,7 +356,8 @@ export async function switchToWartung(customerId, consent, prorationDate, { expe
if (!consent?.termsVersion || !consent?.consentAt) {
throw httpError(400, 'Bitte stimmen Sie den Vertragsbedingungen zu, um zu wechseln.')
}
if (!consent.earlyStartConsentAt) {
// Fassung 6: Ein Betrieb (consent.kundenart 'unternehmen', vom Server aus dem Kundendokument) braucht kein Haekchen und bekommt keinen Zeitstempel
if (!consent.earlyStartConsentAt && consent.kundenart !== 'unternehmen') {
throw httpError(400, 'Bitte bestätigen Sie, dass wir sofort mit dem Wartungs-Abo beginnen sollen.')
}
const problem = previewProblem(prorationDate)
@@ -392,7 +394,8 @@ async function performSwitch(customerId, current, consent, prorationDate, expect
termStartAt: switchedAt,
termsVersion: consent.termsVersion,
termsAcceptedAt: consent.consentAt,
earlyStartConsentAt: consent.earlyStartConsentAt,
// leer (Betrieb, Fassung 6): Stripe loescht den Schluessel, ein Betrieb erbt den Zeitstempel des Hosting-Abschlusses nicht
earlyStartConsentAt: consent.earlyStartConsentAt || '',
consentIp: String(consent.consentIp || '').slice(0, 64),
consentUserAgent: String(consent.consentUserAgent || '').slice(0, 300),
switchedFrom: 'hosting',
@@ -448,6 +451,8 @@ async function performSwitch(customerId, current, consent, prorationDate, expect
previousMetadata: Object.fromEntries(
RESTORABLE_METADATA.map((key) => [key, stripeSub.metadata?.[key] == null ? null : String(stripeSub.metadata[key]).slice(0, 300)])
),
// Fassung 6: Grund fuer den fehlenden Zeitstempel (Betrieb)
...(consent.kundenart === 'unternehmen' ? { kundenart: 'unternehmen' } : {}),
}
// Gewuenschte Funktion (nur Bequemlichkeit): der Nachweis darf dadurch nie zu gross werden
if (funktion && JSON.stringify({ ...details, funktion }).length <= DETAILS_MAX) details.funktion = funktion
@@ -512,7 +517,7 @@ async function recordSwitchEvent({ doc, customerId, consent, status, details })
consentAt: consent.consentAt,
consentIp: consent.consentIp || null,
consentUserAgent: consent.consentUserAgent || null,
earlyStartConsentAt: consent.earlyStartConsentAt,
earlyStartConsentAt: consent.earlyStartConsentAt || '',
},
status,
})