from __future__ import annotations from datetime import datetime, timezone from decimal import Decimal, ROUND_HALF_UP from sqlalchemy import func, or_, select from app.modules.clients.models import Client from app.modules.consultants.models import ConsultantProfile from app.modules.core.iam.models import User from app.modules.core.tenancy.models import Branch, Tenant from app.modules.marketplace.models import MarketplaceLead, MarketplaceLeadAssignment LEAD_STATUSES = ["NEW", "CONTACTED", "QUALIFIED", "ASSIGNED", "ACCEPTED", "REJECTED", "CONVERTED", "LOST"] LEAD_PRIORITIES = ["LOW", "NORMAL", "HIGH", "URGENT"] LEAD_SOURCES = ["manual", "public_website", "consultant", "referral", "campaign", "other"] SERVICE_CATEGORIES = ["GST", "Income Tax", "ROC", "Audit", "Accounting", "Payroll", "Registration", "Advisory", "Other"] def money(value) -> Decimal: if value in (None, ""): return Decimal("0.00") return Decimal(str(value)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) def _role_set(role_names: list[str] | None) -> set[str]: return set(role_names or []) def is_system_admin(role_names: list[str] | None) -> bool: return "System Admin" in _role_set(role_names) def is_firm_admin(role_names: list[str] | None) -> bool: return "Firm Admin" in _role_set(role_names) def is_partner(role_names: list[str] | None) -> bool: return "Partner" in _role_set(role_names) def generate_lead_no(db) -> str: year = datetime.now().year count = db.execute(select(func.count(MarketplaceLead.id))).scalar_one() or 0 return f"ML-{year}-{count + 1:05d}" def list_reference_audit_firms(db) -> list[Tenant]: return list(db.execute(select(Tenant).order_by(Tenant.name.asc())).scalars().all()) def list_reference_branches(db, tenant_id: int | None = None) -> list[Branch]: stmt = select(Branch).order_by(Branch.name.asc()) if tenant_id: stmt = stmt.where(Branch.tenant_id == tenant_id) return list(db.execute(stmt).scalars().all()) def list_reference_partners(db, tenant_id: int | None = None) -> list[User]: stmt = select(User).order_by(User.full_name.asc(), User.email.asc()) if tenant_id: stmt = stmt.where(User.tenant_id == tenant_id) return list(db.execute(stmt).scalars().all()) def list_marketplace_leads(db, *, q: str = "", status: str = "", user=None, role_names: list[str] | None = None) -> list[MarketplaceLead]: stmt = select(MarketplaceLead).order_by(MarketplaceLead.created_at_utc.desc(), MarketplaceLead.id.desc()) roles = _role_set(role_names) if not is_system_admin(role_names): tenant_id = getattr(user, "tenant_id", None) if tenant_id: stmt = stmt.where(MarketplaceLead.assigned_tenant_id == tenant_id) if is_partner(role_names): stmt = stmt.where(MarketplaceLead.assigned_partner_user_id == getattr(user, "id", None)) if status: stmt = stmt.where(MarketplaceLead.status == status) if q: like = f"%{q}%" stmt = stmt.where(or_(MarketplaceLead.lead_no.ilike(like), MarketplaceLead.lead_name.ilike(like), MarketplaceLead.business_name.ilike(like), MarketplaceLead.mobile.ilike(like), MarketplaceLead.email.ilike(like), MarketplaceLead.service_requested.ilike(like))) return list(db.execute(stmt).scalars().all()) def get_marketplace_lead(db, lead_id: int) -> MarketplaceLead | None: return db.get(MarketplaceLead, int(lead_id)) def create_marketplace_lead(db, *, lead_name: str, business_name: str | None, email: str | None, mobile: str | None, city: str | None, state: str | None, service_category: str | None, service_requested: str, message: str | None, source: str = "manual", priority: str = "NORMAL", estimated_value=0, created_by_user_id: int | None = None) -> MarketplaceLead: lead = MarketplaceLead( lead_no=generate_lead_no(db), source=source or "manual", service_category=service_category or None, service_requested=(service_requested or "General enquiry").strip(), lead_name=lead_name.strip(), business_name=(business_name or None), email=(email or None), mobile=(mobile or None), city=(city or None), state=(state or None), message=(message or None), priority=priority if priority in LEAD_PRIORITIES else "NORMAL", estimated_value=money(estimated_value), created_by_user_id=created_by_user_id, ) db.add(lead) db.commit() db.refresh(lead) return lead def assign_marketplace_lead(db, *, lead: MarketplaceLead, tenant_id: int, branch_id: int | None, partner_user_id: int | None, notes: str | None, assigned_by_user_id: int | None) -> MarketplaceLead: now = datetime.now(timezone.utc) lead.assigned_tenant_id = tenant_id lead.assigned_branch_id = branch_id or None lead.assigned_partner_user_id = partner_user_id or None lead.assigned_by_user_id = assigned_by_user_id lead.assigned_at_utc = now lead.status = "ASSIGNED" lead.updated_by_user_id = assigned_by_user_id db.add(MarketplaceLeadAssignment(lead_id=lead.id, tenant_id=tenant_id, branch_id=branch_id or None, partner_user_id=partner_user_id or None, notes=notes or None, assigned_by_user_id=assigned_by_user_id, assigned_at_utc=now)) db.commit() db.refresh(lead) return lead def update_lead_status(db, *, lead: MarketplaceLead, status: str, user_id: int | None) -> MarketplaceLead: if status not in LEAD_STATUSES: raise ValueError("Invalid lead status") lead.status = status lead.updated_by_user_id = user_id db.commit() db.refresh(lead) return lead def convert_lead_to_client(db, *, lead: MarketplaceLead, tenant_id: int, branch_id: int, partner_user_id: int | None, client_code: str | None, user_id: int | None) -> Client: if lead.converted_client_id: existing = db.get(Client, lead.converted_client_id) if existing: return existing code = (client_code or f"LEAD-{lead.id:05d}").strip().upper() name = (lead.business_name or lead.lead_name).strip() client = Client( tenant_id=tenant_id, branch_id=branch_id, partner_id=partner_user_id or None, engagement_mode="internal_managed", client_code=code, client_name=name, trade_name=lead.business_name or None, client_type="Other", contact_person_name=lead.lead_name, mobile=lead.mobile, email=lead.email, city=lead.city, state=lead.state, status="active", is_active=True, is_archived=False, notes=f"Converted from marketplace lead {lead.lead_no}.\n\n{lead.message or ''}".strip(), created_by_user_id=user_id, updated_by_user_id=user_id, ) db.add(client) db.flush() lead.converted_client_id = client.id lead.converted_by_user_id = user_id lead.converted_at_utc = datetime.now(timezone.utc) lead.status = "CONVERTED" lead.updated_by_user_id = user_id db.commit() db.refresh(client) return client def get_marketplace_overview_counts(db) -> dict[str, int]: """Return lightweight public marketplace counts. This is intentionally read-only and tolerant of partially configured data so the public marketplace page never breaks the ERP login/runtime flow. """ try: firm_count = db.execute(select(func.count(Tenant.id)).where(Tenant.is_active.is_(True))).scalar_one() or 0 except Exception: firm_count = 0 try: consultant_count = db.execute(select(func.count(ConsultantProfile.id)).where(ConsultantProfile.is_active.is_(True))).scalar_one() or 0 except Exception: consultant_count = 0 try: lead_count = db.execute(select(func.count(MarketplaceLead.id))).scalar_one() or 0 except Exception: lead_count = 0 return {"audit_firms": int(firm_count), "consultants": int(consultant_count), "service_requests": int(lead_count)} def list_public_marketplace_audit_firms(db, limit: int = 12) -> list[Tenant]: """List active audit firms for public marketplace display. More advanced publication controls can be added later. For Phase 7T.4 this uses only active tenants and does not expose private firm records beyond basic name/type. """ try: stmt = select(Tenant).where(Tenant.is_active.is_(True)).order_by(Tenant.name.asc()).limit(int(limit or 12)) return list(db.execute(stmt).scalars().all()) except Exception: return [] def list_public_marketplace_consultants(db, limit: int = 12) -> list[ConsultantProfile]: """List active consultants for public marketplace display.""" try: stmt = ( select(ConsultantProfile) .where(ConsultantProfile.is_active.is_(True)) .order_by(ConsultantProfile.contact_person.asc(), ConsultantProfile.firm_name.asc()) .limit(int(limit or 12)) ) return list(db.execute(stmt).scalars().all()) except Exception: return [] def is_marketplace_domain_request(request) -> bool: """True when Phase 7T.2 resolved the host as a marketplace domain.""" try: return getattr(request.state, "domain_type", None) == "marketplace" except Exception: return False