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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -0
- data/README.md +105 -10
- data/Rakefile +16 -0
- data/app/controllers/support_desk/tickets_controller.rb +5 -3
- data/app/helpers/support_desk/engine_helper.rb +7 -1
- data/app/views/support_desk/console/tickets/_context_card.html.erb +5 -0
- data/app/views/support_desk/console/tickets/_new_conversation_form.html.erb +91 -0
- data/app/views/support_desk/console/tickets/_ticket_row.html.erb +8 -0
- data/app/views/support_desk/console/tickets/index.html.erb +14 -2
- data/app/views/support_desk/console/tickets/new.html.erb +25 -0
- data/config/console_routes.rb +1 -1
- data/config/locales/support_desk.console.en.yml +26 -0
- data/config/locales/support_desk.console.es.yml +26 -0
- data/config/locales/support_desk.en.yml +3 -0
- data/config/locales/support_desk.es.yml +6 -0
- data/lib/generators/support_desk/console_generator.rb +1 -1
- data/lib/generators/support_desk/install_generator.rb +14 -0
- data/lib/generators/support_desk/templates/add_opened_by_to_support_desk_tickets.rb.erb +79 -0
- data/lib/generators/support_desk/templates/console/controller.rb.erb +1 -1
- data/lib/generators/support_desk/templates/initializer.rb +30 -0
- data/lib/generators/support_desk/upgrade_generator.rb +56 -0
- data/lib/support_desk/configuration.rb +148 -2
- data/lib/support_desk/console.rb +347 -25
- data/lib/support_desk/console_engine.rb +2 -0
- data/lib/support_desk/console_routes.rb +20 -8
- data/lib/support_desk/context_card.rb +23 -0
- data/lib/support_desk/doctor.rb +51 -1
- data/lib/support_desk/errors.rb +5 -0
- data/lib/support_desk/macros.rb +28 -12
- data/lib/support_desk/models/assignment.rb +3 -1
- data/lib/support_desk/models/concerns/agent.rb +39 -4
- data/lib/support_desk/models/concerns/requester.rb +27 -12
- data/lib/support_desk/models/ticket.rb +464 -100
- data/lib/support_desk/summary.rb +1 -1
- data/lib/support_desk/test_helpers.rb +8 -3
- data/lib/support_desk/version.rb +1 -1
- data/lib/support_desk/wizard.rb +4 -12
- data/lib/support_desk.rb +45 -0
- data/lib/tasks/support_desk.rake +27 -0
- metadata +7 -2
data/lib/support_desk/macros.rb
CHANGED
|
@@ -20,11 +20,15 @@ module SupportDesk
|
|
|
20
20
|
module Macros
|
|
21
21
|
# The requester side. `desk:` names which desk this model writes to;
|
|
22
22
|
# `as:` records a role on every ticket ("driver", "passenger") so the
|
|
23
|
-
# console can tell two kinds of requester apart
|
|
24
|
-
|
|
23
|
+
# console can tell two kinds of requester apart; `if:` is the per-record
|
|
24
|
+
# check for "may this person ask for help — and be written to — right
|
|
25
|
+
# now" (`if: :kept?` for a host with soft-deleted accounts).
|
|
26
|
+
def has_support_tickets(desk: :default, as: nil, **options)
|
|
27
|
+
condition = Macros.condition!(options, macro: "has_support_tickets", known: ":if, :desk, :as")
|
|
28
|
+
|
|
25
29
|
include SupportDesk::Requester
|
|
26
30
|
|
|
27
|
-
self.support_desk_requester_options = { desk: desk.to_sym, as: as&.to_s }.freeze
|
|
31
|
+
self.support_desk_requester_options = { desk: desk.to_sym, as: as&.to_s, if: condition }.freeze
|
|
28
32
|
SupportDesk.register_requester(self)
|
|
29
33
|
end
|
|
30
34
|
|
|
@@ -48,26 +52,38 @@ module SupportDesk
|
|
|
48
52
|
|
|
49
53
|
# The agent side: who may answer. `if:` is the per-record eligibility
|
|
50
54
|
# check (a Symbol method name or a callable); `kind:` is :human or :ai.
|
|
51
|
-
#
|
|
52
|
-
#
|
|
55
|
+
# One verb is added — `open_support_conversation_with!`, the only agent
|
|
56
|
+
# action that has no ticket yet. Everything else keeps the ticket as the
|
|
57
|
+
# subject of the sentence (`ticket.assign!(to: lucia)`).
|
|
53
58
|
def acts_as_support_agent(kind: :human, **options)
|
|
59
|
+
condition = Macros.condition!(options, macro: "acts_as_support_agent", known: ":if, :kind")
|
|
60
|
+
|
|
61
|
+
include SupportDesk::Agent
|
|
62
|
+
|
|
63
|
+
self.support_desk_agent_options = { if: condition, kind: kind.to_sym }.freeze
|
|
64
|
+
SupportDesk.register_agent(self)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The `if:` both macros take: a method name or a callable, and the only
|
|
68
|
+
# option either of them accepts beyond its own keywords. Written once
|
|
69
|
+
# because "may this person answer" and "may this person ask" are the same
|
|
70
|
+
# question asked of a different side, and two copies of it would be two
|
|
71
|
+
# places to fix the day one grows a third shape.
|
|
72
|
+
def self.condition!(options, macro:, known:)
|
|
54
73
|
unknown = options.keys - [ :if ]
|
|
55
74
|
if unknown.any?
|
|
56
75
|
raise ConfigurationError,
|
|
57
|
-
"unknown
|
|
58
|
-
"#{unknown.map(&:inspect).join(", ")} — known options are
|
|
76
|
+
"unknown #{macro} option#{"s" if unknown.size > 1} " \
|
|
77
|
+
"#{unknown.map(&:inspect).join(", ")} — known options are #{known}"
|
|
59
78
|
end
|
|
60
79
|
|
|
61
80
|
condition = options[:if]
|
|
62
81
|
unless condition.nil? || condition.is_a?(Symbol) || condition.respond_to?(:call)
|
|
63
82
|
raise ConfigurationError,
|
|
64
|
-
"
|
|
83
|
+
"#{macro} if: must be a method name or a callable, got #{condition.inspect}"
|
|
65
84
|
end
|
|
66
85
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
self.support_desk_agent_options = { if: condition, kind: kind.to_sym }.freeze
|
|
70
|
-
SupportDesk.register_agent(self)
|
|
86
|
+
condition
|
|
71
87
|
end
|
|
72
88
|
end
|
|
73
89
|
end
|
|
@@ -11,7 +11,9 @@ module SupportDesk
|
|
|
11
11
|
class Assignment < ApplicationRecord
|
|
12
12
|
self.table_name = "support_desk_assignments"
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# `opened`: the agent who wrote first holds the case. It is a seat nobody
|
|
15
|
+
# was told about — see Ticket.open!(by:) — not an assignment somebody made.
|
|
16
|
+
REASONS = %w[taken assigned handed_off routed drop_in_takeover escalated reopened opened].freeze
|
|
15
17
|
RELEASE_REASONS = %w[handed_off released shift_end closed escalated].freeze
|
|
16
18
|
|
|
17
19
|
belongs_to :ticket, class_name: "SupportDesk::Ticket", inverse_of: :assignments
|
|
@@ -38,11 +38,46 @@ module SupportDesk
|
|
|
38
38
|
# Whether this record may answer tickets right now — the `if:` condition
|
|
39
39
|
# from the macro, honoured.
|
|
40
40
|
def support_agent?
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
SupportDesk.eligible?(self, self.class.support_desk_agent_options[:if])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Write first, as the desk — the mirror of Requester#ask_support!.
|
|
45
|
+
#
|
|
46
|
+
# lucia.open_support_conversation_with!(alice, "Vimos que tu retirada rebotó", about: withdrawal)
|
|
47
|
+
# lucia.open_support_conversation_with!(alice, "Tu DNI no se lee bien", topic: :verification)
|
|
48
|
+
#
|
|
49
|
+
# Not `message!`: that is a personal chat from Lucía. This speaks as the
|
|
50
|
+
# desk, signs the message with her name, seats her on the case, and lands
|
|
51
|
+
# in Alice's inbox as "Soporte". If Alice already has this conversation
|
|
52
|
+
# open, the message joins it as an ordinary reply, under the desk's reply
|
|
53
|
+
# policy — which can leave the case with whoever already holds it.
|
|
54
|
+
# Returns the SupportDesk::Ticket.
|
|
55
|
+
#
|
|
56
|
+
# Raises NotARequester (nobody to write to), NotAllowed (including an
|
|
57
|
+
# agent aiming at themselves: an agent who needs help asks for it),
|
|
58
|
+
# NotSupportable, UnknownTopic.
|
|
59
|
+
def open_support_conversation_with!(requester, message, about: nil, topic: nil, files: [], via: :in_app,
|
|
60
|
+
desk: nil, request: nil)
|
|
61
|
+
SupportDesk::Ticket.ensure_requester!(requester)
|
|
62
|
+
|
|
63
|
+
if SupportDesk::Ticket.same_actor?(requester, self)
|
|
64
|
+
raise SupportDesk::NotAllowed,
|
|
65
|
+
"#{self.class}##{id} can't open a support conversation with themselves — an agent who needs " \
|
|
66
|
+
"help asks for it (ask_support!), and the case would be theirs to answer"
|
|
67
|
+
end
|
|
44
68
|
|
|
45
|
-
|
|
69
|
+
SupportDesk::Ticket.open!(
|
|
70
|
+
requester: requester,
|
|
71
|
+
by: self,
|
|
72
|
+
message: message,
|
|
73
|
+
about: about,
|
|
74
|
+
topic: topic,
|
|
75
|
+
files: files,
|
|
76
|
+
via: via,
|
|
77
|
+
desk: desk || requester.support_desk,
|
|
78
|
+
requester_role: requester.class.support_desk_requester_options[:as],
|
|
79
|
+
request: request
|
|
80
|
+
)
|
|
46
81
|
end
|
|
47
82
|
|
|
48
83
|
# :human or :ai. Bots disclose themselves through this (04).
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SupportDesk
|
|
4
|
-
# Included by `has_support_tickets`.
|
|
5
|
-
# requester side of the gem:
|
|
4
|
+
# Included by `has_support_tickets`. The whole requester side of the gem:
|
|
6
5
|
#
|
|
7
6
|
# alice.ask_support!("El viaje no aparece verificado", about: ride)
|
|
8
7
|
# alice.support_tickets.open.about(ride)
|
|
8
|
+
# alice.support_requester?
|
|
9
|
+
# alice.support_desk
|
|
9
10
|
# alice.awaiting_support_reply?
|
|
10
11
|
# alice.unread_support_count
|
|
11
12
|
module Requester
|
|
@@ -13,7 +14,7 @@ module SupportDesk
|
|
|
13
14
|
|
|
14
15
|
included do
|
|
15
16
|
class_attribute :support_desk_requester_options, instance_writer: false,
|
|
16
|
-
default: { desk: :default, as: nil }.freeze
|
|
17
|
+
default: { desk: :default, as: nil, if: nil }.freeze
|
|
17
18
|
|
|
18
19
|
has_many :support_tickets,
|
|
19
20
|
class_name: "SupportDesk::Ticket",
|
|
@@ -26,6 +27,26 @@ module SupportDesk
|
|
|
26
27
|
def support_desk_key = support_desk_requester_options[:desk]
|
|
27
28
|
end
|
|
28
29
|
|
|
30
|
+
# Whether this record may ask for help — or be written to — right now:
|
|
31
|
+
# the `if:` condition from `has_support_tickets`, honoured. A closed
|
|
32
|
+
# account answers false and can do neither, while its history stays
|
|
33
|
+
# readable (see Ticket#chat_locked?).
|
|
34
|
+
def support_requester?
|
|
35
|
+
SupportDesk.eligible?(self, self.class.support_desk_requester_options[:if])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The desk this requester writes to (`has_support_tickets desk:`), as a
|
|
39
|
+
# record. Raises ConfigurationError naming the fix when that desk isn't
|
|
40
|
+
# configured — its tickets would otherwise silently land on the default
|
|
41
|
+
# desk.
|
|
42
|
+
def support_desk
|
|
43
|
+
key = self.class.support_desk_key
|
|
44
|
+
SupportDesk.desk(key) ||
|
|
45
|
+
raise(SupportDesk::ConfigurationError,
|
|
46
|
+
"#{self.class} writes to desk #{key.inspect}, which isn't configured — " \
|
|
47
|
+
"its tickets would silently land on the default desk")
|
|
48
|
+
end
|
|
49
|
+
|
|
29
50
|
# Open a ticket and say the first thing. Returns the SupportDesk::Ticket
|
|
30
51
|
# — the existing open one when this requester already has a ticket about
|
|
31
52
|
# the same record (or, for free-form tickets, the same topic).
|
|
@@ -33,15 +54,9 @@ module SupportDesk
|
|
|
33
54
|
# alice.ask_support!("No me han pagado", about: withdrawal)
|
|
34
55
|
# alice.ask_support!("¿Cómo borro mi cuenta?", topic: :account)
|
|
35
56
|
#
|
|
36
|
-
# Raises NotSupportable, UnknownTopic, NotAllowed,
|
|
37
|
-
# TooManyOpenTickets.
|
|
57
|
+
# Raises NotARequester, NotSupportable, UnknownTopic, NotAllowed,
|
|
58
|
+
# RateLimited, TooManyOpenTickets.
|
|
38
59
|
def ask_support!(message, about: nil, topic: nil, files: [], via: :in_app)
|
|
39
|
-
key = self.class.support_desk_requester_options[:desk]
|
|
40
|
-
desk = SupportDesk.desk(key) ||
|
|
41
|
-
raise(SupportDesk::ConfigurationError,
|
|
42
|
-
"#{self.class} writes to desk #{key.inspect}, which isn't configured — " \
|
|
43
|
-
"its tickets would silently land on the default desk")
|
|
44
|
-
|
|
45
60
|
SupportDesk::Ticket.open!(
|
|
46
61
|
requester: self,
|
|
47
62
|
message: message,
|
|
@@ -49,7 +64,7 @@ module SupportDesk
|
|
|
49
64
|
topic: topic,
|
|
50
65
|
files: files,
|
|
51
66
|
via: via,
|
|
52
|
-
desk:
|
|
67
|
+
desk: support_desk,
|
|
53
68
|
requester_role: self.class.support_desk_requester_options[:as]
|
|
54
69
|
)
|
|
55
70
|
end
|