# Arquitectura · NimiApps 0.3.0 (ENT-003)

ENT-003 añade el catálogo comercial, la contratación, el cobro con Stripe y
el sistema central de licencias. Este documento explica cómo está construido
y qué reglas de seguridad sigue. El manual de uso del panel está en
`MANUAL-ADMIN-COMERCIAL.md`; la instalación, en
`ACTUALIZACION-CPANEL-ENT-003.md`.

## 1. Conceptos separados

| Concepto        | Qué es                                                                                   | Tabla                         |
|-----------------|------------------------------------------------------------------------------------------|-------------------------------|
| **Microapp**    | Ficha comercial de una aplicación (textos, imágenes, SEO, FAQ). Se une al código por su `code`. | `microapps`, `microapp_media` |
| **Capacidad**   | Función (`feature`) o límite (`limit`) de una microapp que los planes activan o fijan.     | `capabilities`                |
| **Producto**    | Lo que se vende. *Individual* (una microapp) o *paquete* (bundle, dos o más microapps).   | `products`, `product_microapp`|
| **Plan**        | Una modalidad del producto: público (cualquiera / solo organizaciones), prueba gratis, plazas, capacidades. | `plans`, `plan_capabilities` |
| **Precio**      | Importe y periodicidad de un plan: gratis, pago único (con o sin días de acceso), mensual o anual. Un plan puede tener varios. | `prices` |
| **Promoción / cupón** | Descuento (porcentaje o importe fijo, una vez / N meses / siempre), automático o con código. | `promotions`, `promotion_plan`, `coupons`, `coupon_redemptions` |
| **Pedido**      | Intención de compra de un espacio de trabajo, con el importe calculado por el servidor.  | `orders`                      |
| **Suscripción** | Copia local del estado de una suscripción de Stripe.                                     | `subscriptions`               |
| **Pago**        | Registro de transacciones (cobros y reembolsos) con su id de Stripe.                      | `payments`                    |
| **Licencia**    | Derecho de un espacio de trabajo a usar una microapp. Es lo único que da acceso.          | `licenses`, `license_assignments` |
| **Evento de Stripe** | Cada webhook recibido, con su estado de procesamiento.                              | `stripe_events`               |

Los importes se guardan como enteros en céntimos (nunca decimales). Los
precios se muestran con IVA incluido (`config/commerce.php`).

## 2. Tablas nuevas (6 migraciones `2026_10_22_*` + 1 del módulo demo)

| Migración                                   | Tablas                                                                 |
|---------------------------------------------|------------------------------------------------------------------------|
| `000001_create_catalog_tables`              | `catalog_categories`, `microapps`, `microapp_media`, `capabilities`    |
| `000002_create_products_and_plans_tables`   | `products`, `product_microapp`, `plans`, `plan_capabilities`, `prices` |
| `000003_create_promotions_tables`           | `promotions`, `promotion_plan`, `coupons`                              |
| `000004_create_orders_and_billing_tables`   | `billing_customers`, `orders`, `coupon_redemptions`, `subscriptions`, `payments` |
| `000005_create_licenses_tables`             | `licenses`, `license_assignments`                                      |
| `000006_create_stripe_events_table`         | `stripe_events`                                                        |
| `modules/Demo/.../100000_create_demo_notes_table` | `demo_notes` (solo el módulo de demostración)                    |

Las migraciones solo **crean** tablas: no tocan las de ENT-001/ENT-002 ni
sus datos. Integridad:

- Claves foráneas en todas las relaciones. Lo que tiene historia comercial
  (planes, precios, pedidos, pagos, licencias) usa `restrictOnDelete`: no se
  puede borrar un plan vendido, solo archivarlo.
- Claves únicas que hacen idempotentes las escrituras: `orders.idempotency_key`,
  `subscriptions.stripe_subscription_id`, `payments.stripe_id`,
  `stripe_events.stripe_event_id`, `licenses (order_id, microapp_id)` y
  `(subscription_id, microapp_id)`, `license_assignments (license_id, membership_id)`.
