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