Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/app/services/payment_service.py

677 lines
25 KiB
Python
Executable file

from __future__ import annotations
import json
import logging
from dataclasses import dataclass
from typing import Any, Dict, Optional
import httpx
from sqlalchemy.orm import Session
from app.core.responses import ApiError
from adapters.db.models.payment_gateway import PaymentGateway
from adapters.db.models.wallet import WalletTransaction
from app.services.parsian_gateway import (
WALLET_ORDER_BASE,
build_startpay_url,
callback_paid,
confirm_payment,
decode_order_id,
is_test_token,
make_order_id,
parse_amount,
resolve_login_account,
resolve_startpay_base,
sale_error_message,
sale_payment_request,
)
from app.services.wallet_service import confirm_top_up
logger = logging.getLogger(__name__)
_BITPAY_ERROR_MESSAGES: Dict[int, str] = {
-1: "کلید API بیت‌پی با نوع درگاه سازگار نیست",
-2: "مبلغ نامعتبر است یا کمتر از حداقل مجاز بیت‌پی است",
-3: "آدرس بازگشت (callback) نامعتبر است",
-4: "درگاه بیت‌پی فعال نیست یا یافت نشد",
-5: "شناسه فاکتور تکراری است",
}
# API بیت‌پی مبلغ را به ریال می‌گیرد. حداقل ۵۰٬۰۰۰ ریال (۵٬۰۰۰ تومان).
# amount_unit=toman فقط برای درگاه‌هایی است که صریحاً تومان تنظیم شده‌اند.
_BITPAY_WIRE_MIN = {"toman": 5000, "rial": 50_000}
_BITPAY_WIRE_MAX = {"toman": 50_000_000, "rial": 500_000_000}
def _bitpay_amount_unit(cfg: Dict[str, Any]) -> str:
"""واحد ارسال مبلغ به بیت‌پی: rial (پیش‌فرض، مطابق API) یا toman."""
raw = str(cfg.get("amount_unit") or cfg.get("currency") or "rial").strip().lower()
if raw in ("t", "toman", "irt", "tmn"):
return "toman"
return "rial"
def _bitpay_wire_amount(internal_rial_amount: float, cfg: Dict[str, Any]) -> int:
"""تبدیل مبلغ داخلی (ریال حسابیکس) به واحد ارسالی بیت‌پی."""
amt = float(internal_rial_amount)
if _bitpay_amount_unit(cfg) == "toman":
return int(round(amt / 10.0))
return int(round(amt))
@dataclass
class InitiateResult:
payment_url: str
external_ref: str # Authority/Token/etc.
def _load_config(gw: PaymentGateway) -> Dict[str, Any]:
try:
return json.loads(gw.config_json or "{}")
except Exception:
return {}
def _get_gateway_or_error(db: Session, gateway_id: int) -> PaymentGateway:
gw = db.query(PaymentGateway).filter(PaymentGateway.id == int(gateway_id)).first()
if not gw:
raise ApiError("GATEWAY_NOT_FOUND", "درگاه پرداخت یافت نشد", http_status=404)
if not gw.is_active:
raise ApiError("GATEWAY_DISABLED", "درگاه پرداخت غیرفعال است", http_status=400)
return gw
def initiate_payment(db: Session, business_id: int, tx_id: int, amount: float, gateway_id: int) -> InitiateResult:
gw = _get_gateway_or_error(db, gateway_id)
cfg = _load_config(gw)
provider = gw.provider.lower().strip()
if provider == "zarinpal":
return _initiate_zarinpal(db, gw, cfg, business_id, tx_id, amount)
elif provider == "parsian":
return _initiate_parsian(db, gw, cfg, business_id, tx_id, amount)
elif provider == "bitpay":
return _initiate_bitpay(db, gw, cfg, business_id, tx_id, amount)
else:
raise ApiError("UNSUPPORTED_PROVIDER", f"درگاه '{gw.provider}' پشتیبانی نمی‌شود", http_status=400)
def verify_payment_callback(db: Session, provider: str, params: Dict[str, Any]) -> Dict[str, Any]:
"""
Verify callback by provider; returns standardized dict with:
- transaction_id: int
- success: bool
- external_ref: str | None
- fee_amount: float | None
"""
provider_l = (provider or "").lower().strip()
if provider_l == "zarinpal":
return _verify_zarinpal(db, params)
elif provider_l == "parsian":
return _verify_parsian(db, params)
elif provider_l == "bitpay":
return _verify_bitpay(db, params)
else:
raise ApiError("UNSUPPORTED_PROVIDER", f"درگاه '{provider}' پشتیبانی نمی‌شود", http_status=400)
# --------------------------
# ZARINPAL
# --------------------------
def _initiate_zarinpal(db: Session, gw: PaymentGateway, cfg: Dict[str, Any], business_id: int, tx_id: int, amount: float) -> InitiateResult:
"""
Minimal integration:
- expects cfg fields: merchant_id, callback_url, api_base(optional), startpay_base(optional), description(optional), currency ('IRR' default)
- amount must be in Rials for classic REST
"""
merchant_id = str(cfg.get("merchant_id") or "").strip()
callback_url = str(cfg.get("callback_url") or "").strip()
if not merchant_id or not callback_url:
raise ApiError("INVALID_CONFIG", "merchant_id و callback_url الزامی هستند", http_status=400)
description = str(cfg.get("description") or "Wallet top-up")
# Prefer v4 endpoint; fallback to legacy WebGate if needed
v4_domain = "https://sandbox.zarinpal.com" if gw.is_sandbox else "https://api.zarinpal.com"
api_v4_url = str(cfg.get("api_v4_url") or f"{v4_domain}/pg/v4/payment/request.json")
legacy_base = str(cfg.get("api_base") or ("https://sandbox.zarinpal.com/pg/rest/WebGate" if gw.is_sandbox else "https://www.zarinpal.com/pg/rest/WebGate"))
legacy_url = f"{legacy_base}/PaymentRequest.json"
# StartPay host per Zarinpal docs: sandbox vs payment
startpay_base = str(cfg.get("startpay_base") or ("https://sandbox.zarinpal.com/pg/StartPay" if gw.is_sandbox else "https://payment.zarinpal.com/pg/StartPay"))
# append tx_id to callback
cb_url = callback_url
try:
from urllib.parse import urlencode, urlparse, parse_qsl, urlunparse
u = urlparse(callback_url)
q = dict(parse_qsl(u.query))
q["tx_id"] = str(tx_id)
cb_url = urlunparse((u.scheme, u.netloc, u.path, u.params, urlencode(q), u.fragment))
except Exception:
cb_url = f"{callback_url}{'&' if '?' in callback_url else '?'}tx_id={tx_id}"
# Build payloads
req_payload_v4 = {
"merchant_id": merchant_id,
"amount": int(round(float(amount))),
"callback_url": cb_url,
"description": description,
}
req_payload_legacy = {
"MerchantID": merchant_id,
"Amount": int(round(float(amount))),
"Description": description,
"CallbackURL": cb_url,
}
authority: Optional[str] = None
try:
with httpx.Client(timeout=10.0) as client:
# Try v4 first
try:
resp = client.post(api_v4_url, json=req_payload_v4, headers={"Accept": "application/json", "Content-Type": "application/json"})
data = resp.json() if "application/json" in (resp.headers.get("content-type","")) else {}
d_data = data.get("data") if isinstance(data, dict) else None
code = (d_data or {}).get("code") if isinstance(d_data, dict) else None
auth_v4 = (d_data or {}).get("authority") if isinstance(d_data, dict) else None
if int(code or -1) == 100 and auth_v4:
authority = str(auth_v4)
except Exception:
authority = None
# Fallback to legacy
if not authority:
resp = client.post(legacy_url, json=req_payload_legacy, headers={"Accept": "application/json", "Content-Type": "application/json"})
data = resp.json() if "application/json" in (resp.headers.get("content-type","")) else {}
if int(data.get("Status") or -1) == 100 and data.get("Authority"):
authority = str(data["Authority"])
except Exception:
# Fallback: in dev, generate a pseudo authority to continue flow
authority = authority or f"TEST-AUTH-{tx_id}"
if not authority:
raise ApiError("GATEWAY_INIT_FAILED", "امکان ایجاد تراکنش در زرین‌پال نیست", http_status=502)
payment_url = f"{startpay_base}/{authority}"
# persist ref/url on tx.extra_info
_tx = db.query(WalletTransaction).filter(WalletTransaction.id == int(tx_id)).first()
if _tx:
extra = {}
try:
extra = json.loads(_tx.extra_info or "{}")
except Exception:
extra = {}
extra.update({
"gateway_id": gw.id,
"provider": "zarinpal",
"authority": authority,
"payment_url": payment_url,
})
_tx.external_ref = authority
_tx.extra_info = json.dumps(extra, ensure_ascii=False)
db.flush()
return InitiateResult(payment_url=payment_url, external_ref=authority)
def _verify_zarinpal(db: Session, params: Dict[str, Any]) -> Dict[str, Any]:
# Params expected: Authority, Status, (optionally tx_id)
authority = str(params.get("Authority") or params.get("authority") or "").strip()
status = str(params.get("Status") or params.get("status") or "").lower()
tx_id = int(params.get("tx_id") or 0)
is_ok = status in ("ok", "ok.", "success", "succeeded")
fee_amount = None
ref_id = None
success = False
if tx_id > 0 and is_ok:
# Load tx and gateway to verify via v4 endpoint
tx = db.query(WalletTransaction).filter(WalletTransaction.id == int(tx_id)).first()
gateway_id = None
try:
extra = json.loads(tx.extra_info or "{}") if tx and tx.extra_info else {}
gateway_id = extra.get("gateway_id")
except Exception:
gateway_id = None
gw = db.query(PaymentGateway).filter(PaymentGateway.id == int(gateway_id)).first() if gateway_id else None
if tx and gw:
cfg = _load_config(gw)
merchant_id = str(cfg.get("merchant_id") or "").strip()
if merchant_id:
v4_domain = "https://sandbox.zarinpal.com" if gw.is_sandbox else "https://payment.zarinpal.com"
verify_url = f"{v4_domain}/pg/v4/payment/verify.json"
payload = {
"merchant_id": merchant_id,
"amount": int(round(float(tx.amount or 0))),
"authority": authority,
}
try:
with httpx.Client(timeout=10.0) as client:
resp = client.post(verify_url, json=payload, headers={"Accept": "application/json", "Content-Type": "application/json"})
data = resp.json() if "application/json" in (resp.headers.get("content-type","")) else {}
d_data = data.get("data") if isinstance(data, dict) else None
code = (d_data or {}).get("code") if isinstance(d_data, dict) else None
ref_id = (d_data or {}).get("ref_id")
fee_amount = (d_data or {}).get("fee")
success = int(code or -1) in (100, 101)
except Exception:
success = False
# Confirm or fail based on verify (or status fallback)
user_id = None
if tx_id > 0:
# استخراج user_id از extra_info تراکنش
if tx:
try:
extra = json.loads(tx.extra_info or "{}") if tx.extra_info else {}
user_id = extra.get("created_by_user_id")
except Exception:
pass
# به‌روزرسانی fee_amount در صورت دریافت از درگاه
if fee_amount is not None and tx:
from decimal import Decimal
tx.fee_amount = Decimal(str(fee_amount))
db.flush()
confirm_top_up(db, tx_id, success=(success or is_ok), external_ref=authority or None, user_id=user_id)
return {"transaction_id": tx_id, "success": (success or is_ok), "external_ref": authority, "fee_amount": fee_amount, "ref_id": ref_id}
# --------------------------
# PARSIAN
# --------------------------
def _initiate_parsian(db: Session, gw: PaymentGateway, cfg: Dict[str, Any], business_id: int, tx_id: int, amount: float) -> InitiateResult:
"""Sale SOAP رسمی پارسیان: LoginAccount=PIN، مبلغ به ریال."""
login_account = resolve_login_account(cfg)
callback_url = str(cfg.get("callback_url") or "").strip()
if not login_account or not callback_url:
raise ApiError("INVALID_CONFIG", "شناسه پذیرنده (PIN) و callback_url الزامی هستند", http_status=400)
cb_url = callback_url
try:
from urllib.parse import urlencode, urlparse, parse_qsl, urlunparse
u = urlparse(callback_url)
q = dict(parse_qsl(u.query))
q["tx_id"] = str(tx_id)
cb_url = urlunparse((u.scheme, u.netloc, u.path, u.params, urlencode(q), u.fragment))
except Exception:
cb_url = f"{callback_url}{'&' if '?' in callback_url else '?'}tx_id={tx_id}"
order_id = make_order_id("wallet", int(tx_id))
wire_amount = parse_amount(amount)
token: Optional[str] = None
sale = sale_payment_request(
cfg,
is_sandbox=bool(gw.is_sandbox),
order_id=order_id,
amount=wire_amount,
callback_url=cb_url,
)
if sale.ok and sale.token:
token = str(sale.token)
elif gw.is_sandbox:
logger.warning("parsian_wallet_sandbox_fallback tx_id=%s status=%s", tx_id, sale.status)
token = f"TEST-TOKEN-{tx_id}"
else:
raise ApiError("GATEWAY_INIT_FAILED", sale_error_message(sale), http_status=502)
startpay_base = resolve_startpay_base(cfg, is_sandbox=bool(gw.is_sandbox))
payment_url = build_startpay_url(startpay_base, token)
_tx = db.query(WalletTransaction).filter(WalletTransaction.id == int(tx_id)).first()
if _tx:
extra = {}
try:
extra = json.loads(_tx.extra_info or "{}")
except Exception:
extra = {}
extra.update({
"gateway_id": gw.id,
"provider": "parsian",
"token": token,
"payment_url": payment_url,
"parsian_order_id": order_id,
})
_tx.external_ref = token
_tx.extra_info = json.dumps(extra, ensure_ascii=False)
db.flush()
return InitiateResult(payment_url=payment_url, external_ref=token)
def _verify_parsian(db: Session, params: Dict[str, Any]) -> Dict[str, Any]:
"""تأیید پرداخت پارسیان با همان ConfirmPayment نمونه رسمی درگاه."""
token = str(params.get("Token") or params.get("token") or "").strip()
cb_status = params.get("status") if params.get("status") is not None else params.get("Status")
try:
tx_id = int(params.get("tx_id") or 0)
except (TypeError, ValueError):
tx_id = parse_amount(params.get("tx_id"))
if tx_id <= 0:
raw_oid = parse_amount(params.get("OrderId") or 0)
scope, decoded = decode_order_id(raw_oid)
if scope == "wallet" and decoded > 0:
tx_id = decoded
elif raw_oid > 0 and raw_oid < WALLET_ORDER_BASE:
tx_id = raw_oid
success = False
fee_amount = None
user_id = None
ref_id = params.get("RRN") or params.get("rrn")
card_num = None
tx = None
extra: Dict[str, Any] = {}
if tx_id > 0:
tx = db.query(WalletTransaction).filter(WalletTransaction.id == int(tx_id)).first()
if tx:
try:
extra = json.loads(tx.extra_info or "{}") if tx.extra_info else {}
user_id = extra.get("created_by_user_id")
except Exception:
extra = {}
if not token:
token = str((extra.get("token") if extra else None) or (tx.external_ref if tx else "") or "").strip()
gateway_id = extra.get("gateway_id") if extra else None
gw = db.query(PaymentGateway).filter(PaymentGateway.id == int(gateway_id)).first() if gateway_id else None
paid_at_bank = callback_paid(cb_status)
callback_rrn = parse_amount(params.get("RRN") or params.get("rrn"))
if gw and paid_at_bank and callback_rrn > 0 and token:
if gw.is_sandbox and is_test_token(token):
success = True
else:
cfg = _load_config(gw)
confirm = confirm_payment(
cfg,
is_sandbox=bool(gw.is_sandbox),
token=token,
order_id=parse_amount(params.get("OrderId") or extra.get("parsian_order_id") or make_order_id("wallet", tx_id)),
amount=parse_amount(params.get("Amount")) or parse_amount(tx.amount if tx else 0),
)
success = bool(confirm.ok)
if confirm.rrn:
ref_id = confirm.rrn
card_num = confirm.card_number
if extra is not None:
extra["parsian_confirm_status"] = confirm.status
if confirm.rrn:
extra["parsian_rrn"] = confirm.rrn
if confirm.card_number:
extra["parsian_card"] = confirm.card_number
if tx:
tx.extra_info = json.dumps(extra, ensure_ascii=False)
elif paid_at_bank and gw is None and is_test_token(token):
success = True
confirm_top_up(db, tx_id, success=success, external_ref=(str(ref_id) if ref_id else token) or None, user_id=user_id)
return {
"transaction_id": tx_id,
"success": success,
"external_ref": (str(ref_id) if ref_id else token) or None,
"fee_amount": fee_amount,
"ref_id": ref_id,
"card_num": card_num,
}
# --------------------------
# BITPAY
# --------------------------
def _initiate_bitpay(db: Session, gw: PaymentGateway, cfg: Dict[str, Any], business_id: int, tx_id: int, amount: float) -> InitiateResult:
"""
BitPay integration:
- expects cfg fields: api (52 characters), callback_url (redirect)
- amount_unit (اختیاری): rial (پیش‌فرض، مطابق API بیت‌پی) | toman — مبلغ داخلی همیشه ریال است
- returns id_get which is used to build payment URL
"""
api_key = str(cfg.get("api") or "").strip()
callback_url = str(cfg.get("callback_url") or "").strip()
if not api_key or not callback_url:
raise ApiError("INVALID_CONFIG", "api و callback_url الزامی هستند", http_status=400)
if len(api_key) != 52:
raise ApiError("INVALID_CONFIG", "API key باید 52 کاراکتر باشد", http_status=400)
unit = _bitpay_amount_unit(cfg)
wire_amount = _bitpay_wire_amount(amount, cfg)
if wire_amount < _BITPAY_WIRE_MIN[unit]:
raise ApiError(
"INVALID_AMOUNT",
"حداقل مبلغ پرداخت ۵۰٬۰۰۰ ریال (۵٬۰۰۰ تومان) است",
http_status=400,
)
if wire_amount > _BITPAY_WIRE_MAX[unit]:
raise ApiError(
"AMOUNT_TOO_LARGE",
"حداکثر مبلغ هر تراکنش در بیت‌پی ۵۰۰٬۰۰۰٬۰۰۰ ریال (۵۰٬۰۰۰٬۰۰۰ تومان) است",
http_status=400,
)
# تعیین URL بر اساس sandbox
base_url = "https://bitpay.ir/payment-test" if gw.is_sandbox else "https://bitpay.ir/payment"
gateway_send_url = f"{base_url}/gateway-send"
# اضافه کردن tx_id و source به callback
from urllib.parse import urlencode, urlparse, parse_qsl, urlunparse
# استخراج source از extra_info تراکنش
source = "app" # پیش‌فرض
_tx_for_source = db.query(WalletTransaction).filter(WalletTransaction.id == int(tx_id)).first()
if _tx_for_source and _tx_for_source.extra_info:
try:
extra_source = json.loads(_tx_for_source.extra_info)
source = extra_source.get("source", "app")
except Exception:
pass
try:
u = urlparse(callback_url)
q = dict(parse_qsl(u.query))
q["tx_id"] = str(tx_id)
q["source"] = source
cb_url = urlunparse((u.scheme, u.netloc, u.path, u.params, urlencode(q), u.fragment))
except Exception:
cb_url = f"{callback_url}{'&' if '?' in callback_url else '?'}tx_id={tx_id}&source={source}"
# بارگذاری اطلاعات کاربر از تراکنش
_tx = db.query(WalletTransaction).filter(WalletTransaction.id == int(tx_id)).first()
user_name = None
user_email = None
user_description = "افزایش اعتبار کیف پول"
if _tx and _tx.extra_info:
try:
extra = json.loads(_tx.extra_info)
user_name = extra.get("user_name") or extra.get("user_mobile")
user_email = extra.get("user_email")
if _tx.description:
user_description = _tx.description
except Exception:
pass
# آماده‌سازی داده‌های ارسالی (form data)
data = {
"api": api_key,
"redirect": cb_url,
"amount": wire_amount,
"factorId": str(tx_id), # شماره فاکتور
}
# فیلدهای اختیاری - استفاده از اطلاعات واقعی کاربر
if user_name:
data["name"] = str(user_name)
if user_email:
data["email"] = str(user_email)
# توضیحات از تراکنش
data["description"] = str(user_description)
id_get: Optional[str] = None
response_text = ""
try:
with httpx.Client(timeout=10.0) as client:
# ارسال به صورت form data (نه JSON)
resp = client.post(gateway_send_url, data=data)
# پاسخ یک عدد است (id_get) که باید به string تبدیل شود
response_text = resp.text.strip()
# بررسی اینکه پاسخ یک عدد معتبر است
try:
id_get_int = int(response_text)
if id_get_int > 0:
id_get = str(id_get_int)
except ValueError:
# اگر عدد نبود، ممکن است خطا باشد
id_get = None
except Exception as e:
logger.warning("bitpay_gateway_send_request_failed tx_id=%s amount=%s error=%s", tx_id, amount, e)
id_get = None
if not id_get:
error_msg = "امکان ایجاد تراکنش در بیت‌پی نیست"
try:
err_code = int(response_text)
if err_code < 0:
error_msg = _BITPAY_ERROR_MESSAGES.get(err_code, f"خطای بیت‌پی (کد {err_code})")
logger.warning(
"bitpay_gateway_send_rejected tx_id=%s amount=%s code=%s",
tx_id,
amount,
err_code,
)
except ValueError:
if response_text:
logger.warning(
"bitpay_gateway_send_unexpected_response tx_id=%s amount=%s response=%s",
tx_id,
amount,
response_text[:200],
)
raise ApiError("GATEWAY_INIT_FAILED", error_msg, http_status=502)
# ساخت لینک پرداخت
payment_url = f"{base_url}/gateway-{id_get}-get"
# ذخیره اطلاعات در تراکنش
_tx = db.query(WalletTransaction).filter(WalletTransaction.id == int(tx_id)).first()
if _tx:
extra = {}
try:
extra = json.loads(_tx.extra_info or "{}")
except Exception:
extra = {}
extra.update({
"gateway_id": gw.id,
"provider": "bitpay",
"id_get": id_get,
"payment_url": payment_url,
"bitpay_amount_unit": unit,
"bitpay_wire_amount": wire_amount,
})
_tx.external_ref = id_get
_tx.extra_info = json.dumps(extra, ensure_ascii=False)
db.flush()
return InitiateResult(payment_url=payment_url, external_ref=id_get)
def _verify_bitpay(db: Session, params: Dict[str, Any]) -> Dict[str, Any]:
"""
Verify BitPay payment:
- Params expected: trans_id (id_trans), id_get (get_id), tx_id (optional)
- Returns standardized dict with transaction_id, success, external_ref, fee_amount
"""
trans_id = str(params.get("trans_id") or params.get("id_trans") or "").strip()
id_get = str(params.get("id_get") or params.get("get_id") or "").strip()
tx_id = int(params.get("tx_id") or 0)
if not trans_id or not id_get:
# اگر پارامترهای لازم وجود نداشت، تراکنش ناموفق است
if tx_id > 0:
user_id = None
tx = db.query(WalletTransaction).filter(WalletTransaction.id == int(tx_id)).first()
if tx:
try:
extra = json.loads(tx.extra_info or "{}") if tx.extra_info else {}
user_id = extra.get("created_by_user_id")
except Exception:
pass
confirm_top_up(db, tx_id, success=False, external_ref=None, user_id=user_id)
return {"transaction_id": tx_id, "success": False, "external_ref": None, "fee_amount": None}
# بارگذاری تراکنش و درگاه
tx = db.query(WalletTransaction).filter(WalletTransaction.id == int(tx_id)).first() if tx_id > 0 else None
gateway_id = None
if tx and tx.extra_info:
try:
extra = json.loads(tx.extra_info or "{}")
gateway_id = extra.get("gateway_id")
except Exception:
pass
gw = db.query(PaymentGateway).filter(PaymentGateway.id == int(gateway_id)).first() if gateway_id else None
if not gw:
# اگر درگاه پیدا نشد، تراکنش ناموفق است
if tx_id > 0:
user_id = None
if tx:
try:
extra = json.loads(tx.extra_info or "{}") if tx.extra_info else {}
user_id = extra.get("created_by_user_id")
except Exception:
pass
confirm_top_up(db, tx_id, success=False, external_ref=id_get, user_id=user_id)
return {"transaction_id": tx_id, "success": False, "external_ref": id_get, "fee_amount": None}
cfg = _load_config(gw)
api_key = str(cfg.get("api") or "").strip()
if not api_key:
# اگر API key وجود نداشت، تراکنش ناموفق است
if tx_id > 0:
user_id = None
if tx:
try:
extra = json.loads(tx.extra_info or "{}") if tx.extra_info else {}
user_id = extra.get("created_by_user_id")
except Exception:
pass
confirm_top_up(db, tx_id, success=False, external_ref=id_get, user_id=user_id)
return {"transaction_id": tx_id, "success": False, "external_ref": id_get, "fee_amount": None}
# تعیین URL بر اساس sandbox
base_url = "https://bitpay.ir/payment-test" if gw.is_sandbox else "https://bitpay.ir/payment"
verify_url = f"{base_url}/gateway-result-second"
# آماده‌سازی داده‌های ارسالی (form data با json=1)
data = {
"api": api_key,
"trans_id": trans_id,
"id_get": id_get,
"json": "1", # برای دریافت پاسخ JSON
}
success = False
fee_amount = None
amount = None
card_num = None
factor_id = None
try:
with httpx.Client(timeout=10.0) as client:
# ارسال به صورت form data
resp = client.post(verify_url, data=data)
# پاسخ JSON است
if "application/json" in (resp.headers.get("content-type", "").lower()):
result = resp.json()
else:
# اگر JSON نبود، سعی می‌کنیم parse کنیم
try:
result = resp.json()
except Exception:
result = {}
status = result.get("status")
# بررسی status
# 1 = موفق
# -1 تا -5 = خطا
# 11 = قبلاً verify شده
if status == 1:
success = True
amount = result.get("amount")
card_num = result.get("cardNum")
factor_id = result.get("factorId")
elif status == 11:
# تراکنش قبلاً verify شده است
success = True # قبلاً موفق بوده
else:
# خطا (status < 0)
success = False
except Exception:
success = False
# تایید یا رد تراکنش
user_id = None
if tx_id > 0 and tx:
try:
extra = json.loads(tx.extra_info or "{}") if tx.extra_info else {}
user_id = extra.get("created_by_user_id")
except Exception:
pass
# به‌روزرسانی fee_amount در صورت وجود (BitPay معمولاً fee را برنمی‌گرداند)
confirm_top_up(db, tx_id, success=success, external_ref=id_get, user_id=user_id)
return {
"transaction_id": tx_id,
"success": success,
"external_ref": id_get,
"fee_amount": fee_amount,
"amount": amount,
"card_num": card_num,
"factor_id": factor_id,
}