spree_cm_commissioner 2.14.7 → 2.14.8
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/Gemfile.lock +1 -1
- data/app/controllers/concerns/spree_cm_commissioner/content_cachable.rb +9 -1
- data/app/controllers/concerns/spree_cm_commissioner/guest_owner_authorization.rb +30 -0
- data/app/controllers/spree/admin/stock_managements_controller.rb +12 -1
- data/app/controllers/spree/api/v2/organizer/invites_controller.rb +5 -1
- data/app/controllers/spree/api/v2/storefront/guests_controller.rb +13 -7
- data/app/controllers/spree/api/v2/storefront/id_cards_controller.rb +6 -4
- data/app/controllers/spree/api/v2/storefront/products_controller_decorator.rb +1 -1
- data/app/controllers/spree/api/v2/storefront/s3_signed_urls_controller.rb +4 -3
- data/app/controllers/spree/api/v2/tenant/base_controller.rb +23 -3
- data/app/controllers/spree/api/v2/tenant/cart_controller.rb +1 -1
- data/app/controllers/spree/api/v2/tenant/id_cards_controller.rb +6 -4
- data/app/controllers/spree/api/v2/tenant/payment_methods_controller.rb +1 -2
- data/app/controllers/spree/api/v2/tenant/products_controller.rb +1 -1
- data/app/controllers/spree/api/v2/tenant/s3_signed_urls_controller.rb +4 -3
- data/app/interactors/spree_cm_commissioner/guest_id_card_manager.rb +18 -9
- data/app/interactors/spree_cm_commissioner/id_card_image_updater.rb +6 -3
- data/app/interactors/spree_cm_commissioner/image_saver.rb +4 -3
- data/app/interactors/spree_cm_commissioner/profile_image_updater.rb +6 -3
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +1 -9
- data/app/models/concerns/spree_cm_commissioner/sales_channel_bitwise.rb +44 -0
- data/app/models/concerns/spree_cm_commissioner/user_uploaded_image.rb +17 -0
- data/app/models/spree_cm_commissioner/back_image.rb +2 -0
- data/app/models/spree_cm_commissioner/front_image.rb +2 -0
- data/app/models/spree_cm_commissioner/order_decorator.rb +11 -0
- data/app/models/spree_cm_commissioner/product_decorator.rb +4 -0
- data/app/models/spree_cm_commissioner/s3_presigned_url.rb +8 -7
- data/app/models/spree_cm_commissioner/s3_presigned_url_builder.rb +9 -3
- data/app/models/spree_cm_commissioner/tenant.rb +3 -0
- data/app/models/spree_cm_commissioner/user_profile.rb +2 -0
- data/app/overrides/spree/admin/products/_form/enable_confirmation_email.html.erb.deface +13 -0
- data/app/overrides/spree/admin/products/_form/sellable_on_platform.html.erb.deface +12 -0
- data/app/serializers/spree/v2/storefront/taxon_serializer_decorator.rb +4 -1
- data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +28 -17
- data/app/services/spree_cm_commissioner/api_caches/invalidate_all.rb +5 -7
- data/app/services/spree_cm_commissioner/draft_orders/create.rb +6 -3
- data/app/services/spree_cm_commissioner/events/publish_stock_statuses_to_firestore.rb +30 -4
- data/config/initializers/spree_permitted_attributes.rb +4 -0
- data/config/locales/en.yml +2 -0
- data/config/locales/km.yml +1 -0
- data/config/routes.rb +14 -6
- data/db/migrate/20261103000001_add_sales_channel_to_spree_products.rb +14 -0
- data/lib/spree_cm_commissioner/s3_url_generator.rb +13 -1
- data/lib/spree_cm_commissioner/tenant_slug_constraint.rb +23 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +1 -0
- metadata +8 -8
- data/app/controllers/spree/api/v2/organizer/images_controller.rb +0 -56
- data/app/controllers/spree/api/v2/organizer/s3_signed_urls_controller.rb +0 -29
- data/app/controllers/spree/api/v2/organizer/ticket_images_controller.rb +0 -27
- data/app/controllers/spree/api/v2/organizer/tickets_controller.rb +0 -97
- data/app/serializers/spree/v2/organizer/image_serializer.rb +0 -11
- data/app/serializers/spree/v2/organizer/s3_signed_url_serializer.rb +0 -10
- data/app/serializers/spree/v2/organizer/ticket_serializer.rb +0 -9
|
@@ -11,25 +11,26 @@ module SpreeCmCommissioner
|
|
|
11
11
|
URI.parse(url).host
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# content_length_range: [Range, nil] byte range S3 enforces on the upload itself; nil means no limit.
|
|
15
|
+
def initialize(bucket_name, object_key, uuid = nil, content_length_range: nil)
|
|
15
16
|
@id = uuid.presence || generate_secure_random
|
|
16
17
|
|
|
17
18
|
if object_key.blank?
|
|
18
19
|
@error_message = I18n.t('s3_signed_url.missing_file_name')
|
|
19
20
|
else
|
|
20
|
-
@s3_direct_post = presigned_post(bucket_name, object_key)
|
|
21
|
+
@s3_direct_post = presigned_post(bucket_name, object_key, content_length_range)
|
|
21
22
|
end
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
private
|
|
25
26
|
|
|
26
|
-
def presigned_post(bucket_name, object_key)
|
|
27
|
+
def presigned_post(bucket_name, object_key, content_length_range)
|
|
28
|
+
options = { key: object_key, success_action_status: '201' }
|
|
29
|
+
options[:content_length_range] = content_length_range if content_length_range
|
|
30
|
+
|
|
27
31
|
Aws::S3::Resource.new
|
|
28
32
|
.bucket(bucket_name)
|
|
29
|
-
.presigned_post(
|
|
30
|
-
key: object_key,
|
|
31
|
-
success_action_status: '201'
|
|
32
|
-
)
|
|
33
|
+
.presigned_post(**options)
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
def generate_secure_random
|
|
@@ -6,13 +6,15 @@ module SpreeCmCommissioner
|
|
|
6
6
|
new(options).call
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
# {:bucket_name?, (:object_key | :file_name)? }
|
|
9
|
+
# {:bucket_name?, (:object_key | :file_name)?, :content_length_range? }
|
|
10
|
+
# bucket_name/object_key are for trusted server-side callers only (admin media uploads); API
|
|
11
|
+
# controllers must pass just file_name, so callers can only write under uploads/<uuid>/.
|
|
10
12
|
def initialize(options)
|
|
11
13
|
@options = options
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def call
|
|
15
|
-
SpreeCmCommissioner::S3PresignedUrl.new(bucket_name, object_key, uuid)
|
|
17
|
+
SpreeCmCommissioner::S3PresignedUrl.new(bucket_name, object_key, uuid, content_length_range: options[:content_length_range])
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
private
|
|
@@ -20,7 +22,11 @@ module SpreeCmCommissioner
|
|
|
20
22
|
def object_key
|
|
21
23
|
return options[:object_key] if options[:object_key].present?
|
|
22
24
|
|
|
23
|
-
"
|
|
25
|
+
# basename so a file_name like "../x" or "a/b" can't move the key out of its uuid folder.
|
|
26
|
+
file_name = File.basename(options[:file_name].to_s)
|
|
27
|
+
return nil if file_name.blank? || %w[. .. /].include?(file_name)
|
|
28
|
+
|
|
29
|
+
"uploads/#{uuid}/#{file_name}"
|
|
24
30
|
end
|
|
25
31
|
|
|
26
32
|
def bucket_name
|
|
@@ -21,6 +21,9 @@ module SpreeCmCommissioner
|
|
|
21
21
|
allow_blank: true,
|
|
22
22
|
format: { without: %r{\Ahttps?://}, message: :format }
|
|
23
23
|
|
|
24
|
+
# The slug is the first segment of /api/v2/:tenant_slug/..., so it must not shadow a v2 namespace.
|
|
25
|
+
validates :slug, exclusion: { in: SpreeCmCommissioner::TenantSlugConstraint::RESERVED_SLUGS }
|
|
26
|
+
|
|
24
27
|
validates :sms_sender_id, length: { maximum: 11 }, allow_blank: true
|
|
25
28
|
|
|
26
29
|
validates :default_currency, inclusion: { in: -> (tenant) { tenant.supported_currencies_list } }, allow_blank: true
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!-- insert_after "[data-hook='admin_product_form_promotionable']" -->
|
|
2
|
+
|
|
3
|
+
<div>
|
|
4
|
+
<%= f.field_container :enable_confirmation_email do %>
|
|
5
|
+
<%= f.label :enable_confirmation_email do %>
|
|
6
|
+
<%= f.check_box :enable_confirmation_email %>
|
|
7
|
+
<%= Spree.t(:enable_confirmation_email) %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<small class="form-text text-muted">
|
|
10
|
+
When enabled, customers receive the booking confirmation email after their order is completed.
|
|
11
|
+
</small>
|
|
12
|
+
<% end %>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!-- insert_after "[data-hook='admin_product_form_promotionable']" -->
|
|
2
|
+
|
|
3
|
+
<%# BookMe+'s own products are always sold on the platform, so only tenant products get the choice. %>
|
|
4
|
+
<% if f.object.vendor&.tenant_id.present? %>
|
|
5
|
+
<div data-hook="admin_product_form_sellable_on_platform">
|
|
6
|
+
<%= f.field_container :sellable_on_platform do %>
|
|
7
|
+
<%= f.check_box :sellable_on_platform %>
|
|
8
|
+
<%= f.label :sellable_on_platform, 'Also sell on BookMe+' %>
|
|
9
|
+
<%= f.error_message_on :sales_channel %>
|
|
10
|
+
<% end %>
|
|
11
|
+
</div>
|
|
12
|
+
<% end %>
|
|
@@ -4,7 +4,10 @@ module Spree
|
|
|
4
4
|
module TaxonSerializerDecorator
|
|
5
5
|
def self.prepended(base)
|
|
6
6
|
base.has_many :vendors, serializer: ::Spree::V2::Storefront::VendorSerializer
|
|
7
|
-
|
|
7
|
+
# The app lists an event section's tickets from this relation, so products a tenant keeps off BookMe+ are dropped here.
|
|
8
|
+
base.has_many :visible_products, serializer: ::Spree::V2::Storefront::ProductSerializer do |taxon|
|
|
9
|
+
taxon.visible_products.sellable_on(:platform)
|
|
10
|
+
end
|
|
8
11
|
base.has_many :places, serializer: ::Spree::V2::Storefront::PlaceSerializer
|
|
9
12
|
|
|
10
13
|
base.has_one :vendor, serializer: ::Spree::V2::Storefront::VendorSerializer
|
|
@@ -4,13 +4,13 @@ module SpreeCmCommissioner
|
|
|
4
4
|
# MaintenanceTasks::CacheInvalidation sends. Every record of a model goes through InvalidateAll.
|
|
5
5
|
#
|
|
6
6
|
# Only the record's own storefront is cleared, as InvalidateAll does: its tenant's API
|
|
7
|
-
# (/api/v2
|
|
8
|
-
# is billed, and the other storefront's copy, if anything ever fetched one, expires
|
|
9
|
-
# TTL anyway.
|
|
7
|
+
# (/api/v2/<tenant slug>/ paths) when it belongs to a tenant, the BookMe+ storefront otherwise.
|
|
8
|
+
# Each path is billed, and the other storefront's copy, if anything ever fetched one, expires
|
|
9
|
+
# within a CDN TTL anyway. Legacy /api/v2/tenant/ paths are never CDN-cached, so never cleared.
|
|
10
10
|
class Invalidate
|
|
11
11
|
prepend ::Spree::ServiceModule::Base
|
|
12
12
|
|
|
13
|
-
TENANT_PREFIX = '/api/v2/
|
|
13
|
+
TENANT_PREFIX = '/api/v2/:tenant_slug/'.freeze
|
|
14
14
|
|
|
15
15
|
SUPPORTED_MODELS = {
|
|
16
16
|
'SpreeCmCommissioner::HomepageSection' => SpreeCmCommissioner::HomepageSection,
|
|
@@ -30,7 +30,7 @@ module SpreeCmCommissioner
|
|
|
30
30
|
# path per BIT_SEGMENT key.
|
|
31
31
|
HOMEPAGE_SECTION_PATTERNS = [
|
|
32
32
|
'/api/v2/storefront/homepage*',
|
|
33
|
-
'/api/v2/
|
|
33
|
+
'/api/v2/:tenant_slug/homepage*'
|
|
34
34
|
].freeze
|
|
35
35
|
|
|
36
36
|
HOMEPAGE_BACKGROUND_PATTERNS = ['/api/v2/storefront/homepage*'].freeze
|
|
@@ -42,12 +42,13 @@ module SpreeCmCommissioner
|
|
|
42
42
|
# which vendors are affected.
|
|
43
43
|
#
|
|
44
44
|
# vendor_id is a query param, so the trailing `*` is what clears every
|
|
45
|
-
# vendor's cached variant of this path in one go.
|
|
45
|
+
# vendor's cached variant of this path in one go. CloudFront allows `*` only at the end, so the
|
|
46
|
+
# tenant path expands to one path per tenant.
|
|
46
47
|
#
|
|
47
48
|
# Payment methods change a handful of times a year, so a broad invalidation
|
|
48
49
|
# is cheap.
|
|
49
50
|
PAYMENT_METHOD_PATTERNS = [
|
|
50
|
-
'/api/v2/
|
|
51
|
+
'/api/v2/:tenant_slug/payment_methods*',
|
|
51
52
|
'/api/v2/storefront/payment_methods*'
|
|
52
53
|
].freeze
|
|
53
54
|
|
|
@@ -60,7 +61,7 @@ module SpreeCmCommissioner
|
|
|
60
61
|
invalidate_single_record(model_class: model_class, id: id)
|
|
61
62
|
end
|
|
62
63
|
|
|
63
|
-
# eg. 'Spree::Product', 123 => ['/api/v2/
|
|
64
|
+
# eg. 'Spree::Product', 123 => ['/api/v2/hangmeas/products/123*', '/api/v2/hangmeas/products/slug-of-product*']
|
|
64
65
|
def invalidate_single_record(model_class:, id:)
|
|
65
66
|
return failure(nil, "#{model_class.name} ID is required") if id.blank?
|
|
66
67
|
|
|
@@ -79,17 +80,27 @@ module SpreeCmCommissioner
|
|
|
79
80
|
# The record's own class first (a Show found as a Spree::Taxon keeps its show paths), then the
|
|
80
81
|
# model asked for, since STI subclasses such as Spree::Gateway::Payway have no entry of their own.
|
|
81
82
|
patterns = single_api_pattern[record.class.name] || single_api_pattern[model_class.name]
|
|
82
|
-
return patterns if patterns.equal?(PAYMENT_METHOD_PATTERNS)
|
|
83
|
+
return expand_tenant_slug(patterns, SpreeCmCommissioner::Tenant.pluck(:slug)) if patterns.equal?(PAYMENT_METHOD_PATTERNS)
|
|
83
84
|
|
|
84
85
|
tenant = tenant_of(record)
|
|
85
86
|
patterns.select { |pattern| pattern.start_with?(TENANT_PREFIX) == tenant.present? }.map do |pattern|
|
|
86
87
|
pattern.gsub(/:(\w+)/) do
|
|
87
88
|
key = Regexp.last_match(1)
|
|
88
|
-
record.public_send(key).to_s
|
|
89
|
+
key == 'tenant_slug' ? tenant.slug : record.public_send(key).to_s
|
|
89
90
|
end
|
|
90
91
|
end
|
|
91
92
|
end
|
|
92
93
|
|
|
94
|
+
# One copy of each `:tenant_slug` pattern per tenant; storefront patterns pass through as is.
|
|
95
|
+
def expand_tenant_slug(patterns, slugs)
|
|
96
|
+
slugs = slugs.compact_blank
|
|
97
|
+
patterns.flat_map do |pattern|
|
|
98
|
+
next [pattern] unless pattern.start_with?(TENANT_PREFIX)
|
|
99
|
+
|
|
100
|
+
slugs.map { |slug| pattern.sub(':tenant_slug', slug) }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
93
104
|
# A product, vendor or homepage section has its own tenant; a taxon, show or voting session has
|
|
94
105
|
# its vendor's. Menus, seat layouts and homepage backgrounds are BookMe+ only.
|
|
95
106
|
def tenant_of(record)
|
|
@@ -106,23 +117,23 @@ module SpreeCmCommissioner
|
|
|
106
117
|
# The app reads menus from homepage_data; nothing calls /menus any more.
|
|
107
118
|
'Spree::Menu' => ['/api/v2/storefront/homepage_data*'],
|
|
108
119
|
'Spree::Taxon' => [
|
|
109
|
-
'/api/v2/
|
|
110
|
-
'/api/v2/
|
|
120
|
+
'/api/v2/:tenant_slug/taxons/:id*',
|
|
121
|
+
'/api/v2/:tenant_slug/taxons/:permalink*',
|
|
111
122
|
'/api/v2/storefront/taxons/:id*',
|
|
112
123
|
'/api/v2/storefront/taxons/:permalink*'
|
|
113
124
|
],
|
|
114
125
|
'Spree::Product' => [
|
|
115
|
-
'/api/v2/
|
|
116
|
-
'/api/v2/
|
|
126
|
+
'/api/v2/:tenant_slug/products/:id*',
|
|
127
|
+
'/api/v2/:tenant_slug/products/:slug*',
|
|
117
128
|
'/api/v2/storefront/products/:id*',
|
|
118
129
|
'/api/v2/storefront/products/:slug*'
|
|
119
130
|
],
|
|
120
|
-
'SpreeCmCommissioner::Show' => ['/api/v2/
|
|
131
|
+
'SpreeCmCommissioner::Show' => ['/api/v2/:tenant_slug/shows/:slug*'],
|
|
121
132
|
'SpreeCmCommissioner::VotingSession' => [
|
|
122
|
-
'/api/v2/
|
|
133
|
+
'/api/v2/:tenant_slug/voting_sessions/:id/voting_contestants*'
|
|
123
134
|
],
|
|
124
135
|
'Spree::Vendor' => [
|
|
125
|
-
'/api/v2/
|
|
136
|
+
'/api/v2/:tenant_slug/vendors/:id*',
|
|
126
137
|
'/api/v2/storefront/vendors/:id*',
|
|
127
138
|
'/api/v2/storefront/accommodations/:id*',
|
|
128
139
|
'/api/v2/storefront/accommodations/:slug*'
|
|
@@ -4,10 +4,7 @@ module SpreeCmCommissioner
|
|
|
4
4
|
# serve them, which MaintenanceTasks::CacheInvalidation sends, as for Invalidate. `tenant` is
|
|
5
5
|
# required so a clear-all never pays for both namespaces by accident: nil is the BookMe+
|
|
6
6
|
# storefront (/api/v2/storefront/ paths), a SpreeCmCommissioner::Tenant is that tenant's API
|
|
7
|
-
# (/api/v2
|
|
8
|
-
#
|
|
9
|
-
# Tenant APIs take the tenant from the OAuth token, not the URL, so every tenant shares one path
|
|
10
|
-
# today; taking the record keeps callers the same if tenant paths ever carry it.
|
|
7
|
+
# (/api/v2/<tenant slug>/ paths).
|
|
11
8
|
#
|
|
12
9
|
# eg. InvalidateAll.call(model: 'SpreeCmCommissioner::HomepageSection', tenant: nil)
|
|
13
10
|
class InvalidateAll
|
|
@@ -20,11 +17,11 @@ module SpreeCmCommissioner
|
|
|
20
17
|
'Spree::Menu' => ['/api/v2/storefront/homepage_data*'],
|
|
21
18
|
'Spree::Taxon' => ['/api/v2/storefront/taxons/*'],
|
|
22
19
|
'Spree::Product' => [
|
|
23
|
-
'/api/v2/
|
|
20
|
+
'/api/v2/:tenant_slug/products/*',
|
|
24
21
|
'/api/v2/storefront/products/*'
|
|
25
22
|
],
|
|
26
23
|
'Spree::Vendor' => [
|
|
27
|
-
'/api/v2/
|
|
24
|
+
'/api/v2/:tenant_slug/vendors/*',
|
|
28
25
|
'/api/v2/storefront/vendors/*',
|
|
29
26
|
'/api/v2/storefront/accommodations/*'
|
|
30
27
|
],
|
|
@@ -44,7 +41,8 @@ module SpreeCmCommissioner
|
|
|
44
41
|
|
|
45
42
|
# Side-effect free, so the clear-cache dialog can show what a clear would send.
|
|
46
43
|
def patterns_for(model, tenant:)
|
|
47
|
-
API_PATTERNS.fetch(model, []).select { |pattern| pattern.start_with?(Invalidate::TENANT_PREFIX) == tenant.present? }
|
|
44
|
+
patterns = API_PATTERNS.fetch(model, []).select { |pattern| pattern.start_with?(Invalidate::TENANT_PREFIX) == tenant.present? }
|
|
45
|
+
tenant ? Invalidate.new.expand_tenant_slug(patterns, [tenant.slug]) : patterns
|
|
48
46
|
end
|
|
49
47
|
|
|
50
48
|
# updated_at keys Spree's serializer and collection caches, so only this storefront's records
|
|
@@ -72,7 +72,8 @@ module SpreeCmCommissioner
|
|
|
72
72
|
device_id: options[:device_id],
|
|
73
73
|
last_ip_address: options[:last_ip_address]
|
|
74
74
|
)
|
|
75
|
-
|
|
75
|
+
sellable_variants = store.variants.joins(:product).merge(Spree::Product.sellable_on(Spree::Product.sales_channel_for(tenant)))
|
|
76
|
+
line_items_params.each { |line_item_params| build_line_item!(order, sellable_variants, line_item_params) }
|
|
76
77
|
|
|
77
78
|
ActiveRecord::Base.transaction do
|
|
78
79
|
raise StandardError, order_error_message(order) unless order.save
|
|
@@ -105,8 +106,10 @@ module SpreeCmCommissioner
|
|
|
105
106
|
|
|
106
107
|
# Builds (unsaved) an order.line_items member — persisted together with the order via
|
|
107
108
|
# order.save's cascading autosave of new has_many records, same as TransitOrder::Create.
|
|
108
|
-
def build_line_item!(order,
|
|
109
|
-
variant =
|
|
109
|
+
def build_line_item!(order, sellable_variants, line_item_params)
|
|
110
|
+
variant = sellable_variants.find_by(id: line_item_params[:variant_id])
|
|
111
|
+
raise StandardError, I18n.t('spree_cm_commissioner.draft_order.variant_not_available') if variant.nil?
|
|
112
|
+
|
|
110
113
|
options = (line_item_params[:options] || {}).to_h.with_indifferent_access
|
|
111
114
|
permitted_opts = ::Spree::PermittedAttributes.line_item_attributes.flatten.index_with { |attribute| options[attribute] }
|
|
112
115
|
opts = permitted_opts.merge(currency: order.currency).compact
|
|
@@ -35,10 +35,10 @@ module SpreeCmCommissioner
|
|
|
35
35
|
return success(:skipped) unless firestore_available?
|
|
36
36
|
|
|
37
37
|
products = active_products(taxon).to_a
|
|
38
|
-
return success(:skipped) if products.empty?
|
|
39
|
-
|
|
40
|
-
sums = aggregate_quantities(products)
|
|
41
38
|
existing_products = existing_products_data(taxon)
|
|
39
|
+
return success(:skipped) if products.empty? && existing_products.empty?
|
|
40
|
+
|
|
41
|
+
sums = products.empty? ? {} : aggregate_quantities(products)
|
|
42
42
|
now = Time.zone.now
|
|
43
43
|
|
|
44
44
|
changed = {}
|
|
@@ -51,6 +51,8 @@ module SpreeCmCommissioner
|
|
|
51
51
|
changed[product.id.to_s] = build_entry(product, status, quantities, existing_products, now)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
prune_stale_entries(changed, products, existing_products)
|
|
55
|
+
|
|
54
56
|
return success(:unchanged) if changed.empty?
|
|
55
57
|
|
|
56
58
|
event_document(taxon).set({ products: changed, server_time: now }, merge: true)
|
|
@@ -66,6 +68,20 @@ module SpreeCmCommissioner
|
|
|
66
68
|
|
|
67
69
|
private
|
|
68
70
|
|
|
71
|
+
# The write below is always `merge: true`, which only ever adds or updates map entries — it
|
|
72
|
+
# never drops one. So a product that stops being active (unlisted/unlinked from its section,
|
|
73
|
+
# discontinued, status flipped off — see `active_products`) would otherwise keep its last
|
|
74
|
+
# stock status in Firestore forever, and the app would go on rendering the removed product.
|
|
75
|
+
# Deleting the `products.<id>` field (nested FieldValue.delete, carried in the same merge set)
|
|
76
|
+
# is what actually removes it. existing_products is read even when no product is active, so an
|
|
77
|
+
# event whose every ticket got unlisted still gets its stale entries cleared.
|
|
78
|
+
def prune_stale_entries(changed, products, existing_products)
|
|
79
|
+
active_ids = products.map { |product| product.id.to_s }
|
|
80
|
+
(existing_products.keys - active_ids).each do |stale_id|
|
|
81
|
+
changed[stale_id] = firestore.field_delete
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
69
85
|
# `quantities` is the [available, on_hold, locked, locked_on_hold] tuple from
|
|
70
86
|
# `aggregate_quantities`. `previous_status` is read back from what's already stored, not
|
|
71
87
|
# recomputed — it's the bucket this product was in on the last publish, if any.
|
|
@@ -117,6 +133,16 @@ module SpreeCmCommissioner
|
|
|
117
133
|
# true. Caught via a real event whose products all had available_on: nil: the poll job was
|
|
118
134
|
# returning :skipped for it on every cycle, so Firestore just sat frozen with stale data.
|
|
119
135
|
#
|
|
136
|
+
# `preview: false` for the same reason unlisted products are excluded: events/{event_id} is
|
|
137
|
+
# publicly readable (no auth on the Firestore rule), so a preview product — active but held
|
|
138
|
+
# back from the public pending a PreviewRole, product_decorator.rb `publicly_available` — must
|
|
139
|
+
# not leak a live stock-status entry there before launch.
|
|
140
|
+
#
|
|
141
|
+
# `offline: false` because an offline product is deliberately not inventory-monitored (the
|
|
142
|
+
# admin form labels it "not monitored in Inventory Monitor", and InventoryItems finders like
|
|
143
|
+
# RecentlyChangedFinder already filter it out) — its Redis pools aren't maintained, so any
|
|
144
|
+
# stock status published for it would be meaningless.
|
|
145
|
+
#
|
|
120
146
|
# `where(status: 'active')` instead of core's `Product.active` scope: that scope also joins
|
|
121
147
|
# variants/prices in to filter by currency (`with_currency`), and `spree_variants` carries its
|
|
122
148
|
# own `available_on`/`discontinue_on` columns — once that join is present, the raw,
|
|
@@ -133,7 +159,7 @@ module SpreeCmCommissioner
|
|
|
133
159
|
# is excluded here too rather than still getting a live stock-status entry in Firestore.
|
|
134
160
|
def active_products(taxon)
|
|
135
161
|
now = Time.zone.now
|
|
136
|
-
taxon.event_products.ecommerce.where(status: 'active')
|
|
162
|
+
taxon.event_products.ecommerce.where(status: 'active', preview: false, offline: false)
|
|
137
163
|
.where('available_on IS NULL OR available_on <= ?', now)
|
|
138
164
|
.where('discontinue_on IS NULL OR discontinue_on > ?', now)
|
|
139
165
|
.where(id: taxon.children_classifications.where(visible: true).select(:product_id))
|
|
@@ -16,8 +16,12 @@ module Spree
|
|
|
16
16
|
# store_accessor key (public_metadata), not a DB column, so it must be permitted explicitly
|
|
17
17
|
@@product_attributes << :enable_telegram_alert unless @@product_attributes.include?(:enable_telegram_alert)
|
|
18
18
|
@@product_attributes << :enable_send_receipt unless @@product_attributes.include?(:enable_send_receipt)
|
|
19
|
+
@@product_attributes << :enable_confirmation_email unless @@product_attributes.include?(:enable_confirmation_email)
|
|
19
20
|
@@product_attributes << :enable_social_sharing_email unless @@product_attributes.include?(:enable_social_sharing_email)
|
|
20
21
|
|
|
22
|
+
# Virtual writer toggling the platform bit of sales_channel (SalesChannelBitwise)
|
|
23
|
+
@@product_attributes << :sellable_on_platform unless @@product_attributes.include?(:sellable_on_platform)
|
|
24
|
+
|
|
21
25
|
# Permitted all guest attributes for now as permitting only some guest attributes is not working by design
|
|
22
26
|
# of spree add_item service, if we want to only permit some guest attributes,
|
|
23
27
|
# we need to override most part of add_item service.
|
data/config/locales/en.yml
CHANGED
|
@@ -320,6 +320,7 @@ en:
|
|
|
320
320
|
preset: "Preset"
|
|
321
321
|
enable_telegram_alert: "Enable Telegram Alert"
|
|
322
322
|
enable_send_receipt: "Enable Send Receipt"
|
|
323
|
+
enable_confirmation_email: "Enable Confirmation Email"
|
|
323
324
|
enable_social_sharing_email: "Enable Social Sharing Email"
|
|
324
325
|
telegram_alert_channels: "Telegram Alert Channels"
|
|
325
326
|
telegram_chat_name: "Telegram Chat Name"
|
|
@@ -1342,6 +1343,7 @@ en:
|
|
|
1342
1343
|
failed: "Failed"
|
|
1343
1344
|
draft_order:
|
|
1344
1345
|
line_items_missing: "Line items are missing"
|
|
1346
|
+
variant_not_available: "This item is not available for purchase here"
|
|
1345
1347
|
inventory_item:
|
|
1346
1348
|
variant_taken_on: "The variant is taken on %{date}"
|
|
1347
1349
|
topup_draft_order:
|
data/config/locales/km.yml
CHANGED
|
@@ -919,6 +919,7 @@ km:
|
|
|
919
919
|
failed: "បរាជ័យ"
|
|
920
920
|
draft_order:
|
|
921
921
|
line_items_missing: "គ្មានទំនិញនៅក្នុងកន្ត្រកទេ"
|
|
922
|
+
variant_not_available: "ទំនិញនេះមិនអាចទិញនៅទីនេះបានទេ"
|
|
922
923
|
inventory_item:
|
|
923
924
|
variant_taken_on: "វ៉ារ្យ៉ង់នេះមានរួចហើយនៅថ្ងៃ %{date}"
|
|
924
925
|
topup_draft_order:
|
data/config/routes.rb
CHANGED
|
@@ -513,17 +513,15 @@ Spree::Core::Engine.add_routes do
|
|
|
513
513
|
end
|
|
514
514
|
|
|
515
515
|
namespace :organizer do
|
|
516
|
-
resources :tickets do
|
|
517
|
-
resources :ticket_images
|
|
518
|
-
end
|
|
519
|
-
resources :images
|
|
520
|
-
resource :s3_signed_urls
|
|
521
516
|
resources :invites
|
|
522
517
|
resources :invite_guests, only: %i[show update]
|
|
523
518
|
resources :invite_crews, only: %i[show update]
|
|
524
519
|
end
|
|
525
520
|
|
|
526
|
-
|
|
521
|
+
# Drawn twice below: legacy /api/v2/tenant/... (tenant from the OAuth token) and
|
|
522
|
+
# /api/v2/:tenant_slug/... (tenant from the URL). Tenant APIs are CDN-cached by URL, so only the
|
|
523
|
+
# slug form gives each tenant its own cache key; see Spree::Api::V2::Tenant::BaseController.
|
|
524
|
+
concern :tenant_api do
|
|
527
525
|
resource :token, controller: :access_tokens, only: [:create]
|
|
528
526
|
resource :telegram_oauth_token, controller: :telegram_oauth_tokens, only: [:create]
|
|
529
527
|
resources :vendors
|
|
@@ -665,6 +663,10 @@ Spree::Core::Engine.add_routes do
|
|
|
665
663
|
resources :pricing_previews, only: [:index]
|
|
666
664
|
end
|
|
667
665
|
|
|
666
|
+
namespace :tenant do
|
|
667
|
+
concerns :tenant_api
|
|
668
|
+
end
|
|
669
|
+
|
|
668
670
|
namespace :storefront do
|
|
669
671
|
resource :token, controller: :access_tokens, only: [:create]
|
|
670
672
|
resource :telegram_oauth_token, controller: :telegram_oauth_tokens, only: [:create]
|
|
@@ -869,6 +871,12 @@ Spree::Core::Engine.add_routes do
|
|
|
869
871
|
resources :event_charts, only: %i[index show]
|
|
870
872
|
end
|
|
871
873
|
end
|
|
874
|
+
|
|
875
|
+
# Keep last in v2: the slug segment overlaps every literal namespace above, which
|
|
876
|
+
# TenantSlugConstraint reserves.
|
|
877
|
+
scope ':tenant_slug', module: :tenant, as: :tenant_scoped, constraints: SpreeCmCommissioner::TenantSlugConstraint.new do
|
|
878
|
+
concerns :tenant_api
|
|
879
|
+
end
|
|
872
880
|
end
|
|
873
881
|
|
|
874
882
|
namespace :json_ld do
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class AddSalesChannelToSpreeProducts < ActiveRecord::Migration[7.0]
|
|
2
|
+
def up
|
|
3
|
+
# Default 3 (direct + platform) backfills every existing row with today's behaviour:
|
|
4
|
+
# tenant products stay visible on both their tenant storefront and BookMe+.
|
|
5
|
+
add_column :spree_products, :sales_channel, :integer, default: 0b11, null: false
|
|
6
|
+
|
|
7
|
+
# BookMe+'s own products have no tenant storefront to sell on.
|
|
8
|
+
execute 'UPDATE spree_products SET sales_channel = 2 WHERE tenant_id IS NULL'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def down
|
|
12
|
+
remove_column :spree_products, :sales_channel
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
class S3UrlGenerator
|
|
3
|
+
# Only keys S3PresignedUrlBuilder hands out to API callers. Without this, a client-supplied
|
|
4
|
+
# URL could point at any object in the bucket (e.g. another guest's ID card) and have it
|
|
5
|
+
# copied into their own attachment.
|
|
6
|
+
UPLOAD_KEY_FORMAT = %r{\Auploads/[\w-]+/[^/]+\z}
|
|
7
|
+
|
|
3
8
|
# https://docs.aws.amazon.com/sdk-for-ruby/v1/api/AWS/S3/S3Object.html
|
|
4
9
|
# The bucket is private, ie all the public access is blocked
|
|
5
10
|
# key = "uploads/5f31/1697038252270.jpg"
|
|
11
|
+
# Returns nil for any other key; callers treat that as a failed fetch.
|
|
6
12
|
def self.s3_presigned_url(url)
|
|
7
|
-
key = url
|
|
13
|
+
key = upload_key(url)
|
|
14
|
+
return nil if key.nil?
|
|
8
15
|
|
|
9
16
|
s3 = Aws::S3::Resource.new(region: ENV.fetch('AWS_REGION'),
|
|
10
17
|
access_key_id: ENV.fetch('AWS_ACCESS_KEY_ID'),
|
|
@@ -16,5 +23,10 @@ module SpreeCmCommissioner
|
|
|
16
23
|
object = bucket.object(key)
|
|
17
24
|
object.presigned_url(:get, expires_in: 3600)
|
|
18
25
|
end
|
|
26
|
+
|
|
27
|
+
def self.upload_key(url)
|
|
28
|
+
key = url.to_s.split('amazonaws.com/').last
|
|
29
|
+
key if key&.match?(UPLOAD_KEY_FORMAT)
|
|
30
|
+
end
|
|
19
31
|
end
|
|
20
32
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
# Guards the `/api/v2/:tenant_slug/...` routes, which share their first path segment with every
|
|
3
|
+
# literal v2 namespace. Without it `/api/v2/storefront/products` could resolve as tenant
|
|
4
|
+
# "storefront" depending on which engine draws its routes first. A pure string check, so routing
|
|
5
|
+
# never hits the DB; an unknown but well-formed slug still routes and 404s in
|
|
6
|
+
# Spree::Api::V2::Tenant::BaseController.
|
|
7
|
+
#
|
|
8
|
+
# RESERVED_SLUGS lives here rather than on SpreeCmCommissioner::Tenant so routes don't load an
|
|
9
|
+
# ActiveRecord model at boot. The routing spec fails when a new v2 namespace is missing from it.
|
|
10
|
+
class TenantSlugConstraint
|
|
11
|
+
RESERVED_SLUGS = %w[storefront platform tenant organizer operator].freeze
|
|
12
|
+
SLUG_FORMAT = /\A[a-z0-9][a-z0-9_-]*\z/
|
|
13
|
+
|
|
14
|
+
def self.valid_slug?(slug)
|
|
15
|
+
slug = slug.to_s
|
|
16
|
+
SLUG_FORMAT.match?(slug) && RESERVED_SLUGS.exclude?(slug)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def matches?(request)
|
|
20
|
+
self.class.valid_slug?(request.path_parameters[:tenant_slug])
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -10,6 +10,7 @@ require 'spree_vpago'
|
|
|
10
10
|
|
|
11
11
|
require 'spree_cm_commissioner/engine'
|
|
12
12
|
require 'spree_cm_commissioner/version'
|
|
13
|
+
require 'spree_cm_commissioner/tenant_slug_constraint'
|
|
13
14
|
require 'spree_cm_commissioner/passenger_option'
|
|
14
15
|
require 'spree_cm_commissioner/payment_method_group'
|
|
15
16
|
require 'spree_cm_commissioner/calendar_event'
|
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.
|
|
4
|
+
version: 2.14.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- You
|
|
@@ -815,6 +815,7 @@ files:
|
|
|
815
815
|
- app/controllers/concerns/spree_cm_commissioner/anonymous_booking_guard.rb
|
|
816
816
|
- app/controllers/concerns/spree_cm_commissioner/content_cachable.rb
|
|
817
817
|
- app/controllers/concerns/spree_cm_commissioner/exception_notificable.rb
|
|
818
|
+
- app/controllers/concerns/spree_cm_commissioner/guest_owner_authorization.rb
|
|
818
819
|
- app/controllers/concerns/spree_cm_commissioner/load_test_account_management.rb
|
|
819
820
|
- app/controllers/concerns/spree_cm_commissioner/load_test_account_scope.rb
|
|
820
821
|
- app/controllers/concerns/spree_cm_commissioner/load_test_bypassable.rb
|
|
@@ -954,13 +955,9 @@ files:
|
|
|
954
955
|
- app/controllers/spree/api/v2/operator/recalculate_tickets_controller.rb
|
|
955
956
|
- app/controllers/spree/api/v2/operator/taxons_controller.rb
|
|
956
957
|
- app/controllers/spree/api/v2/organizer/base_controller.rb
|
|
957
|
-
- app/controllers/spree/api/v2/organizer/images_controller.rb
|
|
958
958
|
- app/controllers/spree/api/v2/organizer/invite_crews_controller.rb
|
|
959
959
|
- app/controllers/spree/api/v2/organizer/invite_guests_controller.rb
|
|
960
960
|
- app/controllers/spree/api/v2/organizer/invites_controller.rb
|
|
961
|
-
- app/controllers/spree/api/v2/organizer/s3_signed_urls_controller.rb
|
|
962
|
-
- app/controllers/spree/api/v2/organizer/ticket_images_controller.rb
|
|
963
|
-
- app/controllers/spree/api/v2/organizer/tickets_controller.rb
|
|
964
961
|
- app/controllers/spree/api/v2/platform/homepage_section_relatable_options_controller.rb
|
|
965
962
|
- app/controllers/spree/api/v2/platform/option_types_controller_decorator.rb
|
|
966
963
|
- app/controllers/spree/api/v2/platform/places_controller.rb
|
|
@@ -1517,6 +1514,7 @@ files:
|
|
|
1517
1514
|
- app/models/concerns/spree_cm_commissioner/reschedule_request_state_machine.rb
|
|
1518
1515
|
- app/models/concerns/spree_cm_commissioner/route_order_countable.rb
|
|
1519
1516
|
- app/models/concerns/spree_cm_commissioner/route_type.rb
|
|
1517
|
+
- app/models/concerns/spree_cm_commissioner/sales_channel_bitwise.rb
|
|
1520
1518
|
- app/models/concerns/spree_cm_commissioner/scoped_fee_rule.rb
|
|
1521
1519
|
- app/models/concerns/spree_cm_commissioner/service_calendar_type.rb
|
|
1522
1520
|
- app/models/concerns/spree_cm_commissioner/service_origin.rb
|
|
@@ -1531,6 +1529,7 @@ files:
|
|
|
1531
1529
|
- app/models/concerns/spree_cm_commissioner/user_identity.rb
|
|
1532
1530
|
- app/models/concerns/spree_cm_commissioner/user_notification.rb
|
|
1533
1531
|
- app/models/concerns/spree_cm_commissioner/user_preference.rb
|
|
1532
|
+
- app/models/concerns/spree_cm_commissioner/user_uploaded_image.rb
|
|
1534
1533
|
- app/models/concerns/spree_cm_commissioner/variant_metadata.rb
|
|
1535
1534
|
- app/models/concerns/spree_cm_commissioner/variant_options_concern.rb
|
|
1536
1535
|
- app/models/concerns/spree_cm_commissioner/variant_rate_plannable.rb
|
|
@@ -1913,6 +1912,7 @@ files:
|
|
|
1913
1912
|
- app/overrides/spree/admin/products/_form/allow_transfer.html.erb.deface
|
|
1914
1913
|
- app/overrides/spree/admin/products/_form/available_on.html.erb.deface
|
|
1915
1914
|
- app/overrides/spree/admin/products/_form/discontinue_on.html.erb.deface
|
|
1915
|
+
- app/overrides/spree/admin/products/_form/enable_confirmation_email.html.erb.deface
|
|
1916
1916
|
- app/overrides/spree/admin/products/_form/enable_inventory_hold.html.erb.deface
|
|
1917
1917
|
- app/overrides/spree/admin/products/_form/enable_send_receipt.html.erb.deface
|
|
1918
1918
|
- app/overrides/spree/admin/products/_form/enable_social_sharing_email.html.erb.deface
|
|
@@ -1928,6 +1928,7 @@ files:
|
|
|
1928
1928
|
- app/overrides/spree/admin/products/_form/replace_vendor_label.html.erb.deface
|
|
1929
1929
|
- app/overrides/spree/admin/products/_form/required_self_check_in_location.html.erb.deface
|
|
1930
1930
|
- app/overrides/spree/admin/products/_form/reveal_description_to_user.html.erb.deface
|
|
1931
|
+
- app/overrides/spree/admin/products/_form/sellable_on_platform.html.erb.deface
|
|
1931
1932
|
- app/overrides/spree/admin/products/_form/subscribable_checkbox.html.erb.deface
|
|
1932
1933
|
- app/overrides/spree/admin/products/_form/taxons.html.erb.deface
|
|
1933
1934
|
- app/overrides/spree/admin/products/_form/use_video_as_default.html.erb.deface
|
|
@@ -2106,13 +2107,10 @@ files:
|
|
|
2106
2107
|
- app/serializers/spree/api/v2/platform/service_fee_rule_serializer.rb
|
|
2107
2108
|
- app/serializers/spree/v2/organizer/asset_serializer.rb
|
|
2108
2109
|
- app/serializers/spree/v2/organizer/base_serializer.rb
|
|
2109
|
-
- app/serializers/spree/v2/organizer/image_serializer.rb
|
|
2110
2110
|
- app/serializers/spree/v2/organizer/invite_crews_serializer.rb
|
|
2111
2111
|
- app/serializers/spree/v2/organizer/invite_guest_serializer.rb
|
|
2112
2112
|
- app/serializers/spree/v2/organizer/invite_serializer.rb
|
|
2113
|
-
- app/serializers/spree/v2/organizer/s3_signed_url_serializer.rb
|
|
2114
2113
|
- app/serializers/spree/v2/organizer/taxon_serializer.rb
|
|
2115
|
-
- app/serializers/spree/v2/organizer/ticket_serializer.rb
|
|
2116
2114
|
- app/serializers/spree/v2/organizer/user_serializer.rb
|
|
2117
2115
|
- app/serializers/spree/v2/storefront/accommodation_serializer.rb
|
|
2118
2116
|
- app/serializers/spree/v2/storefront/active_homepage_event_serializer.rb
|
|
@@ -3776,6 +3774,7 @@ files:
|
|
|
3776
3774
|
- db/migrate/20261013000002_move_hidden_from_spree_option_types_to_product_option_types.rb
|
|
3777
3775
|
- db/migrate/20261016000001_add_index_to_spree_line_items_number.rb
|
|
3778
3776
|
- db/migrate/20261021000001_add_section_type_to_cm_homepage_sections.rb
|
|
3777
|
+
- db/migrate/20261103000001_add_sales_channel_to_spree_products.rb
|
|
3779
3778
|
- docker-compose.yml
|
|
3780
3779
|
- docs/api/scoped-access-token-endpoints.md
|
|
3781
3780
|
- docs/option_types/attr_types.md
|
|
@@ -3833,6 +3832,7 @@ files:
|
|
|
3833
3832
|
- lib/spree_cm_commissioner/stay_prices.rb
|
|
3834
3833
|
- lib/spree_cm_commissioner/store_credit_currency_summary.rb
|
|
3835
3834
|
- lib/spree_cm_commissioner/store_credit_summary.rb
|
|
3835
|
+
- lib/spree_cm_commissioner/tenant_slug_constraint.rb
|
|
3836
3836
|
- lib/spree_cm_commissioner/test_helper/factories/address_factory.rb
|
|
3837
3837
|
- lib/spree_cm_commissioner/test_helper/factories/agency_factory.rb
|
|
3838
3838
|
- lib/spree_cm_commissioner/test_helper/factories/asset_factory.rb
|