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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +7 -1
  3. data/Gemfile.lock +1 -1
  4. data/app/controllers/concerns/spree_cm_commissioner/content_cachable.rb +59 -3
  5. data/app/controllers/concerns/spree_cm_commissioner/waiting_room_authorization.rb +41 -8
  6. data/app/controllers/spree/admin/api_caches_controller.rb +23 -8
  7. data/app/controllers/spree/admin/homepage_section_controller.rb +63 -5
  8. data/app/controllers/spree/admin/homepage_section_relatable_controller.rb +24 -0
  9. data/app/controllers/spree/api/v2/organizer/invite_guests_controller.rb +2 -2
  10. data/app/controllers/spree/api/v2/resource_controller_decorator.rb +8 -0
  11. data/app/controllers/spree/api/v2/storefront/draft_orders_controller.rb +6 -2
  12. data/app/controllers/spree/api/v2/storefront/homepage_sections_controller.rb +1 -0
  13. data/app/controllers/spree/api/v2/storefront/invite_guests_controller.rb +2 -2
  14. data/app/controllers/spree/api/v2/storefront/preview_sections_controller.rb +1 -0
  15. data/app/controllers/spree/api/v2/storefront/queue_draft_orders_controller.rb +6 -2
  16. data/app/controllers/spree/api/v2/storefront/trip_search_controller.rb +18 -13
  17. data/app/controllers/spree/api/v2/storefront/user_contacts_controller.rb +2 -0
  18. data/app/controllers/spree/api/v2/tenant/draft_orders_controller.rb +6 -2
  19. data/app/controllers/spree/api/v2/tenant/homepage_sections_controller.rb +1 -0
  20. data/app/controllers/spree/api/v2/tenant/preview_sections_controller.rb +1 -0
  21. data/app/controllers/spree/api/v2/tenant/trip_search_controller.rb +18 -13
  22. data/app/factory/spree_cm_commissioner/order_telegram_message_factory.rb +5 -1
  23. data/app/helpers/spree_cm_commissioner/admin/homepage_segment_helper.rb +28 -0
  24. data/app/interactors/spree_cm_commissioner/account_recover.rb +23 -2
  25. data/app/interactors/spree_cm_commissioner/firebase_id_token_provider.rb +6 -2
  26. data/app/interactors/spree_cm_commissioner/telegram_chats_auto_finder.rb +26 -2
  27. data/app/interactors/spree_cm_commissioner/telegram_chats_finder.rb +1 -1
  28. data/app/interactors/spree_cm_commissioner/user_telegram_oauth_authenticator.rb +2 -11
  29. data/app/models/active_storage/variation_decorator.rb +18 -0
  30. data/app/models/concerns/spree_cm_commissioner/event_metadata.rb +4 -0
  31. data/app/models/concerns/spree_cm_commissioner/order_holdable.rb +3 -2
  32. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +18 -13
  33. data/app/models/spree_cm_commissioner/homepage_section.rb +33 -0
  34. data/app/models/spree_cm_commissioner/homepage_section_app_background.rb +13 -0
  35. data/app/models/spree_cm_commissioner/homepage_section_web_background.rb +13 -0
  36. data/app/models/spree_cm_commissioner/maintenance_tasks/cache_invalidation.rb +95 -11
  37. data/app/models/spree_cm_commissioner/pin_code.rb +2 -1
  38. data/app/models/spree_cm_commissioner/pin_code_event_telegram.rb +31 -0
  39. data/app/models/spree_cm_commissioner/product_decorator.rb +7 -0
  40. data/app/models/spree_cm_commissioner/service_calendar.rb +2 -2
  41. data/app/models/spree_cm_commissioner/taxon_decorator.rb +7 -0
  42. data/app/models/spree_cm_commissioner/vendor_decorator.rb +6 -0
  43. data/app/overrides/spree/admin/taxons/_form/enforce_waiting_room_session.html.erb.deface +10 -0
  44. data/app/queries/spree_cm_commissioner/trip_query.rb +20 -2
  45. data/app/serializers/spree/v2/storefront/homepage_section_serializer.rb +4 -1
  46. data/app/serializers/spree/v2/tenant/homepage_section_serializer.rb +3 -1
  47. data/app/services/spree_cm_commissioner/api_caches/fetch_details.rb +128 -0
  48. data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +31 -14
  49. data/app/services/spree_cm_commissioner/api_caches/invalidate_all.rb +16 -19
  50. data/app/services/spree_cm_commissioner/api_caches/request_clear.rb +71 -0
  51. data/app/services/spree_cm_commissioner/guests/claim_invite_guest_service.rb +5 -3
  52. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_service_calendars.rb +3 -0
  53. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trip_stop.rb +21 -4
  54. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trips.rb +30 -4
  55. data/app/services/spree_cm_commissioner/inventory_items/bulk_generate_permanent_items.rb +15 -4
  56. data/app/services/spree_cm_commissioner/line_items/validate_purchase_limits.rb +56 -5
  57. data/app/services/spree_cm_commissioner/telegram/verify_oauth_id_token.rb +10 -0
  58. data/app/services/spree_cm_commissioner/telegram_alerts/send_telegram_order_alerts.rb +49 -0
  59. data/app/views/spree/admin/api_caches/_dialog.html.erb +47 -0
  60. data/app/views/spree/admin/api_caches/show.html.erb +88 -0
  61. data/app/views/spree/admin/homepage_section/_form.html.erb +44 -0
  62. data/app/views/spree/admin/homepage_section/edit.html.erb +16 -1
  63. data/app/views/spree/admin/shared/_api_cache_button.html.erb +5 -2
  64. data/config/initializers/spree_permitted_attributes.rb +1 -0
  65. data/config/locales/dry_validations.en.yml +4 -0
  66. data/config/locales/en.yml +50 -0
  67. data/config/locales/km.yml +50 -0
  68. data/config/routes.rb +8 -4
  69. data/db/migrate/20261021000001_add_section_type_to_cm_homepage_sections.rb +6 -0
  70. data/lib/spree_cm_commissioner/version.rb +1 -1
  71. metadata +13 -3
  72. 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: e5317bd75c6057165a07eae82d0e30f986371767925346783242d8d12a0a8d24
