Files
support_backend/specs/003-ticketing/tasks.md
T
saqib mirandClaude Sonnet 5 2edfbacf82 feat: implement ticket creation, messages & attachments (003-ticketing)
Implements all 39 tasks from specs/003-ticketing/tasks.md across all
three user stories -- Phase 5 of the roadmap.

Schema (prisma/schema.prisma + migration):
- Ticket (code, status, version for optimistic concurrency,
  idempotencyKey, customerId FK), Problem, TicketMessage,
  TicketAttachment per docs/06, with Product/Category/
  CustomerReference back-relations.

User Story 1 -- ticket/problem creation (P1, MVP):
- Explicit 12-state lifecycle adjacency table
  (ticket-state-machine.ts), not "any transition allowed."
- Ticket code generation (<PRODUCT_CODE>-<YEAR>-<SEQUENCE>) scoped
  by the actual code prefix, not productId -- see the collision bug
  fixed below.
- Idempotency-key enforcement via atomic create-then-catch-conflict
  (never a read-then-write race), completing the FR-012 placeholder
  from 002-saas-integration.
- Explicit-reference-only recurring-problem linking (no fuzzy
  matching -- that's a future AI-support concern).
- POST /v1/support/requests (002-saas-integration) now creates a
  real ticket instead of echoing context back.
- PATCH /tickets/:id/status with expectedVersion-based optimistic
  concurrency (409 on stale version, 400 on an invalid transition).

User Story 2 -- typed messages (P2):
- Message type -> visibleToCustomer mapping is a fixed constant map,
  never caller-supplied; customer-scoped reads filter at the query
  layer so an internal note is never fetched, not just hidden.
- POST/GET /tickets/:id/messages (customer-scoped) and
  GET /agent/tickets/:id/messages (agent-scoped).

User Story 3 -- attachment pipeline (P3):
- Presigned-PUT upload (new getPresignedUploadUrl on the existing
  storageService) -- file bytes never transit this API.
- A MalwareScanner interface with a fail-closed placeholder
  (UnimplementedPlaceholderScanner) since no scanner exists in this
  stack -- it always reports 'infected', never silently 'clean'.
- The existing attachments-queue job stub now actually calls the
  scanner and updates scanStatus; registerAttachmentWorker() is
  wired into bootstrapQueue() (previously defined but never called).
- Downloads are gated on scanStatus === 'clean' -- currently always
  refused until a real scanner replaces the placeholder.
- MinIO added to docker-compose.{test,development}.yml for local/CI
  S3-compatible storage, matching doc 04's explicit guidance.

Two real bugs found and fixed via integration testing against a
live Postgres/Redis/MinIO (not just typechecked):
- Ticket codes could collide across different products: the
  sequence counter was scoped by internal productId, but the code
  column's uniqueness is global, and deriveProductCode's 4-character
  truncation means different products can share a prefix. Fixed by
  counting against the actual code prefix instead.
- Three existing 002-saas-integration integration tests' cleanup
  started failing an FK RESTRICT check once ticket creation was
  wired in (deleting a Product before the Ticket/Problem that now
  reference it). Fixed their afterAll ordering.

All 9 integration test files (24 tests, spanning this feature and
the pre-existing suite) verified passing against real Postgres,
Redis, and MinIO, including a genuine presigned-PUT/GET round trip.
Full quality gate (typecheck/lint/format/architecture/unit tests)
passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 15:21:37 +05:30

14 KiB

description
description
Task list for 003-ticketing

Tasks: Ticket Creation, Messages & Attachments

Input: Design documents from specs/003-ticketing/

Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/ticket-lifecycle-contract.md, quickstart.md

Tests: Included as first-class tasks (state machine, idempotency, and visibility enforcement are exactly the kind of "MUST" requirements regressions silently break), same approach as 002-saas-integration.

Organization: Tasks are grouped by user story (US1 = P1 ticket/problem creation, US2 = P2 messages, US3 = P3 attachments).

Format: [ID] [P?] [Story] Description

All file paths are relative to supporthub-api/ (repo root).


Phase 1: Setup

  • T001 Add a minio service to docker-compose.test.yml and docker-compose.development.yml (image minio/minio, console + API ports), and set AWS_S3_ENDPOINT/AWS_S3_BUCKET/AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY in .env.test/.env.development to point at it (research.md "local/test object storage")
  • T002 [P] Add getPresignedUploadUrl(objectName, contentType, expirySeconds?) to src/infrastructure/storage/storage.service.ts, mirroring the existing getPresignedUrl/PutObjectCommand pattern already used by uploadFile

Checkpoint: Object storage is reachable locally/in CI; the service can mint both upload and download presigned URLs.


Phase 2: Foundational (Blocking Prerequisites)

Purpose: Schema and shared primitives (state machine, visibility map, scanner interface) every user story depends on.

⚠️ CRITICAL: No user-story stage work can begin until this phase is complete.

  • T003 Add Ticket, Problem, TicketMessage, TicketAttachment models to prisma/schema.prisma per data-model.md (including Ticket.version, Ticket.idempotencyKey, Ticket.customerId FK to the existing CustomerReference, Ticket.categoryId FK to the existing Category)
  • T004 Run npm run prisma:generate and create the migration (npm run prisma:migrate) for T003 (depends on T003)
  • T005 [P] Define the 12-state adjacency table and a pure isValidTransition(from, to): boolean function in src/modules/ticketing/tickets/mapper/ticket-state-machine.ts per research.md's exact edge list
  • T006 [P] Define the message type→visibility constant map and a pure isVisibleToCustomer(type): boolean function in src/modules/ticketing/messages/mapper/message-visibility.ts per research.md
  • T007 [P] Define the MalwareScanner interface and the fail-closed UnimplementedPlaceholderScanner in src/modules/ticketing/attachments/mapper/malware-scanner.ts per research.md — logs a loud warning on every call
  • T008 [P] Add a ticket-code generator (generateTicketCode(externalProductId, sequence): string) in src/modules/ticketing/tickets/mapper/ticket-code.ts per research.md's format

Checkpoint: Schema migrated; state machine, visibility map, scanner interface, and code generator exist and are independently unit-testable. User stories can now be built.


Phase 3: User Story 1 - A trusted request creates a durable ticket immediately (Priority: P1) 🎯 MVP

Goal: POST /v1/support/requests (002-saas-integration) creates a real Ticket+Problem instead of echoing context back; ticket status transitions are validated and concurrency-safe.

Independent Test: Quickstart Scenarios 1, 2, 3, 6.

Tests for User Story 1

  • T009 [P] [US1] Unit tests for ticket-state-machine.ts (every valid edge accepted, a sample of invalid edges rejected) in tests/unit/ticketing/ticket-state-machine.test.ts
  • T010 [P] [US1] Unit tests for ticket-code.ts (format, per-product-per-year sequencing) in tests/unit/ticketing/ticket-code.test.ts
  • T011 [US1] Integration test covering Quickstart Scenarios 1, 2, 3, 6 (creation, idempotent retry, recurring-problem linking via referenceIds, concurrent status-update rejection) against a real Postgres in tests/integration/ticket-creation.test.ts

Implementation for User Story 1

  • T012 [US1] Add ProblemsRepository (create; find-by-reference using an explicit prior ticket/problem id — research.md's explicit-reference-only rule) in src/modules/ticketing/tickets/repository/problems.repository.ts (depends on T004)
  • T013 [US1] Add TicketsRepository (atomic create with idempotency-key upsert per data-model.md's @@unique([productId, idempotencyKey]); findById; findByCode; updateStatus using the version-based optimistic-concurrency UPDATE ... WHERE version = ? from research.md) in src/modules/ticketing/tickets/repository/tickets.repository.ts, replacing the old placeholder findAllProducts-style stub (depends on T004)
  • T014 [US1] Add TicketMessagesRepository.create (used internally for the SYSTEM_EVENT creation/transition record — full messages CRUD is User Story 2) in src/modules/ticketing/messages/repository/messages.repository.ts (depends on T004, T006)
  • T015 [US1] Add TicketsService.createFromInboundRequest(reqContext, body): resolves/creates the Problem (T012), creates/fetches the Ticket (T013), writes the creation SYSTEM_EVENT message (T014) — all synchronous within one request (FR-001) — in src/modules/ticketing/tickets/service/tickets.service.ts (depends on T012, T013, T014)
  • T016 [US1] Add TicketsService.updateStatus(ticketId, newStatus, expectedVersion, actor): validates the transition via isValidTransition (T005), calls the repository's optimistic update, writes a SYSTEM_EVENT message on success, throws 409 CONFLICT on version mismatch and 400 INVALID_TRANSITION on an invalid edge (depends on T005, T013, T014)
  • T017 [US1] Replace src/modules/catalog/products/routes/inbound-request.routes.ts's stub handler: call TicketsService.createFromInboundRequest and respond with { ticketId, code, status, problemId } instead of echoing reqContext back (depends on T015)
  • T018 [US1] Add PATCH /tickets/:ticketId/status (body { status, expectedVersion }) and GET /tickets/:ticketId routes, tenant-scoped per FR-015, in src/modules/ticketing/tickets/routes/tickets.routes.ts, replacing the old placeholder GET /tickets list stub; register the module's routes from src/api/routes.ts (depends on T016)
  • T019 [US1] Run Quickstart Scenarios 1, 2, 3, 6 locally and confirm all four pass

Checkpoint: User Story 1 is fully functional — every trusted inbound request produces a real, concurrency-safe, idempotent ticket. This is a deployable/demoable increment even before messages/attachments exist.


Phase 4: User Story 2 - Ticket messages are typed, and internal notes are never visible to customers (Priority: P2)

Goal: Full message CRUD with visibility enforced at the query layer.

Independent Test: Quickstart Scenario 4.

Tests for User Story 2

  • T020 [P] [US2] Unit tests for message-visibility.ts (every type maps correctly, including that the mapping can't be overridden by caller input at the type level) in tests/unit/ticketing/message-visibility.test.ts
  • T021 [US2] Integration test covering Quickstart Scenario 4 (post one of each type; confirm customer-scoped read excludes internal types entirely; confirm agent-scoped read includes all) against a real Postgres in tests/integration/ticket-messages.test.ts

Implementation for User Story 2

  • T022 [US2] Extend TicketMessagesRepository with findVisibleToCustomer(ticketId) (WHERE visibleToCustomer = true, per data-model.md's index) and findAll(ticketId) (agent-scope) — both tenant-scoped per FR-015 (depends on T014)
  • T023 [US2] Add MessagesService.post(ticketId, actor, type, body) (sets visibleToCustomer from isVisibleToCustomer(type) — never from request input, FR-008) and .listForCustomer/.listForAgent in src/modules/ticketing/messages/service/messages.service.ts (depends on T006, T022)
  • T024 [US2] Add routes in src/modules/ticketing/messages/routes/messages.routes.ts: POST /tickets/:ticketId/messages (customer/agent both post, gated by fastify.authenticate), GET /tickets/:ticketId/messages (customer-scoped), GET /agent/tickets/:ticketId/messages (agent-scoped) — register from src/api/routes.ts (depends on T023)
  • T025 [US2] Run Quickstart Scenario 4 locally and confirm it passes

Checkpoint: Both User Story 1 and 2 work together — a created ticket now has a real, correctly-scoped message timeline.


Phase 5: User Story 3 - Attachments are safely stored and only downloadable through expiring, authorized URLs (Priority: P3)

Goal: Presigned-PUT upload → confirm → async scan → scan-gated presigned-GET download.

Independent Test: Quickstart Scenario 5.

Tests for User Story 3

  • T026 [P] [US3] Unit tests for UnimplementedPlaceholderScanner (always resolves 'infected', never throws) in tests/unit/ticketing/malware-scanner.test.ts
  • T027 [US3] Integration test covering Quickstart Scenario 5 (upload-url → confirm → download refused while pending → download refused after the placeholder scanner marks infected) against a real Postgres/Redis/MinIO in tests/integration/ticket-attachments.test.ts

Implementation for User Story 3

  • T028 [US3] Add AttachmentsRepository (create with scanStatus: 'pending'; findById; updateScanStatus) in src/modules/ticketing/attachments/repository/attachments.repository.ts (depends on T004)
  • T029 [US3] Add AttachmentsService.requestUploadUrl(ticketId, fileName, mimeType, sizeBytes): validates type/size against configured limits (FR-012) before calling storageService.getPresignedUploadUrl (T002) — depends on T002
  • T030 [US3] Add AttachmentsService.confirmUpload(ticketId, storageKey, fileName, mimeType, sizeBytes, uploadedBy): creates the TicketAttachment row (T028) and enqueues a job on QueueName.ATTACHMENTS via the existing queueManager (depends on T028)
  • T031 [US3] Add AttachmentsService.requestDownloadUrl(ticketId, attachmentId): returns storageService.getPresignedUrl only when scanStatus === 'clean', else throws 409 with the current status (FR-013/FR-014) — depends on T028
  • T032 [US3] Replace the log-only stub in src/jobs/attachments/index.ts: call the bound MalwareScanner (T007), then AttachmentsRepository.updateScanStatus with the result — depends on T007, T028
  • T033 [US3] Wire registerAttachmentWorker() into src/bootstrap/queue.bootstrap.ts (it's currently defined but never called anywhere) — depends on T032
  • T034 [US3] Add routes in src/modules/ticketing/attachments/routes/attachments.routes.ts: POST /tickets/:ticketId/attachments/upload-url, POST /tickets/:ticketId/attachments/:attachmentId/confirm, GET /tickets/:ticketId/attachments/:attachmentId/download-url — register from src/api/routes.ts (depends on T029, T030, T031)
  • T035 [US3] Run Quickstart Scenario 5 locally and confirm it passes

Checkpoint: All three user stories work independently and together — a ticket now has creation, a message timeline, and a securely-gated attachment pipeline.


Phase 6: Polish & Cross-Cutting Concerns

  • T036 [P] Add a "Ticketing" section to README.md describing the inbound-to-ticket flow, the status-transition contract, and the attachment pipeline (including that downloads are permanently blocked until a real MalwareScanner replaces the placeholder)
  • T037 [P] Update specs/003-ticketing/checklists/requirements.md Notes with any implementation-time findings (e.g. concurrency edge cases discovered while testing T011)
  • T038 Run npx tsx scripts/check-architecture.ts and npm run lint/npm run typecheck to confirm the new modules respect existing module-boundary rules
  • T039 Full regression: npm run test:unit (scoped to tests/unit, per 001/002's fix) to confirm nothing broke elsewhere

Dependencies & Execution Order

Phase Dependencies

  • Setup (Phase 1): No dependencies
  • Foundational (Phase 2): Depends on Setup — BLOCKS all user stories
  • User Story 1 (Phase 3): Depends on Foundational — no dependency on US2/US3
  • User Story 2 (Phase 4): Depends on Foundational + a Ticket existing to attach messages to — practically sequenced after US1, though its own repository/service/routes are independent code
  • User Story 3 (Phase 5): Depends on Foundational + a Ticket existing — independent of US2
  • Polish (Phase 6): Depends on all three user stories

Parallel Opportunities

  • T001/T002 (Setup)
  • T005/T006/T007/T008 (independent Foundational primitives)
  • T009/T010 (independent unit test files)
  • T020 alongside US1's later tasks once T006 exists
  • T026 alongside US1/US2's later tasks once T007 exists
  • T036/T037 in Polish

Implementation Strategy

MVP First (User Story 1 Only)

  1. Setup + Foundational (T001-T008)
  2. User Story 1 (T009-T019)
  3. STOP and VALIDATE: Quickstart Scenarios 1, 2, 3, 6 pass — every trusted request now produces a real, durable, idempotent, concurrency-safe ticket. This alone is a meaningful product milestone even before messages/attachments exist.

Incremental Delivery

  1. Setup + Foundational → schema migrated, primitives tested
  2. Add User Story 1 → tickets are real (MVP)
  3. Add User Story 2 → tickets have a correctly-scoped conversation timeline
  4. Add User Story 3 → tickets support secure attachments
  5. Polish → docs and full regression