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.
- 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
|