shop_bunny 0.8.5 → 0.8.6
Sign up to get free protection for your applications and to get access to all the features.
- data/README +18 -0
- data/Rakefile +33 -0
- data/app/controllers/shop_bunny/application_controller.rb +5 -0
- data/app/controllers/shop_bunny/carts_controller.rb +5 -0
- data/app/models/{cart_item.rb → shop_bunny/cart_item.rb} +9 -13
- data/app/models/{coupon.rb → shop_bunny/coupon.rb} +14 -18
- data/app/models/shop_bunny/coupon_use.rb +7 -0
- data/app/views/{carts → shop_bunny/carts}/show.html.erb +20 -25
- data/config/locales/shop_bunny.en.yml +10 -0
- data/config/routes.rb +3 -62
- data/db/development.sqlite3 +0 -0
- data/db/migrate/20110202235446_add_raw_item_to_cart_items.rb +1 -1
- data/db/test.sqlite3 +0 -0
- data/lib/generators/install/USAGE +8 -0
- data/lib/generators/install/install_generator.rb +23 -0
- data/{config/initializers/shop_bunny.rb → lib/generators/install/templates/initializer.rb} +0 -0
- data/lib/generators/{shop_bunny → install}/templates/shopbunny_model_template.rb +0 -0
- data/lib/shop_bunny/cart_controller_module.rb +6 -14
- data/lib/shop_bunny/cart_module.rb +21 -43
- data/{config/initializers/add_cart_finder_to_controllers.rb → lib/shop_bunny/controller_helpers.rb} +5 -5
- data/lib/shop_bunny/engine.rb +10 -5
- data/lib/shop_bunny/version.rb +3 -0
- data/lib/shop_bunny.rb +6 -1
- data/spec/controllers/shop_bunny/carts_controller_spec.rb +97 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +9 -0
- data/spec/dummy/app/assets/stylesheets/application.css +7 -0
- data/{app → spec/dummy/app}/controllers/application_controller.rb +0 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/{app → spec/dummy/app}/models/cart.rb +1 -1
- data/{app → spec/dummy/app}/models/item.rb +1 -2
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config/application.rb +50 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +30 -0
- data/spec/dummy/config/environments/production.rb +60 -0
- data/spec/dummy/config/environments/test.rb +39 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +10 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config.ru +4 -0
- data/{db → spec/dummy/db}/migrate/20100915073016_create_items.rb +0 -0
- data/spec/dummy/db/schema.rb +61 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +26 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/models/cart_spec.rb +344 -0
- data/spec/models/shop_bunny/cart_item_spec.rb +56 -0
- data/spec/models/shop_bunny/coupon_spec.rb +177 -0
- data/spec/models/shop_bunny/coupon_use_spec.rb +6 -0
- data/spec/requests/shop_bunny/shopping_spec.rb +119 -0
- data/spec/shop_bunny_test.rb +7 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/blueprints/cart_items.rb +4 -0
- data/spec/support/blueprints/carts.rb +2 -0
- data/spec/support/blueprints/coupons.rb +50 -0
- data/spec/support/blueprints/items.rb +4 -0
- metadata +189 -24
- data/app/controllers/carts_controller.rb +0 -4
- data/app/models/coupon_use.rb +0 -15
- data/config/initializers/machinist.rb +0 -19
- data/lib/generators/shop_bunny/install_generator.rb +0 -18
- data/lib/generators/shop_bunny/templates/shopbunny_controller_template.rb +0 -3
@@ -0,0 +1,344 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Cart do
|
4
|
+
include ShopBunny
|
5
|
+
it { should have_many :cart_items}
|
6
|
+
|
7
|
+
context "A Cart" do
|
8
|
+
before(:each) do
|
9
|
+
@article1 = Item.make(:price => 10.0)
|
10
|
+
@article2 = Item.make(:price => 20.0)
|
11
|
+
@cart = Cart.make
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should be able to add articles" do
|
15
|
+
proc {@cart.add_item(@article1, :quantity => 1)}.should change(@cart, :item_count).by(1)
|
16
|
+
@cart.items.size.should be(1)
|
17
|
+
@cart.item_sum.should be_within(0.01).of(10)
|
18
|
+
proc {@cart.add_item(@article2, :quantity => 1)}.should change(@cart, :item_count).by(1)
|
19
|
+
@cart.items.size.should be(2)
|
20
|
+
@cart.item_sum.should be_within(0.01).of(30)
|
21
|
+
proc {@cart.add_item(@article1, :quantity => 3)}.should change(@cart, :item_count).by(3)
|
22
|
+
@cart.items.size.should be(2)
|
23
|
+
@cart.item_sum.should be_within(0.01).of(60)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should be able to set a default quantity with 1" do
|
27
|
+
proc {@cart.add_item(@article1)}.should change(@cart, :item_count).by(1)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "passes shipping_cost requests to the shipping costs calculator" do
|
31
|
+
calculator = mock()
|
32
|
+
calculator.expects(:costs_for).with(@cart, anything).once
|
33
|
+
|
34
|
+
@cart.stubs(:shipping_cost_calculator).returns(calculator)
|
35
|
+
@cart.shipping_costs
|
36
|
+
|
37
|
+
calculator = mock()
|
38
|
+
calculator.expects(:costs_for).with(@cart, :net_costs => true).once
|
39
|
+
|
40
|
+
@cart.stubs(:shipping_cost_calculator).returns(calculator)
|
41
|
+
|
42
|
+
@cart.shipping_costs(:net_costs => true)
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#items_with_coupons" do
|
46
|
+
it "might return a negative value" do
|
47
|
+
@cart.stubs(:item_sum).returns(100.0)
|
48
|
+
@cart.coupons << Coupon.make(:discount_credit => 110)
|
49
|
+
@cart.items_with_coupons.should == -10.0
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "adding coupons using #coupons<<" do
|
54
|
+
it "adds a coupon though CouponUse" do
|
55
|
+
coupon = Coupon.make
|
56
|
+
add_a_coupon = lambda {
|
57
|
+
@cart.coupons << coupon
|
58
|
+
}
|
59
|
+
add_a_coupon.should change { @cart.coupons.count }.by(1)
|
60
|
+
add_a_coupon.should change { CouponUse.count }.by(1)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "adds a Coupon only once" do
|
64
|
+
coupon = Coupon.make
|
65
|
+
lambda {
|
66
|
+
@cart.coupons << coupon
|
67
|
+
@cart.coupons << coupon
|
68
|
+
}.should change { @cart.coupons.count }.by(1)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "#empty" do
|
73
|
+
it "knows that it is empty when empty" do
|
74
|
+
@cart.cart_items.should be_empty
|
75
|
+
@cart.should be_empty
|
76
|
+
end
|
77
|
+
|
78
|
+
it "is not empty when havin a bonus article" do
|
79
|
+
@item = Item.make
|
80
|
+
@coupon = Coupon.make(:bonus_article => @item)
|
81
|
+
@cart.coupon_code = @coupon.code
|
82
|
+
@cart.save
|
83
|
+
@cart.should_not be_empty
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context "with an article" do
|
88
|
+
before(:each) do
|
89
|
+
@cart.add_item(@article1, :quantity => 10)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should destroy empty cart_items" do
|
93
|
+
@cart.cart_items.first.update_attribute :quantity, 0
|
94
|
+
@cart.cart_items.should be_empty
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should be able to remove all occurences of an article in the cart" do
|
98
|
+
# FIXME DRY?
|
99
|
+
article1_count = @cart.cart_items.select {|ci| ci.item == @article1}.first.quantity
|
100
|
+
@cart.item_count.should be(article1_count)
|
101
|
+
proc {@cart.remove_item(@article1)}.should change(@cart, :item_count).by(-1 * article1_count)
|
102
|
+
|
103
|
+
@cart.item_count.should be(0)
|
104
|
+
|
105
|
+
@cart.add_item(@article1, :quantity => 10)
|
106
|
+
proc {@cart.remove_item(@article1, :quantity => 100)}.should change(@cart, :item_count).to(0)
|
107
|
+
@cart.cart_items.should be_empty
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should be able to set the quantity of an article directly" do
|
111
|
+
proc {@cart.update_item(@article1, :quantity => 5)}.should change(@cart, :item_count).to(5)
|
112
|
+
end
|
113
|
+
|
114
|
+
it "JSON representation should include cart_items" do
|
115
|
+
decoded = ActiveSupport::JSON.decode(@cart.to_json)
|
116
|
+
decoded['cart']['cart_items'].should_not be_empty
|
117
|
+
# FIXME Test existence of other values like, total, coupons etc.
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context "adding coupons with #coupon_code=" do
|
122
|
+
it "should show an error when adding an incorrect coupon code" do
|
123
|
+
@cart.coupon_code = "incorrectcode"
|
124
|
+
@cart.should_not be_valid
|
125
|
+
@cart.errors[:coupon_code].should_not be_nil
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should add a coupon after save via coupon_code=" do
|
129
|
+
coupon = Coupon.make(:percent20off)
|
130
|
+
@cart.coupon_code = coupon.code
|
131
|
+
@cart.coupons.should be_empty
|
132
|
+
@cart.save
|
133
|
+
@cart.coupons.should include coupon
|
134
|
+
end
|
135
|
+
|
136
|
+
it "with max_uses=1 via coupon_code= should not invalidate the cart" do
|
137
|
+
coupon = Coupon.make(:max_uses => 1)
|
138
|
+
@cart.coupon_code = coupon.code
|
139
|
+
@cart.save!
|
140
|
+
@cart.should be_valid
|
141
|
+
end
|
142
|
+
|
143
|
+
it "should create coupon_uses" do
|
144
|
+
coupon = Coupon.make
|
145
|
+
@cart.coupon_code = coupon.code
|
146
|
+
lambda {
|
147
|
+
@cart.save!
|
148
|
+
}.should change { coupon.coupon_uses.count }.by(1)
|
149
|
+
end
|
150
|
+
|
151
|
+
it "should only add one coupon of a kind" do
|
152
|
+
coupon = Coupon.make(:percent20off)
|
153
|
+
|
154
|
+
@cart.coupon_code = coupon.code
|
155
|
+
@cart.coupons.should be_empty
|
156
|
+
@cart.save
|
157
|
+
@cart.reload
|
158
|
+
|
159
|
+
@cart.coupons.should include coupon
|
160
|
+
@cart.coupons.size.should == 1
|
161
|
+
|
162
|
+
@cart.coupon_code = ""
|
163
|
+
@cart.save
|
164
|
+
@cart.reload
|
165
|
+
|
166
|
+
@cart.coupons.size.should == 1
|
167
|
+
@cart.coupon_code = coupon.code
|
168
|
+
@cart.save
|
169
|
+
@cart.reload
|
170
|
+
|
171
|
+
@cart.coupons.size.should == 1
|
172
|
+
end
|
173
|
+
|
174
|
+
it "should not add a coupon if they are all used up" do
|
175
|
+
coupon = Coupon.make(:percent20off)
|
176
|
+
coupon.max_uses = -1
|
177
|
+
coupon.save
|
178
|
+
@cart.coupon_code = coupon.code
|
179
|
+
@cart.coupons.should be_empty
|
180
|
+
@cart.save
|
181
|
+
@cart.reload
|
182
|
+
@cart.coupons.size.should == 0
|
183
|
+
end
|
184
|
+
end
|
185
|
+
context "with bonusarticle" do
|
186
|
+
before do
|
187
|
+
@item = Item.make
|
188
|
+
@coupon = Coupon.make(:bonus_article => @item)
|
189
|
+
@coupon2 = Coupon.make()
|
190
|
+
@cart.coupon_code = @coupon.code
|
191
|
+
@cart.save
|
192
|
+
@cart.coupon_code = @coupon2.code
|
193
|
+
@cart.save
|
194
|
+
end
|
195
|
+
|
196
|
+
it "should contain the bonus item" do
|
197
|
+
@cart.bonus_items.should include @item
|
198
|
+
end
|
199
|
+
|
200
|
+
it "should not contain nil articles in bonus articles" do
|
201
|
+
@cart.coupons.size.should == 2
|
202
|
+
@cart.bonus_items.size.should == 1
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
context "with mutiple articles" do
|
207
|
+
before(:each) do
|
208
|
+
@article3 = Item.make(:price => 30.3)
|
209
|
+
@cart.add_item(@article1, :quantity => 10)
|
210
|
+
@cart.add_item(@article2, :quantity => 2)
|
211
|
+
@cart.add_item(@article3, :quantity => 4)
|
212
|
+
end
|
213
|
+
|
214
|
+
it "should be able to calculate the sum" do
|
215
|
+
@cart.item_sum.should be_within(0.01).of(10*10.0+2*20.0+4*30.3)
|
216
|
+
end
|
217
|
+
|
218
|
+
it "can clear all items and coupons from the cart" do
|
219
|
+
@cart.cart_items.size.should be > 0
|
220
|
+
a_cart_item = @cart.cart_items.last
|
221
|
+
|
222
|
+
coupon = Coupon.make()
|
223
|
+
@cart.coupons << coupon
|
224
|
+
@cart.save!
|
225
|
+
@cart.reload
|
226
|
+
@cart.coupons.size.should be > 0
|
227
|
+
a_coupon_use = @cart.coupon_uses.last
|
228
|
+
|
229
|
+
@cart.clear!
|
230
|
+
@cart.cart_items.size.should be 0
|
231
|
+
@cart.coupons.size.should be 0
|
232
|
+
|
233
|
+
CartItem.find_by_id(a_cart_item.id).should be_nil
|
234
|
+
CouponUse.find_by_id(a_coupon_use.id).should be_nil
|
235
|
+
end
|
236
|
+
|
237
|
+
|
238
|
+
context "and coupons" do
|
239
|
+
it "should calculate the sum with a 20% off coupon" do
|
240
|
+
@cart.coupons << Coupon.make(:percent20off)
|
241
|
+
@cart.total.should be_within(0.01).of(@cart.item_sum*0.8 + @cart.shipping_costs)
|
242
|
+
end
|
243
|
+
|
244
|
+
it "should reduce items sum by 10" do
|
245
|
+
@cart.coupons << Coupon.make(:euro10)
|
246
|
+
@cart.total.should be_within(0.01).of(@cart.item_sum - 10 + @cart.shipping_costs)
|
247
|
+
end
|
248
|
+
|
249
|
+
it "should be no shipping costs with a coupon" do
|
250
|
+
@cart.shipping_costs.should be_within(0.01).of(8.90)
|
251
|
+
@cart.coupons << Coupon.make(:shipping)
|
252
|
+
@cart.shipping_costs.should be_within(0.01).of(0)
|
253
|
+
end
|
254
|
+
|
255
|
+
it "can not have a negative total" do
|
256
|
+
@cart.total.should >(0)
|
257
|
+
|
258
|
+
@cart.coupons << Coupon.make(:euro10, :discount_credit => @cart.total + 10)
|
259
|
+
|
260
|
+
@cart.total.should be_within(0.01).of(0)
|
261
|
+
end
|
262
|
+
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
context "automatic coupons" do
|
267
|
+
it "adds coupons automatically which are valid (datewise) and are set to be enabled automatically at a certain total price of a cart" do
|
268
|
+
expensive_product = Item.make(:price => 50.01)
|
269
|
+
intermediate_mock = mock
|
270
|
+
Coupon.stubs(:valid).returns(intermediate_mock)
|
271
|
+
bonus_coupon = Coupon.make
|
272
|
+
intermediate_mock.expects(:automatically_added_over).with(expensive_product.price).returns([bonus_coupon])
|
273
|
+
@cart.add_item(expensive_product)
|
274
|
+
@cart.coupons.should == [bonus_coupon]
|
275
|
+
end
|
276
|
+
|
277
|
+
it "never adds an automatic coupon twice" do
|
278
|
+
coupon = Coupon.make(:value_of_automatic_add => 10)
|
279
|
+
item = Item.make(:price => 10)
|
280
|
+
@cart.clear!
|
281
|
+
@cart.add_item(item)
|
282
|
+
@cart.add_item(item)
|
283
|
+
@cart.coupons.should include coupon
|
284
|
+
@cart.coupons.count.should be
|
285
|
+
end
|
286
|
+
|
287
|
+
it "it removes automatically added coupons that are not longer applicable from the cart whenever an item is removed" do
|
288
|
+
coupon1 = Coupon.make(:value_of_automatic_add => 10)
|
289
|
+
coupon2 = Coupon.make(:value_of_automatic_add => 15)
|
290
|
+
|
291
|
+
item1 = Item.make(:price => 12)
|
292
|
+
item2 = Item.make(:price => 3)
|
293
|
+
|
294
|
+
@cart.clear!
|
295
|
+
@cart.add_item(item1)
|
296
|
+
@cart.add_item(item2)
|
297
|
+
@cart.coupons.size.should be 2
|
298
|
+
@cart.coupons.should include(coupon1)
|
299
|
+
@cart.coupons.should include(coupon2)
|
300
|
+
|
301
|
+
@cart.remove_item(item2)
|
302
|
+
@cart.coupons.size.should be 1
|
303
|
+
@cart.coupons.should include(coupon1)
|
304
|
+
|
305
|
+
@cart.remove_item(item1)
|
306
|
+
@cart.coupons.size.should be 0
|
307
|
+
end
|
308
|
+
|
309
|
+
it "coupons reappear when the cart had enough items, the user then removed some so that the coupon wasn't automatically applicable anymore and then adds more items to the cart" do
|
310
|
+
coupon = Coupon.make(:value_of_automatic_add => 10)
|
311
|
+
|
312
|
+
item1 = Item.make(:price => 5)
|
313
|
+
item2 = Item.make(:price => 6)
|
314
|
+
item3 = Item.make(:price => 7)
|
315
|
+
|
316
|
+
@cart.clear!
|
317
|
+
@cart.add_item(item1)
|
318
|
+
@cart.add_item(item2)
|
319
|
+
@cart.remove_item(item2)
|
320
|
+
@cart.add_item(item3)
|
321
|
+
|
322
|
+
@cart.coupons.size.should be 1
|
323
|
+
@cart.coupons.should include(coupon)
|
324
|
+
end
|
325
|
+
|
326
|
+
it "adds or removes the coupon when updating a cart item" do
|
327
|
+
coupon = Coupon.make(:value_of_automatic_add => 10)
|
328
|
+
item = Item.make(:price => 5)
|
329
|
+
|
330
|
+
@cart.clear!
|
331
|
+
@cart.add_item(item)
|
332
|
+
@cart.coupons.size.should be 0
|
333
|
+
|
334
|
+
@cart.update_item(item, :quantity => 2)
|
335
|
+
@cart.coupons.size.should be 1
|
336
|
+
@cart.coupons.should include(coupon)
|
337
|
+
|
338
|
+
@cart.update_item(item, :quantity => 1)
|
339
|
+
@cart.coupons.size.should be 0
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
end
|
344
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ShopBunny::CartItem do
|
4
|
+
include ShopBunny
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
@cart_item = CartItem.make
|
8
|
+
@cart_item.item = Item.new
|
9
|
+
end
|
10
|
+
|
11
|
+
it {should belong_to :cart}
|
12
|
+
it {should_not belong_to :item}
|
13
|
+
it {should validate_presence_of :cart_id }
|
14
|
+
it {should validate_numericality_of(:quantity)}
|
15
|
+
|
16
|
+
it "knows it's total price" do
|
17
|
+
@cart_item.item.price = 10
|
18
|
+
|
19
|
+
@cart_item.total_price.should be_within(0.001).of(10)
|
20
|
+
|
21
|
+
@cart_item.quantity = 5
|
22
|
+
|
23
|
+
@cart_item.total_price.should be_within(0.001).of(50)
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "item marshalling" do
|
27
|
+
it "marshalls it's item and reloads it on request" do
|
28
|
+
item = Item.make
|
29
|
+
@cart_item.item = item
|
30
|
+
@cart_item.raw_item.should == item.shop_bunny_json_attributes.to_json
|
31
|
+
@cart_item.item.should == item
|
32
|
+
end
|
33
|
+
|
34
|
+
it "is possible to create an item with an item with create or new" do
|
35
|
+
item = Item.make
|
36
|
+
cart_item = CartItem.new(:item => item)
|
37
|
+
cart_item.item.should == item
|
38
|
+
end
|
39
|
+
|
40
|
+
it "uses the shop_bunny_json_attributes method method to marshal if present" do
|
41
|
+
item = Object.new
|
42
|
+
item.expects(:respond_to?).with(:shop_bunny_json_attributes).returns(true)
|
43
|
+
item.expects(:shop_bunny_json_attributes).returns({})
|
44
|
+
|
45
|
+
cart_item = CartItem.new(:item => item)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "uses the attributes method to marshal if the item has no shop_bunny_json_attributes method" do
|
49
|
+
item = Object.new
|
50
|
+
item.expects(:respond_to?).with(:shop_bunny_json_attributes).returns(false)
|
51
|
+
item.expects(:attributes).returns({})
|
52
|
+
|
53
|
+
cart_item = CartItem.new(:item => item)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ShopBunny::Coupon do
|
4
|
+
include ShopBunny
|
5
|
+
subject { Coupon.make }
|
6
|
+
it {should validate_presence_of :title }
|
7
|
+
it {should validate_presence_of :code }
|
8
|
+
it {should validate_uniqueness_of :code }
|
9
|
+
|
10
|
+
it "should not be expired by default" do
|
11
|
+
Coupon.make.should_not be_expired
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should be valid within the given range" do
|
15
|
+
coupon = Coupon.make(:daterange)
|
16
|
+
Time.stubs(:now).returns(Time.local(2010,9,14).to_time)
|
17
|
+
coupon.should_not have_expired
|
18
|
+
coupon.should_not be_not_yet_valid
|
19
|
+
coupon.should be_redeemable
|
20
|
+
Time.stubs(:now).returns(Time.local(2010,2,2).to_time)
|
21
|
+
coupon.reload.should_not have_expired
|
22
|
+
coupon.should be_not_yet_valid
|
23
|
+
coupon.should_not be_redeemable
|
24
|
+
Time.stubs(:now).returns(Time.local(2010,9,22).to_time)
|
25
|
+
coupon.should have_expired
|
26
|
+
coupon.should_not be_redeemable
|
27
|
+
coupon.should_not be_not_yet_valid
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#state" do
|
31
|
+
it "is inactive by default" do
|
32
|
+
Coupon.new.state.should == 'inactive'
|
33
|
+
end
|
34
|
+
|
35
|
+
it "can be overwritten" do
|
36
|
+
Coupon.new(:state => 'active').state.should == 'active'
|
37
|
+
Coupon.make(:state => 'redeemed').state.should == 'redeemed'
|
38
|
+
Coupon.make(:state => 'inactive').state.should == 'inactive'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#activate!" do
|
43
|
+
it "changes the state from inactive to active" do
|
44
|
+
coupon = Coupon.make(:state => 'inactive')
|
45
|
+
coupon.activate!
|
46
|
+
coupon.state.should == 'active'
|
47
|
+
end
|
48
|
+
|
49
|
+
it "fails for all other states" do
|
50
|
+
%w(active redeemed).each do |state|
|
51
|
+
lambda { Coupon.make(:state => state).activate! }.should raise_error(Coupon::InvalidEvent)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
it "saves the record" do
|
56
|
+
coupon = Coupon.make_unsaved(:state => 'inactive')
|
57
|
+
coupon.activate!
|
58
|
+
coupon.should be_persisted
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "#redeem!" do
|
63
|
+
it "changes the state from active to redeemed" do
|
64
|
+
coupon = Coupon.make(:state => 'active')
|
65
|
+
coupon.redeem!
|
66
|
+
coupon.state.should == 'redeemed'
|
67
|
+
end
|
68
|
+
|
69
|
+
it "fails for all other states" do
|
70
|
+
%w(inactive redeemed).each do |state|
|
71
|
+
lambda { Coupon.make(:state => state).redeem! }.should raise_error(Coupon::InvalidEvent)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
it "fails if redeemable? returns false" do
|
76
|
+
lambda {
|
77
|
+
coupon = Coupon.make(:state => 'active')
|
78
|
+
coupon.stubs(:redeemable?).returns(false)
|
79
|
+
coupon.redeem!
|
80
|
+
}.should raise_error
|
81
|
+
end
|
82
|
+
|
83
|
+
it "saves the record" do
|
84
|
+
coupon = Coupon.make_unsaved
|
85
|
+
coupon.redeem!
|
86
|
+
coupon.should be_persisted
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "#redeemable?" do
|
91
|
+
it "is false by default" do
|
92
|
+
Coupon.new.redeemable?.should be_false
|
93
|
+
end
|
94
|
+
|
95
|
+
it "is only true if state is active" do
|
96
|
+
%w(inactive active redeemed).each do |state|
|
97
|
+
Coupon.make(:state => state).redeemable?.should == (state == 'active')
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
it "is false if it is used_up" do
|
102
|
+
coupon = Coupon.make
|
103
|
+
coupon.stubs(:used_up?).returns(true)
|
104
|
+
coupon.should_not be_redeemable
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe "#used_up?" do
|
109
|
+
it "becomes true if used one time" do
|
110
|
+
coupon = Coupon.make(:max_uses => 1)
|
111
|
+
coupon.should_not be_used_up
|
112
|
+
Cart.make(:coupon_code => coupon.code)
|
113
|
+
coupon.reload.should be_used_up
|
114
|
+
end
|
115
|
+
|
116
|
+
it "becomes true if max_uses is reached" do
|
117
|
+
coupon = Coupon.make(:max_uses => 2)
|
118
|
+
coupon.should_not be_used_up
|
119
|
+
2.times { Cart.make(:coupon_code => coupon.code) }
|
120
|
+
coupon.should be_used_up
|
121
|
+
end
|
122
|
+
|
123
|
+
it "is true if max_uses is zero" do
|
124
|
+
coupon = Coupon.make(:max_uses => 0)
|
125
|
+
coupon.should be_used_up
|
126
|
+
end
|
127
|
+
|
128
|
+
it "never becomes true if max_uses is blank (unlimited)" do
|
129
|
+
coupon = Coupon.make(:max_uses => nil)
|
130
|
+
coupon.should_not be_used_up
|
131
|
+
2.times { Cart.make(:coupon_code => coupon.code) }
|
132
|
+
coupon.should_not be_used_up
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
context "scopes" do
|
137
|
+
it "can find the valid coupons" do
|
138
|
+
Coupon.destroy_all # rspec bug?
|
139
|
+
Time.stubs(:now).returns(Time.local(2010,9,22,14,31).to_time)
|
140
|
+
|
141
|
+
valid_coupon = Coupon.make
|
142
|
+
valid_coupon.valid_from = Time.local(2010,9,22,14,30).to_datetime
|
143
|
+
valid_coupon.valid_until = Time.local(2010,9,25,17,30).to_datetime
|
144
|
+
valid_coupon.save!
|
145
|
+
|
146
|
+
invalid_coupon1 = Coupon.make
|
147
|
+
invalid_coupon1.valid_from = Time.local(2010,9,21,10,30).to_datetime
|
148
|
+
invalid_coupon1.valid_until = Time.local(2010,9,22,14,29).to_datetime
|
149
|
+
invalid_coupon1.save!
|
150
|
+
|
151
|
+
invalid_coupon2 = Coupon.make
|
152
|
+
invalid_coupon2.valid_from = Time.local(2010,9,25,17,31).to_datetime
|
153
|
+
invalid_coupon2.valid_until = Time.local(2010,9,26,15,00).to_datetime
|
154
|
+
invalid_coupon2.save!
|
155
|
+
|
156
|
+
inactive_coupon = Coupon.make(:state => 'inactive')
|
157
|
+
|
158
|
+
coupon_without_dates = Coupon.make(:valid_from => nil, :valid_until => nil)
|
159
|
+
|
160
|
+
Coupon.valid.size.should be 2
|
161
|
+
Coupon.valid.should include valid_coupon
|
162
|
+
Coupon.valid.should include coupon_without_dates
|
163
|
+
|
164
|
+
(Coupon.all - Coupon.valid).should include inactive_coupon
|
165
|
+
end
|
166
|
+
|
167
|
+
it "can find coupons with an automatic add value less than the given value" do
|
168
|
+
[10.50, 50, 2].each do |value|
|
169
|
+
lambda {
|
170
|
+
coupon = Coupon.make(:value_of_automatic_add => value)
|
171
|
+
Coupon.make(:value_of_automatic_add => nil)
|
172
|
+
Coupon.automatically_added_over(value).should include(coupon)
|
173
|
+
}.should change { Coupon.automatically_added_over(value).count }.by(1)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "On the cart page" do
|
4
|
+
def add_coupon_code(code)
|
5
|
+
fill_in 'Coupon code', :with => code
|
6
|
+
click_button 'Add coupon'
|
7
|
+
end
|
8
|
+
|
9
|
+
def add_item(item)
|
10
|
+
within "#item_#{item.id}" do
|
11
|
+
click_link "Add to cart"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def remove_item(item)
|
16
|
+
click_link "Remove item #{item.id}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def assert_no_errors
|
20
|
+
find('#error_messages').should have_no_selector('li')
|
21
|
+
end
|
22
|
+
|
23
|
+
before do
|
24
|
+
@item = Item.make(:price => 10)
|
25
|
+
visit cart_path
|
26
|
+
end
|
27
|
+
|
28
|
+
it "shows the expected price numbers for an empty cart" do
|
29
|
+
find('#price-item-sum').should have_content("$0.0")
|
30
|
+
find('#price-shipping-costs').should have_content("$8.90")
|
31
|
+
find('#price-total').should have_content("$8.90")
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "adding an item to the cart" do
|
35
|
+
it "updates the price" do
|
36
|
+
add_item(@item)
|
37
|
+
find('#price-total').should have_content("$18.90")
|
38
|
+
end
|
39
|
+
|
40
|
+
it "lists the item" do
|
41
|
+
add_item(@item)
|
42
|
+
find('#cart .items').should have_content("Remove item #{@item.id}")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "removing an item from the cart" do
|
47
|
+
before { add_item(@item) }
|
48
|
+
it "resets the price" do
|
49
|
+
lambda {
|
50
|
+
remove_item(@item)
|
51
|
+
}.should change { find('#price-total').text }.from("$18.90").to("$8.90")
|
52
|
+
end
|
53
|
+
|
54
|
+
it "removes the item" do
|
55
|
+
find('#cart .items').should have_content("Remove item #{@item.id}")
|
56
|
+
remove_item(@item)
|
57
|
+
find('#cart .items').should have_no_content("Remove item #{@item.id}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "adding coupons" do
|
62
|
+
it "does not show an error if the code is ok" do
|
63
|
+
coupon = ShopBunny::Coupon.make
|
64
|
+
add_coupon_code(coupon.code)
|
65
|
+
assert_no_errors
|
66
|
+
end
|
67
|
+
|
68
|
+
it "does not show an error for one-time coupons" do
|
69
|
+
coupon = ShopBunny::Coupon.make(:max_uses => 1)
|
70
|
+
add_coupon_code(coupon.code)
|
71
|
+
assert_no_errors
|
72
|
+
end
|
73
|
+
|
74
|
+
it "lists the newly added coupon" do
|
75
|
+
coupon = ShopBunny::Coupon.make(:title => "New Coupon")
|
76
|
+
add_coupon_code(coupon.code)
|
77
|
+
find('#your_coupons').should have_content("New Coupon")
|
78
|
+
end
|
79
|
+
|
80
|
+
it "removes the coupon's credit from the total price" do
|
81
|
+
coupon = ShopBunny::Coupon.make(:discount_credit => 5)
|
82
|
+
lambda {
|
83
|
+
add_coupon_code(coupon.code)
|
84
|
+
}.should change { find('#price-total').text }.from("$8.90").to("$3.90")
|
85
|
+
end
|
86
|
+
|
87
|
+
it "does not show a negative total price" do
|
88
|
+
coupon = ShopBunny::Coupon.make(:discount_credit => 10)
|
89
|
+
lambda {
|
90
|
+
add_coupon_code(coupon.code)
|
91
|
+
}.should change { find('#price-total').text }.from("$8.90").to("$0.00")
|
92
|
+
end
|
93
|
+
|
94
|
+
it "shows an error if the code is unknown" do
|
95
|
+
add_coupon_code('unknown')
|
96
|
+
find('#error_messages').should have_content("The provided code is unknown or invalid")
|
97
|
+
end
|
98
|
+
|
99
|
+
it "shows an error if the code is expired" do
|
100
|
+
expired = ShopBunny::Coupon.make(:expired)
|
101
|
+
add_coupon_code(expired.code)
|
102
|
+
find('#error_messages').should have_content("The provided code has expired")
|
103
|
+
page.should have_no_selector('#your_coupons')
|
104
|
+
end
|
105
|
+
|
106
|
+
it "shows an error if the the maximum use count is exceeded" do
|
107
|
+
coupon = ShopBunny::Coupon.make(:max_uses => 1)
|
108
|
+
# use up coupon
|
109
|
+
add_coupon_code(coupon.code)
|
110
|
+
assert_no_errors
|
111
|
+
# try to the same coupon again in a new session
|
112
|
+
reset_session!
|
113
|
+
visit cart_path
|
114
|
+
add_coupon_code(coupon.code)
|
115
|
+
find('#error_messages').should have_content("The number of uses for this code has been exceeded")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|