Fix duplicate AQMM gating on approved engagements
This commit is contained in:
@@ -279,6 +279,128 @@ def _default_internal_target_date(subscription: ClientServiceSubscription, templ
|
||||
return None
|
||||
|
||||
|
||||
def _normalise_task_name_for_setup_sync(value: str | None) -> str:
|
||||
return " ".join((value or "").strip().lower().replace("&", "and").split())
|
||||
|
||||
|
||||
def _is_engagement_acceptance_setup_task_name(value: str | None) -> bool:
|
||||
"""Return True only for execution rows that duplicate the engagement-level AQMM setup.
|
||||
|
||||
These tasks are retained in the work tracker as historical/scope rows, but once the
|
||||
engagement-level AQMM workflow is approved they must not ask staff to repeat the
|
||||
same acceptance, independence, KYC or team-setup approvals.
|
||||
"""
|
||||
name = _normalise_task_name_for_setup_sync(value)
|
||||
if not name:
|
||||
return False
|
||||
if "acceptance and continuance" in name:
|
||||
return True
|
||||
if "independence and conflict" in name:
|
||||
return True
|
||||
if "kyc" in name and "engagement letter" in name:
|
||||
return True
|
||||
if name.startswith("create ") and "engagement" in name and "assign team" in name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _apply_approved_engagement_acceptance_to_task(
|
||||
task: ClientServiceTaskInstance,
|
||||
*,
|
||||
actor_user_id: int,
|
||||
) -> bool:
|
||||
"""Synchronise one duplicate setup task from an already-approved engagement AQMM gate.
|
||||
|
||||
No task is deleted and no executed/reviewed normal work is rewritten. Only the four
|
||||
pre-execution setup controls that duplicate the engagement-level acceptance workflow
|
||||
are satisfied automatically. This removes the circular gate where an active, AQMM-
|
||||
approved engagement asks staff to obtain AQMM approval again before starting work.
|
||||
"""
|
||||
if not _is_engagement_acceptance_setup_task_name(getattr(task, "task_name", None)):
|
||||
return False
|
||||
|
||||
# Never rewrite genuine task execution/rework history. This repair is intentionally
|
||||
# limited to untouched setup rows, plus rows already auto-completed by this sync.
|
||||
status = (getattr(task, "status", None) or "pending").strip().lower()
|
||||
if status not in {"pending", "completed"}:
|
||||
return False
|
||||
if getattr(task, "started_at_utc", None) is not None and status != "completed":
|
||||
return False
|
||||
if (getattr(task, "rework_status", None) or "none").strip().lower() == "open":
|
||||
return False
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
changed = False
|
||||
|
||||
def set_if_different(attr: str, value) -> None:
|
||||
nonlocal changed
|
||||
if getattr(task, attr, None) != value:
|
||||
setattr(task, attr, value)
|
||||
changed = True
|
||||
|
||||
set_if_different("status", "completed")
|
||||
if getattr(task, "completed_at_utc", None) is None:
|
||||
task.completed_at_utc = now
|
||||
changed = True
|
||||
|
||||
# The engagement-level AQMM approval has already performed these reviews. Keep the
|
||||
# task row, but do not make staff repeat Manager/Partner/Review-Partner approvals.
|
||||
set_if_different("normal_review_required", False)
|
||||
set_if_different("normal_review_role", None)
|
||||
set_if_different("normal_review_status", "not_required")
|
||||
set_if_different("aqmm_manager_review_required", False)
|
||||
set_if_different("aqmm_partner_review_required", False)
|
||||
set_if_different("aqmm_review_partner_required", False)
|
||||
set_if_different("manager_review_status", "not_required")
|
||||
set_if_different("partner_review_status", "not_required")
|
||||
set_if_different("review_partner_review_status", "not_required")
|
||||
|
||||
# Evidence for these four setup controls lives in the engagement-level AQMM records
|
||||
# (declarations/KYC/engagement letter). Do not require duplicate task-document evidence.
|
||||
set_if_different("aqmm_evidence_required", False)
|
||||
set_if_different("evidence_status", "not_required")
|
||||
set_if_different("aqmm_review_status", "not_required")
|
||||
set_if_different("aqmm_status", "completed")
|
||||
if getattr(task, "aqmm_completed_at_utc", None) is None:
|
||||
task.aqmm_completed_at_utc = now
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
task.updated_by_user_id = actor_user_id
|
||||
return changed
|
||||
|
||||
|
||||
def synchronise_approved_engagement_acceptance_tasks(
|
||||
db: Session,
|
||||
*,
|
||||
subscription: ClientServiceSubscription,
|
||||
actor_user_id: int,
|
||||
) -> int:
|
||||
"""Self-heal duplicate pre-acceptance execution rows after AQMM approval.
|
||||
|
||||
Returns the number of task rows changed. The caller can use this to decide whether
|
||||
a commit is required. Existing non-setup tasks, task responses, evidence, rework and
|
||||
reviews are left untouched.
|
||||
"""
|
||||
if not quality_required_for_engagement(subscription.engagement_type):
|
||||
return 0
|
||||
if getattr(subscription, "quality_acceptance_status", None) != QUALITY_APPROVED:
|
||||
return 0
|
||||
|
||||
tasks = db.execute(
|
||||
select(ClientServiceTaskInstance).where(
|
||||
ClientServiceTaskInstance.subscription_id == subscription.id,
|
||||
ClientServiceTaskInstance.is_active.is_(True),
|
||||
)
|
||||
).scalars().all()
|
||||
|
||||
changed = 0
|
||||
for task in tasks:
|
||||
if _apply_approved_engagement_acceptance_to_task(task, actor_user_id=actor_user_id):
|
||||
changed += 1
|
||||
return changed
|
||||
|
||||
|
||||
def generate_tasks_for_subscription(db: Session, *, subscription: ClientServiceSubscription, user_id: int) -> int:
|
||||
if quality_required_for_engagement(subscription.engagement_type) and getattr(subscription, "quality_acceptance_status", None) != QUALITY_APPROVED:
|
||||
raise ValueError(getattr(subscription, "quality_block_reason", None) or "AQMM acceptance is pending for this assurance engagement. Complete AQMM before generating tasks.")
|
||||
@@ -372,11 +494,19 @@ def generate_tasks_for_subscription_if_ready(
|
||||
and getattr(subscription, "quality_acceptance_status", None) != QUALITY_APPROVED
|
||||
):
|
||||
return 0
|
||||
return generate_tasks_for_subscription(
|
||||
created = generate_tasks_for_subscription(
|
||||
db,
|
||||
subscription=subscription,
|
||||
user_id=user_id,
|
||||
)
|
||||
repaired = synchronise_approved_engagement_acceptance_tasks(
|
||||
db,
|
||||
subscription=subscription,
|
||||
actor_user_id=user_id,
|
||||
)
|
||||
# Preserve the historical meaning (positive = database changed) while allowing
|
||||
# existing already-generated engagements to self-heal and be committed by callers.
|
||||
return int(created) + int(repaired)
|
||||
|
||||
|
||||
def _sync_normal_review_snapshot_preserving_history(
|
||||
|
||||
Reference in New Issue
Block a user