@cloudcommerce/app-mercadopago 0.0.113
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.
- package/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +1 -0
- package/LICENSE.md +230 -0
- package/README.md +1 -0
- package/assets/onload-expression.js +138 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -0
- package/lib/mercadopago.d.ts +86 -0
- package/lib/mercadopago.js +15 -0
- package/lib/mercadopago.js.map +1 -0
- package/lib/mp-create-transaction.d.ts +85 -0
- package/lib/mp-create-transaction.js +270 -0
- package/lib/mp-create-transaction.js.map +1 -0
- package/lib/mp-list-payments.d.ts +7 -0
- package/lib/mp-list-payments.js +175 -0
- package/lib/mp-list-payments.js.map +1 -0
- package/lib/mp-webhook.d.ts +5 -0
- package/lib/mp-webhook.js +119 -0
- package/lib/mp-webhook.js.map +1 -0
- package/package.json +36 -0
- package/scripts/build.sh +5 -0
- package/src/index.ts +2 -0
- package/src/mercadopago.ts +16 -0
- package/src/mp-create-transaction.ts +290 -0
- package/src/mp-list-payments.ts +198 -0
- package/src/mp-webhook.ts +136 -0
- package/tsconfig.json +6 -0
- package/webhook.js +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import '@cloudcommerce/firebase/lib/init';
|
|
2
|
+
/* eslint-disable import/prefer-default-export */
|
|
3
|
+
import type { AppModuleBody } from '@cloudcommerce/types';
|
|
4
|
+
// eslint-disable-next-line import/no-unresolved
|
|
5
|
+
import { getFirestore } from 'firebase-admin/firestore';
|
|
6
|
+
// eslint-disable-next-line import/no-unresolved
|
|
7
|
+
import handleListPayments from './mp-list-payments';
|
|
8
|
+
import handleCreateTransaction from './mp-create-transaction';
|
|
9
|
+
|
|
10
|
+
export const listPayments = async (modBody: AppModuleBody) => {
|
|
11
|
+
return handleListPayments(modBody);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const createTransaction = async (modBody: AppModuleBody) => {
|
|
15
|
+
return handleCreateTransaction(modBody, getFirestore());
|
|
16
|
+
};
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import type { AppModuleBody } from '@cloudcommerce/types';
|
|
2
|
+
import type{ CreateTransactionParams } from '@cloudcommerce/types/modules/create_transaction:params';
|
|
3
|
+
import type{ CreateTransactionResponse } from '@cloudcommerce/types/modules/create_transaction:response';
|
|
4
|
+
import type { Firestore } from 'firebase-admin/firestore';
|
|
5
|
+
import { logger } from 'firebase-functions';
|
|
6
|
+
import config from '@cloudcommerce/firebase/lib/config';
|
|
7
|
+
import axios from 'axios';
|
|
8
|
+
|
|
9
|
+
const parsePaymentStatus = (status: string) => {
|
|
10
|
+
switch (status) {
|
|
11
|
+
case 'rejected':
|
|
12
|
+
return 'voided';
|
|
13
|
+
case 'in_process':
|
|
14
|
+
return 'under_analysis';
|
|
15
|
+
case 'approved':
|
|
16
|
+
return 'paid';
|
|
17
|
+
default:
|
|
18
|
+
return 'pending';
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default async (appData: AppModuleBody, firestore:Firestore) => {
|
|
23
|
+
const locationId = config.get().httpsFunctionOptions.region;
|
|
24
|
+
const baseUri = `https://${locationId}-${process.env.GCLOUD_PROJECT}.cloudfunctions.net`;
|
|
25
|
+
// body was already pre-validated on @/bin/web.js
|
|
26
|
+
// treat module request body
|
|
27
|
+
const { application, storeId } = appData;
|
|
28
|
+
const params = appData.params as CreateTransactionParams;
|
|
29
|
+
// app configured options
|
|
30
|
+
const configApp = { ...application.data, ...application.hidden_data };
|
|
31
|
+
const notificationUrl = `${baseUri}/mercadopago-webhook`;
|
|
32
|
+
|
|
33
|
+
let token: string| undefined;
|
|
34
|
+
let paymentMethodId: string;
|
|
35
|
+
const isPix = params.payment_method.code === 'account_deposit';
|
|
36
|
+
if (params.credit_card && params.credit_card.hash) {
|
|
37
|
+
const hashParts = params.credit_card.hash.split(' // ');
|
|
38
|
+
[token] = hashParts;
|
|
39
|
+
try {
|
|
40
|
+
paymentMethodId = JSON.parse(hashParts[1]).payment_method_id;
|
|
41
|
+
} catch (e) {
|
|
42
|
+
paymentMethodId = params.credit_card.company || 'visa';
|
|
43
|
+
}
|
|
44
|
+
} else if (params.payment_method.code === 'banking_billet') {
|
|
45
|
+
paymentMethodId = 'bolbradesco';
|
|
46
|
+
} else if (params.payment_method.code === 'account_deposit') {
|
|
47
|
+
paymentMethodId = 'pix';
|
|
48
|
+
} else {
|
|
49
|
+
return {
|
|
50
|
+
status: 400,
|
|
51
|
+
error: 'NO_CARD_ERR',
|
|
52
|
+
message: 'Credit card hash is required',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { buyer } = params;
|
|
57
|
+
const orderId = params.order_id;
|
|
58
|
+
logger.log('> MP Transaction #', storeId, orderId);
|
|
59
|
+
|
|
60
|
+
// https://www.mercadopago.com.br/developers/pt/reference/payments/_payments/post/
|
|
61
|
+
const payerPhone = {
|
|
62
|
+
area_code: buyer.phone.number.substring(0, 2),
|
|
63
|
+
number: buyer.phone.number.substring(2, 11),
|
|
64
|
+
};
|
|
65
|
+
const additionalInfo: {[key:string]: any} = {
|
|
66
|
+
items: [],
|
|
67
|
+
payer: {
|
|
68
|
+
first_name: buyer.fullname.replace(/\s.*/, ''),
|
|
69
|
+
last_name: buyer.fullname.replace(/[^\s]+\s/, ''),
|
|
70
|
+
phone: payerPhone,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (params.to && params.to.street) {
|
|
75
|
+
additionalInfo.shipments = {
|
|
76
|
+
receiver_address: {
|
|
77
|
+
zip_code: params.to.zip,
|
|
78
|
+
street_name: params.to.street,
|
|
79
|
+
street_number: params.to.number || 0,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (params.billing_address && params.billing_address.street) {
|
|
84
|
+
additionalInfo.payer.address = {
|
|
85
|
+
zip_code: params.billing_address.zip,
|
|
86
|
+
street_name: params.billing_address.street,
|
|
87
|
+
street_number: params.billing_address.number || 0,
|
|
88
|
+
};
|
|
89
|
+
} else if (additionalInfo.shipments) {
|
|
90
|
+
additionalInfo.payer.address = additionalInfo.shipments.receiver_address;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (Array.isArray(params.items)) {
|
|
94
|
+
params.items.forEach((item) => {
|
|
95
|
+
additionalInfo.items.push({
|
|
96
|
+
id: item.sku || item.variation_id || item.product_id,
|
|
97
|
+
title: item.name || item.sku,
|
|
98
|
+
description: item.name || item.sku,
|
|
99
|
+
quantity: item.quantity,
|
|
100
|
+
unit_price: item.final_price || item.price,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const payerOrBuyer = {
|
|
106
|
+
...buyer,
|
|
107
|
+
...params.payer,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
Object.keys(payerOrBuyer).forEach(
|
|
111
|
+
(field) => {
|
|
112
|
+
if (!payerOrBuyer[field] && buyer[field]) {
|
|
113
|
+
payerOrBuyer[field] = buyer[field];
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const payment = {
|
|
119
|
+
payer: {
|
|
120
|
+
type: 'customer',
|
|
121
|
+
email: buyer.email,
|
|
122
|
+
first_name: payerOrBuyer.fullname.replace(/\s.*/, ''),
|
|
123
|
+
last_name: payerOrBuyer.fullname.replace(/[^\s]+\s/, ''),
|
|
124
|
+
identification: {
|
|
125
|
+
type: payerOrBuyer.registry_type === 'j' ? 'CNPJ' : 'CPF',
|
|
126
|
+
number: String(payerOrBuyer.doc_number),
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
external_reference: String(params.order_number),
|
|
130
|
+
transaction_amount: params.amount.total,
|
|
131
|
+
description: `Pedido #${params.order_number} de ${buyer.fullname}`.substring(0, 60),
|
|
132
|
+
payment_method_id: paymentMethodId,
|
|
133
|
+
token,
|
|
134
|
+
statement_descriptor: configApp.statement_descriptor || `${params.domain}_MercadoPago`,
|
|
135
|
+
installments: params.installments_number || 1,
|
|
136
|
+
notification_url: notificationUrl,
|
|
137
|
+
additional_info: additionalInfo,
|
|
138
|
+
metadata: {
|
|
139
|
+
ecom_store_id: storeId,
|
|
140
|
+
ecom_order_id: orderId,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
logger.log('>data: ', JSON.stringify(payment));
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
// https://www.mercadopago.com.br/developers/pt/reference/payments/_payments/post
|
|
147
|
+
const { data } = await axios({
|
|
148
|
+
url: 'https://api.mercadopago.com/v1/payments',
|
|
149
|
+
method: 'post',
|
|
150
|
+
headers: {
|
|
151
|
+
Authorization: `Bearer ${configApp.mp_access_token}`,
|
|
152
|
+
'Content-Type': 'application/json',
|
|
153
|
+
},
|
|
154
|
+
data: payment,
|
|
155
|
+
});
|
|
156
|
+
if (data) {
|
|
157
|
+
logger.log('> MP Checkout #', storeId, orderId);
|
|
158
|
+
|
|
159
|
+
const statusPayment = parsePaymentStatus(data.status);
|
|
160
|
+
let isSaveRetry = false;
|
|
161
|
+
const saveToDb = () => {
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
firestore.collection('mp_payments')
|
|
164
|
+
.doc(String(data.id))
|
|
165
|
+
.set({
|
|
166
|
+
transaction_code: data.id,
|
|
167
|
+
store_id: storeId,
|
|
168
|
+
order_id: orderId,
|
|
169
|
+
status: statusPayment,
|
|
170
|
+
paymentMethod: paymentMethodId,
|
|
171
|
+
notificationUrl,
|
|
172
|
+
}, {
|
|
173
|
+
merge: true,
|
|
174
|
+
})
|
|
175
|
+
.then(() => {
|
|
176
|
+
logger.log('> Payment #', String(data.id));
|
|
177
|
+
resolve(true);
|
|
178
|
+
})
|
|
179
|
+
.catch((err) => {
|
|
180
|
+
if (err.code === 13 && !isSaveRetry) {
|
|
181
|
+
isSaveRetry = true;
|
|
182
|
+
setTimeout(saveToDb, 500);
|
|
183
|
+
} else {
|
|
184
|
+
logger.error('PAYMENT_SAVE_ERR', err);
|
|
185
|
+
reject(err);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
await saveToDb();
|
|
191
|
+
|
|
192
|
+
const transaction: CreateTransactionResponse['transaction'] = {
|
|
193
|
+
amount: data.transaction_details.total_paid_amount,
|
|
194
|
+
currency_id: data.currency_id,
|
|
195
|
+
intermediator: {
|
|
196
|
+
payment_method: {
|
|
197
|
+
code: paymentMethodId || params.payment_method.code,
|
|
198
|
+
},
|
|
199
|
+
transaction_id: String(data.id),
|
|
200
|
+
transaction_code: String(data.id),
|
|
201
|
+
transaction_reference: data.external_reference,
|
|
202
|
+
},
|
|
203
|
+
status: {
|
|
204
|
+
current: statusPayment,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (params.payment_method.code === 'credit_card') {
|
|
209
|
+
if (data.card) {
|
|
210
|
+
transaction.credit_card = {
|
|
211
|
+
holder_name: data.card.cardholder.name,
|
|
212
|
+
last_digits: data.card.last_four_digits,
|
|
213
|
+
token,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (data.installments) {
|
|
217
|
+
transaction.installments = {
|
|
218
|
+
number: data.installments,
|
|
219
|
+
tax: (data.transaction_details.total_paid_amount > data.transaction_amount),
|
|
220
|
+
total: data.transaction_details.total_paid_amount,
|
|
221
|
+
value: data.transaction_details.installment_amount,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
} else if (!isPix && data.transaction_details
|
|
225
|
+
&& data.transaction_details.external_resource_url) {
|
|
226
|
+
transaction.payment_link = data.transaction_details.external_resource_url;
|
|
227
|
+
transaction.banking_billet = {
|
|
228
|
+
link: transaction.payment_link,
|
|
229
|
+
};
|
|
230
|
+
if (data.date_of_expiration) {
|
|
231
|
+
const dateValidThru = new Date(data.date_of_expiration);
|
|
232
|
+
if (dateValidThru.getTime() > 0) {
|
|
233
|
+
transaction.banking_billet.valid_thru = dateValidThru.toISOString();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} else if (isPix && data.point_of_interaction && data.point_of_interaction.transaction_data) {
|
|
237
|
+
// https://www.mercadopago.com.br/developers/pt/docs/checkout-api/integration-configuration/integrate-with-pix#bookmark_visualiza%C3%A7%C3%A3o_de_pagamento
|
|
238
|
+
const qrCode = data.point_of_interaction.transaction_data.qr_code;
|
|
239
|
+
const qrCodeBase64 = data.point_of_interaction.transaction_data.qr_code_base64;
|
|
240
|
+
transaction.notes = '<div style="display:block;margin:0 auto"> '
|
|
241
|
+
+ `<img width="280" height="280" style="margin:5px auto" src='data:image/jpeg;base64,${qrCodeBase64}'/> `
|
|
242
|
+
+ `<lable> ${qrCode} </label></div>`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
status: 200,
|
|
247
|
+
redirect_to_payment: false,
|
|
248
|
+
transaction,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
status: 409,
|
|
253
|
+
message: `CREATE_TRANSACTION_ERR #${storeId} - ${orderId} => MP not response`,
|
|
254
|
+
};
|
|
255
|
+
} catch (error: any) {
|
|
256
|
+
let { message } = error;
|
|
257
|
+
//
|
|
258
|
+
const err = {
|
|
259
|
+
message: `CREATE_TRANSACTION_ERR #${storeId} - ${orderId} => ${message}`,
|
|
260
|
+
payment: '',
|
|
261
|
+
status: 0,
|
|
262
|
+
response: '',
|
|
263
|
+
};
|
|
264
|
+
if (error.response) {
|
|
265
|
+
const { status, data } = error.response;
|
|
266
|
+
if (status !== 401 && status !== 403) {
|
|
267
|
+
err.payment = JSON.stringify(payment);
|
|
268
|
+
err.status = status;
|
|
269
|
+
if (typeof data === 'object' && data) {
|
|
270
|
+
err.response = JSON.stringify(data);
|
|
271
|
+
} else {
|
|
272
|
+
err.response = data;
|
|
273
|
+
}
|
|
274
|
+
if (typeof data.message === 'string' && data.message) {
|
|
275
|
+
message = data.message;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
logger.error(err);
|
|
280
|
+
return {
|
|
281
|
+
status: 409,
|
|
282
|
+
error: 'CREATE_TRANSACTION_ERR',
|
|
283
|
+
message,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
export {
|
|
289
|
+
parsePaymentStatus,
|
|
290
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { AppModuleBody } from '@cloudcommerce/types';
|
|
2
|
+
import type{ ListPaymentsParams } from '@cloudcommerce/types/modules/list_payments:params';
|
|
3
|
+
import type{ ListPaymentsResponse } from '@cloudcommerce/types/modules/list_payments:response';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import url from 'url';
|
|
7
|
+
|
|
8
|
+
type Gateway = ListPaymentsResponse['payment_gateways'][number]
|
|
9
|
+
type CodePaymentMethod = Gateway['payment_method']['code']
|
|
10
|
+
|
|
11
|
+
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
|
12
|
+
|
|
13
|
+
export default (data: AppModuleBody) => {
|
|
14
|
+
const { application } = data;
|
|
15
|
+
const params = data.params as ListPaymentsParams;
|
|
16
|
+
// https://apx-mods.e-com.plus/api/v1/list_payments/schema.json?store_id=100
|
|
17
|
+
const amount = params.amount || { total: undefined };
|
|
18
|
+
// const initialTotalAmount = amount.total;
|
|
19
|
+
|
|
20
|
+
const config = {
|
|
21
|
+
...application.data,
|
|
22
|
+
...application.hidden_data,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (!config.mp_public_key || !config.mp_access_token) {
|
|
26
|
+
// must have configured PayPal app ID and secret
|
|
27
|
+
return {
|
|
28
|
+
error: 'LIST_PAYMENTS_ERR',
|
|
29
|
+
message: 'MP Public Key or Access Token is unset on app hidden data (merchant must configure the app)',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// start mounting response body
|
|
34
|
+
// https://apx-mods.e-com.plus/api/v1/list_payments/response_schema.json?store_id=100
|
|
35
|
+
const response: ListPaymentsResponse = {
|
|
36
|
+
payment_gateways: [],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// calculate discount value
|
|
40
|
+
const { discount } = config;
|
|
41
|
+
if (discount && discount.value) {
|
|
42
|
+
if (discount.apply_at !== 'freight') {
|
|
43
|
+
// default discount option
|
|
44
|
+
const { value } = discount;
|
|
45
|
+
response.discount_option = {
|
|
46
|
+
label: config.discount_option_label,
|
|
47
|
+
value,
|
|
48
|
+
};
|
|
49
|
+
// specify the discount type and min amount is optional
|
|
50
|
+
const discountTypeMinAmount = ['type', 'min_amount'];
|
|
51
|
+
discountTypeMinAmount.forEach(
|
|
52
|
+
(prop) => {
|
|
53
|
+
if (response.discount_option && discount[prop]) {
|
|
54
|
+
response.discount_option[prop] = discount[prop];
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (amount.total) {
|
|
61
|
+
// check amount value to apply discount
|
|
62
|
+
if (amount.total < discount.min_amount) {
|
|
63
|
+
discount.value = 0;
|
|
64
|
+
} else {
|
|
65
|
+
delete discount.min_amount;
|
|
66
|
+
|
|
67
|
+
// fix local amount object
|
|
68
|
+
const maxDiscount = amount[discount.apply_at || 'subtotal'];
|
|
69
|
+
let discountValue:number;
|
|
70
|
+
if (discount.type === 'percentage') {
|
|
71
|
+
discountValue = (maxDiscount * discount.value) / 100;
|
|
72
|
+
} else {
|
|
73
|
+
discountValue = discount.value;
|
|
74
|
+
if (discountValue > maxDiscount) {
|
|
75
|
+
discountValue = maxDiscount;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (discountValue) {
|
|
79
|
+
amount.discount = (amount.discount || 0) + discountValue;
|
|
80
|
+
amount.total -= discountValue;
|
|
81
|
+
if (amount.total < 0) {
|
|
82
|
+
amount.total = 0;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// setup payment gateway objects
|
|
90
|
+
const intermediator = {
|
|
91
|
+
code: 'mercadopago',
|
|
92
|
+
link: 'https://www.mercadopago.com.br',
|
|
93
|
+
name: 'Mercado Pago',
|
|
94
|
+
};
|
|
95
|
+
const listPaymentMethods = ['banking_billet', 'credit_card'];
|
|
96
|
+
if (config.account_deposit && config.account_deposit.key_pix) {
|
|
97
|
+
// pix Configured
|
|
98
|
+
listPaymentMethods.push('account_deposit');
|
|
99
|
+
}
|
|
100
|
+
listPaymentMethods.forEach((paymentMethod) => {
|
|
101
|
+
const isCreditCard = paymentMethod === 'credit_card';
|
|
102
|
+
const methodConfig = isCreditCard ? config : config[paymentMethod];
|
|
103
|
+
const minAmount = methodConfig.min_amount || 0;
|
|
104
|
+
const methodEnable = methodConfig.enable || (isCreditCard && !methodConfig.disable);
|
|
105
|
+
if (methodConfig && methodEnable && (amount.total === undefined || amount.total >= minAmount)) {
|
|
106
|
+
let label: string;
|
|
107
|
+
if (methodConfig.label) {
|
|
108
|
+
label = methodConfig.label;
|
|
109
|
+
} else if (isCreditCard) {
|
|
110
|
+
label = 'Cartão de crédito';
|
|
111
|
+
} else {
|
|
112
|
+
label = paymentMethod === 'account_deposit' ? 'Pix' : 'Boleto bancário';
|
|
113
|
+
}
|
|
114
|
+
const gateway:Gateway = {
|
|
115
|
+
label,
|
|
116
|
+
icon: methodConfig.icon,
|
|
117
|
+
text: methodConfig.text,
|
|
118
|
+
payment_method: {
|
|
119
|
+
code: paymentMethod as CodePaymentMethod,
|
|
120
|
+
name: `${label} - ${intermediator.name}`,
|
|
121
|
+
},
|
|
122
|
+
intermediator,
|
|
123
|
+
type: 'payment',
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (isCreditCard) {
|
|
127
|
+
if (!gateway.icon) {
|
|
128
|
+
// gateway.icon = `${baseUri}/checkout-stamp.png`; // TODO: baseUri
|
|
129
|
+
gateway.icon = 'https://us-central1-mercadopago-ecom.cloudfunctions.net/app/checkout-stamp.png';
|
|
130
|
+
}
|
|
131
|
+
gateway.js_client = {
|
|
132
|
+
script_uri: 'https://secure.mlstatic.com/sdk/javascript/v1/mercadopago.js',
|
|
133
|
+
onload_expression: `window.Mercadopago.setPublishableKey("${config.mp_public_key}");${
|
|
134
|
+
fs.readFileSync(path.join(__dirname, '../../assets/onload-expression.min.js'), 'utf8')}`,
|
|
135
|
+
cc_brand: {
|
|
136
|
+
function: '_mpBrand',
|
|
137
|
+
is_promise: true,
|
|
138
|
+
},
|
|
139
|
+
cc_hash: {
|
|
140
|
+
function: '_mpHash',
|
|
141
|
+
is_promise: true,
|
|
142
|
+
},
|
|
143
|
+
cc_installments: {
|
|
144
|
+
function: '_mpInstallments',
|
|
145
|
+
is_promise: true,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// default configured default installments option
|
|
150
|
+
const installmentsOption = config.installments_option;
|
|
151
|
+
if (installmentsOption && installmentsOption.max_number) {
|
|
152
|
+
response.installments_option = installmentsOption;
|
|
153
|
+
const minInstallment = installmentsOption.min_installment;
|
|
154
|
+
|
|
155
|
+
// optional configured installments list
|
|
156
|
+
if (amount.total && Array.isArray(config.installments) && config.installments.length) {
|
|
157
|
+
gateway.installment_options = [];
|
|
158
|
+
config.installments.forEach(({ number, interest }) => {
|
|
159
|
+
if (number >= 2) {
|
|
160
|
+
const value = amount.total / number;
|
|
161
|
+
if (gateway.installment_options && value >= minInstallment) {
|
|
162
|
+
gateway.installment_options.push({
|
|
163
|
+
number,
|
|
164
|
+
value: interest > 0 ? (value + value * interest) / 100 : value,
|
|
165
|
+
tax: Boolean(interest),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// check available discount by payment method
|
|
175
|
+
if (discount && discount.value && discount[paymentMethod] !== false) {
|
|
176
|
+
gateway.discount = {
|
|
177
|
+
apply_at: 'total',
|
|
178
|
+
type: 'fixed',
|
|
179
|
+
value: 0,
|
|
180
|
+
};
|
|
181
|
+
['apply_at', 'type', 'value'].forEach(
|
|
182
|
+
(field) => {
|
|
183
|
+
if (gateway.discount && discount[field]) {
|
|
184
|
+
gateway.discount[field] = discount[field];
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
if (response.discount_option && !response.discount_option.label) {
|
|
189
|
+
response.discount_option.label = label;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
response.payment_gateways.push(gateway);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return response;
|
|
198
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/* eslint-disable import/prefer-default-export */
|
|
2
|
+
import '@cloudcommerce/firebase/lib/init';
|
|
3
|
+
import { logger } from 'firebase-functions';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import api from '@cloudcommerce/api';
|
|
6
|
+
// eslint-disable-next-line import/no-unresolved
|
|
7
|
+
import { getFirestore } from 'firebase-admin/firestore';
|
|
8
|
+
// eslint-disable-next-line import/no-unresolved
|
|
9
|
+
import * as functions from 'firebase-functions/v1';
|
|
10
|
+
import config from '@cloudcommerce/firebase/lib/config';
|
|
11
|
+
import { parsePaymentStatus } from './mp-create-transaction';
|
|
12
|
+
|
|
13
|
+
const ECHO_SKIP = 'SKIP';
|
|
14
|
+
let ECHO_SUCCESS = 'OK';
|
|
15
|
+
|
|
16
|
+
export const mercadopago = {
|
|
17
|
+
webhook: functions
|
|
18
|
+
.region(config.get().httpsFunctionOptions.region)
|
|
19
|
+
.https.onRequest(async (req, res) => {
|
|
20
|
+
const { method } = req;
|
|
21
|
+
if (method === 'POST') {
|
|
22
|
+
const { body } = req;
|
|
23
|
+
logger.log('>> Webhook MP #', JSON.stringify(body), ' <<');
|
|
24
|
+
try {
|
|
25
|
+
const app = (await api.get(
|
|
26
|
+
'applications?app_id=111223&fields=hidden_data',
|
|
27
|
+
)).data.result;
|
|
28
|
+
const accessTokenMp = app[0].hidden_data?.mp_access_token;
|
|
29
|
+
if (accessTokenMp) {
|
|
30
|
+
const notification = req.body;
|
|
31
|
+
if (notification.type !== 'payment' || !notification.data || !notification.data.id) {
|
|
32
|
+
res.status(404).send(ECHO_SKIP);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// setTimeout(() => {
|
|
36
|
+
logger.log('> MP Notification for Payment #', notification.data.id);
|
|
37
|
+
|
|
38
|
+
const docRef = getFirestore().collection('mp_payments')
|
|
39
|
+
.doc(String(notification.data.id));
|
|
40
|
+
|
|
41
|
+
docRef.get()
|
|
42
|
+
.then(async (doc) => {
|
|
43
|
+
if (doc.exists) {
|
|
44
|
+
const data = doc.data();
|
|
45
|
+
const orderId = data?.order_id;
|
|
46
|
+
const order = (await api.get(
|
|
47
|
+
`orders/${orderId}`,
|
|
48
|
+
)).data;
|
|
49
|
+
logger.log('>order ', JSON.stringify(order), '<');
|
|
50
|
+
if (order && order.transactions) {
|
|
51
|
+
const payment = (await axios.get(
|
|
52
|
+
`https://api.mercadopago.com/v1/payments/${notification.data.id}`,
|
|
53
|
+
{
|
|
54
|
+
headers: {
|
|
55
|
+
Authorization: `Bearer ${accessTokenMp}`,
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
)).data;
|
|
60
|
+
logger.log('>payment ', JSON.stringify(payment), ' <');
|
|
61
|
+
const methodPayment = payment.payment_method_id;
|
|
62
|
+
|
|
63
|
+
const transaction = order.transactions.find(({ intermediator }) => {
|
|
64
|
+
return intermediator
|
|
65
|
+
&& intermediator.transaction_code === notification.data.id;
|
|
66
|
+
});
|
|
67
|
+
const status = parsePaymentStatus(payment.status);
|
|
68
|
+
if (transaction) {
|
|
69
|
+
const bodyPaymentHistory = {
|
|
70
|
+
transaction_id: transaction._id,
|
|
71
|
+
date_time: new Date().toISOString(),
|
|
72
|
+
status,
|
|
73
|
+
notification_code: String(notification.id),
|
|
74
|
+
flags: [
|
|
75
|
+
'mercadopago',
|
|
76
|
+
],
|
|
77
|
+
} as any; // TODO: incompatible type=> amount and status
|
|
78
|
+
|
|
79
|
+
if (status !== order.financial_status?.current) {
|
|
80
|
+
// avoid unnecessary API request
|
|
81
|
+
await api.post(`orders/${orderId}/payments_history`, bodyPaymentHistory);
|
|
82
|
+
const updatedAt = new Date().toISOString();
|
|
83
|
+
docRef.set({ status, updatedAt }, { merge: true }).catch(logger.error);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if ((status === 'paid' && methodPayment === 'pix' && transaction)) {
|
|
87
|
+
let { notes } = transaction;
|
|
88
|
+
notes = notes?.replace('display:block', 'display:none'); // disable QR Code
|
|
89
|
+
notes = `${notes} # PIX Aprovado`;
|
|
90
|
+
|
|
91
|
+
// orders/${order._id}/transactions/${transactionId}.json { notes }
|
|
92
|
+
// Update to disable QR Code
|
|
93
|
+
try {
|
|
94
|
+
await api.patch(
|
|
95
|
+
`orders/${order._id}/transactions/${transaction._id}`,
|
|
96
|
+
{ notes },
|
|
97
|
+
);
|
|
98
|
+
ECHO_SUCCESS = 'SUCCESS';
|
|
99
|
+
} catch (e) {
|
|
100
|
+
logger.error(e);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
res.status(200).send(ECHO_SUCCESS);
|
|
105
|
+
} else {
|
|
106
|
+
// transaction not found
|
|
107
|
+
logger.log('> Transaction not found #', notification.data.id);
|
|
108
|
+
res.sendStatus(404);
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
// order or order transaction not found
|
|
112
|
+
logger.log('> Order Not Found #', orderId);
|
|
113
|
+
res.sendStatus(404);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
logger.log('> Payment not found in Firestore #', notification.data.id);
|
|
117
|
+
res.sendStatus(404);
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
.catch((err) => {
|
|
121
|
+
logger.error(err);
|
|
122
|
+
res.sendStatus(503);
|
|
123
|
+
});
|
|
124
|
+
// }, 3000);
|
|
125
|
+
} else {
|
|
126
|
+
res.sendStatus(406);
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
logger.error(e);
|
|
130
|
+
res.sendStatus(500);
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
res.sendStatus(405);
|
|
134
|
+
}
|
|
135
|
+
}),
|
|
136
|
+
};
|
package/tsconfig.json
ADDED
package/webhook.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lib/mp-webhook.js';
|