Files
arrr-erp/app/modules/clients/business_profile_service.py
T
2026-08-22 12:04:44 +05:30

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