diff --git a/backend/elena_app.py b/backend/elena_app.py
index a669557..aff6573 100644
--- a/backend/elena_app.py
+++ b/backend/elena_app.py
@@ -653,6 +653,107 @@ def update_crm():
save_artifact(proj["id"], "crm", crm)
return jsonify({"ok": True, "crm": crm})
+# ── СБП (Сбер) — динамический QR на оплату этапа ─────────────────
+SBER_SBP_ENABLED = os.getenv("SBER_SBP_ENABLED", "0") == "1"
+SBER_SBP_URL = os.getenv("SBER_SBP_URL", "") # базовый URL API СБП Сбера
+SBER_SBP_TOKEN = os.getenv("SBER_SBP_TOKEN", "") # API-токен / secret key
+SBER_SBP_MERCHANT = os.getenv("SBER_SBP_MERCHANT", "") # merchantId / TID
+SBER_SBP_MEMBER = os.getenv("SBER_SBP_MEMBER", "") # memberId (банк-участник СБП)
+SBP_WEBHOOK_URL = "https://wasrusgen1.ru/consulting/api/payment/sber-webhook"
+
+@app.route("/api/payment/sber-qr", methods=["POST"])
+def sber_qr():
+ 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") or 0)
+ if amount <= 0:
+ return jsonify({"error": "bad amount"}), 400
+ stage = data.get("stage_key") or ""
+ desc = data.get("description") or "Оплата консалтинга"
+ order_id = "ORD-" + secrets.token_hex(6)
+ crm = latest_artifact(proj["id"], "crm") or {}
+ crm.setdefault("sber_qr", {})[order_id] = {"stage": stage, "amount": amount, "status": "pending"}
+ save_artifact(proj["id"], "crm", crm)
+ if not SBER_SBP_ENABLED:
+ # демо-режим: реквизитов СберБизнес ещё нет — отдаём заглушку, UX работает
+ return jsonify({
+ "demo": True, "order_id": order_id, "amount": amount,
+ "qr_payload": "https://qr.nspk.ru/DEMO" + order_id.replace("ORD-", ""),
+ "message": "Демо-режим: СБП не настроен. Добавьте SBER_SBP_* в .env."
+ })
+ # боевой режим — регистрация динамического QR в Сбере
+ try:
+ import urllib.request
+ payload = {
+ "merchantId": SBER_SBP_MERCHANT, "memberId": SBER_SBP_MEMBER,
+ "amount": int(round(amount * 100)), "currency": "643",
+ "orderId": order_id, "description": desc[:140],
+ "callbackUrl": SBP_WEBHOOK_URL,
+ }
+ # NB: путь endpoint уточнить по документации СберБизнес (СБП C2B dynamic QR)
+ req = urllib.request.Request(
+ SBER_SBP_URL.rstrip("/") + "/sbp/c2b/qr/dynamic",
+ data=json.dumps(payload).encode("utf-8"),
+ headers={"Authorization": "Bearer " + SBER_SBP_TOKEN, "Content-Type": "application/json"})
+ with urllib.request.urlopen(req, timeout=20) as r:
+ res = json.loads(r.read().decode("utf-8"))
+ return jsonify({"order_id": order_id, "amount": amount,
+ "qr_payload": res.get("payload") or res.get("qrUrl"),
+ "qr_image": res.get("qrImage") or res.get("image"), "raw": res})
+ except Exception as e:
+ return jsonify({"error": "sber_error", "detail": str(e)}), 502
+
+@app.route("/api/payment/sber-webhook", methods=["POST"])
+def sber_webhook():
+ data = request.get_json(force=True) or {}
+ order_id = data.get("orderId") or data.get("order") or data.get("orderNumber")
+ status = str(data.get("status") or data.get("paymentStatus") or "").lower()
+ paid_ok = status in ("paid", "success", "confirmed", "approved", "deposited")
+ if not order_id:
+ return jsonify({"error": "no order"}), 400
+ today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
+ for row in db().execute("SELECT id FROM projects").fetchall():
+ crm = latest_artifact(row["id"], "crm")
+ if crm and order_id in (crm.get("sber_qr") or {}):
+ rec = crm["sber_qr"][order_id]
+ if paid_ok and rec.get("status") != "paid":
+ rec["status"] = "paid"
+ amount = rec.get("amount", 0); stage = rec.get("stage") or None
+ crm.setdefault("payments", []).append({"date": today, "amount": amount,
+ "note": "СБП (Сбер)", "stage": stage, "method": "sbp"})
+ if stage:
+ crm.setdefault("stage_payments", {})[stage] = {"amount": amount, "date": today, "method": "sbp"}
+ crm["paid_amount"] = sum(p.get("amount", 0) for p in crm.get("payments", []))
+ save_artifact(row["id"], "crm", crm)
+ return jsonify({"ok": True})
+ return jsonify({"error": "order not found"}), 404
+
+# ── Ручное подтверждение оплаты по QR (демо / нал-у-стойки) ─────
+@app.route("/api/payment/sber-confirm", methods=["POST"])
+def sber_confirm():
+ data = request.get_json(force=True) or {}
+ proj = get_project(data.get("token"))
+ if not proj:
+ return jsonify({"error": "project not found"}), 404
+ order_id = data.get("order_id")
+ crm = latest_artifact(proj["id"], "crm") or {}
+ rec = (crm.get("sber_qr") or {}).get(order_id)
+ if not rec:
+ return jsonify({"error": "order not found"}), 404
+ if rec.get("status") != "paid":
+ today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
+ rec["status"] = "paid"
+ amount = rec.get("amount", 0); stage = rec.get("stage") or None
+ crm.setdefault("payments", []).append({"date": today, "amount": amount,
+ "note": "СБП (Сбер)", "stage": stage, "method": "sbp"})
+ if stage:
+ crm.setdefault("stage_payments", {})[stage] = {"amount": amount, "date": today, "method": "sbp"}
+ crm["paid_amount"] = sum(p.get("amount", 0) for p in crm.get("payments", []))
+ save_artifact(proj["id"], "crm", crm)
+ return jsonify({"ok": True, "crm": crm})
+
import hashlib
# Версии и тексты юридических документов (хеш фиксируется при акцепте)
diff --git a/docs/crm.html b/docs/crm.html
index 52313e8..3ca2978 100644
--- a/docs/crm.html
+++ b/docs/crm.html
@@ -538,6 +538,43 @@ async function confirmStagePay(k){
]);
syncPaymentReminders();renderClient();
}
+// ── СБП QR (Сбер) ──
+async function sberQR(k){
+ const sp=getStagePrices()||{}; const amount=sp[k]||0;
+ if(!amount){alert('У этапа нет цены');return;}
+ const name=CLIENT_STAGES.find(s=>s.key===k).name;
+ try{
+ const r=await fetch(`${API}/api/payment/sber-qr`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:current,stage_key:k,amount,description:'Этап: '+name})});
+ const d=await r.json();
+ if(d.error){alert('Ошибка СБП: '+(d.detail||d.error));return;}
+ showQRModal(d,name,amount);
+ }catch(e){alert('Ошибка: '+e.message);}
+}
+function showQRModal(d,name,amount){
+ const payload=d.qr_payload||d.qr_url||'';
+ const ov=document.createElement('div');
+ ov.id='qrModal';
+ ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:9999';
+ const qrBox=d.qr_image?``:`