diff --git a/VERSION b/VERSION index 11b9a89..3720c6f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1271 \ No newline at end of file +1272 \ No newline at end of file diff --git a/backend/database.py b/backend/database.py index dba33c8..8065d48 100644 --- a/backend/database.py +++ b/backend/database.py @@ -623,6 +623,9 @@ def _migrate(conn_factory): ("users", "is_partner", "INTEGER NOT NULL DEFAULT 0"), ("users", "founder_number", "INTEGER"), ("users", "is_founder_pending", "INTEGER NOT NULL DEFAULT 0"), + # Gründer-Tickets: Kontingent an 50%-Rabatten, die ein Gründer an geworbene + # Freunde weitergeben kann (Liability-Cap; Admin pro Gründer anpassbar). + ("users", "founder_referral_tickets", "INTEGER NOT NULL DEFAULT 25"), # QR-Rückverfolgung: über welchen physischen QR-Code (Sticker/Flyer) kam die Registrierung ("users", "referred_qr", "TEXT"), # Partner-Code → Besitzer (für Self-Service: eigene QR-Kontingente + Stats einsehen) diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 715addd..375cf3a 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -1352,10 +1352,19 @@ def _get_discount_info(conn, user_id: int) -> dict: referred_by = row["referred_by"] or 0 if referred_by > 0: referrer = conn.execute( - "SELECT is_founder, is_founder_pending FROM users WHERE id=?", (referred_by,) + "SELECT is_founder, is_founder_pending, founder_referral_tickets FROM users WHERE id=?", (referred_by,) ).fetchone() if referrer and (referrer["is_founder"] or referrer["is_founder_pending"]): - return {"discount_pct": 100, "reason": "referred_by_founder", "referral_count": row["referral_count"]} + # 50%-Weitergabe nur innerhalb des Ticket-Kontingents des Gründers + # (Rang unter den verifizierten Geworbenen ≤ Tickets). 50%, NICHT 100%. + rank = conn.execute( + """SELECT COUNT(*) FROM users + WHERE referred_by=? AND email_verified=1 + AND created_at <= (SELECT created_at FROM users WHERE id=?)""", + (referred_by, user_id) + ).fetchone()[0] + if rank <= (referrer["founder_referral_tickets"] or 0): + return {"discount_pct": 50, "reason": "referred_by_founder", "referral_count": row["referral_count"]} count = row["referral_count"] for threshold, pct in [(50, 50), (20, 30), (10, 20)]: diff --git a/backend/routes/partner.py b/backend/routes/partner.py index 7690352..3102133 100644 --- a/backend/routes/partner.py +++ b/backend/routes/partner.py @@ -25,6 +25,7 @@ class PartnerCodeCreate(BaseModel): class GrantRequest(BaseModel): is_founder: Optional[int] = None is_partner: Optional[int] = None + founder_tickets: Optional[int] = Field(None, ge=0, le=200) # 50%-Rabatt-Kontingent # ------------------------------------------------------------------ @@ -129,8 +130,10 @@ def grant_user_status(user_id: int, data: GrantRequest, user=Depends(require_adm updates["is_founder"] = data.is_founder if data.is_partner is not None: updates["is_partner"] = data.is_partner + if data.founder_tickets is not None: + updates["founder_referral_tickets"] = data.founder_tickets if not updates: - raise HTTPException(400, "Mindestens is_founder oder is_partner muss angegeben werden.") + raise HTTPException(400, "Mindestens is_founder, is_partner oder founder_tickets muss angegeben werden.") with db() as conn: target = conn.execute( "SELECT id, is_founder, founder_number FROM users WHERE id=?", (user_id,) @@ -167,7 +170,7 @@ def grant_user_status(user_id: int, data: GrantRequest, user=Depends(require_adm (*updates.values(), user_id) ) row = conn.execute( - "SELECT id, name, email, is_founder, is_partner, founder_number FROM users WHERE id=?", + "SELECT id, name, email, is_founder, is_partner, founder_number, founder_referral_tickets FROM users WHERE id=?", (user_id,) ).fetchone() return dict(row) @@ -178,7 +181,7 @@ def search_users(q: str, user=Depends(require_admin)): """User-Suche für Admin (Name-Präfix, max. 10 Ergebnisse).""" with db() as conn: rows = conn.execute( - """SELECT id, name, email, is_founder, is_partner, rolle + """SELECT id, name, email, is_founder, is_partner, rolle, founder_referral_tickets FROM users WHERE name LIKE ? COLLATE NOCASE ORDER BY name LIMIT 10""", (f"{q}%",) diff --git a/backend/scheduler.py b/backend/scheduler.py index 700d047..8c8009c 100644 --- a/backend/scheduler.py +++ b/backend/scheduler.py @@ -326,12 +326,22 @@ async def _create_renewal_invoice_draft(user: dict, expires: date, tier_label: s discount_reason = "founder" elif (disc_row["referred_by"] or 0) > 0: ref = conn.execute( - "SELECT is_founder, is_founder_pending FROM users WHERE id=?", + "SELECT is_founder, is_founder_pending, founder_referral_tickets FROM users WHERE id=?", (disc_row["referred_by"],) ).fetchone() if ref and (ref["is_founder"] or ref["is_founder_pending"]): - discount_pct = 50 - discount_reason = "referred_by_founder" + # 50%-Weitergabe nur solange der Gründer Tickets hat: dieser Freund + # bekommt sie, wenn sein Rang unter den verifizierten Geworbenen + # (nach Anmeldedatum) das Ticket-Kontingent nicht übersteigt. + rank = conn.execute( + """SELECT COUNT(*) FROM users + WHERE referred_by=? AND email_verified=1 + AND created_at <= (SELECT created_at FROM users WHERE id=?)""", + (disc_row["referred_by"], user["id"]) + ).fetchone()[0] + if rank <= (ref["founder_referral_tickets"] or 0): + discount_pct = 50 + discount_reason = "referred_by_founder" if not discount_reason: for thr, pct in [(50, 50), (20, 30), (10, 20)]: if referral_count >= thr: diff --git a/backend/static/index.html b/backend/static/index.html index f861ff1..e45df41 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@
2. Registrierung mit Code — Wenn sich ein neuer User mit diesem Code registriert, wird er automatisch als Gründer markiert (Platz #1–100, lebenslang kostenlos). Du siehst in der Tabelle wie viele Einlösungen jeder Code hat.
3. Partner-Status vergeben — Den Influencer selbst suchst du unten bei «Nutzer-Status» und setzt Partner-Badge (blaues Badge im Profil) und Gründer-Lizenz. So ist auch er als Gründer #X sichtbar.
Max. 100 Gründer — Ist die Zahl bei einem Code leer, ist sie unbegrenzt. Die globale Grenze über alle Codes hinweg sind 100 Gründer-Plätze.
-Freunde werben — Jeder eingeloggte User hat einen persönlichen Einladungslink (Einstellungen → Freunde werben). Bei 10 geworbenen Usern gibt es 20 % Rabatt, bei 20 → 30 %, bei 50 → 50 % — lebenslang, sobald Bezahlfunktionen aktiv sind.
+Freunde werben — Jeder eingeloggte User hat einen persönlichen Einladungslink (Einstellungen → Freunde werben). Bei 10 geworbenen Usern gibt es 20 % Rabatt, bei 20 → 30 %, bei 50 → 50 % — dauerhaft auf Ban Yaro Pro. Gründer können zusätzlich ihren Geworbenen 50 % schenken (begrenzt durch ihre Gründer-Tickets, Standard 25).
@@ -2571,6 +2571,11 @@ window.Page_admin = (() => { Partner-Badge (Creator) +✓ ${div.dataset.name} ausgewählt${div.dataset.founder==='1' ? ' · ⭐ Gründer' : ''}${div.dataset.partner==='1' ? ' · 🤝 Partner' : ''}
`; }); @@ -2806,14 +2813,14 @@ window.Page_admin = (() => { const btn = e.target.querySelector('[type="submit"]'); const isFounder = e.target.querySelector('[name="is_founder"]').checked ? 1 : 0; const isPartner = e.target.querySelector('[name="is_partner"]').checked ? 1 : 0; + const ticketsRaw = e.target.querySelector('[name="founder_tickets"]').value.trim(); await UI.asyncButton(btn, async () => { - const result = await API.post(`/admin/partner/users/${_grantUserId}/grant`, { - is_founder: isFounder, - is_partner: isPartner, - }); + const body = { is_founder: isFounder, is_partner: isPartner }; + if (ticketsRaw !== '') body.founder_tickets = parseInt(ticketsRaw); + const result = await API.post(`/admin/partner/users/${_grantUserId}/grant`, body); if (!result) throw new Error('Keine Antwort vom Server.'); UI.toast.success(`Status für ${result.name} gesetzt.`); - grantResult.innerHTML = `✓ Gründer: ${result.is_founder ? 'Ja' : 'Nein'} | Partner: ${result.is_partner ? 'Ja' : 'Nein'}
`; + grantResult.innerHTML = `✓ Gründer: ${result.is_founder ? 'Ja' : 'Nein'} | Partner: ${result.is_partner ? 'Ja' : 'Nein'} | 🎟 ${result.founder_referral_tickets ?? 25} Tickets
`; }).catch(e => UI.toast.error(e.message || 'Fehler beim Speichern.')); }); } diff --git a/backend/static/js/pages/gruender.js b/backend/static/js/pages/gruender.js index 1c4d56b..6855717 100644 --- a/backend/static/js/pages/gruender.js +++ b/backend/static/js/pages/gruender.js @@ -203,9 +203,9 @@ window.Page_gruender = (() => { ${benefit('🏅', 'Nummerierte Gründer-Badge', 'Ein „Gründer #N"-Abzeichen, dauerhaft sichtbar in deinem Profil und neben jedem Forum-Beitrag.')} ${benefit('👑', 'Lebenslang Ban Yaro Pro', - 'Alle Pro-Funktionen — kostenlos, für immer. Auch wenn Pro später etwas kostet, bleibt es für Gründer gratis.')} - ${benefit('🤝', 'Freunde mitbringen lohnt sich', - 'Wer sich über deine Einladung registriert, bekommt Ban Yaro Pro dauerhaft zum halben Preis.')} + 'Alle Pro-Funktionen — für dich dauerhaft kostenlos, solange es Ban Yaro gibt.')} + ${benefit('🎟️', '25 Freunde zum halben Preis', + 'Du bekommst 25 Einladungen: Wer sich darüber registriert, erhält Ban Yaro Pro dauerhaft für die Hälfte. Dein Geschenk an deine Liebsten.')} ${benefit('🌱', 'Teil der Geschichte', 'Du gehörst zu den Menschen, die Ban Yaro von Anfang an getragen haben — das bleibt.')}- Der Rabatt gilt für dich — sobald Bezahlfunktionen aktiv sind, dauerhaft und automatisch. + Der Rabatt gilt für dich auf Ban Yaro Pro — dauerhaft und automatisch.
`; diff --git a/backend/static/landing.html b/backend/static/landing.html index 0f51de6..3045c5e 100644 --- a/backend/static/landing.html +++ b/backend/static/landing.html @@ -4,7 +4,7 @@ - +