Skip to content

Commit

Permalink
Merge remote-tracking branch 'solidus/master' into fix_issue_133
Browse files Browse the repository at this point in the history
# Conflicts:
#	app/models/solidus_paypal_commerce_platform/payment_method.rb
#	spec/models/solidus_paypal_commerce_platform/payment_method_spec.rb
  • Loading branch information
retsef committed Dec 20, 2021
2 parents 1c0d333 + 00ec566 commit 591b9e4
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 48 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,27 @@ With product and cart page checkout, the user is directed to the checkout confir

PayPals API does not allow for admin-side payments. Instead, backend users taking payments for customers will need to use the PayPal Virtual Terminal to take payments. [More info is available on the PayPal website.](https://www.paypal.com/merchantapps/appcenter/acceptpayments/virtualterminal?locale.x=en_US)

## Venmo
Venmo is currently available to US merchants and buyers. There are also other [prequisites](https://developer.paypal.com/docs/business/checkout/pay-with-venmo/#eligibility).

If the transaction supports Venmo then a button should appear for it on checkout, cart and product page, depending on your `Payment Method` preferences.

By default, the extension and PayPal will try to render a Venmo button to buyers when prequisites are met.

Set the PaypalCommercePlatform `PaymentMethod` `venmo_control` preference to:
- `enforced`, disregard buyer's location and show button (if other prequisites are met); or
- `enabled` (default), available as a PayPal funding option (if other prequisites are met); or
- `disabled`, disable Venmo as a funding option.

See more about preferences([Configuration](#configuration)) below.

[_As Venmo is only available in the US, you may want to mock your location for testing_](#mocking-your-buyer-country)

## Configuration
The easiest way to change the `Payment Method`'s preferences is through admin: `Settings > Payments > "PayPal Commerce Platform" > Edit`.

See more about preferences [here](https://guides.solidus.io/developers/preferences/add-model-preferences.html#access-your-preferences)/

## Development

### Testing the extension
Expand Down Expand Up @@ -162,6 +183,12 @@ $ bin/rails server
Use Ctrl-C to stop
```

### Mocking your buyer country
PayPal normally looks at your IP geolocation to see where you are located to determine what funding sources are available to you. For example, Venmo is currently only available to US buyers.
Because of this, you may want to pretend you are from US check that that Venmo is correctly integrated for these customers. To do this, set the payment method's preference of `force_buyer_country` to "US". See more information about preferences above.

This preference has no effect on production.

### Updating the changelog

Before and after releases the changelog should be updated to reflect the up-to-date status of
Expand Down
14 changes: 14 additions & 0 deletions app/models/solidus_paypal_commerce_platform/payment_method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ class PaymentMethod < SolidusSupport.payment_method_parent_class
preference :display_on_cart, :boolean, default: true
preference :display_on_product_page, :boolean, default: true
preference :display_credit_messaging, :boolean, default: true
preference :venmo_control, :string, default: 'enabled'
preference :force_buyer_country, :string

validates :preferred_venmo_control, inclusion: {
in: %w[enforced enabled disabled],
message: "must be 'enforced', 'enabled' or 'disabled'."
}

def partial_name
"paypal_commerce_platform"
Expand Down Expand Up @@ -72,6 +79,13 @@ def javascript_sdk_url(order: nil, currency: nil)
currency: currency
}

parameters['enable-funding'] = 'venmo' if options[:venmo_control] == 'enforced'
parameters['disable-funding'] = 'venmo' if options[:venmo_control] == 'disabled'

if !Rails.env.production? && options[:force_buyer_country].present?
parameters['buyer-country'] = options[:force_buyer_country]
end

"https://www.paypal.com/sdk/js?#{parameters.to_query}"
end
end
Expand Down
4 changes: 3 additions & 1 deletion lib/solidus_paypal_commerce_platform/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class Client
request.headers["PayPal-Partner-Attribution-Id"] = SolidusPaypalCommercePlatform.config.partner_code
}.freeze

Response = Struct.new(:status_code, :error)

attr_reader :environment

def initialize(client_id:, client_secret: "", test_mode: nil)
Expand All @@ -29,7 +31,7 @@ def execute(request)
@paypal_client.execute(request)
rescue PayPalHttp::HttpError => e
Rails.logger.error e.result
OpenStruct.new(status_code: 422, error: e.result)
Response.new(status_code: 422, error: e.result)
end

def execute_with_response(request, success_message: nil, failure_message: nil)
Expand Down
6 changes: 3 additions & 3 deletions lib/solidus_paypal_commerce_platform/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def env
end

def default_env
return ENV['PAYPAL_ENV'] if ENV['PAYPAL_ENV'] # rubocop:disable Rails/EnvironmentVariableAccess
return ENV['PAYPAL_ENV'] if ENV['PAYPAL_ENV']

case Rails.env
when 'production'
Expand All @@ -60,11 +60,11 @@ def env_domain
end

def partner_id
@partner_id ||= ENV['PAYPAL_PARTNER_ID'] || DEFAULT_PARTNER_ID[env.to_sym] # rubocop:disable Rails/EnvironmentVariableAccess
@partner_id ||= ENV['PAYPAL_PARTNER_ID'] || DEFAULT_PARTNER_ID[env.to_sym]
end

def partner_client_id
@partner_client_id ||= ENV['PAYPAL_PARTNER_CLIENT_ID'] || DEFAULT_PARTNER_CLIENT_ID[env.to_sym] # rubocop:disable Rails/EnvironmentVariableAccess
@partner_client_id ||= ENV['PAYPAL_PARTNER_CLIENT_ID'] || DEFAULT_PARTNER_CLIENT_ID[env.to_sym]
end

def partner_code
Expand Down
3 changes: 2 additions & 1 deletion solidus_paypal_commerce_platform.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ Gem::Specification.new do |spec|
spec.metadata['homepage_uri'] = spec.homepage
spec.metadata['source_code_uri'] = 'https://github.com/solidusio-contrib/solidus_paypal_commerce_platform'
spec.metadata['changelog_uri'] = 'https://github.com/solidusio-contrib/solidus_paypal_commerce_platform/releases'
spec.metadata['rubygems_mfa_required'] = 'true'

spec.required_ruby_version = Gem::Requirement.new('>= 2.5')
spec.required_ruby_version = Gem::Requirement.new('~> 2.5')

# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
Expand Down
2 changes: 1 addition & 1 deletion spec/features/frontend/checkout_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
describe "paypal payment method" do
let(:order) { Spree::TestingSupport::OrderWalkthrough.up_to(:payment) }
let(:paypal_payment_method) { create(:paypal_payment_method) }
let(:failed_response) { OpenStruct.new(status_code: 500) }
let(:failed_response) { instance_double('response', status_code: 500) }

before do
user = create(:user)
Expand Down
56 changes: 28 additions & 28 deletions spec/lib/solidus_paypal_commerce_platform/configuration_spec.rb
Original file line number Diff line number Diff line change
@@ -1,67 +1,67 @@
require 'spec_helper'

RSpec.describe SolidusPaypalCommercePlatform::Configuration do
let(:subject) { described_class.new }
subject(:test_subject) { described_class.new }

describe '#default_env' do
it "uses ENV['PAYPAL_ENV'] when present" do
expect(ENV).to receive(:[]).with("PAYPAL_ENV").and_return("foo").at_least(:once)
expect(subject.default_env).to eq("foo")
expect(test_subject.default_env).to eq("foo")
end

it "falls back to Rails.env if ENV['PAYPAL_ENV'] is not set" do
expect(ENV).to receive(:[]).with("PAYPAL_ENV").and_return(nil).at_least(:once)

allow(Rails).to receive(:env).and_return("development".inquiry)
expect(subject.default_env).to eq("sandbox")
expect(test_subject.default_env).to eq("sandbox")

allow(Rails).to receive(:env).and_return("test".inquiry)
expect(subject.default_env).to eq("sandbox")
expect(test_subject.default_env).to eq("sandbox")

allow(Rails).to receive(:env).and_return("production".inquiry)
expect(subject.default_env).to eq("live")
expect(test_subject.default_env).to eq("live")

allow(Rails).to receive(:env).and_return("staging".inquiry)
expect{ subject.default_env }.to raise_error(described_class::InvalidEnvironment)
expect{ test_subject.default_env }.to raise_error(described_class::InvalidEnvironment)
end
end

describe '#env' do
it 'returns a string inquirer' do
expect(subject.env).to eq("sandbox")
expect(subject.env).to be_sandbox
expect(test_subject.env).to eq("sandbox")
expect(test_subject.env).to be_sandbox

subject.env = "live"
expect(subject.env).to eq("live")
expect(subject.env).to be_live
test_subject.env = "live"
expect(test_subject.env).to eq("live")
expect(test_subject.env).to be_live

subject.env = "sandbox"
expect(subject.env).to eq("sandbox")
expect(subject.env).to be_sandbox
test_subject.env = "sandbox"
expect(test_subject.env).to eq("sandbox")
expect(test_subject.env).to be_sandbox
end

it 'raises an error when assigned an unsupported value' do
expect{ subject.env = "foo" }.to raise_error(described_class::InvalidEnvironment)
expect{ test_subject.env = "foo" }.to raise_error(described_class::InvalidEnvironment)
end
end

describe '#env_class' do
it 'changes based on the current env' do
subject.env = "live"
expect(subject.env_class).to eq(PayPal::LiveEnvironment)
test_subject.env = "live"
expect(test_subject.env_class).to eq(PayPal::LiveEnvironment)

subject.env = "sandbox"
expect(subject.env_class).to eq(PayPal::SandboxEnvironment)
test_subject.env = "sandbox"
expect(test_subject.env_class).to eq(PayPal::SandboxEnvironment)
end
end

describe '#env_domain' do
it 'changes based on the current env' do
subject.env = "live"
expect(subject.env_domain).to eq("www.paypal.com")
test_subject.env = "live"
expect(test_subject.env_domain).to eq("www.paypal.com")

subject.env = "sandbox"
expect(subject.env_domain).to eq("www.sandbox.paypal.com")
test_subject.env = "sandbox"
expect(test_subject.env_domain).to eq("www.sandbox.paypal.com")
end
end

Expand All @@ -71,21 +71,21 @@
end

it "returns a class" do
expect(subject.state_guesser_class).to be_kind_of(Class)
expect(test_subject.state_guesser_class).to be_kind_of(Class)
end

it "is settable" do
expect(subject.state_guesser_class).to eq(SolidusPaypalCommercePlatform::StateGuesser)
expect(test_subject.state_guesser_class).to eq(SolidusPaypalCommercePlatform::StateGuesser)

subject.state_guesser_class = "SolidusPaypalCommercePlatform::BetterStateGuesser"
test_subject.state_guesser_class = "SolidusPaypalCommercePlatform::BetterStateGuesser"

expect(subject.state_guesser_class).to eq(SolidusPaypalCommercePlatform::BetterStateGuesser)
expect(test_subject.state_guesser_class).to eq(SolidusPaypalCommercePlatform::BetterStateGuesser)
end
end

describe "#partner_code" do
it "returns the correct code" do
expect(subject.partner_code).to eq("Solidus_PCP_SP")
expect(test_subject.partner_code).to eq("Solidus_PCP_SP")
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -77,40 +77,120 @@ def Struct(data) # rubocop:disable Naming/MethodName
describe '.javascript_sdk_url' do
subject(:url) { URI(paypal_payment_method.javascript_sdk_url(order: order)) }

context 'when checkout_steps include "confirm"' do
let(:order) { instance_double(Spree::Order, checkout_steps: { "confirm" => "bar" }) }
let(:order) { build_stubbed(:order) }

context 'when checkout_steps include "confirm"' do
it 'sets autocommit' do
expect(url.query.split("&")).to include("commit=false")
end
end

context 'when checkout_steps does not include "confirm"' do
let(:order) { instance_double(Spree::Order, checkout_steps: { "foo" => "bar" }) }

it 'disables autocommit' do
allow(order).to receive(:checkout_steps).and_return([:address, :delivery, :payment])
expect(url.query.split("&")).to include("commit=true")
end
end

context 'when messaging is turned on' do
let(:order) { instance_double(Spree::Order, checkout_steps: { "foo" => "bar" }) }

it 'includes messaging component' do
paypal_payment_method.preferences.update(display_credit_messaging: true)
expect(url.query.split("&")).to include("components=buttons%2Cmessages")
end
end

context 'when messaging is turned off' do
let(:order) { instance_double(Spree::Order, checkout_steps: { "foo" => "bar" }) }

it 'only includes buttons components' do
paypal_payment_method.preferences.update(display_credit_messaging: false)
expect(url.query.split("&")).not_to include("messages")
expect(url.query.split("&")).to include("components=buttons")
end
end

describe 'preferences' do
context 'with venmo_control' do
it 'is "default" by default' do
expect(paypal_payment_method.preferred_venmo_control).to eq('enabled')
end

it 'only allows "enforced", "enabled" and "disabled" as values', :aggregate_failures do
expect(paypal_payment_method).to be_valid
valid_values = %w[enforced enabled disabled]
invalid_values = %w[foo bar]

valid_values.each do |value|
paypal_payment_method.preferred_venmo_control = value
expect(paypal_payment_method).to be_valid
end

invalid_values.each do |value|
paypal_payment_method.preferred_venmo_control = value
expect(paypal_payment_method).to be_invalid
end
end
end
end

context 'when venmo_control is "enforced"' do
before { paypal_payment_method.preferences.update(venmo_control: 'enforced') }

it 'includes "enable-funding=venmo" as a parameter' do
expect(url.query.split('&')).to include('enable-funding=venmo')
end

it 'does not include the "disable-funding" parameter' do
expect(url.query.split('&')).not_to include(match 'disable-funding')
end
end

context 'when venmo_control is "enabled"' do
before { paypal_payment_method.preferences.update(venmo_control: 'enabled') }

it 'include the "enable-funding" parameter' do
expect(url.query.split('&')).not_to include(match 'enable-funding')
end

it 'does not include the "disable-funding" parameter' do
expect(url.query.split('&')).not_to include(match 'disable-funding')
end
end

context 'when venmo_control is "disabled"' do
before { paypal_payment_method.preferences.update(venmo_control: 'disabled') }

it 'does not include the "enable-funding" parameter' do
expect(url.query.split('&')).not_to include(match 'enable-funding')
end

it 'includes "disable-funding=venmo" as a parameter' do
expect(url.query.split('&')).to include('disable-funding=venmo')
end
end

context 'when force_buyer_country is an empty string' do
it 'does not include the "buyer-country" parameter' do
expect(url.query.split('&')).not_to include(match 'buyer-country')
end
end

context 'when force_buyer_country is "US"' do
before { paypal_payment_method.preferences.update(force_buyer_country: 'US') }

it 'includes "buyer-country=US" as a parameter' do
expect(url.query.split('&')).to include('buyer-country=US')
end
end

context 'when force_buyer_country is "US" but the environment is production' do
before {
allow(Rails.env).to receive(:production?).and_return(true)
paypal_payment_method.preferences.update(force_buyer_country: 'US')
}

it 'includes "buyer-country=US" as a parameter' do
expect(url.query.split('&')).not_to include(match 'buyer-country')
end
end
end

private
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
before do
allow_any_instance_of(SolidusPaypalCommercePlatform::Client).to receive(:execute) do |_client, request|
expect(request).to be_a(SolidusPaypalCommercePlatform::Gateway::OrdersGetRequest) # rubocop:disable RSpec/ExpectInHook
OpenStruct.new(result: OpenStruct.new(status: paypal_order_status))
instance_double(
'response',
result: instance_double('result', status: paypal_order_status)
)
end
end

Expand Down
Loading

0 comments on commit 591b9e4

Please sign in to comment.