spree_core 5.0.4 → 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 (70) 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 +16 -5
  27. data/app/models/spree/order_merger.rb +7 -5
  28. data/app/models/spree/product_property.rb +1 -1
  29. data/app/models/spree/property.rb +3 -1
  30. data/app/models/spree/reports/sales_total.rb +5 -1
  31. data/app/models/spree/role.rb +16 -0
  32. data/app/models/spree/role_user.rb +32 -1
  33. data/app/models/spree/shipment_handler.rb +1 -0
  34. data/app/models/spree/shipping_method.rb +1 -1
  35. data/app/models/spree/store.rb +9 -4
  36. data/app/models/spree/store_credit_category.rb +4 -0
  37. data/app/models/spree/taxon.rb +4 -3
  38. data/app/services/spree/country_to_timezone.rb +273 -0
  39. data/app/services/spree/seeds/admin_user.rb +4 -2
  40. data/app/services/spree/seeds/all.rb +3 -1
  41. data/app/services/spree/seeds/digital_delivery.rb +20 -0
  42. data/app/services/spree/seeds/returns_environment.rb +27 -0
  43. data/app/services/spree/seeds/tax_categories.rb +12 -0
  44. data/app/services/spree/stores/settings_defaults_by_country.rb +38 -0
  45. data/app/services/spree/tags/bulk_add.rb +13 -7
  46. data/app/views/spree/invitation_mailer/invitation_accepted.html.erb +12 -0
  47. data/app/views/spree/invitation_mailer/invitation_email.html.erb +21 -0
  48. data/config/locales/en.yml +43 -9
  49. data/db/migrate/20250407085228_create_spree_integrations.rb +12 -0
  50. data/db/migrate/20250410061306_create_spree_invitations.rb +20 -0
  51. data/db/migrate/20250418174652_add_resource_to_spree_role_users.rb +8 -0
  52. data/db/migrate/20250508060800_add_selected_locale_to_spree_admin_users.rb +8 -0
  53. data/db/migrate/20250509143831_add_session_id_to_spree_assets.rb +5 -0
  54. data/lib/generators/spree/authentication/devise/devise_generator.rb +5 -2
  55. data/lib/generators/spree/authentication/devise/templates/authentication_helpers.rb.tt +3 -3
  56. data/lib/generators/spree/install/install_generator.rb +5 -0
  57. data/lib/spree/core/controller_helpers/auth.rb +15 -14
  58. data/lib/spree/core/controller_helpers/currency.rb +11 -0
  59. data/lib/spree/core/controller_helpers/strong_parameters.rb +3 -2
  60. data/lib/spree/core/engine.rb +13 -2
  61. data/lib/spree/core/version.rb +1 -1
  62. data/lib/spree/core.rb +1 -0
  63. data/lib/spree/permitted_attributes.rb +118 -13
  64. data/lib/spree/testing_support/factories/integration_factory.rb +7 -0
  65. data/lib/spree/testing_support/factories/invitation_factory.rb +6 -0
  66. data/lib/spree/testing_support/factories/promotion_action_factory.rb +4 -0
  67. data/lib/spree/testing_support/factories/user_factory.rb +14 -1
  68. data/lib/spree/translation_migrations.rb +27 -15
  69. data/lib/tasks/core.rake +8 -0
  70. metadata +41 -4
