from __future__ import annotations from dataclasses import asdict from typing import Any from sqlalchemy.orm import Session from govoplan_core.core.postbox import ( PostboxDeliveryCatalogRef, PostboxDirectoryEntryRef, PostboxTargetRef, ) from govoplan_campaign.backend.campaign.field_values import ( effective_entry_field_values, ) from govoplan_campaign.backend.campaign.models import ( Behavior, CampaignConfig, EntryConfig, PostboxTargetConfig, PostboxTargetMatch, PostboxTargetMode, effective_postbox_targets, ) from govoplan_campaign.backend.integrations import postbox_integration from govoplan_campaign.backend.messages.models import ( MessageIssue, MessageValidationStatus, ) def _apply_behavior( current: MessageValidationStatus, behavior: Behavior, ) -> MessageValidationStatus: if behavior == Behavior.BLOCK: return MessageValidationStatus.BLOCKED if behavior == Behavior.DROP: return MessageValidationStatus.EXCLUDED if behavior == Behavior.ASK and current not in { MessageValidationStatus.BLOCKED, MessageValidationStatus.EXCLUDED, }: return MessageValidationStatus.NEEDS_REVIEW if behavior == Behavior.WARN and current == MessageValidationStatus.READY: return MessageValidationStatus.WARNING return current def _issue( *, code: str, message: str, behavior: Behavior, ) -> MessageIssue: return MessageIssue( severity="error" if behavior == Behavior.BLOCK else "warning", code=code, message=message, behavior=behavior.value, source="postbox", ) def _field_value( values: dict[str, Any], field_name: str | None, ) -> str | None: if not field_name: return None value = values.get(field_name) if value is None: return None text = str(value).strip() return text or None def _match_unit( catalog: PostboxDeliveryCatalogRef, value: str | None, match: PostboxTargetMatch, ): if not value: return None return next( ( unit for unit in catalog.organization_units if (unit.id if match == PostboxTargetMatch.ID else unit.slug) == value ), None, ) def _match_function(unit, value: str | None, match: PostboxTargetMatch): if unit is None or not value: return None return next( ( function for function in unit.functions if ( function.id if match == PostboxTargetMatch.ID else function.slug ) == value ), None, ) def _target_ref( target: PostboxTargetConfig, *, values: dict[str, Any], catalog: PostboxDeliveryCatalogRef, ) -> tuple[PostboxTargetRef | None, str | None]: if target.mode == PostboxTargetMode.DIRECT: return ( PostboxTargetRef( postbox_id=target.postbox_id, address_key=target.address_key, ), None, ) unit_value = target.organization_unit_id or _field_value( values, target.organization_unit_field, ) unit_match = ( PostboxTargetMatch.ID if target.organization_unit_id else target.organization_unit_match ) unit = _match_unit(catalog, unit_value, unit_match) if unit is None: return None, ( f"Organization unit {unit_value!r} could not be resolved by " f"{unit_match.value}." ) function_value = target.function_id or _field_value( values, target.function_field, ) function_match = ( PostboxTargetMatch.ID if target.function_id else target.function_match ) function = _match_function(unit, function_value, function_match) if function is None: return None, ( f"Organization function {function_value!r} could not be resolved " f"inside {unit.name!r} by {function_match.value}." ) context_key = target.context_key or _field_value( values, target.context_field, ) return ( PostboxTargetRef( template_id=target.template_id, organization_unit_id=unit.id, function_id=function.id, context_key=context_key, ), None, ) def _resolved_target_payload( target: PostboxTargetConfig, entry: PostboxDirectoryEntryRef, *, position: int, ) -> dict[str, Any]: return { "target_id": target.id, "position": position, "mode": target.mode.value, "requested": target.model_dump(mode="json", exclude_none=True), "postbox_id": entry.id, "address": entry.address, "address_key": entry.address_key, "name": entry.name, "status": entry.status, "classification": entry.classification, "organization_unit_id": entry.organization_unit_id, "organization_unit_name": entry.organization_unit_name, "function_id": entry.function_id, "function_name": entry.function_name, "context_key": entry.context_key, "template_revision_id": entry.template_revision_id, "holder_count": entry.holder_count, "vacant": entry.vacant, } def resolve_entry_postbox_targets( session: Session, *, tenant_id: str, config: CampaignConfig, entry: EntryConfig, validation_status: MessageValidationStatus, materialize: bool, ) -> tuple[ list[dict[str, Any]], list[MessageIssue], MessageValidationStatus, ]: integration = postbox_integration() policy = config.delivery.postbox targets = effective_postbox_targets(config, entry) if not targets: issue = _issue( code="postbox_target_missing", message="Postbox delivery requires at least one target.", behavior=policy.unresolved_target, ) return ( [], [issue], _apply_behavior(validation_status, policy.unresolved_target), ) try: catalog = integration.delivery_catalog(session, tenant_id=tenant_id) except Exception as exc: issue = _issue( code="postbox_unavailable", message=str(exc), behavior=Behavior.BLOCK, ) return [], [issue], MessageValidationStatus.BLOCKED values = effective_entry_field_values(config, entry) resolved: list[dict[str, Any]] = [] issues: list[MessageIssue] = [] status = validation_status seen_postbox_ids: set[str] = set() for position, target in enumerate(targets): target_ref, resolution_error = _target_ref( target, values=values, catalog=catalog, ) if target_ref is None: issue = _issue( code="postbox_target_unresolved", message=resolution_error or "Postbox target could not be resolved.", behavior=policy.unresolved_target, ) issues.append(issue) status = _apply_behavior(status, policy.unresolved_target) continue try: entry_ref = integration.resolve_postbox( session, tenant_id=tenant_id, target=target_ref, materialize=materialize, ) except Exception as exc: issue = _issue( code="postbox_target_unresolved", message=f"Postbox target {target.id!r} could not be resolved: {exc}", behavior=policy.unresolved_target, ) issues.append(issue) status = _apply_behavior(status, policy.unresolved_target) continue if entry_ref is None: issue = _issue( code="postbox_target_unresolved", message=f"Postbox target {target.id!r} does not exist.", behavior=policy.unresolved_target, ) issues.append(issue) status = _apply_behavior(status, policy.unresolved_target) continue if entry_ref.id in seen_postbox_ids: issue = _issue( code="postbox_target_duplicate", message=( f"Postbox {entry_ref.address!r} is selected more than once; " "it will receive one message." ), behavior=policy.duplicate_target, ) issues.append(issue) status = _apply_behavior(status, policy.duplicate_target) continue seen_postbox_ids.add(entry_ref.id) resolved.append( _resolved_target_payload( target, entry_ref, position=position, ) ) if entry_ref.vacant: issue = _issue( code="postbox_target_vacant", message=( f"Postbox {entry_ref.address!r} currently has no function " "holder." ), behavior=policy.vacant_target, ) issues.append(issue) status = _apply_behavior(status, policy.vacant_target) return resolved, issues, status def delivery_catalog_payload(catalog: PostboxDeliveryCatalogRef) -> dict[str, Any]: return asdict(catalog)