This repository contains a complete implementation of the sales challenge on top of the provided .NET 8 template.
The solution was implemented directly inside the existing repository structure, preserving the template conventions where they still made sense and tightening the areas that were incomplete or inconsistent.
The main deliverable is a sales API that supports:
- creating sales
- retrieving a sale by id
- listing sales with pagination and sorting
- updating sales
- cancelling a sale
- cancelling a sale item
- structured event logging for sale lifecycle events
The implementation models sales as an explicit aggregate, persists denormalized external identities, enforces business rules inside the domain model, and includes unit, functional, and integration tests.
The backend lives in template/backend and follows a layered structure with vertical slices inside the application and API layers:
Ambev.DeveloperEvaluation.WebApiAPI contracts, controllers, middleware, HTTP compositionAmbev.DeveloperEvaluation.Applicationuse cases, handlers, validators, mapping, event publishing abstractionAmbev.DeveloperEvaluation.Domainaggregate root, entities, value objects, domain events, repository contractsAmbev.DeveloperEvaluation.ORMEF Core context, mappings, repositories, migrations, database initializationAmbev.DeveloperEvaluation.IoCdependency wiring and infrastructure registrationstestsunit, functional, and integration test suites
- Domain contains business rules and invariants.
- Application orchestrates use cases and publishes application events.
- ORM handles persistence concerns only.
- Web API stays thin and focuses on HTTP contracts and transport behavior.
The core aggregate is:
Saleaggregate root, owns sale state, totals, cancellation state, and lifecycle rulesSaleItemchild entity inside the aggregate, owns per-line quantity, pricing, discount, totals, and item cancellation
The aggregate stores cross-domain references using denormalized snapshots:
- customer:
CustomerId,CustomerName - branch:
BranchId,BranchName - product:
ProductId,ProductName
This preserves historical consistency even if the external customer, branch, or product data changes later.
Discounts are applied per identical sale item line, never globally across the whole sale:
- quantity
1to3:0% - quantity
4to9:10% - quantity
10to20:20% - quantity
21+: invalid
Additional rules:
- a cancelled sale has zero effective total and all items cancelled
- an individually cancelled item no longer contributes to the sale total
- duplicate product lines in the same sale are rejected
- sales are modeled with cancellation semantics instead of destructive deletion
The application publishes lifecycle notifications through IEventPublisher.
Current implementation:
LoggingEventPublisherwrites structured logs
Published events:
SaleCreatedSaleModifiedSaleCancelledItemCancelled
This keeps the architecture ready for a future broker-backed implementation without changing the application handlers.
The challenge mentions CRUD, but destructive deletion is a weak fit for a sales domain. Sales were intentionally implemented with cancellation semantics instead of hard delete to preserve auditability and historical correctness.
Pricing, discount calculation, quantity limits, item cancellation, and total recomputation are all enforced inside the aggregate instead of being spread across handlers or controllers.
EF Core mappings live in the ORM project through IEntityTypeConfiguration<T>. The domain model stays free of EF-specific attributes and persistence logic.
The API initializes the database on startup, making local and containerized execution simpler for evaluation.
- C#
- .NET 8
- ASP.NET Core Web API
- Entity Framework Core
- PostgreSQL
- MediatR
- FluentValidation
- AutoMapper
- Serilog
- xUnit
- FluentAssertions
- WebApplicationFactory
- Testcontainers for PostgreSQL integration tests
- Docker Compose
template/backend
├── src
│ ├── Ambev.DeveloperEvaluation.Application
│ │ ├── Common
│ │ └── Sales
│ ├── Ambev.DeveloperEvaluation.Common
│ ├── Ambev.DeveloperEvaluation.Domain
│ │ ├── Entities/Sales
│ │ ├── Events/Sales
│ │ ├── Repositories/Sales
│ │ └── ValueObjects/Sales
│ ├── Ambev.DeveloperEvaluation.IoC
│ ├── Ambev.DeveloperEvaluation.ORM
│ │ ├── Initialization
│ │ ├── Mapping/Sales
│ │ ├── Migrations
│ │ └── Repositories/Sales
│ └── Ambev.DeveloperEvaluation.WebApi
│ └── Features/Sales
├── tests
│ ├── Ambev.DeveloperEvaluation.Unit
│ ├── Ambev.DeveloperEvaluation.Functional
│ └── Ambev.DeveloperEvaluation.Integration
├── Dockerfile
├── docker-compose.yml
└── coverage-report.sh
POST /api/salesGET /api/sales/{id}GET /api/sales?pageNumber=1&pageSize=10&sortBy=soldAt&descending=truePUT /api/sales/{id}POST /api/sales/{id}/cancelPOST /api/sales/{saleId}/items/{saleItemId}/cancel
POST /api/usersGET /api/users/{id}DELETE /api/users/{id}POST /api/auth
GET /healthGET /health/liveGET /health/ready
POST /api/sales
{
"saleNumber": "SALE-2026-0001",
"soldAt": "2026-03-27T18:30:00Z",
"customer": {
"customerId": "11111111-1111-1111-1111-111111111111",
"customerName": "John Doe"
},
"branch": {
"branchId": "22222222-2222-2222-2222-222222222222",
"branchName": "Sao Paulo Downtown"
},
"items": [
{
"productId": "33333333-3333-3333-3333-333333333333",
"productName": "Beer",
"quantity": 4,
"unitPrice": 10.0
},
{
"productId": "44444444-4444-4444-4444-444444444444",
"productName": "Soda",
"quantity": 10,
"unitPrice": 5.0
}
]
}- .NET 8 SDK
- Docker Desktop or Docker Engine available in WSL
cd template/backend
docker compose up -d db
export ConnectionStrings__DefaultConnection="Host=localhost;Port=5433;Database=developer_evaluation;Username=developer;Password=ev@luAt10n"
export Jwt__SecretKey="DeveloperEvaluationLocalSecretKeyThatIsLongEnough123456"
dotnet run --project src/Ambev.DeveloperEvaluation.WebApi/Ambev.DeveloperEvaluation.WebApi.csprojAPI base URL:
http://localhost:5089when using the default ASP.NET Core profile- or the URL printed by
dotnet run
Stop the database:
docker compose down -vcd template/backend
docker compose up -d --buildVerified endpoints:
- API:
http://localhost:8080 - database host port:
5433 - health check:
http://localhost:8080/health
Stop the stack:
docker compose down -vFrom template/backend:
dotnet test tests/Ambev.DeveloperEvaluation.Unit/Ambev.DeveloperEvaluation.Unit.csproj
dotnet test tests/Ambev.DeveloperEvaluation.Functional/Ambev.DeveloperEvaluation.Functional.csproj
dotnet test tests/Ambev.DeveloperEvaluation.Integration/Ambev.DeveloperEvaluation.Integration.csprojOr run everything:
dotnet test Ambev.DeveloperEvaluation.slnGenerate the HTML and text coverage report from template/backend:
./coverage-report.shWindows:
coverage-report.batOutputs:
- text summary:
template/backend/TestResults/CoverageReport/Summary.txt - HTML report:
template/backend/TestResults/CoverageReport/index.html
Latest verified total line coverage:
85.3%
Focused on:
- discount calculation boundaries
- quantity limit enforcement
- aggregate invariants
- sale and item cancellation behavior
- total recomputation
- supporting domain and web helper components
Focused on:
- HTTP contracts
- status codes
- request validation
- error handling
- sales flows
- user/auth sample flows
- health endpoints
Focused on:
- real application startup
- PostgreSQL persistence
- EF Core mappings and repositories
- end-to-end sales persistence behavior
- Sales use cancellation instead of hard delete.
- The original template user/auth sample was preserved and corrected where necessary, but the main evaluation focus is the sales domain.
- Event publication is intentionally log-based today, with an abstraction designed for future broker replacement.
- Generated EF migration artifacts remain in the repository and are included in the project, but the meaningful coverage target is achieved through application and domain behavior tests rather than generated code.
- The template still emits a NuGet advisory warning for
AutoMapper 13.0.1during build and test. It does not block execution, but it is worth addressing in a follow-up dependency review.