What is it?
A few posts back I wrote about the first version of the 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 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.
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.