Script to show/hide shipping rates based on product tag (multiple conditions)

Solved
M0w45
Excursionist
17 0 2

Hi all,

I am fairly new to Shopify scripts and I was able to create 2 scripts that help me solve my problem. But as I was publishing them, I just found out I can't have multiple scripts running at once for the same type, so I would love to get someone's help with this.

Some of my products are tagged with "status-special-order" and require 10-15 business days to ship, while all other items are in stock and available to ship within 1 business day.

When a user adds both stocked item(s) AND special order items, I want to show these (2) specific shipping rates so the user can pick one of these options:

- Free Shipping: I'm in a rush. Send me stocked item(s) now, and special item(s) when available. 

Free Shipping: I'm in no rush. I can wait for all items to be available and help offset carbon footprint.

I was able to tweak an existing Shopify script so I would see these options, and it would hide all of my other custom and carrier shipping rates whenever a special order item is in the cart. However I would like to hide these (2) shipping rates (listed above) when there is no special order item in the cart.

I wish I could just add another condition somewhere saying that if the product tag "status-special-order" is not included, then it should hide the (2) custom shipping rates I listed higher in my message.

Any help would be very much appreciated! 

Thank you!

 

HIDE_RATES_FOR_PRODUCT = [
  {
    product_selector_match_type: :include,
    product_selector_type: :tag,
    product_selectors: ["status-special-order"],
    rate_match_type: :exact,
    rate_names: ["Free Shipping (Regular)","$5.99 Flat Rate","Expedited Parcel","Xpresspost
","Priority"],
  },
]

# ================================================================
# ProductSelector
# Finds matching products by the entered criteria.
# ================================================================
class ProductSelector
  def initialize(match_type, selector_type, selectors)
    @match_type = match_type
    @comparator = match_type == :include ? 'any?' : 'none?'
    _type = selector_type
    @selectors = selectors
  end

  def match?(line_item)
    if self.respond_to?(@selector_type)
      self.send(@selector_type, line_item)
    else
      raise RuntimeError.new('Invalid product selector type')
    end
  end

  def tag(line_item)
    product_tags = line_item.variant.product.tags.map { |tag| tag.downcase.strip }
    @selectors = @selectors.map { |selector| selector.downcase.strip }
    (@selectors & product_tags).send(@comparator)
  end
end

# ================================================================
# RateNameSelector
# Finds whether the supplied rate names match any of the entered names.
# ================================================================
class RateNameSelector
  def initialize(match_type, rate_names)
    @match_type = match_type
    @comparator = match_type == :exact ? '==' : 'include?'
    @rate_names = rate_names&.map { |rate_name| rate_name.downcase.strip }
  end

  def match?(shipping_rate)
    if @match_type == :all
      true
    else
      @rate_names.any? { |name| shipping_rate.name.downcase.send(@comparator, name) }
    end
  end
end

# ================================================================
# HideRatesForProduct
# If the cart contains any matching items, the entered rate(s) are hidden.
# ================================================================
class HideRatesForProduct
  def initialize(campaigns)
    @campaigns = campaigns
  end

  def run(cart, shipping_rates)
    address = cart.shipping_address

    return if address.nil?

    @campaigns.each do |campaign|
      product_selector = ProductSelector.new(
        campaign[:product_selector_match_type],
        campaign[:product_selector_type],
        campaign[:product_selectors],
      )

      product_match = cart.line_items.any? { |line_item| product_selector.match?(line_item) }

      next unless product_match

      rate_name_selector = RateNameSelector.new(
        campaign[:rate_match_type],
        campaign[:rate_names],
      )

      shipping_rates.delete_if do |shipping_rate|
        rate_name_selector.match?(shipping_rate)
      end
    end
  end
end

CAMPAIGNS = [
  HideRatesForProduct.new(HIDE_RATES_FOR_PRODUCT),
]

CAMPAIGNS.each do |campaign|
  campaign.run(Input.cart, Input.shipping_rates)
end

Output.shipping_rates = Input.shipping_rates

 

 

0 Likes

Hi @M0w45,

I haven't been able to test this (since it would require setting up products and shipping configuration like yours), but I think a small modification could make this work. Being able to create multiple rules in the same script is one of the reasons I work on apps like Playwright. Anyway, onto your script...

You may need to adjust the exact names of the shipping methods if they do not match your shipping configuration. Try this updated script and let me know if it works for you:

HIDE_STANDARD_RATES_FOR_SPECIAL_ORDER = [
  {
    product_selector_match_type: :include,
    product_selector_type: :tag,
    product_selectors: ["status-special-order"],
    rate_match_type: :exact,
    rate_names: ["Free Shipping (Regular)","$5.99 Flat Rate","Expedited Parcel","Xpresspost","Priority"],
  },
]

HIDE_SPECIAL_FREE_RATES_WHEN_NO_SPECIAL_ORDER = [
  {
    product_selector_match_type: :exclude,
    product_selector_type: :tag,
    product_selectors: ["status-special-order"],
    rate_match_type: :exact,
    rate_names: ["Free Shipping: I'm in a rush. Send me stocked item(s) now, and special item(s) when available.","Free Shipping: I'm in no rush. I can wait for all items to be available and help offset carbon footprint."],
  },
]

# ================================================================
# ProductSelector
# Finds matching products by the entered criteria.
# ================================================================
class ProductSelector
  def initialize(match_type, selector_type, selectors)
    @match_type = match_type
    @comparator = match_type == :include ? 'any?' : 'none?'
    _type = selector_type
    @selectors = selectors
  end

  def match?(line_item)
    if self.respond_to?(@selector_type)
      self.send(@selector_type, line_item)
    else
      raise RuntimeError.new('Invalid product selector type')
    end
  end

  def tag(line_item)
    product_tags = line_item.variant.product.tags.map { |tag| tag.downcase.strip }
    @selectors = @selectors.map { |selector| selector.downcase.strip }
    (@selectors & product_tags).send(@comparator)
  end
end

# ================================================================
# RateNameSelector
# Finds whether the supplied rate names match any of the entered names.
# ================================================================
class RateNameSelector
  def initialize(match_type, rate_names)
    @match_type = match_type
    @comparator = match_type == :exact ? '==' : 'include?'
    @rate_names = rate_names&.map { |rate_name| rate_name.downcase.strip }
  end

  def match?(shipping_rate)
    if @match_type == :all
      true
    else
      @rate_names.any? { |name| shipping_rate.name.downcase.send(@comparator, name) }
    end
  end
end

# ================================================================
# HideRatesForProduct
# If the cart contains any matching items, the entered rate(s) are hidden.
# ================================================================
class HideRatesForProduct
  def initialize(campaigns)
    @campaigns = campaigns
  end

  def run(cart, shipping_rates)
    address = cart.shipping_address

    return if address.nil?

    @campaigns.each do |campaign|
      product_selector = ProductSelector.new(
        campaign[:product_selector_match_type],
        campaign[:product_selector_type],
        campaign[:product_selectors],
      )

      product_match = cart.line_items.any? { |line_item| product_selector.match?(line_item) }

      next unless product_match

      rate_name_selector = RateNameSelector.new(
        campaign[:rate_match_type],
        campaign[:rate_names],
      )

      shipping_rates.delete_if do |shipping_rate|
        rate_name_selector.match?(shipping_rate)
      end
    end
  end
end

CAMPAIGNS = [
  HideRatesForProduct.new(HIDE_STANDARD_RATES_FOR_SPECIAL_ORDER),
  HideRatesForProduct.new(HIDE_SPECIAL_FREE_RATES_WHEN_NO_SPECIAL_ORDER)
]

CAMPAIGNS.each do |campaign|
  campaign.run(Input.cart, Input.shipping_rates)
end

Output.shipping_rates = Input.shipping_rates


I hope that is helpful,
Matthew

Playwright | Create Shopify Scripts without writing code | https://playwrightapp.com
- Was my reply helpful? Please Like and Accept Solution.
- Need help with a custom Shopify script? Email me at matthew@playwrightapp.com

M0w45
Excursionist
17 0 2

Hi @playwright-matt ,

Thank you so much for your quick reply, much appreciated!

I was able to test your script, and corrected line 29 to replace

_type = selector_type

 with

@selector_type = selector_type

It's almost perfect :D. 

I tested the (3) scenarios:

- When there is a special order item: I do see the (2) shipping rates available. It works.

M0w45_0-1618521814799.png

 

- When there isn't a special order item: I do see all other shipping rates but the (2) for Special Order Items. It works.

M0w45_2-1618521915156.png

 

- When there is both a stocked item and a special order item: I do not see any shipping rate listed. I checked my shipping rates and they all apply to QC. I am wondering how I can tweak your code to make this work. 

M0w45_1-1618521851130.png

I don't want to be a pain, but I would love to hear your thoughts on this. 

Thank you! 

0 Likes

This is an accepted solution.

@M0w45

Good catch. I've modified the script below. It requires an extra loop through the shipping rates, but not too bad.

HIDE_STANDARD_RATES_FOR_SPECIAL_ORDER = [
  {
    product_selector_match_type: :include,
    product_selector_type: :tag,
    product_selectors: ["status-special-order"],
    rate_match_type: :exact,
    rate_names: ["Free Shipping (Regular)","$5.99 Flat Rate","Expedited Parcel","Xpresspost","Priority"],
  },
]

HIDE_SPECIAL_FREE_RATES_WHEN_NO_SPECIAL_ORDER = [
  {
    product_selector_match_type: :exclude,
    product_selector_type: :tag,
    product_selectors: ["status-special-order"],
    rate_match_type: :exact,
    rate_names: ["Free Shipping: I'm in a rush. Send me stocked item(s) now, and special item(s) when available.","Free Shipping: I'm in no rush. I can wait for all items to be available and help offset carbon footprint."],
  },
]


# ================================================================
# ProductSelector
# Finds matching products by the entered criteria.
# ================================================================
class ProductSelector
  def initialize(match_type, selector_type, selectors)
    @match_type = match_type
    @comparator = match_type == :include ? 'any?' : 'none?'
    @selector_type = selector_type
    @selectors = selectors
  end

  def match?(line_item)
    if self.respond_to?(@selector_type)
      self.send(@selector_type, line_item)
    else
      raise RuntimeError.new('Invalid product selector type')
    end
  end

  def tag(line_item)
    product_tags = line_item.variant.product.tags.map { |tag| tag.downcase.strip }
    @selectors = @selectors.map { |selector| selector.downcase.strip }
    (@selectors & product_tags).send(@comparator)
  end
end

# ================================================================
# RateNameSelector
# Finds whether the supplied rate names match any of the entered names.
# ================================================================
class RateNameSelector
  def initialize(match_type, rate_names)
    @match_type = match_type
    @comparator = match_type == :exact ? '==' : 'include?'
    @rate_names = rate_names&.map { |rate_name| rate_name.downcase.strip }
  end

  def match?(shipping_rate)
    if @match_type == :all
      true
    else
      @rate_names.any? { |name| shipping_rate.name.downcase.send(@comparator, name) }
    end
  end
end

# ================================================================
# HideRatesForProduct
# If the cart contains any matching items, the entered rate(s) are hidden.
# ================================================================
class HideRatesForProduct
  def initialize(campaigns)
    @campaigns = campaigns
  end

  def run(cart, shipping_rates)
    address = cart.shipping_address
    campaign_satisified = false

    return if address.nil?

    @campaigns.each do |campaign|
      product_selector = ProductSelector.new(
        campaign[:product_selector_match_type],
        campaign[:product_selector_type],
        campaign[:product_selectors],
      )

      product_match = cart.line_items.any? { |line_item| product_selector.match?(line_item) }

      next unless product_match

      rate_name_selector = RateNameSelector.new(
        campaign[:rate_match_type],
        campaign[:rate_names],
      )

      shipping_rates.each do |shipping_rate|
        if rate_name_selector.match?(shipping_rate)
          campaign_satisified = true
        end
      end

      shipping_rates.delete_if do |shipping_rate|
        rate_name_selector.match?(shipping_rate)
      end
    end

    return campaign_satisified
  end
end

CAMPAIGNS = [
  HideRatesForProduct.new(HIDE_STANDARD_RATES_FOR_SPECIAL_ORDER),
  HideRatesForProduct.new(HIDE_SPECIAL_FREE_RATES_WHEN_NO_SPECIAL_ORDER)
]

CAMPAIGNS.each do |campaign|
  break if campaign.run(Input.cart, Input.shipping_rates)
end

Output.shipping_rates = Input.shipping_rates

 

Playwright | Create Shopify Scripts without writing code | https://playwrightapp.com
- Was my reply helpful? Please Like and Accept Solution.
- Need help with a custom Shopify script? Email me at matthew@playwrightapp.com

M0w45
Excursionist
17 0 2

@playwright-matt , thank you very much! 

Just tried it and it works like a charm

0 Likes
Glad to help!
Playwright | Create Shopify Scripts without writing code | https://playwrightapp.com
- Was my reply helpful? Please Like and Accept Solution.
- Need help with a custom Shopify script? Email me at matthew@playwrightapp.com

M0w45
Excursionist
17 0 2

@playwright-matt , I am sorry to ask you this, but I figured I could pick your brain

Is there a way to add an extra loop somewhere so that if there is only (1) special order item, or multiple special order items in the cart, it shows a different shipping rate?

As of right now, with at least one special order item and at least one stocked item, it works just fine and shows both options: 

"Free Shipping: I'm in a rush. Send me stocked item(s) now, and special item(s) when available.","Free Shipping: I'm in no rush. I can wait for all items to be available and help offset carbon footprint."

With only stocked items in the cart, it works just fine as well and shows all shipping rates but the (2) listed above:

["Free Shipping (Regular)","$5.99 Flat Rate","Expedited Parcel","Xpresspost","Priority"]

With 1 or + special order item (and no stocked items), it shows both options:

["Free Shipping: I'm in a rush. Send me stocked item(s) now, and special item(s) when available.","Free Shipping: I'm in no rush. I can wait for all items to be available and help offset carbon footprint."]

Since the cart only special order items, I'm thinking having only one option showing, which could be a different message showing (i.e: "Free Shipping: special order items usually take 10 to 15 business days" or something similar). 

Again, you already helped me a lot here, but if there is any chance you have a solution, it would be more than appreciated.

Thank you!

0 Likes

This is an accepted solution.

@M0w45 

Again, check your exact shipping names to make sure they are the same, but I've added a new parameter called product_selector_match_scope that can take "any" or "all" as values:

HIDE_RATES_FOR_ONLY_SPECIAL_ORDER = [
  {
    product_selector_match_type: :include,
    product_selector_match_scope: :all,
    product_selector_type: :tag,
    product_selectors: ["status-special-order"],
    rate_match_type: :exact,
    rate_names: ["Free Shipping (Regular)","$5.99 Flat Rate","Expedited Parcel","Xpresspost","Priority","Free Shipping: I'm in a rush. Send me stocked item(s) now, and special item(s) when available.","Free Shipping: I'm in no rush. I can wait for all items to be available and help offset carbon footprint."],
  },
]

HIDE_RATES_FOR_MIXED_ORDER = [
  {
    product_selector_match_type: :include,
    product_selector_match_scope: :any,
    product_selector_type: :tag,
    product_selectors: ["status-special-order"],
    rate_match_type: :exact,
    rate_names: ["Free Shipping (Regular)","$5.99 Flat Rate","Expedited Parcel","Xpresspost","Priority","Free Shipping: special order items usually take 10 to 15 business days."],
  },
]

HIDE_RATES_FOR_STOCK_ORDER = [
  {
    product_selector_match_type: :exclude,
    product_selector_match_scope: :any,
    product_selector_type: :tag,
    product_selectors: ["status-special-order"],
    rate_match_type: :exact,
    rate_names: ["Free Shipping: I'm in a rush. Send me stocked item(s) now, and special item(s) when available.","Free Shipping: I'm in no rush. I can wait for all items to be available and help offset carbon footprint.","Free Shipping: special order items usually take 10 to 15 business days."],
  },
]


# ================================================================
# ProductSelector
# Finds matching products by the entered criteria.
# ================================================================
class ProductSelector
  def initialize(match_type, selector_type, selectors)
    @match_type = match_type
    @comparator = match_type == :include ? 'any?' : 'none?'
    @selector_type = selector_type
    @selectors = selectors
  end

  def match?(line_item)
    if self.respond_to?(@selector_type)
      self.send(@selector_type, line_item)
    else
      raise RuntimeError.new('Invalid product selector type')
    end
  end

  def tag(line_item)
    product_tags = line_item.variant.product.tags.map { |tag| tag.downcase.strip }
    @selectors = @selectors.map { |selector| selector.downcase.strip }
    (@selectors & product_tags).send(@comparator)
  end
end

# ================================================================
# RateNameSelector
# Finds whether the supplied rate names match any of the entered names.
# ================================================================
class RateNameSelector
  def initialize(match_type, rate_names)
    @match_type = match_type
    @comparator = match_type == :exact ? '==' : 'include?'
    @rate_names = rate_names&.map { |rate_name| rate_name.downcase.strip }
  end

  def match?(shipping_rate)
    if @match_type == :all
      true
    else
      @rate_names.any? { |name| shipping_rate.name.downcase.send(@comparator, name) }
    end
  end
end

# ================================================================
# HideRatesForProduct
# If the cart contains any matching items, the entered rate(s) are hidden.
# ================================================================
class HideRatesForProduct
  def initialize(campaigns)
    @campaigns = campaigns
  end

  def run(cart, shipping_rates)
    address = cart.shipping_address
    campaign_satisified = false

    return if address.nil?

    @campaigns.each do |campaign|
      product_selector = ProductSelector.new(
        campaign[:product_selector_match_type],
        campaign[:product_selector_type],
        campaign[:product_selectors],
      )

      if campaign[:product_selector_match_scope] == :all
        product_match = cart.line_items.all? { |line_item| product_selector.match?(line_item) }
      else
        product_match = cart.line_items.any? { |line_item| product_selector.match?(line_item) }
      end

      next unless product_match

      rate_name_selector = RateNameSelector.new(
        campaign[:rate_match_type],
        campaign[:rate_names],
      )

      shipping_rates.each do |shipping_rate|
        if rate_name_selector.match?(shipping_rate)
          campaign_satisified = true
        end
      end

      shipping_rates.delete_if do |shipping_rate|
        rate_name_selector.match?(shipping_rate)
      end
    end

    return campaign_satisified
  end
end

CAMPAIGNS = [
  HideRatesForProduct.new(HIDE_RATES_FOR_ONLY_SPECIAL_ORDER),
  HideRatesForProduct.new(HIDE_RATES_FOR_MIXED_ORDER),
  HideRatesForProduct.new(HIDE_RATES_FOR_STOCK_ORDER)
]

CAMPAIGNS.each do |campaign|
  break if campaign.run(Input.cart, Input.shipping_rates)
end

Output.shipping_rates = Input.shipping_rates

 

Let me know if that works as expected!

Matthew

Playwright | Create Shopify Scripts without writing code | https://playwrightapp.com
- Was my reply helpful? Please Like and Accept Solution.
- Need help with a custom Shopify script? Email me at matthew@playwrightapp.com

M0w45
Excursionist
17 0 2
Click to expand...
@playwright-matt Brilliant! 

Thank you so much for your help with this!
0 Likes