give IT a try

プログラミング、リモートワーク、田舎暮らし、音楽、etc.

Everyday Railsチャリティセールの経過報告と集計用Railsアプリの紹介

はじめに

これまでこのブログで何度かお知らせしていますが、熊本地震の災害支援のために2016年4月25日から2016年5月31日まで「Everyday Rails - RSpecによるRailsテスト入門」のチャリティセールを実施しています。

今回のエントリでは、昨日(2016年5月15日)時点での収益(=募金額)と、収益を集計するRailsアプリの紹介をします。

開始から3週間で899.55ドル(約9万7000円)に到達!

ありがたいことに、昨日の時点で募金額はなんと899.55ドル(約9万7000円)になりました。

f:id:JunichiIto:20160516043825p:plain

上のグラフを見てもらえれば分かるとおり、日によって多少の違いはあるものの、今のところ毎日売上げが発生しています。
購入してくださったみなさん、どうもありがとうございました。

この調子でいくと当初の目標額だった500ドルの2倍、1000ドル(約10万8000円)も夢じゃないかもしれません。
みなさん、引き続きご協力よろしくお願いします!

Leanpub用・収益集計Railsアプリの紹介

ところで、上のグラフは僕が作ったRailsアプリケーションで表示したものです。
「お金の話だけじゃなくて技術的なネタも読みたい!」と思っているエンジニアさん向けに、このRailsアプリの仕組みを説明します。

処理の流れ

このRailsアプリはこんな感じで処理しています。

  1. LeapubのAPIを叩いて売上データを取得し、データベースに保存する
  2. Web画面を開くとデータベース内のデータを集計し、グラフに表示する

まあ特に変わったところはない、非常にオーソドックスな流れですね。

売上データをAPIからデータベースに保存する処理

LeanpubのAPIからは次のようなJSONデータが返ってきます。
author_royaltiesの1.5(ドル)が収益です。

{
   "cause_royalty_percentage":"0.0",
   "author_royalty_percentage":"5.0",
   "author_royalties":"1.5",
   "cause_royalties":"0.0",
   "author_paid_out_at":null,
   "cause_paid_out_at":null,
   "created_at":"2016-04-21T15:58:45.000Z",
   "royalty_days_hold":45,
   "publisher_royalties":"0.0",
   "publisher_paid_out_at":null,
   "author_username":"foo_bar",
   "publisher_slug":"",
   "user_email":"",
   "purchase_uuid":"HlKg4GLtj-fVwM4UqcRj2",
   "invoice_id":"DimXToR5rhc2z_g8A34LM",
   "date_purchased":"2016-04-21T15:58:45.000Z"
}

上のJSONは1レコード分なので、実際は複数レコードが配列になって返ってきます。

あとはRails側でAPIを呼び出して、配列をぐるぐるループさせながらJSONデータ(Ruby内ではハッシュとして扱う)を保存していけば終わりです。
実際のコードとは若干異なりますが、イメージ的には以下のようなコードになります。

slug = 'everydayrailsrspec-jp'
api_key = '(api key)'
url = "https://leanpub.com/#{slug}/individual_purchases.json?api_key=#{api_key}"
# APIを叩いて売上データを取得
hash_array = JSON.load(open(url))

hash_array.each do |hash|
  # 1件ずつ売上データをデータベースに保存
  purchase_uuid, author_username = hash.values_at('purchase_uuid', 'author_username')
  sale = Sale.find_or_initialize_by(
    purchase_uuid: purchase_uuid, author_username: author_username)
  hash.each do |key, value|
    sale.send("#{key}=", value)
  end
  sale.save!
end
データを保存する処理のテストコード

JSONをパースしてデータベースに保存するところはRSpecでテストも書いています。
(Everyday RailsはそもそもRSpecの本ですからね!)

RSpec.describe Sale, type: :model do
  describe '::save_records_from_hash_array!' do
    let(:json_text) do
      <<-'JSON'
[
  {
     "cause_royalty_percentage":"0.0",
     "author_royalty_percentage":"5.0",
     "author_royalties":"1.5",
     "cause_royalties":"0.0",
     "author_paid_out_at":null,
     "cause_paid_out_at":null,
     "created_at":"2016-04-21T15:58:45.000Z",
     "royalty_days_hold":45,
     "publisher_royalties":"0.0",
     "publisher_paid_out_at":null,
     "author_username":"foo_bar",
     "publisher_slug":"",
     "user_email":"",
     "purchase_uuid":"HlKg4GLtj-fVwM4UqcRj2",
     "invoice_id":"DimXToR5rhc2z_g8A34LM",
     "date_purchased":"2016-04-21T15:58:45.000Z"
  }
]
      JSON
    end

    it 'creates record' do
      # メソッドを呼び出すとレコードが増えることを確認
      expect {
        Sale.save_records_from_hash_array!(JSON.parse(json_text))
      }.to change(Sale, :count).from(0).to(1)

      # JSONの各値がちゃんと保存されていることを確認
      sale = Sale.first
      expect(sale).to have_attributes(
        cause_royalty_percentage: BigDecimal('0.0'),
        author_royalty_percentage: BigDecimal('5.0'),
        author_royalties: BigDecimal('1.5'),
        cause_royalties: BigDecimal('0.0'),
        author_paid_out_at: nil,
        cause_paid_out_at: nil,
        royalty_days_hold: 45,
        publisher_royalties: BigDecimal('0.0'),
        publisher_paid_out_at: nil,
        author_username: 'foo_bar',
        publisher_slug: '',
        user_email: '',
        purchase_uuid: 'HlKg4GLtj-fVwM4UqcRj2',
        invoice_id: 'DimXToR5rhc2z_g8A34LM',
        date_purchased: '2016-04-21T15:58:45.000Z'.to_time
      )
    end
  end
