spree_core 5.1.0.beta3 → 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 (85) 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/payment_source_concern.rb +39 -0
  8. data/app/models/concerns/spree/product_scopes.rb +1 -5
  9. data/app/models/concerns/spree/stores/socials.rb +6 -2
  10. data/app/models/concerns/spree/user_methods.rb +2 -1
  11. data/app/models/spree/ability.rb +2 -0
  12. data/app/models/spree/address.rb +11 -3
  13. data/app/models/spree/credit_card.rb +2 -24
  14. data/app/models/spree/gateway/custom_payment_source_method.rb +33 -0
  15. data/app/models/spree/gift_card.rb +162 -0
  16. data/app/models/spree/gift_card_batch.rb +79 -0
  17. data/app/models/spree/inventory_unit.rb +11 -4
  18. data/app/models/spree/line_item.rb +13 -2
  19. data/app/models/spree/option_type.rb +5 -15
  20. data/app/models/spree/option_value.rb +18 -12
  21. data/app/models/spree/order/checkout.rb +2 -0
  22. data/app/models/spree/order/gift_card.rb +51 -0
  23. data/app/models/spree/order/store_credit.rb +20 -1
  24. data/app/models/spree/order.rb +51 -6
  25. data/app/models/spree/order_merger.rb +12 -0
  26. data/app/models/spree/page_sections/featured_posts.rb +4 -0
  27. data/app/models/spree/payment.rb +2 -1
  28. data/app/models/spree/payment_method/store_credit.rb +1 -1
  29. data/app/models/spree/payment_method.rb +1 -0
  30. data/app/models/spree/payment_source.rb +21 -0
  31. data/app/models/spree/post.rb +1 -0
  32. data/app/models/spree/price.rb +7 -1
  33. data/app/models/spree/product/slugs.rb +103 -0
  34. data/app/models/spree/product.rb +7 -81
  35. data/app/models/spree/product_property.rb +2 -2
  36. data/app/models/spree/promotion_handler/coupon.rb +39 -0
  37. data/app/models/spree/property.rb +2 -8
  38. data/app/models/spree/reimbursement_type/reimbursement_helpers.rb +1 -1
  39. data/app/models/spree/return_item.rb +4 -0
  40. data/app/models/spree/shipment.rb +19 -0
  41. data/app/models/spree/shipping_method.rb +8 -9
  42. data/app/models/spree/store.rb +2 -0
  43. data/app/models/spree/store_credit.rb +9 -6
  44. data/app/models/spree/store_credit_event.rb +8 -4
  45. data/app/models/spree/taxon.rb +8 -1
  46. data/app/models/spree/theme.rb +1 -1
  47. data/app/models/spree/variant.rb +1 -1
  48. data/app/models/spree/wishlist.rb +7 -0
  49. data/app/presenters/spree/csv/product_variant_presenter.rb +14 -3
  50. data/app/services/spree/addresses/phone_validator.rb +20 -0
  51. data/app/services/spree/cart/destroy.rb +1 -1
  52. data/app/services/spree/cart/recalculate.rb +1 -0
  53. data/app/services/spree/gift_cards/apply.rb +66 -0
  54. data/app/services/spree/gift_cards/redeem.rb +17 -0
  55. data/app/services/spree/gift_cards/remove.rb +38 -0
  56. data/app/services/spree/products/prepare_nested_attributes.rb +9 -2
  57. data/app/views/spree/addresses/_form.html.erb +1 -1
  58. data/app/views/spree/shared/_payment.html.erb +13 -4
  59. data/config/locales/en.yml +44 -14
  60. data/config/routes.rb +1 -0
  61. data/db/migrate/20250506073057_create_spree_gift_cards_and_spree_gift_card_batches.rb +37 -0
  62. data/db/migrate/20250530101236_enable_pg_trgm_extension.rb +13 -0
  63. data/db/migrate/20250605131334_add_missing_fields_to_users.rb +25 -0
  64. data/lib/generators/spree/install/install_generator.rb +0 -8
  65. data/lib/spree/core/configuration.rb +4 -0
  66. data/lib/spree/core/controller_helpers/store.rb +13 -1
  67. data/lib/spree/core/dependencies.rb +5 -0
  68. data/lib/spree/core/engine.rb +8 -1
  69. data/lib/spree/core/version.rb +1 -1
  70. data/lib/spree/core.rb +2 -1
  71. data/lib/spree/permitted_attributes.rb +23 -10
  72. data/lib/spree/testing_support/authorization_helpers.rb +5 -9
  73. data/lib/spree/testing_support/common_rake.rb +7 -1
  74. data/lib/spree/testing_support/factories/gift_card_batch_factory.rb +5 -0
  75. data/lib/spree/testing_support/factories/gift_card_factory.rb +17 -0
  76. data/lib/spree/testing_support/factories/page_section_factory.rb +2 -1
  77. data/lib/spree/testing_support/factories/payment_factory.rb +5 -0
  78. data/lib/spree/testing_support/factories/payment_method_factory.rb +5 -0
  79. data/lib/spree/testing_support/factories/payment_source_factory.rb +5 -0
  80. data/lib/spree/testing_support/factories/post_factory.rb +4 -0
  81. data/lib/spree/testing_support/factories/promotion_rule_factory.rb +4 -0
  82. data/lib/spree/testing_support/factories/theme_factory.rb +1 -1
  83. data/lib/spree/testing_support/factories/user_factory.rb +2 -2
  84. metadata +48 -11
  85. data/config/initializers/state_machine.rb +0 -37
