🚧 Partial implementation; API and web surfaces exist, but notification, tenant-boundary, browser, and environment proof gaps remain

Spec: F58 — Internal Support Ticketing System

Status: 🚧 Partial implementation; API and web surfaces exist, but notification, tenant-boundary, browser, and environment proof gaps remain

Current truth (2026-07-16): The former spec-only audit was stale. backend/apps/support/, /api/support/tickets/, authenticated frontend routes, and backend tests are present. This does not establish browser execution, live notification delivery, production behavior, or a tenant-isolated staff queue. See the implementation audit and Contract Graph gaps.

Current repository truth: this document is the product proposal and target contract, not shipped evidence. The checked acceptance criteria below describe the intended build. See the current implementation audit and contracts/features/F58.cue for the explicit implementation gap.

1. Problem & CEO Job

Job: Trust the signal.

When a pulse will not submit, the Brief will not generate, or an approval is stuck, the CEO has no channel to tell Lumi. There is no path from “something is wrong” to “Lumi fixed it” beyond struggling silently or emailing someone they hope still works here.

This spec builds Lumi’s support channel: users submit issues; Lumi staff triage, respond, and resolve them; users receive status updates. It is not a tenant workflow.

2. User Stories & Acceptance Criteria

User — Submit Ticket

  • As a Lumi user (any role), I want to submit a support ticket with a subject and description, so that the Lumi team knows what’s broken.
    • Given any org member, when I navigate to /support/new, then I see a form with: subject, description (textarea), category dropdown, priority selector, optional file attachment. (SupportCreatePage.tsx)
    • Given the form, when I submit with a non-empty subject and description, then a Ticket is created with status open and I’m redirected to /support/<ticket_id>.
    • Given the form, when I submit with an empty subject, then I see a validation error and nothing is created.
    • Given the form, when I click “Attach file”, then a file picker opens; supported types: images, PDFs, plain text (max 10 MB per file, max 3 files per ticket). Backend: POST /attach/ enforces 3-file cumulative limit, 10 MB via UPLOAD_MAX_SIZE.

User — View My Tickets

  • As a user, I want to see all my tickets in one list, so that I can track what I’ve reported.
    • Given /support, the page shows a list of my tickets (created by me) ordered by most recent update. (SupportListPage.tsx)
    • Each ticket row shows: id, subject, status badge, priority badge, category, last updated timestamp.
    • Clicking a ticket row navigates to /support/<ticket_id>.

User — Ticket Detail & Messages

  • As a user, I want to see the full conversation on a ticket, so that I know what the Lumi team has said and can reply.
    • Given /support/<ticket_id>, the page shows: subject, status banner, description (original), and a chronological message thread. (SupportDetailPage.tsx)
    • Each message shows: author name, timestamp, body, and attachments if any.
    • Messages from Lumi staff are visually distinct (badge “Lumi Team” with brand color).
    • Given the ticket is not closed, I can add a reply via a textarea at the bottom of the thread.
    • Given a reply, when I submit, then the message is appended, the ticket’s updated_at refreshes, and the status changes to waiting_on_staff (auto-reopens if previously waiting_on_user).

User — Close Ticket

  • As a user, I want to close my own ticket when the issue is resolved, so that the Lumi team knows I’m satisfied.
    • Given an open or in-progress ticket, a “Close ticket” button is visible to the ticket creator.
    • Given a click on “Close ticket”, then status changes to closed and a confirmation toast appears.

Staff — Ticket Dashboard

  • As a Lumi staff member (is_staff=True), I want a dashboard of all tickets, so that I can triage and resolve issues.
    • Given /hq/support (inside the HQ console, staff-gated by HqLayout), the staff dashboard shows all tickets across all users, filterable by status (open, in_progress, resolved, closed), priority, and category. (pages/hq/HqSupportPage.tsx; legacy /support/manage redirects here)
    • The dashboard shows a summary bar: open count, in-progress count, unresolved older than 48h. (Added 2026-06-14)
    • Clicking a ticket navigates to /support/<ticket_id> with full staff controls.
    • Non-staff users on /hq/support are redirected out by HqLayout (/work/today); the manage API returns 403.

Staff — Assign & Update Status

  • As a Lumi staff member, I want to assign tickets to myself and update their status, so that the team knows who owns each issue.
    • Given /support/<ticket_id>, a staff-only panel shows: assignee dropdown, status dropdown, priority dropdown. (SupportDetailPage.tsx staff controls panel, added 2026-06-14)
    • Given “Assign to me” click, the ticket’s assigned_to is set to the current staff user.
    • Status transitions: open → in_progress → resolved → closed. A ticket can be reopened from resolved to in_progress.
    • Given status changes to resolved, the ticket creator’s next visit shows a “Was this resolved?” feedback prompt (thumbs up/down).

Staff — Internal Notes

  • As a Lumi staff member, I want to add internal notes visible only to staff, so that we can discuss the issue without confusing the user.
    • Given /support/<ticket_id>, a staff-only “Internal note” button toggles a separate textarea. (Added 2026-06-14)
    • Internal notes are rendered in the thread with a ”🔒 Internal” badge and are invisible to the ticket creator.
    • Internal notes do not change the ticket’s waiting_on state.

Notifications

  • As a user, I want to be notified when my ticket status changes or a staff member replies, so that I don’t have to keep checking.
    • Given a staff reply or status change on my ticket, a push notification is sent (F36) and a bell badge increments.
    • Given a staff reply, an email notification is sent to the ticket creator’s registered email (via F35 email infrastructure).
    • The notification includes the ticket subject and a deep link to /support/<ticket_id>.

3. Scope Boundaries

  • In:
    • Ticket CRUD for users (create, view own, reply, close).
    • Staff dashboard with full ticket management.
    • Assignee system (staff self-assign).
    • Status workflow (open → in_progress → resolved → closed).
    • Internal notes (staff-only).
    • File attachments on tickets (via F34 upload infra).
    • Email + push notifications on ticket updates.
    • Ticket feedback prompt on resolve (thumbs up/down).
    • Both en + vi localization for all UI.
  • Out:
    • SLA or escalation rules.
    • CSAT surveys beyond thumbs up/down.
    • Knowledge base / FAQ (separate future feature).
    • Public-facing status page.
    • Bulk ticket operations.
    • Third-party helpdesk integration (Zendesk, Freshdesk, etc.).
    • Per-org ticket visibility (tickets belong to individual users, not orgs).
    • Chat/copilot for support (ticket-based async only).

4. Data Model

4.1 Ticket

class Ticket(UUIDModel):
    TICKET_STATUS_CHOICES = [
        ("open", "Open"),
        ("in_progress", "In Progress"),
        ("waiting_on_staff", "Waiting on Staff"),
        ("waiting_on_user", "Waiting on User"),
        ("resolved", "Resolved"),
        ("closed", "Closed"),
    ]
    PRIORITY_CHOICES = [
        ("low", "Low"),
        ("medium", "Medium"),
        ("high", "High"),
        ("critical", "Critical"),
    ]
    CATEGORY_CHOICES = [
        ("bug", "Bug / Error"),
        ("feature_request", "Feature Request"),
        ("billing", "Billing / Payment"),
        ("account", "Account / Login"),
        ("integration", "Integration / Connector"),
        ("other", "Other"),
    ]

    subject = models.CharField(max_length=200)
    description = models.TextField()
    category = models.CharField(max_length=30, choices=CATEGORY_CHOICES, default="other")
    priority = models.CharField(max_length=20, choices=PRIORITY_CHOICES, default="medium")
    status = models.CharField(max_length=20, choices=TICKET_STATUS_CHOICES, default="open")

    created_by = models.ForeignKey(User, on_delete=CASCADE, related_name="tickets")
    assigned_to = models.ForeignKey(User, on_delete=SET_NULL, null=True, blank=True, related_name="assigned_tickets")

    # Feedback on resolve
    resolved_satisfied = models.BooleanField(null=True, blank=True)  # True = thumbs up

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "support_tickets"
        ordering = ["-updated_at"]

4.2 TicketMessage

class TicketMessage(UUIDModel):
    ticket = models.ForeignKey(Ticket, on_delete=CASCADE, related_name="messages")
    author = models.ForeignKey(User, on_delete=CASCADE)
    body = models.TextField()
    is_internal = models.BooleanField(default=False)  # Staff-only internal note
    attachments = models.ManyToManyField("uploads.Upload", blank=True)  # F34

    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = "support_ticket_messages"
        ordering = ["created_at"]

4.3 No Organization FK

Tickets are cross-tenant. They belong to individual users, not organizations. The created_by FK is on accounts.User, not OrganizationMember, so the user retains access to their tickets even if they leave an org or have multiple orgs.

5. API Contract

MethodPathAuthRequestResponseErrors
GET/api/support/tickets/any authpagination, filters (status, priority, category)list of user’s tickets—
POST/api/support/tickets/any auth{subject, description, category, priority}created ticket400 if subject empty
GET/api/support/tickets/<id>/creator or staff—ticket detail with messages404 if not found, 403 if not creator/staff
POST/api/support/tickets/<id>/messages/creator or staff{body, is_internal} (staff only for is_internal)created message403 if non-staff sets is_internal
PATCH/api/support/tickets/<id>/staff only{status?, priority?, assigned_to?}updated ticket403 for non-staff; 400 invalid transition or unknown assignee
POST/api/support/tickets/<id>/close/creator—status → closed403 if not creator
POST/api/support/tickets/<id>/feedback/creator{satisfied: bool}sets resolved_satisfied400 if ticket not resolved
GET/api/support/tickets/manage/staff onlypagination, filters (status, priority, category, assignee)all tickets403 if non-staff

The shipped management read is contract-owned as F58.AC002.MANAGE_API, and the ticket detail read is contract-owned as F58.AC001.DETAIL_API. Its staff queue remains subject to the unresolved tenant-boundary gap recorded in the implementation audit and Contract Graph; this ownership entry does not imply tenant isolation or runtime execution evidence.

The exact POST /api/support/tickets/ creation surface is owned by TicketViewSet.create with collected proof F58.AC001.HAPPY; guarded backend execution remains explicit in F58.GAP006.

The exact POST /api/support/tickets/{id}/attach/ surface is owned by TicketViewSet.attach with collected proof F58.AC003.ATTACH_HAPPY; guarded backend execution remains explicit in F58.GAP007.

The exact POST /api/support/tickets/{id}/close/ surface is owned by TicketViewSet.close with collected proof F58.AC001.CLOSE_HAPPY; guarded backend execution remains explicit in F58.GAP008.

The exact POST /api/support/tickets/{id}/feedback/ surface is owned by TicketViewSet.feedback with collected proof F58.AC001.FEEDBACK_HAPPY; guarded backend execution remains explicit in F58.GAP009.

The exact POST /api/support/tickets/{id}/messages/ surface is owned by TicketViewSet.add_message with collected proof F58.AC001.MESSAGE_HAPPY; guarded backend execution remains explicit in F58.GAP010.

The exact POST /api/support/tickets/{id}/reopen/ surface is owned by TicketViewSet.reopen with collected proof F58.AC002.REOPEN_HAPPY; guarded backend execution remains explicit in F58.GAP011.

Tenant scoping: Tickets are NOT tenant-scoped. They are user-scoped. A user sees only their own tickets. Staff (is_staff=True) see all tickets.

6. UI / UX

6.1 Support Entry Points

  • Sidebar — A “Support” link in the Admin nav group (always visible to all roles) or a dedicated entry in Settings.
  • Global nav — A ”?” icon or “Help” dropdown in the top bar with “Submit a ticket” option.

6.2 My Tickets Page (/support)

┌─ Support ─────────────────────────────────────────────────┐
│                                                           │
│  [Submit New Ticket]                                      │
│                                                           │
│  My Tickets                                               │
│                                                           │
│  ┌──────────────────────────────────────────────────────┐ │
│  │ #T-042 │🔴 Can't submit pulse today         │ 🟢 │ │
│  │        │ Bug     High    2 hours ago        │ Open │ │
│  ├──────────────────────────────────────────────────────┤ │
│  │ #T-041 │🟡 Brief not generating on time     │ 🟡 │ │
│  │        │ Bug     Medium   yesterday         │ In..│ │
│  ├──────────────────────────────────────────────────────┤ │
│  │ #T-039 │✅ Add Telegram connector            │ ✅ │ │
│  │        │ Featur  Low      3 days ago        │ Clo..│ │
│  └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘

6.3 Ticket Detail (/support/<id>)

┌─ Ticket #T-042 ──────────────────────────────────────────┐
│                                                           │
│  🔴 Open · 🐛 Bug · High Priority                         │
│                                                           │
│  Can't submit pulse today                                 │
│  ─────────────────────────────────────────────            │
│  I've been trying to submit my pulse since 8 AM but       │
│  the form keeps showing a blank page after I click        │
│  "Submit". I've tried on Chrome and Safari.               │
│                                                           │
│  Attachments: screenshot-1.png                            │
│                                                           │
│  ─── Replies ───                                          │
│                                                           │
│  [Lumi Team] Hi, thanks for reporting! Can you check      │
│  if your browser console shows any errors?                │
│  10 minutes ago                                           │
│                                                           │
│  You: I checked — here's what I see: [screenshot]         │
│  5 minutes ago                                            │
│                                                           │
│  ┌────────────────────────────────────────────────────┐   │
│  │ Type your reply here...                [Send]      │   │
│  └────────────────────────────────────────────────────┘   │
│                                                           │
│  [Close Ticket]                                           │
└──────────────────────────────────────────────────────────┘

6.4 Staff Dashboard (/hq/support)

┌─ Support Dashboard ───────────────────────────────────────┐
│                                                           │
│  🟢 12 Open  🟡 8 In Progress  🔴 3 Unresolved >48h     │
│                                                           │
│  [All] [Open] [In Progress] [Resolved] [Closed]           │
│  [All Priorities] [All Categories]                        │
│                                                           │
│  ┌────┬────────────┬──────────┬────────┬────────┬──────┐ │
│  │ ID │ Subject    │ User     │ Status │ Pri    │ Age  │ │
│  ├────┼────────────┼──────────┼────────┼────────┼──────┤ │
│  │042 │ Can't sub..│ ceo@d.. │ 🟢open  │ 🔴high │ 2h  │ │
│  │041 │ Brief not..│ admin@..│ 🟡in.. │ 🟡med  │ 26h │ │
│  └────┴────────────┴──────────┴────────┴────────┴──────┘ │
└──────────────────────────────────────────────────────────┘

6.5 i18n

New namespace: support. All keys in both en/support.json and vi/support.json.

Page heading keys:

Keyenvi
support.titleSupportHỗ trợ
support.new_ticketSubmit new ticketGửi yêu cầu mới
support.my_ticketsMy ticketsYêu cầu của tôi
support.dashboard_titleSupport DashboardBảng điều khiển hỗ trợ

Form keys:

Keyenvi
support.subjectSubjectTiêu đề
support.subject_placeholderBriefly describe your issueMô tả ngắn gọn vấn đề
support.descriptionDescriptionMô tả chi tiết
support.description_placeholderTell us more about what’s happening…Hãy cho chúng tôi biết thêm chi tiết…
support.categoryCategoryDanh mục
support.priorityPriorityMức độ ưu tiên
support.attach_fileAttach fileĐính kèm tệp
support.attach_file_hintMax 10 MB per file, max 3 filesTối đa 10 MB mỗi tệp, tối đa 3 tệp
support.submitSubmit ticketGửi yêu cầu
support.submittingSubmitting…Đang gửi…

Status badges:

Keyenvi
support.status_openOpenĐang mở
support.status_in_progressIn progressĐang xử lý
support.status_waiting_staffWaiting on staffChờ nhân viên
support.status_waiting_userWaiting on youChờ bạn
support.status_resolvedResolvedĐã giải quyết
support.status_closedClosedĐã đóng

Category labels:

Keyenvi
support.category_bugBug / ErrorLỗi
support.category_feature_requestFeature RequestĐề xuất tính năng
support.category_billingBilling / PaymentThanh toán
support.category_accountAccount / LoginTài khoản / Đăng nhập
support.category_integrationIntegration / ConnectorTích hợp
support.category_otherOtherKhác

Priority labels:

Keyenvi
support.priority_lowLowThấp
support.priority_mediumMediumTrung bình
support.priority_highHighCao
support.priority_criticalCriticalNghiêm trọng

Thread/reply keys:

Keyenvi
support.reply_placeholderType your reply here…Nhập phản hồi của bạn…
support.sendSendGửi
support.close_ticketClose ticketĐóng yêu cầu
support.reopen_ticketReopen ticketMở lại yêu cầu
support.internal_noteAdd internal note (staff only)Thêm ghi chú nội bộ (chỉ nhân viên)
support.internal_note_badge🔒 Internal🔒 Nội bộ
support.lumi_team_badgeLumi TeamLumi Team
support.you_labelYouBạn
support.just_nowJust nowVừa xong
support.time_ago{time} ago{time} trước

Staff dashboard keys:

Keyenvi
support.staff_assign_to_meAssign to meGán cho tôi
support.staff_unassignedUnassignedChưa gán
support.staff_assigned_toAssigned to {name}Đã gán cho {name}
support.staff_summary_open{count} Open{count} Đang mở
support.staff_summary_in_progress{count} In progress{count} Đang xử lý
support.staff_unresolved_48h{count} Unresolved >48h{count} Chưa giải quyết >48h
support.staff_filter_allAllTất cả
support.staff_filter_openOpenĐang mở
support.staff_filter_in_progressIn progressĐang xử lý
support.staff_filter_resolvedResolvedĐã giải quyết
support.staff_filter_closedClosedĐã đóng

Feedback keys:

Keyenvi
support.feedback_titleWas this resolved?Vấn đề đã được giải quyết?
support.feedback_thumbs_upYesCó
support.feedback_thumbs_downNot reallyChưa

Notification keys:

Keyenvi
support.notification_replyNew reply on your ticket: {subject}Phản hồi mới cho yêu cầu: {subject}
support.notification_status_changeTicket status updated: {subject} → {status}Trạng thái yêu cầu đã cập nhật: {subject} → {status}
support.notification_push_reply{staff_name} replied to your ticket{staff_name} đã phản hồi yêu cầu của bạn
support.notification_push_statusTicket {id} is now {status}Yêu cầu {id} hiện {status}

Validation/error keys:

Keyenvi
support.error_subject_requiredSubject is requiredVui lòng nhập tiêu đề
support.error_description_requiredDescription is requiredVui lòng nhập mô tả
support.error_file_too_largeFile must be under 10 MBTệp phải dưới 10 MB
support.error_too_many_filesMaximum 3 files allowedTối đa 3 tệp
support.error_ticket_not_foundTicket not foundKhông tìm thấy yêu cầu
support.error_cannot_reply_closedCannot reply to a closed ticketKhông thể phản hồi yêu cầu đã đóng
support.error_feedback_not_resolvedFeedback is only available on resolved ticketsPhản hồi chỉ khả dụng cho yêu cầu đã giải quyết

7. Test Plan

Coverage note (2026-06-14): Support tests live in backend/tests/test_support.py with classes TestTicketCreate, TestTicketListScoping, TestTicketDetail, TestStaffOnly, TestMessages, TestInternalNotes, TestClosePermission, TestStatusWorkflow, TestFeedback, TestTicketIdFormat, TestStaffReopenTicket, TestAttachmentLimits. Rows below use original per-case naming; the file is authoritative.

