spree_core 5.1.0.beta4 → 5.1.0.rc1

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/app/helpers/spree/base_helper.rb +6 -1
  3. data/app/helpers/spree/images_helper.rb +19 -16
  4. data/app/helpers/spree/shipment_helper.rb +12 -0
  5. data/app/jobs/spree/gift_cards/bulk_generate_job.rb +13 -0
  6. data/app/models/concerns/spree/parameterizable_name.rb +11 -0
  7. data/app/models/concerns/spree/product_scopes.rb +1 -5
  8. data/app/models/concerns/spree/stores/socials.rb +6 -2
  9. data/app/models/concerns/spree/user_methods.rb +2 -1
  10. data/app/models/spree/ability.rb +2 -0
  11. data/app/models/spree/address.rb +11 -3
  12. data/app/models/spree/gift_card.rb +162 -0
  13. data/app/models/spree/gift_card_batch.rb +79 -0
  14. data/app/models/spree/inventory_unit.rb +11 -4
  15. data/app/models/spree/line_item.rb +13 -2
  16. data/app/models/spree/option_type.rb +5 -15
  17. data/app/models/spree/option_value.rb +18 -12
  18. data/app/models/spree/order/checkout.rb +2 -0
  19. data/app/models/spree/order/gift_card.rb +51 -0
  20. data/app/models/spree/order/store_credit.rb +20 -1
  21. data/app/models/spree/order.rb +51 -6
  22. data/app/models/spree/order_merger.rb +12 -0
  23. data/app/models/spree/page_sections/featured_posts.rb +4 -0
  24. data/app/models/spree/payment_method/store_credit.rb +1 -1
  25. data/app/models/spree/payment_method.rb +1 -0
  26. data/app/models/spree/post.rb +1 -0
  27. data/app/models/spree/price.rb +7 -1
  28. data/app/models/spree/product/slugs.rb +103 -0
  29. data/app/models/spree/product.rb +7 -81
  30. data/app/models/spree/product_property.rb +2 -2
  31. data/app/models/spree/promotion_handler/coupon.rb +39 -0
  32. data/app/models/spree/property.rb +2 -8
  33. data/app/models/spree/reimbursement_type/reimbursement_helpers.rb +1 -1
  34. data/app/models/spree/return_item.rb +4 -0
  35. data/app/models/spree/shipment.rb +19 -0
  36. data/app/models/spree/shipping_method.rb +8 -9
  37. data/app/models/spree/store.rb +2 -0
  38. data/app/models/spree/store_credit.rb +9 -6
  39. data/app/models/spree/store_credit_event.rb +8 -4
  40. data/app/models/spree/taxon.rb +8 -1
  41. data/app/models/spree/theme.rb +1 -1
  42. data/app/models/spree/variant.rb +1 -1
  43. data/app/presenters/spree/csv/product_variant_presenter.rb +14 -3
  44. data/app/services/spree/addresses/phone_validator.rb +20 -0
  45. data/app/services/spree/cart/destroy.rb +1 -1
  46. data/app/services/spree/cart/recalculate.rb +1 -0
  47. data/app/services/spree/gift_cards/apply.rb +66 -0
  48. data/app/services/spree/gift_cards/redeem.rb +17 -0
  49. data/app/services/spree/gift_cards/remove.rb +38 -0
  50. data/app/services/spree/products/prepare_nested_attributes.rb +2 -2
  51. data/app/views/spree/addresses/_form.html.erb +1 -1
  52. data/app/views/spree/shared/_payment.html.erb +4 -4
  53. data/config/locales/en.yml +44 -14
  54. data/config/routes.rb +1 -0
  55. data/db/migrate/20250506073057_create_spree_gift_cards_and_spree_gift_card_batches.rb +37 -0
  56. data/db/migrate/20250530101236_enable_pg_trgm_extension.rb +13 -0
  57. data/db/migrate/20250605131334_add_missing_fields_to_users.rb +25 -0
  58. data/lib/generators/spree/install/install_generator.rb +0 -8
  59. data/lib/spree/core/configuration.rb +4 -0
  60. data/lib/spree/core/controller_helpers/store.rb +13 -1
  61. data/lib/spree/core/dependencies.rb +5 -0
  62. data/lib/spree/core/engine.rb +7 -1
  63. data/lib/spree/core/version.rb +1 -1
  64. data/lib/spree/core.rb +2 -1
  65. data/lib/spree/permitted_attributes.rb +22 -9
  66. data/lib/spree/testing_support/authorization_helpers.rb +5 -9
  67. data/lib/spree/testing_support/common_rake.rb +7 -1
  68. data/lib/spree/testing_support/factories/gift_card_batch_factory.rb +5 -0
  69. data/lib/spree/testing_support/factories/gift_card_factory.rb +17 -0
  70. data/lib/spree/testing_support/factories/page_section_factory.rb +2 -1
  71. data/lib/spree/testing_support/factories/post_factory.rb +4 -0
  72. data/lib/spree/testing_support/factories/promotion_rule_factory.rb +4 -0
  73. data/lib/spree/testing_support/factories/user_factory.rb +2 -2
  74. metadata +45 -11
  75. data/config/initializers/state_machine.rb +0 -37
