Forge tickets module — tickets state machine + messages/notes/worklogs + SLA scanner. Sprint 57: 4 services + worker extracted. ticket_attachments + routers deferred pending media pipeline refactor.
| Tier | application |
| Role | unclassified (baselined) |
| Path | crates/application/tickets |
| Edition | 2024 |
| Targets | application_tickets |
| Public items | 59 across 11 modules |
| Tests | 10 |
What it is for
application-tickets — Layer 3 module for ticketing.
Sprint 57 extracted the services (tickets, ticket_messages, ticket_notes, ticket_worklogs) plus the sla_scanner worker. Sprint 65 added the admin + customer routers under api.
B-009 Gate-0.5 investigation: unlike application-catalog/application-encounters/ application-reviews, there is no primitives-tickets domain crate for this module to wrap — no domain-tickets (or similarly named) crate exists anywhere in the workspace, and this crate's Cargo.toml depends only on infrastructure-jobs, infrastructure-cron (each already wired by their own application-jobs/forge-cron modules), and platform-dto (types only, no schema). The tickets / ticket_messages / ticket_notes / ticket_worklogs / ticket_attachments tables that services::* reads/writes were authored directly in application-engine's central platform set (crates/application/engine/migrations/ 074_tickets.sql through 078_ticket_attachments.sql) and were never extracted into a standalone library crate crate with its own migrations/ directory, so there is nothing this module can vendor via include_str!. migrations() staying empty is correct, not an oversight; a host mounting application-tickets standalone (outside application-engine) must supply those five tables itself — including the encounters table 074_tickets.sql's own encounter_id FK requires (see application-encounters, already wired).
Capabilities
TicketsModule
application-tickets — Layer 3 module for ticketing.
| Item |
|---|
pub struct TicketsModule |
TicketsModule :: fn new() -> Self |
TicketsModule :: fn name(& self) -> & 'static str |
TicketsModule :: fn version(& self) -> & 'static str |
TicketsModule :: fn migrations(& self) -> Vec <MigrationSet> |
TicketsModule :: async fn migrate_pending(& self, _pool : & PgPool) -> Result <u32, MigrationError> |
api (other)
Ticketing REST routers — Sprint 65: extracted from rust-cms-engine.
| Item |
|---|
fn admin_router <S>() -> Router <S> where S : Clone + Send + Sync + 'static, PgPool : FromRef <S>, |
fn public_router <S>() -> Router <S> where S : Clone + Send + Sync + 'static, PgPool : FromRef <S>, |
api::tickets_admin (other)
Tickets admin REST endpoints (Sprint 43 — Gate 1.5 Q2).
| Item |
|---|
fn router <S>() -> Router <S> where S : Clone + Send + Sync + 'static, PgPool : FromRef <S>, |
api::tickets_customer (other)
Customer-facing tickets REST endpoints (Sprint 43).
| Item |
|---|
fn router <S>() -> Router <S> where S : Clone + Send + Sync + 'static, PgPool : FromRef <S>, |
services::ticket_attachments (other)
Ticket attachments service (Sprint 43).
| Item |
|---|
async fn attach_asset(pool : & PgPool, input : AttachAssetInput,) -> Result <TicketAttachment, TicketError> |
async fn soft_delete_attachment(pool : & PgPool, id : Uuid) -> Result <(), TicketError> |
AttachAssetInput
Ticket attachments service (Sprint 43).
| Item |
|---|
pub struct AttachAssetInput |
TicketAttachment
Ticket attachments service (Sprint 43).
| Item |
|---|
pub struct TicketAttachment |
async fn list_attachments_for_ticket(pool : & PgPool, ticket_id : Uuid, filter : VisibilityFilter,) -> Result <Vec <TicketAttachment>, TicketError> |
services::ticket_messages (other)
Ticket reply thread service (Sprint 43).
| Item |
|---|
async fn list_messages_for_ticket(pool : & PgPool, ticket_id : Uuid, filter : VisibilityFilter,) -> Result <Vec <TicketMessage>, TicketError> |
async fn create_message(pool : & PgPool, input : CreateMessageInput,) -> Result <TicketMessage, TicketError> |
async fn soft_delete_message(pool : & PgPool, id : Uuid) -> Result <(), TicketError> |
CreateMessageInput
Ticket reply thread service (Sprint 43).
| Item |
|---|
pub struct CreateMessageInput |
TicketMessage
Ticket reply thread service (Sprint 43).
| Item |
|---|
pub struct TicketMessage |
async fn get_message(pool : & PgPool, id : Uuid) -> Result <TicketMessage, TicketError> |
VisibilityFilter
Ticket reply thread service (Sprint 43).
| Item |
|---|
pub enum VisibilityFilter |
services::ticket_notes (other)
Internal ticket annotation service (Sprint 43). Staff-only.
| Item |
|---|
async fn create_note(pool : & PgPool, input : CreateNoteInput) -> Result <TicketNote, TicketError> |
async fn soft_delete_note(pool : & PgPool, id : Uuid) -> Result <(), TicketError> |
CreateNoteInput
Internal ticket annotation service (Sprint 43). Staff-only.
| Item |
|---|
pub struct CreateNoteInput |
TicketNote
Internal ticket annotation service (Sprint 43). Staff-only.
| Item |
|---|
pub struct TicketNote |
async fn list_notes_for_ticket(pool : & PgPool, ticket_id : Uuid,) -> Result <Vec <TicketNote>, TicketError> |
async fn record_transition(pool : & PgPool, ticket_id : Uuid, actor_party_id : Option <Uuid>, from_status : & str, to_status : & str,) -> Result <TicketNote, TicketError> |
services::ticket_worklogs (other)
Ticket worklog service — time tracking + timer sessions (Sprint 43).
| Item |
|---|
async fn create_worklog(pool : & PgPool, input : CreateWorklogInput,) -> Result <TicketWorklog, TicketError> |
async fn soft_delete_worklog(pool : & PgPool, id : Uuid) -> Result <(), TicketError> |
async fn cancel_timer(pool : & PgPool, staff_party_id : Uuid) -> Result <(), TicketError> |
CreateWorklogInput
Ticket worklog service — time tracking + timer sessions (Sprint 43).
| Item |
|---|
pub struct CreateWorklogInput |
TicketWorklog
Ticket worklog service — time tracking + timer sessions (Sprint 43).
| Item |
|---|
pub struct TicketWorklog |
async fn list_worklogs_for_ticket(pool : & PgPool, ticket_id : Uuid,) -> Result <Vec <TicketWorklog>, TicketError> |
async fn start_timer(pool : & PgPool, ticket_id : Uuid, staff_party_id : Uuid, description : Option <String>,) -> Result <TicketWorklog, TicketError> |
async fn stop_timer(pool : & PgPool, staff_party_id : Uuid) -> Result <TicketWorklog, TicketError> |
async fn active_timer(pool : & PgPool, staff_party_id : Uuid,) -> Result <Option <TicketWorklog>, TicketError> |
WorklogSummary
Ticket worklog service — time tracking + timer sessions (Sprint 43).
| Item |
|---|
pub struct WorklogSummary |
async fn worklog_summary(pool : & PgPool, ticket_id : Uuid,) -> Result <WorklogSummary, TicketError> |
CreateTicketInput
Tickets service — support ticketing library crate (Sprint 43).
| Item |
|---|
pub struct CreateTicketInput |
ListTicketsFilter
Tickets service — support ticketing library crate (Sprint 43).
| Item |
|---|
pub struct ListTicketsFilter |
Ticket
Tickets service — support ticketing library crate (Sprint 43).
| Item |
|---|
pub struct Ticket |
async fn create_ticket(pool : & PgPool, input : CreateTicketInput) -> Result <Ticket, TicketError> |
async fn get_ticket(pool : & PgPool, id : Uuid) -> Result <Ticket, TicketError> |
async fn list_tickets(pool : & PgPool, filter : ListTicketsFilter,) -> Result <Vec <Ticket>, TicketError> |
async fn update_ticket(pool : & PgPool, id : Uuid, input : UpdateTicketInput,) -> Result <Ticket, TicketError> |
async fn soft_delete_ticket(pool : & PgPool, id : Uuid) -> Result <(), TicketError> |
async fn assign_ticket(pool : & PgPool, id : Uuid, assignee_party_id : Option <Uuid>,) -> Result <Ticket, TicketError> |
async fn transition_ticket(pool : & PgPool, id : Uuid, transition : TicketTransition,) -> Result <Ticket, TicketError> |
TicketError
Tickets service — support ticketing library crate (Sprint 43).
| Item |
|---|
pub enum TicketError |
async fn maybe_set_first_response(pool : & PgPool, id : Uuid) -> Result <(), TicketError> |
async fn scan_sla_breaches(pool : & PgPool) -> Result <Vec <Uuid>, TicketError> |
TicketTransition
Tickets service — support ticketing library crate (Sprint 43).
| Item |
|---|
pub enum TicketTransition |
TicketTransition :: fn target(& self) -> & 'static str |
UpdateTicketInput
Tickets service — support ticketing library crate (Sprint 43).
| Item |
|---|
pub struct UpdateTicketInput |
workers::sla_scanner (other)
SLA scanner worker (Sprint 46; migrated to a real schedule in Sprint 2.4 US-2.4.12).
| Item |
|---|
pub const SLA_SCAN_JOB_TYPE: & str |
pub const DEFAULT_SLA_CRON: & str |
async fn run_sla_scanner_worker(pool : PgPool, tenant_id : Uuid, cron_expr : & str, shutdown : CancellationToken,) |
SlaScanPayload
SLA scanner worker (Sprint 46; migrated to a real schedule in Sprint 2.4 US-2.4.12).
| Item |
|---|
pub struct SlaScanPayload |
How to use it
No examples/ target and no doctest in this crate's rustdoc. The tests listed under Verification are the closest executable usage.
Module structure
application_tickets
apiapi::tickets_adminapi::tickets_customerservicesservices::ticket_attachmentsservices::ticket_messagesservices::ticket_notesservices::ticket_worklogsservices::ticketsworkersworkers::sla_scanner
flowchart TD n_application_tickets["application_tickets"] n_application_tickets --> n_api["api"] n_api --> n_api__tickets_admin["tickets_admin"] n_api --> n_api__tickets_customer["tickets_customer"] n_application_tickets --> n_services["services"] n_services --> n_services__ticket_attachments["ticket_attachments"] n_services --> n_services__ticket_messages["ticket_messages"] n_services --> n_services__ticket_notes["ticket_notes"] n_services --> n_services__ticket_worklogs["ticket_worklogs"] n_services --> n_services__tickets["tickets"] n_application_tickets --> n_workers["workers"] n_workers --> n_workers__sla_scanner["sla_scanner"]
Public surface
`crate root`
| Item | What it is |
|---|---|
pub struct TicketsModule | Tickets module — Layer 3 wrapper. |
TicketsModule :: fn new() -> Self | — |
TicketsModule :: fn name(& self) -> & 'static str | — |
TicketsModule :: fn version(& self) -> & 'static str | — |
TicketsModule :: fn migrations(& self) -> Vec <MigrationSet> | — |
TicketsModule :: async fn migrate_pending(& self, _pool : & PgPool) -> Result <u32, MigrationError> | — |
`api`
| Item | What it is |
|---|---|
fn admin_router <S>() -> Router <S> where S : Clone + Send + Sync + 'static, PgPool : FromRef <S>, | Staff-facing ticketing routes (tickets_admin). |
fn public_router <S>() -> Router <S> where S : Clone + Send + Sync + 'static, PgPool : FromRef <S>, | Customer-facing ticketing routes (tickets_customer). |
`api::tickets_admin`
| Item | What it is |
|---|---|
fn router <S>() -> Router <S> where S : Clone + Send + Sync + 'static, PgPool : FromRef <S>, | — |
`api::tickets_customer`
| Item | What it is |
|---|---|
fn router <S>() -> Router <S> where S : Clone + Send + Sync + 'static, PgPool : FromRef <S>, | — |
`services::ticket_attachments`
| Item | What it is |
|---|---|
pub struct TicketAttachment | — |
pub struct AttachAssetInput | — |
async fn list_attachments_for_ticket(pool : & PgPool, ticket_id : Uuid, filter : VisibilityFilter,) -> Result <Vec <TicketAttachment>, TicketError> | — |
async fn attach_asset(pool : & PgPool, input : AttachAssetInput,) -> Result <TicketAttachment, TicketError> | — |
async fn soft_delete_attachment(pool : & PgPool, id : Uuid) -> Result <(), TicketError> | — |
`services::ticket_messages`
| Item | What it is |
|---|---|
pub struct TicketMessage | — |
pub enum VisibilityFilter | Visibility filter passed to listing functions. |
pub struct CreateMessageInput | — |
async fn list_messages_for_ticket(pool : & PgPool, ticket_id : Uuid, filter : VisibilityFilter,) -> Result <Vec <TicketMessage>, TicketError> | List messages on a ticket |
async fn create_message(pool : & PgPool, input : CreateMessageInput,) -> Result <TicketMessage, TicketError> | Create a message |
async fn get_message(pool : & PgPool, id : Uuid) -> Result <TicketMessage, TicketError> | — |
async fn soft_delete_message(pool : & PgPool, id : Uuid) -> Result <(), TicketError> | — |
`services::ticket_notes`
| Item | What it is |
|---|---|
pub struct TicketNote | — |
pub struct CreateNoteInput | — |
async fn list_notes_for_ticket(pool : & PgPool, ticket_id : Uuid,) -> Result <Vec <TicketNote>, TicketError> | — |
async fn create_note(pool : & PgPool, input : CreateNoteInput) -> Result <TicketNote, TicketError> | — |
async fn record_transition(pool : & PgPool, ticket_id : Uuid, actor_party_id : Option <Uuid>, from_status : & str, to_status : & str,) -> Result <TicketNote, TicketError> | Convenience: write a system-typed note recording a status transition |
async fn soft_delete_note(pool : & PgPool, id : Uuid) -> Result <(), TicketError> | — |
`services::ticket_worklogs`
| Item | What it is |
|---|---|
pub struct TicketWorklog | — |
pub struct CreateWorklogInput | — |
pub struct WorklogSummary | — |
async fn list_worklogs_for_ticket(pool : & PgPool, ticket_id : Uuid,) -> Result <Vec <TicketWorklog>, TicketError> | — |
async fn create_worklog(pool : & PgPool, input : CreateWorklogInput,) -> Result <TicketWorklog, TicketError> | — |
async fn soft_delete_worklog(pool : & PgPool, id : Uuid) -> Result <(), TicketError> | — |
async fn worklog_summary(pool : & PgPool, ticket_id : Uuid,) -> Result <WorklogSummary, TicketError> | — |
async fn start_timer(pool : & PgPool, ticket_id : Uuid, staff_party_id : Uuid, description : Option <String>,) -> Result <TicketWorklog, TicketError> | Start a timer for (staff_party_id) on a ticket |
async fn stop_timer(pool : & PgPool, staff_party_id : Uuid) -> Result <TicketWorklog, TicketError> | Stop the active timer for (staff_party_id) — computes minutes from started_at and writes ended_at=NOW(). |
async fn cancel_timer(pool : & PgPool, staff_party_id : Uuid) -> Result <(), TicketError> | Cancel the active timer for (staff_party_id) — soft-deletes the row instead of stopping it (no minutes recorded). |
async fn active_timer(pool : & PgPool, staff_party_id : Uuid,) -> Result <Option <TicketWorklog>, TicketError> | Returns the active timer for a staff member, if any. |
`services::tickets`
| Item | What it is |
|---|---|
pub enum TicketError | — |
pub struct Ticket | — |
pub enum TicketTransition | State-machine transitions on a ticket |
TicketTransition :: fn target(& self) -> & 'static str | Returns the target status as the DB-canonical string. |
pub struct CreateTicketInput | — |
pub struct UpdateTicketInput | — |
pub struct ListTicketsFilter | — |
async fn create_ticket(pool : & PgPool, input : CreateTicketInput) -> Result <Ticket, TicketError> | Create a ticket |
async fn get_ticket(pool : & PgPool, id : Uuid) -> Result <Ticket, TicketError> | — |
async fn list_tickets(pool : & PgPool, filter : ListTicketsFilter,) -> Result <Vec <Ticket>, TicketError> | — |
async fn update_ticket(pool : & PgPool, id : Uuid, input : UpdateTicketInput,) -> Result <Ticket, TicketError> | — |
async fn soft_delete_ticket(pool : & PgPool, id : Uuid) -> Result <(), TicketError> | — |
async fn assign_ticket(pool : & PgPool, id : Uuid, assignee_party_id : Option <Uuid>,) -> Result <Ticket, TicketError> | — |
async fn transition_ticket(pool : & PgPool, id : Uuid, transition : TicketTransition,) -> Result <Ticket, TicketError> | Execute a state-machine transition |
async fn maybe_set_first_response(pool : & PgPool, id : Uuid) -> Result <(), TicketError> | Update first_response_at on the FIRST staff message (idempotent — only sets if NULL) |
async fn scan_sla_breaches(pool : & PgPool) -> Result <Vec <Uuid>, TicketError> | Nightly SLA scanner — flip sla_breached=true on any ticket that passed sla_due_at without resolution |
`workers::sla_scanner`
| Item | What it is |
|---|---|
pub const SLA_SCAN_JOB_TYPE: & str | Job type enqueued for each scheduled scan (also the cron schedule id). |
pub const DEFAULT_SLA_CRON: & str | Default cron cadence: every 5 minutes, on the wall-clock boundary. |
pub struct SlaScanPayload | Payload for an SLA-scan job (currently empty; reserved for future per-tenant scoping or filter knobs). |
async fn run_sla_scanner_worker(pool : PgPool, tenant_id : Uuid, cron_expr : & str, shutdown : CancellationToken,) | Run the SLA scanner until shutdown fires: a infrastructure-cron schedule emits sla_scan beats and a Worker runs each scan |
No pub use re-exports: every item above is declared in this crate.
Boundary
Reaches into infrastructure, platform.
Shares tier application with 120 other crates: application-agreements, application-ai, application-analytics, application-approvals, application-assessments, application-audit-log, application-auth, application-billing, … (120 total).
_What this crate deliberately does NOT own is a judgment. No committed registry records one for it, so none is stated here._
Where it sits
| Tier (ontology) | application |
| Architectural role (taxonomy) | unclassified (baselined) |
| Location | crates/application/tickets |
| Vocabulary in force (lexicon) | current |
Tier flow. Which tiers this crate's own edges cross.
flowchart LR n_application["application"] --> n_infrastructure["infrastructure"] n_application["application"] --> n_platform["platform"]
Dependencies
Runtime, in this workspace.
| Crate | Tier | Optional | Only on |
|---|---|---|---|
| `application-core` | application | no | always |
| `application-notifications` | application | no | always |
| `infrastructure-cron` | infrastructure | no | always |
| `infrastructure-jobs` | infrastructure | no | always |
| `platform-dto` | platform | no | always |
Runtime, from outside the workspace.
| Crate | Requirement | Features | Optional | Only on |
|---|---|---|---|---|
async-trait | ^0.1 | — | no | always |
axum | ^0.7 | multipart | no | always |
chrono | ^0.4 | serde | no | always |
chrono-tz | ^0.10 | — | no | always |
serde | ^1 | derive | no | always |
serde_json | ^1 | — | no | always |
sqlx | ^0.8 | runtime-tokio, postgres, chrono, uuid, json | no | always |
thiserror | ^2 | — | no | always |
tokio | ^1 | full | no | always |
tokio-util | ^0.7 | rt | no | always |
tracing | ^0.1 | — | no | always |
uuid | ^1 | v4, v7, serde, js | no | always |
Development. None.
Build. None.
Depended on by. Nothing in this workspace.
Signal flow — what reaches this crate, and what it reaches.
flowchart LR SELF["application-tickets"] SELF -->|runtime| n_application_core["application-core"] SELF -->|runtime| n_application_notifications["application-notifications"] SELF -->|runtime| n_infrastructure_cron["infrastructure-cron"] SELF -->|runtime| n_infrastructure_jobs["infrastructure-jobs"] SELF -->|runtime| n_platform_dto["platform-dto"] classDef self fill:#1f883d,stroke:#1f883d,color:#fff; class SELF self;
Feature flags
No Cargo features are defined: every capability is unconditional, so no consumer can receive a half-wired crate.
Targets
| Kind | Name | Source |
|---|---|---|
| lib | application_tickets | `src/lib.rs` |
Error model
| Error type | Named by |
|---|---|
TicketError | active_timer, assign_ticket, attach_asset, cancel_timer, create_message, create_note, … (28 total) |
Operational characteristics
| Property | Evidence |
|---|---|
| async public surface | yes |
| async runtime | yes |
| database access | yes |
| network I/O | yes |
| unsafe code | none detected |
| environment variables | yes |
No unsafe block, unsafe fn, unsafe impl or unsafe trait was found by the parser anywhere in this crate's source.
Configuration
| Variable | Read in |
|---|---|
CARGO_PKG_VERSION | src/lib.rs |
Related capabilities
No workspace crate depends on this one.
Verification
| Kind | Count |
|---|---|
| Unit tests | 10 |
| Integration tests | 0 |
| Examples | 0 |
| Doctests | 0 |
Evidence by module. How often each public module is named by something executable.
| Module | Tests | Examples | Consumers |
|---|---|---|---|
crate root | 1 | 0 | 0 |
api | 2 | 0 | 0 |
api::tickets_admin | 1 | 0 | 0 |
api::tickets_customer | 1 | 0 | 0 |
services::ticket_attachments | 5 | 0 | 0 |
services::ticket_messages | 7 | 0 | 0 |
services::ticket_notes | 6 | 0 | 0 |
services::ticket_worklogs | 11 | 0 | 0 |
services::tickets | 15 | 0 | 0 |
workers::sla_scanner | 4 | 0 | 0 |
What the tests establish, by name:
test_invalid_transition_open_to_closed_directly—src/services/tickets.rstest_invalid_transition_resolved_to_in_progress—src/services/tickets.rstest_transition_auto_close_target—src/services/tickets.rstest_transition_closed_to_reopened—src/services/tickets.rstest_transition_in_progress_to_resolved—src/services/tickets.rstest_transition_open_to_in_progress—src/services/tickets.rstest_transition_resolved_to_closed—src/services/tickets.rstest_transition_waiting_to_open_on_client_reply—src/services/tickets.rstest_transition_waiting_to_resolved_by_staff—src/services/tickets.rstest_waiting_escape_does_not_broaden_open_to_resolved—src/services/tickets.rs
Documentation coverage
| Measure | Documented | Total |
|---|---|---|
| Public items with rustdoc | 21 | 59 |
Public modules with a //! block | 11 | 11 |
pie showData
title Public items with rustdoc
"Documented" : 21
"No rustdoc detected" : 38
Metrics
| Metric | Value |
|---|---|
| Rust source files | 12 |
| Source lines | 2017 |
| Code lines | 1589 |
| Public API items | 59 |
| Public modules | 11 |
| Tests | 10 |
| Examples | 0 |
| Cargo features | 0 |
| Direct runtime dependencies | 17 |
| Workspace reverse dependencies | 0 |
pie showData
title Public API by kind
"constant" : 2
"enum" : 3
"function" : 33
"method" : 6
"struct" : 15
pie showData
title Rust source composition
"Code" : 1589
"Blank or comment" : 428
Generation
Rendered by tools-corpus corpus readme from repository evidence alone, renderer schema 2, lexicon current. No model, network service or database was consulted. Regenerate with tools-corpus corpus readme --write; verify with --check.