spree_cm_commissioner 2.14.3 → 2.14.4

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 (28) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +4 -6
  3. data/Gemfile.lock +1 -1
  4. data/app/controllers/concerns/spree_cm_commissioner/content_cachable.rb +76 -70
  5. data/app/controllers/spree/admin/api_caches_controller.rb +26 -0
  6. data/app/controllers/spree/admin/base_controller_decorator.rb +0 -14
  7. data/app/controllers/spree/admin/guests_controller.rb +43 -4
  8. data/app/overrides/spree/admin/menus/index/clear_cache_button.html.erb.deface +4 -0
  9. data/app/overrides/spree/admin/payment_methods/index/clear_cache_button.html.erb.deface +6 -0
  10. data/app/overrides/spree/admin/products/edit/clear_cache_button.html.erb.deface +1 -1
  11. data/app/overrides/spree/admin/taxons/edit/clear_cache_button.html.erb.deface +1 -4
  12. data/app/overrides/spree/admin/vendors/edit/clear_cache_button.html.erb.deface +5 -0
  13. data/app/serializers/spree/api/v2/base_serializer_decorator.rb +24 -0
  14. data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +39 -87
  15. data/app/services/spree_cm_commissioner/api_caches/invalidate_all.rb +61 -0
  16. data/app/services/spree_cm_commissioner/guests/build_dynamic_field_attributes.rb +34 -0
  17. data/app/services/spree_cm_commissioner/order/guests/update.rb +2 -24
  18. data/app/views/spree/admin/guests/_dynamic_fields.html.erb +67 -0
  19. data/app/views/spree/admin/guests/_form.html.erb +2 -0
  20. data/app/views/spree/admin/homepage_background/index.html.erb +1 -1
  21. data/app/views/spree/admin/homepage_section/edit.html.erb +0 -1
  22. data/app/views/spree/admin/homepage_section/index.html.erb +1 -1
  23. data/app/views/spree/admin/shared/_api_cache_button.html.erb +5 -0
  24. data/config/locales/en.yml +11 -0
  25. data/config/locales/km.yml +11 -0
  26. data/config/routes.rb +5 -1
  27. data/lib/spree_cm_commissioner/version.rb +1 -1
  28. metadata +10 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c169ca8f3c24e2294f37a17142112c820953fbd3b109c7e53c3a39ccf19e88e4
4
- data.tar.gz: ec8aee4c537d0f1f47b705eb4e248a0b148b124979e743adfd02d1b510b4a9f9
3
+ metadata.gz: e5317bd75c6057165a07eae82d0e30f986371767925346783242d8d12a0a8d24
4
+ data.tar.gz: b015f2bebfc1105dee65f7b4b22f63ee909b113541741f9dbc5ed48482ab4b04
5
5
  SHA512:
6
- metadata.gz: 4cb10cc3987eff0d89409c4275cafa26263a9ed349deacb81c816c3fca496927973a640eb1fe875a4e724eebdc43ecc3cec6f5e8626b7f9653e422704bad6553
7
- data.tar.gz: 1f68fa692abff8ab900ac03cc864b5509270c863b62606b8f2ddca4b98c7f05358b61c82663af1f580ab7a5510f690fc3827d2f07c1cc60c574e6146add476be
6
+ metadata.gz: 7c59a388a335fcffdbb53ef8ebd470bf9b310a169ac8057ba6b464c6cef8e4ee6425cccfa5bfda366ea9c7fa89aaca854465c914521df040d460ba39994bd0b3
7
+ data.tar.gz: 0d8572c942886d56c1689ceb532e2d643f208d538dfb8fa40e21209198562dbe52fb7bb8a28f7573d0900407b8651421210eb4b99c6ea80aefa14b73677880c4
data/.env.example CHANGED
@@ -47,14 +47,12 @@ ORGANIZER_URL=http://127.0.0.1:4000/organizer
47
47
  # spree_cm_commissioner/distance.rb
48
48
  DISTANCE_SIGNING_KEY=hei********************VY
49
49
 
50
- # Cache durations (in seconds) for different content types
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
52
  CACHE_AUTO_INVALIDATION_DISABLED="" # Set to yes to disable CDN cache invalidation entirely (kill switch)
53
- CACHE_STATIC_MAX_AGE=86400 # Static content (countries, provinces, seat layouts, CMS pages) - 1 day
54
- CACHE_SEMI_STATIC_MAX_AGE=3600 # Semi-static content (menus, homepage backgrounds, routes) - 1 hour
55
- CACHE_MODERATE_MAX_AGE=1800 # Moderate freshness (products, events, vendors) - 30 minutes
56
- CACHE_REALTIME_MAX_AGE=300 # High freshness (trips, trip search) - 5 minutes
57
- CACHE_DEFAULT_MAX_AGE=300 # Default for unlisted controllers - 5 minutes
53
+ CACHE_SERIALIZER_DISABLED="" # Set to yes to turn off Spree's serializer record cache everywhere
54
+ CACHE_STATIC_MAX_AGE=86400 # Static content (CMS pages) - 1 day
55
+ CACHE_REALTIME_MAX_AGE=300 # Everything else listed (homepage, menus, products, events, vendors, trips, seat layouts, countries, provinces, guest card classes) - 5 minutes
58
56
 
59
57
  # Batch size for processing maintenance tasks in app/jobs/spree_cm_commissioner/maintenance_tasks/orchestrate_job.rb
60
58
  MAINTENANCE_TASKS_BATCH_SIZE=100
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.14.3)
37
+ spree_cm_commissioner (2.14.4)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -1,5 +1,5 @@
1
1
  # Check following file for spec:
2
- # spec/controllers/application_controller_spec.rb
2
+ # spec/controllers/concerns/spree_cm_commissioner/content_cachable_spec.rb
3
3
  module SpreeCmCommissioner
4
4
  module ContentCachable
5
5
  extend ActiveSupport::Concern
@@ -8,88 +8,94 @@ module SpreeCmCommissioner
8
8
  after_action :set_cache_control_for_cdn
9
9
  end
10
10
 