@@ -0,0 +1,153 @@
1
+ module Spree
2
+ class Invitation < Spree.base_class
3
+ has_secure_token
4
+ acts_as_paranoid
5
+
6
+ #
7
+ # Virtual Attributes
8
+ #
9
+ attribute :skip_email, :boolean, default: false
10
+
11
+ #
12
+ # Associations
13
+ #
14
+ belongs_to :resource, polymorphic: true # eg. Store, Vendor, Account
15
+ belongs_to :inviter, polymorphic: true # User or AdminUser
16
+ belongs_to :invitee, polymorphic: true, optional: true # User or AdminUser
17
+ belongs_to :role, class_name: 'Spree::Role'
18
+ has_one :role_user, dependent: :destroy, class_name: 'Spree::RoleUser'
19
+
20
+ #
21
+ # Validations
22
+ #
23
+ validates :email, email: true, presence: true
24
+ validates :token, presence: true, uniqueness: true
25
+ validates :inviter, :resource, :role, presence: true
26
+ validate :invitee_is_not_inviter, on: :create
27
+ validate :invitee_already_exists, on: :create
28
+
29
+ #
30
+ # Scopes
31
+ #
32
+ scope :pending, -> { where(status: 'pending') }
33
+ scope :accepted, -> { where(status: 'accepted') }
34
+ scope :not_expired, -> { where('expires_at > ?', Time.current) }
35
+
36
+ #
37
+ # State Machine
38
+ #
39
+ state_machine initial: :pending, attribute: :status do
40
+ state :accepted do
41
+ validate :accept_invitation_within_time_limit
42
+ validates :invitee, presence: true
43
+ end
44
+
45
+ event :accept do
46
+ transition pending: :accepted
47
+ end
48
+ after_transition to: :accepted, do: :after_accept
49
+ end
50
+
51
+ #
52
+ # Callbacks
53
+ #
54
+ after_initialize :set_defaults, if: :new_record?
55
+ before_validation :set_invitee_from_email, on: :create
56
+ after_create :send_invitation_email, unless: :skip_email
57
+
58
+ # returns the store for the invitation
59
+ # if the resource is a store, return the resource
60
+ # if the resource responds to store, return the store
61
+ # otherwise, return the current store
62
+ # @return [Spree::Store]
63
+ def store
64
+ if resource.is_a?(Spree::Store)
65
+ resource
66
+ elsif resource.respond_to?(:store)
67
+ resource.store
68
+ else
69
+ Spree::Store.current
70
+ end
71
+ end
72
+
73
+ # returns true if the invitation has expired
74
+ # @return [Boolean]
75
+ def expired?
76
+ expires_at < Time.current
77
+ end
78
+
79
+ # Resends the invitation email if the invitation is pending and not expired
80
+ def resend!
81
+ return if expired? || deleted? || accepted?
82
+
83
+ send_invitation_email
84
+ end
85
+
86
+ private
87
+
88
+ # this method can be extended by developers now
89
+ def after_accept
90
+ create_role_user
91
+ set_accepted_at
92
+ send_acceptance_notification
93
+ end
94
+
95
+ def send_invitation_email
96
+ Spree::InvitationMailer.invitation_email(self).deliver_later
97
+ end
98
+
99
+ def send_acceptance_notification
100
+ Spree::InvitationMailer.invitation_accepted(self).deliver_later
101
+ end
102
+
103
+ def set_defaults
104
+ self.expires_at ||= 2.weeks.from_now
105
+ self.resource ||= Spree::Store.current
106
+ self.role ||= Spree::Role.default_admin_role
107
+ end
108
+
109
+ def invitee_is_not_inviter
110
+ if invitee == inviter
111
+ errors.add(:invitee, 'cannot be the same as the inviter')
112
+ end
113
+ end
114
+
115
+ def invitee_already_exists
116
+ return if resource.blank?
117
+
118
+ exists = if invitee.present?
119
+ resource.users.include?(invitee)
120
+ else
121
+ resource.users.exists?(email: email)
122
+ end
123
+
124
+ if exists
125
+ errors.add(:email, 'already exists')
126
+ end
127
+ end
128
+
129
+ def set_accepted_at
130
+ update!(accepted_at: Time.current)
131
+ end
132
+
133
+ def create_role_user
134
+ return if invitee.blank?
135
+
136
+ role_user = resource.add_user(invitee, role)
137
+ self.role_user = role_user
138
+ save!
139
+ end
140
+
141
+ def set_invitee_from_email
142
+ return if invitee.present?
143
+
144
+ self.invitee = Spree.admin_user_class.find_by(email: email)
145
+ end
146
+
147
+ def accept_invitation_within_time_limit
148
+ if Time.current > expires_at
149
+ errors.add(:base, 'Invitation expired')
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,6 @@
1
+ module Spree
2
+ module Invitations
3
+ class Store < Base
4
+ end
5
+ end
6
+ end
@@ -91,6 +91,8 @@ module Spree
91
91
  acts_as_taggable_on :tags
