feat: route bounded hierarchy postbox copies
This commit is contained in:
@@ -150,6 +150,144 @@ class PostboxDeliveryResponse(BaseModel):
|
||||
evidence: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class PostboxLinkedCopyPolicyPayload(BaseModel):
|
||||
enabled: bool = False
|
||||
structure_id: str | None = Field(default=None, max_length=36)
|
||||
relation_type_ids: list[str] = Field(default_factory=list, max_length=20)
|
||||
max_depth: int = Field(default=1, ge=1, le=20)
|
||||
stop_unit_id: str | None = Field(default=None, max_length=36)
|
||||
stop_unit_type_id: str | None = Field(default=None, max_length=36)
|
||||
target_function_type_id: str | None = Field(default=None, max_length=36)
|
||||
target_template_id: str | None = Field(default=None, max_length=36)
|
||||
fanout: Literal["nearest", "all"] = "nearest"
|
||||
allowed_classifications: list[str] = Field(
|
||||
default_factory=lambda: ["internal"],
|
||||
max_length=20,
|
||||
)
|
||||
allowed_producer_modules: list[str] = Field(
|
||||
default_factory=list,
|
||||
max_length=50,
|
||||
)
|
||||
require_expiry: bool = False
|
||||
max_retention_days: int | None = Field(default=None, ge=1, le=36500)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_enabled_policy(self) -> "PostboxLinkedCopyPolicyPayload":
|
||||
self.relation_type_ids = list(dict.fromkeys(self.relation_type_ids))
|
||||
self.allowed_classifications = list(
|
||||
dict.fromkeys(
|
||||
value.strip() for value in self.allowed_classifications
|
||||
)
|
||||
)
|
||||
self.allowed_producer_modules = list(
|
||||
dict.fromkeys(
|
||||
value.strip() for value in self.allowed_producer_modules
|
||||
)
|
||||
)
|
||||
if any(not value for value in self.relation_type_ids):
|
||||
raise ValueError("Relation type IDs must not be empty.")
|
||||
if any(not value for value in self.allowed_classifications):
|
||||
raise ValueError("Allowed classifications must not be empty.")
|
||||
if any(not value for value in self.allowed_producer_modules):
|
||||
raise ValueError("Allowed producer modules must not be empty.")
|
||||
if self.enabled and not all(
|
||||
(
|
||||
self.structure_id,
|
||||
self.target_function_type_id,
|
||||
self.target_template_id,
|
||||
self.allowed_classifications,
|
||||
self.allowed_producer_modules,
|
||||
)
|
||||
):
|
||||
raise ValueError(
|
||||
"Enabled hierarchy copy requires a structure, target function "
|
||||
"type, target template, classification gate, and producer allowlist."
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class PostboxAttentionPolicyPayload(BaseModel):
|
||||
mode: Literal["none", "vacancy_escalation"] = "none"
|
||||
delay_minutes: int | None = Field(default=None, ge=1, le=43200)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_delay(self) -> "PostboxAttentionPolicyPayload":
|
||||
if self.mode == "vacancy_escalation" and self.delay_minutes is None:
|
||||
raise ValueError("Vacancy escalation requires a delay.")
|
||||
if self.mode == "none":
|
||||
self.delay_minutes = None
|
||||
return self
|
||||
|
||||
|
||||
class PostboxSharedVisibilityPolicyPayload(BaseModel):
|
||||
mode: Literal["none"] = "none"
|
||||
|
||||
|
||||
class PostboxRoutingPolicyPayload(BaseModel):
|
||||
linked_copy: PostboxLinkedCopyPolicyPayload = Field(
|
||||
default_factory=PostboxLinkedCopyPolicyPayload
|
||||
)
|
||||
attention: PostboxAttentionPolicyPayload = Field(
|
||||
default_factory=PostboxAttentionPolicyPayload
|
||||
)
|
||||
shared_visibility: PostboxSharedVisibilityPolicyPayload = Field(
|
||||
default_factory=PostboxSharedVisibilityPolicyPayload
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def normalize_legacy_policy(cls, value: Any) -> Any:
|
||||
if value in (None, {}, {"mode": "none"}):
|
||||
return {}
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_semantics(self) -> "PostboxRoutingPolicyPayload":
|
||||
if (
|
||||
self.attention.mode == "vacancy_escalation"
|
||||
and (
|
||||
not self.linked_copy.enabled
|
||||
or self.linked_copy.fanout != "nearest"
|
||||
)
|
||||
):
|
||||
raise ValueError(
|
||||
"Vacancy escalation requires nearest linked-copy routing."
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class PostboxRoutePreviewTarget(BaseModel):
|
||||
depth: int
|
||||
organization_unit_id: str
|
||||
organization_unit_name: str
|
||||
function_id: str | None = None
|
||||
function_name: str | None = None
|
||||
target_postbox_id: str | None = None
|
||||
target_address: str | None = None
|
||||
status: str
|
||||
vacant: bool = True
|
||||
holder_count: int = 0
|
||||
path: list[dict[str, Any]] = Field(default_factory=list)
|
||||
diagnostics: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class PostboxRouteDryRunRequest(BaseModel):
|
||||
target: PostboxTargetPayload
|
||||
producer_module: str = Field(min_length=1, max_length=100)
|
||||
classification: str = Field(default="internal", min_length=1, max_length=50)
|
||||
expires_at: datetime | None = None
|
||||
|
||||
|
||||
class PostboxRouteDryRunResponse(BaseModel):
|
||||
status: str
|
||||
source_postbox_id: str | None = None
|
||||
policy: PostboxRoutingPolicyPayload = Field(
|
||||
default_factory=PostboxRoutingPolicyPayload
|
||||
)
|
||||
routes: list[PostboxRoutePreviewTarget] = Field(default_factory=list)
|
||||
diagnostics: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class PostboxExactCreateRequest(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=500)
|
||||
description: str | None = None
|
||||
@@ -175,6 +313,9 @@ class PostboxTemplateRevisionPayload(BaseModel):
|
||||
)
|
||||
classification: str = Field(default="internal", min_length=1, max_length=50)
|
||||
allow_vacant_delivery: bool = True
|
||||
routing_policy: PostboxRoutingPolicyPayload = Field(
|
||||
default_factory=PostboxRoutingPolicyPayload
|
||||
)
|
||||
|
||||
|
||||
class PostboxTemplateCreateRequest(PostboxTemplateRevisionPayload):
|
||||
@@ -188,7 +329,6 @@ class PostboxTemplateRevisionItem(PostboxTemplateRevisionPayload):
|
||||
revision: int
|
||||
encryption_profile: str
|
||||
history_policy: dict[str, Any] = Field(default_factory=dict)
|
||||
routing_policy: dict[str, Any] = Field(default_factory=dict)
|
||||
retention_policy: dict[str, Any] = Field(default_factory=dict)
|
||||
published_at: datetime | None = None
|
||||
created_at: datetime
|
||||
@@ -240,8 +380,31 @@ class PostboxOrganizationUnitItem(BaseModel):
|
||||
functions: list[PostboxOrganizationFunctionItem] = Field(default_factory=list)
|
||||
|
||||
|
||||
class PostboxOrganizationRelationTypeItem(BaseModel):
|
||||
id: str
|
||||
slug: str
|
||||
name: str
|
||||
structure_id: str | None = None
|
||||
is_hierarchical: bool = True
|
||||
status: str = "active"
|
||||
|
||||
|
||||
class PostboxOrganizationStructureItem(BaseModel):
|
||||
id: str
|
||||
slug: str
|
||||
name: str
|
||||
structure_kind: str
|
||||
status: str = "active"
|
||||
relation_types: list[PostboxOrganizationRelationTypeItem] = Field(
|
||||
default_factory=list
|
||||
)
|
||||
|
||||
|
||||
class PostboxOrganizationTargetsResponse(BaseModel):
|
||||
units: list[PostboxOrganizationUnitItem]
|
||||
structures: list[PostboxOrganizationStructureItem] = Field(
|
||||
default_factory=list
|
||||
)
|
||||
|
||||
|
||||
class PostboxGroupingPayload(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user