Files
Webklar-Kundenbereich/scripts/stripe-products.mjs
T

136 lines
6.1 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- und Terminplaner-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'] // ['wartung', 'hosting', 'mailStorage', 'newsSetup', 'termineSetup']
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
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)
})
}