spree_cm_commissioner 2.12.8 → 2.13.0

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +10 -1
  3. data/Gemfile.lock +1 -1
  4. data/app/controllers/concerns/spree_cm_commissioner/anonymous_booking_guard.rb +22 -0
  5. data/app/controllers/concerns/spree_cm_commissioner/waiting_room_authorization.rb +3 -5
  6. data/app/controllers/spree/admin/orders/customer_details_controller_decorator.rb +53 -3
  7. data/app/controllers/spree/admin/stock_managements_controller.rb +20 -12
  8. data/app/controllers/spree/admin/system/waiting_room_controller.rb +1 -1
  9. data/app/controllers/spree/admin/system/waiting_room_sessions_controller.rb +1 -1
  10. data/app/controllers/spree/api/v2/platform/resource_controller_decorator.rb +48 -0
  11. data/app/controllers/spree/api/v2/storefront/draft_orders_controller.rb +8 -0
  12. data/app/controllers/spree/api/v2/storefront/intercity_taxi/draft_orders_controller.rb +7 -0
  13. data/app/controllers/spree/api/v2/storefront/payment_methods_controller.rb +9 -3
  14. data/app/controllers/spree/api/v2/storefront/queue_draft_orders_controller.rb +8 -0
  15. data/app/controllers/spree/api/v2/storefront/transit/draft_orders_controller.rb +10 -0
  16. data/app/controllers/spree/api/v2/storefront/waiting_room_sessions_controller.rb +5 -5
  17. data/app/controllers/spree/api/v2/tenant/draft_orders_controller.rb +9 -0
  18. data/app/controllers/spree/api/v2/tenant/intercity_taxi/draft_orders_controller.rb +7 -0
  19. data/app/controllers/spree/api/v2/tenant/payment_methods_controller.rb +12 -5
  20. data/app/controllers/spree/api/v2/tenant/show_people_controller.rb +1 -3
  21. data/app/controllers/spree/api/v2/tenant/transit/draft_orders_controller.rb +10 -0
  22. data/app/controllers/spree/api/v2/tenant/waiting_room_sessions_controller.rb +5 -5
  23. data/app/controllers/spree_cm_commissioner/waiting_room_simulation_operations_controller.rb +39 -0
  24. data/app/finders/spree_cm_commissioner/payment_methods/find.rb +12 -9
  25. data/app/finders/spree_cm_commissioner/variants/find_by_option_value_ids.rb +26 -0
  26. data/app/interactors/spree_cm_commissioner/user_telegram_oauth_authenticator.rb +39 -8
  27. data/app/jobs/spree_cm_commissioner/stock_status_consolidation_job.rb +2 -4
  28. data/app/jobs/spree_cm_commissioner/waiting_room/admission/call_guests_chunk_job.rb +20 -0
  29. data/app/jobs/spree_cm_commissioner/waiting_room/advance_queue_job.rb +18 -0
  30. data/app/models/concerns/spree_cm_commissioner/firestore_connection.rb +4 -5
  31. data/app/models/concerns/spree_cm_commissioner/order_holdable.rb +22 -0
  32. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +10 -0
  33. data/app/models/concerns/spree_cm_commissioner/voting_session_timing_concern.rb +22 -4
  34. data/app/models/spree_cm_commissioner/line_item_decorator.rb +1 -1
  35. data/app/models/spree_cm_commissioner/show.rb +1 -1
  36. data/app/models/spree_cm_commissioner/voting_session.rb +2 -0
  37. data/app/models/spree_cm_commissioner/waiting_room_session.rb +1 -1
  38. data/app/overrides/spree/admin/orders/_order_actions/actions.html.erb.deface +4 -4
  39. data/app/overrides/spree/admin/orders/_search/taxon_search_field.html.erb.deface +8 -0
  40. data/app/overrides/spree/admin/orders/customer_details/_form/view_profile_link.html.erb.deface +16 -0
  41. data/app/overrides/spree/admin/orders/customer_details/edit/reassign_customer_search.html.erb.deface +48 -0
  42. data/app/overrides/spree/admin/shared/sub_menu/_product/events_tab.html.erb.deface +4 -1
  43. data/app/overrides/spree/admin/taxons/_form/preview_checkbox.html.erb.deface +4 -0
  44. data/app/overrides/spree/admin/taxons/_form/vendor.html.erb.deface +10 -2
  45. data/app/overrides/spree/admin/users/_form/fields.html.erb.deface +5 -0
  46. data/app/overrides/spree/admin/users/_tabs/page_title.html.erb.deface +6 -0
  47. data/app/overrides/spree/admin/users/_tabs/tabs.html.erb.deface +1 -1
  48. data/app/serializers/spree/v2/tenant/voting_session_serializer.rb +2 -1
  49. data/app/serializers/spree/v2/tenant/waiting_room_session_serializer.rb +1 -1
  50. data/app/serializers/spree_cm_commissioner/v2/storefront/cart_serializer_decorator.rb +1 -0
  51. data/app/serializers/spree_cm_commissioner/v2/storefront/waiting_room_session_serializer.rb +1 -1
  52. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/inventory/unstock_inventory.rb +3 -1
  53. data/app/services/spree_cm_commissioner/inventory_holds/convert.rb +25 -6
  54. data/app/services/spree_cm_commissioner/voting_sessions/publish_live_state.rb +1 -1
  55. data/app/services/spree_cm_commissioner/voting_sessions/publish_to_firestore.rb +15 -3
  56. data/app/services/spree_cm_commissioner/waiting_room/admission/call_guests.rb +279 -0
  57. data/app/services/spree_cm_commissioner/waiting_room/admission/call_guests_chunk.rb +109 -0
  58. data/app/services/spree_cm_commissioner/waiting_room/advance_queue.rb +120 -0
  59. data/app/services/spree_cm_commissioner/waiting_room/bypass_eligibility.rb +1 -1
  60. data/app/services/spree_cm_commissioner/waiting_room/heartbeat/sweep.rb +90 -0
  61. data/app/services/spree_cm_commissioner/waiting_room/positions/stamp.rb +186 -0
  62. data/app/services/spree_cm_commissioner/waiting_room/sessions/issue.rb +232 -0
  63. data/app/services/spree_cm_commissioner/waiting_room_lobby_metadata_fetcher.rb +2 -2
  64. data/app/views/spree/admin/events/_search_form.html.erb +4 -4
  65. data/app/views/spree/admin/shared/_taxon_tabs.html.erb +1 -1
  66. data/app/views/spree/admin/stock_managements/calendar.html.erb +72 -30
  67. data/app/views/spree/admin/stock_managements/index.html.erb +1 -1
  68. data/app/views/spree/admin/user_events/_new_user.html.erb +2 -1
  69. data/app/views/spree/admin/user_events/_users.html.erb +1 -1
  70. data/config/locales/en.yml +16 -0
  71. data/config/locales/km.yml +16 -0
  72. data/config/routes.rb +7 -0
  73. data/db/migrate/20260909000000_rename_jwt_token_to_token_on_cm_waiting_room_sessions.rb +13 -0
  74. data/lib/spree_cm_commissioner/version.rb +1 -1
  75. metadata +19 -7
  76. data/app/interactors/spree_cm_commissioner/waiting_guests_caller.rb +0 -259
  77. data/app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb +0 -226
  78. data/app/jobs/spree_cm_commissioner/waiting_guests_caller_job.rb +0 -15
  79. data/app/jobs/spree_cm_commissioner/waiting_room/stamp_queue_positions_job.rb +0 -14
  80. data/app/services/spree_cm_commissioner/waiting_room/stamp_queue_positions.rb +0 -196
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 742162ce28f156432f76f071c6538e682fecfa8008758acc698d283cb2a3742c
4
- data.tar.gz: bfafd91f275bc28a16234e3153ee8d83aa5b5040be06acca7850770b2573fe01
3
+ metadata.gz: b168ef103cb6aec0e7865fc4a752cc2367d526180fd6855bf21f6fba1cc72412
4
+ data.tar.gz: 05a6401d8ea20de2661c421471a16e0e8e1ace6882178a4f3f1cef0e9825d9b4
5
5
  SHA512:
