Spaces:
Running
Running
| 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!"}) | |
| 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}) | |
| 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 | |
| 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"] | |
| }) | |
| 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 | |
| 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) | |
| 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]}) | |
| 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 | |
| }) | |