Add phase 1 client consultant referral and linkage

This commit is contained in:
A R R R Associates
2026-07-22 23:45:05 +05:30
parent 02a549bd05
commit 8f51d16c7d
10 changed files with 290 additions and 5 deletions
@@ -0,0 +1,53 @@
"""Phase 1 client-consultant referral and communication linkage.
Revision ID: 20260722_phase1_client_consultant_linkage
Revises: 20260720_document_vps_auto_cleanup
"""
from alembic import op
import sqlalchemy as sa
revision = "20260722_phase1_client_consultant_linkage"
down_revision = "20260720_document_vps_auto_cleanup"
branch_labels = None
depends_on = None
def upgrade() -> None:
with op.batch_alter_table("clients") as batch:
batch.add_column(sa.Column("referred_by_consultant_id", sa.Integer(), nullable=True))
batch.add_column(sa.Column("referral_date", sa.Date(), nullable=True))
batch.add_column(sa.Column("referral_reference", sa.String(length=200), nullable=True))
batch.add_column(sa.Column("referral_status", sa.String(length=30), nullable=False, server_default="active"))
batch.add_column(sa.Column("communication_routing_mode", sa.String(length=30), nullable=False, server_default="client_and_consultant"))
batch.create_foreign_key("fk_clients_referred_by_consultant", "consultant_profiles", ["referred_by_consultant_id"], ["id"], ondelete="SET NULL")
batch.create_index("ix_clients_referred_by_consultant_id", ["referred_by_consultant_id"], unique=False)
batch.create_index("ix_clients_referral_status", ["referral_status"], unique=False)
batch.create_index("ix_clients_communication_routing_mode", ["communication_routing_mode"], unique=False)
additions = [
("can_view_engagements", True), ("can_view_task_status", True), ("can_view_assignee", True),
("can_view_document_requests", True), ("can_upload_documents", True),
("can_reply_to_clarifications", True), ("can_view_filing_details", True),
("can_view_final_documents", True), ("can_view_permanent_documents", False),
("can_receive_notifications", True), ("can_act_for_client", True),
]
with op.batch_alter_table("client_consultant_links") as batch:
for name, default in additions:
batch.add_column(sa.Column(name, sa.Boolean(), nullable=False, server_default=sa.true() if default else sa.false()))
batch.add_column(sa.Column("effective_from", sa.Date(), nullable=True))
batch.add_column(sa.Column("effective_to", sa.Date(), nullable=True))
def downgrade() -> None:
with op.batch_alter_table("client_consultant_links") as batch:
batch.drop_column("effective_to")
batch.drop_column("effective_from")
for name in ["can_act_for_client", "can_receive_notifications", "can_view_permanent_documents", "can_view_final_documents", "can_view_filing_details", "can_reply_to_clarifications", "can_upload_documents", "can_view_document_requests", "can_view_assignee", "can_view_task_status", "can_view_engagements"]:
batch.drop_column(name)
with op.batch_alter_table("clients") as batch:
batch.drop_index("ix_clients_communication_routing_mode")
batch.drop_index("ix_clients_referral_status")
batch.drop_index("ix_clients_referred_by_consultant_id")
batch.drop_constraint("fk_clients_referred_by_consultant", type_="foreignkey")
batch.drop_column("communication_routing_mode")
batch.drop_column("referral_status")
batch.drop_column("referral_reference")
batch.drop_column("referral_date")
batch.drop_column("referred_by_consultant_id")
+27
View File
@@ -12,11 +12,18 @@ from sqlalchemy.orm import Session
from app.modules.clients import repository
from app.modules.clients.schemas import ClientCreate
from app.modules.clients.service import create_client_service
from app.modules.consultants.service import get_consultant
TEMPLATE_COLUMNS = [
"uploader_user_id",
"firm_tenant_id",
"partner_user_id",
"referred_by_consultant_id",
"primary_consultant_id",
"referral_date",
"referral_reference",
"referral_status",
"communication_routing_mode",
"branch_id",
"client_code",
"client_name",
@@ -104,6 +111,9 @@ def build_client_import_template_bytes(*, current_user, tenant_id: int, partner_
ref.append(['firm_tenant_id', 'Must match the active firm/tenant context of the upload.'])
ref.append(['partner_user_id', 'Must be an active Partner user mapped to the same firm.'])
ref.append(['branch_id', 'Optional. If blank, uploader branch or partner branch will be used.'])
ref.append(['referred_by_consultant_id', 'Optional active consultant profile id who introduced the client.'])
ref.append(['primary_consultant_id', 'Optional active consultant profile id for the operational client link.'])
ref.append(['communication_routing_mode', 'client_direct, consultant_primary, client_and_consultant, or firm_only.'])
ref.append(['email', 'Used as the client frontend login email.'])
ref.append(['portal_password', 'Minimum 8 characters.'])
ref.append(['portal_password_confirm', 'Must match portal_password.'])
@@ -174,10 +184,27 @@ def build_preview(db: Session, *, current_user, scope, role_names: set[str], upl
if not branch_id:
msgs.append('branch_id is required when uploader and partner have no branch mapped.')
for consultant_field in ("referred_by_consultant_id", "primary_consultant_id"):
raw_consultant_id = cleaned.get(consultant_field)
if raw_consultant_id:
try:
consultant_id = int(raw_consultant_id)
except Exception:
consultant_id = 0
consultant = get_consultant(db, tenant_id=firm_tenant_id, consultant_id=consultant_id) if consultant_id else None
if not consultant or not consultant.is_active:
msgs.append(f"{consultant_field} must be an active consultant profile id in the same firm.")
payload = {
'tenant_id': firm_tenant_id,
'branch_id': branch_id,
'partner_id': partner_user_id or None,
'referred_by_consultant_id': int(cleaned.get('referred_by_consultant_id')) if cleaned.get('referred_by_consultant_id') else None,
'primary_consultant_id': int(cleaned.get('primary_consultant_id')) if cleaned.get('primary_consultant_id') else None,
'referral_date': cleaned.get('referral_date'),
'referral_reference': cleaned.get('referral_reference'),
'referral_status': cleaned.get('referral_status') or 'active',
'communication_routing_mode': cleaned.get('communication_routing_mode') or 'client_and_consultant',
'engagement_mode': cleaned.get('engagement_mode') or 'internal_managed',
'client_code': cleaned.get('client_code') or '',
'client_name': cleaned.get('client_name') or '',
+5
View File
@@ -21,6 +21,11 @@ class Client(CommonBase):
branch_id: Mapped[int] = mapped_column(ForeignKey("branches.id"), nullable=False, index=True)
partner_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
default_review_partner_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
referred_by_consultant_id: Mapped[int | None] = mapped_column(ForeignKey("consultant_profiles.id", ondelete="SET NULL"), nullable=True, index=True)
referral_date: Mapped[date | None] = mapped_column(Date, nullable=True)
referral_reference: Mapped[str | None] = mapped_column(String(200), nullable=True)
referral_status: Mapped[str] = mapped_column(String(30), nullable=False, default="active", index=True)
communication_routing_mode: Mapped[str] = mapped_column(String(30), nullable=False, default="client_and_consultant", index=True)
engagement_mode: Mapped[str] = mapped_column(String(30), nullable=False, default="internal_managed", index=True)
client_code: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
client_name: Mapped[str] = mapped_column(String(200), nullable=False, index=True)
+53 -2
View File
@@ -22,6 +22,12 @@ class ClientBase(BaseModel):
branch_id: int
partner_id: Optional[int] = None
default_review_partner_user_id: Optional[int] = None
referred_by_consultant_id: Optional[int] = None
primary_consultant_id: Optional[int] = None
referral_date: Optional[date] = None
referral_reference: Optional[str] = None
referral_status: str = "active"
communication_routing_mode: str = "client_and_consultant"
engagement_mode: str = "internal_managed"
client_code: str
client_name: str
@@ -82,7 +88,7 @@ class ClientBase(BaseModel):
@field_validator(
"trade_name", "cin_llpin", "msme_no", "iec_code", "contact_person_name", "contact_person_designation",
"address_line_1", "address_line_2", "city", "state", "country", "client_category", "risk_category", "acceptance_review_notes", "acceptance_rejection_reason", "notes",
"address_line_1", "address_line_2", "city", "state", "country", "client_category", "risk_category", "referral_reference", "acceptance_review_notes", "acceptance_rejection_reason", "notes",
mode="before",
)
@classmethod
@@ -94,6 +100,25 @@ class ClientBase(BaseModel):
def uppercase_codes(cls, value):
return normalize_upper(value)
@field_validator("referral_status", "communication_routing_mode", mode="before")
@classmethod
def clean_referral_fields(cls, value):
return (normalize_text(value) or "").lower()
@field_validator("referral_status")
@classmethod
def validate_referral_status(cls, value):
if value not in {"active", "inactive", "ended"}:
raise ValueError("Invalid referral status.")
return value
@field_validator("communication_routing_mode")
@classmethod
def validate_communication_routing_mode(cls, value):
if value not in {"client_direct", "consultant_primary", "client_and_consultant", "firm_only"}:
raise ValueError("Invalid communication routing mode.")
return value
@field_validator("engagement_mode", mode="before")
@classmethod
def clean_engagement_mode(cls, value):
@@ -189,6 +214,12 @@ class ClientUpdate(BaseModel):
branch_id: Optional[int] = None
partner_id: Optional[int] = None
default_review_partner_user_id: Optional[int] = None
referred_by_consultant_id: Optional[int] = None
primary_consultant_id: Optional[int] = None
referral_date: Optional[date] = None
referral_reference: Optional[str] = None
referral_status: Optional[str] = None
communication_routing_mode: Optional[str] = None
engagement_mode: Optional[str] = None
client_name: Optional[str] = None
trade_name: Optional[str] = None
@@ -241,7 +272,7 @@ class ClientUpdate(BaseModel):
@field_validator(
"client_name", "trade_name", "cin_llpin", "msme_no", "iec_code", "contact_person_name", "contact_person_designation",
"address_line_1", "address_line_2", "city", "state", "country", "client_category", "risk_category", "acceptance_review_notes", "acceptance_rejection_reason", "notes",
"address_line_1", "address_line_2", "city", "state", "country", "client_category", "risk_category", "referral_reference", "acceptance_review_notes", "acceptance_rejection_reason", "notes",
mode="before",
)
@classmethod
@@ -253,6 +284,26 @@ class ClientUpdate(BaseModel):
def uppercase_codes(cls, value):
return normalize_upper(value)
@field_validator("referral_status", "communication_routing_mode", mode="before")
@classmethod
def clean_optional_referral_fields(cls, value):
value = normalize_text(value)
return value.lower() if value else None
@field_validator("referral_status")
@classmethod
def validate_optional_referral_status(cls, value):
if value is not None and value not in {"active", "inactive", "ended"}:
raise ValueError("Invalid referral status.")
return value
@field_validator("communication_routing_mode")
@classmethod
def validate_optional_communication_routing_mode(cls, value):
if value is not None and value not in {"client_direct", "consultant_primary", "client_and_consultant", "firm_only"}:
raise ValueError("Invalid communication routing mode.")
return value
@field_validator("engagement_mode", mode="before")
@classmethod
def clean_engagement_mode(cls, value):
+7 -1
View File
@@ -29,11 +29,15 @@ from app.modules.alerts.service import create_alert
from app.modules.core.iam.models import User
from app.modules.core.rbac.models import Role, UserRole
from app.modules.documents.models import PermanentClientDocument
from app.modules.consultants.service import sync_primary_client_consultant_link
def _payload_from_schema(data):
return data.model_dump(exclude_none=True) if hasattr(data, "model_dump") else data.dict(exclude_none=True)
payload = data.model_dump(exclude_none=True) if hasattr(data, "model_dump") else data.dict(exclude_none=True)
# primary_consultant_id belongs to ClientConsultantLink, not the clients table.
payload.pop("primary_consultant_id", None)
return payload
@@ -269,6 +273,7 @@ def create_client_service(db, *, data, actor_user_id: int, scope, current_user_r
_enforce_client_acceptance_controls(payload)
row = repository.create_client(db, payload)
row = _sync_client_portal_user(db, row=row, portal_password=portal_password, portal_password_confirm=portal_password_confirm)
sync_primary_client_consultant_link(db, client=row, consultant_id=getattr(data, "primary_consultant_id", None), actor_user_id=actor_user_id)
_write_association_from_client(db, row, actor_user_id=actor_user_id, current_user_roles=current_user_roles)
repository.write_audit_log(
@@ -309,6 +314,7 @@ def update_client_service(db, *, row, data, actor_user_id: int, scope, current_u
updated = repository.update_client(db, row, payload)
updated = _sync_client_portal_user(db, row=updated, portal_password=portal_password, portal_password_confirm=portal_password_confirm)
sync_primary_client_consultant_link(db, client=updated, consultant_id=getattr(data, "primary_consultant_id", None), actor_user_id=actor_user_id)
_write_association_from_client(db, updated, actor_user_id=actor_user_id, current_user_roles=current_user_roles)
repository.write_audit_log(
@@ -116,7 +116,10 @@
<div><span class="font-medium">Branch:</span> {{ row.branch_id or '-' }}</div>
<div><span class="font-medium">Partner:</span> {{ row.assoc_partner_user_id or row.partner_id or '-' }}</div>
<div><span class="font-medium">Default Review Partner:</span> {{ row.default_review_partner_user_id or '-' }}</div>
<div><span class="font-medium">Consultant:</span> {{ row.assoc_consultant_id or '-' }}</div>
<div><span class="font-medium">Referred by:</span> {{ consultant_summary.referred_by.contact_person if consultant_summary and consultant_summary.referred_by else 'Direct / Not recorded' }}</div>
<div><span class="font-medium">Primary consultant:</span> {{ consultant_summary.primary.contact_person if consultant_summary and consultant_summary.primary else 'Firm managed' }}</div>
<div><span class="font-medium">Communication:</span> {{ (row.communication_routing_mode or 'client_and_consultant')|replace('_', ' ')|title }}</div>
<div><span class="font-medium">Referral status:</span> {{ (row.referral_status or 'active')|title }}</div>
</div>
</section>
</div>
@@ -255,6 +255,49 @@
<div class="md:col-span-2 border-t border-slate-200 pt-4">
<h4 class="text-sm font-semibold text-slate-900">Consultant Referral & Communication</h4>
<p class="mt-1 text-xs text-slate-500">Referral records who introduced the client. Primary consultant controls the operational client link.</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Referred By Consultant</label>
<select name="referred_by_consultant_id" class="mt-1 w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">
<option value="">-- Direct / No consultant --</option>
{% for c in form_options.consultants or [] %}
<option value="{{ c.id }}" {% if (form_data.referred_by_consultant_id or (row.referred_by_consultant_id if is_edit else None)) == c.id %}selected{% endif %}>{{ c.contact_person }}{% if c.firm_name %} — {{ c.firm_name }}{% endif %}</option>
{% endfor %}
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Primary Consultant</label>
<select name="primary_consultant_id" class="mt-1 w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">
<option value="">-- Firm managed only --</option>
{% for c in form_options.consultants or [] %}
<option value="{{ c.id }}" {% if form_data.primary_consultant_id == c.id %}selected{% endif %}>{{ c.contact_person }}{% if c.firm_name %} — {{ c.firm_name }}{% endif %}</option>
{% endfor %}
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Referral Date</label>
<input type="date" name="referral_date" value="{{ form_data.referral_date or (row.referral_date if is_edit else '') }}" class="mt-1 w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Referral Status</label>
<select name="referral_status" class="mt-1 w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">
{% for value, label in [('active','Active'),('inactive','Inactive'),('ended','Ended')] %}<option value="{{ value }}" {% if (form_data.referral_status or (row.referral_status if is_edit else 'active')) == value %}selected{% endif %}>{{ label }}</option>{% endfor %}
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Communication Routing</label>
<select name="communication_routing_mode" class="mt-1 w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">
{% for value, label in [('client_direct','Client Direct'),('consultant_primary','Primary Consultant'),('client_and_consultant','Client and Consultant'),('firm_only','Firm Only')] %}<option value="{{ value }}" {% if (form_data.communication_routing_mode or (row.communication_routing_mode if is_edit else 'client_and_consultant')) == value %}selected{% endif %}>{{ label }}</option>{% endfor %}
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Referral Reference</label>
<input name="referral_reference" value="{{ form_data.referral_reference or (row.referral_reference if is_edit else '') }}" class="mt-1 w-full rounded-xl border border-slate-300 px-4 py-2 text-sm" placeholder="Source, campaign, agreement or note">
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Default Review Partner</label>
<select name="default_review_partner_user_id" class="mt-1 w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">
+13 -1
View File
@@ -76,6 +76,7 @@ from app.modules.billing.client_portal_service import (
from app.modules.billing.services import build_invoice_print_context, create_cashfree_transaction, create_payumoney_transaction, process_cashfree_return, process_cashfree_webhook, process_payumoney_response
from app.modules.email_integration.services import send_auth_otp_email
from app.modules.documents.models import PermanentClientDocument
from app.modules.consultants.service import list_consultants, get_primary_client_consultant_link, get_client_consultant_summary
from app.modules.documents.services import (
get_permanent_version,
get_version,
@@ -173,6 +174,12 @@ def _build_form_payload(request: Request, user, scope, *, include_client_code: b
"branch_id": branch_id,
"partner_id": partner_id,
"default_review_partner_user_id": int(form.get("default_review_partner_user_id")) if form.get("default_review_partner_user_id") not in (None, "", "None") else None,
"referred_by_consultant_id": int(form.get("referred_by_consultant_id")) if form.get("referred_by_consultant_id") not in (None, "", "None") else None,
"primary_consultant_id": int(form.get("primary_consultant_id")) if form.get("primary_consultant_id") not in (None, "", "None") else None,
"referral_date": form.get("referral_date") or None,
"referral_reference": form.get("referral_reference"),
"referral_status": form.get("referral_status") or "active",
"communication_routing_mode": form.get("communication_routing_mode") or "client_and_consultant",
"engagement_mode": form.get("engagement_mode") or "internal_managed",
"client_name": form.get("client_name", ""),
"trade_name": form.get("trade_name"),
@@ -252,6 +259,7 @@ def _form_options(db, scope, form_mode: str):
"review_partners": repository.list_all_partners(db),
"active_tenant_id": tenant_id,
"active_branch_id": branch_id,
"consultants": list_consultants(db, tenant_id=tenant_id, include_inactive=False) if tenant_id else [],
}
return {
@@ -262,6 +270,7 @@ def _form_options(db, scope, form_mode: str):
tenant_id=tenant_id,
branch_id=None if scope.allow_cross_branch else branch_id,
),
"consultants": list_consultants(db, tenant_id=tenant_id, branch_id=branch_id, include_inactive=False),
"review_partners": repository.list_partners_for_scope(
db,
tenant_id=tenant_id,
@@ -572,6 +581,8 @@ def client_new_page(request: Request):
"acceptance_status": "pending_review",
"acceptance_required": True,
"engagement_letter_required": True,
"referral_status": "active",
"communication_routing_mode": "client_and_consultant",
}
return _render(
@@ -685,6 +696,7 @@ def client_detail(request: Request, client_id: int):
can_activate=has("clients.activate"),
can_archive=has("clients.archive"),
can_restore=has("clients.restore"),
consultant_summary=get_client_consultant_summary(db, tenant_id=int(row["tenant_id"]), client_id=client_id),
can_manage_acceptance=has("clients.acceptance.manage"),
can_approve_acceptance=has("clients.acceptance.approve"),
)
@@ -725,7 +737,7 @@ def client_edit_page(request: Request, client_id: int):
user,
title=f"Edit Client • {row.client_name}",
row=row,
form_data=row,
form_data={**row.__dict__, "primary_consultant_id": getattr(get_primary_client_consultant_link(db, tenant_id=row.tenant_id, client_id=row.id), "consultant_id", None)},
form_errors=[],
scope=scope,
form_options=_form_options(db, scope, form_mode),
+13
View File
@@ -162,6 +162,19 @@ class ClientConsultantLink(CommonBase):
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)
can_view_engagements: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
can_view_task_status: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
can_view_assignee: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
can_view_document_requests: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
can_upload_documents: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
can_reply_to_clarifications: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
can_view_filing_details: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
can_view_final_documents: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
can_view_permanent_documents: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
can_receive_notifications: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
can_act_for_client: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
effective_from: Mapped[date | None] = mapped_column(Date, nullable=True)
effective_to: Mapped[date | None] = mapped_column(Date, nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, index=True)
remarks: Mapped[str | None] = mapped_column(Text, nullable=True)
+72
View File
@@ -1388,3 +1388,75 @@ def reject_managed_client_conversion(
managed_client.conversion_firm_notes = normalise_text(firm_notes)
managed_client.updated_by_user_id = user_id
return managed_client
def get_primary_client_consultant_link(db: Session, *, tenant_id: int, client_id: int) -> ClientConsultantLink | None:
return db.execute(
select(ClientConsultantLink)
.where(
ClientConsultantLink.tenant_id == tenant_id,
ClientConsultantLink.client_id == client_id,
ClientConsultantLink.is_primary.is_(True),
ClientConsultantLink.is_active.is_(True),
)
.order_by(ClientConsultantLink.id.asc())
).scalars().first()
def sync_primary_client_consultant_link(
db: Session, *, client: Client, consultant_id: int | None, actor_user_id: int
) -> ClientConsultantLink | None:
"""Synchronise the client's unrestricted primary consultant link without deleting history."""
links = db.execute(
select(ClientConsultantLink).where(
ClientConsultantLink.tenant_id == client.tenant_id,
ClientConsultantLink.client_id == client.id,
)
).scalars().all()
selected = None
for link in links:
if consultant_id and int(link.consultant_id) == int(consultant_id) and link.service_catalogue_id is None:
selected = link
elif link.is_primary:
link.is_primary = False
link.updated_by_user_id = actor_user_id
if consultant_id is None:
db.flush()
return None
consultant = get_consultant(db, tenant_id=int(client.tenant_id), consultant_id=int(consultant_id))
if not consultant or not consultant.is_active:
raise ValueError("Selected primary consultant is not active in this firm.")
if selected is None:
selected = ClientConsultantLink(
tenant_id=client.tenant_id, branch_id=client.branch_id, client_id=client.id,
consultant_id=consultant.id, service_catalogue_id=None,
relationship_type="accounts_consultant", is_primary=True, is_active=True,
can_view_client=True, can_view_services=True, can_view_due_dates=True,
can_view_communications=True, can_view_engagements=True, can_view_task_status=True,
can_view_assignee=True, can_view_document_requests=True, can_upload_documents=True,
can_reply_to_clarifications=True, can_view_filing_details=True,
can_view_final_documents=True, can_view_permanent_documents=False,
can_receive_notifications=True, can_act_for_client=True,
effective_from=getattr(client, "referral_date", None) or date.today(),
created_by_user_id=actor_user_id, updated_by_user_id=actor_user_id,
)
db.add(selected)
else:
selected.branch_id = client.branch_id
selected.is_primary = True
selected.is_active = True
selected.updated_by_user_id = actor_user_id
db.flush()
return selected
def get_client_consultant_summary(db: Session, *, tenant_id: int, client_id: int) -> dict:
primary = get_primary_client_consultant_link(db, tenant_id=tenant_id, client_id=client_id)
referred_id = db.execute(select(Client.referred_by_consultant_id).where(Client.id == client_id)).scalar_one_or_none()
ids = {int(x) for x in (getattr(primary, "consultant_id", None), referred_id) if x}
profiles = db.execute(select(ConsultantProfile).where(ConsultantProfile.id.in_(ids))).scalars().all() if ids else []
by_id = {int(x.id): x for x in profiles}
return {
"primary": by_id.get(int(primary.consultant_id)) if primary else None,
"referred_by": by_id.get(int(referred_id)) if referred_id else None,
}