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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/app/controllers/concerns/spree_cm_commissioner/content_cachable.rb +1 -1
- data/app/controllers/spree/admin/system/waiting_room_sessions_controller.rb +27 -0
- data/app/controllers/spree/admin/vendor_authorized_users_controller.rb +0 -15
- data/app/controllers/spree/api/v2/operator/check_ins_controller.rb +22 -4
- data/app/controllers/spree/api/v2/operator/dashboard_crew_events_controller.rb +1 -1
- data/app/controllers/spree/api/v2/operator/event_charts_controller.rb +34 -12
- data/app/controllers/spree/api/v2/resource_controller_decorator.rb +7 -0
- data/app/controllers/spree/api/v2/storefront/draft_orders_controller.rb +2 -2
- data/app/controllers/spree/api/v2/storefront/queue_draft_orders_controller.rb +55 -0
- data/app/controllers/spree/api/v2/tenant/draft_orders_controller.rb +1 -1
- data/app/controllers/spree/api/v2/tenant/voting_contestants_controller.rb +1 -1
- data/app/controllers/spree/api/v2/tenant/voting_credit_transactions_controller.rb +1 -1
- data/app/controllers/spree_cm_commissioner/admin/variants_controller_decorator.rb +14 -4
- data/app/interactors/spree_cm_commissioner/payment_incomplete_notification_sender.rb +12 -1
- data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold_job.rb +8 -4
- data/app/jobs/spree_cm_commissioner/queue_draft_orders/process_job.rb +23 -0
- data/app/jobs/spree_cm_commissioner/voting_session_auto_open_job.rb +14 -0
- data/app/models/concerns/spree_cm_commissioner/event_metadata.rb +27 -0
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +6 -21
- data/app/models/concerns/spree_cm_commissioner/purchase_limitable.rb +22 -0
- data/app/models/concerns/spree_cm_commissioner/ticket_transferable.rb +2 -2
- data/app/models/concerns/spree_cm_commissioner/vendor_preference.rb +0 -19
- data/app/models/concerns/spree_cm_commissioner/voting_session_timing_concern.rb +25 -7
- data/app/models/spree_cm_commissioner/block.rb +1 -0
- data/app/models/spree_cm_commissioner/inventory_item.rb +26 -10
- data/app/models/spree_cm_commissioner/line_item_decorator.rb +8 -1
- data/app/models/spree_cm_commissioner/maintenance_tasks/event.rb +20 -0
- data/app/models/spree_cm_commissioner/order_decorator.rb +13 -1
- data/app/models/spree_cm_commissioner/price_decorator.rb +16 -0
- data/app/models/spree_cm_commissioner/product_decorator.rb +36 -0
- data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +38 -19
- data/app/models/spree_cm_commissioner/seat_layout.rb +46 -9
- data/app/models/spree_cm_commissioner/seat_section.rb +47 -0
- data/app/models/spree_cm_commissioner/stock/availability_checker.rb +15 -7
- data/app/models/spree_cm_commissioner/stock/availability_validator_decorator.rb +57 -5
- data/app/models/spree_cm_commissioner/stock/line_item_availability_checker.rb +7 -4
- data/app/models/spree_cm_commissioner/stock/order_availability_checker.rb +7 -1
- data/app/models/spree_cm_commissioner/taxon_decorator.rb +12 -0
- data/app/models/spree_cm_commissioner/trip.rb +1 -1
- data/app/models/spree_cm_commissioner/variant_decorator.rb +22 -1
- data/app/models/spree_cm_commissioner/vote.rb +3 -0
- data/app/models/spree_cm_commissioner/voting_contestant.rb +1 -0
- data/app/models/spree_cm_commissioner/voting_credit_transaction.rb +2 -0
- data/app/models/spree_cm_commissioner/voting_session.rb +15 -0
- data/app/models/spree_cm_commissioner/waiting_room_session.rb +20 -0
- data/app/overrides/spree/admin/products/_form/purchase_limits.html.erb.deface +49 -0
- data/app/queries/spree_cm_commissioner/dashboard_crew_event_query.rb +10 -5
- data/app/serializers/spree/v2/storefront/taxon_serializer_decorator.rb +2 -1
- data/app/serializers/spree/v2/tenant/voting_contestant_serializer.rb +1 -2
- data/app/serializers/spree/v2/tenant/voting_credit_transaction_serializer.rb +1 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/block_serializer.rb +1 -1
- data/app/serializers/spree_cm_commissioner/v2/storefront/seat_layout_serializer.rb +1 -1
- data/app/serializers/spree_cm_commissioner/v2/storefront/seat_section_serializer.rb +2 -1
- data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +3 -0
- data/app/services/spree_cm_commissioner/cart/add_guest.rb +12 -7
- data/app/services/spree_cm_commissioner/{draft_order → draft_orders}/create.rb +2 -2
- data/app/services/spree_cm_commissioner/intercity_taxi_order/create.rb +2 -4
- data/app/services/spree_cm_commissioner/inventory_holds/acquire.rb +27 -12
- data/app/services/spree_cm_commissioner/inventory_holds/convert.rb +10 -9
- data/app/services/spree_cm_commissioner/inventory_holds/release.rb +12 -11
- data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities.rb +30 -9
- data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold.rb +33 -8
- data/app/services/spree_cm_commissioner/inventory_items/move_to_locked.rb +159 -0
- data/app/services/spree_cm_commissioner/inventory_items/reset.rb +38 -18
- data/app/services/spree_cm_commissioner/line_items/validate_purchase_limits.rb +29 -7
- data/app/services/spree_cm_commissioner/load_test/generate_booking_ticket_data.rb +56 -20
- data/app/services/spree_cm_commissioner/queue_draft_orders/enqueue.rb +64 -0
- data/app/services/spree_cm_commissioner/queue_draft_orders/process.rb +81 -0
- data/app/services/spree_cm_commissioner/queue_draft_orders/publish_status.rb +39 -0
- data/app/services/spree_cm_commissioner/redis_stock/restock.rb +16 -5
- data/app/services/spree_cm_commissioner/redis_stock/unstock.rb +12 -5
- data/app/services/spree_cm_commissioner/seat_layouts/sanitize_svg.rb +215 -0
- data/app/services/spree_cm_commissioner/seat_layouts/update.rb +79 -0
- data/app/services/spree_cm_commissioner/ticket_transfers/accept.rb +1 -1
- data/app/services/spree_cm_commissioner/transit_order/create.rb +4 -10
- data/app/services/spree_cm_commissioner/vote_credit_deductor.rb +1 -0
- data/app/services/spree_cm_commissioner/voting_contestants/bulk_updater.rb +10 -4
- data/app/services/spree_cm_commissioner/voting_sessions/auto_open.rb +38 -0
- data/app/services/spree_cm_commissioner/voting_sessions/finalize.rb +17 -3
- data/app/services/spree_cm_commissioner/voting_sessions/publish_live_state.rb +15 -0
- data/app/services/spree_cm_commissioner/voting_sessions/publish_to_firestore.rb +32 -23
- data/app/services/spree_cm_commissioner/voting_sessions/schedule_vote_window_jobs.rb +45 -0
- data/app/services/spree_cm_commissioner/waiting_room/stamp_queue_positions.rb +16 -4
- data/app/views/spree/admin/stock_managements/index.html.erb +8 -0
- data/app/views/spree/admin/system/waiting_room/show.html.erb +1 -0
- data/app/views/spree/admin/system/waiting_room_sessions/index.html.erb +118 -0
- data/app/views/spree/admin/variants/fixture.html.erb +3 -3
- data/app/views/spree/admin/vendor_authorized_users/index.html.erb +0 -15
- data/config/initializers/spree_permitted_attributes.rb +10 -0
- data/config/load_test_flows/platform.yml +20 -0
- data/config/locales/en.yml +43 -14
- data/config/locales/km.yml +33 -7
- data/config/routes.rb +7 -0
- data/db/migrate/20260813100000_add_guide_to_cm_seat_layouts.rb +12 -0
- data/db/migrate/20260813100001_add_sub_seat_layout_reference_to_cm_seat_sections.rb +7 -0
- data/db/migrate/20260813100002_add_shape_path_to_cm_blocks.rb +13 -0
- data/db/migrate/20260813100003_add_metadata_to_cm_seat_sections.rb +6 -0
- data/db/migrate/20260814000000_add_contestant_id_to_cm_voting_credit_transactions.rb +7 -0
- data/db/migrate/20260817000000_add_eliminated_voting_session_to_cm_voting_contestants.rb +9 -0
- data/db/migrate/20260817072341_add_metadata_to_cm_seat_layouts.rb +6 -0
- data/db/migrate/20260819100000_add_exit_at_to_cm_waiting_room_sessions.rb +5 -0
- data/db/migrate/20260820000000_add_locked_quantities_to_cm_inventory_items.rb +12 -0
- data/lib/spree_cm_commissioner/cached_inventory_item.rb +9 -0
- data/lib/spree_cm_commissioner/test_helper/factories/block_factory.rb +4 -0
- data/lib/spree_cm_commissioner/test_helper/factories/seat_layout_factory.rb +12 -0
- data/lib/spree_cm_commissioner/test_helper/factories/show_factory.rb +6 -0
- data/lib/spree_cm_commissioner/test_helper/factories/voting_credit_transaction_factory.rb +4 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +27 -4
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1bcaa0a7cc8d39b507cd072bfd4576b28f72c83f2344b11ef2097306aa8454dd
|
|
4
|
+
data.tar.gz: bf8156fe0e0f2a41febcacc07188e8b0bbd9b2cd79c3ccad5a8d1394fa307db7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 19b750a2f5c27cabbc5c507a0e50424a26e295aefa94369787ff490376475f63e56993950e87a9a1b2c3be624f6ec29f2192a51a7e019bfed05e3ee4dcb54018
|
|
7
|
+
data.tar.gz: 4acf6f94be21c3960010398b9a431b950b2e3e7bffed455096ea0dc2ed05fca769bd53884b6ed26be86cc11db2a8c220c73711be800bccebf7fc005854454f11
|
data/Gemfile.lock
CHANGED
|
@@ -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 ||=
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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,23 +7,31 @@ module Spree
|
|
|
7
7
|
ADDITIONAL_FIELDS = %i[participation entry_type].freeze
|
|
8
8
|
|
|
9
9
|
def index
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 =
|
|
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::
|
|
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:
|
|
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::
|
|
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
|
|
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
|
-
@
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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::
|
|
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::
|
|
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
|