Codigo del Proyecto Wallet

Documento consolidado con los archivos solicitados, agrupados por carpeta, con una descripcion breve y el contenido completo de cada archivo para facilitar la lectura.

Arbol del Proyecto

Vista general de la estructura incluida en este documento.

C:.
|   .gitignore
|   WalletSolution.sln
|
+---MDA.Wallet.API
|   |   appsettings.Development.json
|   |   appsettings.json
|   |   MDA.Wallet.API.csproj
|   |   MDA.Wallet.API.csproj.user
|   |   MDA.Wallet.API.http
|   |   Program.cs
|   |   WeatherForecast.cs
|   |
|   +---Controllers
|   |       WalletController.cs
|   |       WeatherForecastController.cs
|   |
|   \---Properties
|           launchSettings.json
|
+---MDA.Wallet.Application
|   |   Class1.cs
|   |   MDA.Wallet.Application.csproj
|   |
|   +---DTOs
|   +---Interfaces
|   |       IWalletRepository.cs
|   |
|   \---UseCases
|           WalletService.cs
|
+---MDA.Wallet.Domain
|   |   Class1.cs
|   |   MDA.Wallet.Domain.csproj
|   |
|   +---Entities
|   |       BalancePocket.cs
|   |       Wallet.cs
|   |
|   +---Interfaces
|   |       IConsumptionStrategy.cs
|   |
|   +---Services
|   |       DefaultConsumptionStrategy.cs
|   |       ProductEarningStrategy.cs
|   |
|   \---ValueObjects
|           IEarningStrategy.cs
|           Money.cs
|           TransactionItem.cs
|
\---MDA.Wallet.Infrastructure
    |   Class1.cs
    |   MDA.Wallet.Infrastructure.csproj
    |
    +---Adapters
    +---Persistence
    \---Repositories
            FakeWalletRepository.cs

Raiz

Archivos generales de la solucion.

.gitignore

Reglas para excluir archivos temporales, compilados y configuracion local del control de versiones.

gitignore
## Archivos de usuario
*.suo
*.user
*.userosscache
*.sln.docstates
*.userprefs

## Configuración local
.vs/
.vscode/
**/Properties/launchSettings.json

## Carpetas de compilación
[Bb]in/
[Oo]bj/
[Ll]og/
[Dd]ebug/
[Rr]elease/
x64/
x86/
bld/

## Archivos temporales y de depuración
*.log
*.tmp
*.pidb
*.pdb

## Paquetes y artefactos
*.nupkg
*.snupkg
artifacts/
**/packages/

## Resultados de pruebas
[Tt]est[Rr]esult*/
TestResult.xml
nunit-*.xml

## Publicaciones y cobertura
Publish/
CodeCoverage/

## Variables de entorno
.env

WalletSolution.sln

Archivo de solucion que agrupa los proyectos de la wallet.

