Skip to content

Checkout Controller

Este controlador se encarga de gestionar la creación de sesiones de pago a través de Stripe en un entorno gestionado con Strapi. Utiliza información del usuario registrada en la base de datos y permite crear metadatos personalizados para enriquecer las transacciones. También valida que la transacción pertenezca a un vertical permitido.


Archivo:

src/api/checkout/controllers/checkout.ts

Checkout Controller

import { CheckoutPayload } from "type/checkout";
import { FIELD_ROOT } from "../../../constants/structures/collections/user";
import { Stripe } from "stripe";
import { validateVertical } from "../../../utils/verify-vertical";
import {
PROD,
LAAB,
CHECKOUT,
CHECKOUT_STRIPE_SERVICES,
CHECKOUT_VALIDATION_KEYS,
} from "../../../constants/structures/apis/checkout";
import {
selectStripeCrm,
syncCustomerWithStripe,
} from "../../../utils/stripe-crm";
import { ERROR_VERTICAL } from "../../../constants/errors";
import {
validateKeys,
validateRequiredFields,
} from "../../../utils/api-validation-queries";
export default {
async createCheckoutSession(ctx) {
try {
validateKeys(ctx.request.body, CHECKOUT_VALIDATION_KEYS, FIELD_ROOT);
validateRequiredFields(ctx.request.body, CHECKOUT_VALIDATION_KEYS);
const {
payment_method_types,
line_items,
mode,
success_url,
cancel_url,
customer_email,
payment_intent_data,
metadata,
vertical,
origin,
}: CheckoutPayload = ctx.request.body;
const stripe = await selectStripeCrm(origin);
let customer: Stripe.Customer;
customer = await syncCustomerWithStripe(stripe, customer_email, origin);
payment_intent_data.metadata = {};
await strapi
.service(CHECKOUT)
.updateMetadataUnified(
line_items,
PROD,
null,
payment_intent_data,
vertical,
origin
);
await strapi
.service(CHECKOUT)
.updateMetadataUnified(
null,
LAAB,
metadata.laab_fetch,
payment_intent_data,
vertical,
origin
);
const validVertical = validateVertical(vertical);
if (!validVertical) ctx.throw(400, ERROR_VERTICAL);
const checkoutSession = await strapi
.service(CHECKOUT_STRIPE_SERVICES)
.createCheckoutSession(
customer,
payment_method_types,
line_items,
mode,
success_url,
cancel_url,
payment_intent_data,
validVertical,
origin
);
ctx.send(checkoutSession, 200);
} catch (error) {
return ctx.badRequest(error);
}
},
};
  • Este es el checkout controller, el que se encarga de toda la logica del checkout

createCheckoutSession: explicación paso a paso

1. Desestructuración del payload del cliente

Se extraen todos los campos necesarios desde ctx.request.body, tipado con CheckoutPayload. Esto incluye los datos del cliente, la información del pago, metadatos, vertical y el origen (CRM).

const {
payment_method_types,
line_items,
mode,
success_url,
cancel_url,
customer_email,
payment_intent_data,
metadata,
vertical,
origin,
}: CheckoutPayload = ctx.request.body;

2. Validación de campos obligatorios

Antes de continuar, se asegura que todos los campos requeridos estén presentes. Así como no se incluyas campos que no están permitidos, para mas información vease Aquí.

validateKeys(ctx.request.body, CHECKOUT_VALIDATION_KEYS, FIELD_ROOT);
validateRequiredFields(ctx.request.body, CHECKOUT_VALIDATION_KEYS);

3. Selección dinámica del cliente de Stripe

Con selectStripeCrm, se selecciona la instancia correspondiente de Stripe (permite trabajar con múltiples cuentas Stripe).

const stripe = await selectStripeCrm(origin);

4. Sincronización del cliente con Stripe

Se llama a la función syncCustomerWithStripe, que se encarga de verificar si el cliente ya existe en Stripe y, si no, lo crea. Esta función también actualiza el stripeID en Strapi.

Se consulta si el cliente ya existe en la base de datos de Stripe (vinculada a ese CRM).

export async function syncCustomerWithStripe(
stripe: stripeType,
email: string,
origin: StripeCrmName
): Promise<stripeType.Customer> {
const userStrapi = await findOne(
USER,
FIELDS_USER,
{ email: email },
COMPONENTS_USER
);
if (userStrapi === NOT_FOUND) throw new Error(ERROR_USER_NOT_FOUND);
let customer: stripeType.Customer;
const user = await getStripeIdByCrm(email, origin);
const customer_stripe: stripeType.ApiList<stripeType.Customer> = await strapi
.service(USER_STRIPE_SERVICE)
.searchCustomer(stripe, email);
if (user?.stripeID === null && customer_stripe.data.length === 0) {
customer = await strapi
.service(USER_STRIPE_SERVICE)
.createCustomer(stripe, user);
updateStripeID(customer.id, origin, userStrapi);
} else if (user?.stripeID === null && customer_stripe.data.length !== 0) {
customer = customer_stripe.data[0];
updateStripeID(customer.id, origin, userStrapi);
} else {
customer = customer_stripe.data[0];
}
return customer;
}

Se evalúan los casos:

  • Si no tiene stripeID y no existe en Stripe → se crea un nuevo cliente.
  • Si no tiene stripeID pero sí existe en Stripe → se actualiza el stripeID en Strapi.
  • Si ya tiene stripeID, se reutiliza el cliente.
if (...) {
customer = await strapi.service(...).createCustomer(...);
updateStripeID(...);
} else if (...) {
customer = customer_stripe.data[0];
updateStripeID(...);
} else {
customer = customer_stripe.data[0];
}

La función updateStripeID se encarga de actualizar los identificadores de Stripe dentro del objeto del usuario en Strapi. Esto retorna una lista con los resultados de clientes encontrados.


5. Inicialización de metadatos

Antes de añadir metadatos, se inicializa el objeto metadata del payment_intent_data.

payment_intent_data.metadata = {};

6. Agregado de metadatos personalizados

Usando la función updateMetadataUnified, se agregan productos y laabs (información personalizada) como metadatos con prefijos (PROD y LAAB) dentro de payment_intent_data.metadata.

await strapi
.service(CHECKOUT)
.updateMetadataUnified(
line_items,
PROD,
null,
payment_intent_data,
vertical,
origin
);
await strapi
.service(CHECKOUT)
.updateMetadataUnified(
null,
LAAB,
metadata.laab_fetch,
payment_intent_data,
vertical,
origin
);

Esta función convierte los elementos a JSON y los guarda con claves como PROD1, PROD2, etc.


7. Validación de vertical

Valida que el vertical que se envía exista en la lista de verticales permitidos definida en constantes. Si no es válido, lanza un error 400.

const validVertical = validateVertical(vertical);
if (!validVertical) ctx.throw(400, ERROR_VERTICAL);

La función validateVertical busca si el nombre enviado se encuentra dentro del arreglo VERTICAL.


8. Creación de sesión de checkout en Stripe

Se invoca el servicio createCheckoutSession con todos los parámetros reunidos anteriormente, incluyendo cliente, datos del pago, URLs y metadatos.

const checkoutSession = await strapi
.service(CHECKOUT_STRIPE_SERVICES)
.createCheckoutSession(
customer,
payment_method_types,
line_items,
mode,
success_url,
cancel_url,
payment_intent_data,
validVertical,
origin
);

El servicio interno de Stripe genera la sesión utilizando su SDK, con detalles como:

  • Campos personalizados (custom_fields),
  • Métodos de pago,
  • Dirección de facturación,
  • Metadatos,
  • Configuración para guardar métodos de pago.
  • Origin para determinar el API token que usará

9. Envío de la respuesta

Si todo va bien, se retorna la sesión de Stripe al cliente con estado HTTP 200. En caso de error, se captura y se envía igualmente.

ctx.send(checkoutSession, 200);