spree_core 5.4.0.beta3 → 5.4.0.beta5

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/app/jobs/spree/exports/generate_job.rb +1 -1
  3. data/app/jobs/spree/images/save_from_url_job.rb +47 -23
  4. data/app/jobs/spree/reports/generate_job.rb +1 -1
  5. data/app/models/concerns/spree/product_scopes.rb +78 -42
  6. data/app/models/concerns/spree/user_methods.rb +1 -0
  7. data/app/models/spree/address.rb +0 -14
  8. data/app/models/spree/category.rb +6 -0
  9. data/app/models/spree/country.rb +2 -23
  10. data/app/models/spree/coupon_code.rb +6 -1
  11. data/app/models/spree/current.rb +3 -2
  12. data/app/models/spree/exports/coupon_codes.rb +18 -0
  13. data/app/models/spree/market.rb +8 -0
  14. data/app/models/spree/payment/gateway_options.rb +5 -0
  15. data/app/models/spree/product.rb +7 -34
  16. data/app/models/spree/store.rb +41 -80
  17. data/app/models/spree/taxon.rb +49 -44
  18. data/app/models/spree/variant.rb +1 -1
  19. data/app/models/spree/webhook_endpoint.rb +17 -0
  20. data/app/models/spree/zone.rb +21 -9
  21. data/app/presenters/spree/csv/coupon_code_presenter.rb +31 -0
  22. data/app/services/spree/cart/create.rb +18 -2
  23. data/app/services/spree/cart/upsert_items.rb +80 -0
  24. data/app/services/spree/gift_cards/apply.rb +5 -4
  25. data/app/services/spree/orders/update.rb +121 -0
  26. data/config/locales/en.yml +5 -0
  27. data/lib/spree/core/configuration.rb +1 -0
  28. data/lib/spree/core/controller_helpers/strong_parameters.rb +1 -1
  29. data/lib/spree/core/dependencies.rb +2 -0
  30. data/lib/spree/core/engine.rb +2 -1
  31. data/lib/spree/core/version.rb +1 -1
  32. data/lib/spree/permitted_attributes.rb +3 -5
  33. data/lib/spree/testing_support/factories/store_factory.rb +0 -9
  34. data/lib/spree_core.rb +0 -1
  35. data/lib/tasks/core.rake +0 -28
  36. metadata +23 -20
  37. data/app/models/concerns/spree/stores/socials.rb +0 -72
  38. data/lib/normalize_string.rb +0 -18
@@ -25,15 +25,16 @@ module Spree
25
25
  return failure(:gift_card_mismatched_customer) if gift_card.user != order.user
26
26
  end
27
27
 
28
- amount = [gift_card.amount_remaining, order.total].min
29
28
  store = order.store
30
29
 
31
- return failure(:gift_card_no_amount_remaining) unless amount.positive? || order.total.zero?
32
-
33
30
  payment_method = ensure_store_credit_payment_method!(store)
34
31
 
35
- gift_card.lock!
36
32
  order.with_lock do
