Skip to content
Nain9DevPublic

About

NainOrder: High-throughput e-commerce order processing API. .NET 10 Clean Architecture with CQRS, MediatR, and EF Core over PostgreSQL.

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

NainOrder Architectural Blueprint

NainOrder

ci CodeQL Demo Online Ecosystem Status Engineering Handbook Cost Architecture Tests Open in GitHub Codespaces .NET 10 Tests Dependencias del cliente

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é.


Compruébalo tú mismo

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 Release

La 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.


Qué resuelve

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.


Arquitectura y Diagrama C4

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
Loading

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.

Flujo de Transacción y Concurrencia Optimista

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
Loading

Decisiones que merecen explicación

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.

// POST /api/orders/{id}/items  ->  409 Conflict
{
  "type": "https://httpstatuses.io/409",
  "title": "Insufficient stock",
  "status": 409,
  "detail": "Insufficient stock: 99 requested but only 3 available.",
  "code": "insufficient_stock",
  "requested": 99,
  "available": 3,
  "traceId": "0HNO2OBO3BIE3:00000001"
}

El panel de cliente

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.


Puesta en marcha

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

Tests

dotnet test NainOrder.slnx -c Release

52 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.

Contenedor

docker build -t nainorder . && docker run -p 8080:8080 nainorder

La imagen se ejecuta con un usuario sin privilegios y escribe la base de datos en /app/data.


API

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.


Documentación

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 decimal y 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.

Stack

C# / .NET 10 · ASP.NET Core · Entity Framework Core (SQLite) · Swagger/OpenAPI · xUnit · FsCheck · JavaScript (módulos ES) · CSS con variables


FinOps & Zero-Cost Architecture (0.00 €)

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.

About

NainOrder: High-throughput e-commerce order processing API. .NET 10 Clean Architecture with CQRS, MediatR, and EF Core over PostgreSQL.

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages