spree_core 5.0.3 → 5.1.0.beta

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/images/logo.png +0 -0
  3. data/app/finders/spree/products/find.rb +28 -1
  4. data/app/finders/spree/taxons/find.rb +1 -1
  5. data/app/helpers/spree/integrations_helper.rb +15 -0
  6. data/app/javascript/spree/core/controllers/disable_submit_button_controller.js +19 -0
  7. data/app/mailers/spree/invitation_mailer.rb +24 -0
  8. data/app/models/concerns/spree/integrations_concern.rb +11 -0
  9. data/app/models/concerns/spree/product_scopes.rb +6 -6
  10. data/app/models/concerns/spree/translatable_resource.rb +4 -0
  11. data/app/models/concerns/spree/translatable_resource_scopes.rb +17 -3
  12. data/app/models/concerns/spree/unique_name.rb +2 -0
  13. data/app/models/concerns/spree/user_management.rb +33 -0
  14. data/app/models/concerns/spree/user_methods.rb +19 -0
  15. data/app/models/concerns/spree/user_roles.rb +43 -17
  16. data/app/models/spree/ability.rb +8 -6
  17. data/app/models/spree/asset.rb +1 -6
  18. data/app/models/spree/base.rb +1 -0
  19. data/app/models/spree/base_analytics_event_handler.rb +7 -2
  20. data/app/models/spree/classification.rb +13 -0
  21. data/app/models/spree/custom_domain.rb +2 -1
  22. data/app/models/spree/export.rb +1 -1
  23. data/app/models/spree/integration.rb +63 -0
  24. data/app/models/spree/invitation.rb +153 -0
  25. data/app/models/spree/invitations/store.rb +6 -0
  26. data/app/models/spree/order.rb +17 -1
  27. data/app/models/spree/order_merger.rb +7 -5
  28. data/app/models/spree/page_blocks/products/buy_buttons.rb +8 -0
  29. data/app/models/spree/page_blocks/products/quantity_selector.rb +4 -0
  30. data/app/models/spree/page_blocks/products/variant_picker.rb +4 -0
  31. data/app/models/spree/page_sections/featured_product.rb +4 -0
  32. data/app/models/spree/page_sections/image_banner.rb +12 -0
  33. data/app/models/spree/page_sections/image_with_text.rb +12 -0
  34. data/app/models/spree/page_sections/newsletter.rb +1 -1
  35. data/app/models/spree/page_sections/rich_text.rb +11 -0
  36. data/app/models/spree/page_sections/video.rb +8 -0
  37. data/app/models/spree/product.rb +10 -2
  38. data/app/models/spree/product_property.rb +1 -1
  39. data/app/models/spree/property.rb +3 -1
  40. data/app/models/spree/reports/sales_total.rb +5 -1
  41. data/app/models/spree/role.rb +16 -0
  42. data/app/models/spree/role_user.rb +32 -1
  43. data/app/models/spree/shipment.rb +1 -1
  44. data/app/models/spree/shipment_handler.rb +1 -0
  45. data/app/models/spree/shipping_method.rb +1 -1
  46. data/app/models/spree/store.rb +11 -4
  47. data/app/models/spree/store_credit_category.rb +4 -0
  48. data/app/models/spree/taxon.rb +4 -3
  49. data/app/models/spree/variant.rb +9 -1
  50. data/app/services/spree/country_to_timezone.rb +273 -0
  51. data/app/services/spree/seeds/admin_user.rb +4 -2
  52. data/app/services/spree/seeds/all.rb +3 -1
  53. data/app/services/spree/seeds/digital_delivery.rb +20 -0
  54. data/app/services/spree/seeds/returns_environment.rb +27 -0
  55. data/app/services/spree/seeds/tax_categories.rb +12 -0
  56. data/app/services/spree/stores/settings_defaults_by_country.rb +38 -0
  57. data/app/services/spree/tags/bulk_add.rb +13 -7
  58. data/app/views/spree/invitation_mailer/invitation_accepted.html.erb +12 -0
  59. data/app/views/spree/invitation_mailer/invitation_email.html.erb +21 -0
  60. data/config/locales/en.yml +48 -9
  61. data/db/migrate/20250407085228_create_spree_integrations.rb +12 -0
  62. data/db/migrate/20250410061306_create_spree_invitations.rb +20 -0
  63. data/db/migrate/20250418174652_add_resource_to_spree_role_users.rb +8 -0
  64. data/db/migrate/20250508060800_add_selected_locale_to_spree_admin_users.rb +8 -0
  65. data/db/migrate/20250509143831_add_session_id_to_spree_assets.rb +5 -0
  66. data/lib/generators/spree/authentication/devise/devise_generator.rb +5 -2
  67. data/lib/generators/spree/authentication/devise/templates/authentication_helpers.rb.tt +3 -3
  68. data/lib/generators/spree/install/install_generator.rb +5 -0
  69. data/lib/spree/core/controller_helpers/auth.rb +16 -14
  70. data/lib/spree/core/controller_helpers/currency.rb +11 -0
  71. data/lib/spree/core/controller_helpers/order.rb +2 -1
  72. data/lib/spree/core/controller_helpers/strong_parameters.rb +3 -2
  73. data/lib/spree/core/engine.rb +13 -2
  74. data/lib/spree/core/version.rb +1 -1
  75. data/lib/spree/core.rb +1 -0
  76. data/lib/spree/permitted_attributes.rb +118 -13
  77. data/lib/spree/testing_support/capybara_config.rb +1 -1
  78. data/lib/spree/testing_support/factories/integration_factory.rb +7 -0
  79. data/lib/spree/testing_support/factories/invitation_factory.rb +6 -0
  80. data/lib/spree/testing_support/factories/promotion_action_factory.rb +4 -0
  81. data/lib/spree/testing_support/factories/stock_item_factory.rb +5 -1
  82. data/lib/spree/testing_support/factories/user_factory.rb +14 -1
  83. data/lib/spree/translation_migrations.rb +27 -15
  84. data/lib/tasks/core.rake +8 -0
  85. metadata +41 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61dc753fc9921cc827589519ccd19bdd4db91ff9ba052174e84e6681578b67a3