33
+ gift_card.lock!
34
+ amount = [gift_card.amount_remaining, order.total].min
35
+
36
+ return failure(:gift_card_no_amount_remaining) unless amount.positive? || order.total.zero?
37
+
37
38
  store_credit = gift_card.store_credits.create!(
38
39
  store: store,
39
40
  user: order.user,
@@ -0,0 +1,121 @@
1
+ module Spree
2
+ module Orders
3
+ # Core order update service with modern conventions:
4
+ # - Flat parameter structure (no wrapping in "order" key)
5
+ # - snake_case field names without "_attributes" suffix
6
+ # - Automatic state management based on what's being updated
7
+ # - Support for line_items with prefixed variant IDs
8
+ #
9
+ # @example Update order with line items
10
+ # Spree::Orders::Update.call(
11
+ # order: order,
12
+ # params: {
13
+ # email: "customer@example.com",
14
+ # line_items: [
15
+ # { variant_id: "variant_123", quantity: 2 },
16
+ # { variant_id: "variant_456", quantity: 1 }
17
+ # ],
18
+ # ship_address: {
19
+ # firstname: "John",
20
+ # lastname: "Doe",
21
+ # address1: "123 Main St",
22
+ # city: "New York",
23
+ # zipcode: "10001",
24
+ # country_iso: "US",
25
+ # state_abbr: "NY"
26
+ # }
27
+ # }
28
+ # )
29
+ #
30
+ class Update
31
+ prepend Spree::ServiceModule::Base
32
+
33
+ def call(order:, params:)
34
+ @order = order
35
+ @params = params.to_h.deep_symbolize_keys
36
+
37
+ ApplicationRecord.transaction do
38
+ assign_order_attributes
39
+ assign_address(:ship_address)
40
+ assign_address(:bill_address)
41
+
42
+ order.save!
43
+
44
+ process_line_items
45
+ end
46
+
47
+ success(order.reload)
48
+ rescue ActiveRecord::RecordNotFound
49
+ raise
50
+ rescue ActiveRecord::RecordInvalid => e
51
+ failure(order, e.record.errors.full_messages.to_sentence)
52
+ rescue StandardError => e
53
+ failure(order, e.message)
54
+ end
55
+
56
+ private
57
+
58
+ attr_reader :order, :params
59
+
60
+ def assign_order_attributes
61
+ order.email = params[:email] if params[:email].present?
62
+ order.special_instructions = params[:special_instructions] if params.key?(:special_instructions)
63
+ order.currency = params[:currency].upcase if params[:currency].present?
64
+ order.locale = params[:locale] if params[:locale].present?
65
+ order.metadata = order.metadata.merge(params[:metadata].to_h) if params[:metadata].present?
66
+ end
67
+
68
+ def assign_address(address_type)
69
+ address_id_param = params[:"#{address_type}_id"]
70
+ address_params = params[address_type]
71
+
72
+ # Priority 1: Direct address ID reference (ship_address_id / bill_address_id)
73
+ if address_id_param.present?
74
+ address_id = resolve_address_id(address_id_param)
75
+ order.public_send(:"#{address_type}_id=", address_id) if address_id
76
+ return
77
+ end
78
+
79
+ # Priority 2: Nested address params (ship_address / bill_address)
80
+ return unless address_params.is_a?(Hash)
81
+
82
+ if address_params[:id].present?
83
+ # Using existing address by ID within nested params
84
+ address_id = resolve_address_id(address_params[:id])
85
+ order.public_send(:"#{address_type}_id=", address_id) if address_id
86
+ else
87
+ # Creating/updating address with provided attributes
88
+ revert_to_address_state if order.has_checkout_step?('address')
89
+ order.public_send(:"#{address_type}_attributes=", address_params)
90
+ end
91
+ end
92
+
93
+ def process_line_items
94
+ return unless params[:line_items].is_a?(Array)
95
+
96
+ result = Spree.cart_upsert_items_service.call(
97
+ order: order,
98
+ line_items: params[:line_items]
99
+ )
100
+
101
+ raise StandardError, result.error.to_s if result.failure?
102
+ end
103
+
104
+ # Translate prefixed ID to internal id
105
+ def resolve_address_id(prefixed_id)
106
+ return unless order.user
107
+
108
+ decoded = Spree::Address.decode_prefixed_id(prefixed_id)
109
+ decoded ? order.user.addresses.find_by(id: decoded)&.id : nil
110
+ end
111
+
112
+ # Revert order state to 'address' when address changes
113
+ # This ensures shipments are recreated when transitioning back to delivery
114
+ def revert_to_address_state
115
+ return if ['cart', 'address'].include?(order.state)
116
+
117
+ order.state = 'address'
118
+ end
119
+ end
120
+ end
121
+ end
@@ -363,6 +363,10 @@ en:
363
363
  cannot_destroy_if_attached_to_line_items: Cannot delete Variants that are added to placed Orders. In such cases, please discontinue them.
364
364
  must_supply_price_for_variant_or_master: Must supply price for variant or master price for product.
365
365
  no_master_variant_found_to_infer_price: No master variant found to infer price
366
+ spree/webhook_endpoint:
367
+ attributes:
368
+ url:
369
+ internal_address_not_allowed: must not point to an internal or private network address
366
370
  spree/wished_item:
367
371
  attributes:
368
372
  variant:
@@ -1231,6 +1235,7 @@ en:
1231
1235
  this_file_language: English (US)
1232
1236
  translations: Translations
1233
1237
  icon: Icon
1238
+ idempotency_key_reused: This Idempotency-Key has already been used with different request parameters.
1234
1239
  image: Image
1235
1240
  image_alt_text: Add Alt text to your image
1236
1241
  images: Images
@@ -47,6 +47,7 @@ module Spree
47
47
  preference :expedited_exchanges_days_window, :integer, default: 14 # the amount of days the customer has to return their item after the expedited exchange is shipped in order to avoid being charged
48
48
  preference :geocode_addresses, :boolean, default: true
49
49
  preference :images_save_from_url_job_attempts, :integer, default: 5
50
+ preference :max_image_download_size, :integer, default: 20_971_520 # 20 MB in bytes
50
51
 
51
52
  # Preprocessed product image variant sizes at 2x retina resolution.
52
53
  # These variants are generated on upload to reduce runtime processing.
@@ -39,7 +39,7 @@ module Spree
39
39
  end
40
40
 
41
41
  def permitted_store_attributes
42
- permitted_attributes.store_attributes + Spree::Store::SUPPORTED_SOCIAL_NETWORKS.map { |social| "store_#{social}" }
42
+ permitted_attributes.store_attributes
43
43
  end
44
44
  end
45
45
  end
@@ -16,6 +16,7 @@ module Spree
16
16
  cart_remove_item_service: 'Spree::Cart::RemoveItem',
17
17
  cart_remove_line_item_service: 'Spree::Cart::RemoveLineItem',
18
18
  cart_set_item_quantity_service: 'Spree::Cart::SetQuantity',
19
+ cart_upsert_items_service: 'Spree::Cart::UpsertItems',
19
20
  cart_estimate_shipping_rates_service: 'Spree::Cart::EstimateShippingRates',
20
21
  cart_empty_service: 'Spree::Cart::Empty',
21
22
  cart_destroy_service: 'Spree::Cart::Destroy',
@@ -41,6 +42,7 @@ module Spree
41
42
  # order
42
43
  order_approve_service: 'Spree::Orders::Approve',
43
44
  order_cancel_service: 'Spree::Orders::Cancel',
45
+ order_update_service: 'Spree::Orders::Update',
44
46
  order_updater: 'Spree::OrderUpdater',
45
47
 
46
48
  # shipment
@@ -180,7 +180,8 @@ module Spree
180
180
  Spree::Exports::Orders,
181
181
  Spree::Exports::Customers,
182
182
  Spree::Exports::GiftCards,
183
- Spree::Exports::NewsletterSubscribers
183
+ Spree::Exports::NewsletterSubscribers,
184
+ Spree::Exports::CouponCodes
184
185
  ]
