A RESTful sales management platform built with .NET 10 and Domain-Driven Design. Handles multi-branch product inventory, shopping carts, the full sales lifecycle (including partial item cancellation), and publishes domain events to RabbitMQ for downstream consumers.
- Architecture
- Tech Stack
- Project Structure
- Domain Model
- API Endpoints
- Event-Driven Integration
- Database
- Running with Docker
- Running Locally
- Testing
- Roadmap
The solution is split into four projects following DDD layering:
┌─────────────────────────────────────────────┐
│ 123vendas.Api │ Presentation layer — controllers,
│ │ Swagger, middleware, API versioning
├─────────────────────────────────────────────┤
│ 123vendas.Application │ Application services, MediatR handlers,
│ │ JWT, manual mapping, FluentValidation
├─────────────────────────────────────────────┤
│ 123vendas.Infrastructure │ EF Core, PostgreSQL, RabbitMQ,
│ │ repository implementations, seeders
├─────────────────────────────────────────────┤
│ 123vendas.Domain │ Entities, enums, interfaces,
│ │ validators, domain exceptions
└─────────────────────────────────────────────┘
Dependencies flow inward: Api → Application → Domain ← Infrastructure. The Domain layer has zero external dependencies — it defines interfaces that Infrastructure implements.
- CQRS (partial): Users and Auth operations go through MediatR commands/queries. The heavier CRUD services (Branches, Products, Sales, Carts) use a service-based pattern for richer business logic.
- CancellationToken everywhere: Every async method — from controllers down to EF Core queries — accepts and propagates a
CancellationToken. - Soft delete: All entities inherit from
BaseEntitywith anIsDeletedflag. No records are physically removed. - Pagination + dynamic ordering: List endpoints accept
page,size, andorderByClauseparameters. Ordering is applied dynamically viaIQueryableextensions. - Primary constructors: Used across services, handlers, and repositories for cleaner dependency injection.
| Category | Technology |
|---|---|
| Runtime | .NET 10 |
| ORM | Entity Framework Core 10 + Npgsql |
| Database | PostgreSQL 17 |
| Message Broker | RabbitMQ 4 |
| Authentication | JWT Bearer + BCrypt |
| Validation | FluentValidation |
| Mapping | Manual (extension methods) |
| Mediation | MediatR |
| Logging | Serilog |
| API Versioning | Asp.Versioning.Mvc |
| Documentation | Swashbuckle (Swagger) |
| Category | Technology |
|---|---|
| Framework | xUnit |
| Assertions | FluentAssertions + Shouldly |
| Mocking | NSubstitute |
| Fake Data | Bogus |
| Integration | WebApplicationFactory + Testcontainers (PostgreSQL, RabbitMQ) |
123sales-api/
├── src/
│ ├── 123vendas.Api/ # Controllers, middleware, Swagger config
│ ├── 123vendas.Application/ # Services, handlers, JWT, manual mappers
│ ├── 123vendas.Domain/ # Entities, enums, interfaces, validators
│ └── 123vendas.Infrastructure/ # DbContext, repositories, RabbitMQ, seeders
├── tests/
│ ├── 123vendas.Unit/ # Service and handler unit tests
│ └── 123vendas.Integration/ # Integration tests with Testcontainers
├── docker-compose.yml
├── Dockerfile
└── 123vendas-server.slnx
| Entity | Description |
|---|---|
Branch |
Physical store location with name, address, phone |
Product |
Catalog product with title, category, price, rating |
BranchProduct |
Per-branch inventory: stock quantity, localized pricing |
Cart |
Shopping cart tied to a user |
CartProduct |
Items inside a cart with quantity |
Sale |
Transaction with status (Created/Canceled), total amount |
SaleItem |
Individual line item with sequence, unit price, discount, cancellation |
User |
Account with email, role (Customer/Manager/Admin), status |
All entities share Id, IsDeleted, CreatedAt, and UpdatedAt from BaseEntity.
- Quantity limits: Max 20 identical items per sale
- Automatic discounts: 4+ items → 10%, 9+ → 15%, 20+ → 20%
- Stock validation: Sale rejected if branch stock is insufficient
- Partial cancellation: Individual sale items can be cancelled without voiding the sale
- Product sync: Updating a product title/category propagates to all
BranchProductentries viaExecuteUpdateAsync
All endpoints are versioned under /v1/. Protected routes require JWT with ManagerOnly policy.
| Resource | Method | Route | Auth |
|---|---|---|---|
| Auth | POST | /v1/api/auth |
Public |
| Branches | GET | /v1/api/branches |
Public |
| Branches | GET | /v1/api/branches/{id} |
Public |
| Branches | POST | /v1/api/branches |
Manager |
| Branches | PUT | /v1/api/branches/{id} |
Manager |
| Branches | DELETE | /v1/api/branches/{id} |
Manager |
| BranchProducts | GET | /v1/api/branch-products |
Public |
| BranchProducts | GET | /v1/api/branch-products/{id} |
Public |
| BranchProducts | POST | /v1/api/branch-products |
Manager |
| BranchProducts | PUT | /v1/api/branch-products/{id} |
Manager |
| BranchProducts | DELETE | /v1/api/branch-products/{id} |
Manager |
| Products | GET | /v1/api/products |
Public |
| Products | GET | /v1/api/products/categories |
Public |
| Products | GET | /v1/api/products/{id} |
Public |
| Products | GET | /v1/api/products/category/{category} |
Public |
| Products | POST | /v1/api/products |
Manager |
| Products | PUT | /v1/api/products/{id} |
Manager |
| Products | DELETE | /v1/api/products/{id} |
Manager |
| Carts | GET | /v1/api/carts |
Public |
| Carts | GET | /v1/api/carts/{id} |
Public |
| Carts | POST | /v1/api/carts |
Public |
| Carts | PUT | /v1/api/carts/{id} |
Public |
| Carts | DELETE | /v1/api/carts/{id} |
Public |
| Sales | GET | /v1/api/sales |
Public |
| Sales | GET | /v1/api/sales/{id} |
Public |
| Sales | POST | /v1/api/sales |
Manager |
| Sales | PUT | /v1/api/sales/{id} |
Manager |
| Sales | DELETE | /v1/api/sales/{id} |
Manager |
| Sales | PUT | /v1/api/sales/{id}/cancel |
Public |
| Sales | GET | /v1/api/sales/{id}/items/{sequence} |
Public |
| Sales | PUT | /v1/api/sales/{id}/items/{sequence}/cancel |
Public |
| Users | GET | /v1/api/users/{id} |
Public |
| Users | POST | /v1/api/users |
Public |
| Users | DELETE | /v1/api/users/{id} |
Manager |
Sales events are published to a RabbitMQ direct exchange (ex_sale) so downstream services can react independently:
| Event | Routing Key | Trigger |
|---|---|---|
SaleCreatedEvent |
SaleCreatedEvent |
New sale registered |
SaleUpdatedEvent |
SaleUpdatedEvent |
Existing sale modified |
SaleCancelledEvent |
SaleCancelledEvent |
Sale fully cancelled |
SaleItemCancelledEvent |
SaleItemCancelledEvent |
Single item cancelled |
SaleCreatedEvent
{
"Id": 1,
"Date": "2024-10-24T14:00:00Z"
}SaleUpdatedEvent
{
"Id": 1,
"UpdatedAt": "2024-10-24T16:00:00Z"
}SaleCancelledEvent
{
"Id": 1,
"CancelledAt": "2024-10-24T15:30:00Z"
}SaleItemCancelledEvent
{
"SaleId": 1,
"SaleItemId": 2,
"Sequence": 1,
"CancelledAt": "2024-10-24T15:00:00Z"
}Consumers can bind their own queues to specific routing keys, picking up only the events they care about.
PostgreSQL with EF Core. Schema is auto-created on startup via Database.EnsureCreated(). Entity configurations are modular, each implementing IEntityTypeConfiguration<T>.
public PostgreDbContext(DbContextOptions<PostgreDbContext> options) : base(options)
{
base.Database.EnsureCreated();
}A seed system populates initial data (users, branches, products, carts, sales) when SEED_DATABASE_FLAG=true. Each seeder checks for existing records before inserting.
Zero config. One command. The docker-compose.yml orchestrates PostgreSQL, RabbitMQ, and the API with health checks, dependency ordering, and database seeding — all pre-configured and ready to go.
docker compose up --buildThe API image uses a multi-stage Dockerfile: the SDK builds the project in the first stage, and only the runtime + published binaries make it into the final image (no SDK bloat).
- PostgreSQL 17 spins up with a
vendasdatabase and a persistent volume - RabbitMQ 4 starts with management UI enabled
- Both services pass their health checks before the API starts
- API builds via multi-stage Dockerfile (SDK → runtime-only image)
- Database auto-creates tables via
EnsureCreated()and seeds initial data (users, branches, products, carts, sales) - API health check kicks in after a 15s grace period
| Service | URL | Credentials |
|---|---|---|
| Swagger UI | http://localhost:8080/swagger | — |
| Health Check | http://localhost:8080/health | — |
| RabbitMQ Management | http://localhost:15672 | guest / guest |
| PostgreSQL | localhost:5432 | postgres / postgres |
All services have
restart: unless-stopped, so they survive machine reboots.
The database seed creates two users for testing the API:
| Role | Password | Access | |
|---|---|---|---|
| Admin | admin@123vendas.com |
admin123 |
Full access (all endpoints) |
| Manager | vendedor@123vendas.com |
seller123 |
Manager-only endpoints (POST/PUT/DELETE on protected resources) |
Use the
POST /v1/api/authendpoint with one of the credentials above to obtain a JWT token. Include it in theAuthorizationheader as a Bearer token for protected endpoints.
You'll need PostgreSQL and RabbitMQ running on your machine. Configure the connection details in src/123vendas.Api/Properties/launchSettings.json:
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"JWT_SECRETKEY": "dR8!v9Kp@zL3xWq#N5gT7mYb$FcJ2sV0",
"POSTGRES_CONNECTION_STRING": "Host=localhost;Port=5432;Database=vendas;Username=postgres;Password=postgres",
"RABBITMQ_HOSTNAME": "localhost",
"RABBITMQ_USERNAME": "guest",
"RABBITMQ_VIRTUALHOST": "/",
"RABBITMQ_PASSWORD": "guest",
"SEED_DATABASE_FLAG": "true",
"LOG_LEVEL": "Information"
}Then:
git clone https://github.com/LucasFernandes0101/123sales-api.git
cd 123sales-api
dotnet restore
dotnet run --project src/123vendas.Api| Variable | Description |
|---|---|
ASPNETCORE_ENVIRONMENT |
Runtime environment (Development, Staging, Production) |
JWT_SECRETKEY |
Symmetric key for signing JWT tokens |
POSTGRES_CONNECTION_STRING |
Npgsql connection string |
RABBITMQ_HOSTNAME |
RabbitMQ broker hostname |
RABBITMQ_USERNAME |
RabbitMQ auth username |
RABBITMQ_VIRTUALHOST |
RabbitMQ virtual host |
RABBITMQ_PASSWORD |
RabbitMQ auth password |
SEED_DATABASE_FLAG |
Enables database seeding on startup |
LOG_LEVEL |
Minimum Serilog log level (Information, Warning, Error, Debug) |
# Run all tests
dotnet test 123vendas-server.slnx
# Run only unit tests
dotnet test 123vendas-server.slnx --filter "Category=Unit"
# Run only integration tests
dotnet test 123vendas-server.slnx --filter "Category=Integration"
# Run with coverage
dotnet test 123vendas-server.slnx --collect:"XPlat Code Coverage"Service layer logic (Branch, BranchProduct, Cart, Product, Sale) and MediatR handlers (Auth, Users). Mocks by NSubstitute, fake data by Bogus, assertions with FluentAssertions and Shouldly.
End-to-end tests against real PostgreSQL and RabbitMQ instances spun up via Testcontainers. Each test class inherits from BaseIntegrationTest, which automatically resets the database before every test method via IAsyncLifetime. JWT authentication is handled by AuthHelper, and HTTP clients are configured with extension methods for token management.
- 149 tests covering all controllers (Auth, Users, Branches, BranchProducts, Products, Carts, Sales)
- Containers:
postgres:16-alpine,rabbitmq:3-management-alpine - Database isolation:
ResetDatabaseAsync()runs before each test viaBaseIntegrationTest.InitializeAsync
- Move secrets to a key vault (AWS Secrets Manager / Azure Key Vault)
- Add integration tests with Testcontainers
- Implement a RabbitMQ consumer service as an event-driven example
- Add OpenTelemetry tracing across services
- Introduce a CI/CD pipeline with automated test + build gates
