Feature: Upgrade-Anfragen-System — User-Flow + Admin-Panel (SW by-v920)

- DB: upgrade_requests-Tabelle (user_id, tier, message, fulfilled_at)
- POST /api/upgrade-request: Anfrage speichern + Admin-Benachrichtigungsmail
- GET/POST /api/admin/upgrade-requests[/{id}/fulfill]: Admin-Endpunkte
  — fulfill setzt subscription_tier + sendet Bestätigungsmail an User
- action-items: upgrades_pending zählt offene Anfragen → Badge im Admin
- Admin-Tab "Upgrades": Tabelle offener/erledigter Anfragen, Freischalten-Button
  mit Confirm-Modal, automatischer Tier-Setzung und Bestätigungsmail
- Settings: Upgrade-Modal sendet echte API-Anfrage statt nur mailto
  — doppelte Anfrage wird erkannt (already:true → Toast statt Fehler)
- api.js: API.auth.upgradeRequest(tier, message) hinzugefügt
- SW by-v920, APP_VER 920
This commit is contained in:
rene 2026-05-14 09:59:11 +02:00
parent d61fd155c5
commit f6b37717b4
9 changed files with 268 additions and 27 deletions

View file

@ -124,13 +124,17 @@ async def action_items(user=Depends(require_mod)):
users_today = conn.execute(
"SELECT COUNT(*) FROM users WHERE DATE(created_at)=DATE('now')"
).fetchone()[0]
upgrades_pending = conn.execute(
"SELECT COUNT(*) FROM upgrade_requests WHERE fulfilled_at IS NULL"
).fetchone()[0]
return {
"jobs_pending": jobs,
"breeder_pending": breeders,
"reports_open": reports,
"fotos_pending": fotos,
"poi_edits_pending": poi_edits,
"users_today": users_today,
"jobs_pending": jobs,
"breeder_pending": breeders,
"reports_open": reports,
"fotos_pending": fotos,
"poi_edits_pending": poi_edits,
"users_today": users_today,
"upgrades_pending": upgrades_pending,
}
@ -1091,3 +1095,66 @@ async def generate_media_previews(user=Depends(require_admin)):
errors += 1
return {"generated": generated, "skipped": skipped, "errors": errors}
# ------------------------------------------------------------------
# GET /api/admin/upgrade-requests — offene Upgrade-Anfragen
# POST /api/admin/upgrade-requests/{id}/fulfill — Tier setzen + Mail
# ------------------------------------------------------------------
@router.get("/upgrade-requests")
async def list_upgrade_requests(user=Depends(require_admin)):
with db() as conn:
rows = conn.execute("""
SELECT r.id, r.user_id, r.tier, r.message, r.created_at, r.fulfilled_at,
u.name, u.email
FROM upgrade_requests r
JOIN users u ON u.id = r.user_id
ORDER BY r.fulfilled_at IS NOT NULL, r.created_at DESC
LIMIT 100
""").fetchall()
return [dict(r) for r in rows]
@router.post("/upgrade-requests/{req_id}/fulfill")
async def fulfill_upgrade_request(req_id: int, user=Depends(require_admin)):
with db() as conn:
req = conn.execute(
"SELECT r.*, u.name, u.email FROM upgrade_requests r JOIN users u ON u.id=r.user_id WHERE r.id=?",
(req_id,)
).fetchone()
if not req:
raise HTTPException(404, "Anfrage nicht gefunden.")
if req["fulfilled_at"]:
raise HTTPException(400, "Bereits erledigt.")
if req["tier"] not in _VALID_TIERS:
raise HTTPException(400, "Ungültiger Tier.")
conn.execute(
"UPDATE users SET subscription_tier=? WHERE id=?",
(req["tier"], req["user_id"])
)
conn.execute(
"UPDATE upgrade_requests SET fulfilled_at=strftime('%Y-%m-%dT%H:%M:%fZ','now'), fulfilled_by=? WHERE id=?",
(user["id"], req_id)
)
_audit(conn, user, "fulfill_upgrade", f"user:{req['user_id']}", f"tier={req['tier']}")
tier_labels = {"pro": "Ban Yaro Pro", "breeder": "Züchter"}
tier_label = tier_labels.get(req["tier"], req["tier"])
try:
from mailer import send_email, email_html
body_html = f"""
<p>Hallo {req['name']},</p>
<p>dein Account wurde soeben auf <strong>{tier_label}</strong> freigeschaltet.</p>
<p>Du kannst alle {tier_label}-Features ab sofort in der App nutzen.
Öffne Ban Yaro und lade die App einmal neu dann ist dein neuer Tarif aktiv.</p>
<p>Vielen Dank für dein Vertrauen!</p>
<p>Viele Grüße<br>René &amp; das Ban Yaro Team</p>"""
html = email_html(body_html, cta_url="https://banyaro.app", cta_label="Ban Yaro öffnen")
plain = (f"Hallo {req['name']},\n\ndein Account wurde auf {tier_label} freigeschaltet.\n"
f"Öffne Ban Yaro und lade die App neu.\n\nViele Grüße\nRené")
await send_email(req["email"], f"Dein {tier_label}-Zugang ist aktiv", html, plain)
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"Bestätigungsmail fehlgeschlagen: {e}")
return {"ok": True, "tier": req["tier"], "user": req["name"]}