From 4776b9a9e0ae77009e799217887c313499e9ea13 Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Sat, 30 May 2026 15:26:47 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20billing=20type=20(paid/free)=20+=20AI?= =?UTF-8?q?=20pricing=20=E2=80=94=20scale=20assessment,=20market=20analysi?= =?UTF-8?q?s,=20packages=20with=20arguments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/elena_app.py | 66 ++++++++++++++++++++++++++++++++++++++++++-- docs/crm.html | 40 ++++++++++++++++++++++++++- 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/backend/elena_app.py b/backend/elena_app.py index 1999786..f7740c5 100644 --- a/backend/elena_app.py +++ b/backend/elena_app.py @@ -634,9 +634,9 @@ def update_crm(): 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": [] + "contact": "", "source": "", "note": "", "payments": [], "billing_type": "paid" } - for k in ("pipeline", "deal_amount", "paid_amount", "contact", "source", "note", "payments"): + for k in ("pipeline", "deal_amount", "paid_amount", "contact", "source", "note", "payments", "billing_type"): if k in data: crm[k] = data[k] # paid_amount = сумма платежей (если есть реестр) if "payments" in crm and isinstance(crm["payments"], list): @@ -924,6 +924,65 @@ def design_screen(): 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) @@ -947,8 +1006,9 @@ def get_project_state(token): "canvas": latest_artifact(proj["id"], "canvas"), "spec": latest_artifact(proj["id"], "spec"), "approvals": latest_artifact(proj["id"], "approvals") or {}, - "crm": latest_artifact(proj["id"], "crm") or {"pipeline":"lead","deal_amount":0,"paid_amount":0,"contact":"","source":"","note":""}, + "crm": latest_artifact(proj["id"], "crm") or {"pipeline":"lead","deal_amount":0,"paid_amount":0,"contact":"","source":"","note":"","billing_type":"paid"}, "tasks": (latest_artifact(proj["id"], "tasks") or {}).get("items", []), + "pricing": latest_artifact(proj["id"], "pricing"), "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()] }) diff --git a/docs/crm.html b/docs/crm.html index 96dd373..14db4cb 100644 --- a/docs/crm.html +++ b/docs/crm.html @@ -218,8 +218,14 @@ function renderPipeline(){ function renderClient(){ const crm=state.crm||{pipeline:"lead",deal_amount:0,paid_amount:0,contact:"",source:"",note:""}; + const billing=crm.billing_type||"paid"; document.getElementById("view").innerHTML=` -
${esc((state.client_name||'?')[0])}
${esc(state.client_name||'Без имени')}
${esc(state.niche||'')} · ${state.messages.length} сообщений
+
${esc((state.client_name||'?')[0])}
${esc(state.client_name||'Без имени')}
${esc(state.niche||'')} · ${state.messages.length} сообщений
+
+ + +
+
Статус воронки
Сумма сделки
@@ -233,8 +239,40 @@ function renderClient(){
`; renderTasks(); renderPayments(); + renderPricing(); renderTab(); } +async function setBilling(t){ + state.crm=state.crm||{};state.crm.billing_type=t; + await fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,billing_type:t})}); + renderClient();await loadProjects(); +} +function renderPricing(){ + const box=document.getElementById("pricingBox");if(!box)return; + const billing=(state.crm||{}).billing_type||"paid"; + if(billing==="free"){box.innerHTML=`
🎁 Бесплатный клиент — пилот / демо / партнёрский. Ценообразование не применяется.
`;return} + const p=state.pricing; + if(!p){box.innerHTML=`
💰
Ценовое предложение
Елена оценит масштаб работы, проанализирует рынок и предложит пакеты с аргументами цены.
`;return} + const SZ={micro:"Микро",small:"Малый",medium:"Средний",large:"Крупный"};const CX={low:"низкая",medium:"средняя",high:"высокая"}; + box.innerHTML=`
💰 Ценовое предложение
+
Масштаб: ${SZ[p.scale.size]||p.scale.size}Сложность: ${CX[p.scale.complexity]||p.scale.complexity}${esc(p.scale.effort_estimate)}
+
Рынок: ${esc(p.market)}
+
${p.packages.map((pk,i)=>`
${p.recommended&&p.recommended.indexOf(pk.name)>=0?'РЕКОМЕНДУЮ':''}
${esc(pk.name)}
${money(pk.price)}
${esc(pk.duration)}
${pk.scope.map(s=>`
${esc(s)}
`).join("")}
${esc(pk.argument)}
`).join("")}
+
Аргумент для клиента: ${esc(p.rationale)}
+
`; +} +async function buildPricing(){ + const btn=document.getElementById("rb-pricing");if(btn){btn.disabled=true;btn.innerHTML=' Елена анализирует рынок...'} + try{const r=await fetch(`${API}/api/build-pricing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current})});const d=await r.json(); + if(d.error){alert("Ошибка: "+d.error);if(btn){btn.disabled=false;btn.textContent="Повторить"}return} + state.pricing=d.pricing;renderPricing(); + }catch(e){alert("Ошибка: "+e.message);if(btn)btn.disabled=false} +} +function applyPrice(price){ + state.crm=state.crm||{};state.crm.deal_amount=price; + document.getElementById("cpDeal").value=price; + fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,deal_amount:price})}).then(()=>{loadProjects();renderPayments();alert("Сумма сделки установлена: "+money(price))}); +} function renderPayments(){ const crm=state.crm||{};const deal=crm.deal_amount||0;const pays=crm.payments||[]; const paid=pays.reduce((s,p)=>s+(p.amount||0),0);const left=deal-paid;