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.
Files changed (146) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +856 -0
  4. data/Rakefile +24 -0
  5. data/app/assets/images/effective_orders/stripe_connect.png +0 -0
  6. data/app/assets/javascripts/effective_orders/shipping_address_toggle.js.coffee +30 -0
  7. data/app/assets/javascripts/effective_orders/stripe_charges.js.coffee +26 -0
  8. data/app/assets/javascripts/effective_orders/stripe_subscriptions.js.coffee +28 -0
  9. data/app/assets/javascripts/effective_orders.js +2 -0
  10. data/app/assets/stylesheets/effective_orders/_order.scss +30 -0
  11. data/app/assets/stylesheets/effective_orders.css.scss +1 -0
  12. data/app/controllers/admin/customers_controller.rb +15 -0
  13. data/app/controllers/admin/orders_controller.rb +22 -0
  14. data/app/controllers/effective/carts_controller.rb +70 -0
  15. data/app/controllers/effective/orders_controller.rb +191 -0
  16. data/app/controllers/effective/providers/moneris.rb +94 -0
  17. data/app/controllers/effective/providers/paypal.rb +29 -0
  18. data/app/controllers/effective/providers/stripe.rb +125 -0
  19. data/app/controllers/effective/providers/stripe_connect.rb +47 -0
  20. data/app/controllers/effective/subscriptions_controller.rb +123 -0
  21. data/app/controllers/effective/webhooks_controller.rb +86 -0
  22. data/app/helpers/effective_carts_helper.rb +90 -0
  23. data/app/helpers/effective_orders_helper.rb +108 -0
  24. data/app/helpers/effective_paypal_helper.rb +37 -0
  25. data/app/helpers/effective_stripe_helper.rb +63 -0
  26. data/app/mailers/effective/orders_mailer.rb +64 -0
  27. data/app/models/concerns/acts_as_purchasable.rb +134 -0
  28. data/app/models/effective/access_denied.rb +17 -0
  29. data/app/models/effective/cart.rb +65 -0
  30. data/app/models/effective/cart_item.rb +40 -0
  31. data/app/models/effective/customer.rb +61 -0
  32. data/app/models/effective/datatables/customers.rb +45 -0
  33. data/app/models/effective/datatables/orders.rb +53 -0
  34. data/app/models/effective/order.rb +247 -0
  35. data/app/models/effective/order_item.rb +69 -0
  36. data/app/models/effective/stripe_charge.rb +35 -0
  37. data/app/models/effective/subscription.rb +95 -0
  38. data/app/models/inputs/price_field.rb +63 -0
  39. data/app/models/inputs/price_form_input.rb +7 -0
  40. data/app/models/inputs/price_formtastic_input.rb +9 -0
  41. data/app/models/inputs/price_input.rb +19 -0
  42. data/app/models/inputs/price_simple_form_input.rb +8 -0
  43. data/app/models/validators/effective/sold_out_validator.rb +7 -0
  44. data/app/views/active_admin/effective_orders/orders/_show.html.haml +70 -0
  45. data/app/views/admin/customers/_actions.html.haml +2 -0
  46. data/app/views/admin/customers/index.html.haml +10 -0
  47. data/app/views/admin/orders/index.html.haml +7 -0
  48. data/app/views/admin/orders/show.html.haml +11 -0
  49. data/app/views/effective/carts/_cart.html.haml +33 -0
  50. data/app/views/effective/carts/show.html.haml +18 -0
  51. data/app/views/effective/orders/_checkout_step_1.html.haml +39 -0
  52. data/app/views/effective/orders/_checkout_step_2.html.haml +18 -0
  53. data/app/views/effective/orders/_my_purchases.html.haml +15 -0
  54. data/app/views/effective/orders/_order.html.haml +4 -0
  55. data/app/views/effective/orders/_order_header.html.haml +21 -0
  56. data/app/views/effective/orders/_order_items.html.haml +39 -0
  57. data/app/views/effective/orders/_order_payment_details.html.haml +11 -0
  58. data/app/views/effective/orders/_order_shipping.html.haml +19 -0
  59. data/app/views/effective/orders/_order_user_fields.html.haml +10 -0
  60. data/app/views/effective/orders/checkout.html.haml +3 -0
  61. data/app/views/effective/orders/declined.html.haml +10 -0
  62. data/app/views/effective/orders/moneris/_form.html.haml +34 -0
  63. data/app/views/effective/orders/my_purchases.html.haml +6 -0
  64. data/app/views/effective/orders/my_sales.html.haml +28 -0
  65. data/app/views/effective/orders/new.html.haml +4 -0
  66. data/app/views/effective/orders/paypal/_form.html.haml +5 -0
  67. data/app/views/effective/orders/purchased.html.haml +10 -0
  68. data/app/views/effective/orders/show.html.haml +17 -0
  69. data/app/views/effective/orders/stripe/_form.html.haml +8 -0
  70. data/app/views/effective/orders/stripe/_subscription_fields.html.haml +7 -0
  71. data/app/views/effective/orders_mailer/order_receipt_to_admin.html.haml +8 -0
  72. data/app/views/effective/orders_mailer/order_receipt_to_buyer.html.haml +8 -0
  73. data/app/views/effective/orders_mailer/order_receipt_to_seller.html.haml +30 -0
  74. data/app/views/effective/subscriptions/index.html.haml +16 -0
  75. data/app/views/effective/subscriptions/new.html.haml +10 -0
  76. data/app/views/effective/subscriptions/show.html.haml +49 -0
  77. data/config/routes.rb +57 -0
  78. data/db/migrate/01_create_effective_orders.rb.erb +91 -0
  79. data/db/upgrade/02_upgrade_effective_orders_from03x.rb.erb +29 -0
  80. data/db/upgrade/upgrade_price_column_on_table.rb.erb +17 -0
  81. data/lib/effective_orders/engine.rb +52 -0
  82. data/lib/effective_orders/version.rb +3 -0
  83. data/lib/effective_orders.rb +76 -0
  84. data/lib/generators/effective_orders/install_generator.rb +38 -0
  85. data/lib/generators/effective_orders/upgrade_from03x_generator.rb +34 -0
  86. data/lib/generators/effective_orders/upgrade_price_column_generator.rb +34 -0
  87. data/lib/generators/templates/README +1 -0
  88. data/lib/generators/templates/effective_orders.rb +210 -0
  89. data/spec/controllers/carts_controller_spec.rb +143 -0
  90. data/spec/controllers/moneris_orders_controller_spec.rb +245 -0
  91. data/spec/controllers/orders_controller_spec.rb +418 -0
  92. data/spec/controllers/stripe_orders_controller_spec.rb +127 -0
  93. data/spec/controllers/webhooks_controller_spec.rb +79 -0
  94. data/spec/dummy/README.rdoc +8 -0
  95. data/spec/dummy/Rakefile +6 -0
  96. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  97. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  98. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  99. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  100. data/spec/dummy/app/models/product.rb +17 -0
  101. data/spec/dummy/app/models/product_with_float_price.rb +17 -0
  102. data/spec/dummy/app/models/user.rb +28 -0
  103. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  104. data/spec/dummy/bin/bundle +3 -0
  105. data/spec/dummy/bin/rails +4 -0
  106. data/spec/dummy/bin/rake +4 -0
  107. data/spec/dummy/config/application.rb +31 -0
  108. data/spec/dummy/config/boot.rb +5 -0
  109. data/spec/dummy/config/database.yml +25 -0
  110. data/spec/dummy/config/environment.rb +5 -0
  111. data/spec/dummy/config/environments/development.rb +37 -0
  112. data/spec/dummy/config/environments/production.rb +83 -0
  113. data/spec/dummy/config/environments/test.rb +39 -0
  114. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  115. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  116. data/spec/dummy/config/initializers/devise.rb +254 -0
  117. data/spec/dummy/config/initializers/effective_addresses.rb +15 -0
  118. data/spec/dummy/config/initializers/effective_orders.rb +22 -0
  119. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  120. data/spec/dummy/config/initializers/inflections.rb +16 -0
  121. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  122. data/spec/dummy/config/initializers/session_store.rb +3 -0
  123. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  124. data/spec/dummy/config/locales/en.yml +23 -0
  125. data/spec/dummy/config/routes.rb +3 -0
  126. data/spec/dummy/config/secrets.yml +22 -0
  127. data/spec/dummy/config.ru +4 -0
  128. data/spec/dummy/db/schema.rb +142 -0
  129. data/spec/dummy/db/test.sqlite3 +0 -0
  130. data/spec/dummy/log/development.log +487 -0
  131. data/spec/dummy/log/test.log +347 -0
  132. data/spec/dummy/public/404.html +67 -0
  133. data/spec/dummy/public/422.html +67 -0
  134. data/spec/dummy/public/500.html +66 -0
  135. data/spec/dummy/public/favicon.ico +0 -0
  136. data/spec/helpers/effective_orders_helper_spec.rb +21 -0
  137. data/spec/models/acts_as_purchasable_spec.rb +107 -0
  138. data/spec/models/customer_spec.rb +71 -0
  139. data/spec/models/factories_spec.rb +13 -0
  140. data/spec/models/order_item_spec.rb +35 -0
  141. data/spec/models/order_spec.rb +323 -0
  142. data/spec/models/stripe_charge_spec.rb +39 -0
  143. data/spec/models/subscription_spec.rb +103 -0
  144. data/spec/spec_helper.rb +44 -0
  145. data/spec/support/factories.rb +118 -0
  146. 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,7 @@
