Cierra el carril de cara al usuario: adjunta un documento desde la ficha del
embarque y lo ve aparecer con badge "En expediente". Si EFC esta caido lo ve
como "Pendiente de enviar" en vez de perder su trabajo, y el sistema lo entrega
solo cuando EFC vuelve, sin duplicarlo.
La subida es de UN paso -guarda, registra y encola- y responde 201 PASE LO QUE
PASE con EFC. La alternativa era llamar a EFC dentro del request, y ahi un EFC
caido devuelve un error al usuario con el archivo ya subido a medias.
La descarga es un proxy con streaming, no una redireccion: EFC nunca entrega una
URL de MinIO, y reescribir el host de una URL ya firmada invalida su SigV4. El
AsyncClient se crea DENTRO del generador y se cierra en finally; creado fuera,
en un `async with`, se cerraria antes de que empiece el streaming -FastAPI
consume el generador despues de devolver la respuesta- y la descarga moriria a
la mitad. Dos guardas: la pertenencia se valida ANTES de tocar EFC, y se manda
el organizacion_id del expediente para que la verificacion de EFC tambien
dispare. Sin la primera, un document_id que coincidiera leeria el expediente de
otro tenant, y el organizacion_id se deriva del expediente, asi que el CRM iria
a preguntarle a la organizacion de otro cliente.
TRES DEFECTOS PREEXISTENTES QUE ESTA FASE CORRIGE DE PASO:
1. `POST /uploads` hacia `await file.read()` COMPLETO antes de validar el
tamano: un archivo de 2 GB se bufferizaba entero en RAM del proceso solo para
responder 422 despues. Ahora la lectura es por partes y aborta al pasar del
tope.
2. No habia allowlist de extension, a diferencia del avatar y el centro de
ayuda, que si la tienen. La de aqui coincide con la de EFC: aceptar algo que
alla se rechaza guardaria el archivo y condenaria su entrega a `failed`.
3. `GET /uploads/url` validaba SOLO que la key empezara con
tenants/{tid}/companies/{cid}/, lo que permitia firmar una URL de lectura
para CUALQUIER objeto de esa company -incluidos los certificados de la FIEL
bajo certificates/- con solo el permiso de modulo crm.access. Ahora exige
ademas uno de los dos subarboles de documentos del CRM. Hay test por cada
prefijo sensible.
El borrado DESASOCIA, no destruye en EFC: el gateway de Anexo22 nunca llama al
DELETE de EFC -verificado, ese metodo no existe en su cliente- y record.Document
alla no tiene vigencia ni purga, asi que la politica implicita es conservar. Un
documento que manana puede ser parte del expediente de un pedimento real es
riesgo de retencion fiscal.
En el frontend, abrir un documento del expediente NO usa window.open sobre la
URL del proxy: esa llamada no lleva el header Authorization y el backend
responderia 401. Va por api.getBlob y URL.createObjectURL. La descarga se
ramifica en tres y el ORDEN importa: efc_document_id primero, porque al
confirmar la entrega `delete_local` borra la copia local y file_key queda en
NULL. `postFormData` solo publica `fetchApiFormDataPost`, que ya existia con la
maquinaria de progreso pero sin exportar, asi que nadie podia usarlo.
Verificacion:
backend pytest tests/ EXIT=0 178 passed 1 skipped (baseline 70)
frontend pnpm run i18n:compile EXIT=0
pnpm run check EXIT=1 38 errores/17 warnings en 20 archivos
IDENTICO al baseline: 55 hallazgos
antes y despues, cero agregados. La
unica diferencia es que el hallazgo
preexistente de RelatedManager se
movio de la linea 251 a la 317 al
insertar codigo arriba.
pnpm run test:unit EXIT=1 2 failed 43 passed (baseline 2/38);
los 2 rojos son los preexistentes de
backend.test.ts, que pide un backend
vivo en el host `backend` de docker
pnpm run build EXIT=0
El verde se vio fallar primero: reintroduciendo la fuga de `/uploads/url` los
tests de certificates/, doda/ y signatures/ se pusieron rojos; volviendo al
read() completo cayo el test que afirma que el tope corta sin leer el archivo
entero; y agregando un tipo de documento solo en el frontend, el test de paridad
lo nombro: "expected [ 'carta_poder' ] to deeply equal []".
Ticket: T2026-08-046 (fase 7 de 7)
107 lines
3.2 KiB
Python
107 lines
3.2 KiB
Python
from datetime import date, datetime
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
|
|
class ExpedienteBase(BaseModel):
|
|
service_request_id: int | None = None
|
|
account_id: int | None = None
|
|
status: str = Field("abierto", max_length=20)
|
|
|
|
|
|
class ExpedienteCreate(BaseModel):
|
|
"""Alta explícita de un expediente.
|
|
|
|
No lleva ``folio``: lo asigna el servidor con el contador de ``folio.py``. Aceptarlo del cliente
|
|
permitiría pisar el consecutivo de otro expediente.
|
|
"""
|
|
|
|
service_request_id: int | None = None
|
|
account_id: int | None = None
|
|
|
|
|
|
class ExpedienteUpdate(BaseModel):
|
|
account_id: int | None = None
|
|
status: str | None = Field(None, max_length=20)
|
|
|
|
|
|
class ExpedienteCompleteInput(BaseModel):
|
|
"""Data aduanera real con la que se completa un expediente provisional."""
|
|
|
|
patente: str = Field(..., max_length=20)
|
|
aduana: str = Field(..., max_length=10)
|
|
numero_pedimento: str = Field(..., max_length=20)
|
|
anio: int = Field(..., ge=1900, le=2999)
|
|
clave_pedimento: str | None = Field(None, max_length=10)
|
|
regimen: str | None = Field(None, max_length=10)
|
|
fecha_pago: date | None = None
|
|
rfc_importador: str | None = Field(None, max_length=20)
|
|
rfc_agente_aduanal: str | None = Field(None, max_length=100)
|
|
|
|
|
|
class ExpedienteResponse(ExpedienteBase):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
folio: str
|
|
period_year: int
|
|
period_month: int
|
|
sequence: int
|
|
|
|
efc_organizacion_id: str | None = None
|
|
efc_pedimento_id: str | None = None
|
|
efc_storage_token: str | None = None
|
|
efc_link_state: str
|
|
efc_error_code: str | None = None
|
|
efc_error_detail: str | None = None
|
|
|
|
patente: str | None = None
|
|
aduana: str | None = None
|
|
numero_pedimento: str | None = None
|
|
anio: int | None = None
|
|
clave_pedimento: str | None = None
|
|
regimen: str | None = None
|
|
fecha_pago: date | None = None
|
|
rfc_importador: str | None = None
|
|
rfc_agente_aduanal: str | None = None
|
|
|
|
created_by: str | None = None
|
|
updated_by: str | None = None
|
|
tenant_id: int
|
|
company_id: int
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class ExpedienteEnsureInput(BaseModel):
|
|
"""Entrada de ``POST /expedientes/ensure``: la solicitud a la que colgar el expediente."""
|
|
|
|
service_request_id: int
|
|
|
|
|
|
class ExpedienteDocumentResponse(BaseModel):
|
|
"""Documento de un expediente, tal como lo ve el frontend.
|
|
|
|
**No lleva ``file_key`` ni ``file_url`` a propósito.** La copia local es de tránsito y se borra
|
|
al confirmar la entrega a EFC, así que exponerla invitaría al frontend a guardarse una
|
|
referencia que va a dejar de existir. Para abrir el archivo está el proxy de descarga.
|
|
"""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
expediente_id: int | None = None
|
|
doc_type: str
|
|
name: str
|
|
content_type: str | None = None
|
|
size_bytes: int | None = None
|
|
# Lo que pinta el badge de la ficha: PENDING | SYNCED | FAILED
|
|
efc_sync_state: str | None = None
|
|
efc_document_ref: str | None = None
|
|
efc_document_id: str | None = None
|
|
efc_error_code: str | None = None
|
|
efc_attempts: int | None = None
|
|
uploaded_by: str | None = None
|
|
created_at: datetime
|
|
updated_at: datetime
|