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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7dbf1c3a02958e3dcb370c47d5cd4e9c209924301fcf451e91cdce682e60ea74
4
- data.tar.gz: 83d62b4be4aeeb60fda78de84dfe73bd5ebf9603b32b5b938274be67a54c25a0
3
+ metadata.gz: a357b0d9005bb8d7ed5b4b56ebe020d34435ac15a402db9d650880ae01c38127
4
+ data.tar.gz: 729183094fa42d6d5b00632e5b9d44b61958643aef7b42d1b7931dd98c87b0f6
5
5
  SHA512:
6
- metadata.gz: fdfef29cde021604e1309c49fd2218ced2bbf37c66b4642a88979955e57d6744a1f6630b1e8937aa4633d626f8c509bf8a7c3fc1c5bf76f0c5ba3f8472bd7fe1
7
- data.tar.gz: a2d843bd61853ce37850019f7d378a83cd7fe88b523f1be07354271350498e851625caa9a49f969cbf338a00c12bb289d55a449f8a212d1a8290bab75d720971
6
+ metadata.gz: 49525a096671b7fce9970204619b1d05d7a421b3d6966b2ca139e492dda1ee2cd3a7339b3ee37ebbb4a31e2e93cdcfc49f8dffd6371ca91f813d9782259bf91f
7
+ data.tar.gz: faff9b9dd0388d354efcef394b6051951426824ee4d9e05ad5e6c3f952cf3eeba46ba40e631ee306f861529f840efb01e5b138075c188300bded5a88aec65d49
@@ -230,7 +230,7 @@ module Spree
230
230
  end
231
231
 
232
232
  def spree_base_cache_key
