GASで楽天ペイの支払いをZaimに自動記帳する設定手順

GASで楽天ペイの支払いをZaimに自動記帳する設定手順

ポイ活の観点から楽天ペイを日常の決済手段として使っているのですが、Zaimとの自動連携に対応していないため、これまで手動で家計簿に入力していました。調べていたところ、Zennに参考になる記事を見つけ、Google Apps Script(GAS)を使って自動記帳を実現することができました。原作者の方に感謝しつつ、設定手順を整理して記録しておきます。

前提条件

  • Googleアカウントを持っていること(GASの実行環境として使用)
  • 楽天ペイの決済通知メールがGmailに届く設定になっていること
  • Zaim上に楽天ペイのアカウントを手動で作成済みであること
楽天ペイの通知メール例

設定手順

1Google Apps Scriptの初期設定

Google Apps Script(GAS)にアクセスして新しいプロジェクトを作成します。プロジェクト名は任意で構いませんが、私は rakutenpay2zaim としました。

プロジェクト作成後、エディタ画面の右側のコード入力エリアに、以下のコードをコピーして貼り付けてください。

GASのコードエディタ画面
const CONSUMER_KEY = PropertiesService.getScriptProperties().getProperty("ZAIM_CONSUMER_ID");
const CONSUMER_SECRET = PropertiesService.getScriptProperties().getProperty("ZAIM_CONSUMER_SECRET")
const RAKUTEN_PAYMENT_ID = parseFloat(PropertiesService.getScriptProperties().getProperty("RAKUTEN_PAYMENT_ID"));
const DEFAULT_CATEGORY_ID = parseFloat(PropertiesService.getScriptProperties().getProperty("DEFAULT_CATEGORY_ID"));
const DEFAULT_GENRE_ID = parseFloat(PropertiesService.getScriptProperties().getProperty("DEFAULT_GENRE_ID"));

// 今日の日付を取得
var date = new Date();
var today = Utilities.formatDate(date, Session.getScriptTimeZone(), "yyyy-MM-dd");

// 1日前の日付を取得
var yesterday = new Date(date.getFullYear(), date.getMonth(), date.getDate() - 1);
yesterday = Utilities.formatDate(yesterday, Session.getScriptTimeZone(), "yyyy-MM-dd");

// 認証のリセット
function reset() {
  var service = getService();
  service.reset();
}

// 認証サービスの設定
function getService() {
  return OAuth1.createService("Zaim")
    .setAccessTokenUrl("https://api.zaim.net/v2/auth/access")
    .setRequestTokenUrl("https://api.zaim.net/v2/auth/request")
    .setAuthorizationUrl("https://auth.zaim.net/users/auth")
    .setConsumerKey(CONSUMER_KEY)
    .setConsumerSecret(CONSUMER_SECRET)
    .setCallbackFunction("authCallback")
    .setPropertyStore(PropertiesService.getUserProperties());
}

// OAuth Callbackの設定
function authCallback(request) {
  var service = getService();
  var authorized = service.handleCallback(request);
  if (authorized) {
    return HtmlService.createHtmlOutput("認証できました!このページを閉じて再びスクリプトを実行してください。");
  } else {
    return HtmlService.createHtmlOutput("認証に失敗");
  }
}

// GETパラメーターを作成
function encodeParams(params) {
  var encodedParams = [];
  for (var key in params) {
    encodedParams.push(encodeURIComponent(key) + "=" + encodeURIComponent(params[key]));
  }
  return encodedParams.join("&");
}

// 過去の支払いデータを取得
function getPastData(service) {
  var url = "https://api.zaim.net/v2/home/money";
  var params = {
    "start_date": yesterday,
    "end_date": today,
  }
  var response = service.fetch(url + "?" + encodeParams(params));
  var result = JSON.parse(response.getContentText());
  var rakutenPayData = result.money.filter(function(item) {
    return item.from_account_id === RAKUTEN_PAYMENT_ID;
  });
  return rakutenPayData
}

