solidus_mp_dois 2.2.2 → 3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2c3187a7e497cd255cf9661e338603946221b2d7cdc4dde18d707c4b636baac
4
- data.tar.gz: c4593ccb461d7f9ef9fc20967db9410878bb2d2e3537cecc846fc49365ace42e
3
+ metadata.gz: 0f47d658a64a40e54735865036c9331ff4b27c0740789c9afd9074ebce5553f6
4
+ data.tar.gz: 595b3dc81627c86ca7c9e5d76214acd727afd78716cd2e376d555e9ba06a2f1d
5
5
  SHA512:
6
- metadata.gz: 4f574a527dd9609a70a13793d359f94b1aad0cf9e6b6b5868b0a5ee1042603d6ee953881355a962282bc974941993814255d686e84b83c64e9a169853847d183
7
- data.tar.gz: 27a4cb0baeb08e06ca551b352a7c0763f32ef6d961f499751a8cd582bdd5d4be08d6a77afea5679d839a0a33ce1d40d78d3e76240bbc34f6561fed65a7195dfa
6
+ metadata.gz: 5d38025a7d3d9097863ddad2cf466a31a522ae5094e4f558a72c8af99593d2ea758d5592d5b57ee47887b340787bbdca30cc7f913328058fa1fe769e1955d3cf
7
+ data.tar.gz: f5a1439538d3d45b38d39effa81537c310d74f34e279a3a0875676cfb0f194a08bd196dff57525f62c5bafb14818e02e2fdec550254fc7d78eef61d8e0db52a4
data/README.md ADDED
@@ -0,0 +1,250 @@
1
+ # Solidus Mercado Pago 2 🇧🇷
2
+
3
+ [![Version](https://img.shields.io/badge/version-2.2.2-blue.svg)](https://github.com/todasessascoisas/solidus_mp_dois)
4
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%202.7-red.svg)](https://ruby-lang.org)
5
+ [![Rails](https://img.shields.io/badge/rails-%3E%3D%206.0-red.svg)](https://rubyonrails.org)
6
+
7
+ Integração moderna do Mercado Pago com Solidus, oferecendo suporte completo para pagamentos via **cartão de crédito** e **PIX**, com interface otimizada e recursos avançados.
8
+
9
+ ## 🚀 Funcionalidades
10
+
11
+ - ✅ **Pagamentos com Cartão de Crédito**
12
+ - Integração com Mercado Pago SDK-JS
13
+ - Suporte a cartões salvos
14
+ - Autenticação 3D Secure
15
+ - Múltiplas parcelas
16
+ - Interface responsiva com Payment Brick
17
+
18
+ - ✅ **Pagamentos via PIX**
19
+ - Geração automática de QR Code
20
+ - Código copia e cola
21
+ - Verificação automática de status
22
+ - Expiração configurável
23
+
24
+ - ✅ **Recursos Avançados**
25
+ - Gerenciamento de clientes
26
+ - Sincronização automática de status
27
+ - Interface administrativa completa
28
+ - Logs detalhados de transações
29
+ - Suporte a reembolsos e cancelamentos
30
+
31
+ - ✅ **Compatibilidade**
32
+ - Solidus 3.0+
33
+ - Rails 6.0+
34
+ - Ruby 2.7+
35
+ - Suporte a User e Spree::User
36
+
37
+ ## 📦 Instalação
38
+
39
+ ### 1. Adicione a gem ao seu Gemfile
40
+
41
+ ```ruby
42
+ gem 'solidus_mp_dois'
43
+ ```
44
+
45
+ ### 2. Execute o bundle install
46
+
47
+ ```bash
48
+ bundle install
49
+ ```
50
+
51
+ ### 3. Execute o gerador de instalação
52
+
53
+ ```bash
54
+ rails generate solidus_mp_dois:install
55
+ ```
56
+
57
+ Este comando irá:
58
+ - Instalar a gem `mp_api`
59
+ - Adicionar as migrações necessárias
60
+ - Configurar o Importmap para o SDK do Mercado Pago
61
+ - Copiar os controladores Stimulus necessários
62
+ - Executar as migrações (opcional)
63
+
64
+ ### 4. Execute as migrações (se não executadas automaticamente)
65
+
66
+ ```bash
67
+ bin/rails railties:install:migrations FROM=solidus_mp_dois
68
+ ```
69
+
70
+ ## ⚙️ Configuração
71
+
72
+ ### 1. Adicione os métodos de pagamento no painel administrativo
73
+
74
+ Acesse o painel administrativo do Solidus:
75
+ 1. Vá para **Configurações > Métodos de Pagamento**
76
+ 2. Clique em **Novo Método de Pagamento**
77
+ 3. Selecione o tipo desejado:
78
+ - `SolidusMpDois::MpCard` para cartão de crédito
79
+ - `SolidusMpDois::MpPix` para PIX
80
+
81
+ ### 2. Configure as credenciais do Mercado Pago
82
+
83
+ Para cada método de pagamento, configure:
84
+
85
+ - **Public Key**: Sua chave pública do Mercado Pago
86
+ - **Access Token**: Seu token de acesso do Mercado Pago
87
+ - **Statement Descriptor**: Descrição que aparece na fatura (opcional)
88
+
89
+ ### 3. Configure o frontend (Stimulus)
90
+
91
+ Se você estiver usando Importmap, os controladores já foram configurados. Para outros bundlers:
92
+
93
+ ```javascript
94
+ // application.js
95
+ import "controllers/card_payment_brick_controller"
96
+ import "controllers/payment_brick_controller"
97
+ import "controllers/three_ds_controller"
98
+ ```
99
+
100
+ ## 🎯 Uso
101
+
102
+ ### Pagamentos com Cartão de Crédito
103
+
104
+ #### Payment Brick (múltiplos métodos)
105
+ ```erb
106
+ <div data-controller="payment-brick"
107
+ data-payment-brick-public-key-value="<%= payment_method.preferences[:public_key] %>"
108
+ data-payment-brick-amount-value="<%= order.total %>"
109
+ data-payment-brick-email-value="<%= order.email %>"
110
+ data-payment-brick-customer-id-value="<%= customer_id %>"
111
+ data-payment-brick-cards-ids-value="<%= customer_cards_ids %>">
112
+ <div id="paymentBrick_container"></div>
113
+ </div>
114
+ ```
115
+
116
+ #### Card Payment Brick (apenas cartão)
117
+ ```erb
118
+ <div data-controller="card-payment-brick"
119
+ data-card-payment-brick-public-key-value="<%= payment_method.preferences[:public_key] %>"
120
+ data-card-payment-brick-amount-value="<%= order.total %>"
121
+ data-card-payment-brick-email-value="<%= order.email %>">
122
+ <div id="cardPaymentBrick_container"></div>
123
+ </div>
124
+ ```
125
+
126
+ #### Autenticação 3D Secure
127
+ ```erb
128
+ <div data-controller="three-ds"
129
+ data-three-ds-three-ds-url-value="<%= payment.source.three_ds_url %>"
130
+ data-three-ds-three-ds-creq-value="<%= payment.source.three_ds_creq %>">
131
+ </div>
132
+ ```
133
+
134
+ ### Pagamentos via PIX
135
+
136
+ O PIX é processado automaticamente. Após a criação do pagamento, o sistema:
137
+ 1. Gera o QR Code e código copia e cola
138
+ 2. Monitora o status automaticamente
139
+ 3. Atualiza o pedido quando o pagamento é confirmado
140
+
141
+ ## 🗄️ Estrutura do Banco de Dados
142
+
143
+ ### Tabelas Criadas
144
+
145
+ - `solidus_mp_dois_credit_card_sources` - Fontes de pagamento por cartão
146
+ - `solidus_mp_dois_pix_sources` - Fontes de pagamento PIX
147
+ - `solidus_mp_dois_customers` - Clientes do Mercado Pago
148
+ - `solidus_mp_dois_cards` - Cartões salvos
149
+
150
+ ### Campos Principais
151
+
152
+ #### Credit Card Sources
153
+ - `external_id` - ID do pagamento no Mercado Pago
154
+ - `status` - Status do pagamento
155
+ - `amount` - Valor do pagamento
156
+ - `installments` - Número de parcelas
157
+ - `three_ds_url` - URL para autenticação 3DS
158
+ - `saved_card` - Indica se é cartão salvo
159
+
160
+ #### PIX Sources
161
+ - `external_id` - ID do pagamento no Mercado Pago
162
+ - `qr_code` - Código copia e cola
163
+ - `qr_code_base64` - QR Code em base64
164
+ - `expiration` - Data de expiração
165
+ - `ticket_url` - URL do ticket de pagamento
166
+
167
+ ## 🔧 API
168
+
169
+ ### Métodos Principais
170
+
171
+ #### MpCard
172
+ ```ruby
173
+ # Criar pagamento
174
+ payment = mp_card.create_payment(order, source_params)
175
+
176
+ # Processar pagamento
177
+ response = mp_card.purchase(payment)
178
+
179
+ # Cancelar pagamento
180
+ mp_card.cancel!(payment)
181
+ ```
182
+
183
+ #### MpPix
184
+ ```ruby
185
+ # Criar pagamento PIX
186
+ payment = mp_pix.create_payment(order)
187
+
188
+ # Verificar se foi pago
189
+ paid = mp_pix.pix_paid?(payment)
190
+
191
+ # Invalidar pagamento
192
+ mp_pix.invalidate_payment(payment)
193
+ ```
194
+
195
+ ### Sincronização de Status
196
+
197
+ Os pagamentos são sincronizados automaticamente com o Mercado Pago:
198
+
199
+ ```ruby
200
+ # Buscar status atualizado
201
+ mp_payment = payment.source.retrieve_from_api
202
+
203
+ # Sincronizar manualmente
204
+ payment_method.purchase(payment)
205
+ ```
206
+
207
+ ## 🎨 Interface Administrativa
208
+
209
+ A gem inclui views administrativas completas:
210
+
211
+ - **Visualização de pagamentos** com todos os detalhes
212
+ - **QR Codes** para pagamentos PIX
213
+ - **Informações do cartão** (últimos 4 dígitos, bandeira, etc.)
214
+ - **Status detalhado** e logs de transação
215
+
216
+ ## 🔒 Segurança
217
+
218
+ - ✅ Tokens seguros para cartões
219
+ - ✅ Autenticação 3D Secure automática
220
+ - ✅ Validação de documentos (CPF/CNPJ)
221
+ - ✅ Criptografia end-to-end via Mercado Pago
222
+ - ✅ Logs detalhados para auditoria
223
+
224
+ ## 🌐 Localização
225
+
226
+ A gem inclui traduções em português brasileiro:
227
+
228
+ ```yaml
229
+ pt-BR:
230
+ activerecord:
231
+ models:
232
+ solidus_mp_dois/mp_pix: "Pix (Mercado Pago 2)"
233
+ solidus_mp_dois/mp_card: "Cartão de Crédito (Mercado Pago 2)"
234
+ ```
235
+
236
+ ## 🧪 Testes
237
+
238
+ ```bash
239
+ # Executar todos os testes
240
+ bundle exec rake
241
+
242
+ # Executar testes específicos
243
+ bundle exec rspec spec/models/
244
+ ```
245
+
246
+ ## 📋 Dependências
247
+
248
+ - [`solidus_brazilian_adaptations`](https://github.com/solidusio/solidus_brazilian_adaptations) - Adaptações para o mercado brasileiro
249
+ - [`mp_api`](https://github.com/todasessascoisas/mp_api) - Cliente Ruby para API do Mercado Pago
250
+ - `@mercadopago/sdk-js` - SDK JavaScript oficial do Mercado Pago
@@ -3,8 +3,7 @@ module SolidusMpDois
3
3
  belongs_to :customer
4
4
 
5
5
  def delete_from_api(access_token)
6
- request = Typhoeus.delete("https://api.mercadopago.com/v1/customers/#{customer.external_id}/cards/#{external_id}", headers: { "Authorization" => "Bearer #{access_token}", "Content-Type" => "application/json" })
7
- request.success?
6
+ MpApi::Client.new(access_token).delete_card(customer_id: customer.external_id, card_id: external_id)
8
7
  end
9
8
  end
10
9
  end
@@ -4,8 +4,8 @@ module SolidusMpDois
4
4
  payment_method.find_payment(external_id)
5
5
  end
6
6
 
7
- def cancel
8
- payment_method.cancel!(external_id)
7
+ def void!
8
+ payment_method.cancel!(payments.sole)
9
9
  end
10
10
  end
11
11
  end
@@ -8,10 +8,6 @@ module SolidusMpDois
8
8
  CreditCardSource
9
9
  end
10
10
 
11
- def gateway_class
12
- MpGateway
13
- end
14
-
15
11
  def supports?(source)
16
12
  source.is_a?(payment_source_class)
17
13
  end
@@ -25,38 +21,54 @@ module SolidusMpDois
25
21
  end
26
22
 
27
23
  def find_payment external_id
28
- MpApi.configuration.access_token = preferences[:access_token]
29
- MpApi::Payment.find_by_id(external_id)
24
+ mp_client.get_payment(external_id)
30
25
  end
31
26
 
32
27
  def create_payment order, source_params
33
28
  customer = find_or_create_customer(order, source_params)
34
29
  save_credit_card(source_params[:token], customer) if customer && source_params[:saved_card] == "false" # Se o cartão usado ja for um cartão salvo, não é necessario executar save_credit_card
30
+ invalidate_valid_payments(order)
35
31
 
36
32
  payment = order.payments.new(amount: order.total, payment_method: self)
37
33
  payment.source = init_source(order, source_params, customer)
38
34
  payment.save
39
35
 
40
- mp_payment = create_mp_payment(payment.source, customer.try(:external_id))
36
+ mp_payment = if payment.source.saved_card
37
+ create_payment_with_saved_card(payment.source, customer.try(:external_id))
38
+ else
39
+ create_payment_with_new_card(payment.source)
40
+ end
41
41
  process_payment_response(payment, mp_payment)
42
42
  payment
43
43
  end
44
44
 
45
- def purchase money, source, options = {}
46
- gateway.purchase(money, source, options)
45
+ def purchase(payment)
46
+ return unless payment.source&.external_id
47
+ 10.times do
48
+ mp_payment = find_payment(payment.source.external_id)
49
+ break if ["approved", "rejected"].include? mp_payment.status
50
+ end
51
+ mp_payment = find_payment(payment.source.external_id)
52
+ sync(payment, mp_payment)
47
53
  end
48
54
 
49
- def cancel!(mp_payment_id)
50
- MpApi.configuration.access_token = preferences[:access_token]
55
+ def cancel!(payment)
56
+ external_id = payment.source&.external_id
57
+ return unless external_id
51
58
  sleep(1)
52
- mp_payment = MpApi::Payment.find_by_id(mp_payment_id)
53
- if !mp_payment.status.in?(["rejected", "cancelled"])
54
- mp_payment.update(status: "cancelled")
55
- end
59
+ purchase
60
+ return unless ["checkout", "pending"].include? payment.state
61
+
62
+ mp_payment = mp_client.update_payment(payment_id: external_id, status: "cancelled")
63
+ sync(payment, mp_payment)
56
64
  end
57
65
 
58
66
  private
59
67
 
68
+ def mp_client
69
+ MpApi::Client.new(preferences[:access_token])
70
+ end
71
+
60
72
  def init_source order, source_params, customer
61
73
  tax_id = customer&.tax_id || source_params[:tax_id]
62
74
  document_type = (tax_id.gsub(/\D/, "").length == 11) ? "CPF" : "CNPJ"
@@ -76,10 +88,19 @@ module SolidusMpDois
76
88
  )
77
89
  end
78
90
 
79
- def create_mp_payment payment_source, customer_id
80
- MpApi.configuration.access_token = preferences[:access_token]
81
- MpApi::Payment.new(
91
+ def create_payment_with_saved_card payment_source, customer_id
92
+ mp_client.create_saved_credit_card_payment(
82
93
  amount: payment_source.amount.to_f,
94
+ token: payment_source.token,
95
+ installments: payment_source.installments,
96
+ customer_id: customer_id
97
+ )
98
+ end
99
+
100
+ def create_payment_with_new_card payment_source
101
+ mp_client.create_credit_card_payment(
102
+ amount: payment_source.amount.to_f,
103
+ statement_descriptor: preferences[:statement_descriptor],
83
104
  payment_method: payment_source.card_brand,
84
105
  payer_email: payment_source.email,
85
106
  payer_identification_type: payment_source.payer_identification_type,
@@ -87,76 +108,56 @@ module SolidusMpDois
87
108
  token: payment_source.token,
88
109
  issuer_id: payment_source.issuer_id,
89
110
  installments: payment_source.installments,
90
- three_d_secure_mode: true,
91
- statement_descriptor: preferences[:statement_descriptor],
92
- saved_card: payment_source.saved_card,
93
- customer_id: customer_id
94
- ).create
111
+ three_d_secure_mode: true
112
+ )
95
113
  end
96
114
 
97
115
  def find_or_create_customer(order, source_params)
98
116
  order_user = order.try(:store_user) || order&.user # Para suportar lojas que usam Spree::User e User
99
117
  return unless order_user
100
118
 
101
- mp_customer = find_mp_customer(order_user)
102
- if mp_customer
103
- # Pode ser que exista na API mas nao no banco de dados
104
- SolidusMpDois::Customer.upsert({
105
- external_id: mp_customer.external_id,
106
- email: mp_customer.email,
107
- first_name: mp_customer.first_name,
108
- identification_type: mp_customer.identification_type,
109
- tax_id: mp_customer.identification_number,
110
- spree_user_id: order_user.id
111
- }, unique_by: [:external_id]
112
- )
113
- else
114
- created_mp_customer = create_mp_customer(order, source_params, order_user)
115
- if created_mp_customer.error.blank?
116
- SolidusMpDois::Customer.create(
117
- external_id: created_mp_customer.external_id,
118
- email: created_mp_customer.email,
119
- first_name: created_mp_customer.first_name,
120
- identification_type: created_mp_customer.identification_type,
121
- tax_id: created_mp_customer.identification_number,
122
- spree_user_id: order_user.id
123
- )
124
- end
119
+ email = source_params[:payer_email].present? ? source_params[:payer_email] : (order_user.try(:email) || order_user.try(:email_address))
120
+ mp_customer = find_mp_customer(email).results[0]
121
+
122
+ if mp_customer.nil?
123
+ mp_customer = create_mp_customer(order, source_params, email)
125
124
  end
126
- SolidusMpDois::Customer.find_by(spree_user_id: order_user.id)
125
+
126
+ SolidusMpDois::Customer.upsert(
127
+ {
128
+ external_id: mp_customer.id,
129
+ email: mp_customer.email,
130
+ first_name: mp_customer.first_name,
131
+ identification_type: mp_customer.identification.type,
132
+ tax_id: mp_customer.identification.number,
133
+ spree_user_id: order_user.id
134
+ }, unique_by: [:external_id]
135
+ )
136
+
137
+ SolidusMpDois::Customer.find_by(external_id: mp_customer.id)
127
138
  end
128
139
 
129
- def create_mp_customer(order, source_params, user)
130
- MpApi.configuration.access_token = preferences[:access_token]
131
- email = user.try(:email) || user.try(:email_address)
132
- MpApi::Customer.new(
140
+ def create_mp_customer(order, source_params, email)
141
+ mp_client.create_customer(
133
142
  email: email,
134
143
  first_name: order.ship_address.name,
135
144
  identification_type: source_params[:payer_identification_type],
136
145
  identification_number: source_params[:tax_id]
137
- ).create
146
+ )
138
147
  end
139
148
 
140
- # A versão da MpApi nessa gem é limitada ("< 1.3.0") então não posso atualizar a mp_api.
141
- # Por conta disso, precisei definir esse metodo aqui
142
- # A limitação da versao foi removida, entao esse metodo pode ser implementado na mp_api
143
- def find_mp_customer(user)
144
- email = user.try(:email) || user.try(:email_address)
145
- search_req = Typhoeus.get("https://api.mercadopago.com/v1/customers/search?email=#{email}", headers: { "Authorization" => "Bearer #{preferences[:access_token]}" })
146
- search_response = JSON.parse(search_req.body)
147
- customer_json = search_response["results"].find { |r| r.dig("email") == email }
148
- MpApi::Customer.new(**MpApi::Customer.build_hash(customer_json)) if customer_json
149
+ def find_mp_customer(email)
150
+ mp_client.get_customer(email)
149
151
  end
150
152
 
151
153
  def save_credit_card(card_token, customer)
152
154
  return if customer.nil?
153
- MpApi.configuration.access_token = preferences[:access_token]
154
- mp_credit_card = MpApi::Card.new(token: card_token, customer_id: customer.external_id).create
155
+ mp_credit_card = mp_client.create_card(customer_id: customer.external_id, token: card_token)
155
156
  SolidusMpDois::Card.upsert(
156
157
  {
157
- external_id: mp_credit_card.external_id,
158
+ external_id: mp_credit_card.id,
158
159
  last_four_digits: mp_credit_card.last_four_digits,
159
- mp_payment_method_id: mp_credit_card.mp_payment_method_id,
160
+ mp_payment_method_id: mp_credit_card.payment_method.id,
160
161
  customer_id: customer.id
161
162
  }, unique_by: :external_id
162
163
  )
@@ -165,49 +166,112 @@ module SolidusMpDois
165
166
  def process_payment_response(payment, mp_payment)
166
167
  payment.source.update(
167
168
  external_id: mp_payment.id,
168
- three_ds_url: mp_payment.three_ds_info_external_resource_url,
169
- three_ds_creq: mp_payment.three_ds_info_creq,
170
- last_four_digits: mp_payment.last_four_digits
169
+ three_ds_url: mp_payment["three_ds_info"]&.external_resource_url,
170
+ three_ds_creq: mp_payment["three_ds_info"]&.creq,
171
+ last_four_digits: mp_payment.card.last_four_digits
171
172
  )
172
- if mp_payment.id.blank? || mp_payment.error || mp_payment.internal_error || mp_payment.status_detail == "pending_review_manual"
173
- handle_payment_error(payment, mp_payment)
173
+ payment.order.update(email: payment.source.email)
174
+ sync(payment, mp_payment)
175
+ end
176
+
177
+ def sync(payment, mp_payment)
178
+ paid_amount = Float(mp_payment.transaction_details.total_paid_amount)
179
+ payment.source.update!(status: mp_payment.status, internal_details: mp_payment.status_detail)
180
+
181
+ if mp_payment.status == "approved" && paid_amount >= payment.amount
182
+ approve_payment(payment, paid_amount)
183
+ elsif mp_payment.status == "approved"
184
+ raise "Payment paid with value less than the created - #{mp_payment.id}"
185
+ elsif (mp_payment.status == "pending" && mp_payment.status_detail == "pending_challenge")
186
+ pend_payment(payment)
187
+ else
188
+ invalid_payment(payment)
189
+ end
190
+ end
191
+
192
+ def approve_payment payment, paid_amount
193
+ payment.complete!
194
+ payment_source = payment.source
195
+ payment_source.update!(status: payment.state, total_paid_amount: paid_amount)
196
+ response = successful_response("Pagamento aprovado", status: payment_source.status, internal_detail: payment_source.internal_details)
197
+ payment.log_entries.create(parsed_payment_response_details_with_fallback: response)
198
+ end
199
+
200
+ def invalid_payment payment
201
+ if payment.checkout?
202
+ payment.invalidate!
174
203
  else
175
- payment.order.update(email: payment.source.email)
176
- update_payment_status(payment, mp_payment)
204
+ payment.failure!
177
205
  end
206
+
207
+ payment_source = payment.source
208
+ payment_source.update!(status: payment.state)
209
+ error_message = internal_error(payment_source.internal_details)
210
+ handle_payment_error(payment, error_message:)
211
+ end
212
+
213
+ def pend_payment payment
214
+ payment.pend! unless payment.pending?
215
+ payment.source.update!(status: payment.state)
216
+ error_message = "Pagamento aguardando 3ds"
217
+ handle_payment_error(payment, error_message:)
178
218
  end
179
219
 
180
- def update_payment_status(payment, mp_payment)
181
- status = case mp_payment.status
182
- when "approved" then "success"
183
- when "pending" then "pending"
220
+ def internal_error internal_detail
221
+ case internal_detail
222
+ when "by_collector"
223
+ "Pagamento cancelado"
224
+ when "refunded"
225
+ "Pagamento reembolsado"
226
+ when "reimbursed"
227
+ "Pagamento reembolsado"
228
+ when "cc_amount_rate_limit_exceeded"
229
+ "O pagamento foi rejeitado porque superou o limite do meio de pagamento"
230
+ when "cc_rejected_bad_filled_date"
231
+ "Data de vencimento inválida"
232
+ when "cc_rejected_bad_filled_card_number"
233
+ "Número do cartão inválido"
234
+ when "cc_rejected_bad_filled_security_code"
235
+ "Código de segurança do cartão (CVV) inválido"
236
+ when "cc_rejected_call_for_authorize"
237
+ "Pagamento recusado. Você deve autorizar o pagamento no seu banco"
238
+ when "cc_rejected_card_disabled"
239
+ "Pagamento recusado. Você deve ativar seu cartão"
240
+ when "cc_rejected_insufficient_amount"
241
+ "Limite insuficiente"
242
+ else
243
+ "Pagamento recusado"
184
244
  end
185
- payment.source.update!(status: status, internal_details: mp_payment.status_detail)
186
245
  end
187
246
 
188
- def handle_payment_error(payment, mp_payment)
189
- payment.invalidate
190
- error_message = mp_payment.error || mp_payment.status_detail || "Erro ao criar o pagamento"
191
- response = failure_response(error_message)
247
+ def invalidate_valid_payments order
248
+ order.payments.valid.each do |payment|
249
+ payment.source.void!
250
+ end
251
+ end
252
+
253
+ def handle_payment_error(payment, error_message:)
254
+ payment_source = payment.source
255
+ response = failure_response(error_message, status: payment_source.status, internal_detail: payment_source.internal_details)
192
256
  payment.log_entries.create(parsed_payment_response_details_with_fallback: response)
193
- payment.source.cancel if mp_payment.id.present? && mp_payment.status_detail == "pending_review_manual"
194
- payment.source.update(internal_error: error_message, status: "error", internal_details: mp_payment.status_detail)
257
+ payment_source.update!(internal_error: error_message)
195
258
  end
196
259
 
197
- def successful_response message, transaction_id
260
+ def successful_response message, status:, internal_detail:
261
+ full_message = message + " - " + status + ": " + internal_detail
198
262
  ActiveMerchant::Billing::Response.new(
199
263
  true,
200
- message,
201
- {},
202
- authorization: transaction_id
264
+ full_message
203
265
  )
204
266
  end
205
267
 
206
- def failure_response message
268
+ def failure_response message, status:, internal_detail:
269
+ full_message = message + " - " + status + ": " + internal_detail
207
270
  ActiveMerchant::Billing::Response.new(
208
271
  false,
209
- message
272
+ full_message
210
273
  )
211
274
  end
275
+
212
276
  end
213
277
  end
@@ -8,10 +8,6 @@ module SolidusMpDois
8
8
  PixSource
9
9
  end
10
10
 
11
- def gateway_class
12
- MpGateway
13
- end
14
-
15
11
  def supports?(source)
16
12
  source.is_a?(payment_source_class)
17
13
  end
@@ -25,40 +21,62 @@ module SolidusMpDois
25
21
  end
26
22
 
27
23
  def find_payment external_id
28
- MpApi.configuration.access_token = preferences[:access_token]
29
- MpApi::Payment.find_by_id(external_id)
24
+ mp_client.get_payment(external_id)
30
25
  end
31
26
 
32
27
  def create_payment order
33
28
  existing_payment = find_existing_payment(order)
34
29
  return existing_payment if payment_usable?(order, existing_payment)
30
+ invalidate_valid_payments(order, existing_payment&.id)
35
31
 
36
32
  payment = order.payments.new(amount: order.total, payment_method: self)
37
33
  payment.source = init_source(order)
38
34
  payment.save
35
+ payment_source = payment.source
36
+ identification_type = (payment_source.tax_id.length > 14) ? "CNPJ" : "CPF"
37
+
38
+ mp_payment = mp_client.create_pix_payment(
39
+ amount: payment_source.amount.to_f,
40
+ description: payment_source.description,
41
+ statement_descriptor: preferences[:statement_descriptor],
42
+ payment_method: "pix",
43
+ payer_email: payment_source.email,
44
+ payer_identification_type: identification_type,
45
+ payer_identification_number: payment_source.tax_id
46
+ )
39
47
 
40
- mp_payment = create_mp_payment(payment.source)
41
48
  process_payment_response(payment, mp_payment)
42
49
  payment
43
50
  end
44
51
 
45
- def invalidate_payment payment_source
46
- return false unless payment_source&.external_id
47
- MpApi.configuration.access_token = preferences[:access_token]
48
- mp_payment = find_payment(payment_source.external_id)
49
- return false if mp_payment.pix_paid?
50
- mp_payment.invalidate_pix!
51
- payment_source.payments[0].log_entries.create!(parsed_payment_response_details_with_fallback: failure_response("Pagamento cancelado"))
52
- payment_source.update(status: "cancelled")
53
- true
52
+ def purchase(payment)
53
+ return unless payment.source&.external_id
54
+ 10.times do
55
+ mp_payment = find_payment(payment.source.external_id)
56
+ break if ["approved", "rejected"].include? mp_payment.status
57
+ end
58
+ mp_payment = find_payment(payment.source.external_id)
59
+ sync(payment, mp_payment)
60
+ end
61
+
62
+ def pix_paid? payment
63
+ purchase(payment)
64
+ payment.completed?
54
65
  end
55
66
 
56
- def purchase(money, source, options = {})
57
- gateway.purchase(money, source, options)
67
+ def invalidate_payment payment
68
+ external_id = payment.source&.external_id
69
+ return if external_id.nil? || pix_paid?(payment)
70
+ mp_payment = mp_client.update_payment(payment_id: external_id, status: "cancelled")
71
+ sync(payment, mp_payment)
58
72
  end
59
73
 
60
74
  private
61
75
 
76
+ def mp_client
77
+ MpApi::Client.new(preferences[:access_token])
78
+ end
79
+
62
80
  def init_source(order)
63
81
  PixSource.new(
64
82
  amount: order.total,
@@ -69,22 +87,8 @@ module SolidusMpDois
69
87
  )
70
88
  end
71
89
 
72
- def create_mp_payment payment_source
73
- MpApi.configuration.access_token = preferences[:access_token]
74
- identification_type = (payment_source.tax_id.length > 14) ? "CNPJ" : "CPF"
75
- MpApi::Payment.new(
76
- payer_email: payment_source.email,
77
- payer_identification_type: identification_type,
78
- payer_identification_number: payment_source.tax_id,
79
- payment_method: "pix",
80
- amount: payment_source.amount.to_f,
81
- statement_descriptor: preferences[:statement_descriptor],
82
- description: payment_source.description
83
- ).create
84
- end
85
-
86
90
  def find_existing_payment(order)
87
- pix_payments = order.payments.checkout.where(source_type: "SolidusMpDois::PixSource")
91
+ pix_payments = order.payments.valid.where(source_type: "SolidusMpDois::PixSource")
88
92
  raise "More than one valid payment for #{order.number}" if pix_payments.count > 1
89
93
  pix_payments.first
90
94
  end
@@ -95,49 +99,102 @@ module SolidusMpDois
95
99
  end
96
100
 
97
101
  def process_payment_response(payment, mp_payment)
98
- payment.update(response_code: mp_payment.id)
99
102
  payment.source.update(
100
103
  external_id: mp_payment.id,
101
- qr_code: mp_payment.qr_code,
102
- qr_code_base64: mp_payment.qr_code_base_64,
103
- ticket_url: mp_payment.ticket_url
104
+ qr_code: mp_payment.point_of_interaction.transaction_data.qr_code,
105
+ qr_code_base64: mp_payment.point_of_interaction.transaction_data.qr_code_base64,
106
+ ticket_url: mp_payment.point_of_interaction.transaction_data.ticket_url
104
107
  )
105
- if mp_payment.error || mp_payment.internal_error
106
- handle_payment_error(payment, mp_payment)
108
+ sync(payment, mp_payment)
109
+ end
110
+
111
+ def sync(payment, mp_payment)
112
+ paid_amount = Float(mp_payment.transaction_details.total_paid_amount)
113
+ payment.source.update!(status: mp_payment.status, internal_details: mp_payment.status_detail)
114
+
115
+ if mp_payment.status == "approved" && paid_amount >= payment.amount
116
+ approve_payment(payment)
117
+ elsif mp_payment.status == "approved"
118
+ raise "Payment paid with value less than the created - #{mp_payment.id}"
119
+ elsif mp_payment.status == "pending" && mp_payment.status_detail == "pending_waiting_transfer"
120
+ pend_payment(payment)
121
+ else
122
+ invalid_payment(payment)
123
+ end
124
+ end
125
+
126
+ def approve_payment payment
127
+ payment.complete!
128
+ payment_source = payment.source
129
+ payment_source.update!(status: payment.state)
130
+ response = successful_response("Pagamento aprovado", status: payment_source.status, internal_details: payment_source.internal_details)
131
+ payment.log_entries.create(parsed_payment_response_details_with_fallback: response)
132
+ end
133
+
134
+ def invalid_payment payment
135
+ if payment.checkout?
136
+ payment.invalidate!
107
137
  else
108
- update_payment_status(payment, mp_payment)
138
+ payment.failure!
109
139
  end
140
+
141
+ payment_source = payment.source
142
+ payment_source.update!(status: payment.state)
143
+ error_message = internal_error(payment_source.internal_details)
144
+ handle_payment_error(payment, error_message:)
145
+ end
146
+
147
+ def pend_payment payment
148
+ payment.pend! unless payment.pending?
149
+ payment.source.update!(status: payment.state)
150
+ error_message = "Aguardando pagamento"
151
+ handle_payment_error(payment, error_message:)
110
152
  end
111
153
 
112
- def update_payment_status(payment, mp_payment)
113
- status = case mp_payment.status
114
- when "pending" then "pending"
154
+ def invalidate_valid_payments order, existing_payment_id
155
+ order.payments.valid.where.not(id: existing_payment_id).each do |payment|
156
+ payment.source.void!
115
157
  end
116
- payment.source.update!(status: status)
117
158
  end
118
159
 
119
- def handle_payment_error(payment, mp_payment)
120
- payment.invalidate
121
- error_message = mp_payment.error || "Erro ao criar o pagamento"
122
- response = failure_response(error_message)
160
+ def handle_payment_error(payment, error_message:)
161
+ payment_source = payment.source
162
+ response = failure_response(error_message, status: payment_source.status, internal_detail: payment_source.internal_details)
123
163
  payment.log_entries.create(parsed_payment_response_details_with_fallback: response)
124
- payment.source.update(internal_error: error_message, status: "error")
164
+ payment_source.update!(internal_error: error_message)
125
165
  end
126
166
 
127
- def successful_response message, transaction_id
167
+ def successful_response message, status:, internal_detail:
168
+ full_message = message + " - " + status + ": " + internal_detail
128
169
  ActiveMerchant::Billing::Response.new(
129
170
  true,
130
- message,
131
- {},
132
- authorization: transaction_id
171
+ full_message
133
172
  )
134
173
  end
135
174
 
136
- def failure_response message
175
+ def failure_response message, status:, internal_detail:
176
+ full_message = message + " - " + status + ": " + internal_detail
137
177
  ActiveMerchant::Billing::Response.new(
138
178
  false,
139
- message
179
+ full_message
140
180
  )
141
181
  end
182
+
183
+ def internal_error internal_detail
184
+ case internal_detail
185
+ when "by_collector"
186
+ "Pagamento cancelado"
187
+ when "refunded"
188
+ "Pagamento reembolsado"
189
+ when "reimbursed"
190
+ "Pagamento reembolsado"
191
+ when "cc_amount_rate_limit_exceeded"
192
+ "O pagamento foi rejeitado porque superou o limite do meio de pagamento"
193
+ when "cc_rejected_insufficient_amount"
194
+ "Saldo insuficiente"
195
+ else
196
+ "Pagamento recusado"
197
+ end
198
+ end
142
199
  end
143
200
  end
@@ -9,12 +9,11 @@ module SolidusMpDois
9
9
  end
10
10
 
11
11
  def paid?
12
- mp_payment = retrieve_from_api
13
- mp_payment.pix_paid?
12
+ payment_method.pix_paid?(payments.sole)
14
13
  end
15
14
 
16
- def invalidate
17
- payment_method.invalidate_payment(self)
15
+ def void!
16
+ payment_method.invalidate_payment(payments.sole)
18
17
  end
19
18
  end
20
19
  end
@@ -22,8 +22,8 @@
22
22
  </div>
23
23
 
24
24
  <div class="field">
25
- <%= label_tag :amount, "Valor" %>
26
- <%= text_field_tag :amount, number_to_currency(payment.source.amount), class:"fullwidth", disabled: true %>
25
+ <%= label_tag :amount, "Valor pago" %>
26
+ <%= text_field_tag :amount, number_to_currency(payment.source.total_paid_amount), class:"fullwidth", disabled: true %>
27
27
  </div>
28
28
 
29
29
  <div class="field">
@@ -0,0 +1,5 @@
1
+ class AddInternalDetailsInSolidusMpDoisPixSources < ActiveRecord::Migration[7.1]
2
+ def change
3
+ add_column :solidus_mp_dois_pix_sources, :internal_details, :string
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddTotalPaidAmountOnSolidusMpDoisCreditCardSources < ActiveRecord::Migration[7.1]
2
+ def change
3
+ add_column :solidus_mp_dois_credit_card_sources, :total_paid_amount, :decimal
4
+ end
5
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solidus_mp_dois
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.2
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Todas Essas Coisas
@@ -44,11 +44,11 @@ executables: []
44
44
  extensions: []
45
45
  extra_rdoc_files: []
46
46
  files:
47
+ - README.md
47
48
  - app/models/solidus_mp_dois/card.rb
48
49
  - app/models/solidus_mp_dois/credit_card_source.rb
49
50
  - app/models/solidus_mp_dois/customer.rb
50
51
  - app/models/solidus_mp_dois/mp_card.rb
51
- - app/models/solidus_mp_dois/mp_gateway.rb
52
52
  - app/models/solidus_mp_dois/mp_pix.rb
53
53
  - app/models/solidus_mp_dois/pix_source.rb
54
54
  - app/views/spree/admin/payments/source_forms/_mercado_pago_card.html.erb
@@ -64,6 +64,8 @@ files:
64
64
  - db/migrate/20241209174725_create_solidus_mp_dois_pix_sources.rb
65
65
  - db/migrate/20250214170355_add_internal_details_to_solidus_mp_dois_credit_card_sources.rb
66
66
  - db/migrate/20250722183651_remove_solidus_mp_dois_customer_foreign_key.rb
67
+ - db/migrate/20250801180726_add_internal_details_in_solidus_mp_dois_pix_sources.rb
68
+ - db/migrate/20250804120023_add_total_paid_amount_on_solidus_mp_dois_credit_card_sources.rb
67
69
  - lib/generators/solidus_mp_dois/install/install_generator.rb
68
70
  - lib/generators/solidus_mp_dois/install/templates/app/javascript/controllers/card_payment_brick_controller.js
69
71
  - lib/generators/solidus_mp_dois/install/templates/app/javascript/controllers/payment_brick_controller.js
@@ -1,44 +0,0 @@
1
- module SolidusMpDois
2
- class MpGateway
3
- def initialize(options)
4
- MpApi.configuration.access_token = options[:access_token]
5
- end
6
-
7
- def purchase money, source, options = {}
8
- mp_payment = source.retrieve_from_api
9
- 10.times do
10
- break if ["approved", "rejected"].include? mp_payment.status
11
- mp_payment = source.retrieve_from_api
12
- end
13
- if mp_payment.status == "approved"
14
- source.update(status: "approved")
15
- successful_response("Pagamento realizado", mp_payment.id)
16
- else
17
- failure_response(mp_payment.status_detail || mp_payment.status)
18
- end
19
- end
20
-
21
- def void(transaction_id, options = {})
22
- # Respondendo sempre com successful_response para funcionar o botão de "Cancelar" do pedido. Reembolso deve ser feito por fora.
23
- successful_response("Pagamento cancelado. Se necessário, realize o reembolso.", transaction_id)
24
- end
25
-
26
- private
27
-
28
- def successful_response message, transaction_id
29
- ActiveMerchant::Billing::Response.new(
30
- true,
31
- message,
32
- {},
33
- authorization: transaction_id
34
- )
35
- end
36
-
37
- def failure_response message
38
- ActiveMerchant::Billing::Response.new(
39
- false,
40
- message
41
- )
42
- end
43
- end
44
- end