DEV Community

Daveyon Mayne 😻
Daveyon Mayne 😻

Posted on

Let your customers subscribe to an out of stock product with Spree Commerce -- Refactored --

Alt Text

This is a refactored version of my original post here that uses the spree_frontend. To be able to follow along, please take 2-3 minutes to have a read. v1, as I would call it, uses an outdated version of Spree Commerce. v2 (this post) now uses Spree v4.5.x which uses the spree_backend gem.

I've updated StockItemsControllerDecorator

# https://github.com/spree/spree_backend/blob/main/app/controllers/spree/admin/stock_items_controller.rb module Spree module Admin module StockItemsControllerDecorator # renamed from NotifyCustomersHelper include NotifyCustomers def self.prepended(base) base.before_action :process_notifiees_on_stock_item, only: :update # We have not taken into account should stock_movement.save fails. # see https://github.com/spree/spree_backend/blob/main/app/controllers/spree/admin/stock_items_controller.rb#L13 base.before_action :process_notifiees_on_stock_movement, only: :create # We don't need to track when deleted as the "track Inventory" can be checked when deleted. # For this, look at Spree::Admin::VariantsIncludingMasterController#update # base.before_action :notify_notifiees, only: :destroy end end end end ::Spree::Admin::StockItemsController.prepend Spree::Admin::StockItemsControllerDecorator if ::Spree::Admin::StockItemsController.included_modules.exclude?(Spree::Admin::StockItemsControllerDecorator) 
Enter fullscreen mode Exit fullscreen mode

VariantsIncludingMasterControllerDecorator was also refactored as we will only send an email when no longer tracking inventory:

module Spree module Admin module VariantsIncludingMasterControllerDecorator include NotifyCustomers def self.prepended(base) # Defined in NotifyCustomers. Continue reading... base.before_action :notify_notifiees, only: :update end end end end ::Spree::Admin::VariantsIncludingMasterController.prepend Spree::Admin::VariantsIncludingMasterControllerDecorator if ::Spree::Admin::VariantsIncludingMasterController.included_modules.exclude?(Spree::Admin::VariantsIncludingMasterControllerDecorator) 
Enter fullscreen mode Exit fullscreen mode

I've also refactored NotifyCustomers and instead look for the variant instead of the product:

module NotifyCustomers private # We've made the executive decision by not keeping stocks. # Alert all customers that the product is available to purchase. def notify_notifiees variant_id = params[:id] not_tracking = !ActiveRecord::Type::Boolean.new.cast( params[:variant][:track_inventory] ) not_tracking && email_all_notifiees(variant_id) end def process_notifiees_on_stock_movement quantity = params[:stock_movement][:quantity].to_i variant_id = params[:variant_id] unless quantity.zero? email_all_notifiees(variant_id) end end def email_all_notifiees(variant_id) notifiees = lookup_notifiees_by(variant_id) send_notification_email(notifiees) # We said we'd delete their email address notifiees.destroy_all end def process_notifiees_on_stock_item # Backorderable: boolean # stock_item.backorderable # Number of items in stock: integer # stock_item.count_on_hand unless stock_item.count_on_hand.zero? variant_id = stock_item.variant.id email_all_notifiees(variant_id) end end def lookup_notifiees_by(variant_id) Spree::VariantNotification.where(variant_id: variant_id) end def send_notification_email(notifiees) if notifiees.present? emails_to_send = notifiees.pluck(:email) puts "---- SENDING EMAIL TO #{emails_to_send.length} email addresses (plural for now)" # Now send the email # You'll need to implement a mailer for this end end end 
Enter fullscreen mode Exit fullscreen mode