- Índices por `workspace_id` + estado en pedidos, suscripciones, pagos y
  licencias (consultas del panel y de la comprobación de acceso).
- Las rutas públicas usan `ulid` (no ids correlativos).

## 3. Licencias: la única puerta de acceso

`App\Support\Licensing\LicenseManager` (contrato
`App\Contracts\Microapps\Entitlements`) decide si una persona puede usar una
microapp en un espacio:

1. La persona es miembro del espacio (consulta a la base de datos en cada
   petición).
2. El espacio tiene una licencia de la microapp que da acceso **ahora**:
   - `active` o `trialing` y no vencida;
   - `past_due` (pago fallido) durante `NIMIAPPS_PAST_DUE_GRACE_DAYS` (7);
   - las licencias de suscripción vencen al final del periodo pagado
     **más 72 h** (`NIMIAPPS_SUBSCRIPTION_GRACE_HOURS`). Si Stripe dejara de
     enviar webhooks de renovación, el acceso se corta solo: el sistema
     falla de forma cerrada.
3. En organizaciones con licencia en modo «solo miembros asignados», la
   persona tiene plaza. Las plazas de quien deja la organización se liberan.
4. Las capacidades son la unión de las licencias válidas.

Estados de licencia: `active`, `trialing`, `past_due`, `pending`,
`suspended`, `canceled`, `expired`, `replaced` (sustituida por otra compra
del mismo producto). Origen: `order`, `subscription` o `manual`
(concedida por un administrador).

Un producto *paquete* crea una licencia por cada microapp incluida.

### Cómo se comprueba en las rutas

Las rutas de una microapp se publican bajo `/apps/<slug>` y llevan siempre,
por construcción (no depende de quien escriba el módulo):

```
web, auth, auth.session, verified, workspace, microapp:<code>
```

Dentro del módulo se añaden `microapp.can:<code>,<capacidad>` y
`microapp.permission:<code>,<permiso>`. Los límites se consultan con
`Entitlements::limit()`. Todo en el servidor; la interfaz solo oculta
botones. Hay una prueba que recorre todas las rutas de microapps y falla si
alguna no lleva la cadena obligatoria.

## 4. Compra y cobro

```
Ficha pública ─► /panel/contratar/{plan} ─► Pedido (importe del servidor)
     │                                         │
     │   plan gratuito: licencia al momento ◄──┤
     │                                         ▼
     │                         Stripe Checkout (página de Stripe)
     │                                         │
     ▼                                         ▼
 Webhook firmado ──► se vuelve a leer el objeto en la API de Stripe ──► Licencia
 Regreso del navegador ──► se lee la sesión en la API de Stripe ──────► (misma lógica, idempotente)
```

Reglas:

- **El navegador nunca concede acceso.** La página de regreso
  (`/panel/pedidos/{ulid}/confirmacion`) solo consulta la sesión de Checkout
  en la API de Stripe con la clave secreta. Si Stripe no dice «pagada», el
  pedido sigue pendiente.
- **Los webhooks son solo un aviso.** Se verifica la firma
  (`STRIPE_WEBHOOK_SECRET`, tolerancia 300 s) y después se vuelve a pedir
  el objeto a la API (sesión, suscripción, factura, cobro). Un webhook con
  contenido falsificado pero firma válida no puede inventar un pago.
- Se comprueba que el importe, la moneda, el pedido y el espacio de la
  sesión coinciden con el pedido local.
- **Idempotencia.** Cada evento se guarda por su id único y se «reclama»
  con un `UPDATE` condicional; un evento repetido o simultáneo no se
  procesa dos veces. Si el proceso falla, el evento queda `failed`, se
  responde 500 (Stripe reintenta) y se puede reprocesar desde el panel.
  Todas las llamadas que crean algo en Stripe llevan clave de idempotencia
  (pedido, cambio de plan, cancelación, reembolso).
- **Pagos duplicados.** Si un espacio paga dos veces el mismo producto
  (dos pestañas), la segunda suscripción se cancela en Stripe y el pedido
  queda marcado «duplicado» en el panel para reembolsarlo. Antes de crear
  una sesión se caducan las sesiones abiertas de otros pedidos del mismo
  producto.
