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,159 +0,0 @@
1
- module SpreeCmCommissioner
2
- class Show < Spree::Taxon
3
- include SpreeCmCommissioner::StoreMetadata
4
-
5
- has_many :voting_credits, class_name: 'SpreeCmCommissioner::VotingCredit', as: :votable, dependent: :destroy
6
- belongs_to :show, class_name: 'SpreeCmCommissioner::Show', foreign_key: :parent_id, optional: true
7
- has_many :seasons, class_name: 'SpreeCmCommissioner::Show', foreign_key: :parent_id, inverse_of: :parent
8
- has_many :show_contestants, class_name: 'SpreeCmCommissioner::ShowContestant', inverse_of: :show, dependent: :destroy
9
- has_many :show_people_assignments, class_name: 'SpreeCmCommissioner::ShowPersonAssignment', dependent: :destroy
10
- has_many :show_people, through: :show_people_assignments, source: :show_person
11
- has_many :episodes, class_name: 'SpreeCmCommissioner::ShowEpisode', foreign_key: :event_id
12
- has_many :voting_sessions, through: :episodes, class_name: 'SpreeCmCommissioner::VotingSession'
13
- has_one :current_episode, -> { current_or_next_upcoming }, class_name: 'SpreeCmCommissioner::ShowEpisode', foreign_key: :event_id
14
- has_one :current_voting_session, through: :current_episode
15
-
16
- before_validation { self.type = self.class.name }
17
- before_validation { self.kind = :event }
18
- before_validation :assign_taxonomy_and_parent, on: :create
19
-
20
- store_private_metadata :season_number, :integer
21
- store_private_metadata :season_year, :integer
22
-
23
- # Define standard voting configuration keys
24
- store_accessor :voting_config,
25
- :free_vote_limit,
26
- :free_vote_limit_type,
27
- :paid_vote_enabled,
28
- :max_paid_votes_per_session,
29
- :sms_vote_enabled,
30
- :sms_vote_cost_usd,
31
- :operator_vote_enabled,
32
- :votes_weight,
33
- :voters_weight
34
-
35
- # voting_credit_scope determines how purchased vote credits are grouped (scoped).
36
- # Changing this after credits have been allocated will cause inconsistencies:
37
- # - existing credits are scoped to the old type (e.g. ShowEpisode),
38
- # but new purchases will create credits under the new type (e.g. Show),
39
- # leading to split balances that cannot be merged.
40
- # UI rule: once voting_credit_scope is set and any VotingCredit exists for this show, the field must be read-only — do not allow changes.
41
- # Note: VoteCredit of user should base on CREDIT_SCOPES
42
- # - if scope is Show, User's credits should be only 1 per show, and can be used for any episode under the show.
43
- # - if scope is ShowEpisode, User's credits should be N per show (N is the number of episodes), and can be only used for specific episode.
44
- # - if scope is VotingSession, User's credits should be N per show (N is the number of voting sessions), and can be only used for specific voting session. # rubocop:disable Layout/LineLength
45
- CREDIT_SCOPES = {
46
- 'Show' => { free_vote_limit_type: 'per_show' }.freeze,
47
- 'ShowEpisode' => { free_vote_limit_type: 'per_episode' }.freeze,
48
- 'VotingSession' => { free_vote_limit_type: 'per_voting_session' }.freeze
49
- }.freeze
50
-
51
- def self.polymorphic_name
52
- name
53
- end
54
-
55
- validate :voting_credit_scope_immutable, if: :voting_credit_scope_changed?
56
-
57
- def credit_votable_type
58
- voting_credit_scope = self[:voting_credit_scope] || 'ShowEpisode'
59
-
60
- type = CREDIT_SCOPES.key?(voting_credit_scope) ? voting_credit_scope : 'ShowEpisode'
61
- "SpreeCmCommissioner::#{type}"
62
- end
63
-
64
- def free_vote_idempotency_key(user_id:, votable_id: nil)
65
- case free_vote_limit_type
66
- when 'per_episode' then "free_claim::episode::#{votable_id}::user::#{user_id}"
67
- when 'per_voting_session' then "free_claim::voting_session::#{votable_id}::user::#{user_id}"
68
- when 'per_show' then "free_claim::show::#{id}::user::#{user_id}"
69
- else raise ArgumentError, "Invalid free_vote_limit_type: #{free_vote_limit_type}"
70
- end
71
- end
72
-
73
- # Define standard fraud configuration keys
74
- store_accessor :fraud_config,
75
- :max_votes_per_minute_per_user,
76
- :max_votes_per_minute_per_ip,
77
- :max_accounts_per_device,
78
- :block_vpn
79
-
80
- def show?
81
- depth == 1
82
- end
83
-
84
- def season?
85
- depth == 2
86
- end
87
-
88
- def display_name
89
- if season? && parent.present?
90
- "#{parent.name} #{name}"
91
- else
92
- name
93
- end
94
- end
95
-
96
- def contestants
97
- show_contestants
98
- end
99
-
100
- def effective_voting_config
101
- config = parse_json_config(voting_config)
102
- return config if show?
103
-
104
- config.reverse_merge(parent&.effective_voting_config || {})
105
- end
106
-
107
- def effective_fraud_config
108
- config = parse_json_config(fraud_config)
109
- return config if show?
110
-
111
- config.reverse_merge(parent&.effective_fraud_config || {})
112
- end
113
-
114
- def build_next_season(attributes = {})
115
- last_season = seasons.order(created_at: :desc).first
116
-
117
- default_number = (last_season&.season_number || 0) + 1
118
- default_year = last_season&.season_year || Time.current.year
119
-
120
- seasons.new(
121
- {
122
- season_number: default_number,
123
- season_year: default_year
124
- }.merge(attributes)
125
- )
126
- end
127
-
128
- private
129
-
130
- def assign_taxonomy_and_parent
131
- if parent.present?
132
- self.taxonomy ||= parent.taxonomy
133
- elsif taxonomy_id.blank?
134
- self.taxonomy = Spree::Taxonomy.shows
135
- self.parent = taxonomy.root
136
- end
137
- end
138
-
139
- def voting_credit_scope_immutable
140
- show_has_credits = SpreeCmCommissioner::VotingCredit.exists?(votable_type: 'SpreeCmCommissioner::Show', votable_id: id)
141
- episode_has_credits = SpreeCmCommissioner::VotingCredit.exists?(votable_type: 'SpreeCmCommissioner::ShowEpisode',
142
- votable_id: episodes.select(:id)
143
- )
144
-
145
- return unless show_has_credits || episode_has_credits
146
-
147
- errors.add(:voting_credit_scope, 'cannot be changed after voting credits have been allocated')
148
- end
149
-
150
- def parse_json_config(value)
151
- return {} if value.nil? || value == ''
152
- return value if value.is_a?(Hash)
153
-
154
- JSON.parse(value)
155
- rescue JSON::ParserError
156
- {}
157
- end
158
- end
159
- end
@@ -1,39 +0,0 @@
1
- module SpreeCmCommissioner
2
- class ShowContestant < SpreeCmCommissioner::Base
3
- belongs_to :show, class_name: 'SpreeCmCommissioner::Show', optional: false
4
- belongs_to :assigned_show_person, class_name: 'SpreeCmCommissioner::ShowPerson', optional: true
5
- belongs_to :user, class_name: 'Spree::User', optional: true
6
- has_many :show_contestant_images, lambda {
7
- order(:position)
8
- }, as: :viewable, class_name: 'SpreeCmCommissioner::ShowContestantImage', dependent: :destroy
9
-
10
- has_many :show_contestant_videos, lambda {
11
- order(:position)
12
- }, as: :viewable, class_name: 'SpreeCmCommissioner::ShowContestantVideo', dependent: :destroy
13
-
14
- has_one :show_person_images, -> { order(:position) }, as: :viewable, class_name: 'SpreeCmCommissioner::ShowPersonImage', dependent: :destroy
15
-
16
- has_many :voting_contestants, class_name: 'SpreeCmCommissioner::VotingContestant', dependent: :destroy
17
- has_one :current_voting_session, through: :show
18
-
19
- enum status: { active: 0, eliminated: 1, advanced: 2, winner: 3, withdrawn: 4 }
20
- enum gender: { :other => 0, :male => 1, :female => 2 }
21
-
22
- enum category: { boys: 0, girls: 1, groups: 2, overs: 3 }
23
-
24
- # Define standard Social Link Keys
25
- store_accessor :social_links, :facebook, :instagram, :twitter, :linkedin, :youtube, :tiktok, :telegram
26
-
27
- validates :name, presence: true
28
- validates :contestant_number, uniqueness: { scope: :show_id }, allow_nil: true
29
-
30
- scope :ordered, -> { order(:position, :id) }
31
-
32
- def eliminate
33
- self.status = :eliminated
34
- self.eliminated_at = Time.current
35
- end
36
-
37
- self.whitelisted_ransackable_attributes = %w[name contestant_number status gender category origin]
38
- end
39
- end
@@ -1,11 +0,0 @@
1
- module SpreeCmCommissioner
2
- class ShowContestantImage < Asset
3
- def asset_styles
4
- {
5
- thumb: '60x60>',
6
- small: '180x180>',
7
- medium: '480x480>'
8
- }
9
- end
10
- end
11
- end
@@ -1,15 +0,0 @@
1
- module SpreeCmCommissioner
2
- class ShowContestantVideo < Video
3
- validate :validate_attachment_file_size
4
-
5
- private
6
-
7
- def validate_attachment_file_size
8
- return unless attachment.attached?
9
-
10
- return unless attachment.blob.byte_size > 5.megabytes
11
-
12
- errors.add(:attachment, 'must be less than 5 MB')
13
- end
14
- end
15
- end
@@ -1,135 +0,0 @@
1
- module SpreeCmCommissioner
2
- class ShowEpisode < Spree::Product
3
- self.inheritance_column = :_type_disabled
4
-
5
- include SpreeCmCommissioner::StoreMetadata
6
-
7
- belongs_to :season, class_name: 'SpreeCmCommissioner::Show', foreign_key: :event_id, optional: false
8
- has_one :show, through: :season
9
-
10
- has_many :voting_credits, class_name: 'SpreeCmCommissioner::VotingCredit', as: :votable, dependent: :destroy
11
- has_many :voting_sessions, class_name: 'SpreeCmCommissioner::VotingSession', foreign_key: :episode_id, dependent: :destroy
12
- has_many :contestants, through: :season, source: :show_contestants
13
-
14
- has_one :current_voting_session, -> { current_or_next_upcoming }, class_name: 'SpreeCmCommissioner::VotingSession', foreign_key: :episode_id
15
-
16
- validates :ends_at, presence: true, if: -> { product_type == 'show_episode' }
17
-
18
- before_validation { self.type = self.class.name }
19
-
20
- store_private_metadata :episode_number, :integer
21
- store_private_metadata :scheduled_at, :datetime
22
- store_private_metadata :ends_at, :datetime
23
- store_private_metadata :aired_at, :datetime
24
-
25
- store_public_metadata :votable_type, :string
26
- store_public_metadata :votable_id, :integer
27
-
28
- # Uses the database clock (CURRENT_TIMESTAMP) for consistency and to avoid application server clock drift.
29
- scope :current, lambda {
30
- where("CAST((spree_products.private_metadata #>> '{}')::jsonb ->> 'scheduled_at' AS timestamp) <= CURRENT_TIMESTAMP")
31
- .where("CAST((spree_products.private_metadata #>> '{}')::jsonb ->> 'ends_at' AS timestamp) >= CURRENT_TIMESTAMP")
32
- }
33
-
34
- scope :upcoming, lambda {
35
- where("CAST((spree_products.private_metadata #>> '{}')::jsonb ->> 'scheduled_at' AS timestamp) > CURRENT_TIMESTAMP")
36
- }
37
-
38
- # Orders episodes by their episode_number stored in private_metadata JSON.
39
- # e.g. [Episode#3 (ep_num=1), Episode#7 (ep_num=2), Episode#1 (ep_num=3)]
40
- scope :ordered_by_episode_number, lambda {
41
- reorder(Arel.sql("CAST((spree_products.private_metadata #>> '{}')::jsonb ->> 'episode_number' AS INTEGER) ASC"))
42
- }
43
-
44
- def self.current_or_next_upcoming
45
- current.or(upcoming)
46
- .reorder(
47
- Arel.sql("CAST((spree_products.private_metadata #>> '{}')::jsonb ->> 'scheduled_at' AS timestamp) ASC"),
48
- Arel.sql("CAST((spree_products.private_metadata #>> '{}')::jsonb ->> 'episode_number' AS integer) ASC")
49
- )
50
- end
51
-
52
- def current?
53
- (scheduled_at.present? && scheduled_at <= Time.zone.now) &&
54
- (ends_at >= Time.zone.now)
55
- end
56
-
57
- def upcoming?
58
- scheduled_at.present? && scheduled_at > Time.zone.now
59
- end
60
-
61
- # Returns the episode that follows this one within the same show, ordered by episode_number.
62
- # e.g. show has [ep1, ep2, ep3]; self=ep2 → returns ep3; self=ep3 → nil
63
- def next_in_season
64
- eps = season.episodes.where(product_type: :show_episode).ordered_by_episode_number.to_a
65
- current_index = eps.index { |ep| ep.id == id }
66
- eps[current_index + 1] if current_index && current_index < eps.length - 1
67
- end
68
-
69
- # True when this episode is the first in the season by episode_number.
70
- # e.g. season has [ep1, ep2]; self=ep1 → true; self=ep2 → false
71
- def first_in_season?
72
- season.episodes.ordered_by_episode_number.first&.id == id
73
- end
74
-
75
- # Returns the first voting session (by position) of the next episode in the show.
76
- # e.g. next episode has sessions [pos:1 "Prelim", pos:2 "Final"] → returns "Prelim" session
77
- # Returns nil when there is no next episode or the next episode has no sessions.
78
- def next_episode_first_session
79
- next_in_season&.voting_sessions&.order(:position)&.first
80
- end
81
-
82
- def self.model_name
83
- Spree::Product.model_name
84
- end
85
-
86
- def self.polymorphic_name
87
- name
88
- end
89
-
90
- def votable_type
91
- public_metadata['votable_type']
92
- end
93
-
94
- def votable_id
95
- public_metadata['votable_id']
96
- end
97
-
98
- private
99
-
100
- # Price is optional for real show episodes, but remains required for other
101
- # product types that also use this model (e.g. vote_package).
102
- def requires_price?
103
- product_type.to_s != 'show_episode'
104
- end
105
-
106
- # Skip master price validation for show_episode records so they can be
107
- # created without product/master price.
108
- def validate_master
109
- return if product_type.to_s == 'show_episode'
110
-
111
- super
112
- end
113
-
114
- # Persist master variant without running price validation only for
115
- # show_episode records.
116
- def save_master
117
- if product_type.to_s == 'show_episode' && master_updated?
118
- master.save(validate: false)
119
- @nested_changes = true
120
- return
121
- end
122
-
123
- super
124
- end
125
-
126
- # Override. This is to ensure that the slug generator uses the correct scope
127
- # when generating slugs for ShowEpisode records.
128
- def scope_for_slug_generator
129
- scope = self.class.base_class.unscoped
130
- scope = scope.friendly unless scope.respond_to?(:exists_by_friendly_id?)
131
- primary_key_name = self.class.primary_key
132
- scope.where(self.class.base_class.arel_table[primary_key_name].not_eq(send(primary_key_name)))
133
- end
134
- end
135
- end
@@ -1,15 +0,0 @@
1
- module SpreeCmCommissioner
2
- class ShowPerson < SpreeCmCommissioner::Base
3
- belongs_to :user, class_name: 'Spree::User', optional: true
4
- belongs_to :tenant, class_name: 'SpreeCmCommissioner::Tenant', optional: true
5
- has_many :show_person_images, -> { order(:position) }, as: :viewable, class_name: 'SpreeCmCommissioner::ShowPersonImage', dependent: :destroy
6
- has_many :assignments, class_name: 'SpreeCmCommissioner::ShowPersonAssignment', dependent: :destroy
7
- has_many :shows, through: :assignments, source: :show
8
- has_many :assigned_contestants, class_name: 'SpreeCmCommissioner::ShowContestant', foreign_key: :assigned_show_person_id, dependent: :nullify
9
-
10
- validates :name, presence: true
11
-
12
- # Define standard Social Link Keys
13
- store_accessor :social_links, :facebook, :instagram, :twitter, :linkedin, :youtube, :tiktok, :telegram
14
- end
15
- end
@@ -1,20 +0,0 @@
1
- module SpreeCmCommissioner
2
- class ShowPersonAssignment < SpreeCmCommissioner::Base
3
- self.table_name = 'cm_show_people_assignments'
4
-
5
- acts_as_list column: :position
6
-
7
- belongs_to :show_person, class_name: 'SpreeCmCommissioner::ShowPerson', optional: false
8
- belongs_to :show, class_name: 'Spree::Taxon', optional: false
9
-
10
- enum role: { coach: 0, judge: 1, host: 2, mentor: 3 }
11
-
12
- validates :role, presence: true
13
-
14
- has_many :assigned_contestants,
15
- -> (assignment) { where(show_id: assignment.show_id) },
16
- class_name: 'SpreeCmCommissioner::ShowContestant',
17
- primary_key: :show_person_id,
18
- foreign_key: :assigned_show_person_id
19
- end
20
- end
@@ -1,11 +0,0 @@
1
- module SpreeCmCommissioner
2
- class ShowPersonImage < Asset
3
- def asset_styles
4
- {
5
- thumb: '60x60>',
6
- small: '180x180>',
7
- medium: '480x480>'
8
- }
9
- end
10
- end
11
- end
@@ -1,16 +0,0 @@
1
- module SpreeCmCommissioner
2
- class Vote < SpreeCmCommissioner::Base
3
- belongs_to :voting_session, class_name: 'SpreeCmCommissioner::VotingSession', optional: false
4
- belongs_to :contestant, class_name: 'SpreeCmCommissioner::VotingContestant', optional: false
5
- belongs_to :user, class_name: 'Spree::User', optional: true
6
- belongs_to :vote_credit, class_name: 'SpreeCmCommissioner::VotingCredit', optional: true
7
- belongs_to :vendor, class_name: 'Spree::Vendor', optional: true
8
-
9
- enum channel: { web: 0, mobile: 1, admin: 2 }
10
-
11
- validates :tenant_id, :vendor_id, :channel, :quantity, presence: true
12
- validates :quantity, numericality: { greater_than: 0 }
13
-
14
- scope :for_session, -> (session) { where(voting_session: session) }
15
- end
16
- end
@@ -1,19 +0,0 @@
1
- module SpreeCmCommissioner
2
- class VoteFraudEvent < SpreeCmCommissioner::Base
3
- belongs_to :voting_session, class_name: 'SpreeCmCommissioner::VotingSession'
4
- belongs_to :user, class_name: 'Spree::User', optional: true
5
- belongs_to :vote, class_name: 'SpreeCmCommissioner::Vote', optional: true
6
- belongs_to :resolved_by, class_name: 'Spree::User', optional: true
7
-
8
- enum fraud_type: {
9
- rate_limit: 0,
10
- device_duplicate: 1,
11
- ip_cluster: 2,
12
- anomaly: 3,
13
- new_account: 4,
14
- manual_flag: 5
15
- }
16
- enum severity: { low: 0, medium: 1, high: 2, critical: 3 }
17
- enum action_taken: { allowed: 0, blocked: 1, flagged: 2, invalidated: 3 }
18
- end
19
- end
@@ -1,46 +0,0 @@
1
- module SpreeCmCommissioner
2
- class VotingContestant < SpreeCmCommissioner::Base
3
- has_paper_trail
4
- enum eliminated_via: { public_vote: 0, judges_deadlock: 1, coach_decision: 2, instant_save_lost: 3 }
5
- belongs_to :show, class_name: 'Spree::Taxon', optional: false
6
- belongs_to :voting_session, class_name: 'SpreeCmCommissioner::VotingSession', optional: false
7
- belongs_to :show_contestant, class_name: 'SpreeCmCommissioner::ShowContestant', optional: false
8
- belongs_to :advanced_to, polymorphic: true, optional: true
9
- belongs_to :advanced_from, polymorphic: true, optional: true
10
- has_many :votes, class_name: 'SpreeCmCommissioner::Vote', foreign_key: :contestant_id, dependent: :restrict_with_error
11
- has_many :show_contestant_images, through: :show_contestant
12
- has_many :show_contestant_videos, through: :show_contestant
13
-
14
- self.whitelisted_ransackable_attributes = %w[eliminated special_rules]
15
- self.whitelisted_ransackable_associations = %w[show_contestant]
16
-
17
- ransacker :special_rule_type do
18
- Arel.sql("cm_voting_contestants.special_rules ->> 'type'")
19
- end
20
-
21
- validates :eliminated_via, presence: true, if: :eliminated?
22
-
23
- # Uniqueness validation: vote_number must be unique per contestant per voting_session
24
- # Scope: [show_contestant_id, voting_session_id]
25
- #
26
- # | contestant_id | session_id | vote_number | Status | Reason |
27
- # |---------------|-----------|-------------|--------|---------------------------|
28
- # | 1 | 1 | 1 | ✓ OK | First record |
29
- # | 1 | 1 | 1 | ✗ FAIL | Duplicate in same scope |
30
- # | 1 | 1 | 2 | ✓ OK | Can update/change number |
31
- # | 2 | 1 | 1 | ✓ OK | Different contestant |
32
- # | 1 | 2 | 1 | ✓ OK | Different session |
33
- validates :vote_number, uniqueness: { scope: %i[show_contestant_id voting_session_id], allow_nil: true }
34
-
35
- delegate :name, :contestant_number, :category, :gender, :eliminated_at, to: :show_contestant, allow_nil: true
36
- delegate :can_vote?, to: :voting_session, allow_nil: true
37
-
38
- def special_rule_type=(value)
39
- self.special_rules = value.present? ? { 'type' => value } : {}
40
- end
41
-
42
- def special_rule_type
43
- (special_rules || {})['type']
44
- end
45
- end
46
- end
@@ -1,72 +0,0 @@
1
- module SpreeCmCommissioner
2
- class VotingCredit < SpreeCmCommissioner::Base
3
- enum category: { purchase: 0, promo: 1, bonus: 2, admin_grant: 3 }
4
- enum credit_type: { expiring: 0, non_expiring: 1 }
5
-
6
- belongs_to :user, class_name: 'Spree::User', optional: false
7
- belongs_to :created_by, class_name: 'Spree::User', optional: true
8
- belongs_to :votable, polymorphic: true, optional: true
9
-
10
- has_many :voting_credit_transactions, class_name: 'SpreeCmCommissioner::VotingCreditTransaction',
11
- foreign_key: :vote_credit_id, dependent: :destroy
12
- has_many :votes, class_name: 'SpreeCmCommissioner::Vote',
13
- foreign_key: :vote_credit_id, dependent: :destroy
14
-
15
- scope :active, -> { where(deleted_at: nil) }
16
- scope :eligible, lambda { |voting_session|
17
- active.where(
18
- '(votable_type = ? AND votable_id = ?) OR ' \
19
- '(votable_type = ? AND votable_id = ?) OR ' \
20
- '(votable_type = ? AND votable_id = ?)',
21
- 'SpreeCmCommissioner::VotingSession', voting_session.id,
22
- 'SpreeCmCommissioner::ShowEpisode', voting_session.episode_id,
23
- 'SpreeCmCommissioner::Show', voting_session.episode.event_id
24
- )
25
- }
26
-
27
- before_validation :set_default_votes, on: :create
28
- before_create :set_created_by
29
-
30
- validates :free_votes_amount, numericality: { greater_than_or_equal_to: 0 }
31
- validates :free_votes_used, numericality: { greater_than_or_equal_to: 0 }
32
- validates :paid_votes_amount, numericality: { greater_than_or_equal_to: 0 }
33
- validates :paid_votes_used, numericality: { greater_than_or_equal_to: 0 }
34
-
35
- def free_votes_remaining
36
- free_votes_amount - free_votes_used
37
- end
38
-
39
- def paid_votes_remaining
40
- paid_votes_amount - paid_votes_used
41
- end
42
-
43
- def available_votes
44
- free_votes_remaining + paid_votes_remaining
45
- end
46
-
47
- def can_use?(requested_amount)
48
- available_votes >= requested_amount
49
- end
50
-
51
- def soft_delete
52
- update(deleted_at: Time.current)
53
- end
54
-
55
- def restore
56
- update(deleted_at: nil)
57
- end
58
-
59
- private
60
-
61
- def set_created_by
62
- self.created_by ||= user
63
- end
64
-
65
- def set_default_votes
66
- self.free_votes_amount = 0 if free_votes_amount.blank?
67
- self.free_votes_used = 0 if free_votes_used.blank?
68
- self.paid_votes_amount = 0 if paid_votes_amount.blank?
69
- self.paid_votes_used = 0 if paid_votes_used.blank?
70
- end
71
- end
72
- end
@@ -1,55 +0,0 @@
1
- module SpreeCmCommissioner
2
- class VotingCreditTransaction < SpreeCmCommissioner::Base
3
- enum action: {
4
- purchase: 0,
5
- canceled: 1,
6
- use: 2,
7
- authorize: 3,
8
- release: 4,
9
- free_claim: 5
10
- }
11
-
12
- attr_accessor :free_claim_votable_id
13
-
14
- belongs_to :vote_credit, class_name: 'SpreeCmCommissioner::VotingCredit', optional: false
15
- belongs_to :episode, class_name: 'SpreeCmCommissioner::ShowEpisode', optional: true
16
- belongs_to :originator, polymorphic: true, optional: true
17
-
18
- scope :by_action, -> (actions) { where(action: actions) }
19
-
20
- before_validation :set_idempotency_key, if: -> { idempotency_key.nil? }
21
-
22
- validates :amount, presence: true
23
- validates :action, presence: true
24
- validates :user_total_amount, presence: true, numericality: { greater_than_or_equal_to: 0 }
25
- validates :idempotency_key, uniqueness: true, allow_nil: true
26
-
27
- def soft_delete
28
- update(deleted_at: Time.current)
29
- end
30
-
31
- def restore
32
- update(deleted_at: nil)
33
- end
34
-
35
- private
36
-
37
- def set_idempotency_key
38
- self.idempotency_key =
39
- case action
40
- when 'purchase', 'canceled' then order_scoped_key
41
- when 'free_claim' then free_claim_key
42
- end
43
- end
44
-
45
- def order_scoped_key
46
- "#{originator.number}::#{action}" if originator.respond_to?(:number)
47
- end
48
-
49
- def free_claim_key
50
- return unless originator.respond_to?(:free_vote_idempotency_key) && vote_credit&.user_id
51
-
52
- originator.free_vote_idempotency_key(user_id: vote_credit.user_id, votable_id: free_claim_votable_id)
53
- end
54
- end
55
- end