spree_cm_commissioner 2.9.1.pre.pre4 → 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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +9 -0
  3. data/Gemfile.lock +2 -2
  4. data/app/controllers/concerns/spree_cm_commissioner/content_cachable.rb +7 -0
  5. data/app/controllers/concerns/spree_cm_commissioner/load_test_account_management.rb +80 -0
  6. data/app/controllers/concerns/spree_cm_commissioner/load_test_account_scope.rb +46 -0
  7. data/app/controllers/concerns/spree_cm_commissioner/load_test_login_token_generation.rb +53 -0
  8. data/app/controllers/spree/admin/guests_controller.rb +4 -2
  9. data/app/controllers/spree/admin/system/load_test_accounts_controller.rb +24 -0
  10. data/app/controllers/spree/admin/system/load_test_flows_controller.rb +11 -0
  11. data/app/controllers/spree/admin/system/load_test_login_tokens_controller.rb +16 -0
  12. data/app/controllers/spree/admin/system/load_testing_controller.rb +9 -0
  13. data/app/controllers/spree/api/v2/storefront/draft_orders_controller.rb +48 -0
  14. data/app/controllers/spree/api/v2/storefront/preview_sections_controller.rb +1 -7
  15. data/app/controllers/spree/api/v2/storefront/telegram_oauth_tokens_controller.rb +28 -0
  16. data/app/controllers/spree/api/v2/tenant/draft_orders_controller.rb +49 -0
  17. data/app/controllers/spree/api/v2/tenant/preview_products_controller.rb +1 -1
  18. data/app/controllers/spree/api/v2/tenant/preview_sections_controller.rb +1 -7
  19. data/app/controllers/spree_cm_commissioner/admin/variants_controller_decorator.rb +11 -0
  20. data/app/controllers/spree_cm_commissioner/api/v2/storefront/checkout_controller_decorator.rb +0 -2
  21. data/app/interactors/spree_cm_commissioner/telegram_oauth_id_token_provider.rb +105 -0
  22. data/app/interactors/spree_cm_commissioner/telegram_oauth_token_exchanger.rb +71 -0
  23. data/app/interactors/spree_cm_commissioner/user_id_token_authenticator.rb +2 -5
  24. data/app/interactors/spree_cm_commissioner/user_password_authenticator.rb +23 -1
  25. data/app/interactors/spree_cm_commissioner/user_telegram_oauth_authenticator.rb +78 -0
  26. data/app/mailers/spree/order_mailer_decorator.rb +1 -0
  27. data/app/models/concerns/spree_cm_commissioner/kyc_bitwise.rb +17 -0
  28. data/app/models/concerns/spree_cm_commissioner/line_items_filter_scope.rb +5 -3
  29. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +11 -0
  30. data/app/models/concerns/spree_cm_commissioner/product_delegation.rb +2 -0
  31. data/app/models/spree_cm_commissioner/homepage_section_relatable.rb +25 -0
  32. data/app/models/spree_cm_commissioner/order_decorator.rb +0 -6
  33. data/app/models/spree_cm_commissioner/payment_method_decorator.rb +2 -0
  34. data/app/models/spree_cm_commissioner/product_decorator.rb +1 -0
  35. data/app/models/spree_cm_commissioner/trip.rb +10 -1
  36. data/app/models/spree_cm_commissioner/user_decorator.rb +6 -0
  37. data/app/overrides/spree/admin/payment_methods/index/pagination.html.erb.deface +3 -0
  38. data/app/overrides/spree/admin/payment_methods/index/preserve_tab_in_search.html.erb.deface +4 -0
  39. data/app/overrides/spree/admin/products/_form/enable_social_sharing_email.html.erb.deface +13 -0
  40. data/app/overrides/spree/admin/variants/index/fixture_action.html.erb.deface +9 -0
  41. data/app/queries/spree_cm_commissioner/multi_leg_trips_query.rb +2 -0
  42. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_result_serializer.rb +2 -2
  43. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_serializer.rb +3 -1
  44. data/app/services/spree_cm_commissioner/draft_order/create.rb +86 -0
  45. data/app/services/spree_cm_commissioner/load_test/create_accounts.rb +68 -0
  46. data/app/services/spree_cm_commissioner/load_test/destroy_accounts.rb +50 -0
  47. data/app/services/spree_cm_commissioner/load_test/flow_registry.rb +26 -0
  48. data/app/services/spree_cm_commissioner/load_test/generate_booking_ticket_data.rb +140 -0
  49. data/app/services/spree_cm_commissioner/load_test/generate_voting_fixture_data.rb +178 -0
  50. data/app/services/spree_cm_commissioner/load_test/login_token.rb +46 -0
  51. data/app/services/spree_cm_commissioner/order/guests/update.rb +56 -0
  52. data/app/services/spree_cm_commissioner/trips/update_single_leg.rb +6 -1
  53. data/app/services/spree_cm_commissioner/user_authenticator.rb +22 -1
  54. data/app/services/spree_cm_commissioner/voting_credits/allocate.rb +13 -2
  55. data/app/views/spree/admin/featured_trips/index.html.erb +1 -1
  56. data/app/views/spree/admin/guests/_form.html.erb +37 -0
  57. data/app/views/spree/admin/shared/_system_tabs.html.erb +7 -0
  58. data/app/views/spree/admin/system/load_test_accounts/_login_tokens_modal.html.erb +52 -0
  59. data/app/views/spree/admin/system/load_test_accounts/index.html.erb +175 -0
  60. data/app/views/spree/admin/system/load_test_flows/index.html.erb +66 -0
  61. data/app/views/spree/admin/system/load_testing/index.html.erb +25 -0
  62. data/app/views/spree/admin/variants/fixture.html.erb +49 -0
  63. data/config/initializers/doorkeeper.rb +10 -6
  64. data/config/initializers/spree_permitted_attributes.rb +30 -0
  65. data/config/load_test_flows/platform.yml +25 -0
  66. data/config/load_test_flows/tenant.yml +46 -0
  67. data/config/locales/en.yml +94 -0
  68. data/config/locales/km.yml +2 -0
  69. data/config/routes.rb +19 -0
  70. data/db/migrate/20260703065239_add_position_to_cm_voting_contestants.rb +1 -1
  71. data/db/migrate/20260708150000_add_name_and_short_name_to_cm_trips.rb +6 -0
  72. data/lib/spree_cm_commissioner/trip_result.rb +3 -1
  73. data/lib/spree_cm_commissioner/version.rb +1 -1
  74. metadata +37 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ebefceb78297eb71b4dcb417b47de1c9440786f17942f341aa5cff394558b2aa