text
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36717.8 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MDA.Wallet.Domain", "MDA.Wallet.Domain\MDA.Wallet.Domain.csproj", "{A385E0B8-E348-446A-97AC-BD2A305107B2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MDA.Wallet.Application", "MDA.Wallet.Application\MDA.Wallet.Application.csproj", "{77917C1C-FCE2-4341-B8BB-1708B5FFFA30}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MDA.Wallet.Infrastructure", "MDA.Wallet.Infrastructure\MDA.Wallet.Infrastructure.csproj", "{84139075-7566-4115-93B8-FE2F7E0FC2E7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MDA.Wallet.API", "MDA.Wallet.API\MDA.Wallet.API.csproj", "{CDCB3758-69BD-4CDB-922E-26295160E86A}"
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU
		Release|Any CPU = Release|Any CPU
	EndGlobalSection
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
		{A385E0B8-E348-446A-97AC-BD2A305107B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{A385E0B8-E348-446A-97AC-BD2A305107B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{A385E0B8-E348-446A-97AC-BD2A305107B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{A385E0B8-E348-446A-97AC-BD2A305107B2}.Release|Any CPU.Build.0 = Release|Any CPU
		{77917C1C-FCE2-4341-B8BB-1708B5FFFA30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{77917C1C-FCE2-4341-B8BB-1708B5FFFA30}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{77917C1C-FCE2-4341-B8BB-1708B5FFFA30}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{77917C1C-FCE2-4341-B8BB-1708B5FFFA30}.Release|Any CPU.Build.0 = Release|Any CPU
		{84139075-7566-4115-93B8-FE2F7E0FC2E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{84139075-7566-4115-93B8-FE2F7E0FC2E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{84139075-7566-4115-93B8-FE2F7E0FC2E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{84139075-7566-4115-93B8-FE2F7E0FC2E7}.Release|Any CPU.Build.0 = Release|Any CPU
		{CDCB3758-69BD-4CDB-922E-26295160E86A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{CDCB3758-69BD-4CDB-922E-26295160E86A}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{CDCB3758-69BD-4CDB-922E-26295160E86A}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{CDCB3758-69BD-4CDB-922E-26295160E86A}.Release|Any CPU.Build.0 = Release|Any CPU
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE
	EndGlobalSection
	GlobalSection(ExtensibilityGlobals) = postSolution
		SolutionGuid = {50185D21-3101-4FD9-B2F0-FBDAAA73D8CD}
	EndGlobalSection
EndGlobal

MDA.Wallet.API

Capa web con configuracion, arranque y controladores HTTP.

MDA.Wallet.API/appsettings.Development.json

Configuracion de logging para ambiente de desarrollo.

json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

MDA.Wallet.API/appsettings.json

Configuracion base de la API y hosts permitidos.

json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

MDA.Wallet.API/MDA.Wallet.API.csproj

Proyecto web principal y sus dependencias.

xml
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\MDA.Wallet.Application\MDA.Wallet.Application.csproj" />
    <ProjectReference Include="..\MDA.Wallet.Infrastructure\MDA.Wallet.Infrastructure.csproj" />
  </ItemGroup>

</Project>

MDA.Wallet.API/MDA.Wallet.API.csproj.user

Preferencias locales del proyecto en Visual Studio.

xml
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <ActiveDebugProfile>IIS Express</ActiveDebugProfile>
    <Controller_SelectedScaffolderID>MvcControllerEmptyScaffolder</Controller_SelectedScaffolderID>
    <Controller_SelectedScaffolderCategoryPath>root/Common/MVC/Controller</Controller_SelectedScaffolderCategoryPath>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
    <DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
  </PropertyGroup>
</Project>

MDA.Wallet.API/MDA.Wallet.API.http

Archivo de pruebas HTTP manuales para la API.

http
@MDA.Wallet.API_HostAddress = http://localhost:5268

GET {{MDA.Wallet.API_HostAddress}}/weatherforecast/
Accept: application/json

###

MDA.Wallet.API/Program.cs

Punto de arranque de la API, registro de dependencias y logging.

csharp
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;
using MDA.Wallet.Domain.ValueObjects;
using Serilog;

var builder = WebApplication.CreateBuilder(args);

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Fatal()

    .MinimumLevel.Override("MDA", Serilog.Events.LogEventLevel.Information)

    .WriteTo.Console()
    .WriteTo.File("logs/wallet-log.txt",
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: 7,
        outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
    .CreateLogger();

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddScoped<IWalletRepository, FakeWalletRepository>();
builder.Services.AddScoped<IConsumptionStrategy, DefaultConsumptionStrategy>();
builder.Services.AddScoped<WalletService>();
builder.Services.AddScoped<IEarningStrategy, ProductEarningStrategy>();

builder.Host.UseSerilog();
builder.Services.AddControllers();

builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseSerilogRequestLogging();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

MDA.Wallet.API/WeatherForecast.cs

Modelo de ejemplo usado por el controlador de clima.

csharp
namespace MDA.Wallet.API
{
    public class WeatherForecast
    {
        public DateOnly Date { get; set; }

        public int TemperatureC { get; set; }

        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

        public string? Summary { get; set; }
    }
}

MDA.Wallet.API/Controllers/WalletController.cs

Endpoints de entrada de la API para debito y credito de wallet.

csharp
using MDA.Wallet.Application.UseCases;
using MDA.Wallet.Domain.ValueObjects;
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();
    }

    [HttpPost("credit/{walletId}")]
    public async Task Credit(Guid walletId, [FromBody] TransactionItem item)
    {
        await _service.Credit(walletId, item);
    }
}

MDA.Wallet.API/Controllers/WeatherForecastController.cs

Controlador de ejemplo generado por la plantilla de ASP.NET.

csharp
using Microsoft.AspNetCore.Mvc;

namespace MDA.Wallet.API.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet(Name = "GetWeatherForecast")]
        public IEnumerable<WeatherForecast> Get()
        {
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

MDA.Wallet.API/Properties/launchSettings.json

Perfiles locales de ejecucion para desarrollo e IIS Express.

json
{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:59145",
      "sslPort": 44364
    }
  },
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "http://localhost:5268",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "https": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "https://localhost:7246;http://localhost:5268",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "swagger",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

MDA.Wallet.Application

Capa de aplicacion que orquesta casos de uso.

MDA.Wallet.Application/Class1.cs

Clase vacia de plantilla en la capa de aplicacion.

csharp
namespace MDA.Wallet.Application
{
    public class Class1
    {

    }
}

MDA.Wallet.Application/MDA.Wallet.Application.csproj

Proyecto de la capa de aplicacion y referencias al dominio.

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\MDA.Wallet.Domain\MDA.Wallet.Domain.csproj" />
  </ItemGroup>

  <ItemGroup>
    <Folder Include="DTOs\" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.7" />
  </ItemGroup>

</Project>

MDA.Wallet.Application/Interfaces/IWalletRepository.cs

Contrato de aplicacion para obtener y guardar wallets.

csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MDA.Wallet.Domain.Entities;

namespace MDA.Wallet.Application.Interfaces
{
    public interface IWalletRepository
    {
        Task<AccountWallet> Get(Guid id);
        Task Save(AccountWallet wallet);
    }
}

MDA.Wallet.Application/UseCases/WalletService.cs

Caso de uso principal para debito, credito y coordinacion del dominio.

csharp
using MDA.Wallet.Application.Interfaces;
using MDA.Wallet.Domain.Interfaces;
using MDA.Wallet.Domain.ValueObjects;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace MDA.Wallet.Application.UseCases
{
    public class WalletService
    {
        private readonly IWalletRepository _repo;
        private readonly IConsumptionStrategy _strategy;
        private readonly IEarningStrategy _earningStrategy;

        private readonly ILogger<WalletService> _logger;

        public WalletService(IWalletRepository repo, IConsumptionStrategy strategy, IEarningStrategy earning, ILogger<WalletService> logger)
        {
            _repo = repo;
            _strategy = strategy;
            _earningStrategy = earning;
            _logger = logger;
        }

        public async Task Debit(Guid walletId, decimal amount)
        {
            _logger.LogInformation("-------------------------------------------------------------------");
            _logger.LogInformation("----------------------- Debit -------------------------------------");
            _logger.LogInformation("-------------------------------------------------------------------");
            _logger.LogInformation("Service (Debit) - START WalletService");

            _logger.LogInformation("Service (Debit) - Enviamos walletId a Infra/Fake Get() para inicio");
            var wallet = await _repo.Get(walletId);

            _logger.LogInformation("Service (Debit) - Enviamos WALLET [..] y amount [{Amount}] a Default->Resolve() para calculo", amount);
            var debits = _strategy.Resolve(wallet, new Money(amount, "MXN"));

            foreach (var (pocket, debitAmount) in debits)
            {
                _logger.LogInformation(
                    " ... Debitslist= Item1 [PocketId: {Id},  Priority: {Priority}, Amount: {Amount1}],  Item2 [Amount: {Amount}]",
                    pocket.PocketTypeId,
                    pocket.Priority,
                    pocket.Balance.Amount,
                    debitAmount.Amount
                 );
            }

            _logger.LogInformation("Service (Debit) - Enviamos Debitslist [..]  a wallet.cs para ApplyDebits()");
            wallet.ApplyDebits(debits);

            foreach (var pockets in wallet.Pockets)
            {
                _logger.LogInformation(".. WALLET [PocketId: {Id}, Priority: {Priority}, Amount: {Amount}] ", pockets.PocketTypeId, pockets.Priority, pockets.Balance.Amount);
            }

            await _repo.Save(wallet);

            _logger.LogInformation("Service (Debit) - END WalletService");
        }

        public async Task Credit(Guid walletId, TransactionItem item)
        {
            _logger.LogInformation("-------------------------------------------------------------------");
            _logger.LogInformation("----------------------- Credit ------------------------------------");
            _logger.LogInformation("-------------------------------------------------------------------");

            _logger.LogInformation("Service (Credit) - Infra/Fake init...");
            var wallet = await _repo.Get(walletId);

            _logger.LogInformation("Service (Credit) - CalculateEarning...");
            var earningAmount = _earningStrategy.CalculateEarning(item);
            _logger.LogInformation("... Abono a acreditar: {abono}", earningAmount.Amount);

            _logger.LogInformation("Service (Credit) - ApplyCredit...");
            if (earningAmount.Amount > 0)
            {
                wallet.ApplyCredit(1, earningAmount);
                foreach (var pockets in wallet.Pockets)
                {
                    _logger.LogInformation("... WALLET [PocketId: {Id}, Priority: {Priority}, Amount: {Amount}] ", pockets.PocketTypeId, pockets.Priority, pockets.Balance.Amount);
                }
                await _repo.Save(wallet);
            }
            else
            {
                _logger.LogInformation("Service (Credit) - Sin acreditacion de abono: {abono}", earningAmount.Amount);
            }
        }
    }
}

MDA.Wallet.Domain

Capa de dominio con entidades, contratos y reglas de negocio.

MDA.Wallet.Domain/Class1.cs

Clase vacia de plantilla en la capa de dominio.

csharp
namespace MDA.Wallet.Domain
{
    public class Class1
    {

    }
}

MDA.Wallet.Domain/MDA.Wallet.Domain.csproj

Proyecto del dominio y paquetes para logging.

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
    <PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
  </ItemGroup>

</Project>

MDA.Wallet.Domain/Entities/BalancePocket.cs

Entidad de bolsillo con operaciones de debito y credito.

csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

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);
    }

    public void Credit(Money amount)
    {
        if (amount.Amount <= 0) throw new ArgumentException("Credit amount must be positive");
        Balance = Balance.Add(amount);
    }
}

MDA.Wallet.Domain/Entities/Wallet.cs

Entidad principal de wallet con sus bolsas y operaciones agregadas.

csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

using MDA.Wallet.Domain.ValueObjects;
namespace MDA.Wallet.Domain.Entities;

public class AccountWallet
{
    public Guid Id { get; private set; }
    public List<BalancePocket> Pockets { get; private set; } = new();

    public AccountWallet(Guid id)
    {
        Id = id;
    }

    public void ApplyDebits(List<(BalancePocket pocket, Money amount)> debits)
    {
        foreach (var d in debits)
        {
            d.pocket.Debit(d.amount);
        }
    }

    public void ApplyCredit(int pocketTypeId, Money amount)
    {
        var pocket = Pockets.FirstOrDefault(p => p.PocketTypeId == pocketTypeId);
        if (pocket == null)
        {
            pocket = new BalancePocket(pocketTypeId, new Money(0, amount.Currency), 99);
            Pockets.Add(pocket);
        }
        pocket.Credit(amount);
    }
}

MDA.Wallet.Domain/Interfaces/IConsumptionStrategy.cs

Contrato de dominio para resolver de que bolsas sale un consumo.

csharp
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Intrinsics.X86;
using System.Text;
using System.Threading.Tasks;
using static System.Runtime.InteropServices.JavaScript.JSType;

using MDA.Wallet.Domain.ValueObjects;
using MDA.Wallet.Domain.Entities;
namespace MDA.Wallet.Domain.Interfaces;

public interface IConsumptionStrategy
{
    List<(BalancePocket, Money)> Resolve(AccountWallet wallet, Money amount);
}

MDA.Wallet.Domain/Services/DefaultConsumptionStrategy.cs

Estrategia por defecto para consumir saldo segun prioridad.

csharp
using MDA.Wallet.Domain.Entities;
using MDA.Wallet.Domain.Interfaces;
using MDA.Wallet.Domain.ValueObjects;
using Microsoft.Extensions.Logging;
using Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MDA.Wallet.Domain.Services
{
    public class DefaultConsumptionStrategy : IConsumptionStrategy
    {
        public List<(BalancePocket, Money)> Resolve(AccountWallet 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;
        }
    }
}

MDA.Wallet.Domain/Services/ProductEarningStrategy.cs

Estrategia de negocio para calcular abonos o equivalencias por producto.

csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using MDA.Wallet.Domain.Interfaces;
using MDA.Wallet.Domain.ValueObjects;

namespace MDA.Wallet.Domain.Services
{
    public class ProductEarningStrategy : IEarningStrategy
    {
        private const decimal DefaultFactorMarcaPropia = 0.02m;
        private const decimal DefaultFactor = 0.01m;

        public Money CalculateEarning(TransactionItem item)
        {
            decimal factor = 0;
            decimal equivalencia = 0;
            int equivalenciaId = 0;

            if (item.ItemSku == "7506472801861")
            {
                factor = 0.05m;
                equivalenciaId = 1;
            }

            if (equivalenciaId > 0)
            {
                equivalencia = factor * item.ItemAmount;
            }
            else if (item.ItemSku.Length == 13)
            {
                factor = DefaultFactor;
                if (IsMarcaPropia(item.ItemSku)) factor = DefaultFactorMarcaPropia;
                equivalencia = item.ItemAmount * factor;
            }

            if (item.ItemSku == "7506472801860") equivalencia = item.ItemAmount;
            if (item.ItemSku == "7506472821684" && item.CurrentDate.Year <= 2025) equivalencia = item.ItemAmount;

            if (IsExcluded(item.ItemSku) || IsBlocked(item.ItemSku, item.StoreId, item.CurrentDate))
            {
                equivalencia = 0;
            }

            return new Money(equivalencia, "MXN");
        }

        private bool IsMarcaPropia(string sku) => sku.StartsWith("750123");
        private bool IsExcluded(string sku) => false;
        private bool IsBlocked(string sku, string store, DateTime date) => false;
    }
}

MDA.Wallet.Domain/ValueObjects/IEarningStrategy.cs

Contrato para estrategias que calculan montos de abono.

csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MDA.Wallet.Domain.ValueObjects
{
    public interface IEarningStrategy
    {
        Money CalculateEarning(TransactionItem item);
    }
}

MDA.Wallet.Domain/ValueObjects/Money.cs

Value object de dinero con validaciones y operaciones basicas.

csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

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);
        }
    }
}

MDA.Wallet.Domain/ValueObjects/TransactionItem.cs

Record con los datos de una transaccion de producto para calcular abonos.

csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MDA.Wallet.Domain.ValueObjects
{
    public record TransactionItem(
        string ItemSku,
        int ItemQuantity,
        decimal ItemPrice,
        decimal ItemAmount,
        string StoreId,
        string TransactionId,
        DateTime CurrentDate
    );
}

MDA.Wallet.Infrastructure

Capa de infraestructura para repositorios y futura persistencia.

MDA.Wallet.Infrastructure/Class1.cs

Clase vacia de plantilla en la capa de infraestructura.

csharp
namespace MDA.Wallet.Infrastructure
{
    public class Class1
    {

    }
}

MDA.Wallet.Infrastructure/MDA.Wallet.Infrastructure.csproj

Proyecto de infraestructura con referencias a aplicacion y dominio.

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\MDA.Wallet.Application\MDA.Wallet.Application.csproj" />
    <ProjectReference Include="..\MDA.Wallet.Domain\MDA.Wallet.Domain.csproj" />
  </ItemGroup>

  <ItemGroup>
    <Folder Include="Adapters\" />
    <Folder Include="Persistence\" />
  </ItemGroup>

</Project>

MDA.Wallet.Infrastructure/Repositories/FakeWalletRepository.cs

Repositorio falso que simula lectura y guardado de la wallet.

csharp
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Serilog.Core;
using System.Net.Sockets;

using MDA.Wallet.Application.Interfaces;
using MDA.Wallet.Domain.Entities;
using MDA.Wallet.Domain.ValueObjects;
namespace MDA.Wallet.Infrastructure.Repositories;

public class FakeWalletRepository : IWalletRepository
{
    private readonly ILogger<FakeWalletRepository> _logger;

    public FakeWalletRepository(ILogger<FakeWalletRepository> logger)
    {
        _logger = logger;
    }

    public Task<AccountWallet> Get(Guid id)
    {
        _logger.LogInformation(" ... connected BD");
        var wallet = new AccountWallet(id);
        _logger.LogInformation(" ... WALLET = [WalletId: {WalletId}]", wallet.Id);

        wallet.Pockets.Add(new BalancePocket(1, new Money(300, "MXN"), 1));
        wallet.Pockets.Add(new BalancePocket(2, new Money(199, "MXN"), 2));
        wallet.Pockets.Add(new BalancePocket(3, new Money(677, "MXN"), 3));

        _logger.LogInformation(" ... WALLET = Pocket [Id: {PocketTypeId}, Amount: {Amount}]", wallet.Pockets[0].PocketTypeId, wallet.Pockets[0].Balance.Amount);
        _logger.LogInformation(" ... WALLET = Pocket [Id: {PocketTypeId}, Amount: {Amount}]", wallet.Pockets[1].PocketTypeId, wallet.Pockets[1].Balance.Amount);
        _logger.LogInformation(" ... WALLET = Pocket [Id: {PocketTypeId}, Amount: {Amount}]", wallet.Pockets[2].PocketTypeId, wallet.Pockets[2].Balance.Amount);

        return Task.FromResult(wallet);
    }

    public Task Save(AccountWallet wallet)
    {
        return Task.CompletedTask;
    }
}