GAS+スプレッドシート+LINE Messaging APIで1日の使用総額を通知し、無駄遣いを防ぐ

はじめに

はじめまして、こんにちは。コンテンツ開発事業部の江田と申します。

スマートフォン向けのソーシャルゲームのサーバサイドの開発・運用をしています。

少し前に、個人用に携帯端末でPayPayやSuicaなどを使った決済時にほぼリアルタイムで、1日の支払総額を通知するシステムを作ったので紹介したいと思います。

作成の動機

  • 1日に支払った金額を大まかでいいので、知りたかった。

  • 電子マネーで決済時、支払金額はわかっても、1日の支払総額はわからなかった。

  • 家計簿アプリを使って、家計簿のチェックをおこなっているが、1日の支払総額はリアルタイムで知ることは無理そうだった。(自分が知らないだけかも)

そこで、支払総額をLINEに通知するシステムを作りました。

開発

ざっとした要件

  • スマホで電子決済するとできるだけすぐにLINEに通知する

  • 通知内容は当日の支払総額と、1ヶ月分の支払総額

  • 1日の支払総額は必ずしも正確でなくても良い(無駄遣いを抑制するのが目的なので)

f:id:tctckd:20200609161426p:plain

問題点

どうやって、支払総額を取得するか?

考えられる方法

  • サービスが提供しているApi

  • スクレイピング

解決策

結局利用しているクレジットカードが展開している、決済時にメールを通知するサービスを使うことにしました。

LINEに通知が来るタイミングはクレジットカードへ照会が行われた時間です。

PayPayで決済時に、クレジットカード決済を経由することで、支払時にLINEに通知を飛ばすことができます。

Suicaは、1度に数千円クレジットカードでチャージする運用をしているので、チャージのタイミングでLINEに通知がきます。

Suicaを使った決済を行ったときには通知はきません。

以下のようなフォーマットで支払い通知メールが送られてきます。 (通知タイミングはお店側が、クレジット会社に問い合わせた時間です。)

XXXX 様

いつもXXXXXカードをご利用頂きありがとうございます。
お客様のカードご利用内容をお知らせいたします。

ご利用カード:XXXXXXX

◇利用日:2020/05/14 11:57
◇利用取引:買物
◇利用金額:1,000,000円

というわけで、次のような構成で実装しました。

f:id:tctckd:20200609161431p:plain

実装はGAS(Google Apps Script)で、トリガーを使って1分ごとに処理が走るようにします。

処理の流れ

  1. 支払通知メールを取得(取得範囲は、処理実行日時の0時0分から現在まで)

  2. 取得したメールを解析、処理当日の支払時刻と支払総額を取得

  3. スプレッドシートから、当日の支払総額と前日までの1ヶ月の支払総額を取得

  4. 処理当日の支払総額とメールから取得、計算した当日の支払総額と比較

  5. メールから取得、計算した支払総額のほうが大きければLINEに通知、スプレッドシートに支払総額を更新する

GASを使った理由は

  • 今まで使用したことがなかったので練習がてらに使おうと思った

  • メールは Gmailを使っていたので、相性が良さそうと思った

といった理由です。

事前準備

クレジットカード決済サービスの設定

決済時にメールを送信できるように設定、メールのフォーマットを把握しておきます。

Googleスプレッドシート

  • 月ごとにシートを準備(例:2020年5月のシート名は 202005)

  • 処理を単純にするために行番号を日付に対応させる(例:31日の支払総額は、31行目に記録する。)

  • 処理を単純にするためにROW番号を日付に対応させる

  • シートIDを記録しておく。

f:id:tctckd:20200527072247p:plain
スプレッドシート

LINE Message Apiを使用可能にする

Messaging APIリファレンス | LINE Developers

上記ページを参考にして、チャンネルアクセストークンを発行、記録しておきます。

また、自分の携帯端末にLINEをインストールしておきます。

Gmailを GASで扱えるようにしておく

特に、設定はありません。

実装

ここまで準備ができたらいよいよ実装していきます。

とりあえず動かすことを目標にしたので、ソースコードが汚いです。

LINEにメッセージを通知

支払総額と、一ヶ月の支払総額を特定のフォーマットでLINEに通知。

//LINEに送信するメッセージフォーマット
function getLineFormat(row ,totalPrice){
  let _price = row.price;
  let _date  = row.view_date;
  return "\r\n合計金額:"+"\r\n"+_date+"\r\n"+_price+"円\r\合計金額(月額):"+totalPrice+"円"; 
}
//LINEに情報を通知する
function lineMesssenger(line_notify_token) {
  
  //チャンネルアクセストークン
  this.line_notify_token = line_notify_token;
  //LINEに情報を通知する
  this.sendMessage = function(lineMessage){
    
    var payload = {'message' : lineMessage};
    var options ={
      "method"  : "post",
      "payload" : payload,
      "headers" : {"Authorization" : "Bearer "+ this.line_notify_token}  
    };
    UrlFetchApp.fetch("https://notify-api.line.me/api/notify", options);
  }
  
}

Gmailから、支払通知メールを取得

メールを取得し、支払ごとに支払金額のリストを生成させます。

//指定した日付以降のメールを獲得
function fetchContactMail1(search_date) {
  //検索条件指定
  let strTerms = '(from:'+ 'クレジットサービス' + ' after:'+search_date+')';
  //取得
  let myThreads = GmailApp.search(strTerms);
  let myMsgs    = GmailApp.getMessagesForThreads(myThreads);
  let resultList  = [];
  let tmpList;
  var cnt =0;
  for(var i = 0; i < myMsgs.length;i++){
    for(var j = 0 ; j < myMsgs[i].length;j++) {
      let body = myMsgs[i][j].getPlainBody().slice(0,2000);
      tmpList  = parsePaymentMail(body);
      for (var k = 0 ;k < tmpList.length; k++) {
        resultList[cnt] = tmpList[k];
        cnt = cnt +1;
      }
    }
  }
  return resultList;
}


//とりあえず、愚直に改行文字で区切って対象の値を取得
function parsePaymentMail(body) {
  let spBody = body.split(/\n/);
  const PAYMENT_DATE     ='◇利用日'  ;
  const PAYMENT_DATETIME ='◇利用日時'
  const PAYMENT_PRICE    = '◇利用金額';
  
  let paymentObj = initPaymentData();
  let paymentObjectList = Array();
  //フォーマットは想定
  //:で区切って対象文字列の横にある値を取得
  for (i=0; i<spBody.length; i++) {
    
    let keyVal = spBody[i].split(':');
    //支払い日時を取得
    if (PAYMENT_DATE === keyVal[0] || PAYMENT_DATETIME === keyVal[0]) {
      dt = new Date(keyVal[1]);
      paymentObj.key = getKeyDate(dt);
      paymentObj.date = keyVal[1];
      paymentObj.dateOrder = dt.getDate(),
      paymentObj.sheet = getDateSheetName(dt);
    }
    //支払金額を取得
    if (PAYMENT_PRICE===keyVal[0]) {
      
      paymentObj.price =Number( keyVal[1].split(/円/)[0].replace(/,/, '') );
      paymentObjectList.push(paymentObj);
      paymentObj =initPaymentData();
      
    }
  }
  return paymentObjectList;
}

//課金情報を初期化
function initPaymentData() {
  return  {
    sheet:null,
    key:null,
    date:null,
    dateOrder:0,
    price:0
  };
}


// 日時情報を指定のフォーマットへ
function getDate(date, format){
  return Utilities.formatDate(date, 'Asia/Tokyo', format);
}
function getKeyDate(dd) {
  return getDate(dd, 'yyyyMMdd');
}

function getDateSheetName(dd) {
  return getDate(dd, 'yyyyMM');
}

//シート獲得 存在しない場合シート作成
function getSheet(ss ,name) {
  return function(n) {
    let _sheet = ss.getSheetByName(n);
    if (_sheet !== null ) return _sheet;
    return ss.insertSheet(n);
  }(name);
}

メールを取得するソースコードは以下のサイトのコードを参考にしました。

tonari-it.com

main処理

支払金額リストから、1日あたりの支払総額と1ヶ月の支払総額を計算

1日あたりの支払総額が更新されていたら、LINEに通知させます。

//1日毎の支払金額を合計する
function sumTotalPriceByDate( paymentList ){
  
  if(paymentList.length ==0) return {} 
  let res = {}
  for(var i = paymentList.length-1; i >= 0; i--){
    let _key   = paymentList[i].key;
    if (res[_key] === void 0 ) {  
      let _date  = paymentList[i].date;
      let _sheet = paymentList[i].sheet;
      let _dateOrder = paymentList[i].dateOrder;
      res[_key] = {sheet:_sheet,price:0 ,view_date:_date ,dateOrder:_dateOrder};
    }
    let _price = paymentList[i].price;
    res[_key].price += _price; 
  }
  return res; 
}

//月別の支払金額を求める
function sumTotalPriceByMonth(sheet ,dateOrder) {
  if (dateOrder === 0){ return 0;}
  let name="A1:A"+dateOrder;
  //指定範囲のListを取得する
  let sheetRangeList = sheet.getRange(name);
  return function(arr) {
    return arr.reduce(function(prev, current, i, arr) {
      return Number(prev) + Number(current);
    });
  }(sheetRangeList.getValues());
}

//main処理を実行
function main() {
  
  const SHEETID= 'xxxxxxxxxxxxxxxxxxxxxxx';
  let ssApp = SpreadsheetApp.openById(SHEETID);
  const line_notfy_token='vvvvvvvvvvvvvvvvvvvvvv'
  let now = new Date();
  let yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
  let targetDate = getDate(yesterday, 'yyyy/MM/dd');
  let paymentList = fetchContactMail(targetDate);
  let totalPriceBydate = sumTotalPriceByDate(paymentList);
  let lineMessenger = new lineMesssenger(line_notfy_token);
  
  for (date_key in totalPriceBydate) {
    //シート獲得
    let sheetName = totalPriceBydate[date_key].sheet;
    let sheet = getSheet(ssApp, sheetName);
    //dateOrderを獲得
    let dateOrder = totalPriceBydate[date_key].dateOrder;
    let cunnrentPrice = totalPriceBydate[date_key].price;
    let totalPriceByMonth = sumTotalPriceByMonth(sheet ,dateOrder-1) + cunnrentPrice;
    let sheetRange = sheet.getRange('A'+dateOrder);
    let priceToday = function(val ,num=1) { 
    if (val == void 0 || val == 0 || val == null || val=='') return num;
      return val + num;
    }(sheetRange.getValue() , 0 );
    
    //スプレッドシートにある1日あたりの合計金額よりも多ければLINEに通知
 
    if (priceToday < cunnrentPrice) {
      lineMessenger.sendMessage(getLineFormat(totalPriceBydate[date_key] , totalPriceByMonth));
     
      //1日あたりの合計金額を更新
      sheetRange.setValue(totalPriceBydate[date_key].price);
    }  
  }
  
}

最後にリアルタイムで支払総額を通知するためにプロジェクトのトリガーを1分ごとに設定します。

これで、クレジットカード決済時にLINEに通知されます。

自分はPayPayで支払う時はクレジットカードでチャージしています。

店舗によっては、クレジットカードサービスへの照会がおそく、リアルタイムで通知されない場合もあるので、あくまで、大まかな支払総額ということに注意してください。

これでどのくらい無駄遣いしているかを簡単に把握することができるようになりました。

f:id:tctckd:20200527072829j:plain
支払総額更新時のLINE(表示されてる金額は本当のものではありません)

問題点と今後の課題

支払ったのに通知がこない

問題点:カード決済後支払い通知メールも来ているのにLINEに通知がこない。

原因:1日あたりのGASからGmailを呼べる回数を超えていた。

とりあえず、深夜は処理を実行しないなど、呼べる回数を超えないように修正予定。

現在は、処理を5分おきに実行しているのでGmailを呼べているが、リアルタイム性がだいぶ薄れてしまった。

クレジット決済以外にも対応

現在は1社のクレジット決済しか対応できていないので、完璧に1日の支払総額を集計できているわけではありません。

他の決済サービスも対応したいです。

特定金額以上使うと警告を行う。

支払総額が一定の値を超えると、「使いすぎ」を警告するようにしたい。

可能であれば、怒ってるスタンプか画像を投稿するようにしたい。

コロナウイルスの流行のせいで使用機会が激変

コロナウイルスの流行のせいで、毎日買い物をするということがなくなり出番が激変しました。

早く収まって、このシステムを利用できる機会が増えればと思います。

以上です。

tecotec.co.jp