92
92
  acts_as_taggable_tenant :store_id
93
93
 
94
+ ASSOCIATED_USER_ATTRIBUTES = [:user_id, :email, :created_by_id, :bill_address_id, :ship_address_id]
95
+
94
96
  belongs_to :user, class_name: "::#{Spree.user_class}", optional: true, autosave: true
95
97
  belongs_to :created_by, class_name: "::#{Spree.admin_user_class}", optional: true
96
98
  belongs_to :approver, class_name: "::#{Spree.admin_user_class}", optional: true
@@ -247,6 +249,13 @@ module Spree
247
249
  pre_tax_item_amount + shipments.sum(:pre_tax_amount)
248
250
  end
249
251
 
252
+ # Returns the subtotal used for analytics integrations
253
+ # It's a sum of the item total and the promo total
254
+ # @return [Float]
255
+ def analytics_subtotal
256
+ (item_total + line_items.sum(:promo_total)).to_f
257
+ end
258
+
250
259
  def shipping_discount
251
260
  shipment_adjustments.non_tax.eligible.sum(:amount) * - 1
252
261
  end
@@ -355,7 +364,7 @@ module Spree
355
364
  self.bill_address ||= user.bill_address
356
365
  self.ship_address ||= user.ship_address
357
366
 
358
- changes = slice(:user_id, :email, :created_by_id, :bill_address_id, :ship_address_id)
367
+ changes = slice(*ASSOCIATED_USER_ATTRIBUTES)
359
368
 
360
369
  # immediately persist the changes we just made, but don't use save
361
370
  # since we might have an invalid address associated
@@ -364,6 +373,12 @@ module Spree
364
373
  end
365
374
  end
366
375
 
376
+ def disassociate_user!
377
+ nullified_attributes = ASSOCIATED_USER_ATTRIBUTES.index_with(nil)
378
+
379
+ update!(nullified_attributes)
380
+ end
381
+
367
382
  def quantity_of(variant, options = {})
368
383
  line_item = find_line_item_by_variant(variant, options)
369
384
  line_item ? line_item.quantity : 0
@@ -815,10 +830,6 @@ module Spree
815
830
  csv_lines
816
831
  end
817
832
 
818
- def all_line_items
819
- line_items
820
- end
821
-
822
833
  private
823
834
 
824
835
  def link_by_email
@@ -7,7 +7,7 @@ module Spree
7
7
  @order = order
8
8
  end
9
9
 
10
- def merge!(other_order, user = nil)
10
+ def merge!(other_order, user = nil, discard_merged: true)
11
11
  other_order.line_items.each do |other_order_line_item|
12
12
  next unless other_order_line_item.currency == order.currency
13
13
 
@@ -16,12 +16,14 @@ module Spree
16
16
  end
17
17
 
18
18
  set_user(user)
19
- clear_addresses(other_order)
19
+ clear_addresses(other_order) if discard_merged
20
20
  persist_merge
21
21
 
22
- # So that the destroy doesn't take out line items which may have been re-assigned
23
- other_order.line_items.reload
24
- other_order.destroy
22
+ if discard_merged
23
+ # So that the destroy doesn't take out line items which may have been re-assigned
24
+ other_order.line_items.reload
25
+ other_order.destroy
26
+ end
25
27
  end
26
28
 
27
29
  # Compare the line item of the other order with mine.
@@ -33,7 +33,7 @@ module Spree
33
33
  scope :filterable, -> { joins(:property).where(Property.table_name => { filterable: true }) }
34
34
  scope :for_products, ->(products) { joins(:product).merge(products) }
