Delete VPS document stage after verified local storage sync

This commit is contained in:
A R R R Associates
2026-07-20 15:27:58 +05:30
parent bca72ab2d9
commit 55c5a7acdf
15 changed files with 361 additions and 7 deletions
+35 -1
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from datetime import datetime, timezone
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.db.common import CommonBase
@@ -39,3 +39,37 @@ class UserAlert(CommonBase):
user = relationship("User", foreign_keys=[user_id])
created_by = relationship("User", foreign_keys=[created_by_user_id])
class WorkflowEscalation(CommonBase):
"""Durable engagement escalation register used by staff, managers and partners."""
__tablename__ = "workflow_escalations"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=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"), nullable=True, index=True)
subscription_id: Mapped[int] = mapped_column(ForeignKey("client_service_subscriptions.id", ondelete="CASCADE"), nullable=False, index=True)
task_id: Mapped[int | None] = mapped_column(ForeignKey("client_service_task_instances.id", ondelete="SET NULL"), nullable=True, index=True)
raised_by_user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="RESTRICT"), nullable=False, index=True)
assigned_to_user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="RESTRICT"), nullable=False, index=True)
escalation_level: Mapped[str] = mapped_column(String(30), nullable=False, index=True)
category: Mapped[str] = mapped_column(String(50), nullable=False, default="workflow_dependency", index=True)
priority: Mapped[str] = mapped_column(String(20), nullable=False, default="high", index=True)
status: Mapped[str] = mapped_column(String(20), nullable=False, default="open", index=True)
message: Mapped[str] = mapped_column(Text, nullable=False)
follow_up_date: Mapped[date | None] = mapped_column(Date, nullable=True, index=True)
acknowledged_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
acknowledged_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
resolved_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
resolved_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
resolution_note: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False, index=True)
updated_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False)
subscription = relationship("ClientServiceSubscription")
task = relationship("ClientServiceTaskInstance")
raised_by = relationship("User", foreign_keys=[raised_by_user_id])
assigned_to = relationship("User", foreign_keys=[assigned_to_user_id])
acknowledged_by = relationship("User", foreign_keys=[acknowledged_by_user_id])
resolved_by = relationship("User", foreign_keys=[resolved_by_user_id])
@@ -0,0 +1,43 @@
from __future__ import annotations
from datetime import date, datetime, timezone
from typing import Any
from sqlalchemy import select
from sqlalchemy.orm import Session, selectinload
from app.modules.alerts.models import WorkflowEscalation
from app.modules.services.models import ClientServiceSubscription
OPEN_ESCALATION_STATUSES = {"open", "acknowledged"}
def create_workflow_escalation(db: Session, *, subscription: ClientServiceSubscription, raised_by_user_id: int, assigned_to_user_id: int, escalation_level: str, message: str, task_id: int | None = None, category: str = "workflow_dependency", priority: str = "high", follow_up_date: date | None = None) -> WorkflowEscalation:
row = WorkflowEscalation(tenant_id=subscription.tenant_id, branch_id=subscription.branch_id, subscription_id=subscription.id, task_id=task_id, raised_by_user_id=raised_by_user_id, assigned_to_user_id=assigned_to_user_id, escalation_level=escalation_level, category=category, priority=priority, status="open", message=message.strip()[:4000], follow_up_date=follow_up_date)
db.add(row)
db.flush()
return row
def list_workflow_escalations(db: Session, *, tenant_id: int | None, branch_id: int | None, assigned_to_user_id: int | None = None, include_resolved: bool = False, limit: int = 200) -> list[dict[str, Any]]:
stmt = select(WorkflowEscalation).options(selectinload(WorkflowEscalation.subscription).selectinload(ClientServiceSubscription.client), selectinload(WorkflowEscalation.subscription).selectinload(ClientServiceSubscription.catalogue), selectinload(WorkflowEscalation.raised_by), selectinload(WorkflowEscalation.assigned_to))
if tenant_id: stmt = stmt.where(WorkflowEscalation.tenant_id == tenant_id)
if branch_id is not None: stmt = stmt.where(WorkflowEscalation.branch_id == branch_id)
if assigned_to_user_id: stmt = stmt.where(WorkflowEscalation.assigned_to_user_id == assigned_to_user_id)
if not include_resolved: stmt = stmt.where(WorkflowEscalation.status.in_(OPEN_ESCALATION_STATUSES))
rows = db.execute(stmt.order_by(WorkflowEscalation.created_at_utc.asc()).limit(limit)).scalars().all()
now = datetime.now(timezone.utc)
out=[]
for row in rows:
created=row.created_at_utc
if created and created.tzinfo is None: created=created.replace(tzinfo=timezone.utc)
sub=row.subscription
out.append({"id":row.id,"subscription_id":row.subscription_id,"task_id":row.task_id,"client_name":getattr(getattr(sub,"client",None),"client_name",None) or "Unlinked Client","service_name":getattr(getattr(sub,"catalogue",None),"service_name",None) or "Service","financial_year":getattr(sub,"financial_year",None) or "-","level":row.escalation_level,"category":row.category,"priority":row.priority,"status":row.status,"message":row.message,"follow_up_date":row.follow_up_date,"raised_by":getattr(row.raised_by,"full_name",None) or getattr(row.raised_by,"email",None) or "User","assigned_to":getattr(row.assigned_to,"full_name",None) or getattr(row.assigned_to,"email",None) or "User","created_at":row.created_at_utc,"age_days":max(0,(now-created).days) if created else 0,"href":f"/manager/reviews/engagements/{row.subscription_id}" if row.escalation_level=="manager" else f"/partner/reviews/engagements/{row.subscription_id}"})
return out
def update_workflow_escalation(db: Session, *, escalation_id: int, actor_user_id: int, action: str, resolution_note: str = "") -> WorkflowEscalation:
row=db.get(WorkflowEscalation, escalation_id)
if not row or int(row.assigned_to_user_id)!=int(actor_user_id): raise ValueError("Escalation not found or not assigned to you.")
now=datetime.now(timezone.utc)
if action=="acknowledge": row.status="acknowledged"; row.acknowledged_at_utc=now; row.acknowledged_by_user_id=actor_user_id
elif action=="resolve": row.status="resolved"; row.resolved_at_utc=now; row.resolved_by_user_id=actor_user_id; row.resolution_note=(resolution_note or "").strip()[:4000] or None
else: raise ValueError("Invalid escalation action.")
db.flush(); return row