Files
arrr-erp/app/modules/services/scope_targets.py
T
2026-08-05 19:36:55 +05:30

355 lines
17 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.modules.clients.models import Client, ClientBusinessUnit, ClientBranch
from app.modules.registrations.models import ClientRegistration, RegistrationType
from app.modules.services.models import ClientServicePlan, ServiceCatalogue
VALID_SCOPE_TYPES = {"client", "business_unit", "client_branch", "registration"}
VALID_REGISTRATION_TYPES = {"GST", "TAN", "PF", "ESI", "PT", "IEC", "FSSAI", "UDYAM", "OTHER"}
@dataclass
class ScopeTarget:
token: str
scope_type: str
scope_key: str
client_id: int
business_unit_id: int | None
client_branch_id: int | None
registration_id: int | None
client_code: str
client_name: str
pan: str
business_unit: str
client_branch: str
registration_type: str
registration_number: str
trade_name: str
state: str
entity_type: str
partner_id: int | None
existing_plan_status: str = ""
def normalize_scope_type(value: str | None) -> str:
value = (value or "client").strip().lower().replace("-", "_").replace(" ", "_")
return value if value in VALID_SCOPE_TYPES else "client"
def normalize_registration_type(value: str | None) -> str | None:
value = (value or "").strip().upper()
return value if value in VALID_REGISTRATION_TYPES else None
def scope_key(scope_type: str, target_id: int) -> str:
prefixes = {
"client": "CLIENT",
"business_unit": "BUSINESS",
"client_branch": "BRANCH",
"registration": "REGISTRATION",
}
return f"{prefixes[scope_type]}:{int(target_id)}"
def _plan_status_map(db: Session, tenant_id: int, service_catalogue_id: int) -> dict[str, str]:
rows = db.execute(
select(ClientServicePlan.scope_key, ClientServicePlan.status).where(
ClientServicePlan.tenant_id == tenant_id,
ClientServicePlan.service_catalogue_id == service_catalogue_id,
)
).all()
return {key: status for key, status in rows if key}
def list_scope_targets(
db: Session,
*,
tenant_id: int,
clients: list[Client],
catalogue: ServiceCatalogue,
) -> list[ScopeTarget]:
client_map = {int(c.id): c for c in clients}
client_ids = list(client_map)
if not client_ids:
return []
selected_scope = normalize_scope_type(catalogue.service_scope_type)
required_registration = normalize_registration_type(catalogue.required_registration_type)
plan_status = _plan_status_map(db, tenant_id, catalogue.id)
result: list[ScopeTarget] = []
if selected_scope == "client":
for client in clients:
key = scope_key("client", client.id)
result.append(ScopeTarget(
token=f"client:{client.id}", scope_type="client", scope_key=key,
client_id=client.id, business_unit_id=None, client_branch_id=None, registration_id=None,
client_code=client.client_code or "", client_name=client.client_name or "",
pan=client.pan or "", business_unit="", client_branch="", registration_type="",
registration_number="", trade_name=client.trade_name or "", state=client.state or "",
entity_type=client.client_type or "", partner_id=client.partner_id,
existing_plan_status=plan_status.get(key, ""),
))
return result
businesses = db.execute(
select(ClientBusinessUnit).where(
ClientBusinessUnit.tenant_id == tenant_id,
ClientBusinessUnit.client_id.in_(client_ids),
ClientBusinessUnit.is_active.is_(True),
).order_by(ClientBusinessUnit.client_id, ClientBusinessUnit.is_primary.desc(), ClientBusinessUnit.business_name)
).scalars().all()
if selected_scope == "business_unit":
actual_clients = set()
for business in businesses:
client = client_map[business.client_id]
actual_clients.add(client.id)
key = scope_key("business_unit", business.id)
result.append(ScopeTarget(
token=f"business:{business.id}", scope_type="business_unit", scope_key=key,
client_id=client.id, business_unit_id=business.id, client_branch_id=None, registration_id=None,
client_code=client.client_code or "", client_name=client.client_name or "", pan=client.pan or "",
business_unit=business.business_name, client_branch="", registration_type="", registration_number="",
trade_name=business.trade_name or client.trade_name or "", state=client.state or "",
entity_type=client.client_type or "", partner_id=client.partner_id,
existing_plan_status=plan_status.get(key, ""),
))
for client in clients:
if client.id not in actual_clients:
result.append(ScopeTarget(
token=f"business:auto:{client.id}", scope_type="business_unit", scope_key=f"AUTO_BUSINESS:{client.id}",
client_id=client.id, business_unit_id=None, client_branch_id=None, registration_id=None,
client_code=client.client_code or "", client_name=client.client_name or "", pan=client.pan or "",
business_unit=client.trade_name or client.client_name, client_branch="", registration_type="",
registration_number="", trade_name=client.trade_name or "", state=client.state or "",
entity_type=client.client_type or "", partner_id=client.partner_id, existing_plan_status="",
))
return result
business_map = {b.id: b for b in businesses}
branches = db.execute(
select(ClientBranch).where(
ClientBranch.tenant_id == tenant_id,
ClientBranch.client_id.in_(client_ids),
ClientBranch.is_active.is_(True),
).order_by(ClientBranch.client_id, ClientBranch.is_primary.desc(), ClientBranch.branch_name)
).scalars().all()
if selected_scope == "client_branch":
for branch in branches:
client = client_map[branch.client_id]
business = business_map.get(branch.business_unit_id)
key = scope_key("client_branch", branch.id)
result.append(ScopeTarget(
token=f"branch:{branch.id}", scope_type="client_branch", scope_key=key,
client_id=client.id, business_unit_id=branch.business_unit_id, client_branch_id=branch.id, registration_id=None,
client_code=client.client_code or "", client_name=client.client_name or "", pan=client.pan or "",
business_unit=business.business_name if business else "", client_branch=branch.branch_name,
registration_type="", registration_number="", trade_name=(business.trade_name if business else "") or client.trade_name or "",
state=branch.state or client.state or "", entity_type=client.client_type or "", partner_id=client.partner_id,
existing_plan_status=plan_status.get(key, ""),
))
return result
branch_map = {b.id: b for b in branches}
registration_rows = db.execute(
select(ClientRegistration, RegistrationType).join(
RegistrationType, RegistrationType.id == ClientRegistration.registration_type_id
).where(
ClientRegistration.tenant_id == tenant_id,
ClientRegistration.client_id.in_(client_ids),
ClientRegistration.status.in_(("active", "valid", "registered")),
RegistrationType.is_active.is_(True),
).order_by(ClientRegistration.client_id, RegistrationType.code, ClientRegistration.registration_number)
).all()
actual_legacy = set()
for registration, registration_type in registration_rows:
registration_code = (registration_type.code or "").strip().upper()
if required_registration and registration_code != required_registration:
continue
client = client_map[registration.client_id]
business = business_map.get(registration.business_unit_id)
branch = branch_map.get(registration.client_branch_id)
key = scope_key("registration", registration.id)
actual_legacy.add((client.id, registration_code))
result.append(ScopeTarget(
token=f"registration:{registration.id}", scope_type="registration", scope_key=key,
client_id=client.id, business_unit_id=registration.business_unit_id,
client_branch_id=registration.client_branch_id, registration_id=registration.id,
client_code=client.client_code or "", client_name=client.client_name or "", pan=client.pan or "",
business_unit=business.business_name if business else "", client_branch=branch.branch_name if branch else "",
registration_type=registration_code, registration_number=registration.registration_number,
trade_name=registration.trade_name or (business.trade_name if business else "") or client.trade_name or "",
state=registration.state or (branch.state if branch else "") or client.state or "",
entity_type=client.client_type or "", partner_id=client.partner_id,
existing_plan_status=plan_status.get(key, ""),
))
# Existing legacy GSTIN/TAN values remain usable immediately after migration.
for client in clients:
legacy_pairs = []
if required_registration in (None, "GST") and client.gstin:
legacy_pairs.append(("GST", client.gstin))
if required_registration in (None, "TAN") and client.tan:
legacy_pairs.append(("TAN", client.tan))
for reg_type, number in legacy_pairs:
if (client.id, reg_type) in actual_legacy:
continue
result.append(ScopeTarget(
token=f"registration:legacy:{reg_type}:{client.id}", scope_type="registration",
scope_key=f"LEGACY_{reg_type}:{client.id}", client_id=client.id,
business_unit_id=None, client_branch_id=None, registration_id=None,
client_code=client.client_code or "", client_name=client.client_name or "", pan=client.pan or "",
business_unit=client.trade_name or client.client_name, client_branch="Primary Branch",
registration_type=reg_type, registration_number=number, trade_name=client.trade_name or "",
state=client.state or "", entity_type=client.client_type or "", partner_id=client.partner_id,
existing_plan_status="",
))
return result
def _next_code(prefix: str, value: int) -> str:
return f"{prefix}{int(value):05d}"
def resolve_scope_target(
db: Session,
*,
tenant_id: int,
clients_by_id: dict[int, Client],
token: str,
actor_user_id: int,
):
parts = (token or "").split(":")
if not parts:
raise ValueError("Invalid subscription scope.")
kind = parts[0]
if kind == "client" and len(parts) == 2:
client = clients_by_id.get(int(parts[1]))
if not client:
raise ValueError("Client is outside the permitted scope.")
return client, "client", scope_key("client", client.id), None, None, None
if kind == "business":
if len(parts) == 2:
business = db.get(ClientBusinessUnit, int(parts[1]))
if not business or business.tenant_id != tenant_id or business.client_id not in clients_by_id:
raise ValueError("Business Unit is outside the permitted scope.")
elif len(parts) == 3 and parts[1] == "auto":
client = clients_by_id.get(int(parts[2]))
if not client:
raise ValueError("Client is outside the permitted scope.")
business = ClientBusinessUnit(
tenant_id=tenant_id, client_id=client.id,
business_code=_next_code("BU", client.id),
business_name=client.trade_name or client.client_name,
trade_name=client.trade_name, is_primary=True, is_active=True,
created_by_user_id=actor_user_id, updated_by_user_id=actor_user_id,
)
db.add(business); db.flush()
else:
raise ValueError("Invalid Business Unit.")
client = clients_by_id[business.client_id]
return client, "business_unit", scope_key("business_unit", business.id), business.id, None, None
if kind == "branch" and len(parts) == 2:
branch = db.get(ClientBranch, int(parts[1]))
if not branch or branch.tenant_id != tenant_id or branch.client_id not in clients_by_id:
raise ValueError("Client Branch is outside the permitted scope.")
client = clients_by_id[branch.client_id]
return client, "client_branch", scope_key("client_branch", branch.id), branch.business_unit_id, branch.id, None
if kind == "registration":
if len(parts) == 2:
reg = db.get(ClientRegistration, int(parts[1]))
if not reg or reg.tenant_id != tenant_id or reg.client_id not in clients_by_id:
raise ValueError("Registration is outside the permitted scope.")
elif len(parts) == 4 and parts[1] == "legacy":
reg_type = parts[2].upper()
client = clients_by_id.get(int(parts[3]))
if not client:
raise ValueError("Client is outside the permitted scope.")
number = client.gstin if reg_type == "GST" else client.tan if reg_type == "TAN" else None
if not number:
raise ValueError("Legacy registration is no longer available.")
business = db.execute(
select(ClientBusinessUnit).where(
ClientBusinessUnit.tenant_id == tenant_id,
ClientBusinessUnit.client_id == client.id,
ClientBusinessUnit.is_active.is_(True),
).order_by(ClientBusinessUnit.is_primary.desc(), ClientBusinessUnit.id)
).scalars().first()
if not business:
business = ClientBusinessUnit(
tenant_id=tenant_id, client_id=client.id,
business_code=_next_code("BU", client.id),
business_name=client.trade_name or client.client_name,
trade_name=client.trade_name, is_primary=True, is_active=True,
created_by_user_id=actor_user_id, updated_by_user_id=actor_user_id,
)
db.add(business); db.flush()
branch = db.execute(
select(ClientBranch).where(
ClientBranch.tenant_id == tenant_id,
ClientBranch.business_unit_id == business.id,
ClientBranch.is_active.is_(True),
).order_by(ClientBranch.is_primary.desc(), ClientBranch.id)
).scalars().first()
if not branch:
branch = ClientBranch(
tenant_id=tenant_id, client_id=client.id, business_unit_id=business.id,
branch_code=_next_code("BR", client.id), branch_name="Primary Branch",
branch_type="head_office", state=client.state, is_primary=True, is_active=True,
created_by_user_id=actor_user_id, updated_by_user_id=actor_user_id,
)
db.add(branch); db.flush()
registration_type = db.execute(
select(RegistrationType).where(RegistrationType.code == reg_type)
).scalar_one_or_none()
if not registration_type:
registration_type = RegistrationType(
code=reg_type,
name=reg_type,
category="registration",
identifier_label=f"{reg_type} Number",
supports_expiry=False,
supports_related_person=False,
is_system=True,
is_active=True,
sort_order=0,
)
db.add(registration_type)
db.flush()
reg = ClientRegistration(
tenant_id=tenant_id,
branch_id=getattr(client, "branch_id", None),
client_id=client.id,
business_unit_id=business.id,
client_branch_id=branch.id,
registration_type_id=registration_type.id,
registration_number=number.strip().upper(),
legal_name=client.client_name,
trade_name=client.trade_name,
state=client.state,
jurisdiction=client.state,
status="active",
primary_registration=True,
responsible_party="firm",
auto_create_task=True,
created_by_user_id=actor_user_id,
)
db.add(reg); db.flush()
else:
raise ValueError("Invalid registration.")
client = clients_by_id[reg.client_id]
return client, "registration", scope_key("registration", reg.id), reg.business_unit_id, reg.client_branch_id, reg.id
raise ValueError("Invalid subscription scope.")