4
- data.tar.gz: 4fae27019658db8352dd7001445b331355d7b20f321b079c211e4e87889789eb
3
+ metadata.gz: ea1290341adc6665c82f0ca238b21620b882af5eff415f8d76866938ada20bf1
4
+ data.tar.gz: 5299a81a6a985807378d89609eed247006ec984a7a304f23d48d6deb659b1dcf
5
5
  SHA512:
6
- metadata.gz: ed1b54c40e5288bc4e04e467c978b531f80b49985e387e4ed5a65908dd9b8152fe024fd4dcfdadba2f279b385fc8250588b3cd674125abf4a4ddd4d0306560b1
7
- data.tar.gz: 283fa4cceb6e50ef28eba6e0eb4ecbdba5ca2984a377009934d057606840ce101db088d4f766c8d37b623bff61fe5b030867c99540ea00e33ed3067c20a4c243
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.1.pre.pre4)
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.0.5.pre.beta)
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, :line_item_id,
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,11 @@
1
+ module Spree
2
+ module Admin
3
+ module System
4
+ class LoadTestFlowsController < Spree::Admin::BaseController
5
+ def index
6
+ @load_test_flows = SpreeCmCommissioner::LoadTest::FlowRegistry.for_workflow('platform')
7
+ end
8
+ end
9
+ end
10
+ end
11
+ 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,9 @@
1
+ module Spree
2
+ module Admin
3
+ module System
4
+ class LoadTestingController < Spree::Admin::BaseController
5
+ def index; end
6
+ end
7
+ end
8
+ end
9
+ 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: section_ids)
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, preview: true)
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: section_ids)
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
@@ -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