Migration file was also updated (I'm doing this in a new project):

class CreateSpreeVariantNotifications < ActiveRecord::Migration[7.0] def change create_table :spree_variant_notifications do |t| t.references :user t.references :variant, null: false t.string :email, null: false t.timestamps end end end 
Enter fullscreen mode Exit fullscreen mode

Updated models:

module Spree class VariantNotification < ApplicationRecord validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP } # Optional as a user doesnt have to be signed in belongs_to :user, class_name: 'Spree::User', optional: true belongs_to :variant, class_name: 'Spree::Variant' end end module Spree module VariantDecorator def self.prepended(base) base.has_many :notifications, class_name: 'Spree::VariantNotification', foreign_key: 'variant_id', dependent: :destroy end end end ::Spree::Variant.prepend Spree::VariantDecorator if ::Spree::Variant.included_modules.exclude?(Spree::VariantDecorator) 
Enter fullscreen mode Exit fullscreen mode

Also the place where the email gets processed into our database, the controller:

module Spree # Though belongs to a variant, we'll keep this for the product module Products class NotifyController < Spree::StoreController before_action :set_variant include ActionView::Helpers::TextHelper # Does not uses a layout layout false def notify_me email = strip_tags(notify_params[:email]) @notif = @variant.notifications.find_or_create_by(email: email) do |perm| # user_id can be null perm.user_id = spree_current_user&.id || notify_params[:user_id] end respond_to do |format| if @notif.save format.turbo_stream else format.html { redirect_to spree.product_path(@variant.product), alert: "Something went wrong", status: 422 } end end end private def notify_params params.fetch(:product_notification, {}).permit(:email) end def set_variant @variant = Spree::Variant.find_by(id: params[:variant_id]) unless @variant.present? redirect_to root_path end end end end end # Route updated to: post '/products/:variant_id/notify', to: 'products/notify#notify_me', as: 'variant_notify' 
Enter fullscreen mode Exit fullscreen mode

_notify_me_when_available.html, inside _cart_form but placed outside the order form was also updated:

<%= form_with( scope: :product_notification, url: spree.variant_notify_path(variant), data: { controller: "reset-form", action: "turbo:submit-end->reset-form#reset" }, id: dom_id(variant, :notify)) do |form| %> <%= form.label :email, "Notify me when in stock" %> <%= form.email_field :email, class: 'spree-flat-input', placeholder: "Enter your email address", required: true %> <%= form.submit "Notify me", class: "btn btn-primary w-100 text-uppercase font-weight-bold mt-2" %> <p class="mt-2 text-sm">After this notification, your email address will be deleted and not used again.</p> <% end %> 
Enter fullscreen mode Exit fullscreen mode

I'm using turbo in this new project of mine. When form gets submitted, I clear the input field and when successful, I update the DOM with a partial (spree/shared/_variant_notify_success):

<div class="p-3 text-center bg-green-600 text-white sm:text-base text-sm"> <p>Great! We'll send you a one-time email when item becomes available.</p> </div> 
Enter fullscreen mode Exit fullscreen mode

Using..

<%= turbo_stream.replace dom_id(@variant, :notify) do %> <%= render "spree/shared/variant_notify_success" %> <% end %> 
Enter fullscreen mode Exit fullscreen mode

...outside the order form:

<%# Outside order form %> <% unless is_product_available_in_currency && @product.can_supply? %> <%= render 'spree/shared/notify_me_when_available', variant: default_variant %> <% end %> 
Enter fullscreen mode Exit fullscreen mode

Additional work is needed to show/hide the notification form should a variant is out of stock. My example will only display the notification form when product cannot supply.

Spree frontend has an event lister for .product-variants-variant-values-radio as seen here. You could put your logic there to show/hide the notification form. Here's the logic:

Inside your click event for OPTION_VALUE_SELECTOR (in your own js file or use Stimulus):

With the selected variant_id, parse the data for data-variants on the cart from. Inside the array of data-variants, check that the variant is purchasable, using variants.find(o.id == variant_id).purchasable, for example. Based on true or false, show/hide the notification form. I got purchasable from here.

That should be all for now. At the time of this post, I have not implement show/hide when an option is selected. Live demo here.

Top comments (0)