services/api/src/modules/reconciliation/erasing-claim-reconciliation.service.js

Overview

Este módulo implementa la reconciliación interna de asignaciones MediaFileClaim que quedaron en estado "erasing". Su única transición permitida es erasing -> deleted, y solo cuando la evidencia del proveedor confirma NOT_FOUND_CONFIRMED mediante erasing-claim-evidence.js

El servicio trabaja sobre una ventana de consistencia controlada. Relee elegibilidad después de consultar al proveedor y luego aplica una escritura tipo compare-and-swap sobre MediaFileClaim. El propio archivo aclara que no borra archivos, no cambia el holder, no reanuda Requirement, y no decide por antigüedad

[!IMPORTANT] Este módulo es interno. El contexto válido solo se crea con createReconciliationContext, y lleva un Symbol privado del propio módulo

How it works

flowchart TD
    Caller[Caller] --> Context[createReconciliationContext]
    Caller --> Reconcile[reconcileErasingClaim]

    Reconcile --> AssertContext[assertContext]
    Reconcile --> ReadClaim[readClaimEvidence]
    ReadClaim --> ClaimModel[MediaFileClaim]
    ReadClaim --> ListingModel[Listing]

    Reconcile --> Eligibility[erasingClaimEligibility]
    Reconcile --> ReadReference[readReference]
    ReadReference --> ClaimModel
    ReadReference --> ListingModel
    ReadReference --> Identity[persistedFileIdentity]

    Reconcile --> ProbeLoop[observe x4]
    ProbeLoop --> Probe[probe getFile]

    Reconcile --> Classify[classifyProviderEvidence]
    Classify --> ProviderDecision[provider evidence]

    Reconcile --> ReRead[re-read eligibility]
    ReRead --> Eligibility

    Reconcile --> Cas[MediaFileClaim updateOne]
    Cas --> Tombstone[set state deleted]

El flujo empieza validando el contexto interno y el contrato probe.getFile. Después lee la evidencia local, comprueba elegibilidad y obtiene una referencia válida si existe. A continuación consulta al proveedor cuatro veces, clasifica la evidencia y, solo si la respuesta es concluyente y sigue siendo elegible, ejecuta la lapida final con updateOne

Key components

{
    "title": "erasing-claim-reconciliation.service.js",
    "content": "Servicio interno que reconcilia asignaciones MediaFileClaim en estado erasing y solo permite la transici\u00f3n erasing a deleted cuando la evidencia externa confirma ausencia del archivo.",
    "type": "function",
    "filePath": "services/api/src/modules/reconciliation/erasing-claim-reconciliation.service.js",
    "badges": [
        "public"
    ]
}
{
    "title": "createReconciliationContext",
    "content": "Crea un contexto interno marcado con un Symbol privado. Valida operatorRef, reason y runId antes de congelar el objeto.",
    "type": "function",
    "filePath": "services/api/src/modules/reconciliation/erasing-claim-reconciliation.service.js",
    "badges": [
        "public"
    ]
}
{
    "title": "RECONCILIATION_REASONS",
    "content": "Lista congelada de motivos permitidos para crear el contexto de reconciliaci\u00f3n. En este archivo solo admite E63_ERASING_RECONCILIATION.",
    "type": "enum",
    "filePath": "services/api/src/modules/reconciliation/erasing-claim-reconciliation.service.js",
    "badges": [
        "public",
        "read-only"
    ]
}
{
    "title": "reconcileErasingClaim",
    "content": "Reconciliaci\u00f3n principal. Lee evidencia local, consulta el proveedor, reeval\u00faa condiciones y aplica una escritura compare-and-swap sobre MediaFileClaim.",
    "type": "function",
    "filePath": "services/api/src/modules/reconciliation/erasing-claim-reconciliation.service.js",
    "badges": [
        "public"
    ]
}

Data & fields

Contexto de reconciliación

[
    {
        "name": "operatorRef",
        "type": "string",
        "required": true,
        "description": "Referencia del operador que ejecuta la reconciliación. Debe coincidir con el patrón SAFE_REF."
    },
    {
        "name": "reason",
        "type": "string",
        "required": true,
        "description": "Motivo de reconciliación. Debe estar incluido en RECONCILIATION_REASONS."
    },
    {
        "name": "runId",
        "type": "string",
        "required": true,
        "description": "Identificador de ejecución. Debe coincidir con el patrón SAFE_REF."
    }
]

El contexto resultante contiene un Symbol privado llamado CONTEXT_BRAND, más operatorRef, reason y runId. El archivo lo congela con Object.freeze y lo usa como marca interna de integridad

Entrada de reconcileErasingClaim

[
    {
        "name": "fileId",
        "type": "string",
        "required": true,
        "description": "Identificador del archivo cuya asignación en erasing se quiere reconciliar."
    },
    {
        "name": "expected",
        "type": "object",
        "required": true,
        "description": "Estado esperado para la compare-and-swap.",
        "children": [
            {
                "name": "listingId",
                "type": "string",
                "required": true,
                "description": "Id del Listing asociado en la comprobación."
            },
            {
                "name": "holder",
                "type": "string",
                "required": true,
                "description": "Id del holder esperado en la comprobación."
            },
            {
                "name": "updatedAt",
                "type": "Date | string",
                "required": true,
                "description": "Marca temporal esperada para detectar cambios intermedios."
            }
        ]
    },
    {
        "name": "referenceFileId",
        "type": "string",
        "required": true,
        "description": "Archivo de referencia activo usado para validar identidad persistida."
    },
    {
        "name": "context",
        "type": "object",
        "required": true,
        "description": "Contexto interno creado por createReconciliationContext."
    },
    {
        "name": "probe",
        "type": "object",
        "required": true,
        "description": "Adaptador que expone getFile(fileId)."
    },
    {
        "name": "hooks",
        "type": "object",
        "description": "Gancho opcional solo para pruebas. Permite ejecutar callbacks en puntos concretos del flujo."
    }
]

Lectura local de evidencia

El módulo usa MediaFileClaim.findById(fileId).lean() para leer la asignación actual. Si existe, también carga el Listing relacionado con un conjunto concreto de campos: _id, intent, createdBy, deletion.status, media.fileId, media.provider, media.filePath y sourceDocuments.fileId

Además, cuenta referencias externas en otros Listing mediante estos campos:

  • media.fileId
  • sourceDocuments.fileId

Eso alimenta foreignReferences, que forma parte de la evaluación de elegibilidad

Evidencia del archivo de referencia

readReference(referenceFileId) solo acepta una asignación activa. Busca la MediaFileClaim, exige state === "active", carga su Listing y comprueba la identidad persistida con persistedFileIdentity

Si la identidad no está presente, devuelve null. Si existe, devuelve:

  • fileId
  • filePath

Observaciones del proveedor

observe(probe, subject, fileId) llama probe.getFile(fileId) y envuelve el resultado con subject. Si el proveedor lanza una excepción, el módulo la transforma en una observación con:

  • kind: "error"
  • status: null
  • errorClass: "PROBE_THREW"

Behaviors

  • createReconciliationContext — valida operatorRef, reason y runId, y devuelve un contexto congelado con una marca privada.
  • assertContext — verifica que el contexto lleve la marca interna del módulo.
  • step — ejecuta hooks de prueba si existen.
  • errorCode — normaliza el código de error a una cadena truncada de 64 caracteres.
  • readClaimEvidence — lee la MediaFileClaim, su Listing asociado y el número de referencias externas.
  • readReference — valida una referencia activa y extrae su identidad persistida.
  • observe — consulta probe.getFile y convierte fallos en observaciones de error.
  • reconcileErasingClaim — coordina toda la reconciliación y devuelve un objeto con outcome, reason y, a veces, code

Outcome contract

reconcileErasingClaim puede devolver estos resultados:

  • tombstoned — la compare-and-swap modificó exactamente un documento.
  • cas_lost — la escritura no coincidió con el estado esperado.
  • exists — el proveedor confirma que el archivo sigue existiendo.
  • unknown — la evidencia del proveedor no es concluyente o es contradictoria.
  • manual_only — la evidencia local impide automatizar la reconciliación.
  • changed — las precondiciones cambiaron entre la primera lectura y la segunda.
  • not_erasing — la asignación ya no está en estado de erasing.
  • not_found — no existe la asignación.
  • pending — ocurrió un fallo en la escritura de la lapida y el módulo lo convierte en resultado controlado

Internal validation and guards

createReconciliationContext

La función rechaza valores fuera del patrón SAFE_REF, que admite caracteres alfanuméricos, punto, guion bajo y guion medio, con longitud de 1 a 64. También exige que reason pertenezca a RECONCILIATION_REASONS

assertContext

El módulo usa assertContext para impedir el uso accidental del servicio sin contexto válido. Si falta la marca interna, lanza:

reconciliation: contexto interno requerido (createReconciliationContext)

probe.getFile

Si probe falta o no expone getFile, el servicio lanza un error inmediato. El contrato mínimo es estricto y visible en el código

[!NOTE] El archivo solo usa el proveedor para leer con getFile. No contiene operaciones de borrado sobre el proveedor externo

Reconciliation flow

  1. Valida el contexto interno.
  2. Verifica que probe.getFile exista.
  3. Lee la evidencia local con readClaimEvidence.
  4. Evalúa elegibilidad con erasingClaimEligibility.
  5. Si la elegibilidad falla, devuelve ese resultado sin continuar.
  6. Busca una referencia válida con readReference.
  7. Si la referencia no existe o no es válida, devuelve unknown.
  8. Ejecuta probe.getFile cuatro veces en secuencia fija.
  9. Clasifica la evidencia con classifyProviderEvidence.
  10. Si el proveedor confirma existencia, devuelve exists.
  11. Si la evidencia no confirma ausencia, devuelve unknown.
  12. Relee la evidencia local.
  13. Si algo cambió, devuelve changed.
  14. Aplica MediaFileClaim.updateOne con la condición exacta observada.
  15. Si la escritura modifica un documento, devuelve tombstoned.
  16. Si no modifica ninguno, devuelve cas_lost
1. Validate context | The service requires a branded context from createReconciliationContext.
2. Read local evidence | It reads the claim, its listing, and foreign references.
3. Confirm eligibility | erasingClaimEligibility must return eligible before any provider call.
4. Read reference | The reference file must belong to an active claim with persisted identity.
5. Probe the provider | The module performs four ordered getFile reads.
6. Classify evidence | classifyProviderEvidence decides whether the target exists or is confirmed absent.
7. Recheck conditions | The service rereads local state after the external call.
8. Tombstone the claim | updateOne converts erasing to deleted only if the compare-and-swap still matches.

Gotchas & edge cases

[!WARNING] La reconciliación puede perder validez entre la primera lectura y la escritura final. En ese caso devuelve changed o cas_lost

[!WARNING] Si la evidencia del proveedor no confirma ausencia de forma consistente, el resultado es unknown. El módulo no fuerza la lapida en ese caso fileciteturn0file18turn0file1

[!WARNING] El archivo no borra archivos del proveedor externo. Tampoco reanuda Requirement ni cambia el holder de la asignación

[!CAUTION] La compare-and-swap incluye _id, listingId, state, holder y updatedAt. Cualquier cambio intermedio invalida la operación final

[!NOTE] hooks solo existe para pruebas. Los nombres usados por el código son reconcile:beforeProbe, reconcile:afterProbe y reconcile:beforeCas

Este módulo depende directamente de services/api/src/modules/reconciliation/erasing-claim-evidence.js para dos decisiones clave: la elegibilidad de la asignación y la clasificación de la evidencia del proveedor fileciteturn0file1turn0file17

También se apoya en los modelos Listing y MediaFileClaim. El primero aporta la vista de identidad persistida y referencias cruzadas; el segundo es la unidad principal de reconciliación y la única escritura final del módulo

La semántica observada en las pruebas del repositorio confirma que este servicio se usa para cerrar casos de erasing bloqueados por evidencia externa, sin introducir borrados adicionales ni flujos de recuperación de Requirement dentro de este archivo fileciteturn0file9turn0file11