spree_cm_commissioner 2.8.15.pre.pre2 → 2.8.15.pre.pre3

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 (114) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +0 -13
  3. data/.github/workflows/test_and_build_gem.yml +1 -2
  4. data/Gemfile.lock +1 -1
  5. data/app/controllers/spree/admin/import_new_orders_controller.rb +0 -25
  6. data/app/controllers/spree/admin/inventory_holds_controller.rb +1 -21
  7. data/app/controllers/spree/admin/system/waiting_room_controller.rb +0 -15
  8. data/app/controllers/spree/admin/taxons_controller_decorator.rb +0 -5
  9. data/app/controllers/spree/api/v2/storefront/account/orders_controller_decorator.rb +0 -5
  10. data/app/controllers/spree/api/v2/storefront/guests_controller.rb +5 -7
  11. data/app/controllers/spree/api/v2/storefront/order_histories_controller.rb +2 -14
  12. data/app/controllers/spree/api/v2/storefront/ticket_transfers_controller.rb +3 -4
  13. data/app/controllers/spree/api/v2/tenant/order_histories_controller.rb +2 -14
  14. data/app/controllers/spree/api/v2/tenant/vote_packages_controller.rb +81 -0
  15. data/app/finders/spree_cm_commissioner/orders/find.rb +3 -33
  16. data/app/interactors/spree_cm_commissioner/waiting_guests_caller.rb +20 -146
  17. data/app/interactors/spree_cm_commissioner/waiting_room_latest_system_metadata_puller.rb +3 -5
  18. data/app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb +6 -25
  19. data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_job.rb +3 -21
  20. data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold_job.rb +3 -34
  21. data/app/jobs/spree_cm_commissioner/waiting_guests_caller_job.rb +1 -6
  22. data/app/jobs/spree_cm_commissioner/waiting_room_latest_system_metadata_puller_job.rb +1 -1
  23. data/app/mailers/spree/order_mailer_decorator.rb +0 -2
  24. data/app/models/concerns/spree_cm_commissioner/line_item_guests_concern.rb +2 -16
  25. data/app/models/concerns/spree_cm_commissioner/order_holdable.rb +0 -6
  26. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +1 -33
  27. data/app/models/concerns/spree_cm_commissioner/product_delegation.rb +0 -2
  28. data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +3 -0
  29. data/app/models/concerns/spree_cm_commissioner/store_preference.rb +0 -1
  30. data/app/models/spree_cm_commissioner/claim.rb +27 -0
  31. data/app/models/spree_cm_commissioner/import.rb +1 -3
  32. data/app/models/spree_cm_commissioner/imports/import_order.rb +0 -21
  33. data/app/models/spree_cm_commissioner/inventory_hold.rb +1 -1
  34. data/app/models/spree_cm_commissioner/line_item_decorator.rb +26 -0
  35. data/app/models/spree_cm_commissioner/notification.rb +0 -1
  36. data/app/models/spree_cm_commissioner/order_decorator.rb +0 -14
  37. data/app/models/spree_cm_commissioner/payment_decorator.rb +0 -11
  38. data/app/models/spree_cm_commissioner/product_decorator.rb +0 -1
  39. data/app/models/spree_cm_commissioner/show.rb +9 -1
  40. data/app/models/spree_cm_commissioner/show_episode.rb +14 -0
  41. data/app/models/spree_cm_commissioner/taxon_decorator.rb +0 -1
  42. data/app/models/spree_cm_commissioner/vendor_decorator.rb +1 -0
  43. data/app/models/spree_cm_commissioner/voting_credit.rb +2 -0
  44. data/app/models/spree_cm_commissioner/voting_session.rb +1 -0
  45. data/app/overrides/spree/admin/products/_form/allow_gift_transfer.html.erb.deface +1 -1
  46. data/app/overrides/spree/admin/products/_form/allow_transfer.html.erb.deface +1 -1
  47. data/app/overrides/spree/admin/stores/_form/store_preferences.html.erb.deface +0 -9
  48. data/app/overrides/spree/admin/taxons/_form/assets_form.html.erb.deface +1 -10
  49. data/app/serializers/spree/v2/storefront/taxon_serializer_decorator.rb +0 -2
  50. data/app/serializers/spree/v2/tenant/show_serializer.rb +1 -1
  51. data/app/serializers/spree/v2/tenant/vote_package_serializer.rb +7 -1
  52. data/app/services/spree_cm_commissioner/cart/add_guest.rb +3 -30
  53. data/app/services/spree_cm_commissioner/cart/remove_guest.rb +1 -26
  54. data/app/services/spree_cm_commissioner/imports/orders/create.rb +0 -1
  55. data/app/services/spree_cm_commissioner/inventory_holds/release.rb +0 -18
  56. data/app/services/spree_cm_commissioner/vote_package/create.rb +9 -4
  57. data/app/services/spree_cm_commissioner/vote_package/update.rb +11 -5
  58. data/app/services/spree_cm_commissioner/voting_contestants/bulk_updater.rb +6 -3
  59. data/app/services/spree_cm_commissioner/voting_credits/admin_grant.rb +37 -0
  60. data/app/services/spree_cm_commissioner/voting_credits/allocate.rb +24 -2
  61. data/app/services/spree_cm_commissioner/voting_credits/claimable_scope_resolver.rb +64 -0
  62. data/app/services/spree_cm_commissioner/waiting_room_system_metadata_fetcher.rb +7 -9
  63. data/app/services/spree_cm_commissioner/waiting_room_system_metadata_setter.rb +6 -16
  64. data/app/views/spree/admin/import_new_orders/index.html.erb +0 -4
  65. data/app/views/spree/admin/import_new_orders/show.html.erb +0 -30
  66. data/app/views/spree/admin/imports/index.html.erb +0 -2
  67. data/app/views/spree/admin/inventory_holds/index.html.erb +3 -8
  68. data/app/views/spree/admin/system/waiting_room/show.html.erb +0 -53
  69. data/app/views/spree_cm_commissioner/order_mailer/purchased_items/_items.html.erb +9 -12
  70. data/config/initializers/spree_permitted_attributes.rb +0 -4
  71. data/config/locales/en.yml +5 -16
  72. data/config/locales/km.yml +4 -12
  73. data/config/routes.rb +3 -4
  74. data/db/migrate/20260623082233_create_cm_claims.rb +14 -0
  75. data/db/migrate/20260624175658_add_unique_index_to_cm_claims.rb +11 -0
  76. data/lib/spree_cm_commissioner/test_helper/factories/claim_factory.rb +14 -0
  77. data/lib/spree_cm_commissioner/version.rb +1 -1
  78. metadata +9 -38
  79. data/app/controllers/spree/api/v2/tenant/free_vote_claims_controller.rb +0 -37
  80. data/app/finders/spree_cm_commissioner/orders/find_by_all_state.rb +0 -11
  81. data/app/interactors/spree_cm_commissioner/payment_incomplete_notification_sender.rb +0 -25
  82. data/app/interactors/spree_cm_commissioner/release_inventory_item_notification_sender.rb +0 -24
  83. data/app/jobs/concerns/spree_cm_commissioner/idempotent_job.rb +0 -12
  84. data/app/jobs/spree_cm_commissioner/cancel_import_order_job.rb +0 -11
  85. data/app/jobs/spree_cm_commissioner/idempotency_keys/prune_job.rb +0 -20
  86. data/app/jobs/spree_cm_commissioner/payment_incomplete_notification_job.rb +0 -12
  87. data/app/jobs/spree_cm_commissioner/telegram_alerts/order_integrity_check_job.rb +0 -17
  88. data/app/jobs/spree_cm_commissioner/waiting_room/publish_lobby_path_job.rb +0 -17
  89. data/app/jobs/spree_cm_commissioner/waiting_room/stamp_queue_positions_job.rb +0 -14
  90. data/app/models/spree_cm_commissioner/idempotency_key.rb +0 -21
  91. data/app/models/spree_cm_commissioner/imported_order.rb +0 -6
  92. data/app/models/spree_cm_commissioner/taxon_ad_banner.rb +0 -12
  93. data/app/notifications/spree_cm_commissioner/order_general_notification.rb +0 -15
  94. data/app/overrides/spree/admin/orders/_search/payment_number_search_field.html.erb.deface +0 -8
  95. data/app/overrides/spree/admin/products/_form/enable_telegram_alert.html.erb.deface +0 -14
  96. data/app/services/spree_cm_commissioner/cart/add_item_decorator.rb +0 -22
  97. data/app/services/spree_cm_commissioner/cart/create_guest.rb +0 -80
  98. data/app/services/spree_cm_commissioner/cart/empty_decorator.rb +0 -23
  99. data/app/services/spree_cm_commissioner/cart/remove_item_decorator.rb +0 -17
  100. data/app/services/spree_cm_commissioner/cart/remove_line_item_decorator.rb +0 -17
  101. data/app/services/spree_cm_commissioner/cart/set_quantity_decorator.rb +0 -17
  102. data/app/services/spree_cm_commissioner/imports/orders/cancel.rb +0 -63
  103. data/app/services/spree_cm_commissioner/telegram_alerts/checks/order_complete_payment_not_paid.rb +0 -21
  104. data/app/services/spree_cm_commissioner/telegram_alerts/checks/payment_paid_order_not_complete.rb +0 -21
  105. data/app/services/spree_cm_commissioner/telegram_alerts/order_integrity_alert.rb +0 -112
  106. data/app/services/spree_cm_commissioner/telegram_alerts/order_integrity_checks_runner.rb +0 -32
  107. data/app/services/spree_cm_commissioner/voting_credits/claim_free_votes.rb +0 -119
  108. data/app/services/spree_cm_commissioner/waiting_room/publish_lobby_path.rb +0 -57
  109. data/app/services/spree_cm_commissioner/waiting_room/stamp_queue_positions.rb +0 -185
  110. data/app/services/spree_cm_commissioner/waiting_room_lobby_metadata_fetcher.rb +0 -47
  111. data/app/views/spree/admin/import_new_orders/_cancel_modal.html.erb +0 -35
  112. data/db/migrate/20260616000001_add_canceled_by_id_to_cm_imports.rb +0 -5
  113. data/db/migrate/20260617000002_create_cm_imported_orders.rb +0 -12
  114. data/db/migrate/20260619000001_create_cm_idempotency_keys.rb +0 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 93c5573306fbc0f28002f0bd03260743213101bdf498bd31f6f1399562a9aa76
4
- data.tar.gz: 47649ceb71b08fa483ff5f045bd1bd05d9d5246ed0346d2cd6cc5ebe78b0526a
3
+ metadata.gz: 3ac1f1c30c374b8f4ad3dd06b486bbe1e25cbfa1f1c26bbd7a3d53895aaefaa2
4
+ data.tar.gz: affc12ef47495c5c51530c1fae89c771380b3759825932ea263ec5474abbb289
5
5
  SHA512:
6
- metadata.gz: 6942ddfbb5af4869b8ae506652edcbe8100f16c9430e51224b4f58d2f4fe68f783587a16bc10a89d5a397d8ae294c8534e59ffbfa6e63546dfd5fe0ea5b98033
7
- data.tar.gz: e214d676b68d2ad73c8efb0a160f6fca2f86d0c33b32259adf1a7fc7a965d8ba6c2d74b2a8fa884f9b0d45ccc47aec7858c7c897646252d8439046616e70a3ae
6
+ metadata.gz: f14fbec2aadcfdf2ecb3e463fe26321d581f40ade6010ace88f1e2eb4dc9f8c3f80d329f8d3a8664133dd223376f17feca17a7413c3f9d515bc1c1f8eaa884d4
7
+ data.tar.gz: 53239fc959124a3cc3d6fe74804bbee08b576161b3914f45dab14b1e9be03c84000cfe716ce012c1c86f7903a3aa72d5127bc2979ff5f3a5a67c8a8a1a643407
data/.env.example CHANGED
@@ -20,13 +20,6 @@ WAITING_ROOM_SESSION_SIGNATURE="e6b2********************26e3"
20
20
  WAITING_ROOM_SESSION_EXPIRE_DURATION_IN_SECOND=
21
21
  WAITING_ROOM_MIN_SESSIONS_COUNT=5
22
22
  WAITING_ROOM_DISABLED=no
23
- WAITING_ROOM_POSITION_STAMP_LIMIT=1000
24
- WAITING_ROOM_FIRESTORE_BATCH_SIZE=500
25
- WAITING_ROOM_MIN_WAIT_TO_ENTER_SECONDS=600 # Waiting Room step floor (10 min)
26
- WAITING_ROOM_MIN_QUEUE_TO_ENTER_SECONDS=300 # Queue step floor (5 min)
27
- WAITING_ROOM_MAX_WAIT_TO_DISPLAY_SECONDS=3600 # above this the ETA is hidden (1 h)
28
- WAITING_ROOM_CALLER_INTERVAL_SECONDS=60 # how often the caller runs; match the waiting_guests_caller cron
29
- WAITING_ROOM_MAX_ETA_BATCH_SIZE=50 # cap on batch size assumed when estimating the ETA
30
23
 
31
24
  # Vattanac Bank
32
25
  VATTANAC_AES_SECRET_KEY= ""
@@ -70,9 +63,3 @@ PAYMENT_LOCK_MIN_DURATION_IN_MINUTES=5 # Minimum remaining hold time requir
70
63
  MAX_ACTIVE_HOLDS_PER_USER=3 # Max concurrent active holds allowed per user across all orders
71
64
  MAX_HOLDS_PER_IP_PER_HOUR=5 # Max hold attempts per IP address within a rolling 1-hour window (abuse prevention)
72
65
  HOLD_COOLDOWN_AFTER_EXPIRY_IN_MINUTES=2 # Cooldown period before a user can re-acquire a hold after one expired on them
73
-
74
- # See: app/models/spree_cm_commissioner/payment_decorator.rb
75
- PAYMENT_INCOMPLETE_NOTIFICATION_DELAY_IN_MINUTES=5 # Delay before notifying a customer that their order is still unpaid after payment is created
76
-
77
- # See: app/jobs/spree_cm_commissioner/telegram_alerts/order_integrity_check_job.rb
78
- ORDER_INTEGRITY_TELEGRAM_ALERT_ENABLED="no" # Set to yes to enable the order integrity Telegram alert job (off by default to avoid flooding Telegram rate limits during traffic spikes)
@@ -163,9 +163,8 @@ jobs:
163
163
  - name: Security
164
164
  env:
165
165
  DATABASE_URL: postgres://myuser:mypassword@localhost:5432/test_db
166
-
167
- # Using --no-threads to bypass a known Ruby 3.2.0 GC segmentation fault in CI.
168
166
  run: |
167
+ # Using --no-threads to bypass a known Ruby 3.2.0 GC segmentation fault in CI.
169
168
  bundle exec brakeman --no-threads --no-exit-on-warn
170
169
 
171
170
  test:
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.8.15.pre.pre2)
37
+ spree_cm_commissioner (2.8.15.pre.pre3)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -23,31 +23,6 @@ module Spree
23
23
  end
24
24
  end
25
25
  end
26
-
27
- # POST: /admin/orders/import_new_orders/:id/cancel
28
- def cancel
29
- import = model_class.find(params[:id])
30
- show_url = spree.admin_import_new_order_url(import.id)
31
-
32
- unless import.cancellable?
33
- flash[:error] = 'These orders cannot be canceled.' # rubocop:disable Rails/I18nLocaleTexts
34
- redirect_to show_url and return
35
- end
36
-
37
- if params[:confirm_name].to_s.strip != import.name.to_s.strip
38
- flash[:error] = 'Import name did not match. Cancellation aborted.' # rubocop:disable Rails/I18nLocaleTexts
39
- redirect_to show_url and return
40
- end
41
-
42
- SpreeCmCommissioner::CancelImportOrderJob.perform_later(
43
- import_order_id: import.id,
44
- canceled_by_user_id: spree_current_user.id,
45
- cancellation_reason: params[:cancellation_reason].to_s.strip.presence
46
- )
47
-
48
- flash[:success] = "Canceling orders from \"#{import.name}\". This runs in the background."
49
- redirect_to show_url
50
- end
51
26
  end
52
27
  end
53
28
  end
@@ -9,7 +9,6 @@ module Spree
9
9
  q['s'] ||= 'created_at desc'
10
10
  q['created_at_gt'] = parse_time_or_nil(q['created_at_gt'], :beginning_of_day) if q['created_at_gt'].present?
11
11
  q['created_at_lt'] = parse_time_or_nil(q['created_at_lt'], :end_of_day) if q['created_at_lt'].present?
12
- normalize_enum_filters!(q)
13
12
 
14
13
  @search = scope.ransack(q)
15
14
  result_scope = @search.result(distinct: true)
@@ -19,11 +18,7 @@ module Spree
19
18
  .page(params[:page])
20
19
  .per(params[:per_page] || Spree::Backend::Config[:admin_orders_per_page])
21
20
 
22
- # Status tab counts must reflect every other filter but NOT the status filter, so
23
- # each tab shows how many holds it would return (otherwise selecting one status
24
- # zeroes out all the other tabs).
25
- @status_counts = scope.ransack(q.except('status_eq')).result(distinct: true)
26
- .reorder(nil).group(:status).count
21
+ @status_counts = result_scope.reorder(nil).group(:status).count
27
22
  end
28
23
 
29
24
  def release
@@ -49,23 +44,8 @@ module Spree
49
44
 
50
45
  private
51
46
 
52
- # `status` and `release_reason` are integer-backed enums. Ransack casts a string
53
- # value for an integer column with String#to_i, so "payment_locked" becomes 0
54
- # (== :pending) and every label silently filters to the wrong status. Translate
55
- # enum labels to their stored integer before searching.
56
- def normalize_enum_filters!(query)
57
- {
58
- 'status_eq' => SpreeCmCommissioner::InventoryHold.statuses,
59
- 'release_reason_eq' => SpreeCmCommissioner::InventoryHold.release_reasons
60
- }.each do |key, mapping|
61
- value = query[key]
62
- query[key] = mapping[value.to_s] if value.present? && mapping.key?(value.to_s)
63
- end
64
- end
65
-
66
47
  def back_params
67
48
  raw_params = params[:back_params].presence || params
68
- raw_params = raw_params.to_unsafe_h if raw_params.respond_to?(:to_unsafe_h)
69
49
  ActionController::Parameters.new(raw_params).permit(:page, :per_page, q: {})
70
50
  end
71
51
 
@@ -4,10 +4,7 @@ module Spree
4
4
  class WaitingRoomController < Spree::Admin::BaseController
5
5
  def show
6
6
  @fetcher = SpreeCmCommissioner::WaitingRoomSystemMetadataFetcher.new
7
- @lobby_fetcher = SpreeCmCommissioner::WaitingRoomLobbyMetadataFetcher.new
8
-
9
7
  @fetcher.load_document_data
10
- @lobby_fetcher.load_document_data
11
8
 
12
9
  @active_sesions_count = SpreeCmCommissioner::WaitingRoomSession.active.count
13
10
  end
@@ -32,18 +29,6 @@ module Spree
32
29
 
33
30
  redirect_back fallback_location: admin_system_waiting_room_path
34
31
  end
35
-
36
- def publish_lobby_path
37
- result = SpreeCmCommissioner::WaitingRoom::PublishLobbyPath.call
38
-
39
- if result.success?
40
- flash[:success] = "Published waiting guests records path: #{result.value[:records_path]}"
41
- else
42
- flash[:error] = result.error.to_s
43
- end
44
-
45
- redirect_back fallback_location: admin_system_waiting_room_path
46
- end
47
32
  end
48
33
  end
49
34
  end
@@ -28,10 +28,6 @@ module Spree
28
28
  remove_asset(@taxon.home_banner)
29
29
  end
30
30
 
31
- def remove_ad_banner
32
- remove_asset(@taxon.ad_banner)
33
- end
34
-
35
31
  def remove_video_banner
36
32
  remove_asset(@taxon.video_banner)
37
33
  end
@@ -53,7 +49,6 @@ module Spree
53
49
  @taxon.build_app_banner(attachment: permitted_resource_params.delete(:app_banner)) if permitted_resource_params[:app_banner]
