import { config } from '../config.js' /** * Abo-Produkte (Hosting-Abo, Wartungs-Abo) und E-Mail-Zusatzspeicher. * * Reine Funktionen ohne Netzwerk: Sie ordnen Stripe-Preise (lookup_key) den Produkten zu und * werten Abo-Positionen und Rechnungszeilen aus. Stripe-Objekte haben die Form der API-Version * des SDK (2025-10-29.clover). */ export const PRODUCT_KEYS = Object.freeze(['wartung', 'hosting']) function httpError(status, message) { const err = new Error(message) err.status = status return err } /** Produkt aus dem Katalog; unbekannter Schlüssel -> 400. */ export function getProduct(key) { const product = PRODUCT_KEYS.includes(key) ? config.products[key] : null if (!product) throw httpError(400, `Unbekanntes Abo: ${key}`) return product } /** lookup_key eines Stripe-Preises -> 'wartung' | 'hosting' | 'mailStorage' | null */ export function productKeyForLookupKey(lookupKey) { if (!lookupKey) return null const product = PRODUCT_KEYS.find((key) => config.products[key].lookupKey === lookupKey) if (product) return product return config.mailStorage.lookupKey === lookupKey ? 'mailStorage' : null } /** * Positionen eines Stripe-Abos (subscription.items.data) auswerten. Der Abo-Posten ist die * erste Position mit einem Hosting- oder Wartungs-Preis, unabhängig von der Reihenfolge. */ export function describeItems(items) { const list = Array.isArray(items) ? items : [] const abo = list.find((item) => PRODUCT_KEYS.includes(productKeyForLookupKey(item?.price?.lookup_key))) const storage = list.find((item) => productKeyForLookupKey(item?.price?.lookup_key) === 'mailStorage') return { productKey: abo ? productKeyForLookupKey(abo.price.lookup_key) : null, aboItemId: abo?.id || null, aboPriceId: abo?.price?.id || null, lookupKey: abo?.price?.lookup_key || null, priceAmount: abo?.price?.unit_amount ?? null, storageItemId: storage?.id || null, storageUnits: storage ? Number(storage.quantity) || 0 : 0, } } const priceIdOfLine = (line) => line?.pricing?.price_details?.price || null /** Preis-IDs aller Rechnungszeilen, jede nur einmal. */ export function priceIdsOfInvoice(invoice) { const ids = (invoice?.lines?.data || []).map(priceIdOfLine).filter(Boolean) return [...new Set(ids)] } /** * Arbeitszeit, die eine bezahlte Rechnung gutschreibt: die Minuten des Produkts der ersten * regulären (nicht anteiligen) Abo-Zeile über 0 €, deren Produkt Arbeitszeit enthält. Anteilige * Verrechnungen, Probezeiten (0 €), Hosting, Zusatzspeicher und unbekannte Preise zählen nicht. * priceKeys: Map Preis-ID -> 'wartung' | 'hosting' | 'mailStorage' | null */ export function creditMinutesForInvoice(invoice, priceKeys) { for (const line of invoice?.lines?.data || []) { const key = priceKeys.get(priceIdOfLine(line)) if (!PRODUCT_KEYS.includes(key)) continue const minutes = config.products[key].minutesPerMonth const regular = line.parent?.type === 'subscription_item_details' && line.parent.subscription_item_details?.proration === false if (minutes > 0 && regular && line.amount > 0) return minutes } return 0 }