from __future__ import annotations from sqlalchemy import func, select from sqlalchemy.orm import Session from app.modules.client_groups.models import ClientGroup from app.modules.clients.models import Client GROUP_TYPES = ("Family", "Business Group", "Promoter Group", "Trust Group", "Common Management", "Other") def normalise_group_code(value: str | None) -> str: return (value or "").strip().upper() def list_groups(db: Session, *, tenant_id: int, include_inactive: bool = False): stmt = ( select(ClientGroup, func.count(Client.id).label("client_count")) .outerjoin(Client, Client.client_group_id == ClientGroup.id) .where(ClientGroup.tenant_id == tenant_id) .group_by(ClientGroup.id) .order_by(ClientGroup.group_name.asc()) ) if not include_inactive: stmt = stmt.where(ClientGroup.is_active.is_(True)) return [{"group": group, "client_count": int(count or 0)} for group, count in db.execute(stmt).all()] def get_group(db: Session, *, tenant_id: int, group_id: int): return db.execute(select(ClientGroup).where(ClientGroup.id == group_id, ClientGroup.tenant_id == tenant_id)).scalar_one_or_none() def get_group_by_code(db: Session, *, tenant_id: int, group_code: str): code = normalise_group_code(group_code) if not code: return None return db.execute(select(ClientGroup).where(ClientGroup.tenant_id == tenant_id, ClientGroup.group_code == code)).scalar_one_or_none() def list_group_clients(db: Session, *, tenant_id: int, group_id: int): return db.execute(select(Client).where(Client.tenant_id == tenant_id, Client.client_group_id == group_id, Client.is_archived.is_(False)).order_by(Client.is_group_head.desc(), Client.client_name.asc())).scalars().all() def create_group(db: Session, *, tenant_id: int, actor_user_id: int, payload: dict): code = normalise_group_code(payload.get("group_code")) name = (payload.get("group_name") or "").strip() if not code or not name: raise ValueError("Group code and group name are required.") if get_group_by_code(db, tenant_id=tenant_id, group_code=code): raise ValueError("Client group code already exists in this firm.") group_type = (payload.get("group_type") or "Family").strip() if group_type not in GROUP_TYPES: raise ValueError("Invalid group type.") row = ClientGroup( tenant_id=tenant_id, group_code=code, group_name=name, group_type=group_type, primary_contact_name=(payload.get("primary_contact_name") or "").strip() or None, primary_contact_mobile=(payload.get("primary_contact_mobile") or "").strip() or None, primary_contact_email=(payload.get("primary_contact_email") or "").strip().lower() or None, assigned_partner_user_id=payload.get("assigned_partner_user_id") or None, primary_consultant_id=payload.get("primary_consultant_id") or None, notes=(payload.get("notes") or "").strip() or None, is_active=bool(payload.get("is_active", True)), created_by_user_id=actor_user_id, updated_by_user_id=actor_user_id, ) db.add(row); db.commit(); db.refresh(row); return row def update_group(db: Session, *, row: ClientGroup, actor_user_id: int, payload: dict): code = normalise_group_code(payload.get("group_code")) name = (payload.get("group_name") or "").strip() if not code or not name: raise ValueError("Group code and group name are required.") duplicate = get_group_by_code(db, tenant_id=row.tenant_id, group_code=code) if duplicate and duplicate.id != row.id: raise ValueError("Client group code already exists in this firm.") group_type = (payload.get("group_type") or "Family").strip() if group_type not in GROUP_TYPES: raise ValueError("Invalid group type.") for key, value in { "group_code": code, "group_name": name, "group_type": group_type, "primary_contact_name": (payload.get("primary_contact_name") or "").strip() or None, "primary_contact_mobile": (payload.get("primary_contact_mobile") or "").strip() or None, "primary_contact_email": (payload.get("primary_contact_email") or "").strip().lower() or None, "assigned_partner_user_id": payload.get("assigned_partner_user_id") or None, "primary_consultant_id": payload.get("primary_consultant_id") or None, "notes": (payload.get("notes") or "").strip() or None, "is_active": bool(payload.get("is_active", False)), "updated_by_user_id": actor_user_id, }.items(): setattr(row, key, value) db.add(row); db.commit(); db.refresh(row); return row def resolve_or_create_group(db: Session, *, tenant_id: int, actor_user_id: int, group_code: str | None, group_name: str | None, group_type: str | None = None): code = normalise_group_code(group_code) name = (group_name or "").strip() if not code and not name: return None if not code: raise ValueError("client_group_code is required when client_group_name is supplied.") existing = get_group_by_code(db, tenant_id=tenant_id, group_code=code) if existing: if name and existing.group_name.strip().lower() != name.lower(): raise ValueError(f"Client group code {code} already exists with name {existing.group_name}.") return existing if not name: raise ValueError(f"Client group {code} does not exist; provide client_group_name to create it.") return create_group(db, tenant_id=tenant_id, actor_user_id=actor_user_id, payload={"group_code": code, "group_name": name, "group_type": group_type or "Family", "is_active": True})