Files
maskanx_crm_frontend/app/schemas.py
T
AFFAANhandClaude Opus 5 f6a54d6f93 Initial commit: Maskan CRM backend
Independent FastAPI backend for Maskan CRM.

Owns contacts, organizations, leads, pipelines, activities, products,
quotes, users, permissions, audit records and first-party integration
credentials, with Alembic migrations against PostgreSQL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:28:41 +05:30

412 lines
11 KiB
Python

from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from typing import Any, Generic, TypeVar
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
T = TypeVar("T")
class ApiModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
class Page(ApiModel, Generic[T]):
items: list[T]
total: int
page: int
page_size: int
class LoginRequest(ApiModel):
workspace: str = Field(min_length=2, max_length=80)
email: EmailStr
password: str = Field(min_length=8, max_length=200)
class UserOut(ApiModel):
id: str
tenant_id: str
email: EmailStr
full_name: str
role: str
class TenantOut(ApiModel):
id: str
slug: str
name: str
mode: str
class TokenResponse(ApiModel):
access_token: str
token_type: str = "bearer"
expires_in: int
user: UserOut
tenant: TenantOut
class SessionResponse(ApiModel):
user: UserOut
tenant: TenantOut
class OrganizationBase(ApiModel):
name: str = Field(min_length=1, max_length=200)
legal_name: str | None = Field(default=None, max_length=240)
website: str | None = Field(default=None, max_length=500)
primary_email: EmailStr | None = None
phone: str | None = Field(default=None, max_length=80)
industry: str | None = Field(default=None, max_length=120)
address: dict[str, Any] = Field(default_factory=dict)
attributes: dict[str, Any] = Field(default_factory=dict)
owner_id: str | None = None
class OrganizationCreate(OrganizationBase):
pass
class OrganizationUpdate(ApiModel):
name: str | None = Field(default=None, min_length=1, max_length=200)
legal_name: str | None = Field(default=None, max_length=240)
website: str | None = Field(default=None, max_length=500)
primary_email: EmailStr | None = None
phone: str | None = Field(default=None, max_length=80)
industry: str | None = Field(default=None, max_length=120)
address: dict[str, Any] | None = None
attributes: dict[str, Any] | None = None
owner_id: str | None = None
class OrganizationOut(OrganizationBase):
id: str
tenant_id: str
created_at: datetime
updated_at: datetime
class ContactBase(ApiModel):
first_name: str = Field(min_length=1, max_length=100)
last_name: str = Field(default="", max_length=100)
job_title: str | None = Field(default=None, max_length=160)
primary_email: EmailStr | None = None
emails: list[dict[str, Any]] = Field(default_factory=list)
phones: list[dict[str, Any]] = Field(default_factory=list)
lifecycle_stage: str = Field(default="lead", max_length=40)
lead_source: str | None = Field(default=None, max_length=120)
score: int = Field(default=0, ge=0, le=100)
organization_id: str | None = None
owner_id: str | None = None
attributes: dict[str, Any] = Field(default_factory=dict)
class ContactCreate(ContactBase):
pass
class ContactUpdate(ApiModel):
first_name: str | None = Field(default=None, min_length=1, max_length=100)
last_name: str | None = Field(default=None, max_length=100)
job_title: str | None = Field(default=None, max_length=160)
primary_email: EmailStr | None = None
emails: list[dict[str, Any]] | None = None
phones: list[dict[str, Any]] | None = None
lifecycle_stage: str | None = Field(default=None, max_length=40)
lead_source: str | None = Field(default=None, max_length=120)
score: int | None = Field(default=None, ge=0, le=100)
organization_id: str | None = None
owner_id: str | None = None
attributes: dict[str, Any] | None = None
class ContactOut(ContactBase):
id: str
tenant_id: str
name: str
organization_name: str | None = None
owner_name: str | None = None
created_at: datetime
updated_at: datetime
class StageOut(ApiModel):
id: str
pipeline_id: str
name: str
position: int
probability: int
color: str
class PipelineOut(ApiModel):
id: str
name: str
is_default: bool
stages: list[StageOut]
class LeadBase(ApiModel):
title: str = Field(min_length=1, max_length=240)
description: str | None = None
value: Decimal = Field(default=Decimal("0"), ge=0)
currency: str = Field(default="USD", min_length=3, max_length=3)
score: int = Field(default=0, ge=0, le=100)
expected_close_at: datetime | None = None
contact_id: str | None = None
organization_id: str | None = None
owner_id: str | None = None
pipeline_id: str
stage_id: str
source_id: str | None = None
type_id: str | None = None
attributes: dict[str, Any] = Field(default_factory=dict)
@field_validator("currency")
@classmethod
def normalize_currency(cls, value: str) -> str:
return value.upper()
class LeadCreate(LeadBase):
pass
class LeadUpdate(ApiModel):
title: str | None = Field(default=None, min_length=1, max_length=240)
description: str | None = None
value: Decimal | None = Field(default=None, ge=0)
currency: str | None = Field(default=None, min_length=3, max_length=3)
status: str | None = Field(default=None, pattern="^(open|won|lost)$")
score: int | None = Field(default=None, ge=0, le=100)
lost_reason: str | None = None
expected_close_at: datetime | None = None
contact_id: str | None = None
organization_id: str | None = None
owner_id: str | None = None
pipeline_id: str | None = None
stage_id: str | None = None
source_id: str | None = None
type_id: str | None = None
attributes: dict[str, Any] | None = None
@field_validator("currency")
@classmethod
def normalize_currency(cls, value: str | None) -> str | None:
return value.upper() if value else value
class LeadMove(ApiModel):
stage_id: str
position: int = Field(default=0, ge=0)
class LeadOut(LeadBase):
id: str
status: str
position: int
lost_reason: str | None
closed_at: datetime | None
contact_name: str | None = None
organization_name: str | None = None
owner_name: str | None = None
pipeline_name: str
stage_name: str
stage_color: str
source_name: str | None = None
type_name: str | None = None
created_at: datetime
updated_at: datetime
class ActivityBase(ApiModel):
activity_type: str = Field(
pattern="^(task|call|meeting|note|email|lunch)$",
)
title: str = Field(min_length=1, max_length=240)
details: str | None = None
starts_at: datetime | None = None
ends_at: datetime | None = None
due_at: datetime | None = None
is_done: bool = False
owner_id: str | None = None
contact_id: str | None = None
organization_id: str | None = None
lead_id: str | None = None
additional: dict[str, Any] = Field(default_factory=dict)
class ActivityCreate(ActivityBase):
pass
class ActivityUpdate(ApiModel):
activity_type: str | None = Field(
default=None,
pattern="^(task|call|meeting|note|email|lunch)$",
)
title: str | None = Field(default=None, min_length=1, max_length=240)
details: str | None = None
starts_at: datetime | None = None
ends_at: datetime | None = None
due_at: datetime | None = None
is_done: bool | None = None
owner_id: str | None = None
contact_id: str | None = None
organization_id: str | None = None
lead_id: str | None = None
additional: dict[str, Any] | None = None
class ActivityOut(ActivityBase):
id: str
completed_at: datetime | None
owner_name: str | None = None
contact_name: str | None = None
organization_name: str | None = None
lead_title: str | None = None
created_at: datetime
updated_at: datetime
class ProductBase(ApiModel):
sku: str = Field(min_length=1, max_length=100)
name: str = Field(min_length=1, max_length=200)
description: str | None = None
quantity: int = Field(default=0, ge=0)
price: Decimal = Field(default=Decimal("0"), ge=0)
currency: str = Field(default="USD", min_length=3, max_length=3)
is_active: bool = True
class ProductCreate(ProductBase):
pass
class ProductOut(ProductBase):
id: str
created_at: datetime
updated_at: datetime
class QuoteOut(ApiModel):
id: str
number: str
subject: str
status: str
currency: str
subtotal: Decimal
grand_total: Decimal
expires_at: datetime | None
created_at: datetime
updated_at: datetime
class QuoteCreate(ApiModel):
number: str = Field(min_length=1, max_length=80)
subject: str = Field(min_length=1, max_length=240)
description: str | None = None
status: str = Field(default="draft", pattern="^(draft|sent|accepted|rejected|expired)$")
currency: str = Field(default="USD", min_length=3, max_length=3)
contact_id: str | None = None
organization_id: str | None = None
lead_id: str | None = None
owner_id: str | None = None
expires_at: datetime | None = None
@field_validator("currency")
@classmethod
def normalize_currency(cls, value: str) -> str:
return value.upper()
class ReferenceItem(ApiModel):
id: str
name: str
class DashboardStageMetric(ApiModel):
stage_id: str
stage_name: str
color: str
lead_count: int
total_value: Decimal
class DashboardSourceMetric(ApiModel):
source: str
lead_count: int
class DashboardResponse(ApiModel):
total_revenue: Decimal
open_pipeline_value: Decimal
total_leads: int
open_leads: int
won_leads: int
lost_leads: int
total_contacts: int
overdue_activities: int
stages: list[DashboardStageMetric]
sources: list[DashboardSourceMetric]
recent_activities: list[ActivityOut]
class IntegrationLeadRequest(ApiModel):
provider: str = Field(min_length=2, max_length=80)
external_id: str = Field(min_length=1, max_length=255)
first_name: str = Field(min_length=1, max_length=100)
last_name: str = Field(default="", max_length=100)
email: EmailStr | None = None
phone: str | None = Field(default=None, max_length=80)
company_name: str | None = Field(default=None, max_length=200)
job_title: str | None = Field(default=None, max_length=160)
lead_title: str | None = Field(default=None, max_length=240)
source: str = Field(default="MaskanX", max_length=120)
score: int = Field(default=0, ge=0, le=100)
campaign: dict[str, Any] = Field(default_factory=dict)
metadata: dict[str, Any] = Field(default_factory=dict)
class IntegrationLeadResponse(ApiModel):
contact_id: str
lead_id: str
created: bool
class IntegrationKeyCreate(ApiModel):
name: str = Field(min_length=2, max_length=120)
class IntegrationKeyResponse(ApiModel):
id: str
name: str
key: str
key_prefix: str
created_at: datetime
class IntegrationCredentialOut(ApiModel):
id: str
name: str
key_prefix: str
is_active: bool
last_used_at: datetime | None
created_at: datetime
updated_at: datetime
class IntegrationStatusResponse(ApiModel):
status: str
product: str
mode: str
tenant_id: str
tenant_name: str
workspace: str
credential_id: str
credential_name: str
key_prefix: str