Add governed contact point snapshots
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -56,6 +56,11 @@ class Contact(Base, TimestampMixin):
|
||||
__table_args__ = (
|
||||
Index("ix_addresses_contacts_book_name", "address_book_id", "display_name"),
|
||||
Index("ix_addresses_contacts_tenant_name", "tenant_id", "display_name"),
|
||||
Index(
|
||||
"ix_addresses_contacts_source_ref",
|
||||
"source_ref",
|
||||
postgresql_using="hash",
|
||||
),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
@@ -191,6 +196,31 @@ class ContactChannelRule(Base, TimestampMixin):
|
||||
contact: Mapped[Contact] = relationship(back_populates="channel_rules")
|
||||
|
||||
|
||||
class ContactPointSnapshot(Base, TimestampMixin):
|
||||
__tablename__ = "addresses_contact_point_snapshots"
|
||||
__table_args__ = (
|
||||
Index("ix_addresses_contact_point_snapshots_source", "tenant_id", "source_id", "created_at"),
|
||||
Index("ix_addresses_contact_point_snapshots_hash", "tenant_id", "snapshot_hash"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
||||
source_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
||||
contract_version: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
source_revision: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
source_fingerprint: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
purpose: Mapped[str | None] = mapped_column(String(120), nullable=True, index=True)
|
||||
effective_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
|
||||
generated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
|
||||
request_payload: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False)
|
||||
resolution_payload: Mapped[list[dict[str, Any]]] = mapped_column(JSON, nullable=False)
|
||||
recipient_count: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
excluded_count: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
snapshot_hash: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||||
created_by_account_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
|
||||
provenance: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
||||
|
||||
|
||||
class AddressList(Base, TimestampMixin):
|
||||
__tablename__ = "addresses_address_lists"
|
||||
__table_args__ = (
|
||||
@@ -371,6 +401,7 @@ __all__ = [
|
||||
"Contact",
|
||||
"ContactEmail",
|
||||
"ContactPhone",
|
||||
"ContactPointSnapshot",
|
||||
"ContactPostalAddress",
|
||||
"new_uuid",
|
||||
]
|
||||
|
||||
@@ -12,6 +12,7 @@ from govoplan_addresses.backend.capabilities import (
|
||||
)
|
||||
from govoplan_addresses.backend.db import models as addresses_models # noqa: F401 - populate address ORM metadata
|
||||
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
|
||||
from govoplan_core.core.contact_points import CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION
|
||||
from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard
|
||||
from govoplan_core.core.people import CAPABILITY_ADDRESSES_PEOPLE_SEARCH
|
||||
from govoplan_core.core.distribution_lists import CAPABILITY_RECIPIENT_CHANNEL_FACTS
|
||||
@@ -42,6 +43,7 @@ from govoplan_addresses.backend.provider_state import (
|
||||
|
||||
|
||||
_addresses_table_retirement_provider = drop_table_retirement_provider(
|
||||
addresses_models.ContactPointSnapshot,
|
||||
addresses_models.AddressSyncDiagnostic,
|
||||
addresses_models.AddressSyncConflict,
|
||||
addresses_models.AddressSyncTombstone,
|
||||
@@ -147,12 +149,19 @@ ROLE_TEMPLATES = (
|
||||
|
||||
|
||||
def _tenant_summary(session, tenant_id: str) -> dict[str, int]:
|
||||
from govoplan_addresses.backend.db.models import AddressBook, AddressList, AddressSyncSource, Contact
|
||||
from govoplan_addresses.backend.db.models import (
|
||||
AddressBook,
|
||||
AddressList,
|
||||
AddressSyncSource,
|
||||
Contact,
|
||||
ContactPointSnapshot,
|
||||
)
|
||||
|
||||
return {
|
||||
"address_books": session.query(AddressBook).filter(AddressBook.tenant_id == tenant_id, AddressBook.deleted_at.is_(None)).count(),
|
||||
"address_lists": session.query(AddressList).filter(AddressList.tenant_id == tenant_id, AddressList.deleted_at.is_(None)).count(),
|
||||
"contacts": session.query(Contact).filter(Contact.tenant_id == tenant_id, Contact.deleted_at.is_(None)).count(),
|
||||
"contact_point_snapshots": session.query(ContactPointSnapshot).filter(ContactPointSnapshot.tenant_id == tenant_id).count(),
|
||||
"sync_sources": session.query(AddressSyncSource).filter(AddressSyncSource.tenant_id == tenant_id, AddressSyncSource.enabled.is_(True)).count(),
|
||||
}
|
||||
|
||||
@@ -226,6 +235,7 @@ manifest = ModuleManifest(
|
||||
ModuleInterfaceProvider(name=CAPABILITY_ADDRESSES_LOOKUP, version="0.1.8"),
|
||||
ModuleInterfaceProvider(name=CAPABILITY_ADDRESSES_PEOPLE_SEARCH, version="0.1.0"),
|
||||
ModuleInterfaceProvider(name=CAPABILITY_ADDRESSES_RECIPIENT_SOURCE, version="0.1.9"),
|
||||
ModuleInterfaceProvider(name=CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION, version="1.0.0"),
|
||||
ModuleInterfaceProvider(name=CAPABILITY_ADDRESSES_CONTACT_WRITER, version="0.1.8"),
|
||||
ModuleInterfaceProvider(name=CAPABILITY_RECIPIENT_CHANNEL_FACTS, version="0.1.0"),
|
||||
),
|
||||
@@ -263,6 +273,10 @@ manifest = ModuleManifest(
|
||||
"govoplan_addresses.backend.capabilities",
|
||||
fromlist=["channel_facts_capability"],
|
||||
).channel_facts_capability(context),
|
||||
CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION: lambda context: __import__(
|
||||
"govoplan_addresses.backend.capabilities",
|
||||
fromlist=["contact_point_resolution_capability"],
|
||||
).contact_point_resolution_capability(context),
|
||||
},
|
||||
uninstall_guard_providers=(
|
||||
persistent_table_uninstall_guard(
|
||||
@@ -272,6 +286,7 @@ manifest = ModuleManifest(
|
||||
addresses_models.AddressSyncSource,
|
||||
addresses_models.AddressListEntry,
|
||||
addresses_models.AddressList,
|
||||
addresses_models.ContactPointSnapshot,
|
||||
addresses_models.AddressBook,
|
||||
addresses_models.Contact,
|
||||
addresses_models.ContactEmail,
|
||||
@@ -297,6 +312,22 @@ manifest = ModuleManifest(
|
||||
related_modules=("campaigns", "mail", "forms", "reporting", "portal", "postbox"),
|
||||
order=30,
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="addresses.contact-point-resolution",
|
||||
title="Contact-point resolution and snapshots",
|
||||
summary="Resolve purpose-aware channel targets and freeze immutable recipient evidence.",
|
||||
body=(
|
||||
"Addresses exposes a versioned contact-point capability for email, postal, internal-mail, and portal targets. "
|
||||
"Callers can request an effective date, communication purpose, address purpose, fallback rule, locale, and "
|
||||
"postal format. Bounded previews remain live; frozen snapshots retain the resolved values, exclusions, "
|
||||
"source and governance revisions, provenance, and a deterministic evidence hash even after contacts change."
|
||||
),
|
||||
layer="configured",
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("tenant_admin", "operator", "module_admin"),
|
||||
related_modules=("dist_lists", "campaigns", "policy", "templates"),
|
||||
order=31,
|
||||
),
|
||||
),
|
||||
external_providers=(CARDDAV_PROVIDER,),
|
||||
external_provider_state_providers=(
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
"""Add immutable contact-point snapshots.
|
||||
|
||||
Revision ID: a3b5c6d7e8f9
|
||||
Revises: f2a4b5c6d7e
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "a3b5c6d7e8f9"
|
||||
down_revision = "f2a4b5c6d7e"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_index(
|
||||
"ix_addresses_contacts_source_ref",
|
||||
"addresses_contacts",
|
||||
["source_ref"],
|
||||
unique=False,
|
||||
postgresql_using="hash",
|
||||
)
|
||||
op.create_table(
|
||||
"addresses_contact_point_snapshots",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("tenant_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("source_id", sa.String(length=255), nullable=False),
|
||||
sa.Column("contract_version", sa.String(length=20), nullable=False),
|
||||
sa.Column("source_revision", sa.String(length=255), nullable=False),
|
||||
sa.Column("source_fingerprint", sa.String(length=64), nullable=False),
|
||||
sa.Column("purpose", sa.String(length=120), nullable=True),
|
||||
sa.Column("effective_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("generated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("request_payload", sa.JSON(), nullable=False),
|
||||
sa.Column("resolution_payload", sa.JSON(), nullable=False),
|
||||
sa.Column("recipient_count", sa.Integer(), nullable=False),
|
||||
sa.Column("excluded_count", sa.Integer(), nullable=False),
|
||||
sa.Column("snapshot_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("created_by_account_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("provenance", sa.JSON(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
for name, columns in (
|
||||
("ix_addresses_contact_point_snapshots_tenant_id", ["tenant_id"]),
|
||||
("ix_addresses_contact_point_snapshots_source_id", ["source_id"]),
|
||||
("ix_addresses_contact_point_snapshots_purpose", ["purpose"]),
|
||||
("ix_addresses_contact_point_snapshots_effective_at", ["effective_at"]),
|
||||
("ix_addresses_contact_point_snapshots_generated_at", ["generated_at"]),
|
||||
("ix_addresses_contact_point_snapshots_snapshot_hash", ["snapshot_hash"]),
|
||||
("ix_addresses_contact_point_snapshots_created_by_account_id", ["created_by_account_id"]),
|
||||
("ix_addresses_contact_point_snapshots_source", ["tenant_id", "source_id", "created_at"]),
|
||||
("ix_addresses_contact_point_snapshots_hash", ["tenant_id", "snapshot_hash"]),
|
||||
):
|
||||
op.create_index(name, "addresses_contact_point_snapshots", columns, unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("addresses_contact_point_snapshots")
|
||||
op.drop_index("ix_addresses_contacts_source_ref", table_name="addresses_contacts")
|
||||
@@ -8,6 +8,11 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.audit.logging import audit_from_principal
|
||||
from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
|
||||
from govoplan_core.core.contact_points import (
|
||||
ContactPointResolutionRequest,
|
||||
ContactPointSourceRequest,
|
||||
)
|
||||
from govoplan_core.core.distribution_lists import DistributionSourceReference
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_addresses.backend.carddav import AddressCardDAVError
|
||||
from govoplan_addresses.backend.db.models import (
|
||||
@@ -22,7 +27,10 @@ from govoplan_addresses.backend.db.models import (
|
||||
ContactChannelRule,
|
||||
ContactPostalAddress,
|
||||
)
|
||||
from govoplan_addresses.backend.capabilities import AddressesContactWriterCapability
|
||||
from govoplan_addresses.backend.capabilities import (
|
||||
AddressesContactPointResolutionCapability,
|
||||
AddressesContactWriterCapability,
|
||||
)
|
||||
from govoplan_addresses.backend.schemas import (
|
||||
AddressBookCreateRequest,
|
||||
AddressBookListResponse,
|
||||
@@ -66,6 +74,11 @@ from govoplan_addresses.backend.schemas import (
|
||||
ContactChannelRuleCreateRequest,
|
||||
ContactChannelRuleListResponse,
|
||||
ContactChannelRuleResponse,
|
||||
ContactPointResolveRequest,
|
||||
ContactPointResolutionResponse,
|
||||
ContactPointSnapshotResponse,
|
||||
ContactPointSourcePreviewResponse,
|
||||
ContactPointSourceRequestPayload,
|
||||
ContactListResponse,
|
||||
ContactResponse,
|
||||
ContactUpdateRequest,
|
||||
@@ -212,6 +225,43 @@ def _write_decision_response(decision) -> AddressBookWriteDecisionResponse:
|
||||
return AddressBookWriteDecisionResponse.model_validate(payload)
|
||||
|
||||
|
||||
def _contact_point_resolution_request(
|
||||
principal: ApiPrincipal,
|
||||
payload: ContactPointResolveRequest,
|
||||
) -> ContactPointResolutionRequest:
|
||||
return ContactPointResolutionRequest(
|
||||
tenant_id=principal.tenant_id,
|
||||
subject=DistributionSourceReference(**payload.subject.model_dump()),
|
||||
effective_at=payload.effective_at,
|
||||
purpose=payload.purpose,
|
||||
requested_channels=tuple(payload.requested_channels),
|
||||
address_purpose=payload.address_purpose,
|
||||
fallback_rule=payload.fallback_rule,
|
||||
locale=payload.locale,
|
||||
postal_format=payload.postal_format,
|
||||
context=payload.context,
|
||||
)
|
||||
|
||||
|
||||
def _contact_point_source_request(
|
||||
principal: ApiPrincipal,
|
||||
payload: ContactPointSourceRequestPayload,
|
||||
) -> ContactPointSourceRequest:
|
||||
return ContactPointSourceRequest(
|
||||
tenant_id=principal.tenant_id,
|
||||
source_id=payload.source_id,
|
||||
effective_at=payload.effective_at,
|
||||
purpose=payload.purpose,
|
||||
requested_channels=tuple(payload.requested_channels),
|
||||
address_purpose=payload.address_purpose,
|
||||
fallback_rule=payload.fallback_rule,
|
||||
locale=payload.locale,
|
||||
postal_format=payload.postal_format,
|
||||
max_items=payload.max_items,
|
||||
context=payload.context,
|
||||
)
|
||||
|
||||
|
||||
def _sync_source_response(sync_source: AddressSyncSource) -> AddressSyncSourceResponse:
|
||||
return AddressSyncSourceResponse.model_validate(
|
||||
{
|
||||
@@ -493,6 +543,124 @@ def api_lookup_addresses(
|
||||
return AddressLookupResponse(contacts=[_contact_response(contact) for contact in contacts])
|
||||
|
||||
|
||||
@router.post("/contact-points/resolve", response_model=ContactPointResolutionResponse)
|
||||
def api_resolve_contact_points(
|
||||
payload: ContactPointResolveRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:read")
|
||||
_require_scope(principal, "addresses:governance:read")
|
||||
try:
|
||||
result = AddressesContactPointResolutionCapability().resolve_contact_points(
|
||||
session,
|
||||
principal,
|
||||
request=_contact_point_resolution_request(principal, payload),
|
||||
)
|
||||
return ContactPointResolutionResponse.model_validate(asdict(result))
|
||||
except (AddressBookError, ValueError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/contact-point-sources/preview",
|
||||
response_model=ContactPointSourcePreviewResponse,
|
||||
)
|
||||
def api_preview_contact_point_source(
|
||||
payload: ContactPointSourceRequestPayload,
|
||||
offset: int = Query(default=0, ge=0),
|
||||
limit: int = Query(default=100, ge=1, le=500),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:read")
|
||||
_require_scope(principal, "addresses:governance:read")
|
||||
try:
|
||||
result = AddressesContactPointResolutionCapability().preview_source(
|
||||
session,
|
||||
principal,
|
||||
request=_contact_point_source_request(principal, payload),
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
)
|
||||
return ContactPointSourcePreviewResponse.model_validate(asdict(result))
|
||||
except (AddressBookError, ValueError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/contact-point-snapshots",
|
||||
response_model=ContactPointSnapshotResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_freeze_contact_point_source(
|
||||
payload: ContactPointSourceRequestPayload,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:read")
|
||||
_require_scope(principal, "addresses:governance:read")
|
||||
try:
|
||||
snapshot = AddressesContactPointResolutionCapability().freeze_source(
|
||||
session,
|
||||
principal,
|
||||
request=_contact_point_source_request(principal, payload),
|
||||
)
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="addresses.contact_point_snapshot_created",
|
||||
object_type="address_contact_point_snapshot",
|
||||
object_id=snapshot.id,
|
||||
details={
|
||||
"source_id": snapshot.request.source_id,
|
||||
"source_revision": snapshot.source_revision,
|
||||
"snapshot_hash": snapshot.snapshot_hash,
|
||||
"recipient_count": snapshot.recipient_count,
|
||||
"excluded_count": snapshot.excluded_count,
|
||||
"purpose": snapshot.request.purpose,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
return ContactPointSnapshotResponse.model_validate(asdict(snapshot))
|
||||
except (AddressBookError, ValueError) as exc:
|
||||
session.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/contact-point-snapshots/{snapshot_id}",
|
||||
response_model=ContactPointSnapshotResponse,
|
||||
)
|
||||
def api_get_contact_point_snapshot(
|
||||
snapshot_id: str,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:read")
|
||||
_require_scope(principal, "addresses:governance:read")
|
||||
snapshot = AddressesContactPointResolutionCapability().get_snapshot(
|
||||
session,
|
||||
principal,
|
||||
snapshot_id=snapshot_id,
|
||||
)
|
||||
if snapshot is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Contact-point snapshot not found.",
|
||||
)
|
||||
return ContactPointSnapshotResponse.model_validate(asdict(snapshot))
|
||||
|
||||
|
||||
@router.get("/address-lists", response_model=AddressListListResponse)
|
||||
def api_list_address_lists(
|
||||
address_book_id: str | None = Query(default=None),
|
||||
|
||||
@@ -25,6 +25,19 @@ AddressChannelDecision = Literal[
|
||||
"returned",
|
||||
"temporarily_unavailable",
|
||||
]
|
||||
AddressDistributionOutcome = Literal[
|
||||
"usable",
|
||||
"unresolved",
|
||||
"invalid",
|
||||
"suppressed",
|
||||
"ambiguous",
|
||||
"duplicate",
|
||||
"policy_blocked",
|
||||
"provider_unavailable",
|
||||
"stale",
|
||||
]
|
||||
AddressContactPointFallbackRule = Literal["none", "primary", "any"]
|
||||
AddressPostalFormat = Literal["domestic", "international"]
|
||||
|
||||
|
||||
class ContactEmailPayload(BaseModel):
|
||||
@@ -282,6 +295,125 @@ class ContactChannelRuleListResponse(BaseModel):
|
||||
rules: list[ContactChannelRuleResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class AddressSourceReferencePayload(BaseModel):
|
||||
provider: str = Field(min_length=1, max_length=120)
|
||||
resource_type: str = Field(min_length=1, max_length=120)
|
||||
resource_id: str = Field(min_length=1, max_length=1000)
|
||||
revision: str | None = Field(default=None, max_length=1000)
|
||||
fingerprint: str | None = Field(default=None, max_length=255)
|
||||
label: str | None = Field(default=None, max_length=500)
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ContactPointResolveRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
subject: AddressSourceReferencePayload
|
||||
effective_at: datetime
|
||||
purpose: str | None = Field(default=None, max_length=120)
|
||||
requested_channels: list[AddressDistributionChannel] = Field(default_factory=list)
|
||||
address_purpose: str | None = Field(default=None, max_length=80)
|
||||
fallback_rule: AddressContactPointFallbackRule = "primary"
|
||||
locale: str | None = Field(default=None, max_length=20)
|
||||
postal_format: AddressPostalFormat = "domestic"
|
||||
context: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ContactPointSourceRequestPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
source_id: str = Field(min_length=1, max_length=1000)
|
||||
effective_at: datetime
|
||||
purpose: str | None = Field(default=None, max_length=120)
|
||||
requested_channels: list[AddressDistributionChannel] = Field(default_factory=list)
|
||||
address_purpose: str | None = Field(default=None, max_length=80)
|
||||
fallback_rule: AddressContactPointFallbackRule = "primary"
|
||||
locale: str | None = Field(default=None, max_length=20)
|
||||
postal_format: AddressPostalFormat = "domestic"
|
||||
max_items: int = Field(default=5000, ge=1, le=20000)
|
||||
context: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ContactPointSourceRequestResponse(ContactPointSourceRequestPayload):
|
||||
tenant_id: str
|
||||
|
||||
|
||||
class ContactPointCandidateResponse(BaseModel):
|
||||
channel: AddressDistributionChannel
|
||||
target: str
|
||||
target_key: str
|
||||
status: AddressDistributionOutcome
|
||||
contact_point_id: str | None = None
|
||||
address_purpose: str | None = None
|
||||
locale: str | None = None
|
||||
preferred: bool = False
|
||||
preference_rank: int | None = None
|
||||
reason_code: str | None = None
|
||||
explanation: str | None = None
|
||||
source: AddressSourceReferencePayload | None = None
|
||||
source_revision: str | None = None
|
||||
preference_revision: str | None = None
|
||||
consent_revision: str | None = None
|
||||
value: dict[str, Any] = Field(default_factory=dict)
|
||||
provenance: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class DistributionExplanationResponse(BaseModel):
|
||||
code: str
|
||||
message: str
|
||||
severity: Literal["info", "warning", "error"]
|
||||
provider: str | None = None
|
||||
source: AddressSourceReferencePayload | None = None
|
||||
provenance: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ContactPointResolutionResponse(BaseModel):
|
||||
contract_version: str
|
||||
subject: AddressSourceReferencePayload
|
||||
status: AddressDistributionOutcome
|
||||
contact_id: str | None = None
|
||||
display_name: str | None = None
|
||||
candidates: list[ContactPointCandidateResponse] = Field(default_factory=list)
|
||||
excluded: list[ContactPointCandidateResponse] = Field(default_factory=list)
|
||||
explanations: list[DistributionExplanationResponse] = Field(default_factory=list)
|
||||
source_revision: str | None = None
|
||||
source_fingerprint: str | None = None
|
||||
provenance: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ContactPointSourcePreviewResponse(BaseModel):
|
||||
contract_version: str
|
||||
source: AddressSourceReferencePayload
|
||||
request: ContactPointSourceRequestResponse
|
||||
resolutions: list[ContactPointResolutionResponse]
|
||||
total_count: int
|
||||
usable_count: int
|
||||
excluded_count: int
|
||||
offset: int
|
||||
limit: int
|
||||
has_more: bool
|
||||
source_revision: str
|
||||
source_fingerprint: str
|
||||
generated_at: datetime
|
||||
provenance: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ContactPointSnapshotResponse(BaseModel):
|
||||
id: str
|
||||
tenant_id: str
|
||||
contract_version: str
|
||||
source: AddressSourceReferencePayload
|
||||
request: ContactPointSourceRequestResponse
|
||||
resolutions: list[ContactPointResolutionResponse]
|
||||
recipient_count: int
|
||||
excluded_count: int
|
||||
source_revision: str
|
||||
source_fingerprint: str
|
||||
snapshot_hash: str
|
||||
generated_at: datetime
|
||||
provenance: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class AddressLookupResponse(BaseModel):
|
||||
contacts: list[ContactResponse]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user