Postfach/Team-Verwaltung/Supporter-Abo als Node.js/Express-Server portiert

Vollständiger 1:1-Port des cloudflare-worker/ (dogfather-universe-postfach)
auf server-internal/: Bewerbungs-Postfach mit Rollen/Rechten, Team-
Zugangsverwaltung (verschlüsselte Owner-only-Codes), DogiCrew-Supporter-Abo
(PayPal Subscriptions, Google-Login, Resend-E-Mail, 30-Monats-Prämienzyklus)
und automatische TikTok-Live-Erkennung. D1 -> better-sqlite3, Cloudflare-
Cron -> node-cron. Läuft ohne gesetzte Secrets weiter (PayPal/Google/Resend/
Ticketanizer/Discord bleiben "nicht konfiguriert" statt kaputt), bis die
echten Zugangsdaten eingetragen werden.
This commit is contained in:
2026-08-05 18:57:19 +02:00
parent e331965bf1
commit e87aca3019
26 changed files with 2962 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
/* =====================================================================
lib/supporter-cycle.js — Reine Rechenlogik für den 30-Monats-Prämienzyklus.
1:1 portiert, unverändert (keine D1-/Netzwerkabhängigkeit).
===================================================================== */
export const CYCLE_LENGTH = 30;
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" },
];
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;
return { totalPaidMonths: total, completedCycles, cycleNumber: completedCycles + 1, currentCycleMonth };
}
export function reachedMilestonesInCurrentCycle(currentCycleMonth) {
return MILESTONES.filter((m) => currentCycleMonth >= m.month);
}
export function nextMilestone(currentCycleMonth) {
return MILESTONES.find((m) => currentCycleMonth < m.month) || null;
}
export function allUnlockedMilestones(totalPaidMonths) {
const { completedCycles, currentCycleMonth } = computeProgress(totalPaidMonths);
const unlocked = [];
for (let c = 1; c <= completedCycles; c++) {
for (const m of MILESTONES) unlocked.push({ cycleNumber: c, ...m });
}
const cycleNumber = completedCycles + 1;
for (const m of reachedMilestonesInCurrentCycle(currentCycleMonth)) {
unlocked.push({ cycleNumber, ...m });
}
return unlocked;
}
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),
})),
};
}