Files
ai-contract-platform/backend/tests/test_contracts_api.py
T
yuqei 3df85fdb04 feat: land phase15 legal docs and commercial ops
Includes legal document generation workflows, prompt governance runtime wiring, legal change impact scans, operation KPI dashboards, invoice files, sales follow-up events, mobile review polish, deployment automation, and updated project docs.
2026-05-21 14:42:44 +08:00

860 lines
37 KiB
Python

import unittest
from datetime import datetime, timezone
from fastapi import HTTPException
from fastapi.testclient import TestClient
from app.api.contracts import get_audit_log_service, get_contract_generation_service, get_tenant_service, require_current_user
from app.main import app
from app.services.contract_generation_service import ExportFormat
class StubAuditLogService:
def __init__(self):
self.operations = []
def record_event(self, **kwargs):
self.operations.append(kwargs)
return kwargs
class StubTenantService:
def __init__(self, blocked_action=None):
self.blocked_action = blocked_action
self.calls = []
def assert_billing_allows(self, current_user, action, requested_file_size_bytes=0):
self.calls.append((action, requested_file_size_bytes))
if action == self.blocked_action:
raise HTTPException(status_code=402, detail="quota exceeded")
class StubContractGenerationService:
def _auto_review_summary(self, review_id="review-auto-123", status="completed"):
return {
"review_id": review_id,
"review_title": "Procurement Template",
"status": status,
"review_as": "party_a",
"contract_type": "procurement",
"risk_level": "high",
"current_stage": "report",
"current_stage_label": "Report generated",
"status_detail": "Auto review completed",
"failure_reason": None,
"next_action": "Open review report",
"can_retry": False,
"review_opinion": "High risk terms found",
"recommendation": "manual_review",
"source_file_id": "file-generated-123",
"source_file_name": "contract.pdf",
"source_text_length": 12,
"processing_time_ms": 1200,
"detail_url": f"/reviews/{review_id}",
"report_download_url": f"/api/v1/reviews/{review_id}/report/download",
"report_file_name": f"review-report-{review_id}.pdf",
"chat_url": f"/chat?review={review_id}",
}
def _record(self, record_id, tenant_id="tenant-demo-001", contract_type="procurement", document_type_code="procurement_goods"):
return {
"id": record_id,
"template_id": "tpl-123",
"template_name": "Procurement Template",
"contract_type": contract_type,
"document_type_code": document_type_code,
"generated_content": "Contract with Supplier A",
"generated_html": "<article>Contract with Supplier A</article>",
"variable_values": {"supplier_name": "Supplier A"},
"elements_snapshot": None,
"missing_fields": [],
"ai_hints": [],
"related_legal_articles": [],
"generated_at": "2026-04-17T00:00:00+00:00",
"export_format": "pdf",
"file_name": "contract.pdf",
"file_size": 128,
"preview_url": f"/api/v1/contracts/{record_id}/preview",
"download_url": f"/api/v1/contracts/{record_id}/download",
"auto_review": self._auto_review_summary(f"review-{record_id}"),
"metadata": {
"generated_by": "user-admin",
"generated_by_name": "Admin",
"tenant_id": tenant_id,
"original_template": "Procurement Template",
"document_type_code": document_type_code,
"export_options": {"watermark": True},
"variables_used": 1,
"asset_file_id": "file-generated-123",
"missing_field_count": 0,
"related_legal_article_count": 0,
"generation_source": None,
},
"tenant_id": tenant_id,
}
def list_generated_contracts(self, current_user, page, page_size, contract_type=None, document_type_code=None, keyword=None):
records = [
self._record("contract-123"),
self._record("contract-456", contract_type="employment", document_type_code="labor_contract"),
self._record("contract-other-tenant", tenant_id="tenant-other-001"),
]
tenant_records = [record for record in records if record["tenant_id"] == current_user["tenant_id"]]
if contract_type:
tenant_records = [record for record in tenant_records if record["contract_type"] == contract_type]
if document_type_code:
tenant_records = [record for record in tenant_records if record["document_type_code"] == document_type_code]
if keyword:
normalized_keyword = keyword.strip().lower()
tenant_records = [
record
for record in tenant_records
if normalized_keyword in record["template_name"].lower()
or normalized_keyword in record["file_name"].lower()
or normalized_keyword in record["metadata"]["generated_by_name"].lower()
]
start = (page - 1) * page_size
return {
"items": [{key: value for key, value in item.items() if key != "tenant_id"} for item in tenant_records[start : start + page_size]],
"total": len(tenant_records),
"page": page,
"page_size": page_size,
}
def list_available_templates(self, current_user, contract_type, document_type_code=None, template_category=None, industry_category=None):
return [
{
"id": "tpl-123",
"name": "Procurement Template",
"contract_type": contract_type or "procurement",
"document_type_code": document_type_code or "procurement_goods",
"description": "Standard procurement contract",
"variables": [{"name": "supplier_name", "label": "Supplier", "required": True}],
"preview_excerpt": "Buyer purchases goods from supplier.",
}
]
def preview_contract(self, current_user, template_id, variable_values):
if template_id == "tpl-missing":
raise HTTPException(status_code=404, detail="template not found")
return {
"preview_content": f"Contract with {variable_values.get('supplier_name', '')}",
"template_name": "Procurement Template",
"variable_summary": [
{
"name": "supplier_name",
"label": "Supplier",
"required": True,
"provided": "supplier_name" in variable_values,
"value_preview": variable_values.get("supplier_name", ""),
"is_empty": not bool(variable_values.get("supplier_name", "").strip()),
}
],
}
def get_generated_contract(self, current_user, contract_id):
records = [
item
for item in self.list_generated_contracts(current_user, page=1, page_size=50)["items"]
if item["id"] == contract_id
]
if not records:
raise HTTPException(status_code=404, detail="contract not found")
return records[0]
def list_generated_contract_versions(self, current_user, contract_id):
self.get_generated_contract(current_user, contract_id)
return {
"contract_id": contract_id,
"base_version": {"version_number": 1, "version_label": "v1", "content": "Contract with Supplier A"},
"items": [
{
"id": "gcv-001",
"contract_id": contract_id,
"version_number": 2,
"version_label": "v2",
"content": "Contract with Supplier A. Payment deadline added.",
"locate_status": "applied",
"detail_url": f"/contracts/{contract_id}?version=gcv-001",
}
],
"total": 1,
}
def get_generated_contract_version(self, current_user, contract_id, version_id):
versions = self.list_generated_contract_versions(current_user, contract_id)
for item in versions["items"]:
if item["id"] == version_id:
return item
raise HTTPException(status_code=404, detail="contract version not found")
def create_generated_contract_version(
self,
current_user,
contract_id,
content,
change_type="editor_save",
change_summary=None,
source_version_id=None,
original_text=None,
replacement_text=None,
locate_status="applied",
):
self.get_generated_contract(current_user, contract_id)
return {
"id": "gcv-editor-001",
"tenant_id": current_user["tenant_id"],
"contract_id": contract_id,
"review_id": None,
"review_risk_action_id": None,
"version_number": 3,
"version_label": "v3",
"source_version_id": source_version_id or "gcv-001",
"change_type": change_type,
"change_summary": change_summary or "Editor save",
"original_text": original_text,
"replacement_text": replacement_text,
"content": content,
"locate_status": locate_status,
"created_by": current_user["id"],
"created_at": "2026-05-20T00:00:00+00:00",
"detail_url": f"/contracts/{contract_id}?version=gcv-editor-001",
}
def generate_contract(
self,
current_user,
template_id,
variable_values,
export_format,
options,
document_type_code=None,
elements_snapshot=None,
element_schema_id=None,
element_schema_version_label=None,
fill_missing_variables=False,
):
if template_id == "tpl-missing":
raise HTTPException(status_code=404, detail="template not found")
return {
"id": "contract-123",
"template_id": template_id,
"template_name": "Procurement Template",
"contract_type": "procurement",
"document_type_code": document_type_code or "procurement_goods",
"generated_content": f"Contract with {variable_values.get('supplier_name', '')}",
"generated_html": "<article>Contract</article>",
"variable_values": variable_values,
"elements_snapshot": elements_snapshot,
"missing_fields": [],
"ai_hints": [],
"related_legal_articles": [],
"generated_at": "2026-04-17T00:00:00+00:00",
"export_format": export_format.value,
"file_name": f"contract.{export_format.value}",
"file_size": 128,
"file_content_base64": "ZmlsZQ==",
"preview_url": "/api/v1/contracts/contract-123/preview",
"download_url": "/api/v1/contracts/contract-123/download",
"auto_review": self._auto_review_summary("review-auto-123"),
"metadata": {
"generated_by": current_user["id"],
"generated_by_name": current_user.get("display_name", "Admin"),
"tenant_id": current_user["tenant_id"],
"original_template": "Procurement Template",
"document_type_code": document_type_code or "procurement_goods",
"elements_snapshot": elements_snapshot,
"element_schema_id": element_schema_id,
"element_schema_version_label": element_schema_version_label,
"export_options": options,
"variables_used": len(variable_values),
"missing_field_count": 0,
"related_legal_article_count": 0,
},
}
def generate_contract_from_elements(
self,
current_user,
document_type_code,
elements,
export_format,
options,
template_id=None,
variable_values=None,
):
result = self.generate_contract(
current_user=current_user,
template_id=template_id or "tpl-auto-selected",
variable_values={"supplier_name": (elements.get("party_b") or {}).get("name", "seller"), **(variable_values or {})},
export_format=export_format,
options=options,
document_type_code=document_type_code,
elements_snapshot=elements,
element_schema_id="ldes-auto-v1",
element_schema_version_label="v1.0",
fill_missing_variables=True,
)
result["missing_fields"] = [{"field_key": "payment.terms", "label": "Payment terms", "placeholder": "please fill"}]
result["ai_hints"] = [{"code": "missing_required_fields", "level": "warning", "message": "Fields still need input"}]
result["related_legal_articles"] = [{"source_id": "lart-civil-585", "title": "Civil Code Article 585"}]
result["metadata"]["missing_field_count"] = 1
result["metadata"]["related_legal_article_count"] = 1
result["metadata"]["generation_source"] = {"mode": "structured_elements", "template_selection": "auto"}
return result
def get_generated_contract_preview(self, current_user, contract_id):
if contract_id == "contract-missing":
raise HTTPException(status_code=404, detail="contract not found")
return {
"id": contract_id,
"template_name": "Procurement Template",
"contract_type": "procurement",
"preview_content": "Contract with Supplier A",
"export_format": "pdf",
"file_name": "contract.pdf",
"generated_at": "2026-04-17T00:00:00+00:00",
"preview_url": f"/api/v1/contracts/{contract_id}/preview",
"download_url": f"/api/v1/contracts/{contract_id}/download",
}
def get_generated_contract_download(self, current_user, contract_id):
if contract_id == "contract-missing":
raise HTTPException(status_code=404, detail="contract not found")
return {
"id": contract_id,
"file_name": "contract.pdf",
"mime_type": "application/pdf",
"content": b"pdf-bytes",
}
class ContractsApiTestCase(unittest.TestCase):
def setUp(self):
self.audit_log_service = StubAuditLogService()
self.tenant_service = StubTenantService()
app.dependency_overrides[get_contract_generation_service] = lambda: StubContractGenerationService()
app.dependency_overrides[get_audit_log_service] = lambda: self.audit_log_service
app.dependency_overrides[get_tenant_service] = lambda: self.tenant_service
self.client = TestClient(app)
def tearDown(self):
app.dependency_overrides.clear()
def _login(self, tenant_id="tenant-demo-001"):
app.dependency_overrides[require_current_user] = lambda: {
"id": "user-admin",
"tenant_id": tenant_id,
"display_name": "Admin",
"role": "admin",
}
def test_user_can_list_contract_templates(self):
self._login()
response = self.client.get("/api/v1/contracts/templates", params={"contract_type": "procurement"})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["data"][0]["document_type_code"], "procurement_goods")
def test_user_can_list_generated_contracts_with_filters(self):
self._login()
response = self.client.get("/api/v1/contracts", params={"document_type_code": "labor_contract"})
self.assertEqual(response.status_code, 200)
body = response.json()
self.assertEqual(body["data"]["total"], 1)
self.assertEqual(body["data"]["items"][0]["document_type_code"], "labor_contract")
def test_generated_contracts_are_tenant_isolated(self):
self._login("tenant-other-001")
response = self.client.get("/api/v1/contracts")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["data"]["items"][0]["id"], "contract-other-tenant")
def test_user_can_get_generated_contract_detail(self):
self._login()
response = self.client.get("/api/v1/contracts/contract-123")
self.assertEqual(response.status_code, 200)
body = response.json()
self.assertEqual(body["data"]["id"], "contract-123")
self.assertEqual(body["data"]["metadata"]["document_type_code"], "procurement_goods")
self.assertIn("generated_content", body["data"])
def test_user_can_list_and_get_generated_contract_versions(self):
self._login()
response = self.client.get("/api/v1/contracts/contract-123/versions")
self.assertEqual(response.status_code, 200)
body = response.json()
self.assertEqual(body["data"]["base_version"]["version_label"], "v1")
self.assertEqual(body["data"]["items"][0]["version_label"], "v2")
detail = self.client.get("/api/v1/contracts/contract-123/versions/gcv-001")
self.assertEqual(detail.status_code, 200)
self.assertEqual(detail.json()["data"]["id"], "gcv-001")
def test_user_can_create_generated_contract_version_and_write_audit_log(self):
self._login()
response = self.client.post(
"/api/v1/contracts/contract-123/versions",
json={
"content": "Contract with Supplier A. Acceptance clause added.",
"change_type": "clause_recommendation",
"change_summary": "Apply acceptance clause",
"original_text": "",
"replacement_text": "Acceptance clause added.",
},
)
self.assertEqual(response.status_code, 200)
body = response.json()
self.assertEqual(body["data"]["version_label"], "v3")
self.assertEqual(body["data"]["change_type"], "clause_recommendation")
self.assertEqual(self.audit_log_service.operations[0]["action"], "contract_version_create")
self.assertEqual(self.audit_log_service.operations[0]["detail_json"]["contract_id"], "contract-123")
def test_user_can_generate_contract_and_write_audit_log(self):
self._login()
response = self.client.post(
"/api/v1/contracts/generate",
data={
"template_id": "tpl-123",
"variable_values_json": '{"supplier_name":"Supplier A"}',
"export_format": "pdf",
"options_json": '{"watermark":true}',
},
)
self.assertEqual(response.status_code, 200)
body = response.json()
self.assertEqual(body["data"]["export_format"], "pdf")
self.assertEqual(body["data"]["metadata"]["export_options"], {"watermark": True})
self.assertEqual([call[0] for call in self.tenant_service.calls], ["generation", "review"])
self.assertEqual(self.audit_log_service.operations[0]["action"], "contract_generation")
def test_user_can_generate_contract_from_elements_with_auto_template(self):
self._login()
response = self.client.post(
"/api/v1/contracts/generate",
data={
"document_type_code": "procurement_goods",
"elements_json": '{"party_a":{"name":"buyer"},"party_b":{"name":"seller"},"goods":{"name":"server"}}',
"export_format": "html",
},
)
self.assertEqual(response.status_code, 200)
body = response.json()
self.assertEqual(body["data"]["template_id"], "tpl-auto-selected")
self.assertEqual(body["data"]["missing_fields"][0]["field_key"], "payment.terms")
self.assertEqual(body["data"]["related_legal_articles"][0]["source_id"], "lart-civil-585")
self.assertEqual(self.audit_log_service.operations[0]["detail_json"]["missing_field_count"], 1)
def test_generate_contract_returns_billing_block_message(self):
self._login()
app.dependency_overrides[get_tenant_service] = lambda: StubTenantService(blocked_action="generation")
response = self.client.post(
"/api/v1/contracts/generate",
data={
"template_id": "tpl-123",
"variable_values_json": '{"supplier_name":"Supplier A"}',
"export_format": "pdf",
},
)
self.assertEqual(response.status_code, 402)
def test_user_can_get_preview_download_and_error_responses(self):
self._login()
preview = self.client.post(
"/api/v1/contracts/preview",
data={"template_id": "tpl-123", "variable_values_json": '{"supplier_name":"Supplier A"}'},
)
self.assertEqual(preview.status_code, 200)
self.assertIn("Supplier A", preview.json()["data"]["preview_content"])
download = self.client.get("/api/v1/contracts/contract-123/download")
self.assertEqual(download.status_code, 200)
self.assertEqual(download.content, b"pdf-bytes")
self.assertEqual(self.audit_log_service.operations[-1]["action"], "download_contract")
invalid_json = self.client.post(
"/api/v1/contracts/preview",
data={"template_id": "tpl-123", "variable_values_json": "{bad json}"},
)
self.assertEqual(invalid_json.status_code, 400)
invalid_format = self.client.post(
"/api/v1/contracts/generate",
data={"template_id": "tpl-123", "variable_values_json": '{"supplier_name":"Supplier A"}', "export_format": "xlsx"},
)
self.assertEqual(invalid_format.status_code, 400)
missing_template = self.client.post(
"/api/v1/contracts/generate",
data={"template_id": "tpl-missing", "variable_values_json": '{"supplier_name":"Supplier A"}'},
)
self.assertEqual(missing_template.status_code, 404)
class ContractGenerationServiceTestCase(unittest.TestCase):
def setUp(self):
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.db import Base
from app.models.contract_template import ContractTemplate
from app.models.generated_contract import GeneratedContract
from app.models.legal_article import LegalArticle
from app.models.legal_document_element_schema import LegalDocumentElementSchema
from app.models.legal_document_type import LegalDocumentType
from app.services.contract_generation_service import ContractGenerationService
self.engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(bind=self.engine)
self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
self.db = self.SessionLocal()
self.ContractTemplate = ContractTemplate
self.GeneratedContract = GeneratedContract
self.LegalArticle = LegalArticle
self.LegalDocumentType = LegalDocumentType
self.LegalDocumentElementSchema = LegalDocumentElementSchema
self.service = ContractGenerationService(self.db)
self.service.file_service = type(
"StubFileService",
(),
{
"create_file_record": lambda _self, **kwargs: {
"id": "file-generated-123",
"file_name": kwargs["file_name"],
"mime_type": kwargs["mime_type"],
"file_size": len(kwargs["content"]),
"file_type": kwargs["file_type"],
}
},
)()
class StubReviewService:
def __init__(self):
self.create_calls = []
def create_review_task(self, **kwargs):
self.create_calls.append(kwargs)
return {"id": "review-auto-generated", "status": "completed"}
def get_review_detail(self, current_user, review_id):
return {
"id": review_id,
"contract_type": "procurement",
"review_as": "party_a",
"status": "completed",
"current_stage": "report",
"current_stage_label": "Report generated",
"status_detail": "done",
"workflow_state": {"risk_level": "low"},
"ai_trace": {"status": "completed"},
"source_document": {"file_name": "contract.docx"},
}
def get_review_result(self, current_user, review_id):
return {
"basic_info": {"contract_type": "procurement"},
"risk_summary": {"overall": "low"},
"risk_items": [],
"review_opinion": "ok",
"recommendation": "approved",
}
def get_review_report(self, current_user, review_id):
return {"file_name": f"review-report-{review_id}.pdf", "download_url": f"/api/v1/reviews/{review_id}/report/download"}
self.review_service = StubReviewService()
self.service.review_service = self.review_service
self.current_user = {"id": "user-admin", "tenant_id": "tenant-demo-001", "display_name": "Admin", "role": "admin"}
def tearDown(self):
self.db.close()
self.engine.dispose()
def _seed_law_doc_004_data(self):
now = datetime.now(timezone.utc)
self.db.add(
self.LegalDocumentType(
id="ldt-procurement",
code="procurement_goods",
name="Procurement goods",
category="contract",
industry="trade",
scenario="purchase",
risk_level="medium",
is_active=True,
sort_order=10,
created_at=now,
updated_at=now,
)
)
self.db.add(
self.LegalDocumentElementSchema(
id="ldes-procurement-v1",
document_type_code="procurement_goods",
version_label="v1.0",
schema_json={
"sections": [
{
"key": "base",
"title": "Base",
"fields": [
{"key": "party_a.name", "label": "Buyer", "type": "text", "required": True},
{"key": "party_b.name", "label": "Supplier", "type": "text", "required": True},
{"key": "goods.name", "label": "Goods", "type": "text", "required": True},
{"key": "price.total", "label": "Amount", "type": "money", "required": True},
{"key": "payment.terms", "label": "Payment", "type": "textarea", "required": True},
],
}
]
},
status="published",
published_at=now,
published_by="seed",
created_by="seed",
updated_by="seed",
created_at=now,
updated_at=now,
)
)
self.db.add(
self.ContractTemplate(
id="tpl-elements",
tenant_id="tenant-demo-001",
name="Procurement Template",
contract_type="procurement",
document_type_code="procurement_goods",
description="Element based template",
content="Buyer {{buyer}} buys {{product_name}} from {{supplier}} for {{amount}}. Payment: {{payment_terms}}. Extra: {{missing_required}}.",
variables_json=[
{"name": "buyer", "label": "Buyer", "required": True},
{"name": "supplier", "label": "Supplier", "required": True},
{"name": "product_name", "label": "Goods", "required": True},
{"name": "amount", "label": "Amount", "required": True},
{"name": "payment_terms", "label": "Payment", "required": True},
{"name": "missing_required", "label": "Extra", "required": True},
],
enabled=True,
created_by="seed",
updated_by="seed",
created_at=now,
updated_at=now,
)
)
self.db.add(
self.LegalArticle(
id="lart-civil-585",
tenant_id="tenant-demo-001",
source_id="civil-code",
source_title="中华人民共和国民法典",
source_type="law",
authority="NPC",
region="CN",
article_no="Article 585",
article_text="当事人可以约定一方违约时应当根据违约情况向对方支付一定数额的违约金。",
tags_json=["违约", "买卖合同"],
index_status="indexed",
published_status="published",
published_at=now,
published_by="seed",
created_at=now,
updated_at=now,
)
)
self.db.commit()
def test_generate_contract_accepts_json_column_variable_definitions(self):
now = datetime.now(timezone.utc)
template = self.ContractTemplate(
id="tpl-real-json",
tenant_id="tenant-demo-001",
name="Procurement Template",
contract_type="procurement",
description="JSON variables",
content="Contract with {{supplier_name}}",
variables_json=[{"name": "supplier_name", "label": "Supplier", "required": True}],
enabled=True,
created_by="seed",
updated_by="seed",
created_at=now,
updated_at=now,
)
self.db.add(template)
self.db.commit()
result = self.service.generate_contract(
current_user=self.current_user,
template_id="tpl-real-json",
variable_values={"supplier_name": "Supplier A"},
)
self.assertEqual(result["template_id"], "tpl-real-json")
self.assertEqual(result["variable_values"]["supplier_name"], "Supplier A")
self.assertEqual(self.review_service.create_calls[0]["source_file_id"], "file-generated-123")
self.assertEqual(result["auto_review"]["status"], "completed")
self.assertEqual(
result["metadata"]["prompt_runtime"]["contract_generation"]["prompt_version"],
"contract_generation.template_render.v1",
)
def test_generate_contract_from_elements_auto_selects_template_and_returns_law_doc_004_payload(self):
self._seed_law_doc_004_data()
result = self.service.generate_contract_from_elements(
current_user=self.current_user,
document_type_code="procurement_goods",
elements={
"party_a": {"name": "Buyer Co"},
"party_b": {"name": "Supplier Co"},
"goods": {"name": "Servers"},
"price": {"total": "180000"},
},
export_format=ExportFormat.HTML,
)
self.assertEqual(result["template_id"], "tpl-elements")
self.assertEqual(result["document_type_code"], "procurement_goods")
self.assertIn("missing-fields", result["generated_html"])
self.assertIn("payment.terms", [item["field_key"] for item in result["missing_fields"]])
self.assertIn("missing_required", [item["field_key"] for item in result["missing_fields"]])
self.assertEqual(result["related_legal_articles"][0]["source_id"], "lart-civil-585")
self.assertTrue(result["ai_hints"])
self.assertIn("generated_html", result)
self.assertEqual(result["metadata"]["generation_source"]["template_selection"], "auto")
self.assertEqual(self.review_service.create_calls[-1]["extraction_metadata"]["missing_field_count"], len(result["missing_fields"]))
def test_get_generated_contract_download_allows_owner_access_to_file_payload(self):
generated_contract = type(
"GeneratedContractRecord",
(),
{
"id": "contract-owner-001",
"tenant_id": "tenant-demo-001",
"file_id": "file-owner-001",
"file_name": "contract.txt",
"mime_type": "text/plain",
"file_content_base64": "",
},
)()
captured = {}
self.service.generated_contract_repository = type(
"StubGeneratedContractRepository",
(),
{"get_by_id": lambda _self, contract_id, tenant_id: generated_contract},
)()
self.service.file_service = type(
"StubFileService",
(),
{
"get_download_payload": lambda _self, current_user, file_id, allow_owner_access=False: captured.setdefault(
"payload",
{
"current_user": current_user,
"file_id": file_id,
"allow_owner_access": allow_owner_access,
"file_name": "contract.txt",
"mime_type": "text/plain",
"content": b"contract-bytes",
},
)
},
)()
result = self.service.get_generated_contract_download(
current_user={"id": "user-member", "tenant_id": "tenant-demo-001", "role": "member"},
contract_id="contract-owner-001",
)
self.assertEqual(result["file_name"], "contract.txt")
self.assertTrue(captured["payload"]["allow_owner_access"])
def test_create_generated_contract_version_saves_editor_content(self):
now = datetime.now(timezone.utc)
self.db.add(
self.ContractTemplate(
id="tpl-editor",
tenant_id="tenant-demo-001",
name="Editor Template",
contract_type="procurement",
document_type_code="procurement_goods",
description="Editor template",
content="Original contract content.",
variables_json=[],
enabled=True,
created_by="seed",
updated_by="seed",
created_at=now,
updated_at=now,
)
)
self.db.add(
self.GeneratedContract(
id="contract-editor-001",
tenant_id="tenant-demo-001",
template_id="tpl-editor",
template_name="Editor Template",
contract_type="procurement",
document_type_code="procurement_goods",
generated_by="user-admin",
generated_by_name="Admin",
export_format="docx",
file_id=None,
file_name="editor.docx",
mime_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
file_size=10,
generated_content="Original contract content.",
variable_values_json={},
export_options_json={},
file_content_base64="",
status="generated",
created_at=now,
)
)
self.db.commit()
result = self.service.create_generated_contract_version(
current_user=self.current_user,
contract_id="contract-editor-001",
content="Original contract content.\nAcceptance clause added.",
change_type="clause_recommendation",
change_summary="Apply acceptance clause",
replacement_text="Acceptance clause added.",
)
self.assertEqual(result["version_label"], "v2")
self.assertEqual(result["change_type"], "clause_recommendation")
self.assertIn("Acceptance clause added", result["content"])
def test_create_generated_contract_version_rejects_unchanged_content(self):
now = datetime.now(timezone.utc)
self.db.add(
self.GeneratedContract(
id="contract-editor-unchanged",
tenant_id="tenant-demo-001",
template_id="tpl-editor",
template_name="Editor Template",
contract_type="procurement",
document_type_code="procurement_goods",
generated_by="user-admin",
generated_by_name="Admin",
export_format="docx",
file_id=None,
file_name="editor.docx",
mime_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
file_size=10,
generated_content="Original contract content.",
variable_values_json={},
export_options_json={},
file_content_base64="",
status="generated",
created_at=now,
)
)
self.db.commit()
with self.assertRaises(HTTPException) as ctx:
self.service.create_generated_contract_version(
current_user=self.current_user,
contract_id="contract-editor-unchanged",
content="Original contract content.",
)
self.assertEqual(ctx.exception.status_code, 409)
if __name__ == "__main__":
unittest.main()