Packages
phoenix_kit
1.7.202
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/phoenix_kit/migrations/postgres/v35.ex
defmodule PhoenixKit.Migrations.Postgres.V35 do
@moduledoc """
PhoenixKit V35 Migration: Support Tickets System
Adds complete customer support ticket system with status workflow,
comments with internal notes, file attachments, and audit trail.
## Changes
### Tickets Table (phoenix_kit_tickets)
- Main ticket storage with status workflow (open/in_progress/resolved/closed)
- Customer user association (who created the ticket)
- Handler assignment (support staff)
- Denormalized comment counter
- Resolved/closed timestamps for tracking
### Ticket Comments (phoenix_kit_ticket_comments)
- Threaded comments with internal notes support
- is_internal flag for staff-only visibility
- Self-referencing for nested replies
### Ticket Attachments (phoenix_kit_ticket_attachments)
- Junction table for files on tickets or comments
- Position-based ordering with captions
### Ticket Status History (phoenix_kit_ticket_status_history)
- Complete audit trail for all status changes
- Who changed, from/to status, optional reason
## Settings
- Module config: enabled, per-page
- Feature toggles: comments, internal notes, attachments
- Workflow: allow reopen
## Features
- UUIDv7 primary keys for time-sortable IDs
- Comprehensive indexes for efficient queries
- Foreign key constraints for data integrity
- Check constraint for attachment parent reference
"""
use Ecto.Migration
@doc """
Run the V34 migration to add support tickets system.
"""
def up(%{prefix: prefix} = _opts) do
# Create tables in dependency order
create_tickets_table(prefix)
create_ticket_comments_table(prefix)
create_ticket_attachments_table(prefix)
create_ticket_status_history_table(prefix)
# Seed SupportAgent role if not exists
seed_support_agent_role(prefix)
# Seed default settings
seed_settings(prefix)
# Update version tracking
execute("COMMENT ON TABLE #{prefix_table_name("phoenix_kit", prefix)} IS '35'")
end
@doc """
Rollback the V35 migration.
"""
def down(%{prefix: prefix} = _opts) do
# Drop tables in reverse order (respecting foreign keys)
drop_if_exists(table(:phoenix_kit_ticket_status_history, prefix: prefix))
drop_if_exists(table(:phoenix_kit_ticket_attachments, prefix: prefix))
drop_if_exists(table(:phoenix_kit_ticket_comments, prefix: prefix))
drop_if_exists(table(:phoenix_kit_tickets, prefix: prefix))
# Remove settings
delete_setting(prefix, "tickets_enabled")
delete_setting(prefix, "tickets_per_page")
delete_setting(prefix, "tickets_comments_enabled")
delete_setting(prefix, "tickets_internal_notes_enabled")
delete_setting(prefix, "tickets_attachments_enabled")
delete_setting(prefix, "tickets_allow_reopen")
# Note: We don't remove SupportAgent role as it may have assignments
# Update version tracking (rollback to V34)
execute("COMMENT ON TABLE #{prefix_table_name("phoenix_kit", prefix)} IS '34'")
end
# Private helper functions
defp create_tickets_table(prefix) do
create_if_not_exists table(:phoenix_kit_tickets, primary_key: false, prefix: prefix) do
add(:id, :uuid, primary_key: true)
add(
:user_id,
references(:phoenix_kit_users, on_delete: :delete_all, prefix: prefix, type: :bigint),
null: false
)
add(
:assigned_to_id,
references(:phoenix_kit_users, on_delete: :nilify_all, prefix: prefix, type: :bigint)
)
add(:title, :string, null: false)
add(:description, :text, null: false)
add(:status, :string, null: false, default: "open")
add(:slug, :string, null: false)
add(:comment_count, :integer, default: 0, null: false)
add(:metadata, :map, default: %{})
add(:resolved_at, :utc_datetime_usec)
add(:closed_at, :utc_datetime_usec)
timestamps(type: :naive_datetime)
end
# Indexes for efficient queries
create_if_not_exists(index(:phoenix_kit_tickets, [:user_id], prefix: prefix))
create_if_not_exists(index(:phoenix_kit_tickets, [:assigned_to_id], prefix: prefix))
create_if_not_exists(index(:phoenix_kit_tickets, [:status], prefix: prefix))
create_if_not_exists(index(:phoenix_kit_tickets, [:slug], prefix: prefix))
create_if_not_exists(index(:phoenix_kit_tickets, [:inserted_at], prefix: prefix))
# Composite indexes for common queries
create_if_not_exists(index(:phoenix_kit_tickets, [:status, :assigned_to_id], prefix: prefix))
create_if_not_exists(index(:phoenix_kit_tickets, [:user_id, :status], prefix: prefix))
create_if_not_exists(index(:phoenix_kit_tickets, [:status, :inserted_at], prefix: prefix))
# Unique slug constraint
create_if_not_exists(unique_index(:phoenix_kit_tickets, [:slug], prefix: prefix))
execute("""
COMMENT ON TABLE #{prefix_table_name("phoenix_kit_tickets", prefix)} IS
'Support tickets with status workflow (open/in_progress/resolved/closed)'
""")
end
defp create_ticket_comments_table(prefix) do
create_if_not_exists table(:phoenix_kit_ticket_comments, primary_key: false, prefix: prefix) do
add(:id, :uuid, primary_key: true)
add(
:ticket_id,
references(:phoenix_kit_tickets, on_delete: :delete_all, prefix: prefix, type: :uuid),
null: false
)
add(
:user_id,
references(:phoenix_kit_users, on_delete: :delete_all, prefix: prefix, type: :bigint),
null: false
)
add(
:parent_id,
references(:phoenix_kit_ticket_comments,
on_delete: :delete_all,
prefix: prefix,
type: :uuid
)
)
add(:content, :text, null: false)
add(:is_internal, :boolean, default: false, null: false)
add(:depth, :integer, default: 0, null: false)
timestamps(type: :naive_datetime)
end
create_if_not_exists(index(:phoenix_kit_ticket_comments, [:ticket_id], prefix: prefix))
create_if_not_exists(index(:phoenix_kit_ticket_comments, [:user_id], prefix: prefix))
create_if_not_exists(index(:phoenix_kit_ticket_comments, [:parent_id], prefix: prefix))
create_if_not_exists(index(:phoenix_kit_ticket_comments, [:is_internal], prefix: prefix))
# Composite indexes for queries
create_if_not_exists(
index(:phoenix_kit_ticket_comments, [:ticket_id, :is_internal], prefix: prefix)
)
create_if_not_exists(
index(:phoenix_kit_ticket_comments, [:ticket_id, :parent_id, :depth], prefix: prefix)
)
create_if_not_exists(
index(:phoenix_kit_ticket_comments, [:ticket_id, :inserted_at], prefix: prefix)
)
execute("""
COMMENT ON TABLE #{prefix_table_name("phoenix_kit_ticket_comments", prefix)} IS
'Ticket comments with internal notes support (is_internal flag for staff-only)'
""")
end
defp create_ticket_attachments_table(prefix) do
create_if_not_exists table(:phoenix_kit_ticket_attachments,
primary_key: false,
prefix: prefix
) do
add(:id, :uuid, primary_key: true)
add(
:ticket_id,
references(:phoenix_kit_tickets, on_delete: :delete_all, prefix: prefix, type: :uuid)
)
add(
:comment_id,
references(:phoenix_kit_ticket_comments,
on_delete: :delete_all,
prefix: prefix,
type: :uuid
)
)
add(
:file_id,
references(:phoenix_kit_files, on_delete: :delete_all, prefix: prefix, type: :uuid),
null: false
)
add(:position, :integer, null: false)
add(:caption, :text)
timestamps(type: :naive_datetime)
end
create_if_not_exists(index(:phoenix_kit_ticket_attachments, [:ticket_id], prefix: prefix))
create_if_not_exists(index(:phoenix_kit_ticket_attachments, [:comment_id], prefix: prefix))
create_if_not_exists(index(:phoenix_kit_ticket_attachments, [:file_id], prefix: prefix))
create_if_not_exists(index(:phoenix_kit_ticket_attachments, [:position], prefix: prefix))
# Check constraint: either ticket_id or comment_id must be set (but not both)
execute("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'phoenix_kit_ticket_attachments_parent_check'
AND conrelid = '#{prefix_table_name("phoenix_kit_ticket_attachments", prefix)}'::regclass
) THEN
ALTER TABLE #{prefix_table_name("phoenix_kit_ticket_attachments", prefix)}
ADD CONSTRAINT phoenix_kit_ticket_attachments_parent_check
CHECK (
(ticket_id IS NOT NULL AND comment_id IS NULL) OR
(ticket_id IS NULL AND comment_id IS NOT NULL)
);
END IF;
END $$;
""")
execute("""
COMMENT ON TABLE #{prefix_table_name("phoenix_kit_ticket_attachments", prefix)} IS
'File attachments for tickets or comments (with position ordering)'
""")
end
defp create_ticket_status_history_table(prefix) do
create_if_not_exists table(:phoenix_kit_ticket_status_history,
primary_key: false,
prefix: prefix
) do
add(:id, :uuid, primary_key: true)
add(
:ticket_id,
references(:phoenix_kit_tickets, on_delete: :delete_all, prefix: prefix, type: :uuid),
null: false
)
add(
:changed_by_id,
references(:phoenix_kit_users, on_delete: :nilify_all, prefix: prefix, type: :bigint),
null: false
)
add(:from_status, :string)
add(:to_status, :string, null: false)
add(:reason, :text)
add(:inserted_at, :naive_datetime, null: false)
end
create_if_not_exists(index(:phoenix_kit_ticket_status_history, [:ticket_id], prefix: prefix))
create_if_not_exists(
index(:phoenix_kit_ticket_status_history, [:changed_by_id], prefix: prefix)
)
create_if_not_exists(
index(:phoenix_kit_ticket_status_history, [:inserted_at], prefix: prefix)
)
# Composite index for ticket history queries
create_if_not_exists(
index(:phoenix_kit_ticket_status_history, [:ticket_id, :inserted_at], prefix: prefix)
)
execute("""
COMMENT ON TABLE #{prefix_table_name("phoenix_kit_ticket_status_history", prefix)} IS
'Audit trail for ticket status transitions (who, when, from/to, reason)'
""")
end
defp seed_support_agent_role(prefix) do
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
# Create SupportAgent as a system role (protected from deletion)
execute("""
INSERT INTO #{prefix_table_name("phoenix_kit_user_roles", prefix)}
(name, description, is_system_role, inserted_at, updated_at)
VALUES ('SupportAgent', 'Support team member with ticket management access', true, '#{now}', '#{now}')
ON CONFLICT (name) DO UPDATE SET is_system_role = true
""")
end
defp seed_settings(prefix) do
settings = [
# Module Configuration
%{key: "tickets_enabled", value: "false"},
%{key: "tickets_per_page", value: "20"},
# Feature Toggles
%{key: "tickets_comments_enabled", value: "true"},
%{key: "tickets_internal_notes_enabled", value: "true"},
%{key: "tickets_attachments_enabled", value: "true"},
# Workflow
%{key: "tickets_allow_reopen", value: "true"}
]
Enum.each(settings, fn setting ->
insert_setting(prefix, setting.key, setting.value)
end)
end
defp insert_setting(prefix, key, value) do
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
execute("""
INSERT INTO #{prefix_table_name("phoenix_kit_settings", prefix)}
(key, value, date_added, date_updated)
VALUES ('#{key}', '#{value}', '#{now}', '#{now}')
ON CONFLICT (key) DO NOTHING
""")
end
defp delete_setting(prefix, key) do
execute("""
DELETE FROM #{prefix_table_name("phoenix_kit_settings", prefix)}
WHERE key = '#{key}'
""")
end
defp prefix_table_name(table_name, nil), do: table_name
defp prefix_table_name(table_name, prefix), do: "#{prefix}.#{table_name}"
end