# frozen_string_literal: true

# Locks an order including its variants.
#
# It should be used when making major changes like checking out the order.
# It can keep stock checking in sync and prevent overselling of an item.
class OrderLocker
  # Locking will not prevent all access to these rows. Other processes are
  # only waiting if they try to lock one of these rows as well.
  #
  #   https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html
  #
  def self.lock_order_and_variants(order)
    return yield if order.nil?

    order.with_lock do
      lock_variants_of(order)
      yield
    end
  end

  # There are many places in which stock is stored in the database. Row locking
  # on variant level ensures that there are no conflicts even when an item is
  # sold through multiple shops.
  def self.lock_variants_of(order)
    variant_ids = order.line_items.select(:variant_id)

    # Ordering the variants by id prevents deadlocks. Plucking the ids sends
    # the locking query without building Spree::Variant objects.
    Spree::Variant.where(id: variant_ids).order(:id).lock.pluck(:id)
  end
  private_class_method :lock_variants_of
end
