Funktion zeiten in config.features (30 €, aboInklusive, consentFrei, teilweiseLive), freiweg() und Standard-Haken in featureRegistry.js, Stripe-Katalog zeitenSetup (Metadaten zeiten-einrichtung) in stripeBilling.js und scripts/stripe-products.mjs, vorläufige zeitenTicketDetails; Schlüssellisten-Pins um zeiten ergänzt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137 lines
6.2 KiB
JavaScript
137 lines
6.2 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Stripe-Preise je Produkt (Wartungs-Abo, Hosting-Abo, E-Mail-Zusatzspeicher, News-, Terminplaner- 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', 'zeitenSetup'] // ['wartung', 'hosting', 'mailStorage', 'newsSetup', 'termineSetup', '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 === '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)
|
||
})
|
||
}
|