Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PayPal Pro Payments #528

Draft
wants to merge 11 commits into
base: the-future
Choose a base branch
from
10 changes: 6 additions & 4 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ JWT_SECRET=1c053cacaa6aa0963e211cdc7cb5b5f4bc0d99cb9b7b5aca0c1f16f104102486053e8
STRIPE_PUBLISHABLE_KEY=pk_test_aQbfVWeOwvtES5FRSY7iIjk9
STRIPE_SECRET_KEY=sk_test_Qk8TgJVXgeC1xJWu0tmwCW0l

BRAINTREE_ENVIRONMENT=sandbox
BRAINTREE_MERCHANT_ID=yxyphwjwxwn5yvtf
BRAINTREE_PUBLIC_KEY=9kb7vby5mfc28gvd
BRAINTREE_PRIVATE_KEY=87a642e593a3edf2a59b0afa1b724a00
PAYPAL_ENVIRONMENT=sandbox
PAYPAL_CLIENT_ID=AZcJV0afwc_FYjxmfZcT_PWmX7jIQAcMiv9Z5OwivUUzO2qzQMBPN5HTITxq_tsU1C-n1Yno6GfvAM7h
PAYPAL_CLIENT_SECRET=EDTD7pSMst33o7_XySy5_Yv4KWfFnJmdsokhBAiQHD_1_xWzaHYA7NOSFihAw5OlVSNo6x_9Cq0HAqzY
PAYPAL_PRO_PLAN=P-34H53708K8545322APIWLKNY
PAYPAL_PATRON_PLAN=P-50324317G1271574TPIWMNIQ


STREAM_API_KEY=sjm3sx9mgcx2
STREAM_API_SECRET=uxupxkuw5ckrd2fe9qz88zne4qagwmdm9yaegbsm8xuymqja6ff8jr678636v3c4
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ gem 'stream_rails', github: 'GetStream/stream-rails',
gem 'typhoeus' # Parallelize scraping tasks

# Cash Money
gem 'paypal-sdk-rest', '~> 1'
gem 'stripe'

# Rack Middleware
Expand Down
5 changes: 5 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,9 @@ GEM
parallel (1.12.1)
paranoia (2.4.1)
activerecord (>= 4.0, < 5.3)
paypal-sdk-rest (1.7.3)
multi_json (~> 1.0)
xml-simple
pg (0.21.0)
pg_query (1.0.2)
pghero (2.2.0)
Expand Down Expand Up @@ -663,6 +666,7 @@ GEM
hashdiff
webrobots (0.1.2)
where-or (0.1.6)
xml-simple (1.1.5)

PLATFORMS
ruby
Expand Down Expand Up @@ -724,6 +728,7 @@ DEPENDENCIES
paperclip-optimizer
parallel
paranoia (~> 2.4)
paypal-sdk-rest (~> 1)
pg
pg_query
pghero
Expand Down
36 changes: 36 additions & 0 deletions app/actions/pro/create_paypal_agreement.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Create a PayPal Billing Agreement to be authorized by a user and return the token which can be
# used to authorize it on the client side.
module Pro
class CreatePaypalAgreement < Action
include PayPal::SDK::REST

parameter :user, required: true, load: User
parameter :tier, required: true

validates :tier, inclusion: { in: %w[pro patron] }

def call
agreement = Agreement.new(
start_date: 30.seconds.from_now.iso8601,
payer: { payment_method: 'paypal' },
name: "Kitsu #{tier.upcase}",
description: "Yearly Kitsu #{tier.upcase} Subscription",
plan: paypal_plan
)

agreement.create!

{ token: agreement.token }
end

private

def paypal_plan_id
ENV["PAYPAL_#{tier.upcase}_PLAN"]
end

def paypal_plan
Plan.new(id: paypal_plan_id)
end
end
end
28 changes: 28 additions & 0 deletions app/actions/pro/subscribe_with_paypal.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Execute a PayPal Billing Agreement based on a provided token, and set up the User's
# ProSubscription to match it.
module Pro
class SubscribeWithPayPal < Action
include PayPal::SDK::REST

parameter :user, required: true, load: User
parameter :tier, required: true
parameter :token, required: true

validates :tier, inclusion: { in: %w[pro patron] }

def call
user.pro_subscription&.destroy!

agreement = Agreement.new(token: token)
agreement.execute!

subscription = ProSubscription::PayPalSubscription.create!(
user: user,
tier: tier,
billing_id: agreement.id
)

{ subscription: subscription }
end
end
end
19 changes: 19 additions & 0 deletions app/models/pro_subscription/paypal_subscription.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class ProSubscription
class PayPalSubscription < ProSubscription
PayPal = PayPal::SDK::REST

def billing_service
:paypal
end

def agreement
@agreement ||= PayPal::Agreement.find(billing_id)
end

alias_method :cancel!, :destroy!

