Cart Module

Cart модуль является ключевым компонентом системы Alifshop API, обеспечивающий полноценное управление корзинами покупок, списками желаний и общими корзинами с поддержкой модерируемых товаров.

Обзор модуля

Cart модуль построен по принципам Clean Architecture и Domain-Driven Design (DDD) с использованием CQRS паттерна для разделения команд и запросов.

Основные функции:

  • Управление корзинами - добавление, удаление, изменение товаров
  • Wishlist функциональность - списки желаний пользователей
  • SharedCarts - общие корзины для совместных покупок
  • Moderated Items - товары, требующие модерации
  • Business Rules - валидация лимитов и ограничений
  • Background Jobs - автоматическая очистка и обслуживание

Поддерживаемые типы покупателей:

  • Авторизованные пользователи - полный функционал
  • Неавторизованные пользователи - гостевые корзины
  • MiniApp пользователи - упрощенный интерфейс

Архитектура

Структура проектов

Cart модуль разделен на несколько проектов согласно Clean Architecture:

Alif.ServiceShop.Modules.Cart.Domain/
├── Aggregates/
│   ├── Cart/                    # Основной aggregate корзины
│   ├── SharedCarts/             # Общие корзины
│   └── WishlistAggregate/       # Списки желаний
├── BusinessRules/               # Бизнес-правила
├── Events/                      # Domain Events
└── Services/                    # Domain Services

Alif.ServiceShop.Modules.Cart.Application/
├── Commands/                    # CQRS Commands
├── Queries/                     # CQRS Queries
├── Handlers/                    # Command/Query Handlers
├── ViewModels/                  # DTO/ViewModels
└── Services/                    # Application Services

Alif.ServiceShop.Modules.Cart.Infrastructure/
├── Repositories/                # Data Access
├── Migrations/                  # Database Migrations
├── BackgroundJobs/              # Scheduled Jobs
└── Configuration/               # DI Configuration

Alif.ServiceShop.Modules.Cart.IntegrationEvents/
├── Incoming/                    # Внешние события
└── Outgoing/                    # Исходящие события

Основные Aggregates

1. Cart Aggregate

Root Entity: Cart.cs

public class Cart : AggregateRoot
{
    public CartId Id { get; private set; }
    public BuyerId BuyerId { get; private set; }
    public List<CartItem> Items { get; private set; }
    public List<ModeratedCartItem> ModeratedItems { get; private set; }
    public DateTime CreatedAt { get; private set; }
    public DateTime? UpdatedAt { get; private set; }
}

Entities:

  • CartItem - обычные товары в корзине
  • ModeratedCartItem - товары, требующие модерации

Value Objects:

  • BnplDetails - детали BNPL (Buy Now Pay Later)
  • LoanCondition - условия кредитования
  • PaymentMethod - способы оплаты
  • PurchaseRestrictions - ограничения покупки

2. SharedCarts Aggregate

Root Entity: SharedCart.cs

public class SharedCart : AggregateRoot
{
    public SharedCartId Id { get; private set; }
    public BuyerId CreatorId { get; private set; }
    public List<BuyerId> Participants { get; private set; }
    public List<Item> Items { get; private set; }
    public DateTime ExpiresAt { get; private set; }
}

3. Wishlist Aggregate

Root Entity: Wishlist.cs

public class Wishlist : AggregateRoot
{
    public WishlistId Id { get; private set; }
    public BuyerId BuyerId { get; private set; }
    public List<Offer> Offers { get; private set; }
    public List<ModeratedOffer> ModeratedOffers { get; private set; }
}

Domain Layer

Business Rules

Cart модуль содержит набор бизнес-правил для обеспечения корректности операций:

Основные Business Rules:

// Ограничения покупки для авторизованных пользователей
public class AuthorizedBuyerPurchaseLimitRule : IBusinessRule
{
    public bool IsBroken() => // Логика проверки лимитов
    public string Message => "Превышен лимит покупки для авторизованного пользователя";
}

// Обязательность условий кредита при рассрочке
public class CartItemsMustHaveLoanConditionByInstallmentPurchaseRule : IBusinessRule
{
    public bool IsBroken() => // Проверка наличия условий кредита
    public string Message => "При покупке в рассрочку необходимы условия кредитования";
}

// Проверка количества на складе
public class QuantityMustNotBeMoreThenInWarehouseRule : IBusinessRule
{
    public bool IsBroken() => // Проверка остатков
    public string Message => "Количество товара превышает остаток на складе";
}

