Stripe: Katalog Terminplaner-Einrichtung (149 €) und Checkout je Funktion aus dem Register

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 11:53:06 +00:00
1 parent c584d220c8
commit 98fa03aad7
6 files changed
+238 -13

No files matched your search

+11
View File
@@ -106,6 +106,16 @@ const newsSetup = {
priceNickname: 'News-Einrichtung 30 €',
}
// Terminplaner: einmalige Einrichtung (Einmalpreis, kein Abo); der Terminplaner selbst laeuft, solange das Wartungs-Abo laeuft
const termineSetup = {
key: 'termineSetup',
name: 'Terminplaner – Einrichtung',
amountCents: 14900,
lookupKey: 'webklar_termine_einrichtung_14900',
oneTime: true,
priceNickname: 'Terminplaner-Einrichtung 149 €',
}
// Ticketsystem (WOMS); eigene Konstante, weil auch die Enable-Banking-Rueckkehradresse darauf aufbaut
const womsBaseUrl = (process.env.WOMS_PUBLIC_URL || 'https://ticket.webklar.com').replace(/\/$/, '')
@@ -313,6 +323,7 @@ export const config = {
products,
mailStorage,
newsSetup,
termineSetup,
ustHinweis,
// Register der kaufbaren Funktionen (siehe oben)
features,
+34 -10
View File
@@ -8,6 +8,7 @@ import {
updateDocument,
} from './appwriteAdmin.js'
import { lateStartAt } from './deadlines.js'
import { featureConfig } from './featureRegistry.js'
import { creditMinutesForInvoice, describeItems, getProduct, priceIdsOfInvoice, productKeyForLookupKey } from './products.js'
/**
@@ -51,19 +52,26 @@ export function isStripeConfigured() {
}
// Produkt-Metadaten "webklar" je Katalog-Schluessel (F09/Produces: ensurePrice)
const PRODUCT_METADATA = { wartung: 'wartungs-abo', hosting: 'hosting-abo', mailStorage: 'mail-speicher', newsSetup: 'news-einrichtung' }
const PRODUCT_METADATA = {
wartung: 'wartungs-abo',
hosting: 'hosting-abo',
mailStorage: 'mail-speicher',
newsSetup: 'news-einrichtung',
termineSetup: 'termine-einrichtung',
}
/** Katalogeintrag zum Schluessel: die beiden Abos aus products.js, der Zusatzspeicher aus config.mailStorage. */
/** Katalogeintrag zum Schluessel: die beiden Abos aus products.js, Zusatzspeicher und Einrichtungen aus config. */
function catalogFor(key) {
if (key === 'mailStorage') return config.mailStorage
if (key === 'newsSetup') return config.newsSetup
if (key === 'termineSetup') return config.termineSetup
return getProduct(key)
}
const priceIdCache = new Map()
/**
* Preis per lookup_key holen oder (einmalig) samt Produkt anlegen. key: 'wartung' | 'hosting' |
* 'mailStorage' | 'newsSetup' (Einmalpreis). Ersetzt das fruehere ensureAboPrice (nur Wartung); lookup_keys werden bei Stripe
* 'mailStorage' | 'newsSetup' | 'termineSetup' (Einmalpreise). Ersetzt das fruehere ensureAboPrice (nur Wartung); lookup_keys werden bei Stripe
* nie uebertragen, ein neuer Preis bekaeme also ohnehin einen neuen lookup_key im Katalog.
*/
export async function ensurePrice(key) {
@@ -639,14 +647,26 @@ export async function createAboCheckout(customer, consent, productKey, offer = n
}
/**
* Checkout fuer die einmalige Einrichtung einer Funktion (News-Pinnwand, 30 € brutto, kein Abo). Die Sitzung
* verfaellt nach 24 Stunden (Stripe-Hoechstwert; fuenf Minuten Luft fuer die Laufzeit des Aufrufs), die
* Rechnung erzeugt Stripe selbst. Nur die Zuordnung steht in den Metadaten, der Zustimmungswortlaut nicht.
* Checkout fuer die einmalige Einrichtung einer Funktion aus dem Register config.features (News-Pinnwand 30 €,
* Terminplaner 149 €; Einmalpreis, kein Abo). Preis, Rechnungstext und Rueckkehradresse kommen aus dem Register-Eintrag
* zu `key`; ein unbekannter Schluessel ist ein Fehler 400, bevor Stripe angesprochen wird. Die Sitzung verfaellt nach
* 24 Stunden (Stripe-Hoechstwert; fuenf Minuten Luft fuer die Laufzeit des Aufrufs), die Rechnung erzeugt Stripe
* selbst und traegt featureKey und projectId in ihren Metadaten. Nur die Zuordnung steht in den Metadaten der Sitzung,
* der Zustimmungswortlaut nicht.
* @returns {Promise<{ id: string, url: string }>}
*/
export async function createFeatureCheckout(customer, { projectId, key = 'news', consentAt }) {
const feature = featureConfig(key)
if (!feature) {
const err = new Error('Unbekannte Funktion.')
err.status = 400
throw err
}
const stripe = getStripe()
const [priceId, stripeCustomerId] = await Promise.all([ensurePrice('newsSetup'), getOrCreateStripeCustomer(customer)])
const [priceId, stripeCustomerId] = await Promise.all([ensurePrice(feature.product), getOrCreateStripeCustomer(customer)])
// Rueckkehr ins Dashboard: bei Funktionen mit returnProjekt (Terminplaner) mit dem Projekt, damit die Seite es oeffnet
const projekt = feature.returnProjekt ? encodeURIComponent(projectId) : ''
const returnUrl = `${config.publicBaseUrl}/dashboard.html#${feature.returnHash}`
const params = {
mode: 'payment',
customer: stripeCustomerId,
@@ -658,12 +678,16 @@ export async function createFeatureCheckout(customer, { projectId, key = 'news',
customer_update: { address: 'auto', name: 'auto' },
invoice_creation: {
enabled: true,
invoice_data: { description: config.newsSetup.name, footer: config.stripe.invoiceFooter },
invoice_data: {
description: catalogFor(feature.product).name,
footer: config.stripe.invoiceFooter,
metadata: { featureKey: key, projectId },
},
},
metadata: { customerId: customer.$id, projectId, featureKey: key, ...(consentAt ? { consentAt } : {}) },
expires_at: Math.floor(Date.now() / 1000) + 24 * 60 * 60 - 5 * 60,
success_url: `${config.publicBaseUrl}/dashboard.html#news?bestellt=1`,
cancel_url: `${config.publicBaseUrl}/dashboard.html#news`,
success_url: `${returnUrl}?bestellt=1${projekt ? `&projekt=${projekt}` : ''}`,
cancel_url: `${returnUrl}${projekt ? `?projekt=${projekt}` : ''}`,
}
let session
try {