ポイントシステム(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回しか与えられない」のテストを書いて実装します。では、また。