Virtual Inventory Location

I work on an application that sells Widgets. You as might know, a Widget can be a pretty serious investment. What you might not know is that for people who take Widgets seriously, choosing the right one can be very personal. Because of this reason, the widget application that I work on offers customers the chance to order a home Try-on kit. After supplying some collateral, we will send you a box of six widgets in various configurations. You have a week to live with them in your home. At the end of the trial period you simply keep the one that you love the most and send the rest back to us, postage paid in full both ways! If you happen to be in the market I highly recommend you give it a try!

Naturally, in order to offer this home try-on service we need to have a demo fleet of widgets and a robust inventory management system. We track all the widgets that exist in the warehouse, the factory and in every package that gets sent out for home try-on. One thing that we didn't track however was the aggregate of all the widget inventory thats currently out on demo. And that's what I was tasked with building today!

When Admin users needs to take stock of an inventory location, the Factory for example, they can expect to see something like this:

Type Quantity
Blue widget 340
Green widget 320
Yellow widget 42
Red widget 630
Pink widget 20

This gives them a high level view of what Widgets might need to be transferred from another location. This has worked well up to this point but now the problem is our home try-on service is so popular we end up with a significant amount of inventory out in transit. This impacts our ability to forecast inventory needs for manufacturing. So one of our admin users requested that we provide a similar table that reports on the aggregate of all the try-on kits currently out in transit.

When I first designed this inventory system I made a decision that try-on kits should partially implement the interface of an inventory location. This made the flow of inventory in and out of Try-on kits very convenient. Reflecting on that gave me the idea of creating a virtual inventory location. It would only need an interface that conforms to what is exposed by the views (since this is a read only use case) and a collection from which it could derive calculated properties:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
describe KitsInventoryLocation do
  before(:each) do
    variant = create(:variant)
    kit_1 = build(:try_on_kit, state: 'shipped')
    kit_1.save validate: false
    kit_1.add_stock(variant, 1)
    kit_1.placements.create(variant_id: variant.id)

    kit_2 = build(:try_on_kit, state: 'shipped')
    kit_2.save validate: false
    kit_2.add_stock(variant, 1)
    kit_2.placements.create(variant_id: variant.id)
  end

  describe '#total_count_on_hand' do
    it 'returns a count of all the products in shipped kits' do
      kits_location = KitsInventoryLocation.new
      expect(kits_location.total_count_on_hand).to eq(2)
    end
  end

  describe '#inventory_items' do
    it 'returns an instance of MockInventoryItemCollection' do
      kits_location = KitsInventoryLocation.new

      expect(kits_location.inventory_items)
        .to be_kind_of(MockInventoryItemCollection)
    end
  end
end

The implementation turned out like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class KitsLocation
  attr_accessor :kits

  def initialize
    self.kits = WidgetKit.shipped
  end

  def name
    'All Try-on Kits'
  end

  def to_param
    'kits_location'
  end

  def total_count_on_hand
    kits.reduce(0) { |a, e| a + e.total_count_on_hand }
  end

  def inventory_items
    @inventory_items ||= MockInventoryItemCollection.new(group_by_variants)
  end

  private

  def group_by_variants
    @group_by_variants ||= kits.flat_map(&:placements).group_by(&:variant)
  end
end

Now I can mix this location in with all the others. In their controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Admin::InventoryLocationsController < Admin::BaseController
  helper_method :inventory_location, :inventory_locations

  def index; end

  def show; end

  private

  def kits_location
    @kits_location = KitsLocation.new
  end

  def inventory_location
    @inventory_location ||= if params[:id] == 'kits_location'
      kits_location
    else
      InventoryLocation.find(params[:id])
    end
  end

  def inventory_items
    @inventory_items ||= inventory_location.inventory_items.active_items
  end

  def inventory_locations
    @inventory_locations ||=
      InventoryLocation.includes(:inventory_items).all.to_a << kits_location
  end
end

And in their views. E.g. index:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<table class="table table-bordered table-striped">
  <thead>
    <tr>
      <th>Name</th>
      <th>Total count on hand</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <% inventory_locations.each do |location| %>
      <tr>
        <td><%= location.name %></td>
        <td><%= location.total_count_on_hand %></td>
        <td width="200">
          <%= link_to "Show", admin_inventory_location_path(location), class: "btn btn-primary" %>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

E.g. show:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<table class="table table-bordered table-striped">
  <tr>
    <th>Product</th>
    <th>Variant</th>
    <th>SKU</th>
    <th>Quantity</th>
  </tr>
  <% @inventory_items.each do |item| %>
    <tr>
      <td><%= item.variant.product.name %></td>
      <td><%= item.variant.name %></td>
      <td><%= item.variant.sku %></td>
      <td><%= item.count_on_hand_less_backorders %></td>
    </tr>
  <% end %>
</table>

KitsInventoryLocation#total_count_on_hand is used to show our virtual inventory location in a collection with real inventory locations, e.g. a index of inventory locations:

Location Total Quantity
The Factory 34,982
The Warehouse 80,6543
All Try-on Kits 113,486

KitsInventoryLocation#inventory_items is a collection of virtual inventory items. I needed a simple collection thats would accept a hash of inventory grouped by Widget type and build out MockInventoryItems which represent the count. That's why we have that private method, KitsLocation#group_by_variants

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
describe MockInventoryItemCollection do
  describe '#initialize' do
    let(:variant) { create(:variant) }
    let(:placement) { create(:kit).placements.create(variant_id: variant.id) }
    let(:valid_argument) { { variant => [placement] } }

    it 'expects a hash of kits grouped by variants' do
      expect { MockInventoryItemCollection.new(nil) }
        .to raise_error(ArgumentError)

      expect { MockInventoryItemCollection.new(valid_argument) }
        .to_not raise_error
    end

    it 'assigns an array of MockInventoryItems to @active_items' do
      collection = MockInventoryItemCollection.new(valid_argument)
      expect(collection.active_items).to be_a(Array)
      expect(
        collection.active_items.all? do |item|
          item.is_a?(MockInventoryItemCollection::MockInventoryItem)
        end
      ).to eq(true)
    end

    it 'sets the properties on the MockInventoryItems' do
      item = MockInventoryItemCollection.new(valid_argument).active_items.first
      expect(item.variant).to eq(variant)
      expect(item.count_on_hand_less_backorders).to eq(1)
    end
  end
end

I found Struct to be really handy in this case for building data types needed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MockInventoryItemCollection
  MockInventoryItem = Struct.new(:variant, :count_on_hand_less_backorders)
  attr_accessor :active_items

  def initialize(group)
    fail ArgumentError unless valid_argument(group)
    self.active_items = group.map { |k, v| MockInventoryItem.new(k, v.length) }
  end

  private

  def valid_argument(arg)
    return false unless arg.try(:is_a?, Hash)
    return false unless arg.keys.first.is_a? Product::Variant
    return false unless arg.values.first.is_a? Array
    arg.values.first[0].is_a? Kit::Placement
  end
end

You can see here that the implementation of MockInventoryItemCollection and MockInventoryItemCollection::MockInventoryItem really is just a simple collection of data types that are tailored to adapt to an existing interface. The meat of the problem was taken care of by passing the results of KitsLocation#group_by_variants into the initializer of MockInventoryItemCollection