Domain Services

public interface ICheckOfferPurchaseLimitForBuyer
{
    Task<bool> CanBuyOfferAsync(BuyerId buyerId, OfferId offerId, int quantity);
}

public interface IOfferQuantityInWarehouseChecker
{
    Task<int> GetAvailableQuantityAsync(OfferId offerId);
}

public interface IOrderCreator
{
    Task<OrderId> CreateOrderAsync(CartId cartId, PaymentMethod paymentMethod);
}

Domain Events

public class ItemAddedToCartDomainEvent : DomainEventBase
{
    public CartId CartId { get; }
    public OfferId OfferId { get; }
    public int Quantity { get; }
    public DateTime OccurredOn { get; }
}

public class CartCreatedDomainEvent : DomainEventBase
{
    public CartId CartId { get; }
    public BuyerId BuyerId { get; }
    public DateTime OccurredOn { get; }
}

Application Layer

Commands (CQRS)

Основные Cart Commands:

// Добавление товара в корзину
public class AddItemToCartCommand : IRequest<Unit>
{
    public int? ClientId { get; set; }
    public string SessionId { get; set; }
    public int OfferId { get; set; }
    public int Quantity { get; set; }
    public int? LoanConditionId { get; set; }
}

// Изменение количества товара
public class ChangeItemQuantityCommand : IRequest<Unit>
{
    public int? ClientId { get; set; }
    public string SessionId { get; set; }
    public int OfferId { get; set; }
    public int NewQuantity { get; set; }
}

// Создание заказа
public class CreateOrderCommand : IRequest<CreateOrderResult>
{
    public int? ClientId { get; set; }
    public string SessionId { get; set; }
    public int PaymentMethodId { get; set; }
    public string DeliveryAddress { get; set; }
}

Moderated Items Commands:

public class AddModeratedItemToCartCommand : IRequest<Unit>
{
    public int? ClientId { get; set; }
    public string SessionId { get; set; }
    public int ModeratedOfferId { get; set; }
    public int Quantity { get; set; }
}

Background Commands:

public class AutoDeleteAbandonedCartCommand : IRequest<Unit>
{
    public int DaysThreshold { get; set; } = 14;
}

public class AutoDelete60DaysOldWishlistCommand : IRequest<Unit>
{
    public int DaysThreshold { get; set; } = 60;
}

Queries

// Получение корзины
public class ViewCartQuery : IRequest<ViewCartVm>
{
    public int? ClientId { get; set; }
    public string SessionId { get; set; }
}

// Получение списка желаний
public class GetWishlistQuery : IRequest<WishlistVm>
{
    public int? ClientId { get; set; }
    public string SessionId { get; set; }
}

// Получение общей корзины
public class GetSharedCartQuery : IRequest<SharedCartVm>
{
    public Guid SharedCartId { get; set; }
    public int RequesterId { get; set; }
}

ViewModels

public class ViewCartVm
{
    public List<CartItemVm> Items { get; set; } = new();
    public List<ModeratedCartItemVm> ModeratedItems { get; set; } = new();
    public decimal TotalAmount { get; set; }
    public int TotalItemsCount { get; set; }
    public List<PaymentMethodVm> AvailablePaymentMethods { get; set; } = new();
}

public class CartItemVm
{
    public int OfferId { get; set; }
    public string Title { get; set; }
    public decimal Price { get; set; }
    public decimal? OldPrice { get; set; }
    public int Quantity { get; set; }
    public List<ItemImageVm> Images { get; set; } = new();
    public BnplDetailsVm BnplDetails { get; set; }
    public LoanConditionVm LoanCondition { get; set; }
    public PurchaseRestrictionsVm PurchaseRestrictions { get; set; }
}

API Endpoints

Cart Controller

POST   /api/cart/items                    # Добавить товар в корзину
GET    /api/cart/items                    # Получить содержимое корзины
DELETE /api/cart/items/{offerId}          # Удалить товар из корзины
PATCH  /api/cart/items/quantity           # Изменить количество товара
PATCH  /api/cart/items/condition          # Изменить условия кредитования
DELETE /api/cart/items                    # Очистить корзину
POST   /api/cart/merge-carts              # Объединить корзины при авторизации
POST   /api/cart/orders                   # Создать заказ из корзины
POST   /api/cart/orders-v2                # Создать заказ с выбранными товарами
DELETE /api/cart/items/bulk               # Удалить выбранные товары

