from __future__ import annotations from dataclasses import dataclass from typing import Any from sqlalchemy import and_, func, or_, select from sqlalchemy.orm import joinedload from app.modules.clients.models import Client from app.modules.core.rbac.deps import get_user_permissions, get_user_roles from app.modules.services.models import ( ClientServiceSubscription, ClientServiceTaskInstance, EngagementClosureChecklist, ServiceCatalogue, ) ASSURANCE_TYPE = "assurance" APPROVED_STATUSES = {"approved", "completed", "complete"} REVIEWED_STATUSES = {"reviewed", "approved", "completed", "complete", "not_required"} EVIDENCE_OK_STATUSES = {"accepted", "approved", "reviewed", "uploaded", "not_required"} CLOSURE_CLOSED_STATUSES = {"closed", "completed"} @dataclass(frozen=True) class AqmmScope: tenant_id: int | None branch_id: int | None own_user_id: int | None def _clean(value: Any) -> str: return str(value or "").strip() def _active_tenant_id(request, user) -> int | None: value = ( request.session.get("active_tenant_id") or request.session.get("selected_tenant_id") or request.session.get("tenant_id") or getattr(user, "tenant_id", None) ) return int(value) if value not in (None, "", 0, "0") else None def _active_branch_id(request, user, permissions: set[str]) -> int | None: value = request.session.get("active_branch_id") if value in (None, "", 0, "0"): if "clients.cross_branch" in permissions or "services.cross_branch" in permissions: return None branch_id = getattr(user, "branch_id", None) return int(branch_id) if branch_id not in (None, "", 0, "0") else None return int(value) def build_scope(db, request, user) -> AqmmScope: roles = set(get_user_roles(db, user.id)) permissions = set(get_user_permissions(db, user.id)) own_user_id = None if "clients.view.own_only" in permissions and "Firm Admin" not in roles: own_user_id = int(user.id) return AqmmScope( tenant_id=_active_tenant_id(request, user), branch_id=_active_branch_id(request, user, permissions), own_user_id=own_user_id, ) def can_view_aqmm_dashboard(db, user) -> bool: """AQMM dashboard is intentionally restricted to Firm Admin and Partner roles. Managers/staff may still complete assigned work and review actions through Work Tracker, but the consolidated AQMM monitoring dashboard is a leadership view and must not be exposed through broad service/client permissions. """ roles = set(get_user_roles(db, user.id)) return bool(roles.intersection({"Firm Admin", "Partner"})) def _subscription_filters(scope: AqmmScope, financial_year: str = "", q: str = ""): filters = [ClientServiceSubscription.engagement_type == ASSURANCE_TYPE] if scope.tenant_id: filters.append(ClientServiceSubscription.tenant_id == scope.tenant_id) if scope.branch_id: filters.append(ClientServiceSubscription.branch_id == scope.branch_id) if financial_year: filters.append(ClientServiceSubscription.financial_year == financial_year) if scope.own_user_id: filters.append( or_( ClientServiceSubscription.assigned_partner_user_id == scope.own_user_id, ClientServiceSubscription.assigned_manager_user_id == scope.own_user_id, ClientServiceSubscription.assigned_staff_user_id == scope.own_user_id, ClientServiceSubscription.review_partner_user_id == scope.own_user_id, Client.partner_id == scope.own_user_id, ) ) if q: like = f"%{q.lower()}%" filters.append( or_( func.lower(Client.client_name).like(like), func.lower(Client.client_code).like(like), func.lower(ServiceCatalogue.service_name).like(like), func.lower(ServiceCatalogue.service_code).like(like), ) ) return filters def _count(db, stmt) -> int: return int(db.execute(stmt).scalar_one() or 0) def _subscription_id_subquery(scope: AqmmScope, financial_year: str = "", q: str = ""): return ( select(ClientServiceSubscription.id) .join(Client, Client.id == ClientServiceSubscription.client_id) .join(ServiceCatalogue, ServiceCatalogue.id == ClientServiceSubscription.service_catalogue_id) .where(and_(*_subscription_filters(scope, financial_year, q))) .subquery() ) def _task_count(db, subq, *extra_filters) -> int: return _count( db, select(func.count(ClientServiceTaskInstance.id)).where( ClientServiceTaskInstance.subscription_id.in_(select(subq.c.id)), ClientServiceTaskInstance.is_aqmm_task.is_(True), *extra_filters, ), ) def _subscription_task_counts(db, subscription_id: int) -> dict[str, int]: base = [ClientServiceTaskInstance.subscription_id == subscription_id, ClientServiceTaskInstance.is_aqmm_task.is_(True)] total = _count(db, select(func.count(ClientServiceTaskInstance.id)).where(*base)) mandatory = _count(db, select(func.count(ClientServiceTaskInstance.id)).where(*base, ClientServiceTaskInstance.aqmm_mandatory.is_(True))) evidence_pending = _count( db, select(func.count(ClientServiceTaskInstance.id)).where( *base, ClientServiceTaskInstance.aqmm_evidence_required.is_(True), ClientServiceTaskInstance.evidence_status.notin_(list(EVIDENCE_OK_STATUSES)), ), ) manager_pending = _count( db, select(func.count(ClientServiceTaskInstance.id)).where( *base, ClientServiceTaskInstance.aqmm_manager_review_required.is_(True), ClientServiceTaskInstance.manager_review_status.notin_(list(REVIEWED_STATUSES)), ), ) partner_pending = _count( db, select(func.count(ClientServiceTaskInstance.id)).where( *base, ClientServiceTaskInstance.aqmm_partner_review_required.is_(True), ClientServiceTaskInstance.partner_review_status.notin_(list(REVIEWED_STATUSES)), ), ) review_partner_pending = _count( db, select(func.count(ClientServiceTaskInstance.id)).where( *base, ClientServiceTaskInstance.aqmm_review_partner_required.is_(True), ClientServiceTaskInstance.review_partner_review_status.notin_(list(REVIEWED_STATUSES)), ), ) completed = _count( db, select(func.count(ClientServiceTaskInstance.id)).where( *base, or_( ClientServiceTaskInstance.aqmm_status.in_(["completed", "complete", "approved"]), ClientServiceTaskInstance.status.in_(["completed", "complete", "done"]), ), ), ) return { "total": total, "mandatory": mandatory, "completed": completed, "evidence_pending": evidence_pending, "manager_pending": manager_pending, "partner_pending": partner_pending, "review_partner_pending": review_partner_pending, } def build_aqmm_dashboard_payload(db, request, user, *, financial_year: str = "", q: str = "") -> dict[str, Any]: financial_year = _clean(financial_year) q = _clean(q) scope = build_scope(db, request, user) subq = _subscription_id_subquery(scope, financial_year, q) total_assurance = _count(db, select(func.count()).select_from(subq)) aqmm_approved = _count( db, select(func.count(ClientServiceSubscription.id)).where( ClientServiceSubscription.id.in_(select(subq.c.id)), ClientServiceSubscription.quality_workflow_status.in_(list(APPROVED_STATUSES)), ), ) aqmm_pending = max(total_assurance - aqmm_approved, 0) kyc_pending = _count( db, select(func.count(ClientServiceSubscription.id)).where( ClientServiceSubscription.id.in_(select(subq.c.id)), ClientServiceSubscription.quality_kyc_status.notin_(list(APPROVED_STATUSES) + ["not_required"]), ), ) declaration_pending = _count( db, select(func.count(ClientServiceSubscription.id)).where( ClientServiceSubscription.id.in_(select(subq.c.id)), or_( ClientServiceSubscription.quality_independence_status.notin_(list(APPROVED_STATUSES) + ["not_required"]), ClientServiceSubscription.quality_conflict_status.notin_(list(APPROVED_STATUSES) + ["not_required"]), ), ), ) letter_pending = _count( db, select(func.count(ClientServiceSubscription.id)).where( ClientServiceSubscription.id.in_(select(subq.c.id)), ClientServiceSubscription.quality_engagement_letter_status.notin_(list(APPROVED_STATUSES) + ["not_required"]), ), ) aqmm_tasks_total = _task_count(db, subq) evidence_pending = _task_count( db, subq, ClientServiceTaskInstance.aqmm_evidence_required.is_(True), ClientServiceTaskInstance.evidence_status.notin_(list(EVIDENCE_OK_STATUSES)), ) manager_review_pending = _task_count( db, subq, ClientServiceTaskInstance.aqmm_manager_review_required.is_(True), ClientServiceTaskInstance.manager_review_status.notin_(list(REVIEWED_STATUSES)), ) partner_review_pending = _task_count( db, subq, ClientServiceTaskInstance.aqmm_partner_review_required.is_(True), ClientServiceTaskInstance.partner_review_status.notin_(list(REVIEWED_STATUSES)), ) review_partner_pending = _task_count( db, subq, ClientServiceTaskInstance.aqmm_review_partner_required.is_(True), ClientServiceTaskInstance.review_partner_review_status.notin_(list(REVIEWED_STATUSES)), ) rework_pending = _task_count( db, subq, ClientServiceTaskInstance.rework_status.in_(["requested", "pending", "rework_required"]), ) closure_closed = _count( db, select(func.count(EngagementClosureChecklist.id)).where( EngagementClosureChecklist.subscription_id.in_(select(subq.c.id)), EngagementClosureChecklist.closure_status.in_(list(CLOSURE_CLOSED_STATUSES)), ), ) closure_pending = max(total_assurance - closure_closed, 0) engagement_rows = db.execute( select(ClientServiceSubscription, EngagementClosureChecklist) .join(Client, Client.id == ClientServiceSubscription.client_id) .join(ServiceCatalogue, ServiceCatalogue.id == ClientServiceSubscription.service_catalogue_id) .outerjoin(EngagementClosureChecklist, EngagementClosureChecklist.subscription_id == ClientServiceSubscription.id) .options( joinedload(ClientServiceSubscription.client), joinedload(ClientServiceSubscription.catalogue), joinedload(ClientServiceSubscription.assigned_partner), joinedload(ClientServiceSubscription.assigned_manager), joinedload(ClientServiceSubscription.assigned_staff), joinedload(ClientServiceSubscription.review_partner), ) .where(and_(*_subscription_filters(scope, financial_year, q))) .order_by(ClientServiceSubscription.updated_at_utc.desc(), ClientServiceSubscription.id.desc()) .limit(50) ).all() engagements = [] for subscription, closure in engagement_rows: counts = _subscription_task_counts(db, subscription.id) blockers = [] if subscription.quality_workflow_status not in APPROVED_STATUSES: blockers.append("AQMM acceptance pending") if counts["evidence_pending"]: blockers.append(f"Evidence pending: {counts['evidence_pending']}") if counts["manager_pending"]: blockers.append(f"Manager review pending: {counts['manager_pending']}") if counts["partner_pending"]: blockers.append(f"Partner review pending: {counts['partner_pending']}") if counts["review_partner_pending"]: blockers.append(f"Review partner review pending: {counts['review_partner_pending']}") if closure and closure.closure_status not in CLOSURE_CLOSED_STATUSES: blockers.append("Closure pending") engagements.append({ "row": subscription, "closure": closure, "counts": counts, "blockers": blockers, }) pending_tasks = db.execute( select(ClientServiceTaskInstance) .options(joinedload(ClientServiceTaskInstance.subscription).joinedload(ClientServiceSubscription.client), joinedload(ClientServiceTaskInstance.subscription).joinedload(ClientServiceSubscription.catalogue)) .where( ClientServiceTaskInstance.subscription_id.in_(select(subq.c.id)), ClientServiceTaskInstance.is_aqmm_task.is_(True), or_( and_(ClientServiceTaskInstance.aqmm_evidence_required.is_(True), ClientServiceTaskInstance.evidence_status.notin_(list(EVIDENCE_OK_STATUSES))), and_(ClientServiceTaskInstance.aqmm_manager_review_required.is_(True), ClientServiceTaskInstance.manager_review_status.notin_(list(REVIEWED_STATUSES))), and_(ClientServiceTaskInstance.aqmm_partner_review_required.is_(True), ClientServiceTaskInstance.partner_review_status.notin_(list(REVIEWED_STATUSES))), and_(ClientServiceTaskInstance.aqmm_review_partner_required.is_(True), ClientServiceTaskInstance.review_partner_review_status.notin_(list(REVIEWED_STATUSES))), ClientServiceTaskInstance.rework_status.in_(["requested", "pending", "rework_required"]), ), ) .order_by(ClientServiceTaskInstance.internal_target_date.asc().nulls_last(), ClientServiceTaskInstance.sequence_no.asc()) .limit(50) ).scalars().all() return { "financial_year": financial_year, "q": q, "scope": scope, "kpis": { "total_assurance": total_assurance, "aqmm_approved": aqmm_approved, "aqmm_pending": aqmm_pending, "declaration_pending": declaration_pending, "kyc_pending": kyc_pending, "engagement_letter_pending": letter_pending, "aqmm_tasks_total": aqmm_tasks_total, "evidence_pending": evidence_pending, "manager_review_pending": manager_review_pending, "partner_review_pending": partner_review_pending, "review_partner_pending": review_partner_pending, "rework_pending": rework_pending, "closure_pending": closure_pending, "closure_closed": closure_closed, }, "engagements": engagements, "pending_tasks": pending_tasks, }