Motor de pedidos para comercio electrónico construido en .NET 10 con Clean Architecture, DDD táctico y CQRS en el lado de lectura. Incluye un panel de cliente propio servido por la misma aplicación, sin proceso de compilación ni dependencias de Node.
Demo interactiva online: Puedes probar el panel directamente en order.naindev.com sin necesidad de clonar ni ejecutar nada localmente.
Un único dotnet run levanta la API, la documentación y la interfaz en http://localhost:8170.
El objetivo del repositorio no es que confíes en lo que afirma, sino que puedas comprobarlo: las reglas críticas están respaldadas por pruebas que las ejercitan, y cada decisión de diseño tiene su ADR con las alternativas que se descartaron y por qué.
Ninguna de estas afirmaciones se sostiene sola. Cada una tiene detrás una prueba que puedes ejecutar:
| Afirmación | Cómo se demuestra |
|---|---|
| La arquitectura limpia está blindada por código | CleanArchitectureTests (NetArchTest) comprueba por reflexión que Domain tiene 0 dependencias y Application no conoce Infrastructure ni API |
| El stock no se puede sobrevender, ni siquiera con peticiones simultáneas | ConcurrencyTests lanza 16 reservas a la vez contra 6 unidades y comprueba que lo concedido más lo restante cuadra exactamente |
| Las modificaciones simultáneas al mismo pedido no corrompen los totales | OrderAggregateConcurrencyProbe demuestra que dos contextos simultáneos mutando líneas del mismo pedido activan concurrencia optimista y protegen el total |
| El stock nunca queda negativo con ninguna secuencia de operaciones | DomainInvariantProperties genera cientos de casos por ejecución buscando el contraejemplo |
| Una reserva rechazada no toca el inventario | Los tests de error comparan el stock antes y después del rechazo |
| Cancelar un pedido devuelve cada unidad al catálogo | Cancelling_an_order_puts_the_reserved_stock_back |
| Un fallo de negocio es 4xx y uno técnico sigue siendo 500 sanitizado | GlobalExceptionHandlerTests y tests de integración comprueban la supresión de detalles internos fuera de desarrollo |
| El artefacto que se despliega funciona | CI construye la imagen y la interroga en marcha: arranque, panel, OpenAPI, 404 de API y usuario no root |
dotnet test NainOrder.slnx -c ReleaseLa correspondencia completa entre cada requisito y la prueba que lo cubre está en
docs/50-traceability.md, con el 100% de los requisitos cubiertos por
la suite automatizada.
El dominio gobierna un ciclo de vida de pedido con reserva de inventario real:
PendingPayment ──▶ Paid ──▶ Processing ──▶ Shipped
│ │ │
└─────────────┴────────────┴──▶ Cancelled (repone el stock reservado)
- Añadir una línea reserva stock en el mismo instante y dentro de la misma transacción.
- Cambiar la cantidad ajusta el inventario por la diferencia exacta, en ambos sentidos.
- Cancelar devuelve al catálogo todas las unidades reservadas.
- Un pedido cobrado congela su cesta; un pedido enviado es un estado final.
Las transiciones permitidas las publica el propio dominio en
GET /api/meta/order-state-machine, y el panel dibuja sus botones a partir de esa respuesta:
una regla nueva en el dominio se refleja en la interfaz sin tocar el front.
graph TD
Client["Navegador / Panel Web Propio<br/>(Vanilla JS / Sin dependencias Node)"]
API["NainOrder.Api<br/>(Controladores REST / RFC 7807 ProblemDetails)"]
AppLayer["NainOrder.Application<br/>(Casos de uso / CQRS lectura / DTOs)"]
DomainLayer["NainOrder.Domain<br/>(Agregados Order y Product / Máquina de estados)"]
InfraLayer["NainOrder.Infrastructure<br/>(EF Core / SQLite / ValueConverters)"]
DB[("SQLite Database<br/>(Persistencia atómica en céntimos enteros)")]
Client -->|HTTP / JSON| API
API --> AppLayer
AppLayer --> DomainLayer
AppLayer --> InfraLayer
InfraLayer -->|Unidad de trabajo / Transacción atómica| DB
Cuatro proyectos con las dependencias apuntando siempre hacia dentro:
| Proyecto | Responsabilidad | Depende de |
|---|---|---|
NainOrder.Domain |
Entidades, invariantes y máquina de estados | nada |
NainOrder.Application |
Casos de uso, DTOs, contratos de persistencia | Domain |
NainOrder.Infrastructure |
EF Core, SQLite, Fluent API, consultas de lectura | Application, Domain |
NainOrder.Api |
Controladores REST, contrato de errores, panel web | Application, Infrastructure |
NainOrder.Domain no referencia ningún paquete: ni EF Core, ni ASP.NET, ni inyección de
dependencias. Esa restricción es lo que mantiene el modelo de negocio verificable de forma
aislada, y es la que comprueban los tests de dominio.
sequenceDiagram
autonumber
actor Cliente as Navegador / SPA
participant API as NainOrder.Api (HTTP)
participant App as NainOrder.Application
participant Domain as NainOrder.Domain (Order Aggregate)
participant DB as SQLite (Transacción Atómica)
Cliente->>API: POST /api/orders/{id}/items (If-Match: ETag)
API->>App: AddOrderItemCommand
App->>DB: Leer Order + Product (con RowVersion / Stock)
App->>Domain: Order.AddLine(product, quantity)
alt Stock suficiente
Domain-->>App: Evento ItemAdded + StockDecrementado
App->>DB: Commit atómico con verificación de ConcurrencyToken
DB-->>App: OK (Nuevo ETag generado)
App-->>API: 200 OK + Payload con totales exactos
API-->>Cliente: Interfaz actualizada en 14ms (ETag / Céntimos enteros)
else Stock insuficiente
Domain-->>App: Excepción InsufficientStockDomainException
App-->>API: 422 UnprocessableEntity (ProblemDetails)
API-->>Cliente: Error de negocio sanitizado sin tocar stock
end
El dinero se persiste en céntimos enteros. SQLite no tiene tipo decimal: EF Core lo
almacenaría como texto, con lo que ORDER BY y SUM operarían sobre cadenas y devolverían
resultados incorrectos. Un ValueConverter a INTEGER hace exactas la ordenación y la
agregación, y elimina el error de coma flotante en el redondeo de importes.
Concurrencia optimista sobre el stock. Product lleva un token de versión declarado como
IsConcurrencyToken(). Si dos peticiones simultáneas intentan reservar la última unidad, la
segunda falla en el UPDATE y recibe un 409, en lugar de sobrevender.
Concurrencia optimista sobre el agregado de pedido. Del mismo modo que el catálogo protege el
stock, Order lleva un token de versión declarado como IsConcurrencyToken(). Dos peticiones
concurrentes mutando la cesta del mismo pedido no pueden provocar un lost update ni
desincronizar los totales materializados (ADR-0007).
Unidad de trabajo explícita. Los repositorios ya no confirman por su cuenta. Añadir una línea toca dos agregados —pedido y producto— y ambos se persisten dentro de la misma transacción, o ninguno.
Los totales del pedido se materializan. Se recalculan en cada cambio de la cesta en lugar de derivarse al leer, de modo que listados y métricas agregan en base de datos sin cargar las líneas de cada pedido.
Errores como contrato, no como texto. Un único IExceptionHandler traduce cada excepción
a ProblemDetails (RFC 7807) con un código estable y legible por máquina. Un fallo de negocio
es 4xx; un fallo técnico sigue siendo 500 y queda registrado con su traza.
Vive en NainOrder.Api/wwwroot y se sirve desde el mismo origen que la API: sin CORS, sin
npm install y sin paso de compilación en el despliegue. Está escrito con módulos ES nativos
y CSS con variables, sin ninguna librería de terceros.
- Panel — ingresos confirmados y pendientes, serie temporal, distribución por estado y ranking de productos, todo agregado en base de datos.
- Catálogo — alta y edición de referencias, ajuste de stock y añadido al pedido en curso.
- Pedidos — listado paginado con filtros de servidor y panel de detalle con el ciclo de vida, la cesta editable y las transiciones que el dominio permite en ese momento.
- Arquitectura — las capas, la máquina de estados leída de la API y una consola con la traza en vivo de cada petición que hace la interfaz, con su latencia real.
Tema claro y oscuro, diseño adaptable, paleta de comandos (Ctrl + K), navegación por
teclado, contraste AA y respeto por prefers-reduced-motion.
No hace falta instalar ningún servidor de base de datos: SQLite genera el fichero al arrancar y la aplicación aplica migraciones y siembra el catálogo de demostración por sí sola.
git clone https://github.com/Nain9Dev/NainOrder.git
cd NainOrder/NainOrder.Api
dotnet run| Ruta | Contenido |
|---|---|
/ |
Panel de cliente |
/swagger |
Documentación interactiva de la API |
/health |
Comprobación de estado |
dotnet test NainOrder.slnx -c Release52 pruebas repartidas en cuatro niveles:
- Ejemplo — la máquina de estados, incluidas todas las transiciones que debe impedir.
- Propiedad (FsCheck) — las invariantes aritméticas se comprueban contra cientos de casos generados: el stock nunca queda negativo, una reserva rechazada no altera el inventario, el total del pedido siempre coincide con la suma de sus líneas y los tokens de versión avanzan.
- Concurrencia multicontexto — probes de carreras simultáneas demostrando el bloqueo y la protección contra lost updates del motor EF Core sobre los agregados.
- Integración — la aplicación real, con sus migraciones y su middleware, atacada por HTTP: flujo completo de compra, caminos de error, paginación, sanitización de 500 y entrega del panel y de Swagger.
docker build -t nainorder . && docker run -p 8080:8080 nainorderLa imagen se ejecuta con un usuario sin privilegios y escribe la base de datos en /app/data.
| Método | Ruta | Descripción |
|---|---|---|
GET |
/api/products |
Catálogo, con filtro por texto y categoría |
POST |
/api/products |
Alta de producto (SKU único) |
PUT |
/api/products/{id} |
Actualiza nombre, precio, categoría y descripción |
POST |
/api/products/{id}/stock |
Ajusta el stock por un delta con signo |
GET |
/api/orders |
Listado paginado con filtros de estado, cliente y referencia |
POST |
/api/orders |
Abre un pedido en PendingPayment |
POST |
/api/orders/{id}/items |
Añade línea y reserva stock |
PUT |
/api/orders/{id}/items/{productId} |
Cambia la cantidad ajustando el inventario |
DELETE |
/api/orders/{id}/items/{productId} |
Quita la línea y repone stock |
POST |
/api/orders/{id}/{pay|process|ship|cancel} |
Transiciones de estado |
GET |
/api/dashboard/stats |
Métricas agregadas del negocio |
GET |
/api/meta/order-state-machine |
Máquina de estados publicada por el dominio |
POST |
/api/demo/simulate-purchase |
Escenario guiado con traza paso a paso |
La API pública está limitada a 120 peticiones por minuto y dirección IP.
El repositorio sigue un flujo dirigido por especificación. La verdad estable del producto vive en
docs/:
| Documento | Contenido |
|---|---|
00-charter.md |
Objetivo, alcance y no-objetivos (lo que falta, falta a propósito) |
10-requirements.md |
Requisitos REQ-### en notación EARS |
20-architecture.md |
Capas y recorrido de una petición, con diagramas Mermaid |
21-data-model.md |
Entidades, invariantes y restricciones del motor |
30-decisions/ |
Siete ADR con las alternativas descartadas y su porqué |
50-traceability.md |
Cada requisito y la prueba que lo demuestra (100% cubierto) |
60-runbook.md |
Arranque, configuración, límites conocidos y recuperación |
Las decisiones que más condicionan el diseño:
- ADR-0001 — el dinero se guarda en
céntimos enteros: SQLite no tiene
decimaly ordenar o sumar sobre texto daba resultados falsos. - ADR-0002 — concurrencia optimista sobre el stock, para que dos peticiones no puedan vender la misma unidad.
- ADR-0003 — la frontera transaccional pertenece al caso de uso, no al repositorio.
- ADR-0007 — concurrencia optimista sobre el agregado de pedido para blindar los totales materializados contra escrituras perdidas.
C# / .NET 10 · ASP.NET Core · Entity Framework Core (SQLite) · Swagger/OpenAPI · xUnit · FsCheck · JavaScript (módulos ES) · CSS con variables
Inspect 0.00 € Infrastructure Topology & Governance
In alignment with the NainDev Zero-Cost Infrastructure Philosophy, this system operates at strictly 0.00 €/month recurring cloud cost:
| Component | Architecture Mechanism | Provider / Runtime | Monthly Cost |
|---|---|---|---|
| Global Edge Delivery | Anycast CDN, HTTP/3, TLS termination & DDoS mitigation | Cloudflare Free Tier | 0.00 € |
| Client Application Shell | Zero-dependency static assets served directly | GitHub Pages / Edge CDN | 0.00 € |
| Compute Execution | Deterministic in-browser evaluation / Local container topology | Client WASM / Local Docker | 0.00 € |
| CI/CD Quality Gate | Continuous integration, NetArchTest enforcement & security scans | GitHub Actions (Linux Runner) | 0.00 € |
| Total Recurring Cost | Zero billable cloud dependencies | 0.00 € |
Desarrollado por Aitor Nain — Arquitecto backend .NET. Diseño infraestructura determinista donde la integridad del dato no es negociable.