Files
ai-contract-platform/backend/app/services/template_service.py
T

1446 lines
78 KiB
Python

import json
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Optional
from uuid import uuid4
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from app.models.contract_template import ContractTemplate
from app.models.contract_template_version import ContractTemplateVersion
from app.repositories.governance_asset_repository import GovernanceAssetRepository
from app.repositories.contract_template_repository import ContractTemplateRepository
from app.repositories.contract_template_version_repository import ContractTemplateVersionRepository
TEMPLATE_LIBRARY_ROOT = Path(__file__).resolve().parents[3] / "project-docs" / "phase13-legal-knowledge-template-library" / "contract-templates"
def _extract_contract_body(markdown_text: str) -> str:
lines = markdown_text.splitlines()
try:
start = lines.index("## 合同正文") + 1
except ValueError:
start = 0
try:
end = lines.index("## 使用边界")
except ValueError:
end = len(lines)
return "\n".join(lines[start:end]).strip()
def _load_phase13_template_asset(
folder_name: str,
template_code: str,
*,
name: str,
contract_type: str,
document_type_code: str,
template_category: str,
industry_category: str,
description: str,
) -> dict[str, Any]:
asset_dir = TEMPLATE_LIBRARY_ROOT / folder_name
markdown = (asset_dir / f"{template_code}.md").read_text(encoding="utf-8-sig")
variables_data = json.loads((asset_dir / f"{template_code}-variables.json").read_text(encoding="utf-8-sig"))
required_variables = variables_data.get("required_variables", [])
optional_variables = variables_data.get("optional_variables", [])
variables = [
{"name": variable_name, "label": variable_name, "required": True}
for variable_name in required_variables
] + [
{"name": variable_name, "label": variable_name, "required": False}
for variable_name in optional_variables
]
return {
"name": name,
"contract_type": contract_type,
"document_type_code": document_type_code,
"template_category": template_category,
"industry_category": industry_category,
"description": description,
"content": _extract_contract_body(markdown),
"variables": variables,
}
DEFAULT_TEMPLATES: list[dict[str, Any]] = [
{
"name": "劳动合同标准模板",
"contract_type": "labor",
"document_type_code": "labor_contract",
"template_category": "employment-hr",
"industry_category": "general-enterprise",
"description": "适用于企业与员工签署的标准劳动合同。",
"content": (
"甲方(用人单位):{{party_a}}\n"
"乙方(员工):{{party_b}}\n\n"
"一、合同期限\n"
"本合同自 {{start_date}} 起至 {{end_date}} 止。\n\n"
"二、工作岗位\n"
"乙方在甲方安排的 {{job_title}} 岗位工作,工作地点为 {{work_location}}。\n\n"
"三、劳动报酬\n"
"甲方每月向乙方支付税前工资人民币 {{salary}} 元,发薪日为每月 {{payday}} 日。\n\n"
"四、签署日期\n"
"{{sign_date}}"
),
"variables": [
{"name": "party_a", "label": "甲方名称"},
{"name": "party_b", "label": "乙方姓名"},
{"name": "start_date", "label": "开始日期"},
{"name": "end_date", "label": "结束日期"},
{"name": "job_title", "label": "岗位名称"},
{"name": "work_location", "label": "工作地点"},
{"name": "salary", "label": "月工资"},
{"name": "payday", "label": "发薪日"},
{"name": "sign_date", "label": "签署日期"},
],
},
{
"name": "货物采购合同模板",
"contract_type": "procurement",
"document_type_code": "procurement_goods",
"template_category": "procurement-supply",
"industry_category": "general-enterprise",
"description": "适用于普通 B2B 货物采购、交付、验收、发票付款、质保售后和违约处理场景。",
"content": (
"甲方(采购方):{{buyer}}\n"
"乙方(供应方):{{supplier}}\n\n"
"一、采购标的与附件\n"
"产品名称:{{product_name}}\n"
"规格型号:{{specification}}\n"
"采购数量:{{quantity}} {{unit}}\n"
"单价:{{unit_price}}\n"
"合同价款:{{amount}}\n"
"质量标准:{{quality_standard}}\n"
"订单/附件编号:{{order_attachment_no}}\n\n"
"二、包装、运输、交付与风险转移\n"
"包装及运输要求:{{packaging_transport}}\n"
"交付期限:{{delivery_deadline}}\n"
"交付地点:{{delivery_location}}\n"
"风险转移节点:{{risk_transfer}}\n"
"随附文件:{{delivery_documents}}\n"
"分批交付安排:{{batch_delivery}}\n\n"
"三、验收与质量异议\n"
"验收标准:{{acceptance_standard}}\n"
"验收期限:{{acceptance_period}}\n\n"
"四、发票、收款账户与付款\n"
"发票类型:{{invoice_type}}\n"
"付款安排:{{payment_terms}}\n"
"付款前置条件:{{payment_conditions}}\n"
"收款账户:{{payee_account}}\n\n"
"五、质量保证与售后\n"
"质量保证期:{{warranty_period}}\n"
"售后响应要求:{{after_sales_response}}\n\n"
"六、违约、解除与通知\n"
"违约责任:{{breach_liability}}\n"
"违约金计算口径:{{liquidated_damages_formula}}\n"
"责任上限:{{liability_cap}}\n"
"解除触发条件:{{rescission_trigger}}\n"
"通知方式及送达地址:{{notice_address}}\n\n"
"七、争议解决与电子签署\n"
"争议解决方式:{{dispute_resolution}}\n"
"电子签署平台:{{e_signature_platform}}\n"
"特殊监管说明:{{regulatory_note}}\n"
"签署日期:{{sign_date}}"
),
"variables": [
{"name": "buyer", "label": "采购方名称"},
{"name": "supplier", "label": "供应方名称"},
{"name": "product_name", "label": "产品名称"},
{"name": "specification", "label": "规格型号"},
{"name": "quantity", "label": "采购数量"},
{"name": "unit", "label": "计量单位"},
{"name": "unit_price", "label": "单价"},
{"name": "amount", "label": "合同金额"},
{"name": "quality_standard", "label": "质量标准"},
{"name": "packaging_transport", "label": "包装及运输要求"},
{"name": "delivery_deadline", "label": "交付期限"},
{"name": "delivery_location", "label": "交付地点"},
{"name": "risk_transfer", "label": "风险转移节点"},
{"name": "delivery_documents", "label": "随附文件清单"},
{"name": "acceptance_standard", "label": "验收标准"},
{"name": "acceptance_period", "label": "验收期限"},
{"name": "invoice_type", "label": "发票类型"},
{"name": "payment_terms", "label": "付款安排"},
{"name": "payee_account", "label": "收款账户"},
{"name": "warranty_period", "label": "质量保证期"},
{"name": "after_sales_response", "label": "售后响应要求"},
{"name": "breach_liability", "label": "违约责任"},
{"name": "dispute_resolution", "label": "争议解决方式"},
{"name": "sign_date", "label": "签署日期"},
{"name": "order_attachment_no", "label": "订单/附件编号", "required": False},
{"name": "batch_delivery", "label": "分批交付安排", "required": False},
{"name": "payment_conditions", "label": "付款前置条件", "required": False},
{"name": "liquidated_damages_formula", "label": "违约金计算口径", "required": False},
{"name": "liability_cap", "label": "责任上限", "required": False},
{"name": "rescission_trigger", "label": "解除触发条件", "required": False},
{"name": "notice_address", "label": "通知方式及送达地址", "required": False},
{"name": "e_signature_platform", "label": "电子签署平台", "required": False},
{"name": "regulatory_note", "label": "特殊监管说明", "required": False},
],
},
{
"name": "通用服务合同模板",
"contract_type": "service",
"document_type_code": "service_general",
"template_category": "service-delivery",
"industry_category": "professional-services",
"description": "适用于普通 B2B 咨询、运营、外包、执行支持等通用服务交付场景;涉及个人信息、重要数据或强监管服务时需专项复核。",
"content": (
"甲方(委托方/服务接受方):{{buyer}}\n"
"乙方(服务方):{{supplier}}\n\n"
"一、服务名称、范围与排除事项\n"
"服务名称:{{service_name}}\n"
"服务范围:{{service_scope}}\n"
"交付成果:{{deliverables}}\n"
"不包含事项:{{service_exclusions}}\n\n"
"二、服务期限、人员安排与服务级别\n"
"服务期限:{{service_period}}\n"
"服务级别:{{service_level}}\n"
"响应时效:{{service_response_time}}\n\n"
"三、费用、结算与发票\n"
"服务费用:{{service_fee}}\n"
"付款安排:{{payment_terms}}\n"
"发票类型:{{invoice_type}}\n"
"付款前置条件:{{payment_conditions}}\n\n"
"四、验收、整改与变更\n"
"验收标准:{{acceptance_standard}}\n"
"验收期限:{{acceptance_period}}\n"
"服务不符合约定的,应在合理期限内整改;整改后仍不符合要求的,甲方可要求减免费用、继续履行、赔偿损失或解除合同。\n\n"
"五、数据、保密与合规\n"
"数据接触范围:{{data_scope}}\n"
"保密范围:{{confidentiality_scope}}\n"
"服务期间如涉及个人信息、重要数据或受监管数据的,应另行完成数据合规专项复核。\n\n"
"六、违约责任、责任限制与解除\n"
"违约责任:{{breach_liability}}\n"
"违约金计算口径:{{liquidated_damages_formula}}\n"
"责任上限:{{liability_cap}}\n"
"解除触发条件:{{rescission_trigger}}\n"
"通知方式及送达地址:{{notice_address}}\n\n"
"七、争议解决、电子签署与特殊监管\n"
"争议解决方式:{{dispute_resolution}}\n"
"电子签署平台:{{e_signature_platform}}\n"
"特殊监管说明:{{regulatory_note}}\n"
"签署日期:{{sign_date}}"
),
"variables": [
{"name": "buyer", "label": "甲方名称"},
{"name": "supplier", "label": "乙方名称"},
{"name": "service_name", "label": "服务名称"},
{"name": "service_scope", "label": "服务范围"},
{"name": "deliverables", "label": "交付成果"},
{"name": "service_exclusions", "label": "不包含事项", "required": False},
{"name": "service_period", "label": "服务期限"},
{"name": "service_level", "label": "服务级别", "required": False},
{"name": "service_response_time", "label": "响应时效", "required": False},
{"name": "service_fee", "label": "服务费用"},
{"name": "payment_terms", "label": "付款安排"},
{"name": "invoice_type", "label": "发票类型", "required": False},
{"name": "payment_conditions", "label": "付款前置条件", "required": False},
{"name": "acceptance_standard", "label": "验收标准"},
{"name": "acceptance_period", "label": "验收期限", "required": False},
{"name": "data_scope", "label": "数据接触范围"},
{"name": "confidentiality_scope", "label": "保密范围"},
{"name": "breach_liability", "label": "违约责任"},
{"name": "liquidated_damages_formula", "label": "违约金计算口径", "required": False},
{"name": "liability_cap", "label": "责任上限", "required": False},
{"name": "rescission_trigger", "label": "解除触发条件", "required": False},
{"name": "notice_address", "label": "通知方式及送达地址", "required": False},
{"name": "dispute_resolution", "label": "争议解决方式"},
{"name": "e_signature_platform", "label": "电子签署平台", "required": False},
{"name": "regulatory_note", "label": "特殊监管说明", "required": False},
{"name": "sign_date", "label": "签署日期"},
],
},
{
"name": "保密协议标准模板",
"contract_type": "nda",
"document_type_code": "nda_commercial",
"template_category": "commercial-compliance",
"industry_category": "general-enterprise",
"description": "适用于商务合作、尽调和方案评估场景的保密协议。",
"content": (
"甲方(披露方):{{disclosing_party}}\n"
"乙方(接收方):{{receiving_party}}\n\n"
"一、合作事项\n"
"合作事项:{{cooperation_matter}}\n\n"
"二、保密信息范围与披露对象\n"
"保密信息范围:{{confidential_scope}}\n"
"允许披露对象:{{permitted_disclosure}}\n\n"
"三、使用目的和例外信息\n"
"使用目的:{{use_purpose}}\n"
"例外信息:{{exceptions}}\n\n"
"四、保密期限\n"
"保密期限:{{confidential_term}}\n\n"
"五、保密措施\n"
"{{protection_measures}}\n\n"
"六、返还、销毁和副本管理\n"
"{{return_or_destroy}}\n\n"
"七、知识产权及衍生资料\n"
"{{ip_and_derivative_rights}}\n\n"
"八、违约责任\n"
"{{liability}}\n\n"
"九、争议解决和电子签署\n"
"争议解决方式:{{dispute_forum}}\n"
"电子签署平台:{{e_signature_platform}}\n\n"
"十、签署日期\n"
"{{sign_date}}"
),
"variables": [
{"name": "disclosing_party", "label": "披露方"},
{"name": "receiving_party", "label": "接收方"},
{"name": "cooperation_matter", "label": "合作事项"},
{"name": "confidential_scope", "label": "保密范围"},
{"name": "permitted_disclosure", "label": "允许披露对象"},
{"name": "use_purpose", "label": "使用目的"},
{"name": "exceptions", "label": "例外信息"},
{"name": "confidential_term", "label": "保密期限"},
{"name": "protection_measures", "label": "保密措施"},
{"name": "return_or_destroy", "label": "返还或销毁方式"},
{"name": "ip_and_derivative_rights", "label": "知识产权及衍生资料归属"},
{"name": "liability", "label": "违约责任"},
{"name": "dispute_forum", "label": "争议解决方式"},
{"name": "e_signature_platform", "label": "电子签署平台"},
{"name": "sign_date", "label": "签署日期"},
],
},
{
"name": "投资协议模板",
"contract_type": "investment_agreement",
"document_type_code": "investment_agreement",
"template_category": "investment-finance",
"industry_category": "general-enterprise",
"description": "适用于股权投资、增资扩股、交割安排、治理权利、信息披露和退出机制场景。",
"content": (
"投资方:{{investor}}\n"
"目标公司:{{target_company}}\n"
"创始股东/实际控制人:{{controller}}\n"
"基础交易文件:{{base_agreement}}\n\n"
"一、投资安排\n"
"投资金额:{{investment_amount}}\n"
"投前/投后估值依据:{{valuation_basis}}\n"
"对应股权比例:{{equity_ratio}}\n"
"资金用途:{{use_of_funds}}\n\n"
"二、公司治理与股东权利\n"
"董事席位/观察员安排:{{board_rights}}\n"
"信息披露与检查权:{{information_rights}}\n"
"重大事项表决机制:{{veto_rights}}\n\n"
"三、交割与先决条件\n"
"交割先决条件:{{closing_conditions}}\n"
"公司及创始股东陈述与保证:{{representations_warranties}}\n"
"监管/审批事项:{{regulatory_note}}\n\n"
"四、股权转让与退出机制\n"
"股权转让限制:{{transfer_restrictions}}\n"
"退出安排:{{exit_mechanism}}\n\n"
"五、争议解决与签署\n"
"争议解决方式:{{dispute_resolution}}\n"
"电子签署平台:{{e_signature_platform}}\n"
"签署日期:{{sign_date}}"
),
"variables": [
{"name": "investor", "label": "投资方名称"},
{"name": "target_company", "label": "目标公司名称"},
{"name": "controller", "label": "创始股东/实际控制人"},
{"name": "base_agreement", "label": "基础交易文件"},
{"name": "investment_amount", "label": "投资金额"},
{"name": "valuation_basis", "label": "投前/投后估值依据"},
{"name": "equity_ratio", "label": "对应股权比例"},
{"name": "use_of_funds", "label": "资金用途"},
{"name": "board_rights", "label": "董事席位/观察员安排"},
{"name": "information_rights", "label": "信息披露与检查权"},
{"name": "veto_rights", "label": "重大事项表决机制", "required": False},
{"name": "closing_conditions", "label": "交割先决条件"},
{"name": "representations_warranties", "label": "陈述与保证"},
{"name": "regulatory_note", "label": "监管/审批事项", "required": False},
{"name": "transfer_restrictions", "label": "股权转让限制"},
{"name": "exit_mechanism", "label": "退出安排"},
{"name": "dispute_resolution", "label": "争议解决方式"},
{"name": "e_signature_platform", "label": "电子签署平台", "required": False},
{"name": "sign_date", "label": "签署日期"},
],
},
{
"name": "企业间借款合同模板",
"contract_type": "intercompany-loan",
"document_type_code": "intercompany_loan_agreement",
"template_category": "investment-finance",
"industry_category": "general-enterprise",
"description": "适用于企业间借款、关联交易、利率、还款、担保和资金合规场景。",
"content": (
"甲方(出借方):{{lender}}\n"
"乙方(借款方):{{borrower}}\n"
"关联关系说明:{{relationship_note}}\n"
"内部审批确认:{{internal_approval}}\n\n"
"一、借款金额与用途\n"
"借款金额:{{loan_amount}}\n"
"借款用途:{{loan_purpose}}\n"
"资金划付账户:{{disbursement_account}}\n"
"资金到账确认:{{receipt_confirmation}}\n\n"
"二、借款期限与利息\n"
"借款期限:{{loan_term}}\n"
"起息日:{{interest_start_date}}\n"
"年化利率/计息方式:{{interest_rate_method}}\n"
"利息支付方式:{{interest_payment_method}}\n\n"
"三、还款与担保\n"
"还款方式:{{repayment_method}}\n"
"还款计划:{{repayment_plan}}\n"
"还款账户:{{repayment_account}}\n"
"担保方式:{{guarantee_method}}\n"
"担保文件:{{guarantee_documents}}\n\n"
"四、合规、违约与争议解决\n"
"资金用途限制说明:{{fund_usage_restriction}}\n"
"违约责任:{{breach_liability}}\n"
"争议解决方式:{{dispute_resolution}}\n"
"电子签署平台:{{e_signature_platform}}\n"
"签署日期:{{sign_date}}"
),
"variables": [
{"name": "lender", "label": "甲方名称"},
{"name": "borrower", "label": "乙方名称"},
{"name": "relationship_note", "label": "关联关系说明"},
{"name": "internal_approval", "label": "内部审批确认"},
{"name": "loan_amount", "label": "借款金额"},
{"name": "loan_purpose", "label": "借款用途"},
{"name": "disbursement_account", "label": "资金划付账户"},
{"name": "receipt_confirmation", "label": "资金到账确认"},
{"name": "loan_term", "label": "借款期限"},
{"name": "interest_start_date", "label": "起息日"},
{"name": "interest_rate_method", "label": "年化利率/计息方式"},
{"name": "interest_payment_method", "label": "利息支付方式"},
{"name": "repayment_method", "label": "还款方式"},
{"name": "repayment_plan", "label": "还款计划"},
{"name": "repayment_account", "label": "还款账户"},
{"name": "guarantee_method", "label": "担保方式"},
{"name": "guarantee_documents", "label": "担保文件", "required": False},
{"name": "fund_usage_restriction", "label": "资金用途限制说明", "required": False},
{"name": "breach_liability", "label": "违约责任", "required": False},
{"name": "dispute_resolution", "label": "争议解决方式"},
{"name": "e_signature_platform", "label": "电子签署平台", "required": False},
{"name": "sign_date", "label": "签署日期"},
],
},
{
"name": "对赌协议模板",
"contract_type": "valuation-adjustment",
"document_type_code": "valuation_adjustment_agreement",
"template_category": "investment-finance",
"industry_category": "general-enterprise",
"description": "适用于投融资中的业绩承诺、估值调整、补偿和回购安排场景。",
"content": (
"投资方:{{investor}}\n"
"目标公司:{{target_company}}\n"
"原股东/实际控制人:{{controller}}\n"
"基础投资协议名称:{{base_agreement}}\n\n"
"一、交易背景与估值调整目标\n"
"估值调整目标:{{valuation_adjustment_target}}\n"
"投资金额:{{investment_amount}}\n"
"投前/投后估值:{{valuation_before_after}}\n\n"
"二、业绩承诺与考核期间\n"
"考核期间:{{assessment_period}}\n"
"业绩承诺指标:{{performance_metric}}\n"
"指标确认方式:{{metric_confirmation_method}}\n"
"审计机构:{{audit_org}}\n\n"
"三、补偿与回购安排\n"
"补偿触发条件:{{compensation_trigger}}\n"
"补偿方式:{{compensation_method}}\n"
"补偿计算公式:{{compensation_formula}}\n"
"补偿履行期限:{{compensation_deadline}}\n"
"补偿上限/下限:{{compensation_cap_floor}}\n"
"回购安排:{{repurchase_arrangement}}\n\n"
"四、公司履行限制与合规提示\n"
"公司履行限制说明:{{company_performance_restriction}}\n"
"担保/增信安排:{{guarantee_enhancement}}\n"
"外资/国资/反垄断说明:{{regulatory_note}}\n\n"
"五、信息披露与审计权\n"
"信息披露与审计权:{{disclosure_audit_right}}\n"
"争议解决方式:{{dispute_resolution}}\n"
"电子签署平台:{{e_signature_platform}}\n"
"签署日期:{{sign_date}}"
),
"variables": [
{"name": "investor", "label": "投资方名称"},
{"name": "target_company", "label": "目标公司名称"},
{"name": "controller", "label": "原股东/实际控制人名称"},
{"name": "base_agreement", "label": "基础投资协议名称"},
{"name": "valuation_adjustment_target", "label": "估值调整目标"},
{"name": "investment_amount", "label": "投资金额", "required": False},
{"name": "valuation_before_after", "label": "投前/投后估值", "required": False},
{"name": "assessment_period", "label": "考核期间"},
{"name": "performance_metric", "label": "业绩承诺指标"},
{"name": "metric_confirmation_method", "label": "指标确认方式"},
{"name": "audit_org", "label": "审计机构", "required": False},
{"name": "compensation_trigger", "label": "补偿触发条件"},
{"name": "compensation_method", "label": "补偿方式"},
{"name": "compensation_formula", "label": "补偿计算公式"},
{"name": "compensation_deadline", "label": "补偿履行期限"},
{"name": "compensation_cap_floor", "label": "补偿上限/下限", "required": False},
{"name": "repurchase_arrangement", "label": "回购安排", "required": False},
{"name": "company_performance_restriction", "label": "公司履行限制说明"},
{"name": "guarantee_enhancement", "label": "担保/增信安排", "required": False},
{"name": "regulatory_note", "label": "外资/国资/反垄断说明", "required": False},
{"name": "disclosure_audit_right", "label": "信息披露与审计权"},
{"name": "dispute_resolution", "label": "争议解决方式"},
{"name": "e_signature_platform", "label": "电子签署平台", "required": False},
{"name": "sign_date", "label": "签署日期"},
],
},
{
"name": "股东会决议模板",
"contract_type": "shareholder-resolution",
"document_type_code": "shareholder_resolution",
"template_category": "corporate-governance",
"industry_category": "general-enterprise",
"description": "适用于股东会/股东大会决议、授权办理和登记备案场景。",
"content": (
"公司名称:{{company_name}}\n"
"统一社会信用代码:{{company_uscc}}\n"
"会议类型:{{meeting_type}}\n"
"会议届次/决议编号:{{meeting_no}}\n"
"会议时间:{{meeting_time}}\n"
"会议地点/表决方式:{{meeting_place_or_method}}\n"
"召集人:{{meeting_convener}}\n"
"主持人:{{meeting_host}}\n\n"
"一、通知与出席情况\n"
"通知发送日期及方式:{{notice_delivery}}\n"
"应到股东及表决权比例:{{attendance_expected_ratio}}\n"
"实到/参与表决股东及表决权比例:{{attendance_actual_ratio}}\n"
"股东名单附件:{{attendance_shareholder_list}}\n\n"
"二、审议议案与表决结果\n"
"审议议案:{{proposal_items}}\n"
"表决结果:{{vote_result}}\n"
"决议事项:{{resolution_items}}\n"
"董事/监事/高管任免名单:{{management_appointments}}\n"
"注册资本/经营范围/住所变更事项:{{registration_change_items}}\n"
"异议股东意见:{{dissenting_opinion}}\n\n"
"三、授权办理与材料留存\n"
"授权办理事项:{{authorization_items}}\n"
"授权代表/经办人:{{authorization_representative}}\n"
"工商变更材料清单:{{filing_materials}}\n"
"文件留存清单:{{evidence_retention_list}}\n"
"公证/见证安排:{{notary_arrangement}}\n"
"电子签署平台:{{e_signature_platform}}\n"
"签署日期:{{sign_date}}"
),
"variables": [
{"name": "company_name", "label": "公司名称"},
{"name": "company_uscc", "label": "统一社会信用代码"},
{"name": "meeting_type", "label": "会议类型"},
{"name": "meeting_no", "label": "会议届次/决议编号"},
{"name": "meeting_time", "label": "会议时间"},
{"name": "meeting_place_or_method", "label": "会议地点/表决方式"},
{"name": "meeting_convener", "label": "召集人"},
{"name": "meeting_host", "label": "主持人"},
{"name": "notice_delivery", "label": "通知发送日期及方式"},
{"name": "attendance_expected_ratio", "label": "应到股东及表决权比例"},
{"name": "attendance_actual_ratio", "label": "实到/参与表决股东及表决权比例"},
{"name": "proposal_items", "label": "审议议案"},
{"name": "vote_result", "label": "表决结果"},
{"name": "resolution_items", "label": "决议事项"},
{"name": "authorization_items", "label": "授权办理事项"},
{"name": "authorization_representative", "label": "授权代表/经办人"},
{"name": "evidence_retention_list", "label": "文件留存清单"},
{"name": "sign_date", "label": "签署日期"},
{"name": "attendance_shareholder_list", "label": "股东名单附件", "required": False},
{"name": "management_appointments", "label": "董事/监事/高管任免名单", "required": False},
{"name": "registration_change_items", "label": "注册资本/经营范围/住所变更事项", "required": False},
{"name": "dissenting_opinion", "label": "异议股东意见", "required": False},
{"name": "filing_materials", "label": "工商变更材料清单", "required": False},
{"name": "notary_arrangement", "label": "公证/见证安排", "required": False},
{"name": "e_signature_platform", "label": "电子签署平台", "required": False},
],
},
{
"name": "技术服务合同模板",
"contract_type": "technical_service",
"document_type_code": "technical_service",
"template_category": "service-delivery",
"industry_category": "technology",
"description": "适用于技术支持、系统实施、接口调试、运维保障和成果交付场景。",
"content": (
"# 技术服务合同\n\n"
"甲方:{{甲方名称}}\n\n"
"乙方:{{乙方名称}}\n\n"
"## 一、服务项目和范围\n\n"
"服务项目名称:{{服务项目名称}}\n\n"
"服务范围:{{服务范围}}\n\n"
"交付成果:{{交付成果}}\n\n"
"## 二、服务期限、验收和费用\n\n"
"服务期限:{{服务期限}}\n\n"
"验收标准:{{验收标准}}\n\n"
"验收日期:{{验收日期}}\n\n"
"服务费用:{{服务费用}}\n\n"
"## 三、知识产权、保密和争议解决\n\n"
"知识产权/成果使用安排:{{知识产权/成果使用安排}}\n\n"
"保密要求:{{保密要求}}\n\n"
"争议解决方式:{{争议解决方式}}\n\n"
"电子签署平台:{{电子签署平台}}\n\n"
"签署日期:{{签署日期}}"
),
"variables": [
{"name": "甲方名称", "label": "甲方名称"},
{"name": "乙方名称", "label": "乙方名称"},
{"name": "服务项目名称", "label": "服务项目名称"},
{"name": "服务范围", "label": "服务范围"},
{"name": "交付成果", "label": "交付成果"},
{"name": "服务期限", "label": "服务期限"},
{"name": "验收标准", "label": "验收标准"},
{"name": "验收日期", "label": "验收日期", "required": False},
{"name": "服务费用", "label": "服务费用"},
{"name": "知识产权/成果使用安排", "label": "知识产权/成果使用安排", "required": False},
{"name": "保密要求", "label": "保密要求", "required": False},
{"name": "争议解决方式", "label": "争议解决方式"},
{"name": "电子签署平台", "label": "电子签署平台", "required": False},
{"name": "签署日期", "label": "签署日期"},
],
},
_load_phase13_template_asset(
"technology-development",
"TPL-TECH-DEVELOPMENT-001",
name="软件技术开发合同模板",
contract_type="software_development",
document_type_code="software_development",
template_category="service-delivery",
industry_category="technology",
description="适用于软件定制开发、交付、验收、知识产权和维护场景。",
),
_load_phase13_template_asset(
"lease",
"TPL-LEASE-HOUSE-001",
name="房屋租赁合同模板",
contract_type="lease_house",
document_type_code="lease_house",
template_category="real-estate",
industry_category="real-estate",
description="适用于办公、商业或居住房屋租赁场景。",
),
_load_phase13_template_asset(
"equity-transfer",
"TPL-EQUITY-TRANSFER-001",
name="股权转让协议模板",
contract_type="equity_transfer",
document_type_code="equity_transfer",
template_category="investment-finance",
industry_category="general-enterprise",
description="适用于有限责任公司股权转让、价款支付和工商变更场景。",
),
_load_phase13_template_asset(
"cooperation",
"TPL-COOPERATION-FRAMEWORK-001",
name="合作框架协议模板",
contract_type="cooperation_framework",
document_type_code="cooperation_framework",
template_category="commercial-cooperation",
industry_category="general-enterprise",
description="适用于双方建立长期合作意向和原则性约定场景。",
),
_load_phase13_template_asset(
"demand-letter",
"TPL-DEMAND-LETTER-001",
name="催告函模板",
contract_type="demand_letter",
document_type_code="demand_letter",
template_category="notices-letters",
industry_category="general-enterprise",
description="适用于付款、交付、整改等履约催告场景。",
),
_load_phase13_template_asset(
"termination-notice",
"TPL-CONTRACT-TERMINATION-NOTICE-001",
name="解除合同通知书模板",
contract_type="rescission_notice",
document_type_code="rescission_notice",
template_category="notices-letters",
industry_category="general-enterprise",
description="适用于依法或依约解除合同并保留证据的通知场景。",
),
_load_phase13_template_asset(
"power-of-attorney",
"TPL-POWER-OF-ATTORNEY-001",
name="授权委托书模板",
contract_type="power_of_attorney",
document_type_code="power_of_attorney",
template_category="corporate-governance",
industry_category="general-enterprise",
description="适用于企业授权、委托代理、签章和登记备案场景。",
),
_load_phase13_template_asset(
"legal-opinion",
"TPL-LEGAL-OPINION-001",
name="法律意见书模板",
contract_type="legal_opinion",
document_type_code="legal_opinion",
template_category="commercial-compliance",
industry_category="general-enterprise",
description="适用于企业法律意见、尽职调查、登记备案、融资和合规事项专业文书场景。",
),
_load_phase13_template_asset(
"property-service",
"TPL-PROPERTY-SERVICE-001",
name="物业服务合同模板",
contract_type="property_service",
document_type_code="property_service",
template_category="real-estate",
industry_category="real-estate",
description="适用于住宅、商办园区和产业园区的物业服务、收费、维修资金、安全协助和资料交接场景。",
),
]
class TemplateService:
def __init__(self, db: Session):
self.db = db
self.repository = ContractTemplateRepository(db) if db is not None else None
self.version_repository = ContractTemplateVersionRepository(db) if db is not None else None
self.governance_asset_repository = GovernanceAssetRepository(db) if db is not None else None
def list_templates(
self,
current_user: Dict[str, Any],
page: int = 1,
page_size: int = 20,
contract_type: Optional[str] = None,
document_type_code: Optional[str] = None,
template_category: Optional[str] = None,
industry_category: Optional[str] = None,
) -> dict:
self._require_admin(current_user)
repository = self._require_repository()
tenant_id = current_user["tenant_id"]
normalized_page = max(page, 1)
normalized_page_size = max(min(page_size, 100), 1)
normalized_contract_type = contract_type.strip() if contract_type else None
normalized_document_type_code = self._normalize_document_type_code(document_type_code, None) if document_type_code else None
normalized_template_category = template_category.strip() if template_category else None
normalized_industry_category = industry_category.strip() if industry_category else None
items = repository.list_paginated(
tenant_id=tenant_id,
page=normalized_page,
page_size=normalized_page_size,
contract_type=normalized_contract_type,
template_category=normalized_template_category,
industry_category=normalized_industry_category,
document_type_code=normalized_document_type_code,
)
return {
"items": [self._to_item(item) for item in items],
"page": normalized_page,
"page_size": normalized_page_size,
"total": repository.count(
tenant_id=tenant_id,
contract_type=normalized_contract_type,
template_category=normalized_template_category,
industry_category=normalized_industry_category,
document_type_code=normalized_document_type_code,
),
}
def get_template_versions(self, current_user: Dict[str, Any], template_id: str) -> dict:
self._require_admin(current_user)
version_repository = self._require_version_repository()
contract_template = self._require_template(current_user, template_id)
versions = version_repository.list_by_template_id(current_user["tenant_id"], template_id)
if not versions:
versions = [self._build_fallback_version(contract_template)]
versions.sort(key=lambda item: (item.is_current, item.created_at), reverse=True)
return {
"template_id": template_id,
"current_version_label": contract_template.current_version_label or "v1.0",
"version_count": max(contract_template.version_count or 0, len(versions), 1),
"items": [self._to_version_item(version) for version in versions],
}
def create_template(
self,
current_user: Dict[str, Any],
name: str,
contract_type: Optional[str],
description: Optional[str],
content: str,
variables_json: Optional[str],
enabled: bool,
change_summary: Optional[str] = None,
template_code: Optional[str] = None,
document_type_code: Optional[str] = None,
template_category: Optional[str] = None,
industry_category: Optional[str] = None,
) -> dict:
self._require_admin(current_user)
repository = self._require_repository()
normalized_change_summary = self._normalize_change_summary(change_summary, default="Initial template version")
normalized_contract_type = contract_type.strip() if contract_type else None
contract_template = ContractTemplate(
id=f"tpl-{uuid4().hex[:12]}",
tenant_id=current_user["tenant_id"],
name=name.strip(),
contract_type=normalized_contract_type,
document_type_code=self._normalize_document_type_code(document_type_code, normalized_contract_type),
template_code=self._normalize_template_code(template_code, normalized_contract_type, name),
template_category=self._normalize_template_category(template_category, normalized_contract_type),
industry_category=self._normalize_industry_category(industry_category, normalized_contract_type),
description=description.strip() if description else None,
content=content.strip(),
variables_json=self._parse_variables(variables_json),
enabled=enabled,
current_version_label="v1.0",
latest_change_summary=normalized_change_summary,
latest_version_created_at=self._now(),
version_count=1,
created_by=current_user.get("id"),
updated_by=current_user.get("id"),
)
created = repository.create(contract_template)
self._create_version_snapshot(created, current_user, normalized_change_summary, is_current=True)
return self._to_item(created)
def update_template(
self,
current_user: Dict[str, Any],
template_id: str,
name: str,
contract_type: Optional[str],
description: Optional[str],
content: str,
variables_json: Optional[str],
enabled: bool,
change_summary: Optional[str] = None,
template_code: Optional[str] = None,
document_type_code: Optional[str] = None,
template_category: Optional[str] = None,
industry_category: Optional[str] = None,
) -> dict:
self._require_admin(current_user)
repository = self._require_repository()
contract_template = self._require_template(current_user, template_id)
next_version_label = self._bump_version_label(contract_template.current_version_label)
normalized_change_summary = self._normalize_change_summary(change_summary, default=f"{next_version_label} template update")
normalized_contract_type = contract_type.strip() if contract_type else None
previous_contract_type = contract_template.contract_type
previous_document_type_code = contract_template.document_type_code
contract_template.name = name.strip()
contract_template.contract_type = normalized_contract_type
if document_type_code is not None:
contract_template.document_type_code = self._normalize_document_type_code(document_type_code, normalized_contract_type)
elif previous_contract_type == normalized_contract_type and previous_document_type_code:
contract_template.document_type_code = previous_document_type_code
else:
contract_template.document_type_code = self._normalize_document_type_code(None, normalized_contract_type)
contract_template.template_code = self._normalize_template_code(template_code or contract_template.template_code, normalized_contract_type, name)
contract_template.template_category = self._normalize_template_category(template_category, normalized_contract_type)
contract_template.industry_category = self._normalize_industry_category(industry_category, normalized_contract_type)
contract_template.description = description.strip() if description else None
contract_template.content = content.strip()
contract_template.variables_json = self._parse_variables(variables_json)
contract_template.enabled = enabled
contract_template.current_version_label = next_version_label
contract_template.latest_change_summary = normalized_change_summary
contract_template.latest_version_created_at = self._now()
contract_template.version_count = max(contract_template.version_count or 0, 1) + 1
contract_template.updated_by = current_user.get("id")
updated = repository.update(contract_template)
self._create_version_snapshot(updated, current_user, normalized_change_summary, is_current=True)
return self._to_item(updated)
def delete_template(self, current_user: Dict[str, Any], template_id: str) -> dict:
self._require_admin(current_user)
repository = self._require_repository()
version_repository = self._require_version_repository()
contract_template = self._require_template(current_user, template_id)
template_data = self._to_item(contract_template)
version_repository.delete_by_template_id(current_user["tenant_id"], template_id)
repository.delete(contract_template)
return template_data
def seed_default_templates(self, current_user: Dict[str, Any]) -> dict:
self._require_admin(current_user)
repository = self._require_repository()
tenant_id = current_user["tenant_id"]
created_items: list[dict[str, Any]] = []
existing_items: list[dict[str, Any]] = []
for template_data in DEFAULT_TEMPLATES:
template_code = self._normalize_template_code(None, template_data["contract_type"], template_data["name"])
existing = None
get_by_template_code = getattr(repository, "get_by_template_code", None)
if callable(get_by_template_code):
existing = get_by_template_code(tenant_id, template_code)
if existing is None:
existing = repository.get_by_name(tenant_id, template_data["name"])
if existing is not None:
existing_items.append(self._to_item(existing))
continue
contract_template = ContractTemplate(
id=f"tpl-{uuid4().hex[:12]}",
tenant_id=tenant_id,
name=template_data["name"],
contract_type=template_data["contract_type"],
document_type_code=template_data["document_type_code"],
template_code=template_code,
template_category=template_data["template_category"],
industry_category=template_data["industry_category"],
description=template_data["description"],
content=template_data["content"],
variables_json=template_data["variables"],
enabled=True,
current_version_label="v1.0",
latest_change_summary="Seeded default template",
latest_version_created_at=self._now(),
version_count=1,
created_by=current_user.get("id"),
updated_by=current_user.get("id"),
)
created = repository.create(contract_template)
self._create_version_snapshot(created, current_user, "Seeded default template", is_current=True)
created_items.append(self._to_item(created))
return {
"created": created_items,
"existing": existing_items,
"created_count": len(created_items),
"existing_count": len(existing_items),
"total": len(DEFAULT_TEMPLATES),
"template_names": [item["name"] for item in created_items],
}
def publish_template(self, current_user: Dict[str, Any], template_id: str) -> dict:
self._require_admin(current_user)
repository = self._require_repository()
governance_repository = self._require_governance_asset_repository()
contract_template = self._require_template(current_user, template_id)
governance_asset = self._require_publishable_governance_asset(current_user, contract_template)
contract_template.enabled = True
contract_template.published_at = self._now()
contract_template.published_by = current_user.get("id")
contract_template.latest_governance_asset_id = governance_asset.id
contract_template.updated_by = current_user.get("id")
updated = repository.update(contract_template)
return self._to_item(updated)
def unpublish_template(self, current_user: Dict[str, Any], template_id: str) -> dict:
self._require_admin(current_user)
repository = self._require_repository()
contract_template = self._require_template(current_user, template_id)
contract_template.enabled = False
contract_template.published_at = None
contract_template.published_by = None
contract_template.latest_governance_asset_id = None
contract_template.updated_by = current_user.get("id")
updated = repository.update(contract_template)
return self._to_item(updated)
def rollback_template(
self,
current_user: Dict[str, Any],
template_id: str,
target_version_label: str,
change_summary: Optional[str] = None,
) -> dict:
self._require_admin(current_user)
repository = self._require_repository()
version_repository = self._require_version_repository()
contract_template = self._require_template(current_user, template_id)
target_version = next(
(item for item in version_repository.list_by_template_id(current_user["tenant_id"], template_id) if item.version_label == target_version_label),
None,
)
if target_version is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="template version not found")
next_version_label = self._bump_version_label(contract_template.current_version_label)
normalized_change_summary = self._normalize_change_summary(change_summary, default=f"Rollback to {target_version_label}")
contract_template.name = target_version.name
contract_template.contract_type = target_version.contract_type
contract_template.document_type_code = target_version.document_type_code
contract_template.template_category = target_version.template_category
contract_template.industry_category = target_version.industry_category
contract_template.description = target_version.description
contract_template.content = target_version.content
contract_template.variables_json = target_version.variables_json
contract_template.current_version_label = next_version_label
contract_template.latest_change_summary = normalized_change_summary
contract_template.latest_version_created_at = self._now()
contract_template.version_count = max(contract_template.version_count or 0, 1) + 1
contract_template.enabled = False
contract_template.published_at = None
contract_template.published_by = None
contract_template.latest_governance_asset_id = None
contract_template.updated_by = current_user.get("id")
updated = repository.update(contract_template)
self._create_version_snapshot(updated, current_user, normalized_change_summary, is_current=True)
return self._to_item(updated)
def _require_template(self, current_user: Dict[str, Any], template_id: str) -> ContractTemplate:
repository = self._require_repository()
contract_template = repository.get_by_id(template_id)
if contract_template is None or contract_template.tenant_id != current_user["tenant_id"]:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="template not found")
return contract_template
def _require_admin(self, current_user: Dict[str, Any]) -> None:
if current_user.get("role") != "admin":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="forbidden")
def _require_repository(self) -> ContractTemplateRepository:
if self.repository is None:
raise RuntimeError("template repository is not configured")
return self.repository
def _require_governance_asset_repository(self) -> GovernanceAssetRepository:
if self.governance_asset_repository is None:
raise RuntimeError("governance asset repository is not configured")
return self.governance_asset_repository
def _require_version_repository(self) -> ContractTemplateVersionRepository:
if self.version_repository is None:
raise RuntimeError("template version repository is not configured")
return self.version_repository
def _parse_variables(self, variables_json: Optional[str]) -> Optional[list[dict[str, Any]]]:
if not variables_json or not variables_json.strip():
return None
try:
parsed = json.loads(variables_json)
except json.JSONDecodeError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="variables_json must be valid JSON") from exc
if not isinstance(parsed, list):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="variables_json must be a JSON array")
for item in parsed:
if not isinstance(item, dict):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="variables_json items must be objects")
return parsed
def _create_version_snapshot(
self,
contract_template: ContractTemplate,
current_user: Dict[str, Any],
change_summary: str,
*,
is_current: bool,
) -> ContractTemplateVersion:
version_repository = self._require_version_repository()
version_repository.clear_current(contract_template.tenant_id, contract_template.id)
version = ContractTemplateVersion(
id=f"tplver-{uuid4().hex[:12]}",
template_id=contract_template.id,
tenant_id=contract_template.tenant_id,
version_label=contract_template.current_version_label or "v1.0",
change_summary=change_summary,
name=contract_template.name,
contract_type=contract_template.contract_type,
document_type_code=contract_template.document_type_code,
template_category=contract_template.template_category,
industry_category=contract_template.industry_category,
description=contract_template.description,
content=contract_template.content,
variables_json=contract_template.variables_json,
is_current=is_current,
created_by=current_user.get("id"),
created_by_name=current_user.get("display_name"),
created_at=contract_template.latest_version_created_at or self._now(),
)
return version_repository.create(version)
def _build_fallback_version(self, contract_template: ContractTemplate) -> ContractTemplateVersion:
return ContractTemplateVersion(
id=f"fallback-{contract_template.id}",
template_id=contract_template.id,
tenant_id=contract_template.tenant_id,
version_label=contract_template.current_version_label or "v1.0",
change_summary=contract_template.latest_change_summary,
name=contract_template.name,
contract_type=contract_template.contract_type,
document_type_code=contract_template.document_type_code,
template_category=contract_template.template_category,
industry_category=contract_template.industry_category,
description=contract_template.description,
content=contract_template.content,
variables_json=contract_template.variables_json,
is_current=True,
created_by=contract_template.updated_by or contract_template.created_by,
created_by_name=None,
created_at=contract_template.latest_version_created_at or contract_template.updated_at or contract_template.created_at,
)
def _normalize_change_summary(self, value: Optional[str], *, default: str) -> str:
normalized = value.strip() if value else ""
return normalized or default
def _normalize_template_category(self, template_category: Optional[str], contract_type: Optional[str]) -> str:
normalized = template_category.strip() if template_category else ""
if normalized:
return normalized
return {
"labor": "employment-hr",
"labor-service": "employment-hr",
"labor_service": "employment-hr",
"technical-service": "service-delivery",
"technical_service": "service-delivery",
"software-development": "service-delivery",
"software_development": "service-delivery",
"technology-development": "service-delivery",
"technology_development": "service-delivery",
"lease-house": "real-estate",
"lease_house": "real-estate",
"equity-transfer": "investment-finance",
"equity_transfer": "investment-finance",
"cooperation-framework": "commercial-cooperation",
"cooperation_framework": "commercial-cooperation",
"agency-distribution": "commercial-cooperation",
"agency_distribution": "commercial-cooperation",
"franchise": "commercial-cooperation",
"franchise_agreement": "commercial-cooperation",
"intercompany-loan": "investment-finance",
"intercompany_loan": "investment-finance",
"investment": "investment-finance",
"investment_agreement": "investment-finance",
"valuation-adjustment": "investment-finance",
"valuation_adjustment": "investment-finance",
"shareholder-resolution": "corporate-governance",
"shareholder_resolution": "corporate-governance",
"power-of-attorney": "corporate-governance",
"power_of_attorney": "corporate-governance",
"legal-opinion": "commercial-compliance",
"legal_opinion": "commercial-compliance",
"property-service": "real-estate",
"property_service": "real-estate",
"demand-letter": "notices-letters",
"demand_letter": "notices-letters",
"rescission-notice": "notices-letters",
"rescission_notice": "notices-letters",
"termination-notice": "notices-letters",
"contract_termination_notice": "notices-letters",
"procurement": "procurement-supply",
"sales": "sales-commercial",
"service": "service-delivery",
"nda": "commercial-compliance",
}.get((contract_type or "").strip().lower(), "general-business")
def _normalize_industry_category(self, industry_category: Optional[str], contract_type: Optional[str]) -> str:
normalized = industry_category.strip() if industry_category else ""
if normalized:
return normalized
return {
"labor": "general-enterprise",
"labor-service": "general-enterprise",
"labor_service": "general-enterprise",
"technical-service": "technology",
"technical_service": "technology",
"software-development": "technology",
"software_development": "technology",
"technology-development": "technology",
"technology_development": "technology",
"lease-house": "real-estate",
"lease_house": "real-estate",
"equity-transfer": "general-enterprise",
"equity_transfer": "general-enterprise",
"cooperation-framework": "general-enterprise",
"cooperation_framework": "general-enterprise",
"agency-distribution": "general-enterprise",
"agency_distribution": "general-enterprise",
"franchise": "general-enterprise",
"franchise_agreement": "general-enterprise",
"intercompany-loan": "general-enterprise",
"intercompany_loan": "general-enterprise",
"investment": "general-enterprise",
"investment_agreement": "general-enterprise",
"valuation-adjustment": "general-enterprise",
"valuation_adjustment": "general-enterprise",
"shareholder-resolution": "general-enterprise",
"shareholder_resolution": "general-enterprise",
"power-of-attorney": "general-enterprise",
"power_of_attorney": "general-enterprise",
"legal-opinion": "general-enterprise",
"legal_opinion": "general-enterprise",
"property-service": "real-estate",
"property_service": "real-estate",
"demand-letter": "general-enterprise",
"demand_letter": "general-enterprise",
"rescission-notice": "general-enterprise",
"rescission_notice": "general-enterprise",
"termination-notice": "general-enterprise",
"contract_termination_notice": "general-enterprise",
"procurement": "general-enterprise",
"sales": "general-enterprise",
"service": "professional-services",
"nda": "general-enterprise",
}.get((contract_type or "").strip().lower(), "general-enterprise")
def _normalize_document_type_code(self, document_type_code: Optional[str], contract_type: Optional[str]) -> Optional[str]:
normalized = document_type_code.strip().lower().replace("-", "_") if document_type_code else ""
if normalized:
return {
"property_service_agreement": "property_service",
"contract_termination_notice": "rescission_notice",
}.get(normalized, normalized)
return {
"labor": "labor_contract",
"labor-service": "labor_service_agreement",
"labor_service": "labor_service_agreement",
"technical-service": "technical_service",
"technical_service": "technical_service",
"software-development": "software_development",
"software_development": "software_development",
"technology-development": "software_development",
"technology_development": "software_development",
"lease-house": "lease_house",
"lease_house": "lease_house",
"equity-transfer": "equity_transfer",
"equity_transfer": "equity_transfer",
"cooperation-framework": "cooperation_framework",
"cooperation_framework": "cooperation_framework",
"agency-distribution": "agency_distribution_agreement",
"agency_distribution": "agency_distribution_agreement",
"franchise": "franchise_agreement",
"franchise_agreement": "franchise_agreement",
"intercompany-loan": "intercompany_loan_agreement",
"intercompany_loan": "intercompany_loan_agreement",
"investment": "investment_agreement",
"investment_agreement": "investment_agreement",
"valuation-adjustment": "valuation_adjustment_agreement",
"valuation_adjustment": "valuation_adjustment_agreement",
"shareholder-resolution": "shareholder_resolution",
"shareholder_resolution": "shareholder_resolution",
"power-of-attorney": "power_of_attorney",
"power_of_attorney": "power_of_attorney",
"legal-opinion": "legal_opinion",
"legal_opinion": "legal_opinion",
"property-service": "property_service",
"property_service": "property_service",
"demand-letter": "demand_letter",
"demand_letter": "demand_letter",
"rescission-notice": "rescission_notice",
"rescission_notice": "rescission_notice",
"termination-notice": "rescission_notice",
"contract_termination_notice": "rescission_notice",
"procurement": "procurement_goods",
"purchase": "procurement_goods",
"sales": "procurement_goods",
"service": "service_general",
"nda": "nda_commercial",
"development": "software_development",
"software": "software_development",
"technical": "technical_service",
}.get((contract_type or "").strip().lower())
def _normalize_template_code(self, template_code: Optional[str], contract_type: Optional[str], name: str) -> str:
normalized = template_code.strip() if template_code else ""
if normalized:
return normalized
normalized_contract_type = (contract_type or "").strip().lower()
explicit_map = {
("labor", "劳动合同标准模板"): "TPL-LABOR-CONTRACT-001",
("procurement", "货物采购合同模板"): "TPL-PURCHASE-GOODS-001",
("procurement", "采购合同标准模板"): "TPL-PURCHASE-GOODS-001",
("purchase", "货物采购合同模板"): "TPL-PURCHASE-GOODS-001",
("purchase", "采购合同标准模板"): "TPL-PURCHASE-GOODS-001",
("nda", "保密协议标准模板"): "TPL-NDA-COMMERCIAL-001",
("technical-service", "技术服务合同模板"): "TPL-TECHNICAL-SERVICE-001",
("technical_service", "技术服务合同模板"): "TPL-TECHNICAL-SERVICE-001",
("software-development", "软件开发合同模板"): "TPL-TECH-DEVELOPMENT-001",
("software_development", "软件开发合同模板"): "TPL-TECH-DEVELOPMENT-001",
("software-development", "软件技术开发合同模板"): "TPL-TECH-DEVELOPMENT-001",
("software_development", "软件技术开发合同模板"): "TPL-TECH-DEVELOPMENT-001",
("technology-development", "软件技术开发合同模板"): "TPL-TECH-DEVELOPMENT-001",
("technology_development", "软件技术开发合同模板"): "TPL-TECH-DEVELOPMENT-001",
("sales", "销售合同模板"): "TPL-SALES-GOODS-001",
("lease-house", "房屋租赁合同模板"): "TPL-LEASE-HOUSE-001",
("lease_house", "房屋租赁合同模板"): "TPL-LEASE-HOUSE-001",
("equity-transfer", "股权转让协议模板"): "TPL-EQUITY-TRANSFER-001",
("equity_transfer", "股权转让协议模板"): "TPL-EQUITY-TRANSFER-001",
("cooperation-framework", "合作框架协议模板"): "TPL-COOPERATION-FRAMEWORK-001",
("cooperation_framework", "合作框架协议模板"): "TPL-COOPERATION-FRAMEWORK-001",
("demand-letter", "催告函模板"): "TPL-DEMAND-LETTER-001",
("demand_letter", "催告函模板"): "TPL-DEMAND-LETTER-001",
("rescission-notice", "解除合同通知书模板"): "TPL-CONTRACT-TERMINATION-NOTICE-001",
("rescission_notice", "解除合同通知书模板"): "TPL-CONTRACT-TERMINATION-NOTICE-001",
("termination-notice", "解除合同通知书模板"): "TPL-CONTRACT-TERMINATION-NOTICE-001",
("contract_termination_notice", "解除合同通知书模板"): "TPL-CONTRACT-TERMINATION-NOTICE-001",
("agency-distribution", "代理/经销协议模板"): "TPL-AGENCY-DISTRIBUTION-001",
("agency_distribution", "代理/经销协议模板"): "TPL-AGENCY-DISTRIBUTION-001",
("franchise", "加盟合同模板"): "TPL-FRANCHISE-001",
("franchise_agreement", "加盟合同模板"): "TPL-FRANCHISE-001",
("intercompany-loan", "企业间借款合同模板"): "TPL-INTERCOMPANY-LOAN-001",
("intercompany_loan", "企业间借款合同模板"): "TPL-INTERCOMPANY-LOAN-001",
("intercompany-loan", "借款合同模板"): "TPL-INTERCOMPANY-LOAN-001",
("intercompany_loan", "借款合同模板"): "TPL-INTERCOMPANY-LOAN-001",
("investment", "投资协议模板"): "TPL-INVESTMENT-AGREEMENT-001",
("investment_agreement", "投资协议模板"): "TPL-INVESTMENT-AGREEMENT-001",
("valuation-adjustment", "对赌协议模板"): "TPL-VALUATION-ADJUSTMENT-001",
("valuation_adjustment", "对赌协议模板"): "TPL-VALUATION-ADJUSTMENT-001",
("shareholder-resolution", "股东会决议模板"): "TPL-SHAREHOLDER-RESOLUTION-001",
("shareholder_resolution", "股东会决议模板"): "TPL-SHAREHOLDER-RESOLUTION-001",
("power-of-attorney", "授权委托书模板"): "TPL-POWER-OF-ATTORNEY-001",
("power_of_attorney", "授权委托书模板"): "TPL-POWER-OF-ATTORNEY-001",
("legal-opinion", "法律意见书模板"): "TPL-LEGAL-OPINION-001",
("legal_opinion", "法律意见书模板"): "TPL-LEGAL-OPINION-001",
("property-service", "物业服务合同模板"): "TPL-PROPERTY-SERVICE-001",
("property_service", "物业服务合同模板"): "TPL-PROPERTY-SERVICE-001",
}
mapped = explicit_map.get((normalized_contract_type, name.strip()))
if mapped:
return mapped
if "saas" in name.strip().lower() or "软件即服务" in name:
return "TPL-SOFTWARE-SAAS-001"
if "技术开发" in name:
return "TPL-TECH-DEVELOPMENT-001"
if "技术服务" in name:
return "TPL-TECHNICAL-SERVICE-001"
if "合作框架" in name:
return "TPL-COOPERATION-FRAMEWORK-001"
if "委托服务" in name:
return "TPL-COMMISSION-SERVICE-001"
if "房屋租赁" in name:
return "TPL-LEASE-HOUSE-001"
if "企业间借款" in name or ("借款合同" in name and "企业" in name) or "intercompany loan" in name.strip().lower():
return "TPL-INTERCOMPANY-LOAN-001"
if normalized_contract_type in {"intercompany-loan", "intercompany_loan"}:
return "TPL-INTERCOMPANY-LOAN-001"
if "投资协议" in name or "investment agreement" in name.strip().lower():
return "TPL-INVESTMENT-AGREEMENT-001"
if normalized_contract_type in {"investment", "investment_agreement"}:
return "TPL-INVESTMENT-AGREEMENT-001"
if "对赌协议" in name or "估值调整" in name or "valuation adjustment" in name.strip().lower():
return "TPL-VALUATION-ADJUSTMENT-001"
if normalized_contract_type in {"valuation-adjustment", "valuation_adjustment"}:
return "TPL-VALUATION-ADJUSTMENT-001"
if "股东会决议" in name or "股东大会决议" in name or "shareholder resolution" in name.strip().lower():
return "TPL-SHAREHOLDER-RESOLUTION-001"
if normalized_contract_type in {"shareholder-resolution", "shareholder_resolution"}:
return "TPL-SHAREHOLDER-RESOLUTION-001"
if "授权委托书" in name or "power of attorney" in name.strip().lower():
return "TPL-POWER-OF-ATTORNEY-001"
if normalized_contract_type in {"power-of-attorney", "power_of_attorney"}:
return "TPL-POWER-OF-ATTORNEY-001"
if "法律意见书" in name or "legal opinion" in name.strip().lower():
return "TPL-LEGAL-OPINION-001"
if normalized_contract_type in {"legal-opinion", "legal_opinion"}:
return "TPL-LEGAL-OPINION-001"
if "物业服务" in name or "property service" in name.strip().lower():
return "TPL-PROPERTY-SERVICE-001"
if normalized_contract_type in {"property-service", "property_service"}:
return "TPL-PROPERTY-SERVICE-001"
if "通用服务" in name or ("服务合同" in name and "物业" not in name and "委托" not in name and "劳动" not in name and "技术" not in name and "软件" not in name):
return "TPL-SERVICE-GENERAL-001"
if "股权转让" in name or "equity transfer" in name.strip().lower():
return "TPL-EQUITY-TRANSFER-001"
if normalized_contract_type in {"equity-transfer", "equity_transfer"}:
return "TPL-EQUITY-TRANSFER-001"
if "催告函" in name or "demand letter" in name.strip().lower():
return "TPL-DEMAND-LETTER-001"
if normalized_contract_type in {"demand-letter", "demand_letter"}:
return "TPL-DEMAND-LETTER-001"
if "解除合同通知书" in name or "termination notice" in name.strip().lower() or "rescission notice" in name.strip().lower():
return "TPL-CONTRACT-TERMINATION-NOTICE-001"
if normalized_contract_type in {"rescission-notice", "rescission_notice", "termination-notice", "contract_termination_notice"}:
return "TPL-CONTRACT-TERMINATION-NOTICE-001"
if "代理/经销" in name or ("代理" in name and "经销" in name):
return "TPL-AGENCY-DISTRIBUTION-001"
if normalized_contract_type in {"agency-distribution", "agency_distribution"}:
return "TPL-AGENCY-DISTRIBUTION-001"
if "加盟合同" in name or "商业特许经营" in name or "franchise" in name.strip().lower():
return "TPL-FRANCHISE-001"
if normalized_contract_type in {"franchise", "franchise_agreement"}:
return "TPL-FRANCHISE-001"
if normalized_contract_type in {"technical-service", "technical_service"}:
return "TPL-TECHNICAL-SERVICE-001"
if normalized_contract_type in {"software-development", "software_development", "technology-development", "technology_development"}:
return "TPL-TECH-DEVELOPMENT-001"
if normalized_contract_type in {"lease-house", "lease_house"}:
return "TPL-LEASE-HOUSE-001"
if normalized_contract_type in {"cooperation-framework", "cooperation_framework"}:
return "TPL-COOPERATION-FRAMEWORK-001"
if normalized_contract_type in {"labor-service", "labor_service"}:
return "TPL-LABOR-SERVICE-001"
if normalized_contract_type == "service":
return "TPL-SERVICE-GENERAL-001"
if normalized_contract_type in {"procurement", "purchase"}:
return "TPL-PURCHASE-GOODS-001"
if normalized_contract_type == "labor":
return "TPL-LABOR-CONTRACT-001"
if normalized_contract_type == "nda":
return "TPL-NDA-COMMERCIAL-001"
if normalized_contract_type == "sales":
return "TPL-SALES-GOODS-001"
slug = re.sub(r"[^A-Za-z0-9]+", "-", name.strip().upper()).strip("-") or "GENERAL"
return f"TPL-{slug[:48]}-001"
def _require_publishable_governance_asset(self, current_user: Dict[str, Any], contract_template: ContractTemplate):
governance_repository = self._require_governance_asset_repository()
if not contract_template.template_code:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="template_code is required before publish")
governance_asset = governance_repository.get_by_identity(current_user["tenant_id"], "contract_template", contract_template.template_code)
if governance_asset is None:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="governance asset not found for template_code")
if governance_asset.production_status not in {"ready", "approved"}:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="governance asset is not approved for production publish")
return governance_asset
def _bump_version_label(self, current_version_label: Optional[str]) -> str:
if not current_version_label:
return "v1.1"
match = re.match(r"^v?(?P<major>\d+)\.(?P<minor>\d+)$", current_version_label.strip(), re.IGNORECASE)
if not match:
return "v1.1"
major = int(match.group("major"))
minor = int(match.group("minor")) + 1
return f"v{major}.{minor}"
def _now(self) -> datetime:
return datetime.now(timezone.utc)
def _to_item(self, contract_template: ContractTemplate) -> dict:
return {
"id": contract_template.id,
"name": contract_template.name,
"contract_type": contract_template.contract_type,
"document_type_code": contract_template.document_type_code,
"template_code": contract_template.template_code,
"template_category": contract_template.template_category,
"industry_category": contract_template.industry_category,
"description": contract_template.description,
"content": contract_template.content,
"variables": contract_template.variables_json or [],
"enabled": contract_template.enabled,
"published_at": contract_template.published_at.isoformat() if contract_template.published_at else None,
"published_by": contract_template.published_by,
"latest_governance_asset_id": contract_template.latest_governance_asset_id,
"current_version_label": contract_template.current_version_label or "v1.0",
"latest_change_summary": contract_template.latest_change_summary,
"latest_version_created_at": (
contract_template.latest_version_created_at.isoformat()
if contract_template.latest_version_created_at is not None
else None
),
"version_count": max(contract_template.version_count or 0, 1),
"created_by": contract_template.created_by,
"updated_by": contract_template.updated_by,
"created_at": contract_template.created_at.isoformat(),
"updated_at": contract_template.updated_at.isoformat(),
}
def _to_version_item(self, version: ContractTemplateVersion) -> dict:
return {
"id": version.id,
"template_id": version.template_id,
"version_label": version.version_label,
"change_summary": version.change_summary,
"name": version.name,
"contract_type": version.contract_type,
"document_type_code": version.document_type_code,
"template_category": version.template_category,
"industry_category": version.industry_category,
"description": version.description,
"content": version.content,
"variables": version.variables_json or [],
"is_current": version.is_current,
"created_by": version.created_by,
"created_by_name": version.created_by_name,
"created_at": version.created_at.isoformat(),
}