Vende complementos
Declara una capacidad en código, ponle precio en el panel de administración y deja que la gente la active, la compre o la reciba con su plan.
Un complemento es una capacidad que tu proyecto enciende para una cuenta: un módulo, un extra de pago, una cuota. El código declara qué capacidades existen; el panel de administración, o el seeder de complementos cuando declaras el complemento mismo en código, decide cómo se vende cada una; la cuenta de facturación responde si una cuenta la tiene. Esta guía cubre el camino completo, desde el método en PlanFeatures hasta el botón que pulsa el usuario.
Enciéndelo
Los complementos viven en el paquete Billing Core, así que el paquete tiene que estar encendido. Los planes no: un complemento gratis funciona sin plan, sin suscripción y sin llave de Stripe.
| Opción | Variable de entorno | Valor por omisión | Qué cambia |
|---|---|---|---|
| Módulo de complementos | FEATURE_ADDONS_ENABLED |
false |
Apagado: /account/add-ons responde 404, el recurso del panel rechaza toda acción, el panel Complementos desaparece de la página del usuario y nada se otorga ni se vende. |
| Billing Core | BILLING_CORE_ENABLED |
true |
Apagado: el paquete no carga modelos, migraciones, rutas ni webhook. |
| Facturación de usuarios | BILLING_USER_ENABLED |
true |
Apagado: las rutas de facturación bajo /account/... no se registran, complementos incluidos. |
| Stripe | STRIPE_KEY, STRIPE_SECRET, STRIPE_WEBHOOK_SECRET |
vacío | Necesarias para vender cualquier cosa. Cada guardado de un complemento o de un precio habla con Stripe. |
Dos preguntas deciden qué se muestra una vez encendida la bandera:
BillingAddon::hasVisibleCatalog()es verdadero cuando la bandera está encendida y la cuenta del contexto de facturación activo puede ver al menos un complemento — uno a la venta, o uno que ya tiene. Esa pregunta, o la del mismo nombre enBillingMeter, muestra el enlace Complementos en el menú de la cuenta, y solo donde ese contexto tiene su propia pantalla de complementos: con la facturación de usuarios apagada no hay enlace. No consulta Stripe.BillingAddon::isActive()pide lo mismo másSTRIPE_SECRET. Decide si la página de inicio pública muestra el catálogo de complementos a los visitantes.
La bandera también programa billing:sync-exchange-rates a diario a las 12:00, el mismo comando que programan los planes, además de billing:expire-trials a las 08:00 y billing:close-meter-periods a las 02:00. La instalación del paquete, las llaves y el webhook están en Enciende los planes y la facturación.
Declara la capacidad en código
PlanFeatures declara qué capacidades existen. Abre app/Classes/PlanFeatures.php y agrega un método público por capacidad. El nombre del método es la llave de la capacidad. El valor que devuelve dice si la capacidad ya está liberada en código, así que puedes vender en preventa algo que todavía no enciende nada.
namespace App\Classes;
class PlanFeatures extends BasePlanFeatures
{
public function electronic_wallet()
{
return true;
}
public function white_label()
{
return false; // se vende en preventa, todavía no enciende nada
}
}
BasePlanFeatures lee la clase por reflexión:
| Método | Devuelve |
|---|---|
PlanFeatures::availableFeatures() |
Las llaves, una por método público no estático. |
PlanFeatures::featureOptions() |
['electronic_wallet' => 'Electronic Wallet', ...], la lista que ofrece el formulario del panel. |
PlanFeatures::isReleased('white_label') |
true sólo cuando la llave está declarada y su método devuelve true. |
La clase se conecta en config/billing-core.php bajo user_adapter.plan_features. Weblabor Base viene sin métodos, así que una instalación limpia no muestra el campo Capacidad ni el catálogo. Una llave guardada en un complemento que el código ya no declara se conserva y se marca en el formulario con "This capability is not declared in code, so the add-on will not switch anything on"; el complemento se puede seguir vendiendo. Si guardas un complemento sin elegir capacidad, la llave se deriva del nombre (Extra Storage se vuelve extra_storage), con un número agregado cuando otro complemento ya la usa (extra_storage_2). Dos complementos nunca comparten llave: el formulario rechaza una llave que ya está tomada.
Declarar la capacidad basta para venderla desde el panel de administración. Para declarar el complemento mismo —su nombre, precios, imágenes y guías— mira Declara el complemento en código.
Créalo en el panel de administración
Ve a Planes → Complementos en el panel de administración, en /admin/billing-addons. El índice lista nombre, llave y si está a la venta, y tiene un botón Categorías que abre /admin/categories/billing-addons, donde creas las categorías por las que filtra el catálogo. Crear abre /admin/billing-addons/create; editar es /admin/billing-addons/{id}/edit. El formulario muestra una advertencia cuando STRIPE_SECRET no está definida: ponla primero, porque guardar crea o actualiza el producto en Stripe.

| Campo | Obligatorio | Qué hace |
|---|---|---|
| Nombre | Sí | Título de la tarjeta. También el nombre del producto en Stripe. |
| Categorías | No | Múltiples, del tipo de categoría billing-addons. El catálogo filtra por ellas. |
| Descripción | Sí | Texto corto en la tarjeta y en Stripe. |
| Capacidad | No | La llave declarada que este complemento enciende. Sólo se muestra cuando el código declara al menos una. |
| Depende de | No | Otro complemento que la cuenta ya tiene que tener. Mientras no lo tenga, este no se puede activar, probar ni comprar. Vacío significa que no depende de nada. |
| Descripción completa | No | Se muestra en la página de detalle bajo "Acerca de este complemento". |
| Ícono | No | Imagen cuadrada, hasta 5 MB, guardada en la biblioteca de medios bajo General/Add-ons. Se muestra en la tarjeta. |
| Capturas | No | Hasta seis imágenes, hasta 5 MB cada una, guardadas en la biblioteca de medios bajo General/Add-ons; se pueden elegir varias a la vez. Se muestran como galería en la página de detalle. |
| Guías | No | Guías del centro de ayuda que explican el complemento, tantas como necesites. La tarjeta del catálogo enlaza a ellas. Sólo se muestra cuando el centro de ayuda tiene guías. |
| A la venta | Sí | Encendido por omisión. Apagado lo quita del catálogo para quien no lo tiene y rechaza compras nuevas. Quien ya lo tiene lo sigue viendo en su catálogo y en su página, y puede cancelarlo. |
| Contexto | Sólo con dos adaptadores | Ambos, Usuario o Equipo. Filtra qué dueño de facturación lo ve. Oculto cuando sólo está registrado el adaptador user. |
| Vendido de por vida | No | Agrega una tabla Precio de por vida: un importe por país y moneda, pagado una sola vez con Stripe Checkout. |
| Días de prueba | No | Acceso completo durante esos días. El usuario guarda una tarjeta para iniciarla y no se le cobra nada hasta que termina, cuando el complemento se cobra a esa tarjeta. Cero no ofrece prueba. Una vez por cuenta, para siempre. |
| Vendido por unidad | No | El usuario elige cuántas; el precio y los límites se multiplican. Necesita al menos un límite y no se combina con los escalones. |
| Vendido por escalones | No | El usuario elige una de varias opciones. Los precios de suscripción ganan una columna Unidades, un renglón por opción. Necesita al menos un límite. |
| Precios de suscripción | No | Una entrada por país: país, moneda y un importe por intervalo de config/pricing.php (monthly y yearly por omisión). Déjalo vacío para una capacidad gratis. |
| Límites | No | Llave de límite de App\Classes\PlanLimits y un valor entero. La cuota que aporta el complemento. El selector también lista las llaves de los medidores a la venta, mira Vende consumo con medidores y paquetes. |
Los precios son precios de Stripe y los precios de Stripe son inmutables. Cada importe que guardas crea un precio recurrente en Stripe; un importe existente no se puede editar. Para cambiar un precio, quita el intervalo y agrégalo de nuevo con el importe nuevo. Un intervalo con suscriptores no se puede quitar. Un complemento con precios tampoco se puede eliminar, porque suscripciones y facturas los referencian: apaga A la venta en su lugar. Eliminar un complemento sin precios archiva su producto en Stripe.
Un complemento solo se vende en la moneda en que ya paga la cuenta, porque Stripe guarda una moneda por cliente: a una cuenta suscrita en pesos nunca se le ofrece un precio en dólares, ni de suscripción ni de por vida. Cuando tus precios dejan fuera la moneda de cuentas que ya están suscritas, la tabla de precios muestra Suscriptores que no pueden comprar este complemento con la moneda y el país que faltan, y la pantalla de edición del plan lista los Complementos que sus suscriptores no pueden comprar. Puedes guardar de todos modos; esas cuentas no pueden comprarlo hasta que un precio en su moneda las cubra.
Cada guardado sincroniza con Stripe: crear el complemento crea un producto con el id del complemento en sus metadatos, actualizarlo actualiza nombre y descripción, y cada variación de precio crea un precio con unit_amount en centavos, la moneda, y el intervalo y el conteo de config/pricing.php.
Haz que un complemento necesite otro
Hay complementos que sólo tienen sentido encima de otro. Elige ese otro en Depende de y este complemento pasa a estar disponible sólo para las cuentas que ya lo tienen. Mientras no lo tengan, su botón dice Requiere y el nombre del que falta, la página de detalle lo dice igual arriba de los bloques, y no se puede activar, probar ni comprar nada: la negativa es la misma se pulse el botón o se repita la petición.
A quien ya tiene el complemento no le afecta: puede desactivarlo, cancelar su línea o cancelar su prueba como siempre pudo, incluso cuando el complemento del que depende ya no está. Obligar a alguien a seguir pagando algo porque un requisito se cayó sería una trampa, no una regla.
La lista ofrece todos los complementos que tienen una capacidad, menos el que estás editando. Los que quitaste de la venta siguen en la lista a propósito: lo que se comprueba es si la cuenta tiene la capacidad, no si se sigue vendiendo, así que uno que dejaste de vender sigue contando para todos los que ya lo tienen.
Sólo se comprueba un paso, y nada impide que escribas un ciclo. Si A necesita B y B necesita C, una cuenta con B puede obtener A sin haber tenido nunca C; y dos complementos que se nombran entre sí se bloquean mutuamente, así que no lo hagas.
Declara el complemento en código
En vez de capturar un complemento a mano en cada entorno, puedes describirlo una sola vez en database/seeders/AddOnSeeder.php. El deploy ya corre los seeders (php artisan db:seed --force), así que el complemento se crea en el primer deploy y se mantiene al día en cada uno de los siguientes. Weblabor Base entrega el seeder vacío.
Cada entrada lleva como llave su capacidad, la misma que declara PlanFeatures:
$addons = [
'extra_storage' => [
'name' => 'Extra storage',
'description' => 'Short text shown on the catalog card.',
'long_description' => 'What it does, shown on the detail page.',
'prices' => [
['country' => 'default', 'currency' => 'usd', 'units' => null, 'intervals' => ['monthly' => 9.99, 'once' => 49]],
['country' => 'mx', 'currency' => 'mxn', 'units' => null, 'intervals' => ['monthly' => 179]],
],
'icon' => 'resources/images/addons/extra-storage.png',
'screenshots' => ['resources/images/addons/extra-storage-1.png'],
'help_guides' => ['sell-add-ons'],
'defaults' => ['is_active' => true],
],
];
| Llave | Qué define |
|---|---|
name, description, long_description |
La tarjeta y la página de detalle. |
prices |
Un renglón por país, moneda y unidades. intervals lleva un importe por intervalo; once es el precio de por vida. units es el tamaño del escalón o del paquete, null para un precio simple. |
icon, screenshots |
Rutas a imágenes dentro de tu repositorio, que se suben a la biblioteca de medios bajo General/Add-ons. Una imagen que no cambió no se vuelve a subir. |
help_guides |
Los slugs de las guías del centro de ayuda que enlaza la tarjeta, en orden. |
defaults |
Cualquier otra cosa que captura el formulario, como is_active, trial_days o sells_per_unit. Se aplica sólo cuando el complemento se crea; después es tuyo para cambiarlo en el panel. |
Una llave que dejas fuera de la entrada se queda como está. PlanSeeder funciona igual para los planes; mira Crea y pon precio a un plan.
Lo que el panel te deja cambiar
Un complemento creado por el seeder abre con un aviso Controlado por código. Su nombre, descripción, descripción completa, capacidad, precios, ícono, capturas y guías son de sólo lectura ahí, y cada deploy los regresa a lo que dice el código. Todo lo demás —categorías, A la venta, días de prueba, las modalidades, los límites, Depende de— sigue siendo editable.
Los precios siguen al código sin revolver Stripe: un importe que no cambió conserva su precio en Stripe, y un importe que cambió lo reemplaza exactamente como lo haría quitarlo y escribir el nuevo en el panel. También aplica el mismo límite: un importe con suscriptores no se puede reemplazar, así que se queda como estaba hasta que dejen de usarlo.
Libéralo del código
Cuando quieras manejar un complemento a mano de ahí en adelante, pulsa Liberar del código en el aviso y confirma. Todos los campos se vuelven editables y el seeder no vuelve a tocar ese complemento, aunque su entrada siga en el código. Esto no se puede deshacer desde el panel.
Lo que el seeder nunca toca
El seeder sólo actualiza lo que creó. Un complemento que ya existía, o que alguien creó en el panel, nunca se toma, aunque una entrada del código use su llave: el seeder lo deja en paz e imprime una advertencia durante el deploy. Un complemento que el seeder creó y que luego alguien eliminó tampoco se trae de vuelta.
Las cinco formas en que un usuario obtiene un complemento
Se pueden combinar en el mismo complemento. El acceso se resuelve desde la más fuerte, en este orden:
lifetime > plan > subscription > trial > free
| Origen | Cómo se otorga | Se pierde cuando | Flujo en el catálogo |
|---|---|---|---|
free |
El usuario pulsa Activar | El usuario lo apaga, o el administrador usa Retirar regalo | Sí |
plan |
El plan incluye el complemento | La suscripción deja de estar activa | Sí |
subscription |
El complemento es una línea de la suscripción de Stripe | Se cancela la línea o la suscripción | Sí |
trial |
El usuario guarda una tarjeta y pulsa Prueba N días, o el administrador otorga una | Pasa ends_at y el complemento se cobra, o el usuario cancela la prueba; la fila se conserva como vencida |
Sí |
lifetime |
El usuario pulsa Comprar de por vida y el webhook confirma el pago, o el administrador lo otorga | Nunca, salvo que lo revoques | Sí |
La inclusión en el plan y las líneas de la suscripción se leen en vivo del pivote del plan y de los ítems de la suscripción, así que no pueden desfasarse. Las activaciones gratis, las pruebas y las compras de por vida son filas en billing_entitlements:
| Columna | Significado |
|---|---|
billing_account_id |
La cuenta que lo tiene. |
feature_key |
La capacidad. |
source |
free, trial o lifetime. Una fila por cuenta, llave y origen. |
status |
active por omisión; pending mientras se confirma un pago de por vida; expired cuando una prueba terminó. Cualquier valor distinto de active se ignora. |
quantity |
Unidades que se tienen, 1 por omisión. |
starts_at, ends_at |
Ambos opcionales. Una fila cuenta sólo entre los dos. |
reference |
Texto libre para tu propio registro, como el id de una sesión de Stripe. |
granted_by, granted_at |
El administrador que lo otorgó a mano, y cuándo. Vacíos para todo lo que hizo el usuario. |
Gratis
Un complemento sin ningún precio mayor a cero es gratis. El catálogo muestra Gratis y un botón Activar. Activarlo escribe un derecho free y no necesita tarjeta, suscripción, plan ni llave de Stripe. Desactivar borra sólo la fila free, así que un plan o una compra que también otorgue la capacidad la sigue otorgando.
Si le pones precio a un complemento que alguien ya activó gratis, lo conserva y dejas de ofrecérselo: su tarjeta del catálogo muestra Regalo en lugar del precio y su botón dice Activado y no hace nada, y la página de detalle quita los bloques de suscripción y de por vida y muestra Sin costo para ti donde estaba el precio, junto a la línea que dice que lo tiene por una activación gratuita y que el regalo no se puede quitar desde su cuenta. No puede comprar la versión de pago encima ni desactivarlo, porque Desactivar sólo se muestra en complementos que siguen siendo gratis. Quítaselo desde el panel Complementos de su página de usuario si hace falta: Retirar regalo junto a él.
Incluido en un plan
El formulario del plan tiene un selector múltiple Complementos incluidos en este plan que escribe el pivote billing_plan_addon. Mientras la suscripción default de la cuenta esté activa, cada complemento de su plan se otorga con origen plan. Su tarjeta del catálogo y su página de detalle muestran Incluido en tu plan donde iba el precio, y el botón de la tarjeta dice Incluido y no hace nada. Ambas muestran la tarifa debajo solo como referencia («Fuera del plan: $5.00 al mes») cuando el complemento tiene una. Los límites de los complementos incluidos se suman a los límites del plan.
Suscripción
El catálogo muestra el precio del intervalo que la cuenta ya paga y un botón Comprar. Lo que pasa depende de la cuenta:
- Todavía no hay suscripción, o la cuenta está en un plan gratis sin tarjeta guardada: se abre un Stripe Checkout con el plan gratis de ese intervalo más el complemento. Debe existir exactamente una variación de plan gratis activa para ese intervalo, o el usuario ve "No free plan available for :interval billing. Please subscribe to a plan first." Si las pruebas gratis están encendidas en
config/pricing.php, la prueba cubre plan y complemento. El éxito regresa al dashboard con?checkout=success; la cancelación regresa a/account/add-ons?checkout=cancel. - Una suscripción de pago, o un plan gratis con tarjeta: el precio del complemento se agrega a la suscripción y se factura de inmediato. La página consulta cada tres segundos hasta que el webhook registra el ítem nuevo, luego se recarga y muestra Cancelar. Con el tiempo real encendido (/help/real-time-with-reverb) no consulta: el webhook avisa a la página en cuanto llega.
Pulsar Cancelar quita el precio de la suscripción sin prorrateo. Se rechaza cuando la cuenta quedaría usando más de lo que su cuota permite sin el complemento; mira "Cuando un cambio dejaría la cuenta por encima de su cuota" más abajo. Los webhooks customer.subscription.updated, invoice.paid y customer.subscription.deleted recalculan el acceso de cada complemento con llave en esa cuenta. Ese recálculo pregunta a todos los orígenes, así que un webhook que ya no ve un complemento entre los ítems no puede apagar una capacidad que una compra de por vida o una activación gratis sigue otorgando.
Prueba
Define Días de prueba en el complemento y el catálogo muestra Prueba N días. La prueba es el principio de una compra, así que tiene que haber una tarjeta guardada antes de otorgarla: a una cuenta que no tiene ninguna le aparece el formulario de tarjeta ahí mismo —lo mismo en la página de detalle que en la tarjeta del catálogo—, no se cobra nada y la prueba empieza en cuanto se guarda. En la página de detalle el formulario se abre arriba de las dos columnas, con ancho suficiente para el campo de tarjeta, y la página sube hasta él; la columna lateral conserva su ancho. Pedir la tarjeta no escribe nada, así que quien se arrepiente en ese punto todavía puede tomar la prueba después. Escribe una fila trial con ends_at y la página de detalle muestra los días que quedan. Una prueba por cuenta y complemento, para siempre: la fila se conserva como expired cuando termina, y esa fila es la que rechaza una segunda. La prueba también se rechaza cuando la cuenta ya tiene la capacidad por cualquier vía.
Tres días antes del fin el usuario recibe un aviso que dice que el complemento se cobrará a su tarjeta para que lo conserve. El día que termina, billing:expire-trials lo cobra —prorrateado por los días que quedan del periodo, agregando el complemento a la suscripción que la cuenta tenga, o abriendo una con el plan gratuito cuando no tiene ninguna— y el usuario recibe un aviso que dice cuánto se cobró y que el siguiente cobro normal ya lo incluye. Si el cobro se rechaza, o la tarjeta se borró mientras tanto, el acceso se retira y el aviso dice que el pago no se pudo hacer. En cualquier caso la prueba queda usada y no se puede volver a tomar. Mira "Por vencimiento" más abajo.
Mientras la prueba corre, la página de detalle ofrece Cancelar prueba. Retira el acceso de inmediato, no cobra nada y no manda ningún aviso —y gasta la prueba, así que después ya no se puede tomar.
La prueba no depende de ninguna otra modalidad. Un complemento que sólo se vende de por vida puede ofrecer una, que es justamente probarlo antes de comprarlo: sin precio recurrente que cobrar, la prueba simplemente termina el día que le toca y no se cobra nada.
De por vida
Enciende Vendido de por vida y llena la tabla Precio de por vida, y el catálogo muestra Comprar de por vida. Pulsarlo abre un Stripe Checkout en modo pago y registra la sesión antes de que el usuario se vaya, con una fila lifetime en estado pending. El acceso no se otorga al volver de Stripe: la página de detalle muestra Pago en curso y consulta hasta que el webhook checkout.session.completed confirma el pago y pone la fila en active. Recargar o pulsar dos veces reutiliza la sesión pendiente, así que nada se paga dos veces.
Un derecho de por vida gana sobre cualquier otro origen, así que el catálogo muestra Adquirido, la página de detalle muestra Pagado de por vida donde iba el precio, el administrador no puede apagarlo y cancelar el plan no lo toca. Cuando la cuenta además paga el mismo complemento como línea de la suscripción, la página de detalle lo dice y ofrece Cancelar la línea recurrente; nada cambia hasta que el usuario la pulsa. El mismo mecanismo de Checkout, usado para cosas que no son complementos, es el tema de Cobra una vez con Stripe Checkout.
Por unidad y por escalones
Las dos venden cuota, así que las dos necesitan al menos un límite, y no se pueden combinar. Vendido por unidad es el precio lineal: con storage_gb = 5 a 10 al mes, la página de detalle muestra un selector de cantidad, el resumen recalcula el importe y la cuota conforme se mueve, y tres unidades son quince gigabytes a 30. Vendido por escalones es el precio no lineal: la tabla de precios gana una columna Unidades y el usuario elige una opción, de modo que diez gigabytes pueden costar 70 y no 100:
| País | Moneda | Unidades | Mensual | Anual |
|---|---|---|---|---|
| default | mxn | 1 | 10.00 | 100.00 |
| default | mxn | 10 | 70.00 | 700.00 |
| default | mxn | 50 | 250.00 | 2,500.00 |
Cambiar la cantidad (Actualizar) o el escalón (Cambiar) ajusta la línea que ya existe; nunca agrega una segunda. La pasarela prorratea la diferencia. Bajar la cantidad o pasar a un escalón menor se rechaza cuando la cuota menor quedaría por debajo de lo que la cuenta ya usa: la línea se queda como estaba y el mensaje nombra cada capacidad que quedaría por encima, con el consumo y el nuevo máximo. Subir cualquiera de los dos nunca se rechaza por la cuota, pero cobra la diferencia al momento: si la tarjeta se rechaza, nada cambia, la línea conserva su cantidad o su escalón, y la página vuelve a mostrar el contratado. Cuando la suscripción tiene un pago pendiente de completar, un cambio de escalón responde primero con eso. Con un escalón contratado, el precio de la página de detalle es el de ese escalón y no el de la primera opción; la tarjeta del catálogo también lo muestra en la pestaña del intervalo que la cuenta paga, y ofrece Cancelar sea cual sea el escalón. La capacidad recurrente se paga contratada, no consumida: quien tiene diez gigabytes paga diez use tres o todos. Para unidades que se gastan y se acaban, mira Vende consumo con medidores y paquetes.
Cuando un cambio dejaría la cuenta por encima de su cuota
Cada acción que reduce lo que da un complemento se compara con lo que la cuenta ya usa, y se rechaza cuando el consumo quedaría por encima del nuevo máximo:
- Bajar la cantidad o el escalón, en la página de detalle.
- Cancelar un complemento de pago, en la página de detalle o en la tarjeta del catálogo.
- Desactivar un complemento gratis, en la página de detalle o en la tarjeta del catálogo.
- Retirar regalo en el panel de administración.
No cambia nada, y el mensaje nombra cada capacidad que quedaría por encima: "Has excedido el límite para Espacio en disco (12 / 10)." Usar exactamente el nuevo máximo está permitido. Para seguir, la cuenta libera lo que sobra, o consigue la cuota por otra vía antes.
Sólo se saca de la cuenta el complemento que se cancela o se desactiva. El plan, los complementos que incluye, lo que se tiene de por vida o en prueba, y los demás complementos de la suscripción siguen contando. Si ese complemento era lo único que daba la capacidad, el nuevo máximo es 0, no ilimitado. Un complemento que no da ningún límite nunca se rechaza, y tampoco una suscripción cancelada automáticamente por un cargo sin pagar.
Activar y desactivar
Desde el catálogo
Los complementos gratis alternan con Activar y Desactivar. Los de pago con Comprar y Cancelar. La tarjeta también ofrece Prueba N días y Comprar de por vida cuando el complemento los tiene. La página de detalle lista cada vía que ofrece el complemento en su propia caja, con Suscribirse (y la cantidad o el escalón), Actualizar o Cambiar sobre una línea que ya se tiene, Cancelar, Cancelar prueba mientras una prueba corre, y Cancelar la línea recurrente cuando conviven una compra de por vida y una línea de suscripción.
Cancelar pide confirmación antes de quitar el complemento, en la tarjeta igual que en la página de detalle; Desactivar en un complemento gratuito también la pide en la página de detalle. Una tarjeta muestra Cancelar para un complemento que la cuenta tiene sea cual sea la pestaña abierta: contratado mensual y visto en la pestaña anual, sigue diciendo Cancelar, con el precio anual en esa pestaña. Una cuenta que ya paga un plan o un complemento no tiene segunda pestaña: los complementos se suman a esa misma suscripción, así que su catálogo muestra sólo su periodicidad. Todo lo que cobra en el momento — Comprar en la tarjeta, Suscribirse, Actualizar y Cambiar en la página de detalle — se rechaza de inmediato cuando la tarjeta se rechaza: el complemento no se agrega, la cantidad o el escalón se quedan como estaban, no queda ninguna factura sin pagar y el mensaje dice lo que respondió el banco.
Una tarjeta cuyo banco pide a quien compra aprobar el pago (3-D Secure) no se rechaza. La pantalla abre Confirma tu pago dentro de la aplicación, y Aprobar pago muestra la autenticación del propio banco. Nada se enciende hasta que el pago se aprueba: aprobado, el complemento, la cantidad o el escalón quedan activos al momento y la pantalla se recarga para decirlo; cancelado, rechazado o fallido, no se cobra nada ni cambia nada, y el mensaje dice que se puede intentar de nuevo. Quien cierra la pestaña con el pago abierto encuentra «Tienes un pago esperando tu aprobación» con Confirmar pago en la página del complemento y en Facturación, hasta que se acaba el plazo del banco (alrededor de un día); entonces desaparece, sin nada cobrado. Mientras un pago espera, cualquier otra compra o cambio en la suscripción se responde con ese mismo aviso. La misma ventana sirve para subir de plan o cambiar el periodo de cobro. El cobro al terminar una prueba sigue ocurriendo sin nadie frente a la pantalla, así que se rechaza al momento cuando el banco pide aprobación.
Tu webhook de Stripe necesita customer.subscription.pending_update_applied y customer.subscription.pending_update_expired para esto; consulta Activa los planes y el cobro.
Cualquiera de ellas que funcione recarga la pantalla y lo confirma con un aviso arriba, así que lo que el complemento habilita o retira — una entrada del menú de la cuenta, una pantalla que antes no estaba — queda en su sitio sin que el usuario recargue nada. La excepción es una compra que aún espera la confirmación del pago: esa pantalla se queda donde está y anuncia el resultado cuando llega la confirmación.
Desde el panel de administración
La página de un usuario en el panel de administración muestra un panel Complementos. Es una lista de sólo lectura: no puedes encender ni apagar un complemento de una cuenta desde el panel de administración. El panel lista los complementos que tiene la cuenta, cada uno con la vía por la que lo tiene, o "Esta cuenta no tiene complementos." Se muestra en la página del usuario, no en el formulario de edición, y guardar el formulario nunca otorga ni quita un complemento. Un complemento de pago que la cuenta tiene sólo porque se lo encendieron a mano gratis — un regalo — lleva junto a él un enlace Retirar regalo. El enlace abre un formulario corto con ese complemento ya elegido; confirmarlo retira el regalo y te devuelve a la página del usuario, con "El regalo se retiró de la cuenta." y el complemento ya fuera de la lista, salvo que la cuenta quedara por encima de su cuota, en cuyo caso se rechaza con el mismo mensaje que vería el dueño de la cuenta y te quedas en el formulario. Un complemento que se tiene por cualquier otra vía no es un regalo y no lleva enlace: cancélalo donde se compró.
Para encender un complemento a una cuenta, inicia sesión como esa cuenta y actívalo desde su pantalla Complementos, o usa una de las dos acciones de abajo.
El mismo panel tiene dos acciones, Conceder de por vida y Conceder una prueba (con los días, quince por omisión). Otorgan el complemento como si se hubiera comprado o probado, registrando qué administrador lo hizo y cuándo. Una prueba manual conserva la regla de una sola vez para siempre. El panel de sólo lectura Historial de accesos de abajo lista cada acceso que la cuenta tuvo alguna vez: complemento, origen, estado, fechas y, cuando fue a mano, quién lo otorgó.
Desde código
Todo es un método de la cuenta de facturación. Obtenla desde el dueño:
use WeblaborMx\BillingCore\BillingManager;
$account = $user->billingAccount; // relación
$account = app(BillingManager::class)->context($user, 'user')->account;
| Método | Qué hace |
|---|---|
$account->grantAddOn($key, $source, $attributes = []) |
Crea o actualiza el derecho para esa llave y origen, pone status en active y refresca la caché. $attributes puede llevar quantity, starts_at, ends_at, reference. |
$account->revokeAddOn($key, $source) |
Borra una fila free, o marca cualquier otro origen como expired, y refresca la caché. Los demás orígenes no se tocan. |
$account->startTrial($addon) |
Inicia la prueba que ofrece el complemento. Devuelve ['success' => bool, 'message' => ...]; se rechaza cuando la prueba ya se usó o la capacidad ya se tiene, y responde 'action' => 'capture_card' cuando la cuenta no tiene tarjeta guardada, que es lo que hace que la pantalla la pida. |
$account->grantManually($key, 'lifetime' o 'trial', $days, $admin) |
Lo que llaman las acciones del administrador. Registra granted_by y granted_at. |
$account->confirmLifetime($reference) |
Pone en active la fila de por vida pendiente con esa referencia. Idempotente: una ya activa responde true y no se otorga nada de nuevo. |
$account->toggleAddOn($variation) |
Compra o cancela un complemento de pago, dada una BillingPriceVariation. Devuelve ['success' => bool, 'message' => ..., 'action' => 'buy' o 'cancel'], o ['success' => true, 'url' => ...] cuando hay que abrir un Checkout. |
$user->toggleAddOn($variation) |
Lo mismo a través del dueño. |
Por vencimiento
Una prueba o cualquier derecho con ends_at deja de contar en el momento en que pasa la fecha, porque hasAddOn() lee las filas cuando se le pregunta. El comando billing:expire-trials, programado a diario a las 08:00 mientras el módulo está encendido, hace el resto: manda el aviso de las pruebas que terminan en tres días, una sola vez, y a las pruebas cuyo tiempo se acabó les cobra el complemento, les marca la fila como expired, refresca la caché features descrita más abajo y manda el aviso que corresponda a lo que pasó.
El cobro toma el precio del complemento al intervalo que la cuenta ya paga, o el mensual cuando todavía no paga nada, una unidad. Antes revisa la tarjeta guardada: a una cuenta que la borró no hay a qué cobrarle, así que el acceso simplemente se retira y el aviso dice que el pago no se pudo hacer. Con una suscripción corriendo, el complemento se le agrega como línea y se cobran prorrateados los días que quedan del periodo; sin ninguna suscripción, se abre una con el plan gratuito más el complemento y se cobra directo a la tarjeta, sin Stripe Checkout, porque no hay nadie ahí para completarlo. Un complemento sin precio a ese intervalo —vendido sólo de por vida, o gratuito— no tiene nada que cobrar, así que su prueba termina como siempre.
La fila queda como expired fuera cual fuera el desenlace, incluso cuando el usuario canceló la prueba él mismo, que es lo que sostiene una prueba por cuenta y complemento para siempre. Entre el momento en que una prueba termina y la siguiente corrida, hasAddOn() ya dice que no mientras la caché features descrita más abajo todavía dice que sí.
Verifica el acceso en tu código
Pregúntale a la cuenta. Es lo único que conoce todos los orígenes.
| Método | Devuelve |
|---|---|
$account->hasAddOn('electronic_wallet') |
true o false. |
$account->addOnSource('electronic_wallet') |
'lifetime', 'plan', 'subscription', 'trial', 'free' o null. |
$account->addOnQuantity('extra_storage') |
Unidades que se tienen: el mayor entre 1, la cantidad del ítem de la suscripción y el quantity del derecho. 0 cuando no se tiene. |
$account->entitlements() |
Las filas de billing_entitlements; ->granted() acota a las que cuentan ahora. |
$user->hasAddOn('electronic_wallet') |
La misma respuesta, preguntada desde el usuario. |
$user->addOnSource('electronic_wallet') |
El mismo origen, preguntado desde el usuario. |
$user->hasLockedAddOn('white_label') |
true cuando el origen es cualquiera menos free. Sólo para pintar el candado, ver abajo. |
$user->getIncludedAddons() |
Los complementos del plan actual. |
Pregunta con hasAddOn(), en el usuario o en la cuenta. Hay una sola forma de preguntar, lee las cinco fuentes tal como están en ese momento, y es la que decide si una pantalla, un menú o un guardia dejan pasar a alguien.
hasLockedAddOn() no es esa pregunta. Responde false para una capacidad otorgada gratis, porque existe para decirle al panel interno cuándo pintar un candado. Condicionar una pantalla con ella se la esconde justo a quien recibió la capacidad de regalo.
El json features es sólo una caché. Vive en billing_accounts.features, y también en la tabla del dueño cuando esa tabla tiene una columna features, que es como un producto que ya guarda la bandera en su propia tabla mantiene esa columna al día. La tabla users de Weblabor Base no tiene esa columna. Se escribe con refreshFeatureCache() después de cada otorgamiento, revocación y webhook, así que se atrasa en los vencimientos: una prueba que terminó hace una hora ahí todavía se lee como otorgada. Nunca decidas con ella.
Límites: $user->getLimitTotal('activities') suma el límite del plan, los límites de los complementos que son ítems de la suscripción, los límites de los complementos incluidos en el plan y los límites de los complementos que se tienen como free, trial o lifetime. Un complemento ya contado como línea o como incluido no se vuelve a contar como otorgado, así que comprar de por vida lo que el plan incluye cuenta una vez. Una línea vendida por unidad aporta su límite por la cantidad; una línea vendida por escalones aporta las unidades del escalón en lugar del valor del límite.
Lo que ve el usuario
El catálogo es /account/add-ons, accesible desde la entrada Complementos del menú de la cuenta en cuanto un complemento está a la venta. Renderiza el mismo componente que usa la página de inicio, así que ambos se ven igual.
- Un encabezado, "Mejora tu plan", y una píldora que cambia el intervalo. Sólo se ofrecen los intervalos que algún complemento realmente cotiza y que están activos en
config/pricing.php. El intervalo arranca en el que la cuenta ya paga, o mensual. Una cuenta que paga un plan, o un complemento con el plan gratis, no ve la píldora: su catálogo se queda en la periodicidad de su suscripción, y un complemento sin precio en ella dice No disponible. Con el plan gratis sin nada pagado, o sin plan, se ofrecen las dos periodicidades, y la que se elige al comprar fija la periodicidad. - Un buscador sobre nombre y descripción, y un selector de categoría que sólo lista categorías con al menos un complemento a la venta.
- Una tarjeta por complemento a la venta: ícono o una pieza de rompecabezas como relleno, nombre que enlaza al detalle, hasta dos categorías, la descripción, el precio del intervalo y el botón. Los precios se eligen para el país de la cuenta y caen a la entrada de país
default. El precio lleva su periodo: "$9.00 /mes", "$90.00 /año". El precio "de por vida" sólo aparece en un complemento que se vende de por vida. - Un complemento con precios pero ninguno para el intervalo elegido, y que no se vende de por vida, se queda en el catálogo cuando el usuario cambia de intervalo: sin precio, con el botón No disponible y la nota "Este complemento todavía no tiene precio para esa periodicidad." Un complemento con precios sólo para otros países no aparece.
- Ver guía debajo de la tarjeta cuando el complemento tiene una guía, que lleva directo a ella; Ver guías cuando tiene varias, que abre una lista corta para elegir. Nada cuando no tiene ninguna. El mismo enlace aparece en la página de inicio pública.
- La página de detalle,
/account/add-ons/{id}, agrega la galería de capturas, la descripción completa, "Qué incluye", construido a partir de los límites del complemento (en uno vendido por niveles, las unidades del nivel contratado, o del nivel elegido en el selector mientras la cuenta no lo tiene), y un panel lateral con una caja por cada vía para obtenerlo: gratis, incluido, suscripción, prueba y de por vida, cada una con su precio y su condición. Dice por qué vía la cuenta ya lo tiene, los días que quedan de una prueba y Pago en proceso mientras una compra de por vida espera su webhook. Cuando la prueba ya se usó y la cuenta no tiene el complemento por otra vía, la caja de prueba dice "La prueba de este complemento ya se usó en esta cuenta." y explica que cada cuenta puede tomar la prueba una sola vez; cuando la cuenta lo tiene por otra vía, la caja dice cuál en su lugar.
El botón dice lo que la cuenta puede hacer:
| Etiqueta | Cuándo |
|---|---|
| Adquirido | El origen es lifetime, o la cuenta lo tiene por su suscripción o por una prueba en curso y el intervalo elegido no tiene precio para él. Deshabilitado. |
| Incluido | El origen es plan. Deshabilitado. |
| Activado | El origen es free en un complemento que tiene precio. Deshabilitado. |
| Próximamente | La capacidad está declarada en código pero todavía no liberada, o el complemento no tiene precio ni capacidad alguna. Deshabilitado, con la nota "Próximamente. Este complemento aún no está disponible." |
| Pago en curso | Una compra de por vida espera su webhook. Deshabilitado. |
| Activar / Desactivar | Complemento gratis, sin activar o ya activado. |
| Comprar / Cancelar | Complemento de pago, sin ser o siendo ya ítem de la suscripción. |
| Comprar de por vida | Se vende de por vida, sin precio recurrente para el intervalo elegido. |
| Prueba N días | Ofrece prueba, todavía no usada, y la cuenta no tiene nada más. |
| No disponible | Complemento de pago sin precio para el intervalo elegido, y que no se vende de por vida. Deshabilitado, con una nota. |
| Requiere ... | La cuenta no tiene el complemento del que este depende. Deshabilitado, con el motivo al lado. |
En la página de inicio pública el catálogo aparece sólo cuando BillingAddon::isActive() es verdadero y el visitante no está en iOS. Ahí cada botón dice Obtener complemento y manda al visitante a la página de registro, y un complemento sin precio para el intervalo elegido se deja fuera en vez de mostrarse como no disponible.
Lo que no está construido
Devoluciones, disputas y contracargos: un acceso de por vida no se revoca solo cuando el pago se revierte después. El prorrateo de una cuota incluida cuando el plan cambia a media del ciclo. Las unidades que se gastan y se acaban, los paquetes prepagados y el consumo cobrado al cierre del ciclo no son complementos: son medidores, en Vende consumo con medidores y paquetes.