Packages
phoenix_kit
1.7.57
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/tickets/ticket.ex
defmodule PhoenixKit.Modules.Tickets.Ticket do
@moduledoc """
Schema for support tickets.
Represents a customer support request with status workflow, assignment,
and comment threads including internal notes.
## Status Flow
- `open` - New ticket, awaiting assignment or response
- `in_progress` - Being worked on by support staff
- `resolved` - Issue resolved, awaiting confirmation
- `closed` - Ticket closed (resolved or abandoned)
## Transitions
- open -> in_progress (when assigned or work begins)
- open -> resolved (direct resolution)
- open -> closed (close without resolution)
- in_progress -> resolved
- in_progress -> open (return/unassign)
- in_progress -> closed
- resolved -> closed (auto or manual)
- resolved -> open (reopen by customer)
- closed -> open (reopen, if allowed)
## Fields
- `user_uuid` - Customer who created the ticket
- `assigned_to_uuid` - Support staff handling the ticket
- `title` - Brief description of the issue
- `description` - Full details of the issue
- `status` - open/in_progress/resolved/closed
- `slug` - URL-friendly identifier
- `comment_count` - Denormalized counter
- `metadata` - Flexible JSONB for future extensions
- `resolved_at` - When ticket was resolved
- `closed_at` - When ticket was closed
## Examples
# New ticket
%Ticket{
uuid: "018e3c4a-9f6b-7890-abcd-ef1234567890",
user_uuid: "018e3c4a-1111-7890-abcd-ef1234567890",
assigned_to_uuid: nil,
title: "Cannot login to my account",
description: "I get an error when trying to login...",
status: "open",
slug: "cannot-login-to-my-account",
comment_count: 0
}
# Ticket being worked on
%Ticket{
user_uuid: "018e3c4a-1111-7890-abcd-ef1234567890",
assigned_to_uuid: "018e3c4a-2222-7890-abcd-ef1234567890",
title: "Payment failed",
description: "...",
status: "in_progress",
comment_count: 3
}
# Resolved ticket
%Ticket{
user_uuid: "018e3c4a-1111-7890-abcd-ef1234567890",
assigned_to_uuid: "018e3c4a-2222-7890-abcd-ef1234567890",
status: "resolved",
resolved_at: ~U[2025-01-15 14:30:00Z]
}
"""
use Ecto.Schema
import Ecto.Changeset
@primary_key {:uuid, UUIDv7, autogenerate: true}
@foreign_key_type UUIDv7
@statuses ["open", "in_progress", "resolved", "closed"]
@type t :: %__MODULE__{
uuid: UUIDv7.t() | nil,
user_uuid: UUIDv7.t(),
assigned_to_uuid: UUIDv7.t() | nil,
title: String.t(),
description: String.t(),
status: String.t(),
slug: String.t(),
comment_count: integer(),
metadata: map(),
resolved_at: DateTime.t() | nil,
closed_at: DateTime.t() | nil,
user: PhoenixKit.Users.Auth.User.t() | Ecto.Association.NotLoaded.t(),
assigned_to: PhoenixKit.Users.Auth.User.t() | Ecto.Association.NotLoaded.t() | nil,
comments:
[PhoenixKit.Modules.Tickets.TicketComment.t()] | Ecto.Association.NotLoaded.t(),
attachments:
[PhoenixKit.Modules.Tickets.TicketAttachment.t()] | Ecto.Association.NotLoaded.t(),
status_history:
[PhoenixKit.Modules.Tickets.TicketStatusHistory.t()] | Ecto.Association.NotLoaded.t(),
inserted_at: DateTime.t() | nil,
updated_at: DateTime.t() | nil
}
schema "phoenix_kit_tickets" do
field :title, :string
field :description, :string
field :status, :string, default: "open"
field :slug, :string
field :comment_count, :integer, default: 0
field :metadata, :map, default: %{}
field :resolved_at, :utc_datetime
field :closed_at, :utc_datetime
belongs_to :user, PhoenixKit.Users.Auth.User,
foreign_key: :user_uuid,
references: :uuid,
type: UUIDv7
belongs_to :assigned_to, PhoenixKit.Users.Auth.User,
foreign_key: :assigned_to_uuid,
references: :uuid,
type: UUIDv7
has_many :comments, PhoenixKit.Modules.Tickets.TicketComment, foreign_key: :ticket_uuid
has_many :attachments, PhoenixKit.Modules.Tickets.TicketAttachment, foreign_key: :ticket_uuid
has_many :status_history, PhoenixKit.Modules.Tickets.TicketStatusHistory,
foreign_key: :ticket_uuid
timestamps(type: :utc_datetime)
end
@doc """
Changeset for creating or updating a ticket.
## Required Fields
- `user_uuid` - Customer who created the ticket
- `title` - Brief description
- `description` - Full details
- `status` - Must be: "open", "in_progress", "resolved", or "closed"
## Validation Rules
- Title max 255 characters
- Status must be valid
- Slug auto-generated from title if not provided
"""
def changeset(ticket, attrs) do
ticket
|> cast(attrs, [
:user_uuid,
:assigned_to_uuid,
:title,
:description,
:status,
:slug,
:metadata,
:resolved_at,
:closed_at
])
|> validate_required([:user_uuid, :title, :description, :status])
|> validate_inclusion(:status, @statuses)
|> validate_length(:title, max: 255)
|> maybe_generate_slug()
|> foreign_key_constraint(:user_uuid)
|> foreign_key_constraint(:assigned_to_uuid)
|> unique_constraint(:slug)
end
@doc """
Returns list of valid statuses.
"""
def statuses, do: @statuses
@doc """
Check if ticket is open.
"""
def open?(%__MODULE__{status: "open"}), do: true
def open?(_), do: false
@doc """
Check if ticket is in progress.
"""
def in_progress?(%__MODULE__{status: "in_progress"}), do: true
def in_progress?(_), do: false
@doc """
Check if ticket is resolved.
"""
def resolved?(%__MODULE__{status: "resolved"}), do: true
def resolved?(_), do: false
@doc """
Check if ticket is closed.
"""
def closed?(%__MODULE__{status: "closed"}), do: true
def closed?(_), do: false
@doc """
Check if ticket is assigned to someone.
"""
def assigned?(%__MODULE__{assigned_to_uuid: nil}), do: false
def assigned?(%__MODULE__{}), do: true
@doc """
Check if ticket can receive comments (not closed).
"""
def can_comment?(%__MODULE__{status: "closed"}), do: false
def can_comment?(%__MODULE__{}), do: true
@doc """
Valid transitions from current status.
"""
def valid_transitions("open"), do: ["in_progress", "resolved", "closed"]
def valid_transitions("in_progress"), do: ["open", "resolved", "closed"]
def valid_transitions("resolved"), do: ["open", "closed"]
def valid_transitions("closed"), do: ["open"]
def valid_transitions(_), do: []
@doc """
Check if transition from current to new status is valid.
"""
def valid_transition?(current_status, new_status) do
new_status in valid_transitions(current_status)
end
# Private Functions
defp maybe_generate_slug(changeset) do
case get_change(changeset, :slug) do
nil ->
title = get_field(changeset, :title)
if title do
slug = slugify(title)
put_change(changeset, :slug, slug)
else
changeset
end
_slug ->
changeset
end
end
defp slugify(title) do
timestamp = System.system_time(:millisecond) |> Integer.to_string() |> String.slice(-6, 6)
base_slug =
title
|> String.downcase()
|> String.replace(~r/[^\w\s-]/, "")
|> String.replace(~r/\s+/, "-")
|> String.trim("-")
|> String.slice(0, 50)
"#{base_slug}-#{timestamp}"
end
end