"""Razorpay order + verify; coupon waivers mint the same billing_token as paid checkout.""" from __future__ import annotations import traceback import uuid from flask import jsonify, request from . import billing_bp from ..config import Config from ..services.supabase_jobs import sync_enabled from ..utils.billing_token import coupon_codes_set, mint_billing_token from ..utils.logger import get_logger from ..utils.supabase_auth import resolve_pipeline_user_id logger = get_logger("mirofish.api.billing") def _razorpay_enabled() -> bool: return bool(Config.RAZORPAY_KEY_ID and Config.RAZORPAY_KEY_SECRET) def _require_pipeline_user_when_sync(): """Same identity as /api/pipeline/* so billing cannot succeed while start would 401.""" if request.method == "OPTIONS": return None if not sync_enabled(): return None uid, err = resolve_pipeline_user_id() if err or not uid: return ( jsonify( { "success": False, "error": err or "Unauthorized — sign in and ensure VITE_PIPELINE_CLIENT_SECRET matches server PIPELINE_CLIENT_SECRET (required to record runs in Supabase).", } ), 401, ) return None @billing_bp.route("/order", methods=["POST"]) def billing_create_order(): """ JSON body: { "coupon": "optional" } - Valid coupon → { waived, billing_token } - Razorpay configured → { waived: false, key_id, order_id, amount, currency } - Razorpay not configured → dev bypass { waived, billing_token, dev_bypass } """ try: gate = _require_pipeline_user_when_sync() if gate: return gate data = request.get_json(silent=True) or {} coupon = (data.get("coupon") or "").strip().upper() codes = coupon_codes_set() if coupon and coupon in codes: tok = mint_billing_token(source="coupon") return jsonify({"success": True, "data": {"waived": True, "billing_token": tok}}) if not _razorpay_enabled(): tok = mint_billing_token(source="dev_bypass") return jsonify( { "success": True, "data": {"waived": True, "billing_token": tok, "dev_bypass": True}, } ) import razorpay client = razorpay.Client(auth=(Config.RAZORPAY_KEY_ID, Config.RAZORPAY_KEY_SECRET)) receipt = "mf_" + str(uuid.uuid4()).replace("-", "")[:24] order = client.order.create( { "amount": Config.BILLING_AMOUNT_MINOR, "currency": Config.BILLING_CURRENCY, "receipt": receipt, "payment_capture": 1, } ) return jsonify( { "success": True, "data": { "waived": False, "key_id": Config.RAZORPAY_KEY_ID, "order_id": order["id"], "amount": int(order["amount"]), "currency": str(order["currency"]), }, } ) except Exception as e: logger.error(traceback.format_exc()) return jsonify({"success": False, "error": str(e)}), 500 @billing_bp.route("/verify", methods=["POST"]) def billing_verify(): """JSON: razorpay_payment_id, razorpay_order_id, razorpay_signature → billing_token.""" try: gate = _require_pipeline_user_when_sync() if gate: return gate if not _razorpay_enabled(): return jsonify({"success": False, "error": "Razorpay is not configured"}), 503 import razorpay from razorpay.errors import SignatureVerificationError data = request.get_json(silent=True) or {} payment_id = (data.get("razorpay_payment_id") or "").strip() order_id = (data.get("razorpay_order_id") or "").strip() signature = (data.get("razorpay_signature") or "").strip() if not payment_id or not order_id or not signature: return jsonify({"success": False, "error": "Missing payment fields"}), 400 client = razorpay.Client(auth=(Config.RAZORPAY_KEY_ID, Config.RAZORPAY_KEY_SECRET)) try: client.utility.verify_payment_signature( { "razorpay_order_id": order_id, "razorpay_payment_id": payment_id, "razorpay_signature": signature, } ) except SignatureVerificationError: return jsonify({"success": False, "error": "Invalid payment signature"}), 400 tok = mint_billing_token(source="razorpay") return jsonify({"success": True, "data": {"billing_token": tok}}) except Exception as e: logger.error(traceback.format_exc()) return jsonify({"success": False, "error": str(e)}), 500