Feature: Dank-Mail an Partner bei bestätigter Registrierung — mit Statistik

Trigger ist die E-Mail-Bestätigung des Geworbenen (nicht die rohe
Registrierung — konsistent zur Registrierungen/Versuche-Zählung) und nur
beim ersten Verify (Doppelklick auf den Link = keine zweite Mail).

Inhalt: Dank + Bilanz (bestätigte Registrierungen gesamt + diesen Monat),
bei QR-Herkunft der Sticker (#seq, Kontingent-Label), bei Gründer-Codes
die offenen Plätze; CTA zur Partner-Statistik. Versand über das
partner@-Konto, Fehler landen in failed_emails (context partner_thank_you).

Env-Fund dabei: SMTP_PASS fehlte in BEIDEN .env (nur SMTP_SUPPORT_PASS da)
— Partner-Konto-Versand wäre fehlgeschlagen; auf der DS ergänzt.
Test: Mail-Capture per monkeypatch, prüft Statistik + Sticker-Nr +
Einmaligkeit. Suite grün.
This commit is contained in:
rene 2026-06-07 18:51:54 +02:00
parent df2f42f8ac
commit 3d7d5dc1c4
7 changed files with 144 additions and 16 deletions

View file

@ -397,6 +397,85 @@ async def me(user=Depends(get_current_user)):
return data
def _notify_partner_registration(user_id: int):
"""Dank-Mail an den Partner (Code-Besitzer), wenn ein Geworbener seine
E-Mail bestätigt hat inkl. kleiner Statistik. Best effort."""
import html as _html
with db() as conn:
u = conn.execute(
"SELECT referred_by, referred_qr FROM users WHERE id=?", (user_id,)
).fetchone()
if not u or (u["referred_by"] or 0) >= 0:
return # kein Partner-Code im Spiel
code_id = -u["referred_by"]
pc = conn.execute(
"""SELECT pc.code, pc.label, pc.grants_founder, pc.owner_user_id,
o.name AS owner_name, o.email AS owner_email
FROM partner_codes pc
LEFT JOIN users o ON o.id = pc.owner_user_id
WHERE pc.id=?""",
(code_id,)
).fetchone()
if not pc or not pc["owner_email"]:
return # Code ohne Besitzer → niemand zu benachrichtigen
total = conn.execute(
"SELECT COUNT(*) FROM users WHERE referred_by=? AND email_verified=1",
(-code_id,)
).fetchone()[0]
month = conn.execute(
"""SELECT COUNT(*) FROM users
WHERE referred_by=? AND email_verified=1
AND strftime('%Y-%m', created_at) = strftime('%Y-%m', 'now')""",
(-code_id,)
).fetchone()[0]
qr_line = ""
if u["referred_qr"]:
qr = conn.execute(
"""SELECT q.seq, b.label FROM partner_qr_codes q
JOIN partner_qr_batches b ON b.id = q.batch_id
WHERE q.token=?""",
(u["referred_qr"],)
).fetchone()
if qr:
qr_line = f"Gekommen über deinen gedruckten QR-Code #{qr['seq']} (Kontingent „{qr['label']}“)."
founder_line = ""
if pc["grants_founder"]:
founders = conn.execute(
"SELECT COUNT(*) FROM users WHERE is_founder=1"
).fetchone()[0]
founder_line = f"Noch {max(0, 100 - founders)} von 100 Gründer-Plätzen frei."
subject = "🐾 Danke! Neue Registrierung über deinen Partner-Code"
_oname = _html.escape(pc["owner_name"] or "Partner")
stats_html = (
f"<p style='margin:0 0 16px'>Deine Bilanz mit dem Code <b>{pc['code']}</b>:<br>"
f"<b>{total}</b> bestätigte Registrierung{'en' if total != 1 else ''} insgesamt · "
f"<b>{month}</b> in diesem Monat.</p>"
)
body_html = f"""
<p style="margin:0 0 16px">Hallo <b>{_oname}</b>,</p>
<p style="margin:0 0 16px">
gerade hat ein neuer Hundefreund seine Registrierung über deinen
Partner-Code bestätigt danke, dass du Ban Yaro weiterträgst! 🎉
</p>
{f'<p style="margin:0 0 16px">{_html.escape(qr_line)}</p>' if qr_line else ''}
{stats_html}
{f'<p style="margin:0 0 16px;color:#888">{_html.escape(founder_line)}</p>' if founder_line else ''}"""
plain = (f"Hallo {pc['owner_name'] or 'Partner'},\n\n"
f"gerade hat ein neuer Hundefreund seine Registrierung über deinen Partner-Code bestätigt — danke!\n"
+ (f"\n{qr_line}\n" if qr_line else "")
+ f"\nDeine Bilanz mit dem Code {pc['code']}: {total} bestätigte Registrierungen insgesamt, {month} in diesem Monat.\n"
+ (f"{founder_line}\n" if founder_line else "")
+ f"\nDeine Statistik: {_APP_URL}/#partner-profil\n")
try:
from routes.outreach import _send_smtp
from mailer import email_html
html = email_html(body_html, cta_url=f"{_APP_URL}/#partner-profil", cta_label="Meine Partner-Statistik")
_send_smtp(pc["owner_email"], subject, plain, "partner", html=html)
except Exception as exc:
_log_smtp_failure(pc["owner_email"], subject, plain, exc, context="partner_thank_you")
@router.get("/verify-email/{token}")
async def verify_email(token: str):
with db() as conn:
@ -409,6 +488,9 @@ async def verify_email(token: str):
"UPDATE users SET email_verified=1, verification_token=NULL WHERE id=?",
(row["id"],)
)
# Dank-Mail an den Partner — nur beim ERSTEN Bestätigen (Link doppelt geklickt = kein Spam)
if not row["email_verified"]:
_notify_partner_registration(row["id"])
return RedirectResponse(f"{_APP_URL}/#settings?verified=1", status_code=302)

