Class: Spree::PriceList

Inherits:
Object
  • Object
show all
Includes:
SingleStoreResource
Defined in:
app/models/spree/price_list.rb

Constant Summary collapse

MATCH_POLICIES =
%w[all any].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#prices_attributesObject

Override default nested attributes to use bulk_update_prices for performance



19
20
21
# File 'app/models/spree/price_list.rb', line 19

def prices_attributes
  @prices_attributes
end

Class Method Details

.for_context(context) ⇒ Object

Returns price lists applicable for a given pricing context

  • active: always applies (within date range)

  • scheduled: applies only within starts_at/ends_at date range



70
71
72
73
74
75
76
# File 'app/models/spree/price_list.rb', line 70

def self.for_context(context)
  timezone = context.store&.preferred_timezone || 'UTC'
  for_store(context.store)
    .with_status(:active, :scheduled)
    .current(timezone)
    .by_position
end

.match_policiesObject



78
79
80
# File 'app/models/spree/price_list.rb', line 78

def self.match_policies
  MATCH_POLICIES.map { |key| [Spree.t(key), key] }
end

Instance Method Details

#active_or_scheduled?Boolean

Returns true if the price list is active or scheduled

Returns:

  • (Boolean)


110
111
112
# File 'app/models/spree/price_list.rb', line 110

def active_or_scheduled?
  active? || scheduled?
end

#add_products(product_ids) ⇒ void

This method returns an undefined value.

Adds products to the price list Creates placeholder prices (with nil amount) for all variants and currencies

Parameters:

  • product_ids (Array<String>)

    of product ids



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'app/models/spree/price_list.rb', line 124

def add_products(product_ids)
  return if product_ids.blank?

  currencies = store.supported_currencies_list.map(&:iso_code)
  variant_ids = Spree::Variant.eligible.where(product_id: product_ids).distinct.pluck(:id)
  return if variant_ids.empty?

  # Get existing variant_id/currency combinations to avoid duplicates
  existing = prices.where(variant_id: variant_ids)
                   .pluck(:variant_id, :currency)
                   .to_set

  now = Time.current

  prices_to_insert = variant_ids.flat_map do |variant_id|
    currencies.filter_map do |currency|
      next if existing.include?([variant_id, currency])

      {
        variant_id: variant_id,
        currency: currency,
        amount: nil,
        price_list_id: id,
        created_at: now,
        updated_at: now
      }
    end
  end

  return if prices_to_insert.empty?

  # Use upsert_all with on_duplicate: :skip to handle race conditions
  Spree::Price.upsert_all(prices_to_insert, on_duplicate: :skip)
  touch_variants(variant_ids)
  touch
end

#applicable?(context) ⇒ Boolean

Returns true if the price list is applicable to the context

Parameters:

Returns:

  • (Boolean)


85
86
87
88
89
90
# File 'app/models/spree/price_list.rb', line 85

def applicable?(context)
  return false unless active_or_scheduled?
  return false unless within_date_range?(context.date || Time.current)

  rules_applicable?(context)
end

#bulk_update_prices(prices_attributes) ⇒ Boolean

Bulk update prices using upsert_all for performance

Parameters:

  • prices_attributes (Array<Hash>)

    array of price attributes with :id, :amount, :compare_at_amount

Returns:

  • (Boolean)

    true if successful



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'app/models/spree/price_list.rb', line 181

def bulk_update_prices(prices_attributes)
  return true if prices_attributes.blank?

  records_to_upsert = []
  variant_ids = Set.new

  # Get current values for comparison
  price_ids = prices_attributes.map { |a| a[:id] || a['id'] }.compact.map(&:to_i)
  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 }] }

  prices_attributes.each do |attrs|
    attrs = (attrs.respond_to?(:to_unsafe_h) ? attrs.to_unsafe_h : attrs.to_h).with_indifferent_access
    next if attrs[:id].blank?

    price_id = attrs[:id].to_i
    current = current_values[price_id] || {}

    # Parse amounts using LocalizedNumber for proper decimal handling
    amount = attrs[:amount].present? ? Spree::LocalizedNumber.parse(attrs[:amount]) : nil
    compare_at_amount = attrs[:compare_at_amount].present? ? Spree::LocalizedNumber.parse(attrs[:compare_at_amount]) : nil

    # Clear compare_at_amount if it equals amount
    compare_at_amount = nil if compare_at_amount == amount

    # Skip if nothing changed
    next if amount == current[:amount] && compare_at_amount == current[:compare_at_amount]

    records_to_upsert << {
      id: price_id,
      variant_id: attrs[:variant_id].to_i,
      currency: attrs[:currency],
      amount: amount,
      compare_at_amount: compare_at_amount,
      price_list_id: id
    }

    variant_ids << attrs[:variant_id].to_i
  end

  return true if records_to_upsert.empty?

  opts = { update_only: [:amount, :compare_at_amount], on_duplicate: :update }
  opts[:unique_by] = :id unless ActiveRecord::Base.connection.adapter_name == 'Mysql2'

  Spree::Price.upsert_all(records_to_upsert, **opts)

  touch_variants(variant_ids.to_a)
  true
end

#currently_active?Boolean

Returns true if the price list is currently in effect (active, or scheduled and within date range)

Returns:

  • (Boolean)


116
117
118
# File 'app/models/spree/price_list.rb', line 116

def currently_active?
  active_or_scheduled? && within_date_range?(Time.current)
end

#process_bulk_prices_updatevoid

This method returns an undefined value.

Processes the bulk prices update



32
33
34
35
36
37
# File 'app/models/spree/price_list.rb', line 32

def process_bulk_prices_update
  return if @prices_attributes.blank?

  bulk_update_prices(@prices_attributes)
  @prices_attributes = nil
end

#remove_products(product_ids) ⇒ void

This method returns an undefined value.

Removes products from the price list Hard deletes prices (not soft delete) to allow re-adding products later

Parameters:

  • product_ids (Array<String>)

    of product ids



165
166
167
168
169
170
171
172
173
174
175
176
# File 'app/models/spree/price_list.rb', line 165

def remove_products(product_ids)
  return if product_ids.blank?

  variant_ids = Spree::Variant.where(product_id: product_ids).distinct.pluck(:id)
  return if variant_ids.empty?

  # Use delete_all for hard delete - this bypasses acts_as_paranoid
  # which is required for the unique index to work when re-adding products
  prices.where(variant_id: variant_ids).delete_all
  touch_variants(variant_ids)
  touch
end

#rules_applicable?(context) ⇒ Boolean

Returns true if the price list rules are applicable to the context

Parameters:

Returns:

  • (Boolean)


95
96
97
98
99
100
101
102
103
104
105
106
# File 'app/models/spree/price_list.rb', line 95

def rules_applicable?(context)
  return true if price_rules.none?

  case match_policy
  when 'all'
    price_rules.all? { |rule| rule.applicable?(context) }
  when 'any'
    price_rules.any? { |rule| rule.applicable?(context) }
  else
    false
  end
end