chore(k8s): Wabenhain auf Build sha-ed17d14 (Bienenpatenschaft), Einrichtungsskript
Docker Image bauen und veroeffentlichen / typecheck (push) Successful in 2m21s
Docker Image bauen und veroeffentlichen / lint (push) Successful in 2m27s
Docker Image bauen und veroeffentlichen / test (push) Successful in 3m19s
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 7m44s

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) <[email protected]>
This commit is contained in:
Nick.RosenloecherandClaude Opus 5.5 committed 2026-10-16 17:26:30 +02:00
1 parent e3b0eb7b05
commit f786601309
2 files changed
+133 -1

No files matched your search

+1 -1
View File
@@ -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
+132
View File
@@ -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=<produkt-slug> (Probelauf)
* tsx scripts/sponsorship-bootstrap.ts --honey=<produkt-slug> --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<void> {
const apply = process.argv.includes("--apply");
const honeySlug = arg("honey");
if (!honeySlug) throw new Error("--honey=<produkt-slug> 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();
});