Implement address quality and reversible contact merges
This commit is contained in:
@@ -15,6 +15,13 @@ AddressSyncConflictResolution = Literal["keep_local", "use_remote", "merge", "ma
|
||||
AddressCardDavAuthType = Literal["none", "basic", "bearer"]
|
||||
AddressSyncPlanAction = Literal["create", "update", "delete", "remote_create", "remote_update", "remote_delete", "conflict", "unchanged", "error"]
|
||||
AddressDistributionChannel = Literal["email", "postal", "internal_mail", "portal"]
|
||||
AddressContactPointChannel = Literal[
|
||||
"email",
|
||||
"phone",
|
||||
"postal",
|
||||
"internal_mail",
|
||||
"portal",
|
||||
]
|
||||
AddressChannelDecision = Literal[
|
||||
"allowed",
|
||||
"opted_in",
|
||||
@@ -38,6 +45,13 @@ AddressDistributionOutcome = Literal[
|
||||
]
|
||||
AddressContactPointFallbackRule = Literal["none", "primary", "any"]
|
||||
AddressPostalFormat = Literal["domestic", "international"]
|
||||
ContactPointQualityState = Literal[
|
||||
"valid",
|
||||
"invalid",
|
||||
"returned",
|
||||
"stale",
|
||||
"undeliverable",
|
||||
]
|
||||
|
||||
|
||||
class ContactEmailPayload(BaseModel):
|
||||
@@ -186,12 +200,75 @@ class ContactUpdateRequest(BaseModel):
|
||||
provenance: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class ContactFieldProvenanceResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: str
|
||||
contact_id: str
|
||||
field_path: str
|
||||
value: Any = None
|
||||
source_kind: str
|
||||
source_ref: str | None = None
|
||||
source_revision: str | None = None
|
||||
precedence: int
|
||||
selected: bool
|
||||
reason_code: str
|
||||
explanation: str | None = None
|
||||
visibility: str
|
||||
merge_record_id: str | None = None
|
||||
created_by_account_id: str | None = None
|
||||
metadata: dict[str, Any] = Field(default_factory=dict, validation_alias="metadata_")
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ContactPointQualityDecisionCreateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
channel: AddressContactPointChannel
|
||||
contact_point_id: str | None = Field(default=None, max_length=36)
|
||||
state: ContactPointQualityState
|
||||
reason_code: str | None = Field(default=None, max_length=120)
|
||||
reason: str | None = None
|
||||
evidence_ref: str | None = Field(default=None, max_length=1000)
|
||||
effective_from: datetime | None = None
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ContactPointQualityDecisionResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: str
|
||||
tenant_id: str | None = None
|
||||
contact_id: str
|
||||
channel: AddressContactPointChannel
|
||||
contact_point_id: str | None = None
|
||||
state: ContactPointQualityState
|
||||
reason_code: str
|
||||
reason: str | None = None
|
||||
evidence_ref: str | None = None
|
||||
effective_from: datetime
|
||||
effective_until: datetime | None = None
|
||||
created_by_account_id: str | None = None
|
||||
metadata: dict[str, Any] = Field(default_factory=dict, validation_alias="metadata_")
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class ContactPointQualityDecisionListResponse(BaseModel):
|
||||
decisions: list[ContactPointQualityDecisionResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ContactEmailResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: str
|
||||
label: str | None = None
|
||||
email: str
|
||||
original_email: str = ""
|
||||
normalized_email: str = ""
|
||||
provenance: dict[str, Any] = Field(default_factory=dict)
|
||||
quality_state: ContactPointQualityState = "valid"
|
||||
quality_reason_code: str | None = None
|
||||
is_primary: bool
|
||||
|
||||
|
||||
@@ -201,6 +278,11 @@ class ContactPhoneResponse(BaseModel):
|
||||
id: str
|
||||
label: str | None = None
|
||||
phone: str
|
||||
original_phone: str = ""
|
||||
normalized_phone: str = ""
|
||||
provenance: dict[str, Any] = Field(default_factory=dict)
|
||||
quality_state: ContactPointQualityState = "valid"
|
||||
quality_reason_code: str | None = None
|
||||
is_primary: bool
|
||||
|
||||
|
||||
@@ -214,6 +296,11 @@ class ContactPostalAddressResponse(BaseModel):
|
||||
locality: str | None = None
|
||||
region: str | None = None
|
||||
country: str | None = None
|
||||
original_value: dict[str, Any] = Field(default_factory=dict)
|
||||
normalized_value: dict[str, Any] = Field(default_factory=dict)
|
||||
provenance: dict[str, Any] = Field(default_factory=dict)
|
||||
quality_state: ContactPointQualityState = "valid"
|
||||
quality_reason_code: str | None = None
|
||||
is_primary: bool
|
||||
|
||||
|
||||
@@ -238,6 +325,7 @@ class ContactResponse(BaseModel):
|
||||
emails: list[ContactEmailResponse]
|
||||
phones: list[ContactPhoneResponse]
|
||||
postal_addresses: list[ContactPostalAddressResponse]
|
||||
field_provenance: list[ContactFieldProvenanceResponse] = Field(default_factory=list)
|
||||
deleted_at: datetime | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
@@ -251,6 +339,103 @@ class ContactListResponse(BaseModel):
|
||||
has_more: bool
|
||||
|
||||
|
||||
class ContactDuplicateFeatureResponse(BaseModel):
|
||||
code: str
|
||||
label: str
|
||||
weight: int
|
||||
value: str
|
||||
|
||||
|
||||
class ContactDuplicateSuggestionResponse(BaseModel):
|
||||
left: ContactResponse
|
||||
right: ContactResponse
|
||||
score: int
|
||||
confidence: Literal["possible", "likely", "strong"]
|
||||
features: list[ContactDuplicateFeatureResponse]
|
||||
|
||||
|
||||
class ContactDuplicateSuggestionListResponse(BaseModel):
|
||||
suggestions: list[ContactDuplicateSuggestionResponse] = Field(default_factory=list)
|
||||
scanned_contacts: int
|
||||
candidate_pairs: int
|
||||
truncated: bool
|
||||
|
||||
|
||||
class ContactMergeRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
winner_contact_id: str = Field(max_length=36)
|
||||
duplicate_contact_ids: list[str] = Field(min_length=1, max_length=20)
|
||||
reason: str = Field(min_length=3)
|
||||
field_sources: dict[str, str] = Field(default_factory=dict)
|
||||
contact_point_strategy: Literal["union", "winner_only"] = "union"
|
||||
source_precedence: list[str] = Field(default_factory=list, max_length=20)
|
||||
|
||||
|
||||
class ContactMergeRecoveryRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
reason: str = Field(min_length=3)
|
||||
expected_after_hash: str = Field(min_length=64, max_length=64)
|
||||
|
||||
|
||||
class ContactMergeRecordResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: str
|
||||
tenant_id: str | None = None
|
||||
address_book_id: str
|
||||
winner_contact_id: str
|
||||
loser_contact_ids: list[str]
|
||||
status: str
|
||||
reason: str
|
||||
survivorship: dict[str, Any]
|
||||
decisions: list[dict[str, Any]]
|
||||
before_hash: str
|
||||
after_hash: str
|
||||
created_by_account_id: str | None = None
|
||||
recovered_at: datetime | None = None
|
||||
recovered_by_account_id: str | None = None
|
||||
recovery_action: str | None = None
|
||||
recovery_reason: str | None = None
|
||||
provenance: dict[str, Any]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class ContactMergeRecordListResponse(BaseModel):
|
||||
merges: list[ContactMergeRecordResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ContactRedirectResponse(BaseModel):
|
||||
requested_contact_id: str
|
||||
resolved_contact_id: str
|
||||
redirected: bool
|
||||
redirect_chain: list[str] = Field(default_factory=list)
|
||||
merge_record_ids: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class AddressQualityCorrectionResponse(BaseModel):
|
||||
contact_id: str
|
||||
display_name: str
|
||||
channel: AddressContactPointChannel
|
||||
contact_point_id: str | None = None
|
||||
state: ContactPointQualityState
|
||||
reason_code: str
|
||||
reason: str | None = None
|
||||
effective_from: datetime
|
||||
|
||||
|
||||
class AddressQualitySummaryResponse(BaseModel):
|
||||
contact_count: int
|
||||
contact_point_count: int
|
||||
quality_counts: dict[str, int] = Field(default_factory=dict)
|
||||
duplicate_suggestion_count: int
|
||||
correction_count: int
|
||||
corrections: list[AddressQualityCorrectionResponse] = Field(default_factory=list)
|
||||
truncated: bool = False
|
||||
|
||||
|
||||
class ContactChannelRuleCreateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user