feat: Bienenpatenschaft mit Urkunde, Abo, Medaillen und 3D-Konto
Setzt Teilprojekte 1-5 der Spec 2026-10-16 um: - Urkunden-Engine (lib/certificate): ein Layout fuer PDF, Vorschau und 3D-Textur, Vorlage classic@1, eingebettete OFL-Schriften, Testmatrix - Einmal-Patenschaft ueber Traegerprodukte (Kategorie PATENSCHAFT), Paket wird im Checkout in Patenschaft (Regelsatz) und Honig aufgeteilt, Kontopflicht, rein digitale Bestellungen (FulfillmentMethod DIGITAL) - Ausstellung nach Zahlung idempotent, Wiederholung im App-Prozess (instrumentation + background-jobs), Ablage in CERTIFICATE_DIR - Verkaufsseite /patenschaft mit Konfigurator und Live-Vorschau, Warenkorb mit Schluessel je Position - Kundenkonto: 3D-Ansicht von Urkunde und Medaille, Downloads, ZIP, Uebertragung per befristetem Einmal-Link, Treuestufen bis VIP - Admin: Angebote, Patenschaften, Druck, Korrektur mit Revisionen, Abos, Erntefenster, Deckung, SEPA-Frist, Treuestufen - Abo ueber Stripe Billing: Lieferung erst nach bezahlten Monaten, SEPA-Neukunden-Frist und Rueckbuchungsschutz, Honigreservierung, Kuendigungsbutton, Monatsrechnungen, Stripe-Einrichtungsskript Startangebote werden inaktiv angelegt; Livebetrieb erst nach Freigabe. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
dcb578f062
commit
f227aeb8f9
269 files changed
+28054
-141
No files matched your search
@@ -0,0 +1,342 @@
|
||||
-- Bienenpatenschaft: Angebote, Patenschaften mit Urkunden-Revisionen,
|
||||
-- Uebertragungen, Abos (Stripe Billing) mit Zahlungen und Lieferungen,
|
||||
-- Erntefenster und Treuestufen. Rein additiv; die beiden neuen Enum-Werte
|
||||
-- (PATENSCHAFT, DIGITAL) werden in dieser Migration nicht benutzt, ALTER TYPE
|
||||
-- ADD VALUE ist deshalb auch innerhalb der Migrationstransaktion zulaessig.
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SponsorshipOfferKind" AS ENUM ('EINMALIG', 'ABO');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SponsorshipCycle" AS ENUM ('VIERTELJAEHRLICH', 'ERNTE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "MedalTier" AS ENUM ('BRONZE', 'SILBER', 'GOLD', 'ABO');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SponsorshipStatus" AS ENUM ('AUSSTEHEND', 'FEHLER', 'BEREIT', 'GEDRUCKT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SubscriptionStatus" AS ENUM ('AUSSTEHEND', 'AKTIV', 'IM_VERZUG', 'GEKUENDIGT', 'BEENDET');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "DeliveryStatus" AS ENUM ('ANGELEGT', 'ZURUECKGESTELLT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "LoyaltyKind" AS ENUM ('STAMMKUNDE', 'ABO');
|
||||
|
||||
-- AlterEnum
|
||||
ALTER TYPE "ProductCategory" ADD VALUE 'PATENSCHAFT';
|
||||
|
||||
-- AlterEnum
|
||||
ALTER TYPE "FulfillmentMethod" ADD VALUE 'DIGITAL';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "customers" ADD COLUMN "sepaBlockedAt" TIMESTAMP(3),
|
||||
ADD COLUMN "stripeCustomerId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "order_items" ADD COLUMN "personalization" JSONB,
|
||||
ADD COLUMN "sponsorshipOfferId" TEXT,
|
||||
ADD COLUMN "sponsorshipRole" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sponsorship_offers" (
|
||||
"id" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"kind" "SponsorshipOfferKind" NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"nameEn" TEXT,
|
||||
"description" TEXT NOT NULL,
|
||||
"descriptionEn" TEXT,
|
||||
"features" JSONB NOT NULL DEFAULT '[]',
|
||||
"featuresEn" JSONB NOT NULL DEFAULT '[]',
|
||||
"priceCents" INTEGER NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"isPhysical" BOOLEAN NOT NULL DEFAULT false,
|
||||
"productId" TEXT,
|
||||
"honeyProductId" TEXT,
|
||||
"honeyJars" INTEGER NOT NULL DEFAULT 0,
|
||||
"honeyShareCentsPerJar" INTEGER NOT NULL DEFAULT 0,
|
||||
"honeyShareCentsPerMonth" INTEGER NOT NULL DEFAULT 0,
|
||||
"cycle" "SponsorshipCycle",
|
||||
"maxActiveSubscriptions" INTEGER,
|
||||
"stripeProductId" TEXT,
|
||||
"stripePriceId" TEXT,
|
||||
"stripePriceAmountCents" INTEGER,
|
||||
"medalTier" "MedalTier" NOT NULL DEFAULT 'BRONZE',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "sponsorship_offers_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sponsorships" (
|
||||
"id" TEXT NOT NULL,
|
||||
"number" TEXT NOT NULL,
|
||||
"ownerId" TEXT NOT NULL,
|
||||
"offerId" TEXT NOT NULL,
|
||||
"orderItemId" TEXT,
|
||||
"subscriptionId" TEXT,
|
||||
"locale" TEXT NOT NULL DEFAULT 'de',
|
||||
"startDate" DATE NOT NULL,
|
||||
"endDate" DATE,
|
||||
"status" "SponsorshipStatus" NOT NULL DEFAULT 'AUSSTEHEND',
|
||||
"currentRevision" INTEGER NOT NULL DEFAULT 0,
|
||||
"pdfAttempts" INTEGER NOT NULL DEFAULT 0,
|
||||
"nextAttemptAt" TIMESTAMP(3),
|
||||
"lastError" TEXT,
|
||||
"mailSentAt" TIMESTAMP(3),
|
||||
"printedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "sponsorships_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sponsorship_revisions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"sponsorshipId" TEXT NOT NULL,
|
||||
"revision" INTEGER NOT NULL,
|
||||
"personalization" JSONB NOT NULL,
|
||||
"term" JSONB NOT NULL,
|
||||
"text" JSONB NOT NULL,
|
||||
"templateVersion" TEXT NOT NULL,
|
||||
"pdfPath" TEXT NOT NULL,
|
||||
"pdfSha256" TEXT NOT NULL,
|
||||
"createdById" TEXT,
|
||||
"createdByLabel" TEXT NOT NULL,
|
||||
"reason" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "sponsorship_revisions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sponsorship_counters" (
|
||||
"year" INTEGER NOT NULL,
|
||||
"lastNumber" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "sponsorship_counters_pkey" PRIMARY KEY ("year")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sponsorship_transfers" (
|
||||
"id" TEXT NOT NULL,
|
||||
"sponsorshipId" TEXT NOT NULL,
|
||||
"tokenHash" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdById" TEXT NOT NULL,
|
||||
"claimedById" TEXT,
|
||||
"claimedAt" TIMESTAMP(3),
|
||||
"revokedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "sponsorship_transfers_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sponsorship_subscriptions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"customerId" TEXT NOT NULL,
|
||||
"offerId" TEXT NOT NULL,
|
||||
"status" "SubscriptionStatus" NOT NULL DEFAULT 'AUSSTEHEND',
|
||||
"personalization" JSONB NOT NULL,
|
||||
"locale" TEXT NOT NULL DEFAULT 'de',
|
||||
"stripeCheckoutSessionId" TEXT,
|
||||
"stripeSubscriptionId" TEXT,
|
||||
"startedAt" TIMESTAMP(3),
|
||||
"currentPeriodEnd" TIMESTAMP(3),
|
||||
"cancelAt" TIMESTAMP(3),
|
||||
"endedAt" TIMESTAMP(3),
|
||||
"delinquentSince" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "sponsorship_subscriptions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "subscription_payments" (
|
||||
"id" TEXT NOT NULL,
|
||||
"subscriptionId" TEXT NOT NULL,
|
||||
"stripeInvoiceId" TEXT NOT NULL,
|
||||
"stripePaymentIntentId" TEXT,
|
||||
"paymentMethodType" TEXT,
|
||||
"amountCents" INTEGER NOT NULL,
|
||||
"paidAt" TIMESTAMP(3) NOT NULL,
|
||||
"periodStart" TIMESTAMP(3) NOT NULL,
|
||||
"periodEnd" TIMESTAMP(3) NOT NULL,
|
||||
"countsFrom" TIMESTAMP(3) NOT NULL,
|
||||
"reversedAt" TIMESTAMP(3),
|
||||
"deliveryId" TEXT,
|
||||
"orderId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "subscription_payments_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "subscription_deliveries" (
|
||||
"id" TEXT NOT NULL,
|
||||
"subscriptionId" TEXT NOT NULL,
|
||||
"dueAt" TIMESTAMP(3) NOT NULL,
|
||||
"jars" INTEGER NOT NULL,
|
||||
"status" "DeliveryStatus" NOT NULL,
|
||||
"orderId" TEXT,
|
||||
"note" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "subscription_deliveries_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "harvest_windows" (
|
||||
"id" TEXT NOT NULL,
|
||||
"offerId" TEXT NOT NULL,
|
||||
"startsOn" DATE NOT NULL,
|
||||
"endsOn" DATE NOT NULL,
|
||||
"plannedJars" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "harvest_windows_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "loyalty_levels" (
|
||||
"id" TEXT NOT NULL,
|
||||
"kind" "LoyaltyKind" NOT NULL,
|
||||
"threshold" INTEGER,
|
||||
"name" TEXT NOT NULL,
|
||||
"nameEn" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "loyalty_levels_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sponsorship_offers_slug_key" ON "sponsorship_offers"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sponsorship_offers_productId_key" ON "sponsorship_offers"("productId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sponsorship_offers_kind_isActive_sortOrder_idx" ON "sponsorship_offers"("kind", "isActive", "sortOrder");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sponsorship_offers_honeyProductId_idx" ON "sponsorship_offers"("honeyProductId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sponsorships_number_key" ON "sponsorships"("number");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sponsorships_orderItemId_key" ON "sponsorships"("orderItemId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sponsorships_subscriptionId_key" ON "sponsorships"("subscriptionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sponsorships_ownerId_idx" ON "sponsorships"("ownerId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sponsorships_status_nextAttemptAt_idx" ON "sponsorships"("status", "nextAttemptAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sponsorships_offerId_idx" ON "sponsorships"("offerId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sponsorship_revisions_sponsorshipId_revision_key" ON "sponsorship_revisions"("sponsorshipId", "revision");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sponsorship_transfers_tokenHash_key" ON "sponsorship_transfers"("tokenHash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sponsorship_transfers_sponsorshipId_idx" ON "sponsorship_transfers"("sponsorshipId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sponsorship_subscriptions_stripeCheckoutSessionId_key" ON "sponsorship_subscriptions"("stripeCheckoutSessionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sponsorship_subscriptions_stripeSubscriptionId_key" ON "sponsorship_subscriptions"("stripeSubscriptionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sponsorship_subscriptions_customerId_idx" ON "sponsorship_subscriptions"("customerId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sponsorship_subscriptions_offerId_status_idx" ON "sponsorship_subscriptions"("offerId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "subscription_payments_stripeInvoiceId_key" ON "subscription_payments"("stripeInvoiceId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "subscription_payments_orderId_key" ON "subscription_payments"("orderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "subscription_payments_subscriptionId_paidAt_idx" ON "subscription_payments"("subscriptionId", "paidAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "subscription_deliveries_orderId_key" ON "subscription_deliveries"("orderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "subscription_deliveries_subscriptionId_dueAt_idx" ON "subscription_deliveries"("subscriptionId", "dueAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "harvest_windows_offerId_startsOn_idx" ON "harvest_windows"("offerId", "startsOn");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "loyalty_levels_kind_threshold_key" ON "loyalty_levels"("kind", "threshold");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "customers_stripeCustomerId_key" ON "customers"("stripeCustomerId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "sponsorship_offers" ADD CONSTRAINT "sponsorship_offers_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "sponsorship_offers" ADD CONSTRAINT "sponsorship_offers_honeyProductId_fkey" FOREIGN KEY ("honeyProductId") REFERENCES "products"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "sponsorships" ADD CONSTRAINT "sponsorships_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "sponsorships" ADD CONSTRAINT "sponsorships_offerId_fkey" FOREIGN KEY ("offerId") REFERENCES "sponsorship_offers"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "sponsorships" ADD CONSTRAINT "sponsorships_orderItemId_fkey" FOREIGN KEY ("orderItemId") REFERENCES "order_items"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "sponsorships" ADD CONSTRAINT "sponsorships_subscriptionId_fkey" FOREIGN KEY ("subscriptionId") REFERENCES "sponsorship_subscriptions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "sponsorship_revisions" ADD CONSTRAINT "sponsorship_revisions_sponsorshipId_fkey" FOREIGN KEY ("sponsorshipId") REFERENCES "sponsorships"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "sponsorship_transfers" ADD CONSTRAINT "sponsorship_transfers_sponsorshipId_fkey" FOREIGN KEY ("sponsorshipId") REFERENCES "sponsorships"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "sponsorship_subscriptions" ADD CONSTRAINT "sponsorship_subscriptions_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "sponsorship_subscriptions" ADD CONSTRAINT "sponsorship_subscriptions_offerId_fkey" FOREIGN KEY ("offerId") REFERENCES "sponsorship_offers"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "subscription_payments" ADD CONSTRAINT "subscription_payments_subscriptionId_fkey" FOREIGN KEY ("subscriptionId") REFERENCES "sponsorship_subscriptions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "subscription_payments" ADD CONSTRAINT "subscription_payments_deliveryId_fkey" FOREIGN KEY ("deliveryId") REFERENCES "subscription_deliveries"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "subscription_payments" ADD CONSTRAINT "subscription_payments_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "orders"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "subscription_deliveries" ADD CONSTRAINT "subscription_deliveries_subscriptionId_fkey" FOREIGN KEY ("subscriptionId") REFERENCES "sponsorship_subscriptions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "subscription_deliveries" ADD CONSTRAINT "subscription_deliveries_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "orders"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "harvest_windows" ADD CONSTRAINT "harvest_windows_offerId_fkey" FOREIGN KEY ("offerId") REFERENCES "sponsorship_offers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
@@ -76,6 +76,17 @@ model Customer {
|
||||
wishlistItems WishlistItem[]
|
||||
auditLogs AuditLog[]
|
||||
|
||||
/// Stripe-Kunde fuer Abos (Stripe Billing). Erst beim ersten Abo-Abschluss
|
||||
/// angelegt; Einmalkaeufe laufen weiter ohne Stripe-Kunde.
|
||||
stripeCustomerId String? @unique
|
||||
/// Gesetzt nach einer SEPA-Rueckbuchung: neue Abos dieses Kontos nur noch
|
||||
/// mit Karte oder Wallet (siehe lib/sponsorship-subscription). Der Admin
|
||||
/// kann die Sperre aufheben.
|
||||
sepaBlockedAt DateTime?
|
||||
|
||||
sponsorships Sponsorship[]
|
||||
subscriptions SponsorshipSubscription[]
|
||||
|
||||
@@map("customers")
|
||||
}
|
||||
|
||||
@@ -213,6 +224,10 @@ enum ProductCategory {
|
||||
KERZEN
|
||||
PROPOLIS
|
||||
SONSTIGES
|
||||
/// Traegerprodukt einer Einmal-Patenschaft (siehe SponsorshipOffer). Nicht
|
||||
/// im Shop gelistet; es haelt Preis, Steuersatz und Rechnungsposition, damit
|
||||
/// Warenkorb, Checkout und Rechnung keinen Sonderweg brauchen.
|
||||
PATENSCHAFT
|
||||
}
|
||||
|
||||
model Product {
|
||||
@@ -291,6 +306,11 @@ model Product {
|
||||
wishlistItems WishlistItem[]
|
||||
stockNotifications StockNotification[]
|
||||
|
||||
/// Einmal-Patenschaft, deren Traegerprodukt dieses Produkt ist.
|
||||
sponsorshipOffer SponsorshipOffer? @relation("SponsorshipOfferProduct")
|
||||
/// Patenschafts-Angebote, deren Honig aus diesem Produkt kommt.
|
||||
sponsorshipHoneyFor SponsorshipOffer[] @relation("SponsorshipOfferHoney")
|
||||
|
||||
@@index([archivedAt])
|
||||
@@map("products")
|
||||
}
|
||||
@@ -384,6 +404,9 @@ enum OrderStatus {
|
||||
enum FulfillmentMethod {
|
||||
VERSAND
|
||||
ABHOLUNG
|
||||
/// Nur digitale Leistungen (Patenschaft ohne Paket, Abo-Monatsrechnung):
|
||||
/// nichts zu versenden, nichts abzuholen. Die Rechnungsanschrift bleibt.
|
||||
DIGITAL
|
||||
}
|
||||
|
||||
model Order {
|
||||
@@ -462,6 +485,10 @@ model Order {
|
||||
invoice Invoice?
|
||||
returnRequest ReturnRequest?
|
||||
withdrawalDeclarations WithdrawalDeclaration[]
|
||||
/// Gesetzt, wenn diese Bestellung eine bezahlte Abo-Monatsrechnung abbildet.
|
||||
subscriptionPayment SubscriptionPayment?
|
||||
/// Gesetzt, wenn diese Bestellung eine Abo-Honiglieferung ist (0 €).
|
||||
subscriptionDelivery SubscriptionDelivery?
|
||||
|
||||
/// Bedient sowohl "Meine Bestellungen" (Filter nach customerId, sortiert nach
|
||||
/// createdAt) als auch den reinen customerId-Filter beim Datenexport.
|
||||
@@ -539,7 +566,20 @@ model OrderItem {
|
||||
/// bestaetigt ist oder das Produkt nicht mit ApiCore verknuepft ist.
|
||||
apicoreAllocations Json?
|
||||
|
||||
/// Personalisierung einer Patenschaft, wie sie beim Kauf bestaetigt wurde
|
||||
/// (lib/certificate/schema.ts), samt Vorlagenversion. Nur an der
|
||||
/// Hauptposition (sponsorshipRole "MAIN"); daraus entsteht nach der Zahlung
|
||||
/// die erste Revision der Urkunde.
|
||||
personalization Json?
|
||||
/// Angebot, zu dem diese Position gehoert — an Haupt- UND Honigposition,
|
||||
/// damit Warenkorb, Rechnung und Admin sie als ein Paket zeigen koennen.
|
||||
sponsorshipOfferId String?
|
||||
/// "MAIN" (Patenschaft, Regelsteuersatz) oder "HONEY" (Honiganteil des
|
||||
/// Pakets zum Honig-Satz). Null bei gewoehnlichen Positionen.
|
||||
sponsorshipRole String?
|
||||
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
sponsorship Sponsorship?
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Restrict)
|
||||
batch Batch? @relation(fields: [batchId], references: [id], onDelete: Restrict)
|
||||
apicoreStockEvents ApiCoreStockEvent[]
|
||||
@@ -951,3 +991,328 @@ model WithdrawalDeclaration {
|
||||
@@index([processedAt])
|
||||
@@map("withdrawal_declarations")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bienenpatenschaft (docs/superpowers/specs/2026-10-16-bienenpatenschaft-design.md)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum SponsorshipOfferKind {
|
||||
EINMALIG
|
||||
ABO
|
||||
}
|
||||
|
||||
enum SponsorshipCycle {
|
||||
/// Lieferanspruch nach jeweils drei bezahlten Monaten.
|
||||
VIERTELJAEHRLICH
|
||||
/// Lieferung in den Erntefenstern, frueheste nach sechs bezahlten Monaten
|
||||
/// seit der letzten Lieferung.
|
||||
ERNTE
|
||||
}
|
||||
|
||||
/// Praegung der Patenschafts-Medaille (lib/medals).
|
||||
enum MedalTier {
|
||||
BRONZE
|
||||
SILBER
|
||||
GOLD
|
||||
ABO
|
||||
}
|
||||
|
||||
enum SponsorshipStatus {
|
||||
/// Bezahlt, PDF noch nicht erzeugt oder Mail noch nicht versendet.
|
||||
AUSSTEHEND
|
||||
/// Alle automatischen Versuche aufgebraucht — braucht den Admin.
|
||||
FEHLER
|
||||
/// PDF liegt vor, Mail ist raus.
|
||||
BEREIT
|
||||
/// Nur physische Pakete: Urkunde gedruckt.
|
||||
GEDRUCKT
|
||||
}
|
||||
|
||||
enum SubscriptionStatus {
|
||||
/// Checkout gestartet, bei Stripe noch nicht abgeschlossen.
|
||||
AUSSTEHEND
|
||||
AKTIV
|
||||
/// Zahlung offen oder zurueckgebucht — keine Lieferungen, Stufe ruht.
|
||||
IM_VERZUG
|
||||
/// Gekuendigt, laeuft bis zum Ende des bezahlten Monats.
|
||||
GEKUENDIGT
|
||||
BEENDET
|
||||
}
|
||||
|
||||
enum DeliveryStatus {
|
||||
/// Bestellung fuer den Versand angelegt.
|
||||
ANGELEGT
|
||||
/// Faellig, aber ohne Bestand oder Anspruch zurueckgestellt.
|
||||
ZURUECKGESTELLT
|
||||
}
|
||||
|
||||
enum LoyaltyKind {
|
||||
STAMMKUNDE
|
||||
ABO
|
||||
}
|
||||
|
||||
/// Ein im Admin pflegbares Patenschafts-Angebot (Einmalpaket oder Abo).
|
||||
model SponsorshipOffer {
|
||||
id String @id @default(cuid())
|
||||
slug String @unique
|
||||
kind SponsorshipOfferKind
|
||||
name String
|
||||
nameEn String?
|
||||
description String @db.Text
|
||||
descriptionEn String? @db.Text
|
||||
/// Leistungsliste als JSON-Array von Strings (lib/json-array.ts).
|
||||
features Json @default("[]")
|
||||
featuresEn Json @default("[]")
|
||||
/// EINMALIG: Bruttopreis des ganzen Pakets. ABO: Monatspreis brutto.
|
||||
priceCents Int
|
||||
isActive Boolean @default(true)
|
||||
sortOrder Int @default(0)
|
||||
/// Mit gedruckter Urkunde, Honig, Grusskarte und Versand.
|
||||
isPhysical Boolean @default(false)
|
||||
/// Nur EINMALIG: Traegerprodukt (Kategorie PATENSCHAFT) fuer Warenkorb,
|
||||
/// Checkout und Rechnung. Sein Preis ist Paketpreis minus Honiganteil.
|
||||
productId String? @unique
|
||||
/// Honigprodukt, dessen Bestand das Paket bzw. Abo verbraucht.
|
||||
honeyProductId String?
|
||||
/// EINMALIG: Glaeser im Paket. ABO: Glaeser je Lieferung.
|
||||
honeyJars Int @default(0)
|
||||
/// EINMALIG: Honiganteil des Paketpreises je Glas (zum Honig-Steuersatz).
|
||||
honeyShareCentsPerJar Int @default(0)
|
||||
/// ABO: Honiganteil des Monatspreises (zum Honig-Steuersatz).
|
||||
honeyShareCentsPerMonth Int @default(0)
|
||||
/// Nur ABO.
|
||||
cycle SponsorshipCycle?
|
||||
/// Nur ABO: Hoechstzahl aktiver Abos (Kontingent); null = unbegrenzt.
|
||||
maxActiveSubscriptions Int?
|
||||
/// Nur ABO: Stripe-Produkt und der Preis, der priceCents abbildet. Bei einer
|
||||
/// Preisaenderung entsteht ein neuer Stripe-Preis; laufende Abos behalten
|
||||
/// ihren alten.
|
||||
stripeProductId String?
|
||||
stripePriceId String?
|
||||
stripePriceAmountCents Int?
|
||||
medalTier MedalTier @default(BRONZE)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
product Product? @relation("SponsorshipOfferProduct", fields: [productId], references: [id], onDelete: Restrict)
|
||||
honeyProduct Product? @relation("SponsorshipOfferHoney", fields: [honeyProductId], references: [id], onDelete: SetNull)
|
||||
sponsorships Sponsorship[]
|
||||
subscriptions SponsorshipSubscription[]
|
||||
harvestWindows HarvestWindow[]
|
||||
|
||||
@@index([kind, isActive, sortOrder])
|
||||
@@index([honeyProductId])
|
||||
@@map("sponsorship_offers")
|
||||
}
|
||||
|
||||
/// Eine gekaufte Patenschaft (eine Biene, ein Name, eine Urkunde).
|
||||
model Sponsorship {
|
||||
id String @id @default(cuid())
|
||||
/// WH-P-JJJJ-NNNNN, fortlaufend ueber SponsorshipCounter.
|
||||
number String @unique
|
||||
ownerId String
|
||||
offerId String
|
||||
/// Einmalkauf: die Hauptposition der Bestellung. Eindeutig — das ist der
|
||||
/// Idempotenzschluessel gegen doppelt zugestellte Webhooks.
|
||||
orderItemId String? @unique
|
||||
/// Abo: das Abo, aus dem die Patenschaft entstand.
|
||||
subscriptionId String? @unique
|
||||
locale String @default("de")
|
||||
startDate DateTime @db.Date
|
||||
/// Leer bei laufendem Abo.
|
||||
endDate DateTime? @db.Date
|
||||
status SponsorshipStatus @default(AUSSTEHEND)
|
||||
/// Nummer der aktuellen Revision; 0 = noch keine Urkunde erzeugt.
|
||||
currentRevision Int @default(0)
|
||||
pdfAttempts Int @default(0)
|
||||
nextAttemptAt DateTime?
|
||||
lastError String? @db.Text
|
||||
mailSentAt DateTime?
|
||||
printedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
owner Customer @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||
offer SponsorshipOffer @relation(fields: [offerId], references: [id], onDelete: Restrict)
|
||||
orderItem OrderItem? @relation(fields: [orderItemId], references: [id], onDelete: SetNull)
|
||||
subscription SponsorshipSubscription? @relation(fields: [subscriptionId], references: [id], onDelete: SetNull)
|
||||
revisions SponsorshipRevision[]
|
||||
transfers SponsorshipTransfer[]
|
||||
|
||||
@@index([ownerId])
|
||||
@@index([status, nextAttemptAt])
|
||||
@@index([offerId])
|
||||
@@map("sponsorships")
|
||||
}
|
||||
|
||||
/// Unveraenderlicher Stand einer Urkunde. Korrekturen und das Abo-Ende
|
||||
/// erzeugen neue Revisionen; alte bleiben samt PDF erhalten.
|
||||
model SponsorshipRevision {
|
||||
id String @id @default(cuid())
|
||||
sponsorshipId String
|
||||
revision Int
|
||||
/// Personalisierung wie bestaetigt (lib/certificate/schema.ts).
|
||||
personalization Json
|
||||
/// Laufzeitform (FIXED/OPEN/ENDED) mit Daten.
|
||||
term Json
|
||||
/// Aufgeloeste Textbausteine — Beleg dessen, was auf der Urkunde steht.
|
||||
text Json
|
||||
templateVersion String
|
||||
/// Relativ zu CERTIFICATE_DIR.
|
||||
pdfPath String
|
||||
pdfSha256 String
|
||||
/// Admin-Konto bei Korrekturen, null bei System-Revisionen.
|
||||
createdById String?
|
||||
/// Lesbarer Ersteller ("System" oder die Admin-Adresse) — bleibt lesbar,
|
||||
/// auch wenn das Admin-Konto spaeter geloescht wird.
|
||||
createdByLabel String
|
||||
reason String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
sponsorship Sponsorship @relation(fields: [sponsorshipId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([sponsorshipId, revision])
|
||||
@@map("sponsorship_revisions")
|
||||
}
|
||||
|
||||
/// Jahreszaehler der Patenschaftsnummern, Muster wie InvoiceCounter.
|
||||
model SponsorshipCounter {
|
||||
year Int @id
|
||||
lastNumber Int @default(0)
|
||||
|
||||
@@map("sponsorship_counters")
|
||||
}
|
||||
|
||||
/// Befristeter, einmal nutzbarer Link zur Uebertragung einer
|
||||
/// Einmal-Patenschaft an ein anderes Konto. Gespeichert wird nur der Hash.
|
||||
model SponsorshipTransfer {
|
||||
id String @id @default(cuid())
|
||||
sponsorshipId String
|
||||
tokenHash String @unique
|
||||
expiresAt DateTime
|
||||
createdById String
|
||||
claimedById String?
|
||||
claimedAt DateTime?
|
||||
revokedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
sponsorship Sponsorship @relation(fields: [sponsorshipId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([sponsorshipId])
|
||||
@@map("sponsorship_transfers")
|
||||
}
|
||||
|
||||
/// Abo-Patenschaft ueber Stripe Billing.
|
||||
model SponsorshipSubscription {
|
||||
id String @id @default(cuid())
|
||||
customerId String
|
||||
offerId String
|
||||
status SubscriptionStatus @default(AUSSTEHEND)
|
||||
/// Personalisierung beim Abschluss bestaetigt; daraus entsteht mit der
|
||||
/// ersten bezahlten Rechnung die Patenschaft.
|
||||
personalization Json
|
||||
locale String @default("de")
|
||||
stripeCheckoutSessionId String? @unique
|
||||
stripeSubscriptionId String? @unique
|
||||
startedAt DateTime?
|
||||
currentPeriodEnd DateTime?
|
||||
/// Gesetzt nach Kuendigung: Ende des bezahlten Zeitraums.
|
||||
cancelAt DateTime?
|
||||
endedAt DateTime?
|
||||
/// Beginn des Verzugs (fehlgeschlagene Zahlung oder Rueckbuchung).
|
||||
delinquentSince DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
offer SponsorshipOffer @relation(fields: [offerId], references: [id], onDelete: Restrict)
|
||||
payments SubscriptionPayment[]
|
||||
deliveries SubscriptionDelivery[]
|
||||
sponsorship Sponsorship?
|
||||
|
||||
@@index([customerId])
|
||||
@@index([offerId, status])
|
||||
@@map("sponsorship_subscriptions")
|
||||
}
|
||||
|
||||
/// Eine bezahlte Abo-Monatsrechnung von Stripe.
|
||||
model SubscriptionPayment {
|
||||
id String @id @default(cuid())
|
||||
subscriptionId String
|
||||
/// Idempotenzschluessel gegen doppelt zugestellte invoice.paid-Ereignisse.
|
||||
stripeInvoiceId String @unique
|
||||
stripePaymentIntentId String?
|
||||
paymentMethodType String?
|
||||
amountCents Int
|
||||
paidAt DateTime
|
||||
periodStart DateTime
|
||||
periodEnd DateTime
|
||||
/// Ab wann diese Zahlung fuer den Lieferanspruch zaehlt (SEPA-Neukunden-
|
||||
/// Frist, siehe lib/sponsorship-subscription/entitlement.ts).
|
||||
countsFrom DateTime
|
||||
/// Zurueckgebucht — zaehlt nie mehr fuer eine Lieferung.
|
||||
reversedAt DateTime?
|
||||
/// Lieferung, die diese Zahlung verbraucht hat.
|
||||
deliveryId String?
|
||||
/// Wabenhain-Bestellung samt Rechnung zu dieser Zahlung.
|
||||
orderId String? @unique
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
subscription SponsorshipSubscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
|
||||
delivery SubscriptionDelivery? @relation(fields: [deliveryId], references: [id], onDelete: SetNull)
|
||||
order Order? @relation(fields: [orderId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([subscriptionId, paidAt])
|
||||
@@map("subscription_payments")
|
||||
}
|
||||
|
||||
/// Eine Abo-Honiglieferung (0-€-Bestellung mit Honigpositionen).
|
||||
model SubscriptionDelivery {
|
||||
id String @id @default(cuid())
|
||||
subscriptionId String
|
||||
dueAt DateTime
|
||||
jars Int
|
||||
status DeliveryStatus
|
||||
orderId String? @unique
|
||||
note String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
subscription SponsorshipSubscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
|
||||
order Order? @relation(fields: [orderId], references: [id], onDelete: SetNull)
|
||||
payments SubscriptionPayment[]
|
||||
|
||||
@@index([subscriptionId, dueAt])
|
||||
@@map("subscription_deliveries")
|
||||
}
|
||||
|
||||
/// Erntefenster eines Abo-Angebots mit Zyklus ERNTE.
|
||||
model HarvestWindow {
|
||||
id String @id @default(cuid())
|
||||
offerId String
|
||||
startsOn DateTime @db.Date
|
||||
endsOn DateTime @db.Date
|
||||
/// Fuer Abos geplante Glaeser — werden reserviert, bevor sie in den
|
||||
/// Freiverkauf gehen.
|
||||
plannedJars Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
offer SponsorshipOffer @relation(fields: [offerId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([offerId, startsOn])
|
||||
@@map("harvest_windows")
|
||||
}
|
||||
|
||||
/// Treuestufe (Stammkunde nach bezahlten Bestellungen, Abo nach bezahlten
|
||||
/// Monaten). threshold null = VIP (nur ABO, nach der hoechsten Zahl).
|
||||
model LoyaltyLevel {
|
||||
id String @id @default(cuid())
|
||||
kind LoyaltyKind
|
||||
threshold Int?
|
||||
name String
|
||||
nameEn String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([kind, threshold])
|
||||
@@map("loyalty_levels")
|
||||
}
|
||||
+115
@@ -83,6 +83,121 @@ async function main() {
|
||||
"Siehe README, wie ein bestehendes Konto nachtraeglich zu ADMIN befoerdert wird."
|
||||
);
|
||||
}
|
||||
|
||||
await seedSponsorshipDefaults();
|
||||
}
|
||||
|
||||
/**
|
||||
* Startangebote der Bienenpatenschaft und Treuestufen — nur wenn es noch
|
||||
* keine gibt, und die Angebote INAKTIV: dieser Seed laeuft bei jedem
|
||||
* Containerstart auch in Produktion, und Preise, Honig und Rechtstexte muss
|
||||
* der Inhaber vor der Veroeffentlichung pruefen (Spec, "Geschaeftliche
|
||||
* Konfiguration"). Erst das Aktivieren im Admin macht /patenschaft sichtbar.
|
||||
*/
|
||||
async function seedSponsorshipDefaults(): Promise<void> {
|
||||
if ((await prisma.sponsorshipOffer.count()) === 0) {
|
||||
const packages = [
|
||||
{
|
||||
slug: "bienenfreund",
|
||||
name: "Bienenfreund",
|
||||
nameEn: "Bee Friend",
|
||||
priceCents: 1900,
|
||||
isPhysical: false,
|
||||
honeyJars: 0,
|
||||
medalTier: "BRONZE" as const,
|
||||
description: "Symbolische Namensgebung mit individuell gestalteter Urkunde als PDF.",
|
||||
descriptionEn: "Symbolic naming with an individually designed certificate as a PDF.",
|
||||
features: ["Symbolische Namensgebung", "Individuell gestaltete PDF-Urkunde", "Medaille und 3D-Ansicht im Kundenkonto", "Sicherer Download"],
|
||||
featuresEn: ["Symbolic naming", "Individually designed PDF certificate", "Medal and 3D view in your account", "Secure download"],
|
||||
},
|
||||
{
|
||||
slug: "honigfreund",
|
||||
name: "Honigfreund",
|
||||
nameEn: "Honey Friend",
|
||||
priceCents: 3900,
|
||||
isPhysical: true,
|
||||
honeyJars: 1,
|
||||
medalTier: "SILBER" as const,
|
||||
description: "Digitale Leistungen plus gedruckte Urkunde, ein Glas Wabenhain-Honig, Grußkarte und Geschenkverpackung.",
|
||||
descriptionEn: "Digital items plus a printed certificate, a jar of Wabenhain honey, a greeting card and gift wrapping.",
|
||||
features: ["Alle digitalen Leistungen", "Hochwertig gedruckte Urkunde", "1 × 500 g Wabenhain-Honig", "Grußkarte und Geschenkverpackung"],
|
||||
featuresEn: ["All digital items", "High-quality printed certificate", "1 × 500 g Wabenhain honey", "Greeting card and gift wrapping"],
|
||||
},
|
||||
{
|
||||
slug: "wabenfreund",
|
||||
name: "Wabenfreund",
|
||||
nameEn: "Comb Friend",
|
||||
priceCents: 5900,
|
||||
isPhysical: true,
|
||||
honeyJars: 3,
|
||||
medalTier: "GOLD" as const,
|
||||
description: "Digitale Leistungen plus gedruckte Urkunde, drei Gläser Wabenhain-Honig, Grußkarte und Geschenkverpackung.",
|
||||
descriptionEn: "Digital items plus a printed certificate, three jars of Wabenhain honey, a greeting card and gift wrapping.",
|
||||
features: ["Alle digitalen Leistungen", "Hochwertig gedruckte Urkunde", "3 × 500 g Wabenhain-Honig", "Grußkarte und Geschenkverpackung"],
|
||||
featuresEn: ["All digital items", "High-quality printed certificate", "3 × 500 g Wabenhain honey", "Greeting card and gift wrapping"],
|
||||
},
|
||||
];
|
||||
for (const [index, pkg] of packages.entries()) {
|
||||
// Honiganteil als Vorschlag (7 € je Glas) — der Inhaber prueft ihn mit
|
||||
// der Steuerberatung, bevor er das Angebot aktiviert.
|
||||
const honeyShareCentsPerJar = pkg.honeyJars > 0 ? 700 : 0;
|
||||
const product = await prisma.product.create({
|
||||
data: {
|
||||
slug: `patenschaft-${pkg.slug}`,
|
||||
name: `Bienenpatenschaft ${pkg.name}`,
|
||||
category: "PATENSCHAFT",
|
||||
description: pkg.description,
|
||||
weightGrams: 0,
|
||||
priceCents: pkg.priceCents - pkg.honeyJars * honeyShareCentsPerJar,
|
||||
pricePerKgCents: 0,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
await prisma.sponsorshipOffer.create({
|
||||
data: {
|
||||
slug: pkg.slug,
|
||||
kind: "EINMALIG",
|
||||
name: pkg.name,
|
||||
nameEn: pkg.nameEn,
|
||||
description: pkg.description,
|
||||
descriptionEn: pkg.descriptionEn,
|
||||
features: pkg.features,
|
||||
featuresEn: pkg.featuresEn,
|
||||
priceCents: pkg.priceCents,
|
||||
isActive: false,
|
||||
sortOrder: index,
|
||||
isPhysical: pkg.isPhysical,
|
||||
productId: product.id,
|
||||
honeyJars: pkg.honeyJars,
|
||||
honeyShareCentsPerJar,
|
||||
medalTier: pkg.medalTier,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("Bienenpatenschaft: drei Startangebote angelegt (inaktiv, bitte im Admin pruefen und aktivieren).");
|
||||
}
|
||||
|
||||
if ((await prisma.loyaltyLevel.count()) === 0) {
|
||||
const regular = [
|
||||
{ threshold: 2, name: "Blütenbesuch", nameEn: "Blossom Visitor" },
|
||||
{ threshold: 5, name: "Wabenbau", nameEn: "Comb Builder" },
|
||||
{ threshold: 10, name: "Honigschatz", nameEn: "Honey Treasure" },
|
||||
{ threshold: 25, name: "Goldwabe", nameEn: "Golden Comb" },
|
||||
];
|
||||
const subscription = Array.from({ length: 24 }, (_, i) => ({
|
||||
threshold: i + 1,
|
||||
name: `Stufe ${i + 1}`,
|
||||
nameEn: `Level ${i + 1}`,
|
||||
}));
|
||||
await prisma.loyaltyLevel.createMany({
|
||||
data: [
|
||||
...regular.map((level) => ({ kind: "STAMMKUNDE" as const, ...level })),
|
||||
...subscription.map((level) => ({ kind: "ABO" as const, ...level })),
|
||||
{ kind: "ABO" as const, threshold: null, name: "VIP", nameEn: "VIP" },
|
||||
],
|
||||
});
|
||||
console.log("Treuestufen angelegt (Stammkunde 2/5/10/25, Abo 1–24 und VIP).");
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
Reference in new issue
Block a user