Implement row-level security (RLS) context management for database sessions. Refactor invoice processing and reverting tasks to utilize scoped database sessions with RLS context. Update middleware to extract and set company ID from requests. Enhance task dispatching to propagate RLS context via Celery headers. Update architecture documentation to reflect RLS implementation details.
This commit is contained in:
@@ -331,7 +331,11 @@ def digitalizar_expediente_archivo(
|
||||
"request_data": body.model_dump(),
|
||||
"company_id": company_id,
|
||||
"tenant_id": tenant_id,
|
||||
}
|
||||
},
|
||||
headers={
|
||||
"rls_tenant_id": str(int(tenant_id)),
|
||||
"rls_company_id": str(int(company_id)),
|
||||
},
|
||||
)
|
||||
|
||||
return DigitalizarResponse(
|
||||
@@ -367,7 +371,11 @@ def registrar_digitalizacion(
|
||||
"request_data": {"rfc_consulta": body.rfc_consulta},
|
||||
"company_id": company_id,
|
||||
"tenant_id": tenant_id,
|
||||
}
|
||||
},
|
||||
headers={
|
||||
"rls_tenant_id": str(int(tenant_id)),
|
||||
"rls_company_id": str(int(company_id)),
|
||||
},
|
||||
)
|
||||
launched.append({"id": record_id, "task_id": task.id})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from celery import Task
|
||||
|
||||
from core.celery_app import celery_app
|
||||
from core.database import CoreSessionLocal
|
||||
from core.database import scoped_core_db
|
||||
from core.exceptions import ValidationException
|
||||
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceStatus
|
||||
@@ -18,43 +18,40 @@ def process_export_invoice_task(self: Task, invoice_id: int, tenant_id: str, com
|
||||
Procesa una factura de exportación ejecutando todas las validaciones y
|
||||
actualizaciones del proceso principal de exportación con reporte de progreso.
|
||||
"""
|
||||
db = CoreSessionLocal()
|
||||
try:
|
||||
# ── Paso 1: Cargar factura ────────────────────────────────────────────
|
||||
_progress(self, 5, "Cargando factura...")
|
||||
invoice: InvoiceHeader | None = db.get(InvoiceHeader, invoice_id)
|
||||
|
||||
if invoice is None:
|
||||
with scoped_core_db(tenant_id=int(tenant_id), company_id=int(company_id)) as db:
|
||||
try:
|
||||
_progress(self, 5, "Cargando factura...")
|
||||
invoice: InvoiceHeader | None = db.get(InvoiceHeader, invoice_id)
|
||||
|
||||
if invoice is None:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Factura con id {invoice_id} no encontrada.",
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
_progress(self, 10, "Verificando estatus de seguridad...")
|
||||
if invoice.status == InvoiceStatus.PROCESSED:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"La factura {invoice.invoice_number} ya se encuentra procesada.",
|
||||
"errors": [{"field": "status", "message": "Factura ya procesada."}],
|
||||
}
|
||||
|
||||
_progress(self, 15, "Iniciando proceso principal de exportación...")
|
||||
result = main_process(db, invoice, tenant_id, company_id, username=username)
|
||||
|
||||
db.commit()
|
||||
_progress(self, 100, "Proceso completado.")
|
||||
return {**result, "invoice_id": invoice_id}
|
||||
|
||||
except ValidationException as exc:
|
||||
db.rollback()
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Factura con id {invoice_id} no encontrada.",
|
||||
"errors": [],
|
||||
"status": "validation_error",
|
||||
"message": exc.message,
|
||||
"errors": exc.errors,
|
||||
}
|
||||
|
||||
_progress(self, 10, "Verificando estatus de seguridad...")
|
||||
if invoice.status == InvoiceStatus.PROCESSED:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"La factura {invoice.invoice_number} ya se encuentra procesada.",
|
||||
"errors": [{"field": "status", "message": "Factura ya procesada."}],
|
||||
}
|
||||
|
||||
_progress(self, 15, "Iniciando proceso principal de exportación...")
|
||||
result = main_process(db, invoice, tenant_id, company_id, username=username)
|
||||
|
||||
db.commit()
|
||||
_progress(self, 100, "Proceso completado.")
|
||||
return {**result, "invoice_id": invoice_id}
|
||||
|
||||
except ValidationException as exc:
|
||||
db.rollback()
|
||||
return {
|
||||
"status": "validation_error",
|
||||
"message": exc.message,
|
||||
"errors": exc.errors,
|
||||
}
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
raise exc
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
raise exc
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from celery import Task
|
||||
|
||||
from core.celery_app import celery_app
|
||||
from core.database import CoreSessionLocal
|
||||
from core.database import scoped_core_db
|
||||
from core.exceptions import ErrorCollector, ValidationException
|
||||
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
@@ -26,73 +26,67 @@ def revert_invoice_task(
|
||||
validaciones y reversiones del proceso principal (revert/main_process) con
|
||||
reporte de progreso.
|
||||
"""
|
||||
db = CoreSessionLocal()
|
||||
try:
|
||||
# ── Paso 1: Cargar factura ────────────────────────────────────────────
|
||||
_progress(self, 5, "Cargando factura...")
|
||||
invoice: InvoiceHeader | None = db.get(InvoiceHeader, invoice_id)
|
||||
if invoice is None:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Factura con id {invoice_id} no encontrada.",
|
||||
"errors": [],
|
||||
}
|
||||
with scoped_core_db(tenant_id=int(tenant_id), company_id=int(company_id)) as db:
|
||||
try:
|
||||
_progress(self, 5, "Cargando factura...")
|
||||
invoice: InvoiceHeader | None = db.get(InvoiceHeader, invoice_id)
|
||||
if invoice is None:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Factura con id {invoice_id} no encontrada.",
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
_progress(self, 10, "Verificando estatus de seguridad...")
|
||||
from api.v1.modules.a76.invoices.models import InvoiceStatus
|
||||
if invoice.status != InvoiceStatus.PROCESSED:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"La factura {invoice.invoice_number} no se puede revertir porque no está procesada.",
|
||||
"errors": [{"field": "status", "message": "Factura no procesada."}],
|
||||
}
|
||||
_progress(self, 10, "Verificando estatus de seguridad...")
|
||||
from api.v1.modules.a76.invoices.models import InvoiceStatus
|
||||
if invoice.status != InvoiceStatus.PROCESSED:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"La factura {invoice.invoice_number} no se puede revertir porque no está procesada.",
|
||||
"errors": [{"field": "status", "message": "Factura no procesada."}],
|
||||
}
|
||||
|
||||
errors = ErrorCollector()
|
||||
errors = ErrorCollector()
|
||||
|
||||
# ── Paso 2: Pre-validaciones ──────────────────────────────────────────
|
||||
_progress(self, 10, "Validando estatus de la factura...")
|
||||
lines = pre_validators(db, invoice, tenant_id, company_id, errors)
|
||||
if not lines:
|
||||
errors.add_error(
|
||||
field="line_items",
|
||||
message="La factura no contiene partidas para revertir",
|
||||
solution=["Verifique que la factura tenga partidas antes de intentar revertirla"],
|
||||
code="NO_LINE_ITEMS",
|
||||
_progress(self, 10, "Validando estatus de la factura...")
|
||||
lines = pre_validators(db, invoice, tenant_id, company_id, errors)
|
||||
if not lines:
|
||||
errors.add_error(
|
||||
field="line_items",
|
||||
message="La factura no contiene partidas para revertir",
|
||||
solution=["Verifique que la factura tenga partidas antes de intentar revertirla"],
|
||||
code="NO_LINE_ITEMS",
|
||||
)
|
||||
errors.raise_if_errors()
|
||||
|
||||
_progress(self, 40, "Verificando saldos de partidas...")
|
||||
sql_errors = revert_process(
|
||||
db=db,
|
||||
invoice=invoice,
|
||||
lines=lines,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
errors=errors,
|
||||
cancelled_by=cancelled_by,
|
||||
)
|
||||
errors.raise_if_errors()
|
||||
|
||||
# ── Paso 3: Validar cantidades y ejecutar reversión ───────────────────
|
||||
_progress(self, 40, "Verificando saldos de partidas...")
|
||||
sql_errors = revert_process(
|
||||
db=db,
|
||||
invoice=invoice,
|
||||
lines=lines,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
errors=errors,
|
||||
cancelled_by=cancelled_by,
|
||||
)
|
||||
_progress(self, 95, "Anulando saldos de inventario y confirmando...")
|
||||
db.flush()
|
||||
db.commit()
|
||||
|
||||
# ── Paso 4: Confirmar transacción ─────────────────────────────────────
|
||||
_progress(self, 95, "Anulando saldos de inventario y confirmando...")
|
||||
db.flush()
|
||||
db.commit()
|
||||
return {
|
||||
"status": "success",
|
||||
"invoice_id": invoice_id,
|
||||
"sql_errors": sql_errors,
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"invoice_id": invoice_id,
|
||||
"sql_errors": sql_errors,
|
||||
}
|
||||
|
||||
except ValidationException as exc:
|
||||
db.rollback()
|
||||
return {
|
||||
"status": "validation_error",
|
||||
"message": exc.message,
|
||||
"errors": exc.errors,
|
||||
}
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
raise exc
|
||||
finally:
|
||||
db.close()
|
||||
except ValidationException as exc:
|
||||
db.rollback()
|
||||
return {
|
||||
"status": "validation_error",
|
||||
"message": exc.message,
|
||||
"errors": exc.errors,
|
||||
}
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
raise exc
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
from celery import Task
|
||||
|
||||
from core.celery_app import celery_app
|
||||
from core.database import CoreSessionLocal
|
||||
from core.database import scoped_core_db
|
||||
from core.exceptions import ValidationException
|
||||
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceStatus
|
||||
@@ -22,56 +22,49 @@ def process_invoice_task(self: Task, invoice_id: int, tenant_id: str, company_id
|
||||
Procesa una factura de importación ejecutando todas las validaciones y
|
||||
actualizaciones del proceso principal (main_process) con reporte de progreso.
|
||||
"""
|
||||
db = CoreSessionLocal()
|
||||
try:
|
||||
# ── Paso 1: Cargar factura ────────────────────────────────────────────
|
||||
# ── Paso 1: Cargar factura ────────────────────────────────────────────
|
||||
_progress(self, 5, "Cargando factura...")
|
||||
invoice: InvoiceHeader | None = db.get(InvoiceHeader, invoice_id)
|
||||
|
||||
if invoice is None:
|
||||
with scoped_core_db(tenant_id=int(tenant_id), company_id=int(company_id)) as db:
|
||||
try:
|
||||
_progress(self, 5, "Cargando factura...")
|
||||
invoice: InvoiceHeader | None = db.get(InvoiceHeader, invoice_id)
|
||||
|
||||
if invoice is None:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Factura con id {invoice_id} no encontrada.",
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
_progress(self, 10, "Verificando estatus de seguridad...")
|
||||
if invoice.status == InvoiceStatus.PROCESSED:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"La factura {invoice.invoice_number} ya se encuentra procesada.",
|
||||
"errors": [{"field": "status", "message": "Factura ya procesada."}],
|
||||
}
|
||||
|
||||
_progress(self, 20, "Iniciando procesamiento de factura...")
|
||||
result = main_process(
|
||||
db=db,
|
||||
invoice=invoice,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
username=username,
|
||||
)
|
||||
|
||||
_progress(self, 95, "Confirmando cambios...")
|
||||
db.commit()
|
||||
|
||||
_progress(self, 100, "Proceso completado.")
|
||||
return result
|
||||
|
||||
except ValidationException as exc:
|
||||
db.rollback()
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Factura con id {invoice_id} no encontrada.",
|
||||
"errors": [],
|
||||
"status": "validation_error",
|
||||
"message": exc.message,
|
||||
"errors": exc.errors,
|
||||
}
|
||||
|
||||
_progress(self, 10, "Verificando estatus de seguridad...")
|
||||
if invoice.status == InvoiceStatus.PROCESSED:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"La factura {invoice.invoice_number} ya se encuentra procesada.",
|
||||
"errors": [{"field": "status", "message": "Factura ya procesada."}],
|
||||
}
|
||||
|
||||
# ── Paso 2: Ejecutar Proceso Principal ───────────────────────────────
|
||||
# Unificamos lógica: El task solo llama al main_process centralizado.
|
||||
_progress(self, 20, "Iniciando procesamiento de factura...")
|
||||
result = main_process(
|
||||
db=db,
|
||||
invoice=invoice,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
username=username
|
||||
)
|
||||
|
||||
# ── Paso 3: Confirmar transacción ─────────────────────────────────────
|
||||
_progress(self, 95, "Confirmando cambios...")
|
||||
db.commit()
|
||||
|
||||
_progress(self, 100, "Proceso completado.")
|
||||
return result
|
||||
|
||||
except ValidationException as exc:
|
||||
db.rollback()
|
||||
return {
|
||||
"status": "validation_error",
|
||||
"message": exc.message,
|
||||
"errors": exc.errors,
|
||||
}
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
logger.error(f"Error en process_invoice_task: {str(exc)}", exc_info=True)
|
||||
raise exc
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
logger.error(f"Error en process_invoice_task: {str(exc)}", exc_info=True)
|
||||
raise exc
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from celery import Task
|
||||
|
||||
from core.celery_app import celery_app
|
||||
from core.database import CoreSessionLocal
|
||||
from core.database import scoped_core_db
|
||||
from core.exceptions import ErrorCollector, ValidationException
|
||||
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
@@ -26,72 +26,66 @@ def revert_invoice_task(
|
||||
validaciones y reversiones del proceso principal (revert/main_process) con
|
||||
reporte de progreso.
|
||||
"""
|
||||
db = CoreSessionLocal()
|
||||
try:
|
||||
# ── Paso 1: Cargar factura ────────────────────────────────────────────
|
||||
_progress(self, 5, "Cargando factura...")
|
||||
invoice: InvoiceHeader | None = db.get(InvoiceHeader, invoice_id)
|
||||
if invoice is None:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Factura con id {invoice_id} no encontrada.",
|
||||
"errors": [],
|
||||
}
|
||||
with scoped_core_db(tenant_id=int(tenant_id), company_id=int(company_id)) as db:
|
||||
try:
|
||||
_progress(self, 5, "Cargando factura...")
|
||||
invoice: InvoiceHeader | None = db.get(InvoiceHeader, invoice_id)
|
||||
if invoice is None:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Factura con id {invoice_id} no encontrada.",
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
_progress(self, 10, "Verificando estatus de seguridad...")
|
||||
from api.v1.modules.a76.invoices.models import InvoiceStatus
|
||||
if invoice.status != InvoiceStatus.PROCESSED:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"La factura {invoice.invoice_number} no se puede revertir porque no está procesada.",
|
||||
"errors": [{"field": "status", "message": "Factura no procesada."}],
|
||||
}
|
||||
_progress(self, 10, "Verificando estatus de seguridad...")
|
||||
from api.v1.modules.a76.invoices.models import InvoiceStatus
|
||||
if invoice.status != InvoiceStatus.PROCESSED:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"La factura {invoice.invoice_number} no se puede revertir porque no está procesada.",
|
||||
"errors": [{"field": "status", "message": "Factura no procesada."}],
|
||||
}
|
||||
|
||||
errors = ErrorCollector()
|
||||
errors = ErrorCollector()
|
||||
|
||||
# ── Paso 2: Pre-validaciones ──────────────────────────────────────────
|
||||
_progress(self, 10, "Validando estatus de la factura...")
|
||||
lines = pre_validators(db, invoice, tenant_id, company_id, errors)
|
||||
if not lines:
|
||||
errors.add_error(
|
||||
field="line_items",
|
||||
message="La factura no contiene partidas para revertir",
|
||||
solution=["Verifique que la factura tenga partidas antes de intentar revertirla"],
|
||||
code="NO_LINE_ITEMS",
|
||||
_progress(self, 10, "Validando estatus de la factura...")
|
||||
lines = pre_validators(db, invoice, tenant_id, company_id, errors)
|
||||
if not lines:
|
||||
errors.add_error(
|
||||
field="line_items",
|
||||
message="La factura no contiene partidas para revertir",
|
||||
solution=["Verifique que la factura tenga partidas antes de intentar revertirla"],
|
||||
code="NO_LINE_ITEMS",
|
||||
)
|
||||
errors.raise_if_errors()
|
||||
|
||||
_progress(self, 40, "Verificando saldos de partidas...")
|
||||
sql_errors = revert_process(
|
||||
db=db,
|
||||
invoice=invoice,
|
||||
lines=lines,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
errors=errors,
|
||||
)
|
||||
errors.raise_if_errors()
|
||||
|
||||
# ── Paso 3: Validar cantidades y ejecutar reversión ───────────────────
|
||||
_progress(self, 40, "Verificando saldos de partidas...")
|
||||
sql_errors = revert_process(
|
||||
db=db,
|
||||
invoice=invoice,
|
||||
lines=lines,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
errors=errors,
|
||||
)
|
||||
_progress(self, 95, "Anulando saldos de inventario y confirmando...")
|
||||
db.flush()
|
||||
db.commit()
|
||||
|
||||
# ── Paso 4: Confirmar transacción ─────────────────────────────────────
|
||||
_progress(self, 95, "Anulando saldos de inventario y confirmando...")
|
||||
db.flush()
|
||||
db.commit()
|
||||
return {
|
||||
"status": "success",
|
||||
"invoice_id": invoice_id,
|
||||
"sql_errors": sql_errors,
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"invoice_id": invoice_id,
|
||||
"sql_errors": sql_errors,
|
||||
}
|
||||
|
||||
except ValidationException as exc:
|
||||
db.rollback()
|
||||
return {
|
||||
"status": "validation_error",
|
||||
"message": exc.message,
|
||||
"errors": exc.errors,
|
||||
}
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
raise exc
|
||||
finally:
|
||||
db.close()
|
||||
except ValidationException as exc:
|
||||
db.rollback()
|
||||
return {
|
||||
"status": "validation_error",
|
||||
"message": exc.message,
|
||||
"errors": exc.errors,
|
||||
}
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
raise exc
|
||||
|
||||
@@ -3,6 +3,8 @@ from typing import Any
|
||||
from celery import Task
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.database import rls_company_var, rls_tenant_var
|
||||
|
||||
from .service import TaskTrackerService
|
||||
|
||||
|
||||
@@ -21,7 +23,25 @@ def track_and_dispatch(
|
||||
task_id: str | None = None,
|
||||
meta_payload: dict[str, Any] | None = None,
|
||||
):
|
||||
celery_task = task.apply_async(args=args or [], kwargs=kwargs or {}, task_id=task_id)
|
||||
# Propaga contexto RLS vía Celery headers (leídos en task_prerun) y
|
||||
# ContextVars (para modo eager, donde before_task_publish no dispara).
|
||||
headers = {"rls_tenant_id": str(int(tenant_id))}
|
||||
if company_id is not None:
|
||||
headers["rls_company_id"] = str(int(company_id))
|
||||
|
||||
token_t = rls_tenant_var.set(int(tenant_id))
|
||||
token_c = rls_company_var.set(int(company_id) if company_id is not None else None)
|
||||
try:
|
||||
celery_task = task.apply_async(
|
||||
args=args or [],
|
||||
kwargs=kwargs or {},
|
||||
task_id=task_id,
|
||||
headers=headers,
|
||||
)
|
||||
finally:
|
||||
rls_tenant_var.reset(token_t)
|
||||
rls_company_var.reset(token_c)
|
||||
|
||||
tracker = TaskTrackerService(db)
|
||||
tracker.register_dispatch(
|
||||
task_id=celery_task.id,
|
||||
|
||||
Reference in New Issue
Block a user