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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ea1290341adc6665c82f0ca238b21620b882af5eff415f8d76866938ada20bf1
|
|
4
|
+
data.tar.gz: 5299a81a6a985807378d89609eed247006ec984a7a304f23d48d6deb659b1dcf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0015dfa2635e073b48c0b0d694e7958d1e7951dac7af8e693a80122a16fc348c413e92bfc4d7ef91da16d232d0c5781e24407165401cad9eeba45c698b01049c
|
|
7
|
+
data.tar.gz: 837d04df6961bd72fbdc3784f3f9ce2a5283d4001eb37cf6588d1c52e52e6c2a6f77d1ea05f9980a678a9859c9415e310e1bc261b579da03abca0cdeb4e6dd83
|
data/.env.example
CHANGED
|
@@ -32,6 +32,15 @@ WAITING_ROOM_MAX_ETA_BATCH_SIZE=50 # cap on batch size assumed when estimating t
|
|
|
32
32
|
VATTANAC_AES_SECRET_KEY= ""
|
|
33
33
|
VATTANAC_PUBLIC_KEY=""
|
|
34
34
|
|
|
35
|
+
# Telegram OAuth (OpenID Connect) login.
|
|
36
|
+
# client_id (audience) the mobile app uses when requesting an id_token from
|
|
37
|
+
# Telegram. Verified as the `aud` claim in
|
|
38
|
+
# app/interactors/spree_cm_commissioner/telegram_oauth_id_token_provider.rb
|
|
39
|
+
TELEGRAM_OAUTH_CLIENT_ID=""
|
|
40
|
+
# client_secret injected server-side when proxying the PKCE code -> id_token
|
|
41
|
+
# exchange in app/interactors/spree_cm_commissioner/telegram_oauth_token_exchanger.rb
|
|
42
|
+
TELEGRAM_OAUTH_CLIENT_SECRET=""
|
|
43
|
+
|
|
35
44
|
# Organizer URL
|
|
36
45
|
ORGANIZER_URL=http://127.0.0.1:4000/organizer
|
|
37
46
|
|
data/Gemfile.lock
CHANGED
|
@@ -34,7 +34,7 @@ GIT
|
|
|
34
34
|
PATH
|
|
35
35
|
remote: .
|
|
36
36
|
specs:
|
|
37
|
-
spree_cm_commissioner (2.9.
|
|
37
|
+
spree_cm_commissioner (2.9.2)
|
|
38
38
|
activerecord-multi-tenant
|
|
39
39
|
activerecord_json_validator (~> 2.1, >= 2.1.3)
|
|
40
40
|
aws-sdk-cloudfront
|
|
@@ -886,7 +886,7 @@ GEM
|
|
|
886
886
|
spree_backend (>= 4.3.0.rc1)
|
|
887
887
|
spree_emails (>= 4.3.0.rc1)
|
|
888
888
|
spree_extension
|
|
889
|
-
spree_vpago (2.
|
|
889
|
+
spree_vpago (2.3.4)
|
|
890
890
|
faraday
|
|
891
891
|
google-cloud-firestore
|
|
892
892
|
spree_api (>= 4.5)
|
|
@@ -27,6 +27,12 @@ module SpreeCmCommissioner
|
|
|
27
27
|
PopularRoutesController
|
|
28
28
|
RoutePlacesController
|
|
29
29
|
ShowsController
|
|
30
|
+
EpisodesController
|
|
31
|
+
ShowPeopleController
|
|
32
|
+
ShowPersonAssignmentsController
|
|
33
|
+
ShowContestantsController
|
|
34
|
+
VotePackagesController
|
|
35
|
+
ShowEliminationSessionsController
|
|
30
36
|
].freeze
|
|
31
37
|
|
|
32
38
|
# Priority 3: Moderate Freshness (30 minutes)
|
|
@@ -46,6 +52,7 @@ module SpreeCmCommissioner
|
|
|
46
52
|
HIGH_FRESHNESS_CONTROLLERS = %w[
|
|
47
53
|
TripsController
|
|
48
54
|
TripSearchController
|
|
55
|
+
VotingContestantsController
|
|
49
56
|
].freeze
|
|
50
57
|
|
|
51
58
|
def shared_max_age
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module LoadTestAccountManagement
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
include SpreeCmCommissioner::LoadTestAccountScope
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
before_action :authorize_load_testing!, only: %i[create bulk_destroy]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Ensures N load-test accounts exist (idempotent) for `load_test_tenant` (nil = platform).
|
|
12
|
+
def create
|
|
13
|
+
result = SpreeCmCommissioner::LoadTest::CreateAccounts.call(**pool_params)
|
|
14
|
+
|
|
15
|
+
if result.success?
|
|
16
|
+
flash[:success] = "#{result.value[:created]} load-test accounts created."
|
|
17
|
+
else
|
|
18
|
+
flash[:error] = result.error.value
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
redirect_to manage_accounts_redirect_path
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
flash[:error] = e.message
|
|
24
|
+
redirect_to manage_accounts_redirect_path
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def bulk_destroy
|
|
28
|
+
result = SpreeCmCommissioner::LoadTest::DestroyAccounts.call(**pool_params)
|
|
29
|
+
|
|
30
|
+
if result.success?
|
|
31
|
+
flash[:success] = "#{result.value[:destroyed]} destroyed, #{result.value[:not_found]} not found (already removed)."
|
|
32
|
+
else
|
|
33
|
+
flash[:error] = result.error.value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
redirect_to manage_accounts_redirect_path
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
flash[:error] = e.message
|
|
39
|
+
redirect_to manage_accounts_redirect_path
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Default: cancancan's `authorize!`. Override in the including controller if it doesn't use
|
|
45
|
+
# cancancan (e.g. the organizer dashboard, which authorizes via RoleAuthorization/can_manage?).
|
|
46
|
+
def authorize_load_testing!
|
|
47
|
+
authorize! :manage, :load_testing
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Where create/bulk_destroy redirect back to. Must be implemented by the including controller
|
|
51
|
+
# (points at its own Load Test Accounts page).
|
|
52
|
+
def manage_accounts_redirect_path
|
|
53
|
+
raise NotImplementedError, "#{self.class} must implement #manage_accounts_redirect_path"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def pool_params
|
|
57
|
+
{
|
|
58
|
+
client_id: resolve_client_id,
|
|
59
|
+
count: params[:count].presence&.to_i || 20,
|
|
60
|
+
email_prefix: params[:email_prefix].presence || 'loadtest',
|
|
61
|
+
email_domain: params[:email_domain].presence
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Carries the form field values through the redirect so the index page re-renders with the
|
|
66
|
+
# same count/prefix/domain/page/per_page the admin/organizer just submitted with.
|
|
67
|
+
def redirect_params
|
|
68
|
+
params.slice(:count, :email_prefix, :email_domain, :page, :per_page).permit!
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def resolve_client_id
|
|
72
|
+
return nil if load_test_tenant.nil?
|
|
73
|
+
|
|
74
|
+
application = Spree::OauthApplication.where(tenant_id: load_test_tenant.id).order(:id).first
|
|
75
|
+
raise "No OAuth application found for tenant #{load_test_tenant.name}" unless application
|
|
76
|
+
|
|
77
|
+
application.uid
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module LoadTestAccountScope
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
# The tenant these accounts are scoped to. nil means the platform (no tenant). Must be
|
|
8
|
+
# implemented by the including controller.
|
|
9
|
+
def load_test_tenant
|
|
10
|
+
raise NotImplementedError, "#{self.class} must implement #load_test_tenant"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Scan cap for the tenant-scoped candidate set below — bounds the query while still comfortably
|
|
14
|
+
# covering realistic load-test pool sizes.
|
|
15
|
+
EXPORT_SCAN_LIMIT = 2000
|
|
16
|
+
|
|
17
|
+
DEFAULT_EXPORT_PER_PAGE = 20
|
|
18
|
+
|
|
19
|
+
# Queried fresh from the DB every time, so it's always in sync with the current pool rather than
|
|
20
|
+
# a stashed file or session state.
|
|
21
|
+
#
|
|
22
|
+
# `load_test_account` lives in the `private_metadata` jsonb store (see UserDecorator's
|
|
23
|
+
# `store_private_metadata`), not a real column, so it can't be filtered in SQL directly —
|
|
24
|
+
# `store ..., coder: JSON` on top of an already-jsonb column double-encodes the value as a JSON
|
|
25
|
+
# string scalar rather than a native object, which breaks Postgres's `@>`/`->>` jsonb operators.
|
|
26
|
+
# Filter with the model's own `load_test_account?` in Ruby instead, which deserializes it
|
|
27
|
+
# correctly regardless of that storage quirk.
|
|
28
|
+
#
|
|
29
|
+
# Filtering happens in Ruby, so this is a plain Array by the time it needs paging — wrap it
|
|
30
|
+
# with Kaminari.paginate_array (same pattern as VoteLogsController) to get the same
|
|
31
|
+
# `page`/`per_page` params and `paginate` view helper every other index page in this app uses,
|
|
32
|
+
# instead of hand-rolled pagination just for this one page.
|
|
33
|
+
#
|
|
34
|
+
# Shared between LoadTestAccountManagement (lists/mutates the pool) and
|
|
35
|
+
# LoadTestLoginTokenGeneration (mints tokens for exactly the page shown), so both controllers
|
|
36
|
+
# resolve the same page the same way.
|
|
37
|
+
def export_accounts
|
|
38
|
+
matches = Spree::User.where(tenant_id: load_test_tenant&.id)
|
|
39
|
+
.order(:id)
|
|
40
|
+
.limit(EXPORT_SCAN_LIMIT)
|
|
41
|
+
.select(&:load_test_account?)
|
|
42
|
+
|
|
43
|
+
Kaminari.paginate_array(matches).page(params[:page]).per(params[:per_page] || DEFAULT_EXPORT_PER_PAGE)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module LoadTestLoginTokenGeneration
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
include SpreeCmCommissioner::LoadTestAccountScope
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
before_action :authorize_load_testing!, only: %i[create]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Mints a fresh, stateless login-token JWT for each account on the requested page (same
|
|
12
|
+
# page/per_page as the Load Test Accounts index) and returns them as JSON. Nothing is
|
|
13
|
+
# persisted — the tokens are self-contained and verified by signature + expiry alone (see
|
|
14
|
+
# SpreeCmCommissioner::LoadTest::LoginToken).
|
|
15
|
+
def create
|
|
16
|
+
render json: login_tokens_payload(export_accounts)
|
|
17
|
+
rescue StandardError => e
|
|
18
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# Default: cancancan's `authorize!`. Override in the including controller if it doesn't use
|
|
24
|
+
# cancancan (e.g. the organizer dashboard, which authorizes via RoleAuthorization/can_manage?).
|
|
25
|
+
def authorize_load_testing!
|
|
26
|
+
authorize! :manage, :load_testing
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# aud claim: the admin/organizer who minted the tokens. No fallback — both hosting controllers
|
|
30
|
+
# already require an authenticated user for every action here, so spree_current_user being
|
|
31
|
+
# absent would be a bug worth raising on, not silently mislabeling as 'system'.
|
|
32
|
+
def token_audience
|
|
33
|
+
spree_current_user.id
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# user_id + the user_id_token are all k6 needs (email and expiry are already claims inside the
|
|
37
|
+
# JWT itself — sub / exp — and the token alone identifies + authenticates the account via
|
|
38
|
+
# UserPasswordAuthenticator's token-only path). expires_at is included too, purely so the
|
|
39
|
+
# generated-tokens list can show it to a human without decoding the JWT. Named `user_id_token`
|
|
40
|
+
# (not `id_token`) so UserAuthenticator can route on the params key alone, without decoding
|
|
41
|
+
# every request's `id_token` to check whether it's a load-test token.
|
|
42
|
+
def login_tokens_payload(accounts)
|
|
43
|
+
accounts.map do |user|
|
|
44
|
+
generated = SpreeCmCommissioner::LoadTest::LoginToken.generate(user: user, audience: token_audience)
|
|
45
|
+
{
|
|
46
|
+
user_id: user.id,
|
|
47
|
+
user_id_token: generated[:token],
|
|
48
|
+
expires_at: generated[:expires_at]
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -80,8 +80,10 @@ module Spree
|
|
|
80
80
|
|
|
81
81
|
def permitted_resource_params
|
|
82
82
|
params.require(object_name).permit(:first_name, :last_name, :gender, :dob, :occupation_id,
|
|
83
|
-
:nationality_id, :age, :emergency_contact,
|
|
84
|
-
:seat_number, :phone_number
|
|
83
|
+
:nationality_id, :age, :emergency_contact,
|
|
84
|
+
:line_item_id, :seat_number, :phone_number, :contact,
|
|
85
|
+
:other_organization, :expectation, :address,
|
|
86
|
+
:social_contact_platform, :social_contact
|
|
85
87
|
)
|
|
86
88
|
end
|
|
87
89
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
module System
|
|
4
|
+
class LoadTestAccountsController < Spree::Admin::BaseController
|
|
5
|
+
include SpreeCmCommissioner::LoadTestAccountManagement
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@export_accounts = export_accounts
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# nil = platform. Admin load testing never operates on a tenant.
|
|
14
|
+
def load_test_tenant
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def manage_accounts_redirect_path
|
|
19
|
+
admin_system_load_test_accounts_path(**redirect_params)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
module System
|
|
4
|
+
class LoadTestLoginTokensController < Spree::Admin::BaseController
|
|
5
|
+
include SpreeCmCommissioner::LoadTestLoginTokenGeneration
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# nil = platform. Admin load testing never operates on a tenant.
|
|
10
|
+
def load_test_tenant
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V2
|
|
4
|
+
module Storefront
|
|
5
|
+
class DraftOrdersController < ::Spree::Api::V2::ResourceController
|
|
6
|
+
# Creates a generic draft order from a locally-assembled cart in a single request.
|
|
7
|
+
# Endpoint: POST /api/v2/storefront/draft_orders
|
|
8
|
+
# Params:
|
|
9
|
+
# - line_items: Array of { variant_id:, quantity:, options: {}, public_metadata: {}, private_metadata: {} } (required)
|
|
10
|
+
def create
|
|
11
|
+
result = SpreeCmCommissioner::DraftOrder::Create.call(
|
|
12
|
+
line_items: draft_order_line_items,
|
|
13
|
+
user: spree_current_user,
|
|
14
|
+
store: current_store,
|
|
15
|
+
currency: current_currency
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if result.success?
|
|
19
|
+
render_serialized_payload { serialize_resource(serializable_order(result.value[:order])) }
|
|
20
|
+
else
|
|
21
|
+
render_error_payload(result.error)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Re-fetch the created order with everything the enriched payload touches eager-loaded, so
|
|
28
|
+
# serializing N line items / M guests stays a fixed number of queries (not N+1 / M+1).
|
|
29
|
+
def serializable_order(order)
|
|
30
|
+
Spree::Order.where(id: order.id)
|
|
31
|
+
.includes(line_items: :guests)
|
|
32
|
+
.first || order
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def draft_order_line_items
|
|
36
|
+
params.permit(line_items: [:variant_id, :quantity, { options: {}, public_metadata: {}, private_metadata: {} }])
|
|
37
|
+
.fetch(:line_items, [])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# override
|
|
41
|
+
def resource_serializer
|
|
42
|
+
Spree::Api::Dependencies.storefront_cart_serializer.constantize
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -7,17 +7,11 @@ module Spree
|
|
|
7
7
|
|
|
8
8
|
# override
|
|
9
9
|
def collection
|
|
10
|
-
allowed_ids = spree_current_user.preview_roles.where(previewable_type: Spree::Taxon.polymorphic_name).pluck(:previewable_id)
|
|
11
|
-
|
|
12
|
-
section_ids = SpreeCmCommissioner::HomepageSectionRelatable
|
|
13
|
-
.where(relatable_type: 'Spree::Taxon', relatable_id: allowed_ids.presence || [])
|
|
14
|
-
.select(:homepage_section_id)
|
|
15
|
-
|
|
16
10
|
@collection ||= model_class.filter_by_segment(params[:homepage_id] || :general)
|
|
17
11
|
.active
|
|
18
12
|
.where(tenant_id: nil)
|
|
19
13
|
.where(preview: true)
|
|
20
|
-
.where(id:
|
|
14
|
+
.where(id: SpreeCmCommissioner::HomepageSectionRelatable.section_ids_previewable_by(spree_current_user))
|
|
21
15
|
.order(position: :asc)
|
|
22
16
|
end
|
|
23
17
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V2
|
|
4
|
+
module Storefront
|
|
5
|
+
# Server-side relay for the Telegram OAuth PKCE `code -> id_token`
|
|
6
|
+
# exchange. The mobile/web clients cannot hit Telegram's /token endpoint
|
|
7
|
+
# directly, so they POST the PKCE parameters here and we forward them to
|
|
8
|
+
# Telegram, returning its response verbatim. Public endpoint: the caller
|
|
9
|
+
# has no access token yet (this is part of logging in).
|
|
10
|
+
class TelegramOauthTokensController < Spree::Api::V2::BaseController
|
|
11
|
+
def create
|
|
12
|
+
result = SpreeCmCommissioner::TelegramOauthTokenExchanger.call(exchange_params.to_h.symbolize_keys)
|
|
13
|
+
|
|
14
|
+
return render_error_payload(result.message) unless result.success?
|
|
15
|
+
|
|
16
|
+
render json: result.body, status: result.status, content_type: 'application/json'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def exchange_params
|
|
22
|
+
params.permit(:client_id, :code, :redirect_uri, :code_verifier)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V2
|
|
4
|
+
module Tenant
|
|
5
|
+
class DraftOrdersController < BaseController
|
|
6
|
+
# Creates a generic draft order from a locally-assembled cart in a single request.
|
|
7
|
+
# Endpoint: POST /api/v2/tenant/draft_orders
|
|
8
|
+
# Params:
|
|
9
|
+
# - line_items: Array of { variant_id:, quantity:, options: {}, public_metadata: {}, private_metadata: {} } (required)
|
|
10
|
+
def create
|
|
11
|
+
result = SpreeCmCommissioner::DraftOrder::Create.call(
|
|
12
|
+
line_items: draft_order_line_items,
|
|
13
|
+
user: spree_current_user,
|
|
14
|
+
store: current_store,
|
|
15
|
+
currency: current_currency,
|
|
16
|
+
tenant: @tenant
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if result.success?
|
|
20
|
+
render_serialized_payload { serialize_resource(serializable_order(result.value[:order])) }
|
|
21
|
+
else
|
|
22
|
+
render_error_payload(result.error)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# Re-fetch the created order with everything the enriched payload touches eager-loaded, so
|
|
29
|
+
# serializing N line items / M guests stays a fixed number of queries (not N+1 / M+1).
|
|
30
|
+
def serializable_order(order)
|
|
31
|
+
Spree::Order.where(id: order.id)
|
|
32
|
+
.includes(line_items: :guests)
|
|
33
|
+
.first || order
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def draft_order_line_items
|
|
37
|
+
params.permit(line_items: [:variant_id, :quantity, { options: {}, public_metadata: {}, private_metadata: {} }])
|
|
38
|
+
.fetch(:line_items, [])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# override
|
|
42
|
+
def resource_serializer
|
|
43
|
+
Spree::V2::Tenant::CartSerializer
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -15,7 +15,7 @@ module Spree
|
|
|
15
15
|
.select(:previewable_id)
|
|
16
16
|
|
|
17
17
|
# Base: all active preview products from the parent scope.
|
|
18
|
-
base = super.where(status: :active
|
|
18
|
+
base = super.rewhere(preview: true).where(status: :active)
|
|
19
19
|
|
|
20
20
|
# Path A: product has its own PreviewRole → user must hold that role directly.
|
|
21
21
|
# Taxon membership is irrelevant for these products.
|
|
@@ -7,16 +7,10 @@ module Spree
|
|
|
7
7
|
|
|
8
8
|
# override
|
|
9
9
|
def collection
|
|
10
|
-
allowed_ids = spree_current_user.preview_roles.where(previewable_type: Spree::Taxon.polymorphic_name).pluck(:previewable_id)
|
|
11
|
-
|
|
12
|
-
section_ids = SpreeCmCommissioner::HomepageSectionRelatable
|
|
13
|
-
.where(relatable_type: 'Spree::Taxon', relatable_id: allowed_ids.presence || [])
|
|
14
|
-
.select(:homepage_section_id)
|
|
15
|
-
|
|
16
10
|
@collection ||= scope.filter_by_segment(params[:homepage_id] || :general)
|
|
17
11
|
.active
|
|
18
12
|
.where(preview: true)
|
|
19
|
-
.where(id:
|
|
13
|
+
.where(id: SpreeCmCommissioner::HomepageSectionRelatable.section_ids_previewable_by(spree_current_user))
|
|
20
14
|
.order(position: :asc)
|
|
21
15
|
end
|
|
22
16
|
end
|
|
@@ -55,6 +55,17 @@ module SpreeCmCommissioner
|
|
|
55
55
|
option_value.validate
|
|
56
56
|
option_value.name
|
|
57
57
|
end
|
|
58
|
+
|
|
59
|
+
# Builds the load-test fixture for this variant so it can be used by the k6 booking flow.
|
|
60
|
+
# Shown on a page (with a check for whether the variant can be booked) for the tester to
|
|
61
|
+
# copy into the load_testing config.
|
|
62
|
+
def fixture
|
|
63
|
+
result = SpreeCmCommissioner::LoadTest::GenerateBookingTicketData.call(variant: @variant)
|
|
64
|
+
@fixture = result.value[:data]
|
|
65
|
+
@fixture_issues = result.value[:issues]
|
|
66
|
+
# The load_testing config calls it "local", not "development".
|
|
67
|
+
@fixture_env = Rails.env.development? ? 'local' : Rails.env.to_s
|
|
68
|
+
end
|
|
58
69
|
end
|
|
59
70
|
end
|
|
60
71
|
end
|
data/app/controllers/spree_cm_commissioner/api/v2/storefront/checkout_controller_decorator.rb
CHANGED
|
@@ -5,10 +5,8 @@ module SpreeCmCommissioner
|
|
|
5
5
|
module CheckoutControllerDecorator
|
|
6
6
|
def self.prepended(base)
|
|
7
7
|
base.include SpreeCmCommissioner::OrderConcern
|
|
8
|
-
base.include SpreeCmCommissioner::TurnstileProtectable
|
|
9
8
|
|
|
10
9
|
base.before_action :set_load_test_bypass_current
|
|
11
|
-
base.before_action -> { verify_turnstile! }, only: %i[create_payment]
|
|
12
10
|
end
|
|
13
11
|
|
|
14
12
|
private
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
# Verifies a Telegram "Login Library / OpenID Connect" id_token and extracts
|
|
3
|
+
# the user claims. The mobile app obtains the id_token directly from Telegram
|
|
4
|
+
# using PKCE (no client secret), so here we only need to validate the JWT
|
|
5
|
+
# signature against Telegram's public JWKS and check iss/aud/exp.
|
|
6
|
+
#
|
|
7
|
+
# Docs: https://core.telegram.org/bots/telegram-login
|
|
8
|
+
class TelegramOauthIdTokenProvider < BaseInteractor
|
|
9
|
+
ISSUER = 'https://oauth.telegram.org'.freeze
|
|
10
|
+
JWKS_URL = 'https://oauth.telegram.org/.well-known/jwks.json'.freeze
|
|
11
|
+
JWKS_CACHE_KEY = 'telegram-oauth-jwks'.freeze
|
|
12
|
+
|
|
13
|
+
# ruby-jwt ships native support for RS256 and ES256. Telegram's JWKS also
|
|
14
|
+
# advertises EdDSA/ES256K keys, but id_tokens are signed with RS256 (kid
|
|
15
|
+
# "oidc-1") by default.
|
|
16
|
+
SUPPORTED_ALGORITHMS = %w[RS256 ES256].freeze
|
|
17
|
+
|
|
18
|
+
# :id_token
|
|
19
|
+
def call
|
|
20
|
+
context.fail!(message: 'telegram_id_token_missing') if context.id_token.blank?
|
|
21
|
+
|
|
22
|
+
audience = expected_audience
|
|
23
|
+
context.fail!(message: 'telegram_oauth_client_id_not_configured') if audience.blank?
|
|
24
|
+
|
|
25
|
+
claim = decode_id_token(audience)
|
|
26
|
+
return if claim.nil?
|
|
27
|
+
|
|
28
|
+
context.claim = claim
|
|
29
|
+
context.provider = extract_provider_params(claim)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def decode_id_token(audience)
|
|
35
|
+
jwk = find_jwk(unverified_header['kid'])
|
|
36
|
+
|
|
37
|
+
if jwk.nil?
|
|
38
|
+
context.fail!(message: 'telegram_unknown_signing_key')
|
|
39
|
+
return nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
payload, = JWT.decode(
|
|
43
|
+
context.id_token,
|
|
44
|
+
JWT::JWK.import(jwk).public_key,
|
|
45
|
+
true,
|
|
46
|
+
algorithms: SUPPORTED_ALGORITHMS,
|
|
47
|
+
iss: ISSUER,
|
|
48
|
+
verify_iss: true,
|
|
49
|
+
aud: audience,
|
|
50
|
+
verify_aud: true
|
|
51
|
+
)
|
|
52
|
+
payload
|
|
53
|
+
rescue JWT::DecodeError => e
|
|
54
|
+
CmAppLogger.error(label: 'telegram_id_token_verification_failed', data: e.message)
|
|
55
|
+
context.fail!(message: 'invalid_telegram_id_token', error: e.message)
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def unverified_header
|
|
60
|
+
JWT.decode(context.id_token, nil, false).last
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def find_jwk(kid)
|
|
64
|
+
jwks.find { |key| key['kid'] == kid } ||
|
|
65
|
+
refresh_jwks.find { |key| key['kid'] == kid }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def jwks
|
|
69
|
+
@jwks ||= fetch_jwks
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def refresh_jwks
|
|
73
|
+
Rails.cache.delete(JWKS_CACHE_KEY)
|
|
74
|
+
@jwks = fetch_jwks
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def fetch_jwks
|
|
78
|
+
Rails.cache.fetch(JWKS_CACHE_KEY, expires_in: 6.hours) do
|
|
79
|
+
JSON.parse(Net::HTTP.get(URI(JWKS_URL))).fetch('keys', [])
|
|
80
|
+
end
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
CmAppLogger.error(label: 'telegram_jwks_fetch_failed', data: e.message)
|
|
83
|
+
context.fail!(message: 'telegram_jwks_fetch_failed', error: e.message)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def expected_audience
|
|
87
|
+
ENV['TELEGRAM_OAUTH_CLIENT_ID'].presence
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# sub is the Telegram user id (also the chat id once the user has /started
|
|
91
|
+
# the bot), which keeps OIDC logins consistent with the Telegram Web App
|
|
92
|
+
# flow that also keys UserIdentityProvider.telegram on the Telegram user id.
|
|
93
|
+
def extract_provider_params(claim)
|
|
94
|
+
{
|
|
95
|
+
identity_type: SpreeCmCommissioner::UserIdentityProvider.identity_types[:telegram],
|
|
96
|
+
sub: claim['sub'].to_s,
|
|
97
|
+
name: claim['name'],
|
|
98
|
+
username: claim['preferred_username'],
|
|
99
|
+
picture: claim['picture'],
|
|
100
|
+
phone_number: claim['phone_number'],
|
|
101
|
+
email: nil # Telegram does not provide email
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|