Skip to content

👾 Product - Tipo Suscripción

👀 Esquema de Product (Tipo Suscripción)

Section titled “👀 Esquema de Product (Tipo Suscripción)”

🧩 Colección products - Tipo Suscripción

Este modelo representa los productos de tipo Suscripción dentro de la colección unificada products. Los productos tipo “Suscripción” manejan planes de pago Premium y Alumni, sus precios, contenido incluido y acceso a series/episodios.

🔍 Campos principales (Tipo Suscripción)

Section titled “🔍 Campos principales (Tipo Suscripción)”
CampoTipo de datoRequeridoÚnicoDescripción
titlestringNombre del producto/suscripción (ej. “Premium Mensual”)
SKUstringCódigo único del producto
typeenumDebe ser “Suscripción”
stripeIDstringID del producto en Stripe (autogenerado)
stripeCrmenumCRM de Stripe: Alebat o Inspiria
verticalmulti-selectVerticales médicos asociados
subscriptionTypeenumTipo: Premium o Alumni
trialPeriodDaysintegerDías de prueba gratis (entre 1 y 730)
stripeDescriptionstringDescripción interna para Stripe
Obligatorio

Para suscripciones debe ser:

  • Suscripción - Tipo de producto de suscripción

subscriptionPrices

Tipo: componentproducts.subscription-price

Repetible:Condicional: Visible solo si type === "Suscripción"

Precios de suscripción con diferentes intervalos (mensual/anual)

includedProducts

Tipo: oneToManyapi::product.product Condicional: Visible solo si type === "Suscripción" Productos concretos incluidos en la suscripción

series

Tipo: oneToManyapi::serie.serie Condicional: Visible solo si type === "Suscripción" Series exclusivas que se desbloquean con la suscripción

episodes

Tipo: oneToManyapi::episode.episode Condicional: Visible solo si type === "Suscripción" Episodios incluidos como parte del plan

categories

Tipo: oneToManyapi::category.category Categorías asociadas al producto

experts

Tipo: oneToManyapi::expert.expert

Expertos asociados al producto

Tipo: richtext

Condicional: Visible solo si type === "Suscripción"

Texto enriquecido para mostrar en la UI dentro de las cards de suscripción.

Los productos tipo “Suscripción” dentro de la colección unificada products sirven como base para todo el flujo de suscripciones: lógica de acceso, tarjetas visuales, precios, y middleware de protección de contenido.


🔄 Lifecycle Hooks (Products - Tipo Suscripción)

Section titled “🔄 Lifecycle Hooks (Products - Tipo Suscripción)”

beforeCreate

Se ejecuta antes de crear un nuevo producto

afterCreate

Se ejecuta después de crear un producto exitosamente

beforeUpdate

Se ejecuta antes de actualizar un producto existente

afterUpdate

Se ejecuta después de actualizar un producto exitosamente

afterDelete

Se ejecuta después de eliminar un producto

  • Valida la relación entre vertical e stripeCrm
  • Verifica la unicidad del SKU
  • Crea producto en Stripe para tipo “Suscripción”
  • Controla el orden de publicación
beforeCreate: async (event) => {
try {
const { data, where } = event.params;
const { vertical, stripeCrm } = data;
// Validación vertical-CRM
if (vertical && vertical.length > 0) {
if (stripeCrm) {
if (vertical.includes(VERTICAL_INSPIRIA) && stripeCrm !== VERTICAL_INSPIRIA) {
throwValidationError(PRODUCT.STRIPECRM, ERROR_INVALID_VERTICALCRM);
}
}
}
// Validación SKU único
const { valid, reason } = await strapi.service(PRODUCT.PRODUCT).uniqueSKU({ data, where });
if (!valid) {
if (reason === ERROR_MISSING) throwValidationError(SKU, ERROR_SKU_REQUIRED);
if (reason === ERROR_DUPLICATE) throwValidationError(SKU, ERROR_SKU_EXITS);
}
// Estado de publicación para ordenamiento
const willBePublished = await isPublishingStatusUpdate(data, productStatusAt);
event.state = { ...event.state, isPublishing: willBePublished };
// Validar orden
await specialChecksOrder(PRODUCT.PRODUCT, data.order, false);
// Crear producto en Stripe para suscripciones
if (data.type === PRODUCT.SUBSCRIPTION_TYPE) {
const stripe = await selectStripeCrm(data.stripeCrm);
data.stripeID = await strapi
.service(PRODUCT.PRODUCT_HANDLER_SERVICE)
.createOrUpdateStripeProduct(stripe, data, data.documentId);
}
} catch (error) {
strapi.log.error(error);
throwValidationError(ERROR_OCCURRED, (error as Error).message);
}
};
  • Genera slug automáticamente desde el title
  • Sincroniza producto completo con Stripe
  • Maneja conexión con Laab si aplica
  • Crea precios de producto y suscripción en Stripe
  • Controla ordenamiento de colección
afterCreate: async (event) => {
if (event.result.locale !== DEFAULT_LOCALE) return;
const {
documentId,
hasLaabConnection,
order,
stripeCrm,
stripeID,
subjectData,
title,
} = event.result;
const isInternalCreation = event.params?.data?._internalUpdate || false;
const isPublishedVersion = event.result.publishedAt !== null;
// Generar slug automáticamente
if (!isInternalCreation && !isPublishedVersion) {
await generateSlug({ collection: PRODUCT.PRODUCT, title, documentId });
}
// Sincronización con Stripe
const stripe = await selectStripeCrm(stripeCrm);
event.result.stripeID = await strapi
.service(PRODUCT.PRODUCT_HANDLER_SERVICE)
.createOrUpdateStripeProduct(stripe, event.result, documentId, publish);
// Manejar syllabus Laab
await strapi
.service(PRODUCT.PRODUCT_HANDLER_SERVICE)
.handleSyllabusLaab(
stripeID,
documentId,
subjectData,
hasLaabConnection,
publish
);
// Crear precios de producto estándar
await strapi
.service(PRODUCT.PRODUCT_HANDLER_SERVICE)
.createOrUpdateStripePriceProduct(stripe, stripeID, event.result, publish);
// Crear precios de suscripción si aplica
const shouldCreateSubscriptionPrices =
event.result.type === PRODUCT.SUBSCRIPTION_TYPE &&
event.state.isPublishing &&
event.result.subscriptionPrices &&
event.result.subscriptionPrices.length > 0;
if (shouldCreateSubscriptionPrices) {
await strapi
.service(PRODUCT.PRODUCT_HANDLER_SERVICE)
.createOrUpdateStripePriceSubscription(
stripe,
event.result.stripeID,
event.result
);
}
// Manejar ordenamiento de colección
await createOrderCollection(
order,
documentId,
PRODUCT.PRODUCT,
event.state.isPublishing
);
};
  • Valida cambios en vertical y stripeCrm
  • Verifica SKU único cuando cambia o se publica
  • Controla actualizaciones internas vs. externas
  • Maneja cambios de orden en la colección
beforeUpdate: async (event) => {
const { data, where } = event.params;
const { vertical, stripeCrm } = data;
// Validación vertical-CRM
if (vertical && vertical.length > 0) {
if (stripeCrm) {
if (
vertical.includes(VERTICAL_INSPIRIA) &&
stripeCrm !== VERTICAL_INSPIRIA
) {
throwValidationError(PRODUCT.STRIPECRM, ERROR_INVALID_VERTICALCRM);
}
}
}
// Saltar validaciones para actualizaciones internas
if (data._internalUpdate || data.isSKUUpdated) return;
// Estado de publicación
const willBePublished = await isPublishingStatusUpdate(data, productStatusAt);
event.state = { ...event.state, isPublishingUpdate: willBePublished };
// Obtener datos iniciales para comparación
const initialSKU = event.state.initialData?.SKU;
const wasPublished = event.state.initialData?.publishedAt;
const isBeingPublished =
productStatusAt in data && data.publishedAt && !wasPublished;
const isSKUChanged = data.SKU && data.SKU !== initialSKU;
// Validar producto en locales secundarios
if (data.locale !== DEFAULT_LOCALE) {
const documentId = where.documentId ?? null;
if (documentId)
await strapi
.service(PRODUCT.PRODUCT)
.getProductByDocumentIdAndLocale(documentId);
}
// Validar SKU cuando sea necesario
if (isBeingPublished || isSKUChanged || isSKUChanged === null) {
const { valid, reason } = await strapi
.service(PRODUCT.PRODUCT)
.uniqueSKU({ data, where });
if (!valid) {
if (reason === ERROR_MISSING)
throwValidationError(SKU, ERROR_SKU_REQUIRED);
if (reason === ERROR_DUPLICATE)
throwValidationError(SKU, ERROR_SKU_EXITS);
}
}
// Validar orden si está presente
if (!hasFieldInPayload(event.params.data, orderSelect)) return;
await specialChecksOrder(PRODUCT.PRODUCT, data.order, true);
const previousCollection = await searchPreviousCollection(
PRODUCT.PRODUCT,
where
);
event.state = { ...event.state, previousOrder: previousCollection?.order };
};
  • Sincroniza cambios con Stripe cuando se actualiza un producto
  • Maneja actualizaciones de syllabus Laab
  • Actualiza precios de producto y suscripción según cambios
  • Controla reordenamiento de colección
afterUpdate: async (event) => {
const { data } = event.params;
if (data._internalUpdate) return;
// Manejar locales secundarios
if (event.result.locale !== DEFAULT_LOCALE) {
const documentId =
event.params?.where?.documentId || event.result?.documentId;
await strapi
.service(PRODUCT.PRODUCT)
.getProductByDocumentIdAndLocale(documentId);
return;
}
const { documentId, subjectData, stripeCrm, stripeID, hasLaabConnection } =
event.result;
// Manejar syllabus Laab
await strapi
.service(PRODUCT.PRODUCT_HANDLER_SERVICE)
.handleSyllabusLaab(stripeID, documentId, subjectData, hasLaabConnection);
// Sincronizar con Stripe
const stripe = await selectStripeCrm(stripeCrm);
await strapi
.service(PRODUCT.PRODUCT_HANDLER_SERVICE)
.createOrUpdateStripeProduct(stripe, event.result, documentId);
// Actualizar precios estándar
await strapi
.service(PRODUCT.PRODUCT_HANDLER_SERVICE)
.createOrUpdateStripePriceProduct(stripe, stripeID, event.result);
// Actualizar precios de suscripción si aplica
const subscriptionPricesChanged = hasFieldInPayload(
event.params.data,
"subscriptionPrices"
);
const publishingNow = event.state.isPublishingUpdate;
const shouldUpdateSubscriptionPrices =
event.result.type === PRODUCT.SUBSCRIPTION_TYPE &&
event.result.subscriptionPrices &&
event.result.subscriptionPrices.length > 0 &&
(subscriptionPricesChanged || publishingNow);
if (shouldUpdateSubscriptionPrices) {
await strapi
.service(PRODUCT.PRODUCT_HANDLER_SERVICE)
.createOrUpdateStripePriceSubscription(
stripe,
event.result.stripeID,
event.result
);
}
// Manejar reordenamiento
if (!hasFieldInPayload(event.params.data, orderSelect)) return;
const { previousOrder } = event.state ?? {};
const newOrder = data.order ?? null;
await updateOrderCollection(
newOrder,
documentId,
PRODUCT.PRODUCT,
previousOrder
);
};

Limpia el ordenamiento de la colección cuando se elimina un producto.

afterDelete: async (event) => {
await deleteOrderCollection(
PRODUCT.PRODUCT,
event.result.documentId,
event.result.order
);
};

🛠️ Servicio Product Handler (Suscripciones)

Section titled “🛠️ Servicio Product Handler (Suscripciones)”

createOrUpdateStripeProduct

Crea o actualiza producto base en Stripe con metadatos

createOrUpdateStripePriceProduct

Maneja precios estándar de productos (no suscripciones)

createOrUpdateStripePriceSubscription

Crea y gestiona precios de suscripción recurrentes

handleSyllabusLaab

Gestiona conexión con sistema Laab para syllabus

Sincroniza el producto base con Stripe, creando o actualizando metadatos según sea necesario.

💰 createOrUpdateStripePriceSubscription

Section titled “💰 createOrUpdateStripePriceSubscription”

Gestiona los precios de suscripción recurrentes basados en el componente subscriptionPrices.

// Ejemplo del componente subscription-price
{
stripePriceID: "price_1234567890",
unitPrice: 2999, // 29.99 EUR
interval: "month",
currency: "eur",
active: true
}

Gestiona la conexión con el sistema Laab para sincronizar syllabus y datos de materias.


🔄 Flujo completo de sincronización (Productos Suscripción)

Section titled “🔄 Flujo completo de sincronización (Productos Suscripción)”

1. Creación

beforeCreate → Validaciones + Stripe base → afterCreate → Precios + Laab + Orden

2. Actualización

beforeUpdate → Validaciones → afterUpdate → Stripe sync + Precios condicionales

3. Eliminación

afterDelete → Limpieza de ordenamiento (Stripe se maneja por servicio)