after_destroy do
agreement.cancel!(note: 'Cancelled on website')
end
end
end
1 change: 1 addition & 0 deletions config/initializers/inflections.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
inflect.acronym 'AMA'
inflect.acronym 'ANN'
inflect.acronym 'MAL'
inflect.acronym 'PayPal'
end
8 changes: 8 additions & 0 deletions config/initializers/paypal-sdk-rest.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require 'paypal-sdk-rest'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name of this source file (paypal-sdk-rest.rb) should use snake_case. (https://github.com/bbatsov/ruby-style-guide#snake-case-files)


PayPal::SDK.configure(
mode: ENV['PAYPAL_ENVIRONMENT'],
client_id: ENV['PAYPAL_CLIENT_ID'],
client_secret: ENV['PAYPAL_CLIENT_SECRET']
)
PayPal::SDK.logger = Rails.logger
6 changes: 6 additions & 0 deletions db/migrate/20190316024235_add_state_to_pro_subscriptions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddStateToProSubscriptions < ActiveRecord::Migration
def change
add_column :pro_subscriptions, :state, :integer, default: 0, null: false
add_column :pro_subscriptions, :error, :string
end
end
4 changes: 3 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 20190223050111) do
ActiveRecord::Schema.define(version: 20190316024235) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand Down Expand Up @@ -1327,6 +1327,8 @@
t.datetime "updated_at", null: false
t.string "type", null: false
t.integer "tier", default: 0, null: false
t.integer "state", default: 0, null: false
t.string "error"
end

add_index "pro_subscriptions", ["user_id"], name: "index_pro_subscriptions_on_user_id", using: :btree
Expand Down
46 changes: 46 additions & 0 deletions lib/tasks/paypal-pro.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace :paypal_pro do
desc 'Resave all paperclip attachments'
task setup: :environment do
[
{
name: 'PRO',
cost: '19.00'
},
{
name: 'PATRON',
cost: '49.00'
}
].each do |tier|
PayPal::SDK.logger = Logger.new(nil)
plan = PayPal::SDK::REST::Plan.new(
name: "Kitsu #{tier[:name]}",
description: "Yearly subscription to Kitsu #{tier[:name]}",
type: 'INFINITE',
payment_definitions: [{
name: "Kitsu #{tier[:name]} Yearly",
type: 'REGULAR',
frequency_interval: '1',
frequency: 'YEAR',
amount: {
currency: 'USD',
value: tier[:cost]
}
}],
merchant_preferences: {
cancel_url: 'https://www.paypal.com/checkoutnow/error',
return_url: 'https://www.paypal.com/checkoutnow/error',
max_fail_attempts: '0',
auto_bill_amount: 'YES',
initial_fail_amount_action: 'CANCEL'
}
)
plan.create!

# They won't let you activate in the same request as creation
patch = PayPal::SDK::REST::Patch.new(op: 'replace', path: '/', value: { state: 'ACTIVE' })
plan.update!(patch)

puts "PAYPAL_#{tier[:name]}_PLAN=#{plan.id}"
end
end
end
8 changes: 8 additions & 0 deletions spec/actions/pro/create_paypal_agreement_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require 'rails_helper'

RSpec.describe Pro::CreatePaypalAgreement do
it 'should create an agreement and return the token' do
response = Pro::CreatePaypalAgreement.call(user: build(:user), tier: 'pro')
expect(response.token).to start_with('EC-')
end
end
13 changes: 13 additions & 0 deletions spec/actions/pro/subscribe_with_paypal_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
require 'rails_helper'

RSpec.describe Pro::SubscribeWithPayPal do
it 'should execute an agreement based on a token' do
agreement = instance_double(PayPal::SDK::REST::Agreement)
expect(agreement).to receive(:execute!)
allow(agreement).to receive(:id).and_return('IH8PAYPAL')
allow(PayPal::SDK::REST::Agreement).to receive(:new).and_return(agreement)

result = Pro::SubscribeWithPayPal.call(user: build(:user), tier: 'pro', token: 'POOP')
expect(result.subscription.billing_id).to eq('IH8PAYPAL')
end
end
20 changes: 20 additions & 0 deletions spec/models/pro_subscription/paypal_subscription_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require 'rails_helper'

RSpec.describe ProSubscription::PayPalSubscription, type: :model do
describe '#billing_service' do
it 'should return :paypal' do
expect(ProSubscription::PayPalSubscription.new.billing_service).to eq(:paypal)
end
end

context 'after destruction' do
it 'should cancel the agreement on PayPal' do
user = create(:user)
sub = ProSubscription::PayPalSubscription.create!(user: user, tier: 'pro', billing_id: 'POOP')
agreement = instance_double(PayPal::SDK::REST::Agreement)
allow(sub).to receive(:agreement).and_return(agreement)
expect(sub.agreement).to receive(:cancel!).once
sub.destroy!
end
end
end
3 changes: 2 additions & 1 deletion spec/support/webmock.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
'lorempixel.com',
'localhost',
'elasticsearch:9200',
'oembed.com'
'oembed.com',
'api.sandbox.paypal.com'
])

RSpec.shared_context 'MAL CDN' do
Expand Down