spree_cm_commissioner 2.12.8 → 2.12.9

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 (70) 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 +51 -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/storefront/draft_orders_controller.rb +8 -0
  11. data/app/controllers/spree/api/v2/storefront/intercity_taxi/draft_orders_controller.rb +7 -0
  12. data/app/controllers/spree/api/v2/storefront/payment_methods_controller.rb +9 -3
  13. data/app/controllers/spree/api/v2/storefront/queue_draft_orders_controller.rb +8 -0
  14. data/app/controllers/spree/api/v2/storefront/transit/draft_orders_controller.rb +10 -0
  15. data/app/controllers/spree/api/v2/storefront/waiting_room_sessions_controller.rb +5 -5
  16. data/app/controllers/spree/api/v2/tenant/draft_orders_controller.rb +9 -0
  17. data/app/controllers/spree/api/v2/tenant/intercity_taxi/draft_orders_controller.rb +7 -0
  18. data/app/controllers/spree/api/v2/tenant/payment_methods_controller.rb +12 -5
  19. data/app/controllers/spree/api/v2/tenant/show_people_controller.rb +1 -3
  20. data/app/controllers/spree/api/v2/tenant/transit/draft_orders_controller.rb +10 -0
  21. data/app/controllers/spree/api/v2/tenant/waiting_room_sessions_controller.rb +5 -5
  22. data/app/controllers/spree_cm_commissioner/waiting_room_simulation_operations_controller.rb +39 -0
  23. data/app/finders/spree_cm_commissioner/payment_methods/find.rb +12 -9
  24. data/app/finders/spree_cm_commissioner/variants/find_by_option_value_ids.rb +26 -0
  25. data/app/interactors/spree_cm_commissioner/user_telegram_oauth_authenticator.rb +39 -8
  26. data/app/jobs/spree_cm_commissioner/stock_status_consolidation_job.rb +2 -4
  27. data/app/jobs/spree_cm_commissioner/waiting_room/admission/call_guests_chunk_job.rb +20 -0
  28. data/app/jobs/spree_cm_commissioner/waiting_room/advance_queue_job.rb +18 -0
  29. data/app/models/concerns/spree_cm_commissioner/firestore_connection.rb +4 -5
  30. data/app/models/concerns/spree_cm_commissioner/order_holdable.rb +22 -0
  31. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +10 -0
  32. data/app/models/concerns/spree_cm_commissioner/voting_session_timing_concern.rb +22 -4
  33. data/app/models/spree_cm_commissioner/show.rb +1 -1
  34. data/app/models/spree_cm_commissioner/voting_session.rb +2 -0
  35. data/app/models/spree_cm_commissioner/waiting_room_session.rb +1 -1
  36. data/app/overrides/spree/admin/orders/customer_details/_form/view_profile_link.html.erb.deface +16 -0
  37. data/app/overrides/spree/admin/orders/customer_details/edit/reassign_customer_search.html.erb.deface +49 -0
  38. data/app/overrides/spree/admin/shared/sub_menu/_product/events_tab.html.erb.deface +4 -1
  39. data/app/overrides/spree/admin/users/_form/fields.html.erb.deface +5 -0
  40. data/app/overrides/spree/admin/users/_tabs/page_title.html.erb.deface +6 -0
  41. data/app/serializers/spree/v2/tenant/voting_session_serializer.rb +2 -1
  42. data/app/serializers/spree/v2/tenant/waiting_room_session_serializer.rb +1 -1
  43. data/app/serializers/spree_cm_commissioner/v2/storefront/cart_serializer_decorator.rb +1 -0
  44. data/app/serializers/spree_cm_commissioner/v2/storefront/waiting_room_session_serializer.rb +1 -1
  45. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/inventory/unstock_inventory.rb +3 -1
  46. data/app/services/spree_cm_commissioner/inventory_holds/convert.rb +25 -6
  47. data/app/services/spree_cm_commissioner/voting_sessions/publish_live_state.rb +1 -1
  48. data/app/services/spree_cm_commissioner/voting_sessions/publish_to_firestore.rb +15 -3
  49. data/app/services/spree_cm_commissioner/waiting_room/admission/call_guests.rb +279 -0
  50. data/app/services/spree_cm_commissioner/waiting_room/admission/call_guests_chunk.rb +109 -0
  51. data/app/services/spree_cm_commissioner/waiting_room/advance_queue.rb +120 -0
  52. data/app/services/spree_cm_commissioner/waiting_room/bypass_eligibility.rb +1 -1
  53. data/app/services/spree_cm_commissioner/waiting_room/heartbeat/sweep.rb +90 -0
  54. data/app/services/spree_cm_commissioner/waiting_room/positions/stamp.rb +186 -0
  55. data/app/services/spree_cm_commissioner/waiting_room/sessions/issue.rb +232 -0
  56. data/app/services/spree_cm_commissioner/waiting_room_lobby_metadata_fetcher.rb +2 -2
  57. data/app/views/spree/admin/events/_search_form.html.erb +4 -4
  58. data/app/views/spree/admin/stock_managements/calendar.html.erb +72 -30
  59. data/app/views/spree/admin/stock_managements/index.html.erb +1 -1
  60. data/config/locales/en.yml +13 -0
  61. data/config/locales/km.yml +13 -0
  62. data/config/routes.rb +7 -0
  63. data/db/migrate/20260909000000_rename_jwt_token_to_token_on_cm_waiting_room_sessions.rb +13 -0
  64. data/lib/spree_cm_commissioner/version.rb +1 -1
  65. metadata +17 -7
  66. data/app/interactors/spree_cm_commissioner/waiting_guests_caller.rb +0 -259
  67. data/app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb +0 -226
  68. data/app/jobs/spree_cm_commissioner/waiting_guests_caller_job.rb +0 -15
  69. data/app/jobs/spree_cm_commissioner/waiting_room/stamp_queue_positions_job.rb +0 -14
  70. 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: 574f5215f2246c2cc433d99ab73113be56a4c82b509758548c1d0951c894caef
