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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2176d57becc66dfbc62cabcb7d0b63104972ebad
|
4
|
+
data.tar.gz: a0a527c78f3633ff7bb81616fe6c34a92398e2ae
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 279d07393c455c6279a3ef25eefb6e1ae5bf2b3dbbcdcadefcc9932c87360746390cc21b39db3116a44de1561934134ac9d031cf178e86af05331b6b4c85c276
|
7
|
+
data.tar.gz: 69452afdba14d5f37c79a239a85f180ff507ef37f2128e1922c39862a90ec71f8101096ce3d60751171a07e9b6e5347838d777d2e98600e66036d5e5a30c1c37
|
File without changes
|
@@ -0,0 +1,25 @@
|
|
1
|
+
//= require spree/frontend
|
2
|
+
|
3
|
+
MercadoPago = {
|
4
|
+
hidePaymentSaveAndContinueButton: function(paymentMethod) {
|
5
|
+
if (MercadoPago.paymentMethodID && paymentMethod.val() == MercadoPago.paymentMethodID) {
|
6
|
+
$('.continue').hide();
|
7
|
+
$('[data-hook=coupon_code]').hide();
|
8
|
+
} else {
|
9
|
+
$('.continue').show();
|
10
|
+
$('[data-hook=coupon_code]').show();
|
11
|
+
}
|
12
|
+
}
|
13
|
+
};
|
14
|
+
|
15
|
+
$(document).ready(function() {
|
16
|
+
checkedPaymentMethod = $('div[data-hook="checkout_payment_step"] input[type="radio"]:checked');
|
17
|
+
MercadoPago.hidePaymentSaveAndContinueButton(checkedPaymentMethod);
|
18
|
+
paymentMethods = $('div[data-hook="checkout_payment_step"] input[type="radio"]').click(function (e) {
|
19
|
+
MercadoPago.hidePaymentSaveAndContinueButton($(e.target));
|
20
|
+
});
|
21
|
+
|
22
|
+
$('button.mercado_pago_button').click(function(event){
|
23
|
+
$(event.target).prop("disabled",true);
|
24
|
+
});
|
25
|
+
});
|
@@ -0,0 +1 @@
|
|
1
|
+
/* Custom backend assets */
|
@@ -0,0 +1 @@
|
|
1
|
+
/* Custom frontend assets */
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Spree
|
2
|
+
class MercadoPagoController < StoreController
|
3
|
+
protect_from_forgery except: :ipn
|
4
|
+
# skip_before_action :set_current_order, only: :ipn
|
5
|
+
|
6
|
+
def checkout
|
7
|
+
current_order.state_name == :payment || raise(ActiveRecord::RecordNotFound)
|
8
|
+
payment_method = PaymentMethod::MercadoPago.find(params[:payment_method_id])
|
9
|
+
payment = current_order.payments
|
10
|
+
.create!(amount: current_order.total, payment_method: payment_method)
|
11
|
+
payment.started_processing!
|
12
|
+
|
13
|
+
preferences = ::MercadoPago::OrderPreferencesBuilder
|
14
|
+
.new(current_order, payment, callback_urls)
|
15
|
+
.preferences_hash
|
16
|
+
|
17
|
+
provider = payment_method.provider
|
18
|
+
provider.create_preferences(preferences)
|
19
|
+
|
20
|
+
redirect_to provider.redirect_url
|
21
|
+
end
|
22
|
+
|
23
|
+
# Success/pending callbacks are currently aliases, this may change
|
24
|
+
# if required.
|
25
|
+
def success
|
26
|
+
payment.order.next
|
27
|
+
flash.notice = Spree.t(:order_processed_successfully)
|
28
|
+
flash['order_completed'] = true
|
29
|
+
redirect_to spree.order_path(payment.order)
|
30
|
+
end
|
31
|
+
|
32
|
+
def failure
|
33
|
+
payment.failure!
|
34
|
+
flash.notice = Spree.t(:payment_processing_failed)
|
35
|
+
flash['order_completed'] = true
|
36
|
+
redirect_to spree.checkout_state_path(state: :payment)
|
37
|
+
end
|
38
|
+
|
39
|
+
def ipn
|
40
|
+
notification = MercadoPago::Notification
|
41
|
+
.new(operation_id: params[:id], topic: params[:topic])
|
42
|
+
|
43
|
+
if notification.save
|
44
|
+
MercadoPago::HandleReceivedNotification.new(notification).process!
|
45
|
+
status = :ok
|
46
|
+
else
|
47
|
+
status = :bad_request
|
48
|
+
end
|
49
|
+
|
50
|
+
render json: :empty, status: status
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def payment
|
56
|
+
@payment ||= Spree::Payment.where(number: params[:external_reference])
|
57
|
+
.first
|
58
|
+
end
|
59
|
+
|
60
|
+
def callback_urls
|
61
|
+
@callback_urls ||= {
|
62
|
+
success: mercado_pago_success_url,
|
63
|
+
pending: mercado_pago_success_url,
|
64
|
+
failure: mercado_pago_failure_url
|
65
|
+
}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'rest_client'
|
2
|
+
require 'mercado_pago/client/authentication'
|
3
|
+
require 'mercado_pago/client/preferences'
|
4
|
+
require 'mercado_pago/client/api'
|
5
|
+
|
6
|
+
module MercadoPago
|
7
|
+
class Client
|
8
|
+
# These three includes are because of the user of line_item_description from
|
9
|
+
# ProductsHelper
|
10
|
+
include Authentication
|
11
|
+
include Preferences
|
12
|
+
include API
|
13
|
+
|
14
|
+
attr_reader :errors
|
15
|
+
attr_reader :auth_response
|
16
|
+
attr_reader :preferences_response
|
17
|
+
|
18
|
+
def initialize(payment_method, options = {})
|
19
|
+
@payment_method = payment_method
|
20
|
+
@api_options = options.clone
|
21
|
+
@errors = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_operation_info(operation_id)
|
25
|
+
url = create_url(notifications_url(operation_id), access_token: access_token)
|
26
|
+
options = { content_type: 'application/x-www-form-urlencoded', accept: 'application/json' }
|
27
|
+
get(url, options, quiet: true)
|
28
|
+
end
|
29
|
+
|
30
|
+
# def get_external_reference(operation_id)
|
31
|
+
# response = send_notification_request(operation_id)
|
32
|
+
# if response
|
33
|
+
# response['collection']['external_reference']
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
|
37
|
+
def get_payment_status(external_reference)
|
38
|
+
response = send_search_request(external_reference: external_reference, access_token: access_token)
|
39
|
+
|
40
|
+
if response['results'].empty?
|
41
|
+
'pending'
|
42
|
+
else
|
43
|
+
response['results'][0]['collection']['status']
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def log_error(msg, response, request, result)
|
50
|
+
Rails.logger.info msg
|
51
|
+
Rails.logger.info "response: #{response}."
|
52
|
+
Rails.logger.info "request args: #{request.args}."
|
53
|
+
Rails.logger.info "result #{result}."
|
54
|
+
end
|
55
|
+
|
56
|
+
def send_search_request(params, _options = {})
|
57
|
+
url = create_url(search_url, params)
|
58
|
+
options = { content_type: 'application/x-www-form-urlencoded', accept: 'application/json' }
|
59
|
+
get(url, options)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
class MercadoPago::Client
|
4
|
+
module API
|
5
|
+
def redirect_url
|
6
|
+
point_key = sandbox ? 'sandbox_init_point' : 'init_point'
|
7
|
+
@preferences_response[point_key]
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def notifications_url(operation_id)
|
13
|
+
sandbox_part = sandbox ? 'sandbox/' : ''
|
14
|
+
"https://api.mercadolibre.com/#{sandbox_part}collections/notifications/#{operation_id}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def search_url
|
18
|
+
sandbox_part = sandbox ? 'sandbox/' : ''
|
19
|
+
"https://api.mercadolibre.com/#{sandbox_part}collections/search"
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_url(url, params = {})
|
23
|
+
uri = URI(url)
|
24
|
+
uri.query = URI.encode_www_form(params)
|
25
|
+
uri.to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
def preferences_url(token)
|
29
|
+
create_url('https://api.mercadolibre.com/checkout/preferences', access_token: token)
|
30
|
+
end
|
31
|
+
|
32
|
+
def sandbox
|
33
|
+
Rails.application.try(:secrets).try(:[], :mercadopago).try(:[], :sandbox)
|
34
|
+
end
|
35
|
+
|
36
|
+
def get(url, request_options = {}, options = {})
|
37
|
+
response = RestClient.get(url, request_options)
|
38
|
+
JSON.parse(response)
|
39
|
+
# TODO: add class to rescue
|
40
|
+
rescue => e
|
41
|
+
raise e unless options[:quiet]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
class MercadoPago::Client
|
4
|
+
module Authentication
|
5
|
+
def authenticate
|
6
|
+
response = send_authentication_request
|
7
|
+
@auth_response = JSON.parse(response)
|
8
|
+
rescue RestClient::Exception => e
|
9
|
+
@errors << I18n.t(:authentication_error, scope: :mercado_pago)
|
10
|
+
raise e.message
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def send_authentication_request
|
16
|
+
RestClient.post(
|
17
|
+
'https://api.mercadolibre.com/oauth/token',
|
18
|
+
{ grant_type: 'client_credentials', client_id: client_id, client_secret: client_secret },
|
19
|
+
content_type: 'application/x-www-form-urlencoded', accept: 'application/json'
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def client_id
|
24
|
+
Rails.application.try(:secrets).try(:[], :mercadopago).try(:[], :client_id)
|
25
|
+
end
|
26
|
+
|
27
|
+
def client_secret
|
28
|
+
Rails.application.try(:secrets).try(:[], :mercadopago).try(:[], :client_secret)
|
29
|
+
end
|
30
|
+
|
31
|
+
def access_token
|
32
|
+
authenticate unless @auth_response
|
33
|
+
@auth_response['access_token']
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
class MercadoPago::Client
|
4
|
+
module Preferences
|
5
|
+
def create_preferences(preferences)
|
6
|
+
response = send_preferences_request(preferences)
|
7
|
+
@preferences_response = JSON.parse(response)
|
8
|
+
rescue RestClient::Exception => e
|
9
|
+
@errors << I18n.t(:authentication_error, scope: :mercado_pago)
|
10
|
+
raise e.message
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def send_preferences_request(preferences)
|
16
|
+
RestClient.post(preferences_url(access_token), preferences.to_json,
|
17
|
+
content_type: 'application/json', accept: 'application/json')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module MercadoPago
|
2
|
+
class Notification < ActiveRecord::Base
|
3
|
+
self.table_name = 'mercado_pago_notifications'
|
4
|
+
|
5
|
+
validates :topic, presence: true, inclusion: { in: %w[payment preapproval authorized_payment merchant_order] }
|
6
|
+
validates :operation_id, presence: true
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module MercadoPago
|
2
|
+
class OrderPreferencesBuilder
|
3
|
+
include ActionView::Helpers::TextHelper
|
4
|
+
include ActionView::Helpers::SanitizeHelper
|
5
|
+
include Spree::ProductsHelper
|
6
|
+
|
7
|
+
def initialize(order, payment, callback_urls, payer_data = nil)
|
8
|
+
@order = order
|
9
|
+
@payment = payment
|
10
|
+
@callback_urls = callback_urls
|
11
|
+
@payer_data = payer_data
|
12
|
+
end
|
13
|
+
|
14
|
+
def preferences_hash
|
15
|
+
{
|
16
|
+
external_reference: @payment.number,
|
17
|
+
back_urls: @callback_urls,
|
18
|
+
payer: @payer_data,
|
19
|
+
items: generate_items
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def generate_items
|
26
|
+
items = []
|
27
|
+
|
28
|
+
items += generate_items_from_line_items
|
29
|
+
items += generate_items_from_adjustments
|
30
|
+
items += generate_items_from_shipments
|
31
|
+
|
32
|
+
items
|
33
|
+
end
|
34
|
+
|
35
|
+
def generate_items_from_shipments
|
36
|
+
@order.shipments.collect do |shipment|
|
37
|
+
{
|
38
|
+
title: shipment.shipping_method.name,
|
39
|
+
unit_price: shipment.cost.to_f + shipment.adjustment_total.to_f,
|
40
|
+
quantity: 1
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def generate_items_from_line_items
|
46
|
+
@order.line_items.collect do |line_item|
|
47
|
+
{
|
48
|
+
title: line_item_description_text(line_item.variant.product.name),
|
49
|
+
unit_price: line_item.price.to_f,
|
50
|
+
quantity: line_item.quantity
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def generate_items_from_adjustments
|
56
|
+
@order.adjustments.eligible.collect do |adjustment|
|
57
|
+
{
|
58
|
+
title: line_item_description_text(adjustment.label),
|
59
|
+
unit_price: adjustment.amount.to_f,
|
60
|
+
quantity: 1
|
61
|
+
}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Spree
|
2
|
+
class PaymentMethod::MercadoPago < PaymentMethod
|
3
|
+
def payment_profiles_supported?
|
4
|
+
false
|
5
|
+
end
|
6
|
+
|
7
|
+
def provider_class
|
8
|
+
::MercadoPago::Client
|
9
|
+
end
|
10
|
+
|
11
|
+
def provider(additional_options = {})
|
12
|
+
@provider ||= begin
|
13
|
+
options = { sandbox: preferred_sandbox }
|
14
|
+
client = provider_class.new(self, options.merge(additional_options))
|
15
|
+
client.authenticate
|
16
|
+
client
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def source_required?
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
def auto_capture?
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
def preferred_sandbox
|
29
|
+
Rails.application.try(:secrets).try(:[], :mercadopago).try(:[], :sandbox)
|
30
|
+
end
|
31
|
+
|
32
|
+
## Admin options
|
33
|
+
|
34
|
+
def can_void?(payment)
|
35
|
+
payment.state != 'void'
|
36
|
+
end
|
37
|
+
|
38
|
+
def actions
|
39
|
+
%w[void]
|
40
|
+
end
|
41
|
+
|
42
|
+
def void(*_args)
|
43
|
+
ActiveMerchant::Billing::Response.new(true, '', {}, {})
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module MercadoPago
|
2
|
+
class HandleReceivedNotification
|
3
|
+
def initialize(notification)
|
4
|
+
@notification = notification
|
5
|
+
end
|
6
|
+
|
7
|
+
# The purpose of this method is to enable async/sync processing
|
8
|
+
# of Mercado Pago IPNs. For simplicity processing is synchronous but
|
9
|
+
# if you would like to enqueue the processing via Resque/Ost/etc you
|
10
|
+
# will be able to do it.
|
11
|
+
def process!
|
12
|
+
# Sync
|
13
|
+
ProcessNotification.new(@notification).process!
|
14
|
+
# Async Will be configurable via block for example:
|
15
|
+
# Resque.enqueue(ProcessNotificationWorker, {id: @notification.id})
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# Process notification:
|
2
|
+
# ---------------------
|
3
|
+
# Fetch collection information
|
4
|
+
# Find payment by external reference
|
5
|
+
# If found
|
6
|
+
# Update payment status
|
7
|
+
# Notify user
|
8
|
+
# If not found
|
9
|
+
# Ignore notification (maybe payment from outside Spree)
|
10
|
+
module MercadoPago
|
11
|
+
class ProcessNotification
|
12
|
+
# Equivalent payment states
|
13
|
+
# MP state => Spree state
|
14
|
+
# =======================
|
15
|
+
#
|
16
|
+
# approved => complete
|
17
|
+
# pending => pend
|
18
|
+
# in_process => pend
|
19
|
+
# rejected => failed
|
20
|
+
# refunded => void
|
21
|
+
# cancelled => void
|
22
|
+
# in_mediation => pend
|
23
|
+
# charged_back => void
|
24
|
+
STATES = {
|
25
|
+
complete: %w[approved],
|
26
|
+
failure: %w[rejected],
|
27
|
+
void: %w[refunded cancelled charged_back]
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
attr_reader :notification
|
31
|
+
|
32
|
+
def initialize(notification)
|
33
|
+
@notification = notification
|
34
|
+
end
|
35
|
+
|
36
|
+
def process!
|
37
|
+
# Fix: Payment method is an instance of Spree::PaymentMethod::MercadoPago not THE class
|
38
|
+
client = ::Spree::PaymentMethod::MercadoPago.first.provider
|
39
|
+
raw_op_info = client.get_operation_info(notification.operation_id)
|
40
|
+
op_info = raw_op_info['collection'] if raw_op_info.present?
|
41
|
+
# TODO: rewrite this.
|
42
|
+
if op_info && (payment = Spree::Payment.where(number: op_info['external_reference']).first)
|
43
|
+
if STATES[:complete].include?(op_info['status'])
|
44
|
+
payment.complete
|
45
|
+
elsif STATES[:failure].include?(op_info['status'])
|
46
|
+
payment.failure
|
47
|
+
elsif STATES[:void].include?(op_info['status'])
|
48
|
+
payment.void
|
49
|
+
end
|
50
|
+
|
51
|
+
# When Spree issue #5246 is fixed we can remove this line
|
52
|
+
payment.order.updater.update
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|