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>
144 lines
4.3 KiB
Python
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)
|