from __future__ import annotations from typing import Any from .models import CampaignConfig, EntryConfig, FieldDefinition def field_definitions_by_name(config: CampaignConfig) -> dict[str, FieldDefinition]: """Return campaign field definitions keyed by field id/name.""" return {field.name: field for field in config.fields} def field_can_override(config: CampaignConfig, field_name: str) -> bool: """Return whether a recipient/entry value may override the global value. Unknown fields remain overridable for backwards compatibility with older campaigns and ad-hoc external mappings. Semantic validation reports unknown field usage separately when a field list is configured. """ field = field_definitions_by_name(config).get(field_name) if field is None: return True return field.can_override def ignored_entry_field_overrides(config: CampaignConfig, entry: EntryConfig) -> list[str]: """Return recipient field keys that are ignored by the override policy.""" return sorted(name for name in entry.fields if not field_can_override(config, name)) def effective_entry_field_values(config: CampaignConfig, entry: EntryConfig) -> dict[str, Any]: """Return the local/effective field value map for one message entry. Global values act as defaults for local template placeholders. Recipient values replace those defaults only when the corresponding field allows overrides. Fields that are unknown to the campaign definition keep the old permissive behavior and remain usable as local values. """ values: dict[str, Any] = dict(config.global_values) for key, value in entry.fields.items(): if field_can_override(config, key) and entry_field_has_override_value(value): values[key] = value return values def entry_field_has_override_value(value: Any) -> bool: """Return whether an entry field should override a global default. Empty recipient values are treated as "not set" so global_values remain the effective local defaults. Numeric zero and boolean false are valid explicit overrides. """ if value is None: return False if isinstance(value, str): return value.strip() != "" return True