solidus_mercado_pago 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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