from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import Response from sqlalchemy.orm import Session from app.core.db.deps import get_common_db from app.core.security.session_auth import require_login from app.modules.clients.access import ClientAccessScope, enforce_partner_scope, is_partner_role from app.modules.clients.schemas import ClientAuditLogOut, ClientFilterOptions, ClientListResponse, ClientOut, ClientUpdate, ClientCreate from app.modules.clients.service import ( activate_client_service, archive_client_service, create_client_service, deactivate_client_service, export_clients_csv, get_client_or_404, get_filter_options, list_client_audit_logs, list_clients_payload, restore_client_service, update_client_service, ) from app.modules.core.rbac.permission_guard import require_permission from app.modules.core.rbac.deps import get_user_roles router = APIRouter(prefix="/api/v1/clients", tags=["clients-api"]) def _api_scope_from_user(db, user): def has(code: str): try: require_permission(db, user, code) return True except Exception: return False own_only = has("clients.view.own_only") or not has("clients.assign_partner") scope = ClientAccessScope( tenant_id=user.tenant_id, branch_id=user.branch_id, allow_cross_branch=has("clients.cross_branch"), allow_cross_tenant=has("clients.cross_tenant"), allow_all_clients=has("clients.view.all") and not own_only, own_only=own_only, locked_partner_id=user.id if own_only else None, can_assign_partner=has("clients.assign_partner"), can_change_branch=has("clients.cross_branch"), can_change_tenant=has("clients.cross_tenant"), ) return enforce_partner_scope(scope, user=user, role_names=get_user_roles(db, user.id)) @router.get("/filters", response_model=ClientFilterOptions) def api_client_filters(): return get_filter_options() @router.get("", response_model=ClientListResponse) def api_list_clients( q: str = Query("", max_length=100), status: str = Query("", max_length=20), client_type: str = Query("", max_length=100), partner_id: int | None = Query(None), include_archived: bool = Query(False), page: int = Query(1, ge=1), per_page: int = Query(10, ge=1, le=100), sort_by: str = Query("client_name"), sort_order: str = Query("asc"), db: Session = Depends(get_common_db), user=Depends(require_login), ): require_permission(db, user, "clients.view") scope = _api_scope_from_user(db, user) if scope.own_only: partner_id = scope.locked_partner_id return list_clients_payload( db, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, partner_id=None if scope.own_only else partner_id, viewer_partner_id=user.id if scope.own_only else None, q=q, status=status, client_type=client_type, include_archived=include_archived, page=page, per_page=per_page, sort_by=sort_by, sort_order=sort_order, ) @router.get("/export") def api_export_clients( q: str = Query("", max_length=100), status: str = Query("", max_length=20), client_type: str = Query("", max_length=100), partner_id: int | None = Query(None), include_archived: bool = Query(False), sort_by: str = Query("client_name"), sort_order: str = Query("asc"), db: Session = Depends(get_common_db), user=Depends(require_login), ): require_permission(db, user, "clients.export") scope = _api_scope_from_user(db, user) if scope.own_only: partner_id = scope.locked_partner_id payload = list_clients_payload( db, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, partner_id=None if scope.own_only else partner_id, viewer_partner_id=user.id if scope.own_only else None, q=q, status=status, client_type=client_type, include_archived=include_archived, page=1, per_page=10000, sort_by=sort_by, sort_order=sort_order, ) csv_text = export_clients_csv(payload) return Response(content=csv_text, media_type="text/csv", headers={"Content-Disposition": "attachment; filename=clients_export.csv"}) @router.get("/{client_id}", response_model=ClientOut) def api_get_client(client_id: int, db: Session = Depends(get_common_db), user=Depends(require_login)): require_permission(db, user, "clients.view") scope = _api_scope_from_user(db, user) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=user.id if scope.own_only else None, allow_review_access=True) return row @router.post("", response_model=ClientOut, status_code=201) def api_create_client(data: ClientCreate, db: Session = Depends(get_common_db), user=Depends(require_login)): require_permission(db, user, "clients.create") scope = _api_scope_from_user(db, user) return create_client_service(db, data=data, actor_user_id=user.id, scope=scope) @router.put("/{client_id}", response_model=ClientOut) def api_update_client(client_id: int, data: ClientUpdate, db: Session = Depends(get_common_db), user=Depends(require_login)): require_permission(db, user, "clients.edit") scope = _api_scope_from_user(db, user) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=user.id if scope.own_only else None) if scope.own_only and row.partner_id != scope.locked_partner_id: raise HTTPException(status_code=404, detail="Client not found.") return update_client_service(db, row=row, data=data, actor_user_id=user.id, scope=scope) @router.post("/{client_id}/deactivate", response_model=ClientOut) def api_deactivate_client(client_id: int, db: Session = Depends(get_common_db), user=Depends(require_login)): require_permission(db, user, "clients.deactivate") scope = _api_scope_from_user(db, user) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=user.id if scope.own_only else None) return deactivate_client_service(db, row=row, actor_user_id=user.id) @router.post("/{client_id}/activate", response_model=ClientOut) def api_activate_client(client_id: int, db: Session = Depends(get_common_db), user=Depends(require_login)): require_permission(db, user, "clients.activate") scope = _api_scope_from_user(db, user) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=user.id if scope.own_only else None) return activate_client_service(db, row=row, actor_user_id=user.id) @router.post("/{client_id}/archive", response_model=ClientOut) def api_archive_client(client_id: int, db: Session = Depends(get_common_db), user=Depends(require_login)): require_permission(db, user, "clients.archive") scope = _api_scope_from_user(db, user) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=user.id if scope.own_only else None) return archive_client_service(db, row=row, actor_user_id=user.id) @router.post("/{client_id}/restore", response_model=ClientOut) def api_restore_client(client_id: int, db: Session = Depends(get_common_db), user=Depends(require_login)): require_permission(db, user, "clients.restore") scope = _api_scope_from_user(db, user) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=user.id if scope.own_only else None) return restore_client_service(db, row=row, actor_user_id=user.id) @router.get("/{client_id}/audit-logs", response_model=list[ClientAuditLogOut]) def api_client_audit_logs(client_id: int, limit: int = Query(50, ge=1, le=200), db: Session = Depends(get_common_db), user=Depends(require_login)): require_permission(db, user, "clients.audit_log.view") scope = _api_scope_from_user(db, user) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=user.id if scope.own_only else None) return list_client_audit_logs(db, row=row, limit=limit)