campaigns.routes.js

Overview

Este archivo define el router HTTP de campañas para la API. Expone operaciones de listado, consulta, creación, actualización, cambio de estado y eliminación.

Todas las rutas requieren autenticación. El módulo usa requireAuth para obtener req.auth.userId y aplicar control de propiedad sobre cada campaña.

[!IMPORTANT] Las operaciones solo actúan sobre campañas creadas por el usuario autenticado.

How it works

El router combina validación de autenticación, validación de payload y acceso a MongoDB mediante Campaign y mongoose.

flowchart TD
    Client[Client]
    subgraph CampaignRoutes[campaigns routes]
        Auth[requireAuth]
        List[List campaigns]
        Detail[Get campaign by id]
        Create[Create campaign]
        Update[Update campaign]
        Status[Update status]
        Delete[Delete campaign]
        Validator[validateCampaignPayload]
        ObjectIdCheck[ObjectId validation]
        CampaignModel[Campaign model]
    end

    Client --> Auth
    Auth --> List
    Auth --> Detail
    Auth --> Create
    Auth --> Update
    Auth --> Status
    Auth --> Delete

    List --> ObjectIdCheck
    Detail --> ObjectIdCheck
    Create --> Validator
    Create --> ObjectIdCheck
    Update --> Validator
    Update --> ObjectIdCheck
    Status --> ObjectIdCheck
    Delete --> ObjectIdCheck

    List --> CampaignModel
    Detail --> CampaignModel
    Create --> CampaignModel
    Update --> CampaignModel
    Status --> CampaignModel
    Delete --> CampaignModel

Cada handler verifica la identidad del usuario primero. Luego filtra por createdBy. Las rutas de escritura validan datos antes de tocar la base de datos.

Key components

{
    "title": "campaigns.routes.js",
    "content": "Express router que registra las rutas protegidas de campa\u00f1as. Agrupa lectura, escritura, cambios de estado y eliminaci\u00f3n.",
    "type": "function",
    "filePath": "services/api/src/modules/campaigns/campaigns.routes.js",
    "badges": [
        "public"
    ]
}
{
    "title": "requireAuth",
    "content": "Middleware de autenticaci\u00f3n usado por todas las rutas del m\u00f3dulo. Debe exponer `req.auth.userId` para las comprobaciones de propiedad.",
    "type": "function",
    "filePath": "services/api/src/modules/auth/auth.middleware.js",
    "badges": [
        "public"
    ]
}
{
    "title": "validateCampaignPayload",
    "content": "Valida el cuerpo de creaci\u00f3n y actualizaci\u00f3n de campa\u00f1as. El handler de PATCH usa validaci\u00f3n parcial.",
    "type": "function",
    "filePath": "services/api/src/modules/campaigns/campaigns.validators.js",
    "badges": [
        "public"
    ]
}
{
    "title": "Campaign",
    "content": "Modelo de Mongoose usado para buscar, contar, crear, actualizar y eliminar campa\u00f1as.",
    "type": "class",
    "filePath": "services/api/src/models/Campaign.js",
    "badges": [
        "public"
    ]
}

Data & fields

Este módulo trabaja con parámetros de consulta, parámetros de ruta y cuerpos JSON. El campo createdBy nunca lo envía el cliente. El router lo inyecta desde la sesión autenticada.

Query parameters del listado

[
    {
        "name": "page",
        "type": "number",
        "description": "Página solicitada. Si es inválida, se usa 1."
    },
    {
        "name": "limit",
        "type": "number",
        "description": "Cantidad de resultados por página. Si es inválida, se usa 10. El máximo permitido es 50."
    },
    {
        "name": "status",
        "type": "string",
        "description": "Filtra campañas por estado exacto."
    },
    {
        "name": "listingId",
        "type": "string",
        "description": "Filtra por el listado asociado. Solo se aplica si es un ObjectId válido."
    }
]

Path parameters

[
    {
        "name": "id",
        "type": "string",
        "required": true,
        "description": "Identificador de la campaña. Debe ser un ObjectId válido."
    }
]

Body fields de estado

[
    {
        "name": "status",
        "type": "string",
        "required": true,
        "description": "Estado permitido para el cambio manual. Solo admite draft, paused o cancelled."
    }
]

Campos protegidos en actualización

  • createdBy [badge:read-only] se elimina del cuerpo antes de actualizar.
  • paymentOrderId [badge:read-only] se elimina del cuerpo antes de actualizar.
  • metrics [badge:read-only] se elimina del cuerpo antes de actualizar.

