---
title: Ticket Bounded Context
type: entity
created: 2026-09-06
updated: 2026-09-06
sources:
    [
        codebase snapshot 2026-09-06,
        raw/sessions/2026-09-06-frontend-integration.md,
    ]
tags: [ticket, context, issue-tracking, issue-model]
---

# Ticket Bounded Context

`Ticket` is the central **issue** model of Project-Board. It was originally a thin scaffold (summary, description, story points, assignee, board) but on 2026-09-06 was extended into the issue aggregate used by the frontend: it now carries a `type` (`feature` / `sub-task`), a `status`, a nullable parent link (for sub-tasks), a human-readable `ticket_key` (e.g. `PE-101`), label ids, and timestamps.

> Updated 2026-09-06: the issue-tracking frontend port turned `Ticket` into the issue aggregate (see below). This supersedes the earlier "no status/type/labels" observation.

## Domain

- **Aggregate:** `Domain\Ticket\Entities\Ticket` (`src/Domain/Ticket/Entities/Ticket.php`)
    - Constants: `TYPE_FEATURE`, `TYPE_SUB_TASK`, and `STATUSES` (`To Do`, `In Progress`, `Awaiting Review`, `QA`, `Done`).
    - Reconstitute-only (`reconstitute(TicketId, ?UserId $assignee, BoardId, ?TicketId $parentTicketId, ticketKey, type, status, summary, description, storyPoints, list<LabelId> $labelIds, createdAt, updatedAt)`).
    - Getters: `id()`, `assignee()` (nullable), `boardId()`, `parentTicketId()` (nullable), `ticketKey()`, `type()`, `status()`, `summary()`, `description()`, `storyPoints()`, `labelIds()`, `createdAt()`, `updatedAt()`, `isSubTask()`.
- **Value object:** `TicketId` (string id).
- **Repository interface:** `TicketRepositoryInterface` — `nextIdentity()`, `save()`, `findById()`, `findByBoard()`, **`findByWorkspace(WorkspaceId): list<Ticket>`** (across a workspace's boards), **`findChildren(TicketId): list<Ticket>`** (sub-tasks).

## Infrastructure

- `TicketModel` (table `tickets`, string PK `ticket_id`; relations: `board()`, `assignee()` (nullable), `parent()`/`children()`, `labels()` BelongsToMany, `timeEntries()` HasMany, `activities()` HasMany). `assignee` is nullable.
- `TicketMapper` — maps label ids via the eager-loaded `labels` relation, and the nullable `assignee`/`parent_ticket_id`.
- `EloquentTicketRepository` — `save()` also syncs the `label_ticket` pivot; the list reads eager-load `labels`.

## Persistence / relationships

The `tickets` table was created in the (misnamed) `2026_09_05_071303_add_tickets_table.php` migration and extended by `2026_09_06_000000_extend_for_issue_tracking.php` (added `ticket_key`, `type`, `status`, nullable `parent_ticket_id`, made `assignee` nullable, and created `label`/`label_ticket`/`time_entries`/`activities`). It has a unique `summary`, FKs to `board.board_id` and `users.user_id`, and a `story_points` integer.

## Application / presentation

- Queries: `ListTickets` (issues for a workspace), `GetTicket` (detail + parent + subtasks + time entries).
- Commands: `CreateTicket`, `UpdateTicket` (records a `changed status` activity), plus `LogTime`/`CreateLabel` for related concerns.
- Presentation: `IssueController` (`/w/{workspace}/issues`) renders the issues list and ticket detail pages.

## Observations / open questions

- **Sub-task statuses** exclude `QA` in the UI, but the aggregate allows any of `STATUSES`; validation for sub-tasks isn't enforced server-side yet.
- **`findByBoard`** is now largely superseded by `findByWorkspace`, which is the read path the UI uses.

## Related pages

- [board](board.md) — parent context
- [workspace](workspace.md) — the top of the hierarchy
- [label](label.md) — labels attached to tickets
- [time-entry](time-entry.md) — time logged against tickets
- [activity](activity.md) — activity recorded on tickets
- [user](user.md) — the assignee reference
- [repository-mapper-pattern](../concepts/repository-mapper-pattern.md) — persistence
- [overview](../overview.md) — Jira-ward goal