Source code for ab.api.models.contacts
"""Contact models for the ACPortal API."""
from __future__ import annotations
from typing import List, Optional
from pydantic import Field
from ab.api.models.base import RequestModel, ResponseModel
from ab.api.models.common import CompanyAddress
from ab.api.models.mixins import FullAuditModel, IdentifiedModel
[docs]
class ContactSimple(ResponseModel, IdentifiedModel):
"""Lightweight contact — GET /contacts/{id} and GET /contacts/user.
The ``/contacts/user`` endpoint returns ``fullName``, ``companyId``,
``companyName`` instead of ``firstName``/``lastName`` — all fields
are optional to accommodate both shapes.
"""
first_name: Optional[str] = Field(None, alias="firstName", description="First name")
last_name: Optional[str] = Field(None, alias="lastName", description="Last name")
full_name: Optional[str] = Field(None, alias="fullName", description="Full display name (from /contacts/user)")
email: Optional[str] = Field(None, description="Email address")
phone: Optional[str] = Field(None, description="Phone number")
company_id: Optional[str] = Field(None, alias="companyId", description="Associated company UUID")
company_name: Optional[str] = Field(None, alias="companyName", description="Associated company name")
# --- extended fields observed in live API responses ---
addresses_list: Optional[List[dict]] = Field(None, alias="addressesList", description="Contact addresses list")
assistant: Optional[str] = Field(None, description="Assistant name")
birth_date: Optional[str] = Field(None, alias="birthDate", description="Birth date")
bol_notes: Optional[str] = Field(None, alias="bolNotes", description="BOL notes")
care_of: Optional[str] = Field(None, alias="careOf", description="Care-of / attention line")
company: Optional[dict] = Field(None, description="Full company object")
contact_display_id: Optional[str] = Field(None, alias="contactDisplayId", description="Display ID")
contact_type_id: Optional[int] = Field(None, alias="contactTypeId", description="Contact type identifier")
department: Optional[str] = Field(None, description="Department name")
editable: Optional[bool] = Field(None, description="Whether the contact is editable")
emails_list: Optional[List[dict]] = Field(None, alias="emailsList", description="Email addresses list")
fax: Optional[str] = Field(None, description="Fax number")
full_name_update_required: Optional[bool] = Field(
None, alias="fullNameUpdateRequired", description="Whether full name needs update",
)
is_active: Optional[bool] = Field(None, alias="isActive", description="Whether the contact is active")
is_business: Optional[bool] = Field(None, alias="isBusiness", description="Whether this is a business contact")
is_empty: Optional[bool] = Field(None, alias="isEmpty", description="Whether the contact record is empty")
is_payer: Optional[bool] = Field(None, alias="isPayer", description="Whether this contact is a payer")
is_prefered: Optional[bool] = Field(None, alias="isPrefered", description="Whether this contact is preferred")
is_primary: Optional[bool] = Field(None, alias="isPrimary", description="Whether this is the primary contact")
is_private: Optional[bool] = Field(None, alias="isPrivate", description="Whether this contact is private")
job_title: Optional[str] = Field(None, alias="jobTitle", description="Job title")
job_title_id: Optional[int] = Field(None, alias="jobTitleId", description="Job title identifier")
legacy_guid: Optional[str] = Field(None, alias="legacyGuid", description="Legacy system GUID")
owner_franchisee_id: Optional[str] = Field(None, alias="ownerFranchiseeId", description="Owner franchisee UUID")
phones_list: Optional[List[dict]] = Field(None, alias="phonesList", description="Phone numbers list")
primary_email: Optional[str] = Field(None, alias="primaryEmail", description="Primary email address")
primary_phone: Optional[str] = Field(None, alias="primaryPhone", description="Primary phone number")
root_contact_id: Optional[int] = Field(None, alias="rootContactId", description="Root contact identifier")
tax_id: Optional[str] = Field(None, alias="taxId", description="Tax ID")
web_site: Optional[str] = Field(None, alias="webSite", description="Website URL")
# ---- Typed sub-models for ContactDetailedInfo nested lists (021) -----------
[docs]
class EmailDetails(ResponseModel):
"""Inner email object within ContactEmailEntry — maps to C# EmailDetails."""
id: Optional[int] = Field(None, description="Email row ID")
email: Optional[str] = Field(None, description="Email address")
invalid: Optional[bool] = Field(None, description="Whether email is invalid")
dont_spam: Optional[bool] = Field(None, alias="dontSpam", description="Do-not-spam flag")
[docs]
class PhoneDetails(ResponseModel):
"""Inner phone object within ContactPhoneEntry — maps to C# PhoneDetails."""
id: Optional[int] = Field(None, description="Phone row ID")
phone: Optional[str] = Field(None, description="Phone number")
[docs]
class ContactEmailEntry(ResponseModel):
"""Email list entry — maps to C# ContactEmailEditDetails (DetailBindingBase)."""
id: Optional[int] = Field(None, description="Mapping row ID")
is_active: Optional[bool] = Field(None, alias="isActive", description="Active flag")
deactivated_reason: Optional[str] = Field(None, alias="deactivatedReason", description="Deactivation reason")
meta_data: Optional[str] = Field(None, alias="metaData", description="Type label (e.g. 'Primary', 'Fax')")
editable: Optional[bool] = Field(None, description="Edit permission")
email: Optional[EmailDetails] = Field(None, description="Nested email details")
[docs]
class ContactPhoneEntry(ResponseModel):
"""Phone list entry — maps to C# ContactPhoneEditDetails (DetailBindingBase)."""
id: Optional[int] = Field(None, description="Mapping row ID")
is_active: Optional[bool] = Field(None, alias="isActive", description="Active flag")
deactivated_reason: Optional[str] = Field(None, alias="deactivatedReason", description="Deactivation reason")
meta_data: Optional[str] = Field(None, alias="metaData", description="Type label")
editable: Optional[bool] = Field(None, description="Edit permission")
phone: Optional[PhoneDetails] = Field(None, description="Nested phone details")
[docs]
class ContactAddressEntry(ResponseModel):
"""Address list entry — maps to C# ContactAddressEditDetails (DetailBindingBase)."""
id: Optional[int] = Field(None, description="Mapping row ID")
is_active: Optional[bool] = Field(None, alias="isActive", description="Active flag")
deactivated_reason: Optional[str] = Field(None, alias="deactivatedReason", description="Deactivation reason")
meta_data: Optional[str] = Field(None, alias="metaData", description="Type label")
editable: Optional[bool] = Field(None, description="Edit permission")
address: Optional[CompanyAddress] = Field(None, description="Nested address (reuses CompanyAddress)")
[docs]
class ContactDetailedInfo(ResponseModel, FullAuditModel):
"""Full editable contact details — GET /contacts/{id}/editdetails.
Maps to C# ContactEditDetails → ContactExtendedDetails<T> → ContactBaseDetails.
"""
# --- ContactBaseDetails fields ---
first_name: Optional[str] = Field(None, alias="firstName", description="First name")
last_name: Optional[str] = Field(None, alias="lastName", description="Last name")
email: Optional[str] = Field(None, description="Email address")
phone: Optional[str] = Field(None, description="Phone number")
contact_display_id: Optional[str] = Field(None, alias="contactDisplayId", description="Display ID")
full_name: Optional[str] = Field(None, alias="fullName", description="Full display name")
contact_type_id: Optional[int] = Field(None, alias="contactTypeId", description="Contact type identifier")
care_of: Optional[str] = Field(None, alias="careOf", description="Care-of / attention line")
bol_notes: Optional[str] = Field(None, alias="bolNotes", description="BOL notes")
tax_id: Optional[str] = Field(None, alias="taxId", description="Tax ID")
is_business: Optional[bool] = Field(None, alias="isBusiness", description="Whether this is a business contact")
is_payer: Optional[bool] = Field(None, alias="isPayer", description="Whether this contact is a payer")
is_prefered: Optional[bool] = Field(None, alias="isPrefered", description="Whether this contact is preferred")
is_private: Optional[bool] = Field(None, alias="isPrivate", description="Whether this contact is private")
is_primary: Optional[bool] = Field(None, alias="isPrimary", description="Whether this is the primary contact")
company_id: Optional[str] = Field(None, alias="companyId", description="Associated company UUID")
root_contact_id: Optional[int] = Field(None, alias="rootContactId", description="Root contact identifier")
owner_franchisee_id: Optional[str] = Field(None, alias="ownerFranchiseeId", description="Owner franchisee UUID")
company: Optional[dict] = Field(None, description="Full company object")
legacy_guid: Optional[str] = Field(None, alias="legacyGuid", description="Legacy system GUID")
assistant: Optional[str] = Field(None, description="Assistant name")
department: Optional[str] = Field(None, description="Department name")
web_site: Optional[str] = Field(None, alias="webSite", description="Website URL")
birth_date: Optional[str] = Field(None, alias="birthDate", description="Birth date")
job_title_id: Optional[int] = Field(None, alias="jobTitleId", description="Job title identifier")
job_title: Optional[str] = Field(None, alias="jobTitle", description="Job title")
# --- ContactExtendedDetails fields ---
emails_list: Optional[List[ContactEmailEntry]] = Field(None, alias="emailsList", description="Typed email entries")
phones_list: Optional[List[ContactPhoneEntry]] = Field(None, alias="phonesList", description="Typed phone entries")
addresses_list: Optional[List[ContactAddressEntry]] = Field(
None, alias="addressesList", description="Typed address entries",
)
fax: Optional[str] = Field(None, description="Fax number")
primary_phone: Optional[str] = Field(None, alias="primaryPhone", description="Primary phone number")
primary_email: Optional[str] = Field(None, alias="primaryEmail", description="Primary email address")
# --- ContactEditDetails fields ---
editable: Optional[bool] = Field(None, description="Whether the contact is editable")
# --- Additional fields from fixture / service layer ---
addresses: Optional[List[dict]] = Field(None, description="Contact addresses (legacy, untyped)")
phones: Optional[List[dict]] = Field(None, description="Phone numbers (legacy, untyped)")
emails: Optional[List[dict]] = Field(None, description="Email addresses (legacy, untyped)")
company_info: Optional[dict] = Field(None, alias="companyInfo", description="Associated company info")
contact_details_company_info: Optional[dict] = Field(
None, alias="contactDetailsCompanyInfo", description="Rich company details with address and branding",
)
full_name_update_required: Optional[bool] = Field(
None, alias="fullNameUpdateRequired", description="Whether full name needs update",
)
is_empty: Optional[bool] = Field(None, alias="isEmpty", description="Whether the contact record is empty")
[docs]
class ContactPrimaryDetails(ResponseModel):
"""Primary contact info — GET /contacts/{id}/primarydetails.
The live API returns ``company`` as a nested dict (full company object)
rather than a plain string as swagger implies.
"""
id: Optional[int] = Field(None, description="Contact integer ID")
full_name: Optional[str] = Field(None, alias="fullName", description="Full display name")
email: Optional[str] = Field(None, description="Primary email")
phone: Optional[str] = Field(None, description="Primary phone")
company: Optional[dict] = Field(None, description="Associated company (full object)")
company_id: Optional[str] = Field(None, alias="companyId", description="Company UUID")
company_name: Optional[str] = Field(None, alias="companyName", description="Company name")
cell_phone: Optional[str] = Field(None, alias="cellPhone", description="Cell phone number")
fax: Optional[str] = Field(None, description="Fax number")
address: Optional[CompanyAddress] = Field(None, description="Contact address")
[docs]
class SearchContactEntityResult(ResponseModel):
"""Single result row from POST /contacts/v2/search.
Maps to C# ``SearchContactEntityResult`` entity (22 properties).
Each row contains contact details, address fields, company info,
and a denormalized ``totalRecords`` count.
"""
contact_id: Optional[int] = Field(None, alias="contactID", description="Contact integer ID")
customer_cell: Optional[str] = Field(None, alias="customerCell", description="Customer cell phone number")
contact_display_id: Optional[str] = Field(None, alias="contactDisplayId", description="Contact display ID")
contact_full_name: Optional[str] = Field(None, alias="contactFullName", description="Contact full display name")
contact_phone: Optional[str] = Field(None, alias="contactPhone", description="Contact phone number")
contact_home_phone: Optional[str] = Field(None, alias="contactHomePhone", description="Contact home phone number")
contact_email: Optional[str] = Field(None, alias="contactEmail", description="Contact email address")
master_constant_value: Optional[str] = Field(None, alias="masterConstantValue", description="Master constant value")
contact_dept: Optional[str] = Field(None, alias="contactDept", description="Contact department")
address1: Optional[str] = Field(None, description="Street address line 1")
address2: Optional[str] = Field(None, description="Street address line 2")
city: Optional[str] = Field(None, description="City")
state: Optional[str] = Field(None, description="State/province")
zip_code: Optional[str] = Field(None, alias="zipCode", description="ZIP/postal code")
country_name: Optional[str] = Field(None, alias="countryName", description="Country name")
company_code: Optional[str] = Field(None, alias="companyCode", description="Company code")
company_id: Optional[str] = Field(None, alias="companyID", description="Company UUID")
company_name: Optional[str] = Field(None, alias="companyName", description="Company name")
company_display_id: Optional[str] = Field(None, alias="companyDisplayId", description="Company display ID")
is_prefered: Optional[bool] = Field(None, alias="isPrefered", description="Whether this contact is preferred")
industry_type: Optional[str] = Field(None, alias="industryType", description="Industry type classification")
total_records: Optional[int] = Field(
None, alias="totalRecords", description="Total matching records (denormalized)"
)
[docs]
class ContactSearchParams(RequestModel):
"""Search filter parameters — the ``mainSearchRequest`` sub-object of POST /contacts/v2/search.
Maps to swagger ``MergeContactsSearchRequestParameters``. All fields are
optional; omit entirely for unfiltered results.
"""
contact_display_id: Optional[int] = Field(
None, alias="contactDisplayId", description="Filter by contact display ID"
)
full_name: Optional[str] = Field(None, alias="fullName", description="Filter by contact name")
company_name: Optional[str] = Field(None, alias="companyName", description="Filter by company name")
company_code: Optional[str] = Field(None, alias="companyCode", description="Filter by company code")
email: Optional[str] = Field(None, description="Filter by email address")
phone: Optional[str] = Field(None, description="Filter by phone number")
company_display_id: Optional[int] = Field(
None, alias="companyDisplayId", description="Filter by company display ID"
)
[docs]
class PageOrderedRequest(RequestModel):
"""Pagination and sorting options — the ``loadOptions`` sub-object of POST /contacts/v2/search.
Maps to swagger ``PageOrderedRequestModel`` / C# ``PagedOrderedRequest``.
"""
page_number: int = Field(..., alias="pageNumber", description="Page number (1-based, required)")
page_size: int = Field(..., alias="pageSize", description="Items per page (required, 1-32767)")
sorting_by: Optional[str] = Field(None, alias="sortingBy", description="Sort field name")
sorting_direction: Optional[int] = Field(
None, alias="sortingDirection", description="Sort direction (0=ascending, 1=descending)"
)
[docs]
class ContactEditParams(RequestModel):
"""Query parameters for contact edit operations."""
franchisee_id: Optional[str] = Field(None, alias="franchiseeId", description="Franchisee UUID filter")
[docs]
class ContactHistoryParams(RequestModel):
"""Query parameters for contact history operations."""
statuses: Optional[str] = Field(None, alias="statuses", description="Comma-separated status filters")
[docs]
class ContactEditRequest(RequestModel):
"""Body for PUT /contacts/{id}/editdetails and POST /contacts/editdetails."""
first_name: Optional[str] = Field(None, alias="firstName", description="First name")
last_name: Optional[str] = Field(None, alias="lastName", description="Last name")
email: Optional[str] = Field(None, description="Email address")
phone: Optional[str] = Field(None, description="Phone number")
addresses: Optional[List[dict]] = Field(None, description="Contact addresses")
[docs]
class ContactSearchRequest(RequestModel):
"""Body for POST /contacts/v2/search.
Uses a nested structure with ``mainSearchRequest`` (search filter
parameters) and ``loadOptions`` (pagination/sort). The flat mixin
fields (page, pageSize, searchText) are replaced by typed sub-models
that match the API's expected shape.
"""
main_search_request: Optional[ContactSearchParams] = Field(
None, alias="mainSearchRequest", description="Search filter parameters (omit for unfiltered results)",
)
load_options: PageOrderedRequest = Field(
..., alias="loadOptions", description="Pagination and sorting options (required)",
)
# ---- Extended contact models (008) ----------------------------------------
[docs]
class ContactHistory(ResponseModel):
"""Contact interaction history — POST /contacts/{contactId}/history."""
events: Optional[List[dict]] = Field(None, description="History events")
total_count: Optional[int] = Field(None, alias="totalCount", description="Total event count")
[docs]
class ContactHistoryAggregated(ResponseModel):
"""Aggregated history — GET /contacts/{contactId}/history/aggregated."""
summary: Optional[dict] = Field(None, description="Aggregated summary")
by_type: Optional[List[dict]] = Field(None, alias="byType", description="Breakdown by type")
[docs]
class ContactGraphData(ResponseModel):
"""Contact graph data — GET /contacts/{contactId}/history/graphdata."""
data_points: Optional[List[dict]] = Field(None, alias="dataPoints", description="Graph data points")
labels: Optional[List[str]] = Field(None, description="Graph labels")
[docs]
class ContactMergePreview(ResponseModel):
"""Merge preview result — POST /contacts/{mergeToId}/merge/preview."""
merge_to: Optional[dict] = Field(None, alias="mergeTo", description="Target contact")
merge_from: Optional[dict] = Field(None, alias="mergeFrom", description="Source contact")
conflicts: Optional[List[dict]] = Field(None, description="Merge conflicts")
# ---- Pattern C → B placeholder models (020) ---------------------------------
[docs]
class ContactHistoryCreateRequest(RequestModel):
"""Body for POST /contacts/{contactId}/history."""
statuses: Optional[str] = Field(None, description="Comma-separated status filters")
[docs]
class ContactMergeRequest(RequestModel):
"""Body for POST /contacts/{mergeToId}/merge/preview and PUT /contacts/{mergeToId}/merge."""
merge_from_id: Optional[str] = Field(None, alias="mergeFromId", description="Source contact ID to merge from")