@pandait.tech/payment-nuvei 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/dist/adapters/index.cjs +54 -0
- package/dist/adapters/index.cjs.map +1 -0
- package/dist/adapters/index.d.cts +35 -0
- package/dist/adapters/index.d.ts +35 -0
- package/dist/adapters/index.js +51 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/handlers/index.cjs +188 -105
- package/dist/handlers/index.cjs.map +1 -1
- package/dist/handlers/index.d.cts +29 -117
- package/dist/handlers/index.d.ts +29 -117
- package/dist/handlers/index.js +188 -106
- package/dist/handlers/index.js.map +1 -1
- package/package.json +17 -9
package/dist/handlers/index.js
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
1
|
import { FieldValue } from 'firebase-admin/firestore';
|
|
3
2
|
import crypto from 'crypto';
|
|
4
3
|
import { z } from 'zod';
|
|
5
4
|
|
|
6
|
-
// src/
|
|
5
|
+
// src/http.ts
|
|
6
|
+
function json(data, init) {
|
|
7
|
+
const headers = new Headers(init?.headers);
|
|
8
|
+
if (!headers.has("content-type")) {
|
|
9
|
+
headers.set("content-type", "application/json; charset=utf-8");
|
|
10
|
+
}
|
|
11
|
+
return new Response(JSON.stringify(data), { ...init, headers });
|
|
12
|
+
}
|
|
13
|
+
function getCookie(request, name) {
|
|
14
|
+
const header = request.headers.get("cookie");
|
|
15
|
+
if (!header) return void 0;
|
|
16
|
+
for (const part of header.split(";")) {
|
|
17
|
+
const eq = part.indexOf("=");
|
|
18
|
+
if (eq === -1) continue;
|
|
19
|
+
const key = part.slice(0, eq).trim();
|
|
20
|
+
if (key === name) {
|
|
21
|
+
return decodeURIComponent(part.slice(eq + 1).trim());
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return void 0;
|
|
25
|
+
}
|
|
7
26
|
var NUVEI_DOMAIN = "paymentez.com";
|
|
8
27
|
function getBaseUrl() {
|
|
9
28
|
const env = process.env.NUVEI_ENV === "prod" ? "prod" : "stg";
|
|
@@ -206,15 +225,15 @@ function createChargeHandler(deps) {
|
|
|
206
225
|
const { db, auth } = deps.firebase;
|
|
207
226
|
return async function POST(request) {
|
|
208
227
|
try {
|
|
209
|
-
const sessionCookie = request
|
|
228
|
+
const sessionCookie = getCookie(request, "__session");
|
|
210
229
|
if (!sessionCookie || !auth) {
|
|
211
|
-
return
|
|
230
|
+
return json({ error: "No autorizado" }, { status: 401 });
|
|
212
231
|
}
|
|
213
232
|
let decodedToken;
|
|
214
233
|
try {
|
|
215
234
|
decodedToken = await auth.verifySessionCookie(sessionCookie, true);
|
|
216
235
|
} catch {
|
|
217
|
-
return
|
|
236
|
+
return json({ error: "Sesion invalida" }, { status: 401 });
|
|
218
237
|
}
|
|
219
238
|
const clientIpForLimit = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-real-ip") || "unknown";
|
|
220
239
|
if (deps.rateLimit) {
|
|
@@ -224,7 +243,7 @@ function createChargeHandler(deps) {
|
|
|
224
243
|
15 * 60 * 1e3
|
|
225
244
|
);
|
|
226
245
|
if (!allowed) {
|
|
227
|
-
return
|
|
246
|
+
return json(
|
|
228
247
|
{ error: "Demasiados intentos. Intent\xE1 de nuevo en unos minutos." },
|
|
229
248
|
{ status: 429 }
|
|
230
249
|
);
|
|
@@ -234,7 +253,7 @@ function createChargeHandler(deps) {
|
|
|
234
253
|
if (deps.turnstile) {
|
|
235
254
|
const turnstileToken = body.turnstileToken;
|
|
236
255
|
if (!turnstileToken) {
|
|
237
|
-
return
|
|
256
|
+
return json(
|
|
238
257
|
{ error: "Verificaci\xF3n de seguridad faltante. Recarg\xE1 la p\xE1gina." },
|
|
239
258
|
{ status: 403 }
|
|
240
259
|
);
|
|
@@ -244,7 +263,7 @@ function createChargeHandler(deps) {
|
|
|
244
263
|
clientIpForLimit === "unknown" ? void 0 : clientIpForLimit
|
|
245
264
|
);
|
|
246
265
|
if (!turnstileResult.success) {
|
|
247
|
-
return
|
|
266
|
+
return json(
|
|
248
267
|
{ error: "No pudimos verificar que sos humano. Recarg\xE1 la p\xE1gina." },
|
|
249
268
|
{ status: 403 }
|
|
250
269
|
);
|
|
@@ -270,18 +289,18 @@ function createChargeHandler(deps) {
|
|
|
270
289
|
userId: userId?.substring(0, 8) + "..."
|
|
271
290
|
});
|
|
272
291
|
if (!token || !orderId || !amount) {
|
|
273
|
-
return
|
|
292
|
+
return json({ error: "Datos incompletos" }, { status: 400 });
|
|
274
293
|
}
|
|
275
294
|
if (decodedToken.uid !== userId) {
|
|
276
|
-
return
|
|
295
|
+
return json({ error: "Usuario no coincide" }, { status: 403 });
|
|
277
296
|
}
|
|
278
297
|
const orderDoc = await db.collection("orders").doc(orderId).get();
|
|
279
298
|
if (!orderDoc.exists) {
|
|
280
|
-
return
|
|
299
|
+
return json({ error: "Orden no encontrada" }, { status: 404 });
|
|
281
300
|
}
|
|
282
301
|
const orderData = orderDoc.data() ?? {};
|
|
283
302
|
if (orderData.status !== "pending") {
|
|
284
|
-
return
|
|
303
|
+
return json(
|
|
285
304
|
{ error: "Esta orden ya fue procesada" },
|
|
286
305
|
{ status: 409 }
|
|
287
306
|
);
|
|
@@ -295,7 +314,7 @@ function createChargeHandler(deps) {
|
|
|
295
314
|
});
|
|
296
315
|
if (customResult.handled) {
|
|
297
316
|
if (!customResult.valid) {
|
|
298
|
-
return
|
|
317
|
+
return json(
|
|
299
318
|
{ error: customResult.error },
|
|
300
319
|
{ status: customResult.status }
|
|
301
320
|
);
|
|
@@ -309,7 +328,7 @@ function createChargeHandler(deps) {
|
|
|
309
328
|
for (const item of orderItems) {
|
|
310
329
|
const productDoc = await db.collection("products").doc(item.productId).get();
|
|
311
330
|
if (!productDoc.exists || !productDoc.data()?.isActive) {
|
|
312
|
-
return
|
|
331
|
+
return json(
|
|
313
332
|
{ error: `El producto "${item.name}" ya no est\xE1 disponible` },
|
|
314
333
|
{ status: 409 }
|
|
315
334
|
);
|
|
@@ -318,7 +337,7 @@ function createChargeHandler(deps) {
|
|
|
318
337
|
const isDigital = productData.isDigital === true;
|
|
319
338
|
const stock = productData.stock ?? 0;
|
|
320
339
|
if (!isDigital && stock < item.quantity) {
|
|
321
|
-
return
|
|
340
|
+
return json(
|
|
322
341
|
{
|
|
323
342
|
error: `Stock insuficiente para "${item.name}" (disponible: ${stock})`
|
|
324
343
|
},
|
|
@@ -331,7 +350,7 @@ function createChargeHandler(deps) {
|
|
|
331
350
|
});
|
|
332
351
|
const expectedItemSubtotal = display.finalSubtotal;
|
|
333
352
|
if (Math.abs(expectedItemSubtotal - item.price) > 0.02) {
|
|
334
|
-
return
|
|
353
|
+
return json(
|
|
335
354
|
{
|
|
336
355
|
error: `El precio de "${item.name}" cambi\xF3. Recarg\xE1 la p\xE1gina para ver el precio actualizado.`
|
|
337
356
|
},
|
|
@@ -346,7 +365,7 @@ function createChargeHandler(deps) {
|
|
|
346
365
|
if (orderPromotionId && orderDiscount > 0) {
|
|
347
366
|
const promoDoc = await db.collection("promotions").doc(orderPromotionId).get();
|
|
348
367
|
if (!promoDoc.exists || !promoDoc.data()?.isActive) {
|
|
349
|
-
return
|
|
368
|
+
return json(
|
|
350
369
|
{ error: "El cupon aplicado ya no es valido. Remuevelo y vuelve a intentar." },
|
|
351
370
|
{ status: 409 }
|
|
352
371
|
);
|
|
@@ -356,13 +375,13 @@ function createChargeHandler(deps) {
|
|
|
356
375
|
const validFrom = promo.rules.validFrom.toDate ? promo.rules.validFrom.toDate() : new Date(promo.rules.validFrom);
|
|
357
376
|
const validUntil = promo.rules.validUntil.toDate ? promo.rules.validUntil.toDate() : new Date(promo.rules.validUntil);
|
|
358
377
|
if (now < validFrom || now > validUntil) {
|
|
359
|
-
return
|
|
378
|
+
return json(
|
|
360
379
|
{ error: "El cupon aplicado ha expirado. Remuevelo y vuelve a intentar." },
|
|
361
380
|
{ status: 409 }
|
|
362
381
|
);
|
|
363
382
|
}
|
|
364
383
|
if (promo.rules.maxTotalUses && promo.currentUses >= promo.rules.maxTotalUses) {
|
|
365
|
-
return
|
|
384
|
+
return json(
|
|
366
385
|
{ error: "El cupon aplicado ya alcanzo su limite de usos." },
|
|
367
386
|
{ status: 409 }
|
|
368
387
|
);
|
|
@@ -381,7 +400,7 @@ function createChargeHandler(deps) {
|
|
|
381
400
|
const verifiedVat = Math.round(verifiedDiscountedSubtotal * 0.15 * 100) / 100;
|
|
382
401
|
const verifiedTotal = Math.round((verifiedDiscountedSubtotal + verifiedVat) * 100) / 100;
|
|
383
402
|
if (verifiedTotal !== amount) {
|
|
384
|
-
return
|
|
403
|
+
return json(
|
|
385
404
|
{
|
|
386
405
|
error: "El monto no coincide con los precios actuales. Actualiza tu carrito."
|
|
387
406
|
},
|
|
@@ -504,7 +523,7 @@ function createChargeHandler(deps) {
|
|
|
504
523
|
(err) => logger.error("[charge] Failed to delete card after payment:", err)
|
|
505
524
|
);
|
|
506
525
|
}
|
|
507
|
-
return
|
|
526
|
+
return json({
|
|
508
527
|
success: true,
|
|
509
528
|
transactionId: nuveiData.transaction.id,
|
|
510
529
|
authorizationCode: hasValidAuthCode ? rawAuthCode : null,
|
|
@@ -519,7 +538,7 @@ function createChargeHandler(deps) {
|
|
|
519
538
|
chargeResponseAt: /* @__PURE__ */ new Date(),
|
|
520
539
|
updatedAt: /* @__PURE__ */ new Date()
|
|
521
540
|
});
|
|
522
|
-
return
|
|
541
|
+
return json({
|
|
523
542
|
review: true,
|
|
524
543
|
orderId,
|
|
525
544
|
transactionId: nuveiData.transaction.id
|
|
@@ -536,7 +555,7 @@ function createChargeHandler(deps) {
|
|
|
536
555
|
...persistDeleteOnPaid,
|
|
537
556
|
updatedAt: /* @__PURE__ */ new Date()
|
|
538
557
|
});
|
|
539
|
-
return
|
|
558
|
+
return json({
|
|
540
559
|
challenge: true,
|
|
541
560
|
challengeHtml: hiddenIframeHtml,
|
|
542
561
|
isDeviceFingerprint: true,
|
|
@@ -552,7 +571,7 @@ function createChargeHandler(deps) {
|
|
|
552
571
|
...persistDeleteOnPaid,
|
|
553
572
|
updatedAt: /* @__PURE__ */ new Date()
|
|
554
573
|
});
|
|
555
|
-
return
|
|
574
|
+
return json({
|
|
556
575
|
otpRequired: true,
|
|
557
576
|
orderId,
|
|
558
577
|
nuveiTransactionId: nuveiData.transaction.id,
|
|
@@ -575,7 +594,7 @@ function createChargeHandler(deps) {
|
|
|
575
594
|
...persistDeleteOnPaid,
|
|
576
595
|
updatedAt: /* @__PURE__ */ new Date()
|
|
577
596
|
});
|
|
578
|
-
return
|
|
597
|
+
return json({
|
|
579
598
|
challenge: true,
|
|
580
599
|
challengeHtml,
|
|
581
600
|
isDeviceFingerprint: false,
|
|
@@ -610,7 +629,7 @@ function createChargeHandler(deps) {
|
|
|
610
629
|
}).catch(() => {
|
|
611
630
|
});
|
|
612
631
|
}
|
|
613
|
-
return
|
|
632
|
+
return json({ error: threeDSErrorMsg }, { status: 400 });
|
|
614
633
|
}
|
|
615
634
|
const failedErrorMsg = getNuveiUserMessage(
|
|
616
635
|
nuveiData.transaction?.status_detail,
|
|
@@ -638,10 +657,10 @@ function createChargeHandler(deps) {
|
|
|
638
657
|
}).catch(() => {
|
|
639
658
|
});
|
|
640
659
|
}
|
|
641
|
-
return
|
|
660
|
+
return json({ error: failedErrorMsg }, { status: 400 });
|
|
642
661
|
} catch (error) {
|
|
643
662
|
logger.error("Payment charge error:", error);
|
|
644
|
-
return
|
|
663
|
+
return json(
|
|
645
664
|
{ error: "Error interno del servidor" },
|
|
646
665
|
{ status: 500 }
|
|
647
666
|
);
|
|
@@ -675,7 +694,7 @@ function createWebhookHandler(deps) {
|
|
|
675
694
|
logger.log("[webhook] Full payload:", JSON.stringify(payload));
|
|
676
695
|
const { transaction } = payload;
|
|
677
696
|
if (!transaction?.id || !transaction?.dev_reference) {
|
|
678
|
-
return
|
|
697
|
+
return json({ error: "Payload inv\xE1lido" }, { status: 400 });
|
|
679
698
|
}
|
|
680
699
|
const incomingCres = extractCres(payload);
|
|
681
700
|
if (incomingCres) {
|
|
@@ -697,7 +716,7 @@ function createWebhookHandler(deps) {
|
|
|
697
716
|
const orderDoc = await orderRef.get();
|
|
698
717
|
if (!orderDoc.exists) {
|
|
699
718
|
logger.error(`Webhook: Order ${orderId} not found`);
|
|
700
|
-
return
|
|
719
|
+
return json({ error: "Orden no encontrada" }, { status: 404 });
|
|
701
720
|
}
|
|
702
721
|
const orderDataEarly = orderDoc.data() ?? {};
|
|
703
722
|
if (incomingCres && orderDataEarly.status === "3ds-pending" && !orderDataEarly.verifyCalledAt) {
|
|
@@ -791,7 +810,7 @@ function createWebhookHandler(deps) {
|
|
|
791
810
|
);
|
|
792
811
|
}
|
|
793
812
|
}
|
|
794
|
-
return
|
|
813
|
+
return json({ received: true, verifiedFromWebhook: true });
|
|
795
814
|
}
|
|
796
815
|
} catch (err) {
|
|
797
816
|
if (err.message !== "ALREADY_CALLED") {
|
|
@@ -885,16 +904,18 @@ function createWebhookHandler(deps) {
|
|
|
885
904
|
);
|
|
886
905
|
}
|
|
887
906
|
}
|
|
888
|
-
return
|
|
907
|
+
return json({ received: true });
|
|
889
908
|
} catch (error) {
|
|
890
909
|
logger.error("Webhook processing error:", error);
|
|
891
|
-
return
|
|
910
|
+
return json(
|
|
892
911
|
{ error: "Error procesando webhook" },
|
|
893
912
|
{ status: 500 }
|
|
894
913
|
);
|
|
895
914
|
}
|
|
896
915
|
};
|
|
897
916
|
}
|
|
917
|
+
|
|
918
|
+
// src/handlers/3ds-callback.ts
|
|
898
919
|
function buildCompletionPage(orderId, transStatus) {
|
|
899
920
|
const html = `<!DOCTYPE html>
|
|
900
921
|
<html><head><meta charset="utf-8"><title>3DS Verification</title></head>
|
|
@@ -919,7 +940,7 @@ function buildCompletionPage(orderId, transStatus) {
|
|
|
919
940
|
Verificando autenticación...
|
|
920
941
|
</p>
|
|
921
942
|
</body></html>`;
|
|
922
|
-
return new
|
|
943
|
+
return new Response(html, {
|
|
923
944
|
status: 200,
|
|
924
945
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
925
946
|
});
|
|
@@ -983,32 +1004,32 @@ function create3dsCompleteHandler(deps) {
|
|
|
983
1004
|
const { db, auth } = deps.firebase;
|
|
984
1005
|
return async function POST(request) {
|
|
985
1006
|
try {
|
|
986
|
-
const sessionCookie = request
|
|
1007
|
+
const sessionCookie = getCookie(request, "__session");
|
|
987
1008
|
if (!sessionCookie || !auth) {
|
|
988
|
-
return
|
|
1009
|
+
return json({ error: "No autorizado" }, { status: 401 });
|
|
989
1010
|
}
|
|
990
1011
|
let decodedToken;
|
|
991
1012
|
try {
|
|
992
1013
|
decodedToken = await auth.verifySessionCookie(sessionCookie, true);
|
|
993
1014
|
} catch {
|
|
994
|
-
return
|
|
1015
|
+
return json({ error: "Sesion invalida" }, { status: 401 });
|
|
995
1016
|
}
|
|
996
1017
|
const rawBody = await request.json();
|
|
997
1018
|
const parsed = ThreeDSCompleteSchema.safeParse(rawBody);
|
|
998
1019
|
if (!parsed.success) {
|
|
999
|
-
return
|
|
1020
|
+
return json({ error: "Datos incompletos" }, { status: 400 });
|
|
1000
1021
|
}
|
|
1001
1022
|
const { orderId, userId, type, nuveiTransactionId: bodyTxId, otpCode } = parsed.data;
|
|
1002
1023
|
if (decodedToken.uid !== userId) {
|
|
1003
|
-
return
|
|
1024
|
+
return json({ error: "Usuario no coincide" }, { status: 403 });
|
|
1004
1025
|
}
|
|
1005
1026
|
const orderDoc = await db.collection("orders").doc(orderId).get();
|
|
1006
1027
|
if (!orderDoc.exists) {
|
|
1007
|
-
return
|
|
1028
|
+
return json({ error: "Orden no encontrada" }, { status: 404 });
|
|
1008
1029
|
}
|
|
1009
1030
|
const orderData = orderDoc.data() ?? {};
|
|
1010
1031
|
if (orderData.status === "paid") {
|
|
1011
|
-
return
|
|
1032
|
+
return json({
|
|
1012
1033
|
success: true,
|
|
1013
1034
|
transactionId: orderData.paymentTransactionId,
|
|
1014
1035
|
authorizationCode: orderData.authorizationCode || null,
|
|
@@ -1019,10 +1040,10 @@ function create3dsCompleteHandler(deps) {
|
|
|
1019
1040
|
logger.log(
|
|
1020
1041
|
`[3ds-complete] verify already called for order ${orderId}, returning current state`
|
|
1021
1042
|
);
|
|
1022
|
-
return
|
|
1043
|
+
return json({ error: "Pago ya procesado" }, { status: 409 });
|
|
1023
1044
|
}
|
|
1024
1045
|
if (orderData.status !== "3ds-pending" && orderData.status !== "otp-pending") {
|
|
1025
|
-
return
|
|
1046
|
+
return json(
|
|
1026
1047
|
{ error: "Esta orden ya fue procesada" },
|
|
1027
1048
|
{ status: 409 }
|
|
1028
1049
|
);
|
|
@@ -1049,29 +1070,29 @@ function create3dsCompleteHandler(deps) {
|
|
|
1049
1070
|
}).catch(() => {
|
|
1050
1071
|
});
|
|
1051
1072
|
}
|
|
1052
|
-
return
|
|
1073
|
+
return json({ error: msg }, { status: 400 });
|
|
1053
1074
|
}
|
|
1054
1075
|
const transactionId = orderData.nuveiTransactionId || bodyTxId;
|
|
1055
1076
|
if (!transactionId) {
|
|
1056
|
-
return
|
|
1077
|
+
return json(
|
|
1057
1078
|
{ error: "No se encontr\xF3 el ID de transacci\xF3n para verificar" },
|
|
1058
1079
|
{ status: 400 }
|
|
1059
1080
|
);
|
|
1060
1081
|
}
|
|
1061
1082
|
if (type === "AUTHENTICATION_CONTINUE" && !orderData.threeDSCres && !orderData.isDeviceFingerprint) {
|
|
1062
1083
|
logger.log(`[3ds-complete] No CRES yet for order ${orderId} \u2014 still pending`);
|
|
1063
|
-
return
|
|
1084
|
+
return json({ stillPending: true });
|
|
1064
1085
|
}
|
|
1065
1086
|
const actualType = type === "AUTHENTICATION_CONTINUE" && orderData.threeDSCres ? "BY_CRES" : type;
|
|
1066
1087
|
const cresValue = actualType === "BY_CRES" ? orderData.threeDSCres : void 0;
|
|
1067
1088
|
if (actualType === "BY_CRES" && !cresValue) {
|
|
1068
|
-
return
|
|
1089
|
+
return json(
|
|
1069
1090
|
{ error: "No se encontr\xF3 el valor de autenticaci\xF3n 3DS (cres)" },
|
|
1070
1091
|
{ status: 400 }
|
|
1071
1092
|
);
|
|
1072
1093
|
}
|
|
1073
1094
|
if (type === "BY_OTP" && !otpCode) {
|
|
1074
|
-
return
|
|
1095
|
+
return json(
|
|
1075
1096
|
{ error: "Debes ingresar el c\xF3digo OTP" },
|
|
1076
1097
|
{ status: 400 }
|
|
1077
1098
|
);
|
|
@@ -1102,7 +1123,7 @@ function create3dsCompleteHandler(deps) {
|
|
|
1102
1123
|
logger.log(
|
|
1103
1124
|
`[3ds-complete] Still pending: txStatus=${txStatus} detail=${txStatusDetail}. Polling continues.`
|
|
1104
1125
|
);
|
|
1105
|
-
return
|
|
1126
|
+
return json({ stillPending: true });
|
|
1106
1127
|
}
|
|
1107
1128
|
if (isSuccess) {
|
|
1108
1129
|
const hasValidAuthCode = typeof txAuthCode === "string" && txAuthCode.trim().length > 0 && txAuthCode !== "null";
|
|
@@ -1188,7 +1209,7 @@ function create3dsCompleteHandler(deps) {
|
|
|
1188
1209
|
(err) => logger.error("[3ds-complete] Failed to delete card after payment:", err)
|
|
1189
1210
|
);
|
|
1190
1211
|
}
|
|
1191
|
-
return
|
|
1212
|
+
return json({
|
|
1192
1213
|
success: true,
|
|
1193
1214
|
transactionId: txId,
|
|
1194
1215
|
authorizationCode: hasValidAuthCode ? txAuthCode : null,
|
|
@@ -1206,7 +1227,7 @@ function create3dsCompleteHandler(deps) {
|
|
|
1206
1227
|
updatedAt: /* @__PURE__ */ new Date(),
|
|
1207
1228
|
threeDSCres: FieldValue.delete()
|
|
1208
1229
|
});
|
|
1209
|
-
return
|
|
1230
|
+
return json({
|
|
1210
1231
|
challenge: true,
|
|
1211
1232
|
challengeHtml,
|
|
1212
1233
|
isDeviceFingerprint: false,
|
|
@@ -1236,48 +1257,50 @@ function create3dsCompleteHandler(deps) {
|
|
|
1236
1257
|
}).catch(() => {
|
|
1237
1258
|
});
|
|
1238
1259
|
}
|
|
1239
|
-
return
|
|
1260
|
+
return json(
|
|
1240
1261
|
{ error: "Pago rechazado tras autenticaci\xF3n 3DS." },
|
|
1241
1262
|
{ status: 400 }
|
|
1242
1263
|
);
|
|
1243
1264
|
} catch (error) {
|
|
1244
1265
|
logger.error("3DS complete error:", error);
|
|
1245
|
-
return
|
|
1266
|
+
return json(
|
|
1246
1267
|
{ error: "Error interno del servidor" },
|
|
1247
1268
|
{ status: 500 }
|
|
1248
1269
|
);
|
|
1249
1270
|
}
|
|
1250
1271
|
};
|
|
1251
1272
|
}
|
|
1273
|
+
|
|
1274
|
+
// src/handlers/3ds-timeout.ts
|
|
1252
1275
|
function create3dsTimeoutHandler(deps) {
|
|
1253
1276
|
const logger = deps.logger ?? console;
|
|
1254
1277
|
const { db, auth } = deps.firebase;
|
|
1255
1278
|
return async function POST(request) {
|
|
1256
|
-
const sessionCookie = request
|
|
1279
|
+
const sessionCookie = getCookie(request, "__session");
|
|
1257
1280
|
if (!sessionCookie || !auth) {
|
|
1258
|
-
return
|
|
1281
|
+
return json({ error: "No autorizado" }, { status: 401 });
|
|
1259
1282
|
}
|
|
1260
1283
|
let decodedToken;
|
|
1261
1284
|
try {
|
|
1262
1285
|
decodedToken = await auth.verifySessionCookie(sessionCookie, true);
|
|
1263
1286
|
} catch {
|
|
1264
|
-
return
|
|
1287
|
+
return json({ error: "Sesion invalida" }, { status: 401 });
|
|
1265
1288
|
}
|
|
1266
1289
|
const { orderId } = await request.json();
|
|
1267
1290
|
if (!orderId || typeof orderId !== "string") {
|
|
1268
|
-
return
|
|
1291
|
+
return json({ error: "orderId requerido" }, { status: 400 });
|
|
1269
1292
|
}
|
|
1270
1293
|
const orderRef = db.collection("orders").doc(orderId);
|
|
1271
1294
|
const orderDoc = await orderRef.get();
|
|
1272
1295
|
if (!orderDoc.exists) {
|
|
1273
|
-
return
|
|
1296
|
+
return json({ error: "Orden no encontrada" }, { status: 404 });
|
|
1274
1297
|
}
|
|
1275
1298
|
const orderData = orderDoc.data() ?? {};
|
|
1276
1299
|
if (decodedToken.uid !== orderData.userId) {
|
|
1277
|
-
return
|
|
1300
|
+
return json({ error: "Usuario no coincide" }, { status: 403 });
|
|
1278
1301
|
}
|
|
1279
1302
|
if (orderData.status !== "3ds-pending") {
|
|
1280
|
-
return
|
|
1303
|
+
return json({ alreadyResolved: true });
|
|
1281
1304
|
}
|
|
1282
1305
|
await orderRef.update({
|
|
1283
1306
|
status: "failed",
|
|
@@ -1298,48 +1321,50 @@ function create3dsTimeoutHandler(deps) {
|
|
|
1298
1321
|
...retryUrl ? { retryUrl } : {}
|
|
1299
1322
|
}).catch((err) => logger.error("[3ds-timeout] Failed to send email:", err));
|
|
1300
1323
|
}
|
|
1301
|
-
return
|
|
1324
|
+
return json({ ok: true });
|
|
1302
1325
|
};
|
|
1303
1326
|
}
|
|
1327
|
+
|
|
1328
|
+
// src/handlers/refund.ts
|
|
1304
1329
|
function createRefundHandler(deps) {
|
|
1305
1330
|
const logger = deps.logger ?? console;
|
|
1306
1331
|
const { db, auth } = deps.firebase;
|
|
1307
1332
|
return async function POST(request) {
|
|
1308
1333
|
try {
|
|
1309
|
-
const sessionCookie = request
|
|
1334
|
+
const sessionCookie = getCookie(request, "__session");
|
|
1310
1335
|
if (!sessionCookie || !auth) {
|
|
1311
|
-
return
|
|
1336
|
+
return json({ error: "No autorizado" }, { status: 401 });
|
|
1312
1337
|
}
|
|
1313
1338
|
let decodedToken;
|
|
1314
1339
|
try {
|
|
1315
1340
|
decodedToken = await auth.verifySessionCookie(sessionCookie, true);
|
|
1316
1341
|
} catch {
|
|
1317
|
-
return
|
|
1342
|
+
return json({ error: "Sesi\xF3n inv\xE1lida" }, { status: 401 });
|
|
1318
1343
|
}
|
|
1319
1344
|
const { orderId } = await request.json();
|
|
1320
1345
|
if (!orderId) {
|
|
1321
|
-
return
|
|
1346
|
+
return json({ error: "orderId requerido" }, { status: 400 });
|
|
1322
1347
|
}
|
|
1323
1348
|
const orderDoc = await db.collection("orders").doc(orderId).get();
|
|
1324
1349
|
if (!orderDoc.exists) {
|
|
1325
|
-
return
|
|
1350
|
+
return json({ error: "Orden no encontrada" }, { status: 404 });
|
|
1326
1351
|
}
|
|
1327
1352
|
const order = orderDoc.data() ?? {};
|
|
1328
1353
|
if (order.userId !== decodedToken.uid) {
|
|
1329
|
-
return
|
|
1354
|
+
return json(
|
|
1330
1355
|
{ error: "No autorizado para esta orden" },
|
|
1331
1356
|
{ status: 403 }
|
|
1332
1357
|
);
|
|
1333
1358
|
}
|
|
1334
1359
|
if (order.status !== "paid") {
|
|
1335
|
-
return
|
|
1360
|
+
return json(
|
|
1336
1361
|
{ error: "Solo se pueden reembolsar \xF3rdenes pagadas" },
|
|
1337
1362
|
{ status: 400 }
|
|
1338
1363
|
);
|
|
1339
1364
|
}
|
|
1340
1365
|
const paymentTransactionId = order.paymentTransactionId;
|
|
1341
1366
|
if (!paymentTransactionId) {
|
|
1342
|
-
return
|
|
1367
|
+
return json(
|
|
1343
1368
|
{ error: "No se encontr\xF3 ID de transacci\xF3n" },
|
|
1344
1369
|
{ status: 400 }
|
|
1345
1370
|
);
|
|
@@ -1361,15 +1386,15 @@ function createRefundHandler(deps) {
|
|
|
1361
1386
|
);
|
|
1362
1387
|
}
|
|
1363
1388
|
}
|
|
1364
|
-
return
|
|
1389
|
+
return json({ success: true, detail: result.detail });
|
|
1365
1390
|
}
|
|
1366
|
-
return
|
|
1391
|
+
return json(
|
|
1367
1392
|
{ error: result.detail || "Error al procesar reembolso" },
|
|
1368
1393
|
{ status: 400 }
|
|
1369
1394
|
);
|
|
1370
1395
|
} catch (error) {
|
|
1371
1396
|
logger.error("Refund error:", error);
|
|
1372
|
-
return
|
|
1397
|
+
return json(
|
|
1373
1398
|
{ error: "Error interno del servidor" },
|
|
1374
1399
|
{ status: 500 }
|
|
1375
1400
|
);
|
|
@@ -1380,20 +1405,20 @@ function createInitCheckoutHandler(deps) {
|
|
|
1380
1405
|
const logger = deps.logger ?? console;
|
|
1381
1406
|
const { auth } = deps.firebase;
|
|
1382
1407
|
return async function POST(request) {
|
|
1383
|
-
const sessionCookie = request
|
|
1408
|
+
const sessionCookie = getCookie(request, "__session");
|
|
1384
1409
|
if (!sessionCookie || !auth) {
|
|
1385
|
-
return
|
|
1410
|
+
return json({ error: "No autorizado" }, { status: 401 });
|
|
1386
1411
|
}
|
|
1387
1412
|
let decodedToken;
|
|
1388
1413
|
try {
|
|
1389
1414
|
decodedToken = await auth.verifySessionCookie(sessionCookie, true);
|
|
1390
1415
|
} catch {
|
|
1391
|
-
return
|
|
1416
|
+
return json({ error: "Sesi\xF3n inv\xE1lida" }, { status: 401 });
|
|
1392
1417
|
}
|
|
1393
1418
|
try {
|
|
1394
1419
|
const { amount, vat, description, devReference } = await request.json();
|
|
1395
1420
|
if (!amount || !devReference) {
|
|
1396
|
-
return
|
|
1421
|
+
return json(
|
|
1397
1422
|
{ error: "amount y devReference son requeridos" },
|
|
1398
1423
|
{ status: 400 }
|
|
1399
1424
|
);
|
|
@@ -1421,36 +1446,38 @@ function createInitCheckoutHandler(deps) {
|
|
|
1421
1446
|
}
|
|
1422
1447
|
});
|
|
1423
1448
|
if (result.error) {
|
|
1424
|
-
return
|
|
1449
|
+
return json(
|
|
1425
1450
|
{ error: result.error.description || "Error al inicializar checkout" },
|
|
1426
1451
|
{ status: 400 }
|
|
1427
1452
|
);
|
|
1428
1453
|
}
|
|
1429
1454
|
if (!result.reference) {
|
|
1430
|
-
return
|
|
1455
|
+
return json(
|
|
1431
1456
|
{ error: "No se recibi\xF3 referencia de checkout" },
|
|
1432
1457
|
{ status: 500 }
|
|
1433
1458
|
);
|
|
1434
1459
|
}
|
|
1435
|
-
return
|
|
1460
|
+
return json({
|
|
1436
1461
|
reference: result.reference,
|
|
1437
1462
|
checkoutUrl: result.checkout_url
|
|
1438
1463
|
});
|
|
1439
1464
|
} catch (error) {
|
|
1440
1465
|
logger.error("Init checkout error:", error);
|
|
1441
|
-
return
|
|
1466
|
+
return json(
|
|
1442
1467
|
{ error: "Error al inicializar checkout" },
|
|
1443
1468
|
{ status: 500 }
|
|
1444
1469
|
);
|
|
1445
1470
|
}
|
|
1446
1471
|
};
|
|
1447
1472
|
}
|
|
1473
|
+
|
|
1474
|
+
// src/handlers/cards.ts
|
|
1448
1475
|
function createCardsHandler(deps) {
|
|
1449
1476
|
const logger = deps.logger ?? console;
|
|
1450
1477
|
const { db, auth } = deps.firebase;
|
|
1451
1478
|
const verifiedCardsEnabled = deps.enableVerifiedCardsTracking !== false;
|
|
1452
1479
|
async function verifySession(request) {
|
|
1453
|
-
const sessionCookie = request
|
|
1480
|
+
const sessionCookie = getCookie(request, "__session");
|
|
1454
1481
|
if (!sessionCookie || !auth) return null;
|
|
1455
1482
|
try {
|
|
1456
1483
|
return await auth.verifySessionCookie(sessionCookie, true);
|
|
@@ -1461,7 +1488,7 @@ function createCardsHandler(deps) {
|
|
|
1461
1488
|
const GET = async function GET2(request) {
|
|
1462
1489
|
const decoded = await verifySession(request);
|
|
1463
1490
|
if (!decoded) {
|
|
1464
|
-
return
|
|
1491
|
+
return json({ error: "No autorizado" }, { status: 401 });
|
|
1465
1492
|
}
|
|
1466
1493
|
try {
|
|
1467
1494
|
const result = await listCards(decoded.uid);
|
|
@@ -1485,12 +1512,12 @@ function createCardsHandler(deps) {
|
|
|
1485
1512
|
const enrichedCards = cards.map(
|
|
1486
1513
|
(c) => c.status === "review" && verifiedTokens.has(c.token) ? { ...c, status: "valid" } : c
|
|
1487
1514
|
);
|
|
1488
|
-
return
|
|
1515
|
+
return json({ cards: enrichedCards });
|
|
1489
1516
|
}
|
|
1490
|
-
return
|
|
1517
|
+
return json({ cards });
|
|
1491
1518
|
} catch (error) {
|
|
1492
1519
|
logger.error("Error listing cards:", error);
|
|
1493
|
-
return
|
|
1520
|
+
return json(
|
|
1494
1521
|
{ error: "Error al obtener tarjetas" },
|
|
1495
1522
|
{ status: 500 }
|
|
1496
1523
|
);
|
|
@@ -1499,12 +1526,12 @@ function createCardsHandler(deps) {
|
|
|
1499
1526
|
const DELETE = async function DELETE2(request) {
|
|
1500
1527
|
const decoded = await verifySession(request);
|
|
1501
1528
|
if (!decoded) {
|
|
1502
|
-
return
|
|
1529
|
+
return json({ error: "No autorizado" }, { status: 401 });
|
|
1503
1530
|
}
|
|
1504
1531
|
try {
|
|
1505
1532
|
const { token } = await request.json();
|
|
1506
1533
|
if (!token) {
|
|
1507
|
-
return
|
|
1534
|
+
return json(
|
|
1508
1535
|
{ error: "Token de tarjeta requerido" },
|
|
1509
1536
|
{ status: 400 }
|
|
1510
1537
|
);
|
|
@@ -1514,7 +1541,7 @@ function createCardsHandler(deps) {
|
|
|
1514
1541
|
const resultWithError = result;
|
|
1515
1542
|
if (resultWithError.error) {
|
|
1516
1543
|
logger.error("[cards/DELETE] Nuvei delete failed:", JSON.stringify(result));
|
|
1517
|
-
return
|
|
1544
|
+
return json(
|
|
1518
1545
|
{ error: "No se pudo eliminar la tarjeta en Nuvei", detail: result },
|
|
1519
1546
|
{ status: 400 }
|
|
1520
1547
|
);
|
|
@@ -1525,10 +1552,10 @@ function createCardsHandler(deps) {
|
|
|
1525
1552
|
} catch {
|
|
1526
1553
|
}
|
|
1527
1554
|
}
|
|
1528
|
-
return
|
|
1555
|
+
return json(result);
|
|
1529
1556
|
} catch (error) {
|
|
1530
1557
|
logger.error("Error deleting card:", error);
|
|
1531
|
-
return
|
|
1558
|
+
return json(
|
|
1532
1559
|
{ error: "Error al eliminar tarjeta" },
|
|
1533
1560
|
{ status: 500 }
|
|
1534
1561
|
);
|
|
@@ -1536,25 +1563,27 @@ function createCardsHandler(deps) {
|
|
|
1536
1563
|
};
|
|
1537
1564
|
return { GET, DELETE };
|
|
1538
1565
|
}
|
|
1566
|
+
|
|
1567
|
+
// src/handlers/verify.ts
|
|
1539
1568
|
function createVerifyHandler(deps) {
|
|
1540
1569
|
const logger = deps.logger ?? console;
|
|
1541
1570
|
const { db, auth } = deps.firebase;
|
|
1542
1571
|
const verifiedCardsEnabled = deps.enableVerifiedCardsTracking !== false;
|
|
1543
1572
|
return async function POST(request) {
|
|
1544
|
-
const sessionCookie = request
|
|
1573
|
+
const sessionCookie = getCookie(request, "__session");
|
|
1545
1574
|
if (!sessionCookie || !auth) {
|
|
1546
|
-
return
|
|
1575
|
+
return json({ error: "No autorizado" }, { status: 401 });
|
|
1547
1576
|
}
|
|
1548
1577
|
let decodedToken;
|
|
1549
1578
|
try {
|
|
1550
1579
|
decodedToken = await auth.verifySessionCookie(sessionCookie, true);
|
|
1551
1580
|
} catch {
|
|
1552
|
-
return
|
|
1581
|
+
return json({ error: "Sesi\xF3n inv\xE1lida" }, { status: 401 });
|
|
1553
1582
|
}
|
|
1554
1583
|
try {
|
|
1555
1584
|
const { cardToken, transactionReference, value } = await request.json();
|
|
1556
1585
|
if (!cardToken && !transactionReference || !value) {
|
|
1557
|
-
return
|
|
1586
|
+
return json(
|
|
1558
1587
|
{ error: "transactionReference y value son requeridos" },
|
|
1559
1588
|
{ status: 400 }
|
|
1560
1589
|
);
|
|
@@ -1565,7 +1594,7 @@ function createVerifyHandler(deps) {
|
|
|
1565
1594
|
value
|
|
1566
1595
|
});
|
|
1567
1596
|
if (result.error) {
|
|
1568
|
-
return
|
|
1597
|
+
return json(
|
|
1569
1598
|
{ error: result.error.description || "Error de verificaci\xF3n" },
|
|
1570
1599
|
{ status: 400 }
|
|
1571
1600
|
);
|
|
@@ -1577,41 +1606,43 @@ function createVerifyHandler(deps) {
|
|
|
1577
1606
|
logger.error("Failed to persist verified card:", err);
|
|
1578
1607
|
}
|
|
1579
1608
|
}
|
|
1580
|
-
return
|
|
1609
|
+
return json({
|
|
1581
1610
|
success: true,
|
|
1582
1611
|
transaction: result.transaction
|
|
1583
1612
|
});
|
|
1584
1613
|
} catch (error) {
|
|
1585
1614
|
logger.error("Card verify error:", error);
|
|
1586
|
-
return
|
|
1615
|
+
return json(
|
|
1587
1616
|
{ error: "Error al verificar tarjeta" },
|
|
1588
1617
|
{ status: 500 }
|
|
1589
1618
|
);
|
|
1590
1619
|
}
|
|
1591
1620
|
};
|
|
1592
1621
|
}
|
|
1622
|
+
|
|
1623
|
+
// src/handlers/test-charge.ts
|
|
1593
1624
|
function createTestChargeHandler(deps) {
|
|
1594
1625
|
const logger = deps.logger ?? console;
|
|
1595
1626
|
const { auth } = deps.firebase;
|
|
1596
1627
|
const defaultDescription = deps.defaultDescription ?? "Test charge";
|
|
1597
1628
|
return async function POST(request) {
|
|
1598
1629
|
if (process.env.NUVEI_ENV === "prod") {
|
|
1599
|
-
return
|
|
1630
|
+
return json({ error: "Not available" }, { status: 404 });
|
|
1600
1631
|
}
|
|
1601
|
-
const sessionCookie = request
|
|
1632
|
+
const sessionCookie = getCookie(request, "__session");
|
|
1602
1633
|
if (!sessionCookie || !auth) {
|
|
1603
|
-
return
|
|
1634
|
+
return json({ error: "No autorizado" }, { status: 401 });
|
|
1604
1635
|
}
|
|
1605
1636
|
let decodedToken;
|
|
1606
1637
|
try {
|
|
1607
1638
|
decodedToken = await auth.verifySessionCookie(sessionCookie, true);
|
|
1608
1639
|
} catch {
|
|
1609
|
-
return
|
|
1640
|
+
return json({ error: "Sesi\xF3n inv\xE1lida" }, { status: 401 });
|
|
1610
1641
|
}
|
|
1611
1642
|
try {
|
|
1612
1643
|
const { token, amount, vat, description, devReference } = await request.json();
|
|
1613
1644
|
if (!token || !amount || !devReference) {
|
|
1614
|
-
return
|
|
1645
|
+
return json(
|
|
1615
1646
|
{ error: "token, amount y devReference son requeridos" },
|
|
1616
1647
|
{ status: 400 }
|
|
1617
1648
|
);
|
|
@@ -1627,13 +1658,13 @@ function createTestChargeHandler(deps) {
|
|
|
1627
1658
|
});
|
|
1628
1659
|
logger.log("Test charge result:", JSON.stringify(result, null, 2));
|
|
1629
1660
|
if (result.transaction && result.transaction.status === "success" && result.transaction.status_detail === 3) {
|
|
1630
|
-
return
|
|
1661
|
+
return json({
|
|
1631
1662
|
success: true,
|
|
1632
1663
|
transaction: result.transaction,
|
|
1633
1664
|
card: result.card
|
|
1634
1665
|
});
|
|
1635
1666
|
}
|
|
1636
|
-
return
|
|
1667
|
+
return json(
|
|
1637
1668
|
{
|
|
1638
1669
|
success: false,
|
|
1639
1670
|
error: result.transaction?.message || result.error?.description || "Pago rechazado",
|
|
@@ -1643,7 +1674,7 @@ function createTestChargeHandler(deps) {
|
|
|
1643
1674
|
);
|
|
1644
1675
|
} catch (error) {
|
|
1645
1676
|
logger.error("Test charge error:", error);
|
|
1646
|
-
return
|
|
1677
|
+
return json(
|
|
1647
1678
|
{ error: "Error interno del servidor" },
|
|
1648
1679
|
{ status: 500 }
|
|
1649
1680
|
);
|
|
@@ -1651,6 +1682,57 @@ function createTestChargeHandler(deps) {
|
|
|
1651
1682
|
};
|
|
1652
1683
|
}
|
|
1653
1684
|
|
|
1654
|
-
|
|
1685
|
+
// src/handlers/proxy.ts
|
|
1686
|
+
function createNuveiProxyHandler(deps = {}) {
|
|
1687
|
+
const logger = deps.logger ?? console;
|
|
1688
|
+
return async function POST(request) {
|
|
1689
|
+
if (request.method !== "POST") {
|
|
1690
|
+
return json({ error: "Method not allowed" }, { status: 405 });
|
|
1691
|
+
}
|
|
1692
|
+
let parsed;
|
|
1693
|
+
try {
|
|
1694
|
+
parsed = await request.json();
|
|
1695
|
+
} catch {
|
|
1696
|
+
return json({ error: "Invalid JSON body" }, { status: 400 });
|
|
1697
|
+
}
|
|
1698
|
+
const { path, method, body: nuveiBody } = parsed;
|
|
1699
|
+
const authToken = request.headers.get("x-nuvei-auth-token");
|
|
1700
|
+
if (!path || !method || !authToken) {
|
|
1701
|
+
return json(
|
|
1702
|
+
{ error: "Missing path, method, or x-nuvei-auth-token header" },
|
|
1703
|
+
{ status: 400 }
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
const env = request.headers.get("x-nuvei-env") || "prod";
|
|
1707
|
+
const baseUrl = env === "prod" ? "https://ccapi.paymentez.com" : "https://ccapi-stg.paymentez.com";
|
|
1708
|
+
const url = `${baseUrl}${path}`;
|
|
1709
|
+
try {
|
|
1710
|
+
const options = {
|
|
1711
|
+
method,
|
|
1712
|
+
headers: {
|
|
1713
|
+
"Content-Type": "application/json",
|
|
1714
|
+
"Auth-Token": authToken
|
|
1715
|
+
}
|
|
1716
|
+
};
|
|
1717
|
+
if (nuveiBody && method === "POST") {
|
|
1718
|
+
options.body = JSON.stringify(nuveiBody);
|
|
1719
|
+
}
|
|
1720
|
+
const response = await fetch(url, options);
|
|
1721
|
+
const responseBody = await response.text();
|
|
1722
|
+
return new Response(responseBody, {
|
|
1723
|
+
status: response.status,
|
|
1724
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
1725
|
+
});
|
|
1726
|
+
} catch (err) {
|
|
1727
|
+
logger.error("[nuveiProxy] Error:", err);
|
|
1728
|
+
return json(
|
|
1729
|
+
{ error: "Proxy error: " + (err instanceof Error ? err.message : String(err)) },
|
|
1730
|
+
{ status: 500 }
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
export { create3dsCallbackHandler, create3dsCompleteHandler, create3dsTimeoutHandler, createCardsHandler, createChargeHandler, createInitCheckoutHandler, createNuveiProxyHandler, createRefundHandler, createTestChargeHandler, createVerifyHandler, createWebhookHandler };
|
|
1655
1737
|
//# sourceMappingURL=index.js.map
|
|
1656
1738
|
//# sourceMappingURL=index.js.map
|