// 楽天ペイ情報をZaimに登録(メイン関数)
function rakutenPayToZaim() {
  var start = 0;
  var max = 2; // 過去いくつまでメールを遡るか
  var query = 'subject: ("楽天ペイアプリご利用内容確認メール" OR "楽天ペイ 注文受付") ';
  var threads = GmailApp.search(query, start, max);

  var service = getService();
  if (service.hasAccess()) {
    var existingData = getPastData(service)
  } else {
    var authorizationUrl = service.authorize();
    Logger.log("次のURLを開いてZaimで認証したあと、再度スクリプトを実行してください。: %s", authorizationUrl);
  }

  for (var i = 0; i < threads.length; i++) {
    var messages = threads[i].getMessages();
    for (var j = 0; j < messages.length; j++) {
      var message = messages[j];
      var subject = message.getSubject();
      var body = message.getPlainBody();

      if (subject == "楽天ペイアプリご利用内容確認メール") {
        var usageDate = body.match(/ご利用日時\s*([\s\S]*?)伝票/)[1].trim();
        var cash = body.match(/楽天キャッシュ\s*([\s\S]*?)\s*円/)[1].trim().replace(/,/g, "");
        var card = body.match(/カード\s*([\s\S]*?)\s*円/)[1].trim().replace(/,/g, "");
        var shop = body.match(/ご利用店舗\s*([\s\S]*?)電話/)[1].trim();
        cash = parseInt(cash, 10)
        card = parseInt(card, 10)
        cash += card
        usageDate = usageDate.replace(/^(\d{4})\/(\d{2})\/(\d{2}).*/, "$1-$2-$3")
      } else if (subject == "楽天ペイ 注文受付(自動配信メール)") {
        var usageDate = body.match(/\d{4}-\d{2}-\d{2}/g)[0]
        var cash = body.match(/[\d,]+円/g).at(-1).replace(/,/g, "").replace(/円/g, "");
        var shop = body.match(/提携サイト「(.*?)>(.*?)>(.*?)<(.*?)」/)[3];
      } else {
        continue;
      }
      var isExisting = false;
      Logger.log("日付:" + usageDate + ", 支払金額:" + cash + ", お店:" + shop)

      if (cash == 0) {
        Logger.log("ポイント払いのためスキップ");
        continue;
      }

      if (usageDate != yesterday & usageDate != today) continue;

      for (var k = 0; k < existingData.length; k++) {
        if (existingData[k]["date"] == usageDate && existingData[k]["amount"] == cash && existingData[k]["place"] == shop) {
          isExisting = true;
          Logger.log("既に入力済み")
          break;
        }
      }

      if (!isExisting) {
        var files = DriveApp.getFilesByName("ZAIM_DB");
        var originalData = []
        if (files.hasNext()) {
          spreadsheet = SpreadsheetApp.open(files.next());
          var originalSheet = spreadsheet.getSheetByName("登録カテゴリー");
          originalData = originalSheet.getRange(2, 1, originalSheet.getLastRow() - 1, 4).getValues();
        }

        var category_id = DEFAULT_CATEGORY_ID;
        var genre_id = DEFAULT_GENRE_ID;
        for (var k = 0; k < originalData.length; k++) {
          var [storeName, exactMatch, categoryId, genreId] = originalData[k];
          if ((exactMatch && storeName === shop) || (!exactMatch && shop.includes(storeName))) {
            category_id = categoryId;
            genre_id = genreId;
            break;
          }
        }

        var url = "https://api.zaim.net/v2/home/money/payment";
        var payload = {
          "category_id": category_id,
          "genre_id": genre_id,
          "amount": cash,
          "date": usageDate,
          "place": shop,
          "from_account_id": RAKUTEN_PAYMENT_ID,
          "comment": "システムから登録"
        };
        var options = { "method": "post", "payload": payload };
        service.fetch(url, options);
        Logger.log("支払い入力完了")
      }
    }
  }
}

