Personalized checkout and custom promotions with Shopify Scripts
I have successfully built a bundle script using shopify scripts, please see code below.
It adds x items to the cart including an identifier and then discounts 10% based on the products.
It works apart from one small problem which is really annoying. It does not allow discount codes to be used to any products at all whilst the script is active, so if a customer adds a product which isn't in the script it still blocks the use of it.
I need the script to be able to discount the products in the script itself, but still be able to apply discount codes as it's currently blocking them all.
Thanks in advance!
class Campaign def initialize(condition, *qualifiers) @condition = condition == :default ? :all? : (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 BundleDiscount < Campaign def initialize(condition, customer_qualifier, cart_qualifier, discount, full_bundles_only, bundle_products) super(condition, customer_qualifier, cart_qualifier, nil) @bundle_products = bundle_products @discount = discount @full_bundles_only = full_bundles_only @split_items = [] @bundle_items = [] end def check_bundles(cart) bundled_items = @bundle_products.map do |bitem| quantity_required = bitem[:quantity].to_i qualifiers = bitem[:qualifiers] type = bitem[:type].to_sym case type when :ptype items = cart.line_items.select { |item| qualifiers.include?(item.variant.product.product_type) && !item.discounted? } when :ptag items = cart.line_items.select { |item| (qualifiers & item.variant.product.tags).length > 0 && !item.discounted? } when :pid qualifiers.map!(&:to_i) items = cart.line_items.select { |item| qualifiers.include?(item.variant.product.id) && !item.discounted? } when :vid qualifiers.map!(&:to_i) items = cart.line_items.select { |item| qualifiers.include?(item.variant.id) && !item.discounted? } when :vsku items = cart.line_items.select { |item| (qualifiers & item.variant.skus).length > 0 && !item.discounted? } end total_quantity = items.reduce(0) { |total, item| total + item.quantity } { has_all: total_quantity >= quantity_required, total_quantity: total_quantity, quantity_required: quantity_required, total_possible: (total_quantity / quantity_required).to_i, items: items } end max_bundle_count = bundled_items.map{ |bundle| bundle[:total_possible] }.min if @full_bundles_only if bundled_items.all? { |item| item[:has_all] } if @full_bundles_only bundled_items.each do |bundle| bundle_quantity = bundle[:quantity_required] * max_bundle_count split_out_extra_quantity(cart, bundle[:items], bundle[:total_quantity], bundle_quantity) end else bundled_items.each do |bundle| bundle[:items].each do |item| @bundle_items << item cart.line_items.delete(item) end end end return true end false end def split_out_extra_quantity(cart, items, total_quantity, quantity_required) items_to_split = quantity_required items.each do |item| break if items_to_split == 0 if item.quantity > items_to_split @bundle_items << item.split({take: items_to_split}) @split_items << item items_to_split = 0 else @bundle_items << item split_quantity = item.quantity items_to_split -= split_quantity end cart.line_items.delete(item) end cart.line_items.concat(@split_items) @split_items.clear end def run(cart) raise "Campaign requires a discount" unless @discount return unless qualifies?(cart) if check_bundles(cart) @bundle_items.each { |item| @discount.apply(item) } end @bundle_items.reverse.each { |item| cart.line_items.prepend(item) } 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 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 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 = [ BundleDiscount.new( :all, nil, ExcludeDiscountCodes.new( :apply_script, "Discount codes are not compatible with our Sets", :reject_except, [] ), PercentageDiscount.new( 10, "Save 10% with our Sets" ), false, [{:type => "vid", :qualifiers => ["19350922264672"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19343338700896"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, ExcludeDiscountCodes.new( :apply_script, "Discount codes are not compatible with our Sets", :reject_except, [] ), PercentageDiscount.new( 10, "Save 10% with our Sets" ), false, [{:type => "vid", :qualifiers => ["19350922264672"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19343434514528"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, ExcludeDiscountCodes.new( :apply_script, "Discount codes are not compatible with our Sets", :reject_except, [] ), PercentageDiscount.new( 10, "Save 10% with our Sets" ), false, [{:type => "vid", :qualifiers => ["19350922264672"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19343123120224"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, ExcludeDiscountCodes.new( :apply_script, "Discount codes are not compatible with our Sets", :reject_except, [] ), PercentageDiscount.new( 10, "Save 10% with our Sets" ), false, [{:type => "vid", :qualifiers => ["19350922264672"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19343386378336"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, ExcludeDiscountCodes.new( :apply_script, "Discount codes are not compatible with our Sets", :reject_except, [] ), PercentageDiscount.new( 10, "Save 10% with our Sets" ), false, [{:type => "vid", :qualifiers => ["19342537359456"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19350922264672"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19350794207328"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, ExcludeDiscountCodes.new( :apply_script, "Discount codes are not compatible with our Sets", :reject_except, [] ), PercentageDiscount.new( 10, "Save 10% with our Sets" ), false, [{:type => "vid", :qualifiers => ["19342537359456"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19350922264672"], :quantity => "1"}, {:type => "vid", :qualifiers => ["12245771944052"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, ExcludeDiscountCodes.new( :apply_script, "Discount codes are not compatible with our Sets", :reject_except, [] ), PercentageDiscount.new( 10, "Save 10% with our Sets" ), false, [{:type => "vid", :qualifiers => ["12245773123700"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19350794207328"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19351013884000"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, ExcludeDiscountCodes.new( :apply_script, "Discount codes are not compatible with our Sets", :reject_except, [] ), PercentageDiscount.new( 10, "Save 10% with our Sets" ), false, [{:type => "vid", :qualifiers => ["19351156064352"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18356506624096"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, ExcludeDiscountCodes.new( :apply_script, "Discount codes are not compatible with our Sets", :reject_except, [] ), PercentageDiscount.new( 10, "Save 10% with our Sets" ), false, [{:type => "vid", :qualifiers => ["19350079733856"], :quantity => "1"}, {:type => "vid", :qualifiers => ["13568284196960"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, ExcludeDiscountCodes.new( :apply_script, "Discount codes are not compatible with our Sets", :reject_except, [] ), PercentageDiscount.new( 10, "Save 10% with our Sets" ), false, [{:type => "vid", :qualifiers => ["18356472283232"], :quantity => "1"}, {:type => "vid", :qualifiers => ["13568284196960"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, ExcludeDiscountCodes.new( :apply_script, "Discount codes are not compatible with our Sets", :reject_except, [] ), PercentageDiscount.new( 10, "Save 10% with our Sets" ), false, [{:type => "vid", :qualifiers => ["18356472283232"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19343338700896"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, ExcludeDiscountCodes.new( :apply_script, "Discount codes are not compatible with our Sets", :reject_except, [] ), PercentageDiscount.new( 10, "Save 10% with our Sets" ), false, [{:type => "vid", :qualifiers => ["18356472283232"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19343434514528"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, ExcludeDiscountCodes.new( :apply_script, "Discount codes are not compatible with our Sets", :reject_except, [] ), PercentageDiscount.new( 10, "Save 10% with our Sets" ), false, [{:type => "vid", :qualifiers => ["18356472283232"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19343123120224"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, ExcludeDiscountCodes.new( :apply_script, "Discount codes are not compatible with our Sets", :reject_except, [] ), PercentageDiscount.new( 10, "Save 10% with our Sets" ), false, [{:type => "vid", :qualifiers => ["18356472283232"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19343386378336"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), ].freeze CAMPAIGNS.each do |campaign| campaign.run_with_hooks(Input.cart) end Output.cart = Input.cart
This code has lots of instances where you reject discount codes. If you didn't want the rejection, why add those lines in? I don't have full context here so could be asking some crazy (and silly) questions.
What part of this script did you not want the rejection to happen on?
(also, app devs spamming their products instead of helping will find their posts magically marked as spam. if you posted - not talking about you igolabs - and it's now gone, that's why)
Sorry for lack of context Jason,
I'm not too hot on Ruby as you can tell, the script blocks all discount codes on every transaction whereas i only want them to be blocked when purchasing a bundle.
We have a temporary situation where we cannot have a SKU for the bundles and have to use permalinks + scripts to apply the discount. The permalinks add 2/3 products + a £0.00 value identifier product for our distributor.
The script sucessfully triggers when the cart recognises the 3/4 products in the cart, and stops discount codes being applied. However if i just added 1 of the products from the bundle or a product not in any of the bundles it still blocks the discount. I want discounts only to be blocked if the 3/4 VID's match.
This could be a Scripts issue which i can't get round this way.
Another thing i've thought of could be adding a £0.00 discount code but as far as i can tell you can't combine cart permalinks and shareable discount url's.
It's the last piece of this complicated puzzle to stop abuse of bundles from people with discount codes. Hopefully this gives you a bit more context!
P.S I've added the code before i added the discount code stuff in below.
class Campaign def initialize(condition, *qualifiers) @condition = condition == :default ? :all? : (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 revert_changes(cart) cart.instance_variable_set(:@line_items, @unmodified_line_items) end end class BundleDiscount < Campaign def initialize(condition, customer_qualifier, cart_qualifier, discount, full_bundles_only, bundle_products) super(condition, customer_qualifier, cart_qualifier) @bundle_products = bundle_products @discount = discount @full_bundles_only = full_bundles_only @split_items = [] @bundle_items = [] end def check_bundles(cart) bundled_items = @bundle_products.map do |bitem| quantity_required = bitem[:quantity].to_i qualifiers = bitem[:qualifiers] type = bitem[:type].to_sym case type when :ptype items = cart.line_items.select { |item| qualifiers.include?(item.variant.product.product_type) } when :ptag items = cart.line_items.select { |item| (qualifiers & item.variant.product.tags).length > 0 } when :pid qualifiers.map!(&:to_i) items = cart.line_items.select { |item| qualifiers.include?(item.variant.product.id) } when :vid qualifiers.map!(&:to_i) items = cart.line_items.select { |item| qualifiers.include?(item.variant.id) } end total_quantity = items.reduce(0) { |total, item| total + item.quantity } { has_all: total_quantity >= quantity_required, total_quantity: total_quantity, quantity_required: quantity_required, total_possible: (total_quantity / quantity_required).to_i, items: items } end max_bundle_count = bundled_items.map{ |bundle| bundle[:total_possible] }.min if @full_bundles_only if bundled_items.all? { |item| item[:has_all] } if @full_bundles_only bundled_items.each do |bundle| bundle_quantity = bundle[:quantity_required] * max_bundle_count split_out_extra_quantity(cart, bundle[:items], bundle[:total_quantity], bundle_quantity) end else bundled_items.each do |bundle| bundle[:items].each do |item| @bundle_items << item cart.line_items.delete(item) end end end return true end false end def split_out_extra_quantity(cart, items, total_quantity, quantity_required) items_to_split = quantity_required items.each do |item| break if items_to_split == 0 if item.quantity > items_to_split @bundle_items << item.split({take: items_to_split}) @split_items << item items_to_split = 0 else @bundle_items << item split_quantity = item.quantity items_to_split -= split_quantity end cart.line_items.delete(item) end cart.line_items.concat(@split_items) @split_items.clear end def run(cart) raise "Campaign requires a discount" unless @discount return unless qualifies?(cart) if check_bundles(cart) @bundle_items.each { |item| @discount.apply(item) } end @bundle_items.reverse.each { |item| cart.line_items.prepend(item) } 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 = [ BundleDiscount.new( :all, nil, nil, PercentageDiscount.new( 10, "Save 10% with our Sets" ), true, [{:type => "vid", :qualifiers => ["12245773123700"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19350794207328"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19351013884000"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, nil, PercentageDiscount.new( 10, "Save 10% with our Sets" ), true, [{:type => "vid", :qualifiers => ["19343338700896"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19350922264672"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, nil, PercentageDiscount.new( 10, "Save 10% with our Sets" ), true, [{:type => "vid", :qualifiers => ["18356506624096"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19351156064352"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, nil, PercentageDiscount.new( 10, "Save 10% with our Sets" ), true, [{:type => "vid", :qualifiers => ["19343434514528"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19350922264672"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, nil, PercentageDiscount.new( 10, "Save 10% with our Sets" ), true, [{:type => "vid", :qualifiers => ["18356472283232"], :quantity => "1"}, {:type => "vid", :qualifiers => ["13568284196960"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, nil, PercentageDiscount.new( 10, "Save 10% with our Sets" ), true, [{:type => "vid", :qualifiers => ["19343338700896"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18356472283232"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, nil, PercentageDiscount.new( 10, "Save 10% with our Sets" ), true, [{:type => "vid", :qualifiers => ["19350922264672"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19343123120224"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, nil, PercentageDiscount.new( 10, "Save 10% with our Sets" ), true, [{:type => "vid", :qualifiers => ["19350922264672"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19343386378336"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, nil, PercentageDiscount.new( 10, "Save 10% with our Sets" ), true, [{:type => "vid", :qualifiers => ["19343123120224"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18356472283232"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, nil, PercentageDiscount.new( 10, "Save 10% with our Sets" ), true, [{:type => "vid", :qualifiers => ["19343386378336"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18356472283232"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, nil, PercentageDiscount.new( 10, "Save 10% with our Sets" ), true, [{:type => "vid", :qualifiers => ["19342537359456"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19350922264672"], :quantity => "1"}, {:type => "vid", :qualifiers => ["12245771944052"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, nil, PercentageDiscount.new( 10, "Save 10% with our Sets" ), true, [{:type => "vid", :qualifiers => ["19342537359456"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19350922264672"], :quantity => "1"}, {:type => "vid", :qualifiers => ["19350794207328"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, nil, PercentageDiscount.new( 10, "Save 10% with our Sets" ), true, [{:type => "vid", :qualifiers => ["19350079733856"], :quantity => "1"}, {:type => "vid", :qualifiers => ["13568284196960"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), BundleDiscount.new( :all, nil, nil, PercentageDiscount.new( 10, "Save 10% with our Sets" ), true, [{:type => "vid", :qualifiers => ["19343434514528"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18356472283232"], :quantity => "1"}, {:type => "vid", :qualifiers => ["18844374270048"], :quantity => "1"}] ), ].freeze CAMPAIGNS.each do |campaign| campaign.run(Input.cart) end Output.cart = Input.cart
Portrait of Stephen positioned next to an image of planet Earth, with the Stephen's World ...
By JasonH Mar 18, 2024Digital marketers and app developers have tracked activity in apps and websites for yea...
By Ollie Mar 13, 2024February was an exciting month with Shopify Editions, informative webinars, and more! F...
By JasonH Mar 7, 2024