185
186
 
186
187
  Rails.application.config.spree.import_types = [
@@ -1,5 +1,5 @@
1
1
  module Spree
2
- VERSION = '5.4.0.beta3'.freeze
2
+ VERSION = '5.4.0.beta5'.freeze
3
3
 
4
4
  def self.version
5
5
  VERSION
@@ -266,15 +266,13 @@ module Spree
266
266
  :meta_description, :default_currency, :default_country_iso, :mail_from_address,
267
267
  :customer_support_email, :description, :address, :contact_phone,
268
268
  :supported_locales, :default_locale, :supported_currencies,
269
- :new_order_notifications_email, :seo_robots,
269
+ :new_order_notifications_email,
270
270
  :preferred_admin_locale, :preferred_timezone, :preferred_weight_unit, :preferred_unit_system,
271
271
  :preferred_digital_asset_authorized_clicks, :preferred_digital_asset_authorized_days,
272
272
  :preferred_limit_digital_download_count, :preferred_limit_digital_download_days,
273
273
  :preferred_digital_asset_link_expire_time,
274
- :logo, :mailer_logo, :social_logo, :favicon_image,
275
- :checkout_message, :preferred_guest_checkout,
276
- :customer_terms_of_service, :customer_privacy_policy,
277
- :customer_returns_policy, :customer_shipping_policy]
274
+ :logo, :mailer_logo,
275
+ :preferred_guest_checkout]
278
276
 
