# app/api/paypal_ipn.py import httpx, urllib.parse, os, re from fastapi import APIRouter, Request, Response, Depends, HTTPException from sqlalchemy.orm import Session from app.core.database import get_db from app.models.topup import Topup, TopupStatus from app.models.user import User router = APIRouter(prefix="/paypal", tags=["paypal"]) PP_IPN_VERIFY_URL = "https://ipnpb.paypal.com/cgi-bin/webscr" # live # Sandbox: "https://ipnpb.sandbox.paypal.com/cgi-bin/webscr" def parse_custom(s: str): # erwartet "topup:|code:" data = {} for part in (s or "").split("|"): if ":" in part: k,v = part.split(":",1) data[k.strip()] = v.strip() return data @router.post("/ipn") async def handle_ipn(request: Request, db: Session = Depends(get_db)): raw = await request.body() params = urllib.parse.parse_qs(raw.decode("utf-8"), keep_blank_values=True) # 1) an PayPal zurückposten: verify_payload = "cmd=_notify-validate&" + raw.decode("utf-8") async with httpx.AsyncClient(timeout=15) as c: vr = await c.post(PP_IPN_VERIFY_URL, data=verify_payload, headers={"Content-Type":"application/x-www-form-urlencoded"}) if vr.text.strip() != "VERIFIED": return Response("ignored", status_code=400) payment_status = (params.get("payment_status",[None])[0] or "").lower() receiver_email = (params.get("receiver_email",[None])[0] or "").lower() mc_currency = params.get("mc_currency",[None])[0] or "" mc_gross = params.get("mc_gross",[None])[0] or "" custom = params.get("custom",[None])[0] or "" item_name = params.get("item_name",[None])[0] or "" txn_id = params.get("txn_id",[None])[0] or "" # Grundvalidierungen if payment_status != "completed": return {"ok": True} # nur completed interessiert # Optional: Empfänger-Email prüfen (gegen Config) # if receiver_email != CONFIG.paypal_receiver.lower(): return Response("wrong receiver", 400) if mc_currency != "EUR": return Response("wrong currency", 400) meta = parse_custom(custom) topup_id = int(meta.get("topup") or 0) code = meta.get("code") or "" t = db.query(Topup).filter(Topup.id == topup_id).first() if not t or t.status == TopupStatus.confirmed: return {"ok": True} # Betrag prüfen try: cents_from_paypal = int(round(float(mc_gross.replace(",", ".")) * 100)) except Exception: return Response("bad amount", 400) if int(t.amount_cents or 0) != cents_from_paypal: return Response("amount mismatch", 400) # Sicherheits-Match: Code im item_name? if code and code not in (item_name or ""): # nicht hart abbrechen, aber flaggen wäre möglich pass # Topup bestätigen + Saldo buchen (idempotent) t.status = TopupStatus.confirmed u = db.query(User).filter(User.id == t.user_id).first() u.balance_cents = int(u.balance_cents or 0) + int(t.amount_cents or 0) # Optional: t.paypal_txn_id = txn_id db.commit() return {"ok": True}