diff --git a/app/controllers/spree/adyen_redirect_controller.rb b/app/controllers/spree/adyen_redirect_controller.rb index 60fc9ab..1f51fe6 100644 --- a/app/controllers/spree/adyen_redirect_controller.rb +++ b/app/controllers/spree/adyen_redirect_controller.rb @@ -5,34 +5,31 @@ class AdyenRedirectController < StoreController skip_before_filter :verify_authenticity_token def confirm - order = current_order - unless authorized? + @payment = current_order.payments.find_by_identifier(extract_payment_identifier_from_merchant_reference(params[:merchantReference])) + @payment.response_code = params[:pspReference] + + if authorized? + @payment.pend + @payment.save + elsif pending? + # Leave in payment in processing state and wait for update from Notification + @payment.save + else + @payment.failure + @payment.save + flash.notice = Spree.t(:payment_processing_failed) - redirect_to checkout_state_path(order.state) and return + redirect_to checkout_state_path(current_order.state) and return end - # cant set payment to complete here due to a validation - # in order transition from payment to complete (it requires at - # least one pending payment) - payment = order.payments.create!( - :amount => order.total, - :payment_method => payment_method, - :response_code => params[:pspReference] - ) + current_order.next - order.next - - if order.complete? - flash.notice = Spree.t(:order_processed_successfully) - redirect_to order_path(order, :token => order.guest_token) - else - redirect_to checkout_state_path(order.state) - end + redirect_to redirect_path and return end + def authorise3d - order = current_order if params[:MD].present? && params[:PaRes].present? md = params[:MD] @@ -43,64 +40,57 @@ def authorise3d response3d = gateway.authorise3d(md, pa_response, request.ip, request.headers.env) + @payment = current_order.payments.find_by_identifier(session[:payment_identifier]) + @payment.response_code = response3d.psp_reference + if response3d.success? - payment = order.payments.create!( - :amount => order.total, - :payment_method => gateway, - :response_code => response3d.psp_reference - ) - - list = gateway.provider.list_recurring_details(order.user_id.present? ? order.user_id : order.email) - - if list.details && list.details.empty? - flash.notice = "Could not find any recurring details" - redirect_to checkout_state_path(order.state) and return - else - credit_card = Spree::CreditCard.create! do |cc| - cc.month = list.details.last[:card][:expiry_date].month - cc.year = list.details.last[:card][:expiry_date].year - cc.name = list.details.last[:card][:holder_name] - cc.cc_type = list.details.last[:variant] - cc.last_digits = list.details.last[:card][:number] - cc.gateway_customer_profile_id = list.details.last[:recurring_detail_reference] - end - end - - # Avoid this payment from being processed and so authorised again - # once the order transitions to complete state. - # See Spree::Order::Checkout for transition events - payment.started_processing! - - # We want to avoid callbacks such as Payment#create_payment_profile on after_save - payment.update_columns source_id: credit_card.id, source_type: credit_card.class.name - - order.next - - if order.complete? - flash.notice = Spree.t(:order_processed_successfully) - redirect_to order_path(order, :token => order.guest_token) - else - if order.errors.any? - flash.notice = order.errors.full_messages.inspect - end - - redirect_to checkout_state_path(order.state) - end + @payment.pend + @payment.save + current_order.next else - flash.notice = response3d.error.inspect - redirect_to checkout_state_path(order.state) + @payment.failure + @payment.save + flash.notice = Spree.t(:payment_processing_failed) end - else - redirect_to checkout_state_path(order.state) + end + + # Update iframe and redirect parent to checkout state + render partial: 'spree/shared/reload_parent', locals: { + new_url: redirect_path + } + end private + def pending? + params[:authResult] == 'PENDING' + end + + def extract_payment_identifier_from_merchant_reference(merchant_reference) + merchant_reference.split('-').last + end + def authorized? params[:authResult] == "AUTHORISED" end + def redirect_path + if current_order.completed? + cookies[:completed_order] = current_order.id + @current_order = nil + flash.notice = Spree.t(:order_processed_successfully) + completion_route + else + checkout_state_path(current_order.state) + end + end + + def completion_route + spree.checkout_complete_path + end + def check_signature unless ::Adyen::Form.redirect_signature_check(params, payment_method.shared_secret) raise "Payment Method not found." @@ -110,7 +100,9 @@ def check_signature # TODO find a way to send the payment method id to Adyen servers and get # it back here to make sure we find the right payment method def payment_method - @payment_method ||= Gateway::AdyenHPP.last # find(params[:merchantReturnData]) + @payment_method = current_order.available_payment_methods.find do |m| + m.is_a?(Spree::Gateway::AdyenHPP) && m.environment == Rails.env + end end end diff --git a/app/models/adyen_notification.rb b/app/models/adyen_notification.rb index eb143ce..2d3f232 100644 --- a/app/models/adyen_notification.rb +++ b/app/models/adyen_notification.rb @@ -37,13 +37,14 @@ class AdyenNotification < ActiveRecord::Base # @raise This method will raise an exception if the notification cannot be stored. # @see Adyen::Notification::HttpPost.log def self.log(params) + converted_params = {} # Assign explicit each attribute from CamelCase notation to notification # For example, merchantReference will be converted to merchant_reference self.new.tap do |notification| params.each do |key, value| - setter = "#{key.to_s.underscore}=" + setter = "#{key.to_s.gsub('.', '_').underscore}=" notification.send(setter, value) if notification.respond_to?(setter) end notification.save! @@ -73,13 +74,32 @@ def successful_authorisation? alias_method :successful_authorization?, :successful_authorisation? - # Invalidate payments that doesnt receive a successful notification +# Invalidate payments that doesnt receive a successful notification def handle! - if (authorisation? || capture?) && !success? + + if (authorisation? || capture?) + payment = Spree::Payment.find_by(response_code: psp_reference) - if payment && !payment.failed? && !payment.invalid? - payment.invalidate! + + store_profile_from_alias payment if payment.present? + + return unless payment && payment.processing? + + if success? && capture_available? + payment.pend + elsif success? + payment.complete + else + payment.failure end end end + + def store_profile_from_alias(payment) + payment.source.update_attribute :gateway_customer_profile_id, additional_data_alias + end + + def capture_available? + !!operations['CAPTURE'] + end end diff --git a/app/models/spree/adyen_common.rb b/app/models/spree/adyen_common.rb index 55f4e02..2bbc852 100644 --- a/app/models/spree/adyen_common.rb +++ b/app/models/spree/adyen_common.rb @@ -4,6 +4,7 @@ module AdyenCommon class RecurringDetailsNotFoundError < StandardError; end class MissingCardSummaryError < StandardError; end + class MissingAliasError < StandardError; end included do preference :api_username, :string @@ -28,7 +29,7 @@ def provider # NOTE Override this with your custom logic for scenarios where you don't # want to redirect customer to 3D Secure auth - def require_3d_secure?(payment) + def require_3d_secure?(amount, source, gateway_options) true end @@ -42,7 +43,7 @@ def capture(amount, response_code, gateway_options = {}) response = provider.capture_payment(response_code, value) if response.success? - def response.authorization; psp_reference; end + def response.authorization; nil; end def response.avs_result; {}; end def response.cvv_result; {}; end else @@ -62,7 +63,7 @@ def void(response_code, source, gateway_options = {}) response = provider.cancel_payment(response_code) if response.success? - def response.authorization; psp_reference; end + def response.authorization; nil; end else # TODO confirm the error response will always have these two methods def response.to_s @@ -77,7 +78,7 @@ def credit(credit_cents, source, response_code, gateway_options) response = provider.refund_payment response_code, amount if response.success? - def response.authorization; psp_reference; end + def response.authorization; nil; end else def response.to_s refusal_reason @@ -110,12 +111,12 @@ def authorise3d(md, pa_response, ip, env) provider.authorise3d_payment(md, pa_response, ip, browser_info) end - def build_authorise_details(payment) - if payment.request_env.is_a?(Hash) && require_3d_secure?(payment) + def build_authorise_options(amount, source, gateway_options) + if gateway_options[:request_env].is_a?(Hash) && require_3d_secure?(amount, source, gateway_options) { browser_info: { - accept_header: payment.request_env['HTTP_ACCEPT'], - user_agent: payment.request_env['HTTP_USER_AGENT'] + accept_header: gateway_options[:request_env]['HTTP_ACCEPT'], + user_agent: gateway_options[:request_env]['HTTP_USER_AGENT'] }, recurring: true } @@ -158,6 +159,8 @@ def set_up_contract(source, card, user, shopper_ip) def authorize_on_card(amount, source, gateway_options, card, options = { recurring: false }) reference = gateway_options[:order_id] + options = build_authorise_options(amount, source, gateway_options) + amount = { currency: gateway_options[:currency], value: amount } shopper_reference = if gateway_options[:customer_id].present? @@ -173,6 +176,8 @@ def authorize_on_card(amount, source, gateway_options, card, options = { recurri response = decide_and_authorise reference, amount, shopper, source, card, options + raise Adyen::Enrolled3DError.new(response, self, gateway_options) if response.respond_to?(:enrolled_3d?) && response.enrolled_3d? + # Needed to make the response object talk nicely with Spree payment/processing api if response.success? def response.authorization; psp_reference; end @@ -197,8 +202,8 @@ def decide_and_authorise(reference, amount, shopper, source, card, options) if require_one_click_payment?(source, shopper) && recurring_detail_reference.present? provider.authorise_one_click_payment reference, amount, shopper, card_cvc, recurring_detail_reference - elsif source.gateway_customer_profile_id.present? - provider.authorise_recurring_payment reference, amount, shopper, source.gateway_customer_profile_id + elsif recurring_detail_reference.present? + provider.authorise_recurring_payment reference, amount, shopper, recurring_detail_reference else provider.authorise_payment reference, amount, shopper, card, options end @@ -212,12 +217,18 @@ def create_profile_on_card(payment, card) :ip => payment.order.last_ip_address, :statement => "Order # #{payment.order.number}" } - amount = build_amount_on_profile_creation payment - options = build_authorise_details payment + # Auth for 0. Adyen will automatically update this to 1 if 0 not supported by Issuer + amount = 0 + + # Still build options as payment may requre 3D Secure + options = build_authorise_details payment.gateway_options - response = provider.authorise_payment payment.order.number, amount, shopper, card, options + response = provider.authorise_payment payment.gateway_options[:order_id], amount, shopper, card, options if response.success? + + store_profile_from_alias(payment, response) + if payment.source.last_digits.blank? last_digits = response.additional_data["cardSummary"] if last_digits.blank? && payment_profiles_supported? @@ -225,17 +236,9 @@ def create_profile_on_card(payment, card) Please request last digits to be sent back to support payment profiles" raise Adyen::MissingCardSummaryError, note end - payment.source.last_digits = last_digits end - fetch_and_update_contract payment.source, shopper[:reference] - - # Avoid this payment from being processed and so authorised again - # once the order transitions to complete state. - # See Spree::Order::Checkout for transition events - payment.started_processing! - elsif response.respond_to?(:enrolled_3d?) && response.enrolled_3d? raise Adyen::Enrolled3DError.new(response, payment.payment_method) else @@ -248,6 +251,16 @@ def create_profile_on_card(payment, card) end end + def store_profile_from_alias(payment, response) + customer_profile_id = response.additional_data["alias"] + if customer_profile_id.blank? && payment_profiles_supported? + note = "Payment was authorized but could not fetch alias (customer_profile_id). + Please request alias to be sent back to support payment profiles" + raise Adyen::MissingAliasError, note + end + payment.source.update_attribute :gateway_customer_profile_id, response.additional_data["alias"] + end + def fetch_and_update_contract(source, shopper_reference) list = provider.list_recurring_details(shopper_reference) @@ -264,6 +277,7 @@ def fetch_and_update_contract(source, shopper_reference) gateway_customer_profile_id: card[:recurring_detail_reference] ) end + end module ClassMethods diff --git a/app/models/spree/alternative_payment_source.rb b/app/models/spree/alternative_payment_source.rb new file mode 100644 index 0000000..eeea08f --- /dev/null +++ b/app/models/spree/alternative_payment_source.rb @@ -0,0 +1,31 @@ +module Spree + # TODO: Tidy comment! + # This source is used to store information about redirecting user's to the Adyen HPP source + # brand_code can obtain the brand of the payment source and if provided and configurtion set with + # Adyen.configuration. :details + # then the user will be redirected direct to the payment source (ie paypal) rather than going to the list oif payment options to select from + # In order fot this record to be created in the Checkout then a brand_code must be provided for Payment.build_source to work + # brand_code should be set to 'adyen' if you want to use the HPP solution in order for this to work + class AlternativePaymentSource < Spree::Base + + belongs_to :payment_method + belongs_to :user, class_name: Spree.user_class, foreign_key: 'user_id' + has_many :payments, as: :source + + validates :brand_code, presence: true + + def actions + %w{capture void credit} + end + + # Indicates whether its possible to void the payment. + def can_void?(payment) + !payment.void? + end + + # Indicates whether its possible to capture the payment + def can_capture?(payment) + payment.pending? || payment.checkout? + end + end +end \ No newline at end of file diff --git a/app/models/spree/gateway/adyen_hpp.rb b/app/models/spree/gateway/adyen_hpp.rb index 7a140a0..73e2d55 100644 --- a/app/models/spree/gateway/adyen_hpp.rb +++ b/app/models/spree/gateway/adyen_hpp.rb @@ -7,7 +7,11 @@ class Gateway::AdyenHPP < Gateway preference :shared_secret, :string def source_required? - false + true + end + + def payment_source_class + Spree::AlternativePaymentSource end def auto_capture? @@ -18,7 +22,7 @@ def auto_capture? # Adyen Hosted Payment Pages where we wouldn't keep # the credit card object # as that entered outside of the store forms def actions - %w{capture void} + %w{capture void authorize} end # Indicates whether its possible to void the payment. @@ -43,6 +47,10 @@ def skin_code ENV['ADYEN_SKIN_CODE'] || preferred_skin_code end + def authorize(amount, source, gateway_options = {}) + raise Adyen::HPPRedirectError.new(source) + end + # According to Spree Processing class API the response object should respond # to an authorization method which return value should be assigned to payment # response_code @@ -50,7 +58,7 @@ def void(response_code, gateway_options = {}) response = provider.cancel_payment(response_code) if response.success? - def response.authorization; psp_reference; end + def response.authorization; nil; end else # TODO confirm the error response will always have these two methods def response.to_s diff --git a/app/models/spree/gateway/adyen_payment_encrypted.rb b/app/models/spree/gateway/adyen_payment_encrypted.rb index cc633a8..6226210 100644 --- a/app/models/spree/gateway/adyen_payment_encrypted.rb +++ b/app/models/spree/gateway/adyen_payment_encrypted.rb @@ -13,12 +13,29 @@ def method_type end def payment_profiles_supported? - true + false end def authorize(amount, source, gateway_options = {}) + card = { encrypted: { json: source.encrypted_data } } - authorize_on_card amount, source, gateway_options, card + + # TODO: Make me conditional. Recurring must be true if payment profiles supported + response = authorize_on_card amount, source, gateway_options, card, { recurring: true } + + # TODO: MOve this to additional_params method to Adyen::API::AuthorisationResponse and merge in params method. + # NOTE: Iterate through entry elements nested in the additionalData element of the response (see SOAP Envelope) + last_digits = response.xml_querier.xpath('//payment:authoriseResponse/payment:paymentResult').text('./payment:additionalData/payment:entry/payment:value') + + # Ensure that this is enabled if using Encrypted Gateway and Payment Profiles supported + if last_digits.blank? && payment_profiles_supported? + Exception.new('Please request last digits to be sent back in Adyen response to support payment profiles') + else + source.last_digits = last_digits + end + + response + end # Do a symbolic authorization, e.g. 1 dollar, so that we can grab a recurring token diff --git a/db/migrate/20140818100716_create_alternative_payment_source.rb b/db/migrate/20140818100716_create_alternative_payment_source.rb new file mode 100644 index 0000000..b8ce3b3 --- /dev/null +++ b/db/migrate/20140818100716_create_alternative_payment_source.rb @@ -0,0 +1,10 @@ +class CreateAlternativePaymentSource < ActiveRecord::Migration + def change + create_table :spree_alternative_payment_sources do |t| + t.string :brand_code + t.integer :payment_method_id + t.integer :user_id + t.timestamps + end + end +end diff --git a/db/migrate/20140905150501_add_additional_data_alias_in_adyen_notifications.rb b/db/migrate/20140905150501_add_additional_data_alias_in_adyen_notifications.rb new file mode 100644 index 0000000..ea18864 --- /dev/null +++ b/db/migrate/20140905150501_add_additional_data_alias_in_adyen_notifications.rb @@ -0,0 +1,6 @@ +class AddAdditionalDataAliasInAdyenNotifications < ActiveRecord::Migration + def change + add_column :adyen_notifications, :additional_data_alias_type, :string + add_column :adyen_notifications, :additional_data_alias, :string + end +end diff --git a/lib/spree/adyen.rb b/lib/spree/adyen.rb index b6e8c74..48f9981 100644 --- a/lib/spree/adyen.rb +++ b/lib/spree/adyen.rb @@ -4,3 +4,4 @@ require "spree/adyen/engine" require "spree/adyen/checkout_rescue" require "spree/adyen/enrolled_3d_error" +require "spree/adyen/hpp_redirect_error" diff --git a/lib/spree/adyen/checkout_rescue.rb b/lib/spree/adyen/checkout_rescue.rb index 7fbce21..db0b9b6 100644 --- a/lib/spree/adyen/checkout_rescue.rb +++ b/lib/spree/adyen/checkout_rescue.rb @@ -9,10 +9,37 @@ module CheckoutRescue def rescue_from_adyen_3d_enrolled(exception) session[:adyen_gateway_id] = exception.gateway.id session[:adyen_gateway_name] = exception.gateway.class.name + session[:payment_identifier] = exception.gateway_options[:payment_identifier] @adyen_3d_response = exception render 'spree/checkout/adyen_3d_form' end + + rescue_from Adyen::HPPRedirectError, :with => :rescue_from_adyen_hpp_redirect + + def rescue_from_adyen_hpp_redirect(exception) + + payment = exception.source.payments.processing.last + + redirect_params = { + currency_code: current_order.currency, + ship_before_date: Date.tomorrow, + session_validity: 10.minutes.from_now, + recurring: false, + merchant_reference: "#{payment.order.number}-#{payment.identifier}", + merchant_account: exception.source.payment_method.merchant_account, + skin_code: exception.source.payment_method.skin_code, + shared_secret: exception.source.payment_method.shared_secret, + payment_amount: (payment.order.total.to_f * 100).to_int, + brandCode: exception.source.brand_code + } + + redirect_params[:resURL] = adyen_confirmation_url + + # TODO: For completeness offer configuration to render a view that will auto POST this information rather than a GET request + redirect_to ::Adyen::Form.redirect_url(@redirect_params.merge(redirect_params)) + end + end end end diff --git a/lib/spree/adyen/enrolled_3d_error.rb b/lib/spree/adyen/enrolled_3d_error.rb index c166159..bea2114 100644 --- a/lib/spree/adyen/enrolled_3d_error.rb +++ b/lib/spree/adyen/enrolled_3d_error.rb @@ -1,15 +1,16 @@ module Spree module Adyen class Enrolled3DError < StandardError - attr_reader :response, :issuer_url, :pa_request, :md, :gateway + attr_reader :response, :issuer_url, :pa_request, :md, :gateway, :gateway_options - def initialize(response, gateway) + def initialize(response, gateway, gateway_options={}) @response = response @issuer_url = response.issuer_url @pa_request = response.pa_request @md = response.md @gateway = gateway + @gateway_options = gateway_options end def messsage diff --git a/lib/spree/adyen/hpp_redirect_error.rb b/lib/spree/adyen/hpp_redirect_error.rb new file mode 100644 index 0000000..e9f4d68 --- /dev/null +++ b/lib/spree/adyen/hpp_redirect_error.rb @@ -0,0 +1,15 @@ +module Spree + module Adyen + class HPPRedirectError < StandardError + attr_reader :source + + def initialize(source) + @source = source + end + + def messsage + source.to_s + end + end + end +end \ No newline at end of file