feat: payment integration — YooKassa (card/SBP QR) + cash, webhook auto-records to ledger

This commit is contained in:
wasrusgen 2026-05-30 15:01:10 +03:00
parent a056f1a7eb
commit e42d42f207

View File

@ -49,6 +49,18 @@ def _key():
env = open("/opt/zashita-api/.env").read()
return re.search(r'ANTHROPIC_API_KEY=(\S+)', env).group(1)
def _yookassa_creds():
"""shopId и secret из .env. Если нет — None (демо-режим)."""
try:
env = open(os.path.join(BASE, ".env")).read()
sid = re.search(r'YOOKASSA_SHOP_ID=(\S+)', env)
sec = re.search(r'YOOKASSA_SECRET=(\S+)', env)
if sid and sec:
return sid.group(1), sec.group(1)
except Exception:
pass
return None, None
client = anthropic.Anthropic(api_key=_key())
SYSTEM_PROMPT = open(PROMPT_PATH, encoding="utf-8").read()
@ -619,6 +631,92 @@ def update_crm():
save_artifact(proj["id"], "crm", crm)
return jsonify({"ok": True, "crm": crm})
@app.route("/api/payment/create", methods=["POST"])
def payment_create():
"""Создаёт платёж. method: card | sbp | cash."""
import urllib.request, urllib.parse
data = request.get_json(force=True) or {}
proj = get_project(data.get("token"))
if not proj:
return jsonify({"error": "project not found"}), 404
amount = float(data.get("amount", 0))
method = data.get("method", "card") # card | sbp | cash
desc = data.get("description", f"Оплата консалтинга — {proj['client_name'] or 'клиент'}")
if amount <= 0:
return jsonify({"error": "сумма должна быть больше 0"}), 400
# Наличные — не онлайн: фиксируем намерение, Руслан подтвердит вручную
if method == "cash":
crm = latest_artifact(proj["id"], "crm") or {}
pending = crm.get("pending_cash", [])
pending.append({"amount": amount, "desc": desc, "at": now()})
crm["pending_cash"] = pending
save_artifact(proj["id"], "crm", crm)
return jsonify({"method": "cash", "instructions": "Оплата наличными при встрече. Консультант подтвердит получение.", "amount": amount})
sid, sec = _yookassa_creds()
return_url = data.get("return_url", "https://wasrusgen1.ru/consulting/cabinet.html")
# Демо-режим если ключей ЮKassa нет
if not sid:
return jsonify({
"method": method, "demo": True,
"confirmation_url": return_url + "?demo_paid=" + str(int(amount)),
"note": "ДЕМО: ключи ЮKassa не настроены. Реальная оплата заработает после добавления YOOKASSA_SHOP_ID/SECRET."
})
# Реальный вызов ЮKassa API
import base64 as b64m, json as jsonm
payload = {
"amount": {"value": f"{amount:.2f}", "currency": "RUB"},
"capture": True,
"description": desc,
"metadata": {"token": proj["token"]},
"confirmation": {"type": "redirect", "return_url": return_url}
}
if method == "sbp":
payload["payment_method_data"] = {"type": "sbp"}
payload["confirmation"] = {"type": "qr"}
try:
body = jsonm.dumps(payload).encode()
req = urllib.request.Request("https://api.yookassa.ru/v3/payments", data=body, method="POST")
auth = b64m.b64encode(f"{sid}:{sec}".encode()).decode()
req.add_header("Authorization", "Basic " + auth)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotence-Key", secrets.token_hex(16))
resp = urllib.request.urlopen(req, timeout=20)
result = jsonm.loads(resp.read())
conf = result.get("confirmation", {})
return jsonify({
"method": method, "payment_id": result.get("id"),
"confirmation_url": conf.get("confirmation_url"),
"qr": conf.get("confirmation_data") # для СБП — QR-данные
})
except Exception as e:
return jsonify({"error": "ЮKassa: " + str(e)}), 500
@app.route("/api/payment/webhook", methods=["POST"])
def payment_webhook():
"""ЮKassa шлёт уведомление о статусе. При succeeded — платёж в реестр."""
import json as jsonm
data = request.get_json(force=True) or {}
event = data.get("event")
obj = data.get("object", {})
if event == "payment.succeeded":
token = obj.get("metadata", {}).get("token")
proj = get_project(token) if token else None
if proj:
amount = float(obj.get("amount", {}).get("value", 0))
method = obj.get("payment_method", {}).get("type", "")
crm = latest_artifact(proj["id"], "crm") or {"payments": []}
crm.setdefault("payments", []).append({
"date": now()[:10], "amount": amount,
"note": f"ЮKassa ({method})", "auto": True
})
crm["paid_amount"] = sum(p.get("amount", 0) for p in crm["payments"])
save_artifact(proj["id"], "crm", crm)
return jsonify({"ok": True}) # ЮKassa требует 200
@app.route("/api/project/delete", methods=["POST"])
def delete_project():
data = request.get_json(force=True) or {}