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.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -4
  3. data/Gemfile.lock +1 -1
  4. data/app/controllers/spree/admin/homepage_section_controller.rb +1 -4
  5. data/app/controllers/spree/admin/taxons_controller_decorator.rb +0 -19
  6. data/app/controllers/spree/api/v2/storefront/homepage_sections_controller.rb +0 -1
  7. data/app/controllers/spree/api/v2/tenant/base_controller.rb +0 -4
  8. data/app/controllers/spree/api/v2/tenant/homepage_sections_controller.rb +0 -1
  9. data/app/controllers/spree/api/v2/tenant/products_controller.rb +1 -1
  10. data/app/controllers/spree/api/v2/tenant/taxons_controller.rb +1 -1
  11. data/app/controllers/spree_cm_commissioner/admin/products_controller_decorator.rb +0 -19
  12. data/app/finders/spree_cm_commissioner/events/find_matches.rb +0 -1
  13. data/app/helpers/spree_cm_commissioner/admin/homepage_segment_helper.rb +0 -2
  14. data/app/models/concerns/spree_cm_commissioner/homepage_section_bitwise.rb +1 -2
  15. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +1 -2
  16. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +0 -10
  17. data/app/models/concerns/spree_cm_commissioner/product_type.rb +1 -1
  18. data/app/models/spree_cm_commissioner/product_decorator.rb +0 -39
  19. data/app/models/spree_cm_commissioner/role_decorator.rb +1 -4
  20. data/app/models/spree_cm_commissioner/taxon_decorator.rb +0 -15
  21. data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +1 -10
  22. data/app/models/spree_cm_commissioner/tenant.rb +0 -9
  23. data/app/models/spree_cm_commissioner/user_decorator.rb +0 -5
  24. data/app/models/spree_cm_commissioner/variant_options.rb +0 -4
  25. data/app/models/spree_cm_commissioner/vendor_decorator.rb +0 -4
  26. data/app/serializers/spree/v2/storefront/homepage_section_serializer.rb +1 -1
  27. data/app/serializers/spree/v2/storefront/product_serializer_decorator.rb +1 -1
  28. data/app/serializers/spree/v2/storefront/role_serializer.rb +1 -1
  29. data/app/serializers/spree/v2/storefront/taxon_serializer_decorator.rb +1 -2
  30. data/app/serializers/spree/v2/tenant/homepage_section_serializer.rb +1 -1
  31. data/app/serializers/spree/v2/tenant/role_serializer.rb +1 -1
  32. data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +0 -12
  33. data/app/views/spree/admin/homepage_section/_form.html.erb +0 -5
  34. data/config/initializers/spree_permitted_attributes.rb +0 -8
  35. data/config/locales/en.yml +0 -14
  36. data/config/locales/km.yml +0 -10
  37. data/config/routes.rb +0 -26
  38. data/lib/spree_cm_commissioner/version.rb +1 -1
  39. data/lib/spree_cm_commissioner.rb +1 -7
  40. data/spree_cm_commissioner.gemspec +1 -1
  41. metadata +4 -116
  42. data/app/controllers/spree/api/v2/storefront/preview_products_controller.rb +0 -48
  43. data/app/controllers/spree/api/v2/storefront/preview_sections_controller.rb +0 -27
  44. data/app/controllers/spree/api/v2/storefront/preview_taxons_controller.rb +0 -18
  45. data/app/controllers/spree/api/v2/storefront/products_controller_decorator.rb +0 -15
  46. data/app/controllers/spree/api/v2/storefront/taxons_controller_decorator.rb +0 -15
  47. data/app/controllers/spree/api/v2/tenant/free_vote_claims_controller.rb +0 -37
  48. data/app/controllers/spree/api/v2/tenant/preview_products_controller.rb +0 -47
  49. data/app/controllers/spree/api/v2/tenant/preview_sections_controller.rb +0 -26
  50. data/app/controllers/spree/api/v2/tenant/preview_shows_controller.rb +0 -19
  51. data/app/controllers/spree/api/v2/tenant/preview_taxons_controller.rb +0 -19
  52. data/app/controllers/spree/api/v2/tenant/show_contestants_controller.rb +0 -52
  53. data/app/controllers/spree/api/v2/tenant/show_elimination_sessions_controller.rb +0 -57
  54. data/app/controllers/spree/api/v2/tenant/show_people_controller.rb +0 -49
  55. data/app/controllers/spree/api/v2/tenant/show_person_assignments_controller.rb +0 -36
  56. data/app/controllers/spree/api/v2/tenant/shows_controller.rb +0 -34
  57. data/app/controllers/spree/api/v2/tenant/votes_controller.rb +0 -94
  58. data/app/controllers/spree/api/v2/tenant/voting_contestants_controller.rb +0 -40
  59. data/app/controllers/spree/api/v2/tenant/voting_credit_transactions_controller.rb +0 -41
  60. data/app/controllers/spree/api/v2/tenant/voting_credits_controller.rb +0 -31
  61. data/app/jobs/spree_cm_commissioner/vote_fraud_event_job.rb +0 -9
  62. data/app/jobs/spree_cm_commissioner/voting_credit_allocation_job.rb +0 -10
  63. data/app/jobs/spree_cm_commissioner/voting_credit_de_allocation_job.rb +0 -10
  64. data/app/models/spree_cm_commissioner/maintenance_tasks/voting_session.rb +0 -36
  65. data/app/models/spree_cm_commissioner/preview_role.rb +0 -8
  66. data/app/models/spree_cm_commissioner/role_user_decorator.rb +0 -8
  67. data/app/models/spree_cm_commissioner/show.rb +0 -159
  68. data/app/models/spree_cm_commissioner/show_contestant.rb +0 -39
  69. data/app/models/spree_cm_commissioner/show_contestant_image.rb +0 -11
  70. data/app/models/spree_cm_commissioner/show_contestant_video.rb +0 -15
  71. data/app/models/spree_cm_commissioner/show_episode.rb +0 -135
  72. data/app/models/spree_cm_commissioner/show_person.rb +0 -15
  73. data/app/models/spree_cm_commissioner/show_person_assignment.rb +0 -20
  74. data/app/models/spree_cm_commissioner/show_person_image.rb +0 -11
  75. data/app/models/spree_cm_commissioner/vote.rb +0 -16
  76. data/app/models/spree_cm_commissioner/vote_fraud_event.rb +0 -19
  77. data/app/models/spree_cm_commissioner/voting_contestant.rb +0 -46
  78. data/app/models/spree_cm_commissioner/voting_credit.rb +0 -72
  79. data/app/models/spree_cm_commissioner/voting_credit_transaction.rb +0 -55
  80. data/app/models/spree_cm_commissioner/voting_session.rb +0 -223
  81. data/app/models/spree_cm_commissioner/voting_session_stat.rb +0 -8
  82. data/app/overrides/spree/admin/products/_form/preview_checkbox.html.erb.deface +0 -9
  83. data/app/overrides/spree/admin/taxons/_form/preview_checkbox.html.erb.deface +0 -7
  84. data/app/serializers/spree/v2/tenant/show_contestant_serializer.rb +0 -21
  85. data/app/serializers/spree/v2/tenant/show_episode_serializer.rb +0 -17
  86. data/app/serializers/spree/v2/tenant/show_person_assignment_serializer.rb +0 -16
  87. data/app/serializers/spree/v2/tenant/show_person_serializer.rb +0 -13
  88. data/app/serializers/spree/v2/tenant/show_serializer.rb +0 -26
  89. data/app/serializers/spree/v2/tenant/video_serializer.rb +0 -9
  90. data/app/serializers/spree/v2/tenant/vote_serializer.rb +0 -14
  91. data/app/serializers/spree/v2/tenant/voting_contestant_serializer.rb +0 -22
  92. data/app/serializers/spree/v2/tenant/voting_credit_serializer.rb +0 -10
  93. data/app/serializers/spree/v2/tenant/voting_credit_transaction_serializer.rb +0 -14
  94. data/app/serializers/spree/v2/tenant/voting_session_serializer.rb +0 -18
  95. data/app/services/spree_cm_commissioner/fraud_check.rb +0 -279
  96. data/app/services/spree_cm_commissioner/show_contestants/normalize_video_highlights.rb +0 -57
  97. data/app/services/spree_cm_commissioner/url_embed/youtube_embed.rb +0 -44
  98. data/app/services/spree_cm_commissioner/vote_counters/audit_counters.rb +0 -43
  99. data/app/services/spree_cm_commissioner/vote_counters/base.rb +0 -31
  100. data/app/services/spree_cm_commissioner/vote_counters/increment.rb +0 -44
  101. data/app/services/spree_cm_commissioner/vote_counters/per_contestant_counter.rb +0 -68
  102. data/app/services/spree_cm_commissioner/vote_counters/rebuild_from_db.rb +0 -70
  103. data/app/services/spree_cm_commissioner/vote_counters/snapshot_to_db.rb +0 -113
  104. data/app/services/spree_cm_commissioner/vote_credit_deductor.rb +0 -68
  105. data/app/services/spree_cm_commissioner/vote_package/create.rb +0 -145
  106. data/app/services/spree_cm_commissioner/vote_package/update.rb +0 -91
  107. data/app/services/spree_cm_commissioner/vote_processor.rb +0 -144
  108. data/app/services/spree_cm_commissioner/voting_contestants/advancer.rb +0 -334
  109. data/app/services/spree_cm_commissioner/voting_contestants/assigner.rb +0 -32
  110. data/app/services/spree_cm_commissioner/voting_contestants/bulk_updater.rb +0 -106
  111. data/app/services/spree_cm_commissioner/voting_credits/allocate.rb +0 -77
  112. data/app/services/spree_cm_commissioner/voting_credits/claim_free_votes.rb +0 -119
  113. data/app/services/spree_cm_commissioner/voting_credits/credit_calculator.rb +0 -35
  114. data/app/services/spree_cm_commissioner/voting_credits/de_allocate.rb +0 -87
  115. data/app/services/spree_cm_commissioner/voting_leaderboards/calculate_score.rb +0 -74
  116. data/app/services/spree_cm_commissioner/voting_sessions/finalize.rb +0 -66
  117. data/db/migrate/20260309230148_create_cm_show_people.rb +0 -14
  118. data/db/migrate/20260309230149_create_cm_show_people_assignments.rb +0 -16
  119. data/db/migrate/20260310082711_create_cm_show_contestants.rb +0 -28
  120. data/db/migrate/20260310082720_create_cm_voting_sessions.rb +0 -21
  121. data/db/migrate/20260310082721_create_cm_voting_contestants.rb +0 -23
  122. data/db/migrate/20260310082734_add_voting_fields_to_spree_taxons.rb +0 -9
  123. data/db/migrate/20260310082735_add_type_to_spree_products.rb +0 -6
  124. data/db/migrate/20260310082749_create_cm_voting_credits.rb +0 -27
  125. data/db/migrate/20260326080200_create_cm_voting_credit_transactions.rb +0 -27
  126. data/db/migrate/20260330160000_create_cm_votes.rb +0 -25
  127. data/db/migrate/20260401072500_add_advanced_from_to_cm_voting_contestants.rb +0 -7
  128. data/db/migrate/20260402000001_add_voting_credit_scope_to_spree_taxons.rb +0 -6
  129. data/db/migrate/20260402000002_rename_scopeable_to_votable_in_cm_voting_credits.rb +0 -12
  130. data/db/migrate/20260403070000_add_name_to_cm_voting_sessions.rb +0 -5
  131. data/db/migrate/20260406000001_add_vendor_id_to_voting_tables.rb +0 -6
  132. data/db/migrate/20260406000001_rename_votes_remaining_to_amount_in_cm_voting_credits.rb +0 -11
  133. data/db/migrate/20260408085255_add_show_id_and_vendor_id_to_cm_voting_sessions.rb +0 -9
  134. data/db/migrate/20260420000001_rename_type_to_credit_type_in_cm_voting_credits.rb +0 -25
  135. data/db/migrate/20260422000001_create_cm_vote_fraud_events.rb +0 -23
  136. data/db/migrate/20260423000001_add_preview_to_taxons_products_and_sections.rb +0 -11
  137. data/db/migrate/20260423000002_create_preview_roles.rb +0 -24
  138. data/db/migrate/20260515120000_add_public_metadata_to_cm_voting_sessions.rb +0 -5
  139. data/db/migrate/20260518090920_add_unique_voter_count_to_voting_contestants.rb +0 -5
  140. data/db/migrate/20260518094322_create_cm_voting_session_stats.rb +0 -17
  141. data/db/migrate/20260520000001_add_scoring_model_to_cm_voting_sessions.rb +0 -5
  142. data/db/migrate/20260520000001_optimize_cm_votes_indexes.rb +0 -22
  143. data/db/migrate/20260525042257_add_vote_number_to_cm_voting_contestants.rb +0 -18
  144. data/db/migrate/20260527035430_add_confirmed_rank_to_cm_voting_contestants.rb +0 -5
  145. data/db/migrate/20260527062005_add_eliminated_at_to_cm_show_contestants.rb +0 -5
  146. data/docs/sql/jsonb_query_guide.md +0 -57
  147. data/lib/spree_cm_commissioner/test_helper/factories/show_episode_factory.rb +0 -12
  148. data/lib/spree_cm_commissioner/test_helper/factories/show_factory.rb +0 -120
  149. data/lib/spree_cm_commissioner/test_helper/factories/vote_credit_factory.rb +0 -37
  150. data/lib/spree_cm_commissioner/test_helper/factories/vote_factory.rb +0 -28
  151. data/lib/spree_cm_commissioner/test_helper/factories/voting_credit_transaction_factory.rb +0 -11
  152. 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