11
- # Cache duration priorities by content type
12
- # Priority 1: Static/Rarely Changed (1 day)
13
- STATIC_CONTENT_CONTROLLERS = %w[
14
- CountriesController
15
- ProvincesController
16
- GuestCardClassesController
17
- SeatLayoutsController
18
- CmsPagesController
19
- AdvertisementsController
20
- ].freeze
11
+ # Every CDN-cached API, by full class name, with its GET actions and TTL tier. Anything not
12
+ # listed keeps Rails' default `max-age=0, private, must-revalidate` and never reaches the CDN.
13
+ #
14
+ # :static (CACHE_STATIC_MAX_AGE, 1 day) is for rarely changed content. :fresh
15
+ # (CACHE_REALTIME_MAX_AGE, 5 minutes) is for anything editors change, short enough that an edit
16
+ # shows up without a CloudFront invalidation.
17
+ #
18
+ # Full names, not demodulized ones: this concern runs on every ActionController::API, and short
19
+ # names collide with admin-only APIs (Platform, Operator, V1, webhooks) that must never be marked
20
+ # public. Never list an action whose response depends on the signed-in user, e.g.
21
+ # Tenant::VotePackagesController#claim_status: the CDN would serve one user's answer to everyone
22
+ # sharing the cache key.
23
+ CDN_CACHEABLE_ACTIONS = {
24
+ # JSON-LD
25
+ 'Spree::Api::JsonLd::VendorsController' => { actions: %w[index show].freeze, max_age: :fresh },
21
26
 
22
- # Priority 2: Semi-Static (1 hour)
23
- SEMI_STATIC_CONTROLLERS = %w[
24
- MenusController
25
- HomepageBackgroundController
26
- HomepageDataController
27
- PopularRoutePlacesController
28
- RoutePlacesController
29
- ShowsController
30
- EpisodesController
31
- ShowPeopleController
32
- ShowPersonAssignmentsController
33
- ShowContestantsController
34
- VotePackagesController
35
- ShowEliminationSessionsController
36
- PaymentMethodsController
37
- ].freeze
27
+ # Storefront
28
+ 'Spree::Api::V2::Storefront::AccommodationsController' => { actions: %w[index show].freeze, max_age: :fresh },
29
+ 'Spree::Api::V2::Storefront::CmsPagesController' => { actions: %w[index show].freeze, max_age: :static },
30
+ 'Spree::Api::V2::Storefront::CountriesController' => { actions: %w[index show].freeze, max_age: :fresh },
31
+ 'Spree::Api::V2::Storefront::DynamicFieldsController' => { actions: %w[index show].freeze, max_age: :fresh },
32
+ 'Spree::Api::V2::Storefront::EventMatchesController' => { actions: %w[index].freeze, max_age: :fresh },
33
+ 'Spree::Api::V2::Storefront::EventsController' => { actions: %w[index show].freeze, max_age: :fresh },
34
+ 'Spree::Api::V2::Storefront::GuestCardClassesController' => { actions: %w[index show].freeze, max_age: :fresh },
35
+ 'Spree::Api::V2::Storefront::HomepageBackgroundController' => { actions: %w[show].freeze, max_age: :fresh },
36
+ 'Spree::Api::V2::Storefront::HomepageDataController' => { actions: %w[show].freeze, max_age: :fresh },
37
+ 'Spree::Api::V2::Storefront::HomepageSectionsController' => { actions: %w[index].freeze, max_age: :fresh },
38
+ 'Spree::Api::V2::Storefront::MenusController' => { actions: %w[index show].freeze, max_age: :fresh },
39
+ 'Spree::Api::V2::Storefront::PaymentMethodsController' => { actions: %w[index].freeze, max_age: :fresh },
40
+ 'Spree::Api::V2::Storefront::PopularRoutePlacesController' => { actions: %w[index].freeze, max_age: :fresh },
41
+ 'Spree::Api::V2::Storefront::ProductsController' => { actions: %w[index show].freeze, max_age: :fresh },
42
+ 'Spree::Api::V2::Storefront::ProvincesController' => { actions: %w[index].freeze, max_age: :fresh },
43
+ 'Spree::Api::V2::Storefront::RoutePlacesController' => { actions: %w[index].freeze, max_age: :fresh },
44
+ 'Spree::Api::V2::Storefront::SeatLayoutsController' => { actions: %w[index show].freeze, max_age: :fresh },
45
+ 'Spree::Api::V2::Storefront::TaxonsController' => { actions: %w[index show].freeze, max_age: :fresh },
46
+ 'Spree::Api::V2::Storefront::TripSearchController' => { actions: %w[index].freeze, max_age: :fresh },
47
+ 'Spree::Api::V2::Storefront::TripsController' => { actions: %w[show].freeze, max_age: :fresh },
48
+ 'Spree::Api::V2::Storefront::VendorPhotosController' => { actions: %w[index show].freeze, max_age: :fresh },
49
+ 'Spree::Api::V2::Storefront::VendorsController' => { actions: %w[index show].freeze, max_age: :fresh },
38
50
 
39
- # Priority 3: Moderate Freshness (30 minutes)
40
- MODERATE_FRESHNESS_CONTROLLERS = %w[
41
- HomepageSectionsController
42
- TaxonsController
43
- ProductsController
44
- EventsController
45
- EventMatchesController
46
- AccommodationsController
47
- VendorsController
48
- VendorPhotosController
49
- ].freeze
51
+ # Tenant
52
+ 'Spree::Api::V2::Tenant::CmsPagesController' => { actions: %w[index show].freeze, max_age: :static },
53
+ 'Spree::Api::V2::Tenant::EpisodesController' => { actions: %w[index show].freeze, max_age: :fresh },
54
+ 'Spree::Api::V2::Tenant::HomepageSectionsController' => { actions: %w[index].freeze, max_age: :fresh },
55
+ 'Spree::Api::V2::Tenant::PaymentMethodsController' => { actions: %w[index].freeze, max_age: :fresh },
56
+ 'Spree::Api::V2::Tenant::PopularRoutePlacesController' => { actions: %w[index].freeze, max_age: :fresh },
57
+ 'Spree::Api::V2::Tenant::ProductsController' => { actions: %w[index show].freeze, max_age: :fresh },
58
+ 'Spree::Api::V2::Tenant::ShowContestantsController' => { actions: %w[show].freeze, max_age: :fresh },
59
+ 'Spree::Api::V2::Tenant::ShowEliminationSessionsController' => { actions: %w[index].freeze, max_age: :fresh },
60
+ 'Spree::Api::V2::Tenant::ShowPeopleController' => { actions: %w[index].freeze, max_age: :fresh },
61
+ 'Spree::Api::V2::Tenant::ShowPersonAssignmentsController' => { actions: %w[index].freeze, max_age: :fresh },
62
+ 'Spree::Api::V2::Tenant::ShowsController' => { actions: %w[show].freeze, max_age: :fresh },
63
+ 'Spree::Api::V2::Tenant::TaxonsController' => { actions: %w[index show].freeze, max_age: :fresh },
64
+ 'Spree::Api::V2::Tenant::TripSearchController' => { actions: %w[index].freeze, max_age: :fresh },
65
+ 'Spree::Api::V2::Tenant::TripsController' => { actions: %w[show].freeze, max_age: :fresh },
66
+ 'Spree::Api::V2::Tenant::VendorPhotosController' => { actions: %w[index].freeze, max_age: :fresh },
67
+ 'Spree::Api::V2::Tenant::VendorsController' => { actions: %w[index show].freeze, max_age: :fresh },
68
+ 'Spree::Api::V2::Tenant::VotePackagesController' => { actions: %w[index].freeze, max_age: :fresh },
69
+ 'Spree::Api::V2::Tenant::VotingContestantsController' => { actions: %w[index].freeze, max_age: :fresh }
70
+ }.freeze
50
71
 
