spree_cm_commissioner 2.8.3.pre.pre12 → 2.8.3.pre.pre14
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/spree/admin/system/waiting_room_controller.rb +15 -0
- data/app/controllers/spree/api/v2/tenant/free_vote_claim_controller.rb +58 -0
- data/app/controllers/spree/api/v2/tenant/vote_packages_controller.rb +33 -0
- data/app/interactors/spree_cm_commissioner/waiting_guests_caller.rb +49 -7
- data/app/jobs/spree_cm_commissioner/waiting_room/publish_lobby_path_job.rb +17 -0
- data/app/models/spree_cm_commissioner/show.rb +10 -4
- data/app/models/spree_cm_commissioner/user_decorator.rb +12 -0
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +1 -0
- data/app/models/spree_cm_commissioner/voting_session.rb +4 -4
- data/app/serializers/spree/v2/tenant/free_vote_claim_serializer.rb +9 -0
- data/app/serializers/spree/v2/tenant/vote_package_serializer.rb +4 -1
- data/app/services/spree_cm_commissioner/voting_credits/claim_free_votes.rb +63 -66
- data/app/services/spree_cm_commissioner/waiting_room/publish_lobby_path.rb +48 -0
- data/app/services/spree_cm_commissioner/waiting_room_lobby_metadata_fetcher.rb +47 -0
- data/app/views/spree/admin/system/waiting_room/show.html.erb +53 -0
- data/config/routes.rb +3 -1
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +8 -3
- data/app/controllers/spree/api/v2/tenant/free_vote_claims_controller.rb +0 -37
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e14575f7ac52d9cfa8c454de179107ec31f080a9b8bdc2ff1bfdebeeb5a42f46
|
|
4
|
+
data.tar.gz: fde706a204a681e6c94931be9dfc915ed5090f3abd81d2481120d598b1e168dd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c69e06e635df7a5259a6fbcab7d1fba55b93eac89ed5c12592e0070baf1bfbaa3ad442754604fd7cffe435d069c340a0f6b427abf39049d5fa5855b5a68fc64f
|
|
7
|
+
data.tar.gz: 33bf29761d4ee458f559c067c5872db8e381c0f9de7805ce02efaa76bde07229e97a5c20f42ba81a77eb585820dc4360eadbcd035cb4be740364ff1d2d947440
|
data/Gemfile.lock
CHANGED
|
@@ -4,7 +4,10 @@ module Spree
|
|
|
4
4
|
class WaitingRoomController < Spree::Admin::BaseController
|
|
5
5
|
def show
|
|
6
6
|
@fetcher = SpreeCmCommissioner::WaitingRoomSystemMetadataFetcher.new
|
|
7
|
+
@lobby_fetcher = SpreeCmCommissioner::WaitingRoomLobbyMetadataFetcher.new
|
|
8
|
+
|
|
7
9
|
@fetcher.load_document_data
|
|
10
|
+
@lobby_fetcher.load_document_data
|
|
8
11
|
|
|
9
12
|
@active_sesions_count = SpreeCmCommissioner::WaitingRoomSession.active.count
|
|
10
13
|
end
|
|
@@ -29,6 +32,18 @@ module Spree
|
|
|
29
32
|
|
|
30
33
|
redirect_back fallback_location: admin_system_waiting_room_path
|
|
31
34
|
end
|
|
35
|
+
|
|
36
|
+
def publish_lobby_path
|
|
37
|
+
result = SpreeCmCommissioner::WaitingRoom::PublishLobbyPath.call
|
|
38
|
+
|
|
39
|
+
if result.success?
|
|
40
|
+
flash[:success] = "Published waiting guests records path: #{result.value[:records_path]}"
|
|
41
|
+
else
|
|
42
|
+
flash[:error] = result.error.to_s
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
redirect_back fallback_location: admin_system_waiting_room_path
|
|
46
|
+
end
|
|
32
47
|
end
|
|
33
48
|
end
|
|
34
49
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V2
|
|
4
|
+
module Tenant
|
|
5
|
+
class FreeVoteClaimController < BaseController
|
|
6
|
+
before_action :require_spree_current_user
|
|
7
|
+
|
|
8
|
+
def show
|
|
9
|
+
load_effective_config_onto_show
|
|
10
|
+
|
|
11
|
+
render_serialized_payload { serialize_resource(current_show) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create
|
|
15
|
+
result = SpreeCmCommissioner::VotingCredits::ClaimFreeVotes.new(
|
|
16
|
+
show: current_show,
|
|
17
|
+
user: spree_current_user,
|
|
18
|
+
tenant_id: MultiTenant.current_tenant_id,
|
|
19
|
+
votable_id: voting_session.id
|
|
20
|
+
).call
|
|
21
|
+
|
|
22
|
+
if result.failure?
|
|
23
|
+
render_error_payload(result.error.to_s)
|
|
24
|
+
elsif result.value.nil?
|
|
25
|
+
head :no_content
|
|
26
|
+
else
|
|
27
|
+
render_serialized_payload { Spree::V2::Tenant::VotingCreditSerializer.new(result.value).serializable_hash }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def resource_serializer
|
|
34
|
+
Spree::V2::Tenant::FreeVoteClaimSerializer
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def load_effective_config_onto_show
|
|
38
|
+
config = voting_session.effective_voting_config(show_config: current_show.effective_voting_config)
|
|
39
|
+
|
|
40
|
+
current_show.claimed = !spree_current_user.free_vote_claimable?(show: current_show, votable_id: voting_session.id)
|
|
41
|
+
current_show.effective_free_vote_limit = config['free_vote_limit']
|
|
42
|
+
current_show.effective_free_vote_limit_type = config['free_vote_limit_type']
|
|
43
|
+
rescue ArgumentError
|
|
44
|
+
current_show.claimed = false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def voting_session
|
|
48
|
+
@voting_session ||= current_vendor.voting_sessions.find(params[:voting_session_id])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def current_show
|
|
52
|
+
@current_show ||= voting_session.show
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V2
|
|
4
|
+
module Tenant
|
|
5
|
+
class VotePackagesController < BaseController
|
|
6
|
+
def index
|
|
7
|
+
render_serialized_payload { serialize_collection(scope) }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def scope
|
|
13
|
+
current_vendor.vote_packages.where(status: :active, event_id: current_season.id)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def current_season
|
|
17
|
+
@current_season ||= current_vendor.shows
|
|
18
|
+
.where.not(parent_id: nil)
|
|
19
|
+
.find_by!(slug: params[:show_id])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def serialize_collection(collection)
|
|
23
|
+
collection_serializer.new(collection, include: resource_includes).serializable_hash
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def collection_serializer
|
|
27
|
+
Spree::V2::Tenant::VotePackageSerializer
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -19,18 +19,47 @@ module SpreeCmCommissioner
|
|
|
19
19
|
max_sessions - active_sessions
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
# This query
|
|
23
|
-
# Client
|
|
22
|
+
# This query requires an index; create it in Firebase beforehand.
|
|
23
|
+
# Client must create waiting_guests documents with :queued_at and :allow_to_enter_room_at set to nil to allow filter + order queries.
|
|
24
|
+
#
|
|
25
|
+
# Yesterday's guests are always older than today's, so fill from yesterday first, then use any
|
|
26
|
+
# leftover slots for today. This way no one queued before the midnight rollover gets skipped.
|
|
27
|
+
# e.g. 5 slots, 2 waiting in yesterday -> take both, then take 3 from today.
|
|
24
28
|
def fetch_long_waiting_guests(available_slots)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
previous_guests = eligible_guests_in(previous_records_path, available_slots)
|
|
30
|
+
|
|
31
|
+
# Pre-flip window: the lobby pointer still points at yesterday, so both paths resolve to the
|
|
32
|
+
# same partition — return now to avoid querying (and double-counting) it twice.
|
|
33
|
+
return previous_guests if records_path == previous_records_path
|
|
34
|
+
|
|
35
|
+
remaining_slots = available_slots - previous_guests.size
|
|
36
|
+
return previous_guests if remaining_slots <= 0
|
|
37
|
+
|
|
38
|
+
previous_guests + eligible_guests_in(records_path, remaining_slots)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def eligible_guests_in(records_path, limit)
|
|
42
|
+
firestore.col(records_path)
|
|
28
43
|
.where('allow_to_enter_room_at', '==', nil)
|
|
29
44
|
.order('queued_at')
|
|
30
|
-
.limit(
|
|
45
|
+
.limit(limit)
|
|
31
46
|
.get.to_a
|
|
32
47
|
end
|
|
33
48
|
|
|
49
|
+
# Published path is authoritative; fall back to the server's own date if not yet published.
|
|
50
|
+
def records_path
|
|
51
|
+
lobby_data&.dig(:waiting_guests_records_path).presence || default_records_path(current_date)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Drain target is derived from the server date, never the (possibly stale) lobby pointer.
|
|
55
|
+
def previous_records_path
|
|
56
|
+
default_records_path(previous_date)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def default_records_path(date)
|
|
60
|
+
"waiting_guests/#{date}/records"
|
|
61
|
+
end
|
|
62
|
+
|
|
34
63
|
# For alert waiting guests to enter room, we just update :allow_to_enter_room_at.
|
|
35
64
|
# App will listen to firebase & start refresh session token to enter room.
|
|
36
65
|
def calling_all(waiting_guests)
|
|
@@ -45,9 +74,22 @@ module SpreeCmCommissioner
|
|
|
45
74
|
Time.zone.now.strftime('%Y-%m-%d')
|
|
46
75
|
end
|
|
47
76
|
|
|
77
|
+
def previous_date
|
|
78
|
+
1.day.ago.strftime('%Y-%m-%d')
|
|
79
|
+
end
|
|
80
|
+
|
|
48
81
|
# When open app, app request to check whether room is full or not via Firebase instead of server to minimize server requests.
|
|
82
|
+
# merge: true so we preserve the published `waiting_guests_records_path` on the lobby doc.
|
|
49
83
|
def mark_as(full:, available_slots:)
|
|
50
|
-
|
|
84
|
+
lobby_document.set({ full: full, available_slots: available_slots }, merge: true)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def lobby_data
|
|
88
|
+
@lobby_data ||= lobby_document.get.data
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def lobby_document
|
|
92
|
+
@lobby_document ||= firestore.col('waiting_rooms').doc('lobby')
|
|
51
93
|
end
|
|
52
94
|
|
|
53
95
|
def fetch_max_sessions
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# waiting_room_lobby_path_publisher:
|
|
2
|
+
# cron: "0 0 * * * Asia/Phnom_Penh" # Once per day at local midnight (date rollover)
|
|
3
|
+
# class: "SpreeCmCommissioner::WaitingRoom::PublishLobbyPathJob"
|
|
4
|
+
module SpreeCmCommissioner
|
|
5
|
+
module WaitingRoom
|
|
6
|
+
class PublishLobbyPathJob < ApplicationJob
|
|
7
|
+
queue_as :waiting_room
|
|
8
|
+
|
|
9
|
+
def perform
|
|
10
|
+
return if ENV['WAITING_ROOM_DISABLED'] == 'yes'
|
|
11
|
+
|
|
12
|
+
# call! so a publish failure raises (Sidekiq retries/alerts) instead of being silently swallowed.
|
|
13
|
+
SpreeCmCommissioner::WaitingRoom::PublishLobbyPath.call!
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -2,14 +2,17 @@ module SpreeCmCommissioner
|
|
|
2
2
|
class Show < Spree::Taxon
|
|
3
3
|
include SpreeCmCommissioner::StoreMetadata
|
|
4
4
|
|
|
5
|
-
has_many :voting_credits, class_name: 'SpreeCmCommissioner::VotingCredit', as: :votable, dependent: :destroy
|
|
6
5
|
belongs_to :show, class_name: 'SpreeCmCommissioner::Show', foreign_key: :parent_id, optional: true
|
|
6
|
+
|
|
7
|
+
has_many :voting_credits, class_name: 'SpreeCmCommissioner::VotingCredit', as: :votable, dependent: :destroy
|
|
7
8
|
has_many :seasons, class_name: 'SpreeCmCommissioner::Show', foreign_key: :parent_id, inverse_of: :parent
|
|
8
9
|
has_many :show_contestants, class_name: 'SpreeCmCommissioner::ShowContestant', inverse_of: :show, dependent: :destroy
|
|
9
10
|
has_many :show_people_assignments, class_name: 'SpreeCmCommissioner::ShowPersonAssignment', dependent: :destroy
|
|
10
11
|
has_many :show_people, through: :show_people_assignments, source: :show_person
|
|
11
12
|
has_many :episodes, class_name: 'SpreeCmCommissioner::ShowEpisode', foreign_key: :event_id
|
|
13
|
+
has_many :vote_packages, -> { where(product_type: :vote_package) }, class_name: 'SpreeCmCommissioner::ShowEpisode', foreign_key: :event_id
|
|
12
14
|
has_many :voting_sessions, through: :episodes, class_name: 'SpreeCmCommissioner::VotingSession'
|
|
15
|
+
|
|
13
16
|
has_one :current_episode, -> { current_or_next_upcoming }, class_name: 'SpreeCmCommissioner::ShowEpisode', foreign_key: :event_id
|
|
14
17
|
has_one :current_voting_session, through: :current_episode
|
|
15
18
|
|
|
@@ -24,6 +27,8 @@ module SpreeCmCommissioner
|
|
|
24
27
|
SHOW_TYPES = %w[show tournament boxing].freeze
|
|
25
28
|
|
|
26
29
|
# Define standard voting configuration keys
|
|
30
|
+
attr_accessor :claimed, :effective_free_vote_limit, :effective_free_vote_limit_type
|
|
31
|
+
|
|
27
32
|
store_accessor :voting_config,
|
|
28
33
|
:free_vote_limit,
|
|
29
34
|
:free_vote_limit_type,
|
|
@@ -64,12 +69,13 @@ module SpreeCmCommissioner
|
|
|
64
69
|
"SpreeCmCommissioner::#{type}"
|
|
65
70
|
end
|
|
66
71
|
|
|
67
|
-
def free_vote_idempotency_key(user_id:, votable_id: nil)
|
|
68
|
-
|
|
72
|
+
def free_vote_idempotency_key(user_id:, votable_id: nil, limit_type: nil)
|
|
73
|
+
effective_limit_type = limit_type || free_vote_limit_type
|
|
74
|
+
case effective_limit_type
|
|
69
75
|
when 'per_episode' then "free_claim::episode::#{votable_id}::user::#{user_id}"
|
|
70
76
|
when 'per_voting_session' then "free_claim::voting_session::#{votable_id}::user::#{user_id}"
|
|
71
77
|
when 'per_show' then "free_claim::show::#{id}::user::#{user_id}"
|
|
72
|
-
else raise ArgumentError, "Invalid free_vote_limit_type: #{
|
|
78
|
+
else raise ArgumentError, "Invalid free_vote_limit_type: #{effective_limit_type}"
|
|
73
79
|
end
|
|
74
80
|
end
|
|
75
81
|
|
|
@@ -196,6 +196,18 @@ module SpreeCmCommissioner
|
|
|
196
196
|
notification
|
|
197
197
|
end
|
|
198
198
|
|
|
199
|
+
def free_vote_claimable?(show:, votable_id: nil)
|
|
200
|
+
voting_session = votable_id.present? ? SpreeCmCommissioner::VotingSession.find_by(id: votable_id, show: show) : nil
|
|
201
|
+
effective_config = voting_session&.effective_voting_config || show.effective_voting_config
|
|
202
|
+
limit_type = effective_config['free_vote_limit_type']
|
|
203
|
+
|
|
204
|
+
return false unless limit_type
|
|
205
|
+
|
|
206
|
+
resolved_votable_id = limit_type == 'per_episode' ? voting_session&.episode_id : votable_id
|
|
207
|
+
idempotency_key = show.free_vote_idempotency_key(user_id: id, votable_id: resolved_votable_id, limit_type: limit_type)
|
|
208
|
+
!voting_credit_transactions.exists?(idempotency_key: idempotency_key)
|
|
209
|
+
end
|
|
210
|
+
|
|
199
211
|
def push_notificable?
|
|
200
212
|
return false if device_tokens_count.blank?
|
|
201
213
|
|
|
@@ -118,6 +118,7 @@ module SpreeCmCommissioner
|
|
|
118
118
|
}, class_name: 'Spree::Taxonomy', dependent: :destroy
|
|
119
119
|
|
|
120
120
|
base.has_many :merchandise_products, -> { where(product_type: :ecommerce, event_id: nil) }, class_name: 'Spree::Product'
|
|
121
|
+
base.has_many :vote_packages, -> { where(product_type: :vote_package) }, class_name: 'SpreeCmCommissioner::ShowEpisode', foreign_key: :vendor_id
|
|
121
122
|
|
|
122
123
|
# Create maintaining task to purge vendor related caches
|
|
123
124
|
base.after_save { SpreeCmCommissioner::MaintenanceTasks::CacheInvalidation.pending.create_or_find_by(maintainable: self) }
|
|
@@ -103,12 +103,12 @@ module SpreeCmCommissioner
|
|
|
103
103
|
end
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
def effective_voting_config
|
|
106
|
+
def effective_voting_config(show_config: nil)
|
|
107
107
|
return {} unless show
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
show_config ||= show.effective_voting_config
|
|
110
|
+
session_config = parse_json_config(voting_config).compact
|
|
111
|
+
show_config.merge(session_config)
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
def effective_fraud_config
|
|
@@ -2,7 +2,10 @@ module Spree
|
|
|
2
2
|
module V2
|
|
3
3
|
module Tenant
|
|
4
4
|
class VotePackageSerializer < BaseSerializer
|
|
5
|
-
attributes :name, :price, :compare_at_price, :
|
|
5
|
+
attributes :name, :price, :compare_at_price, :currency, :display_price,
|
|
6
|
+
:available_on, :discontinue_on, :status, :vote_credits
|
|
7
|
+
|
|
8
|
+
has_many :variants, serializer: Spree::V2::Tenant::VariantSerializer
|
|
6
9
|
end
|
|
7
10
|
end
|
|
8
11
|
end
|
|
@@ -1,82 +1,57 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# - per_show : once per show (no extra params needed)
|
|
6
|
-
# - per_episode : once per episode (pass votable_type: 'SpreeCmCommissioner::ShowEpisode', votable_id: episode.id)
|
|
7
|
-
# - per_voting_session: once per voting session (pass votable_type: 'SpreeCmCommissioner::VotingSession', votable_id: voting_session.id)
|
|
1
|
+
# Grants free votes based on the effective config (voting session overrides show):
|
|
2
|
+
# - per_show : once per show (votable_id optional)
|
|
3
|
+
# - per_episode : once per episode (votable_id: voting_session.id)
|
|
4
|
+
# - per_voting_session: once per voting session (votable_id: voting_session.id)
|
|
8
5
|
#
|
|
6
|
+
# votable_id is always a VotingSession ID. The episode is derived from it when needed.
|
|
7
|
+
# The credit wallet target is determined by the show's voting_credit_scope.
|
|
9
8
|
# Idempotent: a unique idempotency_key on the transaction prevents double-claiming.
|
|
10
|
-
#
|
|
11
|
-
# Usage:
|
|
12
|
-
# Show with per_show limit:
|
|
13
|
-
# SpreeCmCommissioner::VotingCredits::ClaimFreeVotes.new(
|
|
14
|
-
# show: show,
|
|
15
|
-
# user: user,
|
|
16
|
-
# tenant_id: 1
|
|
17
|
-
# ).call
|
|
18
|
-
#
|
|
19
|
-
# Show with per_episode limit:
|
|
20
|
-
# SpreeCmCommissioner::VotingCredits::ClaimFreeVotes.new(
|
|
21
|
-
# show: show,
|
|
22
|
-
# user: user,
|
|
23
|
-
# tenant_id: 1,
|
|
24
|
-
# votable_type: 'SpreeCmCommissioner::ShowEpisode',
|
|
25
|
-
# votable_id: episode.id
|
|
26
|
-
# ).call
|
|
27
|
-
#
|
|
28
|
-
# Show with per_voting_session limit:
|
|
29
|
-
# SpreeCmCommissioner::VotingCredits::ClaimFreeVotes.new(
|
|
30
|
-
# show: show,
|
|
31
|
-
# user: user,
|
|
32
|
-
# tenant_id: 1,
|
|
33
|
-
# votable_type: 'SpreeCmCommissioner::VotingSession',
|
|
34
|
-
# votable_id: voting_session.id
|
|
35
|
-
# ).call
|
|
36
9
|
module SpreeCmCommissioner
|
|
37
10
|
module VotingCredits
|
|
38
11
|
class ClaimFreeVotes
|
|
39
12
|
prepend ::Spree::ServiceModule::Base
|
|
40
13
|
|
|
41
|
-
attr_reader :show, :user, :tenant_id, :
|
|
14
|
+
attr_reader :show, :user, :tenant_id, :votable_id
|
|
42
15
|
|
|
43
|
-
def initialize(show:, user:, tenant_id:,
|
|
16
|
+
def initialize(show:, user:, tenant_id:, votable_id: nil)
|
|
44
17
|
@show = show
|
|
45
18
|
@user = user
|
|
46
19
|
@tenant_id = tenant_id
|
|
47
|
-
@votable_type = votable_type
|
|
48
20
|
@votable_id = votable_id
|
|
49
21
|
end
|
|
50
22
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def call # rubocop:disable Metrics/AbcSize
|
|
54
|
-
return success(nil) unless show.free_vote_limit_type
|
|
23
|
+
def call
|
|
24
|
+
return success(nil) unless effective_limit_type
|
|
55
25
|
|
|
56
|
-
|
|
26
|
+
return failure(nil, 'votable_id is required for per_episode/per_voting_session') if requires_votable_id? && votable_id.blank?
|
|
57
27
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return failure(nil, 'Free votes have already been claimed')
|
|
61
|
-
end
|
|
28
|
+
key_votable_id = effective_limit_type == 'per_episode' ? resolved_voting_session.episode_id : votable_id
|
|
29
|
+
idempotency_key = show.free_vote_idempotency_key(user_id: user.id, votable_id: key_votable_id, limit_type: effective_limit_type)
|
|
62
30
|
|
|
63
|
-
|
|
31
|
+
return failure(nil, 'Free votes have already been claimed') if already_claimed?(idempotency_key)
|
|
32
|
+
|
|
33
|
+
amount = effective_limit.to_i
|
|
64
34
|
|
|
65
35
|
return failure(nil, "Free votes are not available for #{show.voting_credit_scope}") if amount <= 0
|
|
66
36
|
|
|
67
|
-
|
|
68
|
-
when 'per_episode', 'per_voting_session'
|
|
69
|
-
if votable_type.blank? || votable_id.blank?
|
|
70
|
-
return failure(nil, 'votable_type and votable_id are required for per_episode/per_voting_session')
|
|
71
|
-
end
|
|
72
|
-
end
|
|
37
|
+
credit = grant_votes(amount: amount, idempotency_key: idempotency_key)
|
|
73
38
|
|
|
39
|
+
success(credit)
|
|
40
|
+
rescue ActiveRecord::RecordNotUnique
|
|
41
|
+
failure(nil, 'Free votes have already been claimed')
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
failure(nil, e.message)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def grant_votes(amount:, idempotency_key:)
|
|
74
49
|
credit = nil
|
|
75
50
|
|
|
76
51
|
ActiveRecord::Base.transaction do
|
|
77
52
|
credit = user.voting_credits.active.find_or_create_by!(
|
|
78
53
|
tenant_id: tenant_id,
|
|
79
|
-
votable:
|
|
54
|
+
votable: credit_votable
|
|
80
55
|
) { |c| c.category = :promo }
|
|
81
56
|
|
|
82
57
|
credit.with_lock do
|
|
@@ -90,30 +65,52 @@ module SpreeCmCommissioner
|
|
|
90
65
|
amount: amount,
|
|
91
66
|
originator: show,
|
|
92
67
|
free_claim_votable_id: votable_id,
|
|
68
|
+
idempotency_key: idempotency_key,
|
|
93
69
|
user_total_amount: credit.available_votes,
|
|
94
70
|
memo: "Free claim: #{amount} votes | credit_scope: #{show.voting_credit_scope} | key: #{idempotency_key}"
|
|
95
71
|
)
|
|
96
72
|
end
|
|
97
73
|
|
|
98
|
-
|
|
99
|
-
rescue ActiveRecord::RecordNotUnique
|
|
100
|
-
# Two concurrent requests can both pass the existence check before either commits.
|
|
101
|
-
# The one that loses the unique-index race lands here — treat it as already claimed.
|
|
102
|
-
failure(nil, 'Free votes have already been claimed')
|
|
103
|
-
rescue StandardError => e
|
|
104
|
-
failure(nil, e.message)
|
|
74
|
+
credit
|
|
105
75
|
end
|
|
106
76
|
|
|
107
|
-
|
|
108
|
-
# Resolves through the show association to prevent claiming against unrelated votable_ids.
|
|
109
|
-
def votable
|
|
77
|
+
def credit_votable
|
|
110
78
|
case show.credit_votable_type
|
|
111
|
-
when 'SpreeCmCommissioner::Show'
|
|
112
|
-
when 'SpreeCmCommissioner::ShowEpisode'
|
|
113
|
-
when 'SpreeCmCommissioner::VotingSession' then
|
|
79
|
+
when 'SpreeCmCommissioner::Show' then show
|
|
80
|
+
when 'SpreeCmCommissioner::ShowEpisode' then SpreeCmCommissioner::ShowEpisode.find(resolved_voting_session.episode_id)
|
|
81
|
+
when 'SpreeCmCommissioner::VotingSession' then resolved_voting_session
|
|
114
82
|
else raise ActiveRecord::RecordNotFound, "Unknown credit_votable_type: #{show.credit_votable_type}"
|
|
115
83
|
end
|
|
116
84
|
end
|
|
85
|
+
|
|
86
|
+
def resolved_voting_session
|
|
87
|
+
@resolved_voting_session ||= SpreeCmCommissioner::VotingSession.find_by!(id: votable_id, show: show)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def effective_voting_config
|
|
91
|
+
@effective_voting_config ||=
|
|
92
|
+
if votable_id.present?
|
|
93
|
+
resolved_voting_session.effective_voting_config(show_config: show.effective_voting_config)
|
|
94
|
+
else
|
|
95
|
+
show.effective_voting_config
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def effective_limit_type
|
|
100
|
+
effective_voting_config['free_vote_limit_type']
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def effective_limit
|
|
104
|
+
effective_voting_config['free_vote_limit']
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def requires_votable_id?
|
|
108
|
+
effective_limit_type.in?(%w[per_episode per_voting_session])
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def already_claimed?(idempotency_key)
|
|
112
|
+
SpreeCmCommissioner::VotingCreditTransaction.exists?(idempotency_key: idempotency_key)
|
|
113
|
+
end
|
|
117
114
|
end
|
|
118
115
|
end
|
|
119
116
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'google/cloud/firestore'
|
|
2
|
+
|
|
3
|
+
module SpreeCmCommissioner
|
|
4
|
+
module WaitingRoom
|
|
5
|
+
# Publishes the server-owned waiting-guests records path to the lobby document, so mobile
|
|
6
|
+
# and the waiting-room caller share one source of truth for the date partition instead of
|
|
7
|
+
# each building it from their own (possibly skewed) clock/timezone.
|
|
8
|
+
#
|
|
9
|
+
# CRON note: ecause we use Time.zone.now to determine the path, it's important that the CRON job runs at the same
|
|
10
|
+
# timezone as well — otherwise the cron could fire before/after Time.zone.now rolls to the new date.
|
|
11
|
+
# Example: cron: "0 0 * * * Asia/Phnom_Penh"
|
|
12
|
+
#
|
|
13
|
+
# Caching note: No caching needed since the service is expected to be called once per day
|
|
14
|
+
# so a single daily merge write is negligible.
|
|
15
|
+
class PublishLobbyPath
|
|
16
|
+
prepend ::Spree::ServiceModule::Base
|
|
17
|
+
extend SpreeCmCommissioner::ServiceModuleThrowable
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
# merge: true so we never clobber the lobby's `full` / `available_slots` fields.
|
|
21
|
+
lobby_document.set({ waiting_guests_records_path: records_path }, merge: true)
|
|
22
|
+
|
|
23
|
+
success(records_path: records_path)
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
failure(nil, e.message)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Server date is authoritative.
|
|
31
|
+
def records_path
|
|
32
|
+
@records_path ||= "waiting_guests/#{Time.zone.now.strftime('%Y-%m-%d')}/records"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def lobby_document
|
|
36
|
+
@lobby_document ||= firestore.col('waiting_rooms').doc('lobby')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def firestore
|
|
40
|
+
@firestore ||= Google::Cloud::Firestore.new(project_id: service_account[:project_id], credentials: service_account)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def service_account
|
|
44
|
+
@service_account ||= Rails.application.credentials.cloud_firestore_service_account
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'google/cloud/firestore'
|
|
2
|
+
|
|
3
|
+
module SpreeCmCommissioner
|
|
4
|
+
# Reads the lobby document (waiting_rooms/lobby) — the read-side counterpart to
|
|
5
|
+
# WaitingRoom::PublishLobbyPath. Kept separate from WaitingRoomSystemMetadataFetcher,
|
|
6
|
+
# which owns the unrelated metadata/system document.
|
|
7
|
+
class WaitingRoomLobbyMetadataFetcher
|
|
8
|
+
attr_reader :document_data
|
|
9
|
+
|
|
10
|
+
def initialize(firestore: nil)
|
|
11
|
+
@firestore = firestore if firestore.present?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def load_document_data
|
|
15
|
+
@document_data = document.get.data || {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# firebase metadata
|
|
19
|
+
|
|
20
|
+
# server-owned date partition — see WaitingRoom::PublishLobbyPath.
|
|
21
|
+
def waiting_guests_records_path
|
|
22
|
+
document_data[:waiting_guests_records_path]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# written by WaitingGuestsCaller#mark_as.
|
|
26
|
+
def full
|
|
27
|
+
document_data[:full]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# written by WaitingGuestsCaller#mark_as.
|
|
31
|
+
def available_slots
|
|
32
|
+
document_data[:available_slots]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def document
|
|
36
|
+
@document ||= firestore.col('waiting_rooms').doc('lobby')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def firestore
|
|
40
|
+
@firestore ||= Google::Cloud::Firestore.new(project_id: service_account[:project_id], credentials: service_account)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def service_account
|
|
44
|
+
@service_account ||= Rails.application.credentials.cloud_firestore_service_account
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<%= button_link_to Spree.t(:force_pull), force_pull_admin_system_waiting_room_path, method: :post, class: "btn btn-outline-primary" %>
|
|
5
5
|
<% end %>
|
|
6
6
|
|
|
7
|
+
<h6 class="mb-2 text-muted">System</h6>
|
|
7
8
|
<div class="bg-white border rounded table-responsive">
|
|
8
9
|
<table class="table" data-hook>
|
|
9
10
|
<thead class="text-muted">
|
|
@@ -71,3 +72,55 @@
|
|
|
71
72
|
</tbody>
|
|
72
73
|
</table>
|
|
73
74
|
</div>
|
|
75
|
+
|
|
76
|
+
<h6 class="mt-4 mb-2 text-muted">Lobby</h6>
|
|
77
|
+
<div class="bg-white border rounded table-responsive">
|
|
78
|
+
<table class="table" data-hook>
|
|
79
|
+
<thead class="text-muted">
|
|
80
|
+
<tr data-hook="admin_system_lobby_headers">
|
|
81
|
+
<th><%= Spree.t(:field_name) %></th>
|
|
82
|
+
<th><%= Spree.t(:current_value) %></th>
|
|
83
|
+
<th></th>
|
|
84
|
+
</tr>
|
|
85
|
+
</thead>
|
|
86
|
+
<tbody>
|
|
87
|
+
<% lobby_items = [
|
|
88
|
+
{
|
|
89
|
+
field_name: :waiting_guests_records_path,
|
|
90
|
+
value: @lobby_fetcher.waiting_guests_records_path.presence || "Not published yet",
|
|
91
|
+
reset_path: publish_lobby_path_admin_system_waiting_room_path
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
field_name: :full,
|
|
95
|
+
value: @lobby_fetcher.full ? "Yes" : "No",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
field_name: :available_slots,
|
|
99
|
+
value: @lobby_fetcher.available_slots,
|
|
100
|
+
},
|
|
101
|
+
] %>
|
|
102
|
+
|
|
103
|
+
<% lobby_items.each do |item| %>
|
|
104
|
+
<tr data-hook="admin_system_lobby_rows">
|
|
105
|
+
<td>
|
|
106
|
+
<%= item[:field_name].to_s.humanize %>
|
|
107
|
+
<span class="badge text-lowercase"><%= item[:field_name] %></span>
|
|
108
|
+
</td>
|
|
109
|
+
<td>
|
|
110
|
+
<%= item[:value] %>
|
|
111
|
+
</td>
|
|
112
|
+
<td>
|
|
113
|
+
<% if item[:reset_path].present? %>
|
|
114
|
+
<%= link_to_with_icon('arrow-counterclockwise.svg', "Publish path now", item[:reset_path],
|
|
115
|
+
method: :post,
|
|
116
|
+
remote: false,
|
|
117
|
+
class: 'icon_link btn btn-sm outline text-dark m-0 p-0',
|
|
118
|
+
data: { confirm: "Publish today's records path to the lobby now?" }, no_text: true
|
|
119
|
+
) %>
|
|
120
|
+
<% end %>
|
|
121
|
+
</td>
|
|
122
|
+
</tr>
|
|
123
|
+
<% end %>
|
|
124
|
+
</tbody>
|
|
125
|
+
</table>
|
|
126
|
+
</div>
|
data/config/routes.rb
CHANGED
|
@@ -14,6 +14,7 @@ Spree::Core::Engine.add_routes do
|
|
|
14
14
|
resource :waiting_room, controller: :waiting_room, only: [:show] do
|
|
15
15
|
collection do
|
|
16
16
|
post :force_pull
|
|
17
|
+
post :publish_lobby_path
|
|
17
18
|
post :modify_multiplier
|
|
18
19
|
post :modify_max_thread_count
|
|
19
20
|
end
|
|
@@ -604,10 +605,11 @@ Spree::Core::Engine.add_routes do
|
|
|
604
605
|
end
|
|
605
606
|
resources :contestants, controller: :show_contestants, only: %i[show]
|
|
606
607
|
resources :elimination_sessions, controller: :show_elimination_sessions, only: %i[index]
|
|
607
|
-
resources :
|
|
608
|
+
resources :vote_packages, only: :index
|
|
608
609
|
end
|
|
609
610
|
|
|
610
611
|
resources :voting_sessions, only: [] do
|
|
612
|
+
resource :free_vote_claim, controller: :free_vote_claim, only: %i[show create]
|
|
611
613
|
resources :voting_contestants, only: :index
|
|
612
614
|
resources :voting_credits, only: :index
|
|
613
615
|
resources :votes, only: %i[create index]
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_cm_commissioner
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.8.3.pre.
|
|
4
|
+
version: 2.8.3.pre.pre14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- You
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: spree
|
|
@@ -1052,7 +1052,7 @@ files:
|
|
|
1052
1052
|
- app/controllers/spree/api/v2/tenant/customer_notifications_controller.rb
|
|
1053
1053
|
- app/controllers/spree/api/v2/tenant/dynamic_field_options_controller.rb
|
|
1054
1054
|
- app/controllers/spree/api/v2/tenant/episodes_controller.rb
|
|
1055
|
-
- app/controllers/spree/api/v2/tenant/
|
|
1055
|
+
- app/controllers/spree/api/v2/tenant/free_vote_claim_controller.rb
|
|
1056
1056
|
- app/controllers/spree/api/v2/tenant/guests_controller.rb
|
|
1057
1057
|
- app/controllers/spree/api/v2/tenant/homepage_sections_controller.rb
|
|
1058
1058
|
- app/controllers/spree/api/v2/tenant/id_cards_controller.rb
|
|
@@ -1091,6 +1091,7 @@ files:
|
|
|
1091
1091
|
- app/controllers/spree/api/v2/tenant/user_device_tokens_controller.rb
|
|
1092
1092
|
- app/controllers/spree/api/v2/tenant/user_registration_with_pin_codes_controller.rb
|
|
1093
1093
|
- app/controllers/spree/api/v2/tenant/vendors_controller.rb
|
|
1094
|
+
- app/controllers/spree/api/v2/tenant/vote_packages_controller.rb
|
|
1094
1095
|
- app/controllers/spree/api/v2/tenant/votes_controller.rb
|
|
1095
1096
|
- app/controllers/spree/api/v2/tenant/voting_contestants_controller.rb
|
|
1096
1097
|
- app/controllers/spree/api/v2/tenant/voting_credit_transactions_controller.rb
|
|
@@ -1416,6 +1417,7 @@ files:
|
|
|
1416
1417
|
- app/jobs/spree_cm_commissioner/voting_credit_allocation_job.rb
|
|
1417
1418
|
- app/jobs/spree_cm_commissioner/voting_credit_de_allocation_job.rb
|
|
1418
1419
|
- app/jobs/spree_cm_commissioner/waiting_guests_caller_job.rb
|
|
1420
|
+
- app/jobs/spree_cm_commissioner/waiting_room/publish_lobby_path_job.rb
|
|
1419
1421
|
- app/jobs/spree_cm_commissioner/waiting_room_latest_system_metadata_puller_job.rb
|
|
1420
1422
|
- app/jobs/spree_cm_commissioner/waiting_room_session_firebase_logger_job.rb
|
|
1421
1423
|
- app/jobs/spree_cm_commissioner/webhook_subscriber_orders_sender_job.rb
|
|
@@ -1987,6 +1989,7 @@ files:
|
|
|
1987
1989
|
- app/serializers/spree/v2/tenant/digital_link_serializer.rb
|
|
1988
1990
|
- app/serializers/spree/v2/tenant/dynamic_field_option_serializer.rb
|
|
1989
1991
|
- app/serializers/spree/v2/tenant/dynamic_field_serializer.rb
|
|
1992
|
+
- app/serializers/spree/v2/tenant/free_vote_claim_serializer.rb
|
|
1990
1993
|
- app/serializers/spree/v2/tenant/guest_card_class_serializer.rb
|
|
1991
1994
|
- app/serializers/spree/v2/tenant/guest_dynamic_field_serializer.rb
|
|
1992
1995
|
- app/serializers/spree/v2/tenant/guest_serializer.rb
|
|
@@ -2341,6 +2344,8 @@ files:
|
|
|
2341
2344
|
- app/services/spree_cm_commissioner/voting_leaderboards/calculate_score.rb
|
|
2342
2345
|
- app/services/spree_cm_commissioner/voting_leaderboards/combined_result.rb
|
|
2343
2346
|
- app/services/spree_cm_commissioner/voting_sessions/finalize.rb
|
|
2347
|
+
- app/services/spree_cm_commissioner/waiting_room/publish_lobby_path.rb
|
|
2348
|
+
- app/services/spree_cm_commissioner/waiting_room_lobby_metadata_fetcher.rb
|
|
2344
2349
|
- app/services/spree_cm_commissioner/waiting_room_system_metadata_fetcher.rb
|
|
2345
2350
|
- app/services/spree_cm_commissioner/waiting_room_system_metadata_setter.rb
|
|
2346
2351
|
- app/services/spree_cm_commissioner/webhooks/subscribers/handle_request_decorator.rb
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
module Spree
|
|
2
|
-
module Api
|
|
3
|
-
module V2
|
|
4
|
-
module Tenant
|
|
5
|
-
class FreeVoteClaimsController < BaseController
|
|
6
|
-
before_action :require_spree_current_user
|
|
7
|
-
|
|
8
|
-
def create
|
|
9
|
-
show = current_vendor.shows.find(params[:show_id])
|
|
10
|
-
|
|
11
|
-
result = SpreeCmCommissioner::VotingCredits::ClaimFreeVotes.new(
|
|
12
|
-
show: show,
|
|
13
|
-
user: spree_current_user,
|
|
14
|
-
tenant_id: MultiTenant.current_tenant_id,
|
|
15
|
-
votable_type: params[:votable_type],
|
|
16
|
-
votable_id: params[:votable_id]
|
|
17
|
-
).call
|
|
18
|
-
|
|
19
|
-
if result.failure?
|
|
20
|
-
render_error_payload(result.error.to_s)
|
|
21
|
-
elsif result.value.nil?
|
|
22
|
-
head :no_content
|
|
23
|
-
else
|
|
24
|
-
render_serialized_payload { serialize_resource(result.value) }
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def resource_serializer
|
|
31
|
-
Spree::V2::Tenant::VotingCreditSerializer
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|