spree_cm_commissioner 2.14.10 → 2.15.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +1 -2
  3. data/.tool-versions +1 -1
  4. data/Gemfile.lock +2 -2
  5. data/app/controllers/concerns/spree_cm_commissioner/operator/event_authorization.rb +23 -0
  6. data/app/controllers/concerns/spree_cm_commissioner/waiting_room_authorization.rb +12 -1
  7. data/app/controllers/spree/admin/system/waiting_room_sessions_controller.rb +12 -4
  8. data/app/controllers/spree/api/v2/operator/check_ins_controller.rb +3 -1
  9. data/app/controllers/spree/api/v2/operator/event_charts_controller.rb +20 -24
  10. data/app/controllers/spree/api/v2/operator/pie_chart_event_aggregators_controller.rb +8 -4
  11. data/app/controllers/spree/api/v2/storefront/draft_orders_controller.rb +3 -1
  12. data/app/controllers/spree/api/v2/storefront/queue_draft_orders_controller.rb +5 -0
  13. data/app/controllers/spree/api/v2/tenant/draft_orders_controller.rb +3 -1
  14. data/app/controllers/spree/api/v2/tenant/private_sale_products_controller.rb +33 -0
  15. data/app/controllers/spree/api/v2/tenant/queue_draft_orders_controller.rb +64 -0
  16. data/app/controllers/spree/api/v2/tenant/seat_layouts_controller.rb +93 -0
  17. data/app/jobs/spree_cm_commissioner/queue_draft_orders/process_job.rb +6 -3
  18. data/app/models/concerns/spree_cm_commissioner/event_metadata.rb +10 -0
  19. data/app/models/concerns/spree_cm_commissioner/homepage_section_relatable_concern.rb +0 -8
  20. data/app/models/concerns/spree_cm_commissioner/order_attribution.rb +16 -0
  21. data/app/models/spree_cm_commissioner/dynamic_field.rb +0 -11
  22. data/app/models/spree_cm_commissioner/dynamic_field_option.rb +0 -11
  23. data/app/models/spree_cm_commissioner/homepage_background.rb +0 -3
  24. data/app/models/spree_cm_commissioner/homepage_section.rb +0 -3
  25. data/app/models/spree_cm_commissioner/inventory_item.rb +12 -31
  26. data/app/models/spree_cm_commissioner/maintenance_tasks/cache_invalidation.rb +2 -8
  27. data/app/models/spree_cm_commissioner/menu_decorator.rb +0 -3
  28. data/app/models/spree_cm_commissioner/menu_item_decorator.rb +11 -3
  29. data/app/models/spree_cm_commissioner/order_decorator.rb +1 -0
  30. data/app/models/spree_cm_commissioner/product_decorator.rb +0 -13
  31. data/app/models/spree_cm_commissioner/product_dynamic_field.rb +0 -9
  32. data/app/models/spree_cm_commissioner/product_option_type_decorator.rb +0 -6
  33. data/app/models/spree_cm_commissioner/taxon_decorator.rb +0 -6
  34. data/app/models/spree_cm_commissioner/variant_decorator.rb +0 -4
  35. data/app/models/spree_cm_commissioner/vendor_decorator.rb +0 -3
  36. data/app/models/spree_cm_commissioner/waiting_room_session.rb +4 -0
  37. data/app/overrides/spree/admin/menu_items/_form/hidden_mini_apps_field.html.erb.deface +16 -0
  38. data/app/overrides/spree/admin/taxons/_form/check_in_flows.html.erb.deface +18 -0
  39. data/app/serializers/spree/api/v2/base_serializer_decorator.rb +2 -2
  40. data/app/serializers/spree/v2/tenant/cart_serializer.rb +2 -1
  41. data/app/serializers/spree_cm_commissioner/v2/operator/dashboard_crew_event_serializer.rb +32 -1
  42. data/app/serializers/spree_cm_commissioner/v2/storefront/menu_item_serializer_decorator.rb +4 -0
  43. data/app/services/spree_cm_commissioner/api_caches/fetch_details.rb +2 -3
  44. data/app/services/spree_cm_commissioner/checkout/strip_uncollected_guest_fields.rb +57 -0
  45. data/app/services/spree_cm_commissioner/checkout/update_decorator.rb +3 -0
  46. data/app/services/spree_cm_commissioner/draft_orders/create.rb +6 -1
  47. data/app/services/spree_cm_commissioner/events/clone.rb +238 -0
  48. data/app/services/spree_cm_commissioner/events/publish_stock_statuses_to_firestore.rb +11 -36
  49. data/app/services/spree_cm_commissioner/imports/orders/create.rb +27 -10
  50. data/app/services/spree_cm_commissioner/waiting_room/admission/call_guests_chunk.rb +9 -5
  51. data/app/services/spree_cm_commissioner/waiting_room/guests/list.rb +10 -5
  52. data/app/views/spree/admin/private_sales/_stock.html.erb +11 -10
  53. data/app/views/spree/admin/system/waiting_room_sessions/_guests_pagination.html.erb +1 -1
  54. data/app/views/spree/admin/system/waiting_room_sessions/guests.html.erb +19 -7
  55. data/config/initializers/spree_permitted_attributes.rb +3 -0
  56. data/config/locales/en.yml +6 -2
  57. data/config/locales/km.yml +3 -0
  58. data/config/routes.rb +3 -0
  59. data/db/migrate/20261105000001_add_hidden_mini_app_to_spree_menu_items.rb +5 -0
  60. data/db/migrate/20261105000001_add_waiting_room_session_id_to_spree_orders.rb +13 -0
  61. data/db/migrate/20261105000002_add_index_to_spree_orders_waiting_room_session_id.rb +13 -0
  62. data/db/migrate/20261105000003_add_queue_fields_to_cm_waiting_room_sessions.rb +12 -0
  63. data/db/migrate/20261105000004_add_oauth_application_id_to_spree_orders.rb +13 -0
  64. data/lib/spree_cm_commissioner/version.rb +1 -1
  65. metadata +14 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de45fdbe1a6ac4c18da7e6b8fbd3eb6f4fe7241bb10e23c0d4c23973a91b7170
