Files
Webklar-Kundenbereich/scripts/stripe-products.mjs
T
ClaudeandClaude Opus 5.5 e87140e5ba Merge remote-tracking branch 'origin/main' into feature/oeffnungszeiten
Zusammenführung von TP2-Abschluss und Webshop (origin/main cab9c33) mit Öffnungszeiten live. Beide
Funktionen bleiben vollständig: jeder Konflikt ist als Vereinigung gelöst, Reihenfolge der Funktionsschlüssel
überall news, termine, shop, zeiten.

Konfliktdateien (28): public/app.js, dashboard.html, funktionen.js, widerrufen.html; scripts/stripe-products.mjs;
server/config.js, index.js, middleware/kundenansichtGuard.js, routes/abo.js, routes/admin/funktionenAdmin.js,
routes/funktionen.js, services/customerAdmin.js, customerViewLog.js, featureRegistry.js, retention.js,
siteFeatures.js, stripeBilling.js; test/customerViewLog, featureFreischaltung, featureRegistry, funktionenRoutes,
funktionenTermine, kundenansichtGuard, mailTemplates, publicForms, siteFeatures, stripeProductsScript (je
.test.js) und test/support/schema.js.

Entscheidungen: Mailvorlagen G01–G15, K01–K30, T01–T21 lückenlos (K24–K26 Webshop, K27–K30 Öffnungszeiten);
Stripe-Katalog mit shopSetup und zeitenSetup; Widerruf kennt shop und zeiten; ABO_FUNKTIONEN bleibt analyse
und termine (kein zeiten); activate() schreibt die Zeiten-Felder nur über extra; kein Halter von
feature-activate:<id> ruft disable() auf; die Aufbewahrungs-Löschung löscht Zeiten-Daten vor dem Abschalten und
hält den Kunden bei einem Fehler (held); die Admin-Antwort trägt ersatzTicket und warnung; Hash-Routen webshop
und zeiten, Ansichten view-webshop und view-zeiten; assertOwnProject behält die Meldung von main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:12:23 +00:00

138 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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', '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 === '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)
})
}