/* ===================================================================== lib/supporter-cycle.js — Reine Rechenlogik für den 30-Monats- Prämienzyklus des Dogfather Supporter-Abos (die Teddy-Kollektion entsteht in Kooperation mit VanVan, das Abo selbst gehört nur Dogfather). Verbindliche Regeln (siehe Dogfather_VanVan_Supporter_Abo.odt, Abschnitt 11-13, wortwörtlich umgesetzt, NICHT verändert): Monat 6 → kleiner Hasen-Teddy Monat 12 → mittelgroßer Hasen-Teddy Monat 18 → großer Hasen-Teddy Monat 24 → Tasse + Autogrammkarte Monat 30 → Hoodie (Zyklus damit abgeschlossen) ab 31 → neuer Zyklus, neue Kollektion, Fortschritt bei 0 Reine Funktionen ohne D1-/Netzwerkzugriff — dadurch leicht von Hand nachrechenbar und ohne Testrunner überprüfbar (siehe Beispiel im Auftrag: 37 bezahlte Monate → Zyklus 2, Fortschritt 7 von 30). ===================================================================== */ export const CYCLE_LENGTH = 30; // Reihenfolge ist bewusst fest — bestimmt auch premium_type-Zuordnung. export const MILESTONES = [ { month: 6, type: "teddy_klein", labelDe: "Kleiner Hasen-Teddy" }, { month: 12, type: "teddy_mittel", labelDe: "Mittelgroßer Hasen-Teddy" }, { month: 18, type: "teddy_gross", labelDe: "Großer Hasen-Teddy" }, { month: 24, type: "tasse_karte", labelDe: "Tasse & Autogrammkarte" }, { month: 30, type: "hoodie", labelDe: "Exklusiver Hoodie" }, ]; /** * Aus der GESAMTEN Anzahl erfolgreich angerechneter Abo-Monate (bleibt für * immer erhalten, auch über Kündigung/Reaktivierung hinweg) errechnet sich * rein rechnerisch: wie viele 30er-Zyklen sind vollständig abgeschlossen, * in welchem Zyklus befindet man sich gerade (1-basiert) und wie weit ist * der aktuelle Zyklus fortgeschritten (1..30). */ export function computeProgress(totalPaidMonths) { const total = Math.max(0, Math.floor(totalPaidMonths || 0)); if (total === 0) { return { totalPaidMonths: 0, completedCycles: 0, cycleNumber: 1, currentCycleMonth: 0 }; } const completedCycles = Math.floor((total - 1) / CYCLE_LENGTH); const currentCycleMonth = total - completedCycles * CYCLE_LENGTH; // 1..30 return { totalPaidMonths: total, completedCycles, cycleNumber: completedCycles + 1, // 1-basiert = welche Kollektion gerade läuft currentCycleMonth, }; } /** Welche Meilensteine sind im AKTUELLEN, laufenden Zyklus schon erreicht? */ export function reachedMilestonesInCurrentCycle(currentCycleMonth) { return MILESTONES.filter((m) => currentCycleMonth >= m.month); } /** Nächster noch offener Meilenstein im laufenden Zyklus (oder null, wenn Zyklus fertig). */ export function nextMilestone(currentCycleMonth) { return MILESTONES.find((m) => currentCycleMonth < m.month) || null; } /** * Liefert ALLE (Zyklus, Meilenstein)-Kombinationen, die bei der gegebenen * Gesamtmonatszahl freigeschaltet sein MÜSSEN — über alle bereits * abgeschlossenen Zyklen hinweg plus den laufenden Zyklus. Wird beim * Verarbeiten einer erfolgreichen Zahlung benutzt, um herauszufinden, * welche supporter_premiums-Zeilen neu angelegt werden müssen. */ export function allUnlockedMilestones(totalPaidMonths) { const { completedCycles, currentCycleMonth } = computeProgress(totalPaidMonths); const unlocked = []; // Vollständig abgeschlossene Zyklen: ALLE 5 Meilensteine gelten als erreicht. for (let c = 1; c <= completedCycles; c++) { for (const m of MILESTONES) unlocked.push({ cycleNumber: c, ...m }); } // Laufender Zyklus: nur die bereits erreichten. const cycleNumber = completedCycles + 1; for (const m of reachedMilestonesInCurrentCycle(currentCycleMonth)) { unlocked.push({ cycleNumber, ...m }); } return unlocked; } /** Für die Fortschritts-Zeitleiste im Supporter-Bereich (Abschnitt 10, Fenster 2). */ export function timelineForCurrentCycle(totalPaidMonths) { const { currentCycleMonth, cycleNumber } = computeProgress(totalPaidMonths); return { cycleNumber, currentCycleMonth, milestones: MILESTONES.map((m) => ({ ...m, reached: currentCycleMonth >= m.month, isNext: nextMilestone(currentCycleMonth)?.month === m.month, monthsRemaining: Math.max(0, m.month - currentCycleMonth), })), }; }