MiniApp Endpoints

POST   /api/cart/mini-app/items           # Добавить товар (MiniApp)
GET    /api/cart/mini-app/items           # Получить корзину (MiniApp)
DELETE /api/cart/mini-app/items/{offerId} # Удалить товар (MiniApp)

Moderated Items Endpoints

POST   /api/cart/moderated-items                    # Добавить модерируемый товар
DELETE /api/cart/moderated-items/{moderatedOfferId} # Удалить модерируемый товар
PATCH  /api/cart/moderated-items/quantity           # Изменить количество
PATCH  /api/cart/moderated-items/condition          # Изменить условия кредитования

SharedCart Controller

POST /api/shared-carts              # Создать общую корзину
GET  /api/shared-carts/{id}         # Получить общую корзину
GET  /api/shared-carts/count        # Количество общих корзин пользователя

Wishlist Controller

POST   /api/wishlist/offers                    # Добавить в список желаний
GET    /api/wishlist                           # Получить список желаний
DELETE /api/wishlist/offers/{offerId}          # Удалить из списка желаний
POST   /api/wishlist/merge-wishlists           # Объединить списки желаний

Бизнес-правила

Система лимитов покупки

Для авторизованных пользователей:

  • Дневные лимиты - ограничения на сумму покупок в день
  • Товарные лимиты - ограничения по конкретным товарам
  • Кредитные лимиты - ограничения при покупке в рассрочку

Для неавторизованных пользователей:

  • Более строгие лимиты на количество и сумму
  • Ограниченные способы оплаты
  • Обязательная авторизация для крупных покупок

Управление остатками

public class InventoryValidationRule
{
    // Проверка доступности товара на складе
    public async Task<bool> IsQuantityAvailable(int offerId, int requestedQuantity)
    {
        var availableQuantity = await _warehouseService.GetAvailableQuantity(offerId);
        return requestedQuantity <= availableQuantity;
    }
    
    // Резервирование товара в корзине
    public async Task ReserveItem(int offerId, int quantity, TimeSpan reservationTime)
    {
        await _warehouseService.ReserveItem(offerId, quantity, reservationTime);
    }
}

Условия кредитования

  • Обязательность при рассрочке - товары в рассрочку должны иметь условия кредитования
  • Исключение при оплате картой - при оплате картой условия кредитования не применяются
  • Проверка кредитоспособности партнера - партнер должен иметь активные кредитные условия

Integration Events

Входящие события (подписки)

Catalog Module Events:

// Активация/деактивация предложений
public class OfferActivatedIntegrationEvent : IntegrationEvent
{
    public int OfferId { get; set; }
    public DateTime ActivatedAt { get; set; }
}

public class OfferDeactivatedIntegrationEvent : IntegrationEvent
{
    public int OfferId { get; set; }
    public string Reason { get; set; }
}

// Изменения цен и количества
public class OfferPriceChangedIntegrationEvent : IntegrationEvent
{
    public int OfferId { get; set; }
    public decimal OldPrice { get; set; }
    public decimal NewPrice { get; set; }
}

public class OfferQuantityChangedIntegrationEvent : IntegrationEvent
{
    public int OfferId { get; set; }
    public int NewQuantity { get; set; }
}

Orders Module Events:

public class OrdersCreatedIntegrationEvent : IntegrationEvent
{
    public List<int> OrderIds { get; set; }
    public int ClientId { get; set; }
    public List<int> ProcessedOfferIds { get; set; }
}

Исходящие события

Cart модуль предоставляет Facade для других модулей:

public interface ICartModuleFacade
{
    Task<List<string>> GetSessionIdsByDaysInCart(int days);
    Task<List<string>> GetClientIdsByDaysInCartAsync(int days);
    Task<CartDetailsForOrderVm> GetCartDetailsForOrderAsync(string cartId);
    Task RemoveCartOnOrderCreated(int clientId);
    Task RemoveSelectedItemsFromCart(int clientId, List<int> offerIds);
    Task<CartDetailsForOrderVm> GetCartByClientIdAsync(int clientId);
}

Background Jobs

Scheduled Jobs

Автоматическая очистка данных:

[DisableConcurrentExecution(60)]
public class AutoDeleteAbandonedCartsJob : IJob
{
    public async Task Execute(IJobExecutionContext context)
    {
        // Удаление корзин старше 14 дней
        var command = new AutoDeleteAbandonedCartCommand { DaysThreshold = 14 };
        await _mediator.Send(command);
    }
}

[DisableConcurrentExecution(60)]
public class AutoDelete60DaysOldWishlistsJob : IJob
{
    public async Task Execute(IJobExecutionContext context)
    {
        // Удаление списков желаний старше 60 дней
        var command = new AutoDelete60DaysOldWishlistCommand { DaysThreshold = 60 };
        await _mediator.Send(command);
    }
}

Конфигурация Jobs

{
  "Quartz": {
    "AutoDeleteAbandonedCarts": {
      "CronExpression": "0 0 2 * * ?",  // Каждый день в 2:00
      "Enabled": true
    },
    "AutoDeleteOldWishlists": {
      "CronExpression": "0 0 3 * * ?",  // Каждый день в 3:00
      "Enabled": true
    }
  }
}

Database Schema

MongoDB Collections

AuthenticatedBuyerCarts

{
  "_id": ObjectId,
  "buyerId": NumberInt,
  "items": [{
    "offerId": NumberInt,
    "quantity": NumberInt,
    "loanConditionId": NumberInt,
    "addedAt": ISODate,
    "updatedAt": ISODate
  }],
  "moderatedItems": [{
    "moderatedOfferId": NumberInt,
    "quantity": NumberInt,
    "loanConditionId": NumberInt,
    "addedAt": ISODate
  }],
  "createdAt": ISODate,
  "updatedAt": ISODate
}

UnAuthenticatedBuyerCarts

{
  "_id": ObjectId,
  "sessionId": "string",
  "items": [/* аналогично AuthenticatedBuyerCarts */],
  "createdAt": ISODate,
  "updatedAt": ISODate,
  "expiresAt": ISODate  // TTL index для автоудаления
}

SQL Server Tables

DomainEvents

CREATE TABLE [cart].[DomainEvents] (
    [Id] uniqueidentifier NOT NULL,
    [OccurredOn] datetime2 NOT NULL,
    [Type] nvarchar(255) NOT NULL,
    [Data] nvarchar(max) NOT NULL,
    [ProcessedDate] datetime2 NULL
);

InboxMessages / OutboxMessages

CREATE TABLE [cart].[InboxMessages] (
    [Id] uniqueidentifier NOT NULL,
    [OccurredOn] datetime2 NOT NULL,
    [Type] nvarchar(255) NOT NULL,
    [Data] nvarchar(max) NOT NULL,
    [ProcessedDate] datetime2 NULL
);

Тестирование

Unit Tests

Domain Tests

public class CartTests
{
    [Fact]
    public void AddItem_WhenValidItem_ShouldAddSuccessfully()
    {
        // Arrange
        var cart = Cart.Create(BuyerId.Of(1));
        var item = CartItem.Create(OfferId.Of(100), 2);
        
        // Act
        cart.AddItem(item);
        
        // Assert
        Assert.Single(cart.Items);
        Assert.Equal(2, cart.Items.First().Quantity);
    }
    
    [Fact]
    public void AddItem_WhenExceedsLimit_ShouldThrowException()
    {
        // Arrange & Act & Assert
        var cart = Cart.Create(BuyerId.Of(1));
        var item = CartItem.Create(OfferId.Of(100), 1000); // Превышает лимит
        
        Assert.Throws<BusinessRuleValidationException>(() => cart.AddItem(item));
    }
}

Application Tests

public class AddItemToCartCommandHandlerTests
{
    [Fact]
    public async Task Handle_WhenValidCommand_ShouldAddItemToCart()
    {
        // Arrange
        var command = new AddItemToCartCommand 
        { 
            ClientId = 1, 
            OfferId = 100, 
            Quantity = 2 
        };
        
        // Act
        await _handler.Handle(command, CancellationToken.None);
        
        // Assert
        _cartRepository.Verify(x => x.SaveAsync(It.IsAny<Cart>()), Times.Once);
    }
}

Integration Tests

public class CartControllerIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public async Task AddItemToCart_WhenValidRequest_ShouldReturn200()
    {
        // Arrange
        var request = new AddItemToCartRequest 
        { 
            OfferId = 100, 
            Quantity = 2 
        };
        
        // Act
        var response = await _client.PostAsJsonAsync("/api/cart/items", request);
        
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
}

Cart модуль представляет собой сложную систему управления корзинами с поддержкой различных типов пользователей, модерируемых товаров, бизнес-правил и интеграций с другими модулями платформы Alifshop.