Phase 10 — Support Ticket System:
- tickets.py router: SLA status (on_track/at_risk/breached/responded), email
notifications on create+reply via background threads, school_name in list,
priority filter, bulk-close endpoint
- tasks/tickets.py: escalate_stale Celery task (48h→high, 72h no reply→urgent)
- worker.py: escalate_stale scheduled every hour
- templates/email/ticket_notification.html: HTML ticket notification email
- TicketsPage.vue: status tabs, SLA badge, priority badge, school name column,
checkbox bulk-close, pagination
- TicketDetailPage.vue: inline priority/status/assignee selectors, SLA timer,
internal note lock icon, closed-ticket guard
Phase 11 — Monthly Report Generation:
- models/report.py: MonthlyReport + SchoolMonthlyStats ORM models
- tasks/reports.py: send_monthly_reports enhanced with SMS stats, attendance
data, invoice summary, stores MonthlyReport record per school per month
Phase 12 — On-Prem Monthly Report Pull:
- tasks/reports.py: pull_monthly_stats task — httpx GET to each school's
hub_base_url, upserts SchoolMonthlyStats; runs 1st at 5am
- worker.py: pull_monthly_stats scheduled 1st at 5am
Phase 13 — Feature Flags + Suspension:
- models/school.py: hub_base_url, feature_overrides (JSON), onboarding_completed_at
- routers/schools.py: PUT /{id}/feature-overrides endpoint
- routers/sync.py: _tier_features() merges school.feature_overrides into poll config
Phase 14 — Onboarding Wizard + Welcome Email:
- tasks/onboarding.py: send_welcome_email Celery task with license key
- routers/schools.py: auto-trigger welcome email on POST /schools,
POST /{id}/activate (status→active + onboarding_completed_at),
POST /{id}/resend-welcome
Phase 15 — UX Polish + Ops Tools:
- routers/search.py: GET /api/search?q= (schools + invoices + tickets, 5 each)
- routers/audit.py: GET /api/audit-logs (paginated, filterable)
- AppLayout.vue: global search bar with debounced dropdown, result navigation
- AuditLogsPage.vue: new page with filter + pagination
- AppSidebar.vue: Audit Logs nav item added
- router/index.ts: /audit-logs route
- api.ts: globalSearch, getAuditLogs, activateSchool, resendWelcomeEmail,
updateFeatureOverrides, bulkCloseTickets
Deployment:
- docker-compose.yml: x-backend-env anchor (DRY), PDF_DIR env var,
seed service (one-shot python seed.py on first boot)
- migrations/003_phases11_15.py: monthly_reports, school_monthly_stats tables
+ schools hub_base_url/feature_overrides/onboarding_completed_at columns
71 lines
2.5 KiB
Python
71 lines
2.5 KiB
Python
"""Celery tasks: ticket priority escalation."""
|
|
import logging
|
|
from datetime import datetime, timezone, timedelta
|
|
|
|
from app.worker import celery_app
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _make_session():
|
|
import os
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
db_url = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@db:5432/taptrack_hub")
|
|
engine = create_engine(db_url.replace("postgresql+asyncpg://", "postgresql://"), pool_pre_ping=True)
|
|
return sessionmaker(bind=engine)()
|
|
|
|
|
|
@celery_app.task(name="tickets.escalate_stale")
|
|
def escalate_stale():
|
|
"""
|
|
Escalate stale open tickets:
|
|
- open > 48h + priority medium → high
|
|
- open > 72h + no reply yet → urgent
|
|
"""
|
|
from sqlalchemy import select, and_
|
|
from app.models.ticket import SupportTicket, TicketReply, TicketStatus, TicketPriority
|
|
|
|
db = _make_session()
|
|
try:
|
|
now = datetime.now(timezone.utc)
|
|
threshold_48h = now - timedelta(hours=48)
|
|
threshold_72h = now - timedelta(hours=72)
|
|
|
|
# Open tickets > 48h, medium priority → high
|
|
stale_medium = db.execute(
|
|
select(SupportTicket).where(
|
|
and_(
|
|
SupportTicket.status == TicketStatus.open,
|
|
SupportTicket.priority == TicketPriority.medium,
|
|
SupportTicket.created_at < threshold_48h,
|
|
)
|
|
)
|
|
).scalars().all()
|
|
for t in stale_medium:
|
|
t.priority = TicketPriority.high
|
|
logger.info("Escalated ticket %s to high priority (48h)", t.ticket_number)
|
|
|
|
# Open tickets > 72h, no reply → urgent
|
|
stale_72h = db.execute(
|
|
select(SupportTicket).where(
|
|
and_(
|
|
SupportTicket.status == TicketStatus.open,
|
|
SupportTicket.first_response_at == None, # noqa: E711
|
|
SupportTicket.created_at < threshold_72h,
|
|
)
|
|
)
|
|
).scalars().all()
|
|
for t in stale_72h:
|
|
if t.priority != TicketPriority.urgent:
|
|
t.priority = TicketPriority.urgent
|
|
logger.warning("Escalated ticket %s to urgent (72h no response)", t.ticket_number)
|
|
|
|
db.commit()
|
|
logger.info("escalate_stale: %d→high, %d→urgent", len(stale_medium), len(stale_72h))
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error("escalate_stale error: %s", e)
|
|
finally:
|
|
db.close()
|