Sprint 2: Ticket Engine + SLA + Photo Uploads
- Ticket CRUD with full 15-status lifecycle enforcement
- Auto-generated ticket numbers (PAV-YYYY-NNNNN)
- Status transitions validated and auto-logged to timeline
- Reopen with 7-day window + auto-escalation to Ama
- SLA engine: priority-based deadlines and breach detection
- Photo uploads to uploads/ with multipart support
- Category system seeded from PRD §7 taxonomy (20 top-level)
- Flat and tree category listing endpoints
- SLA status check endpoint (/api/tickets/{id}/sla)
- Paginated ticket listing with filters
This commit is contained in:
@@ -15,6 +15,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.security import hash_password
|
||||
from app.models.unit import Unit
|
||||
from app.models.user import User
|
||||
from app.models.category import Category
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -118,3 +119,87 @@ async def seed_units(db: AsyncSession, json_path: str | Path | None = None) -> l
|
||||
else:
|
||||
logger.info("All units already exist")
|
||||
return created
|
||||
|
||||
|
||||
# ── Category taxonomy (PRD §7) ───────────────────────────────────────
|
||||
# Top-level categories with sub-categories
|
||||
SEED_CATEGORIES_DATA: list[dict] = [
|
||||
# ── Maintenance ──────────────────────────────────────────────
|
||||
{"type": "maintenance", "name": "Plumbing", "subs": ["WC not flushing", "Leaky tap", "Pipe burst", "Blocked drain", "Water heater"]},
|
||||
{"type": "maintenance", "name": "Electrical", "subs": ["No power", "Tripped breaker", "Light fitting", "Socket repair", "Voltage issues"]},
|
||||
{"type": "maintenance", "name": "HVAC", "subs": ["AC not cooling", "AC leaking", "Thermostat", "Fan not working"]},
|
||||
{"type": "maintenance", "name": "Furniture", "subs": ["Broken bed", "Broken chair", "Broken table", "Wardrobe issue"]},
|
||||
{"type": "maintenance", "name": "Appliances", "subs": ["Fridge", "Washing machine", "Microwave", "TV", "Kettle"]},
|
||||
{"type": "maintenance", "name": "Cleaning", "subs": ["Deep clean requested", "Post-checkout turnover", "Common area cleaning"]},
|
||||
{"type": "maintenance", "name": "Pest Control", "subs": ["Insects", "Rodents", "Fumigation"]},
|
||||
{"type": "maintenance", "name": "Security", "subs": ["Lock broken", "Door not closing", "Window latch", "CCTV issue"]},
|
||||
{"type": "maintenance", "name": "Internet", "subs": ["WiFi down", "Slow speed", "Router reset"]},
|
||||
{"type": "maintenance", "name": "Structural", "subs": ["Wall crack", "Ceiling leak", "Floor tile", "Paint touch-up"]},
|
||||
# ── Customer Service ─────────────────────────────────────────
|
||||
{"type": "cs", "name": "Check-in", "subs": ["Early check-in", "Key handover", "Welcome instructions"]},
|
||||
{"type": "cs", "name": "Check-out", "subs": ["Late checkout", "Key return", "Inspection"]},
|
||||
{"type": "cs", "name": "Housekeeping", "subs": ["Mid-stay cleaning", "Linen change", "Restocking"]},
|
||||
{"type": "cs", "name": "Lost Property", "subs": ["Guest left items behind"]},
|
||||
{"type": "cs", "name": "Billing", "subs": ["Invoice question", "Payment issue", "Deposit query"]},
|
||||
{"type": "cs", "name": "Staff Behaviour", "subs": ["Staff conduct feedback"]},
|
||||
# ── Emergency ────────────────────────────────────────────────
|
||||
{"type": "emergency", "name": "Fire", "subs": ["Smoke detected", "Fire alarm", "Sprinkler issue"], "sla_urgency": "urgent"},
|
||||
{"type": "emergency", "name": "Flood", "subs": ["Major water leak", "Burst pipe", "Overflowing"], "sla_urgency": "urgent"},
|
||||
{"type": "emergency", "name": "Gas Leak", "subs": ["Gas smell", "Suspected leak"], "sla_urgency": "urgent"},
|
||||
{"type": "emergency", "name": "Electrical Hazard", "subs": ["Sparking", "Exposed wires", "Power outage multiple units"], "sla_urgency": "urgent"},
|
||||
]
|
||||
|
||||
|
||||
async def seed_categories(db: AsyncSession) -> list[Category]:
|
||||
"""Seed the categories table with the full PRD §7 taxonomy."""
|
||||
created: list[Category] = []
|
||||
|
||||
for group in SEED_CATEGORIES_DATA:
|
||||
# Check if parent category exists
|
||||
result = await db.execute(
|
||||
select(Category).where(
|
||||
Category.type == group["type"],
|
||||
Category.name == group["name"],
|
||||
Category.parent_id.is_(None),
|
||||
)
|
||||
)
|
||||
parent = result.scalar_one_or_none()
|
||||
if parent is None:
|
||||
parent = Category(
|
||||
type=group["type"],
|
||||
name=group["name"],
|
||||
parent_id=None,
|
||||
sla_urgency=group.get("sla_urgency"),
|
||||
)
|
||||
db.add(parent)
|
||||
await db.flush()
|
||||
created.append(parent)
|
||||
|
||||
# Seed sub-categories
|
||||
for sub_name in group["subs"]:
|
||||
result = await db.execute(
|
||||
select(Category).where(
|
||||
Category.type == group["type"],
|
||||
Category.name == sub_name,
|
||||
Category.parent_id == parent.id,
|
||||
)
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
if existing is None:
|
||||
sub = Category(
|
||||
type=group["type"],
|
||||
name=sub_name,
|
||||
parent_id=parent.id,
|
||||
sla_urgency=None,
|
||||
)
|
||||
db.add(sub)
|
||||
created.append(sub)
|
||||
|
||||
if created:
|
||||
await db.flush()
|
||||
for c in created:
|
||||
await db.refresh(c)
|
||||
logger.info("Seeded %d categories", len(created))
|
||||
else:
|
||||
logger.info("All seed categories already exist")
|
||||
return created
|
||||
|
||||
Reference in New Issue
Block a user