35
35
  scope :sort_by_property_position, -> {
36
- joins(:property).order("spree_properties.position ASC")
36
+ unscope(:order).joins(:property).order("spree_properties.position ASC")
37
37
  }
38
38
 
39
39
  self.whitelisted_ransackable_attributes = ['value', 'filter_param']
@@ -39,8 +39,10 @@ module Spree
39
39
  KIND_OPTIONS = { short_text: 0, long_text: 1, number: 2, rich_text: 3 }.freeze
40
40
  enum :kind, KIND_OPTIONS
41
41
 
42
+ DEPENDENCY_UPDATE_FIELDS = [:presentation, :name, :kind, :filterable, :display_on, :position].freeze
43
+
42
44
  after_touch :touch_all_products
43
- after_update :touch_all_products, if: -> { saved_changes.key?(:presentation) }
45
+ after_update :touch_all_products, if: -> { DEPENDENCY_UPDATE_FIELDS.any? { |field| saved_changes.key?(field) } }
44
46
  after_save :ensure_product_properties_have_filter_params
45
47
 
46
48
  self.whitelisted_ransackable_attributes = ['presentation', 'filterable']
@@ -2,12 +2,16 @@ module Spree
2
2
  module Reports
3
3
  class SalesTotal < Spree::Report
4
4
  def line_items_scope
