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,125 @@
|
|
|
1
|
+
require 'stripe'
|
|
2
|
+
|
|
3
|
+
module Effective
|
|
4
|
+
module Providers
|
|
5
|
+
module Stripe
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def stripe_charge
|
|
12
|
+
@order = Effective::Order.find(stripe_charge_params[:effective_order_id])
|
|
13
|
+
@stripe_charge = Effective::StripeCharge.new(stripe_charge_params)
|
|
14
|
+
@stripe_charge.order = @order
|
|
15
|
+
|
|
16
|
+
EffectiveOrders.authorized?(self, :update, @order)
|
|
17
|
+
|
|
18
|
+
if @stripe_charge.valid? && (response = process_stripe_charge(@stripe_charge)) != false
|
|
19
|
+
order_purchased(response) # orders_controller#order_purchased
|
|
20
|
+
else
|
|
21
|
+
flash[:danger] = @stripe_charge.errors.full_messages.join(',')
|
|
22
|
+
render 'checkout'
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def process_stripe_charge(charge)
|
|
29
|
+
Effective::Order.transaction do
|
|
30
|
+
begin
|
|
31
|
+
@buyer = Customer.for_user(charge.order.user)
|
|
32
|
+
@buyer.update_card!(charge.token)
|
|
33
|
+
|
|
34
|
+
if EffectiveOrders.stripe_connect_enabled
|
|
35
|
+
return charge_with_stripe_connect(charge, @buyer)
|
|
36
|
+
else
|
|
37
|
+
return charge_with_stripe(charge, @buyer)
|
|
38
|
+
end
|
|
39
|
+
rescue => e
|
|
40
|
+
charge.errors.add(:base, "Unable to process order with Stripe. Your credit card has not been charged. Message: \"#{e.message}\".")
|
|
41
|
+
raise ActiveRecord::Rollback
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def charge_with_stripe(charge, buyer)
|
|
49
|
+
results = {:subscriptions => {}, :charge => nil}
|
|
50
|
+
|
|
51
|
+
# Process subscriptions.
|
|
52
|
+
charge.subscriptions.each do |subscription|
|
|
53
|
+
next if subscription.stripe_plan_id.blank?
|
|
54
|
+
|
|
55
|
+
stripe_subscription = if subscription.stripe_coupon_id.present?
|
|
56
|
+
buyer.stripe_customer.subscriptions.create({:plan => subscription.stripe_plan_id, :coupon => subscription.stripe_coupon_id})
|
|
57
|
+
else
|
|
58
|
+
buyer.stripe_customer.subscriptions.create({:plan => subscription.stripe_plan.id})
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
subscription.stripe_subscription_id = stripe_subscription.id
|
|
62
|
+
subscription.save!
|
|
63
|
+
|
|
64
|
+
results[:subscriptions][subscription.stripe_plan_id] = JSON.parse(stripe_subscription.to_json)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Process regular order_items.
|
|
68
|
+
amount = (charge.order_items.collect(&:total).sum) # A positive integer in cents representing how much to charge the card. The minimum amount is 50 cents.
|
|
69
|
+
description = "Charge for Order ##{charge.order.to_param}"
|
|
70
|
+
|
|
71
|
+
if amount > 0
|
|
72
|
+
results[:charge] = JSON.parse(::Stripe::Charge.create(
|
|
73
|
+
:amount => amount,
|
|
74
|
+
:currency => EffectiveOrders.stripe[:currency],
|
|
75
|
+
:customer => buyer.stripe_customer.id,
|
|
76
|
+
:description => description
|
|
77
|
+
).to_json)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
results
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def charge_with_stripe_connect(charge, buyer)
|
|
84
|
+
# Go through and create Stripe::Tokens for each seller
|
|
85
|
+
items = charge.order_items.group_by(&:seller)
|
|
86
|
+
results = {}
|
|
87
|
+
|
|
88
|
+
# We do all these Tokens first, so if one throws an exception no charges are made
|
|
89
|
+
items.each do |seller, _|
|
|
90
|
+
seller.token = ::Stripe::Token.create({:customer => buyer.stripe_customer.id}, seller.stripe_connect_access_token)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Make one charge per seller, for all his order_items
|
|
94
|
+
items.each do |seller, order_items|
|
|
95
|
+
amount = order_items.sum(&:total)
|
|
96
|
+
description = "Charge for Order ##{charge.order.to_param} with OrderItems ##{order_items.map(&:id).join(', #')}"
|
|
97
|
+
application_fee = order_items.sum(&:stripe_connect_application_fee)
|
|
98
|
+
|
|
99
|
+
results[seller.id] = JSON.parse(::Stripe::Charge.create(
|
|
100
|
+
{
|
|
101
|
+
:amount => amount,
|
|
102
|
+
:currency => EffectiveOrders.stripe[:currency],
|
|
103
|
+
:card => seller.token.id,
|
|
104
|
+
:description => description,
|
|
105
|
+
:application_fee => application_fee
|
|
106
|
+
},
|
|
107
|
+
seller.stripe_connect_access_token
|
|
108
|
+
).to_json)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
results
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# StrongParameters
|
|
115
|
+
def stripe_charge_params
|
|
116
|
+
begin
|
|
117
|
+
params.require(:effective_stripe_charge).permit(:token, :effective_order_id)
|
|
118
|
+
rescue => e
|
|
119
|
+
params[:effective_stripe_charge]
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
module Providers
|
|
3
|
+
module StripeConnect
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
prepend_before_filter :set_stripe_connect_state_params, :only => [:stripe_connect_redirect_uri]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# So this is the postback after Stripe does its oAuth authentication
|
|
11
|
+
def stripe_connect_redirect_uri
|
|
12
|
+
if params[:code].present?
|
|
13
|
+
token_params = request_access_token(params[:code]) # We got a code, so now we make a curl request for the access_token
|
|
14
|
+
customer = Effective::Customer.for_user(current_user)
|
|
15
|
+
|
|
16
|
+
if token_params['access_token'].present? && customer.present?
|
|
17
|
+
if customer.update_attributes(:stripe_connect_access_token => token_params['access_token'])
|
|
18
|
+
flash[:success] = 'Successfully Connected with Stripe Connect'
|
|
19
|
+
else
|
|
20
|
+
flash[:danger] = "Unable to update customer: #{customer.errors[:base].first}"
|
|
21
|
+
end
|
|
22
|
+
else
|
|
23
|
+
flash[:danger] = "Error when connecting to Stripe /oauth/token: #{token_params[:error]}. Please try again."
|
|
24
|
+
end
|
|
25
|
+
else
|
|
26
|
+
flash[:danger] = "Error when connecting to Stripe /oauth/authorize: #{params[:error]}. Please try again."
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
redirect_to URI.parse(@stripe_state_params['redirect_to']).path rescue effective_orders.orders_path
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def request_access_token(code)
|
|
35
|
+
stripe_response = `curl -F client_secret='#{EffectiveOrders.stripe[:secret_key]}' -F code='#{code}' -F grant_type='authorization_code' #{EffectiveStripeHelper::STRIPE_CONNECT_TOKEN_URL}`
|
|
36
|
+
JSON.parse(stripe_response) rescue {}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def set_stripe_connect_state_params
|
|
40
|
+
@stripe_state_params = (JSON.parse(params[:state]) rescue {})
|
|
41
|
+
@stripe_state_params = {} unless @stripe_state_params.kind_of?(Hash)
|
|
42
|
+
|
|
43
|
+
params[:authenticity_token] = @stripe_state_params['form_authenticity_token']
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class SubscriptionsController < ApplicationController
|
|
3
|
+
include EffectiveCartsHelper
|
|
4
|
+
include EffectiveStripeHelper
|
|
5
|
+
|
|
6
|
+
layout (EffectiveOrders.layout.kind_of?(Hash) ? EffectiveOrders.layout[:subscriptions] : EffectiveOrders.layout)
|
|
7
|
+
|
|
8
|
+
before_filter :authenticate_user!
|
|
9
|
+
before_filter :assign_customer
|
|
10
|
+
|
|
11
|
+
# This is a 'My Subscriptions' page
|
|
12
|
+
def index
|
|
13
|
+
@page_title ||= 'My Subscriptions'
|
|
14
|
+
|
|
15
|
+
@subscriptions = @customer.subscriptions.purchased
|
|
16
|
+
|
|
17
|
+
EffectiveOrders.authorized?(self, :index, Effective::Subscription)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def new
|
|
21
|
+
@page_title ||= 'New Subscription'
|
|
22
|
+
|
|
23
|
+
@subscription = @customer.subscriptions.new()
|
|
24
|
+
|
|
25
|
+
purchased_plans = @customer.subscriptions.purchased.map(&:stripe_plan_id)
|
|
26
|
+
@plans = Stripe::Plan.all.reject { |stripe_plan| purchased_plans.include?(stripe_plan.id) }
|
|
27
|
+
|
|
28
|
+
EffectiveOrders.authorized?(self, :new, @subscription)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def create
|
|
32
|
+
@page_title ||= 'New Subscription'
|
|
33
|
+
|
|
34
|
+
# Don't let the user create another Subscription object if it's already created
|
|
35
|
+
@subscription = @customer.subscriptions.where(:stripe_plan_id => subscription_params[:stripe_plan_id]).first_or_initialize
|
|
36
|
+
|
|
37
|
+
EffectiveOrders.authorized?(self, :create, @subscription)
|
|
38
|
+
|
|
39
|
+
if @subscription.update_attributes(subscription_params) && (current_cart.find(@subscription).present? || current_cart.add(@subscription))
|
|
40
|
+
flash[:success] = "Successfully added subscription to cart"
|
|
41
|
+
redirect_to effective_orders.new_order_path
|
|
42
|
+
else
|
|
43
|
+
purchased_plans = @customer.subscriptions.purchased.map(&:stripe_plan_id)
|
|
44
|
+
@plans = Stripe::Plan.all.reject { |stripe_plan| purchased_plans.include?(stripe_plan.id) }
|
|
45
|
+
|
|
46
|
+
flash[:danger] ||= 'Unable to add subscription to cart. Please try again.'
|
|
47
|
+
render :action => :new
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def show
|
|
52
|
+
@plan = Stripe::Plan.retrieve(params[:id])
|
|
53
|
+
|
|
54
|
+
unless @plan.present?
|
|
55
|
+
flash[:danger] = "Unrecognized Stripe Plan: #{params[:id]}"
|
|
56
|
+
raise ActiveRecord::RecordNotFound
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@subscription = @customer.subscriptions.find { |subscription| subscription.stripe_plan_id == params[:id] }
|
|
60
|
+
|
|
61
|
+
unless @subscription.present?
|
|
62
|
+
flash[:danger] = "Unable to find Customer Subscription for plan: #{params[:id]}"
|
|
63
|
+
raise ActiveRecord::RecordNotFound
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
@stripe_subscription = @subscription.try(:stripe_subscription)
|
|
67
|
+
|
|
68
|
+
unless @stripe_subscription.present?
|
|
69
|
+
flash[:danger] = "Unable to find Stripe Subscription for plan: #{params[:id]}"
|
|
70
|
+
raise ActiveRecord::RecordNotFound
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
EffectiveOrders.authorized?(self, :show, @subscription)
|
|
74
|
+
|
|
75
|
+
@invoices = @customer.stripe_customer.invoices.all.select do |invoice|
|
|
76
|
+
invoice.lines.any? { |line| line.id == @stripe_subscription.id }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
@page_title ||= "#{@plan.name}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def destroy
|
|
83
|
+
@plan = Stripe::Plan.retrieve(params[:id])
|
|
84
|
+
raise ActiveRecord::RecordNotFound unless @plan.present?
|
|
85
|
+
|
|
86
|
+
@subscription = @customer.subscriptions.find { |subscription| subscription.stripe_plan_id == params[:id] }
|
|
87
|
+
@stripe_subscription = @subscription.try(:stripe_subscription)
|
|
88
|
+
raise ActiveRecord::RecordNotFound unless @subscription.present?
|
|
89
|
+
|
|
90
|
+
EffectiveOrders.authorized?(self, :destroy, @subscription)
|
|
91
|
+
|
|
92
|
+
if @subscription.present?
|
|
93
|
+
begin
|
|
94
|
+
@stripe_subscription.delete if @stripe_subscription
|
|
95
|
+
@subscription.destroy
|
|
96
|
+
flash[:success] = "Successfully unsubscribed from #{params[:id]}"
|
|
97
|
+
rescue => e
|
|
98
|
+
flash[:danger] = "Unable to unsubscribe. Message: \"#{e.message}\"."
|
|
99
|
+
end
|
|
100
|
+
else
|
|
101
|
+
flash[:danger] = "Unable to find stripe subscription for #{params[:id]}" unless @subscription.present?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
redirect_to effective_orders.subscriptions_path
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def assign_customer
|
|
110
|
+
@customer ||= Customer.for_user(current_user)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# StrongParameters
|
|
114
|
+
def subscription_params
|
|
115
|
+
begin
|
|
116
|
+
params.require(:effective_subscription).permit(:stripe_plan_id, :stripe_coupon_id)
|
|
117
|
+
rescue => e
|
|
118
|
+
params[:effective_subscription]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class WebhooksController < ApplicationController
|
|
3
|
+
protect_from_forgery :except => [:stripe]
|
|
4
|
+
skip_authorization_check if defined?(CanCan)
|
|
5
|
+
|
|
6
|
+
# Webhook from stripe
|
|
7
|
+
def stripe
|
|
8
|
+
(head(:ok) and return) if (params[:livemode] == false && Rails.env.production?) || params[:object] != 'event' || params[:id].blank?
|
|
9
|
+
|
|
10
|
+
# Dont trust the POST, and instead request the actual event from Stripe
|
|
11
|
+
@event = Stripe::Event.retrieve(params[:id]) rescue (head(:ok) and return)
|
|
12
|
+
|
|
13
|
+
Effective::Customer.transaction do
|
|
14
|
+
begin
|
|
15
|
+
case @event.type
|
|
16
|
+
when 'customer.created' ; stripe_customer_created(@event)
|
|
17
|
+
when 'customer.deleted' ; stripe_customer_deleted(@event)
|
|
18
|
+
when 'customer.subscription.created' ; stripe_subscription_created(@event)
|
|
19
|
+
when 'customer.subscription.deleted' ; stripe_subscription_deleted(@event)
|
|
20
|
+
end
|
|
21
|
+
rescue => e
|
|
22
|
+
Rails.logger.info "Stripe Webhook Error: #{e.message}"
|
|
23
|
+
raise ActiveRecord::Rollback
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
head :ok # Always return success
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def stripe_customer_created(event)
|
|
33
|
+
stripe_customer = event.data.object
|
|
34
|
+
user = ::User.where(:email => stripe_customer.email).first
|
|
35
|
+
|
|
36
|
+
if user.present?
|
|
37
|
+
customer = Effective::Customer.for_user(user) # This is a first_or_create
|
|
38
|
+
customer.stripe_customer_id = stripe_customer.id
|
|
39
|
+
customer.save!
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def stripe_customer_deleted(event)
|
|
44
|
+
stripe_customer = event.data.object
|
|
45
|
+
user = ::User.where(:email => stripe_customer.email).first
|
|
46
|
+
|
|
47
|
+
if user.present?
|
|
48
|
+
customer = Effective::Customer.where(:user_id => user.id).first
|
|
49
|
+
customer.destroy! if customer
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def stripe_subscription_created(event)
|
|
54
|
+
stripe_subscription = event.data.object
|
|
55
|
+
@customer = Effective::Customer.where(:stripe_customer_id => stripe_subscription.customer).first
|
|
56
|
+
|
|
57
|
+
if @customer.present?
|
|
58
|
+
subscription = @customer.subscriptions.where(:stripe_plan_id => stripe_subscription.plan.id).first_or_initialize
|
|
59
|
+
|
|
60
|
+
subscription.stripe_subscription_id = stripe_subscription.id
|
|
61
|
+
subscription.stripe_plan_id = (stripe_subscription.plan.id rescue nil)
|
|
62
|
+
subscription.stripe_coupon_id = stripe_subscription.discount.coupon.id if (stripe_subscription.discount.present? rescue false)
|
|
63
|
+
|
|
64
|
+
subscription.save!
|
|
65
|
+
|
|
66
|
+
unless subscription.purchased?
|
|
67
|
+
# Now we have to purchase it
|
|
68
|
+
@order = Effective::Order.new(subscription)
|
|
69
|
+
@order.user = @customer.user
|
|
70
|
+
@order.purchase!("via Stripe webhook #{event.id}")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def stripe_subscription_deleted(event)
|
|
77
|
+
stripe_subscription = event.data.object
|
|
78
|
+
customer = Effective::Customer.where(:stripe_customer_id => stripe_subscription.customer).first
|
|
79
|
+
|
|
80
|
+
if customer.present?
|
|
81
|
+
customer.subscriptions.find { |subscription| subscription.stripe_plan_id == stripe_subscription.plan.id }.try(:destroy)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module EffectiveCartsHelper
|
|
2
|
+
def current_cart(for_user = nil)
|
|
3
|
+
@cart ||= (
|
|
4
|
+
user = for_user || (current_user rescue nil) # rescue protects me against Devise not being installed
|
|
5
|
+
|
|
6
|
+
if user.present?
|
|
7
|
+
Effective::Cart.where(:user_id => user.id).first_or_create.tap do |user_cart|
|
|
8
|
+
if session[:cart].present?
|
|
9
|
+
session_cart = Effective::Cart.where('user_id IS NULL').where(:id => session[:cart]).first
|
|
10
|
+
|
|
11
|
+
if session_cart.present?
|
|
12
|
+
session_cart.cart_items.update_all(:cart_id => user_cart.id)
|
|
13
|
+
session_cart.destroy
|
|
14
|
+
user_cart.reload
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
session[:cart] = nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
elsif session[:cart].present?
|
|
21
|
+
Effective::Cart.where('user_id IS NULL').where(:id => session[:cart]).first_or_create
|
|
22
|
+
else
|
|
23
|
+
cart = Effective::Cart.create!
|
|
24
|
+
session[:cart] = cart.id
|
|
25
|
+
cart
|
|
26
|
+
end
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def link_to_current_cart(opts = {})
|
|
31
|
+
options = {:id => 'current_cart', :rel => :nofollow}.merge(opts)
|
|
32
|
+
|
|
33
|
+
if current_cart.size == 0
|
|
34
|
+
link_to (options.delete(:label) || 'Cart'), effective_orders.cart_path, options
|
|
35
|
+
else
|
|
36
|
+
link_to (options.delete(:label) || "Cart (#{current_cart.size})"), effective_orders.cart_path, options
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def link_to_add_to_cart(purchasable, opts = {})
|
|
41
|
+
raise ArgumentError.new('expecting an acts_as_purchasable object') unless purchasable.respond_to?(:is_effectively_purchasable?)
|
|
42
|
+
|
|
43
|
+
options = {:class => 'btn', :rel => :nofollow, 'data-disable-with' => 'Add to Cart...'}.merge(opts)
|
|
44
|
+
options[:class] = ((options[:class] || '') + ' btn-add-to-cart')
|
|
45
|
+
|
|
46
|
+
link_to (options.delete(:label) || 'Add to Cart'), effective_orders.add_to_cart_path(:purchasable_type => purchasable.class.name, :purchasable_id => purchasable.id.to_i), options
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def link_to_remove_from_cart(cart_item, opts = {})
|
|
50
|
+
raise ArgumentError.new('expecting an Effective::CartItem object') unless cart_item.kind_of?(Effective::CartItem)
|
|
51
|
+
|
|
52
|
+
options = {
|
|
53
|
+
:rel => :nofollow,
|
|
54
|
+
:data => {:confirm => 'Are you sure? This cannot be undone!'},
|
|
55
|
+
:method => :delete
|
|
56
|
+
}.merge(opts)
|
|
57
|
+
options[:class] = ((options[:class] || '') + ' btn-remove-from-cart')
|
|
58
|
+
|
|
59
|
+
link_to (options.delete(:label) || 'Remove'), effective_orders.remove_from_cart_path(cart_item), options
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def link_to_empty_cart(opts = {})
|
|
63
|
+
options = {
|
|
64
|
+
:rel => :nofollow,
|
|
65
|
+
:class => 'btn',
|
|
66
|
+
:data => {:confirm => 'This will clear your entire cart. Are you sure? This cannot be undone!'},
|
|
67
|
+
:method => :delete
|
|
68
|
+
}.merge(opts)
|
|
69
|
+
options[:class] = ((options[:class] || '') + ' btn-empty-cart btn-danger')
|
|
70
|
+
|
|
71
|
+
link_to (options.delete(:label) || 'Empty Cart'), effective_orders.cart_path, options
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def link_to_checkout(opts = {})
|
|
75
|
+
options = {:class => 'btn', :rel => :nofollow}.merge(opts)
|
|
76
|
+
options[:class] = ((options[:class] || '') + ' btn-checkout')
|
|
77
|
+
|
|
78
|
+
link_to (options.delete(:label) || 'Proceed to Checkout'), effective_orders.new_order_path, options
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def render_cart(cart = nil)
|
|
82
|
+
cart ||= current_cart
|
|
83
|
+
render(:partial => 'effective/carts/cart', :locals => {:cart => cart})
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def render_purchasables(*purchasables)
|
|
87
|
+
render(:partial => 'effective/orders/order_items', :locals => {:order => Effective::Order.new(purchasables)})
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
module EffectiveOrdersHelper
|
|
2
|
+
def price_to_currency(price)
|
|
3
|
+
raise 'price_to_currency expects an Integer representing the number of cents in a price' unless price.kind_of?(Integer)
|
|
4
|
+
number_to_currency(price / 100.0)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def order_summary(order)
|
|
8
|
+
content_tag(:p, "#{price_to_currency(order.total)} total for #{pluralize(order.num_items, 'item')}:") +
|
|
9
|
+
|
|
10
|
+
content_tag(:ul) do
|
|
11
|
+
order.order_items.map do |item|
|
|
12
|
+
content_tag(:li) do
|
|
13
|
+
title = item.title.split('<br>')
|
|
14
|
+
"#{item.quantity}x #{title.first} for #{price_to_currency(item.price)}".tap do |output|
|
|
15
|
+
title[1..-1].each { |line| output << "<br>#{line}" }
|
|
16
|
+
end.html_safe
|
|
17
|
+
end
|
|
18
|
+
end.join().html_safe
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def order_item_summary(order_item)
|
|
23
|
+
if order_item.quantity > 1
|
|
24
|
+
content_tag(:p, "#{price_to_currency(order_item.total)} total for #{pluralize(order_item.quantity, 'item')}")
|
|
25
|
+
else
|
|
26
|
+
content_tag(:p, "#{price_to_currency(order_item.total)} total")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# This is called on the My Sales Page and is intended to be overridden in the app if needed
|
|
31
|
+
def acts_as_purchasable_path(purchasable, action = :show)
|
|
32
|
+
polymorphic_path(purchasable)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def order_payment_to_html(order)
|
|
36
|
+
payment = order.payment
|
|
37
|
+
|
|
38
|
+
if order.purchased?(:stripe_connect) && order.payment.kind_of?(Hash)
|
|
39
|
+
payment = Hash[
|
|
40
|
+
order.payment.map do |seller_id, v|
|
|
41
|
+
if (user = Effective::Customer.find(seller_id).try(:user))
|
|
42
|
+
[link_to(user, admin_user_path(user)), order.payment[seller_id]]
|
|
43
|
+
else
|
|
44
|
+
[seller_id, order.payment[seller_id]]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
content_tag(:pre) do
|
|
51
|
+
raw JSON.pretty_generate(payment).html_safe
|
|
52
|
+
.gsub('\"', '')
|
|
53
|
+
.gsub("[\n\n ]", '[]')
|
|
54
|
+
.gsub("{\n }", '{}')
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def render_order(order)
|
|
59
|
+
render(:partial => 'effective/orders/order', :locals => {:order => order})
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render_checkout(order, opts = {})
|
|
63
|
+
raise ArgumentError.new('unable to checkout an order without a user') unless order.user.present?
|
|
64
|
+
|
|
65
|
+
locals = {
|
|
66
|
+
:purchased_redirect_url => nil,
|
|
67
|
+
:declined_redirect_url => nil
|
|
68
|
+
}.merge(opts)
|
|
69
|
+
|
|
70
|
+
if order.new_record?
|
|
71
|
+
render(:partial => 'effective/orders/checkout_step_1', :locals => locals.merge({:order => order}))
|
|
72
|
+
else
|
|
73
|
+
raise ArgumentError.new('unable to checkout a persisted but invalid order') unless order.valid?
|
|
74
|
+
render(:partial => 'effective/orders/checkout_step_2', :locals => locals.merge({:order => order}))
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def link_to_my_purchases(opts = {})
|
|
79
|
+
options = {:rel => :nofollow}.merge(opts)
|
|
80
|
+
link_to (options.delete(:label) || 'My Purchases'), effective_orders.my_purchases_path, options
|
|
81
|
+
end
|
|
82
|
+
alias_method :link_to_order_history, :link_to_my_purchases
|
|
83
|
+
|
|
84
|
+
def render_order_history(user_or_orders, opts = {})
|
|
85
|
+
if user_or_orders.kind_of?(User)
|
|
86
|
+
orders = Effective::Order.purchased_by(user_or_orders)
|
|
87
|
+
elsif user_or_orders.respond_to?(:to_a)
|
|
88
|
+
begin
|
|
89
|
+
orders = user_or_orders.to_a.select { |order| order.purchased? }
|
|
90
|
+
rescue => e
|
|
91
|
+
raise ArgumentError.new('expecting an instance of User or an array/collection of Effective::Order objects')
|
|
92
|
+
end
|
|
93
|
+
else
|
|
94
|
+
raise ArgumentError.new('expecting an instance of User or an array/collection of Effective::Order objects')
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
locals = {
|
|
98
|
+
:orders => orders,
|
|
99
|
+
:order_path => effective_orders.order_path(':id') # The :id string will be replaced with the order id
|
|
100
|
+
}.merge(opts)
|
|
101
|
+
|
|
102
|
+
render(:partial => 'effective/orders/my_purchases', :locals => locals)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
alias_method :render_purchases, :render_order_history
|
|
106
|
+
alias_method :render_my_purchases, :render_order_history
|
|
107
|
+
|
|
108
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module EffectivePaypalHelper
|
|
2
|
+
# These're constants so they only get read once, not every order request
|
|
3
|
+
PAYPAL_CERT_PEM = (File.read(EffectiveOrders.paypal[:paypal_cert]) rescue {})
|
|
4
|
+
APP_CERT_PEM = (File.read(EffectiveOrders.paypal[:app_cert]) rescue {})
|
|
5
|
+
APP_KEY_PEM = (File.read(EffectiveOrders.paypal[:app_key]) rescue {})
|
|
6
|
+
|
|
7
|
+
def paypal_encrypted_payload(order)
|
|
8
|
+
raise ArgumentError.new("unable to read EffectiveOrders PayPal paypal_cert #{EffectiveOrders.paypal[:paypal_cert]}") unless PAYPAL_CERT_PEM.present?
|
|
9
|
+
raise ArgumentError.new("unable to read EffectiveOrders PayPal app_cert #{EffectiveOrders.paypal[:app_cert]}") unless APP_CERT_PEM.present?
|
|
10
|
+
raise ArgumentError.new("unable to read EffectiveOrders PayPal app_key #{EffectiveOrders.paypal[:app_key]}") unless APP_KEY_PEM.present?
|
|
11
|
+
|
|
12
|
+
values = {
|
|
13
|
+
:business => EffectiveOrders.paypal[:seller_email],
|
|
14
|
+
:custom => EffectiveOrders.paypal[:secret],
|
|
15
|
+
:cmd => '_cart',
|
|
16
|
+
:upload => 1,
|
|
17
|
+
:return => effective_orders.order_purchased_url(order),
|
|
18
|
+
:notify_url => effective_orders.paypal_postback_url,
|
|
19
|
+
:cert_id => EffectiveOrders.paypal[:cert_id],
|
|
20
|
+
:currency_code => EffectiveOrders.paypal[:currency],
|
|
21
|
+
:invoice => order.id + EffectiveOrders.paypal[:order_id_nudge].to_i,
|
|
22
|
+
:amount => (order.subtotal / 100.0).round(2),
|
|
23
|
+
:tax_cart => (order.tax / 100.0).round(2)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
order.order_items.each_with_index do |item, x|
|
|
27
|
+
values["item_number_#{x+1}"] = x+1
|
|
28
|
+
values["item_name_#{x+1}"] = item.title
|
|
29
|
+
values["quantity_#{x+1}"] = item.quantity
|
|
30
|
+
values["amount_#{x+1}"] = '%.2f' % (item.price / 100.0)
|
|
31
|
+
values["tax_#{x+1}"] = '%.2f' % ((item.tax / 100.0) / item.quantity) # Tax for 1 of these items
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
signed = OpenSSL::PKCS7::sign(OpenSSL::X509::Certificate.new(APP_CERT_PEM), OpenSSL::PKey::RSA.new(APP_KEY_PEM, ''), values.map { |k, v| "#{k}=#{v}" }.join("\n"), [], OpenSSL::PKCS7::BINARY)
|
|
35
|
+
OpenSSL::PKCS7::encrypt([OpenSSL::X509::Certificate.new(PAYPAL_CERT_PEM)], signed.to_der, OpenSSL::Cipher::Cipher::new("DES3"), OpenSSL::PKCS7::BINARY).to_s.gsub("\n", "")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module EffectiveStripeHelper
|
|
2
|
+
|
|
3
|
+
STRIPE_CONNECT_AUTHORIZE_URL = 'https://connect.stripe.com/oauth/authorize'
|
|
4
|
+
STRIPE_CONNECT_TOKEN_URL = 'https://connect.stripe.com/oauth/token'
|
|
5
|
+
|
|
6
|
+
def is_stripe_connect_seller?(user)
|
|
7
|
+
Effective::Customer.for_user(user).try(:is_stripe_connect_seller?) == true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def link_to_new_stripe_connect_customer(opts = {})
|
|
11
|
+
client_id = EffectiveOrders.stripe[:connect_client_id]
|
|
12
|
+
|
|
13
|
+
raise ArgumentError.new('effective_orders config: stripe.connect_client_id has not been set') unless client_id.present?
|
|
14
|
+
|
|
15
|
+
authorize_params = {
|
|
16
|
+
response_type: :code,
|
|
17
|
+
client_id: client_id, # This is the Application's ClientID
|
|
18
|
+
scope: :read_write,
|
|
19
|
+
state: {
|
|
20
|
+
form_authenticity_token: form_authenticity_token, # Rails standard CSRF
|
|
21
|
+
redirect_to: URI.encode(request.original_url) # TODO: Allow this to be customized
|
|
22
|
+
}.to_json
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Add the stripe_user parameter if it's possible
|
|
26
|
+
stripe_user_params = opts.delete :stripe_user
|
|
27
|
+
authorize_params.merge!({stripe_user: stripe_user_params}) if stripe_user_params.is_a?(Hash)
|
|
28
|
+
|
|
29
|
+
authorize_url = STRIPE_CONNECT_AUTHORIZE_URL.chomp('/') + '?' + authorize_params.to_query
|
|
30
|
+
options = {}.merge(opts)
|
|
31
|
+
link_to image_tag('/assets/effective_orders/stripe_connect.png'), authorize_url, options
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
### Subscriptions Helpers
|
|
35
|
+
def stripe_plans_collection(plans)
|
|
36
|
+
(plans || []).map { |plan| [stripe_plan_description(plan), plan.id, {'data-amount' => plan.amount}] }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def stripe_plan_description(plan)
|
|
40
|
+
occurrence = case plan.interval
|
|
41
|
+
when 'weekly' ; '/week'
|
|
42
|
+
when 'monthly' ; '/month'
|
|
43
|
+
when 'yearly' ; '/year'
|
|
44
|
+
when 'week' ; plan.interval_count == 1 ? '/week' : " every #{plan.interval_count} weeks"
|
|
45
|
+
when 'month' ; plan.interval_count == 1 ? '/month' : " every #{plan.interval_count} months"
|
|
46
|
+
when 'year' ; plan.interval_count == 1 ? '/year' : " every #{plan.interval_count} years"
|
|
47
|
+
else ; plan.interval
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
"#{plan.name} - #{ActionController::Base.helpers.price_to_currency(plan.amount)} #{plan.currency.upcase}#{occurrence}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def stripe_coupon_description(coupon)
|
|
54
|
+
amount = coupon.amount_off.present? ? ActionController::Base.helpers.price_to_currency(coupon.amount_off) : "#{coupon.percent_off}%"
|
|
55
|
+
|
|
56
|
+
if coupon.duration_in_months.present?
|
|
57
|
+
"#{coupon.id} - #{amount} off for #{coupon.duration_in_months} months"
|
|
58
|
+
else
|
|
59
|
+
"#{coupon.id} - #{amount} off #{coupon.duration}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
end
|