80 lines
3.0 KiB
Python
80 lines
3.0 KiB
Python
# 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:<id>|code:<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}
|