@@ -48,7 +48,7 @@ module Spree
48
48
 
49
49
  after_create :update_tax_charge
50
50
 
51
- delegate :name, :description, :sku, :should_track_inventory?, :product, :options_text, :slug, :product_id, to: :variant
51
+ delegate :name, :description, :sku, :should_track_inventory?, :product, :options_text, :slug, :product_id, :dimensions_unit, :weight_unit, to: :variant
52
52
  delegate :brand, :category, to: :product
53
53
  delegate :tax_zone, to: :order
54
54
  delegate :digital?, to: :variant
@@ -85,7 +85,7 @@ module Spree
85
85
  extend DisplayMoney
86
86
  money_methods :amount, :subtotal, :discounted_amount, :final_amount, :total, :price,
87
87
  :adjustment_total, :additional_tax_total, :promo_total, :included_tax_total,
88
- :pre_tax_amount, :shipping_cost, :tax_total
88
+ :pre_tax_amount, :shipping_cost, :tax_total, :compare_at_amount
89
89
 
90
90
  alias single_money display_price
91
91
  alias single_display_amount display_price
@@ -94,6 +94,10 @@ module Spree
94
94
  price * quantity
95
95
  end
96
96
 
97
+ def compare_at_amount
98
+ (variant.compare_at_amount_in(currency) || 0) * quantity
99
+ end
100
+
97
101
  alias subtotal amount
98
102
 
99
103
  def taxable_amount
@@ -114,6 +118,13 @@ module Spree
114
118
  amount + adjustment_total
115
119
  end
116
120
 
121
+ # Returns the weight of the line item
122
+ #
123
+ # @return [BigDecimal]
124
+ def item_weight
125
+ variant.weight * quantity
126
+ end
127
+
117
128
  alias total final_amount
118
129
  alias money display_total
119
130
 
@@ -10,12 +10,11 @@ module Spree
10
10
  include Spree::Webhooks::HasWebhooks
11
11
  end
12
12
 
13
- if Spree.always_use_translations?
14
- TRANSLATABLE_FIELDS = %i[name presentation].freeze
15
- translates(*TRANSLATABLE_FIELDS)
16
- else
17
- TRANSLATABLE_FIELDS = %i[presentation].freeze
18
- translates(*TRANSLATABLE_FIELDS, column_fallback: true)
13
+ TRANSLATABLE_FIELDS = %i[presentation].freeze
14
+ translates(*TRANSLATABLE_FIELDS, column_fallback: !Spree.always_use_translations?)
15
+
16
+ self::Translation.class_eval do
17
+ auto_strip_attributes :presentation
19
18
  end
20
19
 
21
20
  #
@@ -23,7 +22,6 @@ module Spree
23
22
  #
24
23
  self.whitelisted_ransackable_scopes = %w[search_by_name]
25
24
  acts_as_list
26
- auto_strip_attributes :name, :presentation
27
25
 
28
26
  #
29
27
  # Associations
@@ -47,14 +45,6 @@ module Spree
47
45
  scope :colors, -> { where(name: COLOR_NAMES) }
48
46
  scope :filterable, -> { where(filterable: true) }
49
47
 
