Laravel:NextEngineAPIで登録在庫の数を社内販売管理の個数に変更する【その2】

Laravel:NextEngineAPIで登録在庫の数を社内販売管理の個数に変更する【その2】

前回までの関連記事はこちら『access_token取得』『在庫変更の段取りを考える

今回は在庫更新のScriptを書きながら少々の解説をしたいと思います。
ただし、実際のScriptは色々仕込んだ事もあり『600行を超え』になってしまったので省略しながら書いていきます。

作成していて分かったNextEngineAPIの注意点

在庫更新関連で利用したAPIのみですが、幾つか注意すべきことがありました。

  1. refresh_tokenに値を入れているとaccess_tokenは自動的に変更してくれるが、タイミングが読めない
  2. CSVデータではスペースを「レコードの切り替わり」と判断される
  3. 商品マスタアップロードの成功が【更新の成功】ではない

項目1について

有効なaccess_tokenを持っていれば「token取得」から動かす必要はないのでSQLにtokenの格納庫を作成しました。で、有効期間内であれば保存しているtoken情報を使ってAPI接続しようと考えていたのですが問題が発生しました。

有効期間内のtokenであるはずなのにAPI接続で拒否られる

理由はrefresh_tokenが気を利かせてくれたことでした。
まだ有効期間内だったのですが、どうやらaccess_tokenを再発行してくれたようで、2つのtokenの内容と有効期限か変わっていました。何かのトリガーがあるのだと思いますがそこまでは解明できていません。

なので、APIアクセスの度にtokenと有効期限を返してくれるので【有効期限が変わっていたらSQLを変更】するように仕掛けました。

項目2について

社内システムにあってネクストエンジンに未登録の商品があるので、在庫調整の際に一緒に追加してしまおうと模索。そうしたところ「9項目の1行目に対してレコードが5項目しかない」と怒られました。

商品登録の必須項目『商品名』にスペースが入っているものが【次レコード送り】と認識さているようです。

項目3について

ネクストエンジンでは、APIアップロードと同時に適用が入りません。あくまでキューに入れるだけです。
なので『アップロード』と『値の適用』とは別物と考える必要があります。
『値の適用』でエラーが起こるとしても、アップロードでそのエラーを捉えることはできません。
エラーの原因を知りたい場合は【アップロードキュー検索API】を利用して欲しいとの事でした。

在庫数更新のScript

■全体的な流れ

  1. 有効な access_tokenがあるか確認
    ある場合 => 6  / ない場合 => 2
  2. refresh_tokenの有効性を確認
    期限切れの場合 => 4  / 期限内の場合 => 3
  3. refresh_tokenでaccess_tokenを再発行
    成功の場合 => 6  / 失敗の場合 => 4
  4. ログインしてuidなどを取得
    成功の場合 => 5  / 失敗の場合 => エラー吐いて終了
  5. uidからaccess_tokenを取得
    成功の場合 => 6  / 失敗の場合 => エラー吐いて終了
  6. access_tokenを使ってAPI接続
    ※ $selecterCodeに沿った各処理へ投げる

上記 1~5については『access_token取得』で書いたので省略します。

class NEAPI {

    public static function ConnectNE($selecterCode)
    {
        #token取得部(1~5)省略
        //////////////////////////////////////////////////
        // 動作の分岐 ※引数で動作を分けることでtoken発行までを共通化
        //////////////////////////////////////////////////
        Switch($selecterCode){
            case 1:
                #NE在庫数更新 ※ついでに出品中商品の登録
                self::StockUplode_NextEngine($access_token,$refresh_toke,$encode_access,$encode_refresh);
                break;
            case 99:
                #NEアップロードキュー検索 ※期間指定自動で昨日~本日
                self::QueSearch_NextEngine($access_token,$refresh_toke,$encode_access,$encode_refresh);
                break;
            default:
                break;
        }
    }
    
    public static function StockUplode_NextEngine($access_token,$refresh_toke,$encode_access,$encode_refresh)
    {
        #タイムアウトしない & パース時間を設定しない
        ini_set("max_execution_time",0);
        ini_set("max_input_time",0); 

        #ネクストエンジンの商品マスタからテンポラリテーブルを作成
        self::getItemData_NextEngine($access_token,$refresh_toke,$encode_access,$encode_refresh);
        #ネクストエンジンの在庫マスタからテンポラリテーブルに在庫数を登録
        self::getStockData_NextEngine($access_token,$refresh_toke,$encode_access,$encode_refresh);
        #出品中のシステム内商品情報の抽出
        $saleingItems = self::pickUpSaleItem_SystemData();
        If(!empty($saleingItems)){
            #システム内商品情報とテンポラリテーブルを比較して加減算
            self::checkStockSystemData_Temp($saleingItems);
            #テンポラリテーブルのマーカーで抽出ネクストエンジンアップロード
            self::ItemStockUplode_NextEngine($access_token,$refresh_toke,$encode_access,$encode_refresh);
        }
        return;
    }
    /////////////// NextEngin商品マスタ取得 //////////////////
    public static function getItem_NextEngine($access_token,$refresh_toke,$encode_access,$encode_refresh)
    {
        $Endpoint = 'https://api.next-engine.org/api_v1_master_goods/count';
        $header = array("Content-Type: application/x-www-form-urlencoded;charset=UTF-8");
        $params = 'access_token='.$access_token.'&refresh_token='.$refresh_toke.'&wait_flag=1';

        #エンドポイントへアクセスして情報取得
        $response = self::postSSLAPI_Curl($Endpoint,$header,$params);
        #JSONの取り出し
        $pattern = '/\{.+?\}/';
        preg_match_all($pattern, $response, $match);
        $list = (array)json_decode($match[0][0]);

        #sitelogincodesにend_Dateを登録
        //$access_token
        self::saveTokenEndDate_NextEngine($list['access_token'],$list['access_token_end_date'],$encode_access);
        //$refresh_toke
        self::saveTokenEndDate_NextEngine($list['refresh_token'],$list['refresh_token_end_date'],$encode_refresh);
        
        If(!empty($list['count'])){
            $count = (ceil($list['count']/10000));
            
            #NE商品マスタを取得
            $Endpoint = 'https://api.next-engine.org/api_v1_master_goods/search';
            $header = array("Content-Type: application/x-www-form-urlencoded;charset=UTF-8");
            $fields = 'goods_id,goods_representation_id,goods_name,goods_supplier_id,goods_jan_code,goods_maker_model_number,goods_cost_price,goods_display_price,goods_selling_price';
            for($i=0;$i < $count; $i++){
                #forループで取り出し
                $list = $ItemData = array();
                $params = 'access_token='.$access_token.'&refresh_token='.$refresh_toke.'&wait_flag=1&offset='.$i.'&fields='.$fields;
                
                $response = self::postSSLAPI_Curl($Endpoint,$header,$params);
                $list = (array)json_decode($response);

                #sitelogincodesにend_Dateを登録
                //$access_token
                self::saveTokenEndDate_NextEngine($list['access_token'],$list['access_token_end_date'],$encode_access);
                //$refresh_toke
                self::saveTokenEndDate_NextEngine($list['refresh_token'],$list['refresh_token_end_date'],$encode_refresh);
        
                If(!empty($list['data'])){
                    foreach($list['data'] as $id){
                        #取得したデータを商品テンポラリテーブルに格納
                        $item = new Producttemporary;
                        $item->product_id = $id->goods_id;
                        If(!empty($id->goods_maker_model_number)){$item->makercode = $id->goods_maker_model_number;}else{$item->makercode = NULL;}
                        If(!empty($id->goods_jan_code)){$item->jan = $id->goods_jan_code;}else{$item->jan = NULL;}
                        $item->jan = $id->goods_jan_code;
                        $item->productname = $id->goods_name;
                        $item->stock = NULL;
                        $item->originalprice = $id->goods_display_price;
                        $item->assunedcost = $id->goods_cost_price;
                        $item->salesprice = $id->goods_selling_price;
                        $item->shipper_id = $id->goods_supplier_id;
                        $item->mathstocks = NULL;//更新用差分計算結果
                        $item->onoff = 0;//更新適用フラグ
                        $item->save();
                    }
                }
            }
        }else{
            #個数調査失敗※ログに残す
        }
        return;
    }
    /////////////// NextEngin在庫マスタ取得 //////////////////
    public static function getStock_NextEngine($access_token,$refresh_toke,$encode_access,$encode_refresh)
    {
        $Endpoint = 'https://api.next-engine.org/api_v1_master_stock/count';
        $header = array("cache-control: no-cache;charset=UTF-8");
        $params = 'access_token='.$access_token.'&refresh_token='.$refresh_toke.'&wait_flag=1';

        #エンドポイントへアクセスして情報取得
        $response = self::postSSLAPI_Curl($Endpoint,$header,$params);

        #JSONの取り出し
        $pattern = '/\{.+?\}/';
        preg_match_all($pattern, $response, $match);
        $list = (array)json_decode($match[0][0]);

        #sitelogincodesにend_Dateを登録
        //$access_token
        self::saveTokenEndDate_NextEngine($list['access_token'],$list['access_token_end_date'],$encode_access);
        //$refresh_toke
        self::saveTokenEndDate_NextEngine($list['refresh_token'],$list['refresh_token_end_date'],$encode_refresh);
        
        If(!empty($list['count'])){
            $count = (ceil($list['count']/10000));
            
            #NE商品マスタを取得
            $Endpoint = 'https://api.next-engine.org/api_v1_master_stock/search';
            $header = array("Content-Type: application/x-www-form-urlencoded;charset=UTF-8");
            $fields = 'stock_goods_id,stock_quantity';
            for($i=0;$i < $count; $i++){
                #forループで取り出し
                $list = $ItemData = array();
                $params = 'access_token='.$access_token.'&refresh_token='.$refresh_toke.'&wait_flag=1&offset='.$i.'&fields='.$fields;
                $response = self::postSSLAPI_Curl($Endpoint,$header,$params);
                $list = (array)json_decode($response);

                #sitelogincodesにend_Dateを登録
                //$access_token
                self::saveTokenEndDate_NextEngine($list['access_token'],$list['access_token_end_date'],$encode_access);
                //$refresh_toke
                self::saveTokenEndDate_NextEngine($list['refresh_token'],$list['refresh_token_end_date'],$encode_refresh);
        
                If(!Empty($list['data'])){
                    foreach($list['data'] as $id){
                        #テンポラリに在庫数を登録
                        $item = Producttemporary::where('product_id',$id->stock_goods_id)->first();
                        $item->stock = $id->stock_quantity;
                        $item->save();
                    }
                }
            }
        }else{
            #個数調査失敗※ログに残す
        }
        return;
    }
    /////////////// 自社システム出品中抽出 //////////////////
    public static function pickUpSaleItem_SystemData()
    {
        $array = MySQLから出品中商品を抽出;
        return $array;
    }
    /////////////// レコード比較・差分計算 //////////////////
    public static function checkStockSystemData_Temp($saleingItems)
    {
        #出品中商品から抽出した値を使ってループ
        foreach($saleingItems as $saleslist){
            ////////////////////////////////////////
            // Saleitemsに格納されている価格情報を取り出す
            ////////////////////////////////////////
            #Select出品か否かを確認
            If(!empty(optional($saleslist->saleitem)->selectsalesmark)){
                #0はEmptyなのでここには選択肢商品のみが入る ※組み合わせ販売を振りから落せればOK
                If($saleslist->product_id == $saleslist->selectitem_id){
                    #選択肢IDとproduct_idが同じ => パターン販売
                    //価格情報確定
                    $salesprice = optional($saleslist->saleitem)->salesprice;
                    $assumedcost =optional($saleslist->saleitem)->assumedcost;
                    $originalprice = optional($saleslist->saleitem)->originalprice;
                    $shipper_id = optional($saleslist->saleitem->shipper)->pictfolder;
                }else{
                    #組み合わせ販売 か 設定ミス
                    //productconverterに複数のSaleitem_idが登録されていないかチェック
                    $subchecks = 選択肢販売のMySQL登録ルール
                            組合せ販売で発生するMySQL登録ルール
                            有効商品のルール->get();
                    If(!Empty($subchecks)){
                        #レコードが存在する
                        $salesprice = $assumedcost = $originalprice = $flag = 0;
                        foreach($subchecks as $subcheck){
                            #レコードの中にセレクトマーク0の値がないかチェック
                            $saleitemCheck = 商品情報のfind抽出;
                            If(($saleitemCheck->選択肢販売マーカー)&&($flag == 0)){
                                
                                #価格情報を固定
                                $salesprice = $saleitemCheck->販売価格;
                                $assumedcost = $saleitemCheck->仕入原価;
                                $originalprice = $saleitemCheck->定価;
                                $shipper_id = optional($saleitemCheck->仕入企業テーブル)->区分名;
                                $flag = 1;//ループ終了マーカー
                            }
                        }
                    }else{
                        #レコードがない
                        $salesprice = $assumedcost = $originalprice = 0;
                        $shipper_id = '9999';
                    }
                    //組合せのみで出品している商品の諸々価格は0で登録される ※ただし上書き時には既存価格が入っているときスキップする
                }
            }else{
                #NULLと0の境を作れないので0とみなして動作する
                If(!empty(optional($saleslist->saleitem)->salesprice)){
                    #売価が入っていれば情報はnonObjectエラー回避できる
                    $salesprice = optional($saleslist->saleitem)->salesprice;
                    $assumedcost =optional($saleslist->saleitem)->assumedcost;
                    $originalprice = optional($saleslist->saleitem)->originalprice;
                    $shipper_id = optional($saleslist->saleitem)->shipper_id;
                }else{
                    #売価 = 0は無視してよい情報と判断
                    $salesprice = $assumedcost = $originalprice = 0;
                    $shipper_id = 0;
                }
            }
            ////////////////////////////////////////
            // Product_idから在庫数取得
            ////////////////////////////////////////
            #productstocksをチェック、在庫数取得
            $p_stocks = MySQL倉庫別在庫管理テーブル
                    ->select('SKUコード',DB::raw('sum(在庫) as total'))
                    ->get();
            
            #加算在庫数取得
            $addstock = 0;//初期化
            $sts = 在庫タイプ::where(有効マーカ,1)->get();
            $additionals = array();
            foreach($sts as $st){
                $additionals[$st->id] = $st->加算在庫数;
            }
            #在庫加算数を確定
            If(!Empty($additionals[optional($saleslist->商品SKUテーブル)->在庫タイプ_id])){
                $addstock = $additionals[optional($saleslist->商品SKUテーブル)->在庫タイプ_id];
            }else{
                $addstock = 0;
            }
            
            #登録在庫数(最終)
            If(!empty($p_stocks)){
                #在庫数がある時、在庫数+加算在庫数を計上
                $stocks = ($p_stocks[0]->total) + $addstock;
            }else{
                #在庫数登録がない時、加算在庫数を計上
                $stocks = $addstock;
            }
            ////////////////////////////////////////
            // データベースに追加/上書き
            ////////////////////////////////////////
            #Producttemporaryから一致を探す
            $value = Producttemporary::where('product_id',$saleslist->product_id)->first();
            If(!empty($value)){
                #$valueが存在する時、在庫数を計算し格納
                If(($value->stock <> $stocks)or($value->originalprice <> $originalprice)or($value->assunedcost <> $assumedcost)or($value->salesprice <> $salesprice)){
                    If(($value->originalprice <> $originalprice)&&($originalprice <> 0)){$value->originalprice = (INT)$originalprice;}
                    If(($value->assunedcost <> $assumedcost)&&($assumedcost <> 0)){$value->assunedcost = (INT)$assumedcost;}
                    If(($value->salesprice <> $salesprice)&&($salesprice <> 0)){$value->salesprice = (INT)$salesprice;}
                    $value->shipper_id = $shipper_id;
                    $value->mathstocks = $stocks - ($value->stock);//更新用差分計算結果
                    $value->onoff = 1;
                    $value->save();
                }else{
                    #増減ない時、ゼロで更新 ※売価更新などでのエラー回避目的
                    $value->mathstocks = 0;//更新用差分計算結果
                    $value->onoff = 0;
                    $value->save();
                }
            }else{
                #テンポラリテーブルに存在しない時、新規追加
                $item = new Producttemporary;
                $item->product_id = $saleslist->product_id;
                $item->makercode = optional($saleslist->product)->makercode;
                $item->jan = optional($saleslist->product)->jan;
                $item->productname = optional($saleslist->product)->productname;
                $item->stock = NULL;
                $item->originalprice = $originalprice;
                $item->assunedcost = $assumedcost;
                $item->salesprice = $salesprice;
                $item->shipper_id = $shipper_id;
                $item->mathstocks = $stocks;//更新用差分計算結果
                $item->onoff = 1;
                $item->save();
            }
        }
    }
    
    /////////////// NextEngineアップロード //////////////////
    public static function ItemStockUplode_NextEngine($access_token,$refresh_toke,$encode_access,$encode_refresh)
    {
        ////////////////////////////////////////
        // 在庫数更新:商品マスタアップロード
        // ENDPOINT:https://api.next-engine.org/api_v1_master_goods/upload
        ////////////////////////////////////////
        #access_token取得
        $Endpoint = 'https://api.next-engine.org/api_v1_master_goods/upload/';
        
        ////////CSVデータ造作////////
        #$csv_header = ['syohin_code','sire_code','jan_code','kataban','syohin_name','genka_tnk','hyoji_tnk','baika_tnk','zaiko_su'];
        $csv_list = 'syohin_code,sire_code,jan_code,kataban,syohin_name,genka_tnk,hyoji_tnk,baika_tnk,zaiko_su';

        #商品名 = NULLのレコードはエラー回避のため除外
        $values = Producttemporary::where('onoff',1)
                ->whereNotNull('productname')
                ->get();
        
        $i = 0;
        foreach($values as $val){
            If(!empty($val->shipper_id)){
                #$shipper = optional($val->shipper)->pictfolder;
                $shipper = 9999;//テスト店舗用
            }else{
                $shipper = 9999;
            }
            $str = $val->product_id.','.$shipper.','.$val->jan.','.$val->makercode.',"'.$val->productname.'",'.$val->assunedcost.','.$val->originalprice.','.$val->salesprice.','.$val->mathstocks;
            $csv_list = $csv_list."\n".$str;
        }

        $header = array("Content-Type: application/x-www-form-urlencoded;charset=UTF-8");
        $params = 'access_token='.$access_token.'&refresh_token='.$refresh_toke.'&wait_flag=1&data_type=csv&data='.$csv_list;

        $response = self::postSSLAPI_Curl($Endpoint,$header,$params);

        $pattern = '/\{.+?\}/';
        preg_match_all($pattern, $response, $match);
        $list = (array)json_decode($match[0][0]);
        #sitelogincodesにend_Dateを登録
        //$access_token
        self::saveTokenEndDate_NextEngine($list['access_token'],$list['access_token_end_date'],$encode_access);
        //$refresh_toke
        self::saveTokenEndDate_NextEngine($list['refresh_token'],$list['refresh_token_end_date'],$encode_refresh);

        #テンポラリテーブルの全件削除
        #Producttemporary::truncate();
    }
        

    public static function saveTokenEndDate_NextEngine($code,$endDate,$name)
    {
        $necode_token = openssl_encrypt($code, 暗号Method, 暗号化Key);
        $value = MySQLからtokenコードで抽出;
        If(!empty($value)){
            If($value->有効期間 !== $endDate){
                $value->有効期間 = $endDate;
                $value->save();
            }
        }else{
            $value2 = MySQLからtoken名で抽出;
            If(!empty($value2)){
                $value2->token = $necode_token;
                $value2->有効期限 = $endDate;
                $value2->save();
            }
        }
        return;
    }
    
    /////////////// アップロードキューの結果取得 //////////////////
    public static function QueSearch_NextEngine($access_token,$refresh_toke,$encode_access,$encode_refresh)
    {
        #日付取得
        $now = Carbon::now();
        $yesterday = Carbon::yesterday();

        #NE商品マスタを取得
        $Endpoint = 'https://api.next-engine.org/api_v1_system_que/search';
        $header = array("Content-Type: application/x-www-form-urlencoded;charset=UTF-8");
        $fields = 'que_id,que_method_name,que_client_file_name,que_file_name,que_status_id,que_message,que_creation_date';
        $search = 'que_creation_date-gte='.$yesterday.&que_creation_date-lte='.$now;
        #que_creation_date キュー作成日
        $params = 'access_token='.$access_token.'&refresh_token='.$refresh_toke.'&wait_flag=1&offset=1&fields='.$fields.'&'.$search;
            
        $response = self::postSSLAPI_Curl($Endpoint,$header,$params);
        $list = (array)json_decode($response);
        
        return;
    }

    /////////////// 基本的なAPI_POSTリクエスト/////////////// 
    public static function postSSLAPI_Curl($Endpoint,$header,$params)
    {
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $Endpoint);
        curl_setopt($curl, CURLOPT_POST, true);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);//SSL証明書
        curl_setopt($curl, CURLOPT_POSTFIELDS, $params);
        $response = curl_exec($curl);
        
        return $response;
    }
}

お~長い!それも一般的に使うスクリプトと違うし。ほんと個人的なメモですね。
申し訳ない。

大まかな流れは『在庫変更の段取りを考える』でメモした通りです。

ちょっとしたスクリプト解説

393行目~405行目

まず最後のfunctionの部分、これはcurlのPOSTが多いので共通化しておいてあるものです。
このスクリプト内ではselfで呼び出してますが、実際には共通処理をまとめたLibrary に入れてあり『いろんな所』から呼び出して使ってます。

ネクストエンジンAPIではこの中の400行目【SSL証明書】が必須なので気をつけなきゃいけません。

9行目~20行目

selecterCodeという引数でその後のアクションを分けています。
理由はtoken取得までは同じ動きをするから。
スクリプトの例でいえば、引数=1の場合は在庫更新を起動、引数=99の場合はキューの調査を動かします。

23行目~42行目