@@ -92,6 +92,7 @@ module Spree
92
92
  after_transition to: :complete, do: :persist_user_credit_card
93
93
  before_transition to: :payment, do: :set_shipments_cost
94
94
  before_transition to: :payment, do: :create_tax_charge!
95
+ before_transition to: :payment, do: :recalculate_store_credit_payment
95
96
  end
96
97
 
97
98
  before_transition from: :cart, do: :ensure_line_items_present
@@ -118,6 +119,7 @@ module Spree
118
119
 
119
120
  after_transition to: :complete, do: :finalize!
120
121
  after_transition to: :complete, do: :use_all_coupon_codes
122
+ after_transition to: :complete, do: :redeem_gift_card
121
123
  after_transition to: :resumed, do: :after_resume
122
124
  after_transition to: :canceled, do: :after_cancel
123
125
 
@@ -0,0 +1,51 @@
1
+ module Spree
2
+ class Order < Spree.base_class
3
+ module GiftCard
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ # one GiftCard can be used on many orders, until it runs out
8
+ belongs_to :gift_card, class_name: 'Spree::GiftCard', optional: true
9
+
10
+ money_methods :gift_card_total
11
+ end
12
+
13
+ # Returns the total amount of the gift card applied to the order
14
+ # @return [Decimal]
15
+ def gift_card_total
16
+ return 0.to_d unless gift_card.present?
17
+
18
+ store_credit_ids = payments.store_credits.valid.pluck(:source_id)
19
+ store_credits = Spree::StoreCredit.where(id: store_credit_ids, originator: gift_card)
20
+
21
+ store_credits.sum(:amount)
22
+ end
23
+
24
+ # Applies a gift card to the order
25
+ # @param gift_card [Spree::GiftCard] the gift card to apply
26
+ # @return [Spree::Order] the order with the gift card applied
27
+ def apply_gift_card(gift_card)
28
+ Spree::Dependencies.gift_card_apply_service.constantize.call(gift_card: gift_card, order: self)
29
+ end
30
+
31
+ # Removes a gift card from the order
32
+ # @return [Spree::Order] the order with the gift card removed
33
+ def remove_gift_card
34
+ Spree::Dependencies.gift_card_remove_service.constantize.call(order: self)
35
+ end
36
+
37
+ def recalculate_gift_card
38
+ applied_gift_card = gift_card
39
+
40
+ remove_gift_card
41
+ apply_gift_card(applied_gift_card)
42
+ end
43
+
44
+ def redeem_gift_card
45
+ return unless gift_card.present?
46
+
47
+ Spree::Dependencies.gift_card_redeem_service.constantize.call(gift_card: gift_card)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -14,12 +14,19 @@ module Spree
14
14
  end
15
15
  alias covered_by_store_credit covered_by_store_credit?
16
16
 
