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,113 +0,0 @@
1
- # Snapshots final vote counts from cm_votes into:
2
- # - cm_voting_contestants: vote_count, unique_voter_count (per contestant)
3
- # - cm_voting_session_stats: unique_voter_count, total_vote_count, vote_velocity, leading_contestant_id
4
- #
5
- # Called after a voting session closes so the leaderboard can be read from DB
6
- # permanently — no dependency on Redis keys that may expire.
7
- #
8
- # Usage:
9
- # SpreeCmCommissioner::VoteCounters::SnapshotToDb.call(voting_session_id: id)
10
- module SpreeCmCommissioner
11
- module VoteCounters
12
- class SnapshotToDb
13
- prepend ::Spree::ServiceModule::Base
14
-
15
- def call(voting_session_id:)
16
- voting_session = SpreeCmCommissioner::VotingSession.find(voting_session_id)
17
-
18
- # TODO: manual/judge-driven sessions (scoring_model: :manual) should skip vote tallying
19
- # and derive leading_contestant_id from Advancer data (contestants not eliminated) instead.
20
- # Will be implemented in the next PR alongside the full scoring_model enum.
21
-
22
- # Sum all votes per contestant and count how many distinct users voted per contestant.
23
- db_counts = fetch_vote_counts(voting_session_id)
24
- unique_voter_counts = fetch_unique_voter_counts(voting_session_id)
25
-
26
- # Persist per-contestant tallies, then write session-level aggregates.
27
- upsert_contestant_counts(voting_session_id, db_counts, unique_voter_counts)
28
- upsert_session_stat(voting_session, db_counts)
29
-
30
- success(nil)
31
- end
32
-
33
- private
34
-
35
- # Returns { contestant_id => total_quantity } for every vote in the session.
36
- def fetch_vote_counts(voting_session_id)
37
- SpreeCmCommissioner::Vote.for_session(voting_session_id)
38
- .group(:contestant_id)
39
- .sum(:quantity)
40
- end
41
-
42
- # Returns { contestant_id => distinct_user_count } — anonymous votes (user_id nil) are excluded.
43
- def fetch_unique_voter_counts(voting_session_id)
44
- SpreeCmCommissioner::Vote.for_session(voting_session_id)
45
- .where.not(user_id: nil)
46
- .group(:contestant_id)
47
- .distinct
48
- .count(:user_id)
49
- end
50
-
51
- # Writes vote_count + unique_voter_count to each contestant row.
52
- # Uses update_all (not upsert_all) because contestants always pre-exist —
53
- # upsert_all's INSERT path would violate NOT NULL constraints on other columns.
54
- def upsert_contestant_counts(voting_session_id, db_counts, unique_voter_counts)
55
- updates = build_contestant_updates(voting_session_id, db_counts, unique_voter_counts)
56
- updates.each do |attrs|
57
- SpreeCmCommissioner::VotingContestant
58
- .where(id: attrs[:id])
59
- .update_all(vote_count: attrs[:vote_count], unique_voter_count: attrs[:unique_voter_count]) # rubocop:disable Rails/SkipsModelValidations
60
- end
61
- end
62
-
63
- # Maps each contestant ID to its tallied counts; defaults to 0 when no votes exist.
64
- def build_contestant_updates(voting_session_id, db_counts, unique_voter_counts)
65
- SpreeCmCommissioner::VotingContestant
66
- .where(voting_session_id: voting_session_id)
67
- .pluck(:id)
68
- .map do |id|
69
- {
70
- id: id,
71
- vote_count: db_counts[id].to_i,
72
- unique_voter_count: unique_voter_counts[id].to_i
73
- }
74
- end
75
- end
76
-
77
- # Calculates session-level aggregates and upserts the VotingSessionStat record.
78
- def upsert_session_stat(voting_session, db_counts)
79
- total_vote_count = db_counts.values.sum
80
- unique_voter_count = fetch_session_unique_voter_count(voting_session.id)
81
- leading_contestant_id = find_leading_contestant_id(db_counts)
82
- vote_velocity = calculate_vote_velocity(voting_session, total_vote_count)
83
-
84
- stat = SpreeCmCommissioner::VotingSessionStat.find_or_initialize_by(voting_session_id: voting_session.id)
85
- stat.update!(
86
- unique_voter_count: unique_voter_count,
87
- total_vote_count: total_vote_count,
88
- vote_velocity: vote_velocity,
89
- leading_contestant_id: leading_contestant_id
90
- )
91
- end
92
-
93
- # Counts distinct users who cast at least one vote in the session (cross-contestant deduplication).
94
- def fetch_session_unique_voter_count(voting_session_id)
95
- SpreeCmCommissioner::Vote.for_session(voting_session_id)
96
- .where.not(user_id: nil)
97
- .distinct
98
- .count(:user_id)
99
- end
100
-
101
- # Returns the contestant_id with the highest vote total, or nil when db_counts is empty.
102
- def find_leading_contestant_id(db_counts)
103
- db_counts.max_by { |_, v| v }&.first
104
- end
105
-
106
- # Votes per minute over the session window; minimum 1 minute prevents division by zero.
107
- def calculate_vote_velocity(voting_session, total_vote_count)
108
- duration_minutes = [(voting_session.closes_at - voting_session.opens_at) / 60.0, 1].max
109
- (total_vote_count / duration_minutes).round(2)
110
- end
111
- end
112
- end
113
- end
@@ -1,68 +0,0 @@
1
- # Deducts voting credits for one vote submission.
2
- #
3
- # Finds the eligible VotingCredit for the user scoped to the session's show,
4
- # locks the row, deducts free votes first then paid, and records transactions.
5
- # Raises RuntimeError on any business-rule violation so the caller rolls back.
6
- module SpreeCmCommissioner
7
- class VoteCreditDeductor
8
- attr_reader :voting_session, :contestant, :user, :quantity
9
-
10
- def initialize(
11
- voting_session:, # VotingSession the vote belongs to
12
- contestant:, # VotingContestant being voted for
13
- user:, # Spree::User casting the vote
14
- quantity: # number of votes to cast
15
- )
16
- @voting_session = voting_session
17
- @contestant = contestant
18
- @user = user
19
- @quantity = quantity
20
- end
21
-
22
- # Returns the VotingCredit that was deducted.
23
- def call
24
- credit = find_eligible_credit
25
- raise 'No eligible voting credits available' if credit.nil?
26
-
27
- credit.with_lock do
28
- raise 'Insufficient voting credits' unless credit.can_use?(quantity)
29
-
30
- # Deduct free votes first, then paid.
31
- # Example: quantity=5, free_remaining=3, paid_remaining=10
32
- # free_deduct=3, paid_deduct=2
33
- free_deduct = [credit.free_votes_remaining, quantity].min
34
- paid_deduct = quantity - free_deduct
35
-
36
- credit.update!(
37
- free_votes_used: credit.free_votes_used + free_deduct,
38
- paid_votes_used: credit.paid_votes_used + paid_deduct
39
- )
40
-
41
- memo = "Used #{quantity}, paid: #{paid_deduct}, free: #{free_deduct} votes for voting session #{voting_session.id}"
42
- SpreeCmCommissioner::VotingCreditTransaction.create!(
43
- tenant_id: credit.tenant_id,
44
- vote_credit: credit,
45
- action: :use,
46
- amount: quantity,
47
- originator: voting_session,
48
- episode_id: voting_session.episode_id,
49
- user_total_amount: credit.available_votes,
50
- memo: memo
51
- )
52
- end
53
-
54
- credit
55
- end
56
-
57
- private
58
-
59
- def find_eligible_credit
60
- votable_type = voting_session.show.credit_votable_type
61
-
62
- user.voting_credits
63
- .eligible(voting_session)
64
- .where(votable_type: votable_type)
65
- .first
66
- end
67
- end
68
- end
@@ -1,145 +0,0 @@
1
- module SpreeCmCommissioner
2
- module VotePackage
3
- class Create
4
- prepend Spree::ServiceModule::Base
5
-
6
- def call(params:)
7
- @params = params
8
-
9
- # First build the vote package object
10
- build_vote_package
11
-
12
- # Prepare data outside transaction to reduce lock time
13
- assign_store
14
- assign_season
15
- set_public_metadata
16
- set_option_value
17
-
18
- # Only include actual database writes in the transaction
19
- ActiveRecord::Base.transaction do
20
- save_vote_package
21
- create_variant
22
- create_stock_item
23
- end
24
-
25
- success(vote_package: @vote_package)
26
- rescue StandardError => e
27
- failure(nil, e.message)
28
- end
29
-
30
- private
31
-
32
- def build_vote_package
33
- @vote_package = SpreeCmCommissioner::ShowEpisode.new(vote_package_params)
34
- @vote_package.product_type = :vote_package
35
- end
36
-
37
- def save_vote_package
38
- return if @vote_package.save
39
-
40
- raise @vote_package.errors.full_messages.join(', ')
41
- end
42
-
43
- def assign_store
44
- @store = Spree::Store.default
45
- @vote_package.stores << @store if @vote_package.stores.empty?
46
- end
47
-
48
- def assign_season
49
- @season = Spree::Taxon.find_by(id: @params[:event_id])
50
- raise 'Season not found.' unless @season
51
-
52
- @vote_package.event_id = @season.id
53
- @vote_package.taxons = [@season]
54
- @vote_package.vendor = @season.vendor || @params[:vendor]
55
- @vote_package.shipping_category = Spree::ShippingCategory.first_or_create!(name: 'Default')
56
- end
57
-
58
- def set_public_metadata
59
- @vote_package.votable_type = @params[:votable_type]
60
- @vote_package.votable_id = @params[:votable_id] if @params[:votable_id].present?
61
- end
62
-
63
- def set_option_value
64
- @option_type = Spree::OptionType.find_or_create_by!(
65
- name: 'vote-package',
66
- presentation: 'Vote Package'
67
- )
68
-
69
- @color_option_type = Spree::OptionType.find_or_create_by!(
70
- name: 'color',
71
- presentation: 'Color'
72
- )
73
-
74
- @label_option_type = Spree::OptionType.find_or_create_by!(
75
- name: 'label',
76
- presentation: 'Label'
77
- )
78
-
79
- @option_type.update(attr_type: 'integer') if @option_type.respond_to?(:attr_type)
80
- @color_option_type.update(attr_type: 'color') if @color_option_type.respond_to?(:attr_type)
81
- @label_option_type.update(attr_type: 'string') if @label_option_type.respond_to?(:attr_type)
82
-
83
- # auto assign the option types to the vote package
84
- @vote_package.option_types << @option_type unless @vote_package.option_types.include?(@option_type)
85
- @vote_package.option_types << @color_option_type unless @vote_package.option_types.include?(@color_option_type)
86
- @vote_package.option_types << @label_option_type unless @vote_package.option_types.include?(@label_option_type)
87
-
88
- # get vote credit options from params for scalability
89
- # can be either an array of options or a single number
90
- raise 'Vote credits is required.' if @params[:vote_credits].blank?
91
-
92
- vote_credit_options = [
93
- {
94
- name: @params[:vote_credits].to_s,
95
- presentation: "#{@params[:vote_credits]} Vote Credits"
96
- }
97
- ]
98
-
99
- @option_values = vote_credit_options.map do |option|
100
- Spree::OptionValue.find_or_create_by!(
101
- name: option[:name],
102
- presentation: option[:presentation],
103
- option_type_id: @option_type.id
104
- )
105
- end
106
- end
107
-
108
- def create_variant
109
- @variant = @vote_package.variants.new(
110
- price: @vote_package.price,
111
- compare_at_price: @vote_package.compare_at_price,
112
- option_value_ids: @option_values.map(&:id)
113
- )
114
-
115
- return if @variant.save
116
-
117
- raise @variant.errors.full_messages.join(', ')
118
- end
119
-
120
- def create_stock_item
121
- stock_movement_context = SpreeCmCommissioner::Stock::StockMovementCreator.call(
122
- variant_id: @variant.id,
123
- stock_location_id: @params[:stock_location_id],
124
- current_store: @store,
125
- stock_movement_params: { quantity: @params[:count_on_hand] }
126
- )
127
-
128
- return if stock_movement_context.success?
129
-
130
- raise stock_movement_context.message
131
- end
132
-
133
- def vote_package_params
134
- {
135
- name: "#{@params[:vote_credits]} Vote Packs",
136
- price: @params[:price],
137
- compare_at_price: @params[:compare_at_price],
138
- available_on: @params[:available_on],
139
- discontinue_on: @params[:discontinue_on],
140
- status: @params[:status]
141
- }
142
- end
143
- end
144
- end
145
- end
@@ -1,91 +0,0 @@
1
- module SpreeCmCommissioner
2
- module VotePackage
3
- class Update
4
- prepend Spree::ServiceModule::Base
5
-
6
- def call(vote_package:, params:, place_params: nil, option_values_attributes: {})
7
- @vote_package = vote_package
8
- @params = params
9
- @place_params = place_params || params
10
- @option_values_attributes = option_values_attributes.presence || @params.dig(:variant, :option_values_attributes) || {}
11
-
12
- ActiveRecord::Base.transaction do
13
- set_public_metadata
14
- update_variant_option_values
15
- update_vote_package
16
- update_variant_price
17
- end
18
-
19
- success(vote_package: @vote_package)
20
- rescue StandardError => e
21
- failure(nil, e.message)
22
- end
23
-
24
- private
25
-
26
- def set_public_metadata
27
- @vote_package.votable_type = @params[:votable_type] if @params[:votable_type].present?
28
- @vote_package.votable_id = @params[:votable_id] if @params[:votable_id].present?
29
- end
30
-
31
- def update_vote_package
32
- return if @vote_package.update(vote_package_params)
33
-
34
- raise @vote_package.errors.full_messages.join(', ')
35
- end
36
-
37
- def update_variant_option_values
38
- variant = @vote_package.variants.first
39
- return unless variant
40
- return if @option_values_attributes.blank?
41
-
42
- option_values = @option_values_attributes.values
43
- variant.option_values = option_values.each_with_object([]) do |option_value, new_option_values|
44
- option_type_id = option_value[:option_type_id] || option_value['option_type_id']
45
- option_value_name = validated_option_value_name(option_value[:name] || option_value['name'], option_type_id)
46
- next if option_value_name.blank?
47
-
48
- option_type = @vote_package.option_types.find(option_type_id)
49
- existing_option_value = option_type.option_values.find_or_create_by(name: option_value_name) do |new_option_value|
50
- new_option_value.presentation = option_value_name.to_s.strip.titleize
51
- end
52
-
53
- new_option_values << existing_option_value
54
- end
55
-
56
- variant.save!
57
- end
58
-
59
- def update_variant_price
60
- variant = @vote_package.variants.first
61
- return unless variant
62
-
63
- return if variant.update(
64
- price: @vote_package.price,
65
- compare_at_price: @vote_package.compare_at_price
66
- )
67
-
68
- raise variant.errors.full_messages.join(', ')
69
- end
70
-
71
- def validated_option_value_name(name, option_type_id)
72
- return nil if name.blank?
73
-
74
- option_value = Spree::OptionValue.new(name: name, option_type_id: option_type_id)
75
- option_value.validate
76
- option_value.name
77
- end
78
-
79
- def vote_package_params
80
- {
81
- name: @params[:name],
82
- price: @params[:price],
83
- compare_at_price: @params[:compare_at_price],
84
- available_on: @params[:available_on],
85
- discontinue_on: @params[:discontinue_on],
86
- status: @params[:status]
87
- }
88
- end
89
- end
90
- end
91
- end
@@ -1,144 +0,0 @@
1
- # Domain service: orchestrates a single vote submission end-to-end.
2
- #
3
- # Responsibilities (in order):
4
- # 1. Guard-clause validation (session open, contestant scoped, credit user check)
5
- # 2. Fraud detection hook (stub – implement rate-limiting here)
6
- # 3. Credit deduction via VoteCreditDeductor (free votes first, then paid)
7
- # 4. Vote persistence (cm_votes)
8
- # 5. Redis leaderboard increment
9
- #
10
- # Returns a Result value object – never raises to caller.
11
- # Steps 3-4 run inside a single DB transaction so a credit is never
12
- # deducted without a persisted vote row.
13
- # Step 5 (Redis) runs outside the transaction. A Redis failure is logged but
14
- # does NOT fail the request — the DB commit is the source of truth and counters
15
- # can be rebuilt via VoteCounters::RebuildFromDb.
16
- module SpreeCmCommissioner
17
- class VoteProcessor
18
- prepend ::Spree::ServiceModule::Base
19
-
20
- attr_reader :voting_session, :contestant, :user, :params, :request
21
-
22
- def call(voting_session:, contestant:, user:, params: {}, request: nil)
23
- @voting_session = voting_session
24
- @contestant = contestant
25
- @user = user
26
- @params = params
27
- @request = request
28
-
29
- validate!
30
- vote = nil
31
-
32
- ActiveRecord::Base.transaction do
33
- check_fraud!
34
- credit = allocate_credit
35
- vote = persist_vote!(credit)
36
- end
37
-
38
- increment_redis_counter(vote)
39
-
40
- success(vote)
41
- rescue RuntimeError, ActiveRecord::RecordInvalid, ArgumentError => e
42
- failure(nil, e.message)
43
- rescue StandardError => e
44
- CmAppLogger.error(
45
- label: 'VoteProcessor unexpected error',
46
- data: {
47
- voting_session_id: voting_session.id,
48
- contestant_id: contestant.id,
49
- user_id: user&.id,
50
- error_class: e.class.name,
51
- error_message: e.message,
52
- backtrace: e.backtrace&.first(5)&.join("\n")
53
- }
54
- )
55
-
56
- failure(nil, 'Vote could not be processed')
57
- end
58
-
59
- private
60
-
61
- def validate!
62
- raise 'Voting session is not active' unless voting_session.can_vote?
63
- raise 'Contestant does not belong to this voting session' unless contestant_belongs_to_session?
64
- raise 'Authenticated user is required' if user.nil?
65
- raise 'Quantity must be a positive integer' unless quantity.positive?
66
- end
67
-
68
- def contestant_belongs_to_session?
69
- voting_session.voting_contestants.exists?(id: contestant.id)
70
- end
71
-
72
- def check_fraud!
73
- result = SpreeCmCommissioner::FraudCheck.call(
74
- voting_session: voting_session,
75
- contestant: contestant,
76
- user: user,
77
- params: params,
78
- request: request
79
- )
80
-
81
- raise result.error.value if result.failure?
82
- end
83
-
84
- # Redis increment is best-effort: a failure here must not roll back the
85
- # committed vote row or trigger a client retry (which would double-count).
86
- # RebuildFromDb can reconcile counters if drift is detected.
87
- def increment_redis_counter(vote)
88
- SpreeCmCommissioner::VoteCounters::Increment.call(
89
- voting_session_id: voting_session.id,
90
- contestant_id: contestant.id,
91
- quantity: quantity
92
- )
93
- rescue StandardError => e
94
- CmAppLogger.error(
95
- label: 'VoteProcessor Redis increment failed',
96
- data: {
97
- vote_id: vote.id,
98
- voting_session_id: voting_session.id,
99
- contestant_id: contestant.id,
100
- error_class: e.class.name,
101
- error_message: e.message
102
- }
103
- )
104
-
105
- # Create the maintenance task row so OrchestrateJob picks it up once Redis recovers.
106
- # async_execute is intentionally skipped here — it enqueues via Sidekiq which also
107
- # needs Redis, so it would always fail when Redis is down.
108
- SpreeCmCommissioner::MaintenanceTasks::VotingSession.pending.find_or_create_by(
109
- maintainable_type: 'SpreeCmCommissioner::VotingSession',
110
- maintainable_id: voting_session.id
111
- )
112
- end
113
-
114
- def allocate_credit
115
- SpreeCmCommissioner::VoteCreditDeductor.new(
116
- voting_session: voting_session,
117
- contestant: contestant,
118
- user: user,
119
- quantity: quantity
120
- ).call
121
- end
122
-
123
- def persist_vote!(credit)
124
- SpreeCmCommissioner::Vote.create!(
125
- tenant_id: params[:tenant_id],
126
- vendor_id: params[:vendor_id],
127
- voting_session: voting_session,
128
- contestant: contestant,
129
- user: user,
130
- vote_credit: credit,
131
- voter_identifier: params[:voter_identifier],
132
- channel: params[:channel],
133
- quantity: quantity,
134
- device_fingerprint: params[:device_fingerprint],
135
- ip_address: request&.remote_ip,
136
- public_metadata: params[:public_metadata] || {}
137
- )
138
- end
139
-
140
- def quantity
141
- @quantity ||= params[:quantity].to_i
142
- end
143
- end
144
- end