Open fthobe opened 18 hours ago
We've done this several times indeed! 😄
Solidus' core already supports this scenario but it's currently lacking some more high-level primitives for it. I'm available if you need any guidance on how to design this feature technically.
Would you be ok handing over what seems to the best current implementation and we work it over the finish line?
There are some considerations to make also regarding multi storefront.
Saluti da Milano a Chieti :)
Saluti da Milano a Chieti :)
Ah, ciao! 🇮🇹
Here's a very basic implementation (cannot copy real code from client's work):
👇 Tells Solidus to use this custom price selector class
# config/initializers/spree.rb
Spree.config do |config|
...
config.variant_price_selector_class = 'Spree::Variant::PriceSelectorByCompany'
end
👇 Class that implement the high-level logic to pick the right price for the right company
# app/models/spree/variant/price_selector_by_company.rb
# frozen_string_literal: true
module Spree
class Variant < Spree::Base
# This class is responsible for selecting a price for a variant given certain pricing options.
# A variant can have multiple or even dynamic prices. The `price_for_options`
# method determines which price applies under the given circumstances.
#
class PriceSelectorByCompany
# The pricing options represent "given circumstances" for a price: The currency
# we need and the country that the price applies to.
# Every price selector is designed to work with a particular set of pricing options
# embodied in it's pricing options class.
#
def self.pricing_options_class
Spree::Variant::PricingOptionsWithCompany
end
attr_reader :variant
def initialize(variant)
@variant = variant
end
# The variant's Spree::Price record, given a set of pricing options
# @param [Spree::Variant::PricingOptions] price_options Pricing Options to abide by
# @return [Spree::Price, nil] The most specific price for this set of pricing options.
def price_for_options(price_options)
sorted_prices_for(variant).detect do |price|
(price.country_iso == price_options.desired_attributes[:country_iso] ||
price.country_iso.nil?
) && price.currency == price_options.desired_attributes[:currency] &&
(price.company_id == price_options.desired_attributes[:company_id] ||
price.company_id.nil?)
end
end
private
# Returns `#prices` prioritized for being considered as default price
#
# @return [Array<Spree::Price>]
def sorted_prices_for(variant)
variant.prices.select do |price|
variant.discarded? || price.kept?
end.sort_by do |price|
[
price.country_iso.nil? ? 0 : 1,
price.updated_at || Time.zone.now,
price.id || Float::INFINITY,
]
end.reverse
end
end
end
end
👇 The pricing option class that matches the class above
# app/models/spree/variant/pricing_options_with_company.rb
# frozen_string_literal: true
module Spree
class Variant < Spree::Base
# Instances of this class represent the set of circumstances that influence how expensive a
# variant is. For this particular pricing options class, country_iso and currency influence
# the price of a variant.
#
# Pricing options can be instantiated from a line item or from the view context:
# @see Spree::LineItem#pricing_options
# @see Spree::Core::ControllerHelpers::Pricing#current_pricing_options
#
class PricingOptionsWithCompany
# When editing variants in the admin, this is the standard price the admin interacts with:
# The price in the admin's globally configured currency, for the admin's globally configured
# country. These options get merged with any options the user provides when instantiating
# new pricing options.
# @see Spree::Config.default_pricing_options
# @see #initialize
# @return [Hash] The attributes that admin prices usually have
#
def self.default_price_attributes
{
currency: Spree::Config.currency,
country_iso: Spree::Config.admin_vat_country_iso,
company_id: nil
}
end
# This creates the correct pricing options for a line item, taking into account
# its currency and tax address country, if available.
# @see Spree::LineItem#set_pricing_attributes
# @see Spree::LineItem#pricing_options
# @return [Spree::Variant::PricingOptions] pricing options for pricing a line item
#
def self.from_line_item(line_item)
tax_address = line_item.order.try!(:tax_address)
new(
currency: line_item.currency || Spree::Config.currency,
country_iso: tax_address && tax_address.country.try!(:iso),
company_id: line_item.order.user.company_id
)
end
# This creates the correct pricing options for a price, so that we can easily find other prices
# with the same pricing-relevant attributes and mark them as non-default.
# @see Spree::Price#set_default_price
# @return [Spree::Variant::PricingOptions] pricing options for pricing a line item
#
def self.from_price(price)
new(currency: price.currency, country_iso: price.country_iso, company_id: price.company_id)
end
# This creates the correct pricing options for a price, so the store owners can easily customize how to
# find the pricing based on the view context, having available current_store, current_spree_user, request.host_name, etc.
# @return [Spree::Variant::PricingOptions] pricing options for pricing a line item
def self.from_context(context)
new(
currency: context.current_store.try!(:default_currency).presence || Spree::Config[:currency],
country_iso: context.current_store.try!(:cart_tax_country_iso).presence,
company_id: context.current_spree_user.try!(:company_id)
)
end
# @return [Hash] The hash of exact desired attributes
attr_reader :desired_attributes
def initialize(desired_attributes = {})
@desired_attributes = self.class.default_price_attributes.merge(desired_attributes)
end
# A slightly modified version of the `desired_attributes` Hash. Instead of
# having "nil" or an actual country ISO code under the `:country_iso` key,
# this creates an array under the country_iso key that includes both the actual
# country iso we want and nil as a shorthand for the fallback price.
# This is useful so that we can determine the availability of variants by price:
# @see Spree::Variant.with_prices
# @see Spree::Core::Search::Base#retrieve_products
# @return [Hash] arguments to be passed into ActiveRecord.where()
#
def search_arguments
search_arguments = desired_attributes
search_arguments[:country_iso] = [desired_attributes[:country_iso], nil].flatten.uniq
search_arguments
end
# Shorthand for accessing the currency part of the desired attributes
# @return [String,nil] three-digit currency code or nil
#
def currency
desired_attributes[:currency]
end
# Shorthand for accessing the country part of the desired attributes
# @return [String,nil] two-digit country code or nil
#
def country_iso
desired_attributes[:country_iso]
end
# Shorthand for accessing the company part of the desired attributes
# @return [Integer,nil] company id or nil
#
def company_id
desired_attributes[:company_id]
end
# Since the current pricing options determine the price to be shown to users,
# product pages have to be cached and their caches invalidated using the data
# from this object. This method makes it easy to use with Rails `cache` helper.
# @return [String] cache key to be used in views
#
def cache_key
desired_attributes.values.select(&:present?).map(&:to_s).join("/")
end
end
end
end
This implies there's a Company model, which connects prices and users. Let me know if it's clear enough, and thanks for the contribution if you can do it.
Introduction A frequent B2B case is the upload of private price lists allowing the merchant to
Bigcommerce Implementation Shopify We would be willing to develop and provide this as a pull request or sponsor the development.
Desired Behavior Three new admin interfaces nested inside a B2B menu containing:
The association of the correct price to be loaded in front-end could be done entirely in backend. Afterwards upon login the front-end is supplied with the correct price by the backend.