#!/usr/bin/env node /** * Stripe-Preise je Produkt (Wartungs-Abo, Hosting-Abo, E-Mail-Zusatzspeicher, News-, Terminplaner-, Webshop- und Oeffnungszeiten-Einrichtung als Einmalpreise): legt fehlende * Preise an (ensurePrice), haelt die Wartungs-Produktbeschreibung bei Stripe auf dem Katalogtext * (server/config.js) und passt die Kundenportal-Konfiguration an ("WEBklar – Abo verwalten"). * Idempotent: vorhandene Preise werden ueber lookup_key gefunden, nicht neu angelegt. * * Usage (im Container): * node scripts/stripe-products.mjs --pruefen (nur anzeigen, was fehlt oder abweicht, keine Schreibzugriffe) * node scripts/stripe-products.mjs --anlegen (anlegen/anpassen) * * Fix round 1, Minor 7: genau einer der beiden Schalter ist Pflicht - kein stiller Livegang bei * fehlendem oder falsch geschriebenem Schalter. */ import 'dotenv/config' import { config } from '../server/config.js' import { ensurePortalConfiguration, ensurePrice, getStripe, PORTAL_HEADLINE } from '../server/services/stripeBilling.js' import { PRODUCT_KEYS } from '../server/services/products.js' export const KEYS = [...PRODUCT_KEYS, 'mailStorage', 'newsSetup', 'termineSetup', 'shopSetup', 'unterkunftSetup', 'zeitenSetup'] // ['wartung', 'hosting', 'mailStorage', 'newsSetup', 'termineSetup', 'shopSetup', 'zeitenSetup'] const USAGE = 'Usage: node scripts/stripe-products.mjs --pruefen | --anlegen' export function parseArgs(argv) { const args = argv.slice(2) if (args.length === 1 && args[0] === '--pruefen') return 'pruefen' if (args.length === 1 && args[0] === '--anlegen') return 'anlegen' return null } export function catalogEntry(key) { if (key === 'mailStorage') return config.mailStorage if (key === 'newsSetup') return config.newsSetup if (key === 'termineSetup') return config.termineSetup if (key === 'shopSetup') return config.shopSetup if (key === 'unterkunftSetup') return config.unterkunftSetup if (key === 'zeitenSetup') return config.zeitenSetup return config.products[key] } async function findPrice(lookupKey) { const { data } = await getStripe().prices.list({ lookup_keys: [lookupKey], active: true, limit: 1 }) return data[0] || null } async function checkPrice(key) { const catalog = catalogEntry(key) const price = await findPrice(catalog.lookupKey) if (!price) return { ok: false, note: `Preis ${catalog.lookupKey} fehlt bei Stripe` } const mismatches = [] if (price.unit_amount !== catalog.amountCents) mismatches.push(`Betrag ${price.unit_amount} statt ${catalog.amountCents}`) if (catalog.oneTime) { if (price.recurring) mismatches.push('Preis ist wiederkehrend statt einmalig') } else if (price.recurring?.interval !== 'month') mismatches.push(`recurring.interval "${price.recurring?.interval}" statt "month"`) if (price.tax_behavior !== 'inclusive') mismatches.push(`tax_behavior "${price.tax_behavior}" statt "inclusive"`) // Important 2: Produkt ohne Bild faellt in --pruefen auf (Checkout zeigt sonst kein Bild) const productId = typeof price.product === 'string' ? price.product : price.product?.id if (productId) { const product = await getStripe().products.retrieve(productId) if (!product.images?.length) mismatches.push('kein Produktbild') } return { ok: mismatches.length === 0, note: mismatches.length ? mismatches.join('; ') : `OK (${price.id})` } } async function checkWartungDescription() { const price = await findPrice(config.products.wartung.lookupKey) if (!price) return { ok: false, note: 'Preis fehlt, Produkt kann nicht geprüft werden' } const productId = typeof price.product === 'string' ? price.product : price.product.id const product = await getStripe().products.retrieve(productId) const ok = product.description === config.products.wartung.description return { ok, note: ok ? 'OK' : 'Produktbeschreibung weicht vom Katalogtext ab' } } async function checkPortalConfig() { const { data } = await getStripe().billingPortal.configurations.list({ active: true, limit: 20 }) const own = data.find((c) => c.metadata?.webklar === 'wartungs-abo') if (!own) return { ok: false, note: 'Konfiguration fehlt bei Stripe' } const ok = own.business_profile?.headline === PORTAL_HEADLINE return { ok, note: ok ? 'OK' : `Überschrift "${own.business_profile?.headline}" statt "${PORTAL_HEADLINE}"` } } async function pruefen() { let allOk = true for (const key of KEYS) { const r = await checkPrice(key) allOk = allOk && r.ok console.log(`${r.ok ? '✅' : '❌'} Preis ${key}: ${r.note}`) } const desc = await checkWartungDescription() allOk = allOk && desc.ok console.log(`${desc.ok ? '✅' : '❌'} Wartung-Produktbeschreibung: ${desc.note}`) const portal = await checkPortalConfig() allOk = allOk && portal.ok console.log(`${portal.ok ? '✅' : '❌'} Kundenportal-Konfiguration: ${portal.note}`) if (!allOk) { console.log('') console.log('Zum Anlegen/Anpassen: node scripts/stripe-products.mjs --anlegen') // Minor 7: --pruefen bleibt rein lesend, meldet Luecken aber ueber den Exit-Code (fuer Cron/CI) process.exitCode = 1 } } async function anlegen() { for (const key of KEYS) { const priceId = await ensurePrice(key) console.log(`✅ Preis ${key}: ${priceId}`) } const price = await findPrice(config.products.wartung.lookupKey) const productId = typeof price.product === 'string' ? price.product : price.product.id await getStripe().products.update(productId, { description: config.products.wartung.description }) console.log('✅ Wartung-Produktbeschreibung auf Katalogtext gesetzt') const configurationId = await ensurePortalConfiguration() console.log(`✅ Kundenportal-Konfiguration: ${configurationId}`) } async function main() { const mode = parseArgs(process.argv) if (!mode) { console.error(`❌ ${USAGE}`) process.exit(1) } if (!config.stripe.secretKey) { console.error('❌ STRIPE_SECRET_KEY fehlt') process.exit(1) } console.log(mode === 'pruefen' ? 'Modus: --pruefen (nur lesen, keine Änderungen)' : 'Modus: --anlegen (anlegen/anpassen)') console.log('') if (mode === 'pruefen') await pruefen() else await anlegen() } // Nur beim direkten Aufruf laufen lassen, nicht wenn das Modul importiert wird (z. B. fuer // parseArgs in einem Test) - main() wuerde sonst ueber process.exit den Prozess mitreissen. if (import.meta.url === `file://${process.argv[1]}`) { main().catch((err) => { console.error('❌', err.message) process.exit(1) }) }