54
50
  @taxon.build_web_banner(attachment: permitted_resource_params.delete(:web_banner)) if permitted_resource_params[:web_banner]
55
51
  @taxon.build_home_banner(attachment: permitted_resource_params.delete(:home_banner)) if permitted_resource_params[:home_banner]
56
- @taxon.build_ad_banner(attachment: permitted_resource_params.delete(:ad_banner)) if permitted_resource_params[:ad_banner]
57
52
  @taxon.build_video_banner(attachment: permitted_resource_params.delete(:video_banner)) if permitted_resource_params[:video_banner]
58
53
  end
59
54
 
@@ -44,11 +44,6 @@ module Spree
44
44
  def collection_finder
45
45
  SpreeCmCommissioner::Orders::FindByState
46
46
  end
47
-
48
- # this allows fetching a single order regardless of its state.
49
- def resource_finder
50
- SpreeCmCommissioner::Orders::FindByAllState
51
- end
52
47
  end
53
48
  end
54
49
  end
@@ -31,15 +31,13 @@ module Spree
31
31
  def create
32
32
  spree_authorize! :update, spree_current_order, order_token
33
33
 
34
- result = SpreeCmCommissioner::Cart::CreateGuest.call(
35
- line_item: parent,
36
- options: guest_params
37
- )
34
+ resource = scope.new(guest_params)
38
35
 
39
- if result.success?
40
- render_serialized_payload(201) { serialize_resource(result.value) }
36
+ if resource.save
37
+ resource.save_and_move_to_next_stage
38
+ render_serialized_payload(201) { serialize_resource(resource) }
41
39
  else
42
- render_error_payload(result.value.errors, 422)
40
+ render_error_payload(resource.errors, 422)
43
41
  end
44
42
  end
45
43
 
@@ -30,25 +30,13 @@ module Spree
30
30
  private
31
31
 
32
32
  def collection
33
- # WARNING: Do NOT remove .order(id: :desc).
34
- # Mobile relies on this ordering for paginated order_histories.
35
- # Removing it will cause mobile to paginate by oldest records first (last 10 oldest),
36
- # breaking the "most recent orders" display on the order history screen.
37
- # It related to user cancel or complete order flow, which relies on the most recent orders being returned first.
38
33
  if spree_current_user.present?
39
- spree_current_user.orders
40
- .not_archived
41
- .incomplete
42
- .order(id: :desc)
34
+ spree_current_user.orders.payment.not_archived
43
35
  else
44
36
  order_tokens = Array(params[:order_tokens])
45
37
  return Spree::Order.none if order_tokens.empty?
46
38
 
47
- Spree::Order.not_archived
48
- .incomplete
49
- .without_user
50
- .where(token: order_tokens)
51
- .order(id: :desc)
39
+ Spree::Order.payment.not_archived.without_user.where(token: order_tokens)
52
40
  end
53
41
  end
54
42
 
@@ -86,10 +86,9 @@ module Spree
86
86
  end
87
87
 
88
88
  def collection
89
- SpreeCmCommissioner::TicketTransfer
90
- .includes(:order, from_guest: [:event, { line_item: :product }])
91
- .where(from_user_id: spree_current_user.id)
92
- .order(id: :desc)
89
+ SpreeCmCommissioner::TicketTransfer.includes(:order,
90
+ from_guest: [:event, { line_item: :product }]
91
+ ).where(from_user_id: spree_current_user.id)
93
92
  end
94
93
 
95
94
  def resource_serializer
@@ -37,25 +37,13 @@ module Spree
37
37
  end
38
38
 
39
39
  def collection
40
- # WARNING: Do NOT remove .order(id: :desc).
41
- # Mobile relies on this ordering for paginated order_histories.
42
- # Removing it will cause mobile to paginate by oldest records first (last 10 oldest),
43
- # breaking the "most recent orders" display on the order history screen.
44
- # It related to user cancel or complete order flow, which relies on the most recent orders being returned first.
45
40
  if spree_current_user.present?
46
- spree_current_user.orders
47
- .not_archived
48
- .incomplete
49
- .order(id: :desc)
41
+ spree_current_user.orders.payment.not_archived
50
42
  else
51
43
  order_tokens = Array(params[:order_tokens])
52
44
  return Spree::Order.none if order_tokens.empty?
53
45
 
54
- Spree::Order.not_archived
55
- .incomplete
56
- .without_user
57
- .where(token: order_tokens)
58
- .order(id: :desc)
46
+ Spree::Order.payment.not_archived.without_user.where(token: order_tokens)
59
47
  end
60
48
  end
61
49
 