279
277
  @@store_credit_attributes = %i[amount currency category_id memo]
280
278
 
@@ -14,14 +14,5 @@ FactoryBot.define do
14
14
  instagram { 'spreecommerce' }
15
15
  meta_description { 'Sample store description.' }
16
16
 
17
- trait :with_favicon do
18
- after(:build) do |store|
19
- store.favicon_image.attach(
20
- io: File.open(Spree::Core::Engine.root.join('spec', 'fixtures', 'thinking-cat.jpg')),
21
- filename: 'thinking-cat.jpg',
22
- content_type: 'image/jpeg'
23
- )
24
- end
25
- end
26
17
  end
27
18
  end
data/lib/spree_core.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require 'friendly_id/paranoia'
2
2
  require 'friendly_id/history_decorator'
3
3
  require 'mobility/plugins/store_based_fallbacks'
4
- require 'normalize_string'
5
4
 
6
5
  require 'spree/core'
data/lib/tasks/core.rake CHANGED
@@ -8,34 +8,6 @@ namespace :db do
8
8
  end
9
9
  end
10
10
 
11
- desc 'Migrate policies to store policies'
12
- task migrate_policies: :environment do |_t, _args|
13
- # Helper to consistently derive policy name
14
- derive_policy_name = lambda do |name_str|
15
- name_str.to_s.gsub(/customer_/, '').gsub(/_policy$/, '')
16
- end
17
-
18
- # Check if migration has already been run
19
- if Spree::Policy.any?
20
- puts "Policies already exist. Skipping migration to prevent duplicates."
21
- exit
22
- end
23
-
24
- Spree::Store.all.each do |store|
25
- %w[customer_terms_of_service customer_privacy_policy customer_returns_policy customer_shipping_policy].each do |policy_slug|
26
- policy = store.send(policy_slug)
27
- next unless policy.present?
28
-
29
- store.policies.create(
30
- name: Spree.t(derive_policy_name.call(policy_slug)),
31
- body: policy.body
32
- )
33
-
34
- puts "Migrated #{policy_slug} to store #{store.id}"
35
- end
36
- end
37
- end
38
-
39
11
  end
40
12
 
41
13
  namespace :core do
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.beta3
4
+ version: 5.4.0.beta5
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-03-02 00:00:00.000000000 Z
13
+ date: 2026-03-10 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: i18n-tasks
@@ -490,20 +490,6 @@ dependencies:
490
490
  - - ">="
491
491
  - !ruby/object:Gem::Version
492
492
  version: '0'
493
- - !ruby/object:Gem::Dependency
494
- name: any_ascii
495
- requirement: !ruby/object:Gem::Requirement
496
- requirements:
497
- - - "~>"
498
- - !ruby/object:Gem::Version
499
- version: 0.3.2
500
- type: :runtime
501
- prerelease: false
502
- version_requirements: !ruby/object:Gem::Requirement
503
- requirements:
504
- - - "~>"
505
- - !ruby/object:Gem::Version
506
- version: 0.3.2
507
493
  - !ruby/object:Gem::Dependency
508
494
  name: safely_block
509
495
  requirement: !ruby/object:Gem::Requirement
@@ -574,6 +560,20 @@ dependencies:
574
560
  - - "~>"
575
561
  - !ruby/object:Gem::Version
576
562
  version: '0.2'
