Las redes son poco confiables. Un request puede fallar, pero la transacción puede haberse creado. Si reintentas ciegamente, terminas creando dos pagos para un solo pedido, y el usuario es cobrado dos veces.
async function iniciarPagoSeguro(datosRequest) { try { const { data } = await axios.post( `${ZP_API_URL}/InicioPago`, datosRequest, { timeout: 15000 } ); if (data.int_codigo === 1) { return { exitoso: true, url: data.str_url }; } return { exitoso: false, razon: data.str_descripcion_error }; } catch (err) { // Error de red — NO sabemos si el pago se creó o no logger.warn("InicioPago con error de red, verificando", { str_id_pago: datosRequest.InformacionPago.str_id_pago }); // Esperar 5s a que el backend de ZonaPagos termine de escribir (si es que lo hizo) await new Promise(r => setTimeout(r, 5000)); const verif = await verificarPago(datosRequest.InformacionPago.str_id_pago); if (verif.encontrado) { // El pago SÍ se creó a pesar del error de red logger.info("Pago creado a pesar del error", { str_id_pago: datosRequest.InformacionPago.str_id_pago }); // No tenemos str_url, hay que pedir al usuario que reinicie return { exitoso: false, razon: "Pago en estado desconocido — reinicia el checkout", requiere_revision: true }; } // El pago NO se creó, podemos reintentar con mismo str_id_pago throw err; }}
El usuario puede recargar tu página de retorno, o ZonaPagos puede disparar el callback dos veces.
app.get("/pago/retorno", async (req, res) => { const { id_pago } = req.query; // Idempotencia: solo procesar si el pago aún no está cerrado const pago = await db.pagos.findOne({ str_id_pago: id_pago }); if (pago.estado_local === "aprobado") { // Ya fue procesado antes — solo redirigir a la página de gracias return res.redirect(`/gracias/${pago.id}`); } if (pago.estado_local === "rechazado") { return res.redirect(`/pago-rechazado/${pago.id}`); } // Primera vez que llega este callback para este pago const estado = await verificarEstado(id_pago); await procesarEstado(pago, estado); // ...});
async function entregarProducto(pagoId) { const resultado = await db.query( `UPDATE pedidos SET estado = 'entregado', dt_entrega = NOW() WHERE pago_id = $1 AND estado != 'entregado' RETURNING id`, [pagoId] ); if (resultado.rowCount === 0) { logger.info("Pedido ya entregado previamente", { pagoId }); return; } // Solo llegamos aquí si efectivamente cambió el estado await notificarWarehouse(pagoId); await enviarEmailEntrega(pagoId);}