4
- data.tar.gz: 54156df2cef71cff39256c04a949869559c0e477f6c1ba413d8b0ef91761d616
3
+ metadata.gz: b32073d2114c66c1a2f712bbcfb05712cb153b8a651c2c33800665b0422d2541
4
+ data.tar.gz: 790420e894c681441fba0109c0aafc51bbcbd19a3c076743404db71dd0c6ce7b
5
5
  SHA512:
6
- metadata.gz: '0383b1bb4e0459522cc0004b2a841a5697e18b9b28ff62469a8e1ce5501f91e7fb3fb8899689c2174d997484231b71883408069aca5f53ed0af400bb8e091cd6'
7
- data.tar.gz: 5adeb841a23fb3d28a74e0972d34c478a0a80e26ba5ab114bb4d837aac2dadaf98fa9e696aff506fa4ed0de1acaa48af64067e8db8cd34803aa2518e0661945d
6
+ metadata.gz: 79d3b977f4d3d63523ff5920cc17c7598eb320ed3b6e3dae89bff7d3ee3a0d3f40744a2788e033910da0fa61fcddeaff589414d2b6d9373c52b9f444fe44ff2a
7
+ data.tar.gz: ca37c84b2064fe10e2187b9610385c6db5ab1016aa64df2f8e34b520fd65f37c59566f1d879aedb822fa43467f43da8f2acda7609ae9aba56ea67d40dc807e72
data/.env.example CHANGED
@@ -49,8 +49,6 @@ DISTANCE_SIGNING_KEY=hei********************VY
49
49
 
50
50
  # CDN cache durations (s-maxage, in seconds) for listed public APIs; client max-age is always 0, unlisted APIs are never CDN-cached
51
51
  # See: app/controllers/concerns/spree_cm_commissioner/content_cachable.rb
52
- CACHE_AUTO_INVALIDATION_DISABLED="" # Set to yes to stop automatic CDN cache invalidation on save (kill switch); a clear-cache click still sends one
53
- CACHE_SERIALIZER_DISABLED="" # Set to yes to turn off Spree's serializer record cache everywhere
54
52
  CACHE_STATIC_MAX_AGE=86400 # Static content (CMS pages) - 1 day
55
53
  CACHE_REALTIME_MAX_AGE=300 # Everything else listed (homepage, menus, products, events, vendors, trips, seat layouts, countries, provinces, guest card classes) - 5 minutes
56
54
 
@@ -95,3 +93,4 @@ ORDER_INTEGRITY_TELEGRAM_ALERT_ENABLED="no" # Set to yes to enable the order in
95
93
  PRE_INVENTORY_DAYS_TRANSIT=180 # How many days ahead inventory items are pre-generated for transit variants