17
+ # Returns the total amount of store credits available to the user associated with the order.
18
+ # Returns only store credit for this store and same currency as order
19
+ #
20
+ # @return [BigDecimal] The total amount of store credits available to the user associated with the order.
17
21
  def total_available_store_credit
18
22
  return 0.0 unless user
19
23
 
20
24
  user.total_available_store_credit(currency, store)
21
25
  end
22
26
 
27
+ # Returns the available store credits for the user associated with the order.
28
+ #
29
+ # @return [Array<Spree::StoreCredit>] The available store credits for the user associated with the order.
23
30
  def available_store_credits
24
31
  return Spree::StoreCredit.none if user.nil?
25
32
 
@@ -27,15 +34,21 @@ module Spree
27
34
  end
28
35
 
29
36
  def could_use_store_credit?
30
- return false if Spree::PaymentMethod::StoreCredit.available.empty?
37
+ return false if store.payment_methods.store_credit.available.empty?
31
38
 
32
39
  total_available_store_credit > 0
33
40
  end
34
41
 
42
+ # Returns the total amount of the order minus the total amount of store credits applied to the order.
43
+ #
44
+ # @return [BigDecimal] The total amount of the order minus the total amount of store credits applied to the order.
35
45
  def order_total_after_store_credit
36
46
  total - total_applicable_store_credit
37
47
  end
38
48
 
49
+ # Returns the total amount of the order minus the total amount of store credits applied to the order.
50
+ #
51
+ # @return [BigDecimal] The total amount of the order minus the total amount of store credits applied to the order.
39
52
  def total_minus_store_credits
40
53
  total - total_applied_store_credit
41
54
  end
@@ -48,10 +61,16 @@ module Spree
48
61
  end
49
62
  end
50
63
 
64
+ # Returns the total amount of store credits applied to the order.
65
+ #
66
+ # @return [BigDecimal] The total amount of store credits applied to the order.
51
67
  def total_applied_store_credit
52
68
  payments.store_credits.valid.sum(:amount)
53
69
  end
54
70
 
71
+ # Returns true if the order is using store credit.
72
+ #
73
+ # @return [Boolean] True if the order is using store credit, false otherwise.
55
74
  def using_store_credit?
56
75
  total_applied_store_credit > 0
57
76
  end
@@ -4,6 +4,7 @@ require_dependency 'spree/order/digital'
4
4
  require_dependency 'spree/order/payments'
5
5
  require_dependency 'spree/order/store_credit'
6
6
  require_dependency 'spree/order/emails'
7
+ require_dependency 'spree/order/gift_card'
7
8
 
8
9
  module Spree
9
10
  class Order < Spree.base_class
@@ -11,6 +12,8 @@ module Spree
11
12
  SHIPMENT_STATES = %w(backorder canceled partial pending ready shipped)
12
13
  LINE_ITEM_REMOVABLE_STATES = %w(cart address delivery payment confirm resumed)
13
14
 
15
+ extend Spree::DisplayMoney
16
+
14
17
  include Spree::Order::Checkout
15
18
  include Spree::Order::CurrencyUpdater
16
19
  include Spree::Order::Digital
@@ -20,6 +23,7 @@ module Spree
20
23
  include Spree::Order::Emails
21
24
  include Spree::Order::Webhooks
22
25
  include Spree::Core::NumberGenerator.new(prefix: 'R')
26
+ include Spree::Order::GiftCard
23
27
 
24
28
  include Spree::NumberIdentifier
25
29
  include Spree::NumberAsParam
@@ -38,7 +42,6 @@ module Spree
38
42
 
39
43
  MEMOIZED_METHODS = %w(tax_zone)
40
44
 
41
- extend Spree::DisplayMoney
42
45
  money_methods :outstanding_balance, :item_total, :adjustment_total,
43
46
  :included_tax_total, :additional_tax_total, :tax_total,
44
47
  :shipment_total, :promo_total, :total,
@@ -313,10 +316,28 @@ module Spree
313
316
  shipments.any?(&:backordered?)
314
317
  end
315
318
 
319
+ # Check if the shipping address is a quick checkout address
320
+ # quick checkout addresses are incomplete as wallet providers like Apple Pay and Google Pay
321
+ # do not provide all the address fields until the checkout is completed (confirmed) on their side
322
+ # @return [Boolean]
316
323
  def quick_checkout?
317
324
  shipping_address.present? && shipping_address.quick_checkout?
318
325
  end
319
326
 
327
+ # Check if quick checkout is available for this order
328
+ # Either fully digital or not digital at all
329
+ # @return [Boolean]
330
+ def quick_checkout_available?
331
+ payment_required? && shipments.count <= 1 && (digital? || !some_digital? || !delivery_required?)
332
+ end
333
+
334
+ # Check if quick checkout requires an address collection
335
+ # If the order is digital or not delivery required, then we don't need to collect an address
336
+ # @return [Boolean]
337
+ def quick_checkout_require_address?
338
+ !digital? && delivery_required?
339
+ end
340
+
320
341
  # Returns the relevant zone (if any) to be used for taxation purposes.
321
342
  # Uses default tax zone unless there is a specific match
322
343
  def tax_zone
@@ -631,7 +652,7 @@ module Spree
631
652
  #
632
653
  # @return [BigDecimal] the total weight of the inventory units in the order
633
654
  def total_weight
634
- @total_weight ||= line_items.joins(:variant).includes(:variant).map { |li| li.variant.weight * li.quantity }.sum
655
+ @total_weight ||= line_items.joins(:variant).includes(:variant).map(&:item_weight).sum
635
656
  end
636
657
 
637
658
  # Returns line items that have no shipping rates
@@ -742,6 +763,11 @@ module Spree
742
763
  end
743
764
 
744
765
  def can_be_destroyed?
766
+ Spree::Deprecation.warn('Spree::Order#can_be_destroyed? is deprecated and will be removed in the next major version. Use Spree::Order#can_be_deleted? instead.')
767
+ can_be_deleted?
768
+ end
769
+
770
+ def can_be_deleted?
745
771
  !completed? && payments.completed.empty?
746
772
  end
747
773
 
@@ -834,6 +860,10 @@ module Spree
834
860
  line_items
835
861
  end
836
862
 
863
+ def requires_ship_address?
864
+ !digital?
865
+ end
866
+
837
867
  private
838
868
 
839
869
  def link_by_email
@@ -870,10 +900,15 @@ module Spree
870
900
 
871
901
  def after_cancel
872
902
  shipments.each(&:cancel!)
873
- payments.completed.each(&:cancel!)
874
903
 
875
- # Free up authorized store credits
876
- payments.store_credits.pending.each(&:void!)
904
+ # payments fully covered by gift card won't be refunded
905
+ # we want to only void the payment
906
+ if gift_card.present? && covered_by_store_credit?
907
+ payments.completed.store_credits.each(&:void!)
908
+ else
909
+ payments.completed.each(&:cancel!)
910
+ payments.store_credits.pending.each(&:void!)
911
+ end
877
912
 
878
913
  send_cancel_email
879
914
  update_with_updater!
@@ -895,7 +930,7 @@ module Spree
895
930
  end
896
931
 
897
932
  def ensure_currency_presence
898
- self.currency ||= store.default_currency
933
+ self.currency ||= store&.default_currency
899
934
  end
900
935
 
901
936
  def collect_payment_methods(store = nil)
@@ -911,5 +946,15 @@ module Spree
911
946
  def credit_card_nil_payment?(attributes)
912
947
  payments.store_credits.present? && attributes[:amount].to_f.zero?
913
948
  end
949
+
950
+ def recalculate_store_credit_payment
951
+ updater.update_adjustment_total if using_store_credit?
952
+
953
+ if gift_card.present?
954
+ recalculate_gift_card
955
+ elsif using_store_credit?
956
+ Spree::Dependencies.checkout_add_store_credit_service.constantize.call(order: self)
957
+ end
958
+ end
914
959
  end
915
960
  end
@@ -8,6 +8,7 @@ module Spree
8
8
  end