6
- metadata.gz: f8c8857a35fe48099949496f9e69c0741ca32a18a05fbc05afafdc1cb5cd7feb3394b3b0158cbb27dea96f585e378f9a4a14c0d2fbac1b8cee6e54c8a51be443
7
- data.tar.gz: d8e013c7a5172fabbfea21e83ac52d5231351ed380960e1308b458ad1e7b6cd960bb2ca7a6204b82448f3d494c17cb3eb3ebb9a5d704228b901c789dacef09f2
6
+ metadata.gz: 6cc975658385143deea2b8f1f8e7ac7b4d9ccfa2ce96054c1968a99fe4a6f3793a09585341bf389d2d9939f1097b4d9e867f62734a1d6aafaef0429628d2c875
7
+ data.tar.gz: 1baa1785ad8c59d5d3ae287518b19c4e680db6a796678096b31c9e18c7e5c1f856984e18ca19407e478e332a465ed4f0d86f0f3c269378e37e9567bb9e91e46b
data/.env.example CHANGED
@@ -16,12 +16,21 @@ AWS_CF_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
16
16
  CONTENT_HOST_URL=https://content.bookme.plus
17
17
  CONTENT_CACHE_MAX_AGE=7200
18
18
 
19
- WAITING_ROOM_SESSION_SIGNATURE="e6b2********************26e3"
20
19
  WAITING_ROOM_SESSION_EXPIRE_DURATION_IN_SECOND=
20
+ WAITING_ROOM_SESSION_MAX_DURATION_IN_SECOND=600
21
+ WAITING_ROOM_BYPASS_HEADROOM_MULTIPLIER=1.1
21
22
  WAITING_ROOM_MIN_SESSIONS_COUNT=5