このfunctionは在庫更新の動作を5パートに分けて動かしています。

  1. ネクストエンジンの商品マスタAPIを動かし、MySQLテンポラリテーブルに格納【30行目】
  2. ネクストエンジンの在庫マスタAPIを動かし、登録したテンポラリテーブルのstockカラムに格納【32行目】
  3. 社内システムから有効な商品(ネクストエンジンに登録する商品)の在庫数をリストで抽出【34行目】
  4. テンポラリテーブルのstockカラムと照らし合わせ更新用在庫数を登録、テンポラリにない場合は情報追加【37行目】
  5. テンポラリテーブルを参照し、変更のあったレコードをアップロード【39行目】

それぞれのコードのJUMP先は下のようになっています。

  • 44行目~108行目:30行目のコードの着地点。
  • 110行目~163行目:32行目のコードの着地点。
  • 165行目~299行目:34行目のコードの着地点。
  • 302行目~348行目:37行目のコードの着地点。
  • 351行目~369行目:39行目のコードの着地点。

371行目~390行目

selecterCode(引数)=99で読み込みを行うアップロードキューの実行結果取得API。
期間指定を毎回行うのも面倒なので、昨日~本日の値を取ってくるように指示しています。

まとめ

これで在庫数更新自体は機能したのですが、問題も発生。

250レコード以上の登録ができない...。

ん~、なんでだろう。

ネクストエンジンさんにお聞きしたところ、一括登録の制限と言う事ではなさそう。
となると、作成したCSVレコードのどこかが悪いんだろうけど...現状原因不明です。

理由が判明したら別途記事書いて記録しておこう。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください