322 lines
9.3 KiB
Python
322 lines
9.3 KiB
Python
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)
|