spree_core 5.2.6 → 5.3.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/addresses_helper.rb +1 -23
- data/app/helpers/spree/base_helper.rb +25 -77
- data/app/helpers/spree/currency_helper.rb +2 -2
- data/app/helpers/spree/images_helper.rb +5 -0
- data/app/helpers/spree/mail_helper.rb +1 -1
- data/app/helpers/spree/products_helper.rb +1 -1
- data/app/jobs/spree/events/subscriber_job.rb +58 -0
- data/app/jobs/spree/products/refresh_metrics_job.rb +14 -0
- data/app/jobs/spree/products/touch_taxons_job.rb +0 -1
- data/app/jobs/spree/variants/touch_job.rb +9 -0
- data/app/models/concerns/spree/adjustment_source.rb +8 -0
- data/app/models/concerns/spree/parameterizable_name.rb +1 -1
- data/app/models/concerns/spree/product_scopes.rb +54 -8
- data/app/models/concerns/spree/publishable.rb +253 -0
- data/app/models/concerns/spree/unique_name.rb +1 -1
- data/app/models/concerns/spree/user_methods.rb +21 -1
- data/app/models/spree/ability.rb +3 -3
- data/app/models/spree/address.rb +0 -3
- data/app/models/spree/adjustable/promotion_accumulator.rb +1 -1
- data/app/models/spree/adjustment.rb +32 -6
- data/app/models/spree/asset.rb +6 -3
- data/app/models/spree/base.rb +3 -1
- data/app/models/spree/classification.rb +2 -2
- data/app/models/spree/credit_card.rb +0 -3
- data/app/models/spree/current.rb +24 -3
- data/app/models/spree/custom_domain.rb +1 -1
- data/app/models/spree/customer_group.rb +94 -0
- data/app/models/spree/customer_group_user.rb +16 -0
- data/app/models/spree/customer_return.rb +2 -3
- data/app/models/spree/digital.rb +2 -4
- data/app/models/spree/digital_link.rb +2 -3
- data/app/models/spree/event.rb +104 -0
- data/app/models/spree/export.rb +7 -1
- data/app/models/spree/gift_card.rb +13 -1
- data/app/models/spree/gift_card_batch.rb +3 -1
- data/app/models/spree/image.rb +20 -5
- data/app/models/spree/import.rb +17 -12
- data/app/models/spree/import_row.rb +13 -26
- data/app/models/spree/inventory_unit.rb +0 -3
- data/app/models/spree/invitation.rb +12 -6
- data/app/models/spree/line_item.rb +57 -4
- data/app/models/spree/newsletter_subscriber.rb +2 -0
- data/app/models/spree/option_type.rb +2 -5
- data/app/models/spree/option_value.rb +1 -4
- data/app/models/spree/order/checkout.rb +7 -2
- data/app/models/spree/order/emails.rb +3 -11
- data/app/models/spree/order/store_credit.rb +5 -1
- data/app/models/spree/order.rb +80 -31
- data/app/models/spree/order_updater.rb +28 -9
- data/app/models/spree/payment/custom_events.rb +37 -0
- data/app/models/spree/payment.rb +13 -4
- data/app/models/spree/payment_capture_event.rb +0 -4
- data/app/models/spree/payment_method.rb +1 -1
- data/app/models/spree/permission_sets/default_customer.rb +3 -3
- data/app/models/spree/policy.rb +1 -8
- data/app/models/spree/post.rb +2 -7
- data/app/models/spree/post_category.rb +2 -0
- data/app/models/spree/price.rb +8 -6
- data/app/models/spree/price_list.rb +263 -0
- data/app/models/spree/price_rule.rb +26 -0
- data/app/models/spree/price_rules/customer_group_rule.rb +21 -0
- data/app/models/spree/price_rules/user_rule.rb +19 -0
- data/app/models/spree/price_rules/volume_rule.rb +21 -0
- data/app/models/spree/price_rules/zone_rule.rb +19 -0
- data/app/models/spree/product.rb +117 -76
- data/app/models/spree/product_property.rb +2 -2
- data/app/models/spree/promotion/rules/customer_group.rb +22 -0
- data/app/models/spree/promotion/rules/user.rb +2 -2
- data/app/models/spree/promotion.rb +3 -4
- data/app/models/spree/promotion_handler/coupon.rb +3 -3
- data/app/models/spree/property.rb +3 -6
- data/app/models/spree/prototype.rb +0 -3
- data/app/models/spree/refund.rb +2 -3
- data/app/models/spree/reimbursement.rb +5 -3
- data/app/models/spree/report.rb +7 -1
- data/app/models/spree/reports/products_performance.rb +1 -1
- data/app/models/spree/reports/sales_total.rb +1 -1
- data/app/models/spree/return_authorization.rb +7 -3
- data/app/models/spree/return_item.rb +15 -4
- data/app/models/spree/shipment/custom_events.rb +47 -0
- data/app/models/spree/shipment.rb +12 -5
- data/app/models/spree/shipping_category.rb +0 -3
- data/app/models/spree/shipping_method.rb +0 -3
- data/app/models/spree/stock/quantifier.rb +1 -1
- data/app/models/spree/stock_item.rb +2 -0
- data/app/models/spree/stock_location.rb +0 -3
- data/app/models/spree/stock_movement/custom_events.rb +44 -0
- data/app/models/spree/stock_movement.rb +3 -0
- data/app/models/spree/stock_transfer.rb +2 -3
- data/app/models/spree/store.rb +64 -92
- data/app/models/spree/store_credit.rb +3 -4
- data/app/models/spree/store_product.rb +14 -0
- data/app/models/spree/subscriber.rb +186 -0
- data/app/models/spree/tax_category.rb +0 -4
- data/app/models/spree/tax_rate.rb +0 -3
- data/app/models/spree/taxon.rb +3 -36
- data/app/models/spree/taxonomy.rb +0 -3
- data/app/models/spree/variant.rb +114 -21
- data/app/models/spree/webhook_delivery.rb +60 -0
- data/app/models/spree/webhook_endpoint.rb +53 -0
- data/app/models/spree/wished_item.rb +2 -4
- data/app/models/spree/wishlist.rb +2 -3
- data/app/models/spree/zone.rb +0 -9
- data/app/paginators/spree/shared/paginate.rb +4 -0
- data/app/serializers/spree/events/asset_serializer.rb +22 -0
- data/app/serializers/spree/events/base_serializer.rb +61 -0
- data/app/serializers/spree/events/customer_return_serializer.rb +20 -0
- data/app/serializers/spree/events/digital_link_serializer.rb +20 -0
- data/app/serializers/spree/events/digital_serializer.rb +18 -0
- data/app/serializers/spree/events/export_serializer.rb +22 -0
- data/app/serializers/spree/events/gift_card_batch_serializer.rb +24 -0
- data/app/serializers/spree/events/gift_card_serializer.rb +29 -0
- data/app/serializers/spree/events/image_serializer.rb +9 -0
- data/app/serializers/spree/events/import_row_serializer.rb +23 -0
- data/app/serializers/spree/events/import_serializer.rb +24 -0
- data/app/serializers/spree/events/invitation_serializer.rb +28 -0
- data/app/serializers/spree/events/line_item_serializer.rb +31 -0
- data/app/serializers/spree/events/newsletter_subscriber_serializer.rb +21 -0
- data/app/serializers/spree/events/order_serializer.rb +39 -0
- data/app/serializers/spree/events/payment_serializer.rb +24 -0
- data/app/serializers/spree/events/post_category_serializer.rb +20 -0
- data/app/serializers/spree/events/post_serializer.rb +26 -0
- data/app/serializers/spree/events/price_serializer.rb +22 -0
- data/app/serializers/spree/events/product_serializer.rb +24 -0
- data/app/serializers/spree/events/promotion_serializer.rb +32 -0
- data/app/serializers/spree/events/refund_serializer.rb +23 -0
- data/app/serializers/spree/events/reimbursement_serializer.rb +22 -0
- data/app/serializers/spree/events/report_serializer.rb +23 -0
- data/app/serializers/spree/events/return_authorization_serializer.rb +22 -0
- data/app/serializers/spree/events/return_item_serializer.rb +27 -0
- data/app/serializers/spree/events/shipment_serializer.rb +24 -0
- data/app/serializers/spree/events/stock_item_serializer.rb +22 -0
- data/app/serializers/spree/events/stock_movement_serializer.rb +22 -0
- data/app/serializers/spree/events/stock_transfer_serializer.rb +22 -0
- data/app/serializers/spree/events/store_credit_serializer.rb +30 -0
- data/app/serializers/spree/events/user_serializer.rb +18 -0
- data/app/serializers/spree/events/variant_serializer.rb +34 -0
- data/app/serializers/spree/events/wished_item_serializer.rb +20 -0
- data/app/serializers/spree/events/wishlist_serializer.rb +22 -0
- data/app/services/spree/addresses/update.rb +1 -1
- data/app/services/spree/cart/add_item.rb +1 -1
- data/app/services/spree/coupon_codes/coupon_codes_handler.rb +2 -1
- data/app/services/spree/data_feeds/google/required_attributes.rb +4 -4
- data/app/services/spree/newsletter/verify.rb +5 -0
- data/app/services/spree/products/auto_match_taxons.rb +1 -1
- data/app/services/spree/seeds/all.rb +1 -1
- data/app/services/spree/taxons/add_products.rb +8 -4
- data/app/services/spree/taxons/regenerate_products.rb +8 -0
- data/app/services/spree/taxons/remove_products.rb +12 -7
- data/app/subscribers/spree/event_log_subscriber.rb +64 -0
- data/app/subscribers/spree/export_subscriber.rb +26 -0
- data/app/subscribers/spree/invitation_email_subscriber.rb +40 -0
- data/app/subscribers/spree/product_metrics_subscriber.rb +29 -0
- data/app/subscribers/spree/report_subscriber.rb +26 -0
- data/config/locales/en.yml +126 -0
- data/db/migrate/20251110120000_create_spree_price_lists.rb +22 -0
- data/db/migrate/20251110120001_create_spree_price_rules.rb +13 -0
- data/db/migrate/20251110120002_add_price_list_id_to_spree_prices.rb +6 -0
- data/db/migrate/20251110120003_add_price_list_id_to_spree_line_items.rb +5 -0
- data/db/migrate/20251201172118_fix_indexes_on_friendly_id_slugs.rb +8 -0
- data/db/migrate/20251214000001_create_spree_webhook_endpoints.rb +19 -0
- data/db/migrate/20251214000002_create_spree_webhook_deliveries.rb +23 -0
- data/db/migrate/20251222000000_add_performance_indexes_to_spree_adjustments.rb +25 -0
- data/db/migrate/20260112000000_fix_spree_prices_unique_indexes.rb +33 -0
- data/db/migrate/20260115120000_create_spree_customer_groups.rb +14 -0
- data/db/migrate/20260115120001_create_spree_customer_group_users.rb +14 -0
- data/db/migrate/20260117140831_remove_not_null_constraint_from_policy_name.rb +5 -0
- data/db/migrate/20260118120000_add_statistics_to_store_products.rb +11 -0
- data/db/migrate/20260119120000_add_counter_caches_to_spree_products.rb +9 -0
- data/db/migrate/20260119170000_add_counter_caches_to_spree_taxons.rb +9 -0
- data/db/migrate/20260120120000_add_image_count_to_spree_variants.rb +9 -0
- data/lib/generators/spree/dummy/dummy_generator.rb +14 -2
- data/lib/spree/core/configuration.rb +1 -0
- data/lib/spree/core/controller_helpers/auth.rb +0 -15
- data/lib/spree/core/controller_helpers/currency.rb +13 -9
- data/lib/spree/core/controller_helpers/search.rb +1 -1
- data/lib/spree/core/controller_helpers/store.rb +5 -1
- data/lib/spree/core/engine.rb +61 -78
- data/lib/spree/core/importer/order.rb +1 -1
- data/lib/spree/core/importer/product.rb +1 -1
- data/lib/spree/core/preferences/preferable.rb +14 -1
- data/lib/spree/core/pricing/context.rb +63 -0
- data/lib/spree/core/pricing/resolver.rb +129 -0
- data/lib/spree/core/search/base.rb +1 -1
- data/lib/spree/core/token_generator.rb +1 -1
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +42 -47
- data/lib/spree/events/adapters/active_support_notifications.rb +112 -0
- data/lib/spree/events/adapters/base.rb +193 -0
- data/lib/spree/events/registry.rb +99 -0
- data/lib/spree/events.rb +240 -0
- data/lib/spree/permitted_attributes.rb +13 -2
- data/lib/spree/testing_support/common_rake.rb +68 -35
- data/lib/spree/testing_support/factories/customer_group_factory.rb +6 -0
- data/lib/spree/testing_support/factories/customer_group_user_factory.rb +6 -0
- data/lib/spree/testing_support/factories/price_factory.rb +4 -0
- data/lib/spree/testing_support/factories/price_list_factory.rb +34 -0
- data/lib/spree/testing_support/factories/price_rule_factory.rb +49 -0
- data/lib/spree/testing_support/factories/promotion_rule_factory.rb +12 -0
- data/lib/spree/testing_support/factories/stock_item_factory.rb +6 -4
- data/lib/spree/testing_support/factories/store_product_factory.rb +6 -0
- data/lib/spree/testing_support/factories/taxon_factory.rb +0 -1
- data/lib/spree/testing_support/factories/webhook_delivery_factory.rb +48 -0
- data/lib/spree/testing_support/factories/webhook_endpoint_factory.rb +22 -0
- data/lib/spree/testing_support/lifecycle_events.rb +38 -0
- data/lib/spree/testing_support/store.rb +4 -2
- data/lib/spree/webhooks.rb +22 -0
- data/lib/tasks/products.rake +40 -0
- data/lib/tasks/taxons.rake +19 -0
- data/lib/tasks/variants.rake +18 -0
- metadata +112 -114
- data/app/jobs/spree/themes/duplicate_components_job.rb +0 -59
- data/app/jobs/spree/themes/screenshot_job.rb +0 -81
- data/app/models/concerns/spree/has_page_links.rb +0 -53
- data/app/models/spree/page.rb +0 -188
- data/app/models/spree/page_block.rb +0 -73
- data/app/models/spree/page_blocks/buttons.rb +0 -29
- data/app/models/spree/page_blocks/heading.rb +0 -18
- data/app/models/spree/page_blocks/image.rb +0 -20
- data/app/models/spree/page_blocks/link.rb +0 -21
- data/app/models/spree/page_blocks/mega_nav.rb +0 -33
- data/app/models/spree/page_blocks/mega_nav_with_subcategories.rb +0 -32
- data/app/models/spree/page_blocks/metafields.rb +0 -18
- data/app/models/spree/page_blocks/nav.rb +0 -15
- data/app/models/spree/page_blocks/newsletter_form.rb +0 -18
- data/app/models/spree/page_blocks/products/brand.rb +0 -15
- data/app/models/spree/page_blocks/products/buy_buttons.rb +0 -24
- data/app/models/spree/page_blocks/products/description.rb +0 -18
- data/app/models/spree/page_blocks/products/price.rb +0 -18
- data/app/models/spree/page_blocks/products/quantity_selector.rb +0 -20
- data/app/models/spree/page_blocks/products/share.rb +0 -8
- data/app/models/spree/page_blocks/products/title.rb +0 -19
- data/app/models/spree/page_blocks/products/variant_picker.rb +0 -13
- data/app/models/spree/page_blocks/subheading.rb +0 -17
- data/app/models/spree/page_blocks/text.rb +0 -16
- data/app/models/spree/page_link.rb +0 -60
- data/app/models/spree/page_section.rb +0 -222
- data/app/models/spree/page_sections/announcement_bar.rb +0 -28
- data/app/models/spree/page_sections/breadcrumbs.rb +0 -12
- data/app/models/spree/page_sections/collection_banner.rb +0 -18
- data/app/models/spree/page_sections/custom_code.rb +0 -11
- data/app/models/spree/page_sections/featured_posts.rb +0 -45
- data/app/models/spree/page_sections/featured_product.rb +0 -50
- data/app/models/spree/page_sections/featured_taxon.rb +0 -90
- data/app/models/spree/page_sections/featured_taxons.rb +0 -45
- data/app/models/spree/page_sections/footer.rb +0 -101
- data/app/models/spree/page_sections/header.rb +0 -62
- data/app/models/spree/page_sections/image_banner.rb +0 -55
- data/app/models/spree/page_sections/image_with_text.rb +0 -65
- data/app/models/spree/page_sections/main_password_footer.rb +0 -18
- data/app/models/spree/page_sections/main_password_header.rb +0 -20
- data/app/models/spree/page_sections/newsletter.rb +0 -54
- data/app/models/spree/page_sections/page_title.rb +0 -19
- data/app/models/spree/page_sections/post_details.rb +0 -19
- data/app/models/spree/page_sections/post_grid.rb +0 -19
- data/app/models/spree/page_sections/product_details.rb +0 -53
- data/app/models/spree/page_sections/product_grid.rb +0 -13
- data/app/models/spree/page_sections/related_products.rb +0 -58
- data/app/models/spree/page_sections/rich_text.rb +0 -31
- data/app/models/spree/page_sections/taxon_banner.rb +0 -18
- data/app/models/spree/page_sections/taxon_grid.rb +0 -17
- data/app/models/spree/page_sections/video.rb +0 -107
- data/app/models/spree/pages/account.rb +0 -19
- data/app/models/spree/pages/cart.rb +0 -19
- data/app/models/spree/pages/checkout.rb +0 -15
- data/app/models/spree/pages/custom.rb +0 -38
- data/app/models/spree/pages/homepage.rb +0 -72
- data/app/models/spree/pages/login.rb +0 -19
- data/app/models/spree/pages/password.rb +0 -59
- data/app/models/spree/pages/post.rb +0 -27
- data/app/models/spree/pages/post_list.rb +0 -36
- data/app/models/spree/pages/product_details.rb +0 -30
- data/app/models/spree/pages/search_results.rb +0 -43
- data/app/models/spree/pages/shop_all.rb +0 -40
- data/app/models/spree/pages/taxon.rb +0 -29
- data/app/models/spree/pages/taxon_list.rb +0 -41
- data/app/models/spree/pages/wishlist.rb +0 -15
- data/app/models/spree/theme.rb +0 -233
- data/app/models/spree/themes/default.rb +0 -97
- data/app/services/spree/taxons/touch_featured_sections.rb +0 -21
- data/db/migrate/20250120094216_create_page_builder_models.rb +0 -78
- data/db/migrate/20250305121352_remove_page_builder_indices.rb +0 -11
- data/db/migrate/20250825175217_add_missing_page_builder_indexes.rb +0 -7
- data/db/migrate/20250913130044_add_page_links_counter_cache_to_spree_stores.rb +0 -10
- data/lib/generators/spree/dummy/templates/initializers/devise.rb +0 -3
- data/lib/generators/spree/install/install_generator.rb +0 -219
- data/lib/generators/spree/install/templates/config/initializers/spree.rb +0 -126
- data/lib/spree/core/webhooks.rb +0 -21
- data/lib/spree/testing_support/factories/page_block_factory.rb +0 -22
- data/lib/spree/testing_support/factories/page_factory.rb +0 -33
- data/lib/spree/testing_support/factories/page_link_factory.rb +0 -7
- data/lib/spree/testing_support/factories/page_section_factory.rb +0 -27
- data/lib/spree/testing_support/factories/theme_factory.rb +0 -14
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class PriceList < Spree.base_class
|
|
3
|
+
acts_as_paranoid
|
|
4
|
+
acts_as_list scope: :store_id
|
|
5
|
+
|
|
6
|
+
include Spree::SingleStoreResource
|
|
7
|
+
|
|
8
|
+
MATCH_POLICIES = %w[all any].freeze
|
|
9
|
+
|
|
10
|
+
belongs_to :store, class_name: 'Spree::Store'
|
|
11
|
+
|
|
12
|
+
has_many :price_rules, class_name: 'Spree::PriceRule', dependent: :destroy
|
|
13
|
+
has_many :prices, class_name: 'Spree::Price', dependent: :destroy_async
|
|
14
|
+
has_many :variants, -> { where(spree_prices: { deleted_at: nil }).distinct }, through: :prices, source: :variant
|
|
15
|
+
has_many :products, -> { where(spree_prices: { deleted_at: nil }).distinct }, through: :variants, source: :product
|
|
16
|
+
alias price_list_products products
|
|
17
|
+
|
|
18
|
+
# Override default nested attributes to use bulk_update_prices for performance
|
|
19
|
+
attr_reader :prices_attributes
|
|
20
|
+
|
|
21
|
+
# Sets the prices attributes for bulk update
|
|
22
|
+
# @param attributes [Array<Hash>] array of price attributes with :id, :amount, :compare_at_amount
|
|
23
|
+
# @return [Array<Hash>] array of price attributes with :id, :amount, :compare_at_amount
|
|
24
|
+
def prices_attributes=(attributes)
|
|
25
|
+
@prices_attributes = attributes.values
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
after_update :process_bulk_prices_update
|
|
29
|
+
|
|
30
|
+
# Processes the bulk prices update
|
|
31
|
+
# @return [void]
|
|
32
|
+
def process_bulk_prices_update
|
|
33
|
+
return if @prices_attributes.blank?
|
|
34
|
+
|
|
35
|
+
bulk_update_prices(@prices_attributes)
|
|
36
|
+
@prices_attributes = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
validates :name, :store, presence: true
|
|
40
|
+
validates :match_policy, presence: true, inclusion: { in: MATCH_POLICIES }
|
|
41
|
+
validate :starts_at_before_ends_at
|
|
42
|
+
|
|
43
|
+
scope :by_position, -> { order(position: :asc) }
|
|
44
|
+
scope :for_store, ->(store) { where(store: store) }
|
|
45
|
+
scope :current, lambda { |timezone = nil|
|
|
46
|
+
timezone ||= Rails.application.config.time_zone
|
|
47
|
+
# Round to beginning of minute to enable Rails query caching
|
|
48
|
+
current_time = Time.current.in_time_zone(timezone).beginning_of_minute
|
|
49
|
+
where('starts_at IS NULL OR starts_at <= ?', current_time)
|
|
50
|
+
.where('ends_at IS NULL OR ends_at >= ?', current_time)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
state_machine :status, initial: :draft do
|
|
54
|
+
event :activate do
|
|
55
|
+
transition to: :active
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
event :deactivate do
|
|
59
|
+
transition to: :inactive
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
event :schedule do
|
|
63
|
+
transition to: :scheduled
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Returns price lists applicable for a given pricing context
|
|
68
|
+
# - active: always applies (within date range)
|
|
69
|
+
# - scheduled: applies only within starts_at/ends_at date range
|
|
70
|
+
def self.for_context(context)
|
|
71
|
+
timezone = context.store&.preferred_timezone || 'UTC'
|
|
72
|
+
for_store(context.store)
|
|
73
|
+
.with_status(:active, :scheduled)
|
|
74
|
+
.current(timezone)
|
|
75
|
+
.by_position
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.match_policies
|
|
79
|
+
MATCH_POLICIES.map { |key| [Spree.t(key), key] }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Returns true if the price list is applicable to the context
|
|
83
|
+
# @param context [Spree::Pricing::Context]
|
|
84
|
+
# @return [Boolean]
|
|
85
|
+
def applicable?(context)
|
|
86
|
+
return false unless active_or_scheduled?
|
|
87
|
+
return false unless within_date_range?(context.date || Time.current)
|
|
88
|
+
|
|
89
|
+
rules_applicable?(context)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns true if the price list rules are applicable to the context
|
|
93
|
+
# @param context [Spree::Pricing::Context]
|
|
94
|
+
# @return [Boolean]
|
|
95
|
+
def rules_applicable?(context)
|
|
96
|
+
return true if price_rules.none?
|
|
97
|
+
|
|
98
|
+
case match_policy
|
|
99
|
+
when 'all'
|
|
100
|
+
price_rules.all? { |rule| rule.applicable?(context) }
|
|
101
|
+
when 'any'
|
|
102
|
+
price_rules.any? { |rule| rule.applicable?(context) }
|
|
103
|
+
else
|
|
104
|
+
false
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Returns true if the price list is active or scheduled
|
|
109
|
+
# @return [Boolean]
|
|
110
|
+
def active_or_scheduled?
|
|
111
|
+
active? || scheduled?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Returns true if the price list is currently in effect
|
|
115
|
+
# (active, or scheduled and within date range)
|
|
116
|
+
def currently_active?
|
|
117
|
+
active_or_scheduled? && within_date_range?(Time.current)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Adds products to the price list
|
|
121
|
+
# Creates placeholder prices (with nil amount) for all variants and currencies
|
|
122
|
+
# @param product_ids [Array<String>] of product ids
|
|
123
|
+
# @return [void]
|
|
124
|
+
def add_products(product_ids)
|
|
125
|
+
return if product_ids.blank?
|
|
126
|
+
|
|
127
|
+
currencies = store.supported_currencies_list.map(&:iso_code)
|
|
128
|
+
variant_ids = Spree::Variant.eligible.where(product_id: product_ids).distinct.pluck(:id)
|
|
129
|
+
return if variant_ids.empty?
|
|
130
|
+
|
|
131
|
+
# Get existing variant_id/currency combinations to avoid duplicates
|
|
132
|
+
existing = prices.where(variant_id: variant_ids)
|
|
133
|
+
.pluck(:variant_id, :currency)
|
|
134
|
+
.to_set
|
|
135
|
+
|
|
136
|
+
now = Time.current
|
|
137
|
+
|
|
138
|
+
prices_to_insert = variant_ids.flat_map do |variant_id|
|
|
139
|
+
currencies.filter_map do |currency|
|
|
140
|
+
next if existing.include?([variant_id, currency])
|
|
141
|
+
|
|
142
|
+
{
|
|
143
|
+
variant_id: variant_id,
|
|
144
|
+
currency: currency,
|
|
145
|
+
amount: nil,
|
|
146
|
+
price_list_id: id,
|
|
147
|
+
created_at: now,
|
|
148
|
+
updated_at: now
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
return if prices_to_insert.empty?
|
|
154
|
+
|
|
155
|
+
# Use upsert_all with on_duplicate: :skip to handle race conditions
|
|
156
|
+
Spree::Price.upsert_all(prices_to_insert, on_duplicate: :skip)
|
|
157
|
+
touch_variants(variant_ids)
|
|
158
|
+
touch
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Removes products from the price list
|
|
162
|
+
# Hard deletes prices (not soft delete) to allow re-adding products later
|
|
163
|
+
# @param product_ids [Array<String>] of product ids
|
|
164
|
+
# @return [void]
|
|
165
|
+
def remove_products(product_ids)
|
|
166
|
+
return if product_ids.blank?
|
|
167
|
+
|
|
168
|
+
variant_ids = Spree::Variant.where(product_id: product_ids).distinct.pluck(:id)
|
|
169
|
+
return if variant_ids.empty?
|
|
170
|
+
|
|
171
|
+
# Use delete_all for hard delete - this bypasses acts_as_paranoid
|
|
172
|
+
# which is required for the unique index to work when re-adding products
|
|
173
|
+
prices.where(variant_id: variant_ids).delete_all
|
|
174
|
+
touch_variants(variant_ids)
|
|
175
|
+
touch
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Bulk update prices using upsert_all for performance
|
|
179
|
+
# @param prices_attributes [Array<Hash>] array of price attributes with :id, :amount, :compare_at_amount
|
|
180
|
+
# @return [Boolean] true if successful
|
|
181
|
+
def bulk_update_prices(prices_attributes)
|
|
182
|
+
return true if prices_attributes.blank?
|
|
183
|
+
|
|
184
|
+
records_to_upsert = []
|
|
185
|
+
variant_ids = Set.new
|
|
186
|
+
|
|
187
|
+
# Get current values for comparison
|
|
188
|
+
price_ids = prices_attributes.map { |a| a[:id] || a['id'] }.compact.map(&:to_i)
|
|
189
|
+
current_values = prices.where(id: price_ids).pluck(:id, :amount, :compare_at_amount).to_h { |id, amount, compare_at| [id, { amount: amount, compare_at_amount: compare_at }] }
|
|
190
|
+
|
|
191
|
+
prices_attributes.each do |attrs|
|
|
192
|
+
attrs = (attrs.respond_to?(:to_unsafe_h) ? attrs.to_unsafe_h : attrs.to_h).with_indifferent_access
|
|
193
|
+
next if attrs[:id].blank?
|
|
194
|
+
|
|
195
|
+
price_id = attrs[:id].to_i
|
|
196
|
+
current = current_values[price_id] || {}
|
|
197
|
+
|
|
198
|
+
# Parse amounts using LocalizedNumber for proper decimal handling
|
|
199
|
+
amount = attrs[:amount].present? ? Spree::LocalizedNumber.parse(attrs[:amount]) : nil
|
|
200
|
+
compare_at_amount = attrs[:compare_at_amount].present? ? Spree::LocalizedNumber.parse(attrs[:compare_at_amount]) : nil
|
|
201
|
+
|
|
202
|
+
# Clear compare_at_amount if it equals amount
|
|
203
|
+
compare_at_amount = nil if compare_at_amount == amount
|
|
204
|
+
|
|
205
|
+
# Skip if nothing changed
|
|
206
|
+
next if amount == current[:amount] && compare_at_amount == current[:compare_at_amount]
|
|
207
|
+
|
|
208
|
+
records_to_upsert << {
|
|
209
|
+
id: price_id,
|
|
210
|
+
variant_id: attrs[:variant_id].to_i,
|
|
211
|
+
currency: attrs[:currency],
|
|
212
|
+
amount: amount,
|
|
213
|
+
compare_at_amount: compare_at_amount,
|
|
214
|
+
price_list_id: id
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
variant_ids << attrs[:variant_id].to_i
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
return true if records_to_upsert.empty?
|
|
221
|
+
|
|
222
|
+
opts = { update_only: [:amount, :compare_at_amount], on_duplicate: :update }
|
|
223
|
+
opts[:unique_by] = :id unless ActiveRecord::Base.connection.adapter_name == 'Mysql2'
|
|
224
|
+
|
|
225
|
+
Spree::Price.upsert_all(records_to_upsert, **opts)
|
|
226
|
+
|
|
227
|
+
touch_variants(variant_ids.to_a)
|
|
228
|
+
true
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
private
|
|
232
|
+
|
|
233
|
+
# Touches the variants in a background job
|
|
234
|
+
# @param variant_ids [Array<String>] array of variant ids
|
|
235
|
+
# @return [void]
|
|
236
|
+
def touch_variants(variant_ids)
|
|
237
|
+
return if variant_ids.blank?
|
|
238
|
+
|
|
239
|
+
Spree::Variants::TouchJob.perform_later(variant_ids)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def starts_at_before_ends_at
|
|
243
|
+
return if starts_at.blank? || ends_at.blank?
|
|
244
|
+
|
|
245
|
+
if starts_at >= ends_at
|
|
246
|
+
errors.add(:ends_at, :must_be_after_starts_at)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Returns true if the date is within the price list date range
|
|
251
|
+
# @param date [Time] the date to check
|
|
252
|
+
# @return [Boolean]
|
|
253
|
+
def within_date_range?(date)
|
|
254
|
+
timezone = store&.preferred_timezone || Rails.application.config.time_zone
|
|
255
|
+
date_in_tz = date.in_time_zone(timezone)
|
|
256
|
+
|
|
257
|
+
return false if starts_at.present? && date_in_tz < starts_at
|
|
258
|
+
return false if ends_at.present? && date_in_tz > ends_at
|
|
259
|
+
|
|
260
|
+
true
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class PriceRule < Spree.base_class
|
|
3
|
+
belongs_to :price_list, class_name: 'Spree::PriceList', touch: true
|
|
4
|
+
|
|
5
|
+
validates :type, :price_list,presence: true
|
|
6
|
+
|
|
7
|
+
# Returns true if the price rule is applicable to the context
|
|
8
|
+
# @param context [Spree::Pricing::Context]
|
|
9
|
+
# @return [Boolean]
|
|
10
|
+
def applicable?(context)
|
|
11
|
+
raise NotImplementedError, "#{self.class.name} must implement #applicable?"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Returns the human name of the price rule
|
|
15
|
+
# @return [String]
|
|
16
|
+
def self.human_name
|
|
17
|
+
name.demodulize.titleize
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the description of the price rule
|
|
21
|
+
# @return [String]
|
|
22
|
+
def self.description
|
|
23
|
+
''
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module PriceRules
|
|
3
|
+
class CustomerGroupRule < Spree::PriceRule
|
|
4
|
+
preference :customer_group_ids, :array, default: []
|
|
5
|
+
|
|
6
|
+
def applicable?(context)
|
|
7
|
+
return false unless context.user
|
|
8
|
+
return true if preferred_customer_group_ids.empty?
|
|
9
|
+
|
|
10
|
+
user_customer_group_ids = Spree::CustomerGroupUser.where(user_id: context.user.id).pluck(:customer_group_id).map(&:to_s)
|
|
11
|
+
|
|
12
|
+
# Compare as strings to support both integer and UUID primary keys
|
|
13
|
+
(preferred_customer_group_ids.map(&:to_s) & user_customer_group_ids).any?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.description
|
|
17
|
+
Spree.t('price_rules.customer_group_rule.description')
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module PriceRules
|
|
3
|
+
class UserRule < Spree::PriceRule
|
|
4
|
+
preference :user_ids, :array, default: []
|
|
5
|
+
|
|
6
|
+
def applicable?(context)
|
|
7
|
+
return false unless context.user
|
|
8
|
+
return true if preferred_user_ids.empty?
|
|
9
|
+
|
|
10
|
+
# Compare as strings to support both integer and UUID primary keys
|
|
11
|
+
preferred_user_ids.map(&:to_s).include?(context.user.id.to_s)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.description
|
|
15
|
+
'Apply pricing to specific users'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module PriceRules
|
|
3
|
+
class VolumeRule < Spree::PriceRule
|
|
4
|
+
preference :min_quantity, :integer, default: 1
|
|
5
|
+
preference :max_quantity, :integer, nullable: true
|
|
6
|
+
|
|
7
|
+
def applicable?(context)
|
|
8
|
+
return false unless context.quantity
|
|
9
|
+
|
|
10
|
+
return false if context.quantity < preferred_min_quantity
|
|
11
|
+
return false if preferred_max_quantity && context.quantity > preferred_max_quantity
|
|
12
|
+
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.description
|
|
17
|
+
'Apply pricing based on quantity purchased'
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module PriceRules
|
|
3
|
+
class ZoneRule < Spree::PriceRule
|
|
4
|
+
preference :zone_ids, :array, default: []
|
|
5
|
+
|
|
6
|
+
def applicable?(context)
|
|
7
|
+
return false unless context.zone
|
|
8
|
+
return true if preferred_zone_ids.empty?
|
|
9
|
+
|
|
10
|
+
# Compare as strings to support both integer and UUID primary keys
|
|
11
|
+
preferred_zone_ids.map(&:to_s).include?(context.zone.id.to_s)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.description
|
|
15
|
+
'Apply pricing based on the tax/shipping zone'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|