TorbatYar/backend/services/crm/app/api/v1/entities.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

408 lines
14 KiB
Python

"""Phase 6.1 supporting CRM APIs — lookups, tags, addresses, notes, attachments, custom fields, audit."""
from __future__ import annotations
from uuid import UUID
from fastapi import APIRouter, Depends, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_db, get_pagination, require_tenant
from app.core.security import get_current_user
from app.models.types import CrmEntityType
from app.schemas.foundation import (
AddressCreate,
AddressRead,
AddressUpdate,
AttachmentCreate,
AttachmentRead,
AuditRead,
CustomFieldCreate,
CustomFieldRead,
CustomFieldValueRead,
CustomFieldValueUpsert,
LeadSourceRead,
LeadStatusCreate,
LeadStatusRead,
LookupCreate,
LookupRead,
NoteCreate,
NoteRead,
NoteUpdate,
TagAssignRequest,
TagCreate,
TagRead,
)
from app.services.audit_service import AuditService
from app.services.supporting_services import (
AddressService,
AttachmentService,
CustomFieldService,
LookupService,
NoteService,
TagService,
)
from shared.pagination import PaginationParams
from shared.security import CurrentUser
router = APIRouter()
# ── Lookups ──────────────────────────────────────────────────────────────────
@router.post(
"/lookups/lead-sources",
response_model=LeadSourceRead,
status_code=status.HTTP_201_CREATED,
)
async def create_lead_source(
body: LookupCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await LookupService(db).create_lead_source(tenant_id, body)
@router.get("/lookups/lead-sources", response_model=list[LeadSourceRead])
async def list_lead_sources(
tenant_id: UUID = Depends(require_tenant),
pagination: PaginationParams = Depends(get_pagination),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await LookupService(db).list_lead_sources(
tenant_id, offset=pagination.offset, limit=pagination.page_size
)
@router.post(
"/lookups/lead-statuses",
response_model=LeadStatusRead,
status_code=status.HTTP_201_CREATED,
)
async def create_lead_status(
body: LeadStatusCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await LookupService(db).create_lead_status(tenant_id, body)
@router.get("/lookups/lead-statuses", response_model=list[LeadStatusRead])
async def list_lead_statuses(
tenant_id: UUID = Depends(require_tenant),
pagination: PaginationParams = Depends(get_pagination),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await LookupService(db).list_lead_statuses(
tenant_id, offset=pagination.offset, limit=pagination.page_size
)
@router.post(
"/lookups/contact-types",
response_model=LookupRead,
status_code=status.HTTP_201_CREATED,
)
async def create_contact_type(
body: LookupCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await LookupService(db).create_contact_type(tenant_id, body)
@router.get("/lookups/contact-types", response_model=list[LookupRead])
async def list_contact_types(
tenant_id: UUID = Depends(require_tenant),
pagination: PaginationParams = Depends(get_pagination),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await LookupService(db).list_contact_types(
tenant_id, offset=pagination.offset, limit=pagination.page_size
)
@router.post(
"/lookups/organization-types",
response_model=LookupRead,
status_code=status.HTTP_201_CREATED,
)
async def create_organization_type(
body: LookupCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await LookupService(db).create_organization_type(tenant_id, body)
@router.get("/lookups/organization-types", response_model=list[LookupRead])
async def list_organization_types(
tenant_id: UUID = Depends(require_tenant),
pagination: PaginationParams = Depends(get_pagination),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await LookupService(db).list_organization_types(
tenant_id, offset=pagination.offset, limit=pagination.page_size
)
@router.post(
"/lookups/organization-industries",
response_model=LookupRead,
status_code=status.HTTP_201_CREATED,
)
async def create_organization_industry(
body: LookupCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await LookupService(db).create_organization_industry(tenant_id, body)
@router.get("/lookups/organization-industries", response_model=list[LookupRead])
async def list_organization_industries(
tenant_id: UUID = Depends(require_tenant),
pagination: PaginationParams = Depends(get_pagination),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await LookupService(db).list_organization_industries(
tenant_id, offset=pagination.offset, limit=pagination.page_size
)
# ── Tags ─────────────────────────────────────────────────────────────────────
@router.post("/tags", response_model=TagRead, status_code=status.HTTP_201_CREATED)
async def create_tag(
body: TagCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await TagService(db).create(tenant_id, body)
@router.get("/tags", response_model=list[TagRead])
async def list_tags(
tenant_id: UUID = Depends(require_tenant),
pagination: PaginationParams = Depends(get_pagination),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await TagService(db).list(
tenant_id, offset=pagination.offset, limit=pagination.page_size
)
@router.post("/tags/leads/{lead_id}")
async def assign_lead_tag(
lead_id: UUID,
body: TagAssignRequest,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
row = await TagService(db).assign_to_lead(tenant_id, lead_id, body.tag_id)
return {"id": str(row.id), "lead_id": str(row.lead_id), "tag_id": str(row.tag_id)}
@router.post("/tags/contacts/{contact_id}")
async def assign_contact_tag(
contact_id: UUID,
body: TagAssignRequest,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
row = await TagService(db).assign_to_contact(tenant_id, contact_id, body.tag_id)
return {
"id": str(row.id),
"contact_id": str(row.contact_id),
"tag_id": str(row.tag_id),
}
@router.post("/tags/organizations/{organization_id}")
async def assign_organization_tag(
organization_id: UUID,
body: TagAssignRequest,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
row = await TagService(db).assign_to_organization(
tenant_id, organization_id, body.tag_id
)
return {
"id": str(row.id),
"organization_id": str(row.organization_id),
"tag_id": str(row.tag_id),
}
# ── Addresses ────────────────────────────────────────────────────────────────
@router.post(
"/addresses", response_model=AddressRead, status_code=status.HTTP_201_CREATED
)
async def create_address(
body: AddressCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(get_current_user),
):
return await AddressService(db).create(tenant_id, body, actor=user)
@router.get("/addresses", response_model=list[AddressRead])
async def list_addresses(
entity_type: CrmEntityType = Query(...),
entity_id: UUID = Query(...),
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await AddressService(db).list_for_entity(tenant_id, entity_type, entity_id)
@router.patch("/addresses/{address_id}", response_model=AddressRead)
async def update_address(
address_id: UUID,
body: AddressUpdate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(get_current_user),
):
return await AddressService(db).update(tenant_id, address_id, body, actor=user)
# ── Notes ────────────────────────────────────────────────────────────────────
@router.post("/notes", response_model=NoteRead, status_code=status.HTTP_201_CREATED)
async def create_note(
body: NoteCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(get_current_user),
):
return await NoteService(db).create(tenant_id, body, actor=user)
@router.get("/notes", response_model=list[NoteRead])
async def list_notes(
entity_type: CrmEntityType = Query(...),
entity_id: UUID = Query(...),
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await NoteService(db).list_for_entity(tenant_id, entity_type, entity_id)
@router.patch("/notes/{note_id}", response_model=NoteRead)
async def update_note(
note_id: UUID,
body: NoteUpdate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(get_current_user),
):
return await NoteService(db).update(tenant_id, note_id, body, actor=user)
# ── Attachments ──────────────────────────────────────────────────────────────
@router.post(
"/attachments",
response_model=AttachmentRead,
status_code=status.HTTP_201_CREATED,
)
async def create_attachment(
body: AttachmentCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(get_current_user),
):
return await AttachmentService(db).create(tenant_id, body, actor=user)
@router.get("/attachments", response_model=list[AttachmentRead])
async def list_attachments(
entity_type: CrmEntityType = Query(...),
entity_id: UUID = Query(...),
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await AttachmentService(db).list_for_entity(
tenant_id, entity_type, entity_id
)
# ── Custom fields ────────────────────────────────────────────────────────────
@router.post(
"/custom-fields",
response_model=CustomFieldRead,
status_code=status.HTTP_201_CREATED,
)
async def create_custom_field(
body: CustomFieldCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await CustomFieldService(db).create(tenant_id, body)
@router.get("/custom-fields", response_model=list[CustomFieldRead])
async def list_custom_fields(
entity_type: CrmEntityType = Query(...),
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await CustomFieldService(db).list_definitions(tenant_id, entity_type)
@router.put(
"/custom-fields/values",
response_model=CustomFieldValueRead,
)
async def upsert_custom_field_value(
body: CustomFieldValueUpsert,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await CustomFieldService(db).upsert_value(tenant_id, body)
# ── Audit ────────────────────────────────────────────────────────────────────
@router.get("/audit", response_model=list[AuditRead])
async def list_audit(
entity_type: str = Query(...),
entity_id: UUID = Query(...),
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
return await AuditService(db).list_for_entity(tenant_id, entity_type, entity_id)