Add Phase 3 client business intelligence profile
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timezone
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.clients.models import Client, ClientBusinessProfile, ClientBusinessProfileSource
|
||||
|
||||
BUSINESS_MODEL_OPTIONS = (
|
||||
"manufacturing",
|
||||
"trading",
|
||||
"services",
|
||||
"manufacturing_and_trading",
|
||||
"contracting",
|
||||
"construction",
|
||||
"professional",
|
||||
"transport_logistics",
|
||||
"hospitality",
|
||||
"mixed",
|
||||
"other",
|
||||
)
|
||||
SOURCE_TYPE_OPTIONS = ("manual_user", "gst", "income_tax", "tally", "system_inference", "ai_inference", "other")
|
||||
|
||||
|
||||
def get_profile(db: Session, client_id: int) -> ClientBusinessProfile | None:
|
||||
return db.execute(select(ClientBusinessProfile).where(ClientBusinessProfile.client_id == int(client_id))).scalar_one_or_none()
|
||||
|
||||
|
||||
def ensure_profile(db: Session, client: Client, actor_user_id: int | None = None) -> ClientBusinessProfile:
|
||||
row = get_profile(db, client.id)
|
||||
if row:
|
||||
return row
|
||||
row = ClientBusinessProfile(
|
||||
client_id=client.id,
|
||||
tenant_id=client.tenant_id,
|
||||
branch_id=client.branch_id,
|
||||
created_by_user_id=actor_user_id,
|
||||
updated_by_user_id=actor_user_id,
|
||||
)
|
||||
db.add(row)
|
||||
db.flush()
|
||||
return row
|
||||
|
||||
|
||||
def list_sources(db: Session, client_id: int) -> list[ClientBusinessProfileSource]:
|
||||
return list(db.execute(
|
||||
select(ClientBusinessProfileSource)
|
||||
.where(ClientBusinessProfileSource.client_id == int(client_id))
|
||||
.order_by(ClientBusinessProfileSource.created_at_utc.desc(), ClientBusinessProfileSource.id.desc())
|
||||
).scalars().all())
|
||||
|
||||
|
||||
def _optional_bool(value: str | None) -> bool | None:
|
||||
if value in (None, "", "unknown"):
|
||||
return None
|
||||
return str(value).lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def update_profile(db: Session, *, client: Client, actor_user_id: int, form) -> ClientBusinessProfile:
|
||||
row = ensure_profile(db, client, actor_user_id)
|
||||
row.primary_industry = (form.get("primary_industry") or "").strip() or None
|
||||
row.primary_business_activity = (form.get("primary_business_activity") or "").strip() or None
|
||||
row.secondary_business_activities = (form.get("secondary_business_activities") or "").strip() or None
|
||||
business_model = (form.get("business_model") or "").strip()
|
||||
row.business_model = business_model if business_model in BUSINESS_MODEL_OPTIONS else None
|
||||
row.main_products = (form.get("main_products") or "").strip() or None
|
||||
row.main_services = (form.get("main_services") or "").strip() or None
|
||||
row.inventory_maintained = _optional_bool(form.get("inventory_maintained"))
|
||||
row.project_job_based = _optional_bool(form.get("project_job_based"))
|
||||
row.capital_intensive = _optional_bool(form.get("capital_intensive"))
|
||||
row.vehicle_intensive = _optional_bool(form.get("vehicle_intensive"))
|
||||
row.notes = (form.get("notes") or "").strip() or None
|
||||
row.updated_by_user_id = actor_user_id
|
||||
# Editing a confirmed profile returns it to review so automated enrichment can
|
||||
# never silently remain marked confirmed after a human changes the facts.
|
||||
if row.profile_status == "confirmed":
|
||||
row.profile_status = "draft"
|
||||
row.confirmed_by_user_id = None
|
||||
row.confirmed_at_utc = None
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return row
|
||||
|
||||
|
||||
def confirm_profile(db: Session, *, client: Client, actor_user_id: int) -> ClientBusinessProfile:
|
||||
row = ensure_profile(db, client, actor_user_id)
|
||||
row.profile_status = "confirmed"
|
||||
row.confidence_score = 100
|
||||
row.confirmed_by_user_id = actor_user_id
|
||||
row.confirmed_at_utc = datetime.now(timezone.utc)
|
||||
row.updated_by_user_id = actor_user_id
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return row
|
||||
|
||||
|
||||
def add_source(db: Session, *, client: Client, actor_user_id: int, form) -> ClientBusinessProfileSource:
|
||||
profile = ensure_profile(db, client, actor_user_id)
|
||||
source_type = (form.get("source_type") or "manual_user").strip()
|
||||
if source_type not in SOURCE_TYPE_OPTIONS:
|
||||
source_type = "other"
|
||||
raw_confidence = (form.get("source_confidence") or "50").strip()
|
||||
try:
|
||||
confidence = max(0, min(100, int(raw_confidence)))
|
||||
except ValueError:
|
||||
confidence = 50
|
||||
observed_raw = (form.get("observed_on") or "").strip()
|
||||
observed_on = None
|
||||
if observed_raw:
|
||||
try:
|
||||
observed_on = date.fromisoformat(observed_raw)
|
||||
except ValueError:
|
||||
observed_on = None
|
||||
row = ClientBusinessProfileSource(
|
||||
profile_id=profile.id,
|
||||
client_id=client.id,
|
||||
tenant_id=client.tenant_id,
|
||||
source_type=source_type,
|
||||
source_reference=(form.get("source_reference") or "").strip() or None,
|
||||
observed_on=observed_on,
|
||||
confidence_score=confidence,
|
||||
is_authoritative=str(form.get("is_authoritative") or "").lower() in {"1", "true", "yes", "on"},
|
||||
evidence_json={
|
||||
"primary_industry": profile.primary_industry,
|
||||
"primary_business_activity": profile.primary_business_activity,
|
||||
"business_model": profile.business_model,
|
||||
"main_products": profile.main_products,
|
||||
"main_services": profile.main_services,
|
||||
},
|
||||
notes=(form.get("source_notes") or "").strip() or None,
|
||||
created_by_user_id=actor_user_id,
|
||||
)
|
||||
db.add(row)
|
||||
# Confidence is an evidence summary only. User confirmation remains authoritative.
|
||||
if profile.profile_status != "confirmed":
|
||||
scores = [s.confidence_score for s in list_sources(db, client.id)] + [confidence]
|
||||
profile.confidence_score = round(sum(scores) / len(scores)) if scores else 0
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return row
|
||||
Reference in New Issue
Block a user