#CaseTypeWhere
1Create ticket with valid data returns 201backendtests/test_support.py::TestTicketCreate
2Create ticket with empty subject returns 400backendtests/test_support.py
3Create ticket with all optional category+priority fields returns 201backendtests/test_support.py
4User sees only their own ticketsbackendtests/test_support.py::TestTicketListScoping
5Staff sees all ticketsbackendtests/test_support.py
6Non-staff gets 403 on staff dashboard endpointbackendtests/test_support.py::TestStaffOnly
7Close ticket by non-creator returns 403backendtests/test_support.py::TestClosePermission
8Internal note invisible to non-staffbackendtests/test_support.py::TestInternalNotes
9Staff creates internal note on another staff’s ticket — visible to all staffbackendtests/test_support.py
10Status transition: open → in_progress → resolved → closedbackendtests/test_support.py::TestStatusWorkflow
11Status transition: closed → reopened → in_progress (staff only)backendtests/test_support.py
12Invalid status transition returns 400backendtests/test_support.py
13Attachments on ticket messages (F34 integration)backendtests/test_support.py
14Attachments: too many files (>3) returns 400backendtests/test_support.py::TestAttachmentLimits
15Submit ticket → user sees confirmation and detail pagee2esupport-ticketing.spec.ts
16Staff dashboard → filter by status, priority, and categorye2esupport-ticketing.spec.ts
17Staff assigns ticket to self → assigned_to updatede2esupport-ticketing.spec.ts
18User closes own ticket → status = closede2esupport-ticketing.spec.ts
19User replies to own ticket → status changes to waiting_on_staffe2esupport-ticketing.spec.ts
25Support list page loads without errore2esupport-ticketing.spec.ts
26Create ticket form opens and can be fillede2esupport-ticketing.spec.ts
27Support list empty state or ticket items visiblee2esupport-ticketing.spec.ts
28Sidebar has Support link in Admin groupe2esupport-ticketing.spec.ts
29Back navigation from create ticket goes to liste2esupport-ticketing.spec.ts
20Staff adds internal note — invisible to ticket creator via APIbackendtests/test_support.py
21Feedback prompt: resolved ticket shows thumbs up/downbackendtests/test_support.py
22Email notification sent on staff reply (F35 integration)backendtests/test_support.py
23Push notification sent on status change (F36 integration)backendtests/test_support.py
24Cross-tenant isolation: user sees only own ticketsbackendtests/test_multitenancy.py

8. Current Behavior

Partial implementation. apps/support/ contains Ticket + TicketMessage models, TicketViewSet, staff gates, display IDs, F34 attachment M2M, reopen and feedback endpoints. Frontend support pages and routes exist. The current audit does not claim a separate TicketMessageViewSet, live notification dispatch, tenant-isolated staff access, browser execution, or production behavior.

9. Error States

#ScenarioUser-visibleInternalLogRetry
1File too large (>10 MB)Field validation: “File exceeds 10 MB limit.”Validation error, file discardedINFO “support_file_too_large”Re-upload with smaller file
2Too many files (>3)Field validation: “Maximum 3 files per ticket.”Validation error, extra files discardedINFO “support_too_many_files”Re-upload with fewer files
3Invalid file typeField validation: “Unsupported file type. Accepted: images, PDF, text.”Validation error, file discardedINFO “support_invalid_file_type”Re-upload with correct type
4Ticket not found404404 response, loggedWARNING “support_ticket_not_found”n/a
5Non-staff accesses /hq/supportredirected to /work/today by HqLayout; manage API 403Logged for auditINFO “support_staff_access_denied”n/a
6Email notification fails (F35)No visible change — message savedEmail error loggedERROR “support_email_failed”Retry once after 5 min
7Invalid status transition400: “Transition from {current} to {target} is not allowed.”Validation errorWARNING “support_invalid_transition”User selects valid transition
8F34 upload failsToast: “File upload failed. Message saved without attachment.”Upload errorWARNING “support_upload_failed”Re-upload in reply
9Non-creator tries to close ticket403Permission deniedINFO “support_close_denied”n/a
10Feedback submitted for non-resolved ticket400: “Feedback is only available for resolved tickets.”Validation errorWARNING “support_feedback_invalid_state”Wait until resolved
11Ticket creator deleted — ticket still visible to staffTicket shows “Deleted user” as creatorPreserve creator_email, FK = NULLINFO “support_creator_deleted”n/a
12Staff assignee removed from staff — auto-unassignedStaff sees unassigned ticket in dashboardAssignee set to NULLINFO “support_assignee_demoted”Staff reassigns
13Non-staff sets is_internal=true on message403. is_internal silently forced to false.Message saved without internal flag.DEBUG “support_internal_note_denied”n/a
14Staff assigns ticket to non-staff user400: “Assignee must be a staff member.”assigned_to unchanged.INFO “support_assign_non_staff”n/a
15Reply on closed ticket400: “Cannot reply to a closed ticket.” Re-open to reply.Message not created.INFO “support_reply_closed”n/a

10. Diagrams & Docs Touched

  • backend/DIAGRAMS.md — add SupportTicket model + status state machine.
  • GLOSSARY.md — entry for “Ticket”, “Staff”, “Internal Note”.

11. Implementation Audit

