+ Ban Yaro wird von Hundemenschen für Hundemenschen aufgebaut. Die ersten 100,
+ die über einen unserer Partner an Bord kommen, bekommen einen festen Platz in der Geschichte
+ der App — sichtbar, dauerhaft und nie wieder vergeben.
+
+
+
So wirst du Gründer
+
+ ${step(1, 'ticket', 'Einladungscode eines Partners',
+ 'Unsere Partner (Hundeblogs, Trainer:innen, Vereine) haben persönliche Einladungscodes — als Link, QR-Code auf Sticker oder zum Eintippen. Registrierst du dich darüber, ist dein Gründer-Platz reserviert.')}
+ ${step(2, 'paw-print', 'Erstes Hundeprofil anlegen',
+ 'Sobald du deinen Hund anlegst, wird der reservierte Platz fest dir zugeschrieben — du erhältst deine feste Gründer-Nummer in der Reihenfolge der Anmeldung.')}
+ ${step(3, 'seal-check', 'Für immer Gründer #N',
+ 'Deine Nummer bleibt dir — egal was kommt. Auch wenn alle 100 Plätze vergeben sind, behältst du deinen.')}
+
+
+
Deine Vorteile
+
+ ${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.')}
+ ${benefit('🌱', 'Teil der Geschichte',
+ 'Du gehörst zu den Menschen, die Ban Yaro von Anfang an getragen haben — das bleibt.')}
+
+
+
+ ${open > 0
+ ? `Noch ${open} von 100 Plätzen frei. Du kennst keinen Partner?
+ Hier siehst du, wer gerade einlädt.`
+ : `Alle 100 Plätze sind vergeben — diese Gruppe ist für immer geschlossen.`}
+
+
`;
+ }
+
+
return { init, refresh, onDogChange };
})();
diff --git a/backend/static/landing.html b/backend/static/landing.html
index cfed93e..0f51de6 100644
--- a/backend/static/landing.html
+++ b/backend/static/landing.html
@@ -4,7 +4,7 @@
-
+
Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz
diff --git a/backend/static/sw.js b/backend/static/sw.js
index 7998404..a0d0ddf 100644
--- a/backend/static/sw.js
+++ b/backend/static/sw.js
@@ -4,7 +4,7 @@
============================================================ */
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
-const VER = '1270';
+const VER = '1271';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
From 60fb86628378dad44a4c239a0a28a80d54deaf2e Mon Sep 17 00:00:00 2001
From: rene
Date: Mon, 8 Jun 2026 06:20:19 +0200
Subject: [PATCH 2/3] =?UTF-8?q?Gr=C3=BCnder-Tickets:=2050%-Rabatt-Weiterga?=
=?UTF-8?q?be=20pro=20Gr=C3=BCnder=20gedeckelt=20+=20Pro-Wording=20korrigi?=
=?UTF-8?q?ert?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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.
---
VERSION | 2 +-
backend/database.py | 3 +
backend/routes/admin.py | 13 ++++-
backend/routes/partner.py | 9 ++-
backend/scheduler.py | 16 +++++-
backend/static/index.html | 24 ++++----
backend/static/js/app.js | 2 +-
backend/static/js/pages/admin.js | 19 +++++--
backend/static/js/pages/gruender.js | 6 +-
backend/static/js/pages/settings.js | 4 +-
backend/static/landing.html | 2 +-
backend/static/sw.js | 2 +-
tests/test_founder_tickets.py | 87 +++++++++++++++++++++++++++++
13 files changed, 154 insertions(+), 35 deletions(-)
create mode 100644 tests/test_founder_tickets.py
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 @@
Ban Yaro
-
+
-
-
-
-
-
+
+
+
+
+
@@ -620,11 +620,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -634,7 +634,7 @@
-
+
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index 3098125..c9ee7fa 100644
--- a/backend/static/js/app.js
+++ b/backend/static/js/app.js
@@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
-const APP_VER = '1271'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '1272'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
window.APP_VERSION = APP_VERSION;
diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js
index 21120e7..f5ad0ee 100644
--- a/backend/static/js/pages/admin.js
+++ b/backend/static/js/pages/admin.js
@@ -2305,7 +2305,7 @@ window.Page_admin = (() => {
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).
`;
}).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.