114 lines
9.1 KiB
Python
114 lines
9.1 KiB
Python
from __future__ import annotations
|
|
from datetime import date, datetime, timedelta, timezone
|
|
from calendar import monthrange
|
|
from sqlalchemy import and_, or_, select
|
|
from sqlalchemy.orm import Session
|
|
from app.modules.alerts.service import create_alert
|
|
from app.modules.clients.models import Client
|
|
from app.modules.consultants.models import ConsultantProfile
|
|
from app.modules.registrations.models import (
|
|
RegistrationType, ClientRegistration, ClientRelatedPerson, ClientDigitalSignature,
|
|
RegistrationLifecycleRule, RegistrationDocumentTemplate, RegistrationRenewalTask,
|
|
RegistrationRenewalTaskDocument, ClientRegistrationAction,
|
|
)
|
|
|
|
DEFAULT_TYPES = [
|
|
("PAN","PAN","Income Tax Department",False,False),
|
|
("TAN","TAN","Income Tax Department",False,False),
|
|
("GSTIN","GST Registration","GST Department",False,False),
|
|
("CIN","Company Registration","Ministry of Corporate Affairs",False,False),
|
|
("LLPIN","LLP Registration","Ministry of Corporate Affairs",False,False),
|
|
("DSC","Digital Signature Certificate","Certifying Authority",True,True),
|
|
("UDYAM","Udyam / MSME Registration","Ministry of MSME",False,False),
|
|
("FSSAI","FSSAI Licence","FSSAI",True,False),
|
|
("IEC","Import Export Code","DGFT",False,False),
|
|
("SHOP_ACT","Shops and Establishment","State Authority",True,False),
|
|
("TRADE_LICENCE","Trade Licence","Local Authority",True,False),
|
|
("PT","Professional Tax","State Authority",True,False),
|
|
("PF","EPFO Registration","EPFO",False,False),
|
|
("ESI","ESIC Registration","ESIC",False,False),
|
|
("12AB","Section 12AB Registration","Income Tax Department",True,False),
|
|
("80G","Section 80G Approval","Income Tax Department",True,False),
|
|
("FCRA","FCRA Registration","Ministry of Home Affairs",True,False),
|
|
("RERA","RERA Registration","RERA Authority",True,False),
|
|
]
|
|
|
|
def seed_registration_types(db: Session) -> None:
|
|
existing={x for x in db.execute(select(RegistrationType.code)).scalars().all()}
|
|
for i,(code,name,authority,expiry,person) in enumerate(DEFAULT_TYPES,1):
|
|
if code not in existing:
|
|
db.add(RegistrationType(code=code,name=name,issuing_authority=authority,supports_expiry=expiry,supports_related_person=person,sort_order=i*10))
|
|
db.commit()
|
|
|
|
def add_months(value: date, months: int) -> date:
|
|
m=value.month-1+months; y=value.year+m//12; m=m%12+1
|
|
return date(y,m,min(value.day,monthrange(y,m)[1]))
|
|
|
|
def resolve_next_action(reg: ClientRegistration, rule: RegistrationLifecycleRule|None) -> date|None:
|
|
if reg.valid_until and rule and rule.cycle_type=="expiry_renewal":
|
|
return reg.valid_until-timedelta(days=max(0,rule.renewal_window_days))
|
|
base=reg.effective_from or reg.issue_date
|
|
if base and rule and rule.frequency_months:
|
|
candidate=add_months(base,rule.frequency_months)
|
|
while candidate<=date.today(): candidate=add_months(candidate,rule.frequency_months)
|
|
return candidate
|
|
return reg.next_action_date
|
|
|
|
def create_task_documents(db: Session, task: RegistrationRenewalTask, registration_type_id: int) -> None:
|
|
rows=db.execute(select(RegistrationDocumentTemplate).where(RegistrationDocumentTemplate.registration_type_id==registration_type_id,RegistrationDocumentTemplate.action_type==task.action_type,RegistrationDocumentTemplate.is_active.is_(True)).order_by(RegistrationDocumentTemplate.sort_order)).scalars().all()
|
|
for row in rows:
|
|
db.add(RegistrationRenewalTaskDocument(renewal_task_id=task.id,template_id=row.id,document_name=row.document_name,mandatory=row.mandatory))
|
|
|
|
def _alert_once(db: Session,user_id:int|None,title:str,message:str,target_url:str,tenant_id:int,branch_id:int|None):
|
|
if not user_id:return
|
|
today=datetime.now(timezone.utc).date()
|
|
from app.modules.alerts.models import UserAlert
|
|
exists=db.execute(select(UserAlert.id).where(UserAlert.user_id==user_id,UserAlert.title==title,UserAlert.target_url==target_url,UserAlert.created_at_utc>=datetime.combine(today,datetime.min.time(),tzinfo=timezone.utc))).scalar_one_or_none()
|
|
if not exists:create_alert(db,user_id=user_id,title=title,message=message,tenant_id=tenant_id,branch_id=branch_id,role_context="registrations",alert_type="registration_expiry",priority="high",target_url=target_url,commit=False)
|
|
|
|
def run_registration_lifecycle_once(db: Session, today: date|None=None) -> dict:
|
|
today=today or date.today(); created=alerts=0
|
|
regs=db.execute(select(ClientRegistration).where(ClientRegistration.status.in_(["active","renewal_due","update_due"]))).scalars().all()
|
|
for reg in regs:
|
|
rule=db.get(RegistrationLifecycleRule,reg.lifecycle_rule_id) if reg.lifecycle_rule_id else None
|
|
if not rule:
|
|
rule=db.execute(select(RegistrationLifecycleRule).join(Client, Client.id==reg.client_id).where(RegistrationLifecycleRule.registration_type_id==reg.registration_type_id,RegistrationLifecycleRule.is_active.is_(True),or_(RegistrationLifecycleRule.tenant_id.is_(None),RegistrationLifecycleRule.tenant_id==reg.tenant_id),or_(RegistrationLifecycleRule.entity_type.is_(None),RegistrationLifecycleRule.entity_type==Client.client_type)).order_by(RegistrationLifecycleRule.tenant_id.desc(),RegistrationLifecycleRule.id.desc())).scalars().first()
|
|
due=reg.valid_until or reg.next_action_date
|
|
if not due: continue
|
|
next_action=resolve_next_action(reg,rule)
|
|
if next_action and reg.next_action_date!=next_action: reg.next_action_date=next_action
|
|
window=(rule.renewal_window_days if rule else 60)
|
|
if due-timedelta(days=window)<=today and reg.auto_create_task:
|
|
task=db.execute(select(RegistrationRenewalTask).where(RegistrationRenewalTask.registration_id==reg.id,RegistrationRenewalTask.cycle_due_date==due)).scalar_one_or_none()
|
|
if not task:
|
|
rt=db.get(RegistrationType,reg.registration_type_id); client=db.get(Client,reg.client_id)
|
|
action="renewal" if reg.valid_until else "update"
|
|
task=RegistrationRenewalTask(tenant_id=reg.tenant_id,branch_id=reg.branch_id,client_id=reg.client_id,registration_id=reg.id,lifecycle_rule_id=rule.id if rule else None,cycle_due_date=due,action_type=action,title=f"{action.title()} - {rt.name} - {client.client_name}",priority="critical" if due<=today else "high",assigned_user_id=reg.assigned_user_id,assigned_consultant_id=reg.assigned_consultant_id,client_approval_required=bool(rule and rule.requires_client_approval),partner_review_required=bool(rule and rule.requires_partner_review))
|
|
db.add(task);db.flush();create_task_documents(db,task,reg.registration_type_id);created+=1
|
|
client=db.get(Client,reg.client_id)
|
|
days=(due-today).days
|
|
title=f"{db.get(RegistrationType,reg.registration_type_id).name} {'expired' if days<0 else 'due'}: {client.client_name}"
|
|
msg=f"Registration {reg.registration_number} is {'overdue by '+str(abs(days))+' day(s)' if days<0 else 'due in '+str(days)+' day(s)'} on {due:%d-%m-%Y}."
|
|
reminder_days={int(x.strip()) for x in ((rule.reminder_days_csv if rule else "60,30,15,7,0") or "").split(",") if x.strip().lstrip("-").isdigit()}
|
|
should_alert = days in reminder_days or days < 0
|
|
if should_alert:
|
|
_alert_once(db,reg.assigned_user_id or client.partner_id,title,msg,f"/registrations/tasks/{task.id}",reg.tenant_id,reg.branch_id);alerts+=1
|
|
if should_alert and reg.assigned_consultant_id:
|
|
cp=db.get(ConsultantProfile,reg.assigned_consultant_id); _alert_once(db,cp.user_id if cp else None,title,msg,f"/registrations/tasks/{task.id}",reg.tenant_id,reg.branch_id)
|
|
reg.status="expired" if due<today else ("renewal_due" if reg.valid_until else "update_due")
|
|
db.commit();return {"tasks_created":created,"alerts_processed":alerts}
|
|
|
|
def complete_renewal(db:Session, task:RegistrationRenewalTask, *, new_valid_until:date|None, acknowledgement_number:str|None, actor_user_id:int, notes:str|None):
|
|
reg=db.get(ClientRegistration,task.registration_id); previous=reg.valid_until
|
|
task.status="completed"; task.completed_on=date.today(); task.acknowledgement_number=acknowledgement_number; task.notes=notes
|
|
if new_valid_until: reg.valid_until=new_valid_until
|
|
rt=db.get(RegistrationType,reg.registration_type_id)
|
|
if rt and rt.code=="DSC" and new_valid_until:
|
|
dsc=db.execute(select(ClientDigitalSignature).where(ClientDigitalSignature.client_id==reg.client_id,ClientDigitalSignature.related_person_id==reg.related_person_id).order_by(ClientDigitalSignature.id.desc())).scalars().first()
|
|
if dsc:
|
|
dsc.expires_on=new_valid_until; dsc.status="active"
|
|
rule=db.get(RegistrationLifecycleRule,task.lifecycle_rule_id) if task.lifecycle_rule_id else None
|
|
reg.status="active"; reg.next_action_date=resolve_next_action(reg,rule)
|
|
db.add(ClientRegistrationAction(tenant_id=reg.tenant_id,client_id=reg.client_id,registration_id=reg.id,renewal_task_id=task.id,action_type=task.action_type,previous_valid_until=previous,new_valid_until=new_valid_until,submission_date=task.submitted_on,acknowledgement_number=acknowledgement_number,completion_date=date.today(),status="completed",performed_by_user_id=actor_user_id,notes=notes))
|
|
db.commit()
|