9
9
 
10
10
  def merge!(other_order, user = nil, discard_merged: true)
11
+ handle_gift_card(other_order)
11
12
  other_order.line_items.each do |other_order_line_item|
12
13
  next unless other_order_line_item.currency == order.currency
13
14
 
@@ -26,6 +27,8 @@ module Spree
26
27
  end
27
28
  end
28
29
 
30
+ private
31
+
29
32
  # Compare the line item of the other order with mine.
30
33
  # Make sure you allow any extensions to chime in on whether or
31
34
  # not the extension-specific parts of the line item match
@@ -70,5 +73,14 @@ module Spree
70
73
  updater.update_item_total
71
74
  updater.persist_totals
72
75
  end
76
+
77
+ def handle_gift_card(other_order)
78
+ return unless other_order.gift_card.present?
79
+
80
+ gift_card = other_order.gift_card
81
+
82
+ other_order.remove_gift_card
83
+ order.apply_gift_card(gift_card)
84
+ end
73
85
  end
74
86
  end
@@ -19,6 +19,10 @@ module Spree
19
19
  'news'
20
20
  end
21
21
 
22
+ def posts
23
+ Spree::Post.published.by_newest.limit(preferred_max_posts_to_show)
24
+ end
25
+
22
26
  private
23
27
 
24
28
  def make_posts_to_show_valid
@@ -102,7 +102,7 @@ module Spree
102
102
  end
103
103
 
104
104
  def available_for_order?(order)
105
- order.could_use_store_credit?
105
+ order.gift_card.present? || order.could_use_store_credit?
106
106
  end
107
107
 
108
108
  private
@@ -12,6 +12,7 @@ module Spree
12
12
 
13
13
  scope :active, -> { where(active: true).order(position: :asc) }
14
14
  scope :available, -> { active.where(display_on: [:front_end, :back_end, :both]) }
15
+ scope :store_credit, -> { where(type: 'Spree::PaymentMethod::StoreCredit') }
15
16
 
16
17
  after_initialize :set_name, if: :new_record?
17
18
 
@@ -54,6 +54,7 @@ module Spree
54
54
  # Scopes
55
55
  #
56
56
  scope :published, -> { where(published_at: [..Time.current]) }
57
+ scope :by_newest, -> { order(created_at: :desc) }
57
58
 
58
59
  delegate :name, to: :author, prefix: true, allow_nil: true
59
60
  delegate :title, to: :post_category, prefix: true, allow_nil: true
@@ -36,7 +36,13 @@ module Spree
36
36
  scope :with_currency, ->(currency) { where(currency: currency) }
37
37
  scope :non_zero, -> { where.not(amount: [nil, 0]) }
38
38
  scope :discounted, -> { where('compare_at_amount > amount') }
39
- scope :for_products, ->(products) { joins(variant: :product).where("#{Spree::Product.table_name}.id" => products) }
39
+ scope :for_products, ->(products, currency = nil) do
40
+ currency ||= Spree::Store.default.default_currency
41
+
42
+ with_currency(currency).joins(:variant).where(
43
+ Spree::Variant.table_name => { product_id: products }
44
+ )
45
+ end
40
46
 
41
47
  extend DisplayMoney
42
48
  money_methods :amount, :price, :compare_at_amount
