Implement address quality and reversible contact merges

This commit is contained in:
2026-08-02 07:03:27 +02:00
parent 2e78b9ae50
commit 19e9096572
15 changed files with 4369 additions and 31 deletions
@@ -43,6 +43,10 @@ from govoplan_addresses.backend.provider_state import (
_addresses_table_retirement_provider = drop_table_retirement_provider(
addresses_models.ContactFieldProvenance,
addresses_models.ContactRedirect,
addresses_models.ContactMergeRecord,
addresses_models.ContactPointQualityDecision,
addresses_models.ContactPointSnapshot,
addresses_models.AddressSyncDiagnostic,
addresses_models.AddressSyncConflict,
@@ -154,6 +158,8 @@ def _tenant_summary(session, tenant_id: str) -> dict[str, int]:
AddressList,
AddressSyncSource,
Contact,
ContactMergeRecord,
ContactPointQualityDecision,
ContactPointSnapshot,
)
@@ -161,6 +167,8 @@ def _tenant_summary(session, tenant_id: str) -> dict[str, int]:
"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(),
"active_contact_merges": session.query(ContactMergeRecord).filter(ContactMergeRecord.tenant_id == tenant_id, ContactMergeRecord.status == "active").count(),
"contact_quality_decisions": session.query(ContactPointQualityDecision).filter(ContactPointQualityDecision.tenant_id == tenant_id).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(),
}
@@ -286,6 +294,10 @@ manifest = ModuleManifest(
addresses_models.AddressSyncSource,
addresses_models.AddressListEntry,
addresses_models.AddressList,
addresses_models.ContactFieldProvenance,
addresses_models.ContactRedirect,
addresses_models.ContactMergeRecord,
addresses_models.ContactPointQualityDecision,
addresses_models.ContactPointSnapshot,
addresses_models.AddressBook,
addresses_models.Contact,
@@ -328,6 +340,25 @@ manifest = ModuleManifest(
related_modules=("dist_lists", "campaigns", "policy", "templates"),
order=31,
),
DocumentationTopic(
id="addresses.quality-and-merge",
title="Contact quality, duplicates, and reversible merges",
summary="Review address quality and duplicate suggestions without losing source evidence.",
body=(
"Addresses preserves original and normalized contact-point values, records field-level provenance, "
"and projects invalid, returned, stale, or undeliverable states into recipient resolution with stable "
"reason codes. Duplicate suggestions are bounded and explain their matching features. An operator can "
"choose the surviving values, merge contact points, and later undo or split the merge while the recorded "
"post-merge evidence still matches. Contact redirects keep stored references resolvable, and address-list "
"memberships are repaired transactionally. Audit remains an optional integration; the Addresses change "
"sequence and merge evidence are always retained."
),
layer="configured",
documentation_types=("admin", "user"),
audience=("tenant_admin", "operator", "module_admin"),
related_modules=("campaigns", "dist_lists", "policy", "audit"),
order=32,
),
),
external_providers=(CARDDAV_PROVIDER,),
external_provider_state_providers=(