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_attimezone-aware, cononupdate). 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:CartItemusa UUIDstr. - Los enums se almacenan como
String(N)con.value, nunca el tipoEnumde SQLAlchemy (SAEnum). - FK siempre con nombre
{entity}_idy conondeleteexplícito (CASCADE/SET NULL/RESTRICTsegún la relación).
Enum en columna
Define el enum una sola vez y persístelo por su .value:
Paginación¶
- Un único shape canónico:
PaginatedResponse[T]deapp/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(aliassizepara back-compat donde el frontend lo use).
Errores y resiliencia¶
- Las excepciones de dominio heredan de
app.shared.domain.exceptions.DomainException. HTTPExceptionsolo en la capainterface/; nunca endomain/niapplication/.- 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
IntegrityErrory lo re-lanzan comoAlreadyExistsError. - 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__nihandle. - Se invoca
await use_case.execute(...), nuncaawait 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 (nuncaget_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 verboaddpuro está prohibido (NAMING01 lo migró acreate).
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 | Noneen código nuevo, noOptional[X](Optional[]es legacy en retirada).- Colecciones SIEMPRE parametrizadas (
list[Foo],dict[str, int]), nuncalist/dictdesnudos. - Sin
Anyen la API pública de use cases y repos (firma deexecute, puertos). LosAnyresiduales están marcados# transitional: legacy ORM rowy 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¶
StrEnumen vez de magic strings;match/casesobre enums. Un solo owner por enum (no duplicarCourseType/ConversationStatus).- Value objects
frozenparaEmail,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;MoneyVO en el dominio. datetimeSIEMPRE 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).