@pandait.tech/payment-nuvei 0.1.1 → 0.2.1
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 +273 -132
- package/dist/handlers/index.cjs.map +1 -1
- package/dist/handlers/index.d.cts +34 -121
- package/dist/handlers/index.d.ts +34 -121
- package/dist/handlers/index.js +273 -133
- package/dist/handlers/index.js.map +1 -1
- package/package.json +17 -9
package/dist/handlers/index.cjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var server = require('next/server');
|
|
4
3
|
var firestore = require('firebase-admin/firestore');
|
|
5
4
|
var crypto = require('crypto');
|
|
6
5
|
var zod = require('zod');
|
|
@@ -9,7 +8,27 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
|
9
8
|
|
|
10
9
|
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
11
10
|
|
|
12
|
-
// src/
|
|
11
|
+
// src/http.ts
|
|
12
|
+
function json(data, init) {
|
|
13
|
+
const headers = new Headers(init?.headers);
|
|
14
|
+
if (!headers.has("content-type")) {
|
|
15
|
+
headers.set("content-type", "application/json; charset=utf-8");
|
|
16
|
+
}
|
|
17
|
+
return new Response(JSON.stringify(data), { ...init, headers });
|
|
18
|
+
}
|
|
19
|
+
function getCookie(request, name) {
|
|
20
|
+
const header = request.headers.get("cookie");
|
|
21
|
+
if (!header) return void 0;
|
|
22
|
+
for (const part of header.split(";")) {
|
|
23
|
+
const eq = part.indexOf("=");
|
|
24
|
+
if (eq === -1) continue;
|
|
25
|
+
const key = part.slice(0, eq).trim();
|
|
26
|
+
if (key === name) {
|
|
27
|
+
return decodeURIComponent(part.slice(eq + 1).trim());
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
13
32
|
var NUVEI_DOMAIN = "paymentez.com";
|
|
14
33
|
function getBaseUrl() {
|
|
15
34
|
const env = process.env.NUVEI_ENV === "prod" ? "prod" : "stg";
|
|
@@ -212,15 +231,15 @@ function createChargeHandler(deps) {
|
|
|
212
231
|
const { db, auth } = deps.firebase;
|
|
213
232
|
return async function POST(request) {
|
|
214
233
|
try {
|
|
215
|
-
const sessionCookie = request
|
|
234
|
+
const sessionCookie = getCookie(request, "__session");
|
|
216
235
|
if (!sessionCookie || !auth) {
|
|
217
|
-
return
|
|
236
|
+
return json({ error: "No autorizado" }, { status: 401 });
|
|
218
237
|
}
|
|
219
238
|
let decodedToken;
|
|
220
239
|
try {
|
|
221
240
|
decodedToken = await auth.verifySessionCookie(sessionCookie, true);
|
|
222
241
|
} catch {
|
|
223
|
-
return
|
|
242
|
+
return json({ error: "Sesion invalida" }, { status: 401 });
|
|
224
243
|
}
|
|
225
244
|
const clientIpForLimit = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-real-ip") || "unknown";
|
|
226
245
|
if (deps.rateLimit) {
|
|
@@ -230,7 +249,7 @@ function createChargeHandler(deps) {
|
|
|
230
249
|
15 * 60 * 1e3
|
|
231
250
|
);
|
|
232
251
|
if (!allowed) {
|
|
233
|
-
return
|
|
252
|
+
return json(
|
|
234
253
|
{ error: "Demasiados intentos. Intent\xE1 de nuevo en unos minutos." },
|
|
235
254
|
{ status: 429 }
|
|
236
255
|
);
|
|
@@ -240,7 +259,7 @@ function createChargeHandler(deps) {
|
|
|
240
259
|
if (deps.turnstile) {
|
|
241
260
|
const turnstileToken = body.turnstileToken;
|
|
242
261
|
if (!turnstileToken) {
|
|
243
|
-
return
|
|
262
|
+
return json(
|
|
244
263
|
{ error: "Verificaci\xF3n de seguridad faltante. Recarg\xE1 la p\xE1gina." },
|
|
245
264
|
{ status: 403 }
|
|
246
265
|
);
|
|
@@ -250,7 +269,7 @@ function createChargeHandler(deps) {
|
|
|
250
269
|
clientIpForLimit === "unknown" ? void 0 : clientIpForLimit
|
|
251
270
|
);
|
|
252
271
|
if (!turnstileResult.success) {
|
|
253
|
-
return
|
|
272
|
+
return json(
|
|
254
273
|
{ error: "No pudimos verificar que sos humano. Recarg\xE1 la p\xE1gina." },
|
|
255
274
|
{ status: 403 }
|
|
256
275
|
);
|
|
@@ -276,18 +295,18 @@ function createChargeHandler(deps) {
|
|
|
276
295
|
userId: userId?.substring(0, 8) + "..."
|
|
277
296
|
});
|
|
278
297
|
if (!token || !orderId || !amount) {
|
|
279
|
-
return
|
|
298
|
+
return json({ error: "Datos incompletos" }, { status: 400 });
|
|
280
299
|
}
|
|
281
300
|
if (decodedToken.uid !== userId) {
|
|
282
|
-
return
|
|
301
|
+
return json({ error: "Usuario no coincide" }, { status: 403 });
|
|
283
302
|
}
|
|
284
303
|
const orderDoc = await db.collection("orders").doc(orderId).get();
|
|
285
304
|
if (!orderDoc.exists) {
|
|
286
|
-
return
|
|
305
|
+
return json({ error: "Orden no encontrada" }, { status: 404 });
|
|
287
306
|
}
|
|
288
307
|
const orderData = orderDoc.data() ?? {};
|
|
289
308
|
if (orderData.status !== "pending") {
|
|
290
|
-
return
|
|
309
|
+
return json(
|
|
291
310
|
{ error: "Esta orden ya fue procesada" },
|
|
292
311
|
{ status: 409 }
|
|
293
312
|
);
|
|
@@ -301,7 +320,7 @@ function createChargeHandler(deps) {
|
|
|
301
320
|
});
|
|
302
321
|
if (customResult.handled) {
|
|
303
322
|
if (!customResult.valid) {
|
|
304
|
-
return
|
|
323
|
+
return json(
|
|
305
324
|
{ error: customResult.error },
|
|
306
325
|
{ status: customResult.status }
|
|
307
326
|
);
|
|
@@ -315,7 +334,7 @@ function createChargeHandler(deps) {
|
|
|
315
334
|
for (const item of orderItems) {
|
|
316
335
|
const productDoc = await db.collection("products").doc(item.productId).get();
|
|
317
336
|
if (!productDoc.exists || !productDoc.data()?.isActive) {
|
|
318
|
-
return
|
|
337
|
+
return json(
|
|
319
338
|
{ error: `El producto "${item.name}" ya no est\xE1 disponible` },
|
|
320
339
|
{ status: 409 }
|
|
321
340
|
);
|
|
@@ -324,7 +343,7 @@ function createChargeHandler(deps) {
|
|
|
324
343
|
const isDigital = productData.isDigital === true;
|
|
325
344
|
const stock = productData.stock ?? 0;
|
|
326
345
|
if (!isDigital && stock < item.quantity) {
|
|
327
|
-
return
|
|
346
|
+
return json(
|
|
328
347
|
{
|
|
329
348
|
error: `Stock insuficiente para "${item.name}" (disponible: ${stock})`
|
|
330
349
|
},
|
|
@@ -337,7 +356,7 @@ function createChargeHandler(deps) {
|
|
|
337
356
|
});
|
|
338
357
|
const expectedItemSubtotal = display.finalSubtotal;
|
|
339
358
|
if (Math.abs(expectedItemSubtotal - item.price) > 0.02) {
|
|
340
|
-
return
|
|
359
|
+
return json(
|
|
341
360
|
{
|
|
342
361
|
error: `El precio de "${item.name}" cambi\xF3. Recarg\xE1 la p\xE1gina para ver el precio actualizado.`
|
|
343
362
|
},
|
|
@@ -352,7 +371,7 @@ function createChargeHandler(deps) {
|
|
|
352
371
|
if (orderPromotionId && orderDiscount > 0) {
|
|
353
372
|
const promoDoc = await db.collection("promotions").doc(orderPromotionId).get();
|
|
354
373
|
if (!promoDoc.exists || !promoDoc.data()?.isActive) {
|
|
355
|
-
return
|
|
374
|
+
return json(
|
|
356
375
|
{ error: "El cupon aplicado ya no es valido. Remuevelo y vuelve a intentar." },
|
|
357
376
|
{ status: 409 }
|
|
358
377
|
);
|
|
@@ -362,13 +381,13 @@ function createChargeHandler(deps) {
|
|
|
362
381
|
const validFrom = promo.rules.validFrom.toDate ? promo.rules.validFrom.toDate() : new Date(promo.rules.validFrom);
|
|
363
382
|
const validUntil = promo.rules.validUntil.toDate ? promo.rules.validUntil.toDate() : new Date(promo.rules.validUntil);
|
|
364
383
|
if (now < validFrom || now > validUntil) {
|
|
365
|
-
return
|
|
384
|
+
return json(
|
|
366
385
|
{ error: "El cupon aplicado ha expirado. Remuevelo y vuelve a intentar." },
|
|
367
386
|
{ status: 409 }
|
|
368
387
|
);
|
|
369
388
|
}
|
|
370
389
|
if (promo.rules.maxTotalUses && promo.currentUses >= promo.rules.maxTotalUses) {
|
|
371
|
-
return
|
|
390
|
+
return json(
|
|
372
391
|
{ error: "El cupon aplicado ya alcanzo su limite de usos." },
|
|
373
392
|
{ status: 409 }
|
|
374
393
|
);
|
|
@@ -387,7 +406,7 @@ function createChargeHandler(deps) {
|
|
|
387
406
|
const verifiedVat = Math.round(verifiedDiscountedSubtotal * 0.15 * 100) / 100;
|
|
388
407
|
const verifiedTotal = Math.round((verifiedDiscountedSubtotal + verifiedVat) * 100) / 100;
|
|
389
408
|
if (verifiedTotal !== amount) {
|
|
390
|
-
return
|
|
409
|
+
return json(
|
|
391
410
|
{
|
|
392
411
|
error: "El monto no coincide con los precios actuales. Actualiza tu carrito."
|
|
393
412
|
},
|
|
@@ -510,7 +529,7 @@ function createChargeHandler(deps) {
|
|
|
510
529
|
(err) => logger.error("[charge] Failed to delete card after payment:", err)
|
|
511
530
|
);
|
|
512
531
|
}
|
|
513
|
-
return
|
|
532
|
+
return json({
|
|
514
533
|
success: true,
|
|
515
534
|
transactionId: nuveiData.transaction.id,
|
|
516
535
|
authorizationCode: hasValidAuthCode ? rawAuthCode : null,
|
|
@@ -525,7 +544,7 @@ function createChargeHandler(deps) {
|
|
|
525
544
|
chargeResponseAt: /* @__PURE__ */ new Date(),
|
|
526
545
|
updatedAt: /* @__PURE__ */ new Date()
|
|
527
546
|
});
|
|
528
|
-
return
|
|
547
|
+
return json({
|
|
529
548
|
review: true,
|
|
530
549
|
orderId,
|
|
531
550
|
transactionId: nuveiData.transaction.id
|
|
@@ -542,7 +561,7 @@ function createChargeHandler(deps) {
|
|
|
542
561
|
...persistDeleteOnPaid,
|
|
543
562
|
updatedAt: /* @__PURE__ */ new Date()
|
|
544
563
|
});
|
|
545
|
-
return
|
|
564
|
+
return json({
|
|
546
565
|
challenge: true,
|
|
547
566
|
challengeHtml: hiddenIframeHtml,
|
|
548
567
|
isDeviceFingerprint: true,
|
|
@@ -558,7 +577,7 @@ function createChargeHandler(deps) {
|
|
|
558
577
|
...persistDeleteOnPaid,
|
|
559
578
|
updatedAt: /* @__PURE__ */ new Date()
|
|
560
579
|
});
|
|
561
|
-
return
|
|
580
|
+
return json({
|
|
562
581
|
otpRequired: true,
|
|
563
582
|
orderId,
|
|
564
583
|
nuveiTransactionId: nuveiData.transaction.id,
|
|
@@ -581,7 +600,7 @@ function createChargeHandler(deps) {
|
|
|
581
600
|
...persistDeleteOnPaid,
|
|
582
601
|
updatedAt: /* @__PURE__ */ new Date()
|
|
583
602
|
});
|
|
584
|
-
return
|
|
603
|
+
return json({
|
|
585
604
|
challenge: true,
|
|
586
605
|
challengeHtml,
|
|
587
606
|
isDeviceFingerprint: false,
|
|
@@ -616,7 +635,7 @@ function createChargeHandler(deps) {
|
|
|
616
635
|
}).catch(() => {
|
|
617
636
|
});
|
|
618
637
|
}
|
|
619
|
-
return
|
|
638
|
+
return json({ error: threeDSErrorMsg }, { status: 400 });
|
|
620
639
|
}
|
|
621
640
|
const failedErrorMsg = getNuveiUserMessage(
|
|
622
641
|
nuveiData.transaction?.status_detail,
|
|
@@ -644,10 +663,10 @@ function createChargeHandler(deps) {
|
|
|
644
663
|
}).catch(() => {
|
|
645
664
|
});
|
|
646
665
|
}
|
|
647
|
-
return
|
|
666
|
+
return json({ error: failedErrorMsg }, { status: 400 });
|
|
648
667
|
} catch (error) {
|
|
649
668
|
logger.error("Payment charge error:", error);
|
|
650
|
-
return
|
|
669
|
+
return json(
|
|
651
670
|
{ error: "Error interno del servidor" },
|
|
652
671
|
{ status: 500 }
|
|
653
672
|
);
|
|
@@ -681,7 +700,7 @@ function createWebhookHandler(deps) {
|
|
|
681
700
|
logger.log("[webhook] Full payload:", JSON.stringify(payload));
|
|
682
701
|
const { transaction } = payload;
|
|
683
702
|
if (!transaction?.id || !transaction?.dev_reference) {
|
|
684
|
-
return
|
|
703
|
+
return json({ error: "Payload inv\xE1lido" }, { status: 400 });
|
|
685
704
|
}
|
|
686
705
|
const incomingCres = extractCres(payload);
|
|
687
706
|
if (incomingCres) {
|
|
@@ -703,7 +722,7 @@ function createWebhookHandler(deps) {
|
|
|
703
722
|
const orderDoc = await orderRef.get();
|
|
704
723
|
if (!orderDoc.exists) {
|
|
705
724
|
logger.error(`Webhook: Order ${orderId} not found`);
|
|
706
|
-
return
|
|
725
|
+
return json({ error: "Orden no encontrada" }, { status: 404 });
|
|
707
726
|
}
|
|
708
727
|
const orderDataEarly = orderDoc.data() ?? {};
|
|
709
728
|
if (incomingCres && orderDataEarly.status === "3ds-pending" && !orderDataEarly.verifyCalledAt) {
|
|
@@ -797,7 +816,7 @@ function createWebhookHandler(deps) {
|
|
|
797
816
|
);
|
|
798
817
|
}
|
|
799
818
|
}
|
|
800
|
-
return
|
|
819
|
+
return json({ received: true, verifiedFromWebhook: true });
|
|
801
820
|
}
|
|
802
821
|
} catch (err) {
|
|
803
822
|
if (err.message !== "ALREADY_CALLED") {
|
|
@@ -891,17 +910,55 @@ function createWebhookHandler(deps) {
|
|
|
891
910
|
);
|
|
892
911
|
}
|
|
893
912
|
}
|
|
894
|
-
return
|
|
913
|
+
return json({ received: true });
|
|
895
914
|
} catch (error) {
|
|
896
915
|
logger.error("Webhook processing error:", error);
|
|
897
|
-
return
|
|
916
|
+
return json(
|
|
898
917
|
{ error: "Error procesando webhook" },
|
|
899
918
|
{ status: 500 }
|
|
900
919
|
);
|
|
901
920
|
}
|
|
902
921
|
};
|
|
903
922
|
}
|
|
923
|
+
|
|
924
|
+
// src/handlers/3ds-callback.ts
|
|
925
|
+
function pickCres(src) {
|
|
926
|
+
for (const key of Object.keys(src)) {
|
|
927
|
+
const k = key.toLowerCase();
|
|
928
|
+
if (k === "cres" || k === "cres_value" || k === "cresvalue" || k === "value" || k === "param.value" || k === "paramvalue") {
|
|
929
|
+
const v = src[key];
|
|
930
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
return src.cres || src.CRes || src.CRES || src.value || "";
|
|
934
|
+
}
|
|
935
|
+
function transStatusFromCres(cres, logger) {
|
|
936
|
+
try {
|
|
937
|
+
const base64 = cres.replace(/-/g, "+").replace(/_/g, "/");
|
|
938
|
+
const pad = base64.length % 4;
|
|
939
|
+
const padded = pad ? base64 + "=".repeat(4 - pad) : base64;
|
|
940
|
+
const decoded = JSON.parse(
|
|
941
|
+
Buffer.from(padded, "base64").toString("utf-8")
|
|
942
|
+
);
|
|
943
|
+
if (decoded && typeof decoded.transStatus === "string") {
|
|
944
|
+
return decoded.transStatus;
|
|
945
|
+
}
|
|
946
|
+
} catch (e) {
|
|
947
|
+
logger.warn(
|
|
948
|
+
"[3ds-callback] Could not decode CRES payload:",
|
|
949
|
+
e instanceof Error ? e.message : e
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
return null;
|
|
953
|
+
}
|
|
954
|
+
function paramsToObject(params) {
|
|
955
|
+
const obj = {};
|
|
956
|
+
for (const [k, v] of params.entries()) obj[k] = v;
|
|
957
|
+
return obj;
|
|
958
|
+
}
|
|
904
959
|
function buildCompletionPage(orderId, transStatus) {
|
|
960
|
+
const safeOrderId = String(orderId).replace(/[^a-zA-Z0-9_-]/g, "");
|
|
961
|
+
const safeTransStatus = String(transStatus).replace(/[^a-zA-Z0-9]/g, "");
|
|
905
962
|
const html = `<!DOCTYPE html>
|
|
906
963
|
<html><head><meta charset="utf-8"><title>3DS Verification</title></head>
|
|
907
964
|
<body>
|
|
@@ -909,8 +966,8 @@ function buildCompletionPage(orderId, transStatus) {
|
|
|
909
966
|
(function() {
|
|
910
967
|
var message = {
|
|
911
968
|
type: "3DS_COMPLETE",
|
|
912
|
-
orderId: "${
|
|
913
|
-
transStatus: "${
|
|
969
|
+
orderId: "${safeOrderId}",
|
|
970
|
+
transStatus: "${safeTransStatus}"
|
|
914
971
|
};
|
|
915
972
|
try {
|
|
916
973
|
if (window.parent && window.parent !== window) {
|
|
@@ -925,7 +982,7 @@ function buildCompletionPage(orderId, transStatus) {
|
|
|
925
982
|
Verificando autenticación...
|
|
926
983
|
</p>
|
|
927
984
|
</body></html>`;
|
|
928
|
-
return new
|
|
985
|
+
return new Response(html, {
|
|
929
986
|
status: 200,
|
|
930
987
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
931
988
|
});
|
|
@@ -933,46 +990,68 @@ Verificando autenticación...
|
|
|
933
990
|
function create3dsCallbackHandler(deps) {
|
|
934
991
|
const logger = deps.logger ?? console;
|
|
935
992
|
const { db } = deps.firebase;
|
|
936
|
-
async function
|
|
937
|
-
if (orderId
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
993
|
+
async function persist(orderId, cres, transStatus) {
|
|
994
|
+
if (!orderId) return;
|
|
995
|
+
try {
|
|
996
|
+
const ref = db.collection("orders").doc(orderId);
|
|
997
|
+
const snap = await ref.get();
|
|
998
|
+
const status = snap.data()?.status;
|
|
999
|
+
if (!snap.exists || status !== "3ds-pending") {
|
|
1000
|
+
logger.warn(
|
|
1001
|
+
"[3ds-callback] rejected: order not in 3ds-pending state",
|
|
1002
|
+
{ orderId, status }
|
|
1003
|
+
);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const update = { updatedAt: /* @__PURE__ */ new Date() };
|
|
1007
|
+
if (cres) update.threeDSCres = cres;
|
|
1008
|
+
if (transStatus) update.threeDSTransStatus = transStatus;
|
|
1009
|
+
await ref.update(update);
|
|
1010
|
+
} catch (err) {
|
|
1011
|
+
logger.error("[3ds-callback] failed to store cres on order:", err);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
function resolveTransStatus(cres, fallback) {
|
|
1015
|
+
if (cres) {
|
|
1016
|
+
const fromCres = transStatusFromCres(cres, logger);
|
|
1017
|
+
if (fromCres) return fromCres;
|
|
946
1018
|
}
|
|
1019
|
+
return fallback;
|
|
947
1020
|
}
|
|
948
1021
|
const POST = async function POST2(request) {
|
|
949
|
-
const
|
|
950
|
-
const orderId = searchParams.get("orderId") || "";
|
|
1022
|
+
const orderId = new URL(request.url).searchParams.get("orderId") || "";
|
|
951
1023
|
let transStatus = "U";
|
|
952
1024
|
let cres = "";
|
|
953
1025
|
try {
|
|
954
1026
|
const contentType = request.headers.get("content-type") || "";
|
|
1027
|
+
const raw = await request.text();
|
|
955
1028
|
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
cres = params.get("cres") || params.get("CRes") || "";
|
|
1029
|
+
const obj = paramsToObject(new URLSearchParams(raw));
|
|
1030
|
+
transStatus = obj.transStatus || obj.TransStatus || "U";
|
|
1031
|
+
cres = pickCres(obj);
|
|
960
1032
|
} else {
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1033
|
+
try {
|
|
1034
|
+
const body = JSON.parse(raw);
|
|
1035
|
+
transStatus = body.transStatus || body.TransStatus || "U";
|
|
1036
|
+
cres = pickCres(body);
|
|
1037
|
+
} catch {
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (!cres && raw) {
|
|
1041
|
+
cres = pickCres(paramsToObject(new URLSearchParams(raw)));
|
|
964
1042
|
}
|
|
965
1043
|
} catch {
|
|
966
1044
|
}
|
|
967
|
-
|
|
1045
|
+
transStatus = resolveTransStatus(cres, transStatus);
|
|
1046
|
+
await persist(orderId, cres, transStatus);
|
|
968
1047
|
return buildCompletionPage(orderId, transStatus);
|
|
969
1048
|
};
|
|
970
1049
|
const GET = async function GET2(request) {
|
|
971
|
-
const
|
|
972
|
-
const orderId =
|
|
973
|
-
const
|
|
974
|
-
const
|
|
975
|
-
await
|
|
1050
|
+
const params = new URL(request.url).searchParams;
|
|
1051
|
+
const orderId = params.get("orderId") || "";
|
|
1052
|
+
const cres = params.get("cres") || params.get("CRes") || params.get("value") || "";
|
|
1053
|
+
const transStatus = resolveTransStatus(cres, params.get("transStatus") || "Y");
|
|
1054
|
+
await persist(orderId, cres, transStatus);
|
|
976
1055
|
return buildCompletionPage(orderId, transStatus);
|
|
977
1056
|
};
|
|
978
1057
|
return { POST, GET };
|
|
@@ -989,32 +1068,32 @@ function create3dsCompleteHandler(deps) {
|
|
|
989
1068
|
const { db, auth } = deps.firebase;
|
|
990
1069
|
return async function POST(request) {
|
|
991
1070
|
try {
|
|
992
|
-
const sessionCookie = request
|
|
1071
|
+
const sessionCookie = getCookie(request, "__session");
|
|
993
1072
|
if (!sessionCookie || !auth) {
|
|
994
|
-
return
|
|
1073
|
+
return json({ error: "No autorizado" }, { status: 401 });
|
|
995
1074
|
}
|
|
996
1075
|
let decodedToken;
|
|
997
1076
|
try {
|
|
998
1077
|
decodedToken = await auth.verifySessionCookie(sessionCookie, true);
|
|
999
1078
|
} catch {
|
|
1000
|
-
return
|
|
1079
|
+
return json({ error: "Sesion invalida" }, { status: 401 });
|
|
1001
1080
|
}
|
|
1002
1081
|
const rawBody = await request.json();
|
|
1003
1082
|
const parsed = ThreeDSCompleteSchema.safeParse(rawBody);
|
|
1004
1083
|
if (!parsed.success) {
|
|
1005
|
-
return
|
|
1084
|
+
return json({ error: "Datos incompletos" }, { status: 400 });
|
|
1006
1085
|
}
|
|
1007
1086
|
const { orderId, userId, type, nuveiTransactionId: bodyTxId, otpCode } = parsed.data;
|
|
1008
1087
|
if (decodedToken.uid !== userId) {
|
|
1009
|
-
return
|
|
1088
|
+
return json({ error: "Usuario no coincide" }, { status: 403 });
|
|
1010
1089
|
}
|
|
1011
1090
|
const orderDoc = await db.collection("orders").doc(orderId).get();
|
|
1012
1091
|
if (!orderDoc.exists) {
|
|
1013
|
-
return
|
|
1092
|
+
return json({ error: "Orden no encontrada" }, { status: 404 });
|
|
1014
1093
|
}
|
|
1015
1094
|
const orderData = orderDoc.data() ?? {};
|
|
1016
1095
|
if (orderData.status === "paid") {
|
|
1017
|
-
return
|
|
1096
|
+
return json({
|
|
1018
1097
|
success: true,
|
|
1019
1098
|
transactionId: orderData.paymentTransactionId,
|
|
1020
1099
|
authorizationCode: orderData.authorizationCode || null,
|
|
@@ -1025,10 +1104,10 @@ function create3dsCompleteHandler(deps) {
|
|
|
1025
1104
|
logger.log(
|
|
1026
1105
|
`[3ds-complete] verify already called for order ${orderId}, returning current state`
|
|
1027
1106
|
);
|
|
1028
|
-
return
|
|
1107
|
+
return json({ error: "Pago ya procesado" }, { status: 409 });
|
|
1029
1108
|
}
|
|
1030
1109
|
if (orderData.status !== "3ds-pending" && orderData.status !== "otp-pending") {
|
|
1031
|
-
return
|
|
1110
|
+
return json(
|
|
1032
1111
|
{ error: "Esta orden ya fue procesada" },
|
|
1033
1112
|
{ status: 409 }
|
|
1034
1113
|
);
|
|
@@ -1055,29 +1134,29 @@ function create3dsCompleteHandler(deps) {
|
|
|
1055
1134
|
}).catch(() => {
|
|
1056
1135
|
});
|
|
1057
1136
|
}
|
|
1058
|
-
return
|
|
1137
|
+
return json({ error: msg }, { status: 400 });
|
|
1059
1138
|
}
|
|
1060
1139
|
const transactionId = orderData.nuveiTransactionId || bodyTxId;
|
|
1061
1140
|
if (!transactionId) {
|
|
1062
|
-
return
|
|
1141
|
+
return json(
|
|
1063
1142
|
{ error: "No se encontr\xF3 el ID de transacci\xF3n para verificar" },
|
|
1064
1143
|
{ status: 400 }
|
|
1065
1144
|
);
|
|
1066
1145
|
}
|
|
1067
1146
|
if (type === "AUTHENTICATION_CONTINUE" && !orderData.threeDSCres && !orderData.isDeviceFingerprint) {
|
|
1068
1147
|
logger.log(`[3ds-complete] No CRES yet for order ${orderId} \u2014 still pending`);
|
|
1069
|
-
return
|
|
1148
|
+
return json({ stillPending: true });
|
|
1070
1149
|
}
|
|
1071
1150
|
const actualType = type === "AUTHENTICATION_CONTINUE" && orderData.threeDSCres ? "BY_CRES" : type;
|
|
1072
1151
|
const cresValue = actualType === "BY_CRES" ? orderData.threeDSCres : void 0;
|
|
1073
1152
|
if (actualType === "BY_CRES" && !cresValue) {
|
|
1074
|
-
return
|
|
1153
|
+
return json(
|
|
1075
1154
|
{ error: "No se encontr\xF3 el valor de autenticaci\xF3n 3DS (cres)" },
|
|
1076
1155
|
{ status: 400 }
|
|
1077
1156
|
);
|
|
1078
1157
|
}
|
|
1079
1158
|
if (type === "BY_OTP" && !otpCode) {
|
|
1080
|
-
return
|
|
1159
|
+
return json(
|
|
1081
1160
|
{ error: "Debes ingresar el c\xF3digo OTP" },
|
|
1082
1161
|
{ status: 400 }
|
|
1083
1162
|
);
|
|
@@ -1108,7 +1187,7 @@ function create3dsCompleteHandler(deps) {
|
|
|
1108
1187
|
logger.log(
|
|
1109
1188
|
`[3ds-complete] Still pending: txStatus=${txStatus} detail=${txStatusDetail}. Polling continues.`
|
|
1110
1189
|
);
|
|
1111
|
-
return
|
|
1190
|
+
return json({ stillPending: true });
|
|
1112
1191
|
}
|
|
1113
1192
|
if (isSuccess) {
|
|
1114
1193
|
const hasValidAuthCode = typeof txAuthCode === "string" && txAuthCode.trim().length > 0 && txAuthCode !== "null";
|
|
@@ -1194,7 +1273,7 @@ function create3dsCompleteHandler(deps) {
|
|
|
1194
1273
|
(err) => logger.error("[3ds-complete] Failed to delete card after payment:", err)
|
|
1195
1274
|
);
|
|
1196
1275
|
}
|
|
1197
|
-
return
|
|
1276
|
+
return json({
|
|
1198
1277
|
success: true,
|
|
1199
1278
|
transactionId: txId,
|
|
1200
1279
|
authorizationCode: hasValidAuthCode ? txAuthCode : null,
|
|
@@ -1212,7 +1291,7 @@ function create3dsCompleteHandler(deps) {
|
|
|
1212
1291
|
updatedAt: /* @__PURE__ */ new Date(),
|
|
1213
1292
|
threeDSCres: firestore.FieldValue.delete()
|
|
1214
1293
|
});
|
|
1215
|
-
return
|
|
1294
|
+
return json({
|
|
1216
1295
|
challenge: true,
|
|
1217
1296
|
challengeHtml,
|
|
1218
1297
|
isDeviceFingerprint: false,
|
|
@@ -1242,48 +1321,50 @@ function create3dsCompleteHandler(deps) {
|
|
|
1242
1321
|
}).catch(() => {
|
|
1243
1322
|
});
|
|
1244
1323
|
}
|
|
1245
|
-
return
|
|
1324
|
+
return json(
|
|
1246
1325
|
{ error: "Pago rechazado tras autenticaci\xF3n 3DS." },
|
|
1247
1326
|
{ status: 400 }
|
|
1248
1327
|
);
|
|
1249
1328
|
} catch (error) {
|
|
1250
1329
|
logger.error("3DS complete error:", error);
|
|
1251
|
-
return
|
|
1330
|
+
return json(
|
|
1252
1331
|
{ error: "Error interno del servidor" },
|
|
1253
1332
|
{ status: 500 }
|
|
1254
1333
|
);
|
|
1255
1334
|
}
|
|
1256
1335
|
};
|
|
1257
1336
|
}
|
|
1337
|
+
|
|
1338
|
+
// src/handlers/3ds-timeout.ts
|
|
1258
1339
|
function create3dsTimeoutHandler(deps) {
|
|
1259
1340
|
const logger = deps.logger ?? console;
|
|
1260
1341
|
const { db, auth } = deps.firebase;
|
|
1261
1342
|
return async function POST(request) {
|
|
1262
|
-
const sessionCookie = request
|
|
1343
|
+
const sessionCookie = getCookie(request, "__session");
|
|
1263
1344
|
if (!sessionCookie || !auth) {
|
|
1264
|
-
return
|
|
1345
|
+
return json({ error: "No autorizado" }, { status: 401 });
|
|
1265
1346
|
}
|
|
1266
1347
|
let decodedToken;
|
|
1267
1348
|
try {
|
|
1268
1349
|
decodedToken = await auth.verifySessionCookie(sessionCookie, true);
|
|
1269
1350
|
} catch {
|
|
1270
|
-
return
|
|
1351
|
+
return json({ error: "Sesion invalida" }, { status: 401 });
|
|
1271
1352
|
}
|
|
1272
1353
|
const { orderId } = await request.json();
|
|
1273
1354
|
if (!orderId || typeof orderId !== "string") {
|
|
1274
|
-
return
|
|
1355
|
+
return json({ error: "orderId requerido" }, { status: 400 });
|
|
1275
1356
|
}
|
|
1276
1357
|
const orderRef = db.collection("orders").doc(orderId);
|
|
1277
1358
|
const orderDoc = await orderRef.get();
|
|
1278
1359
|
if (!orderDoc.exists) {
|
|
1279
|
-
return
|
|
1360
|
+
return json({ error: "Orden no encontrada" }, { status: 404 });
|
|
1280
1361
|
}
|
|
1281
1362
|
const orderData = orderDoc.data() ?? {};
|
|
1282
1363
|
if (decodedToken.uid !== orderData.userId) {
|
|
1283
|
-
return
|
|
1364
|
+
return json({ error: "Usuario no coincide" }, { status: 403 });
|
|
1284
1365
|
}
|
|
1285
1366
|
if (orderData.status !== "3ds-pending") {
|
|
1286
|
-
return
|
|
1367
|
+
return json({ alreadyResolved: true });
|
|
1287
1368
|
}
|
|
1288
1369
|
await orderRef.update({
|
|
1289
1370
|
status: "failed",
|
|
@@ -1304,48 +1385,50 @@ function create3dsTimeoutHandler(deps) {
|
|
|
1304
1385
|
...retryUrl ? { retryUrl } : {}
|
|
1305
1386
|
}).catch((err) => logger.error("[3ds-timeout] Failed to send email:", err));
|
|
1306
1387
|
}
|
|
1307
|
-
return
|
|
1388
|
+
return json({ ok: true });
|
|
1308
1389
|
};
|
|
1309
1390
|
}
|
|
1391
|
+
|
|
1392
|
+
// src/handlers/refund.ts
|
|
1310
1393
|
function createRefundHandler(deps) {
|
|
1311
1394
|
const logger = deps.logger ?? console;
|
|
1312
1395
|
const { db, auth } = deps.firebase;
|
|
1313
1396
|
return async function POST(request) {
|
|
1314
1397
|
try {
|
|
1315
|
-
const sessionCookie = request
|
|
1398
|
+
const sessionCookie = getCookie(request, "__session");
|
|
1316
1399
|
if (!sessionCookie || !auth) {
|
|
1317
|
-
return
|
|
1400
|
+
return json({ error: "No autorizado" }, { status: 401 });
|
|
1318
1401
|
}
|
|
1319
1402
|
let decodedToken;
|
|
1320
1403
|
try {
|
|
1321
1404
|
decodedToken = await auth.verifySessionCookie(sessionCookie, true);
|
|
1322
1405
|
} catch {
|
|
1323
|
-
return
|
|
1406
|
+
return json({ error: "Sesi\xF3n inv\xE1lida" }, { status: 401 });
|
|
1324
1407
|
}
|
|
1325
1408
|
const { orderId } = await request.json();
|
|
1326
1409
|
if (!orderId) {
|
|
1327
|
-
return
|
|
1410
|
+
return json({ error: "orderId requerido" }, { status: 400 });
|
|
1328
1411
|
}
|
|
1329
1412
|
const orderDoc = await db.collection("orders").doc(orderId).get();
|
|
1330
1413
|
if (!orderDoc.exists) {
|
|
1331
|
-
return
|
|
1414
|
+
return json({ error: "Orden no encontrada" }, { status: 404 });
|
|
1332
1415
|
}
|
|
1333
1416
|
const order = orderDoc.data() ?? {};
|
|
1334
1417
|
if (order.userId !== decodedToken.uid) {
|
|
1335
|
-
return
|
|
1418
|
+
return json(
|
|
1336
1419
|
{ error: "No autorizado para esta orden" },
|
|
1337
1420
|
{ status: 403 }
|
|
1338
1421
|
);
|
|
1339
1422
|
}
|
|
1340
1423
|
if (order.status !== "paid") {
|
|
1341
|
-
return
|
|
1424
|
+
return json(
|
|
1342
1425
|
{ error: "Solo se pueden reembolsar \xF3rdenes pagadas" },
|
|
1343
1426
|
{ status: 400 }
|
|
1344
1427
|
);
|
|
1345
1428
|
}
|
|
1346
1429
|
const paymentTransactionId = order.paymentTransactionId;
|
|
1347
1430
|
if (!paymentTransactionId) {
|
|
1348
|
-
return
|
|
1431
|
+
return json(
|
|
1349
1432
|
{ error: "No se encontr\xF3 ID de transacci\xF3n" },
|
|
1350
1433
|
{ status: 400 }
|
|
1351
1434
|
);
|
|
@@ -1367,15 +1450,15 @@ function createRefundHandler(deps) {
|
|
|
1367
1450
|
);
|
|
1368
1451
|
}
|
|
1369
1452
|
}
|
|
1370
|
-
return
|
|
1453
|
+
return json({ success: true, detail: result.detail });
|
|
1371
1454
|
}
|
|
1372
|
-
return
|
|
1455
|
+
return json(
|
|
1373
1456
|
{ error: result.detail || "Error al procesar reembolso" },
|
|
1374
1457
|
{ status: 400 }
|
|
1375
1458
|
);
|
|
1376
1459
|
} catch (error) {
|
|
1377
1460
|
logger.error("Refund error:", error);
|
|
1378
|
-
return
|
|
1461
|
+
return json(
|
|
1379
1462
|
{ error: "Error interno del servidor" },
|
|
1380
1463
|
{ status: 500 }
|
|
1381
1464
|
);
|
|
@@ -1386,20 +1469,20 @@ function createInitCheckoutHandler(deps) {
|
|
|
1386
1469
|
const logger = deps.logger ?? console;
|
|
1387
1470
|
const { auth } = deps.firebase;
|
|
1388
1471
|
return async function POST(request) {
|
|
1389
|
-
const sessionCookie = request
|
|
1472
|
+
const sessionCookie = getCookie(request, "__session");
|
|
1390
1473
|
if (!sessionCookie || !auth) {
|
|
1391
|
-
return
|
|
1474
|
+
return json({ error: "No autorizado" }, { status: 401 });
|
|
1392
1475
|
}
|
|
1393
1476
|
let decodedToken;
|
|
1394
1477
|
try {
|
|
1395
1478
|
decodedToken = await auth.verifySessionCookie(sessionCookie, true);
|
|
1396
1479
|
} catch {
|
|
1397
|
-
return
|
|
1480
|
+
return json({ error: "Sesi\xF3n inv\xE1lida" }, { status: 401 });
|
|
1398
1481
|
}
|
|
1399
1482
|
try {
|
|
1400
1483
|
const { amount, vat, description, devReference } = await request.json();
|
|
1401
1484
|
if (!amount || !devReference) {
|
|
1402
|
-
return
|
|
1485
|
+
return json(
|
|
1403
1486
|
{ error: "amount y devReference son requeridos" },
|
|
1404
1487
|
{ status: 400 }
|
|
1405
1488
|
);
|
|
@@ -1427,36 +1510,38 @@ function createInitCheckoutHandler(deps) {
|
|
|
1427
1510
|
}
|
|
1428
1511
|
});
|
|
1429
1512
|
if (result.error) {
|
|
1430
|
-
return
|
|
1513
|
+
return json(
|
|
1431
1514
|
{ error: result.error.description || "Error al inicializar checkout" },
|
|
1432
1515
|
{ status: 400 }
|
|
1433
1516
|
);
|
|
1434
1517
|
}
|
|
1435
1518
|
if (!result.reference) {
|
|
1436
|
-
return
|
|
1519
|
+
return json(
|
|
1437
1520
|
{ error: "No se recibi\xF3 referencia de checkout" },
|
|
1438
1521
|
{ status: 500 }
|
|
1439
1522
|
);
|
|
1440
1523
|
}
|
|
1441
|
-
return
|
|
1524
|
+
return json({
|
|
1442
1525
|
reference: result.reference,
|
|
1443
1526
|
checkoutUrl: result.checkout_url
|
|
1444
1527
|
});
|
|
1445
1528
|
} catch (error) {
|
|
1446
1529
|
logger.error("Init checkout error:", error);
|
|
1447
|
-
return
|
|
1530
|
+
return json(
|
|
1448
1531
|
{ error: "Error al inicializar checkout" },
|
|
1449
1532
|
{ status: 500 }
|
|
1450
1533
|
);
|
|
1451
1534
|
}
|
|
1452
1535
|
};
|
|
1453
1536
|
}
|
|
1537
|
+
|
|
1538
|
+
// src/handlers/cards.ts
|
|
1454
1539
|
function createCardsHandler(deps) {
|
|
1455
1540
|
const logger = deps.logger ?? console;
|
|
1456
1541
|
const { db, auth } = deps.firebase;
|
|
1457
1542
|
const verifiedCardsEnabled = deps.enableVerifiedCardsTracking !== false;
|
|
1458
1543
|
async function verifySession(request) {
|
|
1459
|
-
const sessionCookie = request
|
|
1544
|
+
const sessionCookie = getCookie(request, "__session");
|
|
1460
1545
|
if (!sessionCookie || !auth) return null;
|
|
1461
1546
|
try {
|
|
1462
1547
|
return await auth.verifySessionCookie(sessionCookie, true);
|
|
@@ -1467,7 +1552,7 @@ function createCardsHandler(deps) {
|
|
|
1467
1552
|
const GET = async function GET2(request) {
|
|
1468
1553
|
const decoded = await verifySession(request);
|
|
1469
1554
|
if (!decoded) {
|
|
1470
|
-
return
|
|
1555
|
+
return json({ error: "No autorizado" }, { status: 401 });
|
|
1471
1556
|
}
|
|
1472
1557
|
try {
|
|
1473
1558
|
const result = await listCards(decoded.uid);
|
|
@@ -1491,12 +1576,12 @@ function createCardsHandler(deps) {
|
|
|
1491
1576
|
const enrichedCards = cards.map(
|
|
1492
1577
|
(c) => c.status === "review" && verifiedTokens.has(c.token) ? { ...c, status: "valid" } : c
|
|
1493
1578
|
);
|
|
1494
|
-
return
|
|
1579
|
+
return json({ cards: enrichedCards });
|
|
1495
1580
|
}
|
|
1496
|
-
return
|
|
1581
|
+
return json({ cards });
|
|
1497
1582
|
} catch (error) {
|
|
1498
1583
|
logger.error("Error listing cards:", error);
|
|
1499
|
-
return
|
|
1584
|
+
return json(
|
|
1500
1585
|
{ error: "Error al obtener tarjetas" },
|
|
1501
1586
|
{ status: 500 }
|
|
1502
1587
|
);
|
|
@@ -1505,12 +1590,12 @@ function createCardsHandler(deps) {
|
|
|
1505
1590
|
const DELETE = async function DELETE2(request) {
|
|
1506
1591
|
const decoded = await verifySession(request);
|
|
1507
1592
|
if (!decoded) {
|
|
1508
|
-
return
|
|
1593
|
+
return json({ error: "No autorizado" }, { status: 401 });
|
|
1509
1594
|
}
|
|
1510
1595
|
try {
|
|
1511
1596
|
const { token } = await request.json();
|
|
1512
1597
|
if (!token) {
|
|
1513
|
-
return
|
|
1598
|
+
return json(
|
|
1514
1599
|
{ error: "Token de tarjeta requerido" },
|
|
1515
1600
|
{ status: 400 }
|
|
1516
1601
|
);
|
|
@@ -1520,7 +1605,7 @@ function createCardsHandler(deps) {
|
|
|
1520
1605
|
const resultWithError = result;
|
|
1521
1606
|
if (resultWithError.error) {
|
|
1522
1607
|
logger.error("[cards/DELETE] Nuvei delete failed:", JSON.stringify(result));
|
|
1523
|
-
return
|
|
1608
|
+
return json(
|
|
1524
1609
|
{ error: "No se pudo eliminar la tarjeta en Nuvei", detail: result },
|
|
1525
1610
|
{ status: 400 }
|
|
1526
1611
|
);
|
|
@@ -1531,10 +1616,10 @@ function createCardsHandler(deps) {
|
|
|
1531
1616
|
} catch {
|
|
1532
1617
|
}
|
|
1533
1618
|
}
|
|
1534
|
-
return
|
|
1619
|
+
return json(result);
|
|
1535
1620
|
} catch (error) {
|
|
1536
1621
|
logger.error("Error deleting card:", error);
|
|
1537
|
-
return
|
|
1622
|
+
return json(
|
|
1538
1623
|
{ error: "Error al eliminar tarjeta" },
|
|
1539
1624
|
{ status: 500 }
|
|
1540
1625
|
);
|
|
@@ -1542,25 +1627,27 @@ function createCardsHandler(deps) {
|
|
|
1542
1627
|
};
|
|
1543
1628
|
return { GET, DELETE };
|
|
1544
1629
|
}
|
|
1630
|
+
|
|
1631
|
+
// src/handlers/verify.ts
|
|
1545
1632
|
function createVerifyHandler(deps) {
|
|
1546
1633
|
const logger = deps.logger ?? console;
|
|
1547
1634
|
const { db, auth } = deps.firebase;
|
|
1548
1635
|
const verifiedCardsEnabled = deps.enableVerifiedCardsTracking !== false;
|
|
1549
1636
|
return async function POST(request) {
|
|
1550
|
-
const sessionCookie = request
|
|
1637
|
+
const sessionCookie = getCookie(request, "__session");
|
|
1551
1638
|
if (!sessionCookie || !auth) {
|
|
1552
|
-
return
|
|
1639
|
+
return json({ error: "No autorizado" }, { status: 401 });
|
|
1553
1640
|
}
|
|
1554
1641
|
let decodedToken;
|
|
1555
1642
|
try {
|
|
1556
1643
|
decodedToken = await auth.verifySessionCookie(sessionCookie, true);
|
|
1557
1644
|
} catch {
|
|
1558
|
-
return
|
|
1645
|
+
return json({ error: "Sesi\xF3n inv\xE1lida" }, { status: 401 });
|
|
1559
1646
|
}
|
|
1560
1647
|
try {
|
|
1561
1648
|
const { cardToken, transactionReference, value } = await request.json();
|
|
1562
1649
|
if (!cardToken && !transactionReference || !value) {
|
|
1563
|
-
return
|
|
1650
|
+
return json(
|
|
1564
1651
|
{ error: "transactionReference y value son requeridos" },
|
|
1565
1652
|
{ status: 400 }
|
|
1566
1653
|
);
|
|
@@ -1571,7 +1658,7 @@ function createVerifyHandler(deps) {
|
|
|
1571
1658
|
value
|
|
1572
1659
|
});
|
|
1573
1660
|
if (result.error) {
|
|
1574
|
-
return
|
|
1661
|
+
return json(
|
|
1575
1662
|
{ error: result.error.description || "Error de verificaci\xF3n" },
|
|
1576
1663
|
{ status: 400 }
|
|
1577
1664
|
);
|
|
@@ -1583,41 +1670,43 @@ function createVerifyHandler(deps) {
|
|
|
1583
1670
|
logger.error("Failed to persist verified card:", err);
|
|
1584
1671
|
}
|
|
1585
1672
|
}
|
|
1586
|
-
return
|
|
1673
|
+
return json({
|
|
1587
1674
|
success: true,
|
|
1588
1675
|
transaction: result.transaction
|
|
1589
1676
|
});
|
|
1590
1677
|
} catch (error) {
|
|
1591
1678
|
logger.error("Card verify error:", error);
|
|
1592
|
-
return
|
|
1679
|
+
return json(
|
|
1593
1680
|
{ error: "Error al verificar tarjeta" },
|
|
1594
1681
|
{ status: 500 }
|
|
1595
1682
|
);
|
|
1596
1683
|
}
|
|
1597
1684
|
};
|
|
1598
1685
|
}
|
|
1686
|
+
|
|
1687
|
+
// src/handlers/test-charge.ts
|
|
1599
1688
|
function createTestChargeHandler(deps) {
|
|
1600
1689
|
const logger = deps.logger ?? console;
|
|
1601
1690
|
const { auth } = deps.firebase;
|
|
1602
1691
|
const defaultDescription = deps.defaultDescription ?? "Test charge";
|
|
1603
1692
|
return async function POST(request) {
|
|
1604
1693
|
if (process.env.NUVEI_ENV === "prod") {
|
|
1605
|
-
return
|
|
1694
|
+
return json({ error: "Not available" }, { status: 404 });
|
|
1606
1695
|
}
|
|
1607
|
-
const sessionCookie = request
|
|
1696
|
+
const sessionCookie = getCookie(request, "__session");
|
|
1608
1697
|
if (!sessionCookie || !auth) {
|
|
1609
|
-
return
|
|
1698
|
+
return json({ error: "No autorizado" }, { status: 401 });
|
|
1610
1699
|
}
|
|
1611
1700
|
let decodedToken;
|
|
1612
1701
|
try {
|
|
1613
1702
|
decodedToken = await auth.verifySessionCookie(sessionCookie, true);
|
|
1614
1703
|
} catch {
|
|
1615
|
-
return
|
|
1704
|
+
return json({ error: "Sesi\xF3n inv\xE1lida" }, { status: 401 });
|
|
1616
1705
|
}
|
|
1617
1706
|
try {
|
|
1618
1707
|
const { token, amount, vat, description, devReference } = await request.json();
|
|
1619
1708
|
if (!token || !amount || !devReference) {
|
|
1620
|
-
return
|
|
1709
|
+
return json(
|
|
1621
1710
|
{ error: "token, amount y devReference son requeridos" },
|
|
1622
1711
|
{ status: 400 }
|
|
1623
1712
|
);
|
|
@@ -1633,13 +1722,13 @@ function createTestChargeHandler(deps) {
|
|
|
1633
1722
|
});
|
|
1634
1723
|
logger.log("Test charge result:", JSON.stringify(result, null, 2));
|
|
1635
1724
|
if (result.transaction && result.transaction.status === "success" && result.transaction.status_detail === 3) {
|
|
1636
|
-
return
|
|
1725
|
+
return json({
|
|
1637
1726
|
success: true,
|
|
1638
1727
|
transaction: result.transaction,
|
|
1639
1728
|
card: result.card
|
|
1640
1729
|
});
|
|
1641
1730
|
}
|
|
1642
|
-
return
|
|
1731
|
+
return json(
|
|
1643
1732
|
{
|
|
1644
1733
|
success: false,
|
|
1645
1734
|
error: result.transaction?.message || result.error?.description || "Pago rechazado",
|
|
@@ -1649,7 +1738,7 @@ function createTestChargeHandler(deps) {
|
|
|
1649
1738
|
);
|
|
1650
1739
|
} catch (error) {
|
|
1651
1740
|
logger.error("Test charge error:", error);
|
|
1652
|
-
return
|
|
1741
|
+
return json(
|
|
1653
1742
|
{ error: "Error interno del servidor" },
|
|
1654
1743
|
{ status: 500 }
|
|
1655
1744
|
);
|
|
@@ -1657,12 +1746,64 @@ function createTestChargeHandler(deps) {
|
|
|
1657
1746
|
};
|
|
1658
1747
|
}
|
|
1659
1748
|
|
|
1749
|
+
// src/handlers/proxy.ts
|
|
1750
|
+
function createNuveiProxyHandler(deps = {}) {
|
|
1751
|
+
const logger = deps.logger ?? console;
|
|
1752
|
+
return async function POST(request) {
|
|
1753
|
+
if (request.method !== "POST") {
|
|
1754
|
+
return json({ error: "Method not allowed" }, { status: 405 });
|
|
1755
|
+
}
|
|
1756
|
+
let parsed;
|
|
1757
|
+
try {
|
|
1758
|
+
parsed = await request.json();
|
|
1759
|
+
} catch {
|
|
1760
|
+
return json({ error: "Invalid JSON body" }, { status: 400 });
|
|
1761
|
+
}
|
|
1762
|
+
const { path, method, body: nuveiBody } = parsed;
|
|
1763
|
+
const authToken = request.headers.get("x-nuvei-auth-token");
|
|
1764
|
+
if (!path || !method || !authToken) {
|
|
1765
|
+
return json(
|
|
1766
|
+
{ error: "Missing path, method, or x-nuvei-auth-token header" },
|
|
1767
|
+
{ status: 400 }
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
const env = request.headers.get("x-nuvei-env") || "prod";
|
|
1771
|
+
const baseUrl = env === "prod" ? "https://ccapi.paymentez.com" : "https://ccapi-stg.paymentez.com";
|
|
1772
|
+
const url = `${baseUrl}${path}`;
|
|
1773
|
+
try {
|
|
1774
|
+
const options = {
|
|
1775
|
+
method,
|
|
1776
|
+
headers: {
|
|
1777
|
+
"Content-Type": "application/json",
|
|
1778
|
+
"Auth-Token": authToken
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1781
|
+
if (nuveiBody && method === "POST") {
|
|
1782
|
+
options.body = JSON.stringify(nuveiBody);
|
|
1783
|
+
}
|
|
1784
|
+
const response = await fetch(url, options);
|
|
1785
|
+
const responseBody = await response.text();
|
|
1786
|
+
return new Response(responseBody, {
|
|
1787
|
+
status: response.status,
|
|
1788
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
1789
|
+
});
|
|
1790
|
+
} catch (err) {
|
|
1791
|
+
logger.error("[nuveiProxy] Error:", err);
|
|
1792
|
+
return json(
|
|
1793
|
+
{ error: "Proxy error: " + (err instanceof Error ? err.message : String(err)) },
|
|
1794
|
+
{ status: 500 }
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1660
1800
|
exports.create3dsCallbackHandler = create3dsCallbackHandler;
|
|
1661
1801
|
exports.create3dsCompleteHandler = create3dsCompleteHandler;
|
|
1662
1802
|
exports.create3dsTimeoutHandler = create3dsTimeoutHandler;
|
|
1663
1803
|
exports.createCardsHandler = createCardsHandler;
|
|
1664
1804
|
exports.createChargeHandler = createChargeHandler;
|
|
1665
1805
|
exports.createInitCheckoutHandler = createInitCheckoutHandler;
|
|
1806
|
+
exports.createNuveiProxyHandler = createNuveiProxyHandler;
|
|
1666
1807
|
exports.createRefundHandler = createRefundHandler;
|
|
1667
1808
|
exports.createTestChargeHandler = createTestChargeHandler;
|
|
1668
1809
|
exports.createVerifyHandler = createVerifyHandler;
|