@@ -0,0 +1,103 @@
1
+ module Spree
2
+ class Product < Spree.base_class
3
+ module Slugs
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ extend FriendlyId
8
+ include Spree::TranslatableResourceSlug
9
+
10
+ translates :slug
11
+ friendly_id :slug_candidates, use: [:history, :slugged, :scoped, :mobility], scope: spree_base_uniqueness_scope, slug_limit: 255
12
+
13
+ Product::Translation.class_eval do
14
+ before_save :set_slug
15
+ acts_as_paranoid
16
+ # deleted translation values also need to be accessible for index views listing deleted resources
17
+ default_scope { unscope(where: :deleted_at) }
18
+
19
+ private
20
+
21
+ def set_slug
22
+ self.slug = generate_slug
23
+ end
24
+
25
+ def generate_slug
26
+ if name.blank? && slug.blank?
27
+ translated_model.name.to_url
28
+ elsif slug.blank?
29
+ name.to_url
30
+ else
31
+ slug.to_url
32
+ end
33
+ end
34
+ end
35
+
36
+ before_validation :downcase_slug
37
+ before_validation :normalize_slug, on: :update
38
+ after_destroy :punch_slugs
39
+ after_restore :regenerate_slug
40
+
41
+ validates :slug, presence: true, uniqueness: { allow_blank: true, case_sensitive: true, scope: spree_base_uniqueness_scope }
42
+
43
+ def self.slug_available?(slug, id)
44
+ !where(slug: slug).where.not(id: id).exists?
45
+ end
46
+ end
47
+
48
+ def ensure_slug_is_unique(candidate_slug)
49
+ return slug if candidate_slug.blank? || slug.blank?
50
+ return candidate_slug if self.class.slug_available?(candidate_slug, id)
51
+
52
+ normalize_friendly_id([candidate_slug, uuid_for_friendly_id])
53
+ end
54
+
55
+ private
56
+
57
+ def slug_candidates
58
+ if defined?(:deleted_at) && deleted_at.present?
59
+ [
60
+ ['deleted', :name],
61
+ ['deleted', :name, :sku],
62
+ ['deleted', :name, :uuid_for_friendly_id]
63
+ ]
64
+ else
65
+ [
66
+ [:name],
67
+ [:name, :sku],
68
+ [:name, :uuid_for_friendly_id]
69
+ ]
70
+ end
71
+ end
72
+
73
+ def downcase_slug
74
+ slug&.downcase!
75
+ end
76
+
77
+ def normalize_slug
78
+ self.slug = normalize_friendly_id(slug)
79
+ end
80
+
81
+ def regenerate_slug
82
+ self.slug = nil
83
+ save!
84
+ end
85
+
86
+ def punch_slugs
87
+ return if new_record? || frozen?
88
+
89
+ self.slug = nil
90
+
91
+ set_slug
92
+ update_column(:slug, slug)
93
+
94
+ new_slug = ->(rec) { "deleted-#{rec.id}_#{rec.slug}"[..254] }
95
+
96
+ translations.with_deleted.each { |rec| rec.update_columns(slug: new_slug.call(rec)) }
97
+ slugs.with_deleted.each { |rec| rec.update_column(:slug, new_slug.call(rec)) }
98
+
99
+ translations.find_by!(locale: I18n.locale).update_column(:slug, slug) if Spree.use_translations?
100
+ end
101
+ end
102
+ end
103
+ end
@@ -20,14 +20,17 @@
20
20
 
21
21
  module Spree
22
22
  class Product < Spree.base_class
23
- extend FriendlyId
23
+ acts_as_paranoid
24
+ acts_as_taggable_on :tags, :labels
25
+ auto_strip_attributes :name
26
+
24
27
  include Spree::ProductScopes
25
28
  include Spree::MultiStoreResource
26
29
  include Spree::TranslatableResource
27
- include Spree::TranslatableResourceSlug
28
30
  include Spree::MemoizedData
29
31
  include Spree::Metadata
30
32
  include Spree::Product::Webhooks
33
+ include Spree::Product::Slugs
31
34
  if defined?(Spree::VendorConcern)
32
35
  include Spree::VendorConcern
33
36
  end
@@ -50,39 +53,10 @@ module Spree
50
53
  if defined?(PgSearch)
51
54
  include PgSearch::Model
52
55
 
