bodega 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -2
- data/Gemfile.lock +16 -8
- data/README.md +273 -11
- data/VERSION +1 -1
- data/app/controllers/bodega/orders_controller.rb +23 -12
- data/app/helpers/bodega/application_helper.rb +11 -5
- data/app/helpers/bodega/cart_helper.rb +14 -3
- data/app/models/bodega/order.rb +22 -7
- data/app/models/bodega/order_product.rb +37 -17
- data/app/models/bodega/product.rb +9 -4
- data/app/views/bodega/orders/new.html.erb +17 -5
- data/app/views/bodega/orders/show.html.erb +27 -1
- data/bodega.gemspec +13 -7
- data/config/locales/en.yml +3 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20121111170337_create_bodega_orders.rb +4 -3
- data/db/migrate/20121111170420_create_bodega_order_products.rb +2 -4
- data/lib/bodega/engine.rb +2 -6
- data/lib/bodega/payment_method/paypal.rb +3 -2
- data/lib/bodega.rb +6 -1
- data/lib/generators/bodega/product/product_generator.rb +5 -1
- data/lib/generators/bodega/product/templates/migration.rb +1 -1
- data/lib/generators/bodega/product/templates/model.rb +5 -0
- data/lib/generators/bodega/productize/productize_generator.rb +1 -1
- data/lib/generators/bodega/productize/templates/migration.rb +2 -2
- metadata +39 -7
- data/app/controllers/bodega/application_controller.rb +0 -4
- data/lib/bodega/monetize.rb +0 -15
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
GEM
|
2
|
-
remote:
|
2
|
+
remote: https://rubygems.org/
|
3
3
|
specs:
|
4
4
|
actionpack (3.2.9)
|
5
5
|
activemodel (= 3.2.9)
|
@@ -19,7 +19,7 @@ GEM
|
|
19
19
|
multi_json (~> 1.0)
|
20
20
|
builder (3.0.4)
|
21
21
|
coderay (1.0.8)
|
22
|
-
configurator2 (0.1.
|
22
|
+
configurator2 (0.1.2)
|
23
23
|
diff-lcs (1.1.3)
|
24
24
|
erubis (2.7.0)
|
25
25
|
git (1.2.5)
|
@@ -31,8 +31,14 @@ GEM
|
|
31
31
|
rake
|
32
32
|
rdoc
|
33
33
|
journey (1.0.4)
|
34
|
-
json (1.7.
|
34
|
+
json (1.7.6)
|
35
35
|
method_source (0.8.1)
|
36
|
+
money (5.1.0)
|
37
|
+
i18n (~> 0.6.0)
|
38
|
+
money-rails (0.7.1)
|
39
|
+
activesupport (~> 3.0)
|
40
|
+
money (~> 5.1.0)
|
41
|
+
railties (~> 3.0)
|
36
42
|
multi_json (1.5.0)
|
37
43
|
pry (0.9.10)
|
38
44
|
coderay (~> 1.0.5)
|
@@ -52,13 +58,13 @@ GEM
|
|
52
58
|
rake (>= 0.8.7)
|
53
59
|
rdoc (~> 3.4)
|
54
60
|
thor (>= 0.14.6, < 2.0)
|
55
|
-
rake (0.
|
61
|
+
rake (10.0.3)
|
56
62
|
rdoc (3.12)
|
57
63
|
json (~> 1.4)
|
58
|
-
rspec-core (2.12.
|
59
|
-
rspec-expectations (2.12.
|
64
|
+
rspec-core (2.12.2)
|
65
|
+
rspec-expectations (2.12.1)
|
60
66
|
diff-lcs (~> 1.1.3)
|
61
|
-
rspec-mocks (2.12.
|
67
|
+
rspec-mocks (2.12.1)
|
62
68
|
rspec-rails (2.12.0)
|
63
69
|
actionpack (>= 3.0)
|
64
70
|
activesupport (>= 3.0)
|
@@ -79,7 +85,9 @@ PLATFORMS
|
|
79
85
|
ruby
|
80
86
|
|
81
87
|
DEPENDENCIES
|
82
|
-
configurator2 (>= 0.1.
|
88
|
+
configurator2 (>= 0.1.2)
|
89
|
+
i18n
|
83
90
|
jeweler (= 1.8.4)
|
91
|
+
money-rails
|
84
92
|
pry
|
85
93
|
rspec-rails
|
data/README.md
CHANGED
@@ -1,16 +1,278 @@
|
|
1
1
|
# Bodega
|
2
2
|
|
3
|
-
**Bodega allows any ActiveRecord::Base subclass to be purchased
|
3
|
+
**Bodega is a lightweight Rails engine that allows any ActiveRecord::Base subclass to be purchased.** It lives seamlessly next to your Rails app, so installation and configuration is simple and fun.
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
5
|
+
## Installation
|
6
|
+
1. Add `gem 'bodega'` to your Gemfile and bundle
|
7
|
+
2. Run the install generator: `rails generator bodega:install`
|
8
|
+
3. Route to Bodega, like so:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
MyApp::Application.routes.draw do
|
12
|
+
mount Bodega::Engine => 'cart'
|
13
|
+
end
|
14
|
+
```
|
15
|
+
4. Profit (literally, for once)
|
13
16
|
|
14
|
-
##
|
17
|
+
## Configuration
|
15
18
|
|
16
|
-
|
19
|
+
Bodega configuration happens inside of `config/initializers/bodega.rb`. This file is created when you run the installation generator. Configuration is done via a block, like you're used to:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
Bodega.config do
|
23
|
+
option_name :option_value
|
24
|
+
boolean_option_name false
|
25
|
+
end
|
26
|
+
```
|
27
|
+
|
28
|
+
### Options you can configure
|
29
|
+
|
30
|
+
<table>
|
31
|
+
<thead>
|
32
|
+
<tr>
|
33
|
+
<th>Name</th>
|
34
|
+
<th>Default</th>
|
35
|
+
<th>Description</th>
|
36
|
+
</tr>
|
37
|
+
</thead>
|
38
|
+
<tbody>
|
39
|
+
<tr>
|
40
|
+
<td>customer_method</td>
|
41
|
+
<td>:current_user</td>
|
42
|
+
<td>The method on the controller used to associate a customer to an order. Set to nil if you don't want to associate customers to orders.</td>
|
43
|
+
</tr>
|
44
|
+
<tr>
|
45
|
+
<td>payment_method</td>
|
46
|
+
<td>:paypal</td>
|
47
|
+
<td>The payment method used to process payments. Currently only Paypal is supported.</td>
|
48
|
+
</tr>
|
49
|
+
<tr>
|
50
|
+
<td>test_mode</td>
|
51
|
+
<td>`true` in test or development modes; `false` otherwise</td>
|
52
|
+
<td>Whether or not to process payments in test mode. Useful for development. You can override this if you need to but generally you won't need to.</td>
|
53
|
+
</tr>
|
54
|
+
</tbody>
|
55
|
+
</table>
|
56
|
+
|
57
|
+
|
58
|
+
### Sample configuration
|
59
|
+
|
60
|
+
Here's an example of how you might configure Bodega:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
Bodega.config do
|
64
|
+
# We don't associate orders to user / customer records
|
65
|
+
customer_method nil
|
66
|
+
end
|
67
|
+
|
68
|
+
if Rails.env.production?
|
69
|
+
Bodega.config do
|
70
|
+
paypal(
|
71
|
+
username: ENV['PAYPAL_USERNAME'],
|
72
|
+
password: ENV['PAYPAL_PASSWORD'],
|
73
|
+
signature: ENV['PAYPAL_SIGNATURE']
|
74
|
+
)
|
75
|
+
end
|
76
|
+
else
|
77
|
+
Bodega.config do
|
78
|
+
paypal(
|
79
|
+
username: 'my_paypal_sandbox@username.com',
|
80
|
+
password: 'paypal_sandbox_password',
|
81
|
+
signature: 'SOME_SIGNATURE_I_GOT_FROM_PAYPAL'
|
82
|
+
)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
## Making a model purchasable ("productizing")
|
88
|
+
|
89
|
+
Bodega just needs a few database columns and a mixin on a model to make it purchasable. You can do this to models you've already created in your app, or create new product models.
|
90
|
+
|
91
|
+
### Pre-existing models
|
92
|
+
For existing models, you need to run the "productize" generator:
|
93
|
+
|
94
|
+
1. `rails generate bodega:productize existing_class_name`
|
95
|
+
2. Add `include Bodega::Product` to your class definition, so something like this:
|
96
|
+
```ruby
|
97
|
+
class User < ActiveRecord::Base
|
98
|
+
include Bodega::Product
|
99
|
+
# etc …
|
100
|
+
end
|
101
|
+
```
|
102
|
+
3. `rake db:migrate`
|
103
|
+
|
104
|
+
### New models
|
105
|
+
|
106
|
+
Just generate new models using the "product" generator:
|
107
|
+
|
108
|
+
1. `rails generate bodega:product new_class_name`
|
109
|
+
2. `rake db:migrate`
|
110
|
+
|
111
|
+
## Adding an item to the cart
|
112
|
+
|
113
|
+
Once you've productized a model, it's trivial to create an "Add to Cart" button for it. Build your controllers and views the way you want, and when you're ready to make, say, a `Bucket` model purchasable, use the following helper method:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
<%= button_to_cart(@bucket) %>
|
117
|
+
```
|
118
|
+
|
119
|
+
As long as you've correctly productized using the instructions above, this will render a button that adds that instance of `Bucket` to the cart.
|
120
|
+
|
121
|
+
## Associating users to orders
|
122
|
+
|
123
|
+
Bodega will automatically attempt to use `current_user` as the `Bodega::Order#customer` association. If you use a different controller method for accessing the current user / customer / administrator / whatever, just provide it to the config block in your `config/initializers/bodega.rb`:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
Bodega.config do
|
127
|
+
customer_method :this_method_returns_the_customer_on_all_controllers
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
If you don't want to associate a customer record, just set it to nil:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
Bodega.config do
|
135
|
+
customer_method nil
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
## Customizing the cart appearance
|
140
|
+
|
141
|
+
The philosophy behind Bodega is that you decide on text, and we'll decide on markup. There are three ways to customize the cart's appearance.
|
142
|
+
|
143
|
+
### HTML & CSS
|
144
|
+
|
145
|
+
The cart uses the following markup:
|
146
|
+
|
147
|
+
```html
|
148
|
+
<table id="bodega-cart">
|
149
|
+
<thead>
|
150
|
+
<tr>
|
151
|
+
<th class="product-name" colspan="2">Product</th>
|
152
|
+
<th class="price">Price</th>
|
153
|
+
<th class="total" colspan="2">Total</th>
|
154
|
+
</tr>
|
155
|
+
</thead>
|
156
|
+
<tbody>
|
157
|
+
<tr>
|
158
|
+
<td class="quantity-field">
|
159
|
+
<input class="quantity" id="products__quantity" max="7" min="1" name="products[][quantity]" type="number" value="1" />
|
160
|
+
</td>
|
161
|
+
<td class="product-name">
|
162
|
+
{{Product Name}}
|
163
|
+
</td>
|
164
|
+
<td class="price">
|
165
|
+
{{Product Price}}
|
166
|
+
</td>
|
167
|
+
<td class="subtotal">
|
168
|
+
{{Total For Product}}
|
169
|
+
</td>
|
170
|
+
<td class="remove">
|
171
|
+
<a href="#">Remove</a>
|
172
|
+
</td>
|
173
|
+
</tr>
|
174
|
+
</tbody>
|
175
|
+
</table>
|
176
|
+
<button id="bodega-update" type="submit">Update Cart</button>
|
177
|
+
<button id="bodega-checkout" type="submit">Checkout</button>
|
178
|
+
```
|
179
|
+
|
180
|
+
This should create ample room for you to style the cart / checkout view as you see fit. Here's an example from [WomanNYC](http://www.womannyc.com/):
|
181
|
+
|
182
|
+
```css
|
183
|
+
#bodega-cart {
|
184
|
+
border-collapse: collapse;
|
185
|
+
border-width: 0;
|
186
|
+
width: 100%;
|
187
|
+
}
|
188
|
+
|
189
|
+
#bodega-cart thead {
|
190
|
+
border-bottom: 1px solid #ccc;
|
191
|
+
text-align: left;
|
192
|
+
}
|
193
|
+
|
194
|
+
#bodega-cart td,
|
195
|
+
#bodega-cart th {
|
196
|
+
font-size: 110%;
|
197
|
+
padding: 0.2em 1em 0.2em 0;
|
198
|
+
}
|
199
|
+
|
200
|
+
#bodega-cart .product-name img {
|
201
|
+
vertical-align: middle;
|
202
|
+
width: 2em;
|
203
|
+
}
|
204
|
+
|
205
|
+
#bodega-cart .quantity-field {
|
206
|
+
width: 3em;
|
207
|
+
}
|
208
|
+
|
209
|
+
#bodega-cart .quantity-field input {
|
210
|
+
display: inline-block;
|
211
|
+
font-size: 110%;
|
212
|
+
width: 3em;
|
213
|
+
}
|
214
|
+
```
|
215
|
+
|
216
|
+
### I18N
|
217
|
+
|
218
|
+
Bodega allows you to customize the text labels for the "Product", "Price", and "Total" columns, the "Check Out", "Remove", and "Update Cart" button labels, and the empty cart notification text. Here's an example locale for configuring Bodega:
|
219
|
+
|
220
|
+
```yaml
|
221
|
+
en:
|
222
|
+
bodega:
|
223
|
+
product: "Bucket Name"
|
224
|
+
price: "Bucket Price"
|
225
|
+
total: "Total Price"
|
226
|
+
check_out: "Check Out Now"
|
227
|
+
remove: "Remove From Cart"
|
228
|
+
update_cart: "Save Cart Changes"
|
229
|
+
empty_cart: "You don't have any buckets in your cart yet!"
|
230
|
+
```
|
231
|
+
|
232
|
+
### Decorators
|
233
|
+
|
234
|
+
If your product instances respond to a method `Product#decorator`, which returns a decorator class, Bodega will automatically use that to present your product instead of the direct instance. It does this by following the convention of calling `DecoratorClass.decorate(instance)`. Given the following productized model:
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
class Deck < ActiveRecord::Base
|
238
|
+
include Bodega::Product
|
239
|
+
|
240
|
+
def decorator
|
241
|
+
Deckorator
|
242
|
+
end
|
243
|
+
end
|
244
|
+
```
|
245
|
+
|
246
|
+
Bodega would use `Deckorator.decorate(@deck)` to use a decorator for the Deck instance. A common pattern in decorators is something like the following:
|
247
|
+
|
248
|
+
```ruby
|
249
|
+
class Deckorator
|
250
|
+
attr_accessor :product
|
251
|
+
|
252
|
+
class << self
|
253
|
+
def decorate(products)
|
254
|
+
if products.respond_to?(:each)
|
255
|
+
products.map { |product| new(product) }
|
256
|
+
else
|
257
|
+
new(products)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def initialize(product)
|
263
|
+
self.product = product
|
264
|
+
end
|
265
|
+
|
266
|
+
def name
|
267
|
+
%[<img alt="#{product.name}" src="#{photo.url(:thumb)}" /> #{artist.name}: #{product.name}].html_safe
|
268
|
+
end
|
269
|
+
|
270
|
+
protected
|
271
|
+
def method_missing(method, *args)
|
272
|
+
product.send(method, *args)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
```
|
277
|
+
|
278
|
+
Use this to provide Bodega-specific labels for products which are being purchased.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
@@ -1,6 +1,5 @@
|
|
1
1
|
class Bodega::OrdersController < ApplicationController
|
2
2
|
helper 'bodega/cart'
|
3
|
-
include Bodega::CartHelper
|
4
3
|
include Bodega::PaymentMethod
|
5
4
|
|
6
5
|
before_filter :find_order, only: [:show, :update]
|
@@ -13,9 +12,13 @@ class Bodega::OrdersController < ApplicationController
|
|
13
12
|
end
|
14
13
|
|
15
14
|
def complete
|
16
|
-
current_order.
|
17
|
-
|
18
|
-
|
15
|
+
if current_order.finalize!(payment_method)
|
16
|
+
current_products.clear
|
17
|
+
redirect_to order_path(current_order)
|
18
|
+
else
|
19
|
+
flash[:error] = "There was a problem processing this order. Your account has not been charged."
|
20
|
+
redirect_to new_order_path
|
21
|
+
end
|
19
22
|
end
|
20
23
|
|
21
24
|
def create
|
@@ -29,23 +32,31 @@ class Bodega::OrdersController < ApplicationController
|
|
29
32
|
end
|
30
33
|
end
|
31
34
|
|
35
|
+
def remove
|
36
|
+
current_products.delete params[:product_id]
|
37
|
+
redirect_to :back
|
38
|
+
end
|
39
|
+
|
32
40
|
protected
|
33
41
|
def find_order
|
34
|
-
raise ActiveRecord::
|
42
|
+
raise ActiveRecord::RecordNotFound unless @order = Bodega::Order.where(identifier: params[:order_id] || params[:id]).first
|
35
43
|
end
|
36
44
|
|
37
|
-
def update_cart(
|
38
|
-
product_id = "#{
|
39
|
-
if
|
45
|
+
def update_cart(product_hash)
|
46
|
+
product_id = "#{product_hash[:type]}.#{product_hash[:id]}"
|
47
|
+
if product_hash[:remove]
|
40
48
|
current_products.delete product_id
|
41
49
|
else
|
42
|
-
if
|
43
|
-
current_quantity =
|
50
|
+
if current_product = current_products[product_id]
|
51
|
+
current_quantity = current_product[:quantity].to_i
|
44
52
|
else
|
45
53
|
current_quantity = 0
|
46
54
|
end
|
47
|
-
new_quantity =
|
48
|
-
|
55
|
+
new_quantity = product_hash[:quantity] ? product_hash[:quantity].to_i : current_quantity + 1
|
56
|
+
if product = product_hash[:type].constantize.where(id: product_hash[:id], keep_stock: true).first
|
57
|
+
new_quantity = [product.number_in_stock, new_quantity].min
|
58
|
+
end
|
59
|
+
current_products[product_id] = product_hash.merge(quantity: new_quantity)
|
49
60
|
end
|
50
61
|
end
|
51
62
|
end
|
@@ -1,11 +1,17 @@
|
|
1
1
|
module Bodega
|
2
2
|
module ApplicationHelper
|
3
|
-
def
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
button_tag(label)
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
alias :method_missing_without_bodega :method_missing
|
6
|
+
alias :method_missing :method_missing_with_bodega
|
8
7
|
end
|
9
8
|
end
|
9
|
+
protected
|
10
|
+
def method_missing_with_bodega(method_name, *args)
|
11
|
+
if method_name.to_s =~ /.+_(url|path)$/ && main_app.respond_to?(method_name)
|
12
|
+
return main_app.send(method_name, *args)
|
13
|
+
end
|
14
|
+
method_missing_without_bodega method_name, *args
|
15
|
+
end
|
10
16
|
end
|
11
17
|
end
|
@@ -1,15 +1,26 @@
|
|
1
1
|
module Bodega
|
2
2
|
module CartHelper
|
3
|
+
def button_to_cart(product, label = 'Add to Cart', options = {})
|
4
|
+
unless options.key? :disabled
|
5
|
+
options[:disabled] = !product.in_stock?
|
6
|
+
end
|
7
|
+
form_tag(bodega.add_path) do
|
8
|
+
hidden_field_tag('product[type]', product.class) +
|
9
|
+
hidden_field_tag('product[id]', product.id) +
|
10
|
+
button_tag(label, options)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
3
14
|
protected
|
4
15
|
def current_order
|
5
16
|
@current_order ||= Bodega::Order.new.tap do |order|
|
6
|
-
|
17
|
+
begin
|
7
18
|
if Bodega.config.customer_method
|
8
19
|
order.customer = send(Bodega.config.customer_method)
|
9
20
|
end
|
10
|
-
|
21
|
+
rescue NoMethodError
|
11
22
|
raise "Please configure Bodega.config.customer_method to point to a valid method for accessing a customer record (default: current_user)"
|
12
|
-
|
23
|
+
end
|
13
24
|
order.order_products = current_products.map do |type, product|
|
14
25
|
product = product.symbolize_keys
|
15
26
|
OrderProduct.new do |order_product|
|
data/app/models/bodega/order.rb
CHANGED
@@ -1,19 +1,34 @@
|
|
1
1
|
module Bodega
|
2
2
|
class Order < ActiveRecord::Base
|
3
|
-
|
4
|
-
|
3
|
+
before_save :set_total
|
5
4
|
before_create :set_identifier
|
6
5
|
|
7
6
|
belongs_to :customer, polymorphic: true
|
8
7
|
has_many :order_products, class_name: 'Bodega::OrderProduct', dependent: :destroy
|
9
8
|
has_many :products, through: :order_products
|
10
9
|
|
11
|
-
monetize :
|
12
|
-
monetize :
|
13
|
-
|
10
|
+
monetize :tax_cents
|
11
|
+
monetize :total_cents
|
12
|
+
|
13
|
+
def finalize!(payment_method)
|
14
|
+
self.class.transaction do
|
15
|
+
self.save!
|
16
|
+
begin
|
17
|
+
self.payment_id = payment_method.complete!
|
18
|
+
self.save!
|
19
|
+
rescue Exception => e
|
20
|
+
raise ActiveRecord::Rollback
|
21
|
+
raise e.inspect
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
14
25
|
|
15
26
|
def subtotal
|
16
|
-
order_products.inject(0) {|sum, order_product| sum += order_product.subtotal }
|
27
|
+
@subtotal ||= order_products.inject(Money.new(0)) {|sum, order_product| sum += order_product.subtotal }
|
28
|
+
end
|
29
|
+
|
30
|
+
def set_total
|
31
|
+
self.total = subtotal + tax
|
17
32
|
end
|
18
33
|
|
19
34
|
def to_param
|
@@ -22,7 +37,7 @@ module Bodega
|
|
22
37
|
|
23
38
|
protected
|
24
39
|
def set_identifier
|
25
|
-
self.identifier =
|
40
|
+
self.identifier = self.class.count.succ.to_s(36)
|
26
41
|
end
|
27
42
|
end
|
28
43
|
end
|
@@ -1,25 +1,25 @@
|
|
1
1
|
module Bodega
|
2
2
|
class OrderProduct < ActiveRecord::Base
|
3
|
-
|
3
|
+
after_save :update_stock
|
4
|
+
before_save :calculate_total
|
4
5
|
|
5
6
|
belongs_to :order, class_name: 'Bodega::Order'
|
6
7
|
belongs_to :product, polymorphic: true
|
7
8
|
|
8
|
-
delegate :price, to: :product
|
9
|
+
delegate :keep_stock?, :price, to: :product
|
9
10
|
|
10
|
-
monetize :
|
11
|
-
monetize :tax
|
12
|
-
monetize :total
|
11
|
+
monetize :total_cents
|
13
12
|
|
14
13
|
validates_numericality_of :quantity, allow_blank: true, minimum: 1
|
15
14
|
validates_presence_of :quantity
|
15
|
+
validate :product_available?
|
16
16
|
|
17
|
-
def
|
18
|
-
|
17
|
+
def identifier
|
18
|
+
"#{product_type}.#{product_id}"
|
19
19
|
end
|
20
20
|
|
21
21
|
def name
|
22
|
-
|
22
|
+
product.respond_to?(:name) ? product.name : product.to_s
|
23
23
|
end
|
24
24
|
|
25
25
|
def quantity_and_name
|
@@ -27,21 +27,41 @@ module Bodega
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def subtotal
|
30
|
-
|
30
|
+
price * quantity
|
31
31
|
end
|
32
32
|
|
33
|
-
|
34
|
-
|
33
|
+
protected
|
34
|
+
def calculate_total
|
35
|
+
self.total = subtotal
|
35
36
|
end
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
38
|
+
def product_available?
|
39
|
+
unless product.number_in_stock >= quantity
|
40
|
+
quantity_error = case quantity
|
41
|
+
when 1
|
42
|
+
"We're sorry, but the #{name} you requested is no longer in stock."
|
43
|
+
else
|
44
|
+
"We're sorry, but there are no longer #{quantity} #{name.pluralize} in stock."
|
45
|
+
end
|
46
|
+
|
47
|
+
quantity_message = case product.number_in_stock
|
48
|
+
when 0
|
49
|
+
"They are now sold out."
|
50
|
+
when 1
|
51
|
+
"There is now one in stock."
|
52
|
+
else
|
53
|
+
"There are now #{quantity} in stock."
|
54
|
+
end
|
55
|
+
|
56
|
+
errors.add(:quantity, "#{quantity_error} #{quantity_message}.")
|
57
|
+
end
|
40
58
|
end
|
41
59
|
|
42
|
-
def
|
43
|
-
|
44
|
-
|
60
|
+
def update_stock
|
61
|
+
if keep_stock?
|
62
|
+
product.number_in_stock = product.number_in_stock - quantity
|
63
|
+
product.save(validate: false)
|
64
|
+
end
|
45
65
|
end
|
46
66
|
end
|
47
67
|
end
|
@@ -2,15 +2,20 @@ module Bodega
|
|
2
2
|
module Product
|
3
3
|
def self.included(base)
|
4
4
|
base.class_eval do
|
5
|
-
extend Bodega::Monetize
|
6
|
-
|
7
5
|
has_many :order_products, as: :product, class_name: 'Bodega::OrderProduct'
|
8
6
|
has_many :orders, through: :order_products
|
9
7
|
|
10
|
-
monetize :
|
8
|
+
monetize :price_cents
|
11
9
|
|
12
10
|
scope :for_sale, lambda {
|
13
|
-
where
|
11
|
+
where %[
|
12
|
+
for_sale IS TRUE OR
|
13
|
+
(
|
14
|
+
(for_sale_at >= :today OR for_sale_at IS NULL) AND
|
15
|
+
(not_for_sale_at <= :today OR not_for_sale_at IS NULL) AND
|
16
|
+
(for_sale_at IS NULL AND not_for_sale_at IS NULL) IS NOT TRUE
|
17
|
+
)
|
18
|
+
], today: Date.today
|
14
19
|
}
|
15
20
|
|
16
21
|
# TODO: Get this to use a regular JOIN
|
@@ -1,7 +1,10 @@
|
|
1
|
+
<% if current_products.empty? -%>
|
2
|
+
<h3><%= t 'bodega.empty_cart' %></h3>
|
3
|
+
<% else -%>
|
1
4
|
<%= form_for(current_order, url: root_path) do |form| %>
|
2
5
|
<table id="bodega-cart">
|
3
6
|
<thead>
|
4
|
-
<tr><th class="product-name" colspan="2"><%=
|
7
|
+
<tr><th class="product-name" colspan="2"><%= t 'bodega.product' %></th><th class="price"><%= t 'bodega.price' %></th><th class="total" colspan="2"><%= t 'bodega.total' %></th></tr>
|
5
8
|
</thead>
|
6
9
|
<tbody>
|
7
10
|
<% current_order.order_products.each do |order_product| -%>
|
@@ -15,15 +18,24 @@
|
|
15
18
|
<%= hidden_field_tag 'products[][id]', order_product.product_id %>
|
16
19
|
</td>
|
17
20
|
<td class="price">
|
18
|
-
|
21
|
+
<%= humanized_money_with_symbol order_product.price %>
|
19
22
|
</td>
|
20
23
|
<td class="subtotal">
|
21
|
-
|
24
|
+
<%= humanized_money_with_symbol order_product.subtotal %>
|
25
|
+
</td>
|
26
|
+
<td class="remove">
|
27
|
+
<%= link_to t('bodega.remove'), bodega.remove_path(product_id: order_product.identifier) %>
|
22
28
|
</td>
|
23
29
|
</tr>
|
24
30
|
<% end -%>
|
31
|
+
<tr>
|
32
|
+
<td colspan="3"></td>
|
33
|
+
<td><%= humanized_money_with_symbol current_order.subtotal %></td>
|
34
|
+
<td></td>
|
35
|
+
</tr>
|
25
36
|
</tbody>
|
26
37
|
</table>
|
27
|
-
<%= button_tag '
|
28
|
-
<%= button_tag '
|
38
|
+
<%= button_tag t('bodega.update_cart'), id: 'bodega-update', name: :update, value: 1 %>
|
39
|
+
<%= button_tag t('bodega.checkout'), id: 'bodega-checkout', name: :checkout, value: 1 %>
|
29
40
|
<% end =%>
|
41
|
+
<% end -%>
|
@@ -1 +1,27 @@
|
|
1
|
-
|
1
|
+
<h2>Order #<%= @order.identifier %></h2>
|
2
|
+
<table id="bodega-cart">
|
3
|
+
<thead>
|
4
|
+
<tr><th class="product-name"><%= t 'bodega.product' %></th><th class="price"><%= t 'bodega.price' %></th><th class="total"><%= t 'bodega.total' %></th></tr>
|
5
|
+
</thead>
|
6
|
+
<tbody>
|
7
|
+
<% @order.order_products.each do |order_product| -%>
|
8
|
+
<tr>
|
9
|
+
<td class="product-name">
|
10
|
+
<%= order_product.quantity %> x
|
11
|
+
<%= order_product.name %>
|
12
|
+
</td>
|
13
|
+
<td class="price">
|
14
|
+
<%= humanized_money_with_symbol order_product.price %>
|
15
|
+
</td>
|
16
|
+
<td class="subtotal">
|
17
|
+
<%= humanized_money_with_symbol order_product.subtotal %>
|
18
|
+
</td>
|
19
|
+
</tr>
|
20
|
+
<% end -%>
|
21
|
+
<tr>
|
22
|
+
<td colspan="2"></td>
|
23
|
+
<td><%= humanized_money_with_symbol @order.total %></td>
|
24
|
+
<td></td>
|
25
|
+
</tr>
|
26
|
+
</tbody>
|
27
|
+
</table>
|
data/bodega.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "bodega"
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.3.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Flip Sasser"]
|
12
|
-
s.date = "
|
12
|
+
s.date = "2013-01-07"
|
13
13
|
s.description = "Bodega adds checkout logic to any model in your app!"
|
14
14
|
s.email = "flip@x451.com"
|
15
15
|
s.extra_rdoc_files = [
|
@@ -27,7 +27,6 @@ Gem::Specification.new do |s|
|
|
27
27
|
"app/assets/images/bodega/.gitkeep",
|
28
28
|
"app/assets/javascripts/bodega/application.js",
|
29
29
|
"app/assets/stylesheets/bodega/application.css",
|
30
|
-
"app/controllers/bodega/application_controller.rb",
|
31
30
|
"app/controllers/bodega/orders_controller.rb",
|
32
31
|
"app/helpers/bodega/application_helper.rb",
|
33
32
|
"app/helpers/bodega/cart_helper.rb",
|
@@ -37,12 +36,12 @@ Gem::Specification.new do |s|
|
|
37
36
|
"app/views/bodega/orders/new.html.erb",
|
38
37
|
"app/views/bodega/orders/show.html.erb",
|
39
38
|
"bodega.gemspec",
|
39
|
+
"config/locales/en.yml",
|
40
40
|
"config/routes.rb",
|
41
41
|
"db/migrate/20121111170337_create_bodega_orders.rb",
|
42
42
|
"db/migrate/20121111170420_create_bodega_order_products.rb",
|
43
43
|
"lib/bodega.rb",
|
44
44
|
"lib/bodega/engine.rb",
|
45
|
-
"lib/bodega/monetize.rb",
|
46
45
|
"lib/bodega/payment_method.rb",
|
47
46
|
"lib/bodega/payment_method/base.rb",
|
48
47
|
"lib/bodega/payment_method/paypal.rb",
|
@@ -51,6 +50,7 @@ Gem::Specification.new do |s|
|
|
51
50
|
"lib/generators/bodega/product/USAGE",
|
52
51
|
"lib/generators/bodega/product/product_generator.rb",
|
53
52
|
"lib/generators/bodega/product/templates/migration.rb",
|
53
|
+
"lib/generators/bodega/product/templates/model.rb",
|
54
54
|
"lib/generators/bodega/productize/USAGE",
|
55
55
|
"lib/generators/bodega/productize/productize_generator.rb",
|
56
56
|
"lib/generators/bodega/productize/templates/migration.rb",
|
@@ -67,16 +67,22 @@ Gem::Specification.new do |s|
|
|
67
67
|
s.specification_version = 3
|
68
68
|
|
69
69
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
70
|
-
s.add_runtime_dependency(%q<configurator2>, [">= 0.1.
|
70
|
+
s.add_runtime_dependency(%q<configurator2>, [">= 0.1.2"])
|
71
|
+
s.add_runtime_dependency(%q<i18n>, [">= 0"])
|
72
|
+
s.add_runtime_dependency(%q<money-rails>, [">= 0"])
|
71
73
|
s.add_development_dependency(%q<jeweler>, ["= 1.8.4"])
|
72
74
|
s.add_development_dependency(%q<pry>, [">= 0"])
|
73
75
|
else
|
74
|
-
s.add_dependency(%q<configurator2>, [">= 0.1.
|
76
|
+
s.add_dependency(%q<configurator2>, [">= 0.1.2"])
|
77
|
+
s.add_dependency(%q<i18n>, [">= 0"])
|
78
|
+
s.add_dependency(%q<money-rails>, [">= 0"])
|
75
79
|
s.add_dependency(%q<jeweler>, ["= 1.8.4"])
|
76
80
|
s.add_dependency(%q<pry>, [">= 0"])
|
77
81
|
end
|
78
82
|
else
|
79
|
-
s.add_dependency(%q<configurator2>, [">= 0.1.
|
83
|
+
s.add_dependency(%q<configurator2>, [">= 0.1.2"])
|
84
|
+
s.add_dependency(%q<i18n>, [">= 0"])
|
85
|
+
s.add_dependency(%q<money-rails>, [">= 0"])
|
80
86
|
s.add_dependency(%q<jeweler>, ["= 1.8.4"])
|
81
87
|
s.add_dependency(%q<pry>, [">= 0"])
|
82
88
|
end
|
data/config/routes.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
|
+
require 'money-rails'
|
2
|
+
|
1
3
|
class CreateBodegaOrders < ActiveRecord::Migration
|
2
4
|
def change
|
3
5
|
create_table :bodega_orders do |t|
|
4
6
|
t.belongs_to :customer, polymorphic: true
|
5
7
|
t.string :identifier, limit: 20
|
6
8
|
t.string :payment_id
|
7
|
-
t.
|
8
|
-
t.
|
9
|
-
t.integer :total_in_cents
|
9
|
+
t.money :tax
|
10
|
+
t.money :total
|
10
11
|
t.timestamps
|
11
12
|
end
|
12
13
|
end
|
@@ -4,10 +4,8 @@ class CreateBodegaOrderProducts < ActiveRecord::Migration
|
|
4
4
|
t.belongs_to :order
|
5
5
|
t.belongs_to :product, polymorphic: true
|
6
6
|
t.integer :quantity
|
7
|
-
t.
|
8
|
-
t.
|
9
|
-
t.integer :tax_in_cents
|
10
|
-
t.integer :total_in_cents
|
7
|
+
t.money :price
|
8
|
+
t.money :total
|
11
9
|
end
|
12
10
|
end
|
13
11
|
end
|
data/lib/bodega/engine.rb
CHANGED
@@ -4,16 +4,12 @@ module Bodega
|
|
4
4
|
|
5
5
|
initializer "bodega.hookses" do
|
6
6
|
ActiveSupport.on_load :action_controller do
|
7
|
-
#
|
7
|
+
#helper 'bodega/application'
|
8
|
+
helper 'bodega/cart'
|
8
9
|
include Bodega::CartHelper
|
9
10
|
end
|
10
11
|
|
11
12
|
ActiveSupport.on_load :active_record do
|
12
|
-
require 'bodega/monetize'
|
13
|
-
end
|
14
|
-
|
15
|
-
ActiveSupport.on_load :paypal_express do
|
16
|
-
raise 'w0tf'
|
17
13
|
end
|
18
14
|
end
|
19
15
|
end
|
@@ -16,11 +16,13 @@ module Bodega
|
|
16
16
|
options[:PayerID],
|
17
17
|
request
|
18
18
|
)
|
19
|
+
require 'pry'; binding.pry
|
19
20
|
response.payment_info.last.transaction_id
|
20
21
|
end
|
21
22
|
|
22
23
|
protected
|
23
24
|
def client
|
25
|
+
::Paypal.sandbox! if Bodega.config.test_mode
|
24
26
|
@client ||= ::Paypal::Express::Request.new(
|
25
27
|
username: Bodega.config.paypal.username,
|
26
28
|
password: Bodega.config.paypal.password,
|
@@ -30,11 +32,10 @@ module Bodega
|
|
30
32
|
|
31
33
|
def request
|
32
34
|
@request ||= ::Paypal::Payment::Request.new(
|
33
|
-
amount: order.subtotal,
|
35
|
+
amount: order.subtotal.to_f,
|
34
36
|
description: order.order_products.map(&:quantity_and_name).to_sentence
|
35
37
|
)
|
36
38
|
end
|
37
|
-
|
38
39
|
end
|
39
40
|
end
|
40
41
|
end
|
data/lib/bodega.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
require 'bodega/engine'
|
2
2
|
require 'configurator'
|
3
|
+
require 'i18n'
|
4
|
+
require 'money-rails'
|
3
5
|
|
4
6
|
module Bodega
|
5
7
|
autoload :PaymentMethod, 'bodega/payment_method'
|
6
8
|
|
7
9
|
extend Configurator
|
8
10
|
option :customer_method, :current_user
|
9
|
-
option :product_name, 'product'
|
10
11
|
# Auto-detect payment method. If a user has the Paypal gem installed,
|
11
12
|
# it'll use that. If a user has the Plinq gem installed, it'll use that.
|
12
13
|
# Otherwise, it'll be all, "HEY I NEED A PAYMENT METHOD" when checkout
|
@@ -14,4 +15,8 @@ module Bodega
|
|
14
15
|
option :payment_method, lambda {
|
15
16
|
defined?(::Plinq) ? :plinq : defined?(::Paypal) ? :paypal : raise("No payment method detected. Please set one using `Bodega.config.payment_method=`")
|
16
17
|
}
|
18
|
+
|
19
|
+
# Auto-detect test mode. Defaults to true if running in development or test
|
20
|
+
# mode.
|
21
|
+
option :test_mode, lambda { Rails.env.development? || Rails.env.test? }
|
17
22
|
end
|
@@ -12,7 +12,11 @@ module Bodega
|
|
12
12
|
source_root File.expand_path('../templates', __FILE__)
|
13
13
|
|
14
14
|
def copy_migration
|
15
|
-
migration_template
|
15
|
+
migration_template "migration.rb", "db/migrate/create_#{product_name.tableize}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def copy_model
|
19
|
+
template "model.rb", "app/models/#{product_name.tableize.singularize}"
|
16
20
|
end
|
17
21
|
end
|
18
22
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
class Create<%= product_name.classify.pluralize %> < ActiveRecord::Migration
|
2
2
|
def change
|
3
3
|
create_table :<%= product_name.tableize %> do
|
4
|
-
t.
|
4
|
+
t.money :price
|
5
5
|
t.boolean :for_sale, default: true
|
6
6
|
t.boolean :keep_stock, default: false
|
7
7
|
t.integer :number_in_stock
|
@@ -12,7 +12,7 @@ module Bodega
|
|
12
12
|
source_root File.expand_path('../templates', __FILE__)
|
13
13
|
|
14
14
|
def copy_migration
|
15
|
-
migration_template("migration.rb", "db/migrate/
|
15
|
+
migration_template("migration.rb", "db/migrate/productize_#{product_name.tableize}")
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
@@ -1,6 +1,6 @@
|
|
1
|
-
class
|
1
|
+
class Productize<%= product_name.classify.pluralize %> < ActiveRecord::Migration
|
2
2
|
def change
|
3
|
-
|
3
|
+
add_money :<%= product_name.tableize %>, :price
|
4
4
|
add_column :<%= product_name.tableize %>, :for_sale, :boolean, default: true
|
5
5
|
add_column :<%= product_name.tableize %>, :keep_stock, :boolean, default: false
|
6
6
|
add_column :<%= product_name.tableize %>, :number_in_stock, :integer
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bodega
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-01-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: configurator2
|
@@ -18,7 +18,7 @@ dependencies:
|
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version: 0.1.
|
21
|
+
version: 0.1.2
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -26,7 +26,39 @@ dependencies:
|
|
26
26
|
requirements:
|
27
27
|
- - ! '>='
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: 0.1.
|
29
|
+
version: 0.1.2
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: i18n
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: money-rails
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
30
62
|
- !ruby/object:Gem::Dependency
|
31
63
|
name: jeweler
|
32
64
|
requirement: !ruby/object:Gem::Requirement
|
@@ -77,7 +109,6 @@ files:
|
|
77
109
|
- app/assets/images/bodega/.gitkeep
|
78
110
|
- app/assets/javascripts/bodega/application.js
|
79
111
|
- app/assets/stylesheets/bodega/application.css
|
80
|
-
- app/controllers/bodega/application_controller.rb
|
81
112
|
- app/controllers/bodega/orders_controller.rb
|
82
113
|
- app/helpers/bodega/application_helper.rb
|
83
114
|
- app/helpers/bodega/cart_helper.rb
|
@@ -87,12 +118,12 @@ files:
|
|
87
118
|
- app/views/bodega/orders/new.html.erb
|
88
119
|
- app/views/bodega/orders/show.html.erb
|
89
120
|
- bodega.gemspec
|
121
|
+
- config/locales/en.yml
|
90
122
|
- config/routes.rb
|
91
123
|
- db/migrate/20121111170337_create_bodega_orders.rb
|
92
124
|
- db/migrate/20121111170420_create_bodega_order_products.rb
|
93
125
|
- lib/bodega.rb
|
94
126
|
- lib/bodega/engine.rb
|
95
|
-
- lib/bodega/monetize.rb
|
96
127
|
- lib/bodega/payment_method.rb
|
97
128
|
- lib/bodega/payment_method/base.rb
|
98
129
|
- lib/bodega/payment_method/paypal.rb
|
@@ -101,6 +132,7 @@ files:
|
|
101
132
|
- lib/generators/bodega/product/USAGE
|
102
133
|
- lib/generators/bodega/product/product_generator.rb
|
103
134
|
- lib/generators/bodega/product/templates/migration.rb
|
135
|
+
- lib/generators/bodega/product/templates/model.rb
|
104
136
|
- lib/generators/bodega/productize/USAGE
|
105
137
|
- lib/generators/bodega/productize/productize_generator.rb
|
106
138
|
- lib/generators/bodega/productize/templates/migration.rb
|
@@ -121,7 +153,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
121
153
|
version: '0'
|
122
154
|
segments:
|
123
155
|
- 0
|
124
|
-
hash:
|
156
|
+
hash: 2412431068173395090
|
125
157
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
126
158
|
none: false
|
127
159
|
requirements:
|
data/lib/bodega/monetize.rb
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
module Bodega
|
2
|
-
module Monetize
|
3
|
-
def monetize(attribute)
|
4
|
-
class_eval <<-monetize
|
5
|
-
def #{attribute}=(value)
|
6
|
-
self.#{attribute}_in_cents = (value.to_f * 100).to_i
|
7
|
-
end
|
8
|
-
|
9
|
-
def #{attribute}
|
10
|
-
(#{attribute}_in_cents || 0) / 100.0
|
11
|
-
end
|
12
|
-
monetize
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|