effective_orders 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +856 -0
- data/Rakefile +24 -0
- data/app/assets/images/effective_orders/stripe_connect.png +0 -0
- data/app/assets/javascripts/effective_orders/shipping_address_toggle.js.coffee +30 -0
- data/app/assets/javascripts/effective_orders/stripe_charges.js.coffee +26 -0
- data/app/assets/javascripts/effective_orders/stripe_subscriptions.js.coffee +28 -0
- data/app/assets/javascripts/effective_orders.js +2 -0
- data/app/assets/stylesheets/effective_orders/_order.scss +30 -0
- data/app/assets/stylesheets/effective_orders.css.scss +1 -0
- data/app/controllers/admin/customers_controller.rb +15 -0
- data/app/controllers/admin/orders_controller.rb +22 -0
- data/app/controllers/effective/carts_controller.rb +70 -0
- data/app/controllers/effective/orders_controller.rb +191 -0
- data/app/controllers/effective/providers/moneris.rb +94 -0
- data/app/controllers/effective/providers/paypal.rb +29 -0
- data/app/controllers/effective/providers/stripe.rb +125 -0
- data/app/controllers/effective/providers/stripe_connect.rb +47 -0
- data/app/controllers/effective/subscriptions_controller.rb +123 -0
- data/app/controllers/effective/webhooks_controller.rb +86 -0
- data/app/helpers/effective_carts_helper.rb +90 -0
- data/app/helpers/effective_orders_helper.rb +108 -0
- data/app/helpers/effective_paypal_helper.rb +37 -0
- data/app/helpers/effective_stripe_helper.rb +63 -0
- data/app/mailers/effective/orders_mailer.rb +64 -0
- data/app/models/concerns/acts_as_purchasable.rb +134 -0
- data/app/models/effective/access_denied.rb +17 -0
- data/app/models/effective/cart.rb +65 -0
- data/app/models/effective/cart_item.rb +40 -0
- data/app/models/effective/customer.rb +61 -0
- data/app/models/effective/datatables/customers.rb +45 -0
- data/app/models/effective/datatables/orders.rb +53 -0
- data/app/models/effective/order.rb +247 -0
- data/app/models/effective/order_item.rb +69 -0
- data/app/models/effective/stripe_charge.rb +35 -0
- data/app/models/effective/subscription.rb +95 -0
- data/app/models/inputs/price_field.rb +63 -0
- data/app/models/inputs/price_form_input.rb +7 -0
- data/app/models/inputs/price_formtastic_input.rb +9 -0
- data/app/models/inputs/price_input.rb +19 -0
- data/app/models/inputs/price_simple_form_input.rb +8 -0
- data/app/models/validators/effective/sold_out_validator.rb +7 -0
- data/app/views/active_admin/effective_orders/orders/_show.html.haml +70 -0
- data/app/views/admin/customers/_actions.html.haml +2 -0
- data/app/views/admin/customers/index.html.haml +10 -0
- data/app/views/admin/orders/index.html.haml +7 -0
- data/app/views/admin/orders/show.html.haml +11 -0
- data/app/views/effective/carts/_cart.html.haml +33 -0
- data/app/views/effective/carts/show.html.haml +18 -0
- data/app/views/effective/orders/_checkout_step_1.html.haml +39 -0
- data/app/views/effective/orders/_checkout_step_2.html.haml +18 -0
- data/app/views/effective/orders/_my_purchases.html.haml +15 -0
- data/app/views/effective/orders/_order.html.haml +4 -0
- data/app/views/effective/orders/_order_header.html.haml +21 -0
- data/app/views/effective/orders/_order_items.html.haml +39 -0
- data/app/views/effective/orders/_order_payment_details.html.haml +11 -0
- data/app/views/effective/orders/_order_shipping.html.haml +19 -0
- data/app/views/effective/orders/_order_user_fields.html.haml +10 -0
- data/app/views/effective/orders/checkout.html.haml +3 -0
- data/app/views/effective/orders/declined.html.haml +10 -0
- data/app/views/effective/orders/moneris/_form.html.haml +34 -0
- data/app/views/effective/orders/my_purchases.html.haml +6 -0
- data/app/views/effective/orders/my_sales.html.haml +28 -0
- data/app/views/effective/orders/new.html.haml +4 -0
- data/app/views/effective/orders/paypal/_form.html.haml +5 -0
- data/app/views/effective/orders/purchased.html.haml +10 -0
- data/app/views/effective/orders/show.html.haml +17 -0
- data/app/views/effective/orders/stripe/_form.html.haml +8 -0
- data/app/views/effective/orders/stripe/_subscription_fields.html.haml +7 -0
- data/app/views/effective/orders_mailer/order_receipt_to_admin.html.haml +8 -0
- data/app/views/effective/orders_mailer/order_receipt_to_buyer.html.haml +8 -0
- data/app/views/effective/orders_mailer/order_receipt_to_seller.html.haml +30 -0
- data/app/views/effective/subscriptions/index.html.haml +16 -0
- data/app/views/effective/subscriptions/new.html.haml +10 -0
- data/app/views/effective/subscriptions/show.html.haml +49 -0
- data/config/routes.rb +57 -0
- data/db/migrate/01_create_effective_orders.rb.erb +91 -0
- data/db/upgrade/02_upgrade_effective_orders_from03x.rb.erb +29 -0
- data/db/upgrade/upgrade_price_column_on_table.rb.erb +17 -0
- data/lib/effective_orders/engine.rb +52 -0
- data/lib/effective_orders/version.rb +3 -0
- data/lib/effective_orders.rb +76 -0
- data/lib/generators/effective_orders/install_generator.rb +38 -0
- data/lib/generators/effective_orders/upgrade_from03x_generator.rb +34 -0
- data/lib/generators/effective_orders/upgrade_price_column_generator.rb +34 -0
- data/lib/generators/templates/README +1 -0
- data/lib/generators/templates/effective_orders.rb +210 -0
- data/spec/controllers/carts_controller_spec.rb +143 -0
- data/spec/controllers/moneris_orders_controller_spec.rb +245 -0
- data/spec/controllers/orders_controller_spec.rb +418 -0
- data/spec/controllers/stripe_orders_controller_spec.rb +127 -0
- data/spec/controllers/webhooks_controller_spec.rb +79 -0
- data/spec/dummy/README.rdoc +8 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/product.rb +17 -0
- data/spec/dummy/app/models/product_with_float_price.rb +17 -0
- data/spec/dummy/app/models/user.rb +28 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config/application.rb +31 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +83 -0
- data/spec/dummy/config/environments/test.rb +39 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/devise.rb +254 -0
- data/spec/dummy/config/initializers/effective_addresses.rb +15 -0
- data/spec/dummy/config/initializers/effective_orders.rb +22 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/schema.rb +142 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +487 -0
- data/spec/dummy/log/test.log +347 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/helpers/effective_orders_helper_spec.rb +21 -0
- data/spec/models/acts_as_purchasable_spec.rb +107 -0
- data/spec/models/customer_spec.rb +71 -0
- data/spec/models/factories_spec.rb +13 -0
- data/spec/models/order_item_spec.rb +35 -0
- data/spec/models/order_spec.rb +323 -0
- data/spec/models/stripe_charge_spec.rb +39 -0
- data/spec/models/subscription_spec.rb +103 -0
- data/spec/spec_helper.rb +44 -0
- data/spec/support/factories.rb +118 -0
- metadata +387 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class Order < ActiveRecord::Base
|
|
3
|
+
self.table_name = EffectiveOrders.orders_table_name.to_s
|
|
4
|
+
|
|
5
|
+
if EffectiveOrders.obfuscate_order_ids
|
|
6
|
+
acts_as_obfuscated :format => '###-####-###'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
acts_as_addressable :billing => EffectiveOrders.require_billing_address, :shipping => EffectiveOrders.require_shipping_address
|
|
10
|
+
attr_accessor :save_billing_address, :save_shipping_address, :shipping_address_same_as_billing # save these addresses to the user if selected
|
|
11
|
+
|
|
12
|
+
belongs_to :user # This is the user who purchased the order
|
|
13
|
+
has_many :order_items, :inverse_of => :order
|
|
14
|
+
|
|
15
|
+
structure do
|
|
16
|
+
payment :text # serialized hash, see below
|
|
17
|
+
purchase_state :string, :validates => [:inclusion => {:in => [nil, EffectiveOrders::PURCHASED, EffectiveOrders::DECLINED]}]
|
|
18
|
+
purchased_at :datetime, :validates => [:presence => {:if => Proc.new { |order| order.purchase_state == EffectiveOrders::PURCHASED}}]
|
|
19
|
+
|
|
20
|
+
timestamps
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
accepts_nested_attributes_for :order_items, :allow_destroy => false, :reject_if => :all_blank
|
|
24
|
+
accepts_nested_attributes_for :user, :allow_destroy => false, :update_only => true
|
|
25
|
+
|
|
26
|
+
unless EffectiveOrders.skip_user_validation
|
|
27
|
+
validates_presence_of :user_id
|
|
28
|
+
validates_associated :user
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if ((minimum_charge = EffectiveOrders.minimum_charge.to_i) rescue nil).present?
|
|
32
|
+
if EffectiveOrders.allow_free_orders
|
|
33
|
+
validates_numericality_of :total, :greater_than_or_equal_to => minimum_charge, :unless => Proc.new { |order| order.total == 0 }, :message => "A minimum order of #{EffectiveOrders.minimum_charge} is required. Please add additional items to your cart."
|
|
34
|
+
else
|
|
35
|
+
validates_numericality_of :total, :greater_than_or_equal_to => minimum_charge, :message => "A minimum order of #{EffectiveOrders.minimum_charge} is required. Please add additional items to your cart."
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
validates_presence_of :order_items, :message => 'No items are present. Please add one or more item to your cart.'
|
|
40
|
+
validates_associated :order_items
|
|
41
|
+
|
|
42
|
+
serialize :payment, Hash
|
|
43
|
+
|
|
44
|
+
default_scope -> { includes(:user).includes(:order_items => :purchasable).order('created_at DESC') }
|
|
45
|
+
|
|
46
|
+
scope :purchased, -> { where(:purchase_state => EffectiveOrders::PURCHASED) }
|
|
47
|
+
scope :purchased_by, lambda { |user| purchased.where(:user_id => user.try(:id)) }
|
|
48
|
+
scope :declined, -> { where(:purchase_state => EffectiveOrders::DECLINED) }
|
|
49
|
+
|
|
50
|
+
def initialize(cart = {}, user = nil)
|
|
51
|
+
super() # Call super with no arguments
|
|
52
|
+
|
|
53
|
+
# Set up defaults
|
|
54
|
+
self.save_billing_address = true
|
|
55
|
+
self.save_shipping_address = true
|
|
56
|
+
self.shipping_address_same_as_billing = true
|
|
57
|
+
|
|
58
|
+
add_to_order(cart) if cart.present?
|
|
59
|
+
|
|
60
|
+
self.user = user if user.present?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def add(item, quantity = 1)
|
|
64
|
+
raise 'unable to alter a purchased order' if purchased?
|
|
65
|
+
raise 'unable to alter a declined order' if declined?
|
|
66
|
+
|
|
67
|
+
if item.kind_of?(Effective::Cart)
|
|
68
|
+
cart_items = item.cart_items
|
|
69
|
+
else
|
|
70
|
+
purchasables = [item].flatten
|
|
71
|
+
|
|
72
|
+
if purchasables.any? { |p| !p.respond_to?(:is_effectively_purchasable?) }
|
|
73
|
+
raise ArgumentError.new('Effective::Order.add() expects a single acts_as_purchasable item, or an array of acts_as_purchasable items')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
cart_items = purchasables.map do |purchasable|
|
|
77
|
+
CartItem.new(:quantity => quantity).tap { |cart_item| cart_item.purchasable = purchasable }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
retval = cart_items.map do |item|
|
|
82
|
+
order_items.build(
|
|
83
|
+
:title => item.title,
|
|
84
|
+
:quantity => item.quantity,
|
|
85
|
+
:price => item.price,
|
|
86
|
+
:tax_exempt => item.tax_exempt,
|
|
87
|
+
:tax_rate => item.tax_rate,
|
|
88
|
+
:seller_id => (item.purchasable.try(:seller).try(:id) rescue nil)
|
|
89
|
+
).tap { |order_item| order_item.purchasable = item.purchasable }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
retval.size == 1 ? retval.first : retval
|
|
93
|
+
end
|
|
94
|
+
alias_method :add_to_order, :add
|
|
95
|
+
|
|
96
|
+
def user=(user)
|
|
97
|
+
super
|
|
98
|
+
|
|
99
|
+
self.billing_address = user.billing_address if user.respond_to?(:billing_address)
|
|
100
|
+
self.shipping_address = user.shipping_address if user.respond_to?(:shipping_address)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# This is used for updating Subscription codes.
|
|
104
|
+
# We want to update the underlying purchasable object of an OrderItem
|
|
105
|
+
# Passing the order_item_attributes using rails default acts_as_nested creates a new object instead of updating the temporary one.
|
|
106
|
+
# So we override this method to do the updates on the non-persisted OrderItem objects
|
|
107
|
+
# Right now strong_paramaters only lets through stripe_coupon_id
|
|
108
|
+
# {"0"=>{"class"=>"Effective::Subscription", "stripe_coupon_id"=>"50OFF", "id"=>"2"}}}
|
|
109
|
+
def order_items_attributes=(order_item_attributes)
|
|
110
|
+
if self.persisted? == false
|
|
111
|
+
(order_item_attributes || {}).each do |_, atts|
|
|
112
|
+
order_item = self.order_items.find { |oi| oi.purchasable.class.name == atts[:class] && oi.purchasable.id == atts[:id].to_i }
|
|
113
|
+
|
|
114
|
+
if order_item
|
|
115
|
+
order_item.purchasable.attributes = atts.except(:id, :class)
|
|
116
|
+
|
|
117
|
+
# Recalculate the OrderItem based on the updated purchasable object
|
|
118
|
+
order_item.title = order_item.purchasable.title
|
|
119
|
+
order_item.price = order_item.purchasable.price
|
|
120
|
+
order_item.tax_exempt = order_item.purchasable.tax_exempt
|
|
121
|
+
order_item.tax_rate = order_item.purchasable.tax_rate
|
|
122
|
+
order_item.seller_id = (order_item.purchasable.try(:seller).try(:id) rescue nil)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def total
|
|
129
|
+
[order_items.map(&:total).sum, 0].max
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def subtotal
|
|
133
|
+
order_items.map(&:subtotal).sum
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def tax
|
|
137
|
+
[order_items.map(&:tax).sum, 0].max
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def num_items
|
|
141
|
+
order_items.map(&:quantity).sum
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def save_billing_address?
|
|
145
|
+
::ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(self.save_billing_address)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def save_shipping_address?
|
|
149
|
+
::ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(self.save_shipping_address)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def shipping_address_same_as_billing?
|
|
153
|
+
if self.shipping_address_same_as_billing.nil?
|
|
154
|
+
true # Default value
|
|
155
|
+
else
|
|
156
|
+
::ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(self.shipping_address_same_as_billing)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# :validate => false, :email => false
|
|
161
|
+
def purchase!(payment_details = nil, opts = {})
|
|
162
|
+
opts = {:validate => true, :email => true}.merge(opts)
|
|
163
|
+
|
|
164
|
+
raise EffectiveOrders::AlreadyPurchasedException.new('order already purchased') if self.purchased?
|
|
165
|
+
raise EffectiveOrders::AlreadyDeclinedException.new('order already declined') if (self.declined? && opts[:validate])
|
|
166
|
+
|
|
167
|
+
Order.transaction do
|
|
168
|
+
self.purchase_state = EffectiveOrders::PURCHASED
|
|
169
|
+
self.purchased_at ||= Time.zone.now
|
|
170
|
+
self.payment = payment_details.kind_of?(Hash) ? payment_details : {:details => (payment_details || 'none').to_s}
|
|
171
|
+
|
|
172
|
+
order_items.each { |item| item.purchasable.purchased!(self, item) }
|
|
173
|
+
|
|
174
|
+
self.save!(:validate => opts[:validate])
|
|
175
|
+
|
|
176
|
+
if EffectiveOrders.mailer[:send_order_receipt_to_admin] && opts[:email]
|
|
177
|
+
if Rails.env.production?
|
|
178
|
+
(OrdersMailer.order_receipt_to_admin(self).deliver rescue false)
|
|
179
|
+
else
|
|
180
|
+
OrdersMailer.order_receipt_to_admin(self).deliver
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
if EffectiveOrders.mailer[:send_order_receipt_to_buyer] && opts[:email]
|
|
185
|
+
if Rails.env.production?
|
|
186
|
+
(OrdersMailer.order_receipt_to_buyer(self).deliver rescue false)
|
|
187
|
+
else
|
|
188
|
+
OrdersMailer.order_receipt_to_buyer(self).deliver
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
if EffectiveOrders.mailer[:send_order_receipt_to_seller] && self.purchased?(:stripe_connect) && opts[:email]
|
|
193
|
+
self.order_items.group_by(&:seller).each do |seller, order_items|
|
|
194
|
+
if Rails.env.production?
|
|
195
|
+
(OrdersMailer.order_receipt_to_seller(self, seller, order_items).deliver rescue false)
|
|
196
|
+
else
|
|
197
|
+
OrdersMailer.order_receipt_to_seller(self, seller, order_items).deliver
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
return true
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
false
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def decline!(payment_details = nil)
|
|
209
|
+
raise EffectiveOrders::AlreadyPurchasedException.new('order already purchased') if self.purchased?
|
|
210
|
+
raise EffectiveOrders::AlreadyDeclinedException.new('order already declined') if self.declined?
|
|
211
|
+
|
|
212
|
+
Order.transaction do
|
|
213
|
+
self.purchase_state = EffectiveOrders::DECLINED
|
|
214
|
+
self.payment = payment_details.kind_of?(Hash) ? payment_details : {:details => (payment_details || 'none').to_s}
|
|
215
|
+
|
|
216
|
+
order_items.each { |item| item.purchasable.declined!(self, item) }
|
|
217
|
+
|
|
218
|
+
self.save!
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def purchased?(provider = nil)
|
|
223
|
+
return false if (purchase_state != EffectiveOrders::PURCHASED)
|
|
224
|
+
return true if provider == nil
|
|
225
|
+
|
|
226
|
+
begin
|
|
227
|
+
case provider
|
|
228
|
+
when :stripe_connect
|
|
229
|
+
payment.keys.first.kind_of?(Numeric) && payment[payment.keys.first].key?('object') && payment[payment.keys.first]['object'] == 'charge'
|
|
230
|
+
when :stripe
|
|
231
|
+
payment.key?('object') && payment['object'] == 'charge'
|
|
232
|
+
when :moneris
|
|
233
|
+
payment.key?('response_code') && payment.key?('transactionKey')
|
|
234
|
+
when :paypal
|
|
235
|
+
else
|
|
236
|
+
false
|
|
237
|
+
end
|
|
238
|
+
rescue => e
|
|
239
|
+
false
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def declined?
|
|
244
|
+
purchase_state == EffectiveOrders::DECLINED
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class OrderItem < ActiveRecord::Base
|
|
3
|
+
self.table_name = EffectiveOrders.order_items_table_name.to_s
|
|
4
|
+
|
|
5
|
+
belongs_to :order
|
|
6
|
+
belongs_to :purchasable, :polymorphic => true
|
|
7
|
+
belongs_to :seller, :class_name => 'User'
|
|
8
|
+
|
|
9
|
+
structure do
|
|
10
|
+
title :string, :validates => [:presence]
|
|
11
|
+
quantity :integer, :validates => [:presence, :numericality]
|
|
12
|
+
price :integer, :default => 0, :validates => [:numericality]
|
|
13
|
+
tax_exempt :boolean, :validates => [:inclusion => {:in => [true, false]}]
|
|
14
|
+
tax_rate :decimal, :precision => 5, :scale => 3, :default => 0.000, :validates => [:presence]
|
|
15
|
+
|
|
16
|
+
timestamps
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
validates_associated :purchasable
|
|
20
|
+
validates_presence_of :purchasable
|
|
21
|
+
accepts_nested_attributes_for :purchasable, :allow_destroy => false, :reject_if => :all_blank, :update_only => true
|
|
22
|
+
|
|
23
|
+
validates_presence_of :seller_id, :if => Proc.new { |order_item| EffectiveOrders.stripe_connect_enabled }
|
|
24
|
+
|
|
25
|
+
delegate :purchased_download_url, :to => :purchasable
|
|
26
|
+
delegate :purchased?, :declined?, :to => :order
|
|
27
|
+
|
|
28
|
+
scope :sold, -> { joins(:order).where(:orders => {:purchase_state => EffectiveOrders::PURCHASED}) }
|
|
29
|
+
scope :sold_by, lambda { |user| sold().where(:seller_id => user.try(:id)) }
|
|
30
|
+
|
|
31
|
+
def subtotal
|
|
32
|
+
price * quantity
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def tax # This is the total tax, for 3 items if quantity is 3
|
|
36
|
+
tax_exempt ? 0 : (subtotal * tax_rate).ceil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def total
|
|
40
|
+
subtotal + tax
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def price=(value)
|
|
44
|
+
if value.kind_of?(Integer)
|
|
45
|
+
super
|
|
46
|
+
elsif value.kind_of?(String) && !value.include?('.') # Looks like an integer
|
|
47
|
+
super
|
|
48
|
+
else # Could be Float, BigDecimal, or String like 9.99
|
|
49
|
+
ActiveSupport::Deprecation.warn('order_item.price= was passed a non-integer. Expecting an Integer representing the number of cents. Continuing with (price * 100.0).floor conversion') unless EffectiveOrders.silence_deprecation_warnings
|
|
50
|
+
super((value.to_f * 100.0).to_i)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# This is going to return an Effective::Customer object that matches the purchasable.user
|
|
55
|
+
# And is the Customer representing who is selling the product
|
|
56
|
+
# This is really only used for StripeConnect
|
|
57
|
+
def seller
|
|
58
|
+
@seller ||= Effective::Customer.for_user(purchasable.try(:seller))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def stripe_connect_application_fee
|
|
62
|
+
@stripe_connect_application_fee ||= (
|
|
63
|
+
self.instance_exec(self, &EffectiveOrders.stripe_connect_application_fee_method).to_i.tap do |fee|
|
|
64
|
+
raise ArgumentError.new("expected EffectiveOrders.stripe_connect_application_fee_method to return a value between 0 and the order_item total (#{self.total}). Received #{fee}.") if (fee > total || fee < 0)
|
|
65
|
+
end
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# This is an object for the stripe charge form
|
|
2
|
+
module Effective
|
|
3
|
+
class StripeCharge
|
|
4
|
+
extend ActiveModel::Naming
|
|
5
|
+
include ActiveModel::Conversion
|
|
6
|
+
include ActiveModel::Validations
|
|
7
|
+
include ActiveRecord::Reflection
|
|
8
|
+
|
|
9
|
+
attr_accessor :effective_order_id, :order, :token # For our form
|
|
10
|
+
|
|
11
|
+
validates_presence_of :effective_order_id, :token
|
|
12
|
+
|
|
13
|
+
def initialize(params = {})
|
|
14
|
+
if params.kind_of?(Effective::Order)
|
|
15
|
+
@order = params
|
|
16
|
+
@effective_order_id = params.to_param
|
|
17
|
+
else
|
|
18
|
+
params.each { |k, v| self.send("#{k}=", v) if self.respond_to?("#{k}=") }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def persisted?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def order_items
|
|
27
|
+
order.order_items.reject { |order_item| order_item.purchasable.kind_of?(Effective::Subscription) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def subscriptions
|
|
31
|
+
order.order_items.select { |order_item| order_item.purchasable.kind_of?(Effective::Subscription) }.map(&:purchasable)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class Subscription < ActiveRecord::Base
|
|
3
|
+
include EffectiveStripeHelper
|
|
4
|
+
|
|
5
|
+
self.table_name = EffectiveOrders.subscriptions_table_name.to_s
|
|
6
|
+
|
|
7
|
+
acts_as_purchasable
|
|
8
|
+
|
|
9
|
+
belongs_to :customer
|
|
10
|
+
|
|
11
|
+
structure do
|
|
12
|
+
stripe_plan_id :string, :validates => [:presence] # This will be 'Weekly' or something like that
|
|
13
|
+
stripe_subscription_id :string
|
|
14
|
+
stripe_coupon_id :string
|
|
15
|
+
|
|
16
|
+
title :string, :validates => [:presence]
|
|
17
|
+
price :integer, :default => 0, :validates => [:numericality => {:greater_than => 0}]
|
|
18
|
+
|
|
19
|
+
timestamps
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
delegate :user, :user_id, :to => :customer
|
|
23
|
+
|
|
24
|
+
validates_presence_of :customer
|
|
25
|
+
validates_uniqueness_of :customer_id, :scope => [:stripe_plan_id] # Can only be on each plan once.
|
|
26
|
+
|
|
27
|
+
before_validation do
|
|
28
|
+
self.errors.add(:stripe_plan_id, "is an invalid Plan") if stripe_plan_id.present? && stripe_plan.blank?
|
|
29
|
+
self.errors.add(:stripe_coupon_id, "is an invalid Coupon") if stripe_coupon_id.present? && stripe_coupon.blank?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def tax_exempt
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def stripe_plan_id=(plan_id)
|
|
37
|
+
unless self[:stripe_plan_id] == plan_id
|
|
38
|
+
self[:stripe_plan_id] = plan_id
|
|
39
|
+
@stripe_plan = nil # Remove any memoization
|
|
40
|
+
|
|
41
|
+
assign_price_and_title()
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def stripe_coupon_id=(coupon_id)
|
|
46
|
+
unless self[:stripe_coupon_id] == coupon_id
|
|
47
|
+
self[:stripe_coupon_id] = coupon_id
|
|
48
|
+
@stripe_coupon = nil # Remove any memoization
|
|
49
|
+
|
|
50
|
+
assign_price_and_title()
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def stripe_plan
|
|
55
|
+
if stripe_plan_id.present?
|
|
56
|
+
@stripe_plan ||= (Stripe::Plan.retrieve(stripe_plan_id) rescue nil)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def stripe_coupon
|
|
61
|
+
if stripe_coupon_id.present?
|
|
62
|
+
@stripe_coupon ||= (Stripe::Coupon.retrieve(stripe_coupon_id) rescue nil)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def stripe_subscription
|
|
67
|
+
if stripe_subscription_id.present?
|
|
68
|
+
@stripe_subscription ||= (customer.stripe_customer.subscriptions.retrieve(stripe_subscription_id) rescue nil)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def assign_price_and_title
|
|
75
|
+
if stripe_plan
|
|
76
|
+
if stripe_coupon
|
|
77
|
+
self.price = price_with_coupon(stripe_plan.amount, stripe_coupon)
|
|
78
|
+
self.title = stripe_plan_description(stripe_plan) + '<br>Coupon Code: ' + stripe_coupon_description(stripe_coupon)
|
|
79
|
+
else
|
|
80
|
+
self.title = stripe_plan_description(stripe_plan)
|
|
81
|
+
self.price = stripe_plan.amount
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def price_with_coupon(amount, coupon)
|
|
87
|
+
if coupon.percent_off.present?
|
|
88
|
+
(amount * (coupon.percent_off.to_i / 100.0)).floor
|
|
89
|
+
else
|
|
90
|
+
[0, amount - coupon.amount_off].max
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Inputs
|
|
2
|
+
class PriceField
|
|
3
|
+
delegate :content_tag, :text_field_tag, :hidden_field_tag, :to => :@template
|
|
4
|
+
|
|
5
|
+
def initialize(object, object_name, template, method, opts)
|
|
6
|
+
@object = object
|
|
7
|
+
@object_name = object_name
|
|
8
|
+
@template = template
|
|
9
|
+
@method = method
|
|
10
|
+
@opts = opts
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_html
|
|
14
|
+
content_tag(:div, :class => 'input-group') do
|
|
15
|
+
content_tag(:span, '$', :class => 'input-group-addon') +
|
|
16
|
+
text_field_tag(field_name, value, options) +
|
|
17
|
+
hidden_field_tag(field_name, price, :id => price_field)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# These two are for the text field
|
|
24
|
+
|
|
25
|
+
def field_name
|
|
26
|
+
@object_name + "[#{@method}]"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def value
|
|
30
|
+
val = @object.send(@method) || 0
|
|
31
|
+
val.kind_of?(Integer) ? ('%.2f' % (val / 100.0)) : ('%.2f' % val)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# These two are for the hidden input
|
|
35
|
+
def price_field
|
|
36
|
+
"#{field_name.parameterize.gsub('-', '_')}_value_as_integer"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def price
|
|
40
|
+
val = @object.send(@method) || 0
|
|
41
|
+
val.kind_of?(Integer) ? val : (val * 100.0).to_i
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def options
|
|
45
|
+
(@opts || {}).tap do |options|
|
|
46
|
+
if options[:class].blank?
|
|
47
|
+
options[:class] = 'numeric'
|
|
48
|
+
elsif options[:class].kind_of?(Array)
|
|
49
|
+
options[:class] << :numeric
|
|
50
|
+
elsif options[:class].kind_of?(String)
|
|
51
|
+
options[:class] << ' numeric'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
options[:pattern] = "[0-9]+(\\.[0-9][0-9]){1}"
|
|
55
|
+
options[:maxlength] = 14
|
|
56
|
+
options[:title] = 'a price like 19.99, 10000.99 or 0.00'
|
|
57
|
+
options[:onchange] = "console.log(this.value);"
|
|
58
|
+
options[:onchange] = "document.querySelectorAll('##{price_field}')[0].setAttribute('value', ((parseFloat(this.value) || 0.00) * 100.0).toFixed(0));"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# This allows the app to call f.input :something, :as => :price
|
|
2
|
+
# in either Formtastic or SimpleForm, but not both at the same time
|
|
3
|
+
|
|
4
|
+
if defined?(SimpleForm)
|
|
5
|
+
class PriceInput < SimpleForm::Inputs::NumericInput
|
|
6
|
+
def input(wrapper_options = nil)
|
|
7
|
+
options = merge_wrapper_options(input_html_options, wrapper_options)
|
|
8
|
+
Inputs::PriceField.new(object, object_name, template, attribute_name, options).to_html
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
elsif defined?(Formtastic)
|
|
12
|
+
class PriceInput < Formtastic::Inputs::NumberInput
|
|
13
|
+
def to_html
|
|
14
|
+
input_wrapping do
|
|
15
|
+
label_html << Inputs::PriceField.new(@object, @object_name, @template, @method, @options).to_html
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
if defined?(SimpleForm)
|
|
2
|
+
class PriceSimpleFormInput < SimpleForm::Inputs::NumericInput
|
|
3
|
+
def input(wrapper_options = nil)
|
|
4
|
+
options = merge_wrapper_options(input_html_options, wrapper_options)
|
|
5
|
+
Inputs::PriceField.new(object, object_name, template, attribute_name, options).to_html
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
.panel
|
|
2
|
+
%h3= "Order ##{order.to_param} Details"
|
|
3
|
+
.panel_contents
|
|
4
|
+
.attributes_table
|
|
5
|
+
%table
|
|
6
|
+
%tbody
|
|
7
|
+
%tr
|
|
8
|
+
%th order
|
|
9
|
+
%td= "##{order.to_param}"
|
|
10
|
+
%tr
|
|
11
|
+
%th buyer
|
|
12
|
+
%td= link_to order.user, admin_user_path(order.user)
|
|
13
|
+
%tr
|
|
14
|
+
%th purchase state
|
|
15
|
+
%td= order.purchase_state
|
|
16
|
+
- if order.purchased?
|
|
17
|
+
%tr
|
|
18
|
+
%th purchased
|
|
19
|
+
%td= order.purchased_at.strftime("%Y-%m-%d %H:%M")
|
|
20
|
+
- if order.billing_address.present?
|
|
21
|
+
%tr
|
|
22
|
+
%th billing address
|
|
23
|
+
%td= order.billing_address.to_html
|
|
24
|
+
- if order.shipping_address.present?
|
|
25
|
+
%tr
|
|
26
|
+
%th shipping address
|
|
27
|
+
%td= order.shipping_address.to_html
|
|
28
|
+
|
|
29
|
+
.panel
|
|
30
|
+
%h3 Order Items
|
|
31
|
+
.panel_contents
|
|
32
|
+
.attributes_table
|
|
33
|
+
- has_seller = order.purchased?(:stripe_connect)
|
|
34
|
+
%table.table
|
|
35
|
+
%thead
|
|
36
|
+
%tr
|
|
37
|
+
%th Items
|
|
38
|
+
%th Price
|
|
39
|
+
- if has_seller
|
|
40
|
+
%th Seller
|
|
41
|
+
%tbody
|
|
42
|
+
- order.order_items.each do |item|
|
|
43
|
+
%tr
|
|
44
|
+
%td
|
|
45
|
+
- if item.quantity > 1
|
|
46
|
+
= "#{item.quantity}x "
|
|
47
|
+
= item.title
|
|
48
|
+
%td= price_to_currency(item.subtotal)
|
|
49
|
+
- if has_seller
|
|
50
|
+
%td= link_to item.seller.user, admin_user_path(item.seller.user)
|
|
51
|
+
%tfoot
|
|
52
|
+
%tr
|
|
53
|
+
%th Subtotal
|
|
54
|
+
%td= price_to_currency(order.subtotal)
|
|
55
|
+
%tr
|
|
56
|
+
%th Tax
|
|
57
|
+
%td= price_to_currency(order.tax)
|
|
58
|
+
%tr
|
|
59
|
+
%th Total
|
|
60
|
+
%td= price_to_currency(order.total)
|
|
61
|
+
|
|
62
|
+
- if order.payment.present?
|
|
63
|
+
.panel
|
|
64
|
+
%h3 Payment
|
|
65
|
+
.panel_contents
|
|
66
|
+
.attributes_table
|
|
67
|
+
= order_payment_to_html(order) rescue order.payment
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
= render :partial => 'effective/orders/order', :locals => {:order => @order}
|
|
2
|
+
%hr
|
|
3
|
+
= render :partial => 'effective/orders/order_payment_details', :locals => {:order => @order}
|
|
4
|
+
|
|
5
|
+
%hr
|
|
6
|
+
= link_to 'Back', effective_orders.admin_orders_path, :class => 'btn btn-primary'
|
|
7
|
+
- if @order.purchased?
|
|
8
|
+
= '-'
|
|
9
|
+
= link_to 'Resend Receipt', effective_orders.resend_buyer_receipt_path(@order), 'data-confirm' => 'This action will email the buyer a copy of the original email receipt. Send receipt now?', :class => 'btn btn-default'
|
|
10
|
+
|
|
11
|
+
|