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")