51
- # Priority 4: High Freshness (5 minutes)
52
- HIGH_FRESHNESS_CONTROLLERS = %w[
53
- TripsController
54
- TripSearchController
55
- VotingContestantsController
56
- DynamicFieldsController
57
- ].freeze
58
-
59
- # Override This Method based on your UseCase
60
- # Or let it auto-detect based on controller name
61
- def client_max_age
62
- controller_name = self.class.name.demodulize
63
-
64
- if STATIC_CONTENT_CONTROLLERS.include?(controller_name)
65
- ENV.fetch('CACHE_STATIC_MAX_AGE', 1.day.to_i).to_i
66
- elsif SEMI_STATIC_CONTROLLERS.include?(controller_name)
67
- ENV.fetch('CACHE_SEMI_STATIC_MAX_AGE', 1.hour.to_i).to_i
68
- elsif MODERATE_FRESHNESS_CONTROLLERS.include?(controller_name)
69
- ENV.fetch('CACHE_MODERATE_MAX_AGE', 30.minutes.to_i).to_i
70
- elsif HIGH_FRESHNESS_CONTROLLERS.include?(controller_name)
71
- ENV.fetch('CACHE_REALTIME_MAX_AGE', 5.minutes.to_i).to_i
72
- else
73
- ENV.fetch('CACHE_DEFAULT_MAX_AGE', 0.minutes.to_i).to_i
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)
75
+ when :static then ENV.fetch('CACHE_STATIC_MAX_AGE', 1.day.to_i).to_i
76
+ when :fresh then ENV.fetch('CACHE_REALTIME_MAX_AGE', 5.minutes.to_i).to_i
74
77
  end
75
78
  end
76
79
 
77
- # Defaults to the client TTL so a tier's max-age bounds CDN staleness too: most endpoints
78
- # (e.g. trips, trip search) have no invalidation on change, and an unlisted controller
79
- # (max-age 0) must not be CDN-cached at all.
80
- def shared_max_age
81
- client_max_age
80
+ def cdn_cacheable?
81
+ request.get? && cdn_cache_config.present? && cdn_cache_config[:actions].include?(action_name)
82
82
  end
83
83
 
84
84
  def set_cache_control_for_cdn
85
85
  return if response.committed?
86
86
  return if response.status != 200
87
+ return unless cdn_cacheable?
88
+
89
+ # max-age=0 makes the app revalidate with CloudFront on every request, since a CloudFront
90
+ # invalidation can never reach the copy already on a device. Rack::ETag tags each response,
91
+ # so a revalidation is a small 304. s-maxage alone decides how stale a response can get.
92
+ response.headers['Cache-Control'] = "public, max-age=0, s-maxage=#{shared_max_age}"
93
+ end
87
94
 
88
- # max-age: browser/client cache, s-maxage: CDN cache
89
- response.headers['Cache-Control'] = "public, max-age=#{client_max_age}, s-maxage=#{shared_max_age}"
95
+ private
90
96
 
91
- # Expires: Absolute timestamp when cache becomes stale (HTTP/1.0 fallback)
92
- response.headers['Expires'] = (Time.zone.now + client_max_age).httpdate
97
+ def cdn_cache_config
98
+ CDN_CACHEABLE_ACTIONS[self.class.name.to_s]
93
99
  end
94
100
  end
95
101
  end
@@ -0,0 +1,26 @@
1
+ module Spree
2
+ module Admin
3
+ # Behind every admin clear-cache button (spree/admin/shared/_api_cache_button). Admin-only through
4
+ # BaseController#authorize_admin, which checks :api_caches since there is no model class.
5
+ 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.
8
+ 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
15
+
16
+ if result.success?
17
+ flash[:success] = Spree.t(:clear_cache_requested)
18
+ else
19
+ flash[:error] = result.error.to_s
20
+ end
21
+
22
+ redirect_back fallback_location: admin_root_path
23
+ end
24
+ end
25
+ end
26
+ end
@@ -31,20 +31,6 @@ module Spree
31
31
  nil
32
32
  end
33
33
 
34
- # POST
35
- def invalidate_api_caches
36
- if params[:model].present?
37
- result = SpreeCmCommissioner::ApiCaches::Invalidate.call(
38
- model: params[:model],
39
- id: params[:id]
40
- )
41
-
42
- flash[:error] = result.error if result.failure?
43
- end
44
-
45
- redirect_back fallback_location: admin_root_path
46
- end
47
-
48
34
  private
49
35
 
50
36
  # See SpreeCmCommissioner::Current#actor -- lets StoreCreditEventDecorator's after_create
@@ -6,6 +6,7 @@ module Spree
6
6
 
7
7
  before_action :load_order
8
8
  before_action :load_guest, only: %i[check_in uncheck_in]
9
+ before_action :load_edit_data, only: :edit
9
10
 
10
11
  def model_class
11
12
  SpreeCmCommissioner::Guest
@@ -41,10 +42,17 @@ module Spree
41
42
  @kyc_fields = @line_item.kyc_fields
42
43
  end
43
44
 
44
- def edit
45
- @kyc_fields = @object.line_item.kyc_fields
46
- @check_ins = @object.check_ins.includes(:check_in_session, :check_in_by)
47
- @eligible_check_in_sessions = @object.eligible_check_in_sessions.order(:position)
45
+ def update
46
+ result = SpreeCmCommissioner::Guests::Update.call(guest: @object, guest_params: update_params)
47
+
48
+ if result.success?
49
+ flash[:success] = flash_message_for(@object, :successfully_updated)
50
+ redirect_to location_after_save
51
+ else
52
+ flash.now[:error] = result.error.to_s
53
+ load_edit_data
54
+ render :edit, status: :unprocessable_entity
55
+ end
48
56
  end
49
57
 
50
58
  def check_in
@@ -88,6 +96,37 @@ module Spree
88
96
  )
89
97
  end
90
98
 
99
+ # Saved through Guests::Update's single transaction so a failing custom field does not leave
100
+ # KYC or seat_number half-saved.
101
+ def update_params
102
+ attrs = permitted_resource_params.to_h.symbolize_keys
103
+ dynamic_fields = submitted_dynamic_fields
104
+ return attrs if dynamic_fields.blank?
105
+
106
+ attrs.merge(
107
+ guest_dynamic_fields_attributes: SpreeCmCommissioner::Guests::BuildDynamicFieldAttributes.call(
108
+ guest: @object, dynamic_fields: dynamic_fields
109
+ )
110
+ )
111
+ end
112
+
113
+ # The form lists every field of every stage, so blank scalar answers mean "not filled in" and
114
+ # are skipped rather than failing the presence validation (clearing an answer is not supported).
115
+ def submitted_dynamic_fields
116
+ raw = params.require(object_name).permit(dynamic_fields: {})[:dynamic_fields]
117
+
118
+ raw.to_h.reject { |_id, value| value.is_a?(String) && value.strip.empty? }
119
+ end
120
+
121
+ def load_edit_data
122
+ @kyc_fields = @object.line_item.kyc_fields
123
+ @check_ins = @object.check_ins.includes(:check_in_session, :check_in_by)
124
+ @eligible_check_in_sessions = @object.eligible_check_in_sessions.order(:position)
125
+ @dynamic_fields = @object.line_item.product.dynamic_fields.includes(:dynamic_field_options)
126
+ .reorder(:data_fill_stage, :position)
127
+ @guest_dynamic_fields = @object.guest_dynamic_fields.to_a
128
+ end
129
+
91
130
  def load_guest
