Let your customers subscribe to an out of stock product with Spree Commerce
Tags: spreecommerce, javascript, ruby, rails
Published: 2021-03-06 10:56:26

---
title: Let your customers subscribe to an out of stock product with Spree Commerce
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)

----- Updated 👉 [Refactored version](https://dev.to/mirmayne/let-your-customers-subscribe-to-an-out-of-stock-product-with-spree-commerce-refactored--2jp0) ------

Sometimes it's good to have your customers be notified when an item they wish to purchase comes in stock. In reality they'd go elsewhere to buy, right? If it's available elsewhere, cheaper, they'd pretty much go somewhere else to purchase. But what if what you sell is unique, of better quality or your customer loves your support or you in general? We're going to implement an out of stock email notification system; I call this "notifiees". The beauty of this is that we can name or variables/definitions just the way we like 😂

Before diving right in, let's see how Spree updates the product quantity. The "amount of items in stock" is what we want to target although most businesses do not use "track inventory". We'll come to that in future explanation.

If your business relies on inventory/stock, then this tutorial is for you.

In the [StockItemsController](https://github.com/spree/spree/blob/master/backend/app/controllers/spree/admin/stock_items_controller.rb), we want to "watch" three actions:

* update
* create
* destroy

### Update
When we update a stock item, we should email all customers, providing the quantity is more then 0 🙄

### Create
Again, send an email to all customers when stock movement is added.

### Destroy
We've decided that we're no longer tracking inventory. This enables the add to cart button so why not email all customers.

```rb
# see https://github.com/spree/spree/blob/master/backend/app/controllers/spree/admin/stock_items_controller.rb
module Spree
    module Admin
        module StockItemsControllerDecorator
            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/blob/master/backend/app/controllers/spree/admin/stock_items_controller.rb#L13
                base.before_action :process_notifiees_on_stock_movement, only: :create

                base.before_action :notify_notifiees, only: :destroy
            end

            

            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 = stock_item.variant.id

                    email_all_notifiees(variant_id)
                end

                def process_notifiees_on_stock_movement
                    quantity = params[:stock_movement][:quantity].to_i
                    variant_id = params[:variant_id]

                    if quantity > 0
                        email_all_notifiees(variant_id)
                    end

                end

                def email_all_notifiees(variant_id)
                    product_id = Spree::Variant.find(variant_id).product.id

                    notifiees = lookup_notifiees_by(product_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
                    
                    if stock_item.count_on_hand > 0
                        variant_id = stock_item.variant.id
                        email_all_notifiees(variant_id)
                    end
                end

                def lookup_notifiees_by(product_id)
                    ProductNotification.where(product_id: product_id)
                end

                def send_notification_email(notifiees)
                    if notifiees.present?
                        emails_to_send = notifiees.pluck(:email)
                        # send the email
                    end
                end
        end
    end
end
::Spree::Admin::StockItemsController.prepend Spree::Admin::StockItemsControllerDecorator if ::Spree::Admin::StockItemsController.included_modules.exclude?(Spree::Admin::StockItemsControllerDecorator)
```
This logic does not take into account where the customer would subscribe to the variant but instead the product itself. Ideally you'd want to change this code to suite your business logic. [My online store](https://www.imaynerobotics.com/) won't be using different types of variants but as I go along, it will.

In the code you'll notice I have `ProductNotification`. `ProductNotification` model is where I save the "notifiees". I save their email address, product id (you may want to save the variant instead, I may change this up) and, optionally, a user id.

### Migration:

```rb
class CreateProductNotifications < ActiveRecord::Migration[6.1]
  def change
    create_table :product_notifications do |t|
      t.references  :user, null: true
      t.references  :product
      t.string      :email

      t.timestamps
    end
  end
end
```

### Model:

```rb
class ProductNotification < ApplicationRecord
    validates   :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP } 
    belongs_to  :user, class_name: 'Spree::User', optional: true
    belongs_to  :product, class_name: 'Spree::Product'
end

```

The `user` is optional because I want guests to also subscribe and if a user is logged in, it will use their user id.

### Spree Product Model

```rb
module Spree
    module ProductDecorator
        def self.prepended(base)
            base.has_many :product_notifications, class_name: 'ProductNotification', foreign_key: 'product_id', dependent: :destroy
        end
    end
end

::Spree::Product.prepend Spree::ProductDecorator if ::Spree::Product.included_modules.exclude?(Spree::ProductDecorator)
```

### Controller:

```rb
class Spree::Products::NotifyController < ApplicationController
    include ActionView::Helpers::TextHelper

    layout false
    
    def notify_me
        email = strip_tags(notify_params[:email])

        @notif = ProductNotification.find_or_create_by(email: email) do |perm|
            perm.product_id = notify_params[:product_id]
            perm.user_id = notify_params[:user_id]
        end

        if @notif.save
            @notif_saved = true
        else
            @notif_saved = false
        end
    end

    private
        def notify_params
            params.require(:product_notification).permit(
                :email,
                :product_id
            ).tap do |p|
                # Overkill to have this here, I know.
                p[:user_id] = spree_current_user ? spree_current_user.id : nil
            end
        end
end
```

### Routes
I save all routes under ` Spree::Core::Engine.add_routes do` block:

```rb
[..]

Spree::Core::Engine.add_routes do
  [..]
  post '/products/notify', to: 'products/notify#notify_me', as: 'product_notify'
  [..]
end
```

### Frontend
For the frontend, I modify the `_cart_form.html` and my notify form is shown when I cannot supply:

```erb
[..]
<% if !@product.can_supply? %>
  <%= render 'notify_me_when_available' %>
<% end %>
```

And inside `_notify_me_when_available.html.erb`:

```erb
<form
    data-controller="product-notify"
    data-action="ajax:success->product-notify#result"
    class="product-notify-me mt-4" data-remote="true" action="<%= spree.product_notify_path %>" method="post">
    <input
        type="hidden"
        name="product_notification[product_id]"
        value="<%= @product.id %>"
    />
    <div class="form-group mb-2">
        <label>Notify me when in stock</label>
        <input 
            name="product_notification[email]"
            data-product-notify-target="email"
            class="spree-flat-input" 
            type="text" placeholder="Enter your email address" 
        />    
    </div>
    <span class="product-description">Your email address will only be used for this notification, after which it gets deleted.</span>
    <div>
        <button class="btn btn-primary w-100 text-uppercase font-weight-bold mt-2">Notify me</button>
    </div>
    <div data-product-notify-target="display">
    </div>
</form>
```
I haven't change things up to use rails form elements as yet but you should. I use Stimulus here, you really don't need to. I'm only making an ajax request to the above controller, passing in the `product_id` and `email` then update the UI if any errors.

Inside `views/spree/products/notify/notify_me.html.erb`:

```rb
<%# This is server-rendered %>

<% if @notif_saved %>
    <div class="alert alert-success">
        Great! We'll send you a one-time email when item becomes available.
    </div>
<% else %>
    <div class="alert alert-danger">
        Oops! You may have provided an invalid email address.
    </div>
<% end %>
```

### Stimulus Controller:

```js
import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "display", "email" ]

  result(event) {
    const data = event.detail[0].body.innerHTML
    if (data.length > 0) {
      this.emailTarget.value = ""
      return this.displayTarget.innerHTML = data
    }
  }
}
```
Using Stimulus for this is overkill as you could achieve what you want with a simple, plain JavaScript. If you want to use Stimulus, be sure to import the js file in `views/spree/shared/_head.html.erb`

At this stage, when you save an email address, you should see this:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/41qz0l78d8ltgchz0h6u.png)

## Show the notifees count
To see the total amount of potential customers watching your product, in `views/spree/admin/products/index.html.erb`, within the loop, inside the table just add:

```erb
 <td class="notify"><%= product.product_notifications.count %></td>
```

### Accidentally sending out email
You may want to put a confirmation action on "backordable" as whenever that's ticked/unticked, emails will be sent out 😁
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8eka42i8mzzm60d3s4sz.png)

## Big Issue

There's a flaw to this implementation. Should you untick/tick "Track Inventory", these actions will never get triggered because that action is located at [Spree::Admin::VariantsIncludingMasterController#update](https://github.com/spree/spree/blob/master/backend/app/controllers/spree/admin/variants_including_master_controller.rb) which points to [here](https://github.com/spree/spree/blob/master/backend/app/controllers/spree/admin/resource_controller.rb#L25).

## Conclusion
I'm still scratching the surface on what [Spree Commerce](https://spreecommerce.org/) is capable of. I don't feel this implementation is perfect but it's a starting point for me. I will find a better way in doing this but I don't want to go to the Rails event path to watch for database updates. If anyone has a better solution, please let me know. For now, I'll use this method, change the code to have customers subscribe to variant id instead of the product id.

## EDIT
Write once, use everywhere. We need or logic in `VariantsIncludingMasterController` so let's move our code from `StockItemsControllerDecorator` into a helper method:

```rb
module NotifyCustomersHelper
    # 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 = stock_item.variant.id

        email_all_notifiees(variant_id)
    end

    def process_notifiees_on_stock_movement
        quantity = params[:stock_movement][:quantity].to_i
        variant_id = params[:variant_id]

        if quantity > 0
            email_all_notifiees(variant_id)
        end

    end

    def email_all_notifiees(variant_id)
        product_id = Spree::Variant.find(variant_id).product.id

        notifiees = lookup_notifiees_by(product_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
        
        if stock_item.count_on_hand > 0
            variant_id = stock_item.variant.id
            email_all_notifiees(variant_id)
        end
    end

    def lookup_notifiees_by(product_id)
        ProductNotification.where(product_id: product_id)
    end

    def send_notification_email(notifiees)
        if notifiees.present?
            emails_to_send = notifiees.pluck(:email)
            # send the email
        end
    end
end
```

