"""Shared invariants for Feishu approval submission idempotency.""" from __future__ import annotations from uuid import UUID, uuid5 from app.core.exceptions import AppException # Stable, repo-owned namespace. Domain rows created before the outbound # Feishu call use UUIDv5 so an uncertain HTTP/MCP retry addresses the exact # same pending row instead of minting a second allocation/charge. _APPROVAL_RESOURCE_NAMESPACE = UUID("ba483c11-30ea-4c26-99a0-30a49bdb9319") # These flows have authoritative domain entry points which create and bind a # local resource before submission. Generic approval endpoints must reject # them or callers can bypass quota, catalog, pricing, and ownership checks. SPECIALIZED_APPROVAL_TEMPLATE_KEYS = frozenset({ "software_seat", "software_addon", }) def normalize_approval_idempotency_key(value: str) -> str: """Validate and normalize a required approval idempotency key.""" if not isinstance(value, str): raise AppException( code="APPROVAL_IDEMPOTENCY_KEY_REQUIRED", message="Idempotency-Key is required for approval submissions.", status_code=422, ) key = value.strip() if not key or len(key) > 128 or not key.isprintable(): raise AppException( code="APPROVAL_IDEMPOTENCY_KEY_INVALID", message="Idempotency-Key must be 1-128 printable characters.", status_code=422, ) return key def stable_approval_resource_id( *, scope: str, applicant_id: UUID, idempotency_key: str, ) -> UUID: """Return the stable local-row id for one logical approval submission.""" key = normalize_approval_idempotency_key(idempotency_key) return uuid5( _APPROVAL_RESOURCE_NAMESPACE, f"{scope}\0{applicant_id}\0{key}", )