4
- data.tar.gz: 9e992274856bc445de5a7b4041126721e992e1554aa0d677ff87c908a52c6cc9
3
+ metadata.gz: 7c1195535152943b227b48f47f34f8a53164727cff509e72ef64a720b4dfbf36
4
+ data.tar.gz: e88f4d0212bf72d2ef986faae3bd97a1793cffa2fa215a7d078f47044a1ad8f8
5
5
  SHA512:
6
- metadata.gz: 6a6b74d64d7be259de034cfc831f6eaf9877f9f801a3312adda2af3983060948e8434e2ef4d7ad4b3fdbeac7ff366ae6d87a210b27f7172769abbda9243e46b5
7
- data.tar.gz: f31e47f912ee551e853fe190e1acd34009d3c191cc9666797db4986fbecbb445062e6533de35489998dbdea569d85fd9bf0fd54ee3e27047c5c373e6c2a62ad6
6
+ metadata.gz: 702032f4dbd79afda63c303e4f2a957e982f6cac18f95dded6f241a920f6600b3ea5e08253780185b7e87ec4d676dd4fa7d6fa92c99e43e4cb79420afeef94ad
7
+ data.tar.gz: 72a2841b3265909bcc97404cc8c504216dc65033b251010ba96acc2e666ece5c30cba4902eed857ced89598c9b411659f424a376c61230d5d71771deda6660cb
Binary file
@@ -11,6 +11,7 @@ module Spree
11
11
  @currency = params.dig(:filter, :currency) || params[:currency] || Spree::Store.default.default_currency
12
12
  @taxons = taxon_ids(params.dig(:filter, :taxons))
13
13
  @concat_taxons = taxon_ids(params.dig(:filter, :concat_taxons))
14
+ @taxonomies = params.dig(:filter, :taxonomy_ids).to_h
14
15
  @name = params.dig(:filter, :name)
15
16
  @slug = params.dig(:filter, :slug)
16
17
  @options = params.dig(:filter, :options).try(:to_unsafe_hash)
@@ -52,6 +53,7 @@ module Spree
52
53
  products = show_only_backorderable(products)
53
54
  products = show_only_purchasable(products)
54
55
  products = show_only_out_of_stock(products)
