141 lines
5.6 KiB
Python
141 lines
5.6 KiB
Python
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
|