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

2 3 stable fix #12

Open
wants to merge 12 commits into
base: 2-3-stable
Choose a base branch
from
126 changes: 59 additions & 67 deletions app/controllers/spree/adyen_redirect_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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."
Expand All @@ -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
Expand Down
30 changes: 25 additions & 5 deletions app/models/adyen_notification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down Expand Up @@ -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
56 changes: 35 additions & 21 deletions app/models/spree/adyen_common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module AdyenCommon

class RecurringDetailsNotFoundError < StandardError; end
class MissingCardSummaryError < StandardError; end
class MissingAliasError < StandardError; end

included do
preference :api_username, :string
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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?
Expand All @@ -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
Expand All @@ -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
Expand All @@ -212,30 +217,28 @@ 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?
note = "Payment was authorized but could not fetch last digits.
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
Expand All @@ -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)

Expand All @@ -264,6 +277,7 @@ def fetch_and_update_contract(source, shopper_reference)
gateway_customer_profile_id: card[:recurring_detail_reference]
)
end

end

module ClassMethods
Expand Down
Loading