spree_cm_commissioner 2.9.2 → 2.9.4
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/Gemfile.lock +1 -1
- data/app/controllers/concerns/spree_cm_commissioner/load_test_account_scope.rb +14 -21
- data/app/controllers/concerns/spree_cm_commissioner/load_test_bypassable.rb +29 -0
- data/app/controllers/spree/api/chatrace/guests_controller.rb +1 -1
- data/app/controllers/spree/api/v2/storefront/account/guest_dynamic_fields_controller.rb +19 -48
- data/app/controllers/spree/api/v2/storefront/account/mark_guest_info_complete_controller.rb +2 -2
- data/app/controllers/spree/api/v2/storefront/account/orders_controller_decorator.rb +5 -15
- data/app/controllers/spree/api/v2/storefront/account/saved_guests_controller.rb +65 -0
- data/app/controllers/spree/api/v2/storefront/draft_orders_controller.rb +3 -0
- data/app/controllers/spree/api/v2/storefront/guests_controller.rb +10 -9
- data/app/controllers/spree/api/v2/storefront/order_histories_controller.rb +0 -1
- data/app/controllers/spree/api/v2/tenant/account/saved_guests_controller.rb +70 -0
- data/app/controllers/spree/api/v2/tenant/episodes_controller.rb +3 -1
- data/app/controllers/spree/api/v2/tenant/guests_controller.rb +11 -21
- data/app/controllers/spree/api/v2/tenant/order_histories_controller.rb +0 -1
- data/app/controllers/spree_cm_commissioner/admin/variants_controller_decorator.rb +7 -1
- data/app/controllers/spree_cm_commissioner/api/v2/storefront/checkout_controller_decorator.rb +3 -14
- data/app/jobs/spree_cm_commissioner/maintenance_tasks/guest_data_fill_stage_batch_job.rb +28 -0
- data/app/models/concerns/spree_cm_commissioner/guest_data_fill_stage_concern.rb +82 -0
- data/app/models/concerns/spree_cm_commissioner/line_item_durationable.rb +4 -0
- data/app/models/concerns/spree_cm_commissioner/line_item_guests_concern.rb +1 -1
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +13 -31
- data/app/models/spree_cm_commissioner/current.rb +6 -3
- data/app/models/spree_cm_commissioner/dynamic_field.rb +12 -0
- data/app/models/spree_cm_commissioner/guest.rb +1 -144
- data/app/models/spree_cm_commissioner/guest_dynamic_field.rb +43 -71
- data/app/models/spree_cm_commissioner/maintenance_tasks/guest_data_fill_stage.rb +21 -0
- data/app/models/spree_cm_commissioner/order_decorator.rb +26 -0
- data/app/models/spree_cm_commissioner/product_decorator.rb +0 -22
- data/app/models/spree_cm_commissioner/product_dynamic_field.rb +17 -0
- data/app/models/spree_cm_commissioner/request_guard.rb +19 -0
- data/app/models/spree_cm_commissioner/user_decorator.rb +26 -1
- data/app/models/spree_cm_commissioner/voting_contestant.rb +13 -2
- data/app/queries/spree_cm_commissioner/event_chart_queries.rb +2 -2
- data/app/queries/spree_cm_commissioner/pie_chart_event_aggregator_queries.rb +2 -2
- data/app/serializers/spree/v2/storefront/line_item_serializer_decorator.rb +2 -2
- data/app/serializers/spree/v2/storefront/user_serializer_decorator.rb +3 -1
- data/app/serializers/spree/v2/tenant/saved_guest_serializer.rb +23 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/cart_serializer_decorator.rb +1 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/guest_serializer.rb +3 -6
- data/app/serializers/spree_cm_commissioner/v2/storefront/ticket_transfer_serializer.rb +2 -2
- data/app/services/spree_cm_commissioner/{cart/create_guest.rb → guests/create.rb} +12 -10
- data/app/services/spree_cm_commissioner/guests/refresh_data_fill_stage.rb +20 -0
- data/app/services/spree_cm_commissioner/guests/send_dynamic_field_notification.rb +15 -0
- data/app/services/spree_cm_commissioner/guests/update.rb +68 -0
- data/app/services/spree_cm_commissioner/inventory_holds/validate_limits.rb +8 -3
- data/app/services/spree_cm_commissioner/load_test/destroy_accounts.rb +7 -10
- data/app/services/spree_cm_commissioner/load_test/generate_booking_ticket_data.rb +83 -19
- data/app/services/spree_cm_commissioner/order/guests/update.rb +5 -8
- data/app/services/spree_cm_commissioner/voting_contestants/advancer.rb +10 -3
- data/app/services/spree_cm_commissioner/voting_contestants/assigner.rb +20 -2
- data/app/services/spree_cm_commissioner/voting_contestants/bulk_updater.rb +15 -9
- data/app/views/spree/admin/variants/fixture.html.erb +1 -1
- data/config/initializers/spree_permitted_attributes.rb +19 -0
- data/config/load_test_flows/tenant.yml +47 -0
- data/config/locales/en.yml +2 -5
- data/config/locales/km.yml +2 -0
- data/config/routes.rb +3 -1
- data/db/migrate/20260713022227_add_advanced_to_episode_id_to_cm_voting_contestants.rb +8 -0
- data/db/migrate/20260714002626_rename_data_fill_stage_phase_on_cm_guests.rb +5 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/tasks/recompute_guest_data_fill_stage.rake +19 -0
- metadata +17 -12
- data/app/controllers/spree/api/v2/storefront/saved_guests_controller.rb +0 -77
- data/app/interactors/spree_cm_commissioner/guest_dynamic_field_notification_sender.rb +0 -11
- data/app/interactors/spree_cm_commissioner/guest_dynamic_fields_manager.rb +0 -33
- data/app/serializers/spree/v2/storefront/order_serializer_decorator.rb +0 -63
- data/app/serializers/spree_cm_commissioner/v2/storefront/guest_minimal_serializer.rb +0 -30
- data/app/serializers/spree_cm_commissioner/v2/storefront/line_item_minimal_serializer.rb +0 -57
- data/app/serializers/spree_cm_commissioner/v2/storefront/ticket_transfer_minimal_serializer.rb +0 -17
- data/app/services/spree_cm_commissioner/update_guest_service.rb +0 -129
- data/app/services/spree_cm_commissioner/users/incomplete_guest_checker_service.rb +0 -34
|
@@ -52,7 +52,6 @@ module SpreeCmCommissioner
|
|
|
52
52
|
base.scope :by_non_tenant, -> { where(tenant_id: nil) }
|
|
53
53
|
base.scope :vendor_users, -> (vendor_id) { joins(:role_users => :role).where(spree_roles: { vendor_id: vendor_id }).distinct }
|
|
54
54
|
|
|
55
|
-
base.store :public_metadata, accessors: [:has_incomplete_guest_info], coder: JSON
|
|
56
55
|
base.store :private_metadata, accessors: %i[otp_secret otp_required_for_login consumed_timestep], coder: JSON
|
|
57
56
|
|
|
58
57
|
base.store_public_metadata :primary_service_type, :string
|
|
@@ -66,6 +65,15 @@ module SpreeCmCommissioner
|
|
|
66
65
|
# the id_token param. Nothing token-related is stored on the account itself.
|
|
67
66
|
base.store_private_metadata :load_test_account, :boolean, default: false
|
|
68
67
|
|
|
68
|
+
# SQL counterpart of `load_test_account?`, so the flag can be filtered in the DB instead of
|
|
69
|
+
# loading rows into Ruby to call the predicate. private_metadata is stored double-encoded (a
|
|
70
|
+
# JSON string scalar, not a native jsonb object — see store_metadata), so unwrap the outer
|
|
71
|
+
# string with `#>> '{}'` before keying into it. Keeps the storage detail in one place next to
|
|
72
|
+
# the declaration above rather than duplicating the raw SQL in every caller.
|
|
73
|
+
base.scope :load_test_accounts, lambda {
|
|
74
|
+
where("(private_metadata #>> '{}')::jsonb ->> 'load_test_account' = 'true'")
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
# Validations
|
|
70
78
|
base.validates :primary_service_type,
|
|
71
79
|
inclusion: { in: SpreeCmCommissioner::ServiceType::SERVICE_TYPES.map(&:to_s) },
|
|
@@ -223,6 +231,23 @@ module SpreeCmCommissioner
|
|
|
223
231
|
super(request)
|
|
224
232
|
end
|
|
225
233
|
end
|
|
234
|
+
|
|
235
|
+
def has_incomplete_guest_info? # rubocop:disable Naming/PredicateName
|
|
236
|
+
# expires_in is a safety net, not the primary invalidation path — see the matching comment
|
|
237
|
+
# on Order#has_incomplete_guest_info?. Joins straight to guests.post_registration instead of
|
|
238
|
+
# calling Order#has_incomplete_guest_info? per order (which would be N+1 query calls).
|
|
239
|
+
Rails.cache.fetch(incomplete_guest_info_cache_key, expires_in: 1.hour) do
|
|
240
|
+
orders.complete.joins(:guests).merge(SpreeCmCommissioner::Guest.post_registration).exists?
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def clear_has_incomplete_guest_info_cache
|
|
245
|
+
Rails.cache.delete(incomplete_guest_info_cache_key)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def incomplete_guest_info_cache_key
|
|
249
|
+
"user/#{id}/has_incomplete_guest_info"
|
|
250
|
+
end
|
|
226
251
|
end
|
|
227
252
|
end
|
|
228
253
|
|
|
@@ -5,6 +5,7 @@ module SpreeCmCommissioner
|
|
|
5
5
|
belongs_to :show, class_name: 'Spree::Taxon', optional: false
|
|
6
6
|
belongs_to :voting_session, class_name: 'SpreeCmCommissioner::VotingSession', optional: false
|
|
7
7
|
belongs_to :show_contestant, class_name: 'SpreeCmCommissioner::ShowContestant', optional: false
|
|
8
|
+
belongs_to :advanced_to_episode, class_name: 'SpreeCmCommissioner::ShowEpisode', optional: true
|
|
8
9
|
belongs_to :advanced_to, polymorphic: true, optional: true
|
|
9
10
|
belongs_to :advanced_from, polymorphic: true, optional: true
|
|
10
11
|
has_many :votes, class_name: 'SpreeCmCommissioner::Vote', foreign_key: :contestant_id, dependent: :restrict_with_error
|
|
@@ -37,6 +38,7 @@ module SpreeCmCommissioner
|
|
|
37
38
|
# | 1 | 1 | 2 | ✓ OK | Can update/change number |
|
|
38
39
|
# | 1 | 2 | 1 | ✓ OK | Different session |
|
|
39
40
|
validates :vote_number, uniqueness: { scope: :voting_session_id, allow_nil: true }
|
|
41
|
+
validate :advanced_to_and_advanced_from_must_differ
|
|
40
42
|
|
|
41
43
|
delegate :name, :contestant_number, :category, :gender, :eliminated_at, to: :show_contestant, allow_nil: true
|
|
42
44
|
delegate :can_vote?, to: :voting_session, allow_nil: true
|
|
@@ -50,11 +52,11 @@ module SpreeCmCommissioner
|
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
def advanced_from_name
|
|
53
|
-
advanced_from&.name
|
|
55
|
+
[advanced_from&.episode&.name, advanced_from&.name].compact.join(' - ')
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
def advanced_to_name
|
|
57
|
-
advanced_to&.name
|
|
59
|
+
[advanced_to&.episode&.name || advanced_to_episode&.name, advanced_to&.name].compact.join(' - ')
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
private
|
|
@@ -97,5 +99,14 @@ module SpreeCmCommissioner
|
|
|
97
99
|
errors.add(:base, :has_advanced_to_session)
|
|
98
100
|
throw :abort
|
|
99
101
|
end
|
|
102
|
+
|
|
103
|
+
# Prevent a contestant from being advanced to the same record it was advanced from,
|
|
104
|
+
# which would create a self-referencing loop between sessions.
|
|
105
|
+
def advanced_to_and_advanced_from_must_differ
|
|
106
|
+
return if advanced_to_id.blank? || advanced_from_id.blank?
|
|
107
|
+
return unless advanced_to_type == advanced_from_type && advanced_to_id == advanced_from_id
|
|
108
|
+
|
|
109
|
+
errors.add(:advanced_to_id, :same_as_advanced_from)
|
|
110
|
+
end
|
|
100
111
|
end
|
|
101
112
|
end
|
|
@@ -59,7 +59,7 @@ module SpreeCmCommissioner
|
|
|
59
59
|
WHEN cm_check_ins.id IS NOT NULL THEN 'show'
|
|
60
60
|
ELSE 'no_show'
|
|
61
61
|
END AS name,
|
|
62
|
-
COUNT(
|
|
62
|
+
COUNT(DISTINCT cm_guests.id) AS total"
|
|
63
63
|
)
|
|
64
64
|
.group('spree_products.id, CASE WHEN cm_check_ins.id IS NOT NULL THEN \'show\' ELSE \'no_show\' END')
|
|
65
65
|
.group_by(&:product_id)
|
|
@@ -98,7 +98,7 @@ module SpreeCmCommissioner
|
|
|
98
98
|
WHEN cm_check_ins.entry_type = 0 THEN 'normal'
|
|
99
99
|
WHEN cm_check_ins.entry_type = 1 THEN 'VIP'
|
|
100
100
|
END AS name,
|
|
101
|
-
COUNT(
|
|
101
|
+
COUNT(DISTINCT cm_guests.id) AS total"
|
|
102
102
|
)
|
|
103
103
|
.group('spree_products.id, cm_check_ins.entry_type')
|
|
104
104
|
.group_by(&:product_id)
|
|
@@ -58,7 +58,7 @@ module SpreeCmCommissioner
|
|
|
58
58
|
WHEN cm_check_ins.id IS NOT NULL THEN 'show'
|
|
59
59
|
ELSE 'no_show'
|
|
60
60
|
END AS name,
|
|
61
|
-
COUNT(
|
|
61
|
+
COUNT(DISTINCT cm_guests.id) AS total"
|
|
62
62
|
)
|
|
63
63
|
.group('spree_products.id, CASE WHEN cm_check_ins.id IS NOT NULL THEN \'show\' ELSE \'no_show\' END')
|
|
64
64
|
.group_by(&:product_id)
|
|
@@ -110,7 +110,7 @@ module SpreeCmCommissioner
|
|
|
110
110
|
CASE WHEN cm_check_ins.entry_type = 0 THEN 'normal'
|
|
111
111
|
WHEN cm_check_ins.entry_type = 1 THEN 'VIP'
|
|
112
112
|
ELSE NULL END AS name,
|
|
113
|
-
COUNT(
|
|
113
|
+
COUNT(DISTINCT cm_guests.id) AS total"
|
|
114
114
|
)
|
|
115
115
|
.where.not(cm_check_ins: { id: nil })
|
|
116
116
|
.group('spree_products.id, cm_check_ins.entry_type')
|
|
@@ -35,11 +35,11 @@ module Spree
|
|
|
35
35
|
|
|
36
36
|
base.has_one :google_wallet, serializer: SpreeCmCommissioner::V2::Storefront::GoogleWalletSerializer
|
|
37
37
|
|
|
38
|
-
base.has_many :guests, serializer: SpreeCmCommissioner::V2::Storefront::
|
|
38
|
+
base.has_many :guests, serializer: SpreeCmCommissioner::V2::Storefront::GuestSerializer
|
|
39
39
|
|
|
40
40
|
base.has_many :saved_guests, serializer: SpreeCmCommissioner::V2::Storefront::SavedGuestSerializer
|
|
41
41
|
|
|
42
|
-
base.has_many :pending_guests, serializer: SpreeCmCommissioner::V2::Storefront::
|
|
42
|
+
base.has_many :pending_guests, serializer: SpreeCmCommissioner::V2::Storefront::GuestSerializer
|
|
43
43
|
|
|
44
44
|
base.has_one :product, serializer: Spree::V2::Storefront::ProductSerializer
|
|
45
45
|
|
|
@@ -5,10 +5,12 @@ module Spree
|
|
|
5
5
|
def self.prepended(base)
|
|
6
6
|
base.attributes :first_name, :last_name, :gender, :phone_number, :intel_phone_number,
|
|
7
7
|
:country_code, :otp_enabled, :otp_email, :otp_phone_number,
|
|
8
|
-
:confirm_pin_code_enabled, :tenant_id,
|
|
8
|
+
:confirm_pin_code_enabled, :tenant_id,
|
|
9
9
|
:login, :qr_data, :qr_data_version, :qr_data_invalidated_at,
|
|
10
10
|
:unread_notifications_count, :request_orders_count
|
|
11
11
|
|
|
12
|
+
base.attribute :has_incomplete_guest_info, &:has_incomplete_guest_info?
|
|
13
|
+
|
|
12
14
|
base.has_one :profile, serializer: ::Spree::V2::Storefront::UserProfileSerializer
|
|
13
15
|
base.has_many :device_tokens, serializer: Spree::V2::Storefront::UserDeviceTokenSerializer
|
|
14
16
|
base.has_many :spree_roles, serializer: Spree::V2::Storefront::RoleSerializer
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module V2
|
|
3
|
+
module Tenant
|
|
4
|
+
class SavedGuestSerializer < BaseSerializer
|
|
5
|
+
attributes :first_name,
|
|
6
|
+
:last_name,
|
|
7
|
+
:dob,
|
|
8
|
+
:age,
|
|
9
|
+
:gender,
|
|
10
|
+
:email,
|
|
11
|
+
:country_code,
|
|
12
|
+
:phone_number,
|
|
13
|
+
:intel_phone_number,
|
|
14
|
+
:user_id,
|
|
15
|
+
:age_group,
|
|
16
|
+
:nationality_group
|
|
17
|
+
|
|
18
|
+
belongs_to :occupation, serializer: Spree::V2::Tenant::TaxonSerializer
|
|
19
|
+
belongs_to :nationality, serializer: Spree::V2::Tenant::TaxonSerializer
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -7,13 +7,10 @@ module SpreeCmCommissioner
|
|
|
7
7
|
attributes :first_name, :last_name, :dob, :gender, :kyc_fields, :other_occupation, :created_at, :updated_at,
|
|
8
8
|
:qr_data, :social_contact_platform, :social_contact, :available_social_contact_platforms,
|
|
9
9
|
:age, :emergency_contact, :other_organization, :expectation, :upload_later, :address, :formatted_bib_number, :phone_number,
|
|
10
|
-
:token, :country_code, :contact, :
|
|
10
|
+
:token, :country_code, :contact, :seat_number,
|
|
11
11
|
:saved_guest_id, :state
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
attribute :seat_number do |object|
|
|
15
|
-
object.seat_number || object.formatted_bib_number
|
|
16
|
-
end
|
|
13
|
+
attribute :data_fill_stage_phase, &:data_fill_stage
|
|
17
14
|
|
|
18
15
|
attribute :allowed_checkout, &:allowed_checkout?
|
|
19
16
|
attribute :allowed_upload_later, &:allowed_upload_later?
|
|
@@ -30,7 +27,7 @@ module SpreeCmCommissioner
|
|
|
30
27
|
|
|
31
28
|
has_many :check_ins, serializer: SpreeCmCommissioner::V2::Storefront::CheckInSerializer
|
|
32
29
|
|
|
33
|
-
has_many :ticket_transfers, serializer: SpreeCmCommissioner::V2::Storefront::
|
|
30
|
+
has_many :ticket_transfers, serializer: SpreeCmCommissioner::V2::Storefront::TicketTransferSerializer
|
|
34
31
|
|
|
35
32
|
# allowed_checkout updates frequently
|
|
36
33
|
cache_options store: nil
|
|
@@ -9,8 +9,8 @@ module SpreeCmCommissioner
|
|
|
9
9
|
|
|
10
10
|
belongs_to :from_user, serializer: ::Spree::V2::Storefront::UserSerializer
|
|
11
11
|
belongs_to :to_user, serializer: ::Spree::V2::Storefront::UserSerializer
|
|
12
|
-
belongs_to :from_guest, serializer: SpreeCmCommissioner::V2::Storefront::
|
|
13
|
-
belongs_to :to_guest, serializer: SpreeCmCommissioner::V2::Storefront::
|
|
12
|
+
belongs_to :from_guest, serializer: SpreeCmCommissioner::V2::Storefront::GuestSerializer
|
|
13
|
+
belongs_to :to_guest, serializer: SpreeCmCommissioner::V2::Storefront::GuestSerializer
|
|
14
14
|
belongs_to :event, serializer: Spree::V2::Storefront::TaxonSerializer
|
|
15
15
|
belongs_to :product, serializer: Spree::V2::Storefront::ProductSerializer
|
|
16
16
|
belongs_to :order, serializer: ::Spree::V2::Storefront::OrderSerializer
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
# the source of truth, so quantity is raised to cover every guest (never trimmed), then
|
|
3
3
|
# the cart is recalculated. The line item is locked so the insert + quantity sync are
|
|
4
4
|
# atomic, mirroring Cart::AddGuest and preventing concurrent / double-triggered creates
|
|
5
|
-
# from clobbering the quantity.
|
|
5
|
+
# from clobbering the quantity. Quantity/hold adjustments are skipped once the order is
|
|
6
|
+
# completed (e.g. a vendor adding a guest to an already-paid booking) so this stays safe
|
|
7
|
+
# to call from both the storefront cart flow and tenant booking-on-behalf-of flow.
|
|
6
8
|
module SpreeCmCommissioner
|
|
7
|
-
module
|
|
8
|
-
class
|
|
9
|
+
module Guests
|
|
10
|
+
class Create
|
|
9
11
|
prepend Spree::ServiceModule::Base
|
|
10
12
|
|
|
11
|
-
def call(line_item:,
|
|
12
|
-
guest = line_item.guests.new(
|
|
13
|
+
def call(line_item:, guest_params: {})
|
|
14
|
+
guest = line_item.guests.new(guest_params)
|
|
13
15
|
|
|
14
16
|
# Release holds BEFORE mutating, using the quantities the hold was placed against
|
|
15
17
|
# (Release reads live line items). Only release when this guest will actually bump
|
|
@@ -22,16 +24,15 @@ module SpreeCmCommissioner
|
|
|
22
24
|
line_item.with_lock do
|
|
23
25
|
next unless guest.save
|
|
24
26
|
|
|
25
|
-
guest.save_and_move_to_next_stage
|
|
26
27
|
bump_quantity_to_cover_guests(line_item) if need_to_bump_quantity?(line_item)
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
return failure(guest) unless guest.persisted?
|
|
30
31
|
|
|
31
|
-
success(guest)
|
|
32
|
+
success(guest: guest)
|
|
32
33
|
rescue StandardError => e
|
|
33
34
|
CmAppLogger.error(
|
|
34
|
-
label: 'SpreeCmCommissioner::
|
|
35
|
+
label: 'SpreeCmCommissioner::Guests::Create#call',
|
|
35
36
|
data: {
|
|
36
37
|
error_class: e.class.name,
|
|
37
38
|
error_message: e.message
|
|
@@ -44,9 +45,10 @@ module SpreeCmCommissioner
|
|
|
44
45
|
|
|
45
46
|
# Raise quantity one unit at a time (each unit adds variant.number_of_guests of
|
|
46
47
|
# capacity) until number_of_guests covers guests.count, then recalculate. Skipped for
|
|
47
|
-
# custom guest-count metadata (accommodation/transit)
|
|
48
|
-
# guest capacity.
|
|
48
|
+
# custom guest-count metadata (accommodation/transit), variants with no per-unit
|
|
49
|
+
# guest capacity, and completed orders (quantity is no longer adjustable post-payment).
|
|
49
50
|
def need_to_bump_quantity?(line_item)
|
|
51
|
+
return false if line_item.order.completed?
|
|
50
52
|
return false if line_item.custom_guest_count?
|
|
51
53
|
return false if line_item.variant.number_of_guests.to_i <= 0
|
|
52
54
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Guests
|
|
3
|
+
class RefreshDataFillStage
|
|
4
|
+
prepend ::Spree::ServiceModule::Base
|
|
5
|
+
|
|
6
|
+
def call(guest:, notify: true)
|
|
7
|
+
# incomplete? is always live, so it can't detect a before/after transition on its own —
|
|
8
|
+
# comparing it to itself would always agree. completed? reads the persisted (possibly
|
|
9
|
+
# stale) column instead, which is the actual "before" state worth comparing against.
|
|
10
|
+
was_marked_complete = guest.completed?
|
|
11
|
+
|
|
12
|
+
guest.refresh_data_fill_stage
|
|
13
|
+
|
|
14
|
+
SpreeCmCommissioner::Guests::SendDynamicFieldNotification.call(guest: guest) if notify && was_marked_complete && guest.incomplete?
|
|
15
|
+
|
|
16
|
+
success(guest: guest)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Guests
|
|
3
|
+
class SendDynamicFieldNotification
|
|
4
|
+
prepend ::Spree::ServiceModule::Base
|
|
5
|
+
|
|
6
|
+
def call(guest:)
|
|
7
|
+
notification = SpreeCmCommissioner::GuestDynamicFieldNotification.with(
|
|
8
|
+
guest: guest
|
|
9
|
+
).deliver_later(guest.user)
|
|
10
|
+
|
|
11
|
+
success(notification: notification)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Guests
|
|
3
|
+
# Updates guest records with dynamic fields.
|
|
4
|
+
#
|
|
5
|
+
# Per-field replace semantics: for every dynamic_field_id mentioned in the payload, the
|
|
6
|
+
# submitted rows are the complete set of answers for that field; unmentioned fields are
|
|
7
|
+
# untouched. This still handles checkbox deselection (omit that option's row) without a
|
|
8
|
+
# submit containing only e.g. pre_registration fields wiping post_registration answers.
|
|
9
|
+
class Update
|
|
10
|
+
prepend ::Spree::ServiceModule::Base
|
|
11
|
+
|
|
12
|
+
attr_reader :guest, :guest_params
|
|
13
|
+
|
|
14
|
+
def call(guest:, guest_params:)
|
|
15
|
+
@guest = guest
|
|
16
|
+
@guest_params = guest_params
|
|
17
|
+
|
|
18
|
+
ActiveRecord::Base.transaction do
|
|
19
|
+
replace_dynamic_fields(guest_params[:guest_dynamic_fields_attributes]) if guest_params[:guest_dynamic_fields_attributes]
|
|
20
|
+
guest.update!(guest_params.except(:guest_dynamic_fields_attributes))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
success(guest: guest)
|
|
24
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
25
|
+
failure(e.record, e.record.errors)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def replace_dynamic_fields(dynamic_fields_attributes)
|
|
31
|
+
rows = dynamic_fields_attributes.map { |raw| raw.to_h.symbolize_keys }
|
|
32
|
+
|
|
33
|
+
to_destroy, to_upsert = rows.partition { |attrs| ActiveModel::Type::Boolean.new.cast(attrs[:_destroy]) }
|
|
34
|
+
to_destroy.each { |attrs| destroy_guest_dynamic_field(attrs[:id]) }
|
|
35
|
+
|
|
36
|
+
kept = to_upsert.map { |attrs| upsert_guest_dynamic_field(attrs) }
|
|
37
|
+
|
|
38
|
+
touched_field_ids = kept.map(&:dynamic_field_id)
|
|
39
|
+
guest.guest_dynamic_fields.where(dynamic_field_id: touched_field_ids).where.not(id: kept.map(&:id)).destroy_all
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Falls back to (dynamic_field_id, dynamic_field_option_id) when no id is given (e.g. Order::Guests::Update,
|
|
43
|
+
# which never has one) so a resubmit updates the prior row instead of creating a new one and relying on the
|
|
44
|
+
# cleanup pass below to delete the old one. This also collapses duplicate option ids submitted in one
|
|
45
|
+
# request: the second lookup finds the row the first upsert just inserted, in the same transaction.
|
|
46
|
+
def upsert_guest_dynamic_field(attrs)
|
|
47
|
+
field = if attrs[:id].present?
|
|
48
|
+
guest.guest_dynamic_fields.find_by(id: attrs[:id])
|
|
49
|
+
else
|
|
50
|
+
guest.guest_dynamic_fields.find_by(
|
|
51
|
+
dynamic_field_id: attrs[:dynamic_field_id],
|
|
52
|
+
dynamic_field_option_id: attrs[:dynamic_field_option_id]
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
field ||= guest.guest_dynamic_fields.build
|
|
56
|
+
field.assign_attributes(attrs.except(:id, :_destroy))
|
|
57
|
+
field.save!
|
|
58
|
+
field
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def destroy_guest_dynamic_field(id)
|
|
62
|
+
return if id.blank?
|
|
63
|
+
|
|
64
|
+
guest.guest_dynamic_fields.find_by(id: id)&.destroy!
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -3,6 +3,7 @@ module SpreeCmCommissioner
|
|
|
3
3
|
class ValidateLimits
|
|
4
4
|
prepend ::Spree::ServiceModule::Base
|
|
5
5
|
extend SpreeCmCommissioner::ServiceModuleThrowable
|
|
6
|
+
include SpreeCmCommissioner::RequestGuard
|
|
6
7
|
|
|
7
8
|
ExceededActiveHoldsError = Class.new(StandardError)
|
|
8
9
|
IpRateLimitError = Class.new(StandardError)
|
|
@@ -14,9 +15,13 @@ module SpreeCmCommissioner
|
|
|
14
15
|
|
|
15
16
|
# Validates all inventory hold limits for the given order
|
|
16
17
|
def call(order:)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
# Skipped for trusted load-test requests (see RequestGuard/LoadTestBypassable).
|
|
19
|
+
# Safe: overselling is still blocked by the Redis stock check in Acquire.
|
|
20
|
+
with_request_guard do
|
|
21
|
+
validate_user_hold_limits!(order) if order.user.present?
|
|
22
|
+
validate_ip_rate_limit!(order)
|
|
23
|
+
validate_cooldown!(order)
|
|
24
|
+
end
|
|
20
25
|
|
|
21
26
|
success(nil)
|
|
22
27
|
rescue StandardError => e
|
|
@@ -7,10 +7,6 @@ module SpreeCmCommissioner
|
|
|
7
7
|
class DestroyAccounts
|
|
8
8
|
prepend ::Spree::ServiceModule::Base
|
|
9
9
|
|
|
10
|
-
# Scan cap for the candidate set below — bounds the query while comfortably covering realistic
|
|
11
|
-
# load-test pool sizes (mirrors the admin controller's export scan).
|
|
12
|
-
SCAN_LIMIT = 2000
|
|
13
|
-
|
|
14
10
|
def call(client_id: nil, count: 20, **_ignored)
|
|
15
11
|
tenant = resolve_tenant(client_id)
|
|
16
12
|
|
|
@@ -24,14 +20,15 @@ module SpreeCmCommissioner
|
|
|
24
20
|
|
|
25
21
|
private
|
|
26
22
|
|
|
27
|
-
# `load_test_account`
|
|
28
|
-
#
|
|
29
|
-
#
|
|
23
|
+
# Filters the `load_test_account` flag in SQL via the model's `load_test_accounts` scope (see
|
|
24
|
+
# UserDecorator), newest first so a fresh pool is destroyed before older leftovers. No scan
|
|
25
|
+
# cap: works even on tenants with far more than 2,000 users, unlike the old
|
|
26
|
+
# `order(:id).limit(2000).select(&:...)` approach. `.first(count)` in #call adds the LIMIT, so
|
|
27
|
+
# this stays a single query.
|
|
30
28
|
def load_test_accounts(tenant)
|
|
31
29
|
Spree::User.where(tenant_id: tenant&.id)
|
|
32
|
-
.
|
|
33
|
-
.
|
|
34
|
-
.select(&:load_test_account?)
|
|
30
|
+
.load_test_accounts
|
|
31
|
+
.order(id: :desc)
|
|
35
32
|
end
|
|
36
33
|
|
|
37
34
|
def resolve_tenant(client_id)
|
|
@@ -1,22 +1,43 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
module LoadTest
|
|
3
|
-
# Generates the
|
|
4
|
-
# k6 booking flow. An admin picks a ticket variant in the
|
|
5
|
-
# product / variant / event straight off the models and returns the
|
|
6
|
-
# plus a "can this actually be booked?" check so the tester knows it
|
|
3
|
+
# Generates the cash_on_booking_ticket_flow / aba_booking_ticket_flow load-test data for one variant
|
|
4
|
+
# so it can be used by the k6 booking flow. An admin/organizer picks a ticket variant in the
|
|
5
|
+
# dashboard; this reads its product / variant / event straight off the models and returns the
|
|
6
|
+
# payload the flow needs, plus a "can this actually be booked?" check so the tester knows it
|
|
7
|
+
# will drive the flow. Mirrors GenerateVotingFixtureData's cash-on/ABA payment resolution
|
|
8
|
+
# (gems/spree_cm_commissioner/app/services/spree_cm_commissioner/load_test/
|
|
9
|
+
# generate_voting_fixture_data.rb) for the same two payment methods, applied to a variant
|
|
10
|
+
# instead of a voting session.
|
|
7
11
|
#
|
|
8
|
-
# Flow: pick a ticket variant in admin -> copy this payload into
|
|
9
|
-
# load_testing/config/fixture
|
|
10
|
-
# that file when running k6
|
|
12
|
+
# Flow: pick a ticket variant in admin/organizer -> copy this payload into
|
|
13
|
+
# load_testing/config/fixture/<platform|tenant>/<flow>/<env>.json -> Env.fixture()/
|
|
14
|
+
# Env.optionalFixture() reads that file when running k6. Note: the k6 flows themselves still
|
|
15
|
+
# read the actual payment_method_id/payway config from env vars (TENANT_PAYMENT_METHOD_ID,
|
|
16
|
+
# TENANT_PAYWAY_*), not from this JSON — payment_method_id/payway are included here purely so
|
|
17
|
+
# whoever generates the fixture can see (and copy) what's actually configured, same as
|
|
18
|
+
# GenerateVotingFixtureData's topup flows.
|
|
11
19
|
#
|
|
12
|
-
# Returns success({ data: {...}, issues: [...] }). `issues` is empty when the variant
|
|
13
|
-
#
|
|
14
|
-
# `issues` (plus a `ready` flag) are
|
|
15
|
-
# the copied JSON — the k6 flow ignores
|
|
20
|
+
# Returns success({ data: {...}, issues: [...] }). `issues` is empty when the variant is ready
|
|
21
|
+
# to load-test; otherwise it lists why the booking flow would fail — including "no payment
|
|
22
|
+
# method configured" for whichever flow you asked for. `issues` (plus a `ready` flag) are
|
|
23
|
+
# duplicated into `data[:metadata]` so they travel with the copied JSON — the k6 flow ignores
|
|
24
|
+
# that key, it's there for a human to read later.
|
|
16
25
|
class GenerateBookingTicketData
|
|
17
26
|
prepend ::Spree::ServiceModule::Base
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
FLOWS = %w[cash_on_booking_ticket_flow aba_booking_ticket_flow].freeze
|
|
29
|
+
|
|
30
|
+
# ABA's checkout endpoint is fixed regardless of tenant/environment — only `host` (sandbox
|
|
31
|
+
# vs production) differs, and that comes off the payment method. See
|
|
32
|
+
# Vpago::PaywayV2::Checkout#checkout_url in gems/spree_vpago. Mirrors
|
|
33
|
+
# GenerateVotingFixtureData::PAYWAY_CHECKOUT_PATH.
|
|
34
|
+
PAYWAY_CHECKOUT_PATH = '/api/payment-gateway/v1/payments/purchase'.freeze
|
|
35
|
+
|
|
36
|
+
# flow: which payment method to resolve — 'cash_on_booking_ticket_flow' (the default) or
|
|
37
|
+
# 'aba_booking_ticket_flow' (ABA/PayWay v2, adds a `payway` block).
|
|
38
|
+
def call(variant:, flow: 'cash_on_booking_ticket_flow')
|
|
39
|
+
raise ArgumentError, "Unknown flow: #{flow.inspect} (expected one of: #{FLOWS.join(', ')})" unless FLOWS.include?(flow)
|
|
40
|
+
|
|
20
41
|
product = variant.product
|
|
21
42
|
event = product.event
|
|
22
43
|
issues = issues_for(variant, event)
|
|
@@ -25,15 +46,15 @@ module SpreeCmCommissioner
|
|
|
25
46
|
event_permalink: event&.permalink,
|
|
26
47
|
product_slug: product.slug,
|
|
27
48
|
product_id: product.id,
|
|
28
|
-
variant_id: variant.id
|
|
29
|
-
# Informational only — the k6 flow never reads this. Carries the readiness check into
|
|
30
|
-
# the copied JSON itself, so it's visible later without regenerating from admin.
|
|
31
|
-
metadata: {
|
|
32
|
-
ready: issues.empty?,
|
|
33
|
-
issues: issues
|
|
34
|
-
}
|
|
49
|
+
variant_id: variant.id
|
|
35
50
|
}
|
|
36
51
|
|
|
52
|
+
issues.concat(payment_method_issues(variant, flow, data))
|
|
53
|
+
|
|
54
|
+
# Informational only — the k6 flow never reads this. Carries the readiness check into
|
|
55
|
+
# the copied JSON itself, so it's visible later without regenerating from admin/organizer.
|
|
56
|
+
data[:metadata] = { ready: issues.empty?, issues: issues }
|
|
57
|
+
|
|
37
58
|
success(data: data, issues: issues)
|
|
38
59
|
rescue StandardError => e
|
|
39
60
|
failure(nil, e.message)
|
|
@@ -41,6 +62,49 @@ module SpreeCmCommissioner
|
|
|
41
62
|
|
|
42
63
|
private
|
|
43
64
|
|
|
65
|
+
# Resolves the payment method for `flow` into `data`, and (for ABA) its payway payload.
|
|
66
|
+
# Returns the readiness issues — "no payment method configured" is blocking, same as
|
|
67
|
+
# GenerateVotingFixtureData's topup_flow_issues.
|
|
68
|
+
def payment_method_issues(variant, flow, data)
|
|
69
|
+
payment_method = resolve_payment_method(variant.vendor, flow)
|
|
70
|
+
|
|
71
|
+
return ["No #{payment_method_label(flow)} payment method configured for this tenant."] if payment_method.nil?
|
|
72
|
+
|
|
73
|
+
data[:payment_method_id] = payment_method.id
|
|
74
|
+
return [] unless flow == 'aba_booking_ticket_flow'
|
|
75
|
+
|
|
76
|
+
data[:payway] = payway_payload(payment_method)
|
|
77
|
+
return [] if payment_method.preferred_host.to_s.match?(/sandbox/i)
|
|
78
|
+
|
|
79
|
+
[
|
|
80
|
+
"ABA payment method's host (#{payment_method.preferred_host.inspect}) doesn't look like a sandbox URL — " \
|
|
81
|
+
'load tests should run against ABA\'s sandbox, not production.'
|
|
82
|
+
]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def resolve_payment_method(vendor, flow)
|
|
86
|
+
type = flow == 'aba_booking_ticket_flow' ? Vpago::PaymentMethodDecorator::TYPE_PAYWAY_V2 : Vpago::PaymentMethodDecorator::TYPE_CASH_ON
|
|
87
|
+
|
|
88
|
+
Spree::PaymentMethod.active.where(vendor_id: vendor&.id, type: type).first
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def payment_method_label(flow)
|
|
92
|
+
flow == 'aba_booking_ticket_flow' ? 'ABA' : 'cash-on'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def payway_payload(payment_method)
|
|
96
|
+
{
|
|
97
|
+
host: payment_method.preferred_host,
|
|
98
|
+
checkout_path: PAYWAY_CHECKOUT_PATH,
|
|
99
|
+
merchant_id: payment_method.preferred_merchant_id,
|
|
100
|
+
api_key: payment_method.preferred_api_key,
|
|
101
|
+
payment_option: payment_method.preferred_payment_option,
|
|
102
|
+
transaction_fee_fix: payment_method.preferred_transaction_fee_fix.to_f,
|
|
103
|
+
transaction_fee_percentage: payment_method.preferred_transaction_fee_percentage.to_f,
|
|
104
|
+
enable_pre_auth: payment_method.enable_pre_auth?
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
44
108
|
# Each check returns an explanatory string when it would break the booking flow, or nil when the
|
|
45
109
|
# variant is fine on that axis. Order mirrors the flow: availability first, then checkout shape.
|
|
46
110
|
def issues_for(variant, event)
|
|
@@ -3,7 +3,7 @@ module SpreeCmCommissioner
|
|
|
3
3
|
module Guests
|
|
4
4
|
# Whitelists submitted guest params down to whatever KYC fields and dynamic
|
|
5
5
|
# fields are actually configured for the guest's line item, then delegates
|
|
6
|
-
# persistence to
|
|
6
|
+
# persistence to SpreeCmCommissioner::Guests::Update.
|
|
7
7
|
#
|
|
8
8
|
# Expects params shaped as:
|
|
9
9
|
# { first_name: '...', ..., dynamic_fields: { '<dynamic_field_id>' => value_or_option_id_or_array_of_option_ids } }
|
|
@@ -11,10 +11,7 @@ module SpreeCmCommissioner
|
|
|
11
11
|
prepend ::Spree::ServiceModule::Base
|
|
12
12
|
|
|
13
13
|
def call(guest:, params:)
|
|
14
|
-
SpreeCmCommissioner::
|
|
15
|
-
success(guest: guest)
|
|
16
|
-
rescue ActiveRecord::RecordInvalid => e
|
|
17
|
-
failure(guest, e.record.errors.full_messages.to_sentence)
|
|
14
|
+
SpreeCmCommissioner::Guests::Update.call(guest: guest, guest_params: permitted_params(guest, params))
|
|
18
15
|
end
|
|
19
16
|
|
|
20
17
|
private
|
|
@@ -28,11 +25,11 @@ module SpreeCmCommissioner
|
|
|
28
25
|
filtered
|
|
29
26
|
end
|
|
30
27
|
|
|
31
|
-
# Builds the guest_dynamic_fields_attributes array
|
|
32
|
-
# restricted to dynamic fields actually enabled for this guest's product (one
|
|
28
|
+
# Builds the guest_dynamic_fields_attributes array SpreeCmCommissioner::Guests::Update
|
|
29
|
+
# expects, restricted to dynamic fields actually enabled for this guest's product (one
|
|
33
30
|
# `pluck`/load, not a query per submitted field). Option-based fields (checkbox/
|
|
34
31
|
# radio/selection) submit one or more selected option ids and become one
|
|
35
|
-
# GuestDynamicField row per selected option;
|
|
32
|
+
# GuestDynamicField row per selected option; Guests::Update's own auto-cleanup
|
|
36
33
|
# destroys any existing rows for options that were deselected.
|
|
37
34
|
def build_dynamic_field_attributes(guest, dynamic_fields)
|
|
38
35
|
return [] if dynamic_fields.blank?
|
|
@@ -63,9 +63,15 @@ module SpreeCmCommissioner
|
|
|
63
63
|
# --- Elimination ---------------------------------------------------------
|
|
64
64
|
|
|
65
65
|
def sync_elimination_status
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
|
|
66
|
+
# Episode-only advancement (no session mirror was ever created) — just
|
|
67
|
+
# drop the marker when eliminated; nothing to unwind.
|
|
68
|
+
# e.g. vc_a { advanced_to_id: nil, advanced_to_episode_id: 5, eliminated: true } → clear it
|
|
69
|
+
if @voting_contestant.advanced_to_id.blank?
|
|
70
|
+
if @voting_contestant.eliminated? && @voting_contestant.advanced_to_episode_id.present?
|
|
71
|
+
@voting_contestant.update!(advanced_to_episode_id: nil)
|
|
72
|
+
end
|
|
73
|
+
return
|
|
74
|
+
end
|
|
69
75
|
|
|
70
76
|
# Incomplete polymorphic ref — can't resolve a session without both parts.
|
|
71
77
|
# e.g. vc_a { advanced_to_type: nil } → return
|
|
@@ -203,6 +209,7 @@ module SpreeCmCommissioner
|
|
|
203
209
|
@voting_contestant.update!(
|
|
204
210
|
advanced_to_type: nil,
|
|
205
211
|
advanced_to_id: nil,
|
|
212
|
+
advanced_to_episode_id: nil,
|
|
206
213
|
updated_at: Time.current
|
|
207
214
|
)
|
|
208
215
|
end
|