support_desk 0.1.2 → 0.1.3
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 +14 -0
- data/README.md +8 -2
- data/app/controllers/support_desk/tickets_controller.rb +6 -0
- data/app/views/support_desk/tickets/_write.html.erb +1 -0
- data/lib/support_desk/console.rb +11 -1
- data/lib/support_desk/models/ticket.rb +54 -35
- data/lib/support_desk/topic.rb +8 -29
- data/lib/support_desk/version.rb +1 -1
- data/lib/support_desk/wizard.rb +6 -15
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 587ddd67c318b1c6b4cbe7b496a65ec2bfb70b39818556a5d90ad81415db17c8
|
|
4
|
+
data.tar.gz: 3ab9972eca4fc07680e1b33d139400d7f1c6ef9116e3778967f070d05f5111e1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 27e60ca1419b9fd778e6d57ac35dea358b7f422829b92c36f00d123153b967a0171c8f7edfeeef8715e597b2e169fa48629325d4314aa2fdc2593f8c27662924
|
|
7
|
+
data.tar.gz: f04e5ba138fd6e847c0aad98ef5f42eeaf1018900eccd4f3b72af15a9ba82a00e9506bf2412e900145918a2eb97299b5b0fbb2d1b0bcabba0c23b890ae027fc3
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.1.3] - 2026-09-17
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Opening a case and its first message is atomic; rejected uploads render validation errors with the draft preserved.
|
|
12
|
+
- Reopening historical cases preserves both conversations when a newer case already exists; new submissions still reuse an open case.
|
|
13
|
+
- Assignment authorizes its actor, and assignment, hand-off, release and reply policy check current state under the row lock.
|
|
14
|
+
- Delayed message registration derives whose turn it is from both clocks and cannot reopen a case closed after the message.
|
|
15
|
+
- Reading an authorized console transcript marks the desk's read horizon through the last displayed message.
|
|
16
|
+
- Topics resolve within their ticket's desk; wizard routing and response promises honor nondefault desks.
|
|
17
|
+
- The requester upload form uses multipart encoding, and its send budget is shared with chats.
|
|
18
|
+
- Requires chats >= 0.3.2, whose authors need no messaging capabilities of
|
|
19
|
+
their own, so staff sign a desk reply without becoming messagers.
|
|
20
|
+
|
|
7
21
|
## [0.1.2] - 2026-09-16
|
|
8
22
|
|
|
9
23
|
### Fixed
|
data/README.md
CHANGED
|
@@ -429,13 +429,19 @@ end
|
|
|
429
429
|
|
|
430
430
|
Pass `key:` from anywhere that runs more than once (a `to_prepare` block, an engine initializer) and re-registering replaces that subscriber instead of stacking a copy on every reload.
|
|
431
431
|
|
|
432
|
-
Keep
|
|
432
|
+
Keep push titles and bodies generic — `ticket.notification_title` is safe for previews.
|
|
433
|
+
`ticket.notification_body` contains case details for an authenticated feed, not a lock screen.
|
|
433
434
|
|
|
434
435
|
## Compatibility
|
|
435
436
|
|
|
436
437
|
Rails 7.2, 8.0 and 8.1; Ruby >= 3.2; PostgreSQL, SQLite and MySQL; bigint or UUID primary keys (the migration follows your app's `primary_key_type`).
|
|
437
438
|
|
|
438
|
-
PostgreSQL and SQLite
|
|
439
|
+
PostgreSQL and SQLite enforce unique new-case creation and one open assignment per ticket
|
|
440
|
+
with **partial unique indexes**. New submissions reuse an existing open case. Reopening
|
|
441
|
+
historical cases is deliberately exempt from new-case deduplication: if a newer case
|
|
442
|
+
already exists, both histories remain open and support is notified of the reply. No
|
|
443
|
+
conversation is silently merged, closed or discarded. MySQL has no partial indexes,
|
|
444
|
+
so these guarantees are model-only there; the suite's matrix is SQLite and PostgreSQL.
|
|
439
445
|
|
|
440
446
|
## Testing
|
|
441
447
|
|
|
@@ -14,6 +14,7 @@ module SupportDesk
|
|
|
14
14
|
CLOSED_TICKETS_SHOWN = 20
|
|
15
15
|
|
|
16
16
|
before_action :set_wizard, only: %i[new create]
|
|
17
|
+
include Chats::SendRateLimited
|
|
17
18
|
|
|
18
19
|
# Their cases: open ones as rows, closed ones folded away, and the door
|
|
19
20
|
# to open another.
|
|
@@ -45,6 +46,9 @@ module SupportDesk
|
|
|
45
46
|
|
|
46
47
|
ticket = @wizard.open!(message, files: files)
|
|
47
48
|
redirect_to conversation_path_for(ticket)
|
|
49
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
50
|
+
@error = e.record.errors.full_messages.to_sentence
|
|
51
|
+
render :new, status: :unprocessable_entity
|
|
48
52
|
rescue SupportDesk::RateLimited, SupportDesk::TooManyOpenTickets => e
|
|
49
53
|
render_limit_wall(e)
|
|
50
54
|
rescue SupportDesk::InvalidTransition
|
|
@@ -66,6 +70,8 @@ module SupportDesk
|
|
|
66
70
|
|
|
67
71
|
private
|
|
68
72
|
|
|
73
|
+
def chat_rate_limit_messager = current_requester
|
|
74
|
+
|
|
69
75
|
def set_wizard
|
|
70
76
|
@wizard = SupportDesk::Wizard.new(current_requester, params)
|
|
71
77
|
# A token that is forged, expired, or points at somebody else's record
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
<%= render "support_desk/tickets/context_card", subject: wizard.subject if wizard.subject %>
|
|
21
21
|
|
|
22
22
|
<%= form_with url: support_desk_routes.tickets_path,
|
|
23
|
+
multipart: true,
|
|
23
24
|
method: :post,
|
|
24
25
|
class: "support-desk-form",
|
|
25
26
|
data: { turbo_frame: "_top" } do %>
|
data/lib/support_desk/console.rb
CHANGED
|
@@ -102,6 +102,7 @@ module SupportDesk
|
|
|
102
102
|
before_action :set_support_ticket, only: MEMBER_ACTIONS
|
|
103
103
|
before_action :authorize_support_console!
|
|
104
104
|
before_action :require_offered_action!, only: TRANSITIONS
|
|
105
|
+
after_action :mark_support_transcript_read, only: :show
|
|
105
106
|
|
|
106
107
|
helper_method :current_agent, :support_desk_record, :support_queue, :support_transcript,
|
|
107
108
|
:console_ticket_path, :console_tickets_path, :console_file_path
|
|
@@ -218,7 +219,10 @@ module SupportDesk
|
|
|
218
219
|
# Override to paginate a long one.
|
|
219
220
|
def support_transcript(ticket = @ticket)
|
|
220
221
|
scope = ticket.messages.visible.oldest_first.includes(:sender, :author)
|
|
221
|
-
scope
|
|
222
|
+
scope = scope.with_attached_files if scope.respond_to?(:with_attached_files)
|
|
223
|
+
scope.load
|
|
224
|
+
@support_read_through = scope.last&.created_at if ticket == @ticket
|
|
225
|
+
scope
|
|
222
226
|
end
|
|
223
227
|
|
|
224
228
|
# The default queue tab and the tickets behind it, for hosts that want
|
|
@@ -276,6 +280,12 @@ module SupportDesk
|
|
|
276
280
|
|
|
277
281
|
private
|
|
278
282
|
|
|
283
|
+
def mark_support_transcript_read
|
|
284
|
+
return unless response.successful? && @support_read_through
|
|
285
|
+
|
|
286
|
+
@ticket.conversation.participant_for(@ticket.desk)&.read!(at: @support_read_through)
|
|
287
|
+
end
|
|
288
|
+
|
|
279
289
|
# --- Who is asking ------------------------------------------------------------
|
|
280
290
|
|
|
281
291
|
# The person answering. Define `current_agent` in your controller, or
|
|
@@ -55,11 +55,18 @@ module SupportDesk
|
|
|
55
55
|
has_many :events, class_name: "SupportDesk::Event", inverse_of: :ticket, dependent: :delete_all
|
|
56
56
|
has_many :messages, through: :conversation, source: :messages
|
|
57
57
|
|
|
58
|
-
#
|
|
59
|
-
#
|
|
58
|
+
# Persist only the path. Resolving behavior needs this ticket's desk;
|
|
59
|
+
# an attribute caster has no record context and cannot choose the tree.
|
|
60
60
|
attribute :topic, Topic::Type.new
|
|
61
61
|
attribute :metadata, default: -> { {} }
|
|
62
62
|
|
|
63
|
+
def topic
|
|
64
|
+
path = self[:topic]
|
|
65
|
+
return if path.nil?
|
|
66
|
+
|
|
67
|
+
desk_config.topics.find(path) || Topic::Unknown.new(path)
|
|
68
|
+
end
|
|
69
|
+
|
|
63
70
|
validates :status, inclusion: { in: STATUSES }
|
|
64
71
|
validates :awaiting, inclusion: { in: AWAITING_STATES }
|
|
65
72
|
validates :opened_via, inclusion: { in: CHANNELS }
|
|
@@ -166,24 +173,22 @@ module SupportDesk
|
|
|
166
173
|
node = resolve_topic!(topic, about, desk, requester, about)
|
|
167
174
|
|
|
168
175
|
cardinality = cardinality_key_for(requester: requester, subject: about, topic: node)
|
|
169
|
-
existing =
|
|
176
|
+
existing = existing_for(requester: requester, desk: desk, subject: about, topic: node)
|
|
170
177
|
return post_opening_message(existing, message, files) if existing
|
|
171
178
|
|
|
172
179
|
enforce_rate_limit!(requester, desk)
|
|
173
180
|
enforce_open_ticket_cap!(requester, desk)
|
|
174
181
|
|
|
175
|
-
ticket
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
SupportDesk.emit_after_commit(:ticket_opened, ticket) if inserted
|
|
186
|
-
ticket
|
|
182
|
+
ticket = transaction do
|
|
183
|
+
created, inserted = insert_ticket!(
|
|
184
|
+
requester: requester, desk: desk, node: node, about: about, via: via,
|
|
185
|
+
requester_role: requester_role, title: title, metadata: metadata, cardinality_key: cardinality
|
|
186
|
+
)
|
|
187
|
+
post_opening_message(created, message, files)
|
|
188
|
+
SupportDesk.emit_after_commit(:ticket_opened, created) if inserted
|
|
189
|
+
created
|
|
190
|
+
end
|
|
191
|
+
ticket.reload
|
|
187
192
|
end
|
|
188
193
|
|
|
189
194
|
# A human-friendly, unguessable-enough reference: "T-AB12CD".
|
|
@@ -218,6 +223,16 @@ module SupportDesk
|
|
|
218
223
|
end
|
|
219
224
|
end
|
|
220
225
|
|
|
226
|
+
# New submissions reuse an open case, including a reopened history.
|
|
227
|
+
# Reopening itself never merges or discards a different conversation.
|
|
228
|
+
def existing_for(requester:, desk:, subject:, topic:)
|
|
229
|
+
return if subject && !subject.class.one_open_support_ticket?
|
|
230
|
+
|
|
231
|
+
scope = not_closed.where(requester: requester, desk: desk, subject: subject)
|
|
232
|
+
scope = scope.where(topic: topic.to_s) unless subject
|
|
233
|
+
scope.newest_first.first
|
|
234
|
+
end
|
|
235
|
+
|
|
221
236
|
private
|
|
222
237
|
|
|
223
238
|
# "on this desk, and waiting longer than its own threshold" — with an
|
|
@@ -325,7 +340,9 @@ module SupportDesk
|
|
|
325
340
|
attempts = 0
|
|
326
341
|
begin
|
|
327
342
|
attempts += 1
|
|
328
|
-
|
|
343
|
+
# A unique-index loser must roll back a savepoint before querying
|
|
344
|
+
# the winner; PostgreSQL forbids reads in an aborted transaction.
|
|
345
|
+
ticket, opened = transaction(requires_new: true) do
|
|
329
346
|
ticket = create!(
|
|
330
347
|
desk: desk, requester: requester, requester_role: requester_role, subject: about,
|
|
331
348
|
topic: node, title: title.presence, reference: unique_reference, status: "open",
|
|
@@ -491,14 +508,14 @@ module SupportDesk
|
|
|
491
508
|
def reply!(body = nil, by: nil, files: [], request: nil)
|
|
492
509
|
actor = resolve_actor(by)
|
|
493
510
|
ensure_agent!(actor)
|
|
494
|
-
ensure_writable!
|
|
495
511
|
|
|
496
512
|
# One transaction, because taking the ticket and announcing it are
|
|
497
513
|
# part of answering: a reply that raises (an empty body, a locked
|
|
498
514
|
# conversation, a rate limit) must not leave the agent holding a case
|
|
499
515
|
# they never answered, or "Lucía se ocupa de tu consulta" sitting in
|
|
500
516
|
# the requester's thread with no reply under it.
|
|
501
|
-
|
|
517
|
+
with_lock do
|
|
518
|
+
ensure_writable!
|
|
502
519
|
apply_reply_policy!(actor, request: request)
|
|
503
520
|
desk.message!(conversation, body, files: files, author: actor)
|
|
504
521
|
end
|
|
@@ -521,13 +538,14 @@ module SupportDesk
|
|
|
521
538
|
# handed it. Repeating an assignment to the current holder does nothing.
|
|
522
539
|
def assign!(to:, by: nil, reason: nil, note: nil, request: nil)
|
|
523
540
|
actor = resolve_actor(by)
|
|
541
|
+
ensure_agent!(actor)
|
|
524
542
|
ensure_assignable!(to)
|
|
525
|
-
raise InvalidTransition, "can't assign a closed ticket — reopen it first" if closed?
|
|
526
543
|
|
|
527
544
|
reason ||= to == actor ? :taken : :assigned
|
|
528
545
|
assignment = nil
|
|
529
546
|
|
|
530
547
|
event = write_transition!(:assigned, actor: actor, request: request) do
|
|
548
|
+
raise InvalidTransition, "can't assign a closed ticket — reopen it first" if closed?
|
|
531
549
|
next false if assigned_to?(to)
|
|
532
550
|
|
|
533
551
|
assignment = Assignment.open!(ticket: self, agent: to, by: actor, reason: reason, note: note)
|
|
@@ -545,18 +563,19 @@ module SupportDesk
|
|
|
545
563
|
# whoever picks it up. Hand-off notes are always internal.
|
|
546
564
|
def hand_off!(to:, note: nil, by: nil, request: nil)
|
|
547
565
|
actor = resolve_actor(by)
|
|
548
|
-
|
|
549
|
-
raise NotTheAssignee, "#{describe_actor(actor)} doesn't hold ticket #{reference} " \
|
|
550
|
-
"(#{assignee ? describe_actor(assignee) : "nobody"} does) — use assign! to override"
|
|
551
|
-
end
|
|
566
|
+
ensure_agent!(actor)
|
|
552
567
|
ensure_assignable!(to)
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
from = assignee
|
|
568
|
+
from = nil
|
|
556
569
|
assignment = nil
|
|
557
570
|
event = write_transition!(:handed_off, actor: actor, request: request) do
|
|
571
|
+
unless assigned_to?(actor)
|
|
572
|
+
raise NotTheAssignee, "#{describe_actor(actor)} doesn't hold ticket #{reference} " \
|
|
573
|
+
"(#{assignee ? describe_actor(assignee) : "nobody"} does) — use assign! to override"
|
|
574
|
+
end
|
|
575
|
+
raise InvalidTransition, "can't hand off a closed ticket" if closed?
|
|
558
576
|
next false if assigned_to?(to)
|
|
559
577
|
|
|
578
|
+
from = assignee
|
|
560
579
|
assignment = Assignment.open!(ticket: self, agent: to, by: actor, reason: :handed_off, note: note,
|
|
561
580
|
release_reason: :handed_off)
|
|
562
581
|
update!(assignee: to)
|
|
@@ -573,12 +592,12 @@ module SupportDesk
|
|
|
573
592
|
def release!(by: nil, reason: :released, request: nil)
|
|
574
593
|
actor = resolve_actor(by)
|
|
575
594
|
ensure_agent!(actor)
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
from = assignee
|
|
595
|
+
from = nil
|
|
579
596
|
event = write_transition!(:released, actor: actor, request: request) do
|
|
597
|
+
raise InvalidTransition, "can't release a closed ticket" if closed?
|
|
580
598
|
next false if unassigned?
|
|
581
599
|
|
|
600
|
+
from = assignee
|
|
582
601
|
assignments.open.each { |assignment| assignment.release!(reason: reason) }
|
|
583
602
|
update!(assignee: nil)
|
|
584
603
|
{ "from" => SupportDesk.actor_key(from), "reason" => reason.to_s }
|
|
@@ -625,7 +644,8 @@ module SupportDesk
|
|
|
625
644
|
# one update! hash reads the closed ticket's "none" and stores nil —
|
|
626
645
|
# a reopened case that no SLA scope can see.
|
|
627
646
|
assign_attributes(status: "open", closed_at: nil, closed_by: nil,
|
|
628
|
-
reopen_count: reopen_count.to_i + 1, awaiting: awaiting_from_clocks
|
|
647
|
+
reopen_count: reopen_count.to_i + 1, awaiting: awaiting_from_clocks,
|
|
648
|
+
cardinality_key: "reopened:#{id}")
|
|
629
649
|
self.waiting_since = waiting_since_from_clocks
|
|
630
650
|
save!
|
|
631
651
|
restore_assignment!(by: actor)
|
|
@@ -711,11 +731,10 @@ module SupportDesk
|
|
|
711
731
|
attributes = { last_registered_message_id: message.id }
|
|
712
732
|
case role
|
|
713
733
|
when :requester
|
|
714
|
-
attributes[:awaiting] = "agent"
|
|
715
734
|
attributes[:last_requester_message_at] = message.created_at
|
|
716
|
-
if closed? && desk_config.closed_tickets == :reopen_on_reply
|
|
735
|
+
if closed? && message.created_at > closed_at && desk_config.closed_tickets == :reopen_on_reply
|
|
717
736
|
attributes.merge!(status: "open", closed_at: nil, closed_by: nil,
|
|
718
|
-
reopen_count: reopen_count.to_i + 1)
|
|
737
|
+
reopen_count: reopen_count.to_i + 1, cardinality_key: "reopened:#{id}")
|
|
719
738
|
reopened = true
|
|
720
739
|
end
|
|
721
740
|
when :agent
|
|
@@ -724,10 +743,10 @@ module SupportDesk
|
|
|
724
743
|
# A closed case owes nobody anything. An agent adding one last
|
|
725
744
|
# word keeps the clocks honest without putting the case back in a
|
|
726
745
|
# queue that `close!` just took it out of.
|
|
727
|
-
attributes[:awaiting] = "requester" unless closed?
|
|
728
746
|
end
|
|
729
747
|
|
|
730
748
|
assign_attributes(attributes)
|
|
749
|
+
self.awaiting = closed? ? "none" : awaiting_from_clocks
|
|
731
750
|
self.waiting_since = waiting_since_from_clocks
|
|
732
751
|
save!
|
|
733
752
|
|
|
@@ -825,7 +844,7 @@ module SupportDesk
|
|
|
825
844
|
I18n.t("support_desk.notifications.title", desk: desk.name)
|
|
826
845
|
end
|
|
827
846
|
|
|
828
|
-
#
|
|
847
|
+
# Case detail for an authenticated feed. Do not use for push previews.
|
|
829
848
|
def notification_body = label
|
|
830
849
|
|
|
831
850
|
# A GDPR-friendly dump of the case as the requester experienced it:
|
data/lib/support_desk/topic.rb
CHANGED
|
@@ -192,9 +192,12 @@ module SupportDesk
|
|
|
192
192
|
|
|
193
193
|
# The desk key tickets under this topic belong to.
|
|
194
194
|
def desk_key
|
|
195
|
-
|
|
195
|
+
desk_override || :default
|
|
196
196
|
end
|
|
197
197
|
|
|
198
|
+
# nil means inherit the wizard's desk, not route to the default desk.
|
|
199
|
+
def desk_override = inherited_or_own(:desk)&.to_sym
|
|
200
|
+
|
|
198
201
|
# Hidden from the wizard; the label still resolves for historic tickets.
|
|
199
202
|
def retired? = !!inherited_or_own(:retired)
|
|
200
203
|
|
|
@@ -257,34 +260,10 @@ module SupportDesk
|
|
|
257
260
|
def inspect = "#<SupportDesk::Topic::Unknown #{path}>"
|
|
258
261
|
end
|
|
259
262
|
|
|
260
|
-
#
|
|
261
|
-
#
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
class Type < ActiveModel::Type::Value
|
|
265
|
-
def type = :string
|
|
266
|
-
|
|
267
|
-
def cast(value)
|
|
268
|
-
case value
|
|
269
|
-
when nil then nil
|
|
270
|
-
when Topic then value
|
|
271
|
-
else SupportDesk.find_topic(value.to_s)
|
|
272
|
-
end
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def serialize(value)
|
|
276
|
-
case value
|
|
277
|
-
when nil then nil
|
|
278
|
-
when Topic then value.path
|
|
279
|
-
else value.to_s
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
def deserialize(value) = cast(value)
|
|
284
|
-
|
|
285
|
-
def changed_in_place?(raw_old_value, new_value)
|
|
286
|
-
raw_old_value != serialize(new_value)
|
|
287
|
-
end
|
|
263
|
+
# Casting must not consult a global topic tree: the same path may mean
|
|
264
|
+
# different things on two desks. Ticket#topic resolves the stored path.
|
|
265
|
+
class Type < ActiveModel::Type::String
|
|
266
|
+
def cast(value) = value&.to_s
|
|
288
267
|
end
|
|
289
268
|
end
|
|
290
269
|
end
|
data/lib/support_desk/version.rb
CHANGED
data/lib/support_desk/wizard.rb
CHANGED
|
@@ -84,7 +84,7 @@ module SupportDesk
|
|
|
84
84
|
# requester's desk, and a #desk that depended on the topic would ask the
|
|
85
85
|
# tree for the topic to find out which tree to ask.
|
|
86
86
|
def target_desk
|
|
87
|
-
key = topic&.
|
|
87
|
+
key = topic&.desk_override
|
|
88
88
|
return desk if key.nil? || key == desk.key
|
|
89
89
|
|
|
90
90
|
SupportDesk.desk(key) || desk
|
|
@@ -186,7 +186,7 @@ module SupportDesk
|
|
|
186
186
|
end
|
|
187
187
|
|
|
188
188
|
# The promise as a Duration, for hosts that want to phrase it themselves.
|
|
189
|
-
def promise_within =
|
|
189
|
+
def promise_within = target_desk.config.reply_within
|
|
190
190
|
|
|
191
191
|
# "1 day", "4 horas" — through ActionView's date helper so it speaks the
|
|
192
192
|
# requester's language, falling back to Duration#inspect in the (rare)
|
|
@@ -207,10 +207,7 @@ module SupportDesk
|
|
|
207
207
|
def existing_ticket
|
|
208
208
|
return nil if topic.nil? || topic.branch?
|
|
209
209
|
|
|
210
|
-
|
|
211
|
-
return nil if key.start_with?("free:")
|
|
212
|
-
|
|
213
|
-
Ticket.not_closed.find_by(requester: requester, desk: target_desk, cardinality_key: key)
|
|
210
|
+
Ticket.existing_for(requester: requester, desk: target_desk, subject: subject, topic: topic)
|
|
214
211
|
end
|
|
215
212
|
|
|
216
213
|
# Which records in +choices+ the requester already has an open case
|
|
@@ -270,15 +267,9 @@ module SupportDesk
|
|
|
270
267
|
raise InvalidTransition, "the wizard is still on the #{step} step — pick one before submitting"
|
|
271
268
|
end
|
|
272
269
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
requester.ask_support!(message, about: subject, topic: topic.path, files: files)
|
|
277
|
-
else
|
|
278
|
-
Ticket.open!(requester: requester, message: message, about: subject, topic: topic.path,
|
|
279
|
-
files: files, desk: target_desk,
|
|
280
|
-
requester_role: requester.class.support_desk_requester_options[:as])
|
|
281
|
-
end
|
|
270
|
+
Ticket.open!(requester: requester, message: message, about: subject, topic: topic.path,
|
|
271
|
+
files: files, desk: target_desk,
|
|
272
|
+
requester_role: requester.class.support_desk_requester_options[:as])
|
|
282
273
|
end
|
|
283
274
|
|
|
284
275
|
# A signed token for the currently chosen subject, to round-trip through
|
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.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-09-
|
|
10
|
+
date: 2026-09-17 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activerecord
|
|
@@ -56,6 +56,9 @@ dependencies:
|
|
|
56
56
|
- - "~>"
|
|
57
57
|
- !ruby/object:Gem::Version
|
|
58
58
|
version: '0.3'
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: 0.3.2
|
|
59
62
|
type: :runtime
|
|
60
63
|
prerelease: false
|
|
61
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -63,6 +66,9 @@ dependencies:
|
|
|
63
66
|
- - "~>"
|
|
64
67
|
- !ruby/object:Gem::Version
|
|
65
68
|
version: '0.3'
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: 0.3.2
|
|
66
72
|
- !ruby/object:Gem::Dependency
|
|
67
73
|
name: globalid
|
|
68
74
|
requirement: !ruby/object:Gem::Requirement
|