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