563
+ - !ruby/object:Gem::Dependency
564
+ name: ssrf_filter
565
+ requirement: !ruby/object:Gem::Requirement
566
+ requirements:
567
+ - - "~>"
568
+ - !ruby/object:Gem::Version
569
+ version: '1.0'
570
+ type: :runtime
571
+ prerelease: false
572
+ version_requirements: !ruby/object:Gem::Requirement
573
+ requirements:
574
+ - - "~>"
575
+ - !ruby/object:Gem::Version
576
+ version: '1.0'
577
577
  description: Spree Models, Helpers, Services and core libraries
578
578
  email: hello@spreecommerce.org
579
579
  executables: []
@@ -879,7 +879,6 @@ files:
879
879
  - app/models/concerns/spree/store_scoped_resource.rb
880
880
  - app/models/concerns/spree/stores/markets.rb
881
881
  - app/models/concerns/spree/stores/setup.rb
882
- - app/models/concerns/spree/stores/socials.rb
883
882
  - app/models/concerns/spree/translatable_resource.rb
884
883
  - app/models/concerns/spree/translatable_resource_scopes.rb
885
884
  - app/models/concerns/spree/translatable_resource_slug.rb
@@ -922,6 +921,7 @@ files:
922
921
  - app/models/spree/calculator/shipping/price_sack.rb
923
922
  - app/models/spree/calculator/tiered_flat_rate.rb
924
923
  - app/models/spree/calculator/tiered_percent.rb
924
+ - app/models/spree/category.rb
925
925
  - app/models/spree/classification.rb
926
926
  - app/models/spree/country.rb
927
927
  - app/models/spree/coupon_code.rb
@@ -935,6 +935,7 @@ files:
935
935
  - app/models/spree/event.rb
936
936
  - app/models/spree/exchange.rb
937
937
  - app/models/spree/export.rb
938
+ - app/models/spree/exports/coupon_codes.rb
938
939
  - app/models/spree/exports/customers.rb
939
940
  - app/models/spree/exports/gift_cards.rb
940
941
  - app/models/spree/exports/newsletter_subscribers.rb
@@ -1165,6 +1166,7 @@ files:
1165
1166
  - app/models/spree/zone.rb
1166
1167
  - app/models/spree/zone_member.rb
1167
1168
  - app/paginators/spree/shared/paginate.rb
1169
+ - app/presenters/spree/csv/coupon_code_presenter.rb
1168
1170
  - app/presenters/spree/csv/customer_presenter.rb
1169
1171
  - app/presenters/spree/csv/gift_card_presenter.rb
1170
1172
  - app/presenters/spree/csv/metafields_helper.rb
@@ -1199,6 +1201,7 @@ files:
1199
1201
  - app/services/spree/cart/remove_out_of_stock_items.rb
1200
1202
  - app/services/spree/cart/set_quantity.rb
1201
1203
  - app/services/spree/cart/update.rb
1204
+ - app/services/spree/cart/upsert_items.rb
1202
1205
  - app/services/spree/checkout/add_store_credit.rb
1203
1206
  - app/services/spree/checkout/advance.rb
1204
1207
  - app/services/spree/checkout/complete.rb
@@ -1230,6 +1233,7 @@ files:
1230
1233
  - app/services/spree/orders/approve.rb
1231
1234
  - app/services/spree/orders/cancel.rb
1232
1235
  - app/services/spree/orders/create_user_account.rb
1236
+ - app/services/spree/orders/update.rb
1233
1237
  - app/services/spree/orders/update_contact_information.rb
1234
1238
  - app/services/spree/payments/create.rb
1235
1239
  - app/services/spree/products/auto_match_taxons.rb
@@ -1463,7 +1467,6 @@ files:
1463
1467
  - lib/generators/spree/model_decorator/model_decorator_generator.rb
1464
1468
  - lib/generators/spree/model_decorator/templates/model_decorator.rb.tt
1465
1469
  - lib/mobility/plugins/store_based_fallbacks.rb
1466
- - lib/normalize_string.rb
1467
1470
  - lib/spree/analytics.rb
1468
1471
  - lib/spree/core.rb
