Docker Image bauen und veroeffentlichen / typecheck (push) Successful in 2m26s
Docker Image bauen und veroeffentlichen / test (push) Successful in 5m17s
Docker Image bauen und veroeffentlichen / lint (push) Successful in 4m13s
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 8m55s
Live angelegt am 2026-10-16 (inaktiv, Stripe-Preise price_1URDJE..., price_1URDJF...). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
135 lines
5.6 KiB
TypeScript
135 lines
5.6 KiB
TypeScript
/**
|
||
* 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;
|
||
}
|
||
|
||
// Kurze Namen: die Verkaufsseite zeigt sie unter "Im Abo", Stripe und die
|
||
// Rechnung als "Bienenpatenschaft-Abo <Name>".
|
||
// 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: "Vierteljährlich",
|
||
nameEn: "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: "Nach der Ernte",
|
||
nameEn: "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();
|
||
});
|