Let your customers subscribe to an out of stock product with Spree Commerce -- Refactored --
Tags: spreecommerce, javascript, ruby, rails
Published: 2023-03-19 11:53:01

---
title: Let your customers subscribe to an out of stock product with Spree Commerce -- Refactored --
published: true
description: Implement a product subscription model using Spree Commerce
tags: spreecommerce, js, ruby, rubyonrails
//cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/flarn3q4tekv8lfj9q0d.png
---

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/flarn3q4tekv8lfj9q0d.png)

This is a refactored version of my original post [here](https://dev.to/mirmayne/let-your-customers-subscribe-to-an-out-of-stock-product-with-spree-commerce-2mgl/) that uses the [spree_frontend](https://github.com/spree/spree_legacy_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`

```rb
# 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)
```
VariantsIncludingMasterControllerDecorator was also refactored as we will only send an email when no longer tracking inventory:

```rb
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)
```
I've also refactored `NotifyCustomers` and instead look for the variant instead of the product:

```rb
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
```
Migration file was also updated (I'm doing this in a new project):

```rb
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
```
Updated models:

```rb
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)
```

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

```rb
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'

```

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

```erb
<%= 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 %>
```
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`):

```erb
<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>
```

Using..

```erb
<%= turbo_stream.replace dom_id(@variant, :notify) do %>
  <%= render "spree/shared/variant_notify_success" %>
<% end %>
```
...outside the order form:

```erb
<%# Outside order form %>
<% unless is_product_available_in_currency && @product.can_supply? %>
  <%= render 'spree/shared/notify_me_when_available', variant: default_variant %>
<% end %>
```
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](https://github.com/spree/spree_legacy_frontend/blob/main/app/assets/javascripts/spree/frontend/views/spree/products/cart_form.js#L7). 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](https://github.com/spree/spree_legacy_frontend/blob/main/app/assets/javascripts/spree/frontend/views/spree/products/cart_form.js#L216).

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](https://www.imaynerobotics.com/products/deebot-x1-omni-robot-vacuum-and-mop).