4
- data.tar.gz: b015f2bebfc1105dee65f7b4b22f63ee909b113541741f9dbc5ed48482ab4b04
3
+ metadata.gz: 743a7213a36a799ebd7d68c8e2a770a9a253b9ca9e544c53dc89114344c35eec
4
+ data.tar.gz: c9697afd92b4175c73c1291224522b4b56fdf4056da7c8b4f441b33f0c828af4
5
5
  SHA512:
6
- metadata.gz: 7c59a388a335fcffdbb53ef8ebd470bf9b310a169ac8057ba6b464c6cef8e4ee6425cccfa5bfda366ea9c7fa89aaca854465c914521df040d460ba39994bd0b3
7
- data.tar.gz: 0d8572c942886d56c1689ceb532e2d643f208d538dfb8fa40e21209198562dbe52fb7bb8a28f7573d0900407b8651421210eb4b99c6ea80aefa14b73677880c4
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 disable CDN cache invalidation entirely (kill switch)
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
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.14.4)
37
+ spree_cm_commissioner (2.14.6)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -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
- # CDN (s-maxage) TTL, or nil for an unlisted controller. Override in a controller for a different one.
73
- def shared_max_age
74
- case cdn_cache_config&.dig(:max_age)
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 ENV['WAITING_ROOM_DISABLED'] == 'yes'
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
- return if SpreeCmCommissioner::WaitingRoomSession.active.exists?(token: params[:waiting_room_session_token])
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
- # POST /admin/api_caches/clear?model=Spree::Product&id=1, or without id to clear every record of
7
- # model on one storefront: tenant_id for that tenant's API, none for the BookMe+ storefront.
9
+ before_action :load_tenant
10
+
8
11
  def clear
9
- result = if params[:id].present?
10
- SpreeCmCommissioner::ApiCaches::Invalidate.call(model: params[:model], id: params[:id])
11
- else
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
- .require(:spree_cm_commissioner_homepage_section)
68
- .permit(:title, :description, :active, :preview)
69
- .merge(segment: segment_value, mini_app: mini_app_value)
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) if guest.event.vendor.preferred_telegram_chat_id.present?
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
- variant_ids = draft_order_line_items.pluck(:variant_id).compact
129
- Spree::Variant.where(id: variant_ids).includes(:product).map(&:product)
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
@@ -23,6 +23,7 @@ module Spree
23
23
  .where(preview: false)
24
24
  .for_feed(params[:homepage_id] || :general, params[:mini_app])
25
25
  .order(position: :asc)
26
+ .includes(:app_background, :web_background)
26
27
  end
27
28
  end
28
29
  end
@@ -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) if guest.event&.vendor&.preferred_telegram_chat_id.present?
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) }
@@ -13,6 +13,7 @@ module Spree
13
13
  .where(preview: true)
14
14
  .where(id: SpreeCmCommissioner::HomepageSectionRelatable.section_ids_previewable_by(spree_current_user))
15
15
  .order(position: :asc)
16
+ .includes(:app_background, :web_background)
16
17
  end
17
18
  end
18
19
  end
@@ -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
- variant_ids = draft_order_line_items.pluck(:variant_id).compact
64
- Spree::Variant.where(id: variant_ids).includes(:product).map(&:product)
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
- render_serialized_payload do
10
- Rails.cache.fetch(collection_cache_key, collection_cache_opts) do
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
- serialize_collection(trips)
22
- end
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 = [
@@ -3,6 +3,8 @@ module Spree
3
3
  module V2
4
4
  module Storefront
5
5
  class UserContactsController < Spree::Api::V2::ResourceController
6
+ before_action :require_spree_current_user
7
+
6
8
  def update
7
9
  options = user_with_pin_code_params
8
10
 
@@ -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
- variant_ids = draft_order_line_items.pluck(:variant_id).compact
95
- Spree::Variant.where(id: variant_ids).includes(:product).map(&:product)
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
@@ -8,6 +8,7 @@ module Spree
8
8
  .where(preview: false)
9
9
  .active
10
10
  .order(position: :asc)
11
+ .includes(:app_background, :web_background)
11
12
  end
12
13
 
13
14
  def scope
@@ -12,6 +12,7 @@ module Spree
12
12
  .where(preview: true)
13
13
  .where(id: SpreeCmCommissioner::HomepageSectionRelatable.section_ids_previewable_by(spree_current_user))
14
14
  .order(position: :asc)
15
+ .includes(:app_background, :web_background)
15
16
  end
16
17
  end
17
18
  end
@@ -6,25 +6,30 @@ module Spree
6
6
  CACHE_EXPIRES_IN = 5.minutes
7
7
 
8
8
  def index
9
- render_serialized_payload do
10
- Rails.cache.fetch(collection_cache_key, collection_cache_opts) do
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
- serialize_collection(trips)
22
- end
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 = [