from __future__ import annotations from datetime import date, datetime, timezone from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from app.core.db.common import CommonBase class ConsultantProfile(CommonBase): """Portal-enabled consultant / ecosystem partner profile. The login/security account remains in users. This table stores consultant business/profile details and links the consultant user to firm clients. """ __tablename__ = "consultant_profiles" __table_args__ = ( UniqueConstraint("tenant_id", "user_id", name="uq_consultant_profiles_tenant_user"), UniqueConstraint("tenant_id", "email", name="uq_consultant_profiles_tenant_email"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True) branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id"), nullable=True, index=True) user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True) consultant_type: Mapped[str] = mapped_column(String(50), nullable=False, default="external_consultant", index=True) firm_name: Mapped[str | None] = mapped_column(String(200), nullable=True) contact_person: Mapped[str] = mapped_column(String(200), nullable=False) email: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True) mobile: Mapped[str | None] = mapped_column(String(20), nullable=True) specialisation: Mapped[str | None] = mapped_column(String(200), nullable=True) gstin: Mapped[str | None] = mapped_column(String(20), nullable=True) pan: Mapped[str | None] = mapped_column(String(20), nullable=True) address: Mapped[str | None] = mapped_column(Text, nullable=True) status: Mapped[str] = mapped_column(String(30), nullable=False, default="active", index=True) onboarding_status: Mapped[str] = mapped_column(String(30), nullable=False, default="approved", index=True) is_platform_partner: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_franchise_partner: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_saas_customer: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, index=True) remarks: Mapped[str | None] = mapped_column(Text, nullable=True) created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) updated_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) created_at_utc: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False ) updated_at_utc: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False, ) user = relationship("User", foreign_keys=[user_id]) links = relationship( "ClientConsultantLink", back_populates="consultant", cascade="all, delete-orphan", passive_deletes=True, ) workspace = relationship( "ConsultantWorkspace", back_populates="consultant", uselist=False, cascade="all, delete-orphan", passive_deletes=True, ) managed_clients = relationship( "ConsultantManagedClient", back_populates="consultant", cascade="all, delete-orphan", passive_deletes=True, ) service_requests = relationship( "ConsultantServiceRequest", back_populates="consultant", cascade="all, delete-orphan", passive_deletes=True, ) class ConsultantWorkspace(CommonBase): """SaaS/franchise workspace settings for a consultant portal account. This is a foundation table only. It does not change firm-owned clients or consultant-managed clients. It records whether the consultant operates as a SaaS customer, franchise partner, platform partner, or a normal external consultant, along with soft limits used by later subscription/billing phases. """ __tablename__ = "consultant_workspaces" __table_args__ = ( UniqueConstraint("tenant_id", "consultant_id", name="uq_consultant_workspaces_tenant_consultant"), UniqueConstraint("tenant_id", "workspace_code", name="uq_consultant_workspaces_tenant_code"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True) branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id"), nullable=True, index=True) consultant_id: Mapped[int] = mapped_column( ForeignKey("consultant_profiles.id", ondelete="CASCADE"), nullable=False, index=True ) workspace_code: Mapped[str] = mapped_column(String(50), nullable=False, index=True) workspace_name: Mapped[str] = mapped_column(String(200), nullable=False) workspace_type: Mapped[str] = mapped_column(String(50), nullable=False, default="consultant_saas", index=True) plan_code: Mapped[str] = mapped_column(String(50), nullable=False, default="starter", index=True) billing_cycle: Mapped[str] = mapped_column(String(30), nullable=False, default="manual", index=True) subscription_status: Mapped[str] = mapped_column(String(30), nullable=False, default="trial", index=True) subscription_start_date: Mapped[date | None] = mapped_column(Date, nullable=True) subscription_end_date: Mapped[date | None] = mapped_column(Date, nullable=True) max_managed_clients: Mapped[int] = mapped_column(Integer, nullable=False, default=25) max_user_accounts: Mapped[int] = mapped_column(Integer, nullable=False, default=1) allow_client_portal: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) allow_firm_referrals: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) allow_service_marketplace: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, index=True) remarks: Mapped[str | None] = mapped_column(Text, nullable=True) created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) updated_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) created_at_utc: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False ) updated_at_utc: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False, ) consultant = relationship("ConsultantProfile", back_populates="workspace") class ClientConsultantLink(CommonBase): """Explicit link between a firm client and a consultant portal profile.""" __tablename__ = "client_consultant_links" __table_args__ = ( UniqueConstraint("tenant_id", "client_id", "consultant_id", name="uq_client_consultant_links_client_consultant"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True) branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id"), nullable=True, index=True) client_id: Mapped[int] = mapped_column(ForeignKey("clients.id", ondelete="CASCADE"), nullable=False, index=True) consultant_id: Mapped[int] = mapped_column( ForeignKey("consultant_profiles.id", ondelete="CASCADE"), nullable=False, index=True ) service_catalogue_id: Mapped[int | None] = mapped_column(ForeignKey("service_catalogues.id", ondelete="SET NULL"), nullable=True, index=True) relationship_type: Mapped[str] = mapped_column(String(50), nullable=False, default="accounts_consultant", index=True) is_primary: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) can_view_client: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) can_view_services: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) can_view_due_dates: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) can_view_communications: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, index=True) remarks: Mapped[str | None] = mapped_column(Text, nullable=True) created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) updated_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) created_at_utc: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False ) updated_at_utc: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False, ) consultant = relationship("ConsultantProfile", back_populates="links") client = relationship("Client") service_catalogue = relationship("ServiceCatalogue") class ConsultantManagedClient(CommonBase): """Client/contact managed by a consultant inside the consultant portal. This table is intentionally separate from the firm `clients` master. It lets consultants maintain their own client book without affecting audit-firm client records. A later phase can convert/link a managed client to the firm client master through an approval workflow. """ __tablename__ = "consultant_managed_clients" __table_args__ = ( UniqueConstraint("tenant_id", "consultant_id", "client_code", name="uq_consultant_managed_clients_code"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True) branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id"), nullable=True, index=True) consultant_id: Mapped[int] = mapped_column( ForeignKey("consultant_profiles.id", ondelete="CASCADE"), nullable=False, index=True ) linked_firm_client_id: Mapped[int | None] = mapped_column(ForeignKey("clients.id", ondelete="SET NULL"), nullable=True, index=True) client_code: Mapped[str | None] = mapped_column(String(50), nullable=True, index=True) client_name: Mapped[str] = mapped_column(String(200), nullable=False, index=True) trade_name: Mapped[str | None] = mapped_column(String(200), nullable=True) client_type: Mapped[str] = mapped_column(String(100), nullable=False, default="Other") pan: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True) gstin: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True) tan: Mapped[str | None] = mapped_column(String(20), nullable=True) contact_person_name: Mapped[str | None] = mapped_column(String(200), nullable=True) mobile: Mapped[str | None] = mapped_column(String(20), nullable=True) email: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True) address_line_1: Mapped[str | None] = mapped_column(String(255), nullable=True) address_line_2: Mapped[str | None] = mapped_column(String(255), nullable=True) city: Mapped[str | None] = mapped_column(String(100), nullable=True) state: Mapped[str | None] = mapped_column(String(100), nullable=True) pincode: Mapped[str | None] = mapped_column(String(20), nullable=True) country: Mapped[str | None] = mapped_column(String(100), nullable=True, default="India") service_interest: Mapped[str | None] = mapped_column(Text, nullable=True) relationship_stage: Mapped[str] = mapped_column(String(30), nullable=False, default="managed", index=True) status: Mapped[str] = mapped_column(String(30), nullable=False, default="active", index=True) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, index=True) notes: Mapped[str | None] = mapped_column(Text, nullable=True) conversion_status: Mapped[str] = mapped_column(String(30), nullable=False, default="not_requested", index=True) conversion_requested_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) conversion_requested_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True) conversion_reviewed_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) conversion_reviewed_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True) conversion_notes: Mapped[str | None] = mapped_column(Text, nullable=True) conversion_firm_notes: Mapped[str | None] = mapped_column(Text, nullable=True) created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) updated_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) created_at_utc: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False ) updated_at_utc: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False, ) consultant = relationship("ConsultantProfile", back_populates="managed_clients") linked_firm_client = relationship("Client") class ConsultantServiceRequest(CommonBase): """Service request raised by a consultant to the audit firm. A request can relate either to a consultant-managed client or to an already linked firm client. The request itself does not create engagements; firm users review it first and decide whether to accept, reject, or keep it under review. """ __tablename__ = "consultant_service_requests" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True) branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id"), nullable=True, index=True) consultant_id: Mapped[int] = mapped_column( ForeignKey("consultant_profiles.id", ondelete="CASCADE"), nullable=False, index=True ) managed_client_id: Mapped[int | None] = mapped_column( ForeignKey("consultant_managed_clients.id", ondelete="SET NULL"), nullable=True, index=True ) firm_client_id: Mapped[int | None] = mapped_column(ForeignKey("clients.id", ondelete="SET NULL"), nullable=True, index=True) service_catalogue_id: Mapped[int | None] = mapped_column( ForeignKey("service_catalogues.id", ondelete="SET NULL"), nullable=True, index=True ) request_no: Mapped[str] = mapped_column(String(50), nullable=False, index=True) request_type: Mapped[str] = mapped_column(String(50), nullable=False, default="service_request", index=True) status: Mapped[str] = mapped_column(String(40), nullable=False, default="submitted", index=True) priority: Mapped[str] = mapped_column(String(30), nullable=False, default="normal", index=True) requested_service_name: Mapped[str] = mapped_column(String(200), nullable=False) requested_due_date: Mapped[date | None] = mapped_column(Date, nullable=True) subject: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) consultant_notes: Mapped[str | None] = mapped_column(Text, nullable=True) firm_response: Mapped[str | None] = mapped_column(Text, nullable=True) created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True) updated_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True) reviewed_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True) reviewed_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at_utc: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False ) updated_at_utc: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False, ) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, index=True) consultant = relationship("ConsultantProfile", back_populates="service_requests") managed_client = relationship("ConsultantManagedClient") firm_client = relationship("Client") service_catalogue = relationship("ServiceCatalogue") created_by = relationship("User", foreign_keys=[created_by_user_id]) reviewed_by = relationship("User", foreign_keys=[reviewed_by_user_id])