Edge Cases Catalog (formerly §9, 2026-06-14; 17 items): ticket creator account deletion (creator_email preserved), staff member loses is_staff (assigned tickets auto-unassigned), multiple close/reopen cycles (feedback prompt resets), race condition on simultaneous staff+user reply (last-write-wins), file upload failure (message saved without attachment), email notification failure (message saved, retry once after 5m), wrong category (staff can change), long ticket thread (paginated 20/page), anonymous user 401 redirect, weekend reply auto-status, notification infra down (ticket saved regardless), internal note does not change waiting state, staff user creates own ticket (normal ticket not auto-assigned), message with no body but attachment (valid), XSS sanitization via markdown sanitizer (F28), multiple staff assign (last-write-wins), status transition rules backend-enforced. Full catalog in git history.

#ClaimStatusEvidence
1Ticket + TicketMessage models✅ matchbackend/apps/support/models.py
2backend/apps/support/ app (migrations, views, serializers, services, urls)✅ matchbackend/apps/support/ (full app)
3TicketViewSet only; message creation via add_message @action(url_path="messages") on TicketViewSet (no separate TicketMessageViewSet exists)✅ matchbackend/apps/support/views.py line 124
4Staff permission check (User.is_staff gate)✅ matchInline in backend/apps/support/views.py — no standalone permissions.py exists in the support app; is_staff checks are in TicketViewSet.get_queryset(), update(), add_message(), attach_file(), reopen(), and close()
5File attachments via F34 (Ticket.attachments M2M → uploads.Upload)✅ matchbackend/apps/support/serializers.py; models.py
6Frontend pages + routes (/support, /hq/support)✅ matchRegistry E2E ✅; frontend/src/pages/Support*, frontend/src/pages/hq/HqSupportPage.tsx
7support i18n namespace (all 60+ locales)✅ matchfrontend/src/i18n/locales/*/support.json
8display_id field (T-{id}) via _next_display_id()✅ matchBL 2026-06-14; TestTicketIdFormat
9Attachment limits (10 MB, 3 files) + POST /attach/ enforcement✅ matchBL 2026-06-14; UPLOAD_MAX_SIZE (F34)
10Ticket feedback endpoint (POST /api/support/tickets/<id>/feedback/)✅ matchBL 2026-06-14; TestFeedback
11Staff re-open closed tickets (POST /reopen/; VALID_TRANSITIONS["closed"])✅ matchBL 2026-06-14; TestStaffReopenTicket

11.1 Key Design Constraints

ConstraintDetail
Tickets are user-scoped, NOT org-scopedcreated_by FK on User, not OrganizationMember. User retains tickets across orgs
Staff dashboard is is_staff gatedSame pattern as F43 HQ
Attachments reuse F34 infrastructurePre-signed S3 uploads — no separate upload endpoint
Internal notes are invisible to non-staffBackend enforces: is_internal=True messages filtered out in user’s ticket detail response
Notifications are best-effortTicket saved regardless of notification delivery — never block ticket creation on notification send
Status transitions are backend-enforcedFrontend only shows valid transitions — backend validates regardless

12. Open Questions / Decisions

  1. Display ID format. Auto-increment T-{id} vs UUID for ticket list. Decision: UI shows T-{id}, PK stays UUID.

  2. File storage for attachments. Reuse F34 (pre-signed S3 uploads) or accept direct uploads. Decision: reuse F34 infrastructure — consistent, already audited.

  3. Ticket deletion. Should users be able to delete their own tickets? Decision: no. Tickets are immutable audit records once created. Users can close them.

  4. Staff flag vs dedicated permission model. Use Django’s built-in User.is_staff or create a separate support_staff group. Decision: use is_staff for v1. It’s already available, tested in HQ routes. A dedicated support role can be added later if needed.

13. Bug / Change Log

2026-07-02 — §5/§11 audit drift: PATCH gate, TicketMessageViewSet (docs-only)

Type: Documentation

Summary:

  • §5 PATCH row: auth column corrected “creator or staff” → “staff only”; errors column corrected “403 if non-staff sets assigned_to” → “403 for non-staff (blanket)”. Verified: partial_update in backend/apps/support/views.py (~L87–88) returns 403 for all non-staff unconditionally — not just when assigned_to is set.
  • §11 row 3: removed false TicketMessageViewSet claim; corrected to TicketViewSet only with message creation via add_message @action(url_path="messages") at views.py line 124. No TicketMessageViewSet class exists in the file.
  • No code changes.

2026-07-02 — Fix stale path: backend/apps/support/permissions.py (batch 3)

Type: Documentation

Summary:

  • §11 row 4 referenced backend/apps/support/permissions.py as the evidence for the User.is_staff permission gate. This file does not exist — ls backend/apps/support/ confirms no permissions.py. The is_staff checks are inline in backend/apps/support/views.py (multiple methods in TicketViewSet and TicketMessageViewSet).
  • Updated the evidence cell to point at backend/apps/support/views.py with the specific method list.
  • No code changes.

2026-06-14 — Spec restructured to TEMPLATE.md canonical order (§1–§13)

Type: Documentation Change

Summary:

  • §9 Edge Cases (17 items, 9.1–9.17) absorbed into §11 Implementation Audit EC note.
  • §10 Error States → §9, §11 Diagrams & Docs Touched → §10, §12 Implementation Audit → §11 (canonical 4-col table, 11 rows, all ✅ match).
  • §12.1 Key Design Constraints (h2 subheading, non-standard) → ### 11.1 Key Design Constraints (h3 under §11).
  • §13 Open Questions → §12, §14 Bug / Change Log → §13.

Reason:

  • Align with TEMPLATE.md canonical section order per SP fleet restructuring pass.

Impact:

  • Spec authors and future implementation agents.

2026-06-13 — Spec refinement: numbered edge cases, detailed error matrix, expanded test plan

Type: Documentation Change

Summary:

  • Added §9 with 9 numbered edge cases (9.1–9.9) covering: account deletion, staff revocation, multiple close/reopen cycles, race conditions on reply, file upload failure, email notification failure, wrong category, long threads, and unauthenticated access.
  • Replaced §10 error states with 12-row detailed matrix (user-visible/internal/log/retry), adding coverage for file types, feedback state validation, creator deletion, and staff demotion.
  • Expanded §7 test plan from 11 to 24 test cases, adding coverage for optional fields, cross-staff notes, reopen transitions, attachment limits, feedback prompt, email/push notifications, and cross-tenant isolation.
  • Updated spec status from draft to agreed.

Reason:

  • Spec needed hardening per implementation audit. Edge cases were absent, test plan was thin, error states lacked structured detail.

Impact:

  • Spec authors and future implementation agents.

Tests:

  • Spec-only change — no code tests affected.

2026-06-14 — Confirmed no backend support app exists; updated §12

Type: Documentation Change

Summary:

  • Updated §12 Implementation Audit with concrete scan results: no apps/support/ directory, no support in INSTALLED_APPS, no Ticket/TicketMessage models found.
  • No AC, API, or model changes needed — spec already thorough from round 1 hardening.

Reason:

  • Per T4-G round 2 task: verify support app backend completeness. Found empty — documented for implementation agents.

Impact:

  • Implementation agents must create apps/support/ from scratch per §4 data model. Tenant isolation checks (TestSupportIsolation) will pass only after the model exists.

Tests:

  • Spec-only change — no code tests affected.

2026-06-14 — Attachment limits, reopen action, all rework items closed

Type: Feature

Summary:

  • Added Ticket.attachments = ManyToManyField("uploads.Upload", ...) + migration 0002_add_ticket_attachments.
  • Added POST /api/support/tickets/<id>/attach/ action: enforces cumulative ≤3 file limit, requires status=completed, returns 400 on violation.
  • Added POST /api/support/tickets/<id>/reopen/ action: staff-only, ticket must be closed, transitions to open.
  • TicketDetailSerializer now includes attachments as embedded upload summaries (id, filename, public_url, content_type, byte_size).
  • All 4 rework items struck — display_id and feedback were already implemented; attachment limits and reopen are new.

Tests:

  • TestAttachmentLimits (5 tests): single attach, too-many-in-one-request, cumulative limit exceeded, pending upload rejected, non-creator-non-staff blocked.
  • TestStaffReopenTicket (3 tests): staff reopen success, non-staff 403, non-closed 400.