spree_core 5.4.3 → 5.5.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.
- checksums.yaml +4 -4
- data/app/helpers/spree/base_helper.rb +0 -82
- data/app/helpers/spree/currency_helper.rb +0 -12
- data/app/helpers/spree/products_helper.rb +0 -8
- data/app/jobs/spree/base_job.rb +18 -0
- data/app/jobs/spree/events/subscriber_job.rb +2 -1
- data/app/jobs/spree/exports/generate_job.rb +11 -0
- data/app/jobs/spree/images/save_from_url_job.rb +23 -8
- data/app/jobs/spree/imports/assign_tags_job.rb +11 -0
- data/app/jobs/spree/imports/base_job.rb +15 -0
- data/app/jobs/spree/imports/create_categories_job.rb +37 -0
- data/app/jobs/spree/imports/create_rows_job.rb +1 -3
- data/app/jobs/spree/imports/process_group_job.rb +8 -6
- data/app/jobs/spree/imports/process_rows_job.rb +1 -3
- data/app/jobs/spree/media/migrate_product_assets_job.rb +83 -0
- data/app/jobs/spree/products/refresh_metrics_job.rb +15 -4
- data/app/jobs/spree/reports/generate_job.rb +11 -0
- data/app/jobs/spree/search_provider/index_job.rb +5 -1
- data/app/jobs/spree/search_provider/remove_job.rb +4 -0
- data/app/jobs/spree/stock_reservations/expire_job.rb +11 -0
- data/app/models/concerns/spree/calculated_adjustments.rb +34 -1
- data/app/models/concerns/spree/display_on.rb +31 -0
- data/app/models/concerns/spree/metafields.rb +167 -5
- data/app/models/concerns/spree/preference_schema.rb +191 -0
- data/app/models/concerns/spree/prefixed_id.rb +94 -11
- data/app/models/concerns/spree/product_scopes.rb +36 -17
- data/app/models/concerns/spree/ransackable_attributes.rb +5 -1
- data/app/models/concerns/spree/search_indexable.rb +8 -7
- data/app/models/concerns/spree/searchable.rb +11 -2
- data/app/models/concerns/spree/stores/channels.rb +20 -0
- data/app/models/concerns/spree/stores/markets.rb +21 -5
- data/app/models/concerns/spree/typed_associations.rb +120 -0
- data/app/models/concerns/spree/user_methods.rb +71 -12
- data/app/models/spree/ability.rb +4 -117
- data/app/models/spree/api_key.rb +53 -0
- data/app/models/spree/asset.rb +28 -5
- data/app/models/spree/authentication/strategy_registry.rb +72 -0
- data/app/models/spree/base.rb +18 -1
- data/app/models/spree/channel.rb +159 -0
- data/app/models/spree/country.rb +2 -0
- data/app/models/spree/current.rb +5 -1
- data/app/models/spree/custom_field.rb +9 -0
- data/app/models/spree/custom_field_definition.rb +7 -0
- data/app/models/spree/customer_group.rb +8 -2
- data/app/models/spree/export.rb +30 -3
- data/app/models/spree/gateway.rb +25 -0
- data/app/models/spree/gift_card.rb +1 -1
- data/app/models/spree/gift_card_batch.rb +4 -1
- data/app/models/spree/import.rb +5 -0
- data/app/models/spree/import_row.rb +12 -0
- data/app/models/spree/line_item.rb +6 -1
- data/app/models/spree/market.rb +32 -1
- data/app/models/spree/metafield.rb +38 -0
- data/app/models/spree/metafield_definition.rb +29 -6
- data/app/models/spree/metafields/json.rb +10 -0
- data/app/models/spree/newsletter_subscriber.rb +19 -3
- data/app/models/spree/option_type.rb +48 -7
- data/app/models/spree/order/checkout.rb +3 -3
- data/app/models/spree/order.rb +102 -6
- data/app/models/spree/order_approval.rb +19 -0
- data/app/models/spree/order_cancellation.rb +19 -0
- data/app/models/spree/order_routing/has_strategy_preference.rb +28 -0
- data/app/models/spree/order_routing/rules/default_location.rb +16 -0
- data/app/models/spree/order_routing/rules/minimize_splits.rb +45 -0
- data/app/models/spree/order_routing/rules/preferred_location.rb +22 -0
- data/app/models/spree/order_routing/strategy/base.rb +47 -0
- data/app/models/spree/order_routing/strategy/legacy.rb +33 -0
- data/app/models/spree/order_routing/strategy/reducer.rb +68 -0
- data/app/models/spree/order_routing/strategy/rules.rb +81 -0
- data/app/models/spree/order_routing_rule.rb +75 -0
- data/app/models/spree/permission_sets/configuration_management.rb +16 -0
- data/app/models/spree/permission_sets/product_display.rb +2 -0
- data/app/models/spree/permission_sets/product_management.rb +2 -0
- data/app/models/spree/price.rb +14 -1
- data/app/models/spree/price_list.rb +129 -17
- data/app/models/spree/price_rule.rb +11 -1
- data/app/models/spree/price_rules/customer_group_rule.rb +15 -1
- data/app/models/spree/price_rules/market_rule.rb +16 -1
- data/app/models/spree/price_rules/user_rule.rb +21 -2
- data/app/models/spree/product/channels.rb +149 -0
- data/app/models/spree/product/legacy_multi_store_support.rb +40 -0
- data/app/models/spree/product/slugs.rb +1 -1
- data/app/models/spree/product.rb +172 -31
- data/app/models/spree/product_publication.rb +43 -0
- data/app/models/spree/promotion/actions/create_adjustment.rb +4 -0
- data/app/models/spree/promotion/actions/create_item_adjustments.rb +4 -0
- data/app/models/spree/promotion/actions/create_line_items.rb +32 -14
- data/app/models/spree/promotion/rules/country.rb +40 -18
- data/app/models/spree/promotion/rules/customer_group.rb +10 -1
- data/app/models/spree/promotion/rules/product.rb +4 -0
- data/app/models/spree/promotion/rules/taxon.rb +24 -1
- data/app/models/spree/promotion/rules/user.rb +21 -0
- data/app/models/spree/promotion/rules/user_logged_in.rb +6 -0
- data/app/models/spree/promotion.rb +22 -1
- data/app/models/spree/promotion_action.rb +17 -11
- data/app/models/spree/promotion_rule.rb +17 -18
- data/app/models/spree/search_provider/meilisearch.rb +12 -2
- data/app/models/spree/stock/availability_validator.rb +1 -1
- data/app/models/spree/stock/quantifier.rb +89 -9
- data/app/models/spree/stock_item.rb +36 -0
- data/app/models/spree/stock_location.rb +52 -0
- data/app/models/spree/stock_reservation.rb +38 -0
- data/app/models/spree/stock_reservations/insufficient_stock_error.rb +12 -0
- data/app/models/spree/store.rb +18 -72
- data/app/models/spree/store_credit.rb +0 -8
- data/app/models/spree/store_product.rb +11 -23
- data/app/models/spree/taxon.rb +0 -5
- data/app/models/spree/user_identity.rb +1 -2
- data/app/models/spree/variant.rb +132 -18
- data/app/models/spree/variant_media.rb +46 -0
- data/app/models/spree/webhook_delivery.rb +1 -1
- data/app/models/spree/webhook_endpoint.rb +24 -0
- data/app/models/spree/wished_item.rb +0 -13
- data/app/presenters/spree/csv/product_variant_presenter.rb +23 -3
- data/app/presenters/spree/search_provider/product_presenter.rb +11 -4
- data/app/presenters/spree/variant_presenter.rb +4 -3
- data/app/services/spree/addresses/update.rb +6 -8
- data/app/services/spree/cart/add_item.rb +10 -0
- data/app/services/spree/cart/empty.rb +2 -0
- data/app/services/spree/cart/remove_line_item.rb +10 -0
- data/app/services/spree/cart/remove_out_of_stock_items.rb +1 -1
- data/app/services/spree/cart/set_quantity.rb +10 -0
- data/app/services/spree/carts/complete.rb +1 -0
- data/app/services/spree/carts/create.rb +1 -0
- data/app/services/spree/carts/update.rb +18 -2
- data/app/services/spree/carts/upsert_items.rb +6 -6
- data/app/services/spree/imports/row_processors/customer.rb +4 -1
- data/app/services/spree/imports/row_processors/product_variant.rb +95 -57
- data/app/services/spree/newsletter/link_user.rb +53 -0
- data/app/services/spree/newsletter/subscribe.rb +31 -9
- data/app/services/spree/orders/approve.rb +27 -6
- data/app/services/spree/orders/build_shipments.rb +29 -0
- data/app/services/spree/orders/cancel.rb +34 -3
- data/app/services/spree/orders/complete.rb +53 -0
- data/app/services/spree/orders/create.rb +156 -0
- data/app/services/spree/orders/update.rb +51 -0
- data/app/services/spree/orders/upsert_items.rb +70 -0
- data/app/services/spree/prices/bulk_upsert.rb +201 -0
- data/app/services/spree/products/duplicator.rb +1 -1
- data/app/services/spree/products/prepare_nested_attributes.rb +2 -30
- data/app/services/spree/sample_data/loader.rb +30 -0
- data/app/services/spree/stock_reservations/extend.rb +19 -0
- data/app/services/spree/stock_reservations/release.rb +12 -0
- data/app/services/spree/stock_reservations/reserve.rb +103 -0
- data/app/services/spree/taxons/remove_products.rb +7 -1
- data/app/subscribers/spree/product_metrics_subscriber.rb +3 -7
- data/app/views/spree/invitation_mailer/invitation_email.html.erb +4 -0
- data/config/locales/en.yml +27 -10
- data/config/routes.rb +9 -0
- data/db/migrate/20260429000001_create_spree_order_cancellations.rb +25 -0
- data/db/migrate/20260429000002_create_spree_order_approvals.rb +22 -0
- data/db/migrate/20260429000003_add_status_to_spree_orders.rb +6 -0
- data/db/migrate/20260429000004_add_scopes_to_spree_api_keys.rb +11 -0
- data/db/migrate/20260501000001_create_spree_stock_reservations.rb +19 -0
- data/db/migrate/20260507162651_create_spree_variant_media.rb +23 -0
- data/db/migrate/20260508175303_add_pickup_to_spree_stock_locations.rb +12 -0
- data/db/migrate/20260508204040_create_spree_channels.rb +18 -0
- data/db/migrate/20260508204041_create_spree_order_routing_rules.rb +18 -0
- data/db/migrate/20260508204042_add_preferred_stock_location_to_spree_orders.rb +5 -0
- data/db/migrate/20260508204043_add_channel_id_to_spree_orders.rb +10 -0
- data/db/migrate/20260511000001_backfill_status_on_spree_orders.rb +57 -0
- data/db/migrate/20260515000001_add_store_id_to_spree_newsletter_subscribers.rb +25 -0
- data/db/migrate/20260529000001_add_unique_index_to_spree_price_rules.rb +41 -0
- data/db/migrate/20260529000002_add_unique_index_to_spree_promotion_rules.rb +37 -0
- data/db/migrate/20260601000001_create_spree_product_publications.rb +14 -0
- data/db/migrate/20260601000002_add_store_id_to_spree_products.rb +16 -0
- data/db/migrate/20260602000001_add_default_to_spree_channels.rb +14 -0
- data/db/sample_data/channels.rb +12 -0
- data/db/sample_data/orders.rb +1 -1
- data/db/sample_data/products.csv +212 -212
- data/lib/generators/spree/api_resource/api_resource_generator.rb +353 -0
- data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +23 -0
- data/lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt +59 -0
- data/lib/generators/spree/api_resource/templates/admin_serializer.rb.tt +11 -0
- data/lib/generators/spree/api_resource/templates/factory.rb.tt +26 -0
- data/lib/generators/spree/api_resource/templates/store_aliased_serializer.rb.tt +12 -0
- data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +31 -0
- data/lib/generators/spree/api_resource/templates/store_controller_spec.rb.tt +61 -0
- data/lib/generators/spree/api_resource/templates/store_serializer.rb.tt +14 -0
- data/lib/generators/spree/controller_decorator/controller_decorator_generator.rb +66 -0
- data/lib/generators/spree/controller_decorator/templates/controller_decorator.rb.tt +25 -0
- data/lib/generators/spree/model/model_generator.rb +73 -7
- data/lib/generators/spree/model/templates/create_table_migration.rb.tt +40 -0
- data/lib/generators/spree/model/templates/model.rb.tt +28 -2
- data/lib/spree/core/configuration.rb +7 -0
- data/lib/spree/core/controller_helpers/auth.rb +0 -12
- data/lib/spree/core/controller_helpers/currency.rb +0 -17
- data/lib/spree/core/controller_helpers/order.rb +0 -19
- data/lib/spree/core/dependencies.rb +5 -2
- data/lib/spree/core/engine.rb +54 -7
- data/lib/spree/core/permission_configuration.rb +15 -0
- data/lib/spree/core/preferences/masking.rb +47 -0
- data/lib/spree/core/preferences/preferable_class_methods.rb +7 -1
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +56 -5
- data/lib/spree/permitted_attributes.rb +9 -7
- data/lib/spree/testing_support/factories/address_factory.rb +16 -9
- data/lib/spree/testing_support/factories/api_key_factory.rb +1 -0
- data/lib/spree/testing_support/factories/channel_factory.rb +8 -0
- data/lib/spree/testing_support/factories/line_item_factory.rb +2 -8
- data/lib/spree/testing_support/factories/newsletter_subscriber_factory.rb +2 -0
- data/lib/spree/testing_support/factories/product_factory.rb +16 -7
- data/lib/spree/testing_support/factories/product_publication_factory.rb +6 -0
- data/lib/spree/testing_support/factories/refresh_token_factory.rb +15 -0
- data/lib/spree/testing_support/factories/stock_location_factory.rb +2 -2
- data/lib/spree/testing_support/factories/stock_reservation_factory.rb +31 -0
- data/lib/spree/testing_support/factories/variant_factory.rb +3 -3
- data/lib/spree/testing_support/order_walkthrough.rb +1 -1
- data/lib/spree/testing_support/store.rb +10 -0
- data/lib/spree/upgrades/5_4_to_5_5/manifest.yml +53 -0
- data/lib/tasks/channels.rake +94 -0
- data/lib/tasks/core.rake +1 -0
- data/lib/tasks/media.rake +27 -0
- data/lib/tasks/products.rake +4 -6
- data/lib/tasks/publications.rake +60 -0
- data/lib/tasks/upgrade.rake +211 -0
- metadata +83 -18
- data/app/finders/spree/variants/visible_finder.rb +0 -23
- data/app/paginators/spree/shared/paginate.rb +0 -30
- data/app/presenters/spree/filters/price_presenter.rb +0 -23
- data/app/presenters/spree/filters/price_range_presenter.rb +0 -30
- data/app/presenters/spree/filters/quantified_price_range_presenter.rb +0 -45
- data/app/presenters/spree/product_summary_presenter.rb +0 -27
- data/app/presenters/spree/variants/options_presenter.rb +0 -82
- data/app/services/spree/classifications/reposition.rb +0 -23
- data/app/sorters/spree/orders/sort.rb +0 -10
- data/lib/spree/core/controller_helpers/common.rb +0 -14
- data/lib/spree/core/token_generator.rb +0 -23
- data/lib/spree/database_type_utilities.rb +0 -22
- data/lib/spree/testing_support/bar_ability.rb +0 -14
- data/lib/spree/testing_support/factories/store_product_factory.rb +0 -6
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class StockReservation < Spree.base_class
|
|
3
|
+
has_prefix_id :res
|
|
4
|
+
|
|
5
|
+
publishes_lifecycle_events
|
|
6
|
+
|
|
7
|
+
belongs_to :stock_item, class_name: 'Spree::StockItem', inverse_of: :stock_reservations
|
|
8
|
+
belongs_to :line_item, class_name: 'Spree::LineItem', inverse_of: :stock_reservations
|
|
9
|
+
belongs_to :order, class_name: 'Spree::Order', inverse_of: :stock_reservations
|
|
10
|
+
|
|
11
|
+
validates :stock_item, :line_item, :order, :quantity, :expires_at, presence: true
|
|
12
|
+
validates :quantity, numericality: { greater_than: 0, only_integer: true }, presence: true
|
|
13
|
+
validates :line_item_id, uniqueness: { scope: :stock_item_id }, presence: true
|
|
14
|
+
|
|
15
|
+
scope :active, -> { where('spree_stock_reservations.expires_at > ?', Time.current) }
|
|
16
|
+
scope :expired, -> { where('spree_stock_reservations.expires_at <= ?', Time.current) }
|
|
17
|
+
scope :for_order, ->(order) { where(order_id: order.id) }
|
|
18
|
+
scope :for_store, ->(store) {
|
|
19
|
+
joins(:order).where(spree_orders: { store_id: store.id })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
self.whitelisted_ransackable_attributes = %w[stock_item_id line_item_id order_id quantity expires_at]
|
|
23
|
+
self.whitelisted_ransackable_associations = %w[stock_item line_item order]
|
|
24
|
+
|
|
25
|
+
def active?
|
|
26
|
+
expires_at > Time.current
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Resolves the reservation TTL: per-Store preference if set, otherwise
|
|
30
|
+
# the global Spree::Config[:default_stock_reservation_ttl_minutes]. Falls
|
|
31
|
+
# back to 10 minutes if both are unset (e.g. early-boot / fixture state).
|
|
32
|
+
def self.ttl_for(order)
|
|
33
|
+
minutes = order&.store&.preferred_stock_reservation_ttl_minutes
|
|
34
|
+
minutes = Spree::Config[:default_stock_reservation_ttl_minutes] if minutes.blank?
|
|
35
|
+
minutes.to_i.then { |m| m > 0 ? m : 10 }.minutes
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/app/models/spree/store.rb
CHANGED
|
@@ -12,8 +12,10 @@ module Spree
|
|
|
12
12
|
include Spree::Metadata
|
|
13
13
|
include Spree::Stores::Setup
|
|
14
14
|
include Spree::Stores::Markets
|
|
15
|
+
include Spree::Stores::Channels
|
|
15
16
|
include Spree::Security::Stores if defined?(Spree::Security::Stores)
|
|
16
17
|
include Spree::UserManagement
|
|
18
|
+
include Spree::OrderRouting::HasStrategyPreference
|
|
17
19
|
|
|
18
20
|
#
|
|
19
21
|
# Magic methods
|
|
@@ -46,6 +48,7 @@ module Spree
|
|
|
46
48
|
# Checkout preferences
|
|
47
49
|
preference :guest_checkout, :boolean, default: true
|
|
48
50
|
preference :special_instructions_enabled, :boolean, default: false
|
|
51
|
+
preference :stock_reservation_ttl_minutes, :integer, default: 10
|
|
49
52
|
# Address preferences
|
|
50
53
|
preference :company_field_enabled, :boolean, default: false
|
|
51
54
|
# digital assets preferences
|
|
@@ -54,6 +57,9 @@ module Spree
|
|
|
54
57
|
preference :digital_asset_authorized_clicks, :integer, default: 5
|
|
55
58
|
preference :digital_asset_authorized_days, :integer, default: 7
|
|
56
59
|
preference :digital_asset_link_expire_time, :integer, default: 300
|
|
60
|
+
# Class name of the Spree::OrderRouting::Strategy::Base subclass that
|
|
61
|
+
# decides which StockLocation fulfills which items.
|
|
62
|
+
preference :order_routing_strategy, :string, default: 'Spree::OrderRouting::Strategy::Rules'
|
|
57
63
|
|
|
58
64
|
#
|
|
59
65
|
# Associations
|
|
@@ -71,10 +77,11 @@ module Spree
|
|
|
71
77
|
has_many :store_payment_methods, class_name: 'Spree::StorePaymentMethod'
|
|
72
78
|
has_many :payment_methods, through: :store_payment_methods, class_name: 'Spree::PaymentMethod'
|
|
73
79
|
|
|
74
|
-
has_many :
|
|
75
|
-
has_many :
|
|
80
|
+
has_many :products, class_name: 'Spree::Product', dependent: :nullify
|
|
81
|
+
has_many :product_publications, through: :channels, source: :publications, class_name: 'Spree::ProductPublication'
|
|
76
82
|
has_many :variants, through: :products, class_name: 'Spree::Variant', source: :variants_including_master
|
|
77
83
|
has_many :stock_items, through: :variants, class_name: 'Spree::StockItem'
|
|
84
|
+
has_many :prices, through: :variants, class_name: 'Spree::Price'
|
|
78
85
|
has_many :inventory_units, through: :variants, class_name: 'Spree::InventoryUnit'
|
|
79
86
|
has_many :option_value_variants, through: :variants, class_name: 'Spree::OptionValueVariant'
|
|
80
87
|
has_many :customer_returns, class_name: 'Spree::CustomerReturn', inverse_of: :store
|
|
@@ -108,6 +115,9 @@ module Spree
|
|
|
108
115
|
has_many :webhook_endpoints, class_name: 'Spree::WebhookEndpoint', dependent: :destroy, inverse_of: :store
|
|
109
116
|
has_many :webhook_deliveries, through: :webhook_endpoints, class_name: 'Spree::WebhookDelivery'
|
|
110
117
|
|
|
118
|
+
has_many :channels, class_name: 'Spree::Channel', dependent: :destroy
|
|
119
|
+
has_many :order_routing_rules, through: :channels, class_name: 'Spree::OrderRoutingRule'
|
|
120
|
+
|
|
111
121
|
has_many :customer_groups, class_name: 'Spree::CustomerGroup', dependent: :destroy, inverse_of: :store
|
|
112
122
|
|
|
113
123
|
has_many :api_keys, class_name: 'Spree::ApiKey', dependent: :destroy
|
|
@@ -121,6 +131,7 @@ module Spree
|
|
|
121
131
|
end
|
|
122
132
|
validates :preferred_digital_asset_authorized_clicks, numericality: { only_integer: true, greater_than: 0 }
|
|
123
133
|
validates :preferred_digital_asset_authorized_days, numericality: { only_integer: true, greater_than: 0 }
|
|
134
|
+
validates :preferred_stock_reservation_ttl_minutes, numericality: { only_integer: true, greater_than: 0 }
|
|
124
135
|
validates :mail_from_address, email: { allow_blank: false }
|
|
125
136
|
# FIXME: we should remove this condition in v5
|
|
126
137
|
if !ENV['SPREE_DISABLE_DB_CONNECTION'] &&
|
|
@@ -141,7 +152,6 @@ module Spree
|
|
|
141
152
|
# Callbacks
|
|
142
153
|
before_validation :set_default_code, on: :create
|
|
143
154
|
before_save :ensure_default_exists_and_is_unique
|
|
144
|
-
after_create :ensure_default_market
|
|
145
155
|
after_create :create_default_policies
|
|
146
156
|
|
|
147
157
|
#
|
|
@@ -165,7 +175,7 @@ module Spree
|
|
|
165
175
|
else
|
|
166
176
|
Spree::Deprecation.warn(
|
|
167
177
|
'Spree::Store.default returning a new unpersisted store when no default store exists is deprecated ' \
|
|
168
|
-
'and will be removed in Spree
|
|
178
|
+
'and will be removed in Spree 6.0. Please ensure a default store is created before calling Store.default.'
|
|
169
179
|
)
|
|
170
180
|
new(default: true)
|
|
171
181
|
end
|
|
@@ -175,6 +185,10 @@ module Spree
|
|
|
175
185
|
Spree::Store.default&.supported_locales_list || []
|
|
176
186
|
end
|
|
177
187
|
|
|
188
|
+
def default_channel
|
|
189
|
+
channels.find_by(code: Spree::Channel::DEFAULT_CODE) || channels.active.first
|
|
190
|
+
end
|
|
191
|
+
|
|
178
192
|
# @deprecated Use Markets instead. Will be removed in Spree 5.5.
|
|
179
193
|
def checkout_zone
|
|
180
194
|
Spree::Deprecation.warn('Store#checkout_zone is deprecated and will be removed in Spree 5.5. Use Markets instead.')
|
|
@@ -366,25 +380,6 @@ module Spree
|
|
|
366
380
|
|
|
367
381
|
private
|
|
368
382
|
|
|
369
|
-
def ensure_default_market
|
|
370
|
-
return if markets.exists?
|
|
371
|
-
|
|
372
|
-
country = @default_country_for_market
|
|
373
|
-
return if country.blank?
|
|
374
|
-
|
|
375
|
-
iso_country = ISO3166::Country[country.iso]
|
|
376
|
-
|
|
377
|
-
Spree::Events.disable do
|
|
378
|
-
markets.create!(
|
|
379
|
-
name: country.name,
|
|
380
|
-
currency: iso_country&.currency_code || read_attribute(:default_currency) || 'USD',
|
|
381
|
-
default_locale: iso_country&.languages_official&.first || read_attribute(:default_locale) || 'en',
|
|
382
|
-
default: true,
|
|
383
|
-
countries: [country]
|
|
384
|
-
)
|
|
385
|
-
end
|
|
386
|
-
end
|
|
387
|
-
|
|
388
383
|
def create_default_policies
|
|
389
384
|
Spree::Events.disable do
|
|
390
385
|
[
|
|
@@ -401,55 +396,6 @@ module Spree
|
|
|
401
396
|
end
|
|
402
397
|
end
|
|
403
398
|
|
|
404
|
-
def ensure_default_taxonomies_are_created
|
|
405
|
-
Spree::Deprecation.warn('Store#ensure_default_taxonomies_are_created is deprecated and will be removed in Spree 5.5. Please remove it from your codebase')
|
|
406
|
-
|
|
407
|
-
Spree::Events.disable do
|
|
408
|
-
[
|
|
409
|
-
translate_with_store_locale_fallback('spree.taxonomy_categories_name'),
|
|
410
|
-
translate_with_store_locale_fallback('spree.taxonomy_brands_name'),
|
|
411
|
-
translate_with_store_locale_fallback('spree.taxonomy_collections_name')
|
|
412
|
-
].each do |taxonomy_name|
|
|
413
|
-
# Manual exists?/create to work around Mobility bug with find_or_create_by
|
|
414
|
-
next if taxonomies.with_matching_name(taxonomy_name).exists?
|
|
415
|
-
|
|
416
|
-
taxonomies.create(name: taxonomy_name)
|
|
417
|
-
end
|
|
418
|
-
end
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
def ensure_default_automatic_taxons
|
|
422
|
-
Spree::Deprecation.warn('Store#ensure_default_automatic_taxons is deprecated and will be removed in Spree 5.5. Please remove it from your codebase')
|
|
423
|
-
|
|
424
|
-
Spree::Events.disable do
|
|
425
|
-
# Use Mobility-safe lookup for taxonomy
|
|
426
|
-
collections_taxonomy = taxonomies.with_matching_name(translate_with_store_locale_fallback('spree.taxonomy_collections_name')).first
|
|
427
|
-
return unless collections_taxonomy.present?
|
|
428
|
-
|
|
429
|
-
automatic_taxons_config = [
|
|
430
|
-
{ name: translate_with_store_locale_fallback('spree.automatic_taxon_names.on_sale'),
|
|
431
|
-
rule_type: 'Spree::TaxonRules::Sale', rule_value: 'true' },
|
|
432
|
-
{ name: translate_with_store_locale_fallback('spree.automatic_taxon_names.new_arrivals'), rule_type: 'Spree::TaxonRules::AvailableOn', rule_value: 30 }
|
|
433
|
-
]
|
|
434
|
-
|
|
435
|
-
automatic_taxons_config.map do |config|
|
|
436
|
-
# Manual exists?/create to work around Mobility bug with first_or_create
|
|
437
|
-
taxon_scope = collections_taxonomy.taxons.automatic.with_matching_name(config[:name])
|
|
438
|
-
|
|
439
|
-
if taxon_scope.exists?
|
|
440
|
-
taxon_scope.first
|
|
441
|
-
else
|
|
442
|
-
collections_taxonomy.taxons.create!(
|
|
443
|
-
name: config[:name],
|
|
444
|
-
automatic: true,
|
|
445
|
-
parent: collections_taxonomy.root,
|
|
446
|
-
taxon_rules: [TaxonRule.new(type: config[:rule_type], value: config[:rule_value])]
|
|
447
|
-
)
|
|
448
|
-
end
|
|
449
|
-
end
|
|
450
|
-
end
|
|
451
|
-
end
|
|
452
|
-
|
|
453
399
|
# Translates a key using the store's default locale with fallback to :en
|
|
454
400
|
def translate_with_store_locale_fallback(key)
|
|
455
401
|
locale = default_locale.presence&.to_sym || :en
|
|
@@ -189,14 +189,6 @@ module Spree
|
|
|
189
189
|
"#{id}-SC-#{Time.now.utc.strftime('%Y%m%d%H%M%S%6N')}"
|
|
190
190
|
end
|
|
191
191
|
|
|
192
|
-
class << self
|
|
193
|
-
def default_created_by
|
|
194
|
-
Spree::Deprecation.warn('StoreCredit#default_created_by is deprecated and will be removed in Spree 5.5. Please use store.users.first instead.')
|
|
195
|
-
|
|
196
|
-
Spree::Store.current.users.first
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
|
|
200
192
|
private
|
|
201
193
|
|
|
202
194
|
def create_credit_record(amount, action_attributes = {})
|
|
@@ -1,29 +1,17 @@
|
|
|
1
1
|
module Spree
|
|
2
|
+
# Thin AR wrapper over the legacy +spree_products_stores+ join table.
|
|
3
|
+
# Pre-5.5 core used this table to attach products to stores; 5.5+ moved
|
|
4
|
+
# that responsibility onto +Spree::Product#store_id+ + +ProductPublication+.
|
|
5
|
+
#
|
|
6
|
+
# The model exists only to power the 5.4 → 5.5 backfill rake task
|
|
7
|
+
# (+spree:upgrade:populate_publications+). Host apps upgrading from 5.4
|
|
8
|
+
# still have the table; after the backfill runs, +spree_multi_store+ (for
|
|
9
|
+
# multi-store catalogs) keeps the table around, and single-store
|
|
10
|
+
# installations may drop it.
|
|
2
11
|
class StoreProduct < Spree.base_class
|
|
3
|
-
has_prefix_id :sp
|
|
4
|
-
|
|
5
12
|
self.table_name = 'spree_products_stores'
|
|
6
13
|
|
|
7
|
-
belongs_to :
|
|
8
|
-
belongs_to :
|
|
9
|
-
|
|
10
|
-
validates :store, :product, presence: true
|
|
11
|
-
validates :store_id, uniqueness: { scope: :product_id }
|
|
12
|
-
|
|
13
|
-
def refresh_metrics!
|
|
14
|
-
return if product.nil?
|
|
15
|
-
|
|
16
|
-
completed_order_ids = product.completed_orders.where(store_id: store_id).select(:id)
|
|
17
|
-
variant_ids = product.variants_including_master.ids
|
|
18
|
-
|
|
19
|
-
line_items = Spree::LineItem.joins(:order)
|
|
20
|
-
.where(spree_orders: { id: completed_order_ids })
|
|
21
|
-
.where(variant_id: variant_ids)
|
|
22
|
-
|
|
23
|
-
update!(
|
|
24
|
-
units_sold_count: line_items.sum(:quantity),
|
|
25
|
-
revenue: line_items.sum(:pre_tax_amount)
|
|
26
|
-
)
|
|
27
|
-
end
|
|
14
|
+
belongs_to :product, class_name: 'Spree::Product'
|
|
15
|
+
belongs_to :store, class_name: 'Spree::Store'
|
|
28
16
|
end
|
|
29
17
|
end
|
data/app/models/spree/taxon.rb
CHANGED
|
@@ -340,11 +340,6 @@ module Spree
|
|
|
340
340
|
end
|
|
341
341
|
end
|
|
342
342
|
|
|
343
|
-
def active_products
|
|
344
|
-
Spree::Deprecation.warn('active_products is deprecated and will be removed in Spree 5.5. Please use taxon.products.active instead.')
|
|
345
|
-
products.active
|
|
346
|
-
end
|
|
347
|
-
|
|
348
343
|
def regenerate_pretty_name_and_permalink
|
|
349
344
|
Mobility.with_locale(nil) do
|
|
350
345
|
update_columns(pretty_name: generate_pretty_name, permalink: generate_slug, updated_at: Time.current)
|
|
@@ -9,8 +9,7 @@ module Spree
|
|
|
9
9
|
|
|
10
10
|
validates :provider, inclusion: {
|
|
11
11
|
in: ->(_record) {
|
|
12
|
-
|
|
13
|
-
(config.store_authentication_strategies.keys + config.admin_authentication_strategies.keys).uniq.map(&:to_s)
|
|
12
|
+
(Spree.store_authentication_strategies.keys + Spree.admin_authentication_strategies.keys).uniq.map(&:to_s)
|
|
14
13
|
}
|
|
15
14
|
}
|
|
16
15
|
|
data/app/models/spree/variant.rb
CHANGED
|
@@ -8,6 +8,7 @@ module Spree
|
|
|
8
8
|
include Spree::MemoizedData
|
|
9
9
|
include Spree::Metafields
|
|
10
10
|
include Spree::Metadata
|
|
11
|
+
include Spree::Searchable
|
|
11
12
|
include Spree::Variant::Webhooks
|
|
12
13
|
|
|
13
14
|
publishes_lifecycle_events
|
|
@@ -36,31 +37,43 @@ module Spree
|
|
|
36
37
|
with_options inverse_of: :variant do
|
|
37
38
|
has_many :inventory_units
|
|
38
39
|
has_many :line_items
|
|
39
|
-
has_many :stock_items, dependent: :destroy
|
|
40
|
+
has_many :stock_items, dependent: :destroy, autosave: true
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
has_many :orders, through: :line_items
|
|
43
44
|
with_options through: :stock_items do
|
|
44
45
|
has_many :stock_locations
|
|
45
46
|
has_many :stock_movements
|
|
47
|
+
has_many :stock_reservations
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
has_many :option_value_variants, class_name: 'Spree::OptionValueVariant'
|
|
49
51
|
has_many :option_values, through: :option_value_variants, dependent: :destroy, class_name: 'Spree::OptionValue'
|
|
50
52
|
|
|
51
53
|
has_many :images, -> { order(:position) }, as: :viewable, dependent: :destroy, class_name: 'Spree::Asset'
|
|
54
|
+
|
|
55
|
+
has_many :variant_media, class_name: 'Spree::VariantMedia', dependent: :destroy
|
|
56
|
+
# Order through the asset's product-level position so a variant's gallery
|
|
57
|
+
# follows whatever ordering the merchant set on the product. There's no
|
|
58
|
+
# per-variant reordering — link/unlink only.
|
|
59
|
+
has_many :associated_media,
|
|
60
|
+
-> { order(Spree::Asset.arel_table[:position].asc) },
|
|
61
|
+
through: :variant_media, source: :asset, class_name: 'Spree::Asset'
|
|
62
|
+
|
|
52
63
|
belongs_to :primary_media, class_name: 'Spree::Asset', optional: true, foreign_key: :primary_media_id
|
|
53
64
|
|
|
54
65
|
has_many :prices,
|
|
55
66
|
class_name: 'Spree::Price',
|
|
56
67
|
dependent: :destroy,
|
|
57
|
-
inverse_of: :variant
|
|
68
|
+
inverse_of: :variant,
|
|
69
|
+
autosave: true
|
|
58
70
|
|
|
59
71
|
has_many :wished_items, dependent: :destroy
|
|
60
72
|
|
|
61
73
|
has_many :digitals
|
|
62
74
|
|
|
63
75
|
before_validation :set_cost_currency
|
|
76
|
+
before_validation :apply_pending_options, if: :pending_options?
|
|
64
77
|
|
|
65
78
|
validate :check_price, if: -> { Spree::Config.enable_legacy_default_price }
|
|
66
79
|
|
|
@@ -128,11 +141,29 @@ module Spree
|
|
|
128
141
|
|
|
129
142
|
scope :with_digital_assets, -> { joins(:digitals) }
|
|
130
143
|
|
|
131
|
-
|
|
132
|
-
|
|
144
|
+
# Free-text variant search: SKU, parent product name, and any
|
|
145
|
+
# option-value presentation (e.g. "Red", "XL"). The 3-char floor
|
|
146
|
+
# keeps single-letter queries from triggering a full scan.
|
|
147
|
+
def self.search(query)
|
|
148
|
+
return none if query.blank? || query.length < 3
|
|
133
149
|
|
|
134
|
-
|
|
135
|
-
|
|
150
|
+
conditions = [
|
|
151
|
+
search_condition(self, :sku, query),
|
|
152
|
+
search_condition(Spree::OptionValue, :presentation, query),
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
if Spree.use_translations?
|
|
156
|
+
translation_table = Product::Translation.arel_table.alias(Product.translation_table_alias)
|
|
157
|
+
sanitized = sanitize_query_for_search(query)
|
|
158
|
+
conditions << translation_table[:name].lower.matches("%#{sanitized}%", '\\')
|
|
159
|
+
else
|
|
160
|
+
conditions << search_condition(Spree::Product, :name, query)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
relation = joins(:product).left_joins(:option_values)
|
|
164
|
+
relation = relation.join_translation_table(Product) if Spree.use_translations?
|
|
165
|
+
relation.where(conditions.reduce(:or)).distinct
|
|
166
|
+
end
|
|
136
167
|
|
|
137
168
|
# Backward compatibility alias — remove in Spree 6.0
|
|
138
169
|
scope :multi_search, ->(*args) { search(*args) }
|
|
@@ -166,8 +197,8 @@ module Spree
|
|
|
166
197
|
|
|
167
198
|
self.whitelisted_ransackable_associations = %w[option_values product tax_category prices default_price]
|
|
168
199
|
self.whitelisted_ransackable_attributes = %w[weight depth width height sku discontinue_on is_master cost_price cost_currency track_inventory
|
|
169
|
-
deleted_at]
|
|
170
|
-
self.whitelisted_ransackable_scopes = %i(product_name_or_sku_cont search_by_product_name_or_sku)
|
|
200
|
+
deleted_at product_id]
|
|
201
|
+
self.whitelisted_ransackable_scopes = %i(product_name_or_sku_cont search_by_product_name_or_sku search)
|
|
171
202
|
|
|
172
203
|
def self.product_name_or_sku_cont(query)
|
|
173
204
|
sanitized_query = ActiveRecord::Base.sanitize_sql_like(query.to_s.downcase.strip)
|
|
@@ -260,16 +291,21 @@ module Spree
|
|
|
260
291
|
end
|
|
261
292
|
|
|
262
293
|
# Returns the variant's media gallery.
|
|
263
|
-
#
|
|
294
|
+
# Prefers product-level media linked via variant_media (5.5+) — these reuse
|
|
295
|
+
# a single blob across variants. Falls back to direct variant images for
|
|
296
|
+
# legacy uploads.
|
|
264
297
|
# @return [ActiveRecord::Relation]
|
|
265
298
|
def gallery_media
|
|
299
|
+
return associated_media if has_associated_media?
|
|
300
|
+
|
|
266
301
|
images
|
|
267
302
|
end
|
|
268
303
|
|
|
269
|
-
# Returns true if the variant has media.
|
|
270
|
-
# Uses loaded
|
|
304
|
+
# Returns true if the variant has media (linked product-level or direct images).
|
|
305
|
+
# Uses loaded associations when available, otherwise falls back to counter cache.
|
|
271
306
|
# @return [Boolean]
|
|
272
307
|
def has_media?
|
|
308
|
+
return true if has_associated_media?
|
|
273
309
|
return images.any? if images.loaded?
|
|
274
310
|
|
|
275
311
|
media_count.positive?
|
|
@@ -277,6 +313,13 @@ module Spree
|
|
|
277
313
|
|
|
278
314
|
alias has_images? has_media?
|
|
279
315
|
|
|
316
|
+
# @return [Boolean] true if any product-level media is linked to this variant
|
|
317
|
+
def has_associated_media?
|
|
318
|
+
return variant_media.any? if variant_media.loaded?
|
|
319
|
+
|
|
320
|
+
variant_media.exists?
|
|
321
|
+
end
|
|
322
|
+
|
|
280
323
|
# @deprecated Use #primary_media instead.
|
|
281
324
|
def default_image
|
|
282
325
|
Spree::Deprecation.warn('Spree::Variant#default_image is deprecated and will be removed in Spree 6.0. Please use Spree::Variant#primary_media instead.')
|
|
@@ -285,8 +328,10 @@ module Spree
|
|
|
285
328
|
|
|
286
329
|
# Updates primary_media_id to the first media item by position.
|
|
287
330
|
# Called when media is added, removed, or reordered.
|
|
331
|
+
# Uses gallery_media so product-level assets linked via VariantMedia are
|
|
332
|
+
# considered alongside legacy variant-pinned images.
|
|
288
333
|
def update_thumbnail!
|
|
289
|
-
first_media =
|
|
334
|
+
first_media = gallery_media.first
|
|
290
335
|
update_column(:primary_media_id, first_media&.id)
|
|
291
336
|
end
|
|
292
337
|
|
|
@@ -330,6 +375,11 @@ module Spree
|
|
|
330
375
|
# @param options [Array<Hash>] the options to set
|
|
331
376
|
# @return [void]
|
|
332
377
|
def options=(options = {})
|
|
378
|
+
if product.nil?
|
|
379
|
+
@pending_options = options
|
|
380
|
+
return
|
|
381
|
+
end
|
|
382
|
+
|
|
333
383
|
options.each do |option|
|
|
334
384
|
next if option[:name].blank? || option[:value].blank?
|
|
335
385
|
|
|
@@ -438,6 +488,51 @@ module Spree
|
|
|
438
488
|
price_in(currency).try(:compare_at_amount)
|
|
439
489
|
end
|
|
440
490
|
|
|
491
|
+
# Syncs base prices from an array of hashes.
|
|
492
|
+
# Upserts prices for listed currencies, removes base prices for unlisted currencies.
|
|
493
|
+
# On new records, builds prices in memory (saved when variant is saved).
|
|
494
|
+
# On persisted records, saves prices immediately and removes unlisted currencies.
|
|
495
|
+
# An empty array clears every base price — distinguished from `nil` (no
|
|
496
|
+
# change requested), which falls through to the default ActiveRecord setter.
|
|
497
|
+
# @param prices_params [Array<Hash>, nil] array of { currency:, amount:, compare_at_amount: }
|
|
498
|
+
# @return [void]
|
|
499
|
+
def prices=(prices_params)
|
|
500
|
+
return super if prices_params.nil? || prices_params.first.is_a?(Spree::Price)
|
|
501
|
+
|
|
502
|
+
currencies_in_payload = []
|
|
503
|
+
|
|
504
|
+
prices_params.each do |price_data|
|
|
505
|
+
price_data = price_data.to_h.with_indifferent_access
|
|
506
|
+
currencies_in_payload << price_data[:currency]
|
|
507
|
+
set_price(price_data[:currency], price_data[:amount], price_data[:compare_at_amount])
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# Remove base prices for currencies not in the payload (including the
|
|
511
|
+
# `prices_params == []` case, which clears every base price).
|
|
512
|
+
prices.base_prices.where.not(currency: currencies_in_payload).destroy_all if persisted?
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# Syncs stock items from an array of hashes.
|
|
516
|
+
# Upserts stock for listed locations, soft-deletes stock items for unlisted locations.
|
|
517
|
+
# On new records, defers to after_create callback.
|
|
518
|
+
# @param stock_items_params [Array<Hash>] array of { stock_location_id:, count_on_hand:, backorderable: }
|
|
519
|
+
# @return [void]
|
|
520
|
+
def stock_items=(stock_items_params)
|
|
521
|
+
return super if stock_items_params.blank? || stock_items_params.first.is_a?(Spree::StockItem)
|
|
522
|
+
|
|
523
|
+
location_ids_in_payload = []
|
|
524
|
+
|
|
525
|
+
stock_items_params.each do |stock_data|
|
|
526
|
+
stock_data = stock_data.to_h.with_indifferent_access
|
|
527
|
+
location = Spree::StockLocation.find_by_param(stock_data[:stock_location_id])
|
|
528
|
+
location_ids_in_payload << location.id
|
|
529
|
+
set_stock(stock_data[:count_on_hand], stock_data[:backorderable], location)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Soft-delete stock items for locations not in the payload
|
|
533
|
+
stock_items.where.not(stock_location_id: location_ids_in_payload).destroy_all if persisted?
|
|
534
|
+
end
|
|
535
|
+
|
|
441
536
|
# Sets the base price (global price, not for a price list) for the given currency.
|
|
442
537
|
# @param currency [String] the currency to set the price for
|
|
443
538
|
# @param amount [BigDecimal] the amount to set
|
|
@@ -465,16 +560,18 @@ module Spree
|
|
|
465
560
|
Spree::Pricing::Resolver.new(context).resolve
|
|
466
561
|
end
|
|
467
562
|
|
|
468
|
-
# Sets the stock for the variant
|
|
563
|
+
# Sets the stock for the variant at a given location.
|
|
564
|
+
# Mirrors set_price: find-or-initialize, set attrs, save only if persisted.
|
|
469
565
|
# @param count_on_hand [Integer] the count on hand
|
|
470
566
|
# @param backorderable [Boolean] the backorderable flag
|
|
471
|
-
# @param stock_location [Spree::StockLocation] the stock location to
|
|
567
|
+
# @param stock_location [Spree::StockLocation] the stock location (defaults to store default)
|
|
472
568
|
# @return [void]
|
|
473
|
-
def set_stock(count_on_hand, backorderable = nil)
|
|
474
|
-
|
|
569
|
+
def set_stock(count_on_hand, backorderable = nil, stock_location = nil)
|
|
570
|
+
stock_location ||= default_stock_location
|
|
571
|
+
stock_item = stock_items.find_or_initialize_by(stock_location: stock_location)
|
|
475
572
|
stock_item.count_on_hand = count_on_hand
|
|
476
573
|
stock_item.backorderable = backorderable if backorderable.present?
|
|
477
|
-
stock_item.save!
|
|
574
|
+
stock_item.save! if persisted?
|
|
478
575
|
end
|
|
479
576
|
|
|
480
577
|
def default_stock_location
|
|
@@ -532,7 +629,7 @@ module Spree
|
|
|
532
629
|
@on_sale ||= price_in(currency)&.discounted?
|
|
533
630
|
end
|
|
534
631
|
|
|
535
|
-
delegate :total_on_hand, :can_supply?, to: :quantifier
|
|
632
|
+
delegate :total_on_hand, :available_stock, :reserved_quantity, :can_supply?, to: :quantifier
|
|
536
633
|
|
|
537
634
|
alias is_backorderable? backorderable?
|
|
538
635
|
|
|
@@ -585,6 +682,23 @@ module Spree
|
|
|
585
682
|
|
|
586
683
|
private
|
|
587
684
|
|
|
685
|
+
def pending_options?
|
|
686
|
+
@pending_options.present?
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def apply_pending_options
|
|
690
|
+
return unless @pending_options
|
|
691
|
+
|
|
692
|
+
options_to_apply = @pending_options
|
|
693
|
+
@pending_options = nil
|
|
694
|
+
|
|
695
|
+
options_to_apply.each do |option|
|
|
696
|
+
next if option[:name].blank? || option[:value].blank?
|
|
697
|
+
|
|
698
|
+
set_option_value(option[:name], option[:value], option[:position])
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
|
|
588
702
|
def ensure_not_in_complete_orders
|
|
589
703
|
if orders.complete.any?
|
|
590
704
|
errors.add(:base, :cannot_destroy_if_attached_to_line_items)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# FK column is `media_id` (not `asset_id`) to match the 6.0 rename
|
|
3
|
+
# Spree::Asset → Spree::Media. The `:asset` association name follows the
|
|
4
|
+
# current parent class; in 6.0 it renames to `:media` without a column change.
|
|
5
|
+
class VariantMedia < Spree.base_class
|
|
6
|
+
self.table_name = 'spree_variant_media'
|
|
7
|
+
|
|
8
|
+
belongs_to :variant, class_name: 'Spree::Variant', touch: true
|
|
9
|
+
belongs_to :asset, class_name: 'Spree::Asset', foreign_key: :media_id, inverse_of: :variant_media
|
|
10
|
+
|
|
11
|
+
validates :variant, :asset, presence: true
|
|
12
|
+
validates :media_id, uniqueness: { scope: :variant_id }
|
|
13
|
+
validate :asset_belongs_to_variant_product
|
|
14
|
+
|
|
15
|
+
after_commit :refresh_variant_thumbnail, on: %i[create destroy]
|
|
16
|
+
|
|
17
|
+
# Resolves an array of variant identifiers (prefixed ids or raw ids) to the
|
|
18
|
+
# numeric ids of variants that belong to `product`. Anything else — bad
|
|
19
|
+
# prefix, foreign product, garbage — is dropped. This is the security
|
|
20
|
+
# boundary used by Spree::Asset#variant_ids=, so callers (forms, API params)
|
|
21
|
+
# can't link assets to variants from another product.
|
|
22
|
+
def self.resolve_variant_ids(product, variant_ids)
|
|
23
|
+
ids = Array(variant_ids).reject(&:blank?)
|
|
24
|
+
return [] if ids.empty?
|
|
25
|
+
|
|
26
|
+
product.variants.filter_map do |variant|
|
|
27
|
+
token = variant.id.to_s
|
|
28
|
+
prefixed = variant.prefixed_id
|
|
29
|
+
variant.id if ids.any? { |id| id.to_s == token || id == prefixed }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def asset_belongs_to_variant_product
|
|
36
|
+
return if asset.blank? || variant.blank?
|
|
37
|
+
return if asset.product&.id == variant.product_id
|
|
38
|
+
|
|
39
|
+
errors.add(:asset, 'must belong to the same product as the variant')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def refresh_variant_thumbnail
|
|
43
|
+
variant&.update_thumbnail!
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -19,7 +19,7 @@ module Spree
|
|
|
19
19
|
scope :for_event, ->(event_name) { where(event_name: event_name) }
|
|
20
20
|
|
|
21
21
|
# Ransack configuration
|
|
22
|
-
self.whitelisted_ransackable_attributes = %w[event_name response_code execution_time success delivered_at]
|
|
22
|
+
self.whitelisted_ransackable_attributes = %w[event_name response_code execution_time success delivered_at created_at]
|
|
23
23
|
|
|
24
24
|
# Check if the delivery was successful
|
|
25
25
|
#
|
|
@@ -22,6 +22,12 @@ module Spree
|
|
|
22
22
|
validate :url_must_not_resolve_to_private_ip, if: -> { !Rails.env.development? && url.present? && url_changed? }
|
|
23
23
|
|
|
24
24
|
before_create :generate_secret_key
|
|
25
|
+
after_create { @reveal_secret_in_response = true }
|
|
26
|
+
# Re-enabling via a direct `update(active: true)` (e.g., the dashboard's
|
|
27
|
+
# edit form) must also clear the auto-disable bookkeeping so the endpoint
|
|
28
|
+
# rejoins the `enabled` scope. `#enable!` handles this too, but we can't
|
|
29
|
+
# rely on every call site using it.
|
|
30
|
+
before_save :clear_disabled_state_when_reactivated
|
|
25
31
|
|
|
26
32
|
self.whitelisted_ransackable_attributes = %w[name url active]
|
|
27
33
|
|
|
@@ -29,6 +35,17 @@ module Spree
|
|
|
29
35
|
scope :inactive, -> { where(active: false) }
|
|
30
36
|
scope :enabled, -> { active.where(disabled_at: nil) }
|
|
31
37
|
|
|
38
|
+
# Returns the plaintext `secret_key` only on the create response.
|
|
39
|
+
#
|
|
40
|
+
# `@reveal_secret_in_response` is set by the `after_create` callback above
|
|
41
|
+
# — a per-instance flag, not derived from `previous_changes`, so a reload
|
|
42
|
+
# or any subsequent save can't accidentally re-expose the secret.
|
|
43
|
+
#
|
|
44
|
+
# @return [String, nil]
|
|
45
|
+
def secret_key_for_response
|
|
46
|
+
@reveal_secret_in_response ? secret_key : nil
|
|
47
|
+
end
|
|
48
|
+
|
|
32
49
|
# Number of consecutive failed deliveries before auto-disabling
|
|
33
50
|
AUTO_DISABLE_THRESHOLD = 15
|
|
34
51
|
|
|
@@ -128,6 +145,13 @@ module Spree
|
|
|
128
145
|
self.secret_key ||= SecureRandom.hex(32)
|
|
129
146
|
end
|
|
130
147
|
|
|
148
|
+
def clear_disabled_state_when_reactivated
|
|
149
|
+
return unless will_save_change_to_active? && active
|
|
150
|
+
|
|
151
|
+
self.disabled_at = nil
|
|
152
|
+
self.disabled_reason = nil
|
|
153
|
+
end
|
|
154
|
+
|
|
131
155
|
def url_must_not_resolve_to_private_ip
|
|
132
156
|
uri = URI.parse(url)
|
|
133
157
|
blacklist = SsrfFilter::IPV4_BLACKLIST + SsrfFilter::IPV6_BLACKLIST
|