Skip to content

userator/ddd-cart

Repository files navigation

Система расчёта скидок для корзины покупок

PHP-приложение для расчёта и применения скидок в корзине покупок. Построено на принципах Clean Architecture и Domain-Driven Design.

Возможности

  • Расчёт скидок на основе различных стратегий (процентные, фиксированные)
  • Поддержка разделяемых и неразделяемых скидок
  • Ограничение количества скидок
  • Ограничение процента скидки от стоимости товара
  • Приоритизация скидок по размеру

Технологический стек

  • PHP 8.5 с Composer
  • Symfony Console — CLI-команды
  • PHP-DI — внедрение зависимостей
  • PHPUnit 11.x — тестирование
  • PHPStan — статический анализ (уровень 8)

Требования

  • PHP 8.1+
  • Docker (опционально)

Установка

Через Docker

make init

Локальная установка

composer install

Использование

Запуск примера

# Через Docker
make run

# Локально
php bin/cli app:example

Запуск тестов

# Через Docker
make test

# Локально
composer run test

Запуск статического анализа

# Через Docker
make stan

# Локально
composer run stan

Запуск всех проверок

composer run check

Архитектура

src/
├── Application/UseCase/      # Сценарии использования
├── Domain/
│   ├── CartProcessor/        # Обработчики корзины (Chain of Responsibility)
│   ├── Collection/           # Коллекции
│   ├── DiscountStrategy/    # Стратегии скидок (Strategy)
│   ├── Entity/              # Сущности
│   ├── Factory/             # Фабрики
│   ├── Service/             # Доменные сервисы
│   └── ValueObject/        # Value Objects
├── Presentation/Cli/         # CLI-команды
├── Presentation/Tool/        # Форматтеры
└── Shared/Collection/        # Базовые классы

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

  • 121 юнит-тест — покрытие доменной логики
  • 2 интеграционных теста — тестирование CLI-команды

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

Стратегии скидок находятся в файле src/Domain/Factory/DiscountStrategiesFactory.php.

Обработчики корзины находятся в файле src/Domain/Factory/CartProcessorFactory.php.

Пример вывода

{
    "items": [
        {
            "id": 1,
            "name": "Платиновая ручка",
            "price": 1000,
            "oldPrice": null,
            "image": null
        },
        ...
    ],
    "discounts": [
        {
            "itemId": 1,
            "name": "Фиксированная скидка общая",
            "type": "fixed",
            "separation": "separable",
            "price": 10
        },
        ...
    ],
    "itemsTotalPrice": 1900,
    "discountsTotalPrice": 230,
    "totalPrice": 1670
}

Программное использование

Пример: Создание корзины со скидками

<?php

declare(strict_types=1);

use App\Domain\Entity\Cart;
use App\Domain\Entity\CartItem;
use App\Domain\Collection\CartItems;
use App\Domain\Service\DiscountFiller;
use App\Domain\Service\DiscountCalculator;

// Создаём товары
$items = new CartItems(
    new CartItem(id: 1, name: 'Ноутбук', price: 50000),
    new CartItem(id: 2, name: 'Мышь', price: 1500),
    new CartItem(id: 3, name: 'Клавиатура', price: 3000),
);

// Создаём корзину
$cart = new Cart(id: 1);
$cart->setItems($items);

// Применяем скидки
$filler = new DiscountFiller($strategies);
$cart = $filler->fill($cart);

// Рассчитываем итоговые скидки
$calculator = new DiscountCalculator($separableProcessor, $nonSeparableProcessor);
$cart = $calculator->calculate($cart);

// Получаем результат
$totalPrice = $cart->getTotalPrice();
$discountsTotal = $cart->getDiscounts()->getTotalPrice();

Пример: Добавление собственной стратегии скидки

<?php

use App\Domain\DiscountStrategy\AbstractDiscountStrategy;
use App\Domain\ValueObject\DiscountType;
use App\Domain\ValueObject\DiscountSeparation;

class VIPDiscountStrategy extends AbstractDiscountStrategy
{
    public function __construct()
    {
        parent::__construct(
            name: 'VIP-скидка 15%',
            size: 15,
            separation: DiscountSeparation::SEPARABLE,
            itemId: 1, // только для первого товара
        );
    }

    public function getType(): DiscountType
    {
        return DiscountType::PERCENT;
    }

    protected function calculateDiscount(int $price): int
    {
        return (int)($price * $this->getSize() / 100);
    }
}

Пример: Использование через PHP-DI контейнер

<?php

use App\Domain\Service\DiscountCalculator;
use App\Domain\Service\DiscountFiller;
use App\Presentation\Tool\CartFormatter;

$container = require __DIR__ . '/config/di.php';

$filler = $container->get(DiscountFiller::class);
$calculator = $container->get(DiscountCalculator::class);
$formatter = $container->get(CartFormatter::class);

$cart = new Cart(id: 1);
$cart->setItems(new CartItems(
    new CartItem(id: 1, name: 'Товар', price: 1000),
));

$cart = $filler->fill($cart);
$cart = $calculator->calculate($cart);

echo $formatter->formatAsJson($cart);

Типы скидок

Тип Описание
PERCENT Процентная скидка от цены товара
FIXED Фиксированная скидка в рублях

Типы разделения

Тип Описание
SEPARABLE Скидка может применяться совместно с другими
NONSEPARABLE Скидка применяется отдельно (исключает другие)

Обработчики корзины

Система использует паттерн Chain of Responsibility для обработки скидок:

  • RearrangeCartProcessor — сортировка скидок по размеру
  • MaxPercentCartProcessor — ограничение процента скидки
  • MaxCountCartProcessor — ограничение количества скидок

Лицензия

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages