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:
parent
d61fd155c5
commit
f6b37717b4
9 changed files with 268 additions and 27 deletions
|
|
@ -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é & 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"]}
|
||||
|
|
|
|||
|
|
@ -335,6 +335,46 @@ async def forgot_password(data: ForgotPasswordRequest, request: Request):
|
|||
return {"ok": True}
|
||||
|
||||
|
||||
class UpgradeRequestBody(BaseModel):
|
||||
tier: str
|
||||
message: Optional[str] = None
|
||||
|
||||
@router.post("/upgrade-request")
|
||||
async def create_upgrade_request(data: UpgradeRequestBody, user=Depends(get_current_user)):
|
||||
_VALID = {"pro", "breeder"}
|
||||
if data.tier not in _VALID:
|
||||
raise HTTPException(400, "Ungültiger Tarif.")
|
||||
with db() as conn:
|
||||
existing = conn.execute(
|
||||
"SELECT id FROM upgrade_requests WHERE user_id=? AND tier=? AND fulfilled_at IS NULL",
|
||||
(user["id"], data.tier)
|
||||
).fetchone()
|
||||
if existing:
|
||||
return {"ok": True, "already": True}
|
||||
conn.execute(
|
||||
"INSERT INTO upgrade_requests (user_id, tier, message) VALUES (?,?,?)",
|
||||
(user["id"], data.tier, data.message or None)
|
||||
)
|
||||
email = conn.execute("SELECT email FROM users WHERE id=?", (user["id"],)).fetchone()["email"]
|
||||
|
||||
tier_labels = {"pro": "Ban Yaro Pro", "breeder": "Züchter"}
|
||||
tier_label = tier_labels[data.tier]
|
||||
admin_email = os.getenv("ADMIN_EMAIL", "")
|
||||
if admin_email:
|
||||
try:
|
||||
from routes.outreach import _send_smtp
|
||||
subject = f"[Ban Yaro] Upgrade-Anfrage: {tier_label} — {user['name']}"
|
||||
body = (f"Neue Upgrade-Anfrage:\n\n"
|
||||
f"Nutzer: {user['name']} ({email})\n"
|
||||
f"Tarif: {tier_label}\n"
|
||||
f"Nachricht: {data.message or '—'}\n\n"
|
||||
f"Admin-Panel: https://banyaro.app/#admin")
|
||||
_send_smtp(admin_email, subject, body, "support")
|
||||
except Exception:
|
||||
pass
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.post("/reset-password")
|
||||
async def reset_password(data: ResetPasswordRequest, request: Request):
|
||||
rl_check(request, max_requests=5, window_seconds=3600, key="reset_pw")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue