spree_core 5.4.2 → 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 +3 -2
- 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/publishable.rb +1 -1
- 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 +37 -14
- 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/event.rb +6 -6
- data/app/models/spree/export.rb +32 -5
- data/app/models/spree/exports/product_translations.rb +1 -1
- data/app/models/spree/gateway/bogus.rb +6 -1
- 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 +7 -2
- data/app/models/spree/market.rb +57 -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_inventory.rb +24 -2
- 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/payment_setup_sessions/bogus.rb +4 -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/shipment.rb +10 -4
- 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/subscriber.rb +12 -12
- 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/formula_sanitizer.rb +28 -0
- 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/credit_cards/destroy.rb +1 -1
- 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/payments/handle_webhook.rb +3 -10
- 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/event_log_subscriber.rb +1 -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 +35 -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/20260504103113_add_type_to_spree_payment_setup_sessions.rb +6 -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/events/adapters/active_support_notifications.rb +1 -1
- data/lib/spree/events/adapters/base.rb +3 -3
- data/lib/spree/events/registry.rb +1 -1
- data/lib/spree/events.rb +1 -1
- 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 +86 -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
|
@@ -11,10 +11,11 @@ module Spree
|
|
|
11
11
|
|
|
12
12
|
belongs_to :store, class_name: 'Spree::Store'
|
|
13
13
|
|
|
14
|
-
has_many :price_rules, class_name: 'Spree::PriceRule', dependent: :destroy
|
|
14
|
+
has_many :price_rules, class_name: 'Spree::PriceRule', autosave: true, dependent: :destroy
|
|
15
|
+
alias rules price_rules
|
|
15
16
|
has_many :prices, class_name: 'Spree::Price', dependent: :destroy_async
|
|
16
|
-
has_many :variants, -> {
|
|
17
|
-
has_many :products, -> {
|
|
17
|
+
has_many :variants, -> { distinct }, through: :prices, source: :variant
|
|
18
|
+
has_many :products, -> { distinct }, through: :variants, source: :product
|
|
18
19
|
alias price_list_products products
|
|
19
20
|
|
|
20
21
|
# Override default nested attributes to use bulk_update_prices for performance
|
|
@@ -28,20 +29,59 @@ module Spree
|
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
after_update :process_bulk_prices_update
|
|
32
|
+
after_save :apply_pending_rules
|
|
33
|
+
after_save :apply_pending_product_ids
|
|
34
|
+
after_save :apply_pending_prices
|
|
35
|
+
|
|
36
|
+
# @return [Array<String>] prefixed product ids in this list,
|
|
37
|
+
# encoded inline to avoid hydrating N Product records.
|
|
38
|
+
def product_prefixed_ids
|
|
39
|
+
prefix = Spree::Product._prefix_id_prefix
|
|
40
|
+
product_ids.sort.map { |pk| "#{prefix}_#{Spree::PrefixedId::SQIDS.encode([pk])}" }
|
|
41
|
+
end
|
|
31
42
|
|
|
32
|
-
#
|
|
43
|
+
# Reconciles list membership. Removes prices for products no longer
|
|
44
|
+
# in `ids` and adds placeholder prices for the new ones.
|
|
45
|
+
#
|
|
46
|
+
# @param ids [Array<String>] raw product PKs (prefixed strings are
|
|
47
|
+
# resolved upstream by `Spree::Base#assign_attributes`).
|
|
33
48
|
# @return [void]
|
|
34
|
-
def
|
|
35
|
-
|
|
49
|
+
def product_ids=(ids)
|
|
50
|
+
@pending_product_ids = Array(ids).compact.uniq
|
|
51
|
+
end
|
|
36
52
|
|
|
37
|
-
|
|
38
|
-
|
|
53
|
+
# Flat-payload writer for `prices`. Bulk-upserts the listed rows in
|
|
54
|
+
# `after_save` so newly-added products have their placeholder rows
|
|
55
|
+
# materialized first. Nullability contract:
|
|
56
|
+
# - `nil` → no-op
|
|
57
|
+
# - `[]` → clear every override on this list
|
|
58
|
+
# - `[…]` → upsert listed rows, leave the rest alone
|
|
59
|
+
#
|
|
60
|
+
# @param rows [Array<Hash>, Array<Spree::Price>, nil]
|
|
61
|
+
# @return [void]
|
|
62
|
+
def prices=(rows)
|
|
63
|
+
first = Array(rows).first
|
|
64
|
+
return super(rows) if first.is_a?(Spree::Price)
|
|
65
|
+
return if rows.nil?
|
|
66
|
+
|
|
67
|
+
@pending_prices = Array(rows).map do |row|
|
|
68
|
+
row.respond_to?(:to_unsafe_h) ? row.to_unsafe_h.with_indifferent_access : row.with_indifferent_access
|
|
69
|
+
end
|
|
70
|
+
@pending_prices_clear = rows.empty?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Flat-payload writer for `rules`. See
|
|
74
|
+
# {Spree::TypedAssociations#assign_typed_association}.
|
|
75
|
+
def rules=(rows)
|
|
76
|
+
assign_typed_association(:price_rules, rows)
|
|
39
77
|
end
|
|
40
78
|
|
|
41
79
|
validates :name, :store, presence: true
|
|
42
80
|
validates :match_policy, presence: true, inclusion: { in: MATCH_POLICIES }
|
|
43
81
|
validate :starts_at_before_ends_at
|
|
44
82
|
|
|
83
|
+
self.whitelisted_ransackable_attributes = %w[status match_policy starts_at ends_at]
|
|
84
|
+
|
|
45
85
|
scope :by_position, -> { order(position: :asc) }
|
|
46
86
|
scope :for_store, ->(store) { where(store: store) }
|
|
47
87
|
scope :current, lambda { |timezone = nil|
|
|
@@ -119,9 +159,10 @@ module Spree
|
|
|
119
159
|
active_or_scheduled? && within_date_range?(Time.current)
|
|
120
160
|
end
|
|
121
161
|
|
|
122
|
-
# Adds products to the price
|
|
123
|
-
#
|
|
124
|
-
#
|
|
162
|
+
# Adds products to the list, materializing a placeholder price
|
|
163
|
+
# (amount nil) for every variant × store currency.
|
|
164
|
+
#
|
|
165
|
+
# @param product_ids [Array<String>] raw product PKs
|
|
125
166
|
# @return [void]
|
|
126
167
|
def add_products(product_ids)
|
|
127
168
|
return if product_ids.blank?
|
|
@@ -130,7 +171,6 @@ module Spree
|
|
|
130
171
|
variant_ids = Spree::Variant.eligible.where(product_id: product_ids).distinct.pluck(:id)
|
|
131
172
|
return if variant_ids.empty?
|
|
132
173
|
|
|
133
|
-
# Get existing variant_id/currency combinations to avoid duplicates
|
|
134
174
|
existing = prices.where(variant_id: variant_ids)
|
|
135
175
|
.pluck(:variant_id, :currency)
|
|
136
176
|
.to_set
|
|
@@ -160,9 +200,12 @@ module Spree
|
|
|
160
200
|
touch
|
|
161
201
|
end
|
|
162
202
|
|
|
163
|
-
# Removes products from the
|
|
164
|
-
#
|
|
165
|
-
#
|
|
203
|
+
# Removes products from the list. Hard-deletes their prices so the
|
|
204
|
+
# unique index doesn't block re-adding the same products later
|
|
205
|
+
# (acts_as_paranoid would leave soft-deleted rows blocking the
|
|
206
|
+
# `(variant_id, currency, price_list_id)` slot).
|
|
207
|
+
#
|
|
208
|
+
# @param product_ids [Array<String>] raw product PKs
|
|
166
209
|
# @return [void]
|
|
167
210
|
def remove_products(product_ids)
|
|
168
211
|
return if product_ids.blank?
|
|
@@ -195,7 +238,12 @@ module Spree
|
|
|
195
238
|
next if attrs[:id].blank?
|
|
196
239
|
|
|
197
240
|
price_id = attrs[:id].to_i
|
|
198
|
-
|
|
241
|
+
# Reject rows that aren't in *this* list's prices — `upsert_all`
|
|
242
|
+
# otherwise keys solely by primary id and would silently cross
|
|
243
|
+
# list boundaries.
|
|
244
|
+
next unless current_values.key?(price_id)
|
|
245
|
+
|
|
246
|
+
current = current_values[price_id]
|
|
199
247
|
|
|
200
248
|
# Parse amounts using LocalizedNumber for proper decimal handling
|
|
201
249
|
amount = attrs[:amount].present? ? Spree::LocalizedNumber.parse(attrs[:amount]) : nil
|
|
@@ -222,7 +270,7 @@ module Spree
|
|
|
222
270
|
return true if records_to_upsert.empty?
|
|
223
271
|
|
|
224
272
|
opts = { update_only: [:amount, :compare_at_amount], on_duplicate: :update }
|
|
225
|
-
opts[:unique_by] = :id unless
|
|
273
|
+
opts[:unique_by] = :id unless mysql_adapter?
|
|
226
274
|
|
|
227
275
|
Spree::Price.upsert_all(records_to_upsert, **opts)
|
|
228
276
|
|
|
@@ -232,6 +280,70 @@ module Spree
|
|
|
232
280
|
|
|
233
281
|
private
|
|
234
282
|
|
|
283
|
+
# Processes the bulk prices update
|
|
284
|
+
# @return [void]
|
|
285
|
+
def process_bulk_prices_update
|
|
286
|
+
return if @prices_attributes.blank?
|
|
287
|
+
|
|
288
|
+
bulk_update_prices(@prices_attributes)
|
|
289
|
+
@prices_attributes = nil
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def apply_pending_rules
|
|
293
|
+
flush_pending_typed_association(:price_rules)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def apply_pending_product_ids
|
|
297
|
+
return unless @pending_product_ids
|
|
298
|
+
|
|
299
|
+
desired = @pending_product_ids
|
|
300
|
+
@pending_product_ids = nil
|
|
301
|
+
|
|
302
|
+
current = product_ids
|
|
303
|
+
to_remove = current - desired
|
|
304
|
+
to_add = desired - current
|
|
305
|
+
|
|
306
|
+
remove_products(to_remove) if to_remove.any?
|
|
307
|
+
add_products(to_add) if to_add.any?
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def apply_pending_prices
|
|
311
|
+
pending = @pending_prices
|
|
312
|
+
cleared = @pending_prices_clear
|
|
313
|
+
return if pending.nil?
|
|
314
|
+
|
|
315
|
+
@pending_prices = nil
|
|
316
|
+
@pending_prices_clear = nil
|
|
317
|
+
|
|
318
|
+
if cleared
|
|
319
|
+
variant_ids = prices.distinct.pluck(:variant_id)
|
|
320
|
+
prices.update_all(amount: nil, compare_at_amount: nil, updated_at: Time.current)
|
|
321
|
+
touch_variants(variant_ids)
|
|
322
|
+
return
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
rows = pending.filter_map do |row|
|
|
326
|
+
# `variant_id` may arrive as a prefixed string (legacy callers,
|
|
327
|
+
# console) or already decoded (the controller's `permitted_params`
|
|
328
|
+
# runs through `normalize_params`). Handle both.
|
|
329
|
+
raw = row[:variant_id]
|
|
330
|
+
variant_id = Spree::PrefixedId.prefixed_id?(raw) ? Spree::PrefixedId.decode_prefixed_id(raw) : raw
|
|
331
|
+
next if variant_id.blank? || row[:currency].blank?
|
|
332
|
+
|
|
333
|
+
{
|
|
334
|
+
variant_id: variant_id,
|
|
335
|
+
currency: row[:currency],
|
|
336
|
+
price_list_id: id,
|
|
337
|
+
amount: row[:amount],
|
|
338
|
+
compare_at_amount: row[:compare_at_amount]
|
|
339
|
+
}
|
|
340
|
+
end
|
|
341
|
+
return if rows.empty?
|
|
342
|
+
|
|
343
|
+
Spree::Prices::BulkUpsert.call(rows: rows)
|
|
344
|
+
touch_variants(rows.map { |r| r[:variant_id] }.uniq)
|
|
345
|
+
end
|
|
346
|
+
|
|
235
347
|
# Touches the variants in a background job
|
|
236
348
|
# @param variant_ids [Array<String>] array of variant ids
|
|
237
349
|
# @return [void]
|
|
@@ -4,7 +4,10 @@ module Spree
|
|
|
4
4
|
|
|
5
5
|
belongs_to :price_list, class_name: 'Spree::PriceList', touch: true
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
delegate :store, to: :price_list
|
|
8
|
+
|
|
9
|
+
validates :type, :price_list, presence: true
|
|
10
|
+
validates :type, uniqueness: { scope: [:price_list_id, *spree_base_uniqueness_scope] }
|
|
8
11
|
|
|
9
12
|
# Returns true if the price rule is applicable to the context
|
|
10
13
|
# @param context [Spree::Pricing::Context]
|
|
@@ -24,5 +27,12 @@ module Spree
|
|
|
24
27
|
def self.description
|
|
25
28
|
''
|
|
26
29
|
end
|
|
30
|
+
|
|
31
|
+
# Pull the rule registry off the global pricing config so
|
|
32
|
+
# PreferenceSchema's `.subclasses_with_preference_schema` can power
|
|
33
|
+
# the admin's "Add rule" picker.
|
|
34
|
+
def self.registered_subclasses
|
|
35
|
+
Array(Spree.pricing&.rules)
|
|
36
|
+
end
|
|
27
37
|
end
|
|
28
38
|
end
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module PriceRules
|
|
3
3
|
class CustomerGroupRule < Spree::PriceRule
|
|
4
|
-
|
|
4
|
+
# Stored as raw IDs. Accepts prefixed IDs (`cg_…`) from API
|
|
5
|
+
# callers and decodes them on write so eligibility checks compare
|
|
6
|
+
# against raw `customer_group_id` rows directly. Scope confines
|
|
7
|
+
# the existence check to the price-list's store.
|
|
8
|
+
preference :customer_group_ids, :array, default: [],
|
|
9
|
+
parse_on_set: normalize_id_preference(
|
|
10
|
+
klass: Spree::CustomerGroup,
|
|
11
|
+
scope: ->(rule) { rule.store.customer_groups }
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
def customer_groups
|
|
15
|
+
return [] if preferred_customer_group_ids.blank?
|
|
16
|
+
|
|
17
|
+
Spree::CustomerGroup.where(id: preferred_customer_group_ids)
|
|
18
|
+
end
|
|
5
19
|
|
|
6
20
|
def applicable?(context)
|
|
7
21
|
return false unless context.user
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module PriceRules
|
|
3
3
|
class MarketRule < Spree::PriceRule
|
|
4
|
-
|
|
4
|
+
# Stored as raw IDs. Accepts prefixed IDs (`mkt_…`) from API
|
|
5
|
+
# callers and decodes them on write so eligibility checks compare
|
|
6
|
+
# against raw `market_id` rows directly. Scope confines the
|
|
7
|
+
# existence check to the price-list's store so cross-store market
|
|
8
|
+
# IDs can't sneak in.
|
|
9
|
+
preference :market_ids, :array, default: [],
|
|
10
|
+
parse_on_set: normalize_id_preference(
|
|
11
|
+
klass: Spree::Market,
|
|
12
|
+
scope: ->(rule) { rule.store.markets }
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
def markets
|
|
16
|
+
return [] if preferred_market_ids.blank?
|
|
17
|
+
|
|
18
|
+
Spree::Market.where(id: preferred_market_ids)
|
|
19
|
+
end
|
|
5
20
|
|
|
6
21
|
def applicable?(context)
|
|
7
22
|
return false unless context.market
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module PriceRules
|
|
3
3
|
class UserRule < Spree::PriceRule
|
|
4
|
-
|
|
4
|
+
# Stored as raw IDs. Accepts prefixed IDs (the user class's prefix,
|
|
5
|
+
# e.g. `usr_…`) from API callers and decodes them on write. Resolves
|
|
6
|
+
# `Spree.user_class` lazily — the user class is configured at boot,
|
|
7
|
+
# and class-body evaluation runs before that on cold loads.
|
|
8
|
+
preference :user_ids, :array, default: [], parse_on_set: ->(values) {
|
|
9
|
+
normalize_id_preference(klass: Spree.user_class).call(values)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
def users
|
|
13
|
+
return [] if preferred_user_ids.blank?
|
|
14
|
+
|
|
15
|
+
Spree.user_class.where(id: preferred_user_ids)
|
|
16
|
+
end
|
|
5
17
|
|
|
6
18
|
def applicable?(context)
|
|
7
19
|
return false unless context.user
|
|
@@ -12,7 +24,14 @@ module Spree
|
|
|
12
24
|
end
|
|
13
25
|
|
|
14
26
|
def self.description
|
|
15
|
-
'Apply pricing to specific
|
|
27
|
+
'Apply pricing to specific customers'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Public-facing label — keeps the wire `api_type` as `user_rule`
|
|
31
|
+
# (preference column is `user_ids`) so existing data stays valid,
|
|
32
|
+
# but every UI surface reads "Customer rule".
|
|
33
|
+
def self.human_name
|
|
34
|
+
'Customer rule'
|
|
16
35
|
end
|
|
17
36
|
end
|
|
18
37
|
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class Product
|
|
3
|
+
module Channels
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
DEPRECATED_DATE_TO_PUBLICATION_FIELD = {
|
|
7
|
+
available_on: :published_at,
|
|
8
|
+
discontinue_on: :unpublished_at
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
belongs_to :store, class_name: 'Spree::Store', optional: true
|
|
13
|
+
|
|
14
|
+
# No +dependent: :destroy+: Product uses +acts_as_paranoid+, so destroy
|
|
15
|
+
# soft-deletes and publications outlive the product.
|
|
16
|
+
# +inverse_of: :product+ is what wires the parent into a child built off
|
|
17
|
+
# an unsaved Product (via +accepts_nested_attributes_for+ on the legacy
|
|
18
|
+
# alias below). Without it the child fails +validates :product,
|
|
19
|
+
# presence: true+ on +create+. Declared on both associations to keep
|
|
20
|
+
# the two in-memory caches symmetric (V-3454).
|
|
21
|
+
has_many :product_publications, class_name: 'Spree::ProductPublication',
|
|
22
|
+
inverse_of: :product, autosave: true
|
|
23
|
+
has_many :channels, -> { distinct }, through: :product_publications, class_name: 'Spree::Channel'
|
|
24
|
+
|
|
25
|
+
# Legacy Rails admin alias. The admin form submits
|
|
26
|
+
# +legacy_product_publications_attributes+ (with +_destroy+ flags and
|
|
27
|
+
# +reject_if+ semantics); the v3 API submits +product_publications+
|
|
28
|
+
# and goes through the custom writer below. Two names, one table —
|
|
29
|
+
# no +dependent:+ for the same +acts_as_paranoid+ reason as above.
|
|
30
|
+
has_many :legacy_product_publications, class_name: 'Spree::ProductPublication',
|
|
31
|
+
foreign_key: :product_id,
|
|
32
|
+
inverse_of: :product, autosave: true
|
|
33
|
+
accepts_nested_attributes_for :legacy_product_publications,
|
|
34
|
+
allow_destroy: true,
|
|
35
|
+
reject_if: ->(attrs) { attrs[:channel_id].blank? }
|
|
36
|
+
|
|
37
|
+
before_validation :assign_default_store, if: -> { store.nil? }
|
|
38
|
+
after_create :apply_pending_publications, if: :pending_publications?
|
|
39
|
+
|
|
40
|
+
DEPRECATED_DATE_TO_PUBLICATION_FIELD.each do |legacy_attr, publication_attr|
|
|
41
|
+
define_method("#{legacy_attr}=") do |value|
|
|
42
|
+
Spree::Deprecation.warn(
|
|
43
|
+
"Spree::Product##{legacy_attr}= is deprecated; set #{publication_attr} on " \
|
|
44
|
+
"ProductPublication instead (writes to every channel's publication). "
|
|
45
|
+
)
|
|
46
|
+
super(value)
|
|
47
|
+
product_publications.each { |publication| publication.public_send("#{publication_attr}=", value) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Reading +available_on+/+discontinue_on+ prefers the current-channel
|
|
51
|
+
# publication's date and falls back to the legacy Product column
|
|
52
|
+
# whenever the publication's value is nil. This 5.5 transition
|
|
53
|
+
# behavior is dropped in 6.0 when the legacy columns are removed.
|
|
54
|
+
define_method(legacy_attr) do
|
|
55
|
+
channel = Spree::Current.channel
|
|
56
|
+
publication = channel && publication_for(channel)
|
|
57
|
+
(publication && publication.public_send(publication_attr)) || super()
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns the publication for the given channel, or nil if the product isn't published there.
|
|
63
|
+
# @param channel [Spree::Channel] the channel to find the publication for
|
|
64
|
+
# @return [Spree::ProductPublication, nil] the publication for the channel, or nil if not published
|
|
65
|
+
def publication_for(channel)
|
|
66
|
+
return nil unless channel
|
|
67
|
+
|
|
68
|
+
if product_publications.loaded?
|
|
69
|
+
product_publications.find { |p| p.channel_id == channel.id }
|
|
70
|
+
else
|
|
71
|
+
product_publications.find_by(channel_id: channel.id)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Syncs product publications from an array of hashes.
|
|
76
|
+
# Creates new publications, updates existing ones (matched by +:id+ or
|
|
77
|
+
# +:channel_id+), and removes ones absent from the payload. An empty
|
|
78
|
+
# array detaches the product from every channel.
|
|
79
|
+
# @param publications_params [Array<Hash>] array of publication attribute hashes
|
|
80
|
+
# @return [void]
|
|
81
|
+
def product_publications=(publications_params)
|
|
82
|
+
return super if publications_params.nil?
|
|
83
|
+
return super if publications_params.respond_to?(:first) && publications_params.first.is_a?(Spree::ProductPublication)
|
|
84
|
+
|
|
85
|
+
if new_record?
|
|
86
|
+
@pending_publications_params = publications_params
|
|
87
|
+
return
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
apply_product_publications(publications_params)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def assign_default_store
|
|
96
|
+
self.store ||= Spree::Current.store || Spree::Store.default
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def pending_publications?
|
|
100
|
+
@pending_publications_params.present?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def apply_pending_publications
|
|
104
|
+
return unless @pending_publications_params
|
|
105
|
+
|
|
106
|
+
apply_product_publications(@pending_publications_params)
|
|
107
|
+
@pending_publications_params = nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def apply_product_publications(publications_params)
|
|
111
|
+
publication_ids_in_payload = []
|
|
112
|
+
|
|
113
|
+
publications_params.each do |publication_data|
|
|
114
|
+
publication_data = publication_data.to_h.with_indifferent_access
|
|
115
|
+
publication_id = publication_data.delete(:id)
|
|
116
|
+
channel_id = decode_publication_channel_id(publication_data[:channel_id])
|
|
117
|
+
|
|
118
|
+
if publication_id.present?
|
|
119
|
+
decoded_id = Spree::PrefixedId.prefixed_id?(publication_id) ?
|
|
120
|
+
Spree::PrefixedId.decode_prefixed_id(publication_id) :
|
|
121
|
+
publication_id
|
|
122
|
+
publication = product_publications.find_by(id: decoded_id)
|
|
123
|
+
next unless publication
|
|
124
|
+
|
|
125
|
+
# Channel is immutable; ignore any rebind attempt.
|
|
126
|
+
publication.update!(publication_data.slice(:published_at, :unpublished_at))
|
|
127
|
+
publication_ids_in_payload << publication.id
|
|
128
|
+
elsif channel_id.present?
|
|
129
|
+
# Upsert by channel_id so repeat submissions are idempotent
|
|
130
|
+
# against the unique (product_id, channel_id) index.
|
|
131
|
+
publication = product_publications.find_or_initialize_by(channel_id: channel_id)
|
|
132
|
+
publication.assign_attributes(publication_data.slice(:published_at, :unpublished_at))
|
|
133
|
+
publication.save!
|
|
134
|
+
publication_ids_in_payload << publication.id
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
product_publications.where.not(id: publication_ids_in_payload).destroy_all
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def decode_publication_channel_id(value)
|
|
142
|
+
return nil if value.blank?
|
|
143
|
+
return value unless Spree::PrefixedId.prefixed_id?(value)
|
|
144
|
+
|
|
145
|
+
Spree::PrefixedId.decode_prefixed_id(value) || value
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class Product
|
|
3
|
+
# Legacy multi-store support. In Spree 5.5+ the +Spree::ProductPublication+ model
|
|
4
|
+
# handles the Product↔Store relation, so this module only provides a fallback
|
|
5
|
+
# for legacy code that still references +Spree::Product#stores+.
|
|
6
|
+
module LegacyMultiStoreSupport
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
def stores
|
|
11
|
+
Spree::Deprecation.warn(
|
|
12
|
+
"Spree::Product#stores is deprecated. Please use Spree::Product.store instead. If you want to continue using multiple stores please install spree_multi_store gem"
|
|
13
|
+
)
|
|
14
|
+
store ? [store] : []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def store_ids
|
|
18
|
+
Spree::Deprecation.warn(
|
|
19
|
+
"Spree::Product#store_ids is deprecated. Please use Spree::Product.store_id instead. If you want to continue using multiple stores please install spree_multi_store gem"
|
|
20
|
+
)
|
|
21
|
+
store_id ? [store_id] : []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stores=(stores)
|
|
25
|
+
Spree::Deprecation.warn(
|
|
26
|
+
"Spree::Product#stores= is deprecated. Please use Spree::Product.store= instead. If you want to continue using multiple stores please install spree_multi_store gem"
|
|
27
|
+
)
|
|
28
|
+
self.store = Array(stores).compact.first
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def store_ids=(store_ids)
|
|
32
|
+
Spree::Deprecation.warn(
|
|
33
|
+
"Spree::Product#store_ids= is deprecated. Please use Spree::Product.store_id= instead. If you want to continue using multiple stores please install spree_multi_store gem"
|
|
34
|
+
)
|
|
35
|
+
self.store_id = Array(store_ids).compact_blank.first
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|