spree_cm_commissioner 2.10.2.pre.pre1 → 2.12.0.pre.pre1

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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/concerns/spree_cm_commissioner/content_cachable.rb +1 -1
  4. data/app/controllers/spree/admin/system/waiting_room_sessions_controller.rb +27 -0
  5. data/app/controllers/spree/admin/vendor_authorized_users_controller.rb +0 -15
  6. data/app/controllers/spree/api/v2/operator/check_ins_controller.rb +22 -4
  7. data/app/controllers/spree/api/v2/operator/dashboard_crew_events_controller.rb +1 -1
  8. data/app/controllers/spree/api/v2/operator/event_charts_controller.rb +34 -12
  9. data/app/controllers/spree/api/v2/resource_controller_decorator.rb +7 -0
  10. data/app/controllers/spree/api/v2/storefront/draft_orders_controller.rb +2 -2
  11. data/app/controllers/spree/api/v2/storefront/queue_draft_orders_controller.rb +55 -0
  12. data/app/controllers/spree/api/v2/tenant/draft_orders_controller.rb +1 -1
  13. data/app/controllers/spree/api/v2/tenant/voting_contestants_controller.rb +1 -1
  14. data/app/controllers/spree/api/v2/tenant/voting_credit_transactions_controller.rb +1 -1
  15. data/app/controllers/spree_cm_commissioner/admin/variants_controller_decorator.rb +14 -4
  16. data/app/interactors/spree_cm_commissioner/payment_incomplete_notification_sender.rb +12 -1
  17. data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold_job.rb +8 -4
  18. data/app/jobs/spree_cm_commissioner/queue_draft_orders/process_job.rb +23 -0
  19. data/app/jobs/spree_cm_commissioner/voting_session_auto_open_job.rb +14 -0
  20. data/app/models/concerns/spree_cm_commissioner/event_metadata.rb +27 -0
  21. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +6 -21
  22. data/app/models/concerns/spree_cm_commissioner/purchase_limitable.rb +22 -0
  23. data/app/models/concerns/spree_cm_commissioner/ticket_transferable.rb +2 -2
  24. data/app/models/concerns/spree_cm_commissioner/vendor_preference.rb +0 -19
  25. data/app/models/concerns/spree_cm_commissioner/voting_session_timing_concern.rb +25 -7
  26. data/app/models/spree_cm_commissioner/block.rb +1 -0
  27. data/app/models/spree_cm_commissioner/inventory_item.rb +26 -10
  28. data/app/models/spree_cm_commissioner/line_item_decorator.rb +8 -1
  29. data/app/models/spree_cm_commissioner/maintenance_tasks/event.rb +20 -0
  30. data/app/models/spree_cm_commissioner/order_decorator.rb +13 -1
  31. data/app/models/spree_cm_commissioner/price_decorator.rb +16 -0
  32. data/app/models/spree_cm_commissioner/product_decorator.rb +36 -0
  33. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +38 -19
  34. data/app/models/spree_cm_commissioner/seat_layout.rb +46 -9
  35. data/app/models/spree_cm_commissioner/seat_section.rb +47 -0
  36. data/app/models/spree_cm_commissioner/stock/availability_checker.rb +15 -7
  37. data/app/models/spree_cm_commissioner/stock/availability_validator_decorator.rb +57 -5
  38. data/app/models/spree_cm_commissioner/stock/line_item_availability_checker.rb +7 -4
  39. data/app/models/spree_cm_commissioner/stock/order_availability_checker.rb +7 -1
  40. data/app/models/spree_cm_commissioner/taxon_decorator.rb +12 -0
  41. data/app/models/spree_cm_commissioner/trip.rb +1 -1
  42. data/app/models/spree_cm_commissioner/variant_decorator.rb +22 -1
  43. data/app/models/spree_cm_commissioner/vote.rb +3 -0
  44. data/app/models/spree_cm_commissioner/voting_contestant.rb +1 -0
  45. data/app/models/spree_cm_commissioner/voting_credit_transaction.rb +2 -0
  46. data/app/models/spree_cm_commissioner/voting_session.rb +15 -0
  47. data/app/models/spree_cm_commissioner/waiting_room_session.rb +20 -0
  48. data/app/overrides/spree/admin/products/_form/purchase_limits.html.erb.deface +49 -0
  49. data/app/queries/spree_cm_commissioner/dashboard_crew_event_query.rb +10 -5
  50. data/app/serializers/spree/v2/storefront/taxon_serializer_decorator.rb +2 -1
  51. data/app/serializers/spree/v2/tenant/voting_contestant_serializer.rb +1 -2
  52. data/app/serializers/spree/v2/tenant/voting_credit_transaction_serializer.rb +1 -0
  53. data/app/serializers/spree_cm_commissioner/v2/storefront/block_serializer.rb +1 -1
  54. data/app/serializers/spree_cm_commissioner/v2/storefront/seat_layout_serializer.rb +1 -1
  55. data/app/serializers/spree_cm_commissioner/v2/storefront/seat_section_serializer.rb +2 -1
  56. data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +3 -0
  57. data/app/services/spree_cm_commissioner/cart/add_guest.rb +12 -7
  58. data/app/services/spree_cm_commissioner/{draft_order → draft_orders}/create.rb +2 -2
  59. data/app/services/spree_cm_commissioner/intercity_taxi_order/create.rb +2 -4
  60. data/app/services/spree_cm_commissioner/inventory_holds/acquire.rb +27 -12
  61. data/app/services/spree_cm_commissioner/inventory_holds/convert.rb +10 -9
  62. data/app/services/spree_cm_commissioner/inventory_holds/release.rb +12 -11
  63. data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities.rb +30 -9
  64. data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold.rb +33 -8
  65. data/app/services/spree_cm_commissioner/inventory_items/move_to_locked.rb +159 -0
  66. data/app/services/spree_cm_commissioner/inventory_items/reset.rb +38 -18
  67. data/app/services/spree_cm_commissioner/line_items/validate_purchase_limits.rb +29 -7
  68. data/app/services/spree_cm_commissioner/load_test/generate_booking_ticket_data.rb +56 -20
  69. data/app/services/spree_cm_commissioner/queue_draft_orders/enqueue.rb +64 -0
  70. data/app/services/spree_cm_commissioner/queue_draft_orders/process.rb +81 -0
  71. data/app/services/spree_cm_commissioner/queue_draft_orders/publish_status.rb +39 -0
  72. data/app/services/spree_cm_commissioner/redis_stock/restock.rb +16 -5
  73. data/app/services/spree_cm_commissioner/redis_stock/unstock.rb +12 -5
  74. data/app/services/spree_cm_commissioner/seat_layouts/sanitize_svg.rb +215 -0
  75. data/app/services/spree_cm_commissioner/seat_layouts/update.rb +79 -0
  76. data/app/services/spree_cm_commissioner/ticket_transfers/accept.rb +1 -1
  77. data/app/services/spree_cm_commissioner/transit_order/create.rb +4 -10
  78. data/app/services/spree_cm_commissioner/vote_credit_deductor.rb +1 -0
  79. data/app/services/spree_cm_commissioner/voting_contestants/bulk_updater.rb +10 -4
  80. data/app/services/spree_cm_commissioner/voting_sessions/auto_open.rb +38 -0
  81. data/app/services/spree_cm_commissioner/voting_sessions/finalize.rb +17 -3
  82. data/app/services/spree_cm_commissioner/voting_sessions/publish_live_state.rb +15 -0
  83. data/app/services/spree_cm_commissioner/voting_sessions/publish_to_firestore.rb +32 -23
  84. data/app/services/spree_cm_commissioner/voting_sessions/schedule_vote_window_jobs.rb +45 -0
  85. data/app/services/spree_cm_commissioner/waiting_room/stamp_queue_positions.rb +16 -4
  86. data/app/views/spree/admin/stock_managements/index.html.erb +8 -0
  87. data/app/views/spree/admin/system/waiting_room/show.html.erb +1 -0
  88. data/app/views/spree/admin/system/waiting_room_sessions/index.html.erb +118 -0
  89. data/app/views/spree/admin/variants/fixture.html.erb +3 -3
  90. data/app/views/spree/admin/vendor_authorized_users/index.html.erb +0 -15
  91. data/config/initializers/spree_permitted_attributes.rb +10 -0
  92. data/config/load_test_flows/platform.yml +20 -0
  93. data/config/locales/en.yml +43 -14
  94. data/config/locales/km.yml +33 -7
  95. data/config/routes.rb +7 -0
  96. data/db/migrate/20260813100000_add_guide_to_cm_seat_layouts.rb +12 -0
  97. data/db/migrate/20260813100001_add_sub_seat_layout_reference_to_cm_seat_sections.rb +7 -0
  98. data/db/migrate/20260813100002_add_shape_path_to_cm_blocks.rb +13 -0
  99. data/db/migrate/20260813100003_add_metadata_to_cm_seat_sections.rb +6 -0
  100. data/db/migrate/20260814000000_add_contestant_id_to_cm_voting_credit_transactions.rb +7 -0
  101. data/db/migrate/20260817000000_add_eliminated_voting_session_to_cm_voting_contestants.rb +9 -0
  102. data/db/migrate/20260817072341_add_metadata_to_cm_seat_layouts.rb +6 -0
  103. data/db/migrate/20260819100000_add_exit_at_to_cm_waiting_room_sessions.rb +5 -0
  104. data/db/migrate/20260820000000_add_locked_quantities_to_cm_inventory_items.rb +12 -0
  105. data/lib/spree_cm_commissioner/cached_inventory_item.rb +9 -0
  106. data/lib/spree_cm_commissioner/test_helper/factories/block_factory.rb +4 -0
  107. data/lib/spree_cm_commissioner/test_helper/factories/seat_layout_factory.rb +12 -0
  108. data/lib/spree_cm_commissioner/test_helper/factories/show_factory.rb +6 -0
  109. data/lib/spree_cm_commissioner/test_helper/factories/voting_credit_transaction_factory.rb +4 -0
  110. data/lib/spree_cm_commissioner/version.rb +1 -1
  111. metadata +27 -4
  112. data/app/models/concerns/spree_cm_commissioner/order_channel_bitwise.rb +0 -70
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5552124388d0792051115573a9cc5e51044c8edcf7f8e89c6f03b43d2ebd0ab
4
- data.tar.gz: '0694645f790079a03ebad09626ca155e7495c37ea8b45f83064a3c31b2aa6f75'
3
+ metadata.gz: 1bcaa0a7cc8d39b507cd072bfd4576b28f72c83f2344b11ef2097306aa8454dd
4
+ data.tar.gz: bf8156fe0e0f2a41febcacc07188e8b0bbd9b2cd79c3ccad5a8d1394fa307db7
5
5
  SHA512:
6
- metadata.gz: 06b79003d8dee84088c17d6cd6f90b7d2b7d5eeca20b4045ca1775b467b3455b66898e72e47d33e9655a6b8b7c1b36dcef355b8b4ff5258daa066d87e8cdfb02
7
- data.tar.gz: 386411dbfdd18ce5d25ade6d55e045b1bcaea788d3b8cb49437723789ab8e1e5e7e1b77e4cbece9ee962427f8b0c5c603c1cd1cdc091400ca8cf6e860dfd39a4
6
+ metadata.gz: 19b750a2f5c27cabbc5c507a0e50424a26e295aefa94369787ff490376475f63e56993950e87a9a1b2c3be624f6ec29f2192a51a7e019bfed05e3ee4dcb54018
7
+ data.tar.gz: 4acf6f94be21c3960010398b9a431b950b2e3e7bffed455096ea0dc2ed05fca769bd53884b6ed26be86cc11db2a8c220c73711be800bccebf7fc005854454f11
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.10.2.pre.pre1)
37
+ spree_cm_commissioner (2.12.0.pre.pre1)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -14,7 +14,7 @@ module SpreeCmCommissioner
14
14
  CountriesController
15
15
  ProvincesController
16
16
  GuestCardClassesController
17
- SeatLayoutController
17
+ SeatLayoutsController
18
18
  CmsPagesController
19
19
  AdvertisementsController
20
20
  ].freeze
@@ -0,0 +1,27 @@
1
+ module Spree
2
+ module Admin
3
+ module System
4
+ class WaitingRoomSessionsController < Spree::Admin::BaseController
5
+ def index
6
+ @filter = params[:filter] if params[:filter] == 'load_test'
7
+
8
+ sessions = SpreeCmCommissioner::WaitingRoomSession.active
9
+ sessions = sessions.load_test if @filter == 'load_test'
10
+
11
+ @waiting_room_sessions = sessions.order(created_at: :desc).page(params[:page]).per(50)
12
+ end
13
+
14
+ # Kills only the sessions the admin checked (Gmail-style select + delete), instead of the
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).
17
+ def kill
18
+ ids = Array(params[:ids])
19
+ count = SpreeCmCommissioner::WaitingRoomSession.kill_by_ids!(ids)
20
+ flash[:success] = "Killed #{count} waiting-room session(s)."
21
+
22
+ redirect_back fallback_location: admin_system_waiting_room_sessions_path
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -14,8 +14,6 @@ module Spree
14
14
  end
15
15
 
16
16
  def update_telegram_chat
17
- @vendor.update(preferred_telegram_alert_channels: submitted_alert_channels_mask)
18
-
19
17
  context = ::SpreeCmCommissioner::TelegramChatsFinder.call(
20
18
  telegram_chat_name: params['vendor']['preferred_telegram_chat_name'],
21
19
  telegram_chat_type: params['vendor']['preferred_telegram_chat_type']
@@ -36,19 +34,6 @@ module Spree
36
34
  redirect_to collection_url(@vendor)
37
35
  end
38
36
 
39
- # OR the bits, never sum them. Sum is not idempotent: if the same bit arrives twice
40
- # (duplicated checkbox, double-submit, hand-crafted post) 1 + 1 = 2, which silently means
41
- # google_form instead of spree. ["0","1","1","16"].sum => 18 => [google_form, offline_sell];
42
- # the same input OR'd => 17 => [spree, offline_sell].
43
- #
44
- # Masking against the known bits also drops junk from a crafted post (999 => 7).
45
- def submitted_alert_channels_mask
46
- known = SpreeCmCommissioner::OrderChannelBitwise::CHANNEL_ALERT_BITS.values.reduce(:|)
47
-
48
- Array(params.dig(:vendor, :telegram_alert_channels))
49
- .reduce(0) { |acc, value| acc | value.to_i } & known
50
- end
51
-
52
37
  # @overrided
53
38
  def model_class
54
39
  Spree::User
@@ -10,10 +10,14 @@ module Spree
10
10
  CACHE_EXPIRES_IN = 1.minute
11
11
 
12
12
  def collection
13
- @collection ||= SpreeCmCommissioner::CheckIn
14
- .where(checkinable_type: 'Spree::Taxon', checkinable_id: params[:taxon_id])
15
- .page(params[:page])
16
- .per(params[:per_page])
13
+ @collection ||= if restricted?
14
+ SpreeCmCommissioner::CheckIn.none
15
+ else
16
+ SpreeCmCommissioner::CheckIn
17
+ .where(checkinable_type: 'Spree::Taxon', checkinable_id: params[:taxon_id])
18
+ .page(params[:page])
19
+ .per(params[:per_page])
20
+ end
17
21
  end
18
22
 
19
23
  # override
@@ -41,6 +45,20 @@ module Spree
41
45
 
42
46
  private
43
47
 
48
+ # Non-organizers may only see check-in history for an event they're assigned
49
+ # to, and only while it's ongoing — once it ends, it becomes organizer-owned
50
+ # history (admins assign themselves the organizer role explicitly if they
51
+ # need access). A missing/nonexistent taxon_id is restricted the same way.
52
+ def restricted?
53
+ return false if spree_current_user.organizer?
54
+
55
+ taxon = Spree::Taxon.find_by(id: params[:taxon_id])
56
+ return true if taxon.nil?
57
+ return true unless spree_current_user.events.exists?(id: taxon.id)
58
+
59
+ taxon.ended?
60
+ end
61
+
44
62
  def model_class
45
63
  SpreeCmCommissioner::CheckIn
46
64
  end
@@ -7,7 +7,7 @@ module Spree
7
7
 
8
8
  def collection
9
9
  SpreeCmCommissioner::DashboardCrewEventQuery.new(
10
- user_id: spree_current_user.id,
10
+ user: spree_current_user,
11
11
  section: params[:section] || 'incoming'
12
12
  ).events
13
13
  end
@@ -7,23 +7,31 @@ module Spree
7
7
  ADDITIONAL_FIELDS = %i[participation entry_type].freeze
8
8
 
9
9
  def index
10
- kyc_fields = fetch_filtered_kyc_fields
11
- types = ADDITIONAL_FIELDS + kyc_fields
12
-
13
- resources = types.map do |type|
14
- # Create unique ID by using chart_type as the identifier
15
- SpreeCmCommissioner::EventChart.new(
16
- id: type.to_s,
17
- chart_type: type,
18
- product_charts: nil
19
- )
20
- end
10
+ resources = if restricted?
11
+ []
12
+ else
13
+ kyc_fields = fetch_filtered_kyc_fields
14
+ types = ADDITIONAL_FIELDS + kyc_fields
15
+
16
+ types.map do |type|
17
+ # Create unique ID by using chart_type as the identifier
18
+ SpreeCmCommissioner::EventChart.new(
19
+ id: type.to_s,
20
+ chart_type: type,
21
+ product_charts: nil
22
+ )
23
+ end
24
+ end
21
25
 
22
26
  render json: SpreeCmCommissioner::V2::Operator::PieChartEventAggregatorSerializer.new(resources).serializable_hash
23
27
  end
24
28
 
25
29
  def resource
26
- @resource = event_chart_aggregator_queries.call
30
+ @resource = if restricted?
31
+ SpreeCmCommissioner::EventChart.new(id: params[:id], chart_type: params[:id] || 'participation', product_charts: [])
32
+ else
33
+ event_chart_aggregator_queries.call
34
+ end
27
35
  end
28
36
 
29
37
  def resource_serializer
@@ -32,6 +40,20 @@ module Spree
32
40
 
33
41
  private
34
42
 
43
+ # Non-organizers may only see chart data for an event they're assigned to,
44
+ # and only while it's ongoing — once it ends, it becomes organizer-owned
45
+ # history (admins assign themselves the organizer role explicitly if they
46
+ # need access). A missing/nonexistent taxon_id is restricted the same way.
47
+ def restricted?
48
+ return false if spree_current_user&.organizer?
49
+
50
+ taxon = Spree::Taxon.find_by(id: params[:taxon_id])
51
+ return true if taxon.nil?
52
+ return true unless spree_current_user&.events&.exists?(id: taxon.id)
53
+
54
+ taxon.ended?
55
+ end
56
+
35
57
  def event_chart_aggregator_queries
36
58
  SpreeCmCommissioner::EventChartQueries.new(
37
59
  taxon_id: params[:taxon_id],
@@ -18,6 +18,13 @@ module Spree
18
18
  request.headers['X-Cm-App-Name']
19
19
  end
20
20
 
21
+ # Set by DefaultInterceptor (cm-market-app/packages/core) on every request, across every
22
+ # app (customer, operator, terminal) that shares it — the single source for device_id,
23
+ # replacing the old per-endpoint params[:device_id] (never actually sent by any client).
24
+ def device_id
25
+ request.headers['X-Cm-Device-Id']
26
+ end
27
+
21
28
  def required_schema_validation!
22
29
  return true if required_schema.nil?
23
30
 
@@ -11,12 +11,12 @@ module Spree
11
11
  # Params:
12
12
  # - line_items: Array of { variant_id:, quantity:, options: {}, public_metadata: {}, private_metadata: {} } (required)
13
13
  def create
14
- result = SpreeCmCommissioner::DraftOrder::Create.call(
14
+ result = SpreeCmCommissioner::DraftOrders::Create.call(
15
15
  line_items: draft_order_line_items,
16
16
  user: spree_current_user,
17
17
  store: current_store,
18
18
  currency: current_currency,
19
- options: { device_id: params[:device_id], last_ip_address: request.remote_ip }
19
+ options: { device_id: device_id, last_ip_address: request.remote_ip }
20
20
  )
21
21
 
22
22
  if result.success?
@@ -0,0 +1,55 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Storefront
5
+ class QueueDraftOrdersController < ::Spree::Api::V2::ResourceController
6
+ # Creating the order (once the job runs) acquires the inventory hold, so honor the
7
+ # load-test bypass here too, same as DraftOrdersController.
8
+ include SpreeCmCommissioner::LoadTestBypassable
9
+
10
+ # Accepts a draft order request without building it: writes a queued Firestore status
11
+ # document and schedules the real build (SpreeCmCommissioner::DraftOrders::Create) on a
12
+ # background job. The app watches the returned document for the eventual outcome
13
+ # instead of waiting on this request.
14
+ #
15
+ # Endpoint: POST /api/v2/storefront/queue_draft_orders
16
+ # Params: same as POST /api/v2/storefront/draft_orders — line_items: Array of
17
+ # { variant_id:, quantity:, options: {}, public_metadata: {}, private_metadata: {} } (required)
18
+ def create
19
+ result = SpreeCmCommissioner::QueueDraftOrders::Enqueue.call(
20
+ create_type: 'generic',
21
+ creation_params: {
22
+ line_items: draft_order_line_items,
23
+ user: spree_current_user,
24
+ store: current_store,
25
+ currency: current_currency,
26
+ options: { device_id: device_id, last_ip_address: request.remote_ip }
27
+ },
28
+ locale: I18n.locale.to_s
29
+ )
30
+
31
+ if result.success?
32
+ render_serialized_payload { result.value }
33
+ else
34
+ render_error_payload(result.error)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ # .to_h.deep_symbolize_keys converts every level (including line_items and their nested
41
+ # options/metadata) from ActionController::Parameters into plain Ruby Hashes with symbol
42
+ # keys — required because this crosses an ActiveJob serialization boundary
43
+ # (QueueDraftOrders::ProcessJob), unlike DraftOrdersController's version of this method,
44
+ # which hands Parameters straight to DraftOrders::Create within the same request.
45
+ def draft_order_line_items
46
+ params.permit(line_items: [:variant_id, :quantity, { options: {}, public_metadata: {}, private_metadata: {} }])
47
+ .to_h
48
+ .deep_symbolize_keys
49
+ .fetch(:line_items, [])
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -8,7 +8,7 @@ module Spree
8
8
  # Params:
9
9
  # - line_items: Array of { variant_id:, quantity:, options: {}, public_metadata: {}, private_metadata: {} } (required)
10
10
  def create
11
- result = SpreeCmCommissioner::DraftOrder::Create.call(
11
+ result = SpreeCmCommissioner::DraftOrders::Create.call(
12
12
  line_items: draft_order_line_items,
13
13
  user: spree_current_user,
14
14
  store: current_store,
@@ -13,7 +13,7 @@ module Spree
13
13
  def collection
14
14
  @collection ||= @voting_session
15
15
  .voting_contestants
16
- .includes(:voting_session, { show_contestant: :assigned_show_person },
16
+ .includes(:voting_session, :eliminated_voting_session, { show_contestant: :assigned_show_person },
17
17
  :show_contestant_images, :show_contestant_videos,
18
18
  :advanced_to, :advanced_from
19
19
  ).order(:vote_number)
@@ -20,7 +20,7 @@ module Spree
20
20
 
21
21
  def scope
22
22
  scope = spree_current_user.voting_credit_transactions
23
- .includes(:vote_credit, :originator)
23
+ .includes(:vote_credit, :originator, contestant: :voting_session)
24
24
  .order(created_at: :desc)
25
25
 
26
26
  scope = scope.by_action(params[:actions].split(',')) if params[:actions].present?
@@ -56,21 +56,31 @@ module SpreeCmCommissioner
56
56
  option_value.name
57
57
  end
58
58
 
59
- # Builds the load-test fixture for this variant so it can be used by the k6 booking flow.
59
+ # Builds the load-test fixture for this variant so it can be used by a k6 booking flow.
60
60
  # Shown on a page (with a check for whether the variant can be booked) for the tester to
61
61
  # copy into the load_testing config. A product with a tenant_id belongs to a tenant
62
62
  # (SaaS-hosted) store rather than the platform — the two k6 workflows keep separate fixture
63
63
  # folders (load_testing/config/fixture/platform/booking_ticket_flow/ vs
64
64
  # load_testing/config/fixture/tenant/cash_on_booking_ticket_flow/ — the tenant flow is named
65
65
  # cash_on_* since it has an ABA sibling; the platform flow doesn't).
66
+ #
67
+ # Whether this variant needs seat_selection_booking_flow isn't picked here — it's read off
68
+ # the generated fixture's metadata[:select_seat] (see GenerateBookingTicketData), which
69
+ # derives it the same way add-to-cart itself does.
66
70
  def fixture
67
71
  @fixture_workflow = @variant.product.tenant_id.present? ? 'tenant' : 'platform'
68
- @fixture_flow = @fixture_workflow == 'tenant' ? 'cash_on_booking_ticket_flow' : 'booking_ticket_flow'
72
+ @fixture_env = Rails.env.development? ? 'local' : Rails.env.to_s # the load_testing config calls it "local", not "development"
73
+
69
74
  result = SpreeCmCommissioner::LoadTest::GenerateBookingTicketData.call(variant: @variant)
70
75
  @fixture = result.value[:data]
71
76
  @fixture_issues = result.value[:issues]
72
- # The load_testing config calls it "local", not "development".
73
- @fixture_env = Rails.env.development? ? 'local' : Rails.env.to_s
77
+ @fixture_flow = @fixture[:metadata][:select_seat] ? 'seat_selection_booking_flow' : fixture_flow_for(@fixture_workflow)
78
+ end
79
+
80
+ private
81
+
82
+ def fixture_flow_for(workflow)
83
+ workflow == 'tenant' ? 'cash_on_booking_ticket_flow' : 'booking_ticket_flow'
74
84
  end
75
85
  end
76
86
  end
@@ -8,12 +8,23 @@ module SpreeCmCommissioner
8
8
  SpreeCmCommissioner::OrderGeneralNotification.with(
9
9
  notificable: order,
10
10
  title: title,
11
- message: message(order)
11
+ message: message(order),
12
+ payload: payload(order)
12
13
  ).deliver_later(order.user)
13
14
  end
14
15
 
15
16
  private
16
17
 
18
+ # Overrides notification_type so the app can route this tap to the payment screen instead of
19
+ # the booking details every other OrderGeneralNotification lands on. The screen loads the cart
20
+ # by token, not by number, so the token has to ride along or the tap has nothing to open.
21
+ def payload(order)
22
+ {
23
+ notification_type: 'payment_incomplete_notification',
24
+ order_token: order.token
25
+ }
26
+ end
27
+
17
28
  def title
18
29
  I18n.t('notifications.spree_cm_commissioner.payment_incomplete_notification.title')
19
30
  end
@@ -24,13 +24,17 @@ module SpreeCmCommissioner
24
24
  "order:#{options[:order_id]}:hold:#{adjustments_fingerprint(options[:inventory_id_and_quantities])}"
25
25
  end
26
26
 
27
- # Readable, order-independent fingerprint of the adjustment payload, e.g. "1=2,2=-1".
27
+ # Readable, order-independent fingerprint of the adjustment payload, e.g. "1=2,2=-1:locked".
28
28
  # Delimiter hierarchy: ":" separates key fields, "," separates adjustments, "=" pairs id/qty.
29
+ #
30
+ # The pool is part of the identity: a mixed order can carry a public and a locked adjustment
31
+ # with the same inventory_id and quantity, and without this they fingerprint identically — so
32
+ # one of the two would be dropped as a duplicate and its counter never mirrored.
29
33
  def adjustments_fingerprint(adjustments)
30
34
  Array(adjustments)
31
- .map { |adj| adj.with_indifferent_access.values_at(:inventory_id, :quantity) }
32
- .sort_by { |inventory_id, quantity| [inventory_id.to_s, quantity.to_s] }
33
- .map { |inventory_id, quantity| "#{inventory_id}=#{quantity}" }
35
+ .map { |adj| adj.with_indifferent_access.values_at(:inventory_id, :quantity, :locked) }
36
+ .sort_by { |inventory_id, quantity, locked| [inventory_id.to_s, quantity.to_s, locked.to_s] }
37
+ .map { |inventory_id, quantity, locked| locked ? "#{inventory_id}=#{quantity}:locked" : "#{inventory_id}=#{quantity}" }
34
38
  .join(',')
35
39
  end
36
40
 
@@ -0,0 +1,23 @@
1
+ module SpreeCmCommissioner
2
+ module QueueDraftOrders
3
+ # Thin trigger for QueueDraftOrders::Process — owns only the locale wrapping (so any
4
+ # user-facing message the create call surfaces renders in the request's original locale, not
5
+ # the job worker's default), not the order-creation/Firestore-publishing logic itself.
6
+ class ProcessJob < SpreeCmCommissioner::ApplicationJob
7
+ # Own queue (not the inherited :default) — before this feature, order creation happened
8
+ # synchronously in the request cycle, i.e. at effectively top priority. Sharing :default's
9
+ # weight-3 slot with unrelated background work would let a :default backlog slow down
10
+ # checkout, the opposite of what a flash-sale/high-demand feature should do. See
11
+ # bookmebus/cm-market-server's config/sidekiq.yml for the matching queue weight.
12
+ queue_as :queue_draft_orders
13
+
14
+ def perform(document_path, create_type, creation_params, locale = nil)
15
+ I18n.with_locale(locale.presence || I18n.default_locale) do
16
+ SpreeCmCommissioner::QueueDraftOrders::Process.call(
17
+ document_path: document_path, create_type: create_type, creation_params: creation_params
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,14 @@
1
+ module SpreeCmCommissioner
2
+ # Enqueued by VotingSessions::ScheduleVoteWindowJobs with wait_until: vote_open_at.
3
+ # See VotingSessions::AutoOpen for the actual open/publish logic.
4
+ class VotingSessionAutoOpenJob < ApplicationJob
5
+ queue_as :default
6
+
7
+ def perform(voting_session_id)
8
+ voting_session = SpreeCmCommissioner::VotingSession.find_by(id: voting_session_id)
9
+ return if voting_session.nil?
10
+
11
+ SpreeCmCommissioner::VotingSessions::AutoOpen.call(voting_session: voting_session)
12
+ end
13
+ end
14
+ end
@@ -41,6 +41,21 @@ module SpreeCmCommissioner
41
41
  store_public_metadata :preload_seat_layout_ids, :array, default: []
42
42
  store_public_metadata :preload_active_seat_layout_ids, :array, default: []
43
43
 
44
+ # Use to indicate if a seat layout is the guide layout for this event. 1 guide layout per event.
45
+ store_public_metadata :preload_guide_seat_layout_id, :integer
46
+ store_public_metadata :preload_active_guide_seat_layout_id, :integer
47
+
48
+ # Denormalised price range across the event's variants, so the client can show it (e.g. "from $X")
49
+ # without loading every variant. Kept in sync by PriceDecorator's after_commit hook
50
+ store_public_metadata :min_price, :string
51
+ store_public_metadata :max_price, :string
52
+ store_public_metadata :currency, :string
53
+
54
+ store_public_metadata :organizer_rule, :text
55
+
56
+ store_public_metadata :spotify_embed_url, :string
57
+ store_public_metadata :spotify_embed_skeleton_color, :string, default: '#900033'
58
+
44
59
  before_validation :validate_at_least_one_check_in_flow_presence
45
60
  end
46
61
 
@@ -51,6 +66,18 @@ module SpreeCmCommissioner
51
66
  flows
52
67
  end
53
68
 
69
+ def display_min_price
70
+ return nil if min_price.blank?
71
+
72
+ Spree::Money.new(min_price, currency: currency).to_s
73
+ end
74
+
75
+ def display_max_price
76
+ return nil if max_price.blank?
77
+
78
+ Spree::Money.new(max_price, currency: currency).to_s
79
+ end
80
+
54
81
  private
55
82
 
56
83
  def validate_at_least_one_check_in_flow_presence
@@ -1,7 +1,7 @@
1
1
  # Make sure to put this concern below other concern or methods that generating additional order info like guests, seat number, etc.
2
2
  # This will ensure that when each notification is sent with neccessary data.
3
3
  module SpreeCmCommissioner
4
- module OrderStateMachine # rubocop:disable Metrics/ModuleLength
4
+ module OrderStateMachine
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do # rubocop:disable Metrics/BlockLength
@@ -271,30 +271,15 @@ module SpreeCmCommissioner
271
271
  return unless telegram_alert_enabled?
272
272
 
273
273
  vendor_list.each do |vendor|
274
- # `next`, not `return`: the mask is per-vendor, so one opted-out vendor on a
275
- # multi-vendor order must not suppress the others.
276
- next unless vendor.alerts_telegram_for_channel?(channel_key)
277
-
274
+ title = '🎫 --- [NEW ORDER FROM BOOKME+] ---'
278
275
  chat_id = vendor.preferred_telegram_chat_id
279
- next if chat_id.blank?
280
-
281
- factory = OrderTelegramMessageFactory.new(title: vendor_alert_title(vendor), order: self, vendor: vendor)
282
- TelegramNotificationSenderJob.perform_later(chat_id: chat_id, message: factory.message, parse_mode: factory.parse_mode)
276
+ if chat_id.present?
277
+ factory = OrderTelegramMessageFactory.new(title: title, order: self, vendor: vendor)
278
+ TelegramNotificationSenderJob.perform_later(chat_id: chat_id, message: factory.message, parse_mode: factory.parse_mode)
279
+ end
283
280
  end
284
281
  end
285
282
 
286
- # White-label vendors announce their own brand; platform vendors stay BookMe+.
287
- #
288
- # vendor.name is operator-supplied data interpolated into an HTML parse_mode message
289
- # (TelegramMessageFactory#header wraps the title in <b>), so it MUST be escaped —
290
- # Telegram rejects unescaped &, < and > with "400 can't parse entities", and
291
- # TelegramNotificationSender swallows that error, so the alert would vanish silently.
292
- def vendor_alert_title(vendor)
293
- return '🎫 --- [NEW ORDER FROM BOOKME+] ---' unless vendor.tenant_present?
294
-
295
- "🎫 --- [NEW ORDER FROM #{CGI.escapeHTML(vendor.name.to_s)}] ---"
296
- end
297
-
298
283
  def notify_order_complete_app_notification_to_user
299
284
  return if products.all?(&:vote_package?) # disable app push notification for vote package orders
300
285
 
@@ -12,6 +12,14 @@ module SpreeCmCommissioner
12
12
  max_quantity_per_ip
13
13
  ].freeze
14
14
 
15
+ # Caps that aggregate across orders. The order cap is bounded by each of these:
16
+ # whichever is smaller is what the buyer actually gets.
17
+ AGGREGATE_LIMIT_KEYS = %i[
18
+ max_quantity_per_account
19
+ max_quantity_per_device
20
+ max_quantity_per_ip
21
+ ].freeze
22
+
15
23
  POSITIVE_INTEGER = { only_integer: true, greater_than: 0, message: :must_be_positive_integer }.freeze
16
24
 
17
25
  included do
@@ -27,6 +35,7 @@ module SpreeCmCommissioner
27
35
 
28
36
  validate :validate_max_quantity_per_account
29
37
  validate :validate_max_quantity_per_device
38
+ validate :validate_order_cap_within_aggregate_caps
30
39
 
31
40
  before_validation :normalize_purchase_limits
32
41
  end
@@ -50,6 +59,19 @@ module SpreeCmCommissioner
50
59
  errors.add(:base, I18n.t('purchase_limit.account_cap_requires_named_booking'))
51
60
  end
52
61
 
62
+ # An order cap above an aggregate cap is unreachable - the aggregate check counts the same
63
+ # cart quantity plus past orders, so it rejects first. Only the tightest cap is named:
64
+ # fixing it clears the rest. A blank cap reads back as 0, hence the positive? filter.
65
+ def validate_order_cap_within_aggregate_caps
66
+ order_cap = max_quantity_per_order.to_i
67
+ key = AGGREGATE_LIMIT_KEYS.select { |k| public_send(k).to_i.positive? && public_send(k).to_i < order_cap }
68
+ .min_by { |k| public_send(k).to_i }
69
+ return if key.nil?
70
+
71
+ cap_name = I18n.t("purchase_limit.cap_names.#{key.to_s.delete_prefix('max_quantity_per_')}")
72
+ errors.add(:max_quantity_per_order, I18n.t('purchase_limit.order_cap_exceeds_scope_cap', cap_name: cap_name))
73
+ end
74
+
53
75
  def validate_max_quantity_per_device
54
76
  return unless max_quantity_per_device.to_i.positive? && !purchasable_on_app?
55
77
 
@@ -5,11 +5,11 @@ module SpreeCmCommissioner
5
5
  included do
6
6
  has_one :ticket_transfer, class_name: 'SpreeCmCommissioner::TicketTransfer'
7
7
 
8
- scope :not_ticket_transfer, -> { where.not(channel: SpreeCmCommissioner::OrderChannelBitwise::TICKET_TRANSFER_CHANNEL) }
8
+ scope :not_ticket_transfer, -> { where.not(channel: SpreeCmCommissioner::OrderDecorator::TICKET_TRANSFER_CHANNEL) }
9
9
  end
10
10
 
11
11
  def ticket_transfer?
12
- channel == SpreeCmCommissioner::OrderChannelBitwise::TICKET_TRANSFER_CHANNEL
12
+ channel == SpreeCmCommissioner::OrderDecorator::TICKET_TRANSFER_CHANNEL
13
13
  end
14
14
 
15
15
  # Completes an accepted ticket transfer for this order. Called from
@@ -8,25 +8,6 @@ module SpreeCmCommissioner
8
8
  preference :telegram_chat_id, :string
9
9
  preference :telegram_chat_name, :string
10
10
  preference :telegram_chat_type, :string
11
- preference :telegram_alert_channels, :integer, default: 1
12
- end
13
-
14
- # Should this vendor's Telegram group be alerted for an order on this channel?
15
- # An unmapped channel alerts — a stray message is a better failure than silence.
16
- def alerts_telegram_for_channel?(channel_key)
17
- bit = SpreeCmCommissioner::OrderChannelBitwise::CHANNEL_ALERT_BITS[channel_key]
18
- return true if bit.nil?
19
-
20
- telegram_alert_channels_mask.anybits?(bit)
21
- end
22
-
23
- # Spree's getter is `preferences.fetch(name) { default }`, which falls back only when the
24
- # key is ABSENT. A key present with a nil value returns nil, and nil.to_i == 0 would mute
25
- # EVERY channel silently. Fall back to the default instead; a deliberate 0 survives
26
- # because 0 is truthy in Ruby.
27
- def telegram_alert_channels_mask
28
- (preferred_telegram_alert_channels ||
29
- SpreeCmCommissioner::OrderChannelBitwise::DEFAULT_TELEGRAM_ALERT_CHANNELS).to_i
30
11
  end
31
12
  end
32
13
  end