53
- if connected? && connection.extension_enabled?('pg_trgm')
54
- pg_search_scope :search_by_name, against: { name: 'A', meta_title: 'B' }, using: { trigram: { threshold: 0.3, word_similarity: true } }
55
- else
56
- pg_search_scope :search_by_name, against: { name: 'A', meta_title: 'B' }, using: { tsearch: { any_word: true, prefix: true } }
57
- end
58
- end
59
-
60
- before_save :set_slug
61
- acts_as_paranoid
62
- # deleted translation values also need to be accessible for index views listing deleted resources
63
- default_scope { unscope(where: :deleted_at) }
64
- def set_slug
65
- self.slug = generate_slug
66
- end
67
-
68
- private
69
-
70
- def generate_slug
71
- if name.blank? && slug.blank?
72
- translated_model.name.to_url
73
- elsif slug.blank?
74
- name.to_url
75
- else
76
- slug.to_url
77
- end
56
+ pg_search_scope :search_by_name, against: { name: 'A', meta_title: 'B' }, using: { trigram: { threshold: 0.3, word_similarity: true } }
78
57
  end
79
58
  end
80
59
 
81
- friendly_id :slug_candidates, use: [:history, :scoped, :mobility], scope: spree_base_uniqueness_scope
82
- acts_as_paranoid
83
- auto_strip_attributes :name
84
- acts_as_taggable_on :tags, :labels
85
-
86
60
  # we need to have this callback before any dependent: :destroy associations
87
61
  # https://github.com/rails/rails/issues/3458
88
62
  before_destroy :ensure_not_in_complete_orders
@@ -148,17 +122,12 @@ module Spree
148
122
  after_initialize :ensure_master
149
123
  after_initialize :assign_default_tax_category
150
124
 
151
- before_validation :downcase_slug
152
- before_validation :normalize_slug, on: :update
153
125
  before_validation :validate_master
154
126
  before_validation :ensure_default_shipping_category
155
127
 
156
128
  after_create :add_associations_from_prototype
157
129
  after_create :build_variants_from_option_values_hash, if: :option_values_hash
158
130
 
159
- after_destroy :punch_slug
160
- after_restore :update_slug_history
161
-
162
131
  after_save :save_master
163
132
  after_save :run_touch_callbacks, if: :anything_changed?
164
133
  after_save :reset_nested_changes
@@ -176,7 +145,6 @@ module Spree
176
145
  validates :price, if: :requires_price?
177
146
  end
178
147
 
179
- validates :slug, presence: true, uniqueness: { allow_blank: true, case_sensitive: true, scope: spree_base_uniqueness_scope }
180
148
  validate :discontinue_on_must_be_later_than_make_active_at, if: -> { make_active_at && discontinue_on }
181
149
 
182
150
  scope :for_store, ->(store) { joins(:store_products).where(StoreProduct.table_name => { store_id: store.id }) }
@@ -231,7 +199,7 @@ module Spree
231
199
  having("SUM(#{Spree::StockItem.table_name}.count_on_hand) <= 0")
232
200
  }
233
201
  scope :out_of_stock, lambda {
234
- joins(:stock_items).where("#{Spree::StockItem.table_name}.count_on_hand <= ? OR #{Spree::Variant.table_name}.track_inventory = ?", 0, false)
202
+ joins(:stock_items).where("#{Spree::Variant.table_name}.track_inventory = ? OR #{Spree::StockItem.table_name}.count_on_hand <= ?", false, 0)
235
203
  }
236
204
 
