spree_cm_commissioner 2.9.1.pre.pre3 → 2.9.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/.env.example +9 -0
- data/Gemfile.lock +2 -2
- data/app/controllers/concerns/spree_cm_commissioner/content_cachable.rb +7 -0
- data/app/controllers/concerns/spree_cm_commissioner/load_test_account_management.rb +80 -0
- data/app/controllers/concerns/spree_cm_commissioner/load_test_account_scope.rb +46 -0
- data/app/controllers/concerns/spree_cm_commissioner/load_test_login_token_generation.rb +53 -0
- data/app/controllers/spree/admin/guests_controller.rb +4 -2
- data/app/controllers/spree/admin/system/load_test_accounts_controller.rb +24 -0
- data/app/controllers/spree/admin/system/load_test_flows_controller.rb +11 -0
- data/app/controllers/spree/admin/system/load_test_login_tokens_controller.rb +16 -0
- data/app/controllers/spree/admin/system/load_testing_controller.rb +9 -0
- data/app/controllers/spree/api/v2/storefront/draft_orders_controller.rb +48 -0
- data/app/controllers/spree/api/v2/storefront/preview_sections_controller.rb +1 -7
- data/app/controllers/spree/api/v2/storefront/telegram_oauth_tokens_controller.rb +28 -0
- data/app/controllers/spree/api/v2/tenant/draft_orders_controller.rb +49 -0
- data/app/controllers/spree/api/v2/tenant/preview_products_controller.rb +1 -1
- data/app/controllers/spree/api/v2/tenant/preview_sections_controller.rb +1 -7
- data/app/controllers/spree_cm_commissioner/admin/variants_controller_decorator.rb +11 -0
- data/app/controllers/spree_cm_commissioner/api/v2/storefront/checkout_controller_decorator.rb +0 -2
- data/app/interactors/spree_cm_commissioner/telegram_oauth_id_token_provider.rb +105 -0
- data/app/interactors/spree_cm_commissioner/telegram_oauth_token_exchanger.rb +71 -0
- data/app/interactors/spree_cm_commissioner/user_id_token_authenticator.rb +2 -5
- data/app/interactors/spree_cm_commissioner/user_password_authenticator.rb +23 -1
- data/app/interactors/spree_cm_commissioner/user_telegram_oauth_authenticator.rb +78 -0
- data/app/mailers/spree/order_mailer_decorator.rb +1 -0
- data/app/models/concerns/spree_cm_commissioner/kyc_bitwise.rb +17 -0
- data/app/models/concerns/spree_cm_commissioner/line_items_filter_scope.rb +5 -3
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +11 -0
- data/app/models/concerns/spree_cm_commissioner/product_delegation.rb +2 -0
- data/app/models/spree_cm_commissioner/homepage_section_relatable.rb +25 -0
- data/app/models/spree_cm_commissioner/order_decorator.rb +0 -6
- data/app/models/spree_cm_commissioner/payment_method_decorator.rb +2 -0
- data/app/models/spree_cm_commissioner/pricing_model_route.rb +8 -0
- data/app/models/spree_cm_commissioner/product_decorator.rb +1 -0
- data/app/models/spree_cm_commissioner/trip.rb +13 -1
- data/app/models/spree_cm_commissioner/user_decorator.rb +6 -0
- data/app/overrides/spree/admin/payment_methods/index/pagination.html.erb.deface +3 -0
- data/app/overrides/spree/admin/payment_methods/index/preserve_tab_in_search.html.erb.deface +4 -0
- data/app/overrides/spree/admin/products/_form/enable_social_sharing_email.html.erb.deface +13 -0
- data/app/overrides/spree/admin/variants/index/fixture_action.html.erb.deface +9 -0
- data/app/queries/spree_cm_commissioner/multi_leg_trips_query.rb +2 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/trip_result_serializer.rb +2 -2
- data/app/serializers/spree_cm_commissioner/v2/storefront/trip_serializer.rb +3 -1
- data/app/services/spree_cm_commissioner/draft_order/create.rb +86 -0
- data/app/services/spree_cm_commissioner/load_test/create_accounts.rb +68 -0
- data/app/services/spree_cm_commissioner/load_test/destroy_accounts.rb +50 -0
- data/app/services/spree_cm_commissioner/load_test/flow_registry.rb +26 -0
- data/app/services/spree_cm_commissioner/load_test/generate_booking_ticket_data.rb +140 -0
- data/app/services/spree_cm_commissioner/load_test/generate_voting_fixture_data.rb +178 -0
- data/app/services/spree_cm_commissioner/load_test/login_token.rb +46 -0
- data/app/services/spree_cm_commissioner/order/guests/update.rb +56 -0
- data/app/services/spree_cm_commissioner/pricing_models/create_with_rule_groups.rb +9 -3
- data/app/services/spree_cm_commissioner/pricing_models/preview.rb +49 -6
- data/app/services/spree_cm_commissioner/pricing_models/update_with_rule_groups.rb +10 -7
- data/app/services/spree_cm_commissioner/trips/update_single_leg.rb +6 -1
- data/app/services/spree_cm_commissioner/user_authenticator.rb +22 -1
- data/app/services/spree_cm_commissioner/voting_credits/allocate.rb +13 -2
- data/app/views/spree/admin/featured_trips/index.html.erb +1 -1
- data/app/views/spree/admin/guests/_form.html.erb +37 -0
- data/app/views/spree/admin/shared/_system_tabs.html.erb +7 -0
- data/app/views/spree/admin/system/load_test_accounts/_login_tokens_modal.html.erb +52 -0
- data/app/views/spree/admin/system/load_test_accounts/index.html.erb +175 -0
- data/app/views/spree/admin/system/load_test_flows/index.html.erb +66 -0
- data/app/views/spree/admin/system/load_testing/index.html.erb +25 -0
- data/app/views/spree/admin/variants/fixture.html.erb +49 -0
- data/config/initializers/doorkeeper.rb +10 -6
- data/config/initializers/spree_permitted_attributes.rb +30 -0
- data/config/load_test_flows/platform.yml +25 -0
- data/config/load_test_flows/tenant.yml +46 -0
- data/config/locales/en.yml +94 -0
- data/config/locales/km.yml +2 -0
- data/config/routes.rb +19 -0
- data/db/migrate/20260630000001_add_trip_to_cm_pricing_model_routes.rb +7 -0
- data/db/migrate/20260703065239_add_position_to_cm_voting_contestants.rb +1 -1
- data/db/migrate/20260708150000_add_name_and_short_name_to_cm_trips.rb +6 -0
- data/lib/spree_cm_commissioner/test_helper/factories/pricing_model_trip_factory.rb +11 -0
- data/lib/spree_cm_commissioner/trip_result.rb +3 -1
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +39 -4
|
@@ -39,29 +39,72 @@ module SpreeCmCommissioner
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def find_pricing_models(group_contexts)
|
|
42
|
-
|
|
42
|
+
transit_pricing_models(group_contexts) + variant_pricing_models(group_contexts)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
# Trip-level pricing overrides route-level pricing for the same journey; route pricing
|
|
46
|
+
# is the fallback when the trip (or connected journey's parent trip) has none assigned.
|
|
47
|
+
def transit_pricing_models(group_contexts)
|
|
46
48
|
transit_contexts = group_contexts.select { |ctx| ctx.trip_id.present? }
|
|
47
49
|
return [] if transit_contexts.blank?
|
|
48
50
|
|
|
49
51
|
origin_place_id, destination_place_id = determine_origin_destination(transit_contexts)
|
|
50
52
|
return [] if origin_place_id.blank? || destination_place_id.blank?
|
|
51
53
|
|
|
52
|
-
vendor_ids = transit_contexts
|
|
54
|
+
vendor_ids = transit_vendor_ids(transit_contexts)
|
|
53
55
|
return [] if vendor_ids.blank?
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
trip_level_pricing_models(
|
|
58
|
+
trip_ids: trip_candidate_ids(transit_contexts),
|
|
59
|
+
origin_place_id: origin_place_id,
|
|
60
|
+
destination_place_id: destination_place_id,
|
|
61
|
+
vendor_ids: vendor_ids
|
|
62
|
+
).presence || route_level_pricing_models(
|
|
63
|
+
route_ids: trip_route_ids(transit_contexts),
|
|
64
|
+
origin_place_id: origin_place_id,
|
|
65
|
+
destination_place_id: destination_place_id,
|
|
66
|
+
vendor_ids: vendor_ids
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def trip_level_pricing_models(trip_ids:, origin_place_id:, destination_place_id:, vendor_ids:)
|
|
71
|
+
return [] if trip_ids.empty?
|
|
56
72
|
|
|
57
73
|
scope = SpreeCmCommissioner::PricingModelRoute
|
|
58
74
|
.active
|
|
75
|
+
.trip_scoped
|
|
76
|
+
.where(trip_id: trip_ids)
|
|
59
77
|
.for_origin_destination(origin_place_id, destination_place_id)
|
|
60
|
-
.includes(:pricing_model)
|
|
61
78
|
|
|
79
|
+
pricing_models_for(scope, vendor_ids)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def route_level_pricing_models(route_ids:, origin_place_id:, destination_place_id:, vendor_ids:)
|
|
83
|
+
scope = SpreeCmCommissioner::PricingModelRoute
|
|
84
|
+
.active
|
|
85
|
+
.route_scoped
|
|
86
|
+
.for_origin_destination(origin_place_id, destination_place_id)
|
|
62
87
|
scope = scope.where(route_id: route_ids) if route_ids.any?
|
|
63
88
|
|
|
64
|
-
scope
|
|
89
|
+
pricing_models_for(scope, vendor_ids)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Resolves a PricingModelRoute scope into its pricing models, restricted to vendors
|
|
93
|
+
# actually present among the line items being priced.
|
|
94
|
+
def pricing_models_for(scope, vendor_ids)
|
|
95
|
+
scope.includes(:pricing_model).map(&:pricing_model).select { |pm| vendor_ids.include?(pm.vendor_id) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# A booking's legs can each have their own trip-level price, and a connected journey
|
|
99
|
+
# (multiple legs sharing connected_trip_id) can also have its own price on the shared
|
|
100
|
+
# parent trip. This returns every trip id worth checking for either case.
|
|
101
|
+
def trip_candidate_ids(transit_contexts)
|
|
102
|
+
connected_trip_id = Integer(transit_contexts.first&.connected_trip_id.to_s, exception: false)
|
|
103
|
+
(transit_contexts.map(&:trip_id) + [connected_trip_id]).compact.uniq
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def transit_vendor_ids(transit_contexts)
|
|
107
|
+
transit_contexts.map { |ctx| variant_vendor_id(ctx.variant_id) }.compact.uniq
|
|
65
108
|
end
|
|
66
109
|
|
|
67
110
|
def variant_pricing_models(group_contexts)
|
|
@@ -3,11 +3,11 @@ module SpreeCmCommissioner
|
|
|
3
3
|
class UpdateWithRuleGroups
|
|
4
4
|
prepend ::Spree::ServiceModule::Base
|
|
5
5
|
|
|
6
|
-
def call(pricing_model:, params:, route: nil, origin_place_id: nil, destination_place_id: nil)
|
|
6
|
+
def call(pricing_model:, params:, route: nil, trip: nil, origin_place_id: nil, destination_place_id: nil) # rubocop:disable Metrics/ParameterLists
|
|
7
7
|
ApplicationRecord.transaction do
|
|
8
8
|
pricing_model.update!(model_params(params))
|
|
9
9
|
sync_rule_groups(pricing_model, params[:rule_groups])
|
|
10
|
-
update_route_assignment(pricing_model, route, origin_place_id, destination_place_id) if route.present?
|
|
10
|
+
update_route_assignment(pricing_model, route, trip, origin_place_id, destination_place_id) if route.present?
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
success(pricing_model)
|
|
@@ -47,14 +47,17 @@ module SpreeCmCommissioner
|
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
# Each pricing model created via this flow has a single assignment row; update it in place so
|
|
51
|
+
# the assigned trip (and O-D) can be changed on edit. Trip-scoped rows take O-D from the trip.
|
|
52
|
+
def update_route_assignment(pricing_model, route, trip, origin_place_id, destination_place_id)
|
|
53
|
+
origin_place_id = trip.origin_place_id if trip.present?
|
|
54
|
+
destination_place_id = trip.destination_place_id if trip.present?
|
|
51
55
|
return unless origin_place_id.present? && destination_place_id.present?
|
|
52
56
|
|
|
53
|
-
route_assignment = PricingModelRoute.find_or_initialize_by(
|
|
54
|
-
route: route,
|
|
55
|
-
pricing_model: pricing_model
|
|
56
|
-
)
|
|
57
|
+
route_assignment = PricingModelRoute.find_or_initialize_by(pricing_model: pricing_model)
|
|
57
58
|
route_assignment.update!(
|
|
59
|
+
route: route,
|
|
60
|
+
trip: trip,
|
|
58
61
|
origin_place_id: origin_place_id,
|
|
59
62
|
destination_place_id: destination_place_id
|
|
60
63
|
)
|
|
@@ -81,7 +81,12 @@ module SpreeCmCommissioner
|
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
def trip_attrs(form)
|
|
84
|
-
{
|
|
84
|
+
{
|
|
85
|
+
allow_booking: form.allow_booking,
|
|
86
|
+
allow_seat_selection: form.allow_seat_selection,
|
|
87
|
+
name: form.name,
|
|
88
|
+
short_name: form.short_name
|
|
89
|
+
}.compact
|
|
85
90
|
end
|
|
86
91
|
|
|
87
92
|
def product_attrs(form)
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
module SpreeCmCommissioner
|
|
3
3
|
class UserAuthenticator
|
|
4
4
|
# :username, :password
|
|
5
|
-
# :id_token
|
|
5
|
+
# :id_token (Firebase social login)
|
|
6
|
+
# :telegram_id_token -> Telegram OIDC login (Firebase/Google social login)
|
|
7
|
+
# :user_id_token (load-test JWT, self-identifying, minted by LoadTest::LoginToken)
|
|
6
8
|
# :fb_access_token
|
|
7
9
|
# :telegram_init_data, :tg_bot
|
|
8
10
|
# :session_id
|
|
@@ -33,9 +35,17 @@ module SpreeCmCommissioner
|
|
|
33
35
|
tenant_id = oauth_application&.tenant_id
|
|
34
36
|
|
|
35
37
|
case flow_type(params)
|
|
38
|
+
when 'load_test_auth'
|
|
39
|
+
# k6 sends the load-test JWT as `user_id_token`; internally it's a login_token (see
|
|
40
|
+
# UserPasswordAuthenticator's self-identifying, token-only path).
|
|
41
|
+
options = { login_token: params[:user_id_token], tenant_id: tenant_id }
|
|
42
|
+
SpreeCmCommissioner::UserPasswordAuthenticator.call(options)
|
|
36
43
|
when 'login_auth'
|
|
37
44
|
options = { login: params[:username], password: params[:password], tenant_id: tenant_id }
|
|
38
45
|
SpreeCmCommissioner::UserPasswordAuthenticator.call(options)
|
|
46
|
+
when 'telegram_oauth_auth'
|
|
47
|
+
options = { id_token: params[:telegram_id_token], tenant_id: tenant_id }
|
|
48
|
+
SpreeCmCommissioner::UserTelegramOauthAuthenticator.call(options)
|
|
39
49
|
when 'social_auth'
|
|
40
50
|
options = { id_token: params[:id_token], tenant_id: tenant_id }
|
|
41
51
|
SpreeCmCommissioner::UserIdTokenAuthenticator.call(options)
|
|
@@ -52,7 +62,12 @@ module SpreeCmCommissioner
|
|
|
52
62
|
end
|
|
53
63
|
|
|
54
64
|
def self.flow_type(params)
|
|
65
|
+
# A separate params key from `id_token` (social login) means we only ever attempt to decode
|
|
66
|
+
# a token that's actually claiming to be a load-test token, instead of decoding every
|
|
67
|
+
# request's id_token to find out.
|
|
68
|
+
return 'load_test_auth' if load_test_id_token?(params[:user_id_token])
|
|
55
69
|
return 'login_auth' if params.key?(:username) && params.key?(:password)
|
|
70
|
+
return 'telegram_oauth_auth' if params[:telegram_id_token].present?
|
|
56
71
|
return 'social_auth' if params.key?(:id_token)
|
|
57
72
|
return 'facebook_auth' if params.key?(:fb_access_token)
|
|
58
73
|
return 'telegram_web_app_auth' if params.key?(:telegram_init_data) && params.key?(:tg_bot)
|
|
@@ -61,6 +76,12 @@ module SpreeCmCommissioner
|
|
|
61
76
|
raise exception(I18n.t('authenticator.invalid_or_missing_params'))
|
|
62
77
|
end
|
|
63
78
|
|
|
79
|
+
# True only for a load-test id_token — it's signed with our own secret, so it decodes here
|
|
80
|
+
# whereas a real Firebase/Google id_token (RS256, Google's keys) returns nil.
|
|
81
|
+
def self.load_test_id_token?(token)
|
|
82
|
+
token.present? && SpreeCmCommissioner::LoadTest::LoginToken.decode(token).present?
|
|
83
|
+
end
|
|
84
|
+
|
|
64
85
|
def self.exception(message)
|
|
65
86
|
Doorkeeper::Errors::DoorkeeperError.new(message)
|
|
66
87
|
end
|
|
@@ -29,8 +29,8 @@ module SpreeCmCommissioner
|
|
|
29
29
|
voting_credit.save!
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
record_transaction(voting_credit, total_paid_vote_credits) if total_paid_vote_credits.positive?
|
|
33
|
+
record_transaction_free(voting_credit, total_free_vote_credits) if total_free_vote_credits.positive?
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
record_free_claims
|
|
@@ -99,6 +99,17 @@ module SpreeCmCommissioner
|
|
|
99
99
|
)
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
+
def record_transaction_free(vote_credit, total_amount)
|
|
103
|
+
vote_credit.voting_credit_transactions.create!(
|
|
104
|
+
tenant_id: @order.tenant_id,
|
|
105
|
+
action: :free_claim,
|
|
106
|
+
amount: total_amount,
|
|
107
|
+
originator: @order,
|
|
108
|
+
memo: "Free claim for order ##{@order.number}",
|
|
109
|
+
user_total_amount: vote_credit.available_votes
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
102
113
|
def record_free_claims
|
|
103
114
|
return if @order.user_id.blank?
|
|
104
115
|
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
<% if @featured_trips.any? %>
|
|
86
86
|
<% @featured_trips.each do |trip| %>
|
|
87
87
|
<tr id="<%= spree_dom_id trip %>">
|
|
88
|
-
<td><%= link_to trip.
|
|
88
|
+
<td><%= link_to trip.display_name || trip.id, spree.edit_admin_product_path(trip.product) if trip.product %></td>
|
|
89
89
|
<td><%= trip.vendor&.name %></td>
|
|
90
90
|
<td><%= [trip.origin_place&.name, trip.destination_place&.name].compact.join(' to ') %></td>
|
|
91
91
|
<td><%= l(trip.departure_time, format: :short) if trip.departure_time %></td>
|
|
@@ -106,6 +106,43 @@
|
|
|
106
106
|
<%= f.label :phone_number, raw(Spree.t(:phone_number) + required_span_tag) %>
|
|
107
107
|
<%= f.text_field :phone_number, class: 'form-control' %>
|
|
108
108
|
<% end %>
|
|
109
|
+
<% when :guest_contact %>
|
|
110
|
+
<%= f.field_container :contact do %>
|
|
111
|
+
<%= f.label :contact, raw(Spree.t(:contact) + required_span_tag) %>
|
|
112
|
+
<%= f.text_field :contact, class: 'form-control' %>
|
|
113
|
+
<% end %>
|
|
114
|
+
<% when :guest_organization %>
|
|
115
|
+
<%= f.field_container :other_organization do %>
|
|
116
|
+
<%= f.label :other_organization, raw(Spree.t(:organization) + required_span_tag) %>
|
|
117
|
+
<%= f.text_field :other_organization, class: 'form-control' %>
|
|
118
|
+
<% end %>
|
|
119
|
+
<% when :guest_address %>
|
|
120
|
+
<%= f.field_container :address do %>
|
|
121
|
+
<%= f.label :address, raw(Spree.t(:address) + required_span_tag) %>
|
|
122
|
+
<%= f.text_field :address, class: 'form-control' %>
|
|
123
|
+
<% end %>
|
|
124
|
+
<% when :guest_social_contact %>
|
|
125
|
+
<div class="row">
|
|
126
|
+
<div class="col-12 col-lg-6">
|
|
127
|
+
<%= f.field_container :social_contact_platform do %>
|
|
128
|
+
<%= f.label :social_contact_platform, raw(Spree.t(:social_contact_platform) + required_span_tag) %>
|
|
129
|
+
<%= f.select :social_contact_platform,
|
|
130
|
+
SpreeCmCommissioner::Guest.social_contact_platforms.keys.map { |platform| [platform.to_s.capitalize, platform] },
|
|
131
|
+
{ include_blank: true }, class: 'fullwidth select2' %>
|
|
132
|
+
<% end %>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="col-12 col-lg-6">
|
|
135
|
+
<%= f.field_container :social_contact do %>
|
|
136
|
+
<%= f.label :social_contact, raw(Spree.t(:social_contact) + required_span_tag) %>
|
|
137
|
+
<%= f.text_field :social_contact, class: 'form-control' %>
|
|
138
|
+
<% end %>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
<% when :guest_expectation %>
|
|
142
|
+
<%= f.field_container :expectation do %>
|
|
143
|
+
<%= f.label :expectation, raw(Spree.t(:expectation) + required_span_tag) %>
|
|
144
|
+
<%= f.text_area :expectation, class: 'form-control', rows: 3 %>
|
|
145
|
+
<% end %>
|
|
109
146
|
<% end %>
|
|
110
147
|
<% end %>
|
|
111
148
|
</div>
|
|
@@ -23,4 +23,11 @@
|
|
|
23
23
|
admin_system_maintenance_tasks_path,
|
|
24
24
|
class: "nav-link #{'active' if current == :maintenance_tasks}" %>
|
|
25
25
|
<% end %>
|
|
26
|
+
|
|
27
|
+
<%= content_tag :li, class: 'nav-item' do %>
|
|
28
|
+
<%= link_to_with_icon 'speedometer.svg',
|
|
29
|
+
Spree.t('load_test.nav_label'),
|
|
30
|
+
admin_system_load_testing_index_path,
|
|
31
|
+
class: "nav-link #{'active' if current == :load_testing}" %>
|
|
32
|
+
<% end %>
|
|
26
33
|
<% end %>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<%# Static, always on the page (hidden until the JS in manage_accounts.html.erb adds `.open`). The
|
|
2
|
+
Generate button opens this immediately, then fetches the tokens over AJAX and fills the
|
|
3
|
+
textarea — no page navigation. Fully self-contained: visuals come from the inline <style> here,
|
|
4
|
+
no dependency on Bootstrap's JS/CSS or any external bundle. %>
|
|
5
|
+
<div id="login_tokens_modal_backdrop" class="login-tokens-modal-backdrop"></div>
|
|
6
|
+
<div id="loginTokensModal" class="login-tokens-modal" role="dialog" aria-labelledby="loginTokensModalLabel">
|
|
7
|
+
<div class="login-tokens-modal-header">
|
|
8
|
+
<h5 class="mb-0" id="loginTokensModalLabel"><%= Spree.t('load_test.accounts.login_tokens_json') %></h5>
|
|
9
|
+
<button type="button" class="btn btn-sm btn-outline-secondary login-tokens-modal-close" data-dismiss="modal" aria-label="<%= Spree.t(:close) %>">
|
|
10
|
+
<span aria-hidden="true">×</span>
|
|
11
|
+
</button>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="login-tokens-modal-body">
|
|
14
|
+
<div class="d-flex justify-content-between align-items-center mb-2 flex-shrink-0">
|
|
15
|
+
<label class="mb-0"><%= Spree.t('load_test.accounts.copy_into_k6') %></label>
|
|
16
|
+
<button type="button" class="btn btn-sm btn-outline-primary p-1" title="<%= Spree.t('load_test.accounts.copy_json') %>" aria-label="<%= Spree.t('load_test.accounts.copy_json') %>"
|
|
17
|
+
onclick="navigator.clipboard.writeText(document.getElementById('login_tokens_json').value); show_flash('success', <%= Spree.t('load_test.accounts.json_copied').to_json %>)">
|
|
18
|
+
<%= svg_icon name: 'clipboard', width: 16, height: 16, classes: 'm-0' %>
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
<div id="login_tokens_status" class="text-muted small mb-2 flex-shrink-0"></div>
|
|
22
|
+
<%= text_area_tag :login_tokens_json, nil,
|
|
23
|
+
readonly: true, class: 'form-control font-monospace login-tokens-modal-textarea' %>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<style>
|
|
28
|
+
.login-tokens-modal-backdrop {
|
|
29
|
+
position: fixed; inset: 0; background: rgba(0, 0, 0, 0.4);
|
|
30
|
+
opacity: 0; visibility: hidden; transition: opacity .2s ease; z-index: 1040;
|
|
31
|
+
}
|
|
32
|
+
.login-tokens-modal-backdrop.open { opacity: 1; visibility: visible; }
|
|
33
|
+
.login-tokens-modal {
|
|
34
|
+
position: fixed; top: 0; right: 0; height: 100%; width: 560px; max-width: 92vw;
|
|
35
|
+
background: #fff; box-shadow: -2px 0 12px rgba(0, 0, 0, .2);
|
|
36
|
+
transform: translateX(100%); transition: transform .25s ease; z-index: 1050;
|
|
37
|
+
display: flex; flex-direction: column;
|
|
38
|
+
}
|
|
39
|
+
.login-tokens-modal.open { transform: translateX(0); }
|
|
40
|
+
.login-tokens-modal-header {
|
|
41
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
42
|
+
padding: 1rem 1.25rem; border-bottom: 1px solid #e9ecef; flex-shrink: 0;
|
|
43
|
+
}
|
|
44
|
+
.login-tokens-modal-close {
|
|
45
|
+
line-height: 1; font-size: 1.25rem; padding: 0 .5rem;
|
|
46
|
+
}
|
|
47
|
+
.login-tokens-modal-body {
|
|
48
|
+
padding: 1.25rem; overflow: hidden; flex: 1; min-height: 0;
|
|
49
|
+
display: flex; flex-direction: column;
|
|
50
|
+
}
|
|
51
|
+
.login-tokens-modal-textarea { flex: 1; min-height: 0; resize: none; }
|
|
52
|
+
</style>
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<%= render 'spree/admin/shared/system_tabs', current: :load_testing %>
|
|
2
|
+
|
|
3
|
+
<%= link_to Spree.t('load_test.back_to_load_testing'), admin_system_load_testing_index_path, class: 'btn btn-outline-secondary btn-sm mb-3' %>
|
|
4
|
+
|
|
5
|
+
<div class="row">
|
|
6
|
+
<div class="col-md-12">
|
|
7
|
+
<div class="card mb-3">
|
|
8
|
+
<div class="card-header"><h5 class="mb-0"><%= Spree.t('load_test.accounts.manage_accounts_platform') %></h5></div>
|
|
9
|
+
<div class="card-body">
|
|
10
|
+
<p class="text-muted">
|
|
11
|
+
<%= Spree.t('load_test.accounts.manage_accounts_body_html').html_safe %>
|
|
12
|
+
</p>
|
|
13
|
+
<%= form_tag admin_system_load_test_accounts_path, method: :post do %>
|
|
14
|
+
<div class="row">
|
|
15
|
+
<div class="col-md-3 form-group">
|
|
16
|
+
<label for="count"><%= Spree.t('load_test.accounts.count') %></label>
|
|
17
|
+
<%= number_field_tag :count, params[:count] || 20, class: 'form-control', min: 1 %>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="col-md-4 form-group">
|
|
20
|
+
<label for="email_prefix"><%= Spree.t('load_test.accounts.email_prefix') %></label>
|
|
21
|
+
<%= text_field_tag :email_prefix, params[:email_prefix] || 'loadtest', class: 'form-control' %>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="col-md-5 form-group">
|
|
24
|
+
<label for="email_domain"><%= Spree.t('load_test.accounts.email_domain') %></label>
|
|
25
|
+
<%= text_field_tag :email_domain, params[:email_domain], class: 'form-control', placeholder: Spree.t('load_test.accounts.email_domain_placeholder') %>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
<%= button_tag Spree.t('load_test.accounts.create_accounts'), type: :submit, class: 'btn btn-primary' %>
|
|
29
|
+
<%= button_tag Spree.t('load_test.accounts.destroy_accounts'), type: :submit,
|
|
30
|
+
formaction: bulk_destroy_admin_system_load_test_accounts_path,
|
|
31
|
+
data: { confirm: Spree.t('load_test.accounts.destroy_accounts_confirm') },
|
|
32
|
+
class: 'btn btn-outline-danger' %>
|
|
33
|
+
<% end %>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="card mb-3">
|
|
38
|
+
<div class="card-header"><h5 class="mb-0"><%= Spree.t('load_test.accounts.generate_login_tokens_platform') %></h5></div>
|
|
39
|
+
<div class="card-body">
|
|
40
|
+
<p class="text-muted">
|
|
41
|
+
<%= Spree.t('load_test.accounts.generate_login_tokens_body_html', ttl: SpreeCmCommissioner::LoadTest::LoginToken::TTL.inspect).html_safe %>
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
<div class="d-flex justify-content-between align-items-start flex-wrap">
|
|
45
|
+
<%# Drives the table's own pagination below — a plain GET reload, separate from the AJAX
|
|
46
|
+
Generate Login Tokens form. Changing Per Page resets back to page 1. A typed number
|
|
47
|
+
input rather than Spree's usual 25/50/75 dropdown, since the load-test pool can run
|
|
48
|
+
well past those fixed sizes. %>
|
|
49
|
+
<%= form_tag admin_system_load_test_accounts_path, method: :get, class: 'form-inline mb-3' do %>
|
|
50
|
+
<label for="per_page" class="mr-2"><%= Spree.t('load_test.accounts.per_page') %></label>
|
|
51
|
+
<%= number_field_tag :per_page, @export_accounts.limit_value, class: 'form-control mr-2 mb-2', style: 'width: 100px', min: 1 %>
|
|
52
|
+
<%= button_tag Spree.t('load_test.accounts.apply'), type: :submit, name: nil, class: 'btn btn-outline-secondary mb-2' %>
|
|
53
|
+
<% end %>
|
|
54
|
+
|
|
55
|
+
<%# JS below intercepts submit: opens the drawer immediately, then fetches the tokens as
|
|
56
|
+
JSON into it — no navigation. data-turbo:false keeps Turbo out of the form entirely.
|
|
57
|
+
Hidden page/per_page mirror the table's own pagination below, so the generated tokens
|
|
58
|
+
always match what's on screen — page the table to change which accounts get tokens. %>
|
|
59
|
+
<%= form_tag admin_system_load_test_login_tokens_path,
|
|
60
|
+
method: :post, id: 'login_tokens_form', class: 'form-inline mb-3', data: { turbo: false } do %>
|
|
61
|
+
<%= hidden_field_tag :page, @export_accounts.current_page %>
|
|
62
|
+
<%= hidden_field_tag :per_page, @export_accounts.limit_value %>
|
|
63
|
+
<%= button_tag Spree.t('load_test.accounts.generate_login_tokens'), type: :submit, class: 'btn btn-primary mb-2' %>
|
|
64
|
+
<% end %>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<% if @export_accounts.any? %>
|
|
68
|
+
<div class="table-responsive">
|
|
69
|
+
<table class="table mb-0">
|
|
70
|
+
<thead class="text-muted">
|
|
71
|
+
<tr>
|
|
72
|
+
<th><%= Spree.t('load_test.accounts.table_no') %></th>
|
|
73
|
+
<th><%= Spree.t('load_test.accounts.table_email') %></th>
|
|
74
|
+
<th><%= Spree.t('load_test.accounts.table_user_id') %></th>
|
|
75
|
+
<th><%= Spree.t('load_test.accounts.table_created_at') %></th>
|
|
76
|
+
<th><%= Spree.t('load_test.accounts.table_last_sign_in_at') %></th>
|
|
77
|
+
<th><%= Spree.t('load_test.accounts.table_current_sign_in_at') %></th>
|
|
78
|
+
</tr>
|
|
79
|
+
</thead>
|
|
80
|
+
<tbody>
|
|
81
|
+
<% @export_accounts.each_with_index do |account, index| %>
|
|
82
|
+
<tr>
|
|
83
|
+
<td><%= @export_accounts.offset_value + index + 1 %></td>
|
|
84
|
+
<td><%= account.email %></td>
|
|
85
|
+
<td><code><%= account.id %></code></td>
|
|
86
|
+
<td><%= account.created_at&.strftime('%Y-%m-%d %H:%M') %></td>
|
|
87
|
+
<td><%= account.last_sign_in_at&.strftime('%Y-%m-%d %H:%M') || '—' %></td>
|
|
88
|
+
<td><%= account.current_sign_in_at&.strftime('%Y-%m-%d %H:%M') || '—' %></td>
|
|
89
|
+
</tr>
|
|
90
|
+
<% end %>
|
|
91
|
+
</tbody>
|
|
92
|
+
</table>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<%= paginate @export_accounts, theme: 'admin-twitter-bootstrap-4' %>
|
|
96
|
+
<% else %>
|
|
97
|
+
<div class="m-5 text-center no-objects-found">
|
|
98
|
+
<%= Spree.t('load_test.accounts.no_accounts_found') %>
|
|
99
|
+
</div>
|
|
100
|
+
<% end %>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
<%# Always on the page (outside the .row/.col so its position:fixed is viewport-relative), hidden
|
|
108
|
+
until the JS below opens it. Populated over AJAX — see the fetch handler. %>
|
|
109
|
+
<%= render "login_tokens_modal" %>
|
|
110
|
+
|
|
111
|
+
<script>
|
|
112
|
+
(function() {
|
|
113
|
+
var i18n = {
|
|
114
|
+
generating: <%= Spree.t('load_test.accounts.generating').to_json.html_safe %>,
|
|
115
|
+
errorPrefix: <%= Spree.t('load_test.accounts.error_prefix').to_json.html_safe %>,
|
|
116
|
+
requestFailedTemplate: <%= Spree.t('load_test.accounts.request_failed', status: '%{status}').to_json.html_safe %>
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
var form = document.getElementById('login_tokens_form');
|
|
120
|
+
var drawer = document.getElementById('loginTokensModal');
|
|
121
|
+
var backdrop = document.getElementById('login_tokens_modal_backdrop');
|
|
122
|
+
var textarea = document.getElementById('login_tokens_json');
|
|
123
|
+
var status = document.getElementById('login_tokens_status');
|
|
124
|
+
if (!form || !drawer || !backdrop) return;
|
|
125
|
+
|
|
126
|
+
function openDrawer() {
|
|
127
|
+
drawer.classList.add('open');
|
|
128
|
+
backdrop.classList.add('open');
|
|
129
|
+
document.body.style.overflow = 'hidden';
|
|
130
|
+
}
|
|
131
|
+
function closeDrawer() {
|
|
132
|
+
drawer.classList.remove('open');
|
|
133
|
+
backdrop.classList.remove('open');
|
|
134
|
+
document.body.style.removeProperty('overflow');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
backdrop.addEventListener('click', closeDrawer);
|
|
138
|
+
drawer.querySelectorAll('[data-dismiss="modal"]').forEach(function(btn) {
|
|
139
|
+
btn.addEventListener('click', closeDrawer);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
form.addEventListener('submit', function(event) {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
|
|
145
|
+
// Open first, show a loading state — the drawer never waits on the request to appear.
|
|
146
|
+
openDrawer();
|
|
147
|
+
textarea.value = '';
|
|
148
|
+
status.textContent = i18n.generating;
|
|
149
|
+
|
|
150
|
+
var csrf = document.querySelector('meta[name="csrf-token"]');
|
|
151
|
+
fetch(form.action, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
credentials: 'same-origin',
|
|
154
|
+
headers: {
|
|
155
|
+
'Accept': 'application/json',
|
|
156
|
+
'X-CSRF-Token': csrf ? csrf.content : ''
|
|
157
|
+
},
|
|
158
|
+
body: new FormData(form)
|
|
159
|
+
})
|
|
160
|
+
.then(function(response) {
|
|
161
|
+
return response.json().then(function(data) {
|
|
162
|
+
if (!response.ok) throw new Error(data.error || i18n.requestFailedTemplate.replace('%{status}', response.status));
|
|
163
|
+
return data;
|
|
164
|
+
});
|
|
165
|
+
})
|
|
166
|
+
.then(function(data) {
|
|
167
|
+
textarea.value = JSON.stringify(data, null, 2);
|
|
168
|
+
status.textContent = '';
|
|
169
|
+
})
|
|
170
|
+
.catch(function(error) {
|
|
171
|
+
status.textContent = i18n.errorPrefix + error.message;
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
})();
|
|
175
|
+
</script>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<%= render 'spree/admin/shared/system_tabs', current: :load_testing %>
|
|
2
|
+
|
|
3
|
+
<%= link_to Spree.t('load_test.back_to_load_testing'), admin_system_load_testing_index_path, class: 'btn btn-outline-secondary btn-sm mb-3' %>
|
|
4
|
+
|
|
5
|
+
<p class="text-muted">
|
|
6
|
+
<%= Spree.t('load_test.flows.description_html').html_safe %>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<% @load_test_flows.each_with_index do |(flow_name, flow), index| %>
|
|
10
|
+
<details class="card mb-3" <%= 'open' if index.zero? %>>
|
|
11
|
+
<summary class="card-header d-flex justify-content-between align-items-center" style="cursor: pointer; list-style: none;">
|
|
12
|
+
<div>
|
|
13
|
+
<h6 class="mb-1"><%= flow_name.to_s.humanize %></h6>
|
|
14
|
+
<span class="text-muted small"><%= flow[:description] %></span>
|
|
15
|
+
</div>
|
|
16
|
+
<%= svg_icon name: 'chevron-down', width: 16, height: 16 %>
|
|
17
|
+
</summary>
|
|
18
|
+
|
|
19
|
+
<div class="card-body">
|
|
20
|
+
<div class="mb-3">
|
|
21
|
+
<div class="text-muted text-uppercase small mb-1" style="letter-spacing: .04em;"><%= Spree.t('load_test.flows.save_to') %></div>
|
|
22
|
+
<code><%= flow[:destination] %></code>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<% if flow[:data_button] %>
|
|
26
|
+
<div class="alert alert-primary d-flex mb-3" role="alert">
|
|
27
|
+
<%= svg_icon name: 'cursor-fill', width: 18, height: 18, classes: 'mr-2 flex-shrink-0' %>
|
|
28
|
+
<div>
|
|
29
|
+
<strong><%= Spree.t('load_test.flows.data_button_label', label: flow[:data_button][:label]) %></strong> — <%= flow[:data_button][:where] %>
|
|
30
|
+
<% if flow[:data_button][:note] %>
|
|
31
|
+
<div class="text-muted small mt-1"><%= flow[:data_button][:note] %></div>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<% end %>
|
|
36
|
+
|
|
37
|
+
<% if flow[:source] %>
|
|
38
|
+
<div class="mb-3">
|
|
39
|
+
<div class="text-muted text-uppercase small mb-2" style="letter-spacing: .04em;"><%= Spree.t('load_test.flows.look_up_manually') %></div>
|
|
40
|
+
<div class="table-responsive">
|
|
41
|
+
<table class="table table-sm mb-0">
|
|
42
|
+
<thead class="text-muted">
|
|
43
|
+
<tr>
|
|
44
|
+
<th><%= Spree.t('load_test.flows.field') %></th>
|
|
45
|
+
<th><%= Spree.t('load_test.flows.where_to_find_it') %></th>
|
|
46
|
+
</tr>
|
|
47
|
+
</thead>
|
|
48
|
+
<tbody>
|
|
49
|
+
<% flow[:source].each do |field, instructions| %>
|
|
50
|
+
<tr>
|
|
51
|
+
<td class="align-top text-nowrap"><code><%= field %></code></td>
|
|
52
|
+
<td class="text-muted"><%= instructions %></td>
|
|
53
|
+
</tr>
|
|
54
|
+
<% end %>
|
|
55
|
+
</tbody>
|
|
56
|
+
</table>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<% end %>
|
|
60
|
+
|
|
61
|
+
<% if flow[:doc] %>
|
|
62
|
+
<p class="text-muted small mb-0"><%= Spree.t('load_test.flows.full_request_table_html', doc: flow[:doc]).html_safe %></p>
|
|
63
|
+
<% end %>
|
|
64
|
+
</div>
|
|
65
|
+
</details>
|
|
66
|
+
<% end %>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<%= render 'spree/admin/shared/system_tabs', current: :load_testing %>
|
|
2
|
+
|
|
3
|
+
<div class="row">
|
|
4
|
+
<div class="col-md-12">
|
|
5
|
+
<div class="list-group">
|
|
6
|
+
<%= link_to admin_system_load_test_accounts_path,
|
|
7
|
+
class: 'list-group-item list-group-item-action d-flex justify-content-between align-items-center' do %>
|
|
8
|
+
<div>
|
|
9
|
+
<strong><%= Spree.t('load_test.index.manage_accounts') %></strong>
|
|
10
|
+
<div class="text-muted small"><%= Spree.t('load_test.index.manage_accounts_description') %></div>
|
|
11
|
+
</div>
|
|
12
|
+
<%= svg_icon name: 'chevron-right', width: 20, height: 20 %>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<%= link_to admin_system_load_test_flows_path,
|
|
16
|
+
class: 'list-group-item list-group-item-action d-flex justify-content-between align-items-center' do %>
|
|
17
|
+
<div>
|
|
18
|
+
<strong><%= Spree.t('load_test.index.test_data_sources') %></strong>
|
|
19
|
+
<div class="text-muted small"><%= Spree.t('load_test.index.test_data_sources_description') %></div>
|
|
20
|
+
</div>
|
|
21
|
+
<%= svg_icon name: 'chevron-right', width: 20, height: 20 %>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<%= render partial: 'spree/admin/shared/product_tabs', locals: { current: :variants } %>
|
|
2
|
+
|
|
3
|
+
<% content_for :page_title do %>
|
|
4
|
+
<%= Spree.t(:fixture, default: 'Fixture') %> — <%= @variant.options_text.presence || @variant.sku %>
|
|
5
|
+
<% end %>
|
|
6
|
+
|
|
7
|
+
<% filename = "#{@fixture_env}.json" %>
|
|
8
|
+
|
|
9
|
+
<div class="card mb-4">
|
|
10
|
+
<div class="card-body">
|
|
11
|
+
<% if @fixture_issues.blank? %>
|
|
12
|
+
<div class="alert alert-success" role="alert">
|
|
13
|
+
<strong>Ready for load testing.</strong> This variant is purchasable and sits under an event, so the
|
|
14
|
+
booking flow can run against it end-to-end.
|
|
15
|
+
</div>
|
|
16
|
+
<% else %>
|
|
17
|
+
<div class="alert alert-warning" role="alert">
|
|
18
|
+
<strong>Not ready for load testing.</strong> The booking flow will not complete against this variant until
|
|
19
|
+
the following is fixed:
|
|
20
|
+
<ul class="mb-0 mt-2">
|
|
21
|
+
<% @fixture_issues.each do |issue| %>
|
|
22
|
+
<li><%= issue %></li>
|
|
23
|
+
<% end %>
|
|
24
|
+
</ul>
|
|
25
|
+
</div>
|
|
26
|
+
<% end %>
|
|
27
|
+
|
|
28
|
+
<p class="text-muted">
|
|
29
|
+
Save this as <code><%= filename %></code> in
|
|
30
|
+
<code>load_testing/config/fixture/platform/booking_ticket_flow/</code>
|
|
31
|
+
(git-ignored).
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<div class="position-relative" data-controller="clipboard">
|
|
35
|
+
<div class="position-absolute d-flex" style="top: .5rem; right: .5rem;">
|
|
36
|
+
<button type="button" class="btn btn-sm btn-primary"
|
|
37
|
+
data-action="clipboard#copy"
|
|
38
|
+
onclick="show_flash('success', '<%= j Spree.t(:copied_to_clipboard, default: 'Copied to clipboard') %>')">
|
|
39
|
+
<%= Spree.t(:copy, default: 'Copy') %>
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
<textarea readonly data-clipboard-target="source" rows="12"
|
|
43
|
+
class="form-control text-monospace bg-light"
|
|
44
|
+
style="resize: vertical;"><%= JSON.pretty_generate(@fixture) %></textarea>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<%= link_to Spree.t(:back), spree.admin_product_variants_path(@product), class: 'btn btn-outline-secondary' %>
|