Stripe: Einmalpreis News-Einrichtung 30 €
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
85bee3190c
commit
16fbb03c13
4 files changed
+43
-8
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* Stripe-Preise je Produkt (Wartungs-Abo, Hosting-Abo, E-Mail-Zusatzspeicher): legt fehlende
|
* Stripe-Preise je Produkt (Wartungs-Abo, Hosting-Abo, E-Mail-Zusatzspeicher, News-Einrichtung als Einmalpreis): legt fehlende
|
||||||
* Preise an (ensurePrice), haelt die Wartungs-Produktbeschreibung bei Stripe auf dem Katalogtext
|
* Preise an (ensurePrice), haelt die Wartungs-Produktbeschreibung bei Stripe auf dem Katalogtext
|
||||||
* (server/config.js) und passt die Kundenportal-Konfiguration an ("WEBklar – Abo verwalten").
|
* (server/config.js) und passt die Kundenportal-Konfiguration an ("WEBklar – Abo verwalten").
|
||||||
* Idempotent: vorhandene Preise werden ueber lookup_key gefunden, nicht neu angelegt.
|
* Idempotent: vorhandene Preise werden ueber lookup_key gefunden, nicht neu angelegt.
|
||||||
@@ -17,7 +17,7 @@ import { config } from '../server/config.js'
|
|||||||
import { ensurePortalConfiguration, ensurePrice, getStripe, PORTAL_HEADLINE } from '../server/services/stripeBilling.js'
|
import { ensurePortalConfiguration, ensurePrice, getStripe, PORTAL_HEADLINE } from '../server/services/stripeBilling.js'
|
||||||
import { PRODUCT_KEYS } from '../server/services/products.js'
|
import { PRODUCT_KEYS } from '../server/services/products.js'
|
||||||
|
|
||||||
const KEYS = [...PRODUCT_KEYS, 'mailStorage'] // ['wartung', 'hosting', 'mailStorage']
|
const KEYS = [...PRODUCT_KEYS, 'mailStorage', 'newsSetup'] // ['wartung', 'hosting', 'mailStorage', 'newsSetup']
|
||||||
const USAGE = 'Usage: node scripts/stripe-products.mjs --pruefen | --anlegen'
|
const USAGE = 'Usage: node scripts/stripe-products.mjs --pruefen | --anlegen'
|
||||||
|
|
||||||
export function parseArgs(argv) {
|
export function parseArgs(argv) {
|
||||||
@@ -28,7 +28,9 @@ export function parseArgs(argv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function catalogEntry(key) {
|
function catalogEntry(key) {
|
||||||
return key === 'mailStorage' ? config.mailStorage : config.products[key]
|
if (key === 'mailStorage') return config.mailStorage
|
||||||
|
if (key === 'newsSetup') return config.newsSetup
|
||||||
|
return config.products[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findPrice(lookupKey) {
|
async function findPrice(lookupKey) {
|
||||||
@@ -42,7 +44,9 @@ async function checkPrice(key) {
|
|||||||
if (!price) return { ok: false, note: `Preis ${catalog.lookupKey} fehlt bei Stripe` }
|
if (!price) return { ok: false, note: `Preis ${catalog.lookupKey} fehlt bei Stripe` }
|
||||||
const mismatches = []
|
const mismatches = []
|
||||||
if (price.unit_amount !== catalog.amountCents) mismatches.push(`Betrag ${price.unit_amount} statt ${catalog.amountCents}`)
|
if (price.unit_amount !== catalog.amountCents) mismatches.push(`Betrag ${price.unit_amount} statt ${catalog.amountCents}`)
|
||||||
if (price.recurring?.interval !== 'month') mismatches.push(`recurring.interval "${price.recurring?.interval}" statt "month"`)
|
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"`)
|
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)
|
// 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
|
const productId = typeof price.product === 'string' ? price.product : price.product?.id
|
||||||
|
|||||||
@@ -88,6 +88,16 @@ const mailStorage = {
|
|||||||
hetznerPackage: { name: 'Webhosting M', gb: 50, warnGb: 40 },
|
hetznerPackage: { name: 'Webhosting M', gb: 50, warnGb: 40 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// News-Pinnwand: einmalige Einrichtung (Einmalpreis, kein Abo)
|
||||||
|
const newsSetup = {
|
||||||
|
key: 'newsSetup',
|
||||||
|
name: 'News-Pinnwand – Einrichtung',
|
||||||
|
amountCents: 3000,
|
||||||
|
lookupKey: 'webklar_news_einrichtung_3000',
|
||||||
|
oneTime: true,
|
||||||
|
priceNickname: 'News-Einrichtung 30 €',
|
||||||
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
port: Number(process.env.PORT) || 3000,
|
port: Number(process.env.PORT) || 3000,
|
||||||
sessionSecret: process.env.SESSION_SECRET || '',
|
sessionSecret: process.env.SESSION_SECRET || '',
|
||||||
@@ -133,6 +143,7 @@ export const config = {
|
|||||||
|
|
||||||
products,
|
products,
|
||||||
mailStorage,
|
mailStorage,
|
||||||
|
newsSetup,
|
||||||
|
|
||||||
// Wartungs-Abo (Stripe Billing). Preis/Produkt legt der Server per lookup_key selbst an.
|
// Wartungs-Abo (Stripe Billing). Preis/Produkt legt der Server per lookup_key selbst an.
|
||||||
// Die Werte kommen aus dem Produktkatalog (products.wartung).
|
// Die Werte kommen aus dem Produktkatalog (products.wartung).
|
||||||
|
|||||||
@@ -51,17 +51,19 @@ export function isStripeConfigured() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Produkt-Metadaten "webklar" je Katalog-Schluessel (F09/Produces: ensurePrice)
|
// Produkt-Metadaten "webklar" je Katalog-Schluessel (F09/Produces: ensurePrice)
|
||||||
const PRODUCT_METADATA = { wartung: 'wartungs-abo', hosting: 'hosting-abo', mailStorage: 'mail-speicher' }
|
const PRODUCT_METADATA = { wartung: 'wartungs-abo', hosting: 'hosting-abo', mailStorage: 'mail-speicher', newsSetup: 'news-einrichtung' }
|
||||||
|
|
||||||
/** Katalogeintrag zum Schluessel: die beiden Abos aus products.js, der Zusatzspeicher aus config.mailStorage. */
|
/** Katalogeintrag zum Schluessel: die beiden Abos aus products.js, der Zusatzspeicher aus config.mailStorage. */
|
||||||
function catalogFor(key) {
|
function catalogFor(key) {
|
||||||
return key === 'mailStorage' ? config.mailStorage : getProduct(key)
|
if (key === 'mailStorage') return config.mailStorage
|
||||||
|
if (key === 'newsSetup') return config.newsSetup
|
||||||
|
return getProduct(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
const priceIdCache = new Map()
|
const priceIdCache = new Map()
|
||||||
/**
|
/**
|
||||||
* Preis per lookup_key holen oder (einmalig) samt Produkt anlegen. key: 'wartung' | 'hosting' |
|
* Preis per lookup_key holen oder (einmalig) samt Produkt anlegen. key: 'wartung' | 'hosting' |
|
||||||
* 'mailStorage'. Ersetzt das fruehere ensureAboPrice (nur Wartung); lookup_keys werden bei Stripe
|
* 'mailStorage' | 'newsSetup' (Einmalpreis). 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.
|
* nie uebertragen, ein neuer Preis bekaeme also ohnehin einen neuen lookup_key im Katalog.
|
||||||
*/
|
*/
|
||||||
export async function ensurePrice(key) {
|
export async function ensurePrice(key) {
|
||||||
@@ -94,7 +96,7 @@ export async function ensurePrice(key) {
|
|||||||
product: product.id,
|
product: product.id,
|
||||||
currency: 'eur',
|
currency: 'eur',
|
||||||
unit_amount: catalog.amountCents,
|
unit_amount: catalog.amountCents,
|
||||||
recurring: { interval: 'month' },
|
...(catalog.oneTime ? {} : { recurring: { interval: 'month' } }),
|
||||||
tax_behavior: 'inclusive',
|
tax_behavior: 'inclusive',
|
||||||
lookup_key: catalog.lookupKey,
|
lookup_key: catalog.lookupKey,
|
||||||
nickname: catalog.priceNickname,
|
nickname: catalog.priceNickname,
|
||||||
|
|||||||
@@ -242,3 +242,21 @@ test('Spiegel: termStartAt aus Metadaten (Wechsel), sonst Leistungsbeginn; stora
|
|||||||
assert.equal(doc.termStartAt, '2026-10-05T10:00:00.000Z')
|
assert.equal(doc.termStartAt, '2026-10-05T10:00:00.000Z')
|
||||||
assert.equal(doc.storageUnits, 3)
|
assert.equal(doc.storageUnits, 3)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('news_einrichtung_einmalpreis', async () => {
|
||||||
|
const before = st.calls.length
|
||||||
|
const id = await ensurePrice('newsSetup')
|
||||||
|
const calls = st.calls.slice(before)
|
||||||
|
const created = calls.filter((c) => c.method === 'POST' && c.path === '/v1/prices')
|
||||||
|
assert.equal(created.length, 1)
|
||||||
|
assert.equal(created[0].body['unit_amount'], '3000')
|
||||||
|
assert.equal(created[0].body['lookup_key'], 'webklar_news_einrichtung_3000')
|
||||||
|
assert.ok(!Object.keys(created[0].body).some((k) => k.startsWith('recurring')), 'kein recurring')
|
||||||
|
assert.equal(st.prices.get(id).recurring, null)
|
||||||
|
const products = calls.filter((c) => c.method === 'POST' && c.path === '/v1/products')
|
||||||
|
assert.equal(products[0].body['metadata[webklar]'], 'news-einrichtung')
|
||||||
|
// zweiter Aufruf: kein zweiter Preis (auch ohne Cache per lookup_key gefunden)
|
||||||
|
const again = st.calls.length
|
||||||
|
assert.equal(await ensurePrice('newsSetup'), id)
|
||||||
|
assert.equal(st.calls.slice(again).filter((c) => c.method === 'POST').length, 0)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user