// Spreadsheetに書き込み
function writeCategoriesToSpreadsheet(categories, genres, accounts) {
  const SPREADSHEET_NAME = "ZAIM_DB";
  const CATEGORY_SHEET_NAME = "カテゴリと内訳";
  const ACCOUNT_SHEET_NAME = "支払方法";
  const ORIGINAL_SHEET_NAME = "登録カテゴリー";

  let spreadsheet;
  var files = DriveApp.getFilesByName(SPREADSHEET_NAME);
  if (files.hasNext()) {
    spreadsheet = SpreadsheetApp.open(files.next());
  } else {
    spreadsheet = SpreadsheetApp.create(SPREADSHEET_NAME);
  }

  let categorySheet = spreadsheet.getSheetByName(CATEGORY_SHEET_NAME);
  if (categorySheet) { categorySheet.clear(); } else { categorySheet = spreadsheet.insertSheet(CATEGORY_SHEET_NAME); }

  let accountSheet = spreadsheet.getSheetByName(ACCOUNT_SHEET_NAME);
  if (accountSheet) { accountSheet.clear(); } else { accountSheet = spreadsheet.insertSheet(ACCOUNT_SHEET_NAME); }

  let originalSheet = spreadsheet.getSheetByName(ORIGINAL_SHEET_NAME);
  if (!originalSheet) {
    originalSheet = spreadsheet.insertSheet(ORIGINAL_SHEET_NAME);
    var originalHeaders = ["店舗名", "完全一致", "カテゴリーID", "内訳ID"];
    originalSheet.getRange(1, 1, 1, originalHeaders.length).setValues([originalHeaders]);
  }

  let defaultSheet = spreadsheet.getSheetByName("シート1");
  if (defaultSheet) { spreadsheet.deleteSheet(defaultSheet); }

  var categoryHeaders = ["カテゴリーID", "カテゴリー名", "内訳ID", "内訳名"];
  categorySheet.appendRow(categoryHeaders);
  var accountHeaders = ["支払ID", "支払方法"];
  accountSheet.appendRow(accountHeaders);

  var categoryRows = [];
  categories.forEach(category => {
    var categoryGenres = genres.filter(genre => genre.category_id === category.id);
    if (categoryGenres.length > 0) {
      categoryGenres.forEach(genre => {
        categoryRows.push([category.id, category.name, genre.id, genre.name]);
      });
    } else {
      categoryRows.push([category.id, category.name, "", ""]);
    }
  });
  var accountRows = accounts.map(account => [account.id, account.name]);

  categorySheet.getRange(2, 1, categoryRows.length, categoryHeaders.length).setValues(categoryRows);
  accountSheet.getRange(2, 1, accountRows.length, accountHeaders.length).setValues(accountRows);
  Logger.log("カテゴリ情報、内訳、支払い方法一覧を取得しました: " + spreadsheet.getUrl());
}

// カテゴリと内訳の取得(スプレッドシート作成のメイン関数)
function getInfo() {
  var service = getService();
  if (!service.hasAccess()) {
    var authorizationUrl = service.authorize();
    Logger.log("次のURLを開いてZaimで認証したあと、再度スクリプトを実行してください。: %s", authorizationUrl);
  }

  var categoryUrl = "https://api.zaim.net/v2/home/category";
  var response = service.fetch(categoryUrl)
  var category = JSON.parse(response.getContentText()).categories;
  var filterCategory = category.filter(item => item.mode === "payment" && item.active === 1.0).sort((a, b) => a.sort - b.sort);

  var genreUrl = "https://api.zaim.net/v2/home/genre";
  response = service.fetch(genreUrl)
  var genre = JSON.parse(response.getContentText()).genres;
  var filterGenre = genre.filter(item => item.active === 1.0).sort((a, b) => a.sort - b.sort);

  var accountUrl = "https://api.zaim.net/v2/home/account";
  var response = service.fetch(accountUrl);
  var account = JSON.parse(response.getContentText()).accounts;
  account = account.filter(item => item.active === 1.0).sort((a, b) => a.sort - b.sort);

  writeCategoriesToSpreadsheet(filterCategory, filterGenre, account);
}

コードにはコメントが丁寧に記載されており、全体の流れは読み取れるようになっています。

2Zaim APIの取得

Zaim Developers にZaimアカウントでログインし、新しいアプリケーションを登録します。入力内容は以下の通りです。

  • 名称(Name): 任意
  • サービス種別(Service Type): 「ブラウザ」を選択
  • 概要(Description): 任意
  • 組織(Organization): 自分のIDなどを入力
  • サービスURL(Service URL): https://script.google.com/
  • チェックボックス: 3つすべてにチェックを入れる
