spree_cm_commissioner 2.8.2.pre.pre.2 → 2.8.2
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/.gitignore +0 -4
- data/Gemfile.lock +1 -1
- data/app/controllers/spree/admin/homepage_section_controller.rb +1 -4
- data/app/controllers/spree/admin/taxons_controller_decorator.rb +0 -19
- data/app/controllers/spree/api/v2/storefront/homepage_sections_controller.rb +0 -1
- data/app/controllers/spree/api/v2/tenant/base_controller.rb +0 -4
- data/app/controllers/spree/api/v2/tenant/homepage_sections_controller.rb +0 -1
- data/app/controllers/spree/api/v2/tenant/products_controller.rb +1 -1
- data/app/controllers/spree/api/v2/tenant/taxons_controller.rb +1 -1
- data/app/controllers/spree_cm_commissioner/admin/products_controller_decorator.rb +0 -19
- data/app/finders/spree_cm_commissioner/events/find_matches.rb +0 -1
- data/app/helpers/spree_cm_commissioner/admin/homepage_segment_helper.rb +0 -2
- data/app/models/concerns/spree_cm_commissioner/homepage_section_bitwise.rb +1 -2
- data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +1 -2
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +0 -10
- data/app/models/concerns/spree_cm_commissioner/product_type.rb +1 -1
- data/app/models/spree_cm_commissioner/product_decorator.rb +0 -39
- data/app/models/spree_cm_commissioner/role_decorator.rb +1 -4
- data/app/models/spree_cm_commissioner/taxon_decorator.rb +0 -15
- data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +1 -10
- data/app/models/spree_cm_commissioner/tenant.rb +0 -9
- data/app/models/spree_cm_commissioner/user_decorator.rb +0 -5
- data/app/models/spree_cm_commissioner/variant_options.rb +0 -4
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +0 -4
- data/app/serializers/spree/v2/storefront/homepage_section_serializer.rb +1 -1
- data/app/serializers/spree/v2/storefront/product_serializer_decorator.rb +1 -1
- data/app/serializers/spree/v2/storefront/role_serializer.rb +1 -1
- data/app/serializers/spree/v2/storefront/taxon_serializer_decorator.rb +1 -2
- data/app/serializers/spree/v2/tenant/homepage_section_serializer.rb +1 -1
- data/app/serializers/spree/v2/tenant/role_serializer.rb +1 -1
- data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +0 -12
- data/app/views/spree/admin/homepage_section/_form.html.erb +0 -5
- data/config/initializers/spree_permitted_attributes.rb +0 -8
- data/config/locales/en.yml +0 -14
- data/config/locales/km.yml +0 -10
- data/config/routes.rb +0 -26
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +1 -7
- data/spree_cm_commissioner.gemspec +1 -1
- metadata +4 -116
- data/app/controllers/spree/api/v2/storefront/preview_products_controller.rb +0 -48
- data/app/controllers/spree/api/v2/storefront/preview_sections_controller.rb +0 -27
- data/app/controllers/spree/api/v2/storefront/preview_taxons_controller.rb +0 -18
- data/app/controllers/spree/api/v2/storefront/products_controller_decorator.rb +0 -15
- data/app/controllers/spree/api/v2/storefront/taxons_controller_decorator.rb +0 -15
- data/app/controllers/spree/api/v2/tenant/free_vote_claims_controller.rb +0 -37
- data/app/controllers/spree/api/v2/tenant/preview_products_controller.rb +0 -47
- data/app/controllers/spree/api/v2/tenant/preview_sections_controller.rb +0 -26
- data/app/controllers/spree/api/v2/tenant/preview_shows_controller.rb +0 -19
- data/app/controllers/spree/api/v2/tenant/preview_taxons_controller.rb +0 -19
- data/app/controllers/spree/api/v2/tenant/show_contestants_controller.rb +0 -52
- data/app/controllers/spree/api/v2/tenant/show_elimination_sessions_controller.rb +0 -57
- data/app/controllers/spree/api/v2/tenant/show_people_controller.rb +0 -49
- data/app/controllers/spree/api/v2/tenant/show_person_assignments_controller.rb +0 -36
- data/app/controllers/spree/api/v2/tenant/shows_controller.rb +0 -34
- data/app/controllers/spree/api/v2/tenant/votes_controller.rb +0 -94
- data/app/controllers/spree/api/v2/tenant/voting_contestants_controller.rb +0 -40
- data/app/controllers/spree/api/v2/tenant/voting_credit_transactions_controller.rb +0 -41
- data/app/controllers/spree/api/v2/tenant/voting_credits_controller.rb +0 -31
- data/app/jobs/spree_cm_commissioner/vote_fraud_event_job.rb +0 -9
- data/app/jobs/spree_cm_commissioner/voting_credit_allocation_job.rb +0 -10
- data/app/jobs/spree_cm_commissioner/voting_credit_de_allocation_job.rb +0 -10
- data/app/models/spree_cm_commissioner/maintenance_tasks/voting_session.rb +0 -36
- data/app/models/spree_cm_commissioner/preview_role.rb +0 -8
- data/app/models/spree_cm_commissioner/role_user_decorator.rb +0 -8
- data/app/models/spree_cm_commissioner/show.rb +0 -159
- data/app/models/spree_cm_commissioner/show_contestant.rb +0 -39
- data/app/models/spree_cm_commissioner/show_contestant_image.rb +0 -11
- data/app/models/spree_cm_commissioner/show_contestant_video.rb +0 -15
- data/app/models/spree_cm_commissioner/show_episode.rb +0 -135
- data/app/models/spree_cm_commissioner/show_person.rb +0 -15
- data/app/models/spree_cm_commissioner/show_person_assignment.rb +0 -20
- data/app/models/spree_cm_commissioner/show_person_image.rb +0 -11
- data/app/models/spree_cm_commissioner/vote.rb +0 -16
- data/app/models/spree_cm_commissioner/vote_fraud_event.rb +0 -19
- data/app/models/spree_cm_commissioner/voting_contestant.rb +0 -46
- data/app/models/spree_cm_commissioner/voting_credit.rb +0 -72
- data/app/models/spree_cm_commissioner/voting_credit_transaction.rb +0 -55
- data/app/models/spree_cm_commissioner/voting_session.rb +0 -223
- data/app/models/spree_cm_commissioner/voting_session_stat.rb +0 -8
- data/app/overrides/spree/admin/products/_form/preview_checkbox.html.erb.deface +0 -9
- data/app/overrides/spree/admin/taxons/_form/preview_checkbox.html.erb.deface +0 -7
- data/app/serializers/spree/v2/tenant/show_contestant_serializer.rb +0 -21
- data/app/serializers/spree/v2/tenant/show_episode_serializer.rb +0 -17
- data/app/serializers/spree/v2/tenant/show_person_assignment_serializer.rb +0 -16
- data/app/serializers/spree/v2/tenant/show_person_serializer.rb +0 -13
- data/app/serializers/spree/v2/tenant/show_serializer.rb +0 -26
- data/app/serializers/spree/v2/tenant/video_serializer.rb +0 -9
- data/app/serializers/spree/v2/tenant/vote_serializer.rb +0 -14
- data/app/serializers/spree/v2/tenant/voting_contestant_serializer.rb +0 -22
- data/app/serializers/spree/v2/tenant/voting_credit_serializer.rb +0 -10
- data/app/serializers/spree/v2/tenant/voting_credit_transaction_serializer.rb +0 -14
- data/app/serializers/spree/v2/tenant/voting_session_serializer.rb +0 -18
- data/app/services/spree_cm_commissioner/fraud_check.rb +0 -279
- data/app/services/spree_cm_commissioner/show_contestants/normalize_video_highlights.rb +0 -57
- data/app/services/spree_cm_commissioner/url_embed/youtube_embed.rb +0 -44
- data/app/services/spree_cm_commissioner/vote_counters/audit_counters.rb +0 -43
- data/app/services/spree_cm_commissioner/vote_counters/base.rb +0 -31
- data/app/services/spree_cm_commissioner/vote_counters/increment.rb +0 -44
- data/app/services/spree_cm_commissioner/vote_counters/per_contestant_counter.rb +0 -68
- data/app/services/spree_cm_commissioner/vote_counters/rebuild_from_db.rb +0 -70
- data/app/services/spree_cm_commissioner/vote_counters/snapshot_to_db.rb +0 -113
- data/app/services/spree_cm_commissioner/vote_credit_deductor.rb +0 -68
- data/app/services/spree_cm_commissioner/vote_package/create.rb +0 -145
- data/app/services/spree_cm_commissioner/vote_package/update.rb +0 -91
- data/app/services/spree_cm_commissioner/vote_processor.rb +0 -144
- data/app/services/spree_cm_commissioner/voting_contestants/advancer.rb +0 -334
- data/app/services/spree_cm_commissioner/voting_contestants/assigner.rb +0 -32
- data/app/services/spree_cm_commissioner/voting_contestants/bulk_updater.rb +0 -106
- data/app/services/spree_cm_commissioner/voting_credits/allocate.rb +0 -77
- data/app/services/spree_cm_commissioner/voting_credits/claim_free_votes.rb +0 -119
- data/app/services/spree_cm_commissioner/voting_credits/credit_calculator.rb +0 -35
- data/app/services/spree_cm_commissioner/voting_credits/de_allocate.rb +0 -87
- data/app/services/spree_cm_commissioner/voting_leaderboards/calculate_score.rb +0 -74
- data/app/services/spree_cm_commissioner/voting_sessions/finalize.rb +0 -66
- data/db/migrate/20260309230148_create_cm_show_people.rb +0 -14
- data/db/migrate/20260309230149_create_cm_show_people_assignments.rb +0 -16
- data/db/migrate/20260310082711_create_cm_show_contestants.rb +0 -28
- data/db/migrate/20260310082720_create_cm_voting_sessions.rb +0 -21
- data/db/migrate/20260310082721_create_cm_voting_contestants.rb +0 -23
- data/db/migrate/20260310082734_add_voting_fields_to_spree_taxons.rb +0 -9
- data/db/migrate/20260310082735_add_type_to_spree_products.rb +0 -6
- data/db/migrate/20260310082749_create_cm_voting_credits.rb +0 -27
- data/db/migrate/20260326080200_create_cm_voting_credit_transactions.rb +0 -27
- data/db/migrate/20260330160000_create_cm_votes.rb +0 -25
- data/db/migrate/20260401072500_add_advanced_from_to_cm_voting_contestants.rb +0 -7
- data/db/migrate/20260402000001_add_voting_credit_scope_to_spree_taxons.rb +0 -6
- data/db/migrate/20260402000002_rename_scopeable_to_votable_in_cm_voting_credits.rb +0 -12
- data/db/migrate/20260403070000_add_name_to_cm_voting_sessions.rb +0 -5
- data/db/migrate/20260406000001_add_vendor_id_to_voting_tables.rb +0 -6
- data/db/migrate/20260406000001_rename_votes_remaining_to_amount_in_cm_voting_credits.rb +0 -11
- data/db/migrate/20260408085255_add_show_id_and_vendor_id_to_cm_voting_sessions.rb +0 -9
- data/db/migrate/20260420000001_rename_type_to_credit_type_in_cm_voting_credits.rb +0 -25
- data/db/migrate/20260422000001_create_cm_vote_fraud_events.rb +0 -23
- data/db/migrate/20260423000001_add_preview_to_taxons_products_and_sections.rb +0 -11
- data/db/migrate/20260423000002_create_preview_roles.rb +0 -24
- data/db/migrate/20260515120000_add_public_metadata_to_cm_voting_sessions.rb +0 -5
- data/db/migrate/20260518090920_add_unique_voter_count_to_voting_contestants.rb +0 -5
- data/db/migrate/20260518094322_create_cm_voting_session_stats.rb +0 -17
- data/db/migrate/20260520000001_add_scoring_model_to_cm_voting_sessions.rb +0 -5
- data/db/migrate/20260520000001_optimize_cm_votes_indexes.rb +0 -22
- data/db/migrate/20260525042257_add_vote_number_to_cm_voting_contestants.rb +0 -18
- data/db/migrate/20260527035430_add_confirmed_rank_to_cm_voting_contestants.rb +0 -5
- data/db/migrate/20260527062005_add_eliminated_at_to_cm_show_contestants.rb +0 -5
- data/docs/sql/jsonb_query_guide.md +0 -57
- data/lib/spree_cm_commissioner/test_helper/factories/show_episode_factory.rb +0 -12
- data/lib/spree_cm_commissioner/test_helper/factories/show_factory.rb +0 -120
- data/lib/spree_cm_commissioner/test_helper/factories/vote_credit_factory.rb +0 -37
- data/lib/spree_cm_commissioner/test_helper/factories/vote_factory.rb +0 -28
- data/lib/spree_cm_commissioner/test_helper/factories/voting_credit_transaction_factory.rb +0 -11
- data/lib/spree_cm_commissioner/test_helper/factories/voting_session_factory.rb +0 -11
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
module SpreeCmCommissioner
|
|
2
|
-
module VotingContestants
|
|
3
|
-
# Keeps one VotingContestant row in sync with the "next" round (another session or episode).
|
|
4
|
-
#
|
|
5
|
-
# Flow (after +update!+ on the current row):
|
|
6
|
-
#
|
|
7
|
-
# 1. **Elimination changed** (`eliminated` toggled) and this row has an +advanced_to+:
|
|
8
|
-
# - Marking eliminated: validate reversion order (block if the advance chain continues in a
|
|
9
|
-
# later session or the next hop has votes), then remove only the **immediate** linked row
|
|
10
|
-
# in the destination session (if no votes). Unwind later rounds first, then earlier ones.
|
|
11
|
-
# - Un-eliminating: +create_or_update_destination_record+ (restore the linked row).
|
|
12
|
-
#
|
|
13
|
-
# 2. **Advance target changed** (+advanced_to_type+ / +advanced_to_id+):
|
|
14
|
-
# - +cleanup_previous_destination+, then if still active and pointing somewhere,
|
|
15
|
-
# +create_or_update_destination_record+ at the new destination.
|
|
16
|
-
#
|
|
17
|
-
# Linked row = the VotingContestant in the next session with the same +show_contestant+,
|
|
18
|
-
# +advanced_from+ = this row's +voting_session+.
|
|
19
|
-
class Advancer
|
|
20
|
-
prepend ::Spree::ServiceModule::Base
|
|
21
|
-
|
|
22
|
-
def call(voting_contestant:, attributes:)
|
|
23
|
-
@voting_contestant = voting_contestant
|
|
24
|
-
@attributes = attributes || {}
|
|
25
|
-
|
|
26
|
-
ActiveRecord::Base.transaction do
|
|
27
|
-
update_contestant
|
|
28
|
-
sync_advancement
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
success(voting_contestant: @voting_contestant.reload)
|
|
32
|
-
rescue StandardError => e
|
|
33
|
-
failure(nil, e.message)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
private
|
|
37
|
-
|
|
38
|
-
def update_contestant
|
|
39
|
-
# Persist whatever fields the caller changed.
|
|
40
|
-
# e.g. @attributes = { eliminated: true } → vc_a.eliminated becomes true
|
|
41
|
-
@voting_contestant.update!(@attributes)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def sync_advancement
|
|
45
|
-
# Each flag is checked independently — both branches can fire in one request.
|
|
46
|
-
# e.g. attributes = { eliminated: true, advanced_to_id: 2 } → both branches run
|
|
47
|
-
|
|
48
|
-
# eliminated flag toggled (false → true or true → false)
|
|
49
|
-
sync_elimination_status if @voting_contestant.saved_change_to_eliminated?
|
|
50
|
-
|
|
51
|
-
# advanced_to pointer assigned, changed, or cleared
|
|
52
|
-
sync_advanced_to if advanced_to_changed?
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def advanced_to_changed?
|
|
56
|
-
# Either part of the polymorphic pair changing counts as a destination change.
|
|
57
|
-
# e.g. advanced_to_id: 1 → 2 (same type, different session) → true
|
|
58
|
-
# e.g. only eliminated changed, advanced_to untouched → false
|
|
59
|
-
@voting_contestant.saved_change_to_advanced_to_id? ||
|
|
60
|
-
@voting_contestant.saved_change_to_advanced_to_type?
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# --- Elimination ---------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
def sync_elimination_status
|
|
66
|
-
# No destination id — contestant was never advanced forward; nothing to mirror.
|
|
67
|
-
# e.g. vc_a { advanced_to_id: nil } → return
|
|
68
|
-
return if @voting_contestant.advanced_to_id.blank?
|
|
69
|
-
|
|
70
|
-
# Incomplete polymorphic ref — can't resolve a session without both parts.
|
|
71
|
-
# e.g. vc_a { advanced_to_type: nil } → return
|
|
72
|
-
return if @voting_contestant.advanced_to_type.blank?
|
|
73
|
-
|
|
74
|
-
if @voting_contestant.eliminated?
|
|
75
|
-
# Guard unwind order: refuse if downstream chain still has votes or extends further.
|
|
76
|
-
# e.g. chain A → B → C; vc_b → vc_c exists → raise (must eliminate C before A)
|
|
77
|
-
validate_reversion!
|
|
78
|
-
|
|
79
|
-
# Destroy the immediate mirror row in the destination session when it has 0 votes.
|
|
80
|
-
# e.g. vc_b (Alice, Session B, 0 votes) → destroy vc_b → clear vc_a.advanced_to
|
|
81
|
-
# If vc_b has votes → destroy_destination_record returns false → pointer kept
|
|
82
|
-
clear_advanced_to_pointer if destroy_destination_record(@voting_contestant.advanced_to_type, @voting_contestant.advanced_to_id)
|
|
83
|
-
else
|
|
84
|
-
# Un-eliminating: recreate the mirror row so Alice re-enters the next session.
|
|
85
|
-
# e.g. vc_a.eliminated true → false → create (or restore) vc_b in Session B
|
|
86
|
-
create_or_update_destination_record
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# --- advanced_to pointer ------------------------------------------------
|
|
91
|
-
|
|
92
|
-
def sync_advanced_to
|
|
93
|
-
# Remove the mirror row we created for the *old* destination before pointing elsewhere.
|
|
94
|
-
# e.g. vc_a.advanced_to changed Session B → Session C → destroy vc_b first
|
|
95
|
-
cleanup_previous_destination
|
|
96
|
-
|
|
97
|
-
# Create a fresh mirror at the new destination (skipped if eliminated or no destination).
|
|
98
|
-
# e.g. vc_a now points to Session C → create vc_c for Alice in Session C
|
|
99
|
-
create_or_update_destination_record if should_create_destination_record?
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def should_create_destination_record?
|
|
103
|
-
# No destination id — nothing to mirror.
|
|
104
|
-
# e.g. vc_a { advanced_to_id: nil } → false
|
|
105
|
-
return false if @voting_contestant.advanced_to_id.blank?
|
|
106
|
-
|
|
107
|
-
# Incomplete polymorphic ref — can't resolve the session.
|
|
108
|
-
# e.g. vc_a { advanced_to_type: nil } → false
|
|
109
|
-
return false if @voting_contestant.advanced_to_type.blank?
|
|
110
|
-
|
|
111
|
-
# Eliminated contestants must not be mirrored forward.
|
|
112
|
-
# e.g. vc_a { eliminated: true } → false
|
|
113
|
-
return false if @voting_contestant.eliminated?
|
|
114
|
-
|
|
115
|
-
true
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def cleanup_previous_destination
|
|
119
|
-
# Read values that were set *before* the current save.
|
|
120
|
-
# e.g. advanced_to was Session B before user changed it to Session C
|
|
121
|
-
old_type = @voting_contestant.advanced_to_type_before_last_save
|
|
122
|
-
old_id = @voting_contestant.advanced_to_id_before_last_save
|
|
123
|
-
|
|
124
|
-
# No previous destination — nothing to clean up.
|
|
125
|
-
# e.g. contestant had no advanced_to before this save → return
|
|
126
|
-
return if old_type.blank? || old_id.blank?
|
|
127
|
-
|
|
128
|
-
# Resolve the old session from the previous polymorphic pointer.
|
|
129
|
-
# e.g. old_type='VotingSession', old_id=2 → Session B
|
|
130
|
-
old_session = find_target_session(old_type, old_id)
|
|
131
|
-
|
|
132
|
-
# Old session was deleted or not found — nothing to clean up.
|
|
133
|
-
return unless old_session
|
|
134
|
-
|
|
135
|
-
# Find the mirror row we previously created in the old session.
|
|
136
|
-
# e.g. vc_b (Alice, advanced_from: Session A) inside old Session B
|
|
137
|
-
linked = find_linked_voting_contestant(old_session, @voting_contestant)
|
|
138
|
-
|
|
139
|
-
# Mirror was never created (e.g. contestant was eliminated at the time) — skip.
|
|
140
|
-
return unless linked
|
|
141
|
-
|
|
142
|
-
# Refuse if vc_b has votes or if vc_b itself advances further (vc_c exists).
|
|
143
|
-
# e.g. vc_b.vote_count = 3 → raise; or vc_b → vc_c exists → raise
|
|
144
|
-
validate_cleanup_old_mirror!(linked)
|
|
145
|
-
|
|
146
|
-
# Recursively destroy vc_b and any children (vc_c, vc_d …) that have 0 votes.
|
|
147
|
-
destroy_mirror_branch_from(linked)
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def create_or_update_destination_record
|
|
151
|
-
# Resolve the destination session from the current polymorphic pointer.
|
|
152
|
-
# e.g. advanced_to: VotingSession#2 → Session B
|
|
153
|
-
session = find_target_session(@voting_contestant.advanced_to_type, @voting_contestant.advanced_to_id)
|
|
154
|
-
|
|
155
|
-
# Target session not found (deleted or bad reference) — nothing to mirror.
|
|
156
|
-
return unless session
|
|
157
|
-
|
|
158
|
-
# Find an existing mirror or initialize a new one, keyed by show_contestant.
|
|
159
|
-
# e.g. find vc_b (Alice in Session B) or build a new record
|
|
160
|
-
record = session.voting_contestants.find_or_initialize_by(show_contestant_id: @voting_contestant.show_contestant_id)
|
|
161
|
-
|
|
162
|
-
# Stamp the mirror with a back-reference so we can locate it later.
|
|
163
|
-
# advanced_from links vc_b → Session A (source of the advancement).
|
|
164
|
-
record.assign_attributes(
|
|
165
|
-
show_id: @voting_contestant.show_id,
|
|
166
|
-
advanced_from: @voting_contestant.voting_session,
|
|
167
|
-
eliminated: @voting_contestant.eliminated
|
|
168
|
-
)
|
|
169
|
-
record.save!
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
# Single-hop destroy: removes only the immediate mirror row (used when eliminating).
|
|
173
|
-
# Returns true → caller may clear the advanced_to pointer.
|
|
174
|
-
# Returns false → mirror has votes; pointer must be kept so the record stays traceable.
|
|
175
|
-
def destroy_destination_record(type, id)
|
|
176
|
-
# Resolve the destination session.
|
|
177
|
-
# e.g. type='VotingSession', id=2 → Session B
|
|
178
|
-
session = find_target_session(type, id)
|
|
179
|
-
|
|
180
|
-
# Session not found — treat as already gone; pointer can be cleared.
|
|
181
|
-
return true unless session
|
|
182
|
-
|
|
183
|
-
# Look for the mirror row (Alice in Session B, advanced_from Session A).
|
|
184
|
-
linked = find_linked_voting_contestant(session, @voting_contestant)
|
|
185
|
-
|
|
186
|
-
# No mirror exists (never created or already removed) — pointer can be cleared.
|
|
187
|
-
return true if linked.nil?
|
|
188
|
-
|
|
189
|
-
# Mirror has votes — destroying it would erase vote history; keep the pointer.
|
|
190
|
-
# e.g. vc_b.vote_count = 5 → return false
|
|
191
|
-
return false if linked.vote_count.to_i.positive?
|
|
192
|
-
|
|
193
|
-
# Safe to destroy (0 votes); signal that the pointer can be cleared.
|
|
194
|
-
# e.g. vc_b.vote_count = 0 → destroy vc_b, return true
|
|
195
|
-
linked.destroy!
|
|
196
|
-
true
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def clear_advanced_to_pointer
|
|
200
|
-
# Sever the forward link after the mirror row has been safely destroyed.
|
|
201
|
-
# e.g. vc_a { advanced_to_type: 'VotingSession', advanced_to_id: 2 } → both set to nil
|
|
202
|
-
@voting_contestant.update!(
|
|
203
|
-
advanced_to_type: nil,
|
|
204
|
-
advanced_to_id: nil,
|
|
205
|
-
updated_at: Time.current
|
|
206
|
-
)
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def validate_cleanup_old_mirror!(linked_row)
|
|
210
|
-
# Mirror has votes — deleting it would lose cast vote data.
|
|
211
|
-
# e.g. vc_b.vote_count = 3 → raise
|
|
212
|
-
raise Spree.t('voting_contestant_advancer.reversion_error') if linked_row.vote_count.to_i.positive?
|
|
213
|
-
|
|
214
|
-
# Mirror itself advances further — unwind the outer hop first.
|
|
215
|
-
# e.g. vc_b.advanced_to = Session C and vc_c exists → raise
|
|
216
|
-
raise Spree.t('voting_contestant_advancer.reversion_error') if downstream_mirror_chain_continues?(linked_row)
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
# Recursively destroys a mirror branch deepest-first (vc_c before vc_b).
|
|
220
|
-
# Only recurses into a child when the child has 0 votes (safe to remove).
|
|
221
|
-
def destroy_mirror_branch_from(linked_row)
|
|
222
|
-
# If this row points forward, try to remove its child subtree first.
|
|
223
|
-
if linked_row.advanced_to_id.present? && linked_row.advanced_to_type.present?
|
|
224
|
-
# Resolve the next session in the chain.
|
|
225
|
-
# e.g. vc_b.advanced_to = Session C → grandchild_session = Session C
|
|
226
|
-
session = find_target_session(linked_row.advanced_to_type, linked_row.advanced_to_id)
|
|
227
|
-
|
|
228
|
-
if session
|
|
229
|
-
# Find the grandchild mirror (Alice in Session C).
|
|
230
|
-
child = find_linked_voting_contestant(session, linked_row)
|
|
231
|
-
|
|
232
|
-
# Recurse only when the grandchild has no votes; leave it intact otherwise.
|
|
233
|
-
# e.g. vc_c.vote_count = 0 → destroy_mirror_branch_from(vc_c) → destroy vc_c
|
|
234
|
-
destroy_mirror_branch_from(child) if child && child.vote_count.to_i.zero?
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
# Destroy this node after its subtree is cleaned up (post-order).
|
|
239
|
-
# e.g. destroy vc_b after vc_c was already removed
|
|
240
|
-
linked_row.destroy!
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
# --- Reversion order (unwind from furthest session toward this one) -----
|
|
244
|
-
|
|
245
|
-
def validate_reversion!
|
|
246
|
-
# Enforce unwind order: eliminate the furthest-advanced session first.
|
|
247
|
-
# e.g. chain A → B → C; must eliminate C before B, then B before A → raise if locked
|
|
248
|
-
raise Spree.t('voting_contestant_advancer.reversion_error') if reversion_locked?(@voting_contestant)
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def reversion_locked?(source_vc)
|
|
252
|
-
# Find the immediate downstream mirror.
|
|
253
|
-
# e.g. source_vc = vc_a → downstream = vc_b (Alice in Session B)
|
|
254
|
-
downstream = find_downstream_voting_contestant(source_vc)
|
|
255
|
-
|
|
256
|
-
# No mirror downstream — nothing blocking the reversion.
|
|
257
|
-
# e.g. vc_a has no vc_b → not locked
|
|
258
|
-
return false if downstream.blank?
|
|
259
|
-
|
|
260
|
-
# Mirror has votes — cannot eliminate source while destination is still active.
|
|
261
|
-
# e.g. vc_b.vote_count = 5 → locked
|
|
262
|
-
return true if downstream.vote_count.to_i.positive?
|
|
263
|
-
|
|
264
|
-
# Mirror itself advances further — must unwind the further chain first.
|
|
265
|
-
# e.g. vc_b → vc_c exists → locked (eliminate C before A)
|
|
266
|
-
return true if downstream_mirror_chain_continues?(downstream)
|
|
267
|
-
|
|
268
|
-
false
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
def downstream_mirror_chain_continues?(downstream)
|
|
272
|
-
# Downstream row has no forward pointer — chain ends here.
|
|
273
|
-
# e.g. vc_b { advanced_to_id: nil } → false
|
|
274
|
-
return false if downstream.advanced_to_id.blank? || downstream.advanced_to_type.blank?
|
|
275
|
-
|
|
276
|
-
# Resolve the session two hops away (grandchild session).
|
|
277
|
-
# e.g. vc_b.advanced_to = Session C → grandchild_session = Session C
|
|
278
|
-
grandchild_session = find_target_session(downstream.advanced_to_type, downstream.advanced_to_id)
|
|
279
|
-
|
|
280
|
-
# Grandchild session not found — chain doesn't continue.
|
|
281
|
-
return false unless grandchild_session
|
|
282
|
-
|
|
283
|
-
# Chain continues only when a mirror actually exists in the grandchild session.
|
|
284
|
-
# e.g. vc_c (Alice, Session C) exists → true (must unwind C first)
|
|
285
|
-
# e.g. no vc_c yet → false (safe to proceed)
|
|
286
|
-
find_linked_voting_contestant(grandchild_session, downstream).present?
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def find_downstream_voting_contestant(source_vc)
|
|
290
|
-
# source_vc must have a forward pointer to look for a downstream mirror.
|
|
291
|
-
# e.g. vc_a { advanced_to_id: nil } → nil (no downstream)
|
|
292
|
-
return unless source_vc.advanced_to_id.present? && source_vc.advanced_to_type.present?
|
|
293
|
-
|
|
294
|
-
# Resolve the destination session from the polymorphic pointer.
|
|
295
|
-
# e.g. vc_a.advanced_to = VotingSession#2 → Session B
|
|
296
|
-
session = find_target_session(source_vc.advanced_to_type, source_vc.advanced_to_id)
|
|
297
|
-
|
|
298
|
-
# Destination session deleted or not found.
|
|
299
|
-
return unless session
|
|
300
|
-
|
|
301
|
-
# Return the mirror row for source_vc in that session.
|
|
302
|
-
# e.g. vc_b (Alice in Session B, advanced_from: Session A)
|
|
303
|
-
find_linked_voting_contestant(session, source_vc)
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
def find_linked_voting_contestant(session, source_vc)
|
|
307
|
-
# Match by same show_contestant AND the back-reference to the source session.
|
|
308
|
-
# Both must match to avoid false positives when the same contestant appears in
|
|
309
|
-
# multiple chains (e.g. re-advanced after un-elimination).
|
|
310
|
-
# e.g. session=B, source=vc_a
|
|
311
|
-
# → find_by(show_contestant_id: Alice.id, advanced_from: Session A) → vc_b
|
|
312
|
-
session.voting_contestants.find_by(
|
|
313
|
-
show_contestant_id: source_vc.show_contestant_id,
|
|
314
|
-
advanced_from: source_vc.voting_session
|
|
315
|
-
)
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
# advanced_to is polymorphic: either a VotingSession directly or a ShowEpisode
|
|
319
|
-
# (resolved to the first session of that episode, ordered by position).
|
|
320
|
-
def find_target_session(type, id)
|
|
321
|
-
case type.to_s
|
|
322
|
-
when 'SpreeCmCommissioner::VotingSession'
|
|
323
|
-
# Direct session reference — straightforward lookup.
|
|
324
|
-
# e.g. type='...VotingSession', id=2 → VotingSession#2
|
|
325
|
-
::SpreeCmCommissioner::VotingSession.find_by(id: id)
|
|
326
|
-
when 'SpreeCmCommissioner::ShowEpisode'
|
|
327
|
-
# Episode reference — resolve to the opening (lowest-position) session of that episode.
|
|
328
|
-
# e.g. Episode#5 has sessions [position:1, position:2] → returns the position:1 session
|
|
329
|
-
::SpreeCmCommissioner::ShowEpisode.find_by(id: id)&.voting_sessions&.order(:position)&.first
|
|
330
|
-
end
|
|
331
|
-
end
|
|
332
|
-
end
|
|
333
|
-
end
|
|
334
|
-
end
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
module SpreeCmCommissioner
|
|
2
|
-
module VotingContestants
|
|
3
|
-
class Assigner
|
|
4
|
-
prepend ::Spree::ServiceModule::Base
|
|
5
|
-
|
|
6
|
-
def call(voting_session:, show_contestant_ids: [])
|
|
7
|
-
@voting_session = voting_session
|
|
8
|
-
@episode = voting_session.episode
|
|
9
|
-
@show_contestant_ids = show_contestant_ids || []
|
|
10
|
-
|
|
11
|
-
assign_contestants
|
|
12
|
-
|
|
13
|
-
success(voting_session: @voting_session)
|
|
14
|
-
rescue StandardError => e
|
|
15
|
-
failure(nil, e.message)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
def assign_contestants
|
|
21
|
-
target_ids = @show_contestant_ids.uniq
|
|
22
|
-
return if target_ids.empty?
|
|
23
|
-
|
|
24
|
-
@episode.season.show_contestants.where(id: target_ids).find_each do |show_contestant|
|
|
25
|
-
@voting_session.voting_contestants.find_or_create_by!(show_contestant: show_contestant) do |voting_contestant|
|
|
26
|
-
voting_contestant.show_id = @voting_session.show_id
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
module SpreeCmCommissioner
|
|
2
|
-
module VotingContestants
|
|
3
|
-
# Updates multiple voting contestants in a single transaction.
|
|
4
|
-
# Accepts a hash of { contestant_id => attributes } and delegates
|
|
5
|
-
# each update to Advancer. Rolls back all changes if any update fails.
|
|
6
|
-
class BulkUpdater
|
|
7
|
-
prepend ::Spree::ServiceModule::Base
|
|
8
|
-
|
|
9
|
-
# Polymorphic targets a contestant may advance to after a round.
|
|
10
|
-
ALLOWED_ADVANCED_TO_TYPES = %w[
|
|
11
|
-
SpreeCmCommissioner::VotingSession
|
|
12
|
-
SpreeCmCommissioner::ShowEpisode
|
|
13
|
-
].freeze
|
|
14
|
-
|
|
15
|
-
def call(voting_session:, updates:)
|
|
16
|
-
@voting_session = voting_session
|
|
17
|
-
@updates = updates || {}
|
|
18
|
-
@errors = []
|
|
19
|
-
|
|
20
|
-
ActiveRecord::Base.transaction do
|
|
21
|
-
process_updates
|
|
22
|
-
raise ActiveRecord::Rollback if @errors.any?
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
if @errors.empty?
|
|
26
|
-
success(voting_session: @voting_session)
|
|
27
|
-
else
|
|
28
|
-
failure(nil, @errors.to_sentence)
|
|
29
|
-
end
|
|
30
|
-
rescue StandardError => e
|
|
31
|
-
failure(nil, e.message)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
# Iterates over each update entry and delegates to Advancer.
|
|
37
|
-
def process_updates
|
|
38
|
-
@updates.each do |id, attrs|
|
|
39
|
-
vc = @voting_session.voting_contestants.find(id)
|
|
40
|
-
sanitized = sanitize_attrs(attrs)
|
|
41
|
-
next if sanitized.nil?
|
|
42
|
-
|
|
43
|
-
result = Advancer.call(voting_contestant: vc, attributes: sanitized)
|
|
44
|
-
update_eliminated_contestant_status!(voting_contestant: vc, attributes: sanitized) if result.success?
|
|
45
|
-
|
|
46
|
-
@errors << (result.error&.value || "Failed to update contestant #{vc.id}") if result.failure?
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def update_eliminated_contestant_status!(voting_contestant:, attributes:)
|
|
51
|
-
return unless attributes.key?(:eliminated)
|
|
52
|
-
return unless attributes[:eliminated]
|
|
53
|
-
|
|
54
|
-
voting_contestant.show_contestant.eliminate
|
|
55
|
-
voting_contestant.show_contestant.save!
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Normalizes raw params into a safe attributes hash.
|
|
59
|
-
# Returns nil and appends an error if advanced_to is invalid.
|
|
60
|
-
def sanitize_attrs(attrs)
|
|
61
|
-
params = (attrs.respond_to?(:to_unsafe_h) ? attrs.to_unsafe_h : (attrs || {})).with_indifferent_access
|
|
62
|
-
out = {}
|
|
63
|
-
|
|
64
|
-
if params.key?(:eliminated)
|
|
65
|
-
eliminated = ActiveModel::Type::Boolean.new.cast(params[:eliminated])
|
|
66
|
-
out.merge!(eliminated: eliminated, eliminated_via: eliminated ? params[:eliminated_via].presence : nil)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
if params.key?(:advanced_to)
|
|
70
|
-
case (advanced = advanced_to_attrs(params[:advanced_to]))
|
|
71
|
-
when :invalid
|
|
72
|
-
@errors << 'Invalid advanced_to type'
|
|
73
|
-
return nil
|
|
74
|
-
when Hash
|
|
75
|
-
out.merge!(advanced)
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
out[:special_rule_type] = params[:special_rule_type] if params.key?(:special_rule_type)
|
|
80
|
-
out[:vote_number] = params[:vote_number] if params.key?(:vote_number)
|
|
81
|
-
|
|
82
|
-
out
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Parses "ClassName:id" format into { advanced_to_type:, advanced_to_id: }.
|
|
86
|
-
# Returns :invalid if the type is not in ALLOWED_ADVANCED_TO_TYPES.
|
|
87
|
-
# Returns {} (no-op) when the value doesn't include a colon separator.
|
|
88
|
-
def advanced_to_attrs(value)
|
|
89
|
-
return { advanced_to_type: nil, advanced_to_id: nil } if value.blank?
|
|
90
|
-
|
|
91
|
-
parts = value.to_s.split(':')
|
|
92
|
-
return {} if parts.size < 2
|
|
93
|
-
|
|
94
|
-
advanced_to_id = parts.pop.to_i
|
|
95
|
-
advanced_to_type = parts.join(':')
|
|
96
|
-
|
|
97
|
-
return :invalid unless ALLOWED_ADVANCED_TO_TYPES.include?(advanced_to_type)
|
|
98
|
-
|
|
99
|
-
{
|
|
100
|
-
advanced_to_id: advanced_to_id,
|
|
101
|
-
advanced_to_type: advanced_to_type
|
|
102
|
-
}
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
end
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
module SpreeCmCommissioner
|
|
2
|
-
module VotingCredits
|
|
3
|
-
class Allocate
|
|
4
|
-
def initialize(order)
|
|
5
|
-
@order = order
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
def call
|
|
9
|
-
return unless @order.completed?
|
|
10
|
-
return if episode_line_items.empty?
|
|
11
|
-
|
|
12
|
-
ActiveRecord::Base.transaction do
|
|
13
|
-
voting_credit = find_or_create_credit
|
|
14
|
-
allocations = episode_line_items.map do |line_item|
|
|
15
|
-
SpreeCmCommissioner::VotingCredits::CreditCalculator.new(line_item).call
|
|
16
|
-
end
|
|
17
|
-
total_paid_vote_credits = allocations.sum
|
|
18
|
-
|
|
19
|
-
# Use with_lock to ensure we have a lock on the voting credit record while we:
|
|
20
|
-
# - Modify data inside the block
|
|
21
|
-
# - Need to prevent race conditions
|
|
22
|
-
voting_credit.with_lock do
|
|
23
|
-
voting_credit.paid_votes_amount += total_paid_vote_credits
|
|
24
|
-
voting_credit.save!
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
record_transaction(voting_credit, total_paid_vote_credits) if total_paid_vote_credits.positive?
|
|
28
|
-
end
|
|
29
|
-
rescue ActiveRecord::RecordNotUnique
|
|
30
|
-
# already allocated — safe to ignore
|
|
31
|
-
rescue StandardError => e
|
|
32
|
-
CmAppLogger.error(
|
|
33
|
-
label: 'VotingCredits::Allocate failed',
|
|
34
|
-
data: {
|
|
35
|
-
order_id: @order.try(:id),
|
|
36
|
-
error_class: e.class.name,
|
|
37
|
-
error_message: e.message
|
|
38
|
-
}
|
|
39
|
-
)
|
|
40
|
-
false
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
def vote_pack_product
|
|
46
|
-
@vote_pack_product ||= episode_line_items.first&.variant&.product
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def episode_line_items
|
|
50
|
-
@episode_line_items ||= @order.line_items
|
|
51
|
-
.joins(variant: :product)
|
|
52
|
-
.where(spree_products: { type: SpreeCmCommissioner::ShowEpisode.name, product_type: :vote_package })
|
|
53
|
-
.to_a
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def find_or_create_credit
|
|
57
|
-
SpreeCmCommissioner::VotingCredit.active.find_or_create_by!(
|
|
58
|
-
tenant_id: @order.tenant_id,
|
|
59
|
-
user: @order.user,
|
|
60
|
-
votable_type: vote_pack_product.votable_type,
|
|
61
|
-
votable_id: vote_pack_product.votable_id
|
|
62
|
-
) { |c| c.category = :purchase }
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def record_transaction(vote_credit, total_amount)
|
|
66
|
-
vote_credit.voting_credit_transactions.create!(
|
|
67
|
-
tenant_id: @order.tenant_id,
|
|
68
|
-
action: :purchase,
|
|
69
|
-
amount: total_amount,
|
|
70
|
-
originator: @order,
|
|
71
|
-
memo: "Purchase for order ##{@order.number}",
|
|
72
|
-
user_total_amount: vote_credit.available_votes
|
|
73
|
-
)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
# Mobile can check that where ClaimFreeVotes Screen should Appear based on the show's voting_config.
|
|
2
|
-
# free_vote_limit and voting_config.free_vote_limit_type, then call this service to claim free votes.
|
|
3
|
-
#
|
|
4
|
-
# Grants free votes to a user based on the show's free_vote_limit_type:
|
|
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)
|
|
8
|
-
#
|
|
9
|
-
# 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
|
-
module SpreeCmCommissioner
|
|
37
|
-
module VotingCredits
|
|
38
|
-
class ClaimFreeVotes
|
|
39
|
-
prepend ::Spree::ServiceModule::Base
|
|
40
|
-
|
|
41
|
-
attr_reader :show, :user, :tenant_id, :votable_type, :votable_id
|
|
42
|
-
|
|
43
|
-
def initialize(show:, user:, tenant_id:, votable_type: nil, votable_id: nil)
|
|
44
|
-
@show = show
|
|
45
|
-
@user = user
|
|
46
|
-
@tenant_id = tenant_id
|
|
47
|
-
@votable_type = votable_type
|
|
48
|
-
@votable_id = votable_id
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Returns the VotingCredit after granting free votes,
|
|
52
|
-
# or returns msg if free votes cannot be granted (e.g. limit reached).
|
|
53
|
-
def call # rubocop:disable Metrics/AbcSize
|
|
54
|
-
return success(nil) unless show.free_vote_limit_type
|
|
55
|
-
|
|
56
|
-
idempotency_key = show.free_vote_idempotency_key(user_id: user.id, votable_id: votable_id)
|
|
57
|
-
|
|
58
|
-
# idempotency_key is uniquely indexed — faster than scoping through user.voting_transactions association.
|
|
59
|
-
if SpreeCmCommissioner::VotingCreditTransaction.exists?(idempotency_key: idempotency_key)
|
|
60
|
-
return failure(nil, 'Free votes have already been claimed')
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
amount = show.free_vote_limit.to_i
|
|
64
|
-
|
|
65
|
-
return failure(nil, "Free votes are not available for #{show.voting_credit_scope}") if amount <= 0
|
|
66
|
-
|
|
67
|
-
case show.free_vote_limit_type
|
|
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
|
|
73
|
-
|
|
74
|
-
credit = nil
|
|
75
|
-
|
|
76
|
-
ActiveRecord::Base.transaction do
|
|
77
|
-
credit = user.voting_credits.active.find_or_create_by!(
|
|
78
|
-
tenant_id: tenant_id,
|
|
79
|
-
votable: votable
|
|
80
|
-
) { |c| c.category = :promo }
|
|
81
|
-
|
|
82
|
-
credit.with_lock do
|
|
83
|
-
credit.free_votes_amount += amount
|
|
84
|
-
credit.save!
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
credit.voting_credit_transactions.create!(
|
|
88
|
-
tenant_id: tenant_id,
|
|
89
|
-
action: :free_claim,
|
|
90
|
-
amount: amount,
|
|
91
|
-
originator: show,
|
|
92
|
-
free_claim_votable_id: votable_id,
|
|
93
|
-
user_total_amount: credit.available_votes,
|
|
94
|
-
memo: "Free claim: #{amount} votes | credit_scope: #{show.voting_credit_scope} | key: #{idempotency_key}"
|
|
95
|
-
)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
success(credit)
|
|
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)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Always find votable by the show's credit_votable_type, to ensure consistency with where the credits are allocated.
|
|
108
|
-
# Resolves through the show association to prevent claiming against unrelated votable_ids.
|
|
109
|
-
def votable
|
|
110
|
-
case show.credit_votable_type
|
|
111
|
-
when 'SpreeCmCommissioner::Show' then show
|
|
112
|
-
when 'SpreeCmCommissioner::ShowEpisode' then show.episodes.find(votable_id)
|
|
113
|
-
when 'SpreeCmCommissioner::VotingSession' then show.voting_sessions.find(votable_id)
|
|
114
|
-
else raise ActiveRecord::RecordNotFound, "Unknown credit_votable_type: #{show.credit_votable_type}"
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
end
|