@@ -0,0 +1,81 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Tenant
5
+ class VotePackagesController < BaseController
6
+ before_action :require_spree_current_user, only: :claim_status
7
+ before_action :require_votable_params, only: :index
8
+
9
+ # Overrided
10
+ def index
11
+ render_serialized_payload { serialize_collection(sorted_collection) }
12
+ end
13
+
14
+ def claim_status
15
+ product = current_season.vote_packages.find(params[:id])
16
+
17
+ result = SpreeCmCommissioner::VotingCredits::ClaimableScopeResolver.new(
18
+ product: product,
19
+ voting_session_id: params[:voting_session_id]
20
+ ).call
21
+
22
+ return render_error_payload(result.error, 422) if result.failure?
23
+
24
+ scope = result.value
25
+
26
+ claimed = SpreeCmCommissioner::Claim.free_claimed?(
27
+ user_id: spree_current_user.id,
28
+ product_id: product.id,
29
+ claimable_id: scope[:claimable_id],
30
+ claimable_type: scope[:claimable_type]
31
+ )
32
+
33
+ render_serialized_payload { { claimed: claimed }.merge(scope) }
34
+ end
35
+
36
+ private
37
+
38
+ # Overrided
39
+ def collection
40
+ current_vendor.vote_packages
41
+ .where(status: :active, event_id: current_season.id)
42
+ .by_votable(params[:votable_type], params[:votable_id])
43
+ end
44
+
45
+ # Overrided
46
+ def sorted_collection
47
+ packages = current_season.allow_free_vote? ? collection.to_a : collection.reject(&:free?)
48
+ packages.sort_by { |p| [p.free? ? 0 : 1, p.vote_credits.to_i] }
49
+ end
50
+
51
+ # Overrided
52
+ def serialize_collection(collection)
53
+ collection_serializer.new(
54
+ collection,
55
+ include: resource_includes
56
+ ).serializable_hash
57
+ end
58
+
59
+ # Overrided
60
+ def collection_serializer
61
+ Spree::V2::Tenant::VotePackageSerializer
62
+ end
63
+
64
+ def current_season
65
+ @current_season ||= current_vendor.shows
66
+ .where.not(parent_id: nil)
67
+ .find_by!(slug: params[:show_id])
68
+ end
69
+
70
+ # We need to require votable_type and votable_id params because we are filtering VotePackages by ShowEpisode.
71
+ # VotePackage Metadata(votable_type and votable_id) are important when allocating voting credits to users.
72
+ def require_votable_params
73
+ return if params[:votable_type].present? && params[:votable_id].present?
74
+
75
+ render_error_payload('votable_type and votable_id are required', 422)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -4,30 +4,14 @@
4
4
  module SpreeCmCommissioner
5
5
  module Orders
6
6
  class Find
7
- CURRENT_CART_STATES = %w[cart address].freeze
8
- CART_MAX_AGE_IN_MINUTES = ENV.fetch('CART_MAX_AGE_IN_MINUTES', '720').to_i # Default: 12 hours
9
-
10
7
  def execute(store:, user:, currency:, token: nil, state: nil)
11
- state = Array(state).map(&:to_s)
12
-
13
8
  params = { store_id: store.id, currency: currency }
14
9
  params[:state] = state if state.present?
15
10
 
16
- order = if token.present?
17
- find_by_token(params, token)
18
- elsif user.present?
19
- find_by_user(params, user)
20
- end
21
-
22
- # Only enforce hold and age expiration when fetching current cart states ('cart' or 'address').
23
- # For other states (like 'payment,complete'), we want to return the order even if it's old or expired
24
- # because some pages on client still need some specific state of order to display.
25
- if state.intersect?(CURRENT_CART_STATES)
26
- return nil if hold_expired?(order)
27
- return nil if cart_too_old?(order)
28
- end
11
+ return find_by_token(params, token) if token.present?
12
+ return find_by_user(params, user) if user.present?
29
13
 
30
- order
14
+ nil
31
15
  end
32
16
 
33
17
  def find_by_token(params, token)
@@ -40,20 +24,6 @@ module SpreeCmCommissioner
40
24
  scope.order(created_at: :desc).find_by(params)
41
25
  end
42
26
 
43
- def hold_expired?(order)
44
- return false if order.nil?
45
-
46
- # Check if the order has a hold_expires_at column and if it has already passed
47
- order.hold_expires_at.present? && order.hold_expires_at < Time.current
48
- end
49
-
50
- def cart_too_old?(order)
51
- return false if order.nil?
52
-
53
- # Treat the cart as active based on its last update time, not its creation time.
54
- order.updated_at < CART_MAX_AGE_IN_MINUTES.minutes.ago
55
- end
56
-
57
27
  private
58
28
 
59
29
  def scope
@@ -3,97 +3,41 @@ require 'google/cloud/firestore'
3
3
  # TODO: alert when available_slots is negative.
4
4
  module SpreeCmCommissioner
5
5
  class WaitingGuestsCaller < BaseInteractor
6
- # 10 min floor — Waiting Room step (full journey, most uncertainty)
7
- MIN_WAIT_TO_ENTER_SECONDS = (ENV['WAITING_ROOM_MIN_WAIT_TO_ENTER_SECONDS'] || 600).to_i
8
-
9
- # 5 min floor — Queue step (position assigned, sub-segment of the journey)
10
- MIN_QUEUE_TO_ENTER_SECONDS = (ENV['WAITING_ROOM_MIN_QUEUE_TO_ENTER_SECONDS'] || 300).to_i
11
-
12
- # Firestore bounds a batch update by payload size (10 MiB); 500 ops/commit leaves us far under that.
13
- FIRESTORE_BATCH_SIZE = (ENV['WAITING_ROOM_FIRESTORE_BATCH_SIZE'] || 500).to_i
14
-
15
6
  def call