Now, our `StockItemsControllerDecorator` becomes:

```rb
# see https://github.com/spree/spree/blob/master/backend/app/controllers/spree/admin/stock_items_controller.rb
module Spree
    module Admin
        module StockItemsControllerDecorator
            include NotifyCustomersHelper

            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/blob/master/backend/app/controllers/spree/admin/stock_items_controller.rb#L13
                base.before_action :process_notifiees_on_stock_movement, only: :create

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

Next, create `VariantsIncludingMasterControllerDecorator` inside `spree/admin/variants_including_master_controller_decorator.rb`:

```rb
# See: https://github.com/spree/spree/blob/master/backend/app/controllers/spree/admin/variants_including_master_controller.rb
module Spree
    module Admin
        module VariantsIncludingMasterControllerDecorator
            include NotifyCustomersHelper

            def self.prepended(base)
                base.before_action :send_notification_email_on_inventory_change, only: :update
            end

            def send_notification_email_on_inventory_change
                variant_id = params[:id].to_i
                track_inventory = params[:variant][:track_inventory]
                
                # If we're no longer tracking, send email
                # track_inventory comes in the form of string "true" or "false"
                if track_inventory == 'false'
                    email_all_notifiees(variant_id)
                end
            end
        end
    end
end
::Spree::Admin::VariantsIncludingMasterController.prepend Spree::Admin::VariantsIncludingMasterControllerDecorator if ::Spree::Admin::VariantsIncludingMasterController.included_modules.exclude?(Spree::Admin::VariantsIncludingMasterControllerDecorator)
```

Now when we no longer track the inventory, all customers get an email notification. Be careful with this approach. You may accidentally unticked/ticked these boxes which triggers an email to send to all "watchable" customers. You may want to create a dedicated button for this action.

## Homework
You will need to pass the product that the customer was watching to the `send_notification_email` function. In the email, your customer would click a link that takes them directly to the product page.