96
94
  PRE_INVENTORY_DAYS_ACCOMMODATION=365 # How many days ahead inventory items are pre-generated for accommodation variants
97
95
  PRE_INVENTORY_DAYS_SERVICE=180 # How many days ahead inventory items are pre-generated for service variants
96
+ PAYMENT_BASE_URL=http://localhost:4000
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.2.0
1
+ ruby 3.2.11
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.14.10)
37
+ spree_cm_commissioner (2.15.0)
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.3.4)
889
+ spree_vpago (2.4.6)
890
890
  faraday
891
891
  google-cloud-firestore
892
892
  spree_api (>= 4.5)
@@ -3,6 +3,8 @@ module SpreeCmCommissioner
3
3
  module EventAuthorization
4
4
  extend ActiveSupport::Concern
5
5
 
6
+ OPERATOR_CHART_TYPE = 'entry_type'.freeze
7
+
6
8
  private
7
9
 
8
10
  # True only when the current user holds the organizer role AND owns the
@@ -14,6 +16,27 @@ module SpreeCmCommissioner
14
16
 
15
17
  spree_current_user.vendors.exists?(id: taxon.vendor_id)
16
18
  end
19
+
20
+ # Whether the current user may load `chart_type` for `taxon`. A user holding the operator role —
21
+ # even if also an organizer — sees only the entry type chart, and only while the event allows
22
+ # operators the total ticket number or the event overview (allow_total_ticket_order_for_operator,
23
+ # allow_event_overview_for_operator); the participation chart exposes the event's overall
24
+ # show / no-show totals, so it is never shown to them. Everyone else: the event's vendor
25
+ # organizer sees every chart, and a user assigned to the event sees only the entry type chart.
26
+ def chart_type_allowed?(taxon, chart_type)
27
+ return false if taxon.nil? || spree_current_user.nil?
28
+ return operator_chart_allowed?(taxon, chart_type) if spree_current_user.operator?
29
+ return true if organizer_for_event?(taxon)
30
+ return false unless spree_current_user.events.exists?(id: taxon.id)
31
+
32
+ chart_type.to_s == OPERATOR_CHART_TYPE
33
+ end
34
+
35
+ def operator_chart_allowed?(taxon, chart_type)
36
+ (taxon.allow_total_ticket_order_for_operator || taxon.allow_event_overview_for_operator) &&
37
+ chart_type.to_s == OPERATOR_CHART_TYPE &&
38
+ spree_current_user.events.exists?(id: taxon.id)
39
+ end
17
40
  end
18
41
  end
19
42
  end
@@ -53,7 +53,18 @@ module SpreeCmCommissioner
53
53
  end
54
54
 
55
55
  def active_waiting_room_session?
56
- SpreeCmCommissioner::WaitingRoomSession.active.exists?(token: params[:waiting_room_session_token])
56
+ current_waiting_room_session.present?
57
+ end
58
+
59
+ # The active session behind this request's token, memoized so the guard and the draft order it
60
+ # admits share one lookup. Draft-order actions store its id on the order for analytics even when
61
+ # the guard didn't run (non-enforced product, private sale, waiting room disabled): the app sends
62
+ # the token on every request while its session is live, and only an active one is ever linked.
63
+ def current_waiting_room_session
64
+ return @current_waiting_room_session if defined?(@current_waiting_room_session)
65
+
66
+ token = params[:waiting_room_session_token]
67
+ @current_waiting_room_session = token.present? ? SpreeCmCommissioner::WaitingRoomSession.active.find_by(token: token) : nil
57
68
  end
58
69
 
59
70
  # Verified, not just present: a made-up `st` must not skip the queue, so a token that fails
@@ -33,6 +33,7 @@ module Spree
33
33
  @doc_id = params[:doc_id].presence
34
34
  @per_page = guests_per_page
35
35
  @in_queue_only = guests_in_queue_only?
36
+ @newest_first = guests_newest_first?
36
37
  end
37
38
 
38
39
  # GET .../waiting_room_sessions/guests_status -- the poll target, mirroring
@@ -43,6 +44,7 @@ module Spree
43
44
  date = params[:date].presence || Time.zone.today.strftime('%Y-%m-%d')
44
45
  doc_id = params[:doc_id].presence
45
46
  in_queue_only = guests_in_queue_only?
47
+ newest_first = guests_newest_first?
46
48
  error = nil
47
49
 
48
50
  guests =
@@ -56,17 +58,17 @@ module Spree
56
58
  else
57
59
  # Cached for the same span as the client's poll interval so N concurrently open admin
58
60
  # tabs (each polling every 10s) collapse into one Firestore read per date per 10s,
59
- # instead of one per open tab. Keyed by filter too -- "in queue" and "all" are
60
- # different Firestore queries, not a client-side slice of the same result.
61
+ # instead of one per open tab. Keyed by filter and order too -- "in queue"/"all" and
62
+ # oldest/newest are different Firestore queries, not a client-side slice of one result.
61
63
  #
62
64
  # A failed fetch (e.g. a missing Firestore composite index for in_queue_only's
63
65
  # where+order combination) is never cached: caching `[]` would make the next 10s
64
66
  # poll -- and the one after that -- believe the queue is empty instead of retrying.
65
- cache_key = "admin/waiting_room_guests/#{date}/#{in_queue_only}"
67
+ cache_key = "admin/waiting_room_guests/#{date}/#{in_queue_only}/#{newest_first}"
66
68
  docs = Rails.cache.read(cache_key)
67
69
 
68
70
  if docs.nil?
69
- result = SpreeCmCommissioner::WaitingRoom::Guests::List.call(date: date, in_queue_only: in_queue_only)
71
+ result = SpreeCmCommissioner::WaitingRoom::Guests::List.call(date: date, in_queue_only: in_queue_only, newest_first: newest_first)
70
72
  if result.success?
71
73
  docs = result.value[:guests]
72
74
  Rails.cache.write(cache_key, docs, expires_in: 10.seconds)
@@ -122,6 +124,12 @@ module Spree
122
124
  params[:status] != 'all'
123
125
  end
124
126
 
127
+ # Defaults to oldest-first (queue order). ?order=newest is for spotting who joined last,
128
+ # which oldest-first buries on the final page -- or past MAX_DOCS, doesn't load at all.
129
+ def guests_newest_first?
130
+ params[:order] == 'newest'
131
+ end
132
+
125
133
  # Same 10s window as the guest list itself: this is one extra doc read per render, and the
126
134
  # "people ahead" figure it feeds only changes as often as Positions::Stamp ticks (10s),
127
135
  # so caching it any shorter would just be re-fetching a value that hasn't moved.
@@ -50,10 +50,12 @@ module Spree
50
50
  # The organizer who owns the event's vendor always sees check-in history.
51
51
  # Crew/operators assigned via user.events only see it while the event is
52
52
  # ongoing — once it ends, it becomes organizer-owned history. A missing/
53
- # nonexistent taxon_id is restricted the same way.
53
+ # nonexistent taxon_id is restricted the same way. Any user, organizer included,
54
+ # loses it when the event turns off allow_check_in_history_for_operator (mirrors DashboardCrewEventSerializer#permissions).
54
55
  def restricted?
55
56
  taxon = Spree::Taxon.find_by(id: params[:taxon_id])
56
57
  return true if taxon.nil?
58
+ return true unless taxon.allow_check_in_history_for_operator
57
59
  return false if organizer_for_event?(taxon)
58
60
  return true unless spree_current_user.events.exists?(id: taxon.id)
59
61
 
@@ -11,30 +11,26 @@ module Spree
11
11
  ADDITIONAL_FIELDS = %i[participation entry_type].freeze
12
12
 
13
13
  def index
14
- resources = if restricted?
15
- []
16
- else
17
- kyc_fields = fetch_filtered_kyc_fields
18
- types = ADDITIONAL_FIELDS + kyc_fields
14
+ kyc_fields = fetch_filtered_kyc_fields
15
+ types = (ADDITIONAL_FIELDS + kyc_fields).select { |type| chart_type_allowed?(event_taxon, type) }
19
16
 
20
- types.map do |type|
21
- # Create unique ID by using chart_type as the identifier
22
- SpreeCmCommissioner::EventChart.new(
23
- id: type.to_s,
24
- chart_type: type,
25
- product_charts: nil
26
- )
27
- end
28
- end
17
+ resources = types.map do |type|
18
+ # Create unique ID by using chart_type as the identifier
19
+ SpreeCmCommissioner::EventChart.new(
20
+ id: type.to_s,
21
+ chart_type: type,
22
+ product_charts: nil
23
+ )
24
+ end
29
25
 
30
26
  render json: SpreeCmCommissioner::V2::Operator::PieChartEventAggregatorSerializer.new(resources).serializable_hash
31
27
  end
32
28
 
33
29
  def resource
34
- @resource = if restricted?
35
- SpreeCmCommissioner::EventChart.new(id: params[:id], chart_type: params[:id] || 'participation', product_charts: [])
36
- else
30
+ @resource = if chart_type_allowed?(event_taxon, chart_type)
37
31
  event_chart_aggregator_queries.call
32
+ else
33
+ SpreeCmCommissioner::EventChart.new(id: params[:id], chart_type: chart_type, product_charts: [])
38
34
  end
39
35
  end
40
36
 
@@ -44,18 +40,18 @@ module Spree
44
40
 
45
41
  private
46
42
 
47
- # Only the organizer who owns the event's vendor may see chart data —
48
- # crew/operators assigned via user.events never see it, regardless of
49
- # whether the event is ongoing or ended. See organizer_for_event? (EventAuthorization).
50
- def restricted?
51
- taxon = Spree::Taxon.find_by(id: params[:taxon_id])
52
- !organizer_for_event?(taxon)
43
+ def chart_type
44
+ params[:id] || 'participation'
45
+ end
46
+
47
+ def event_taxon
48
+ @event_taxon ||= Spree::Taxon.find_by(id: params[:taxon_id])
53
49
  end
54
50
 
55
51
  def event_chart_aggregator_queries
56
52
  SpreeCmCommissioner::EventChartQueries.new(
57
53
  taxon_id: params[:taxon_id],
58
- chart_type: params[:id] || 'participation',
54
+ chart_type: chart_type,
59
55
  check_in_session_id: params[:check_in_session_id],
60
56
  refreshed: params[:refreshed] || false
61
57
  )
@@ -10,12 +10,12 @@ module Spree
10
10
  def resource
11
11
  taxon = Spree::Taxon.find_by(id: params[:taxon_id])
12
12
 
13
- @resource = if organizer_for_event?(taxon)
13
+ @resource = if chart_type_allowed?(taxon, chart_type)
14
14
  pie_chart_event_aggregator_queries.call
15
15
  else
16
16
  SpreeCmCommissioner::EventChart.new(
17
- id: params[:chart_type] || 'participation',
18
- chart_type: params[:chart_type] || 'participation',
17
+ id: chart_type,
18
+ chart_type: chart_type,
19
19
  product_charts: []
20
20
  )
21
21
  end
@@ -27,10 +27,14 @@ module Spree
27
27
 
28
28
  private
29
29
 
30
+ def chart_type
31
+ params[:chart_type] || 'participation'
32
+ end
33
+
30
34
  def pie_chart_event_aggregator_queries
31
35
  SpreeCmCommissioner::PieChartEventAggregatorQueries.new(
32
36
  taxon_id: params[:taxon_id],
33
- chart_type: params[:chart_type] || 'participation',
37
+ chart_type: chart_type,
34
38
  refreshed: params[:refreshed] || false
35
39
  )
36
40
  end
@@ -30,7 +30,9 @@ module Spree
30
30
  options: {
31
31
  device_id: device_id,
32
32
  last_ip_address: request.remote_ip,
33
- private_sale_token: params[:st]
33
+ private_sale_token: params[:st],
34
+ waiting_room_session_id: current_waiting_room_session&.id,
35
+ oauth_application_id: doorkeeper_token&.application_id
34
36
  }
35
37
  )
36
38
 
@@ -32,6 +32,11 @@ module Spree
32
32
  device_id: device_id,
33
33
  last_ip_address: request.remote_ip,
34
34
  private_sale_token: params[:st],
35
+
36
+ # Resolved now, not in ProcessJob: the session may have expired by the time the job runs.
37
+ waiting_room_session_id: current_waiting_room_session&.id,
38
+ oauth_application_id: doorkeeper_token&.application_id,
39
+
35
40
  # Carried into the job so it can bypass hold limits for a load test (see ProcessJob).
36
41
  load_test_bypass: SpreeCmCommissioner::Current.load_test_bypass
37
42
  }
@@ -27,7 +27,9 @@ module Spree
27
27
  options: {
28
28
  device_id: params[:device_id],
29
29
  last_ip_address: request.remote_ip,
30
- private_sale_token: params[:st]
30
+ private_sale_token: params[:st],
31
+ waiting_room_session_id: current_waiting_room_session&.id,
32
+ oauth_application_id: doorkeeper_token&.application_id
31
33
  }
32
34
  )
33
35
 
@@ -0,0 +1,33 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Tenant
5
+ # GET /api/v2/tenant/private_sale_products?st=<private sale token>
6
+ class PrivateSaleProductsController < ::Spree::Api::V2::Tenant::ProductsController
7
+ before_action :require_spree_current_user
8
+ before_action :authorize_private_sale_token!
9
+
10
+ private
11
+
12
+ attr_reader :private_sale
13
+
14
+ def authorize_private_sale_token!
15
+ result = SpreeCmCommissioner::PrivateSales::Authorize.call(
16
+ token: params[:st],
17
+ user: spree_current_user
18
+ )
19
+
20
+ return @private_sale = result.value if result.success?
21
+
22
+ render_error_payload(SpreeCmCommissioner::PrivateSales::Authorize.error_message(result.error.value), 403)
23
+ end
24
+
25
+ # override — super keeps it to this tenant's products, so a token for another tenant's sale returns nothing.
26
+ def scope
27
+ super.where(id: private_sale.product_ids)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,64 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Tenant
5
+ class QueueDraftOrdersController < BaseController
6
+ include SpreeCmCommissioner::LoadTestBypassable
7
+ include SpreeCmCommissioner::AnonymousBookingGuard
8
+
9
+ before_action -> { ensure_anonymous_booking_allowed!(draft_order_products) }, only: :create
10
+ before_action -> { ensure_waiting_room_session!(draft_order_products) }, only: :create
11
+
12
+ # Tenant counterpart of Storefront::QueueDraftOrdersController: the order is built later by
13
+ # QueueDraftOrders::ProcessJob, under this tenant.
14
+ #
15
+ # Endpoint: POST /api/v2/tenant/queue_draft_orders
16
+ # Params: same as POST /api/v2/tenant/draft_orders
17
+ def create
18
+ result = SpreeCmCommissioner::QueueDraftOrders::Enqueue.call(
19
+ create_type: 'generic',
20
+ creation_params: {
21
+ line_items: draft_order_line_items,
22
+ user: spree_current_user,
23
+ store: current_store,
24
+ currency: current_currency,
25
+ tenant: @tenant,
26
+ options: {
27
+ device_id: params[:device_id],
28
+ last_ip_address: request.remote_ip,
29
+ private_sale_token: params[:st],
30
+ load_test_bypass: SpreeCmCommissioner::Current.load_test_bypass
31
+ }
32
+ },
33
+ locale: I18n.locale.to_s
34
+ )
35
+
36
+ if result.success?
37
+ render_serialized_payload { result.value }
38
+ else
39
+ render_error_payload(result.error)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ # Plain symbol-keyed Hashes: this crosses the ActiveJob serialization boundary.
46
+ def draft_order_line_items
47
+ params.permit(line_items: [:variant_id, :quantity, { options: {}, public_metadata: {}, private_metadata: {} }])
48
+ .to_h
49
+ .deep_symbolize_keys
50
+ .fetch(:line_items, [])
51
+ end
52
+
53
+ # Memoized: both the anonymous-booking and waiting-room guards read it on the same request.
54
+ def draft_order_products
55
+ @draft_order_products ||= begin
56
+ variant_ids = draft_order_line_items.pluck(:variant_id).compact
57
+ Spree::Variant.where(id: variant_ids).includes(product: :event).map(&:product)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,93 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Tenant
5
+ class SeatLayoutsController < BaseController
6
+ # GET /api/v2/tenant/seat_layouts/:id
7
+ # - include_details=true|false (optional, default: true)
8
+ #
9
+ # Always returns `product_ids` (see SeatLayout#product_ids).
10
+ def show
11
+ render_serialized_payload { serialize_resource(resource) }
12
+ end
13
+
14
+ private
15
+
16
+ # GET /api/v2/tenant/seat_layouts?
17
+ # - variant_ids[]=[1,2]
18
+ # - include_details=true|false (optional, default: true)
19
+ #
20
+ # override
21
+ def collection
22
+ return @collection if defined?(@collection)
23
+
24
+ collection = scope.active.joins(:blocks).where(blocks: { variant_id: params[:variant_ids] }).distinct
25
+ collection = collection.includes(:top_level_blocks, seat_sections: :blocks) if include_details?
26
+
27
+ @collection = collection
28
+ end
29
+
30
+ # override
31
+ def resource
32
+ SpreeCmCommissioner::SeatLayout.includes(
33
+ :top_level_blocks,
34
+ { seat_sections: :blocks }
35
+ ).where(id: scope.select(:id)).find(params[:id])
36
+ end
37
+
38
+ # Seat layouts have no tenant_id. Event layouts reach the tenant through their blocks' variants;
39
+ # trip layouts are copied without variant_id on their blocks, so they go through the trip's vendor.
40
+ # override
41
+ def scope
42
+ event_layouts = SpreeCmCommissioner::SeatLayout.joins(blocks: { variant: :vendor })
43
+ .where(spree_vendors: { tenant_id: @tenant.id })
44
+ tenant_trips = SpreeCmCommissioner::Trip.joins(:vendor).where(spree_vendors: { tenant_id: @tenant.id })
45
+ trip_layouts = SpreeCmCommissioner::SeatLayout.where(
46
+ layoutable_type: 'SpreeCmCommissioner::Trip',
47
+ layoutable_id: tenant_trips.select(:id)
48
+ )
49
+
50
+ SpreeCmCommissioner::SeatLayout.where(id: event_layouts.select(:id)).or(
51
+ SpreeCmCommissioner::SeatLayout.where(id: trip_layouts.select(:id))
52
+ )
53
+ end
54
+
55
+ # override
56
+ def default_resource_includes
57
+ [
58
+ 'top_level_blocks',
59
+ 'seat_sections',
60
+ 'seat_sections.blocks'
61
+ ]
62
+ end
63
+
64
+ # override
65
+ def required_schema
66
+ return nil unless action_name == 'index'
67
+
68
+ SpreeCmCommissioner::SeatLayoutSchema
69
+ end
70
+
71
+ # override
72
+ def resource_serializer
73
+ SpreeCmCommissioner::V2::Storefront::SeatLayoutSerializer
74
+ end
75
+
76
+ # override
77
+ def collection_serializer
78
+ SpreeCmCommissioner::V2::Storefront::SeatLayoutSerializer
79
+ end
80
+
81
+ # override
82
+ def serializer_params
83
+ super.merge(include_details: include_details?, include_product_ids: action_name == 'show')
84
+ end
85
+
86
+ def include_details?
87
+ params.fetch(:include_details, 'true') == 'true'
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -16,9 +16,12 @@ module SpreeCmCommissioner
16
16
  # Current doesn't cross into the job, so restore the load-test-bypass flag here — otherwise
17
17
  # ValidateLimits would reject a trusted load test.
18
18
  SpreeCmCommissioner::Current.set(load_test_bypass: creation_params.dig(:options, :load_test_bypass) || false) do
19
- SpreeCmCommissioner::QueueDraftOrders::Process.call(
20
- document_path: document_path, create_type: create_type, creation_params: creation_params
21
- )
19
+ # Order#set_tenant_id reads MultiTenant.current_tenant_id, which the request set but the job doesn't.
20
+ MultiTenant.with(creation_params[:tenant]) do
21
+ SpreeCmCommissioner::QueueDraftOrders::Process.call(
22
+ document_path: document_path, create_type: create_type, creation_params: creation_params
23
+ )
24
+ end
22
25
  end
23
26
  end
24
27
  end
@@ -18,6 +18,16 @@ module SpreeCmCommissioner
18
18
  # For certain large events, admin can disable this manually.
19
19
  store_public_metadata :allow_manual_search_for_operator, :boolean, default: true
20
20
 
21
+ # Whether operators may see this event's total ticket number (and the entry type chart) in the
22
+ # operator app. Organizers of the event's vendor always see it. Admin can disable per event.
23
+ store_public_metadata :allow_total_ticket_order_for_operator, :boolean, default: true
24
+
25
+ # Operator app switches for this event. Only users holding the operator role are restricted
26
+ # (see DashboardCrewEventSerializer): check-in history screens, and the full Home screen with
27
+ # the dashboard charts ("event overview").
28
+ store_public_metadata :allow_check_in_history_for_operator, :boolean, default: true
29
+ store_public_metadata :allow_event_overview_for_operator, :boolean, default: true
30
+
21
31
  # Requires an active waiting-room session to create a draft order for this event's products
22
32
  # (Product#enforce_waiting_room_session?). Server-only switch, so private.
23
33
  store_private_metadata :enforce_waiting_room_session, :boolean, default: false
@@ -12,14 +12,6 @@ module SpreeCmCommissioner
12
12
  has_many :homepage_sections,
13
13
  through: :homepage_section_relatables,