end
データを集計する処理

データの集計はがっつりSQLを書いて集計しています。

class Sale < ActiveRecord::Base
  def self.sum_by_date
    sql = <<-SQL
WITH jst_sales AS (
    SELECT
      *,
      date_purchased + INTERVAL '9 hours' AS jst_date_purchased
    FROM sales
),
    sum_by_date AS (
      SELECT
        to_char(jst_date_purchased, 'YYYY/MM/DD') AS purchased_on,
        count(DISTINCT purchase_uuid)             AS purchase_count,
        sum(author_royalties)                     AS royalties
      FROM jst_sales
      GROUP BY purchased_on
  )
SELECT
  *,
  SUM(royalties)
  OVER (
    ORDER BY purchased_on) AS cum_royalties
FROM sum_by_date
ORDER BY purchased_on DESC
    SQL

    find_by_sql(sql)
  end
end

"WITH jst_sales AS ( )"の部分はCTE(共通テーブル式)というPostgreSQLの機能を使っています。
CTEはSQL内だけで有効なビュー(Railsではなく、データベースのVIEW)を作り出す機能と考えてもらえばOKです。
まずここで購入日時を日本時間に変換しています。

"sum_by_date AS ( )"の部分もCTEです。
ここは普通に日付単位で収益を集計しています。

一番最後に出てくるSELECT文が最終的に返却するデータのSELECT文になります。
ポイントは "SUM( ) OVER ( ORDER BY )" の部分です。
ここはPostgreSQLのWindow関数という機能を使って、収益の累積値を計算しています。

このSQLを実行すると以下のような結果が返ってきます。

日付 購入数 収益 累計額
2016/05/15 5 $55.31 $899.55
2016/05/14 1 $7.60 $844.24
... ... ... ...
2016/04/28 3 $22.80 $427.29
2016/04/27 16 $151.94 $404.49
2016/04/26 17 $167.93 $252.55
2016/04/25 9 $84.62 $84.62
データをグラフとして表示する処理

グラフの表示は Flot というjQuery用ライブラリを使っています。
RailsからJavaScriptには直接データを渡せないので、divタグのdata属性にFlot用に加工したデータを埋め込みます。

<div data-cum-royalties="[
  [1463238000000, 899.55], 
  [1463151600000, 844.24], 
  ... , 
  [1461510000000, 84.62]
]" ... id="placeholder"></div>

"1463238000000"は日付を表すタイムスタンプで、"899.55"が値(ここでは収益の累積値)です。

最後にJavaScript側でデータを読み取って Flot に渡せばおしまいです。

$(function () {
  // 売上冊数
  var purchaseCount = $('#placeholder').data().purchaseCount;

  // 収益の累積値
  var cumRoyalties = $('#placeholder').data().cumRoyalties;

  // 目標値
  var goalData = $('#placeholder').data().goalData;

  var data = [{
    data: purchaseCount
  }, {
    data: cumRoyalties
  }, {
    data: goalData
  }];

  var options = {
    legend: {position: "ne"},
    colors: ["#eb941f", "#55a868", "#c60c30"],
    grid: {
      hoverable: true
    }
  };

  // グラフを表示
  $.plot("#placeholder", data, options);
});
このRailsアプリのソースコード

このRailsアプリのソースコードはGitHubに置いています。
上で説明したコードはブログ用に簡略化してる部分も多いので、処理の詳細を知りたい方はGitHubのコードを参照してください。
(自分用に作ったRailsアプリなので、公開するには恥ずかしい部分もいろいろあります。悪しからず・・・)

まとめ

というわけで、このエントリではEveryday Railsチャリティセールの経過報告と集計用Railsアプリの紹介をしました。
Everyday Railsのチャリティセールは5月31日まで続きます。
引き続きみなさんのご協力をお願いします!

Everyday Rails - RSpecによるRailsテスト入門
f:id:JunichiIto:20160424081240p:plain

チャリティセールの詳細や購入方法についてはこちらのエントリをご覧ください。