- **Tarjetas.** NimiApps no ve ni guarda datos de tarjeta: el cobro se hace
  en la página de Stripe. Solo se guardan ids de Stripe, importes y estados.
- **Claves.** Solo en `.env` (`STRIPE_SECRET`, `STRIPE_WEBHOOK_SECRET`). Ni
  el panel ni `microapps:check` ni los registros las muestran. Una clave
  `sk_live_` se rechaza salvo que `STRIPE_ALLOW_LIVE=true`; un evento live
  se ignora con claves de prueba.
- **Sin credenciales**, el catálogo se administra igual y los planes
  gratuitos funcionan; los planes de pago muestran «pagos no disponibles»
  y no se pueden contratar. No se simula ningún pago.
- Los objetos de Stripe (cliente, producto, precio, cupón) se crean bajo
  demanda y se guardan por modo (test / live), así que pasar a real no
  mezcla objetos de prueba.

### Eventos de Stripe que se procesan

`checkout.session.completed`, `checkout.session.async_payment_succeeded`,
`checkout.session.async_payment_failed`, `checkout.session.expired`,
`customer.subscription.created`, `customer.subscription.updated`,
`customer.subscription.deleted`, `customer.subscription.paused`,
`customer.subscription.resumed`, `invoice.paid`, `invoice.payment_failed`,
`charge.refunded`, `refund.created`, `refund.updated`.

El resto se guarda como «ignorado». Versión de API fijada:
`2025-08-27.basil`.

### Ciclo de vida de la suscripción

| Situación                    | Comportamiento                                                                 |
|------------------------------|--------------------------------------------------------------------------------|
| Prueba gratis                | Licencia `trialing`. Sin prueba si el espacio ya tuvo ese producto.            |
| Renovación pagada            | `invoice.paid` → se amplía el fin de la licencia.                              |
| Pago fallido                 | `past_due`: 7 días de acceso mientras Stripe reintenta; después se corta.      |
| Cancelación por el cliente   | Al final del periodo; se puede reactivar antes.                                |
| Cancelación por administrador| Inmediata (desde `/admin/comercial/suscripciones`).                            |
| Ampliación de plan           | Inmediata; Stripe cobra la diferencia en el acto. Si el cobro falla, no se aplica. |
| Reducción de plan            | Inmediata; la parte no usada queda como saldo a favor.                         |
| Paso a plan gratuito         | Cancelación al final del periodo; entonces se concede el plan gratuito.        |
| Recurrente → pago único      | No se ofrece (hay que cancelar y comprar).                                     |
| Reembolso total de pago único| Retira la licencia.                                                            |
| Reembolso de suscripción     | No retira el acceso por sí solo; para eso, cancelar.                           |
| Disputas (contracargos)      | **No se gestionan** en ENT-003: se ven en Stripe.                              |

## 5. Organizaciones

- Solo el **propietario** puede contratar, cambiar de plan o cancelar en
  nombre de la organización (permiso `billing.manage`). Los
  **administradores** ven las contrataciones (`billing.view`) y gestionan
  las plazas (`licenses.manage`). Los **miembros** no ven la facturación.
- El espacio para el que se compra se elige en la página de contratación y
  se vuelve a comprobar en el servidor (pertenencia y permiso).
- Licencia en modo «todos los miembros» o «solo asignados». Un plan con
  plazas (`seats_limit`) obliga al modo asignado y no deja superar el límite.
- Aislamiento: todas las consultas de pedidos, suscripciones, pagos y
  licencias del panel filtran por el espacio activo; un id de otra
  organización devuelve 404. Las tablas de módulos usan el rasgo
  `BelongsToWorkspace`.

## 6. Panel de administración (`/admin/comercial`)

Solo administradores de plataforma (ENT-002). Todo se gestiona desde el
navegador: categorías, microapps (con importación de capacidades desde el
`module.json` e imágenes JPEG/PNG/WebP comprobadas por contenido),
productos, planes, precios, promociones, cupones, pedidos, suscripciones,
pagos y reembolsos, eventos de Stripe (con reprocesado) y licencias
(concesión manual, suspensión, reactivación, revocación).

- Validación de cada formulario con mensajes en español.
- Confirmación explícita en un diálogo antes de cada acción destructiva
  (borrar, desactivar, suspender, revocar, cancelar, reprocesar) y casilla
  obligatoria en los reembolsos.
- Lo vendido queda protegido: el código de una microapp vendida y la
  composición de un producto vendido no se pueden cambiar; un precio no se
  edita (se desactiva y se crea otro, así los clientes actuales conservan
  el suyo).
- Cada cambio queda en el registro de auditoría (`audit_logs`, eventos
  `catalog.*` y `commerce.*`), visible en
  `/admin/comercial/auditoria`.
- El panel **no permite subir código**. Los módulos se instalan como
  archivos con un paquete de actualización.

## 7. Catálogo público y SEO

- `/aplicaciones`: búsqueda, filtro por categoría y por tipo de precio
  (gratis, pago único, suscripción), orden por destacadas, nombre, precio o
  recientes.
- `/aplicaciones/{slug}`: landing con título y descripción SEO, URL
  canónica, Open Graph, datos estructurados `SoftwareApplication` (JSON-LD),
  planes con selector mensual/anual, paquetes que la incluyen y FAQ.
- `noindex` mientras `NIMIAPPS_INDEXABLE=false` (staging).

## 8. Contratos para microapps futuras (documentación)

La futura microapp de documentación **no está construida**. Lo que queda
preparado es el contrato que usará:

| Contrato (`app/Contracts/...`)      | Para qué                                                   |
|-------------------------------------|------------------------------------------------------------|
| `Microapps\MicroappRegistry`        | Qué microapps aporta el código instalado (`module.json`).  |
| `Microapps\ModuleRegistry`          | Módulos instalados y habilitados.                          |
| `Microapps\Entitlements`            | Licencia, capacidades, límites y permisos por rol.         |
| `Microapps\CatalogIntegration`      | Ficha comercial de un código e importación de capacidades. |
| `Billing\PaymentGateway`            | Pasarela de pago (implementada por `StripeGateway`).        |

Para añadir una microapp: crear `modules/<Nombre>/` con el bloque
`microapp` en `module.json` (ver `modules/README.md`), instalarla con un
paquete de actualización, crear su ficha en `/admin/comercial` con el mismo
código, importar capacidades, crear producto, planes y precios y publicar.

El módulo `modules/Demo` («Demo de licencias») es una microapp de prueba
controlada que usa todo lo anterior: notas por espacio, límite de notas por
plan, exportación CSV como capacidad, borrar solo para propietario o
administrador. No tiene uso comercial y debe desactivarse antes de abrir el
sitio al público (`"enabled": false`).

## 9. Estructura de carpetas (lo nuevo)

```
app/Actions/Commerce/          Casos de uso: pedir, pagar, cancelar, cambiar plan, reembolsar, plazas
app/Billing/                   Stripe: pasarela, webhooks, conciliación, sincronización, registro de pagos
app/Contracts/                 Contratos para microapps y pasarela de pago
app/Enums/Commerce/            Estados y tipos
app/Http/Controllers/Admin/Commerce/  Panel /admin/comercial
app/Http/Controllers/Panel/    Contratar, pedidos, contrataciones, licencias
app/Http/Middleware/EnsureMicroapp*   microapp, microapp.can, microapp.permission
app/Models/Commerce/           Modelos del catálogo, ventas y licencias
app/Support/Commerce/          Presupuesto, importes, entrega de lo contratado
app/Support/Licensing/         Gestor y emisor de licencias
config/commerce.php            Valores técnicos (moneda, márgenes, Stripe desde .env)
modules/Demo/                  Microapp de demostración controlada
resources/views/admin/commerce/, catalog/, panel/billing, checkout, orders, licenses
tests/Feature/Commerce/, tests/Unit/Commerce/, tests/Support/ (simulador de Stripe)
```
