Abo-API: Bestellung je Produkt, Katalog, Speicherstand, Wechsel-Möglichkeit

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Fable 5.1 committed 2026-09-28 09:43:09 +00:00
1 parent 05363d06ad
commit c4607e9c43
6 files changed
+784 -40

No files matched your search

+38 -22
View File
@@ -431,34 +431,58 @@ export async function ensureTestCoupon(productKey) {
return created.id
}
// Preis wie in den Bedingungen: "15,00 €" (normales Leerzeichen, kein geschuetztes wie bei toLocaleString)
const fmtEuro = (cents) => `${(Number(cents || 0) / 100).toFixed(2).replace('.', ',')} €`
/**
* Stripe Checkout fuer das Abo. Der Kunde hat seine Bestellung vorher im Portal
* abgegeben ("Zahlungspflichtig bestellen"); hier hinterlegt er die Zahlungsart.
* Hinweis ueber dem Bezahlknopf im Checkout (custom_text): Vertragsschluss, Leistungsbeginn, Preis
* und Mindestlaufzeit des bestellten Produkts; bei einem Angebot der vereinbarte Preis.
*/
function checkoutMessage(product, lateStart, offer) {
const intro =
`Ihre Bestellung haben Sie im Kundenportal abgegeben. Hier hinterlegen Sie die Zahlungsart; mit ${lateStart ? 'dem Abschluss' : 'der ersten Zahlung'} kommt der Vertrag zustande.` +
(lateStart ? ' Wie gewählt beginnen wir nach Ablauf der Widerrufsfrist – bis dahin erbringen wir keine Leistungen und buchen nichts ab.' : '')
const price = offer
? `Preis wie vereinbart: ${offer.text}.`
: `${lateStart ? 'Danach ' : ''}${fmtEuro(product.amountCents)} monatlich,`
return `${intro} ${price} Mindestlaufzeit ${product.minTermMonths} Monate, danach monatlich kündbar. Gemäß § 19 UStG ohne Umsatzsteuer.`
}
/**
* Stripe Checkout fuer ein Abo (productKey 'wartung' | 'hosting'). Der Kunde hat seine Bestellung
* vorher im Portal abgegeben ("Zahlungspflichtig bestellen"); hier hinterlegt er die Zahlungsart.
* 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).
* 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).
*/
export async function createAboCheckout(customer, consent, offer = null) {
export async function createAboCheckout(customer, consent, productKey, offer = null) {
const product = getProduct(productKey) // unbekanntes Produkt -> 400
// Hoechstens ein laufendes Abo je Kunde (Spec 7); der Wechsel zwischen den Abos laeuft ueber planSwitch
const current = await getCurrentSubscription(customer.$id)
if (current && ACTIVE_STATUSES.has(current.status)) {
const err = new Error('Sie haben das Wartungs-Abo bereits abgeschlossen.')
const err = new Error('Sie haben bereits ein laufendes Abo abgeschlossen.')
err.status = 409
throw err
}
const stripe = getStripe()
const [priceId, stripeCustomerId] = await Promise.all([
ensurePrice('wartung'),
ensurePrice(productKey),
getOrCreateStripeCustomer(customer),
])
const lateStart = consent.startChoice === 'nach_frist'
const serviceStartAt = lateStart ? consent.serviceStartAt || lateStartAt() : null
const isTestAccount = config.abo.testEmails.includes(String(customer.email || '').toLowerCase())
const testCoupon = isTestAccount ? await ensureTestCoupon('wartung') : null
// Stripe-Metadaten: max. 500 Zeichen je Wert
const testCoupon = isTestAccount ? await ensureTestCoupon(productKey) : null
const offerMeta = offer
? { offerId: offer.id, offerText: offer.text.slice(0, 300), ...(testCoupon ? { offerDeferred: '1' } : {}) }
: {}
// Stripe-Metadaten am Abo (max. 500 Zeichen je Wert): Zuordnung, Produkt und Nachweis der Zustimmung
const meta = {
webklarCustomerId: customer.$id,
email: customer.email || '',
productKey,
termsVersion: config.abo.termsVersion,
consentTextVersion: config.abo.termsVersion,
termsAcceptedAt: consent.termsAcceptedAt,
@@ -466,13 +490,10 @@ export async function createAboCheckout(customer, consent, offer = null) {
earlyStartConsentAt: consent.earlyStartConsentAt || '',
consentIp: String(consent.consentIp || '').slice(0, 64),
consentUserAgent: String(consent.consentUserAgent || '').slice(0, 300),
...(offer
? { offerId: offer.id, offerText: offer.text.slice(0, 300), ...(testCoupon ? { offerDeferred: '1' } : {}) }
: {}),
...offerMeta,
}
// Checkout erlaubt nur einen Rabatt: Testkonto -> Testrabatt, sonst der Angebots-Coupon
const coupon = testCoupon || offer?.stripeCouponId || null
const offerPrice = offer ? ` Preis wie vereinbart: ${offer.text}.` : ''
const params = {
mode: 'subscription',
customer: stripeCustomerId,
@@ -484,20 +505,15 @@ export async function createAboCheckout(customer, consent, offer = null) {
customer_update: { address: 'auto', name: 'auto' },
subscription_data: {
metadata: meta,
description: config.stripe.productName,
description: product.name,
...(serviceStartAt ? { trial_end: Math.floor(serviceStartAt.getTime() / 1000) } : {}),
},
metadata: meta,
// An der Checkout-Sitzung nur, was der Webhook braucht (Zuordnung, Produkt, Angebot). Keine
// Zustimmungsdaten: abgeschlossene Sitzungen lassen sich spaeter nicht mehr bereinigen
// (Loeschfristen, Datenschutzerklaerung Abschnitt 7) - die stehen nur am Abo.
metadata: { webklarCustomerId: customer.$id, productKey, ...offerMeta },
...(coupon ? { discounts: [{ coupon }] } : {}),
custom_text: {
submit: {
message: offer
? `Ihre Bestellung haben Sie im Kundenportal abgegeben. Hier hinterlegen Sie die Zahlungsart; mit ${lateStart ? 'dem Abschluss' : 'der ersten Zahlung'} kommt der Vertrag zustande.${lateStart ? ' Wie gewählt beginnen wir nach Ablauf der Widerrufsfrist – bis dahin erbringen wir keine Leistungen und buchen nichts ab.' : ''}${offerPrice} Mindestlaufzeit 12 Monate, danach monatlich kündbar.`
: lateStart
? 'Ihre Bestellung haben Sie im Kundenportal abgegeben. Hier hinterlegen Sie die Zahlungsart; mit dem Abschluss kommt der Vertrag zustande. Wie gewählt beginnen wir nach Ablauf der Widerrufsfrist – bis dahin erbringen wir keine Leistungen und buchen nichts ab. Danach 50,00 € monatlich, Mindestlaufzeit 12 Monate, danach monatlich kündbar.'
: 'Ihre Bestellung haben Sie im Kundenportal abgegeben. Hier hinterlegen Sie die Zahlungsart; mit der ersten Zahlung kommt der Vertrag zustande. 50,00 € monatlich, Mindestlaufzeit 12 Monate, danach monatlich kündbar.',
},
},
custom_text: { submit: { message: checkoutMessage(product, lateStart, offer) } },
success_url: `${config.publicBaseUrl}/dashboard.html?abo=success`,
cancel_url: `${config.publicBaseUrl}/dashboard.html?abo=cancel`,
}