92
131
  @guest = @order.guests.find(params[:id])
93
132
  end
@@ -0,0 +1,4 @@
1
+ <!-- insert_after "erb[silent]:contains('content_for :page_actions')" -->
2
+
3
+ <%# Menus are only served through homepage_data, whatever the menu, so one clear covers them all. %>
4
+ <%= render 'spree/admin/shared/api_cache_button', model: Spree::Menu.name %>
@@ -0,0 +1,6 @@
1
+ <!-- insert_after "erb[silent]:contains('content_for :page_actions')" -->
2
+
3
+ <%# A payment method's paths don't depend on the record (vendor_id is a query param), so one clear
4
+ on the list covers them all. This admin lists BookMe+'s storefront; a tenant clears its own
5
+ from the dashboard. %>
6
+ <%= render 'spree/admin/shared/api_cache_button', model: Spree::PaymentMethod.name %>
@@ -1,3 +1,3 @@
1
1
  <!-- insert_after "erb[silent]:contains('content_for :page_actions')" -->
2
2
 
3
- <%= button_link_to Spree.t(:clear_cache), admin_invalidate_api_caches_path(model: Spree::Product.name, id: @product.id), method: :post, class: "btn btn-outline-primary" %>
3
+ <%= render 'spree/admin/shared/api_cache_button', model: Spree::Product.name, id: @product.id %>
@@ -1,8 +1,5 @@
1
1
  <!-- insert_after "erb[silent]:contains('content_for :page_actions')" -->
2
2
 
3
3
  <% if @taxon.depth == 1 %>
4
- <%= button_link_to Spree.t(:clear_cache),
5
- admin_invalidate_api_caches_path(model: Spree::Taxon.name, id: @taxon.id),
6
- method: :post,
7
- class: "btn btn-outline-primary" %>
4
+ <%= render 'spree/admin/shared/api_cache_button', model: Spree::Taxon.name, id: @taxon.id %>
8
5
  <% end %>
@@ -0,0 +1,5 @@
1
+ <!-- insert_before "erb[loud]:contains('form_for [:admin, @vendor]')" -->
2
+
3
+ <% content_for :page_actions do %>
4
+ <%= render 'spree/admin/shared/api_cache_button', model: Spree::Vendor.name, id: @vendor.id %>
5
+ <% end %>
@@ -0,0 +1,24 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ # Spree caches every serializer's records keyed by each record's updated_at, so anything
5
+ # computed from other tables stays stale, and behind CloudFront an invalidation can't purge
6
+ # it. CACHE_SERIALIZER_DISABLED=yes turns it off everywhere.
7
+ module BaseSerializerDecorator
8
+ def self.prepended(base)
9
+ base.singleton_class.prepend(ClassMethods)
10
+ end
11
+
12
+ module ClassMethods
13
+ # jsonapi-serializer copies Spree's store into each subclass as it is defined, so this
14
+ # reader, which every subclass resolves through, is the one place to turn it off.
15
+ def cache_store_instance
16
+ ENV['CACHE_SERIALIZER_DISABLED'] == 'yes' ? nil : super
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ Spree::Api::V2::BaseSerializer.prepend(Spree::Api::V2::BaseSerializerDecorator)
@@ -1,5 +1,6 @@
1
1
  module SpreeCmCommissioner
2
2
  module ApiCaches
3
+ # Clears one record's cached API responses. Every record of a model goes through InvalidateAll.
3
4
  class Invalidate
4
5
  prepend ::Spree::ServiceModule::Base
5
6
 
@@ -16,17 +17,39 @@ module SpreeCmCommissioner
16
17
  'SpreeCmCommissioner::VotingSession' => SpreeCmCommissioner::VotingSession
17
18
  }.freeze
18
19
 
19
- # eg. 'Spree::Product', 123 or 'Spree::Product', nil
20
- def call(model:, id: nil)
20
+ # CloudFront bills per path and only allows `*` at the end, so one prefix per namespace covers
21
+ # every segment's homepage_sections and homepage_background, plus homepage_data, instead of a
22
+ # path per BIT_SEGMENT key.
23
+ HOMEPAGE_SECTION_PATTERNS = [
24
+ '/api/v2/storefront/homepage*',
25
+ '/api/v2/tenant/homepage*'
26
+ ].freeze
27
+
28
+ HOMEPAGE_BACKGROUND_PATTERNS = ['/api/v2/storefront/homepage*'].freeze
29
+
30
+ # Deliberately broad, and identical for single-record and all-record
31
+ # invalidation: a payment method is either vendor-owned or store-level, and a
32
+ # store-level one is served for every vendor through the fallback in
33
+ # SpreeCmCommissioner::PaymentMethods::Find. From the record alone we cannot
34
+ # tell which vendors are affected.
35
+ #
36
+ # vendor_id is a query param, so the trailing `*` is what clears every
37
+ # vendor's cached variant of this path in one go.
38
+ #
39
+ # Payment methods change a handful of times a year, so a broad invalidation
40
+ # is cheap.
41
+ PAYMENT_METHOD_PATTERNS = [
42
+ '/api/v2/tenant/payment_methods*',
43
+ '/api/v2/storefront/payment_methods*'
44
+ ].freeze
45
+
46
+ # eg. 'Spree::Product', 123
47
+ def call(model:, id:)
21
48
  model_class = SUPPORTED_MODELS[model]
22
49
 
23
50
  return failure(nil, 'Model is not supported') if model_class.nil?
24
51
 
25
- if id.present?
26
- invalidate_single_record(model_class: model_class, id: id)
27
- else
28
- invalidate_all_records(model_class: model_class)
29
- end
52
+ invalidate_single_record(model_class: model_class, id: id)
30
53
  end
31
54
 
32
55
  # eg. 'Spree::Product', 123 => ['/api/v2/tenant/products/123', '/api/v2/tenant/products/slug-of-product', ...]
@@ -38,7 +61,10 @@ module SpreeCmCommissioner
38
61
 
39
62
  refresh_record_cache_key(record)
40
63
 
41
- api_patterns = single_api_pattern[record.class.name].map do |pattern|
64
+ # The record's own class first (a Show found as a Spree::Taxon keeps its show paths), then the
65
+ # model asked for, since STI subclasses such as Spree::Gateway::Payway have no entry of their own.
66
+ patterns = single_api_pattern[record.class.name] || single_api_pattern[model_class.name]
67
+ api_patterns = patterns.map do |pattern|
42
68
  pattern.gsub(/:(\w+)/) do
43
69
  key = Regexp.last_match(1)
44
70
  record.public_send(key).to_s
@@ -50,26 +76,13 @@ module SpreeCmCommissioner
50
76
  success(record: record, api_patterns: api_patterns)
51
77
  end
52
78
 
53
- # eg. 'Spree::Product' => ['/api/v2/tenant/products/*', '/api/v2/storefront/products/*']
54
- def invalidate_all_records(model_class:)
55
- model_class.find_each do |record|
56
- refresh_record_cache_key(record)
57
- end
58
-
59
- enqueue_invalidate(invalidate_uris[model_class.name])
60
-
61
- success(api_patterns: invalidate_uris[model_class.name])
62
- end
63
-
64
79
  def single_api_pattern
65
80
  {
66
- 'SpreeCmCommissioner::HomepageSection' => homepage_section_patterns,
67
- 'SpreeCmCommissioner::HomepageBackground' => homepage_background_patterns,
81
+ 'SpreeCmCommissioner::HomepageSection' => HOMEPAGE_SECTION_PATTERNS,
82
+ 'SpreeCmCommissioner::HomepageBackground' => HOMEPAGE_BACKGROUND_PATTERNS,
68
83
  'SpreeCmCommissioner::SeatLayout' => ['/api/v2/storefront/seat_layouts/:id*'],
69
- 'Spree::Menu' => [
70
- '/api/v2/storefront/menus/:id*',
71
- '/api/v2/storefront/homepage_data*'
72
- ],
84
+ # The app reads menus from homepage_data; nothing calls /menus any more.
85
+ 'Spree::Menu' => ['/api/v2/storefront/homepage_data*'],
73
86
  'Spree::Taxon' => [
74
87
  '/api/v2/tenant/taxons/:id*',
75
88
  '/api/v2/tenant/taxons/:permalink*',
@@ -92,50 +105,12 @@ module SpreeCmCommissioner
92
105
  '/api/v2/storefront/accommodations/:id*',
93
106
  '/api/v2/storefront/accommodations/:slug*'
94
107
  ],
95
- 'Spree::PaymentMethod' => payment_method_patterns
96
- }
97
- end
98
-
99
- def invalidate_uris
100
- {
101
- 'SpreeCmCommissioner::HomepageSection' => homepage_section_patterns,
102
- 'SpreeCmCommissioner::HomepageBackground' => homepage_background_patterns,
103
- 'SpreeCmCommissioner::SeatLayout' => ['/api/v2/storefront/seat_layouts/*'],
104
- 'Spree::Menu' => ['/api/v2/storefront/menus/*'],
105
- 'Spree::Taxon' => ['/api/v2/storefront/taxons/*'],
106
- 'Spree::Product' => [
107
- '/api/v2/tenant/products/*',
108
- '/api/v2/storefront/products/*'
109
- ],
110
- 'Spree::Vendor' => [
111
- '/api/v2/tenant/vendors/*',
112
- '/api/v2/storefront/vendors/*',
113
- '/api/v2/storefront/accommodations/*'
114
- ],
115
- 'Spree::PaymentMethod' => payment_method_patterns
108
+ 'Spree::PaymentMethod' => PAYMENT_METHOD_PATTERNS
116
109
  }
117
110
  end
118
111
 
119
112
  private
120
113
 
121
- # Deliberately broad, and identical for single-record and all-record
122
- # invalidation: a payment method is either vendor-owned or store-level, and a
123
- # store-level one is served for every vendor through the fallback in
124
- # SpreeCmCommissioner::PaymentMethods::Find. From the record alone we cannot
125
- # tell which vendors are affected.
126
- #
127
- # vendor_id is a query param, so the trailing `*` is what clears every
128
- # vendor's cached variant of this path in one go.
129
- #
130
- # Payment methods change a handful of times a year, so a broad invalidation
131
- # is cheap.
132
- def payment_method_patterns
133
- [
134
- '/api/v2/tenant/payment_methods*',
135
- '/api/v2/storefront/payment_methods*'
136
- ]
137
- end
138
-
139
114
  # eg. updated_at: Fri, 26 Dec 2025 10:40:56.505450000 +07 +07:00
140
115
  def refresh_record_cache_key(record)
141
116
  # rubocop:disable Rails/SkipsModelValidations
@@ -147,29 +122,6 @@ module SpreeCmCommissioner
147
122
  def enqueue_invalidate(patterns)
148
123
  SpreeCmCommissioner::InvalidateCacheRequestJob.perform_later(patterns: patterns)
149
124
  end
150
-
151
- # Every key of BIT_SEGMENT (general, ticket, ..., ferry).
152
- def homepage_segment
153
- SpreeCmCommissioner::HomepageSectionBitwise::BIT_SEGMENT.keys
154
- end
155
-
156
- def homepage_section_patterns
157
- homepage_segment.flat_map do |segment|
158
- [
159
- "/api/v2/storefront/homepage/#{segment}/homepage_sections*",
160
- "/api/v2/tenant/homepage/#{segment}/homepage_sections*"
161
- ]
162
- end
163
- end
164
-
165
- def homepage_background_patterns
166
- patterns = homepage_segment.map do |segment|
167
- "/api/v2/storefront/homepage/#{segment}/homepage_background*"
168
- end
169
-
170
- patterns << '/api/v2/storefront/homepage_data*'
171
- patterns
172
- end
173
125
  end
174
126
  end
175
127
  end
@@ -0,0 +1,61 @@
1
+ module SpreeCmCommissioner
2
+ module ApiCaches
3
+ # Clears every record of a model for one storefront. `tenant` is required so a clear-all never
4
+ # pays for both namespaces by accident: nil is the BookMe+ storefront (/api/v2/storefront/
5
+ # paths), a SpreeCmCommissioner::Tenant is that tenant's API (/api/v2/tenant/ paths).
6
+ #
7
+ # Tenant APIs take the tenant from the OAuth token, not the URL, so every tenant shares one path
8
+ # today; taking the record keeps callers the same if tenant paths ever carry it.
9
+ #
10
+ # eg. InvalidateAll.call(model: 'SpreeCmCommissioner::HomepageSection', tenant: nil)
11
+ class InvalidateAll
12
+ prepend ::Spree::ServiceModule::Base
13
+
14
+ TENANT_PREFIX = '/api/v2/tenant/'.freeze
15
+
16
+ API_PATTERNS = {
17
+ 'SpreeCmCommissioner::HomepageSection' => Invalidate::HOMEPAGE_SECTION_PATTERNS,
18
+ 'SpreeCmCommissioner::HomepageBackground' => Invalidate::HOMEPAGE_BACKGROUND_PATTERNS,
19
+ 'SpreeCmCommissioner::SeatLayout' => ['/api/v2/storefront/seat_layouts/*'],
20
+ 'Spree::Menu' => ['/api/v2/storefront/homepage_data*'],
21
+ 'Spree::Taxon' => ['/api/v2/storefront/taxons/*'],
22
+ 'Spree::Product' => [
23
+ '/api/v2/tenant/products/*',
24
+ '/api/v2/storefront/products/*'
25
+ ],
26
+ 'Spree::Vendor' => [
27
+ '/api/v2/tenant/vendors/*',
28
+ '/api/v2/storefront/vendors/*',
29
+ '/api/v2/storefront/accommodations/*'
30
+ ],
31
+ 'Spree::PaymentMethod' => Invalidate::PAYMENT_METHOD_PATTERNS
32
+ }.freeze
33
+
34
+ def call(model:, tenant:)
35
+ model_class = Invalidate::SUPPORTED_MODELS[model] if API_PATTERNS.key?(model)
36
+ return failure(nil, 'Model is not supported') if model_class.nil?
37
+ return failure(nil, 'tenant must be a SpreeCmCommissioner::Tenant or nil') unless tenant.nil? || tenant.is_a?(SpreeCmCommissioner::Tenant)
38
+
39
+ refresh_records_cache_key(model_class, tenant)
40
+
41
+ api_patterns = API_PATTERNS[model].select { |pattern| pattern.start_with?(TENANT_PREFIX) == tenant.present? }
42
+
43
+ # Empty when the model has no API in this namespace, e.g. homepage backgrounds for a tenant,
44
+ # and CloudFront rejects an empty batch.
45
+ SpreeCmCommissioner::InvalidateCacheRequestJob.perform_later(patterns: api_patterns) if api_patterns.any?
46
+
47
+ success(api_patterns: api_patterns)
48
+ end
49
+
50
+ private
51
+
52
+ # updated_at keys Spree's serializer and collection caches, so only this storefront's records
53
+ # are touched when the model is tenant-scoped.
54
+ def refresh_records_cache_key(model_class, tenant)
55
+ scope = model_class.column_names.include?('tenant_id') ? model_class.where(tenant_id: tenant&.id) : model_class.all
56
+
57
+ scope.find_each(&:touch)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,34 @@
1
+ module SpreeCmCommissioner
2
+ module Guests
3
+ # Option-based fields (checkbox/radio/selection) become one GuestDynamicField row per selected
4
+ # option id; Guests::Update's own auto-cleanup destroys rows for deselected options. Fields not
5
+ # enabled on the guest's product are dropped.
6
+ class BuildDynamicFieldAttributes
7
+ def self.call(guest:, dynamic_fields:)
8
+ new(guest, dynamic_fields).call
9
+ end
10
+
11
+ def initialize(guest, dynamic_fields)
12
+ @guest = guest
13
+ @dynamic_fields = dynamic_fields
14
+ end
15
+
16
+ def call
17
+ return [] if @dynamic_fields.blank?
18
+
19
+ enabled_fields = @guest.line_item.product.dynamic_fields.index_by(&:id)
20
+
21
+ @dynamic_fields.flat_map do |dynamic_field_id, value|
22
+ dynamic_field = enabled_fields[dynamic_field_id.to_s.to_i]
23
+ next [] unless dynamic_field
24
+
25
+ if dynamic_field.requires_dynamic_field_options?
26
+ Array(value).compact_blank.map { |option_id| { dynamic_field_id: dynamic_field.id, dynamic_field_option_id: option_id } }
27
+ else
28
+ [{ dynamic_field_id: dynamic_field.id, value: value }]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -21,32 +21,10 @@ module SpreeCmCommissioner
21
21
  editable_attrs = guest.kyc_fields.flat_map { |field| SpreeCmCommissioner::KycBitwise::KYC_FIELD_ATTRIBUTES[field] || [] }
22
22
 
23
23
  filtered = attrs.slice(*editable_attrs)
24
- filtered[:guest_dynamic_fields_attributes] = build_dynamic_field_attributes(guest, attrs[:dynamic_fields])
24
+ filtered[:guest_dynamic_fields_attributes] =
25
+ SpreeCmCommissioner::Guests::BuildDynamicFieldAttributes.call(guest: guest, dynamic_fields: attrs[:dynamic_fields])
25
26
  filtered
26
27
  end
27
-
28
- # Builds the guest_dynamic_fields_attributes array SpreeCmCommissioner::Guests::Update
29
- # expects, restricted to dynamic fields actually enabled for this guest's product (one
30
- # `pluck`/load, not a query per submitted field). Option-based fields (checkbox/
31
- # radio/selection) submit one or more selected option ids and become one
32
- # GuestDynamicField row per selected option; Guests::Update's own auto-cleanup
33
- # destroys any existing rows for options that were deselected.
34
- def build_dynamic_field_attributes(guest, dynamic_fields)
35
- return [] if dynamic_fields.blank?
36
-
37
- enabled_fields = guest.line_item.product.dynamic_fields.index_by(&:id)
38
-
39
- dynamic_fields.flat_map do |dynamic_field_id, value|
40
- dynamic_field = enabled_fields[dynamic_field_id.to_s.to_i]
41
- next [] unless dynamic_field
42
-
43
- if dynamic_field.requires_dynamic_field_options?
44
- Array(value).compact_blank.map { |option_id| { dynamic_field_id: dynamic_field.id, dynamic_field_option_id: option_id } }
45
- else
46
- [{ dynamic_field_id: dynamic_field.id, value: value }]
47
- end
48
- end
49
- end
50
28
  end
51
29
  end
52
30
  end
@@ -0,0 +1,67 @@
1
+ <% answers_by_field_id = @guest_dynamic_fields.group_by(&:dynamic_field_id) %>
2
+ <% i18n_scope = 'views.admin.guests.dynamic_fields' %>
3
+
4
+ <div class="row">
5
+ <div class="col-12">
6
+ <div class="card mb-3">
7
+ <div class="card-header">
8
+ <h5 class="card-title mb-0 h6"><%= I18n.t('title', scope: i18n_scope) %></h5>
9
+ </div>
10
+
11
+ <div class="card-body">
12
+ <% @dynamic_fields.group_by(&:data_fill_stage).each do |stage, dynamic_fields| %>
13
+ <h6 class="text-muted"><%= I18n.t(stage, scope: "#{i18n_scope}.stages") %></h6>
14
+
15
+ <% dynamic_fields.each do |dynamic_field| %>
16
+ <% answers = answers_by_field_id[dynamic_field.id] || [] %>
17
+ <% answer = answers.first %>
18
+ <% input_name = "#{f.object_name}[dynamic_fields][#{dynamic_field.id}]" %>
19
+ <% input_id = "dynamic_field_#{dynamic_field.id}" %>
20
+ <% selected_option_ids = answers.map(&:dynamic_field_option_id) %>
21
+
22
+ <div class="form-group">
23
+ <%= label_tag input_id, dynamic_field.label %>
24
+
25
+ <% case dynamic_field.data_type.to_sym %>
26
+ <% when :text %>
27
+ <%= text_field_tag input_name, answer&.value, id: input_id, class: 'form-control' %>
28
+ <% when :textarea %>
29
+ <%= text_area_tag input_name, answer&.value, id: input_id, rows: 3, class: 'form-control' %>
30
+ <% when :number %>
31
+ <%= number_field_tag input_name, answer&.value, id: input_id, min: 0, class: 'form-control' %>
32
+ <% when :boolean %>
33
+ <%= select_tag input_name,
34
+ options_for_select([[I18n.t('yes', scope: i18n_scope), 'true'], [I18n.t('no', scope: i18n_scope), 'false']], answer&.value),
35
+ include_blank: true, id: input_id, class: 'form-control' %>
36
+ <% when :radio %>
37
+ <% dynamic_field.dynamic_field_options.each do |option| %>
38
+ <div class="radio">
39
+ <label>
40
+ <%= radio_button_tag input_name, option.id, selected_option_ids.include?(option.id) %>
41
+ <%= option.value %>
42
+ </label>
43
+ </div>
44
+ <% end %>
45
+ <% when :selection, :checkbox %>
46
+ <% if dynamic_field.checkbox? || dynamic_field.multiple_select %>
47
+ <% dynamic_field.dynamic_field_options.each do |option| %>
48
+ <div class="checkbox">
49
+ <label>
50
+ <%= check_box_tag "#{input_name}[]", option.id, selected_option_ids.include?(option.id), id: nil %>
51
+ <%= option.value %>
52
+ </label>
53
+ </div>
54
+ <% end %>
55
+ <% else %>
56
+ <%= select_tag input_name,
57
+ options_from_collection_for_select(dynamic_field.dynamic_field_options, :id, :value, selected_option_ids.first),
58
+ include_blank: true, id: input_id, class: 'form-control' %>
59
+ <% end %>
60
+ <% end %>
61
+ </div>
62
+ <% end %>
63
+ <% end %>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </div>
@@ -149,3 +149,5 @@
149
149
  </div>
150
150
  </div>
151
151
  </div>
152
+
153
+ <%= render partial: 'spree/admin/guests/dynamic_fields', locals: { f: f } if @dynamic_fields.present? %>
@@ -3,7 +3,7 @@
3
3
  <% end %>
4
4
 
5
5
  <% content_for :page_actions do %>
6
- <%= button_link_to Spree.t(:clear_cache), admin_invalidate_api_caches_path(model: SpreeCmCommissioner::HomepageBackground.name), method: :post, class: "btn btn-outline-primary" %>
6
+ <%= render 'spree/admin/shared/api_cache_button', model: SpreeCmCommissioner::HomepageBackground.name %>
7
7
  <%= button_link_to Spree.t(:new_homepage_background), new_admin_homepage_feed_homepage_background_path, { class: "btn-success", icon: 'add.svg', id: 'admin_new_homepage_background' } %>
8
8
  <% end %>
9
9
 
@@ -4,7 +4,6 @@
4
4
  <%= link_to plural_resource_name(SpreeCmCommissioner::HomepageSection), spree.admin_homepage_feed_homepage_section_index_url %> /
5
5
  <%= Spree.t(:edit_section_relatable) %>
6
6
  </div>
7
- <%= button_link_to Spree.t(:clear_cache), admin_invalidate_api_caches_path(model: SpreeCmCommissioner::HomepageSection.name, id: @object.id), method: :post, class: "btn btn-outline-primary" %>
8
7
  </div>
9
8
  <% end %>
10
9
 
@@ -18,7 +18,7 @@
18
18
  <% end %>
19
19
 
20
20
  <% content_for :page_actions do %>
21
- <%= button_link_to Spree.t(:clear_cache), admin_invalidate_api_caches_path(model: SpreeCmCommissioner::HomepageSection.name), method: :post, class: "btn btn-outline-primary" %>
21
+ <%= render 'spree/admin/shared/api_cache_button', model: SpreeCmCommissioner::HomepageSection.name, tenant_id: selected_tenant&.id %>
22
22
  <%= button_link_to Spree.t(:new_homepage_section), new_admin_homepage_feed_homepage_section_path(tenant_id: params[:tenant_id]), { class: "btn-success", icon: 'add.svg', id: 'admin_new_homepage_section' } %>
23
23
  <% end %>
24
24
 
@@ -0,0 +1,5 @@
1
+ <%# locals: model (an ApiCaches::Invalidate::SUPPORTED_MODELS key), id (one record), tenant_id (with
2
+ no id, clears every record of model on that tenant's API; neither clears the BookMe+ storefront). %>
3
+ <% id ||= nil %>
4
+ <% tenant_id ||= nil %>
5
+ <%= button_link_to Spree.t(:clear_cache), clear_admin_api_caches_path(model: model, id: id, tenant_id: tenant_id), method: :post, class: 'btn btn-outline-primary' %>
@@ -2,6 +2,16 @@
2
2
  # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3
3
  en:
4
4
  views:
5
+ admin:
6
+ guests:
7
+ dynamic_fields:
8
+ title: "Custom fields"
9
+ stages:
10
+ pre_registration: "Before registration"
11
+ post_registration: "After registration"
12
+ during_check_in: "During check-in"
13
+ "yes": "Yes"
14
+ "no": "No"
5
15
  manage_team:
6
16
  add:
7
17
  user_not_found: "User not found"
@@ -265,6 +275,7 @@ en:
265
275
  riel: "៛"
266
276
 
267
277
  spree:
278
+ clear_cache_requested: "Clearing cache. Changes appear in the app within about a minute."
268
279
  default_inventory: "Default Inventory"
269
280
  title: "Title"
270
281
  confirm_clone_customer_notification: "Are you sure you want to clone \"%{title}\"?"
@@ -3,6 +3,16 @@
3
3
 
4
4
  km:
5
5
  views:
6
+ admin:
7
+ guests:
8
+ dynamic_fields:
9
+ title: "ព័ត៌មានបន្ថែម"
10
+ stages:
11
+ pre_registration: "មុនចុះឈ្មោះ"
12
+ post_registration: "ក្រោយចុះឈ្មោះ"
13
+ during_check_in: "ពេលចូលរួម"
14
+ "yes": "បាទ/ចាស"
15
+ "no": "ទេ"
6
16
  manage_team:
7
17
  add:
8
18
  user_not_found: "រកមិនឃើញអ្នកប្រើប្រាស់"
@@ -222,6 +232,7 @@ km:
222
232
  riel: "៛"
223
233
 
224
234
  spree:
235
+ clear_cache_requested: "កំពុងសម្អាតឃ្លាំងទិន្នន័យ។ ការផ្លាស់ប្តូរនឹងបង្ហាញក្នុងកម្មវិធីក្នុងរយៈពេលប្រហែលមួយនាទី។"
225
236
  default_inventory: "ស្តុកលំនាំដើម"
226
237
  title: "ចំណងជើង"
227
238
  confirm_clone_customer_notification: "តើអ្នកប្រាកដថាចង់ចម្លង \"%{title}\" មែនទេ?"
data/config/routes.rb CHANGED
@@ -20,7 +20,11 @@ Spree::Core::Engine.add_routes do
20
20
  end
