Add scope-aware subscriptions using existing registrations
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
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.")
|
||||
Reference in New Issue
Block a user