spree_core 5.4.0.beta2 → 5.4.0.beta3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: af6f55a25483bfb02dab0b15f67356a2732cbeb190074b9ddb75a1692594fb7a
4
- data.tar.gz: 8f0b8f77983432ef1e1baa52def6ed2674f9e0794bc632dd91757551c4ee71c4
3
+ metadata.gz: ea9fcfc9e4ac5937296c789f151d5d431b60a2fadb131e5a7bca034cae47b2ab
4
+ data.tar.gz: e89e3fd21bf14b0bb0bda0de42c0ecc49d173add3d9ce58c29b3c781eafb55fa
5
5
  SHA512:
6
- metadata.gz: 46c6fc812dc90ba1ae3061b2e4066df5e9dd8ad530fde2db85095a7b5cb444fec2d51f4736a528c61ee4423e64d986478fdcf5478c40888e99818b849e444d60
7
- data.tar.gz: aa2913c4e9882dd0e77d03ddcfae4f8277e4657b807080ce2d3549fd63f5c6da18448e2574b7742ad82b227a299dccfce3717a7df504be4442e3c7f0bf21629e
6
+ metadata.gz: b9bd748be9f9b4949d9b71887990fe3ac2377a6f4e7ed3d3060b0be073d6c199bd92a633bb2c62e7b8ee13dc0d7dae07e1bedd3d4d9dc5edebf3e4fc3839cae7
7
+ data.tar.gz: 8f8639ca080722372b01f91df71e54e0dc3f8f39d82cdbd1b3ad72103d995dbaf9bc50ef0f2a6032b0d400c2e94387edd2e06f64a52f3932391ed49ee5650014
@@ -22,14 +22,8 @@ module Spree
22
22
  @in_stock = params.dig(:filter, :in_stock)
23
23
  @backorderable = params.dig(:filter, :backorderable)
24
24
  @purchasable = params.dig(:filter, :purchasable)
25
- @out_of_stock = params.dig(:filter, :out_of_stock).to_b
26
25
  @tags = params.dig(:filter, :tags).to_s.split(',').compact_blank
27
26
  @vendor_ids = params.dig(:filter, :vendor_ids)&.split(',')&.compact_blank || []
28
-
29
- if @purchasable.present? && @out_of_stock.present?
30
- @purchasable = false
31
- @out_of_stock = false
32
- end
33
27
  end
34
28
 
35
29
  def execute
@@ -50,7 +44,6 @@ module Spree
50
44
  products = show_only_stock(products)
51
45
  products = show_only_backorderable(products)
52
46
  products = show_only_purchasable(products)
53
- products = show_only_out_of_stock(products)
54
47
  products = by_taxonomies(products)
55
48
  products = ordered(products)
56
49
  products = by_vendor_ids(products)
@@ -62,7 +55,7 @@ module Spree
62
55
 
63
56
  attr_reader :ids, :skus, :price, :currency, :taxons, :concat_taxons, :name, :options, :option_value_ids, :scope,
64
57
  :sort_by, :deleted, :discontinued, :store, :in_stock, :backorderable, :purchasable, :tags,
65
- :query, :vendor_ids, :out_of_stock, :slug, :taxonomies
58
+ :query, :vendor_ids, :slug, :taxonomies
66
59
 
67
60
  def query?
68
61
  query.present?
@@ -294,12 +287,6 @@ module Spree
294
287
  products.in_stock_or_backorderable
295
288
  end
296
289
 
297
- def show_only_out_of_stock(products)
298
- return products unless out_of_stock.present?
299
-
300
- products.out_of_stock
301
- end
302
-
303
290
  def map_prices(prices)
304
291
  prices.map do |price|
305
292
  price == 'Infinity' ? BigDecimal::INFINITY : price.to_f
@@ -81,9 +81,26 @@ module Spree
81
81
  where(Price.table_name => { amount: price.. })
82
82
  end
83
83
 
84
- add_search_scope :in_stock do
85
- joins(:variants_including_master).merge(Spree::Variant.in_stock)
84
+ # Can't use add_search_scope for this as it needs a default argument
85
+ # Ransack calls with '1' to activate, '0' or nil to skip
86
+ # In Ruby code: in_stock(true) for in-stock, in_stock(false) for out-of-stock
87
+ def self.in_stock(in_stock = true)
88
+ if in_stock == '0' || !in_stock
89
+ all
90
+ else
91
+ joins(:variants_including_master).merge(Spree::Variant.in_stock_or_backorderable)
92
+ end
93
+ end
94
+ search_scopes << :in_stock
95
+
96
+ def self.out_of_stock(out_of_stock = true)
97
+ if out_of_stock == '0' || !out_of_stock
98
+ all
99
+ else
100
+ where.not(id: joins(:variants_including_master).merge(Spree::Variant.in_stock_or_backorderable))
101
+ end
86
102
  end
103
+ search_scopes << :out_of_stock
87
104
 
88
105
  add_search_scope :backorderable do
89
106
  joins(:variants_including_master).merge(Spree::Variant.backorderable)
@@ -6,11 +6,14 @@ module Spree
6
6
  PREFIXES = { 'publishable' => 'pk_', 'secret' => 'sk_' }.freeze
7
7
  TOKEN_LENGTH = 24
8
8
 
9
- # @!attribute [r] plaintext_token
10
- # The raw token value, only available in memory immediately after creation
11
- # of a secret key. Not persisted to the database.
12
- # @return [String, nil]
13
- attr_reader :plaintext_token
9
+ # Returns the raw token value. For publishable keys this is the persisted
10
+ # +token+ column. For secret keys it is only available in memory immediately
11
+ # after creation (not persisted).
12
+ #
13
+ # @return [String, nil]
14
+ def plaintext_token
15
+ publishable? ? token : @plaintext_token
16
+ end
14
17
 
15
18
  belongs_to :store, class_name: 'Spree::Store'
16
19
  belongs_to :created_by, polymorphic: true, optional: true
@@ -44,8 +44,7 @@ module Spree
44
44
  joins(:market_countries)
45
45
  .where(store_id: store.id)
46
46
  .where(spree_market_countries: { country_id: country.id })
47
- .order(:position)
48
- .first
47
+ .take
49
48
  end
50
49
 
51
50
  # Returns the default market for a store, or falls back to the first by position
@@ -8,6 +8,7 @@ module Spree
8
8
  validates :market, :country, presence: true
9
9
  validates :country_id, uniqueness: { scope: :market_id }
10
10
  validate :country_covered_by_shipping_zone
11
+ validate :country_unique_per_store
11
12
 
12
13
  private
13
14
 
@@ -21,5 +22,21 @@ module Spree
21
22
  errors.add(:country, :not_in_shipping_zone)
22
23
  end
23
24
  end
25
+
26
+ def country_unique_per_store
27
+ return if market.blank? || country.blank?
28
+
29
+ store = market.store
30
+ return if store.blank?
31
+
32
+ existing = self.class.joins(:market)
33
+ .where(country_id: country_id)
34
+ .where(spree_markets: { store_id: store.id, deleted_at: nil })
35
+ .where.not(id: id)
36
+
37
+ if existing.exists?
38
+ errors.add(:country, :already_in_market)
39
+ end
40
+ end
24
41
  end
25
42
  end
@@ -1,10 +1,7 @@
1
- require_dependency 'spree/newsletter_subscriber/emails'
2
-
3
1
  module Spree
4
2
  class NewsletterSubscriber < Spree.base_class
5
3
  has_prefix_id :sub
6
4
 
7
- include Spree::NewsletterSubscriber::Emails
8
5
  include Spree::Metafields
9
6
 
10
7
  publishes_lifecycle_events
@@ -3,7 +3,6 @@ require_dependency 'spree/order/currency_updater'
3
3
  require_dependency 'spree/order/digital'
4
4
  require_dependency 'spree/order/payments'
5
5
  require_dependency 'spree/order/store_credit'
6
- require_dependency 'spree/order/emails'
7
6
  require_dependency 'spree/order/gift_card'
8
7
 
9
8
  module Spree
@@ -22,7 +21,6 @@ module Spree
22
21
  include Spree::Order::Payments
23
22
  include Spree::Order::StoreCredit
24
23
  include Spree::Order::AddressBook
25
- include Spree::Order::Emails
26
24
  include Spree::Order::Webhooks
27
25
  include Spree::Core::NumberGenerator.new(prefix: 'R')
28
26
  include Spree::Order::GiftCard
@@ -311,9 +309,9 @@ module Spree
311
309
  # @return [Boolean]
312
310
  def order_refunded?
313
311
  return false if item_count.zero?
312
+ return false if refunds_total.zero?
314
313
 
315
- (payment_state.in?(%w[void failed]) && refunds_total.positive?) ||
316
- refunds_total == total_minus_store_credits - additional_tax_total.abs
314
+ payment_state.in?(%w[void failed]) || refunds_total == total_minus_store_credits - additional_tax_total.abs
317
315
  end
318
316
 
319
317
  def refunds_total
@@ -965,7 +963,6 @@ module Spree
965
963
  payments.store_credits.pending.each(&:void!)
966
964
  end
967
965
 
968
- send_cancel_email
969
966
  update_with_updater!
970
967
  send_order_canceled_webhook
971
968
  publish_order_canceled_event
@@ -197,18 +197,6 @@ module Spree
197
197
  scope :by_source, ->(source) { send(source) }
198
198
  scope :paused, -> { where(status: 'paused') }
199
199
  scope :published, -> { where(status: 'active') }
200
- scope :in_stock_items, -> { joins(:variants).merge(Spree::Variant.in_stock_or_backorderable) }
201
- scope :out_of_stock_items, lambda {
202
- joins(variants_including_master: :stock_items).
203
- where(spree_variants: { track_inventory: true }).
204
- where.not(id: Spree::Variant.where(track_inventory: false).pluck(:product_id).uniq).
205
- where(spree_stock_items: { backorderable: false }).
206
- group(:id).
207
- having("SUM(#{Spree::StockItem.table_name}.count_on_hand) <= 0")
208
- }
209
- scope :out_of_stock, lambda {
210
- joins(:stock_items).where("#{Spree::Variant.table_name}.track_inventory = ? OR #{Spree::StockItem.table_name}.count_on_hand <= ?", false, 0)
211
- }
212
200
 
213
201
  attr_accessor :option_values_hash
214
202
 
@@ -232,7 +220,7 @@ module Spree
232
220
  self.whitelisted_ransackable_associations = %w[taxons stores variants_including_master master variants tags labels
233
221
  shipping_category classifications option_types]
234
222
  self.whitelisted_ransackable_scopes = %w[not_discontinued search_by_name in_taxon price_between
235
- multi_search in_stock_items out_of_stock_items with_option_value_ids
223
+ multi_search in_stock out_of_stock with_option_value_ids
236
224
  ascend_by_price descend_by_price]
237
225
 
238
226
  [
@@ -4,7 +4,6 @@ module Spree
4
4
 
5
5
  include Spree::Core::NumberGenerator.new(prefix: 'RI', length: 9)
6
6
  include Spree::NumberIdentifier
7
- include Spree::Reimbursement::Emails
8
7
 
9
8
  class IncompleteReimbursementError < StandardError; end
10
9
 
@@ -116,7 +115,6 @@ module Spree
116
115
  if unpaid_amount_within_tolerance?
117
116
  reimbursed!
118
117
  reimbursement_success_hooks.each { |h| h.call self }
119
- send_reimbursement_email
120
118
  else
121
119
  errored!
122
120
  reimbursement_failure_hooks.each { |h| h.call self }
@@ -14,7 +14,6 @@ module Spree
14
14
  if defined?(Spree::VendorConcern)
15
15
  include Spree::VendorConcern
16
16
  end
17
- include Spree::Shipment::Emails
18
17
  include Spree::Shipment::Webhooks
19
18
  include Spree::Shipment::CustomEvents
20
19
 
@@ -1,6 +1,5 @@
1
1
  module Spree
2
2
  class ShipmentHandler
3
- include Spree::Shipment::Emails
4
3
  include Spree::IntegrationsConcern
5
4
 
6
5
  class << self
@@ -25,7 +24,6 @@ module Spree
25
24
  @shipment.process_order_payments if Spree::Config[:auto_capture_on_dispatch]
26
25
  @shipment.touch :shipped_at
27
26
  update_order_shipment_state
28
- send_shipped_email
29
27
  end
30
28
 
31
29
  protected
@@ -19,8 +19,8 @@ module Spree
19
19
  end
20
20
  end
21
21
 
22
- # deliver confirmation email after the transaction is completed
23
- subscriber.deliver_newsletter_email_verification unless subscriber.verified?
22
+ # publish event to trigger email delivery via subscriber
23
+ subscriber.publish_event('newsletter_subscriber.subscribed') unless subscriber.verified?
24
24
  subscriber
25
25
  end
26
26
 
@@ -34,7 +34,7 @@ module Spree
34
34
 
35
35
  def find_invitation(event)
36
36
  invitation_id = event.payload['id']
37
- Spree::Invitation.find_by(id: invitation_id)
37
+ Spree::Invitation.find_by_prefix_id(invitation_id)
38
38
  end
39
39
  end
40
40
  end
@@ -292,6 +292,7 @@ en:
292
292
  spree/market_country:
293
293
  attributes:
294
294
  country:
295
+ already_in_market: is already assigned to another market in this store
295
296
  not_in_shipping_zone: is not covered by any shipping zone. Please set up a shipping zone first.
296
297
  spree/payment:
297
298
  attributes:
@@ -1,5 +1,5 @@
1
1
  module Spree
2
- VERSION = '5.4.0.beta2'.freeze
2
+ VERSION = '5.4.0.beta3'.freeze
3
3
 
4
4
  def self.version
5
5
  VERSION
data/lib/spree/core.rb CHANGED
@@ -427,7 +427,6 @@ require 'spree/events'
427
427
  require 'spree/webhooks'
428
428
 
429
429
  require 'spree/core/partials'
430
- require 'spree/core/importer'
431
430
  require 'spree/core/controller_helpers/auth'
432
431
  require 'spree/core/controller_helpers/common'
433
432
  require 'spree/core/controller_helpers/order'
@@ -0,0 +1,50 @@
1
+ namespace :spree do
2
+ namespace :cli do
3
+ desc 'Ensure a default publishable API key exists and print its token'
4
+ task ensure_api_key: :environment do
5
+ store = Spree::Store.default
6
+ key = store.api_keys.active.publishable.first ||
7
+ store.api_keys.create!(name: 'Default', key_type: 'publishable')
8
+ print key.plaintext_token
9
+ end
10
+
11
+ desc 'Create an API key'
12
+ task create_api_key: :environment do
13
+ name = ENV.fetch('NAME')
14
+ key_type = ENV.fetch('KEY_TYPE')
15
+ store = Spree::Store.default
16
+ key = store.api_keys.create!(name: name, key_type: key_type)
17
+ print key.plaintext_token
18
+ end
19
+
20
+ desc 'List API keys (pipe-delimited)'
21
+ task list_api_keys: :environment do
22
+ Spree::Store.default.api_keys.order(created_at: :desc).each do |k|
23
+ status = k.revoked_at ? 'revoked' : 'active'
24
+ token = k.secret? ? k.token_prefix : k.token
25
+ puts [k.prefixed_id, k.name, k.key_type, token, k.created_at.strftime('%Y-%m-%d %H:%M'), status].join('|')
26
+ end
27
+ end
28
+
29
+ desc 'Revoke an API key by prefixed ID'
30
+ task revoke_api_key: :environment do
31
+ id = ENV.fetch('ID')
32
+ key = Spree::Store.default.api_keys.find_by_prefix_id!(id)
33
+ key.revoke!
34
+ print key.name
35
+ end
36
+
37
+ desc 'Create an admin user'
38
+ task create_admin: :environment do
39
+ email = ENV.fetch('EMAIL')
40
+ password = ENV.fetch('PASSWORD')
41
+ admin = Spree.admin_user_class.create!(
42
+ email: email,
43
+ password: password,
44
+ password_confirmation: password
45
+ )
46
+ admin.add_role('admin', Spree::Store.default)
47
+ print admin.email
48
+ end
49
+ end
50
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_core
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.4.0.beta2
4
+ version: 5.4.0.beta3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Schofield
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2026-02-28 00:00:00.000000000 Z
13
+ date: 2026-03-02 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: i18n-tasks
@@ -859,7 +859,6 @@ files:
859
859
  - app/models/concerns/spree/default_price.rb
860
860
  - app/models/concerns/spree/display_money.rb
861
861
  - app/models/concerns/spree/display_on.rb
862
- - app/models/concerns/spree/filter_param.rb
863
862
  - app/models/concerns/spree/has_image_alt_text.rb
864
863
  - app/models/concerns/spree/image_methods.rb
865
864
  - app/models/concerns/spree/integrations_concern.rb
@@ -977,7 +976,6 @@ files:
977
976
  - app/models/spree/metafields/rich_text.rb
978
977
  - app/models/spree/metafields/short_text.rb
979
978
  - app/models/spree/newsletter_subscriber.rb
980
- - app/models/spree/newsletter_subscriber/emails.rb
981
979
  - app/models/spree/option_type.rb
982
980
  - app/models/spree/option_type_prototype.rb
983
981
  - app/models/spree/option_value.rb
@@ -987,7 +985,6 @@ files:
987
985
  - app/models/spree/order/checkout.rb
988
986
  - app/models/spree/order/currency_updater.rb
989
987
  - app/models/spree/order/digital.rb
990
- - app/models/spree/order/emails.rb
991
988
  - app/models/spree/order/gift_card.rb
992
989
  - app/models/spree/order/payments.rb
993
990
  - app/models/spree/order/store_credit.rb
@@ -1075,7 +1072,6 @@ files:
1075
1072
  - app/models/spree/refund_reason.rb
1076
1073
  - app/models/spree/reimbursement.rb
1077
1074
  - app/models/spree/reimbursement/credit.rb
1078
- - app/models/spree/reimbursement/emails.rb
1079
1075
  - app/models/spree/reimbursement/reimbursement_type_engine.rb
1080
1076
  - app/models/spree/reimbursement/reimbursement_type_validator.rb
1081
1077
  - app/models/spree/reimbursement_performer.rb
@@ -1109,7 +1105,6 @@ files:
1109
1105
  - app/models/spree/role_user.rb
1110
1106
  - app/models/spree/shipment.rb
1111
1107
  - app/models/spree/shipment/custom_events.rb
1112
- - app/models/spree/shipment/emails.rb
1113
1108
  - app/models/spree/shipment/webhooks.rb
1114
1109
  - app/models/spree/shipment_handler.rb
1115
1110
  - app/models/spree/shipping_calculator.rb
@@ -1453,8 +1448,6 @@ files:
1453
1448
  - lib/generators/spree/authentication/dummy/dummy_generator.rb
1454
1449
  - lib/generators/spree/authentication/dummy/templates/authentication_helpers.rb.tt
1455
1450
  - lib/generators/spree/authentication/dummy/templates/create_spree_admin_users.rb.tt
1456
- - lib/generators/spree/cursor_rules/cursor_rules_generator.rb
1457
- - lib/generators/spree/cursor_rules/templates/spree_rules.mdc
1458
1451
  - lib/generators/spree/dummy/dummy_generator.rb
1459
1452
  - lib/generators/spree/dummy/templates/app/assets/config/manifest.js
1460
1453
  - lib/generators/spree/dummy/templates/rails/application.rb
@@ -1486,9 +1479,6 @@ files:
1486
1479
  - lib/spree/core/dependencies.rb
1487
1480
  - lib/spree/core/dependencies_helper.rb
1488
1481
  - lib/spree/core/engine.rb
1489
- - lib/spree/core/importer.rb
1490
- - lib/spree/core/importer/order.rb
1491
- - lib/spree/core/importer/product.rb
1492
1482
  - lib/spree/core/number_generator.rb
1493
1483
  - lib/spree/core/partials.rb
1494
1484
  - lib/spree/core/permission_configuration.rb
@@ -1634,6 +1624,7 @@ files:
1634
1624
  - lib/spree/translation_migrations.rb
1635
1625
  - lib/spree/webhooks.rb
1636
1626
  - lib/spree_core.rb
1627
+ - lib/tasks/cli.rake
1637
1628
  - lib/tasks/core.rake
1638
1629
  - lib/tasks/dependencies.rake
1639
1630
  - lib/tasks/images.rake
@@ -1662,9 +1653,9 @@ licenses:
1662
1653
  - BSD-3-Clause
1663
1654
  metadata:
1664
1655
  bug_tracker_uri: https://github.com/spree/spree/issues
1665
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.0.beta2
1656
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.0.beta3
1666
1657
  documentation_uri: https://docs.spreecommerce.org/
1667
- source_code_uri: https://github.com/spree/spree/tree/v5.4.0.beta2
1658
+ source_code_uri: https://github.com/spree/spree/tree/v5.4.0.beta3
1668
1659
  post_install_message:
1669
1660
  rdoc_options: []
1670
1661
  require_paths:
@@ -1,21 +0,0 @@
1
- module Spree
2
- module FilterParam
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- before_save :set_filter_param
7
- end
8
-
9
- protected
10
-
11
- def set_filter_param
12
- return if param_candidate.blank?
13
-
14
- self.filter_param = param_candidate.parameterize
15
- end
16
-
17
- def param_candidate
18
- name
19
- end
20
- end
21
- end
@@ -1,12 +0,0 @@
1
- module Spree
2
- class NewsletterSubscriber < Spree.base_class
3
- module Emails
4
- extend ActiveSupport::Concern
5
-
6
- def deliver_newsletter_email_verification
7
- # you can overwrite this method in your application / extension to send out the confirmation email
8
- # or use `spree_emails` gem
9
- end
10
- end
11
- end
12
- end
@@ -1,24 +0,0 @@
1
- module Spree
2
- class Order < Spree.base_class
3
- module Emails
4
- extend ActiveSupport::Concern
5
-
6
- def deliver_order_confirmation_email
7
- Spree::Deprecation.warn('Spree::Order#deliver_order_confirmation_email is deprecated and will be removed in Spree 5.5. Please create a Subscriber for order.completed event.')
8
- end
9
-
10
- # If you would like to also send confirmation email to store owner(s)
11
- def deliver_store_owner_order_notification_email?
12
- false
13
- end
14
-
15
- def deliver_store_owner_order_notification_email
16
- Spree::Deprecation.warn('Spree::Order#deliver_store_owner_order_notification_email is deprecated and will be removed in Spree 5.5. Please create a Subscriber for order.completed event.')
17
- end
18
-
19
- def send_cancel_email
20
- Spree::Deprecation.warn('Spree::Order#send_cancel_email is deprecated and will be removed in Spree 5.5. Please create a Subscriber for order.canceled event.')
21
- end
22
- end
23
- end
24
- end
@@ -1,11 +0,0 @@
1
- module Spree
2
- class Reimbursement < Spree.base_class
3
- module Emails
4
- def send_reimbursement_email
5
- # you can overwrite this method in your application / extension to send out the confirmation email
6
- # or use `spree_emails` gem
7
- # YourEmailVendor.deliver_reimbursement_email(id) # `id` = ID of the Reimbursement being sent, you can also pass the entire object using `self`
8
- end
9
- end
10
- end
11
- end
@@ -1,12 +0,0 @@
1
- module Spree
2
- class Shipment < Spree.base_class
3
- module Emails
4
- def send_shipped_email
5
- Spree::Deprecation.warn("Shipment#send_shipped_email is deprecated and will be removed in Spree 5.5. Please use events")
6
- # you can overwrite this method in your application / extension to send out the confirmation email
7
- # or use `spree_emails` gem
8
- # YourEmailVendor.deliver_shipment_notification_email(@shipment.id)
9
- end
10
- end
11
- end
12
- end
@@ -1,19 +0,0 @@
1
- module Spree
2
- class CursorRulesGenerator < Rails::Generators::Base
3
- desc 'Set up Cursor Rules - copies all Spree cursor rules to .cursor/rules directory'
4
-
5
- def self.source_paths
6
- paths = superclass.source_paths
7
- paths << File.expand_path('templates', __dir__)
8
- paths.flatten
9
- end
10
-
11
- def create_cursor_rules_directory
12
- empty_directory '.cursor/rules'
13
- end
14
-
15
- def copy_cursor_rules
16
- copy_file 'spree_rules.mdc', '.cursor/rules/spree_rules.mdc'
17
- end
18
- end
19
- end
@@ -1,385 +0,0 @@
1
- ---
2
- alwaysApply: true
3
- ---
4
-
5
- # Cursor Rules for Spree Commerce Development
6
-
7
- ## General Development Guidelines
8
-
9
- ### Framework & Architecture
10
-
11
- - Spree is built on Ruby on Rails and follows MVC architecture
12
- - All Spree code must be namespaced under `Spree::` module
13
- - Spree is distributed as Rails engines with separate gems (core, admin, api, storefront, emails, etc.)
14
- - Follow Rails conventions and the Rails Security Guide
15
- - Prefer Rails idioms and standard patterns over custom solutions
16
-
17
- ### Code Organization
18
-
19
- - Place all models in `app/models/spree/` directory
20
- - Place all controllers in `app/controllers/spree/` directory
21
- - Place all views in `app/views/spree/` directory
22
- - Place all services in `app/services/spree/` directory
23
- - Place all mailers in `app/mailers/spree/` directory
24
- - Place all API serializers in `app/serializers/spree/` directory
25
- - Place all helpers in `app/helpers/spree/` directory
26
- - Place all jobs in `app/jobs/spree/` directory
27
- - Place all presenters in `app/presenters/spree/` directory
28
- - Use consistent file naming: `spree/product.rb` for `Spree::Product` class
29
- - Group related functionality into concerns when appropriate
30
- - Do not call `Spree::User` directly, use `Spree.user_class` instead
31
- - Do not call `Spree::AdminUser` directly, use `Spree.admin_user_class` instead
32
-
33
- ## Naming Conventions & Structure
34
-
35
- ### Classes & Modules
36
-
37
- ```ruby
38
- # ✅ Correct naming
39
- module Spree
40
- class Product < Spree.base_class
41
- end
42
- end
43
-
44
- module Spree
45
- module Admin
46
- class ProductsController < ResourceController
47
- end
48
- end
49
- end
50
-
51
- # ❌ Incorrect - missing namespace
52
- class Product < ApplicationRecord
53
- end
54
- ```
55
-
56
- Always inherit from `Spree.base_class` when creating models.
57
-
58
- ### File Paths
59
-
60
- - Models: `app/models/spree/product.rb`
61
- - Controllers: `app/controllers/spree/admin/products_controller.rb`
62
- - Views: `app/views/spree/admin/products/`
63
- - Decorators: `app/models/spree/product_decorator.rb`
64
-
65
- ## Model Development
66
-
67
- ### Model Patterns
68
-
69
- - Use ActiveRecord associations appropriately, always pass `class_name` and `dependent` options
70
- - Implement concerns for shared functionality
71
- - Use scopes for reusable query patterns
72
- - Include `Spree::Metafields` concern for models that need metadata support
73
-
74
- ```ruby
75
- # ✅ Good model structure
76
- class Spree::Product < Spree.base_class
77
- include Spree::Metafields
78
-
79
- has_many :variants, class_name: 'Spree::Variant', dependent: :destroy
80
-
81
- scope :available, -> { where(available_on: ..Time.current) }
82
-
83
- validates :name, presence: true
84
- validates :slug, presence: true, uniqueness: { scope: spree_base_uniqueness_scope }
85
- end
86
- ```
87
-
88
- For uniqueness validation, always use `scope: spree_base_uniqueness_scope`
89
-
90
- ## Controller Development
91
-
92
- ### Controller Inheritance
93
-
94
- - Admin controllers inherit from `Spree::Admin::ResourceController` which handles most of CRUD operations
95
- - API controllers inherit from `Spree::Api::V2::BaseController`
96
- - Storefront controllers inherit from `Spree::StoreController`
97
-
98
- ### Parameter Handling
99
-
100
- - Always use strong parameters
101
-
102
- ```ruby
103
- # ✅ Proper parameter handling
104
- def permitted_product_params
105
- params.require(:product).permit(:name, :description, :price)
106
- end
107
- ```
108
-
109
- ## Customization & Extensions
110
-
111
- ### Spree::Dependencies System (Preferred Method)
112
-
113
- Dependencies allow you to replace parts of Spree core with custom implementations. This is the preferred method for customization.
114
-
115
- #### Global Customization
116
-
117
- In `config/initializers/spree.rb`:
118
-
119
- ```ruby
120
- # Single service replacement
121
- Spree::Dependencies.cart_add_item_service = 'MyAddToCartService'
122
-
123
- # Or using block syntax
124
- Spree.dependencies do |dependencies|
125
- dependencies.cart_add_item_service = 'MyAddToCartService'
126
- dependencies.checkout_complete_service = 'MyCheckoutCompleteService'
127
- end
128
- ```
129
-
130
- #### API Level Customization
131
-
132
- ```ruby
133
- # Storefront API specific
134
- Spree::Api::Dependencies.storefront_cart_serializer = 'MyCartSerializer'
135
- Spree::Api::Dependencies.storefront_cart_add_item_service = 'MyAddToCartService'
136
-
137
- # Platform API specific
138
- Spree::Api::Dependencies.platform_product_serializer = 'MyProductSerializer'
139
- ```
140
-
141
- #### Service Implementation
142
-
143
- ```ruby
144
- # ✅ Proper service inheritance
145
- class MyAddToCartService < Spree::Cart::AddItem
146
- def call(order:, variant:, quantity: nil, public_metadata: {}, private_metadata: {}, options: {})
147
- ApplicationRecord.transaction do
148
- run :add_to_line_item
149
- run Spree.cart_recalculate_service
150
- run :update_external_system
151
- end
152
- end
153
-
154
- private
155
-
156
- def update_external_system(result)
157
- # Custom logic here
158
- end
159
- end
160
- ```
161
-
162
- #### Available Injection Points
163
-
164
- Common dependencies you can override:
165
- - Cart services: `cart_add_item_service`, `cart_update_service`, `cart_remove_item_service`
166
- - Checkout services: `checkout_next_service`, `checkout_complete_service`
167
- - Order services: `order_approve_service`, `order_cancel_service`
168
- - Payment services: `payment_create_service`, `payment_process_service`
169
- - Ability classes: `ability_class`
170
- - Serializers: Various API serializers for different endpoints
171
-
172
- ### Decorators (Use Sparingly)
173
-
174
- Decorators should be a last resort - they make upgrades difficult. Use `Module.prepend` pattern for decorators.
175
-
176
- #### Model Decorators
177
-
178
- ```ruby
179
- # ✅ Proper decorator structure
180
- module Spree
181
- module ProductDecorator
182
- def self.prepended(base)
183
- base.has_many :videos, class_name: 'Spree::Video', dependent: :destroy
184
- base.before_validation :strip_whitespaces
185
- end
186
-
187
- def custom_name
188
- name.upcase
189
- end
190
-
191
- private
192
-
193
- def strip_whitespaces
194
- self.name = name.strip if name.present?
195
- end
196
- end
197
-
198
- Product.prepend(ProductDecorator)
199
- end
200
- ```
201
-
202
- #### Controller Decorators
203
-
204
- ```ruby
205
- # ✅ Controller decorator with dependency injection
206
- module Spree
207
- module Admin
208
- module ProductsControllerDecorator
209
- def self.prepended(base)
210
- base.before_action :load_custom_data, only: [:show, :edit]
211
- end
212
-
213
- def custom_action
214
- # Custom action implementation
215
- end
216
-
217
- private
218
-
219
- def load_custom_data
220
- @custom_data = fetch_custom_data
221
- end
222
- end
223
-
224
- ProductsController.prepend(ProductsControllerDecorator)
225
- end
226
- end
227
- ```
228
-
229
- ### View Customization
230
-
231
- #### Admin Panel Injection Points
232
-
233
- Use partial injection for admin customization:
234
-
235
- ```ruby
236
- # In config/initializers/spree.rb
237
- Rails.application.config.spree_admin.head_partials << 'spree/admin/shared/custom_head'
238
- Rails.application.config.spree_admin.body_end_partials << 'spree/admin/shared/custom_footer'
239
- ```
240
-
241
- Available injection points:
242
- - `head_partials` - Injects into `<head>` tag
243
- - `body_start_partials` - Injects at start of `<body>`
244
- - `body_end_partials` - Injects at end of `<body>`
245
-
246
- #### Storefront Themes
247
-
248
- Create custom themes for storefront customization:
249
-
250
- ```bash
251
- bin/rails g spree:storefront:theme MyTheme
252
- ```
253
-
254
- ```ruby
255
- # Register theme in config/initializers/spree.rb
256
- Spree.page_builder.themes << Spree::Themes::MyTheme
257
- ```
258
-
259
- #### View Overrides
260
-
261
- Override specific views by creating files in your app:
262
-
263
- ```
264
- app/views/themes/my_theme/spree/products/index.html.erb
265
- ```
266
-
267
- Do not override views for admin, only for storefront. Avoid overriding any views for Checkout or Cart.
268
-
269
- ### Authentication Integration
270
-
271
- ```ruby
272
- # In config/initializers/spree.rb
273
- Spree.user_class = 'User'
274
- Spree.admin_user_class = 'AdminUser'
275
-
276
- # Custom authentication
277
- Rails.application.config.to_prepare do
278
- Spree::ApplicationController.include MyAuthenticationModule
279
- end
280
- ```
281
-
282
- ## Testing Guidelines
283
-
284
- ### Test Structure
285
-
286
- - Use RSpec for testing
287
- - Place specs in `spec/` directory following Rails conventions
288
- - Use Spree's testing helpers and factories
289
-
290
- ```ruby
291
- # ✅ Proper test structure
292
- require 'spec_helper'
293
-
294
- RSpec.describe Spree::Product, type: :model do
295
- let(:product) { create(:product) }
296
-
297
- describe '#available?' do
298
- it 'returns true when product is available' do
299
- expect(product.available?).to be true
300
- end
301
- end
302
- end
303
- ```
304
-
305
- ### Factory Usage
306
-
307
- ```ruby
308
- # Use Spree factories
309
- create(:product, name: 'Test Product')
310
- create(:order_with_line_items)
311
- create(:user)
312
- ```
313
-
314
- ## Performance & Security
315
-
316
- ### Database Queries
317
-
318
- - Use includes/joins to avoid N+1 queries
319
- - Add database indexes for frequently queried fields
320
- - Use counter caches for associations that are counted frequently
321
-
322
- ### Security
323
-
324
- - Always use strong parameters in controllers
325
- - Sanitize user input
326
- - Use Spree's built-in authorization system (CanCanCan)
327
- - Validate file uploads and restrict file types
328
-
329
- ## Common Patterns
330
-
331
- ### Service Objects
332
-
333
- ```ruby
334
- # ✅ Spree service pattern
335
- module Spree
336
- class MyCustomService
337
- prepend Spree::ServiceModule::Base
338
-
339
- def call(param1:, param2: nil)
340
- # Service logic here
341
- success(result_data)
342
- rescue StandardError => e
343
- failure(e.message)
344
- end
345
- end
346
- end
347
- ```
348
-
349
- ### API Development
350
-
351
- ```ruby
352
- # ✅ Custom API endpoint
353
- module Spree
354
- module Api
355
- module V2
356
- class CustomController < Spree::Api::V2::BaseController
357
- def index
358
- render json: serialized_collection
359
- end
360
-
361
- private
362
-
363
- def serialized_collection
364
- Spree.api.storefront_product_serializer.new(
365
- collection,
366
- include: resource_includes,
367
- fields: sparse_fields
368
- ).serializable_hash
369
- end
370
- end
371
- end
372
- end
373
- end
374
- ```
375
-
376
- ## Avoid These Patterns
377
-
378
- ❌ Direct inheritance from Rails classes without Spree namespace
379
- ❌ Monkey patching without using decorators or dependencies
380
- ❌ Hard-coding configuration values
381
- ❌ Direct SQL queries without using ActiveRecord
382
- ❌ Creating models outside Spree namespace when extending core functionality
383
- ❌ Using class_eval decorators (use Module.prepend instead)
384
- ❌ Overriding entire view files when partial injection would work
385
- ❌ Modifying core Spree files directly
@@ -1,244 +0,0 @@
1
- module Spree
2
- module Core
3
- module Importer
4
- class Order
5
- def self.import(user, params)
6
- Spree::Deprecation.warn('Spree::Core::Importer::Order is deprecated and will be removed in Spree 5.5. Please use `Spree::Imports::Order` instead.')
7
-
8
- ensure_country_id_from_params params[:ship_address_attributes]
9
- ensure_state_id_from_params params[:ship_address_attributes]
10
- ensure_country_id_from_params params[:bill_address_attributes]
11
- ensure_state_id_from_params params[:bill_address_attributes]
12
-
13
- create_params = params.slice :currency
14
- order = Spree::Order.create! create_params
15
- order.associate_user!(user)
16
-
17
- shipments_attrs = params.delete(:shipments_attributes)
18
-
19
- create_line_items_from_params(params.delete(:line_items_attributes), order)
20
- create_shipments_from_params(shipments_attrs, order)
21
- create_adjustments_from_params(params.delete(:adjustments_attributes), order)
22
- create_payments_from_params(params.delete(:payments_attributes), order)
23
-
24
- if completed_at = params.delete(:completed_at)
25
- order.completed_at = completed_at
26
- order.state = 'complete'
27
- end
28
-
29
- params.delete(:user_id) unless user.try(:has_spree_role?, 'admin') && params.key?(:user_id)
30
-
31
- order.update!(params)
32
-
33
- order.create_proposed_shipments unless shipments_attrs.present?
34
-
35
- # Really ensure that the order totals & states are correct
36
- order.updater.update
37
- if shipments_attrs.present?
38
- order.shipments.each_with_index do |shipment, index|
39
- shipment.update_columns(cost: shipments_attrs[index][:cost].to_f) if shipments_attrs[index][:cost].present?
40
- end
41
- end
42
- order.reload
43
- rescue StandardError => e
44
- order.destroy if order&.persisted?
45
- raise e.message
46
- end
47
-
48
- def self.create_shipments_from_params(shipments_hash, order)
49
- return [] unless shipments_hash
50
-
51
- shipments_hash.each do |s|
52
- shipment = order.shipments.build
53
- shipment.tracking = s[:tracking]
54
- shipment.stock_location = Spree::StockLocation.find_by(admin_name: s[:stock_location]) ||
55
- Spree::StockLocation.find_by!(name: s[:stock_location])
56
- inventory_units = create_inventory_units_from_order_and_params(order, s[:inventory_units])
57
-
58
- inventory_units.each do |inventory_unit|
59
- inventory_unit.shipment = shipment
60
-
61
- if s[:shipped_at].present?
62
- inventory_unit.pending = false
63
- inventory_unit.state = 'shipped'
64
- end
65
-
66
- inventory_unit.save!
67
- end
68
-
69
- if s[:shipped_at].present?
70
- shipment.shipped_at = s[:shipped_at]
71
- shipment.state = 'shipped'
72
- end
73
-
74
- shipment.save!
75
-
76
- shipping_method = Spree::ShippingMethod.find_by(name: s[:shipping_method]) ||
77
- Spree::ShippingMethod.find_by!(admin_name: s[:shipping_method])
78
- rate = shipment.shipping_rates.create!(shipping_method: shipping_method, cost: s[:cost])
79
-
80
- shipment.selected_shipping_rate_id = rate.id
81
- shipment.update_amounts
82
-
83
- adjustments = s.delete(:adjustments_attributes)
84
- create_adjustments_from_params(adjustments, order, shipment)
85
- rescue StandardError => e
86
- raise "Order import shipments: #{e.message} #{s}"
87
- end
88
- end
89
-
90
- def self.create_inventory_units_from_order_and_params(order, inventory_unit_params)
91
- inventory_unit_params.each_with_object([]) do |inventory_unit_param, inventory_units|
92
- ensure_variant_id_from_params(inventory_unit_param)
93
- existing = inventory_units.detect { |unit| unit.variant_id == inventory_unit_param[:variant_id] }
94
- if existing
95
- existing.quantity += 1
96
- else
97
- line_item = order.line_items.detect { |ln| ln.variant_id == inventory_unit_param[:variant_id] }
98
- inventory_units << InventoryUnit.new(line_item: line_item, order_id: order.id, variant: line_item.variant, quantity: 1)
99
- end
100
- end
101
- end
102
-
103
- def self.create_line_items_from_params(line_items, order)
104
- return {} unless line_items
105
-
106
- line_items.each do |line_item|
107
- adjustments = line_item.delete(:adjustments_attributes)
108
- extra_params = line_item.except(:variant_id, :quantity, :sku)
109
- line_item = ensure_variant_id_from_params(line_item)
110
- variant = Spree::Variant.find(line_item[:variant_id])
111
- line_item = Cart::AddItem.call(order: order, variant: variant, quantity: line_item[:quantity]).value
112
- # Raise any errors with saving to prevent import succeeding with line items
113
- # failing silently.
114
- if extra_params.present?
115
- line_item.update!(extra_params)
116
- else
117
- line_item.save!
118
- end
119
- create_adjustments_from_params(adjustments, order, line_item)
120
- rescue StandardError => e
121
- raise "Order import line items: #{e.message} #{line_item}"
122
- end
123
- end
124
-
125
- def self.create_adjustments_from_params(adjustments, order, adjustable = nil)
126
- return [] unless adjustments
127
-
128
- adjustments.each do |a|
129
- adjustment = (adjustable || order).adjustments.build(
130
- order: order,
131
- amount: a[:amount].to_f,
132
- label: a[:label],
133
- source_type: source_type_from_adjustment(a)
134
- )
135
- adjustment.save!
136
- adjustment.close!
137
- rescue StandardError => e
138
- raise "Order import adjustments: #{e.message} #{a}"
139
- end
140
- end
141
-
142
- def self.create_payments_from_params(payments_hash, order)
143
- return [] unless payments_hash
144
-
145
- payments_hash.each do |p|
146
- payment = order.payments.build order: order
147
- payment.amount = p[:amount].to_f
148
- # Order API should be using state as that's the normal payment field.
149
- # spree_wombat serializes payment state as status so imported orders should fall back to status field.
150
- payment.state = p[:state] || p[:status] || 'completed'
151
- payment.created_at = p[:created_at] if p[:created_at]
152
- payment.payment_method = Spree::PaymentMethod.find_by!(name: p[:payment_method])
153
- payment.source = create_source_payment_from_params(p[:source], payment) if p[:source]
154
- payment.save!
155
- rescue StandardError => e
156
- raise "Order import payments: #{e.message} #{p}"
157
- end
158
- end
159
-
160
- def self.create_source_payment_from_params(source_hash, payment)
161
- Spree::CreditCard.create(
162
- month: source_hash[:month],
163
- year: source_hash[:year],
164
- cc_type: source_hash[:cc_type],
165
- last_digits: source_hash[:last_digits],
166
- name: source_hash[:name],
167
- payment_method: payment.payment_method,
168
- gateway_customer_profile_id: source_hash[:gateway_customer_profile_id],
169
- gateway_payment_profile_id: source_hash[:gateway_payment_profile_id],
170
- imported: true
171
- )
172
- rescue StandardError => e
173
- raise "Order import source payments: #{e.message} #{source_hash}"
174
- end
175
-
176
- def self.ensure_variant_id_from_params(hash)
177
- sku = hash.delete(:sku)
178
- unless hash[:variant_id].present?
179
- hash[:variant_id] = Spree::Variant.active.find_by!(sku: sku).id
180
- end
181
- hash
182
- rescue ActiveRecord::RecordNotFound => e
183
- raise "Ensure order import variant: Variant w/SKU #{sku} not found."
184
- rescue StandardError => e
185
- raise "Ensure order import variant: #{e.message} #{hash}"
186
- end
187
-
188
- def self.ensure_country_id_from_params(address)
189
- return if address.nil? || address[:country_id].present? || address[:country].nil?
190
-
191
- begin
192
- search = {}
193
- if name = address[:country]['name']
194
- search[:name] = name
195
- elsif iso_name = address[:country]['iso_name']
196
- search[:iso_name] = iso_name.upcase
197
- elsif iso = address[:country]['iso']
198
- search[:iso] = iso.upcase
199
- elsif iso3 = address[:country]['iso3']
200
- search[:iso3] = iso3.upcase
201
- end
202
-
203
- address.delete(:country)
204
- address[:country_id] = Spree::Country.where(search).first!.id
205
- rescue StandardError => e
206
- raise "Ensure order import address country: #{e.message} #{search}"
207
- end
208
- end
209
-
210
- def self.ensure_state_id_from_params(address)
211
- return if address.nil? || address[:state_id].present? || address[:state].nil?
212
-
213
- begin
214
- search = {}
215
- if name = address[:state]['name']
216
- search[:name] = name
217
- elsif abbr = address[:state]['abbr']
218
- search[:abbr] = abbr.upcase
219
- end
220
-
221
- address.delete(:state)
222
- search[:country_id] = address[:country_id]
223
-
224
- if state = Spree::State.where(search).first
225
- address[:state_id] = state.id
226
- else
227
- address[:state_name] = search[:name] || search[:abbr]
228
- end
229
- rescue StandardError => e
230
- raise "Ensure order import address state: #{e.message} #{search}"
231
- end
232
- end
233
-
234
- def self.source_type_from_adjustment(adjustment)
235
- if adjustment[:tax]
236
- 'Spree::TaxRate'
237
- elsif adjustment[:promotion]
238
- 'Spree::PromotionAction'
239
- end
240
- end
241
- end
242
- end
243
- end
244
- end
@@ -1,67 +0,0 @@
1
- module Spree
2
- module Core
3
- module Importer
4
- class Product
5
- attr_reader :product, :product_attrs, :variants_attrs, :options_attrs, :store
6
-
7
- def initialize(product, product_params, options = {})
8
- Spree::Deprecation.warn('Spree::Core::Importer::Product is deprecated and will be removed in Spree 5.5. Please use `Spree::Imports::Product` instead.')
9
-
10
- @store = options[:store] || Spree::Store.default
11
- @product = product || Spree::Product.new(product_params)
12
- @product.stores << @store if @product.stores.exclude?(@store)
13
-
14
- @product_attrs = product_params.to_h
15
- @variants_attrs = (options[:variants_attrs] || []).map(&:to_h)
16
- @options_attrs = options[:options_attrs] || []
17
- end
18
-
19
- def create
20
- if product.save
21
- variants_attrs.each do |variant_attribute|
22
- # make sure the product is assigned before the options=
23
- product.variants.create({ product: product }.merge(variant_attribute))
24
- end
25
-
26
- set_up_options
27
- end
28
-
29
- product
30
- end
31
-
32
- def update
33
- if product.update(product_attrs)
34
- variants_attrs.each do |variant_attribute|
35
- # update the variant if the id is present in the payload
36
- if variant_attribute['id'].present?
37
- product.variants.find(variant_attribute['id']).update(variant_attribute)
38
- else
39
- # make sure the product is assigned before the options=
40
- product.variants.create({ product: product }.merge(variant_attribute))
41
- end
42
- end
43
-
44
- set_up_options
45
- end
46
-
47
- product
48
- end
49
-
50
- private
51
-
52
- def set_up_options
53
- options_attrs.each do |name|
54
- option_type = Spree::OptionType.where(name: name).first_or_initialize do |option_type|
55
- option_type.presentation = name
56
- option_type.save!
57
- end
58
-
59
- unless product.option_types.include?(option_type)
60
- product.option_types << option_type
61
- end
62
- end
63
- end
64
- end
65
- end
66
- end
67
- end
@@ -1,9 +0,0 @@
1
- module Spree
2
- module Core
3
- module Importer
4
- end
5
- end
6
- end
7
-
8
- require 'spree/core/importer/order'
9
- require 'spree/core/importer/product'