Hace unas semanas, tuvimos que implementar pagos únicos de Paypal en Courseit. Fue más dificil de lo que pensábamos, porque si bien Paypal ofrece un botón de pago muy bueno, debido a nuestro flujo de tiempo de suscripción, es necesario tenerlo integrado con un backend, donde podamos modificar a los usuarios de una manera más segura. Es por todo esto, que decidimos hacer una integración directa con la api de Paypal, y acá es cuando empezaron los problemas con la documentación.
La documentación de Paypal suele ser muy intuitiva, fácil de comprender, y fácil de probar, pero, debido a cómo manejan los pagos únicos, tuvimos dificultades.
Ahora bien, en este blog, les voy a mostrar paso a paso cómo replicar una integración con la api de paypal, utilizando express. La integración va a consistir en crear un link de pago, con la información correspondiente a la compra, capturar el pago, y recibir las notificaciones webhooks correspondientes.
Empecemos.
Debemos ingresar al dashboard de desarrollador de paypal. Una vez dentro, tenemos que asegurarnos de estar parados sobre sandbox.
Luego, tenemos que crear una nueva Aplicación. Lo hacemos clickeando en el botón create app
Luego, completamos el formulario como se muestra en la imágen, utilizando un nombre acorde.
Una vez tengamos creada la aplicación nos va a redirigir a la siguiente página, donde vamos a poder ver un email, el client ID, y el client Secret
Otro detalle importante es este campo, que es la url de redirección luego de la transacción. Por ahora dejemoslo en blanco, pero sepan que si eventualmente van a llevar esto a producción, van a necesitar configurarlo
Vamos a abrir la terminal, crear una carpeta que se llama paypal-api, o como ustedes deseen, y parados dentro de esa carpeta ejecutar el código:
npx express-generator --no-view
Una vez terminado, vamos a ejecutar el comando
npm install
También vamos a necesitar axios para hacer los llamados a la api de Paypal, asi que para instalarlo ejecutamos:
npm install axios
Y por último vamos a abrirlo en nuestro editor preferido, en mi caso visual studio code.
Ni bien tengamos abierto el proyecto en visual studio code, vamos a ir al archivo app.js
, y pasar todas las var a const. Vamos a hacer exactamente lo mismo en los archivos /routes/index.js
y /routes/users.js
Adicionalmente, vamos a eliminar la carpeta public y todo su contenido.
El controller, es una clase de javascript, que vamos a instanciar, para poder utilizar las funciones que se encuentran en dicha clase. Su propósito va a ser manejar los request y las responses de manera eficiente, y ordenada.
Para eso, vamos a crear al mismo nivel que la carpeta routes
una carpeta que se llame controllers
, dentro de esa carpeta vamos a crear un archivo que se llame PaypalController.js
Dentro del controller, vamos a crear una clase que se llame PaypalController, que va a tener un constructor, y eventualmente 4 funciones.
const axios = require("axios");
//importamos axios para hacer las llamadas a la api de Paypal
const querystring = require("querystring");
//lo vamos a necesitar para pedir el token, no es necesario instalarlo
class PaypalController {
constructor() {
this.Paypal = {
url: "https://api.sandbox.paypal.com",
//usamos sandbox porque nuestro clientId y clientSecret son de sandbox
clientId: "clientId" //acá va su client ID,
clientSecret: "clientSecret" //acá va su client Secret
//acá vamos a guardar de una manera muy insegura las keys de paypal (lo ideal es guardarlas en .env, y llamarlas donde sea indicado)
};
}
async getPaymentLink(req, res) {
//esta función va a devolver el link de pago
}
async generateLink({ price }) {
//esta función va a generar el link de pago
}
generateToken() {
//esta función va a generar el token para poder hacer los request a Paypal
}
async webhook(req, res) {
//esta función va a recibir los webhooks, solamente los vamos a poder probar si tenemos nuestra api deployada
}
}
module.exports = PaypalController;
//exportamos la clase
Dentro de nuestra función generateToken()
que se encuentra dentro de nuestro Controller. Vamos a hacer un request a una url
Esa url es la siguiente: ${this.Paypal.url}/v1/oauth2/token
Estamos usando this, para acceder al entorno de la clase, y luego hacemos this.Paypal, para acceder al objeto que declaramos en nuestro constructor, y por último this.Paypal.url para acceder a la url de la api de Paypal.
Luego, tenemos que hacer un request del tipo POST, y envíar en el body un objeto. Este objeto, contiene las preferencias que vamos a utilizar para generar el token, algunas son requeridas por Paypal, para poder autenticarnos y obtener el token.
Como vamos a utilizar axios, para hacer este request POST, tenemos que aclararle el tipo de request, y el tipo de información que estamos enviando, por eso creamos el objeto settings. En síntesis, el request, quedaría de la siguiente manera
const settings = {
method: "POST",
//aclaramos que vamos a hacer un POST
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
//aclaramos en los headers el tipo de contenido a enviar
data: querystring.stringify({ grant_type: "client_credentials" }),
//mandamos en el body el grant_type, que es algo requerido por paypal
auth: {
username: this.Paypal.clientId,
password: this.Paypal.clientSecret
},
//enviamos el objeto auth, que contiene el clientId y el clientSecret
url: `${this.Paypal.url}/v1/oauth2/token`
//determinamos la url a dónde lo queremos enviar
};
return axios(settings).then((response) => {
//esperamos a que se complete el request, y devolvemos la información requerida
return response.data;
});
Este token de paypal, lo vamos a necesitar para generar el link de pago, que es lo que vamos a hacer a continuación
Dentro de nuestra función generateLink()
vamos a generar el link de pago. Para hacerlo, tenemos que decirle a paypal determinada información, entre esas cosas se encuentran: el precio y la moneda de pago.
async generateLink({ price }) {
const url = `${this.Paypal.url}/v2/checkout/orders`;
//determinamos la url
const data = await this.generateToken();
//generamos el token
const access_token = await data.access_token;
//leemos el access_token
const settings = {
method: "POST",
//hacemos un POST
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${access_token}`
},
//en el header mandamos el TOKEN
data: {
//enviamos la información requerida por PAYPAL
intent: "CAPTURE",
purchase_units: [
//este es un array de artículos, con su respectivo precio
{
amount: {
currency_code: "USD",
//la moneda a utilizar
value: price.toString()
//el precio, que deber ser un string
}
}
],
application_context: {
//información adicional sobre cómo queremos que sea el checkout
brand_name: "Marca",
//el nombre de la marca que va a aparecer cuando el usuario intente comprar
locale: "es-ES",
//el idioma que va a intentar a utilizar en el checkout
user_action: "PAY_NOW",
//la acción que va a realizar el usuario, generalmente siempre queremos PAY_NOW
landing_page: "NO_PREFERENCE",
//esto es por si lo queremos enviar a un flujo puntual de paypal, por ejemplo al login, a pagar, o a otro lado.
payment_method: {
//esta es la información que limita los métodos de pago
payer_selected: "PAYPAL",
payee_preferred: "IMMEDIATE_PAYMENT_REQUIRED"
},
shipping_preference: "NO_SHIPPING",
//aclaramos que no vamos a relizar un envio, ya que es un servicio en este ejemplo
return_url: ""
//tenemos la opción de ingresar la url a la cual será redirigido el usuario una vez que la transacción sea completada
}
},
url
//el endpoint de la api de Payapal
};
return axios(settings).then(async (response) => {
// suelen venir muchos links en la respuesta, es por eso que tenemos que hacer un find para encontrar el que necesitamos
if (response && response.data && response.data.links) {
const link = response.data.links.find((link) => {
return link.rel == "approve";
//el que necesitamos viene con un rel: "approve"
});
//retornamos el link que tiene rel: "approve"
return { link: link.href };
}
return;
});
}
Muy bien, ya estamos terminando con la generación del link. Lo que tenemos que hacer ahora es escuchar los request, y enviar la response. Para eso en nuestra función getPaymentLink(req, res)
async getPaymentLink(req, res) {
try {
const payment = await this.generateLink({ price: 100 });
//ejecutamos la función de generarl el link, pasandole por param, el precio, esto es para simular un POST a nuestra api
return res.json({
linkPaypal: payment.link
});
//retornamos el link
} catch (e) {
//si pasa algo mal, devolvemos un status 500, y un mensaje de error
return res.sendStatus(500).json({
error: true,
message: "Hubo un error al general el link de pago"
});
}
}
Hasta el paso 8, hicimos toda la generación del link de pago, pero hay un problema. Paypal no solamente requiere que el usuario pague, si no también que nosotros, capturemos el pago, ¿cómo hacemos eso? con los webhooks.
Lo que vamos a tener que hacer es esperar a recibir una notificación webhook donde el valor de event_type
sea CHECKOUT.ORDER.APPROVED
Una vez que sabemos que la orden está aprobada, podemos pasar a capturarla. Para capturarla tenemos que hacer un request del tipo POST al endpoint siguiente:
`${this.Paypal.url}/v2/checkout/orders/${req.body.resource.id}/capture
Donde resourse.id
, es el id de la orden que estamos tratando de capturar.
Entonces, sabiendo esto, nuestra función webhook()
queda de la siguiente manera:
async webhook(req, res) {
if (req.body.event_type == "CHECKOUT.ORDER.APPROVED") {
//verificamos que el tipo de webhook sea CHECKOUT.ORDER.APPROVED
const data = await this.paypalService.getToken();
const access_token = await data.access_token;
//generamos nuevo token
//tenemos que capturar la orden para cobrarla
await axios({
method: "POST",
//hacemos un post
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${access_token}`
//enviamos el token
},
url: `${this.Paypal.url}/v2/checkout/orders/${req.body.resource.id}/capture`
//esta url contiene la orden que estamos tratando de capturar
});
}
return res.status(200);
//este return es obligatorio para hacerle saber a Paypal que se recibió la notificación que nos envió
}
Antes que nada, necesitamos un endpoint en nuestra api para poder ejecutar la función que genera el link de pago. Asi como también otra ruta que permita que paypal nos envíe las notificaciones webhooks.
Por estas razones, vamos a crear dentro de la carpeta routes
un archivo que se llame paypal.js
Dentro de ese archivo paypal.js
vamos a insertar lo siguiente:
const express = require("express");
const router = express.Router();
const PaypalController = require("../controllers/PaypalController");
//importamos el Controller
const PaypalInstance = new PaypalController();
//instanciamos la clase que esta en el controller
router.get("/", (req, res) => {
//creamos la ruta, que en definitiva es /paypal/
//ejecutamos la funcion getPaymentLink de nuestro controller
PaypalInstance.getPaymentLink(req, res);
});
router.post("/webhook", (req, res) => {
//tiene que ser una ruta tipo POST para recibir los webhooks
//creamos la ruta, que en definitiva es /paypal/webhook
//ejecutamos la funcion webhook que esta en nuestro controller
PaypalInstance.webhook(req, res);
});
module.exports = router;
Y por último, vamos a modificar el archivo app.js
para que todo funcione bien. Este archivo, debería quedar de la siguiente forma:
const express = require("express");
const path = require("path");
const cookieParser = require("cookie-parser");
const logger = require("morgan");
const paypalRouter = require("./routes/paypal");
const app = express();
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use("/paypal", paypalRouter);
module.exports = app;
Y eso es todo folks
add webhook
. Recuerden de poner la url completa, más /paypal/webhook
Gracias por leer!