from __future__ import annotations from datetime import date from fastapi import APIRouter, Form, Request from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy import or_, select from sqlalchemy.exc import IntegrityError from app.core.db.common import CommonSessionLocal from app.core.security.csrf import get_or_create_csrf_token, validate_csrf from app.core.security.session_auth import get_current_user from app.core.templating import templates from app.modules.clients.models import Client, ClientBusinessUnit, ClientBranch from app.modules.consultants.models import ConsultantProfile from app.modules.core.rbac.deps import get_user_permissions, get_user_roles from app.modules.core.rbac.permission_guard import require_permission from app.modules.registrations.models import * from app.modules.registrations.service import seed_registration_types, run_registration_lifecycle_once, complete_renewal router=APIRouter(prefix="/registrations", tags=["registrations-ui"]) def _ctx(request,user,db,**kw): base={"request":request,"current_user":user,"current_user_roles":get_user_roles(db,user.id),"current_user_permissions":get_user_permissions(db,user.id),"csrf_token":get_or_create_csrf_token(request)};base.update(kw);return base def _user(request,db): user=get_current_user(request,db) if not user: return None require_permission(db,user,"clients.view");return user def _tenant(request,user): return int(request.session.get("active_tenant_id") or user.tenant_id) def _branch(request,user): raw=request.session.get("active_branch_id");return int(raw) if raw not in (None,"",0,"0") else None def _can_manage(db,user): try: require_permission(db,user,"clients.edit");return True except Exception:return False def _d(value): return date.fromisoformat(value) if value else None @router.get("",response_class=HTMLResponse) def dashboard(request:Request): with CommonSessionLocal() as db: user=_user(request,db) if not user:return RedirectResponse('/login',303) seed_registration_types(db);run_registration_lifecycle_once(db) tid=_tenant(request,user); bid=_branch(request,user) regq=select(ClientRegistration,Client,RegistrationType).join(Client,Client.id==ClientRegistration.client_id).join(RegistrationType,RegistrationType.id==ClientRegistration.registration_type_id).where(ClientRegistration.tenant_id==tid) taskq=select(RegistrationRenewalTask,Client).join(Client,Client.id==RegistrationRenewalTask.client_id).where(RegistrationRenewalTask.tenant_id==tid,RegistrationRenewalTask.status.notin_(["completed","cancelled"])) if bid: regq=regq.where(ClientRegistration.branch_id==bid);taskq=taskq.where(RegistrationRenewalTask.branch_id==bid) regs=db.execute(regq.order_by(ClientRegistration.valid_until.asc().nullslast(),Client.client_name)).all();tasks=db.execute(taskq.order_by(RegistrationRenewalTask.cycle_due_date)).all() counts={"active":0,"due":0,"expired":0,"tasks":len(tasks)} for reg,_,_ in regs: if reg.status=="expired":counts["expired"]+=1 elif reg.status in {"renewal_due","update_due"}:counts["due"]+=1 else:counts["active"]+=1 return templates.TemplateResponse("modules/registrations/templates/registrations/dashboard.html",_ctx(request,user,db,registrations=regs,tasks=tasks,counts=counts,can_manage=_can_manage(db,user),today=date.today())) @router.get("/masters",response_class=HTMLResponse) def masters(request:Request): with CommonSessionLocal() as db: user=_user(request,db) if not user:return RedirectResponse('/login',303) seed_registration_types(db);tid=_tenant(request,user) return templates.TemplateResponse("modules/registrations/templates/registrations/masters.html",_ctx(request,user,db,types=db.execute(select(RegistrationType).order_by(RegistrationType.sort_order)).scalars().all(),rules=db.execute(select(RegistrationLifecycleRule,RegistrationType).join(RegistrationType).where(or_(RegistrationLifecycleRule.tenant_id.is_(None),RegistrationLifecycleRule.tenant_id==tid))).all(),templates_rows=db.execute(select(RegistrationDocumentTemplate,RegistrationType).join(RegistrationType).where(or_(RegistrationDocumentTemplate.tenant_id.is_(None),RegistrationDocumentTemplate.tenant_id==tid))).all(),can_manage=_can_manage(db,user))) @router.post("/masters/types") async def create_type(request:Request,code:str=Form(...),name:str=Form(...),category:str=Form("registration"),issuing_authority:str=Form(""),supports_expiry:bool=Form(False),supports_related_person:bool=Form(False),csrf_token:str=Form(...)): with CommonSessionLocal() as db: user=_user(request,db);validate_csrf(request,csrf_token);require_permission(db,user,"clients.edit") code=code.strip().upper();existing=db.execute(select(RegistrationType).where(RegistrationType.code==code)).scalar_one_or_none() if existing: existing.name=name.strip();existing.category=category;existing.issuing_authority=issuing_authority or None;existing.supports_expiry=supports_expiry;existing.supports_related_person=supports_related_person;existing.is_active=True else: db.add(RegistrationType(code=code,name=name.strip(),category=category,issuing_authority=issuing_authority or None,supports_expiry=supports_expiry,supports_related_person=supports_related_person,is_system=False)) db.commit();return RedirectResponse('/registrations/masters',303) @router.post("/masters/rules") async def create_rule(request:Request,registration_type_id:int=Form(...),entity_type:str=Form(""),cycle_type:str=Form(...),frequency_months:str=Form(""),renewal_window_days:int=Form(60),reminder_days_csv:str=Form("90,60,30,15,7,0"),grace_period_days:int=Form(0),requires_client_approval:bool=Form(False),requires_partner_review:bool=Form(False),auto_create_task:bool=Form(False),csrf_token:str=Form(...)): with CommonSessionLocal() as db: user=_user(request,db);validate_csrf(request,csrf_token);require_permission(db,user,"clients.edit") db.add(RegistrationLifecycleRule(tenant_id=_tenant(request,user),registration_type_id=registration_type_id,entity_type=entity_type or None,cycle_type=cycle_type,frequency_months=int(frequency_months) if frequency_months else None,renewal_window_days=renewal_window_days,reminder_days_csv=reminder_days_csv,grace_period_days=grace_period_days,requires_client_approval=requires_client_approval,requires_partner_review=requires_partner_review,auto_create_task=auto_create_task,created_by_user_id=user.id));db.commit();return RedirectResponse('/registrations/masters',303) @router.post("/masters/documents") async def create_doc_template(request:Request,registration_type_id:int=Form(...),action_type:str=Form(...),document_name:str=Form(...),description:str=Form(""),mandatory:bool=Form(False),csrf_token:str=Form(...)): with CommonSessionLocal() as db: user=_user(request,db);validate_csrf(request,csrf_token);require_permission(db,user,"clients.edit") db.add(RegistrationDocumentTemplate(tenant_id=_tenant(request,user),registration_type_id=registration_type_id,action_type=action_type,document_name=document_name.strip(),description=description or None,mandatory=mandatory));db.commit();return RedirectResponse('/registrations/masters',303) @router.get("/bulk", response_class=HTMLResponse) def bulk_registration_page( request: Request, created: int = 0, skipped: int = 0, invalid: int = 0, ): with CommonSessionLocal() as db: user = _user(request, db) if not user: return RedirectResponse("/login", 303) require_permission(db, user, "clients.edit") seed_registration_types(db) tenant_id = _tenant(request, user) branch_id = _branch(request, user) gstins_by_client = {} gstin_rows = db.execute( select( ClientRegistration.client_id, ClientRegistration.registration_number, ) .join( RegistrationType, RegistrationType.id == ClientRegistration.registration_type_id, ) .where( ClientRegistration.tenant_id == tenant_id, RegistrationType.code == "GSTIN", ) .order_by( ClientRegistration.client_id.asc(), ClientRegistration.registration_number.asc(), ) ).all() for gstin_client_id, gstin_number in gstin_rows: number = (gstin_number or "").strip() if number: gstins_by_client.setdefault(int(gstin_client_id), []).append(number) clients_query = select(Client).where( Client.tenant_id == tenant_id, Client.is_active.is_(True), ) if branch_id: clients_query = clients_query.where(Client.branch_id == branch_id) clients = db.execute( clients_query.order_by(Client.client_name, Client.client_code) ).scalars().all() client_ids = [row.id for row in clients] business_units = [] client_branches = [] if client_ids: business_units = 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() client_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() businesses_by_client: dict[int, list[ClientBusinessUnit]] = {} for row in business_units: businesses_by_client.setdefault(row.client_id, []).append(row) branches_by_client: dict[int, list[ClientBranch]] = {} for row in client_branches: branches_by_client.setdefault(row.client_id, []).append(row) types = db.execute( select(RegistrationType).where( RegistrationType.is_active.is_(True) ).order_by(RegistrationType.sort_order, RegistrationType.name) ).scalars().all() rules = db.execute( select(RegistrationLifecycleRule, RegistrationType).join( RegistrationType, RegistrationType.id == RegistrationLifecycleRule.registration_type_id, ).where( or_( RegistrationLifecycleRule.tenant_id.is_(None), RegistrationLifecycleRule.tenant_id == tenant_id, ), RegistrationLifecycleRule.is_active.is_(True), ).order_by(RegistrationType.name, RegistrationLifecycleRule.id) ).all() consultants = db.execute( select(ConsultantProfile).where( ConsultantProfile.tenant_id == tenant_id, ConsultantProfile.is_active.is_(True), ) ).scalars().all() return templates.TemplateResponse( "modules/registrations/templates/registrations/bulk.html", _ctx( request, user, db, title="Bulk Registration Addition", clients=clients, businesses_by_client=businesses_by_client, branches_by_client=branches_by_client, types=types, rules=rules, consultants=consultants, gstins_by_client=gstins_by_client, created=created, skipped=skipped, invalid=invalid, ), ) @router.post("/bulk") async def bulk_registration_submit( request: Request, client_ids: list[int] = Form([]), registration_type_id: int = Form(...), default_scope: str = Form("client"), jurisdiction: str = Form(""), state_code: str = Form(""), issue_date: str = Form(""), effective_from: str = Form(""), valid_until: str = Form(""), next_action_date: str = Form(""), lifecycle_rule_id: str = Form(""), responsible_party: str = Form("firm"), assigned_consultant_id: str = Form(""), auto_create_task: bool = Form(False), notes: str = Form(""), csrf_token: str = Form(...), ): validate_csrf(request, csrf_token) with CommonSessionLocal() as db: user = _user(request, db) if not user: return RedirectResponse("/login", 303) require_permission(db, user, "clients.edit") tenant_id = _tenant(request, user) branch_id = _branch(request, user) registration_type = db.get(RegistrationType, registration_type_id) if not registration_type or not registration_type.is_active: return RedirectResponse("/registrations/bulk?invalid=1", 303) selected_ids = list(dict.fromkeys(int(value) for value in client_ids)) if not selected_ids: return RedirectResponse("/registrations/bulk?invalid=1", 303) clients_query = select(Client).where( Client.tenant_id == tenant_id, Client.id.in_(selected_ids), Client.is_active.is_(True), ) if branch_id: clients_query = clients_query.where(Client.branch_id == branch_id) permitted_clients = db.execute(clients_query).scalars().all() clients_by_id = {row.id: row for row in permitted_clients} form = await request.form() created = 0 skipped = 0 invalid = 0 for client_id in selected_ids: client = clients_by_id.get(client_id) if not client: invalid += 1 continue registration_number = ( str(form.get(f"registration_number_{client_id}", "")) .strip() .upper() ) if not registration_number: invalid += 1 continue business_unit_id = None client_branch_id = None raw_business_id = str( form.get(f"business_unit_id_{client_id}", "") ).strip() raw_branch_id = str( form.get(f"client_branch_id_{client_id}", "") ).strip() if raw_business_id: business = db.get(ClientBusinessUnit, int(raw_business_id)) if ( not business or business.tenant_id != tenant_id or business.client_id != client.id or not business.is_active ): invalid += 1 continue business_unit_id = business.id if raw_branch_id: client_branch = db.get(ClientBranch, int(raw_branch_id)) if ( not client_branch or client_branch.tenant_id != tenant_id or client_branch.client_id != client.id or not client_branch.is_active ): invalid += 1 continue client_branch_id = client_branch.id business_unit_id = client_branch.business_unit_id if default_scope == "primary_business" and not business_unit_id: 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: invalid += 1 continue business_unit_id = business.id if default_scope == "primary_branch" and not client_branch_id: client_branch = db.execute( select(ClientBranch).where( ClientBranch.tenant_id == tenant_id, ClientBranch.client_id == client.id, ClientBranch.is_active.is_(True), ).order_by( ClientBranch.is_primary.desc(), ClientBranch.id, ) ).scalars().first() if not client_branch: invalid += 1 continue client_branch_id = client_branch.id business_unit_id = client_branch.business_unit_id existing = db.execute( select(ClientRegistration).where( ClientRegistration.tenant_id == tenant_id, ClientRegistration.registration_type_id == registration_type.id, ClientRegistration.registration_number == registration_number, ) ).scalar_one_or_none() if existing: skipped += 1 continue legal_name = str(form.get(f"legal_name_{client_id}", "")).strip() trade_name = str(form.get(f"trade_name_{client_id}", "")).strip() row_state = str(form.get(f"state_{client_id}", "")).strip() db.add( ClientRegistration( tenant_id=tenant_id, branch_id=client.branch_id, client_id=client.id, business_unit_id=business_unit_id, client_branch_id=client_branch_id, registration_type_id=registration_type.id, registration_number=registration_number, legal_name=legal_name or client.client_name, trade_name=trade_name or client.trade_name or None, state=row_state or client.state or None, jurisdiction=jurisdiction.strip() or row_state or client.state or None, state_code=state_code.strip().upper() or None, issue_date=_d(issue_date), effective_from=_d(effective_from), valid_until=_d(valid_until), next_action_date=_d(next_action_date), lifecycle_rule_id=int(lifecycle_rule_id) if lifecycle_rule_id else None, responsible_party=responsible_party, assigned_user_id=client.partner_id, assigned_consultant_id=( int(assigned_consultant_id) if assigned_consultant_id else None ), auto_create_task=auto_create_task, status="active", notes=notes.strip() or None, created_by_user_id=user.id, ) ) try: db.flush() except IntegrityError: db.rollback() skipped += 1 continue created += 1 db.commit() run_registration_lifecycle_once(db) return RedirectResponse( f"/registrations/bulk?created={created}&skipped={skipped}&invalid={invalid}", status_code=303, ) @router.get("/clients/{client_id}",response_class=HTMLResponse) def client_register(request:Request,client_id:int): with CommonSessionLocal() as db: user=_user(request,db) if not user:return RedirectResponse('/login',303) client=db.get(Client,client_id) if not client or client.tenant_id!=_tenant(request,user):return RedirectResponse('/registrations',303) seed_registration_types(db) regs=db.execute(select(ClientRegistration,RegistrationType).join(RegistrationType).where(ClientRegistration.client_id==client_id).order_by(RegistrationType.name)).all() people=db.execute(select(ClientRelatedPerson).where(ClientRelatedPerson.client_id==client_id).order_by(ClientRelatedPerson.full_name)).scalars().all() dscs=db.execute(select(ClientDigitalSignature,ClientRelatedPerson).join(ClientRelatedPerson).where(ClientDigitalSignature.client_id==client_id).order_by(ClientDigitalSignature.expires_on)).all() rules=db.execute(select(RegistrationLifecycleRule).where(or_(RegistrationLifecycleRule.tenant_id.is_(None),RegistrationLifecycleRule.tenant_id==client.tenant_id),RegistrationLifecycleRule.is_active.is_(True))).scalars().all() consultants=db.execute(select(ConsultantProfile).where(ConsultantProfile.tenant_id==client.tenant_id,ConsultantProfile.is_active.is_(True))).scalars().all() return templates.TemplateResponse("modules/registrations/templates/registrations/client.html",_ctx(request,user,db,client=client,registrations=regs,people=people,dscs=dscs,types=db.execute(select(RegistrationType).where(RegistrationType.is_active.is_(True)).order_by(RegistrationType.sort_order)).scalars().all(),rules=rules,consultants=consultants,can_manage=_can_manage(db,user))) @router.post("/clients/{client_id}/people") async def add_person(request:Request,client_id:int,person_type:str=Form(...),full_name:str=Form(...),designation:str=Form(""),pan:str=Form(""),din:str=Form(""),date_of_birth:str=Form(""),appointment_date:str=Form(""),mobile:str=Form(""),email:str=Form(""),authorised_signatory:bool=Form(False),notes:str=Form(""),csrf_token:str=Form(...)): with CommonSessionLocal() as db: user=_user(request,db);validate_csrf(request,csrf_token);require_permission(db,user,"clients.edit");c=db.get(Client,client_id) db.add(ClientRelatedPerson(tenant_id=c.tenant_id,client_id=c.id,person_type=person_type,full_name=full_name.strip(),designation=designation or None,pan=pan.strip().upper() or None,din=din or None,date_of_birth=_d(date_of_birth),appointment_date=_d(appointment_date),mobile=mobile or None,email=email or None,authorised_signatory=authorised_signatory,notes=notes or None,created_by_user_id=user.id));db.commit();return RedirectResponse(f'/registrations/clients/{client_id}',303) @router.post("/clients/{client_id}/records") async def add_registration(request:Request,client_id:int,registration_type_id:int=Form(...),related_person_id:str=Form(""),registration_number:str=Form(...),jurisdiction:str=Form(""),state_code:str=Form(""),issue_date:str=Form(""),effective_from:str=Form(""),valid_until:str=Form(""),next_action_date:str=Form(""),lifecycle_rule_id:str=Form(""),responsible_party:str=Form("firm"),assigned_user_id:str=Form(""),assigned_consultant_id:str=Form(""),auto_create_task:bool=Form(False),notes:str=Form(""),csrf_token:str=Form(...)): with CommonSessionLocal() as db: user=_user(request,db);validate_csrf(request,csrf_token);require_permission(db,user,"clients.edit");c=db.get(Client,client_id) db.add(ClientRegistration(tenant_id=c.tenant_id,branch_id=c.branch_id,client_id=c.id,registration_type_id=registration_type_id,related_person_id=int(related_person_id) if related_person_id else None,registration_number=registration_number.strip().upper(),jurisdiction=jurisdiction or None,state_code=state_code or None,issue_date=_d(issue_date),effective_from=_d(effective_from),valid_until=_d(valid_until),next_action_date=_d(next_action_date),lifecycle_rule_id=int(lifecycle_rule_id) if lifecycle_rule_id else None,responsible_party=responsible_party,assigned_user_id=int(assigned_user_id) if assigned_user_id else c.partner_id,assigned_consultant_id=int(assigned_consultant_id) if assigned_consultant_id else None,auto_create_task=auto_create_task,notes=notes or None,created_by_user_id=user.id));db.commit();run_registration_lifecycle_once(db);return RedirectResponse(f'/registrations/clients/{client_id}',303) @router.post("/clients/{client_id}/dsc") async def add_dsc(request:Request,client_id:int,related_person_id:int=Form(...),certificate_serial_number:str=Form(""),provider:str=Form(""),token_make:str=Form(""),class_type:str=Form(""),issued_on:str=Form(""),expires_on:str=Form(...),custody_location:str=Form(""),renewal_responsibility:str=Form("firm"),assigned_consultant_id:str=Form(""),notes:str=Form(""),csrf_token:str=Form(...)): with CommonSessionLocal() as db: user=_user(request,db);validate_csrf(request,csrf_token);require_permission(db,user,"clients.edit");c=db.get(Client,client_id) expiry=_d(expires_on) dsc=ClientDigitalSignature(tenant_id=c.tenant_id,client_id=c.id,related_person_id=related_person_id,certificate_serial_number=certificate_serial_number or None,provider=provider or None,token_make=token_make or None,class_type=class_type or None,issued_on=_d(issued_on),expires_on=expiry,custody_location=custody_location or None,renewal_responsibility=renewal_responsibility,assigned_consultant_id=int(assigned_consultant_id) if assigned_consultant_id else None,notes=notes or None,created_by_user_id=user.id) db.add(dsc) dsc_type=db.execute(select(RegistrationType).where(RegistrationType.code=="DSC")).scalar_one() serial=(certificate_serial_number or f"DSC-{client_id}-{related_person_id}-{expiry.isoformat()}").strip().upper() existing=db.execute(select(ClientRegistration).where(ClientRegistration.tenant_id==c.tenant_id,ClientRegistration.registration_type_id==dsc_type.id,ClientRegistration.registration_number==serial)).scalar_one_or_none() if not existing: db.add(ClientRegistration(tenant_id=c.tenant_id,branch_id=c.branch_id,client_id=c.id,registration_type_id=dsc_type.id,related_person_id=related_person_id,registration_number=serial,issue_date=_d(issued_on),effective_from=_d(issued_on),valid_until=expiry,responsible_party=renewal_responsibility,assigned_user_id=c.partner_id,assigned_consultant_id=int(assigned_consultant_id) if assigned_consultant_id else None,auto_create_task=True,notes=notes or None,created_by_user_id=user.id)) db.commit();run_registration_lifecycle_once(db);return RedirectResponse(f'/registrations/clients/{client_id}',303) @router.get("/tasks/{task_id}",response_class=HTMLResponse) def task_detail(request:Request,task_id:int): with CommonSessionLocal() as db: user=_user(request,db) if not user:return RedirectResponse('/login',303) task=db.get(RegistrationRenewalTask,task_id) if not task or task.tenant_id!=_tenant(request,user):return RedirectResponse('/registrations',303) reg=db.get(ClientRegistration,task.registration_id);client=db.get(Client,task.client_id);rt=db.get(RegistrationType,reg.registration_type_id) docs=db.execute(select(RegistrationRenewalTaskDocument).where(RegistrationRenewalTaskDocument.renewal_task_id==task.id)).scalars().all() actions=db.execute(select(ClientRegistrationAction).where(ClientRegistrationAction.registration_id==reg.id).order_by(ClientRegistrationAction.created_at_utc.desc())).scalars().all() return templates.TemplateResponse("modules/registrations/templates/registrations/task.html",_ctx(request,user,db,task=task,registration=reg,client=client,registration_type=rt,documents=docs,actions=actions,can_manage=_can_manage(db,user))) @router.post("/tasks/{task_id}/status") async def update_task(request:Request,task_id:int,status:str=Form(...),submitted_on:str=Form(""),acknowledgement_number:str=Form(""),notes:str=Form(""),csrf_token:str=Form(...)): with CommonSessionLocal() as db: user=_user(request,db);validate_csrf(request,csrf_token);require_permission(db,user,"clients.edit");task=db.get(RegistrationRenewalTask,task_id);task.status=status;task.submitted_on=_d(submitted_on);task.acknowledgement_number=acknowledgement_number or None;task.notes=notes or None;db.commit();return RedirectResponse(f'/registrations/tasks/{task_id}',303) @router.post("/tasks/{task_id}/complete") async def complete(request:Request,task_id:int,new_valid_until:str=Form(""),acknowledgement_number:str=Form(""),notes:str=Form(""),csrf_token:str=Form(...)): with CommonSessionLocal() as db: user=_user(request,db);validate_csrf(request,csrf_token);require_permission(db,user,"clients.edit");task=db.get(RegistrationRenewalTask,task_id);complete_renewal(db,task,new_valid_until=_d(new_valid_until),acknowledgement_number=acknowledgement_number or None,actor_user_id=user.id,notes=notes or None);return RedirectResponse(f'/registrations/tasks/{task_id}',303) @router.post("/run-engine") async def run_engine(request:Request,csrf_token:str=Form(...)): with CommonSessionLocal() as db: user=_user(request,db);validate_csrf(request,csrf_token);require_permission(db,user,"clients.edit");run_registration_lifecycle_once(db);return RedirectResponse('/registrations',303)