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 +4 -4
- data/README.md +250 -0
- data/app/models/solidus_mp_dois/card.rb +1 -2
- data/app/models/solidus_mp_dois/credit_card_source.rb +2 -2
- data/app/models/solidus_mp_dois/mp_card.rb +154 -90
- data/app/models/solidus_mp_dois/mp_pix.rb +112 -55
- data/app/models/solidus_mp_dois/pix_source.rb +3 -4
- data/app/views/spree/admin/payments/source_views/_mercado_pago_card.html.erb +2 -2
- data/db/migrate/20250801180726_add_internal_details_in_solidus_mp_dois_pix_sources.rb +5 -0
- data/db/migrate/20250804120023_add_total_paid_amount_on_solidus_mp_dois_credit_card_sources.rb +5 -0
- metadata +4 -2
- data/app/models/solidus_mp_dois/mp_gateway.rb +0 -44
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f47d658a64a40e54735865036c9331ff4b27c0740789c9afd9074ebce5553f6
|
4
|
+
data.tar.gz: 595b3dc81627c86ca7c9e5d76214acd727afd78716cd2e376d555e9ba06a2f1d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5d38025a7d3d9097863ddad2cf466a31a522ae5094e4f558a72c8af99593d2ea758d5592d5b57ee47887b340787bbdca30cc7f913328058fa1fe769e1955d3cf
|
7
|
+
data.tar.gz: f5a1439538d3d45b38d39effa81537c310d74f34e279a3a0875676cfb0f194a08bd196dff57525f62c5bafb14818e02e2fdec550254fc7d78eef61d8e0db52a4
|
data/README.md
ADDED
@@ -0,0 +1,250 @@
|
|
1
|
+
# Solidus Mercado Pago 2 🇧🇷
|
2
|
+
|
3
|
+
[](https://github.com/todasessascoisas/solidus_mp_dois)
|
4
|
+
[](https://ruby-lang.org)
|
5
|
+
[](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
|
-
|
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
|
@@ -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
|
-
|
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 =
|
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
|
46
|
-
|
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!(
|
50
|
-
|
55
|
+
def cancel!(payment)
|
56
|
+
external_id = payment.source&.external_id
|
57
|
+
return unless external_id
|
51
58
|
sleep(1)
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
80
|
-
|
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
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
-
|
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,
|
130
|
-
|
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
|
-
)
|
146
|
+
)
|
138
147
|
end
|
139
148
|
|
140
|
-
|
141
|
-
|
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
|
-
|
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.
|
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.
|
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
|
169
|
-
three_ds_creq: mp_payment
|
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
|
-
|
173
|
-
|
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.
|
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
|
181
|
-
|
182
|
-
when "
|
183
|
-
|
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
|
189
|
-
payment
|
190
|
-
|
191
|
-
|
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
|
-
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
46
|
-
return
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
57
|
-
|
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.
|
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.
|
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
|
-
|
106
|
-
|
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
|
-
|
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
|
113
|
-
|
114
|
-
|
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,
|
120
|
-
payment.
|
121
|
-
|
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
|
-
|
164
|
+
payment_source.update!(internal_error: error_message)
|
125
165
|
end
|
126
166
|
|
127
|
-
def successful_response message,
|
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
|
-
|
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
|
-
|
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
|
-
|
13
|
-
mp_payment.pix_paid?
|
12
|
+
payment_method.pix_paid?(payments.sole)
|
14
13
|
end
|
15
14
|
|
16
|
-
def
|
17
|
-
payment_method.invalidate_payment(
|
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.
|
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">
|
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:
|
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
|