1
+ module Inputs
2
+ module PriceFormInput
3
+ def price_field(method, opts = {})
4
+ Inputs::PriceField.new(@object, @object_name, @template, method, opts).to_html
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ if defined?(Formtastic)
2
+ class PriceFormtasticInput < Formtastic::Inputs::NumberInput
3
+ def to_html
4
+ input_wrapping do
5
+ label_html << Inputs::PriceField.new(@object, @object_name, @template, @method, @options).to_html
6
+ end
7
+ end
8
+ end
9
+ end
@@ -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,7 @@
1
+ module Effective
2
+ class SoldOutValidator < ActiveModel::Validator
3
+ def validate(record)
4
+ record.errors[:base] << "sold out" if record.sold_out?
5
+ end
6
+ end
7
+ 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,2 @@
1
+ - if customer.stripe_customer_id.present?
2
+ = link_to 'Manage', "https://manage.stripe.com/customers/#{customer.stripe_customer_id}"
@@ -0,0 +1,10 @@
1
+ %h2 Customers
2
+
3
+ - unless @datatable.collection.length > 0
4
+ %p
5
+ There are no customers
6
+ - else
7
+ = render_datatable @datatable
8
+
9
+ %hr
10
+ %p= link_to 'Stripe Dashboard: Customers', 'https://manage.stripe.com/customers', :class => 'btn btn-primary'
@@ -0,0 +1,7 @@
1
+ %h2 Orders
2
+
3
+ - unless @datatable.collection.length > 0
4
+ %p
5
+ There are no orders
6
+ - else
7
+ = render_datatable @datatable
@@ -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
+