feat: comando dedup_documents para duplicados legados (T2025-09-004)

Limpia documentos duplicados (misma sub-entidad + mismo document_type) creados
ANTES del fix de reemplazo del microservicio (jun-2026). Conserva el mas reciente
con archivo valido en storage, borra el resto (archivo MinIO si no lo referencia
otra fila + fila + ajuste de cuota). --dry-run, conteo previo, idempotente; solo
docs ligados a entidad (partida/cove/edocument).

La creacion ya reemplaza desde jun-2026: verificado 0 duplicados posteriores al fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 12:26:14 -06:00
parent 2e7d78fd8b
commit b805c791dc
3 changed files with 193 additions and 0 deletions

View File

@@ -780,3 +780,52 @@ class DocumentFKResolutionTests(TestCase):
call_command("backfill_document_links", pedimento=str(self.pedimento.id), stdout=StringIO())
d_pt.refresh_from_db()
self.assertEqual(d_pt.partida_id, self.partida.id)
def _tres_copias_edoc(self):
"""3 copias del mismo edoc (type 5) con created_at d1<d2<d3 y archivos distintos."""
from django.utils import timezone
import datetime
d1 = self._doc(f"vu_ED_{self.APP}_EDOC001_aaa.pdf", 5)
d2 = self._doc(f"vu_ED_{self.APP}_EDOC001_bbb.pdf", 5)
d3 = self._doc(f"vu_ED_{self.APP}_EDOC001_ccc.pdf", 5)
base = timezone.now()
Document.objects.filter(id=d1.id).update(created_at=base - datetime.timedelta(days=2))
Document.objects.filter(id=d2.id).update(created_at=base - datetime.timedelta(days=1))
Document.objects.filter(id=d3.id).update(created_at=base)
return d1, d2, d3
def test_dedup_conserva_mas_reciente_y_es_idempotente(self):
from unittest.mock import patch
d1, d2, d3 = self._tres_copias_edoc()
self.assertEqual(Document.objects.filter(edocument=self.edoc, document_type_id=5).count(), 3)
with patch('api.customs.management.commands.dedup_documents.storage_service') as st:
st.file_exists.return_value = True
call_command('dedup_documents', pedimento=str(self.pedimento.id), stdout=StringIO())
self.assertEqual(st.delete_file.call_count, 2) # borró los 2 viejos de MinIO
restantes = list(Document.objects.filter(edocument=self.edoc, document_type_id=5))
self.assertEqual(len(restantes), 1)
self.assertEqual(restantes[0].id, d3.id) # conservó el más reciente
# idempotente: re-correr no borra nada
with patch('api.customs.management.commands.dedup_documents.storage_service') as st2:
st2.file_exists.return_value = True
call_command('dedup_documents', pedimento=str(self.pedimento.id), stdout=StringIO())
self.assertEqual(st2.delete_file.call_count, 0)
def test_dedup_dry_run_no_borra(self):
from unittest.mock import patch
self._tres_copias_edoc()
with patch('api.customs.management.commands.dedup_documents.storage_service') as st:
call_command('dedup_documents', pedimento=str(self.pedimento.id), dry_run=True, stdout=StringIO())
self.assertEqual(st.delete_file.call_count, 0)
self.assertEqual(Document.objects.filter(edocument=self.edoc, document_type_id=5).count(), 3)
def test_dedup_conserva_el_que_tiene_archivo_en_storage(self):
from unittest.mock import patch
d1, d2, d3 = self._tres_copias_edoc() # d3 el más reciente
# En storage solo existe el de d1 (el más viejo); los más nuevos no.
with patch('api.customs.management.commands.dedup_documents.storage_service') as st:
st.file_exists.side_effect = lambda nombre: nombre.endswith('_aaa.pdf')
call_command('dedup_documents', pedimento=str(self.pedimento.id), stdout=StringIO())
restantes = list(Document.objects.filter(edocument=self.edoc, document_type_id=5))
self.assertEqual(len(restantes), 1)
self.assertEqual(restantes[0].id, d1.id) # conservó el único con archivo válido