@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.
@@ -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
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "declaration": true
5
+ }
6
+ }
package/webhook.js ADDED
@@ -0,0 +1 @@
1
+ export * from './lib/mp-webhook.js';