From 5a94b2cea349e02595aab14c63b48066ad833c83 Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Wed, 3 Jun 2026 10:25:22 +0300 Subject: [PATCH] security: remove backend/ from public repo (leaked bot token, prod code) - backend/elena_app.py contained hardcoded Telegram bot token (now revoked) - backend/ is production code, does not belong in public mockups repo - added backend/ to .gitignore; local files preserved (--cached) --- .gitignore | 1 + backend/.env.sber.example | 19 - backend/elena_app.py | 1763 ------------------------------ backend/legal/dogovor_oferta.md | 123 --- backend/legal/politika_pdn.md | 89 -- backend/legal/soglashenie_pep.md | 97 -- 6 files changed, 1 insertion(+), 2091 deletions(-) delete mode 100644 backend/.env.sber.example delete mode 100644 backend/elena_app.py delete mode 100644 backend/legal/dogovor_oferta.md delete mode 100644 backend/legal/politika_pdn.md delete mode 100644 backend/legal/soglashenie_pep.md diff --git a/.gitignore b/.gitignore index 8c359e0..02dbee2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ desktop.ini Thumbs.db .DS_Store *.log +backend/ diff --git a/backend/.env.sber.example b/backend/.env.sber.example deleted file mode 100644 index 0132026..0000000 --- a/backend/.env.sber.example +++ /dev/null @@ -1,19 +0,0 @@ -# ── СБП (Сбер) — динамический QR ───────────────────────── -# Активация боевого режима: заполнить значения, выставить SBER_SBP_ENABLED=1, -# положить в /opt/elena-consulting/.env (или Environment= в systemd-юните), -# затем: systemctl restart elena-consulting -# -# Реквизиты берутся в СберБизнес: СБП → Приём по QR → API/Интеграция -SBER_SBP_ENABLED=0 -SBER_SBP_URL= # базовый URL API СБП Сбера (из документации СберБизнес) -SBER_SBP_TOKEN= # API-токен / secret key мерчанта -SBER_SBP_MERCHANT= # merchantId / TID -SBER_SBP_MEMBER= # memberId (банк-участник СБП), если требуется - -# Webhook (указать в кабинете СберБизнес как callback/notification URL): -# https://wasrusgen1.ru/consulting/api/payment/sber-webhook -# -# ВНИМАНИЕ: путь endpoint регистрации QR в коде задан как -# {SBER_SBP_URL}/sbp/c2b/qr/dynamic -# — сверить с реальной документацией СберБизнес и поправить при необходимости -# (функция sber_qr в elena_app.py). diff --git a/backend/elena_app.py b/backend/elena_app.py deleted file mode 100644 index ee9295a..0000000 --- a/backend/elena_app.py +++ /dev/null @@ -1,1763 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -ELENA CONSULTING — Backend API -@wasrusgen1 | КОНСАЛТИНГ -Реальный чат-интервью + построение бизнес-модели через Opus 4.8. -Крутится на Finnish VPS (RU IP блокируется Anthropic). -""" -import os, re, json, sqlite3, secrets, time, base64, hmac, hashlib -from urllib.parse import parse_qsl -from datetime import datetime, timezone -from flask import Flask, request, jsonify, g -from flask_cors import CORS -import anthropic - -# ── Config ─────────────────────────────────────────── -BASE = "/opt/elena-consulting" -DB_PATH = os.path.join(BASE, "elena.db") -PROMPT_PATH = os.path.join(BASE, "elena_system_prompt.md") -UPLOAD_DIR = os.path.join(BASE, "uploads") -MODEL = "claude-opus-4-8" - -def extract_text(path, fname): - ext = fname.lower().rsplit(".", 1)[-1] if "." in fname else "" - try: - if ext == "pdf": - import pdfplumber - with pdfplumber.open(path) as pdf: - return "\n".join(p.extract_text() or "" for p in pdf.pages) - elif ext == "docx": - import docx - return "\n".join(p.text for p in docx.Document(path).paragraphs) - elif ext in ("xlsx", "xlsm"): - import openpyxl - wb = openpyxl.load_workbook(path, read_only=True, data_only=True) - out = [] - for ws in wb.worksheets: - out.append(f"# Лист: {ws.title}") - for row in ws.iter_rows(values_only=True): - cells = [str(c) for c in row if c is not None] - if cells: out.append(" | ".join(cells)) - return "\n".join(out) - elif ext in ("txt", "csv", "md"): - return open(path, encoding="utf-8", errors="replace").read() - except Exception as e: - return f"[не удалось извлечь текст из {fname}: {e}]" - return f"[{fname}: формат .{ext} не поддержан для извлечения текста]" - -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() - -# ── Операторская авторизация (Telegram initData + пароль на десктопе) ── -def _env(key, default=None): - try: - env = open(os.path.join(BASE, ".env")).read() - m = re.search(rf'^{re.escape(key)}=(.*)$', env, re.M) - if m: - return m.group(1).strip() - except Exception: - pass - return default - -TG_BOT_TOKEN = "8767209545:AAEVgfL-bAhg6j0fHUyKWUze4SLTfJbLklM" -OPERATOR_PASSWORD = _env("OPERATOR_PASSWORD", "") -ADMIN_TG_IDS = set(x for x in (_env("ADMIN_TG_IDS", "") or "").replace(" ", "").split(",") if x) - -def _op_token(): - """Серверный операторский токен (зависит от пароля — смена пароля инвалидирует сессии).""" - secret = (OPERATOR_PASSWORD or "elena-op-fallback").encode() - return hmac.new(secret, b"operator-session-v1", hashlib.sha256).hexdigest() - -def verify_tg_initdata(init_data): - """Проверка подписи Telegram WebApp initData. Возвращает dict пользователя или None.""" - try: - data = dict(parse_qsl(init_data, keep_blank_values=True)) - recv = data.pop("hash", None) - if not recv: - return None - check = "\n".join(f"{k}={data[k]}" for k in sorted(data)) - secret = hmac.new(b"WebAppData", TG_BOT_TOKEN.encode(), hashlib.sha256).digest() - calc = hmac.new(secret, check.encode(), hashlib.sha256).hexdigest() - if not hmac.compare_digest(calc, recv): - return None - return json.loads(data.get("user", "{}")) - except Exception: - return None - -def is_operator(): - """True если запрос несёт валидный операторский токен.""" - return bool(OPERATOR_PASSWORD) and request.headers.get("X-Operator-Token") == _op_token() - -app = Flask(__name__) -CORS(app) - -def now(): - return datetime.now(timezone.utc).isoformat() - -# ── DB ─────────────────────────────────────────────── -def db(): - if "db" not in g: - g.db = sqlite3.connect(DB_PATH) - g.db.row_factory = sqlite3.Row - return g.db - -@app.teardown_appcontext -def close_db(exc): - d = g.pop("db", None) - if d: d.close() - -def init_db(): - con = sqlite3.connect(DB_PATH) - con.executescript(""" - CREATE TABLE IF NOT EXISTS projects ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - token TEXT UNIQUE NOT NULL, - client_name TEXT, - niche TEXT, - description TEXT, - status TEXT DEFAULT 'interview', - created_at TEXT, - tg_chat_id TEXT - ); - CREATE TABLE IF NOT EXISTS messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - project_id INTEGER NOT NULL, - role TEXT NOT NULL, -- 'user' (клиент) | 'assistant' (Елена) - content TEXT NOT NULL, - created_at TEXT, - FOREIGN KEY(project_id) REFERENCES projects(id) - ); - CREATE TABLE IF NOT EXISTS models ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - project_id INTEGER NOT NULL, - blocks_json TEXT NOT NULL, - created_at TEXT, - FOREIGN KEY(project_id) REFERENCES projects(id) - ); - CREATE TABLE IF NOT EXISTS artifacts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - project_id INTEGER NOT NULL, - kind TEXT NOT NULL, -- 'selection' | 'canvas' | 'vsm' | 'spec' - data_json TEXT NOT NULL, - created_at TEXT, - FOREIGN KEY(project_id) REFERENCES projects(id) - ); - CREATE TABLE IF NOT EXISTS acceptances ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - project_id INTEGER NOT NULL, - doc TEXT NOT NULL, -- 'offer' | 'pep' | 'pdn' - doc_version TEXT NOT NULL, - doc_hash TEXT NOT NULL, -- SHA-256 текста документа (что подписано) - identifier TEXT, -- телефон/email подписанта - code TEXT, -- код подтверждения - ip TEXT, - user_agent TEXT, - payment_id TEXT, -- акцепт оплатой - accepted_at TEXT NOT NULL -- append-only, не редактируется - ); - """) - # Миграции — добавляем колонки если нет (идемпотентно) - for sql in [ - "ALTER TABLE projects ADD COLUMN tg_chat_id TEXT", - "ALTER TABLE messages ADD COLUMN channel TEXT DEFAULT 'interview'", - ]: - try: - con.execute(sql) - except Exception: - pass - con.commit() - con.close() - -# Миграции должны идти и под gunicorn (он импортирует модуль, __main__ не выполняется) -init_db() - -# ── Helpers ────────────────────────────────────────── -def get_project(token): - return db().execute("SELECT * FROM projects WHERE token=?", (token,)).fetchone() - -def history(project_id): - # Только канал интервью — он питает билд-инструменты. Q&A по этапам сюда не попадает. - rows = db().execute( - "SELECT role, content FROM messages WHERE project_id=? AND (channel='interview' OR channel IS NULL) ORDER BY id", - (project_id,) - ).fetchall() - return [{"role": r["role"], "content": r["content"]} for r in rows] - -def interview_as_text(project_id): - """Интервью в виде текста — для подмешивания в контекст Q&A без нарушения чередования ролей.""" - rows = db().execute( - "SELECT role, content FROM messages WHERE project_id=? AND (channel='interview' OR channel IS NULL) ORDER BY id", - (project_id,) - ).fetchall() - if not rows: - return None - out = ["ИНТЕРВЬЮ С КЛИЕНТОМ (этап 1, диагностика):"] - for r in rows: - who = "Клиент" if r["role"] == "user" else "Елена" - out.append(f"{who}: {r['content']}") - return "\n".join(out) - -def get_deviations(project_id): - d = latest_artifact(project_id, "deviations") - return d.get("items", []) if d else [] - -def add_deviation(project_id, item): - items = get_deviations(project_id) - item["at"] = now() - items.append(item) - save_artifact(project_id, "deviations", {"items": items}) - -def stage_artifact_context(project_id, stage): - """Текущий артефакт этапа + уже зафиксированные отклонения — как контекст для Елены.""" - s = (stage or "").lower() - parts = [] - if s in ("4", "canvas", "idef0", "analysis", "model", "анализ"): - cv = latest_artifact(project_id, "canvas") - if cv: - parts.append("ТЕКУЩАЯ СТРАТЕГИЯ (Business Model Canvas):\n" + json.dumps(cv, ensure_ascii=False)[:4000]) - mrow = db().execute( - "SELECT blocks_json FROM models WHERE project_id=? ORDER BY id DESC LIMIT 1", (project_id,) - ).fetchone() - if mrow: - parts.append("ТЕКУЩАЯ ФУНКЦИОНАЛЬНАЯ МОДЕЛЬ (IDEF0):\n" + mrow["blocks_json"][:4000]) - elif s in ("5", "spec", "план", "тз"): - sp = latest_artifact(project_id, "spec") - if sp: - parts.append("ТЕКУЩЕЕ ТЗ:\n" + json.dumps(sp, ensure_ascii=False)[:5000]) - dev = get_deviations(project_id) - if dev: - parts.append("УЖЕ ЗАФИКСИРОВАННЫЕ ОТКЛОНЕНИЯ КЛИЕНТА (не дублируй их):\n" + json.dumps(dev, ensure_ascii=False)[:2000]) - return "\n\n".join(parts) if parts else None - -def save_artifact(project_id, kind, data): - con = db() - con.execute( - "INSERT INTO artifacts (project_id, kind, data_json, created_at) VALUES (?,?,?,?)", - (project_id, kind, json.dumps(data, ensure_ascii=False), now()) - ) - con.commit() - -def latest_artifact(project_id, kind): - row = db().execute( - "SELECT data_json FROM artifacts WHERE project_id=? AND kind=? ORDER BY id DESC LIMIT 1", - (project_id, kind) - ).fetchone() - return json.loads(row["data_json"]) if row else None - -def run_tool(project_id, tool, tool_name, instruction, extra_context=None, max_tokens=4096): - """Универсальный вызов forced-tool Opus на основе истории интервью.""" - msgs = history(project_id) - if not msgs: - return None, "no interview data" - ctx = instruction - if extra_context: - ctx = extra_context + "\n\n" + instruction - msgs = msgs + [{"role": "user", "content": ctx}] - try: - resp = client.messages.create( - model=MODEL, max_tokens=max_tokens, - system=[{"type": "text", "text": SYSTEM_PROMPT, "cache_control": {"type": "ephemeral"}}], - tools=[tool], - tool_choice={"type": "tool", "name": tool_name}, - messages=msgs - ) - block = next(b for b in resp.content if b.type == "tool_use") - return block.input, {"in": resp.usage.input_tokens, "out": resp.usage.output_tokens} - except Exception as e: - return None, str(e) - -# ── Routes ─────────────────────────────────────────── -@app.route("/api/health") -def health(): - return jsonify({"ok": True, "model": MODEL, "time": now()}) - -@app.route("/api/ai-status") -def ai_status(): - """Лёгкая проверка доступности AI-движка (пинг в 1 токен). Не бросает — всегда 200.""" - try: - client.messages.create(model=MODEL, max_tokens=1, messages=[{"role": "user", "content": "ping"}]) - return jsonify({"ok": True}) - except Exception as e: - msg = str(e) - low = ("credit balance" in msg.lower()) or ("too low" in msg.lower()) or ("billing" in msg.lower()) - return jsonify({"ok": False, "reason": "low_balance" if low else "error", "detail": msg[:200]}) - -@app.route("/api/operator/auth", methods=["POST"]) -def operator_auth(): - """Вход оператора: либо Telegram initData (tg-id в ADMIN_TG_IDS), либо пароль (десктоп).""" - data = request.get_json(force=True) or {} - init_data = data.get("init_data") - password = data.get("password") - name = "Оператор" - if init_data: - user = verify_tg_initdata(init_data) - if not user: - return jsonify({"ok": False, "error": "bad_initdata"}), 401 - if str(user.get("id")) not in ADMIN_TG_IDS: - return jsonify({"ok": False, "error": "not_admin"}), 403 - name = user.get("first_name") or "Оператор" - elif password is not None: - if not OPERATOR_PASSWORD or not hmac.compare_digest(str(password), OPERATOR_PASSWORD): - return jsonify({"ok": False, "error": "bad_password"}), 401 - else: - return jsonify({"ok": False, "error": "no_creds"}), 400 - return jsonify({"ok": True, "op_token": _op_token(), "name": name}) - -@app.route("/api/operator/check") -def operator_check(): - return jsonify({"ok": is_operator()}) - -@app.route("/api/project/new", methods=["POST"]) -def new_project(): - data = request.get_json(force=True) or {} - token = secrets.token_urlsafe(8) - con = db() - cur = con.execute( - "INSERT INTO projects (token, client_name, niche, status, created_at) VALUES (?,?,?,?,?)", - (token, data.get("client_name", ""), data.get("niche", ""), "interview", now()) - ) - con.commit() - pid = cur.lastrowid - - # Первое приветствие Елены — открывает интервью - greeting = _elena_first_message(data.get("client_name", ""), data.get("niche", "")) - con.execute( - "INSERT INTO messages (project_id, role, content, created_at) VALUES (?,?,?,?)", - (pid, "assistant", greeting, now()) - ) - con.commit() - return jsonify({"token": token, "greeting": greeting}) - -def _elena_first_message(name, niche): - nm = f", {name}" if name else "" - return (f"Здравствуйте{nm}! Я Елена, ваш консультант. " - f"Моя задача — разобрать ваш бизнес по полочкам и показать где можно ускориться и заработать больше.\n\n" - f"Расскажите своими словами: чем вы занимаетесь и что сейчас беспокоит больше всего? " - f"Можно текстом или голосом — как удобнее.") - -@app.route("/api/project/profile", methods=["POST"]) -def save_profile(): - data = request.get_json(force=True) or {} - proj = get_project(data.get("token")) - if not proj: - return jsonify({"error": "project not found"}), 404 - name = (data.get("client_name") or "").strip() - niche = (data.get("niche") or "").strip() - desc = (data.get("description") or "").strip() - con = db() - con.execute("UPDATE projects SET client_name=?, niche=?, description=? WHERE id=?", - (name, niche, desc, proj["id"])) - con.commit() - - # Профиль становится первым реальным сообщением клиента — Елена сразу в контексте. - # Удаляем старое дефолтное приветствие и формируем диалог заново. - con.execute("DELETE FROM messages WHERE project_id=?", (proj["id"],)) - profile_msg = f"Меня зовут {name}." if name else "" - if niche: profile_msg += f" Сфера деятельности: {niche}." - if desc: profile_msg += f"\n\nО моей деятельности: {desc}" - profile_msg = profile_msg.strip() or "Хочу разобрать свой бизнес." - con.execute("INSERT INTO messages (project_id, role, content, created_at) VALUES (?,?,?,?)", - (proj["id"], "user", profile_msg, now())) - con.commit() - - # Елена отвечает на профиль - try: - resp = client.messages.create( - model=MODEL, max_tokens=900, - system=[{"type": "text", "text": SYSTEM_PROMPT, "cache_control": {"type": "ephemeral"}}], - messages=[{"role": "user", "content": profile_msg}] - ) - reply = resp.content[0].text - except Exception as e: - return jsonify({"error": str(e)}), 500 - con.execute("INSERT INTO messages (project_id, role, content, created_at) VALUES (?,?,?,?)", - (proj["id"], "assistant", reply, now())) - con.commit() - return jsonify({"ok": True, "profile_msg": profile_msg, "reply": reply}) - -@app.route("/api/chat", methods=["POST"]) -def chat(): - data = request.get_json(force=True) or {} - token = data.get("token") - msg = (data.get("message") or "").strip() - if not token or not msg: - return jsonify({"error": "token and message required"}), 400 - proj = get_project(token) - if not proj: - return jsonify({"error": "project not found"}), 404 - - con = db() - con.execute( - "INSERT INTO messages (project_id, role, content, created_at) VALUES (?,?,?,?)", - (proj["id"], "user", msg, now()) - ) - con.commit() - - msgs = history(proj["id"]) - - try: - resp = client.messages.create( - model=MODEL, - max_tokens=1024, - system=[{ - "type": "text", - "text": SYSTEM_PROMPT, - "cache_control": {"type": "ephemeral"} - }], - messages=msgs - ) - reply = resp.content[0].text - usage = {"in": resp.usage.input_tokens, "out": resp.usage.output_tokens} - except Exception as e: - return jsonify({"error": str(e)}), 500 - - con.execute( - "INSERT INTO messages (project_id, role, content, created_at) VALUES (?,?,?,?)", - (proj["id"], "assistant", reply, now()) - ) - con.commit() - return jsonify({"reply": reply, "usage": usage}) - -# ── «Спросить Елену» на этапах 3–5: Q&A с полной памятью + фиксация отклонений ── -ASK_GUIDE = """РЕЖИМ ВОПРОСОВ ПО ЭТАПУ. -Клиент задаёт вопросы по уже построенному артефакту (стратегия / функциональная модель / ТЗ). Правила: -1. Отвечай как Елена — простым языком, по делу, без воды. У тебя есть полный контекст: интервью, документы, текущий артефакт. -2. Если клиент НЕ согласен и настаивает на своём («мне так удобно», «у нас так нельзя») — не переубеждай силой. Вызови инструмент record_deviation: что рекомендуешь ты (методологически верно), что выбрал клиент и ПОЧЕМУ (причина клиента — самое важное, часто это реальное ограничение, не учтённое в интервью). -3. Сам артефакт в этом режиме ты НЕ перестраиваешь — только объясняешь и фиксируешь отклонения. Перестройку сделает консультант. -4. После фиксации отклонения — коротко подтверди клиенту, что его пожелание записано и будет учтено при внедрении, но честно оставь свою рекомендацию в силе.""" - -DEVIATION_TOOL = { - "name": "record_deviation", - "description": "Зафиксировать отклонение: клиент настоял на варианте, отличном от методологически верного. Вызывай ТОЛЬКО когда клиент явно отказывается от рекомендации и выбирает своё.", - "input_schema": { - "type": "object", - "properties": { - "stage": {"type": "string", "description": "Этап/артефакт: canvas, idef0, spec, documents"}, - "node": {"type": "string", "description": "Конкретный узел/блок/функция, к которому относится отклонение"}, - "elena_rec": {"type": "string", "description": "Что рекомендует Елена — методологически верно"}, - "client_choice": {"type": "string", "description": "Что выбрал клиент"}, - "reason": {"type": "string", "description": "Причина клиента — почему ему так нужно/удобно (самое важное)"} - }, - "required": ["node", "elena_rec", "client_choice", "reason"] - } -} - -def run_ask(messages, system, pid): - """Агентный цикл: Елена отвечает и при необходимости фиксирует отклонение через tool.""" - recorded = False - for _ in range(3): - resp = client.messages.create( - model=MODEL, max_tokens=1100, - system=[{"type": "text", "text": system, "cache_control": {"type": "ephemeral"}}], - tools=[DEVIATION_TOOL], - messages=messages - ) - tool_uses = [b for b in resp.content if b.type == "tool_use"] - text = "".join(b.text for b in resp.content if b.type == "text") - if tool_uses: - messages.append({"role": "assistant", "content": resp.content}) - results = [] - for tu in tool_uses: - if tu.name == "record_deviation": - item = dict(tu.input) - item.setdefault("stage", "") - add_deviation(pid, item) - recorded = True - results.append({"type": "tool_result", "tool_use_id": tu.id, "content": "Отклонение зафиксировано."}) - messages.append({"role": "user", "content": results}) - continue - return (text or "Записала ваши пожелания."), recorded - return "Записала ваши пожелания — учтём при внедрении.", recorded - -@app.route("/api/ask", methods=["POST"]) -def ask(): - data = request.get_json(force=True) or {} - token = data.get("token") - msg = (data.get("message") or "").strip() - stage = (data.get("stage") or "").strip() - if not token or not msg: - return jsonify({"error": "token and message required"}), 400 - proj = get_project(token) - if not proj: - return jsonify({"error": "project not found"}), 404 - pid = proj["id"] - - con = db() - con.execute( - "INSERT INTO messages (project_id, role, content, created_at, channel) VALUES (?,?,?,?,?)", - (pid, "user", msg, now(), "qa") - ) - con.commit() - - # Полный контекст проекта — одним блоком, чтобы не ломать чередование ролей - ctx_parts = [] - iv = interview_as_text(pid) - if iv: - ctx_parts.append(iv) - docs = documents_context(pid) - if docs: - ctx_parts.append(docs) - art = stage_artifact_context(pid, stage) - if art: - ctx_parts.append(art) - context_block = "\n\n".join(ctx_parts) - - qa = [{"role": m["role"], "content": m["content"]} for m in db().execute( - "SELECT role, content FROM messages WHERE project_id=? AND channel='qa' ORDER BY id", (pid,) - ).fetchall()] - - messages = [ - {"role": "user", "content": (context_block or "Контекст проекта пока пуст.") - + "\n\n[Ниже — вопросы клиента по текущему этапу. Отвечай как Елена.]"}, - {"role": "assistant", "content": "Держу весь контекст проекта. Готова ответить на ваши вопросы по этапу."}, - ] + qa - - try: - reply, recorded = run_ask(messages, SYSTEM_PROMPT + "\n\n" + ASK_GUIDE, pid) - except Exception as e: - return jsonify({"error": str(e)}), 500 - - con.execute( - "INSERT INTO messages (project_id, role, content, created_at, channel) VALUES (?,?,?,?,?)", - (pid, "assistant", reply, now(), "qa") - ) - con.commit() - return jsonify({"reply": reply, "deviation_recorded": recorded}) - -# ── Организационный слой: оргструктура + должностные инструкции (между IDEF0 и ТЗ) ── -ORGCHART_TOOL = { - "name": "build_orgchart", - "description": "Строит целевую оргструктуру (TO-BE) из функциональной модели и интервью.", - "input_schema": { - "type": "object", - "properties": { - "units": {"type": "array", "items": {"type": "object", "properties": { - "role": {"type": "string", "description": "Должность/роль"}, - "reports_to": {"type": "string", "description": "Кому подчиняется (роль) или '—' для верхнего уровня"}, - "headcount": {"type": "integer", "description": "Сколько человек на этой роли"}, - "owns_functions": {"type": "array", "items": {"type": "string"}, "description": "Функции IDEF0 (node_id или название), за которые отвечает"}, - "note": {"type": "string", "description": "Комментарий: совмещения, особенности, конфликт интересов"} - }, "required": ["role", "reports_to", "headcount"]}}, - "insight": {"type": "string", "description": "Ключевой вывод: узкие места, перегруз, конфликты интересов в структуре"} - }, - "required": ["units", "insight"] - } -} -ORG_INSTRUCTION = """Построй целевую оргструктуру (TO-BE) бизнеса клиента. -Опирайся на функциональную модель (mechanisms — кто выполняет функции) и интервью. -Учитывай реальный масштаб — не раздувай штат. Где есть совмещения ролей или конфликт интересов (один человек и исполняет, и контролирует) — отметь в note соответствующей роли и в insight. -Если зафиксированы отклонения клиента — учитывай их (например, совмещение склада и пошива оставлено по требованию клиента). -Вызови build_orgchart.""" - -@app.route("/api/build-orgchart", methods=["POST"]) -def build_orgchart_route(): - data = request.get_json(force=True) or {} - proj = get_project(data.get("token")) - if not proj: - return jsonify({"error": "not found"}), 404 - extra = stage_artifact_context(proj["id"], "idef0") # canvas + idef0 + отклонения - result, usage = run_tool(proj["id"], ORGCHART_TOOL, "build_orgchart", ORG_INSTRUCTION, extra_context=extra, max_tokens=2500) - if result is None: - return jsonify({"error": usage}), 500 - save_artifact(proj["id"], "orgchart", result) - return jsonify({"orgchart": result, "usage": usage}) - -JOBS_TOOL = { - "name": "build_job_descriptions", - "description": "Должностные инструкции по ролям из оргструктуры и функций, с учётом отклонений клиента.", - "input_schema": { - "type": "object", - "properties": { - "roles": {"type": "array", "items": {"type": "object", "properties": { - "role": {"type": "string"}, - "purpose": {"type": "string", "description": "Цель должности одним предложением"}, - "responsibilities": {"type": "array", "items": {"type": "string"}, "description": "Зоны ответственности"}, - "kpis": {"type": "array", "items": {"type": "string"}, "description": "Показатели эффективности (измеримые)"}, - "reports_to": {"type": "string", "description": "Кому подчиняется"}, - "authority": {"type": "array", "items": {"type": "string"}, "description": "Права и полномочия"}, - "deviation_note": {"type": "string", "description": "Если роль затронута отклонением клиента — как именно (совмещение и риск)"} - }, "required": ["role", "purpose", "responsibilities", "kpis"]}} - }, - "required": ["roles"] - } -} -JOBS_INSTRUCTION = """Составь должностные инструкции по ролям из целевой оргструктуры и функциональной модели. -Для каждой роли: цель должности, зоны ответственности, измеримые KPI, кому подчиняется, права/полномочия. -ВАЖНО: если роль затронута отклонением клиента (совмещение функций и т.п.) — отрази это честно в deviation_note и в обязанностях, с оговоркой про риск (например, совмещение склада и пошива — риск «плавающего» учёта остатков). -Будь лаконичен: 4-6 зон ответственности, 2-4 KPI, 1-3 пункта полномочий на роль. -Вызови build_job_descriptions.""" - -@app.route("/api/build-jobs", methods=["POST"]) -def build_jobs_route(): - data = request.get_json(force=True) or {} - proj = get_project(data.get("token")) - if not proj: - return jsonify({"error": "not found"}), 404 - pid = proj["id"] - parts = [] - org = latest_artifact(pid, "orgchart") - if org: - parts.append("ЦЕЛЕВАЯ ОРГСТРУКТУРА:\n" + json.dumps(org, ensure_ascii=False)[:3000]) - art = stage_artifact_context(pid, "idef0") # idef0 + canvas + отклонения - if art: - parts.append(art) - extra = "\n\n".join(parts) if parts else None - result, usage = run_tool(pid, JOBS_TOOL, "build_job_descriptions", JOBS_INSTRUCTION, extra_context=extra, max_tokens=8000) - if result is None: - return jsonify({"error": usage}), 500 - save_artifact(pid, "jobs", result) - return jsonify({"jobs": result, "usage": usage}) - -# ── Tool schema: строгий IDEF0 (ICOM + декомпозиция) ── -ARROW = { - "type": "object", - "properties": { - "name": {"type": "string", "description": "Что за поток (название стрелки)"}, - "source": {"type": "string", "description": "Откуда: внешний источник или node_id функции"}, - "exists": {"type": "boolean", "description": "true если реально существует в бизнесе, false если ДОЛЖЕН быть но отсутствует"} - }, - "required": ["name", "source", "exists"] -} -OUT_ARROW = { - "type": "object", - "properties": { - "name": {"type": "string"}, - "target": {"type": "string", "description": "Куда идёт: внешний потребитель или node_id функции; 'НИКУДА' если выход никто не использует"}, - "exists": {"type": "boolean"} - }, - "required": ["name", "target", "exists"] -} -MECH = { - "type": "object", - "properties": { - "name": {"type": "string", "description": "Исполнитель или инструмент"}, - "type": {"type": "string", "enum": ["human", "equipment", "software", "none"]}, - "load": {"type": "string", "description": "Нагрузка/объём потребления, если известно"} - }, - "required": ["name", "type"] -} - -def _activity_schema(): - return { - "type": "object", - "properties": { - "node_id": {"type": "string", "description": "Идентификатор по IDEF0: A0, A1, A2, A1.1 ..."}, - "parent": {"type": "string", "description": "node_id родителя или 'A0' для верхнего уровня"}, - "function": {"type": "string", "description": "Функция ГЛАГОЛОМ + объект, напр. 'Раскроить ткань', 'Принять заказ'"}, - "inputs": {"type": "array", "items": ARROW, "description": "I — что ПРЕОБРАЗУЕТСЯ (сырьё, данные на обработку)"}, - "controls": {"type": "array", "items": ARROW, "description": "C — что УПРАВЛЯЕТ но не расходуется (нормы, регламенты, ТЗ, лекала). Если нет — пустой массив = функция без контроля"}, - "outputs": {"type": "array", "items": OUT_ARROW, "description": "O — результат функции"}, - "mechanisms":{"type": "array", "items": MECH, "description": "M — кто/чем выполняется (люди, оборудование, ПО)"}, - "completeness": {"type": "integer", "description": "0-100"}, - "issues": {"type": "array", "items": {"type": "string"}} - }, - "required": ["node_id","parent","function","inputs","controls","outputs","mechanisms","completeness","issues"] - } - -MODEL_TOOL = { - "name": "build_idef0_model", - "description": "Строит функциональную модель бизнеса клиента по стандарту IDEF0: контекстная диаграмма A-0, декомпозиция A0, анализ стрелок ICOM.", - "input_schema": { - "type": "object", - "properties": { - "client_summary": {"type": "string", "description": "1-2 предложения: кто клиент и чем занимается"}, - "business_pattern": {"type": "string", "description": "Тип бизнеса и модель монетизации"}, - "context": { - "type": "object", - "description": "Контекстная диаграмма A-0: весь бизнес как ОДНА функция", - "properties": { - "function": {"type": "string", "description": "Главная функция бизнеса глаголом, напр. 'Производить и продавать швейные изделия'"}, - "inputs": {"type": "array", "items": ARROW}, - "controls": {"type": "array", "items": ARROW}, - "outputs": {"type": "array", "items": OUT_ARROW}, - "mechanisms":{"type": "array", "items": MECH} - }, - "required": ["function","inputs","controls","outputs","mechanisms"] - }, - "activities": { - "type": "array", - "description": "Декомпозиция A0 на функции (3-8 для МСБ), при необходимости под-функции A1.1 и т.д.", - "items": _activity_schema() - }, - "arrow_issues": { - "type": "array", - "description": "Анализ стрелок — разрывы модели", - "items": { - "type": "object", - "properties": { - "type": {"type": "string", "enum": ["missing_control","dangling_output","dangling_input","overloaded_mechanism","manual_bridge","broken_flow"]}, - "title": {"type": "string"}, - "description": {"type": "string"}, - "node_id": {"type": "string", "description": "К какой функции относится"}, - "severity": {"type": "string", "enum": ["critical","high","medium"]} - }, - "required": ["type","title","description","node_id","severity"] - } - }, - "missing_info": {"type": "array", "items": {"type": "string"}} - }, - "required": ["client_summary","business_pattern","context","activities","arrow_issues","missing_info"] - } -} - -BUILD_INSTRUCTION = """Построй функциональную модель бизнеса клиента по строгому стандарту IDEF0. - -ПРАВИЛА ICOM (различай 4 типа стрелок): -- INPUT (I, слева): то что ПРЕОБРАЗУЕТСЯ функцией в выход (сырьё, заявка, данные на обработку). -- CONTROL (C, сверху): то что УПРАВЛЯЕТ функцией но НЕ расходуется (нормы, регламенты, ТЗ, лекала, прайс, ГОСТ). КРИТИЧНО: если у функции нет контроля — оставь controls пустым, это сигнал проблемы 'работают как привыкли'. -- OUTPUT (O, справа): результат функции. Если выход никто не использует — target='НИКУДА'. -- MECHANISM (M, снизу): кто/чем выполняется (человек, оборудование, ПО). type='none' если механизма по сути нет. - -ШАГИ: -1. context (A-0): опиши весь бизнес как ОДНУ функцию глаголом, с её внешними ICOM. -2. activities (A0): декомпозируй на 3-8 главных функций. Каждая — ГЛАГОЛ+объект ('Принять заказ', 'Раскроить ткань'). Присвой node_id A1, A2... Свяжи стрелки: выход одной функции = вход или контроль другой (source/target = node_id). -3. Где функция сложная — декомпозируй глубже (A1.1, A1.2) с parent. -4. arrow_issues: проанализируй стрелки: - - missing_control: функция без управляющих норм - - dangling_output: выход никто не потребляет - - dangling_input: вход ниоткуда не приходит - - overloaded_mechanism: один механизм (человек) на много функций - - manual_bridge: человек вручную переносит данные между функциями - - broken_flow: выход одной функции не доходит до входа следующей -5. Помечай exists=false для стрелок которые ДОЛЖНЫ быть, но в бизнесе отсутствуют. - -Функция = глагол. Не выдумывай то чего нет в интервью. -Вызови build_idef0_model.""" - -@app.route("/api/build-model", methods=["POST"]) -def build_model(): - data = request.get_json(force=True) or {} - token = data.get("token") - proj = get_project(token) - if not proj: - return jsonify({"error": "project not found"}), 404 - - msgs = history(proj["id"]) - if not msgs: - return jsonify({"error": "no interview data"}), 400 - - docs = documents_context(proj["id"]) - build_text = (docs + "\n\n" + BUILD_INSTRUCTION) if docs else BUILD_INSTRUCTION - msgs = msgs + [{"role": "user", "content": build_text}] - - try: - resp = client.messages.create( - model=MODEL, - max_tokens=4096, - system=[{"type": "text", "text": SYSTEM_PROMPT, "cache_control": {"type": "ephemeral"}}], - tools=[MODEL_TOOL], - tool_choice={"type": "tool", "name": "build_idef0_model"}, - messages=msgs - ) - block = next(b for b in resp.content if b.type == "tool_use") - model_data = block.input - usage = {"in": resp.usage.input_tokens, "out": resp.usage.output_tokens} - except Exception as e: - return jsonify({"error": str(e)}), 500 - - con = db() - con.execute( - "INSERT INTO models (project_id, blocks_json, created_at) VALUES (?,?,?)", - (proj["id"], json.dumps(model_data, ensure_ascii=False), now()) - ) - con.execute("UPDATE projects SET status='model_ready' WHERE id=?", (proj["id"],)) - con.commit() - return jsonify({"model": model_data, "usage": usage}) - -# ══ СЕЛЕКТОР МЕТОДОЛОГИЙ ═════════════════════════════ -SELECT_TOOL = { - "name": "recommend_methodologies", - "description": "Анализирует бизнес клиента и рекомендует набор методологий моделирования.", - "input_schema": { - "type": "object", - "properties": { - "business_type": {"type": "string", "description": "Распознанный тип: эксперт / услуги / производство / торговля / иное"}, - "recommended": { - "type": "array", - "items": { - "type": "object", - "properties": { - "method": {"type": "string", "enum": ["canvas", "idef0", "vsm", "dfd", "erd"]}, - "use": {"type": "boolean", "description": "Применять ли"}, - "depth": {"type": "string", "enum": ["full", "light", "skip"]}, - "reason": {"type": "string", "description": "Почему именно так для этого клиента"} - }, - "required": ["method", "use", "depth", "reason"] - } - }, - "rationale": {"type": "string", "description": "Общее обоснование набора"} - }, - "required": ["business_type", "recommended", "rationale"] - } -} -SELECT_INSTRUCTION = """Проанализируй бизнес клиента и реши какие методологии моделирования применить. -Доступны: canvas (Business Model Canvas — стратегия), idef0 (функциональная модель), vsm (поток ценности, потери), dfd (потоки данных), erd (модель данных). -Правила: canvas нужен почти всем. idef0 — где есть процессы. vsm — где есть поток материала/товара (производство, торговля). Для простого эксперта без производства vsm пропусти. -Для каждой метод. укажи use, depth (full/light/skip) и reason под ЭТОГО клиента. Дай rationale. -Вызови recommend_methodologies.""" - -@app.route("/api/select-methodologies", methods=["POST"]) -def select_methodologies(): - data = request.get_json(force=True) or {} - proj = get_project(data.get("token")) - if not proj: - return jsonify({"error": "project not found"}), 404 - result, usage = run_tool(proj["id"], SELECT_TOOL, "recommend_methodologies", SELECT_INSTRUCTION, max_tokens=1500) - if result is None: - return jsonify({"error": usage}), 500 - save_artifact(proj["id"], "selection", result) - return jsonify({"selection": result, "usage": usage}) - -# ══ CANVAS — Business Model Canvas (9 блоков) ════════ -CANVAS_BLOCK = { - "type": "object", - "properties": { - "items": {"type": "array", "items": {"type": "string"}, "description": "Пункты блока"}, - "completeness": {"type": "integer"}, - "note": {"type": "string", "description": "Комментарий / выявленная проблема по блоку"} - }, - "required": ["items", "completeness", "note"] -} -CANVAS_TOOL = { - "name": "build_canvas", - "description": "Строит Business Model Canvas клиента (9 блоков) из интервью.", - "input_schema": { - "type": "object", - "properties": { - "value_propositions": CANVAS_BLOCK, - "customer_segments": CANVAS_BLOCK, - "channels": CANVAS_BLOCK, - "customer_relationships": CANVAS_BLOCK, - "revenue_streams": CANVAS_BLOCK, - "key_resources": CANVAS_BLOCK, - "key_activities": CANVAS_BLOCK, - "key_partners": CANVAS_BLOCK, - "cost_structure": CANVAS_BLOCK, - "insight": {"type": "string", "description": "Главный стратегический вывод по бизнес-модели"} - }, - "required": ["value_propositions","customer_segments","channels","customer_relationships","revenue_streams","key_resources","key_activities","key_partners","cost_structure","insight"] - } -} -CANVAS_INSTRUCTION = """Построй Business Model Canvas клиента из интервью. 9 блоков: -- value_propositions: ценность, которую получает клиент -- customer_segments: кто платит, сегменты -- channels: как доходит ценность до клиента -- customer_relationships: тип отношений с клиентами -- revenue_streams: откуда деньги, модель монетизации -- key_resources: ключевые ресурсы -- key_activities: ключевые действия -- key_partners: партнёры, поставщики -- cost_structure: основные издержки -Каждый блок: items (пункты), completeness (0-100), note (проблема/комментарий). Дай insight — главный стратегический вывод. -Не выдумывай. Где данных нет — низкий completeness. Вызови build_canvas.""" - -@app.route("/api/build-canvas", methods=["POST"]) -def build_canvas(): - data = request.get_json(force=True) or {} - proj = get_project(data.get("token")) - if not proj: - return jsonify({"error": "project not found"}), 404 - result, usage = run_tool(proj["id"], CANVAS_TOOL, "build_canvas", CANVAS_INSTRUCTION, extra_context=documents_context(proj["id"]), max_tokens=3000) - if result is None: - return jsonify({"error": usage}), 500 - save_artifact(proj["id"], "canvas", result) - return jsonify({"canvas": result, "usage": usage}) - -# ══ ГЕНЕРАТОР ТЗ (части A-C из IDEF0) ════════════════ -SPEC_TOOL = { - "name": "build_tech_spec", - "description": "Собирает техническое задание на ПО из модели бизнеса: контекст, модули, модель данных.", - "input_schema": { - "type": "object", - "properties": { - "overview": {"type": "string", "description": "A1-A2: что за система, для кого, какую проблему решает, границы"}, - "roles": { - "type": "array", - "description": "A3: роли пользователей (из mechanisms IDEF0)", - "items": {"type": "object", "properties": { - "name": {"type": "string"}, "does": {"type": "string"}, "access": {"type": "string"} - }, "required": ["name","does","access"]} - }, - "modules": { - "type": "array", - "description": "B: модули системы (из функций IDEF0)", - "items": {"type": "object", "properties": { - "name": {"type": "string", "description": "Название модуля"}, - "source_node": {"type": "string", "description": "node_id функции IDEF0"}, - "purpose": {"type": "string"}, - "screens": {"type": "array", "items": {"type": "string"}, "description": "Экраны модуля"}, - "inputs_data": {"type": "string", "description": "Какие данные вводятся"}, - "outputs_data": {"type": "string", "description": "Что система создаёт/показывает"}, - "rules": {"type": "array", "items": {"type": "string"}, "description": "Бизнес-правила (из controls)"}, - "roles": {"type": "array", "items": {"type": "string"}, "description": "Кто пользуется"} - }, "required": ["name","source_node","purpose","screens","inputs_data","outputs_data","rules","roles"]} - }, - "entities": { - "type": "array", - "description": "C: модель данных (таблицы)", - "items": {"type": "object", "properties": { - "name": {"type": "string", "description": "Сущность/таблица"}, - "fields": {"type": "array", "items": {"type": "object", "properties": { - "field": {"type": "string"}, "type": {"type": "string"}, "note": {"type": "string"} - }, "required": ["field","type"]}}, - "relations": {"type": "array", "items": {"type": "string"}, "description": "Связи с другими таблицами"}, - "example": {"type": "string", "description": "Пример строки данных"} - }, "required": ["name","fields","relations","example"]} - }, - "open_questions": {"type": "array", "items": {"type": "string"}, "description": "Что уточнить перед разработкой"} - }, - "required": ["overview","roles","modules","entities","open_questions"] - } -} - -@app.route("/api/build-spec", methods=["POST"]) -def build_spec(): - data = request.get_json(force=True) or {} - proj = get_project(data.get("token")) - if not proj: - return jsonify({"error": "project not found"}), 404 - # Подгружаем готовую IDEF0 модель как контекст - model_row = db().execute("SELECT blocks_json FROM models WHERE project_id=? ORDER BY id DESC LIMIT 1", (proj["id"],)).fetchone() - if not model_row: - return jsonify({"error": "сначала постройте IDEF0 модель"}), 400 - idef0 = model_row["blocks_json"] - instruction = ("На основе интервью и построенной IDEF0-модели собери ТЗ на программу для бизнеса клиента.\n" - "МАППИНГ: функция IDEF0 → модуль; Input → вводимые данные; Output → что показывает; " - "Control → бизнес-правила; Mechanism → роли; хранилища → таблицы данных.\n" - "overview (A1-A2): что за система, для кого, проблема, границы.\n" - "roles (A3): из mechanisms.\n" - "modules (B): каждая функция = модуль с экранами, данными, правилами.\n" - "entities (C): модель данных — таблицы с полями, связями, примером строки.\n" - "Думай как проектировщик ПО. Вызови build_tech_spec.\n\n" - f"IDEF0-МОДЕЛЬ:\n{idef0}") - result, usage = run_tool(proj["id"], SPEC_TOOL, "build_tech_spec", instruction, max_tokens=8192) - if result is None: - return jsonify({"error": usage}), 500 - save_artifact(proj["id"], "spec", result) - con = db(); con.execute("UPDATE projects SET status='spec_ready' WHERE id=?", (proj["id"],)); con.commit() - return jsonify({"spec": result, "usage": usage}) - -@app.route("/api/build-spec-client", methods=["POST"]) -def build_spec_client(): - """ТЗ под вариант клиента: полная пересборка с учётом зафиксированных отклонений и орг. слоя.""" - data = request.get_json(force=True) or {} - proj = get_project(data.get("token")) - if not proj: - return jsonify({"error": "project not found"}), 404 - pid = proj["id"] - model_row = db().execute("SELECT blocks_json FROM models WHERE project_id=? ORDER BY id DESC LIMIT 1", (pid,)).fetchone() - if not model_row: - return jsonify({"error": "сначала постройте IDEF0 модель"}), 400 - devs = get_deviations(pid) - if not devs: - return jsonify({"error": "нет зафиксированных отклонений — вариант клиента совпадает с эталоном"}), 400 - idef0 = model_row["blocks_json"] - extra = [] - org = latest_artifact(pid, "orgchart") - if org: - extra.append("ЦЕЛЕВАЯ ОРГСТРУКТУРА:\n" + json.dumps(org, ensure_ascii=False)[:3000]) - jobs = latest_artifact(pid, "jobs") - if jobs: - extra.append("ДОЛЖНОСТНЫЕ ИНСТРУКЦИИ:\n" + json.dumps(jobs, ensure_ascii=False)[:3000]) - extra.append("ЗАФИКСИРОВАННЫЕ ОТКЛОНЕНИЯ КЛИЕНТА (реализуем ИХ вариант, не эталон):\n" + json.dumps(devs, ensure_ascii=False)[:3000]) - instruction = ("Собери ТЗ на программу под РЕАЛЬНЫЙ вариант клиента (с его отклонениями), а не под методологический эталон.\n" - "Базис — интервью и IDEF0, НО там, где клиент настоял на своём (см. отклонения) — проектируй под выбор клиента.\n" - "Например, если клиент оставил совмещение склада и пошива — модули и роли должны это отражать (один человек, общий доступ).\n" - "ОБЯЗАТЕЛЬНО: по КАЖДОМУ отклонению добавь пункт в open_questions с риском, о котором предупреждала Елена " - "(формат: «Риск [узел]: [что может пойти не так из-за выбора клиента] — рекомендация Елены была [...]»). " - "Это защита: при внедрении видно, где клиент пошёл против методологии.\n" - "МАППИНГ: функция → модуль; Input → данные; Output → показ; Control → правила; Mechanism → роли; хранилища → таблицы.\n" - "Думай как проектировщик ПО. Вызови build_tech_spec.\n\n" - f"IDEF0-МОДЕЛЬ:\n{idef0}\n\n" + "\n\n".join(extra)) - result, usage = run_tool(pid, SPEC_TOOL, "build_tech_spec", instruction, max_tokens=8192) - if result is None: - return jsonify({"error": usage}), 500 - save_artifact(pid, "spec_client", result) - return jsonify({"spec_client": result, "usage": usage}) - -@app.route("/api/project/crm", methods=["POST"]) -def update_crm(): - if not is_operator(): - return jsonify({"error": "unauthorized"}), 401 - data = request.get_json(force=True) or {} - proj = get_project(data.get("token")) - if not proj: - return jsonify({"error": "project not found"}), 404 - crm = latest_artifact(proj["id"], "crm") or { - "pipeline": "lead", "deal_amount": 0, "paid_amount": 0, - "contact": "", "source": "", "note": "", "payments": [], "billing_type": "paid" - } - for k in ("pipeline", "deal_amount", "paid_amount", "contact", "source", "note", "payments", "billing_type", "payment_schedule", "stage_payments", "stage_prices", "complexity", "stage_due"): - if k in data: crm[k] = data[k] - # paid_amount = сумма платежей (если есть реестр) - if "payments" in crm and isinstance(crm["payments"], list): - crm["paid_amount"] = sum(p.get("amount", 0) for p in crm["payments"]) - save_artifact(proj["id"], "crm", crm) - return jsonify({"ok": True, "crm": crm}) - -# ── Sber API «Платежи» (PAY): счета для юрлиц через OAuth2 + mTLS (.p12) ── -# КАРКАС: активируется когда в .env появятся SBER_PAY_ENABLED=1 + пароль .p12 + client_secret. -SBER_PAY_ENABLED = _env("SBER_PAY_ENABLED", "0") == "1" -SBER_CLIENT_ID = _env("SBER_CLIENT_ID", "") -SBER_CLIENT_SECRET = _env("SBER_CLIENT_SECRET", "") -SBER_P12_PATH = _env("SBER_P12_PATH", os.path.join(BASE, "certs", "sber.p12")) -SBER_P12_PASSWORD = _env("SBER_P12_PASSWORD", "") -SBER_API_BASE = _env("SBER_API_BASE", "https://api.sberbank.ru") # уточнить по докам СберБизнес -SBER_SCOPE = _env("SBER_SCOPE", "") -_SBER_TOKEN_CACHE = {"access": None, "exp": 0} - -def _sber_session(): - """requests-сессия с mTLS из .p12. (session, paths) или (None, reason).""" - if not SBER_PAY_ENABLED: - return None, "SBER_PAY_ENABLED!=1" - try: - import requests, tempfile - from cryptography.hazmat.primitives.serialization import ( - pkcs12, Encoding, PrivateFormat, NoEncryption) - raw = open(SBER_P12_PATH, "rb").read() - pwd = SBER_P12_PASSWORD.encode() if SBER_P12_PASSWORD else None - key, cert, _ = pkcs12.load_key_and_certificates(raw, pwd) - cf = tempfile.NamedTemporaryFile(delete=False, suffix=".pem") - cf.write(cert.public_bytes(Encoding.PEM)); cf.close() - kf = tempfile.NamedTemporaryFile(delete=False, suffix=".pem") - kf.write(key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())); kf.close() - s = requests.Session(); s.cert = (cf.name, kf.name) - return s, (cf.name, kf.name) - except Exception as e: - return None, f"mTLS error: {e}" - -def _sber_token(): - """OAuth2 access token с кэшем/рефрешем. None если не настроено.""" - if not SBER_PAY_ENABLED: - return None - if _SBER_TOKEN_CACHE["access"] and _SBER_TOKEN_CACHE["exp"] > time.time() + 30: - return _SBER_TOKEN_CACHE["access"] - sess, _info = _sber_session() - if not sess: - return None - try: - r = sess.post(SBER_API_BASE.rstrip("/") + "/ic/sso/api/v2/oauth", - data={"grant_type": "client_credentials", "scope": SBER_SCOPE}, - auth=(SBER_CLIENT_ID, SBER_CLIENT_SECRET), timeout=20) - j = r.json() - tok = j.get("access_token") - if tok: - _SBER_TOKEN_CACHE["access"] = tok - _SBER_TOKEN_CACHE["exp"] = time.time() + int(j.get("expires_in", 3600)) - return tok - except Exception: - return None - -@app.route("/api/payment/sber-status") -def sber_status(): - """Диагностика готовности Sber PAY.""" - if not SBER_PAY_ENABLED: - return jsonify({"enabled": False, "reason": "каркас готов — ждёт SBER_PAY_ENABLED=1 + пароль .p12 + client_secret в .env"}) - sess, info = _sber_session() - if not sess: - return jsonify({"enabled": True, "mtls": False, "reason": info}) - return jsonify({"enabled": True, "mtls": True, "client_id": bool(SBER_CLIENT_ID), "secret": bool(SBER_CLIENT_SECRET), "token": bool(_sber_token())}) - -@app.route("/api/payment/sber-invoice", methods=["POST"]) -def sber_invoice(): - """Выставление счёта юрлицу (Sber PAY). endpoint/поля уточнить по докам Сбера.""" - 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 - if not SBER_PAY_ENABLED: - return jsonify({"demo": True, "reason": "Sber PAY не активирован (нет пароля .p12 / client_secret). Каркас готов."}) - tok = _sber_token() - if not tok: - return jsonify({"error": "sber auth failed"}), 502 - sess, _info = _sber_session() - order = "INV-" + secrets.token_hex(5) - crm = latest_artifact(proj["id"], "crm") or {} - crm.setdefault("sber_invoices", {})[order] = {"amount": amount, "status": "issued", "at": now()} - save_artifact(proj["id"], "crm", crm) - try: - payload = { - "amount": int(round(amount * 100)), "currency": "RUB", - "orderNumber": order, - "description": (data.get("description") or "Оплата консалтинга")[:255], - "callbackUrl": "https://wasrusgen1.ru/consulting/api/payment/sber-webhook", - } - r = sess.post(SBER_API_BASE.rstrip("/") + "/v1/invoices", # путь уточнить по докам - json=payload, headers={"Authorization": "Bearer " + tok}, timeout=25) - return jsonify({"ok": True, "order": order, "status": r.status_code, "sber": r.json()}) - except Exception as e: - return jsonify({"error": str(e), "order": order}), 502 - -# ── СБП (Сбер) — динамический 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 - -# Версии и тексты юридических документов (хеш фиксируется при акцепте) -LEGAL_DOCS = {"offer": "1.0", "pep": "1.0", "pdn": "1.0"} -def _doc_hash(doc): - """SHA-256 текста документа — для доказательства что подписано.""" - path = os.path.join(BASE, "legal", {"offer": "dogovor_oferta.md", "pep": "soglashenie_pep.md", "pdn": "politika_pdn.md"}.get(doc, "")) - try: - return hashlib.sha256(open(path, "rb").read()).hexdigest() - 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 журнал с хешем документов.""" - data = request.get_json(force=True) or {} - proj = get_project(data.get("token")) - if not proj: - return jsonify({"error": "project not found"}), 404 - docs = data.get("docs", ["offer", "pep"]) # какие документы акцептованы - identifier = data.get("identifier", "") # телефон/email - code = data.get("code", "") - payment_id = data.get("payment_id", "") - 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, payment_id, now()) - ) - recorded.append({"doc": doc, "version": LEGAL_DOCS[doc], "hash": h[:16] + "..."}) - con.commit() - return jsonify({"ok": True, "accepted": recorded, "at": now()}) - -@app.route("/api/acceptances/") -def get_acceptances(token): - """Выписка из журнала акцептов (доказательная база).""" - proj = get_project(token) - if not proj: - return jsonify({"error": "not found"}), 404 - rows = db().execute( - "SELECT doc, doc_version, doc_hash, identifier, code, ip, payment_id, accepted_at FROM acceptances WHERE project_id=? ORDER BY id", (proj["id"],) - ).fetchall() - return jsonify({"acceptances": [dict(r) for r in rows]}) - -@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(): - if not is_operator(): - return jsonify({"error": "unauthorized"}), 401 - data = request.get_json(force=True) or {} - proj = get_project(data.get("token")) - if not proj: - return jsonify({"error": "project not found"}), 404 - pid = proj["id"] - con = db() - con.execute("DELETE FROM messages WHERE project_id=?", (pid,)) - con.execute("DELETE FROM models WHERE project_id=?", (pid,)) - con.execute("DELETE FROM artifacts WHERE project_id=?", (pid,)) - con.execute("DELETE FROM projects WHERE id=?", (pid,)) - con.commit() - # удалить загруженные документы - import shutil - pdir = os.path.join(UPLOAD_DIR, proj["token"]) - if os.path.isdir(pdir): - shutil.rmtree(pdir, ignore_errors=True) - return jsonify({"ok": True}) - -@app.route("/api/project/tasks", methods=["POST"]) -def update_tasks(): - if not is_operator(): - return jsonify({"error": "unauthorized"}), 401 - data = request.get_json(force=True) or {} - proj = get_project(data.get("token")) - if not proj: - return jsonify({"error": "project not found"}), 404 - tasks = data.get("tasks", []) # [{text, due, done}] - save_artifact(proj["id"], "tasks", {"items": tasks}) - return jsonify({"ok": True, "tasks": tasks}) - -@app.route("/api/project/approve", methods=["POST"]) -def approve_stage(): - if not is_operator(): - return jsonify({"error": "unauthorized"}), 401 - data = request.get_json(force=True) or {} - proj = get_project(data.get("token")) - if not proj: - return jsonify({"error": "project not found"}), 404 - stage = data.get("stage") - approved = bool(data.get("approved", True)) - con = db() - # approvals хранятся как один артефакт kind='approvals' — словарь stage->bool - row = db().execute("SELECT id, data_json FROM artifacts WHERE project_id=? AND kind='approvals' ORDER BY id DESC LIMIT 1", (proj["id"],)).fetchone() - appr = json.loads(row["data_json"]) if row else {} - if approved: appr[stage] = now() - else: appr.pop(stage, None) - if row: - con.execute("UPDATE artifacts SET data_json=? WHERE id=?", (json.dumps(appr, ensure_ascii=False), row["id"])) - else: - con.execute("INSERT INTO artifacts (project_id, kind, data_json, created_at) VALUES (?,?,?,?)", - (proj["id"], "approvals", json.dumps(appr, ensure_ascii=False), now())) - con.commit() - return jsonify({"ok": True, "approvals": appr}) - -@app.route("/api/upload", methods=["POST"]) -def upload_doc(): - data = request.get_json(force=True) or {} - proj = get_project(data.get("token")) - if not proj: - return jsonify({"error": "project not found"}), 404 - fname = (data.get("filename") or "file").replace("/", "_").replace("\\", "_") - b64 = data.get("content") or "" - try: - raw = base64.b64decode(b64.split(",")[-1]) - except Exception as e: - return jsonify({"error": "bad base64: " + str(e)}), 400 - if len(raw) > 15 * 1024 * 1024: - return jsonify({"error": "файл больше 15 МБ"}), 400 - pdir = os.path.join(UPLOAD_DIR, proj["token"]) - os.makedirs(pdir, exist_ok=True) - path = os.path.join(pdir, fname) - with open(path, "wb") as f: - f.write(raw) - text = extract_text(path, fname) - save_artifact(proj["id"], "document", {"filename": fname, "text": text[:10000], "size": len(raw)}) - return jsonify({"ok": True, "filename": fname, "chars": len(text), "size": len(raw)}) - -def documents_context(project_id): - rows = db().execute("SELECT data_json FROM artifacts WHERE project_id=? AND kind='document' ORDER BY id", (project_id,)).fetchall() - if not rows: - return None - parts = ["ЗАГРУЖЕННЫЕ ДОКУМЕНТЫ КЛИЕНТА:"] - for r in rows: - d = json.loads(r["data_json"]) - parts.append(f"\n=== {d['filename']} ===\n{d.get('text','')[:6000]}") - return "\n".join(parts) - -@app.route("/api/projects") -def list_projects(): - if not is_operator(): - return jsonify({"error": "unauthorized"}), 401 - rows = db().execute( - "SELECT p.token, p.client_name, p.niche, p.status, p.created_at, " - "(SELECT COUNT(*) FROM messages m WHERE m.project_id=p.id) as msg_count " - "FROM projects p ORDER BY p.id DESC" - ).fetchall() - out = [] - for r in rows: - pid = db().execute("SELECT id FROM projects WHERE token=?", (r["token"],)).fetchone()["id"] - out.append({ - "token": r["token"], "client_name": r["client_name"] or "Без имени", - "niche": r["niche"] or "", "status": r["status"], - "created_at": r["created_at"], "msg_count": r["msg_count"], - "has_selection": latest_artifact(pid, "selection") is not None, - "has_canvas": latest_artifact(pid, "canvas") is not None, - "has_idef0": db().execute("SELECT 1 FROM models WHERE project_id=? LIMIT 1", (pid,)).fetchone() is not None, - "has_org": latest_artifact(pid, "orgchart") is not None, - "has_spec": latest_artifact(pid, "spec") is not None, - "approvals": latest_artifact(pid, "approvals") or {}, - "crm": latest_artifact(pid, "crm") or {"pipeline":"lead","deal_amount":0,"paid_amount":0,"contact":"","source":"","note":""}, - "tasks": (latest_artifact(pid, "tasks") or {}).get("items", []) - }) - return jsonify({"projects": out}) - -SCREEN_TOOL = { - "name": "design_screen", - "description": "Проектирует один экран программы: layout, поля, действия, пример данных.", - "input_schema": { - "type": "object", - "properties": { - "title": {"type": "string", "description": "Название экрана"}, - "type": {"type": "string", "enum": ["list", "card", "form", "dashboard", "kanban"], "description": "Тип экрана"}, - "columns": {"type": "array", "items": {"type": "string"}, "description": "Для list/kanban: колонки таблицы или стадии"}, - "fields": {"type": "array", "items": {"type": "object", "properties": { - "label": {"type": "string"}, "kind": {"type": "string", "enum": ["text","number","select","date","money","status","textarea"]} - }, "required": ["label","kind"]}, "description": "Для form/card: поля"}, - "rows": {"type": "array", "items": {"type": "array", "items": {"type": "string"}}, "description": "Для list: 2-3 примера строк (значения по колонкам)"}, - "actions": {"type": "array", "items": {"type": "string"}, "description": "Кнопки/действия на экране"}, - "kpis": {"type": "array", "items": {"type": "object", "properties": {"value": {"type": "string"}, "label": {"type": "string"}}, "required": ["value","label"]}, "description": "Для dashboard: показатели"} - }, - "required": ["title", "type", "actions"] - } -} - -@app.route("/api/design-screen", methods=["POST"]) -def design_screen(): - data = request.get_json(force=True) or {} - proj = get_project(data.get("token")) - if not proj: - return jsonify({"error": "project not found"}), 404 - module = data.get("module", "") - instr = (f"Спроектируй ОДИН экран программы для модуля «{module}» бизнеса клиента.\n" - "Используй реальные данные клиента из интервью для примеров (имена, суммы, статусы).\n" - "Выбери подходящий type (list/card/form/dashboard/kanban). Заполни поля/колонки/примеры строк/KPI.\n" - "actions — конкретные кнопки. Вызови design_screen.") - result, usage = run_tool(proj["id"], SCREEN_TOOL, "design_screen", instr, max_tokens=1500) - if result is None: - return jsonify({"error": usage}), 500 - return jsonify({"screen": result, "usage": usage}) - -# ══ ЦЕНООБРАЗОВАНИЕ под клиента ══════════════════════ -PRICING_TOOL = { - "name": "build_pricing", - "description": "Оценивает масштаб работы по клиенту, анализирует рынок консалтинга и предлагает таблицу цен с аргументами.", - "input_schema": { - "type": "object", - "properties": { - "scale": { - "type": "object", - "description": "Оценка масштаба работы", - "properties": { - "size": {"type": "string", "enum": ["micro", "small", "medium", "large"], "description": "micro=соло, small=2-10, medium=10-50, large=50+"}, - "complexity": {"type": "string", "enum": ["low", "medium", "high"]}, - "scope": {"type": "string", "description": "Объём работ: сколько ролей интервьюировать, процессов, документов"}, - "effort_estimate": {"type": "string", "description": "Оценка трудозатрат (часы/недели) и токенов AI"} - }, - "required": ["size", "complexity", "scope", "effort_estimate"] - }, - "market": {"type": "string", "description": "Анализ рынка: вилка цен на аналогичный консалтинг в РФ, с кем сравниваем"}, - "packages": { - "type": "array", - "description": "2-3 пакета услуг", - "items": {"type": "object", "properties": { - "name": {"type": "string"}, - "scope": {"type": "array", "items": {"type": "string"}, "description": "Что входит"}, - "price": {"type": "integer", "description": "Цена в рублях"}, - "duration": {"type": "string", "description": "Срок"}, - "argument": {"type": "string", "description": "Аргумент почему такая цена"} - }, "required": ["name", "scope", "price", "duration", "argument"]} - }, - "recommended": {"type": "string", "description": "Какой пакет рекомендуется этому клиенту и почему"}, - "rationale": {"type": "string", "description": "Главный аргумент по цене для продажи (ROI, экономия клиента)"} - }, - "required": ["scale", "market", "packages", "recommended", "rationale"] - } -} - -@app.route("/api/build-pricing", methods=["POST"]) -def build_pricing(): - data = request.get_json(force=True) or {} - proj = get_project(data.get("token")) - if not proj: - return jsonify({"error": "project not found"}), 404 - # Контекст: интервью + IDEF0 модель если есть - model_row = db().execute("SELECT blocks_json FROM models WHERE project_id=? ORDER BY id DESC LIMIT 1", (proj["id"],)).fetchone() - extra = ("ПОСТРОЕННАЯ МОДЕЛЬ БИЗНЕСА (для оценки масштаба):\n" + model_row["blocks_json"]) if model_row else None - instr = ("На основе первичного интервью (и модели бизнеса, если есть) сформируй ЦЕНОВОЕ ПРЕДЛОЖЕНИЕ для клиента.\n" - "1. Оцени МАСШТАБ работы: размер бизнеса, сложность, сколько ролей интервьюировать, процессов, документов, оценка трудозатрат.\n" - "2. Проанализируй РЫНОК: вилка цен на аналогичный консалтинг (разбор бизнеса + ТЗ на ПО) в РФ для такого масштаба.\n" - "3. Предложи 2-3 ПАКЕТА (напр. Экспресс / Стандарт / Премиум или по этапам) с ценами в рублях, составом, сроком и аргументом цены.\n" - "4. Рекомендуй пакет под этого клиента.\n" - "5. Главный аргумент по цене — через ROI/экономию клиента (сколько он теряет сейчас vs стоимость).\n" - "Цены реалистичные для МСБ РФ. Вызови build_pricing.") - result, usage = run_tool(proj["id"], PRICING_TOOL, "build_pricing", instr, extra_context=extra, max_tokens=2500) - if result is None: - return jsonify({"error": usage}), 500 - save_artifact(proj["id"], "pricing", result) - return jsonify({"pricing": result, "usage": usage}) - -@app.route("/api/project/") -def get_project_state(token): - proj = get_project(token) - if not proj: - return jsonify({"error": "not found"}), 404 - msgs = db().execute( - "SELECT role, content, created_at FROM messages WHERE project_id=? AND (channel='interview' OR channel IS NULL) ORDER BY id", (proj["id"],) - ).fetchall() - model_row = db().execute( - "SELECT blocks_json FROM models WHERE project_id=? ORDER BY id DESC LIMIT 1", (proj["id"],) - ).fetchone() - crm = latest_artifact(proj["id"], "crm") or {"pipeline":"lead","deal_amount":0,"paid_amount":0,"contact":"","source":"","note":"","billing_type":"paid"} - payments = crm.get("payments", []) if isinstance(crm.get("payments"), list) else [] - paid_total = sum(p.get("amount", 0) for p in payments) if payments else (crm.get("paid_amount", 0) or 0) - deal_amount = crm.get("deal_amount", 0) or 0 - debt = max(0, deal_amount - paid_total) - billing = crm.get("billing_type", "paid") - # Разблокировка выгрузки документов/ТЗ: бесплатный клиент ИЛИ долг закрыт (платный с deal>0) - unlocked = (billing == "free") or (deal_amount > 0 and debt <= 0) - crm["paid_amount"] = paid_total - crm["debt"] = debt - return jsonify({ - "token": token, - "client_name": proj["client_name"], - "niche": proj["niche"], - "description": proj["description"] if "description" in proj.keys() else "", - "status": proj["status"], - "messages": [{"role": m["role"], "content": m["content"], "at": m["created_at"]} for m in msgs], - "model": json.loads(model_row["blocks_json"]) if model_row else None, - "selection": latest_artifact(proj["id"], "selection"), - "canvas": latest_artifact(proj["id"], "canvas"), - "orgchart": latest_artifact(proj["id"], "orgchart"), - "jobs": latest_artifact(proj["id"], "jobs"), - "spec": latest_artifact(proj["id"], "spec"), - "spec_client": latest_artifact(proj["id"], "spec_client"), - "approvals": latest_artifact(proj["id"], "approvals") or {}, - "crm": crm, - "unlocked": unlocked, - "debt": debt, - "paid_total": paid_total, - "deal_amount": deal_amount, - "billing_type": billing, - "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()], - "qa": [{"role": m["role"], "content": m["content"]} for m in db().execute("SELECT role, content FROM messages WHERE project_id=? AND channel='qa' ORDER BY id", (proj["id"],)).fetchall()], - "deviations": get_deviations(proj["id"]) - }) - -# ── Telegram Bot ───────────────────────────────────── -TG_TOKEN = "8767209545:AAEVgfL-bAhg6j0fHUyKWUze4SLTfJbLklM" -TG_API = f"https://api.telegram.org/bot{TG_TOKEN}" -CABINET_URL = "https://wasrusgen1.ru/consulting/cabinet.html" - -def tg_send(chat_id, text, reply_markup=None): - import urllib.request as ur - payload = {"chat_id": chat_id, "text": text, "parse_mode": "HTML"} - if reply_markup: - payload["reply_markup"] = json.dumps(reply_markup) - body = json.dumps(payload).encode() - req = ur.Request(f"{TG_API}/sendMessage", data=body, - headers={"Content-Type": "application/json"}) - try: - ur.urlopen(req, timeout=8) - except Exception as e: - app.logger.error(f"tg_send error: {e}") - -@app.route("/api/tg/webhook", methods=["POST"]) -def tg_webhook(): - data = request.get_json(silent=True) or {} - msg = data.get("message") or data.get("callback_query", {}).get("message") - if not msg: - return jsonify({"ok": True}) - chat_id = msg["chat"]["id"] - uid = str((msg.get("from") or {}).get("id", chat_id)) - text = (msg.get("text") or "").strip() - cmd = text.split()[0].split("@")[0].lower() if text.startswith("/") else "" - CRM_URL = "https://wasrusgen1.ru/consulting/crm.html" - - # /myid — узнать свой Telegram ID (для регистрации оператора и алертов) - if cmd == "/myid": - tg_send(chat_id, f"Ваш Telegram ID: {uid}\nПередайте его консультанту для доступа к операторской CRM.") - return jsonify({"ok": True}) - - # /start [token] — оператору CRM, клиенту кабинет - if cmd == "/start": - if uid in ADMIN_TG_IDS: - tg_send(chat_id, "👋 Операторская CRM @wasrusgen1 | КОНСАЛТИНГ", - reply_markup={"inline_keyboard": [[{"text": "🖥 Открыть CRM", "web_app": {"url": CRM_URL}}]]}) - return jsonify({"ok": True}) - parts = text.split() - token = parts[1] if len(parts) > 1 else None - if token: - proj = get_project(token) - if proj: - url = f"{CABINET_URL}?t={token}" - tg_send(chat_id, - f"Привет! Ваш проект: {proj['client_name'] or 'без названия'}\n" - f"Кабинет: {url}", - reply_markup={"inline_keyboard": [[ - {"text": "Открыть кабинет", "web_app": {"url": url}} - ]]} - ) - return jsonify({"ok": True}) - tg_send(chat_id, - "Добро пожаловать в @wasrusgen1 | КОНСАЛТИНГ\n\n" - "Для доступа к кабинету вам нужна персональная ссылка от консультанта.\n\n" - "Напишите нам: @wasrusgen1", - ) - - # /status — статус проекта по tg_id (если привязан) - elif cmd == "/status": - row = db().execute( - "SELECT token, client_name, status FROM projects WHERE tg_chat_id=? ORDER BY id DESC LIMIT 1", - (str(chat_id),) - ).fetchone() - if row: - tg_send(chat_id, - f"Проект: {row['client_name']}\n" - f"Статус: {row['status']}\n" - f"Кабинет: {CABINET_URL}?t={row['token']}" - ) - else: - tg_send(chat_id, "Привязанный проект не найден. Откройте кабинет по ссылке от консультанта.") - - elif cmd == "/help": - tg_send(chat_id, - "/start — открыть кабинет\n" - "/status — статус проекта\n\n" - "По вопросам: @wasrusgen1" - ) - - return jsonify({"ok": True}) - - -if __name__ == "__main__": - init_db() - app.run(host="0.0.0.0", port=5002) diff --git a/backend/legal/dogovor_oferta.md b/backend/legal/dogovor_oferta.md deleted file mode 100644 index c466f6e..0000000 --- a/backend/legal/dogovor_oferta.md +++ /dev/null @@ -1,123 +0,0 @@ -# ДОГОВОР-ОФЕРТА на оказание консультационных услуг -# @wasrusgen1 | КОНСАЛТИНГ · ИП Васильев Р.Г. -# Редакция от «__» __________ 20__ г. · версия 1.0 - -> ⚠️ Заполнить: 781909921730, 325784700271898, 40802810355710022284, СЕВЕРО-ЗАПАДНЫЙ БАНК ПАО СБЕРБАНК, 044030653, 30101810500000000653, 198412, г. Санкт-Петербург, г. Ломоносов, ул. Ломоносова, 2, литера А, i@wasrusgen.ru, +7 993 079-45-42, wasrusgen1.ru/consulting -> Подготовлено ⚖️ Юрием. УСН 6%. Перед публикацией — проверка профильным юристом + подать уведомление в РКН об обработке ПДн. - ---- - -Настоящий документ — официальное публичное предложение (оферта) ИП **Васильева Руслана Геннадьевича** (ИНН 781909921730, ОГРНИП 325784700271898), «Исполнитель», заключить договор возмездного оказания консультационных услуг с любым отозвавшимся лицом («Заказчик»). - -Согласно п. 2 ст. 437 ГК РФ оферта адресована неопределённому кругу лиц. Согласно п. 3 ст. 438 ГК РФ акцептом является совершение действий раздела 4. С момента акцепта документ — заключённый Договор. - -## 1. Термины -1.1. **Услуги** — консультационные услуги дистанционно, включая разработку и передачу результатов интеллектуальной деятельности. -1.2. **Сайт** — wasrusgen1.ru/consulting и сервисы Исполнителя. -1.3. **Тариф** — состав, объём, сроки, стоимость Услуг (на Сайте/в счёте). Неотъемлемая часть Договора. -1.4. **ТЗ (Результат работ)** — документ, отчёт, методика, регламент, схема, программное решение, передаваемые Заказчику, если предусмотрено Тарифом. -1.5. **«Елена»** — программно-аппаратный комплекс (AI-ассистент) Исполнителя. -1.6. **Потребитель** — физлицо, заказывающее Услуги для личных нужд. -1.7. **Личный кабинет** — раздел Сайта для доступа к Услугам. -1.8. **ПЭП** — простая электронная подпись (Соглашение wasrusgen1.ru/consulting). - -## 2. Предмет -2.1. Исполнитель оказывает Услуги по Тарифу, Заказчик принимает и оплачивает. -2.2. Если Тарифом предусмотрено ТЗ — применяются нормы о подряде (гл. 37 ГК) и отчуждении прав; Договор смешанный (п. 3 ст. 421 ГК). -2.3. Перечень и сроки — в Тарифе/переписке, подтверждаются ПЭП. -2.4. Услуги дистанционно. Исполнитель вправе привлекать соисполнителей. -2.5. Исполнитель на УСН (объект «доходы»). НДС не облагается (ст. 346.11 НК РФ). - -## 3. Права и обязанности -3.1. **Исполнитель обязан:** оказать качественно и в срок; информировать о ходе; передать ТЗ после полной оплаты; соблюдать конфиденциальность и обработку ПДн. -3.2. **Исполнитель вправе:** запрашивать информацию; приостановить при непредоставлении данных/неоплате; привлекать соисполнителей и «Елену»; отказаться в случаях закона. -3.3. **Заказчик обязан:** предоставлять достоверную информацию; своевременно оплачивать; принимать Услуги; хранить доступ к ПЭП. -3.4. **Заказчик вправе:** получать Услуги; запрашивать информацию; отказаться по разделу 11. - -## 4. Акцепт оферты -4.1. Акцепт — совокупно или по отдельности: выбор Тарифа и оформление заказа; отметка о согласии с Договором, Соглашением ПЭП, Политикой ПДн; подтверждение ПЭП (ввод Кода) и/или оплата. -4.2. С момента акцепта Договор заключён, имеет силу бумажного. -4.3. Акцептуя, Заказчик подтверждает ознакомление и дееспособность. -4.4. Исполнитель вправе отказать/аннулировать заказ, вернув оплату. - -## 5. Стоимость и оплата -5.1. Стоимость по Тарифу на момент акцепта, в рублях. НДС не начисляется (УСН). -5.2. **Порядок оплаты гибкий, определяется Тарифом/счётом.** Варианты (или сочетание): -- **100% предоплата** до начала Услуг; -- **частичная предоплата (например 50%)** с доплатой в срок Тарифа (в т.ч. по факту ТЗ); -- **поэтапная оплата** по графику Тарифа; -- иной согласованный порядок. -Порядок согласован с момента акцепта Тарифа/счёта (подтверждён ПЭП). -5.3. Оплата безналично на р/с 40802810355710022284, банк СЕВЕРО-ЗАПАДНЫЙ БАНК ПАО СБЕРБАНК, БИК 044030653, к/с 30101810500000000653, либо через платёжные сервисы Сайта. Обязательство исполнено с поступления средств. -5.4. Расходы по перечислению (комиссии) несёт Заказчик. -5.5. Исполнитель вправе менять Тарифы. К акцептованному заказу — Тариф на момент акцепта. -5.6. При поэтапной оплате Исполнитель вправе приостановить Услуги/передачу ТЗ до платежа. - -## 6. Сдача и приёмка -6.1. По завершении Исполнитель направляет акт/уведомление электронно. -6.2. Заказчик в течение **5 рабочих дней** принимает (подписывает ПЭП) или направляет мотивированный отказ. -6.3. При отсутствии реакции — Услуги считаются принятыми. Не применяется к Потребителю в части, ухудшающей его положение по ЗоЗПП. -6.4. Обоснованные недостатки устраняются в разумный срок. -6.5. Документооборот электронный с ПЭП, юридически значим. - -## 7. Искусственный интеллект. Обезличенные данные -7.1. Заказчик согласен на использование AI, включая «Елену». -7.2. Заказчик предоставляет право обрабатывать и использовать **обезличенные данные** (включая обезличенные тексты диалогов) для: улучшения сервиса; обучения и совершенствования AI-моделей; разработки новых продуктов; аналитики. -7.3. Обезличивание исключает идентификацию. ПДн предварительно удаляются/обезличиваются. -7.4. Право использования обезличенных данных — без ограничения срока/территории, без вознаграждения. Отзыв согласия на ПДн не ограничивает использование ранее обезличенных данных. -7.5. Результаты AI носят рекомендательный характер. Исполнитель не гарантирует конкретного экономического результата. - -## 8. Интеллектуальные права на ТЗ -8.1. Исключительное право на ТЗ переходит к Заказчику **с момента полной оплаты** (ст. 1234, п. 1 ст. 1296 ГК). -8.2. До полной оплаты право у Исполнителя. Заказчик не вправе использовать неоплаченный Результат, кроме ознакомления для приёмки. -8.3. С полной оплатой Заказчик получает право в полном объёме (если Тариф не ограничивает). -8.4. Исполнитель гарантирует ненарушение прав третьих лиц. Методики, ноу-хау, неохраняемые элементы остаются у Исполнителя (использование в обезличенном виде — раздел 7). -8.5. Исполнитель вправе указывать факт работы в портфолио без конфиденциальной информации, если Заказчик не возразит. -8.6. **Обратная лицензия на обезличенную структуру.** Заказчик предоставляет Исполнителю безвозмездную неисключительную бессрочную лицензию на использование обезличенной структуры бизнес-модели, функциональной схемы (IDEF0) и Технического задания — без наименования Заказчика, персональных данных, коммерческой тайны и конкретных финансовых показателей — в целях разработки типовых отраслевых решений, шаблонов и их коммерческого использования третьим лицам. Обезличивание производится Исполнителем до любого такого использования. Настоящий пункт не ограничивает исключительных прав Заказчика на полученный Результат (п. 8.1–8.3) и не нарушает режим конфиденциальности (раздел 9). - -## 9. Конфиденциальность -9.1. Стороны сохраняют конфиденциальность, не раскрывают третьим лицам без согласия. -9.2. Не нарушение: раскрытие по требованию госорганов; общедоступное; соисполнителям под обязательство конфиденциальности. -9.3. Действует весь срок Договора и **3 года** после. -9.4. Использование обезличенных данных (раздел 7) — не нарушение. - -## 10. Персональные данные -10.1. Акцептуя, Заказчик-физлицо даёт согласие на обработку ПДн (152-ФЗ, Политика wasrusgen1.ru/consulting). -10.2. Цели: заключение/исполнение Договора, расчёты, требования закона, уведомления. Состав: ФИО, телефон, email, платёжные данные. -10.3. Срок — действие Договора + срок хранения по закону. Отзыв — на i@wasrusgen.ru; не влияет на обработанное до и на обезличенные данные. -10.4. Исполнитель защищает ПДн от неправомерного доступа. - -## 11. Срок, изменение, расторжение -11.1. Договор действует до полного исполнения. -11.2. Исполнитель вправе менять оферту/Тарифы, публикуя на Сайте. К акцептованным — редакция на момент акцепта. -11.3. Заказчик вправе отказаться, оплатив фактические расходы и фактически оказанные Услуги (ст. 782 ГК). Для Потребителя — ст. 32 ЗоЗПП. -11.4. Исполнитель вправе отказаться при возмещении убытков (ст. 782 ГК) или существенном нарушении (неоплата). -11.5. Неотработанная предоплата возвращается за вычетом фактически оказанного в течение 10 рабочих дней. - -## 12. Ответственность -12.1. По Договору и законодательству РФ. -12.2. **Ограничение (для не-Потребителей):** совокупная ответственность Исполнителя ограничена оплаченной стоимостью этапа, по которому возникло требование. Упущенная выгода и косвенные убытки не возмещаются. -12.3. Исполнитель не отвечает за решения Заказчика на основе консультаций/ТЗ (рекомендательный характер). -12.4. Не отвечает за сбои Интернета, оборудования, операторов, платёжных систем. -12.5. **Права Потребителей:** ничто не ограничивает прав по ЗоЗПП. Условия об ограничении ответственности к Потребителю применяются лишь в части, не противоречащей императивным нормам; противоречащие — ничтожны. -12.6. Заказчик отвечает за достоверность информации и действия с ключом ПЭП. -12.7. Освобождение от ответственности при форс-мажоре. - -## 13. Разрешение споров -13.1. Переговоры. Претензионный порядок обязателен: срок ответа **10 рабочих дней**. -13.2. Для не-Потребителей — суд по месту Исполнителя. Споры с Потребителем — по подсудности ЗоЗПП (по выбору Потребителя). -13.3. Применимое право — РФ. -13.4. Сообщения через интерфейс, email, почту — считаются полученными (ст. 165.1 ГК). - -## 14. Реквизиты. Заключительные положения -14.1. Договор, Тариф, Соглашение ПЭП, Политика ПДн — единый комплекс. При противоречии приоритет Тарифа в части состава/стоимости/оплаты. -14.2. Недействительность условия не влечёт недействительности остальных. -14.3. Документы с ПЭП равнозначны собственноручно подписанным (ст. 6 63-ФЗ). - -**Реквизиты Исполнителя:** -ИП **Васильев Руслан Геннадьевич** -ИНН: 781909921730 · ОГРНИП: 325784700271898 -Адрес: 198412, г. Санкт-Петербург, г. Ломоносов, ул. Ломоносова, 2, литера А -Р/с: 40802810355710022284 · Банк: СЕВЕРО-ЗАПАДНЫЙ БАНК ПАО СБЕРБАНК · БИК: 044030653 · К/с: 30101810500000000653 -E-mail: i@wasrusgen.ru · Телефон: +7 993 079-45-42 · Сайт: wasrusgen1.ru/consulting -УСН, объект «доходы» (НДС не облагается) diff --git a/backend/legal/politika_pdn.md b/backend/legal/politika_pdn.md deleted file mode 100644 index d0530c4..0000000 --- a/backend/legal/politika_pdn.md +++ /dev/null @@ -1,89 +0,0 @@ -# ПОЛИТИКА в отношении обработки персональных данных -# @wasrusgen1 | КОНСАЛТИНГ · ИП Васильев Р.Г. -# Редакция от «__» __________ 2026 г. · версия 1.0 - -> ⚠️ Перед публикацией — проверка профильным юристом + подать уведомление об обработке ПДн в Роскомнадзор (ст. 22 152-ФЗ). -> Подготовлено ⚖️ Юрием. УСН 6%. На эту Политику ссылаются разделы 4 и 10 Договора-оферты. - ---- - -Настоящая Политика определяет порядок обработки и защиты персональных данных физических лиц («Субъекты») Оператором — ИП **Васильев Руслан Геннадьевич** (ИНН 781909921730, ОГРНИП 325784700271898), и разработана во исполнение Федерального закона от 27.07.2006 № 152-ФЗ «О персональных данных». - -## 1. Термины -1.1. **Персональные данные (ПДн)** — любая информация, относящаяся к прямо или косвенно определённому физическому лицу. -1.2. **Оператор** — ИП Васильев Р.Г., организующий и осуществляющий обработку ПДн. -1.3. **Обработка ПДн** — любое действие с ПДн: сбор, запись, систематизация, накопление, хранение, уточнение, использование, передача, обезличивание, блокирование, удаление. -1.4. **Субъект** — физическое лицо, чьи ПДн обрабатываются (Заказчик, представитель Заказчика, посетитель Сайта). -1.5. **Сайт** — wasrusgen1.ru/consulting и сервисы Оператора (включая Личный кабинет, Telegram-бот). -1.6. **«Елена»** — программно-аппаратный комплекс (AI-ассистент) Оператора. -1.7. **Обезличивание** — действия, в результате которых невозможно без дополнительной информации определить принадлежность ПДн конкретному Субъекту. - -## 2. Принципы обработки -2.1. Обработка осуществляется законно и справедливо. -2.2. Обработка ограничивается достижением конкретных, заранее определённых и законных целей. -2.3. Не допускается объединение баз данных, обработка которых несовместима между собой. -2.4. Обрабатываются только ПДн, отвечающие целям обработки; содержание и объём соответствуют заявленным целям, без избыточности. -2.5. Обеспечиваются точность и актуальность ПДн. -2.6. Хранение — не дольше, чем требуют цели обработки, если срок не установлен законом или договором. - -## 3. Категории Субъектов и состав ПДн -3.1. **Заказчики и их представители:** фамилия, имя, отчество; телефон; адрес электронной почты; платёжные реквизиты (в объёме, необходимом для расчётов); сведения о бизнесе, сообщённые в ходе оказания услуг; идентификатор Telegram (chat_id), если используется Telegram-бот. -3.2. **Посетители Сайта:** данные, автоматически передаваемые при использовании Сайта (cookie-файлы, IP-адрес, сведения о браузере) — в объёме, минимально необходимом для функционирования. -3.3. Оператор **не обрабатывает** специальные категории ПДн (раса, здоровье, политические взгляды и т.п.) и биометрические ПДн. - -## 4. Цели обработки -4.1. Заключение и исполнение Договора-оферты, оказание консультационных услуг. -4.2. Идентификация Субъекта в Личном кабинете и Telegram-боте, предоставление доступа к результатам. -4.3. Осуществление расчётов, выставление счетов, исполнение требований налогового законодательства. -4.4. Направление уведомлений, связанных с оказанием услуг (статус проекта, документы, напоминания). -4.5. Подтверждение действий простой электронной подписью (ведение журнала акцептов). -4.6. Исполнение требований законодательства РФ. -4.7. Улучшение качества услуг и работы сервиса — **исключительно на обезличенных данных** (см. раздел 8). - -## 5. Правовые основания -5.1. 152-ФЗ, иные нормативные акты РФ в области ПДн. -5.2. Договор-оферта, стороной которого является Субъект. -5.3. Согласие Субъекта на обработку ПДн (даётся акцептом оферты, отметкой о согласии при регистрации/подписании). -5.4. Налоговый кодекс РФ, Федеральный закон № 402-ФЗ «О бухгалтерском учёте» — в части, обязывающей хранить документы. - -## 6. Порядок и условия обработки -6.1. Обработка осуществляется с согласия Субъекта, а также в случаях, когда согласие не требуется по закону (исполнение договора, требования закона). -6.2. Обработка ведётся как с использованием средств автоматизации, так и без них. -6.3. Оператор вправе поручить обработку третьим лицам (соисполнителям, платёжным сервисам, хостинг-провайдерам) на основании договора с условием соблюдения конфиденциальности и требований 152-ФЗ. Перечень предоставляется по запросу. -6.4. **Трансграничная передача.** Часть инфраструктуры Оператора (серверы обработки запросов AI) размещена за пределами РФ. Передача ПДн на территорию иностранного государства осуществляется при условии обеспечения адекватной защиты прав Субъектов в соответствии со ст. 12 152-ФЗ; в Личный кабинет и на серверы обработки передаётся минимально необходимый объём данных, диалоги для обучения AI используются только в обезличенном виде. -6.5. Оператор не принимает решений, порождающих юридические последствия для Субъекта, исключительно на основании автоматизированной обработки ПДн без участия человека. Результаты AI носят рекомендательный характер и проверяются Оператором. - -## 7. Хранение и защита -7.1. ПДн хранятся в форме, позволяющей определить Субъекта, не дольше целей обработки либо срока, установленного законом (для бухгалтерских/налоговых документов — не менее 5 лет). -7.2. Базы данных Субъектов-граждан РФ хранятся с использованием баз данных, расположенных на территории РФ (ст. 18 ч. 5 152-ФЗ); за пределы РФ в обезличенном/минимальном виде передаётся только то, что необходимо для функционирования сервиса (раздел 6.4). -7.3. Оператор принимает правовые, организационные и технические меры защиты: ограничение доступа, разграничение прав, шифрование каналов передачи, резервное копирование, журналирование доступа. -7.4. По достижении целей обработки либо при отзыве согласия (если иное хранение не требуется законом) ПДн уничтожаются или обезличиваются. - -## 8. Обезличенные данные -8.1. Для улучшения сервиса и обучения AI-моделей Оператор использует **обезличенные** данные, в том числе обезличенные тексты диалогов, из которых исключена возможность идентификации Субъекта. -8.2. Обезличивание производится до использования данных в указанных целях; ПДн предварительно удаляются. -8.3. Право использования обезличенных данных не ограничено сроком и территорией; отзыв согласия на обработку ПДн не распространяется на ранее обезличенные данные (которые перестали быть персональными). - -## 9. Права Субъекта -9.1. Получать сведения об обработке своих ПДн, целях, способах, сроках. -9.2. Требовать уточнения, блокирования или уничтожения ПДн, если они неполны, устарели, неточны, получены незаконно или не нужны для целей обработки. -9.3. Отозвать согласие на обработку ПДн. -9.4. Обжаловать действия Оператора в Роскомнадзор или в судебном порядке. -9.5. Запросы и отзыв согласия направляются на e-mail **i@wasrusgen.ru** либо по адресу Оператора. Оператор отвечает в срок, установленный 152-ФЗ. -9.6. Отзыв согласия не прекращает обработку, осуществляемую на иных законных основаниях (исполнение договора, требования закона), и не распространяется на обезличенные данные. - -## 10. Cookie-файлы -10.1. Сайт использует cookie и аналогичные технологии для обеспечения работы и улучшения сервиса. -10.2. Субъект может отключить cookie в настройках браузера; часть функций Сайта при этом может стать недоступной. - -## 11. Заключительные положения -11.1. Политика действует бессрочно до замены новой редакцией. -11.2. Актуальная редакция размещается на Сайте по адресу wasrusgen1.ru/consulting. -11.3. Оператор вправе изменять Политику; новая редакция вступает в силу с момента публикации, если иное не предусмотрено редакцией. - -**Оператор:** -ИП **Васильев Руслан Геннадьевич** -ИНН: 781909921730 · ОГРНИП: 325784700271898 -Адрес: 198412, г. Санкт-Петербург, г. Ломоносов, ул. Ломоносова, 2, литера А -E-mail: i@wasrusgen.ru · Телефон: +7 993 079-45-42 -Сайт: wasrusgen1.ru/consulting diff --git a/backend/legal/soglashenie_pep.md b/backend/legal/soglashenie_pep.md deleted file mode 100644 index bc49b84..0000000 --- a/backend/legal/soglashenie_pep.md +++ /dev/null @@ -1,97 +0,0 @@ -# СОГЛАШЕНИЕ ОБ ИСПОЛЬЗОВАНИИ ПРОСТОЙ ЭЛЕКТРОННОЙ ПОДПИСИ -# @wasrusgen1 | КОНСАЛТИНГ · ИП Васильев Р.Г. -# Редакция от «__» __________ 20__ г. · версия 1.0 - -> ⚠️ Заполнить плейсхолдеры: 781909921730, 325784700271898, 198412, г. Санкт-Петербург, г. Ломоносов, ул. Ломоносова, 2, литера А, i@wasrusgen.ru, +7 993 079-45-42, wasrusgen1.ru/consulting -> Подготовлено ⚖️ Юрием по 63-ФЗ. Перед публикацией — проверка профильным юристом. - ---- - -Настоящее Соглашение об использовании простой электронной подписи (далее — «Соглашение») заключается между Индивидуальным предпринимателем **Васильевым Русланом Геннадьевичем** (ИНН 781909921730, ОГРНИП 325784700271898), именуемым в дальнейшем «Исполнитель», и любым дееспособным физическим лицом, индивидуальным предпринимателем или юридическим лицом, принявшим условия настоящего Соглашения (далее — «Пользователь»). - -Соглашение является неотъемлемой частью Договора-оферты на оказание консультационных услуг, размещённого по адресу wasrusgen1.ru/consulting, и регулирует порядок использования простой электронной подписи (далее — «ПЭП»). - -## 1. Термины и определения - -1.1. **Простая электронная подпись (ПЭП)** — электронная подпись, которая посредством использования кодов, паролей или иных средств подтверждает факт формирования электронной подписи определённым лицом (п. 2 ст. 5 ФЗ от 06.04.2011 № 63-ФЗ). - -1.2. **Ключ ПЭП** — уникальная последовательность из идентификатора Пользователя (логин — номер телефона и/или email при регистрации) и одноразового кода подтверждения (Код). - -1.3. **Код подтверждения (Код)** — последовательность символов, направляемая Пользователю по SMS и/или email, имеющая ограниченный срок действия и однократное использование. - -1.4. **Электронный документ** — документ в электронно-цифровой форме: Договор, Тариф, счёт, акцепт, акт, заявка, согласие, уведомление. - -1.5. **Журнал (лог)** — электронный протокол, фиксирующий: дату/время направления Кода, факт его ввода, IP-адрес, идентификатор сессии, реквизиты подписанного документа. - -## 2. Предмет Соглашения - -2.1. Соглашение в соответствии с п. 2 ст. 6 ФЗ № 63-ФЗ устанавливает случаи и порядок признания электронных документов, подписанных ПЭП, равнозначными документам на бумажном носителе, подписанным собственноручной подписью. - -2.2. Стороны используют ПЭП при заключении Договора, согласовании Тарифа, обмене заявками, актами, согласиями. - -2.3. Использование ПЭП добровольно. Принимая Соглашение, Пользователь подтверждает техническую возможность получать Коды и осознаёт правовые последствия. - -## 3. Ключ ПЭП и порядок подписания - -3.1. Ключом ПЭП является совокупность идентификатора (телефон/email) и Кода, направляемого на них. - -3.2. Подписание: -1. Пользователь совершает действие («Принять», «Подписать», «Оплатить», «Подтвердить») с явным указанием на документ; -2. Исполнитель направляет Код на телефон/email; -3. Пользователь вводит Код; -4. При корректном вводе в срок действия документ считается подписанным ПЭП. - -3.3. Факт корректного ввода Кода в Журнале означает подписание именно тем Пользователем, которому принадлежит идентификатор. - -3.4. В отдельных случаях переход по уникальной персональной ссылке с подтверждающим действием приравнивается к вводу Кода. - -## 4. Определение подписавшего лица - -4.1. Подписавшим признаётся Пользователь, которому принадлежит идентификатор, на который направлен и корректно введён Код. - -4.2. Стороны исходят из того, что доступ к телефону/email и Кодам имеется только у Пользователя. - -4.3. Документ, подписанный ПЭП, считается исходящим от Пользователя, пока не доказано обратное. - -4.4. Документы с ПЭП влекут те же последствия, что и бумажные с собственноручной подписью. - -## 5. Обязанности Сторон. Конфиденциальность ключа - -5.1. Пользователь обязуется: сохранять Коды в тайне; обеспечивать сохранность телефона и почты; не передавать доступ третьим лицам; незамедлительно уведомить Исполнителя (i@wasrusgen.ru) о компрометации. - -5.2. До получения уведомления все действия с ключом ПЭП считаются совершёнными Пользователем; риск неправомерного использования несёт Пользователь. - -5.3. Исполнитель обязуется: обеспечивать конфиденциальность Кодов; не направлять Коды иным лицам; вести Журнал; предоставлять сведения из Журнала по запросу. - -5.4. Исполнитель не отвечает за компрометацию по причинам вне его контроля. - -## 6. Порядок акцепта - -6.1. Соглашение размещается в открытом доступе wasrusgen1.ru/consulting. - -6.2. Акцепт — любое из: проставление галочки согласия; ввод первого Кода; начало использования ПЭП. - -6.3. Совершение действий п. 6.2 означает ознакомление, понимание и согласие. - -6.4. Соглашение действует весь срок использования сервиса. - -## 7. Доказательная сила Журнала - -7.1. Журнал является надлежащим, достаточным и допустимым доказательством факта, содержания, даты подписания. - -7.2. Электронные документы с ПЭП равнозначны бумажным с собственноручной подписью и используются как доказательства в суде. - -7.3. Бремя доказывания недостоверности данных Журнала лежит на оспаривающей Стороне. - -7.4. Записи Журнала имеют доказательственную силу. Срок хранения — не менее срока исковой давности. - -## 8. Заключительные положения - -8.1. Исполнитель вправе изменять Соглашение, публикуя новую редакцию wasrusgen1.ru/consulting. - -8.2. В остальном — Договор и законодательство РФ (63-ФЗ, ГК РФ). - -8.3. Недействительность отдельного положения не влечёт недействительности остальных. - -**Исполнитель: ИП Васильев Руслан Геннадьевич** -ИНН 781909921730 · ОГРНИП 325784700271898 · 198412, г. Санкт-Петербург, г. Ломоносов, ул. Ломоносова, 2, литера А · i@wasrusgen.ru · +7 993 079-45-42