# Denya OneCare — Agent memory Denya OneCare is a centralized maintenance/issue tracking system for Pavilion Accra (FastAPI + SQLite + Alpine.js + Twilio). ## Quick start ```bash # Start the app docker-compose up # Or directly uvicorn app.main:app --reload ``` ## Project structure ``` app/ ├── core/ # config, database, security (JWT + bcrypt + RBAC) ├── models/ # SQLAlchemy ORM models ├── schemas/ # Pydantic request/response schemas ├── services/ # Business logic (auth, seed, ticket, sla) └── routers/ # FastAPI route handlers alembic/ # Database migrations tests/ # pytest suite; conftest.py swaps DATABASE_URL to a temp SQLite uploads/ # Photo uploads (created at runtime) ``` ## Commands - `alembic upgrade head` — apply migrations - `alembic revision --autogenerate -m "msg"` — new migration - `pytest` — run the API test suite (tests/; pagination contract anchored in tests/test_tickets_pagination.py) ## Seed data Users, units, and categories are auto-seeded on first startup via lifespan hook: - 17 users covering all roles (Admin/Jerome, Admin/Wahab, CS Rep, CS Manager, FM Dispatcher, Tech, CEO, Director) - 120 apartment units (East/West, 10 floors × 6 apts per wing) - Default password for all seed users: `denya123` - Units load from `apartment_mapping.json` if present, else built-in fallback - Categories: 23 top-level (13 Maintenance, 6 CS, 4 Emergency) with sub-categories, seeded from `app/services/seed.py::SEED_CATEGORIES_DATA` ## Key API endpoints ### Auth & Health | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/health` | No | Health check | | POST | `/api/auth/register` | No | Create user | | POST | `/api/auth/login` | No | Get JWT tokens | | POST | `/api/auth/refresh` | Token | Refresh tokens | | GET | `/api/auth/me` | Bearer | Current user | | GET | `/api/auth/admin-only` | Admin/Jerome, Admin/Wahab | RBAC demo endpoint | | GET | `/api/auth/users` | Bearer | List users (id, name, role) for the assign-technician picker | | POST | `/api/whatsapp/mock` | No | Mock WhatsApp | | GET | `/api/whatsapp/mock-log` | No | Recent mock submissions | ### Pages (Sprint 3) — Jinja2 templates at `app/templates/` | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/login` | No | Login page | | GET | `/dashboard/cs` | Client | CS dashboard | | GET | `/dashboard/fm` | Client | FM dashboard | | GET | `/dashboard/ceo` | Client | CEO dashboard | | GET | `/tickets` | Client | All Issues filterable table | | GET | `/tickets/new` | Client | Create Issue form | | GET | `/tickets/{id}` | Client | Issue detail with timeline | Frontend: Alpine.js (CDN) + Tailwind CSS (CDN). Auth state in localStorage. Role-based nav routing in `base.html`. ### Tickets (Sprint 2) | Method | Path | Auth | Description | |--------|------|------|-------------| | POST | `/api/tickets` | Bearer | Create ticket (auto-number PAV-YYYY-NNNNN) | | GET | `/api/tickets` | No | List tickets (filter: status, priority, property, building, unit_id, category_id, assigned_to, date_from, date_to) | | GET | `/api/tickets/{id}` | No | Get ticket detail with timeline, photos, SLA status, nested unit/category, phone | | GET | `/api/tickets/{id}/transitions` | No | Valid next statuses for the ticket's current status (drives the detail-page status picker) | | PATCH | `/api/tickets/{id}` | Bearer | Update ticket (validates status transitions; assigning a technician auto-advances New/Logged/Triage to Assigned) | | DELETE | `/api/tickets/{id}` | Admin/Jerome, Admin/Wahab | Delete ticket + children (timeline/photos/escalations); test/scratch cleanup only | | POST | `/api/tickets/{id}/status` | Bearer | Change status with note | | GET | `/api/tickets/{id}/sla` | No | Check SLA breach status | | POST | `/api/tickets/{id}/photos` | Bearer | Upload photos (multipart, is_before param) | | GET | `/api/tickets/{id}/photos` | No | List photos | | GET | `/api/tickets/categories` | No | Category tree (filters: `type`; alert-only hidden unless `include_hidden=true`) | | GET | `/api/tickets/categories/flat` | No | Flat category list (same `type`/`include_hidden` filters) | ## Auth - JWT access (30min) + refresh (7d) tokens - Roles: CS Rep, CS Manager, FM Dispatcher, Admin/Jerome, Admin/Wahab, Tech, CEO, Director - Use `require_roles("Admin/Jerome", "Admin/Wahab")` dependency for RBAC - `sub` claim holds string user ID ## Ticket System (Sprint 2) ### Status Lifecycle (16 statuses) New → Logged → Triage → Assigned → Accepted → Travelling → On Site → In Progress → Waiting Parts → Escalated → Completed → On-Field Verification → Wahab Review → Closed → Reopened → Cancelled Cancelled is terminal (no outgoing transitions) and reachable from any active state; it is excluded from SLA breach reporting and dashboard "active" counts. Valid transitions defined in `app/services/ticket.py::VALID_TRANSITIONS`. Invalid transitions return 400. ### SLA Engine Defined in `app/services/sla.py`. Priority-based targets: - Urgent: respond 15min, resolve 4h - High: respond 30min, resolve 24h - Medium: respond 4h, resolve 72h (3d) - Low: respond 24h, resolve 168h (7d) `sla_deadline` auto-calculated on ticket creation. SLA status check at `GET /api/tickets/{id}/sla`. ### Ticket Number Format `PAV-YYYY-NNNNN` — sequential per year (e.g., PAV-2026-00001). ### Photo Uploads Stored under `uploads/` with UUID filenames. Static-files mounted at `/uploads/`. Multipart POST with `is_before` query param. ## Database SQLite via aiosqlite with async SQLAlchemy 2.0. Alembic for migrations. Tables: users, units, categories, tickets, ticket_timeline, ticket_photos, escalations, whatsapp_log ## Categories & location (Sprint A) - `Category.show_in_form` (default True) marks alert-only categories: `Gas Leak` is hidden from the issue picker but keeps `sla_urgency="urgent"` for SLA/alert/reporting. Pickers (`/api/tickets/categories[/flat]`) exclude them unless `include_hidden=true` (used by the emergency quick path on `tickets/new.html`). Seed taxonomy lives in `app/services/seed.py` (`SEED_CATEGORIES_DATA`); the Lost Property → Missing Item rename is a data migration (alembic `b2f4a6c8e0d2`), with a startup self-heal (`app/main.py::ensure_legacy_schema`) applying the same column/rename fix to legacy create_all databases. Seeds are idempotent and sync `show_in_form` on existing rows. - Location hierarchy is Property → Building → Apartment (uses `Unit.building`). `GET /api/tickets/units/grouped` returns `{property: {building: [units]}}`; `/api/tickets` accepts additive `building`/`unit_id` filters. Unit data is deterministic from the committed `apartment_mapping.json` (built-in fallback in `seed.py`). - Priority grouping ("Group by priority") is client-side via `app().groupByPriority()` in `app/templates/base.html`; used by `tickets/list.html` and `dashboard/fm.html`. ## Maintaining this file Keep this file for knowledge useful to almost every future agent session in this project. Do not repeat what the codebase already shows; point to the authoritative file or command instead. Prefer rewriting or pruning existing entries over appending new ones. When updating this file, preserve this bar for all agents and keep entries concise.