4
+ data.tar.gz: 0fec7f50315d67e24a41779dafd38467c5c7648180c4b54b3ec1d05f1cc7f1a6
5
5
  SHA512:
6
- metadata.gz: f8c8857a35fe48099949496f9e69c0741ca32a18a05fbc05afafdc1cb5cd7feb3394b3b0158cbb27dea96f585e378f9a4a14c0d2fbac1b8cee6e54c8a51be443
7
- data.tar.gz: d8e013c7a5172fabbfea21e83ac52d5231351ed380960e1308b458ad1e7b6cd960bb2ca7a6204b82448f3d494c17cb3eb3ebb9a5d704228b901c789dacef09f2
6
+ metadata.gz: 700f502becf735bfb55744ef153508ecfdb14f0f89f6a9a0b2063739c9739a0cbcfaab2d6b7f992858b0c601e212330a90d927a5c8c9b07621b96ef41071b35a
7
+ data.tar.gz: 31e2cc185129609a659dc3257a343d62ff9dc5b491ab8120a8e4b3daf2b9f77f8d3aca445f7a128b6e2f30bfad4504a5d2dfb0d2a6980ddcb9a33f8eb41551cf
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.12.9)
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,35 @@ 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
19
+ @order.associate_user!(user)
20
+ flash[:success] = Spree.t('admin.orders.customer_details.customer_assigned', email: user.email)
21
+ else
22
+ flash[:error] = Spree.t(:user_not_found)
23
+ end
24
+
25
+ redirect_to spree.edit_admin_order_customer_path(@order)
26
+ end
27
+
5
28
  # Override
6
29
  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?)
30
+ scope = tenant_user_scope
31
+ @user = scope.find_by(id: order_params[:user_id]) ||
32
+ (scope.find_by(email: order_params[:email]) if order_params[:email].present?) ||
33
+ (scope.find_by(phone_number: order_params[:phone_number]) if order_params[:phone_number].present?)
10
34
 
11
35
  return if @user
12
36
 
@@ -21,6 +45,30 @@ module Spree
21
45
  ship_address_attributes: permitted_address_attributes
22
46
  )
23
47
  end
48
+
49
+ private
50
+
51
+ # Looks up a candidate to reassign this order to by whatever the admin typed —
52
+ # email, phone number, or login — the same lookup used for real user sign-in
53
+ # ([UserIdentity#find_user_by_login]), scoped to the order's own tenant so an
54
+ # admin can never attach a user from a different tenant. Only sets `@order.email`
55
+ # in memory (never saved here) — `assign_customer` is what actually persists it,
56
+ # once the admin confirms the match by clicking Assign.
57
+ def search_for_customer
58
+ @customer_search_query = params[:customer_search_query].presence
59
+ return if @customer_search_query.nil?
60
+
61
+ @customer_search_result = Spree.user_class.find_user_by_login(@customer_search_query, @order.tenant_id)
62
+ @order.email = @customer_search_result.email if @customer_search_result
63
+ end
64
+
65
+ # Same tenant boundary `find_user_by_login` uses — the actual save path (`update`)
66
+ # must enforce this itself rather than trust the search box above, since a
67
+ # crafted `order[user_id]`/`order[email]` could otherwise pull in a user from
68
+ # another tenant.
69
+ def tenant_user_scope
70
+ @order.tenant_id.present? ? Spree.user_class.by_tenant(@order.tenant_id) : Spree.user_class.by_non_tenant
71
+ end
24
72
  end
25
73
  end
26
74
  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)
@@ -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
@@ -3,29 +3,32 @@ module SpreeCmCommissioner
3
3
  # Order & user independent lookup of the payment methods a vendor accepts.
4
4
  #
5
5
  # Mirrors the scope selection of Vpago::OrderDecorator#available_payment_methods,
6
- # minus everything that depends on the order or the current user, so the result
7
- # is safe to cache on the vendor id alone:
6
+ # minus everything that depends on the current user, so the result stays cacheable
7
+ # on vendor id (+ payout):
8
8
  #
9
9
  # - available_for_order? (only Spree::PaymentMethod::StoreCredit varies, on user balance)
10
10
  # - available_on_frontend_for_early_adopter (user attribute)
11
- # - support_payout? (order line items)
12
11
  # - preferred_payment_method_id (user, affects sort only)
13
12
  #
14
- # Callers needing the per-order truth must keep using
13
+ # `payout` covers `support_payout?`, since callers already know whether their order
14
+ # requires it (`Order#required_payway_payout?`, serialized onto the cart/order response)
15
+ # without needing to look at line items/shipments here.
16
+ #
17
+ # Callers needing the full per-order truth must keep using
15
18
  # Spree::Api::V2::*::CartPaymentMethodGroupsController.
16
19
  class Find
17
- attr_reader :vendor, :store, :type
20
+ attr_reader :vendor, :store, :type, :payout
18
21
 
19
- def initialize(vendor:, store: nil, type: nil)
22
+ def initialize(vendor:, store: nil, type: nil, payout: false)
20
23
  @vendor = vendor
21
24
  @store = store
22
25
  @type = type
26
+ @payout = payout
23
27
  end
24
28
 
25
29
  def execute
26
- return mini_app_scope if type.present?
27
-
28
- scope.reject(&:mini_app?)
30
+ result = type.present? ? mini_app_scope : scope.reject(&:mini_app?)
31
+ payout ? result.select(&:support_payout?) : result
29
32
  end
30
33
 
31
34
  private