Gründer-Tickets: 50%-Rabatt-Weitergabe pro Gründer gedeckelt + Pro-Wording korrigiert
Rene: 'ungern jemandem auf ewig die Möglichkeit geben 50% Rabatt zu vergeben — bei 100 Gründern ein großer Faktor. Ich hätte jedem 25–50 Tickets gegeben.' - users.founder_referral_tickets (Default 25): Kontingent an 50%-Rabatten, die ein Gründer an Geworbene weitergeben kann. Technisch = die ersten N VERIFIZIERTEN Geworbenen (nach Anmeldedatum) bekommen 50%, danach 0. Unbestätigte verbrauchen kein Ticket. In scheduler.py (Rechnung) + admin.py (Vorschau) konsistent. - BUGFIX nebenbei: admin.py zeigte für referred_by_founder fälschlich 100% statt 50% (scheduler war korrekt) — jetzt beide 50%. - Admin: Grant-Formular bekommt Feld 'Gründer-Tickets' (0–200, Vorbelegung aus User-Stand); Endpoint /grant akzeptiert founder_tickets. - Gründer-Seite + Settings + Admin-Hilfe: 'sobald Bezahlfunktionen aktiv sind' raus (Pro kostet bereits); Vorteil 'lebenslang Pro gratis' + '25 Freunde zum halben Preis' (Ticket-Framing). - Tests: test_founder_tickets.py (Cap, Unverified-Schutz, 50%-Bugfix, Grant). Suite: 64 passed.
This commit is contained in:
parent
98ec6c36c6
commit
60fb866283
13 changed files with 154 additions and 35 deletions
87
tests/test_founder_tickets.py
Normal file
87
tests/test_founder_tickets.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"""Gründer-Tickets: 50%-Rabatt-Weitergabe ist pro Gründer auf sein Kontingent gedeckelt.
|
||||
|
||||
Hintergrund: Ein Gründer kann geworbenen Freunden 50% auf Pro schenken. Ohne Cap
|
||||
könnten 100 Gründer unbegrenzt viele 50%-Rabatte vergeben — unkalkulierbare Liability.
|
||||
Jeder Gründer hat daher ein Ticket-Kontingent (Standard 25), das die ersten N
|
||||
verifizierten Geworbenen abdeckt.
|
||||
"""
|
||||
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
def _make_founder(email, tickets=25):
|
||||
from database import db
|
||||
with db() as conn:
|
||||
uid = conn.execute("SELECT id FROM users WHERE email=?", (email,)).fetchone()["id"]
|
||||
conn.execute(
|
||||
"UPDATE users SET is_founder=1, founder_number=99, founder_referral_tickets=? WHERE id=?",
|
||||
(tickets, uid),
|
||||
)
|
||||
return uid
|
||||
|
||||
|
||||
def _add_referred(founder_id, n, verified=True, base_minutes=0):
|
||||
"""Legt n direkt in der DB an, die vom Gründer geworben wurden (mit gestaffeltem created_at)."""
|
||||
from database import db
|
||||
ids = []
|
||||
with db() as conn:
|
||||
for i in range(n):
|
||||
ts = (datetime(2026, 1, 1) + timedelta(minutes=base_minutes + i)).isoformat()
|
||||
conn.execute(
|
||||
"""INSERT INTO users (email, name, pw_hash, referred_by, email_verified, created_at)
|
||||
VALUES (?,?,?,?,?,?)""",
|
||||
(f"ref-{secrets.token_hex(5)}@example.com", f"r{secrets.token_hex(3)}",
|
||||
"x", founder_id, 1 if verified else 0, ts),
|
||||
)
|
||||
ids.append(conn.execute("SELECT last_insert_rowid()").fetchone()[0])
|
||||
return ids
|
||||
|
||||
|
||||
def _discount(client, admin, uid):
|
||||
r = client.get(f"/api/admin/users/{uid}/discount", headers=admin["headers"])
|
||||
assert r.status_code == 200, r.text
|
||||
return r.json()
|
||||
|
||||
|
||||
def test_referred_by_founder_is_50_not_100(client, admin, user):
|
||||
"""Bugfix-Absicherung: Geworbene eines Gründers bekommen 50%, nicht 100%."""
|
||||
fid = _make_founder(user["email"], tickets=25)
|
||||
friend = _add_referred(fid, 1)[0]
|
||||
d = _discount(client, admin, friend)
|
||||
assert d["discount_pct"] == 50
|
||||
assert d["reason"] == "referred_by_founder"
|
||||
|
||||
|
||||
def test_tickets_cap_the_50_percent(client, admin, user):
|
||||
"""Mit 2 Tickets bekommen nur die ersten 2 Geworbenen 50%, der 3. nichts."""
|
||||
fid = _make_founder(user["email"], tickets=2)
|
||||
f1, f2, f3 = _add_referred(fid, 3)
|
||||
assert _discount(client, admin, f1)["discount_pct"] == 50
|
||||
assert _discount(client, admin, f2)["discount_pct"] == 50
|
||||
d3 = _discount(client, admin, f3)
|
||||
assert d3["discount_pct"] == 0
|
||||
assert d3["reason"] is None
|
||||
|
||||
|
||||
def test_unverified_dont_consume_tickets(client, admin, user):
|
||||
"""Unbestätigte Geworbene verbrauchen kein Ticket — ein späterer bestätigter bekommt 50%."""
|
||||
fid = _make_founder(user["email"], tickets=1)
|
||||
# 2 unbestätigte zuerst, dann 1 bestätigter
|
||||
_add_referred(fid, 2, verified=False, base_minutes=0)
|
||||
later = _add_referred(fid, 1, verified=True, base_minutes=10)[0]
|
||||
assert _discount(client, admin, later)["discount_pct"] == 50
|
||||
|
||||
|
||||
def test_admin_grant_sets_tickets(client, admin, user):
|
||||
"""Admin kann das Ticket-Kontingent über den Grant-Endpoint setzen."""
|
||||
from database import db
|
||||
with db() as conn:
|
||||
uid = conn.execute("SELECT id FROM users WHERE email=?", (user["email"],)).fetchone()["id"]
|
||||
r = client.post(f"/api/admin/partner/users/{uid}/grant", headers=admin["headers"],
|
||||
json={"is_founder": 1, "founder_tickets": 50})
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.json()["founder_referral_tickets"] == 50
|
||||
with db() as conn:
|
||||
val = conn.execute("SELECT founder_referral_tickets FROM users WHERE id=?", (uid,)).fetchone()[0]
|
||||
assert val == 50
|
||||
Loading…
Add table
Add a link
Reference in a new issue