├── images ├── me_on_git_project.gif ├── me_and_mark_seemann.gif ├── me_on_result_pattern.gif ├── me_and_mads_torgersen.gif ├── me_and_robert_c_martin.gif ├── SystemBoundriesBasketApi.png ├── me_and_rebecca_wirfs_brock.gif ├── me_on_domain-driven-design.gif └── me_on_functional_programming.gif ├── src ├── VOEConsulting.Flame.BasketContext.Api │ ├── appsettings.Development.json │ ├── VOEConsulting.Flame.BasketContext.Api.http │ ├── appsettings.json │ ├── Program.cs │ ├── VOEConsulting.Flame.BasketContext.Api.csproj │ ├── Properties │ │ └── launchSettings.json │ ├── Dockerfile │ ├── Controllers │ │ ├── BasketController.cs │ │ └── BaseController.cs │ └── Extensions │ │ └── ProblemDetailsExtensions.cs ├── VOEConsulting.Flame.Common.Domain │ ├── Services │ │ └── IDateTimeProvider.cs │ ├── IId.cs │ ├── Events │ │ ├── IDomainEventHandler.cs │ │ ├── Decorators │ │ │ └── AggregateTypeAttribute.cs │ │ ├── IDomainEvent.cs │ │ └── DomainEvent.cs │ ├── IAuditableEntity.cs │ ├── Errors │ │ ├── IDomainError.cs │ │ ├── ErrorType.cs │ │ └── DomainError.cs │ ├── VOEConsulting.Flame.Common.Domain.csproj │ ├── Exceptions │ │ ├── EventualConsistencyException.cs │ │ └── ValidationException.cs │ ├── Entity.cs │ ├── ValueObject.cs │ ├── AggregateRoot.cs │ ├── DateRange.cs │ ├── Id.cs │ └── Extensions │ │ └── ValidationExtensions.cs ├── VOEConsulting.Flame.BasketContext.Application │ ├── Coupons │ │ └── ApplyCouponCommand.cs │ ├── Baskets │ │ ├── Commands │ │ │ ├── CreateBasket │ │ │ │ ├── CreateBasketCommand.cs │ │ │ │ ├── CreateBasketCommandValidator.cs │ │ │ │ └── CreateBasketCommandHandler.cs │ │ │ ├── CalculateTotalAmount │ │ │ │ └── CalculateTotalAmountCommand.cs │ │ │ ├── DeleteBasketItem │ │ │ │ └── DeleteBasketItemCommand.cs │ │ │ ├── UpdateBasketItem │ │ │ │ └── UpdateBasketItemCountCommand.cs │ │ │ ├── ClearBasket │ │ │ │ └── ClearBasketCommand.cs │ │ │ └── AddItemToBasket │ │ │ │ ├── AddItemToBasketCommandValidator.cs │ │ │ │ ├── AddItemToBasketCommand.cs │ │ │ │ └── AddItemToBasketCommandHandler.cs │ │ ├── Dtos │ │ │ ├── CustomerDto.cs │ │ │ ├── BasketItemInfoDto.cs │ │ │ ├── SellerDto.cs │ │ │ ├── BasketItemDto.cs │ │ │ └── BasketDto.cs │ │ └── Queries │ │ │ └── GetBasket │ │ │ ├── GetBasketQuery.cs │ │ │ └── GetBasketQueryHandler.cs │ ├── Repositories │ │ ├── IUnitOfWork.cs │ │ ├── IBasketRepository.cs │ │ └── IRepository.cs │ ├── Abstractions │ │ ├── IQueryHandler.cs │ │ ├── ICommand.cs │ │ ├── IDomainEventDispatcher.cs │ │ ├── IQuery.cs │ │ ├── ICommandHandler.cs │ │ ├── CommandHandlerBase.cs │ │ └── Messaging │ │ │ └── IIntegrationEventPublisher.cs │ ├── GlobalUsings.cs │ ├── Events │ │ ├── Integration │ │ │ └── BasketCreatedIntegrationEvent.cs │ │ ├── Handlers │ │ │ └── BasketCreatedEventHandler.cs │ │ └── Dispatchers │ │ │ └── DomainEventDispatcher.cs │ ├── VOEConsulting.Flame.BasketContext.Application.csproj │ ├── Behaviours │ │ ├── LoggingPipelineBehaviour.cs │ │ ├── ValidationPipelineBehaviour.cs │ │ └── ExceptionHandlingPipelineBehaviour.cs │ ├── ServiceCollectionExtensions.cs │ └── MappingProfiles │ │ └── BasketProfile.cs ├── VOEConsulting.Flame.BasketContext.Domain │ ├── GlobalUsings.cs │ ├── Baskets │ │ ├── Services │ │ │ ├── ISellerLimitService.cs │ │ │ └── ICouponService.cs │ │ ├── Events │ │ │ ├── BasketItemsDeletedEvent.cs │ │ │ ├── BaseBasketDomainEvent.cs │ │ │ ├── BasketCreatedEvent.cs │ │ │ ├── CustomerAssignedEvent.cs │ │ │ ├── BasketItemAddedEvent.cs │ │ │ ├── BasketItemDeletedEvent.cs │ │ │ ├── BasketItemsAmountCalculatedEvent.cs │ │ │ ├── BasketItemActivatedEvent.cs │ │ │ ├── BasketItemDeactivatedEvent.cs │ │ │ ├── TotalAmountCalculatedEvent.cs │ │ │ ├── CouponAppliedEvent.cs │ │ │ ├── CouponRemovedEvent.cs │ │ │ ├── BasketItemCountUpdatedEvent.cs │ │ │ └── ShippingAmountCalculatedEvent.cs │ │ ├── Customer.cs │ │ ├── Seller.cs │ │ ├── BasketItem.cs │ │ ├── Quantity.cs │ │ └── Basket.cs │ ├── Coupons │ │ ├── Events │ │ │ ├── CouponActivatedEvent.cs │ │ │ ├── CouponDeactivatedEvent.cs │ │ │ ├── CouponCreatedEvent.cs │ │ │ └── BaseCouponDomainEvent.cs │ │ ├── Amount.cs │ │ └── Coupon.cs │ ├── Common │ │ └── BasketEventConstants.cs │ └── VOEConsulting.Flame.BasketContext.Domain.csproj ├── VOEConsulting.Flame.Common.Core │ ├── VOEConsulting.Flame.Common.Core.csproj │ ├── Events │ │ ├── IIntegrationEvent.cs │ │ └── IntegrationEvent.cs │ └── Exceptions │ │ └── FlameApplicationException.cs └── VOEConsulting.Flame.BasketContext.Infrastructure │ ├── Persistence │ ├── Entities │ │ ├── CustomerEntity.cs │ │ ├── SellerEntity.cs │ │ ├── CouponEntity.cs │ │ ├── BasketEntity.cs │ │ └── BasketItemEntity.cs │ ├── Configurations │ │ ├── CustomerConfiguration.cs │ │ ├── BasketItemConfiguration.cs │ │ ├── SellerEntityConfiguration.cs │ │ ├── CouponConfiguration.cs │ │ └── BasketConfiguration.cs │ ├── UnitOfWork.cs │ ├── BasketAppDbContext.cs │ ├── Profiles │ │ └── BasketProfile.cs │ └── Repositories │ │ └── BasketRepository.cs │ ├── VOEConsulting.Flame.BasketContext.Infrastructure.csproj │ ├── Messaging │ └── KafkaIntegrationEventPublisher.cs │ ├── InfrastructureExtensions.cs │ └── Migrations │ ├── 20241223104415_Initial.cs │ ├── BasketAppDbContextModelSnapshot.cs │ └── 20241223104415_Initial.Designer.cs ├── launchSettings.json ├── tests ├── VOEConsulting.Flame.BasketContext.Tests.Unit │ ├── GlobalUsings.cs │ ├── Factories │ │ ├── CustomerFactory.cs │ │ ├── BasketFactory.cs │ │ ├── QuantityFactory.cs │ │ ├── CouponFactory.cs │ │ ├── BasketItemFactory.cs │ │ └── SellerFactory.cs │ ├── Extensions │ │ └── ObjectAssertionsExtensions.cs │ ├── VOEConsulting.Flame.BasketContext.Tests.Unit.csproj │ ├── BasketItemTests.cs │ └── CouponTests.cs └── VOEConsulting.Flame.BasketContext.Tests.Data │ ├── BasketData.cs │ ├── CustomerData.cs │ ├── VOEConsulting.Flame.BasketContext.Tests.Data.csproj │ ├── CouponData.cs │ └── BasketItemData.cs ├── .dockerignore ├── docker-compose.dcproj ├── docker-compose.override.yml ├── LICENSE ├── docker-compose.yml ├── FlameBasketApp.sln └── .gitignore /images/me_on_git_project.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuralSuleymani/the-real-DDD-CQRS-CleanArchitecture/HEAD/images/me_on_git_project.gif -------------------------------------------------------------------------------- /images/me_and_mark_seemann.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuralSuleymani/the-real-DDD-CQRS-CleanArchitecture/HEAD/images/me_and_mark_seemann.gif -------------------------------------------------------------------------------- /images/me_on_result_pattern.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuralSuleymani/the-real-DDD-CQRS-CleanArchitecture/HEAD/images/me_on_result_pattern.gif -------------------------------------------------------------------------------- /images/me_and_mads_torgersen.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuralSuleymani/the-real-DDD-CQRS-CleanArchitecture/HEAD/images/me_and_mads_torgersen.gif -------------------------------------------------------------------------------- /images/me_and_robert_c_martin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuralSuleymani/the-real-DDD-CQRS-CleanArchitecture/HEAD/images/me_and_robert_c_martin.gif -------------------------------------------------------------------------------- /images/SystemBoundriesBasketApi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuralSuleymani/the-real-DDD-CQRS-CleanArchitecture/HEAD/images/SystemBoundriesBasketApi.png -------------------------------------------------------------------------------- /images/me_and_rebecca_wirfs_brock.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuralSuleymani/the-real-DDD-CQRS-CleanArchitecture/HEAD/images/me_and_rebecca_wirfs_brock.gif -------------------------------------------------------------------------------- /images/me_on_domain-driven-design.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuralSuleymani/the-real-DDD-CQRS-CleanArchitecture/HEAD/images/me_on_domain-driven-design.gif -------------------------------------------------------------------------------- /images/me_on_functional_programming.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuralSuleymani/the-real-DDD-CQRS-CleanArchitecture/HEAD/images/me_on_functional_programming.gif -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/Services/IDateTimeProvider.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.Common.Domain.Services 2 | { 3 | public interface IDateTimeProvider 4 | { 5 | DateTimeOffset UtcNow(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Coupons/ApplyCouponCommand.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Application.Coupons 2 | { 3 | public record ApplyCouponCommand(Guid BasketId, Guid CouponId) : IQuery; 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/IId.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.Common.Domain 2 | { 3 | public interface IId : IComparable, IComparable, IComparable, IEquatable 4 | { 5 | Guid Value { get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Docker Compose": { 4 | "commandName": "DockerCompose", 5 | "commandVersion": "1.0", 6 | "serviceActions": { 7 | "voeconsulting.flame.basketcontext.api": "StartDebugging" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Api/VOEConsulting.Flame.BasketContext.Api.http: -------------------------------------------------------------------------------- 1 | @VOEConsulting.Flame.BasketContext.Api_HostAddress = http://localhost:5016 2 | 3 | GET {{VOEConsulting.Flame.BasketContext.Api_HostAddress}}/weatherforecast/ 4 | Accept: application/json 5 | 6 | ### 7 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using VOEConsulting.Flame.Common.Domain; 2 | global using VOEConsulting.Flame.Common.Domain.Events; 3 | global using VOEConsulting.Flame.Common.Domain.Events.Decorators; 4 | global using VOEConsulting.Flame.Common.Domain.Extensions; 5 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Services/ISellerLimitService.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets.Services 2 | { 3 | public interface ISellerLimitService 4 | { 5 | int GetLimitForProduct(Guid sellerId, string productName); 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/Events/IDomainEventHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace VOEConsulting.Flame.Common.Domain.Events 4 | { 5 | public interface IDomainEventHandler : INotificationHandler 6 | where TDomainEvent : IDomainEvent 7 | { } 8 | } 9 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/IAuditableEntity.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.Common.Domain 2 | { 3 | public interface IAuditableEntity 4 | { 5 | public DateTimeOffset CreatedAtUtc { get; } 6 | public DateTimeOffset LastModifiedAtUtc { get; } 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Baskets/Commands/CreateBasket/CreateBasketCommand.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Application.Baskets.Commands.CreateBasket 2 | { 3 | public record CreateBasketCommand(decimal TaxPercentage, CustomerDto Customer) : ICommand; 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Baskets/Dtos/CustomerDto.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Application.Baskets.Dtos 2 | { 3 | public class CustomerDto 4 | { 5 | public Guid Id { get; set; } 6 | public bool IsEliteMember { get; set; } 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Coupons/Events/CouponActivatedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Coupons.Events 2 | { 3 | public sealed class CouponActivatedEvent : BaseCouponDomainEvent 4 | { 5 | public CouponActivatedEvent(Id id):base(id) { } 6 | } 7 | } -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Repositories/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Application.Repositories 2 | { 3 | public interface IUnitOfWork : IDisposable 4 | { 5 | Task SaveChangesAsync(CancellationToken cancellationToken = default); 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Coupons/Events/CouponDeactivatedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Coupons.Events 2 | { 3 | public sealed class CouponDeactivatedEvent :BaseCouponDomainEvent 4 | { 5 | public CouponDeactivatedEvent(Id id) : base(id) { } 6 | } 7 | } -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Core/VOEConsulting.Flame.Common.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/VOEConsulting.Flame.BasketContext.Tests.Unit/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using FluentAssertions; 2 | global using VOEConsulting.Flame.BasketContext.Domain.Baskets; 3 | global using VOEConsulting.Flame.BasketContext.Domain.Baskets.Events; 4 | global using TestFactories = VOEConsulting.Flame.BasketContext.Tests.Unit.Factories; -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Coupons/Events/CouponCreatedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Coupons.Events 2 | { 3 | public sealed class CouponCreatedEvent : BaseCouponDomainEvent 4 | { 5 | public CouponCreatedEvent(Id couponId) : base(couponId) { } 6 | } 7 | } -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Baskets/Commands/CalculateTotalAmount/CalculateTotalAmountCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Application.Baskets.Commands.CalculateTotalAmount 4 | { 5 | public record CalculateTotalAmountCommand(Guid BasketId) : IRequest; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/Errors/IDomainError.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.Common.Domain.Errors 2 | { 3 | public interface IDomainError 4 | { 5 | string? ErrorMessage { get; init; } 6 | ErrorType ErrorType { get; init; } 7 | public List? Errors { get; init; } 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Baskets/Commands/DeleteBasketItem/DeleteBasketItemCommand.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Application.Abstractions; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Application.Baskets.Commands.DeleteBasketItem 4 | { 5 | public record DeleteBasketItemCommand(Guid BasketId, Guid ItemId) : ICommand; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Events/BasketItemsDeletedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets.Events 2 | { 3 | public sealed class BasketItemsDeletedEvent : BaseBasketDomainEvent 4 | { 5 | public BasketItemsDeletedEvent(Id basketId) 6 | : base(basketId) { } 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Common/BasketEventConstants.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Common 2 | { 3 | public static class BasketEventConstants 4 | { 5 | public const string BasketsAggregateTypeName = "basket-service.baskets"; 6 | public const string CouponsAggregateTypeName = "basket-service.coupons"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/Events/Decorators/AggregateTypeAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.Common.Domain.Events.Decorators 2 | { 3 | [AttributeUsage(AttributeTargets.Class)] 4 | public class AggregateTypeAttribute(string aggregateType) : Attribute 5 | { 6 | public string AggregateType { get; } = aggregateType; 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Core/Events/IIntegrationEvent.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.Common.Core.Events 2 | { 3 | public interface IIntegrationEvent 4 | { 5 | int Version { get; } 6 | string EventType { get; } 7 | Guid Id { get; } 8 | DateTimeOffset OccurredOnUtc { get; } 9 | Guid AggregateId { get; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/VOEConsulting.Flame.BasketContext.Tests.Data/BasketData.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Domain.Baskets; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Tests.Data 4 | { 5 | public static class BasketData 6 | { 7 | public const decimal TaxAmount = 18; 8 | public static Customer Customer = Customer.Create(false, null); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Baskets/Dtos/BasketItemInfoDto.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Application.Baskets.Dtos 2 | { 3 | public class BasketItemInfoDto 4 | { 5 | public IList Items { get; set; } = new List(); 6 | public decimal ShippingAmountLeft { get; set; } 7 | } 8 | 9 | 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/Persistence/Entities/CustomerEntity.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Infrastructure.Entities 2 | { 3 | public class CustomerEntity 4 | { 5 | public Guid Id { get; set; } 6 | public bool IsEliteMember { get; set; } 7 | public ICollection Baskets { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Baskets/Commands/UpdateBasketItem/UpdateBasketItemCountCommand.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Application.Abstractions; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Application.Baskets.Commands.UpdateBasketItem 4 | { 5 | public record UpdateBasketItemCountCommand(Guid BasketId, Guid ItemId, int Quantity) : ICommand; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Abstractions/IQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Application.Abstractions 4 | { 5 | public interface IQueryHandler : IRequestHandler> 6 | where TRequest : IQuery 7 | where TResponse : notnull 8 | { } 9 | } 10 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Baskets/Queries/GetBasket/GetBasketQuery.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Application.Abstractions; 2 | using VOEConsulting.Flame.BasketContext.Application.Baskets.Dtos; 3 | 4 | namespace VOEConsulting.Flame.BasketContext.Application.Baskets.Queries.GetBasket 5 | { 6 | public record GetBasketQuery(Guid BasketId) : IQuery; 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Abstractions/ICommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Application.Abstractions 4 | { 5 | public interface ICommand : IRequestBase, IRequest> 6 | where TResponse : notnull 7 | { } 8 | 9 | public interface ICommand : IRequestBase, IRequest> { } 10 | } 11 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Abstractions/IDomainEventDispatcher.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.Common.Domain.Events; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Application.Abstractions 4 | { 5 | public interface IDomainEventDispatcher 6 | { 7 | Task DispatchAsync(IEnumerable domainEvents, CancellationToken cancellationToken = default); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using AutoMapper; 2 | global using CSharpFunctionalExtensions; 3 | global using VOEConsulting.Flame.BasketContext.Application.Abstractions; 4 | global using VOEConsulting.Flame.BasketContext.Application.Baskets.Dtos; 5 | global using VOEConsulting.Flame.Common.Domain.Errors; 6 | global using VOEConsulting.Flame.BasketContext.Application.Repositories; -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Services/ICouponService.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Domain.Coupons; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets.Services 4 | { 5 | public interface ICouponService 6 | { 7 | Task ApplyDiscountAsync(Id couponId, decimal totalAmount); 8 | Task IsActive(Id couponId); 9 | } 10 | } -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Baskets/Commands/ClearBasket/ClearBasketCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace VOEConsulting.Flame.BasketContext.Application.Baskets.Commands.ClearBasket 9 | { 10 | public record ClearBasketCommand(Guid BasketId) : IRequest; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Abstractions/IQuery.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Application.Abstractions 4 | { 5 | public interface IRequestBase { } 6 | public interface IQuery : IRequestBase, IRequest> 7 | where TResponse : notnull 8 | { } 9 | 10 | public interface IQuery : IRequestBase, IRequest 11 | { } 12 | } 13 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Repositories/IBasketRepository.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Domain.Baskets; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Application.Repositories 4 | { 5 | public interface IBasketRepository : IRepository { 6 | 7 | Task AddBasketItemAsync(Guid basketId, BasketItem basketItem); 8 | Task IsExistByCustomerIdAsync(Guid id); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Baskets/Dtos/SellerDto.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Application.Baskets.Dtos 2 | { 3 | public class SellerDto 4 | { 5 | public Guid Id { get; set; } 6 | public string Name { get; set; } 7 | public float Rating { get; set; } 8 | public decimal ShippingLimit { get; set; } 9 | public decimal ShippingCost { get; set; } 10 | } 11 | 12 | 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Events/BaseBasketDomainEvent.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Domain.Common; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets.Events 4 | { 5 | [AggregateType(BasketEventConstants.BasketsAggregateTypeName)] 6 | public abstract class BaseBasketDomainEvent(Guid aggregateId, DateTimeOffset? occurredOnUtc = null) 7 | : DomainEvent(aggregateId, occurredOnUtc ?? DateTimeOffset.UtcNow) { } 8 | } 9 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Events/BasketCreatedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets.Events 2 | { 3 | public sealed class BasketCreatedEvent : BaseBasketDomainEvent 4 | { 5 | public BasketCreatedEvent(Id basketId, Guid customerId) 6 | : base(basketId) 7 | { 8 | CustomerId = customerId; 9 | } 10 | 11 | public Guid CustomerId { get; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Events/CustomerAssignedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets.Events 2 | { 3 | public sealed class CustomerAssignedEvent : BaseBasketDomainEvent 4 | { 5 | public CustomerAssignedEvent(Id basketId, Customer customer) 6 | : base(basketId) 7 | { 8 | Customer = customer; 9 | } 10 | public Customer Customer { get; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/Persistence/Entities/SellerEntity.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Infrastructure.Entities 2 | { 3 | public class SellerEntity 4 | { 5 | public Guid Id { get; set; } 6 | public string Name { get; set; } = null!; 7 | public float Rating { get; set; } 8 | public decimal ShippingLimit { get; set; } 9 | public decimal ShippingCost { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Coupons/Events/BaseCouponDomainEvent.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Domain.Common; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Domain.Coupons.Events 4 | { 5 | [AggregateType(BasketEventConstants.CouponsAggregateTypeName)] 6 | public abstract class BaseCouponDomainEvent(Id aggregateId, DateTimeOffset? occurredOnUtc = null) 7 | : DomainEvent(aggregateId, occurredOnUtc ?? DateTimeOffset.UtcNow) { } 8 | } 9 | -------------------------------------------------------------------------------- /tests/VOEConsulting.Flame.BasketContext.Tests.Data/CustomerData.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Domain.Baskets; 2 | using VOEConsulting.Flame.Common.Domain; 3 | 4 | namespace VOEConsulting.Flame.BasketContext.Tests.Data 5 | { 6 | public static class CustomerData 7 | { 8 | public static Customer EliteCustomer = Customer.Create(true, Id.New()); 9 | public static Customer NonEliteCustomer = Customer.Create(false, null); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/VOEConsulting.Flame.BasketContext.Tests.Unit/Factories/CustomerFactory.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.Common.Domain; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Tests.Unit.Factories 4 | { 5 | public static class CustomerFactory 6 | { 7 | public static Customer Create(bool? isEliteMember = null, Id? id = null) 8 | { 9 | return Customer.Create(isEliteMember ?? false, id ?? Id.New()); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Events/BasketItemAddedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets.Events 2 | { 3 | public sealed class BasketItemAddedEvent : BaseBasketDomainEvent 4 | { 5 | public BasketItemAddedEvent(Id basketId, BasketItem basketItem) 6 | : base(basketId) 7 | { 8 | BasketItem = basketItem; 9 | } 10 | public BasketItem BasketItem { get; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Events/BasketItemDeletedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets.Events 2 | { 3 | public sealed class BasketItemDeletedEvent : BaseBasketDomainEvent 4 | { 5 | public BasketItemDeletedEvent(Id basketId, BasketItem basketItem) 6 | : base(basketId) 7 | { 8 | BasketItem = basketItem; 9 | } 10 | public BasketItem BasketItem { get; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Events/BasketItemsAmountCalculatedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets.Events 2 | { 3 | public sealed class BasketItemsAmountCalculatedEvent : BaseBasketDomainEvent 4 | { 5 | public BasketItemsAmountCalculatedEvent(Id basketId, decimal amount) 6 | : base(basketId) 7 | { 8 | Amount = amount; 9 | } 10 | public decimal Amount { get; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/VOEConsulting.Flame.Common.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Events/BasketItemActivatedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets.Events 2 | { 3 | public sealed class BasketItemActivatedEvent : BaseBasketDomainEvent 4 | { 5 | public BasketItemActivatedEvent(Id basketId, BasketItem basketItem) 6 | : base(basketId) 7 | { 8 | BasketItem = basketItem; 9 | } 10 | 11 | public BasketItem BasketItem { get; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Events/BasketItemDeactivatedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets.Events 2 | { 3 | public sealed class BasketItemDeactivatedEvent : BaseBasketDomainEvent 4 | { 5 | public BasketItemDeactivatedEvent(Id basketId, BasketItem basketItem) 6 | : base(basketId) 7 | { 8 | BasketItem = basketItem; 9 | } 10 | public BasketItem BasketItem { get; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Events/TotalAmountCalculatedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets.Events 2 | { 3 | public sealed class TotalAmountCalculatedEvent : BaseBasketDomainEvent 4 | { 5 | public TotalAmountCalculatedEvent(Id basketId, decimal totalAmount) 6 | : base(basketId) 7 | { 8 | TotalAmount = totalAmount; 9 | } 10 | 11 | public decimal TotalAmount { get; } 12 | } 13 | } -------------------------------------------------------------------------------- /tests/VOEConsulting.Flame.BasketContext.Tests.Data/VOEConsulting.Flame.BasketContext.Tests.Data.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md 26 | !**/.gitignore 27 | !.git/HEAD 28 | !.git/config 29 | !.git/packed-refs 30 | !.git/refs/heads/** -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/Events/IDomainEvent.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace VOEConsulting.Flame.Common.Domain.Events 4 | { 5 | public interface IDomainEvent : INotification 6 | { 7 | int Version { get; } 8 | 9 | string AggregateType { get; } 10 | 11 | string EventType { get; } 12 | 13 | Guid Id { get; } 14 | 15 | DateTimeOffset OccurredOnUtc { get; } 16 | 17 | Guid AggregateId { get; } 18 | 19 | string? TraceInfo { get; } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Events/CouponAppliedEvent.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Domain.Coupons; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets.Events 4 | { 5 | public sealed class CouponAppliedEvent : BaseBasketDomainEvent 6 | { 7 | public CouponAppliedEvent(Id basketId, Id couponId) 8 | : base(basketId) 9 | { 10 | CouponId = couponId; 11 | } 12 | 13 | public Id CouponId { get; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Events/CouponRemovedEvent.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Domain.Coupons; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets.Events 4 | { 5 | public sealed class CouponRemovedEvent : BaseBasketDomainEvent 6 | { 7 | public CouponRemovedEvent(Id basketId, Id couponId) 8 | : base(basketId) 9 | { 10 | CouponId = couponId; 11 | } 12 | 13 | public Id CouponId { get; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "Kafka": { 9 | "BootstrapServers": "localhost:29092", 10 | "DefaultTopic": "integration-events" 11 | }, 12 | "ConnectionStrings": { 13 | "DefaultConnection": "Server=localhost;Database=BasketServiceApp;Trusted_Connection=True;TrustServerCertificate=true;MultipleActiveResultSets=true" 14 | }, 15 | "AllowedHosts": "*" 16 | } 17 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Abstractions/ICommandHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Application.Abstractions 4 | { 5 | public interface ICommandHandler : IRequestHandler> 6 | where TRequest : ICommand 7 | where TResponse : notnull 8 | { } 9 | 10 | public interface ICommandHandler : IRequestHandler> 11 | where TRequest : ICommand 12 | { } 13 | } 14 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Baskets/Commands/CreateBasket/CreateBasketCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Application.Baskets.Commands.CreateBasket 4 | { 5 | public class CreateBasketCommandValidator : AbstractValidator 6 | { 7 | public CreateBasketCommandValidator() 8 | { 9 | RuleFor(x => x.Customer).NotNull(); 10 | RuleFor(x => x.TaxPercentage).GreaterThanOrEqualTo(0); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Baskets/Dtos/BasketItemDto.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Application.Baskets.Dtos 2 | { 3 | public class BasketItemDto 4 | { 5 | public Guid Id { get; set; } 6 | public string Name { get; set; } 7 | public string ImageUrl { get; set; } 8 | public decimal Price { get; set; } 9 | public int Quantity { get; set; } 10 | public decimal TotalPrice { get; set; } 11 | public bool IsActive { get; set; } 12 | } 13 | 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Repositories/IRepository.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Application.Repositories 2 | { 3 | public interface IRepository where TEntity : class 4 | { 5 | Task GetByIdAsync(Guid id); 6 | Task IsExistsAsync(Guid id); 7 | Task> GetAllAsync(); 8 | Task AddAsync(TEntity entity,CancellationToken cancellationToken); 9 | Task UpdateAsync(TEntity entity); 10 | Task DeleteAsync(Guid id); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /tests/VOEConsulting.Flame.BasketContext.Tests.Unit/Factories/BasketFactory.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Tests.Data; 2 | using static VOEConsulting.Flame.BasketContext.Tests.Data.BasketData; 3 | namespace VOEConsulting.Flame.BasketContext.Tests.Unit.Factories 4 | { 5 | public static class BasketFactory 6 | { 7 | public static Basket Create(decimal? taxPercentage = null, Customer? customer = null) 8 | { 9 | return Basket.Create(taxPercentage ?? TaxAmount, customer?? BasketData.Customer); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/VOEConsulting.Flame.BasketContext.Tests.Data/CouponData.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Domain.Coupons; 2 | using VOEConsulting.Flame.Common.Domain; 3 | 4 | namespace VOEConsulting.Flame.BasketContext.Tests.Data 5 | { 6 | public static class CouponData 7 | { 8 | public static string Code = "TL343443"; 9 | public static Amount PercentageAmount = Amount.Percentage(34); 10 | public static Amount FixAmount = Amount.Fix(12); 11 | public static DateRange Range = DateRange.FromString("2024-01-01", "2024-08-31"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/Persistence/Configurations/CustomerConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using VOEConsulting.Flame.BasketContext.Infrastructure.Entities; 4 | 5 | public class CustomerConfiguration : IEntityTypeConfiguration 6 | { 7 | public void Configure(EntityTypeBuilder builder) 8 | { 9 | builder.HasKey(c => c.Id); 10 | 11 | builder.Property(c => c.IsEliteMember) 12 | .IsRequired(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Baskets/Dtos/BasketDto.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Application.Baskets.Dtos 2 | { 3 | public class BasketDto 4 | { 5 | public Guid Id { get; set; } 6 | public IDictionary BasketItems { get; set; } = new Dictionary(); 7 | public decimal TaxPercentage { get; set; } 8 | public decimal TotalAmount { get; set; } 9 | public CustomerDto Customer { get; set; } 10 | public Guid? CouponId { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Events/BasketItemCountUpdatedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets.Events 2 | { 3 | public sealed class BasketItemCountUpdatedEvent : BaseBasketDomainEvent 4 | { 5 | public BasketItemCountUpdatedEvent(Id basketId, BasketItem basketItem, int count) 6 | : base(basketId) 7 | { 8 | BasketItem = basketItem; 9 | Count = count; 10 | } 11 | 12 | public BasketItem BasketItem { get; } 13 | public int Count { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/VOEConsulting.Flame.BasketContext.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Events/ShippingAmountCalculatedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets.Events 2 | { 3 | public sealed class ShippingAmountCalculatedEvent : BaseBasketDomainEvent 4 | { 5 | public ShippingAmountCalculatedEvent(Id basketId, Seller seller, decimal shippingAmountLeft) 6 | : base(basketId) 7 | { 8 | Seller = seller; 9 | ShippingAmountLeft = shippingAmountLeft; 10 | } 11 | 12 | public Seller Seller { get; } 13 | public decimal ShippingAmountLeft { get; } 14 | } 15 | } -------------------------------------------------------------------------------- /tests/VOEConsulting.Flame.BasketContext.Tests.Unit/Factories/QuantityFactory.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Tests.Data; 2 | namespace VOEConsulting.Flame.BasketContext.Tests.Unit.Factories 3 | { 4 | public static class QuantityFactory 5 | { 6 | public static Quantity Create(int? value = null, int? limit = null, decimal? pricePerUnit = null) 7 | { 8 | return Quantity.Create(value ?? BasketItemData.BasketItemQuantity.Value, limit ?? BasketItemData.BasketItemQuantity.Limit 9 | , pricePerUnit ?? BasketItemData.BasketItemQuantity.PricePerUnit); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/Persistence/Entities/CouponEntity.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Infrastructure.Entities 2 | { 3 | public class CouponEntity 4 | { 5 | public Guid Id { get; set; } 6 | public string Code { get; set; } 7 | public bool IsActive { get; set; } 8 | public decimal Value { get; set; } 9 | public string CouponType { get; set; } // "Fix" or "Percentage" 10 | public DateTime StartDate { get; set; } 11 | public DateTime EndDate { get; set; } 12 | public ICollection Baskets { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Core/Exceptions/FlameApplicationException.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.Common.Core.Exceptions 2 | { 3 | 4 | [Serializable] 5 | public class FlameApplicationException : Exception 6 | { 7 | public FlameApplicationException() { } 8 | public FlameApplicationException(string message) : base(message) { } 9 | public FlameApplicationException(string message, Exception inner) : base(message, inner) { } 10 | protected FlameApplicationException( 11 | System.Runtime.Serialization.SerializationInfo info, 12 | System.Runtime.Serialization.StreamingContext context) : base(info, context) { } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Baskets/Commands/AddItemToBasket/AddItemToBasketCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Application.Baskets.Commands.AddItemToBasket 4 | { 5 | public class AddItemToBasketCommandValidator : AbstractValidator 6 | { 7 | public AddItemToBasketCommandValidator() 8 | { 9 | RuleFor(x => x.BasketId).NotEmpty(); 10 | RuleFor(x => x.BasketItem).NotEmpty(); 11 | RuleFor(x => x.Quantity).NotNull(); 12 | RuleFor(x => x.Seller).NotNull(); 13 | } 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/Exceptions/EventualConsistencyException.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.Common.Domain.Exceptions 2 | { 3 | public class EventualConsistencyException : Exception 4 | { 5 | public string ErrorCode { get; } 6 | public string ErrorMessage { get; } 7 | public List Details { get; } 8 | 9 | public EventualConsistencyException(string errorCode, string errorMessage, List? details = null) 10 | : base(message: errorMessage) 11 | { 12 | ErrorCode = errorCode; 13 | ErrorMessage = errorMessage; 14 | Details = details ?? new(); 15 | } 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /tests/VOEConsulting.Flame.BasketContext.Tests.Unit/Factories/CouponFactory.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Domain.Coupons; 2 | using VOEConsulting.Flame.BasketContext.Tests.Data; 3 | using VOEConsulting.Flame.Common.Domain; 4 | using static VOEConsulting.Flame.BasketContext.Tests.Data.CouponData; 5 | namespace VOEConsulting.Flame.BasketContext.Tests.Unit.Factories 6 | { 7 | public static class CouponFactory 8 | { 9 | public static Coupon Create(string? code = null, Amount? amount = null, DateRange? dateRange = null) 10 | { 11 | return Coupon.Create(code ?? Code, amount ?? PercentageAmount, dateRange ?? CouponData.Range); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/Persistence/Entities/BasketEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Infrastructure.Entities 4 | { 5 | public class BasketEntity 6 | { 7 | public Guid Id { get; set; } 8 | public decimal TaxPercentage { get; set; } 9 | public decimal TotalAmount { get; set; } 10 | public Guid CustomerId { get; set; } 11 | public CustomerEntity Customer { get; set; } 12 | public Guid? CouponId { get; set; } 13 | public CouponEntity Coupon { get; set; } 14 | public ICollection BasketItems { get; set; } 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/Persistence/UnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Application.Repositories; 2 | using VOEConsulting.Infrastructure.Persistence; 3 | 4 | public class UnitOfWork(BasketAppDbContext dbContext) : IUnitOfWork 5 | { 6 | private readonly BasketAppDbContext _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); 7 | 8 | public async Task SaveChangesAsync(CancellationToken cancellationToken = default) 9 | { 10 | // Save changes to the database 11 | return await _dbContext.SaveChangesAsync(cancellationToken); 12 | } 13 | 14 | public void Dispose() => _dbContext.Dispose(); 15 | } 16 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Baskets/Commands/AddItemToBasket/AddItemToBasketCommand.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Application.Baskets.Commands.AddItemToBasket 2 | { 3 | public record AddItemToBasketCommand(Guid BasketId, SellerRequest Seller, BasketItemRequest BasketItem 4 | , QuantityRequest Quantity) : ICommand 5 | { 6 | } 7 | 8 | public record QuantityRequest(int Value, int QuantityLimit, decimal PricePerUnit); 9 | public record SellerRequest(Guid Id, string Name, float Rating, decimal ShippingLimit, decimal ShippingCost); 10 | public record BasketItemRequest(Guid ItemId, string Name, string ImageUrl, int Limit); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Events/Integration/BasketCreatedIntegrationEvent.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Domain.Baskets; 2 | using VOEConsulting.Flame.Common.Core.Events; 3 | using VOEConsulting.Flame.Common.Domain; 4 | 5 | namespace VOEConsulting.Flame.BasketContext.Application.Events.Integration 6 | { 7 | public sealed class BasketCreatedIntegrationEvent : IntegrationEvent 8 | { 9 | public BasketCreatedIntegrationEvent(Id basketId, Guid customerId) 10 | : base(basketId) 11 | { 12 | CustomerId = customerId; 13 | } 14 | public BasketCreatedIntegrationEvent() { } 15 | 16 | public Guid CustomerId { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/Persistence/Entities/BasketItemEntity.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Infrastructure.Entities 2 | { 3 | public class BasketItemEntity 4 | { 5 | public Guid Id { get; set; } 6 | public string Name { get; set; } = null!; 7 | public string ImageUrl { get; set; } = null!; 8 | public int QuantityValue { get; set; } 9 | public int QuantityLimit { get; set; } 10 | public decimal PricePerUnit { get; set; } 11 | public Guid SellerId { get; set; } 12 | public Guid BasketId { get; set; } 13 | public SellerEntity Seller { get; set; } = null!; 14 | public BasketEntity Basket { get; set; } = null!; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/VOEConsulting.Flame.BasketContext.Tests.Unit/Extensions/ObjectAssertionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions.Primitives; 2 | using VOEConsulting.Flame.Common.Domain.Events; 3 | 4 | namespace VOEConsulting.Flame.BasketContext.Tests.Unit.Extensions 5 | { 6 | public static class ObjectAssertionsExtensions 7 | { 8 | public static AndConstraint BeEquivalentEventTo(this ObjectAssertions obj, TExpectation expectation, string because = "", 9 | params object[] becauseArgs) where TExpectation : IDomainEvent 10 | { 11 | return obj.BeEquivalentTo(expectation, 12 | options => options.Excluding(t => t.OccurredOnUtc) 13 | .Excluding(t => t.Id)); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/VOEConsulting.Flame.BasketContext.Tests.Unit/Factories/BasketItemFactory.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Tests.Data; 2 | using VOEConsulting.Flame.Common.Domain; 3 | using static VOEConsulting.Flame.BasketContext.Tests.Data.BasketItemData; 4 | namespace VOEConsulting.Flame.BasketContext.Tests.Unit.Factories 5 | { 6 | public static class BasketItemFactory 7 | { 8 | public static BasketItem Create(string? name = null, Quantity? quantity = null, string? imageUrl = null, 9 | Seller? seller = null, Id? id = null) 10 | { 11 | return BasketItem.Create(name?? BasketItemName, quantity?? BasketItemQuantity, imageUrl?? BasketItemImageUrl, 12 | seller?? SellerFactory.Create(), id?? BasketItemId); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/VOEConsulting.Flame.BasketContext.Tests.Unit/Factories/SellerFactory.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Tests.Data; 2 | using VOEConsulting.Flame.Common.Domain; 3 | namespace VOEConsulting.Flame.BasketContext.Tests.Unit.Factories 4 | { 5 | public static class SellerFactory 6 | { 7 | public static Seller Create(Id? sellerId = null, string? name = null, float? rating = null, 8 | decimal? shippingLimit = null, decimal? shippingCost = null) 9 | { 10 | return Seller.Create(name ?? BasketItemData.Seller.Name, rating ?? BasketItemData.Seller.Rating 11 | , shippingLimit ?? BasketItemData.Seller.ShippingLimit, shippingCost ?? BasketItemData.Seller.ShippingCost, 12 | sellerId ?? BasketItemData.Seller.Id); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/VOEConsulting.Flame.BasketContext.Tests.Data/BasketItemData.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Domain.Baskets; 2 | using VOEConsulting.Flame.Common.Domain; 3 | 4 | namespace VOEConsulting.Flame.BasketContext.Tests.Data 5 | { 6 | public static class BasketItemData 7 | { 8 | public const int BasketItemLimit = 10; 9 | public static Id BasketItemId = Id.New(); 10 | public const string BasketItemName = "Test Basket Item"; 11 | public const string BasketItemImageUrl = "https://test.com/image.jpg"; 12 | public static Quantity BasketItemQuantity = Quantity.Create(1, BasketItemLimit, 40); 13 | public const decimal BasketItemPrice = 130.0m; 14 | public static Seller Seller = Seller.Create("Test Seller", 8.8f, 200, 39.9m, Id.New()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Customer.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets 2 | { 3 | public sealed class Customer : Entity 4 | { 5 | public bool IsEliteMember { get; } 6 | 7 | private Customer(bool isEliteMember, Id? id) 8 | : base(id ?? Id.FromString("00000000-0000-0000-0000-000000000001")) 9 | { 10 | IsEliteMember = isEliteMember; 11 | } 12 | 13 | public decimal DiscountPercentage 14 | { 15 | get 16 | { 17 | return IsEliteMember ? 0.1m : 0; 18 | } 19 | } 20 | 21 | public static Customer Create(bool isEliteMember, Id? id = null) 22 | { 23 | return new Customer(isEliteMember, id); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docker-compose.dcproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2.1 5 | Linux 6 | False 7 | 893c5b51-a811-4393-bdea-8c7ad9427918 8 | LaunchBrowser 9 | {Scheme}://localhost:{ServicePort}/api/basket 10 | voeconsulting.flame.basketcontext.api 11 | 12 | 13 | 14 | docker-compose.yml 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/Entity.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.Common.Domain 2 | { 3 | public abstract class Entity : IAuditableEntity 4 | { 5 | public Id Id { get; } 6 | public DateTimeOffset CreatedAtUtc { get; } 7 | public DateTimeOffset LastModifiedAtUtc { get; } 8 | 9 | protected Entity(Id id) 10 | { 11 | Id = id; 12 | CreatedAtUtc = DateTimeOffset.UtcNow; 13 | LastModifiedAtUtc = DateTimeOffset.UtcNow; 14 | } 15 | 16 | protected Entity() : this(Id.New()) { } 17 | public override bool Equals(object? obj) 18 | { 19 | if(obj is Entity entity) 20 | { 21 | return entity.Id == Id; 22 | } 23 | return false; 24 | } 25 | 26 | public override int GetHashCode() 27 | { 28 | return Id.GetHashCode(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/ValueObject.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.Common.Domain 2 | { 3 | // You can handle value object creation using records. 4 | // No need to create a separate ValueObject class and inherit from it 5 | public abstract class ValueObject 6 | { 7 | protected abstract IEnumerable GetEqualityComponents(); 8 | 9 | public override bool Equals(object obj) 10 | { 11 | if (obj == null || obj.GetType() != GetType()) 12 | { 13 | return false; 14 | } 15 | 16 | var other = (ValueObject)obj; 17 | return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); 18 | } 19 | 20 | public override int GetHashCode() 21 | { 22 | return GetEqualityComponents() 23 | .Select(x => x != null ? x.GetHashCode() : 0) 24 | .Aggregate((x, y) => x ^ y); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | voeconsulting.flame.basketcontext.api: 5 | environment: 6 | - ASPNETCORE_ENVIRONMENT=Development 7 | - ASPNETCORE_HTTP_PORTS=80 8 | - ASPNETCORE_HTTPS_PORTS=443 9 | - ConnectionStrings__DefaultConnection=Server=sqlserver,1433;Database=BasketDb;User=sa;Password=KENhaLynconUngUIstRI;Encrypt=true;TrustServerCertificate=true 10 | - Kafka__BootstrapServers=kafka1:9092 11 | - Kafka__DefaultTopic=integration-events 12 | ports: 13 | - "8080:80" 14 | - "8081:443" 15 | volumes: 16 | - ${APPDATA}/Microsoft/UserSecrets:/home/app/.microsoft/usersecrets:ro 17 | - ${APPDATA}/ASP.NET/Https:/home/app/.aspnet/https:ro 18 | networks: 19 | - my-app-network 20 | 21 | sqlserver: 22 | ports: 23 | - "1444:1433" 24 | networks: 25 | - my-app-network 26 | 27 | zookeeper: 28 | networks: 29 | - my-app-network 30 | 31 | kafka1: 32 | networks: 33 | - my-app-network 34 | 35 | kafka-ui: 36 | networks: 37 | - my-app-network 38 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/Persistence/BasketAppDbContext.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.EntityFrameworkCore; 3 | using VOEConsulting.Flame.BasketContext.Infrastructure.Entities; 4 | 5 | namespace VOEConsulting.Infrastructure.Persistence; 6 | 7 | public class BasketAppDbContext : DbContext 8 | { 9 | public BasketAppDbContext(DbContextOptions options) 10 | : base(options) { } 11 | 12 | public DbSet Baskets { get; set; } 13 | public DbSet BasketItems { get; set; } 14 | public DbSet Customers { get; set; } 15 | public DbSet Sellers { get; set; } 16 | public DbSet Coupons { get; set; } 17 | protected override void OnModelCreating(ModelBuilder modelBuilder) 18 | { 19 | // Apply Fluent API configurations from the current assembly 20 | modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); 21 | 22 | // Optional: Add additional configurations if required 23 | 24 | base.OnModelCreating(modelBuilder); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tural Suleymani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/Persistence/Configurations/BasketItemConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using VOEConsulting.Flame.BasketContext.Infrastructure.Entities; 4 | 5 | public class BasketItemConfiguration : IEntityTypeConfiguration 6 | { 7 | public void Configure(EntityTypeBuilder builder) 8 | { 9 | builder.HasKey(bi => bi.Id); 10 | 11 | builder.Property(bi => bi.Name) 12 | .IsRequired() 13 | .HasMaxLength(200); 14 | 15 | builder.Property(bi => bi.ImageUrl) 16 | .IsRequired(); 17 | 18 | builder.Property(bi => bi.QuantityValue) 19 | .IsRequired(); 20 | 21 | builder.Property(bi => bi.QuantityLimit) 22 | .IsRequired(); 23 | 24 | builder.Property(bi => bi.PricePerUnit) 25 | .IsRequired(); 26 | 27 | builder.HasOne(bi => bi.Seller) 28 | .WithMany() 29 | .HasForeignKey(bi => bi.SellerId) 30 | .OnDelete(DeleteBehavior.Cascade); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/Persistence/Configurations/SellerEntityConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 2 | using Microsoft.EntityFrameworkCore; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using VOEConsulting.Flame.BasketContext.Infrastructure.Entities; 9 | 10 | namespace VOEConsulting.Flame.BasketContext.Infrastructure.Persistence.Configurations 11 | { 12 | public class SellerEntityConfiguration : IEntityTypeConfiguration 13 | { 14 | public void Configure(EntityTypeBuilder builder) 15 | { 16 | builder.HasKey(s => s.Id); 17 | 18 | builder.Property(s => s.Name) 19 | .IsRequired() 20 | .HasMaxLength(200); 21 | 22 | builder.Property(s => s.Rating) 23 | .IsRequired(); 24 | 25 | builder.Property(s => s.ShippingLimit) 26 | .IsRequired(); 27 | 28 | builder.Property(s => s.ShippingCost) 29 | .IsRequired(); 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/Persistence/Configurations/CouponConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using VOEConsulting.Flame.BasketContext.Infrastructure.Entities; 4 | using static Azure.Core.HttpHeader; 5 | 6 | public class CouponConfiguration : IEntityTypeConfiguration 7 | { 8 | public void Configure(EntityTypeBuilder builder) 9 | { 10 | builder.ToTable("Coupons"); 11 | 12 | builder.HasKey(c => c.Id); 13 | 14 | builder.Property(c => c.Code) 15 | .IsRequired() 16 | .HasMaxLength(10); 17 | 18 | builder.Property(c => c.IsActive) 19 | .IsRequired(); 20 | 21 | builder.Property(c => c.Value) 22 | .IsRequired() 23 | .HasColumnType("decimal(18,2)"); 24 | 25 | builder.Property(c => c.CouponType) 26 | .IsRequired() 27 | .HasMaxLength(20); 28 | 29 | builder.Property(c => c.StartDate) 30 | .IsRequired(); 31 | 32 | builder.Property(c => c.EndDate) 33 | .IsRequired(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Events/Handlers/BasketCreatedEventHandler.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Application.Abstractions.Messaging; 2 | using VOEConsulting.Flame.BasketContext.Application.Events.Integration; 3 | using VOEConsulting.Flame.BasketContext.Domain.Baskets.Events; 4 | using VOEConsulting.Flame.Common.Domain.Events; 5 | 6 | namespace VOEConsulting.Flame.BasketContext.Application.Events.Handlers 7 | { 8 | public class BasketCreatedDomainEventHandler : IDomainEventHandler 9 | { 10 | private readonly IIntegrationEventPublisher _integrationEventPublisher; 11 | 12 | public BasketCreatedDomainEventHandler(IIntegrationEventPublisher integrationEventPublisher) 13 | { 14 | _integrationEventPublisher = integrationEventPublisher; 15 | } 16 | 17 | public async Task Handle(BasketCreatedEvent domainEvent, CancellationToken cancellationToken) 18 | { 19 | var integrationEvent = new BasketCreatedIntegrationEvent(domainEvent.AggregateId, domainEvent.CustomerId); 20 | await _integrationEventPublisher.PublishAsync(integrationEvent); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Baskets/Queries/GetBasket/GetBasketQueryHandler.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Application.Baskets.Queries.GetBasket 2 | { 3 | public class GetBasketQueryHandler : IQueryHandler 4 | { 5 | private readonly IBasketRepository _basketRepository; 6 | private readonly IMapper _mapper; 7 | public GetBasketQueryHandler(IBasketRepository basketRepository, IMapper mapper) 8 | { 9 | _basketRepository = basketRepository; 10 | _mapper = mapper; 11 | } 12 | public async Task> Handle(GetBasketQuery request, CancellationToken cancellationToken) 13 | { 14 | var basket = await _basketRepository.GetByIdAsync(request.BasketId); 15 | if (basket is null) 16 | return Result.Failure(DomainError.NotFound("Basket not found.")); 17 | 18 | // Map the Basket domain object to a BasketDto 19 | var basketDto = _mapper.Map(basket); 20 | return Result.Success(basketDto); 21 | 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Core/Events/IntegrationEvent.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.Common.Core.Events 2 | { 3 | public abstract class IntegrationEvent : IIntegrationEvent 4 | { 5 | public int Version { get; set; } = 1; // Default version for the event 6 | public Guid Id { get; set; } = Guid.NewGuid(); // Unique identifier for the event 7 | public Guid AggregateId { get; set; } // Identifier of the related aggregate 8 | public DateTimeOffset OccurredOnUtc { get; set; } = DateTimeOffset.UtcNow; // Timestamp of event occurrence 9 | public string EventType { get; set; } // Type of the event 10 | 11 | // Constructor to initialize basic properties 12 | protected IntegrationEvent(Guid aggregateId) 13 | { 14 | if (aggregateId == Guid.Empty) 15 | { 16 | throw new ArgumentNullException(nameof(aggregateId), "AggregateId cannot be empty."); 17 | } 18 | 19 | AggregateId = aggregateId; 20 | EventType = GetType().Name; // Use the class name as the event type 21 | } 22 | 23 | // Default constructor for serialization frameworks 24 | protected IntegrationEvent() { } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Coupons/Amount.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.SmartEnum; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Domain.Coupons 4 | { 5 | public sealed class Amount : ValueObject 6 | { 7 | private Amount(decimal value, CouponType couponType) 8 | { 9 | Value = value.EnsureGreaterThan(0); 10 | CouponType = couponType.EnsureNonNull(); 11 | } 12 | 13 | public static Amount Fix(decimal value) => new(value, CouponType.Fix); 14 | public static Amount Percentage(decimal value) => new(value, CouponType.Percentage); 15 | 16 | public decimal Value { get; } 17 | public CouponType CouponType { get; } 18 | protected override IEnumerable GetEqualityComponents() 19 | { 20 | yield return Value; 21 | yield return CouponType; 22 | } 23 | } 24 | 25 | public sealed class CouponType : SmartEnum 26 | { 27 | public static readonly CouponType Fix = new CouponType(nameof(Fix), 1); 28 | public static readonly CouponType Percentage = new CouponType(nameof(Percentage), 2); 29 | 30 | private CouponType(string name, int value) : base(name, value) { } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using VOEConsulting.Flame.BasketContext.Application; 3 | using VOEConsulting.Flame.BasketContext.Infrastructure; 4 | 5 | namespace VOEConsulting.Flame.BasketContext.Api 6 | { 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | var builder = WebApplication.CreateBuilder(args); 12 | 13 | // Add services to the container. 14 | 15 | builder.Services.AddControllers(); 16 | builder.Services.AddApplicationLayer(); 17 | builder.Services.AddInfrastructureServices(builder.Configuration); 18 | 19 | // Register MediatR 20 | builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(Assembly.GetExecutingAssembly())); 21 | 22 | var app = builder.Build(); 23 | 24 | // Apply migrations at runtime 25 | app.ApplyMigrations(); 26 | 27 | // Configure the HTTP request pipeline. 28 | 29 | app.UseHttpsRedirection(); 30 | 31 | app.UseAuthorization(); 32 | 33 | 34 | app.MapControllers(); 35 | 36 | app.Run(); 37 | } 38 | 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/Persistence/Configurations/BasketConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using VOEConsulting.Flame.BasketContext.Infrastructure.Entities; 4 | 5 | public class BasketConfiguration : IEntityTypeConfiguration 6 | { 7 | public void Configure(EntityTypeBuilder builder) 8 | { 9 | builder.ToTable("Baskets"); 10 | 11 | builder.HasKey(b => b.Id); 12 | 13 | builder.Property(b => b.TaxPercentage) 14 | .IsRequired(); 15 | 16 | builder.Property(b => b.TotalAmount) 17 | .HasDefaultValue(0); 18 | 19 | builder.HasOne(b => b.Customer) 20 | .WithMany(c => c.Baskets) 21 | .HasForeignKey(b => b.CustomerId) 22 | .OnDelete(DeleteBehavior.Cascade); 23 | 24 | builder.HasOne(b => b.Coupon) 25 | .WithMany(c => c.Baskets) 26 | .HasForeignKey(b => b.CouponId) 27 | .OnDelete(DeleteBehavior.SetNull); 28 | 29 | builder.HasMany(b => b.BasketItems) 30 | .WithOne(bi => bi.Basket) 31 | .HasForeignKey(bi => bi.BasketId) 32 | .OnDelete(DeleteBehavior.Cascade); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/AggregateRoot.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.Common.Domain.Events; 2 | using VOEConsulting.Flame.Common.Domain.Extensions; 3 | 4 | namespace VOEConsulting.Flame.Common.Domain 5 | { 6 | public interface IAggregateRoot 7 | { 8 | IReadOnlyCollection DomainEvents { get; } 9 | IReadOnlyCollection PopDomainEvents(); 10 | void ClearEvents(); 11 | } 12 | 13 | public abstract class AggregateRoot : Entity, IAggregateRoot 14 | where TModel : IAuditableEntity 15 | { 16 | private readonly IList _domainEvents = []; 17 | public IReadOnlyCollection DomainEvents => _domainEvents.AsReadOnly(); 18 | 19 | public IReadOnlyCollection PopDomainEvents() 20 | { 21 | var events = _domainEvents.ToList(); 22 | ClearEvents(); 23 | return events; 24 | } 25 | public void ClearEvents() 26 | { 27 | _domainEvents.Clear(); 28 | } 29 | 30 | protected void RaiseDomainEvent(IDomainEvent domainEvent) 31 | { 32 | domainEvent.EnsureNonNull(); 33 | _domainEvents.Add(domainEvent); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/DateRange.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.Common.Domain.Extensions; 2 | 3 | namespace VOEConsulting.Flame.Common.Domain 4 | { 5 | public sealed class DateRange : ValueObject 6 | { 7 | private DateRange(DateTimeOffset startDate, DateTimeOffset endDate) 8 | { 9 | StartDate = startDate; 10 | EndDate = endDate.EnsureGreaterThan(startDate); 11 | } 12 | 13 | public static DateRange FromString(string startDate, string endDate) 14 | { 15 | return new DateRange(DateTimeOffset.Parse(startDate), DateTimeOffset.Parse(endDate)); 16 | } 17 | 18 | public static DateRange From(DateTimeOffset startDate, DateTimeOffset endDate) 19 | { 20 | return new DateRange(startDate, endDate); 21 | } 22 | 23 | public DateTimeOffset StartDate { get; } 24 | public DateTimeOffset EndDate { get; } 25 | protected override IEnumerable GetEqualityComponents() 26 | { 27 | yield return StartDate; 28 | yield return EndDate; 29 | } 30 | 31 | public void InRange(DateTimeOffset utcNow) 32 | { 33 | utcNow.EnsureWithinRange(StartDate, EndDate); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/VOEConsulting.Flame.BasketContext.Tests.Unit/VOEConsulting.Flame.BasketContext.Tests.Unit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/VOEConsulting.Flame.BasketContext.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Seller.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Domain.Baskets.Services; 2 | 3 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets 4 | { 5 | public sealed class Seller : Entity 6 | { 7 | private Seller(Id sellerId, string name, float rating, decimal shippingLimit, decimal shippingCost) 8 | : base(sellerId) 9 | { 10 | Name = name; 11 | Rating = rating; 12 | ShippingLimit = shippingLimit; 13 | ShippingCost = shippingCost; 14 | } 15 | 16 | public static Seller Create(string name, float rating, decimal shippingLimit, decimal shippingCost, Id? sellerId) 17 | { 18 | return new Seller(sellerId ?? Id.New(), name, rating, shippingLimit, shippingCost); 19 | } 20 | 21 | public string Name { get; } 22 | public float Rating { get; } 23 | public decimal ShippingLimit { get; } 24 | public decimal ShippingCost { get; } 25 | 26 | public int GetLimitForProduct(string productName, ISellerLimitService limitService) 27 | { 28 | // Delegate the logic to a domain or application service 29 | return limitService.GetLimitForProduct(Id, productName); 30 | } 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/VOEConsulting.Flame.BasketContext.Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Api/VOEConsulting.Flame.BasketContext.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 7cd7bbb5-f7b8-4f2d-b974-86e382f948cb 8 | Linux 9 | ..\.. 10 | ..\..\docker-compose.dcproj 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/Errors/ErrorType.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.SmartEnum; 2 | 3 | namespace VOEConsulting.Flame.Common.Domain.Errors 4 | { 5 | public abstract class ErrorType(string name, int value) : SmartEnum(name, value) 6 | { 7 | public static readonly ErrorType Conflict = new ConflictEnum(); 8 | public static readonly ErrorType NotFound = new NotFoundEnum(); 9 | public static readonly ErrorType BadRequest = new BadRequestEnum(); 10 | public static readonly ErrorType Validation = new ValidationEnum(); 11 | public static readonly ErrorType Unexpected = new UnexpectedEnum(); 12 | 13 | // Define each specific ErrorType as a nested class that extends SmartEnum 14 | private class ConflictEnum : ErrorType 15 | { 16 | public ConflictEnum() : base("Conflict", 0) { } 17 | } 18 | 19 | private class NotFoundEnum : ErrorType 20 | { 21 | public NotFoundEnum() : base("NotFound", 1) { } 22 | } 23 | 24 | private class BadRequestEnum : ErrorType 25 | { 26 | public BadRequestEnum() : base("BadRequest", 2) { } 27 | } 28 | 29 | private class ValidationEnum : ErrorType 30 | { 31 | public ValidationEnum() : base("Validation", 3) { } 32 | } 33 | private class UnexpectedEnum : ErrorType 34 | { 35 | public UnexpectedEnum() : base("Unexpected", 4) { } 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Behaviours/LoggingPipelineBehaviour.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.Extensions.Logging; 3 | using System.Diagnostics; 4 | 5 | namespace VOEConsulting.Flame.BasketContext.Application.Behaviours 6 | { 7 | public class LoggingPipelineBehaviour : IPipelineBehavior 8 | where TRequest : notnull, IRequest 9 | where TResponse : notnull 10 | { 11 | private readonly ILogger> _logger; 12 | 13 | public LoggingPipelineBehaviour(ILogger> logger) 14 | { 15 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 16 | } 17 | 18 | public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) 19 | { 20 | _logger.LogInformation($"Handling process started for {request}"); 21 | var metric = Stopwatch.StartNew(); 22 | var response = await next(); 23 | metric.Stop(); 24 | 25 | if (metric.Elapsed.Seconds > 5) 26 | _logger.LogWarning($"Handling process took too much time. Maybe it needs to be refactored: {metric.Elapsed}"); 27 | 28 | _logger.LogInformation($"Handling process done for {request} and you have response {response}"); 29 | return response; 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/BasketItem.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets 2 | { 3 | public sealed class BasketItem : Entity 4 | { 5 | public string Name { get; } 6 | public string ImageUrl { get; } 7 | 8 | public const int MinItemCount = 1; 9 | public Quantity Quantity { get; private set; } 10 | public Seller Seller { get; } 11 | public bool IsActive { get; private set; } 12 | 13 | private BasketItem(string name, Quantity quantity, string imageUrl, Seller seller, Id? id) 14 | : base(id ?? Id.New()) 15 | { 16 | Name = name.EnsureNonBlank(); 17 | ImageUrl = imageUrl.EnsureImageUrl(); 18 | Quantity = quantity; 19 | Seller = seller; 20 | IsActive = true; 21 | } 22 | 23 | public static BasketItem Create(string name, Quantity quantity, string imageUrl, Seller seller, Id? id = null) 24 | { 25 | return new BasketItem(name, quantity,imageUrl, seller, id); 26 | } 27 | 28 | public void UpdateCount(int basketItemCount) 29 | { 30 | basketItemCount.EnsureGreaterThan(MinItemCount); 31 | Quantity = Quantity.UpdateValue(basketItemCount); 32 | } 33 | public void Deactivate() 34 | { 35 | IsActive = false; 36 | } 37 | public void Activate() 38 | { 39 | IsActive = true; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Behaviours/ValidationPipelineBehaviour.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using MediatR; 3 | using Validation = VOEConsulting.Flame.Common.Domain.Exceptions; 4 | 5 | namespace VOEConsulting.Flame.BasketContext.Application.Behaviours 6 | { 7 | public class ValidationPipelineBehaviour : IPipelineBehavior 8 | where TRequest : notnull, IRequest 9 | where TResponse : notnull 10 | { 11 | private readonly IEnumerable> _validators; 12 | 13 | public ValidationPipelineBehaviour(IEnumerable> validators) 14 | { 15 | _validators = validators ?? throw new ArgumentNullException(nameof(validators)); 16 | } 17 | 18 | public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) 19 | { 20 | if (!_validators.Any()) return await next(); 21 | 22 | var validationContext = new ValidationContext(request); 23 | var validationResponse = await Task.WhenAll(_validators.Select(x => x.ValidateAsync(validationContext, cancellationToken))); 24 | 25 | var validationErrors = validationResponse 26 | .SelectMany(x => x.Errors) 27 | .Where(e => e != null) 28 | .Select(x => x.ErrorMessage) 29 | .ToList(); 30 | 31 | if (validationErrors.Any()) throw new Validation.ValidationException(validationErrors); 32 | 33 | return await next(); 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Quantity.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets 2 | { 3 | public sealed class Quantity : ValueObject 4 | { 5 | public int Value { get; } 6 | public int Limit { get; } 7 | public decimal PricePerUnit { get; } 8 | public decimal TotalPrice => Value * PricePerUnit; 9 | 10 | private Quantity(int value, int limit, decimal pricePerUnit) 11 | { 12 | value.EnsureGreaterThan(0); 13 | limit.EnsureGreaterThan(0); 14 | limit.EnsureAtLeast(value); 15 | pricePerUnit.EnsureGreaterThan(0); 16 | Value = value; 17 | Limit = limit; 18 | PricePerUnit = pricePerUnit; 19 | } 20 | 21 | public static Quantity Create(int value, int limit, decimal pricePerUnit) 22 | { 23 | return new Quantity(value, limit, pricePerUnit); 24 | } 25 | 26 | public Quantity UpdateValue(int newValue) 27 | { 28 | return Create(newValue, Limit, PricePerUnit); 29 | } 30 | 31 | public Quantity UpdateLimit(int newLimit) 32 | { 33 | return Create(Value, newLimit, PricePerUnit); 34 | } 35 | 36 | public Quantity UpdatePrice(decimal newPricePerUnit) 37 | { 38 | return Create(Value, Limit, newPricePerUnit); 39 | } 40 | 41 | protected override IEnumerable GetEqualityComponents() 42 | { 43 | yield return Value; 44 | yield return Limit; 45 | yield return PricePerUnit; 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Coupons/Coupon.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Domain.Coupons.Events; 2 | using VOEConsulting.Flame.Common.Domain.Services; 3 | 4 | namespace VOEConsulting.Flame.BasketContext.Domain.Coupons 5 | { 6 | public sealed class Coupon : AggregateRoot 7 | { 8 | private Coupon(string code, Amount amount, DateRange validityPeriod) 9 | { 10 | Code = code.EnsureLengthInRange(6, 10); 11 | Amount = amount.EnsureNonNull(); 12 | ValidityPeriod = validityPeriod.EnsureNonNull(); 13 | IsActive = true; 14 | } 15 | 16 | public string Code { get; } 17 | public bool IsActive { get; private set; } 18 | public Amount Amount { get; } 19 | public DateRange ValidityPeriod { get; } 20 | 21 | public static Coupon Create(string code, Amount amount, DateRange dateRange) 22 | { 23 | var coupon = new Coupon(code, amount, dateRange); 24 | coupon.RaiseDomainEvent(new CouponCreatedEvent(coupon.Id)); 25 | 26 | return coupon; 27 | } 28 | 29 | public void Activate(IDateTimeProvider dateTimeProvider) 30 | { 31 | IsActive.EnsureFalse(); 32 | ValidityPeriod.InRange(dateTimeProvider.UtcNow()); 33 | IsActive = true; 34 | 35 | RaiseDomainEvent(new CouponActivatedEvent(this.Id)); 36 | } 37 | 38 | public void Deactivate() 39 | { 40 | IsActive.EnsureTrue(); 41 | IsActive = false; 42 | 43 | RaiseDomainEvent(new CouponDeactivatedEvent(this.Id)); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "http": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "launchUrl": "api/basket", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development" 9 | }, 10 | "dotnetRunMessages": true, 11 | "applicationUrl": "http://localhost:5016" 12 | }, 13 | "https": { 14 | "commandName": "Project", 15 | "launchBrowser": true, 16 | "launchUrl": "api/basket", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | }, 20 | "dotnetRunMessages": true, 21 | "applicationUrl": "https://localhost:7274;http://localhost:5016" 22 | }, 23 | "IIS Express": { 24 | "commandName": "IISExpress", 25 | "launchBrowser": true, 26 | "launchUrl": "api/basket", 27 | "environmentVariables": { 28 | "ASPNETCORE_ENVIRONMENT": "Development" 29 | } 30 | }, 31 | "Container (Dockerfile)": { 32 | "commandName": "Docker", 33 | "launchBrowser": true, 34 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/api/basket", 35 | "environmentVariables": { 36 | "ASPNETCORE_HTTPS_PORTS": "8081", 37 | "ASPNETCORE_HTTP_PORTS": "8080" 38 | }, 39 | "publishAllPorts": true, 40 | "useSSL": false 41 | } 42 | }, 43 | "$schema": "http://json.schemastore.org/launchsettings.json", 44 | "iisSettings": { 45 | "windowsAuthentication": false, 46 | "anonymousAuthentication": true, 47 | "iisExpress": { 48 | "applicationUrl": "http://localhost:63788", 49 | "sslPort": 44342 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using VOEConsulting.Flame.Common.Domain.Events; 4 | using VOEConsulting.Flame.BasketContext.Application.Behaviours; 5 | using VOEConsulting.Flame.BasketContext.Application.Events.Dispatchers; 6 | 7 | namespace VOEConsulting.Flame.BasketContext.Application 8 | { 9 | public static class ServiceCollectionExtensions 10 | { 11 | public static IServiceCollection AddApplicationLayer(this IServiceCollection services) 12 | { 13 | services.AddScoped(); 14 | 15 | services.AddMediatR(configuration => 16 | { 17 | configuration.RegisterServicesFromAssembly(typeof(ServiceCollectionExtensions).Assembly); 18 | configuration.AddOpenBehavior(typeof(ExceptionHandlingPipelineBehavior<,>)); 19 | configuration.AddOpenBehavior(typeof(LoggingPipelineBehaviour<,>)); 20 | configuration.AddOpenBehavior(typeof(ValidationPipelineBehaviour<,>)); 21 | }); 22 | 23 | // Register all domain event handlers 24 | services.Scan(scan => scan 25 | .FromAssemblyOf>() 26 | .AddClasses(classes => classes.AssignableTo(typeof(IDomainEventHandler<>))) 27 | .AsImplementedInterfaces() 28 | .WithScopedLifetime()); 29 | 30 | services.AddValidatorsFromAssembly(typeof(ServiceCollectionExtensions).Assembly); 31 | services.AddAutoMapper(typeof(BasketMappingProfile)); 32 | return services; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/Id.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.Common.Domain.Extensions; 2 | namespace VOEConsulting.Flame.Common.Domain 3 | { 4 | public sealed record Id : IId, IComparable, IComparable, IComparable, IEquatable 5 | { 6 | public Guid Value { get; init; } 7 | 8 | // Constructors 9 | public Id(Guid value) => Value = value.EnsureNotDefault(nameof(value)); 10 | public Id() : this(Guid.NewGuid()) { } 11 | 12 | // Factory methods 13 | public static Id New() => new(Guid.NewGuid()); 14 | public static Id FromId(Id id) => new(id.Value); 15 | public static Id FromGuid(Guid id) => new(id); 16 | public static Id FromString(string id) => new(Guid.Parse(id)); 17 | 18 | // Implicit conversions 19 | public static implicit operator Guid?(Id? id) => id?.Value; 20 | 21 | public static implicit operator Guid(Id id) => id.Value; 22 | 23 | public static implicit operator Id(Guid id) => new(id); 24 | 25 | // Comparisons 26 | public int CompareTo(object? obj) 27 | { 28 | if (obj is IId otherId) return CompareTo(otherId); 29 | if (obj is Guid otherGuid) return CompareTo(otherGuid); 30 | if (obj == null) return 1; 31 | 32 | throw new ArgumentException("Object must be of type IId or Guid", nameof(obj)); 33 | } 34 | 35 | public int CompareTo(IId? other) => other?.Value.CompareTo(Value) ?? 1; 36 | public int CompareTo(Guid other) => Value.CompareTo(other); 37 | 38 | // Equality 39 | public bool Equals(IId? other) => other?.Value == Value; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/Errors/DomainError.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace VOEConsulting.Flame.Common.Domain.Errors 3 | { 4 | public record DomainError : IDomainError 5 | { 6 | // Static factory methods for common error types 7 | public static DomainError Conflict(string? message = "The data provided conflicts with existing data.") => 8 | new(message ?? "The data provided conflicts with existing data.", ErrorType.Conflict); 9 | 10 | public static DomainError NotFound(string? message = "The requested item could not be found.") => 11 | new(message ?? "The requested item could not be found.", ErrorType.NotFound); 12 | 13 | public static DomainError BadRequest(string? message = "Invalid request or parameters.") => 14 | new(message ?? "Invalid request or parameters.", ErrorType.BadRequest); 15 | 16 | public static DomainError Validation(string? message = "Validation Failed.", List? errors = null) => 17 | new(message ?? "Validation Failed.", ErrorType.Validation, errors); 18 | 19 | public static DomainError UnExpected(string? message = "Unexpected error happened.") => 20 | new(message ?? "Something when wrong.", ErrorType.Unexpected); 21 | 22 | // Constructor is private to enforce the usage of static methods 23 | private DomainError(string? message, ErrorType errorType, List? errors = null) 24 | { 25 | ErrorMessage = message; 26 | ErrorType = errorType; 27 | Errors = errors ?? new List(); 28 | } 29 | 30 | // Properties for error message and type 31 | public string? ErrorMessage { get; init; } 32 | public ErrorType ErrorType { get; init; } 33 | public List? Errors { get; init; } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Baskets/Commands/CreateBasket/CreateBasketCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Domain.Baskets; 2 | using VOEConsulting.Flame.Common.Domain; 3 | 4 | namespace VOEConsulting.Flame.BasketContext.Application.Baskets.Commands.CreateBasket 5 | { 6 | public class CreateBasketCommandHandler : CommandHandlerBase 7 | { 8 | private readonly IBasketRepository _basketRepository; 9 | private Basket? _createdBasket; 10 | 11 | public CreateBasketCommandHandler( 12 | IBasketRepository basketRepository, 13 | IUnitOfWork unitOfWork, 14 | IDomainEventDispatcher domainEventDispatcher) 15 | : base(domainEventDispatcher, unitOfWork) 16 | { 17 | _basketRepository = basketRepository; 18 | } 19 | 20 | protected override async Task> ExecuteAsync(CreateBasketCommand request, CancellationToken cancellationToken) 21 | { 22 | // Step 1: Core operation 23 | if(await _basketRepository.IsExistByCustomerIdAsync(request.Customer.Id)) 24 | { 25 | return Result.Failure(DomainError.Conflict("Basket already exist for the given customer")); 26 | } 27 | Customer customer = Customer.Create(request.Customer.IsEliteMember, request.Customer.Id); 28 | _createdBasket = Basket.Create(request.TaxPercentage, customer); 29 | 30 | await _basketRepository.AddAsync(_createdBasket, cancellationToken); 31 | 32 | return Result.Success(_createdBasket.Id); 33 | } 34 | 35 | protected override IAggregateRoot? GetAggregateRoot(Result result) 36 | { 37 | // Return the created aggregate root to dispatch domain events 38 | return _createdBasket; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Api/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base 4 | USER app 5 | WORKDIR /app 6 | EXPOSE 8080 7 | EXPOSE 8081 8 | 9 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 10 | ARG BUILD_CONFIGURATION=Release 11 | WORKDIR /src 12 | COPY ["src/VOEConsulting.Flame.BasketContext.Api/VOEConsulting.Flame.BasketContext.Api.csproj", "src/VOEConsulting.Flame.BasketContext.Api/"] 13 | COPY ["src/VOEConsulting.Flame.BasketContext.Application/VOEConsulting.Flame.BasketContext.Application.csproj", "src/VOEConsulting.Flame.BasketContext.Application/"] 14 | COPY ["src/VOEConsulting.Flame.BasketContext.Domain/VOEConsulting.Flame.BasketContext.Domain.csproj", "src/VOEConsulting.Flame.BasketContext.Domain/"] 15 | COPY ["src/VOEConsulting.Flame.Common.Domain/VOEConsulting.Flame.Common.Domain.csproj", "src/VOEConsulting.Flame.Common.Domain/"] 16 | COPY ["src/VOEConsulting.Flame.Common.Core/VOEConsulting.Flame.Common.Core.csproj", "src/VOEConsulting.Flame.Common.Core/"] 17 | COPY ["src/VOEConsulting.Flame.BasketContext.Infrastructure/VOEConsulting.Flame.BasketContext.Infrastructure.csproj", "src/VOEConsulting.Flame.BasketContext.Infrastructure/"] 18 | RUN dotnet restore "./src/VOEConsulting.Flame.BasketContext.Api/VOEConsulting.Flame.BasketContext.Api.csproj" 19 | COPY . . 20 | WORKDIR "/src/src/VOEConsulting.Flame.BasketContext.Api" 21 | RUN dotnet build "./VOEConsulting.Flame.BasketContext.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build 22 | 23 | FROM build AS publish 24 | ARG BUILD_CONFIGURATION=Release 25 | RUN dotnet publish "./VOEConsulting.Flame.BasketContext.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false 26 | 27 | FROM base AS final 28 | WORKDIR /app 29 | COPY --from=publish /app/publish . 30 | ENTRYPOINT ["dotnet", "VOEConsulting.Flame.BasketContext.Api.dll"] -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/Exceptions/ValidationException.cs: -------------------------------------------------------------------------------- 1 | namespace VOEConsulting.Flame.Common.Domain.Exceptions 2 | { 3 | 4 | [Serializable] 5 | public class ValidationException : Exception 6 | { 7 | public IReadOnlyList Errors { get; } 8 | 9 | // Constructor to initialize with multiple errors 10 | public ValidationException(IEnumerable errors) 11 | : base("Validation failed.") 12 | { 13 | Errors = errors?.ToList() ?? new List(); 14 | } 15 | 16 | // Constructor to initialize with a single error 17 | public ValidationException(string error) 18 | : base(error) 19 | { 20 | Errors = new List(); 21 | } 22 | 23 | // Constructor to provide a custom message and an inner exception 24 | public ValidationException(string message, Exception inner) 25 | : base(message, inner) 26 | { 27 | Errors = new List() { inner.Message }; // Default to an empty list 28 | } 29 | 30 | // Serialization constructor 31 | protected ValidationException( 32 | System.Runtime.Serialization.SerializationInfo info, 33 | System.Runtime.Serialization.StreamingContext context) 34 | : base(info, context) 35 | { 36 | Errors = (IReadOnlyList)info.GetValue(nameof(Errors), typeof(IReadOnlyList)) 37 | ?? new List(); 38 | } 39 | 40 | // Override GetObjectData to serialize the Errors property 41 | public override void GetObjectData( 42 | System.Runtime.Serialization.SerializationInfo info, 43 | System.Runtime.Serialization.StreamingContext context) 44 | { 45 | base.GetObjectData(info, context); 46 | info.AddValue(nameof(Errors), Errors, typeof(IReadOnlyList)); 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/MappingProfiles/BasketProfile.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Domain.Baskets; 2 | 3 | public class BasketMappingProfile : Profile 4 | { 5 | public BasketMappingProfile() 6 | { 7 | // Map Basket to BasketDto 8 | CreateMap() 9 | .ForMember(dest => dest.BasketItems, opt => opt.MapFrom(src => 10 | src.BasketItems.ToDictionary( 11 | kvp => MapSeller(kvp.Key), 12 | kvp => new BasketItemInfoDto 13 | { 14 | Items = kvp.Value.Items.Select(MapBasketItem).ToList(), 15 | ShippingAmountLeft = kvp.Value.ShippingAmountLeft 16 | }))) 17 | .ForMember(dest => dest.CouponId, opt => opt.MapFrom(src => src.CouponId)); 18 | 19 | // Map Customer to CustomerDto 20 | CreateMap(); 21 | 22 | // Map BasketItem to BasketItemDto 23 | CreateMap() 24 | .ForMember(dest => dest.Quantity, opt => opt.MapFrom(src => src.Quantity)); 25 | 26 | // Map Seller to SellerDto 27 | CreateMap(); 28 | } 29 | 30 | // Manual mappings for complex dictionary structures 31 | private static SellerDto MapSeller(Seller seller) 32 | { 33 | return new SellerDto 34 | { 35 | Id = seller.Id.Value, 36 | Name = seller.Name, 37 | Rating = seller.Rating, 38 | ShippingLimit = seller.ShippingLimit, 39 | ShippingCost = seller.ShippingCost 40 | }; 41 | } 42 | 43 | private static BasketItemDto MapBasketItem(BasketItem basketItem) 44 | { 45 | return new BasketItemDto 46 | { 47 | Id = basketItem.Id.Value, 48 | Price = basketItem.Quantity.PricePerUnit, 49 | Quantity = basketItem.Quantity.Value, 50 | TotalPrice = basketItem.Quantity.TotalPrice, 51 | IsActive = basketItem.IsActive 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Api/Controllers/BasketController.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.AspNetCore.Mvc; 3 | using VOEConsulting.Flame.BasketContext.Application.Baskets.Commands.AddItemToBasket; 4 | using VOEConsulting.Flame.BasketContext.Application.Baskets.Commands.CreateBasket; 5 | using VOEConsulting.Flame.BasketContext.Application.Baskets.Queries.GetBasket; 6 | 7 | namespace VOEConsulting.Flame.BasketContext.Api.Controllers 8 | { 9 | [ApiController] 10 | [Route("api/[controller]")] 11 | public class BasketController(ISender sender, ILogger logger) : BaseController(logger) 12 | { 13 | private readonly ISender _sender = sender; 14 | 15 | // GET api/basket/{id} 16 | [HttpGet("{id}")] 17 | public async Task GetBasket(Guid id) 18 | { 19 | var result = await _sender.Send(new GetBasketQuery(id)); 20 | return result.IsSuccess ? Ok(result) : HandleError(result.Error); 21 | } 22 | 23 | // POST api/basket 24 | [HttpPost] 25 | public async Task CreateBasket([FromBody] CreateBasketCommand command) 26 | { 27 | var result = await _sender.Send(command); 28 | if (result.IsSuccess) 29 | return CreatedAtAction(nameof(GetBasket), new { id = result.Value }, result.Value); 30 | return HandleError(result.Error); 31 | } 32 | 33 | // POST api/basket/{id}/items 34 | [HttpPost("{id}/items")] 35 | public async Task AddItemToBasket(Guid id, [FromBody] AddItemToBasketCommand command) 36 | { 37 | if (id != command.BasketId) 38 | return BadRequest("Basket ID in URL does not match the command."); 39 | 40 | var result = await _sender.Send(command); 41 | if(result.IsSuccess) 42 | return NoContent(); 43 | else 44 | return HandleError(result.Error); 45 | } 46 | 47 | [HttpGet("/test")] 48 | public string Test() 49 | { 50 | return "hello"; 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Baskets/Commands/AddItemToBasket/AddItemToBasketCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Domain.Baskets; 2 | using VOEConsulting.Flame.Common.Domain; 3 | 4 | namespace VOEConsulting.Flame.BasketContext.Application.Baskets.Commands.AddItemToBasket 5 | { 6 | public class AddItemToBasketCommandHandler : CommandHandlerBase 7 | { 8 | private readonly IBasketRepository _basketRepository; 9 | private Basket? _basket; 10 | 11 | public AddItemToBasketCommandHandler( 12 | IBasketRepository basketRepository, 13 | IUnitOfWork unitOfWork, 14 | IDomainEventDispatcher domainEventDispatcher) 15 | : base( domainEventDispatcher, unitOfWork) 16 | { 17 | _basketRepository = basketRepository; 18 | } 19 | 20 | protected override async Task> ExecuteAsync(AddItemToBasketCommand request, CancellationToken cancellationToken) 21 | { 22 | 23 | var quantity = Quantity.Create(request.Quantity.Value, request.Quantity.QuantityLimit, request.Quantity.PricePerUnit); 24 | 25 | var seller = Seller.Create(request.Seller.Name, request.Seller.Rating, request.Seller.ShippingLimit, request.Seller.ShippingCost, request.Seller.Id); 26 | 27 | _basket = await _basketRepository.GetByIdAsync(request.BasketId); 28 | 29 | if (_basket == null) 30 | return Result.Failure(DomainError.NotFound()); 31 | 32 | var basketItem = BasketItem.Create(request.BasketItem.Name, quantity, request.BasketItem.ImageUrl, seller,request.BasketItem.ItemId); 33 | 34 | _basket.AddItem(basketItem); 35 | 36 | await _basketRepository.AddBasketItemAsync(request.BasketId, basketItem); 37 | 38 | return Result.Success(basketItem.Id); 39 | } 40 | 41 | protected override IAggregateRoot? GetAggregateRoot(Result result) 42 | { 43 | // Return the created aggregate root to dispatch domain events 44 | return _basket; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | networks: 4 | my-app-network: 5 | driver: bridge 6 | 7 | services: 8 | voeconsulting.flame.basketcontext.api: 9 | image: ${DOCKER_REGISTRY-}voeconsultingflamebasketcontextapi 10 | build: 11 | context: . 12 | dockerfile: src/VOEConsulting.Flame.BasketContext.Api/Dockerfile 13 | networks: 14 | - my-app-network 15 | depends_on: 16 | - sqlserver 17 | - kafka1 18 | 19 | sqlserver: 20 | image: mcr.microsoft.com/mssql/server:2019-latest 21 | ports: 22 | - "1444:1433" 23 | environment: 24 | ACCEPT_EULA: "Y" 25 | SA_PASSWORD: "KENhaLynconUngUIstRI" 26 | networks: 27 | - my-app-network 28 | volumes: 29 | - sql_data:/var/opt/mssql 30 | 31 | zookeeper: 32 | image: bitnami/zookeeper:3.8 33 | ports: 34 | - "2181:2181" 35 | volumes: 36 | - zookeeper_data:/bitnami 37 | environment: 38 | ALLOW_ANONYMOUS_LOGIN: "yes" 39 | networks: 40 | - my-app-network 41 | 42 | kafka1: 43 | image: bitnami/kafka:3.6 44 | volumes: 45 | - kafka_data1:/bitnami 46 | environment: 47 | KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper:2181 48 | KAFKA_CFG_LISTENERS: INTERNAL://:9092,EXTERNAL://0.0.0.0:29092 49 | KAFKA_CFG_ADVERTISED_LISTENERS: INTERNAL://kafka1:9092,EXTERNAL://localhost:29092 50 | KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT 51 | KAFKA_CFG_INTER_BROKER_LISTENER_NAME: INTERNAL 52 | KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: 'true' 53 | ALLOW_PLAINTEXT_LISTENER: 'yes' 54 | ports: 55 | - "9092:9092" 56 | - "29092:29092" 57 | depends_on: 58 | - zookeeper 59 | networks: 60 | - my-app-network 61 | 62 | kafka-ui: 63 | image: provectuslabs/kafka-ui:latest 64 | ports: 65 | - 9100:8080 66 | environment: 67 | KAFKA_CLUSTERS_0_NAME: local 68 | KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka1:9092 69 | KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181 70 | depends_on: 71 | - kafka1 72 | networks: 73 | - my-app-network 74 | 75 | volumes: 76 | zookeeper_data: 77 | driver: local 78 | kafka_data1: 79 | driver: local 80 | sql_data: 81 | driver: local 82 | -------------------------------------------------------------------------------- /tests/VOEConsulting.Flame.BasketContext.Tests.Unit/BasketItemTests.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Tests.Data; 2 | using VOEConsulting.Flame.Common.Domain.Exceptions; 3 | 4 | namespace VOEConsulting.Flame.BasketContext.Tests.Unit 5 | { 6 | public class BasketItemTests 7 | { 8 | 9 | [Fact] 10 | public void Create_WhenValidArgumentProvided_ShouldCreateBasketItem() 11 | { 12 | //Arrange & Act 13 | BasketItem basketItem = BasketItem.Create(BasketItemData.BasketItemName, 14 | BasketItemData.BasketItemQuantity,BasketItemData.BasketItemImageUrl, 15 | BasketItemData.Seller); 16 | 17 | //Assert 18 | basketItem.Should().NotBeNull(); 19 | } 20 | 21 | [Theory] 22 | [InlineData(10, 13)] 23 | [InlineData(8, 15)] 24 | [InlineData(3, 19)] 25 | [InlineData(49, 56)] 26 | [InlineData(101, 134)] 27 | public void UpdateCount_WhenItemCountIsGreaterThanLimit_ShouldFail(int basketItemLimit, int basketItemCount) 28 | { 29 | //Arrange 30 | var quantity = TestFactories.QuantityFactory.Create(limit: basketItemLimit); 31 | var basketItem = TestFactories.BasketItemFactory.Create(quantity: quantity); 32 | 33 | //Act 34 | var action = () => basketItem.UpdateCount(basketItemCount); 35 | 36 | //Assert 37 | action.Should().ThrowExactly(); 38 | } 39 | 40 | [Theory] 41 | [InlineData(20, 13)] 42 | [InlineData(80, 15)] 43 | [InlineData(31, 19)] 44 | [InlineData(491, 56)] 45 | [InlineData(181, 134)] 46 | public void UpdateCount_WhenItemCountIsLessThanLimit_ShouldUpdateBasketItem(int basketItemLimit, int basketItemCount) 47 | { 48 | //Arrange 49 | var quantity = TestFactories.QuantityFactory.Create(limit: basketItemLimit); 50 | var basketItem = TestFactories.BasketItemFactory.Create(quantity: quantity); 51 | 52 | //Act 53 | basketItem.UpdateCount(basketItemCount); 54 | 55 | //Assert 56 | basketItem.Quantity.Value.Should().Be(basketItemCount); 57 | 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/Events/DomainEvent.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using VOEConsulting.Flame.Common.Domain.Events.Decorators; 3 | 4 | namespace VOEConsulting.Flame.Common.Domain.Events 5 | { 6 | public class DomainEvent : IDomainEvent 7 | { 8 | public int Version { get; set; } = 1; // Default version set to 1 9 | 10 | public Guid Id { get; set; } = Guid.NewGuid(); 11 | 12 | public Guid AggregateId { get; set; } 13 | 14 | public DateTimeOffset OccurredOnUtc { get; set; } 15 | 16 | public string EventType { get; set; } 17 | 18 | public string AggregateType { get; set; } 19 | 20 | public string? TraceInfo { get; set; } 21 | 22 | // Default constructor 23 | public DomainEvent() { } 24 | 25 | // Parameterized constructor 26 | protected DomainEvent(Guid aggregateId, DateTimeOffset occurredOnUtc) 27 | { 28 | AggregateId = aggregateId != Guid.Empty ? aggregateId : throw new ArgumentNullException(nameof(aggregateId)); 29 | OccurredOnUtc = occurredOnUtc; 30 | AggregateType = GetAggregateType(GetType()) ?? throw new InvalidOperationException("Aggregate type cannot be null."); 31 | EventType = GetEventType(this); 32 | } 33 | 34 | // Retrieves the AggregateType for a given event type 35 | public static string GetAggregateType() where TEvent : IDomainEvent => 36 | GetAggregateType(typeof(TEvent)); 37 | 38 | public static string GetAggregateType(Type eventType) 39 | { 40 | var attribute = eventType.GetCustomAttribute(); 41 | return attribute?.AggregateType ?? string.Empty; 42 | } 43 | 44 | // Retrieves the EventType based on the event and its aggregate type 45 | public static string GetEventType(IDomainEvent @event) => 46 | GetEventType(@event.GetType(), @event.AggregateType); 47 | 48 | public static string GetEventType() where TEvent : IDomainEvent => 49 | GetEventType(typeof(TEvent)); 50 | 51 | public static string GetEventType(Type eventType, string? prefix = null) 52 | { 53 | prefix ??= GetAggregateType(eventType); 54 | return $"{prefix}.{eventType.Name}"; 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/Messaging/KafkaIntegrationEventPublisher.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Confluent.Kafka; 3 | using VOEConsulting.Flame.BasketContext.Application.Abstractions.Messaging; 4 | using VOEConsulting.Flame.Common.Core.Events; 5 | 6 | namespace Infrastructure.Messaging 7 | { 8 | public class KafkaIntegrationEventPublisher : IIntegrationEventPublisher 9 | { 10 | private readonly IProducer _producer; 11 | private readonly string _defaultTopic; 12 | 13 | public KafkaIntegrationEventPublisher(string bootstrapServers, string defaultTopic) 14 | { 15 | var config = new ProducerConfig 16 | { 17 | BootstrapServers = bootstrapServers, 18 | Acks = Acks.All, // Wait for all replicas to acknowledge 19 | EnableIdempotence = true, // Ensure exactly-once delivery 20 | CompressionType = CompressionType.Snappy, // Optimize message size 21 | LingerMs = 5 // Batch messages for better throughput 22 | }; 23 | 24 | _producer = new ProducerBuilder(config).Build(); 25 | _defaultTopic = defaultTopic; 26 | } 27 | 28 | public async Task PublishAsync(T @event) where T : IntegrationEvent 29 | { 30 | // Determine the topic based on the event type 31 | var topic = _defaultTopic ?? @event.GetType().Name; 32 | 33 | // Serialize the event to JSON 34 | var message = JsonSerializer.Serialize(@event); 35 | 36 | try 37 | { 38 | // Produce the message 39 | var kafkaMessage = new Message 40 | { 41 | Key = @event.AggregateId.ToString(), 42 | Value = message 43 | }; 44 | 45 | var deliveryResult = await _producer.ProduceAsync(topic, kafkaMessage); 46 | 47 | Console.WriteLine($"Delivered event to Kafka: {deliveryResult.TopicPartitionOffset}"); 48 | } 49 | catch (ProduceException ex) 50 | { 51 | // Log the exception or handle it as per your needs 52 | Console.WriteLine($"Failed to deliver event: {ex.Message}"); 53 | throw; 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/InfrastructureExtensions.cs: -------------------------------------------------------------------------------- 1 | using Infrastructure.Messaging; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using System.Reflection; 7 | using VOEConsulting.Flame.BasketContext.Application.Abstractions.Messaging; 8 | using VOEConsulting.Flame.BasketContext.Application.Repositories; 9 | using VOEConsulting.Flame.BasketContext.Infrastructure.Persistence.Repositories; 10 | using VOEConsulting.Infrastructure.Persistence; 11 | namespace VOEConsulting.Flame.BasketContext.Infrastructure 12 | { 13 | public static class InfrastructureExtensions 14 | { 15 | public static void ApplyMigrations(this IHost app) 16 | { 17 | using (var scope = app.Services.CreateScope()) 18 | { 19 | var services = scope.ServiceProvider; 20 | try 21 | { 22 | var context = services.GetRequiredService(); 23 | context.Database.Migrate(); 24 | Console.WriteLine("Database migrations applied successfully."); 25 | } 26 | catch (Exception ex) 27 | { 28 | Console.WriteLine($"An error occurred while applying migrations: {ex.Message}"); 29 | throw; 30 | } 31 | } 32 | } 33 | 34 | public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration) 35 | { 36 | services.AddScoped(); 37 | services.AddScoped(); 38 | 39 | // Register AutoMapper 40 | services.AddAutoMapper(Assembly.GetExecutingAssembly()); 41 | 42 | // Get Kafka configuration from appsettings.json 43 | var kafkaConfig = configuration.GetSection("Kafka"); 44 | var bootstrapServers = kafkaConfig["BootstrapServers"]; 45 | var defaultTopic = kafkaConfig["DefaultTopic"]; 46 | 47 | // Register KafkaIntegrationEventPublisher 48 | services.AddSingleton(sp => 49 | { 50 | return new KafkaIntegrationEventPublisher(bootstrapServers!, defaultTopic!); 51 | }); 52 | 53 | // Register DbContext with a connection string 54 | services.AddDbContext(options => 55 | options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"))); 56 | 57 | return services; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Behaviours/ExceptionHandlingPipelineBehaviour.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using VOEConsulting.Flame.Common.Core.Exceptions; 3 | using VOEConsulting.Flame.Common.Domain.Exceptions; 4 | 5 | namespace VOEConsulting.Flame.BasketContext.Application.Behaviours 6 | { 7 | public class ExceptionHandlingPipelineBehavior : IPipelineBehavior 8 | where TRequest : notnull, IRequest 9 | where TResponse : notnull 10 | { 11 | public async Task Handle( 12 | TRequest request, 13 | RequestHandlerDelegate next, 14 | CancellationToken cancellationToken) 15 | { 16 | try 17 | { 18 | // Proceed to the next behavior or actual handler 19 | return await next(); 20 | } 21 | catch (ValidationException ex) 22 | { 23 | // Handle validation exceptions by returning a DomainError.Validation 24 | var domainError = DomainError.Validation(ex.Message, ex.Errors.ToList()); 25 | var failureResult = Result.Failure(domainError); 26 | 27 | if (failureResult is TResponse response) 28 | { 29 | return response; 30 | } 31 | 32 | throw new InvalidCastException("Failed to cast validation error result to the expected TResponse type."); 33 | } 34 | catch (FlameApplicationException ex) 35 | { 36 | // Handle application-specific exceptions 37 | var domainError = DomainError.BadRequest(ex.Message); 38 | var failureResult = Result.Failure(domainError); 39 | 40 | if (failureResult is TResponse response) 41 | { 42 | return response; 43 | } 44 | 45 | throw new InvalidCastException("Failed to cast bad request error result to the expected TResponse type."); 46 | } 47 | catch (Exception ex) 48 | { 49 | // Handle unexpected exceptions by returning a DomainError.Unexpected 50 | var domainError = DomainError.UnExpected($"An unexpected error occurred: {ex.Message}"); 51 | var failureResult = Result.Failure(domainError); 52 | 53 | if (failureResult is TResponse response) 54 | { 55 | return response; 56 | } 57 | 58 | throw new InvalidCastException("Failed to cast unexpected error result to the expected TResponse type."); 59 | } 60 | } 61 | } 62 | 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Api/Controllers/BaseController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using VOEConsulting.Flame.BasketContext.Api.Extensions; 3 | using VOEConsulting.Flame.Common.Domain.Errors; 4 | 5 | namespace VOEConsulting.Flame.BasketContext.Api.Controllers 6 | { 7 | [ApiController] 8 | public abstract partial class BaseController : ControllerBase 9 | { 10 | protected readonly ILogger _logger; 11 | private readonly Dictionary?, ObjectResult>> _errorHandlers; 12 | 13 | protected BaseController(ILogger logger) 14 | { 15 | _logger = logger; 16 | _errorHandlers = new Dictionary?, ObjectResult>> 17 | { 18 | { ErrorType.Conflict, (details, errors) => ConflictResponse(details, errors) }, 19 | { ErrorType.NotFound, (details, errors) => NotFoundResponse(details, errors) }, 20 | { ErrorType.BadRequest, (details, errors) => BadRequestResponse(details, errors) }, 21 | { ErrorType.Validation, (details, errors) => ValidationResponse(details, errors) }, 22 | { ErrorType.Unexpected, (details, errors) => UnexpectedResponse(details, errors) } 23 | }; 24 | } 25 | 26 | protected ObjectResult HandleError(IDomainError error) 27 | { 28 | if (_errorHandlers.TryGetValue(error.ErrorType, out var handler)) 29 | { 30 | return handler(error.ErrorMessage, error.Errors); 31 | } 32 | 33 | throw new InvalidOperationException($"Unsupported error type: {error.ErrorType}"); 34 | } 35 | 36 | protected ObjectResult NotFoundResponse(string? message = null, IEnumerable? errors = null) => 37 | NotFound(ProblemDetailsFactory.CreateNotFound(HttpContext, message, errors)); 38 | 39 | protected ObjectResult BadRequestResponse(string? details = null, IEnumerable? errors = null) => 40 | BadRequest(ProblemDetailsFactory.CreateBadRequest(HttpContext, details, errors)); 41 | 42 | protected ObjectResult ConflictResponse(string? details = null, IEnumerable? errors = null) => 43 | Conflict(ProblemDetailsFactory.CreateConflict(HttpContext, details, errors)); 44 | 45 | protected ObjectResult ValidationResponse(string? details = null, IEnumerable? errors = null) => 46 | BadRequest(ProblemDetailsFactory.CreateValidation(HttpContext, details, errors)); 47 | 48 | protected ObjectResult UnexpectedResponse(string? details = null, IEnumerable? errors = null) => 49 | BadRequest(ProblemDetailsFactory.CreateUnexpectedResponse(HttpContext, details, errors)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Abstractions/CommandHandlerBase.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.Common.Domain; 2 | using VOEConsulting.Flame.Common.Domain.Events; 3 | 4 | namespace VOEConsulting.Flame.BasketContext.Application.Abstractions 5 | { 6 | public abstract class CommandHandlerBase : ICommandHandler 7 | where TCommand : ICommand 8 | where TResponse : notnull 9 | { 10 | private readonly IDomainEventDispatcher _domainEventDispatcher; 11 | private readonly IUnitOfWork _unitOfWork; 12 | 13 | protected CommandHandlerBase( 14 | IDomainEventDispatcher domainEventDispatcher, 15 | IUnitOfWork unitOfWork) 16 | { 17 | _domainEventDispatcher = domainEventDispatcher; 18 | _unitOfWork = unitOfWork; 19 | } 20 | 21 | public async Task> Handle(TCommand request, CancellationToken cancellationToken) 22 | { 23 | 24 | // Step 1: Execute core operation 25 | var operationResult = await ExecuteAsync(request, cancellationToken); 26 | if (!operationResult.IsSuccess) 27 | { 28 | return operationResult; // Return failure result 29 | } 30 | 31 | // Step 2: Commit Unit of Work 32 | await _unitOfWork.SaveChangesAsync(cancellationToken); 33 | 34 | // Step 3: Dispatch Domain Events 35 | var aggregateRoot = GetAggregateRoot(operationResult); 36 | if (aggregateRoot != null) 37 | { 38 | var domainEvents = aggregateRoot.PopDomainEvents(); 39 | await DispatchDomainEventsAsync(domainEvents, cancellationToken); 40 | } 41 | 42 | // Step 4: Return Result 43 | return operationResult; 44 | 45 | 46 | } 47 | 48 | /// 49 | /// Executes the core operation logic for the command. 50 | /// 51 | protected abstract Task> ExecuteAsync(TCommand request, CancellationToken cancellationToken); 52 | 53 | /// 54 | /// Extracts the aggregate root for dispatching domain events. 55 | /// 56 | protected abstract IAggregateRoot? GetAggregateRoot(Result result); 57 | 58 | /// 59 | /// Manually dispatches a collection of domain events. 60 | /// 61 | protected async Task DispatchDomainEventsAsync(IEnumerable domainEvents, CancellationToken cancellationToken) 62 | { 63 | if (domainEvents == null) return; 64 | await _domainEventDispatcher.DispatchAsync(domainEvents, cancellationToken); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/Persistence/Profiles/BasketProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using VOEConsulting.Flame.BasketContext.Domain.Baskets; 3 | using VOEConsulting.Flame.BasketContext.Infrastructure.Entities; 4 | 5 | public class BasketMappingProfile : Profile 6 | { 7 | public BasketMappingProfile() 8 | { 9 | // Domain -> Entity mappings 10 | CreateMap() 11 | .ForMember(dest => dest.CustomerId, opt => opt.MapFrom(src => src.Customer.Id)) 12 | .ForMember(dest => dest.BasketItems, opt => opt.MapFrom(src => src.BasketItems.SelectMany(kvp => kvp.Value.Items))); 13 | 14 | CreateMap() 15 | .ForMember(dest => dest.PricePerUnit, opt => opt.MapFrom(src => src.Quantity.PricePerUnit)) 16 | .ForMember(dest => dest.SellerId, opt => opt.MapFrom(src => src.Seller.Id)) 17 | .ForMember(dest => dest.Seller, opt => opt.MapFrom(src => src.Seller)); // Avoid circular mapping 18 | 19 | CreateMap(); 20 | CreateMap(); 21 | 22 | // Entity -> Domain mappings 23 | CreateMap() 24 | .ConstructUsing((entity, context) => 25 | { 26 | var customer = context.Mapper.Map(entity.Customer); 27 | return Basket.Create(entity.TaxPercentage, customer); 28 | }) 29 | .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) 30 | .ForMember(dest => dest.BasketItems, opt => opt.Ignore()) // Manual mapping for complex structure 31 | .AfterMap((entity, domain, context) => 32 | { 33 | if (entity.BasketItems != null) 34 | { 35 | foreach (var itemEntity in entity.BasketItems) 36 | { 37 | var seller = context.Mapper.Map(itemEntity.Seller); 38 | var basketItem = context.Mapper.Map(itemEntity); 39 | domain.AddItem(basketItem); 40 | } 41 | } 42 | }); 43 | 44 | CreateMap() 45 | .ConstructUsing((entity, context) => 46 | { 47 | var seller = context.Mapper.Map(entity.Seller); 48 | var quantity = Quantity.Create(entity.QuantityValue, entity.QuantityLimit, entity.PricePerUnit); 49 | return BasketItem.Create(entity.Name, quantity, entity.ImageUrl, seller, entity.Id); 50 | }); 51 | 52 | CreateMap() 53 | .ConstructUsing(entity => Seller.Create(entity.Name, entity.Rating, entity.ShippingLimit, entity.ShippingCost, entity.Id)); 54 | 55 | CreateMap() 56 | .ConstructUsing(entity => Customer.Create(entity.IsEliteMember, entity.Id)); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Api/Extensions/ProblemDetailsExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Infrastructure; 2 | using Microsoft.AspNetCore.Mvc; 3 | using System.Text; 4 | 5 | namespace VOEConsulting.Flame.BasketContext.Api.Extensions 6 | { 7 | public static class ProblemDetailsExtensions 8 | { 9 | public static ProblemDetails CreateNotFound( 10 | this ProblemDetailsFactory detailsFactory, 11 | HttpContext context, 12 | string? details = null, 13 | IEnumerable? errors = null) 14 | { 15 | return CreateProblemDetailsWith(detailsFactory, StatusCodes.Status404NotFound, context, details, errors); 16 | } 17 | 18 | public static ProblemDetails CreateBadRequest( 19 | this ProblemDetailsFactory detailsFactory, 20 | HttpContext context, 21 | string? details = null, 22 | IEnumerable? errors = null) 23 | { 24 | return CreateProblemDetailsWith(detailsFactory, StatusCodes.Status400BadRequest, context, details, errors); 25 | } 26 | 27 | public static ProblemDetails CreateConflict( 28 | this ProblemDetailsFactory detailsFactory, 29 | HttpContext context, 30 | string? details = null, 31 | IEnumerable? errors = null) 32 | { 33 | return CreateProblemDetailsWith(detailsFactory, StatusCodes.Status409Conflict, context, details, errors); 34 | } 35 | 36 | public static ProblemDetails CreateValidation( 37 | this ProblemDetailsFactory detailsFactory, 38 | HttpContext context, 39 | string? details = null, 40 | IEnumerable? errors = null) 41 | { 42 | return CreateProblemDetailsWith(detailsFactory, StatusCodes.Status400BadRequest, context, details, errors); 43 | } 44 | 45 | public static ProblemDetails CreateUnexpectedResponse( 46 | this ProblemDetailsFactory detailsFactory, 47 | HttpContext context, 48 | string? details = null, 49 | IEnumerable? errors = null) 50 | { 51 | return CreateProblemDetailsWith(detailsFactory, StatusCodes.Status500InternalServerError, context, details, errors); 52 | } 53 | 54 | 55 | private static ProblemDetails CreateProblemDetailsWith(ProblemDetailsFactory detailsFactory, int statusCode, 56 | HttpContext context, 57 | string? message = null, 58 | IEnumerable? errors = null) 59 | { 60 | if (errors != null && errors.Any()) 61 | { 62 | StringBuilder errorList = new StringBuilder(); 63 | errorList.AppendJoin(",", errors); 64 | 65 | return detailsFactory.CreateProblemDetails(context, statusCode: statusCode, detail: errorList.ToString()); 66 | } 67 | else 68 | return detailsFactory.CreateProblemDetails(context, statusCode: statusCode, detail: message); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Events/Dispatchers/DomainEventDispatcher.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using VOEConsulting.Flame.Common.Domain; 4 | using VOEConsulting.Flame.Common.Domain.Events; 5 | 6 | namespace VOEConsulting.Flame.BasketContext.Application.Events.Dispatchers 7 | { 8 | public class DomainEventDispatcherWithoutMediatr : IDomainEventDispatcher 9 | { 10 | private readonly IServiceProvider _serviceProvider; 11 | 12 | public DomainEventDispatcherWithoutMediatr(IServiceProvider serviceProvider) 13 | { 14 | _serviceProvider = serviceProvider; 15 | } 16 | 17 | public async Task DispatchAsync(IEnumerable events, CancellationToken cancellationToken = default) 18 | { 19 | var eventQueue = new Queue(events); 20 | 21 | while (eventQueue.Count > 0) 22 | { 23 | var domainEvent = eventQueue.Dequeue(); 24 | var handlerType = typeof(IDomainEventHandler<>).MakeGenericType(domainEvent.GetType()); 25 | var handlers = _serviceProvider.GetServices(handlerType); 26 | 27 | foreach (var handler in handlers) 28 | { 29 | var handleMethod = handlerType.GetMethod("Handle"); 30 | if (handleMethod == null) continue; 31 | 32 | await (Task)handleMethod.Invoke(handler, new object[] { domainEvent, cancellationToken }); 33 | 34 | // If the handler raises additional events, add them to the queue 35 | if (domainEvent is IAggregateRoot aggregateRoot) 36 | { 37 | var additionalEvents = aggregateRoot.PopDomainEvents(); 38 | foreach (var additionalEvent in additionalEvents) 39 | { 40 | eventQueue.Enqueue(additionalEvent); 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | } 48 | 49 | //default Dispatcher 50 | public class DomainEventDispatcher : IDomainEventDispatcher 51 | { 52 | private readonly IMediator _mediator; 53 | 54 | public DomainEventDispatcher(IMediator mediator) 55 | { 56 | _mediator = mediator; 57 | } 58 | 59 | public async Task DispatchAsync(IEnumerable initialEvents, CancellationToken cancellationToken = default) 60 | { 61 | var eventQueue = new Queue(initialEvents); 62 | 63 | while (eventQueue.Count > 0) 64 | { 65 | var currentEvent = eventQueue.Dequeue(); 66 | 67 | // Publish the current event 68 | await _mediator.Publish(currentEvent, cancellationToken); 69 | 70 | // If the current event is associated with an aggregate, check for new events 71 | if (currentEvent is IAggregateRoot aggregateRoot) 72 | { 73 | var additionalEvents = aggregateRoot.PopDomainEvents(); 74 | foreach (var additionalEvent in additionalEvents) 75 | { 76 | eventQueue.Enqueue(additionalEvent); 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/VOEConsulting.Flame.BasketContext.Tests.Unit/CouponTests.cs: -------------------------------------------------------------------------------- 1 | using NSubstitute; 2 | using VOEConsulting.Flame.BasketContext.Domain.Coupons; 3 | using VOEConsulting.Flame.BasketContext.Domain.Coupons.Events; 4 | using VOEConsulting.Flame.BasketContext.Tests.Unit.Extensions; 5 | using VOEConsulting.Flame.Common.Domain; 6 | using VOEConsulting.Flame.Common.Domain.Exceptions; 7 | using VOEConsulting.Flame.Common.Domain.Services; 8 | 9 | namespace VOEConsulting.Flame.BasketContext.Tests.Unit 10 | { 11 | public class CouponTests 12 | { 13 | [Theory] 14 | [InlineData("TESTCOUPON", 20, "2024-10-10", "2024-11-11")] 15 | public void Create_WhenValidCouponDataProvided_ShouldRaiseCouponCreatedEvent( 16 | string code, decimal value, string startDate, string endDate) 17 | { 18 | // Arrange&Act 19 | var coupon = Coupon.Create(code, Amount.Percentage(value), DateRange.FromString(startDate, endDate)); 20 | var expectedEvent = new CouponCreatedEvent(coupon.Id); 21 | 22 | // Assert 23 | var actualEvent = coupon.DomainEvents.Single(); 24 | actualEvent.Should().BeEquivalentEventTo(expectedEvent); 25 | } 26 | 27 | [Theory] 28 | [InlineData("TESTCOUPON", 20, "2024-10-10", "2024-11-11")] 29 | [InlineData("TESTCPN", 56, "2022-10-10", "2023-06-11")] 30 | public void Create_WhenValidCouponDataProvided_ShouldReturnCouponWithCorrectProperties( 31 | string code, decimal value, string startDate, string endDate) 32 | { 33 | //Arrange 34 | 35 | Amount amount = Amount.Percentage(value); 36 | DateRange dateRange = DateRange.FromString(startDate, endDate); 37 | 38 | // Act 39 | var coupon = Coupon.Create(code, amount, dateRange); 40 | 41 | // Assert 42 | coupon.Code.Should().Be(code); 43 | coupon.Amount.Should().Be(amount); 44 | coupon.ValidityPeriod.Should().Be(dateRange); 45 | coupon.IsActive.Should().BeTrue(); 46 | } 47 | 48 | [Theory] 49 | [InlineData("TST", 20, "2024-10-10", "2024-11-11")] 50 | [InlineData("TEST", 56, "2022-10-10", "2023-06-11")] 51 | public void Create_WhenInValidCouponCodeProvided_ShouldFail( 52 | string code, decimal value, string startDate, string endDate) 53 | { 54 | //Arrange 55 | Amount amount = Amount.Percentage(value); 56 | DateRange dateRange = DateRange.FromString(startDate, endDate); 57 | 58 | // Act 59 | var action = () => Coupon.Create(code, amount, dateRange); 60 | 61 | // Assert 62 | action.Should().Throw(); 63 | } 64 | 65 | [Fact] 66 | public void Deactivate_WhenCouponIsActive_ShouldRaiseCouponDeactivatedEvent() 67 | { 68 | // Arrange 69 | var coupon = TestFactories.CouponFactory.Create(); 70 | 71 | var expectedEvent = new CouponDeactivatedEvent(coupon.Id); 72 | 73 | // Act 74 | coupon.Deactivate(); 75 | 76 | // Assert 77 | coupon.DomainEvents.Last().Should().BeEquivalentEventTo(expectedEvent); 78 | } 79 | 80 | [Fact] 81 | public void Deactivate_WhenCouponIsAlreadyDeactive_ShouldFail() 82 | { 83 | // Arrange 84 | var coupon = TestFactories.CouponFactory.Create(); 85 | coupon.Deactivate(); 86 | 87 | // Act 88 | var action = () => coupon.Deactivate(); 89 | 90 | // Assert 91 | action.Should().ThrowExactly(); 92 | } 93 | 94 | [Theory] 95 | [InlineData("2024-02-01", "2024-11-11", "2024-08-11")] 96 | public void Activate_WhenCouponIsNotActiveAndDateIsInRange_ShouldRaiseCouponActivatedEvent( 97 | string startDate, string endDate, string couponActivateDate) 98 | { 99 | // Arrange 100 | var validDateRange = DateRange.FromString(startDate, endDate); 101 | var coupon = TestFactories.CouponFactory.Create(dateRange: validDateRange); 102 | var dateTimeProvider = Substitute.For(); 103 | dateTimeProvider.UtcNow().Returns(DateTime.Parse(couponActivateDate)); 104 | 105 | var expectedEvent = new CouponActivatedEvent(coupon.Id); 106 | 107 | coupon.Deactivate(); 108 | 109 | // Act 110 | coupon.Activate(dateTimeProvider); 111 | 112 | // Assert 113 | coupon.DomainEvents.Last().Should().BeEquivalentEventTo(expectedEvent); 114 | } 115 | 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Application/Abstractions/Messaging/IIntegrationEventPublisher.cs: -------------------------------------------------------------------------------- 1 | using Confluent.Kafka; 2 | using System.Text.Json; 3 | using VOEConsulting.Flame.BasketContext.Application.Events.Integration; 4 | using VOEConsulting.Flame.Common.Core.Events; 5 | 6 | namespace VOEConsulting.Flame.BasketContext.Application.Abstractions.Messaging 7 | { 8 | public interface IIntegrationEventPublisher 9 | { 10 | Task PublishAsync(T @event) where T : IntegrationEvent; 11 | } 12 | 13 | public interface IIntegrationEventHandler where TEvent : IntegrationEvent 14 | { 15 | Task HandleAsync(TEvent @event, CancellationToken cancellationToken); 16 | } 17 | 18 | public interface IIntegrationEventConsumer 19 | { 20 | Task ConsumeAsync(CancellationToken cancellationToken); 21 | } 22 | 23 | public class BasketValidatedHandler : IIntegrationEventHandler 24 | { 25 | public Task HandleAsync(BasketCreatedIntegrationEvent @event, CancellationToken cancellationToken) 26 | { 27 | throw new NotImplementedException(); 28 | } 29 | } 30 | 31 | public class BasketValidationFailedHandler : IIntegrationEventHandler 32 | { 33 | public async Task HandleAsync(BasketCreatedIntegrationEvent @event, CancellationToken cancellationToken) 34 | { 35 | Console.WriteLine($"Basket validation failed: Error = {@event.EventType}"); 36 | await Task.CompletedTask; 37 | } 38 | } 39 | 40 | public class IntegrationEventConsumer : IIntegrationEventConsumer 41 | { 42 | private readonly IConsumer _consumer; 43 | private readonly Dictionary _handlers; // Maps event types to their handlers 44 | private readonly Dictionary _eventTypeMappings; // Maps topics to event types 45 | 46 | public IntegrationEventConsumer( 47 | IConsumer consumer, 48 | Dictionary handlers, 49 | Dictionary eventTypeMappings) 50 | { 51 | _consumer = consumer ?? throw new ArgumentNullException(nameof(consumer)); 52 | _handlers = handlers ?? throw new ArgumentNullException(nameof(handlers)); 53 | _eventTypeMappings = eventTypeMappings ?? throw new ArgumentNullException(nameof(eventTypeMappings)); 54 | } 55 | 56 | public async Task ConsumeAsync(CancellationToken cancellationToken) 57 | { 58 | try 59 | { 60 | while (!cancellationToken.IsCancellationRequested) 61 | { 62 | // Consume message 63 | var consumeResult = _consumer.Consume(cancellationToken); 64 | 65 | // Get the event type from the topic 66 | if (_eventTypeMappings.TryGetValue(consumeResult.Topic, out var eventType)) 67 | { 68 | // Deserialize the message into the event type 69 | var integrationEvent = JsonSerializer.Deserialize(consumeResult.Message.Value, eventType); 70 | 71 | if (integrationEvent != null) 72 | { 73 | // Dispatch the event to the appropriate handler 74 | await DispatchEventAsync(integrationEvent, cancellationToken); 75 | } 76 | } 77 | else 78 | { 79 | Console.WriteLine($"No event type mapping found for topic: {consumeResult.Topic}"); 80 | } 81 | } 82 | } 83 | catch (OperationCanceledException) 84 | { 85 | // Graceful shutdown 86 | } 87 | catch (Exception ex) 88 | { 89 | Console.WriteLine($"Error during message consumption: {ex.Message}"); 90 | } 91 | finally 92 | { 93 | _consumer.Close(); 94 | } 95 | } 96 | 97 | private async Task DispatchEventAsync(object integrationEvent, CancellationToken cancellationToken) 98 | { 99 | var eventType = integrationEvent.GetType(); 100 | 101 | // Find the handler for the event type 102 | if (_handlers.TryGetValue(eventType, out var handler)) 103 | { 104 | if (handler is IIntegrationEventHandler typedHandler) 105 | { 106 | // Safely cast and invoke the handler 107 | await typedHandler.HandleAsync((dynamic)integrationEvent, cancellationToken); 108 | } 109 | else 110 | { 111 | Console.WriteLine($"Handler for {eventType.Name} does not implement the correct interface."); 112 | } 113 | } 114 | else 115 | { 116 | Console.WriteLine($"No handler found for event type: {eventType.Name}"); 117 | } 118 | } 119 | } 120 | 121 | 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/Persistence/Repositories/BasketRepository.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Microsoft.EntityFrameworkCore; 3 | using VOEConsulting.Flame.BasketContext.Application.Repositories; 4 | using VOEConsulting.Flame.BasketContext.Domain.Baskets; 5 | using VOEConsulting.Flame.BasketContext.Infrastructure.Entities; 6 | using VOEConsulting.Flame.Common.Core.Exceptions; 7 | using VOEConsulting.Infrastructure.Persistence; 8 | 9 | namespace VOEConsulting.Flame.BasketContext.Infrastructure.Persistence.Repositories 10 | { 11 | public class BasketRepository : IBasketRepository 12 | { 13 | private readonly BasketAppDbContext _dbContext; 14 | private readonly IMapper _mapper; 15 | public BasketRepository(BasketAppDbContext dbContext, IMapper mapper) 16 | { 17 | _dbContext = dbContext; 18 | _mapper = mapper; 19 | } 20 | 21 | public async Task AddAsync(Basket basket, CancellationToken cancellationToken) 22 | { 23 | var basketEntity = _mapper.Map(basket); 24 | await _dbContext.AddAsync(basketEntity); 25 | } 26 | 27 | public async Task RemoveBasketItemAsync(Guid basketId, Guid basketItemId) 28 | { 29 | // Load the basket 30 | var basket = await GetByBasketEntityIdAsync(basketId); 31 | 32 | // Find the basket item to be removed 33 | var basketItem = basket.BasketItems 34 | .FirstOrDefault(bi => bi.Id == basketItemId) 35 | ?? throw new FlameApplicationException("Basket item not found."); 36 | 37 | // Get the associated seller 38 | var seller = basketItem.Seller; 39 | 40 | // Remove the basket item 41 | _dbContext.BasketItems.Remove(basketItem); 42 | 43 | // Check if any other basket items reference this seller 44 | bool isSellerReferenced = _dbContext.BasketItems 45 | .Any(bi => bi.Seller.Id == seller.Id && bi.Id != basketItemId); 46 | 47 | if (!isSellerReferenced) 48 | { 49 | // Delete the seller if no other references exist 50 | _dbContext.Sellers.Remove(seller); 51 | } 52 | } 53 | 54 | public async Task AddBasketItemAsync(Guid basketId, BasketItem basketItem) 55 | { 56 | var basketItemEntity = _mapper.Map(basketItem); 57 | basketItemEntity.BasketId = basketId; 58 | // Load the basket 59 | var basketEntity = await GetByBasketEntityIdAsync(basketId); 60 | 61 | if (basketEntity is null) 62 | throw new FlameApplicationException("Basket is doesn't exist"); 63 | 64 | var existingBasketItem = basketEntity.BasketItems.FirstOrDefault(x => x.Name == basketItem.Name); 65 | 66 | if (existingBasketItem is not null) 67 | throw new FlameApplicationException("This basket item already exists"); 68 | 69 | // Check if the seller already exists in the database 70 | var existingSeller = await _dbContext.Sellers 71 | .FirstOrDefaultAsync(s => s.Name == basketItem.Seller.Name); 72 | 73 | // Reuse existing seller or associate the new one 74 | if (existingSeller is not null) 75 | { 76 | // Reuse the existing seller 77 | basketItemEntity.SellerId = existingSeller.Id; 78 | basketItemEntity.Seller = null; // Avoid attaching the Seller again 79 | } 80 | else 81 | { 82 | // Use the new seller 83 | basketItemEntity.SellerId = basketItemEntity.Seller.Id; 84 | _dbContext.Sellers.Attach(basketItemEntity.Seller); // Attach the new Seller 85 | } 86 | 87 | // Add the basket item to the basket 88 | 89 | await _dbContext.BasketItems.AddAsync(basketItemEntity); 90 | 91 | } 92 | 93 | public Task DeleteAsync(Guid id) 94 | { 95 | return Task.FromResult(_dbContext.Remove(id)); 96 | } 97 | 98 | public Task> GetAllAsync() 99 | { 100 | throw new NotImplementedException(); 101 | } 102 | 103 | public async Task GetByIdAsync(Guid id) 104 | { 105 | var basketEntity = await _dbContext.Baskets 106 | .Include(b => b.Customer) 107 | .Include(b => b.Coupon) 108 | .Include(b => b.BasketItems) 109 | .ThenInclude(bi => bi.Seller) 110 | .Where(b => b.Id == id) 111 | .FirstOrDefaultAsync(); 112 | 113 | return _mapper.Map(basketEntity); 114 | } 115 | 116 | private async Task GetByBasketEntityIdAsync(Guid basketId) 117 | { 118 | // Retrieve the basket including its items and associated sellers 119 | return await _dbContext.Baskets 120 | .Include(b => b.BasketItems) 121 | .ThenInclude(bi => bi.Seller) 122 | .FirstOrDefaultAsync(b => b.Id == basketId) 123 | ?? throw new InvalidOperationException("Basket not found."); 124 | } 125 | 126 | public Task UpdateAsync(Basket entity) 127 | { 128 | return Task.FromResult(_dbContext.Update(entity)); 129 | } 130 | 131 | public async Task IsExistsAsync(Guid id) 132 | { 133 | return (await _dbContext.Baskets.FirstOrDefaultAsync(x => x.Id == id)) != null; 134 | } 135 | 136 | public async Task IsExistByCustomerIdAsync(Guid customerId) 137 | { 138 | return (await _dbContext.Baskets.FirstOrDefaultAsync(x => x.CustomerId == customerId)) != null; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/Migrations/20241223104415_Initial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace VOEConsulting.Flame.BasketContext.Infrastructure.Migrations 7 | { 8 | /// 9 | public partial class Initial : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.CreateTable( 15 | name: "Coupons", 16 | columns: table => new 17 | { 18 | Id = table.Column(type: "uniqueidentifier", nullable: false), 19 | Code = table.Column(type: "nvarchar(10)", maxLength: 10, nullable: false), 20 | IsActive = table.Column(type: "bit", nullable: false), 21 | Value = table.Column(type: "decimal(18,2)", nullable: false), 22 | CouponType = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false), 23 | StartDate = table.Column(type: "datetime2", nullable: false), 24 | EndDate = table.Column(type: "datetime2", nullable: false) 25 | }, 26 | constraints: table => 27 | { 28 | table.PrimaryKey("PK_Coupons", x => x.Id); 29 | }); 30 | 31 | migrationBuilder.CreateTable( 32 | name: "Customers", 33 | columns: table => new 34 | { 35 | Id = table.Column(type: "uniqueidentifier", nullable: false), 36 | IsEliteMember = table.Column(type: "bit", nullable: false) 37 | }, 38 | constraints: table => 39 | { 40 | table.PrimaryKey("PK_Customers", x => x.Id); 41 | }); 42 | 43 | migrationBuilder.CreateTable( 44 | name: "Sellers", 45 | columns: table => new 46 | { 47 | Id = table.Column(type: "uniqueidentifier", nullable: false), 48 | Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), 49 | Rating = table.Column(type: "real", nullable: false), 50 | ShippingLimit = table.Column(type: "decimal(18,2)", nullable: false), 51 | ShippingCost = table.Column(type: "decimal(18,2)", nullable: false) 52 | }, 53 | constraints: table => 54 | { 55 | table.PrimaryKey("PK_Sellers", x => x.Id); 56 | }); 57 | 58 | migrationBuilder.CreateTable( 59 | name: "Baskets", 60 | columns: table => new 61 | { 62 | Id = table.Column(type: "uniqueidentifier", nullable: false), 63 | TaxPercentage = table.Column(type: "decimal(18,2)", nullable: false), 64 | TotalAmount = table.Column(type: "decimal(18,2)", nullable: false, defaultValue: 0m), 65 | CustomerId = table.Column(type: "uniqueidentifier", nullable: false), 66 | CouponId = table.Column(type: "uniqueidentifier", nullable: true) 67 | }, 68 | constraints: table => 69 | { 70 | table.PrimaryKey("PK_Baskets", x => x.Id); 71 | table.ForeignKey( 72 | name: "FK_Baskets_Coupons_CouponId", 73 | column: x => x.CouponId, 74 | principalTable: "Coupons", 75 | principalColumn: "Id", 76 | onDelete: ReferentialAction.SetNull); 77 | table.ForeignKey( 78 | name: "FK_Baskets_Customers_CustomerId", 79 | column: x => x.CustomerId, 80 | principalTable: "Customers", 81 | principalColumn: "Id", 82 | onDelete: ReferentialAction.Cascade); 83 | }); 84 | 85 | migrationBuilder.CreateTable( 86 | name: "BasketItems", 87 | columns: table => new 88 | { 89 | Id = table.Column(type: "uniqueidentifier", nullable: false), 90 | Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), 91 | ImageUrl = table.Column(type: "nvarchar(max)", nullable: false), 92 | QuantityValue = table.Column(type: "int", nullable: false), 93 | QuantityLimit = table.Column(type: "int", nullable: false), 94 | PricePerUnit = table.Column(type: "decimal(18,2)", nullable: false), 95 | SellerId = table.Column(type: "uniqueidentifier", nullable: false), 96 | BasketId = table.Column(type: "uniqueidentifier", nullable: false) 97 | }, 98 | constraints: table => 99 | { 100 | table.PrimaryKey("PK_BasketItems", x => x.Id); 101 | table.ForeignKey( 102 | name: "FK_BasketItems_Baskets_BasketId", 103 | column: x => x.BasketId, 104 | principalTable: "Baskets", 105 | principalColumn: "Id", 106 | onDelete: ReferentialAction.Cascade); 107 | table.ForeignKey( 108 | name: "FK_BasketItems_Sellers_SellerId", 109 | column: x => x.SellerId, 110 | principalTable: "Sellers", 111 | principalColumn: "Id", 112 | onDelete: ReferentialAction.Cascade); 113 | }); 114 | 115 | migrationBuilder.CreateIndex( 116 | name: "IX_BasketItems_BasketId", 117 | table: "BasketItems", 118 | column: "BasketId"); 119 | 120 | migrationBuilder.CreateIndex( 121 | name: "IX_BasketItems_SellerId", 122 | table: "BasketItems", 123 | column: "SellerId"); 124 | 125 | migrationBuilder.CreateIndex( 126 | name: "IX_Baskets_CouponId", 127 | table: "Baskets", 128 | column: "CouponId"); 129 | 130 | migrationBuilder.CreateIndex( 131 | name: "IX_Baskets_CustomerId", 132 | table: "Baskets", 133 | column: "CustomerId"); 134 | } 135 | 136 | /// 137 | protected override void Down(MigrationBuilder migrationBuilder) 138 | { 139 | migrationBuilder.DropTable( 140 | name: "BasketItems"); 141 | 142 | migrationBuilder.DropTable( 143 | name: "Baskets"); 144 | 145 | migrationBuilder.DropTable( 146 | name: "Sellers"); 147 | 148 | migrationBuilder.DropTable( 149 | name: "Coupons"); 150 | 151 | migrationBuilder.DropTable( 152 | name: "Customers"); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /FlameBasketApp.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.10.35122.118 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0CB68504-0F37-431E-B512-6E8B230B776F}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4746A963-03C3-4CC0-87A6-DF0618E058BA}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VOEConsulting.Flame.BasketContext.Domain", "src\VOEConsulting.Flame.BasketContext.Domain\VOEConsulting.Flame.BasketContext.Domain.csproj", "{31E2D5D7-8A50-4D35-AD25-3825DAA17254}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VOEConsulting.Flame.BasketContext.Tests.Unit", "tests\VOEConsulting.Flame.BasketContext.Tests.Unit\VOEConsulting.Flame.BasketContext.Tests.Unit.csproj", "{AA56B415-5D2D-475D-896E-10A5F1BCB2FC}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VOEConsulting.Flame.BasketContext.Tests.Data", "tests\VOEConsulting.Flame.BasketContext.Tests.Data\VOEConsulting.Flame.BasketContext.Tests.Data.csproj", "{24B49C84-052C-41C4-A844-9F9D6A53F8E9}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VOEConsulting.Flame.Common.Domain", "src\VOEConsulting.Flame.Common.Domain\VOEConsulting.Flame.Common.Domain.csproj", "{E1A68B49-C432-47C0-B633-A2EE9BE6F6FF}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VOEConsulting.Flame.BasketContext.Api", "src\VOEConsulting.Flame.BasketContext.Api\VOEConsulting.Flame.BasketContext.Api.csproj", "{6B876145-A825-44DB-999C-0B77E6F6A1C9}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VOEConsulting.Flame.BasketContext.Application", "src\VOEConsulting.Flame.BasketContext.Application\VOEConsulting.Flame.BasketContext.Application.csproj", "{5EFA0332-E30C-4B4A-949E-4CECA6665B22}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VOEConsulting.Flame.BasketContext.Infrastructure", "src\VOEConsulting.Flame.BasketContext.Infrastructure\VOEConsulting.Flame.BasketContext.Infrastructure.csproj", "{4B8B53C8-4C5B-4A6A-AE96-4A9810413E28}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VOEConsulting.Flame.Common.Core", "src\VOEConsulting.Flame.Common.Core\VOEConsulting.Flame.Common.Core.csproj", "{CD8C4376-E775-45DC-829B-F656E5F2C79D}" 25 | EndProject 26 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{810F474E-A080-4A89-892A-C85D6C8FDC57}" 27 | ProjectSection(SolutionItems) = preProject 28 | README.md = README.md 29 | EndProjectSection 30 | EndProject 31 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "images", "images", "{D6480F6B-2CD9-47D1-A5A3-790C21B13396}" 32 | ProjectSection(SolutionItems) = preProject 33 | images\me_and_mads_torgersen.gif = images\me_and_mads_torgersen.gif 34 | images\me_and_mark_seemann.gif = images\me_and_mark_seemann.gif 35 | images\me_and_rebecca_wirfs_brock.gif = images\me_and_rebecca_wirfs_brock.gif 36 | images\me_and_robert_c_martin.gif = images\me_and_robert_c_martin.gif 37 | images\me_on_domain-driven-design.gif = images\me_on_domain-driven-design.gif 38 | images\me_on_functional_programming.gif = images\me_on_functional_programming.gif 39 | images\me_on_git_project.gif = images\me_on_git_project.gif 40 | images\me_on_result_pattern.gif = images\me_on_result_pattern.gif 41 | ..\..\Downloads\System Boundries.excalidraw = ..\..\Downloads\System Boundries.excalidraw 42 | ..\..\Downloads\SystemBoundriesBasketApi.png = ..\..\Downloads\SystemBoundriesBasketApi.png 43 | EndProjectSection 44 | EndProject 45 | Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{893C5B51-A811-4393-BDEA-8C7AD9427918}" 46 | EndProject 47 | Global 48 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 49 | Debug|Any CPU = Debug|Any CPU 50 | Release|Any CPU = Release|Any CPU 51 | EndGlobalSection 52 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 53 | {31E2D5D7-8A50-4D35-AD25-3825DAA17254}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {31E2D5D7-8A50-4D35-AD25-3825DAA17254}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {31E2D5D7-8A50-4D35-AD25-3825DAA17254}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {31E2D5D7-8A50-4D35-AD25-3825DAA17254}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {AA56B415-5D2D-475D-896E-10A5F1BCB2FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {AA56B415-5D2D-475D-896E-10A5F1BCB2FC}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {AA56B415-5D2D-475D-896E-10A5F1BCB2FC}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {AA56B415-5D2D-475D-896E-10A5F1BCB2FC}.Release|Any CPU.Build.0 = Release|Any CPU 61 | {24B49C84-052C-41C4-A844-9F9D6A53F8E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 62 | {24B49C84-052C-41C4-A844-9F9D6A53F8E9}.Debug|Any CPU.Build.0 = Debug|Any CPU 63 | {24B49C84-052C-41C4-A844-9F9D6A53F8E9}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {24B49C84-052C-41C4-A844-9F9D6A53F8E9}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {E1A68B49-C432-47C0-B633-A2EE9BE6F6FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {E1A68B49-C432-47C0-B633-A2EE9BE6F6FF}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {E1A68B49-C432-47C0-B633-A2EE9BE6F6FF}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {E1A68B49-C432-47C0-B633-A2EE9BE6F6FF}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {6B876145-A825-44DB-999C-0B77E6F6A1C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 70 | {6B876145-A825-44DB-999C-0B77E6F6A1C9}.Debug|Any CPU.Build.0 = Debug|Any CPU 71 | {6B876145-A825-44DB-999C-0B77E6F6A1C9}.Release|Any CPU.ActiveCfg = Release|Any CPU 72 | {6B876145-A825-44DB-999C-0B77E6F6A1C9}.Release|Any CPU.Build.0 = Release|Any CPU 73 | {5EFA0332-E30C-4B4A-949E-4CECA6665B22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 74 | {5EFA0332-E30C-4B4A-949E-4CECA6665B22}.Debug|Any CPU.Build.0 = Debug|Any CPU 75 | {5EFA0332-E30C-4B4A-949E-4CECA6665B22}.Release|Any CPU.ActiveCfg = Release|Any CPU 76 | {5EFA0332-E30C-4B4A-949E-4CECA6665B22}.Release|Any CPU.Build.0 = Release|Any CPU 77 | {4B8B53C8-4C5B-4A6A-AE96-4A9810413E28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 78 | {4B8B53C8-4C5B-4A6A-AE96-4A9810413E28}.Debug|Any CPU.Build.0 = Debug|Any CPU 79 | {4B8B53C8-4C5B-4A6A-AE96-4A9810413E28}.Release|Any CPU.ActiveCfg = Release|Any CPU 80 | {4B8B53C8-4C5B-4A6A-AE96-4A9810413E28}.Release|Any CPU.Build.0 = Release|Any CPU 81 | {CD8C4376-E775-45DC-829B-F656E5F2C79D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 82 | {CD8C4376-E775-45DC-829B-F656E5F2C79D}.Debug|Any CPU.Build.0 = Debug|Any CPU 83 | {CD8C4376-E775-45DC-829B-F656E5F2C79D}.Release|Any CPU.ActiveCfg = Release|Any CPU 84 | {CD8C4376-E775-45DC-829B-F656E5F2C79D}.Release|Any CPU.Build.0 = Release|Any CPU 85 | {893C5B51-A811-4393-BDEA-8C7AD9427918}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 86 | {893C5B51-A811-4393-BDEA-8C7AD9427918}.Debug|Any CPU.Build.0 = Debug|Any CPU 87 | {893C5B51-A811-4393-BDEA-8C7AD9427918}.Release|Any CPU.ActiveCfg = Release|Any CPU 88 | {893C5B51-A811-4393-BDEA-8C7AD9427918}.Release|Any CPU.Build.0 = Release|Any CPU 89 | EndGlobalSection 90 | GlobalSection(SolutionProperties) = preSolution 91 | HideSolutionNode = FALSE 92 | EndGlobalSection 93 | GlobalSection(NestedProjects) = preSolution 94 | {31E2D5D7-8A50-4D35-AD25-3825DAA17254} = {0CB68504-0F37-431E-B512-6E8B230B776F} 95 | {AA56B415-5D2D-475D-896E-10A5F1BCB2FC} = {4746A963-03C3-4CC0-87A6-DF0618E058BA} 96 | {24B49C84-052C-41C4-A844-9F9D6A53F8E9} = {4746A963-03C3-4CC0-87A6-DF0618E058BA} 97 | {E1A68B49-C432-47C0-B633-A2EE9BE6F6FF} = {0CB68504-0F37-431E-B512-6E8B230B776F} 98 | {6B876145-A825-44DB-999C-0B77E6F6A1C9} = {0CB68504-0F37-431E-B512-6E8B230B776F} 99 | {5EFA0332-E30C-4B4A-949E-4CECA6665B22} = {0CB68504-0F37-431E-B512-6E8B230B776F} 100 | {4B8B53C8-4C5B-4A6A-AE96-4A9810413E28} = {0CB68504-0F37-431E-B512-6E8B230B776F} 101 | {CD8C4376-E775-45DC-829B-F656E5F2C79D} = {0CB68504-0F37-431E-B512-6E8B230B776F} 102 | {D6480F6B-2CD9-47D1-A5A3-790C21B13396} = {810F474E-A080-4A89-892A-C85D6C8FDC57} 103 | EndGlobalSection 104 | GlobalSection(ExtensibilityGlobals) = postSolution 105 | SolutionGuid = {266CDB49-A113-4342-9A1C-DFCFFD67C743} 106 | EndGlobalSection 107 | EndGlobal 108 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.Common.Domain/Extensions/ValidationExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Runtime.CompilerServices; 4 | using System.Text.RegularExpressions; 5 | using ValidationExp = VOEConsulting.Flame.Common.Domain.Exceptions; 6 | 7 | namespace VOEConsulting.Flame.Common.Domain.Extensions 8 | { 9 | public static class Validators 10 | { 11 | #region Null and Default Checks 12 | 13 | private static ValidationExp.ValidationException Invalid(string message) => new ValidationExp.ValidationException(message); 14 | 15 | public static T? EnsureNull(this T? model, [CallerArgumentExpression("model")] string name = "") 16 | => model == null ? model : throw Invalid($"{name} must be null."); 17 | 18 | public static T EnsureNonNull(this T? model, [CallerArgumentExpression("model")] string name = "") 19 | => model ?? throw Invalid($"{name} cannot be null."); 20 | 21 | public static T EnsureNotDefault(this T model, [CallerArgumentExpression("model")] string name = "") where T : struct 22 | { 23 | if (model.Equals(default(T))) 24 | throw Invalid($"{name} cannot be null or default."); 25 | return model; 26 | } 27 | 28 | public static T EnsureNotDefault(this T? model, [CallerArgumentExpression("model")] string name = "") where T : struct 29 | { 30 | var value = model.GetValueOrDefault(); 31 | if (value.Equals(default(T))) 32 | throw Invalid($"{name} cannot be null or default."); 33 | return value; 34 | } 35 | 36 | #endregion 37 | 38 | #region Numeric Checks 39 | 40 | public static T EnsureNonZero(this T model, [CallerArgumentExpression("model")] string name = "") where T : struct 41 | => Convert.ToDecimal(model) != 0 ? model : throw Invalid($"{name} cannot be zero."); 42 | 43 | public static T EnsurePositive(this T model, [CallerArgumentExpression("model")] string name = "") where T : struct 44 | => model.EnsureNonZero(name).EnsureNonNegative(name); 45 | 46 | public static T EnsureNonNegative(this T model, [CallerArgumentExpression("model")] string name = "") where T : struct 47 | => Convert.ToDecimal(model) >= 0 ? model : throw Invalid($"{name} cannot be negative."); 48 | 49 | public static T EnsureGreaterThan(this T model, T min, [CallerArgumentExpression("model")] string modelExpression = "", [CallerArgumentExpression("min")] string minExpression = "") where T : struct, IComparable 50 | => model.CompareTo(min) > 0 ? model : throw Invalid($"{modelExpression}={model} must be greater than {minExpression}={min}."); 51 | 52 | public static T EnsureAtLeast(this T model, T min, [CallerArgumentExpression("model")] string modelExpression = "", [CallerArgumentExpression("min")] string minExpression = "") where T : struct, IComparable 53 | => model.CompareTo(min) >= 0 ? model : throw Invalid($"{modelExpression}={model} must be greater than or equal to {minExpression}={min}."); 54 | 55 | public static T EnsureWithinRange(this T model, T min, T max, bool excludeMin = false, bool excludeMax = false, [CallerArgumentExpression("model")] string modelExpression = "", [CallerArgumentExpression("min")] string minExpression = "", [CallerArgumentExpression("max")] string maxExpression = "") where T : struct, IComparable 56 | { 57 | bool tooLow = excludeMin ? model.CompareTo(min) <= 0 : model.CompareTo(min) < 0; 58 | bool tooHigh = excludeMax ? model.CompareTo(max) >= 0 : model.CompareTo(max) > 0; 59 | 60 | if (tooLow || tooHigh) 61 | throw Invalid($"{modelExpression}={model} must be between {minExpression}={min} and {maxExpression}={max}."); 62 | 63 | return model; 64 | } 65 | 66 | #endregion 67 | 68 | #region String Checks 69 | 70 | public static string EnsureNonEmpty(this string? model, [CallerArgumentExpression("model")] string name = "") 71 | => !string.IsNullOrEmpty(model) ? model : throw Invalid($"{name} cannot be empty."); 72 | 73 | public static string EnsureNonBlank(this string? model, [CallerArgumentExpression("model")] string name = "") 74 | => !string.IsNullOrWhiteSpace(model) ? model : throw Invalid($"{name} cannot be blank."); 75 | 76 | public static string EnsureMatchesPattern(this string model, string pattern, [CallerArgumentExpression("model")] string name = "") 77 | => Regex.IsMatch(model, pattern) ? model : throw Invalid($"{name} does not match the required pattern."); 78 | 79 | public static string EnsureImageUrl(this string model, [CallerArgumentExpression("model")] string name = "") 80 | => Regex.IsMatch(model, "^https?:\\/\\/.*\\/.*\\.(png|gif|webp|jpeg|jpg)\\??.*$") ? model : throw Invalid($"{name} is not a valid image url."); 81 | 82 | public static string EnsureValidEmail(this string email, [CallerArgumentExpression("email")] string name = "") 83 | => new EmailAddressAttribute().IsValid(email) ? email : throw Invalid($"{email} is not a valid email address."); 84 | 85 | public static string EnsureExactLength(this string model, int length, [CallerArgumentExpression("model")] string name = "") 86 | => model.Length == length ? model : throw Invalid($"{name} must have exactly {length} characters, but it has {model.Length}."); 87 | 88 | public static string EnsureLengthInRange(this string model, int minLength, int maxLength, [CallerArgumentExpression("model")] string name = "") 89 | { 90 | if (model.Length < minLength || model.Length > maxLength) 91 | throw Invalid($"{name} length must be between {minLength} and {maxLength} characters, but it has {model.Length}."); 92 | return model; 93 | } 94 | 95 | #endregion 96 | 97 | #region Collection Checks 98 | 99 | public static T EnsureNonEmpty(this T? collection, [CallerArgumentExpression("collection")] string name = "") where T : ICollection 100 | => collection?.Count > 0 ? collection : throw Invalid($"{name} cannot be empty."); 101 | 102 | #endregion 103 | 104 | #region Enum Checks 105 | 106 | public static T EnsureEnumValueDefined(this T model, [CallerArgumentExpression("model")] string name = "") where T : Enum 107 | => Enum.IsDefined(typeof(T), model) ? model : throw Invalid($"{name} is not a valid {typeof(T).Name} value."); 108 | 109 | public static T EnsureEnumValueDefined(this int model, [CallerArgumentExpression("model")] string name = "") where T : Enum 110 | => Enum.IsDefined(typeof(T), model) ? (T)Enum.ToObject(typeof(T), model) : throw Invalid($"{name}={model} is not a valid {typeof(T).Name} value."); 111 | 112 | public static T EnsureEnumValueDefined(this string model, [CallerArgumentExpression("model")] string name = "") where T : Enum 113 | => Enum.IsDefined(typeof(T), model) ? (T)Enum.Parse(typeof(T), model) : throw Invalid($"{name}={model} is not a valid {typeof(T).Name} value."); 114 | 115 | #endregion 116 | 117 | #region Dictionary Checks 118 | public static TValue EnsureKeyExists(this IDictionary model, TKey key, [CallerArgumentExpression("model")] string name = "") 119 | { 120 | model.EnsureNonNull(name); 121 | key.EnsureNonNull(); 122 | 123 | if (!model.ContainsKey(key)) 124 | throw Invalid($"{key} does not exist inside {name} value."); 125 | return model[key]; 126 | } 127 | #endregion 128 | 129 | #region Boolean Checks 130 | public static bool? EnsureTrue(this bool model, [CallerArgumentExpression("model")] string name = "") 131 | => model == true ? model : throw Invalid($"{name} must be true."); 132 | 133 | public static bool? EnsureFalse(this bool model, [CallerArgumentExpression("model")] string name = "") 134 | => model == false ? model : throw Invalid($"{name} must be false."); 135 | #endregion 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Domain/Baskets/Basket.cs: -------------------------------------------------------------------------------- 1 | using VOEConsulting.Flame.BasketContext.Domain.Baskets.Events; 2 | using VOEConsulting.Flame.BasketContext.Domain.Baskets.Services; 3 | using VOEConsulting.Flame.BasketContext.Domain.Coupons; 4 | using VOEConsulting.Flame.Common.Domain.Exceptions; 5 | 6 | namespace VOEConsulting.Flame.BasketContext.Domain.Baskets 7 | { 8 | public sealed class Basket : AggregateRoot 9 | { 10 | public IDictionary Items, decimal ShippingAmountLeft)> BasketItems { get; private set; } 11 | public decimal TaxPercentage { get; } 12 | public decimal TotalAmount { get; private set; } 13 | public Customer Customer { get; private set; } 14 | public Id? CouponId { get; private set; } = null; 15 | 16 | private Basket(decimal taxPercentage, Customer customer) 17 | { 18 | BasketItems = new Dictionary, decimal)>(); 19 | TaxPercentage = taxPercentage.EnsurePositive(); 20 | TotalAmount = 0; 21 | Customer = customer; 22 | } 23 | 24 | public void AddItem(BasketItem basketItem) 25 | { 26 | if (BasketItems.TryGetValue(basketItem.Seller, out (IList Items, decimal ShippingAmountLeft) value)) 27 | { 28 | value.Items.Add(basketItem); 29 | } 30 | else 31 | BasketItems.Add(basketItem.Seller, (new List { basketItem }, basketItem.Seller.ShippingLimit)); 32 | 33 | RaiseDomainEvent(new BasketItemAddedEvent(this.Id, basketItem)); 34 | } 35 | 36 | public static Basket Create(decimal taxPercentage, Customer customer) 37 | { 38 | var basket = new Basket(taxPercentage, customer); 39 | //basket.RaiseDomainEvent(new BasketCreatedEvent(basket.Id, customer.Id)); 40 | return basket; 41 | } 42 | 43 | public void UpdateItemCount(BasketItem basketItem, int count) 44 | { 45 | BasketItems.EnsureKeyExists(basketItem.Seller); 46 | 47 | var existingBasketItem = BasketItems[basketItem.Seller].Items.FirstOrDefault(x => x.Id == basketItem.Id); 48 | 49 | existingBasketItem.EnsureNonNull(); 50 | 51 | existingBasketItem!.UpdateCount(count); 52 | 53 | RaiseDomainEvent(new BasketItemCountUpdatedEvent(this.Id, basketItem, count)); 54 | } 55 | 56 | public void DeleteItem(BasketItem basketItem) 57 | { 58 | BasketItems.EnsureKeyExists(basketItem.Seller); 59 | 60 | var items = BasketItems[basketItem.Seller].Items; 61 | items.EnsureNonNull(); 62 | 63 | items.Remove(basketItem); 64 | 65 | RaiseDomainEvent(new BasketItemDeletedEvent(this.Id, basketItem!)); 66 | } 67 | 68 | public void DeleteAll() 69 | { 70 | BasketItems.Clear(); 71 | RaiseDomainEvent(new BasketItemsDeletedEvent(this.Id)); 72 | } 73 | 74 | public void CalculateShippingAmount(Seller seller) 75 | { 76 | // Calculate the total amount 77 | decimal totalAmount = CalculateSellerAmount(seller); 78 | 79 | var (Items, ShippingAmountLeft) = BasketItems[seller]; 80 | 81 | // Determine and update shipping amount left 82 | ShippingAmountLeft = totalAmount > seller.ShippingLimit 83 | ? 0 // No shipping cost 84 | : ShippingAmountLeft - totalAmount; 85 | 86 | RaiseDomainEvent(new ShippingAmountCalculatedEvent(this.Id, seller, ShippingAmountLeft)); 87 | } 88 | 89 | public void CalculateBasketItemsAmount() 90 | { 91 | decimal totalBasketItemsAmount = 0; 92 | 93 | if (BasketItems.Count > 0) 94 | { 95 | foreach (var seller in BasketItems.Keys) 96 | { 97 | totalBasketItemsAmount += CalculateSellerAmount(seller); 98 | } 99 | } 100 | 101 | RaiseDomainEvent(new BasketItemsAmountCalculatedEvent(this.Id, totalBasketItemsAmount)); 102 | } 103 | 104 | public async Task CalculateTotalAmount(ICouponService couponService) 105 | { 106 | decimal totalAmount = CalculateTotalBasketAmount(); 107 | totalAmount = await ApplyCouponDiscount(totalAmount, couponService); 108 | totalAmount = ApplyEliteMemberDiscount(totalAmount); 109 | TotalAmount = ApplyTax(totalAmount); 110 | 111 | RaiseDomainEvent(new TotalAmountCalculatedEvent(this.Id, totalAmount)); 112 | } 113 | public void AssignCustomer(Customer customer) 114 | { 115 | Customer = customer; 116 | RaiseDomainEvent(new CustomerAssignedEvent(this.Id, customer)); 117 | } 118 | 119 | public void DeactivateBasketItem(BasketItem basketItem) 120 | { 121 | basketItem.IsActive.EnsureTrue(); 122 | 123 | BasketItems.EnsureKeyExists(basketItem.Seller); 124 | 125 | var items = BasketItems[basketItem.Seller].Items; 126 | items.EnsureNonNull(); 127 | 128 | var existingBasketItem = items.FirstOrDefault(x => x.Id == basketItem.Id); 129 | existingBasketItem.EnsureNonNull(); 130 | 131 | existingBasketItem!.Deactivate(); 132 | 133 | RaiseDomainEvent(new BasketItemDeactivatedEvent(this.Id, basketItem)); 134 | } 135 | 136 | public void ActivateBasketItem(BasketItem basketItem) 137 | { 138 | basketItem.IsActive.EnsureFalse(); 139 | 140 | BasketItems.EnsureKeyExists(basketItem.Seller); 141 | 142 | var items = BasketItems[basketItem.Seller].Items; 143 | items.EnsureNonNull(); 144 | 145 | var existingBasketItem = items.FirstOrDefault(x => x.Id == basketItem.Id); 146 | existingBasketItem.EnsureNonNull(); 147 | 148 | existingBasketItem!.Activate(); 149 | 150 | RaiseDomainEvent(new BasketItemActivatedEvent(this.Id, basketItem)); 151 | } 152 | 153 | public async Task ApplyCoupon(Id couponId, ICouponService couponService) 154 | { 155 | if (CouponId == couponId) 156 | return; // Already applied, no action needed. 157 | 158 | if (!await couponService.IsActive(couponId)) 159 | { 160 | throw new ValidationException("Coupon is not active!"); 161 | } 162 | 163 | CouponId = couponId; 164 | RaiseDomainEvent(new CouponAppliedEvent(this.Id, couponId)); 165 | } 166 | 167 | public void RemoveCoupon() 168 | { 169 | CouponId.EnsureNonNull(); 170 | var couponId = CouponId; 171 | CouponId = null; 172 | RaiseDomainEvent(new CouponRemovedEvent(this.Id, couponId!)); 173 | } 174 | private async Task ApplyCouponDiscount(decimal totalAmount, ICouponService couponService) 175 | { 176 | if (CouponId is not null) 177 | { 178 | return await couponService.ApplyDiscountAsync(CouponId, totalAmount); 179 | } 180 | 181 | return totalAmount; 182 | } 183 | 184 | private decimal ApplyEliteMemberDiscount(decimal totalAmount) 185 | { 186 | return Customer.IsEliteMember ? totalAmount - (totalAmount * Customer.DiscountPercentage) : totalAmount; 187 | } 188 | 189 | private decimal CalculateTotalBasketAmount() 190 | { 191 | decimal totalAmount = 0; 192 | 193 | foreach (var seller in BasketItems.Keys) 194 | { 195 | decimal totalAmountBySeller = CalculateSellerAmount(seller); 196 | decimal costOfShipping = CalculateShippingCost(seller, totalAmountBySeller); 197 | 198 | totalAmount += costOfShipping + totalAmountBySeller; 199 | } 200 | 201 | return totalAmount; 202 | } 203 | 204 | private static decimal CalculateShippingCost(Seller seller, decimal totalAmountBySeller) 205 | { 206 | return totalAmountBySeller > seller.ShippingLimit ? 0 : seller.ShippingCost; 207 | } 208 | 209 | private decimal ApplyTax(decimal amount) 210 | { 211 | return amount + ((amount * TaxPercentage) / 100); 212 | } 213 | 214 | private decimal CalculateSellerAmount(Seller seller) 215 | { 216 | var (Items, ShippingAmountLeft) = BasketItems.EnsureKeyExists(seller); 217 | 218 | var items = Items.EnsureNonNull(); 219 | 220 | decimal totalAmount = items.Where(x => x.IsActive).Sum(basketItem => basketItem.Quantity.TotalPrice); 221 | 222 | return totalAmount; 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | # but not Directory.Build.rsp, as it configures directory-level build defaults 86 | !Directory.Build.rsp 87 | *.sbr 88 | *.tlb 89 | *.tli 90 | *.tlh 91 | *.tmp 92 | *.tmp_proj 93 | *_wpftmp.csproj 94 | *.log 95 | *.tlog 96 | *.vspscc 97 | *.vssscc 98 | .builds 99 | *.pidb 100 | *.svclog 101 | *.scc 102 | 103 | # Chutzpah Test files 104 | _Chutzpah* 105 | 106 | # Visual C++ cache files 107 | ipch/ 108 | *.aps 109 | *.ncb 110 | *.opendb 111 | *.opensdf 112 | *.sdf 113 | *.cachefile 114 | *.VC.db 115 | *.VC.VC.opendb 116 | 117 | # Visual Studio profiler 118 | *.psess 119 | *.vsp 120 | *.vspx 121 | *.sap 122 | 123 | # Visual Studio Trace Files 124 | *.e2e 125 | 126 | # TFS 2012 Local Workspace 127 | $tf/ 128 | 129 | # Guidance Automation Toolkit 130 | *.gpState 131 | 132 | # ReSharper is a .NET coding add-in 133 | _ReSharper*/ 134 | *.[Rr]e[Ss]harper 135 | *.DotSettings.user 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Coverlet is a free, cross platform Code Coverage Tool 148 | coverage*.json 149 | coverage*.xml 150 | coverage*.info 151 | 152 | # Visual Studio code coverage results 153 | *.coverage 154 | *.coveragexml 155 | 156 | # NCrunch 157 | _NCrunch_* 158 | .*crunch*.local.xml 159 | nCrunchTemp_* 160 | 161 | # MightyMoose 162 | *.mm.* 163 | AutoTest.Net/ 164 | 165 | # Web workbench (sass) 166 | .sass-cache/ 167 | 168 | # Installshield output folder 169 | [Ee]xpress/ 170 | 171 | # DocProject is a documentation generator add-in 172 | DocProject/buildhelp/ 173 | DocProject/Help/*.HxT 174 | DocProject/Help/*.HxC 175 | DocProject/Help/*.hhc 176 | DocProject/Help/*.hhk 177 | DocProject/Help/*.hhp 178 | DocProject/Help/Html2 179 | DocProject/Help/html 180 | 181 | # Click-Once directory 182 | publish/ 183 | 184 | # Publish Web Output 185 | *.[Pp]ublish.xml 186 | *.azurePubxml 187 | # Note: Comment the next line if you want to checkin your web deploy settings, 188 | # but database connection strings (with potential passwords) will be unencrypted 189 | *.pubxml 190 | *.publishproj 191 | 192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 193 | # checkin your Azure Web App publish settings, but sensitive information contained 194 | # in these scripts will be unencrypted 195 | PublishScripts/ 196 | 197 | # NuGet Packages 198 | *.nupkg 199 | # NuGet Symbol Packages 200 | *.snupkg 201 | # The packages folder can be ignored because of Package Restore 202 | **/[Pp]ackages/* 203 | # except build/, which is used as an MSBuild target. 204 | !**/[Pp]ackages/build/ 205 | # Uncomment if necessary however generally it will be regenerated when needed 206 | #!**/[Pp]ackages/repositories.config 207 | # NuGet v3's project.json files produces more ignorable files 208 | *.nuget.props 209 | *.nuget.targets 210 | 211 | # Microsoft Azure Build Output 212 | csx/ 213 | *.build.csdef 214 | 215 | # Microsoft Azure Emulator 216 | ecf/ 217 | rcf/ 218 | 219 | # Windows Store app package directories and files 220 | AppPackages/ 221 | BundleArtifacts/ 222 | Package.StoreAssociation.xml 223 | _pkginfo.txt 224 | *.appx 225 | *.appxbundle 226 | *.appxupload 227 | 228 | # Visual Studio cache files 229 | # files ending in .cache can be ignored 230 | *.[Cc]ache 231 | # but keep track of directories ending in .cache 232 | !?*.[Cc]ache/ 233 | 234 | # Others 235 | ClientBin/ 236 | ~$* 237 | *~ 238 | *.dbmdl 239 | *.dbproj.schemaview 240 | *.jfm 241 | *.pfx 242 | *.publishsettings 243 | orleans.codegen.cs 244 | 245 | # Including strong name files can present a security risk 246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 247 | #*.snk 248 | 249 | # Since there are multiple workflows, uncomment next line to ignore bower_components 250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 251 | #bower_components/ 252 | 253 | # RIA/Silverlight projects 254 | Generated_Code/ 255 | 256 | # Backup & report files from converting an old project file 257 | # to a newer Visual Studio version. Backup files are not needed, 258 | # because we have git ;-) 259 | _UpgradeReport_Files/ 260 | Backup*/ 261 | UpgradeLog*.XML 262 | UpgradeLog*.htm 263 | ServiceFabricBackup/ 264 | *.rptproj.bak 265 | 266 | # SQL Server files 267 | *.mdf 268 | *.ldf 269 | *.ndf 270 | 271 | # Business Intelligence projects 272 | *.rdl.data 273 | *.bim.layout 274 | *.bim_*.settings 275 | *.rptproj.rsuser 276 | *- [Bb]ackup.rdl 277 | *- [Bb]ackup ([0-9]).rdl 278 | *- [Bb]ackup ([0-9][0-9]).rdl 279 | 280 | # Microsoft Fakes 281 | FakesAssemblies/ 282 | 283 | # GhostDoc plugin setting file 284 | *.GhostDoc.xml 285 | 286 | # Node.js Tools for Visual Studio 287 | .ntvs_analysis.dat 288 | node_modules/ 289 | 290 | # Visual Studio 6 build log 291 | *.plg 292 | 293 | # Visual Studio 6 workspace options file 294 | *.opt 295 | 296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 297 | *.vbw 298 | 299 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 300 | *.vbp 301 | 302 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 303 | *.dsw 304 | *.dsp 305 | 306 | # Visual Studio 6 technical files 307 | *.ncb 308 | *.aps 309 | 310 | # Visual Studio LightSwitch build output 311 | **/*.HTMLClient/GeneratedArtifacts 312 | **/*.DesktopClient/GeneratedArtifacts 313 | **/*.DesktopClient/ModelManifest.xml 314 | **/*.Server/GeneratedArtifacts 315 | **/*.Server/ModelManifest.xml 316 | _Pvt_Extensions 317 | 318 | # Paket dependency manager 319 | .paket/paket.exe 320 | paket-files/ 321 | 322 | # FAKE - F# Make 323 | .fake/ 324 | 325 | # CodeRush personal settings 326 | .cr/personal 327 | 328 | # Python Tools for Visual Studio (PTVS) 329 | __pycache__/ 330 | *.pyc 331 | 332 | # Cake - Uncomment if you are using it 333 | # tools/** 334 | # !tools/packages.config 335 | 336 | # Tabs Studio 337 | *.tss 338 | 339 | # Telerik's JustMock configuration file 340 | *.jmconfig 341 | 342 | # BizTalk build output 343 | *.btp.cs 344 | *.btm.cs 345 | *.odx.cs 346 | *.xsd.cs 347 | 348 | # OpenCover UI analysis results 349 | OpenCover/ 350 | 351 | # Azure Stream Analytics local run output 352 | ASALocalRun/ 353 | 354 | # MSBuild Binary and Structured Log 355 | *.binlog 356 | 357 | # NVidia Nsight GPU debugger configuration file 358 | *.nvuser 359 | 360 | # MFractors (Xamarin productivity tool) working folder 361 | .mfractor/ 362 | 363 | # Local History for Visual Studio 364 | .localhistory/ 365 | 366 | # Visual Studio History (VSHistory) files 367 | .vshistory/ 368 | 369 | # BeatPulse healthcheck temp database 370 | healthchecksdb 371 | 372 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 373 | MigrationBackup/ 374 | 375 | # Ionide (cross platform F# VS Code tools) working folder 376 | .ionide/ 377 | 378 | # Fody - auto-generated XML schema 379 | FodyWeavers.xsd 380 | 381 | # VS Code files for those working on multiple tools 382 | .vscode/* 383 | !.vscode/settings.json 384 | !.vscode/tasks.json 385 | !.vscode/launch.json 386 | !.vscode/extensions.json 387 | *.code-workspace 388 | 389 | # Local History for Visual Studio Code 390 | .history/ 391 | 392 | # Windows Installer files from build outputs 393 | *.cab 394 | *.msi 395 | *.msix 396 | *.msm 397 | *.msp 398 | 399 | # JetBrains Rider 400 | *.sln.iml 401 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/Migrations/BasketAppDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using VOEConsulting.Infrastructure.Persistence; 8 | 9 | #nullable disable 10 | 11 | namespace VOEConsulting.Flame.BasketContext.Infrastructure.Migrations 12 | { 13 | [DbContext(typeof(BasketAppDbContext))] 14 | partial class BasketAppDbContextModelSnapshot : ModelSnapshot 15 | { 16 | protected override void BuildModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "9.0.0") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128); 22 | 23 | SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); 24 | 25 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.BasketEntity", b => 26 | { 27 | b.Property("Id") 28 | .ValueGeneratedOnAdd() 29 | .HasColumnType("uniqueidentifier"); 30 | 31 | b.Property("CouponId") 32 | .HasColumnType("uniqueidentifier"); 33 | 34 | b.Property("CustomerId") 35 | .HasColumnType("uniqueidentifier"); 36 | 37 | b.Property("TaxPercentage") 38 | .HasColumnType("decimal(18,2)"); 39 | 40 | b.Property("TotalAmount") 41 | .ValueGeneratedOnAdd() 42 | .HasColumnType("decimal(18,2)") 43 | .HasDefaultValue(0m); 44 | 45 | b.HasKey("Id"); 46 | 47 | b.HasIndex("CouponId"); 48 | 49 | b.HasIndex("CustomerId"); 50 | 51 | b.ToTable("Baskets", (string)null); 52 | }); 53 | 54 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.BasketItemEntity", b => 55 | { 56 | b.Property("Id") 57 | .ValueGeneratedOnAdd() 58 | .HasColumnType("uniqueidentifier"); 59 | 60 | b.Property("BasketId") 61 | .HasColumnType("uniqueidentifier"); 62 | 63 | b.Property("ImageUrl") 64 | .IsRequired() 65 | .HasColumnType("nvarchar(max)"); 66 | 67 | b.Property("Name") 68 | .IsRequired() 69 | .HasMaxLength(200) 70 | .HasColumnType("nvarchar(200)"); 71 | 72 | b.Property("PricePerUnit") 73 | .HasColumnType("decimal(18,2)"); 74 | 75 | b.Property("QuantityLimit") 76 | .HasColumnType("int"); 77 | 78 | b.Property("QuantityValue") 79 | .HasColumnType("int"); 80 | 81 | b.Property("SellerId") 82 | .HasColumnType("uniqueidentifier"); 83 | 84 | b.HasKey("Id"); 85 | 86 | b.HasIndex("BasketId"); 87 | 88 | b.HasIndex("SellerId"); 89 | 90 | b.ToTable("BasketItems"); 91 | }); 92 | 93 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.CouponEntity", b => 94 | { 95 | b.Property("Id") 96 | .ValueGeneratedOnAdd() 97 | .HasColumnType("uniqueidentifier"); 98 | 99 | b.Property("Code") 100 | .IsRequired() 101 | .HasMaxLength(10) 102 | .HasColumnType("nvarchar(10)"); 103 | 104 | b.Property("CouponType") 105 | .IsRequired() 106 | .HasMaxLength(20) 107 | .HasColumnType("nvarchar(20)"); 108 | 109 | b.Property("EndDate") 110 | .HasColumnType("datetime2"); 111 | 112 | b.Property("IsActive") 113 | .HasColumnType("bit"); 114 | 115 | b.Property("StartDate") 116 | .HasColumnType("datetime2"); 117 | 118 | b.Property("Value") 119 | .HasColumnType("decimal(18,2)"); 120 | 121 | b.HasKey("Id"); 122 | 123 | b.ToTable("Coupons", (string)null); 124 | }); 125 | 126 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.CustomerEntity", b => 127 | { 128 | b.Property("Id") 129 | .ValueGeneratedOnAdd() 130 | .HasColumnType("uniqueidentifier"); 131 | 132 | b.Property("IsEliteMember") 133 | .HasColumnType("bit"); 134 | 135 | b.HasKey("Id"); 136 | 137 | b.ToTable("Customers"); 138 | }); 139 | 140 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.SellerEntity", b => 141 | { 142 | b.Property("Id") 143 | .ValueGeneratedOnAdd() 144 | .HasColumnType("uniqueidentifier"); 145 | 146 | b.Property("Name") 147 | .IsRequired() 148 | .HasMaxLength(200) 149 | .HasColumnType("nvarchar(200)"); 150 | 151 | b.Property("Rating") 152 | .HasColumnType("real"); 153 | 154 | b.Property("ShippingCost") 155 | .HasColumnType("decimal(18,2)"); 156 | 157 | b.Property("ShippingLimit") 158 | .HasColumnType("decimal(18,2)"); 159 | 160 | b.HasKey("Id"); 161 | 162 | b.ToTable("Sellers"); 163 | }); 164 | 165 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.BasketEntity", b => 166 | { 167 | b.HasOne("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.CouponEntity", "Coupon") 168 | .WithMany("Baskets") 169 | .HasForeignKey("CouponId") 170 | .OnDelete(DeleteBehavior.SetNull); 171 | 172 | b.HasOne("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.CustomerEntity", "Customer") 173 | .WithMany("Baskets") 174 | .HasForeignKey("CustomerId") 175 | .OnDelete(DeleteBehavior.Cascade) 176 | .IsRequired(); 177 | 178 | b.Navigation("Coupon"); 179 | 180 | b.Navigation("Customer"); 181 | }); 182 | 183 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.BasketItemEntity", b => 184 | { 185 | b.HasOne("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.BasketEntity", "Basket") 186 | .WithMany("BasketItems") 187 | .HasForeignKey("BasketId") 188 | .OnDelete(DeleteBehavior.Cascade) 189 | .IsRequired(); 190 | 191 | b.HasOne("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.SellerEntity", "Seller") 192 | .WithMany() 193 | .HasForeignKey("SellerId") 194 | .OnDelete(DeleteBehavior.Cascade) 195 | .IsRequired(); 196 | 197 | b.Navigation("Basket"); 198 | 199 | b.Navigation("Seller"); 200 | }); 201 | 202 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.BasketEntity", b => 203 | { 204 | b.Navigation("BasketItems"); 205 | }); 206 | 207 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.CouponEntity", b => 208 | { 209 | b.Navigation("Baskets"); 210 | }); 211 | 212 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.CustomerEntity", b => 213 | { 214 | b.Navigation("Baskets"); 215 | }); 216 | #pragma warning restore 612, 618 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/VOEConsulting.Flame.BasketContext.Infrastructure/Migrations/20241223104415_Initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | using VOEConsulting.Infrastructure.Persistence; 9 | 10 | #nullable disable 11 | 12 | namespace VOEConsulting.Flame.BasketContext.Infrastructure.Migrations 13 | { 14 | [DbContext(typeof(BasketAppDbContext))] 15 | [Migration("20241223104415_Initial")] 16 | partial class Initial 17 | { 18 | /// 19 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 20 | { 21 | #pragma warning disable 612, 618 22 | modelBuilder 23 | .HasAnnotation("ProductVersion", "9.0.0") 24 | .HasAnnotation("Relational:MaxIdentifierLength", 128); 25 | 26 | SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); 27 | 28 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.BasketEntity", b => 29 | { 30 | b.Property("Id") 31 | .ValueGeneratedOnAdd() 32 | .HasColumnType("uniqueidentifier"); 33 | 34 | b.Property("CouponId") 35 | .HasColumnType("uniqueidentifier"); 36 | 37 | b.Property("CustomerId") 38 | .HasColumnType("uniqueidentifier"); 39 | 40 | b.Property("TaxPercentage") 41 | .HasColumnType("decimal(18,2)"); 42 | 43 | b.Property("TotalAmount") 44 | .ValueGeneratedOnAdd() 45 | .HasColumnType("decimal(18,2)") 46 | .HasDefaultValue(0m); 47 | 48 | b.HasKey("Id"); 49 | 50 | b.HasIndex("CouponId"); 51 | 52 | b.HasIndex("CustomerId"); 53 | 54 | b.ToTable("Baskets", (string)null); 55 | }); 56 | 57 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.BasketItemEntity", b => 58 | { 59 | b.Property("Id") 60 | .ValueGeneratedOnAdd() 61 | .HasColumnType("uniqueidentifier"); 62 | 63 | b.Property("BasketId") 64 | .HasColumnType("uniqueidentifier"); 65 | 66 | b.Property("ImageUrl") 67 | .IsRequired() 68 | .HasColumnType("nvarchar(max)"); 69 | 70 | b.Property("Name") 71 | .IsRequired() 72 | .HasMaxLength(200) 73 | .HasColumnType("nvarchar(200)"); 74 | 75 | b.Property("PricePerUnit") 76 | .HasColumnType("decimal(18,2)"); 77 | 78 | b.Property("QuantityLimit") 79 | .HasColumnType("int"); 80 | 81 | b.Property("QuantityValue") 82 | .HasColumnType("int"); 83 | 84 | b.Property("SellerId") 85 | .HasColumnType("uniqueidentifier"); 86 | 87 | b.HasKey("Id"); 88 | 89 | b.HasIndex("BasketId"); 90 | 91 | b.HasIndex("SellerId"); 92 | 93 | b.ToTable("BasketItems"); 94 | }); 95 | 96 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.CouponEntity", b => 97 | { 98 | b.Property("Id") 99 | .ValueGeneratedOnAdd() 100 | .HasColumnType("uniqueidentifier"); 101 | 102 | b.Property("Code") 103 | .IsRequired() 104 | .HasMaxLength(10) 105 | .HasColumnType("nvarchar(10)"); 106 | 107 | b.Property("CouponType") 108 | .IsRequired() 109 | .HasMaxLength(20) 110 | .HasColumnType("nvarchar(20)"); 111 | 112 | b.Property("EndDate") 113 | .HasColumnType("datetime2"); 114 | 115 | b.Property("IsActive") 116 | .HasColumnType("bit"); 117 | 118 | b.Property("StartDate") 119 | .HasColumnType("datetime2"); 120 | 121 | b.Property("Value") 122 | .HasColumnType("decimal(18,2)"); 123 | 124 | b.HasKey("Id"); 125 | 126 | b.ToTable("Coupons", (string)null); 127 | }); 128 | 129 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.CustomerEntity", b => 130 | { 131 | b.Property("Id") 132 | .ValueGeneratedOnAdd() 133 | .HasColumnType("uniqueidentifier"); 134 | 135 | b.Property("IsEliteMember") 136 | .HasColumnType("bit"); 137 | 138 | b.HasKey("Id"); 139 | 140 | b.ToTable("Customers"); 141 | }); 142 | 143 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.SellerEntity", b => 144 | { 145 | b.Property("Id") 146 | .ValueGeneratedOnAdd() 147 | .HasColumnType("uniqueidentifier"); 148 | 149 | b.Property("Name") 150 | .IsRequired() 151 | .HasMaxLength(200) 152 | .HasColumnType("nvarchar(200)"); 153 | 154 | b.Property("Rating") 155 | .HasColumnType("real"); 156 | 157 | b.Property("ShippingCost") 158 | .HasColumnType("decimal(18,2)"); 159 | 160 | b.Property("ShippingLimit") 161 | .HasColumnType("decimal(18,2)"); 162 | 163 | b.HasKey("Id"); 164 | 165 | b.ToTable("Sellers"); 166 | }); 167 | 168 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.BasketEntity", b => 169 | { 170 | b.HasOne("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.CouponEntity", "Coupon") 171 | .WithMany("Baskets") 172 | .HasForeignKey("CouponId") 173 | .OnDelete(DeleteBehavior.SetNull); 174 | 175 | b.HasOne("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.CustomerEntity", "Customer") 176 | .WithMany("Baskets") 177 | .HasForeignKey("CustomerId") 178 | .OnDelete(DeleteBehavior.Cascade) 179 | .IsRequired(); 180 | 181 | b.Navigation("Coupon"); 182 | 183 | b.Navigation("Customer"); 184 | }); 185 | 186 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.BasketItemEntity", b => 187 | { 188 | b.HasOne("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.BasketEntity", "Basket") 189 | .WithMany("BasketItems") 190 | .HasForeignKey("BasketId") 191 | .OnDelete(DeleteBehavior.Cascade) 192 | .IsRequired(); 193 | 194 | b.HasOne("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.SellerEntity", "Seller") 195 | .WithMany() 196 | .HasForeignKey("SellerId") 197 | .OnDelete(DeleteBehavior.Cascade) 198 | .IsRequired(); 199 | 200 | b.Navigation("Basket"); 201 | 202 | b.Navigation("Seller"); 203 | }); 204 | 205 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.BasketEntity", b => 206 | { 207 | b.Navigation("BasketItems"); 208 | }); 209 | 210 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.CouponEntity", b => 211 | { 212 | b.Navigation("Baskets"); 213 | }); 214 | 215 | modelBuilder.Entity("VOEConsulting.Flame.BasketContext.Infrastructure.Entities.CustomerEntity", b => 216 | { 217 | b.Navigation("Baskets"); 218 | }); 219 | #pragma warning restore 612, 618 220 | } 221 | } 222 | } 223 | --------------------------------------------------------------------------------