solidus_mercado_pago 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/app/assets/javascripts/spree/backend/solidus_mercado_pago.js +0 -0
- data/app/assets/javascripts/spree/frontend/solidus_mercado_pago.js +25 -0
- data/app/assets/stylesheets/spree/backend/solidus_mercado_pago.css +1 -0
- data/app/assets/stylesheets/spree/frontend/solidus_mercado_pago.css +1 -0
- data/app/controllers/spree/mercado_pago_controller.rb +68 -0
- data/app/models/mercado_pago/client.rb +62 -0
- data/app/models/mercado_pago/client/api.rb +44 -0
- data/app/models/mercado_pago/client/authentication.rb +36 -0
- data/app/models/mercado_pago/client/preferences.rb +20 -0
- data/app/models/mercado_pago/notification.rb +8 -0
- data/app/models/mercado_pago/order_preferences_builder.rb +65 -0
- data/app/models/spree/payment_method/mercado_pago.rb +46 -0
- data/app/services/mercado_pago/handle_received_notification.rb +18 -0
- data/app/services/mercado_pago/process_notification.rb +56 -0
- data/app/views/spree/admin/payments/source_forms/_mercadopago.html.erb +1 -0
- data/app/views/spree/admin/payments/source_views/_mercadopago.html.erb +1 -0
- data/app/views/spree/checkout/mercado_pago_error.html.erb +0 -0
- data/app/views/spree/checkout/payment/_mercadopago.html.erb +15 -0
- data/config/locales/en.yml +4 -0
- data/config/locales/es.yml +4 -0
- data/config/locales/pt-BR.yml +4 -0
- data/config/routes.rb +6 -0
- data/db/migrate/20141201204026_create_solidus_mercado_pago_notifications.rb +9 -0
- data/lib/generators/solidus_mercado_pago/install/install_generator.rb +46 -0
- data/lib/solidus_mercado_pago.rb +3 -0
- data/lib/solidus_mercado_pago/engine.rb +35 -0
- data/lib/solidus_mercado_pago/version.rb +3 -0
- data/lib/tasks/mercado_user.rake +5 -0
- data/spec/controllers/spree/mercado_pago_controller_spec.rb +30 -0
- data/spec/fixtures/authenticated.json +7 -0
- data/spec/fixtures/preferences_created.json +41 -0
- data/spec/lib/generators/solidus_mercado_pago/install/install_generator_spec.rb +33 -0
- data/spec/lib/solidus_mercado_pago/version_spec.rb +5 -0
- data/spec/models/mercado_pago/client_spec.rb +130 -0
- data/spec/models/mercado_pago/notification_spec.rb +17 -0
- data/spec/models/mercado_pago/order_preferences_builder_spec.rb +58 -0
- data/spec/models/spree/payment_method/mercado_pago_spec.rb +34 -0
- data/spec/services/mercado_pago/process_notification_spec.rb +82 -0
- data/spec/spec_helper.rb +133 -0
- metadata +200 -0
@@ -0,0 +1 @@
|
|
1
|
+
<!-- Source form -->
|
@@ -0,0 +1 @@
|
|
1
|
+
<!-- Source view -->
|
File without changes
|
@@ -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>
|
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,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,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,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,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,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
|