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 CHANGED
@@ -1,6 +1,8 @@
1
- source "http://rubygems.org"
1
+ source "https://rubygems.org"
2
2
 
3
- gem 'configurator2', '>= 0.1.1'
3
+ gem 'configurator2', '>= 0.1.2'
4
+ gem 'i18n'
5
+ gem 'money-rails'
4
6
 
5
7
  group :development, :test do
6
8
  gem 'jeweler', '1.8.4'
data/Gemfile.lock CHANGED
@@ -1,5 +1,5 @@
1
1
  GEM
2
- remote: http://rubygems.org/
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.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.5)
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.9.2.2)
61
+ rake (10.0.3)
56
62
  rdoc (3.12)
57
63
  json (~> 1.4)
58
- rspec-core (2.12.1)
59
- rspec-expectations (2.12.0)
64
+ rspec-core (2.12.2)
65
+ rspec-expectations (2.12.1)
60
66
  diff-lcs (~> 1.1.3)
61
- rspec-mocks (2.12.0)
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.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 with a few simple steps:**
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
- 1. Install Bodega (add `gem 'bodega'` to your Gemfile and bundle)
6
- 2. For existing models:
7
- 1. `rails g bodega:productize existing_class_name`
8
- 2. Add `include Bodega::Product` to your class definition
9
- 3. For new models:
10
- 1. `rails g bodega:product new_class_name`
11
- 4. Add `mount Bodega::Engine => 'cart'` to your `config/routes.rb` file
12
- 5. Profit (literally, for once)
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
- ## WIP
17
+ ## Configuration
15
18
 
16
- This is a work-in-progress and is currently only in use on [womannyc.com](http://www.womannyc.com). Play with it if you want. It's not that exciting; shut up.
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.2.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.payment_id = payment_method.complete!
17
- current_order.save!
18
- redirect_to order_path(current_order)
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::NotFound unless @order = Bodega::Order.where(id: params[:order_id] || params[:id]).first
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(product)
38
- product_id = "#{product[:type]}.#{product[:id]}"
39
- if product[:remove]
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 current = current_products[product_id]
43
- current_quantity = current[:quantity].to_i
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 = product[:quantity] ? product[:quantity] : current_quantity + 1
48
- current_products[product_id] = product.merge(quantity: new_quantity)
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 button_to_cart(product, label = 'Add to Cart')
4
- form_tag(bodega.add_path) do
5
- hidden_field_tag('product[type]', product.class) +
6
- hidden_field_tag('product[id]', product.id) +
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
- #begin
17
+ begin
7
18
  if Bodega.config.customer_method
8
19
  order.customer = send(Bodega.config.customer_method)
9
20
  end
10
- #rescue NoMethodError
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
- #end
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|
@@ -1,19 +1,34 @@
1
1
  module Bodega
2
2
  class Order < ActiveRecord::Base
3
- extend Bodega::Monetize
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 :subtotal
12
- monetize :tax
13
- monetize :total
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 = "#{Time.now.to_i}--#{rand(12)}"
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
- extend Bodega::Monetize
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 :subtotal
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 decorated_product
18
- product.respond_to?(:decorator) ? product.decorator.decorate(product) : product
17
+ def identifier
18
+ "#{product_type}.#{product_id}"
19
19
  end
20
20
 
21
21
  def name
22
- decorated_product.respond_to?(:name) ? decorated_product.name : product.to_s
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
- read_attribute(:subtotal) || price * quantity
30
+ price * quantity
31
31
  end
32
32
 
33
- def total
34
- read_attribute(:total) || subtotal + calculate_tax
33
+ protected
34
+ def calculate_total
35
+ self.total = subtotal
35
36
  end
36
37
 
37
- protected
38
- def calculate_tax
39
- self.tax = 0
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 calculate_total
43
- self.subtotal = price * quantity
44
- self.total = subtotal + calculate_tax
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 :price
8
+ monetize :price_cents
11
9
 
12
10
  scope :for_sale, lambda {
13
- where('for_sale IS TRUE OR ((for_sale_at >= :today OR for_sale_at IS NULL) AND (not_for_sale_at <= :today OR not_for_sale_at IS NULL))', today: Date.today)
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"><%= Bodega.config.product_name.titleize %></th><th class="price">Price</th><th class="total">Total</th></tr>
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
- $<%= order_product.price %>
21
+ <%= humanized_money_with_symbol order_product.price %>
19
22
  </td>
20
23
  <td class="subtotal">
21
- $<%= order_product.subtotal %>
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 'Update Cart', name: :update, value: 1 %>
28
- <%= button_tag 'Checkout', name: :checkout, value: 1 %>
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
- <%= @order.inspect %>
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.2.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 = "2012-12-26"
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.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.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.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
@@ -0,0 +1,3 @@
1
+ en:
2
+ bodega:
3
+ empty_cart: "Your cart is currently empty."
data/config/routes.rb CHANGED
@@ -5,6 +5,7 @@ Bodega::Engine.routes.draw do
5
5
 
6
6
  # Add products to an order
7
7
  post :add, to: 'orders#add'
8
+ get 'remove/:product_id', as: :remove, constraints: {product_id: /.+\.\d+/}, to: 'orders#remove'
8
9
 
9
10
  # Processing orders
10
11
  get :complete, to: 'orders#complete'
@@ -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.integer :subtotal_in_cents
8
- t.integer :tax_in_cents
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.integer :price_in_cents
8
- t.integer :subtotal_in_cents
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
- #require 'bodega/action_controller'
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("migration.rb", "db/migrate/bodegaize_#{product_name.tableize}")
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.integer :price_in_cents
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
@@ -0,0 +1,5 @@
1
+ class <%= product_name.singularize.classify %> < ActiveRecord::Base
2
+ include Bodega::Product
3
+
4
+
5
+ end
@@ -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/bodegaize_#{product_name.tableize}")
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 Bodegaize<%= product_name.classify.pluralize %> < ActiveRecord::Migration
1
+ class Productize<%= product_name.classify.pluralize %> < ActiveRecord::Migration
2
2
  def change
3
- add_column :<%= product_name.tableize %>, :price_in_cents, :integer
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.2.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: 2012-12-26 00:00:00.000000000 Z
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.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.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: 640289259444713481
156
+ hash: 2412431068173395090
125
157
  required_rubygems_version: !ruby/object:Gem::Requirement
126
158
  none: false
127
159
  requirements:
@@ -1,4 +0,0 @@
1
- module Bodega
2
- class ApplicationController < ActionController::Base
3
- end
4
- end
@@ -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