1469
1472
  - lib/spree/core/components.rb
@@ -1653,9 +1656,9 @@ licenses:
1653
1656
  - BSD-3-Clause
1654
1657
  metadata:
1655
1658
  bug_tracker_uri: https://github.com/spree/spree/issues
1656
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.0.beta3
1659
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.0.beta5
1657
1660
  documentation_uri: https://docs.spreecommerce.org/
1658
- source_code_uri: https://github.com/spree/spree/tree/v5.4.0.beta3
1661
+ source_code_uri: https://github.com/spree/spree/tree/v5.4.0.beta5
1659
1662
  post_install_message:
1660
1663
  rdoc_options: []
1661
1664
  require_paths:
@@ -1,72 +0,0 @@
1
- module Spree
2
- module Stores
3
- module Socials
4
- extend ActiveSupport::Concern
5
-
6
- SUPPORTED_SOCIAL_NETWORKS = %w[instagram facebook twitter pinterest tiktok youtube spotify discord].freeze
7
-
8
- SOCIAL_NETWORKS_CONFIG = {
9
- twitter: {
10
- input_placeholder: 'https://twitter.com/your_handle',
11
- profile_link: 'https://twitter.com/your_handle'
12
- },
13
- instagram: {
14
- input_placeholder: 'https://www.instagram.com/your_handle',
15
- profile_link: 'https://www.instagram.com/your_handle'
16
- },
17
- facebook: {
18
- input_placeholder: 'https://www.facebook.com/your_page',
19
- profile_link: 'https://www.facebook.com/your_page'
20
- },
21
- youtube: {
22
- input_placeholder: 'https://www.youtube.com/@your_channel',
23
- profile_link: 'https://www.youtube.com/@your_channel'
24
- },
25
- pinterest: {
26
- input_placeholder: 'https://pinterest.com/your_handle',
27
- profile_link: 'https://pinterest.com/your_handle'
28
- },
29
- tiktok: {
30
- input_placeholder: 'your_handle',
31
- profile_link: 'https://www.tiktok.com/@your_handle'
32
- },
33
- spotify: {
34
- input_placeholder: 'https://open.spotify.com/user/your_handle',
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'
40
- }
41
- }.freeze
42
-
43
- included do
44
- # generate methods for social links
45
- SUPPORTED_SOCIAL_NETWORKS.each do |social|
46
- # store the social handle in the public metadata
47
- store_accessor :public_metadata, social
48
-
49
- define_method "#{social}_link" do
50
- return if send(social).blank?
51
-
52
- send(social).match(/http/) ? send(social) : SOCIAL_NETWORKS_CONFIG[social.to_sym][:profile_link].gsub(/your_handle|your_page|your_channel/, send(social).sub(/^\//, '').sub(/^@/, ''))
53
- end
54
-
55
- define_method "#{social}_handle" do
56
- return if send(social).blank?
57
-
58
- (send(social).match(/http/) ? send(social).split('/').last : send(social)).sub(/^\//, '').gsub('@', '').split('?').first
59
- end
60
- end
61
- end
62
-
63
- def social_handle
64
- @social_handle ||= instagram_handle || youtube_handle || tiktok_handle
65
- end
66
-
67
- def social_links
68
- @social_links ||= [instagram_link, facebook_link, twitter_link, pinterest_link, youtube_link, tiktok_link, spotify_link, discord_link].compact_blank
69
- end
70
- end
71
- end
72
- end
@@ -1,18 +0,0 @@
1
- require 'any_ascii'
2
-
3
- module NormalizeString
4
- def self.normalize(string)
5
- return unless string.present?
6
-
7
- AnyAscii.transliterate(string)
8
- end
9
-
10
- def self.remove_emoji_and_normalize(string, keep_emoji_when_empty: false)
11
- return unless string.present?
12
-
13
- result = AnyAscii.transliterate(string.gsub(/\p{So}/, ''))
14
- return result if result.present? || !keep_emoji_when_empty
15
-
16
- normalize(string)
17
- end
18
- end