diff --git a/app/controllers/spree/api/adyen_redirect_controller.rb b/app/controllers/spree/api/adyen_redirect_controller.rb new file mode 100644 index 00000000..d61189cc --- /dev/null +++ b/app/controllers/spree/api/adyen_redirect_controller.rb @@ -0,0 +1,96 @@ +module Spree + module Api + class AdyenRedirectController < Spree::Api::BaseController + before_action :find_order + around_action :lock_order + before_action :find_payment_method + before_filter :check_signature, only: :confirm + + def confirm + if @order.complete? + confirm_order_already_completed + else + confirm_order_incomplete + end + end + + private + + def confirm_order_already_completed + if psp_reference + payment = @order.payments.find_by!(response_code: psp_reference) + else + payment = @order.payments.where(source_type: "Spree::Adyen::HppSource").last + end + + payment.source.update(permitted_params) + + respond_with(@order, default_template: 'spree/api/orders/show', status: :ok) + end + + def confirm_order_incomplete + source = Adyen::HppSource.new(permitted_params) + + return handle_failure unless source.authorised? + + @order.payments.create!( + amount: @order.total, + payment_method: @payment_method, + source: source, + response_code: psp_reference, + state: "checkout" + ) + + if complete + respond_with(@order, default_template: 'spree/api/orders/show', status: :ok) + else + invalid_resource!(@order) + end + end + + def handle_failure + render json: { + error: I18n.t(:invalid_resource, scope: "spree.api"), + errors: { source: Spree.t(:payment_processing_failed) } + }, status: 422 + end + + def find_order + @order = Spree::Order.find_by!(number: order_id) + authorize! :read, @order, order_token + end + + def find_payment_method + _, payment_method_id = params[:merchantReturnData].split("|") + @payment_method = Spree::PaymentMethod.find_by!(id: payment_method_id) + end + + def check_signature + unless ::Adyen::HPP::Signature.verify(permitted_params, @payment_method.shared_secret) + raise "Payment Method not found." + end + end + + def permitted_params + params.permit( + :authResult, + :merchantReference, + :merchantReturnData, + :merchantSig, + :paymentMethod, + :pspReference, + :shopperLocale, + :skinCode) + end + + def complete + @order.contents.advance + @order.complete + end + + def psp_reference + params[:pspReference] + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 02c7569e..c41a4f22 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,6 +7,10 @@ end end + namespace :api, defaults: { format: 'json' } do + post "/checkouts/:order_id/payment/adyen", to: "adyen_redirect#confirm" + end + get "checkout/payment/adyen", to: "adyen_redirect#confirm", as: :adyen_confirmation post "adyen/notify", to: "adyen_notifications#notify" post "adyen/authorise3d", to: "adyen_redirect#authorise3d", as: :adyen_authorise3d diff --git a/spec/controllers/spree/api/adyen_redirect_controller_spec.rb b/spec/controllers/spree/api/adyen_redirect_controller_spec.rb new file mode 100644 index 00000000..c29e8861 --- /dev/null +++ b/spec/controllers/spree/api/adyen_redirect_controller_spec.rb @@ -0,0 +1,147 @@ +require "spec_helper" + +RSpec.describe Spree::Api::AdyenRedirectController, type: :controller do + include_context "mock adyen client", success: true + + let(:order) do + create( + :order_with_line_items, + state: "payment", + store: store + ) + end + + let!(:store) { create :store } + let!(:gateway) { create :hpp_gateway } + + before do + allow_any_instance_of(Spree::Ability).to receive(:can?).and_return(true) + + stub_authentication! + + allow(controller).to receive(:check_signature) + + allow(Spree::Order).to receive(:find_by!). + with(number: order.number). + and_return(order) + end + + subject { } + + describe "confirm" do + subject(:action) { post :confirm, params.reverse_merge!({ format: :json }) } + + let(:psp_reference) { "8813824003752247" } + let(:payment_method) { "amex" } + let(:merchantReturnData) { "#{order.guest_token}|#{gateway.id}" } + let(:params) do + { order_id: order.number, + merchantReference: order.number, + skinCode: "xxxxxxxx", + shopperLocale: "en_GB", + paymentMethod: payment_method, + authResult: auth_result, + pspReference: psp_reference, + merchantSig: "erewrwerewrewrwer", + merchantReturnData: merchantReturnData + } + end + + shared_examples "api payment is successful" do + it "changes the order state to completed" do + subject + order.reload + expect(order).to have_attributes( + state: "complete", + payment_state: "balance_due", + shipment_state: "pending" + ) + end + + it "has pending payments" do + expect(order.payments).to all be_pending + end + + it "renders an order in complete state" do + is_expected.to have_http_status(:ok) + end + + it "creates a payment" do + subject + expect(order.reload.payments.count).to eq 1 + end + + context "and the order cannot complete" do + before do + expect(order).to receive(complete).and_return(false) + end + + it "voids the payment" + end + end + + shared_examples "api payment is not successful" do + it "does not change order state" do + expect{ subject }.to_not change{ order.state } + end + + it "redirects to the order payment page" do + is_expected.to have_http_status(422) + end + end + + context "when the payment is AUTHORISED" do + include_examples "api payment is successful" + + let(:auth_result) { "AUTHORISED" } + + context "and the authorisation notification has already been received" do + let(:payment_method) { notification.payment_method } + + let(:notification) do + create( + :notification, + :auth, + processed: true, + psp_reference: psp_reference, + merchant_reference: order.number) + end + + let(:source) { create(:hpp_source, psp_reference: psp_reference, order: order) } + + before do + create(:hpp_payment, source: source, order: order) + + order.contents.advance + order.complete + end + + it { expect { subject }.to_not change { order.payments.count }.from 1 } + + it "updates the source" do + expect(order.payments.last.source).to have_attributes( + auth_result: "AUTHORISED", + skin_code: "XXXXXXXX" + ) + end + + include_examples "api payment is successful" + end + end + + context "when the payment is PENDING" do + include_examples "api payment is successful" + let(:auth_result) { "PENDING" } + end + + context "when the payment is CANCELLED" do + include_examples "api payment is not successful" + let(:auth_result) { "CANCELLED" } + end + + context "when the payment is REFUSED" do + include_examples "api payment is not successful" + let(:auth_result) { "REFUSED" } + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 12d8fcd9..966b7982 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -29,6 +29,8 @@ require "spree/testing_support/controller_requests" require "spree/testing_support/url_helpers" require "spree/testing_support/authorization_helpers" +require "spree/api/testing_support/helpers" +require "spree/api/testing_support/setup" # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. @@ -54,6 +56,16 @@ config.include FactoryGirl::Syntax::Methods config.include Spree::TestingSupport::ControllerRequests, type: :controller config.include Spree::TestingSupport::UrlHelpers + config.include Spree::Api::TestingSupport::Helpers, type: :controller + config.include Spree::Api::TestingSupport::Setup, type: :controller + + if Object.const_defined?('VersionCake') + config.include VersionCake::TestHelpers, type: :controller + + config.before(:each, type: :controller) do + set_request_version('', 1) + end + end config.before(:suite) do DatabaseCleaner.clean_with(:truncation)