265 lines
14 KiB
Markdown
265 lines
14 KiB
Markdown
---
|
|||
|
|
description: "Task list for 003-ticketing"
|
||
|
|
---
|
||
|
|
|
||
|
|
# Tasks: Ticket Creation, Messages & Attachments
|
||
|
|
|
||
|
|
**Input**: Design documents from `specs/003-ticketing/`
|
||
|
|
|
||
|
|
**Prerequisites**: [plan.md](./plan.md), [spec.md](./spec.md), [research.md](./research.md),
|
||
|
|
[data-model.md](./data-model.md), [contracts/ticket-lifecycle-contract.md](./contracts/ticket-lifecycle-contract.md),
|
||
|
|
[quickstart.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
|