mirror of
https://github.com/wasrusgen/wasrusgen1-crm.git
synced 2026-06-03 17:44:46 +00:00
feat: e-signing endpoints — code request, confirm with journal, legal text, signed status
This commit is contained in:
parent
4776b9a9e0
commit
48043ac0b9
@ -656,6 +656,70 @@ def _doc_hash(doc):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
@app.route("/api/legal/<doc>")
|
||||||
|
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"])
|
@app.route("/api/accept", methods=["POST"])
|
||||||
def accept_documents():
|
def accept_documents():
|
||||||
"""Фиксация акцепта (ПЭП) в append-only журнал с хешем документов."""
|
"""Фиксация акцепта (ПЭП) в append-only журнал с хешем документов."""
|
||||||
@ -1009,6 +1073,7 @@ def get_project_state(token):
|
|||||||
"crm": latest_artifact(proj["id"], "crm") or {"pipeline":"lead","deal_amount":0,"paid_amount":0,"contact":"","source":"","note":"","billing_type":"paid"},
|
"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", []),
|
"tasks": (latest_artifact(proj["id"], "tasks") or {}).get("items", []),
|
||||||
"pricing": latest_artifact(proj["id"], "pricing"),
|
"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()]
|
"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()]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user