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