TorbatYar/backend/services/crm/app/schemas/foundation.py
Mortezakoohjani 064d67f099 Ship CRM Core Platform (phases 6.0–6.3) with docs and prod deploy.
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>
2026-07-24 20:32:00 +03:30

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