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:
rene 2026-06-08 06:20:19 +02:00
parent 98ec6c36c6
commit 60fb866283
13 changed files with 154 additions and 35 deletions

View file

@ -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;

View file

@ -2305,7 +2305,7 @@ window.Page_admin = (() => {
<p style="margin:0"><strong>2. Registrierung mit Code</strong> Wenn sich ein neuer User mit diesem Code registriert, wird er automatisch als <em>Gründer</em> markiert (Platz #1100, lebenslang kostenlos). Du siehst in der Tabelle wie viele Einlösungen jeder Code hat.</p>
<p style="margin:0"><strong>3. Partner-Status vergeben</strong> Den Influencer selbst suchst du unten bei «Nutzer-Status» und setzt <em>Partner-Badge</em> (blaues Badge im Profil) und <em>Gründer-Lizenz</em>. So ist auch er als Gründer #X sichtbar.</p>
<p style="margin:0"><strong>Max. 100 Gründer</strong> Ist die Zahl bei einem Code leer, ist sie unbegrenzt. Die globale Grenze über alle Codes hinweg sind 100 Gründer-Plätze.</p>
<p style="margin:0"><strong>Freunde werben</strong> 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.</p>
<p style="margin:0"><strong>Freunde werben</strong> 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).</p>
</div>
</div>
@ -2571,6 +2571,11 @@ window.Page_admin = (() => {
Partner-Badge (Creator)
</label>
</div>
<div>
<label class="form-label text-xs">Gründer-Tickets <span class="text-muted">(50%-Rabatt-Kontingent für geworbene Freunde · Standard 25)</span></label>
<input class="form-control" name="founder_tickets" id="adm-grant-tickets" type="number" min="0" max="200"
placeholder="25" style="max-width:120px">
</div>
<button type="submit" class="btn btn-secondary btn-sm" style="align-self:flex-start">
${UI.icon('check')} Status setzen
</button>
@ -2772,6 +2777,7 @@ window.Page_admin = (() => {
grantResult.innerHTML = users.map(u => `
<div class="adm-grant-user" data-id="${u.id}" data-name="${u.name}"
data-founder="${u.is_founder||0}" data-partner="${u.is_partner||0}"
data-tickets="${u.founder_referral_tickets ?? 25}"
style="padding:var(--space-2) var(--space-3);border-radius:var(--radius-sm);
cursor:pointer;background:var(--c-surface-2);margin-bottom:2px;
font-size:var(--text-sm);display:flex;justify-content:space-between">
@ -2790,6 +2796,7 @@ window.Page_admin = (() => {
if (form) {
form.querySelector('[name="is_founder"]').checked = div.dataset.founder === '1';
form.querySelector('[name="is_partner"]').checked = div.dataset.partner === '1';
form.querySelector('[name="founder_tickets"]').value = div.dataset.tickets ?? 25;
}
grantResult.innerHTML = `<p style="font-size:var(--text-xs);color:var(--c-success,#16a34a)">✓ ${div.dataset.name} ausgewählt${div.dataset.founder==='1' ? ' · ⭐ Gründer' : ''}${div.dataset.partner==='1' ? ' · 🤝 Partner' : ''}</p>`;
});
@ -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 = `<p style="font-size:var(--text-xs);color:var(--c-success,#16a34a)">✓ Gründer: ${result.is_founder ? 'Ja' : 'Nein'} | Partner: ${result.is_partner ? 'Ja' : 'Nein'}</p>`;
grantResult.innerHTML = `<p style="font-size:var(--text-xs);color:var(--c-success,#16a34a)">✓ Gründer: ${result.is_founder ? 'Ja' : 'Nein'} | Partner: ${result.is_partner ? 'Ja' : 'Nein'} | 🎟 ${result.founder_referral_tickets ?? 25} Tickets</p>`;
}).catch(e => UI.toast.error(e.message || 'Fehler beim Speichern.'));
});
}

View file

@ -203,9 +203,9 @@ window.Page_gruender = (() => {
${benefit('🏅', 'Nummerierte Gründer-Badge',
'Ein <strong>„Gründer #N"</strong>-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 <em>deine</em> 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.')}
</div>

View file

@ -882,7 +882,7 @@ window.Page_settings = (() => {
<div style="padding:var(--space-4);border-bottom:1px solid var(--c-border)">
<div style="font-weight:600;margin-bottom:2px">${UI.icon('arrow-square-out')} Freunde werben dauerhafter Rabatt</div>
<div class="text-xs-secondary">
10 Freunde 20% · 20 Freunde 30% · 50 Freunde 50% lebenslang, sobald Bezahlfunktionen aktiv sind.
10 Freunde 20% · 20 Freunde 30% · 50 Freunde 50% dauerhaft auf Ban Yaro Pro.
</div>
</div>
<div id="referral-body" class="p-4">Lade</div>
@ -1821,7 +1821,7 @@ window.Page_settings = (() => {
</div>
</div>
<p style="font-size:var(--text-xs);color:var(--c-text-muted);text-align:center;margin:0">
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.
</p>
`;