spree_cm_commissioner 2.8.2.pre.pre.2 → 2.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +0 -4
- data/Gemfile.lock +1 -1
- data/app/controllers/spree/admin/homepage_section_controller.rb +1 -4
- data/app/controllers/spree/admin/taxons_controller_decorator.rb +0 -19
- data/app/controllers/spree/api/v2/storefront/homepage_sections_controller.rb +0 -1
- data/app/controllers/spree/api/v2/tenant/base_controller.rb +0 -4
- data/app/controllers/spree/api/v2/tenant/homepage_sections_controller.rb +0 -1
- data/app/controllers/spree/api/v2/tenant/products_controller.rb +1 -1
- data/app/controllers/spree/api/v2/tenant/taxons_controller.rb +1 -1
- data/app/controllers/spree_cm_commissioner/admin/products_controller_decorator.rb +0 -19
- data/app/finders/spree_cm_commissioner/events/find_matches.rb +0 -1
- data/app/helpers/spree_cm_commissioner/admin/homepage_segment_helper.rb +0 -2
- data/app/models/concerns/spree_cm_commissioner/homepage_section_bitwise.rb +1 -2
- data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +1 -2
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +0 -10
- data/app/models/concerns/spree_cm_commissioner/product_type.rb +1 -1
- data/app/models/spree_cm_commissioner/product_decorator.rb +0 -39
- data/app/models/spree_cm_commissioner/role_decorator.rb +1 -4
- data/app/models/spree_cm_commissioner/taxon_decorator.rb +0 -15
- data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +1 -10
- data/app/models/spree_cm_commissioner/tenant.rb +0 -9
- data/app/models/spree_cm_commissioner/user_decorator.rb +0 -5
- data/app/models/spree_cm_commissioner/variant_options.rb +0 -4
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +0 -4
- data/app/serializers/spree/v2/storefront/homepage_section_serializer.rb +1 -1
- data/app/serializers/spree/v2/storefront/product_serializer_decorator.rb +1 -1
- data/app/serializers/spree/v2/storefront/role_serializer.rb +1 -1
- data/app/serializers/spree/v2/storefront/taxon_serializer_decorator.rb +1 -2
- data/app/serializers/spree/v2/tenant/homepage_section_serializer.rb +1 -1
- data/app/serializers/spree/v2/tenant/role_serializer.rb +1 -1
- data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +0 -12
- data/app/views/spree/admin/homepage_section/_form.html.erb +0 -5
- data/config/initializers/spree_permitted_attributes.rb +0 -8
- data/config/locales/en.yml +0 -14
- data/config/locales/km.yml +0 -10
- data/config/routes.rb +0 -26
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +1 -7
- data/spree_cm_commissioner.gemspec +1 -1
- metadata +4 -116
- data/app/controllers/spree/api/v2/storefront/preview_products_controller.rb +0 -48
- data/app/controllers/spree/api/v2/storefront/preview_sections_controller.rb +0 -27
- data/app/controllers/spree/api/v2/storefront/preview_taxons_controller.rb +0 -18
- data/app/controllers/spree/api/v2/storefront/products_controller_decorator.rb +0 -15
- data/app/controllers/spree/api/v2/storefront/taxons_controller_decorator.rb +0 -15
- data/app/controllers/spree/api/v2/tenant/free_vote_claims_controller.rb +0 -37
- data/app/controllers/spree/api/v2/tenant/preview_products_controller.rb +0 -47
- data/app/controllers/spree/api/v2/tenant/preview_sections_controller.rb +0 -26
- data/app/controllers/spree/api/v2/tenant/preview_shows_controller.rb +0 -19
- data/app/controllers/spree/api/v2/tenant/preview_taxons_controller.rb +0 -19
- data/app/controllers/spree/api/v2/tenant/show_contestants_controller.rb +0 -52
- data/app/controllers/spree/api/v2/tenant/show_elimination_sessions_controller.rb +0 -57
- data/app/controllers/spree/api/v2/tenant/show_people_controller.rb +0 -49
- data/app/controllers/spree/api/v2/tenant/show_person_assignments_controller.rb +0 -36
- data/app/controllers/spree/api/v2/tenant/shows_controller.rb +0 -34
- data/app/controllers/spree/api/v2/tenant/votes_controller.rb +0 -94
- data/app/controllers/spree/api/v2/tenant/voting_contestants_controller.rb +0 -40
- data/app/controllers/spree/api/v2/tenant/voting_credit_transactions_controller.rb +0 -41
- data/app/controllers/spree/api/v2/tenant/voting_credits_controller.rb +0 -31
- data/app/jobs/spree_cm_commissioner/vote_fraud_event_job.rb +0 -9
- data/app/jobs/spree_cm_commissioner/voting_credit_allocation_job.rb +0 -10
- data/app/jobs/spree_cm_commissioner/voting_credit_de_allocation_job.rb +0 -10
- data/app/models/spree_cm_commissioner/maintenance_tasks/voting_session.rb +0 -36
- data/app/models/spree_cm_commissioner/preview_role.rb +0 -8
- data/app/models/spree_cm_commissioner/role_user_decorator.rb +0 -8
- data/app/models/spree_cm_commissioner/show.rb +0 -159
- data/app/models/spree_cm_commissioner/show_contestant.rb +0 -39
- data/app/models/spree_cm_commissioner/show_contestant_image.rb +0 -11
- data/app/models/spree_cm_commissioner/show_contestant_video.rb +0 -15
- data/app/models/spree_cm_commissioner/show_episode.rb +0 -135
- data/app/models/spree_cm_commissioner/show_person.rb +0 -15
- data/app/models/spree_cm_commissioner/show_person_assignment.rb +0 -20
- data/app/models/spree_cm_commissioner/show_person_image.rb +0 -11
- data/app/models/spree_cm_commissioner/vote.rb +0 -16
- data/app/models/spree_cm_commissioner/vote_fraud_event.rb +0 -19
- data/app/models/spree_cm_commissioner/voting_contestant.rb +0 -46
- data/app/models/spree_cm_commissioner/voting_credit.rb +0 -72
- data/app/models/spree_cm_commissioner/voting_credit_transaction.rb +0 -55
- data/app/models/spree_cm_commissioner/voting_session.rb +0 -223
- data/app/models/spree_cm_commissioner/voting_session_stat.rb +0 -8
- data/app/overrides/spree/admin/products/_form/preview_checkbox.html.erb.deface +0 -9
- data/app/overrides/spree/admin/taxons/_form/preview_checkbox.html.erb.deface +0 -7
- data/app/serializers/spree/v2/tenant/show_contestant_serializer.rb +0 -21
- data/app/serializers/spree/v2/tenant/show_episode_serializer.rb +0 -17
- data/app/serializers/spree/v2/tenant/show_person_assignment_serializer.rb +0 -16
- data/app/serializers/spree/v2/tenant/show_person_serializer.rb +0 -13
- data/app/serializers/spree/v2/tenant/show_serializer.rb +0 -26
- data/app/serializers/spree/v2/tenant/video_serializer.rb +0 -9
- data/app/serializers/spree/v2/tenant/vote_serializer.rb +0 -14
- data/app/serializers/spree/v2/tenant/voting_contestant_serializer.rb +0 -22
- data/app/serializers/spree/v2/tenant/voting_credit_serializer.rb +0 -10
- data/app/serializers/spree/v2/tenant/voting_credit_transaction_serializer.rb +0 -14
- data/app/serializers/spree/v2/tenant/voting_session_serializer.rb +0 -18
- data/app/services/spree_cm_commissioner/fraud_check.rb +0 -279
- data/app/services/spree_cm_commissioner/show_contestants/normalize_video_highlights.rb +0 -57
- data/app/services/spree_cm_commissioner/url_embed/youtube_embed.rb +0 -44
- data/app/services/spree_cm_commissioner/vote_counters/audit_counters.rb +0 -43
- data/app/services/spree_cm_commissioner/vote_counters/base.rb +0 -31
- data/app/services/spree_cm_commissioner/vote_counters/increment.rb +0 -44
- data/app/services/spree_cm_commissioner/vote_counters/per_contestant_counter.rb +0 -68
- data/app/services/spree_cm_commissioner/vote_counters/rebuild_from_db.rb +0 -70
- data/app/services/spree_cm_commissioner/vote_counters/snapshot_to_db.rb +0 -113
- data/app/services/spree_cm_commissioner/vote_credit_deductor.rb +0 -68
- data/app/services/spree_cm_commissioner/vote_package/create.rb +0 -145
- data/app/services/spree_cm_commissioner/vote_package/update.rb +0 -91
- data/app/services/spree_cm_commissioner/vote_processor.rb +0 -144
- data/app/services/spree_cm_commissioner/voting_contestants/advancer.rb +0 -334
- data/app/services/spree_cm_commissioner/voting_contestants/assigner.rb +0 -32
- data/app/services/spree_cm_commissioner/voting_contestants/bulk_updater.rb +0 -106
- data/app/services/spree_cm_commissioner/voting_credits/allocate.rb +0 -77
- data/app/services/spree_cm_commissioner/voting_credits/claim_free_votes.rb +0 -119
- data/app/services/spree_cm_commissioner/voting_credits/credit_calculator.rb +0 -35
- data/app/services/spree_cm_commissioner/voting_credits/de_allocate.rb +0 -87
- data/app/services/spree_cm_commissioner/voting_leaderboards/calculate_score.rb +0 -74
- data/app/services/spree_cm_commissioner/voting_sessions/finalize.rb +0 -66
- data/db/migrate/20260309230148_create_cm_show_people.rb +0 -14
- data/db/migrate/20260309230149_create_cm_show_people_assignments.rb +0 -16
- data/db/migrate/20260310082711_create_cm_show_contestants.rb +0 -28
- data/db/migrate/20260310082720_create_cm_voting_sessions.rb +0 -21
- data/db/migrate/20260310082721_create_cm_voting_contestants.rb +0 -23
- data/db/migrate/20260310082734_add_voting_fields_to_spree_taxons.rb +0 -9
- data/db/migrate/20260310082735_add_type_to_spree_products.rb +0 -6
- data/db/migrate/20260310082749_create_cm_voting_credits.rb +0 -27
- data/db/migrate/20260326080200_create_cm_voting_credit_transactions.rb +0 -27
- data/db/migrate/20260330160000_create_cm_votes.rb +0 -25
- data/db/migrate/20260401072500_add_advanced_from_to_cm_voting_contestants.rb +0 -7
- data/db/migrate/20260402000001_add_voting_credit_scope_to_spree_taxons.rb +0 -6
- data/db/migrate/20260402000002_rename_scopeable_to_votable_in_cm_voting_credits.rb +0 -12
- data/db/migrate/20260403070000_add_name_to_cm_voting_sessions.rb +0 -5
- data/db/migrate/20260406000001_add_vendor_id_to_voting_tables.rb +0 -6
- data/db/migrate/20260406000001_rename_votes_remaining_to_amount_in_cm_voting_credits.rb +0 -11
- data/db/migrate/20260408085255_add_show_id_and_vendor_id_to_cm_voting_sessions.rb +0 -9
- data/db/migrate/20260420000001_rename_type_to_credit_type_in_cm_voting_credits.rb +0 -25
- data/db/migrate/20260422000001_create_cm_vote_fraud_events.rb +0 -23
- data/db/migrate/20260423000001_add_preview_to_taxons_products_and_sections.rb +0 -11
- data/db/migrate/20260423000002_create_preview_roles.rb +0 -24
- data/db/migrate/20260515120000_add_public_metadata_to_cm_voting_sessions.rb +0 -5
- data/db/migrate/20260518090920_add_unique_voter_count_to_voting_contestants.rb +0 -5
- data/db/migrate/20260518094322_create_cm_voting_session_stats.rb +0 -17
- data/db/migrate/20260520000001_add_scoring_model_to_cm_voting_sessions.rb +0 -5
- data/db/migrate/20260520000001_optimize_cm_votes_indexes.rb +0 -22
- data/db/migrate/20260525042257_add_vote_number_to_cm_voting_contestants.rb +0 -18
- data/db/migrate/20260527035430_add_confirmed_rank_to_cm_voting_contestants.rb +0 -5
- data/db/migrate/20260527062005_add_eliminated_at_to_cm_show_contestants.rb +0 -5
- data/docs/sql/jsonb_query_guide.md +0 -57
- data/lib/spree_cm_commissioner/test_helper/factories/show_episode_factory.rb +0 -12
- data/lib/spree_cm_commissioner/test_helper/factories/show_factory.rb +0 -120
- data/lib/spree_cm_commissioner/test_helper/factories/vote_credit_factory.rb +0 -37
- data/lib/spree_cm_commissioner/test_helper/factories/vote_factory.rb +0 -28
- data/lib/spree_cm_commissioner/test_helper/factories/voting_credit_transaction_factory.rb +0 -11
- data/lib/spree_cm_commissioner/test_helper/factories/voting_session_factory.rb +0 -11
|
@@ -1,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
|