Edit document doc_type and appraiser side from the case UI
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m26s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m26s
Until now changing a document's doc_type required a manual SQL update.
Adds an inline editor on the document badge so the chair can retag
without leaving the case page, and threads an appraiser_side tag
(committee / appellant / deciding) through the appraisal pipeline so
betterment-levy cases — which usually have 2-3 appraisers — render
conflicts with the deciding appraiser's view marked as governing.
Backend
- New appraiser_facts.appraiser_side column (V5.1) populated from
documents.metadata.appraiser_side at extraction time.
- extract_appraiser_facts now returns status='sides_missing' with the
list of untagged appraisals instead of running with empty side
labels — chair must tag every appraisal first via the UI.
- Conflict detection orders entries committee → appellant → deciding so
the deciding appraiser appears last; block-tet's prompt instructs the
writer to phrase the deciding appraiser's view as the governing
factual finding ("ואולם, השמאי המכריע קבע...").
- New PATCH /api/cases/{n}/documents/{doc_id} (Pydantic model with
whitelist validation) and matching document_update MCP tool. Both
merge appraiser_side into metadata JSONB instead of touching the
schema.
UI
- New shared doc-types module exports the canonical 11 doc_type
options plus the 3 appraiser-side options; both upload-sheet and
the document badge now read from it instead of duplicating Hebrew
labels.
- New DocumentTypeEditor renders a Popover off the doc-type Badge
with two Selects. The save button stays disabled while doc_type is
appraisal but no side has been picked, mirroring the backend
enforcement so the user finds out before triggering extraction.
- usePatchDocument React-Query mutation invalidates the case detail
on success so the badge updates without a manual refresh.
This commit is contained in:
@@ -187,6 +187,17 @@ async def document_list(case_number: str) -> str:
|
||||
return await documents.document_list(case_number)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def document_update(
|
||||
case_number: str,
|
||||
doc_id: str,
|
||||
doc_type: str = "",
|
||||
appraiser_side: str = "",
|
||||
) -> str:
|
||||
"""עדכון תיוג מסמך — doc_type ו/או appraiser_side (committee/appellant/deciding). ריק = ללא שינוי."""
|
||||
return await documents.document_update(case_number, doc_id, doc_type, appraiser_side)
|
||||
|
||||
|
||||
# Claims extraction
|
||||
@mcp.tool()
|
||||
async def extract_claims(
|
||||
|
||||
@@ -4,11 +4,16 @@
|
||||
לאפשר זיהוי אוטומטי של סתירות בין שמאים שונים על אותו זיהוי (תכנית או היתר).
|
||||
|
||||
שמירה ב-DB: טבלת appraiser_facts (case_id, document_id, appraiser_name,
|
||||
fact_type, identifier, details JSONB, page_number).
|
||||
appraiser_side, fact_type, identifier, details JSONB, page_number).
|
||||
|
||||
Precondition: כל מסמך doc_type='appraisal' חייב להיות מתויג עם
|
||||
metadata.appraiser_side מתוך {committee, appellant, deciding}. החילוץ עוצר
|
||||
ומחזיר status='sides_missing' אם יש מסמכים לא מתויגים.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from uuid import UUID
|
||||
|
||||
@@ -17,6 +22,13 @@ from legal_mcp.services import claude_session, db
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Allowed sides for an appraiser in an appeals committee case.
|
||||
# committee = שמאי הוועדה המקומית
|
||||
# appellant = שמאי העורר / הצד שכנגד הוועדה
|
||||
# deciding = שמאי מכריע
|
||||
VALID_APPRAISER_SIDES = {"committee", "appellant", "deciding"}
|
||||
|
||||
|
||||
EXTRACT_FACTS_PROMPT = """אתה מנתח שומות מקרקעין לטובת ועדת ערר לתכנון ובניה.
|
||||
|
||||
תפקידך: לחלץ מתוך השומה שתי קטגוריות של עובדות אובייקטיביות שעליהן השמאי מבסס את חוות דעתו:
|
||||
@@ -77,6 +89,7 @@ async def extract_facts_from_document(
|
||||
case_id: UUID,
|
||||
document_id: UUID,
|
||||
appraiser_name: str,
|
||||
appraiser_side: str,
|
||||
text: str,
|
||||
) -> list[dict]:
|
||||
"""Extract structured facts from a single appraisal document via Claude Code."""
|
||||
@@ -107,32 +120,63 @@ async def extract_facts_from_document(
|
||||
continue
|
||||
all_facts.append({
|
||||
"appraiser_name": appraiser_name,
|
||||
"appraiser_side": appraiser_side,
|
||||
"fact_type": item["fact_type"],
|
||||
"identifier": _normalize_identifier(ident),
|
||||
"details": item.get("details") or {},
|
||||
"page_number": item.get("page_number"),
|
||||
})
|
||||
|
||||
if all_facts:
|
||||
await db.replace_appraiser_facts(case_id, document_id, all_facts)
|
||||
else:
|
||||
await db.replace_appraiser_facts(case_id, document_id, [])
|
||||
await db.replace_appraiser_facts(case_id, document_id, all_facts)
|
||||
return all_facts
|
||||
|
||||
|
||||
def _doc_metadata(doc: dict) -> dict:
|
||||
metadata = doc.get("metadata") or {}
|
||||
if isinstance(metadata, str):
|
||||
try:
|
||||
metadata = json.loads(metadata)
|
||||
except json.JSONDecodeError:
|
||||
metadata = {}
|
||||
return metadata if isinstance(metadata, dict) else {}
|
||||
|
||||
|
||||
def _infer_appraiser_name(doc: dict) -> str:
|
||||
"""Best-effort extraction of the appraiser's name from document title/metadata."""
|
||||
metadata = doc.get("metadata") or {}
|
||||
name = metadata.get("appraiser_name") if isinstance(metadata, dict) else None
|
||||
meta = _doc_metadata(doc)
|
||||
name = meta.get("appraiser_name")
|
||||
if name:
|
||||
return name
|
||||
title = doc.get("title", "")
|
||||
return title or f"שמאי (מסמך {doc.get('id', '')[:8]})"
|
||||
|
||||
|
||||
def _get_appraiser_side(doc: dict) -> str:
|
||||
"""Return the tagged side, or '' if not tagged."""
|
||||
return _doc_metadata(doc).get("appraiser_side", "") or ""
|
||||
|
||||
|
||||
def _validate_sides_tagged(appraisals: list[dict]) -> list[dict]:
|
||||
"""Return the subset of appraisals missing a valid appraiser_side tag."""
|
||||
missing: list[dict] = []
|
||||
for doc in appraisals:
|
||||
side = _get_appraiser_side(doc)
|
||||
if side not in VALID_APPRAISER_SIDES:
|
||||
missing.append({
|
||||
"document_id": doc["id"],
|
||||
"title": doc.get("title", ""),
|
||||
"current_side": side,
|
||||
})
|
||||
return missing
|
||||
|
||||
|
||||
async def extract_appraiser_facts(case_id: UUID) -> dict:
|
||||
"""Extract facts from every appraisal document in the case + detect conflicts.
|
||||
|
||||
Blocks if any appraisal is missing metadata.appraiser_side — the chair must
|
||||
tag each one via the UI before extraction runs, so that conflict rendering
|
||||
in block-tet can identify the deciding appraiser's view as authoritative.
|
||||
|
||||
Returns a summary dict ready for serialization back to the caller.
|
||||
"""
|
||||
docs = await db.list_documents(case_id)
|
||||
@@ -146,6 +190,18 @@ async def extract_appraiser_facts(case_id: UUID) -> dict:
|
||||
"conflicts": [],
|
||||
}
|
||||
|
||||
missing_sides = _validate_sides_tagged(appraisals)
|
||||
if missing_sides:
|
||||
return {
|
||||
"status": "sides_missing",
|
||||
"appraisal_count": len(appraisals),
|
||||
"missing": missing_sides,
|
||||
"message": (
|
||||
"חסר תיוג appraiser_side במסמכי שומה. תייג כל שומה דרך ה-UI "
|
||||
"(ועדה / עורר / מכריע) והרץ שוב."
|
||||
),
|
||||
}
|
||||
|
||||
by_doc = []
|
||||
total_facts = 0
|
||||
for doc in appraisals:
|
||||
@@ -160,11 +216,13 @@ async def extract_appraiser_facts(case_id: UUID) -> dict:
|
||||
continue
|
||||
|
||||
appraiser_name = _infer_appraiser_name(doc)
|
||||
appraiser_side = _get_appraiser_side(doc)
|
||||
try:
|
||||
facts = await extract_facts_from_document(
|
||||
case_id=case_id,
|
||||
document_id=UUID(doc["id"]),
|
||||
appraiser_name=appraiser_name,
|
||||
appraiser_side=appraiser_side,
|
||||
text=text,
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -183,6 +241,7 @@ async def extract_appraiser_facts(case_id: UUID) -> dict:
|
||||
"document_id": doc["id"],
|
||||
"title": doc.get("title", ""),
|
||||
"appraiser_name": appraiser_name,
|
||||
"appraiser_side": appraiser_side,
|
||||
"status": "completed",
|
||||
"facts_extracted": len(facts),
|
||||
"plans": sum(1 for f in facts if f["fact_type"] == "plan"),
|
||||
|
||||
@@ -190,8 +190,10 @@ BLOCK_PROMPTS = {
|
||||
|
||||
## כללי ציון סתירות בין שמאים (קריטי):
|
||||
- אם שני שמאים או יותר מסרו מידע שונה על אותה תכנית או היתר — חובה לסמן זאת במפורש בנוסח ניטרלי, למשל:
|
||||
> "יצוין כי השמאי X ציין כי תכנית פלונית חלה על המקרקעין במלואה, בעוד השמאי Y סבר כי חלקה של התכנית בלבד חל"
|
||||
- אין להכריע בסתירה בבלוק זה — ההכרעה (אם נדרשת) תבוא בבלוק י.
|
||||
> "יצוין כי שמאי הוועדה ציין כי תכנית פלונית חלה על המקרקעין במלואה, בעוד שמאי העורר סבר כי חלקה של התכנית בלבד חל"
|
||||
- **כשקיים שמאי מכריע** — השומה שלו היא הקובעת עובדתית. סמן זאת במפורש בסוף הדיון בסתירה, בנוסח: "ואולם, השמאי המכריע קבע כי..." או "השמאי המכריע, שבחן את עמדות הצדדים, הכריע כי...". הצג את עמדת המכריע **אחרונה** כדי שההקשר יבנה אליה.
|
||||
- השתמש בתוויות הצד המדויקות: "שמאי הוועדה המקומית", "שמאי העורר", "שמאי מכריע" — ולא בשמות פרטיים אלא אם נדרש לבהירות.
|
||||
- אין להכריע בסתירה משפטית או להגיע למסקנה נורמטיבית בבלוק זה — ההכרעה המשפטית (אם נדרשת) תבוא בבלוק י. כאן מציגים רק את הממצא העובדתי כפי שהוא, כולל הכרעת המכריע העובדתית.
|
||||
- אם אין סתירה — אין להזכיר זאת.
|
||||
|
||||
## כללים נוספים:
|
||||
@@ -502,22 +504,43 @@ async def _build_plans_context(case_id: UUID) -> str:
|
||||
return "(לא זוהו תכניות)"
|
||||
|
||||
|
||||
APPRAISER_SIDE_LABEL_HE = {
|
||||
"committee": "שמאי הוועדה המקומית",
|
||||
"appellant": "שמאי העורר",
|
||||
"deciding": "שמאי מכריע",
|
||||
"": "שמאי (לא תויג)",
|
||||
}
|
||||
|
||||
# Sort key: committee → appellant → deciding → untagged. This matches the order
|
||||
# used by db.detect_appraiser_conflicts so the deciding appraiser is last —
|
||||
# i.e. the conclusion reads most naturally ("...and the deciding appraiser ruled...").
|
||||
_SIDE_ORDER = {"committee": 1, "appellant": 2, "deciding": 3, "": 4}
|
||||
|
||||
|
||||
def _side_label(side: str) -> str:
|
||||
return APPRAISER_SIDE_LABEL_HE.get(side or "", APPRAISER_SIDE_LABEL_HE[""])
|
||||
|
||||
|
||||
async def _build_appraiser_facts_context(case_id: UUID) -> str:
|
||||
"""Group appraiser_facts by appraiser, then list each appraiser's plans+permits."""
|
||||
"""Group appraiser_facts by side (then name), list each appraiser's plans+permits."""
|
||||
facts = await db.list_appraiser_facts(case_id)
|
||||
if not facts:
|
||||
return "(לא חולצו עובדות שמאיות. הרץ extract_appraiser_facts.)"
|
||||
|
||||
by_appraiser: dict[str, dict[str, list[dict]]] = {}
|
||||
# (side, name) → {plan: [...], permit: [...]}
|
||||
groups: dict[tuple[str, str], dict[str, list[dict]]] = {}
|
||||
for f in facts:
|
||||
bucket = by_appraiser.setdefault(f["appraiser_name"], {"plan": [], "permit": []})
|
||||
key = (f.get("appraiser_side", "") or "", f["appraiser_name"])
|
||||
bucket = groups.setdefault(key, {"plan": [], "permit": []})
|
||||
bucket[f["fact_type"]].append(f)
|
||||
|
||||
ordered_keys = sorted(groups.keys(), key=lambda k: (_SIDE_ORDER.get(k[0], 9), k[1]))
|
||||
|
||||
lines: list[str] = []
|
||||
for name in sorted(by_appraiser.keys()):
|
||||
lines.append(f"\n### {name}")
|
||||
for side, name in ordered_keys:
|
||||
lines.append(f"\n### {_side_label(side)} — {name}")
|
||||
for label, key in (("תכניות", "plan"), ("היתרים", "permit")):
|
||||
items = by_appraiser[name][key]
|
||||
items = groups[(side, name)][key]
|
||||
if not items:
|
||||
continue
|
||||
lines.append(f"**{label}:**")
|
||||
@@ -543,7 +566,12 @@ async def _build_appraiser_facts_context(case_id: UUID) -> str:
|
||||
|
||||
|
||||
async def _build_appraiser_conflicts_context(case_id: UUID) -> str:
|
||||
"""Render conflict groups so the prompt can quote them in the body."""
|
||||
"""Render conflict groups so the prompt can quote them in the body.
|
||||
|
||||
Entries arrive pre-ordered from the DB by side (committee→appellant→deciding).
|
||||
When a deciding appraiser exists, the prompt must treat their view as the
|
||||
governing factual determination.
|
||||
"""
|
||||
conflicts = await db.detect_appraiser_conflicts(case_id)
|
||||
if not conflicts:
|
||||
return "(אין סתירות בין שמאים)"
|
||||
@@ -551,15 +579,19 @@ async def _build_appraiser_conflicts_context(case_id: UUID) -> str:
|
||||
type_label = {"plan": "תכנית", "permit": "היתר"}
|
||||
lines: list[str] = []
|
||||
for c in conflicts:
|
||||
lines.append(
|
||||
f"\n### סתירה — {type_label.get(c['fact_type'], c['fact_type'])}: {c['identifier']}"
|
||||
)
|
||||
has_deciding = any(e.get("appraiser_side") == "deciding" for e in c["entries"])
|
||||
header = f"\n### סתירה — {type_label.get(c['fact_type'], c['fact_type'])}: {c['identifier']}"
|
||||
if has_deciding:
|
||||
header += " _(יש שמאי מכריע — עמדתו קובעת)_"
|
||||
lines.append(header)
|
||||
for entry in c["entries"]:
|
||||
side = entry.get("appraiser_side", "") or ""
|
||||
details = entry.get("details") or {}
|
||||
scope = (details.get("scope") or "").strip()
|
||||
status = (details.get("status") or "").strip()
|
||||
quote = (details.get("raw_quote") or "").strip()
|
||||
parts = [f"**{entry['appraiser_name']}**"]
|
||||
marker = "★ " if side == "deciding" else ""
|
||||
parts = [f"**{marker}{_side_label(side)} — {entry['appraiser_name']}**"]
|
||||
if status:
|
||||
parts.append(f"סטטוס: {status}")
|
||||
if scope:
|
||||
|
||||
@@ -491,8 +491,17 @@ CREATE TABLE IF NOT EXISTS appraiser_facts (
|
||||
CREATE INDEX IF NOT EXISTS idx_appraiser_facts_case ON appraiser_facts(case_id, fact_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_appraiser_facts_identifier ON appraiser_facts(case_id, identifier);
|
||||
|
||||
-- V5.1: appraiser_side — which party this appraiser represents.
|
||||
-- Values: 'committee' (הוועדה), 'appellant' (העורר), 'deciding' (מכריע).
|
||||
-- Required by extract_appraiser_facts; the chair tags it via the UI before extraction.
|
||||
-- Set via documents.metadata.appraiser_side at upload/edit time, then propagated here
|
||||
-- so that conflict rendering in block-tet can label each entry with its side.
|
||||
ALTER TABLE appraiser_facts ADD COLUMN IF NOT EXISTS appraiser_side TEXT DEFAULT '';
|
||||
CREATE INDEX IF NOT EXISTS idx_appraiser_facts_side ON appraiser_facts(case_id, appraiser_side);
|
||||
|
||||
-- documents.metadata.is_post_hearing: flag for materials submitted after the hearing
|
||||
-- (השלמות טיעון, הצעות פשרה). Used by block-chet to include them in the proceedings narrative.
|
||||
-- documents.metadata.appraiser_side: which side the appraiser represents (see above).
|
||||
-- No schema change needed — uses existing JSONB metadata column.
|
||||
"""
|
||||
|
||||
@@ -1333,7 +1342,7 @@ async def replace_appraiser_facts(
|
||||
) -> int:
|
||||
"""Replace all appraiser_facts for a given document.
|
||||
|
||||
Each fact dict: appraiser_name, fact_type ('plan'|'permit'),
|
||||
Each fact dict: appraiser_name, appraiser_side, fact_type ('plan'|'permit'),
|
||||
identifier, details (dict), page_number (optional).
|
||||
"""
|
||||
pool = await get_pool()
|
||||
@@ -1345,11 +1354,12 @@ async def replace_appraiser_facts(
|
||||
for f in facts:
|
||||
await conn.execute(
|
||||
"""INSERT INTO appraiser_facts
|
||||
(case_id, document_id, appraiser_name, fact_type,
|
||||
identifier, details, page_number)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)""",
|
||||
(case_id, document_id, appraiser_name, appraiser_side,
|
||||
fact_type, identifier, details, page_number)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)""",
|
||||
case_id, document_id,
|
||||
f["appraiser_name"],
|
||||
f.get("appraiser_side", ""),
|
||||
f["fact_type"],
|
||||
f["identifier"],
|
||||
json.dumps(f.get("details", {}), ensure_ascii=False),
|
||||
@@ -1396,7 +1406,9 @@ async def detect_appraiser_conflicts(case_id: UUID) -> list[dict]:
|
||||
|
||||
A conflict exists when the SAME identifier (e.g., "תמ"א 38") was reported
|
||||
differently by two appraisers — different details, or one cited it and the
|
||||
other did not. Returns list of conflict groups.
|
||||
other did not. Returns list of conflict groups. Each entry in a group
|
||||
carries the appraiser's side so the caller can label it as committee /
|
||||
appellant / deciding.
|
||||
"""
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
@@ -1404,10 +1416,19 @@ async def detect_appraiser_conflicts(case_id: UUID) -> list[dict]:
|
||||
"""SELECT identifier, fact_type,
|
||||
json_agg(jsonb_build_object(
|
||||
'appraiser_name', appraiser_name,
|
||||
'appraiser_side', appraiser_side,
|
||||
'details', details,
|
||||
'page_number', page_number,
|
||||
'document_id', document_id
|
||||
) ORDER BY appraiser_name) AS entries,
|
||||
) ORDER BY
|
||||
CASE appraiser_side
|
||||
WHEN 'committee' THEN 1
|
||||
WHEN 'appellant' THEN 2
|
||||
WHEN 'deciding' THEN 3
|
||||
ELSE 4
|
||||
END,
|
||||
appraiser_name
|
||||
) AS entries,
|
||||
COUNT(DISTINCT appraiser_name) AS n_appraisers
|
||||
FROM appraiser_facts
|
||||
WHERE case_id = $1
|
||||
|
||||
@@ -392,3 +392,98 @@ async def get_claims(case_number: str, party_role: str = "") -> str:
|
||||
})
|
||||
|
||||
return json.dumps(formatted, default=str, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
# Whitelist of doc_type values; mirrors web/app.py:DOC_TYPE_NAMES.
|
||||
ALLOWED_DOC_TYPES = {
|
||||
"appeal", "response", "protocol", "plan", "decision",
|
||||
"court_decision", "permit", "appraisal", "exhibit",
|
||||
"objection", "reference",
|
||||
}
|
||||
|
||||
# Allowed appraiser_side values; '' (empty) clears the tag.
|
||||
ALLOWED_APPRAISER_SIDES = {"committee", "appellant", "deciding", ""}
|
||||
|
||||
|
||||
async def document_update(
|
||||
case_number: str,
|
||||
doc_id: str,
|
||||
doc_type: str = "",
|
||||
appraiser_side: str = "",
|
||||
) -> str:
|
||||
"""עדכון תיוג מסמך — doc_type ו/או appraiser_side. ריק = אין שינוי.
|
||||
|
||||
הולידציה זהה ל-PATCH endpoint ב-web/app.py. appraiser_side נשמר ב-
|
||||
documents.metadata JSONB (מתפרסם משם ע"י extract_appraiser_facts).
|
||||
|
||||
Args:
|
||||
case_number: מספר תיק הערר (לאישור שייכות)
|
||||
doc_id: UUID של המסמך
|
||||
doc_type: ערך חדש (appeal/response/protocol/plan/decision/court_decision/
|
||||
permit/appraisal/exhibit/objection/reference). ריק = אין שינוי.
|
||||
appraiser_side: ערך חדש (committee/appellant/deciding). ריק = אין שינוי;
|
||||
העבר במפורש מחרוזת ריקה לא-default אם רוצים לנקות.
|
||||
"""
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
return json.dumps({"status": "error",
|
||||
"message": f"תיק {case_number} לא נמצא."},
|
||||
ensure_ascii=False, indent=2)
|
||||
|
||||
try:
|
||||
doc_uuid = UUID(doc_id)
|
||||
except ValueError:
|
||||
return json.dumps({"status": "error",
|
||||
"message": f"doc_id לא תקין: {doc_id}"},
|
||||
ensure_ascii=False, indent=2)
|
||||
|
||||
doc = await db.get_document(doc_uuid)
|
||||
if not doc:
|
||||
return json.dumps({"status": "error",
|
||||
"message": f"מסמך {doc_id} לא נמצא."},
|
||||
ensure_ascii=False, indent=2)
|
||||
|
||||
if doc.get("case_id") != case["id"]:
|
||||
return json.dumps({"status": "error",
|
||||
"message": f"מסמך {doc_id} לא שייך לתיק {case_number}."},
|
||||
ensure_ascii=False, indent=2)
|
||||
|
||||
updates: dict = {}
|
||||
|
||||
if doc_type:
|
||||
if doc_type not in ALLOWED_DOC_TYPES:
|
||||
return json.dumps({
|
||||
"status": "error",
|
||||
"message": f"doc_type לא תקין: {doc_type}",
|
||||
"allowed": sorted(ALLOWED_DOC_TYPES),
|
||||
}, ensure_ascii=False, indent=2)
|
||||
updates["doc_type"] = doc_type
|
||||
|
||||
# appraiser_side is optional. The MCP tool can't distinguish "skip" from
|
||||
# "set to empty string", so we use the convention: only update if non-empty.
|
||||
# To clear, the operator must edit metadata directly (rare).
|
||||
if appraiser_side:
|
||||
if appraiser_side not in ALLOWED_APPRAISER_SIDES:
|
||||
return json.dumps({
|
||||
"status": "error",
|
||||
"message": f"appraiser_side לא תקין: {appraiser_side}",
|
||||
"allowed": sorted(s for s in ALLOWED_APPRAISER_SIDES if s),
|
||||
}, ensure_ascii=False, indent=2)
|
||||
metadata = doc.get("metadata") or {}
|
||||
if isinstance(metadata, str):
|
||||
metadata = json.loads(metadata)
|
||||
metadata["appraiser_side"] = appraiser_side
|
||||
updates["metadata"] = metadata
|
||||
|
||||
if not updates:
|
||||
return json.dumps({"status": "noop", "message": "אין שינוי לבצע."},
|
||||
ensure_ascii=False, indent=2)
|
||||
|
||||
await db.update_document(doc_uuid, **updates)
|
||||
fresh = await db.get_document(doc_uuid)
|
||||
return json.dumps({
|
||||
"status": "completed",
|
||||
"doc_id": doc_id,
|
||||
"doc_type": fresh.get("doc_type"),
|
||||
"metadata": fresh.get("metadata"),
|
||||
}, default=str, ensure_ascii=False, indent=2)
|
||||
|
||||
Reference in New Issue
Block a user