"""CONTRACT_CREATE node skeleton for YiSaiQi Windows integration. This first version proves the state-center callback path and leaves the contract-system page automation behind a single replaceable runner function. """ from __future__ import annotations import argparse import os import uuid import webbrowser from dataclasses import dataclass from pathlib import Path from typing import Any, Callable from .callback_client import StateCenterClient from .contract_draft import ContractDraft, load_contract_draft from .contract_flow_steps import ContractFlowContext, run_contract_create_flow from .pdf_utils import ensure_merged_pdf from .yisaiqi_adapter_template import YiSaiQiBrowserAdapter NODE_CODE = "CONTRACT_CREATE" CONTRACT_URL = "http://contract-test.gzmpc.com:8080/" class ContractCreateError(RuntimeError): error_code = "UI_ELEMENT_NOT_FOUND" retryable = False route_role = "OPS" class LoginFailed(ContractCreateError): error_code = "SYS_LOGIN_FAIL" retryable = True route_role = "OPS" class SupplierContractDisabled(ContractCreateError): error_code = "BIZ_CONTRACT_DISABLED" retryable = False route_role = "PURCHASER" class PdfMergeUnsupported(ContractCreateError): error_code = "CAP_PDF_MERGE_UNSUPPORTED" retryable = False route_role = "OPS" @dataclass(frozen=True) class ContractCreateInput: job_id: str supplier_name: str order_date: str source_type: str contract_system_no: str | None = None attempt_no: int = 1 contract_url: str = CONTRACT_URL username: str = "test002" password: str = "1" open_browser: bool = False draft_path: str | None = None merged_pdf_output: str | None = None ContractRunner = Callable[[ContractCreateInput], str] def execute_contract_create( spec: ContractCreateInput, client: StateCenterClient, *, contract_runner: ContractRunner | None = None, ) -> dict[str, Any]: client.start_node(spec.job_id, NODE_CODE, attempt_no=spec.attempt_no) runner = contract_runner or create_contract_in_contract_system try: contract_no = runner(spec) complete_payload = build_complete_payload(spec, contract_no) return client.complete_node(spec.job_id, NODE_CODE, complete_payload) except ContractCreateError as exc: fail_payload = build_fail_payload(spec, exc) return client.fail_node(spec.job_id, NODE_CODE, fail_payload) def create_contract_in_contract_system(spec: ContractCreateInput) -> str: """Draft the contract in the contract system and return contract number.""" if spec.open_browser: webbrowser.open(spec.contract_url) if spec.contract_system_no: return spec.contract_system_no if not spec.draft_path: raise ContractCreateError("draft_path is required for real contract drafting") draft = load_contract_draft(spec.draft_path) ctx = build_flow_context(spec, draft) browser = YiSaiQiBrowserAdapter() try: return run_contract_create_flow(browser, ctx) except NotImplementedError as exc: raise ContractCreateError( "YiSaiQiBrowserAdapter still needs to be mapped to YiSaiQi designer activities" ) from exc def build_flow_context(spec: ContractCreateInput, draft: ContractDraft) -> ContractFlowContext: merged_pdf_path = draft.merged_pdf_path if draft.source_type == "YYC": output_path = spec.merged_pdf_output or str( Path("runtime") / "contracts" / spec.job_id / "yyc-contract-merged.pdf" ) try: merged_pdf_path = ensure_merged_pdf(draft.platform_pdf_paths, output_path) except (RuntimeError, ValueError) as exc: raise PdfMergeUnsupported(str(exc)) from exc return ContractFlowContext( contract_url=spec.contract_url, username=spec.username, password=spec.password, supplier_name=draft.supplier_name or spec.supplier_name, order_date=draft.order_date or spec.order_date, source_type=draft.source_type or spec.source_type, template_type=draft.template_type, template_name=draft.template_name, customer_code=draft.customer_code, customer_name=draft.customer_name, contract_category=draft.contract_category, settlement_type=draft.settlement_type, contract_background=draft.contract_background, contract_nature=draft.contract_nature, amount_currency=draft.amount_currency, amount=draft.amount, contract_end_date=draft.contract_end_date, is_major_contract=draft.is_major_contract, is_prepayment=draft.is_prepayment, belong_year=draft.belong_year, payment_method=draft.payment_method, payment_days=draft.payment_days, contract_status=draft.contract_status, contains_blood_products=draft.contains_blood_products, submit_mode=draft.submit_mode, goods_lines=draft.goods_lines, merged_pdf_path=merged_pdf_path, ) def build_complete_payload(spec: ContractCreateInput, contract_no: str) -> dict[str, Any]: return { "attemptNo": spec.attempt_no, "executorRunId": f"YSQ-CONTRACT-{uuid.uuid4().hex[:12]}", "idempotencyKey": f"{spec.job_id}:{NODE_CODE}:{spec.attempt_no}:{contract_no}", "businessRefs": {"contractSystemNo": contract_no}, "summary": { "supplierName": spec.supplier_name, "contractDate": spec.order_date, "sourceType": spec.source_type, "contractUrl": spec.contract_url, "uploadRule": "merge_pdf_before_upload" if spec.source_type == "YYC" else "skip_contract_upload", }, "artifacts": [], } def build_fail_payload(spec: ContractCreateInput, exc: ContractCreateError) -> dict[str, Any]: return { "attemptNo": spec.attempt_no, "errorCode": exc.error_code, "errorSummary": str(exc), "retryable": exc.retryable, "routeRole": exc.route_role, "artifacts": [], } def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Run YiSaiQi CONTRACT_CREATE callback skeleton.") parser.add_argument("--state-center-url", default=os.getenv("STATE_CENTER_URL", "http://127.0.0.1:8000")) parser.add_argument("--token", default=os.getenv("AGENT_TOKEN", "dev-agent-token")) parser.add_argument("--job-id", required=True) parser.add_argument("--supplier-name", required=True) parser.add_argument("--order-date", required=True) parser.add_argument("--source-type", choices=("YYC", "XYY", "OFFLINE"), default="YYC") parser.add_argument("--contract-system-no", default=os.getenv("CONTRACT_SYSTEM_NO")) parser.add_argument("--attempt-no", type=int, default=1) parser.add_argument("--contract-url", default=os.getenv("CONTRACT_URL", CONTRACT_URL)) parser.add_argument("--username", default=os.getenv("CONTRACT_USERNAME", "test002")) parser.add_argument("--password", default=os.getenv("CONTRACT_PASSWORD", "1")) parser.add_argument("--open-browser", action="store_true") parser.add_argument("--draft-path", default=os.getenv("CONTRACT_DRAFT_PATH")) parser.add_argument("--merged-pdf-output", default=os.getenv("MERGED_PDF_OUTPUT")) return parser.parse_args() def main() -> int: args = parse_args() spec = ContractCreateInput( job_id=args.job_id, supplier_name=args.supplier_name, order_date=args.order_date, source_type=args.source_type, contract_system_no=args.contract_system_no, attempt_no=args.attempt_no, contract_url=args.contract_url, username=args.username, password=args.password, open_browser=args.open_browser, draft_path=args.draft_path, merged_pdf_output=args.merged_pdf_output, ) client = StateCenterClient(args.state_center_url, args.token) result = execute_contract_create(spec, client) print(result) return 0 if __name__ == "__main__": raise SystemExit(main())