spree_cm_commissioner 2.14.3 → 2.14.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.env.example +10 -6
- data/Gemfile.lock +1 -1
- data/app/controllers/concerns/spree_cm_commissioner/content_cachable.rb +108 -69
- data/app/controllers/concerns/spree_cm_commissioner/waiting_room_authorization.rb +41 -8
- data/app/controllers/spree/admin/api_caches_controller.rb +26 -0
- data/app/controllers/spree/admin/base_controller_decorator.rb +0 -14
- data/app/controllers/spree/admin/guests_controller.rb +43 -4
- data/app/controllers/spree/admin/homepage_section_controller.rb +63 -5
- data/app/controllers/spree/api/v2/storefront/draft_orders_controller.rb +6 -2
- data/app/controllers/spree/api/v2/storefront/homepage_sections_controller.rb +1 -0
- data/app/controllers/spree/api/v2/storefront/preview_sections_controller.rb +1 -0
- data/app/controllers/spree/api/v2/storefront/queue_draft_orders_controller.rb +6 -2
- data/app/controllers/spree/api/v2/tenant/draft_orders_controller.rb +6 -2
- data/app/controllers/spree/api/v2/tenant/homepage_sections_controller.rb +1 -0
- data/app/controllers/spree/api/v2/tenant/preview_sections_controller.rb +1 -0
- data/app/models/active_storage/variation_decorator.rb +18 -0
- data/app/models/concerns/spree_cm_commissioner/event_metadata.rb +4 -0
- data/app/models/concerns/spree_cm_commissioner/order_holdable.rb +3 -2
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +17 -0
- data/app/models/spree_cm_commissioner/homepage_section.rb +33 -0
- data/app/models/spree_cm_commissioner/homepage_section_app_background.rb +13 -0
- data/app/models/spree_cm_commissioner/homepage_section_web_background.rb +13 -0
- data/app/models/spree_cm_commissioner/product_decorator.rb +7 -0
- data/app/overrides/spree/admin/menus/index/clear_cache_button.html.erb.deface +4 -0
- data/app/overrides/spree/admin/payment_methods/index/clear_cache_button.html.erb.deface +6 -0
- data/app/overrides/spree/admin/products/edit/clear_cache_button.html.erb.deface +1 -1
- data/app/overrides/spree/admin/taxons/_form/enforce_waiting_room_session.html.erb.deface +10 -0
- data/app/overrides/spree/admin/taxons/edit/clear_cache_button.html.erb.deface +1 -4
- data/app/overrides/spree/admin/vendors/edit/clear_cache_button.html.erb.deface +5 -0
- data/app/serializers/spree/api/v2/base_serializer_decorator.rb +24 -0
- data/app/serializers/spree/v2/storefront/homepage_section_serializer.rb +4 -1
- data/app/serializers/spree/v2/tenant/homepage_section_serializer.rb +3 -1
- data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +39 -87
- data/app/services/spree_cm_commissioner/api_caches/invalidate_all.rb +61 -0
- data/app/services/spree_cm_commissioner/guests/build_dynamic_field_attributes.rb +34 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trip_stop.rb +21 -4
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trips.rb +1 -0
- data/app/services/spree_cm_commissioner/line_items/validate_purchase_limits.rb +56 -5
- data/app/services/spree_cm_commissioner/order/guests/update.rb +2 -24
- data/app/views/spree/admin/guests/_dynamic_fields.html.erb +67 -0
- data/app/views/spree/admin/guests/_form.html.erb +2 -0
- data/app/views/spree/admin/homepage_background/index.html.erb +1 -1
- data/app/views/spree/admin/homepage_section/_form.html.erb +44 -0
- data/app/views/spree/admin/homepage_section/edit.html.erb +0 -1
- data/app/views/spree/admin/homepage_section/index.html.erb +1 -1
- data/app/views/spree/admin/shared/_api_cache_button.html.erb +5 -0
- data/config/initializers/spree_permitted_attributes.rb +1 -0
- data/config/locales/dry_validations.en.yml +4 -0
- data/config/locales/en.yml +22 -0
- data/config/locales/km.yml +22 -0
- data/config/routes.rb +10 -1
- data/db/migrate/20261021000001_add_section_type_to_cm_homepage_sections.rb +6 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +16 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module ActiveStorage
|
|
2
|
+
module VariationDecorator
|
|
3
|
+
# ActiveStorage reuses a JPEG upload's own extension as the variant format, but libvips
|
|
4
|
+
# picks its encoder from the output extension and rejects aliases like `.jfif`
|
|
5
|
+
# ("is not a known file format"). Mapping here rather than renaming blobs also fixes
|
|
6
|
+
# variant URLs already handed out, since their signed key embeds `format: "jfif"`.
|
|
7
|
+
JPEG_FORMAT_ALIASES = %w[jfif jpe].freeze
|
|
8
|
+
|
|
9
|
+
def format
|
|
10
|
+
format = super
|
|
11
|
+
JPEG_FORMAT_ALIASES.include?(format.to_s.downcase) ? 'jpg' : format
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
unless ActiveStorage::Variation.ancestors.include?(ActiveStorage::VariationDecorator)
|
|
17
|
+
ActiveStorage::Variation.prepend(ActiveStorage::VariationDecorator)
|
|
18
|
+
end
|
|
@@ -18,6 +18,10 @@ module SpreeCmCommissioner
|
|
|
18
18
|
# For certain large events, admin can disable this manually.
|
|
19
19
|
store_public_metadata :allow_manual_search_for_operator, :boolean, default: true
|
|
20
20
|
|
|
21
|
+
# Requires an active waiting-room session to create a draft order for this event's products
|
|
22
|
+
# (Product#enforce_waiting_room_session?). Server-only switch, so private.
|
|
23
|
+
store_private_metadata :enforce_waiting_room_session, :boolean, default: false
|
|
24
|
+
|
|
21
25
|
# ⚠️ CRITICAL: Cached seat layout IDs kept in sync by SeatLayout callback.
|
|
22
26
|
# If callback is removed/modified without updating this, availability checks will fail.
|
|
23
27
|
#
|
|
@@ -4,8 +4,9 @@ module SpreeCmCommissioner
|
|
|
4
4
|
|
|
5
5
|
included do
|
|
6
6
|
# Store the earliest expiration time across all holds (seats + inventory)
|
|
7
|
-
# in `public_metadata[:hold_expires_at]`. This is used
|
|
8
|
-
# (e.g., a countdown timer) and
|
|
7
|
+
# in `public_metadata[:hold_expires_at]`. This is used for UI display
|
|
8
|
+
# (e.g., a countdown timer) and by LineItems::ValidatePurchaseLimits to count an
|
|
9
|
+
# unpaid cart against the buyer's caps; it does not control the release logic.
|
|
9
10
|
# Each hold is released independently when its own expiration time is reached.
|
|
10
11
|
store_public_metadata :hold_expires_at, :datetime
|
|
11
12
|
|
|
@@ -7,6 +7,7 @@ module SpreeCmCommissioner
|
|
|
7
7
|
included do # rubocop:disable Metrics/BlockLength
|
|
8
8
|
after_commit :clear_has_incomplete_guest_info_cache, on: %i[create update]
|
|
9
9
|
|
|
10
|
+
state_machine.before_transition to: :address, do: :ensure_within_purchase_limits
|
|
10
11
|
state_machine.before_transition to: :address, do: :hold_order_holds
|
|
11
12
|
|
|
12
13
|
# Same transitions core calls create_tax_charge! from (gems/core/app/models/spree/order/checkout.rb,
|
|
@@ -107,6 +108,22 @@ module SpreeCmCommissioner
|
|
|
107
108
|
end
|
|
108
109
|
end
|
|
109
110
|
|
|
111
|
+
# Line items are only validated when they change, so a cart sent back to `cart` (hold
|
|
112
|
+
# released) and advanced again would skip the purchase-limit check and could be held next to
|
|
113
|
+
# another cart the buyer opened meanwhile. Runs before hold_order_holds so a rejection never
|
|
114
|
+
# leaves a hold behind.
|
|
115
|
+
def ensure_within_purchase_limits
|
|
116
|
+
line_items.each do |line_item|
|
|
117
|
+
result = SpreeCmCommissioner::LineItems::ValidatePurchaseLimits.call(line_item: line_item)
|
|
118
|
+
next if result.success?
|
|
119
|
+
|
|
120
|
+
errors.add(:base, result.error.to_s)
|
|
121
|
+
return false
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
true
|
|
125
|
+
end
|
|
126
|
+
|
|
110
127
|
# Best-effort, like apply_prepaid_payment!'s other callers: a user with no (or no
|
|
111
128
|
# currency-matching) store credit just doesn't get a payment here, and the app can still
|
|
112
129
|
# remove/reapply it afterwards through the existing checkout store-credit endpoints.
|
|
@@ -10,10 +10,26 @@ module SpreeCmCommissioner
|
|
|
10
10
|
attribute :segment, :integer, default: 0
|
|
11
11
|
attribute :mini_app, :integer, default: 0
|
|
12
12
|
|
|
13
|
+
enum section_type: { default: 0, trending: 1, big_banner: 2 }
|
|
14
|
+
|
|
13
15
|
self.whitelisted_ransackable_attributes = %w[title active]
|
|
14
16
|
|
|
15
17
|
has_many :homepage_section_relatables, -> { active }, inverse_of: :homepage_section, dependent: :destroy
|
|
16
18
|
|
|
19
|
+
has_one :app_background, as: :viewable, dependent: :destroy, class_name: 'SpreeCmCommissioner::HomepageSectionAppBackground'
|
|
20
|
+
has_one :web_background, as: :viewable, dependent: :destroy, class_name: 'SpreeCmCommissioner::HomepageSectionWebBackground'
|
|
21
|
+
|
|
22
|
+
# `font`/backgrounds only apply to `trending` sections. The admin form only hides those
|
|
23
|
+
# fields via JS when another type is picked — it doesn't clear them — and a direct API call
|
|
24
|
+
# could set them on any type, so normalize here rather than relying on the form. A hard
|
|
25
|
+
# validation would instead reject the common "switch an existing trending section back to
|
|
26
|
+
# default" edit, since the hidden `font` field still submits its stale value.
|
|
27
|
+
#
|
|
28
|
+
# Guarded beyond `unless: :trending?` so a routine save of an already-default/big_banner
|
|
29
|
+
# section (the steady-state case) skips the `app_background`/`web_background` association
|
|
30
|
+
# loads entirely instead of querying them on every save forever.
|
|
31
|
+
before_save :clear_non_trending_fields, if: :clear_non_trending_fields?
|
|
32
|
+
|
|
17
33
|
scope :active, lambda {
|
|
18
34
|
where(active: true)
|
|
19
35
|
.joins(:homepage_section_relatables)
|
|
@@ -47,5 +63,22 @@ module SpreeCmCommissioner
|
|
|
47
63
|
|
|
48
64
|
# Create maintaining task to purge homepage section related caches
|
|
49
65
|
after_save { SpreeCmCommissioner::MaintenanceTasks::CacheInvalidation.pending.create_or_find_by(maintainable: self) }
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# `.target` (not `.loaded?`) so this only fires for a background actually built/loaded in
|
|
70
|
+
# memory on *this* save. `.loaded?` stays true forever once an object has been saved once
|
|
71
|
+
# with the association touched, even after the target is nil'd out by a prior destroy —
|
|
72
|
+
# which made every later save of that same in-memory record re-run the clear unnecessarily.
|
|
73
|
+
def clear_non_trending_fields?
|
|
74
|
+
!trending? && (font.present? || section_type_changed? ||
|
|
75
|
+
association(:app_background).target.present? || association(:web_background).target.present?)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def clear_non_trending_fields
|
|
79
|
+
self.font = nil
|
|
80
|
+
app_background&.destroy
|
|
81
|
+
web_background&.destroy
|
|
82
|
+
end
|
|
50
83
|
end
|
|
51
84
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
class HomepageSectionAppBackground < Asset
|
|
3
|
+
# 3:2 — taller than 16:9 so the image fills a `trending` card's full height instead of
|
|
4
|
+
# letterboxing. `medium` (960x640) is the upload target; `mini`/`small` are downscaled copies.
|
|
5
|
+
def asset_styles
|
|
6
|
+
{
|
|
7
|
+
mini: '160x107>',
|
|
8
|
+
small: '480x320>',
|
|
9
|
+
medium: '960x640>'
|
|
10
|
+
}
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
class HomepageSectionWebBackground < Asset
|
|
3
|
+
# 5:1 — wide and short so the image spans a `trending` banner's full width without pushing
|
|
4
|
+
# page content down. `medium` (1440x288) is the upload target; `mini`/`small` are downscaled copies.
|
|
5
|
+
def asset_styles
|
|
6
|
+
{
|
|
7
|
+
mini: '160x32>',
|
|
8
|
+
small: '720x144>',
|
|
9
|
+
medium: '1440x288>'
|
|
10
|
+
}
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -222,6 +222,13 @@ module SpreeCmCommissioner
|
|
|
222
222
|
end
|
|
223
223
|
end
|
|
224
224
|
|
|
225
|
+
# Configured once on the event rather than per ticket product. Other product types (trips,
|
|
226
|
+
# rooms, ...) that need it later resolve their own source here; the draft-order guard
|
|
227
|
+
# (WaitingRoomAuthorization#ensure_waiting_room_session!) only ever asks the product.
|
|
228
|
+
def enforce_waiting_room_session?
|
|
229
|
+
event&.enforce_waiting_room_session? || false
|
|
230
|
+
end
|
|
231
|
+
|
|
225
232
|
# Not yet on sale. Kept separate from available? so a scheduled product's real stock still
|
|
226
233
|
# shows via in_stock? — Stock::AvailabilityValidatorDecorator checks this to block add-to-cart.
|
|
227
234
|
def scheduled?
|
|
@@ -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
|
-
<%=
|
|
3
|
+
<%= render 'spree/admin/shared/api_cache_button', model: Spree::Product.name, id: @product.id %>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<!-- insert_before "erb[loud]:contains('field_container :hide_from_nav')" -->
|
|
2
|
+
|
|
3
|
+
<%= f.field_container :enforce_waiting_room_session, class: ['custom-control', 'custom-checkbox', 'my-4'] do %>
|
|
4
|
+
<%= f.check_box :enforce_waiting_room_session, class: 'custom-control-input' %>
|
|
5
|
+
<%= f.label :enforce_waiting_room_session, Spree.t(:enforce_waiting_room_session), class: 'custom-control-label' %>
|
|
6
|
+
<%= f.error_message_on :enforce_waiting_room_session %>
|
|
7
|
+
<small class="form-text text-muted">
|
|
8
|
+
<%= Spree.t(:enforce_waiting_room_session_hint) %>
|
|
9
|
+
</small>
|
|
10
|
+
<% end if @taxon.event? && @taxon.depth == 1 %> %>
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
<!-- insert_after "erb[silent]:contains('content_for :page_actions')" -->
|
|
2
2
|
|
|
3
3
|
<% if @taxon.depth == 1 %>
|
|
4
|
-
|
|
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,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)
|
|
@@ -2,8 +2,11 @@ module Spree
|
|
|
2
2
|
module V2
|
|
3
3
|
module Storefront
|
|
4
4
|
class HomepageSectionSerializer < BaseSerializer
|
|
5
|
-
attributes :id, :title, :segments, :mini_apps, :description, :active, :position, :preview,
|
|
5
|
+
attributes :id, :title, :segments, :mini_apps, :description, :active, :position, :preview,
|
|
6
|
+
:section_type, :font, :created_at, :updated_at
|
|
6
7
|
has_many :homepage_section_relatables
|
|
8
|
+
has_one :app_background, serializer: ::SpreeCmCommissioner::V2::Storefront::AssetSerializer
|
|
9
|
+
has_one :web_background, serializer: ::SpreeCmCommissioner::V2::Storefront::AssetSerializer
|
|
7
10
|
end
|
|
8
11
|
end
|
|
9
12
|
end
|
|
@@ -3,9 +3,11 @@ module Spree
|
|
|
3
3
|
module Tenant
|
|
4
4
|
class HomepageSectionSerializer < BaseSerializer
|
|
5
5
|
attributes :id, :title, :segments, :description,
|
|
6
|
-
:active, :position, :preview, :created_at, :updated_at
|
|
6
|
+
:active, :position, :preview, :section_type, :font, :created_at, :updated_at
|
|
7
7
|
|
|
8
8
|
has_many :homepage_section_relatables, serializer: ::Spree::V2::Tenant::HomepageSectionRelatableSerializer
|
|
9
|
+
has_one :app_background, serializer: ::Spree::V2::Tenant::AssetSerializer
|
|
10
|
+
has_one :web_background, serializer: ::Spree::V2::Tenant::AssetSerializer
|
|
9
11
|
end
|
|
10
12
|
end
|
|
11
13
|
end
|
|
@@ -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
|
-
#
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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' =>
|
|
67
|
-
'SpreeCmCommissioner::HomepageBackground' =>
|
|
81
|
+
'SpreeCmCommissioner::HomepageSection' => HOMEPAGE_SECTION_PATTERNS,
|
|
82
|
+
'SpreeCmCommissioner::HomepageBackground' => HOMEPAGE_BACKGROUND_PATTERNS,
|
|
68
83
|
'SpreeCmCommissioner::SeatLayout' => ['/api/v2/storefront/seat_layouts/:id*'],
|
|
69
|
-
|
|
70
|
-
|
|
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' =>
|
|
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
|
data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trip_stop.rb
CHANGED
|
@@ -38,6 +38,8 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
|
|
|
38
38
|
stop_times = trip_mapping.external_payload&.dig('stop_times')
|
|
39
39
|
return [] unless stop_times.is_a?(Array)
|
|
40
40
|
|
|
41
|
+
previous_time = nil
|
|
42
|
+
|
|
41
43
|
synced_mappings = stop_times.each_with_index.map do |stop_time_data, index|
|
|
42
44
|
# BookMeBus's stop_times[i]['arrival_time'] is the time the trip arrives at the NEXT
|
|
43
45
|
# physical stop (i+1), not at stop i itself - it's 0/absent on the last entry, which has
|
|
@@ -50,7 +52,15 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
|
|
|
50
52
|
# missing' on every booking attempt - never caught by specs because their fixtures set
|
|
51
53
|
# arrival_time == departure_time on the same entry, sidestepping the real shape.
|
|
52
54
|
incoming_arrival_time = index.positive? ? stop_times[index - 1]['arrival_time'] : nil
|
|
53
|
-
|
|
55
|
+
|
|
56
|
+
# BookMeBus sends time-of-day only, so an overnight trip (23:00 -> 05:00) needs its days rolled
|
|
57
|
+
# here: TransitOrder::Create trusts the stored datetimes and raises when arrival < departure.
|
|
58
|
+
arrival_time = roll_forward(parse_time(incoming_arrival_time), previous_time)
|
|
59
|
+
departure_time = roll_forward(parse_time(stop_time_data['departure_time']), arrival_time || previous_time)
|
|
60
|
+
previous_time = departure_time || arrival_time || previous_time
|
|
61
|
+
|
|
62
|
+
times = { arrival_time: arrival_time, departure_time: departure_time }
|
|
63
|
+
sync_item!(trip_mapping, stop_time_data, times, index + 1, stop_times.size)
|
|
54
64
|
end.compact
|
|
55
65
|
|
|
56
66
|
cleanup_stale_mappings!(trip_mapping, synced_mappings)
|
|
@@ -82,7 +92,7 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
|
|
|
82
92
|
end
|
|
83
93
|
|
|
84
94
|
# Sync a single trip stop
|
|
85
|
-
def sync_item!(trip_mapping, stop_time_data,
|
|
95
|
+
def sync_item!(trip_mapping, stop_time_data, times, sequence, total_stops)
|
|
86
96
|
stop_data = stop_time_data['stop']
|
|
87
97
|
location_data = stop_time_data['location']
|
|
88
98
|
return nil unless stop_data.is_a?(Hash) && location_data.is_a?(Hash)
|
|
@@ -119,8 +129,8 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
|
|
|
119
129
|
# BookMeBus returns one entry per search.
|
|
120
130
|
allow_boarding: sequence == 1,
|
|
121
131
|
allow_drop_off: sequence == total_stops,
|
|
122
|
-
departure_time:
|
|
123
|
-
arrival_time:
|
|
132
|
+
departure_time: times[:departure_time],
|
|
133
|
+
arrival_time: times[:arrival_time]
|
|
124
134
|
)
|
|
125
135
|
tracker.save_if_changed!(trip_stop)
|
|
126
136
|
end
|
|
@@ -206,6 +216,13 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
|
|
|
206
216
|
mapping&.internal
|
|
207
217
|
end
|
|
208
218
|
|
|
219
|
+
def roll_forward(time, not_before)
|
|
220
|
+
return time if time.nil? || not_before.nil?
|
|
221
|
+
|
|
222
|
+
time += 1.day while time < not_before
|
|
223
|
+
time
|
|
224
|
+
end
|
|
225
|
+
|
|
209
226
|
def parse_time(seconds_from_midnight)
|
|
210
227
|
return nil unless seconds_from_midnight.present? && seconds_from_midnight.to_i.positive?
|
|
211
228
|
|