spree_cm_commissioner 2.14.2 → 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 (42) 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/factory/spree_cm_commissioner/order_telegram_message_factory.rb +26 -5
  9. data/app/jobs/spree_cm_commissioner/audit_events/cleanup_expired_job.rb +18 -0
  10. data/app/models/concerns/spree_cm_commissioner/vendor_preference.rb +1 -1
  11. data/app/models/spree_cm_commissioner/audit_event.rb +5 -0
  12. data/app/overrides/spree/admin/menus/index/clear_cache_button.html.erb.deface +4 -0
  13. data/app/overrides/spree/admin/payment_methods/index/clear_cache_button.html.erb.deface +6 -0
  14. data/app/overrides/spree/admin/products/edit/clear_cache_button.html.erb.deface +1 -1
  15. data/app/overrides/spree/admin/taxons/edit/clear_cache_button.html.erb.deface +1 -4
  16. data/app/overrides/spree/admin/vendors/edit/clear_cache_button.html.erb.deface +5 -0
  17. data/app/serializers/spree/api/v2/base_serializer_decorator.rb +24 -0
  18. data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +39 -87
  19. data/app/services/spree_cm_commissioner/api_caches/invalidate_all.rb +61 -0
  20. data/app/services/spree_cm_commissioner/audit_events/cleanup_expired.rb +58 -0
  21. data/app/services/spree_cm_commissioner/audit_logger.rb +1 -1
  22. data/app/services/spree_cm_commissioner/guests/build_dynamic_field_attributes.rb +34 -0
  23. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_seat_layout.rb +45 -128
  24. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb +36 -148
  25. data/app/services/spree_cm_commissioner/inventory_items/reset.rb +103 -35
  26. data/app/services/spree_cm_commissioner/order/guests/update.rb +2 -24
  27. data/app/services/spree_cm_commissioner/rates_availabilities/bulk_update.rb +14 -8
  28. data/app/services/spree_cm_commissioner/seat_layouts/grid.rb +56 -0
  29. data/app/views/spree/admin/guests/_dynamic_fields.html.erb +67 -0
  30. data/app/views/spree/admin/guests/_form.html.erb +2 -0
  31. data/app/views/spree/admin/homepage_background/index.html.erb +1 -1
  32. data/app/views/spree/admin/homepage_section/edit.html.erb +0 -1
  33. data/app/views/spree/admin/homepage_section/index.html.erb +1 -1
  34. data/app/views/spree/admin/inventory_items/_details.html.erb +16 -0
  35. data/app/views/spree/admin/rates_availabilities/_script.html.erb +49 -7
  36. data/app/views/spree/admin/rates_availabilities/index.html.erb +4 -0
  37. data/app/views/spree/admin/shared/_api_cache_button.html.erb +5 -0
  38. data/config/locales/en.yml +25 -1
  39. data/config/locales/km.yml +25 -1
  40. data/config/routes.rb +5 -1
  41. data/lib/spree_cm_commissioner/version.rb +1 -1
  42. metadata +13 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d7be2a8511db3eae8f7fc7bfac3b348effc7ff5c99a9d9ce5816bb1a0e09cdd
4
- data.tar.gz: 98a4899d6a5ca428856e869a342da28a4447ae5f65c19ec7e27f4aaf996a6b97
3
+ metadata.gz: e5317bd75c6057165a07eae82d0e30f986371767925346783242d8d12a0a8d24
4
+ data.tar.gz: b015f2bebfc1105dee65f7b4b22f63ee909b113541741f9dbc5ed48482ab4b04
5
5
  SHA512:
6
- metadata.gz: 99c278aa62a3ae56513ec4489b7945545cb937279df43b51478f95169bb7b22525ba9a4dd67fb42116b3aee61d06ec2973bee3a18b9978629857ae725e26cbe6
7
- data.tar.gz: 74fba240707520ee96ea0a4f27ae585c7879c77d9e4515b037927ee9f3c1c9d7aec8d19824feaeb93e7f53b9f8afff8b06e39055ac830cb27a50402a6f41c080
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.2)
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
@@ -3,17 +3,18 @@
3
3
  # Order Number:
4
4
  # <code>R621016753</code>
5
5
  #
6
- # [ x1 ]
6
+ # 🎪 Event: Temple Run 2023
7
+ # 🏪 Vendor: Temple Run with Sai
8
+ #
7
9
  # <b>5km Running Ticket</b>
10
+ # Quantity: 1
8
11
  # <i>👉 Distance: 5km, T-shirt: M</i>
9
12
  # <i>🗓️ Nov 15, 2023</i>
10
- # <i>🏪 Temple Run with Sai</i>
11
13
  #
12
- # [ x2 ]
13
14
  # <b>5km Running Ticket</b>
15
+ # Quantity: 2
14
16
  # <i>👉 Distance: 5km, T-shirt: S</i>
15
17
  # <i>🗓️ Nov 15, 2023 -> Nov 17, 2023</i>
16
- # <i>🏪 Temple Run with Sai</i>
17
18
  #
18
19
  # <b>🙍 Customer Info</b>
19
20
  # Name: Vaneath Awesome
@@ -43,10 +44,31 @@ module SpreeCmCommissioner
43
44
  def body
44
45
  text = []
45
46
  text << "Order Number:\n#{inline_code(order.number)}\n"
47
+ summary = summary_content
48
+ text << "#{summary}\n" if summary.present?
46
49
  text << selected_line_items.map { |item| line_item_content(item) }.compact.join("\n\n")
47
50
  text.compact.join("\n")
48
51
  end
49
52
 
53
+ # Stated once for the whole order rather than per ticket: an order is almost always one
54
+ # event and one vendor, so a per-ticket line would just repeat the same name.
55
+ def summary_content
56
+ line_items = selected_line_items.to_a
57
+ ActiveRecord::Associations::Preloader.new(records: line_items, associations: %i[event vendor]).call
58
+
59
+ [
60
+ names_line('🎪 Event', line_items.map(&:event)),
61
+ names_line('🏪 Vendor', line_items.map(&:vendor))
62
+ ].compact.join("\n").presence
63
+ end
64
+
65
+ def names_line(label, records)
66
+ names = records.compact.map(&:name).compact_blank.uniq
67
+ return nil if names.empty?
68
+
69
+ "#{label}: #{names.map { |name| ERB::Util.html_escape(name) }.join(', ')}"
70
+ end
71
+
50
72
  def line_item_content(line_item)
51
73
  return transit_line_item_content(line_item) if line_item.transit?
52
74
 
@@ -56,7 +78,6 @@ module SpreeCmCommissioner
56
78
  text << "Quantity: #{line_item.quantity}"
57
79
  text << italic("👉 #{line_item.options_text}") if line_item.options_text.present?
58
80
  text << italic(pretty_date_for(line_item)) if pretty_date_for(line_item).present?
59
- text << italic("🏪 #{line_item.vendor.name}") if line_item.vendor&.name.present? && vendor.blank?
60
81
  text << seat_labels_text(line_item) if seat_labels_text(line_item).present?
61
82
 
62
83
  text.compact.join("\n")
@@ -0,0 +1,18 @@
1
+ module SpreeCmCommissioner
2
+ module AuditEvents
3
+ class CleanupExpiredJob < SpreeCmCommissioner::ApplicationJob
4
+ # Gap before continuing a backlog, so the Sidekiq thread is free for other jobs in between.
5
+ CONTINUE_DELAY = 10.seconds
6
+
7
+ queue_as :maintenance
8
+
9
+ # Not an ApplicationUniqueJob: `unique :until_executed` would drop the continuation below as a duplicate of the
10
+ # running job. Overlapping with the daily cron is harmless because deleting is idempotent.
11
+ def perform
12
+ result = SpreeCmCommissioner::AuditEvents::CleanupExpired.new.call
13
+
14
+ self.class.set(wait: CONTINUE_DELAY).perform_later if result.value[:more_remaining]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -2,7 +2,7 @@ module SpreeCmCommissioner
2
2
  module VendorPreference
3
3
  extend ActiveSupport::Concern
4
4
 
5
- TELEGRAM_CHAT_TYPE = %i[channel group].freeze
5
+ TELEGRAM_CHAT_TYPE = %i[channel group supergroup].freeze
6
6
 
7
7
  included do
8
8
  preference :telegram_chat_id, :string
@@ -1,5 +1,8 @@
1
1
  module SpreeCmCommissioner
2
2
  class AuditEvent < Base
3
+ # How long AuditLogger keeps an event when the caller gives no retention_days.
4
+ DEFAULT_RETENTION_DAYS = 90
5
+
3
6
  belongs_to :auditable, polymorphic: true, optional: true
4
7
  belongs_to :user, class_name: 'Spree::User', optional: true
5
8
 
@@ -7,5 +10,7 @@ module SpreeCmCommissioner
7
10
 
8
11
  scope :success, -> { where(success: true) }
9
12
  scope :failed, -> { where(success: false) }
13
+
14
+ scope :expired, -> (now = Time.current) { where(retention_until: ...now.to_date) }
10
15
  end
11
16
  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)