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.
Files changed (72) hide show
  1. data/README +18 -0
  2. data/Rakefile +33 -0
  3. data/app/controllers/shop_bunny/application_controller.rb +5 -0
  4. data/app/controllers/shop_bunny/carts_controller.rb +5 -0
  5. data/app/models/{cart_item.rb → shop_bunny/cart_item.rb} +9 -13
  6. data/app/models/{coupon.rb → shop_bunny/coupon.rb} +14 -18
  7. data/app/models/shop_bunny/coupon_use.rb +7 -0
  8. data/app/views/{carts → shop_bunny/carts}/show.html.erb +20 -25
  9. data/config/locales/shop_bunny.en.yml +10 -0
  10. data/config/routes.rb +3 -62
  11. data/db/development.sqlite3 +0 -0
  12. data/db/migrate/20110202235446_add_raw_item_to_cart_items.rb +1 -1
  13. data/db/test.sqlite3 +0 -0
  14. data/lib/generators/install/USAGE +8 -0
  15. data/lib/generators/install/install_generator.rb +23 -0
  16. data/{config/initializers/shop_bunny.rb → lib/generators/install/templates/initializer.rb} +0 -0
  17. data/lib/generators/{shop_bunny → install}/templates/shopbunny_model_template.rb +0 -0
  18. data/lib/shop_bunny/cart_controller_module.rb +6 -14
  19. data/lib/shop_bunny/cart_module.rb +21 -43
  20. data/{config/initializers/add_cart_finder_to_controllers.rb → lib/shop_bunny/controller_helpers.rb} +5 -5
  21. data/lib/shop_bunny/engine.rb +10 -5
  22. data/lib/shop_bunny/version.rb +3 -0
  23. data/lib/shop_bunny.rb +6 -1
  24. data/spec/controllers/shop_bunny/carts_controller_spec.rb +97 -0
  25. data/spec/dummy/Rakefile +7 -0
  26. data/spec/dummy/app/assets/javascripts/application.js +9 -0
  27. data/spec/dummy/app/assets/stylesheets/application.css +7 -0
  28. data/{app → spec/dummy/app}/controllers/application_controller.rb +0 -0
  29. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  30. data/{app → spec/dummy/app}/models/cart.rb +1 -1
  31. data/{app → spec/dummy/app}/models/item.rb +1 -2
  32. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  33. data/spec/dummy/config/application.rb +50 -0
  34. data/spec/dummy/config/boot.rb +10 -0
  35. data/spec/dummy/config/database.yml +25 -0
  36. data/spec/dummy/config/environment.rb +5 -0
  37. data/spec/dummy/config/environments/development.rb +30 -0
  38. data/spec/dummy/config/environments/production.rb +60 -0
  39. data/spec/dummy/config/environments/test.rb +39 -0
  40. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  41. data/spec/dummy/config/initializers/inflections.rb +10 -0
  42. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  43. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  44. data/spec/dummy/config/initializers/session_store.rb +8 -0
  45. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  46. data/spec/dummy/config/locales/en.yml +5 -0
  47. data/spec/dummy/config/routes.rb +3 -0
  48. data/spec/dummy/config.ru +4 -0
  49. data/{db → spec/dummy/db}/migrate/20100915073016_create_items.rb +0 -0
  50. data/spec/dummy/db/schema.rb +61 -0
  51. data/spec/dummy/public/404.html +26 -0
  52. data/spec/dummy/public/422.html +26 -0
  53. data/spec/dummy/public/500.html +26 -0
  54. data/spec/dummy/public/favicon.ico +0 -0
  55. data/spec/dummy/script/rails +6 -0
  56. data/spec/models/cart_spec.rb +344 -0
  57. data/spec/models/shop_bunny/cart_item_spec.rb +56 -0
  58. data/spec/models/shop_bunny/coupon_spec.rb +177 -0
  59. data/spec/models/shop_bunny/coupon_use_spec.rb +6 -0
  60. data/spec/requests/shop_bunny/shopping_spec.rb +119 -0
  61. data/spec/shop_bunny_test.rb +7 -0
  62. data/spec/spec_helper.rb +28 -0
  63. data/spec/support/blueprints/cart_items.rb +4 -0
  64. data/spec/support/blueprints/carts.rb +2 -0
  65. data/spec/support/blueprints/coupons.rb +50 -0
  66. data/spec/support/blueprints/items.rb +4 -0
  67. metadata +189 -24
  68. data/app/controllers/carts_controller.rb +0 -4
  69. data/app/models/coupon_use.rb +0 -15
  70. data/config/initializers/machinist.rb +0 -19
  71. data/lib/generators/shop_bunny/install_generator.rb +0 -18
  72. 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,6 @@
1
+ require 'spec_helper'
2
+
3
+ describe ShopBunny::CouponUse do
4
+ it { should validate_presence_of :cart_id }
5
+ it { should validate_presence_of :coupon_id }
6
+ 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
+