@pandait.tech/payment-nuvei 1.0.0 → 1.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 +12 -6
- package/dist/handlers/index.cjs +428 -436
- package/dist/handlers/index.cjs.map +1 -1
- package/dist/handlers/index.js +428 -436
- package/dist/handlers/index.js.map +1 -1
- package/package.json +1 -1
package/dist/handlers/index.js
CHANGED
|
@@ -183,7 +183,207 @@ async function verifyThreeDS(params) {
|
|
|
183
183
|
return nuveiRequest("/v2/transaction/verify/", "POST", body);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
// src/handlers/charge.ts
|
|
186
|
+
// src/handlers/charge/validate-order.ts
|
|
187
|
+
async function validateStandardOrder({
|
|
188
|
+
db,
|
|
189
|
+
getPriceDisplay,
|
|
190
|
+
orderData,
|
|
191
|
+
amount
|
|
192
|
+
}) {
|
|
193
|
+
const orderItems = orderData.items ?? [];
|
|
194
|
+
let verifiedSubtotal = 0;
|
|
195
|
+
for (const item of orderItems) {
|
|
196
|
+
const productDoc = await db.collection("products").doc(item.productId).get();
|
|
197
|
+
if (!productDoc.exists || !productDoc.data()?.isActive) {
|
|
198
|
+
return json(
|
|
199
|
+
{ error: `El producto "${item.name}" ya no est\xE1 disponible` },
|
|
200
|
+
{ status: 409 }
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
const productData = productDoc.data();
|
|
204
|
+
const isDigital = productData.isDigital === true;
|
|
205
|
+
const stock = productData.stock ?? 0;
|
|
206
|
+
if (!isDigital && stock < item.quantity) {
|
|
207
|
+
return json(
|
|
208
|
+
{
|
|
209
|
+
error: `Stock insuficiente para "${item.name}" (disponible: ${stock})`
|
|
210
|
+
},
|
|
211
|
+
{ status: 409 }
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const display = getPriceDisplay({
|
|
215
|
+
price: productData.price ?? 0,
|
|
216
|
+
autoDiscounts: productData.autoDiscounts
|
|
217
|
+
});
|
|
218
|
+
const expectedItemSubtotal = display.finalSubtotal;
|
|
219
|
+
if (Math.abs(expectedItemSubtotal - item.price) > 0.02) {
|
|
220
|
+
return json(
|
|
221
|
+
{
|
|
222
|
+
error: `El precio de "${item.name}" cambi\xF3. Recarg\xE1 la p\xE1gina para ver el precio actualizado.`
|
|
223
|
+
},
|
|
224
|
+
{ status: 409 }
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
verifiedSubtotal += expectedItemSubtotal * item.quantity;
|
|
228
|
+
}
|
|
229
|
+
let verifiedDiscount = 0;
|
|
230
|
+
const orderDiscount = orderData.discount ?? 0;
|
|
231
|
+
const orderPromotionId = orderData.promotionId;
|
|
232
|
+
if (orderPromotionId && orderDiscount > 0) {
|
|
233
|
+
const promoDoc = await db.collection("promotions").doc(orderPromotionId).get();
|
|
234
|
+
if (!promoDoc.exists || !promoDoc.data()?.isActive) {
|
|
235
|
+
return json(
|
|
236
|
+
{ error: "El cupon aplicado ya no es valido. Remuevelo y vuelve a intentar." },
|
|
237
|
+
{ status: 409 }
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
const promo = promoDoc.data();
|
|
241
|
+
const now = /* @__PURE__ */ new Date();
|
|
242
|
+
const validFrom = promo.rules.validFrom.toDate ? promo.rules.validFrom.toDate() : new Date(promo.rules.validFrom);
|
|
243
|
+
const validUntil = promo.rules.validUntil.toDate ? promo.rules.validUntil.toDate() : new Date(promo.rules.validUntil);
|
|
244
|
+
if (now < validFrom || now > validUntil) {
|
|
245
|
+
return json(
|
|
246
|
+
{ error: "El cupon aplicado ha expirado. Remuevelo y vuelve a intentar." },
|
|
247
|
+
{ status: 409 }
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (promo.rules.maxTotalUses && promo.currentUses >= promo.rules.maxTotalUses) {
|
|
251
|
+
return json(
|
|
252
|
+
{ error: "El cupon aplicado ya alcanzo su limite de usos." },
|
|
253
|
+
{ status: 409 }
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (promo.type === "percentage") {
|
|
257
|
+
verifiedDiscount = verifiedSubtotal * (promo.value / 100);
|
|
258
|
+
if (promo.maxDiscountAmount) {
|
|
259
|
+
verifiedDiscount = Math.min(verifiedDiscount, promo.maxDiscountAmount);
|
|
260
|
+
}
|
|
261
|
+
} else if (promo.type === "fixed_amount") {
|
|
262
|
+
verifiedDiscount = Math.min(promo.value, verifiedSubtotal);
|
|
263
|
+
}
|
|
264
|
+
verifiedDiscount = Math.round(verifiedDiscount * 100) / 100;
|
|
265
|
+
}
|
|
266
|
+
const verifiedDiscountedSubtotal = Math.max(0, verifiedSubtotal - verifiedDiscount);
|
|
267
|
+
const verifiedVat = Math.round(verifiedDiscountedSubtotal * 0.15 * 100) / 100;
|
|
268
|
+
const verifiedTotal = Math.round((verifiedDiscountedSubtotal + verifiedVat) * 100) / 100;
|
|
269
|
+
if (verifiedTotal !== amount) {
|
|
270
|
+
return json(
|
|
271
|
+
{
|
|
272
|
+
error: "El monto no coincide con los precios actuales. Actualiza tu carrito."
|
|
273
|
+
},
|
|
274
|
+
{ status: 409 }
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
async function finalizePaidOrder(deps, ctx) {
|
|
280
|
+
const { db, logger } = deps;
|
|
281
|
+
const {
|
|
282
|
+
orderId,
|
|
283
|
+
transactionId,
|
|
284
|
+
rawAuthCode,
|
|
285
|
+
debitResponse,
|
|
286
|
+
auditResponseLabel,
|
|
287
|
+
installments,
|
|
288
|
+
installmentsType,
|
|
289
|
+
userId,
|
|
290
|
+
userEmail,
|
|
291
|
+
vat,
|
|
292
|
+
amount,
|
|
293
|
+
token,
|
|
294
|
+
deleteCardAfterPayment,
|
|
295
|
+
extraOrderFields,
|
|
296
|
+
preReadOrder,
|
|
297
|
+
includePostPurchaseNote
|
|
298
|
+
} = ctx;
|
|
299
|
+
const hasValidAuthCode = typeof rawAuthCode === "string" && rawAuthCode.trim().length > 0 && rawAuthCode !== "null";
|
|
300
|
+
const batch = db.batch();
|
|
301
|
+
const orderRef = db.collection("orders").doc(orderId);
|
|
302
|
+
if (hasValidAuthCode) {
|
|
303
|
+
batch.update(orderRef, {
|
|
304
|
+
status: "paid",
|
|
305
|
+
paymentTransactionId: transactionId,
|
|
306
|
+
authorizationCode: rawAuthCode,
|
|
307
|
+
...installments ? { installments, installmentsType: installmentsType ?? 0 } : {},
|
|
308
|
+
chargeResponseAt: /* @__PURE__ */ new Date(),
|
|
309
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
310
|
+
...extraOrderFields ?? {}
|
|
311
|
+
});
|
|
312
|
+
} else {
|
|
313
|
+
logger.error(
|
|
314
|
+
`[AUDIT:MISSING_AUTH_CODE] orderId=${orderId} txId=${transactionId} ${auditResponseLabel ?? "debitResponse"}=${JSON.stringify(debitResponse)}`
|
|
315
|
+
);
|
|
316
|
+
batch.update(orderRef, {
|
|
317
|
+
status: "paid",
|
|
318
|
+
paymentTransactionId: transactionId,
|
|
319
|
+
missingAuthCodeFlagged: true,
|
|
320
|
+
missingAuthCodeLoggedAt: /* @__PURE__ */ new Date(),
|
|
321
|
+
emailPending: true,
|
|
322
|
+
...installments ? { installments, installmentsType: installmentsType ?? 0 } : {},
|
|
323
|
+
chargeResponseAt: /* @__PURE__ */ new Date(),
|
|
324
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
325
|
+
...extraOrderFields ?? {}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
const orderInfo = preReadOrder ?? ((await orderRef.get()).data() ?? {});
|
|
329
|
+
if (orderInfo.promotionId) {
|
|
330
|
+
const promoRef = db.collection("promotions").doc(orderInfo.promotionId);
|
|
331
|
+
batch.update(promoRef, {
|
|
332
|
+
currentUses: FieldValue.increment(1)
|
|
333
|
+
});
|
|
334
|
+
const usageRef = promoRef.collection("usages").doc();
|
|
335
|
+
batch.set(usageRef, {
|
|
336
|
+
userId: orderInfo.userId,
|
|
337
|
+
orderId,
|
|
338
|
+
discountApplied: orderInfo.discount || 0,
|
|
339
|
+
usedAt: /* @__PURE__ */ new Date()
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
await batch.commit();
|
|
343
|
+
if (deps.onPaymentSucceeded) {
|
|
344
|
+
try {
|
|
345
|
+
await deps.onPaymentSucceeded({ ...orderInfo, id: orderId });
|
|
346
|
+
} catch (err) {
|
|
347
|
+
logger.error(
|
|
348
|
+
`[charge] onPaymentSucceeded hook failed for order ${orderId}:`,
|
|
349
|
+
err
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
let emailSent = false;
|
|
354
|
+
if (hasValidAuthCode) {
|
|
355
|
+
const emailResult = await deps.email.sendPaymentConfirmation({
|
|
356
|
+
to: userEmail,
|
|
357
|
+
customerName: orderInfo.shippingAddress?.fullName || "",
|
|
358
|
+
orderId,
|
|
359
|
+
transactionId,
|
|
360
|
+
authorizationCode: rawAuthCode,
|
|
361
|
+
items: orderInfo.items || [],
|
|
362
|
+
subtotal: orderInfo.subtotal || amount,
|
|
363
|
+
discount: orderInfo.discount || void 0,
|
|
364
|
+
couponCode: orderInfo.couponCode,
|
|
365
|
+
vat: orderInfo.vat || vat,
|
|
366
|
+
total: amount,
|
|
367
|
+
...includePostPurchaseNote !== false && orderInfo.postPurchaseNote ? { postPurchaseNote: orderInfo.postPurchaseNote } : {}
|
|
368
|
+
}).catch(() => ({ success: false }));
|
|
369
|
+
emailSent = emailResult.success;
|
|
370
|
+
if (emailSent) {
|
|
371
|
+
await orderRef.update({ emailSentAt: /* @__PURE__ */ new Date() });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (deleteCardAfterPayment && token) {
|
|
375
|
+
deleteCard(token, userId).catch(
|
|
376
|
+
(err) => logger.error("[charge] Failed to delete card after payment:", err)
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
transactionId,
|
|
381
|
+
authorizationCode: hasValidAuthCode ? rawAuthCode : null,
|
|
382
|
+
emailSent
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/handlers/charge/error-messages.ts
|
|
187
387
|
var NUVEI_ERROR_MESSAGES = {
|
|
188
388
|
0: "Error del procesador de pagos. Intenta de nuevo.",
|
|
189
389
|
// 1 = review/pending — handled separately, not shown as error
|
|
@@ -229,6 +429,141 @@ function getNuveiUserMessage(statusDetail, rawMessage) {
|
|
|
229
429
|
}
|
|
230
430
|
return "No se pudo procesar el pago. Verifica los datos de tu tarjeta o intenta con otra.";
|
|
231
431
|
}
|
|
432
|
+
|
|
433
|
+
// src/handlers/charge/handle-3ds-results.ts
|
|
434
|
+
async function handleNonSuccessResult(deps, ctx) {
|
|
435
|
+
const { db, logger } = deps;
|
|
436
|
+
const { nuveiData, orderId, userEmail, amount, token, deleteCardAfterPayment } = ctx;
|
|
437
|
+
if (nuveiData.transaction?.status === "pending" && nuveiData.transaction?.status_detail === 1) {
|
|
438
|
+
await db.collection("orders").doc(orderId).update({
|
|
439
|
+
status: "processing",
|
|
440
|
+
paymentTransactionId: nuveiData.transaction.id || null,
|
|
441
|
+
chargeResponseAt: /* @__PURE__ */ new Date(),
|
|
442
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
443
|
+
});
|
|
444
|
+
return json({
|
|
445
|
+
review: true,
|
|
446
|
+
orderId,
|
|
447
|
+
transactionId: nuveiData.transaction.id
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
const persistDeleteOnPaid = deleteCardAfterPayment ? { deleteCardAfterPayment: true, paymentToken: token } : {};
|
|
451
|
+
if (nuveiData.transaction?.status_detail === 35) {
|
|
452
|
+
const threeDSData = nuveiData["3ds"];
|
|
453
|
+
const hiddenIframeHtml = threeDSData?.browser_response?.hidden_iframe || "";
|
|
454
|
+
await db.collection("orders").doc(orderId).update({
|
|
455
|
+
status: "3ds-pending",
|
|
456
|
+
isDeviceFingerprint: true,
|
|
457
|
+
nuveiTransactionId: nuveiData.transaction.id || null,
|
|
458
|
+
...persistDeleteOnPaid,
|
|
459
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
460
|
+
});
|
|
461
|
+
return json({
|
|
462
|
+
challenge: true,
|
|
463
|
+
challengeHtml: hiddenIframeHtml,
|
|
464
|
+
isDeviceFingerprint: true,
|
|
465
|
+
orderId,
|
|
466
|
+
nuveiTransactionId: nuveiData.transaction.id,
|
|
467
|
+
statusDetail: 35
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (nuveiData.transaction?.status_detail === 31) {
|
|
471
|
+
await db.collection("orders").doc(orderId).update({
|
|
472
|
+
status: "otp-pending",
|
|
473
|
+
nuveiTransactionId: nuveiData.transaction.id || null,
|
|
474
|
+
...persistDeleteOnPaid,
|
|
475
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
476
|
+
});
|
|
477
|
+
return json({
|
|
478
|
+
otpRequired: true,
|
|
479
|
+
orderId,
|
|
480
|
+
nuveiTransactionId: nuveiData.transaction.id,
|
|
481
|
+
statusDetail: 31
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
if (nuveiData.transaction?.status_detail === 36 || nuveiData.transaction?.status_detail === 37) {
|
|
485
|
+
const threeDSData = nuveiData["3ds"];
|
|
486
|
+
const challengeHtml = threeDSData?.browser_response?.challenge_request || threeDSData?.browser_response?.hidden_iframe || "";
|
|
487
|
+
const rawDebit = nuveiData;
|
|
488
|
+
const debitCres = rawDebit["3ds"]?.authentication?.["cres"] || rawDebit["3ds"]?.browser_response?.["cres"] || rawDebit["3ds"]?.["cres"] || rawDebit.transaction?.["cres"] || rawDebit["cres"] || rawDebit["value"] || null;
|
|
489
|
+
logger.log(
|
|
490
|
+
`[charge] status ${nuveiData.transaction.status_detail} \u2014 debitCres captured: ${debitCres ? "YES (len=" + debitCres.length + ")" : "NO"}`
|
|
491
|
+
);
|
|
492
|
+
if (challengeHtml) {
|
|
493
|
+
await db.collection("orders").doc(orderId).update({
|
|
494
|
+
status: "3ds-pending",
|
|
495
|
+
nuveiTransactionId: nuveiData.transaction?.id || null,
|
|
496
|
+
...debitCres ? { threeDSCres: debitCres } : {},
|
|
497
|
+
...persistDeleteOnPaid,
|
|
498
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
499
|
+
});
|
|
500
|
+
return json({
|
|
501
|
+
challenge: true,
|
|
502
|
+
challengeHtml,
|
|
503
|
+
isDeviceFingerprint: false,
|
|
504
|
+
orderId,
|
|
505
|
+
nuveiTransactionId: nuveiData.transaction?.id,
|
|
506
|
+
statusDetail: nuveiData.transaction?.status_detail
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const threeDSStatus = nuveiData["3ds"]?.authentication?.status;
|
|
511
|
+
if (threeDSStatus && THREEDS_AUTH_MESSAGES[threeDSStatus]) {
|
|
512
|
+
const threeDSErrorMsg = THREEDS_AUTH_MESSAGES[threeDSStatus];
|
|
513
|
+
const currentOrder2 = await db.collection("orders").doc(orderId).get();
|
|
514
|
+
const currentOrderData2 = currentOrder2.data() ?? {};
|
|
515
|
+
if (currentOrderData2.status === "pending") {
|
|
516
|
+
await db.collection("orders").doc(orderId).update({
|
|
517
|
+
status: "failed",
|
|
518
|
+
chargeResponseAt: /* @__PURE__ */ new Date(),
|
|
519
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
if (userEmail) {
|
|
523
|
+
const retryUrl = deps.getRetryUrl?.({ ...currentOrderData2, id: orderId });
|
|
524
|
+
deps.email.sendPaymentFailed({
|
|
525
|
+
to: userEmail,
|
|
526
|
+
customerName: currentOrderData2.shippingAddress?.fullName || "",
|
|
527
|
+
orderId,
|
|
528
|
+
errorMessage: threeDSErrorMsg,
|
|
529
|
+
items: currentOrderData2.items || [],
|
|
530
|
+
total: currentOrderData2.total || amount,
|
|
531
|
+
...retryUrl ? { retryUrl } : {}
|
|
532
|
+
}).catch(() => {
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
return json({ error: threeDSErrorMsg }, { status: 400 });
|
|
536
|
+
}
|
|
537
|
+
const failedErrorMsg = getNuveiUserMessage(
|
|
538
|
+
nuveiData.transaction?.status_detail,
|
|
539
|
+
nuveiData.transaction?.message || nuveiData.error?.description
|
|
540
|
+
);
|
|
541
|
+
const currentOrder = await db.collection("orders").doc(orderId).get();
|
|
542
|
+
const currentOrderData = currentOrder.data() ?? {};
|
|
543
|
+
if (currentOrderData.status === "pending") {
|
|
544
|
+
await db.collection("orders").doc(orderId).update({
|
|
545
|
+
status: "failed",
|
|
546
|
+
chargeResponseAt: /* @__PURE__ */ new Date(),
|
|
547
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
if (userEmail) {
|
|
551
|
+
const retryUrl = deps.getRetryUrl?.({ ...currentOrderData, id: orderId });
|
|
552
|
+
deps.email.sendPaymentFailed({
|
|
553
|
+
to: userEmail,
|
|
554
|
+
customerName: currentOrderData.shippingAddress?.fullName || "",
|
|
555
|
+
orderId,
|
|
556
|
+
errorMessage: failedErrorMsg,
|
|
557
|
+
items: currentOrderData.items || [],
|
|
558
|
+
total: currentOrderData.total || amount,
|
|
559
|
+
...retryUrl ? { retryUrl } : {}
|
|
560
|
+
}).catch(() => {
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
return json({ error: failedErrorMsg }, { status: 400 });
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/handlers/charge.ts
|
|
232
567
|
function createChargeHandler(deps) {
|
|
233
568
|
const logger = deps.logger ?? console;
|
|
234
569
|
const { db, auth } = deps.firebase;
|
|
@@ -284,6 +619,7 @@ function createChargeHandler(deps) {
|
|
|
284
619
|
orderId,
|
|
285
620
|
amount,
|
|
286
621
|
vat,
|
|
622
|
+
taxableAmount,
|
|
287
623
|
description,
|
|
288
624
|
userId,
|
|
289
625
|
userEmail,
|
|
@@ -332,89 +668,14 @@ function createChargeHandler(deps) {
|
|
|
332
668
|
}
|
|
333
669
|
}
|
|
334
670
|
if (runStandardValidation) {
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
const productData = productDoc.data();
|
|
346
|
-
const isDigital = productData.isDigital === true;
|
|
347
|
-
const stock = productData.stock ?? 0;
|
|
348
|
-
if (!isDigital && stock < item.quantity) {
|
|
349
|
-
return json(
|
|
350
|
-
{
|
|
351
|
-
error: `Stock insuficiente para "${item.name}" (disponible: ${stock})`
|
|
352
|
-
},
|
|
353
|
-
{ status: 409 }
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
const display = deps.getPriceDisplay({
|
|
357
|
-
price: productData.price ?? 0,
|
|
358
|
-
autoDiscounts: productData.autoDiscounts
|
|
359
|
-
});
|
|
360
|
-
const expectedItemSubtotal = display.finalSubtotal;
|
|
361
|
-
if (Math.abs(expectedItemSubtotal - item.price) > 0.02) {
|
|
362
|
-
return json(
|
|
363
|
-
{
|
|
364
|
-
error: `El precio de "${item.name}" cambi\xF3. Recarg\xE1 la p\xE1gina para ver el precio actualizado.`
|
|
365
|
-
},
|
|
366
|
-
{ status: 409 }
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
verifiedSubtotal += expectedItemSubtotal * item.quantity;
|
|
370
|
-
}
|
|
371
|
-
let verifiedDiscount = 0;
|
|
372
|
-
const orderDiscount = orderData.discount ?? 0;
|
|
373
|
-
const orderPromotionId = orderData.promotionId;
|
|
374
|
-
if (orderPromotionId && orderDiscount > 0) {
|
|
375
|
-
const promoDoc = await db.collection("promotions").doc(orderPromotionId).get();
|
|
376
|
-
if (!promoDoc.exists || !promoDoc.data()?.isActive) {
|
|
377
|
-
return json(
|
|
378
|
-
{ error: "El cupon aplicado ya no es valido. Remuevelo y vuelve a intentar." },
|
|
379
|
-
{ status: 409 }
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
const promo = promoDoc.data();
|
|
383
|
-
const now = /* @__PURE__ */ new Date();
|
|
384
|
-
const validFrom = promo.rules.validFrom.toDate ? promo.rules.validFrom.toDate() : new Date(promo.rules.validFrom);
|
|
385
|
-
const validUntil = promo.rules.validUntil.toDate ? promo.rules.validUntil.toDate() : new Date(promo.rules.validUntil);
|
|
386
|
-
if (now < validFrom || now > validUntil) {
|
|
387
|
-
return json(
|
|
388
|
-
{ error: "El cupon aplicado ha expirado. Remuevelo y vuelve a intentar." },
|
|
389
|
-
{ status: 409 }
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
if (promo.rules.maxTotalUses && promo.currentUses >= promo.rules.maxTotalUses) {
|
|
393
|
-
return json(
|
|
394
|
-
{ error: "El cupon aplicado ya alcanzo su limite de usos." },
|
|
395
|
-
{ status: 409 }
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
if (promo.type === "percentage") {
|
|
399
|
-
verifiedDiscount = verifiedSubtotal * (promo.value / 100);
|
|
400
|
-
if (promo.maxDiscountAmount) {
|
|
401
|
-
verifiedDiscount = Math.min(verifiedDiscount, promo.maxDiscountAmount);
|
|
402
|
-
}
|
|
403
|
-
} else if (promo.type === "fixed_amount") {
|
|
404
|
-
verifiedDiscount = Math.min(promo.value, verifiedSubtotal);
|
|
405
|
-
}
|
|
406
|
-
verifiedDiscount = Math.round(verifiedDiscount * 100) / 100;
|
|
407
|
-
}
|
|
408
|
-
const verifiedDiscountedSubtotal = Math.max(0, verifiedSubtotal - verifiedDiscount);
|
|
409
|
-
const verifiedVat = Math.round(verifiedDiscountedSubtotal * 0.15 * 100) / 100;
|
|
410
|
-
const verifiedTotal = Math.round((verifiedDiscountedSubtotal + verifiedVat) * 100) / 100;
|
|
411
|
-
if (verifiedTotal !== amount) {
|
|
412
|
-
return json(
|
|
413
|
-
{
|
|
414
|
-
error: "El monto no coincide con los precios actuales. Actualiza tu carrito."
|
|
415
|
-
},
|
|
416
|
-
{ status: 409 }
|
|
417
|
-
);
|
|
671
|
+
const validationError = await validateStandardOrder({
|
|
672
|
+
db,
|
|
673
|
+
getPriceDisplay: deps.getPriceDisplay,
|
|
674
|
+
orderData,
|
|
675
|
+
amount
|
|
676
|
+
});
|
|
677
|
+
if (validationError) {
|
|
678
|
+
return validationError;
|
|
418
679
|
}
|
|
419
680
|
}
|
|
420
681
|
const functionsBase = deps.cloudFunctionsBaseUrl ?? process.env.CLOUD_FUNCTIONS_BASE_URL;
|
|
@@ -426,7 +687,7 @@ function createChargeHandler(deps) {
|
|
|
426
687
|
}
|
|
427
688
|
const clientIp = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-real-ip") || "127.0.0.1";
|
|
428
689
|
const enrichedBrowserInfo = browserInfo ? { ...browserInfo, ip_address: clientIp } : void 0;
|
|
429
|
-
const discountedSubtotalForTax = amount - (vat ?? 0);
|
|
690
|
+
const discountedSubtotalForTax = taxableAmount ?? amount - (vat ?? 0);
|
|
430
691
|
const nuveiData = await debitWithToken({
|
|
431
692
|
userId,
|
|
432
693
|
userEmail,
|
|
@@ -440,7 +701,6 @@ function createChargeHandler(deps) {
|
|
|
440
701
|
...installments ? { installments, installmentsType: installmentsType ?? 0 } : {},
|
|
441
702
|
...enrichedBrowserInfo && termUrl ? { browserInfo: enrichedBrowserInfo, termUrl } : {}
|
|
442
703
|
});
|
|
443
|
-
logger.log("[charge] Nuvei debit response FULL:", JSON.stringify(nuveiData));
|
|
444
704
|
logger.log(
|
|
445
705
|
"[charge] Nuvei debit response summary:",
|
|
446
706
|
JSON.stringify({
|
|
@@ -452,221 +712,52 @@ function createChargeHandler(deps) {
|
|
|
452
712
|
})
|
|
453
713
|
);
|
|
454
714
|
if (nuveiData.transaction && nuveiData.transaction.status === "success" && nuveiData.transaction.status_detail === 3) {
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
authorizationCode: rawAuthCode,
|
|
464
|
-
...installments ? { installments, installmentsType: installmentsType ?? 0 } : {},
|
|
465
|
-
chargeResponseAt: /* @__PURE__ */ new Date(),
|
|
466
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
467
|
-
});
|
|
468
|
-
} else {
|
|
469
|
-
logger.error(
|
|
470
|
-
`[AUDIT:MISSING_AUTH_CODE] orderId=${orderId} txId=${nuveiData.transaction.id} debitResponse=${JSON.stringify(nuveiData)}`
|
|
471
|
-
);
|
|
472
|
-
batch.update(orderRef, {
|
|
473
|
-
status: "paid",
|
|
474
|
-
paymentTransactionId: nuveiData.transaction.id,
|
|
475
|
-
missingAuthCodeFlagged: true,
|
|
476
|
-
missingAuthCodeLoggedAt: /* @__PURE__ */ new Date(),
|
|
477
|
-
emailPending: true,
|
|
478
|
-
...installments ? { installments, installmentsType: installmentsType ?? 0 } : {},
|
|
479
|
-
chargeResponseAt: /* @__PURE__ */ new Date(),
|
|
480
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
const orderSnap = await orderRef.get();
|
|
484
|
-
const orderInfo = orderSnap.data() ?? {};
|
|
485
|
-
if (orderInfo.promotionId) {
|
|
486
|
-
const promoRef = db.collection("promotions").doc(orderInfo.promotionId);
|
|
487
|
-
batch.update(promoRef, {
|
|
488
|
-
currentUses: FieldValue.increment(1)
|
|
489
|
-
});
|
|
490
|
-
const usageRef = promoRef.collection("usages").doc();
|
|
491
|
-
batch.set(usageRef, {
|
|
492
|
-
userId: orderInfo.userId,
|
|
493
|
-
orderId,
|
|
494
|
-
discountApplied: orderInfo.discount || 0,
|
|
495
|
-
usedAt: /* @__PURE__ */ new Date()
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
await batch.commit();
|
|
499
|
-
if (deps.onPaymentSucceeded) {
|
|
500
|
-
try {
|
|
501
|
-
await deps.onPaymentSucceeded({ ...orderInfo, id: orderId });
|
|
502
|
-
} catch (err) {
|
|
503
|
-
logger.error(
|
|
504
|
-
`[charge] onPaymentSucceeded hook failed for order ${orderId}:`,
|
|
505
|
-
err
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
let emailSent = false;
|
|
510
|
-
if (hasValidAuthCode) {
|
|
511
|
-
const emailResult = await deps.email.sendPaymentConfirmation({
|
|
512
|
-
to: userEmail,
|
|
513
|
-
customerName: orderInfo.shippingAddress?.fullName || "",
|
|
715
|
+
const finalizeResult = await finalizePaidOrder(
|
|
716
|
+
{
|
|
717
|
+
db,
|
|
718
|
+
email: deps.email,
|
|
719
|
+
onPaymentSucceeded: deps.onPaymentSucceeded,
|
|
720
|
+
logger
|
|
721
|
+
},
|
|
722
|
+
{
|
|
514
723
|
orderId,
|
|
515
724
|
transactionId: nuveiData.transaction.id,
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
if (emailSent) {
|
|
527
|
-
await orderRef.update({ emailSentAt: /* @__PURE__ */ new Date() });
|
|
725
|
+
rawAuthCode: nuveiData.transaction.authorization_code,
|
|
726
|
+
debitResponse: nuveiData,
|
|
727
|
+
installments,
|
|
728
|
+
installmentsType,
|
|
729
|
+
userId,
|
|
730
|
+
userEmail,
|
|
731
|
+
vat,
|
|
732
|
+
amount,
|
|
733
|
+
token,
|
|
734
|
+
deleteCardAfterPayment: body.deleteCardAfterPayment
|
|
528
735
|
}
|
|
529
|
-
|
|
530
|
-
if (body.deleteCardAfterPayment && token) {
|
|
531
|
-
deleteCard(token, userId).catch(
|
|
532
|
-
(err) => logger.error("[charge] Failed to delete card after payment:", err)
|
|
533
|
-
);
|
|
534
|
-
}
|
|
736
|
+
);
|
|
535
737
|
return json({
|
|
536
738
|
success: true,
|
|
537
|
-
transactionId:
|
|
538
|
-
authorizationCode:
|
|
539
|
-
orderId,
|
|
540
|
-
emailSent
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
if (nuveiData.transaction?.status === "pending" && nuveiData.transaction?.status_detail === 1) {
|
|
544
|
-
await db.collection("orders").doc(orderId).update({
|
|
545
|
-
status: "processing",
|
|
546
|
-
paymentTransactionId: nuveiData.transaction.id || null,
|
|
547
|
-
chargeResponseAt: /* @__PURE__ */ new Date(),
|
|
548
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
549
|
-
});
|
|
550
|
-
return json({
|
|
551
|
-
review: true,
|
|
739
|
+
transactionId: finalizeResult.transactionId,
|
|
740
|
+
authorizationCode: finalizeResult.authorizationCode,
|
|
552
741
|
orderId,
|
|
553
|
-
|
|
742
|
+
emailSent: finalizeResult.emailSent
|
|
554
743
|
});
|
|
555
744
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
566
|
-
});
|
|
567
|
-
return json({
|
|
568
|
-
challenge: true,
|
|
569
|
-
challengeHtml: hiddenIframeHtml,
|
|
570
|
-
isDeviceFingerprint: true,
|
|
571
|
-
orderId,
|
|
572
|
-
nuveiTransactionId: nuveiData.transaction.id,
|
|
573
|
-
statusDetail: 35
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
if (nuveiData.transaction?.status_detail === 31) {
|
|
577
|
-
await db.collection("orders").doc(orderId).update({
|
|
578
|
-
status: "otp-pending",
|
|
579
|
-
nuveiTransactionId: nuveiData.transaction.id || null,
|
|
580
|
-
...persistDeleteOnPaid,
|
|
581
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
582
|
-
});
|
|
583
|
-
return json({
|
|
584
|
-
otpRequired: true,
|
|
745
|
+
return handleNonSuccessResult(
|
|
746
|
+
{
|
|
747
|
+
db,
|
|
748
|
+
email: deps.email,
|
|
749
|
+
getRetryUrl: deps.getRetryUrl,
|
|
750
|
+
logger
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
nuveiData,
|
|
585
754
|
orderId,
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
if (nuveiData.transaction?.status_detail === 36 || nuveiData.transaction?.status_detail === 37) {
|
|
591
|
-
const threeDSData = nuveiData["3ds"];
|
|
592
|
-
const challengeHtml = threeDSData?.browser_response?.challenge_request || threeDSData?.browser_response?.hidden_iframe || "";
|
|
593
|
-
const rawDebit = nuveiData;
|
|
594
|
-
const debitCres = rawDebit["3ds"]?.authentication?.["cres"] || rawDebit["3ds"]?.browser_response?.["cres"] || rawDebit["3ds"]?.["cres"] || rawDebit.transaction?.["cres"] || rawDebit["cres"] || rawDebit["value"] || null;
|
|
595
|
-
logger.log(
|
|
596
|
-
`[charge] status ${nuveiData.transaction.status_detail} \u2014 debitCres captured: ${debitCres ? "YES (len=" + debitCres.length + ")" : "NO"}`
|
|
597
|
-
);
|
|
598
|
-
if (challengeHtml) {
|
|
599
|
-
await db.collection("orders").doc(orderId).update({
|
|
600
|
-
status: "3ds-pending",
|
|
601
|
-
nuveiTransactionId: nuveiData.transaction?.id || null,
|
|
602
|
-
...debitCres ? { threeDSCres: debitCres } : {},
|
|
603
|
-
...persistDeleteOnPaid,
|
|
604
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
605
|
-
});
|
|
606
|
-
return json({
|
|
607
|
-
challenge: true,
|
|
608
|
-
challengeHtml,
|
|
609
|
-
isDeviceFingerprint: false,
|
|
610
|
-
orderId,
|
|
611
|
-
nuveiTransactionId: nuveiData.transaction?.id,
|
|
612
|
-
statusDetail: nuveiData.transaction?.status_detail
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
const threeDSStatus = nuveiData["3ds"]?.authentication?.status;
|
|
617
|
-
if (threeDSStatus && THREEDS_AUTH_MESSAGES[threeDSStatus]) {
|
|
618
|
-
const threeDSErrorMsg = THREEDS_AUTH_MESSAGES[threeDSStatus];
|
|
619
|
-
const currentOrder2 = await db.collection("orders").doc(orderId).get();
|
|
620
|
-
const currentOrderData2 = currentOrder2.data() ?? {};
|
|
621
|
-
if (currentOrderData2.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?.({ ...currentOrderData2, id: orderId });
|
|
630
|
-
deps.email.sendPaymentFailed({
|
|
631
|
-
to: userEmail,
|
|
632
|
-
customerName: currentOrderData2.shippingAddress?.fullName || "",
|
|
633
|
-
orderId,
|
|
634
|
-
errorMessage: threeDSErrorMsg,
|
|
635
|
-
items: currentOrderData2.items || [],
|
|
636
|
-
total: currentOrderData2.total || amount,
|
|
637
|
-
...retryUrl ? { retryUrl } : {}
|
|
638
|
-
}).catch(() => {
|
|
639
|
-
});
|
|
755
|
+
userEmail,
|
|
756
|
+
amount,
|
|
757
|
+
token,
|
|
758
|
+
deleteCardAfterPayment: body.deleteCardAfterPayment
|
|
640
759
|
}
|
|
641
|
-
return json({ error: threeDSErrorMsg }, { status: 400 });
|
|
642
|
-
}
|
|
643
|
-
const failedErrorMsg = getNuveiUserMessage(
|
|
644
|
-
nuveiData.transaction?.status_detail,
|
|
645
|
-
nuveiData.transaction?.message || nuveiData.error?.description
|
|
646
760
|
);
|
|
647
|
-
const currentOrder = await db.collection("orders").doc(orderId).get();
|
|
648
|
-
const currentOrderData = currentOrder.data() ?? {};
|
|
649
|
-
if (currentOrderData.status === "pending") {
|
|
650
|
-
await db.collection("orders").doc(orderId).update({
|
|
651
|
-
status: "failed",
|
|
652
|
-
chargeResponseAt: /* @__PURE__ */ new Date(),
|
|
653
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
if (userEmail) {
|
|
657
|
-
const retryUrl = deps.getRetryUrl?.({ ...currentOrderData, id: orderId });
|
|
658
|
-
deps.email.sendPaymentFailed({
|
|
659
|
-
to: userEmail,
|
|
660
|
-
customerName: currentOrderData.shippingAddress?.fullName || "",
|
|
661
|
-
orderId,
|
|
662
|
-
errorMessage: failedErrorMsg,
|
|
663
|
-
items: currentOrderData.items || [],
|
|
664
|
-
total: currentOrderData.total || amount,
|
|
665
|
-
...retryUrl ? { retryUrl } : {}
|
|
666
|
-
}).catch(() => {
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
return json({ error: failedErrorMsg }, { status: 400 });
|
|
670
761
|
} catch (error) {
|
|
671
762
|
logger.error("Payment charge error:", error);
|
|
672
763
|
return json(
|
|
@@ -765,72 +856,28 @@ function createWebhookHandler(deps) {
|
|
|
765
856
|
const vTxId = verifyResult.transaction?.id ?? raw["transaction_id"] ?? orderDataEarly.nuveiTransactionId ?? transaction.id;
|
|
766
857
|
const verifySuccess = (vStatus === "success" || vStatus === 1) && vDetail === 3;
|
|
767
858
|
if (verifySuccess) {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
status: "paid",
|
|
772
|
-
paymentTransactionId: vTxId,
|
|
773
|
-
...hasAuth ? { authorizationCode: vAuthCode } : {
|
|
774
|
-
missingAuthCodeFlagged: true,
|
|
775
|
-
missingAuthCodeLoggedAt: /* @__PURE__ */ new Date(),
|
|
776
|
-
emailPending: true
|
|
777
|
-
},
|
|
778
|
-
chargeResponseAt: /* @__PURE__ */ new Date(),
|
|
779
|
-
updatedAt: /* @__PURE__ */ new Date(),
|
|
780
|
-
threeDSCres: FieldValue.delete(),
|
|
781
|
-
threeDSTransStatus: FieldValue.delete()
|
|
782
|
-
});
|
|
783
|
-
if (orderDataEarly.promotionId) {
|
|
784
|
-
const promoRef = db.collection("promotions").doc(orderDataEarly.promotionId);
|
|
785
|
-
batch.update(promoRef, {
|
|
786
|
-
currentUses: FieldValue.increment(1)
|
|
787
|
-
});
|
|
788
|
-
const usageRef = promoRef.collection("usages").doc();
|
|
789
|
-
batch.set(usageRef, {
|
|
790
|
-
userId: orderDataEarly.userId,
|
|
791
|
-
orderId,
|
|
792
|
-
discountApplied: orderDataEarly.discount || 0,
|
|
793
|
-
usedAt: /* @__PURE__ */ new Date()
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
|
-
await batch.commit();
|
|
797
|
-
if (hasAuth && orderDataEarly.userEmail) {
|
|
798
|
-
await deps.email.sendPaymentConfirmation({
|
|
799
|
-
to: orderDataEarly.userEmail,
|
|
800
|
-
customerName: orderDataEarly.shippingAddress?.fullName || "",
|
|
859
|
+
await finalizePaidOrder(
|
|
860
|
+
{ db, email: deps.email, onPaymentSucceeded: deps.onPaymentSucceeded, logger },
|
|
861
|
+
{
|
|
801
862
|
orderId,
|
|
802
863
|
transactionId: vTxId,
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
vat:
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
(err) => logger.error(
|
|
819
|
-
"[webhook] Failed to delete card after payment:",
|
|
820
|
-
err
|
|
821
|
-
)
|
|
822
|
-
);
|
|
823
|
-
}
|
|
824
|
-
if (deps.onPaymentSucceeded) {
|
|
825
|
-
try {
|
|
826
|
-
await deps.onPaymentSucceeded({ ...orderDataEarly, id: orderId });
|
|
827
|
-
} catch (err) {
|
|
828
|
-
logger.error(
|
|
829
|
-
`[webhook] onPaymentSucceeded hook failed for ${orderId}:`,
|
|
830
|
-
err
|
|
831
|
-
);
|
|
864
|
+
rawAuthCode: vAuthCode ?? null,
|
|
865
|
+
debitResponse: verifyResult,
|
|
866
|
+
auditResponseLabel: "verifyResponse",
|
|
867
|
+
userId: orderDataEarly.userId,
|
|
868
|
+
userEmail: orderDataEarly.userEmail || "",
|
|
869
|
+
vat: 0,
|
|
870
|
+
amount: orderDataEarly.total || 0,
|
|
871
|
+
token: orderDataEarly.paymentToken,
|
|
872
|
+
deleteCardAfterPayment: orderDataEarly.deleteCardAfterPayment,
|
|
873
|
+
preReadOrder: orderDataEarly,
|
|
874
|
+
includePostPurchaseNote: false,
|
|
875
|
+
extraOrderFields: {
|
|
876
|
+
threeDSCres: FieldValue.delete(),
|
|
877
|
+
threeDSTransStatus: FieldValue.delete()
|
|
878
|
+
}
|
|
832
879
|
}
|
|
833
|
-
|
|
880
|
+
);
|
|
834
881
|
return json({ received: true, verifiedFromWebhook: true });
|
|
835
882
|
}
|
|
836
883
|
} catch (err) {
|
|
@@ -1206,94 +1253,39 @@ function create3dsCompleteHandler(deps) {
|
|
|
1206
1253
|
return json({ stillPending: true });
|
|
1207
1254
|
}
|
|
1208
1255
|
if (isSuccess) {
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
updatedAt: /* @__PURE__ */ new Date(),
|
|
1218
|
-
threeDSCres: FieldValue.delete(),
|
|
1219
|
-
threeDSTransStatus: FieldValue.delete(),
|
|
1220
|
-
isDeviceFingerprint: FieldValue.delete()
|
|
1221
|
-
});
|
|
1222
|
-
} else {
|
|
1223
|
-
logger.error(
|
|
1224
|
-
`[AUDIT:MISSING_AUTH_CODE] orderId=${orderId} txId=${txId} verifyResponse=${JSON.stringify(verifyResult)}`
|
|
1225
|
-
);
|
|
1226
|
-
batch.update(orderRef, {
|
|
1227
|
-
status: "paid",
|
|
1228
|
-
paymentTransactionId: txId,
|
|
1229
|
-
missingAuthCodeFlagged: true,
|
|
1230
|
-
missingAuthCodeLoggedAt: /* @__PURE__ */ new Date(),
|
|
1231
|
-
emailPending: true,
|
|
1232
|
-
chargeResponseAt: /* @__PURE__ */ new Date(),
|
|
1233
|
-
updatedAt: /* @__PURE__ */ new Date(),
|
|
1234
|
-
threeDSCres: FieldValue.delete(),
|
|
1235
|
-
threeDSTransStatus: FieldValue.delete(),
|
|
1236
|
-
isDeviceFingerprint: FieldValue.delete()
|
|
1237
|
-
});
|
|
1238
|
-
}
|
|
1239
|
-
if (orderData.promotionId) {
|
|
1240
|
-
const promoRef = db.collection("promotions").doc(orderData.promotionId);
|
|
1241
|
-
batch.update(promoRef, {
|
|
1242
|
-
currentUses: FieldValue.increment(1)
|
|
1243
|
-
});
|
|
1244
|
-
const usageRef = promoRef.collection("usages").doc();
|
|
1245
|
-
batch.set(usageRef, {
|
|
1246
|
-
userId: orderData.userId,
|
|
1247
|
-
orderId,
|
|
1248
|
-
discountApplied: orderData.discount || 0,
|
|
1249
|
-
usedAt: /* @__PURE__ */ new Date()
|
|
1250
|
-
});
|
|
1251
|
-
}
|
|
1252
|
-
await batch.commit();
|
|
1253
|
-
if (deps.onPaymentSucceeded) {
|
|
1254
|
-
try {
|
|
1255
|
-
await deps.onPaymentSucceeded({ ...orderData, id: orderId });
|
|
1256
|
-
} catch (err) {
|
|
1257
|
-
logger.error(
|
|
1258
|
-
`[3ds-complete] onPaymentSucceeded hook failed for ${orderId}:`,
|
|
1259
|
-
err
|
|
1260
|
-
);
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
let emailSent = false;
|
|
1264
|
-
if (hasValidAuthCode) {
|
|
1265
|
-
const userEmail2 = orderData.userEmail || decodedToken.email || "";
|
|
1266
|
-
const emailResult = await deps.email.sendPaymentConfirmation({
|
|
1267
|
-
to: userEmail2,
|
|
1268
|
-
customerName: orderData.shippingAddress?.fullName || "",
|
|
1256
|
+
const finalizeResult = await finalizePaidOrder(
|
|
1257
|
+
{
|
|
1258
|
+
db,
|
|
1259
|
+
email: deps.email,
|
|
1260
|
+
onPaymentSucceeded: deps.onPaymentSucceeded,
|
|
1261
|
+
logger
|
|
1262
|
+
},
|
|
1263
|
+
{
|
|
1269
1264
|
orderId,
|
|
1270
1265
|
transactionId: txId,
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
vat:
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1266
|
+
rawAuthCode: txAuthCode,
|
|
1267
|
+
debitResponse: verifyResult,
|
|
1268
|
+
auditResponseLabel: "verifyResponse",
|
|
1269
|
+
userId: orderData.userId,
|
|
1270
|
+
userEmail: orderData.userEmail || decodedToken.email || "",
|
|
1271
|
+
vat: 0,
|
|
1272
|
+
amount: orderData.total || 0,
|
|
1273
|
+
token: orderData.paymentToken,
|
|
1274
|
+
deleteCardAfterPayment: orderData.deleteCardAfterPayment,
|
|
1275
|
+
preReadOrder: orderData,
|
|
1276
|
+
includePostPurchaseNote: false,
|
|
1277
|
+
extraOrderFields: {
|
|
1278
|
+
threeDSCres: FieldValue.delete(),
|
|
1279
|
+
threeDSTransStatus: FieldValue.delete(),
|
|
1280
|
+
isDeviceFingerprint: FieldValue.delete()
|
|
1281
|
+
}
|
|
1282
1282
|
}
|
|
1283
|
-
|
|
1284
|
-
if (orderData.deleteCardAfterPayment && orderData.paymentToken) {
|
|
1285
|
-
deleteCard(
|
|
1286
|
-
orderData.paymentToken,
|
|
1287
|
-
orderData.userId
|
|
1288
|
-
).catch(
|
|
1289
|
-
(err) => logger.error("[3ds-complete] Failed to delete card after payment:", err)
|
|
1290
|
-
);
|
|
1291
|
-
}
|
|
1283
|
+
);
|
|
1292
1284
|
return json({
|
|
1293
1285
|
success: true,
|
|
1294
|
-
transactionId:
|
|
1295
|
-
authorizationCode:
|
|
1296
|
-
emailSent,
|
|
1286
|
+
transactionId: finalizeResult.transactionId,
|
|
1287
|
+
authorizationCode: finalizeResult.authorizationCode,
|
|
1288
|
+
emailSent: finalizeResult.emailSent,
|
|
1297
1289
|
orderId
|
|
1298
1290
|
});
|
|
1299
1291
|
}
|