Files
crm-system/app/services/contact_service.py
T

136 lines
3.6 KiB
Python
Raw Normal View History

2026-06-04 00:06:25 +00:00
"""Contact service: create, get, list (with filters), update, soft-delete."""
from __future__ import annotations
from datetime import UTC, datetime
from typing import Optional
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.account import Account
from app.models.contact import Contact
from app.schemas.contact import ContactCreate, ContactUpdate
from app.services._base import OrgScopedQuery
class ContactNotFound(Exception):
"""Raised when a contact lookup fails."""
class InvalidAccount(Exception):
"""Raised when account_id points to a non-existent account."""
async def _validate_account(
db: AsyncSession, account_id: int, *, org_id: int
) -> None:
"""Raise if account_id does not exist within the org."""
if account_id is None:
return
q = OrgScopedQuery(Account, db, org_id=org_id)
exists = await q.get(account_id)
if exists is None:
raise InvalidAccount(f"Account {account_id} not found in this org")
async def create_contact(
db: AsyncSession,
payload: ContactCreate,
*,
org_id: int,
owner_id: int,
) -> Contact:
"""Create a new contact. Validates account_id if provided."""
await _validate_account(db, payload.account_id, org_id=org_id)
contact = Contact(
org_id=org_id,
first_name=payload.first_name,
last_name=payload.last_name,
email=payload.email,
phone=payload.phone,
account_id=payload.account_id,
owner_id=owner_id,
)
db.add(contact)
try:
await db.commit()
except IntegrityError as e:
await db.rollback()
raise InvalidAccount(str(e)) from e
await db.refresh(contact)
return contact
async def get_contact(
db: AsyncSession, contact_id: int, *, org_id: int
) -> Optional[Contact]:
"""Fetch a single contact by id."""
q = OrgScopedQuery(Contact, db, org_id=org_id)
return await q.get(contact_id)
async def list_contacts(
db: AsyncSession,
*,
org_id: int,
skip: int = 0,
limit: int = 20,
account_id: Optional[int] = None,
owner_id: Optional[int] = None,
q: Optional[str] = None,
) -> list[Contact]:
"""List contacts with filters and search."""
scoped = OrgScopedQuery(Contact, db, org_id=org_id)
base = await scoped.list(
skip=skip,
limit=limit,
order_by=Contact.id,
account_id=account_id,
owner_id=owner_id,
)
if q:
needle = q.lower()
base = [
c
for c in base
if needle in c.first_name.lower()
or needle in c.last_name.lower()
or (c.email and needle in c.email.lower())
]
return base
async def update_contact(
db: AsyncSession, contact: Contact, payload: ContactUpdate
) -> Contact:
"""Apply partial updates to a contact."""
data = payload.model_dump(exclude_unset=True)
if "account_id" in data and data["account_id"] is not None:
await _validate_account(db, data["account_id"], org_id=contact.org_id)
for field, value in data.items():
if value is not None:
setattr(contact, field, value)
await db.commit()
await db.refresh(contact)
return contact
async def soft_delete_contact(db: AsyncSession, contact: Contact) -> Contact:
"""Soft-delete a contact."""
contact.deleted_at = datetime.now(UTC)
await db.commit()
await db.refresh(contact)
return contact
__all__ = [
"ContactNotFound",
"InvalidAccount",
"create_contact",
"get_contact",
"list_contacts",
"soft_delete_contact",
"update_contact",
]