spree_cm_commissioner 2.9.4 → 2.9.5
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/Gemfile.lock +1 -1
- data/app/controllers/concerns/spree_cm_commissioner/turnstile_protectable.rb +9 -7
- data/app/controllers/spree/api/v2/operator/event_charts_controller.rb +1 -0
- data/app/controllers/spree/api/v2/operator/guest_json_gzips_controller.rb +14 -4
- data/app/controllers/spree/api/v2/storefront/account_checker_controller.rb +4 -0
- data/app/controllers/spree/api/v2/tenant/account/orders_controller.rb +1 -1
- data/app/controllers/spree/api/v2/tenant/my_votes_controller.rb +51 -0
- data/app/controllers/spree/api/v2/tenant/show_people_controller.rb +1 -4
- data/app/controllers/spree/api/v2/tenant/show_person_assignments_controller.rb +4 -1
- data/app/controllers/spree/api/v2/tenant/voting_contestants_controller.rb +6 -2
- data/app/interactors/spree_cm_commissioner/host_matcher.rb +6 -5
- data/app/interactors/spree_cm_commissioner/turnstile_token_validator.rb +43 -13
- data/app/models/concerns/spree_cm_commissioner/order_qr_codeable.rb +39 -0
- data/app/models/concerns/spree_cm_commissioner/order_scopes.rb +5 -0
- data/app/models/spree_cm_commissioner/check_in_session.rb +36 -0
- data/app/models/spree_cm_commissioner/exports/operator_guest_json_gzip.rb +26 -1
- data/app/models/spree_cm_commissioner/guest.rb +4 -0
- data/app/models/spree_cm_commissioner/maintenance_tasks/event.rb +9 -1
- data/app/models/spree_cm_commissioner/order_decorator.rb +2 -37
- data/app/models/spree_cm_commissioner/role_decorator.rb +41 -4
- data/app/models/spree_cm_commissioner/role_user_decorator.rb +11 -0
- data/app/models/spree_cm_commissioner/show_person.rb +5 -1
- data/app/models/spree_cm_commissioner/show_person_assignment.rb +1 -1
- data/app/models/spree_cm_commissioner/taxon_decorator.rb +2 -0
- data/app/models/spree_cm_commissioner/tenant.rb +17 -6
- data/app/models/spree_cm_commissioner/user_decorator.rb +32 -0
- data/app/models/spree_cm_commissioner/voting_contestant.rb +2 -0
- data/app/queries/spree_cm_commissioner/event_chart_queries.rb +23 -7
- data/app/queries/spree_cm_commissioner/guest_searcher_query.rb +16 -3
- data/app/queries/spree_cm_commissioner/pie_chart_event_aggregator_queries.rb +1 -0
- data/app/queries/spree_cm_commissioner/user_votes_per_contestant_query.rb +48 -0
- data/app/serializers/spree/v2/tenant/my_vote_serializer.rb +19 -0
- data/app/serializers/spree/v2/tenant/show_serializer.rb +2 -1
- data/app/serializers/spree/v2/tenant/voting_contestant_serializer.rb +2 -0
- data/app/services/spree_cm_commissioner/check_ins/create_bulk.rb +10 -6
- data/app/services/spree_cm_commissioner/imports/contestants/create.rb +19 -1
- data/app/services/spree_cm_commissioner/operator_guest_json_gzips/create.rb +14 -7
- data/app/services/spree_cm_commissioner/organizer/export_guest_csv_service.rb +12 -11
- data/app/services/spree_cm_commissioner/show_contestants/normalize_video_highlights.rb +5 -1
- data/app/views/spree/admin/tenants/_form.html.erb +13 -1
- data/config/locales/en.yml +12 -7
- data/config/locales/km.yml +12 -4
- data/config/routes.rb +1 -0
- data/db/migrate/20260720000001_add_check_in_fields_to_spree_roles.rb +11 -0
- data/db/migrate/20260723000000_add_payment_host_to_cm_tenants.rb +6 -0
- data/db/migrate/20260723000001_add_metadata_to_spree_roles.rb +11 -0
- data/db/migrate/20260728074153_add_role_to_cm_exports.rb +5 -0
- data/lib/spree_cm_commissioner/test_helper/factories/vehicle_factory.rb +1 -1
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +10 -2
|
@@ -5,7 +5,11 @@ module SpreeCmCommissioner
|
|
|
5
5
|
has_many :show_person_images, -> { order(:position) }, as: :viewable, class_name: 'SpreeCmCommissioner::ShowPersonImage', dependent: :destroy
|
|
6
6
|
has_many :assignments, class_name: 'SpreeCmCommissioner::ShowPersonAssignment', dependent: :destroy
|
|
7
7
|
has_many :shows, through: :assignments, source: :show
|
|
8
|
-
has_many :assigned_contestants,
|
|
8
|
+
has_many :assigned_contestants,
|
|
9
|
+
-> { active },
|
|
10
|
+
class_name: 'SpreeCmCommissioner::ShowContestant',
|
|
11
|
+
foreign_key: :assigned_show_person_id,
|
|
12
|
+
dependent: :nullify
|
|
9
13
|
|
|
10
14
|
validates :name, presence: true
|
|
11
15
|
|
|
@@ -12,7 +12,7 @@ module SpreeCmCommissioner
|
|
|
12
12
|
validates :role, presence: true
|
|
13
13
|
|
|
14
14
|
has_many :assigned_contestants,
|
|
15
|
-
-> (assignment) { where(show_id: assignment.show_id) },
|
|
15
|
+
-> (assignment) { where(show_id: assignment.show_id).active },
|
|
16
16
|
class_name: 'SpreeCmCommissioner::ShowContestant',
|
|
17
17
|
primary_key: :show_person_id,
|
|
18
18
|
foreign_key: :assigned_show_person_id,
|
|
@@ -18,6 +18,8 @@ module SpreeCmCommissioner
|
|
|
18
18
|
base.has_many :products, through: :classifications, class_name: 'Spree::Product'
|
|
19
19
|
base.has_many :guests, foreign_key: :event_id, class_name: 'SpreeCmCommissioner::Guest', dependent: :nullify
|
|
20
20
|
base.has_many :check_ins, as: :checkinable, class_name: 'SpreeCmCommissioner::CheckIn', dependent: :nullify
|
|
21
|
+
# Check-in groups for this event (Spree::Role, role_type :check_in). Scoped by event_id (indexed),
|
|
22
|
+
base.has_many :check_in_groups, -> { filter_check_in }, class_name: 'Spree::Role', foreign_key: :event_id
|
|
21
23
|
base.has_many :customer_taxons, class_name: 'SpreeCmCommissioner::CustomerTaxon'
|
|
22
24
|
|
|
23
25
|
base.has_many :seat_layouts, as: :layoutable, class_name: 'SpreeCmCommissioner::SeatLayout', dependent: :destroy
|
|
@@ -13,10 +13,13 @@ module SpreeCmCommissioner
|
|
|
13
13
|
through: :vendors,
|
|
14
14
|
source: :payment_methods
|
|
15
15
|
|
|
16
|
-
validates :host, presence: true,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
validates :host, presence: true,
|
|
17
|
+
uniqueness: true,
|
|
18
|
+
format: { without: %r{\Ahttps?://}, message: :format }
|
|
19
|
+
|
|
20
|
+
validates :payment_host, uniqueness: true,
|
|
21
|
+
allow_blank: true,
|
|
22
|
+
format: { without: %r{\Ahttps?://}, message: :format }
|
|
20
23
|
|
|
21
24
|
validates :sms_sender_id, length: { maximum: 11 }, allow_blank: true
|
|
22
25
|
|
|
@@ -35,13 +38,21 @@ module SpreeCmCommissioner
|
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
def formatted_url
|
|
38
|
-
|
|
41
|
+
format_host(host)
|
|
42
|
+
end
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
def payment_url
|
|
45
|
+
format_host(payment_host)
|
|
41
46
|
end
|
|
42
47
|
|
|
43
48
|
private
|
|
44
49
|
|
|
50
|
+
def format_host(host)
|
|
51
|
+
return if host.blank?
|
|
52
|
+
|
|
53
|
+
Rails.env.development? || Rails.env.test? ? "http://#{host}" : "https://#{host}"
|
|
54
|
+
end
|
|
55
|
+
|
|
45
56
|
def generate_slug
|
|
46
57
|
self.slug = name.parameterize if slug.blank?
|
|
47
58
|
end
|
|
@@ -154,6 +154,38 @@ module SpreeCmCommissioner
|
|
|
154
154
|
has_spree_role?('operator')
|
|
155
155
|
end
|
|
156
156
|
|
|
157
|
+
# Check-in Group: this crew's check-in group (Spree::Role, role_type :check_in) for the event.
|
|
158
|
+
# One role per user per event — memoized per event so a bulk check-in looks it up once, not per
|
|
159
|
+
# guest. `nil` = no group (can check in any product). Uses `key?` (not `||=`) so the common "no
|
|
160
|
+
# group" result — itself `nil` — is cached too, instead of re-querying on every call.
|
|
161
|
+
def check_in_group_for(event_id)
|
|
162
|
+
@check_in_group_cache ||= {}
|
|
163
|
+
return @check_in_group_cache[event_id] if @check_in_group_cache.key?(event_id)
|
|
164
|
+
|
|
165
|
+
@check_in_group_cache[event_id] = spree_roles.find_by(role_type: :check_in, event_id: event_id)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Whether this user may check in the given guest. Admins and organizers are never restricted by a
|
|
169
|
+
# check-in group; otherwise the crew may check in the guest's ticket if their check-in group allows
|
|
170
|
+
# it (include/exclude evaluated on that one group).
|
|
171
|
+
def can_check_in_guest?(guest)
|
|
172
|
+
return true if admin? || organizer?
|
|
173
|
+
|
|
174
|
+
group = check_in_group_for(guest.event_id)
|
|
175
|
+
return true if group.nil?
|
|
176
|
+
|
|
177
|
+
group.check_in_allows?(guest.line_item)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# This user's check-in role for the event, used to scope the operator offline export (one role, one
|
|
181
|
+
# export — see Exports::OperatorGuestJsonGzip#role). `nil` = unrestricted (admins/organizers, or
|
|
182
|
+
# crew in no group).
|
|
183
|
+
def check_in_role_for(event)
|
|
184
|
+
return nil if admin? || organizer?
|
|
185
|
+
|
|
186
|
+
check_in_group_for(event.id)
|
|
187
|
+
end
|
|
188
|
+
|
|
157
189
|
def vendor_organizer?(vendor)
|
|
158
190
|
spree_roles.any? do |role|
|
|
159
191
|
role.name == "#{vendor.id}-organizer"
|
|
@@ -41,6 +41,8 @@ module SpreeCmCommissioner
|
|
|
41
41
|
validate :advanced_to_and_advanced_from_must_differ
|
|
42
42
|
|
|
43
43
|
delegate :name, :contestant_number, :category, :gender, :eliminated_at, to: :show_contestant, allow_nil: true
|
|
44
|
+
delegate :assigned_show_person, to: :show_contestant, allow_nil: true
|
|
45
|
+
delegate :name, to: :assigned_show_person, prefix: true, allow_nil: true
|
|
44
46
|
delegate :can_vote?, to: :voting_session, allow_nil: true
|
|
45
47
|
|
|
46
48
|
def special_rule_type=(value)
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
class EventChartQueries
|
|
3
|
-
attr_reader :taxon_id, :chart_type, :refreshed
|
|
3
|
+
attr_reader :taxon_id, :chart_type, :check_in_session_id, :refreshed
|
|
4
4
|
|
|
5
5
|
# chart_type: 'participation | gender | entry_type' taxon_id: taxon_id, refreshed: false
|
|
6
|
-
|
|
6
|
+
# check_in_session_id: scopes check-in counts to a single session. When nil, a guest
|
|
7
|
+
# is counted once regardless of how many sessions they checked into.
|
|
8
|
+
def initialize(taxon_id:, chart_type:, check_in_session_id: nil, refreshed: false)
|
|
7
9
|
@taxon_id = taxon_id
|
|
8
10
|
@chart_type = chart_type || 'participation'
|
|
11
|
+
@check_in_session_id = check_in_session_id.presence
|
|
9
12
|
@refreshed = refreshed
|
|
10
13
|
end
|
|
11
14
|
|
|
12
15
|
def call
|
|
13
|
-
cache_key =
|
|
16
|
+
cache_key = [chart_type, taxon_id, check_in_session_id].compact.join('-')
|
|
14
17
|
Rails.cache.delete(cache_key) if refreshed
|
|
15
18
|
|
|
16
19
|
product_charts = Rails.cache.fetch(cache_key, expires_in: 30.minutes) do
|
|
@@ -38,10 +41,10 @@ module SpreeCmCommissioner
|
|
|
38
41
|
|
|
39
42
|
def base_joins
|
|
40
43
|
query = SpreeCmCommissioner::Guest
|
|
41
|
-
join_type = chart_type.in?(%w[participation
|
|
44
|
+
join_type = chart_type.in?(%w[participation guest_occupation guest_nationality]) ? 'LEFT' : 'INNER'
|
|
42
45
|
|
|
43
46
|
query
|
|
44
|
-
.joins("#{join_type} JOIN cm_check_ins ON
|
|
47
|
+
.joins("#{join_type} JOIN cm_check_ins ON #{check_in_join_condition}")
|
|
45
48
|
.joins('INNER JOIN spree_line_items ON spree_line_items.id = cm_guests.line_item_id')
|
|
46
49
|
.joins('INNER JOIN spree_variants ON spree_variants.id = spree_line_items.variant_id')
|
|
47
50
|
.joins('INNER JOIN spree_orders ON spree_orders.id = spree_line_items.order_id')
|
|
@@ -49,6 +52,19 @@ module SpreeCmCommissioner
|
|
|
49
52
|
.joins('INNER JOIN spree_products_taxons ON spree_products.id = spree_products_taxons.product_id')
|
|
50
53
|
.joins('INNER JOIN spree_taxons ON spree_taxons.id = spree_products_taxons.taxon_id')
|
|
51
54
|
.where(spree_orders: { state: 'complete' }, spree_taxons: { parent_id: taxon_id })
|
|
55
|
+
.where.not(cm_guests: { state: :transferred })
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Filter check-ins to a single session inside the JOIN's ON clause (not WHERE), so
|
|
59
|
+
# LEFT JOINs still keep no-show guests. When no session is given, every check-in row
|
|
60
|
+
# is joined and the DISTINCT-guest counts below collapse multiple sessions into one.
|
|
61
|
+
def check_in_join_condition
|
|
62
|
+
condition = 'cm_check_ins.guest_id = cm_guests.id'
|
|
63
|
+
return condition if check_in_session_id.blank?
|
|
64
|
+
|
|
65
|
+
ActiveRecord::Base.sanitize_sql_array(
|
|
66
|
+
["#{condition} AND cm_check_ins.check_in_session_id = ?", check_in_session_id]
|
|
67
|
+
)
|
|
52
68
|
end
|
|
53
69
|
|
|
54
70
|
def participation_event_chart
|
|
@@ -117,7 +133,7 @@ module SpreeCmCommissioner
|
|
|
117
133
|
spree_products.name AS product_name,
|
|
118
134
|
occupation_taxons.name AS name,
|
|
119
135
|
COUNT(DISTINCT cm_guests.id) AS total,
|
|
120
|
-
COUNT(DISTINCT cm_check_ins.
|
|
136
|
+
COUNT(DISTINCT cm_check_ins.guest_id) AS show'
|
|
121
137
|
)
|
|
122
138
|
.group('spree_products.id, spree_products.name, occupation_taxons.name')
|
|
123
139
|
.group_by(&:product_id)
|
|
@@ -136,7 +152,7 @@ module SpreeCmCommissioner
|
|
|
136
152
|
spree_products.name AS product_name,
|
|
137
153
|
nationality_taxons.name AS name,
|
|
138
154
|
COUNT(DISTINCT cm_guests.id) AS total,
|
|
139
|
-
COUNT(DISTINCT cm_check_ins.
|
|
155
|
+
COUNT(DISTINCT cm_check_ins.guest_id) AS show'
|
|
140
156
|
)
|
|
141
157
|
.group('spree_products.id, spree_products.name, nationality_taxons.name')
|
|
142
158
|
.group_by(&:product_id)
|
|
@@ -7,10 +7,25 @@ module SpreeCmCommissioner
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def event_id = params[:event_id]
|
|
10
|
+
def product_ids = params[:product_ids]
|
|
10
11
|
|
|
11
|
-
def call
|
|
12
|
+
def call
|
|
12
13
|
return SpreeCmCommissioner::Guest.none if event_id.blank?
|
|
13
14
|
|
|
15
|
+
# Optional check-in-group scope (see Role#check_in_scoped_product_ids), set by the operator export
|
|
16
|
+
# and the dashboard preview. nil = unrestricted; an array narrows to guests whose ticket's product
|
|
17
|
+
# is in it — an EMPTY array is a real "no tickets" scope and correctly yields no guests.
|
|
18
|
+
if product_ids.nil?
|
|
19
|
+
query
|
|
20
|
+
else
|
|
21
|
+
query.joins(line_item: :variant).where(spree_variants: { product_id: product_ids })
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# The base guest search: QR (order / line item / guest / user), term, ids, or the whole event.
|
|
28
|
+
def query # rubocop:disable Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity
|
|
14
29
|
if params[:qr_data].present? && order_qr_data?
|
|
15
30
|
search_by_order_qr
|
|
16
31
|
elsif params[:qr_data].present? && line_item_qr_data?
|
|
@@ -31,8 +46,6 @@ module SpreeCmCommissioner
|
|
|
31
46
|
end
|
|
32
47
|
end
|
|
33
48
|
|
|
34
|
-
private
|
|
35
|
-
|
|
36
49
|
def search_by_order_qr
|
|
37
50
|
order = Spree::Order.complete_or_canceled.search_by_qr_data!(params[:qr_data])
|
|
38
51
|
order.guests.where(event_id: event_id)
|
|
@@ -48,6 +48,7 @@ module SpreeCmCommissioner
|
|
|
48
48
|
.joins('INNER JOIN spree_products_taxons ON spree_products.id = spree_products_taxons.product_id')
|
|
49
49
|
.joins('INNER JOIN spree_taxons ON spree_taxons.id = spree_products_taxons.taxon_id')
|
|
50
50
|
.where(spree_orders: { state: 'complete' }, spree_taxons: { parent_id: taxon_id })
|
|
51
|
+
.where.not(cm_guests: { state: :transferred })
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
def participation_pie_chart
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Returns a voting_session's contestants, each carrying the current user's own
|
|
2
|
+
# vote tally for that contestant as a computed my_vote_count attribute.
|
|
3
|
+
#
|
|
4
|
+
# The DB (cm_votes) is the source of truth for per-user tallies — Redis only
|
|
5
|
+
# holds the global, non-user-scoped leaderboard totals, so it cannot answer
|
|
6
|
+
# "how many votes did THIS user give each contestant".
|
|
7
|
+
#
|
|
8
|
+
# Runs as two queries total, regardless of contestant count:
|
|
9
|
+
# 1. voting_contestants LEFT JOINed against a per-contestant SUM(quantity)
|
|
10
|
+
# subquery scoped to this user+session (no N+1, no per-record query)
|
|
11
|
+
# 2. a batched preload of show_contestant (needed for name/contestant_number)
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# SpreeCmCommissioner::UserVotesPerContestantQuery.new(
|
|
15
|
+
# user: current_user, voting_session: session
|
|
16
|
+
# ).contestants
|
|
17
|
+
module SpreeCmCommissioner
|
|
18
|
+
class UserVotesPerContestantQuery
|
|
19
|
+
attr_reader :user, :voting_session
|
|
20
|
+
|
|
21
|
+
def initialize(user:, voting_session:)
|
|
22
|
+
@user = user
|
|
23
|
+
@voting_session = voting_session
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def contestants
|
|
27
|
+
return SpreeCmCommissioner::VotingContestant.none if user.nil? || voting_session.nil?
|
|
28
|
+
|
|
29
|
+
voting_session.voting_contestants
|
|
30
|
+
.includes(:show_contestant)
|
|
31
|
+
.select('cm_voting_contestants.*, COALESCE(user_votes.total_quantity, 0) AS my_vote_count')
|
|
32
|
+
.joins(
|
|
33
|
+
"LEFT JOIN (#{user_votes_subquery.to_sql}) user_votes " \
|
|
34
|
+
'ON user_votes.contestant_id = cm_voting_contestants.id'
|
|
35
|
+
)
|
|
36
|
+
.order(:vote_number)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def user_votes_subquery
|
|
42
|
+
SpreeCmCommissioner::Vote
|
|
43
|
+
.select(:contestant_id, 'SUM(quantity) AS total_quantity')
|
|
44
|
+
.where(user_id: user.id, voting_session_id: voting_session.id)
|
|
45
|
+
.group(:contestant_id)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module V2
|
|
3
|
+
module Tenant
|
|
4
|
+
# Serializes a VotingContestant together with the current user's own vote
|
|
5
|
+
# tally (my_vote_count) for that contestant in a voting session.
|
|
6
|
+
#
|
|
7
|
+
# my_vote_count is a virtual attribute (see VotingContestant) that the
|
|
8
|
+
# controller sets on each record before serialization — see
|
|
9
|
+
# MyVotesController#collection.
|
|
10
|
+
class MyVoteSerializer < BaseSerializer
|
|
11
|
+
set_type :voting_contestant
|
|
12
|
+
|
|
13
|
+
attributes :name, :contestant_number, :vote_number, :my_vote_count
|
|
14
|
+
|
|
15
|
+
attribute :voting_session_id, &:voting_session_id
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -5,7 +5,8 @@ module Spree
|
|
|
5
5
|
set_type :show
|
|
6
6
|
|
|
7
7
|
attributes :slug, :description, :permalink, :from_date, :to_date, :preview, :created_at, :updated_at,
|
|
8
|
-
:free_vote_limit, :free_vote_limit_type, :show_type, :credit_votable_type
|
|
8
|
+
:free_vote_limit, :free_vote_limit_type, :show_type, :credit_votable_type,
|
|
9
|
+
:background_color, :foreground_color
|
|
9
10
|
|
|
10
11
|
attribute :name, &:display_name
|
|
11
12
|
|
|
@@ -10,6 +10,8 @@ module Spree
|
|
|
10
10
|
|
|
11
11
|
attribute :voting_session_id, &:voting_session_id
|
|
12
12
|
|
|
13
|
+
attribute :assigned_person_name, &:assigned_show_person_name
|
|
14
|
+
|
|
13
15
|
attribute :can_vote, &:can_vote?
|
|
14
16
|
|
|
15
17
|
belongs_to :voting_session, serializer: ::Spree::V2::Tenant::VotingSessionSerializer
|
|
@@ -19,12 +19,7 @@ module SpreeCmCommissioner
|
|
|
19
19
|
guest = indexed_guests[attrs[:guest_id].to_i]
|
|
20
20
|
raise ActiveRecord::RecordNotFound, 'Guest or related record not found' if guest.blank?
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
fail_check_in!(guest, 'Cannot check in Guest from cancelled order') if order.canceled?
|
|
24
|
-
|
|
25
|
-
# Validate guest state for check-in
|
|
26
|
-
fail_check_in!(guest, 'Guest is locked and cannot be checked in') if guest.locked?
|
|
27
|
-
fail_check_in!(guest, 'Guest has been transferred and cannot be checked in') if guest.transferred?
|
|
22
|
+
validate_guest_for_check_in!(guest, check_in_by)
|
|
28
23
|
|
|
29
24
|
check_in = check_in!(guest, attrs, check_in_by, confirmed_at)
|
|
30
25
|
update_guest!(guest, attrs[:guest_attributes]) if attrs[:guest_attributes].present?
|
|
@@ -48,6 +43,15 @@ module SpreeCmCommissioner
|
|
|
48
43
|
raise error
|
|
49
44
|
end
|
|
50
45
|
|
|
46
|
+
def validate_guest_for_check_in!(guest, check_in_by)
|
|
47
|
+
fail_check_in!(guest, I18n.t('spree.check_in.order_canceled')) if guest.line_item.order.canceled?
|
|
48
|
+
fail_check_in!(guest, I18n.t('spree.check_in.guest_locked')) if guest.locked?
|
|
49
|
+
fail_check_in!(guest, I18n.t('spree.check_in.guest_transferred')) if guest.transferred?
|
|
50
|
+
|
|
51
|
+
# Check-in Groups: crew may only check in the ticket types in their check-in groups for this event.
|
|
52
|
+
fail_check_in!(guest, I18n.t('spree.check_in.check_in_group_not_allowed')) if check_in_by && !check_in_by.can_check_in_guest?(guest)
|
|
53
|
+
end
|
|
54
|
+
|
|
51
55
|
def check_in!(guest, check_in_attributes, check_in_by, confirmed_at)
|
|
52
56
|
check_in = SpreeCmCommissioner::CheckIn.new(
|
|
53
57
|
guest: guest,
|
|
@@ -8,6 +8,7 @@ module SpreeCmCommissioner
|
|
|
8
8
|
IMPORTABLE_COLUMNS = %w[
|
|
9
9
|
contestant_number name gender category status origin age bio
|
|
10
10
|
facebook instagram twitter linkedin youtube tiktok telegram
|
|
11
|
+
video_highlights
|
|
11
12
|
].freeze
|
|
12
13
|
MAX_ROWS = 50
|
|
13
14
|
SOCIAL_LINK_COLUMNS = %w[facebook instagram twitter linkedin youtube tiktok telegram].freeze
|
|
@@ -71,7 +72,7 @@ module SpreeCmCommissioner
|
|
|
71
72
|
end
|
|
72
73
|
end
|
|
73
74
|
|
|
74
|
-
def extract_attrs(row)
|
|
75
|
+
def extract_attrs(row) # rubocop:disable Metrics/AbcSize
|
|
75
76
|
normalized = row.transform_keys { |k| k.to_s.strip.downcase.tr(' ', '_') }
|
|
76
77
|
|
|
77
78
|
social_links = SOCIAL_LINK_COLUMNS.each_with_object({}) do |key, hash|
|
|
@@ -91,9 +92,26 @@ module SpreeCmCommissioner
|
|
|
91
92
|
}.compact
|
|
92
93
|
|
|
93
94
|
attrs[:social_links] = social_links if social_links.any?
|
|
95
|
+
|
|
96
|
+
video_highlights = extract_video_highlights(normalized['video_highlights'])
|
|
97
|
+
attrs[:video_highlights] = video_highlights unless video_highlights.nil?
|
|
98
|
+
|
|
94
99
|
attrs
|
|
95
100
|
end
|
|
96
101
|
|
|
102
|
+
def extract_video_highlights(raw_value)
|
|
103
|
+
raw_value = raw_value.to_s.strip
|
|
104
|
+
return nil if raw_value.blank?
|
|
105
|
+
|
|
106
|
+
parsed = JSON.parse(raw_value)
|
|
107
|
+
raise "video_highlights must be a JSON array, got #{parsed.class}" unless parsed.is_a?(Array)
|
|
108
|
+
|
|
109
|
+
result = SpreeCmCommissioner::ShowContestants::NormalizeVideoHighlights.call(raw_video_highlights: parsed)
|
|
110
|
+
raise "invalid video_highlights: #{result.error.value}" unless result.success?
|
|
111
|
+
|
|
112
|
+
result.value[:video_highlights]
|
|
113
|
+
end
|
|
114
|
+
|
|
97
115
|
def resolve_enum(mapping, value)
|
|
98
116
|
return nil if value.blank?
|
|
99
117
|
|
|
@@ -5,13 +5,15 @@ module SpreeCmCommissioner
|
|
|
5
5
|
class Create
|
|
6
6
|
prepend ::Spree::ServiceModule::Base
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# `role`: nil = unrestricted (full event); a Spree::Role (check_in type) scopes the file to that
|
|
9
|
+
# role's tickets (see User#check_in_role_for). One role, one export.
|
|
10
|
+
def call(exportable:, role: nil, force_create: false, options: {})
|
|
9
11
|
unless force_create
|
|
10
|
-
recent_export = find_recent_export(exportable)
|
|
12
|
+
recent_export = find_recent_export(exportable, role)
|
|
11
13
|
return success(recent_export) if recent_export
|
|
12
14
|
end
|
|
13
15
|
|
|
14
|
-
export = build_export(exportable, options)
|
|
16
|
+
export = build_export(exportable, role, options)
|
|
15
17
|
if export.save
|
|
16
18
|
success(export)
|
|
17
19
|
else
|
|
@@ -21,24 +23,29 @@ module SpreeCmCommissioner
|
|
|
21
23
|
|
|
22
24
|
private
|
|
23
25
|
|
|
24
|
-
def find_recent_export(exportable)
|
|
26
|
+
def find_recent_export(exportable, role)
|
|
25
27
|
# Cache exports for 24 hours (configurable via OPERATOR_GUEST_JSON_GZIP_CACHE_HOURS).
|
|
26
|
-
# This ensures
|
|
28
|
+
# This ensures staff sharing the same scope see consistent guest data throughout the event day,
|
|
27
29
|
# minimizes server load, and prevents mid-event data refresh issues.
|
|
28
30
|
#
|
|
31
|
+
# The cache is keyed on exportable AND role_id so a scoped operator is never handed a broader —
|
|
32
|
+
# or another group's — cached file, while same-role operators still share one. `role_id: nil`
|
|
33
|
+
# correctly matches unrestricted exports.
|
|
34
|
+
#
|
|
29
35
|
# Use force_create=true to manually trigger fresh exports when needed.
|
|
30
36
|
cache_duration = ENV.fetch('OPERATOR_GUEST_JSON_GZIP_CACHE_HOURS', '24').to_i.hours
|
|
31
37
|
|
|
32
|
-
SpreeCmCommissioner::Exports::OperatorGuestJsonGzip.where(exportable: exportable)
|
|
38
|
+
SpreeCmCommissioner::Exports::OperatorGuestJsonGzip.where(exportable: exportable, role_id: role&.id)
|
|
33
39
|
.not_failed
|
|
34
40
|
.where('created_at > ?', cache_duration.ago)
|
|
35
41
|
.order(created_at: :desc)
|
|
36
42
|
.first
|
|
37
43
|
end
|
|
38
44
|
|
|
39
|
-
def build_export(exportable, options)
|
|
45
|
+
def build_export(exportable, role, options)
|
|
40
46
|
SpreeCmCommissioner::Exports::OperatorGuestJsonGzip.new(
|
|
41
47
|
exportable: exportable,
|
|
48
|
+
role: role,
|
|
42
49
|
**options
|
|
43
50
|
)
|
|
44
51
|
end
|
|
@@ -19,7 +19,7 @@ module SpreeCmCommissioner
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
21
|
def headers
|
|
22
|
-
columns.map { |col| col.titleize(keep_id_suffix: true) }.uniq
|
|
22
|
+
columns.map { |col| dynamic_fields_by_id[col]&.label || col.titleize(keep_id_suffix: true) }.uniq
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def build_row(guest)
|
|
@@ -41,15 +41,11 @@ module SpreeCmCommissioner
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def dynamic_field?(column)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
dynamic_field_labels.include?(column)
|
|
44
|
+
dynamic_fields_by_id.key?(column)
|
|
47
45
|
end
|
|
48
46
|
|
|
49
|
-
def
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@dynamic_field_labels ||= load_dynamic_field_labels
|
|
47
|
+
def dynamic_fields_by_id
|
|
48
|
+
@dynamic_fields_by_id ||= load_dynamic_fields.index_by { |field| field.id.to_s }
|
|
53
49
|
end
|
|
54
50
|
|
|
55
51
|
def dynamic_fields_exist?
|
|
@@ -58,12 +54,17 @@ module SpreeCmCommissioner
|
|
|
58
54
|
@has_dynamic_fields = event.event_products.joins(:product_dynamic_fields).exists?
|
|
59
55
|
end
|
|
60
56
|
|
|
61
|
-
def
|
|
62
|
-
|
|
57
|
+
def load_dynamic_fields
|
|
58
|
+
return [] unless dynamic_fields_exist?
|
|
59
|
+
|
|
60
|
+
SpreeCmCommissioner::DynamicField
|
|
61
|
+
.joins(:product_dynamic_fields)
|
|
62
|
+
.where(cm_product_dynamic_fields: { product_id: event.event_products.pluck(:id) })
|
|
63
|
+
.distinct
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
def fetch_dynamic_field_value(guest, column)
|
|
66
|
-
guest_dynamic_field = guest.guest_dynamic_fields.find { |gdf| gdf.
|
|
67
|
+
guest_dynamic_field = guest.guest_dynamic_fields.find { |gdf| gdf.dynamic_field_id.to_s == column }
|
|
67
68
|
return '' unless guest_dynamic_field
|
|
68
69
|
|
|
69
70
|
if guest_dynamic_field.dynamic_field_option.present?
|
|
@@ -31,7 +31,7 @@ module SpreeCmCommissioner
|
|
|
31
31
|
|
|
32
32
|
def normalize_video(payload)
|
|
33
33
|
payload_hash = payload.to_h
|
|
34
|
-
uri =
|
|
34
|
+
uri = extract_uri(payload_hash).to_s.strip
|
|
35
35
|
|
|
36
36
|
{
|
|
37
37
|
'title' => extract_value(payload_hash, 'title').to_s.strip,
|
|
@@ -44,6 +44,10 @@ module SpreeCmCommissioner
|
|
|
44
44
|
hash[key] || hash[key.to_sym]
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
def extract_uri(hash)
|
|
48
|
+
extract_value(hash, 'uri').presence || extract_value(hash, 'url')
|
|
49
|
+
end
|
|
50
|
+
|
|
47
51
|
def embed_youtube_url(uri)
|
|
48
52
|
result = UrlEmbed::YoutubeEmbed.call(url: uri)
|
|
49
53
|
result.success? ? result.value : uri
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
<% end %>
|
|
35
35
|
<% end %>
|
|
36
36
|
</div>
|
|
37
|
-
<div class="col-
|
|
37
|
+
<div class="col-6">
|
|
38
38
|
<%= f.field_container :host do %>
|
|
39
39
|
<%= f.label :host, raw(Spree.t(:host) + required_span_tag) %>
|
|
40
40
|
<%= f.text_field :host, required: true, class: 'form-control', placeholder: 'example.com' %>
|
|
@@ -43,6 +43,18 @@
|
|
|
43
43
|
<% end %>
|
|
44
44
|
<% end %>
|
|
45
45
|
</div>
|
|
46
|
+
<div class="col-6">
|
|
47
|
+
<%= f.field_container :payment_host do %>
|
|
48
|
+
<%= f.label :payment_host, raw(Spree.t(:payment_host)) %>
|
|
49
|
+
<span class="text-muted" style="font-size: 0.875rem; font-weight: normal; margin-left: 8px;">(eg. server.example.com)</span>
|
|
50
|
+
<span title="When set, this tenant's payment URLs (ABA callback + iframe pages) point directly at this host instead of routing through the tenant's client domain."
|
|
51
|
+
style="cursor: help; margin-left: 4px;">ⓘ</span>
|
|
52
|
+
<%= f.text_field :payment_host, class: 'form-control', placeholder: 'server.example.com' %>
|
|
53
|
+
<% if f.object.errors[:payment_host].any? %>
|
|
54
|
+
<div class="error text-danger"><%= f.object.errors[:payment_host].join(', ') %></div>
|
|
55
|
+
<% end %>
|
|
56
|
+
<% end %>
|
|
57
|
+
</div>
|
|
46
58
|
<div class="col-6">
|
|
47
59
|
<%= f.field_container :state do %>
|
|
48
60
|
<%= f.label :state, raw(Spree.t(:state)) %>
|
data/config/locales/en.yml
CHANGED
|
@@ -77,7 +77,11 @@ en:
|
|
|
77
77
|
action_not_matched: "Action is not matched"
|
|
78
78
|
|
|
79
79
|
turnstile:
|
|
80
|
+
token_missing: "Verification token is missing"
|
|
80
81
|
verification_failed: "Human verification failed. Please try again."
|
|
82
|
+
token_expired: "Verification expired. Please try again."
|
|
83
|
+
invalid_token: "Verification token is invalid. Please try again."
|
|
84
|
+
service_unavailable: "Verification service is temporarily unavailable. Please try again shortly."
|
|
81
85
|
|
|
82
86
|
taxon:
|
|
83
87
|
icon: Icon (Square)
|
|
@@ -244,6 +248,7 @@ en:
|
|
|
244
248
|
no_internal_note: "No note"
|
|
245
249
|
save_internal_note: "Save Note"
|
|
246
250
|
internal_note_updated: "Internal note updated"
|
|
251
|
+
payment_host: "Payment Host"
|
|
247
252
|
voting_contestant_advancer:
|
|
248
253
|
reversion_error: "Cannot eliminate here yet. This contestant appears in a later session."
|
|
249
254
|
is_open_dated: "Is Open Dated Trip?"
|
|
@@ -538,6 +543,12 @@ en:
|
|
|
538
543
|
already_checked_in: "Guest has already checked in for this session"
|
|
539
544
|
check_in_session_required: "Check-in session required"
|
|
540
545
|
check_in_session_is_not_allowed_or_invalid: "Check-in session is not allowed or invalid for this guest/ticket"
|
|
546
|
+
check_in_group_not_allowed: "This ticket is not in your check-in group"
|
|
547
|
+
order_canceled: "Cannot check in guest from a cancelled order"
|
|
548
|
+
guest_locked: "Guest is locked and cannot be checked in"
|
|
549
|
+
guest_transferred: "Guest has been transferred and cannot be checked in"
|
|
550
|
+
check_in_session:
|
|
551
|
+
end_time_after_start_time: "End time must be after start time"
|
|
541
552
|
load_test:
|
|
542
553
|
nav_label: "Load Testing"
|
|
543
554
|
back_to_load_testing: "Back to Load Testing"
|
|
@@ -671,9 +682,7 @@ en:
|
|
|
671
682
|
paid_votes_amount:
|
|
672
683
|
less_than_used: "cannot be less than paid votes used (%{used})"
|
|
673
684
|
spree_cm_commissioner/tenant:
|
|
674
|
-
|
|
675
|
-
host:
|
|
676
|
-
format: "must not include protocol (http:// or https://)"
|
|
685
|
+
format: "must not include protocol (http:// or https://)"
|
|
677
686
|
|
|
678
687
|
mail:
|
|
679
688
|
order_mailer:
|
|
@@ -907,10 +916,6 @@ en:
|
|
|
907
916
|
past_date: "Cannot redeem for a past date"
|
|
908
917
|
return_before_outbound: "Return date must be after the outbound trip date"
|
|
909
918
|
trip_not_available: "Trip is not available for redemption"
|
|
910
|
-
turnstile:
|
|
911
|
-
token_missing: "Verification token is missing"
|
|
912
|
-
verification_failed: "Human verification failed"
|
|
913
|
-
service_unavailable: "Verification service is temporarily unavailable"
|
|
914
919
|
spree_cm_commissioner:
|
|
915
920
|
draft_order:
|
|
916
921
|
line_items_missing: "Line items are missing"
|