From f786601309949de1e8b779f5e241e83f9e0b6079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nick=20Rosenl=C3=B6cher?= Date: Fri, 16 Oct 2026 17:26:30 +0200 Subject: [PATCH] chore(k8s): Wabenhain auf Build sha-ed17d14 (Bienenpatenschaft), Einrichtungsskript Ausgerollt am 2026-10-16, Migrationen und Seed beim Start gelaufen. scripts/sponsorship-bootstrap.ts verknuepft den Paket-Honig und legt die Abo-Angebote inaktiv samt Stripe-Preis an. Co-Authored-By: Claude Opus 5.5 (1M context) --- k8s/05-app-deployment.yaml | 2 +- scripts/sponsorship-bootstrap.ts | 132 +++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 scripts/sponsorship-bootstrap.ts diff --git a/k8s/05-app-deployment.yaml b/k8s/05-app-deployment.yaml index 24f4637..1d9af81 100644 --- a/k8s/05-app-deployment.yaml +++ b/k8s/05-app-deployment.yaml @@ -86,7 +86,7 @@ spec: # und hier nachziehen. Tag plus Digest: der Tag ist lesbar, der # Digest macht das Image unveraenderlich (Tags lassen sich in der # Registry ueberschreiben). - image: git.encry.de/nick.rosenloecher/wabenhain:sha-0d254db@sha256:2a81f6a0ccfa27139517558aea2b61c9c258122e15ecce00effd9abec9b36d51 + image: git.encry.de/nick.rosenloecher/wabenhain:sha-ed17d14@sha256:9f3c66f603aaa3d2c7e921b654130cc755b0f84119bd498f9a1a22768e6da6c9 imagePullPolicy: IfNotPresent securityContext: allowPrivilegeEscalation: false diff --git a/scripts/sponsorship-bootstrap.ts b/scripts/sponsorship-bootstrap.ts new file mode 100644 index 0000000..ac52e0c --- /dev/null +++ b/scripts/sponsorship-bootstrap.ts @@ -0,0 +1,132 @@ +/** + * Ersteinrichtung der Bienenpatenschaft in einer bestehenden Installation. + * + * Wiederholbar: legt nur an, was fehlt, und aendert nie ein Angebot, das der + * Inhaber schon bearbeitet hat (Felder werden nur gesetzt, wenn sie leer sind). + * + * - verknuepft die physischen Einmal-Pakete mit einem Honigprodukt, + * - legt die beiden Abo-Angebote INAKTIV an (Testpreise, im Admin pruefen), + * - legt fuer jedes Abo-Angebot Stripe-Produkt, Monatspreis und + * Traegerprodukt an (syncOfferStripePrice). + * + * Aufruf (Werkzeug-Image, Umgebung wie die App): + * tsx scripts/sponsorship-bootstrap.ts --honey= (Probelauf) + * tsx scripts/sponsorship-bootstrap.ts --honey= --apply (ausfuehren) + */ +import { prisma } from "@/lib/prisma"; +import { syncOfferStripePrice } from "@/lib/sponsorship-subscription/stripe-prices"; + +interface AboTemplate { + slug: string; + name: string; + nameEn: string; + description: string; + descriptionEn: string; + features: string[]; + featuresEn: string[]; + priceCents: number; + honeyJars: number; + honeyShareCentsPerMonth: number; + cycle: "VIERTELJAEHRLICH" | "ERNTE"; + maxActiveSubscriptions: number; + sortOrder: number; +} + +// Testpreise — vor der Aktivierung kaufmaennisch pruefen (Spec, "Geschaeftliche +// Konfiguration"). Honiganteil je Monat = Glaeser je Lieferung × 7 € ÷ Monate +// je Lieferung, zum Honig-Steuersatz. +const ABO_TEMPLATES: AboTemplate[] = [ + { + slug: "abo-vierteljaehrlich", + name: "Patenschaft im Abo, vierteljährlich", + nameEn: "Sponsorship subscription, quarterly", + description: "Monatlich zahlen, alle drei bezahlten Monate zwei Gläser Wabenhain-Honig. Versand inklusive, monatlich kündbar.", + descriptionEn: "Pay monthly, two jars of Wabenhain honey after every three paid months. Shipping included, cancel monthly.", + features: ["Symbolische Namensgebung mit Urkunde", "2 × 500 g Honig nach jeweils 3 bezahlten Monaten", "Versand inklusive", "Monatlich kündbar"], + featuresEn: ["Symbolic naming with certificate", "2 × 500 g honey after every 3 paid months", "Shipping included", "Cancel monthly"], + priceCents: 1290, + honeyJars: 2, + honeyShareCentsPerMonth: 467, + cycle: "VIERTELJAEHRLICH", + maxActiveSubscriptions: 20, + sortOrder: 10, + }, + { + slug: "abo-ernte", + name: "Patenschaft im Abo, nach der Ernte", + nameEn: "Sponsorship subscription, after the harvest", + description: "Monatlich zahlen, Honig frisch nach der Ernte: drei Gläser, sobald seit der letzten Lieferung sechs Monate bezahlt sind. Versand inklusive, monatlich kündbar.", + descriptionEn: "Pay monthly, honey fresh after the harvest: three jars once six months have been paid since the last delivery. Shipping included, cancel monthly.", + features: ["Symbolische Namensgebung mit Urkunde", "3 × 500 g Honig im Erntefenster", "Versand inklusive", "Monatlich kündbar"], + featuresEn: ["Symbolic naming with certificate", "3 × 500 g honey in the harvest window", "Shipping included", "Cancel monthly"], + priceCents: 990, + honeyJars: 3, + honeyShareCentsPerMonth: 350, + cycle: "ERNTE", + maxActiveSubscriptions: 10, + sortOrder: 11, + }, +]; + +function arg(name: string): string | undefined { + const hit = process.argv.find((value) => value.startsWith(`--${name}=`)); + return hit ? hit.slice(name.length + 3) : undefined; +} + +async function main(): Promise { + const apply = process.argv.includes("--apply"); + const honeySlug = arg("honey"); + if (!honeySlug) throw new Error("--honey= fehlt."); + + const honey = await prisma.product.findUnique({ where: { slug: honeySlug }, select: { id: true, name: true, category: true, isActive: true } }); + if (!honey || honey.category !== "HONIG" || !honey.isActive) { + throw new Error(`Honigprodukt "${honeySlug}" nicht gefunden, nicht HONIG oder nicht aktiv.`); + } + console.log(`${apply ? "AUSFUEHREN" : "PROBELAUF"} — Honig: ${honey.name}`); + + const physical = await prisma.sponsorshipOffer.findMany({ + where: { kind: "EINMALIG", isPhysical: true, honeyProductId: null }, + select: { id: true, slug: true }, + }); + for (const offer of physical) { + console.log(` Paket ${offer.slug}: Honig verknuepfen`); + if (apply) await prisma.sponsorshipOffer.update({ where: { id: offer.id }, data: { honeyProductId: honey.id } }); + } + + for (const template of ABO_TEMPLATES) { + const existing = await prisma.sponsorshipOffer.findUnique({ where: { slug: template.slug }, select: { id: true, stripePriceId: true } }); + if (existing) { + console.log(` Abo ${template.slug}: vorhanden${existing.stripePriceId ? "" : ", Stripe-Preis fehlt"}`); + if (apply && !existing.stripePriceId) await syncOfferStripePrice(existing.id); + continue; + } + console.log(` Abo ${template.slug}: anlegen (inaktiv, ${(template.priceCents / 100).toFixed(2)} €/Monat) + Stripe-Preis`); + if (!apply) continue; + const created = await prisma.sponsorshipOffer.create({ + data: { + ...template, + kind: "ABO", + isActive: false, + isPhysical: true, + honeyProductId: honey.id, + medalTier: "ABO", + }, + }); + await syncOfferStripePrice(created.id); + } + + const offers = await prisma.sponsorshipOffer.findMany({ + orderBy: { sortOrder: "asc" }, + select: { slug: true, kind: true, isActive: true, priceCents: true, honeyProductId: true, stripePriceId: true, productId: true }, + }); + console.table(offers); +} + +main() + .catch((error: unknown) => { + console.error(error); + process.exitCode = 1; + }) + .finally(async () => { + await prisma.$disconnect(); + });