From 69d75908341423f5e3aeda013ef9cd022f2520a3 Mon Sep 17 00:00:00 2001 From: Consultoria AS Date: Sun, 15 Mar 2026 23:39:00 +0000 Subject: [PATCH] feat: add MercadoPago payments, subscription service, and webhooks - MercadoPago PreApproval integration for recurring subscriptions - Subscription service with caching, manual payment, payment history - Webhook handler with HMAC-SHA256 signature verification - Admin endpoints for subscription management and payment links - Email notifications on payment success/failure/cancellation Co-Authored-By: Claude Opus 4.6 --- apps/api/package.json | 1 + apps/api/src/app.ts | 4 + apps/api/src/config/env.ts | 3 + .../controllers/subscription.controller.ts | 51 ++++ .../api/src/controllers/webhook.controller.ts | 95 +++++++ apps/api/src/routes/subscription.routes.ts | 16 ++ apps/api/src/routes/webhook.routes.ts | 9 + .../services/payment/mercadopago.service.ts | 103 ++++++++ .../services/payment/subscription.service.ts | 232 ++++++++++++++++++ pnpm-lock.yaml | 50 ++++ 10 files changed, 564 insertions(+) create mode 100644 apps/api/src/controllers/subscription.controller.ts create mode 100644 apps/api/src/controllers/webhook.controller.ts create mode 100644 apps/api/src/routes/subscription.routes.ts create mode 100644 apps/api/src/routes/webhook.routes.ts create mode 100644 apps/api/src/services/payment/mercadopago.service.ts create mode 100644 apps/api/src/services/payment/subscription.service.ts diff --git a/apps/api/package.json b/apps/api/package.json index ba0f0ab..3e42216 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -28,6 +28,7 @@ "fast-xml-parser": "^5.3.3", "helmet": "^8.0.0", "jsonwebtoken": "^9.0.2", + "mercadopago": "^2.12.0", "node-cron": "^4.2.1", "node-forge": "^1.3.3", "nodemailer": "^8.0.2", diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 4633258..8e7d2d5 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -15,6 +15,8 @@ import { usuariosRoutes } from './routes/usuarios.routes.js'; import { tenantsRoutes } from './routes/tenants.routes.js'; import fielRoutes from './routes/fiel.routes.js'; import satRoutes from './routes/sat.routes.js'; +import { webhookRoutes } from './routes/webhook.routes.js'; +import { subscriptionRoutes } from './routes/subscription.routes.js'; const app: Express = express(); @@ -47,6 +49,8 @@ app.use('/api/usuarios', usuariosRoutes); app.use('/api/tenants', tenantsRoutes); app.use('/api/fiel', fielRoutes); app.use('/api/sat', satRoutes); +app.use('/api/webhooks', webhookRoutes); +app.use('/api/subscriptions', subscriptionRoutes); // Error handling app.use(errorMiddleware); diff --git a/apps/api/src/config/env.ts b/apps/api/src/config/env.ts index 8d81332..7cbde06 100644 --- a/apps/api/src/config/env.ts +++ b/apps/api/src/config/env.ts @@ -14,6 +14,9 @@ const envSchema = z.object({ JWT_REFRESH_EXPIRES_IN: z.string().default('7d'), CORS_ORIGIN: z.string().default('http://localhost:3000'), + // Frontend URL (for MercadoPago back_url, emails, etc.) + FRONTEND_URL: z.string().default('https://horux360.consultoria-as.com'), + // FIEL encryption (separate from JWT to allow independent rotation) FIEL_ENCRYPTION_KEY: z.string().min(32).default('dev-fiel-encryption-key-min-32-chars!!'), FIEL_STORAGE_PATH: z.string().default('/var/horux/fiel'), diff --git a/apps/api/src/controllers/subscription.controller.ts b/apps/api/src/controllers/subscription.controller.ts new file mode 100644 index 0000000..7a5d2fc --- /dev/null +++ b/apps/api/src/controllers/subscription.controller.ts @@ -0,0 +1,51 @@ +import type { Request, Response, NextFunction } from 'express'; +import * as subscriptionService from '../services/payment/subscription.service.js'; + +export async function getSubscription(req: Request, res: Response, next: NextFunction) { + try { + const tenantId = String(req.params.tenantId); + const subscription = await subscriptionService.getActiveSubscription(tenantId); + if (!subscription) { + return res.status(404).json({ message: 'No se encontró suscripción' }); + } + res.json(subscription); + } catch (error) { + next(error); + } +} + +export async function generatePaymentLink(req: Request, res: Response, next: NextFunction) { + try { + const tenantId = String(req.params.tenantId); + const result = await subscriptionService.generatePaymentLink(tenantId); + res.json(result); + } catch (error) { + next(error); + } +} + +export async function markAsPaid(req: Request, res: Response, next: NextFunction) { + try { + const tenantId = String(req.params.tenantId); + const { amount } = req.body; + + if (!amount || amount <= 0) { + return res.status(400).json({ message: 'Monto inválido' }); + } + + const payment = await subscriptionService.markAsPaidManually(tenantId, amount); + res.json(payment); + } catch (error) { + next(error); + } +} + +export async function getPayments(req: Request, res: Response, next: NextFunction) { + try { + const tenantId = String(req.params.tenantId); + const payments = await subscriptionService.getPaymentHistory(tenantId); + res.json(payments); + } catch (error) { + next(error); + } +} diff --git a/apps/api/src/controllers/webhook.controller.ts b/apps/api/src/controllers/webhook.controller.ts new file mode 100644 index 0000000..a223285 --- /dev/null +++ b/apps/api/src/controllers/webhook.controller.ts @@ -0,0 +1,95 @@ +import type { Request, Response, NextFunction } from 'express'; +import * as mpService from '../services/payment/mercadopago.service.js'; +import * as subscriptionService from '../services/payment/subscription.service.js'; +import { prisma } from '../config/database.js'; + +export async function handleMercadoPagoWebhook(req: Request, res: Response, next: NextFunction) { + try { + const { type, data } = req.body; + const xSignature = req.headers['x-signature'] as string; + const xRequestId = req.headers['x-request-id'] as string; + + // Verify webhook signature + if (xSignature && xRequestId && data?.id) { + const isValid = mpService.verifyWebhookSignature(xSignature, xRequestId, String(data.id)); + if (!isValid) { + console.warn('[WEBHOOK] Invalid MercadoPago signature'); + return res.status(401).json({ message: 'Invalid signature' }); + } + } + + if (type === 'payment') { + await handlePaymentNotification(String(data.id)); + } else if (type === 'subscription_preapproval') { + await handlePreapprovalNotification(String(data.id)); + } + + // Always respond 200 to acknowledge receipt + res.status(200).json({ received: true }); + } catch (error) { + console.error('[WEBHOOK] Error processing MercadoPago webhook:', error); + // Still respond 200 to prevent retries for processing errors + res.status(200).json({ received: true, error: 'processing_error' }); + } +} + +async function handlePaymentNotification(paymentId: string) { + const payment = await mpService.getPaymentDetails(paymentId); + + if (!payment.externalReference) { + console.warn('[WEBHOOK] Payment without external_reference:', paymentId); + return; + } + + const tenantId = payment.externalReference; + + // Find the subscription for this tenant + const subscription = await prisma.subscription.findFirst({ + where: { tenantId }, + orderBy: { createdAt: 'desc' }, + }); + + if (!subscription) { + console.warn('[WEBHOOK] No subscription found for tenant:', tenantId); + return; + } + + await subscriptionService.recordPayment({ + tenantId, + subscriptionId: subscription.id, + mpPaymentId: paymentId, + amount: payment.transactionAmount || 0, + status: payment.status || 'unknown', + paymentMethod: payment.paymentMethodId || 'unknown', + }); + + // If payment approved, ensure subscription is active + if (payment.status === 'approved') { + await prisma.subscription.update({ + where: { id: subscription.id }, + data: { status: 'authorized' }, + }); + subscriptionService.invalidateSubscriptionCache(tenantId); + } + + // Broadcast cache invalidation to PM2 cluster workers + if (typeof process.send === 'function') { + process.send({ type: 'invalidate-tenant-cache', tenantId }); + } +} + +async function handlePreapprovalNotification(preapprovalId: string) { + const preapproval = await mpService.getPreapproval(preapprovalId); + + if (preapproval.status) { + await subscriptionService.updateSubscriptionStatus(preapprovalId, preapproval.status); + } + + // Broadcast cache invalidation + const subscription = await prisma.subscription.findFirst({ + where: { mpPreapprovalId: preapprovalId }, + }); + if (subscription && typeof process.send === 'function') { + process.send({ type: 'invalidate-tenant-cache', tenantId: subscription.tenantId }); + } +} diff --git a/apps/api/src/routes/subscription.routes.ts b/apps/api/src/routes/subscription.routes.ts new file mode 100644 index 0000000..3ebde96 --- /dev/null +++ b/apps/api/src/routes/subscription.routes.ts @@ -0,0 +1,16 @@ +import { Router, type IRouter } from 'express'; +import { authenticate } from '../middlewares/auth.middleware.js'; +import * as subscriptionController from '../controllers/subscription.controller.js'; + +const router: IRouter = Router(); + +// All endpoints require authentication +router.use(authenticate); + +// Admin subscription management +router.get('/:tenantId', subscriptionController.getSubscription); +router.post('/:tenantId/generate-link', subscriptionController.generatePaymentLink); +router.post('/:tenantId/mark-paid', subscriptionController.markAsPaid); +router.get('/:tenantId/payments', subscriptionController.getPayments); + +export { router as subscriptionRoutes }; diff --git a/apps/api/src/routes/webhook.routes.ts b/apps/api/src/routes/webhook.routes.ts new file mode 100644 index 0000000..7096d02 --- /dev/null +++ b/apps/api/src/routes/webhook.routes.ts @@ -0,0 +1,9 @@ +import { Router, type IRouter } from 'express'; +import { handleMercadoPagoWebhook } from '../controllers/webhook.controller.js'; + +const router: IRouter = Router(); + +// Public endpoint — no auth middleware +router.post('/mercadopago', handleMercadoPagoWebhook); + +export { router as webhookRoutes }; diff --git a/apps/api/src/services/payment/mercadopago.service.ts b/apps/api/src/services/payment/mercadopago.service.ts new file mode 100644 index 0000000..8007e56 --- /dev/null +++ b/apps/api/src/services/payment/mercadopago.service.ts @@ -0,0 +1,103 @@ +import { MercadoPagoConfig, PreApproval, Payment as MPPayment } from 'mercadopago'; +import { env } from '../../config/env.js'; +import { createHmac } from 'crypto'; + +const config = new MercadoPagoConfig({ + accessToken: env.MP_ACCESS_TOKEN || '', +}); + +const preApprovalClient = new PreApproval(config); +const paymentClient = new MPPayment(config); + +/** + * Creates a recurring subscription (preapproval) in MercadoPago + */ +export async function createPreapproval(params: { + tenantId: string; + reason: string; + amount: number; + payerEmail: string; +}) { + const response = await preApprovalClient.create({ + body: { + reason: params.reason, + external_reference: params.tenantId, + payer_email: params.payerEmail, + auto_recurring: { + frequency: 1, + frequency_type: 'months', + transaction_amount: params.amount, + currency_id: 'MXN', + }, + back_url: `${env.FRONTEND_URL}/configuracion/suscripcion`, + }, + }); + + return { + preapprovalId: response.id!, + initPoint: response.init_point!, + status: response.status!, + }; +} + +/** + * Gets subscription (preapproval) status from MercadoPago + */ +export async function getPreapproval(preapprovalId: string) { + const response = await preApprovalClient.get({ id: preapprovalId }); + return { + id: response.id, + status: response.status, + payerEmail: response.payer_email, + nextPaymentDate: response.next_payment_date, + autoRecurring: response.auto_recurring, + }; +} + +/** + * Gets payment details from MercadoPago + */ +export async function getPaymentDetails(paymentId: string) { + const response = await paymentClient.get({ id: paymentId }); + return { + id: response.id, + status: response.status, + statusDetail: response.status_detail, + transactionAmount: response.transaction_amount, + currencyId: response.currency_id, + payerEmail: response.payer?.email, + dateApproved: response.date_approved, + paymentMethodId: response.payment_method_id, + externalReference: response.external_reference, + }; +} + +/** + * Verifies MercadoPago webhook signature (HMAC-SHA256) + */ +export function verifyWebhookSignature( + xSignature: string, + xRequestId: string, + dataId: string +): boolean { + if (!env.MP_WEBHOOK_SECRET) return true; // Skip in dev + + // Parse x-signature header: "ts=...,v1=..." + const parts: Record = {}; + for (const part of xSignature.split(',')) { + const [key, value] = part.split('='); + parts[key.trim()] = value.trim(); + } + + const ts = parts['ts']; + const v1 = parts['v1']; + if (!ts || !v1) return false; + + // Build the manifest string + const manifest = `id:${dataId};request-id:${xRequestId};ts:${ts};`; + const hmac = createHmac('sha256', env.MP_WEBHOOK_SECRET) + .update(manifest) + .digest('hex'); + + return hmac === v1; +} diff --git a/apps/api/src/services/payment/subscription.service.ts b/apps/api/src/services/payment/subscription.service.ts new file mode 100644 index 0000000..1c32070 --- /dev/null +++ b/apps/api/src/services/payment/subscription.service.ts @@ -0,0 +1,232 @@ +import { prisma } from '../../config/database.js'; +import * as mpService from './mercadopago.service.js'; +import { emailService } from '../email/email.service.js'; + +// Simple in-memory cache with TTL +const subscriptionCache = new Map(); + +export function invalidateSubscriptionCache(tenantId: string) { + subscriptionCache.delete(`sub:${tenantId}`); +} + +/** + * Creates a subscription record in DB and a MercadoPago preapproval + */ +export async function createSubscription(params: { + tenantId: string; + plan: string; + amount: number; + payerEmail: string; +}) { + const tenant = await prisma.tenant.findUnique({ + where: { id: params.tenantId }, + }); + if (!tenant) throw new Error('Tenant no encontrado'); + + // Create MercadoPago preapproval + const mp = await mpService.createPreapproval({ + tenantId: params.tenantId, + reason: `Horux360 - Plan ${params.plan} - ${tenant.nombre}`, + amount: params.amount, + payerEmail: params.payerEmail, + }); + + // Create DB record + const subscription = await prisma.subscription.create({ + data: { + tenantId: params.tenantId, + plan: params.plan as any, + status: mp.status || 'pending', + amount: params.amount, + frequency: 'monthly', + mpPreapprovalId: mp.preapprovalId, + }, + }); + + invalidateSubscriptionCache(params.tenantId); + + return { + subscription, + paymentUrl: mp.initPoint, + }; +} + +/** + * Gets active subscription for a tenant (cached 5 min) + */ +export async function getActiveSubscription(tenantId: string) { + const cached = subscriptionCache.get(`sub:${tenantId}`); + if (cached && cached.expires > Date.now()) return cached.data; + + const subscription = await prisma.subscription.findFirst({ + where: { tenantId }, + orderBy: { createdAt: 'desc' }, + }); + + subscriptionCache.set(`sub:${tenantId}`, { + data: subscription, + expires: Date.now() + 5 * 60 * 1000, + }); + + return subscription; +} + +/** + * Updates subscription status from webhook notification + */ +export async function updateSubscriptionStatus(mpPreapprovalId: string, status: string) { + const subscription = await prisma.subscription.findFirst({ + where: { mpPreapprovalId }, + }); + if (!subscription) return null; + + const updated = await prisma.subscription.update({ + where: { id: subscription.id }, + data: { status }, + }); + + invalidateSubscriptionCache(subscription.tenantId); + + // Handle cancellation + if (status === 'cancelled') { + const tenant = await prisma.tenant.findUnique({ + where: { id: subscription.tenantId }, + include: { users: { where: { role: 'admin' }, take: 1 } }, + }); + if (tenant && tenant.users[0]) { + emailService.sendSubscriptionCancelled(tenant.users[0].email, { + nombre: tenant.nombre, + plan: subscription.plan, + }).catch(err => console.error('[EMAIL] Subscription cancelled notification failed:', err)); + } + } + + return updated; +} + +/** + * Records a payment from MercadoPago webhook + */ +export async function recordPayment(params: { + tenantId: string; + subscriptionId: string; + mpPaymentId: string; + amount: number; + status: string; + paymentMethod: string; +}) { + const payment = await prisma.payment.create({ + data: { + tenantId: params.tenantId, + subscriptionId: params.subscriptionId, + mpPaymentId: params.mpPaymentId, + amount: params.amount, + status: params.status, + paymentMethod: params.paymentMethod, + }, + }); + + // Send email notifications based on payment status + const tenant = await prisma.tenant.findUnique({ + where: { id: params.tenantId }, + include: { users: { where: { role: 'admin' }, take: 1 } }, + }); + + if (tenant && tenant.users[0]) { + const subscription = await prisma.subscription.findUnique({ + where: { id: params.subscriptionId }, + }); + + if (params.status === 'approved') { + emailService.sendPaymentConfirmed(tenant.users[0].email, { + nombre: tenant.nombre, + amount: params.amount, + plan: subscription?.plan || 'N/A', + date: new Date().toLocaleDateString('es-MX'), + }).catch(err => console.error('[EMAIL] Payment confirmed notification failed:', err)); + } else if (params.status === 'rejected') { + emailService.sendPaymentFailed(tenant.users[0].email, { + nombre: tenant.nombre, + amount: params.amount, + plan: subscription?.plan || 'N/A', + }).catch(err => console.error('[EMAIL] Payment failed notification failed:', err)); + } + } + + return payment; +} + +/** + * Manually marks a subscription as paid (for bank transfers) + */ +export async function markAsPaidManually(tenantId: string, amount: number) { + const subscription = await getActiveSubscription(tenantId); + if (!subscription) throw new Error('No hay suscripción activa'); + + // Update subscription status + await prisma.subscription.update({ + where: { id: subscription.id }, + data: { status: 'authorized' }, + }); + + // Record the manual payment + const payment = await prisma.payment.create({ + data: { + tenantId, + subscriptionId: subscription.id, + mpPaymentId: `manual-${Date.now()}`, + amount, + status: 'approved', + paymentMethod: 'bank_transfer', + }, + }); + + invalidateSubscriptionCache(tenantId); + return payment; +} + +/** + * Generates a payment link for a tenant + */ +export async function generatePaymentLink(tenantId: string) { + const tenant = await prisma.tenant.findUnique({ + where: { id: tenantId }, + include: { users: { where: { role: 'admin' }, take: 1 } }, + }); + if (!tenant) throw new Error('Tenant no encontrado'); + if (!tenant.users[0]) throw new Error('No admin user found'); + + const subscription = await getActiveSubscription(tenantId); + const plan = subscription?.plan || tenant.plan; + const amount = subscription?.amount || 0; + + if (!amount) throw new Error('No se encontró monto de suscripción'); + + const mp = await mpService.createPreapproval({ + tenantId, + reason: `Horux360 - Plan ${plan} - ${tenant.nombre}`, + amount, + payerEmail: tenant.users[0].email, + }); + + // Update subscription with new MP preapproval ID + if (subscription) { + await prisma.subscription.update({ + where: { id: subscription.id }, + data: { mpPreapprovalId: mp.preapprovalId }, + }); + } + + return { paymentUrl: mp.initPoint }; +} + +/** + * Gets payment history for a tenant + */ +export async function getPaymentHistory(tenantId: string) { + return prisma.payment.findMany({ + where: { tenantId }, + orderBy: { createdAt: 'desc' }, + take: 50, + }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ebae2a9..df60a96 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: jsonwebtoken: specifier: ^9.0.2 version: 9.0.3 + mercadopago: + specifier: ^2.12.0 + version: 2.12.0 node-cron: specifier: ^4.2.1 version: 4.2.1 @@ -1881,6 +1884,9 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + mercadopago@2.12.0: + resolution: {integrity: sha512-9S+ZB/Fltd4BV9/U79r7U/+LrYJP844kxxvtAlVbbeVmhOE9rZt0YhPy1GXO3Yf4XyQaHwZ/SCyL2kebAicaLw==} + merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} @@ -1963,6 +1969,15 @@ packages: resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==} engines: {node: '>=6.0.0'} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-forge@1.3.3: resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==} engines: {node: '>= 6.13.0'} @@ -2444,6 +2459,9 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + traverse@0.3.9: resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} @@ -2559,6 +2577,10 @@ packages: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -2566,6 +2588,12 @@ packages: victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + wmf@1.0.2: resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} engines: {node: '>=0.8'} @@ -4216,6 +4244,13 @@ snapshots: media-typer@0.3.0: {} + mercadopago@2.12.0: + dependencies: + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + merge-descriptors@1.0.3: {} merge2@1.4.1: {} @@ -4290,6 +4325,10 @@ snapshots: node-cron@4.2.1: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-forge@1.3.3: {} node-releases@2.0.27: {} @@ -4783,6 +4822,8 @@ snapshots: toidentifier@1.0.1: {} + tr46@0.0.3: {} + traverse@0.3.9: {} ts-interface-checker@0.1.13: {} @@ -4884,6 +4925,8 @@ snapshots: uuid@8.3.2: {} + uuid@9.0.1: {} + vary@1.1.2: {} victory-vendor@36.9.2: @@ -4903,6 +4946,13 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + wmf@1.0.2: {} word@0.3.0: {}