solidus_mercado_pago 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/app/assets/javascripts/spree/backend/solidus_mercado_pago.js +0 -0
  3. data/app/assets/javascripts/spree/frontend/solidus_mercado_pago.js +25 -0
  4. data/app/assets/stylesheets/spree/backend/solidus_mercado_pago.css +1 -0
  5. data/app/assets/stylesheets/spree/frontend/solidus_mercado_pago.css +1 -0
  6. data/app/controllers/spree/mercado_pago_controller.rb +68 -0
  7. data/app/models/mercado_pago/client.rb +62 -0
  8. data/app/models/mercado_pago/client/api.rb +44 -0
  9. data/app/models/mercado_pago/client/authentication.rb +36 -0
  10. data/app/models/mercado_pago/client/preferences.rb +20 -0
  11. data/app/models/mercado_pago/notification.rb +8 -0
  12. data/app/models/mercado_pago/order_preferences_builder.rb +65 -0
  13. data/app/models/spree/payment_method/mercado_pago.rb +46 -0
  14. data/app/services/mercado_pago/handle_received_notification.rb +18 -0
  15. data/app/services/mercado_pago/process_notification.rb +56 -0
  16. data/app/views/spree/admin/payments/source_forms/_mercadopago.html.erb +1 -0
  17. data/app/views/spree/admin/payments/source_views/_mercadopago.html.erb +1 -0
  18. data/app/views/spree/checkout/mercado_pago_error.html.erb +0 -0
  19. data/app/views/spree/checkout/payment/_mercadopago.html.erb +15 -0
  20. data/config/locales/en.yml +4 -0
  21. data/config/locales/es.yml +4 -0
  22. data/config/locales/pt-BR.yml +4 -0
  23. data/config/routes.rb +6 -0
  24. data/db/migrate/20141201204026_create_solidus_mercado_pago_notifications.rb +9 -0
  25. data/lib/generators/solidus_mercado_pago/install/install_generator.rb +46 -0
  26. data/lib/solidus_mercado_pago.rb +3 -0
  27. data/lib/solidus_mercado_pago/engine.rb +35 -0
  28. data/lib/solidus_mercado_pago/version.rb +3 -0
  29. data/lib/tasks/mercado_user.rake +5 -0
  30. data/spec/controllers/spree/mercado_pago_controller_spec.rb +30 -0
  31. data/spec/fixtures/authenticated.json +7 -0
  32. data/spec/fixtures/preferences_created.json +41 -0
  33. data/spec/lib/generators/solidus_mercado_pago/install/install_generator_spec.rb +33 -0
  34. data/spec/lib/solidus_mercado_pago/version_spec.rb +5 -0
  35. data/spec/models/mercado_pago/client_spec.rb +130 -0
  36. data/spec/models/mercado_pago/notification_spec.rb +17 -0
  37. data/spec/models/mercado_pago/order_preferences_builder_spec.rb +58 -0
  38. data/spec/models/spree/payment_method/mercado_pago_spec.rb +34 -0
  39. data/spec/services/mercado_pago/process_notification_spec.rb +82 -0
  40. data/spec/spec_helper.rb +133 -0
  41. metadata +200 -0
@@ -0,0 +1 @@
1
+ <!-- Source form -->
@@ -0,0 +1 @@
1
+ <!-- Source view -->
@@ -0,0 +1,15 @@
1
+ <script>
2
+ MercadoPago.paymentMethodID = "<%= payment_method.id %>";
3
+ </script>
4
+
5
+ <div class="row">
6
+ <div class="twelve rows alpha omega">
7
+ <img src="http://imgmp.mlstatic.com/org-img/banners/ar/medios/575X40.jpg" title="MercadoPago - Medios de pago" alt="MercadoPago - Medios de pago" width="575" height="40"/>
8
+ </div>
9
+ </div>
10
+
11
+ <div class="row">
12
+ <div class="twelve rows alpha omega">
13
+ <%= link_to I18n.t(:make_payment, scope: :mercado_pago), mercado_pago_checkout_path(payment_method_id: payment_method.id), method: :post, class: "button primary" %>
14
+ </div>
15
+ </div>
@@ -0,0 +1,4 @@
1
+ en:
2
+ mercado_pago:
3
+ make_payment: "Pay with Mercado Pago"
4
+ authentication_error: "It was not possible to authenticate with Mercado Pago"
@@ -0,0 +1,4 @@
1
+ es:
2
+ mercado_pago:
3
+ make_payment: "Realizar pago en Mercado Pago"
4
+ authentication_error: "Hubo un error al realizar la autenticación con Mercado Pago"
@@ -0,0 +1,4 @@
1
+ pt-BR:
2
+ mercado_pago:
3
+ make_payment: "Pagar com Mercado Pago"
4
+ authentication_error: "Não foi possível autenticar com Mercado Pago"
data/config/routes.rb ADDED
@@ -0,0 +1,6 @@
1
+ Spree::Core::Engine.routes.draw do
2
+ post '/mercado_pago/checkout', to: 'mercado_pago#checkout', as: :mercado_pago_checkout
3
+ get '/mercado_pago/success', to: 'mercado_pago#success', as: :mercado_pago_success
4
+ get '/mercado_pago/failure', to: 'mercado_pago#failure', as: :mercado_pago_failure
5
+ post '/mercado_pago/ipn', to: 'mercado_pago#ipn', as: :mercado_pago_ipn
6
+ end
@@ -0,0 +1,9 @@
1
+ class CreateSolidusMercadoPagoNotifications < ActiveRecord::Migration[4.2]
2
+ def change
3
+ create_table :mercado_pago_notifications do |t|
4
+ t.string :topic
5
+ t.string :operation_id
6
+ t.timestamps
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,46 @@
1
+ # rubocop:disable Style/RegexpLiteral
2
+ # rubocop:disable Rails/Output
3
+
4
+ module SolidusMercadoPago
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ class_option :auto_run_migrations, type: :boolean, default: false
8
+ class_option :auto_skip_migrations, type: :boolean, default: false
9
+
10
+ def add_javascripts
11
+ append_file 'vendor/assets/javascripts/spree/frontend/all.js', "//= require spree/frontend/solidus_mercado_pago\n"
12
+ append_file 'vendor/assets/javascripts/spree/backend/all.js', "//= require spree/backend/solidus_mercado_pago\n"
13
+ end
14
+
15
+ def add_stylesheets
16
+ frontend_css_file = 'vendor/assets/stylesheets/spree/frontend/all.css'
17
+ backend_css_file = 'vendor/assets/stylesheets/spree/backend/all.css'
18
+
19
+ return unless File.exist?(backend_css_file) && File.exist?(frontend_css_file)
20
+
21
+ inject_into_file frontend_css_file, " *= require spree/frontend/solidus_mercado_pago\n", before: /\*\//, verbose: true
22
+ inject_into_file backend_css_file, " *= require spree/backend/solidus_mercado_pago\n", before: /\*\//, verbose: true
23
+ end
24
+
25
+ def add_migrations
26
+ if !options[:auto_skip_migrations]
27
+ run 'bundle exec rake solidus_mercado_pago:install:migrations'
28
+ else
29
+ puts 'Skipping rake solidus_mercado_pago:install:migrations, don\'t forget to run it!'
30
+ end
31
+ end
32
+
33
+ def run_migrations
34
+ run_migrations = options[:auto_skip_migrations] ||
35
+ options[:auto_run_migrations] ||
36
+ ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]'))
37
+
38
+ if run_migrations && !options[:auto_skip_migrations]
39
+ run 'bundle exec rake db:migrate'
40
+ else
41
+ puts 'Skipping rake db:migrate, don\'t forget to run it!'
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,3 @@
1
+ require 'solidus_core'
2
+ require 'solidus_frontend'
3
+ require 'solidus_mercado_pago/engine'
@@ -0,0 +1,35 @@
1
+ module SolidusMercadoPago
2
+ class Engine < Rails::Engine
3
+ require 'spree/core'
4
+ isolate_namespace Spree
5
+ engine_name 'solidus_mercado_pago'
6
+
7
+ # use rspec for tests
8
+ config.generators do |g|
9
+ g.test_framework :rspec
10
+ end
11
+
12
+ initializer 'spree_payment_network.register.payment_methods' do |app|
13
+ app.config.spree.payment_methods << Spree::PaymentMethod::MercadoPago
14
+ end
15
+
16
+ def self.activate
17
+ load_decorators
18
+ load_overrides
19
+ end
20
+
21
+ def self.load_decorators
22
+ Dir.glob(File.join(File.dirname(__FILE__), '../../app/**/*_decorator*.rb')) do |c|
23
+ Rails.application.config.cache_classes ? require(c) : load(c)
24
+ end
25
+ end
26
+
27
+ def self.load_overrides
28
+ Dir.glob(File.join(File.dirname(__FILE__), '../../app/overrides/*.rb')) do |c|
29
+ Rails.application.config.cache_classes ? require(c) : load(c)
30
+ end
31
+ end
32
+
33
+ config.to_prepare(&method(:activate).to_proc)
34
+ end
35
+ end
@@ -0,0 +1,3 @@
1
+ module SolidusMercadoPago
2
+ VERSION = '1.0.0'.freeze
3
+ end
@@ -0,0 +1,5 @@
1
+ namespace :mercado_pago do
2
+ desc 'Creates test user using MP API'
3
+ task test_user: :environment do
4
+ end
5
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Spree::MercadoPagoController, type: :controller do
4
+ describe '#ipn' do
5
+ let(:operation_id) { 'op123' }
6
+
7
+ context 'for valid notifications' do
8
+ let(:use_case) { double('use_case') }
9
+
10
+ it 'handles notification and returns success' do
11
+ allow(MercadoPago::HandleReceivedNotification).to receive(:new).and_return(use_case)
12
+ expect(use_case).to receive(:process!)
13
+
14
+ post :ipn, params: { id: operation_id, topic: 'payment', format: :json }
15
+ expect(response).to be_success
16
+
17
+ notification = MercadoPago::Notification.order(:created_at).last
18
+ expect(notification.topic).to eq('payment')
19
+ expect(notification.operation_id).to eq(operation_id)
20
+ end
21
+ end
22
+
23
+ context 'for invalid notification' do
24
+ it 'responds with invalid request' do
25
+ post :ipn, params: { id: operation_id, topic: 'nonexistent_topic', format: :raw }
26
+ expect(response).to be_bad_request
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,7 @@
1
+ {
2
+ "access_token": "TU_ACCESS_TOKEN",
3
+ "token_type": "bearer",
4
+ "expires_in": 10800,
5
+ "scope": "...",
6
+ "refresh_token": "REFRESH_TOKEN"
7
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "external_reference": "Reference_1234",
3
+ "items": [
4
+ {
5
+ "id": "Código",
6
+ "title": "Título de lo que estás pagando",
7
+ "description": "Descripción",
8
+ "quantity": 1,
9
+ "unit_price": 50.5,
10
+ "currency_id": "Tipo de moneda",
11
+ "picture_url": "https://www.mercadopago.com/org-img/MP3/home/logomp3.gif"
12
+ }
13
+ ],
14
+ "date_created": "2011-08-16T21:28:42.606-04:00",
15
+ "id": "identificador_de_la_preferencia",
16
+ "collector_id": "collector identifier",
17
+ "init_point": "https://www.mercadopago.com/checkout/pay?pref_id=identificador_de_la_preferencia",
18
+ "payer": {
19
+ "email": "payer@email.com",
20
+ "name": "payer-name",
21
+ "surname": "payer-surname"
22
+ },
23
+ "back_urls": {
24
+ "success": "https://www.success.com",
25
+ "failure": "http://www.failure.com",
26
+ "pending": "http://www.pending.com"
27
+ },
28
+ "payment_methods": {
29
+ "excluded_payment_methods": [
30
+ {
31
+ "id": "amex"
32
+ }
33
+ ],
34
+ "excluded_payment_types": [
35
+ {
36
+ "id": "ticket"
37
+ }
38
+ ],
39
+ "installments": 12
40
+ }
41
+ }
@@ -0,0 +1,33 @@
1
+ # rubocop:disable Style/RegexpLiteral
2
+ # rubocop:disable RSpec/FilePath
3
+
4
+ require 'spec_helper'
5
+ require 'generator_spec'
6
+ require 'generators/solidus_mercado_pago/install/install_generator'
7
+
8
+ describe SolidusMercadoPago::Generators::InstallGenerator, type: :generator do
9
+ destination File.expand_path('../../../../tmp', __FILE__)
10
+
11
+ let(:dest) do
12
+ File.expand_path('../../../../tmp', __FILE__)
13
+ end
14
+
15
+ before do
16
+ prepare_destination
17
+ FileUtils.mkdir_p "#{dest}/vendor/assets/javascripts/spree/frontend"
18
+ FileUtils.mkdir_p "#{dest}/vendor/assets/javascripts/spree/backend"
19
+ FileUtils.touch "#{dest}/vendor/assets/javascripts/spree/frontend/all.js"
20
+ FileUtils.touch "#{dest}/vendor/assets/javascripts/spree/backend/all.js"
21
+ run_generator %w[--auto_skip_migrations]
22
+ end
23
+
24
+ it 'creates a test initializer' do
25
+ assert_file "#{dest}/vendor/assets/javascripts/spree/frontend/all.js", /^\/\/= require spree\/frontend\/solidus_mercado_pago/
26
+ end
27
+
28
+ it { assert_file "#{dest}/vendor/assets/javascripts/spree/backend/all.js", /^\/\/= require spree\/backend\/solidus_mercado_pago/ }
29
+
30
+ after do
31
+ FileUtils.rm_rf destination_root
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe SolidusMercadoPago do
4
+ it { expect(SolidusMercadoPago::VERSION).not_to be nil }
5
+ end
@@ -0,0 +1,130 @@
1
+ require 'spec_helper'
2
+ require 'json'
3
+
4
+ RSpec.describe MercadoPago::Client, type: :model do
5
+ SPEC_ROOT = File.expand_path('../', File.dirname(__FILE__))
6
+
7
+ let(:payment_method) { double('payment_method', preferred_client_id: 1, preferred_client_secret: 1) }
8
+
9
+ let(:order) { double('order', payment_method: payment_method, number: 'testorder', line_items: [], ship_total: 1000) }
10
+ let(:url_callbacks) { { success: 'url', failure: 'url', pending: 'url' } }
11
+ let(:payment_method) { double :payment_method, id: 1, preferred_client_id: 'app id', preferred_client_secret: 'app secret' }
12
+ let(:payment) { double :payment, payment_method: payment_method, id: 1, identifier: 'fruta' }
13
+
14
+ let(:login_json_response) { File.open("#{SPEC_ROOT}/../fixtures/authenticated.json").read }
15
+ let(:preferences_json_response) { File.open("#{SPEC_ROOT}/../fixtures/preferences_created.json").read }
16
+ let(:client) { MercadoPago::Client.new(payment_method) }
17
+
18
+ describe '#initialize' do
19
+ it "doesn't raise error with all params" do
20
+ expect { client }.not_to raise_error
21
+ end
22
+ end
23
+
24
+ describe '#authenticate' do
25
+ context 'On success' do
26
+ let(:http_response) do
27
+ response = double('response')
28
+ allow(response).to receive(:code).and_return 200
29
+ allow(response).to receive(:to_str).and_return login_json_response
30
+ response
31
+ end
32
+ let(:js_response) { JSON.parse(http_response) }
33
+
34
+ before do
35
+ allow(RestClient).to receive(:post).and_return(http_response)
36
+ end
37
+
38
+ it 'returns a response object' do
39
+ expect(client.authenticate).to eq(js_response)
40
+ end
41
+
42
+ it '#errors returns empty array' do
43
+ client.authenticate
44
+ expect(client.errors).to be_empty
45
+ end
46
+
47
+ it 'sets the access token' do
48
+ client.authenticate
49
+ expect(client.auth_response['access_token']).to eq('TU_ACCESS_TOKEN')
50
+ end
51
+ end
52
+
53
+ context 'On failure' do
54
+ let(:bad_request_response) do
55
+ response = double('response')
56
+ allow(response).to receive(:code).and_return 400
57
+ allow(response).to receive(:to_str).and_return ''
58
+ response
59
+ end
60
+
61
+ before do
62
+ allow(RestClient).to receive(:post).and_raise(RestClient::Exception.new('foo'))
63
+ end
64
+
65
+ it 'raise exception on invalid authentication' do
66
+ expect { client.authenticate }.to raise_error(RuntimeError) do |_error|
67
+ expect(client.errors).to include(I18n.t(:authentication_error, scope: :mercado_pago))
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ describe '#check_payment_status' do
74
+ let(:collection) { {} }
75
+ let(:expected_response) { { results: [collection: collection] } }
76
+
77
+ before do
78
+ allow(subject).to receive(:send_search_request).with(external_reference: payment.id).and_return(expected_response)
79
+ allow(subject).to receive(:check_status).with(payment, {})
80
+ end
81
+ end
82
+
83
+ describe '#create_preferences' do
84
+ context 'On success' do
85
+ let(:preferences) { { foo: 'bar' } }
86
+
87
+ before do
88
+ response = double('response')
89
+ allow(response).to receive(:code).and_return(200, 201)
90
+ allow(response).to receive(:to_str).and_return(login_json_response, preferences_json_response)
91
+ allow(RestClient).to receive(:post).exactly(2).times { response }
92
+
93
+ client.authenticate
94
+ end
95
+
96
+ it 'return value should not be nil' do
97
+ response = client.create_preferences(preferences)
98
+ expect(response).to be_truthy
99
+ end
100
+
101
+ it '#redirect_url returns offsite checkout url' do
102
+ client.create_preferences(preferences)
103
+ expect(client.redirect_url).to be_present
104
+ expect(client.redirect_url).to eq('https://www.mercadopago.com/checkout/pay?pref_id=identificador_de_la_preferencia')
105
+ end
106
+ end
107
+
108
+ context 'on failure' do
109
+ before do
110
+ expect(RestClient).to receive(:post).exactly(2).times do
111
+ if !@is_second_time
112
+ @is_second_time = true
113
+ '{}'
114
+ else
115
+ raise RestClient::Exception, 'foo'
116
+ end
117
+ end
118
+ client.authenticate
119
+ end
120
+
121
+ let(:preferences) { { foo: 'bar' } }
122
+
123
+ it 'throws exception and populate errors' do
124
+ expect { client.create_preferences(preferences) }.to raise_error(RuntimeError) do |_variable|
125
+ expect(client.errors).to include(I18n.t(:authentication_error, scope: :mercado_pago))
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ module MercadoPago
4
+ describe Notification do
5
+ describe 'without basic parameters' do
6
+ it { expect(Notification.new).not_to be_valid }
7
+ end
8
+
9
+ describe 'with unknown topic' do
10
+ it { expect(Notification.new(topic: 'foo', operation_id: 'op123')).not_to be_valid }
11
+ end
12
+
13
+ describe 'with correct parameters' do
14
+ it { expect(Notification.new(topic: 'payment', operation_id: 'op123')).to be_valid }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'OrderPreferencesBuilder' do
4
+ # Factory order_with_line_items is incredibly slow..
5
+ let(:order) do
6
+ order = create(:order)
7
+ create_list(:line_item, 2, order: order)
8
+ order.line_items.reload
9
+ order
10
+ end
11
+
12
+ let(:payment) { create(:payment) }
13
+ let(:callback_urls) { { success: 'http://example.com/success', pending: 'http://example.com/pending', failure: 'http://example.com/failure' } }
14
+ let(:payer_data) { { email: 'jmperez@devartis.com' } }
15
+
16
+ include ActionView::Helpers::TextHelper
17
+ include ActionView::Helpers::SanitizeHelper
18
+ include Spree::ProductsHelper
19
+
20
+ context 'Calling preferences_hash' do
21
+ let(:subject) { MercadoPago::OrderPreferencesBuilder.new(order, payment, callback_urls, payer_data).preferences_hash }
22
+
23
+ it 'returns external reference' do
24
+ end
25
+
26
+ it 'sets callback urls' do
27
+ expect(subject).to include(back_urls: callback_urls)
28
+ end
29
+
30
+ it 'sets payer data if brought' do
31
+ expect(subject).to include(payer: payer_data)
32
+ end
33
+
34
+ it 'sets an item for every line item' do
35
+ expect(subject).to include(:items)
36
+
37
+ order.line_items.each do |line_item|
38
+ expect(subject[:items]).to include(title: line_item_description_text(line_item.variant.product.name),
39
+ unit_price: line_item.price.to_f,
40
+ quantity: line_item.quantity.to_f)
41
+ end
42
+ end
43
+
44
+ context 'for order with adjustments' do
45
+ let!(:adjustment) { Spree::Adjustment.create!(adjustable: order, order: order, label: 'Descuento', amount: -10.0) }
46
+
47
+ it 'sets its adjustments as items' do
48
+ expect(subject[:items]).to include(title: line_item_description_text(adjustment.label),
49
+ unit_price: adjustment.amount.to_f,
50
+ quantity: 1)
51
+ end
52
+
53
+ it 'onlies have line items and adjustments in items' do
54
+ expect(subject[:items]).to have(order.line_items.count + order.adjustments.count).items
55
+ end
56
+ end
57
+ end
58
+ end