Add subscription registration scope mapping
This commit is contained in:
@@ -34,7 +34,8 @@ from app.modules.services.client_services import (
|
||||
recurrence_requires_period,
|
||||
review_partner_required_for_engagement,
|
||||
)
|
||||
from app.modules.clients.models import Client
|
||||
from app.modules.clients.models import Client, ClientBusinessUnit, ClientBranch
|
||||
from app.modules.registrations.models import ClientRegistration, RegistrationType
|
||||
from app.modules.services.due_dates import apply_due_date_rule_to_subscription
|
||||
from app.modules.services.scope_targets import list_scope_targets, resolve_scope_target
|
||||
from app.modules.services.models import (
|
||||
@@ -74,6 +75,85 @@ def _partner_scope_id(db, user) -> int | None:
|
||||
roles = set(get_user_roles(db, user.id))
|
||||
return int(user.id) if "Partner" in roles else None
|
||||
|
||||
|
||||
def _registration_scope_context(db, *, tenant_id: int, plan: ClientServicePlan):
|
||||
businesses = db.execute(
|
||||
select(ClientBusinessUnit).where(
|
||||
ClientBusinessUnit.tenant_id == tenant_id,
|
||||
ClientBusinessUnit.client_id == plan.client_id,
|
||||
ClientBusinessUnit.is_active.is_(True),
|
||||
).order_by(
|
||||
ClientBusinessUnit.is_primary.desc(),
|
||||
ClientBusinessUnit.business_name,
|
||||
)
|
||||
).scalars().all()
|
||||
|
||||
branches = db.execute(
|
||||
select(ClientBranch).where(
|
||||
ClientBranch.tenant_id == tenant_id,
|
||||
ClientBranch.client_id == plan.client_id,
|
||||
ClientBranch.is_active.is_(True),
|
||||
).order_by(
|
||||
ClientBranch.is_primary.desc(),
|
||||
ClientBranch.branch_name,
|
||||
)
|
||||
).scalars().all()
|
||||
|
||||
registration_rows = db.execute(
|
||||
select(ClientRegistration, RegistrationType)
|
||||
.join(
|
||||
RegistrationType,
|
||||
RegistrationType.id == ClientRegistration.registration_type_id,
|
||||
)
|
||||
.where(
|
||||
ClientRegistration.tenant_id == tenant_id,
|
||||
ClientRegistration.client_id == plan.client_id,
|
||||
ClientRegistration.status.in_(("active", "valid", "registered")),
|
||||
RegistrationType.is_active.is_(True),
|
||||
)
|
||||
.order_by(
|
||||
RegistrationType.code,
|
||||
ClientRegistration.registration_number,
|
||||
)
|
||||
).all()
|
||||
|
||||
registrations = []
|
||||
registration_type_by_id = {}
|
||||
for registration, registration_type in registration_rows:
|
||||
registrations.append(registration)
|
||||
registration_type_by_id[registration.id] = registration_type
|
||||
|
||||
business_by_id = {row.id: row for row in businesses}
|
||||
branch_by_id = {row.id: row for row in branches}
|
||||
registration_by_id = {row.id: row for row in registrations}
|
||||
|
||||
return {
|
||||
"businesses": businesses,
|
||||
"branches": branches,
|
||||
"registrations": registrations,
|
||||
"registration_type_by_id": registration_type_by_id,
|
||||
"scope_business": business_by_id.get(plan.business_unit_id),
|
||||
"scope_branch": branch_by_id.get(plan.client_branch_id),
|
||||
"scope_registration": registration_by_id.get(plan.registration_id),
|
||||
"scope_registration_type": (
|
||||
registration_type_by_id.get(plan.registration_id)
|
||||
if plan.registration_id
|
||||
else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _scope_key_for(scope_type: str, target_id: int) -> str:
|
||||
prefixes = {
|
||||
"client": "CLIENT",
|
||||
"business_unit": "BUSINESS",
|
||||
"client_branch": "BRANCH",
|
||||
"registration": "REGISTRATION",
|
||||
}
|
||||
if scope_type not in prefixes:
|
||||
raise ValueError("scope_type")
|
||||
return f"{prefixes[scope_type]}:{int(target_id)}"
|
||||
|
||||
def _ctx(request, db, user, **extra):
|
||||
data = {
|
||||
"request": request,
|
||||
@@ -522,6 +602,322 @@ def subscription_bulk_submit(
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
|
||||
@router.get("/{plan_id}/scope")
|
||||
def subscription_scope_edit_page(
|
||||
request: Request,
|
||||
plan_id: int,
|
||||
saved: int = 0,
|
||||
updated_engagements: int = 0,
|
||||
error: str = "",
|
||||
):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse("/login", 303)
|
||||
try:
|
||||
require_permission(db, user, "clients.edit")
|
||||
except Exception:
|
||||
return _denied()
|
||||
|
||||
tenant_id = _tenant_id(request, user)
|
||||
branch_id = _branch_id(request, user)
|
||||
plan = _load_plan(
|
||||
db,
|
||||
plan_id=plan_id,
|
||||
tenant_id=tenant_id,
|
||||
branch_id=branch_id,
|
||||
)
|
||||
if not plan:
|
||||
return RedirectResponse("/services/subscriptions", 303)
|
||||
|
||||
scope_context = _registration_scope_context(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
plan=plan,
|
||||
)
|
||||
error_messages = {
|
||||
"scope_type": "Select a valid subscription scope.",
|
||||
"business_unit": "Select a valid Business Unit belonging to this client.",
|
||||
"client_branch": "Select a valid Client Branch belonging to this client.",
|
||||
"registration": "Select a valid active registration belonging to this client.",
|
||||
"registration_type": "The selected registration type does not match the Service Catalogue requirement.",
|
||||
"duplicate_plan": "Another subscription already exists for this service and selected scope.",
|
||||
"duplicate_engagement": "An engagement already exists for the selected scope, financial year and period.",
|
||||
"propagation": "Select a valid propagation option.",
|
||||
}
|
||||
return templates.TemplateResponse(
|
||||
"modules/services/templates/services/subscriptions/scope.html",
|
||||
_ctx(
|
||||
request,
|
||||
db,
|
||||
user,
|
||||
title="Map Subscription Scope",
|
||||
plan=plan,
|
||||
saved=saved,
|
||||
updated_engagements=updated_engagements,
|
||||
error_message=error_messages.get(error, ""),
|
||||
**scope_context,
|
||||
),
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/{plan_id}/scope")
|
||||
def subscription_scope_edit_submit(
|
||||
request: Request,
|
||||
plan_id: int,
|
||||
scope_type: str = Form(...),
|
||||
business_unit_id: str = Form(""),
|
||||
client_branch_id: str = Form(""),
|
||||
registration_id: str = Form(""),
|
||||
propagation_mode: str = Form("open_engagements"),
|
||||
scope_change_reason: str = Form(""),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
validate_csrf(request, csrf_token)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse("/login", 303)
|
||||
try:
|
||||
require_permission(db, user, "clients.edit")
|
||||
except Exception:
|
||||
return _denied()
|
||||
|
||||
tenant_id = _tenant_id(request, user)
|
||||
branch_id = _branch_id(request, user)
|
||||
plan = _load_plan(
|
||||
db,
|
||||
plan_id=plan_id,
|
||||
tenant_id=tenant_id,
|
||||
branch_id=branch_id,
|
||||
)
|
||||
if not plan:
|
||||
return RedirectResponse("/services/subscriptions", 303)
|
||||
|
||||
normalized_scope = (
|
||||
scope_type or ""
|
||||
).strip().lower().replace("-", "_").replace(" ", "_")
|
||||
if normalized_scope not in {
|
||||
"client",
|
||||
"business_unit",
|
||||
"client_branch",
|
||||
"registration",
|
||||
}:
|
||||
return RedirectResponse(
|
||||
f"/services/subscriptions/{plan.id}/scope?error=scope_type",
|
||||
303,
|
||||
)
|
||||
if propagation_mode not in {"subscription_only", "open_engagements"}:
|
||||
return RedirectResponse(
|
||||
f"/services/subscriptions/{plan.id}/scope?error=propagation",
|
||||
303,
|
||||
)
|
||||
|
||||
selected_business_id = _optional_int(business_unit_id)
|
||||
selected_branch_id = _optional_int(client_branch_id)
|
||||
selected_registration_id = _optional_int(registration_id)
|
||||
|
||||
business = None
|
||||
client_branch = None
|
||||
registration = None
|
||||
registration_type = None
|
||||
|
||||
if selected_business_id:
|
||||
business = db.get(ClientBusinessUnit, selected_business_id)
|
||||
if (
|
||||
not business
|
||||
or business.tenant_id != tenant_id
|
||||
or business.client_id != plan.client_id
|
||||
or not business.is_active
|
||||
):
|
||||
return RedirectResponse(
|
||||
f"/services/subscriptions/{plan.id}/scope?error=business_unit",
|
||||
303,
|
||||
)
|
||||
|
||||
if selected_branch_id:
|
||||
client_branch = db.get(ClientBranch, selected_branch_id)
|
||||
if (
|
||||
not client_branch
|
||||
or client_branch.tenant_id != tenant_id
|
||||
or client_branch.client_id != plan.client_id
|
||||
or not client_branch.is_active
|
||||
):
|
||||
return RedirectResponse(
|
||||
f"/services/subscriptions/{plan.id}/scope?error=client_branch",
|
||||
303,
|
||||
)
|
||||
selected_business_id = client_branch.business_unit_id
|
||||
business = db.get(ClientBusinessUnit, selected_business_id)
|
||||
|
||||
if selected_registration_id:
|
||||
registration = db.get(ClientRegistration, selected_registration_id)
|
||||
if (
|
||||
not registration
|
||||
or registration.tenant_id != tenant_id
|
||||
or registration.client_id != plan.client_id
|
||||
or registration.status not in {"active", "valid", "registered"}
|
||||
):
|
||||
return RedirectResponse(
|
||||
f"/services/subscriptions/{plan.id}/scope?error=registration",
|
||||
303,
|
||||
)
|
||||
registration_type = db.get(
|
||||
RegistrationType,
|
||||
registration.registration_type_id,
|
||||
)
|
||||
selected_business_id = (
|
||||
registration.business_unit_id or selected_business_id
|
||||
)
|
||||
selected_branch_id = (
|
||||
registration.client_branch_id or selected_branch_id
|
||||
)
|
||||
if selected_business_id:
|
||||
business = db.get(ClientBusinessUnit, selected_business_id)
|
||||
if selected_branch_id:
|
||||
client_branch = db.get(ClientBranch, selected_branch_id)
|
||||
|
||||
if normalized_scope == "client":
|
||||
target_id = plan.client_id
|
||||
selected_business_id = None
|
||||
selected_branch_id = None
|
||||
selected_registration_id = None
|
||||
elif normalized_scope == "business_unit":
|
||||
if not business:
|
||||
return RedirectResponse(
|
||||
f"/services/subscriptions/{plan.id}/scope?error=business_unit",
|
||||
303,
|
||||
)
|
||||
target_id = business.id
|
||||
selected_branch_id = None
|
||||
selected_registration_id = None
|
||||
elif normalized_scope == "client_branch":
|
||||
if not client_branch:
|
||||
return RedirectResponse(
|
||||
f"/services/subscriptions/{plan.id}/scope?error=client_branch",
|
||||
303,
|
||||
)
|
||||
target_id = client_branch.id
|
||||
selected_registration_id = None
|
||||
else:
|
||||
if not registration or not registration_type:
|
||||
return RedirectResponse(
|
||||
f"/services/subscriptions/{plan.id}/scope?error=registration",
|
||||
303,
|
||||
)
|
||||
required_type = (
|
||||
getattr(plan.catalogue, "required_registration_type", None)
|
||||
or ""
|
||||
).strip().upper()
|
||||
actual_type = (registration_type.code or "").strip().upper()
|
||||
if required_type and required_type != actual_type:
|
||||
return RedirectResponse(
|
||||
f"/services/subscriptions/{plan.id}/scope?error=registration_type",
|
||||
303,
|
||||
)
|
||||
target_id = registration.id
|
||||
|
||||
new_scope_key = _scope_key_for(normalized_scope, target_id)
|
||||
|
||||
conflicting_plan = db.execute(
|
||||
select(ClientServicePlan).where(
|
||||
ClientServicePlan.tenant_id == tenant_id,
|
||||
ClientServicePlan.service_catalogue_id
|
||||
== plan.service_catalogue_id,
|
||||
ClientServicePlan.scope_key == new_scope_key,
|
||||
ClientServicePlan.id != plan.id,
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if conflicting_plan:
|
||||
return RedirectResponse(
|
||||
f"/services/subscriptions/{plan.id}/scope?error=duplicate_plan",
|
||||
303,
|
||||
)
|
||||
|
||||
open_engagements = []
|
||||
if propagation_mode == "open_engagements":
|
||||
open_engagements = db.execute(
|
||||
select(ClientServiceSubscription).where(
|
||||
ClientServiceSubscription.tenant_id == tenant_id,
|
||||
ClientServiceSubscription.service_plan_id == plan.id,
|
||||
)
|
||||
).scalars().all()
|
||||
open_engagements = [
|
||||
row for row in open_engagements if _engagement_is_open(row)
|
||||
]
|
||||
|
||||
for row in open_engagements:
|
||||
conflict = db.execute(
|
||||
select(ClientServiceSubscription.id).where(
|
||||
ClientServiceSubscription.tenant_id == tenant_id,
|
||||
ClientServiceSubscription.service_catalogue_id
|
||||
== plan.service_catalogue_id,
|
||||
ClientServiceSubscription.scope_key == new_scope_key,
|
||||
ClientServiceSubscription.financial_year
|
||||
== row.financial_year,
|
||||
ClientServiceSubscription.period_label
|
||||
== row.period_label,
|
||||
ClientServiceSubscription.id != row.id,
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if conflict:
|
||||
return RedirectResponse(
|
||||
f"/services/subscriptions/{plan.id}/scope?error=duplicate_engagement",
|
||||
303,
|
||||
)
|
||||
|
||||
old_scope = plan.scope_key or f"CLIENT:{plan.client_id}"
|
||||
plan.scope_type = normalized_scope
|
||||
plan.scope_key = new_scope_key
|
||||
plan.business_unit_id = selected_business_id
|
||||
plan.client_branch_id = selected_branch_id
|
||||
plan.registration_id = selected_registration_id
|
||||
plan.updated_by_user_id = user.id
|
||||
plan.updated_at_utc = datetime.now(timezone.utc)
|
||||
|
||||
updated_engagements = 0
|
||||
if propagation_mode == "open_engagements":
|
||||
for row in open_engagements:
|
||||
row.scope_type = normalized_scope
|
||||
row.scope_key = new_scope_key
|
||||
row.business_unit_id = selected_business_id
|
||||
row.client_branch_id = selected_branch_id
|
||||
row.registration_id = selected_registration_id
|
||||
row.updated_by_user_id = user.id
|
||||
row.updated_at_utc = datetime.now(timezone.utc)
|
||||
if scope_change_reason.strip():
|
||||
note = (
|
||||
f"Subscription scope changed from {old_scope} "
|
||||
f"to {new_scope_key} on "
|
||||
f"{datetime.now(timezone.utc).date().isoformat()} "
|
||||
f"by user {user.id}: "
|
||||
f"{scope_change_reason.strip()}"
|
||||
)
|
||||
row.remarks = (
|
||||
f"{row.remarks.strip()}\n{note}"
|
||||
if row.remarks
|
||||
else note
|
||||
)
|
||||
updated_engagements += 1
|
||||
|
||||
db.commit()
|
||||
return RedirectResponse(
|
||||
f"/services/subscriptions/{plan.id}/scope?saved=1"
|
||||
f"&updated_engagements={updated_engagements}",
|
||||
303,
|
||||
)
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@router.get("/{plan_id}/edit")
|
||||
def subscription_master_edit_page(
|
||||
request: Request,
|
||||
@@ -796,6 +1192,11 @@ def subscription_master_detail(request: Request, plan_id: int):
|
||||
plan=plan,
|
||||
engagements=engagements,
|
||||
can_edit_subscription="clients.edit" in permissions,
|
||||
**_registration_scope_context(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
plan=plan,
|
||||
),
|
||||
),
|
||||
)
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user