stripe.provider.js

Overview

Este archivo encapsula la integración con Stripe dentro del módulo de pagos. Convierte configuración de entorno y órdenes internas en clientes de Stripe, sesiones de checkout, sesiones del Billing Portal, cambios de suscripción y validación de webhooks.

También actúa como punto de adaptación para otras piezas del módulo. services/api/src/modules/payments/payments.routes.js consume varios helpers de este archivo para checkout, portal y cancelación, services/api/src/modules/payments/payments.service.js usa su mapeo de planes y el cálculo del periodo actual, y services/api/src/modules/payments/payments.webhooks.js usa constructWebhookEvent para validar eventos firmados.

[!IMPORTANT] El módulo depende de env.stripeSecretKey, env.stripeWebhookSecret, env.appUrl, env.stripePriceProfessionalMonthly y env.stripePriceCreatorMonthly. Las tres primeras son críticas para los flujos principales, y las dos últimas son necesarias para los checkouts de plan.

How it works

flowchart TD
  Caller[Caller from payments routes or webhooks] --> Provider[stripe.provider]
  Provider --> Client[getClient]
  Client --> StripeSDK[Stripe SDK]

  Provider --> PlanMap[PLAN_PRICE_MAP]
  PlanMap --> PriceVars[env stripePriceProfessionalMonthly and env stripePriceCreatorMonthly]

  Provider --> Checkout[createCheckout]
  Checkout --> CheckoutAPI[Stripe checkout sessions create]
  Checkout --> ReturnData[checkoutUrl and externalId]

  Provider --> Customer[getOrCreateCustomer]
  Customer --> CustomersAPI[Stripe customers create and retrieve]

  Provider --> SubscriptionChange[changeSubscriptionPlan]
  SubscriptionChange --> SubscriptionsAPI[Stripe subscriptions update]

  Provider --> CancelPeriod[cancelSubscriptionAtPeriodEnd]
  CancelPeriod --> SubscriptionsAPI

  Provider --> Portal[createBillingPortalSession]
  Portal --> PortalAPI[Stripe billing portal sessions create]

  Provider --> Webhook[constructWebhookEvent]
  Webhook --> WebhookAPI[Stripe webhooks constructEvent]

El módulo siempre parte de getClient(). Ese helper valida la clave secreta y crea el cliente de Stripe. Después, PLAN_PRICE_MAP traduce professional y creator a los Price IDs configurados. Con eso, createCheckout decide si usa suscripción o pago único, y los demás helpers operan sobre customers, subscriptions y webhooks.

[!NOTE] createCheckout cambia su comportamiento según order.type === "plan". Los planes usan mode: "subscription" y un Price ID preconfigurado. Las órdenes de campaña usan mode: "payment" con price_data dinámico.

Key components

getClient

{
    "title": "getClient",
    "content": "Crea una instancia de Stripe con `env.stripeSecretKey`. Lanza un error si la clave no existe.",
    "type": "function",
    "filePath": "services/api/src/modules/payments/providers/stripe.provider.js",
    "badges": [
        "read-only"
    ]
}

getPlanPriceId

{
    "title": "getPlanPriceId",
    "content": "Devuelve el Price ID de Stripe asociado a un `planTarget` configurado, o `null` si no existe.",
    "type": "function",
    "filePath": "services/api/src/modules/payments/providers/stripe.provider.js",
    "badges": [
        "public"
    ]
}

getPlanKeyFromPriceId

{
    "title": "getPlanKeyFromPriceId",
    "content": "Convierte un Price ID de Stripe en la clave de plan `professional` o `creator`. Devuelve `null` si no coincide.",
    "type": "function",
    "filePath": "services/api/src/modules/payments/providers/stripe.provider.js",
    "badges": [
        "public"
    ]
}

getOrCreateCustomer

{
    "title": "getOrCreateCustomer",
    "content": "Reutiliza el Stripe Customer del usuario si sigue siendo v\u00e1lido. Si no existe, crea uno nuevo con metadatos del usuario.",
    "type": "function",
    "filePath": "services/api/src/modules/payments/providers/stripe.provider.js",
    "badges": [
        "public"
    ]
}

changeSubscriptionPlan

{
    "title": "changeSubscriptionPlan",
    "content": "Actualiza el Price de una suscripci\u00f3n existente sin crear una segunda suscripci\u00f3n. Aplica prorrateo inmediato.",
    "type": "function",
    "filePath": "services/api/src/modules/payments/providers/stripe.provider.js",
    "badges": [
        "public"
    ]
}

cancelSubscriptionAtPeriodEnd

{
    "title": "cancelSubscriptionAtPeriodEnd",
    "content": "Marca una suscripci\u00f3n para cancelar al final del periodo ya pagado. No corta el acceso de forma inmediata.",
    "type": "function",
    "filePath": "services/api/src/modules/payments/providers/stripe.provider.js",
    "badges": [
        "public"
    ]
}

retrieveSubscription

{
    "title": "retrieveSubscription",
    "content": "Recupera una suscripci\u00f3n completa desde Stripe para leer estado, periodos y otros campos.",
    "type": "function",
    "filePath": "services/api/src/modules/payments/providers/stripe.provider.js",
    "badges": [
        "public"
    ]
}

getSubscriptionCurrentPeriodEnd

{
    "title": "getSubscriptionCurrentPeriodEnd",
    "content": "Calcula el fin del periodo actual de una suscripci\u00f3n. Prioriza los `SubscriptionItem` y cae al campo legado si hace falta.",
    "type": "function",
    "filePath": "services/api/src/modules/payments/providers/stripe.provider.js",
    "badges": [
        "public"
    ]
}

createBillingPortalSession

{
    "title": "createBillingPortalSession",
    "content": "Crea una sesi\u00f3n del Stripe Billing Portal para que el cliente gestione su suscripci\u00f3n en Stripe.",
    "type": "function",
    "filePath": "services/api/src/modules/payments/providers/stripe.provider.js",
    "badges": [
        "public"
    ]
}

createCheckout

{
    "title": "createCheckout",
    "content": "Crea una Checkout Session de Stripe y devuelve `checkoutUrl` y `externalId`.",
    "type": "function",
    "filePath": "services/api/src/modules/payments/providers/stripe.provider.js",
    "badges": [
        "public"
    ]
}

constructWebhookEvent

{
    "title": "constructWebhookEvent",
    "content": "Verifica la firma del webhook y devuelve el evento parseado por Stripe.",
    "type": "function",
    "filePath": "services/api/src/modules/payments/providers/stripe.provider.js",
    "badges": [
        "public"
    ]
}

Data & fields

Estos son los valores de entorno y las estructuras que el módulo usa directamente. La mayoría de los flujos dependen de configuración externa y de objetos de Stripe o de la orden interna.

[
    {
        "name": "env.stripeSecretKey",
        "type": "string",
        "required": true,
        "description": "Clave secreta usada por `getClient()` para instanciar el SDK de Stripe."
    },
    {
        "name": "env.stripeWebhookSecret",
        "type": "string",
        "required": true,
        "description": "Secreto usado por `constructWebhookEvent()` para validar la firma del webhook."
    },
    {
        "name": "env.appUrl",
        "type": "string",
        "required": true,
        "description": "Base URL usada por `createCheckout()` para construir URLs de éxito y cancelación."
    },
    {
        "name": "env.stripePriceProfessionalMonthly",
        "type": "string",
        "description": "Price ID configurado para el plan `professional`. Si falta, el checkout del plan falla."
    },
    {
        "name": "env.stripePriceCreatorMonthly",
        "type": "string",
        "description": "Price ID configurado para el plan `creator`. Si falta, el checkout del plan falla."
    }
]
  • PLAN_PRICE_MAP [badge:read-only] — mapea professional y creator a sus Price IDs configurados.
[
    {
        "name": "planTarget",
        "type": "string",
        "required": true,
        "description": "Clave de plan usada por `getPlanPriceId()` y `getPlanKeyFromPriceId()`."
    },
    {
        "name": "priceId",
        "type": "string",
        "description": "Price ID de Stripe usado para encontrar el plan inverso en `getPlanKeyFromPriceId()`."
    }
]
[
    {
        "name": "user",
        "type": "object",
        "required": true,
        "description": "Usuario interno que se sincroniza con Stripe.",
        "children": [
            {
                "name": "id",
                "type": "string",
                "description": "Identificador del usuario, usado en `metadata.userId`."
            },
            {
                "name": "email",
                "type": "string",
                "required": true,
                "description": "Correo del usuario para crear o reutilizar el Customer."
            },
            {
                "name": "name",
                "type": "string",
                "required": true,
                "description": "Nombre del usuario para el Customer de Stripe."
            },
            {
                "name": "stripeCustomerId",
                "type": "string",
                "description": "Customer ya guardado en la aplicación."
            }
        ]
    }
]
[
    {
        "name": "subscriptionId",
        "type": "string",
        "required": true,
        "description": "Identificador de la suscripción de Stripe que se va a leer o mutar."
    },
    {
        "name": "newPriceId",
        "type": "string",
        "required": true,
        "description": "Price ID nuevo usado por `changeSubscriptionPlan()`."
    }
]
[
    {
        "name": "order",
        "type": "object",
        "required": true,
        "description": "Orden interna que se transforma en una sesión de checkout.",
        "children": [
            {
                "name": "_id",
                "type": "string | ObjectId",
                "required": true,
                "description": "Identificador de la orden."
            },
            {
                "name": "type",
                "type": "string",
                "required": true,
                "description": "Tipo de orden. El código distingue `plan` y otros pagos."
            },
            {
                "name": "currency",
                "type": "string",
                "required": true,
                "description": "Moneda del cobro."
            },
            {
                "name": "amount",
                "type": "number",
                "required": true,
                "description": "Importe de la orden. Se convierte a centavos."
            },
            {
                "name": "createdBy",
                "type": "string | ObjectId",
                "required": true,
                "description": "Usuario que creó la orden. Se usa en la metadata."
            },
            {
                "name": "planTarget",
                "type": "string",
                "description": "Clave de plan. Solo se envía cuando `type` es `plan`."
            },
            {
                "name": "campaignId",
                "type": "string | ObjectId",
                "description": "Campaña asociada. Solo se añade a la metadata si existe."
            }
        ]
    },
    {
        "name": "productName",
        "type": "string",
        "required": true,
        "description": "Nombre visible del producto o concepto en Stripe."
    },
    {
        "name": "customerId",
        "type": "string",
        "description": "Customer de Stripe reutilizado al crear el checkout."
    }
]
[
    {
        "name": "customerId",
        "type": "string",
        "required": true,
        "description": "Customer de Stripe usado por el Billing Portal."
    },
    {
        "name": "returnUrl",
        "type": "string",
        "required": true,
        "description": "URL de retorno que Stripe usará al cerrar el portal."
    }
]
[
    {
        "name": "rawBody",
        "type": "Buffer",
        "required": true,
        "description": "Cuerpo crudo del webhook recibido."
    },
    {
        "name": "signature",
        "type": "string",
        "required": true,
        "description": "Firma recibida en el encabezado `stripe-signature`."
    }
]
[
    {
        "name": "subscription",
        "type": "Stripe.Subscription",
        "required": true,
        "description": "Objeto completo de suscripción devuelto por Stripe.",
        "children": [
            {
                "name": "items.data[].current_period_end",
                "type": "number",
                "description": "Fin del periodo por item de suscripción."
            },
            {
                "name": "current_period_end",
                "type": "number",
                "description": "Campo legado de nivel superior usado como fallback."
            }
        ]
    }
]

La función createCheckout() agrega metadatos comunes y específicos del tipo de orden. El código guarda paymentOrderId, userId y source, y añade plan o campaignId cuando corresponde.

  • paymentOrderId — identifica la orden interna.
  • userId — vincula la sesión con el creador de la orden.
  • source — usa el valor fijo plano_web.
  • plan — solo aparece en órdenes de tipo plan.
  • campaignId — solo aparece si la orden tiene campaña asociada.

Behaviors

  • getClient() — crea un cliente de Stripe con env.stripeSecretKey y lanza STRIPE_SECRET_KEY no configurada. si falta la clave.
  • getPlanPriceId(planTarget) — devuelve el Price ID configurado para professional o creator, o null si no existe.
  • getPlanKeyFromPriceId(priceId) — invierte el mapeo y devuelve la clave de plan asociada a un Price ID.
  • getOrCreateCustomer(user) — reutiliza user.stripeCustomerId si Stripe lo acepta; si no, crea un Customer nuevo con email, nombre y metadata.userId.
  • changeSubscriptionPlan({ subscriptionId, newPriceId }) — recupera la suscripción, toma el primer item y actualiza su price con prorrateo inmediato.
  • cancelSubscriptionAtPeriodEnd({ subscriptionId }) — marca la suscripción para cancelarse al final del periodo ya pagado.
  • retrieveSubscription(subscriptionId) — devuelve la suscripción completa de Stripe.
  • getSubscriptionCurrentPeriodEnd(subscription) — calcula el fin del periodo actual usando el valor más tardío de items.data[].current_period_end. Si no existe, usa current_period_end de nivel superior.
  • createBillingPortalSession({ customerId, returnUrl }) — crea una sesión del Billing Portal y devuelve el resultado de Stripe.
  • createCheckout({ order, productName, customerId }) — crea una Checkout Session y devuelve { checkoutUrl, externalId }. Usa subscription para planes y payment para campañas.
  • constructWebhookEvent(rawBody, signature) — valida la firma del webhook y devuelve el evento parseado por Stripe.

[!WARNING] constructWebhookEvent() necesita el cuerpo crudo como Buffer. La firma se valida contra ese payload exacto. Si el body cambia, la verificación falla.

[!WARNING] createCheckout() para planes falla si no existe el Price ID correspondiente en env.stripePriceProfessionalMonthly o env.stripePriceCreatorMonthly. El código corta el flujo con un error de configuración.

[!CAUTION] changeSubscriptionPlan() aplica proration_behavior: "create_prorations". El cambio de plan es inmediato y usa prorrateo en la facturación siguiente.

[!NOTE] getSubscriptionCurrentPeriodEnd() prioriza el modelo moderno de Stripe. El código toma el mayor current_period_end disponible entre los items de la suscripción.

[!IMPORTANT] createBillingPortalSession() solo crea la sesión. El portal debe estar configurado en el Dashboard de Stripe para que funcione.

  • services/api/src/modules/payments/payments.routes.js importa createCheckout as stripeCheckout, getOrCreateCustomer, getPlanPriceId, changeSubscriptionPlan, cancelSubscriptionAtPeriodEnd, createBillingPortalSession y getSubscriptionCurrentPeriodEnd.
  • services/api/src/modules/payments/payments.service.js importa getPlanKeyFromPriceId y getSubscriptionCurrentPeriodEnd para sincronizar el estado de planes.
  • services/api/src/modules/payments/payments.webhooks.js usa constructWebhookEvent para validar eventos de Stripe.

Este archivo también encaja con la lógica de pagos descrita en el módulo. El checkout guarda la referencia externa de Stripe, y los webhooks después usan esos datos para cerrar el ciclo de pago.