YAMAP エンジニア Advent Calendar 2019 の16日目になります。
YAMAPで、フロントエンドエンジニア?として活動中の @issyxissy です。
いつもは、フロントエンド周りをやっていますが、 YAMAP STORE の運用部分も携わっています。
YAMAP STOREとは?
YAMAPでは、実は、アウトドア・登山のギア(道具)を販売している YAMAP STORE というサイトがあります。
YAMAP STORE で販売しているギアは、YAMAPスタッフが目利きを行い、自分たちが良いと思うものを仕入れて、販売しています。
珍しいところでいうと、パーゴワークスさん、ティートンブロスさん、他にもいろいろ取り扱っています。
その他にも、プライベートブランドも開発して、いろんなユーザさんに使ってもらっています。
(宣伝になってきた・・・)
そのYAMAP STOREですが、ShopifyというSaas型のEコマースシステムで運用しています。
Shopifyって?
- Saas型のEコマース
- 運用楽ちん
- テーマと呼ばれる、テンプレートがあり、いろんなUI・デザインを選ぶことができる
- そのテーマをカスタマイズもできる
- 決済周りでは、Shopify Paymentを利用して決済も可能
- 他の決済システムとの連動も可能
- コンビニ決済や、代引も導入しようとすれば可能
- 越境ECに対応
- 各国の通貨に合わせた決済が実現可能(バンザイ、越境EC)
- APIも豊富
- StoreFront APIと呼ばれるUI部分に組み込むことができるAPIの利用も可能
- 運用面で、REST Admin APIで、いろんなマスタを読み書き可能
- plugableに、機能を増やすことが可能
- 世界のShopify Developerさんたちが、日々開発して、新しい機能を提供しています
- MAツールっぽいものもあるので、エンジニアでなくても、容易に導入しやすいです
- Saas型の割に、いろいろカスタマイズ可能
- 割引などのカート内に関わるプログラムを書くことができる Script Editorや、トリガーをきっかけにアクションを実行できるShopify Flowなど、意外とカスタマイズができます。
本題のデータ連携
概要
具体的にどんなことをしているかというと、、、
のように連携して、出荷業務等を自動化しています(今は半自動化・・・)
連携しているデータは
- 出荷管理
- 入荷管理
- 商品・在庫管理
の3つになります。
GASを選んだ理由
自動化する上で、GASを選んだ理由・・・なんとなく、手軽だから・・・(ノ ꒪⌓꒪)ノ
というわけではないんですが、仕入れ管理等を行う上で、一からシステムを構築したくないというのがありました。
また、一から構築するとなると、かなりの時間とコストが掛かるため、サービス立ち上げ当初なら、ということで、選びました。
メリット
- 共有の管理がしやすい
- 履歴も残る
- Scriptも書きやすい
- Spreadsheet / Google Calendarなど、google関連のサービスとの連携が楽ちん
- みんなが慣れている
※メリットは言わずとも、あるあるな感じです
デメリット
- 基幹システムようなものより安定性にかける・・・
- たまに、トリガーが動かないが、、、稀ですが・・・( ꒪⌓꒪;
- ES6など、モダンな書き方ができない
- やろうと思えばできるっぽいけど、後々、そっちに移行したい(ノ ꒪⌓꒪)ノ
出荷連携の実装例
あくまでも簡易的ですが、おおよそ↓のようになるのかなと思います。
Shopify
管理画面から、 アプリ管理 -> プライベートアプリ管理 で、APIキーを発行します。
GAS
GASでは、Shopifyから注文データを取得します。
(実行状況をSlackに通知するので、SLACK_WEBHOOKの情報もプロジェクトのプロパティに設定しています)
サンプルコードは↓
var SLACK_WEBHOOK = PropertiesService.getScriptProperties().getProperty('SLACK_WEBHOOK');
var SHOPIFY_API_KEY = PropertiesService.getScriptProperties().getProperty('SHOPIFY_API_KEY');
var SHOPIFY_PASSWORD = PropertiesService.getScriptProperties().getProperty('SHOPIFY_PASSWORD');
var SHOPIFY_API_URL = 'https://xxxxxxxxx.myshopify.com/admin/api/xxxx-xx';
function exportOrders() {
// Shopifyの注文データを取得
var response = _shopifyAPI('/orders.json?status=open&fulfillment_status=unshipped&limit=250', 'get');
// 注文データを加工
var orders = [];
for (var index of response.orders) {
var order = response.orders[index];
// ~~ 注文データをごにょごにょ ~~
}
// WMSへデータ送信
var response = _wmsAPI(xxxx, 'post', orders);
// Slackに終わったよを送信
_postSlackMessage('WMSに出荷データを送信したお');
}
// ShopifyAPIへリクエスト
function _shopifyAPI(url, method, params) {
var options = {
method: method,
payload: JSON.stringify(params),
headers: {
"Authorization": "Basic " + Utilities.base64Encode(SHOPIFY_API_KEY + ":" + SHOPIFY_PASSWORD),
"Content-type": 'application/json',
},
muteHttpExceptions: false,
};
var response = UrlFetchApp.fetch(SHOPIFY_API_URL + url, options);
return JSON.parse(response.getContentText());
}
// Slackメッセージを送信
function _postSlackMessage(message) {
var jsonData = {
username: 'YAMAP STORE bot',
text: message
};
var options ={
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(jsonData)
};
UrlFetchApp.fetch(SLACK_WEBHOOK, options);
}
// WMS API へのリクエスト
function _wmsAPI(url, method, params) {
// ~~~~~~~~~~~~~
return xxx;
}
構成の応用
- 履歴管理とかした場合、GASからSpreadsheetに出力しておくと、色々と便利です。
- また、原価管理とか、データ分析を行うのなら、こんな感じでもよいかもしれません
今後
カート〜チェックアウトフローのUIにこだわったり、他システムとの連携を柔軟にしたいので、↓のことをやってみたいなーって思います。
- チェックアウトフローのUIをこだわって、作ってみたいなー(ノ ꒪⌓꒪)ノ
- headless commerceで、構築してみたいなー(ノ ꒪⌓꒪)ノ
- GraphQLも使ってみたいなー(ノ ꒪⌓꒪)ノ
- 記事周りを柔軟にするために、headless cmsを利用したいなー(ノ ꒪⌓꒪)ノ
ちなみに
- Shopifyの契約プランに応じて、できることは限られますので、ご注意をください(ノ ꒪⌓꒪)ノ
最後に小技
GASで日時指定して、実行したい場合、triggerで次の日時をセットすることで、毎日同じ時間に実行することができます。
例えば、毎日10時に、exportOrders関数を実行した場合は、↓で実現可能です。
var trigger = {
set: function(type, date) {
ScriptApp.newTrigger(type).timeBased().at(date).create();
},
remove: function(type) {
var triggers = ScriptApp.getProjectTriggers();
for(var i=0; i < triggers.length; i++) {
if (triggers[i].getHandlerFunction() == type) {
ScriptApp.deleteTrigger(triggers[i]);
}
}
}
};
var date = new Date();
date.setDate(date.getDate() + 1);
date.setHours(10);
date.setMinutes(0);
trigger.remove('exportOrders');
trigger.set('exportOrders', date);