Kundenansicht neu P7: Bestellung Fassung 6 mit Kundenart vom Server und Wortlaut-Nachweis
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bf1e968a83
commit
99e1987bc4
5 files changed
+324
-15
No files matched your search
+73
-12
@@ -26,7 +26,9 @@ 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 { bestelltexteFuerClient } from '../services/bestelltexte.js'
|
||||
import { FEHLER_BEDINGUNGEN, FEHLER_NACHWEIS, FEHLER_UNVOLLSTAENDIG, 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'
|
||||
import {
|
||||
ACTIVE_STATUSES,
|
||||
@@ -242,26 +244,51 @@ router.get('/', requireSession, async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
/** Bestellung: Zustimmungen pruefen, dann Stripe Checkout (nur Zahlungsart). */
|
||||
/**
|
||||
* Bestellung: Zustimmungen pruefen, dann Stripe Checkout (nur Zahlungsart).
|
||||
* Fassung 6 (neue Ansicht, body.termsVersion = config.abo.termsVersionNeu): Die Kundenart kommt allein aus dem Kundendokument
|
||||
* (kundenartWirksam), nie aus dem Body; Betriebe beginnen sofort ohne Haekchen, Privatkunden nur mit earlyStart === true sofort, sonst
|
||||
* nach der Widerrufsfrist. Der Nachweis (angezeigter Wortlaut samt Kundenart) wird vor der Stripe-Sitzung geschrieben. Alle anderen
|
||||
* Anfragen laufen wie bisher (Fassung 5, Body mit startChoice).
|
||||
*/
|
||||
router.post('/checkout', requireSession, async (req, res) => {
|
||||
try {
|
||||
if (req.session.viewAs) {
|
||||
return res.status(403).json({ error: 'In der Kundenansicht kann nicht bestellt werden.' })
|
||||
}
|
||||
const { productKey, acceptTerms, startChoice, termsVersion, offerId, lateStartAt: shownLateStart } = req.body || {}
|
||||
const { productKey, acceptTerms, startChoice, termsVersion, offerId, lateStartAt: shownLateStart, earlyStart } = req.body || {}
|
||||
if (!PRODUCT_KEYS.includes(productKey)) {
|
||||
return res.status(400).json({ error: 'Bitte wählen Sie ein Abo (Hosting-Abo oder Wartungs-Abo).' })
|
||||
}
|
||||
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 bestellen.' })
|
||||
return sechs
|
||||
? res.status(400).json({ error: FEHLER_UNVOLLSTAENDIG })
|
||||
: res.status(400).json({ error: 'Bitte stimmen Sie den Vertragsbedingungen zu, um zu bestellen.' })
|
||||
}
|
||||
if (startChoice !== 'sofort' && startChoice !== 'nach_frist') {
|
||||
// startChoice verlangt nur der alte Weg; eine Bestellung mit Fassung 6 oder aus der neuen Ansicht mit fremder Version bekommt gleich
|
||||
// den Hinweis auf die geaenderten Bedingungen
|
||||
const alterWeg = fassung === 'alt' || (fassung === null && !neueAnsicht(req) && !istNeueFassung(termsVersion))
|
||||
if (alterWeg && startChoice !== 'sofort' && startChoice !== 'nach_frist') {
|
||||
return res.status(400).json({ error: 'Bitte wählen Sie, wann wir mit der Leistung beginnen sollen.' })
|
||||
}
|
||||
if (termsVersion !== config.abo.termsVersion) {
|
||||
return res.status(409).json({ error: 'Die Bedingungen wurden aktualisiert. Bitte Seite neu laden.' })
|
||||
if (fassung === null) {
|
||||
return res.status(409).json({ error: neueAnsicht(req) ? FEHLER_BEDINGUNGEN : 'Die Bedingungen wurden aktualisiert. Bitte Seite neu laden.' })
|
||||
}
|
||||
const customerId = getSessionCustomerId(req)
|
||||
let customer
|
||||
if (sechs) {
|
||||
// Ein Lesefehler ist ein Serverfehler, nie ein Rueckfall auf eine Kundenart
|
||||
try {
|
||||
customer = await getDocument(config.collections.customers, customerId)
|
||||
} catch (err) {
|
||||
console.error('[abo] Kunde für die Bestellung nicht lesbar:', err.message)
|
||||
return res.status(500).json({ error: FEHLER_NACHWEIS })
|
||||
}
|
||||
} else {
|
||||
customer = await getDocument(config.collections.customers, customerId)
|
||||
}
|
||||
const customer = await getDocument(config.collections.customers, getSessionCustomerId(req))
|
||||
// Bestellt wird genau der Preis, den die Bestelluebersicht gezeigt hat: das offene Angebot fuer
|
||||
// genau dieses Produkt (F32). Gehoert die mitgeschickte Angebots-ID zum jeweils anderen Produkt,
|
||||
// ist die Meldung eindeutiger als das allgemeine "hat sich geändert".
|
||||
@@ -279,25 +306,57 @@ router.post('/checkout', requireSession, async (req, res) => {
|
||||
if (!isStripeConfigured() || !isPilotAllowed(customer, offer)) {
|
||||
return res.status(403).json({ error: 'Die Online-Bestellung ist noch nicht freigeschaltet.' })
|
||||
}
|
||||
// Beginn: im alten Weg die Wahl des Kunden; in Fassung 6 allein die Kundenart des Servers und earlyStart === true
|
||||
const kundenart = sechs ? kundenartWirksam(customer) : null
|
||||
const beginnWahl = sechs ? (kundenart === 'unternehmen' || earlyStart === true ? 'sofort' : 'nach_frist') : startChoice
|
||||
const haekchenGesetzt = sechs && kundenart !== 'unternehmen' && earlyStart === true
|
||||
// Ebenso genau der Leistungsbeginn, den die Bestellseite gezeigt hat (verschiebt sich um Mitternacht)
|
||||
const serviceStartAt = startChoice === 'nach_frist' ? lateStartAt() : null
|
||||
const serviceStartAt = beginnWahl === 'nach_frist' ? lateStartAt() : null
|
||||
if (serviceStartAt && shownLateStart !== serviceStartAt.toISOString()) {
|
||||
return res.status(409).json({ error: 'Der Leistungsbeginn hat sich durch den Datumswechsel verschoben. Bitte laden Sie die Seite neu.' })
|
||||
}
|
||||
// Nachweis der Zustimmung (§ 305 Abs. 2, § 357a Abs. 2 BGB): Zeitpunkt, Version, Wahl, IP, Browser
|
||||
const now = new Date().toISOString()
|
||||
const angebot = offer ? { id: offer.$id, stripeCouponId: offer.stripeCouponId, text: offerText(offer) } : null
|
||||
if (sechs) {
|
||||
// Der Wortlaut, den der Kunde gesehen hat, wird gespeichert, bevor Stripe etwas anlegt; scheitert das, passiert nichts
|
||||
const produkt = productView(productKey)
|
||||
await speichereNachweisFuerRoute({
|
||||
customerId: customer.$id,
|
||||
art: 'abo',
|
||||
bezug: productKey,
|
||||
texte: nachweisTexte('abo', {
|
||||
kundenart,
|
||||
fassung: termsVersion,
|
||||
beginnGehakt: haekchenGesetzt,
|
||||
bestellung: bestellZeile('abo', {
|
||||
produkt: productLabel(productKey),
|
||||
monatCents: produkt.amountCents,
|
||||
mindestMonate: produkt.minTermMonths,
|
||||
beginn: beginnWahl,
|
||||
beginnAm: serviceStartAt?.toISOString(),
|
||||
angebot: angebot?.text || undefined,
|
||||
}),
|
||||
}),
|
||||
fassungen: { zustimmung: termsVersion },
|
||||
kundenart,
|
||||
req,
|
||||
at: now,
|
||||
})
|
||||
}
|
||||
const url = await createAboCheckout(
|
||||
customer,
|
||||
{
|
||||
termsAcceptedAt: now,
|
||||
startChoice,
|
||||
earlyStartConsentAt: startChoice === 'sofort' ? now : '',
|
||||
startChoice: beginnWahl,
|
||||
earlyStartConsentAt: beginnWahl === 'sofort' && (!sechs || haekchenGesetzt) ? now : '',
|
||||
serviceStartAt,
|
||||
consentIp: req.ip,
|
||||
consentUserAgent: req.get('user-agent') || '',
|
||||
...(sechs ? { termsVersion } : {}),
|
||||
},
|
||||
productKey,
|
||||
offer ? { id: offer.$id, stripeCouponId: offer.stripeCouponId, text: offerText(offer) } : null
|
||||
angebot
|
||||
)
|
||||
return res.json({ url })
|
||||
} catch (err) {
|
||||
@@ -590,6 +649,8 @@ async function recordAbschluss(sub, session) {
|
||||
amountCents: sub.priceAmount ?? null,
|
||||
checkoutSessionId: session.id || null,
|
||||
...(sub.offerId ? { offerId: sub.offerId, offerText: sub.offerText || '' } : {}),
|
||||
// Fassung 6: Sofortbeginn ohne Zeitstempel gibt es nur fuer Betriebe (Grund fuer die Mails und den Nachweis)
|
||||
...(istNeueFassung(sub.termsVersion) && sub.startChoice === 'sofort' && !sub.earlyStartConsentAt ? { kundenart: 'unternehmen' } : {}),
|
||||
},
|
||||
consent: {
|
||||
termsVersion: sub.termsVersion || null,
|
||||
|
||||
@@ -574,6 +574,7 @@ function checkoutMessage(product, lateStart, offer) {
|
||||
* Position, Beschreibung, Hinweistext und Testrabatt richten sich nach dem Produkt.
|
||||
* startChoice: 'sofort' (Leistung und erste Abbuchung sofort) oder 'nach_frist' (Probezeit ohne Leistung
|
||||
* und ohne Abbuchung bis consent.serviceStartAt, dem Tag nach dem Ende der Widerrufsfrist, deadlines.js).
|
||||
* consent.termsVersion (optional, Fassung 6): die Fassung, der der Kunde zugestimmt hat; ohne sie gilt config.abo.termsVersion.
|
||||
* offer: individuelles Angebot { id, stripeCouponId, text } -> Coupon im Checkout; bei Testkonten
|
||||
* bleibt der Testrabatt im Checkout und das Angebot wird nach dem Abschluss angehaengt (offerDeferred).
|
||||
*/
|
||||
@@ -603,8 +604,8 @@ export async function createAboCheckout(customer, consent, productKey, offer = n
|
||||
webklarCustomerId: customer.$id,
|
||||
email: customer.email || '',
|
||||
productKey,
|
||||
termsVersion: config.abo.termsVersion,
|
||||
consentTextVersion: config.abo.termsVersion,
|
||||
termsVersion: consent.termsVersion || config.abo.termsVersion,
|
||||
consentTextVersion: consent.termsVersion || config.abo.termsVersion,
|
||||
termsAcceptedAt: consent.termsAcceptedAt,
|
||||
startChoice: consent.startChoice,
|
||||
earlyStartConsentAt: consent.earlyStartConsentAt || '',
|
||||
|
||||
@@ -112,3 +112,17 @@ export async function speichereVertragsNachweis({ customerId, art, bezug = '', t
|
||||
throw Object.assign(new Error(FEHLER_NACHWEIS), { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wie speichereVertragsNachweis, fuer die Routen: jeder Fehler (auch eine Pruefung der Eingaben) wird zu einem 500 mit FEHLER_NACHWEIS,
|
||||
* damit der Kunde nie einen internen Text sieht und die Handlung (Stripe-Sitzung, Wechsel, Speicheraenderung, Antrag) unterbleibt.
|
||||
*/
|
||||
export async function speichereNachweisFuerRoute(args) {
|
||||
try {
|
||||
return await speichereVertragsNachweis(args)
|
||||
} catch (err) {
|
||||
if (err?.status === 500 && err.message === FEHLER_NACHWEIS) throw err
|
||||
console.error(`[vertragsnachweis] Nachweis abgelehnt: ${err?.name ?? 'Fehler'} ${err?.status ?? ''} ${err?.message ?? ''}`.trim())
|
||||
throw Object.assign(new Error(FEHLER_NACHWEIS), { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user