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

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
Application
Use Cases
Domain
Reglas / Business
Infrastructure
Implementaciones