effective_orders 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|