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



21
22
23
# File 'app/models/spree/price_list.rb', line 21

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



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

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



80
81
82
# File 'app/models/spree/price_list.rb', line 80

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)


112
113
114
# File 'app/models/spree/price_list.rb', line 112

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



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
160
161
# File 'app/models/spree/price_list.rb', line 126

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)


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

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



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
230
231
# File 'app/models/spree/price_list.rb', line 183

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)


118
119
120
# File 'app/models/spree/price_list.rb', line 118

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



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

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



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

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)


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

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