ZaimAPI申請の入力画面

登録が完了すると、Consumer KeyとConsumer Secretが発行されます。次のステップで使用するので控えておいてください。

KeyとSecretの取得画面

3スクリプトのプロパティを設定

GASの「歯車アイコン(設定)」メニューから「スクリプトのプロパティ」を開き、以下の2行を追加します。

ZAIM_CONSUMER_ID: [Consumer Key]
ZAIM_CONSUMER_SECRET: [Consumer Secret]
Script Propertyへの入力画面

4OAuth1ライブラリの追加

ライブラリの右にある「+」ボタンをクリックし、以下のスクリプトIDを入力してOAuth1を追加してください。

1CXDCY5sqT9ph64fFwSzVtXnbjpSfWdRymafDrtIZ7Z_hwysTY7IIhi7s

5データベースの生成

実行する関数を「デバッグ」の横のプルダウンで「getInfo」に切り替え、「実行」をクリックします。

getInfo実行画面

初回実行時はアカウントの認証が求められます。認証完了後の最初の実行は失敗することがありますが、もう一度「実行」すれば正常に動作します。成功するとGoogleドライブ内に「ZAIM_DB」というスプレッドシートが作成されます。

ZAIM_DB生成後の画面

6楽天ペイのアカウントIDを設定

「ZAIM_DB」の「支払方法」シートを開き、楽天ペイに対応する8桁のIDを確認します。スクリプトのプロパティに以下を追加してください。

Zaimに楽天ペイのアカウントを手動で作成していない場合、IDは存在しません。先に作成を済ませておく必要があります。
RAKUTEN_PAYMENT_ID: [8桁のID]

7デフォルトのカテゴリーと内訳を設定

「ZAIM_DB」の「カテゴリと内訳」シートから、デフォルトとして設定したいカテゴリーIDと内訳IDを確認し、スクリプトのプロパティに追加します。

DEFAULT_CATEGORY_ID: [カテゴリーID]
DEFAULT_GENRE_ID: [内訳ID]

8よく使う店舗のカテゴリーを登録

「ZAIM_DB」の「登録カテゴリー」シートに、よく利用する店舗とその分類を事前に設定しておくことができます。スクリプトはこのシートを参照して自動的に適切なカテゴリーを割り当てます。私は試しに「吉野家」を登録してみました。

登録カテゴリーシートの設定例

9動作テスト

実行する関数を「rakutenPayToZaim」に変更して「実行」をクリックします。下図のようなログが出力されれば正常に動作しています。

テスト実行画面稼働ログの出力例

10定時トリガーの設定

左側メニューの「トリガー」をクリックして設定画面を開き、実行間隔を指定します。毎日・毎時間などから好みに合わせて選べます。

トリガーメニューの場所トリガー設定画面

実現できる機能のまとめ

機能内容
自動実行設定した間隔で楽天ペイの通知メールを参照して処理
対応メール実店舗決済メール・オンライン決済メールの2種類に対応
重複チェック過去2日間のZaim登録データと照合し、重複は追加しない
決済日の正確性メールから取得した実際の決済日で登録
出金元Zaim内で手動設定した楽天ペイアカウント(IDで照合)
店舗名実店舗は店舗名、オンラインは利用サイト名を記録
カテゴリー登録カテゴリーシートで店舗ごとに設定可能、未設定はデフォルト値を適用
ポイント払いの扱い支払金額が0円の場合は自動的にスキップ
備考欄「システムから登録」と記載(コード内で変更可能)

設定が完了すれば、楽天ペイで決済するたびに次のトリガー実行タイミングで自動的にZaimへ記帳されます。これ以降は基本的に何もしなくて済むので、手動入力の手間が完全になくなりました。今後は店舗ごとのカテゴリー自動判定をさらに充実させるなど、応用の余地もあります。その辺りは別の記事で書いていく予定です。

シェア・購読:
🧡 この記事が気に入ったら、RSSフィードを購読して最新の更新を受け取りましょう。