Add the Sales CRM service through collaboration, sync architecture/registry docs, expose crm.torbatyar.ir, and include a production deploy script. Co-authored-by: Cursor <cursoragent@cursor.com>
582 lines
18 KiB
Python
582 lines
18 KiB
Python
"""CRM foundation DTOs — Phase 6.0 pipelines + Phase 6.1 core entities."""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
from uuid import UUID
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app.models.types import (
|
|
AddressType,
|
|
AttachmentKind,
|
|
AuditAction,
|
|
ContactKind,
|
|
ContactMethod,
|
|
ContactStatus,
|
|
CrmEntityType,
|
|
CustomFieldType,
|
|
LeadPriority,
|
|
LeadStatus,
|
|
OpportunityStatus,
|
|
OrganizationKind,
|
|
OrganizationStatus,
|
|
QuoteStatus,
|
|
TagScope,
|
|
)
|
|
from app.schemas.common import ORMBase
|
|
|
|
|
|
# ── Lead ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class LeadCreate(BaseModel):
|
|
first_name: str = Field(default="", max_length=100)
|
|
last_name: str = Field(default="", max_length=100)
|
|
full_name: str | None = Field(default=None, max_length=255)
|
|
title: str | None = Field(default=None, max_length=255)
|
|
company: str | None = Field(default=None, max_length=255)
|
|
company_name: str | None = Field(default=None, max_length=255)
|
|
job_title: str | None = Field(default=None, max_length=150)
|
|
email: str | None = Field(default=None, max_length=255)
|
|
mobile: str | None = Field(default=None, max_length=50)
|
|
phone: str | None = Field(default=None, max_length=50)
|
|
website: str | None = Field(default=None, max_length=255)
|
|
lead_source_id: UUID | None = None
|
|
lead_status_id: UUID | None = None
|
|
status: LeadStatus = LeadStatus.NEW
|
|
source: str | None = Field(default=None, max_length=100)
|
|
assigned_user_id: str | None = Field(default=None, max_length=100)
|
|
priority: LeadPriority = LeadPriority.MEDIUM
|
|
estimated_value: Decimal = Decimal("0")
|
|
expected_close_date: date | None = None
|
|
description: str | None = None
|
|
owner_user_id: str | None = Field(default=None, max_length=100)
|
|
lead_number: str | None = Field(default=None, max_length=50)
|
|
contact_id: UUID | None = None
|
|
organization_id: UUID | None = None
|
|
|
|
|
|
class LeadUpdate(BaseModel):
|
|
first_name: str | None = Field(default=None, max_length=100)
|
|
last_name: str | None = Field(default=None, max_length=100)
|
|
full_name: str | None = Field(default=None, max_length=255)
|
|
title: str | None = Field(default=None, max_length=255)
|
|
company: str | None = Field(default=None, max_length=255)
|
|
job_title: str | None = Field(default=None, max_length=150)
|
|
email: str | None = Field(default=None, max_length=255)
|
|
mobile: str | None = Field(default=None, max_length=50)
|
|
phone: str | None = Field(default=None, max_length=50)
|
|
website: str | None = Field(default=None, max_length=255)
|
|
lead_source_id: UUID | None = None
|
|
lead_status_id: UUID | None = None
|
|
status: LeadStatus | None = None
|
|
source: str | None = Field(default=None, max_length=100)
|
|
assigned_user_id: str | None = Field(default=None, max_length=100)
|
|
priority: LeadPriority | None = None
|
|
estimated_value: Decimal | None = None
|
|
expected_close_date: date | None = None
|
|
description: str | None = None
|
|
owner_user_id: str | None = Field(default=None, max_length=100)
|
|
contact_id: UUID | None = None
|
|
organization_id: UUID | None = None
|
|
|
|
|
|
class LeadRead(ORMBase):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
lead_number: str
|
|
first_name: str
|
|
last_name: str
|
|
full_name: str
|
|
title: str | None
|
|
company: str | None
|
|
job_title: str | None
|
|
email: str | None
|
|
mobile: str | None
|
|
phone: str | None
|
|
website: str | None
|
|
lead_source_id: UUID | None
|
|
lead_status_id: UUID | None
|
|
status: LeadStatus
|
|
source: str | None
|
|
assigned_user_id: str | None
|
|
priority: LeadPriority
|
|
estimated_value: Decimal
|
|
expected_close_date: date | None
|
|
description: str | None
|
|
owner_user_id: str
|
|
contact_id: UUID | None
|
|
organization_id: UUID | None
|
|
is_deleted: bool
|
|
created_by: str | None
|
|
updated_by: str | None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class LeadAssignRequest(BaseModel):
|
|
assigned_user_id: str = Field(max_length=100)
|
|
note: str | None = None
|
|
|
|
|
|
# ── Contact ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class ContactCreate(BaseModel):
|
|
first_name: str = Field(max_length=100)
|
|
last_name: str = Field(max_length=100)
|
|
kind: ContactKind = ContactKind.BUSINESS
|
|
contact_type_id: UUID | None = None
|
|
email: str | None = Field(default=None, max_length=255)
|
|
phone: str | None = Field(default=None, max_length=50)
|
|
emails: list[str] | None = None
|
|
phones: list[str] | None = None
|
|
default_contact_method: ContactMethod | None = None
|
|
social_links: dict[str, str] | None = None
|
|
birthday: date | None = None
|
|
job_title: str | None = Field(default=None, max_length=150)
|
|
job_position: str | None = Field(default=None, max_length=150)
|
|
organization_id: UUID | None = None
|
|
owner_user_id: str | None = Field(default=None, max_length=100)
|
|
notes_summary: str | None = None
|
|
status: ContactStatus = ContactStatus.ACTIVE
|
|
|
|
|
|
class ContactUpdate(BaseModel):
|
|
first_name: str | None = Field(default=None, max_length=100)
|
|
last_name: str | None = Field(default=None, max_length=100)
|
|
kind: ContactKind | None = None
|
|
contact_type_id: UUID | None = None
|
|
email: str | None = Field(default=None, max_length=255)
|
|
phone: str | None = Field(default=None, max_length=50)
|
|
emails: list[str] | None = None
|
|
phones: list[str] | None = None
|
|
default_contact_method: ContactMethod | None = None
|
|
social_links: dict[str, str] | None = None
|
|
birthday: date | None = None
|
|
job_title: str | None = Field(default=None, max_length=150)
|
|
job_position: str | None = Field(default=None, max_length=150)
|
|
organization_id: UUID | None = None
|
|
owner_user_id: str | None = Field(default=None, max_length=100)
|
|
notes_summary: str | None = None
|
|
status: ContactStatus | None = None
|
|
|
|
|
|
class ContactRead(ORMBase):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
first_name: str
|
|
last_name: str
|
|
full_name: str
|
|
kind: ContactKind
|
|
contact_type_id: UUID | None
|
|
email: str | None
|
|
phone: str | None
|
|
emails: list | None
|
|
phones: list | None
|
|
default_contact_method: ContactMethod | None
|
|
social_links: dict | None
|
|
birthday: date | None
|
|
job_title: str | None
|
|
job_position: str | None
|
|
status: ContactStatus
|
|
organization_id: UUID | None
|
|
owner_user_id: str | None
|
|
notes_summary: str | None
|
|
is_deleted: bool
|
|
created_by: str | None
|
|
updated_by: str | None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
# ── Organization ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
class OrganizationCreate(BaseModel):
|
|
name: str = Field(max_length=255)
|
|
kind: OrganizationKind = OrganizationKind.COMPANY
|
|
legal_name: str | None = Field(default=None, max_length=255)
|
|
trade_name: str | None = Field(default=None, max_length=255)
|
|
national_identifier: str | None = Field(default=None, max_length=50)
|
|
tax_number: str | None = Field(default=None, max_length=50)
|
|
organization_type_id: UUID | None = None
|
|
organization_industry_id: UUID | None = None
|
|
industry: str | None = Field(default=None, max_length=150)
|
|
website: str | None = Field(default=None, max_length=255)
|
|
email: str | None = Field(default=None, max_length=255)
|
|
phone: str | None = Field(default=None, max_length=50)
|
|
parent_organization_id: UUID | None = None
|
|
owner_user_id: str | None = Field(default=None, max_length=100)
|
|
description: str | None = None
|
|
status: OrganizationStatus = OrganizationStatus.ACTIVE
|
|
|
|
|
|
class OrganizationUpdate(BaseModel):
|
|
name: str | None = Field(default=None, max_length=255)
|
|
kind: OrganizationKind | None = None
|
|
legal_name: str | None = Field(default=None, max_length=255)
|
|
trade_name: str | None = Field(default=None, max_length=255)
|
|
national_identifier: str | None = Field(default=None, max_length=50)
|
|
tax_number: str | None = Field(default=None, max_length=50)
|
|
organization_type_id: UUID | None = None
|
|
organization_industry_id: UUID | None = None
|
|
industry: str | None = Field(default=None, max_length=150)
|
|
website: str | None = Field(default=None, max_length=255)
|
|
email: str | None = Field(default=None, max_length=255)
|
|
phone: str | None = Field(default=None, max_length=50)
|
|
parent_organization_id: UUID | None = None
|
|
owner_user_id: str | None = Field(default=None, max_length=100)
|
|
description: str | None = None
|
|
status: OrganizationStatus | None = None
|
|
|
|
|
|
class OrganizationRead(ORMBase):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
name: str
|
|
kind: OrganizationKind
|
|
legal_name: str | None
|
|
trade_name: str | None
|
|
national_identifier: str | None
|
|
tax_number: str | None
|
|
organization_type_id: UUID | None
|
|
organization_industry_id: UUID | None
|
|
industry: str | None
|
|
website: str | None
|
|
email: str | None
|
|
phone: str | None
|
|
parent_organization_id: UUID | None
|
|
status: OrganizationStatus
|
|
owner_user_id: str | None
|
|
description: str | None
|
|
is_deleted: bool
|
|
created_by: str | None
|
|
updated_by: str | None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
# ── Lookups ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class LookupCreate(BaseModel):
|
|
code: str = Field(max_length=50)
|
|
name: str = Field(max_length=150)
|
|
description: str | None = None
|
|
is_active: bool = True
|
|
|
|
|
|
class LeadStatusCreate(BaseModel):
|
|
code: str = Field(max_length=50)
|
|
name: str = Field(max_length=150)
|
|
position: int = 0
|
|
is_terminal: bool = False
|
|
is_converted: bool = False
|
|
is_active: bool = True
|
|
color: str | None = Field(default=None, max_length=20)
|
|
|
|
|
|
class LookupRead(ORMBase):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
code: str
|
|
name: str
|
|
is_active: bool
|
|
created_at: datetime
|
|
|
|
|
|
class LeadSourceRead(LookupRead):
|
|
description: str | None
|
|
|
|
|
|
class LeadStatusRead(ORMBase):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
code: str
|
|
name: str
|
|
position: int
|
|
is_terminal: bool
|
|
is_converted: bool
|
|
is_active: bool
|
|
color: str | None
|
|
created_at: datetime
|
|
|
|
|
|
# ── Tags / Addresses / Notes / Attachments / Custom Fields / Audit ───────────
|
|
|
|
|
|
class TagCreate(BaseModel):
|
|
name: str = Field(max_length=100)
|
|
color: str | None = Field(default=None, max_length=20)
|
|
group_name: str | None = Field(default=None, max_length=100)
|
|
scope: TagScope = TagScope.ANY
|
|
is_active: bool = True
|
|
|
|
|
|
class TagRead(ORMBase):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
name: str
|
|
color: str | None
|
|
group_name: str | None
|
|
scope: TagScope
|
|
is_active: bool
|
|
created_at: datetime
|
|
|
|
|
|
class TagAssignRequest(BaseModel):
|
|
tag_id: UUID
|
|
|
|
|
|
class AddressCreate(BaseModel):
|
|
entity_type: CrmEntityType
|
|
entity_id: UUID
|
|
address_type: AddressType
|
|
label: str | None = Field(default=None, max_length=100)
|
|
line1: str = Field(max_length=255)
|
|
line2: str | None = Field(default=None, max_length=255)
|
|
city: str | None = Field(default=None, max_length=100)
|
|
state: str | None = Field(default=None, max_length=100)
|
|
postal_code: str | None = Field(default=None, max_length=30)
|
|
country: str | None = Field(default=None, max_length=100)
|
|
latitude: Decimal | None = None
|
|
longitude: Decimal | None = None
|
|
is_default: bool = False
|
|
|
|
|
|
class AddressUpdate(BaseModel):
|
|
address_type: AddressType | None = None
|
|
label: str | None = Field(default=None, max_length=100)
|
|
line1: str | None = Field(default=None, max_length=255)
|
|
line2: str | None = Field(default=None, max_length=255)
|
|
city: str | None = Field(default=None, max_length=100)
|
|
state: str | None = Field(default=None, max_length=100)
|
|
postal_code: str | None = Field(default=None, max_length=30)
|
|
country: str | None = Field(default=None, max_length=100)
|
|
latitude: Decimal | None = None
|
|
longitude: Decimal | None = None
|
|
is_default: bool | None = None
|
|
|
|
|
|
class AddressRead(ORMBase):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
entity_type: CrmEntityType
|
|
entity_id: UUID
|
|
address_type: AddressType
|
|
label: str | None
|
|
line1: str
|
|
line2: str | None
|
|
city: str | None
|
|
state: str | None
|
|
postal_code: str | None
|
|
country: str | None
|
|
latitude: Decimal | None
|
|
longitude: Decimal | None
|
|
is_default: bool
|
|
is_deleted: bool
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class NoteCreate(BaseModel):
|
|
entity_type: CrmEntityType
|
|
entity_id: UUID
|
|
body: str
|
|
body_format: str = "richtext"
|
|
is_pinned: bool = False
|
|
|
|
|
|
class NoteUpdate(BaseModel):
|
|
body: str | None = None
|
|
is_pinned: bool | None = None
|
|
|
|
|
|
class NoteRead(ORMBase):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
entity_type: CrmEntityType
|
|
entity_id: UUID
|
|
body: str
|
|
body_format: str
|
|
version: int
|
|
is_pinned: bool
|
|
is_deleted: bool
|
|
created_by: str | None
|
|
updated_by: str | None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class AttachmentCreate(BaseModel):
|
|
entity_type: CrmEntityType
|
|
entity_id: UUID
|
|
file_storage_id: str = Field(max_length=100)
|
|
file_name: str = Field(max_length=255)
|
|
content_type: str | None = Field(default=None, max_length=150)
|
|
size_bytes: int | None = None
|
|
kind: AttachmentKind = AttachmentKind.DOCUMENT
|
|
description: str | None = None
|
|
|
|
|
|
class AttachmentRead(ORMBase):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
entity_type: CrmEntityType
|
|
entity_id: UUID
|
|
file_storage_id: str
|
|
file_name: str
|
|
content_type: str | None
|
|
size_bytes: int | None
|
|
kind: AttachmentKind
|
|
description: str | None
|
|
is_deleted: bool
|
|
created_by: str | None
|
|
created_at: datetime
|
|
|
|
|
|
class CustomFieldCreate(BaseModel):
|
|
entity_type: CrmEntityType
|
|
field_key: str = Field(max_length=100)
|
|
label: str = Field(max_length=150)
|
|
field_type: CustomFieldType
|
|
is_required: bool = False
|
|
options: list[str] | None = None
|
|
validation_rules: dict | None = None
|
|
position: int = 0
|
|
is_active: bool = True
|
|
|
|
|
|
class CustomFieldRead(ORMBase):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
entity_type: CrmEntityType
|
|
field_key: str
|
|
label: str
|
|
field_type: CustomFieldType
|
|
is_required: bool
|
|
options: list | None
|
|
validation_rules: dict | None
|
|
position: int
|
|
is_active: bool
|
|
created_at: datetime
|
|
|
|
|
|
class CustomFieldValueUpsert(BaseModel):
|
|
custom_field_id: UUID
|
|
entity_type: CrmEntityType
|
|
entity_id: UUID
|
|
value_text: str | None = None
|
|
value_number: Decimal | None = None
|
|
value_boolean: bool | None = None
|
|
value_date: date | None = None
|
|
value_json: list | dict | None = None
|
|
|
|
|
|
class CustomFieldValueRead(ORMBase):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
custom_field_id: UUID
|
|
entity_type: CrmEntityType
|
|
entity_id: UUID
|
|
value_text: str | None
|
|
value_number: Decimal | None
|
|
value_boolean: bool | None
|
|
value_date: date | None
|
|
value_json: list | dict | None
|
|
created_at: datetime
|
|
|
|
|
|
class AuditRead(ORMBase):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
entity_type: str
|
|
entity_id: UUID
|
|
action: AuditAction
|
|
actor_user_id: str | None
|
|
changes: dict | None
|
|
message: str | None
|
|
created_at: datetime
|
|
|
|
|
|
class LeadAssignmentRead(ORMBase):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
lead_id: UUID
|
|
assigned_user_id: str
|
|
assigned_by: str | None
|
|
assigned_at: datetime
|
|
note: str | None
|
|
is_current: bool
|
|
created_at: datetime
|
|
|
|
|
|
# ── Pipeline / Opportunity (Phase 6.2) / Activity (Phase 6.3) / Quote ────────
|
|
|
|
from app.schemas.collaboration import ( # noqa: E402
|
|
SalesActivityCreate,
|
|
SalesActivityRead,
|
|
SalesActivityUpdate,
|
|
)
|
|
from app.schemas.sales_engine import ( # noqa: E402
|
|
OpportunityCreate,
|
|
OpportunityLoseRequest,
|
|
OpportunityRead,
|
|
OpportunityStageChangeRequest,
|
|
OpportunityUpdate,
|
|
OpportunityWinRequest,
|
|
PipelineCreate,
|
|
PipelineRead,
|
|
PipelineStageCreate,
|
|
PipelineStageRead,
|
|
PipelineStageUpdate,
|
|
PipelineUpdate,
|
|
StageHistoryRead,
|
|
)
|
|
|
|
|
|
class QuoteCreate(BaseModel):
|
|
title: str = Field(max_length=255)
|
|
quote_number: str = Field(max_length=50)
|
|
amount: Decimal = Decimal("0")
|
|
currency_code: str = Field(default="IRR", max_length=3)
|
|
valid_until: date | None = None
|
|
opportunity_id: UUID | None = None
|
|
organization_id: UUID | None = None
|
|
contact_id: UUID | None = None
|
|
owner_user_id: str | None = Field(default=None, max_length=100)
|
|
notes: str | None = None
|
|
|
|
|
|
class QuoteUpdate(BaseModel):
|
|
title: str | None = Field(default=None, max_length=255)
|
|
amount: Decimal | None = None
|
|
currency_code: str | None = Field(default=None, max_length=3)
|
|
valid_until: date | None = None
|
|
opportunity_id: UUID | None = None
|
|
organization_id: UUID | None = None
|
|
contact_id: UUID | None = None
|
|
owner_user_id: str | None = Field(default=None, max_length=100)
|
|
notes: str | None = None
|
|
status: QuoteStatus | None = None
|
|
|
|
|
|
class QuoteRead(ORMBase):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
title: str
|
|
quote_number: str
|
|
status: QuoteStatus
|
|
amount: Decimal
|
|
currency_code: str
|
|
valid_until: date | None
|
|
opportunity_id: UUID | None
|
|
organization_id: UUID | None
|
|
contact_id: UUID | None
|
|
owner_user_id: str | None
|
|
notes: str | None
|
|
created_at: datetime
|
|
updated_at: datetime
|