---
title: "Arquitectura de News Kingdom"
description: "Cómo una entrada del blog de un miembro se convierte en una noticia de la página principal. Puntuación con IA, promoción de pago mediante PaymentConductor, aprobación del administrador en Telegram, tablas de noticias en JSONB y el registro de auditoría de envíos."
locale: "es"
---
# Arquitectura de News Kingdom

> **Info**
> Usa las pestañas **Fundador** / **Desarrollador** de la barra lateral de la documentación para filtrar esta página. Los fundadores ven el flujo de negocio y los ajustes del operador. Los desarrolladores ven los servicios, las rutas, las variables de entorno y una prueba local.

News Kingdom es la capa de promoción que se apoya en el módulo de noticias de Ring. Los miembros con el rol `member` o superior escriben artículos que viven en su espacio personal en `/{username}/{slug}`. Después, el autor puede pedir un lugar en el feed principal `/news`. Ring puntúa el artículo, pide al autor que pague un precio basado en esa puntuación y lo envía a los administradores de la plataforma en Telegram para que den el sí o el no definitivo.

Cada solicitud de promoción pasa por un único campo del artículo, `mainPageStatus`:

| Etapa | Qué ocurre | `mainPageStatus` |
|-------|--------------|------------------|
| Envío | El autor pide aparecer en la página principal | `submitted` |
| Puntuación | La IA o una heurística puntúa ética, spam, duplicados y mérito | `ai_scored` |
| Bloqueo estricto | Ética demasiado baja, o riesgo de spam o de duplicado demasiado alto | `published_blog_only` |
| Pago | Se fija el precio y se abre un checkout de PaymentConductor | `payment_pending` |
| Pagado | El webhook confirma el pago y los administradores reciben una tarjeta en Telegram | `awaiting_admin_approval` |
| Aprobado | El artículo pasa al feed principal de todo el sitio | `published_main` |
| Rechazado | El administrador lo rechaza. El artículo se queda en el blog del autor | `rejected` |

### For founders

## Por qué importa para tu clon

- **Los miembros aportan el contenido y pagan por el alcance.** Diriges un periódico sin contratar una redacción. Los autores que quieren la portada pagan por ella, y la decisión editorial final sigue siendo tuya.
- **Lo malo se filtra primero.** La puntuación detiene el spam, los duplicados y las publicaciones de baja ética antes de que nadie pague y antes de que un administrador les dedique tiempo.
- **Apruebas desde el móvil.** Cada envío pagado llega a tus administradores como un mensaje de Telegram con los botones Approve, Reject, View y Counter.
- **Las buenas historias tienen recompensa.** Cuando un administrador aprueba una historia escrita por un miembro, Ring pone en cola un crédito de recompensa para el autor e inicia las traducciones a tus otros idiomas.

  
- **[Blogs de miembros](/docs/features/member-blog.md)** — Dónde vive el artículo antes de la promoción: la URL personal del autor.

  
- **[Módulo de noticias](/docs/features/news.md)** — El feed principal, las categorías, los me gusta y el editor que usan los autores.

  
- **[PaymentConductor](/docs/features/payment-conductor.md)** — El checkout que cobra la tarifa de promoción con el procesador que hayas configurado.

  
- **[Gestión desde Telegram](/docs/features/manage-via-telegram.md)** — El bot de administración que lleva los botones de aprobación.

```mermaid
flowchart LR
  A["Artículo del miembro en /username/slug"] --> B["Solicitar promoción"]
  B --> C{"Puntuación IA"}
  C -->|bloqueo estricto| D["Se queda solo en el blog"]
  C -->|ok| E["Pagar el precio de promoción"]
  E --> F["Tarjeta de Telegram para administradores"]
  F -->|Approve| G["Feed principal /news"]
  F -->|Reject| H["Se queda en el blog"]
  F -->|Counter| E
```

### Escenarios típicos

1. **Periódico comunitario.** Un clon de ciudad deja que los miembros locales publiquen en sus blogs y cobra una pequeña tarifa por las historias de portada. Los administradores aprueban las mejores en Telegram.
2. **Red de expertos.** Un clon profesional mantiene alto el listón del mérito: los artículos sólidos reciben un precio bajo y los débiles quedan fuera por precio.
3. **Centro de campañas moderado.** Un clon de un movimiento se apoya en las reglas de bloqueo estricto para que el spam nunca llegue a la cola del administrador.

### Ajustes del operador

Los precios se fijan en UAH con variables de entorno del servidor. Si no las defines, se aplican estos valores por defecto:

| Ajuste | Por defecto | Efecto |
|---------|---------|--------|
| Precio base | 50 UAH | Precio mínimo para un artículo de mérito alto |
| Escala | 2000 UAH | Cuánto sube el precio a medida que baja el mérito |
| Máximo | 5000 UAH | Tope de precio |
| Umbral de mérito bajo | mérito inferior a 0.25 | El precio sube al menos a 4999 UAH |
| Contraoferta del administrador | +25% | El botón **Counter** de Telegram fija un nuevo importe que el autor debe pagar |

### For developers

## Mapa de módulos (verificado en `ring/web`)

| Aspecto | Ruta |
|---------|------|
| Máquina de estados, auditoría, aprobación y rechazo | `features/news/services/news-promotion-workflow.ts` |
| Puntuación con OpenRouter y precio | `features/news/services/news-scoring-service.ts` |
| Heurística de duplicados | `features/news/services/news-duplicate-check.ts` |
| Fachada de checkout (`purpose: 'news_promotion'`) | `features/news/services/news-payment-service.ts` |
| Manejadores del webhook de pago | `lib/payments/conductor/handlers/news-promotion.ts`, `news-promotion-paypal.ts` |
| Tarjeta y teclado de administración en Telegram | `features/news/services/news-telegram-approval.ts` |
| Gestión de callbacks de Telegram | `lib/telegram/admin-bot/news-callback-handler.ts` (conectado desde `app/api/telegram/admin-bot/webhook/route.ts`) |
| Interfaz de promoción para miembros | `features/news/components/news-promotion-panel.tsx` en `/my-news` |
| Tipos de estado y de campos | `features/news/types/index.ts` (`MainPageStatus`, `NewsContentType`, `NewsVisibility`) |
| Mapeador de filas JSONB | `lib/news/map-news-document.ts` |
| Marca y URL absolutas | `lib/site-branding.ts` (`localeBlogArticleUrl`, `absoluteSiteUrl`) |

## Rutas

| Método y ruta | Autenticación | Función |
|-----------------|------|------|
| `POST /api/news/promotion/submit` | Sesión, solo el autor del artículo | Marca `promoteToMainPage`, puntúa y abre el checkout |
| `PATCH /api/news/promotion/submit` | Cabecera `x-news-promotion-secret` | Dev o pruebas: marcar como pagado y avisar a los administradores |
| `POST /api/payments/{wayforpay,stripe,paypal}/webhook` | Firma del procesador | Confirmación canónica del pago |
| `POST /api/news/promotion/wayforpay-webhook` | Payload del procesador | Delegado obsoleto hacia el webhook canónico de WayForPay |
| `POST /api/mcp/v1/news/[id]/{score,approve,reject,publish}` | Token de servicio (`withMcpGuard`) | Operaciones de agentes y MCP sobre el mismo flujo |

## Reglas de puntuación

`scoreNewsForMainPage()` llama a OpenRouter cuando `OPENROUTER_API_KEY` está definida. Sin clave, mantiene los valores heurísticos por defecto (ethics 0.7, spam 0.2, merit 0.5) y registra `model: 'heuristic'`.

- **Riesgo de duplicado:** 0.95 si otro artículo comparte `slug` o `siteWideSlug`. 0.9 si un artículo publicado (entre los primeros 200) tiene el mismo título.
- **Bloqueo estricto:** `ethics < NEWS_ETHICS_MIN` o `spamRisk > NEWS_SPAM_MAX` o `duplicateRisk > NEWS_DUPLICATE_MAX`. Resultado: `published_blog_only` y sin checkout.
- **Precio:** `clamp(base, max, base + (1 - merit) * scale)`. Un mérito inferior a `NEWS_MERIT_SOFT_REJECT` sube el precio al menos a 4999. El mérito bajo no cambia el estado.

## Variables de entorno

Ninguna de las claves `NEWS_*` está en `env.local.template`. El código las lee con estos valores por defecto.

| Variable | Por defecto | Se lee en |
|----------|---------|---------|
| `NEWS_PROMO_BASE_UAH` / `NEWS_PROMO_SCALE_UAH` / `NEWS_PROMO_MAX_UAH` | 50 / 2000 / 5000 | `news-scoring-service.ts` |
| `NEWS_ETHICS_MIN` / `NEWS_SPAM_MAX` / `NEWS_DUPLICATE_MAX` | 0.35 / 0.85 / 0.9 | `news-scoring-service.ts` |
| `NEWS_MERIT_SOFT_REJECT` | 0.25 | puntuación y flujo de promoción |
| `OPENROUTER_API_KEY`, `OPENROUTER_MODEL_PRIMARY`, `OPENROUTER_MODEL_FALLBACK` | sin definir | `news-scoring-service.ts` |
| `PAYMENT_NEWS_PROCESSOR` | recurre a `PAYMENT_DEFAULT_PROCESSOR` y luego a `wayforpay` | `lib/payments/payment.config.ts` |
| `NEWS_PROMOTION_WEBHOOK_SECRET` | sin definir | `PATCH /api/news/promotion/submit` |
| `ADMIN_BOT_TOKEN` | sin definir | Respuestas a callbacks de Telegram |

## Datos

Las cuatro tablas usan el modelo de documentos JSONB (`id`, `data jsonb`, marcas de tiempo) en `data/schema.sql` (aplanado v4.1.0): `news`, `news_categories`, `news_likes`, `news_submission_audit`. Cada transición añade una entrada a `data.mainPageStatusHistory` y escribe una fila en `news_submission_audit`. `data/migrations/002_news_content_schema.sql` y `003_news_kingdom_upgrade.sql` son históricos. Las instalaciones nuevas solo aplican `schema.sql`.

## Efectos secundarios de la aprobación

`approveMainPagePublication()` fija `status: 'published'` y `visibility: 'site-wide'`. En las historias escritas por usuarios, después pone en cola `enqueueRewardCreditAddEvent({ trigger: 'newsStoryApproved' })` y lanza `generateArticleTranslations()`. Los destinatarios en Telegram son usuarios con un rol de administrador de la plataforma y un `communication.telegramId`.

## Prueba local

**Aplica el esquema y crea un artículo de miembro**

Aplica `data/schema.sql` e inicia sesión como un usuario con rol `member` o superior. Crea un artículo en `/news/create`. Queda marcado con `blogUsername` y aparece en `/{username}/{slug}` una vez publicado.

**Envíalo a promoción**

{`curl -X POST http://localhost:3000/api/news/promotion/submit \\
  -H "Content-Type: application/json" \\
  -b "" \\
  -d '{"articleId":""}'`}

La respuesta incluye `status`, `aiScore` y una redirección o los campos de pago para el checkout.

**Simula el pago en dev**

{`curl -X PATCH http://localhost:3000/api/news/promotion/submit \\
  -H "Content-Type: application/json" \\
  -H "x-news-promotion-secret: $NEWS_PROMOTION_WEBHOOK_SECRET" \\
  -d '{"articleId":"","payment":{}}'`}

**Aprueba**

Pulsa **Approve** en la tarjeta de Telegram o llama a `POST /api/mcp/v1/news//approve` con un token de servicio. Comprueba que `mainPageStatus` es `published_main` y que existe una fila en `news_submission_audit`.

## Carencias conocidas

## Documentación relacionada

  
- [features/member-blog](/docs/features/member-blog.md) — Mismo flujo: dónde vive el artículo antes y después de solicitar la promoción.

  
- [features/news](/docs/features/news.md) — Requisito previo: el módulo de noticias, el editor y el feed principal al que promociona News Kingdom.

  
- [architecture/payment-conductor](/docs/architecture/payment-conductor.md) — Depende de: cómo se enrutan el checkout news_promotion y sus webhooks.

  
- [features/manage-via-telegram](/docs/features/manage-via-telegram.md) — Ver también: el bot de administración que lleva los botones Approve, Reject y Counter.
