tienhiep-api / backend /api /payment.py
Cong123779
deploy: update backend production to new Space
d9bfc2d
Raw
History Blame Contribute Delete
8.39 kB
import os
import re
from datetime import datetime
from flask import Blueprint, request, jsonify, session
from backend.config import Config
from backend.core.decorators import get_current_user
from backend.core.security import verify_access_token
from backend.core.rate_limit import check_rate_limit, get_client_ip
from backend.database.db_manager import get_user_db_conn
from backend.services.payment_service import (
create_payment_order, confirm_payment, verify_payos_webhook_signature
)
from backend.services.auth_service import check_vip_expiry, activate_vip
payment_bp = Blueprint("payment", __name__, url_prefix="/api/payment")
def _get_user_id_from_request():
"""Helper to get user_id from session or JWT."""
user = get_current_user()
if user:
return user["id"]
auth_header = request.headers.get("Authorization", "")
if auth_header.startswith("Bearer "):
payload = verify_access_token(auth_header[7:])
if payload:
return int(payload["sub"])
return None
def _process_payment_webhook(order_id, amount):
"""Internal handler called from the webhook to confirm payment & activate VIP/topup."""
conn = get_user_db_conn()
payment = conn.execute(
"SELECT * FROM payments WHERE order_id = ? AND status = 'pending'", (order_id,)
).fetchone()
if not payment:
conn.close()
return jsonify({"error": "Order not found or already processed"}), 404
if amount > 0 and abs(amount - payment["amount"]) > 100:
conn.close()
return jsonify({"error": "Amount mismatch"}), 400
now = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
conn.execute(
"UPDATE payments SET status = 'completed', completed_at = ? WHERE id = ?",
(now, payment["id"])
)
conn.commit()
conn.close()
if payment["plan"].startswith("topup_"):
conn = get_user_db_conn()
conn.execute(
"UPDATE users SET api_balance = COALESCE(api_balance, 0.0) + ? WHERE id = ?",
(payment["amount"], payment["user_id"])
)
conn.commit()
conn.close()
return jsonify({"success": True, "message": f"Nạp thành công {payment['amount']:,}đ vào số dư API!".replace(",", ".")})
else:
activate_vip(payment["user_id"], payment["plan"])
if session.get("user_id") == payment["user_id"]:
session["vip_status"] = 1
print(f"[PAYMENT] ✅ Order {order_id} completed — User #{payment['user_id']} → VIP {payment['plan']}")
return jsonify({"success": True, "message": "Payment confirmed, VIP activated!"})
@payment_bp.route("/plans", methods=["GET"])
def api_payment_plans():
lang = request.args.get("lang", "vi")
plans = []
for key, plan in Config.VIP_PLANS.items():
plans.append({
"id": key,
"name": plan.get(f"name_{lang}", plan["name_vi"]),
"description": plan.get(f"description_{lang}", plan["description_vi"]),
"price": plan["price"],
"price_formatted": f"{plan['price']:,}đ".replace(",", "."),
"duration_days": plan["duration_days"],
})
return jsonify({"plans": plans})
@payment_bp.route("/create", methods=["POST"])
def api_payment_create():
user_id = _get_user_id_from_request()
if not user_id:
return jsonify({"error": "Vui lòng đăng nhập để mua VIP."}), 401
ip = get_client_ip()
if check_rate_limit("payment", ip):
return jsonify({"error": "Bạn đã tạo quá nhiều đơn hàng. Vui lòng thử lại sau."}), 429
data = request.json or {}
plan = data.get("plan", "month")
result, status_code = create_payment_order(user_id, plan)
return jsonify(result), status_code
@payment_bp.route("/status/<order_id>", methods=["GET"])
def api_payment_status(order_id):
user_id = _get_user_id_from_request()
conn = get_user_db_conn()
if user_id:
payment = conn.execute(
"SELECT * FROM payments WHERE order_id = ? AND user_id = ?", (order_id, user_id)
).fetchone()
else:
payment = conn.execute(
"SELECT * FROM payments WHERE order_id = ?", (order_id,)
).fetchone()
conn.close()
if not payment:
return jsonify({"error": "Payment order not found."}), 404
return jsonify({
"order_id": payment["order_id"],
"plan": payment["plan"],
"amount": payment["amount"],
"status": payment["status"],
"created_at": payment["created_at"],
"completed_at": payment["completed_at"]
})
@payment_bp.route("/webhook", methods=["POST"])
def api_payment_webhook():
# Verify PAYMENT_WEBHOOK_KEY if set in environment
webhook_key = os.environ.get("PAYMENT_WEBHOOK_KEY", "")
if webhook_key:
auth_header = request.headers.get("Authorization", "")
provided_key = auth_header.replace("Bearer ", "").replace("Apikey ", "").strip()
query_key = request.args.get("webhook_key", "")
if provided_key != webhook_key and query_key != webhook_key:
return jsonify({"error": "Unauthorized webhook request"}), 401
data = request.json or {}
# PayOS format
if "data" in data and "orderCode" in data.get("data", {}):
order_code = str(data["data"]["orderCode"])
amount = data["data"].get("amount", 0)
checksum_key = Config.PAYMENT_CONFIG.get("payos_checksum_key", "")
if checksum_key and "signature" in data:
if not verify_payos_webhook_signature(data["data"], data.get("signature", ""), checksum_key):
return jsonify({"error": "Invalid signature"}), 403
return _process_payment_webhook(order_code, amount)
# SePay / Generic format
if "transferAmount" in data:
content = data.get("content", "")
amount = data.get("transferAmount", 0)
match = re.search(r"(VIP\w+)", content.upper())
if match:
return _process_payment_webhook(match.group(1), amount)
return jsonify({"error": "Invalid webhook payload"}), 400
@payment_bp.route("/confirm-manual", methods=["POST"])
def api_payment_confirm_manual():
data = request.json or {}
admin_key = data.get("admin_key", "")
order_id = data.get("order_id", "")
ADMIN_KEY = os.environ.get("ADMIN_PAYMENT_KEY")
if not ADMIN_KEY:
ADMIN_KEY = "SECURE_SECRET_RANDOM_KEY_9988_ADMIN"
is_admin = False
if admin_key and admin_key == ADMIN_KEY:
is_admin = True
if not is_admin:
return jsonify({"error": "Unauthorized admin access required"}), 403
if not order_id:
return jsonify({"error": "order_id is required"}), 400
return _process_payment_webhook(order_id, 0)
@payment_bp.route("/history", methods=["GET"])
def api_payment_history():
user_id = _get_user_id_from_request()
if not user_id:
return jsonify({"error": "Unauthorized"}), 401
conn = get_user_db_conn()
payments = conn.execute(
"SELECT order_id, plan, amount, status, created_at, completed_at FROM payments WHERE user_id = ? ORDER BY created_at DESC LIMIT 20",
(user_id,)
).fetchall()
conn.close()
return jsonify({"payments": [dict(p) for p in payments]})
@payment_bp.route("/vip-status", methods=["GET"])
def api_user_vip_status():
user_id = _get_user_id_from_request()
if not user_id:
return jsonify({"error": "Unauthorized"}), 401
check_vip_expiry(user_id)
conn = get_user_db_conn()
user = conn.execute("SELECT vip_status, vip_plan, vip_expiry FROM users WHERE id = ?", (user_id,)).fetchone()
conn.close()
if not user:
return jsonify({"error": "User not found"}), 404
days_remaining = 0
if user["vip_expiry"]:
try:
expiry_val = user["vip_expiry"]
if isinstance(expiry_val, str):
expiry = datetime.strptime(expiry_val, "%Y-%m-%d %H:%M:%S")
else:
expiry = expiry_val
days_remaining = max(0, (expiry - datetime.utcnow()).days)
except Exception:
pass
return jsonify({
"vip_status": user["vip_status"],
"vip_plan": user["vip_plan"],
"vip_expiry": user["vip_expiry"],
"days_remaining": days_remaining,
"is_active": user["vip_status"] == 1
})