Add phase 4 registration and renewal lifecycle management
This commit is contained in:
@@ -167,6 +167,14 @@ def run_notification_escalation_once(*, db: Session | None = None, now: datetime
|
||||
result = AutomationResult()
|
||||
|
||||
try:
|
||||
# Phase 4: materialize registration renewal/update work before alert processing.
|
||||
try:
|
||||
if _table_exists("registration_renewal_tasks"):
|
||||
from app.modules.registrations.service import run_registration_lifecycle_once
|
||||
run_registration_lifecycle_once(session, today=now_utc.date())
|
||||
except Exception:
|
||||
logger.exception("Registration lifecycle automation failed")
|
||||
|
||||
if not _table_exists("user_alerts"):
|
||||
logger.warning("Phase 7O skipped: user_alerts table is not available. Apply Phase 7H first.")
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
from __future__ import annotations
|
||||
from datetime import date, datetime, timezone
|
||||
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Integer, JSON, Numeric, String, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from app.core.db.common import CommonBase
|
||||
|
||||
|
||||
def utcnow(): return datetime.now(timezone.utc)
|
||||
|
||||
class RegistrationType(CommonBase):
|
||||
__tablename__="registration_types"
|
||||
__table_args__=(UniqueConstraint("code", name="uq_registration_types_code"),)
|
||||
id: Mapped[int]=mapped_column(Integer, primary_key=True)
|
||||
code: Mapped[str]=mapped_column(String(60), nullable=False, index=True)
|
||||
name: Mapped[str]=mapped_column(String(160), nullable=False)
|
||||
category: Mapped[str]=mapped_column(String(80), nullable=False, default="registration")
|
||||
issuing_authority: Mapped[str|None]=mapped_column(String(200))
|
||||
identifier_label: Mapped[str]=mapped_column(String(100), nullable=False, default="Registration Number")
|
||||
supports_expiry: Mapped[bool]=mapped_column(Boolean, nullable=False, default=False)
|
||||
supports_related_person: Mapped[bool]=mapped_column(Boolean, nullable=False, default=False)
|
||||
is_system: Mapped[bool]=mapped_column(Boolean, nullable=False, default=True)
|
||||
is_active: Mapped[bool]=mapped_column(Boolean, nullable=False, default=True)
|
||||
sort_order: Mapped[int]=mapped_column(Integer, nullable=False, default=0)
|
||||
created_at_utc: Mapped[datetime]=mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
|
||||
updated_at_utc: Mapped[datetime]=mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False)
|
||||
|
||||
class ClientRelatedPerson(CommonBase):
|
||||
__tablename__="client_related_persons"
|
||||
id: Mapped[int]=mapped_column(Integer, primary_key=True)
|
||||
tenant_id: Mapped[int]=mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
client_id: Mapped[int]=mapped_column(ForeignKey("clients.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
person_type: Mapped[str]=mapped_column(String(60), nullable=False, index=True)
|
||||
full_name: Mapped[str]=mapped_column(String(200), nullable=False)
|
||||
designation: Mapped[str|None]=mapped_column(String(120))
|
||||
pan: Mapped[str|None]=mapped_column(String(20), index=True)
|
||||
din: Mapped[str|None]=mapped_column(String(20), index=True)
|
||||
date_of_birth: Mapped[date|None]=mapped_column(Date)
|
||||
appointment_date: Mapped[date|None]=mapped_column(Date)
|
||||
cessation_date: Mapped[date|None]=mapped_column(Date)
|
||||
mobile: Mapped[str|None]=mapped_column(String(20))
|
||||
email: Mapped[str|None]=mapped_column(String(255))
|
||||
authorised_signatory: Mapped[bool]=mapped_column(Boolean, nullable=False, default=False)
|
||||
is_active: Mapped[bool]=mapped_column(Boolean, nullable=False, default=True)
|
||||
notes: Mapped[str|None]=mapped_column(Text)
|
||||
created_by_user_id: Mapped[int|None]=mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
|
||||
created_at_utc: Mapped[datetime]=mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
|
||||
updated_at_utc: Mapped[datetime]=mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False)
|
||||
|
||||
class ClientRegistration(CommonBase):
|
||||
__tablename__="client_registrations"
|
||||
__table_args__=(UniqueConstraint("tenant_id","registration_type_id","registration_number", name="uq_client_registration_number"),)
|
||||
id: Mapped[int]=mapped_column(Integer, primary_key=True)
|
||||
tenant_id: Mapped[int]=mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
branch_id: Mapped[int|None]=mapped_column(ForeignKey("branches.id", ondelete="SET NULL"), index=True)
|
||||
client_id: Mapped[int]=mapped_column(ForeignKey("clients.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
registration_type_id: Mapped[int]=mapped_column(ForeignKey("registration_types.id"), nullable=False, index=True)
|
||||
related_person_id: Mapped[int|None]=mapped_column(ForeignKey("client_related_persons.id", ondelete="SET NULL"), index=True)
|
||||
registration_number: Mapped[str]=mapped_column(String(120), nullable=False, index=True)
|
||||
jurisdiction: Mapped[str|None]=mapped_column(String(160))
|
||||
state_code: Mapped[str|None]=mapped_column(String(10))
|
||||
issue_date: Mapped[date|None]=mapped_column(Date)
|
||||
effective_from: Mapped[date|None]=mapped_column(Date)
|
||||
valid_until: Mapped[date|None]=mapped_column(Date, index=True)
|
||||
next_action_date: Mapped[date|None]=mapped_column(Date, index=True)
|
||||
status: Mapped[str]=mapped_column(String(40), nullable=False, default="active", index=True)
|
||||
primary_registration: Mapped[bool]=mapped_column(Boolean, nullable=False, default=False)
|
||||
lifecycle_rule_id: Mapped[int|None]=mapped_column(ForeignKey("registration_lifecycle_rules.id", use_alter=True, name="fk_client_registration_rule"), index=True)
|
||||
responsible_party: Mapped[str]=mapped_column(String(30), nullable=False, default="firm")
|
||||
assigned_user_id: Mapped[int|None]=mapped_column(ForeignKey("users.id", ondelete="SET NULL"), index=True)
|
||||
assigned_consultant_id: Mapped[int|None]=mapped_column(ForeignKey("consultant_profiles.id", ondelete="SET NULL"), index=True)
|
||||
auto_create_task: Mapped[bool]=mapped_column(Boolean, nullable=False, default=True)
|
||||
portal_url: Mapped[str|None]=mapped_column(String(500))
|
||||
notes: Mapped[str|None]=mapped_column(Text)
|
||||
metadata_json: Mapped[dict|None]=mapped_column(JSON)
|
||||
created_by_user_id: Mapped[int|None]=mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
|
||||
created_at_utc: Mapped[datetime]=mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
|
||||
updated_at_utc: Mapped[datetime]=mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False)
|
||||
|
||||
class ClientDigitalSignature(CommonBase):
|
||||
__tablename__="client_digital_signatures"
|
||||
id: Mapped[int]=mapped_column(Integer, primary_key=True)
|
||||
tenant_id: Mapped[int]=mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
client_id: Mapped[int]=mapped_column(ForeignKey("clients.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
related_person_id: Mapped[int]=mapped_column(ForeignKey("client_related_persons.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
certificate_serial_number: Mapped[str|None]=mapped_column(String(200), index=True)
|
||||
provider: Mapped[str|None]=mapped_column(String(120))
|
||||
token_make: Mapped[str|None]=mapped_column(String(120))
|
||||
class_type: Mapped[str|None]=mapped_column(String(60))
|
||||
issued_on: Mapped[date|None]=mapped_column(Date)
|
||||
expires_on: Mapped[date]=mapped_column(Date, nullable=False, index=True)
|
||||
status: Mapped[str]=mapped_column(String(30), nullable=False, default="active", index=True)
|
||||
custody_location: Mapped[str|None]=mapped_column(String(200))
|
||||
renewal_responsibility: Mapped[str]=mapped_column(String(30), nullable=False, default="firm")
|
||||
assigned_consultant_id: Mapped[int|None]=mapped_column(ForeignKey("consultant_profiles.id", ondelete="SET NULL"), index=True)
|
||||
notes: Mapped[str|None]=mapped_column(Text)
|
||||
created_by_user_id: Mapped[int|None]=mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
|
||||
created_at_utc: Mapped[datetime]=mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
|
||||
updated_at_utc: Mapped[datetime]=mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False)
|
||||
|
||||
class RegistrationLifecycleRule(CommonBase):
|
||||
__tablename__="registration_lifecycle_rules"
|
||||
id: Mapped[int]=mapped_column(Integer, primary_key=True)
|
||||
tenant_id: Mapped[int|None]=mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), index=True)
|
||||
registration_type_id: Mapped[int]=mapped_column(ForeignKey("registration_types.id"), nullable=False, index=True)
|
||||
entity_type: Mapped[str|None]=mapped_column(String(100), index=True)
|
||||
jurisdiction: Mapped[str|None]=mapped_column(String(160))
|
||||
state_code: Mapped[str|None]=mapped_column(String(10))
|
||||
cycle_type: Mapped[str]=mapped_column(String(40), nullable=False, default="expiry_renewal")
|
||||
frequency_months: Mapped[int|None]=mapped_column(Integer)
|
||||
renewal_window_days: Mapped[int]=mapped_column(Integer, nullable=False, default=60)
|
||||
reminder_days_csv: Mapped[str]=mapped_column(String(100), nullable=False, default="90,60,30,15,7,0")
|
||||
grace_period_days: Mapped[int]=mapped_column(Integer, nullable=False, default=0)
|
||||
service_catalogue_id: Mapped[int|None]=mapped_column(ForeignKey("service_catalogue.id", ondelete="SET NULL"), index=True)
|
||||
requires_client_approval: Mapped[bool]=mapped_column(Boolean, nullable=False, default=False)
|
||||
requires_partner_review: Mapped[bool]=mapped_column(Boolean, nullable=False, default=False)
|
||||
auto_create_task: Mapped[bool]=mapped_column(Boolean, nullable=False, default=True)
|
||||
is_active: Mapped[bool]=mapped_column(Boolean, nullable=False, default=True)
|
||||
effective_from: Mapped[date|None]=mapped_column(Date)
|
||||
effective_to: Mapped[date|None]=mapped_column(Date)
|
||||
created_by_user_id: Mapped[int|None]=mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
|
||||
created_at_utc: Mapped[datetime]=mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
|
||||
updated_at_utc: Mapped[datetime]=mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False)
|
||||
|
||||
class RegistrationDocumentTemplate(CommonBase):
|
||||
__tablename__="registration_document_templates"
|
||||
id: Mapped[int]=mapped_column(Integer, primary_key=True)
|
||||
tenant_id: Mapped[int|None]=mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), index=True)
|
||||
registration_type_id: Mapped[int]=mapped_column(ForeignKey("registration_types.id"), nullable=False, index=True)
|
||||
action_type: Mapped[str]=mapped_column(String(40), nullable=False, default="renewal", index=True)
|
||||
document_name: Mapped[str]=mapped_column(String(200), nullable=False)
|
||||
description: Mapped[str|None]=mapped_column(Text)
|
||||
mandatory: Mapped[bool]=mapped_column(Boolean, nullable=False, default=True)
|
||||
sort_order: Mapped[int]=mapped_column(Integer, nullable=False, default=0)
|
||||
is_active: Mapped[bool]=mapped_column(Boolean, nullable=False, default=True)
|
||||
|
||||
class RegistrationRenewalTask(CommonBase):
|
||||
__tablename__="registration_renewal_tasks"
|
||||
__table_args__=(UniqueConstraint("registration_id","cycle_due_date", name="uq_registration_renewal_cycle"),)
|
||||
id: Mapped[int]=mapped_column(Integer, primary_key=True)
|
||||
tenant_id: Mapped[int]=mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
branch_id: Mapped[int|None]=mapped_column(ForeignKey("branches.id", ondelete="SET NULL"), index=True)
|
||||
client_id: Mapped[int]=mapped_column(ForeignKey("clients.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
registration_id: Mapped[int]=mapped_column(ForeignKey("client_registrations.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
lifecycle_rule_id: Mapped[int|None]=mapped_column(ForeignKey("registration_lifecycle_rules.id", ondelete="SET NULL"), index=True)
|
||||
cycle_due_date: Mapped[date]=mapped_column(Date, nullable=False, index=True)
|
||||
action_type: Mapped[str]=mapped_column(String(40), nullable=False, default="renewal")
|
||||
title: Mapped[str]=mapped_column(String(255), nullable=False)
|
||||
status: Mapped[str]=mapped_column(String(40), nullable=False, default="pending", index=True)
|
||||
priority: Mapped[str]=mapped_column(String(20), nullable=False, default="normal")
|
||||
assigned_user_id: Mapped[int|None]=mapped_column(ForeignKey("users.id", ondelete="SET NULL"), index=True)
|
||||
assigned_consultant_id: Mapped[int|None]=mapped_column(ForeignKey("consultant_profiles.id", ondelete="SET NULL"), index=True)
|
||||
client_approval_required: Mapped[bool]=mapped_column(Boolean, nullable=False, default=False)
|
||||
partner_review_required: Mapped[bool]=mapped_column(Boolean, nullable=False, default=False)
|
||||
submitted_on: Mapped[date|None]=mapped_column(Date)
|
||||
acknowledgement_number: Mapped[str|None]=mapped_column(String(160))
|
||||
completed_on: Mapped[date|None]=mapped_column(Date)
|
||||
notes: Mapped[str|None]=mapped_column(Text)
|
||||
created_at_utc: Mapped[datetime]=mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
|
||||
updated_at_utc: Mapped[datetime]=mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False)
|
||||
|
||||
class RegistrationRenewalTaskDocument(CommonBase):
|
||||
__tablename__="registration_renewal_task_documents"
|
||||
id: Mapped[int]=mapped_column(Integer, primary_key=True)
|
||||
renewal_task_id: Mapped[int]=mapped_column(ForeignKey("registration_renewal_tasks.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
template_id: Mapped[int|None]=mapped_column(ForeignKey("registration_document_templates.id", ondelete="SET NULL"))
|
||||
document_name: Mapped[str]=mapped_column(String(200), nullable=False)
|
||||
mandatory: Mapped[bool]=mapped_column(Boolean, nullable=False, default=True)
|
||||
status: Mapped[str]=mapped_column(String(30), nullable=False, default="pending")
|
||||
document_id: Mapped[int|None]=mapped_column(Integer)
|
||||
remarks: Mapped[str|None]=mapped_column(Text)
|
||||
|
||||
class ClientRegistrationAction(CommonBase):
|
||||
__tablename__="client_registration_actions"
|
||||
id: Mapped[int]=mapped_column(Integer, primary_key=True)
|
||||
tenant_id: Mapped[int]=mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
client_id: Mapped[int]=mapped_column(ForeignKey("clients.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
registration_id: Mapped[int]=mapped_column(ForeignKey("client_registrations.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
renewal_task_id: Mapped[int|None]=mapped_column(ForeignKey("registration_renewal_tasks.id", ondelete="SET NULL"), index=True)
|
||||
action_type: Mapped[str]=mapped_column(String(40), nullable=False, index=True)
|
||||
previous_valid_until: Mapped[date|None]=mapped_column(Date)
|
||||
new_valid_until: Mapped[date|None]=mapped_column(Date)
|
||||
application_date: Mapped[date|None]=mapped_column(Date)
|
||||
submission_date: Mapped[date|None]=mapped_column(Date)
|
||||
acknowledgement_number: Mapped[str|None]=mapped_column(String(160))
|
||||
completion_date: Mapped[date|None]=mapped_column(Date)
|
||||
status: Mapped[str]=mapped_column(String(40), nullable=False, default="completed")
|
||||
performed_by_user_id: Mapped[int|None]=mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
|
||||
performed_by_consultant_id: Mapped[int|None]=mapped_column(ForeignKey("consultant_profiles.id", ondelete="SET NULL"))
|
||||
notes: Mapped[str|None]=mapped_column(Text)
|
||||
created_at_utc: Mapped[datetime]=mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
|
||||
@@ -0,0 +1,113 @@
|
||||
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()
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{% extends "ui/templates/base/layout.html" %}{% block content %}<div class="space-y-6"><div class="flex items-center justify-between"><div><h1 class="text-2xl font-semibold">Registrations & Renewals</h1><p class="text-sm text-slate-500">Expiry, periodic updates, generated renewal work and escalation.</p></div>{% if can_manage %}<div class="flex gap-2"><a href="/registrations/masters" class="rounded border px-3 py-2">Lifecycle Masters</a><form method="post" action="/registrations/run-engine"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="rounded bg-brand-600 px-3 py-2 text-white">Run Lifecycle Engine</button></form></div>{% endif %}</div><div class="grid gap-4 md:grid-cols-4">{% for label,value in [('Active',counts.active),('Due / Update Due',counts.due),('Expired',counts.expired),('Open Renewal Tasks',counts.tasks)] %}<div class="rounded-xl border bg-white p-4"><div class="text-sm text-slate-500">{{ label }}</div><div class="text-3xl font-semibold">{{ value }}</div></div>{% endfor %}</div><div class="rounded-xl border bg-white"><div class="border-b p-4 font-semibold">Open renewal and update work</div><div class="overflow-x-auto"><table class="min-w-full text-sm"><thead><tr class="bg-slate-50"><th class="p-3 text-left">Due</th><th class="p-3 text-left">Client</th><th class="p-3 text-left">Work</th><th class="p-3 text-left">Status</th></tr></thead><tbody>{% for task,client in tasks %}<tr class="border-t"><td class="p-3">{{ task.cycle_due_date }}</td><td class="p-3">{{ client.client_name }}</td><td class="p-3"><a class="text-brand-600" href="/registrations/tasks/{{ task.id }}">{{ task.title }}</a></td><td class="p-3">{{ task.status|replace('_',' ')|title }}</td></tr>{% else %}<tr><td colspan="4" class="p-6 text-center text-slate-500">No open lifecycle work.</td></tr>{% endfor %}</tbody></table></div></div><div class="rounded-xl border bg-white"><div class="border-b p-4 font-semibold">Client registrations</div><div class="overflow-x-auto"><table class="min-w-full text-sm"><thead><tr class="bg-slate-50"><th class="p-3 text-left">Client</th><th class="p-3 text-left">Type</th><th class="p-3 text-left">Number</th><th class="p-3 text-left">Expiry / Next action</th><th class="p-3 text-left">Status</th></tr></thead><tbody>{% for reg,client,rt in registrations %}<tr class="border-t"><td class="p-3"><a class="text-brand-600" href="/registrations/clients/{{ client.id }}">{{ client.client_name }}</a></td><td class="p-3">{{ rt.name }}</td><td class="p-3">{{ reg.registration_number }}</td><td class="p-3">{{ reg.valid_until or reg.next_action_date or '—' }}</td><td class="p-3">{{ reg.status|replace('_',' ')|title }}</td></tr>{% else %}<tr><td colspan="5" class="p-6 text-center text-slate-500">No registration records.</td></tr>{% endfor %}</tbody></table></div></div></div>{% endblock %}
|
||||
@@ -0,0 +1 @@
|
||||
{% extends "ui/templates/base/layout.html" %}{% block content %}<div class="space-y-6"><div><h1 class="text-2xl font-semibold">Registration Lifecycle Masters</h1><a class="text-sm text-brand-600" href="/registrations">← Dashboard</a></div>{% if can_manage %}<div class="grid gap-4 lg:grid-cols-3"><form method="post" action="/registrations/masters/types" class="space-y-3 rounded-xl border bg-white p-4"><h2 class="font-semibold">Registration Type</h2><input type="hidden" name="csrf_token" value="{{csrf_token}}"><input class="w-full rounded border p-2" name="code" placeholder="Code" required><input class="w-full rounded border p-2" name="name" placeholder="Name" required><input class="w-full rounded border p-2" name="issuing_authority" placeholder="Issuing authority"><label class="block"><input type="checkbox" name="supports_expiry"> Expiry based</label><label class="block"><input type="checkbox" name="supports_related_person"> Person based</label><button class="rounded bg-brand-600 px-3 py-2 text-white">Save Type</button></form><form method="post" action="/registrations/masters/rules" class="space-y-3 rounded-xl border bg-white p-4"><h2 class="font-semibold">Lifecycle Rule</h2><input type="hidden" name="csrf_token" value="{{csrf_token}}"><select class="w-full rounded border p-2" name="registration_type_id">{% for x in types %}<option value="{{x.id}}">{{x.name}}</option>{% endfor %}</select><input class="w-full rounded border p-2" name="entity_type" placeholder="Entity type (blank = all)"><select class="w-full rounded border p-2" name="cycle_type"><option value="expiry_renewal">Expiry renewal</option><option value="annual_update">Annual update</option><option value="annual_confirmation">Annual confirmation</option><option value="periodic_revalidation">Periodic revalidation</option><option value="event_based_update">Event based update</option><option value="no_expiry">No expiry</option></select><input class="w-full rounded border p-2" name="frequency_months" type="number" placeholder="Frequency months"><input class="w-full rounded border p-2" name="renewal_window_days" type="number" value="60"><input class="w-full rounded border p-2" name="reminder_days_csv" value="90,60,30,15,7,0"><label><input type="checkbox" name="auto_create_task" checked> Auto-create renewal work</label><label class="block"><input type="checkbox" name="requires_client_approval"> Client approval</label><label class="block"><input type="checkbox" name="requires_partner_review"> Partner review</label><button class="rounded bg-brand-600 px-3 py-2 text-white">Save Rule</button></form><form method="post" action="/registrations/masters/documents" class="space-y-3 rounded-xl border bg-white p-4"><h2 class="font-semibold">Document Requirement</h2><input type="hidden" name="csrf_token" value="{{csrf_token}}"><select class="w-full rounded border p-2" name="registration_type_id">{% for x in types %}<option value="{{x.id}}">{{x.name}}</option>{% endfor %}</select><select class="w-full rounded border p-2" name="action_type"><option value="registration">Registration</option><option value="renewal" selected>Renewal</option><option value="update">Update</option></select><input class="w-full rounded border p-2" name="document_name" placeholder="Document name" required><textarea class="w-full rounded border p-2" name="description" placeholder="Description"></textarea><label><input type="checkbox" name="mandatory" checked> Mandatory</label><button class="rounded bg-brand-600 px-3 py-2 text-white">Save Requirement</button></form></div>{% endif %}<div class="grid gap-4 lg:grid-cols-2"><div class="rounded-xl border bg-white p-4"><h2 class="font-semibold">Types</h2>{% for x in types %}<div class="border-b py-2"><b>{{x.code}}</b> — {{x.name}}</div>{% endfor %}</div><div class="rounded-xl border bg-white p-4"><h2 class="font-semibold">Rules & document templates</h2>{% for r,t in rules %}<div class="border-b py-2">{{t.name}} — {{r.cycle_type|replace('_',' ')}} — {{r.renewal_window_days}} days</div>{% endfor %}{% for d,t in templates_rows %}<div class="border-b py-2">{{t.name}} / {{d.action_type}} — {{d.document_name}}</div>{% endfor %}</div></div></div>{% endblock %}
|
||||
@@ -0,0 +1 @@
|
||||
{% extends "ui/templates/base/layout.html" %}{% block content %}<div class="space-y-6"><div><h1 class="text-2xl font-semibold">{{task.title}}</h1><a class="text-sm text-brand-600" href="/registrations">← Dashboard</a></div><div class="grid gap-4 lg:grid-cols-3"><div class="rounded-xl border bg-white p-4"><h2 class="font-semibold">Lifecycle work</h2><dl class="mt-3 space-y-2 text-sm"><div><dt class="text-slate-500">Client</dt><dd>{{client.client_name}}</dd></div><div><dt class="text-slate-500">Registration</dt><dd>{{registration_type.name}} — {{registration.registration_number}}</dd></div><div><dt class="text-slate-500">Due</dt><dd>{{task.cycle_due_date}}</dd></div><div><dt class="text-slate-500">Status</dt><dd>{{task.status}}</dd></div></dl></div><div class="rounded-xl border bg-white p-4"><h2 class="font-semibold">Document checklist</h2>{% for d in documents %}<div class="border-b py-2">{{d.document_name}} {% if d.mandatory %}<span class="text-red-600">*</span>{% endif %} — {{d.status}}</div>{% else %}<p class="py-3 text-slate-500">No template requirements.</p>{% endfor %}</div><div class="rounded-xl border bg-white p-4"><h2 class="font-semibold">History</h2>{% for a in actions %}<div class="border-b py-2 text-sm">{{a.action_type|title}} · {{a.completion_date or a.created_at_utc.date()}} · {{a.acknowledgement_number or ''}}</div>{% else %}<p class="py-3 text-slate-500">No completed lifecycle actions.</p>{% endfor %}</div></div>{% if can_manage %}<div class="grid gap-4 lg:grid-cols-2"><form method="post" action="/registrations/tasks/{{task.id}}/status" class="space-y-3 rounded-xl border bg-white p-4"><h2 class="font-semibold">Update progress</h2><input type="hidden" name="csrf_token" value="{{csrf_token}}"><select class="w-full rounded border p-2" name="status"><option>pending</option><option>documents_pending</option><option>prepared</option><option>awaiting_client_approval</option><option>submitted</option><option>query_raised</option><option>under_review</option></select><input class="w-full rounded border p-2" type="date" name="submitted_on"><input class="w-full rounded border p-2" name="acknowledgement_number" placeholder="Acknowledgement number"><textarea class="w-full rounded border p-2" name="notes" placeholder="Notes"></textarea><button class="rounded bg-brand-600 px-3 py-2 text-white">Update</button></form><form method="post" action="/registrations/tasks/{{task.id}}/complete" class="space-y-3 rounded-xl border bg-white p-4"><h2 class="font-semibold">Complete and schedule next cycle</h2><input type="hidden" name="csrf_token" value="{{csrf_token}}"><label class="text-sm">New validity date</label><input class="w-full rounded border p-2" type="date" name="new_valid_until"><input class="w-full rounded border p-2" name="acknowledgement_number" placeholder="Acknowledgement / reference"><textarea class="w-full rounded border p-2" name="notes" placeholder="Completion notes"></textarea><button class="rounded bg-emerald-600 px-3 py-2 text-white">Complete Renewal / Update</button></form></div>{% endif %}</div>{% endblock %}
|
||||
@@ -0,0 +1,150 @@
|
||||
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 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
|
||||
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("/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)
|
||||
Reference in New Issue
Block a user