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