56
+ products = by_taxonomies(products)
55
57
  products = ordered(products)
56
58
  products = by_vendor_ids(products)
57
59
 
@@ -62,7 +64,7 @@ module Spree
62
64
 
63
65
  attr_reader :ids, :skus, :price, :currency, :taxons, :concat_taxons, :name, :options, :option_value_ids, :scope,
64
66
  :sort_by, :deleted, :discontinued, :properties, :store, :in_stock, :backorderable, :purchasable, :tags,
65
- :query, :vendor_ids, :out_of_stock, :slug
67
+ :query, :vendor_ids, :out_of_stock, :slug, :taxonomies
66
68
 
67
69
  def query?
68
70
  query.present?
@@ -169,6 +171,21 @@ module Spree
169
171
  products.where(id: product_ids)
170
172
  end
171
173
 
174
+ def by_taxonomies(products)
175
+ return products if taxonomies.none?
176
+
177
+ taxon_groups = taxonomies.values.map { |taxonomy| taxon_ids(taxonomy[:taxon_ids].join(',')) }.compact_blank
178
+
179
+ return products if taxon_groups.empty?
180
+
181
+ taxonomies_products = products.joins(:classifications).where(Classification.table_name => { taxon_id: taxon_groups.flatten.uniq })
182
+
183
+ # No need to filter if there is only one taxonomy
184
+ return taxonomies_products if taxonomies.size == 1
185
+
186
+ products.where(id: products_matching_all_taxonomies_ids(taxonomies_products.ids, taxon_groups))
187
+ end
188
+
172
189
  def by_name(products)
173
190
  return products unless name?
174
191
 
@@ -336,6 +353,16 @@ module Spree
336
353
  def order_by_best_selling(scope)
337
354
  scope.by_best_selling(:desc)
338
355
  end
356
+
357
+ def products_matching_all_taxonomies_ids(products_ids, taxon_groups)
358
+ classifications = Spree::Classification.grouped_taxon_ids_for_products(products_ids, taxon_groups.flatten)
359
+ classifications_hash = classifications.to_h.transform_values { |taxon_ids| taxon_ids.split(',') }
360
+
361
+ # Find products ids that match all taxonomies to tighten filter results
362
+ classifications_hash.filter_map do |product_id, product_taxon_ids|
363
+ product_id if taxon_groups.all? { |group| group.intersect?(product_taxon_ids) }
364
+ end
365
+ end
339
366
  end
340
367
  end
341
368
  end
@@ -68,7 +68,7 @@ module Spree
68
68
  if Spree.use_translations?
69
69
  taxons.joins(:parent).
70
70
  join_translation_table(Taxon, 'parents_spree_taxons').
71
- where(["#{Taxon.translation_table_alias}.permalink = ?", parent_permalink])
71
+ where(Taxon.translation_table_alias => { permalink: parent_permalink })
72
72
  else
73
73
  taxons.joins(:parent).where(parent: { permalink: parent_permalink })
74
74
  end
