Files
maskanx_crm_backend/app/api/activities.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

144 lines
4.3 KiB
Python

from datetime import UTC, datetime
from fastapi import APIRouter, Query, Response, status
from sqlalchemy import func, select
from app.core.security import Admin, CurrentUser, Database, Writer
from app.models import Activity, Contact, Lead, Organization, User
from app.schemas import ActivityCreate, ActivityOut, ActivityUpdate, Page
from app.services import (
activity_load_options,
activity_to_out,
add_audit,
apply_updates,
model_or_404,
verify_optional_reference,
)
router = APIRouter(tags=["Activities"])
def _validate_references(
db: Database,
tenant_id: str,
values: dict[str, object],
) -> None:
verify_optional_reference(db, User, values.get("owner_id"), tenant_id)
verify_optional_reference(db, Contact, values.get("contact_id"), tenant_id)
verify_optional_reference(db, Organization, values.get("organization_id"), tenant_id)
verify_optional_reference(db, Lead, values.get("lead_id"), tenant_id)
@router.get("/activities", response_model=Page[ActivityOut])
def list_activities(
user: CurrentUser,
db: Database,
is_done: bool | None = None,
lead_id: str | None = None,
contact_id: str | None = None,
organization_id: str | None = None,
page: int = Query(default=1, ge=1),
page_size: int = Query(default=25, ge=1, le=100),
) -> Page[ActivityOut]:
filters = [Activity.tenant_id == user.tenant_id]
if is_done is not None:
filters.append(Activity.is_done == is_done)
if lead_id:
filters.append(Activity.lead_id == lead_id)
if contact_id:
filters.append(Activity.contact_id == contact_id)
if organization_id:
filters.append(Activity.organization_id == organization_id)
total = db.scalar(select(func.count(Activity.id)).where(*filters)) or 0
records = db.scalars(
select(Activity)
.where(*filters)
.options(*activity_load_options())
.order_by(Activity.is_done, Activity.due_at, Activity.created_at.desc())
.offset((page - 1) * page_size)
.limit(page_size),
).all()
return Page(
items=[activity_to_out(item) for item in records],
total=total,
page=page,
page_size=page_size,
)
@router.post(
"/activities",
response_model=ActivityOut,
status_code=status.HTTP_201_CREATED,
)
def create_activity(
payload: ActivityCreate,
user: Writer,
db: Database,
) -> ActivityOut:
values = payload.model_dump()
_validate_references(db, user.tenant_id, values)
if values["is_done"]:
values["completed_at"] = datetime.now(UTC)
activity = Activity(tenant_id=user.tenant_id, **values)
db.add(activity)
db.flush()
add_audit(
db,
actor=user,
tenant_id=user.tenant_id,
action="activity.created",
entity_type="activity",
entity_id=activity.id,
)
db.commit()
activity = db.scalar(
select(Activity)
.where(Activity.id == activity.id)
.options(*activity_load_options()),
)
return activity_to_out(activity)
@router.patch("/activities/{activity_id}", response_model=ActivityOut)
def update_activity(
activity_id: str,
payload: ActivityUpdate,
user: Writer,
db: Database,
) -> ActivityOut:
activity = model_or_404(db, Activity, activity_id, user.tenant_id)
values = payload.model_dump(exclude_unset=True)
_validate_references(db, user.tenant_id, values)
if "is_done" in values:
values["completed_at"] = datetime.now(UTC) if values["is_done"] else None
apply_updates(activity, values)
add_audit(
db,
actor=user,
tenant_id=user.tenant_id,
action="activity.updated",
entity_type="activity",
entity_id=activity.id,
payload={"fields": sorted(values)},
)
db.commit()
activity = db.scalar(
select(Activity)
.where(Activity.id == activity.id)
.options(*activity_load_options()),
)
return activity_to_out(activity)
@router.delete("/activities/{activity_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_activity(
activity_id: str,
user: Admin,
db: Database,
) -> Response:
activity = model_or_404(db, Activity, activity_id, user.tenant_id)
db.delete(activity)
db.commit()
return Response(status_code=status.HTTP_204_NO_CONTENT)