Personalized checkout and custom promotions with Shopify Scripts
I need a automatic discount of 10% on select product collections for 1st time customers.
The discount would automatically show up at checkout BUT if a customer enters a discount code then the discount is showing double dipping. I don't want 2 promos to be possible. Either the preloaded on by the script or a manually entered code.
I have been trying to use the github shopify script builder but have ran into issues.
My code is below.
class Campaign def initialize(condition, *qualifiers) @condition = (condition.to_s + '?').to_sym @qualifiers = PostCartAmountQualifier ? [] : [] rescue qualifiers.compact @line_item_selector = qualifiers.last unless @line_item_selector qualifiers.compact.each do |qualifier| is_multi_select = qualifier.instance_variable_get(:@conditions).is_a?(Array) if is_multi_select qualifier.instance_variable_get(:@conditions).each do |nested_q| @post_amount_qualifier = nested_q if nested_q.is_a?(PostCartAmountQualifier) @qualifiers << qualifier end else @post_amount_qualifier = qualifier if qualifier.is_a?(PostCartAmountQualifier) @qualifiers << qualifier end end if @qualifiers.empty? end def qualifies?(cart) return true if @qualifiers.empty? @unmodified_line_items = cart.line_items.map do |item| new_item = item.dup new_item.instance_variables.each do |var| val = item.instance_variable_get(var) new_item.instance_variable_set(var, val.dup) if val.respond_to?(:dup) end new_item end if @post_amount_qualifier @qualifiers.send(@condition) do |qualifier| is_selector = false if qualifier.is_a?(Selector) || qualifier.instance_variable_get(:@conditions).any? { |q| q.is_a?(Selector) } is_selector = true end rescue nil if is_selector raise "Missing line item match type" if @li_match_type.nil? cart.line_items.send(@li_match_type) { |item| qualifier.match?(item) } else qualifier.match?(cart, @line_item_selector) end end end def run_with_hooks(cart) before_run(cart) if respond_to?(:before_run) run(cart) after_run(cart) end def after_run(cart) @discount.apply_final_discount if @discount && @discount.respond_to?(:apply_final_discount) revert_changes(cart) unless @post_amount_qualifier.nil? || @post_amount_qualifier.match?(cart) end def revert_changes(cart) cart.instance_variable_set(:@line_items, @unmodified_line_items) end end class ConditionalDiscount < Campaign def initialize(condition, customer_qualifier, cart_qualifier, line_item_selector, discount, max_discounts) super(condition, customer_qualifier, cart_qualifier) @line_item_selector = line_item_selector @discount = discount @items_to_discount = max_discounts == 0 ? nil : max_discounts end def run(cart) raise "Campaign requires a discount" unless @discount return unless qualifies?(cart) applicable_items = cart.line_items.select { |item| @line_item_selector.nil? || @line_item_selector.match?(item) } applicable_items = applicable_items.sort_by { |item| item.variant.price } applicable_items.each do |item| break if @items_to_discount == 0 if (!@items_to_discount.nil? && item.quantity > @items_to_discount) discounted_items = item.split(take: @items_to_discount) @discount.apply(discounted_items) cart.line_items << discounted_items @items_to_discount = 0 else @discount.apply(item) @items_to_discount -= item.quantity if !@items_to_discount.nil? end end end end class OrSelector def initialize(*conditions) @conditions = conditions.compact end def match?(item, selector = nil) @conditions.any? do |condition| if selector condition.match?(item, selector) else condition.match?(item) end end end end class Qualifier def partial_match(match_type, item_info, possible_matches) match_type = (match_type.to_s + '?').to_sym if item_info.kind_of?(Array) possible_matches.any? do |possibility| item_info.any? do |search| search.send(match_type, possibility) end end else possible_matches.any? do |possibility| item_info.send(match_type, possibility) end end end def compare_amounts(compare, comparison_type, compare_to) case comparison_type when :greater_than return compare > compare_to when :greater_than_or_equal return compare >= compare_to when :less_than return compare < compare_to when :less_than_or_equal return compare <= compare_to when :equal_to return compare == compare_to else raise "Invalid comparison type" end end end class CustomerOrderCountQualifier < Qualifier def initialize(comparison_type, amount) @comparison_type = comparison_type @amount = amount end def match?(cart, selector = nil) return false if cart.customer.nil? total = cart.customer.orders_count compare_amounts(total, @comparison_type, @amount) end end class AndSelector def initialize(*conditions) @conditions = conditions.compact end def match?(item, selector = nil) @conditions.all? do |condition| if selector condition.match?(item, selector) else condition.match?(item) end end end end class PostCartAmountQualifier < Qualifier def initialize(comparison_type, amount) @comparison_type = comparison_type @amount = Money.new(cents: amount * 100) end def match?(cart, selector = nil) total = cart.subtotal_price compare_amounts(total, @comparison_type, @amount) end end class ExcludeDiscountCodes < Qualifier def initialize(behaviour, message, match_type = :reject_except, discount_codes = []) @reject = behaviour == :apply_script @message = message == "" ? "Discount codes cannot be used with this offer" : message @match_type = match_type @discount_codes = discount_codes.map(&:downcase) end def match?(cart, selector = nil) return true if cart.discount_code.nil? return false if !@reject discount_code = cart.discount_code.code.downcase should_reject = true case @match_type when :reject_except should_reject = !@discount_codes.include?(discount_code) when :accept_except should_reject = @discount_codes.include?(discount_code) end if should_reject cart.discount_code.reject({message: @message}) end return true end end class Selector def partial_match(match_type, item_info, possible_matches) match_type = (match_type.to_s + '?').to_sym if item_info.kind_of?(Array) possible_matches.any? do |possibility| item_info.any? do |search| search.send(match_type, possibility) end end else possible_matches.any? do |possibility| item_info.send(match_type, possibility) end end end end class ProductTypeSelector < Selector def initialize(match_type, product_types) @invert = match_type == :not_one @product_types = product_types.map(&:downcase) end def match?(line_item) @invert ^ @product_types.include?(line_item.variant.product.product_type.downcase) end end class PercentageDiscount def initialize(percent, message) @discount = (100 - percent) / 100.0 @message = message end def apply(line_item) line_item.change_line_price(line_item.line_price * @discount, message: @message) end end CAMPAIGNS = [ ConditionalDiscount.new( :all, OrSelector.new( nil, CustomerOrderCountQualifier.new( :less_than_or_equal, 5 ), nil ), AndSelector.new( PostCartAmountQualifier.new( :greater_than, 1 ), ExcludeDiscountCodes.new( :apply_discount, "", :reject_except, [] ), nil ), ProductTypeSelector.new( :is_one, ["Screw-In Filters", "Motion Picture & Television Filter", "Accessories", "Shoulder Bag", "Lighting", "Apparel", "Monopod", "Photo Tripod", "Video Tripod", "Softbox", "Drone Filters", "Bracket", "Umbrella", "Diffuser Panel", "Dolly", "Shoulder Pad", "Wrap", "Pouch", "Guard Bag"] ), PercentageDiscount.new( 10, "New Customer Discount 10% Off" ), 0 ) ].freeze CAMPAIGNS.each do |campaign| campaign.run_with_hooks(Input.cart) end Output.cart = Input.cart
Discover how to increase the efficiency of commerce operations with Shopify Academy's l...
By Jacqui Mar 26, 2025Shopify and our financial partners regularly review and update verification requiremen...
By Jacqui Mar 14, 2025Unlock the potential of marketing on your business growth with Shopify Academy's late...
By Shopify Mar 12, 2025