21
21
 
22
22
  namespace :admin do
23
- post '/invalidate_api_caches', to: 'base#invalidate_api_caches'
23
+ resources :api_caches, only: [] do
24
+ collection do
25
+ post :clear
26
+ end
27
+ end
24
28
 
25
29
  get '/system', to: redirect('/admin/system/waiting_room')
26
30
 
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.14.3'.freeze
2
+ VERSION = '2.14.4'.freeze
3
3
 
4
4
  module_function
5
5
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_cm_commissioner
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.14.3
4
+ version: 2.14.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - You
@@ -824,6 +824,7 @@ files:
824
824
  - app/controllers/concerns/spree_cm_commissioner/turnstile_protectable.rb
825
825
  - app/controllers/concerns/spree_cm_commissioner/waiting_room_authorization.rb
826
826
  - app/controllers/spree/admin/account_deletions_controller.rb
827
+ - app/controllers/spree/admin/api_caches_controller.rb
827
828
  - app/controllers/spree/admin/base_controller_decorator.rb
828
829
  - app/controllers/spree/admin/base_import_orders_controller.rb
829
830
  - app/controllers/spree/admin/blazer_queries_controller.rb
@@ -1873,6 +1874,7 @@ files:
1873
1874
  - app/overrides/spree/admin/cms_pages/index/cms_pages_tabs.html.erb.deface
1874
1875
  - app/overrides/spree/admin/menu_items/_form/badge_field.html.erb.deface
1875
1876
  - app/overrides/spree/admin/menus/edit/add_import_sprite_button.html.erb.deface
1877
+ - app/overrides/spree/admin/menus/index/clear_cache_button.html.erb.deface
1876
1878
  - app/overrides/spree/admin/oauth_applications/_form/tenant_fields.html.erb.deface
1877
1879
  - app/overrides/spree/admin/option_types/_form/attr_type.html.erb.deface
1878
1880
  - app/overrides/spree/admin/option_types/_form/checkbox.html.erb.deface
@@ -1897,6 +1899,7 @@ files:
1897
1899
  - app/overrides/spree/admin/orders/index/line_items_row.html.erb.deface
1898
1900
  - app/overrides/spree/admin/orders/index/row_actions.html.erb.deface
1899
1901
  - app/overrides/spree/admin/payment_methods/_form/display_on_field.html.erb.deface
1902
+ - app/overrides/spree/admin/payment_methods/index/clear_cache_button.html.erb.deface
1900
1903
  - app/overrides/spree/admin/payment_methods/index/pagination.html.erb.deface
1901
1904
  - app/overrides/spree/admin/payment_methods/index/preserve_tab_in_search.html.erb.deface
1902
1905
  - app/overrides/spree/admin/payments/_form/amount_text_field.html.erb.deface
@@ -2039,6 +2042,7 @@ files:
2039
2042
  - app/overrides/spree/admin/vendors/_form/star_rating.html.erb.deface
2040
2043
  - app/overrides/spree/admin/vendors/_form/term_of_use.html.erb.deface
2041
2044
  - app/overrides/spree/admin/vendors/_form/vendor_min_max_price.html.erb.deface
2045
+ - app/overrides/spree/admin/vendors/edit/clear_cache_button.html.erb.deface
2042
2046
  - app/overrides/spree/admin/vendors/edit/vendor_tabs.html.erb.deface
2043
2047
  - app/overrides/spree/admin/vendors/index/index.html.erb.deface
2044
2048
  - app/overrides/spree/admin/webhooks_subscribers/_form/api_key.html.erb.deface
@@ -2089,6 +2093,7 @@ files:
2089
2093
  - app/serializables/spree_cm_commissioner/pie_chart_event_aggregator.rb
2090
2094
  - app/serializers/.gitkeep
2091
2095
  - app/serializers/concerns/spree_cm_commissioner/api/v2/resource_serializer_concern_decorator.rb
2096
+ - app/serializers/spree/api/v2/base_serializer_decorator.rb
2092
2097
  - app/serializers/spree/api/v2/platform/create_date_specific_item_adjustment_serializer.rb
2093
2098
  - app/serializers/spree/api/v2/platform/create_guest_adjustment_serializer.rb
2094
2099
  - app/serializers/spree/api/v2/platform/create_line_item_adjustment_serializer.rb
@@ -2315,6 +2320,7 @@ files:
2315
2320
  - app/services/spree_cm_commissioner/agency_users/create.rb
2316
2321
  - app/services/spree_cm_commissioner/agency_users/destroy.rb
2317
2322
  - app/services/spree_cm_commissioner/api_caches/invalidate.rb
2323
+ - app/services/spree_cm_commissioner/api_caches/invalidate_all.rb
2318
2324
  - app/services/spree_cm_commissioner/assign_product_to_section/update.rb
2319
2325
  - app/services/spree_cm_commissioner/audit_events/cleanup_expired.rb
2320
2326
  - app/services/spree_cm_commissioner/audit_logger.rb
@@ -2363,6 +2369,7 @@ files:
2363
2369
  - app/services/spree_cm_commissioner/google_wallets/hotel_class_creator.rb
2364
2370
  - app/services/spree_cm_commissioner/google_wallets/hotel_class_updater.rb
2365
2371
  - app/services/spree_cm_commissioner/google_wallets/hotel_object_builder.rb
2372
+ - app/services/spree_cm_commissioner/guests/build_dynamic_field_attributes.rb
2366
2373
  - app/services/spree_cm_commissioner/guests/claim_invite_guest_service.rb
2367
2374
  - app/services/spree_cm_commissioner/guests/create.rb
2368
2375
  - app/services/spree_cm_commissioner/guests/finalize.rb
@@ -2810,6 +2817,7 @@ files:
2810
2817
  - app/views/spree/admin/guest_qr_codes/_guest_qr_codes_print_template.html.erb
2811
2818
  - app/views/spree/admin/guest_qr_codes/index.html.erb
2812
2819
  - app/views/spree/admin/guests/_check_in_status.html.erb
2820
+ - app/views/spree/admin/guests/_dynamic_fields.html.erb
2813
2821
  - app/views/spree/admin/guests/_form.html.erb
2814
2822
  - app/views/spree/admin/guests/_table.html.erb
2815
2823
  - app/views/spree/admin/guests/edit.html.erb
@@ -2970,6 +2978,7 @@ files:
2970
2978
  - app/views/spree/admin/service_fee_rules/edit.html.erb
2971
2979
  - app/views/spree/admin/service_fee_rules/index.html.erb
2972
2980
  - app/views/spree/admin/service_fee_rules/new.html.erb
2981
+ - app/views/spree/admin/shared/_api_cache_button.html.erb
2973
2982
  - app/views/spree/admin/shared/_availability_percentage.html.erb
2974
2983
  - app/views/spree/admin/shared/_cms_pages_tabs.html.erb
2975
2984
  - app/views/spree/admin/shared/_customer_notification_tabs.html.erb