@pandait.tech/payment-nuvei 0.1.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.
@@ -0,0 +1,1671 @@
1
+ 'use strict';
2
+
3
+ var server = require('next/server');
4
+ var firestore = require('firebase-admin/firestore');
5
+ var crypto = require('crypto');
6
+ var zod = require('zod');
7
+
8
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
+
10
+ var crypto__default = /*#__PURE__*/_interopDefault(crypto);
11
+
12
+ // src/handlers/charge.ts
13
+ var NUVEI_DOMAIN = "paymentez.com";
14
+ function getBaseUrl() {
15
+ const env = process.env.NUVEI_ENV === "prod" ? "prod" : "stg";
16
+ return env === "prod" ? `https://ccapi.${NUVEI_DOMAIN}` : `https://ccapi-stg.${NUVEI_DOMAIN}`;
17
+ }
18
+ function getServerCredentials() {
19
+ const appCode = process.env.NUVEI_SERVER_APP_CODE;
20
+ const appKey = process.env.NUVEI_SERVER_APP_KEY;
21
+ if (!appCode || !appKey) {
22
+ throw new Error("Nuvei server credentials not configured");
23
+ }
24
+ return { appCode, appKey };
25
+ }
26
+ function generateAuthToken() {
27
+ const { appCode, appKey } = getServerCredentials();
28
+ const timestamp = Math.floor(Date.now() / 1e3);
29
+ const uniqToken = crypto__default.default.createHash("sha256").update(`${appKey}${timestamp}`).digest("hex");
30
+ return Buffer.from(`${appCode};${timestamp};${uniqToken}`).toString(
31
+ "base64"
32
+ );
33
+ }
34
+ function getProxyUrl() {
35
+ return process.env.NUVEI_PROXY_URL || null;
36
+ }
37
+ async function nuveiRequest(path, method, body) {
38
+ const authToken = generateAuthToken();
39
+ const proxyUrl = getProxyUrl();
40
+ if (proxyUrl) {
41
+ console.log(`[nuvei] ${method} ${path} via proxy`);
42
+ const proxyResponse = await fetch(proxyUrl, {
43
+ method: "POST",
44
+ headers: {
45
+ "Content-Type": "application/json",
46
+ "x-nuvei-auth-token": authToken,
47
+ "x-nuvei-env": process.env.NUVEI_ENV || "stg"
48
+ },
49
+ body: JSON.stringify({ path, method, body })
50
+ });
51
+ if (!proxyResponse.ok) {
52
+ const errorBody = await proxyResponse.text();
53
+ console.error(`[nuvei] ${method} ${path} proxy failed with status ${proxyResponse.status}. Body: ${errorBody}`);
54
+ try {
55
+ return JSON.parse(errorBody);
56
+ } catch {
57
+ throw new Error(`Nuvei proxy ${method} ${path} failed: ${proxyResponse.status}`);
58
+ }
59
+ }
60
+ return proxyResponse.json();
61
+ }
62
+ const url = `${getBaseUrl()}${path}`;
63
+ const options = {
64
+ method,
65
+ headers: {
66
+ "Content-Type": "application/json",
67
+ "Auth-Token": authToken
68
+ }
69
+ };
70
+ if (body && method === "POST") {
71
+ options.body = JSON.stringify(body);
72
+ }
73
+ const response = await fetch(url, options);
74
+ const responseText = await response.text();
75
+ console.log(`[nuvei] ${method} ${path} \u2192 ${response.status} | body: ${responseText.slice(0, 300)}`);
76
+ if (!response.ok) {
77
+ console.error(`[nuvei] ${method} ${path} failed with status ${response.status}`);
78
+ try {
79
+ return JSON.parse(responseText);
80
+ } catch {
81
+ throw new Error(`Nuvei ${method} ${path} failed: ${response.status}`);
82
+ }
83
+ }
84
+ try {
85
+ return JSON.parse(responseText);
86
+ } catch {
87
+ throw new Error(`Nuvei ${method} ${path}: invalid JSON response`);
88
+ }
89
+ }
90
+ async function listCards(uid) {
91
+ return nuveiRequest(`/v2/card/list?uid=${encodeURIComponent(uid)}`, "GET");
92
+ }
93
+ async function deleteCard(token, uid) {
94
+ return nuveiRequest("/v2/card/delete/", "POST", {
95
+ card: { token },
96
+ user: { id: uid }
97
+ });
98
+ }
99
+ async function refundTransaction(transactionId) {
100
+ return nuveiRequest("/v2/transaction/refund/", "POST", {
101
+ transaction: { id: transactionId }
102
+ });
103
+ }
104
+ async function verifyCard(params) {
105
+ return nuveiRequest("/v2/transaction/verify/", "POST", {
106
+ user: {
107
+ id: params.userId
108
+ },
109
+ transaction: {
110
+ id: params.transactionReference
111
+ },
112
+ type: "BY_AMOUNT",
113
+ value: params.value
114
+ });
115
+ }
116
+ async function debitWithToken(params) {
117
+ const order = {
118
+ amount: params.amount,
119
+ description: params.description,
120
+ dev_reference: params.devReference,
121
+ vat: params.vat ?? 0,
122
+ taxable_amount: params.taxableAmount ?? params.amount - (params.vat ?? 0),
123
+ tax_percentage: 15
124
+ };
125
+ if (params.installments && params.installments > 0) {
126
+ order.installments = params.installments;
127
+ order.installments_type = params.installmentsType ?? 0;
128
+ }
129
+ const body = {
130
+ user: {
131
+ id: params.userId,
132
+ email: params.userEmail
133
+ },
134
+ order,
135
+ card: {
136
+ token: params.cardToken,
137
+ ...params.cvc ? { cvc: params.cvc } : {}
138
+ }
139
+ };
140
+ if (params.browserInfo && params.termUrl) {
141
+ body.extra_params = {
142
+ threeDS2_data: {
143
+ term_url: params.termUrl,
144
+ device_type: "browser"
145
+ },
146
+ browser_info: params.browserInfo
147
+ };
148
+ }
149
+ return nuveiRequest("/v2/transaction/debit/", "POST", body);
150
+ }
151
+ async function verifyThreeDS(params) {
152
+ const body = {
153
+ user: {
154
+ id: params.userId
155
+ },
156
+ transaction: { id: params.transactionId },
157
+ type: params.type,
158
+ value: params.value ?? "",
159
+ more_info: true
160
+ };
161
+ return nuveiRequest("/v2/transaction/verify/", "POST", body);
162
+ }
163
+
164
+ // src/handlers/charge.ts
165
+ var NUVEI_ERROR_MESSAGES = {
166
+ 0: "Error del procesador de pagos. Intenta de nuevo.",
167
+ // 1 = review/pending — handled separately, not shown as error
168
+ 2: "Error en la validaci\xF3n del banco. Verifica los datos de tu tarjeta.",
169
+ // 3 = success, not used here
170
+ 4: "Tarjeta rechazada por el banco. Intenta con otra tarjeta.",
171
+ 5: "Transacci\xF3n no permitida por el banco emisor.",
172
+ 6: "Error de comunicaci\xF3n con el banco. Intenta en unos minutos.",
173
+ 7: "Tarjeta reportada como perdida o robada. Contacta a tu banco.",
174
+ 8: "Tarjeta rechazada por seguridad antifraude.",
175
+ 9: "Transacci\xF3n denegada por el banco. Contacta a tu banco o intenta con otra tarjeta.",
176
+ 10: "La tarjeta no pudo ser procesada. Intenta con otra tarjeta.",
177
+ 11: "Transacci\xF3n rechazada por el sistema antifraude.",
178
+ 12: "Tarjeta en lista restringida. Contacta a tu banco.",
179
+ 13: "Tarjeta inv\xE1lida o deshabilitada. Contacta a tu banco.",
180
+ 14: "El monto excede el l\xEDmite permitido por tu tarjeta.",
181
+ 19: "Transacci\xF3n rechazada por filtro antifraude.",
182
+ 20: "Tarjeta vencida. Actualiza tu m\xE9todo de pago.",
183
+ 21: "C\xF3digo de seguridad (CVV) incorrecto.",
184
+ 22: "Tipo de tarjeta no soportado para esta transacci\xF3n.",
185
+ 23: "Transacci\xF3n rechazada. Intenta de nuevo m\xE1s tarde.",
186
+ 31: "Tu banco requiere verificaci\xF3n OTP. Ingresa el c\xF3digo enviado a tu tel\xE9fono.",
187
+ 36: "Tu banco requiere verificaci\xF3n adicional 3DS. Completa el proceso en la ventana emergente.",
188
+ 37: "Tu banco requiere verificaci\xF3n adicional 3DS. Completa el proceso en la ventana emergente."
189
+ };
190
+ var THREEDS_AUTH_MESSAGES = {
191
+ N: "Autenticaci\xF3n 3DS rechazada por tu banco. Intenta con otra tarjeta.",
192
+ R: "Tu banco rechaz\xF3 la autenticaci\xF3n 3DS.",
193
+ U: "No se pudo verificar la autenticaci\xF3n 3DS. Intenta de nuevo."
194
+ };
195
+ function getNuveiUserMessage(statusDetail, rawMessage) {
196
+ if (statusDetail !== void 0 && NUVEI_ERROR_MESSAGES[statusDetail]) {
197
+ return NUVEI_ERROR_MESSAGES[statusDetail];
198
+ }
199
+ if (rawMessage?.toLowerCase().includes("insufficient")) {
200
+ return NUVEI_ERROR_MESSAGES[9];
201
+ }
202
+ if (rawMessage?.toLowerCase().includes("expired")) {
203
+ return NUVEI_ERROR_MESSAGES[20];
204
+ }
205
+ if (rawMessage?.toLowerCase().includes("cvv")) {
206
+ return NUVEI_ERROR_MESSAGES[21];
207
+ }
208
+ return "No se pudo procesar el pago. Verifica los datos de tu tarjeta o intenta con otra.";
209
+ }
210
+ function createChargeHandler(deps) {
211
+ const logger = deps.logger ?? console;
212
+ const { db, auth } = deps.firebase;
213
+ return async function POST(request) {
214
+ try {
215
+ const sessionCookie = request.cookies.get("__session")?.value;
216
+ if (!sessionCookie || !auth) {
217
+ return server.NextResponse.json({ error: "No autorizado" }, { status: 401 });
218
+ }
219
+ let decodedToken;
220
+ try {
221
+ decodedToken = await auth.verifySessionCookie(sessionCookie, true);
222
+ } catch {
223
+ return server.NextResponse.json({ error: "Sesion invalida" }, { status: 401 });
224
+ }
225
+ const clientIpForLimit = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-real-ip") || "unknown";
226
+ if (deps.rateLimit) {
227
+ const allowed = await deps.rateLimit(
228
+ `charge:${clientIpForLimit}`,
229
+ 10,
230
+ 15 * 60 * 1e3
231
+ );
232
+ if (!allowed) {
233
+ return server.NextResponse.json(
234
+ { error: "Demasiados intentos. Intent\xE1 de nuevo en unos minutos." },
235
+ { status: 429 }
236
+ );
237
+ }
238
+ }
239
+ const body = await request.json();
240
+ if (deps.turnstile) {
241
+ const turnstileToken = body.turnstileToken;
242
+ if (!turnstileToken) {
243
+ return server.NextResponse.json(
244
+ { error: "Verificaci\xF3n de seguridad faltante. Recarg\xE1 la p\xE1gina." },
245
+ { status: 403 }
246
+ );
247
+ }
248
+ const turnstileResult = await deps.turnstile(
249
+ turnstileToken,
250
+ clientIpForLimit === "unknown" ? void 0 : clientIpForLimit
251
+ );
252
+ if (!turnstileResult.success) {
253
+ return server.NextResponse.json(
254
+ { error: "No pudimos verificar que sos humano. Recarg\xE1 la p\xE1gina." },
255
+ { status: 403 }
256
+ );
257
+ }
258
+ }
259
+ const {
260
+ token,
261
+ cvc,
262
+ orderId,
263
+ amount,
264
+ vat,
265
+ description,
266
+ userId,
267
+ userEmail,
268
+ browserInfo,
269
+ installments,
270
+ installmentsType
271
+ } = body;
272
+ logger.log("[charge] Request:", {
273
+ token: token?.substring(0, 10) + "...",
274
+ orderId,
275
+ amount,
276
+ userId: userId?.substring(0, 8) + "..."
277
+ });
278
+ if (!token || !orderId || !amount) {
279
+ return server.NextResponse.json({ error: "Datos incompletos" }, { status: 400 });
280
+ }
281
+ if (decodedToken.uid !== userId) {
282
+ return server.NextResponse.json({ error: "Usuario no coincide" }, { status: 403 });
283
+ }
284
+ const orderDoc = await db.collection("orders").doc(orderId).get();
285
+ if (!orderDoc.exists) {
286
+ return server.NextResponse.json({ error: "Orden no encontrada" }, { status: 404 });
287
+ }
288
+ const orderData = orderDoc.data() ?? {};
289
+ if (orderData.status !== "pending") {
290
+ return server.NextResponse.json(
291
+ { error: "Esta orden ya fue procesada" },
292
+ { status: 409 }
293
+ );
294
+ }
295
+ let runStandardValidation = true;
296
+ if (deps.validateCustomOrder) {
297
+ const customResult = await deps.validateCustomOrder({
298
+ orderId,
299
+ orderData,
300
+ amount
301
+ });
302
+ if (customResult.handled) {
303
+ if (!customResult.valid) {
304
+ return server.NextResponse.json(
305
+ { error: customResult.error },
306
+ { status: customResult.status }
307
+ );
308
+ }
309
+ runStandardValidation = false;
310
+ }
311
+ }
312
+ if (runStandardValidation) {
313
+ const orderItems = orderData.items ?? [];
314
+ let verifiedSubtotal = 0;
315
+ for (const item of orderItems) {
316
+ const productDoc = await db.collection("products").doc(item.productId).get();
317
+ if (!productDoc.exists || !productDoc.data()?.isActive) {
318
+ return server.NextResponse.json(
319
+ { error: `El producto "${item.name}" ya no est\xE1 disponible` },
320
+ { status: 409 }
321
+ );
322
+ }
323
+ const productData = productDoc.data();
324
+ const isDigital = productData.isDigital === true;
325
+ const stock = productData.stock ?? 0;
326
+ if (!isDigital && stock < item.quantity) {
327
+ return server.NextResponse.json(
328
+ {
329
+ error: `Stock insuficiente para "${item.name}" (disponible: ${stock})`
330
+ },
331
+ { status: 409 }
332
+ );
333
+ }
334
+ const display = deps.getPriceDisplay({
335
+ price: productData.price ?? 0,
336
+ autoDiscounts: productData.autoDiscounts
337
+ });
338
+ const expectedItemSubtotal = display.finalSubtotal;
339
+ if (Math.abs(expectedItemSubtotal - item.price) > 0.02) {
340
+ return server.NextResponse.json(
341
+ {
342
+ error: `El precio de "${item.name}" cambi\xF3. Recarg\xE1 la p\xE1gina para ver el precio actualizado.`
343
+ },
344
+ { status: 409 }
345
+ );
346
+ }
347
+ verifiedSubtotal += expectedItemSubtotal * item.quantity;
348
+ }
349
+ let verifiedDiscount = 0;
350
+ const orderDiscount = orderData.discount ?? 0;
351
+ const orderPromotionId = orderData.promotionId;
352
+ if (orderPromotionId && orderDiscount > 0) {
353
+ const promoDoc = await db.collection("promotions").doc(orderPromotionId).get();
354
+ if (!promoDoc.exists || !promoDoc.data()?.isActive) {
355
+ return server.NextResponse.json(
356
+ { error: "El cupon aplicado ya no es valido. Remuevelo y vuelve a intentar." },
357
+ { status: 409 }
358
+ );
359
+ }
360
+ const promo = promoDoc.data();
361
+ const now = /* @__PURE__ */ new Date();
362
+ const validFrom = promo.rules.validFrom.toDate ? promo.rules.validFrom.toDate() : new Date(promo.rules.validFrom);
363
+ const validUntil = promo.rules.validUntil.toDate ? promo.rules.validUntil.toDate() : new Date(promo.rules.validUntil);
364
+ if (now < validFrom || now > validUntil) {
365
+ return server.NextResponse.json(
366
+ { error: "El cupon aplicado ha expirado. Remuevelo y vuelve a intentar." },
367
+ { status: 409 }
368
+ );
369
+ }
370
+ if (promo.rules.maxTotalUses && promo.currentUses >= promo.rules.maxTotalUses) {
371
+ return server.NextResponse.json(
372
+ { error: "El cupon aplicado ya alcanzo su limite de usos." },
373
+ { status: 409 }
374
+ );
375
+ }
376
+ if (promo.type === "percentage") {
377
+ verifiedDiscount = verifiedSubtotal * (promo.value / 100);
378
+ if (promo.maxDiscountAmount) {
379
+ verifiedDiscount = Math.min(verifiedDiscount, promo.maxDiscountAmount);
380
+ }
381
+ } else if (promo.type === "fixed_amount") {
382
+ verifiedDiscount = Math.min(promo.value, verifiedSubtotal);
383
+ }
384
+ verifiedDiscount = Math.round(verifiedDiscount * 100) / 100;
385
+ }
386
+ const verifiedDiscountedSubtotal = Math.max(0, verifiedSubtotal - verifiedDiscount);
387
+ const verifiedVat = Math.round(verifiedDiscountedSubtotal * 0.15 * 100) / 100;
388
+ const verifiedTotal = Math.round((verifiedDiscountedSubtotal + verifiedVat) * 100) / 100;
389
+ if (verifiedTotal !== amount) {
390
+ return server.NextResponse.json(
391
+ {
392
+ error: "El monto no coincide con los precios actuales. Actualiza tu carrito."
393
+ },
394
+ { status: 409 }
395
+ );
396
+ }
397
+ }
398
+ const functionsBase = deps.cloudFunctionsBaseUrl ?? process.env.CLOUD_FUNCTIONS_BASE_URL;
399
+ const termUrl = functionsBase ? `${functionsBase}/threeDSCallback?orderId=${orderId}` : void 0;
400
+ if (browserInfo && !termUrl) {
401
+ logger.error(
402
+ "[charge] browserInfo provided but cloudFunctionsBaseUrl is not configured. 3DS challenges will not initiate. Configure ChargeHandlerDeps.cloudFunctionsBaseUrl (or CLOUD_FUNCTIONS_BASE_URL env var) pointing to your 3DS callback endpoint."
403
+ );
404
+ }
405
+ const clientIp = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-real-ip") || "127.0.0.1";
406
+ const enrichedBrowserInfo = browserInfo ? { ...browserInfo, ip_address: clientIp } : void 0;
407
+ const discountedSubtotalForTax = amount - (vat ?? 0);
408
+ const nuveiData = await debitWithToken({
409
+ userId,
410
+ userEmail,
411
+ amount,
412
+ description: description || deps.merchantName,
413
+ devReference: orderId,
414
+ cardToken: token,
415
+ ...cvc ? { cvc } : {},
416
+ vat: vat ?? 0,
417
+ taxableAmount: discountedSubtotalForTax,
418
+ ...installments ? { installments, installmentsType: installmentsType ?? 0 } : {},
419
+ ...enrichedBrowserInfo && termUrl ? { browserInfo: enrichedBrowserInfo, termUrl } : {}
420
+ });
421
+ logger.log("[charge] Nuvei debit response FULL:", JSON.stringify(nuveiData));
422
+ logger.log(
423
+ "[charge] Nuvei debit response summary:",
424
+ JSON.stringify({
425
+ status: nuveiData.transaction?.status,
426
+ status_detail: nuveiData.transaction?.status_detail,
427
+ id: nuveiData.transaction?.id,
428
+ "3ds_auth": nuveiData["3ds"]?.authentication?.status,
429
+ error: nuveiData.error
430
+ })
431
+ );
432
+ if (nuveiData.transaction && nuveiData.transaction.status === "success" && nuveiData.transaction.status_detail === 3) {
433
+ const rawAuthCode = nuveiData.transaction.authorization_code;
434
+ const hasValidAuthCode = typeof rawAuthCode === "string" && rawAuthCode.trim().length > 0 && rawAuthCode !== "null";
435
+ const batch = db.batch();
436
+ const orderRef = db.collection("orders").doc(orderId);
437
+ if (hasValidAuthCode) {
438
+ batch.update(orderRef, {
439
+ status: "paid",
440
+ paymentTransactionId: nuveiData.transaction.id,
441
+ authorizationCode: rawAuthCode,
442
+ ...installments ? { installments, installmentsType: installmentsType ?? 0 } : {},
443
+ chargeResponseAt: /* @__PURE__ */ new Date(),
444
+ updatedAt: /* @__PURE__ */ new Date()
445
+ });
446
+ } else {
447
+ logger.error(
448
+ `[AUDIT:MISSING_AUTH_CODE] orderId=${orderId} txId=${nuveiData.transaction.id} debitResponse=${JSON.stringify(nuveiData)}`
449
+ );
450
+ batch.update(orderRef, {
451
+ status: "paid",
452
+ paymentTransactionId: nuveiData.transaction.id,
453
+ missingAuthCodeFlagged: true,
454
+ missingAuthCodeLoggedAt: /* @__PURE__ */ new Date(),
455
+ emailPending: true,
456
+ ...installments ? { installments, installmentsType: installmentsType ?? 0 } : {},
457
+ chargeResponseAt: /* @__PURE__ */ new Date(),
458
+ updatedAt: /* @__PURE__ */ new Date()
459
+ });
460
+ }
461
+ const orderSnap = await orderRef.get();
462
+ const orderInfo = orderSnap.data() ?? {};
463
+ if (orderInfo.promotionId) {
464
+ const promoRef = db.collection("promotions").doc(orderInfo.promotionId);
465
+ batch.update(promoRef, {
466
+ currentUses: firestore.FieldValue.increment(1)
467
+ });
468
+ const usageRef = promoRef.collection("usages").doc();
469
+ batch.set(usageRef, {
470
+ userId: orderInfo.userId,
471
+ orderId,
472
+ discountApplied: orderInfo.discount || 0,
473
+ usedAt: /* @__PURE__ */ new Date()
474
+ });
475
+ }
476
+ await batch.commit();
477
+ if (deps.onPaymentSucceeded) {
478
+ try {
479
+ await deps.onPaymentSucceeded({ ...orderInfo, id: orderId });
480
+ } catch (err) {
481
+ logger.error(
482
+ `[charge] onPaymentSucceeded hook failed for order ${orderId}:`,
483
+ err
484
+ );
485
+ }
486
+ }
487
+ let emailSent = false;
488
+ if (hasValidAuthCode) {
489
+ const emailResult = await deps.email.sendPaymentConfirmation({
490
+ to: userEmail,
491
+ customerName: orderInfo.shippingAddress?.fullName || "",
492
+ orderId,
493
+ transactionId: nuveiData.transaction.id,
494
+ authorizationCode: rawAuthCode,
495
+ items: orderInfo.items || [],
496
+ subtotal: orderInfo.subtotal || amount,
497
+ discount: orderInfo.discount || void 0,
498
+ couponCode: orderInfo.couponCode,
499
+ vat: orderInfo.vat || vat,
500
+ total: amount,
501
+ ...orderInfo.postPurchaseNote ? { postPurchaseNote: orderInfo.postPurchaseNote } : {}
502
+ }).catch(() => ({ success: false }));
503
+ emailSent = emailResult.success;
504
+ if (emailSent) {
505
+ await orderRef.update({ emailSentAt: /* @__PURE__ */ new Date() });
506
+ }
507
+ }
508
+ if (body.deleteCardAfterPayment && token) {
509
+ deleteCard(token, userId).catch(
510
+ (err) => logger.error("[charge] Failed to delete card after payment:", err)
511
+ );
512
+ }
513
+ return server.NextResponse.json({
514
+ success: true,
515
+ transactionId: nuveiData.transaction.id,
516
+ authorizationCode: hasValidAuthCode ? rawAuthCode : null,
517
+ orderId,
518
+ emailSent
519
+ });
520
+ }
521
+ if (nuveiData.transaction?.status === "pending" && nuveiData.transaction?.status_detail === 1) {
522
+ await db.collection("orders").doc(orderId).update({
523
+ status: "processing",
524
+ paymentTransactionId: nuveiData.transaction.id || null,
525
+ chargeResponseAt: /* @__PURE__ */ new Date(),
526
+ updatedAt: /* @__PURE__ */ new Date()
527
+ });
528
+ return server.NextResponse.json({
529
+ review: true,
530
+ orderId,
531
+ transactionId: nuveiData.transaction.id
532
+ });
533
+ }
534
+ const persistDeleteOnPaid = body.deleteCardAfterPayment ? { deleteCardAfterPayment: true, paymentToken: token } : {};
535
+ if (nuveiData.transaction?.status_detail === 35) {
536
+ const threeDSData = nuveiData["3ds"];
537
+ const hiddenIframeHtml = threeDSData?.browser_response?.hidden_iframe || "";
538
+ await db.collection("orders").doc(orderId).update({
539
+ status: "3ds-pending",
540
+ isDeviceFingerprint: true,
541
+ nuveiTransactionId: nuveiData.transaction.id || null,
542
+ ...persistDeleteOnPaid,
543
+ updatedAt: /* @__PURE__ */ new Date()
544
+ });
545
+ return server.NextResponse.json({
546
+ challenge: true,
547
+ challengeHtml: hiddenIframeHtml,
548
+ isDeviceFingerprint: true,
549
+ orderId,
550
+ nuveiTransactionId: nuveiData.transaction.id,
551
+ statusDetail: 35
552
+ });
553
+ }
554
+ if (nuveiData.transaction?.status_detail === 31) {
555
+ await db.collection("orders").doc(orderId).update({
556
+ status: "otp-pending",
557
+ nuveiTransactionId: nuveiData.transaction.id || null,
558
+ ...persistDeleteOnPaid,
559
+ updatedAt: /* @__PURE__ */ new Date()
560
+ });
561
+ return server.NextResponse.json({
562
+ otpRequired: true,
563
+ orderId,
564
+ nuveiTransactionId: nuveiData.transaction.id,
565
+ statusDetail: 31
566
+ });
567
+ }
568
+ if (nuveiData.transaction?.status_detail === 36 || nuveiData.transaction?.status_detail === 37) {
569
+ const threeDSData = nuveiData["3ds"];
570
+ const challengeHtml = threeDSData?.browser_response?.challenge_request || threeDSData?.browser_response?.hidden_iframe || "";
571
+ const rawDebit = nuveiData;
572
+ const debitCres = rawDebit["3ds"]?.authentication?.["cres"] || rawDebit["3ds"]?.browser_response?.["cres"] || rawDebit["3ds"]?.["cres"] || rawDebit.transaction?.["cres"] || rawDebit["cres"] || rawDebit["value"] || null;
573
+ logger.log(
574
+ `[charge] status ${nuveiData.transaction.status_detail} \u2014 debitCres captured: ${debitCres ? "YES (len=" + debitCres.length + ")" : "NO"}`
575
+ );
576
+ if (challengeHtml) {
577
+ await db.collection("orders").doc(orderId).update({
578
+ status: "3ds-pending",
579
+ nuveiTransactionId: nuveiData.transaction?.id || null,
580
+ ...debitCres ? { threeDSCres: debitCres } : {},
581
+ ...persistDeleteOnPaid,
582
+ updatedAt: /* @__PURE__ */ new Date()
583
+ });
584
+ return server.NextResponse.json({
585
+ challenge: true,
586
+ challengeHtml,
587
+ isDeviceFingerprint: false,
588
+ orderId,
589
+ nuveiTransactionId: nuveiData.transaction?.id,
590
+ statusDetail: nuveiData.transaction?.status_detail
591
+ });
592
+ }
593
+ }
594
+ const threeDSStatus = nuveiData["3ds"]?.authentication?.status;
595
+ if (threeDSStatus && THREEDS_AUTH_MESSAGES[threeDSStatus]) {
596
+ const threeDSErrorMsg = THREEDS_AUTH_MESSAGES[threeDSStatus];
597
+ const currentOrder2 = await db.collection("orders").doc(orderId).get();
598
+ const currentOrderData2 = currentOrder2.data() ?? {};
599
+ if (currentOrderData2.status === "pending") {
600
+ await db.collection("orders").doc(orderId).update({
601
+ status: "failed",
602
+ chargeResponseAt: /* @__PURE__ */ new Date(),
603
+ updatedAt: /* @__PURE__ */ new Date()
604
+ });
605
+ }
606
+ if (userEmail) {
607
+ const retryUrl = deps.getRetryUrl?.({ ...currentOrderData2, id: orderId });
608
+ deps.email.sendPaymentFailed({
609
+ to: userEmail,
610
+ customerName: currentOrderData2.shippingAddress?.fullName || "",
611
+ orderId,
612
+ errorMessage: threeDSErrorMsg,
613
+ items: currentOrderData2.items || [],
614
+ total: currentOrderData2.total || amount,
615
+ ...retryUrl ? { retryUrl } : {}
616
+ }).catch(() => {
617
+ });
618
+ }
619
+ return server.NextResponse.json({ error: threeDSErrorMsg }, { status: 400 });
620
+ }
621
+ const failedErrorMsg = getNuveiUserMessage(
622
+ nuveiData.transaction?.status_detail,
623
+ nuveiData.transaction?.message || nuveiData.error?.description
624
+ );
625
+ const currentOrder = await db.collection("orders").doc(orderId).get();
626
+ const currentOrderData = currentOrder.data() ?? {};
627
+ if (currentOrderData.status === "pending") {
628
+ await db.collection("orders").doc(orderId).update({
629
+ status: "failed",
630
+ chargeResponseAt: /* @__PURE__ */ new Date(),
631
+ updatedAt: /* @__PURE__ */ new Date()
632
+ });
633
+ }
634
+ if (userEmail) {
635
+ const retryUrl = deps.getRetryUrl?.({ ...currentOrderData, id: orderId });
636
+ deps.email.sendPaymentFailed({
637
+ to: userEmail,
638
+ customerName: currentOrderData.shippingAddress?.fullName || "",
639
+ orderId,
640
+ errorMessage: failedErrorMsg,
641
+ items: currentOrderData.items || [],
642
+ total: currentOrderData.total || amount,
643
+ ...retryUrl ? { retryUrl } : {}
644
+ }).catch(() => {
645
+ });
646
+ }
647
+ return server.NextResponse.json({ error: failedErrorMsg }, { status: 400 });
648
+ } catch (error) {
649
+ logger.error("Payment charge error:", error);
650
+ return server.NextResponse.json(
651
+ { error: "Error interno del servidor" },
652
+ { status: 500 }
653
+ );
654
+ }
655
+ };
656
+ }
657
+ function extractCres(payload) {
658
+ if (!payload || typeof payload !== "object") return null;
659
+ const p = payload;
660
+ const candidates = [
661
+ p["cres"],
662
+ p["CRes"],
663
+ p["value"],
664
+ p["3ds"]?.authentication?.["cres"],
665
+ p["3ds"]?.browser_response?.["cres"],
666
+ p["3ds"]?.["cres"],
667
+ p.transaction?.["cres"],
668
+ p.transaction?.["3ds"]?.authentication?.["cres"]
669
+ ];
670
+ for (const c of candidates) {
671
+ if (typeof c === "string" && c.trim().length > 0) return c;
672
+ }
673
+ return null;
674
+ }
675
+ function createWebhookHandler(deps) {
676
+ const logger = deps.logger ?? console;
677
+ const { db } = deps.firebase;
678
+ return async function POST(request) {
679
+ try {
680
+ const payload = await request.json();
681
+ logger.log("[webhook] Full payload:", JSON.stringify(payload));
682
+ const { transaction } = payload;
683
+ if (!transaction?.id || !transaction?.dev_reference) {
684
+ return server.NextResponse.json({ error: "Payload inv\xE1lido" }, { status: 400 });
685
+ }
686
+ const incomingCres = extractCres(payload);
687
+ if (incomingCres) {
688
+ logger.log(
689
+ `[webhook] CRES present in webhook payload (len=${incomingCres.length}) for txId=${transaction.id}`
690
+ );
691
+ }
692
+ const devRef = transaction.dev_reference;
693
+ if (deps.handleCustomDevReference) {
694
+ const customResponse = await deps.handleCustomDevReference(
695
+ devRef,
696
+ transaction,
697
+ payload
698
+ );
699
+ if (customResponse) return customResponse;
700
+ }
701
+ const orderId = devRef;
702
+ const orderRef = db.collection("orders").doc(orderId);
703
+ const orderDoc = await orderRef.get();
704
+ if (!orderDoc.exists) {
705
+ logger.error(`Webhook: Order ${orderId} not found`);
706
+ return server.NextResponse.json({ error: "Orden no encontrada" }, { status: 404 });
707
+ }
708
+ const orderDataEarly = orderDoc.data() ?? {};
709
+ if (incomingCres && orderDataEarly.status === "3ds-pending" && !orderDataEarly.verifyCalledAt) {
710
+ try {
711
+ await db.runTransaction(async (tx) => {
712
+ const doc = await tx.get(orderRef);
713
+ if (doc.data()?.verifyCalledAt) throw new Error("ALREADY_CALLED");
714
+ tx.update(orderRef, {
715
+ verifyCalledAt: /* @__PURE__ */ new Date(),
716
+ threeDSCres: incomingCres
717
+ });
718
+ });
719
+ logger.log(`[webhook] Calling verify BY_CRES for order ${orderId}`);
720
+ const verifyResult = await verifyThreeDS({
721
+ transactionId: orderDataEarly.nuveiTransactionId || transaction.id,
722
+ userId: orderDataEarly.userId,
723
+ type: "BY_CRES",
724
+ value: incomingCres
725
+ });
726
+ logger.log(`[webhook] verify response: ${JSON.stringify(verifyResult)}`);
727
+ const raw = verifyResult;
728
+ const vStatus = verifyResult.transaction?.status ?? raw["status"];
729
+ const vDetail = verifyResult.transaction?.status_detail ?? raw["status_detail"];
730
+ const vAuthCode = verifyResult.transaction?.authorization_code ?? raw["authorization_code"] ?? null;
731
+ const vTxId = verifyResult.transaction?.id ?? raw["transaction_id"] ?? orderDataEarly.nuveiTransactionId ?? transaction.id;
732
+ const verifySuccess = (vStatus === "success" || vStatus === 1) && vDetail === 3;
733
+ if (verifySuccess) {
734
+ const hasAuth = typeof vAuthCode === "string" && vAuthCode.trim().length > 0 && vAuthCode !== "null";
735
+ const batch = db.batch();
736
+ batch.update(orderRef, {
737
+ status: "paid",
738
+ paymentTransactionId: vTxId,
739
+ ...hasAuth ? { authorizationCode: vAuthCode } : {
740
+ missingAuthCodeFlagged: true,
741
+ missingAuthCodeLoggedAt: /* @__PURE__ */ new Date(),
742
+ emailPending: true
743
+ },
744
+ chargeResponseAt: /* @__PURE__ */ new Date(),
745
+ updatedAt: /* @__PURE__ */ new Date(),
746
+ threeDSCres: firestore.FieldValue.delete(),
747
+ threeDSTransStatus: firestore.FieldValue.delete()
748
+ });
749
+ if (orderDataEarly.promotionId) {
750
+ const promoRef = db.collection("promotions").doc(orderDataEarly.promotionId);
751
+ batch.update(promoRef, {
752
+ currentUses: firestore.FieldValue.increment(1)
753
+ });
754
+ const usageRef = promoRef.collection("usages").doc();
755
+ batch.set(usageRef, {
756
+ userId: orderDataEarly.userId,
757
+ orderId,
758
+ discountApplied: orderDataEarly.discount || 0,
759
+ usedAt: /* @__PURE__ */ new Date()
760
+ });
761
+ }
762
+ await batch.commit();
763
+ if (hasAuth && orderDataEarly.userEmail) {
764
+ await deps.email.sendPaymentConfirmation({
765
+ to: orderDataEarly.userEmail,
766
+ customerName: orderDataEarly.shippingAddress?.fullName || "",
767
+ orderId,
768
+ transactionId: vTxId,
769
+ authorizationCode: vAuthCode,
770
+ items: orderDataEarly.items || [],
771
+ subtotal: orderDataEarly.subtotal || orderDataEarly.total || 0,
772
+ discount: orderDataEarly.discount || void 0,
773
+ couponCode: orderDataEarly.couponCode,
774
+ vat: orderDataEarly.vat || 0,
775
+ total: orderDataEarly.total || 0
776
+ }).catch(() => ({ success: false }));
777
+ await orderRef.update({ emailSentAt: /* @__PURE__ */ new Date() });
778
+ }
779
+ if (orderDataEarly.deleteCardAfterPayment && orderDataEarly.paymentToken) {
780
+ deleteCard(
781
+ orderDataEarly.paymentToken,
782
+ orderDataEarly.userId
783
+ ).catch(
784
+ (err) => logger.error(
785
+ "[webhook] Failed to delete card after payment:",
786
+ err
787
+ )
788
+ );
789
+ }
790
+ if (deps.onPaymentSucceeded) {
791
+ try {
792
+ await deps.onPaymentSucceeded({ ...orderDataEarly, id: orderId });
793
+ } catch (err) {
794
+ logger.error(
795
+ `[webhook] onPaymentSucceeded hook failed for ${orderId}:`,
796
+ err
797
+ );
798
+ }
799
+ }
800
+ return server.NextResponse.json({ received: true, verifiedFromWebhook: true });
801
+ }
802
+ } catch (err) {
803
+ if (err.message !== "ALREADY_CALLED") {
804
+ logger.error(
805
+ `[webhook] Failed to verify BY_CRES for order ${orderId}:`,
806
+ err
807
+ );
808
+ }
809
+ }
810
+ }
811
+ const isApproved = transaction.status === "success" && transaction.status_detail === 3;
812
+ const currentStatus = orderDoc.data()?.status;
813
+ const finalStatuses = ["paid", "delivered", "shipped"];
814
+ const shouldUpdateStatus = isApproved || !finalStatuses.includes(currentStatus);
815
+ const orderData = orderDoc.data() ?? {};
816
+ const webhookAuthCode = transaction.authorization_code;
817
+ const hasValidAuthCode = typeof webhookAuthCode === "string" && webhookAuthCode.trim().length > 0 && webhookAuthCode !== "null";
818
+ const updatePayload = {
819
+ ...shouldUpdateStatus && {
820
+ status: isApproved ? "paid" : "cancelled"
821
+ },
822
+ paymentTransactionId: transaction.id,
823
+ authorizationCode: webhookAuthCode || null,
824
+ webhookStatus: transaction.status,
825
+ webhookStatusDetail: transaction.status_detail,
826
+ webhookReceivedAt: /* @__PURE__ */ new Date(),
827
+ updatedAt: /* @__PURE__ */ new Date()
828
+ };
829
+ const shouldSendPendingEmail = isApproved && orderData.emailPending === true && hasValidAuthCode && orderData.userEmail;
830
+ if (shouldSendPendingEmail) {
831
+ updatePayload.emailPending = firestore.FieldValue.delete();
832
+ updatePayload.missingAuthCodeFlagged = firestore.FieldValue.delete();
833
+ updatePayload.emailSentAt = /* @__PURE__ */ new Date();
834
+ }
835
+ await orderRef.update(updatePayload);
836
+ if (shouldSendPendingEmail) {
837
+ try {
838
+ await deps.email.sendPaymentConfirmation({
839
+ to: orderData.userEmail,
840
+ customerName: orderData.shippingAddress?.fullName || "",
841
+ orderId,
842
+ transactionId: transaction.id,
843
+ authorizationCode: webhookAuthCode,
844
+ items: orderData.items || [],
845
+ subtotal: orderData.subtotal || orderData.total || 0,
846
+ discount: orderData.discount || void 0,
847
+ couponCode: orderData.couponCode,
848
+ vat: orderData.vat || 0,
849
+ total: orderData.total || 0
850
+ });
851
+ logger.log(`[webhook] Pending confirmation email sent for order ${orderId}`);
852
+ } catch (err) {
853
+ logger.error(`[webhook] Failed to send pending confirmation email:`, err);
854
+ }
855
+ } else if (isApproved && orderData.emailPending === true && !hasValidAuthCode && orderData.userEmail) {
856
+ logger.error(
857
+ `[AUDIT:EMAIL_SENT_WITHOUT_AUTH_CODE] orderId=${orderId} txId=${transaction.id}`
858
+ );
859
+ try {
860
+ await deps.email.sendPaymentPending({
861
+ to: orderData.userEmail,
862
+ customerName: orderData.shippingAddress?.fullName || "",
863
+ orderId,
864
+ transactionId: transaction.id,
865
+ items: orderData.items || [],
866
+ subtotal: orderData.subtotal || orderData.total || 0,
867
+ discount: orderData.discount || void 0,
868
+ couponCode: orderData.couponCode,
869
+ vat: orderData.vat || 0,
870
+ total: orderData.total || 0
871
+ });
872
+ await orderRef.update({
873
+ emailPending: firestore.FieldValue.delete(),
874
+ emailSentAt: /* @__PURE__ */ new Date()
875
+ });
876
+ } catch (err) {
877
+ logger.error(`[webhook] Failed to send pending-status email:`, err);
878
+ }
879
+ }
880
+ const newStatus = shouldUpdateStatus ? isApproved ? "paid" : "cancelled" : currentStatus;
881
+ logger.log(
882
+ `Webhook: Order ${orderId} \u2192 ${newStatus} (status_detail: ${transaction.status_detail})`
883
+ );
884
+ if (newStatus === "paid" && deps.onPaymentSucceeded) {
885
+ try {
886
+ await deps.onPaymentSucceeded({ ...orderData, id: orderId });
887
+ } catch (err) {
888
+ logger.error(
889
+ `[webhook] onPaymentSucceeded hook failed for ${orderId}:`,
890
+ err
891
+ );
892
+ }
893
+ }
894
+ return server.NextResponse.json({ received: true });
895
+ } catch (error) {
896
+ logger.error("Webhook processing error:", error);
897
+ return server.NextResponse.json(
898
+ { error: "Error procesando webhook" },
899
+ { status: 500 }
900
+ );
901
+ }
902
+ };
903
+ }
904
+ function buildCompletionPage(orderId, transStatus) {
905
+ const html = `<!DOCTYPE html>
906
+ <html><head><meta charset="utf-8"><title>3DS Verification</title></head>
907
+ <body>
908
+ <script>
909
+ (function() {
910
+ var message = {
911
+ type: "3DS_COMPLETE",
912
+ orderId: "${orderId}",
913
+ transStatus: "${transStatus}"
914
+ };
915
+ try {
916
+ if (window.parent && window.parent !== window) {
917
+ window.parent.postMessage(message, "*");
918
+ } else if (window.opener) {
919
+ window.opener.postMessage(message, "*");
920
+ }
921
+ } catch(e) {}
922
+ })();
923
+ </script>
924
+ <p style="font-family:sans-serif;color:#666;text-align:center;margin-top:40px">
925
+ Verificando autenticaci&oacute;n...
926
+ </p>
927
+ </body></html>`;
928
+ return new server.NextResponse(html, {
929
+ status: 200,
930
+ headers: { "Content-Type": "text/html; charset=utf-8" }
931
+ });
932
+ }
933
+ function create3dsCallbackHandler(deps) {
934
+ const logger = deps.logger ?? console;
935
+ const { db } = deps.firebase;
936
+ async function storeCres(orderId, cres) {
937
+ if (orderId && cres) {
938
+ try {
939
+ await db.collection("orders").doc(orderId).update({
940
+ threeDSCres: cres,
941
+ updatedAt: /* @__PURE__ */ new Date()
942
+ });
943
+ } catch (err) {
944
+ logger.error("Failed to store cres on order:", err);
945
+ }
946
+ }
947
+ }
948
+ const POST = async function POST2(request) {
949
+ const { searchParams } = new URL(request.url);
950
+ const orderId = searchParams.get("orderId") || "";
951
+ let transStatus = "U";
952
+ let cres = "";
953
+ try {
954
+ const contentType = request.headers.get("content-type") || "";
955
+ if (contentType.includes("application/x-www-form-urlencoded")) {
956
+ const text = await request.text();
957
+ const params = new URLSearchParams(text);
958
+ transStatus = params.get("transStatus") || "U";
959
+ cres = params.get("cres") || params.get("CRes") || "";
960
+ } else {
961
+ const body = await request.json();
962
+ transStatus = body.transStatus || "U";
963
+ cres = body.cres || body.CRes || "";
964
+ }
965
+ } catch {
966
+ }
967
+ await storeCres(orderId, cres);
968
+ return buildCompletionPage(orderId, transStatus);
969
+ };
970
+ const GET = async function GET2(request) {
971
+ const { searchParams } = new URL(request.url);
972
+ const orderId = searchParams.get("orderId") || "";
973
+ const transStatus = searchParams.get("transStatus") || "Y";
974
+ const cres = searchParams.get("cres") || searchParams.get("CRes") || "";
975
+ await storeCres(orderId, cres);
976
+ return buildCompletionPage(orderId, transStatus);
977
+ };
978
+ return { POST, GET };
979
+ }
980
+ var ThreeDSCompleteSchema = zod.z.object({
981
+ orderId: zod.z.string().min(1),
982
+ userId: zod.z.string().min(1),
983
+ type: zod.z.enum(["AUTHENTICATION_CONTINUE", "BY_CRES", "BY_OTP"]),
984
+ nuveiTransactionId: zod.z.string().optional(),
985
+ otpCode: zod.z.string().optional()
986
+ });
987
+ function create3dsCompleteHandler(deps) {
988
+ const logger = deps.logger ?? console;
989
+ const { db, auth } = deps.firebase;
990
+ return async function POST(request) {
991
+ try {
992
+ const sessionCookie = request.cookies.get("__session")?.value;
993
+ if (!sessionCookie || !auth) {
994
+ return server.NextResponse.json({ error: "No autorizado" }, { status: 401 });
995
+ }
996
+ let decodedToken;
997
+ try {
998
+ decodedToken = await auth.verifySessionCookie(sessionCookie, true);
999
+ } catch {
1000
+ return server.NextResponse.json({ error: "Sesion invalida" }, { status: 401 });
1001
+ }
1002
+ const rawBody = await request.json();
1003
+ const parsed = ThreeDSCompleteSchema.safeParse(rawBody);
1004
+ if (!parsed.success) {
1005
+ return server.NextResponse.json({ error: "Datos incompletos" }, { status: 400 });
1006
+ }
1007
+ const { orderId, userId, type, nuveiTransactionId: bodyTxId, otpCode } = parsed.data;
1008
+ if (decodedToken.uid !== userId) {
1009
+ return server.NextResponse.json({ error: "Usuario no coincide" }, { status: 403 });
1010
+ }
1011
+ const orderDoc = await db.collection("orders").doc(orderId).get();
1012
+ if (!orderDoc.exists) {
1013
+ return server.NextResponse.json({ error: "Orden no encontrada" }, { status: 404 });
1014
+ }
1015
+ const orderData = orderDoc.data() ?? {};
1016
+ if (orderData.status === "paid") {
1017
+ return server.NextResponse.json({
1018
+ success: true,
1019
+ transactionId: orderData.paymentTransactionId,
1020
+ authorizationCode: orderData.authorizationCode || null,
1021
+ orderId
1022
+ });
1023
+ }
1024
+ if (orderData.verifyCalledAt) {
1025
+ logger.log(
1026
+ `[3ds-complete] verify already called for order ${orderId}, returning current state`
1027
+ );
1028
+ return server.NextResponse.json({ error: "Pago ya procesado" }, { status: 409 });
1029
+ }
1030
+ if (orderData.status !== "3ds-pending" && orderData.status !== "otp-pending") {
1031
+ return server.NextResponse.json(
1032
+ { error: "Esta orden ya fue procesada" },
1033
+ { status: 409 }
1034
+ );
1035
+ }
1036
+ const storedTransStatus = orderData.threeDSTransStatus;
1037
+ if (storedTransStatus && storedTransStatus !== "Y" && storedTransStatus !== "A") {
1038
+ await db.collection("orders").doc(orderId).update({
1039
+ status: "failed",
1040
+ chargeResponseAt: /* @__PURE__ */ new Date(),
1041
+ updatedAt: /* @__PURE__ */ new Date()
1042
+ });
1043
+ const msg = storedTransStatus === "N" ? "Autenticaci\xF3n 3DS rechazada por tu banco." : storedTransStatus === "R" ? "Tu banco rechaz\xF3 la autenticaci\xF3n 3DS." : "No se pudo verificar la autenticaci\xF3n 3DS.";
1044
+ const userEmail2 = orderData.userEmail || decodedToken.email || "";
1045
+ if (userEmail2) {
1046
+ const retryUrl = deps.getRetryUrl?.({ ...orderData, id: orderId });
1047
+ deps.email.sendPaymentFailed({
1048
+ to: userEmail2,
1049
+ customerName: orderData.shippingAddress?.fullName || "",
1050
+ orderId,
1051
+ errorMessage: msg,
1052
+ items: orderData.items || [],
1053
+ total: orderData.total || 0,
1054
+ ...retryUrl ? { retryUrl } : {}
1055
+ }).catch(() => {
1056
+ });
1057
+ }
1058
+ return server.NextResponse.json({ error: msg }, { status: 400 });
1059
+ }
1060
+ const transactionId = orderData.nuveiTransactionId || bodyTxId;
1061
+ if (!transactionId) {
1062
+ return server.NextResponse.json(
1063
+ { error: "No se encontr\xF3 el ID de transacci\xF3n para verificar" },
1064
+ { status: 400 }
1065
+ );
1066
+ }
1067
+ if (type === "AUTHENTICATION_CONTINUE" && !orderData.threeDSCres && !orderData.isDeviceFingerprint) {
1068
+ logger.log(`[3ds-complete] No CRES yet for order ${orderId} \u2014 still pending`);
1069
+ return server.NextResponse.json({ stillPending: true });
1070
+ }
1071
+ const actualType = type === "AUTHENTICATION_CONTINUE" && orderData.threeDSCres ? "BY_CRES" : type;
1072
+ const cresValue = actualType === "BY_CRES" ? orderData.threeDSCres : void 0;
1073
+ if (actualType === "BY_CRES" && !cresValue) {
1074
+ return server.NextResponse.json(
1075
+ { error: "No se encontr\xF3 el valor de autenticaci\xF3n 3DS (cres)" },
1076
+ { status: 400 }
1077
+ );
1078
+ }
1079
+ if (type === "BY_OTP" && !otpCode) {
1080
+ return server.NextResponse.json(
1081
+ { error: "Debes ingresar el c\xF3digo OTP" },
1082
+ { status: 400 }
1083
+ );
1084
+ }
1085
+ const verifyValue = type === "BY_OTP" ? otpCode : cresValue;
1086
+ const orderRef = db.collection("orders").doc(orderId);
1087
+ await orderRef.update({ lastVerifyAttemptAt: /* @__PURE__ */ new Date() });
1088
+ logger.log(
1089
+ `[3ds-complete] BEFORE verify: orderId=${orderId}, type=${actualType}, hasValue=${!!verifyValue}`
1090
+ );
1091
+ const verifyResult = await verifyThreeDS({
1092
+ transactionId,
1093
+ userId,
1094
+ type: actualType,
1095
+ value: verifyValue
1096
+ });
1097
+ logger.log(
1098
+ `[3ds-complete] AFTER verify: authCode=${verifyResult.transaction?.authorization_code ?? "MISSING"} status=${verifyResult.transaction?.status} detail=${verifyResult.transaction?.status_detail}`
1099
+ );
1100
+ const raw = verifyResult;
1101
+ const txStatus = verifyResult.transaction?.status ?? raw["status"];
1102
+ const txStatusDetail = verifyResult.transaction?.status_detail ?? raw["status_detail"];
1103
+ const txId = verifyResult.transaction?.id ?? raw["transaction_id"] ?? transactionId;
1104
+ const txAuthCode = verifyResult.transaction?.authorization_code ?? raw["authorization_code"] ?? null;
1105
+ const isSuccess = (txStatus === "success" || txStatus === 1) && txStatusDetail === 3;
1106
+ const isStillPending = !isSuccess && (txStatusDetail === 36 || txStatusDetail === 37 || txStatus === "pending");
1107
+ if (isStillPending) {
1108
+ logger.log(
1109
+ `[3ds-complete] Still pending: txStatus=${txStatus} detail=${txStatusDetail}. Polling continues.`
1110
+ );
1111
+ return server.NextResponse.json({ stillPending: true });
1112
+ }
1113
+ if (isSuccess) {
1114
+ const hasValidAuthCode = typeof txAuthCode === "string" && txAuthCode.trim().length > 0 && txAuthCode !== "null";
1115
+ const batch = db.batch();
1116
+ if (hasValidAuthCode) {
1117
+ batch.update(orderRef, {
1118
+ status: "paid",
1119
+ paymentTransactionId: txId,
1120
+ authorizationCode: txAuthCode,
1121
+ chargeResponseAt: /* @__PURE__ */ new Date(),
1122
+ updatedAt: /* @__PURE__ */ new Date(),
1123
+ threeDSCres: firestore.FieldValue.delete(),
1124
+ threeDSTransStatus: firestore.FieldValue.delete(),
1125
+ isDeviceFingerprint: firestore.FieldValue.delete()
1126
+ });
1127
+ } else {
1128
+ logger.error(
1129
+ `[AUDIT:MISSING_AUTH_CODE] orderId=${orderId} txId=${txId} verifyResponse=${JSON.stringify(verifyResult)}`
1130
+ );
1131
+ batch.update(orderRef, {
1132
+ status: "paid",
1133
+ paymentTransactionId: txId,
1134
+ missingAuthCodeFlagged: true,
1135
+ missingAuthCodeLoggedAt: /* @__PURE__ */ new Date(),
1136
+ emailPending: true,
1137
+ chargeResponseAt: /* @__PURE__ */ new Date(),
1138
+ updatedAt: /* @__PURE__ */ new Date(),
1139
+ threeDSCres: firestore.FieldValue.delete(),
1140
+ threeDSTransStatus: firestore.FieldValue.delete(),
1141
+ isDeviceFingerprint: firestore.FieldValue.delete()
1142
+ });
1143
+ }
1144
+ if (orderData.promotionId) {
1145
+ const promoRef = db.collection("promotions").doc(orderData.promotionId);
1146
+ batch.update(promoRef, {
1147
+ currentUses: firestore.FieldValue.increment(1)
1148
+ });
1149
+ const usageRef = promoRef.collection("usages").doc();
1150
+ batch.set(usageRef, {
1151
+ userId: orderData.userId,
1152
+ orderId,
1153
+ discountApplied: orderData.discount || 0,
1154
+ usedAt: /* @__PURE__ */ new Date()
1155
+ });
1156
+ }
1157
+ await batch.commit();
1158
+ if (deps.onPaymentSucceeded) {
1159
+ try {
1160
+ await deps.onPaymentSucceeded({ ...orderData, id: orderId });
1161
+ } catch (err) {
1162
+ logger.error(
1163
+ `[3ds-complete] onPaymentSucceeded hook failed for ${orderId}:`,
1164
+ err
1165
+ );
1166
+ }
1167
+ }
1168
+ let emailSent = false;
1169
+ if (hasValidAuthCode) {
1170
+ const userEmail2 = orderData.userEmail || decodedToken.email || "";
1171
+ const emailResult = await deps.email.sendPaymentConfirmation({
1172
+ to: userEmail2,
1173
+ customerName: orderData.shippingAddress?.fullName || "",
1174
+ orderId,
1175
+ transactionId: txId,
1176
+ authorizationCode: txAuthCode,
1177
+ items: orderData.items || [],
1178
+ subtotal: orderData.subtotal || orderData.total || 0,
1179
+ discount: orderData.discount || void 0,
1180
+ couponCode: orderData.couponCode,
1181
+ vat: orderData.vat || 0,
1182
+ total: orderData.total || 0
1183
+ }).catch(() => ({ success: false }));
1184
+ emailSent = emailResult.success;
1185
+ if (emailSent) {
1186
+ await orderRef.update({ emailSentAt: /* @__PURE__ */ new Date() });
1187
+ }
1188
+ }
1189
+ if (orderData.deleteCardAfterPayment && orderData.paymentToken) {
1190
+ deleteCard(
1191
+ orderData.paymentToken,
1192
+ orderData.userId
1193
+ ).catch(
1194
+ (err) => logger.error("[3ds-complete] Failed to delete card after payment:", err)
1195
+ );
1196
+ }
1197
+ return server.NextResponse.json({
1198
+ success: true,
1199
+ transactionId: txId,
1200
+ authorizationCode: hasValidAuthCode ? txAuthCode : null,
1201
+ emailSent,
1202
+ orderId
1203
+ });
1204
+ }
1205
+ if (txStatusDetail === 36 || txStatusDetail === 37) {
1206
+ const threeDSData = verifyResult["3ds"];
1207
+ const challengeHtml = threeDSData?.browser_response?.challenge_request || threeDSData?.browser_response?.hidden_iframe || "";
1208
+ if (challengeHtml) {
1209
+ await db.collection("orders").doc(orderId).update({
1210
+ nuveiTransactionId: txId,
1211
+ isDeviceFingerprint: firestore.FieldValue.delete(),
1212
+ updatedAt: /* @__PURE__ */ new Date(),
1213
+ threeDSCres: firestore.FieldValue.delete()
1214
+ });
1215
+ return server.NextResponse.json({
1216
+ challenge: true,
1217
+ challengeHtml,
1218
+ isDeviceFingerprint: false,
1219
+ orderId,
1220
+ nuveiTransactionId: txId,
1221
+ statusDetail: txStatusDetail
1222
+ });
1223
+ }
1224
+ }
1225
+ await db.collection("orders").doc(orderId).update({
1226
+ status: "failed",
1227
+ chargeResponseAt: /* @__PURE__ */ new Date(),
1228
+ updatedAt: /* @__PURE__ */ new Date(),
1229
+ threeDSCres: firestore.FieldValue.delete()
1230
+ });
1231
+ const userEmail = orderData.userEmail || decodedToken.email || "";
1232
+ if (userEmail) {
1233
+ const retryUrl = deps.getRetryUrl?.({ ...orderData, id: orderId });
1234
+ deps.email.sendPaymentFailed({
1235
+ to: userEmail,
1236
+ customerName: orderData.shippingAddress?.fullName || "",
1237
+ orderId,
1238
+ errorMessage: "Pago rechazado tras autenticaci\xF3n 3DS.",
1239
+ items: orderData.items || [],
1240
+ total: orderData.total || 0,
1241
+ ...retryUrl ? { retryUrl } : {}
1242
+ }).catch(() => {
1243
+ });
1244
+ }
1245
+ return server.NextResponse.json(
1246
+ { error: "Pago rechazado tras autenticaci\xF3n 3DS." },
1247
+ { status: 400 }
1248
+ );
1249
+ } catch (error) {
1250
+ logger.error("3DS complete error:", error);
1251
+ return server.NextResponse.json(
1252
+ { error: "Error interno del servidor" },
1253
+ { status: 500 }
1254
+ );
1255
+ }
1256
+ };
1257
+ }
1258
+ function create3dsTimeoutHandler(deps) {
1259
+ const logger = deps.logger ?? console;
1260
+ const { db, auth } = deps.firebase;
1261
+ return async function POST(request) {
1262
+ const sessionCookie = request.cookies.get("__session")?.value;
1263
+ if (!sessionCookie || !auth) {
1264
+ return server.NextResponse.json({ error: "No autorizado" }, { status: 401 });
1265
+ }
1266
+ let decodedToken;
1267
+ try {
1268
+ decodedToken = await auth.verifySessionCookie(sessionCookie, true);
1269
+ } catch {
1270
+ return server.NextResponse.json({ error: "Sesion invalida" }, { status: 401 });
1271
+ }
1272
+ const { orderId } = await request.json();
1273
+ if (!orderId || typeof orderId !== "string") {
1274
+ return server.NextResponse.json({ error: "orderId requerido" }, { status: 400 });
1275
+ }
1276
+ const orderRef = db.collection("orders").doc(orderId);
1277
+ const orderDoc = await orderRef.get();
1278
+ if (!orderDoc.exists) {
1279
+ return server.NextResponse.json({ error: "Orden no encontrada" }, { status: 404 });
1280
+ }
1281
+ const orderData = orderDoc.data() ?? {};
1282
+ if (decodedToken.uid !== orderData.userId) {
1283
+ return server.NextResponse.json({ error: "Usuario no coincide" }, { status: 403 });
1284
+ }
1285
+ if (orderData.status !== "3ds-pending") {
1286
+ return server.NextResponse.json({ alreadyResolved: true });
1287
+ }
1288
+ await orderRef.update({
1289
+ status: "failed",
1290
+ failureReason: "3ds-timeout",
1291
+ chargeResponseAt: /* @__PURE__ */ new Date(),
1292
+ updatedAt: /* @__PURE__ */ new Date()
1293
+ });
1294
+ const userEmail = orderData.userEmail || decodedToken.email || "";
1295
+ if (userEmail) {
1296
+ const retryUrl = deps.getRetryUrl?.({ ...orderData, id: orderId });
1297
+ deps.email.sendPaymentFailed({
1298
+ to: userEmail,
1299
+ customerName: orderData.shippingAddress?.fullName || "",
1300
+ orderId,
1301
+ errorMessage: "No completaste la verificaci\xF3n 3DS a tiempo. Tu pago no fue procesado. Intenta de nuevo.",
1302
+ items: orderData.items || [],
1303
+ total: orderData.total || 0,
1304
+ ...retryUrl ? { retryUrl } : {}
1305
+ }).catch((err) => logger.error("[3ds-timeout] Failed to send email:", err));
1306
+ }
1307
+ return server.NextResponse.json({ ok: true });
1308
+ };
1309
+ }
1310
+ function createRefundHandler(deps) {
1311
+ const logger = deps.logger ?? console;
1312
+ const { db, auth } = deps.firebase;
1313
+ return async function POST(request) {
1314
+ try {
1315
+ const sessionCookie = request.cookies.get("__session")?.value;
1316
+ if (!sessionCookie || !auth) {
1317
+ return server.NextResponse.json({ error: "No autorizado" }, { status: 401 });
1318
+ }
1319
+ let decodedToken;
1320
+ try {
1321
+ decodedToken = await auth.verifySessionCookie(sessionCookie, true);
1322
+ } catch {
1323
+ return server.NextResponse.json({ error: "Sesi\xF3n inv\xE1lida" }, { status: 401 });
1324
+ }
1325
+ const { orderId } = await request.json();
1326
+ if (!orderId) {
1327
+ return server.NextResponse.json({ error: "orderId requerido" }, { status: 400 });
1328
+ }
1329
+ const orderDoc = await db.collection("orders").doc(orderId).get();
1330
+ if (!orderDoc.exists) {
1331
+ return server.NextResponse.json({ error: "Orden no encontrada" }, { status: 404 });
1332
+ }
1333
+ const order = orderDoc.data() ?? {};
1334
+ if (order.userId !== decodedToken.uid) {
1335
+ return server.NextResponse.json(
1336
+ { error: "No autorizado para esta orden" },
1337
+ { status: 403 }
1338
+ );
1339
+ }
1340
+ if (order.status !== "paid") {
1341
+ return server.NextResponse.json(
1342
+ { error: "Solo se pueden reembolsar \xF3rdenes pagadas" },
1343
+ { status: 400 }
1344
+ );
1345
+ }
1346
+ const paymentTransactionId = order.paymentTransactionId;
1347
+ if (!paymentTransactionId) {
1348
+ return server.NextResponse.json(
1349
+ { error: "No se encontr\xF3 ID de transacci\xF3n" },
1350
+ { status: 400 }
1351
+ );
1352
+ }
1353
+ const result = await refundTransaction(paymentTransactionId);
1354
+ if (result.status === "success") {
1355
+ await db.collection("orders").doc(orderId).update({
1356
+ status: "cancelled",
1357
+ refundedAt: /* @__PURE__ */ new Date(),
1358
+ updatedAt: /* @__PURE__ */ new Date()
1359
+ });
1360
+ if (deps.onRefundSucceeded) {
1361
+ try {
1362
+ await deps.onRefundSucceeded({ ...order, id: orderId });
1363
+ } catch (err) {
1364
+ logger.error(
1365
+ `[refund] onRefundSucceeded hook failed for order ${orderId}:`,
1366
+ err
1367
+ );
1368
+ }
1369
+ }
1370
+ return server.NextResponse.json({ success: true, detail: result.detail });
1371
+ }
1372
+ return server.NextResponse.json(
1373
+ { error: result.detail || "Error al procesar reembolso" },
1374
+ { status: 400 }
1375
+ );
1376
+ } catch (error) {
1377
+ logger.error("Refund error:", error);
1378
+ return server.NextResponse.json(
1379
+ { error: "Error interno del servidor" },
1380
+ { status: 500 }
1381
+ );
1382
+ }
1383
+ };
1384
+ }
1385
+ function createInitCheckoutHandler(deps) {
1386
+ const logger = deps.logger ?? console;
1387
+ const { auth } = deps.firebase;
1388
+ return async function POST(request) {
1389
+ const sessionCookie = request.cookies.get("__session")?.value;
1390
+ if (!sessionCookie || !auth) {
1391
+ return server.NextResponse.json({ error: "No autorizado" }, { status: 401 });
1392
+ }
1393
+ let decodedToken;
1394
+ try {
1395
+ decodedToken = await auth.verifySessionCookie(sessionCookie, true);
1396
+ } catch {
1397
+ return server.NextResponse.json({ error: "Sesi\xF3n inv\xE1lida" }, { status: 401 });
1398
+ }
1399
+ try {
1400
+ const { amount, vat, description, devReference } = await request.json();
1401
+ if (!amount || !devReference) {
1402
+ return server.NextResponse.json(
1403
+ { error: "amount y devReference son requeridos" },
1404
+ { status: 400 }
1405
+ );
1406
+ }
1407
+ const sessionId = crypto__default.default.randomBytes(16).toString("hex");
1408
+ const result = await nuveiRequest("/v2/transaction/init_reference/", "POST", {
1409
+ locale: "es",
1410
+ session_id: sessionId,
1411
+ order: {
1412
+ amount,
1413
+ description: description || deps.defaultDescription,
1414
+ vat: vat ?? 0,
1415
+ dev_reference: devReference,
1416
+ installments_type: 0
1417
+ },
1418
+ user: {
1419
+ id: decodedToken.uid,
1420
+ email: decodedToken.email || ""
1421
+ },
1422
+ conf: {
1423
+ theme: {
1424
+ primary_color: deps.themeColors.primary,
1425
+ secondary_color: deps.themeColors.secondary
1426
+ }
1427
+ }
1428
+ });
1429
+ if (result.error) {
1430
+ return server.NextResponse.json(
1431
+ { error: result.error.description || "Error al inicializar checkout" },
1432
+ { status: 400 }
1433
+ );
1434
+ }
1435
+ if (!result.reference) {
1436
+ return server.NextResponse.json(
1437
+ { error: "No se recibi\xF3 referencia de checkout" },
1438
+ { status: 500 }
1439
+ );
1440
+ }
1441
+ return server.NextResponse.json({
1442
+ reference: result.reference,
1443
+ checkoutUrl: result.checkout_url
1444
+ });
1445
+ } catch (error) {
1446
+ logger.error("Init checkout error:", error);
1447
+ return server.NextResponse.json(
1448
+ { error: "Error al inicializar checkout" },
1449
+ { status: 500 }
1450
+ );
1451
+ }
1452
+ };
1453
+ }
1454
+ function createCardsHandler(deps) {
1455
+ const logger = deps.logger ?? console;
1456
+ const { db, auth } = deps.firebase;
1457
+ const verifiedCardsEnabled = deps.enableVerifiedCardsTracking !== false;
1458
+ async function verifySession(request) {
1459
+ const sessionCookie = request.cookies.get("__session")?.value;
1460
+ if (!sessionCookie || !auth) return null;
1461
+ try {
1462
+ return await auth.verifySessionCookie(sessionCookie, true);
1463
+ } catch {
1464
+ return null;
1465
+ }
1466
+ }
1467
+ const GET = async function GET2(request) {
1468
+ const decoded = await verifySession(request);
1469
+ if (!decoded) {
1470
+ return server.NextResponse.json({ error: "No autorizado" }, { status: 401 });
1471
+ }
1472
+ try {
1473
+ const result = await listCards(decoded.uid);
1474
+ logger.log(
1475
+ "[cards/GET] Raw Nuvei response:",
1476
+ JSON.stringify(
1477
+ result.cards?.map((c) => ({
1478
+ token: c.token?.slice(-6),
1479
+ status: c.status,
1480
+ type: c.type,
1481
+ number: c.number
1482
+ }))
1483
+ )
1484
+ );
1485
+ const cards = (result.cards || []).filter(
1486
+ (c) => c.status === "valid" || c.status === "review" || c.status === "pending"
1487
+ );
1488
+ if (verifiedCardsEnabled && cards.some((c) => c.status === "review")) {
1489
+ const verifiedDoc = await db.collection("users").doc(decoded.uid).collection("verifiedCards").get();
1490
+ const verifiedTokens = new Set(verifiedDoc.docs.map((d) => d.id));
1491
+ const enrichedCards = cards.map(
1492
+ (c) => c.status === "review" && verifiedTokens.has(c.token) ? { ...c, status: "valid" } : c
1493
+ );
1494
+ return server.NextResponse.json({ cards: enrichedCards });
1495
+ }
1496
+ return server.NextResponse.json({ cards });
1497
+ } catch (error) {
1498
+ logger.error("Error listing cards:", error);
1499
+ return server.NextResponse.json(
1500
+ { error: "Error al obtener tarjetas" },
1501
+ { status: 500 }
1502
+ );
1503
+ }
1504
+ };
1505
+ const DELETE = async function DELETE2(request) {
1506
+ const decoded = await verifySession(request);
1507
+ if (!decoded) {
1508
+ return server.NextResponse.json({ error: "No autorizado" }, { status: 401 });
1509
+ }
1510
+ try {
1511
+ const { token } = await request.json();
1512
+ if (!token) {
1513
+ return server.NextResponse.json(
1514
+ { error: "Token de tarjeta requerido" },
1515
+ { status: 400 }
1516
+ );
1517
+ }
1518
+ const result = await deleteCard(token, decoded.uid);
1519
+ logger.log("[cards/DELETE] Nuvei response:", JSON.stringify(result));
1520
+ const resultWithError = result;
1521
+ if (resultWithError.error) {
1522
+ logger.error("[cards/DELETE] Nuvei delete failed:", JSON.stringify(result));
1523
+ return server.NextResponse.json(
1524
+ { error: "No se pudo eliminar la tarjeta en Nuvei", detail: result },
1525
+ { status: 400 }
1526
+ );
1527
+ }
1528
+ if (verifiedCardsEnabled) {
1529
+ try {
1530
+ await db.collection("users").doc(decoded.uid).collection("verifiedCards").doc(token).delete();
1531
+ } catch {
1532
+ }
1533
+ }
1534
+ return server.NextResponse.json(result);
1535
+ } catch (error) {
1536
+ logger.error("Error deleting card:", error);
1537
+ return server.NextResponse.json(
1538
+ { error: "Error al eliminar tarjeta" },
1539
+ { status: 500 }
1540
+ );
1541
+ }
1542
+ };
1543
+ return { GET, DELETE };
1544
+ }
1545
+ function createVerifyHandler(deps) {
1546
+ const logger = deps.logger ?? console;
1547
+ const { db, auth } = deps.firebase;
1548
+ const verifiedCardsEnabled = deps.enableVerifiedCardsTracking !== false;
1549
+ return async function POST(request) {
1550
+ const sessionCookie = request.cookies.get("__session")?.value;
1551
+ if (!sessionCookie || !auth) {
1552
+ return server.NextResponse.json({ error: "No autorizado" }, { status: 401 });
1553
+ }
1554
+ let decodedToken;
1555
+ try {
1556
+ decodedToken = await auth.verifySessionCookie(sessionCookie, true);
1557
+ } catch {
1558
+ return server.NextResponse.json({ error: "Sesi\xF3n inv\xE1lida" }, { status: 401 });
1559
+ }
1560
+ try {
1561
+ const { cardToken, transactionReference, value } = await request.json();
1562
+ if (!cardToken && !transactionReference || !value) {
1563
+ return server.NextResponse.json(
1564
+ { error: "transactionReference y value son requeridos" },
1565
+ { status: 400 }
1566
+ );
1567
+ }
1568
+ const result = await verifyCard({
1569
+ userId: decodedToken.uid,
1570
+ transactionReference: transactionReference || cardToken,
1571
+ value
1572
+ });
1573
+ if (result.error) {
1574
+ return server.NextResponse.json(
1575
+ { error: result.error.description || "Error de verificaci\xF3n" },
1576
+ { status: 400 }
1577
+ );
1578
+ }
1579
+ if (verifiedCardsEnabled && cardToken) {
1580
+ try {
1581
+ await db.collection("users").doc(decodedToken.uid).collection("verifiedCards").doc(cardToken).set({ verifiedAt: /* @__PURE__ */ new Date() });
1582
+ } catch (err) {
1583
+ logger.error("Failed to persist verified card:", err);
1584
+ }
1585
+ }
1586
+ return server.NextResponse.json({
1587
+ success: true,
1588
+ transaction: result.transaction
1589
+ });
1590
+ } catch (error) {
1591
+ logger.error("Card verify error:", error);
1592
+ return server.NextResponse.json(
1593
+ { error: "Error al verificar tarjeta" },
1594
+ { status: 500 }
1595
+ );
1596
+ }
1597
+ };
1598
+ }
1599
+ function createTestChargeHandler(deps) {
1600
+ const logger = deps.logger ?? console;
1601
+ const { auth } = deps.firebase;
1602
+ const defaultDescription = deps.defaultDescription ?? "Test charge";
1603
+ return async function POST(request) {
1604
+ if (process.env.NUVEI_ENV === "prod") {
1605
+ return server.NextResponse.json({ error: "Not available" }, { status: 404 });
1606
+ }
1607
+ const sessionCookie = request.cookies.get("__session")?.value;
1608
+ if (!sessionCookie || !auth) {
1609
+ return server.NextResponse.json({ error: "No autorizado" }, { status: 401 });
1610
+ }
1611
+ let decodedToken;
1612
+ try {
1613
+ decodedToken = await auth.verifySessionCookie(sessionCookie, true);
1614
+ } catch {
1615
+ return server.NextResponse.json({ error: "Sesi\xF3n inv\xE1lida" }, { status: 401 });
1616
+ }
1617
+ try {
1618
+ const { token, amount, vat, description, devReference } = await request.json();
1619
+ if (!token || !amount || !devReference) {
1620
+ return server.NextResponse.json(
1621
+ { error: "token, amount y devReference son requeridos" },
1622
+ { status: 400 }
1623
+ );
1624
+ }
1625
+ const result = await debitWithToken({
1626
+ userId: decodedToken.uid,
1627
+ userEmail: decodedToken.email || "",
1628
+ amount,
1629
+ description: description || defaultDescription,
1630
+ devReference,
1631
+ cardToken: token,
1632
+ vat: vat ?? 0
1633
+ });
1634
+ logger.log("Test charge result:", JSON.stringify(result, null, 2));
1635
+ if (result.transaction && result.transaction.status === "success" && result.transaction.status_detail === 3) {
1636
+ return server.NextResponse.json({
1637
+ success: true,
1638
+ transaction: result.transaction,
1639
+ card: result.card
1640
+ });
1641
+ }
1642
+ return server.NextResponse.json(
1643
+ {
1644
+ success: false,
1645
+ error: result.transaction?.message || result.error?.description || "Pago rechazado",
1646
+ detail: result
1647
+ },
1648
+ { status: 400 }
1649
+ );
1650
+ } catch (error) {
1651
+ logger.error("Test charge error:", error);
1652
+ return server.NextResponse.json(
1653
+ { error: "Error interno del servidor" },
1654
+ { status: 500 }
1655
+ );
1656
+ }
1657
+ };
1658
+ }
1659
+
1660
+ exports.create3dsCallbackHandler = create3dsCallbackHandler;
1661
+ exports.create3dsCompleteHandler = create3dsCompleteHandler;
1662
+ exports.create3dsTimeoutHandler = create3dsTimeoutHandler;
1663
+ exports.createCardsHandler = createCardsHandler;
1664
+ exports.createChargeHandler = createChargeHandler;
1665
+ exports.createInitCheckoutHandler = createInitCheckoutHandler;
1666
+ exports.createRefundHandler = createRefundHandler;
1667
+ exports.createTestChargeHandler = createTestChargeHandler;
1668
+ exports.createVerifyHandler = createVerifyHandler;
1669
+ exports.createWebhookHandler = createWebhookHandler;
1670
+ //# sourceMappingURL=index.cjs.map
1671
+ //# sourceMappingURL=index.cjs.map