Implement address quality and reversible contact merges
This commit is contained in:
@@ -39,6 +39,11 @@ export type ContactEmail = {
|
||||
id?: string;
|
||||
label?: string | null;
|
||||
email: string;
|
||||
original_email?: string;
|
||||
normalized_email?: string;
|
||||
provenance?: Record<string, unknown>;
|
||||
quality_state?: ContactPointQualityState;
|
||||
quality_reason_code?: string | null;
|
||||
is_primary: boolean;
|
||||
};
|
||||
|
||||
@@ -46,6 +51,11 @@ export type ContactPhone = {
|
||||
id?: string;
|
||||
label?: string | null;
|
||||
phone: string;
|
||||
original_phone?: string;
|
||||
normalized_phone?: string;
|
||||
provenance?: Record<string, unknown>;
|
||||
quality_state?: ContactPointQualityState;
|
||||
quality_reason_code?: string | null;
|
||||
is_primary: boolean;
|
||||
};
|
||||
|
||||
@@ -57,9 +67,35 @@ export type ContactPostalAddress = {
|
||||
locality?: string | null;
|
||||
region?: string | null;
|
||||
country?: string | null;
|
||||
original_value?: Record<string, unknown>;
|
||||
normalized_value?: Record<string, unknown>;
|
||||
provenance?: Record<string, unknown>;
|
||||
quality_state?: ContactPointQualityState;
|
||||
quality_reason_code?: string | null;
|
||||
is_primary: boolean;
|
||||
};
|
||||
|
||||
export type ContactPointQualityState = "valid" | "invalid" | "returned" | "stale" | "undeliverable";
|
||||
|
||||
export type ContactFieldProvenance = {
|
||||
id: string;
|
||||
contact_id: string;
|
||||
field_path: string;
|
||||
value?: unknown;
|
||||
source_kind: string;
|
||||
source_ref?: string | null;
|
||||
source_revision?: string | null;
|
||||
precedence: number;
|
||||
selected: boolean;
|
||||
reason_code: string;
|
||||
explanation?: string | null;
|
||||
visibility: "inherit" | "private" | "restricted" | "public";
|
||||
merge_record_id?: string | null;
|
||||
created_by_account_id?: string | null;
|
||||
metadata: Record<string, unknown>;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type Contact = {
|
||||
id: string;
|
||||
tenant_id?: string | null;
|
||||
@@ -79,11 +115,95 @@ export type Contact = {
|
||||
emails: ContactEmail[];
|
||||
phones: ContactPhone[];
|
||||
postal_addresses: ContactPostalAddress[];
|
||||
field_provenance?: ContactFieldProvenance[];
|
||||
deleted_at?: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type ContactPointQualityDecision = {
|
||||
id: string;
|
||||
tenant_id?: string | null;
|
||||
contact_id: string;
|
||||
channel: "email" | "phone" | "postal" | "internal_mail" | "portal";
|
||||
contact_point_id?: string | null;
|
||||
state: ContactPointQualityState;
|
||||
reason_code: string;
|
||||
reason?: string | null;
|
||||
evidence_ref?: string | null;
|
||||
effective_from: string;
|
||||
effective_until?: string | null;
|
||||
created_by_account_id?: string | null;
|
||||
metadata: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type ContactDuplicateFeature = {
|
||||
code: string;
|
||||
label: string;
|
||||
weight: number;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type ContactDuplicateSuggestion = {
|
||||
left: Contact;
|
||||
right: Contact;
|
||||
score: number;
|
||||
confidence: "possible" | "likely" | "strong";
|
||||
features: ContactDuplicateFeature[];
|
||||
};
|
||||
|
||||
export type ContactDuplicateSuggestionList = {
|
||||
suggestions: ContactDuplicateSuggestion[];
|
||||
scanned_contacts: number;
|
||||
candidate_pairs: number;
|
||||
truncated: boolean;
|
||||
};
|
||||
|
||||
export type ContactMergeRecord = {
|
||||
id: string;
|
||||
tenant_id?: string | null;
|
||||
address_book_id: string;
|
||||
winner_contact_id: string;
|
||||
loser_contact_ids: string[];
|
||||
status: string;
|
||||
reason: string;
|
||||
survivorship: Record<string, unknown>;
|
||||
decisions: Array<Record<string, unknown>>;
|
||||
before_hash: string;
|
||||
after_hash: string;
|
||||
created_by_account_id?: string | null;
|
||||
recovered_at?: string | null;
|
||||
recovered_by_account_id?: string | null;
|
||||
recovery_action?: string | null;
|
||||
recovery_reason?: string | null;
|
||||
provenance: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type AddressQualityCorrection = {
|
||||
contact_id: string;
|
||||
display_name: string;
|
||||
channel: "email" | "phone" | "postal" | "internal_mail" | "portal";
|
||||
contact_point_id?: string | null;
|
||||
state: ContactPointQualityState;
|
||||
reason_code: string;
|
||||
reason?: string | null;
|
||||
effective_from: string;
|
||||
};
|
||||
|
||||
export type AddressQualitySummary = {
|
||||
contact_count: number;
|
||||
contact_point_count: number;
|
||||
quality_counts: Record<string, number>;
|
||||
duplicate_suggestion_count: number;
|
||||
correction_count: number;
|
||||
corrections: AddressQualityCorrection[];
|
||||
truncated: boolean;
|
||||
};
|
||||
|
||||
export type AddressDistributionChannel = "email" | "postal" | "internal_mail" | "portal";
|
||||
export type AddressChannelDecision =
|
||||
| "allowed"
|
||||
@@ -392,6 +512,14 @@ type ContactChannelRuleListResponse = {
|
||||
rules: ContactChannelRule[];
|
||||
};
|
||||
|
||||
type ContactPointQualityDecisionListResponse = {
|
||||
decisions: ContactPointQualityDecision[];
|
||||
};
|
||||
|
||||
type ContactMergeRecordListResponse = {
|
||||
merges: ContactMergeRecord[];
|
||||
};
|
||||
|
||||
function queryString(params: Record<string, string | number | null | undefined>): string {
|
||||
const search = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
@@ -677,6 +805,110 @@ export function restoreContact(settings: ApiSettings, contactId: string): Promis
|
||||
return apiFetch<Contact>(settings, `/api/v1/addresses/contacts/${contactId}/restore`, { method: "POST" });
|
||||
}
|
||||
|
||||
export function getAddressQualitySummary(settings: ApiSettings, addressBookId: string): Promise<AddressQualitySummary> {
|
||||
return apiFetch<AddressQualitySummary>(settings, `/api/v1/addresses/address-books/${addressBookId}/quality-summary`);
|
||||
}
|
||||
|
||||
export function listContactDuplicateSuggestions(
|
||||
settings: ApiSettings,
|
||||
addressBookId: string,
|
||||
options: { contactId?: string | null; minimumScore?: number; limit?: number; scanLimit?: number } = {}
|
||||
): Promise<ContactDuplicateSuggestionList> {
|
||||
return apiFetch<ContactDuplicateSuggestionList>(
|
||||
settings,
|
||||
`/api/v1/addresses/address-books/${addressBookId}/duplicate-suggestions${queryString({
|
||||
contact_id: options.contactId,
|
||||
minimum_score: options.minimumScore,
|
||||
limit: options.limit,
|
||||
scan_limit: options.scanLimit
|
||||
})}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function listContactQualityDecisions(settings: ApiSettings, contactId: string): Promise<ContactPointQualityDecision[]> {
|
||||
const response = await apiFetch<ContactPointQualityDecisionListResponse>(
|
||||
settings,
|
||||
`/api/v1/addresses/contacts/${contactId}/quality-decisions`
|
||||
);
|
||||
return response.decisions;
|
||||
}
|
||||
|
||||
export function createContactQualityDecision(
|
||||
settings: ApiSettings,
|
||||
contactId: string,
|
||||
payload: {
|
||||
channel: ContactPointQualityDecision["channel"];
|
||||
contact_point_id?: string | null;
|
||||
state: ContactPointQualityState;
|
||||
reason_code?: string | null;
|
||||
reason?: string | null;
|
||||
evidence_ref?: string | null;
|
||||
}
|
||||
): Promise<ContactPointQualityDecision> {
|
||||
return apiFetch<ContactPointQualityDecision>(settings, `/api/v1/addresses/contacts/${contactId}/quality-decisions`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export function listContactProvenance(
|
||||
settings: ApiSettings,
|
||||
contactId: string,
|
||||
options: { currentOnly?: boolean; limit?: number } = {}
|
||||
): Promise<ContactFieldProvenance[]> {
|
||||
return apiFetch<ContactFieldProvenance[]>(
|
||||
settings,
|
||||
`/api/v1/addresses/contacts/${contactId}/provenance${queryString({
|
||||
current_only: options.currentOnly ? "true" : null,
|
||||
limit: options.limit
|
||||
})}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function listContactMerges(
|
||||
settings: ApiSettings,
|
||||
options: { addressBookId?: string | null; contactId?: string | null; limit?: number } = {}
|
||||
): Promise<ContactMergeRecord[]> {
|
||||
const response = await apiFetch<ContactMergeRecordListResponse>(
|
||||
settings,
|
||||
`/api/v1/addresses/contact-merges${queryString({
|
||||
address_book_id: options.addressBookId,
|
||||
contact_id: options.contactId,
|
||||
limit: options.limit
|
||||
})}`
|
||||
);
|
||||
return response.merges;
|
||||
}
|
||||
|
||||
export function mergeContacts(
|
||||
settings: ApiSettings,
|
||||
payload: {
|
||||
winner_contact_id: string;
|
||||
duplicate_contact_ids: string[];
|
||||
reason: string;
|
||||
field_sources?: Record<string, string>;
|
||||
contact_point_strategy?: "union" | "winner_only";
|
||||
source_precedence?: string[];
|
||||
}
|
||||
): Promise<ContactMergeRecord> {
|
||||
return apiFetch<ContactMergeRecord>(settings, "/api/v1/addresses/contact-merges", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export function recoverContactMerge(
|
||||
settings: ApiSettings,
|
||||
merge: ContactMergeRecord,
|
||||
action: "undo" | "split",
|
||||
reason: string
|
||||
): Promise<ContactMergeRecord> {
|
||||
return apiFetch<ContactMergeRecord>(settings, `/api/v1/addresses/contact-merges/${merge.id}/${action}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ reason, expected_after_hash: merge.after_hash })
|
||||
});
|
||||
}
|
||||
|
||||
export async function listContactChannelRules(settings: ApiSettings, contactId: string): Promise<ContactChannelRule[]> {
|
||||
const response = await apiFetch<ContactChannelRuleListResponse>(
|
||||
settings,
|
||||
|
||||
Reference in New Issue
Block a user