WalletSolution
Documentación completa del sistema de gestión de billeteras y motor contable.
Estructura de Archivos
WalletSolution
│
├── MDA.Wallet.Domain
│ ├── Entities (Wallet.cs, BalancePocket.cs)
│ ├── ValueObjects (Money.cs)
│ ├── Interfaces (IConsumptionStrategy.cs)
│ └── Services (DefaultConsumptionStrategy.cs)
│
├── MDA.Wallet.Application
│ ├── Interfaces (IWalletRepository.cs)
│ └── UseCases (WalletService.cs)
│
├── MDA.Wallet.Infrastructure
│ └── Repositories (FakeWalletRepository.cs)
│
└── MDA.Wallet.API
├── Controllers (WalletController.cs)
└── Program.cs
│
├── MDA.Wallet.Domain
│ ├── Entities (Wallet.cs, BalancePocket.cs)
│ ├── ValueObjects (Money.cs)
│ ├── Interfaces (IConsumptionStrategy.cs)
│ └── Services (DefaultConsumptionStrategy.cs)
│
├── MDA.Wallet.Application
│ ├── Interfaces (IWalletRepository.cs)
│ └── UseCases (WalletService.cs)
│
├── MDA.Wallet.Infrastructure
│ └── Repositories (FakeWalletRepository.cs)
│
└── MDA.Wallet.API
├── Controllers (WalletController.cs)
└── Program.cs
MDA.Wallet.Domain
Money.cs
Domain/ValueObjects/Money.cs
namespace MDA.Wallet.Domain.ValueObjects;
public class Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
if (amount < 0) throw new ArgumentException("Amount cannot be negative");
if (string.IsNullOrWhiteSpace(currency)) throw new ArgumentException("Currency is required");
Amount = amount;
Currency = currency;
}
public Money Add(Money other)
{
if (Currency != other.Currency) throw new Exception("Currency mismatch");
return new Money(Amount + other.Amount, Currency);
}
public Money Subtract(Money other)
{
if (Currency != other.Currency) throw new Exception("Currency mismatch");
return new Money(Amount - other.Amount, Currency);
}
}
Value Object
Representa dinero con moneda y reglas, evitando errores al usar decimales sueltos.
BalancePocket.cs
Domain/Entities/BalancePocket.cs
using MDA.Wallet.Domain.ValueObjects;
namespace MDA.Wallet.Domain.Entities;
public class BalancePocket
{
public int PocketTypeId { get; private set; }
public Money Balance { get; private set; }
public int Priority { get; private set; }
public BalancePocket(int typeId, Money balance, int priority)
{
PocketTypeId = typeId;
Balance = balance;
Priority = priority;
}
public void Debit(Money amount)
{
if (Balance.Amount < amount.Amount)
throw new InvalidOperationException("Insufficient balance");
Balance = Balance.Subtract(amount);
}
}
Entidad
Representa un bolsillo de saldo que sabe cómo debitar su propio balance.
Wallet.cs
Domain/Entities/Wallet.cs
using MDA.Wallet.Domain.ValueObjects;
namespace MDA.Wallet.Domain.Entities;
public class Wallet
{
public Guid Id { get; private set; }
public List<BalancePocket> Pockets { get; private set; } = new();
public Wallet(Guid id) => Id = id;
public void ApplyDebits(List<(BalancePocket pocket, Money amount)> debits)
{
foreach (var d in debits)
{
d.pocket.Debit(d.amount);
}
}
}
Aggregate Root
Agrupa varios balances y coordina operaciones sobre ellos.
IConsumptionStrategy.cs
Domain/Interfaces/IConsumptionStrategy.cs
using MDA.Wallet.Domain.Entities;
using MDA.Wallet.Domain.ValueObjects;
namespace MDA.Wallet.Domain.Interfaces;
public interface IConsumptionStrategy
{
List<(BalancePocket, Money)> Resolve(Wallet wallet, Money amount);
}
Interfaz de Dominio
Define la regla para decidir de qué balances se debe consumir un monto.
DefaultConsumptionStrategy.cs
Domain/Services/DefaultConsumptionStrategy.cs
using MDA.Wallet.Domain.Entities;
using MDA.Wallet.Domain.Interfaces;
using MDA.Wallet.Domain.ValueObjects;
namespace MDA.Wallet.Domain.Services;
public class DefaultConsumptionStrategy : IConsumptionStrategy
{
public List<(BalancePocket, Money)> Resolve(Wallet wallet, Money amount)
{
var result = new List<(BalancePocket, Money)>();
decimal remaining = amount.Amount;
foreach (var pocket in wallet.Pockets.OrderBy(p => p.Priority))
{
if (remaining <= 0) break;
var toUse = Math.Min(pocket.Balance.Amount, remaining);
if (toUse > 0)
{
result.Add((pocket, new Money(toUse, amount.Currency)));
remaining -= toUse;
}
}
return result;
}
}
Domain Service
Decide consumir el dinero recorriendo los balances por prioridad.
MDA.Wallet.Application
IWalletRepository.cs
Application/Interfaces/IWalletRepository.cs
using MDA.Wallet.Domain.Entities;
namespace MDA.Wallet.Application.Interfaces;
public interface IWalletRepository
{
Task<Wallet> Get(Guid id);
Task Save(Wallet wallet);
}
Interfaz de Aplicación
Define cómo obtener y guardar una wallet sin saber dónde se almacena.
WalletService.cs
Application/UseCases/WalletService.cs
using MDA.Wallet.Application.Interfaces;
using MDA.Wallet.Domain.Interfaces;
using MDA.Wallet.Domain.ValueObjects;
namespace MDA.Wallet.Application.UseCases;
public class WalletService
{
private readonly IWalletRepository _repo;
private readonly IConsumptionStrategy _strategy;
public WalletService(IWalletRepository repo, IConsumptionStrategy strategy)
{
_repo = repo;
_strategy = strategy;
}
public async Task Debit(Guid walletId, decimal amount)
{
var wallet = await _repo.Get(walletId);
var debits = _strategy.Resolve(wallet, new Money(amount, "MXN"));
wallet.ApplyDebits(debits);
await _repo.Save(wallet);
}
}
Application Service / Use Case
Coordina el caso de uso de debitar una wallet.
MDA.Wallet.Infrastructure
FakeWalletRepository.cs
Infrastructure/Repositories/FakeWalletRepository.cs
using MDA.Wallet.Application.Interfaces;
using MDA.Wallet.Domain.Entities;
using MDA.Wallet.Domain.ValueObjects;
namespace MDA.Wallet.Infrastructure.Repositories;
public class FakeWalletRepository : IWalletRepository
{
public Task<Wallet> Get(Guid id)
{
var wallet = new Wallet(id);
wallet.Pockets.Add(new BalancePocket(1, new Money(500, "MXN"), 1));
wallet.Pockets.Add(new BalancePocket(2, new Money(300, "MXN"), 2));
return Task.FromResult(wallet);
}
public Task Save(Wallet wallet) => Task.CompletedTask;
}
Implementación
Simula una base de datos para pruebas o demos.
MDA.Wallet.API
WalletController.cs
API/Controllers/WalletController.cs
using MDA.Wallet.Application.UseCases;
using Microsoft.AspNetCore.Mvc;
namespace MDA.Wallet.API.Controllers;
[ApiController]
[Route("wallet")]
public class WalletController : ControllerBase
{
private readonly WalletService _service;
public WalletController(WalletService service) => _service = service;
[HttpPost("debit")]
public async Task<IActionResult> Debit(Guid walletId, decimal amount)
{
await _service.Debit(walletId, amount);
return Ok();
}
}
API Controller
Expone el caso de uso de débito a través de HTTP.
Program.cs
API/Program.cs
using MDA.Wallet.Application.Interfaces;
using MDA.Wallet.Application.UseCases;
using MDA.Wallet.Domain.Interfaces;
using MDA.Wallet.Domain.Services;
using MDA.Wallet.Infrastructure.Repositories;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Inyección de Dependencias
builder.Services.AddScoped<IWalletRepository, FakeWalletRepository>();
builder.Services.AddScoped<IConsumptionStrategy, DefaultConsumptionStrategy>();
builder.Services.AddScoped<WalletService>();
var app = builder.Build();
if (app.Environment.IsDevelopment()) {
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapControllers();
app.Run();
Ensamblador
Ensambla todas las piezas del sistema y arranca la aplicación.
Resumen Final por Capas
API
Controllers
Controllers
Application
Use Cases
Use Cases
Domain
Reglas / Business
Reglas / Business
Infrastructure
Implementaciones
Implementaciones