22
23
  WAITING_ROOM_DISABLED=no
23
24
  WAITING_ROOM_POSITION_STAMP_LIMIT=1000
24
25
  WAITING_ROOM_FIRESTORE_BATCH_SIZE=500
26
+ WAITING_ROOM_HEARTBEAT_TTL_SECONDS=180
27
+ WAITING_ROOM_SWEEP_LIMIT=1000
28
+ WAITING_ROOM_SIMULATION_TICK_KEY=
29
+
30
+ # See: app/services/spree_cm_commissioner/waiting_room/admission/call_guests.rb
31
+ WAITING_ROOM_MAX_CALLS_PER_RUN=200
32
+ WAITING_ROOM_CALL_CHUNK_SIZE=50
33
+ WAITING_ROOM_CALL_CLAIM_TTL_SECONDS=120
25
34
 
26
35
  # Vattanac Bank
27
36
  VATTANAC_AES_SECRET_KEY= ""
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.12.8)
37
+ spree_cm_commissioner (2.13.0)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -0,0 +1,22 @@
1
+ module SpreeCmCommissioner
2
+ # Draft-order endpoints let a request through with no `spree_current_user` so guests can book —
3
+ # but only for products that opt into that via `Product#allow_anonymous_booking`. Include in any
4
+ # draft-order controller and call `ensure_anonymous_booking_allowed!(products)` before doing any
5
+ # writes, so a request for a login-required product fails fast with 401 (the app can then
6
+ # re-authenticate/open login and retry) instead of creating an order no signed-in account owns.
7
+ module AnonymousBookingGuard
8
+ extend ActiveSupport::Concern
9
+
10
+ private
11
+
12
+ # products: [Array<Spree::Product>] every distinct product referenced by this request.
13
+ # Vacuously passes on an empty/unresolved list — an invalid variant/trip id is the underlying
14
+ # create service's error to raise, not this guard's.
15
+ def ensure_anonymous_booking_allowed!(products)
16
+ return if spree_current_user.present?
17
+ return if products.all?(&:allow_anonymous_booking)
18
+
19
+ raise Doorkeeper::Errors::DoorkeeperError
20
+ end
21
+ end
22
+ end
@@ -3,10 +3,6 @@ module SpreeCmCommissioner
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- rescue_from JWT::ExpiredSignature, with: :handle_waiting_room_session_token_error
7
- rescue_from JWT::VerificationError, with: :handle_waiting_room_session_token_error
8
- rescue_from JWT::DecodeError, with: :handle_waiting_room_session_token_error
9
-
10
6
  before_action :ensure_waiting_room_session_token
11
7
  end
12
8
 
@@ -15,7 +11,9 @@ module SpreeCmCommissioner
15
11
  return if request_from_client?
16
12
  return unless required_waiting_room_session?
17
13
 
18
- JWT.decode(params[:waiting_room_session_token], ENV.fetch('WAITING_ROOM_SESSION_SIGNATURE', nil), true, { algorithm: 'HS256' })
14
+ return if SpreeCmCommissioner::WaitingRoomSession.active.exists?(token: params[:waiting_room_session_token])
15
+
16
+ handle_waiting_room_session_token_error
19
17
  end
20
18
 
21
19
  def required_waiting_room_session?
@@ -2,11 +2,37 @@ module Spree
2
2
  module Admin
3
3
  module Orders
4
4
  module CustomerDetailsControllerDecorator
5
+ # Override
6
+ def edit
7
+ super
8
+ search_for_customer
9
+ end
10
+
11
+ # Persists the customer found by `search_for_customer` right away — no separate
12
+ # "Update" click, no manual Guest Checkout toggle. `associate_user!` only backfills
13
+ # bill/ship address when the order doesn't already have one (`||=`), so an order's
14
+ # existing addresses are left as-is; this is a user swap only.
15
+ def assign_customer
16
+ user = tenant_user_scope.find_by(id: params[:user_id])
17
+
18
+ if user.nil?
19
+ flash[:error] = Spree.t(:user_not_found)
20
+ elsif user.id == @order.user_id
21
+ flash[:notice] = Spree.t('admin.orders.customer_details.user_already_assigned', email: user.email)
22
+ else
23
+ @order.associate_user!(user)
24
+ flash[:success] = Spree.t('admin.orders.customer_details.user_assigned', email: user.email)
25
+ end
26
+
27
+ redirect_to spree.edit_admin_order_customer_path(@order)
28
+ end
29
+
5
30
  # Override
6
31
  def load_user
7
- @user = Spree.user_class.find_by(id: order_params[:user_id]) ||
8
- (Spree.user_class.find_by(email: order_params[:email]) if order_params[:email].present?) ||
9
- (Spree.user_class.find_by(phone_number: order_params[:phone_number]) if order_params[:phone_number].present?)
32
+ scope = tenant_user_scope
33
+ @user = scope.find_by(id: order_params[:user_id]) ||
34
+ (scope.find_by(email: order_params[:email]) if order_params[:email].present?) ||
35
+ (scope.find_by(phone_number: order_params[:phone_number]) if order_params[:phone_number].present?)
10
36
 
