Saltar a contenido

Referencia: convenciones de código

Esta página es una referencia normativa: enumera las reglas que todo código nuevo del backend debe cumplir. Su fuente es la sección "Convenciones y buenas prácticas (post-migración)" de backend/AGENTS.md (heredada por backend/CLAUDE.md).

Términos

  • Use case: clase de la capa application/ que ejecuta una operación de negocio.
  • Repo (repositorio): adaptador de persistencia de un agregado.
  • VO (value object): objeto de valor inmutable del dominio (p. ej. Money).
  • DTO: objeto de transferencia de datos entre capas.
  • CC: complejidad ciclomática.

Estado objetivo vs. estado actual

Algunas reglas describen el estándar objetivo, aún no implementado al 100 % en todo el legacy. Son la meta a alcanzar, no una descripción del estado actual. El código nuevo debe cumplirlas siempre.

Páginas relacionadas: Arquitectura hexagonal.


Modelos ORM

  • Todos heredan de app.core.database.Base + TimestampMixin (created_at / updated_at timezone-aware, con onupdate).
  • SoftDeleteMixin (is_deleted + deleted_at) en entidades con valor de audit trail: Product, Category, Menu, Restaurant, AdminUser, Table.
  • PK siempre id: Mapped[int]. Excepción documentada: CartItem usa UUID str.
  • Los enums se almacenan como String(N) con .value, nunca el tipo Enum de SQLAlchemy (SAEnum).
  • FK siempre con nombre {entity}_id y con ondelete explícito (CASCADE / SET NULL / RESTRICT según la relación).

Enum en columna

Define el enum una sola vez y persístelo por su .value:

status: Mapped[str] = mapped_column(String(20))  # guarda ComandaStatus.DRAFT.value

Paginación

  • Un único shape canónico: PaginatedResponse[T] de app/shared/pagination.py, con los campos {items, total, page, per_page, pages, has_next, has_prev}.
  • Construir siempre con la factory create(); nunca a mano.
  • Parámetros de entrada siempre page + per_page (alias size para back-compat donde el frontend lo use).

Errores y resiliencia

  • Las excepciones de dominio heredan de app.shared.domain.exceptions.DomainException.
  • HTTPException solo en la capa interface/; nunca en domain/ ni application/.
  • Handlers globales en main.py, uno por código:
Excepción Código HTTP
NotFoundError 404
UnauthorizedError 403
ValidationError 422
AlreadyExistsError 409
OptimisticLockError 409
DomainException 400
  • Los repos capturan IntegrityError y lo re-lanzan como AlreadyExistsError.
  • Validación IA y side-effects críticos: FAIL-CLOSED — ante fallo, rechazar o escalar al manager, nunca auto-aprobar.
  • Servicios externos (IA, OAuth): timeout + retry con backoff; el dominio nunca ve errores de red crudos.

Nomenclatura

Una sola convención por operación (NAMING01 unificó las dos que el codebase había acumulado).

Use cases — método de entrada

  • SIEMPRE async def execute(self, command/query) -> DTO. Nunca __call__ ni handle.
  • Se invoca await use_case.execute(...), nunca await use_case(...).

Excepción residual

BuildOAuthUrlUseCase usa __call__ síncrono (dead code pendiente de borrar, finding NAMING01). No replicar ese patrón.

Repos — lecturas

  • find_by_id(...) para lookup por PK (nunca get_by_id).
  • get_* / list_* se reservan para lecturas especializadas con semántica propia (get_with_messages, get_or_create_by_session, get_active_by_session_token, listados paginados).

Repos — escrituras

  • create() inserta una entidad nueva.
  • update() muta una existente.
  • save() persiste create+update unificado donde hay optimistic-lock (payment, session). No fusionar create/update donde están separados a propósito.
  • delete() elimina.
  • add_*() añade hijos de un agregado (add_line, add_user_message) y no es persistencia. El verbo add puro está prohibido (NAMING01 lo migró a create).

Booleanos

  • Prefijo is_ / has_ en código nuevo (is_active, is_editable(), has_terrace).
  • Los booleanos sin prefijo que sobreviven (passed, approved, valid, ok, exists, available…) están contract/BD-locked (campo Pydantic, evento SSE o columna) y NO se renombran sin sign-off.

Type-safety

Convención normativa. mypy está cableado (mypy.ini + make typecheck), pero el gate en CI queda no-bloqueante mientras el baseline tenga errores pre-existentes. El gate duro en CI es make lint-arch + pytest tests/contract/ (CI-ARCH01).

  • X | None en código nuevo, no Optional[X] (Optional[] es legacy en retirada).
  • Colecciones SIEMPRE parametrizadas (list[Foo], dict[str, int]), nunca list / dict desnudos.
  • Sin Any en la API pública de use cases y repos (firma de execute, puertos). Los Any residuales están marcados # transitional: legacy ORM row y son deuda registrada.

Complejidad

  • CC ≤ 10 en código nuevo (COMPLEXITY01 bajó 8 hot-spots de >10 a ≤10).
  • Las funciones que orquestan extraen helpers privados con docstring; los helpers también ≤ 10.
  • Verificación: venv/bin/radon cc -s <archivo> (rank A/B esperado). Aún no es gate de CI — disciplina manual.

Dominio

  • StrEnum en vez de magic strings; match / case sobre enums. Un solo owner por enum (no duplicar CourseType / ConversationStatus).
  • Value objects frozen para Email, Money, SessionToken, QrCode, Quantity; primitivos solo en el borde.
  • State machines explícitas (patrón de comanda/domain/state_machine.py) con guards en el dominio.
  • Money siempre en céntimos (int) en BD; Money VO en el dominio.
  • datetime SIEMPRE timezone-aware (datetime.now(timezone.utc)), nunca naive.
  • Eventos de dominio se publican DESPUÉS del commit, nunca antes.
  • Agregados: los hijos se modifican solo vía la raíz (Comanda → ComandaLine, Conversation → ChatMessage).