Add client acceptance controls for AQMM and peer review workflow
This commit is contained in:
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import io
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
@@ -16,6 +17,7 @@ from app.modules.clients.constants import (
|
||||
CLIENT_STATUS,
|
||||
CLIENT_TYPES,
|
||||
RISK_CATEGORIES,
|
||||
CLIENT_ACCEPTANCE_APPROVAL_REQUIRED_RISKS,
|
||||
)
|
||||
|
||||
|
||||
@@ -25,6 +27,53 @@ def _payload_from_schema(data):
|
||||
|
||||
|
||||
|
||||
|
||||
def _is_high_risk(risk_category: str | None) -> bool:
|
||||
return (risk_category or "").strip().lower() in CLIENT_ACCEPTANCE_APPROVAL_REQUIRED_RISKS
|
||||
|
||||
|
||||
def _client_acceptance_ready(payload: dict) -> bool:
|
||||
if not payload.get("acceptance_required", True):
|
||||
return True
|
||||
if not payload.get("independence_check_completed"):
|
||||
return False
|
||||
if not payload.get("conflict_check_completed"):
|
||||
return False
|
||||
if not payload.get("kyc_completed"):
|
||||
return False
|
||||
if payload.get("engagement_letter_required", True) and not payload.get("engagement_letter_received"):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _enforce_client_acceptance_controls(payload: dict, *, existing_row=None):
|
||||
risk = payload.get("risk_category")
|
||||
if risk is None and existing_row is not None:
|
||||
risk = getattr(existing_row, "risk_category", None)
|
||||
|
||||
status = payload.get("status")
|
||||
if status is None and existing_row is not None:
|
||||
status = getattr(existing_row, "status", None)
|
||||
|
||||
acceptance_status = payload.get("acceptance_status")
|
||||
if acceptance_status is None and existing_row is not None:
|
||||
acceptance_status = getattr(existing_row, "acceptance_status", "pending_review")
|
||||
acceptance_status = acceptance_status or "pending_review"
|
||||
|
||||
if _is_high_risk(risk) and status == "active" and acceptance_status != "approved":
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="High/Critical risk clients cannot be active until client acceptance is approved by an authorised partner or firm admin.",
|
||||
)
|
||||
|
||||
if acceptance_status == "approved" and not _client_acceptance_ready(payload):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Client acceptance cannot be approved until independence, conflict, KYC and required engagement letter controls are completed.",
|
||||
)
|
||||
|
||||
return payload
|
||||
|
||||
def _ensure_portal_passwords(email: str | None, portal_password: str | None, portal_password_confirm: str | None, *, required: bool):
|
||||
email_clean = (email or '').strip().lower()
|
||||
pw = (portal_password or '').strip()
|
||||
@@ -206,6 +255,7 @@ def create_client_service(db, *, data, actor_user_id: int, scope, current_user_r
|
||||
raise HTTPException(status_code=400, detail="GSTIN already exists for another client in this tenant.")
|
||||
|
||||
payload = _payload_from_schema(data)
|
||||
_enforce_client_acceptance_controls(payload)
|
||||
row = repository.create_client(db, payload)
|
||||
row = _sync_client_portal_user(db, row=row, portal_password=portal_password, portal_password_confirm=portal_password_confirm)
|
||||
_write_association_from_client(db, row, actor_user_id=actor_user_id, current_user_roles=current_user_roles)
|
||||
@@ -234,6 +284,7 @@ def update_client_service(db, *, row, data, actor_user_id: int, scope, current_u
|
||||
)
|
||||
|
||||
payload = _payload_from_schema(data)
|
||||
_enforce_client_acceptance_controls(payload, existing_row=row)
|
||||
|
||||
if payload.get("pan"):
|
||||
existing_pan = repository.get_client_by_pan(db, tenant_id=payload["tenant_id"], pan=payload["pan"])
|
||||
@@ -305,6 +356,18 @@ def deactivate_client_service(db, *, row, actor_user_id: int):
|
||||
|
||||
|
||||
def activate_client_service(db, *, row, actor_user_id: int):
|
||||
payload = {
|
||||
"status": "active",
|
||||
"risk_category": getattr(row, "risk_category", None),
|
||||
"acceptance_status": getattr(row, "acceptance_status", "pending_review"),
|
||||
"acceptance_required": getattr(row, "acceptance_required", True),
|
||||
"independence_check_completed": getattr(row, "independence_check_completed", False),
|
||||
"conflict_check_completed": getattr(row, "conflict_check_completed", False),
|
||||
"kyc_completed": getattr(row, "kyc_completed", False),
|
||||
"engagement_letter_required": getattr(row, "engagement_letter_required", True),
|
||||
"engagement_letter_received": getattr(row, "engagement_letter_received", False),
|
||||
}
|
||||
_enforce_client_acceptance_controls(payload, existing_row=row)
|
||||
row = repository.update_client(db, row, {"status": "active"})
|
||||
repository.write_audit_log(
|
||||
db,
|
||||
@@ -450,3 +513,78 @@ def reset_client_portal_password_service(db, *, current_user, new_password: str)
|
||||
db.commit()
|
||||
db.refresh(current_user)
|
||||
return current_user
|
||||
|
||||
|
||||
def approve_client_acceptance_service(db, *, row, actor_user_id: int, review_notes: str | None = None):
|
||||
payload = {
|
||||
"acceptance_status": "approved",
|
||||
"acceptance_required": getattr(row, "acceptance_required", True),
|
||||
"independence_check_completed": getattr(row, "independence_check_completed", False),
|
||||
"conflict_check_completed": getattr(row, "conflict_check_completed", False),
|
||||
"kyc_completed": getattr(row, "kyc_completed", False),
|
||||
"engagement_letter_required": getattr(row, "engagement_letter_required", True),
|
||||
"engagement_letter_received": getattr(row, "engagement_letter_received", False),
|
||||
"risk_category": getattr(row, "risk_category", None),
|
||||
"status": getattr(row, "status", None),
|
||||
}
|
||||
_enforce_client_acceptance_controls(payload, existing_row=row)
|
||||
now = datetime.now(timezone.utc)
|
||||
updated = repository.update_client(db, row, {
|
||||
"acceptance_status": "approved",
|
||||
"acceptance_approved_by_user_id": actor_user_id,
|
||||
"acceptance_approved_at_utc": now,
|
||||
"acceptance_review_notes": review_notes or getattr(row, "acceptance_review_notes", None),
|
||||
"acceptance_rejection_reason": None,
|
||||
})
|
||||
repository.write_audit_log(
|
||||
db,
|
||||
client_id=updated.id,
|
||||
tenant_id=updated.tenant_id,
|
||||
branch_id=updated.branch_id,
|
||||
actor_user_id=actor_user_id,
|
||||
action="acceptance_approved",
|
||||
summary="Client acceptance approved.",
|
||||
payload_json={"review_notes": review_notes},
|
||||
)
|
||||
return updated
|
||||
|
||||
|
||||
def reject_client_acceptance_service(db, *, row, actor_user_id: int, rejection_reason: str | None = None):
|
||||
updated = repository.update_client(db, row, {
|
||||
"acceptance_status": "rejected",
|
||||
"acceptance_approved_by_user_id": None,
|
||||
"acceptance_approved_at_utc": None,
|
||||
"acceptance_rejection_reason": rejection_reason,
|
||||
})
|
||||
repository.write_audit_log(
|
||||
db,
|
||||
client_id=updated.id,
|
||||
tenant_id=updated.tenant_id,
|
||||
branch_id=updated.branch_id,
|
||||
actor_user_id=actor_user_id,
|
||||
action="acceptance_rejected",
|
||||
summary="Client acceptance rejected.",
|
||||
payload_json={"rejection_reason": rejection_reason},
|
||||
)
|
||||
return updated
|
||||
|
||||
|
||||
def mark_client_acceptance_pending_service(db, *, row, actor_user_id: int, review_notes: str | None = None):
|
||||
updated = repository.update_client(db, row, {
|
||||
"acceptance_status": "pending_review",
|
||||
"acceptance_approved_by_user_id": None,
|
||||
"acceptance_approved_at_utc": None,
|
||||
"acceptance_review_notes": review_notes or getattr(row, "acceptance_review_notes", None),
|
||||
"acceptance_rejection_reason": None,
|
||||
})
|
||||
repository.write_audit_log(
|
||||
db,
|
||||
client_id=updated.id,
|
||||
tenant_id=updated.tenant_id,
|
||||
branch_id=updated.branch_id,
|
||||
actor_user_id=actor_user_id,
|
||||
action="acceptance_pending_review",
|
||||
summary="Client acceptance moved to pending review.",
|
||||
payload_json={"review_notes": review_notes},
|
||||
)
|
||||
return updated
|
||||
|
||||
Reference in New Issue
Block a user