Abosystem mit dauerhaften Rabatten und Teddy-Prämien
Drei Mitgliedschaftsstufen (Klein 4,99€/5%, Mittel 9,99€/10%, Groß 19,99€/20%) mit automatischem Bestellrabatt und Teddy-Prämien nach Meilensteinen (12-Monats- Rhythmus bzw. 6+12 bei Groß). Preise/Rabatte und alle Admin-Regeln (Kombinier- barkeit mit anderen Rabatten, Fortsetzung bei Wiederanmeldung, Monate über Stufen zusammenzählen, kostenloser Versand bei Prämie) sind über Decap CMS einstellbar (src/content/abo-einstellungen.json). Neuer "Mein Abo"-Bereich im Kundenkonto (6. Nav-Kachel) mit Plan-Vergleich, Live-Status (Laufzeit, nächste Zahlung, Rabatt, Fortschritt bis zur nächsten Prämie), Einlösen-Buttons für freigeschaltete Teddys, Kündigen/Wechseln sowie einer übersetzten Benachrichtigungsliste. Dazu ein extra hervorgehobener "Mein Abo verwalten"-Button neben Abmelden in der Kopfkarte. Warenkorb/Checkout rechnen den Abo-Rabatt korrekt in die Summe ein und stellen eingelöste Teddys als 0€-Gratisposition dar. Alles in 4 Sprachen (DE/EN/CH/FR). Unterwegs zwei echte Bugs gefunden und behoben: addMonateISO() vermischte lokale Zeit mit toISOString() (UTC) und verlor dadurch bei jeder Monats- verlängerung reproduzierbar einen Tag; die Checkout-Positionsliste zeigte für Gratis-Prämien fälschlich den vollen Preis statt 0€. Wie beim Treuebonus gilt: Speicherung aktuell im localStorage des Geräts (Phase 1) statt einer echten geräteübergreifenden Datenbank — folgt mit der Zahlungsanbindung in Phase 2. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
0cb7d3ca65
commit
208d995941
@@ -0,0 +1,85 @@
|
||||
// Abo-System: Preise/Rabatte/Verhaltensregeln kommen aus dem Adminbereich (Decap CMS, siehe
|
||||
// public/admin/config.yml → "🎁 Abo-System" → src/content/abo-einstellungen.json). Reine,
|
||||
// zustandslose Berechnungsfunktionen — der eigentliche Konto-Zustand (welches Abo, wie viele
|
||||
// bezahlte Monate, freigeschaltete Prämien) lebt in scripts/account.ts.
|
||||
|
||||
// @ts-ignore -- JSON-Import, zur Build-Zeit von Astro/Vite aufgelöst
|
||||
import raw from "../content/abo-einstellungen.json";
|
||||
|
||||
export type AboStufe = "klein" | "mittel" | "gross";
|
||||
|
||||
export interface AboStufenKonfig {
|
||||
preis: number;
|
||||
rabattProzent: number;
|
||||
}
|
||||
|
||||
export interface AboEinstellungen {
|
||||
klein: AboStufenKonfig;
|
||||
mittel: AboStufenKonfig;
|
||||
gross: AboStufenKonfig;
|
||||
kombinierbarMitAnderenRabatten: boolean;
|
||||
versandKostenlosBeiPraemie: boolean;
|
||||
fortsetzungBeiWiederanmeldung: boolean;
|
||||
monateUeberStufenZusammenzaehlen: boolean;
|
||||
}
|
||||
|
||||
export const aboEinstellungen = raw as AboEinstellungen;
|
||||
|
||||
export function aboStufenKonfig(stufe: AboStufe): AboStufenKonfig {
|
||||
return aboEinstellungen[stufe];
|
||||
}
|
||||
|
||||
export const ABO_STUFEN: AboStufe[] = ["klein", "mittel", "gross"];
|
||||
|
||||
/** Wie viele Teddy-Prämien welcher Art bei X durchgehend bezahlten Monaten in dieser Stufe
|
||||
* INSGESAMT freigeschaltet sein müssten (kumulativ, nicht nur "neu diesen Monat"):
|
||||
* - Klein-Abo: 1 kleiner Teddy pro volles Jahr.
|
||||
* - Mittel-Abo: 1 großer Teddy pro volles Jahr.
|
||||
* - Groß-Abo: 1 kleiner Teddy alle 6 Monate UND zusätzlich 1 großer Teddy pro volles Jahr
|
||||
* (nach 6 Monaten: 1 klein · nach 12: 2 klein + 1 groß · nach 18: 3 klein · nach 24: 4 klein + 2 groß). */
|
||||
export function teddyMeilensteine(stufe: AboStufe, monate: number): { klein: number; gross: number } {
|
||||
if (monate <= 0) return { klein: 0, gross: 0 };
|
||||
if (stufe === "klein") return { klein: Math.floor(monate / 12), gross: 0 };
|
||||
if (stufe === "mittel") return { klein: 0, gross: Math.floor(monate / 12) };
|
||||
return { klein: Math.floor(monate / 6), gross: Math.floor(monate / 12) };
|
||||
}
|
||||
|
||||
/** Wie viele Monate fehlen noch bis zum jeweils nächsten Meilenstein (für die
|
||||
* Fortschrittsanzeige im Kundenkonto), null wenn diese Stufe die Prämienart gar nicht kennt. */
|
||||
export function naechsteMeilensteine(stufe: AboStufe, monate: number): { klein: number | null; gross: number | null } {
|
||||
const kleinIntervall = stufe === "gross" ? 6 : stufe === "klein" ? 12 : null;
|
||||
const grossIntervall = stufe === "klein" ? null : 12;
|
||||
return {
|
||||
klein: kleinIntervall ? kleinIntervall - (monate % kleinIntervall) : null,
|
||||
gross: grossIntervall ? grossIntervall - (monate % grossIntervall) : null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Ein Abo ist aktiv, solange der aktuell bezahlte Abrechnungszeitraum noch nicht abgelaufen
|
||||
* ist — reiner Datumsvergleich zur echten, aktuellen Uhrzeit (läuft also automatisch ab, ganz
|
||||
* ohne Cronjob/Backend), genau wie befristete Produkt-Rabatte (siehe data/rabatt.ts). Gilt
|
||||
* bewusst UNABHÄNGIG davon, ob das Abo schon gekündigt wurde — gekündigte Abos behalten ihre
|
||||
* Vorteile bis zum Ende der bezahlten Laufzeit. */
|
||||
export function aboAktuellAktiv(bezahltBisDatum: string | null): boolean {
|
||||
if (!bezahltBisDatum) return false;
|
||||
return new Date() <= new Date(`${bezahltBisDatum}T23:59:59`);
|
||||
}
|
||||
|
||||
/** Heutiges Datum als lokales Kalenderdatum (NICHT über toISOString(), das auf UTC umrechnet und
|
||||
* in Zeitzonen östlich von UTC nachts kurzzeitig das falsche — gestrige — Datum liefern würde). */
|
||||
export function heutigesDatumISO(): string {
|
||||
const heute = new Date();
|
||||
const jahr = heute.getFullYear();
|
||||
const monat = String(heute.getMonth() + 1).padStart(2, "0");
|
||||
const tag = String(heute.getDate()).padStart(2, "0");
|
||||
return `${jahr}-${monat}-${tag}`;
|
||||
}
|
||||
|
||||
/** Rechnet rein kalendarisch in UTC (bewusst NICHT über lokale Zeit + toISOString() gemischt —
|
||||
* das würde je nach Zeitzone des Geräts bei jedem Aufruf einen Tag "verlieren" und sich über
|
||||
* viele Monate hinweg zu einem spürbaren Datumsfehler aufsummieren; mit reinem UTC-Rechnen ist
|
||||
* das Ergebnis unabhängig von der Zeitzone des Kunden immer exakt). */
|
||||
export function addMonateISO(datumISO: string, monate: number): string {
|
||||
const [jahr, monat, tag] = datumISO.split("-").map(Number);
|
||||
return new Date(Date.UTC(jahr, monat - 1 + monate, tag)).toISOString().slice(0, 10);
|
||||
}
|
||||
Reference in New Issue
Block a user