├── global.json ├── docs ├── layers.xcf ├── clean-architecture-dotnet-core.png ├── clean-architecture-use-cases.png ├── clean-architecture-manga-layers.png ├── clean-architecture-manga-swagger.png ├── clean-architecture-manga-use-cases.png └── clean-architecture-manga-ports-and-adapters.png ├── src ├── WebApi │ ├── DependencyInjection │ │ ├── FeatureFlags │ │ │ ├── Features.cs │ │ │ └── FeatureFlagsExtensions.cs │ │ ├── BusinessExceptionExtensions.cs │ │ ├── UserInterfaceV2Extensions.cs │ │ ├── HttpMetricsExtensions.cs │ │ ├── InMemoryInfrastructureExtensions.cs │ │ ├── VersioningExtensions.cs │ │ ├── Authentication │ │ │ └── UserService.cs │ │ ├── SQLServerInfrastructureExtensions.cs │ │ └── ApplicationExtensions.cs │ ├── appsettings.json │ ├── appsettings.Development.json │ ├── Dockerfile │ ├── UseCases │ │ ├── V1 │ │ │ ├── CloseAccount │ │ │ │ ├── CloseAccountRequest.cs │ │ │ │ ├── CloseAccountPresenter.cs │ │ │ │ ├── CloseAccountResponse.cs │ │ │ │ └── AccountsController.cs │ │ │ ├── GetAccountDetails │ │ │ │ ├── GetAccountDetailsRequest.cs │ │ │ │ ├── GetAccountDetailsResponse.cs │ │ │ │ ├── GetAccountDetailsPresenter.cs │ │ │ │ └── AccountsController.cs │ │ │ ├── Login │ │ │ │ └── LoginController.cs │ │ │ ├── Register │ │ │ │ ├── RegisterRequest.cs │ │ │ │ ├── RegisterResponse.cs │ │ │ │ ├── RegisterPresenter.cs │ │ │ │ └── CustomersController.cs │ │ │ ├── Deposit │ │ │ │ ├── DepositRequest.cs │ │ │ │ ├── DepositPresenter.cs │ │ │ │ ├── DepositResponse.cs │ │ │ │ └── AccountsController.cs │ │ │ ├── Withdraw │ │ │ │ ├── WithdrawRequest.cs │ │ │ │ ├── WithdrawResponse.cs │ │ │ │ ├── WithdrawPresenter.cs │ │ │ │ └── AccountsController.cs │ │ │ ├── Transfer │ │ │ │ ├── TransferRequest.cs │ │ │ │ ├── TransferResponse.cs │ │ │ │ └── TransferPresenter.cs │ │ │ └── GetCustomerDetails │ │ │ │ ├── GetCustomerDetailsResponse.cs │ │ │ │ ├── CustomersController.cs │ │ │ │ └── GetCustomerDetailsPresenter.cs │ │ └── V2 │ │ │ └── GetAccountDetails │ │ │ ├── GetAccountDetailsRequestV2.cs │ │ │ ├── GetAccountDetailsPresenterV2.cs │ │ │ └── AccountsController.cs │ ├── Program.cs │ ├── appsettings.Production.json │ ├── Filters │ │ ├── BusinessExceptionFilter.cs │ │ ├── SwaggerDefaultValues.cs │ │ └── SwaggerDocumentFilter.cs │ └── ViewModels │ │ ├── TransactionModel.cs │ │ └── AccountDetailsModel.cs ├── Application │ ├── Boundaries │ │ ├── IUseCaseOutput.cs │ │ ├── IUseCaseInput.cs │ │ ├── Deposit │ │ │ ├── IOutputPort.cs │ │ │ ├── IUseCase.cs │ │ │ ├── DepositInput.cs │ │ │ ├── DepositOutput.cs │ │ │ └── Transaction.cs │ │ ├── Transfer │ │ │ ├── IOutputPort.cs │ │ │ ├── IUseCase.cs │ │ │ ├── TransferInput.cs │ │ │ ├── TransferOutput.cs │ │ │ └── Transaction.cs │ │ ├── CloseAccount │ │ │ ├── IOutputPort.cs │ │ │ ├── IUseCase.cs │ │ │ ├── CloseAccountInput.cs │ │ │ └── CloseAccountOutput.cs │ │ ├── GetCustomerDetails │ │ │ ├── GetCustomerDetailsInput.cs │ │ │ ├── IOutputPort.cs │ │ │ ├── IUseCase.cs │ │ │ ├── Transaction.cs │ │ │ └── GetCustomerDetailsOutput.cs │ │ ├── GetAccountDetails │ │ │ ├── IOutputPort.cs │ │ │ ├── IUseCaseV2.cs │ │ │ ├── IUseCase.cs │ │ │ ├── GetAccountDetailsInput.cs │ │ │ └── Transaction.cs │ │ ├── Register │ │ │ ├── IUseCase.cs │ │ │ ├── IOutputPort.cs │ │ │ ├── RegisterInput.cs │ │ │ ├── Transaction.cs │ │ │ ├── Account.cs │ │ │ └── Customer.cs │ │ ├── Withdraw │ │ │ ├── IUseCase.cs │ │ │ ├── IOutputPort.cs │ │ │ ├── WithdrawInput.cs │ │ │ ├── WithdrawOutput.cs │ │ │ └── Transaction.cs │ │ ├── IOutputPortNotFound.cs │ │ ├── IOutputPortStandard.cs │ │ └── IUseCase.cs │ ├── Services │ │ └── IUnitOfWork.cs │ ├── Application.csproj │ └── UseCases │ │ └── GetAccountDetails.cs ├── Domain │ ├── Customers │ │ ├── ValueObjects │ │ │ ├── InvalidSSNException.cs │ │ │ ├── EmptyCustomerIdException.cs │ │ │ ├── SSNShouldNotBeEmptyException.cs │ │ │ ├── NameShouldNotBeEmptyException.cs │ │ │ ├── Name.cs │ │ │ ├── CustomerId.cs │ │ │ └── SSN.cs │ │ ├── CustomerNotFoundException.cs │ │ ├── ICustomerFactory.cs │ │ ├── ICustomer.cs │ │ ├── ICustomerRepository.cs │ │ ├── Customer.cs │ │ └── AccountCollection.cs │ ├── Accounts │ │ ├── Debits │ │ │ ├── EmptyDebitIdException.cs │ │ │ ├── IDebit.cs │ │ │ ├── Debit.cs │ │ │ └── DebitId.cs │ │ ├── Credits │ │ │ ├── EmptyCreditIdException.cs │ │ │ ├── ICredit.cs │ │ │ ├── CreditId.cs │ │ │ └── Credit.cs │ │ ├── ValueObjects │ │ │ ├── EmptyAccountIdException.cs │ │ │ ├── MoneyShouldBePositiveException.cs │ │ │ ├── AccountId.cs │ │ │ ├── PositiveMoney.cs │ │ │ └── Money.cs │ │ ├── AccountNotFoundException.cs │ │ ├── IAccountFactory.cs │ │ ├── IAccount.cs │ │ └── IAccountRepository.cs │ ├── Security │ │ ├── User.cs │ │ ├── ValueObjects │ │ │ ├── ExternalUserIdShouldNotBeEmptyException.cs │ │ │ └── ExternalUserId.cs │ │ ├── UserNotFoundException.cs │ │ ├── IUser.cs │ │ ├── IUserFactory.cs │ │ ├── Services │ │ │ └── IUserService.cs │ │ ├── IUserRepository.cs │ │ └── SecurityService.cs │ ├── DomainException.cs │ ├── stylecop.json │ └── Domain.csproj └── Infrastructure │ ├── InMemoryDataAccess │ ├── User.cs │ ├── UnitOfWork.cs │ ├── Debit.cs │ ├── Credit.cs │ ├── Services │ │ ├── UserService.cs │ │ └── TestUserService.cs │ ├── Customer.cs │ ├── Presenters │ │ ├── DepositPresenter.cs │ │ ├── TransferPresenter.cs │ │ ├── RegisterPresenter.cs │ │ ├── CloseAccountPresenter.cs │ │ ├── GetAccountDetailsPresenter.cs │ │ ├── GetCustomerDetailsPresenter.cs │ │ └── WithdrawPresenter.cs │ ├── Account.cs │ ├── Repositories │ │ ├── UserRepository.cs │ │ └── CustomerRepository.cs │ └── EntityFactory.cs │ ├── EntityFrameworkDataAccess │ ├── Entities │ │ ├── User.cs │ │ ├── Debit.cs │ │ ├── Credit.cs │ │ ├── Customer.cs │ │ └── Account.cs │ ├── MangaContext.cs │ ├── Configuration │ │ ├── AccountConfiguration.cs │ │ ├── UserConfiguration.cs │ │ ├── CustomerConfiguration.cs │ │ ├── DebitConfiguration.cs │ │ └── CreditConfiguration.cs │ ├── UnitOfWork.cs │ ├── ContextFactory.cs │ ├── Repositories │ │ ├── UserRepository.cs │ │ └── CustomerRepository.cs │ ├── EntityFactory.cs │ ├── Migrations │ │ └── 20191126172117_ValueObjects.cs │ └── SeedData.cs │ └── Infrastructure.csproj ├── docker-compose.yml ├── test ├── UnitTests │ ├── UseCaseTests │ │ ├── Deposit │ │ │ ├── NegativeDataSetup.cs │ │ │ └── PositiveDataSetup.cs │ │ ├── Transfer │ │ │ ├── PositiveDataSetup.cs │ │ │ └── TransferUseCaseTests.cs │ │ ├── Withdraw │ │ │ ├── PositiveDataSetup.cs │ │ │ └── WithdrawTests.cs │ │ ├── Register │ │ │ └── PositiveDataSetup.cs │ │ └── CloseAccount │ │ │ └── PositiveDataSetup.cs │ ├── InputValidationTests │ │ ├── RegisterInputValidationTests.cs │ │ ├── CloseAccountInputValidationTests.cs │ │ ├── GetAccountDetailsInputValidationTests.cs │ │ ├── DepositInputValidationTests.cs │ │ ├── WithdrawInputValidationTests.cs │ │ └── TransferInputValidationTests.cs │ ├── EntitiesTests │ │ └── CustomerTests.cs │ ├── UnitTests.csproj │ ├── PresenterTests │ │ └── RegisterPresenterTests.cs │ └── TestFixtures │ │ └── StandardFixture.cs ├── AcceptanceTests │ ├── HttpClientExtensions.cs │ └── ComponentTests.csproj └── IntegrationTests │ └── IntegrationTests.csproj ├── sql-docker-up.sh ├── .vscode ├── settings.json └── launch.json └── azure-pipelines.yml /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "3.1.100" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /docs/layers.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piraces/clean-architecture-manga/master/docs/layers.xcf -------------------------------------------------------------------------------- /docs/clean-architecture-dotnet-core.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piraces/clean-architecture-manga/master/docs/clean-architecture-dotnet-core.png -------------------------------------------------------------------------------- /docs/clean-architecture-use-cases.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piraces/clean-architecture-manga/master/docs/clean-architecture-use-cases.png -------------------------------------------------------------------------------- /docs/clean-architecture-manga-layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piraces/clean-architecture-manga/master/docs/clean-architecture-manga-layers.png -------------------------------------------------------------------------------- /docs/clean-architecture-manga-swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piraces/clean-architecture-manga/master/docs/clean-architecture-manga-swagger.png -------------------------------------------------------------------------------- /docs/clean-architecture-manga-use-cases.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piraces/clean-architecture-manga/master/docs/clean-architecture-manga-use-cases.png -------------------------------------------------------------------------------- /docs/clean-architecture-manga-ports-and-adapters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piraces/clean-architecture-manga/master/docs/clean-architecture-manga-ports-and-adapters.png -------------------------------------------------------------------------------- /src/WebApi/DependencyInjection/FeatureFlags/Features.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.DependencyInjection.FeatureFlags 2 | { 3 | public enum Features 4 | { 5 | Transfer, 6 | GetAccountDetailsV2, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Application/Boundaries/IUseCaseOutput.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries 2 | { 3 | /// 4 | /// Output Message Marker. 5 | /// 6 | public interface IUseCaseOutput 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Application/Boundaries/IUseCaseInput.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries 2 | { 3 | /// 4 | /// Input Message interface marker. 5 | /// 6 | public interface IUseCaseInput 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/WebApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | webapi: 4 | image: ivanpaulovich/clean-architecture-manga 5 | build: 6 | context: . 7 | dockerfile: src/WebApi/Dockerfile 8 | ports: 9 | - 5500:80 10 | environment: 11 | - ASPNETCORE_ENVIRONMENT=Development -------------------------------------------------------------------------------- /src/Application/Boundaries/Deposit/IOutputPort.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Deposit 2 | { 3 | /// 4 | /// Output Port. 5 | /// 6 | public interface IOutputPort 7 | : IOutputPortStandard, IOutputPortNotFound 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Transfer/IOutputPort.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Transfer 2 | { 3 | /// 4 | /// Transfer Output Port. 5 | /// 6 | public interface IOutputPort 7 | : IOutputPortStandard, IOutputPortNotFound 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Domain/Customers/ValueObjects/InvalidSSNException.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Customers.ValueObjects 2 | { 3 | internal sealed class InvalidSSNException : DomainException 4 | { 5 | internal InvalidSSNException(string message) 6 | : base(message) 7 | { 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Application/Boundaries/CloseAccount/IOutputPort.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.CloseAccount 2 | { 3 | /// 4 | /// Output Port. 5 | /// 6 | public interface IOutputPort 7 | : IOutputPortStandard, IOutputPortNotFound 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Application/Boundaries/GetCustomerDetails/GetCustomerDetailsInput.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.GetCustomerDetails 2 | { 3 | /// 4 | /// Get Customer Details Input Message. 5 | /// 6 | public sealed class GetCustomerDetailsInput : IUseCaseInput 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/UnitTests/UseCaseTests/Deposit/NegativeDataSetup.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTests.UseCasesTests.Deposit 2 | { 3 | using Xunit; 4 | 5 | internal sealed class NegativeDataSetup : TheoryData 6 | { 7 | public NegativeDataSetup() 8 | { 9 | Add(-100); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Domain/Customers/ValueObjects/EmptyCustomerIdException.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Customers.ValueObjects 2 | { 3 | internal sealed class EmptyCustomerIdException : DomainException 4 | { 5 | internal EmptyCustomerIdException(string message) 6 | : base(message) 7 | { 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Application/Boundaries/GetAccountDetails/IOutputPort.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.GetAccountDetails 2 | { 3 | /// 4 | /// Output Port. 5 | /// 6 | public interface IOutputPort 7 | : IOutputPortStandard, IOutputPortNotFound 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/UnitTests/UseCaseTests/Transfer/PositiveDataSetup.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTests.UseCaseTests.Transfer 2 | { 3 | using Xunit; 4 | 5 | internal sealed class PositiveDataSetup : TheoryData 6 | { 7 | public PositiveDataSetup() 8 | { 9 | Add(100, 600); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test/UnitTests/UseCaseTests/Withdraw/PositiveDataSetup.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTests.UseCasesTests.Withdraw 2 | { 3 | using Xunit; 4 | 5 | internal sealed class PositiveDataSetup : TheoryData 6 | { 7 | public PositiveDataSetup() 8 | { 9 | Add(100, 600); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Application/Boundaries/GetCustomerDetails/IOutputPort.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.GetCustomerDetails 2 | { 3 | /// 4 | /// Output Port. 5 | /// 6 | public interface IOutputPort 7 | : IOutputPortStandard, IOutputPortNotFound 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/UnitTests/UseCaseTests/Deposit/PositiveDataSetup.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTests.UseCasesTests.Deposit 2 | { 3 | using Xunit; 4 | 5 | internal sealed class PositiveDataSetup : TheoryData 6 | { 7 | public PositiveDataSetup() 8 | { 9 | Add(0); 10 | Add(100); 11 | Add(200); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /test/UnitTests/UseCaseTests/Register/PositiveDataSetup.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTests.UseCasesTests.Register 2 | { 3 | using Xunit; 4 | 5 | internal sealed class PositiveDataSetup : TheoryData 6 | { 7 | public PositiveDataSetup() 8 | { 9 | Add(0); 10 | Add(100); 11 | Add(200); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/WebApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "FeatureManagement": { 10 | "Transfer": true, 11 | "GetAccountDetailsV2": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/UnitTests/UseCaseTests/CloseAccount/PositiveDataSetup.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTests.UseCasesTests.CloseAccount 2 | { 3 | using Xunit; 4 | 5 | internal sealed class PositiveDataSetup : TheoryData 6 | { 7 | public PositiveDataSetup() 8 | { 9 | Add(0.5M); 10 | Add(100M); 11 | Add(200M); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Domain/Accounts/Debits/EmptyDebitIdException.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Accounts.Debits 2 | { 3 | /// 4 | /// Empty DebitId Exception. 5 | /// 6 | public sealed class EmptyDebitIdException : DomainException 7 | { 8 | internal EmptyDebitIdException(string message) 9 | : base(message) 10 | { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Register/IUseCase.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Register 2 | { 3 | /// 4 | /// Use Case Domain-Driven Design Pattern. 5 | /// 6 | public interface IUseCase : IUseCase 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Domain/Accounts/Credits/EmptyCreditIdException.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Accounts.Credits 2 | { 3 | /// 4 | /// Empty CreditId Exception. 5 | /// 6 | public sealed class EmptyCreditIdException : DomainException 7 | { 8 | internal EmptyCreditIdException(string message) 9 | : base(message) 10 | { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Deposit/IUseCase.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Deposit 2 | { 3 | /// 4 | /// Deposit Use Case Domain-Driven Design Pattern. 5 | /// 6 | public interface IUseCase : IUseCase 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Application/Boundaries/GetAccountDetails/IUseCaseV2.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.GetAccountDetails 2 | { 3 | /// 4 | /// Use Case Domain-Driven Design Pattern. 5 | /// 6 | public interface IUseCaseV2 : IUseCase 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Transfer/IUseCase.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Transfer 2 | { 3 | /// 4 | /// Transfer Use Case Domain-Driven Design Pattern. 5 | /// 6 | public interface IUseCase : IUseCase 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Withdraw/IUseCase.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Withdraw 2 | { 3 | /// 4 | /// Withdraw Use Case Domain-Driven Design Pattern. 5 | /// 6 | public interface IUseCase : IUseCase 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Domain/Accounts/ValueObjects/EmptyAccountIdException.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Accounts.ValueObjects 2 | { 3 | /// 4 | /// Empty Account Id Exception. 5 | /// 6 | public sealed class EmptyAccountIdException : DomainException 7 | { 8 | internal EmptyAccountIdException(string message) 9 | : base(message) 10 | { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Application/Boundaries/CloseAccount/IUseCase.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.CloseAccount 2 | { 3 | /// 4 | /// Close Account Use Case Domain-Driven Design Pattern. 5 | /// 6 | public interface IUseCase : IUseCase 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Application/Boundaries/GetAccountDetails/IUseCase.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.GetAccountDetails 2 | { 3 | /// 4 | /// Use Case Domain-Driven Design Pattern. 5 | /// 6 | public interface IUseCase : IUseCase 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Application/Boundaries/GetCustomerDetails/IUseCase.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.GetCustomerDetails 2 | { 3 | /// 4 | /// Use Case Domain-Driven Design Pattern. 5 | /// 6 | public interface IUseCase : IUseCase 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/WebApi/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.0 AS base 2 | WORKDIR /app 3 | EXPOSE 80 4 | 5 | FROM mcr.microsoft.com/dotnet/core/sdk:3.0 AS build 6 | COPY . . 7 | RUN dotnet test 8 | 9 | FROM build AS publish 10 | WORKDIR /src/WebApi 11 | RUN dotnet publish "WebApi.csproj" -c Release -o /app 12 | 13 | FROM base AS final 14 | WORKDIR /app 15 | COPY --from=publish /app . 16 | ENTRYPOINT ["dotnet", "WebApi.dll"] -------------------------------------------------------------------------------- /src/Domain/Customers/ValueObjects/SSNShouldNotBeEmptyException.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Customers.ValueObjects 2 | { 3 | /// 4 | /// SSN Should Not Be Empty Exception. 5 | /// 6 | public sealed class SSNShouldNotBeEmptyException : DomainException 7 | { 8 | internal SSNShouldNotBeEmptyException(string message) 9 | : base(message) 10 | { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Domain/Security/User.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Security 2 | { 3 | using Domain.Customers.ValueObjects; 4 | using Domain.Security.ValueObjects; 5 | 6 | /// 7 | public abstract class User : IUser 8 | { 9 | /// 10 | public ExternalUserId ExternalUserId { get; set; } 11 | 12 | /// 13 | public CustomerId CustomerId { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Domain/Accounts/ValueObjects/MoneyShouldBePositiveException.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Accounts.ValueObjects 2 | { 3 | /// 4 | /// Money Should Be Positive Exception. 5 | /// 6 | public sealed class MoneyShouldBePositiveException : DomainException 7 | { 8 | internal MoneyShouldBePositiveException(string message) 9 | : base(message) 10 | { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Domain/Customers/ValueObjects/NameShouldNotBeEmptyException.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Customers.ValueObjects 2 | { 3 | /// 4 | /// Name Should Not Be Empty Exception. 5 | /// 6 | public sealed class NameShouldNotBeEmptyException : DomainException 7 | { 8 | internal NameShouldNotBeEmptyException(string message) 9 | : base(message) 10 | { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Application/Boundaries/IOutputPortNotFound.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries 2 | { 3 | /// 4 | /// Not Found Output Port. 5 | /// 6 | public interface IOutputPortNotFound 7 | { 8 | /// 9 | /// Informs the resource was not found. 10 | /// 11 | /// Text description. 12 | void NotFound(string message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Domain/Security/ValueObjects/ExternalUserIdShouldNotBeEmptyException.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Security.ValueObjects 2 | { 3 | /// 4 | /// External User Id Should Not Be Empty Exception. 5 | /// 6 | public sealed class ExternalUserIdShouldNotBeEmptyException : DomainException 7 | { 8 | internal ExternalUserIdShouldNotBeEmptyException(string message) 9 | : base(message) 10 | { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Application/Services/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Services 2 | { 3 | using System.Threading.Tasks; 4 | 5 | /// 6 | /// Unit Of Work. Should only be used by Use Cases. 7 | /// 8 | public interface IUnitOfWork 9 | { 10 | /// 11 | /// Applies all database changes. 12 | /// 13 | /// Number of affected rows. 14 | Task Save(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/CloseAccount/CloseAccountRequest.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.CloseAccount 2 | { 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | /// 7 | /// The Close Account Request. 8 | /// 9 | public sealed class CloseAccountRequest 10 | { 11 | /// 12 | /// Gets or sets account ID. 13 | /// 14 | [Required] 15 | public Guid AccountId { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Application/Boundaries/Register/IOutputPort.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Register 2 | { 3 | /// 4 | /// Output Port. 5 | /// 6 | public interface IOutputPort 7 | : IOutputPortStandard 8 | { 9 | /// 10 | /// Informs the user is already registered. 11 | /// 12 | /// Custom message. 13 | void CustomerAlreadyRegistered(string message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Withdraw/IOutputPort.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Withdraw 2 | { 3 | /// 4 | /// Output Port. 5 | /// 6 | public interface IOutputPort 7 | : IOutputPortStandard, IOutputPortNotFound 8 | { 9 | /// 10 | /// Informs it is out of balance. 11 | /// 12 | /// Custom message. 13 | void OutOfBalance(string message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Domain/Accounts/Debits/IDebit.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Accounts.Debits 2 | { 3 | using Domain.Accounts.ValueObjects; 4 | 5 | /// 6 | /// Debit. 7 | /// 8 | public interface IDebit 9 | { 10 | /// 11 | /// Calculates the sum of two positive amounts. 12 | /// 13 | /// Positive amount. 14 | /// The sum. 15 | PositiveMoney Sum(PositiveMoney amount); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Infrastructure/InMemoryDataAccess/User.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.InMemoryDataAccess 2 | { 3 | using Domain.Customers.ValueObjects; 4 | using Domain.Security.ValueObjects; 5 | 6 | public class User : Domain.Security.User 7 | { 8 | public User(CustomerId customerId, ExternalUserId externalUserId) 9 | { 10 | CustomerId = customerId; 11 | ExternalUserId = externalUserId; 12 | } 13 | 14 | protected User() 15 | { 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/GetAccountDetails/GetAccountDetailsRequest.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.GetAccountDetails 2 | { 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | /// 7 | /// The Get Account Details Request. 8 | /// 9 | public sealed class GetAccountDetailsRequest 10 | { 11 | /// 12 | /// Gets or sets account ID. 13 | /// 14 | [Required] 15 | public Guid AccountId { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/WebApi/UseCases/V2/GetAccountDetails/GetAccountDetailsRequestV2.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V2.GetAccountDetails 2 | { 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | /// 7 | /// The Get Account Details Request. 8 | /// 9 | public sealed class GetAccountDetailsRequestV2 10 | { 11 | /// 12 | /// Gets or sets account ID. 13 | /// 14 | [Required] 15 | public Guid AccountId { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Infrastructure/EntityFrameworkDataAccess/Entities/User.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.EntityFrameworkDataAccess 2 | { 3 | using Domain.Customers.ValueObjects; 4 | using Domain.Security.ValueObjects; 5 | 6 | public class User : Domain.Security.User 7 | { 8 | public User(CustomerId customerId, ExternalUserId externalUserId) 9 | { 10 | CustomerId = customerId; 11 | ExternalUserId = externalUserId; 12 | } 13 | 14 | protected User() 15 | { 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Domain/Accounts/Credits/ICredit.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Accounts.Credits 2 | { 3 | using Domain.Accounts.ValueObjects; 4 | 5 | /// 6 | /// Credit Entity Interface. 7 | /// 8 | public interface ICredit 9 | { 10 | /// 11 | /// Calculates the sum between positive amounts. 12 | /// 13 | /// Positive amount. 14 | /// The positive sum. 15 | PositiveMoney Sum(PositiveMoney amount); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Infrastructure/InMemoryDataAccess/UnitOfWork.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.InMemoryDataAccess 2 | { 3 | using System.Threading.Tasks; 4 | using Application.Services; 5 | 6 | public sealed class UnitOfWork : IUnitOfWork 7 | { 8 | private readonly MangaContext _context; 9 | 10 | public UnitOfWork(MangaContext context) 11 | { 12 | _context = context; 13 | } 14 | 15 | public async Task Save() 16 | { 17 | return await Task.FromResult(0); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Domain/DomainException.cs: -------------------------------------------------------------------------------- 1 | namespace Domain 2 | { 3 | using System; 4 | 5 | /// 6 | /// Domain Exception. 7 | /// 8 | public class DomainException : Exception 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// Message. 14 | public DomainException(string businessMessage) 15 | : base(businessMessage) 16 | { 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/Login/LoginController.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.Login 2 | { 3 | using Microsoft.AspNetCore.Authentication; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | [AllowAnonymous] 8 | [Route("[controller]")] 9 | public sealed class LoginController : Controller 10 | { 11 | [HttpGet] 12 | public IActionResult Login(string returnUrl = "/") 13 | { 14 | return Challenge(new AuthenticationProperties() { RedirectUri = returnUrl }); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Domain/Security/UserNotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Security 2 | { 3 | /// 4 | /// User Not Found Exception. 5 | /// 6 | public sealed class UserNotFoundException : DomainException 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// Message. 12 | public UserNotFoundException(string message) 13 | : base(message) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Domain/Accounts/AccountNotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Accounts 2 | { 3 | /// 4 | /// Account Not Found Exception. 5 | /// 6 | public sealed class AccountNotFoundException : DomainException 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// Message. 12 | public AccountNotFoundException(string message) 13 | : base(message) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Domain/Customers/CustomerNotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Customers 2 | { 3 | /// 4 | /// Customer Not Found Exception. 5 | /// 6 | public sealed class CustomerNotFoundException : DomainException 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// Message. 12 | public CustomerNotFoundException(string message) 13 | : base(message) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/WebApi/DependencyInjection/BusinessExceptionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.DependencyInjection 2 | { 3 | using Microsoft.Extensions.DependencyInjection; 4 | using WebApi.Filters; 5 | 6 | public static class BusinessExceptionExtensions 7 | { 8 | public static IServiceCollection AddBusinessExceptionFilter(this IServiceCollection services) 9 | { 10 | services.AddMvc(options => 11 | { 12 | options.Filters.Add(typeof(BusinessExceptionFilter)); 13 | }); 14 | 15 | return services; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Application/Boundaries/IOutputPortStandard.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries 2 | { 3 | /// 4 | /// Standard Output Port. 5 | /// 6 | /// Any IUseCaseOutput. 7 | public interface IOutputPortStandard 8 | where TUseCaseOutput : IUseCaseOutput 9 | { 10 | /// 11 | /// Writes to the Standard Output. 12 | /// 13 | /// The Output Port Message. 14 | void Standard(TUseCaseOutput output); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/Register/RegisterRequest.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.Register 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | /// 6 | /// Registration Request. 7 | /// 8 | public sealed class RegisterRequest 9 | { 10 | /// 11 | /// Gets or sets sSN. 12 | /// 13 | [Required] 14 | public string SSN { get; set; } 15 | 16 | /// 17 | /// Gets or sets initial Amount. 18 | /// 19 | [Required] 20 | public decimal InitialAmount { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /src/WebApi/Program.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi 2 | { 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Hosting; 5 | 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/UnitTests/InputValidationTests/RegisterInputValidationTests.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTests.InputValidationTests 2 | { 3 | using Application.Boundaries.Register; 4 | using Domain.Accounts.ValueObjects; 5 | using Domain.Customers.ValueObjects; 6 | using Xunit; 7 | 8 | public sealed class RegisterInputValidationTests 9 | { 10 | [Fact] 11 | public void GivenValidData_InputCreated() 12 | { 13 | var actual = new RegisterInput( 14 | new SSN("19860817999"), 15 | new PositiveMoney(10)); 16 | Assert.NotNull(actual); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Application/Boundaries/IUseCase.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries 2 | { 3 | using System.Threading.Tasks; 4 | 5 | /// 6 | /// Use Case. 7 | /// 8 | /// Any Input Message. 9 | public interface IUseCase 10 | where TUseCaseInput : IUseCaseInput 11 | { 12 | /// 13 | /// Executes the Use Case. 14 | /// 15 | /// Input Message. 16 | /// Task. 17 | Task Execute(TUseCaseInput input); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sql-docker-up.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo docker pull mcr.microsoft.com/mssql/server:2017-latest 3 | sudo docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=' -p 1433:1433 --name sql1 -d mcr.microsoft.com/mssql/server:2017-latest 4 | sudo docker exec -it sql1 /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P '' -Q 'ALTER LOGIN SA WITH PASSWORD=""' 5 | 6 | 7 | # query 8 | 9 | # sudo docker exec -it sql1 "bash" 10 | # /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P '' 11 | # SELECT Name from sys.Databases 12 | # GO 13 | # USE MangaDB01 14 | # GO 15 | # SELECT * FROM Account 16 | # GO -------------------------------------------------------------------------------- /src/Domain/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | // ACTION REQUIRED: This file was automatically added to your project, but it 3 | // will not take effect until additional steps are taken to enable it. See the 4 | // following page for additional information: 5 | // 6 | // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md 7 | 8 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 9 | "settings": { 10 | "documentationRules": { 11 | "companyName": "Ivan Paulovich" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/Deposit/DepositRequest.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.Deposit 2 | { 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | /// 7 | /// The request to Deposit. 8 | /// 9 | public sealed class DepositRequest 10 | { 11 | /// 12 | /// Gets or sets the Account ID. 13 | /// 14 | [Required] 15 | public Guid AccountId { get; set; } 16 | 17 | /// 18 | /// Gets or sets the amount to Deposit. 19 | /// 20 | [Required] 21 | public decimal Amount { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/WebApi/appsettings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Warning" 7 | } 8 | }, 9 | "ConnectionStrings": { 10 | "DefaultConnection": "Server=localhost;User Id=sa;Password=;Database=MangaDB01" 11 | }, 12 | "FeatureManagement": { 13 | "Transfer": true, 14 | "GetAccountDetailsV2": true 15 | }, 16 | "GitHub": { 17 | "ClientId": "97c352e5dd81b63da60c", 18 | "ClientSecret": "584e60c5b3cb801f778c4e7a30dc457f1275f8f1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/CloseAccount/CloseAccountPresenter.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.CloseAccount 2 | { 3 | using Application.Boundaries.CloseAccount; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | public sealed class CloseAccountPresenter : IOutputPort 7 | { 8 | public IActionResult ViewModel { get; private set; } 9 | 10 | public void NotFound(string message) 11 | { 12 | ViewModel = new NotFoundObjectResult(message); 13 | } 14 | 15 | public void Standard(CloseAccountOutput closeAccountOutput) 16 | { 17 | ViewModel = new OkObjectResult(closeAccountOutput.AccountId); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/Withdraw/WithdrawRequest.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.Withdraw 2 | { 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | /// 7 | /// Withdraw Request. 8 | /// 9 | public class WithdrawRequest 10 | { 11 | /// 12 | /// Gets or sets the Account ID. 13 | /// 14 | [Required] 15 | public Guid AccountId { get; set; } 16 | 17 | /// 18 | /// Gets or sets the amount to withdraw. 19 | /// 20 | [Required] 21 | public decimal Amount { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /test/AcceptanceTests/HttpClientExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ComponentTests 2 | { 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | 6 | public static class HttpClientExtensions 7 | { 8 | public static async Task PatchAsync(this HttpClient client, string requestUri, HttpContent content) 9 | { 10 | var method = new HttpMethod("PATCH"); 11 | var request = new HttpRequestMessage(method, requestUri) 12 | { 13 | Content = content, 14 | }; 15 | 16 | var response = await client.SendAsync(request); 17 | return response; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/CloseAccount/CloseAccountResponse.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.CloseAccount 2 | { 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | using Application.Boundaries.CloseAccount; 6 | 7 | /// 8 | /// Close Account Response. 9 | /// 10 | public sealed class CloseAccountResponse 11 | { 12 | public CloseAccountResponse(CloseAccountOutput output) 13 | { 14 | AccountId = output.AccountId.ToGuid(); 15 | } 16 | 17 | /// 18 | /// Gets account ID. 19 | /// 20 | [Required] 21 | public Guid AccountId { get; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Infrastructure/EntityFrameworkDataAccess/Entities/Debit.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.EntityFrameworkDataAccess 2 | { 3 | using System; 4 | using Domain.Accounts; 5 | using Domain.Accounts.ValueObjects; 6 | 7 | public class Debit : Domain.Accounts.Debits.Debit 8 | { 9 | public Debit(IAccount account, PositiveMoney amountToWithdraw, DateTime transactionDate) 10 | { 11 | AccountId = account.Id; 12 | Amount = amountToWithdraw; 13 | TransactionDate = transactionDate; 14 | } 15 | 16 | protected Debit() 17 | { 18 | } 19 | 20 | public AccountId AccountId { get; protected set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Infrastructure/EntityFrameworkDataAccess/Entities/Credit.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.EntityFrameworkDataAccess 2 | { 3 | using System; 4 | using Domain.Accounts; 5 | using Domain.Accounts.ValueObjects; 6 | 7 | public class Credit : Domain.Accounts.Credits.Credit 8 | { 9 | public Credit(IAccount account, PositiveMoney amountToDeposit, DateTime transactionDate) 10 | { 11 | AccountId = account.Id; 12 | Amount = amountToDeposit; 13 | TransactionDate = transactionDate; 14 | } 15 | 16 | protected Credit() 17 | { 18 | } 19 | 20 | public AccountId AccountId { get; protected set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Domain/Customers/ICustomerFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Customers 2 | { 3 | using Domain.Customers.ValueObjects; 4 | 5 | /// 6 | /// Customer Entity Factory Domain-Driven Design Pattern. 7 | /// 8 | public interface ICustomerFactory 9 | { 10 | /// 11 | /// Creates a new Customer. 12 | /// 13 | /// SSN. 14 | /// Name. 15 | /// New Customer instance. 16 | ICustomer NewCustomer(SSN ssn, Name name); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Infrastructure/InMemoryDataAccess/Debit.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.InMemoryDataAccess 2 | { 3 | using System; 4 | using Domain.Accounts; 5 | using Domain.Accounts.ValueObjects; 6 | 7 | public class Debit : Domain.Accounts.Debits.Debit 8 | { 9 | public Debit( 10 | IAccount account, 11 | PositiveMoney amountToWithdraw, 12 | DateTime transactionDate) 13 | { 14 | AccountId = account.Id; 15 | Amount = amountToWithdraw; 16 | TransactionDate = transactionDate; 17 | } 18 | 19 | protected Debit() 20 | { 21 | } 22 | 23 | public AccountId AccountId { get; protected set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Infrastructure/InMemoryDataAccess/Credit.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.InMemoryDataAccess 2 | { 3 | using System; 4 | using Domain.Accounts; 5 | using Domain.Accounts.ValueObjects; 6 | 7 | public class Credit : Domain.Accounts.Credits.Credit 8 | { 9 | public Credit( 10 | IAccount account, 11 | PositiveMoney amountToDeposit, 12 | DateTime transactionDate) 13 | { 14 | AccountId = account.Id; 15 | Amount = amountToDeposit; 16 | TransactionDate = transactionDate; 17 | } 18 | 19 | protected Credit() 20 | { 21 | } 22 | 23 | public AccountId AccountId { get; protected set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Domain/Security/IUser.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Security 2 | { 3 | using Domain.Customers.ValueObjects; 4 | using Domain.Security.ValueObjects; 5 | 6 | /// 7 | /// User Aggregate Root Domain-Driven Design Pattern. 8 | /// 9 | public interface IUser 10 | { 11 | /// 12 | /// Gets the External UserId. 13 | /// 14 | ExternalUserId ExternalUserId { get; } 15 | 16 | /// 17 | /// Gets the CustomerId. 18 | /// 19 | CustomerId CustomerId { get; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Infrastructure/InMemoryDataAccess/Services/UserService.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.InMemoryDataAccess.Services 2 | { 3 | using System; 4 | using Domain.Customers.ValueObjects; 5 | using Domain.Security.Services; 6 | using Domain.Security.ValueObjects; 7 | 8 | public sealed class UserService : IUserService 9 | { 10 | public CustomerId? GetCustomerId() 11 | { 12 | return new CustomerId(Guid.NewGuid()); 13 | } 14 | 15 | public ExternalUserId GetExternalUserId() 16 | { 17 | return new ExternalUserId("github/ivanpaulovich"); 18 | } 19 | 20 | public Name GetUserName() 21 | { 22 | return new Name("Ivan Paulovich"); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.background": "#1f6fd0", 4 | "activityBar.activeBorder": "#ee90bb", 5 | "activityBar.foreground": "#e7e7e7", 6 | "activityBar.inactiveForeground": "#e7e7e799", 7 | "activityBarBadge.background": "#ee90bb", 8 | "activityBarBadge.foreground": "#15202b", 9 | "titleBar.activeBackground": "#1857a4", 10 | "titleBar.inactiveBackground": "#1857a499", 11 | "titleBar.activeForeground": "#e7e7e7", 12 | "titleBar.inactiveForeground": "#e7e7e799", 13 | "statusBar.background": "#1857a4", 14 | "statusBarItem.hoverBackground": "#1f6fd0", 15 | "statusBar.foreground": "#e7e7e7" 16 | }, 17 | "peacock.color": "#1857a4" 18 | } -------------------------------------------------------------------------------- /src/Domain/Security/IUserFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Security 2 | { 3 | using Domain.Customers.ValueObjects; 4 | using Domain.Security.ValueObjects; 5 | 6 | /// 7 | /// User Entity Factory Domain-Driven Design Pattern. 8 | /// 9 | public interface IUserFactory 10 | { 11 | /// 12 | /// Creates new User. 13 | /// 14 | /// Customer object. 15 | /// ExternalUserId. 16 | /// New User instance. 17 | IUser NewUser(CustomerId customer, ExternalUserId externalUserId); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Application/Boundaries/CloseAccount/CloseAccountInput.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.CloseAccount 2 | { 3 | using Domain.Accounts.ValueObjects; 4 | 5 | /// 6 | /// Close Account Input Message. 7 | /// 8 | public sealed class CloseAccountInput : IUseCaseInput 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// Account Id. 14 | public CloseAccountInput(AccountId accountId) 15 | { 16 | this.AccountId = accountId; 17 | } 18 | 19 | /// 20 | /// Gets AccountId. 21 | /// 22 | public AccountId AccountId { get; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Infrastructure/InMemoryDataAccess/Customer.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.InMemoryDataAccess 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Domain.Accounts.ValueObjects; 6 | using Domain.Customers; 7 | using Domain.Customers.ValueObjects; 8 | 9 | public class Customer : Domain.Customers.Customer 10 | { 11 | public Customer() 12 | { 13 | } 14 | 15 | public Customer(SSN ssn, Name name) 16 | { 17 | Id = new CustomerId(Guid.NewGuid()); 18 | SSN = ssn; 19 | Name = name; 20 | } 21 | 22 | public void LoadAccounts(IEnumerable accounts) 23 | { 24 | Accounts = new AccountCollection(); 25 | Accounts.Add(accounts); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Application/Boundaries/CloseAccount/CloseAccountOutput.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.CloseAccount 2 | { 3 | using Domain.Accounts; 4 | using Domain.Accounts.ValueObjects; 5 | 6 | /// 7 | /// Close Account Output Message. 8 | /// 9 | public sealed class CloseAccountOutput : IUseCaseOutput 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// IAccount object. 15 | public CloseAccountOutput(IAccount account) 16 | { 17 | this.AccountId = account.Id; 18 | } 19 | 20 | /// 21 | /// Gets AccountId. 22 | /// 23 | public AccountId AccountId { get; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Application/Boundaries/GetAccountDetails/GetAccountDetailsInput.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.GetAccountDetails 2 | { 3 | using Domain.Accounts.ValueObjects; 4 | 5 | /// 6 | /// Get Account Details Input Message. 7 | /// 8 | public sealed class GetAccountDetailsInput : IUseCaseInput 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// Account Id. 14 | public GetAccountDetailsInput(AccountId accountId) 15 | { 16 | this.AccountId = accountId; 17 | } 18 | 19 | /// 20 | /// Gets the AccountId. 21 | /// 22 | public AccountId AccountId { get; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/WebApi/Filters/BusinessExceptionFilter.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Filters 2 | { 3 | using Domain; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.Filters; 6 | 7 | public sealed class BusinessExceptionFilter : IExceptionFilter 8 | { 9 | public void OnException(ExceptionContext context) 10 | { 11 | if (context.Exception is DomainException) 12 | { 13 | var problemDetails = new ProblemDetails 14 | { 15 | Status = 400, 16 | Title = "Bad Request", 17 | Detail = context.Exception.Message, 18 | }; 19 | 20 | context.Result = new BadRequestObjectResult(problemDetails); 21 | context.Exception = null; 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Domain/Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | enable 6 | true 7 | 8.0 8 | true 9 | 10 | 11 | 12 | true 13 | 14 | 15 | 16 | Domain.ruleset 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Infrastructure/InMemoryDataAccess/Presenters/DepositPresenter.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.InMemoryDataAccess.Presenters 2 | { 3 | using System.Collections.ObjectModel; 4 | using Application.Boundaries.Deposit; 5 | 6 | public sealed class DepositPresenter : IOutputPort 7 | { 8 | public DepositPresenter() 9 | { 10 | Deposits = new Collection(); 11 | NotFounds = new Collection(); 12 | } 13 | 14 | public Collection Deposits { get; } 15 | 16 | public Collection NotFounds { get; } 17 | 18 | public void Standard(DepositOutput output) 19 | { 20 | Deposits.Add(output); 21 | } 22 | 23 | public void NotFound(string message) 24 | { 25 | NotFounds.Add(message); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Domain/Security/Services/IUserService.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Security.Services 2 | { 3 | using Domain.Customers.ValueObjects; 4 | using Domain.Security.ValueObjects; 5 | 6 | /// 7 | /// User Service. 8 | /// 9 | public interface IUserService 10 | { 11 | /// 12 | /// Get Customer Id. 13 | /// 14 | /// CustomerId. 15 | CustomerId? GetCustomerId(); 16 | 17 | /// 18 | /// Get External User Id. 19 | /// 20 | /// External User Id. 21 | ExternalUserId GetExternalUserId(); 22 | 23 | /// 24 | /// Get User Name. 25 | /// 26 | /// User Name. 27 | Name GetUserName(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Application/Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | enable 6 | true 7 | 8.0 8 | true 9 | 10 | 11 | 12 | true 13 | 14 | 15 | 16 | Application.ruleset 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Infrastructure/InMemoryDataAccess/Presenters/TransferPresenter.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.InMemoryDataAccess.Presenters 2 | { 3 | using System.Collections.ObjectModel; 4 | using Application.Boundaries.Transfer; 5 | 6 | public sealed class TransferPresenter : IOutputPort 7 | { 8 | public TransferPresenter() 9 | { 10 | Transfers = new Collection(); 11 | NotFounds = new Collection(); 12 | } 13 | 14 | public Collection Transfers { get; } 15 | 16 | public Collection NotFounds { get; } 17 | 18 | public void Standard(TransferOutput output) 19 | { 20 | Transfers.Add(output); 21 | } 22 | 23 | public void NotFound(string message) 24 | { 25 | NotFounds.Add(message); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /test/UnitTests/EntitiesTests/CustomerTests.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTests.EntitiesTests 2 | { 3 | using Domain.Customers; 4 | using Domain.Customers.ValueObjects; 5 | using Xunit; 6 | 7 | public class CustomerTests 8 | { 9 | [Fact] 10 | public void Customer_Should_Be_Registered_With_1_Account() 11 | { 12 | var entityFactory = new Infrastructure.InMemoryDataAccess.EntityFactory(); 13 | 14 | // Arrange 15 | ICustomer sut = entityFactory.NewCustomer( 16 | new SSN("198608179922"), 17 | new Name("Ivan Paulovich")); 18 | 19 | var account = entityFactory.NewAccount(sut.Id); 20 | 21 | // Act 22 | sut.Register(account.Id); 23 | 24 | // Assert 25 | Assert.Single(sut.Accounts.GetAccountIds()); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Infrastructure/EntityFrameworkDataAccess/Entities/Customer.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.EntityFrameworkDataAccess 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Domain.Accounts.ValueObjects; 6 | using Domain.Customers; 7 | using Domain.Customers.ValueObjects; 8 | 9 | public class Customer : Domain.Customers.Customer 10 | { 11 | public Customer( 12 | SSN ssn, 13 | Name name) 14 | { 15 | Id = new CustomerId(Guid.NewGuid()); 16 | SSN = ssn; 17 | Name = name; 18 | } 19 | 20 | protected Customer() 21 | { 22 | } 23 | 24 | public void LoadAccounts(IEnumerable accounts) 25 | { 26 | Accounts = new AccountCollection(); 27 | Accounts.Add(accounts); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/Transfer/TransferRequest.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.Transfer 2 | { 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | /// 7 | /// Transfer Request. 8 | /// 9 | public sealed class TransferRequest 10 | { 11 | /// 12 | /// Gets or sets origin Account ID. 13 | /// 14 | [Required] 15 | public Guid OriginAccountId { get; set; } 16 | 17 | /// 18 | /// Gets or sets destination Account ID. 19 | /// 20 | [Required] 21 | public Guid DestinationAccountId { get; set; } 22 | 23 | /// 24 | /// Gets or sets amount Transferred. 25 | /// 26 | [Required] 27 | public decimal Amount { get; set; } 28 | } 29 | } -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/Transfer/TransferResponse.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.Transfer 2 | { 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | public sealed class TransferResponse 7 | { 8 | public TransferResponse(decimal amount, string description, DateTime transactionDate, decimal updatedBalance) 9 | { 10 | Amount = amount; 11 | Description = description; 12 | TransactionDate = transactionDate; 13 | UpdateBalance = updatedBalance; 14 | } 15 | 16 | [Required] 17 | public decimal Amount { get; } 18 | 19 | [Required] 20 | public string Description { get; } 21 | 22 | [Required] 23 | public DateTime TransactionDate { get; } 24 | 25 | [Required] 26 | public decimal UpdateBalance { get; } 27 | } 28 | } -------------------------------------------------------------------------------- /src/WebApi/DependencyInjection/UserInterfaceV2Extensions.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.DependencyInjection 2 | { 3 | using Domain.Accounts; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using WebApi.UseCases.V2.GetAccountDetails; 6 | 7 | public static class UserInterfaceV2Extensions 8 | { 9 | public static IServiceCollection AddPresentersV2(this IServiceCollection services) 10 | { 11 | services.AddScoped(); 12 | services.AddScoped( 13 | ctx => new Application.UseCases.GetAccountDetails( 14 | ctx.GetRequiredService(), 15 | ctx.GetRequiredService())); 16 | 17 | return services; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Infrastructure/InMemoryDataAccess/Presenters/RegisterPresenter.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.InMemoryDataAccess.Presenters 2 | { 3 | using System.Collections.ObjectModel; 4 | using Application.Boundaries.Register; 5 | 6 | public sealed class RegisterPresenter : IOutputPort 7 | { 8 | public RegisterPresenter() 9 | { 10 | Registers = new Collection(); 11 | AlreadyRegistered = new Collection(); 12 | } 13 | 14 | public Collection Registers { get; } 15 | 16 | public Collection AlreadyRegistered { get; } 17 | 18 | public void Standard(RegisterOutput output) 19 | { 20 | Registers.Add(output); 21 | } 22 | 23 | public void CustomerAlreadyRegistered(string message) 24 | { 25 | AlreadyRegistered.Add(message); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Infrastructure/InMemoryDataAccess/Presenters/CloseAccountPresenter.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.InMemoryDataAccess.Presenters 2 | { 3 | using System.Collections.ObjectModel; 4 | using Application.Boundaries.CloseAccount; 5 | 6 | public sealed class CloseAccountPresenter : IOutputPort 7 | { 8 | public CloseAccountPresenter() 9 | { 10 | ClosedAccounts = new Collection(); 11 | NotFounds = new Collection(); 12 | } 13 | 14 | public Collection ClosedAccounts { get; } 15 | 16 | public Collection NotFounds { get; } 17 | 18 | public void Standard(CloseAccountOutput output) 19 | { 20 | ClosedAccounts.Add(output); 21 | } 22 | 23 | public void NotFound(string message) 24 | { 25 | NotFounds.Add(message); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /test/UnitTests/InputValidationTests/CloseAccountInputValidationTests.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTests.InputValidationTests 2 | { 3 | using System; 4 | using Application.Boundaries.CloseAccount; 5 | using Domain.Accounts.ValueObjects; 6 | using Xunit; 7 | 8 | public sealed class CloseAccountInputValidationTests 9 | { 10 | [Fact] 11 | public void GivenEmptyAccountId_InputNotCreated_ThrowsInputValidationException() 12 | { 13 | var actualEx = Assert.Throws( 14 | () => new AccountId(Guid.Empty)); 15 | Assert.Contains("accountId", actualEx.Message); 16 | } 17 | 18 | [Fact] 19 | public void GivenValidData_InputCreated() 20 | { 21 | var actual = new CloseAccountInput( 22 | new AccountId(Guid.NewGuid())); 23 | Assert.NotNull(actual); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Domain/Security/IUserRepository.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Security 2 | { 3 | using System.Threading.Tasks; 4 | using Domain.Security.ValueObjects; 5 | 6 | /// 7 | /// User Repository Domain-Driven Design Pattern. 8 | /// 9 | public interface IUserRepository 10 | { 11 | /// 12 | /// Gets User. 13 | /// 14 | /// External UserId. 15 | /// User. 16 | Task Get(ExternalUserId externalUserId); 17 | 18 | /// 19 | /// Adds the User. 20 | /// 21 | /// User instance. 22 | /// Task. 23 | Task Add(IUser user); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Infrastructure/EntityFrameworkDataAccess/MangaContext.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.EntityFrameworkDataAccess 2 | { 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | public sealed class MangaContext : DbContext 6 | { 7 | public MangaContext(DbContextOptions options) 8 | : base(options) 9 | { 10 | } 11 | 12 | public DbSet Users { get; set; } = null!; 13 | 14 | public DbSet Accounts { get; set; } = null!; 15 | 16 | public DbSet Customers { get; set; } = null!; 17 | 18 | public DbSet Credits { get; set; } = null!; 19 | 20 | public DbSet Debits { get; set; } = null!; 21 | 22 | protected override void OnModelCreating(ModelBuilder builder) 23 | { 24 | builder.ApplyConfigurationsFromAssembly(typeof(MangaContext).Assembly); 25 | SeedData.Seed(builder); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/WebApi/DependencyInjection/HttpMetricsExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.DependencyInjection 2 | { 3 | using Microsoft.AspNetCore.Builder; 4 | using Prometheus; 5 | 6 | public static class HttpMetricsExtensions 7 | { 8 | public static IApplicationBuilder UseMangaHttpMetrics(this IApplicationBuilder appBuilder) 9 | { 10 | return appBuilder.UseHttpMetrics(options => 11 | { 12 | options.RequestDuration.Enabled = false; 13 | options.InProgress.Enabled = false; 14 | options.RequestCount.Counter = Metrics.CreateCounter( 15 | "http_requests_total", 16 | "HTTP Requests Total", 17 | new CounterConfiguration 18 | { 19 | LabelNames = new[] { "controller", "method", "code" }, 20 | }); 21 | }); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/Deposit/DepositPresenter.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.Deposit 2 | { 3 | using Application.Boundaries.Deposit; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | public sealed class DepositPresenter : IOutputPort 7 | { 8 | public IActionResult ViewModel { get; private set; } 9 | 10 | public void NotFound(string message) 11 | { 12 | ViewModel = new NotFoundObjectResult(message); 13 | } 14 | 15 | public void Standard(DepositOutput depositOutput) 16 | { 17 | var depositResponse = new DepositResponse( 18 | depositOutput.Transaction.Amount.ToMoney().ToDecimal(), 19 | depositOutput.Transaction.Description, 20 | depositOutput.Transaction.TransactionDate, 21 | depositOutput.UpdatedBalance.ToDecimal()); 22 | ViewModel = new ObjectResult(depositResponse); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Domain/Customers/ICustomer.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Customers 2 | { 3 | using Domain.Accounts.ValueObjects; 4 | using Domain.Customers.ValueObjects; 5 | 6 | /// 7 | /// Customer Aggregate Root Domain-Driven Design Pattern. 8 | /// 9 | public interface ICustomer 10 | { 11 | /// 12 | /// Gets the CustomerId. 13 | /// 14 | CustomerId Id { get; } 15 | 16 | /// 17 | /// Gets the Accounts. 18 | /// 19 | AccountCollection Accounts { get; } 20 | 21 | /// 22 | /// Register the Account into the Customer. 23 | /// 24 | /// Account Id. 25 | void Register(AccountId accountId); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Infrastructure/InMemoryDataAccess/Presenters/GetAccountDetailsPresenter.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.InMemoryDataAccess.Presenters 2 | { 3 | using System.Collections.ObjectModel; 4 | using Application.Boundaries.GetAccountDetails; 5 | 6 | public sealed class GetAccountDetailsPresenter : IOutputPort 7 | { 8 | public GetAccountDetailsPresenter() 9 | { 10 | GetAccountDetails = new Collection(); 11 | NotFounds = new Collection(); 12 | } 13 | 14 | public Collection GetAccountDetails { get; } 15 | 16 | public Collection NotFounds { get; } 17 | 18 | public void Standard(GetAccountDetailsOutput output) 19 | { 20 | GetAccountDetails.Add(output); 21 | } 22 | 23 | public void NotFound(string message) 24 | { 25 | NotFounds.Add(message); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Infrastructure/InMemoryDataAccess/Presenters/GetCustomerDetailsPresenter.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.InMemoryDataAccess.Presenters 2 | { 3 | using System.Collections.ObjectModel; 4 | using Application.Boundaries.GetCustomerDetails; 5 | 6 | public sealed class GetCustomerDetailsPresenter : IOutputPort 7 | { 8 | public GetCustomerDetailsPresenter() 9 | { 10 | GetCustomerDetails = new Collection(); 11 | NotFounds = new Collection(); 12 | } 13 | 14 | public Collection GetCustomerDetails { get; } 15 | 16 | public Collection NotFounds { get; } 17 | 18 | public void Standard(GetCustomerDetailsOutput output) 19 | { 20 | GetCustomerDetails.Add(output); 21 | } 22 | 23 | public void NotFound(string message) 24 | { 25 | NotFounds.Add(message); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/Withdraw/WithdrawResponse.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.Withdraw 2 | { 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | public sealed class WithdrawResponse 7 | { 8 | public WithdrawResponse( 9 | decimal amount, 10 | string description, 11 | DateTime transactionDate, 12 | decimal updatedBalance) 13 | { 14 | Amount = amount; 15 | Description = description; 16 | TransactionDate = transactionDate; 17 | UpdateBalance = updatedBalance; 18 | } 19 | 20 | [Required] 21 | public decimal Amount { get; } 22 | 23 | [Required] 24 | public string Description { get; } 25 | 26 | [Required] 27 | public DateTime TransactionDate { get; } 28 | 29 | [Required] 30 | public decimal UpdateBalance { get; } 31 | } 32 | } -------------------------------------------------------------------------------- /src/WebApi/DependencyInjection/FeatureFlags/FeatureFlagsExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.DependencyInjection.FeatureFlags 2 | { 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.FeatureManagement; 6 | 7 | public static class FeatureFlagsExtensions 8 | { 9 | public static IServiceCollection AddFeatureFlags(this IServiceCollection services, IConfiguration configuration) 10 | { 11 | services.AddFeatureManagement(configuration); 12 | 13 | var featureManager = services.BuildServiceProvider() 14 | .GetRequiredService(); 15 | 16 | services.AddMvc() 17 | .ConfigureApplicationPartManager(apm => 18 | apm.FeatureProviders.Add( 19 | new CustomControllerFeatureProvider(featureManager))); 20 | 21 | return services; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/UnitTests/InputValidationTests/GetAccountDetailsInputValidationTests.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTests.InputValidationTests 2 | { 3 | using System; 4 | using Application.Boundaries.GetAccountDetails; 5 | using Domain.Accounts.ValueObjects; 6 | using Xunit; 7 | 8 | public sealed class GetAccountDetailsInputValidationTests 9 | { 10 | [Fact] 11 | public void GivenEmptyAccountId_InputNotCreated_ThrowsInputValidationException() 12 | { 13 | var actualEx = Assert.Throws( 14 | () => new GetAccountDetailsInput( 15 | new AccountId(Guid.Empty))); 16 | Assert.Contains("accountId", actualEx.Message); 17 | } 18 | 19 | [Fact] 20 | public void GivenValidData_InputCreated() 21 | { 22 | var actual = new GetAccountDetailsInput( 23 | new AccountId(Guid.NewGuid())); 24 | Assert.NotNull(actual); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Infrastructure/InMemoryDataAccess/Account.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.InMemoryDataAccess 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Domain.Accounts.Credits; 6 | using Domain.Accounts.Debits; 7 | using Domain.Accounts.ValueObjects; 8 | using Domain.Customers.ValueObjects; 9 | 10 | public class Account : Domain.Accounts.Account 11 | { 12 | public Account(CustomerId customerId) 13 | { 14 | Id = new AccountId(Guid.NewGuid()); 15 | CustomerId = customerId; 16 | } 17 | 18 | protected Account() 19 | { 20 | } 21 | 22 | public CustomerId CustomerId { get; protected set; } 23 | 24 | public void Load(IList credits, IList debits) 25 | { 26 | Credits = new CreditsCollection(); 27 | Credits.Add(credits); 28 | 29 | Debits = new DebitsCollection(); 30 | Debits.Add(debits); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/Transfer/TransferPresenter.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.Transfer 2 | { 3 | using Application.Boundaries.Transfer; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | public sealed class TransferPresenter : IOutputPort 7 | { 8 | public IActionResult ViewModel { get; private set; } 9 | 10 | public void NotFound(string message) 11 | { 12 | ViewModel = new NotFoundObjectResult(message); 13 | } 14 | 15 | public void Standard(TransferOutput transferOutput) 16 | { 17 | var transferResponse = new 18 | { 19 | Amount = transferOutput.Transaction.Amount, 20 | Description = transferOutput.Transaction.Description, 21 | TransactionDate = transferOutput.Transaction.TransactionDate, 22 | UpdatedBalance = transferOutput.UpdatedBalance, 23 | }; 24 | ViewModel = new ObjectResult(transferResponse); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | pool: 2 | vmImage: 'windows-2019' 3 | 4 | variables: 5 | buildConfiguration: 'Release' 6 | 7 | steps: 8 | - task: UseDotNet@2 9 | inputs: 10 | packageType: 'sdk' 11 | useGlobalJson: true 12 | 13 | - task: SonarCloudPrepare@1 14 | inputs: 15 | SonarCloud: 'Clean-Architecture' 16 | organization: 'piraces' 17 | scannerMode: 'MSBuild' 18 | projectKey: 'Clean-Architecture-tests' 19 | projectName: 'Clean-Architecture' 20 | 21 | - script: dotnet restore 22 | displayName: 'dotnet restore' 23 | 24 | - script: dotnet build --configuration $(buildConfiguration) 25 | displayName: 'dotnet build $(buildConfiguration)' 26 | 27 | - task: DotNetCoreCLI@2 28 | displayName: 'dotnet test $(buildConfiguration)' 29 | inputs: 30 | command: test 31 | projects: '**/*Tests/*.csproj' 32 | arguments: '--configuration $(buildConfiguration) --collect "Code coverage"' 33 | 34 | - task: SonarCloudAnalyze@1 35 | 36 | - task: SonarCloudPublish@1 37 | inputs: 38 | pollingTimeoutSec: '300' 39 | -------------------------------------------------------------------------------- /test/UnitTests/InputValidationTests/DepositInputValidationTests.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTests.InputValidationTests 2 | { 3 | using System; 4 | using Application.Boundaries.Deposit; 5 | using Domain.Accounts.ValueObjects; 6 | using Xunit; 7 | 8 | public sealed class DepositInputValidationTests 9 | { 10 | [Fact] 11 | public void GivenEmptyAccountId_InputNotCreated_ThrowsInputValidationException() 12 | { 13 | var actualEx = Assert.Throws( 14 | () => new DepositInput( 15 | new AccountId(Guid.Empty), 16 | new PositiveMoney(10))); 17 | Assert.Contains("accountId", actualEx.Message); 18 | } 19 | 20 | [Fact] 21 | public void GivenValidData_InputCreated() 22 | { 23 | var actual = new DepositInput( 24 | new AccountId(Guid.NewGuid()), 25 | new PositiveMoney(10)); 26 | Assert.NotNull(actual); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Infrastructure/EntityFrameworkDataAccess/Entities/Account.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.EntityFrameworkDataAccess 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Domain.Accounts.Credits; 6 | using Domain.Accounts.Debits; 7 | using Domain.Accounts.ValueObjects; 8 | using Domain.Customers; 9 | using Domain.Customers.ValueObjects; 10 | 11 | public class Account : Domain.Accounts.Account 12 | { 13 | public Account(CustomerId customerId) 14 | { 15 | Id = new AccountId(Guid.NewGuid()); 16 | CustomerId = customerId; 17 | } 18 | 19 | protected Account() 20 | { 21 | } 22 | 23 | public CustomerId CustomerId { get; protected set; } 24 | 25 | public void Load(IList credits, IList debits) 26 | { 27 | Credits = new CreditsCollection(); 28 | Credits.Add(credits); 29 | 30 | Debits = new DebitsCollection(); 31 | Debits.Add(debits); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/UnitTests/InputValidationTests/WithdrawInputValidationTests.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTests.InputValidationTests 2 | { 3 | using System; 4 | using Application.Boundaries.Withdraw; 5 | using Domain.Accounts; 6 | using Domain.Accounts.ValueObjects; 7 | using Xunit; 8 | 9 | public sealed class WithdrawInputValidationTests 10 | { 11 | [Fact] 12 | public void GivenEmptyAccountId_InputNotCreated_ThrowsInputValidationException() 13 | { 14 | var actualEx = Assert.Throws( 15 | () => new WithdrawInput( 16 | new AccountId(Guid.Empty), 17 | new PositiveMoney(10))); 18 | Assert.Contains("accountId", actualEx.Message); 19 | } 20 | 21 | [Fact] 22 | public void GivenValidData_InputCreated() 23 | { 24 | var actual = new WithdrawInput( 25 | new AccountId(Guid.NewGuid()), 26 | new PositiveMoney(10)); 27 | Assert.NotNull(actual); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Withdraw/WithdrawInput.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Withdraw 2 | { 3 | using Domain.Accounts.ValueObjects; 4 | 5 | /// 6 | /// Withdraw Input Message. 7 | /// 8 | public sealed class WithdrawInput : IUseCaseInput 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// Account Id. 14 | /// Amount. 15 | public WithdrawInput( 16 | AccountId accountId, 17 | PositiveMoney amount) 18 | { 19 | this.AccountId = accountId; 20 | this.Amount = amount; 21 | } 22 | 23 | /// 24 | /// Gets the Account Id. 25 | /// 26 | public AccountId AccountId { get; } 27 | 28 | /// 29 | /// Gets the Amount. 30 | /// 31 | public PositiveMoney Amount { get; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Deposit/DepositInput.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Deposit 2 | { 3 | using Domain.Accounts.ValueObjects; 4 | 5 | /// 6 | /// Deposit Input Message. 7 | /// 8 | public sealed class DepositInput : IUseCaseInput 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// AccountId. 14 | /// Positive amount to deposit. 15 | public DepositInput( 16 | AccountId accountId, 17 | PositiveMoney amount) 18 | { 19 | this.AccountId = accountId; 20 | this.Amount = amount; 21 | } 22 | 23 | /// 24 | /// Gets AccountId. 25 | /// 26 | public AccountId AccountId { get; } 27 | 28 | /// 29 | /// Gets the Amount. 30 | /// 31 | public PositiveMoney Amount { get; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/Withdraw/WithdrawPresenter.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.Withdraw 2 | { 3 | using Application.Boundaries.Withdraw; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | public sealed class WithdrawPresenter : IOutputPort 7 | { 8 | public IActionResult ViewModel { get; private set; } 9 | 10 | public void NotFound(string message) 11 | { 12 | ViewModel = new NotFoundObjectResult(message); 13 | } 14 | 15 | public void OutOfBalance(string message) 16 | { 17 | ViewModel = new BadRequestObjectResult(message); 18 | } 19 | 20 | public void Standard(WithdrawOutput withdrawOutput) 21 | { 22 | var withdrawResponse = new WithdrawResponse( 23 | withdrawOutput.Transaction.Amount.ToMoney().ToDecimal(), 24 | withdrawOutput.Transaction.Description, 25 | withdrawOutput.Transaction.TransactionDate, 26 | withdrawOutput.UpdatedBalance.ToDecimal()); 27 | ViewModel = new ObjectResult(withdrawResponse); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/WebApi/ViewModels/TransactionModel.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.ViewModels 2 | { 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | using Domain.Accounts.ValueObjects; 6 | 7 | /// 8 | /// Transaction. 9 | /// 10 | public sealed class TransactionModel 11 | { 12 | public TransactionModel(PositiveMoney amount, string description, DateTime transactionDate) 13 | { 14 | Amount = amount.ToMoney().ToDecimal(); 15 | Description = description; 16 | TransactionDate = transactionDate; 17 | } 18 | 19 | /// 20 | /// Gets Amount. 21 | /// 22 | [Required] 23 | public decimal Amount { get; } 24 | 25 | /// 26 | /// Gets Description. 27 | /// 28 | [Required] 29 | public string Description { get; } 30 | 31 | /// 32 | /// Gets Transaction Date. 33 | /// 34 | [Required] 35 | public DateTime TransactionDate { get; } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Infrastructure/EntityFrameworkDataAccess/Configuration/AccountConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.EntityFrameworkDataAccess.Configuration 2 | { 3 | using Domain.Accounts.ValueObjects; 4 | using Domain.Customers.ValueObjects; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 7 | 8 | public class AccountConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.ToTable("Account"); 13 | 14 | builder.Property(b => b.Id) 15 | .HasConversion( 16 | v => v.ToGuid(), 17 | v => new AccountId(v)) 18 | .IsRequired(); 19 | 20 | builder.Property(b => b.CustomerId) 21 | .HasConversion( 22 | v => v.ToGuid(), 23 | v => new CustomerId(v)) 24 | .IsRequired(); 25 | 26 | builder.Ignore(p => p.Credits) 27 | .Ignore(p => p.Debits); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Register/RegisterInput.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Register 2 | { 3 | using Domain.Accounts.ValueObjects; 4 | using Domain.Customers.ValueObjects; 5 | 6 | /// 7 | /// Register Input Message. 8 | /// 9 | public sealed class RegisterInput : IUseCaseInput 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// SSN. 15 | /// Positive amount. 16 | public RegisterInput( 17 | SSN ssn, 18 | PositiveMoney initialAmount) 19 | { 20 | this.SSN = ssn; 21 | this.InitialAmount = initialAmount; 22 | } 23 | 24 | /// 25 | /// Gets the SSN. 26 | /// 27 | public SSN SSN { get; } 28 | 29 | /// 30 | /// Gets the Initial Amount. 31 | /// 32 | public PositiveMoney InitialAmount { get; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Domain/Customers/ValueObjects/Name.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Customers.ValueObjects 2 | { 3 | /// 4 | /// Name Entity Design Pattern. 5 | /// 6 | public readonly struct Name 7 | { 8 | private readonly string _text; 9 | 10 | /// 11 | /// Initializes a new instance of the struct. 12 | /// 13 | /// Name. 14 | public Name(string text) 15 | { 16 | if (string.IsNullOrWhiteSpace(text)) 17 | { 18 | throw new NameShouldNotBeEmptyException($"The {nameof(text)} field is required."); 19 | } 20 | 21 | this._text = text; 22 | } 23 | 24 | /// 25 | /// Converts into string. 26 | /// 27 | /// string. 28 | public override string ToString() 29 | { 30 | return this._text; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Infrastructure/InMemoryDataAccess/Presenters/WithdrawPresenter.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.InMemoryDataAccess.Presenters 2 | { 3 | using System.Collections.ObjectModel; 4 | using Application.Boundaries.Withdraw; 5 | 6 | public sealed class WithdrawPresenter : IOutputPort 7 | { 8 | public WithdrawPresenter() 9 | { 10 | Withdrawals = new Collection(); 11 | NotFounds = new Collection(); 12 | OutOfBalances = new Collection(); 13 | } 14 | 15 | public Collection Withdrawals { get; } 16 | 17 | public Collection NotFounds { get; } 18 | 19 | public Collection OutOfBalances { get; } 20 | 21 | public void Standard(WithdrawOutput output) 22 | { 23 | Withdrawals.Add(output); 24 | } 25 | 26 | public void NotFound(string message) 27 | { 28 | NotFounds.Add(message); 29 | } 30 | 31 | public void OutOfBalance(string message) 32 | { 33 | OutOfBalances.Add(message); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/Infrastructure/EntityFrameworkDataAccess/Configuration/UserConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.EntityFrameworkDataAccess.Configuration 2 | { 3 | using Domain.Customers.ValueObjects; 4 | using Domain.Security.ValueObjects; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 7 | 8 | public class UserConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.ToTable("User"); 13 | 14 | builder.Property(b => b.ExternalUserId) 15 | .HasConversion( 16 | v => v.ToString(), 17 | v => new ExternalUserId(v)) 18 | .IsRequired(); 19 | 20 | builder.Property(b => b.CustomerId) 21 | .HasConversion( 22 | v => v.ToGuid(), 23 | v => new CustomerId(v)) 24 | .IsRequired(); 25 | 26 | builder.HasKey( 27 | c => new { c.ExternalUserId, c.CustomerId } 28 | ); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Infrastructure/EntityFrameworkDataAccess/UnitOfWork.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.EntityFrameworkDataAccess 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using Application.Services; 6 | 7 | public sealed class UnitOfWork : IUnitOfWork, IDisposable 8 | { 9 | private readonly MangaContext _context; 10 | private bool _disposed = false; 11 | 12 | public UnitOfWork(MangaContext context) 13 | { 14 | _context = context; 15 | } 16 | 17 | public async Task Save() 18 | { 19 | int affectedRows = await _context.SaveChangesAsync(); 20 | return affectedRows; 21 | } 22 | 23 | public void Dispose() 24 | { 25 | Dispose(true); 26 | GC.SuppressFinalize(this); 27 | } 28 | 29 | private void Dispose(bool disposing) 30 | { 31 | if (!_disposed) 32 | { 33 | if (disposing) 34 | { 35 | _context.Dispose(); 36 | } 37 | } 38 | 39 | _disposed = true; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Domain/Security/ValueObjects/ExternalUserId.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Security.ValueObjects 2 | { 3 | /// 4 | /// ExternalUserId Entity Design Pattern. 5 | /// 6 | public readonly struct ExternalUserId 7 | { 8 | private readonly string _text; 9 | 10 | /// 11 | /// Initializes a new instance of the struct. 12 | /// 13 | /// External User Id. 14 | public ExternalUserId(string text) 15 | { 16 | if (string.IsNullOrWhiteSpace(text)) 17 | { 18 | throw new ExternalUserIdShouldNotBeEmptyException($"The '{nameof(text)}' field is required."); 19 | } 20 | 21 | this._text = text; 22 | } 23 | 24 | /// 25 | /// Converts into string. 26 | /// 27 | /// String. 28 | public override string ToString() 29 | { 30 | return this._text; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/AcceptanceTests/ComponentTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | false 6 | true 7 | 8 | 9 | 10 | ComponentTests.ruleset 11 | 12 | 13 | 14 | ComponentTests.ruleset 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Domain/Customers/ICustomerRepository.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Customers 2 | { 3 | using System.Threading.Tasks; 4 | using Domain.Customers.ValueObjects; 5 | 6 | /// 7 | /// Customer Repository Domain-Driven Design Pattern. 8 | /// 9 | public interface ICustomerRepository 10 | { 11 | /// 12 | /// Gets the Customer by Id. 13 | /// 14 | /// CustomerId. 15 | /// Customer. 16 | Task GetBy(CustomerId customerId); 17 | 18 | /// 19 | /// Adds the Customer. 20 | /// 21 | /// Customer object. 22 | /// Task. 23 | Task Add(ICustomer customer); 24 | 25 | /// 26 | /// Updates the Customer. 27 | /// 28 | /// Customer object. 29 | /// Task. 30 | Task Update(ICustomer customer); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Domain/Customers/Customer.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Customers 2 | { 3 | using Domain.Accounts.ValueObjects; 4 | using Domain.Customers.ValueObjects; 5 | 6 | /// 7 | public abstract class Customer : ICustomer 8 | { 9 | /// 10 | /// Initializes a new instance of the class. 11 | /// 12 | public Customer() 13 | { 14 | this.Accounts = new AccountCollection(); 15 | } 16 | 17 | /// 18 | public CustomerId Id { get; protected set; } 19 | 20 | /// 21 | /// Gets or sets Name. 22 | /// 23 | public Name Name { get; protected set; } 24 | 25 | /// 26 | /// Gets or sets SSN. 27 | /// 28 | public SSN SSN { get; protected set; } 29 | 30 | /// 31 | public AccountCollection Accounts { get; protected set; } 32 | 33 | /// 34 | public void Register(AccountId accountId) 35 | { 36 | this.Accounts ??= new AccountCollection(); 37 | this.Accounts.Add(accountId); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Infrastructure/EntityFrameworkDataAccess/Configuration/CustomerConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.EntityFrameworkDataAccess.Configuration 2 | { 3 | using Domain.Customers.ValueObjects; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | public class CustomerConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder 10 | builder) 11 | { 12 | builder.ToTable("Customer"); 13 | 14 | builder.Property(b => b.SSN) 15 | .HasConversion( 16 | v => v.ToString(), 17 | v => new SSN(v)) 18 | .IsRequired(); 19 | 20 | builder.Property(b => b.Name) 21 | .HasConversion( 22 | v => v.ToString(), 23 | v => new Name(v)) 24 | .IsRequired(); 25 | 26 | builder.Property(b => b.Id) 27 | .HasConversion( 28 | v => v.ToGuid(), 29 | v => new CustomerId(v)) 30 | .IsRequired(); 31 | 32 | builder.Ignore(p => p.Accounts); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Infrastructure/EntityFrameworkDataAccess/ContextFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.EntityFrameworkDataAccess 2 | { 3 | using System.IO; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Design; 6 | using Microsoft.Extensions.Configuration; 7 | 8 | public sealed class ContextFactory : IDesignTimeDbContextFactory 9 | { 10 | public MangaContext CreateDbContext(string[] args) 11 | { 12 | string connectionString = ReadDefaultConnectionStringFromAppSettings(); 13 | 14 | var builder = new DbContextOptionsBuilder(); 15 | builder.UseSqlServer(connectionString); 16 | return new MangaContext(builder.Options); 17 | } 18 | 19 | private string ReadDefaultConnectionStringFromAppSettings() 20 | { 21 | IConfigurationRoot configuration = new ConfigurationBuilder() 22 | .SetBasePath(Directory.GetCurrentDirectory()) 23 | .AddJsonFile("appsettings.Production.json") 24 | .Build(); 25 | 26 | string connectionString = configuration.GetConnectionString("DefaultConnection"); 27 | return connectionString; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Infrastructure/InMemoryDataAccess/Repositories/UserRepository.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.InMemoryDataAccess.Repositories 2 | { 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Domain.Security; 6 | using Domain.Security.ValueObjects; 7 | 8 | public sealed class UserRepository : IUserRepository 9 | { 10 | private readonly MangaContext _context; 11 | 12 | public UserRepository(MangaContext context) 13 | { 14 | _context = context; 15 | } 16 | 17 | public async Task Add(IUser user) 18 | { 19 | _context.Users.Add((InMemoryDataAccess.User)user); 20 | await Task.CompletedTask; 21 | } 22 | 23 | public async Task Get(ExternalUserId externalUserId) 24 | { 25 | User user = _context.Users 26 | .Where(e => e.ExternalUserId.Equals(externalUserId)) 27 | .SingleOrDefault(); 28 | 29 | if (user is null) 30 | { 31 | throw new UserNotFoundException($"The user {externalUserId} does not exist or is not processed yet."); 32 | } 33 | 34 | return await Task.FromResult(user); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/WebApi/ViewModels/AccountDetailsModel.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.ViewModels 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | using Domain.Accounts.ValueObjects; 7 | 8 | /// 9 | /// Account Details. 10 | /// 11 | public sealed class AccountDetailsModel 12 | { 13 | public AccountDetailsModel( 14 | AccountId accountId, 15 | Money currentBalance, 16 | List transactions) 17 | { 18 | AccountId = accountId.ToGuid(); 19 | CurrentBalance = currentBalance.ToDecimal(); 20 | Transactions = transactions; 21 | } 22 | 23 | /// 24 | /// Gets account ID. 25 | /// 26 | [Required] 27 | public Guid AccountId { get; } 28 | 29 | /// 30 | /// Gets current Balance. 31 | /// 32 | [Required] 33 | public decimal CurrentBalance { get; } 34 | 35 | /// 36 | /// Gets transactions. 37 | /// 38 | [Required] 39 | public List Transactions { get; } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/WebApi/DependencyInjection/InMemoryInfrastructureExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.DependencyInjection 2 | { 3 | using Application.Services; 4 | using Domain.Accounts; 5 | using Domain.Customers; 6 | using Domain.Security; 7 | using Infrastructure.InMemoryDataAccess; 8 | using Infrastructure.InMemoryDataAccess.Repositories; 9 | using Microsoft.Extensions.DependencyInjection; 10 | 11 | public static class InMemoryInfrastructureExtensions 12 | { 13 | public static IServiceCollection AddInMemoryPersistence(this IServiceCollection services) 14 | { 15 | services.AddScoped(); 16 | services.AddScoped(); 17 | services.AddScoped(); 18 | 19 | services.AddSingleton(); 20 | services.AddScoped(); 21 | 22 | services.AddScoped(); 23 | services.AddScoped(); 24 | services.AddScoped(); 25 | 26 | return services; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Infrastructure/EntityFrameworkDataAccess/Configuration/DebitConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Domain.Accounts.Debits; 2 | using Domain.Accounts.ValueObjects; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace Infrastructure.EntityFrameworkDataAccess.Configuration 7 | { 8 | public class DebitConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.ToTable("Debit"); 13 | 14 | builder.Property(debit => debit.Amount) 15 | .HasConversion( 16 | value => value.ToMoney().ToDecimal(), 17 | value => new PositiveMoney(value)) 18 | .IsRequired(); 19 | 20 | builder.Property(debit => debit.Id) 21 | .HasConversion( 22 | value => value.ToGuid(), 23 | value => new DebitId(value)) 24 | .IsRequired(); 25 | 26 | builder.Property(debit => debit.AccountId) 27 | .HasConversion( 28 | value => value.ToGuid(), 29 | value => new AccountId(value)) 30 | .IsRequired(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Infrastructure/EntityFrameworkDataAccess/Configuration/CreditConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Domain.Accounts.Credits; 2 | using Domain.Accounts.ValueObjects; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace Infrastructure.EntityFrameworkDataAccess.Configuration 7 | { 8 | public class CreditConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.ToTable("Credit"); 13 | 14 | builder.Property(credit => credit.Amount) 15 | .HasConversion( 16 | value => value.ToMoney().ToDecimal(), 17 | value => new PositiveMoney(value)) 18 | .IsRequired(); 19 | 20 | builder.Property(credit => credit.Id) 21 | .HasConversion( 22 | value => value.ToGuid(), 23 | value => new CreditId(value)) 24 | .IsRequired(); 25 | 26 | builder.Property(credit => credit.AccountId) 27 | .HasConversion( 28 | value => value.ToGuid(), 29 | value => new AccountId(value)) 30 | .IsRequired(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Withdraw/WithdrawOutput.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Withdraw 2 | { 3 | using Domain.Accounts.Debits; 4 | using Domain.Accounts.ValueObjects; 5 | 6 | /// 7 | /// Withdraw Output Message. 8 | /// 9 | public sealed class WithdrawOutput : IUseCaseOutput 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Debit object. 15 | /// Updated balance. 16 | public WithdrawOutput(IDebit debit, Money updatedBalance) 17 | { 18 | Debit debitEntity = (Debit)debit; 19 | 20 | this.Transaction = new Transaction( 21 | debitEntity.Description, 22 | debitEntity.Amount, 23 | debitEntity.TransactionDate); 24 | 25 | this.UpdatedBalance = updatedBalance; 26 | } 27 | 28 | /// 29 | /// Gets the Transaction. 30 | /// 31 | public Transaction Transaction { get; } 32 | 33 | /// 34 | /// Gets the Updated Balance. 35 | /// 36 | public Money UpdatedBalance { get; } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/GetAccountDetails/GetAccountDetailsResponse.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.GetAccountDetails 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | using Domain.Accounts.ValueObjects; 7 | using WebApi.ViewModels; 8 | 9 | /// 10 | /// Get Account Details. 11 | /// 12 | public sealed class GetAccountDetailsResponse 13 | { 14 | public GetAccountDetailsResponse( 15 | AccountId accountId, 16 | Money currentBalance, 17 | List transactions) 18 | { 19 | AccountId = accountId.ToGuid(); 20 | CurrentBalance = currentBalance.ToDecimal(); 21 | Transactions = transactions; 22 | } 23 | 24 | /// 25 | /// Gets account ID. 26 | /// 27 | [Required] 28 | public Guid AccountId { get; } 29 | 30 | /// 31 | /// Gets current Balance. 32 | /// 33 | [Required] 34 | public decimal CurrentBalance { get; } 35 | 36 | /// 37 | /// Gets transactions. 38 | /// 39 | [Required] 40 | public IList Transactions { get; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/WebApi/Filters/SwaggerDefaultValues.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Filters 2 | { 3 | using Microsoft.AspNetCore.Mvc.ApiExplorer; 4 | using Microsoft.OpenApi.Models; 5 | using Swashbuckle.AspNetCore.SwaggerGen; 6 | 7 | /// 8 | /// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter. 9 | /// 10 | /// This is only required due to bugs in the . 11 | /// Once they are fixed and published, this class can be removed. 12 | public class SwaggerDefaultValues : IOperationFilter 13 | { 14 | /// 15 | /// Applies the filter to the specified operation using the given context. 16 | /// 17 | /// The operation to apply the filter to. 18 | /// The current operation filter context. 19 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 20 | { 21 | var apiDescription = context.ApiDescription; 22 | 23 | operation.Deprecated |= apiDescription.IsDeprecated(); 24 | 25 | if (operation.Parameters is null) 26 | { 27 | return; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Domain/Accounts/Debits/Debit.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Accounts.Debits 2 | { 3 | using System; 4 | using Domain.Accounts.ValueObjects; 5 | 6 | /// 7 | /// Debit Entity Design Pattern. 8 | /// 9 | public abstract class Debit : IDebit 10 | { 11 | /// 12 | /// Gets or sets Id. 13 | /// 14 | public DebitId Id { get; protected set; } 15 | 16 | /// 17 | /// Gets or sets Amount. 18 | /// 19 | public PositiveMoney Amount { get; protected set; } 20 | 21 | /// 22 | /// Gets Description. 23 | /// 24 | public string Description { get => "Debit"; } 25 | 26 | /// 27 | /// Gets or sets Transaction Date. 28 | /// 29 | public DateTime TransactionDate { get; protected set; } 30 | 31 | /// 32 | /// Calculates the sum of amounts. 33 | /// 34 | /// Positive amount. 35 | /// The positive sum. 36 | public PositiveMoney Sum(PositiveMoney amount) => this.Amount.Add(amount); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Deposit/DepositOutput.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Deposit 2 | { 3 | using Domain.Accounts.Credits; 4 | using Domain.Accounts.ValueObjects; 5 | 6 | /// 7 | /// Deposit Output Message. 8 | /// 9 | public sealed class DepositOutput : IUseCaseOutput 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Credit object. 15 | /// The updated balance. 16 | public DepositOutput( 17 | ICredit credit, 18 | Money updatedBalance) 19 | { 20 | Credit creditEntity = (Credit)credit; 21 | 22 | this.Transaction = new Transaction( 23 | creditEntity.Description, 24 | creditEntity.Amount, 25 | creditEntity.TransactionDate); 26 | 27 | this.UpdatedBalance = updatedBalance; 28 | } 29 | 30 | /// 31 | /// Gets the Transaction object. 32 | /// 33 | public Transaction Transaction { get; } 34 | 35 | /// 36 | /// Gets the updated balance. 37 | /// 38 | public Money UpdatedBalance { get; } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/WebApi/DependencyInjection/VersioningExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.DependencyInjection 2 | { 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | public static class VersioningExtensions 6 | { 7 | public static IServiceCollection AddVersioning(this IServiceCollection services) 8 | { 9 | services.AddApiVersioning( 10 | options => 11 | { 12 | // reporting api versions will return the headers "api-supported-versions" and "api-deprecated-versions" 13 | options.ReportApiVersions = true; 14 | }); 15 | services.AddVersionedApiExplorer( 16 | options => 17 | { 18 | // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service 19 | // note: the specified format code will format the version as "'v'major[.minor][-status]" 20 | options.GroupNameFormat = "'v'VVV"; 21 | 22 | // note: this option is only necessary when versioning by url segment. the SubstitutionFormat 23 | // can also be used to control the format of the API version in route templates 24 | options.SubstituteApiVersionInUrl = true; 25 | }); 26 | 27 | return services; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/Deposit/DepositResponse.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.Deposit 2 | { 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | /// 7 | /// The response for a successfull Deposit. 8 | /// 9 | public sealed class DepositResponse 10 | { 11 | public DepositResponse( 12 | decimal amount, 13 | string description, 14 | DateTime transactionDate, 15 | decimal updatedBalance) 16 | { 17 | Amount = amount; 18 | Description = description; 19 | TransactionDate = transactionDate; 20 | UpdateBalance = updatedBalance; 21 | } 22 | 23 | /// 24 | /// Gets amount Deposited. 25 | /// 26 | [Required] 27 | public decimal Amount { get; } 28 | 29 | /// 30 | /// Gets description. 31 | /// 32 | [Required] 33 | public string Description { get; } 34 | 35 | /// 36 | /// Gets transaction Date. 37 | /// 38 | [Required] 39 | public DateTime TransactionDate { get; } 40 | 41 | /// 42 | /// Gets updated Balance. 43 | /// 44 | [Required] 45 | public decimal UpdateBalance { get; } 46 | } 47 | } -------------------------------------------------------------------------------- /test/IntegrationTests/IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | false 6 | true 7 | 8 | 9 | 10 | IntegrationTests.ruleset 11 | 12 | 13 | 14 | IntegrationTests.ruleset 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/UnitTests/UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | false 6 | true 7 | 8 | 9 | 10 | UnitTests.ruleset 11 | 12 | 13 | 14 | UnitTests.ruleset 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Withdraw/Transaction.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Withdraw 2 | { 3 | using System; 4 | using Domain.Accounts.ValueObjects; 5 | 6 | /// 7 | /// Transaction. 8 | /// 9 | public sealed class Transaction 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Text description. 15 | /// Amount. 16 | /// Transaction Date. 17 | public Transaction( 18 | string description, 19 | PositiveMoney amount, 20 | DateTime transactionDate) 21 | { 22 | this.Description = description; 23 | this.Amount = amount; 24 | this.TransactionDate = transactionDate; 25 | } 26 | 27 | /// 28 | /// Gets the Description. 29 | /// 30 | public string Description { get; } 31 | 32 | /// 33 | /// Gets the Amount. 34 | /// 35 | public PositiveMoney Amount { get; } 36 | 37 | /// 38 | /// Gets the Transaction Date. 39 | /// 40 | public DateTime TransactionDate { get; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/WebApi/DependencyInjection/Authentication/UserService.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.DependencyInjection.Authentication 2 | { 3 | using System; 4 | using Domain.Customers.ValueObjects; 5 | using Domain.Security.Services; 6 | using Domain.Security.ValueObjects; 7 | using Microsoft.AspNetCore.Http; 8 | 9 | public sealed class UserService : IUserService 10 | { 11 | private readonly IHttpContextAccessor _httpContextAccessor; 12 | 13 | public UserService(IHttpContextAccessor httpContextAccessor) 14 | { 15 | _httpContextAccessor = httpContextAccessor; 16 | } 17 | 18 | public CustomerId? GetCustomerId() 19 | { 20 | return new CustomerId(Guid.NewGuid()); 21 | } 22 | 23 | public ExternalUserId GetExternalUserId() 24 | { 25 | var user = _httpContextAccessor.HttpContext.User; 26 | 27 | var login = user.FindFirst(c => c.Type == "urn:github:login")?.Value; 28 | var externalUserId = new ExternalUserId($"{user.Identity.AuthenticationType}/{login}"); 29 | 30 | return externalUserId; 31 | } 32 | 33 | public Name GetUserName() 34 | { 35 | var user = _httpContextAccessor.HttpContext.User; 36 | 37 | var username = new Name(user.Identity.Name); 38 | 39 | return username; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Domain/Accounts/Debits/DebitId.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Accounts.Debits 2 | { 3 | using System; 4 | 5 | /// 6 | /// Debit Value Object Domain-Driven Design Pattern. 7 | /// 8 | public readonly struct DebitId 9 | { 10 | private readonly Guid _debitId; 11 | 12 | /// 13 | /// Initializes a new instance of the struct. 14 | /// 15 | /// DebitId Guid. 16 | public DebitId(Guid debitId) 17 | { 18 | if (debitId == Guid.Empty) 19 | { 20 | throw new EmptyDebitIdException($"{nameof(debitId)} cannot be empty."); 21 | } 22 | 23 | this._debitId = debitId; 24 | } 25 | 26 | /// 27 | /// Converts into string. 28 | /// 29 | /// Serialized string. 30 | public override string ToString() 31 | { 32 | return this._debitId.ToString(); 33 | } 34 | 35 | /// 36 | /// Converts into Guid. 37 | /// 38 | /// Guid representation. 39 | public Guid ToGuid() => this._debitId; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Register/Transaction.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Register 2 | { 3 | using System; 4 | using Domain.Accounts.ValueObjects; 5 | 6 | /// 7 | /// Transaction. 8 | /// 9 | public sealed class Transaction 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Text description. 15 | /// Positive amount. 16 | /// Transaction date. 17 | public Transaction( 18 | string description, 19 | PositiveMoney amount, 20 | DateTime transactionDate) 21 | { 22 | this.Description = description; 23 | this.Amount = amount; 24 | this.TransactionDate = transactionDate; 25 | } 26 | 27 | /// 28 | /// Gets the text description. 29 | /// 30 | public string Description { get; } 31 | 32 | /// 33 | /// Gets the Amount. 34 | /// 35 | public PositiveMoney Amount { get; } 36 | 37 | /// 38 | /// Gets the transaction date. 39 | /// 40 | public DateTime TransactionDate { get; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Domain/Accounts/ValueObjects/AccountId.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Accounts.ValueObjects 2 | { 3 | using System; 4 | 5 | /// 6 | /// AccountId Entity Design Pattern. 7 | /// 8 | public readonly struct AccountId 9 | { 10 | private readonly Guid _accountId; 11 | 12 | /// 13 | /// Initializes a new instance of the struct. 14 | /// 15 | /// AccountId Guid. 16 | public AccountId(Guid accountId) 17 | { 18 | if (accountId == Guid.Empty) 19 | { 20 | throw new EmptyAccountIdException($"{nameof(accountId)} cannot be empty."); 21 | } 22 | 23 | this._accountId = accountId; 24 | } 25 | 26 | /// 27 | /// Converts into string. 28 | /// 29 | /// String. 30 | public override string ToString() 31 | { 32 | return this._accountId.ToString(); 33 | } 34 | 35 | /// 36 | /// Converts into Guid. 37 | /// 38 | /// Guid representation. 39 | public Guid ToGuid() => this._accountId; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Domain/Customers/ValueObjects/CustomerId.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Customers.ValueObjects 2 | { 3 | using System; 4 | 5 | /// 6 | /// CustomerId Entity Design Pattern. 7 | /// 8 | public readonly struct CustomerId 9 | { 10 | private readonly Guid _customerId; 11 | 12 | /// 13 | /// Initializes a new instance of the struct. 14 | /// 15 | /// Customer Guid. 16 | public CustomerId(Guid customerId) 17 | { 18 | if (customerId == Guid.Empty) 19 | { 20 | throw new EmptyCustomerIdException($"{nameof(customerId)} cannot be empty."); 21 | } 22 | 23 | this._customerId = customerId; 24 | } 25 | 26 | /// 27 | /// Converts into String. 28 | /// 29 | /// String. 30 | public override string ToString() 31 | { 32 | return this._customerId.ToString(); 33 | } 34 | 35 | /// 36 | /// Converts into Guid. 37 | /// 38 | /// Guid. 39 | public Guid ToGuid() => this._customerId; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Deposit/Transaction.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Deposit 2 | { 3 | using System; 4 | using Domain.Accounts.ValueObjects; 5 | 6 | /// 7 | /// Transaction Output object. 8 | /// 9 | public sealed class Transaction 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Text Description. 15 | /// Positive amount. 16 | /// Transaction Date. 17 | public Transaction( 18 | string description, 19 | PositiveMoney amount, 20 | DateTime transactionDate) 21 | { 22 | this.Description = description; 23 | this.Amount = amount; 24 | this.TransactionDate = transactionDate; 25 | } 26 | 27 | /// 28 | /// Gets the description. 29 | /// 30 | public string Description { get; } 31 | 32 | /// 33 | /// Gets the amount. 34 | /// 35 | public PositiveMoney Amount { get; } 36 | 37 | /// 38 | /// Gets the transaction date. 39 | /// 40 | public DateTime TransactionDate { get; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Domain/Accounts/Credits/CreditId.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Accounts.Credits 2 | { 3 | using System; 4 | 5 | /// 6 | /// CreditId Value Object Domain-Driven Design Pattern. 7 | /// 8 | public readonly struct CreditId 9 | { 10 | private readonly Guid _creditId; 11 | 12 | /// 13 | /// Initializes a new instance of the struct. 14 | /// 15 | /// CreditId. 16 | public CreditId(Guid creditId) 17 | { 18 | if (creditId == Guid.Empty) 19 | { 20 | throw new EmptyCreditIdException($"{nameof(creditId)} cannot be empty."); 21 | } 22 | 23 | this._creditId = creditId; 24 | } 25 | 26 | /// 27 | /// Converts into string. 28 | /// 29 | /// String representation. 30 | public override string ToString() 31 | { 32 | return this._creditId.ToString(); 33 | } 34 | 35 | /// 36 | /// Converts into Guid. 37 | /// 38 | /// Guid representation. 39 | public Guid ToGuid() => this._creditId; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Application/Boundaries/GetCustomerDetails/Transaction.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.GetCustomerDetails 2 | { 3 | using System; 4 | using Domain.Accounts.ValueObjects; 5 | 6 | /// 7 | /// Transaction. 8 | /// 9 | public sealed class Transaction 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Text description. 15 | /// Positive amount. 16 | /// Transaction date. 17 | public Transaction( 18 | string description, 19 | PositiveMoney amount, 20 | DateTime transactionDate) 21 | { 22 | this.Description = description; 23 | this.Amount = amount; 24 | this.TransactionDate = transactionDate; 25 | } 26 | 27 | /// 28 | /// Gets the description. 29 | /// 30 | public string Description { get; } 31 | 32 | /// 33 | /// Gets the amount. 34 | /// 35 | public PositiveMoney Amount { get; } 36 | 37 | /// 38 | /// Gets the transaction date. 39 | /// 40 | public DateTime TransactionDate { get; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Register/Account.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Register 2 | { 3 | using System.Collections.Generic; 4 | using Domain.Accounts.ValueObjects; 5 | 6 | /// 7 | /// Account. 8 | /// 9 | public sealed class Account 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Account Id. 15 | /// Current Balance. 16 | /// Transactions list. 17 | public Account( 18 | AccountId accountId, 19 | Money currentBalance, 20 | List transactions) 21 | { 22 | this.AccountId = accountId; 23 | this.CurrentBalance = currentBalance; 24 | this.Transactions = transactions; 25 | } 26 | 27 | /// 28 | /// Gets the AccountId. 29 | /// 30 | public AccountId AccountId { get; } 31 | 32 | /// 33 | /// Gets the Current Balance. 34 | /// 35 | public Money CurrentBalance { get; } 36 | 37 | /// 38 | /// Gets the Transactions. 39 | /// 40 | public List Transactions { get; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Infrastructure/EntityFrameworkDataAccess/Repositories/UserRepository.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.EntityFrameworkDataAccess.Repositories 2 | { 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Domain.Security; 7 | using Domain.Security.ValueObjects; 8 | using Microsoft.EntityFrameworkCore; 9 | 10 | public sealed class UserRepository : IUserRepository 11 | { 12 | private readonly MangaContext _context; 13 | 14 | public UserRepository(MangaContext context) 15 | { 16 | _context = context ?? 17 | throw new ArgumentNullException(nameof(context)); 18 | } 19 | 20 | public async Task Add(IUser user) 21 | { 22 | await _context.Users.AddAsync((EntityFrameworkDataAccess.User)user); 23 | await _context.SaveChangesAsync(); 24 | } 25 | 26 | public async Task Get(ExternalUserId externalUserId) 27 | { 28 | var user = await _context 29 | .Users 30 | .Where(a => a.ExternalUserId.Equals(externalUserId)) 31 | .SingleOrDefaultAsync(); 32 | 33 | if (user is null) 34 | { 35 | throw new UserNotFoundException($"The user {externalUserId} does not exist or is not processed yet."); 36 | } 37 | 38 | return user; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Application/Boundaries/GetAccountDetails/Transaction.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.GetAccountDetails 2 | { 3 | using System; 4 | using Domain.Accounts.ValueObjects; 5 | 6 | /// 7 | /// Transaction. 8 | /// 9 | public sealed class Transaction 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Text description. 15 | /// Positive amount. 16 | /// Transaction date. 17 | public Transaction( 18 | string description, 19 | PositiveMoney amount, 20 | DateTime transactionDate) 21 | { 22 | this.Description = description; 23 | this.Amount = amount; 24 | this.TransactionDate = transactionDate; 25 | } 26 | 27 | /// 28 | /// Gets the text description. 29 | /// 30 | public string Description { get; } 31 | 32 | /// 33 | /// Gets the amount. 34 | /// 35 | public PositiveMoney Amount { get; } 36 | 37 | /// 38 | /// Gets the transaction date. 39 | /// 40 | public DateTime TransactionDate { get; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Domain/Accounts/ValueObjects/PositiveMoney.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Accounts.ValueObjects 2 | { 3 | /// 4 | /// PositiveMoney Entity Design Pattern. 5 | /// 6 | public readonly struct PositiveMoney 7 | { 8 | private readonly Money _value; 9 | 10 | /// 11 | /// Initializes a new instance of the struct. 12 | /// 13 | /// Decimal amount. 14 | public PositiveMoney(decimal value) 15 | { 16 | if (value < 0) 17 | { 18 | throw new MoneyShouldBePositiveException("The 'Amount' should be positive."); 19 | } 20 | 21 | this._value = new Money(value); 22 | } 23 | 24 | /// 25 | /// Converts into Money Value Object. 26 | /// 27 | /// Money. 28 | public Money ToMoney() => this._value; 29 | 30 | internal PositiveMoney Add(PositiveMoney positiveAmount) 31 | { 32 | return this._value.Add(positiveAmount._value); 33 | } 34 | 35 | internal Money Subtract(PositiveMoney positiveAmount) 36 | { 37 | return this._value.Subtract(positiveAmount._value); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/WebApi/DependencyInjection/SQLServerInfrastructureExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.DependencyInjection 2 | { 3 | using Application.Services; 4 | using Domain.Accounts; 5 | using Domain.Customers; 6 | using Domain.Security; 7 | using Infrastructure.EntityFrameworkDataAccess; 8 | using Infrastructure.EntityFrameworkDataAccess.Repositories; 9 | using Microsoft.EntityFrameworkCore; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | 13 | public static class SQLServerInfrastructureExtensions 14 | { 15 | public static IServiceCollection AddSQLServerPersistence( 16 | this IServiceCollection services, 17 | IConfiguration configuration) 18 | { 19 | services.AddScoped(); 20 | services.AddScoped(); 21 | services.AddScoped(); 22 | 23 | services.AddDbContext( 24 | options => options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"))); 25 | services.AddScoped(); 26 | 27 | services.AddScoped(); 28 | services.AddScoped(); 29 | 30 | return services; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/GetAccountDetails/GetAccountDetailsPresenter.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.GetAccountDetails 2 | { 3 | using System.Collections.Generic; 4 | using Application.Boundaries.GetAccountDetails; 5 | using Microsoft.AspNetCore.Mvc; 6 | using WebApi.ViewModels; 7 | 8 | public sealed class GetAccountDetailsPresenter : IOutputPort 9 | { 10 | public IActionResult ViewModel { get; private set; } 11 | 12 | public void NotFound(string message) 13 | { 14 | ViewModel = new NotFoundObjectResult(message); 15 | } 16 | 17 | public void Standard(GetAccountDetailsOutput getAccountDetailsOutput) 18 | { 19 | List transactions = new List(); 20 | 21 | foreach (var item in getAccountDetailsOutput.Transactions) 22 | { 23 | var transaction = new TransactionModel( 24 | item.Amount, 25 | item.Description, 26 | item.TransactionDate); 27 | 28 | transactions.Add(transaction); 29 | } 30 | 31 | var getAccountDetailsResponse = new GetAccountDetailsResponse( 32 | getAccountDetailsOutput.AccountId, 33 | getAccountDetailsOutput.CurrentBalance, 34 | transactions); 35 | 36 | ViewModel = new OkObjectResult(getAccountDetailsResponse); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/Register/RegisterResponse.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.Register 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | using Domain.Customers.ValueObjects; 7 | using WebApi.ViewModels; 8 | 9 | /// 10 | /// The response for Registration. 11 | /// 12 | public sealed class RegisterResponse 13 | { 14 | public RegisterResponse( 15 | CustomerId customerId, 16 | SSN ssn, 17 | Name name, 18 | List accounts) 19 | { 20 | CustomerId = customerId.ToGuid(); 21 | SSN = ssn.ToString(); 22 | Name = name.ToString(); 23 | Accounts = accounts; 24 | } 25 | 26 | /// 27 | /// Gets customer ID. 28 | /// 29 | [Required] 30 | public Guid CustomerId { get; } 31 | 32 | /// 33 | /// Gets sSN. 34 | /// 35 | [Required] 36 | public string SSN { get; } 37 | 38 | /// 39 | /// Gets name. 40 | /// 41 | [Required] 42 | public string Name { get; } 43 | 44 | /// 45 | /// Gets accounts. 46 | /// 47 | [Required] 48 | public List Accounts { get; } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Domain/Accounts/Credits/Credit.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Accounts.Credits 2 | { 3 | using System; 4 | using Domain.Accounts.ValueObjects; 5 | 6 | /// 7 | /// Credit Entity Design Pattern. 8 | /// 9 | public abstract class Credit : ICredit 10 | { 11 | /// 12 | /// Gets or sets Id. 13 | /// 14 | public CreditId Id { get; protected set; } 15 | 16 | /// 17 | /// Gets or sets Amount. 18 | /// 19 | public PositiveMoney Amount { get; protected set; } 20 | 21 | /// 22 | /// Gets Description. 23 | /// 24 | public string Description 25 | { 26 | get { return "Credit"; } 27 | } 28 | 29 | /// 30 | /// Gets or sets Transaction Date. 31 | /// 32 | public DateTime TransactionDate { get; protected set; } 33 | 34 | /// 35 | /// Calculate the sum of positive amounts. 36 | /// 37 | /// Positive amount. 38 | /// The positive sum. 39 | public PositiveMoney Sum(PositiveMoney amount) 40 | { 41 | return this.Amount.Add(amount); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Domain/Accounts/ValueObjects/Money.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Accounts.ValueObjects 2 | { 3 | /// 4 | /// Money Entity Design Pattern. 5 | /// 6 | public readonly struct Money 7 | { 8 | private readonly decimal _money; 9 | 10 | /// 11 | /// Initializes a new instance of the struct. 12 | /// 13 | /// Decimal amount. 14 | public Money(decimal value) 15 | { 16 | this._money = value; 17 | } 18 | 19 | /// 20 | /// Converts into decimal. 21 | /// 22 | /// decimal amount. 23 | public decimal ToDecimal() => this._money; 24 | 25 | internal bool LessThan(PositiveMoney amount) 26 | { 27 | return this._money < amount.ToMoney()._money; 28 | } 29 | 30 | internal bool IsZero() 31 | { 32 | return this._money == 0; 33 | } 34 | 35 | internal PositiveMoney Add(Money value) 36 | { 37 | return new PositiveMoney(this._money + value.ToDecimal()); 38 | } 39 | 40 | internal Money Subtract(Money value) 41 | { 42 | return new Money(this._money - value.ToDecimal()); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Transfer/TransferInput.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Transfer 2 | { 3 | using Domain.Accounts.ValueObjects; 4 | 5 | /// 6 | /// Transfer Input Message. 7 | /// 8 | public sealed class TransferInput : IUseCaseInput 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// Origin Account Id. 14 | /// Destination Account Id. 15 | /// Positive amount. 16 | public TransferInput( 17 | AccountId originAccountId, 18 | AccountId destinationAccountId, 19 | PositiveMoney amount) 20 | { 21 | this.OriginAccountId = originAccountId; 22 | this.DestinationAccountId = destinationAccountId; 23 | this.Amount = amount; 24 | } 25 | 26 | /// 27 | /// Gets Origin Account Id. 28 | /// 29 | public AccountId OriginAccountId { get; } 30 | 31 | /// 32 | /// Gets Destination Account Id. 33 | /// 34 | public AccountId DestinationAccountId { get; } 35 | 36 | /// 37 | /// Gets the Amount. 38 | /// 39 | public PositiveMoney Amount { get; } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/GetCustomerDetails/GetCustomerDetailsResponse.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.GetCustomerDetails 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | using Domain.Customers.ValueObjects; 7 | using WebApi.ViewModels; 8 | 9 | /// 10 | /// The Customer Details. 11 | /// 12 | public sealed class GetCustomerDetailsResponse 13 | { 14 | public GetCustomerDetailsResponse( 15 | CustomerId customerId, 16 | SSN ssn, 17 | Name name, 18 | List accounts) 19 | { 20 | CustomerId = customerId.ToGuid(); 21 | SSN = ssn.ToString(); 22 | Name = name.ToString(); 23 | Accounts = accounts; 24 | } 25 | 26 | /// 27 | /// Gets customer ID. 28 | /// 29 | [Required] 30 | public Guid CustomerId { get; } 31 | 32 | /// 33 | /// Gets SSN. 34 | /// 35 | [Required] 36 | public string SSN { get; } 37 | 38 | /// 39 | /// Gets name. 40 | /// 41 | [Required] 42 | public string Name { get; } 43 | 44 | /// 45 | /// Gets accounts. 46 | /// 47 | [Required] 48 | public IList Accounts { get; } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/UnitTests/UseCaseTests/Withdraw/WithdrawTests.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTests.UseCasesTests.Withdraw 2 | { 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Application.Boundaries.Withdraw; 6 | using Application.UseCases; 7 | using Domain.Accounts.ValueObjects; 8 | using Infrastructure.InMemoryDataAccess.Presenters; 9 | using UnitTests.TestFixtures; 10 | using Xunit; 11 | 12 | public sealed class WithdrawTests : IClassFixture 13 | { 14 | private readonly StandardFixture _fixture; 15 | 16 | public WithdrawTests(StandardFixture fixture) 17 | { 18 | _fixture = fixture; 19 | } 20 | 21 | [Theory] 22 | [ClassData(typeof(PositiveDataSetup))] 23 | public async Task Withdraw_Valid_Amount( 24 | decimal amount, 25 | decimal expectedBalance) 26 | { 27 | var presenter = new WithdrawPresenter(); 28 | var sut = new Withdraw( 29 | _fixture.AccountService, 30 | presenter, 31 | _fixture.AccountRepository, 32 | _fixture.UnitOfWork); 33 | 34 | await sut.Execute(new WithdrawInput( 35 | _fixture.Context.DefaultAccountId, 36 | new PositiveMoney(amount))); 37 | 38 | var actual = presenter.Withdrawals.Last(); 39 | Assert.Equal(expectedBalance, actual.UpdatedBalance.ToDecimal()); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/UnitTests/PresenterTests/RegisterPresenterTests.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTests.PresenterTests 2 | { 3 | using System.Net; 4 | using Application.Boundaries.Register; 5 | using Domain.Customers; 6 | using Domain.Customers.ValueObjects; 7 | using Domain.Security; 8 | using Domain.Security.ValueObjects; 9 | using Microsoft.AspNetCore.Mvc; 10 | using WebApi.UseCases.V1.Register; 11 | using Xunit; 12 | 13 | public sealed class RegisterPresenterTests 14 | { 15 | [Fact] 16 | public void GivenValidData_Handle_WritesOkObjectResult() 17 | { 18 | var customer = new Infrastructure.InMemoryDataAccess.Customer( 19 | new SSN("198608178888"), 20 | new Name("Ivan Paulovich")); 21 | 22 | var account = new Infrastructure.InMemoryDataAccess.Account( 23 | customer.Id); 24 | 25 | var registerOutput = new RegisterOutput( 26 | new ExternalUserId("github/ivanpaulovich"), 27 | customer, 28 | account); 29 | 30 | var sut = new RegisterPresenter(); 31 | sut.Standard(registerOutput); 32 | 33 | var actual = Assert.IsType(sut.ViewModel); 34 | Assert.Equal((int)HttpStatusCode.Created, actual.StatusCode); 35 | 36 | var actualValue = (RegisterResponse)actual.Value; 37 | Assert.Equal(customer.Id.ToGuid(), actualValue.CustomerId); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Domain/Customers/ValueObjects/SSN.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Customers.ValueObjects 2 | { 3 | using System.Text.RegularExpressions; 4 | 5 | /// 6 | /// SSN Entity Design Pattern. 7 | /// 8 | public readonly struct SSN 9 | { 10 | private const string RegExForValidation = @"^\d{6,8}[-|(\s)]{0,1}\d{4}$"; 11 | 12 | private readonly string _text; 13 | 14 | /// 15 | /// Initializes a new instance of the struct. 16 | /// 17 | /// SSN. 18 | public SSN(string text) 19 | { 20 | if (string.IsNullOrWhiteSpace(text)) 21 | { 22 | throw new SSNShouldNotBeEmptyException($"The {nameof(text)} field is required."); 23 | } 24 | 25 | Regex regex = new Regex(RegExForValidation); 26 | Match match = regex.Match(text); 27 | 28 | if (!match.Success) 29 | { 30 | throw new InvalidSSNException($"Invalid {nameof(text)} format. Use YYMMDDNNNN."); 31 | } 32 | 33 | this._text = text; 34 | } 35 | 36 | /// 37 | /// Converts into string. 38 | /// 39 | /// string. 40 | public override string ToString() => this._text; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Infrastructure/InMemoryDataAccess/EntityFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.InMemoryDataAccess 2 | { 3 | using System; 4 | using Domain.Accounts; 5 | using Domain.Accounts.Credits; 6 | using Domain.Accounts.Debits; 7 | using Domain.Accounts.ValueObjects; 8 | using Domain.Customers; 9 | using Domain.Customers.ValueObjects; 10 | using Domain.Security; 11 | using Domain.Security.ValueObjects; 12 | 13 | /// 14 | /// Entity Factory Domain-Driven Design Pattern. 15 | /// 16 | public sealed class EntityFactory : IUserFactory, ICustomerFactory, IAccountFactory 17 | { 18 | public IAccount NewAccount(CustomerId customerId) => new Account(customerId); 19 | 20 | public ICredit NewCredit( 21 | IAccount account, 22 | PositiveMoney amountToDeposit, 23 | DateTime transactionDate) => new Credit(account, amountToDeposit, transactionDate); 24 | 25 | public ICustomer NewCustomer( 26 | SSN ssn, 27 | Name name) => new Customer(ssn, name); 28 | 29 | public IDebit NewDebit( 30 | IAccount account, 31 | PositiveMoney amountToWithdraw, 32 | DateTime transactionDate) => new Debit(account, amountToWithdraw, transactionDate); 33 | 34 | public IUser NewUser(CustomerId customerId, ExternalUserId externalUserId) => new User(customerId, externalUserId); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Infrastructure/EntityFrameworkDataAccess/EntityFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.EntityFrameworkDataAccess 2 | { 3 | using System; 4 | using Domain.Accounts; 5 | using Domain.Accounts.Credits; 6 | using Domain.Accounts.Debits; 7 | using Domain.Accounts.ValueObjects; 8 | using Domain.Customers; 9 | using Domain.Customers.ValueObjects; 10 | using Domain.Security; 11 | using Domain.Security.ValueObjects; 12 | 13 | /// 14 | /// Entity Factory Domain-Driven Design Pattern. 15 | /// 16 | public sealed class EntityFactory : IUserFactory, ICustomerFactory, IAccountFactory 17 | { 18 | public IAccount NewAccount(CustomerId customerId) => new Account(customerId); 19 | 20 | public ICredit NewCredit( 21 | IAccount account, 22 | PositiveMoney amountToDeposit, 23 | DateTime transactionDate) => new Credit(account, amountToDeposit, transactionDate); 24 | 25 | public ICustomer NewCustomer( 26 | SSN ssn, 27 | Name name) => new Customer(ssn, name); 28 | 29 | public IDebit NewDebit( 30 | IAccount account, 31 | PositiveMoney amountToWithdraw, 32 | DateTime transactionDate) => new Debit(account, amountToWithdraw, transactionDate); 33 | 34 | public IUser NewUser(CustomerId customerId, ExternalUserId externalUserId) => new User(customerId, externalUserId); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Infrastructure/Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | enable 6 | true 7 | 8.0 8 | true 9 | 10 | 11 | 12 | Infrastructure.ruleset 13 | 14 | 15 | 16 | Infrastructure.ruleset 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/src/WebApi/bin/Debug/netcoreapp3.0/WebApi.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/src/WebApi", 16 | "stopAtEntry": false, 17 | // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser 18 | "serverReadyAction": { 19 | "action": "openExternally", 20 | "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" 21 | }, 22 | "env": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "sourceFileMap": { 26 | "/Views": "${workspaceFolder}/Views" 27 | } 28 | }, 29 | { 30 | "name": ".NET Core Attach", 31 | "type": "coreclr", 32 | "request": "attach", 33 | "processId": "${command:pickProcess}" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /src/Infrastructure/InMemoryDataAccess/Repositories/CustomerRepository.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.InMemoryDataAccess.Repositories 2 | { 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Domain.Customers; 6 | using Domain.Customers.ValueObjects; 7 | 8 | public sealed class CustomerRepository : ICustomerRepository 9 | { 10 | private readonly MangaContext _context; 11 | 12 | public CustomerRepository(MangaContext context) 13 | { 14 | _context = context; 15 | } 16 | 17 | public async Task Add(ICustomer customer) 18 | { 19 | _context.Customers.Add((InMemoryDataAccess.Customer)customer); 20 | await Task.CompletedTask; 21 | } 22 | 23 | public async Task GetBy(CustomerId customerId) 24 | { 25 | var customer = _context.Customers 26 | .Where(e => e.Id.Equals(customerId)) 27 | .SingleOrDefault(); 28 | 29 | if (customer is null) 30 | { 31 | throw new CustomerNotFoundException($"The customer {customerId} does not exist or is not processed yet."); 32 | } 33 | 34 | return await Task.FromResult(customer); 35 | } 36 | 37 | public async Task Update(ICustomer customer) 38 | { 39 | Customer customerOld = _context.Customers 40 | .Where(e => e.Id.Equals(customer.Id)) 41 | .SingleOrDefault(); 42 | 43 | customerOld = (Customer)customer; 44 | await Task.CompletedTask; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/UnitTests/UseCaseTests/Transfer/TransferUseCaseTests.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTests.UseCaseTests.Transfer 2 | { 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Application.Boundaries.Transfer; 6 | using Application.UseCases; 7 | using Domain.Accounts.ValueObjects; 8 | using Infrastructure.InMemoryDataAccess.Presenters; 9 | using UnitTests.TestFixtures; 10 | using Xunit; 11 | 12 | public sealed class TransferUseCaseTests : IClassFixture 13 | { 14 | private readonly StandardFixture _fixture; 15 | 16 | public TransferUseCaseTests(StandardFixture fixture) 17 | { 18 | _fixture = fixture; 19 | } 20 | 21 | [Theory] 22 | [ClassData(typeof(PositiveDataSetup))] 23 | public async Task Transfer_ChangesBalance_WhenAccountExists( 24 | decimal amount, 25 | decimal expectedOriginBalance) 26 | { 27 | var presenter = new TransferPresenter(); 28 | var sut = new Transfer( 29 | _fixture.AccountService, 30 | presenter, 31 | _fixture.AccountRepository, 32 | _fixture.UnitOfWork); 33 | 34 | await sut.Execute( 35 | new TransferInput( 36 | _fixture.Context.DefaultAccountId, 37 | _fixture.Context.SecondAccountId, 38 | new PositiveMoney(amount))); 39 | 40 | var actual = presenter.Transfers.Last(); 41 | Assert.Equal(expectedOriginBalance, actual.UpdatedBalance.ToDecimal()); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/UnitTests/InputValidationTests/TransferInputValidationTests.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTests.InputValidationTests 2 | { 3 | using System; 4 | using Application.Boundaries.Transfer; 5 | using Domain.Accounts.ValueObjects; 6 | using Xunit; 7 | 8 | public sealed class TransferInputValidationTests 9 | { 10 | [Fact] 11 | public void GivenEmptyOriginAccountId_InputNotCreated_ThrowsInputValidationException() 12 | { 13 | var actualEx = Assert.Throws( 14 | () => new TransferInput( 15 | new AccountId(Guid.Empty), 16 | new AccountId(Guid.NewGuid()), 17 | new PositiveMoney(10))); 18 | Assert.Contains("accountId", actualEx.Message); 19 | } 20 | 21 | [Fact] 22 | public void GivenEmptyDestinationAccountId_InputNotCreated_ThrowsInputValidationException() 23 | { 24 | var actualEx = Assert.Throws( 25 | () => new TransferInput( 26 | new AccountId(Guid.NewGuid()), 27 | new AccountId(Guid.Empty), 28 | new PositiveMoney(10))); 29 | Assert.Contains("accountId", actualEx.Message); 30 | } 31 | 32 | [Fact] 33 | public void GivenValidData_InputCreated() 34 | { 35 | var actual = new TransferInput( 36 | new AccountId(Guid.NewGuid()), 37 | new AccountId(Guid.NewGuid()), 38 | new PositiveMoney(10)); 39 | Assert.NotNull(actual); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Infrastructure/InMemoryDataAccess/Services/TestUserService.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.InMemoryDataAccess.Services 2 | { 3 | using System; 4 | using System.Linq; 5 | using Domain.Customers.ValueObjects; 6 | using Domain.Security.Services; 7 | using Domain.Security.ValueObjects; 8 | 9 | public sealed class TestUserService : IUserService 10 | { 11 | private readonly MangaContext _mangaContext; 12 | private readonly Guid _sessionId; 13 | 14 | public TestUserService(MangaContext mangaContext) 15 | { 16 | _sessionId = Guid.NewGuid(); 17 | _mangaContext = mangaContext; 18 | } 19 | 20 | public CustomerId? GetCustomerId() 21 | { 22 | var user = _mangaContext.Users 23 | .Where(e => e.ExternalUserId.Equals(GetExternalUserId())) 24 | .SingleOrDefault(); 25 | 26 | if (user is null) 27 | { 28 | return null; 29 | } 30 | 31 | var customer = _mangaContext.Customers 32 | .Where(e => e.Id.Equals(user.CustomerId)) 33 | .SingleOrDefault(); 34 | 35 | if (customer is null) 36 | { 37 | return null; 38 | } 39 | 40 | return customer.Id; 41 | } 42 | 43 | public ExternalUserId GetExternalUserId() 44 | { 45 | return new ExternalUserId($"github/tempuser{_sessionId}"); 46 | } 47 | 48 | public Name GetUserName() 49 | { 50 | return new Name($"Temporary User {_sessionId}"); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/GetCustomerDetails/CustomersController.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.GetCustomerDetails 2 | { 3 | using System.Threading.Tasks; 4 | using Application.Boundaries.GetCustomerDetails; 5 | using FluentMediator; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | [ApiVersion("1.0")] 10 | [Route("api/v{version:apiVersion}/[controller]")] 11 | [ApiController] 12 | public sealed class CustomersController : ControllerBase 13 | { 14 | private readonly IMediator _mediator; 15 | private readonly GetCustomerDetailsPresenter _presenter; 16 | 17 | public CustomersController( 18 | IMediator mediator, 19 | GetCustomerDetailsPresenter presenter) 20 | { 21 | _mediator = mediator; 22 | _presenter = presenter; 23 | } 24 | 25 | /// 26 | /// Get the Customer details. 27 | /// 28 | /// An asynchronous . 29 | [HttpGet(Name = "GetCustomer")] 30 | [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetCustomerDetailsResponse))] 31 | [ProducesResponseType(StatusCodes.Status404NotFound)] 32 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 33 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 34 | public async Task GetCustomer() 35 | { 36 | var input = new GetCustomerDetailsInput(); 37 | await _mediator.PublishAsync(input); 38 | return _presenter.ViewModel; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Transfer/TransferOutput.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Transfer 2 | { 3 | using Domain.Accounts.Debits; 4 | using Domain.Accounts.ValueObjects; 5 | 6 | /// 7 | /// Transfer Output. 8 | /// 9 | public sealed class TransferOutput : IUseCaseOutput 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Debit object. 15 | /// Updated balance. 16 | /// Origin Account Id. 17 | /// Destination Account Id. 18 | public TransferOutput( 19 | IDebit debit, 20 | Money updatedBalance, 21 | AccountId originAccountId, 22 | AccountId destinationAccountId) 23 | { 24 | Debit debitEntity = (Debit)debit; 25 | 26 | this.Transaction = new Transaction( 27 | originAccountId, 28 | destinationAccountId, 29 | debitEntity.Description, 30 | debitEntity.Amount, 31 | debitEntity.TransactionDate); 32 | 33 | this.UpdatedBalance = updatedBalance; 34 | } 35 | 36 | /// 37 | /// Gets the Transaction. 38 | /// 39 | public Transaction Transaction { get; } 40 | 41 | /// 42 | /// Gets the Updated Balance. 43 | /// 44 | public Money UpdatedBalance { get; } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Domain/Accounts/IAccountFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Accounts 2 | { 3 | using System; 4 | using Domain.Accounts.Credits; 5 | using Domain.Accounts.Debits; 6 | using Domain.Accounts.ValueObjects; 7 | using Domain.Customers.ValueObjects; 8 | 9 | /// 10 | /// Account Entity Factory Domain-Driven Design Pattern. 11 | /// 12 | public interface IAccountFactory 13 | { 14 | /// 15 | /// Creates a new Account. 16 | /// 17 | /// CustomerId. 18 | /// New Account instance. 19 | IAccount NewAccount(CustomerId customerId); 20 | 21 | /// 22 | /// Creates a new Credit. 23 | /// 24 | /// Account object. 25 | /// Amount to Deposit. 26 | /// Transaction date. 27 | /// New Credit instance. 28 | ICredit NewCredit(IAccount account, PositiveMoney amountToDeposit, DateTime transactionDate); 29 | 30 | /// 31 | /// Creates a new Debit. 32 | /// 33 | /// Account object. 34 | /// Amount to Withdraw. 35 | /// Transaction date. 36 | /// New Debit instance. 37 | IDebit NewDebit(IAccount account, PositiveMoney amountToWithdraw, DateTime transactionDate); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Domain/Security/SecurityService.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Security 2 | { 3 | using System.Threading.Tasks; 4 | using Domain.Customers.ValueObjects; 5 | using Domain.Security.ValueObjects; 6 | 7 | /// 8 | /// Security Domain Service Domain-Driven Design Pattern. 9 | /// 10 | public class SecurityService 11 | { 12 | private readonly IUserFactory _userFactory; 13 | private readonly IUserRepository _userRepository; 14 | 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// User Factory. 19 | /// User Repository. 20 | public SecurityService( 21 | IUserFactory userFactory, 22 | IUserRepository userRepository) 23 | { 24 | this._userFactory = userFactory; 25 | this._userRepository = userRepository; 26 | } 27 | 28 | /// 29 | /// Create User Credentials. 30 | /// 31 | /// CustomerId. 32 | /// External User Id. 33 | /// The User. 34 | public async Task CreateUserCredentials(CustomerId customerId, ExternalUserId externalUserId) 35 | { 36 | var user = this._userFactory.NewUser(customerId, externalUserId); 37 | await this._userRepository.Add(user); 38 | return user; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/WebApi/DependencyInjection/ApplicationExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.DependencyInjection 2 | { 3 | using Domain.Accounts; 4 | using Domain.Customers; 5 | using Domain.Security; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | /// 9 | /// Adds Use Cases classes. 10 | /// 11 | public static class ApplicationExtensions 12 | { 13 | /// 14 | /// Adds Use Cases to the ServiceCollection. 15 | /// 16 | /// Service Collection. 17 | /// The modified instance. 18 | public static IServiceCollection AddUseCases(this IServiceCollection services) 19 | { 20 | services.AddScoped(); 21 | services.AddScoped(); 22 | services.AddScoped(); 23 | services.AddScoped(); 24 | services.AddScoped(); 25 | services.AddScoped(); 26 | services.AddScoped(); 27 | services.AddScoped(); 28 | services.AddScoped(); 29 | services.AddScoped(); 30 | 31 | return services; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/UnitTests/TestFixtures/StandardFixture.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTests.TestFixtures 2 | { 3 | using Domain.Accounts; 4 | using Domain.Customers; 5 | using Domain.Security; 6 | using Infrastructure.InMemoryDataAccess; 7 | using Infrastructure.InMemoryDataAccess.Repositories; 8 | using Infrastructure.InMemoryDataAccess.Services; 9 | 10 | public sealed class StandardFixture 11 | { 12 | public StandardFixture() 13 | { 14 | Context = new MangaContext(); 15 | AccountRepository = new AccountRepository(Context); 16 | CustomerRepository = new CustomerRepository(Context); 17 | UserRepository = new UserRepository(Context); 18 | UnitOfWork = new UnitOfWork(Context); 19 | EntityFactory = new EntityFactory(); 20 | UserService = new UserService(); 21 | CustomerService = new CustomerService(EntityFactory, CustomerRepository); 22 | SecurityService = new SecurityService(EntityFactory, UserRepository); 23 | AccountService = new AccountService(EntityFactory, AccountRepository); 24 | } 25 | 26 | public EntityFactory EntityFactory { get; } 27 | 28 | public MangaContext Context { get; } 29 | 30 | public AccountRepository AccountRepository { get; } 31 | 32 | public CustomerRepository CustomerRepository { get; } 33 | 34 | public UserRepository UserRepository { get; } 35 | 36 | public UnitOfWork UnitOfWork { get; } 37 | 38 | public UserService UserService { get; } 39 | 40 | public CustomerService CustomerService { get; } 41 | 42 | public SecurityService SecurityService { get; } 43 | 44 | public AccountService AccountService { get; } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Domain/Accounts/IAccount.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Accounts 2 | { 3 | using Domain.Accounts.Credits; 4 | using Domain.Accounts.Debits; 5 | using Domain.Accounts.ValueObjects; 6 | 7 | /// 8 | /// Account Aggregate Root Domain-Driven Design Pattern. 9 | /// 10 | public interface IAccount 11 | { 12 | /// 13 | /// Gets Id. 14 | /// 15 | AccountId Id { get; } 16 | 17 | /// 18 | /// Deposits into account. 19 | /// 20 | /// Factory to create new credits. 21 | /// Amount. 22 | /// Credit created. 23 | ICredit Deposit(IAccountFactory entityFactory, PositiveMoney amountToDeposit); 24 | 25 | /// 26 | /// Withdrawls from account. 27 | /// 28 | /// Factory to create new debits. 29 | /// Amount. 30 | /// Debit created. 31 | IDebit Withdraw(IAccountFactory entityFactory, PositiveMoney amountToWithdraw); 32 | 33 | /// 34 | /// Check if closing account is allowed. 35 | /// 36 | /// True if is allowed. 37 | bool IsClosingAllowed(); 38 | 39 | /// 40 | /// Gets the current balance considering credits and debits totals. 41 | /// 42 | /// The current balance. 43 | Money GetCurrentBalance(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/WebApi/Filters/SwaggerDocumentFilter.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Filters 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using Microsoft.OpenApi.Models; 7 | using Swashbuckle.AspNetCore.SwaggerGen; 8 | 9 | public class SwaggerDocumentFilter : IDocumentFilter 10 | { 11 | private readonly List _tags = new List 12 | { 13 | new OpenApiTag 14 | { 15 | Name = "RoutingApi", 16 | Description = "This is a description for the api routes", 17 | }, 18 | }; 19 | 20 | public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) 21 | { 22 | if (swaggerDoc is null) 23 | { 24 | throw new ArgumentNullException(nameof(swaggerDoc)); 25 | } 26 | 27 | swaggerDoc.Tags = GetFilteredTagDefinitions(context); 28 | swaggerDoc.Paths = GetSortedPaths(swaggerDoc); 29 | } 30 | 31 | private List GetFilteredTagDefinitions(DocumentFilterContext context) 32 | { 33 | // Filtering ensures route for tag is present 34 | var currentGroupNames = context.ApiDescriptions 35 | .Select(description => description.GroupName); 36 | 37 | return _tags 38 | .Where(tag => currentGroupNames.Contains(tag.Name)) 39 | .ToList(); 40 | } 41 | 42 | private OpenApiPaths GetSortedPaths( 43 | OpenApiDocument swaggerDoc) 44 | { 45 | IDictionary dic = swaggerDoc.Paths 46 | .OrderBy(pair => pair.Key) 47 | .ToDictionary(pair => pair.Key, pair => pair.Value); 48 | 49 | return null; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Domain/Customers/AccountCollection.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Customers 2 | { 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | using Domain.Accounts.ValueObjects; 6 | 7 | /// 8 | /// Accounts First-Class Collection Design Pattern. 9 | /// 10 | public sealed class AccountCollection 11 | { 12 | private readonly IList _accountIds; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | public AccountCollection() 18 | { 19 | this._accountIds = new List(); 20 | } 21 | 22 | /// 23 | /// Adds Accounts. 24 | /// 25 | /// Accounts list. 26 | public void Add(IEnumerable accounts) 27 | { 28 | foreach (var account in accounts) 29 | { 30 | this.Add(account); 31 | } 32 | } 33 | 34 | /// 35 | /// Add a single account. 36 | /// 37 | /// AccountId. 38 | public void Add(AccountId accountId) => this._accountIds.Add(accountId); 39 | 40 | /// 41 | /// Gets the AccountIds. 42 | /// 43 | /// ReadOnlyCollection. 44 | public IReadOnlyCollection GetAccountIds() 45 | { 46 | IReadOnlyCollection accountIds = new ReadOnlyCollection(this._accountIds); 47 | return accountIds; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/Register/RegisterPresenter.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.Register 2 | { 3 | using System.Collections.Generic; 4 | using Application.Boundaries.Register; 5 | using Microsoft.AspNetCore.Mvc; 6 | using WebApi.ViewModels; 7 | 8 | public sealed class RegisterPresenter : IOutputPort 9 | { 10 | public IActionResult ViewModel { get; private set; } 11 | 12 | public void CustomerAlreadyRegistered(string message) 13 | { 14 | ViewModel = new BadRequestObjectResult(message); 15 | } 16 | 17 | public void Standard(RegisterOutput output) 18 | { 19 | var transactions = new List(); 20 | 21 | foreach (var item in output.Account.Transactions) 22 | { 23 | var transaction = new TransactionModel( 24 | item.Amount, 25 | item.Description, 26 | item.TransactionDate); 27 | 28 | transactions.Add(transaction); 29 | } 30 | 31 | var account = new AccountDetailsModel( 32 | output.Account.AccountId, 33 | output.Account.CurrentBalance, 34 | transactions); 35 | 36 | var accounts = new List(); 37 | accounts.Add(account); 38 | 39 | var registerResponse = new RegisterResponse( 40 | output.Customer.CustomerId, 41 | output.Customer.SSN, 42 | output.Customer.Name, 43 | accounts); 44 | 45 | ViewModel = new CreatedAtRouteResult( 46 | "GetCustomer", 47 | new 48 | { 49 | customerId = registerResponse.CustomerId, 50 | version = "1.0", 51 | }, 52 | registerResponse); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/Infrastructure/EntityFrameworkDataAccess/Migrations/20191126172117_ValueObjects.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace Infrastructure.EntityFrameworkDataAccess.Migrations 5 | { 6 | public partial class ValueObjects : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.UpdateData( 11 | table: "Credit", 12 | keyColumn: "Id", 13 | keyValue: new Guid("f5117315-e789-491a-b662-958c37237f9b"), 14 | column: "TransactionDate", 15 | value: new DateTime(2019, 11, 26, 17, 21, 17, 192, DateTimeKind.Utc).AddTicks(890)); 16 | 17 | migrationBuilder.UpdateData( 18 | table: "Debit", 19 | keyColumn: "Id", 20 | keyValue: new Guid("3d6032df-7a3b-46e6-8706-be971e3d539f"), 21 | column: "TransactionDate", 22 | value: new DateTime(2019, 11, 26, 17, 21, 17, 192, DateTimeKind.Utc).AddTicks(2670)); 23 | } 24 | 25 | protected override void Down(MigrationBuilder migrationBuilder) 26 | { 27 | migrationBuilder.UpdateData( 28 | table: "Credit", 29 | keyColumn: "Id", 30 | keyValue: new Guid("f5117315-e789-491a-b662-958c37237f9b"), 31 | column: "TransactionDate", 32 | value: new DateTime(2019, 11, 24, 22, 39, 24, 636, DateTimeKind.Utc).AddTicks(5740)); 33 | 34 | migrationBuilder.UpdateData( 35 | table: "Debit", 36 | keyColumn: "Id", 37 | keyValue: new Guid("3d6032df-7a3b-46e6-8706-be971e3d539f"), 38 | column: "TransactionDate", 39 | value: new DateTime(2019, 11, 24, 22, 39, 24, 636, DateTimeKind.Utc).AddTicks(6780)); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Infrastructure/EntityFrameworkDataAccess/Repositories/CustomerRepository.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.EntityFrameworkDataAccess.Repositories 2 | { 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Domain.Customers; 7 | using Domain.Customers.ValueObjects; 8 | using Microsoft.EntityFrameworkCore; 9 | 10 | public sealed class CustomerRepository : ICustomerRepository 11 | { 12 | private readonly MangaContext _context; 13 | 14 | public CustomerRepository(MangaContext context) 15 | { 16 | _context = context ?? 17 | throw new ArgumentNullException(nameof(context)); 18 | } 19 | 20 | public async Task Add(ICustomer customer) 21 | { 22 | await _context.Customers.AddAsync((EntityFrameworkDataAccess.Customer)customer); 23 | await _context.SaveChangesAsync(); 24 | } 25 | 26 | public async Task GetBy(CustomerId customerId) 27 | { 28 | var customer = await _context.Customers 29 | .Where(c => c.Id.Equals(customerId)) 30 | .SingleOrDefaultAsync(); 31 | 32 | if (customer is null) 33 | { 34 | throw new CustomerNotFoundException($"The customer {customerId} does not exist or is not processed yet."); 35 | } 36 | 37 | var accounts = _context.Accounts 38 | .Where(e => e.CustomerId.Equals(customer.Id)) 39 | .Select(e => e.Id) 40 | .ToList(); 41 | 42 | customer.LoadAccounts(accounts); 43 | 44 | return customer; 45 | } 46 | 47 | public async Task Update(ICustomer customer) 48 | { 49 | _context.Customers.Update((EntityFrameworkDataAccess.Customer)customer); 50 | await _context.SaveChangesAsync(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/GetAccountDetails/AccountsController.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.GetAccountDetails 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Threading.Tasks; 5 | using Application.Boundaries.GetAccountDetails; 6 | using Domain.Accounts.ValueObjects; 7 | using FluentMediator; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.AspNetCore.Mvc; 10 | 11 | [ApiVersion("1.0")] 12 | [Route("api/v{version:apiVersion}/[controller]")] 13 | [ApiController] 14 | public sealed class AccountsController : ControllerBase 15 | { 16 | private readonly IMediator _mediator; 17 | private readonly GetAccountDetailsPresenter _presenter; 18 | 19 | public AccountsController( 20 | IMediator mediator, 21 | GetAccountDetailsPresenter presenter) 22 | { 23 | _mediator = mediator; 24 | _presenter = presenter; 25 | } 26 | 27 | /// 28 | /// Get an account details. 29 | /// 30 | /// A . 31 | /// An asynchronous . 32 | [HttpGet("{AccountId}", Name = "GetAccount")] 33 | [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetAccountDetailsResponse))] 34 | [ProducesResponseType(StatusCodes.Status404NotFound)] 35 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 36 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 37 | public async Task Get([FromRoute][Required] GetAccountDetailsRequest request) 38 | { 39 | var input = new GetAccountDetailsInput(new AccountId(request.AccountId)); 40 | await _mediator.PublishAsync(input); 41 | return _presenter.ViewModel; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/GetCustomerDetails/GetCustomerDetailsPresenter.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.GetCustomerDetails 2 | { 3 | using System.Collections.Generic; 4 | using Application.Boundaries.GetCustomerDetails; 5 | using Microsoft.AspNetCore.Mvc; 6 | using WebApi.ViewModels; 7 | 8 | public sealed class GetCustomerDetailsPresenter : IOutputPort 9 | { 10 | public IActionResult ViewModel { get; private set; } 11 | 12 | public void NotFound(string message) 13 | { 14 | ViewModel = new NotFoundObjectResult(message); 15 | } 16 | 17 | public void Standard(GetCustomerDetailsOutput getCustomerDetailsOutput) 18 | { 19 | List accounts = new List(); 20 | 21 | foreach (var account in getCustomerDetailsOutput.Accounts) 22 | { 23 | List transactions = new List(); 24 | 25 | foreach (var item in account.Transactions) 26 | { 27 | var transaction = new TransactionModel( 28 | item.Amount, 29 | item.Description, 30 | item.TransactionDate); 31 | 32 | transactions.Add(transaction); 33 | } 34 | 35 | accounts.Add(new AccountDetailsModel( 36 | account.AccountId, 37 | account.CurrentBalance, 38 | transactions)); 39 | } 40 | 41 | var getCustomerDetailsResponse = new GetCustomerDetailsResponse( 42 | getCustomerDetailsOutput.CustomerId, 43 | getCustomerDetailsOutput.SSN, 44 | getCustomerDetailsOutput.Name, 45 | accounts); 46 | 47 | ViewModel = new OkObjectResult(getCustomerDetailsResponse); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/CloseAccount/AccountsController.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.CloseAccount 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Threading.Tasks; 5 | using Application.Boundaries.CloseAccount; 6 | using Domain.Accounts.ValueObjects; 7 | using FluentMediator; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.AspNetCore.Mvc; 10 | 11 | [ApiVersion("1.0")] 12 | [Route("api/v{version:apiVersion}/[controller]")] 13 | [ApiController] 14 | public sealed class AccountsController : ControllerBase 15 | { 16 | private readonly IMediator _mediator; 17 | private readonly CloseAccountPresenter _presenter; 18 | 19 | public AccountsController( 20 | IMediator mediator, 21 | CloseAccountPresenter presenter) 22 | { 23 | _mediator = mediator; 24 | _presenter = presenter; 25 | } 26 | 27 | /// 28 | /// Close an Account. 29 | /// 30 | /// The closed account id. 31 | /// Bad request. 32 | /// Error. 33 | /// The request to Close an Account. 34 | /// The account id. 35 | [HttpDelete("{AccountId}")] 36 | [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(CloseAccountResponse))] 37 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 38 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 39 | public async Task Close([FromRoute][Required] CloseAccountRequest request) 40 | { 41 | var input = new CloseAccountInput(new AccountId(request.AccountId)); 42 | await _mediator.PublishAsync(input); 43 | return _presenter.ViewModel; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Register/Customer.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Register 2 | { 3 | using System.Collections.Generic; 4 | using Domain.Customers; 5 | using Domain.Customers.ValueObjects; 6 | using Domain.Security.ValueObjects; 7 | 8 | /// 9 | /// Customer. 10 | /// 11 | public sealed class Customer 12 | { 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | /// External User Id. 17 | /// Customer object. 18 | /// Accounts list. 19 | public Customer( 20 | ExternalUserId externalUserId, 21 | ICustomer customer, 22 | List accounts) 23 | { 24 | var customerEntity = (Domain.Customers.Customer)customer; 25 | this.ExternalUserId = externalUserId; 26 | this.CustomerId = customerEntity.Id; 27 | this.SSN = customerEntity.SSN; 28 | this.Name = customerEntity.Name; 29 | this.Accounts = accounts; 30 | } 31 | 32 | /// 33 | /// Gets the External User Id. 34 | /// 35 | public ExternalUserId ExternalUserId { get; } 36 | 37 | /// 38 | /// Gets the Customer Id. 39 | /// 40 | public CustomerId CustomerId { get; } 41 | 42 | /// 43 | /// Gets the SSN. 44 | /// 45 | public SSN SSN { get; } 46 | 47 | /// 48 | /// Gets the name. 49 | /// 50 | public Name Name { get; } 51 | 52 | /// 53 | /// Gets the Accounts. 54 | /// 55 | public IReadOnlyList Accounts { get; } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Domain/Accounts/IAccountRepository.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Accounts 2 | { 3 | using System.Threading.Tasks; 4 | using Domain.Accounts.Credits; 5 | using Domain.Accounts.Debits; 6 | using Domain.Accounts.ValueObjects; 7 | 8 | /// 9 | /// Account Repository Domain-Driven Design Pattern. 10 | /// 11 | public interface IAccountRepository 12 | { 13 | /// 14 | /// Gets an Account. 15 | /// 16 | /// AccountId. 17 | /// An Account instance. 18 | Task Get(AccountId id); 19 | 20 | /// 21 | /// Adds an Account. 22 | /// 23 | /// Account object. 24 | /// Credit object. 25 | /// Task. 26 | Task Add(IAccount account, ICredit credit); 27 | 28 | /// 29 | /// Updates an Account. 30 | /// 31 | /// Account object. 32 | /// Credit object. 33 | /// Task. 34 | Task Update(IAccount account, ICredit credit); 35 | 36 | /// 37 | /// Updates the Account. 38 | /// 39 | /// Account object. 40 | /// Debit object. 41 | /// Task. 42 | Task Update(IAccount account, IDebit debit); 43 | 44 | /// 45 | /// Deletes the Account. 46 | /// 47 | /// Account object. 48 | /// Task. 49 | Task Delete(IAccount account); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/Deposit/AccountsController.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.Deposit 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Threading.Tasks; 5 | using Application.Boundaries.Deposit; 6 | using Domain.Accounts.ValueObjects; 7 | using FluentMediator; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.AspNetCore.Mvc; 10 | 11 | [ApiVersion("1.0")] 12 | [Route("api/v{version:apiVersion}/[controller]")] 13 | [ApiController] 14 | public sealed class AccountsController : ControllerBase 15 | { 16 | private readonly IMediator _mediator; 17 | private readonly DepositPresenter _presenter; 18 | 19 | public AccountsController( 20 | IMediator mediator, 21 | DepositPresenter presenter) 22 | { 23 | _mediator = mediator; 24 | _presenter = presenter; 25 | } 26 | 27 | /// 28 | /// Deposit on an account. 29 | /// 30 | /// The updated balance. 31 | /// Bad request. 32 | /// Error. 33 | /// The request to deposit. 34 | /// The updated balance. 35 | [HttpPatch("Deposit")] 36 | [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(DepositResponse))] 37 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 38 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 39 | public async Task Deposit([FromForm][Required] DepositRequest request) 40 | { 41 | var input = new DepositInput( 42 | new AccountId(request.AccountId), 43 | new PositiveMoney(request.Amount)); 44 | 45 | await _mediator.PublishAsync(input); 46 | return _presenter.ViewModel; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/Withdraw/AccountsController.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.Withdraw 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Threading.Tasks; 5 | using Application.Boundaries.Withdraw; 6 | using Domain.Accounts.ValueObjects; 7 | using FluentMediator; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.AspNetCore.Mvc; 10 | 11 | [ApiVersion("1.0")] 12 | [Route("api/v{version:apiVersion}/[controller]")] 13 | [ApiController] 14 | public sealed class AccountsController : ControllerBase 15 | { 16 | private readonly IMediator _mediator; 17 | private readonly WithdrawPresenter _presenter; 18 | 19 | public AccountsController( 20 | IMediator mediator, 21 | WithdrawPresenter presenter) 22 | { 23 | _mediator = mediator; 24 | _presenter = presenter; 25 | } 26 | 27 | /// 28 | /// Withdraw on an account. 29 | /// 30 | /// The updated balance. 31 | /// Bad request. 32 | /// Error. 33 | /// The request to Withdraw. 34 | /// The updated balance. 35 | [HttpPatch("Withdraw")] 36 | [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(WithdrawResponse))] 37 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 38 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 39 | public async Task Withdraw([FromForm][Required] WithdrawRequest request) 40 | { 41 | var input = new WithdrawInput( 42 | new AccountId(request.AccountId), 43 | new PositiveMoney(request.Amount)); 44 | await _mediator.PublishAsync(input); 45 | return _presenter.ViewModel; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V2/GetAccountDetails/GetAccountDetailsPresenterV2.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V2.GetAccountDetails 2 | { 3 | using System; 4 | using System.Data; 5 | using Application.Boundaries.GetAccountDetails; 6 | using Microsoft.AspNetCore.Mvc; 7 | using OfficeOpenXml; 8 | 9 | public sealed class GetAccountDetailsPresenterV2 : IOutputPort 10 | { 11 | public IActionResult ViewModel { get; private set; } 12 | 13 | public void NotFound(string message) 14 | { 15 | ViewModel = new NotFoundObjectResult(message); 16 | } 17 | 18 | public void Standard(GetAccountDetailsOutput getAccountDetailsOutput) 19 | { 20 | var dataTable = new DataTable(); 21 | dataTable.Columns.Add("Amount", typeof(decimal)); 22 | dataTable.Columns.Add("Description", typeof(string)); 23 | dataTable.Columns.Add("TransactionDate", typeof(DateTime)); 24 | 25 | foreach (var item in getAccountDetailsOutput.Transactions) 26 | { 27 | dataTable.Rows.Add(new object[] 28 | { 29 | item.Amount.ToMoney().ToDecimal(), 30 | item.Description, 31 | item.TransactionDate, 32 | }); 33 | } 34 | 35 | byte[] fileContents; 36 | 37 | using (ExcelPackage pck = new ExcelPackage()) 38 | { 39 | ExcelWorksheet ws = pck.Workbook.Worksheets.Add(getAccountDetailsOutput.AccountId.ToString()); 40 | ws.Cells["A1"].LoadFromDataTable(dataTable, true); 41 | ws.Row(1).Style.Font.Bold = true; 42 | ws.Column(3).Style.Numberformat.Format = "dd/MM/yyyy HH:mm"; 43 | fileContents = pck.GetAsByteArray(); 44 | } 45 | 46 | ViewModel = new FileContentResult(fileContents, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/WebApi/UseCases/V1/Register/CustomersController.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V1.Register 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Threading.Tasks; 5 | using Application.Boundaries.Register; 6 | using Domain.Accounts.ValueObjects; 7 | using Domain.Customers.ValueObjects; 8 | using FluentMediator; 9 | using Microsoft.AspNetCore.Http; 10 | using Microsoft.AspNetCore.Mvc; 11 | 12 | [ApiVersion("1.0")] 13 | [Route("api/v{version:apiVersion}/[controller]")] 14 | [ApiController] 15 | public sealed class CustomersController : ControllerBase 16 | { 17 | private readonly IMediator _mediator; 18 | private readonly RegisterPresenter _presenter; 19 | 20 | public CustomersController( 21 | IMediator mediator, 22 | RegisterPresenter presenter) 23 | { 24 | _mediator = mediator; 25 | _presenter = presenter; 26 | } 27 | 28 | /// 29 | /// Register a customer. 30 | /// 31 | /// The registered customer was create successfully. 32 | /// Bad request. 33 | /// Error. 34 | /// The request to register a customer. 35 | /// The newly registered customer. 36 | [HttpPost] 37 | [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RegisterResponse))] 38 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 39 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 40 | public async Task Post([FromForm][Required] RegisterRequest request) 41 | { 42 | var input = new RegisterInput( 43 | new SSN(request.SSN), 44 | new PositiveMoney(request.InitialAmount)); 45 | await _mediator.PublishAsync(input); 46 | return _presenter.ViewModel; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Application/Boundaries/GetCustomerDetails/GetCustomerDetailsOutput.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.GetCustomerDetails 2 | { 3 | using System.Collections.Generic; 4 | using Domain.Customers; 5 | using Domain.Customers.ValueObjects; 6 | using Domain.Security.ValueObjects; 7 | 8 | /// 9 | /// Gets Customer Details Output Message. 10 | /// 11 | public sealed class GetCustomerDetailsOutput : IUseCaseOutput 12 | { 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | /// External User Id. 17 | /// Customer object. 18 | /// Accounts list. 19 | public GetCustomerDetailsOutput( 20 | ExternalUserId externalUserId, 21 | ICustomer customer, 22 | List accounts) 23 | { 24 | Customer customerEntity = (Customer)customer; 25 | this.ExternalUserId = externalUserId; 26 | this.CustomerId = customerEntity.Id; 27 | this.SSN = customerEntity.SSN; 28 | this.Name = customerEntity.Name; 29 | this.Accounts = accounts; 30 | } 31 | 32 | /// 33 | /// Gets the ExternalUserId. 34 | /// 35 | public ExternalUserId ExternalUserId { get; } 36 | 37 | /// 38 | /// Gets the CustomerId. 39 | /// 40 | public CustomerId CustomerId { get; } 41 | 42 | /// 43 | /// Gets the SSN. 44 | /// 45 | public SSN SSN { get; } 46 | 47 | /// 48 | /// Gets the Name. 49 | /// 50 | public Name Name { get; } 51 | 52 | /// 53 | /// Gets the Accounts. 54 | /// 55 | public IReadOnlyList Accounts { get; } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/WebApi/UseCases/V2/GetAccountDetails/AccountsController.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.UseCases.V2.GetAccountDetails 2 | { 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Threading.Tasks; 5 | using Application.Boundaries.GetAccountDetails; 6 | using Domain.Accounts.ValueObjects; 7 | using FluentMediator; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.FeatureManagement.Mvc; 11 | using WebApi.DependencyInjection.FeatureFlags; 12 | 13 | [FeatureGate(Features.GetAccountDetailsV2)] 14 | [ApiVersion("2.0")] 15 | [Route("api/v{version:apiVersion}/[controller]")] 16 | [ApiController] 17 | public sealed class AccountsController : ControllerBase 18 | { 19 | private readonly IMediator _mediator; 20 | private readonly GetAccountDetailsPresenterV2 _presenter; 21 | 22 | public AccountsController( 23 | IMediator mediator, 24 | GetAccountDetailsPresenterV2 presenter) 25 | { 26 | _mediator = mediator; 27 | _presenter = presenter; 28 | } 29 | 30 | /// 31 | /// Get an account details. 32 | /// 33 | /// A . 34 | /// An asynchronous . 35 | [HttpGet("{AccountId}", Name = "GetAccountV2")] 36 | [ProducesResponseType(StatusCodes.Status200OK)] 37 | [ProducesResponseType(StatusCodes.Status404NotFound)] 38 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 39 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 40 | public async Task Get([FromRoute][Required] GetAccountDetailsRequestV2 request) 41 | { 42 | var input = new GetAccountDetailsInput(new AccountId(request.AccountId)); 43 | await _mediator.PublishAsync(input, "GetAccountDetailsV2"); 44 | return _presenter.ViewModel; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Application/Boundaries/Transfer/Transaction.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Boundaries.Transfer 2 | { 3 | using System; 4 | using Domain.Accounts.ValueObjects; 5 | 6 | /// 7 | /// Transaction Message. 8 | /// 9 | public sealed class Transaction 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Origin Account Id. 15 | /// Destination Account Id. 16 | /// Text description. 17 | /// Positive amount. 18 | /// Transaction date. 19 | public Transaction( 20 | AccountId originAccountId, 21 | AccountId destinationAccountId, 22 | string description, 23 | PositiveMoney amount, 24 | DateTime transactionDate) 25 | { 26 | this.OriginAccountId = originAccountId; 27 | this.DestinationAccountId = destinationAccountId; 28 | this.Description = description; 29 | this.Amount = amount; 30 | this.TransactionDate = transactionDate; 31 | } 32 | 33 | /// 34 | /// Gets the Origin AccountId. 35 | /// 36 | public AccountId OriginAccountId { get; } 37 | 38 | /// 39 | /// Gets the Destination Account Id. 40 | /// 41 | public AccountId DestinationAccountId { get; } 42 | 43 | /// 44 | /// Gets the description. 45 | /// 46 | public string Description { get; } 47 | 48 | /// 49 | /// Gets the Amount. 50 | /// 51 | public PositiveMoney Amount { get; } 52 | 53 | /// 54 | /// Gets the Transaction date. 55 | /// 56 | public DateTime TransactionDate { get; } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Infrastructure/EntityFrameworkDataAccess/SeedData.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.EntityFrameworkDataAccess 2 | { 3 | using System; 4 | using Domain.Accounts.Credits; 5 | using Domain.Accounts.Debits; 6 | using Domain.Accounts.ValueObjects; 7 | using Domain.Customers.ValueObjects; 8 | using Domain.Security.ValueObjects; 9 | using Microsoft.EntityFrameworkCore; 10 | 11 | public static class SeedData 12 | { 13 | public static readonly CustomerId DefaultCustomerId = new CustomerId(new Guid("197d0438-e04b-453d-b5de-eca05960c6ae")); 14 | 15 | public static readonly AccountId DefaultAccountId = new AccountId(new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f")); 16 | 17 | public static void Seed(ModelBuilder builder) 18 | { 19 | builder.Entity().HasData( 20 | new { Id = DefaultCustomerId, Name = new Name("Test User"), SSN = new SSN("19860817-9999") }); 21 | 22 | builder.Entity().HasData( 23 | new { Id = DefaultAccountId, CustomerId = DefaultCustomerId }); 24 | 25 | builder.Entity().HasData( 26 | new 27 | { 28 | Id = new CreditId(new Guid("f5117315-e789-491a-b662-958c37237f9b")), 29 | AccountId = DefaultAccountId, 30 | Amount = new PositiveMoney(400), 31 | Description = "Credit", 32 | TransactionDate = DateTime.UtcNow 33 | }); 34 | 35 | builder.Entity().HasData( 36 | new 37 | { 38 | Id = new DebitId(new Guid("3d6032df-7a3b-46e6-8706-be971e3d539f")), 39 | AccountId = DefaultAccountId, 40 | Amount = new PositiveMoney(400), 41 | Description = "Debit", 42 | TransactionDate = DateTime.UtcNow 43 | }); 44 | 45 | builder.Entity().HasData( 46 | new { CustomerId = DefaultCustomerId, ExternalUserId = new ExternalUserId("github/ivanpaulovich") }); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Application/UseCases/GetAccountDetails.cs: -------------------------------------------------------------------------------- 1 | namespace Application.UseCases 2 | { 3 | using System.Threading.Tasks; 4 | using Application.Boundaries.GetAccountDetails; 5 | using Domain.Accounts; 6 | 7 | /// 8 | /// Get Account Details Use Case Domain-Driven Design Pattern. 9 | /// 10 | public sealed class GetAccountDetails : IUseCase, IUseCaseV2 11 | { 12 | private readonly IOutputPort _outputPort; 13 | private readonly IAccountRepository _accountRepository; 14 | 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// Output Port. 19 | /// Account Repository. 20 | public GetAccountDetails( 21 | IOutputPort outputPort, 22 | IAccountRepository accountRepository) 23 | { 24 | this._outputPort = outputPort; 25 | this._accountRepository = accountRepository; 26 | } 27 | 28 | /// 29 | /// Executes the Use Case. 30 | /// 31 | /// Input Message. 32 | /// Task. 33 | public async Task Execute(GetAccountDetailsInput input) 34 | { 35 | IAccount account; 36 | 37 | try 38 | { 39 | account = await this._accountRepository.Get(input.AccountId); 40 | } 41 | catch (AccountNotFoundException ex) 42 | { 43 | this._outputPort.NotFound(ex.Message); 44 | return; 45 | } 46 | 47 | this.BuildOutput(account); 48 | } 49 | 50 | private void BuildOutput(IAccount account) 51 | { 52 | var output = new GetAccountDetailsOutput(account); 53 | this._outputPort.Standard(output); 54 | } 55 | } 56 | } 57 | --------------------------------------------------------------------------------