Files
Wabenhain/scripts/sponsorship-bootstrap.ts
T
Nick.RosenloecherandClaude Opus 5.5 f786601309
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
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) <[email protected]>
2026-10-16 17:26:30 +02:00

133 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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();
});