diff --git a/backend/elena_app.py b/backend/elena_app.py index f7740c5..31b0d6b 100644 --- a/backend/elena_app.py +++ b/backend/elena_app.py @@ -656,6 +656,70 @@ def _doc_hash(doc): except Exception: return "" +@app.route("/api/legal/") +def legal_text(doc): + """Отдаёт текст юридического документа для ознакомления.""" + fname = {"offer": "dogovor_oferta.md", "pep": "soglashenie_pep.md", "pdn": "politika_pdn.md"}.get(doc) + if not fname: + return jsonify({"error": "unknown doc"}), 404 + try: + text = open(os.path.join(BASE, "legal", fname), encoding="utf-8").read() + return jsonify({"doc": doc, "version": LEGAL_DOCS.get(doc, "1.0"), "text": text}) + except Exception as e: + return jsonify({"error": str(e)}), 404 + +@app.route("/api/sign/request", methods=["POST"]) +def sign_request(): + """Генерирует код подтверждения (ПЭП). В проде — отправка по SMS/email.""" + data = request.get_json(force=True) or {} + proj = get_project(data.get("token")) + if not proj: + return jsonify({"error": "project not found"}), 404 + identifier = (data.get("identifier") or "").strip() + if not identifier: + return jsonify({"error": "укажите телефон или email"}), 400 + code = "".join(secrets.choice("0123456789") for _ in range(4)) + # храним код в crm artifact (TTL ~10 мин через timestamp) + crm = latest_artifact(proj["id"], "crm") or {} + crm["_sign_code"] = code + crm["_sign_identifier"] = identifier + crm["_sign_at"] = now() + save_artifact(proj["id"], "crm", crm) + # ДЕМО: возвращаем код (в проде — SMS/email, код не возвращается) + return jsonify({"ok": True, "demo_code": code, "note": "ДЕМО: в проде код придёт по SMS/email"}) + +@app.route("/api/sign/confirm", methods=["POST"]) +def sign_confirm(): + """Проверяет код и фиксирует подписание (акцепт) в журнал.""" + data = request.get_json(force=True) or {} + proj = get_project(data.get("token")) + if not proj: + return jsonify({"error": "project not found"}), 404 + code = (data.get("code") or "").strip() + crm = latest_artifact(proj["id"], "crm") or {} + if not crm.get("_sign_code") or code != crm["_sign_code"]: + return jsonify({"error": "неverный код"}), 400 + identifier = crm.get("_sign_identifier", "") + docs = data.get("docs", ["offer", "pep"]) + ip = request.headers.get("X-Real-IP") or request.headers.get("X-Forwarded-For", request.remote_addr or "") + ua = request.headers.get("User-Agent", "")[:300] + con = db() + recorded = [] + for doc in docs: + if doc not in LEGAL_DOCS: + continue + h = _doc_hash(doc) + con.execute( + "INSERT INTO acceptances (project_id, doc, doc_version, doc_hash, identifier, code, ip, user_agent, payment_id, accepted_at) VALUES (?,?,?,?,?,?,?,?,?,?)", + (proj["id"], doc, LEGAL_DOCS[doc], h, identifier, code, ip, ua, "", now()) + ) + recorded.append(doc) + # очищаем временный код + crm.pop("_sign_code", None); crm.pop("_sign_at", None) + save_artifact(proj["id"], "crm", crm) + con.commit() + return jsonify({"ok": True, "signed": recorded, "identifier": identifier, "at": now()}) + @app.route("/api/accept", methods=["POST"]) def accept_documents(): """Фиксация акцепта (ПЭП) в append-only журнал с хешем документов.""" @@ -1009,6 +1073,7 @@ def get_project_state(token): "crm": latest_artifact(proj["id"], "crm") or {"pipeline":"lead","deal_amount":0,"paid_amount":0,"contact":"","source":"","note":"","billing_type":"paid"}, "tasks": (latest_artifact(proj["id"], "tasks") or {}).get("items", []), "pricing": latest_artifact(proj["id"], "pricing"), + "signed": db().execute("SELECT 1 FROM acceptances WHERE project_id=? AND doc='offer' LIMIT 1", (proj["id"],)).fetchone() is not None, "documents": [json.loads(r["data_json"]) and {"filename": json.loads(r["data_json"])["filename"], "size": json.loads(r["data_json"]).get("size",0)} for r in db().execute("SELECT data_json FROM artifacts WHERE project_id=? AND kind='document' ORDER BY id", (proj["id"],)).fetchall()] })