support_desk 0.1.3 → 0.2.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -0
  3. data/README.md +105 -10
  4. data/Rakefile +16 -0
  5. data/app/controllers/support_desk/tickets_controller.rb +5 -3
  6. data/app/helpers/support_desk/engine_helper.rb +7 -1
  7. data/app/views/support_desk/console/tickets/_context_card.html.erb +5 -0
  8. data/app/views/support_desk/console/tickets/_new_conversation_form.html.erb +91 -0
  9. data/app/views/support_desk/console/tickets/_ticket_row.html.erb +8 -0
  10. data/app/views/support_desk/console/tickets/index.html.erb +14 -2
  11. data/app/views/support_desk/console/tickets/new.html.erb +25 -0
  12. data/config/console_routes.rb +1 -1
  13. data/config/locales/support_desk.console.en.yml +26 -0
  14. data/config/locales/support_desk.console.es.yml +26 -0
  15. data/config/locales/support_desk.en.yml +3 -0
  16. data/config/locales/support_desk.es.yml +6 -0
  17. data/lib/generators/support_desk/console_generator.rb +1 -1
  18. data/lib/generators/support_desk/install_generator.rb +14 -0
  19. data/lib/generators/support_desk/templates/add_opened_by_to_support_desk_tickets.rb.erb +79 -0
  20. data/lib/generators/support_desk/templates/console/controller.rb.erb +1 -1
  21. data/lib/generators/support_desk/templates/initializer.rb +30 -0
  22. data/lib/generators/support_desk/upgrade_generator.rb +56 -0
  23. data/lib/support_desk/configuration.rb +148 -2
  24. data/lib/support_desk/console.rb +347 -25
  25. data/lib/support_desk/console_engine.rb +2 -0
  26. data/lib/support_desk/console_routes.rb +20 -8
  27. data/lib/support_desk/context_card.rb +23 -0
  28. data/lib/support_desk/doctor.rb +51 -1
  29. data/lib/support_desk/errors.rb +5 -0
  30. data/lib/support_desk/macros.rb +28 -12
  31. data/lib/support_desk/models/assignment.rb +3 -1
  32. data/lib/support_desk/models/concerns/agent.rb +39 -4
  33. data/lib/support_desk/models/concerns/requester.rb +27 -12
  34. data/lib/support_desk/models/ticket.rb +464 -100
  35. data/lib/support_desk/summary.rb +1 -1
  36. data/lib/support_desk/test_helpers.rb +8 -3
  37. data/lib/support_desk/version.rb +1 -1
  38. data/lib/support_desk/wizard.rb +4 -12
  39. data/lib/support_desk.rb +45 -0
  40. data/lib/tasks/support_desk.rake +27 -0
  41. metadata +7 -2
@@ -46,7 +46,7 @@ module SupportDesk
46
46
  # this speaks the reader's language. Duration#inspect is English
47
47
  # whatever the locale, which left one untranslatable string in an
48
48
  # otherwise Spanish console.
49
- Wizard.humanize_duration(duration)
49
+ SupportDesk.humanize_duration(duration)
50
50
  end
51
51
 
52
52
  # The whole line.
@@ -6,6 +6,7 @@ module SupportDesk
6
6
  # include SupportDesk::TestHelpers
7
7
  #
8
8
  # ticket = open_support_ticket(for: users(:alice), about: rides(:sevilla), message: "No aparece")
9
+ # ticket = open_support_ticket(for: users(:alice), by: users(:lucia), message: "Vimos que…")
9
10
  # reply_as users(:lucia), ticket, "Lo miramos"
10
11
  # assert_awaiting_requester ticket
11
12
  # assert_ticket_event ticket, :handed_off, from: users(:lucia), to: users(:pedro)
@@ -15,10 +16,14 @@ module SupportDesk
15
16
  # The assertions read the same way the gem's own suite does, which is the
16
17
  # point: your acceptance tests and ours describe the same behaviour.
17
18
  module TestHelpers
18
- # Open a ticket the way a requester would, and hand it back.
19
- def open_support_ticket(message: "Necesito ayuda", about: nil, topic: nil, **options)
19
+ # Open a ticket the way a requester would — or, with `by:`, the way the
20
+ # desk does when it writes first. Through the public sugar in both cases,
21
+ # so this helper exercises what hosts actually type.
22
+ def open_support_ticket(message: "Necesito ayuda", about: nil, topic: nil, by: nil, **options)
20
23
  requester = options.fetch(:for) { raise ArgumentError, "open_support_ticket needs for: a requester" }
21
- requester.ask_support!(message, about: about, topic: topic)
24
+ return requester.ask_support!(message, about: about, topic: topic) if by.nil? || by == requester
25
+
26
+ by.open_support_conversation_with!(requester, message, about: about, topic: topic)
22
27
  end
23
28
 
24
29
  # Answer as an agent. Returns the Chats::Message.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SupportDesk
4
- VERSION = "0.1.3"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -188,18 +188,10 @@ module SupportDesk
188
188
  # The promise as a Duration, for hosts that want to phrase it themselves.
189
189
  def promise_within = target_desk.config.reply_within
190
190
 
191
- # "1 day", "4 horas" — through ActionView's date helper so it speaks the
192
- # requester's language, falling back to Duration#inspect in the (rare)
193
- # app that has no ActionView.
194
- def self.humanize_duration(duration)
195
- return duration.inspect unless defined?(ActionView::Helpers::DateHelper)
196
-
197
- @duration_words ||= Object.new.extend(ActionView::Helpers::DateHelper)
198
- words = @duration_words.distance_of_time_in_words(duration.to_i).to_s
199
- # An app whose locale has no date translations (no rails-i18n) would
200
- # otherwise show "Translation missing" to a customer.
201
- words.start_with?("Translation missing") ? duration.inspect : words
202
- end
191
+ # "1 day", "4 horas" — the summary, the helper and an opening line's
192
+ # `%{reply_within}` all say it too, so it lives on SupportDesk now. Kept
193
+ # here as a delegation: it has been public since 0.1.
194
+ def self.humanize_duration(duration) = SupportDesk.humanize_duration(duration)
203
195
 
204
196
  # The open case this would land in, when the requester already has one
205
197
  # about this thing — so the wizard can say "you already have a
data/lib/support_desk.rb CHANGED
@@ -158,6 +158,17 @@ module SupportDesk
158
158
  # Every class that has declared itself able to answer.
159
159
  def agent_class_names = @agent_class_names ||= Set.new
160
160
 
161
+ # The registered classes themselves, for the places that have to restrict
162
+ # a lookup to them — the console's GlobalID allow-lists, where a raw
163
+ # token is an identifier and never an authorization to call `find` on
164
+ # whatever class name it names.
165
+ #
166
+ # Names that no longer resolve are dropped rather than raised on: the
167
+ # registries store names so they survive reloads, and a class that went
168
+ # away is simply not one of the classes people can be looked up as.
169
+ def requester_classes = resolve_all(requester_class_names)
170
+ def supportable_classes = resolve_all(supportable_class_names)
171
+
161
172
  # Whether +klass+ (a Class, an instance, or a class name) is supportable.
162
173
  def supportable_class?(klass) = registered?(supportable_class_names, klass)
163
174
  # Whether +klass+ asks for support / answers it. Ancestor-aware, so an
@@ -165,6 +176,20 @@ module SupportDesk
165
176
  def requester_class?(klass) = registered?(requester_class_names, klass)
166
177
  def agent_class?(klass) = registered?(agent_class_names, klass)
167
178
 
179
+ # --- Macro conditions -----------------------------------------------------------
180
+
181
+ # Whether +record+ passes an `if:` condition from a macro — nil is always
182
+ # yes. Both `has_support_tickets if:` (may this person ask for help, and
183
+ # be written to) and `acts_as_support_agent if:` (may this person answer)
184
+ # are read through here, so the two conditions can never drift into two
185
+ # meanings of the same option.
186
+ def eligible?(record, condition)
187
+ return true if condition.nil?
188
+ return !!record.public_send(condition) if condition.is_a?(Symbol)
189
+
190
+ !!condition.call(record)
191
+ end
192
+
168
193
  # --- The chats seam -------------------------------------------------------------
169
194
 
170
195
  # Subscribe the gem's own `:message_created` listener, which is what
@@ -257,6 +282,22 @@ module SupportDesk
257
282
  ]
258
283
  end
259
284
 
285
+ # A duration in the reader's own language ("1 día", "4 hours") — the
286
+ # answer promise in the wizard's line, the wait in a console summary, and
287
+ # the `%{reply_within}` an opening line can interpolate.
288
+ #
289
+ # Duration#inspect is English whatever the locale, which is what left one
290
+ # untranslatable string in an otherwise Spanish console.
291
+ def humanize_duration(duration)
292
+ return duration.inspect unless defined?(ActionView::Helpers::DateHelper)
293
+
294
+ @duration_words ||= Object.new.extend(ActionView::Helpers::DateHelper)
295
+ words = @duration_words.distance_of_time_in_words(duration.to_i).to_s
296
+ # An app whose locale has no date translations (no rails-i18n) would
297
+ # otherwise show "Translation missing" to a customer.
298
+ words.start_with?("Translation missing") ? duration.inspect : words
299
+ end
300
+
260
301
  # A stable, URL-safe key for an actor (agent, requester, desk), used in
261
302
  # cache keys and event payloads. GlobalID params already encode class +
262
303
  # id, so two classes can never collide.
@@ -274,6 +315,10 @@ module SupportDesk
274
315
  klass
275
316
  end
276
317
 
318
+ def resolve_all(registry)
319
+ registry.filter_map { |name| name.safe_constantize }
320
+ end
321
+
277
322
  def registered?(registry, klass)
278
323
  klass = klass.class unless klass.is_a?(Class) || klass.is_a?(String)
279
324
  name = klass.is_a?(String) ? klass : klass.name
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :support_desk do
4
+ desc "Point every case with no provenance at its requester (the 0.1 → 0.2 catch-up)"
5
+ task backfill_opened_by: :environment do
6
+ # The same UPDATE the 0.2.0 migration runs, for the handover window: a
7
+ # 0.1 process still serving traffic writes NULL provenance after the
8
+ # migration's own backfill, so the catch-up is run once more when those
9
+ # processes are gone.
10
+ #
11
+ # Idempotent, and safe to run at any time, for one reason: 0.2 opens
12
+ # every case as a record (automation openers are refused — see
13
+ # docs/12-open-questions.md Q17), so a NULL `opened_by` can only be a row
14
+ # 0.1 wrote. The day automation lands, NULL becomes legitimate provenance
15
+ # and this task stops being safe — that is part of the work Q17 names.
16
+ relation = SupportDesk::Ticket.where(opened_by_id: nil)
17
+ pending = relation.count
18
+
19
+ if pending.zero?
20
+ puts "[support_desk] every case already says who opened it."
21
+ next
22
+ end
23
+
24
+ updated = relation.update_all("opened_by_type = requester_type, opened_by_id = requester_id")
25
+ puts "[support_desk] #{updated} case(s) now point at their requester."
26
+ end
27
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: support_desk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-09-17 00:00:00.000000000 Z
10
+ date: 2026-09-18 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord
@@ -144,11 +144,13 @@ files:
144
144
  - app/views/support_desk/console/tickets/_context_card.html.erb
145
145
  - app/views/support_desk/console/tickets/_message.html.erb
146
146
  - app/views/support_desk/console/tickets/_nav_badge.html.erb
147
+ - app/views/support_desk/console/tickets/_new_conversation_form.html.erb
147
148
  - app/views/support_desk/console/tickets/_tabs.html.erb
148
149
  - app/views/support_desk/console/tickets/_ticket_row.html.erb
149
150
  - app/views/support_desk/console/tickets/_timeline.html.erb
150
151
  - app/views/support_desk/console/tickets/_transcript.html.erb
151
152
  - app/views/support_desk/console/tickets/index.html.erb
153
+ - app/views/support_desk/console/tickets/new.html.erb
152
154
  - app/views/support_desk/console/tickets/show.html.erb
153
155
  - app/views/support_desk/tickets/_context_card.html.erb
154
156
  - app/views/support_desk/tickets/_door.html.erb
@@ -172,10 +174,12 @@ files:
172
174
  - gemfiles/rails_8.1.gemfile
173
175
  - lib/generators/support_desk/console_generator.rb
174
176
  - lib/generators/support_desk/install_generator.rb
177
+ - lib/generators/support_desk/templates/add_opened_by_to_support_desk_tickets.rb.erb
175
178
  - lib/generators/support_desk/templates/console/controller.rb.erb
176
179
  - lib/generators/support_desk/templates/console/resource.rb.erb
177
180
  - lib/generators/support_desk/templates/create_support_desk_tables.rb.erb
178
181
  - lib/generators/support_desk/templates/initializer.rb
182
+ - lib/generators/support_desk/upgrade_generator.rb
179
183
  - lib/generators/support_desk/views_generator.rb
180
184
  - lib/support_desk.rb
181
185
  - lib/support_desk/configuration.rb
@@ -205,6 +209,7 @@ files:
205
209
  - lib/support_desk/topic_tree.rb
206
210
  - lib/support_desk/version.rb
207
211
  - lib/support_desk/wizard.rb
212
+ - lib/tasks/support_desk.rake
208
213
  homepage: https://github.com/rameerez/support_desk
209
214
  licenses:
210
215
  - MIT