spree_cm_commissioner 2.14.4 → 2.14.6
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 +7 -1
- data/Gemfile.lock +1 -1
- data/app/controllers/concerns/spree_cm_commissioner/content_cachable.rb +59 -3
- data/app/controllers/concerns/spree_cm_commissioner/waiting_room_authorization.rb +41 -8
- data/app/controllers/spree/admin/api_caches_controller.rb +23 -8
- data/app/controllers/spree/admin/homepage_section_controller.rb +63 -5
- data/app/controllers/spree/admin/homepage_section_relatable_controller.rb +24 -0
- data/app/controllers/spree/api/v2/organizer/invite_guests_controller.rb +2 -2
- data/app/controllers/spree/api/v2/resource_controller_decorator.rb +8 -0
- data/app/controllers/spree/api/v2/storefront/draft_orders_controller.rb +6 -2
- data/app/controllers/spree/api/v2/storefront/homepage_sections_controller.rb +1 -0
- data/app/controllers/spree/api/v2/storefront/invite_guests_controller.rb +2 -2
- data/app/controllers/spree/api/v2/storefront/preview_sections_controller.rb +1 -0
- data/app/controllers/spree/api/v2/storefront/queue_draft_orders_controller.rb +6 -2
- data/app/controllers/spree/api/v2/storefront/trip_search_controller.rb +18 -13
- data/app/controllers/spree/api/v2/storefront/user_contacts_controller.rb +2 -0
- data/app/controllers/spree/api/v2/tenant/draft_orders_controller.rb +6 -2
- data/app/controllers/spree/api/v2/tenant/homepage_sections_controller.rb +1 -0
- data/app/controllers/spree/api/v2/tenant/preview_sections_controller.rb +1 -0
- data/app/controllers/spree/api/v2/tenant/trip_search_controller.rb +18 -13
- data/app/factory/spree_cm_commissioner/order_telegram_message_factory.rb +5 -1
- data/app/helpers/spree_cm_commissioner/admin/homepage_segment_helper.rb +28 -0
- data/app/interactors/spree_cm_commissioner/account_recover.rb +23 -2
- data/app/interactors/spree_cm_commissioner/firebase_id_token_provider.rb +6 -2
- data/app/interactors/spree_cm_commissioner/telegram_chats_auto_finder.rb +26 -2
- data/app/interactors/spree_cm_commissioner/telegram_chats_finder.rb +1 -1
- data/app/interactors/spree_cm_commissioner/user_telegram_oauth_authenticator.rb +2 -11
- data/app/models/active_storage/variation_decorator.rb +18 -0
- data/app/models/concerns/spree_cm_commissioner/event_metadata.rb +4 -0
- data/app/models/concerns/spree_cm_commissioner/order_holdable.rb +3 -2
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +18 -13
- data/app/models/spree_cm_commissioner/homepage_section.rb +33 -0
- data/app/models/spree_cm_commissioner/homepage_section_app_background.rb +13 -0
- data/app/models/spree_cm_commissioner/homepage_section_web_background.rb +13 -0
- data/app/models/spree_cm_commissioner/maintenance_tasks/cache_invalidation.rb +95 -11
- data/app/models/spree_cm_commissioner/pin_code.rb +2 -1
- data/app/models/spree_cm_commissioner/pin_code_event_telegram.rb +31 -0
- data/app/models/spree_cm_commissioner/product_decorator.rb +7 -0
- data/app/models/spree_cm_commissioner/service_calendar.rb +2 -2
- data/app/models/spree_cm_commissioner/taxon_decorator.rb +7 -0
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +6 -0
- data/app/overrides/spree/admin/taxons/_form/enforce_waiting_room_session.html.erb.deface +10 -0
- data/app/queries/spree_cm_commissioner/trip_query.rb +20 -2
- data/app/serializers/spree/v2/storefront/homepage_section_serializer.rb +4 -1
- data/app/serializers/spree/v2/tenant/homepage_section_serializer.rb +3 -1
- data/app/services/spree_cm_commissioner/api_caches/fetch_details.rb +128 -0
- data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +31 -14
- data/app/services/spree_cm_commissioner/api_caches/invalidate_all.rb +16 -19
- data/app/services/spree_cm_commissioner/api_caches/request_clear.rb +71 -0
- data/app/services/spree_cm_commissioner/guests/claim_invite_guest_service.rb +5 -3
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_service_calendars.rb +3 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trip_stop.rb +21 -4
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trips.rb +30 -4
- data/app/services/spree_cm_commissioner/inventory_items/bulk_generate_permanent_items.rb +15 -4
- data/app/services/spree_cm_commissioner/line_items/validate_purchase_limits.rb +56 -5
- data/app/services/spree_cm_commissioner/telegram/verify_oauth_id_token.rb +10 -0
- data/app/services/spree_cm_commissioner/telegram_alerts/send_telegram_order_alerts.rb +49 -0
- data/app/views/spree/admin/api_caches/_dialog.html.erb +47 -0
- data/app/views/spree/admin/api_caches/show.html.erb +88 -0
- data/app/views/spree/admin/homepage_section/_form.html.erb +44 -0
- data/app/views/spree/admin/homepage_section/edit.html.erb +16 -1
- data/app/views/spree/admin/shared/_api_cache_button.html.erb +5 -2
- data/config/initializers/spree_permitted_attributes.rb +1 -0
- data/config/locales/dry_validations.en.yml +4 -0
- data/config/locales/en.yml +50 -0
- data/config/locales/km.yml +50 -0
- data/config/routes.rb +8 -4
- data/db/migrate/20261021000001_add_section_type_to_cm_homepage_sections.rb +6 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +13 -3
- data/app/jobs/spree_cm_commissioner/invalidate_cache_request_job.rb +0 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 743a7213a36a799ebd7d68c8e2a770a9a253b9ca9e544c53dc89114344c35eec
|
|
4
|
+
data.tar.gz: c9697afd92b4175c73c1291224522b4b56fdf4056da7c8b4f441b33f0c828af4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0bc416a5439a60e3daaa23cb031fee13bd2a5ab82795e05aa52005713bd27adf298f0b09ae793d7109f2b3b55bd8fd0b937d50323b3f53031c64217ad1396bff
|
|
7
|
+
data.tar.gz: '073149c66ec7fc0b4b32d0c9b76ee35c934a96e489ae68ca6116adc4b60f831dc4271dd696ce996dbe6f80cd1db2658d0ffdeabfc25630e238d9c7cff5301bbd'
|
data/.env.example
CHANGED
|
@@ -49,11 +49,17 @@ DISTANCE_SIGNING_KEY=hei********************VY
|
|
|
49
49
|
|
|
50
50
|
# CDN cache durations (s-maxage, in seconds) for listed public APIs; client max-age is always 0, unlisted APIs are never CDN-cached
|
|
51
51
|
# See: app/controllers/concerns/spree_cm_commissioner/content_cachable.rb
|
|
52
|
-
CACHE_AUTO_INVALIDATION_DISABLED="" # Set to yes to
|
|
52
|
+
CACHE_AUTO_INVALIDATION_DISABLED="" # Set to yes to stop automatic CDN cache invalidation on save (kill switch); a clear-cache click still sends one
|
|
53
53
|
CACHE_SERIALIZER_DISABLED="" # Set to yes to turn off Spree's serializer record cache everywhere
|
|
54
54
|
CACHE_STATIC_MAX_AGE=86400 # Static content (CMS pages) - 1 day
|
|
55
55
|
CACHE_REALTIME_MAX_AGE=300 # Everything else listed (homepage, menus, products, events, vendors, trips, seat layouts, countries, provinces, guest card classes) - 5 minutes
|
|
56
56
|
|
|
57
|
+
# Shared secret(s) CloudFront sends to the origin as the X-Origin-Verify custom header. When set, listed CDN-cacheable
|
|
58
|
+
# APIs return 403 to requests without a matching header. Comma-separate two values while rotating. Leave blank to turn off.
|
|
59
|
+
# Set only after every client reaches these APIs through CloudFront, or they get 403.
|
|
60
|
+
CDN_ORIGIN_SECRETS=""
|
|
61
|
+
CDN_ORIGIN_REPORT_ONLY="" # Set to yes to log unverified requests instead of refusing them (use before enforcing)
|
|
62
|
+
|
|
57
63
|
# Batch size for processing maintenance tasks in app/jobs/spree_cm_commissioner/maintenance_tasks/orchestrate_job.rb
|
|
58
64
|
MAINTENANCE_TASKS_BATCH_SIZE=100
|
|
59
65
|
|
data/Gemfile.lock
CHANGED
|
@@ -5,9 +5,14 @@ module SpreeCmCommissioner
|
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
7
|
included do
|
|
8
|
+
# Prepended so a request that skipped the CDN is refused before any other callback hits the DB.
|
|
9
|
+
prepend_before_action :verify_cdn_origin!, if: :cdn_cacheable?
|
|
8
10
|
after_action :set_cache_control_for_cdn
|
|
9
11
|
end
|
|
10
12
|
|
|
13
|
+
# CloudFront sends this as an origin custom header; clients never see or send it.
|
|
14
|
+
CDN_ORIGIN_HEADER = 'X-Origin-Verify'.freeze
|
|
15
|
+
|
|
11
16
|
# Every CDN-cached API, by full class name, with its GET actions and TTL tier. Anything not
|
|
12
17
|
# listed keeps Rails' default `max-age=0, private, must-revalidate` and never reaches the CDN.
|
|
13
18
|
#
|
|
@@ -69,14 +74,20 @@ module SpreeCmCommissioner
|
|
|
69
74
|
'Spree::Api::V2::Tenant::VotingContestantsController' => { actions: %w[index].freeze, max_age: :fresh }
|
|
70
75
|
}.freeze
|
|
71
76
|
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
# Seconds for a TTL tier, or nil for none. Public so the clear-cache dialog can say how long an
|
|
78
|
+
# edit takes to show without a clear.
|
|
79
|
+
def self.cdn_max_age(tier)
|
|
80
|
+
case tier
|
|
75
81
|
when :static then ENV.fetch('CACHE_STATIC_MAX_AGE', 1.day.to_i).to_i
|
|
76
82
|
when :fresh then ENV.fetch('CACHE_REALTIME_MAX_AGE', 5.minutes.to_i).to_i
|
|
77
83
|
end
|
|
78
84
|
end
|
|
79
85
|
|
|
86
|
+
# CDN (s-maxage) TTL, or nil for an unlisted controller. Override in a controller for a different one.
|
|
87
|
+
def shared_max_age
|
|
88
|
+
ContentCachable.cdn_max_age(cdn_cache_config&.dig(:max_age))
|
|
89
|
+
end
|
|
90
|
+
|
|
80
91
|
def cdn_cacheable?
|
|
81
92
|
request.get? && cdn_cache_config.present? && cdn_cache_config[:actions].include?(action_name)
|
|
82
93
|
end
|
|
@@ -92,8 +103,53 @@ module SpreeCmCommissioner
|
|
|
92
103
|
response.headers['Cache-Control'] = "public, max-age=0, s-maxage=#{shared_max_age}"
|
|
93
104
|
end
|
|
94
105
|
|
|
106
|
+
# Refuses a listed action when it did not come through CloudFront, so the CDN can't be skipped by
|
|
107
|
+
# calling the origin directly. Off while CDN_ORIGIN_SECRETS is unset. It takes a comma-separated
|
|
108
|
+
# list so a new secret can be added to CloudFront before the old one is dropped. With
|
|
109
|
+
# CDN_ORIGIN_REPORT_ONLY=yes it only logs, to find clients still calling the origin before enforcing.
|
|
110
|
+
def verify_cdn_origin!
|
|
111
|
+
return if cdn_origin_secrets.empty?
|
|
112
|
+
return if cdn_origin_verified?
|
|
113
|
+
|
|
114
|
+
provided = request.headers[CDN_ORIGIN_HEADER].to_s
|
|
115
|
+
if ENV['CDN_ORIGIN_REPORT_ONLY'] == 'yes'
|
|
116
|
+
CmAppLogger.log(
|
|
117
|
+
label: 'cdn_origin_unverified',
|
|
118
|
+
data: {
|
|
119
|
+
controller: self.class.name,
|
|
120
|
+
action: action_name,
|
|
121
|
+
path: request.path,
|
|
122
|
+
header_present: provided.present?,
|
|
123
|
+
via_cloudfront: request.headers['X-Amz-Cf-Id'].present?
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
return
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
head :forbidden
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# True when CloudFront will cache this response, so a controller-level Rails.cache in front of it
|
|
133
|
+
# only stacks its TTL on CloudFront's and hands a stale body to the refetch that follows a
|
|
134
|
+
# CloudFront invalidation. Requires a matching secret, not just the header: a header any caller
|
|
135
|
+
# can set must not let them skip both caches and hit the query directly.
|
|
136
|
+
def served_to_cdn?
|
|
137
|
+
return @served_to_cdn if defined?(@served_to_cdn)
|
|
138
|
+
|
|
139
|
+
@served_to_cdn = cdn_cacheable? && cdn_origin_verified?
|
|
140
|
+
end
|
|
141
|
+
|
|
95
142
|
private
|
|
96
143
|
|
|
144
|
+
def cdn_origin_verified?
|
|
145
|
+
provided = request.headers[CDN_ORIGIN_HEADER].to_s
|
|
146
|
+
cdn_origin_secrets.any? { |secret| ActiveSupport::SecurityUtils.secure_compare(provided, secret) }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def cdn_origin_secrets
|
|
150
|
+
ENV.fetch('CDN_ORIGIN_SECRETS', '').split(',').map(&:strip).compact_blank
|
|
151
|
+
end
|
|
152
|
+
|
|
97
153
|
def cdn_cache_config
|
|
98
154
|
CDN_CACHEABLE_ACTIONS[self.class.name.to_s]
|
|
99
155
|
end
|
|
@@ -7,11 +7,30 @@ module SpreeCmCommissioner
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def ensure_waiting_room_session_token
|
|
10
|
-
return if
|
|
11
|
-
return if request_from_client?
|
|
10
|
+
return if waiting_room_disabled?
|
|
12
11
|
return unless required_waiting_room_session?
|
|
12
|
+
return if active_waiting_room_session?
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
handle_waiting_room_session_token_error
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Product-based counterpart to the path-based check above, for draft-order endpoints: gating by
|
|
18
|
+
# path would hold every booking (hotel, transit, ...) behind the waiting room, while this only
|
|
19
|
+
# holds orders for products that opt in via Product#enforce_waiting_room_session?. Call it as a
|
|
20
|
+
# before_action on #create, next to ensure_anonymous_booking_allowed!.
|
|
21
|
+
#
|
|
22
|
+
# Products covered by the request's private sale link (`st`) are exempt: that buyer draws from a
|
|
23
|
+
# pool locked for them, not the public stock the queue is rationing. The exemption is per
|
|
24
|
+
# request, not per product — the same product is usually on public sale too.
|
|
25
|
+
#
|
|
26
|
+
# products: [Array<Spree::Product>] every distinct product referenced by this request.
|
|
27
|
+
def ensure_waiting_room_session!(products)
|
|
28
|
+
return if waiting_room_disabled?
|
|
29
|
+
|
|
30
|
+
private_sale_product_ids = waiting_room_private_sale&.product_ids || []
|
|
31
|
+
enforced = products.select(&:enforce_waiting_room_session?).reject { |product| private_sale_product_ids.include?(product.id) }
|
|
32
|
+
return if enforced.empty?
|
|
33
|
+
return if active_waiting_room_session?
|
|
15
34
|
|
|
16
35
|
handle_waiting_room_session_token_error
|
|
17
36
|
end
|
|
@@ -23,13 +42,27 @@ module SpreeCmCommissioner
|
|
|
23
42
|
apis.any? { |prefix| request.path.start_with?(prefix) }
|
|
24
43
|
end
|
|
25
44
|
|
|
26
|
-
# temporary disable session check from any client server requests.
|
|
27
|
-
def request_from_client?
|
|
28
|
-
request.headers['X-Cm-Api-Client-Version'].present?
|
|
29
|
-
end
|
|
30
|
-
|
|
31
45
|
def handle_waiting_room_session_token_error
|
|
32
46
|
render_error_payload(Spree.t(:invalid_session_token), 400)
|
|
33
47
|
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def waiting_room_disabled?
|
|
52
|
+
ENV['WAITING_ROOM_DISABLED'] == 'yes'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def active_waiting_room_session?
|
|
56
|
+
SpreeCmCommissioner::WaitingRoomSession.active.exists?(token: params[:waiting_room_session_token])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Verified, not just present: a made-up `st` must not skip the queue, so a token that fails
|
|
60
|
+
# Authorize simply earns no exemption.
|
|
61
|
+
def waiting_room_private_sale
|
|
62
|
+
return if params[:st].blank?
|
|
63
|
+
|
|
64
|
+
result = SpreeCmCommissioner::PrivateSales::Authorize.call(token: params[:st], user: spree_current_user)
|
|
65
|
+
result.value if result.success?
|
|
66
|
+
end
|
|
34
67
|
end
|
|
35
68
|
end
|
|
@@ -2,16 +2,16 @@ module Spree
|
|
|
2
2
|
module Admin
|
|
3
3
|
# Behind every admin clear-cache button (spree/admin/shared/_api_cache_button). Admin-only through
|
|
4
4
|
# BaseController#authorize_admin, which checks :api_caches since there is no model class.
|
|
5
|
+
#
|
|
6
|
+
# Both actions take model and id, or no id to clear every record of model on one storefront:
|
|
7
|
+
# tenant_id for that tenant's API, none for the BookMe+ storefront.
|
|
5
8
|
class ApiCachesController < Spree::Admin::BaseController
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
before_action :load_tenant
|
|
10
|
+
|
|
8
11
|
def clear
|
|
9
|
-
result =
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
tenant = SpreeCmCommissioner::Tenant.find(params[:tenant_id]) if params[:tenant_id].present?
|
|
13
|
-
SpreeCmCommissioner::ApiCaches::InvalidateAll.call(model: params[:model], tenant: tenant)
|
|
14
|
-
end
|
|
12
|
+
result = SpreeCmCommissioner::ApiCaches::RequestClear.call(
|
|
13
|
+
model: params[:model], id: params[:id], tenant: @tenant, user: try_spree_current_user
|
|
14
|
+
)
|
|
15
15
|
|
|
16
16
|
if result.success?
|
|
17
17
|
flash[:success] = Spree.t(:clear_cache_requested)
|
|
@@ -21,6 +21,21 @@ module Spree
|
|
|
21
21
|
|
|
22
22
|
redirect_back fallback_location: admin_root_path
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
# The dialog's contents, loaded into spree/admin/api_caches/_dialog's frame by the button.
|
|
26
|
+
def show
|
|
27
|
+
result = SpreeCmCommissioner::ApiCaches::FetchDetails.call(model: params[:model], id: params[:id], tenant: @tenant)
|
|
28
|
+
raise ActiveRecord::RecordNotFound if result.failure?
|
|
29
|
+
|
|
30
|
+
@details = result.value
|
|
31
|
+
render layout: false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def load_tenant
|
|
37
|
+
@tenant = SpreeCmCommissioner::Tenant.find(params[:tenant_id]) if params[:tenant_id].present?
|
|
38
|
+
end
|
|
24
39
|
end
|
|
25
40
|
end
|
|
26
41
|
end
|
|
@@ -4,6 +4,8 @@ module Spree
|
|
|
4
4
|
helper SpreeCmCommissioner::Admin::HomepageSegmentHelper
|
|
5
5
|
|
|
6
6
|
before_action :load_tenants, only: [:index]
|
|
7
|
+
before_action :reject_invalid_section_type, only: %i[create update]
|
|
8
|
+
before_action :build_assets, only: %i[create update]
|
|
7
9
|
|
|
8
10
|
helper_method :selected_tenant
|
|
9
11
|
|
|
@@ -39,8 +41,62 @@ module Spree
|
|
|
39
41
|
end
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
def remove_app_background
|
|
45
|
+
remove_asset(@object.app_background)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def remove_web_background
|
|
49
|
+
remove_asset(@object.web_background)
|
|
50
|
+
end
|
|
51
|
+
|
|
42
52
|
private
|
|
43
53
|
|
|
54
|
+
# Always redirects back to `edit` rather than re-rendering it on failure (unlike the
|
|
55
|
+
# sibling GuestCardClassesController#remove_asset) — this controller's `edit` action sets
|
|
56
|
+
# extra ivars (`@homepage_section_relatables`) that this action never loads, so rendering
|
|
57
|
+
# the template directly here would raise.
|
|
58
|
+
def remove_asset(asset)
|
|
59
|
+
if asset&.destroy
|
|
60
|
+
flash[:success] = Spree.t('notice_messages.icon_removed')
|
|
61
|
+
else
|
|
62
|
+
flash[:error] = Spree.t('errors.messages.cannot_remove_icon')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
redirect_to edit_admin_homepage_feed_homepage_section_url(@object)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Only `trending` sections use these assets; building them for another type would let a
|
|
69
|
+
# direct API call attach a background the form never shows and the model then has to
|
|
70
|
+
# discard on save (see HomepageSection#clear_non_trending_fields). Either way the raw
|
|
71
|
+
# upload must be pulled out of permitted_resource_params — left in place, it gets assigned
|
|
72
|
+
# straight to the has_one association by update_resource/create_resource, which raises
|
|
73
|
+
# ActiveRecord::AssociationTypeMismatch instead of being silently ignored.
|
|
74
|
+
def build_assets
|
|
75
|
+
unless trending_section?
|
|
76
|
+
permitted_resource_params.delete(:app_background)
|
|
77
|
+
permitted_resource_params.delete(:web_background)
|
|
78
|
+
return
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
@object.build_app_background(attachment: permitted_resource_params.delete(:app_background)) if permitted_resource_params[:app_background]
|
|
82
|
+
@object.build_web_background(attachment: permitted_resource_params.delete(:web_background)) if permitted_resource_params[:web_background]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def trending_section?
|
|
86
|
+
(permitted_resource_params[:section_type].presence || @object.section_type).to_s == 'trending'
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# `enum section_type:` raises ArgumentError on assignment of an unrecognized value, which
|
|
90
|
+
# would otherwise 500 instead of a normal validation failure — strip it here and flash
|
|
91
|
+
# instead, before it ever reaches create_resource/update_resource's mass-assignment.
|
|
92
|
+
def reject_invalid_section_type
|
|
93
|
+
submitted = permitted_resource_params[:section_type]
|
|
94
|
+
return if submitted.blank? || SpreeCmCommissioner::HomepageSection.section_types.key?(submitted.to_s)
|
|
95
|
+
|
|
96
|
+
permitted_resource_params.delete(:section_type)
|
|
97
|
+
flash[:error] = Spree.t('errors.messages.invalid_section_type')
|
|
98
|
+
end
|
|
99
|
+
|
|
44
100
|
def collection
|
|
45
101
|
return @collection if defined?(@collection)
|
|
46
102
|
|
|
@@ -57,16 +113,18 @@ module Spree
|
|
|
57
113
|
@collection = scoped.page(params[:page]).per(params[:per_page])
|
|
58
114
|
end
|
|
59
115
|
|
|
60
|
-
# overrided
|
|
116
|
+
# overrided — memoized so `.delete()` in build_assets mutates the same hash instance
|
|
61
117
|
def permitted_resource_params
|
|
118
|
+
return @permitted_resource_params if defined?(@permitted_resource_params)
|
|
119
|
+
|
|
62
120
|
object_params = params[:spree_cm_commissioner_homepage_section]
|
|
63
121
|
segment_value = helpers.calculate_segment_value(object_params)
|
|
64
122
|
mini_app_value = helpers.calculate_mini_app_value(object_params)
|
|
65
123
|
|
|
66
|
-
params
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
124
|
+
@permitted_resource_params = params
|
|
125
|
+
.require(:spree_cm_commissioner_homepage_section)
|
|
126
|
+
.permit(:title, :description, :active, :preview, :section_type, :font, :app_background, :web_background)
|
|
127
|
+
.merge(segment: segment_value, mini_app: mini_app_value)
|
|
70
128
|
end
|
|
71
129
|
|
|
72
130
|
# The storefront being viewed; no tenant in the URL means BookMe+. Also gates the mini app
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module Admin
|
|
3
3
|
class HomepageSectionRelatableController < Spree::Admin::ResourceController
|
|
4
|
+
skip_before_action :load_resource, only: :sort_by_start_date
|
|
5
|
+
|
|
4
6
|
def model_class
|
|
5
7
|
SpreeCmCommissioner::HomepageSectionRelatable
|
|
6
8
|
end
|
|
@@ -27,6 +29,28 @@ module Spree
|
|
|
27
29
|
def permitted_resource_params
|
|
28
30
|
params.require(object_name).permit(:homepage_section_id, :relatable_id, :relatable_type, :available_on, :discontinue_on)
|
|
29
31
|
end
|
|
32
|
+
|
|
33
|
+
# One-shot bulk reorder: ranks taxon relatables with an event `from_date` ascending, keeps every other
|
|
34
|
+
# relatable (dateless taxons, products) at the bottom in its current relative order. Drag-and-drop remains
|
|
35
|
+
# available afterward and simply overwrites these positions again, same as it would after a manual reorder.
|
|
36
|
+
def sort_by_start_date
|
|
37
|
+
homepage_section = SpreeCmCommissioner::HomepageSection.find(params[:homepage_section_id])
|
|
38
|
+
relatables = homepage_section.homepage_section_relatables.includes(:relatable).to_a
|
|
39
|
+
|
|
40
|
+
dated, undated = relatables.partition { |relatable| relatable.relatable.is_a?(Spree::Taxon) && relatable.relatable.from_date.present? }
|
|
41
|
+
dated.sort_by! { |relatable| relatable.relatable.from_date }
|
|
42
|
+
undated.sort_by!(&:position)
|
|
43
|
+
|
|
44
|
+
SpreeCmCommissioner::HomepageSectionRelatable.transaction do
|
|
45
|
+
(dated + undated).each.with_index(1) do |relatable, position|
|
|
46
|
+
relatable.update_columns(position: position, updated_at: Time.current) # rubocop:disable Rails/SkipsModelValidations
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
homepage_section.update_columns(updated_at: Time.current) # rubocop:disable Rails/SkipsModelValidations
|
|
50
|
+
|
|
51
|
+
redirect_to edit_admin_homepage_feed_homepage_section_url(homepage_section),
|
|
52
|
+
notice: Spree.t(:successfully_updated, resource: 'Homepage Section Relatable')
|
|
53
|
+
end
|
|
30
54
|
end
|
|
31
55
|
end
|
|
32
56
|
end
|
|
@@ -20,7 +20,7 @@ module Spree
|
|
|
20
20
|
|
|
21
21
|
if guest.save
|
|
22
22
|
@invite_guest.update(claimed_status: :claimed) if @line_item.guests.count == @invite_guest.quantity
|
|
23
|
-
send_guest_claimed_invitation_telegram_alert_to_vendor(guest)
|
|
23
|
+
send_guest_claimed_invitation_telegram_alert_to_vendor(guest)
|
|
24
24
|
|
|
25
25
|
render json: SpreeCmCommissioner::V2::Storefront::GuestSerializer.new(guest.reload).serializable_hash
|
|
26
26
|
else
|
|
@@ -32,7 +32,7 @@ module Spree
|
|
|
32
32
|
|
|
33
33
|
def send_guest_claimed_invitation_telegram_alert_to_vendor(guest)
|
|
34
34
|
title = '📣 --- [NEW GUEST CLAIMED INVITATION] ---' # Style/StringLiterals
|
|
35
|
-
chat_id = guest.event.vendor.preferred_telegram_chat_id
|
|
35
|
+
chat_id = guest.event.telegram_alert_chat_id || guest.event.vendor.preferred_telegram_chat_id
|
|
36
36
|
return if chat_id.blank?
|
|
37
37
|
|
|
38
38
|
factory = SpreeCmCommissioner::InviteGuestClaimedTelegramMessageFactory.new(
|
|
@@ -10,6 +10,14 @@ module Spree
|
|
|
10
10
|
base.attr_reader :schema
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
# override: Spree wraps this in a Rails.cache.fetch keyed on ids + max updated_at (1h TTL),
|
|
14
|
+
# which goes stale when only an association changes. See ContentCachable#served_to_cdn?.
|
|
15
|
+
def index
|
|
16
|
+
return render_serialized_payload { serialize_collection(paginated_collection) } if served_to_cdn?
|
|
17
|
+
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
|
|
13
21
|
def app_version
|
|
14
22
|
request.headers['X-Cm-App-Version']
|
|
15
23
|
end
|
|
@@ -8,6 +8,7 @@ module Spree
|
|
|
8
8
|
include SpreeCmCommissioner::AnonymousBookingGuard
|
|
9
9
|
|
|
10
10
|
before_action -> { ensure_anonymous_booking_allowed!(draft_order_products) }, only: :create
|
|
11
|
+
before_action -> { ensure_waiting_room_session!(draft_order_products) }, only: :create
|
|
11
12
|
|
|
12
13
|
# A top-up credits a wallet, which must belong to a real user -- unlike #create,
|
|
13
14
|
# this action has no guest-checkout path.
|
|
@@ -124,9 +125,12 @@ module Spree
|
|
|
124
125
|
).fetch(:line_items, [])
|
|
125
126
|
end
|
|
126
127
|
|
|
128
|
+
# Memoized: both the anonymous-booking and waiting-room guards read it on the same request.
|
|
127
129
|
def draft_order_products
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
@draft_order_products ||= begin
|
|
131
|
+
variant_ids = draft_order_line_items.pluck(:variant_id).compact
|
|
132
|
+
Spree::Variant.where(id: variant_ids).includes(product: :event).map(&:product)
|
|
133
|
+
end
|
|
130
134
|
end
|
|
131
135
|
|
|
132
136
|
# override
|
|
@@ -32,7 +32,7 @@ module Spree
|
|
|
32
32
|
|
|
33
33
|
def send_guest_claimed_invitation_telegram_alert_to_vendor(guest)
|
|
34
34
|
title = '📣 --- [NEW GUEST CLAIMED INVITATION] ---' # Style/StringLiterals
|
|
35
|
-
chat_id = guest.event&.vendor&.preferred_telegram_chat_id
|
|
35
|
+
chat_id = guest.event&.telegram_alert_chat_id || guest.event&.vendor&.preferred_telegram_chat_id
|
|
36
36
|
return if chat_id.blank?
|
|
37
37
|
|
|
38
38
|
factory = SpreeCmCommissioner::InviteGuestClaimedTelegramMessageFactory.new(
|
|
@@ -97,7 +97,7 @@ module Spree
|
|
|
97
97
|
def finalize_guest_claim(guest)
|
|
98
98
|
if guest.save
|
|
99
99
|
@invite_guest.update(claimed_status: :claimed) if @line_item.guests.count == @invite_guest.quantity
|
|
100
|
-
send_guest_claimed_invitation_telegram_alert_to_vendor(guest)
|
|
100
|
+
send_guest_claimed_invitation_telegram_alert_to_vendor(guest)
|
|
101
101
|
|
|
102
102
|
order = @invite_guest.order
|
|
103
103
|
render_serialized_payload { serialize_resource(order) }
|
|
@@ -9,6 +9,7 @@ module Spree
|
|
|
9
9
|
include SpreeCmCommissioner::AnonymousBookingGuard
|
|
10
10
|
|
|
11
11
|
before_action -> { ensure_anonymous_booking_allowed!(draft_order_products) }, only: :create
|
|
12
|
+
before_action -> { ensure_waiting_room_session!(draft_order_products) }, only: :create
|
|
12
13
|
|
|
13
14
|
# Accepts a draft order request without building it: writes a queued Firestore status
|
|
14
15
|
# document and schedules the real build (SpreeCmCommissioner::DraftOrders::Create) on a
|
|
@@ -59,9 +60,12 @@ module Spree
|
|
|
59
60
|
.fetch(:line_items, [])
|
|
60
61
|
end
|
|
61
62
|
|
|
63
|
+
# Memoized: both the anonymous-booking and waiting-room guards read it on the same request.
|
|
62
64
|
def draft_order_products
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
@draft_order_products ||= begin
|
|
66
|
+
variant_ids = draft_order_line_items.pluck(:variant_id).compact
|
|
67
|
+
Spree::Variant.where(id: variant_ids).includes(product: :event).map(&:product)
|
|
68
|
+
end
|
|
65
69
|
end
|
|
66
70
|
end
|
|
67
71
|
end
|
|
@@ -6,25 +6,30 @@ module Spree
|
|
|
6
6
|
CACHE_EXPIRES_IN = 5.minutes
|
|
7
7
|
|
|
8
8
|
def index
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
trips = SpreeCmCommissioner::TripQuery.new(
|
|
12
|
-
origin_id: params[:origin_id],
|
|
13
|
-
destination_id: params[:destination_id],
|
|
14
|
-
date: params[:date],
|
|
15
|
-
route_type: params[:route_type],
|
|
16
|
-
vendor_id: params[:vendor_id],
|
|
17
|
-
number_of_guests: params[:number_of_guests],
|
|
18
|
-
options: params
|
|
19
|
-
).call
|
|
9
|
+
# CloudFront already caches this action; see ContentCachable#served_to_cdn?.
|
|
10
|
+
return render_serialized_payload { search_and_serialize } if served_to_cdn?
|
|
20
11
|
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
render_serialized_payload do
|
|
13
|
+
Rails.cache.fetch(collection_cache_key, collection_cache_opts) { search_and_serialize }
|
|
23
14
|
end
|
|
24
15
|
end
|
|
25
16
|
|
|
26
17
|
private
|
|
27
18
|
|
|
19
|
+
def search_and_serialize
|
|
20
|
+
trips = SpreeCmCommissioner::TripQuery.new(
|
|
21
|
+
origin_id: params[:origin_id],
|
|
22
|
+
destination_id: params[:destination_id],
|
|
23
|
+
date: params[:date],
|
|
24
|
+
route_type: params[:route_type],
|
|
25
|
+
vendor_id: params[:vendor_id],
|
|
26
|
+
number_of_guests: params[:number_of_guests],
|
|
27
|
+
options: params
|
|
28
|
+
).call
|
|
29
|
+
|
|
30
|
+
serialize_collection(trips)
|
|
31
|
+
end
|
|
32
|
+
|
|
28
33
|
# override
|
|
29
34
|
def collection_cache_key
|
|
30
35
|
cache_key_parts = [
|
|
@@ -6,6 +6,7 @@ module Spree
|
|
|
6
6
|
include SpreeCmCommissioner::AnonymousBookingGuard
|
|
7
7
|
|
|
8
8
|
before_action -> { ensure_anonymous_booking_allowed!(draft_order_products) }, only: :create
|
|
9
|
+
before_action -> { ensure_waiting_room_session!(draft_order_products) }, only: :create
|
|
9
10
|
|
|
10
11
|
# A top-up credits a wallet, which must belong to a real user -- unlike #create,
|
|
11
12
|
# this action has no guest-checkout path.
|
|
@@ -90,9 +91,12 @@ module Spree
|
|
|
90
91
|
.fetch(:line_items, [])
|
|
91
92
|
end
|
|
92
93
|
|
|
94
|
+
# Memoized: both the anonymous-booking and waiting-room guards read it on the same request.
|
|
93
95
|
def draft_order_products
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
@draft_order_products ||= begin
|
|
97
|
+
variant_ids = draft_order_line_items.pluck(:variant_id).compact
|
|
98
|
+
Spree::Variant.where(id: variant_ids).includes(product: :event).map(&:product)
|
|
99
|
+
end
|
|
96
100
|
end
|
|
97
101
|
|
|
98
102
|
# override
|
|
@@ -6,25 +6,30 @@ module Spree
|
|
|
6
6
|
CACHE_EXPIRES_IN = 5.minutes
|
|
7
7
|
|
|
8
8
|
def index
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
trips = SpreeCmCommissioner::TripQuery.new(
|
|
12
|
-
origin_id: params[:origin_id],
|
|
13
|
-
destination_id: params[:destination_id],
|
|
14
|
-
date: params[:date],
|
|
15
|
-
route_type: params[:route_type],
|
|
16
|
-
number_of_guests: params[:number_of_guests],
|
|
17
|
-
tenant_id: @tenant.id,
|
|
18
|
-
options: params
|
|
19
|
-
).call
|
|
9
|
+
# CloudFront already caches this action; see ContentCachable#served_to_cdn?.
|
|
10
|
+
return render_serialized_payload { search_and_serialize } if served_to_cdn?
|
|
20
11
|
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
render_serialized_payload do
|
|
13
|
+
Rails.cache.fetch(collection_cache_key, collection_cache_opts) { search_and_serialize }
|
|
23
14
|
end
|
|
24
15
|
end
|
|
25
16
|
|
|
26
17
|
private
|
|
27
18
|
|
|
19
|
+
def search_and_serialize
|
|
20
|
+
trips = SpreeCmCommissioner::TripQuery.new(
|
|
21
|
+
origin_id: params[:origin_id],
|
|
22
|
+
destination_id: params[:destination_id],
|
|
23
|
+
date: params[:date],
|
|
24
|
+
route_type: params[:route_type],
|
|
25
|
+
number_of_guests: params[:number_of_guests],
|
|
26
|
+
tenant_id: @tenant.id,
|
|
27
|
+
options: params
|
|
28
|
+
).call
|
|
29
|
+
|
|
30
|
+
serialize_collection(trips)
|
|
31
|
+
end
|
|
32
|
+
|
|
28
33
|
# override
|
|
29
34
|
def collection_cache_key
|
|
30
35
|
cache_key_parts = [
|