14
14
  class_name: 'SpreeCmCommissioner::HomepageSection'
15
-
16
- after_save :schedule_homepage_section_caches_invalidation
17
- end
18
-
19
- def schedule_homepage_section_caches_invalidation
20
- homepage_sections.each do |homepage_section|
21
- SpreeCmCommissioner::MaintenanceTasks::CacheInvalidation.pending.create_or_find_by(maintainable: homepage_section)
22
- end
23
15
  end
24
16
  end
25
17
  end
@@ -0,0 +1,16 @@
1
+ module SpreeCmCommissioner
2
+ # Where an order came from, recorded once by DraftOrders::Create for analytics.
3
+ module OrderAttribution
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ # The active session the order was created through. Continuing checkout on another device or
8
+ # from booking history never updates it, so a later admission by the same buyer shows up as a
9
+ # session with no order of its own.
10
+ belongs_to :waiting_room_session, class_name: 'SpreeCmCommissioner::WaitingRoomSession', optional: true
11
+
12
+ # The OAuth client app whose access token created the order. nil for a request without a token.
13
+ belongs_to :oauth_application, class_name: 'Spree::OauthApplication', optional: true
14
+ end
15
+ end
16
+ end
@@ -19,7 +19,6 @@ module SpreeCmCommissioner
19
19
  validate :validate_data_fill_stage_phase
20
20
 
21
21
  after_commit :enqueue_data_fill_stage_maintenance_for_products, on: %i[update], if: :saved_change_to_data_fill_stage?
22
- after_commit :enqueue_cache_invalidation_for_products, on: %i[update]
23
22
 
24
23
  scope :with_dynamic_field_options, -> { includes(dynamic_field_options: :dynamic_field) }
25
24
 
@@ -58,16 +57,6 @@ module SpreeCmCommissioner
58
57
  end
59
58
  end
60
59
 
61
- # Destroy isn't handled here: by after_commit the join rows are gone, and each
62
- # ProductDynamicField destroyed with this field enqueues its own product.
63
- def enqueue_cache_invalidation_for_products
64
- product_dynamic_fields.distinct.pluck(:product_id).each do |product_id|
65
- SpreeCmCommissioner::MaintenanceTasks::CacheInvalidation
66
- .pending
67
- .create_or_find_by(maintainable_type: 'Spree::Product', maintainable_id: product_id)
68
- end
69
- end
70
-
71
60
  def validate_data_fill_stage_phase
72
61
  return if data_fill_stage.blank?
73
62
 
@@ -11,19 +11,8 @@ module SpreeCmCommissioner
11
11
  validates :status, presence: true
12
12
  validate :parent_field_must_be_selection_type
13
13
 
14
- after_commit :enqueue_cache_invalidation_for_products
15
-
16
14
  private
17
15
 
18
- # Options are embedded in the storefront dynamic_fields response of every product using the parent field.
19
- def enqueue_cache_invalidation_for_products
20
- SpreeCmCommissioner::ProductDynamicField.where(dynamic_field_id: dynamic_field_id).distinct.pluck(:product_id).each do |product_id|
21
- SpreeCmCommissioner::MaintenanceTasks::CacheInvalidation
22
- .pending
23
- .create_or_find_by(maintainable_type: 'Spree::Product', maintainable_id: product_id)
24
- end
25
- end
26
-
27
16
  def parent_field_must_be_selection_type
28
17
  return unless dynamic_field
29
18
 
@@ -12,9 +12,6 @@ module SpreeCmCommissioner
12
12
 
13
13
  enum segment: SpreeCmCommissioner::HomepageSectionBitwise::BIT_SEGMENT.keys
14
14
 
15
- # Create maintaining task to purge homepage background related caches
16
- after_save { SpreeCmCommissioner::MaintenanceTasks::CacheInvalidation.pending.create_or_find_by(maintainable: self) }
17
-
18
15
  def toggle_status!
19
16
  self.active = !active?
20
17
 
@@ -61,9 +61,6 @@ module SpreeCmCommissioner
61
61
  curated.exists? ? curated : everything
62
62
  }
63
63
 
64
- # Create maintaining task to purge homepage section related caches
65
- after_save { SpreeCmCommissioner::MaintenanceTasks::CacheInvalidation.pending.create_or_find_by(maintainable: self) }
66
-
67
64
  private
68
65
 
69
66
  # `.target` (not `.loaded?`) so this only fires for a background actually built/loaded in