5
- store.line_items.where(
5
+ scope = store.line_items.where(
6
6
  order: Spree::Order.complete.where(
7
7
  currency: currency,
8
8
  completed_at: (date_from.to_time.beginning_of_day)..(date_to.to_time.end_of_day)
9
9
  )
10
10
  ).includes(:order, variant: :product)
11
+
12
+ scope = scope.where(vendor_id: vendor.id) if defined?(vendor) && vendor.present?
13
+
14
+ scope
11
15
  end
12
16
  end
13
17
  end
@@ -4,8 +4,24 @@ module Spree
4
4
 
5
5
  ADMIN_ROLE = 'admin'
6
6
 
7
+ #
8
+ # Associations
9
+ #
7
10
  has_many :role_users, class_name: 'Spree::RoleUser', dependent: :destroy
8
11
  has_many :users, through: :role_users, source: :user, source_type: Spree.user_class.to_s
9
12
  has_many :admin_users, through: :role_users, source: :user, source_type: Spree.admin_user_class.to_s
13
+ has_many :invitations, class_name: 'Spree::Invitation', dependent: :destroy
14
+
15
+ #
16
+ # Scopes
17
+ #
18
+ scope :admin, -> { where(name: ADMIN_ROLE) }
19
+
20
+ #
21
+ # Class Methods
22
+ #
23
+ def self.default_admin_role
24
+ find_or_create_by(name: ADMIN_ROLE)
25
+ end
10
26
  end
11
27
  end
@@ -1,6 +1,37 @@
1
1
  module Spree
2
2
  class RoleUser < Spree.base_class
3
- belongs_to :role, class_name: 'Spree::Role'
3
+ #
4
+ # Associations
5
+ #
6
+ belongs_to :role, class_name: 'Spree::Role', foreign_key: :role_id
4
7
  belongs_to :user, polymorphic: true
8
+ belongs_to :resource, polymorphic: true
9
+ belongs_to :invitation, class_name: 'Spree::Invitation', optional: true
10
+
11
+ #
12
+ # Validations
13
+ #
14
+ validates :role, presence: true
15
+ validates :user, presence: true
16
+ validates :resource, presence: true
17
+ validates :role_id, uniqueness: { scope: [:user_id, :resource_id, :user_type, :resource_type] }
18
+
19
+ #
20
+ # Delegations
21
+ #
22
+ delegate :name, to: :user
23
+
24
+ #
25
+ # Callbacks
26
+ #
27
+ before_validation :set_default_resource
28
+
29
+ private
30
+
31
+ # Set the default resource to the default store if the resource is not set
32
+ # this will allow a graceful migration from the old roles system to the new one
33
+ def set_default_resource
34
+ self.resource ||= Spree::Store.current
35
+ end
5
36
  end
6
37
  end
@@ -1,6 +1,7 @@
1
1
  module Spree
2
2
  class ShipmentHandler
3
3
  include Spree::Shipment::Emails
4
+ include Spree::IntegrationsConcern
4
5
 
5
6
  class << self
6
7
  def factory(shipment)
@@ -50,7 +50,7 @@ module Spree
50
50
  def include?(address)
51
51
  return false unless address
52
52
 
53
- zones.includes(:zone_members).any? do |zone|
53
+ zones.includes(zone_members: :zoneable).any? do |zone|
54
54
  zone.include?(address)
55
55
  end
56
56
  end
@@ -13,6 +13,7 @@ module Spree
13
13
  include Spree::Stores::Socials
14
14
  include Spree::Webhooks::HasWebhooks if defined?(Spree::Webhooks::HasWebhooks)
15
15
  include Spree::Security::Stores if defined?(Spree::Security::Stores)
16
+ include Spree::UserManagement
16
17
 
17
18
  #
18
19
  # Magic methods
@@ -100,8 +101,10 @@ module Spree
100
101
  has_many :custom_domains, class_name: 'Spree::CustomDomain', dependent: :destroy
101
102
  has_one :default_custom_domain, -> { where(default: true) }, class_name: 'Spree::CustomDomain'
102
103
 
103
- has_many :posts
104
- has_many :post_categories
104
+ has_many :posts, class_name: 'Spree::Post'
105
+ has_many :post_categories, class_name: 'Spree::PostCategory'
106
+
107
+ has_many :integrations, class_name: 'Spree::Integration'
105
108
 
106
109
  #
107
110
  # Page Builder associations
@@ -342,7 +345,7 @@ module Spree
342
345
  # @return [Spree::StockLocation]
343
346
  def default_stock_location
344
347
  @default_stock_location ||= begin
345
- stock_location_scope = Spree::StockLocation.order_default
348
+ stock_location_scope = Spree::StockLocation.where(default: true)
346
349
  stock_location_scope.first || ActiveRecord::Base.connected_to(role: :writing) do
347
350
  stock_location_scope.create(default: true, name: Spree.t(:default_stock_location_name), country: default_country)
348
351
  end
@@ -350,7 +353,9 @@ module Spree
350
353
  end
351
354
 
352
355
  def admin_users
353
- @admin_users ||= Spree.admin_user_class.joins(:spree_roles).where(spree_roles: { name: :admin })
356
+ Spree::Deprecation.warn('Store#admin_users is deprecated and will be removed in Spree 6.0. Please use Store#users instead.')
357
+
358
+ users
354
359
  end
355
360
 
356
361
  def favicon
@@ -28,6 +28,10 @@ module Spree
28
28
  end
29
29
  end
30
30
 
31
+ def can_be_deleted?
32
+ !store_credit_category_used?
33
+ end
34
+
31
35
  class << self
32
36
  def default_reimbursement_category(_options = {})
33
37
  Spree::StoreCreditCategory.first
@@ -1,4 +1,3 @@
1
- # TODO: let friendly id take care of sanitizing the url
2
1
  require 'stringex'
3
2
 
4
3
  module Spree
@@ -96,9 +95,11 @@ module Spree
96
95
  if Spree.use_translations?
97
96
  joins(:taxonomy).
98
97
  join_translation_table(Taxonomy).
99
- where(["LOWER(#{Taxonomy.translation_table_alias}.name) = ?", taxonomy_name.downcase.strip])
98
+ where(
99
+ Taxonomy.arel_table_alias[:name].lower.matches(taxonomy_name.downcase.strip)
100
+ )
100
101
  else
101
- joins(:taxonomy).where(["LOWER(#{Spree::Taxonomy.table_name}.name) = ?", taxonomy_name.downcase.strip])
102
+ joins(:taxonomy).where(Spree::Taxonomy.arel_table[:name].lower.matches(taxonomy_name.downcase.strip))
102
103
  end
103
104
  }
104
105