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