[!NOTE] La validación de creación y edición depende de validateCampaignPayload. Este archivo no define el esquema completo del payload.

Forma de respuesta

  • success [badge:required] boolean — Indica éxito o fallo.
  • message [badge:optional] string — Mensaje humano para operaciones y errores.
  • count [badge:optional] number — Cantidad de campañas devueltas en la página actual.
  • total [badge:optional] number — Total de campañas que cumplen el filtro.
  • page [badge:optional] number — Página actual.
  • pages [badge:optional] number — Total de páginas calculadas.
  • data [badge:optional] array | object — Lista o documento de campaña.
  • errors [badge:optional] array — Detalle de errores de validación.

Behaviors

  • GET /api/campaigns — Devuelve campañas del usuario autenticado, con paginación y filtros opcionales.
  • GET /api/campaigns/:id — Devuelve una sola campaña si pertenece al usuario.
  • POST /api/campaigns — Valida el payload, agrega createdBy y crea una campaña nueva.
  • PATCH /api/campaigns/:id — Valida parcialmente el payload y actualiza una campaña propia.
  • PATCH /api/campaigns/:id/status — Cambia el estado manual de la campaña dentro de un conjunto permitido.
  • DELETE /api/campaigns/:id — Elimina una campaña propia solo en estados permitidos.

API

Cada endpoint tiene su propio bloque. Todos están protegidos por autenticación.

GET /api/campaigns

Lista las campañas del usuario autenticado. El resultado incluye paginación, total y filtros por status y listingId.

{
    "title": "List Campaigns",
    "description": "Returns the authenticated user's campaigns with pagination and optional filters.",
    "method": "GET",
    "baseUrl": "/api",
    "endpoint": "/campaigns",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        }
    ],
    "queryParams": [
        {
            "key": "page",
            "value": "1",
            "required": false
        },
        {
            "key": "limit",
            "value": "10",
            "required": false
        },
        {
            "key": "status",
            "value": "draft",
            "required": false
        },
        {
            "key": "listingId",
            "value": "64f1c2a1e8f2d5b9c4a12345",
            "required": false
        }
    ],
    "pathParams": [],
    "bodyType": "none",
    "requestBody": "",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Success",
            "body": "{\n  \"success\": true,\n  \"count\": 1,\n  \"total\": 3,\n  \"page\": 1,\n  \"pages\": 3,\n  \"data\": [\n    {\n      \"_id\": \"66a1c2a1e8f2d5b9c4a67890\",\n      \"createdBy\": \"64f0c2a1e8f2d5b9c4a11111\",\n      \"status\": \"draft\",\n      \"listingId\": {\n        \"_id\": \"64f1c2a1e8f2d5b9c4a12345\",\n        \"title\": \"Listado demo\",\n        \"meta\": {\n          \"entityType\": \"listing\"\n        },\n        \"publication\": {\n          \"status\": \"published\"\n        }\n      }\n    }\n  ]\n}"
        },
        "500": {
            "description": "Server error",
            "body": "{\n  \"success\": false,\n  \"message\": \"No se pudieron obtener las campa\u00f1as.\"\n}"
        }
    }
}

GET /api/campaigns/:id

Devuelve una campaña concreta del usuario autenticado. Si el id no es válido, responde con 400.

{
    "title": "Get Campaign",
    "description": "Returns a single campaign owned by the authenticated user.",
    "method": "GET",
    "baseUrl": "/api",
    "endpoint": "/campaigns/:id",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [
        {
            "key": "id",
            "value": "66a1c2a1e8f2d5b9c4a67890",
            "required": true
        }
    ],
    "bodyType": "none",
    "requestBody": "",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Success",
            "body": "{\n  \"success\": true,\n  \"data\": {\n    \"_id\": \"66a1c2a1e8f2d5b9c4a67890\",\n    \"createdBy\": \"64f0c2a1e8f2d5b9c4a11111\",\n    \"listingId\": {\n      \"_id\": \"64f1c2a1e8f2d5b9c4a12345\",\n      \"title\": \"Listado demo\",\n      \"meta\": {\n        \"entityType\": \"listing\"\n      },\n      \"publication\": {\n        \"status\": \"published\"\n      },\n      \"location\": {\n        \"city\": \"Madrid\"\n      }\n    }\n  }\n}"
        },
        "400": {
            "description": "Invalid ID",
            "body": "{\n  \"success\": false,\n  \"message\": \"ID inv\u00e1lido.\"\n}"
        },
        "404": {
            "description": "Not found or forbidden",
            "body": "{\n  \"success\": false,\n  \"message\": \"Campa\u00f1a no encontrada o sin permisos.\"\n}"
        },
        "500": {
            "description": "Server error",
            "body": "{\n  \"success\": false,\n  \"message\": \"No se pudo obtener la campa\u00f1a.\"\n}"
        }
    }
}

POST /api/campaigns

Crea una nueva campaña. El handler agrega createdBy desde la autenticación y valida el payload completo.

{
    "title": "Create Campaign",
    "description": "Creates a new campaign for the authenticated user after validating the payload.",
    "method": "POST",
    "baseUrl": "/api",
    "endpoint": "/campaigns",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [],
    "bodyType": "json",
    "requestBody": "{\n  \"listingId\": \"64f1c2a1e8f2d5b9c4a12345\"\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "201": {
            "description": "Created",
            "body": "{\n  \"success\": true,\n  \"message\": \"Campa\u00f1a creada correctamente.\",\n  \"data\": {\n    \"_id\": \"66a1c2a1e8f2d5b9c4a67890\",\n    \"createdBy\": \"64f0c2a1e8f2d5b9c4a11111\",\n    \"listingId\": \"64f1c2a1e8f2d5b9c4a12345\"\n  }\n}"
        },
        "400": {
            "description": "Invalid payload or listingId",
            "body": "{\n  \"success\": false,\n  \"message\": \"Payload inv\u00e1lido.\",\n  \"errors\": []\n}"
        },
        "500": {
            "description": "Server error",
            "body": "{\n  \"success\": false,\n  \"message\": \"No se pudo crear la campa\u00f1a.\"\n}"
        }
    }
}

PATCH /api/campaigns/:id

Actualiza una campaña propia. El handler usa validación parcial. También elimina createdBy, paymentOrderId y metrics del cuerpo recibido.

{
    "title": "Update Campaign",
    "description": "Updates an owned campaign using partial validation and protected field stripping.",
    "method": "PATCH",
    "baseUrl": "/api",
    "endpoint": "/campaigns/:id",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [
        {
            "key": "id",
            "value": "66a1c2a1e8f2d5b9c4a67890",
            "required": true
        }
    ],
    "bodyType": "json",
    "requestBody": "{\n  \"status\": \"paused\"\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Updated",
            "body": "{\n  \"success\": true,\n  \"message\": \"Campa\u00f1a actualizada correctamente.\",\n  \"data\": {\n    \"_id\": \"66a1c2a1e8f2d5b9c4a67890\",\n    \"createdBy\": \"64f0c2a1e8f2d5b9c4a11111\",\n    \"status\": \"paused\"\n  }\n}"
        },
        "400": {
            "description": "Invalid request",
            "body": "{\n  \"success\": false,\n  \"message\": \"Payload inv\u00e1lido.\",\n  \"errors\": []\n}"
        },
        "404": {
            "description": "Not found or forbidden",
            "body": "{\n  \"success\": false,\n  \"message\": \"Campa\u00f1a no encontrada o sin permisos.\"\n}"
        },
        "500": {
            "description": "Server error",
            "body": "{\n  \"success\": false,\n  \"message\": \"No se pudo actualizar la campa\u00f1a.\"\n}"
        }
    }
}

PATCH /api/campaigns/:id/status

Cambia el estado de la campaña. Solo acepta draft, paused y cancelled. El comentario del código indica que active y completed los controla el sistema de pagos y webhooks.

{
    "title": "Update Campaign Status",
    "description": "Changes the campaign status to a user-allowed value only.",
    "method": "PATCH",
    "baseUrl": "/api",
    "endpoint": "/campaigns/:id/status",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [
        {
            "key": "id",
            "value": "66a1c2a1e8f2d5b9c4a67890",
            "required": true
        }
    ],
    "bodyType": "json",
    "requestBody": "{\n  \"status\": \"paused\"\n}",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Status updated",
            "body": "{\n  \"success\": true,\n  \"message\": \"Campa\u00f1a paused.\",\n  \"data\": {\n    \"_id\": \"66a1c2a1e8f2d5b9c4a67890\",\n    \"status\": \"paused\"\n  }\n}"
        },
        "400": {
            "description": "Invalid ID or invalid status",
            "body": "{\n  \"success\": false,\n  \"message\": \"Transici\u00f3n inv\u00e1lida. El usuario puede establecer: draft, paused, cancelled.\"\n}"
        },
        "404": {
            "description": "Not found or forbidden",
            "body": "{\n  \"success\": false,\n  \"message\": \"Campa\u00f1a no encontrada o sin permisos.\"\n}"
        },
        "500": {
            "description": "Server error",
            "body": "{\n  \"success\": false,\n  \"message\": \"No se pudo actualizar el estado.\"\n}"
        }
    }
}

DELETE /api/campaigns/:id

Elimina una campaña propia. Solo permite borrar campañas en estado draft o cancelled.

{
    "title": "Delete Campaign",
    "description": "Deletes an owned campaign only when the current status allows removal.",
    "method": "DELETE",
    "baseUrl": "/api",
    "endpoint": "/campaigns/:id",
    "headers": [
        {
            "key": "Authorization",
            "value": "Bearer <token>",
            "required": true
        }
    ],
    "queryParams": [],
    "pathParams": [
        {
            "key": "id",
            "value": "66a1c2a1e8f2d5b9c4a67890",
            "required": true
        }
    ],
    "bodyType": "none",
    "requestBody": "",
    "formData": [],
    "rawBody": "",
    "responses": {
        "200": {
            "description": "Deleted",
            "body": "{\n  \"success\": true,\n  \"message\": \"Campa\u00f1a eliminada correctamente.\"\n}"
        },
        "400": {
            "description": "Invalid ID",
            "body": "{\n  \"success\": false,\n  \"message\": \"ID inv\u00e1lido.\"\n}"
        },
        "404": {
            "description": "Not found or forbidden",
            "body": "{\n  \"success\": false,\n  \"message\": \"Campa\u00f1a no encontrada o sin permisos.\"\n}"
        },
        "409": {
            "description": "Status not allowed",
            "body": "{\n  \"success\": false,\n  \"message\": \"Solo se pueden eliminar campa\u00f1as en estado draft o cancelled.\"\n}"
        },
        "500": {
            "description": "Server error",
            "body": "{\n  \"success\": false,\n  \"message\": \"No se pudo eliminar la campa\u00f1a.\"\n}"
        }
    }
}

Workflows

La secuencia general es la misma para todas las rutas. Cambian la validación y la operación de Mongoose.

sequenceDiagram
    participant Client
    participant Routes as campaigns routes
    participant Auth as requireAuth
    participant Validator as validateCampaignPayload
    participant Model as Campaign

    Client->>Routes: HTTP request
    Routes->>Auth: authenticate request
    Auth-->>Routes: req auth user id

    alt GET list or detail or delete
        Routes->>Model: query owned campaigns
        Model-->>Routes: documents
    else POST create
        Routes->>Validator: validate payload
        Validator-->>Routes: valid or errors
        Routes->>Model: create campaign
        Model-->>Routes: campaign
    else PATCH update
        Routes->>Validator: validate partial payload
        Validator-->>Routes: valid or errors
        Routes->>Model: update owned campaign
        Model-->>Routes: campaign
    else PATCH status
        Routes->>Model: update status
        Model-->>Routes: campaign
    end

    Routes-->>Client: JSON response

Gotchas & edge cases

[!WARNING] GET /api/campaigns no falla si listingId es inválido en la consulta. Solo ignora ese filtro.

[!WARNING] limit tiene tope de 50. Valores mayores se reemplazan por 10.

[!NOTE] pages se calcula con Math.ceil(total / limit). Si no hay resultados, puede devolver 0.

[!CAUTION] PATCH /api/campaigns/:id/status solo acepta draft, paused y cancelled. Cualquier otro valor devuelve 400.

[!CAUTION] DELETE /api/campaigns/:id solo funciona en draft o cancelled. Otros estados devuelven 409.

[!IMPORTANT] PATCH /api/campaigns/:id borra createdBy, paymentOrderId y metrics del cuerpo. Esos campos no se actualizan desde el cliente.

Este archivo se conecta con piezas concretas del backend de campañas y autenticación.

  • services/api/src/modules/campaigns/campaigns.validators.js — valida los datos de entrada.
  • services/api/src/modules/auth/auth.middleware.js — protege las rutas y aporta req.auth.userId.
  • services/api/src/models/Campaign.js — persiste y consulta campañas en MongoDB.
  • mongoose — valida ObjectId, ejecuta consultas y aplica populate.
  • express — crea el router y registra los handlers HTTP.