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:
1 parent
05363d06ad
commit
c4607e9c43
6 files changed
+784
-40
No files matched your search
@@ -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`,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user