├── LoanApplication.TacticalDdd ├── LoanApplication.TacticalDdd │ ├── infra-down.sh │ ├── infra-up.sh │ ├── DomainModel │ │ ├── ApplicationScore.cs │ │ ├── Ddd │ │ │ ├── IUnitOfWork.cs │ │ │ ├── Entity.cs │ │ │ ├── IEventPublisher.cs │ │ │ ├── DomainEvent.cs │ │ │ └── ValueObject.cs │ │ ├── IDebtorRegistry.cs │ │ ├── LoanApplicationStatus.cs │ │ ├── IOperatorRepository.cs │ │ ├── ILoanApplicationRepository.cs │ │ ├── SysTime.cs │ │ ├── ScoringRulesFactory.cs │ │ ├── DomainEvents │ │ │ ├── LoanApplicationAccepted.cs │ │ │ └── LoanApplicationRejected.cs │ │ ├── NationalIdentifier.cs │ │ ├── Name.cs │ │ ├── Decision.cs │ │ ├── Property.cs │ │ ├── Registration.cs │ │ ├── ScoringResult.cs │ │ ├── AgeInYears.cs │ │ ├── Percent.cs │ │ ├── Address.cs │ │ ├── Customer.cs │ │ ├── MonetaryAmount.cs │ │ ├── Loan.cs │ │ ├── ScoringRules.cs │ │ ├── Operator.cs │ │ └── LoanApplication.cs │ ├── appsettings.Development.json │ ├── Application │ │ ├── Api │ │ │ ├── AddressDto.cs │ │ │ ├── LoanApplicationSearchCriteriaDto.cs │ │ │ ├── LoanApplicationInfoDto.cs │ │ │ ├── ValidatorsInstaller.cs │ │ │ ├── LoanApplicationDto.cs │ │ │ └── LoanApplicationSubmissionDto.cs │ │ ├── ApplicationServicesInstaller.cs │ │ ├── LoanApplicationEvaluationService.cs │ │ ├── LoanApplicationDecisionService.cs │ │ └── LoanApplicationSubmissionService.cs │ ├── Infrastructure │ │ ├── DataAccess │ │ │ ├── EfUnitOfWork.cs │ │ │ ├── EfOperatorRepository.cs │ │ │ ├── EfLoanApplicationRepository.cs │ │ │ ├── EfInstaller.cs │ │ │ ├── EfDbInitializer.cs │ │ │ ├── DbInitializer.cs │ │ │ └── LoanDbContext.cs │ │ ├── MessageQueue │ │ │ ├── RabbitMqEventPublisher.cs │ │ │ └── MessageQueueClientInstaller.cs │ │ └── ExternalServices │ │ │ ├── ExternalServicesInstaller.cs │ │ │ ├── DebtorRegistry.cs │ │ │ └── DebtorRegistryClient.cs │ ├── ReadModel │ │ ├── ReadModelInstaller.cs │ │ └── LoanApplicationFinder.cs │ ├── appsettings.json │ ├── docker-compose.yml │ ├── Properties │ │ └── launchSettings.json │ ├── LoanApplication.TacticalDdd.csproj │ ├── Security │ │ └── BasicAuthenticationHandler.cs │ ├── Program.cs │ ├── Web │ │ └── LoanApplicationApi.cs │ └── SetupScripts │ │ └── dbschema │ │ └── schema_and_tables.sql ├── DebtorRegistryMock │ ├── hosting.json │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── DebtorInfoRequest.cs │ ├── DebtorRegistryMock.csproj │ ├── Dockerfile │ ├── Properties │ │ └── launchSettings.json │ └── Program.cs ├── Docs │ ├── class_model_aggregates.png │ ├── class_model_scoring_rules.png │ ├── class_model_decision_services.png │ ├── class_model_scoring_rules.plantuml │ ├── class_model_decision_services.plantuml │ └── class_model_aggregates.plantuml ├── LoanApplication.TacticalDdd.Tests │ ├── Mocks │ │ ├── UnitOfWorkMock.cs │ │ ├── DebtorRegistryMock.cs │ │ ├── InMemoryBus.cs │ │ ├── InMemoryOperatorRepository.cs │ │ └── InMemoryLoanApplicationRepository.cs │ ├── DomainTests │ │ ├── LoanTest.cs │ │ ├── AgeInYearsTest.cs │ │ ├── MonetaryAmountTests.cs │ │ ├── PropertyTest.cs │ │ ├── CustomerTest.cs │ │ ├── ScoringRulesTests.cs │ │ └── LoanApplicationTest.cs │ ├── Builders │ │ ├── OperatorBuilder.cs │ │ ├── PropertyBuilder.cs │ │ ├── LoanBuilder.cs │ │ ├── CustomerBuilder.cs │ │ └── LoanApplicationBuilder.cs │ ├── LoanApplication.TacticalDdd.Tests.csproj │ ├── Asserts │ │ ├── DomainEventsAssert.cs │ │ └── LoanApplicationAssert.cs │ └── ApplicationTests │ │ ├── LoanApplicationEvaluationServiceTests.cs │ │ ├── LoanApplicationDecisionServiceTests.cs │ │ └── LoanApplicationSubmissionServiceTests.cs └── LoanApplication.TacticalDdd.sln ├── LoanApplication.EnterpriseCake ├── DebtorRegistryMock │ ├── hosting.json │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── DebtorRegistryMock.csproj │ ├── DebtorInfoRequest.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Program.cs │ ├── Controllers │ │ └── DebtorInfoController.cs │ └── Startup.cs ├── LoanApplication.BusinessLogic │ ├── ApplicationScore.cs │ ├── LoanApplicationStatus.cs │ ├── Property.cs │ ├── Address.cs │ ├── Operator.cs │ ├── Customer.cs │ ├── LoanApplication.BusinessLogic.csproj │ ├── DebtorRegistryClient.cs │ ├── LoanApplication.cs │ ├── ScoringService.cs │ ├── ValidationService.cs │ └── LoanApplicationService.cs ├── LoanApplication.Infrastructure │ ├── Common │ │ └── Entity.cs │ ├── DataAccess │ │ ├── IUnitOfWork.cs │ │ └── IGenericRepository.cs │ ├── LoanApplication.Infrastructure.csproj │ └── Security │ │ └── BasicAuthenticationHandler.cs ├── LoanApplication.Contracts │ ├── LoanApplication.Contracts.csproj │ ├── AddressDto.cs │ ├── LoanApplicationSearchCriteriaDto.cs │ ├── LoanApplicationInfoDto.cs │ └── LoanApplicationDto.cs ├── LoanApplication.WebApi │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── LoanApplication.WebApi.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ └── Controllers │ │ └── LoanApplicationController.cs ├── LoanApplication.DataAccess │ ├── EfUnitOfWork.cs │ ├── LoanApplication.DataAccess.csproj │ ├── LoanApplicationDbContext.cs │ ├── DbInitializer.cs │ └── GenericRepository.cs └── LoanApplication.EnterpriseCake.sln ├── .github └── workflows │ └── dotnet.yml ├── README.MD └── .gitignore /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/infra-down.sh: -------------------------------------------------------------------------------- 1 | docker-compose down -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/DebtorRegistryMock/hosting.json: -------------------------------------------------------------------------------- 1 | { 2 | "server.urls": "http://localhost:5005" 3 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/infra-up.sh: -------------------------------------------------------------------------------- 1 | docker-compose up -d --build --force-recreate -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/DebtorRegistryMock/hosting.json: -------------------------------------------------------------------------------- 1 | { 2 | "server.urls": "http://localhost:5005" 3 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/Docs/class_model_aggregates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asc-lab/better-code-with-ddd/HEAD/LoanApplication.TacticalDdd/Docs/class_model_aggregates.png -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/Docs/class_model_scoring_rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asc-lab/better-code-with-ddd/HEAD/LoanApplication.TacticalDdd/Docs/class_model_scoring_rules.png -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/Docs/class_model_decision_services.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asc-lab/better-code-with-ddd/HEAD/LoanApplication.TacticalDdd/Docs/class_model_decision_services.png -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/ApplicationScore.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.DomainModel; 2 | 3 | public enum ApplicationScore 4 | { 5 | Red, 6 | Green 7 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.BusinessLogic/ApplicationScore.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.BusinessLogic 2 | { 3 | public enum ApplicationScore 4 | { 5 | Red, 6 | Green 7 | } 8 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/Ddd/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | 3 | public interface IUnitOfWork 4 | { 5 | void CommitChanges(); 6 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.Infrastructure/Common/Entity.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.Infrastructure.Common 2 | { 3 | public class Entity 4 | { 5 | public long? Id { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/Ddd/Entity.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | 3 | public abstract class Entity 4 | { 5 | public T Id { get; protected set; } 6 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/Ddd/IEventPublisher.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | 3 | public interface IEventPublisher 4 | { 5 | void Publish(DomainEvent @event); 6 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/IDebtorRegistry.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.DomainModel; 2 | 3 | public interface IDebtorRegistry 4 | { 5 | bool IsRegisteredDebtor(Customer customer); 6 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/LoanApplicationStatus.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.DomainModel; 2 | 3 | public enum LoanApplicationStatus 4 | { 5 | New, 6 | Accepted, 7 | Rejected 8 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.BusinessLogic/LoanApplicationStatus.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.BusinessLogic 2 | { 3 | public enum LoanApplicationStatus 4 | { 5 | New, 6 | Accepted, 7 | Rejected 8 | } 9 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/DebtorRegistryMock/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/DebtorRegistryMock/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.Contracts/LoanApplication.Contracts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.WebApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Application/Api/AddressDto.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.Application.Api; 2 | 3 | public record AddressDto 4 | ( 5 | string Country, 6 | string ZipCode, 7 | string City, 8 | string Street 9 | ); -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.Infrastructure/DataAccess/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace LoanApplication.Infrastructure.DataAccess 4 | { 5 | public interface IUnitOfWork 6 | { 7 | Task CommitChanges(); 8 | } 9 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/IOperatorRepository.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.DomainModel; 2 | 3 | public interface IOperatorRepository 4 | { 5 | void Add(Operator @operator); 6 | 7 | Operator WithLogin(Login login); 8 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/DebtorRegistryMock/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/DebtorRegistryMock/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.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 | -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/DebtorRegistryMock/DebtorRegistryMock.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/DebtorRegistryMock/DebtorInfoRequest.cs: -------------------------------------------------------------------------------- 1 | namespace DebtorRegistryMock; 2 | 3 | public class DebtorInfo 4 | { 5 | public string Pesel { get; set; } 6 | public List Debts { get; set; } 7 | } 8 | 9 | public class Debt 10 | { 11 | public decimal Amount { get; set; } 12 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/DebtorRegistryMock/DebtorRegistryMock.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | disable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.BusinessLogic/Property.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.Infrastructure.Common; 2 | 3 | namespace LoanApplication.BusinessLogic 4 | { 5 | public class Property : Entity 6 | { 7 | public decimal Value { get; set; } 8 | public virtual Address Address { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/Mocks/UnitOfWorkMock.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | 3 | namespace LoanApplication.TacticalDdd.Tests.Mocks; 4 | 5 | public class UnitOfWorkMock : IUnitOfWork 6 | { 7 | public void CommitChanges() 8 | { 9 | //do nothing 10 | } 11 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/ILoanApplicationRepository.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.DomainModel; 2 | 3 | public interface ILoanApplicationRepository 4 | { 5 | void Add(LoanApplication loanApplication); 6 | 7 | LoanApplication WithNumber(LoanApplicationNumber loanApplicationNumber); 8 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Application/Api/LoanApplicationSearchCriteriaDto.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.Application.Api; 2 | 3 | public record LoanApplicationSearchCriteriaDto 4 | ( 5 | string ApplicationNumber, 6 | string CustomerNationalIdentifier, 7 | string DecisionBy, 8 | string RegisteredBy 9 | ); -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.Contracts/AddressDto.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.Contracts 2 | { 3 | public class AddressDto 4 | { 5 | public string Country { get; set; } 6 | public string ZipCode { get; set; } 7 | public string City { get; set; } 8 | public string Street { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Application/Api/LoanApplicationInfoDto.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.Application.Api; 2 | 3 | public record LoanApplicationInfoDto 4 | ( 5 | string Number, 6 | string Status, 7 | string CustomerName, 8 | DateTime? DecisionDate, 9 | decimal LoanAmount, 10 | string DecisionBy 11 | ); -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/DebtorRegistryMock/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine as build 2 | WORKDIR /app 3 | COPY . . 4 | RUN dotnet restore 5 | RUN dotnet publish -o /app/published-app 6 | 7 | FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine as runtime 8 | WORKDIR /app 9 | COPY --from=build /app/published-app /app 10 | ENTRYPOINT [ "dotnet", "/app/DebtorRegistryMock.dll" ] -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Infrastructure/DataAccess/EfUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | 3 | namespace LoanApplication.TacticalDdd.Infrastructure.DataAccess; 4 | 5 | public class EfUnitOfWork(LoanDbContext dbContext) : IUnitOfWork 6 | { 7 | public void CommitChanges() 8 | { 9 | dbContext.SaveChanges(); 10 | } 11 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/SysTime.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.DomainModel; 2 | 3 | public class SysTime 4 | { 5 | public static Func CurrentTimeProvider { get; set; } = () => DateTime.Now; 6 | 7 | public static DateTime Now() => CurrentTimeProvider(); 8 | 9 | public static DateOnly Today() => DateOnly.FromDateTime(Now()); 10 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/DebtorRegistryMock/DebtorInfoRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace DebtorRegistryMock 5 | { 6 | public class DebtorInfo 7 | { 8 | public string Pesel { get; set; } 9 | public List Debts { get; set; } 10 | } 11 | 12 | public class Debt 13 | { 14 | public decimal Amount { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.BusinessLogic/Address.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.Infrastructure.Common; 2 | 3 | namespace LoanApplication.BusinessLogic 4 | { 5 | public class Address : Entity 6 | { 7 | public string Country { get; set; } 8 | public string ZipCode { get; set; } 9 | public string City { get; set; } 10 | public string Street { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/ReadModel/ReadModelInstaller.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.ReadModel; 2 | 3 | public static class ReadModelInstaller 4 | { 5 | public static void AddReadModelServices(this IServiceCollection services, ConfigurationManager cfgManger) 6 | { 7 | services.AddSingleton(_ => new LoanApplicationFinder(cfgManger.GetConnectionString("LoanDb"))); 8 | } 9 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.Contracts/LoanApplicationSearchCriteriaDto.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.Contracts 2 | { 3 | public class LoanApplicationSearchCriteriaDto 4 | { 5 | public string ApplicationNumber { get; set; } 6 | public string CustomerNationalIdentifier { get; set; } 7 | public string DecisionBy { get; set; } 8 | public string RegisteredBy { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "ConnectionStrings" : { 11 | "LoanDb" : "User ID=lab_user;Password=lab_pass;Database=ddd_loans_ef;Host=localhost;Port=5434" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Application/Api/ValidatorsInstaller.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace LoanApplication.TacticalDdd.Application.Api; 4 | 5 | public static class ValidatorsInstaller 6 | { 7 | public static void AddFluentValidators(this IServiceCollection services) => 8 | services.AddScoped, LoanApplicationSubmissionDtoValidator>(); 9 | 10 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Infrastructure/MessageQueue/RabbitMqEventPublisher.cs: -------------------------------------------------------------------------------- 1 | using EasyNetQ; 2 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 3 | 4 | namespace LoanApplication.TacticalDdd.Infrastructure.MessageQueue; 5 | 6 | public class RabbitMqEventPublisher(IBus bus) : IEventPublisher 7 | { 8 | public void Publish(DomainEvent @event) 9 | { 10 | bus.PubSub.Publish(@event); 11 | } 12 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Infrastructure/ExternalServices/ExternalServicesInstaller.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel; 2 | 3 | namespace LoanApplication.TacticalDdd.Infrastructure.ExternalServices; 4 | 5 | public static class ExternalServicesInstaller 6 | { 7 | public static void AddExternalServicesClients(this IServiceCollection services) 8 | { 9 | services.AddSingleton(); 10 | } 11 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.BusinessLogic/Operator.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.Infrastructure.Common; 2 | 3 | namespace LoanApplication.BusinessLogic 4 | { 5 | public class Operator : Entity 6 | { 7 | public string Login { get; set; } 8 | public string Password { get; set; } 9 | public string FirstName { get; set; } 10 | public string LastName { get; set; } 11 | public decimal CompetenceLevel { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.Infrastructure/LoanApplication.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/Mocks/DebtorRegistryMock.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel; 2 | 3 | namespace LoanApplication.TacticalDdd.Tests.Mocks; 4 | 5 | public class DebtorRegistryMock : IDebtorRegistry 6 | { 7 | public const string DebtorNationalIdentifier = "11111111116"; 8 | public bool IsRegisteredDebtor(Customer customer) 9 | { 10 | return customer.NationalIdentifier == new NationalIdentifier(DebtorNationalIdentifier); 11 | } 12 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.Contracts/LoanApplicationInfoDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LoanApplication.Contracts 4 | { 5 | public class LoanApplicationInfoDto 6 | { 7 | public string Number { get; set; } 8 | public string Status { get; set; } 9 | public string CustomerName { get; set; } 10 | public DateTime? DecisionDate { get; set; } 11 | public decimal LoanAmount { get; set; } 12 | public string DecisionBy { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Application/ApplicationServicesInstaller.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.Application; 2 | 3 | public static class ApplicationServicesInstaller 4 | { 5 | public static void AddApplicationServices(this IServiceCollection services) 6 | { 7 | services.AddScoped(); 8 | services.AddScoped(); 9 | services.AddScoped(); 10 | } 11 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Infrastructure/DataAccess/EfOperatorRepository.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel; 2 | 3 | namespace LoanApplication.TacticalDdd.Infrastructure.DataAccess; 4 | 5 | public class EfOperatorRepository(LoanDbContext dbContext) : IOperatorRepository 6 | { 7 | public void Add(Operator @operator) => dbContext.Operators.Add(@operator); 8 | 9 | public Operator WithLogin(Login login) => dbContext.Operators.FirstOrDefault(o => o.Login == login); 10 | 11 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/Mocks/InMemoryBus.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 3 | 4 | namespace LoanApplication.TacticalDdd.Tests.Mocks; 5 | 6 | public class InMemoryBus : IEventPublisher 7 | { 8 | private readonly List events = new (); 9 | public void Publish(DomainEvent @event) 10 | { 11 | events.Add(@event); 12 | } 13 | 14 | public ReadOnlyCollection Events => events.AsReadOnly(); 15 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Infrastructure/ExternalServices/DebtorRegistry.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel; 2 | 3 | namespace LoanApplication.TacticalDdd.Infrastructure.ExternalServices; 4 | 5 | public class DebtorRegistry : IDebtorRegistry 6 | { 7 | public bool IsRegisteredDebtor(Customer customer) 8 | { 9 | var client = new DebtorRegistryClient(); 10 | var debtorInfo = client.GetDebtorInfo(customer.NationalIdentifier.Value).Result; 11 | 12 | return debtorInfo.Debts.Any(); 13 | } 14 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.BusinessLogic/Customer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LoanApplication.Infrastructure.Common; 3 | 4 | namespace LoanApplication.BusinessLogic 5 | { 6 | public class Customer : Entity 7 | { 8 | public string NationalIdentifier { get; set; } 9 | public string FirstName { get; set; } 10 | public string LastName { get; set; } 11 | public DateTime Birthdate { get; set; } 12 | public decimal MonthlyIncome { get; set; } 13 | public virtual Address Address { get; set; } 14 | 15 | } 16 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/Ddd/DomainEvent.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | 3 | public abstract class DomainEvent 4 | { 5 | public Guid Id { get; protected set; } 6 | public DateTime OccuredOn { get; protected set; } 7 | 8 | protected DomainEvent(Guid id, DateTime occuredOn) 9 | { 10 | Id = id; 11 | OccuredOn = occuredOn; 12 | } 13 | 14 | protected DomainEvent() 15 | { 16 | Id = Guid.NewGuid(); 17 | OccuredOn = SysTime.Now(); 18 | } 19 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/ScoringRulesFactory.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.DomainModel; 2 | 3 | public class ScoringRulesFactory(IDebtorRegistry debtorRegistry) 4 | { 5 | public ScoringRules DefaultSet => new ScoringRules(new List 6 | { 7 | new LoanAmountMustBeLowerThanPropertyValue(), 8 | new CustomerAgeAtTheDateOfLastInstallmentMustBeBelow65(), 9 | new InstallmentAmountMustBeLowerThen15PercentOfCustomerIncome(), 10 | new CustomerIsNotARegisteredDebtor(debtorRegistry) 11 | }); 12 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.Infrastructure/DataAccess/IGenericRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using LoanApplication.Infrastructure.Common; 4 | 5 | namespace LoanApplication.Infrastructure.DataAccess 6 | { 7 | public interface IGenericRepository where TEntity : Entity 8 | { 9 | IQueryable Query(); 10 | 11 | Task GetById(int id); 12 | 13 | Task Create(TEntity entity); 14 | 15 | void Update(int id, TEntity entity); 16 | 17 | Task Delete(int id); 18 | } 19 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Infrastructure/MessageQueue/MessageQueueClientInstaller.cs: -------------------------------------------------------------------------------- 1 | using EasyNetQ; 2 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 3 | 4 | namespace LoanApplication.TacticalDdd.Infrastructure.MessageQueue; 5 | 6 | public static class MessageQueueClientInstaller 7 | { 8 | public static void AddRabbitMqClient(this IServiceCollection services, string brokerAddress) 9 | { 10 | services.AddSingleton(_ => RabbitHutch.CreateBus(brokerAddress)); 11 | services.AddSingleton(); 12 | } 13 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.DataAccess/EfUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using LoanApplication.Infrastructure.DataAccess; 3 | 4 | namespace LoanApplication.DataAccess 5 | { 6 | public class EfUnitOfWork : IUnitOfWork 7 | { 8 | private readonly LoanApplicationDbContext dbContext; 9 | 10 | public EfUnitOfWork(LoanApplicationDbContext dbContext) 11 | { 12 | this.dbContext = dbContext; 13 | } 14 | 15 | public async Task CommitChanges() 16 | { 17 | await dbContext.SaveChangesAsync(); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.DataAccess/LoanApplication.DataAccess.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.WebApi/LoanApplication.WebApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.DataAccess/LoanApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.BusinessLogic; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace LoanApplication.DataAccess 5 | { 6 | public class LoanApplicationDbContext : DbContext 7 | { 8 | public LoanApplicationDbContext(DbContextOptions options) : base(options) 9 | { 10 | 11 | } 12 | 13 | public DbSet LoanApplications { get; set; } 14 | 15 | public DbSet Operators { get; set; } 16 | 17 | 18 | } 19 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/DomainEvents/LoanApplicationAccepted.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | using Newtonsoft.Json; 3 | 4 | namespace LoanApplication.TacticalDdd.DomainModel.DomainEvents; 5 | 6 | public class LoanApplicationAccepted : DomainEvent 7 | { 8 | public Guid LoanApplicationId { get; } 9 | 10 | public LoanApplicationAccepted(LoanApplication loanApplication) 11 | : this(loanApplication.Id.Value) 12 | { 13 | } 14 | 15 | [JsonConstructor] 16 | protected LoanApplicationAccepted(Guid id) 17 | { 18 | LoanApplicationId = id; 19 | } 20 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/DomainEvents/LoanApplicationRejected.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | using Newtonsoft.Json; 3 | 4 | namespace LoanApplication.TacticalDdd.DomainModel.DomainEvents; 5 | 6 | public class LoanApplicationRejected : DomainEvent 7 | { 8 | public Guid LoanApplicationId { get; } 9 | 10 | public LoanApplicationRejected(LoanApplication loanApplication) 11 | : this(loanApplication.Id.Value) 12 | { 13 | } 14 | 15 | [JsonConstructor] 16 | protected LoanApplicationRejected(Guid id) 17 | { 18 | LoanApplicationId = id; 19 | } 20 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Infrastructure/DataAccess/EfLoanApplicationRepository.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel; 2 | 3 | namespace LoanApplication.TacticalDdd.Infrastructure.DataAccess; 4 | 5 | public class EfLoanApplicationRepository(LoanDbContext dbContext) : ILoanApplicationRepository 6 | { 7 | public void Add(DomainModel.LoanApplication loanApplication) => dbContext.LoanApplications.Add(loanApplication); 8 | 9 | public DomainModel.LoanApplication WithNumber(LoanApplicationNumber loanApplicationNumber) => 10 | dbContext.LoanApplications.FirstOrDefault(l => l.Number == loanApplicationNumber); 11 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/DebtorRegistryMock/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:23302", 8 | "sslPort": 44334 9 | } 10 | }, 11 | "profiles": { 12 | "DebtorRegistryMock": { 13 | "commandName": "Project", 14 | "launchBrowser": true, 15 | "launchUrl": "DebtorInfo/11111111116", 16 | "applicationUrl": "http://localhost:5005", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/DebtorRegistryMock/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:23302", 8 | "sslPort": 44334 9 | } 10 | }, 11 | "profiles": { 12 | "DebtorRegistryMock": { 13 | "commandName": "Project", 14 | "launchBrowser": true, 15 | "launchUrl": "DebtorInfo/11111111116", 16 | "applicationUrl": "http://localhost:5005", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/DebtorRegistryMock/Program.cs: -------------------------------------------------------------------------------- 1 | using DebtorRegistryMock; 2 | 3 | var builder = WebApplication.CreateBuilder(); 4 | 5 | var app = builder.Build(); 6 | 7 | app.MapGet("/DebtorInfo/{pesel}", (string pesel) => 8 | { 9 | app.Logger.LogInformation($"Getting debtor info for pesel = {pesel}"); 10 | 11 | if (pesel == "11111111116") 12 | { 13 | return new DebtorInfo 14 | { 15 | Pesel = pesel, 16 | Debts = new () 17 | { 18 | new Debt { Amount = 3000M} 19 | } 20 | }; 21 | } 22 | 23 | return new DebtorInfo 24 | { 25 | Pesel = pesel, 26 | Debts = new () 27 | }; 28 | 29 | }); 30 | 31 | 32 | app.Run(); -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/DomainTests/LoanTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using LoanApplication.TacticalDdd.DomainModel; 3 | using static LoanApplication.TacticalDdd.Tests.Builders.LoanBuilder; 4 | using Xunit; 5 | 6 | namespace LoanApplication.TacticalDdd.Tests.DomainTests; 7 | 8 | public class LoanTest 9 | { 10 | [Fact] 11 | public void Can_calculate_monthly_installment() 12 | { 13 | var loan = GivenLoan() 14 | .WithAmount(420_000M) 15 | .WithNumberOfYears(3) 16 | .WithInterestRate(5M) 17 | .Build(); 18 | 19 | var installment = loan.MonthlyInstallment(); 20 | 21 | installment.Should().Be(new MonetaryAmount(12_587.78M)); 22 | } 23 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/DebtorRegistryMock/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace DebtorRegistryMock 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); 22 | } 23 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.BusinessLogic/LoanApplication.BusinessLogic.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.WebApi/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace LoanApplication.WebApi 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); 22 | } 23 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.4" 2 | services: 3 | dbef: 4 | container_name: dbef 5 | image: postgres:12.0-alpine 6 | volumes: 7 | - pg-data-efdata:/var/lib/postgresql/data 8 | - ./SetupScripts/dbschema:/docker-entrypoint-initdb.d 9 | ports: 10 | - 5434:5432 11 | environment: 12 | POSTGRES_DB: ddd_loans_ef 13 | POSTGRES_USER: lab_user 14 | POSTGRES_PASSWORD: lab_pass 15 | 16 | rabbitmq: 17 | image: "rabbitmq:3-management" 18 | hostname: "rabbit" 19 | ports: 20 | - "15672:15672" 21 | - "5672:5672" 22 | labels: 23 | NAME: "rabbitmq" 24 | volumes: 25 | - ./rabbitmq-isolated.conf:/etc/rabbitmq/rabbitmq.config 26 | 27 | volumes: 28 | pg-data-efdata: -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Infrastructure/ExternalServices/DebtorRegistryClient.cs: -------------------------------------------------------------------------------- 1 | using RestEase; 2 | 3 | namespace LoanApplication.TacticalDdd.Infrastructure.ExternalServices; 4 | 5 | public interface IDebtorRegistryService 6 | { 7 | [Get("{pesel}")] 8 | Task Get([Path] string pesel); 9 | } 10 | 11 | public class DebtorInfo 12 | { 13 | public string Pesel { get; set; } 14 | public List Debts { get; set; } 15 | } 16 | 17 | public class Debt 18 | { 19 | public decimal Amount { get; set; } 20 | } 21 | 22 | public class DebtorRegistryClient 23 | { 24 | public async Task GetDebtorInfo(string pesel) 25 | { 26 | return await RestClient.For("http://localhost:5005/DebtorInfo").Get(pesel); 27 | } 28 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/DomainTests/AgeInYearsTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using LoanApplication.TacticalDdd.DomainModel; 3 | using Xunit; 4 | 5 | namespace LoanApplication.TacticalDdd.Tests.DomainTests; 6 | 7 | public class AgeInYearsTest 8 | { 9 | [Fact] 10 | public void AgeInYears_PersonBorn1974_AfterBirthdateIn2019_45() 11 | { 12 | var age = AgeInYears.Between(new DateOnly(1974, 6, 26), new DateOnly(2019, 11, 28)); 13 | 14 | age.Should().Be(45.Years()); 15 | } 16 | 17 | [Fact] 18 | public void AgeInYears_PersonBorn1974_BeforeBirthdateIn2019_45() 19 | { 20 | var age = AgeInYears.Between(new DateOnly(1974, 6, 26), new DateOnly(2019, 5, 28)); 21 | 22 | age.Should().Be(45.Years()); 23 | } 24 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.BusinessLogic/DebtorRegistryClient.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using RestEase; 4 | 5 | namespace LoanApplication.BusinessLogic 6 | { 7 | public interface IDebtorRegistry 8 | { 9 | [Get("{pesel}")] 10 | Task Get([Path] string pesel); 11 | } 12 | 13 | public class DebtorInfo 14 | { 15 | public string Pesel { get; set; } 16 | public List Debts { get; set; } 17 | } 18 | 19 | public class Debt 20 | { 21 | public decimal Amount { get; set; } 22 | } 23 | 24 | public class DebtorRegistryClient 25 | { 26 | public async Task GetDebtorInfo(string pesel) 27 | { 28 | return await RestClient.For("http://localhost:5005/DebtorInfo").Get(pesel); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/Builders/OperatorBuilder.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel; 2 | 3 | namespace LoanApplication.TacticalDdd.Tests.Builders; 4 | 5 | public class OperatorBuilder 6 | { 7 | private string login = "admin"; 8 | private decimal competenceLevel = 1_000_000M; 9 | 10 | public static OperatorBuilder GivenOperator() => new OperatorBuilder(); 11 | 12 | public OperatorBuilder WithLogin(string login) 13 | { 14 | this.login = login; 15 | return this; 16 | } 17 | 18 | public OperatorBuilder WithCompetenceLevel(decimal level) 19 | { 20 | this.competenceLevel = level; 21 | return this; 22 | } 23 | 24 | public Operator Build() 25 | { 26 | return new Operator(new Login(login), new Password(login),new Name(login,login),new MonetaryAmount(competenceLevel)); 27 | } 28 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/Builders/PropertyBuilder.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel; 2 | 3 | namespace LoanApplication.TacticalDdd.Tests.Builders; 4 | 5 | public class PropertyBuilder 6 | { 7 | private MonetaryAmount value = new MonetaryAmount(400_000M); 8 | private Address address = new Address("PL","00-001","Warsaw","Zielona 6"); 9 | 10 | public PropertyBuilder WithValue(decimal propertyValue) 11 | { 12 | value = new MonetaryAmount(propertyValue); 13 | return this; 14 | } 15 | 16 | public PropertyBuilder WithAddress(string country,string zip,string city,string street) 17 | { 18 | this.address = new Address(country,zip,city,street); 19 | return this; 20 | } 21 | 22 | public Property Build() 23 | { 24 | return new Property 25 | ( 26 | value, 27 | address 28 | ); 29 | } 30 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/Mocks/InMemoryOperatorRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using LoanApplication.TacticalDdd.DomainModel; 3 | 4 | namespace LoanApplication.TacticalDdd.Tests.Mocks; 5 | 6 | public class InMemoryOperatorRepository : IOperatorRepository 7 | { 8 | private readonly ConcurrentDictionary operators = new ConcurrentDictionary(); 9 | 10 | public InMemoryOperatorRepository(IEnumerable initialData) 11 | { 12 | foreach (var @operator in initialData) 13 | { 14 | operators[@operator.Id] = @operator; 15 | } 16 | } 17 | 18 | public void Add(Operator @operator) 19 | { 20 | operators[@operator.Id] = @operator; 21 | } 22 | 23 | public Operator WithLogin(Login login) 24 | { 25 | return operators.Values.FirstOrDefault(o => o.Login == login); 26 | } 27 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/NationalIdentifier.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | 3 | namespace LoanApplication.TacticalDdd.DomainModel; 4 | 5 | public class NationalIdentifier : ValueObject 6 | { 7 | public string Value { get; } 8 | 9 | public NationalIdentifier(string value) 10 | { 11 | if (string.IsNullOrWhiteSpace(value)) 12 | throw new ArgumentException("National Identifier cannot be null or empty string"); 13 | 14 | if (value.Length!=11) 15 | throw new ArgumentException("National Identifier must be 11 chars long"); 16 | 17 | Value = value; 18 | } 19 | 20 | //To satisfy EF Core 21 | protected NationalIdentifier() 22 | { 23 | } 24 | 25 | protected override IEnumerable GetAttributesToIncludeInEqualityCheck() 26 | { 27 | yield return Value; 28 | } 29 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Infrastructure/DataAccess/EfInstaller.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel; 2 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace LoanApplication.TacticalDdd.Infrastructure.DataAccess; 6 | 7 | public static class EfInstaller 8 | { 9 | public static void AddEfDbAdapters(this IServiceCollection services, ConfigurationManager cfgManager) 10 | { 11 | services.AddDbContext(opts => 12 | { 13 | opts 14 | .UseNpgsql(cfgManager.GetConnectionString("LoanDb")); 15 | }); 16 | 17 | services.AddScoped(); 18 | services.AddScoped(); 19 | services.AddScoped(); 20 | services.AddHostedService(); 21 | } 22 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.WebApi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:5341", 8 | "sslPort": 44306 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "weatherforecast", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "LoanApplication.WebApi": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "LoanApplication", 24 | "applicationUrl": "http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:26463", 8 | "sslPort": 44339 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "LoanApplication", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "LoanApplication.TacticalDdd": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "swagger/index.html", 24 | "applicationUrl": "http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/Name.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | 3 | namespace LoanApplication.TacticalDdd.DomainModel; 4 | 5 | public class Name : ValueObject 6 | { 7 | public Name(string first, string last) 8 | { 9 | if (string.IsNullOrWhiteSpace(first)) 10 | throw new ArgumentException("First name cannot be empty"); 11 | if (string.IsNullOrWhiteSpace(last)) 12 | throw new ArgumentException("Last name cannot be empty"); 13 | 14 | First = first; 15 | Last = last; 16 | } 17 | 18 | //To satisfy EF Core 19 | protected Name() 20 | { 21 | } 22 | 23 | public string First { get; private set; } 24 | public string Last { get; private set; } 25 | 26 | protected override IEnumerable GetAttributesToIncludeInEqualityCheck() 27 | { 28 | yield return First; 29 | yield return Last; 30 | } 31 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/Decision.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | using Newtonsoft.Json; 3 | 4 | namespace LoanApplication.TacticalDdd.DomainModel; 5 | 6 | public class Decision : ValueObject 7 | { 8 | public DateOnly DecisionDate { get; } 9 | public OperatorId DecisionBy { get; } 10 | 11 | public Decision(DateOnly decisionDate, Operator decisionBy) 12 | : this(decisionDate,decisionBy.Id) 13 | { 14 | } 15 | 16 | [JsonConstructor] 17 | public Decision(DateOnly decisionDate, OperatorId decisionBy) 18 | { 19 | DecisionDate = decisionDate; 20 | DecisionBy = decisionBy; 21 | } 22 | 23 | // To Satisfy EF Core 24 | protected Decision() 25 | { 26 | } 27 | 28 | protected override IEnumerable GetAttributesToIncludeInEqualityCheck() 29 | { 30 | yield return DecisionDate; 31 | yield return DecisionBy; 32 | } 33 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/Property.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | 3 | namespace LoanApplication.TacticalDdd.DomainModel; 4 | 5 | public class Property : ValueObject 6 | { 7 | public MonetaryAmount Value { get; } 8 | public Address Address { get; } 9 | 10 | public Property(MonetaryAmount value, Address address) 11 | { 12 | if (value==null) 13 | throw new ArgumentException("Value cannot be null"); 14 | if (address==null) 15 | throw new ArgumentException("Address cannot be null"); 16 | if (value <= MonetaryAmount.Zero) 17 | throw new ArgumentException("Property value must be higher than 0"); 18 | 19 | Value = value; 20 | Address = address; 21 | } 22 | 23 | //To satisfy EF Core 24 | protected Property() 25 | { 26 | } 27 | 28 | protected override IEnumerable GetAttributesToIncludeInEqualityCheck() => new List {Value, Address}; 29 | 30 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/Docs/class_model_scoring_rules.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | class ScoringRules <> { 3 | ScoringResult Evaluate(LoanApplication loanApplication) 4 | } 5 | 6 | interface IScoringRule { 7 | bool IsSatisfiedBy(LoanApplication loanApplication) 8 | string Message() 9 | } 10 | 11 | class LoanAmountMustBeLowerThanPropertyValue 12 | 13 | class CustomerAgeAtTheDateOfLastInstallmentMustBeBelow65 14 | 15 | class InstallmentAmountMustBeLowerThen15PercentOfCustomerIncome 16 | 17 | class CustomerIsNotARegisteredDebtor 18 | 19 | interface IDebtorRegistry { 20 | bool IsRegisteredDebtor(Customer customer) 21 | } 22 | 23 | ScoringRules *--> IScoringRule : rules 24 | 25 | IScoringRule <|-- LoanAmountMustBeLowerThanPropertyValue 26 | 27 | IScoringRule <|-- CustomerAgeAtTheDateOfLastInstallmentMustBeBelow65 28 | 29 | IScoringRule <|-- InstallmentAmountMustBeLowerThen15PercentOfCustomerIncome 30 | 31 | IScoringRule <|-- CustomerIsNotARegisteredDebtor 32 | 33 | CustomerIsNotARegisteredDebtor ..> IDebtorRegistry : <> 34 | 35 | @enduml -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/Registration.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | using Newtonsoft.Json; 3 | 4 | namespace LoanApplication.TacticalDdd.DomainModel; 5 | 6 | public class Registration : ValueObject 7 | { 8 | public DateOnly RegistrationDate { get; } 9 | 10 | public OperatorId RegisteredBy { get; } 11 | 12 | public Registration(DateOnly registrationDate, Operator registeredBy) 13 | : this(registrationDate, registeredBy.Id) 14 | { 15 | } 16 | 17 | [JsonConstructor] 18 | public Registration(DateOnly registrationDate, OperatorId registeredBy) 19 | { 20 | RegistrationDate = registrationDate; 21 | RegisteredBy = registeredBy; 22 | } 23 | 24 | //To satisfy EF Core 25 | protected Registration() 26 | { 27 | } 28 | 29 | protected override IEnumerable GetAttributesToIncludeInEqualityCheck() 30 | { 31 | yield return RegistrationDate; 32 | yield return RegisteredBy; 33 | } 34 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Infrastructure/DataAccess/EfDbInitializer.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel; 2 | 3 | namespace LoanApplication.TacticalDdd.Infrastructure.DataAccess; 4 | 5 | public class EfDbInitializer(IServiceProvider serviceProvider) : IHostedService 6 | { 7 | public async Task StartAsync(CancellationToken cancellationToken) 8 | { 9 | using var scope = serviceProvider.CreateScope(); 10 | 11 | var dbCtx = scope.ServiceProvider.GetService(); 12 | 13 | if (!dbCtx.Operators.Any(o=>o.Login=="admin")) 14 | { 15 | dbCtx.Operators.Add(new Operator 16 | ( 17 | new Login("admin"), 18 | new Password("admin"), 19 | new Name("admin","admin"), 20 | new MonetaryAmount(1_000_000M) 21 | )); 22 | 23 | } 24 | 25 | await dbCtx.SaveChangesAsync(cancellationToken); 26 | } 27 | 28 | public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; 29 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.BusinessLogic/LoanApplication.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using LoanApplication.Infrastructure.Common; 5 | 6 | namespace LoanApplication.BusinessLogic 7 | { 8 | public class LoanApplication : Entity 9 | { 10 | public string Number { get; set; } 11 | public LoanApplicationStatus Status { get; set; } 12 | public ApplicationScore? Score { get; set; } 13 | 14 | public string ScoreExplanation { get; set; } 15 | public virtual Customer Customer { get; set; } 16 | public virtual Property Property { get; set; } 17 | public decimal LoanAmount { get; set; } 18 | public int LoanNumberOfYears { get; set; } 19 | public decimal InterestRate { get; set; } 20 | public DateTime? DecisionDate { get; set; } 21 | public virtual Operator DecisionBy { get; set; } 22 | public virtual Operator RegisteredBy { get; set; } 23 | public DateTime RegistrationDate { get; set; } 24 | } 25 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/Builders/LoanBuilder.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel; 2 | 3 | namespace LoanApplication.TacticalDdd.Tests.Builders; 4 | 5 | public class LoanBuilder 6 | { 7 | private MonetaryAmount amount = new MonetaryAmount(200_000M); 8 | private int numberOfYears = 20; 9 | private Percent interestRate = 1.Percent(); 10 | 11 | public static LoanBuilder GivenLoan() => new LoanBuilder(); 12 | 13 | public LoanBuilder WithAmount(decimal loanAmount) 14 | { 15 | amount = new MonetaryAmount(loanAmount); 16 | return this; 17 | } 18 | 19 | public LoanBuilder WithNumberOfYears(int numOfYears) 20 | { 21 | numberOfYears = numOfYears; 22 | return this; 23 | } 24 | 25 | public LoanBuilder WithInterestRate(decimal rate) 26 | { 27 | interestRate = new Percent(rate); 28 | return this; 29 | } 30 | 31 | public Loan Build() 32 | { 33 | return new Loan(amount,numberOfYears,interestRate); 34 | } 35 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Application/Api/LoanApplicationDto.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.Application.Api; 2 | 3 | public record LoanApplicationDto 4 | ( 5 | string Number, 6 | string Status, 7 | string Score, 8 | string CustomerNationalIdentifier, 9 | string CustomerFirstName, 10 | string CustomerLastName, 11 | DateTime CustomerBirthdate, 12 | decimal CustomerMonthlyIncome, 13 | AddressDto CustomerAddress, 14 | decimal PropertyValue, 15 | AddressDto PropertyAddress, 16 | decimal LoanAmount, 17 | int LoanNumberOfYears, 18 | decimal InterestRate, 19 | DateTime? DecisionDate, 20 | string DecisionBy, 21 | string RegisteredBy, 22 | DateTime RegistrationDate 23 | ) 24 | { 25 | //this one is needed to allow dapper to create instance of it using reflection 26 | protected LoanApplicationDto() : this(default, default, default, default, default, default, default, default, 27 | default, default, default, default, default, default, default, default, default, default) 28 | { 29 | } 30 | }; -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.Contracts/LoanApplicationDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LoanApplication.Contracts 4 | { 5 | public class LoanApplicationDto 6 | { 7 | public string Number { get; set; } 8 | public string Status { get; set; } 9 | public string Score { get; set; } 10 | public string CustomerNationalIdentifier { get; set; } 11 | public string CustomerFirstName { get; set; } 12 | public string CustomerLastName { get; set; } 13 | public DateTime CustomerBirthdate { get; set; } 14 | public decimal CustomerMonthlyIncome { get; set; } 15 | public AddressDto CustomerAddress { get; set; } 16 | public decimal PropertyValue { get; set; } 17 | public AddressDto PropertyAddress { get; set; } 18 | public decimal LoanAmount { get; set; } 19 | public int LoanNumberOfYears { get; set; } 20 | public decimal InterestRate { get; set; } 21 | public DateTime? DecisionDate { get; set; } 22 | public string DecisionBy { get; set; } 23 | public string RegisteredBy { get; set; } 24 | } 25 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/LoanApplication.TacticalDdd.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | disable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/Mocks/InMemoryLoanApplicationRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using LoanApplication.TacticalDdd.DomainModel; 3 | 4 | namespace LoanApplication.TacticalDdd.Tests.Mocks; 5 | 6 | public class InMemoryLoanApplicationRepository : ILoanApplicationRepository 7 | { 8 | private readonly ConcurrentDictionary applications = 9 | new ConcurrentDictionary(); 10 | 11 | public InMemoryLoanApplicationRepository(IEnumerable initialData) 12 | { 13 | foreach (var application in initialData) 14 | { 15 | applications[application.Id] = application; 16 | } 17 | } 18 | public void Add(DomainModel.LoanApplication loanApplication) 19 | { 20 | applications[loanApplication.Id] = loanApplication; 21 | } 22 | 23 | public DomainModel.LoanApplication WithNumber(LoanApplicationNumber loanApplicationNumber) 24 | { 25 | return applications.Values.FirstOrDefault(a => a.Number == loanApplicationNumber); 26 | } 27 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/ScoringResult.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | using Newtonsoft.Json; 3 | 4 | namespace LoanApplication.TacticalDdd.DomainModel; 5 | 6 | public class ScoringResult : ValueObject 7 | { 8 | public ApplicationScore? Score { get; } 9 | public string Explanation { get; } 10 | 11 | [JsonConstructor] 12 | private ScoringResult(ApplicationScore? score, string explanation) 13 | { 14 | Score = score; 15 | Explanation = explanation; 16 | } 17 | 18 | //To satisfy EF Core 19 | protected ScoringResult() 20 | { 21 | } 22 | 23 | protected override IEnumerable GetAttributesToIncludeInEqualityCheck() 24 | { 25 | yield return Score; 26 | yield return Explanation; 27 | } 28 | 29 | public static ScoringResult Green() => 30 | new (ApplicationScore.Green, null); 31 | 32 | 33 | public static ScoringResult Red(string[] messages) 34 | => new (ApplicationScore.Red, string.Join(Environment.NewLine,messages)); 35 | 36 | 37 | public bool IsRed() =>Score == ApplicationScore.Red; 38 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Application/LoanApplicationEvaluationService.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.Application; 2 | 3 | using DomainModel; 4 | using DomainModel.Ddd; 5 | 6 | public class LoanApplicationEvaluationService 7 | { 8 | private readonly IUnitOfWork unitOfWork; 9 | private readonly ILoanApplicationRepository loanApplications; 10 | private readonly ScoringRulesFactory scoringRulesFactory; 11 | 12 | public LoanApplicationEvaluationService 13 | ( 14 | IUnitOfWork unitOfWork, 15 | ILoanApplicationRepository loanApplications, 16 | IDebtorRegistry debtorRegistry 17 | ) 18 | { 19 | this.unitOfWork = unitOfWork; 20 | this.loanApplications = loanApplications; 21 | this.scoringRulesFactory = new ScoringRulesFactory(debtorRegistry); 22 | } 23 | public void EvaluateLoanApplication(string applicationNumber) 24 | { 25 | var loanApplication = loanApplications.WithNumber(LoanApplicationNumber.Of(applicationNumber)); 26 | 27 | loanApplication.Evaluate(scoringRulesFactory.DefaultSet); 28 | 29 | unitOfWork.CommitChanges(); 30 | } 31 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Infrastructure/DataAccess/DbInitializer.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.Infrastructure.DataAccess; 2 | 3 | /* 4 | public class DbInitializer : IHostedService 5 | { 6 | private readonly IServiceProvider serviceProvider; 7 | 8 | public DbInitializer(IServiceProvider serviceProvider) 9 | { 10 | this.serviceProvider = serviceProvider; 11 | } 12 | 13 | public async Task StartAsync(CancellationToken cancellationToken) 14 | { 15 | using var scope = serviceProvider.CreateScope(); 16 | 17 | using var session = scope.ServiceProvider.GetService().LightweightSession(); 18 | 19 | if (!session.Query().Any(o=>o.Login=="admin")) 20 | { 21 | session.Insert(new Operator 22 | ( 23 | new Login("admin"), 24 | new Password("admin"), 25 | new Name("admin","admin"), 26 | new MonetaryAmount(1_000_000M) 27 | )); 28 | 29 | } 30 | 31 | await session.SaveChangesAsync(cancellationToken); 32 | } 33 | 34 | public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; 35 | } 36 | */ -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/Ddd/ValueObject.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | 3 | public abstract class ValueObject where T : ValueObject 4 | { 5 | protected abstract IEnumerable GetAttributesToIncludeInEqualityCheck(); 6 | 7 | public override bool Equals(object other) 8 | { 9 | return Equals(other as T); 10 | } 11 | 12 | public virtual bool Equals(T other) 13 | { 14 | if (other == null) 15 | { 16 | return false; 17 | } 18 | return GetAttributesToIncludeInEqualityCheck().SequenceEqual(other.GetAttributesToIncludeInEqualityCheck()); 19 | } 20 | 21 | public static bool operator ==(ValueObject left, ValueObject right) 22 | { 23 | return Equals(left, right); 24 | } 25 | 26 | public static bool operator !=(ValueObject left, ValueObject right) 27 | { 28 | return !(left == right); 29 | } 30 | 31 | public override int GetHashCode() 32 | { 33 | var hash = 17; 34 | foreach (var obj in this.GetAttributesToIncludeInEqualityCheck()) 35 | hash = hash * 31 + (obj == null ? 0 : obj.GetHashCode()); 36 | 37 | return hash; 38 | } 39 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | disable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: .NET 5 | 6 | on: 7 | push: 8 | branches: [ "ef_core" ] 9 | pull_request: 10 | branches: [ "ef_core" ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | # Step 1: Checkout the code 18 | - name: Checkout code 19 | uses: actions/checkout@v3 20 | 21 | # Step 2: Setup .NET Core SDK 22 | - name: Setup .NET Core SDK 23 | uses: actions/setup-dotnet@v3 24 | with: 25 | dotnet-version: '9.0' 26 | 27 | # Step 3: Restore dependencies 28 | - name: Restore dependencies 29 | run: dotnet restore LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.sln 30 | 31 | # Step 4: Build the solution 32 | - name: Build solution 33 | run: dotnet build LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.sln --no-restore --configuration Release 34 | 35 | # Step 5: Run tests 36 | - name: Run tests 37 | run: dotnet test LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/LoanApplication.TacticalDdd.Tests.csproj --no-build --configuration Release 38 | -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/Docs/class_model_decision_services.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | class LoanApplicationDecisionService <> { 3 | void RejectApplication(string applicationNumber, ClaimsPrincipal principal, string rejectionReason) 4 | void AcceptApplication(string applicationNumber, ClaimsPrincipal principal) 5 | } 6 | 7 | 8 | interface ILoanApplicationRepository <> { 9 | void Add(LoanApplication loanApplication) 10 | LoanApplication WithNumber(LoanApplicationNumber loanApplicationNumber) 11 | } 12 | 13 | interface IOperatorRepository <>{ 14 | void Add(Operator @operator) 15 | Operator WithLogin(Login login) 16 | } 17 | 18 | interface IEventPublisher { 19 | void Publish(DomainEvent @event) 20 | } 21 | 22 | 23 | LoanApplicationDecisionService ..> ILoanApplicationRepository : <> 24 | 25 | LoanApplicationDecisionService ..> IOperatorRepository : <> 26 | 27 | LoanApplicationDecisionService ..> IEventPublisher : <> 28 | 29 | 30 | class EfLoanApplicationRepository 31 | 32 | class EfOperatorRepository 33 | 34 | class RabbitMqEventPublisher 35 | 36 | EfLoanApplicationRepository --|> ILoanApplicationRepository 37 | 38 | EfOperatorRepository --|> IOperatorRepository 39 | 40 | RabbitMqEventPublisher --|> IEventPublisher 41 | 42 | @enduml -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.DataAccess/DbInitializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using LoanApplication.BusinessLogic; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | 8 | namespace LoanApplication.DataAccess 9 | { 10 | public class DbInitializer : IHostedService 11 | { 12 | private readonly IServiceProvider serviceProvider; 13 | 14 | public DbInitializer(IServiceProvider serviceProvider) 15 | { 16 | this.serviceProvider = serviceProvider; 17 | } 18 | 19 | 20 | public async Task StartAsync(CancellationToken cancellationToken) 21 | { 22 | using var scope = serviceProvider.CreateScope(); 23 | 24 | var dbCtx = scope.ServiceProvider.GetService(); 25 | 26 | dbCtx.Operators.Add(new Operator 27 | { 28 | FirstName = "admin", 29 | LastName = "admin", 30 | Login = "admin", 31 | CompetenceLevel = 1_000_000M 32 | }); 33 | 34 | await dbCtx.SaveChangesAsync(cancellationToken); 35 | } 36 | 37 | public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; 38 | } 39 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/AgeInYears.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | 3 | namespace LoanApplication.TacticalDdd.DomainModel; 4 | 5 | public class AgeInYears : ValueObject, IComparable 6 | { 7 | private readonly int age; 8 | 9 | public AgeInYears(int age) 10 | { 11 | this.age = age; 12 | } 13 | 14 | public static AgeInYears Between(DateOnly start, DateOnly end) 15 | { 16 | return new AgeInYears(end.Year - start.Year); 17 | } 18 | 19 | protected override IEnumerable GetAttributesToIncludeInEqualityCheck() 20 | { 21 | yield return age; 22 | } 23 | 24 | public static bool operator >(AgeInYears one, AgeInYears two) => one.CompareTo(two)>0; 25 | 26 | public static bool operator <(AgeInYears one, AgeInYears two) => one.CompareTo(two)<0; 27 | 28 | public static bool operator >=(AgeInYears one, AgeInYears two) => one.CompareTo(two)>=0; 29 | 30 | public static bool operator <=(AgeInYears one, AgeInYears two) => one.CompareTo(two)<=0; 31 | 32 | public int CompareTo(AgeInYears other) => age.CompareTo(other.age); 33 | } 34 | 35 | public static class AgeInYearsExtensions 36 | { 37 | public static AgeInYears Years(this int age) => new AgeInYears(age); 38 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/Docs/class_model_aggregates.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | class Customer <> { 3 | NationalIdentifier NationalIdentifier 4 | Name Name 5 | DateTime Birthdate 6 | MonetaryAmount MonthlyIncome 7 | Address Address 8 | AgeInYears AgeInYearsAt(DateTime date) 9 | } 10 | 11 | class LoanApplication <> { 12 | LoanApplicationNumber Number 13 | LoanApplicationStatus Status 14 | ScoringResult Score 15 | Decision Decision 16 | Registration Registration 17 | 18 | void Evaluate(ScoringRules rules) 19 | void Accept(Operator decisionBy) 20 | void Reject(Operator decisionBy) 21 | } 22 | 23 | class Loan <> { 24 | MonetaryAmount LoanAmount 25 | int LoanNumberOfYears 26 | Percent InterestRate 27 | 28 | MonetaryAmount MonthlyInstallment() 29 | DateTime LastInstallmentsDate() 30 | } 31 | 32 | class Property <> { 33 | MonetaryAmount Value 34 | Address Address 35 | } 36 | 37 | class Operator <> { 38 | bool CanAccept(MonetaryAmount loanLoanAmount) 39 | } 40 | 41 | class ScoringRules <> 42 | 43 | LoanApplication *--> Customer : customer 44 | 45 | 46 | LoanApplication *--> Loan : loan 47 | 48 | LoanApplication *--> Property : property 49 | 50 | LoanApplication ..> ScoringRules : <> 51 | 52 | @enduml -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.DataAccess/GenericRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using LoanApplication.Infrastructure.Common; 4 | using LoanApplication.Infrastructure.DataAccess; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace LoanApplication.DataAccess 8 | { 9 | public class GenericRepository : IGenericRepository where TEntity : Entity 10 | { 11 | private readonly LoanApplicationDbContext dbContext; 12 | 13 | public GenericRepository(LoanApplicationDbContext dbContext) 14 | { 15 | this.dbContext = dbContext; 16 | } 17 | 18 | public IQueryable Query() 19 | { 20 | return dbContext.Set(); 21 | } 22 | 23 | public async Task GetById(int id) 24 | { 25 | return await dbContext.Set() 26 | .FirstOrDefaultAsync(e => e.Id == id); 27 | } 28 | 29 | public async Task Create(TEntity entity) 30 | { 31 | await dbContext.Set().AddAsync(entity); 32 | } 33 | 34 | public void Update(int id, TEntity entity) 35 | { 36 | dbContext.Set().Update(entity); 37 | } 38 | 39 | public async Task Delete(int id) 40 | { 41 | var entity = await GetById(id); 42 | dbContext.Set().Remove(entity); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/Asserts/DomainEventsAssert.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using FluentAssertions.Execution; 3 | using FluentAssertions.Primitives; 4 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 5 | 6 | namespace LoanApplication.TacticalDdd.Tests.Asserts; 7 | 8 | public static class DomainEventsAssertExtension 9 | { 10 | public static DomainEventsAssert Should(this IEnumerable events) 11 | { 12 | return new DomainEventsAssert(events); 13 | } 14 | } 15 | 16 | public class DomainEventsAssert : ReferenceTypeAssertions,DomainEventsAssert> 17 | { 18 | public DomainEventsAssert(IEnumerable events) : base(events) 19 | { 20 | } 21 | 22 | public AndConstraint HaveExpectedNumberOfEvents(int expectedNumberOfEvents) 23 | { 24 | Subject.Count().Should().Be(expectedNumberOfEvents); 25 | return new AndConstraint(this); 26 | } 27 | 28 | public AndConstraint ContainEvent(Predicate matcher) where T : DomainEvent 29 | { 30 | Execute.Assertion 31 | .ForCondition(Subject.Any(e => e.GetType() == typeof(T) && matcher((T) e))) 32 | .FailWith("List of events does not contain any that meets criteria"); 33 | 34 | return new AndConstraint(this); 35 | } 36 | 37 | protected override string Identifier => "DomainEventsAssert"; 38 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/DebtorRegistryMock/Controllers/DebtorInfoController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace DebtorRegistryMock.Controllers 9 | { 10 | [ApiController] 11 | [Route("[controller]")] 12 | public class DebtorInfoController : ControllerBase 13 | { 14 | private readonly ILogger logger; 15 | 16 | public DebtorInfoController(ILogger logger) 17 | { 18 | this.logger = logger; 19 | } 20 | 21 | [HttpGet("{pesel}")] 22 | public DebtorInfo Get([FromRoute]string pesel) 23 | { 24 | logger.Log(LogLevel.Information, $"Getting debtor info for pesel = {pesel}"); 25 | 26 | if (pesel == "11111111116") 27 | { 28 | return new DebtorInfo 29 | { 30 | Pesel = pesel, 31 | Debts = new List 32 | { 33 | new Debt { Amount = 3000M} 34 | } 35 | }; 36 | } 37 | else 38 | { 39 | return new DebtorInfo 40 | { 41 | Pesel = pesel, 42 | Debts = new List() 43 | }; 44 | } 45 | } 46 | 47 | } 48 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/Percent.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | 3 | namespace LoanApplication.TacticalDdd.DomainModel; 4 | 5 | public class Percent : ValueObject, IComparable 6 | { 7 | public decimal Value { get; } 8 | 9 | public static readonly Percent Zero = new Percent(0M); 10 | 11 | public Percent(decimal value) 12 | { 13 | if (value < 0) 14 | throw new ArgumentException("Percent value cannot be negative"); 15 | 16 | Value = value; 17 | } 18 | 19 | //To satisfy EF Core 20 | protected Percent() 21 | { 22 | } 23 | 24 | public static bool operator >(Percent one, Percent two) => one.CompareTo(two)>0; 25 | 26 | public static bool operator <(Percent one, Percent two) => one.CompareTo(two)<0; 27 | 28 | public static bool operator >=(Percent one, Percent two) => one.CompareTo(two)>=0; 29 | 30 | public static bool operator <=(Percent one, Percent two) => one.CompareTo(two)<=0; 31 | 32 | public int CompareTo(Percent other) => Value.CompareTo(other.Value); 33 | 34 | protected override IEnumerable GetAttributesToIncludeInEqualityCheck() 35 | { 36 | yield return Value; 37 | } 38 | } 39 | 40 | public static class PercentExtensions 41 | { 42 | public static Percent Percent(this int value) => new Percent(value); 43 | 44 | public static Percent Percent(this decimal value) => new Percent(value); 45 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Application/LoanApplicationDecisionService.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using LoanApplication.TacticalDdd.DomainModel; 3 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 4 | using LoanApplication.TacticalDdd.DomainModel.DomainEvents; 5 | 6 | namespace LoanApplication.TacticalDdd.Application; 7 | 8 | public class LoanApplicationDecisionService(IUnitOfWork unitOfWork, ILoanApplicationRepository loanApplications, IOperatorRepository operators, IEventPublisher eventPublisher) 9 | { 10 | 11 | public void RejectApplication(string applicationNumber, ClaimsPrincipal principal, string rejectionReason) 12 | { 13 | var loanApplication = loanApplications.WithNumber(LoanApplicationNumber.Of(applicationNumber)); 14 | var user = operators.WithLogin(Login.Of(principal.Identity.Name)); 15 | 16 | loanApplication.Reject(user); 17 | 18 | unitOfWork.CommitChanges(); 19 | 20 | eventPublisher.Publish(new LoanApplicationRejected(loanApplication)); 21 | } 22 | 23 | public void AcceptApplication(string applicationNumber, ClaimsPrincipal principal) 24 | { 25 | var loanApplication = loanApplications.WithNumber(LoanApplicationNumber.Of(applicationNumber)); 26 | var user = operators.WithLogin(Login.Of(principal.Identity.Name)); 27 | 28 | loanApplication.Accept(user); 29 | 30 | unitOfWork.CommitChanges(); 31 | 32 | eventPublisher.Publish(new LoanApplicationAccepted(loanApplication)); 33 | } 34 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/DebtorRegistryMock/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.HttpsPolicy; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Hosting; 12 | using Microsoft.Extensions.Logging; 13 | 14 | namespace DebtorRegistryMock 15 | { 16 | public class Startup 17 | { 18 | public Startup(IConfiguration configuration) 19 | { 20 | Configuration = configuration; 21 | } 22 | 23 | public IConfiguration Configuration { get; } 24 | 25 | // This method gets called by the runtime. Use this method to add services to the container. 26 | public void ConfigureServices(IServiceCollection services) 27 | { 28 | services.AddControllers(); 29 | } 30 | 31 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 32 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 33 | { 34 | if (env.IsDevelopment()) 35 | { 36 | app.UseDeveloperExceptionPage(); 37 | } 38 | 39 | app.UseHttpsRedirection(); 40 | 41 | app.UseRouting(); 42 | 43 | app.UseAuthorization(); 44 | 45 | app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/Address.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 3 | 4 | namespace LoanApplication.TacticalDdd.DomainModel; 5 | 6 | public class Address : ValueObject
7 | { 8 | public string Country { get; } 9 | public string ZipCode { get; } 10 | public string City { get; } 11 | public string Street { get; } 12 | 13 | public Address(string country, string zipCode, string city, string street) 14 | { 15 | if (string.IsNullOrWhiteSpace(country)) 16 | throw new ArgumentException("Country cannot be empty."); 17 | if (string.IsNullOrWhiteSpace(zipCode)) 18 | throw new ArgumentException("Zip code cannot be empty."); 19 | if (string.IsNullOrWhiteSpace(city)) 20 | throw new ArgumentException("City cannot be empty."); 21 | if (string.IsNullOrWhiteSpace(street)) 22 | throw new ArgumentException("Street cannot be empty."); 23 | if (!new Regex("[0-9]{2}-[0-9]{3}").Match(zipCode).Success) 24 | throw new ArgumentException("Zip code must be NN-NNN format."); 25 | 26 | Country = country; 27 | ZipCode = zipCode; 28 | City = city; 29 | Street = street; 30 | } 31 | 32 | //To satisfy EF Core 33 | protected Address() 34 | { 35 | } 36 | 37 | protected override IEnumerable GetAttributesToIncludeInEqualityCheck() 38 | { 39 | yield return Country; 40 | yield return ZipCode; 41 | yield return City; 42 | yield return Street; 43 | } 44 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/Customer.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | 3 | namespace LoanApplication.TacticalDdd.DomainModel; 4 | 5 | public class Customer : ValueObject 6 | { 7 | public NationalIdentifier NationalIdentifier { get; } 8 | public Name Name { get; } 9 | public DateOnly Birthdate { get; } 10 | public MonetaryAmount MonthlyIncome { get; } 11 | public Address Address { get; } 12 | 13 | public Customer 14 | ( 15 | NationalIdentifier nationalIdentifier, 16 | Name name, 17 | DateOnly birthdate, 18 | MonetaryAmount monthlyIncome, 19 | Address address 20 | ) 21 | { 22 | if (nationalIdentifier==null) 23 | throw new ArgumentException("National identifier cannot be null"); 24 | if (name==null) 25 | throw new ArgumentException("Name cannot be null"); 26 | if (monthlyIncome==null) 27 | throw new ArgumentException("Monthly income cannot be null"); 28 | if (address==null) 29 | throw new ArgumentException("Address cannot be null"); 30 | if (birthdate==default) 31 | throw new ArgumentException("Birthdate cannot be empty"); 32 | 33 | NationalIdentifier = nationalIdentifier; 34 | Name = name; 35 | Birthdate = birthdate; 36 | MonthlyIncome = monthlyIncome; 37 | Address = address; 38 | } 39 | 40 | //To satisfy EF Core 41 | protected Customer() 42 | { 43 | } 44 | 45 | public AgeInYears AgeInYearsAt(DateOnly date) => AgeInYears.Between(Birthdate, date); 46 | 47 | protected override IEnumerable GetAttributesToIncludeInEqualityCheck() 48 | { 49 | return new List 50 | { 51 | NationalIdentifier, 52 | Name, 53 | Birthdate, 54 | MonthlyIncome, 55 | Address 56 | }; 57 | } 58 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/Builders/CustomerBuilder.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel; 2 | 3 | namespace LoanApplication.TacticalDdd.Tests.Builders; 4 | 5 | public class CustomerBuilder 6 | { 7 | private Name name = new Name("Jan","B"); 8 | private NationalIdentifier nationalIdentifier = new NationalIdentifier("11111111111"); 9 | private MonetaryAmount income = new MonetaryAmount(15_500M); 10 | private Address address = new Address("PL","00-001","Warsaw","Zielona 6"); 11 | private DateOnly birthDate = SysTime.Today().AddYears(-25); 12 | 13 | public static CustomerBuilder GivenCustomer() => new CustomerBuilder(); 14 | 15 | public CustomerBuilder WithIdentifier(string nationalId) 16 | { 17 | nationalIdentifier = new NationalIdentifier(nationalId); 18 | return this; 19 | } 20 | 21 | public CustomerBuilder WithName(string first, string last) 22 | { 23 | name = new Name(first,last); 24 | return this; 25 | } 26 | 27 | public CustomerBuilder WithAge(int age) 28 | { 29 | this.birthDate = SysTime.Today().AddYears(-1 * age); 30 | return this; 31 | } 32 | 33 | public CustomerBuilder BornOn(DateOnly birthDate) 34 | { 35 | this.birthDate = birthDate; 36 | return this; 37 | } 38 | 39 | public CustomerBuilder WithIncome(decimal income) 40 | { 41 | this.income = new MonetaryAmount(income); 42 | return this; 43 | } 44 | 45 | public CustomerBuilder WithAddress(string country,string zip,string city,string street) 46 | { 47 | this.address = new Address(country,zip,city,street); 48 | return this; 49 | } 50 | 51 | public Customer Build() 52 | { 53 | return new Customer 54 | ( 55 | nationalIdentifier, 56 | name, 57 | birthDate, 58 | income, 59 | address 60 | ); 61 | } 62 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/MonetaryAmount.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | 3 | namespace LoanApplication.TacticalDdd.DomainModel; 4 | 5 | public class MonetaryAmount : ValueObject, IComparable 6 | { 7 | public decimal Amount { get; } 8 | 9 | public static readonly MonetaryAmount Zero = new MonetaryAmount(0M); 10 | 11 | //To satisfy EF Core 12 | protected MonetaryAmount() 13 | { 14 | } 15 | 16 | public MonetaryAmount(decimal amount) => Amount = decimal.Round(amount,2,MidpointRounding.ToEven); 17 | 18 | public MonetaryAmount Add(MonetaryAmount other) => new MonetaryAmount(Amount + other.Amount); 19 | 20 | public MonetaryAmount Subtract(MonetaryAmount other) => new MonetaryAmount(Amount - other.Amount); 21 | 22 | public MonetaryAmount MultiplyByPercent(Percent percent) => new MonetaryAmount((this.Amount * percent.Value)/100M); 23 | 24 | public static MonetaryAmount operator +(MonetaryAmount one, MonetaryAmount two) => one.Add(two); 25 | 26 | public static MonetaryAmount operator -(MonetaryAmount one, MonetaryAmount two) => one.Subtract(two); 27 | 28 | public static MonetaryAmount operator *(MonetaryAmount one, Percent percent) => one.MultiplyByPercent(percent); 29 | 30 | public static bool operator >(MonetaryAmount one, MonetaryAmount two) => one.CompareTo(two)>0; 31 | 32 | public static bool operator <(MonetaryAmount one, MonetaryAmount two) => one.CompareTo(two)<0; 33 | 34 | public static bool operator >=(MonetaryAmount one, MonetaryAmount two) => one.CompareTo(two)>=0; 35 | 36 | public static bool operator <=(MonetaryAmount one, MonetaryAmount two) => one.CompareTo(two)<=0; 37 | 38 | protected override IEnumerable GetAttributesToIncludeInEqualityCheck() 39 | { 40 | yield return Amount; 41 | } 42 | 43 | public int CompareTo(MonetaryAmount other) => Amount.CompareTo(other.Amount); 44 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/DomainTests/MonetaryAmountTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using LoanApplication.TacticalDdd.DomainModel; 3 | using Xunit; 4 | 5 | namespace LoanApplication.TacticalDdd.Tests.DomainTests; 6 | 7 | public class MonetaryAmountTests 8 | { 9 | [Fact] 10 | public void The_same_amounts_are_equal() 11 | { 12 | var one = new MonetaryAmount(10M); 13 | var two = new MonetaryAmount(10M); 14 | 15 | one.Equals(two).Should().BeTrue(); 16 | (one == two).Should().BeTrue(); 17 | } 18 | 19 | [Fact] 20 | public void Not_the_same_amounts_are_not_equal() 21 | { 22 | var one = new MonetaryAmount(10M); 23 | var two = new MonetaryAmount(11M); 24 | 25 | one.Equals(two).Should().BeFalse(); 26 | (one != two).Should().BeTrue(); 27 | } 28 | 29 | [Fact] 30 | public void Two_amounts_can_be_compared() 31 | { 32 | var one = new MonetaryAmount(10M); 33 | var two = new MonetaryAmount(11M); 34 | 35 | (one < two).Should().BeTrue(); 36 | (one > two).Should().BeFalse(); 37 | } 38 | 39 | [Fact] 40 | public void Can_add() 41 | { 42 | var one = new MonetaryAmount(10M); 43 | var two = new MonetaryAmount(11M); 44 | 45 | var sum = one + two; 46 | 47 | sum.Should().Be(new MonetaryAmount(21M)); 48 | } 49 | 50 | [Fact] 51 | public void Can_subtract() 52 | { 53 | var one = new MonetaryAmount(10M); 54 | var two = new MonetaryAmount(5M); 55 | 56 | var diff = one - two; 57 | 58 | diff.Should().Be(new MonetaryAmount(5M)); 59 | } 60 | 61 | [Fact] 62 | public void Can_multiply_by_percent() 63 | { 64 | var one = new MonetaryAmount(10M); 65 | var tenPercent = 10.Percent(); 66 | 67 | var percentOfOne = one * tenPercent; 68 | 69 | percentOfOne.Should().Be(new MonetaryAmount(1M)); 70 | } 71 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoanApplication.TacticalDdd", "LoanApplication.TacticalDdd\LoanApplication.TacticalDdd.csproj", "{1DF6D93E-C743-4F5C-BD16-19AA58E8377B}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoanApplication.TacticalDdd.Tests", "LoanApplication.TacticalDdd.Tests\LoanApplication.TacticalDdd.Tests.csproj", "{C3BD8217-C23B-4DB1-9E14-84F5C8F2DD27}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DebtorRegistryMock", "DebtorRegistryMock\DebtorRegistryMock.csproj", "{7FC39857-218B-457B-A051-3D986E6BC44B}" 8 | EndProject 9 | Global 10 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 11 | Debug|Any CPU = Debug|Any CPU 12 | Release|Any CPU = Release|Any CPU 13 | EndGlobalSection 14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 15 | {1DF6D93E-C743-4F5C-BD16-19AA58E8377B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 16 | {1DF6D93E-C743-4F5C-BD16-19AA58E8377B}.Debug|Any CPU.Build.0 = Debug|Any CPU 17 | {1DF6D93E-C743-4F5C-BD16-19AA58E8377B}.Release|Any CPU.ActiveCfg = Release|Any CPU 18 | {1DF6D93E-C743-4F5C-BD16-19AA58E8377B}.Release|Any CPU.Build.0 = Release|Any CPU 19 | {C3BD8217-C23B-4DB1-9E14-84F5C8F2DD27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {C3BD8217-C23B-4DB1-9E14-84F5C8F2DD27}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {C3BD8217-C23B-4DB1-9E14-84F5C8F2DD27}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {C3BD8217-C23B-4DB1-9E14-84F5C8F2DD27}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {7FC39857-218B-457B-A051-3D986E6BC44B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {7FC39857-218B-457B-A051-3D986E6BC44B}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {7FC39857-218B-457B-A051-3D986E6BC44B}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {7FC39857-218B-457B-A051-3D986E6BC44B}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | GlobalSection(NestedProjects) = preSolution 29 | EndGlobalSection 30 | EndGlobal 31 | -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Security/BasicAuthenticationHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using System.Security.Claims; 3 | using System.Text; 4 | using System.Text.Encodings.Web; 5 | using Microsoft.AspNetCore.Authentication; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace LoanApplication.TacticalDdd.Security; 9 | 10 | public class BasicAuthenticationHandler( 11 | IOptionsMonitor options, 12 | ILoggerFactory logger, 13 | UrlEncoder encoder) 14 | : AuthenticationHandler(options, logger, encoder) 15 | { 16 | protected override async Task HandleAuthenticateAsync() 17 | { 18 | if (!Request.Headers.ContainsKey("Authorization")) 19 | return AuthenticateResult.Fail("Missing Authorization Header"); 20 | 21 | string validatedLogin; 22 | try 23 | { 24 | var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]); 25 | var credentialBytes = Convert.FromBase64String(authHeader.Parameter); 26 | var credentials = Encoding.UTF8.GetString(credentialBytes).Split([':'], 2); 27 | var username = credentials[0]; 28 | var password = credentials[1]; 29 | validatedLogin = (username=="admin" && password=="admin") ? "admin" : null; 30 | } 31 | catch 32 | { 33 | return AuthenticateResult.Fail("Invalid Authorization Header"); 34 | } 35 | 36 | if (validatedLogin == null) 37 | return AuthenticateResult.Fail("Invalid Username or Password"); 38 | 39 | var claims = new[] { 40 | new Claim(ClaimTypes.NameIdentifier, validatedLogin), 41 | new Claim(ClaimTypes.Name, validatedLogin), 42 | }; 43 | var identity = new ClaimsIdentity(claims, Scheme.Name); 44 | var principal = new ClaimsPrincipal(identity); 45 | var ticket = new AuthenticationTicket(principal, Scheme.Name); 46 | 47 | return AuthenticateResult.Success(ticket); 48 | } 49 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/Loan.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | using static System.Math; 3 | using static System.Linq.Enumerable; 4 | 5 | namespace LoanApplication.TacticalDdd.DomainModel; 6 | 7 | public class Loan : ValueObject 8 | { 9 | public MonetaryAmount LoanAmount { get; } 10 | public int LoanNumberOfYears { get; } 11 | public Percent InterestRate { get; } 12 | 13 | public Loan(MonetaryAmount loanAmount, int loanNumberOfYears, Percent interestRate) 14 | { 15 | if (loanAmount==null) 16 | throw new ArgumentException("Loan amount cannot be null"); 17 | if (interestRate==null) 18 | throw new ArgumentException("Interest rate cannot be null"); 19 | if (loanAmount<=MonetaryAmount.Zero) 20 | throw new ArgumentException("Loan amount must be grated than 0"); 21 | if (interestRate <= Percent.Zero) 22 | throw new ArgumentException("Interest rate must be higher than 0"); 23 | if (loanNumberOfYears<=0) 24 | throw new ArgumentException("Loan number of years must be greater than 0"); 25 | 26 | LoanAmount = loanAmount; 27 | LoanNumberOfYears = loanNumberOfYears; 28 | InterestRate = interestRate; 29 | } 30 | 31 | public MonetaryAmount MonthlyInstallment() 32 | { 33 | var totalInstallments = LoanNumberOfYears * 12; 34 | 35 | var x = Range(1, totalInstallments).Sum( 36 | i => Pow(1.0 + (double) InterestRate.Value / 100 / 12, -1 * i)); 37 | 38 | return new MonetaryAmount(LoanAmount.Amount / Convert.ToDecimal(x)); 39 | } 40 | 41 | public DateOnly LastInstallmentsDate() => SysTime.Today().AddYears(LoanNumberOfYears); 42 | 43 | protected override IEnumerable GetAttributesToIncludeInEqualityCheck() 44 | { 45 | return new List 46 | { 47 | LoanAmount, 48 | LoanNumberOfYears, 49 | InterestRate 50 | }; 51 | } 52 | 53 | //To Satisfy EF Core 54 | protected Loan() 55 | { 56 | } 57 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Program.cs: -------------------------------------------------------------------------------- 1 | using Carter; 2 | using LoanApplication.TacticalDdd.Application; 3 | using LoanApplication.TacticalDdd.Application.Api; 4 | using LoanApplication.TacticalDdd.Infrastructure.DataAccess; 5 | using LoanApplication.TacticalDdd.Infrastructure.ExternalServices; 6 | using LoanApplication.TacticalDdd.Infrastructure.MessageQueue; 7 | using LoanApplication.TacticalDdd.Security; 8 | using LoanApplication.TacticalDdd.ReadModel; 9 | using Microsoft.AspNetCore.Authentication; 10 | using Microsoft.OpenApi.Models; 11 | 12 | var builder = WebApplication.CreateBuilder(); 13 | 14 | builder.Services 15 | .AddAuthentication("BasicAuthentication") 16 | .AddScheme("BasicAuthentication", opts => 17 | { 18 | opts.TimeProvider = TimeProvider.System; 19 | }); 20 | builder.Services.AddAuthorization(); 21 | builder.Services.AddFluentValidators(); 22 | builder.Services.AddEfDbAdapters(builder.Configuration); 23 | builder.Services.AddRabbitMqClient("host=localhost"); 24 | builder.Services.AddExternalServicesClients(); 25 | builder.Services.AddApplicationServices(); 26 | builder.Services.AddReadModelServices(builder.Configuration); 27 | builder.Services.AddEndpointsApiExplorer(); 28 | builder.Services.AddSwaggerGen(options => 29 | { 30 | options.AddSecurityDefinition("basic", new OpenApiSecurityScheme 31 | { 32 | Name = "Authorization", 33 | Type = SecuritySchemeType.Http, 34 | Scheme = "basic", 35 | In = ParameterLocation.Header, 36 | Description = "Basic Auth" 37 | }); 38 | options.AddSecurityRequirement(new OpenApiSecurityRequirement 39 | { 40 | { 41 | new OpenApiSecurityScheme 42 | { 43 | Reference = new OpenApiReference 44 | { 45 | Type = ReferenceType.SecurityScheme, 46 | Id="basic" 47 | } 48 | }, 49 | Array.Empty() 50 | } 51 | }); 52 | }); 53 | builder.Services.AddCarter(); 54 | 55 | var app = builder.Build(); 56 | 57 | 58 | app.UseAuthentication(); 59 | app.UseAuthorization(); 60 | app.UseSwagger(); 61 | app.UseSwaggerUI(); 62 | app.MapCarter(); 63 | 64 | app.Run(); -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/Asserts/LoanApplicationAssert.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using FluentAssertions.Primitives; 3 | using LoanApplication.TacticalDdd.DomainModel; 4 | 5 | namespace LoanApplication.TacticalDdd.Tests.Asserts; 6 | 7 | public static class LoanApplicationAssertExtension 8 | { 9 | public static LoanApplicationAssert Should(this DomainModel.LoanApplication loanApplication) 10 | => new LoanApplicationAssert(loanApplication); 11 | } 12 | 13 | public class LoanApplicationAssert : ReferenceTypeAssertions 14 | { 15 | public LoanApplicationAssert(DomainModel.LoanApplication loanApplication) 16 | : base(loanApplication) 17 | { 18 | 19 | } 20 | 21 | public AndConstraint BeInStatus(LoanApplicationStatus expectedStatus) 22 | { 23 | Subject.Status.Should().Be(expectedStatus); 24 | return new AndConstraint(this); 25 | } 26 | 27 | public AndConstraint BeAccepted() 28 | { 29 | return BeInStatus(LoanApplicationStatus.Accepted); 30 | } 31 | 32 | public AndConstraint BeRejected() 33 | { 34 | return BeInStatus(LoanApplicationStatus.Rejected); 35 | } 36 | 37 | public AndConstraint BeNew() 38 | { 39 | return BeInStatus(LoanApplicationStatus.New); 40 | } 41 | 42 | public AndConstraint ScoreIsNull() 43 | { 44 | Subject.Score.Should().BeNull(); 45 | return new AndConstraint(this); 46 | } 47 | 48 | public AndConstraint ScoreIs(ApplicationScore expectedScore) 49 | { 50 | Subject.Score?.Score.Should().Be(expectedScore); 51 | return new AndConstraint(this); 52 | } 53 | 54 | public AndConstraint HaveRedScore() 55 | { 56 | return ScoreIs(ApplicationScore.Red); 57 | } 58 | 59 | public AndConstraint HaveGreenScore() 60 | { 61 | return ScoreIs(ApplicationScore.Green); 62 | } 63 | 64 | protected override string Identifier => "LoanApplicationAssert"; 65 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.BusinessLogic/ScoringService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace LoanApplication.BusinessLogic 7 | { 8 | public class ScoringService 9 | { 10 | private readonly IList messages = new List(); 11 | private readonly DebtorRegistryClient debtorRegistryClient; 12 | 13 | public ScoringService(DebtorRegistryClient debtorRegistryClient) 14 | { 15 | this.debtorRegistryClient = debtorRegistryClient; 16 | } 17 | 18 | public async Task EvaluateApplication(LoanApplication loanApplication) 19 | { 20 | var score = ApplicationScore.Green; 21 | var explanation = new List(); 22 | 23 | //property value 24 | if (loanApplication.Property.Value < loanApplication.LoanAmount) 25 | { 26 | score = ApplicationScore.Red; 27 | explanation.Add("Property value is lower than loan amount."); 28 | } 29 | 30 | //max age 31 | if (DateTime.Now.Year + loanApplication.LoanNumberOfYears - loanApplication.Customer.Birthdate.Year > 65) 32 | { 33 | score = ApplicationScore.Red; 34 | explanation.Add("Customer age at last installment date is above 65"); 35 | } 36 | 37 | //income vs installment 38 | if (loanApplication.LoanAmount / (loanApplication.LoanNumberOfYears * 12) > 39 | loanApplication.Customer.MonthlyIncome * 0.15M) 40 | { 41 | score = ApplicationScore.Red; 42 | explanation.Add("Installment is higher than 15% of customer's income"); 43 | } 44 | 45 | //is debtor 46 | var debtorInfo = await debtorRegistryClient.GetDebtorInfo(loanApplication.Customer.NationalIdentifier); 47 | if (debtorInfo.Debts.Any()) 48 | { 49 | score = ApplicationScore.Red; 50 | explanation.Add("Customer is registered in debtor registry"); 51 | } 52 | 53 | loanApplication.Score = score; 54 | loanApplication.ScoreExplanation = string.Join(Environment.NewLine,explanation.ToArray()); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.Infrastructure/Security/BasicAuthenticationHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http.Headers; 3 | using System.Security.Claims; 4 | using System.Text; 5 | using System.Text.Encodings.Web; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Authentication; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.Extensions.Options; 10 | 11 | namespace LoanApplication.Infrastructure.Security 12 | { 13 | public class BasicAuthenticationHandler : AuthenticationHandler 14 | { 15 | public BasicAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) 16 | { 17 | } 18 | 19 | protected override async Task HandleAuthenticateAsync() 20 | { 21 | if (!Request.Headers.ContainsKey("Authorization")) 22 | return AuthenticateResult.Fail("Missing Authorization Header"); 23 | 24 | string validatedLogin; 25 | try 26 | { 27 | var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]); 28 | var credentialBytes = Convert.FromBase64String(authHeader.Parameter); 29 | var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2); 30 | var username = credentials[0]; 31 | var password = credentials[1]; 32 | validatedLogin = (username=="admin" && password=="admin") ? "admin" : null; 33 | } 34 | catch 35 | { 36 | return AuthenticateResult.Fail("Invalid Authorization Header"); 37 | } 38 | 39 | if (validatedLogin == null) 40 | return AuthenticateResult.Fail("Invalid Username or Password"); 41 | 42 | var claims = new[] { 43 | new Claim(ClaimTypes.NameIdentifier, validatedLogin), 44 | new Claim(ClaimTypes.Name, validatedLogin), 45 | }; 46 | var identity = new ClaimsIdentity(claims, Scheme.Name); 47 | var principal = new ClaimsPrincipal(identity); 48 | var ticket = new AuthenticationTicket(principal, Scheme.Name); 49 | 50 | return AuthenticateResult.Success(ticket); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Application/Api/LoanApplicationSubmissionDto.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace LoanApplication.TacticalDdd.Application.Api; 4 | 5 | public record LoanApplicationSubmissionDto 6 | ( 7 | string CustomerNationalIdentifier, 8 | string CustomerFirstName, 9 | string CustomerLastName, 10 | DateTime CustomerBirthdate, 11 | decimal CustomerMonthlyIncome, 12 | AddressDto CustomerAddress, 13 | decimal PropertyValue, 14 | AddressDto PropertyAddress, 15 | decimal LoanAmount, 16 | int LoanNumberOfYears, 17 | decimal InterestRate 18 | ); 19 | 20 | public class LoanApplicationSubmissionDtoValidator : AbstractValidator 21 | { 22 | public LoanApplicationSubmissionDtoValidator() 23 | { 24 | RuleFor(l => l.CustomerNationalIdentifier) 25 | .NotEmpty() 26 | .MinimumLength(11) 27 | .MaximumLength(11); 28 | 29 | RuleFor(l => l.CustomerFirstName) 30 | .NotEmpty(); 31 | 32 | RuleFor(l => l.CustomerLastName) 33 | .NotEmpty(); 34 | 35 | RuleFor(l => l.CustomerBirthdate) 36 | .NotEmpty(); 37 | 38 | RuleFor(l => l.CustomerMonthlyIncome) 39 | .GreaterThanOrEqualTo(0); 40 | 41 | RuleFor(l => l.PropertyValue) 42 | .GreaterThanOrEqualTo(0); 43 | 44 | 45 | RuleFor(l => l.LoanAmount) 46 | .GreaterThan(0); 47 | 48 | 49 | RuleFor(l => l.LoanNumberOfYears) 50 | .GreaterThan(0); 51 | 52 | RuleFor(l => l.InterestRate) 53 | .GreaterThan(0); 54 | 55 | RuleFor(l => l.CustomerAddress) 56 | .NotNull() 57 | .SetValidator(new AddressDtoValidator()); 58 | 59 | RuleFor(l => l.PropertyAddress) 60 | .NotNull() 61 | .SetValidator(new AddressDtoValidator()); 62 | } 63 | } 64 | 65 | 66 | public class AddressDtoValidator : AbstractValidator 67 | { 68 | public AddressDtoValidator() 69 | { 70 | RuleFor(a => a.Country) 71 | .NotEmpty(); 72 | 73 | RuleFor(a => a.ZipCode) 74 | .NotEmpty(); 75 | 76 | RuleFor(a => a.City) 77 | .NotEmpty(); 78 | 79 | RuleFor(a => a.Street) 80 | .NotEmpty(); 81 | } 82 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/ScoringRules.cs: -------------------------------------------------------------------------------- 1 | namespace LoanApplication.TacticalDdd.DomainModel; 2 | 3 | public class ScoringRules(IList rules) 4 | { 5 | public ScoringResult Evaluate(LoanApplication loanApplication) 6 | { 7 | var brokenRules = rules 8 | .Where(r => !r.IsSatisfiedBy(loanApplication)) 9 | .ToList(); 10 | 11 | return brokenRules.Any() ? 12 | ScoringResult.Red(brokenRules.Select(r=>r.Message).ToArray()) : ScoringResult.Green(); 13 | } 14 | } 15 | 16 | public interface IScoringRule 17 | { 18 | bool IsSatisfiedBy(LoanApplication loanApplication); 19 | 20 | string Message { get; } 21 | } 22 | 23 | public class LoanAmountMustBeLowerThanPropertyValue : IScoringRule 24 | { 25 | public bool IsSatisfiedBy(LoanApplication loanApplication) 26 | { 27 | return loanApplication.Loan.LoanAmount < loanApplication.Property.Value; 28 | } 29 | 30 | public string Message => "Property value is lower than loan amount."; 31 | } 32 | 33 | public class CustomerAgeAtTheDateOfLastInstallmentMustBeBelow65 : IScoringRule 34 | { 35 | public bool IsSatisfiedBy(LoanApplication loanApplication) 36 | { 37 | var lastInstallmentDate = loanApplication.Loan.LastInstallmentsDate(); 38 | return loanApplication.Customer.AgeInYearsAt(lastInstallmentDate) < 65.Years(); 39 | } 40 | 41 | public string Message => "Customer age at last installment date is above 65."; 42 | } 43 | 44 | public class InstallmentAmountMustBeLowerThen15PercentOfCustomerIncome : IScoringRule 45 | { 46 | public bool IsSatisfiedBy(LoanApplication loanApplication) 47 | { 48 | return loanApplication.Loan.MonthlyInstallment() 49 | < loanApplication.Customer.MonthlyIncome * 15.Percent(); 50 | } 51 | 52 | public string Message => "Installment is higher than 15% of customer's income."; 53 | } 54 | 55 | public class CustomerIsNotARegisteredDebtor : IScoringRule 56 | { 57 | private readonly IDebtorRegistry debtorRegistry; 58 | 59 | public CustomerIsNotARegisteredDebtor(IDebtorRegistry debtorRegistry) 60 | { 61 | this.debtorRegistry = debtorRegistry; 62 | } 63 | 64 | public bool IsSatisfiedBy(LoanApplication loanApplication) 65 | { 66 | return !debtorRegistry.IsRegisteredDebtor(loanApplication.Customer); 67 | } 68 | 69 | public string Message => "Customer is registered in debtor registry"; 70 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.BusinessLogic/ValidationService.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace LoanApplication.BusinessLogic 4 | { 5 | public class ValidationService 6 | { 7 | public void ValidateLoanApplication(LoanApplication loanApplication) 8 | { 9 | new LoanApplicationValidator().ValidateAndThrow(loanApplication); 10 | } 11 | 12 | class LoanApplicationValidator : AbstractValidator 13 | { 14 | public LoanApplicationValidator() 15 | { 16 | RuleFor(x => x.Number).NotEmpty(); 17 | RuleFor(x => x.Customer).NotEmpty().SetValidator(new CustomerValidator()); 18 | RuleFor(x => x.Property).NotEmpty().SetValidator(new PropertyValidator()); 19 | RuleFor(x => x.LoanAmount).NotEmpty().GreaterThan(0M); 20 | RuleFor(x => x.LoanNumberOfYears).NotEmpty().GreaterThanOrEqualTo(1); 21 | RuleFor(x => x.InterestRate).NotEmpty().GreaterThan(0M); 22 | RuleFor(x => x.RegisteredBy).NotEmpty(); 23 | RuleFor(x => x.RegistrationDate).NotEmpty(); 24 | } 25 | } 26 | 27 | class CustomerValidator : AbstractValidator 28 | { 29 | public CustomerValidator() 30 | { 31 | RuleFor(x => x.FirstName).NotEmpty(); 32 | RuleFor(x => x.LastName).NotEmpty(); 33 | RuleFor(x => x.NationalIdentifier).NotEmpty().MinimumLength(11).MaximumLength(11); 34 | RuleFor(x => x.Birthdate).NotEmpty(); 35 | RuleFor(x => x.MonthlyIncome).NotEmpty(); 36 | RuleFor(x => x.Address).NotEmpty().SetValidator(new AddressValidator()); 37 | } 38 | } 39 | 40 | class PropertyValidator : AbstractValidator 41 | { 42 | public PropertyValidator() 43 | { 44 | RuleFor(x => x.Value).NotEmpty().GreaterThan(0M); 45 | RuleFor(x => x.Address).NotEmpty().SetValidator(new AddressValidator()); 46 | } 47 | } 48 | 49 | class AddressValidator : AbstractValidator
50 | { 51 | public AddressValidator() 52 | { 53 | RuleFor(x => x.Country).NotEmpty(); 54 | RuleFor(x => x.City).NotEmpty(); 55 | RuleFor(x => x.Street).NotEmpty(); 56 | RuleFor(x => x.ZipCode).NotEmpty().Matches("[0-9]{2}-[0-9]{3}"); 57 | 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Application/LoanApplicationSubmissionService.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using LoanApplication.TacticalDdd.Application.Api; 3 | using LoanApplication.TacticalDdd.DomainModel; 4 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 5 | 6 | namespace LoanApplication.TacticalDdd.Application; 7 | 8 | public class LoanApplicationSubmissionService(IUnitOfWork unitOfWork,ILoanApplicationRepository loanApplications, IOperatorRepository operators) 9 | { 10 | public string SubmitLoanApplication(LoanApplicationSubmissionDto loanApplicationDto, ClaimsPrincipal principal) 11 | { 12 | var user = operators.WithLogin(Login.Of(principal.Identity.Name)); 13 | 14 | var application = new DomainModel.LoanApplication 15 | ( 16 | LoanApplicationNumber.NewNumber(), 17 | new Customer 18 | ( 19 | new NationalIdentifier(loanApplicationDto.CustomerNationalIdentifier), 20 | new Name(loanApplicationDto.CustomerFirstName, loanApplicationDto.CustomerLastName), 21 | DateOnly.FromDateTime(loanApplicationDto.CustomerBirthdate), 22 | new MonetaryAmount(loanApplicationDto.CustomerMonthlyIncome), 23 | new Address 24 | ( 25 | loanApplicationDto.CustomerAddress.Country, 26 | loanApplicationDto.CustomerAddress.ZipCode, 27 | loanApplicationDto.CustomerAddress.City, 28 | loanApplicationDto.CustomerAddress.Street 29 | ) 30 | ), 31 | new Property 32 | ( 33 | new MonetaryAmount(loanApplicationDto.PropertyValue), 34 | new Address 35 | ( 36 | loanApplicationDto.PropertyAddress.Country, 37 | loanApplicationDto.PropertyAddress.ZipCode, 38 | loanApplicationDto.PropertyAddress.City, 39 | loanApplicationDto.PropertyAddress.Street 40 | ) 41 | ), 42 | new Loan 43 | ( 44 | new MonetaryAmount(loanApplicationDto.LoanAmount), 45 | loanApplicationDto.LoanNumberOfYears, 46 | new Percent(loanApplicationDto.InterestRate) 47 | ), 48 | user 49 | ); 50 | 51 | loanApplications.Add(application); 52 | 53 | unitOfWork.CommitChanges(); 54 | 55 | return application.Number; 56 | 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/DomainTests/PropertyTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using LoanApplication.TacticalDdd.DomainModel; 3 | using Xunit; 4 | 5 | namespace LoanApplication.TacticalDdd.Tests.DomainTests; 6 | 7 | public class PropertyTest 8 | { 9 | [Fact] 10 | public void PropertiesWithTheSameValueAndAddress_AreEqual() 11 | { 12 | var propOne = new Property 13 | ( 14 | new MonetaryAmount(100000), 15 | new Address 16 | ( 17 | "PL", 18 | "01-001", 19 | "Warsaw", 20 | "Zielona 7" 21 | ) 22 | ); 23 | var propTwo = new Property 24 | ( 25 | new MonetaryAmount(100000), 26 | new Address 27 | ( 28 | "PL", 29 | "01-001", 30 | "Warsaw", 31 | "Zielona 7" 32 | ) 33 | ); 34 | 35 | propOne.Equals(propTwo).Should().BeTrue(); 36 | } 37 | 38 | [Fact] 39 | public void PropertiesWithDifferentValueAndTheSameAddress_AreNotEqual() 40 | { 41 | var propOne = new Property 42 | ( 43 | new MonetaryAmount(100000), 44 | new Address 45 | ( 46 | "PL", 47 | "01-001", 48 | "Warsaw", 49 | "Zielona 7" 50 | ) 51 | ); 52 | var propTwo = new Property 53 | ( 54 | new MonetaryAmount(100001), 55 | new Address 56 | ( 57 | "PL", 58 | "01-001", 59 | "Warsaw", 60 | "Zielona 7" 61 | ) 62 | ); 63 | 64 | propOne.Equals(propTwo).Should().BeFalse(); 65 | } 66 | 67 | [Fact] 68 | public void PropertiesWithTheSameValueAndDifferentAddress_AreNotEqual() 69 | { 70 | var propOne = new Property 71 | ( 72 | new MonetaryAmount(100000), 73 | new Address 74 | ( 75 | "PL", 76 | "01-001", 77 | "Warsaw", 78 | "Zielona 7" 79 | ) 80 | ); 81 | var propTwo = new Property 82 | ( 83 | new MonetaryAmount(100000), 84 | new Address 85 | ( 86 | "PL", 87 | "01-001", 88 | "Warsaw", 89 | "Zielona 8" 90 | ) 91 | ); 92 | 93 | propOne.Equals(propTwo).Should().BeFalse(); 94 | } 95 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/Operator.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | 3 | namespace LoanApplication.TacticalDdd.DomainModel; 4 | 5 | public class Operator : Entity 6 | { 7 | public Login Login { get; private set; } 8 | public Password Password { get; private set; } 9 | public Name Name { get; private set; } 10 | public MonetaryAmount CompetenceLevel { get; private set; } 11 | 12 | public Operator(Login login, Password password, Name name, MonetaryAmount competenceLevel) 13 | { 14 | Id = new OperatorId(Guid.NewGuid()); 15 | Login = login; 16 | Password = password; 17 | Name = name; 18 | CompetenceLevel = competenceLevel; 19 | } 20 | 21 | //To satisfy EF Core 22 | protected Operator() 23 | { 24 | } 25 | 26 | public bool CanAccept(MonetaryAmount loanLoanAmount) => loanLoanAmount <= CompetenceLevel; 27 | 28 | } 29 | 30 | public class OperatorId : ValueObject 31 | { 32 | public Guid Value { get; } 33 | 34 | public OperatorId(Guid value) 35 | { 36 | Value = value; 37 | } 38 | 39 | protected override IEnumerable GetAttributesToIncludeInEqualityCheck() 40 | { 41 | yield return Value; 42 | } 43 | 44 | protected OperatorId() 45 | { 46 | } 47 | } 48 | 49 | 50 | public class Login : ValueObject 51 | { 52 | public string Value { get; } 53 | public Login(string value) 54 | { 55 | if (string.IsNullOrWhiteSpace(value)) 56 | throw new ArgumentException("Login cannot be null or empty string"); 57 | Value = value; 58 | } 59 | 60 | 61 | public static Login Of(string login) => new Login(login); 62 | 63 | public static implicit operator string(Login login) => login.Value; 64 | 65 | protected override IEnumerable GetAttributesToIncludeInEqualityCheck() 66 | { 67 | yield return Value; 68 | } 69 | } 70 | 71 | public class Password : ValueObject 72 | { 73 | public string Value { get; } 74 | public Password(string value) 75 | { 76 | if (string.IsNullOrWhiteSpace(value)) 77 | throw new ArgumentException("Password cannot be null or empty string"); 78 | Value = value; 79 | } 80 | 81 | 82 | public static Password Of(string value) => new Password(value); 83 | 84 | public static implicit operator string(Password password) => password.Value; 85 | 86 | protected override IEnumerable GetAttributesToIncludeInEqualityCheck() 87 | { 88 | yield return Value; 89 | } 90 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/ApplicationTests/LoanApplicationEvaluationServiceTests.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.Application; 2 | using LoanApplication.TacticalDdd.DomainModel; 3 | using LoanApplication.TacticalDdd.Tests.Asserts; 4 | using LoanApplication.TacticalDdd.Tests.Mocks; 5 | using Xunit; 6 | using static LoanApplication.TacticalDdd.Tests.Builders.LoanApplicationBuilder; 7 | 8 | namespace LoanApplication.TacticalDdd.Tests.ApplicationTests; 9 | 10 | public class LoanApplicationEvaluationServiceTests 11 | { 12 | [Fact] 13 | public void LoanApplicationEvaluationService_ApplicationThatSatisfiesAllRules_IsEvaluatedGreen() 14 | { 15 | var existingApplications = new InMemoryLoanApplicationRepository(new [] 16 | { 17 | GivenLoanApplication() 18 | .WithNumber("123") 19 | .WithCustomer(customer => customer.WithAge(25).WithIncome(15_000M)) 20 | .WithLoan(loan => loan.WithAmount(200_000).WithNumberOfYears(25).WithInterestRate(1.1M)) 21 | .WithProperty(prop => prop.WithValue(250_000M)) 22 | .Build() 23 | }); 24 | 25 | var evaluationService = new LoanApplicationEvaluationService 26 | ( 27 | new UnitOfWorkMock(), 28 | existingApplications, 29 | new DebtorRegistryMock() 30 | ); 31 | 32 | evaluationService.EvaluateLoanApplication("123"); 33 | 34 | existingApplications.WithNumber(new LoanApplicationNumber("123")) 35 | .Should() 36 | .HaveGreenScore(); 37 | } 38 | 39 | [Fact] 40 | public void LoanApplicationEvaluationService_ApplicationThatDoesNotSatisfyAllRules_IsEvaluatedRedAndRejected() 41 | { 42 | var existingApplications = new InMemoryLoanApplicationRepository(new [] 43 | { 44 | GivenLoanApplication() 45 | .WithNumber("123") 46 | .WithCustomer(customer => customer.WithAge(55).WithIncome(15_000M)) 47 | .WithLoan(loan => loan.WithAmount(200_000).WithNumberOfYears(25).WithInterestRate(1.1M)) 48 | .WithProperty(prop => prop.WithValue(250_000M)) 49 | .Build() 50 | }); 51 | 52 | var evaluationService = new LoanApplicationEvaluationService 53 | ( 54 | new UnitOfWorkMock(), 55 | existingApplications, 56 | new DebtorRegistryMock() 57 | ); 58 | 59 | evaluationService.EvaluateLoanApplication("123"); 60 | 61 | existingApplications.WithNumber(new LoanApplicationNumber("123")) 62 | .Should() 63 | .HaveRedScore() 64 | .And 65 | .BeRejected(); 66 | } 67 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.WebApi/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using LoanApplication.BusinessLogic; 6 | using LoanApplication.DataAccess; 7 | using LoanApplication.Infrastructure.DataAccess; 8 | using LoanApplication.Infrastructure.Security; 9 | using Microsoft.AspNetCore.Authentication; 10 | using Microsoft.AspNetCore.Builder; 11 | using Microsoft.AspNetCore.Hosting; 12 | using Microsoft.AspNetCore.HttpsPolicy; 13 | using Microsoft.AspNetCore.Mvc; 14 | using Microsoft.EntityFrameworkCore; 15 | using Microsoft.Extensions.Configuration; 16 | using Microsoft.Extensions.DependencyInjection; 17 | using Microsoft.Extensions.Hosting; 18 | using Microsoft.Extensions.Logging; 19 | 20 | namespace LoanApplication.WebApi 21 | { 22 | public class Startup 23 | { 24 | public Startup(IConfiguration configuration) 25 | { 26 | Configuration = configuration; 27 | } 28 | 29 | public IConfiguration Configuration { get; } 30 | 31 | // This method gets called by the runtime. Use this method to add services to the container. 32 | public void ConfigureServices(IServiceCollection services) 33 | { 34 | services.AddControllers(); 35 | 36 | services.AddAuthentication("BasicAuthentication") 37 | .AddScheme("BasicAuthentication", null); 38 | 39 | services.AddScoped(); 40 | services.AddScoped(); 41 | services.AddScoped(); 42 | services.AddSingleton(); 43 | 44 | services.AddDbContext(opts => 45 | { 46 | opts.UseInMemoryDatabase("Loans"); 47 | opts.UseLazyLoadingProxies(); 48 | }); 49 | services.AddHostedService(); 50 | 51 | services.AddScoped(); 52 | services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>)); 53 | } 54 | 55 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 56 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 57 | { 58 | if (env.IsDevelopment()) 59 | { 60 | app.UseDeveloperExceptionPage(); 61 | } 62 | 63 | app.UseHttpsRedirection(); 64 | 65 | app.UseRouting(); 66 | 67 | app.UseAuthentication(); 68 | app.UseAuthorization(); 69 | 70 | app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.WebApi/Controllers/LoanApplicationController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Security.Claims; 3 | using System.Threading.Tasks; 4 | using LoanApplication.BusinessLogic; 5 | using LoanApplication.Contracts; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | namespace LoanApplication.WebApi.Controllers 10 | { 11 | [Authorize] 12 | [ApiController] 13 | [Route("[controller]")] 14 | public class LoanApplicationController : ControllerBase 15 | { 16 | private readonly LoanApplicationService loanApplicationService; 17 | 18 | public LoanApplicationController(LoanApplicationService loanApplicationService) 19 | { 20 | this.loanApplicationService = loanApplicationService; 21 | } 22 | 23 | [HttpPost] 24 | public async Task Create([FromBody] LoanApplicationDto loanApplicationDto) 25 | { 26 | var newApplicationNumber = await loanApplicationService.CreateLoanApplication(loanApplicationDto, User); 27 | return await loanApplicationService.GetLoanApplication(newApplicationNumber); 28 | } 29 | 30 | [HttpPost("find")] 31 | public async Task> Find([FromBody] LoanApplicationSearchCriteriaDto criteria) 32 | { 33 | return await loanApplicationService.FindLoanApplication(criteria); 34 | } 35 | 36 | [HttpPost("evaluate/{applicationNumber}")] 37 | public async Task Evaluate([FromRoute] string applicationNumber) 38 | { 39 | await loanApplicationService.EvaluateLoanApplication(applicationNumber); 40 | return await loanApplicationService.GetLoanApplication(applicationNumber); 41 | } 42 | 43 | [HttpPost("accept/{applicationNumber}")] 44 | public async Task Accept([FromRoute] string applicationNumber) 45 | { 46 | await loanApplicationService.AcceptApplication(applicationNumber,User); 47 | return await loanApplicationService.GetLoanApplication(applicationNumber); 48 | } 49 | 50 | [HttpPost("reject/{applicationNumber}")] 51 | public async Task Reject([FromRoute] string applicationNumber) 52 | { 53 | await loanApplicationService.RejectApplication(applicationNumber,User, null); 54 | return await loanApplicationService.GetLoanApplication(applicationNumber); 55 | } 56 | 57 | [HttpGet("{applicationNumber}")] 58 | public async Task Get([FromRoute] string applicationNumber) 59 | { 60 | return await loanApplicationService.GetLoanApplication(applicationNumber); 61 | } 62 | 63 | 64 | } 65 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Web/LoanApplicationApi.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using LoanApplication.TacticalDdd.Application; 3 | using LoanApplication.TacticalDdd.Application.Api; 4 | using LoanApplication.TacticalDdd.ReadModel; 5 | using O9d.AspNet.FluentValidation; 6 | 7 | namespace LoanApplication.TacticalDdd.Web; 8 | 9 | using Carter; 10 | 11 | public class LoanApplicationApi : ICarterModule 12 | { 13 | public void AddRoutes(IEndpointRouteBuilder app) 14 | { 15 | var group = app.MapGroup("LoanApplication") 16 | .RequireAuthorization() 17 | .WithValidationFilter(); 18 | 19 | group 20 | .MapPost("",([Validate] LoanApplicationSubmissionDto loanApplicationDto,ClaimsPrincipal user, LoanApplicationSubmissionService loanApplicationSubmissionService) => 21 | { 22 | var newApplicationNumber = loanApplicationSubmissionService.SubmitLoanApplication(loanApplicationDto, user); 23 | return Results.Ok(newApplicationNumber); 24 | }) 25 | .Produces() 26 | .WithName("SubmitLoanApplication"); 27 | 28 | group 29 | .MapPut("evaluate/{applicationNumber}", (string applicationNumber, LoanApplicationEvaluationService loanApplicationEvaluationService) => 30 | { 31 | loanApplicationEvaluationService.EvaluateLoanApplication(applicationNumber); 32 | return Results.Ok(); 33 | }) 34 | .Produces(200); 35 | 36 | group 37 | .MapPut("accept/{applicationNumber}", (string applicationNumber, ClaimsPrincipal user, LoanApplicationDecisionService loanApplicationDecisionService) => 38 | { 39 | loanApplicationDecisionService.AcceptApplication(applicationNumber,user); 40 | return Results.Ok(); 41 | }) 42 | .Produces(200); 43 | 44 | 45 | group 46 | .MapPut("reject/{applicationNumber}", (string applicationNumber, ClaimsPrincipal user ,LoanApplicationDecisionService loanApplicationDecisionService) => 47 | { 48 | loanApplicationDecisionService.RejectApplication(applicationNumber,user, null); 49 | return Results.Ok(); 50 | }) 51 | .Produces(200); 52 | 53 | 54 | group 55 | .MapGet("{applicationNumber}", (string applicationNumber, LoanApplicationFinder loanApplicationFinder) => loanApplicationFinder.GetLoanApplication(applicationNumber)) 56 | .Produces(); 57 | 58 | group 59 | .MapGet("find", (string applicationNumber, string customerNationalIdentifier,string decisionBy,string registeredBy, LoanApplicationFinder loanApplicationFinder) 60 | => loanApplicationFinder.FindLoadApplication(new LoanApplicationSearchCriteriaDto(applicationNumber,customerNationalIdentifier,decisionBy,registeredBy))) 61 | .Produces>(); 62 | 63 | } 64 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/SetupScripts/dbschema/schema_and_tables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE public."Operators" 2 | ( 3 | "Password" character varying(50), 4 | "FirstName" character varying(50), 5 | "LastName" character varying(75), 6 | "CompetenceLevel_Amount" numeric(19,2), 7 | "Id" uuid NOT NULL, 8 | "Login" character varying(50), 9 | CONSTRAINT operators_pk PRIMARY KEY ("Id") 10 | ); 11 | 12 | CREATE TABLE public."LoanApplications" 13 | ( 14 | "Number" character varying(50), 15 | "Status" character varying(50), 16 | "Score_Score" character varying(50), 17 | "Score_Explanation" character varying(250), 18 | "Customer_NationalIdentifier_Value" character varying(50), 19 | "Customer_Name_First" character varying(50), 20 | "Customer_Name_Last" character varying(50), 21 | "Customer_Birthdate" date, 22 | "Customer_MonthlyIncome_Amount" numeric(19,2), 23 | "Customer_Address_Country" character varying(50), 24 | "Customer_Address_ZipCode" character varying(50), 25 | "Customer_Address_City" character varying(50), 26 | "Customer_Address_Street" character varying(50), 27 | "Property_Value_Amount" numeric(19,2), 28 | "Property_Address_Country" character varying(50), 29 | "Property_Address_ZipCode" character varying(50), 30 | "Property_Address_City" character varying(50), 31 | "Property_Address_Street" character varying(50), 32 | "Loan_LoanAmount_Amount" numeric(19,2), 33 | "Loan_LoanNumberOfYears" integer, 34 | "Loan_InterestRate_Value" numeric(19,2), 35 | "Decision_DecisionDate" date, 36 | "Decision_DecisionBy_Value" uuid, 37 | "Registration_RegistrationDate" date, 38 | "Registration_RegisteredBy_Value" uuid, 39 | "Id" uuid NOT NULL, 40 | CONSTRAINT loan_applications_pk PRIMARY KEY ("Id") 41 | ); 42 | 43 | 44 | 45 | 46 | CREATE OR REPLACE VIEW loan_details_view AS 47 | SELECT 48 | loan."Id" id, 49 | loan."Number" AS number, 50 | loan."Status" AS status, 51 | loan."Score_Score" AS score, 52 | loan."Customer_NationalIdentifier_Value" AS customernationalidentifier, 53 | loan."Customer_Name_First" AS customerfirstname, 54 | loan."Customer_Name_Last" AS customerlastname, 55 | loan."Customer_Birthdate" AS customerbirthdate, 56 | loan."Customer_MonthlyIncome_Amount" AS customermonthlyincome, 57 | loan."Customer_Address_Country" AS customeraddress_country, 58 | loan."Customer_Address_ZipCode" AS customeraddress_zipcode, 59 | loan."Customer_Address_City" AS customeraddress_city, 60 | loan."Customer_Address_Street" AS customeraddress_street, 61 | loan."Property_Value_Amount" AS propertyvalue, 62 | loan."Property_Address_Country" AS propertyaddress_country, 63 | loan."Property_Address_ZipCode" AS propertyaddress_zipcode, 64 | loan."Property_Address_City" AS propertyaddress_city, 65 | loan."Property_Address_Street" AS propertyaddress_street, 66 | loan."Loan_LoanAmount_Amount" AS loanamount, 67 | loan."Loan_LoanNumberOfYears" AS loannumberofyears, 68 | loan."Loan_InterestRate_Value" AS interestrate, 69 | loan."Decision_DecisionDate" AS decisiondate, 70 | opdec."Login" AS decisionby, 71 | opreg."Login" AS registeredby, 72 | loan."Registration_RegistrationDate" AS registrationdate 73 | FROM "LoanApplications" loan 74 | LEFT JOIN "Operators" opreg ON loan."Registration_RegisteredBy_Value" = opreg."Id" 75 | LEFT JOIN "Operators" opdec ON loan."Decision_DecisionBy_Value" = opdec."Id"; -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/Builders/LoanApplicationBuilder.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel; 2 | using LoanApplication.TacticalDdd.Tests.Mocks; 3 | 4 | namespace LoanApplication.TacticalDdd.Tests.Builders; 5 | 6 | public class LoanApplicationBuilder 7 | { 8 | private Operator user = new Operator(new Login("admin"), new Password("admin"), new Name("admin", "admin"), new MonetaryAmount(1_000_000)); 9 | private Customer customer = new CustomerBuilder().Build(); 10 | private Property property = new PropertyBuilder().Build(); 11 | private Loan loan = new LoanBuilder().Build(); 12 | private LoanApplicationNumber applicationNumber = new LoanApplicationNumber(Guid.NewGuid().ToString()); 13 | private bool evaluated = false; 14 | private LoanApplicationStatus targetStatus = LoanApplicationStatus.New; 15 | private ScoringRulesFactory scoringRulesFactory = new ScoringRulesFactory(new DebtorRegistryMock()); 16 | public static LoanApplicationBuilder GivenLoanApplication() => new LoanApplicationBuilder(); 17 | 18 | public LoanApplicationBuilder Accepted() 19 | { 20 | targetStatus = LoanApplicationStatus.Accepted; 21 | return this; 22 | } 23 | 24 | public LoanApplicationBuilder Rejected() 25 | { 26 | targetStatus = LoanApplicationStatus.Rejected; 27 | return this; 28 | } 29 | 30 | public LoanApplicationBuilder Evaluated() 31 | { 32 | evaluated = true; 33 | return this; 34 | } 35 | 36 | public LoanApplicationBuilder NotEvaluated() 37 | { 38 | evaluated = false; 39 | return this; 40 | } 41 | 42 | public LoanApplicationBuilder WithNumber(string number) 43 | { 44 | applicationNumber = new LoanApplicationNumber(number); 45 | return this; 46 | } 47 | 48 | public LoanApplicationBuilder WithOperator(string login) 49 | { 50 | user = new Operator(new Login(login), new Password(login),new Name(login,login),new MonetaryAmount(1_000_000)); 51 | return this; 52 | } 53 | 54 | public LoanApplicationBuilder WithCustomer(Action customizeCustomer) 55 | { 56 | var customerBuilder = new CustomerBuilder(); 57 | customizeCustomer(customerBuilder); 58 | customer = customerBuilder.Build(); 59 | return this; 60 | } 61 | 62 | public LoanApplicationBuilder WithProperty(Action propertyCustomizer) 63 | { 64 | var propertyBuilder = new PropertyBuilder(); 65 | propertyCustomizer(propertyBuilder); 66 | property = propertyBuilder.Build(); 67 | return this; 68 | } 69 | 70 | public LoanApplicationBuilder WithLoan(Action loanCustomizer) 71 | { 72 | var loanBuilder = new LoanBuilder(); 73 | loanCustomizer(loanBuilder); 74 | loan = loanBuilder.Build(); 75 | return this; 76 | } 77 | 78 | public DomainModel.LoanApplication Build() 79 | { 80 | var application = new DomainModel.LoanApplication 81 | ( 82 | applicationNumber, 83 | customer, 84 | property, 85 | loan, 86 | user 87 | ); 88 | 89 | if (evaluated) 90 | { 91 | application.Evaluate(scoringRulesFactory.DefaultSet); 92 | } 93 | 94 | if (targetStatus == LoanApplicationStatus.Accepted) 95 | { 96 | application.Accept(user); 97 | } 98 | 99 | if (targetStatus == LoanApplicationStatus.Rejected) 100 | { 101 | application.Reject(user); 102 | } 103 | 104 | return application; 105 | } 106 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.EnterpriseCake.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoanApplication.Contracts", "LoanApplication.Contracts\LoanApplication.Contracts.csproj", "{4D293290-E4AA-4D9B-8476-0A5D1E87A944}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoanApplication.BusinessLogic", "LoanApplication.BusinessLogic\LoanApplication.BusinessLogic.csproj", "{C4A1BAE8-CDB3-4084-A3A2-6A449D9F0B38}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoanApplication.DataAccess", "LoanApplication.DataAccess\LoanApplication.DataAccess.csproj", "{CA95B150-82B2-482A-96BC-01CA79689F30}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoanApplication.Infrastructure", "LoanApplication.Infrastructure\LoanApplication.Infrastructure.csproj", "{75D28097-658B-4945-842D-6A647C4672ED}" 10 | EndProject 11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoanApplication.WebApi", "LoanApplication.WebApi\LoanApplication.WebApi.csproj", "{6E907BB7-13F2-48BC-8D01-89544EE7479C}" 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DebtorRegistryMock", "DebtorRegistryMock\DebtorRegistryMock.csproj", "{016E7E78-31A1-45A3-9C09-446A64BB038A}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {C7630968-27B0-4035-A387-2E941B00A5CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {C7630968-27B0-4035-A387-2E941B00A5CE}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {C7630968-27B0-4035-A387-2E941B00A5CE}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {C7630968-27B0-4035-A387-2E941B00A5CE}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {4D293290-E4AA-4D9B-8476-0A5D1E87A944}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {4D293290-E4AA-4D9B-8476-0A5D1E87A944}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {4D293290-E4AA-4D9B-8476-0A5D1E87A944}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {4D293290-E4AA-4D9B-8476-0A5D1E87A944}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {C4A1BAE8-CDB3-4084-A3A2-6A449D9F0B38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {C4A1BAE8-CDB3-4084-A3A2-6A449D9F0B38}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {C4A1BAE8-CDB3-4084-A3A2-6A449D9F0B38}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {C4A1BAE8-CDB3-4084-A3A2-6A449D9F0B38}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {CA95B150-82B2-482A-96BC-01CA79689F30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {CA95B150-82B2-482A-96BC-01CA79689F30}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {CA95B150-82B2-482A-96BC-01CA79689F30}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {CA95B150-82B2-482A-96BC-01CA79689F30}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {75D28097-658B-4945-842D-6A647C4672ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {75D28097-658B-4945-842D-6A647C4672ED}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {75D28097-658B-4945-842D-6A647C4672ED}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {75D28097-658B-4945-842D-6A647C4672ED}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {6E907BB7-13F2-48BC-8D01-89544EE7479C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {6E907BB7-13F2-48BC-8D01-89544EE7479C}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {6E907BB7-13F2-48BC-8D01-89544EE7479C}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {6E907BB7-13F2-48BC-8D01-89544EE7479C}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {016E7E78-31A1-45A3-9C09-446A64BB038A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {016E7E78-31A1-45A3-9C09-446A64BB038A}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {016E7E78-31A1-45A3-9C09-446A64BB038A}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {016E7E78-31A1-45A3-9C09-446A64BB038A}.Release|Any CPU.Build.0 = Release|Any CPU 49 | EndGlobalSection 50 | EndGlobal 51 | -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/ApplicationTests/LoanApplicationDecisionServiceTests.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using LoanApplication.TacticalDdd.Application; 3 | using LoanApplication.TacticalDdd.DomainModel; 4 | using LoanApplication.TacticalDdd.DomainModel.DomainEvents; 5 | using LoanApplication.TacticalDdd.Tests.Asserts; 6 | using LoanApplication.TacticalDdd.Tests.Mocks; 7 | using Xunit; 8 | using static LoanApplication.TacticalDdd.Tests.Builders.LoanApplicationBuilder; 9 | using static LoanApplication.TacticalDdd.Tests.Builders.OperatorBuilder; 10 | 11 | namespace LoanApplication.TacticalDdd.Tests.ApplicationTests; 12 | 13 | public class LoanApplicationDecisionServiceTests 14 | { 15 | [Fact] 16 | public void LoanApplicationDecisionService_GreenApplication_CanBeAccepted() 17 | { 18 | var operators = new InMemoryOperatorRepository(new List 19 | { 20 | GivenOperator().WithLogin("admin").Build() 21 | }); 22 | 23 | var existingApplications = new InMemoryLoanApplicationRepository(new [] 24 | { 25 | GivenLoanApplication() 26 | .WithNumber("123") 27 | .WithCustomer(customer => customer.WithAge(25).WithIncome(15_000M)) 28 | .WithLoan(loan => loan.WithAmount(200_000).WithNumberOfYears(25).WithInterestRate(1.1M)) 29 | .WithProperty(prop => prop.WithValue(250_000M)) 30 | .Evaluated() 31 | .Build() 32 | }); 33 | 34 | var eventBus = new InMemoryBus(); 35 | 36 | var decisionService = new LoanApplicationDecisionService 37 | ( 38 | new UnitOfWorkMock(), 39 | existingApplications, 40 | operators, 41 | eventBus 42 | ); 43 | 44 | 45 | decisionService.AcceptApplication("123", OperatorIdentity("admin")); 46 | 47 | existingApplications.WithNumber(new LoanApplicationNumber("123")) 48 | .Should() 49 | .BeAccepted(); 50 | 51 | eventBus.Events 52 | .Should() 53 | .HaveExpectedNumberOfEvents(1) 54 | .And.ContainEvent(e => e.LoanApplicationId==existingApplications.WithNumber(new LoanApplicationNumber("123")).Id.Value); 55 | } 56 | 57 | [Fact] 58 | public void LoanApplicationDecisionService_GreenApplication_CanBeRejected() 59 | { 60 | var operators = new InMemoryOperatorRepository(new List 61 | { 62 | GivenOperator().WithLogin("admin").Build() 63 | }); 64 | 65 | var existingApplications = new InMemoryLoanApplicationRepository(new [] 66 | { 67 | GivenLoanApplication() 68 | .WithNumber("123") 69 | .WithCustomer(customer => customer.WithAge(25).WithIncome(15_000M)) 70 | .WithLoan(loan => loan.WithAmount(200_000).WithNumberOfYears(25).WithInterestRate(1.1M)) 71 | .WithProperty(prop => prop.WithValue(250_000M)) 72 | .Evaluated() 73 | .Build() 74 | }); 75 | 76 | var eventBus = new InMemoryBus(); 77 | 78 | var decisionService = new LoanApplicationDecisionService 79 | ( 80 | new UnitOfWorkMock(), 81 | existingApplications, 82 | operators, 83 | eventBus 84 | ); 85 | 86 | 87 | decisionService.RejectApplication("123", OperatorIdentity("admin"), null); 88 | 89 | existingApplications.WithNumber(new LoanApplicationNumber("123")) 90 | .Should() 91 | .BeRejected(); 92 | 93 | eventBus.Events 94 | .Should() 95 | .HaveExpectedNumberOfEvents(1) 96 | .And.ContainEvent(e => e.LoanApplicationId==existingApplications.WithNumber(new LoanApplicationNumber("123")).Id.Value); 97 | } 98 | 99 | private ClaimsPrincipal OperatorIdentity(string login) 100 | { 101 | return new ClaimsPrincipal(new ClaimsIdentity(new Claim[] 102 | { 103 | new Claim(ClaimTypes.Name, login) 104 | })); 105 | } 106 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/DomainTests/CustomerTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using LoanApplication.TacticalDdd.DomainModel; 3 | using Xunit; 4 | using static LoanApplication.TacticalDdd.Tests.Builders.CustomerBuilder; 5 | 6 | namespace LoanApplication.TacticalDdd.Tests.DomainTests; 7 | 8 | public class CustomerTest 9 | { 10 | [Fact] 11 | public void Customer_Born1974_IsAt2019_45YearsOld() 12 | { 13 | var customer = GivenCustomer() 14 | .BornOn(new DateOnly(1974, 6, 26)) 15 | .Build(); 16 | 17 | var ageAt2019 = customer.AgeInYearsAt(new DateOnly(2019, 1, 1)); 18 | 19 | ageAt2019.Should().Be(45.Years()); 20 | } 21 | 22 | [Fact] 23 | public void Customer_Born1974_IsAt2020_46YearsOld() 24 | { 25 | var customer = GivenCustomer() 26 | .BornOn(new DateOnly(1974, 6, 26)) 27 | .Build(); 28 | 29 | 30 | var ageAt2020 = customer.AgeInYearsAt(new DateOnly(2020, 1, 1)); 31 | 32 | ageAt2020.Should().Be(46.Years()); 33 | } 34 | 35 | [Fact] 36 | public void Customer_Born1974_IsAt2021_47YearsOld() 37 | { 38 | var customer = GivenCustomer() 39 | .BornOn(new DateOnly(1974, 6, 26)) 40 | .Build(); 41 | 42 | 43 | var ageAt2021 = customer.AgeInYearsAt(new DateOnly(2021, 1, 1)); 44 | 45 | ageAt2021.Should().Be(47.Years()); 46 | } 47 | 48 | [Fact] 49 | public void Customer_CannotBeCreatedWithout_Identifier() 50 | { 51 | Action act = () => new Customer 52 | ( 53 | null, 54 | new Name("Jan", "B"), 55 | new DateOnly(1974, 6, 26), 56 | new MonetaryAmount(5_000M), 57 | new Address("Poland", "00-001", "Warsaw", "Zielona 8") 58 | ); 59 | 60 | act 61 | .Should() 62 | .Throw() 63 | .WithMessage("National identifier cannot be null"); 64 | } 65 | 66 | [Fact] 67 | public void Customer_CannotBeCreatedWithout_Name() 68 | { 69 | Action act = () => new Customer 70 | ( 71 | new NationalIdentifier("11111111116"), 72 | null, 73 | new DateOnly(1974, 6, 26), 74 | new MonetaryAmount(5_000M), 75 | new Address("Poland", "00-001", "Warsaw", "Zielona 8") 76 | ); 77 | 78 | act 79 | .Should() 80 | .Throw() 81 | .WithMessage("Name cannot be null"); 82 | } 83 | 84 | [Fact] 85 | public void Customer_CannotBeCreatedWithout_Birthdate() 86 | { 87 | Action act = () => new Customer 88 | ( 89 | new NationalIdentifier("11111111116"), 90 | new Name("Jan","B"), 91 | default, 92 | new MonetaryAmount(5_000M), 93 | new Address("Poland","00-001","Warsaw","Zielona 8") 94 | ); 95 | 96 | act 97 | .Should() 98 | .Throw() 99 | .WithMessage("Birthdate cannot be empty"); 100 | } 101 | 102 | [Fact] 103 | public void Customer_CannotBeCreatedWithout_Income() 104 | { 105 | Action act = () => new Customer 106 | ( 107 | new NationalIdentifier("11111111116"), 108 | new Name("Jan","B"), 109 | new DateOnly(1974,6,26), 110 | null, 111 | new Address("Poland","00-001","Warsaw","Zielona 8") 112 | ); 113 | 114 | act 115 | .Should() 116 | .Throw() 117 | .WithMessage("Monthly income cannot be null"); 118 | } 119 | 120 | [Fact] 121 | public void Customer_CannotBeCreatedWithout_Address() 122 | { 123 | Action act = () => new Customer 124 | ( 125 | new NationalIdentifier("11111111116"), 126 | new Name("Jan","B"), 127 | new DateOnly(1974,6,26), 128 | new MonetaryAmount(5_000M), 129 | null 130 | ); 131 | 132 | act 133 | .Should() 134 | .Throw() 135 | .WithMessage("Address cannot be null"); 136 | } 137 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/ReadModel/LoanApplicationFinder.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Dapper; 3 | using LoanApplication.TacticalDdd.Application.Api; 4 | using Npgsql; 5 | 6 | namespace LoanApplication.TacticalDdd.ReadModel; 7 | 8 | public class LoanApplicationFinder(string connectionString) 9 | { 10 | public IList FindLoadApplication(LoanApplicationSearchCriteriaDto criteria) 11 | { 12 | using var cn = new NpgsqlConnection(connectionString); 13 | 14 | cn.Open(); 15 | return cn 16 | .Query(BuildSearchQuery(criteria), criteria) 17 | .ToList(); 18 | } 19 | 20 | private static string BuildSearchQuery(LoanApplicationSearchCriteriaDto criteria) 21 | { 22 | var query = new StringBuilder(); 23 | query.AppendLine 24 | (""" 25 | SELECT 26 | number AS Number, 27 | status AS Status, 28 | CustomerFirstName || ' ' || CustomerLastName AS CustomerName, 29 | decisionDate AS DecisionDate, 30 | LoanAmount AS LoanAmount, 31 | DecisionBy AS DecisionBy 32 | FROM loan_details_view 33 | WHERE 1=1 34 | """); 35 | 36 | if (!string.IsNullOrWhiteSpace(criteria.ApplicationNumber)) 37 | { 38 | query.AppendLine(" AND number = :ApplicationNumber"); 39 | } 40 | 41 | if (!string.IsNullOrWhiteSpace(criteria.CustomerNationalIdentifier)) 42 | { 43 | query.AppendLine(" AND customerNationalIdentifier = :CustomerNationalIdentifier"); 44 | } 45 | 46 | if (!string.IsNullOrWhiteSpace(criteria.DecisionBy)) 47 | { 48 | query.AppendLine(" AND decisionBy = :DecisionBy"); 49 | } 50 | 51 | if (!string.IsNullOrWhiteSpace(criteria.RegisteredBy)) 52 | { 53 | query.AppendLine(" AND registeredBy = :RegisteredBy"); 54 | } 55 | 56 | return query.ToString(); 57 | } 58 | 59 | public LoanApplicationDto GetLoanApplication(string applicationNumber) 60 | { 61 | using var cn = new NpgsqlConnection(connectionString); 62 | 63 | cn.Open(); 64 | return cn 65 | .Query 66 | ( 67 | BuildSelectDetailsQuery(), 68 | (loan, customerAddress, propertyAddress) => 69 | { 70 | loan = loan with { CustomerAddress = customerAddress }; 71 | loan = loan with { PropertyAddress = propertyAddress }; 72 | return loan; 73 | }, 74 | new { ApplicationNumber = applicationNumber }, 75 | splitOn: "Country,Country") 76 | .FirstOrDefault(); 77 | } 78 | 79 | private static string BuildSelectDetailsQuery() 80 | { 81 | return """ 82 | SELECT 83 | 84 | number AS Number, 85 | status AS Status, 86 | score AS Score, 87 | 88 | customerNationalIdentifier AS CustomerNationalIdentifier, 89 | customerFirstName AS CustomerFirstName, 90 | customerLastName AS CustomerLastName, 91 | customerBirthdate AS CustomerBirthdate, 92 | customerMonthlyIncome AS CustomerMonthlyIncome, 93 | 94 | propertyValue AS PropertyValue, 95 | 96 | loanAmount AS LoanAmount, 97 | loanNumberOfYears AS LoanNumberOfYears, 98 | interestRate AS InterestRate, 99 | 100 | registeredBy AS RegisteredBy, 101 | registrationDate AS RegistrationDate, 102 | 103 | decisionDate AS DecisionDate, 104 | decisionBy AS DecisionBy, 105 | 106 | customerAddress_country AS Country, 107 | customerAddress_zipCode AS ZipCode, 108 | customerAddress_city AS City, 109 | customerAddress_street AS Street, 110 | 111 | propertyAddress_country AS Country, 112 | propertyAddress_zipCode AS ZipCode, 113 | propertyAddress_city AS City, 114 | propertyAddress_street AS Street 115 | 116 | FROM loan_details_view 117 | WHERE number = :ApplicationNumber 118 | """; 119 | } 120 | } -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Better code with DDD building blocks 2 | 3 | [![.NET](https://github.com/asc-lab/better-code-with-ddd/actions/workflows/dotnet.yml/badge.svg)](https://github.com/asc-lab/better-code-with-ddd/actions/workflows/dotnet.yml) 4 | 5 | This repository contains code that accompanies presentation ASC LAB team gave at meetup about “Creating better code with Domain Driven Design”. 6 | 7 | Check out our article on [Altkom Software & Consulting blog](https://www.altkomsoftware.com/blog/better-code-using-domain-driven-design/). 8 | 9 | ## Business case 10 | 11 | Domain Driven Design tactical patterns are presented here using mortgage loan application processing use case. Business wants to increase efficiency of mortgage loan application processing for individual customers by: automating application score calculation, combining information from online available sources, auto rejecting applications with RED score, having ability to relatively easy add new rules for scoring. 12 | 13 | The process: 14 | * operator submits loan application with property information, customer data, loan information and attached documents provided by customer 15 | * system performs basic validation, required fields, correct formats 16 | * operator commands the system to calculates score based on rules 17 | * if score is RED application is rejected and explanation is provided 18 | * if score is GREEN then operator validates attached documents and accepts application or rejects it due to discrepancies between provided data and documents. System validates operator’s competence level 19 | 20 | The rules: 21 | * property value must not exceed requested loan amount 22 | * customer age at the day of last loan installment must not exceed 65 years 23 | * customer must not be registered debtor in national debtors registry system 24 | * loan monthly installment must not exceed 15% of customer monthly income 25 | 26 | 27 | ## Solutions 28 | 29 | Repository contains two solutions. First solution presents typical layered application building approach with anemic model and business logic scattered in services that reside in application layer. This solution also presents usage of generic repository and mixing read and write concerns in the same application service class. First solution is located in the LoanApplication.EnterpriseCake folder. 30 | 31 | Second solution presents usage of DDD tactical patterns like: value objects, entities, repositories, factories, domain services and application services to achieve better readability and expressiveness of the code. Applying DDD patterns together with ubiquitous language closes the gap between language spoken by experts and the team and language used in the code. 32 | Solution with DDD building blocks applied is located in the LoanApplication.TacticalDdd folder. 33 | 34 | 35 | ## Domain model 36 | Domain model is pretty simple. There are two aggregates LoanApplication and Operator. LoanApplication represenst, as you can guess, a loan application submitted for processsing. 37 | LoadApplication is composed of several value objects, which in turn are also composed from other value object. 38 | Operator represents a bank employee responsible for processing loan application. 39 | 40 |
41 | 42 | ![Domain model - aggregates](https://raw.githubusercontent.com/asc-lab/better-code-with-ddd/ef_core/LoanApplication.TacticalDdd/Docs/class_model_aggregates.png) 43 | Domain mode - aggregates 44 | 45 |
46 | 47 | 48 | The core functionality of our service is loan application evaluation. Each rule is implemented as a separate class that implements IScoringRule interface. A domain service is created to encapsulate rules and score calculation. 49 | 50 |
51 | 52 | ![Domain model - rules](https://github.com/asc-lab/better-code-with-ddd/blob/ef_core/LoanApplication.TacticalDdd/Docs/class_model_scoring_rules.png?raw=true) 53 | Domain mode - business rules 54 | 55 |
56 | 57 | Application services are clients of domain model. We have three application services one that is responsible for loan application submission, second that evaluates applications and third one that lets users accept or reject application. 58 | Diagram below presents dependencies between one sample application service and domain model parts. It also presents dependency between infrastructure layer and domain model, where infrastructure layer provides implementations for interfaces defined as part of the domain model (for example for repositories). 59 | 60 | 61 |
62 | 63 | ![Application services interaction with the domain model](https://github.com/asc-lab/better-code-with-ddd/blob/ef_core/LoanApplication.TacticalDdd/Docs/class_model_decision_services.png?raw=true) 64 | Application services interaction with the domain model 65 | 66 |
67 | -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/ApplicationTests/LoanApplicationSubmissionServiceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Security.Claims; 4 | using FluentAssertions; 5 | using LoanApplication.TacticalDdd.Application; 6 | using LoanApplication.TacticalDdd.Application.Api; 7 | using LoanApplication.TacticalDdd.DomainModel; 8 | using LoanApplication.TacticalDdd.Tests.Mocks; 9 | using Xunit; 10 | using static LoanApplication.TacticalDdd.Tests.Builders.OperatorBuilder; 11 | 12 | namespace LoanApplication.TacticalDdd.Tests.ApplicationTests; 13 | 14 | public class LoanApplicationSubmissionServiceTests 15 | { 16 | [Fact] 17 | public void LoanApplicationSubmissionService_ValidApplication_GetsSubmitted() 18 | { 19 | var operators = new InMemoryOperatorRepository(new List 20 | { 21 | GivenOperator().WithLogin("admin").Build() 22 | }); 23 | 24 | var existingApplications = new InMemoryLoanApplicationRepository(new List()); 25 | 26 | var loanApplicationSubmissionService = new LoanApplicationSubmissionService 27 | ( 28 | new UnitOfWorkMock(), 29 | existingApplications, 30 | operators 31 | ); 32 | 33 | var validApplication = new LoanApplicationSubmissionDto 34 | ( 35 | CustomerNationalIdentifier : "11111111119", 36 | CustomerFirstName : "Frank", 37 | CustomerLastName : "Oz", 38 | CustomerBirthdate : SysTime.Now().AddYears(-25), 39 | CustomerMonthlyIncome : 10_000M, 40 | CustomerAddress : new AddressDto 41 | ( 42 | Country : "PL", 43 | City : "Warsaw", 44 | Street : "Chłodna 52", 45 | ZipCode : "00-121" 46 | ), 47 | PropertyValue : 320_000M, 48 | PropertyAddress : new AddressDto 49 | ( 50 | Country : "PL", 51 | City : "Warsaw", 52 | Street : "Wilcza 10", 53 | ZipCode : "00-421" 54 | ), 55 | LoanAmount : 100_000M, 56 | LoanNumberOfYears : 25, 57 | InterestRate : 1.1M 58 | ); 59 | 60 | var newApplicationNumber = loanApplicationSubmissionService 61 | .SubmitLoanApplication(validApplication, OperatorIdentity("admin")); 62 | 63 | newApplicationNumber.Should().NotBeEmpty(); 64 | var createdLoanApplication = existingApplications.WithNumber(new LoanApplicationNumber(newApplicationNumber)); 65 | createdLoanApplication.Should().NotBeNull(); 66 | } 67 | 68 | [Fact] 69 | public void LoanApplicationSubmissionService_InvalidApplication_IsNotSaved() 70 | { 71 | var operators = new InMemoryOperatorRepository(new List 72 | { 73 | GivenOperator().WithLogin("admin").Build() 74 | }); 75 | 76 | var existingApplications = new InMemoryLoanApplicationRepository(new List()); 77 | 78 | var loanApplicationSubmissionService = new LoanApplicationSubmissionService 79 | ( 80 | new UnitOfWorkMock(), 81 | existingApplications, 82 | operators 83 | ); 84 | 85 | var validApplication = new LoanApplicationSubmissionDto 86 | ( 87 | CustomerNationalIdentifier : "11111111119111", 88 | CustomerFirstName : "Frank", 89 | CustomerLastName : "Oz", 90 | CustomerBirthdate : SysTime.Now().AddYears(-25), 91 | CustomerMonthlyIncome : 10_000M, 92 | CustomerAddress : new AddressDto 93 | ( 94 | Country : "PL", 95 | City : "Warsaw", 96 | Street : "Chłodna 52", 97 | ZipCode : "00-121" 98 | ), 99 | PropertyValue : 320_000M, 100 | PropertyAddress : new AddressDto 101 | ( 102 | Country : "PL", 103 | City : "Warsaw", 104 | Street : "Wilcza 10", 105 | ZipCode : "00-421" 106 | ), 107 | LoanAmount : 100_000M, 108 | LoanNumberOfYears : 25, 109 | InterestRate : 1.1M 110 | ); 111 | 112 | Action act = () => loanApplicationSubmissionService 113 | .SubmitLoanApplication(validApplication, OperatorIdentity("admin")); 114 | 115 | act 116 | .Should() 117 | .Throw() 118 | .WithMessage("National Identifier must be 11 chars long"); 119 | } 120 | 121 | private ClaimsPrincipal OperatorIdentity(string login) 122 | { 123 | return new ClaimsPrincipal(new ClaimsIdentity(new Claim[] 124 | { 125 | new Claim(ClaimTypes.Name, login) 126 | })); 127 | } 128 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/DomainModel/LoanApplication.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel.Ddd; 2 | 3 | namespace LoanApplication.TacticalDdd.DomainModel; 4 | 5 | public class LoanApplication : Entity 6 | { 7 | public LoanApplicationNumber Number { get; } 8 | public LoanApplicationStatus Status { get; private set; } 9 | public ScoringResult Score { get; private set; } 10 | public Customer Customer { get; } 11 | public Property Property { get; } 12 | public Loan Loan { get; } 13 | 14 | public Decision Decision { get; private set; } 15 | 16 | public Registration Registration { get; } 17 | 18 | 19 | public LoanApplication(LoanApplicationNumber number, Customer customer, Property property, Loan loan, Operator registeredBy) 20 | : this(number, LoanApplicationStatus.New, customer, property,loan, 21 | null,new Registration(SysTime.Today(), registeredBy), null) 22 | { 23 | } 24 | 25 | // To satisfy EF Core 26 | protected LoanApplication() 27 | { 28 | } 29 | 30 | public void Evaluate(ScoringRules rules) 31 | { 32 | Score = rules.Evaluate(this); 33 | if (Score.IsRed()) 34 | { 35 | Status = LoanApplicationStatus.Rejected; 36 | } 37 | } 38 | 39 | public void Accept(Operator decisionBy) 40 | { 41 | if (Status != LoanApplicationStatus.New) 42 | { 43 | throw new ApplicationException("Cannot accept application that is already accepted or rejected"); 44 | } 45 | 46 | if (Score == null) 47 | { 48 | throw new ApplicationException("Cannot accept application before scoring"); 49 | } 50 | 51 | if (!decisionBy.CanAccept(this.Loan.LoanAmount)) 52 | { 53 | throw new ApplicationException("Operator does not have required competence level to accept application"); 54 | } 55 | 56 | Status = LoanApplicationStatus.Accepted; 57 | Decision = new Decision(SysTime.Today(), decisionBy); 58 | } 59 | 60 | public void Reject(Operator decisionBy) 61 | { 62 | if (Status != LoanApplicationStatus.New) 63 | { 64 | throw new ApplicationException("Cannot reject application that is already accepted or rejected"); 65 | } 66 | 67 | Status = LoanApplicationStatus.Rejected; 68 | Decision = new Decision(SysTime.Today(), decisionBy); 69 | } 70 | 71 | private LoanApplication( 72 | LoanApplicationNumber number, 73 | LoanApplicationStatus status, 74 | Customer customer, 75 | Property property, 76 | Loan loan, 77 | ScoringResult score, 78 | Registration registration, 79 | Decision decision) 80 | { 81 | if (number==null) 82 | throw new ArgumentException("Number cannot be null"); 83 | if (customer==null) 84 | throw new ArgumentException("Customer cannot be null"); 85 | if (property==null) 86 | throw new ArgumentException("Property cannot be null"); 87 | if (loan==null) 88 | throw new ArgumentException("Loan cannot be null"); 89 | if (registration==null) 90 | throw new ArgumentException("Registration cannot be null"); 91 | 92 | Id = new LoanApplicationId(Guid.NewGuid()); 93 | Number = number; 94 | Status = status; 95 | Score = score; 96 | Customer = customer; 97 | Property = property; 98 | Loan = loan; 99 | Registration = registration; 100 | Decision = decision; 101 | } 102 | } 103 | 104 | public class LoanApplicationId : ValueObject 105 | { 106 | public Guid Value { get; } 107 | 108 | public LoanApplicationId(Guid value) 109 | { 110 | Value = value; 111 | } 112 | 113 | protected override IEnumerable GetAttributesToIncludeInEqualityCheck() 114 | { 115 | yield return Value; 116 | } 117 | 118 | protected LoanApplicationId() 119 | { 120 | } 121 | } 122 | 123 | public class LoanApplicationNumber : ValueObject 124 | { 125 | public string Number { get; } 126 | public LoanApplicationNumber(string number) 127 | { 128 | if (string.IsNullOrWhiteSpace(number)) 129 | throw new ArgumentException("Loan application number cannot be null or empty string"); 130 | Number = number; 131 | } 132 | 133 | 134 | public static LoanApplicationNumber NewNumber() => new LoanApplicationNumber(Guid.NewGuid().ToString()); 135 | 136 | public static LoanApplicationNumber Of(string number) => new LoanApplicationNumber(number); 137 | 138 | public static implicit operator string(LoanApplicationNumber number) => number.Number; 139 | 140 | protected override IEnumerable GetAttributesToIncludeInEqualityCheck() 141 | { 142 | yield return Number; 143 | } 144 | 145 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | .vscode 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | [tT]arget/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc 265 | 266 | */.settings/* 267 | */.classpath 268 | */.project -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd/Infrastructure/DataAccess/LoanDbContext.cs: -------------------------------------------------------------------------------- 1 | using LoanApplication.TacticalDdd.DomainModel; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace LoanApplication.TacticalDdd.Infrastructure.DataAccess; 6 | 7 | public class LoanDbContext : DbContext 8 | { 9 | public DbSet LoanApplications => Set(); 10 | 11 | public DbSet Operators => Set(); 12 | 13 | public LoanDbContext(DbContextOptions options) : base(options) 14 | { 15 | } 16 | 17 | protected override void OnModelCreating(ModelBuilder modelBuilder) 18 | { 19 | modelBuilder.ApplyConfiguration(new OperatorMapping()); 20 | modelBuilder.ApplyConfiguration(new LoanApplicationMapping()); 21 | } 22 | } 23 | 24 | class OperatorMapping : IEntityTypeConfiguration 25 | { 26 | public void Configure(EntityTypeBuilder builder) 27 | { 28 | builder.ToTable("Operators"); 29 | 30 | builder.HasKey(x => x.Id); 31 | builder.Property(x => x.Id) 32 | .HasConversion(x => x.Value, x => new OperatorId(x)); 33 | 34 | builder.Property(x => x.Login) 35 | .HasConversion(x => x.Value, x => new Login(x)); 36 | 37 | builder.Property(x => x.Password) 38 | .HasConversion(x => x.Value, x => new Password(x)); 39 | 40 | builder.OwnsOne(x => x.Name, opts => 41 | { 42 | opts.Property(x => x.First).HasColumnName("FirstName"); 43 | opts.Property(x => x.Last).HasColumnName("LastName"); 44 | }).Navigation(x => x.Name).IsRequired(); 45 | 46 | builder.Property(x => x.CompetenceLevel) 47 | .HasConversion(x => x != null ? x.Amount : (decimal?)null, 48 | x => x.HasValue ? new MonetaryAmount(x.Value) : null) 49 | .HasColumnName("CompetenceLevel_Amount"); 50 | } 51 | } 52 | 53 | class LoanApplicationMapping : IEntityTypeConfiguration 54 | { 55 | public void Configure(EntityTypeBuilder builder) 56 | { 57 | builder.ToTable("LoanApplications"); 58 | 59 | builder.HasKey(x => x.Id); 60 | builder 61 | .Property(x => x.Id) 62 | .HasConversion(x => x.Value, x => new LoanApplicationId(x)); 63 | 64 | builder 65 | .Property(x => x.Number) 66 | .HasConversion(x => x.Number, x => new LoanApplicationNumber(x)); 67 | 68 | builder 69 | .Property(x => x.Status) 70 | .HasConversion(); 71 | 72 | builder.OwnsOne(x => x.Score, opts => 73 | { 74 | opts.Property(x => x.Explanation); 75 | opts.Property(x => x.Score).HasConversion(); 76 | }); 77 | 78 | builder.OwnsOne(x => x.Customer, opts => 79 | { 80 | opts 81 | .Property(x => x.NationalIdentifier) 82 | .HasConversion(x => x.Value, x => new NationalIdentifier(x)) 83 | .HasColumnName("Customer_NationalIdentifier_Value") 84 | .IsRequired(); 85 | 86 | opts.OwnsOne(x => x.Name, name => 87 | { 88 | name.Property(x => x.First).IsRequired(); 89 | name.Property(x => x.Last).IsRequired(); 90 | }).Navigation(x=>x.Name).IsRequired(); 91 | 92 | opts.Property(x => x.Birthdate).IsRequired(); 93 | 94 | opts 95 | .Property(x => x.MonthlyIncome) 96 | .HasConversion(x => x.Amount, x => new MonetaryAmount(x)) 97 | .HasColumnName("Customer_MonthlyIncome_Amount"); 98 | 99 | opts.OwnsOne(x => x.Address, addr => 100 | { 101 | addr.Property(x => x.Country).IsRequired(); 102 | addr.Property(x => x.ZipCode).IsRequired(); 103 | addr.Property(x => x.City).IsRequired(); 104 | addr.Property(x => x.Street).IsRequired(); 105 | }).Navigation(x => x.Address).IsRequired(); 106 | 107 | }).Navigation(x=>x.Customer).IsRequired(); 108 | 109 | builder.OwnsOne(x => x.Property, opts => 110 | { 111 | opts.Property(x => x.Value) 112 | .HasConversion(x => x.Amount, x => new MonetaryAmount(x)) 113 | .HasColumnName("Property_Value_Amount"); 114 | 115 | opts.OwnsOne(x => x.Address, addr => 116 | { 117 | addr.Property(x => x.Country).IsRequired(); 118 | addr.Property(x => x.ZipCode).IsRequired(); 119 | addr.Property(x => x.City).IsRequired(); 120 | addr.Property(x => x.Street).IsRequired(); 121 | }).Navigation(x => x.Address).IsRequired(); 122 | }).Navigation(x => x.Property).IsRequired(); 123 | 124 | builder.OwnsOne(x => x.Loan, opts => 125 | { 126 | opts.Property(x => x.InterestRate) 127 | .HasConversion(x => x.Value, x => new Percent(x)) 128 | .HasColumnName("Loan_InterestRate_Value") 129 | .IsRequired(); 130 | 131 | opts.Property(x => x.LoanAmount) 132 | .HasConversion(x => x.Amount, x => new MonetaryAmount(x)) 133 | .HasColumnName("Loan_LoanAmount_Amount") 134 | .IsRequired(); 135 | 136 | opts.Property(x => x.LoanNumberOfYears).IsRequired(); 137 | }).Navigation(x => x.Loan).IsRequired(); 138 | 139 | builder.OwnsOne(x => x.Decision, opts => 140 | { 141 | opts.Property(x => x.DecisionDate); 142 | opts.Property(x => x.DecisionBy) 143 | .HasConversion(x => x != null ? x.Value : (Guid?)null, x => x.HasValue ? new OperatorId(x.Value) : null) 144 | .HasColumnName("Decision_DecisionBy_Value"); 145 | }); 146 | 147 | builder.OwnsOne(x => x.Registration, opts => 148 | { 149 | opts.Property(x => x.RegistrationDate); 150 | opts.Property(x => x.RegisteredBy) 151 | .HasConversion(x => x != null ? x.Value : (Guid?)null, x => x.HasValue ? new OperatorId(x.Value) : null) 152 | .HasColumnName("Registration_RegisteredBy_Value"); 153 | }); 154 | } 155 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/DomainTests/ScoringRulesTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using LoanApplication.TacticalDdd.DomainModel; 3 | using LoanApplication.TacticalDdd.Tests.Mocks; 4 | using Xunit; 5 | using static LoanApplication.TacticalDdd.Tests.Builders.LoanApplicationBuilder; 6 | 7 | namespace LoanApplication.TacticalDdd.Tests.DomainTests; 8 | 9 | public class ScoringRulesTests 10 | { 11 | private readonly ScoringRulesFactory scoringRulesFactory = new ScoringRulesFactory(new DebtorRegistryMock()); 12 | 13 | [Fact] 14 | public void PropertyValueHigherThanLoan_LoanAmountMustBeLowerThanPropertyValue_IsSatisfied() 15 | { 16 | var application = GivenLoanApplication() 17 | .WithProperty(prop => prop.WithValue(750_000M)) 18 | .WithLoan(loan => loan.WithAmount(300_000M)) 19 | .Build(); 20 | 21 | var rule = new LoanAmountMustBeLowerThanPropertyValue(); 22 | var ruleCheckResult = rule.IsSatisfiedBy(application); 23 | 24 | ruleCheckResult.Should().BeTrue(); 25 | } 26 | 27 | [Fact] 28 | public void PropertyValueLowerThanLoan_LoanAmountMustBeLowerThanPropertyValue_IsNotSatisfied() 29 | { 30 | var application = GivenLoanApplication() 31 | .WithProperty(prop => prop.WithValue(750_000M)) 32 | .WithLoan(loan => loan.WithAmount(800_000M)) 33 | .Build(); 34 | 35 | var rule = new LoanAmountMustBeLowerThanPropertyValue(); 36 | var ruleCheckResult = rule.IsSatisfiedBy(application); 37 | 38 | ruleCheckResult.Should().BeFalse(); 39 | } 40 | 41 | [Fact] 42 | public void CustomerNotOlderThan65AtEndOfLoan_CustomerAgeAtTheDateOfLastInstallmentMustBeBelow65_IsSatisfied() 43 | { 44 | var application = GivenLoanApplication() 45 | .WithLoan(loan => loan.WithNumberOfYears(20)) 46 | .WithCustomer(customer => customer.WithAge(26)) 47 | .Build(); 48 | 49 | var rule = new CustomerAgeAtTheDateOfLastInstallmentMustBeBelow65(); 50 | var ruleCheckResult = rule.IsSatisfiedBy(application); 51 | 52 | ruleCheckResult.Should().BeTrue(); 53 | } 54 | 55 | [Fact] 56 | public void CustomerOlderThan65AtEndOfLoan_CustomerAgeAtTheDateOfLastInstallmentMustBeBelow65_IsNotSatisfied() 57 | { 58 | var application = GivenLoanApplication() 59 | .WithLoan(loan => loan.WithNumberOfYears(20)) 60 | .WithCustomer(customer => customer.WithAge(46)) 61 | .Build(); 62 | 63 | var rule = new CustomerAgeAtTheDateOfLastInstallmentMustBeBelow65(); 64 | var ruleCheckResult = rule.IsSatisfiedBy(application); 65 | 66 | ruleCheckResult.Should().BeFalse(); 67 | } 68 | 69 | [Fact] 70 | public void CustomerIncome15PercentHigherThenOfInstallment_InstallmentAmountMustBeLowerThen15PercentOfCustomerIncome_IsSatisfied() 71 | { 72 | var application = GivenLoanApplication() 73 | .WithLoan(loan => loan.WithNumberOfYears(25).WithAmount(400_000M).WithInterestRate(1M)) 74 | .WithCustomer(customer => customer.WithIncome(11_000M)) 75 | .Build(); 76 | 77 | var rule = new InstallmentAmountMustBeLowerThen15PercentOfCustomerIncome(); 78 | var ruleCheckResult = rule.IsSatisfiedBy(application); 79 | 80 | ruleCheckResult.Should().BeTrue(); 81 | } 82 | 83 | [Fact] 84 | public void CustomerIncome15PercentLowerThenOfInstallment_InstallmentAmountMustBeLowerThen15PercentOfCustomerIncome_IsNotSatisfied() 85 | { 86 | var application = GivenLoanApplication() 87 | .WithLoan(loan => loan.WithNumberOfYears(20).WithAmount(400_000M).WithInterestRate(1M)) 88 | .WithCustomer(customer => customer.WithIncome(4_000M)) 89 | .Build(); 90 | 91 | var rule = new InstallmentAmountMustBeLowerThen15PercentOfCustomerIncome(); 92 | var ruleCheckResult = rule.IsSatisfiedBy(application); 93 | 94 | ruleCheckResult.Should().BeFalse(); 95 | } 96 | 97 | [Fact] 98 | public void CustomerIsNotARegisteredDebtor_CustomerNotADebtor_IsSatisfied() 99 | { 100 | var application = GivenLoanApplication() 101 | .WithCustomer(customer => customer.WithIdentifier("71041864667")) 102 | .Build(); 103 | 104 | var rule = new CustomerIsNotARegisteredDebtor(new DebtorRegistryMock()); 105 | var ruleCheckResult = rule.IsSatisfiedBy(application); 106 | 107 | ruleCheckResult.Should().BeTrue(); 108 | } 109 | 110 | [Fact] 111 | public void CustomerIsNotARegisteredDebtor_CustomerNotADebtor_IsNotSatisfied() 112 | { 113 | var application = GivenLoanApplication() 114 | .WithCustomer(customer => customer.WithIdentifier(DebtorRegistryMock.DebtorNationalIdentifier)) 115 | .Build(); 116 | 117 | var rule = new CustomerIsNotARegisteredDebtor(new DebtorRegistryMock()); 118 | var ruleCheckResult = rule.IsSatisfiedBy(application); 119 | 120 | ruleCheckResult.Should().BeFalse(); 121 | } 122 | 123 | [Fact] 124 | public void WhenAnyRuleIsNotSatisfied_ScoringResult_IsRed() 125 | { 126 | var application = GivenLoanApplication() 127 | .WithLoan(loan => loan.WithNumberOfYears(20).WithAmount(400_000M).WithInterestRate(1M)) 128 | .WithCustomer(customer => customer.WithIncome(4_000M)) 129 | .Build(); 130 | 131 | var score = scoringRulesFactory.DefaultSet.Evaluate(application); 132 | 133 | score.Score.Should().Be(ApplicationScore.Red); 134 | } 135 | 136 | [Fact] 137 | public void WhenAllRulesAreSatisfied_ScoringResult_IsGreen() 138 | { 139 | var application = GivenLoanApplication() 140 | .WithCustomer(customer => customer.WithAge(25).WithIncome(15_000M)) 141 | .WithLoan(loan => loan.WithAmount(200_000).WithNumberOfYears(25).WithInterestRate(1.1M)) 142 | .WithProperty(prop => prop.WithValue(250_000M)) 143 | .Build(); 144 | 145 | var score = scoringRulesFactory.DefaultSet.Evaluate(application); 146 | 147 | score.Score.Should().Be(ApplicationScore.Green); 148 | } 149 | } -------------------------------------------------------------------------------- /LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/DomainTests/LoanApplicationTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using LoanApplication.TacticalDdd.DomainModel; 3 | using LoanApplication.TacticalDdd.Tests.Asserts; 4 | using LoanApplication.TacticalDdd.Tests.Mocks; 5 | using Xunit; 6 | using static LoanApplication.TacticalDdd.Tests.Builders.LoanApplicationBuilder; 7 | using static LoanApplication.TacticalDdd.Tests.Builders.OperatorBuilder; 8 | 9 | namespace LoanApplication.TacticalDdd.Tests.DomainTests; 10 | 11 | public class LoanApplicationTest 12 | { 13 | private readonly ScoringRulesFactory scoringRulesFactory = new ScoringRulesFactory(new DebtorRegistryMock()); 14 | 15 | [Fact] 16 | public void NewApplication_IsCreatedIn_NewStatus_AndNullScore() 17 | { 18 | var application = GivenLoanApplication() 19 | .WithCustomer(customer => customer.WithAge(25).WithIncome(15_000M)) 20 | .WithLoan(loan => loan.WithAmount(200_000).WithNumberOfYears(25).WithInterestRate(1.1M)) 21 | .WithProperty(prop => prop.WithValue(250_000M)) 22 | .Build(); 23 | 24 | application 25 | .Should() 26 | .BeNew() 27 | .And 28 | .ScoreIsNull(); 29 | } 30 | 31 | [Fact] 32 | public void ValidApplication_EvaluationScore_IsGreen() 33 | { 34 | var application = GivenLoanApplication() 35 | .WithCustomer(customer => customer.WithAge(25).WithIncome(15_000M)) 36 | .WithLoan(loan => loan.WithAmount(200_000).WithNumberOfYears(25).WithInterestRate(1.1M)) 37 | .WithProperty(prop => prop.WithValue(250_000M)) 38 | .Build(); 39 | 40 | application.Evaluate(scoringRulesFactory.DefaultSet); 41 | 42 | application 43 | .Should() 44 | .BeNew() 45 | .And 46 | .HaveGreenScore(); 47 | } 48 | 49 | [Fact] 50 | public void InvalidApplication_EvaluationScore_IsRed_And_StatusIsRejected() 51 | { 52 | var application = GivenLoanApplication() 53 | .WithCustomer(customer => customer.WithAge(55).WithIncome(15_000M)) 54 | .WithLoan(loan => loan.WithAmount(200_000).WithNumberOfYears(25).WithInterestRate(1.1M)) 55 | .WithProperty(prop => prop.WithValue(250_000M)) 56 | .Build(); 57 | 58 | application.Evaluate(scoringRulesFactory.DefaultSet); 59 | 60 | application 61 | .Should() 62 | .BeRejected() 63 | .And.HaveRedScore(); 64 | } 65 | 66 | [Fact] 67 | public void LoanApplication_InStatusNew_EvaluatedGreen_OperatorHasCompetenceLevel_CanBeAccepted() 68 | { 69 | var application = GivenLoanApplication() 70 | .WithCustomer(customer => customer.WithAge(25).WithIncome(15_000M)) 71 | .WithLoan(loan => loan.WithAmount(200_000).WithNumberOfYears(25).WithInterestRate(1.1M)) 72 | .WithProperty(prop => prop.WithValue(250_000M)) 73 | .Evaluated() 74 | .Build(); 75 | 76 | var user = GivenOperator() 77 | .WithCompetenceLevel(1_000_000M) 78 | .Build(); 79 | 80 | application.Accept(user); 81 | 82 | application 83 | .Should() 84 | .BeAccepted() 85 | .And.HaveGreenScore(); 86 | } 87 | 88 | [Fact] 89 | public void LoanApplication_InStatusNew_EvaluatedGreen_OperatorDoesNotHaveCompetenceLevel_CannotBeAccepted() 90 | { 91 | var application = GivenLoanApplication() 92 | .WithCustomer(customer => customer.WithAge(25).WithIncome(15_000M)) 93 | .WithLoan(loan => loan.WithAmount(200_000).WithNumberOfYears(25).WithInterestRate(1.1M)) 94 | .WithProperty(prop => prop.WithValue(250_000M)) 95 | .Evaluated() 96 | .Build(); 97 | 98 | var user = GivenOperator() 99 | .WithCompetenceLevel(100_000M) 100 | .Build(); 101 | 102 | Action act = () => application.Accept(user); 103 | 104 | act 105 | .Should() 106 | .Throw() 107 | .WithMessage("Operator does not have required competence level to accept application"); 108 | } 109 | 110 | [Fact] 111 | public void LoanApplication_InStatusNew_EvaluatedGreen_CanBeRejected() 112 | { 113 | var application = GivenLoanApplication() 114 | .WithCustomer(customer => customer.WithAge(25).WithIncome(15_000M)) 115 | .WithLoan(loan => loan.WithAmount(200_000).WithNumberOfYears(25).WithInterestRate(1.1M)) 116 | .WithProperty(prop => prop.WithValue(250_000M)) 117 | .Evaluated() 118 | .Build(); 119 | 120 | var user = GivenOperator().Build(); 121 | application.Reject(user); 122 | 123 | application 124 | .Should() 125 | .BeRejected() 126 | .And.HaveGreenScore(); 127 | } 128 | 129 | [Fact] 130 | public void LoanApplication_WithoutScore_CannotBeAccepted() 131 | { 132 | var application = GivenLoanApplication() 133 | .WithCustomer(customer => customer.WithAge(25).WithIncome(15_000M)) 134 | .WithLoan(loan => loan.WithAmount(200_000).WithNumberOfYears(25).WithInterestRate(1.1M)) 135 | .WithProperty(prop => prop.WithValue(250_000M)) 136 | .NotEvaluated() 137 | .Build(); 138 | 139 | var user = GivenOperator().Build(); 140 | 141 | Action act = () => application.Accept(user); 142 | 143 | act 144 | .Should() 145 | .Throw() 146 | .WithMessage("Cannot accept application before scoring"); 147 | } 148 | 149 | [Fact] 150 | public void LoanApplication_WithoutScore_CanBeRejected() 151 | { 152 | var application = GivenLoanApplication() 153 | .WithCustomer(customer => customer.WithAge(25).WithIncome(15_000M)) 154 | .WithLoan(loan => loan.WithAmount(200_000).WithNumberOfYears(25).WithInterestRate(1.1M)) 155 | .WithProperty(prop => prop.WithValue(250_000M)) 156 | .NotEvaluated() 157 | .Build(); 158 | 159 | var user = GivenOperator().Build(); 160 | application.Reject(user); 161 | 162 | application 163 | .Should() 164 | .BeRejected() 165 | .And.ScoreIsNull(); 166 | } 167 | 168 | [Fact] 169 | public void LoanApplication_Accepted_CannotBeRejected() 170 | { 171 | var application = GivenLoanApplication() 172 | .Evaluated() 173 | .Accepted() 174 | .Build(); 175 | 176 | var user = GivenOperator().Build(); 177 | 178 | Action act = () => application.Reject(user); 179 | 180 | act 181 | .Should() 182 | .Throw() 183 | .WithMessage("Cannot reject application that is already accepted or rejected"); 184 | } 185 | 186 | [Fact] 187 | public void LoanApplication_Rejected_CannotBeAccepted() 188 | { 189 | var application = GivenLoanApplication() 190 | .WithCustomer(customer => customer.WithAge(25).WithIncome(15_000M)) 191 | .WithLoan(loan => loan.WithAmount(200_000).WithNumberOfYears(25).WithInterestRate(1.1M)) 192 | .WithProperty(prop => prop.WithValue(250_000M)) 193 | .Evaluated() 194 | .Rejected() 195 | .Build(); 196 | 197 | var user = GivenOperator().Build(); 198 | 199 | Action act = () => application.Accept(user); 200 | 201 | act 202 | .Should() 203 | .Throw() 204 | .WithMessage("Cannot accept application that is already accepted or rejected"); 205 | } 206 | } -------------------------------------------------------------------------------- /LoanApplication.EnterpriseCake/LoanApplication.BusinessLogic/LoanApplicationService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Security.Claims; 5 | using System.Threading.Tasks; 6 | using LoanApplication.Contracts; 7 | using LoanApplication.Infrastructure.DataAccess; 8 | using Microsoft.EntityFrameworkCore; 9 | 10 | namespace LoanApplication.BusinessLogic 11 | { 12 | public class LoanApplicationService 13 | { 14 | private readonly IUnitOfWork unitOfWork; 15 | private readonly IGenericRepository loanApplicationRepository; 16 | private readonly IGenericRepository operatorsRepository; 17 | private readonly ValidationService validationService; 18 | private readonly ScoringService scoringService; 19 | 20 | public LoanApplicationService 21 | ( 22 | IGenericRepository loanApplicationRepository, 23 | ValidationService validationService, 24 | ScoringService scoringService, 25 | IUnitOfWork unitOfWork, IGenericRepository operatorsRepository) 26 | { 27 | this.loanApplicationRepository = loanApplicationRepository; 28 | this.validationService = validationService; 29 | this.scoringService = scoringService; 30 | this.unitOfWork = unitOfWork; 31 | this.operatorsRepository = operatorsRepository; 32 | } 33 | 34 | public async Task CreateLoanApplication(LoanApplicationDto loanApplicationDto, ClaimsPrincipal principal) 35 | { 36 | var user = await GetOperatorByLogin(principal.Identity.Name); 37 | 38 | var application = new LoanApplication 39 | { 40 | Number = GenerateLoanApplicationNumber(), 41 | Status = LoanApplicationStatus.New, 42 | Customer = new Customer 43 | { 44 | NationalIdentifier = loanApplicationDto.CustomerNationalIdentifier, 45 | FirstName = loanApplicationDto.CustomerFirstName, 46 | LastName = loanApplicationDto.CustomerLastName, 47 | Birthdate = loanApplicationDto.CustomerBirthdate, 48 | MonthlyIncome = loanApplicationDto.CustomerMonthlyIncome, 49 | Address = new Address 50 | { 51 | Country = loanApplicationDto.CustomerAddress.Country, 52 | City = loanApplicationDto.CustomerAddress.City, 53 | ZipCode = loanApplicationDto.CustomerAddress.ZipCode, 54 | Street = loanApplicationDto.CustomerAddress.Street 55 | } 56 | }, 57 | Property = new Property 58 | { 59 | Value = loanApplicationDto.PropertyValue, 60 | Address = new Address 61 | { 62 | Country = loanApplicationDto.PropertyAddress.Country, 63 | City = loanApplicationDto.PropertyAddress.City, 64 | ZipCode = loanApplicationDto.PropertyAddress.ZipCode, 65 | Street = loanApplicationDto.PropertyAddress.Street 66 | } 67 | }, 68 | LoanAmount = loanApplicationDto.LoanAmount, 69 | LoanNumberOfYears = loanApplicationDto.LoanNumberOfYears, 70 | InterestRate = loanApplicationDto.InterestRate, 71 | RegisteredBy = user, 72 | RegistrationDate = DateTime.Now 73 | }; 74 | 75 | validationService.ValidateLoanApplication(application); 76 | 77 | await loanApplicationRepository.Create(application); 78 | 79 | await unitOfWork.CommitChanges(); 80 | 81 | return application.Number; 82 | } 83 | 84 | private string GenerateLoanApplicationNumber() 85 | { 86 | return Guid.NewGuid().ToString(); 87 | } 88 | 89 | public async Task EvaluateLoanApplication(string applicationNumber) 90 | { 91 | var application = await GetLoanApplicationByNumber(applicationNumber); 92 | 93 | if (application.Status!=LoanApplicationStatus.New) 94 | throw new ApplicationException("Cannot evaluate application that is already accepted or rejected"); 95 | 96 | await scoringService.EvaluateApplication(application); 97 | 98 | if (application.Score == ApplicationScore.Red) 99 | application.Status = LoanApplicationStatus.Rejected; 100 | 101 | await unitOfWork.CommitChanges(); 102 | } 103 | 104 | public async Task> FindLoanApplication(LoanApplicationSearchCriteriaDto criteria) 105 | { 106 | var queryAll = loanApplicationRepository.Query(); 107 | 108 | if (!string.IsNullOrWhiteSpace(criteria.ApplicationNumber)) 109 | { 110 | queryAll = queryAll.Where(a => a.Number == criteria.ApplicationNumber); 111 | } 112 | 113 | if (!string.IsNullOrWhiteSpace(criteria.CustomerNationalIdentifier)) 114 | { 115 | queryAll = queryAll.Where(a => a.Customer.NationalIdentifier == criteria.CustomerNationalIdentifier); 116 | } 117 | 118 | if (!string.IsNullOrWhiteSpace(criteria.DecisionBy)) 119 | { 120 | queryAll = queryAll.Where(a => a.DecisionBy.Login == criteria.DecisionBy); 121 | } 122 | 123 | if (!string.IsNullOrWhiteSpace(criteria.RegisteredBy)) 124 | { 125 | queryAll = queryAll.Where(a => a.RegisteredBy.Login == criteria.RegisteredBy); 126 | } 127 | 128 | return await queryAll.Select(a => new LoanApplicationInfoDto 129 | { 130 | Number = a.Number, 131 | Status = a.Status.ToString(), 132 | CustomerName = $"{a.Customer.FirstName} {a.Customer.LastName}", 133 | LoanAmount = a.LoanAmount, 134 | DecisionBy = a.DecisionBy!=null ? a.DecisionBy.Login : null, 135 | DecisionDate = a.DecisionDate 136 | }) 137 | .ToListAsync(); 138 | } 139 | 140 | public async Task GetLoanApplication(string applicationNumber) 141 | { 142 | var loanApplication = await GetLoanApplicationByNumber(applicationNumber); 143 | 144 | return new LoanApplicationDto 145 | { 146 | Number = loanApplication.Number, 147 | Status = loanApplication.Status.ToString(), 148 | CustomerAddress = new AddressDto 149 | { 150 | City = loanApplication.Customer.Address.City, 151 | Country = loanApplication.Customer.Address.Country, 152 | Street = loanApplication.Customer.Address.Street, 153 | ZipCode = loanApplication.Customer.Address.ZipCode 154 | }, 155 | CustomerBirthdate = loanApplication.Customer.Birthdate, 156 | CustomerFirstName = loanApplication.Customer.FirstName, 157 | CustomerLastName = loanApplication.Customer.LastName, 158 | CustomerMonthlyIncome = loanApplication.Customer.MonthlyIncome, 159 | CustomerNationalIdentifier = loanApplication.Customer.NationalIdentifier, 160 | DecisionBy = loanApplication.DecisionBy?.Login, 161 | DecisionDate = loanApplication.DecisionDate, 162 | Score = loanApplication.Score?.ToString(), 163 | InterestRate = loanApplication.InterestRate, 164 | LoanAmount = loanApplication.LoanAmount, 165 | PropertyAddress = new AddressDto 166 | { 167 | City = loanApplication.Property.Address.City, 168 | Country = loanApplication.Property.Address.Country, 169 | Street = loanApplication.Property.Address.Street, 170 | ZipCode = loanApplication.Property.Address.ZipCode 171 | }, 172 | PropertyValue = loanApplication.Property.Value, 173 | RegisteredBy = loanApplication.RegisteredBy.Login, 174 | LoanNumberOfYears = loanApplication.LoanNumberOfYears 175 | }; 176 | } 177 | 178 | public async Task RejectApplication(string applicationNumber, ClaimsPrincipal principal, string rejectionReason) 179 | { 180 | var loanApplication = await GetLoanApplicationByNumber(applicationNumber); 181 | var user = await GetOperatorByLogin(principal.Identity.Name); 182 | 183 | if (loanApplication.Status!=LoanApplicationStatus.New) 184 | throw new ApplicationException("Cannot reject application that is already accepted or rejected"); 185 | 186 | loanApplication.Status = LoanApplicationStatus.Rejected; 187 | loanApplication.DecisionBy = user; 188 | loanApplication.DecisionDate = DateTime.Now; 189 | 190 | await unitOfWork.CommitChanges(); 191 | } 192 | 193 | public async Task AcceptApplication(string applicationNumber, ClaimsPrincipal principal) 194 | { 195 | var loanApplication = await GetLoanApplicationByNumber(applicationNumber); 196 | var user = await GetOperatorByLogin(principal.Identity.Name); 197 | 198 | if (loanApplication.Status!=LoanApplicationStatus.New) 199 | throw new ApplicationException("Cannot accept application that is already accepted or rejected"); 200 | 201 | loanApplication.Status = LoanApplicationStatus.Accepted; 202 | loanApplication.DecisionBy = user; 203 | loanApplication.DecisionDate = DateTime.Now; 204 | 205 | await unitOfWork.CommitChanges(); 206 | } 207 | 208 | private async Task GetLoanApplicationByNumber(string applicationNumber) 209 | { 210 | return await loanApplicationRepository.Query() 211 | .FirstOrDefaultAsync(a => a.Number == applicationNumber); 212 | } 213 | 214 | private async Task GetOperatorByLogin(string login) 215 | { 216 | return await operatorsRepository.Query() 217 | .FirstOrDefaultAsync(op => op.Login == login); 218 | 219 | } 220 | } 221 | } --------------------------------------------------------------------------------