from __future__ import annotations import argparse from collections import Counter from sqlalchemy import func, select from app.core.db.common import CommonSessionLocal from app.modules.services.execution import generate_tasks_for_subscription_if_ready from app.modules.services.models import ( ClientServiceSubscription, ClientServiceTaskInstance, FirmServiceTaskTemplate, ) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description=( "Generate missing task instances for existing active engagements. " "Dry-run is the default; pass --apply to commit." ) ) parser.add_argument("--apply", action="store_true", help="Commit generated tasks.") parser.add_argument("--tenant-id", type=int, default=None, help="Optional tenant filter.") parser.add_argument( "--subscription-id", type=int, action="append", default=[], help="Repair only this engagement/subscription ID. May be repeated.", ) return parser.parse_args() def main() -> int: args = parse_args() db = CommonSessionLocal() try: existing_task_count = ( select( ClientServiceTaskInstance.subscription_id.label("subscription_id"), func.count(ClientServiceTaskInstance.id).label("task_count"), ) .group_by(ClientServiceTaskInstance.subscription_id) .subquery() ) stmt = ( select(ClientServiceSubscription) .outerjoin( existing_task_count, existing_task_count.c.subscription_id == ClientServiceSubscription.id, ) .where( ClientServiceSubscription.status == "active", ClientServiceSubscription.is_active.is_(True), func.coalesce(existing_task_count.c.task_count, 0) == 0, ) .order_by(ClientServiceSubscription.tenant_id, ClientServiceSubscription.id) ) if args.tenant_id is not None: stmt = stmt.where(ClientServiceSubscription.tenant_id == args.tenant_id) if args.subscription_id: stmt = stmt.where(ClientServiceSubscription.id.in_(args.subscription_id)) engagements = db.execute(stmt).scalars().all() print(f"Candidate active engagements with zero task instances: {len(engagements)}") summary = Counter() total_created = 0 for engagement in engagements: template_count = db.execute( select(func.count(FirmServiceTaskTemplate.id)).where( FirmServiceTaskTemplate.tenant_id == engagement.tenant_id, FirmServiceTaskTemplate.service_catalogue_id == engagement.service_catalogue_id, FirmServiceTaskTemplate.is_active.is_(True), ) ).scalar_one() actor_user_id = ( engagement.updated_by_user_id or engagement.created_by_user_id or engagement.assigned_partner_user_id or engagement.assigned_manager_user_id or engagement.assigned_staff_user_id ) if not template_count: summary["no_active_templates"] += 1 print( f"SKIP engagement={engagement.id}: " "no active Firm Service Task Templates." ) continue if not actor_user_id: summary["no_actor_user"] += 1 print( f"SKIP engagement={engagement.id}: " "no valid existing actor/team user available for audit fields." ) continue created = generate_tasks_for_subscription_if_ready( db, subscription=engagement, user_id=int(actor_user_id), ) if created: total_created += created summary["repaired"] += 1 print( f"READY engagement={engagement.id}: " f"{created} task instance(s) generated from {template_count} active template(s)." ) else: summary["quality_gate_or_no_change"] += 1 print( f"NO CHANGE engagement={engagement.id}: " "AQMM acceptance gate is pending or no task was required." ) if args.apply: db.commit() print(f"COMMITTED. Total task instances generated: {total_created}") else: db.rollback() print(f"DRY RUN ONLY. Would generate {total_created} task instance(s). No database changes committed.") print("Summary:") for key in sorted(summary): print(f" {key}: {summary[key]}") return 0 except Exception: db.rollback() raise finally: db.close() if __name__ == "__main__": raise SystemExit(main())