@@ -0,0 +1,15 @@
1
+ module Spree
2
+ module IntegrationsHelper
3
+ def store_integrations
4
+ @store_integrations ||= current_store.integrations.active.to_a
5
+ end
6
+
7
+ def store_integration(name)
8
+ store_integrations.find { |integration| integration.type.to_s.demodulize.underscore == name }
9
+ end
10
+
11
+ def grouped_available_store_integrations
12
+ Rails.application.config.spree.integrations.group_by(&:integration_group).sort_by { |group, _| group }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["button"]
5
+
6
+ connect() {
7
+ this.element.addEventListener('submit', this.toggleDisabledButton)
8
+ }
9
+
10
+ disconnect() {
11
+ this.element.removeEventListener('submit', this.toggleDisabledButton)
12
+ }
13
+
14
+ toggleDisabledButton = () => {
15
+ if (this.hasButtonTarget) {
16
+ this.buttonTarget.disabled = !this.buttonTarget.disabled
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,24 @@
1
+ module Spree
2
+ class InvitationMailer < BaseMailer
3
+ # invitation email, sending email to the invited to let them know they have been invited to join a store/account/vendor
4
+ def invitation_email(invitation)
5
+ @invitation = invitation
6
+ mail(to: invitation.email,
7
+ from: from_address,
8
+ reply_to: reply_to_address,
9
+ subject: Spree.t('invitation_mailer.invitation_email.subject',
10
+ resource_name: invitation.resource&.name))
11
+ end
12
+
13
+ # sending email to the inviter to let them know the invitee has accepted the invitation
14
+ def invitation_accepted(invitation)
15
+ @invitation = invitation
16
+ mail(to: invitation.inviter.email,
17
+ from: from_address,
18
+ reply_to: reply_to_address,
19
+ subject: Spree.t('invitation_mailer.invitation_accepted.subject',
20
+ invitee_name: invitation.invitee&.name,
21
+ resource_name: invitation.resource&.name))
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,11 @@
1
+ module Spree
2
+ module IntegrationsConcern
3
+ def store_integrations
4
+ @store_integrations ||= Spree::Store.current.integrations.active.to_a
5
+ end
6
+
7
+ def store_integration(name)
8
+ store_integrations.find { |integration| integration.type.to_s.demodulize.underscore == name }
9
+ end
10
+ end
11
+ end
@@ -49,11 +49,11 @@ module Spree
49
49
  add_simple_scopes simple_scopes
50
50
 
51
51
  add_search_scope :ascend_by_master_price do
52
- order("#{price_table_name}.amount ASC")
52
+ order(price_table_name => { amount: :asc })
53
53
  end
54
54
 
55
55
  add_search_scope :descend_by_master_price do
56
- order("#{price_table_name}.amount DESC")
56
+ order(price_table_name => { amount: :desc })
57
57
  end
58
58
 
59
59
  add_search_scope :price_between do |low, high|
@@ -61,11 +61,11 @@ module Spree
61
61
  end
62
62
 
63
63
  add_search_scope :master_price_lte do |price|
64
- where("#{price_table_name}.amount <= ?", price)
64
+ where(Price.table_name => { amount: ..price })
65
65
  end
66
66
 
67
67
  add_search_scope :master_price_gte do |price|
68
- where("#{price_table_name}.amount >= ?", price)
68
+ where(Price.table_name => { amount: price.. })
69
69
  end
70
70
 
71
71
  add_search_scope :in_stock do
@@ -137,11 +137,11 @@ module Spree
137
137
  joins(:properties).
138
138
  join_translation_table(Property).
139
139
  join_translation_table(ProductProperty).
140
- where("#{ProductProperty.translation_table_alias}.value = ?", value).
140
+ where(ProductProperty.translation_table_alias => { value: value }).
141
141
  where(property_conditions(property))
142
142
  else
143
143
  joins(:properties).
144
- where("#{ProductProperty.table_name}.value = ?", value).
144
+ where(ProductProperty.table_name => { value: value }).
145
145
  where(property_conditions(property))
146
146
  end
147
147
  end
@@ -20,6 +20,10 @@ module Spree
20
20
  def translation_table_alias
21
21
  "#{self::Translation.table_name}_#{Mobility.normalize_locale(Mobility.locale)}"
22
22
  end
23
+
24
+ def arel_table_alias
25
+ Arel::Table.new(translation_table_alias)
26
+ end
23
27
  end
24
28
  end
25
29
  end
@@ -15,9 +15,23 @@ module Spree
15
15
  end
16
16
  translatable_class_foreign_key = "#{translatable_class.table_name.singularize}_id"
17
17
 
18
- joins("LEFT OUTER JOIN #{translatable_class::Translation.table_name} #{translatable_class.translation_table_alias}
19
- ON #{translatable_class.translation_table_alias}.#{translatable_class_foreign_key} = #{join_on_table_name}.id
20
- AND #{translatable_class.translation_table_alias}.locale = '#{Mobility.locale}'")
18
+ joins(
19
+ Arel::Nodes::OuterJoin.new(
20
+ Arel::Table.new(translatable_class::Translation.table_name).alias(translatable_class.translation_table_alias),
21
+ Arel::Nodes::On.new(
22
+ Arel::Nodes::And.new([
23
+ Arel::Nodes::Equality.new(
24
+ Arel::Table.new(translatable_class.translation_table_alias)[translatable_class_foreign_key],
25
+ Arel::Table.new(join_on_table_name)[:id]
26
+ ),
27
+ Arel::Nodes::Equality.new(
28
+ Arel::Table.new(translatable_class.translation_table_alias)[:locale],
29
+ Arel::Nodes::Quoted.new(Mobility.locale.to_s)
30
+ )
31
+ ])
32
+ )
33
+ )
34
+ )
21
35
  end
22
36
  end
23
37
  end
@@ -3,6 +3,8 @@ module Spree
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
+ auto_strip_attributes :name
7
+
6
8
  validates :name, presence: true,
7
9
  uniqueness: { case_sensitive: false, allow_blank: true, scope: spree_base_uniqueness_scope }
8
10
  end
@@ -0,0 +1,33 @@
1
+ module Spree
2
+ module UserManagement
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_many :role_users, class_name: 'Spree::RoleUser', as: :resource, dependent: :destroy
7
+ has_many :users, through: :role_users, source: :user, source_type: Spree.admin_user_class.to_s
8
+ has_many :invitations, class_name: 'Spree::Invitation', as: :resource, dependent: :destroy
9
+ end
10
+
11
+ # Adds a user to the resource with the default user role
12
+ # If no role is provided, the default user role will be used
13
+ # If a role is provided, it will be used instead of the default user role
14
+ # @param user [Spree.admin_user_class] The user to add to the resource
15
+ # @param role [Spree::Role] The role to add the user to
16
+ def add_user(user, role = nil)
17
+ role = role || default_user_role
18
+ role_users.find_or_create_by!(user: user, role: role)
19
+ end
20
+
21
+ # Revokes a user's access to the resource
22
+ # @param user [Spree.admin_user_class] The user to remove from the resource
23
+ # @return [void]
24
+ def remove_user(user)
25
+ role_users.where(user: user).destroy_all
26
+ end
27
+
28
+ # this can be overridden in the base model to use a different user role, eg. 'vendor'
29
+ def default_user_role
30
+ Spree::Role.default_admin_role
31
+ end
32
+ end
33
+ end
@@ -84,6 +84,11 @@ module Spree
84
84
  end
85
85
  end
86
86
 
87
+ # Returns the last incomplete spree order for the current store
88
+ # @param [Spree::Store] store
89
+ # @param [Hash] options
90
+ # @option options [Array<Symbol>] :includes
91
+ # @return [Spree::Order]
87
92
  def last_incomplete_spree_order(store, options = {})
88
93
  orders.where(store: store).incomplete.not_canceled.
89
94
  includes(options[:includes]).
@@ -91,12 +96,19 @@ module Spree
91
96
  first
92
97
  end
93
98
 
99
+ # Returns the total available store credit for the current store per currency
100
+ # @param [Spree::Store] store
101
+ # @param [String] currency
102
+ # @return [Float]
94
103
  def total_available_store_credit(currency = nil, store = nil)
95
104
  store ||= Store.default
96
105
  currency ||= store.default_currency
97
106
  store_credits.for_store(store).where(currency: currency).reload.to_a.sum(&:amount_remaining)
98
107
  end
99
108
 
109
+ # Returns the available store credits for the current store per currency
110
+ # @param [Spree::Store] store
111
+ # @return [Array<Spree::Money>]
100
112
  def available_store_credits(store)
101
113
  store ||= Store.default
102
114
 
@@ -105,12 +117,18 @@ module Spree
105
117
  end
106
118
  end
107
119
 
120
+ # Returns the default wishlist for the current store
121
+ # if no default wishlist exists, it creates one
122
+ # @param [Spree::Store] current_store
123
+ # @return [Spree::Wishlist]
108
124
  def default_wishlist_for_store(current_store)
109
125
  wishlists.find_by(is_default: true, store_id: current_store.id) || ActiveRecord::Base.connected_to(role: :writing) do
110
126
  wishlists.create!(store: current_store, is_default: true, name: Spree.t(:default_wishlist_name))
111
127
  end
112
128
  end
113
129
 
130
+ # Returns true if the user can be deleted
131
+ # @return [Boolean]
114
132
  def can_be_deleted?
115
133
  orders.complete.none?
116
134
  end
@@ -140,6 +158,7 @@ module Spree
140
158
  use_billing.in?([true, 'true', '1'])
141
159
  end
142
160
 
161
+ # Scrambles the email and names of the user
143
162
  def scramble_email_and_names
144
163
  self.email = "#{SecureRandom.uuid}@example.net"
145
164
  self.first_name = 'Deleted'
@@ -5,38 +5,64 @@ module Spree
5
5
  included do
6
6
  has_many :role_users, class_name: 'Spree::RoleUser', foreign_key: :user_id, as: :user, dependent: :destroy
7
7
  has_many :spree_roles, through: :role_users, class_name: 'Spree::Role', source: :role
8
+ has_many :stores, through: :role_users, source: :resource, source_type: 'Spree::Store'
9
+ has_many :invitations, class_name: 'Spree::Invitation', foreign_key: :invitee_id, dependent: :destroy
8
10
 
9
- scope :spree_admin, -> { joins(:spree_roles).where(Spree::Role.table_name => { name: 'admin' }) }
11
+ scope :spree_admin, -> { joins(:spree_roles).where(Spree::Role.table_name => { name: Spree::Role::ADMIN_ROLE }) }
10
12
 
11
- # has_spree_role? simply needs to return true or false whether a user has a role or not.
12
- def has_spree_role?(role_name)
13
- spree_roles.exists?(name: role_name)
14
- end
15
-
16
- def self.admin
17
- Spree::Deprecation.warn('`User#admin` is deprecated and will be removed in Spree 5. Please use `User#spree_admin`')
13
+ # Adds a role to a resource
14
+ #
15
+ # @param role_name [String] The name of the role to add, eg. 'admin'
16
+ # @param resource [Spree::Base] The resource to add the role to
17
+ # @return [Spree::RoleUser] The role user created
18
+ def add_role(role_name, resource = nil)
19
+ resource ||= Spree::Store.current
20
+ role = Spree::Role.find_by(name: role_name)
21
+ return if role.nil?
18
22
 
19
- spree_admin
23
+ role_users.find_or_create_by!(role: role, resource: resource)
20
24
  end
21
25
 
22
- def self.admin_created?
23
- Spree::Deprecation.warn('`User#admin_created?` is deprecated and will be removed in Spree 5. Please use `User#spree_admin_created?`')
26
+ # Removes a role from a resource
27
+ #
28
+ # @param role_name [String] The name of the role to remove, eg. 'admin'
29
+ # @param resource [Spree::Base] The resource to remove the role from
30
+ def remove_role(role_name, resource = nil)
31
+ resource ||= Spree::Store.current
32
+ role = Spree::Role.find_by(name: role_name)
33
+ return if role.nil?
24
34
 
25
- spree_admin_created?
35
+ role_users.where(role: role, resource: resource).destroy_all
26
36
  end
27
37
 
28
- def admin?
29
- Spree::Deprecation.warn('`User#admin?` is deprecated and will be removed in Spree 5. Please use `User#spree_admin?`')
38
+ # has_spree_role? simply needs to return true or false whether a user has a role or not.
39
+ #
40
+ # @param role_name [String] The name of the role to check for
41
+ # @param resource [Spree::Base] The resource to get roles for
42
+ # @return [Boolean] Whether the user has the role for the resource
43
+ def has_spree_role?(role_name, resource = nil)
44
+ resource ||= Spree::Store.current
30
45
 
31
- spree_admin?
46
+ role_users.where(resource: resource).joins(:role).where(Spree::Role.table_name => { name: role_name }).exists?
32
47
  end
33
48
 
34
49
  def self.spree_admin_created?
35
50
  spree_admin.exists?
36
51
  end
37
52
 
38
- def spree_admin?
39
- has_spree_role?('admin')
53
+ # Returns true if the user has the admin role for a given resource
54
+ #
55
+ # @param resource [Spree::Base] The resource to check the admin role for
56
+ # @return [Boolean] Whether the user has the admin role for the resource
57
+ def spree_admin?(resource = nil)
58
+ resource ||= Spree::Store.current
59
+ has_spree_role?(Spree::Role::ADMIN_ROLE, resource)
60
+ end
61
+
62
+ # Returns the user who invited the current user
63
+ # @return [Spree.admin_user_class]
64
+ def invited_by
65
+ invitations.first&.inviter
40
66
  end
41
67
  end
42
68
  end
@@ -23,15 +23,16 @@ module Spree
23
23
  abilities.delete(ability)
24
24
  end
25
25
 
26
- def initialize(user)
26
+ def initialize(user, options = {})
27
27
  alias_cancan_delete_action
28
28
 
29
29
  user ||= Spree.user_class.new
30
+ store ||= options[:store] || Spree::Current.store
30
31
 
31
- if user.persisted? && user.try(:spree_admin?)
32
- apply_admin_permissions(user)
32
+ if user.persisted? && user.is_a?(Spree.admin_user_class) && user.try(:spree_admin?, store)
33
+ apply_admin_permissions(user, options)
33
34
  else
34
- apply_user_permissions(user)
35
+ apply_user_permissions(user, options)
35
36
  end
36
37
 
37
38
  # Include any abilities registered by extensions, etc.
@@ -56,7 +57,7 @@ module Spree
56
57
  alias_action :create, :update, :destroy, to: :modify
57
58
  end
58
59
 
59
- def apply_admin_permissions(user)
60
+ def apply_admin_permissions(_user, _options)
60
61
  can :manage, :all
61
62
  cannot :cancel, Spree::Order
62
63
  can :cancel, Spree::Order, &:allow_cancel?
@@ -64,7 +65,7 @@ module Spree
64
65
  cannot [:edit, :update], Spree::ReimbursementType, mutable: false
65
66
  end
66
67
 
67
- def apply_user_permissions(user)
68
+ def apply_user_permissions(user, _options)
68
69
  can :read, ::Spree::Country
69
70
  can :read, ::Spree::OptionType
70
71
  can :read, ::Spree::OptionValue
@@ -95,6 +96,7 @@ module Spree
95
96
  can [:create, :update, :destroy], ::Spree::WishedItem do |wished_item|
96
97
  wished_item.wishlist.user == user
97
98
  end
99
+ can :accept, Spree::Invitation, invitee_id: [user.id, nil], invitee_type: user.class.name, status: 'pending'
98
100
  end
99
101
 
100
102
  def protect_admin_role
@@ -17,12 +17,7 @@ module Spree
17
17
 
18
18
  store_accessor :private_metadata, :session_uploaded_assets_uuid
19
19
  scope :with_session_uploaded_assets_uuid, lambda { |uuid|
20
- case ActiveRecord::Base.connection.adapter_name
21
- when 'PostgreSQL'
22
- where("#{table_name}.private_metadata @> ?", { session_uploaded_assets_uuid: uuid }.to_json)
23
- when 'Mysql2', 'SQLite'
24
- where("JSON_EXTRACT(private_metadata, '$.session_uploaded_assets_uuid') = '#{uuid}'")
25
- end
20
+ where(session_id: uuid)
26
21
  }
27
22
 
28
23
  def product
@@ -2,6 +2,7 @@ class Spree::Base < ApplicationRecord
2
2
  include Spree::Preferences::Preferable
3
3
  include Spree::RansackableAttributes
4
4
  include Spree::TranslatableResourceScopes
5
+ include Spree::IntegrationsConcern
5
6
 
6
7
  after_initialize do
7
8
  if has_attribute?(:preferences) && !preferences.nil?
@@ -5,13 +5,16 @@ module Spree
5
5
  # @param opts [Hash] The options
6
6
  # @option opts [User] :user
7
7
  # @option opts [String] :session_id
8
+ # @option opts [Spree::Store] :store
8
9
  def initialize(opts = {})
9
10
  @user = opts[:user]
10
11
  @session = opts[:session]
11
12
  @request = opts[:request]
13
+ @store = opts[:store]
14
+ @visitor_id = opts[:visitor_id]
12
15
  end
13
16
 
14
- attr_reader :user, :session, :request
17
+ attr_reader :user, :session, :request, :store, :visitor_id
15
18
 
16
19
  # Returns the client
17
20
  # @return [Object] The client object
@@ -42,7 +45,9 @@ module Spree
42
45
  def identity_hash
43
46
  {
44
47
  user_id: user&.id,
45
- session_id: session&.id
48
+ # session.id is a custom class (not a string), which has overridden the `to_json` method, we have to convert it to a string first so it does not send garbage to the analytics service
49
+ session_id: session&.id&.to_s,
50
+ visitor_id: visitor_id
46
51
  }
47
52
  end
48
53
  end
@@ -23,5 +23,18 @@ module Spree
23
23
  group("#{Spree::Classification.table_name}.id").
24
24
  reorder(completed_orders_count: order_direction, completed_orders_total: order_direction)
25
25
  }
26
+
27
+ scope :grouped_taxon_ids_for_products, lambda { |product_ids, taxon_groups|
28
+ where(product_id: product_ids, taxon_id: taxon_groups).
29
+ group(:product_id).
30
+ then do |query|
31
+ case ActiveRecord::Base.connection.adapter_name
32
+ when 'PostgreSQL'
33
+ query.pluck(:product_id, Arel.sql("STRING_AGG(taxon_id::text, ',')"))
34
+ when 'Mysql2', 'SQLite'
35
+ query.pluck(:product_id, Arel.sql('GROUP_CONCAT(taxon_id)'))
36
+ end
37
+ end
38
+ }
26
39
  end
27
40
  end
@@ -14,7 +14,7 @@ module Spree
14
14
  # Validations
15
15
  #
16
16
  validates :url, presence: true, uniqueness: true, format: {
17
- with: %r{[a-z][a-z0-9-]*[a-z0-9]}i
17
+ with: %r{\A(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\z}i
18
18
  }, length: { in: 1..63 }
19
19
  validate :url_is_valid
20
20
 
@@ -26,6 +26,7 @@ module Spree
26
26
  after_validation :ensure_default, on: :create
27
27
 
28
28
  def url_is_valid
29
+ return if url.blank?
29
30
  parts = url.split('.')
30
31
 
31
32
  errors.add(:url, 'use domain or subdomain') if (parts[0] != 'www' && parts.size > 3) || (parts[0] == 'www' && parts.size > 4) || parts.size < 2
@@ -124,7 +124,7 @@ module Spree
124
124
  end
125
125
 
126
126
  def current_ability
127
- @current_ability ||= Spree::Dependencies.ability_class.constantize.new(user)
127
+ @current_ability ||= Spree::Dependencies.ability_class.constantize.new(user, { store: store })
128
128
  end
129
129
 
130
130
  # eg. Spree::Exports::Products => products-store-my-store-code-20241030133348.csv
@@ -0,0 +1,63 @@
1
+ module Spree
2
+ class Integration < Spree.base_class
3
+ include Spree::SingleStoreResource
4
+
5
+ #
6
+ # Associations
7
+ #
8
+ belongs_to :store, class_name: 'Spree::Store', touch: true
9
+
10
+ #
11
+ # Validations
12
+ #
13
+ validates :type, presence: true
14
+ validates :store, presence: true, uniqueness: { scope: :type }
15
+
16
+ #
17
+ # Scopes
18
+ #
19
+ scope :active, -> { where(active: true) }
20
+
21
+ # This attribute is used to temporarily store connection-related error messages
22
+ # that can be displayed to users when testing or validating integration connections.
23
+ # It is not persisted to the database and is reset on each new connection attempt.
24
+ # @param message [String, nil] The error message to be stored
25
+ # @return [String, nil] The current error message
26
+ attr_accessor :connection_error_message
27
+
28
+ # Associates the integration to a group.
29
+ # The name here will be used as Spree.t key to display the group name.
30
+ # Leave blank to leave the integration ungrouped.
31
+ def self.integration_group
32
+ nil
33
+ end
34
+
35
+ def self.icon_path
36
+ nil
37
+ end
38
+
39
+ def self.integration_name
40
+ name.demodulize.titleize.strip
41
+ end
42
+
43
+ def self.integration_key
44
+ name.demodulize.underscore
45
+ end
46
+
47
+ def name
48
+ self.class.integration_name
49
+ end
50
+
51
+ def key
52
+ self.class.integration_key
53
+ end
54
+
55
+ # Checks if the integration can establish a connection.
56
+ # This is a base implementation that always returns true.
57
+ # Subclasses should override this method to implement their own connection validation logic.
58
+ # @return [Boolean] true if the integration can connect, false otherwise
59
+ def can_connect?
60
+ true
61
+ end
62
+ end
63
+ end