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