237
205
  scope :by_best_selling, lambda { |order_direction = :desc|
@@ -515,17 +483,6 @@ module Spree
515
483
  where conditions.inject(:or)
516
484
  end
517
485
 
518
- def self.slug_available?(slug, id)
519
- !where(slug: slug).where.not(id: id).exists?
520
- end
521
-
522
- def ensure_slug_is_unique(candidate_slug)
523
- return slug if candidate_slug.blank? || slug.blank?
524
- return candidate_slug if self.class.slug_available?(candidate_slug, id)
525
-
526
- normalize_friendly_id([candidate_slug, uuid_for_friendly_id])
527
- end
528
-
529
486
  # Suitable for displaying only variants that has at least one option value.
530
487
  # There may be scenarios where an option type is removed and along with it
531
488
  # all option values. At that point all variants associated with only those
@@ -741,25 +698,6 @@ module Spree
741
698
  self.tax_category = Spree::TaxCategory.default if new_record?
742
699
  end
743
700
 
744
- def normalize_slug
745
- self.slug = normalize_friendly_id(slug)
746
- end
747
-
748
- def punch_slug
749
- # punch slug with date prefix to allow reuse of original
750
- return if frozen?
751
-
752
- update_column(:slug, "#{Time.current.to_i}_#{slug}"[0..254])
753
-
754
- translations.with_deleted.each do |t|
755
- t.update_column :slug, "#{Time.current.to_i}_#{t.slug}"[0..254]
756
- end
757
- end
758
-
759
- def update_slug_history
760
- save!
761
- end
762
-
763
701
  def anything_changed?
764
702
  saved_changes? || @nested_changes
765
703
  end
@@ -815,14 +753,6 @@ module Spree
815
753
  end
816
754
  end
817
755
 
818
- # Try building a slug based on the following fields in increasing order of specificity.
819
- def slug_candidates
820
- [
821
- :name,
822
- [:name, :sku]
823
- ]
824
- end
825
-
826
756
  def run_touch_callbacks
827
757
  run_callbacks(:touch)
828
758
  end
@@ -870,10 +800,6 @@ module Spree
870
800
  previously_new_record? || tag_list_previously_changed? || available_on_previously_changed?
871
801
  end
872
802
 
873
- def downcase_slug
874
- slug&.downcase!
875
- end
876
-
877
803
  def after_activate
878
804
  # Implement your logic here
879
805
  end
@@ -31,9 +31,9 @@ module Spree
31
31
  default_scope { order(:position) }
32
32
 
33
33
  scope :filterable, -> { joins(:property).where(Property.table_name => { filterable: true }) }
34
- scope :for_products, ->(products) { joins(:product).merge(products) }
34
+ scope :for_products, ->(products) { where(product_id: products) }
35
35
  scope :sort_by_property_position, -> {
36
- unscope(:order).joins(:property).order("spree_properties.position ASC")
36
+ unscope(:order).joins(:property).order(Spree::Property.table_name => { position: :asc })
37
37
  }
38
38
 
39
39
  self.whitelisted_ransackable_attributes = ['value', 'filter_param']
@@ -11,6 +11,27 @@ module Spree
11
11
  end
12
12
 
13
13
  def apply
14
+ if load_gift_card_code
15
+
16
+ if @gift_card.expired?
17
+ set_error_code :gift_card_expired
18
+ return self
19
+ elsif @gift_card.redeemed?
20
+ set_error_code :gift_card_already_redeemed
21
+ return self
22
+ end
23
+
24
+ result = order.apply_gift_card(@gift_card)
25
+
26
+ if result.success?
27
+ set_success_code(:gift_card_applied)
28
+ else
29
+ set_error_code(result.value, result.error.value || {})
30
+ end
31
+
32
+ return self
33
+ end
34
+
14
35
  if order.coupon_code.present?
15
36
  if promotion.present? && promotion.actions.exists?
16
37
  handle_present_promotion
@@ -26,6 +47,18 @@ module Spree
26
47
  end
27
48
 
28
49
  def remove(coupon_code)
50
+ if order.gift_card
51
+ result = order.remove_gift_card
52
+
53
+ if result.success?
54
+ set_success_code(:gift_card_removed)
55
+ else
56
+ set_error_code(result.value)
57
+ end
58
+
59
+ return self
60
+ end
61
+
29
62
  promotion = order.promotions.with_coupon_code(coupon_code)
30
63
  if promotion.present?
31
64
  # Order promotion has to be destroyed before line item removing
@@ -179,6 +212,12 @@ module Spree
179
212
  def handle_coupon_code(discount, coupon_code)
180
213
  discount.source.promotion.coupon_codes.unused.find_by(code: coupon_code)&.apply_order!(order)
181
214
  end
215
+
216
+ def load_gift_card_code
217
+ return unless order.coupon_code.present?
218
+
219
+ @gift_card = order.store.gift_cards.find_by(code: order.coupon_code.downcase)
220
+ end
182
221
  end
183
222
  end
184
223
  end