11
37
  return if @user
12
38
 
@@ -21,6 +47,30 @@ module Spree
21
47
  ship_address_attributes: permitted_address_attributes
22
48
  )
23
49
  end
50
+
51
+ private
52
+
53
+ # Looks up a candidate to reassign this order to by whatever the admin typed —
54
+ # email, phone number, or login — the same lookup used for real user sign-in
55
+ # ([UserIdentity#find_user_by_login]), scoped to the order's own tenant so an
56
+ # admin can never attach a user from a different tenant. Only sets `@order.email`
57
+ # in memory (never saved here) — `assign_customer` is what actually persists it,
58
+ # once the admin confirms the match by clicking Assign.
59
+ def search_for_customer
60
+ @customer_search_query = params[:customer_search_query].presence
61
+ return if @customer_search_query.nil?
62
+
63
+ @customer_search_result = Spree.user_class.find_user_by_login(@customer_search_query, @order.tenant_id)
64
+ @order.email = @customer_search_result.email if @customer_search_result
65
+ end
66
+
67
+ # Same tenant boundary `find_user_by_login` uses — the actual save path (`update`)
68
+ # must enforce this itself rather than trust the search box above, since a
69
+ # crafted `order[user_id]`/`order[email]` could otherwise pull in a user from
70
+ # another tenant.
71
+ def tenant_user_scope
72
+ @order.tenant_id.present? ? Spree.user_class.by_tenant(@order.tenant_id) : Spree.user_class.by_non_tenant
73
+ end
24
74
  end
25
75
  end
26
76
  end
@@ -40,24 +40,32 @@ module Spree
40
40
  redirect_back fallback_location: admin_product_stock_managements_path(@product)
41
41
  end
42
42
 
43
- # GET /products/:slug/stock_managements/calendar?year=&selected_variant_id=
43
+ # GET /products/:slug/stock_managements/calendar?year=&selected_option_value_ids[]=
44
44
  def calendar
45
45
  @year = params[:year].present? ? params[:year].to_i : Time.zone.today.year
46
46
 
47
47
  from_date = Date.new(@year, 1, 1).beginning_of_year
48
48
  to_date = Date.new(@year, 1, 1).end_of_year
49
49
 
50
- @variants = @product.variants
51
- @inventory_items = if params[:selected_variant_id].present?
52
- @product.inventory_items.includes(:variant, :prices).where(
53
- inventory_date: from_date..to_date,
54
- variant_id: params[:selected_variant_id]
55
- ).to_a
56
- else
57
- @product.inventory_items.includes(:variant, :prices).where(
58
- inventory_date: from_date..to_date
59
- ).to_a
60
- end
50
+ @variant_kind_option_types = Spree::Variants::OptionTypesFinder.new(variant_ids: @product.variants.ids)
51
+ .execute.select(&:variant?)
52
+ @selected_option_value_ids = Array(params[:selected_option_value_ids]).map(&:to_i)
53
+
54
+ inventory_items = @product.inventory_items.includes(:variant, :prices).where(
55
+ inventory_date: from_date..to_date
56
+ )
57
+
58
+ if @selected_option_value_ids.present?
59
+ variant_ids = SpreeCmCommissioner::Variants::FindByOptionValueIds.new(
60
+ product: @product, option_value_ids: @selected_option_value_ids
61
+ ).execute.ids
62
+ inventory_items = inventory_items.where(variant_id: variant_ids)
63
+ @matched_variants_count = variant_ids.size
64
+ else
65
+ @matched_variants_count = @product.variants.count
66
+ end
67
+
68
+ @inventory_items = inventory_items.to_a
61
69
 
62
70
  @cached_inventory_items = ::SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(@inventory_items)
63
71
  .call
@@ -35,7 +35,7 @@ module Spree
35
35
 
36
36
  def force_pull
37
37
  SpreeCmCommissioner::WaitingRoomLatestSystemMetadataPullerJob.perform_now
38
- SpreeCmCommissioner::WaitingGuestsCallerJob.perform_now
38
+ SpreeCmCommissioner::WaitingRoom::AdvanceQueueJob.perform_now
39
39
 
40
40
  redirect_back fallback_location: admin_system_waiting_room_path
41
41
  end
@@ -13,7 +13,7 @@ module Spree
13
13
 
14
14
  # Kills only the sessions the admin checked (Gmail-style select + delete), instead of the
15
15
  # room's whole active set. Firebase's active_sessions/full reflect this only on the next
16
- # WaitingGuestsCaller run (they're a once-a-minute mirror, not live).
16
+ # Admission::CallGuests run (they're a once-a-minute mirror, not live).
17
17
  def kill
18
18
  ids = Array(params[:ids])
19
19
  count = SpreeCmCommissioner::WaitingRoomSession.kill_by_ids!(ids)
@@ -0,0 +1,48 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Platform
5
+ # Single reusable entry point for tenant-scoping any Platform API resource: pass
6
+ # `?tenant_id=X` on the request and the collection is narrowed to that tenant, without each
7
+ # controller having to wire up its own filter/param handling.
8
+ #
9
+ # Tri-state on the param itself:
10
+ # - absent: no scoping applied, collection flows through unrestricted (existing callers,
11
+ # eg. the products/users pickers in checkouts and private sales, are unaffected).
12
+ # - "0": scope to records with no tenant (tenant_id IS NULL) - most vendors today aren't
13
+ # attached to a tenant, so this is the common case for tenant-scoped search.
14
+ # - any other value: scope to that exact tenant_id.
15
+ #
16
+ # Default assumes the model owns a `tenant_id` column directly (true for Spree::User,
17
+ # Spree::Vendor, SpreeCmCommissioner::HomepageSection - see their `multi_tenant :tenant`
18
+ # declarations). A model related to tenant only indirectly should override
19
+ # `scope_by_tenant` in its own controller decorator.
20
+ module ResourceControllerDecorator
21
+ def scope
22
+ base_scope = super
23
+
24
+ # Keep the existing behavior when no tenant_id param is passed,
25
+ # so that existing controllers are unaffected.
26
+ return base_scope if params[:tenant_id].nil?
27
+
28
+ scope_by_tenant(base_scope, params[:tenant_id])
29
+ end
30
+
31
+ private
32
+
33
+ def scope_by_tenant(scope, tenant_id)
34
+ return scope unless scope.klass.column_names.include?('tenant_id')
35
+
36
+ if tenant_id.to_s == '0'
37
+ scope.where(tenant_id: nil)
38
+ else
39
+ scope.where(tenant_id: tenant_id)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ Spree::Api::V2::Platform::ResourceController.prepend(Spree::Api::V2::Platform::ResourceControllerDecorator)
@@ -5,6 +5,9 @@ module Spree
5
5
  class DraftOrdersController < ::Spree::Api::V2::ResourceController
6
6
  # Creating the order acquires the inventory hold, so honor the load-test bypass here too.
7
7
  include SpreeCmCommissioner::LoadTestBypassable
8
+ include SpreeCmCommissioner::AnonymousBookingGuard
9
+
10
+ before_action -> { ensure_anonymous_booking_allowed!(draft_order_products) }, only: :create
8
11
 
9
12
  # Creates a generic draft order from a locally-assembled cart in a single request.
10
13
  # Endpoint: POST /api/v2/storefront/draft_orders
@@ -46,6 +49,11 @@ module Spree
46
49
  .fetch(:line_items, [])
47
50
  end
48
51
 
52
+ def draft_order_products
53
+ variant_ids = draft_order_line_items.pluck(:variant_id).compact
54
+ Spree::Variant.where(id: variant_ids).includes(:product).map(&:product)
55
+ end
56
+
49
57
  # override
50
58
  def resource_serializer
51
59
  Spree::Api::Dependencies.storefront_cart_serializer.constantize
@@ -6,8 +6,10 @@ module Spree
6
6
  class DraftOrdersController < ::Spree::Api::V2::ResourceController
7
7
  include Spree::Api::V2::Storefront::OrderConcern
8
8
  include SpreeCmCommissioner::OrderConcern
9
+ include SpreeCmCommissioner::AnonymousBookingGuard
9
10
 
10
11
  before_action :ensure_order, only: :update
12
+ before_action -> { ensure_anonymous_booking_allowed!(intercity_taxi_products) }, only: :create
11
13
 
12
14
  def create
13
15
  result = SpreeCmCommissioner::IntercityTaxiOrder::Create.call(
@@ -64,6 +66,11 @@ module Spree
64
66
 
65
67
  private
66
68
 
69
+ def intercity_taxi_products
70
+ trip = SpreeCmCommissioner::Trip.find_by(id: params[:trip_id])
71
+ trip ? [trip.product] : []
72
+ end
73
+
67
74
  def required_schema
68
75
  if action_name == 'create'
69
76
  SpreeCmCommissioner::IntercityTaxiDraftOrderSchema
@@ -3,9 +3,10 @@ module Spree
3
3
  module V2
4
4
  module Storefront
5
5
  # Order & user independent catalog of the payment methods a vendor accepts.
6
- # Takes vendor_id as a required query param. See
6
+ # Takes vendor_id as a required query param, and an optional payout param
7
+ # (caller passes Order#required_payway_payout? — see PaymentMethods::Find). See
7
8
  # Spree::Api::V2::Tenant::PaymentMethodsController for the caching caveat
8
- # that comes with that, and for what this deliberately leaves out.
9
+ # that comes with vendor_id, and for what else this deliberately leaves out.
9
10
  class PaymentMethodsController < Spree::Api::V2::ResourceController
10
11
  # override
11
12
  def index
@@ -19,7 +20,8 @@ module Spree
19
20
  @collection ||= SpreeCmCommissioner::PaymentMethods::Find.new(
20
21
  vendor: vendor,
21
22
  store: current_store,
22
- type: params[:type]
23
+ type: params[:type],
24
+ payout: payout?
23
25
  ).execute
24
26
  end
25
27
 
@@ -33,6 +35,10 @@ module Spree
33
35
  def vendor
34
36
  @vendor ||= ::Spree::Vendor.active.find(params[:vendor_id])
35
37
  end
38
+
39
+ def payout?
40
+ ActiveModel::Type::Boolean.new.cast(params[:payout])
41
+ end
36
42
  end
37
43
  end
38
44
  end
@@ -6,6 +6,9 @@ module Spree
6
6
  # Sets Current.load_test_bypass from the X-Request-Verify secret. The hold is acquired later
7
7
  # in ProcessJob, so `create` passes this flag through options for the job to restore there.
8
8
  include SpreeCmCommissioner::LoadTestBypassable
9
+ include SpreeCmCommissioner::AnonymousBookingGuard
10
+
11
+ before_action -> { ensure_anonymous_booking_allowed!(draft_order_products) }, only: :create
9
12
 
10
13
  # Accepts a draft order request without building it: writes a queued Firestore status
11
14
  # document and schedules the real build (SpreeCmCommissioner::DraftOrders::Create) on a
@@ -55,6 +58,11 @@ module Spree
55
58
  .deep_symbolize_keys
56
59
  .fetch(:line_items, [])
57
60
  end
61
+
62
+ def draft_order_products
63
+ variant_ids = draft_order_line_items.pluck(:variant_id).compact
64
+ Spree::Variant.where(id: variant_ids).includes(:product).map(&:product)
65
+ end
58
66
  end
59
67
  end
60
68
  end
@@ -4,6 +4,8 @@ module Spree
4
4
  module Storefront
5
5
  module Transit
6
6
  class DraftOrdersController < ::Spree::Api::V2::ResourceController
7
+ include SpreeCmCommissioner::AnonymousBookingGuard
8
+
7
9
  # Creates a draft transit order.
8
10
  # Endpoint: POST /api/v2/storefront/transit/draft_orders
9
11
  # Params:
@@ -15,6 +17,7 @@ module Spree
15
17
  def create
16
18
  @outbound_legs = params[:outbound_legs].is_a?(Array) && params[:outbound_legs].any? ? build_legs(:outbound, params[:outbound_legs]) : []
17
19
  @inbound_legs = params[:inbound_legs].is_a?(Array) && params[:inbound_legs].any? ? build_legs(:inbound, params[:inbound_legs]) : []
20
+ ensure_anonymous_booking_allowed!(transit_leg_products)
18
21
 
19
22
  result = SpreeCmCommissioner::TransitOrder::Create.call(
20
23
  outbound_date: params[:outbound_date]&.to_date,
@@ -36,6 +39,13 @@ module Spree
36
39
  SpreeCmCommissioner::Transit::LegsBuilderService.new(direction: direction, legs_params: legs_params).call
37
40
  end
38
41
 
42
+ private
43
+
44
+ def transit_leg_products
45
+ variant_ids = (@outbound_legs + @inbound_legs).flat_map(&:seat_selections).map(&:variant_id)
46
+ Spree::Variant.where(id: variant_ids).includes(:product).map(&:product)
47
+ end
48
+
39
49
  # override
40
50
  def resource_serializer
41
51
  Spree::Api::Dependencies.storefront_cart_serializer.constantize
@@ -10,7 +10,7 @@ module Spree
10
10
  skip_before_action :ensure_waiting_room_session_token
11
11
 
12
12
  def create
13
- context = SpreeCmCommissioner::WaitingRoomSessionCreator.call(
13
+ result = SpreeCmCommissioner::WaitingRoom::Sessions::Issue.call(
14
14
  tenant_id: nil,
15
15
  remote_ip: request.remote_ip,
16
16
  waiting_guest_firebase_doc_id: params[:waiting_guest_firebase_doc_id],
@@ -21,10 +21,10 @@ module Spree
21
21
  user: spree_current_user
22
22
  )
23
23
 
24
- if context.success?
25
- render_serialized_payload { serialize_resource(context.room_session) }
24
+ if result.success?
25
+ render_serialized_payload { serialize_resource(result.value[:room_session]) }
26
26
  else
27
- render_error_payload(context.message)
27
+ render_error_payload(result.error)
28
28
  end
29
29
  end
30
30
 
@@ -35,7 +35,7 @@ module Spree
35
35
  private
36
36
 
37
37
  def permitted_device_attributes
38
- params.permit(*SpreeCmCommissioner::WaitingRoomSessionCreator::PERMITTED_DEVICE_ATTRIBUTES)
38
+ params.permit(*SpreeCmCommissioner::WaitingRoom::Sessions::Issue::PERMITTED_DEVICE_ATTRIBUTES)
39
39
  end
40
40
  end
41
41
  end
@@ -3,6 +3,10 @@ module Spree
3
3
  module V2
4
4
  module Tenant
5
5
  class DraftOrdersController < BaseController
6
+ include SpreeCmCommissioner::AnonymousBookingGuard
7
+
8
+ before_action -> { ensure_anonymous_booking_allowed!(draft_order_products) }, only: :create
9
+
6
10
  # Creates a generic draft order from a locally-assembled cart in a single request.
7
11
  # Endpoint: POST /api/v2/tenant/draft_orders
8
12
  # Params:
@@ -44,6 +48,11 @@ module Spree
44
48
  .fetch(:line_items, [])
45
49
  end
46
50
 
51
+ def draft_order_products
52
+ variant_ids = draft_order_line_items.pluck(:variant_id).compact
53
+ Spree::Variant.where(id: variant_ids).includes(:product).map(&:product)
54
+ end
55
+
47
56
  # override
48
57
  def resource_serializer
49
58
  Spree::V2::Tenant::CartSerializer
@@ -6,8 +6,10 @@ module Spree
6
6
  class DraftOrdersController < BaseController
7
7
  include Spree::Api::V2::Storefront::OrderConcern
8
8
  include SpreeCmCommissioner::OrderConcern
9
+ include SpreeCmCommissioner::AnonymousBookingGuard
9
10
 
10
11
  before_action :ensure_order, only: :update
12
+ before_action -> { ensure_anonymous_booking_allowed!(intercity_taxi_products) }, only: :create
11
13
 
12
14
  def create
13
15
  result = SpreeCmCommissioner::IntercityTaxiOrder::Create.call(
@@ -65,6 +67,11 @@ module Spree
65
67
 
66
68
  private
67
69
 
70
+ def intercity_taxi_products
71
+ trip = SpreeCmCommissioner::Trip.find_by(id: params[:trip_id])
72
+ trip ? [trip.product] : []
73
+ end
74
+
68
75
  def required_schema
69
76
  if action_name == 'create'
70
77
  SpreeCmCommissioner::IntercityTaxiDraftOrderSchema
@@ -4,9 +4,11 @@ module Spree
4
4
  module Tenant
5
5
  # Order & user independent catalog of the payment methods a vendor accepts.
6
6
  #
7
- # vendor_id is a required query param. A vendor id is globally unique and
8
- # belongs to exactly one tenant, so it is what identifies tenant, vendor and
9
- # store here the tenant itself comes from the Doorkeeper token, not the URL.
7
+ # vendor_id is a required query param, and payout is an optional one (caller
8
+ # passes Order#required_payway_payout? see PaymentMethods::Find). A vendor id
9
+ # is globally unique and belongs to exactly one tenant, so it is what identifies
10
+ # tenant, vendor and store here — the tenant itself comes from the Doorkeeper
11
+ # token, not the URL.
10
12
  #
11
13
  # IMPORTANT for caching (see SpreeCmCommissioner::ContentCachable): the CDN
12
14
  # must include vendor_id in its cache key. If query strings are stripped,
@@ -14,7 +16,7 @@ module Spree
14
16
  # are served. Moving to /vendors/:vendor_id/payment_methods would remove that
15
17
  # dependency — tracked as a follow-up.
16
18
  #
17
- # For the per-order truth (store credit, early adopter, payout filtering)
19
+ # For the full per-order truth (store credit, early adopter filtering)
18
20
  # use CartPaymentMethodGroupsController instead.
19
21
  class PaymentMethodsController < BaseController
20
22
  # override
@@ -29,7 +31,8 @@ module Spree
29
31
  @collection ||= SpreeCmCommissioner::PaymentMethods::Find.new(
30
32
  vendor: vendor,
31
33
  store: current_store,
32
- type: params[:type]
34
+ type: params[:type],
35
+ payout: payout?
33
36
  ).execute
34
37
  end
35
38
 
@@ -47,6 +50,10 @@ module Spree
47
50
  ::Spree::Vendor.active.find(params[:vendor_id])
48
51
  end
49
52
  end
53
+
54
+ def payout?
55
+ ActiveModel::Type::Boolean.new.cast(params[:payout])
56
+ end
50
57
  end
51
58
  end
52
59
  end
@@ -13,9 +13,7 @@ module Spree
13
13
  # override
14
14
  def scope
15
15
  scope = current_show.show_people
16
- .joins(:assignments)
17
- .distinct
18
- .includes(:assignments, :show_person_images)
16
+ .includes(:show_person_images)
19
17
 
20
18
  role = params[:role].presence
21
19
  return scope if role.blank?
@@ -4,6 +4,8 @@ module Spree
4
4
  module Tenant
5
5
  module Transit
6
6
  class DraftOrdersController < BaseController
7
+ include SpreeCmCommissioner::AnonymousBookingGuard
8
+
7
9
  # Creates a draft transit order for tenant.
8
10
  # Endpoint: POST /api/v2/tenant/transit/draft_orders
9
11
  # Params:
@@ -15,6 +17,7 @@ module Spree
15
17
  def create
16
18
  @outbound_legs = params[:outbound_legs].is_a?(Array) && params[:outbound_legs].any? ? build_legs(:outbound, params[:outbound_legs]) : []
17
19
  @inbound_legs = params[:inbound_legs].is_a?(Array) && params[:inbound_legs].any? ? build_legs(:inbound, params[:inbound_legs]) : []
20
+ ensure_anonymous_booking_allowed!(transit_leg_products)
18
21
 
19
22
  result = SpreeCmCommissioner::TransitOrder::Create.call(
20
23
  outbound_date: params[:outbound_date]&.to_date,
@@ -36,6 +39,13 @@ module Spree
36
39
  SpreeCmCommissioner::Transit::LegsBuilderService.new(direction: direction, legs_params: legs_params).call
37
40
  end
38
41
 
42
+ private
43
+
44
+ def transit_leg_products
45
+ variant_ids = (@outbound_legs + @inbound_legs).flat_map(&:seat_selections).map(&:variant_id)
46
+ Spree::Variant.where(id: variant_ids).includes(:product).map(&:product)
47
+ end
48
+
39
49
  # override
40
50
  def resource_serializer
41
51
  Spree::V2::Tenant::TransitCartSerializer
@@ -6,7 +6,7 @@ module Spree
6
6
  skip_before_action :ensure_waiting_room_session_token
7
7
 
8
8
  def create
9
- context = SpreeCmCommissioner::WaitingRoomSessionCreator.call(
9
+ result = SpreeCmCommissioner::WaitingRoom::Sessions::Issue.call(
10
10
  tenant_id: MultiTenant.current_tenant_id,
11
11
  remote_ip: request.remote_ip,
12
12
  waiting_guest_firebase_doc_id: params[:waiting_guest_firebase_doc_id],
@@ -14,10 +14,10 @@ module Spree
14
14
  device_attributes: permitted_device_attributes
15
15
  )
16
16
 
17
- if context.success?
18
- render_serialized_payload { serialize_resource(context.room_session) }
17
+ if result.success?
18
+ render_serialized_payload { serialize_resource(result.value[:room_session]) }
19
19
  else
20
- render_error_payload(context.message)
20
+ render_error_payload(result.error)
21
21
  end
22
22
  end
23
23
 
@@ -28,7 +28,7 @@ module Spree
28
28
  private
29
29
 
30
30
  def permitted_device_attributes
31
- params.permit(*SpreeCmCommissioner::WaitingRoomSessionCreator::PERMITTED_DEVICE_ATTRIBUTES)
31
+ params.permit(*SpreeCmCommissioner::WaitingRoom::Sessions::Issue::PERMITTED_DEVICE_ATTRIBUTES)
32
32
  end
33
33
  end
34
34
  end
@@ -0,0 +1,39 @@
1
+ module SpreeCmCommissioner
2
+ # The one server-side endpoint the standalone waiting_room_simulation/ CLI needs — everything
3
+ # else it does directly against Firestore with its own service account (seeding, heartbeats,
4
+ # reading frontier/lobby state), and session mint/renew already has a real public endpoint
5
+ # (WaitingRoomSessionsController). This exists only because WaitingRoom::AdvanceQueue is a Ruby
6
+ # class that only exists inside this Rails app, with no equivalent reachable over Firestore.
7
+ #
8
+ # A machine client with no session, so CSRF protection doesn't apply here the way it does for a
9
+ # browser-originated request — authenticated instead by a shared secret
10
+ # (WAITING_ROOM_SIMULATION_TICK_KEY), same shape as LoadTestBypassable's X-Request-Verify check.
11
+ # Unset key => the endpoint 404s entirely, never a default-open state.
12
+ class WaitingRoomSimulationOperationsController < ApplicationController
13
+ skip_before_action :verify_authenticity_token
14
+ before_action :verify_key!
15
+
16
+ # AdvanceQueue always runs its full sweep+stamp+call sequence — there's only ever one thing
17
+ # this endpoint can ask for, so the response always echoes both results. The response returns
18
+ # before CallGuestsChunkJob finishes writing Firestore — a caller polling for
19
+ # allow_to_enter_room_at needs to wait a beat.
20
+ def create
21
+ advance_result = SpreeCmCommissioner::WaitingRoom::AdvanceQueue.call!.value
22
+
23
+ render json: {
24
+ stamp: { stamped: advance_result[:stamped] },
25
+ call: { called: true, called_count: advance_result[:called_count] }
26
+ }
27
+ end
28
+
29
+ private
30
+
31
+ def verify_key!
32
+ key = ENV.fetch('WAITING_ROOM_SIMULATION_TICK_KEY', nil)
33
+ return head(:not_found) if key.blank?
34
+
35
+ provided = request.headers.fetch('X-Waiting-Room-Simulation-Key', nil).to_s
36
+ head(:unauthorized) unless ActiveSupport::SecurityUtils.secure_compare(provided, key)
37
+ end
38
+ end
39
+ end