diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72ef0cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*.gem +\#* +*~ +.#* +.DS_Store +.idea +.project +.sass-cache +coverage +Gemfile.lock +tmp +nbproject +pkg +*.swp +spec/dummy +coverage/**/* diff --git a/app/controllers/spree/mercadopago_controller.rb b/app/controllers/spree/mercadopago_controller.rb index 7da791f..978973a 100644 --- a/app/controllers/spree/mercadopago_controller.rb +++ b/app/controllers/spree/mercadopago_controller.rb @@ -12,7 +12,7 @@ def checkout .create!(amount: current_order.total, payment_method: payment_method) payment.started_processing! - preferences = ::Mercadopago::OrderPreferencesBuilder + preferences = Mercadopago::Services::OrderPreferencesBuilder .new(current_order, payment, callback_urls) .preferences_hash @@ -43,7 +43,7 @@ def ipn .new(operation_id: params[:id], topic: params[:topic]) if notification.save - Mercadopago::HandleReceivedNotification.new(notification).process! + Mercadopago::Services::HandleReceivedNotification.new(notification).process! status = :ok else status = :bad_request diff --git a/app/models/mercadopago/services/handle_received_notification.rb b/app/models/mercadopago/services/handle_received_notification.rb new file mode 100644 index 0000000..d31457c --- /dev/null +++ b/app/models/mercadopago/services/handle_received_notification.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Mercadopago + module Services + class HandleReceivedNotification + def initialize(notification) + @notification = notification + end + + # The purpose of this method is to enable async/sync processing + # of Mercado Pago IPNs. For simplicity processing is synchronous but + # if you would like to enqueue the processing via Resque/Ost/etc you + # will be able to do it. + def process! + # Sync + ::ProcessNotification.new(@notification).process! + # Async Will be configurable via block for example: + # Resque.enqueue(ProcessNotificationWorker, {id: @notification.id}) + end + end + end +end diff --git a/app/models/mercadopago/services/process_notification.rb b/app/models/mercadopago/services/process_notification.rb new file mode 100644 index 0000000..5a64f4c --- /dev/null +++ b/app/models/mercadopago/services/process_notification.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +# Process notification: +# --------------------- +# Fetch collection information +# Find payment by external reference +# If found +# Update payment status +# Notify user +# If not found +# Ignore notification (maybe payment from outside Spree) +module Mercadopago + module Services + class ProcessNotification + # Equivalent payment states + # MP state => Spree state + # ======================= + # + # approved => complete + # pending => pend + # in_process => pend + # rejected => failed + # refunded => void + # cancelled => void + # in_mediation => pend + # charged_back => void + STATES = { + complete: %w[approved], + failure: %w[rejected], + void: %w[refunded cancelled charged_back] + }.freeze + + attr_reader :notification + + def initialize(notification) + @notification = notification + end + + def process! + # Fix: Payment method is an instance of Spree::PaymentMethod::Mercadopago not THE class + client = ::Spree::PaymentMethod::Mercadopago.first.provider + raw_op_info = client.get_operation_info(notification.operation_id) + op_info = raw_op_info['collection'] if raw_op_info.present? + # TODO: rewrite this. + if op_info && (payment = Spree::Payment.where(number: op_info['external_reference']).first) + if STATES[:complete].include?(op_info['status']) + payment.complete + elsif STATES[:failure].include?(op_info['status']) + payment.failure + elsif STATES[:void].include?(op_info['status']) + payment.void + end + + # When Spree issue #5246 is fixed we can remove this line + payment.order.updater.update + end + end + end + end +end diff --git a/app/models/spree/payment_method/mercadopago.rb b/app/models/spree/payment_method/mercadopago.rb index b0f2d42..ca0d039 100644 --- a/app/models/spree/payment_method/mercadopago.rb +++ b/app/models/spree/payment_method/mercadopago.rb @@ -3,8 +3,8 @@ module Spree class PaymentMethod::Mercadopago < PaymentMethod preference :sandbox, :boolean, default: true - preference :client_id, :string, default: ENV['Mercadopago_CLIENT_ID'] - preference :client_secret, :string, default: ENV['Mercadopago_CLIENT_SECRET'] + preference :client_id, :string, default: ENV['MERCADOPAGO_CLIENT_ID'] + preference :client_secret, :string, default: ENV['MERCADOPAGO_CLIENT_SECRET'] def payment_profiles_supported? false diff --git a/app/services/mercadopago/handle_received_notification.rb b/app/services/mercadopago/handle_received_notification.rb deleted file mode 100644 index b15e7fd..0000000 --- a/app/services/mercadopago/handle_received_notification.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -module Mercadopago - class HandleReceivedNotification - def initialize(notification) - @notification = notification - end - - # The purpose of this method is to enable async/sync processing - # of Mercado Pago IPNs. For simplicity processing is synchronous but - # if you would like to enqueue the processing via Resque/Ost/etc you - # will be able to do it. - def process! - # Sync - ProcessNotification.new(@notification).process! - # Async Will be configurable via block for example: - # Resque.enqueue(ProcessNotificationWorker, {id: @notification.id}) - end - end -end diff --git a/app/services/mercadopago/process_notification.rb b/app/services/mercadopago/process_notification.rb deleted file mode 100644 index 04a358f..0000000 --- a/app/services/mercadopago/process_notification.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -# Process notification: -# --------------------- -# Fetch collection information -# Find payment by external reference -# If found -# Update payment status -# Notify user -# If not found -# Ignore notification (maybe payment from outside Spree) -module Mercadopago - class ProcessNotification - # Equivalent payment states - # MP state => Spree state - # ======================= - # - # approved => complete - # pending => pend - # in_process => pend - # rejected => failed - # refunded => void - # cancelled => void - # in_mediation => pend - # charged_back => void - STATES = { - complete: %w[approved], - failure: %w[rejected], - void: %w[refunded cancelled charged_back] - }.freeze - - attr_reader :notification - - def initialize(notification) - @notification = notification - end - - def process! - # Fix: Payment method is an instance of Spree::PaymentMethod::Mercadopago not THE class - client = ::Spree::PaymentMethod::Mercadopago.first.provider - raw_op_info = client.get_operation_info(notification.operation_id) - op_info = raw_op_info['collection'] if raw_op_info.present? - # TODO: rewrite this. - if op_info && (payment = Spree::Payment.where(number: op_info['external_reference']).first) - if STATES[:complete].include?(op_info['status']) - payment.complete - elsif STATES[:failure].include?(op_info['status']) - payment.failure - elsif STATES[:void].include?(op_info['status']) - payment.void - end - - # When Spree issue #5246 is fixed we can remove this line - payment.order.updater.update - end - end - end -end diff --git a/spec/controllers/spree/mercado_pago_controller_spec.rb b/spec/controllers/spree/mercado_pago_controller_spec.rb index 5d55597..bc23e31 100644 --- a/spec/controllers/spree/mercado_pago_controller_spec.rb +++ b/spec/controllers/spree/mercado_pago_controller_spec.rb @@ -10,7 +10,7 @@ let(:use_case) { double('use_case') } it 'handles notification and returns success' do - allow(Mercadopago::HandleReceivedNotification).to receive(:new).and_return(use_case) + allow(Mercadopago::Services::HandleReceivedNotification).to receive(:new).and_return(use_case) expect(use_case).to receive(:process!) post :ipn, params: { id: operation_id, topic: 'payment', format: :json } diff --git a/spec/models/mercadopago/order_preferences_builder_spec.rb b/spec/models/mercadopago/order_preferences_builder_spec.rb index f5331d0..3bdcbf3 100644 --- a/spec/models/mercadopago/order_preferences_builder_spec.rb +++ b/spec/models/mercadopago/order_preferences_builder_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'OrderPreferencesBuilder' do +describe Mercadopago::OrderPreferencesBuilder do # Factory order_with_line_items is incredibly slow.. let(:order) do order = create(:order) @@ -20,7 +20,12 @@ include Spree::ProductsHelper context 'Calling preferences_hash' do - let(:subject) { Mercadopago::OrderPreferencesBuilder.new(order, payment, callback_urls, payer_data).preferences_hash } + let(:subject) do + Mercadopago::OrderPreferencesBuilder.new(order, + payment, + callback_urls, + payer_data).preferences_hash + end it 'returns external reference' do end diff --git a/spec/models/mercadopago/services/process_notification_spec.rb b/spec/models/mercadopago/services/process_notification_spec.rb new file mode 100644 index 0000000..1caf591 --- /dev/null +++ b/spec/models/mercadopago/services/process_notification_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Mercadopago::Services::ProcessNotification do + let(:order) { FactoryBot.create(:completed_order_with_pending_payment) } + let(:payment) { order.payments.first } + + let(:operation_id) { 'op123' } + let(:notification) do + Mercadopago::Notification.new(topic: 'payment', operation_id: operation_id) + end + let(:operation_info) do + { + 'collection' => { + 'external_reference' => order.payments.first.number, + 'status' => 'approved' + } + } + end + + describe 'with valid operation_info' do + # The first payment method of this kind will be picked by the process task + before do + fake_client = double('fake_client') + fake_payment_method = double('fake_payment_method', provider: fake_client) + allow(Spree::PaymentMethod::Mercadopago).to receive(:first).and_return(fake_payment_method) + allow(fake_client).to receive(:get_operation_info).with(operation_id) + .and_return(operation_info) + # TODO: check this test + # payment.pend! + expect(payment.state).to eq('pending') + end + + describe '#process!' do + it 'completes payment for approved payment' do + Mercadopago::Services::ProcessNotification.new(notification).process! + payment.reload + expect(payment.state).to eq('completed') + end + + it 'fails payment for rejected payment' do + operation_info['collection']['status'] = 'rejected' + Mercadopago::Services::ProcessNotification.new(notification).process! + payment.reload + expect(payment.state).to eq('failed') + end + + it 'voids payment for rejected payment' do + operation_info['collection']['status'] = 'cancelled' + Mercadopago::Services::ProcessNotification.new(notification).process! + payment.reload + expect(payment.state).to eq('void') + end + + it 'pends payment for pending payment' do + operation_info['collection']['status'] = 'pending' + Mercadopago::Services::ProcessNotification.new(notification).process! + payment.reload + expect(payment.state).to eq('pending') + end + end + end + + describe 'with invalid operation_info' do + # The first payment method of this kind will be picked by the process task + before do + fake_client = double('fake_client') + fake_payment_method = double('fake_payment_method', provider: fake_client) + allow(Spree::PaymentMethod::Mercadopago).to receive(:first).and_return(fake_payment_method) + allow(fake_client).to receive(:get_operation_info).with(operation_id) + .and_return(nil) + # TODO: check this test + # payment.pend! + expect(payment.state).to eq('pending') + end + + describe '#process!' do + it 'does not crash' do + Mercadopago::Services::ProcessNotification.new(notification).process! + end + end + end +end diff --git a/spec/services/mercadopago/process_notification_spec.rb b/spec/services/mercadopago/process_notification_spec.rb deleted file mode 100644 index 10baa5f..0000000 --- a/spec/services/mercadopago/process_notification_spec.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -module Mercadopago - describe ProcessNotification do - let(:order) { FactoryBot.create(:completed_order_with_pending_payment) } - let(:payment) { order.payments.first } - - let(:operation_id) { 'op123' } - let(:notification) { Notification.new(topic: 'payment', operation_id: operation_id) } - let(:operation_info) do - { - 'collection' => { - 'external_reference' => order.payments.first.number, - 'status' => 'approved' - } - } - end - - describe 'with valid operation_info' do - # The first payment method of this kind will be picked by the process task - before do - fake_client = double('fake_client') - fake_payment_method = double('fake_payment_method', provider: fake_client) - allow(Spree::PaymentMethod::Mercadopago).to receive(:first).and_return(fake_payment_method) - allow(fake_client).to receive(:get_operation_info).with(operation_id) - .and_return(operation_info) - # TODO: check this test - # payment.pend! - expect(payment.state).to eq('pending') - end - - describe '#process!' do - it 'completes payment for approved payment' do - ProcessNotification.new(notification).process! - payment.reload - expect(payment.state).to eq('completed') - end - - it 'fails payment for rejected payment' do - operation_info['collection']['status'] = 'rejected' - ProcessNotification.new(notification).process! - payment.reload - expect(payment.state).to eq('failed') - end - - it 'voids payment for rejected payment' do - operation_info['collection']['status'] = 'cancelled' - ProcessNotification.new(notification).process! - payment.reload - expect(payment.state).to eq('void') - end - - it 'pends payment for pending payment' do - operation_info['collection']['status'] = 'pending' - ProcessNotification.new(notification).process! - payment.reload - expect(payment.state).to eq('pending') - end - end - end - - describe 'with invalid operation_info' do - # The first payment method of this kind will be picked by the process task - before do - fake_client = double('fake_client') - fake_payment_method = double('fake_payment_method', provider: fake_client) - allow(Spree::PaymentMethod::Mercadopago).to receive(:first).and_return(fake_payment_method) - allow(fake_client).to receive(:get_operation_info).with(operation_id) - .and_return(nil) - # TODO: check this test - # payment.pend! - expect(payment.state).to eq('pending') - end - - describe '#process!' do - it 'does not crash' do - ProcessNotification.new(notification).process! - end - end - end - end -end