233
- [
233
+ @spree_base_cache_key ||= [
234
234
  I18n.locale,
235
235
  (current_currency if defined?(current_currency)),
236
236
  defined?(try_spree_current_user) && try_spree_current_user.present?,
@@ -246,6 +246,11 @@ module Spree
246
246
  Spree::DatabaseTypeUtilities.maximum_value_for(:integer)
247
247
  end
248
248
 
249
+ def payment_method_icon_tag(payment_method, opts = {})
250
+ image_tag "payment_icons/#{payment_method}.svg", opts
251
+ rescue Sprockets::Rails::Helper::AssetNotFound
252
+ end
253
+
249
254
  private
250
255
 
251
256
  def create_product_image_tag(image, product, options, style)
@@ -15,21 +15,23 @@ module Spree
15
15
  end
16
16
 
17
17
  def spree_image_url(image, options = {})
18
+ return unless image
19
+ return unless image.variable?
20
+ return if image.respond_to?(:attached?) && !image.attached?
21
+
22
+ url_helpers = respond_to?(:main_app) ? main_app : Rails.application.routes.url_helpers
18
23
  width = options[:width]
19
24
  height = options[:height]
20
25
 
21
- width = width * 2 if width.present?
22
- height = height * 2 if height.present?
23
-
24
- return unless image.attached?
25
- return unless image.variable?
26
+ width *= 2 if width.present?
27
+ height *= 2 if height.present?
26
28
 
27
29
  if width.present? && height.present?
28
- main_app.cdn_image_url(
30
+ url_helpers.cdn_image_url(
29
31
  image.variant(spree_image_variant_options(resize_to_fill: [width, height]))
30
32
  )
31
33
  else
32
- main_app.cdn_image_url(
34
+ url_helpers.cdn_image_url(
33
35
  image.variant(spree_image_variant_options(resize_to_limit: [width, height]))
34
36
  )
35
37
  end
@@ -51,17 +53,18 @@ module Spree
51
53
  return unless height
52
54
  return if height.zero?
53
55
 
54
- w, h = width.to_f, height.to_f
56
+ w = width.to_f
57
+ h = height.to_f
55
58
 
56
59
  # Always return width / height, flipping if needed
57
- if h > w
58
- ratio = h / w
59
- elsif h < w
60
- ratio = w / h
61
- else
62
- # h == w, square image
63
- ratio = 1.0
64
- end
60
+ ratio = if h > w
61
+ h / w
62
+ elsif h < w
63
+ w / h
64
+ else
65
+ # h == w, square image
66
+ 1.0
67
+ end
65
68
 
66
69
  ratio.round(3)
67
70
  end
@@ -0,0 +1,12 @@
1
+ module Spree
2
+ module ShipmentHelper
3
+ def shipment_tracking_link_to(shipment:, name: nil, html_options: {})
4
+ tracking_url = shipment.tracking_url.presence
5
+ return '' unless tracking_url
6
+
7
+ display_text = name || shipment.tracking.presence || tracking_url
8
+
9
+ link_to display_text, tracking_url, html_options
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ module Spree
2
+ module GiftCards
3
+ class BulkGenerateJob < ::Spree::BaseJob
4
+ queue_as Spree.queues.gift_cards
5
+
6
+ def perform(id)
7
+ gift_cards_batch = Spree::GiftCardBatch.find(id)
8
+
9
+ gift_cards_batch.create_gift_cards
10
+ end
11
+ end
12
+ end
13
+ end
@@ -3,12 +3,23 @@ module Spree
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
+ auto_strip_attributes :name, :presentation
7
+
6
8
  #
7
9
  # Callbacks
8
10
  #
9
11
  before_validation :set_name_from_presentation, if: -> { name.blank? }
10
12
  before_validation :normalize_name
11
13
 
14
+ #
15
+ # Scopes
16
+ #
17
+ scope :search_by_name, ->(query) do
18
+ i18n do
19
+ name.matches("%#{query.downcase}%").or(presentation.matches("%#{query.downcase}%"))
20
+ end
21
+ end
22
+
12
23
  def set_name_from_presentation
13
24
  self.name = presentation
14
25
  end
@@ -0,0 +1,39 @@
1
+ module Spree
2
+ module PaymentSourceConcern
3
+ extend ActiveSupport::Concern
4
+
5
+ # Available actions for the payment source.
6
+ # @return [Array<String>]
7
+ def actions
8
+ %w{capture void credit}
9
+ end
10
+
11
+ # Indicates whether its possible to capture the payment
12
+ # @param payment [Spree::Payment]
13
+ # @return [Boolean]
14
+ def can_capture?(payment)
15
+ payment.pending? || payment.checkout?
16
+ end
17
+
18
+ # Indicates whether its possible to void the payment.
19
+ # @param payment [Spree::Payment]
20
+ # @return [Boolean]
21
+ def can_void?(payment)
22
+ !payment.failed? && !payment.void?
23
+ end
24
+
25
+ # Indicates whether its possible to credit the payment. Note that most gateways require that the
26
+ # payment be settled first which generally happens within 12-24 hours of the transaction.
27
+ # @param payment [Spree::Payment]
28
+ # @return [Boolean]
29
+ def can_credit?(payment)
30
+ payment.completed? && payment.credit_allowed > 0
31
+ end
32
+
33
+ # Returns true if the payment source has a payment profile.
34
+ # @return [Boolean]
35
+ def has_payment_profile?
36
+ gateway_customer_profile_id.present? || gateway_payment_profile_id.present?
37
+ end
38
+ end
39
+ end
@@ -306,11 +306,7 @@ module Spree
306
306
  if defined?(PgSearch)
307
307
  include PgSearch::Model
308
308
 
309
- if connected? && connection.extension_enabled?('pg_trgm')
310
- pg_search_scope :search_by_name, against: { name: 'A', meta_title: 'B' }, using: { trigram: { threshold: 0.3, word_similarity: true } }
311
- else
312
- pg_search_scope :search_by_name, against: { name: 'A', meta_title: 'B' }, using: { tsearch: { any_word: true, prefix: true } }
313
- end
309
+ pg_search_scope :search_by_name, against: { name: 'A', meta_title: 'B' }, using: { trigram: { threshold: 0.3, word_similarity: true } }
314
310
  else
315
311
  def self.search_by_name(query)
316
312
  i18n { name.lower.matches("%#{query.downcase}%") }
@@ -3,7 +3,7 @@ module Spree
3
3
  module Socials
4
4
  extend ActiveSupport::Concern
5
5
 
6
- SUPPORTED_SOCIAL_NETWORKS = %w[instagram facebook twitter pinterest tiktok youtube spotify].freeze
6
+ SUPPORTED_SOCIAL_NETWORKS = %w[instagram facebook twitter pinterest tiktok youtube spotify discord].freeze
7
7
 
8
8
  SOCIAL_NETWORKS_CONFIG = {
9
9
  twitter: {
@@ -33,6 +33,10 @@ module Spree
33
33
  spotify: {
34
34
  input_placeholder: 'https://open.spotify.com/user/your_handle',
35
35
  profile_link: 'https://open.spotify.com/user/your_handle'
36
+ },
37
+ discord: {
38
+ input_placeholder: 'https://discord.com/invite/your_handle',
39
+ profile_link: 'https://discord.com/invite/your_handle'
36
40
  }
37
41
  }.freeze
38
42
 
@@ -61,7 +65,7 @@ module Spree
61
65
  end
62
66
 
63
67
  def social_links
64
- @social_links ||= [instagram_link, facebook_link, twitter_link, pinterest_link, youtube_link, tiktok_link, spotify_link].compact_blank
68
+ @social_links ||= [instagram_link, facebook_link, twitter_link, pinterest_link, youtube_link, tiktok_link, spotify_link, discord_link].compact_blank
65
69
  end
66
70
  end
67
71
  end
@@ -31,6 +31,7 @@ module Spree
31
31
  has_many :wishlists, class_name: 'Spree::Wishlist', foreign_key: :user_id, dependent: :destroy
32
32
  has_many :wished_items, through: :wishlists, source: :wished_items
33
33
  has_many :gateway_customers, class_name: 'Spree::GatewayCustomer', foreign_key: :user_id
34
+ has_many :gift_cards, class_name: 'Spree::GiftCard', foreign_key: :user_id, dependent: :destroy
34
35
  belongs_to :ship_address, class_name: 'Spree::Address', optional: true
35
36
  belongs_to :bill_address, class_name: 'Spree::Address', optional: true
36
37
 
@@ -103,7 +104,7 @@ module Spree
103
104
  def total_available_store_credit(currency = nil, store = nil)
104
105
  store ||= Store.default
105
106
  currency ||= store.default_currency
106
- store_credits.for_store(store).where(currency: currency).reload.to_a.sum(&:amount_remaining)
107
+ store_credits.without_gift_card.for_store(store).where(currency: currency).reload.to_a.sum(&:amount_remaining)
107
108
  end
108
109
 
109
110
  # Returns the available store credits for the current store per currency
@@ -61,6 +61,8 @@ module Spree
61
61
  can :manage, :all
62
62
  cannot :cancel, Spree::Order
63
63
  can :cancel, Spree::Order, &:allow_cancel?
64
+ cannot :destroy, Spree::Order
65
+ can :destroy, Spree::Order, &:can_be_deleted?
64
66
  cannot [:edit, :update], Spree::RefundReason, mutable: false
65
67
  cannot [:edit, :update], Spree::ReimbursementType, mutable: false
66
68
  end
@@ -6,9 +6,6 @@ module Spree
6
6
  if defined?(Spree::Webhooks::HasWebhooks)
7
7
  include Spree::Webhooks::HasWebhooks
8
8
  end
9
- if defined?(Spree::Security::Addresses)
10
- include Spree::Security::Addresses
11
- end
12
9
 
13
10
  serialize :preferences, type: Hash, coder: YAML, default: {}
14
11
 
@@ -32,6 +29,10 @@ module Spree
32
29
  EXCLUDED_KEYS_FOR_COMPARISON = %w(id updated_at created_at deleted_at label user_id public_metadata private_metadata)
33
30
  FIELDS_TO_NORMALIZE = %w(firstname lastname phone alternative_phone company address1 address2 city zipcode)
34
31
 
32
+ if defined?(Spree::Security::Addresses)
33
+ include Spree::Security::Addresses
34
+ end
35
+
35
36
  scope :not_deleted, -> { where(deleted_at: nil) }
36
37
 
37
38
  scope :by_state_name_or_abbr, lambda { |state_name|
@@ -64,6 +65,7 @@ module Spree
64
65
  end
65
66
 
66
67
  validate :state_validate, :postal_code_validate
68
+ validate :address_validators, on: [:create, :update]
67
69
 
68
70
  validates :label, uniqueness: { conditions: -> { where(deleted_at: nil) },
69
71
  scope: :user_id,
@@ -71,6 +73,12 @@ module Spree
71
73
  allow_blank: true,
72
74
  allow_nil: true }
73
75
 
76
+ def address_validators
77
+ Rails.application.config.spree.validators.addresses.each do |validator|
78
+ validates_with validator
79
+ end
80
+ end
81
+
74
82
  delegate :name, :iso3, :iso, :iso_name, to: :country, prefix: true
75
83
  delegate :abbr, to: :state, prefix: true, allow_nil: true
76
84
 
@@ -2,6 +2,8 @@ module Spree
2
2
  class CreditCard < Spree.base_class
3
3
  include ActiveMerchant::Billing::CreditCardMethods
4
4
  include Spree::Metadata
5
+ include Spree::PaymentSourceConcern
6
+
5
7
  if defined?(Spree::Webhooks::HasWebhooks)
6
8
  include Spree::Webhooks::HasWebhooks
7
9
  end
@@ -135,30 +137,6 @@ module Spree
135
137
  brand.present? ? brand.upcase : Spree.t(:no_cc_type)
136
138
  end
137
139
 
138
- def actions
139
- %w{capture void credit}
140
- end
141
-
142
- # Indicates whether its possible to capture the payment
143
- def can_capture?(payment)
144
- payment.pending? || payment.checkout?
145
- end
146
-
147
- # Indicates whether its possible to void the payment.
148
- def can_void?(payment)
149
- !payment.failed? && !payment.void?
150
- end
151
-
152
- # Indicates whether its possible to credit the payment. Note that most gateways require that the
153
- # payment be settled first which generally happens within 12-24 hours of the transaction.
154
- def can_credit?(payment)
155
- payment.completed? && payment.credit_allowed > 0
156
- end
157
-
158
- def has_payment_profile?
159
- gateway_customer_profile_id.present? || gateway_payment_profile_id.present?
160
- end
161
-
162
140
  # ActiveMerchant needs first_name/last_name because we pass it a Spree::CreditCard and it calls those methods on it.
163
141
  # Looking at the ActiveMerchant source code we should probably be calling #to_active_merchant before passing
164
142
  # the object to ActiveMerchant but this should do for now.
@@ -0,0 +1,33 @@
1
+ module Spree
2
+ class Gateway::CustomPaymentSourceMethod < Gateway
3
+ def provider_class
4
+ self.class
5
+ end
6
+
7
+ def payment_source_class
8
+ Spree::PaymentSource
9
+ end
10
+
11
+ def payment_profiles_supported?
12
+ true
13
+ end
14
+
15
+ def show_in_admin?
16
+ false
17
+ end
18
+
19
+ def create_profile(payment)
20
+ return if payment.source.gateway_customer.present?
21
+
22
+ user = payment.source.user || payment.order.user
23
+ return if user.blank?
24
+
25
+ find_or_create_customer(user)
26
+ end
27
+
28
+ # simulate a 3rd party payment gateway api to fetch/or create a customer
29
+ def find_or_create_customer(user)
30
+ gateway_customers.find_or_create_by!(user: user, profile_id: "CUSTOMER-#{user.id}")
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,162 @@
1
+ module Spree
2
+ class GiftCard < Spree.base_class
3
+ extend DisplayMoney
4
+ include Spree::SingleStoreResource
5
+ include Spree::Security::GiftCards if defined?(Spree::Security::GiftCards)
6
+
7
+ #
8
+ # State machine
9
+ #
10
+ state_machine :state, initial: :active do
11
+ event :cancel do
12
+ transition active: :canceled
13
+ end
14
+
15
+ event :redeem do
16
+ transition active: :redeemed
17
+ end
18
+ after_transition to: :redeemed, do: :after_redeem
19
+
20
+ event :partial_redeem do
21
+ transition active: :partially_redeemed
22
+ end
23
+ end
24
+
25
+ #
26
+ # Validations
27
+ #
28
+ validates :code, presence: true, uniqueness: { scope: :store_id }
29
+ validates :store, :currency, presence: true
30
+ validates :amount, presence: true, numericality: { greater_than: 0 }
31
+ validates :amount_used, :amount_authorized, presence: true, numericality: { greater_than_or_equal_to: 0 }
32
+
33
+ #
34
+ # Associations
35
+ #
36
+ belongs_to :store, class_name: 'Spree::Store'
37
+ belongs_to :user, class_name: Spree.user_class.to_s, optional: true
38
+ belongs_to :created_by, class_name: Spree.admin_user_class.to_s, optional: true
39
+ belongs_to :batch, class_name: 'Spree::GiftCardBatch', optional: true, foreign_key: :gift_card_batch_id
40
+
41
+ has_many :store_credits, class_name: 'Spree::StoreCredit', as: :originator
42
+ has_many :orders, inverse_of: :gift_card, class_name: 'Spree::Order'
43
+ has_many :users, through: :orders, class_name: Spree.user_class.to_s
44
+
45
+ #
46
+ # Scopes
47
+ #
48
+ scope :active, -> { where(state: [:active, :partially_redeemed]).where(expires_at: [nil, Date.tomorrow..]) }
49
+ scope :expired, -> { where(state: :active).where(expires_at: ..Date.current) }
50
+ scope :redeemed, -> { where(state: [:redeemed]) }
51
+ scope :partially_redeemed, -> { where(state: [:partially_redeemed]) }
52
+
53
+ #
54
+ # Ransack
55
+ #
56
+ self.whitelisted_ransackable_attributes = %w[code user_id]
57
+ self.whitelisted_ransackable_associations = %w[users orders batch]
58
+
59
+ auto_strip_attributes :code
60
+
61
+ #
62
+ # Callbacks
63
+ #
64
+ before_validation :generate_code
65
+ before_validation :normalize_code
66
+ before_validation :set_currency
67
+ before_destroy :ensure_can_be_deleted
68
+
69
+ #
70
+ # Money
71
+ #
72
+ money_methods :amount, :amount_used, :amount_authorized, :amount_remaining
73
+
74
+ # Sets the amount
75
+ # @param amount [String]
76
+ def amount=(amount)
77
+ self[:amount] = Spree::LocalizedNumber.parse(amount)
78
+ end
79
+
80
+ # Calculates the remaining amount
81
+ # @return [Decimal]
82
+ def amount_remaining
83
+ amount - amount_used - amount_authorized
84
+ end
85
+
86
+ delegate :email, to: :user, prefix: true, allow_nil: true
87
+
88
+ def self.json_api_columns
89
+ %w[code amount expires_at]
90
+ end
91
+
92
+ # Checks if the gift card is editable
93
+ # @return [Boolean]
94
+ def editable?
95
+ active?
96
+ end
97
+
98
+ # Checks if the gift card can be deleted
99
+ # @return [Boolean]
100
+ def can_be_deleted?
101
+ !redeemed? && !partially_redeemed?
102
+ end
103
+
104
+ # Displays the code in uppercase, eg. ABC1234
105
+ # @return [String]
106
+ def display_code
107
+ code.upcase
108
+ end
109
+
110
+ # Checks if the gift card is expired
111
+ # @return [Boolean]
112
+ def expired?
113
+ !redeemed? && expires_at.present? && expires_at <= Date.current
114
+ end
115
+
116
+ # Checks if the gift card is active, i.e. not expired and not redeemed
117
+ # @return [Boolean]
118
+ def active?
119
+ super && !expired?
120
+ end
121
+
122
+ # Displays state as expired if the gift card is expired, otherwise displays the state
123
+ # @return [String]
124
+ def display_state
125
+ if expired?
126
+ :expired
127
+ else
128
+ state
129
+ end.to_s
130
+ end
131
+
132
+ private
133
+
134
+ def generate_code
135
+ return if code.present?
136
+
137
+ self.code = loop do
138
+ random_token = SecureRandom.hex(8).downcase
139
+ break random_token unless self.class.exists?(code: random_token, store_id: store_id)
140
+ end
141
+ end
142
+
143
+ def normalize_code
144
+ self.code = code.downcase if code.present?
145
+ end
146
+
147
+ def after_redeem
148
+ update!(redeemed_at: Time.current)
149
+ end
150
+
151
+ def ensure_can_be_deleted
152
+ return if can_be_deleted?
153
+
154
+ errors.add(:base, :cannot_destroy_used_gift_card)
155
+ throw(:abort)
156
+ end
157
+
158
+ def set_currency
159
+ self.currency ||= store&.default_currency
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,79 @@
1
+ module Spree
2
+ class GiftCardBatch < Spree.base_class
3
+ extend DisplayMoney
4
+ include Spree::SingleStoreResource
5
+
6
+ #
7
+ # Associations
8
+ #
9
+ belongs_to :store, class_name: 'Spree::Store'
10
+ belongs_to :created_by, class_name: Spree.admin_user_class.to_s, optional: true
11
+ has_many :gift_cards, class_name: 'Spree::GiftCard', inverse_of: :batch, dependent: :destroy
12
+
13
+
14
+ #
15
+ # Validations
16
+ #
17
+ validates :codes_count, :amount, :prefix, presence: true
18
+ validates :codes_count, numericality: { greater_than: 0, less_than_or_equal_to: Spree::Config[:gift_card_batch_limit].to_i }
19
+ validates :store, :currency, presence: true
20
+ validates :amount, numericality: { greater_than: 0 }
21
+
22
+ #
23
+ # Callbacks
24
+ #
25
+ before_validation :set_currency
26
+ after_create :generate_gift_cards
27
+
28
+ auto_strip_attributes :prefix
29
+
30
+ money_methods :amount
31
+
32
+ self.whitelisted_ransackable_attributes = %w[prefix]
33
+
34
+ def generate_gift_cards
35
+ if codes_count < Spree::Config[:gift_card_batch_web_limit].to_i
36
+ create_gift_cards
37
+ else
38
+ Spree::GiftCards::BulkGenerateJob.perform_later(id)
39
+ end
40
+ end
41
+
42
+ def create_gift_cards
43
+ @gift_cards_to_insert = []
44
+
45
+ Spree::GiftCard.transaction do
46
+ (codes_count - gift_cards.count).times do
47
+ @gift_cards_to_insert << gift_card_hash
48
+ end
49
+ Spree::GiftCard.insert_all @gift_cards_to_insert if @gift_cards_to_insert.any?
50
+ end
51
+ end
52
+
53
+ def generate_code
54
+ loop do
55
+ code = "#{prefix.downcase}#{SecureRandom.hex(3).downcase}"
56
+ break code unless Spree::GiftCard.exists?(code: code) || @gift_cards_to_insert.detect { |gc| gc[:code] == code }
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def gift_card_hash
63
+ {
64
+ state: :active,
65
+ gift_card_batch_id: id,
66
+ amount: amount,
67
+ currency: currency,
68
+ code: generate_code,
69
+ store_id: store_id,
70
+ created_by_id: created_by_id,
71
+ expires_at: expires_at
72
+ }
73
+ end
74
+
75
+ def set_currency
76
+ self.currency ||= store&.default_currency
77
+ end
78
+ end
79
+ end
@@ -1,5 +1,6 @@
1
1
  module Spree
2
2
  class InventoryUnit < Spree.base_class
3
+ extend Spree::DisplayMoney
3
4
  if defined?(Spree::Webhooks::HasWebhooks)
4
5
  include Spree::Webhooks::HasWebhooks
5
6
  end
@@ -30,6 +31,8 @@ module Spree
30
31
 
31
32
  validates :quantity, numericality: { greater_than: 0 }
32
33
 
34
+ money_methods :charged_amount
35
+
33
36
  # state machine (see http://github.com/pluginaweek/state_machine/tree/master for details)
34
37
  state_machine initial: :on_hand do
35
38
  event :fill_backorder do
@@ -113,6 +116,14 @@ module Spree
113
116
  original_return_item_id?
114
117
  end
115
118
 
119
+ def charged_amount
120
+ percentage_of_line_item * line_item.pre_tax_amount
121
+ end
122
+
123
+ def percentage_of_line_item
124
+ quantity / BigDecimal(line_item.quantity)
125
+ end
126
+
116
127
  private
117
128
 
118
129
  def allow_ship?
@@ -124,10 +135,6 @@ module Spree
124
135
  order.fulfill!
125
136
  end
126
137
 
127
- def percentage_of_line_item
128
- quantity / BigDecimal(line_item.quantity)
129
- end
130
-
131
138
  def current_return_item
132
139
  return_items.not_cancelled.first
133
140
  end