ポイントシステム(2) -- ログインポイントの付与

2013/09/20

前回に引き続き、「ポイントシステム」の実装を続けます。

Reward モデル

私たちの目下の懸案事項は、Customer#points をどう実装するかです。すぐに思いつくのは customers テーブルに整数型の points カラムを追加してその値を増減させるという方法ですが、ポイントの増減履歴が記録されないという問題があります。

そこで、新たに rewards テーブルを作って customers テーブルと関連付け、rewards テーブルの points カラムにポイントの増減額を記録することにします。customer_id でレコードを絞り込んだ上で points カラムの値を合計すれば顧客の保有ポイントを得ることができます。

$ rails g model reward

db/migrate/..._create_rewards.rb を次のように修正して、

class CreateCustomers < ActiveRecord::Migration
  def change
    create_table :rewards do |t|
      t.references :customer
      t.integer :points

      t.timestamps
    end
  end
end

テスト環境のためのデータベースを準備します。

$ rake db:migrate
$ rake db:test:prepare

Customer モデルと Reward モデルを関連付けます。

app/models/customer.rb を次のように修正します(5行目挿入)。

require 'nkf'
require 'bcrypt'

class Customer < ActiveRecord::Base
  has_many :rewards

  attr_accessor :password

  # (省略)
end

app/models/reward.rb を次のように修正します(2行目挿入)。

class Reward < ActiveRecord::Base
  belongs_to :customer
end

とりあえず Reward モデルのテストは書かないことにします。削除しておきましょう。

$ rm spec/models/reward_spec.rb

Customer#points メソッドのテストと実装

読者の中にはこの程度ならテストなど書かなくても実装できる方もいらっしゃると思いますが、本連載は RSpec がテーマなので、めんどくさがらずにテストを書いてから実装しましょう。

spec/models/customer_spec.rb に次のエグザンプルを挿入します(password= メソッドの次に)。

describe Customer, '#points' do
  let(:customer) { create(:customer, username: 'taro') }

  specify '関連付けられたRewardのpointsを合計して返す' do
    customer.rewards.create(points: 1)
    customer.rewards.create(points: 5)
    customer.rewards.create(points: -2)

    expect(customer.points).to eq(4)
  end
end

テストの実行結果:

Pending:
  Customer.authenticate ログインに成功すると、ユーザーの保有ポイントが1増える
    # Customer#pointsが未実装
    # ./spec/models/customer_spec.rb:120

Failures:

  1) Customer#points 関連付けられたRewardのpointsを合計して返す
     Failure/Error: expect(customer.points).to eq(4)
     NoMethodError:
       undefined method `points' for #<Customer:0xb990b0c8>
     # ./spec/models/customer_spec.rb:92:in `block (2 levels) in <top (required)>'

app/models/customer.rb を次のように修正します(11-13行挿入)。

require 'nkf'
require 'bcrypt'

class Customer < ActiveRecord::Base
  has_many :rewards

  attr_accessor :password

  # (省略)

  def points
    rewards.sum(:points)
  end

  class << self
    def authenticate(username, password)
      customer = find_by_username(username)
      if customer.try(:password_digest) && BCrypt::Password.new(customer.password_digest) == password
        customer
      else
        nil
      end
    end
  end
end

Customer#points メソッドを次のように実装しています:

rewards.sum(:points)

これは次のような SQL 文を発行して値を取得するのと同じです(Customer オブジェクトの ID を 7 とした場合)。

SELECT SUM(points) FROM rewards WHERE customer_id = 7

これで Customer#points メソッドのエグザンプルは通ります。

ログインポイントの実装に戻る

ペンディングになっているエグザンプルに戻ります。現在は、次のようになっています:

  specify 'ログインに成功すると、ユーザーの保有ポイントが1増える' do
    pending('Customer#pointsが未実装')
    customer.stub(:points).and_return(0)
    expect {
      Customer.authenticate(customer.username, 'correct_password')
    }.to change { customer.points }.by(1)
  end

これを次のように変更します:

  specify 'ログインに成功すると、ユーザーの保有ポイントが1増える' do
    expect {
      Customer.authenticate(customer.username, 'correct_password')
    }.to change { customer.points }.by(1)
  end

テストの実行結果:

Failures:

  1) Customer.authenticate ログインに成功すると、ユーザーの保有ポイントが1増える
     Failure/Error: expect {
       result should have been changed by 1, but was changed by 0
     # ./spec/models/customer_spec.rb:121:in `block (2 levels) in <top (required)>'

app/models/customer.rb を次のように修正します(11-13行挿入)。

require 'nkf'
require 'bcrypt'

class Customer < ActiveRecord::Base
  # (省略)

  class << self
    def authenticate(username, password)
      customer = find_by_username(username)
      if customer.try(:password_digest) && BCrypt::Password.new(customer.password_digest) == password
        customer.rewards.create(points: 1)
        customer
      else
        nil
      end
    end
  end
end

挿入されたのは次のコードです:

customer.rewards.create(points: 1)

これで、この顧客にポイントが1点付与されます。

次回は

次回は、ポイントシステムの第2の仕様「ログインポイントはユーザーごとに1日1回しか与えられない」のテストを書いて実装します。では、また。