16
- started_at = Time.zone.now
17
7
  available_slots = fetch_available_slots
8
+ return unless available_slots.positive?
18
9
 
19
- # Always run through to mark_as — even with no slots — so the lobby keeps a fresh heartbeat
20
- # (logs.waiting_guests_caller_job) during peak/full periods instead of going stale
21
- # and looking like a dead cron.
22
- long_waiting_guests = available_slots.positive? ? fetch_long_waiting_guests(available_slots) : []
23
- calling_all(long_waiting_guests) if long_waiting_guests.any?
10
+ long_waiting_guests = fetch_long_waiting_guests(available_slots)
11
+ calling_all(long_waiting_guests)
24
12
 
25
- mark_as(
26
- full: long_waiting_guests.size >= available_slots,
27
- available_slots: available_slots - long_waiting_guests.size,
28
- avg_wait_to_enter_seconds: compute_avg_wait_to_enter_seconds(long_waiting_guests),
29
- avg_queue_to_enter_seconds: compute_avg_queue_to_enter_seconds(long_waiting_guests),
30
- recent_slots_per_call: available_slots.positive? ? available_slots : nil,
31
- active_sessions: @active_sessions,
32
- max_sessions: @max_sessions,
33
- logs: call_logs(started_at: started_at, called_count: long_waiting_guests.size)
34
- )
13
+ mark_as(full: long_waiting_guests.size >= available_slots, available_slots: available_slots - long_waiting_guests.size)
35
14
  end
36
15
 
37
16
  def fetch_available_slots
38
- @max_sessions = fetch_max_sessions
39
- @active_sessions = SpreeCmCommissioner::WaitingRoomSession.active.count
40
- [@max_sessions - @active_sessions, 0].max
17
+ max_sessions = fetch_max_sessions
18
+ active_sessions = SpreeCmCommissioner::WaitingRoomSession.active.count
19
+ max_sessions - active_sessions
41
20
  end
42
21
 
43
- # This query requires an index; create it in Firebase beforehand.
44
- # Client must create waiting_guests documents with :queued_at and :allow_to_enter_room_at set to nil to allow filter + order queries.
45
- #
46
- # Yesterday's guests are always older than today's, so fill from yesterday first, then use any
47
- # leftover slots for today. This way no one queued before the midnight rollover gets skipped.
48
- # e.g. 5 slots, 2 waiting in yesterday -> take both, then take 3 from today.
22
+ # This query required index. create them in Firebase beforehand.
23
+ # Client side must create waiting_guests document with :queued_at & :allow_to_enter_room_at to null to allow fillter & order.
49
24
  def fetch_long_waiting_guests(available_slots)
50
- previous_guests = eligible_guests_in(previous_records_path, available_slots)
51
-
52
- # Pre-flip window: the lobby pointer still points at yesterday, so both paths resolve to the
53
- # same partition — return now to avoid querying (and double-counting) it twice.
54
- return previous_guests if records_path == previous_records_path
55
-
56
- remaining_slots = available_slots - previous_guests.size
57
- return previous_guests if remaining_slots <= 0
58
-
59
- previous_guests + eligible_guests_in(records_path, remaining_slots)
60
- end
61
-
62
- def eligible_guests_in(records_path, limit)
63
- firestore.col(records_path)
25
+ firestore.col('waiting_guests')
26
+ .doc(current_date)
27
+ .col('records')
64
28
  .where('allow_to_enter_room_at', '==', nil)
65
29
  .order('queued_at')
66
- .limit(limit)
30
+ .limit(available_slots)
67
31
  .get.to_a
68
32
  end
69
33
 
70
- # Published path is authoritative; fall back to the server's own date if not yet published.
71
- def records_path
72
- lobby_data&.dig(:waiting_guests_records_path).presence || default_records_path(current_date)
73
- end
74
-
75
- # Drain target is derived from the server date, never the (possibly stale) lobby pointer.
76
- def previous_records_path
77
- default_records_path(previous_date)
78
- end
79
-
80
- def default_records_path(date)
81
- "waiting_guests/#{date}/records"
82
- end
83
-
84
34
  # For alert waiting guests to enter room, we just update :allow_to_enter_room_at.
85
35
  # App will listen to firebase & start refresh session token to enter room.
86
- #
87
- # Commit in Firestore batches (chunks of FIRESTORE_BATCH_SIZE) instead of one update per guest,
88
- # so e.g. 1000 guests = 2 commits, not 1000 round-trips. update merges, so we only send the
89
- # changed field and leave the rest of each doc intact.
90
36
  def calling_all(waiting_guests)
91
- allow_at = Time.zone.now
92
-
93
- waiting_guests.each_slice(FIRESTORE_BATCH_SIZE) do |slice|
94
- firestore.batch do |b|
95
- slice.each { |document| b.update(document.ref, { allow_to_enter_room_at: allow_at }) }
96
- end
37
+ waiting_guests.each do |document|
38
+ data = document.data.dup
39
+ data[:allow_to_enter_room_at] = Time.zone.now
40
+ document.ref.update(data)
97
41
  end
