Stripe: Testrabatt-Namen, Produktbild, Hosting-Leistungen, Tests für Bestandskunden

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-28 10:02:03 +00:00
1 parent c4607e9c43
commit 7df0b72019
7 files changed
+297 -52

No files matched your search

+3 -1
View File
@@ -43,7 +43,9 @@ export function productView(key) {
amountCents: product.amountCents,
minTermMonths: product.minTermMonths,
minutesPerMonth: product.minutesPerMonth,
features: product.features,
// Kopie, kein Verweis auf den Katalog: aufrufender Code darf das zurueckgegebene Array
// veraendern, ohne den geteilten Katalogeintrag fuer alle anderen zu verfaelschen (B10)
features: product.features.map((row) => [...row]),
}
}
+43 -16
View File
@@ -81,6 +81,9 @@ export async function ensurePrice(key) {
name: catalog.name,
...(catalog.description ? { description: catalog.description } : {}),
...(catalog.statementDescriptor ? { statement_descriptor: catalog.statementDescriptor } : {}),
// Produktbild: eigenes Katalogbild, sonst das generische Logo (Fix round 1, Important 2 - war
// beim Verallgemeinern von ensureAboPrice zu ensurePrice verloren gegangen)
images: [catalog.image || config.stripe.productImage],
metadata: { webklar: PRODUCT_METADATA[key] },
})
const price = await stripe.prices.create({
@@ -100,6 +103,11 @@ export async function ensurePrice(key) {
export const PORTAL_HEADLINE = 'WEBklar – Abo verwalten'
let cachedPortalConfigId = null
/** Nur fuer Tests: Cache zuruecksetzen, damit beide Zweige (Update noetig / schon richtig) einzeln pruefbar sind. */
export function resetPortalConfigCacheForTests() {
cachedPortalConfigId = null
}
/** Kundenportal-Konfiguration (Zahlungsmittel, Rechnungen, Kuendigung zum Periodenende). */
export async function ensurePortalConfiguration() {
if (cachedPortalConfigId) return cachedPortalConfigId
@@ -107,16 +115,25 @@ export async function ensurePortalConfiguration() {
const { data } = await stripe.billingPortal.configurations.list({ active: true, limit: 20 })
const own = data.find((c) => c.metadata?.webklar === 'wartungs-abo')
if (own) {
cachedPortalConfigId = own.id
// Aeltere Konfiguration (frueher nur Wartung) traegt noch die alte Ueberschrift -> anpassen
// Aeltere Konfiguration (frueher nur Wartung) traegt noch die alte Ueberschrift -> anpassen.
// Fix round 1, Minor 6: Cache erst nach Erfolg setzen, damit ein Fehlschlag beim naechsten
// Aufruf (z. B. Klick auf "Abo verwalten") erneut versucht wird statt fuer immer stehen zu
// bleiben; ein Fehlschlag bricht den Aufruf selbst nicht ab (die Portal-Sitzung soll trotzdem
// zustande kommen), nur eine Warnung im Log.
if (own.business_profile?.headline !== PORTAL_HEADLINE) {
await stripe.billingPortal.configurations.update(own.id, {
business_profile: {
headline: PORTAL_HEADLINE,
terms_of_service_url: `${config.publicBaseUrl}/abo-bedingungen.html`,
},
})
try {
await stripe.billingPortal.configurations.update(own.id, {
business_profile: {
headline: PORTAL_HEADLINE,
terms_of_service_url: `${config.publicBaseUrl}/abo-bedingungen.html`,
},
})
} catch (err) {
console.warn(`[stripe] Kundenportal-Konfiguration ${own.id}: Überschrift konnte nicht aktualisiert werden - ${err.message}`)
return own.id
}
}
cachedPortalConfigId = own.id
return own.id
}
const created = await stripe.billingPortal.configurations.create({
@@ -299,7 +316,13 @@ export function minTermMonthsFor(termsVersion) {
*/
export function termStartOf(sub) {
const termStartAt = sub.metadata?.termStartAt
if (termStartAt) return new Date(termStartAt)
if (termStartAt) {
const date = new Date(termStartAt)
if (!Number.isNaN(date.getTime())) return date
// Von Hand verstellte/beschaedigte Metadaten duerfen syncSubscription (toISOString) und
// cancelSubscriptionAt nicht mit einer Exception abschiessen - Warnung, Fallback wie ohne Wechsel
console.warn(`[stripe] Abo ${sub.id || '?'}: ungültiges termStartAt in den Metadaten ("${termStartAt}") - falle auf Leistungsbeginn zurück`)
}
return new Date(((sub.trial_end || sub.start_date) || 0) * 1000)
}
@@ -405,12 +428,17 @@ async function getOrCreateStripeCustomer(customer) {
return created.id
}
// Testrabatt-IDs je Produkt: amount_off = amountCents - 50 -> es werden nur 0,50 € abgebucht
const TEST_COUPON_IDS = { wartung: config.abo.testCouponId, hosting: 'WEBKLAR-TEST-050-HOSTING' }
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1)
/** Rabatt fuer Testkonten holen oder (einmalig) anlegen. productKey: 'wartung' | 'hosting'. */
/**
* Rabatt fuer Testkonten holen oder (einmalig) anlegen. productKey: 'wartung' | 'hosting'; die ID
* kommt aus dem Katalog (products.<key>.testCouponId), wie amountCents und der lookup_key.
* Fix round 1, Important 1: Stripe begrenzt den Coupon-Namen auf 40 Zeichen - der volle Produktname
* ("Testzugang WEBklar (0,50 €) – WEBklar Hosting-Abo" = 49) hätte in echtem Stripe 400 zurückgegeben.
*/
export async function ensureTestCoupon(productKey) {
const id = TEST_COUPON_IDS[productKey]
const catalog = getProduct(productKey) // 'mailStorage' oder unbekannt -> 400 (kein Testrabatt dafuer)
const id = catalog.testCouponId
if (!id) {
const err = new Error(`Kein Testrabatt fuer ${productKey}`)
err.status = 400
@@ -419,14 +447,13 @@ export async function ensureTestCoupon(productKey) {
const stripe = getStripe()
const existing = await stripe.coupons.retrieve(id).catch(() => null)
if (existing) return existing.id
const catalog = getProduct(productKey)
const created = await stripe.coupons.create({
id,
name: `Testzugang WEBklar (0,50 €) – ${catalog.name}`,
name: `Testzugang ${capitalize(productKey)} (0,50 €)`,
amount_off: catalog.amountCents - 50,
currency: 'eur',
duration: 'forever',
metadata: { webklar: `${productKey}-test` },
metadata: { webklar: `${PRODUCT_METADATA[productKey]}-test` },
})
return created.id
}