"Pumpkin RTS" (Ready-to-ship app)

using Laravel / Vue.js

Posted on February 5, 2019 in laravel, webdev

What is it?

A few posts back I wrote about the first version of our Etsy shop order manager.

At the time I was using the MEAN stack for most of my projects. I've since become more of a Laravel/Vue.js enthusiast, so for our newest version, "Pumpkin RTS", that's what I used. "Pumpkin RTS" combines the features of past order managers with an inventory management system. As I learned what our employees preferred in the workflow I simplified the front-end and re-worked the backend to be more efficient.

The Problem

As our average weekly orders increased, we realized we couldn't keep up if we printed the stickers as the orders came in; we needed an inventory. Without an up-to-date inventory log it would get messy assigning tasks to employees, so we needed to keep track of our inventory digitally.

Also, The previous version had a few bugs and needed refactoring to be scalable. And some features weren't used so they were taken out.

Vue Components

Vue.js let's us deal with small snippets of html and their corresponding scripts together, making it easy to see the app's layout just from viewing the file structure.

<div id="app">
  {{ message }}
</div>
<script>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})
<script>

Since I'm retrieving data about "sticker" type orders, I made "order" and "orders" components. Using v-for I can display multiple orders.

<div class="card" v-for="(order, index) in orders" :key="order.order_id" 
style="margin-bottom: 40px">
    <div class="card-body">
      <div class="row">
      <h4 style="color: #F97D62"><b>{{order.name}}
      </b></h4><hr />
    </div>

    <table class="table table-borderless table-hover table-sm">
       <thead>
         <tr>
           <th scope="col"></th>
           <th scope="col" class="text-center">Sheet</th>
            <th scope="col">Color</th>
            <th scope="col" class="text-center">Inventory</th>
         </tr>
      </thead>
      <tbody>
        <tr v-for="(t, index) in order.Transactions" 
        v-bind:class="{'table-success': t.currency_code > 0}">
          <td class="text-center">
            <span v-if="t.quantity > 1" style="color: #F97D62">
              <b>({{t.quantity}})</b>
            </span>
          </td>
          <td class="text-center">{{t.title | extract_number}}</td> 
          <td><span v-if="t.variations[0]">
            {{t.variations[0].formatted_value | format_color}}
          </span></td>
          <td class="text-center" v-if="t.currency_code != 'USD' && 
          t.variations[0] != null" style="color: red">
            {{t.currency_code}} 
            <button v-if="t.currency_code > 0"  class="btn btn-primary btn-sm"
            @click="markInventoryItemUsed(t.title.substr(1, 3), 
            t.variations[0].formatted_value, t.currency_code);  
            t.currency_code = 0">Used</button>
          </td>
          <td class="text-center" v-else-if="t.currency_code != 'USD'" style="color: red">
            {{t.currency_code}} <button v-if="t.currency_code > 0" 
            class="btn btn-primary btn-sm" 
            @click="markInventoryItemUsed(t.title.substr(1, 3), null, t.currency_code); 
            t.currency_code = 0">Used</button>
          </td>
        </tr>
      </tbody>
    </table>

    <div class="alert alert-info" role="alert" v-if="order.message_from_buyer" 
    style="padding: 5px; margin-top:10px">
    <span v-html="order.message_from_buyer"></span></div>

    <h4 v-if="order.note" style="color: red">Note:</h4>
      @{{order.note}}
      <div class="input-group" style="margin-top: 20px">
        <input type="text" v-model="order.gift_message" class="form-control" >
        <span class="input-group-btn">
          <button class="btn btn-secondary" v-bind:class="{'btn-success': saving}" 
          @click="saveNote(order.order_id, order.creation_tsz, order.gift_message)" 
          type="button">Save</button>
        </span>
      </div>
   </div>
</div>

GET Orders

The app's dashboard shows a given day's sticker orders. Since we had an influx of orders, I couldn't load all of the orders at one time anymore -- Etsy has a limit per request. So I used axios to request all "pages" of results asynchronously to get all orders into one array on the front-end.

getOrders: function(min_date, max_date, offset){
   this.orders = []
   this.order_count = 0
   this.loading = true
   axios.get('/api/orders/dates/' + min_date + '/' + max_date + '/' + offset)
       .then(response => {
           var promises = []
           this.order_count = response.data.count
           this.orders = this.orders.concat(response.data.results)
           if (this.orders.length < this.order_count){
               var limit = response.data.pagination.effective_limit
               var iterations = (this.order_count / limit) - 1
               var promises = []
               var that = this
               for (var i = 0; i < iterations; i++) {
                   var next_offset = (i+1) * limit
                   promises.push(
                       axios.get('/api/orders/dates/' + min_date + '/' + max_date + 
                       '/' + next_offset).then(response => {
                           that.orders = that.orders.concat(response.data.results);
                       })
                   )
               }
               axios.all(promises).then(function() {
                   that.getNotes(min_date, max_date)                    
               });
           } else {
                this.getNotes(min_date, max_date)
           }
           this.loading = false
    });
}

Then I made an inventory list component and an "inventory item" component. This is the "todo" page where our employees can see which items they need to stock and stock them.

Integrating Etsy's API

With version 1 I used a node.js library to connect to Etsy's API. In this version I used this PHP library along with these Binance API docs. I used Laravel's service providers to load an Etsy API connection for the session.

public function register() {
   $this->app->singleton('etsy', function ($app) {
        $auth = env('services.etsy');
        $client = new \Etsy\EtsyClient($auth['consumer_key'], $auth['consumer_secret']);
        $client->authorize($auth['access_token'], $auth['access_token_secret']);
        return new \Etsy\EtsyApi($client);
    });
}

The service provider allows me to reference api methods like this...

$data = app('etsy')->findAllShopReceipts( array(
    'params' => array(
        'shop_id' => 'pumpkinpaperco',
    ),
    'data' => array(
        'min_created' => (int)$epoch_start,
        'limit' => 100
    ),
    associations' => array(
        'Transactions' => array('limit' => 100)
)));

Laravel Jobs

Instead of building the inventory all at once, which would be unnecessarily costly, we decided to use the upcoming months' orders to inform our decision about which items we'd stock up first. In order to collect the sales data and organize it, I used Laravel Scheduler to request and sort new orders twice a day.

Inside of Kernel.php in the "schedule" method I call a new command called "updateinventory"... $schedule->command('updateinventory')->twiceDaily(1, 13)

This calls on the "findAllShopReceipts" method mentioned earlier. Once we get the new orders, I save the relevant info into an "Inventory Item" model. With eloquent I can instantiate like this: $item = InventoryItem::firstOrNew(['sheet_id' => $sheet_id, 'color' => $color]). Those models can now be edited by our users on the front-end as they mark their progress in the fulfillment process.

With Laravel Forge, it's really easy to setup a new cron job. I just added the last row...

Next Steps

Since we're collecting specific order data now, I'd like to analyze it and see if we can make smarter business decisions using the insights it gives us. I also want to let the site auto-delegate inventory tasks to improve efficiency at the office.


comments powered by Disqus