payments: Platega + YooKassa integration, opt-in via PAYMENTS_ENABLED
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
c9955b2774
commit
5b30d1f148
6 changed files with 326 additions and 8 deletions
23
.env.example
23
.env.example
|
|
@ -31,3 +31,26 @@ XRAY_SHORT_ID_XHTTP=
|
||||||
|
|
||||||
# Public hostname clients connect to for the local node (A record -> this server)
|
# Public hostname clients connect to for the local node (A record -> this server)
|
||||||
DE1_ADDRESS=de1.example.com
|
DE1_ADDRESS=de1.example.com
|
||||||
|
|
||||||
|
# Payments — off by default, bot keeps handing out free subscriptions on button press.
|
||||||
|
# Flip to true only once at least one provider below is configured and its webhook is live.
|
||||||
|
PAYMENTS_ENABLED=false
|
||||||
|
|
||||||
|
# Prices in RUB per plan (whole numbers). Only used when PAYMENTS_ENABLED=true.
|
||||||
|
PRICE_7D=150
|
||||||
|
PRICE_1M=399
|
||||||
|
PRICE_3M=999
|
||||||
|
PRICE_6M=1799
|
||||||
|
PRICE_1Y=2999
|
||||||
|
|
||||||
|
# ЮKassa — https://yookassa.ru, shop id + secret key from your account settings.
|
||||||
|
# Webhook to add in their dashboard: https://<PANEL_DOMAIN>/payments/webhook/yookassa
|
||||||
|
YOOKASSA_ENABLED=false
|
||||||
|
YOOKASSA_SHOP_ID=
|
||||||
|
YOOKASSA_SECRET_KEY=
|
||||||
|
|
||||||
|
# Platega — https://platega.io, merchant id + secret from your manager.
|
||||||
|
# Webhook to add in their dashboard: https://<PANEL_DOMAIN>/payments/webhook/platega
|
||||||
|
PLATEGA_ENABLED=false
|
||||||
|
PLATEGA_MERCHANT_ID=
|
||||||
|
PLATEGA_SECRET=
|
||||||
|
|
|
||||||
77
api.py
77
api.py
|
|
@ -9,10 +9,11 @@ from fastapi.responses import HTMLResponse, PlainTextResponse, FileResponse
|
||||||
import db
|
import db
|
||||||
import links
|
import links
|
||||||
import nodeprov
|
import nodeprov
|
||||||
|
import payments
|
||||||
import xray_manager
|
import xray_manager
|
||||||
from config import (
|
from config import (
|
||||||
PLANS, PLANS_BY_CODE, SITE_DOMAIN, SUB_DOMAIN, PANEL_DOMAIN,
|
PLANS, PLANS_BY_CODE, SITE_DOMAIN, SUB_DOMAIN, PANEL_DOMAIN,
|
||||||
ADMIN_PANEL_PASSWORD, BOT_USERNAME,
|
ADMIN_PANEL_PASSWORD, BOT_USERNAME, BOT_TOKEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
db.init_db()
|
db.init_db()
|
||||||
|
|
@ -251,6 +252,80 @@ def install_script(token: str):
|
||||||
return nodeprov.render_install_script(node)
|
return nodeprov.render_install_script(node)
|
||||||
|
|
||||||
|
|
||||||
|
def _tg_send_message(tg_id: int, text: str):
|
||||||
|
import urllib.request
|
||||||
|
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
|
||||||
|
data = json.dumps({"chat_id": tg_id, "text": text, "parse_mode": "HTML"}).encode()
|
||||||
|
req = urllib.request.Request(url, data=data, method="POST", headers={"Content-Type": "application/json"})
|
||||||
|
try:
|
||||||
|
urllib.request.urlopen(req, timeout=10)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _grant_paid_subscription(payment_id: str):
|
||||||
|
payment = db.mark_payment_paid(payment_id)
|
||||||
|
if not payment:
|
||||||
|
return
|
||||||
|
plan = PLANS_BY_CODE.get(payment["plan"])
|
||||||
|
node = db.get_node(payment["node"])
|
||||||
|
if not plan or not node:
|
||||||
|
return
|
||||||
|
sub = db.create_subscription(payment["tg_id"], payment["node"], plan["days"], payment["plan"], source="payment")
|
||||||
|
xray_manager.add_client_to_node(node, sub["uuid"], email=sub["uuid"])
|
||||||
|
user = db.get_or_create_user(payment["tg_id"], None)
|
||||||
|
_tg_send_message(
|
||||||
|
payment["tg_id"],
|
||||||
|
f"<b>Оплата получена</b>\n\n"
|
||||||
|
f"Сервер: {node['label']}\n"
|
||||||
|
f"Срок: {plan['label']} — до {sub['expires_at'][:10]}\n\n"
|
||||||
|
f"Ссылка-подписка:\nhttps://{SUB_DOMAIN}/sub/{user['token']}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/payments/webhook/yookassa")
|
||||||
|
async def yookassa_webhook(request: Request):
|
||||||
|
body = await request.json()
|
||||||
|
if not payments.verify_yookassa_notification(body):
|
||||||
|
raise HTTPException(400, "unexpected event")
|
||||||
|
obj = body.get("object", {})
|
||||||
|
if obj.get("status") != "succeeded":
|
||||||
|
return {"ok": True}
|
||||||
|
payment_id = (obj.get("metadata") or {}).get("payment_id")
|
||||||
|
if not payment_id:
|
||||||
|
raise HTTPException(400, "missing payment_id")
|
||||||
|
_grant_paid_subscription(payment_id)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/payments/webhook/platega")
|
||||||
|
async def platega_webhook(request: Request):
|
||||||
|
raw = await request.body()
|
||||||
|
signature = request.headers.get("x-signature") or request.headers.get("signature") or ""
|
||||||
|
if not payments.verify_platega_signature(raw, signature):
|
||||||
|
raise HTTPException(401, "bad signature")
|
||||||
|
body = json.loads(raw)
|
||||||
|
status = (body.get("status") or "").lower()
|
||||||
|
payment_id = body.get("id") or body.get("paymentId")
|
||||||
|
if status not in ("succeeded", "success", "paid") or not payment_id:
|
||||||
|
return {"ok": True}
|
||||||
|
_grant_paid_subscription(payment_id)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/pay/done", response_class=HTMLResponse)
|
||||||
|
def pay_done():
|
||||||
|
return (
|
||||||
|
"<!doctype html><html lang='ru'><head><meta charset='utf-8'>"
|
||||||
|
"<meta name='viewport' content='width=device-width, initial-scale=1'>"
|
||||||
|
"<title>Оплата</title></head>"
|
||||||
|
"<body style='background:#0a0b0f;color:#eceef2;font-family:sans-serif;"
|
||||||
|
"display:flex;align-items:center;justify-content:center;min-height:100vh;text-align:center'>"
|
||||||
|
"<div><h2>Спасибо!</h2><p>Возвращайся в Telegram — подписка придёт туда автоматически "
|
||||||
|
"в течение минуты после подтверждения оплаты.</p></div></body></html>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/nodes/register/{token}")
|
@app.post("/nodes/register/{token}")
|
||||||
async def register_node(token: str, request: Request):
|
async def register_node(token: str, request: Request):
|
||||||
node = db.get_node_by_token(token)
|
node = db.get_node_by_token(token)
|
||||||
|
|
|
||||||
50
bot.py
50
bot.py
|
|
@ -9,8 +9,9 @@ from aiogram.enums import ParseMode
|
||||||
|
|
||||||
import db
|
import db
|
||||||
import links
|
import links
|
||||||
|
import payments
|
||||||
import xray_manager
|
import xray_manager
|
||||||
from config import BOT_TOKEN, ADMIN_IDS, PLANS, PLANS_BY_CODE, SUB_DOMAIN, SITE_DOMAIN
|
from config import BOT_TOKEN, ADMIN_IDS, PLANS, PLANS_BY_CODE, SUB_DOMAIN, SITE_DOMAIN, PAYMENTS_ENABLED
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
log = logging.getLogger("mbs-bot")
|
log = logging.getLogger("mbs-bot")
|
||||||
|
|
@ -49,7 +50,8 @@ def nodes_kb(prefix: str) -> InlineKeyboardMarkup:
|
||||||
def plans_kb(prefix: str, node_code: str) -> InlineKeyboardMarkup:
|
def plans_kb(prefix: str, node_code: str) -> InlineKeyboardMarkup:
|
||||||
rows = []
|
rows = []
|
||||||
for p in PLANS:
|
for p in PLANS:
|
||||||
rows.append([InlineKeyboardButton(text=p["label"], callback_data=f"{prefix}:{node_code}:{p['code']}")])
|
label = f"{p['label']} — {p['price']} ₽" if PAYMENTS_ENABLED and p["price"] > 0 else p["label"]
|
||||||
|
rows.append([InlineKeyboardButton(text=label, callback_data=f"{prefix}:{node_code}:{p['code']}")])
|
||||||
rows.append([InlineKeyboardButton(text="Назад", callback_data="menu:get")])
|
rows.append([InlineKeyboardButton(text="Назад", callback_data="menu:get")])
|
||||||
return InlineKeyboardMarkup(inline_keyboard=rows)
|
return InlineKeyboardMarkup(inline_keyboard=rows)
|
||||||
|
|
||||||
|
|
@ -145,10 +147,27 @@ async def cb_node(cb: CallbackQuery):
|
||||||
await cb.answer()
|
await cb.answer()
|
||||||
|
|
||||||
|
|
||||||
|
def providers_kb(node_code: str, plan_code: str) -> InlineKeyboardMarkup:
|
||||||
|
rows = []
|
||||||
|
for p in payments.available_providers():
|
||||||
|
rows.append([InlineKeyboardButton(text=payments.PROVIDER_NAMES[p], callback_data=f"pay:{p}:{node_code}:{plan_code}")])
|
||||||
|
rows.append([InlineKeyboardButton(text="Назад", callback_data=f"node:{node_code}")])
|
||||||
|
return InlineKeyboardMarkup(inline_keyboard=rows)
|
||||||
|
|
||||||
|
|
||||||
@dp.callback_query(F.data.startswith("plan:"))
|
@dp.callback_query(F.data.startswith("plan:"))
|
||||||
async def cb_plan(cb: CallbackQuery):
|
async def cb_plan(cb: CallbackQuery):
|
||||||
_, node_code, plan_code = cb.data.split(":")
|
_, node_code, plan_code = cb.data.split(":")
|
||||||
plan = PLANS_BY_CODE[plan_code]
|
plan = PLANS_BY_CODE[plan_code]
|
||||||
|
db.get_or_create_user(cb.from_user.id, cb.from_user.username)
|
||||||
|
|
||||||
|
if PAYMENTS_ENABLED and plan["price"] > 0 and payments.available_providers():
|
||||||
|
await cb.message.edit_text(
|
||||||
|
f"<b>{plan['label']}</b> — {plan['price']} ₽\n\nВыбери способ оплаты:",
|
||||||
|
reply_markup=providers_kb(node_code, plan_code),
|
||||||
|
)
|
||||||
|
return await cb.answer()
|
||||||
|
|
||||||
user = db.get_or_create_user(cb.from_user.id, cb.from_user.username)
|
user = db.get_or_create_user(cb.from_user.id, cb.from_user.username)
|
||||||
sub = db.create_subscription(cb.from_user.id, node_code, plan["days"], plan_code, source="bot")
|
sub = db.create_subscription(cb.from_user.id, node_code, plan["days"], plan_code, source="bot")
|
||||||
node_row = db.get_node(node_code)
|
node_row = db.get_node(node_code)
|
||||||
|
|
@ -168,6 +187,33 @@ async def cb_plan(cb: CallbackQuery):
|
||||||
await cb.answer("Подписка выдана")
|
await cb.answer("Подписка выдана")
|
||||||
|
|
||||||
|
|
||||||
|
@dp.callback_query(F.data.startswith("pay:"))
|
||||||
|
async def cb_pay(cb: CallbackQuery):
|
||||||
|
_, provider, node_code, plan_code = cb.data.split(":")
|
||||||
|
plan = PLANS_BY_CODE[plan_code]
|
||||||
|
node_row = db.get_node(node_code)
|
||||||
|
payment_id = payments.new_payment_id()
|
||||||
|
db.create_payment(payment_id, cb.from_user.id, node_code, plan_code, provider, plan["price"])
|
||||||
|
try:
|
||||||
|
external_id, pay_url = payments.create_payment_link(
|
||||||
|
provider, payment_id, plan["price"], f"MBS Panel — {node_row['label']}, {plan['label']}",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
log.exception("payment creation failed")
|
||||||
|
db.mark_payment_failed(payment_id)
|
||||||
|
return await cb.answer("Не получилось создать платёж, попробуй позже", show_alert=True)
|
||||||
|
db.set_payment_external(payment_id, external_id, pay_url)
|
||||||
|
kb = InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="Оплатить", url=pay_url)],
|
||||||
|
[InlineKeyboardButton(text="Назад", callback_data=f"plan:{node_code}:{plan_code}")],
|
||||||
|
])
|
||||||
|
await cb.message.edit_text(
|
||||||
|
f"Счёт на {plan['price']} ₽ создан.\nПосле оплаты подписка выдастся автоматически.",
|
||||||
|
reply_markup=kb,
|
||||||
|
)
|
||||||
|
await cb.answer()
|
||||||
|
|
||||||
|
|
||||||
@dp.callback_query(F.data == "menu:mysub")
|
@dp.callback_query(F.data == "menu:mysub")
|
||||||
async def cb_mysub(cb: CallbackQuery):
|
async def cb_mysub(cb: CallbackQuery):
|
||||||
user = db.get_or_create_user(cb.from_user.id, cb.from_user.username)
|
user = db.get_or_create_user(cb.from_user.id, cb.from_user.username)
|
||||||
|
|
|
||||||
20
config.py
20
config.py
|
|
@ -98,10 +98,20 @@ DE1_TRANSPORTS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
PLANS = [
|
PLANS = [
|
||||||
{"code": "7d", "label": "7 дней", "days": 7},
|
{"code": "7d", "label": "7 дней", "days": 7, "price": int(env("PRICE_7D", "150"))},
|
||||||
{"code": "1m", "label": "1 месяц", "days": 30},
|
{"code": "1m", "label": "1 месяц", "days": 30, "price": int(env("PRICE_1M", "399"))},
|
||||||
{"code": "3m", "label": "3 месяца", "days": 90},
|
{"code": "3m", "label": "3 месяца", "days": 90, "price": int(env("PRICE_3M", "999"))},
|
||||||
{"code": "6m", "label": "6 месяцев", "days": 180},
|
{"code": "6m", "label": "6 месяцев", "days": 180, "price": int(env("PRICE_6M", "1799"))},
|
||||||
{"code": "1y", "label": "1 год", "days": 365},
|
{"code": "1y", "label": "1 год", "days": 365, "price": int(env("PRICE_1Y", "2999"))},
|
||||||
]
|
]
|
||||||
PLANS_BY_CODE = {p["code"]: p for p in PLANS}
|
PLANS_BY_CODE = {p["code"]: p for p in PLANS}
|
||||||
|
|
||||||
|
PAYMENTS_ENABLED = env("PAYMENTS_ENABLED", "false").lower() == "true"
|
||||||
|
|
||||||
|
YOOKASSA_ENABLED = env("YOOKASSA_ENABLED", "false").lower() == "true"
|
||||||
|
YOOKASSA_SHOP_ID = env("YOOKASSA_SHOP_ID", "")
|
||||||
|
YOOKASSA_SECRET_KEY = env("YOOKASSA_SECRET_KEY", "")
|
||||||
|
|
||||||
|
PLATEGA_ENABLED = env("PLATEGA_ENABLED", "false").lower() == "true"
|
||||||
|
PLATEGA_MERCHANT_ID = env("PLATEGA_MERCHANT_ID", "")
|
||||||
|
PLATEGA_SECRET = env("PLATEGA_SECRET", "")
|
||||||
|
|
|
||||||
68
db.py
68
db.py
|
|
@ -40,6 +40,20 @@ CREATE TABLE IF NOT EXISTS admin_sessions (
|
||||||
expires_at TEXT NOT NULL
|
expires_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS payments (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
tg_id INTEGER NOT NULL,
|
||||||
|
node TEXT NOT NULL,
|
||||||
|
plan TEXT NOT NULL,
|
||||||
|
provider TEXT NOT NULL,
|
||||||
|
external_id TEXT,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
pay_url TEXT,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
paid_at TEXT
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS nodes (
|
CREATE TABLE IF NOT EXISTS nodes (
|
||||||
code TEXT PRIMARY KEY,
|
code TEXT PRIMARY KEY,
|
||||||
label TEXT NOT NULL,
|
label TEXT NOT NULL,
|
||||||
|
|
@ -342,3 +356,57 @@ def stats():
|
||||||
"gifts_created": gifts_created,
|
"gifts_created": gifts_created,
|
||||||
"gifts_used": gifts_used,
|
"gifts_used": gifts_used,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_payment(payment_id: str, tg_id: int, node: str, plan: str, provider: str, amount: int):
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO payments (id, tg_id, node, plan, provider, amount, status, created_at) "
|
||||||
|
"VALUES (?,?,?,?,?,?, 'pending', ?)",
|
||||||
|
(payment_id, tg_id, node, plan, provider, amount, now_iso()),
|
||||||
|
)
|
||||||
|
return get_payment(payment_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_payment(payment_id: str):
|
||||||
|
with get_conn() as conn:
|
||||||
|
row = conn.execute("SELECT * FROM payments WHERE id=?", (payment_id,)).fetchone()
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def set_payment_external(payment_id: str, external_id: str, pay_url: str):
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE payments SET external_id=?, pay_url=? WHERE id=?",
|
||||||
|
(external_id, pay_url, payment_id),
|
||||||
|
)
|
||||||
|
return get_payment(payment_id)
|
||||||
|
|
||||||
|
|
||||||
|
def mark_payment_paid(payment_id: str):
|
||||||
|
with get_conn() as conn:
|
||||||
|
row = conn.execute("SELECT status FROM payments WHERE id=?", (payment_id,)).fetchone()
|
||||||
|
if not row or row["status"] == "paid":
|
||||||
|
return None
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE payments SET status='paid', paid_at=? WHERE id=?",
|
||||||
|
(now_iso(), payment_id),
|
||||||
|
)
|
||||||
|
return get_payment(payment_id)
|
||||||
|
|
||||||
|
|
||||||
|
def mark_payment_failed(payment_id: str):
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE payments SET status='failed' WHERE id=? AND status='pending'",
|
||||||
|
(payment_id,),
|
||||||
|
)
|
||||||
|
return get_payment(payment_id)
|
||||||
|
|
||||||
|
|
||||||
|
def list_payments(limit: int = 200):
|
||||||
|
with get_conn() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM payments ORDER BY created_at DESC LIMIT ?", (limit,)
|
||||||
|
).fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
|
||||||
96
payments.py
Normal file
96
payments.py
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
|
import secrets
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
from config import (
|
||||||
|
PANEL_DOMAIN,
|
||||||
|
YOOKASSA_ENABLED, YOOKASSA_SHOP_ID, YOOKASSA_SECRET_KEY,
|
||||||
|
PLATEGA_ENABLED, PLATEGA_MERCHANT_ID, PLATEGA_SECRET,
|
||||||
|
)
|
||||||
|
|
||||||
|
PROVIDER_NAMES = {"yookassa": "ЮKassa", "platega": "Platega"}
|
||||||
|
|
||||||
|
|
||||||
|
def available_providers() -> list[str]:
|
||||||
|
providers = []
|
||||||
|
if YOOKASSA_ENABLED:
|
||||||
|
providers.append("yookassa")
|
||||||
|
if PLATEGA_ENABLED:
|
||||||
|
providers.append("platega")
|
||||||
|
return providers
|
||||||
|
|
||||||
|
|
||||||
|
def new_payment_id() -> str:
|
||||||
|
return secrets.token_hex(16)
|
||||||
|
|
||||||
|
|
||||||
|
def _post_json(url: str, body: dict, headers: dict, timeout: int = 15) -> dict:
|
||||||
|
data = json.dumps(body).encode()
|
||||||
|
req = urllib.request.Request(url, data=data, method="POST", headers=headers)
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||||
|
return json.loads(resp.read().decode())
|
||||||
|
|
||||||
|
|
||||||
|
def create_yookassa_payment(payment_id: str, amount_rub: int, description: str) -> str:
|
||||||
|
auth = base64.b64encode(f"{YOOKASSA_SHOP_ID}:{YOOKASSA_SECRET_KEY}".encode()).decode()
|
||||||
|
data = _post_json(
|
||||||
|
"https://api.yookassa.ru/v3/payments",
|
||||||
|
{
|
||||||
|
"amount": {"value": f"{amount_rub}.00", "currency": "RUB"},
|
||||||
|
"confirmation": {"type": "redirect", "return_url": f"https://{PANEL_DOMAIN}/pay/done"},
|
||||||
|
"capture": True,
|
||||||
|
"description": description,
|
||||||
|
"metadata": {"payment_id": payment_id},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Basic {auth}",
|
||||||
|
"Idempotence-Key": payment_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
external_id = data["id"]
|
||||||
|
pay_url = data["confirmation"]["confirmation_url"]
|
||||||
|
return external_id, pay_url
|
||||||
|
|
||||||
|
|
||||||
|
def verify_yookassa_notification(body: dict) -> bool:
|
||||||
|
return body.get("event") == "payment.succeeded" and "object" in body
|
||||||
|
|
||||||
|
|
||||||
|
def create_platega_payment(payment_id: str, amount_rub: int, description: str) -> str:
|
||||||
|
data = _post_json(
|
||||||
|
"https://app.platega.io/transaction/process",
|
||||||
|
{
|
||||||
|
"paymentMethod": 2,
|
||||||
|
"id": payment_id,
|
||||||
|
"paymentDetails": {"amount": amount_rub, "currency": "RUB"},
|
||||||
|
"description": description,
|
||||||
|
"return": f"https://{PANEL_DOMAIN}/pay/done",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-MerchantId": PLATEGA_MERCHANT_ID,
|
||||||
|
"X-Secret": PLATEGA_SECRET,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
external_id = data.get("id") or data.get("transactionId")
|
||||||
|
pay_url = data.get("redirectUrl") or data.get("url")
|
||||||
|
return external_id, pay_url
|
||||||
|
|
||||||
|
|
||||||
|
def verify_platega_signature(raw_body: bytes, signature: str) -> bool:
|
||||||
|
if not signature:
|
||||||
|
return False
|
||||||
|
expected = hmac.new(PLATEGA_SECRET.encode(), raw_body, hashlib.sha256).hexdigest()
|
||||||
|
return hmac.compare_digest(expected, signature)
|
||||||
|
|
||||||
|
|
||||||
|
def create_payment_link(provider: str, payment_id: str, amount_rub: int, description: str):
|
||||||
|
if provider == "yookassa":
|
||||||
|
return create_yookassa_payment(payment_id, amount_rub, description)
|
||||||
|
if provider == "platega":
|
||||||
|
return create_platega_payment(payment_id, amount_rub, description)
|
||||||
|
raise ValueError(f"unknown provider: {provider}")
|
||||||
Loading…
Add table
Add a link
Reference in a new issue