View file

@ -86,14 +86,14 @@
<title>Ban Yaro</title>
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
<script src="/js/boot-early.js?v=1259"></script>
<script src="/js/boot-early.js?v=1260"></script>
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
<link rel="stylesheet" href="/css/design-system.css?v=1259">
<link rel="stylesheet" href="/css/layout.css?v=1259">
<link rel="stylesheet" href="/css/components.css?v=1259">
<link rel="stylesheet" href="/css/utilities.css?v=1259">
<link rel="stylesheet" href="/css/lists.css?v=1259">
<link rel="stylesheet" href="/css/design-system.css?v=1260">
<link rel="stylesheet" href="/css/layout.css?v=1260">
<link rel="stylesheet" href="/css/components.css?v=1260">
<link rel="stylesheet" href="/css/utilities.css?v=1260">
<link rel="stylesheet" href="/css/lists.css?v=1260">
</head>
<body>
@ -612,11 +612,11 @@
<div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=1259"></script>
<script src="/js/ui.js?v=1259"></script>
<script src="/js/app.js?v=1259"></script>
<script src="/js/worlds.js?v=1259"></script>
<script src="/js/offline-indicator.js?v=1259"></script>
<script src="/js/api.js?v=1260"></script>
<script src="/js/ui.js?v=1260"></script>
<script src="/js/app.js?v=1260"></script>
<script src="/js/worlds.js?v=1260"></script>
<script src="/js/offline-indicator.js?v=1260"></script>
<!-- Feature-Seiten werden lazy geladen -->
@ -626,7 +626,7 @@
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
<script src="/js/boot.js?v=1259"></script>
<script src="/js/boot.js?v=1260"></script>
</body>

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '1259'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '1260'; // ← 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

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<script src="/js/landing-init.js?v=1259"></script>
<script src="/js/landing-init.js?v=1260"></script>
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, direkt im Browser.">
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">

View file

@ -4,7 +4,7 @@
============================================================ */
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
const VER = '1259';
const VER = '1260';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten