services/api/src/modules/listings/source-documents.js

Overview

Este módulo centraliza la lógica pura para el “documento fuente del anunciante”. El archivo distingue este documento de Listing.pdf, que el sistema genera bajo demanda, y aclara que el contenido original no se extrae ni interpreta aquí. Las rutas viven en source-documents.routes.js, mientras este archivo aporta reglas de acceso, saneado, validación y serialización .

También conecta con la autorización derivada del usuario. La función canAttachSourceDocument se expone en sanitizeUser y se usa en la ruta de adjuntos para decidir si una cuenta puede subir documentos fuente fileciteturn0file5turn0file7.

[!NOTE] Este módulo no define endpoints HTTP. Solo exporta helpers que usan la capa de rutas y la serialización pública fileciteturn0file0turn0file7.

How it works

El flujo del módulo es sencillo y determinista. Primero define la política de acceso por plan y rol. Después valida que el archivo sea un PDF real. Finalmente, convierte el documento persistido a una forma pública sin exponer fileId .

flowchart TD
  User[User] --> Access[canAttachSourceDocument]
  Name[Original name] --> Clean[sanitizeSourceDocumentName]
  File[Uploaded file] --> Validate[validateSourceDocumentFile]
  Validate --> Mime[MIME application pdf]
  Validate --> Ext[Extension pdf]
  Validate --> Sig[Signature percent PDF dash]
  Doc[Stored document] --> Serialize[serializeSourceDocument]
  Docs[Document list] --> SerializeMany[serializeSourceDocuments]
  Serialize --> Public[Public data without fileId]
  SerializeMany --> PublicList[Public list]

La capa de rutas consulta canAttachSourceDocument antes de aceptar una subida. Luego aplica validateSourceDocumentFile al archivo recibido por multer. Si la subida prospera, sanitizeSourceDocumentName construye el nombre visible. Después, serializeSourceDocument y serializeSourceDocuments preparan la respuesta pública para la UI fileciteturn0file0turn0file7.

Key components

Este archivo expone cuatro funciones públicas y dos constantes de política interna. Todas operan con datos simples y retornan resultados predecibles. La intención es que la capa HTTP haga el trabajo de transporte, mientras este módulo concentra la regla de negocio local .

{
    "title": "canAttachSourceDocument",
    "content": "Eval\u00faa si un usuario puede adjuntar un documento fuente. Solo devuelve true cuando accountRole es creator y el plan pertenece al conjunto permitido.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/source-documents.js",
    "badges": [
        "public"
    ]
}
{
    "title": "sanitizeSourceDocumentName",
    "content": "Construye un nombre visible seguro para mostrar en la interfaz. Conserva solo el basename, normaliza Unicode y elimina caracteres de control y separadores peligrosos.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/source-documents.js",
    "badges": [
        "public"
    ]
}
{
    "title": "validateSourceDocumentFile",
    "content": "Verifica que el archivo subido sea un PDF v\u00e1lido. Comprueba presencia de buffer, MIME declarado, extensi\u00f3n .pdf y la firma inicial %PDF-.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/source-documents.js",
    "badges": [
        "public"
    ]
}
{
    "title": "serializeSourceDocument",
    "content": "Convierte un documento persistido a la forma p\u00fablica. Expone _id, url, name, mimeType, sizeBytes y uploadedAt, pero nunca fileId.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/source-documents.js",
    "badges": [
        "public"
    ]
}
{
    "title": "serializeSourceDocuments",
    "content": "Serializa un arreglo de documentos fuente. Si la entrada no es un arreglo, devuelve una lista vac\u00eda.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/source-documents.js",
    "badges": [
        "public"
    ]
}

Data & fields

La política del archivo gira alrededor de dos constantes internas y de la forma pública del documento. SOURCE_DOCUMENT_MIME_TYPE fija el tipo admitido. SOURCE_DOCUMENT_PLANS limita la capacidad a creator y enterprise. El comentario del archivo aclara que enterprise es el plan legado migrado del plan Creador .

  • SOURCE_DOCUMENT_MIME_TYPE [badge:read-only] string — Valor fijo: application/pdf.
  • SOURCE_DOCUMENT_PLANS [badge:read-only] Set<string> — Planes permitidos: creator y enterprise.

Forma pública de serializeSourceDocument

[
    {
        "name": "_id",
        "type": "string",
        "required": true,
        "description": "Identificador público del subdocumento. Si no existe, se devuelve cadena vacía."
    },
    {
        "name": "url",
        "type": "string",
        "required": true,
        "description": "URL pública del archivo almacenado."
    },
    {
        "name": "name",
        "type": "string",
        "required": true,
        "description": "Nombre visible saneado del documento."
    },
    {
        "name": "mimeType",
        "type": "string",
        "required": true,
        "description": "Tipo MIME del documento. Si falta, usa application/pdf."
    },
    {
        "name": "sizeBytes",
        "type": "number | null",
        "description": "Tamaño en bytes, o null cuando no existe."
    },
    {
        "name": "uploadedAt",
        "type": "Date | null",
        "description": "Fecha de subida, o null cuando no existe."
    }
]

La serialización pública no expone fileId. Ese identificador se mantiene interno y no sale por la API pública de listado. La función también normaliza valores faltantes con cadenas vacías o null, según el campo .

Behaviors

Las funciones del módulo implementan comportamientos muy concretos. No dependen de estado global mutable. Por eso la capa de rutas puede combinarlas en distintos puntos del flujo sin variar sus resultados .

  • canAttachSourceDocument(user) — Devuelve false para usuarios ausentes. Solo devuelve true si accountRole === "creator" y plan es creator o enterprise.
  • sanitizeSourceDocumentName(originalName) — Extrae el basename, reemplaza barras invertidas, normaliza a NFC, elimina controles y recorta a 160 caracteres. Si queda vacío, devuelve documento.pdf.
  • validateSourceDocumentFile(file) — Devuelve un código de error o null. Rechaza archivos sin buffer, con MIME distinto, con extensión no .pdf o con una firma inicial distinta de %PDF-.
  • serializeSourceDocument(doc) — Devuelve una representación pública del documento. Nunca incluye fileId.
  • serializeSourceDocuments(docs) — Mapea el arreglo con serializeSourceDocument. Si la entrada no es un arreglo, devuelve [].

[!IMPORTANT] La validación aquí no hace parsing profundo del PDF. Solo verifica tipo declarado, extensión y firma inicial. El límite de tamaño se aplica antes, desde multer, en la capa de rutas fileciteturn0file0turn0file7.

Workflows

Este archivo no define un flujo de negocio completo por sí solo. Aun así, sus helpers encajan en un recorrido claro de subida y respuesta. La ruta de adjuntos usa primero la autorización, luego la validación, y al final la serialización pública fileciteturn0file7turn0file0.

1. Evaluar acceso | `canAttachSourceDocument` determina si la cuenta puede adjuntar un documento fuente.
2. Preparar nombre visible | `sanitizeSourceDocumentName` limpia el nombre del archivo subido.
3. Validar archivo | `validateSourceDocumentFile` comprueba que el archivo sea un PDF real.
4. Exponer respuesta pública | `serializeSourceDocument` y `serializeSourceDocuments` ocultan `fileId` y devuelven solo metadatos públicos.

Gotchas & edge cases

[!WARNING] sanitizeSourceDocumentName nunca conserva la ruta completa. Solo deja el basename final. Eso evita que la interfaz muestre separadores o segmentos de directorio .

[!CAUTION] validateSourceDocumentFile depende del buffer completo del archivo. Si no existe file.buffer, devuelve SOURCE_DOCUMENT_FILE_REQUIRED. La función no intenta reconstruir ni descargar el contenido .

[!NOTE] serializeSourceDocument siempre oculta fileId. Ese dato pertenece al almacenamiento interno y no forma parte de la forma pública .

[!WARNING] canAttachSourceDocument no se basa solo en accountRole. También exige que user.plan esté en el conjunto permitido. La UI puede mostrar la capacidad, pero la autorización real ocurre en backend fileciteturn0file5turn0file7.

services/api/src/modules/listings/source-documents.routes.js consume estas funciones para autorizar, validar, subir y serializar documentos fuente. Ese archivo también define los mensajes de error y el manejo de storage externo .

services/api/src/modules/auth/auth.service.js expone canAttachSourceDocument dentro de sanitizeUser. Así, la interfaz puede conocer la capacidad derivada del usuario sin decidir permisos por su cuenta .

El módulo se relaciona con la política de documentos del anunciante que el repositorio distingue de Listing.pdf. Esa separación está documentada en el propio archivo y en la ruta asociada fileciteturn0file0turn0file4.