50
- if defined?(PgSearch)
51
- # full text search
52
- include PgSearch::Model
53
- pg_search_scope :search_by_name, against: %i[name presentation]
54
- else
55
- scope :search_by_name, ->(query) { where('name LIKE ?', "%#{query}%") }
56
- end
57
-
58
48
  #
59
49
  # Attributes
60
50
  #
@@ -7,19 +7,17 @@ module Spree
7
7
  include Spree::Webhooks::HasWebhooks
8
8
  end
9
9
 
10
- if Spree.always_use_translations?
11
- TRANSLATABLE_FIELDS = %i[name presentation].freeze
12
- translates(*TRANSLATABLE_FIELDS)
13
- else
14
- TRANSLATABLE_FIELDS = %i[presentation].freeze
15
- translates(*TRANSLATABLE_FIELDS, column_fallback: true)
10
+ TRANSLATABLE_FIELDS = %i[presentation].freeze
11
+ translates(*TRANSLATABLE_FIELDS, column_fallback: !Spree.always_use_translations?)
12
+
13
+ self::Translation.class_eval do
14
+ auto_strip_attributes :presentation
16
15
  end
17
16
 
18
17
  #
19
18
  # Magic methods
20
19
  #
21
20
  acts_as_list scope: :option_type
22
- auto_strip_attributes :name, :presentation
23
21
  self.whitelisted_ransackable_attributes = ['presentation']
24
22
 
25
23
  #
@@ -48,8 +46,8 @@ module Spree
48
46
  }
49
47
 
50
48
  scope :for_products, lambda { |products|
51
- joins(:variants).
52
- where(Variant.table_name => { product_id: products.map(&:id) })
49
+ # we need to use map(&:id) to avoid SQL errors when merging with other scopes
50
+ joins(:variants).where(Spree::Variant.table_name => { product_id: products.map(&:id) })
53
51
  }
54
52
 
55
53
  #
@@ -61,15 +59,23 @@ module Spree
61
59
 
62
60
  delegate :name, :presentation, to: :option_type, prefix: true, allow_nil: true
63
61
 
62
+ # Using map here instead of pluck, as these values are translatable via Mobility gem
63
+ # @return [Array<Hash>]
64
64
  def self.to_tom_select_json
65
- all.pluck(:id, :presentation).map do |id, presentation|
65
+ all.map do |ov|
66
66
  {
67
- id: id,
68
- name: presentation
67
+ id: ov.name,
68
+ name: ov.presentation
69
69
  }
70
70
  end
71
71
  end
72
72
 
73
+ # Returns the presentation with the option type presentation, eg. "Color: Red"
74
+ # @return [String]
75
+ def display_presentation
76
+ @display_presentation ||= "#{option_type.presentation}: #{presentation}"
77
+ end
78
+
73
79
  private
74
80
 
75
81
  def touch_all_variants
@@ -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
@@ -302,7 +302,8 @@ module Spree
302
302
  # Payment profile cannot be created without source
303
303
  return unless source
304
304
  # Imported payments shouldn't create a payment profile.
305
- return if source.imported
305
+ # Imported is only available on Spree::CreditCard, non-credit card payments should not have this attribute.
306
+ return if source.respond_to?(:imported) && source.imported
306
307
 
307
308
  payment_method.create_profile(self)
308
309
  rescue ActiveMerchant::ConnectionError => e
@@ -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
 
@@ -1,10 +1,31 @@
1
+ # This model is used to store payment sources for non-credit card payments, eg wallet, account, etc.
1
2
  module Spree
2
3
  class PaymentSource < Spree.base_class
3
4
  include Spree::Metadata
5
+ include Spree::PaymentSourceConcern
4
6
 
7
+ #
8
+ # Associations
9
+ #
5
10
  belongs_to :payment_method, class_name: 'Spree::PaymentMethod'
6
11
  belongs_to :user, class_name: Spree.user_class.to_s, optional: true
7
12
 
13
+ #
14
+ # Validations
15
+ #
8
16
  validates_uniqueness_of :gateway_payment_profile_id, scope: :type
17
+
18
+ #
19
+ # Delegations
20
+ #
21
+ delegate :profile_id, to: :gateway_customer, prefix: true, allow_nil: true
22
+
23
+ # Returns the gateway customer for the user.
24
+ # @return [Spree::GatewayCustomer]
25
+ def gateway_customer
26
+ return if user.blank?
27
+
28
+ payment_method.gateway_customers.find_by(user: user)
29
+ end
9
30
  end
10
31
  end
@@ -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