98
42
  end
99
43
 
@@ -101,79 +45,9 @@ module SpreeCmCommissioner
101
45
  Time.zone.now.strftime('%Y-%m-%d')
102
46
  end
103
47
 
104
- def previous_date
105
- 1.day.ago.strftime('%Y-%m-%d')
106
- end
107
-
108
- # merge: true so we preserve the published `waiting_guests_records_path` on the lobby doc.
109
- # App-facing fields plus live capacity state (full / available_slots / active_sessions /
110
- # max_sessions / ETAs) stay top-level; this run's observability snapshot is namespaced under
111
- # `logs.waiting_guests_caller_job` (see #call_logs). merge: true deep-merges nested maps, so
112
- # each job owns its own key under `logs` without clobbering the others.
113
- def mark_as( # rubocop:disable Metrics/ParameterLists
114
- full:,
115
- available_slots:,
116
- avg_wait_to_enter_seconds: nil,
117
- avg_queue_to_enter_seconds: nil,
118
- recent_slots_per_call: nil,
119
- active_sessions: nil,
120
- max_sessions: nil,
121
- logs: nil
122
- )
123
- data = { full: full, available_slots: available_slots }
124
- data[:avg_wait_to_enter_seconds] = avg_wait_to_enter_seconds if avg_wait_to_enter_seconds
125
- data[:avg_queue_to_enter_seconds] = avg_queue_to_enter_seconds if avg_queue_to_enter_seconds
126
- data[:recent_slots_per_call] = recent_slots_per_call if recent_slots_per_call
127
- data[:active_sessions] = active_sessions unless active_sessions.nil?
128
- data[:max_sessions] = max_sessions unless max_sessions.nil?
129
- data[:logs] = { waiting_guests_caller_job: logs } if logs
130
-
131
- lobby_document.set(data, merge: true)
132
- end
133
-
134
- # Point-in-time snapshot (overwritten each run, never cumulative) of this caller run: last-run
135
- # timing (heartbeat) and how many guests were called. Capacity state (active/max sessions,
136
- # available_slots) lives top-level, not here.
137
- def call_logs(started_at:, called_count:)
138
- finished_at = Time.zone.now
139
- {
140
- called_count: called_count,
141
- last_started_at: started_at,
142
- last_finished_at: finished_at,
143
- last_duration_ms: ((finished_at - started_at) * 1000).round
144
- }
145
- end
146
-
147
- # Average total wait (queued_at → allow_to_enter_room_at) for just-called guests.
148
- # Used for the Waiting Room step ETA: how long until I get in from when I joined.
149
- def compute_avg_wait_to_enter_seconds(guests)
150
- return nil if guests.empty?
151
-
152
- now = Time.zone.now
153
- total = guests.sum { |doc| (now - doc.data[:queued_at].to_time).to_f }
154
- [(total / guests.size).round, MIN_WAIT_TO_ENTER_SECONDS].max
155
- end
156
-
157
- # Average queue wait (position_set_at → allow_to_enter_room_at) for just-called guests.
158
- # Used for the Queue step ETA: how long after getting a position until I actually enter.
159
- # Returns nil when none of the called guests had position_set_at (first run, or no prior stamp).
160
- def compute_avg_queue_to_enter_seconds(guests)
161
- return nil if guests.empty?
162
-
163
- now = Time.zone.now
164
- positioned = guests.select { |doc| doc.data[:position_set_at] }
165
- return nil if positioned.empty?
166
-
167
- total = positioned.sum { |doc| (now - doc.data[:position_set_at].to_time).to_f }
168
- [(total / positioned.size).round, MIN_QUEUE_TO_ENTER_SECONDS].max
169
- end
170
-
171
- def lobby_data
172
- @lobby_data ||= lobby_document.get.data
173
- end
174
-
175
- def lobby_document
176
- @lobby_document ||= firestore.col('waiting_rooms').doc('lobby')
48
+ # When open app, app request to check whether room is full or not via Firebase instead of server to minimize server requests.
49
+ def mark_as(full:, available_slots:)
50
+ firestore.col('waiting_rooms').doc('lobby').set({ full: full, available_slots: available_slots })
177
51
  end
178
52
 
179
53
  def fetch_max_sessions
@@ -10,14 +10,12 @@ module SpreeCmCommissioner
10
10
  secret_access_key: ENV.fetch('AWS_SECRET_ACCESS_KEY')
11
11
  )
12
12
 
13
- response = ecs_client.describe_services(
13
+ tasks_response = ecs_client.list_tasks(
14
14
  cluster: ENV.fetch('AWS_CLUSTER_NAME'),
15
- services: [ENV.fetch('AWS_CLUSTER_SERVICE_NAME')]
15
+ launch_type: 'FARGATE'
16
16
  )
17
17
 
18
- running_count = response.services.first.running_count
19
-
20
- SpreeCmCommissioner::WaitingRoomSystemMetadataSetter.new.set(server_running_count: running_count)
18
+ SpreeCmCommissioner::WaitingRoomSystemMetadataSetter.new.set(server_running_count: [tasks_response.task_arns.size, 1].max)
21
19
  end
22
20
  end
23
21
  end