better-auth-mercadopago 0.1.8 → 0.2.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.
package/dist/index.mjs DELETED
@@ -1,1620 +0,0 @@
1
- // index.ts
2
- import { generateId } from "better-auth";
3
- import { APIError as APIError2, createAuthEndpoint } from "better-auth/api";
4
- import {
5
- Customer,
6
- MercadoPagoConfig,
7
- Payment,
8
- PreApproval,
9
- PreApprovalPlan,
10
- Preference
11
- } from "mercadopago";
12
- import { z } from "zod";
13
-
14
- // security.ts
15
- import crypto from "crypto";
16
- import { APIError } from "better-auth/api";
17
- function verifyWebhookSignature(params) {
18
- const { xSignature, xRequestId, dataId, secret } = params;
19
- if (!xSignature || !xRequestId) {
20
- return false;
21
- }
22
- const parts = xSignature.split(",");
23
- const ts = parts.find((p) => p.startsWith("ts="))?.split("=")[1];
24
- const hash = parts.find((p) => p.startsWith("v1="))?.split("=")[1];
25
- if (!ts || !hash) {
26
- return false;
27
- }
28
- const manifest = `id:${dataId};request-id:${xRequestId};ts:${ts};`;
29
- const hmac = crypto.createHmac("sha256", secret);
30
- hmac.update(manifest);
31
- const expectedHash = hmac.digest("hex");
32
- return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(expectedHash));
33
- }
34
- var RateLimiter = class {
35
- constructor() {
36
- this.attempts = /* @__PURE__ */ new Map();
37
- }
38
- check(key, maxAttempts, windowMs) {
39
- const now = Date.now();
40
- const record = this.attempts.get(key);
41
- if (!record || now > record.resetAt) {
42
- this.attempts.set(key, {
43
- count: 1,
44
- resetAt: now + windowMs
45
- });
46
- return true;
47
- }
48
- if (record.count >= maxAttempts) {
49
- return false;
50
- }
51
- record.count++;
52
- return true;
53
- }
54
- cleanup() {
55
- const now = Date.now();
56
- for (const [key, record] of this.attempts.entries()) {
57
- if (now > record.resetAt) {
58
- this.attempts.delete(key);
59
- }
60
- }
61
- }
62
- };
63
- var rateLimiter = new RateLimiter();
64
- setInterval(() => rateLimiter.cleanup(), 5 * 60 * 1e3);
65
- function validatePaymentAmount(requestedAmount, mpPaymentAmount, tolerance = 0.01) {
66
- const diff = Math.abs(requestedAmount - mpPaymentAmount);
67
- return diff <= tolerance;
68
- }
69
- function sanitizeMetadata(metadata) {
70
- const sanitized = {};
71
- for (const [key, value] of Object.entries(metadata)) {
72
- if (key === "__proto__" || key === "constructor" || key === "prototype") {
73
- continue;
74
- }
75
- if (typeof value === "string" && value.length > 5e3) {
76
- sanitized[key] = value.substring(0, 5e3);
77
- } else if (typeof value === "object" && value !== null) {
78
- sanitized[key] = sanitizeMetadata(value);
79
- } else {
80
- sanitized[key] = value;
81
- }
82
- }
83
- return sanitized;
84
- }
85
- function validateCallbackUrl(url, allowedDomains) {
86
- try {
87
- const parsed = new URL(url);
88
- if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
89
- return false;
90
- }
91
- const hostname = parsed.hostname;
92
- return allowedDomains.some((domain) => {
93
- if (domain.startsWith("*.")) {
94
- const baseDomain = domain.substring(2);
95
- return hostname.endsWith(baseDomain);
96
- }
97
- return hostname === domain;
98
- });
99
- } catch {
100
- return false;
101
- }
102
- }
103
- function validateIdempotencyKey(key) {
104
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
105
- const customRegex = /^[a-zA-Z0-9_-]{8,64}$/;
106
- return uuidRegex.test(key) || customRegex.test(key);
107
- }
108
- var MercadoPagoError = class extends Error {
109
- constructor(code, message, statusCode = 400, details) {
110
- super(message);
111
- this.code = code;
112
- this.message = message;
113
- this.statusCode = statusCode;
114
- this.details = details;
115
- this.name = "MercadoPagoError";
116
- }
117
- toAPIError() {
118
- const errorMap = {
119
- 400: "BAD_REQUEST",
120
- 401: "UNAUTHORIZED",
121
- 403: "FORBIDDEN",
122
- 404: "NOT_FOUND",
123
- 429: "TOO_MANY_REQUESTS",
124
- 500: "INTERNAL_SERVER_ERROR"
125
- };
126
- const type = errorMap[this.statusCode] || "BAD_REQUEST";
127
- return new APIError(type, {
128
- message: this.message,
129
- details: this.details
130
- });
131
- }
132
- };
133
- function handleMercadoPagoError(error) {
134
- if (error.status) {
135
- const mpError = new MercadoPagoError(
136
- error.code || "unknown_error",
137
- error.message || "An error occurred with Mercado Pago",
138
- error.status,
139
- error.cause
140
- );
141
- throw mpError.toAPIError();
142
- }
143
- throw new APIError("INTERNAL_SERVER_ERROR", {
144
- message: "Failed to process Mercado Pago request"
145
- });
146
- }
147
- var VALID_WEBHOOK_TOPICS = [
148
- "payment",
149
- "merchant_order",
150
- "subscription_preapproval",
151
- "subscription_preapproval_plan",
152
- "subscription_authorized_payment",
153
- "point_integration_wh",
154
- "topic_claims_integration_wh",
155
- "topic_merchant_order_wh",
156
- "delivery_cancellation"
157
- ];
158
- function isValidWebhookTopic(topic) {
159
- return VALID_WEBHOOK_TOPICS.includes(topic);
160
- }
161
- var IdempotencyStore = class {
162
- constructor() {
163
- // biome-ignore lint/suspicious/noExplicitAny: <necessary>
164
- this.store = /* @__PURE__ */ new Map();
165
- }
166
- // biome-ignore lint/suspicious/noExplicitAny: <necessary>
167
- get(key) {
168
- const record = this.store.get(key);
169
- if (!record || Date.now() > record.expiresAt) {
170
- this.store.delete(key);
171
- return null;
172
- }
173
- return record.result;
174
- }
175
- // biome-ignore lint/suspicious/noExplicitAny: <necessary>
176
- set(key, result, ttlMs = 24 * 60 * 60 * 1e3) {
177
- this.store.set(key, {
178
- result,
179
- expiresAt: Date.now() + ttlMs
180
- });
181
- }
182
- cleanup() {
183
- const now = Date.now();
184
- for (const [key, record] of this.store.entries()) {
185
- if (now > record.expiresAt) {
186
- this.store.delete(key);
187
- }
188
- }
189
- }
190
- };
191
- var idempotencyStore = new IdempotencyStore();
192
- setInterval(() => idempotencyStore.cleanup(), 60 * 60 * 1e3);
193
- var ValidationRules = {
194
- email: (email) => {
195
- const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
196
- return regex.test(email) && email.length <= 255;
197
- },
198
- amount: (amount) => {
199
- return amount > 0 && amount <= 999999999 && !Number.isNaN(amount);
200
- },
201
- currency: (currency) => {
202
- const validCurrencies = [
203
- "ARS",
204
- "BRL",
205
- "CLP",
206
- "MXN",
207
- "COP",
208
- "PEN",
209
- "UYU",
210
- "USD"
211
- ];
212
- return validCurrencies.includes(currency);
213
- },
214
- frequency: (frequency) => {
215
- return frequency > 0 && frequency <= 365 && Number.isInteger(frequency);
216
- },
217
- userId: (userId) => {
218
- return /^[a-zA-Z0-9_-]{1,100}$/.test(userId);
219
- }
220
- };
221
-
222
- // client.ts
223
- var mercadoPagoClientPlugin = () => {
224
- return {
225
- id: "mercadopago",
226
- $InferServerPlugin: {},
227
- getActions: ($fetch) => ({
228
- /**
229
- * Get or create a Mercado Pago customer for the authenticated user
230
- */
231
- getOrCreateCustomer: async (data, fetchOptions) => {
232
- return await $fetch("/mercado-pago/customer", {
233
- method: "POST",
234
- body: data || {},
235
- ...fetchOptions
236
- });
237
- },
238
- /**
239
- * Create a payment and get checkout URL
240
- *
241
- * @example
242
- * ```ts
243
- * const { data } = await authClient.mercadoPago.createPayment({
244
- * items: [{
245
- * title: "Premium Plan",
246
- * quantity: 1,
247
- * unitPrice: 99.90,
248
- * currencyId: "ARS"
249
- * }]
250
- * });
251
- *
252
- * // Redirect user to checkout
253
- * window.location.href = data.checkoutUrl;
254
- * ```
255
- */
256
- createPayment: async (data, fetchOptions) => {
257
- return await $fetch("/mercado-pago/payment/create", {
258
- method: "POST",
259
- body: data,
260
- ...fetchOptions
261
- });
262
- },
263
- /**
264
- * Create a marketplace payment with automatic split
265
- *
266
- * You need to have the seller's MP User ID (collector_id) which they get
267
- * after authorizing your app via OAuth.
268
- *
269
- * @example
270
- * ```ts
271
- * const { data } = await authClient.mercadoPago.createPayment({
272
- * items: [{
273
- * title: "Product from Seller",
274
- * quantity: 1,
275
- * unitPrice: 100
276
- * }],
277
- * marketplace: {
278
- * collectorId: "123456789", // Seller's MP User ID
279
- * applicationFeePercentage: 10 // Platform keeps 10%
280
- * }
281
- * });
282
- * ```
283
- */
284
- createMarketplacePayment: async (data, fetchOptions) => {
285
- return await $fetch("/mercado-pago/payment/create", {
286
- method: "POST",
287
- body: data,
288
- ...fetchOptions
289
- });
290
- },
291
- /**
292
- * Create a subscription with recurring payments
293
- *
294
- * Supports two modes:
295
- * 1. With preapproval plan (reusable): Pass preapprovalPlanId
296
- * 2. Direct subscription (one-off): Pass reason + autoRecurring
297
- *
298
- * @example With plan
299
- * ```ts
300
- * const { data } = await authClient.mercadoPago.createSubscription({
301
- * preapprovalPlanId: "plan_abc123"
302
- * });
303
- * ```
304
- *
305
- * @example Direct (without plan)
306
- * ```ts
307
- * const { data } = await authClient.mercadoPago.createSubscription({
308
- * reason: "Premium Monthly Plan",
309
- * autoRecurring: {
310
- * frequency: 1,
311
- * frequencyType: "months",
312
- * transactionAmount: 99.90,
313
- * currencyId: "ARS"
314
- * }
315
- * });
316
- * ```
317
- */
318
- createSubscription: async (data, fetchOptions) => {
319
- return await $fetch("/mercado-pago/subscription/create", {
320
- method: "POST",
321
- body: data,
322
- ...fetchOptions
323
- });
324
- },
325
- /**
326
- * Cancel a subscription
327
- *
328
- * @example
329
- * ```ts
330
- * await authClient.mercadoPago.cancelSubscription({
331
- * subscriptionId: "sub_123"
332
- * });
333
- * ```
334
- */
335
- cancelSubscription: async (data, fetchOptions) => {
336
- return await $fetch("/mercado-pago/subscription/cancel", {
337
- method: "POST",
338
- body: data,
339
- ...fetchOptions
340
- });
341
- },
342
- /**
343
- * Create a reusable preapproval plan (subscription template)
344
- *
345
- * Plans can be reused for multiple subscriptions. Create once,
346
- * use many times with createSubscription({ preapprovalPlanId })
347
- *
348
- * @example
349
- * ```ts
350
- * const { data } = await authClient.mercadoPago.createPreapprovalPlan({
351
- * reason: "Premium Monthly",
352
- * autoRecurring: {
353
- * frequency: 1,
354
- * frequencyType: "months",
355
- * transactionAmount: 99.90,
356
- * freeTrial: {
357
- * frequency: 7,
358
- * frequencyType: "days"
359
- * }
360
- * },
361
- * repetitions: 12 // 12 months, omit for infinite
362
- * });
363
- *
364
- * // Use the plan
365
- * const planId = data.plan.mercadoPagoPlanId;
366
- * ```
367
- */
368
- createPreapprovalPlan: async (data, fetchOptions) => {
369
- return await $fetch("/mercado-pago/plan/create", {
370
- method: "POST",
371
- body: data,
372
- ...fetchOptions
373
- });
374
- },
375
- /**
376
- * List all preapproval plans
377
- *
378
- * @example
379
- * ```ts
380
- * const { data } = await authClient.mercadoPago.listPreapprovalPlans();
381
- *
382
- * data.plans.forEach(plan => {
383
- * console.log(plan.reason); // "Premium Monthly"
384
- * console.log(plan.transactionAmount); // 99.90
385
- * });
386
- * ```
387
- */
388
- listPreapprovalPlans: async (fetchOptions) => {
389
- return await $fetch("/mercado-pago/plans", {
390
- method: "GET",
391
- ...fetchOptions
392
- });
393
- },
394
- /**
395
- * Get payment by ID
396
- */
397
- getPayment: async (paymentId, fetchOptions) => {
398
- return await $fetch(`/mercado-pago/payment/${paymentId}`, {
399
- method: "GET",
400
- ...fetchOptions
401
- });
402
- },
403
- /**
404
- * List all payments for the authenticated user
405
- *
406
- * @example
407
- * ```ts
408
- * const { data } = await authClient.mercadoPago.listPayments({
409
- * limit: 20,
410
- * offset: 0
411
- * });
412
- * ```
413
- */
414
- listPayments: async (params, fetchOptions) => {
415
- const query = new URLSearchParams();
416
- if (params?.limit) query.set("limit", params.limit.toString());
417
- if (params?.offset) query.set("offset", params.offset.toString());
418
- return await $fetch(`/mercado-pago/payments?${query.toString()}`, {
419
- method: "GET",
420
- ...fetchOptions
421
- });
422
- },
423
- /**
424
- * List all subscriptions for the authenticated user
425
- *
426
- * @example
427
- * ```ts
428
- * const { data } = await authClient.mercadoPago.listSubscriptions();
429
- * ```
430
- */
431
- listSubscriptions: async (fetchOptions) => {
432
- return await $fetch(`/mercado-pago/subscriptions`, {
433
- method: "GET",
434
- ...fetchOptions
435
- });
436
- },
437
- /**
438
- * Get OAuth authorization URL for marketplace sellers
439
- *
440
- * This is Step 1 of OAuth flow. Redirect the seller to this URL so they
441
- * can authorize your app to process payments on their behalf.
442
- *
443
- * @example
444
- * ```ts
445
- * const { data } = await authClient.mercadoPago.getOAuthUrl({
446
- * redirectUri: "https://myapp.com/oauth/callback"
447
- * });
448
- *
449
- * // Redirect seller to authorize
450
- * window.location.href = data.authUrl;
451
- * ```
452
- */
453
- getOAuthUrl: async (params, fetchOptions) => {
454
- const query = new URLSearchParams();
455
- query.set("redirectUri", params.redirectUri);
456
- return await $fetch(
457
- `/mercado-pago/oauth/authorize?${query.toString()}`,
458
- {
459
- method: "GET",
460
- ...fetchOptions
461
- }
462
- );
463
- },
464
- /**
465
- * Exchange OAuth code for access token
466
- *
467
- * This is Step 2 of OAuth flow. After the seller authorizes and MP redirects
468
- * them back with a code, exchange that code for an access token.
469
- *
470
- * @example
471
- * ```ts
472
- * // In your /oauth/callback page:
473
- * const code = new URLSearchParams(window.location.search).get("code");
474
- *
475
- * const { data } = await authClient.mercadoPago.exchangeOAuthCode({
476
- * code,
477
- * redirectUri: "https://myapp.com/oauth/callback"
478
- * });
479
- *
480
- * // Now you have the seller's MP User ID
481
- * console.log(data.oauthToken.mercadoPagoUserId);
482
- * ```
483
- */
484
- exchangeOAuthCode: async (data, fetchOptions) => {
485
- return await $fetch("/mercado-pago/oauth/callback", {
486
- method: "POST",
487
- body: data,
488
- ...fetchOptions
489
- });
490
- }
491
- })
492
- };
493
- };
494
-
495
- // index.ts
496
- var mercadoPagoPlugin = (options) => {
497
- const client = new MercadoPagoConfig({
498
- accessToken: options.accessToken
499
- });
500
- const preferenceClient = new Preference(client);
501
- const paymentClient = new Payment(client);
502
- const customerClient = new Customer(client);
503
- const preApprovalClient = new PreApproval(client);
504
- const preApprovalPlanClient = new PreApprovalPlan(client);
505
- return {
506
- id: "mercadopago",
507
- schema: {
508
- // Customer table - stores MP customer info
509
- mercadoPagoCustomer: {
510
- fields: {
511
- id: { type: "string", required: true },
512
- userId: {
513
- type: "string",
514
- required: true,
515
- references: { model: "user", field: "id", onDelete: "cascade" }
516
- },
517
- mercadoPagoId: { type: "string", required: true, unique: true },
518
- email: { type: "string", required: true },
519
- createdAt: { type: "date", required: true },
520
- updatedAt: { type: "date", required: true }
521
- }
522
- },
523
- // Payment table - one-time payments
524
- mercadoPagoPayment: {
525
- fields: {
526
- id: { type: "string", required: true },
527
- userId: {
528
- type: "string",
529
- required: true,
530
- references: { model: "user", field: "id", onDelete: "cascade" }
531
- },
532
- mercadoPagoPaymentId: {
533
- type: "string",
534
- required: true,
535
- unique: true
536
- },
537
- preferenceId: { type: "string", required: true },
538
- status: { type: "string", required: true },
539
- // pending, approved, authorized, rejected, cancelled, refunded, charged_back
540
- statusDetail: { type: "string" },
541
- // accredited, pending_contingency, pending_review_manual, cc_rejected_*, etc
542
- amount: { type: "number", required: true },
543
- currency: { type: "string", required: true },
544
- paymentMethodId: { type: "string" },
545
- // visa, master, pix, etc
546
- paymentTypeId: { type: "string" },
547
- // credit_card, debit_card, ticket, etc
548
- metadata: { type: "string" },
549
- // JSON stringified
550
- createdAt: { type: "date", required: true },
551
- updatedAt: { type: "date", required: true }
552
- }
553
- },
554
- // Subscription table
555
- mercadoPagoSubscription: {
556
- fields: {
557
- id: { type: "string", required: true },
558
- userId: {
559
- type: "string",
560
- required: true,
561
- references: { model: "user", field: "id", onDelete: "cascade" }
562
- },
563
- mercadoPagoSubscriptionId: {
564
- type: "string",
565
- required: true,
566
- unique: true
567
- },
568
- planId: { type: "string", required: true },
569
- status: { type: "string", required: true },
570
- // authorized, paused, cancelled, pending
571
- reason: { type: "string" },
572
- // Reason for status (e.g., payment_failed, user_cancelled)
573
- nextPaymentDate: { type: "date" },
574
- lastPaymentDate: { type: "date" },
575
- summarized: { type: "string" },
576
- // JSON with charges, charged_amount, pending_charge_amount
577
- metadata: { type: "string" },
578
- // JSON stringified
579
- createdAt: { type: "date", required: true },
580
- updatedAt: { type: "date", required: true }
581
- }
582
- },
583
- // Preapproval Plan table (reusable subscription plans)
584
- mercadoPagoPreapprovalPlan: {
585
- fields: {
586
- id: { type: "string", required: true },
587
- mercadoPagoPlanId: { type: "string", required: true, unique: true },
588
- reason: { type: "string", required: true },
589
- // Plan description
590
- frequency: { type: "number", required: true },
591
- frequencyType: { type: "string", required: true },
592
- // days, months
593
- transactionAmount: { type: "number", required: true },
594
- currencyId: { type: "string", required: true },
595
- repetitions: { type: "number" },
596
- // null = infinite
597
- freeTrial: { type: "string" },
598
- // JSON with frequency and frequency_type
599
- metadata: { type: "string" },
600
- // JSON stringified
601
- createdAt: { type: "date", required: true },
602
- updatedAt: { type: "date", required: true }
603
- }
604
- },
605
- // Split payments table (for marketplace)
606
- mercadoPagoMarketplaceSplit: {
607
- fields: {
608
- id: { type: "string", required: true },
609
- paymentId: {
610
- type: "string",
611
- required: true,
612
- references: {
613
- model: "mercadoPagoPayment",
614
- field: "id",
615
- onDelete: "cascade"
616
- }
617
- },
618
- // Changed naming to be more clear
619
- collectorId: { type: "string", required: true },
620
- // MP User ID who receives the money (seller)
621
- collectorEmail: { type: "string", required: true },
622
- // Email of who receives money
623
- applicationFeeAmount: { type: "number" },
624
- // Platform commission in absolute value
625
- applicationFeePercentage: { type: "number" },
626
- // Platform commission percentage
627
- netAmount: { type: "number", required: true },
628
- // Amount that goes to collector (seller)
629
- metadata: { type: "string" },
630
- createdAt: { type: "date", required: true }
631
- }
632
- },
633
- // OAuth tokens for marketplace (to make payments on behalf of sellers)
634
- mercadoPagoOAuthToken: {
635
- fields: {
636
- id: { type: "string", required: true },
637
- userId: {
638
- type: "string",
639
- required: true,
640
- references: { model: "user", field: "id", onDelete: "cascade" }
641
- },
642
- accessToken: { type: "string", required: true },
643
- refreshToken: { type: "string", required: true },
644
- publicKey: { type: "string", required: true },
645
- mercadoPagoUserId: { type: "string", required: true, unique: true },
646
- expiresAt: { type: "date", required: true },
647
- createdAt: { type: "date", required: true },
648
- updatedAt: { type: "date", required: true }
649
- }
650
- }
651
- },
652
- endpoints: {
653
- // Get or create customer automatically
654
- getOrCreateCustomer: createAuthEndpoint(
655
- "/mercado-pago/customer",
656
- {
657
- method: "POST",
658
- requireAuth: true,
659
- body: z.object({
660
- email: z.string().email().optional(),
661
- firstName: z.string().optional(),
662
- lastName: z.string().optional()
663
- })
664
- },
665
- async (ctx) => {
666
- const session = ctx.context.session;
667
- if (!session) {
668
- throw new APIError2("UNAUTHORIZED", {
669
- message: "You must be logged in"
670
- });
671
- }
672
- const { email, firstName, lastName } = ctx.body;
673
- const userEmail = email || session.user.email;
674
- const existingCustomer = await ctx.context.adapter.findOne({
675
- model: "mercadoPagoCustomer",
676
- where: [{ field: "userId", value: session.user.id }]
677
- });
678
- if (existingCustomer) {
679
- return ctx.json({ customer: existingCustomer });
680
- }
681
- const mpCustomer = await customerClient.create({
682
- body: {
683
- email: userEmail,
684
- first_name: firstName,
685
- last_name: lastName
686
- }
687
- });
688
- const customer = await ctx.context.adapter.create({
689
- model: "mercadoPagoCustomer",
690
- data: {
691
- id: generateId(),
692
- userId: session.user.id,
693
- mercadoPagoId: mpCustomer.id,
694
- email: userEmail,
695
- createdAt: /* @__PURE__ */ new Date(),
696
- updatedAt: /* @__PURE__ */ new Date()
697
- }
698
- });
699
- return ctx.json({ customer });
700
- }
701
- ),
702
- // OAuth: Get authorization URL for marketplace sellers
703
- getOAuthUrl: createAuthEndpoint(
704
- "/mercado-pago/oauth/authorize",
705
- {
706
- method: "GET",
707
- requireAuth: true,
708
- query: z.object({
709
- redirectUri: z.string().url()
710
- })
711
- },
712
- async (ctx) => {
713
- const session = ctx.context.session;
714
- if (!session) {
715
- throw new APIError2("UNAUTHORIZED");
716
- }
717
- if (!options.appId) {
718
- throw new APIError2("BAD_REQUEST", {
719
- message: "OAuth not configured. Please provide appId in plugin options"
720
- });
721
- }
722
- const { redirectUri } = ctx.query;
723
- if (!ctx.context.isTrustedOrigin(redirectUri)) {
724
- throw new APIError2("FORBIDDEN", {
725
- message: "Redirect URI not in trusted origins"
726
- });
727
- }
728
- const authUrl = `https://auth.mercadopago.com/authorization?client_id=${options.appId}&response_type=code&platform_id=mp&state=${session.user.id}&redirect_uri=${encodeURIComponent(redirectUri)}`;
729
- return ctx.json({ authUrl });
730
- }
731
- ),
732
- // OAuth: Exchange code for access token
733
- exchangeOAuthCode: createAuthEndpoint(
734
- "/mercado-pago/oauth/callback",
735
- {
736
- method: "POST",
737
- requireAuth: true,
738
- body: z.object({
739
- code: z.string(),
740
- redirectUri: z.string().url()
741
- })
742
- },
743
- async (ctx) => {
744
- const session = ctx.context.session;
745
- if (!session) {
746
- throw new APIError2("UNAUTHORIZED");
747
- }
748
- if (!options.appId || !options.appSecret) {
749
- throw new APIError2("BAD_REQUEST", {
750
- message: "OAuth not configured"
751
- });
752
- }
753
- const { code, redirectUri } = ctx.body;
754
- const tokenResponse = await fetch(
755
- "https://api.mercadopago.com/oauth/token",
756
- {
757
- method: "POST",
758
- headers: { "Content-Type": "application/json" },
759
- body: JSON.stringify({
760
- client_id: options.appId,
761
- client_secret: options.appSecret,
762
- grant_type: "authorization_code",
763
- code,
764
- redirect_uri: redirectUri
765
- })
766
- }
767
- );
768
- if (!tokenResponse.ok) {
769
- throw new APIError2("BAD_REQUEST", {
770
- message: "Failed to exchange OAuth code"
771
- });
772
- }
773
- const tokenData = await tokenResponse.json();
774
- const oauthToken = await ctx.context.adapter.create({
775
- model: "mercadoPagoOAuthToken",
776
- data: {
777
- id: generateId(),
778
- userId: session.user.id,
779
- accessToken: tokenData.access_token,
780
- refreshToken: tokenData.refresh_token,
781
- publicKey: tokenData.public_key,
782
- mercadoPagoUserId: tokenData.user_id.toString(),
783
- expiresAt: new Date(Date.now() + tokenData.expires_in * 1e3),
784
- createdAt: /* @__PURE__ */ new Date(),
785
- updatedAt: /* @__PURE__ */ new Date()
786
- }
787
- });
788
- return ctx.json({
789
- success: true,
790
- oauthToken: {
791
- id: oauthToken.id,
792
- mercadoPagoUserId: oauthToken.mercadoPagoUserId,
793
- expiresAt: oauthToken.expiresAt
794
- }
795
- });
796
- }
797
- ),
798
- // Create a reusable preapproval plan (subscription plan)
799
- createPreapprovalPlan: createAuthEndpoint(
800
- "/mercado-pago/plan/create",
801
- {
802
- method: "POST",
803
- body: z.object({
804
- reason: z.string(),
805
- // Plan description (e.g., "Premium Monthly")
806
- autoRecurring: z.object({
807
- frequency: z.number(),
808
- // 1, 7, 30, etc
809
- frequencyType: z.enum(["days", "months"]),
810
- transactionAmount: z.number(),
811
- currencyId: z.string().default("ARS"),
812
- freeTrial: z.object({
813
- frequency: z.number(),
814
- frequencyType: z.enum(["days", "months"])
815
- }).optional()
816
- }),
817
- repetitions: z.number().optional(),
818
- // null = infinite
819
- backUrl: z.string().optional(),
820
- metadata: z.record(z.any()).optional()
821
- })
822
- },
823
- async (ctx) => {
824
- const { reason, autoRecurring, repetitions, backUrl, metadata } = ctx.body;
825
- const baseUrl = options.baseUrl || ctx.context.baseURL;
826
- const planBody = {
827
- reason,
828
- auto_recurring: {
829
- frequency: autoRecurring.frequency,
830
- frequency_type: autoRecurring.frequencyType,
831
- transaction_amount: autoRecurring.transactionAmount,
832
- currency_id: autoRecurring.currencyId
833
- },
834
- back_url: backUrl || `${baseUrl}/plan/created`
835
- };
836
- if (repetitions && planBody.auto_recurring) {
837
- planBody.auto_recurring.repetitions = repetitions;
838
- }
839
- if (autoRecurring.freeTrial && planBody.auto_recurring) {
840
- planBody.auto_recurring.free_trial = {
841
- frequency: autoRecurring.freeTrial.frequency,
842
- frequency_type: autoRecurring.freeTrial.frequencyType
843
- };
844
- }
845
- const mpPlan = await preApprovalPlanClient.create({ body: planBody });
846
- const plan = await ctx.context.adapter.create({
847
- model: "mercadoPagoPreapprovalPlan",
848
- data: {
849
- id: generateId(),
850
- mercadoPagoPlanId: mpPlan.id,
851
- reason,
852
- frequency: autoRecurring.frequency,
853
- frequencyType: autoRecurring.frequencyType,
854
- transactionAmount: autoRecurring.transactionAmount,
855
- currencyId: autoRecurring.currencyId,
856
- repetitions: repetitions || null,
857
- freeTrial: autoRecurring.freeTrial ? JSON.stringify(autoRecurring.freeTrial) : null,
858
- metadata: JSON.stringify(metadata || {}),
859
- createdAt: /* @__PURE__ */ new Date(),
860
- updatedAt: /* @__PURE__ */ new Date()
861
- }
862
- });
863
- return ctx.json({ plan });
864
- }
865
- ),
866
- // List all preapproval plans
867
- listPreapprovalPlans: createAuthEndpoint(
868
- "/mercado-pago/plans",
869
- {
870
- method: "GET"
871
- },
872
- async (ctx) => {
873
- const plans = await ctx.context.adapter.findMany({
874
- model: "mercadoPagoPreapprovalPlan"
875
- });
876
- return ctx.json({ plans });
877
- }
878
- ),
879
- // Create payment preference
880
- createPayment: createAuthEndpoint(
881
- "/mercado-pago/payment/create",
882
- {
883
- method: "POST",
884
- requireAuth: true,
885
- body: z.object({
886
- items: z.array(
887
- z.object({
888
- id: z.string(),
889
- title: z.string().min(1).max(256),
890
- quantity: z.number().int().min(1).max(1e4),
891
- unitPrice: z.number().positive().max(999999999),
892
- currencyId: z.string().default("ARS")
893
- })
894
- ).min(1).max(100),
895
- metadata: z.record(z.any()).optional(),
896
- marketplace: z.object({
897
- collectorId: z.string(),
898
- applicationFee: z.number().positive().optional(),
899
- applicationFeePercentage: z.number().min(0).max(100).optional()
900
- }).optional(),
901
- successUrl: z.string().url().optional(),
902
- failureUrl: z.string().url().optional(),
903
- pendingUrl: z.string().url().optional(),
904
- idempotencyKey: z.string().optional()
905
- })
906
- },
907
- async (ctx) => {
908
- const session = ctx.context.session;
909
- if (!session) {
910
- throw new APIError2("UNAUTHORIZED");
911
- }
912
- const rateLimitKey = `payment:create:${session.user.id}`;
913
- if (!rateLimiter.check(rateLimitKey, 10, 60 * 1e3)) {
914
- throw new APIError2("TOO_MANY_REQUESTS", {
915
- message: "Too many payment creation attempts. Please try again later."
916
- });
917
- }
918
- const {
919
- items,
920
- metadata,
921
- marketplace,
922
- successUrl,
923
- failureUrl,
924
- pendingUrl,
925
- idempotencyKey
926
- } = ctx.body;
927
- if (idempotencyKey) {
928
- if (!validateIdempotencyKey(idempotencyKey)) {
929
- throw new APIError2("BAD_REQUEST", {
930
- message: "Invalid idempotency key format"
931
- });
932
- }
933
- const cachedResult = idempotencyStore.get(idempotencyKey);
934
- if (cachedResult) {
935
- return ctx.json(cachedResult);
936
- }
937
- }
938
- if (options.trustedOrigins) {
939
- const urls = [successUrl, failureUrl, pendingUrl].filter(
940
- Boolean
941
- );
942
- for (const url of urls) {
943
- if (!validateCallbackUrl(url, options.trustedOrigins)) {
944
- throw new APIError2("FORBIDDEN", {
945
- message: `URL ${url} is not in trusted origins`
946
- });
947
- }
948
- }
949
- }
950
- if (items.some((item) => !ValidationRules.currency(item.currencyId))) {
951
- throw new APIError2("BAD_REQUEST", {
952
- message: "Invalid currency code"
953
- });
954
- }
955
- const sanitizedMetadata = metadata ? sanitizeMetadata(metadata) : {};
956
- let customer = await ctx.context.adapter.findOne({
957
- model: "mercadoPagoCustomer",
958
- where: [{ field: "userId", value: session.user.id }]
959
- });
960
- if (!customer) {
961
- try {
962
- const mpCustomer = await customerClient.create({
963
- body: { email: session.user.email }
964
- });
965
- customer = await ctx.context.adapter.create({
966
- model: "mercadoPagoCustomer",
967
- data: {
968
- id: generateId(),
969
- userId: session.user.id,
970
- mercadoPagoId: mpCustomer.id,
971
- email: session.user.email,
972
- createdAt: /* @__PURE__ */ new Date(),
973
- updatedAt: /* @__PURE__ */ new Date()
974
- }
975
- });
976
- } catch (error) {
977
- handleMercadoPagoError(error);
978
- }
979
- }
980
- const baseUrl = options.baseUrl || ctx.context.baseURL;
981
- const totalAmount = items.reduce(
982
- (sum, item) => sum + item.unitPrice * item.quantity,
983
- 0
984
- );
985
- if (!ValidationRules.amount(totalAmount)) {
986
- throw new APIError2("BAD_REQUEST", {
987
- message: "Invalid payment amount"
988
- });
989
- }
990
- let applicationFeeAmount = 0;
991
- if (marketplace) {
992
- if (marketplace.applicationFee) {
993
- applicationFeeAmount = marketplace.applicationFee;
994
- } else if (marketplace.applicationFeePercentage) {
995
- applicationFeeAmount = totalAmount * marketplace.applicationFeePercentage / 100;
996
- }
997
- if (applicationFeeAmount >= totalAmount) {
998
- throw new APIError2("BAD_REQUEST", {
999
- message: "Application fee cannot exceed total amount"
1000
- });
1001
- }
1002
- }
1003
- const preferenceBody = {
1004
- items: items.map((item) => ({
1005
- id: item.id,
1006
- title: item.title,
1007
- quantity: item.quantity,
1008
- unit_price: item.unitPrice,
1009
- currency_id: item.currencyId
1010
- })),
1011
- payer: {
1012
- email: session.user.email
1013
- },
1014
- back_urls: {
1015
- success: successUrl || `${baseUrl}/payment/success`,
1016
- failure: failureUrl || `${baseUrl}/payment/failure`,
1017
- pending: pendingUrl || `${baseUrl}/payment/pending`
1018
- },
1019
- notification_url: `${baseUrl}/api/auth/mercado-pago/webhook`,
1020
- metadata: {
1021
- ...sanitizedMetadata,
1022
- userId: session.user.id,
1023
- customerId: customer?.id
1024
- },
1025
- expires: true,
1026
- expiration_date_from: (/* @__PURE__ */ new Date()).toISOString(),
1027
- expiration_date_to: new Date(
1028
- Date.now() + 30 * 24 * 60 * 60 * 1e3
1029
- ).toISOString()
1030
- // 30 days
1031
- };
1032
- if (marketplace) {
1033
- preferenceBody.marketplace = marketplace.collectorId;
1034
- preferenceBody.marketplace_fee = applicationFeeAmount;
1035
- }
1036
- let preference;
1037
- try {
1038
- preference = await preferenceClient.create({
1039
- body: preferenceBody
1040
- });
1041
- } catch (error) {
1042
- handleMercadoPagoError(error);
1043
- }
1044
- const payment = await ctx.context.adapter.create({
1045
- model: "mercadoPagoPayment",
1046
- data: {
1047
- id: generateId(),
1048
- userId: session.user.id,
1049
- mercadoPagoPaymentId: preference.id,
1050
- preferenceId: preference.id,
1051
- status: "pending",
1052
- amount: totalAmount,
1053
- currency: items[0].currencyId,
1054
- metadata: JSON.stringify(sanitizedMetadata),
1055
- createdAt: /* @__PURE__ */ new Date(),
1056
- updatedAt: /* @__PURE__ */ new Date()
1057
- }
1058
- });
1059
- if (marketplace) {
1060
- await ctx.context.adapter.create({
1061
- model: "mercadoPagoMarketplaceSplit",
1062
- data: {
1063
- id: generateId(),
1064
- paymentId: payment.id,
1065
- collectorId: marketplace.collectorId,
1066
- collectorEmail: "",
1067
- // Will be updated via webhook
1068
- applicationFeeAmount,
1069
- applicationFeePercentage: marketplace.applicationFeePercentage,
1070
- netAmount: totalAmount - applicationFeeAmount,
1071
- metadata: JSON.stringify({}),
1072
- createdAt: /* @__PURE__ */ new Date()
1073
- }
1074
- });
1075
- }
1076
- const result = {
1077
- checkoutUrl: preference.init_point,
1078
- preferenceId: preference.id,
1079
- payment
1080
- };
1081
- if (idempotencyKey) {
1082
- idempotencyStore.set(idempotencyKey, result);
1083
- }
1084
- return ctx.json(result);
1085
- }
1086
- ),
1087
- // Create subscription (supports both with and without preapproval plan)
1088
- createSubscription: createAuthEndpoint(
1089
- "/mercado-pago/subscription/create",
1090
- {
1091
- method: "POST",
1092
- requireAuth: true,
1093
- body: z.object({
1094
- // Option 1: Use existing preapproval plan
1095
- preapprovalPlanId: z.string().optional(),
1096
- // Option 2: Create subscription directly without plan
1097
- reason: z.string().optional(),
1098
- // Description of subscription
1099
- autoRecurring: z.object({
1100
- frequency: z.number(),
1101
- // 1 for monthly
1102
- frequencyType: z.enum(["days", "months"]),
1103
- transactionAmount: z.number(),
1104
- currencyId: z.string().default("ARS"),
1105
- startDate: z.string().optional(),
1106
- // ISO date
1107
- endDate: z.string().optional(),
1108
- // ISO date
1109
- freeTrial: z.object({
1110
- frequency: z.number(),
1111
- frequencyType: z.enum(["days", "months"])
1112
- }).optional()
1113
- }).optional(),
1114
- backUrl: z.string().optional(),
1115
- metadata: z.record(z.any()).optional()
1116
- })
1117
- },
1118
- async (ctx) => {
1119
- const session = ctx.context.session;
1120
- if (!session) {
1121
- throw new APIError2("UNAUTHORIZED");
1122
- }
1123
- const {
1124
- preapprovalPlanId,
1125
- reason,
1126
- autoRecurring,
1127
- backUrl,
1128
- metadata
1129
- } = ctx.body;
1130
- if (!preapprovalPlanId) {
1131
- if (!reason || !autoRecurring) {
1132
- throw new APIError2("BAD_REQUEST", {
1133
- message: "Must provide either preapprovalPlanId or (reason + autoRecurring)"
1134
- });
1135
- }
1136
- }
1137
- let customer = await ctx.context.adapter.findOne({
1138
- model: "mercadoPagoCustomer",
1139
- where: [{ field: "userId", value: session.user.id }]
1140
- });
1141
- if (!customer) {
1142
- const mpCustomer = await customerClient.create({
1143
- body: { email: session.user.email }
1144
- });
1145
- customer = await ctx.context.adapter.create({
1146
- model: "mercadoPagoCustomer",
1147
- data: {
1148
- id: generateId(),
1149
- userId: session.user.id,
1150
- mercadoPagoId: mpCustomer.id,
1151
- email: session.user.email,
1152
- createdAt: /* @__PURE__ */ new Date(),
1153
- updatedAt: /* @__PURE__ */ new Date()
1154
- }
1155
- });
1156
- }
1157
- const baseUrl = options.baseUrl || ctx.context.baseURL;
1158
- const subscriptionId = generateId();
1159
- let preapproval;
1160
- if (preapprovalPlanId) {
1161
- preapproval = await preApprovalClient.create({
1162
- body: {
1163
- preapproval_plan_id: preapprovalPlanId,
1164
- payer_email: session.user.email,
1165
- card_token_id: void 0,
1166
- // Will be provided in checkout
1167
- back_url: backUrl || `${baseUrl}/subscription/success`,
1168
- status: "pending",
1169
- external_reference: subscriptionId
1170
- }
1171
- });
1172
- } else if (autoRecurring) {
1173
- const ar = autoRecurring;
1174
- const autoRecurringBody = {
1175
- frequency: ar.frequency,
1176
- frequency_type: ar.frequencyType,
1177
- transaction_amount: ar.transactionAmount,
1178
- currency_id: ar.currencyId
1179
- };
1180
- if (ar.startDate) {
1181
- autoRecurringBody.start_date = ar.startDate;
1182
- }
1183
- if (ar.endDate) {
1184
- autoRecurringBody.end_date = ar.endDate;
1185
- }
1186
- if (ar.freeTrial) {
1187
- autoRecurringBody.free_trial = {
1188
- frequency: ar.freeTrial.frequency,
1189
- frequency_type: ar.freeTrial.frequencyType
1190
- };
1191
- }
1192
- preapproval = await preApprovalClient.create({
1193
- body: {
1194
- reason,
1195
- auto_recurring: autoRecurringBody,
1196
- payer_email: session.user.email,
1197
- back_url: backUrl || `${baseUrl}/subscription/success`,
1198
- status: "pending",
1199
- external_reference: subscriptionId
1200
- }
1201
- });
1202
- }
1203
- if (!preapproval) {
1204
- throw new APIError2("BAD_REQUEST", {
1205
- message: "Failed to create subscription"
1206
- });
1207
- }
1208
- const subscription = await ctx.context.adapter.create({
1209
- model: "mercadoPagoSubscription",
1210
- data: {
1211
- id: subscriptionId,
1212
- userId: session.user.id,
1213
- mercadoPagoSubscriptionId: preapproval.id,
1214
- planId: preapprovalPlanId || reason || "direct",
1215
- status: "pending",
1216
- metadata: JSON.stringify(metadata || {}),
1217
- createdAt: /* @__PURE__ */ new Date(),
1218
- updatedAt: /* @__PURE__ */ new Date()
1219
- }
1220
- });
1221
- return ctx.json({
1222
- checkoutUrl: preapproval.init_point,
1223
- subscription
1224
- });
1225
- }
1226
- ),
1227
- // Cancel subscription
1228
- cancelSubscription: createAuthEndpoint(
1229
- "/mercado-pago/subscription/cancel",
1230
- {
1231
- method: "POST",
1232
- requireAuth: true,
1233
- body: z.object({
1234
- subscriptionId: z.string()
1235
- })
1236
- },
1237
- async (ctx) => {
1238
- const session = ctx.context.session;
1239
- if (!session) {
1240
- throw new APIError2("UNAUTHORIZED");
1241
- }
1242
- const { subscriptionId } = ctx.body;
1243
- const subscription = await ctx.context.adapter.findOne({
1244
- model: "mercadoPagoSubscription",
1245
- where: [
1246
- { field: "id", value: subscriptionId },
1247
- { field: "userId", value: session.user.id }
1248
- ]
1249
- });
1250
- if (!subscription) {
1251
- throw new APIError2("NOT_FOUND", {
1252
- message: "Subscription not found"
1253
- });
1254
- }
1255
- await preApprovalClient.update({
1256
- id: subscription.mercadoPagoSubscriptionId,
1257
- body: { status: "cancelled" }
1258
- });
1259
- await ctx.context.adapter.update({
1260
- model: "mercadoPagoSubscription",
1261
- where: [{ field: "id", value: subscriptionId }],
1262
- update: {
1263
- status: "cancelled",
1264
- updatedAt: /* @__PURE__ */ new Date()
1265
- }
1266
- });
1267
- return ctx.json({ success: true });
1268
- }
1269
- ),
1270
- // Get payment status
1271
- getPayment: createAuthEndpoint(
1272
- "/mercado-pago/payment/:id",
1273
- {
1274
- method: "GET",
1275
- requireAuth: true
1276
- },
1277
- async (ctx) => {
1278
- const paymentId = ctx.params.id;
1279
- const session = ctx.context.session;
1280
- if (!session) {
1281
- throw new APIError2("UNAUTHORIZED");
1282
- }
1283
- const payment = await ctx.context.adapter.findOne({
1284
- model: "mercadoPagoPayment",
1285
- where: [
1286
- { field: "id", value: paymentId },
1287
- { field: "userId", value: session.user.id }
1288
- ]
1289
- });
1290
- if (!payment) {
1291
- throw new APIError2("NOT_FOUND", {
1292
- message: "Payment not found"
1293
- });
1294
- }
1295
- return ctx.json({ payment });
1296
- }
1297
- ),
1298
- // List user payments
1299
- listPayments: createAuthEndpoint(
1300
- "/mercado-pago/payments",
1301
- {
1302
- method: "GET",
1303
- requireAuth: true,
1304
- query: z.object({
1305
- limit: z.coerce.number().optional().default(10),
1306
- offset: z.coerce.number().optional().default(0)
1307
- })
1308
- },
1309
- async (ctx) => {
1310
- const session = ctx.context.session;
1311
- const { limit, offset } = ctx.query;
1312
- if (!session) {
1313
- throw new APIError2("UNAUTHORIZED");
1314
- }
1315
- const payments = await ctx.context.adapter.findMany({
1316
- model: "mercadoPagoPayment",
1317
- where: [{ field: "userId", value: session.user.id }],
1318
- limit,
1319
- offset
1320
- });
1321
- return ctx.json({ payments });
1322
- }
1323
- ),
1324
- // List user subscriptions
1325
- listSubscriptions: createAuthEndpoint(
1326
- "/mercado-pago/subscriptions",
1327
- {
1328
- method: "GET",
1329
- requireAuth: true
1330
- },
1331
- async (ctx) => {
1332
- const session = ctx.context.session;
1333
- if (!session) {
1334
- throw new APIError2("UNAUTHORIZED");
1335
- }
1336
- const subscriptions = await ctx.context.adapter.findMany({
1337
- model: "mercadoPagoSubscription",
1338
- where: [{ field: "userId", value: session.user.id }]
1339
- });
1340
- return ctx.json({ subscriptions });
1341
- }
1342
- ),
1343
- // Webhook handler
1344
- webhook: createAuthEndpoint(
1345
- "/mercado-pago/webhook",
1346
- {
1347
- method: "POST"
1348
- },
1349
- async (ctx) => {
1350
- const webhookRateLimitKey = "webhook:global";
1351
- if (!rateLimiter.check(webhookRateLimitKey, 1e3, 60 * 1e3)) {
1352
- throw new APIError2("TOO_MANY_REQUESTS", {
1353
- message: "Webhook rate limit exceeded"
1354
- });
1355
- }
1356
- let notification;
1357
- try {
1358
- notification = ctx.body;
1359
- } catch {
1360
- throw new APIError2("BAD_REQUEST", {
1361
- message: "Invalid JSON payload"
1362
- });
1363
- }
1364
- if (!notification.type || !isValidWebhookTopic(notification.type) || !notification.data?.id) {
1365
- ctx.context.logger.warn("Invalid webhook topic received", {
1366
- type: notification.type
1367
- });
1368
- return ctx.json({ received: true });
1369
- }
1370
- if (!ctx.request) {
1371
- throw new APIError2("BAD_REQUEST", {
1372
- message: "Missing request"
1373
- });
1374
- }
1375
- if (options.webhookSecret) {
1376
- const xSignature = ctx.request.headers.get("x-signature");
1377
- const xRequestId = ctx.request.headers.get("x-request-id");
1378
- const dataId = notification.data?.id?.toString();
1379
- if (!dataId) {
1380
- throw new APIError2("BAD_REQUEST", {
1381
- message: "Missing data.id in webhook payload"
1382
- });
1383
- }
1384
- const isValid = verifyWebhookSignature({
1385
- xSignature,
1386
- xRequestId,
1387
- dataId,
1388
- secret: options.webhookSecret
1389
- });
1390
- if (!isValid) {
1391
- ctx.context.logger.error("Invalid webhook signature", {
1392
- xSignature,
1393
- xRequestId,
1394
- dataId
1395
- });
1396
- throw new APIError2("UNAUTHORIZED", {
1397
- message: "Invalid webhook signature"
1398
- });
1399
- }
1400
- }
1401
- const webhookId = `webhook:${notification.data?.id}:${notification.type}`;
1402
- const alreadyProcessed = idempotencyStore.get(webhookId);
1403
- if (alreadyProcessed) {
1404
- ctx.context.logger.info("Webhook already processed", { webhookId });
1405
- return ctx.json({ received: true });
1406
- }
1407
- idempotencyStore.set(webhookId, true, 24 * 60 * 60 * 1e3);
1408
- try {
1409
- if (notification.type === "payment") {
1410
- const paymentId = notification.data.id;
1411
- if (!paymentId) {
1412
- throw new APIError2("BAD_REQUEST", {
1413
- message: "Missing payment ID"
1414
- });
1415
- }
1416
- let mpPayment;
1417
- try {
1418
- mpPayment = await paymentClient.get({
1419
- id: paymentId
1420
- });
1421
- } catch (error) {
1422
- ctx.context.logger.error("Failed to fetch payment from MP", {
1423
- paymentId,
1424
- error
1425
- });
1426
- throw new APIError2("BAD_REQUEST", {
1427
- message: "Failed to fetch payment details"
1428
- });
1429
- }
1430
- const existingPayment = await ctx.context.adapter.findOne({
1431
- model: "mercadoPagoPayment",
1432
- where: [
1433
- {
1434
- field: "mercadoPagoPaymentId",
1435
- value: paymentId.toString()
1436
- }
1437
- ]
1438
- });
1439
- if (existingPayment) {
1440
- if (!validatePaymentAmount(
1441
- existingPayment.amount,
1442
- mpPayment.transaction_amount || 0
1443
- )) {
1444
- ctx.context.logger.error("Payment amount mismatch", {
1445
- expected: existingPayment.amount,
1446
- received: mpPayment.transaction_amount
1447
- });
1448
- throw new APIError2("BAD_REQUEST", {
1449
- message: "Payment amount mismatch"
1450
- });
1451
- }
1452
- await ctx.context.adapter.update({
1453
- model: "mercadoPagoPayment",
1454
- where: [{ field: "id", value: existingPayment.id }],
1455
- update: {
1456
- status: mpPayment.status,
1457
- statusDetail: mpPayment.status_detail || void 0,
1458
- paymentMethodId: mpPayment.payment_method_id || void 0,
1459
- paymentTypeId: mpPayment.payment_type_id || void 0,
1460
- updatedAt: /* @__PURE__ */ new Date()
1461
- }
1462
- });
1463
- if (options.onPaymentUpdate) {
1464
- try {
1465
- await options.onPaymentUpdate({
1466
- payment: existingPayment,
1467
- status: mpPayment.status,
1468
- statusDetail: mpPayment.status_detail || "",
1469
- mpPayment
1470
- });
1471
- } catch (error) {
1472
- ctx.context.logger.error(
1473
- "Error in onPaymentUpdate callback",
1474
- { error }
1475
- );
1476
- }
1477
- }
1478
- }
1479
- }
1480
- if (notification.type === "subscription_preapproval" || notification.type === "subscription_preapproval_plan") {
1481
- const preapprovalId = notification.data.id;
1482
- if (!preapprovalId) {
1483
- throw new APIError2("BAD_REQUEST", {
1484
- message: "Missing preapproval ID"
1485
- });
1486
- }
1487
- let mpPreapproval;
1488
- try {
1489
- mpPreapproval = await preApprovalClient.get({
1490
- id: preapprovalId
1491
- });
1492
- } catch (error) {
1493
- ctx.context.logger.error(
1494
- "Failed to fetch preapproval from MP",
1495
- { preapprovalId, error }
1496
- );
1497
- throw new APIError2("BAD_REQUEST", {
1498
- message: "Failed to fetch subscription details"
1499
- });
1500
- }
1501
- const existingSubscription = await ctx.context.adapter.findOne({
1502
- model: "mercadoPagoSubscription",
1503
- where: [
1504
- {
1505
- field: "mercadoPagoSubscriptionId",
1506
- value: preapprovalId
1507
- }
1508
- ]
1509
- });
1510
- if (existingSubscription) {
1511
- await ctx.context.adapter.update({
1512
- model: "mercadoPagoSubscription",
1513
- where: [{ field: "id", value: existingSubscription.id }],
1514
- update: {
1515
- status: mpPreapproval.status,
1516
- reason: mpPreapproval.reason || void 0,
1517
- nextPaymentDate: mpPreapproval.next_payment_date ? new Date(mpPreapproval.next_payment_date) : void 0,
1518
- lastPaymentDate: mpPreapproval.last_modified ? new Date(mpPreapproval.last_modified) : void 0,
1519
- summarized: mpPreapproval.summarized ? JSON.stringify(mpPreapproval.summarized) : void 0,
1520
- updatedAt: /* @__PURE__ */ new Date()
1521
- }
1522
- });
1523
- if (options.onSubscriptionUpdate) {
1524
- try {
1525
- await options.onSubscriptionUpdate({
1526
- subscription: existingSubscription,
1527
- status: mpPreapproval.status,
1528
- reason: mpPreapproval.reason || "",
1529
- mpPreapproval
1530
- });
1531
- } catch (error) {
1532
- ctx.context.logger.error(
1533
- "Error in onSubscriptionUpdate callback",
1534
- { error }
1535
- );
1536
- }
1537
- }
1538
- }
1539
- }
1540
- if (notification.type === "subscription_authorized_payment" || notification.type === "authorized_payment") {
1541
- const paymentId = notification.data.id;
1542
- if (!paymentId) {
1543
- throw new APIError2("BAD_REQUEST", {
1544
- message: "Missing payment ID"
1545
- });
1546
- }
1547
- let mpPayment;
1548
- try {
1549
- mpPayment = await paymentClient.get({
1550
- id: paymentId
1551
- });
1552
- } catch (error) {
1553
- ctx.context.logger.error(
1554
- "Failed to fetch authorized payment from MP",
1555
- { paymentId, error }
1556
- );
1557
- throw new APIError2("BAD_REQUEST", {
1558
- message: "Failed to fetch payment details"
1559
- });
1560
- }
1561
- if (mpPayment.external_reference) {
1562
- const subscription = await ctx.context.adapter.findOne({
1563
- model: "mercadoPagoSubscription",
1564
- where: [
1565
- {
1566
- field: "id",
1567
- // External reference holds the local subscription ID
1568
- value: mpPayment.external_reference
1569
- }
1570
- ]
1571
- });
1572
- if (subscription) {
1573
- if (options.onSubscriptionPayment) {
1574
- try {
1575
- await options.onSubscriptionPayment({
1576
- subscription,
1577
- // In a real app, we should map this properly or align types
1578
- payment: mpPayment,
1579
- status: mpPayment.status
1580
- });
1581
- } catch (error) {
1582
- ctx.context.logger.error(
1583
- "Error in onSubscriptionPayment callback",
1584
- { error }
1585
- );
1586
- }
1587
- }
1588
- } else {
1589
- ctx.context.logger.warn(
1590
- "Subscription not found for authorized payment",
1591
- {
1592
- paymentId,
1593
- externalReference: mpPayment.external_reference
1594
- }
1595
- );
1596
- }
1597
- }
1598
- }
1599
- } catch (error) {
1600
- ctx.context.logger.error("Error processing webhook", {
1601
- error,
1602
- notification
1603
- });
1604
- if (error instanceof APIError2) {
1605
- throw error;
1606
- }
1607
- }
1608
- return ctx.json({ received: true });
1609
- }
1610
- )
1611
- },
1612
- // Add trusted origins from options
1613
- ...options.trustedOrigins && { trustedOrigins: options.trustedOrigins }
1614
- };
1615
- };
1616
- export {
1617
- mercadoPagoClientPlugin,
1618
- mercadoPagoPlugin
1619
- };
1620
- //# sourceMappingURL=index.mjs.map