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.
- checksums.yaml +4 -4
- data/.env.example +0 -13
- data/.github/workflows/test_and_build_gem.yml +1 -2
- data/Gemfile.lock +1 -1
- data/app/controllers/spree/admin/import_new_orders_controller.rb +0 -25
- data/app/controllers/spree/admin/inventory_holds_controller.rb +1 -21
- data/app/controllers/spree/admin/system/waiting_room_controller.rb +0 -15
- data/app/controllers/spree/admin/taxons_controller_decorator.rb +0 -5
- data/app/controllers/spree/api/v2/storefront/account/orders_controller_decorator.rb +0 -5
- data/app/controllers/spree/api/v2/storefront/guests_controller.rb +5 -7
- data/app/controllers/spree/api/v2/storefront/order_histories_controller.rb +2 -14
- data/app/controllers/spree/api/v2/storefront/ticket_transfers_controller.rb +3 -4
- data/app/controllers/spree/api/v2/tenant/order_histories_controller.rb +2 -14
- data/app/controllers/spree/api/v2/tenant/vote_packages_controller.rb +81 -0
- data/app/finders/spree_cm_commissioner/orders/find.rb +3 -33
- data/app/interactors/spree_cm_commissioner/waiting_guests_caller.rb +20 -146
- data/app/interactors/spree_cm_commissioner/waiting_room_latest_system_metadata_puller.rb +3 -5
- data/app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb +6 -25
- data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_job.rb +3 -21
- data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold_job.rb +3 -34
- data/app/jobs/spree_cm_commissioner/waiting_guests_caller_job.rb +1 -6
- data/app/jobs/spree_cm_commissioner/waiting_room_latest_system_metadata_puller_job.rb +1 -1
- data/app/mailers/spree/order_mailer_decorator.rb +0 -2
- data/app/models/concerns/spree_cm_commissioner/line_item_guests_concern.rb +2 -16
- data/app/models/concerns/spree_cm_commissioner/order_holdable.rb +0 -6
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +1 -33
- data/app/models/concerns/spree_cm_commissioner/product_delegation.rb +0 -2
- data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +3 -0
- data/app/models/concerns/spree_cm_commissioner/store_preference.rb +0 -1
- data/app/models/spree_cm_commissioner/claim.rb +27 -0
- data/app/models/spree_cm_commissioner/import.rb +1 -3
- data/app/models/spree_cm_commissioner/imports/import_order.rb +0 -21
- data/app/models/spree_cm_commissioner/inventory_hold.rb +1 -1
- data/app/models/spree_cm_commissioner/line_item_decorator.rb +26 -0
- data/app/models/spree_cm_commissioner/notification.rb +0 -1
- data/app/models/spree_cm_commissioner/order_decorator.rb +0 -14
- data/app/models/spree_cm_commissioner/payment_decorator.rb +0 -11
- data/app/models/spree_cm_commissioner/product_decorator.rb +0 -1
- data/app/models/spree_cm_commissioner/show.rb +9 -1
- data/app/models/spree_cm_commissioner/show_episode.rb +14 -0
- data/app/models/spree_cm_commissioner/taxon_decorator.rb +0 -1
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +1 -0
- data/app/models/spree_cm_commissioner/voting_credit.rb +2 -0
- data/app/models/spree_cm_commissioner/voting_session.rb +1 -0
- data/app/overrides/spree/admin/products/_form/allow_gift_transfer.html.erb.deface +1 -1
- data/app/overrides/spree/admin/products/_form/allow_transfer.html.erb.deface +1 -1
- data/app/overrides/spree/admin/stores/_form/store_preferences.html.erb.deface +0 -9
- data/app/overrides/spree/admin/taxons/_form/assets_form.html.erb.deface +1 -10
- data/app/serializers/spree/v2/storefront/taxon_serializer_decorator.rb +0 -2
- data/app/serializers/spree/v2/tenant/show_serializer.rb +1 -1
- data/app/serializers/spree/v2/tenant/vote_package_serializer.rb +7 -1
- data/app/services/spree_cm_commissioner/cart/add_guest.rb +3 -30
- data/app/services/spree_cm_commissioner/cart/remove_guest.rb +1 -26
- data/app/services/spree_cm_commissioner/imports/orders/create.rb +0 -1
- data/app/services/spree_cm_commissioner/inventory_holds/release.rb +0 -18
- data/app/services/spree_cm_commissioner/vote_package/create.rb +9 -4
- data/app/services/spree_cm_commissioner/vote_package/update.rb +11 -5
- data/app/services/spree_cm_commissioner/voting_contestants/bulk_updater.rb +6 -3
- data/app/services/spree_cm_commissioner/voting_credits/admin_grant.rb +37 -0
- data/app/services/spree_cm_commissioner/voting_credits/allocate.rb +24 -2
- data/app/services/spree_cm_commissioner/voting_credits/claimable_scope_resolver.rb +64 -0
- data/app/services/spree_cm_commissioner/waiting_room_system_metadata_fetcher.rb +7 -9
- data/app/services/spree_cm_commissioner/waiting_room_system_metadata_setter.rb +6 -16
- data/app/views/spree/admin/import_new_orders/index.html.erb +0 -4
- data/app/views/spree/admin/import_new_orders/show.html.erb +0 -30
- data/app/views/spree/admin/imports/index.html.erb +0 -2
- data/app/views/spree/admin/inventory_holds/index.html.erb +3 -8
- data/app/views/spree/admin/system/waiting_room/show.html.erb +0 -53
- data/app/views/spree_cm_commissioner/order_mailer/purchased_items/_items.html.erb +9 -12
- data/config/initializers/spree_permitted_attributes.rb +0 -4
- data/config/locales/en.yml +5 -16
- data/config/locales/km.yml +4 -12
- data/config/routes.rb +3 -4
- data/db/migrate/20260623082233_create_cm_claims.rb +14 -0
- data/db/migrate/20260624175658_add_unique_index_to_cm_claims.rb +11 -0
- data/lib/spree_cm_commissioner/test_helper/factories/claim_factory.rb +14 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +9 -38
- data/app/controllers/spree/api/v2/tenant/free_vote_claims_controller.rb +0 -37
- data/app/finders/spree_cm_commissioner/orders/find_by_all_state.rb +0 -11
- data/app/interactors/spree_cm_commissioner/payment_incomplete_notification_sender.rb +0 -25
- data/app/interactors/spree_cm_commissioner/release_inventory_item_notification_sender.rb +0 -24
- data/app/jobs/concerns/spree_cm_commissioner/idempotent_job.rb +0 -12
- data/app/jobs/spree_cm_commissioner/cancel_import_order_job.rb +0 -11
- data/app/jobs/spree_cm_commissioner/idempotency_keys/prune_job.rb +0 -20
- data/app/jobs/spree_cm_commissioner/payment_incomplete_notification_job.rb +0 -12
- data/app/jobs/spree_cm_commissioner/telegram_alerts/order_integrity_check_job.rb +0 -17
- data/app/jobs/spree_cm_commissioner/waiting_room/publish_lobby_path_job.rb +0 -17
- data/app/jobs/spree_cm_commissioner/waiting_room/stamp_queue_positions_job.rb +0 -14
- data/app/models/spree_cm_commissioner/idempotency_key.rb +0 -21
- data/app/models/spree_cm_commissioner/imported_order.rb +0 -6
- data/app/models/spree_cm_commissioner/taxon_ad_banner.rb +0 -12
- data/app/notifications/spree_cm_commissioner/order_general_notification.rb +0 -15
- data/app/overrides/spree/admin/orders/_search/payment_number_search_field.html.erb.deface +0 -8
- data/app/overrides/spree/admin/products/_form/enable_telegram_alert.html.erb.deface +0 -14
- data/app/services/spree_cm_commissioner/cart/add_item_decorator.rb +0 -22
- data/app/services/spree_cm_commissioner/cart/create_guest.rb +0 -80
- data/app/services/spree_cm_commissioner/cart/empty_decorator.rb +0 -23
- data/app/services/spree_cm_commissioner/cart/remove_item_decorator.rb +0 -17
- data/app/services/spree_cm_commissioner/cart/remove_line_item_decorator.rb +0 -17
- data/app/services/spree_cm_commissioner/cart/set_quantity_decorator.rb +0 -17
- data/app/services/spree_cm_commissioner/imports/orders/cancel.rb +0 -63
- data/app/services/spree_cm_commissioner/telegram_alerts/checks/order_complete_payment_not_paid.rb +0 -21
- data/app/services/spree_cm_commissioner/telegram_alerts/checks/payment_paid_order_not_complete.rb +0 -21
- data/app/services/spree_cm_commissioner/telegram_alerts/order_integrity_alert.rb +0 -112
- data/app/services/spree_cm_commissioner/telegram_alerts/order_integrity_checks_runner.rb +0 -32
- data/app/services/spree_cm_commissioner/voting_credits/claim_free_votes.rb +0 -119
- data/app/services/spree_cm_commissioner/waiting_room/publish_lobby_path.rb +0 -57
- data/app/services/spree_cm_commissioner/waiting_room/stamp_queue_positions.rb +0 -185
- data/app/services/spree_cm_commissioner/waiting_room_lobby_metadata_fetcher.rb +0 -47
- data/app/views/spree/admin/import_new_orders/_cancel_modal.html.erb +0 -35
- data/db/migrate/20260616000001_add_canceled_by_id_to_cm_imports.rb +0 -5
- data/db/migrate/20260617000002_create_cm_imported_orders.rb +0 -12
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ac1f1c30c374b8f4ad3dd06b486bbe1e25cbfa1f1c26bbd7a3d53895aaefaa2
|
|
4
|
+
data.tar.gz: affc12ef47495c5c51530c1fae89c771380b3759825932ea263ec5474abbb289
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
35
|
-
line_item: parent,
|
|
36
|
-
options: guest_params
|
|
37
|
-
)
|
|
34
|
+
resource = scope.new(guest_params)
|
|
38
35
|
|
|
39
|
-
if
|
|
40
|
-
|
|
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(
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
44
|
-
# Client must create waiting_guests
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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(
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
13
|
+
tasks_response = ecs_client.list_tasks(
|
|
14
14
|
cluster: ENV.fetch('AWS_CLUSTER_NAME'),
|
|
15
|
-
|
|
15
|
+
launch_type: 'FARGATE'
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
-
|
|
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
|