escalated 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +302 -0
  4. data/app/controllers/escalated/admin/bulk_actions_controller.rb +42 -0
  5. data/app/controllers/escalated/admin/canned_responses_controller.rb +73 -0
  6. data/app/controllers/escalated/admin/departments_controller.rb +135 -0
  7. data/app/controllers/escalated/admin/escalation_rules_controller.rb +121 -0
  8. data/app/controllers/escalated/admin/macros_controller.rb +73 -0
  9. data/app/controllers/escalated/admin/reports_controller.rb +152 -0
  10. data/app/controllers/escalated/admin/settings_controller.rb +111 -0
  11. data/app/controllers/escalated/admin/sla_policies_controller.rb +109 -0
  12. data/app/controllers/escalated/admin/tags_controller.rb +67 -0
  13. data/app/controllers/escalated/admin/tickets_controller.rb +299 -0
  14. data/app/controllers/escalated/agent/bulk_actions_controller.rb +42 -0
  15. data/app/controllers/escalated/agent/dashboard_controller.rb +94 -0
  16. data/app/controllers/escalated/agent/tickets_controller.rb +330 -0
  17. data/app/controllers/escalated/application_controller.rb +110 -0
  18. data/app/controllers/escalated/customer/satisfaction_ratings_controller.rb +44 -0
  19. data/app/controllers/escalated/customer/tickets_controller.rb +169 -0
  20. data/app/controllers/escalated/guest/tickets_controller.rb +231 -0
  21. data/app/controllers/escalated/inbound_controller.rb +79 -0
  22. data/app/jobs/escalated/check_sla_job.rb +36 -0
  23. data/app/jobs/escalated/close_resolved_job.rb +51 -0
  24. data/app/jobs/escalated/evaluate_escalations_job.rb +24 -0
  25. data/app/jobs/escalated/poll_imap_job.rb +74 -0
  26. data/app/jobs/escalated/purge_activities_job.rb +24 -0
  27. data/app/mailers/escalated/application_mailer.rb +6 -0
  28. data/app/mailers/escalated/ticket_mailer.rb +93 -0
  29. data/app/models/escalated/application_record.rb +5 -0
  30. data/app/models/escalated/attachment.rb +46 -0
  31. data/app/models/escalated/canned_response.rb +45 -0
  32. data/app/models/escalated/department.rb +43 -0
  33. data/app/models/escalated/escalated_setting.rb +43 -0
  34. data/app/models/escalated/escalation_rule.rb +96 -0
  35. data/app/models/escalated/inbound_email.rb +60 -0
  36. data/app/models/escalated/macro.rb +18 -0
  37. data/app/models/escalated/reply.rb +42 -0
  38. data/app/models/escalated/satisfaction_rating.rb +21 -0
  39. data/app/models/escalated/sla_policy.rb +54 -0
  40. data/app/models/escalated/tag.rb +28 -0
  41. data/app/models/escalated/ticket.rb +166 -0
  42. data/app/models/escalated/ticket_activity.rb +60 -0
  43. data/app/policies/escalated/canned_response_policy.rb +40 -0
  44. data/app/policies/escalated/department_policy.rb +36 -0
  45. data/app/policies/escalated/escalation_rule_policy.rb +36 -0
  46. data/app/policies/escalated/sla_policy_policy.rb +36 -0
  47. data/app/policies/escalated/tag_policy.rb +36 -0
  48. data/app/policies/escalated/ticket_policy.rb +111 -0
  49. data/config/routes.rb +81 -0
  50. data/db/migrate/001_create_escalated_departments.rb +18 -0
  51. data/db/migrate/002_create_escalated_sla_policies.rb +23 -0
  52. data/db/migrate/003_create_escalated_tags.rb +15 -0
  53. data/db/migrate/004_create_escalated_tickets.rb +48 -0
  54. data/db/migrate/005_create_escalated_replies.rb +21 -0
  55. data/db/migrate/006_create_escalated_attachments.rb +17 -0
  56. data/db/migrate/007_create_escalated_ticket_tags.rb +13 -0
  57. data/db/migrate/008_create_escalated_support_tables.rb +49 -0
  58. data/db/migrate/009_create_escalated_ticket_activities.rb +20 -0
  59. data/db/migrate/010_create_escalated_settings.rb +29 -0
  60. data/db/migrate/011_add_guest_fields_to_escalated_tickets.rb +28 -0
  61. data/db/migrate/012_create_escalated_inbound_emails.rb +30 -0
  62. data/db/migrate/013_create_escalated_macros.rb +18 -0
  63. data/db/migrate/014_create_escalated_ticket_followers.rb +18 -0
  64. data/db/migrate/015_create_escalated_satisfaction_ratings.rb +21 -0
  65. data/db/migrate/016_add_is_pinned_to_escalated_replies.rb +6 -0
  66. data/lib/escalated/configuration.rb +111 -0
  67. data/lib/escalated/drivers/cloud_driver.rb +134 -0
  68. data/lib/escalated/drivers/hosted_api_client.rb +166 -0
  69. data/lib/escalated/drivers/local_driver.rb +341 -0
  70. data/lib/escalated/drivers/synced_driver.rb +124 -0
  71. data/lib/escalated/engine.rb +45 -0
  72. data/lib/escalated/mail/adapters/base_adapter.rb +60 -0
  73. data/lib/escalated/mail/adapters/imap_adapter.rb +209 -0
  74. data/lib/escalated/mail/adapters/mailgun_adapter.rb +93 -0
  75. data/lib/escalated/mail/adapters/postmark_adapter.rb +94 -0
  76. data/lib/escalated/mail/adapters/ses_adapter.rb +179 -0
  77. data/lib/escalated/mail/inbound_message.rb +78 -0
  78. data/lib/escalated/manager.rb +33 -0
  79. data/lib/escalated/services/assignment_service.rb +85 -0
  80. data/lib/escalated/services/attachment_service.rb +110 -0
  81. data/lib/escalated/services/escalation_service.rb +159 -0
  82. data/lib/escalated/services/inbound_email_service.rb +255 -0
  83. data/lib/escalated/services/macro_service.rb +49 -0
  84. data/lib/escalated/services/notification_service.rb +157 -0
  85. data/lib/escalated/services/sla_service.rb +203 -0
  86. data/lib/escalated/services/ticket_service.rb +113 -0
  87. data/lib/escalated.rb +25 -0
  88. data/lib/generators/escalated/install_generator.rb +75 -0
  89. data/lib/generators/escalated/templates/initializer.rb +89 -0
  90. metadata +227 -0
@@ -0,0 +1,157 @@
1
+ require "net/http"
2
+ require "json"
3
+ require "uri"
4
+
5
+ module Escalated
6
+ module Services
7
+ class NotificationService
8
+ class << self
9
+ def dispatch(event, payload = {})
10
+ send_webhook(event, payload) if webhook_configured?
11
+ notify_followers(event, payload) if should_notify_followers?(event)
12
+ instrument_event(event, payload)
13
+ end
14
+
15
+ def send_webhook(event, payload)
16
+ return unless webhook_configured?
17
+
18
+ webhook_payload = build_webhook_payload(event, payload)
19
+
20
+ Thread.new do
21
+ begin
22
+ uri = URI.parse(Escalated.configuration.webhook_url)
23
+ http = Net::HTTP.new(uri.host, uri.port)
24
+ http.use_ssl = uri.scheme == "https"
25
+ http.open_timeout = 10
26
+ http.read_timeout = 10
27
+
28
+ request = Net::HTTP::Post.new(uri.path)
29
+ request["Content-Type"] = "application/json"
30
+ request["User-Agent"] = "Escalated-Webhook/0.1.0"
31
+ request["X-Escalated-Event"] = event.to_s
32
+ request["X-Escalated-Signature"] = compute_signature(webhook_payload)
33
+ request.body = webhook_payload.to_json
34
+
35
+ response = http.request(request)
36
+
37
+ unless response.is_a?(Net::HTTPSuccess)
38
+ Rails.logger.warn(
39
+ "[Escalated::NotificationService] Webhook returned #{response.code} for event #{event}"
40
+ )
41
+ end
42
+ rescue StandardError => e
43
+ Rails.logger.error(
44
+ "[Escalated::NotificationService] Webhook failed for event #{event}: #{e.message}"
45
+ )
46
+ end
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def webhook_configured?
53
+ Escalated.configuration.webhook_url.present?
54
+ end
55
+
56
+ def build_webhook_payload(event, payload)
57
+ data = {
58
+ event: event.to_s,
59
+ timestamp: Time.current.iso8601,
60
+ data: {}
61
+ }
62
+
63
+ if payload[:ticket]
64
+ ticket = payload[:ticket]
65
+ data[:data][:ticket] = {
66
+ id: ticket.respond_to?(:id) ? ticket.id : nil,
67
+ reference: ticket.respond_to?(:reference) ? ticket.reference : nil,
68
+ subject: ticket.respond_to?(:subject) ? ticket.subject : nil,
69
+ status: ticket.respond_to?(:status) ? ticket.status : nil,
70
+ priority: ticket.respond_to?(:priority) ? ticket.priority : nil
71
+ }
72
+ end
73
+
74
+ if payload[:reply]
75
+ reply = payload[:reply]
76
+ data[:data][:reply] = {
77
+ id: reply.respond_to?(:id) ? reply.id : nil,
78
+ is_internal: reply.respond_to?(:is_internal) ? reply.is_internal : nil
79
+ }
80
+ end
81
+
82
+ if payload[:agent]
83
+ agent = payload[:agent]
84
+ data[:data][:agent] = {
85
+ id: agent.respond_to?(:id) ? agent.id : nil,
86
+ email: agent.respond_to?(:email) ? agent.email : nil
87
+ }
88
+ end
89
+
90
+ # Include any extra scalar values
91
+ payload.each do |key, value|
92
+ next if [:ticket, :reply, :agent, :rule, :recipients].include?(key)
93
+ data[:data][key] = value if value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(TrueClass) || value.is_a?(FalseClass)
94
+ end
95
+
96
+ data
97
+ end
98
+
99
+ def compute_signature(payload)
100
+ key = Escalated.configuration.hosted_api_key || "escalated-webhook-secret"
101
+ OpenSSL::HMAC.hexdigest("SHA256", key, payload.to_json)
102
+ end
103
+
104
+ def should_notify_followers?(event)
105
+ [:reply_added, :status_changed, :ticket_assigned, :priority_changed].include?(event)
106
+ end
107
+
108
+ def notify_followers(event, payload)
109
+ ticket = payload[:ticket]
110
+ return unless ticket.respond_to?(:followers)
111
+
112
+ reply = payload[:reply]
113
+ actor = payload[:agent]
114
+
115
+ ticket.followers.each do |follower|
116
+ # Skip the actor (person who performed the action)
117
+ next if actor && follower.id == actor.id
118
+ # Skip the reply author
119
+ next if reply && reply.respond_to?(:author) && reply.author == follower
120
+ # Skip the assignee (they already get notified separately)
121
+ next if ticket.respond_to?(:assigned_to) && ticket.assigned_to == follower.id
122
+ # Skip the requester (they already get notified separately)
123
+ next if ticket.respond_to?(:requester) && ticket.requester == follower
124
+
125
+ if Escalated.configuration.notification_channels.include?(:email)
126
+ begin
127
+ case event
128
+ when :reply_added
129
+ Escalated::TicketMailer.reply_received(ticket, reply).deliver_later
130
+ when :status_changed
131
+ Escalated::TicketMailer.status_changed(ticket).deliver_later
132
+ when :ticket_assigned
133
+ Escalated::TicketMailer.ticket_assigned(ticket).deliver_later
134
+ when :priority_changed
135
+ # Priority change notification to followers
136
+ Escalated::TicketMailer.status_changed(ticket).deliver_later
137
+ end
138
+ rescue StandardError => e
139
+ Rails.logger.warn(
140
+ "[Escalated::NotificationService] Follower notification failed for #{follower.id}: #{e.message}"
141
+ )
142
+ end
143
+ end
144
+ end
145
+ rescue StandardError => e
146
+ Rails.logger.warn(
147
+ "[Escalated::NotificationService] Follower notification dispatch failed: #{e.message}"
148
+ )
149
+ end
150
+
151
+ def instrument_event(event, payload)
152
+ ActiveSupport::Notifications.instrument("escalated.notification.#{event}", payload)
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,203 @@
1
+ module Escalated
2
+ module Services
3
+ class SlaService
4
+ class << self
5
+ def attach_policy(ticket, policy = nil)
6
+ return unless Escalated.configuration.sla_enabled?
7
+
8
+ policy ||= find_policy_for(ticket)
9
+ return unless policy
10
+
11
+ ticket.update!(
12
+ sla_policy_id: policy.id,
13
+ sla_first_response_due_at: calculate_due_date(policy.first_response_hours_for(ticket.priority)),
14
+ sla_resolution_due_at: calculate_due_date(policy.resolution_hours_for(ticket.priority))
15
+ )
16
+ end
17
+
18
+ def check_breaches
19
+ return unless Escalated.configuration.sla_enabled?
20
+
21
+ breached_tickets = []
22
+
23
+ # Check first response breaches
24
+ Escalated::Ticket
25
+ .by_open
26
+ .where(sla_breached: false)
27
+ .where.not(sla_first_response_due_at: nil)
28
+ .where(first_response_at: nil)
29
+ .where("sla_first_response_due_at < ?", Time.current)
30
+ .find_each do |ticket|
31
+ mark_breached(ticket, :first_response)
32
+ breached_tickets << ticket
33
+ end
34
+
35
+ # Check resolution breaches
36
+ Escalated::Ticket
37
+ .by_open
38
+ .where(sla_breached: false)
39
+ .where.not(sla_resolution_due_at: nil)
40
+ .where(resolved_at: nil)
41
+ .where("sla_resolution_due_at < ?", Time.current)
42
+ .find_each do |ticket|
43
+ mark_breached(ticket, :resolution)
44
+ breached_tickets << ticket
45
+ end
46
+
47
+ breached_tickets
48
+ end
49
+
50
+ def check_warnings
51
+ return unless Escalated.configuration.sla_enabled?
52
+
53
+ warning_tickets = []
54
+
55
+ # First response warnings (1 hour before breach)
56
+ Escalated::Ticket
57
+ .by_open
58
+ .where(sla_breached: false)
59
+ .where.not(sla_first_response_due_at: nil)
60
+ .where(first_response_at: nil)
61
+ .where("sla_first_response_due_at BETWEEN ? AND ?", Time.current, 1.hour.from_now)
62
+ .find_each do |ticket|
63
+ warning_tickets << { ticket: ticket, type: :first_response_warning }
64
+ end
65
+
66
+ # Resolution warnings (2 hours before breach)
67
+ Escalated::Ticket
68
+ .by_open
69
+ .where(sla_breached: false)
70
+ .where.not(sla_resolution_due_at: nil)
71
+ .where(resolved_at: nil)
72
+ .where("sla_resolution_due_at BETWEEN ? AND ?", Time.current, 2.hours.from_now)
73
+ .find_each do |ticket|
74
+ warning_tickets << { ticket: ticket, type: :resolution_warning }
75
+ end
76
+
77
+ warning_tickets
78
+ end
79
+
80
+ def calculate_due_date(hours)
81
+ return nil unless hours
82
+
83
+ if Escalated.configuration.business_hours_only?
84
+ calculate_business_hours_due_date(hours)
85
+ else
86
+ Time.current + hours.hours
87
+ end
88
+ end
89
+
90
+ def recalculate_for_ticket(ticket)
91
+ return unless ticket.sla_policy
92
+
93
+ policy = ticket.sla_policy
94
+
95
+ updates = {}
96
+ unless ticket.first_response_at
97
+ updates[:sla_first_response_due_at] = calculate_due_date(
98
+ policy.first_response_hours_for(ticket.priority)
99
+ )
100
+ end
101
+
102
+ unless ticket.resolved_at
103
+ updates[:sla_resolution_due_at] = calculate_due_date(
104
+ policy.resolution_hours_for(ticket.priority)
105
+ )
106
+ end
107
+
108
+ ticket.update!(updates) if updates.any?
109
+ end
110
+
111
+ def stats
112
+ return {} unless Escalated.configuration.sla_enabled?
113
+
114
+ total = Escalated::Ticket.where.not(sla_policy_id: nil).count
115
+ breached = Escalated::Ticket.where(sla_breached: true).count
116
+
117
+ responded = Escalated::Ticket.where.not(first_response_at: nil, sla_first_response_due_at: nil)
118
+ on_time_responses = responded.where("first_response_at <= sla_first_response_due_at").count
119
+
120
+ resolved = Escalated::Ticket.where.not(resolved_at: nil, sla_resolution_due_at: nil)
121
+ on_time_resolutions = resolved.where("resolved_at <= sla_resolution_due_at").count
122
+
123
+ {
124
+ total_with_sla: total,
125
+ total_breached: breached,
126
+ breach_rate: total > 0 ? (breached.to_f / total * 100).round(1) : 0,
127
+ first_response_on_time: on_time_responses,
128
+ first_response_on_time_rate: responded.count > 0 ? (on_time_responses.to_f / responded.count * 100).round(1) : 0,
129
+ resolution_on_time: on_time_resolutions,
130
+ resolution_on_time_rate: resolved.count > 0 ? (on_time_resolutions.to_f / resolved.count * 100).round(1) : 0
131
+ }
132
+ end
133
+
134
+ private
135
+
136
+ def find_policy_for(ticket)
137
+ if ticket.department&.default_sla_policy_id.present?
138
+ Escalated::SlaPolicy.find_by(id: ticket.department.default_sla_policy_id)
139
+ else
140
+ Escalated::SlaPolicy.default_policy.first
141
+ end
142
+ end
143
+
144
+ def mark_breached(ticket, breach_type)
145
+ ActiveRecord::Base.transaction do
146
+ ticket.update!(sla_breached: true)
147
+
148
+ ticket.activities.create!(
149
+ action: "sla_breached",
150
+ causer: nil,
151
+ details: { breach_type: breach_type.to_s }
152
+ )
153
+ end
154
+
155
+ if Escalated.configuration.notification_channels.include?(:email)
156
+ Escalated::TicketMailer.sla_breach(ticket).deliver_later
157
+ end
158
+
159
+ NotificationService.dispatch(:sla_breached, ticket: ticket, breach_type: breach_type)
160
+
161
+ ActiveSupport::Notifications.instrument("escalated.sla.breached", {
162
+ ticket: ticket,
163
+ breach_type: breach_type
164
+ })
165
+ end
166
+
167
+ def calculate_business_hours_due_date(hours)
168
+ bh = Escalated.configuration.business_hours
169
+ start_hour = bh[:start] || 9
170
+ end_hour = bh[:end] || 17
171
+ working_days = bh[:working_days] || [1, 2, 3, 4, 5]
172
+ tz = bh[:timezone] || "UTC"
173
+
174
+ current_time = Time.current.in_time_zone(tz)
175
+ remaining_hours = hours.to_f
176
+
177
+ while remaining_hours > 0
178
+ if working_days.include?(current_time.wday)
179
+ day_start = current_time.change(hour: start_hour, min: 0, sec: 0)
180
+ day_end = current_time.change(hour: end_hour, min: 0, sec: 0)
181
+
182
+ current_time = day_start if current_time < day_start
183
+
184
+ if current_time < day_end
185
+ available_hours = (day_end - current_time) / 3600.0
186
+
187
+ if remaining_hours <= available_hours
188
+ return current_time + remaining_hours.hours
189
+ else
190
+ remaining_hours -= available_hours
191
+ end
192
+ end
193
+ end
194
+
195
+ current_time = (current_time + 1.day).change(hour: start_hour, min: 0, sec: 0)
196
+ end
197
+
198
+ current_time
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,113 @@
1
+ module Escalated
2
+ module Services
3
+ class TicketService
4
+ class << self
5
+ def create(params)
6
+ ticket = driver.create_ticket(params)
7
+
8
+ if Escalated.configuration.notification_channels.include?(:email)
9
+ Escalated::TicketMailer.new_ticket(ticket).deliver_later
10
+ end
11
+
12
+ Services::NotificationService.dispatch(:ticket_created, ticket: ticket)
13
+
14
+ ticket
15
+ end
16
+
17
+ def update(ticket, params, actor:)
18
+ driver.update_ticket(ticket, params, actor: actor)
19
+ end
20
+
21
+ def transition_status(ticket, new_status, actor:, note: nil)
22
+ result = driver.transition_status(ticket, new_status, actor: actor, note: note)
23
+
24
+ if Escalated.configuration.notification_channels.include?(:email)
25
+ Escalated::TicketMailer.status_changed(result).deliver_later
26
+ end
27
+
28
+ if new_status.to_s == "resolved"
29
+ Escalated::TicketMailer.ticket_resolved(result).deliver_later if Escalated.configuration.notification_channels.include?(:email)
30
+ end
31
+
32
+ Services::NotificationService.dispatch(:status_changed, ticket: result, status: new_status)
33
+
34
+ result
35
+ end
36
+
37
+ def assign(ticket, agent, actor:)
38
+ result = driver.assign_ticket(ticket, agent, actor: actor)
39
+
40
+ if Escalated.configuration.notification_channels.include?(:email)
41
+ Escalated::TicketMailer.ticket_assigned(result).deliver_later
42
+ end
43
+
44
+ Services::NotificationService.dispatch(:ticket_assigned, ticket: result, agent: agent)
45
+
46
+ result
47
+ end
48
+
49
+ def unassign(ticket, actor:)
50
+ driver.unassign_ticket(ticket, actor: actor)
51
+ end
52
+
53
+ def reply(ticket, params)
54
+ reply = driver.add_reply(ticket, params)
55
+
56
+ if !params[:is_internal] && Escalated.configuration.notification_channels.include?(:email)
57
+ Escalated::TicketMailer.reply_received(ticket, reply).deliver_later
58
+ end
59
+
60
+ Services::NotificationService.dispatch(:reply_added, ticket: ticket, reply: reply)
61
+
62
+ reply
63
+ end
64
+
65
+ def find(id)
66
+ driver.get_ticket(id)
67
+ end
68
+
69
+ def list(filters = {})
70
+ driver.list_tickets(filters)
71
+ end
72
+
73
+ def add_tags(ticket, tag_ids, actor:)
74
+ driver.add_tags(ticket, tag_ids, actor: actor)
75
+ end
76
+
77
+ def remove_tags(ticket, tag_ids, actor:)
78
+ driver.remove_tags(ticket, tag_ids, actor: actor)
79
+ end
80
+
81
+ def change_department(ticket, department, actor:)
82
+ driver.change_department(ticket, department, actor: actor)
83
+ end
84
+
85
+ def change_priority(ticket, new_priority, actor:)
86
+ result = driver.change_priority(ticket, new_priority, actor: actor)
87
+
88
+ Services::NotificationService.dispatch(:priority_changed, ticket: result, priority: new_priority)
89
+
90
+ result
91
+ end
92
+
93
+ def close(ticket, actor:)
94
+ transition_status(ticket, :closed, actor: actor)
95
+ end
96
+
97
+ def reopen(ticket, actor:)
98
+ transition_status(ticket, :reopened, actor: actor)
99
+ end
100
+
101
+ def resolve(ticket, actor:)
102
+ transition_status(ticket, :resolved, actor: actor)
103
+ end
104
+
105
+ private
106
+
107
+ def driver
108
+ Escalated.driver
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
data/lib/escalated.rb ADDED
@@ -0,0 +1,25 @@
1
+ require "escalated/engine"
2
+ require "escalated/configuration"
3
+ require "escalated/manager"
4
+
5
+ module Escalated
6
+ class << self
7
+ attr_writer :configuration
8
+
9
+ def configuration
10
+ @configuration ||= Configuration.new
11
+ end
12
+
13
+ def configure
14
+ yield(configuration)
15
+ end
16
+
17
+ def driver
18
+ Manager.driver
19
+ end
20
+
21
+ def table_name(name)
22
+ "#{configuration.table_prefix}#{name}"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,75 @@
1
+ require "rails/generators"
2
+ require "rails/generators/migration"
3
+
4
+ module Escalated
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Installs the Escalated support ticket system"
12
+
13
+ def self.next_migration_number(dirname)
14
+ Time.now.strftime("%Y%m%d%H%M%S")
15
+ end
16
+
17
+ def copy_initializer
18
+ template "initializer.rb", "config/initializers/escalated.rb"
19
+ say_status :create, "config/initializers/escalated.rb", :green
20
+ end
21
+
22
+ def copy_migrations
23
+ rake "escalated:install:migrations"
24
+ say_status :info, "Copied migrations. Run `rails db:migrate` to apply.", :yellow
25
+ end
26
+
27
+ def add_user_concern
28
+ inject_into_file(
29
+ "app/models/user.rb",
30
+ after: "class User < ApplicationRecord\n"
31
+ ) do
32
+ <<-RUBY
33
+ # Escalated support system role methods
34
+ # Customize these methods to match your authorization system
35
+ def escalated_agent?
36
+ # Return true if this user is a support agent
37
+ respond_to?(:role) && %w[agent admin].include?(role)
38
+ end
39
+
40
+ def escalated_admin?
41
+ # Return true if this user is a support admin
42
+ respond_to?(:role) && role == "admin"
43
+ end
44
+
45
+ def self.escalated_agents
46
+ # Return a scope of all support agents
47
+ where(role: %w[agent admin])
48
+ end
49
+
50
+ RUBY
51
+ end
52
+ say_status :inject, "app/models/user.rb (Escalated role methods)", :green
53
+ rescue StandardError => e
54
+ say_status :skip, "Could not inject into User model: #{e.message}", :yellow
55
+ say " Add these methods to your User model manually:"
56
+ say " escalated_agent? - returns true for support agents"
57
+ say " escalated_admin? - returns true for support admins"
58
+ say " self.escalated_agents - scope returning all agents"
59
+ end
60
+
61
+ def show_post_install
62
+ say ""
63
+ say "Escalated installed successfully!", :green
64
+ say ""
65
+ say "Next steps:"
66
+ say " 1. Run `rails db:migrate`"
67
+ say " 2. Configure config/initializers/escalated.rb"
68
+ say " 3. Add escalated_agent? and escalated_admin? methods to your User model"
69
+ say " 4. Install Vue components: copy from vendor/escalated/resources/js/"
70
+ say " 5. Set up your Inertia page resolver to include Escalated pages"
71
+ say ""
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,89 @@
1
+ Escalated.configure do |config|
2
+ # ============================================================
3
+ # Hosting Mode
4
+ # ============================================================
5
+ # :self_hosted - All data in your local database (default)
6
+ # :synced - Local database + cloud sync
7
+ # :cloud - All data proxied to Escalated Cloud
8
+ config.mode = :self_hosted
9
+
10
+ # ============================================================
11
+ # User Configuration
12
+ # ============================================================
13
+ # Your application's user model class name
14
+ config.user_class = "User"
15
+
16
+ # ============================================================
17
+ # Database
18
+ # ============================================================
19
+ # Prefix for all Escalated database tables
20
+ config.table_prefix = "escalated_"
21
+
22
+ # ============================================================
23
+ # Routing
24
+ # ============================================================
25
+ # URL prefix for Escalated routes (e.g., /support/tickets)
26
+ config.route_prefix = "support"
27
+
28
+ # ============================================================
29
+ # Authentication & Authorization
30
+ # ============================================================
31
+ # Middleware applied to all Escalated routes
32
+ config.middleware = [:authenticate_user!]
33
+
34
+ # Additional middleware for admin routes (nil = same as middleware)
35
+ config.admin_middleware = nil
36
+
37
+ # ============================================================
38
+ # Ticket Settings
39
+ # ============================================================
40
+ # Allow customers to close their own tickets
41
+ config.allow_customer_close = true
42
+
43
+ # Automatically close resolved tickets after N days (nil to disable)
44
+ config.auto_close_resolved_after_days = 7
45
+
46
+ # Default priority for new tickets
47
+ config.default_priority = :medium
48
+
49
+ # ============================================================
50
+ # Attachments
51
+ # ============================================================
52
+ # Maximum number of attachments per ticket/reply
53
+ config.max_attachments = 5
54
+
55
+ # Maximum file size in KB (10 MB default)
56
+ config.max_attachment_size_kb = 10_240
57
+
58
+ # ActiveStorage service to use (:local, :amazon, :google, etc.)
59
+ config.storage_service = :local
60
+
61
+ # ============================================================
62
+ # SLA Configuration
63
+ # ============================================================
64
+ config.sla = {
65
+ enabled: true,
66
+ business_hours_only: true,
67
+ business_hours: {
68
+ start: 9,
69
+ end: 17,
70
+ timezone: "UTC",
71
+ working_days: [1, 2, 3, 4, 5] # Monday through Friday
72
+ }
73
+ }
74
+
75
+ # ============================================================
76
+ # Notifications
77
+ # ============================================================
78
+ # Available channels: :email
79
+ config.notification_channels = [:email]
80
+
81
+ # Webhook URL for external integrations (nil to disable)
82
+ config.webhook_url = nil
83
+
84
+ # ============================================================
85
+ # Cloud Configuration (only for :synced and :cloud modes)
86
+ # ============================================================
87
+ # config.hosted_api_url = "https://cloud.escalated.dev/api/v1"
88
+ # config.hosted_api_key = ENV["ESCALATED_API_KEY"]
89
+ end