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