diff --git a/app/assets/javascripts/spree/frontend/solidus_paypal_commerce_platform.js b/app/assets/javascripts/spree/frontend/solidus_paypal_commerce_platform.js index a79f2e94..cb192765 100644 --- a/app/assets/javascripts/spree/frontend/solidus_paypal_commerce_platform.js +++ b/app/assets/javascripts/spree/frontend/solidus_paypal_commerce_platform.js @@ -1,2 +1,2 @@ -// Placeholder manifest file. -// the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/frontend/all.js' \ No newline at end of file +//= require spree/frontend/solidus_paypal_commerce_platform/namespace +//= require spree/frontend/solidus_paypal_commerce_platform/buttons diff --git a/app/assets/javascripts/spree/frontend/solidus_paypal_commerce_platform/buttons.js b/app/assets/javascripts/spree/frontend/solidus_paypal_commerce_platform/buttons.js new file mode 100644 index 00000000..0bcc670e --- /dev/null +++ b/app/assets/javascripts/spree/frontend/solidus_paypal_commerce_platform/buttons.js @@ -0,0 +1,21 @@ +SolidusPaypalCommercePlatform.renderButton = function(payment_method_id) { + paypal.Buttons({ + createOrder: function (data, actions) { + return Spree.ajax({ + url: '/solidus_paypal_commerce_platform/orders', + method: 'POST', + data: { + payment_method_id: payment_method_id, + order_id: Spree.current_order_id, + order_token: Spree.current_order_token + } + }).then(function(res) { + return res.table.id; + }) + }, + onApprove: function (data, actions) { + $("#payments_source_paypal_order_id").val(data.orderID) + $("#checkout_form_payment").submit() + } + }).render('#paypal-button-container') +} diff --git a/app/assets/javascripts/spree/frontend/solidus_paypal_commerce_platform/namespace.js b/app/assets/javascripts/spree/frontend/solidus_paypal_commerce_platform/namespace.js new file mode 100644 index 00000000..43c38cc7 --- /dev/null +++ b/app/assets/javascripts/spree/frontend/solidus_paypal_commerce_platform/namespace.js @@ -0,0 +1 @@ +window.SolidusPaypalCommercePlatform = {} diff --git a/app/controllers/solidus_paypal_commerce_platform/orders_controller.rb b/app/controllers/solidus_paypal_commerce_platform/orders_controller.rb new file mode 100644 index 00000000..6158fa66 --- /dev/null +++ b/app/controllers/solidus_paypal_commerce_platform/orders_controller.rb @@ -0,0 +1,26 @@ +module SolidusPaypalCommercePlatform + class OrdersController < ::Spree::Api::BaseController + before_action :load_order + before_action :load_payment_method + skip_before_action :authenticate_user + + def create + authorize! :update, @order, order_token + request = SolidusPaypalCommercePlatform::Requests.new( + @payment_method.client_id, @payment_method.client_secret + ).create_order(@order, @payment_method.auto_capture) + render json: request, status: :ok + end + + private + + def load_order + @order = Spree::Order.find_by!(number: params[:order_id]) + end + + def load_payment_method + @payment_method = Spree::PaymentMethod.find(params[:payment_method_id]) + end + + end +end diff --git a/app/controllers/solidus_paypal_commerce_platform/payments_controller.rb b/app/controllers/solidus_paypal_commerce_platform/payments_controller.rb new file mode 100644 index 00000000..e4881fde --- /dev/null +++ b/app/controllers/solidus_paypal_commerce_platform/payments_controller.rb @@ -0,0 +1,43 @@ +module SolidusPaypalCommercePlatform + class PaymentsController < ::Spree::Api::BaseController + before_action :load_order + skip_before_action :authenticate_user + + def create + authorize! :update, @order, order_token + paypal_order_id = paypal_params[:paypal_order_id] + + if !paypal_order_id + return redirect_to checkout_state_path(@order.state), notice: "Invalid order confirmation data passed in" + end + + if @order.complete? + return redirect_to spree.order_path(@order), notice: "Order is already in complete state" + end + + source = SolidusPaypalCommercePlatform::Source.new(paypal_order_id: paypal_order_id) + + source.transaction do + if source.save! + payment = @order.payments.create!({ + payment_method_id: paypal_params[:payment_method_id], + source: source + }) + + render json: {}, status: :ok + end + end + end + + private + + def paypal_params + params.permit(:paypal_order_id, :order_id, :order_token, :payment_method_id) + end + + def load_order + @order = Spree::Order.find_by!(number: params[:order_id]) + end + + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 94ce9ecd..2c69e386 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,5 +1,8 @@ en: activerecord: + attributes: + solidus_paypal_commerce_platform/source: + paypal_order_id: "" models: solidus_paypal_commerce_platform/gateway: Paypal Commerce Platform hello: Hello world diff --git a/config/routes.rb b/config/routes.rb index 62d3bd56..6264bec5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,5 +3,6 @@ SolidusPaypalCommercePlatform::Engine.routes.draw do # Add your extension routes here resources :wizard, only: [:create] - # post :paypal_wizard, to: "wizard#create" + resources :orders, only: [:create] + resources :payments, only: [:create] end diff --git a/lib/views/frontend/spree/checkout/payment/_paypal_commerce_platform.html.erb b/lib/views/frontend/spree/checkout/payment/_paypal_commerce_platform.html.erb new file mode 100644 index 00000000..7669af48 --- /dev/null +++ b/lib/views/frontend/spree/checkout/payment/_paypal_commerce_platform.html.erb @@ -0,0 +1,12 @@ + + +
+ + + diff --git a/spec/factories/paypal_payment_method_factory.rb b/spec/factories/paypal_payment_method_factory.rb new file mode 100644 index 00000000..6649e89d --- /dev/null +++ b/spec/factories/paypal_payment_method_factory.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :paypal_payment_method, class: 'SolidusPaypalCommercePlatform::Gateway' do + type { "SolidusPaypalCommercePlatform::Gateway" } + name { "PayPal Payment Method" } + preferences { + { + client_id: SecureRandom.hex(8), + client_secret: SecureRandom.hex(10) + } + } + end +end \ No newline at end of file diff --git a/spec/features/frontend/checkout_spec.rb b/spec/features/frontend/checkout_spec.rb new file mode 100644 index 00000000..05cff6c4 --- /dev/null +++ b/spec/features/frontend/checkout_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +RSpec.describe "Checkout" do + + context "paypal payment method" do + let(:order) { Spree::TestingSupport::OrderWalkthrough.up_to(:payment) } + let(:paypal_payment_method) { create(:paypal_payment_method) } + + before do + user = create(:user) + order.user = user + order.recalculate + + paypal_payment_method + allow_any_instance_of(Spree::CheckoutController).to receive_messages(current_order: order, try_spree_current_user: user) + end + + it "should generate a js file with the correct credentials and intent attached" do + visit '/checkout/payment' + expect(page).to have_css( + 'script[src*="sdk/js?client-id=' + paypal_payment_method.preferences[:client_id] + '&intent=authorize"]', visible: false + ) + end + + context "when auto-capture is set to true" do + it "should generate a js file with intent capture" do + paypal_payment_method.update(auto_capture: true) + visit '/checkout/payment' + expect(page).to have_css( + 'script[src*="sdk/js?client-id=' + paypal_payment_method.preferences[:client_id] + '&intent=capture"]', visible: false + ) + end + end + + context "if no payment has been made" do + it "should fail to process" do + visit '/checkout/payment' + choose(option: paypal_payment_method.id) + click_button("Save and Continue") + expect(page).to have_content("Payments source can't be blank") + end + end + + context "if payment has been made" do + it "should proceed to the next step" do + visit '/checkout/payment' + choose(option: paypal_payment_method.id) + find(:xpath, "//input[@id='payments_source_paypal_order_id']", visible: false).set SecureRandom.hex(8) + click_button("Save and Continue") + expect(page).to have_css(".current", text: "Confirm") + end + end + + end +end diff --git a/spec/models/gateway_spec.rb b/spec/models/gateway_spec.rb new file mode 100644 index 00000000..a5600894 --- /dev/null +++ b/spec/models/gateway_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' +require 'paypal-checkout-sdk' + +RSpec.describe "SolidusPaypalCommercePlatform::Gateway", type: :model do + let(:paypal_payment_method) { create(:paypal_payment_method) } + let(:payment) { create(:payment) } + + before do + response = OpenStruct.new( + status_code: 201, + result: OpenStruct.new( + purchase_units: [ + OpenStruct.new( + payments: OpenStruct.new( + authorizations: [ + OpenStruct.new(id: SecureRandom.hex(4)) + ] + ) + ) + ] + ) + ) + + allow_any_instance_of(PayPal::PayPalHttpClient).to receive(:execute) { response } + allow_any_instance_of(PayPal::SandboxEnvironment).to receive(:authorizationString) { "test auth" } + end + + context "#purchase" do + it "should send a purchase request to paypal" do + paypal_order_id = SecureRandom.hex(8) + source = paypal_payment_method.payment_source_class.create(paypal_order_id: paypal_order_id) + request = { + path: "/v2/checkout/orders/#{paypal_order_id}/capture", + headers: { + "Content-Type" => "application/json", + "Authoriation" => "test auth", + "PayPal-Partner-Attribution-Id" => "Solidus_PCP_SP", + }, + verb: "POST" + } + expect(SolidusPaypalCommercePlatform::Requests::Request).to receive(:new).with(request) + paypal_payment_method.purchase(1000,source,{}) + end + end + + context "#authorize" do + it "should send an authorize request to paypal" do + paypal_order_id = SecureRandom.hex(8) + source = paypal_payment_method.payment_source_class.create(paypal_order_id: paypal_order_id) + request = { + path: "/v2/checkout/orders/#{paypal_order_id}/authorize", + headers: { + "Content-Type" => "application/json", + "Authoriation" => "test auth", + "PayPal-Partner-Attribution-Id" => "Solidus_PCP_SP", + }, + verb: "POST" + } + expect(SolidusPaypalCommercePlatform::Requests::Request).to receive(:new).with(request) + paypal_payment_method.authorize(1000,source,{}) + end + end + + context "#capture" do + it "should send a capture request to paypal" do + authorization_id = SecureRandom.hex(8) + source = paypal_payment_method.payment_source_class.create(authorization_id: authorization_id) + payment.source = source + request = { + path: "/v2/payments/authorizations/#{authorization_id}/capture", + headers: { + "Content-Type" => "application/json", + "Authoriation" => "test auth", + "PayPal-Partner-Attribution-Id" => "Solidus_PCP_SP", + }, + verb: "POST" + } + expect(SolidusPaypalCommercePlatform::Requests::Request).to receive(:new).with(request) + paypal_payment_method.capture(1000,{},{originator: payment}) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 95edb125..6311d5a2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -10,6 +10,7 @@ # Requires factories and other useful helpers defined in spree_core. require 'solidus_dev_support/rspec/feature_helper' +require 'spree/testing_support/order_walkthrough' # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories.