"""Pruebas del asignador de folios de expediente. El folio es lo primero que el usuario ve de un expediente y lo que comunica al cliente, así que un duplicado no es un detalle: dos expedientes con el mismo folio son dos hilos documentales que alguien va a mezclar. Estas pruebas fijan el formato, el reinicio mensual, el aislamiento por company y que un rollback deje hueco en vez de duplicar. """ from datetime import date import pytest from api.v1.modules.crm.expedientes.folio import ( format_folio, next_folio, peek_last_sequence, storage_token, ) from tests.conftest import COMPANY_ID, TENANT_ID def test_formato_del_folio(): assert format_folio(2026, 8, 1) == "EXP2026-08-001" assert format_folio(2026, 12, 42) == "EXP2026-12-042" def test_folio_de_cuatro_digitos_al_pasar_de_999(): """Al pasar de 999 el folio CRECE, no se trunca ni reinicia. Truncar cambiaría la forma de un folio ya comunicado al cliente, y reiniciar duplicaría uno anterior del mismo mes. """ assert format_folio(2026, 8, 1000) == "EXP2026-08-1000" def test_storage_token_lleva_company_y_no_parece_pedimento_real(): import re token = storage_token(1, "EXP2026-08-001") assert token == "CRM-1-EXP2026-08-001" # La llave de un pedimento real es todo dígitos con tres guiones. El provisional empieza con # letras justamente para que sea imposible que colisione. assert not re.match(r"^\d{2}-\d{2}-\d{4}-\d{7}$", token) assert len(token) <= 25 # cabe en Pedimento.pedimento_app de EFC def test_storage_token_de_dos_companies_del_mismo_tenant_no_colisiona(): """H5: tenant → organización es 1:1 pero un tenant tiene N companies. Sin el company_id dentro del token, dos companies generando EXP2026-08-001 chocarían en el unique_together de EFC y el get_or_create le devolvería a una el provisional de la otra. """ assert storage_token(1, "EXP2026-08-001") != storage_token(2, "EXP2026-08-001") def test_tres_folios_seguidos_son_consecutivos(db): on = date(2026, 8, 15) f1, *_ = next_folio(db, TENANT_ID, COMPANY_ID, on=on) f2, *_ = next_folio(db, TENANT_ID, COMPANY_ID, on=on) f3, *_ = next_folio(db, TENANT_ID, COMPANY_ID, on=on) assert [f1, f2, f3] == ["EXP2026-08-001", "EXP2026-08-002", "EXP2026-08-003"] def test_devuelve_el_folio_descompuesto(db): folio, year, month, sequence = next_folio(db, TENANT_ID, COMPANY_ID, on=date(2026, 8, 15)) assert (folio, year, month, sequence) == ("EXP2026-08-001", 2026, 8, 1) def test_el_consecutivo_reinicia_cada_mes(db): next_folio(db, TENANT_ID, COMPANY_ID, on=date(2026, 8, 15)) next_folio(db, TENANT_ID, COMPANY_ID, on=date(2026, 8, 16)) septiembre, *_ = next_folio(db, TENANT_ID, COMPANY_ID, on=date(2026, 9, 1)) assert septiembre == "EXP2026-09-001" # Y volver a agosto sigue donde se quedó: el contador es por (tenant, company, mes). agosto, *_ = next_folio(db, TENANT_ID, COMPANY_ID, on=date(2026, 8, 20)) assert agosto == "EXP2026-08-003" def test_cada_company_lleva_su_propio_consecutivo(db): on = date(2026, 8, 15) a1, *_ = next_folio(db, TENANT_ID, 1, on=on) b1, *_ = next_folio(db, TENANT_ID, 2, on=on) a2, *_ = next_folio(db, TENANT_ID, 1, on=on) assert a1 == "EXP2026-08-001" assert b1 == "EXP2026-08-001" # company 2 arranca de cero assert a2 == "EXP2026-08-002" def test_next_folio_no_commitea_y_el_rollback_deja_hueco(db): """Un rollback tiene que poder deshacer el folio, y el siguiente AVANZA igual. Es la razón de que ``next_folio`` no commitee: sus llamadores lo invocan dentro de la transacción del alta. Los huecos en la secuencia son aceptables; los duplicados no. """ on = date(2026, 8, 15) primero, *_ = next_folio(db, TENANT_ID, COMPANY_ID, on=on) assert primero == "EXP2026-08-001" db.rollback() assert peek_last_sequence(db, TENANT_ID, COMPANY_ID, on=on) == 0 segundo, *_ = next_folio(db, TENANT_ID, COMPANY_ID, on=on) db.commit() tercero, *_ = next_folio(db, TENANT_ID, COMPANY_ID, on=on) db.rollback() cuarto, *_ = next_folio(db, TENANT_ID, COMPANY_ID, on=on) # El tercero se revirtió; el cuarto vuelve a tomar ese número. Lo que importa es que NUNCA # convivan dos filas con el mismo, y de eso se encarga uq_crm_expedientes_periodo_seq. assert segundo == "EXP2026-08-001" assert cuarto == tercero def test_peek_no_mueve_el_contador(db): on = date(2026, 8, 15) assert peek_last_sequence(db, TENANT_ID, COMPANY_ID, on=on) == 0 next_folio(db, TENANT_ID, COMPANY_ID, on=on) assert peek_last_sequence(db, TENANT_ID, COMPANY_ID, on=on) == 1 assert peek_last_sequence(db, TENANT_ID, COMPANY_ID, on=on) == 1 @pytest.mark.skip( reason="Requiere PostgreSQL de verdad: la concurrencia del ON CONFLICT no se puede " "ejercitar en el SQLite en memoria de conftest, que tiene un solo escritor." ) def test_n_sesiones_concurrentes_dan_n_folios_distintos(): """N sesiones pidiendo folio a la vez → N folios distintos, sin duplicados. Es LA prueba del asignador, y solo dice algo contra PostgreSQL: el ON CONFLICT DO UPDATE serializa sobre la fila del contador y cada sesión recibe su propio valor. En SQLite en memoria con StaticPool hay un único escritor, así que pasaría por construcción y no probaría nada. Para correrla: apuntar a la base de e2e (docker-compose.e2e.yml) y abrir N sesiones reales. """