# Symfony API System

API расчёта цены и тестовой оплаты с интерактивным разбором суммы. Исправлены модель товара, миграции, валидация и арифметика скидок.

Статус: passed

Версия: `3b826a6f1ff4acb743f2235b7b5e2a6f7313140c`. Исходное тестовое задание: сентябрь 2023. Обновление и демо: 7 сентября 2026.

## Задача

Клиент должен получить воспроизводимую итоговую цену, а ошибочный налоговый код, товар или купон — понятный отказ.

## Аудитория

Разработчики checkout и API расчёта стоимости.

## Личный вклад

- Symfony-контракты расчёта и покупки, валидация и платёжный слой.

- Восстановление модели Product и схемы БД, переход на Symfony 6.4/PHP 8.4.

- Калькулятор целыми центами, unit-тесты и интерактивный API-экран.

## Решения

- Сначала рассчитывается налог, затем скидка; итог ограничен снизу нулём.

- Внутренняя арифметика использует целые центы и округление half-up.

- Покупка заново рассчитывает сумму на сервере, не доверяя результату из браузера.

## Границы и ограничения

- Налоги и купоны — учебные справочники; это не налоговая консультация.

- PayPal/Stripe заменены детерминированными моками; внешних списаний нет.

- Каталог read-only, история десяти запросов живёт в памяти вкладки.

## Что попробовать

- Рассчитайте цену с налогом и процентным купоном: пример даёт 116,56 €.

- Проверьте фиксированную скидку и разбивку суммы.

- Введите неправильный код, неизвестный купон или товар.

- Выполните успешную тестовую оплату и отклонённый платёж 402.

## Проверки

- Пять unit-тестов калькулятора и 12 production-состояний; старый общий ApiControllerTest не заявляется пройденным.

- Original calculation 11656 cents

- Fixed and empty coupons

- IT validation

- Invalid tax 400

- Unknown coupon 422

- Missing product 404

- Invalid field types 400

- Mock payment success and decline 402

- Reset and mobile overflow

- Подтверждённого GitHub CI для этого SHA нет; перечислены выполненные локальные проверки и сценарии опубликованной версии. Исторические CI-ссылки, если есть, отмечены отдельно.

## Результаты

- Сохранено 12 содержательных кадров фактического интерфейса.

- Дата, сценарий и версия указаны у каждого кадра.

- Максимум RAM контейнеров в коротком тесте: 41.34 MiB. Общие службы учитываются отдельно; магазин включает свою БД.

- Измерено 6 повторений сценария; медиана 306.0 мс, максимум 2364 мс. Время включает браузер и HTTPS; методика и ограничения приложены.

## Запуск и эксплуатация

- Исходный API сохранён: POST /calculate-price возвращает число, /demo/quote — подробный разбор. POST /purchase выполняет повторный серверный расчёт.

- Сборка Dockerfile.portfolio вне VPS использует закреплённый PHP base и composer.lock. Запуск миграций: php bin/console doctrine:migrations:migrate --no-interaction, seed: php bin/console app:fill.

- Nginx проксирует localhost:18401 на Apache. БД symfony имеет отдельного пользователя в общем MySQL. /healthz проверяет приложение. Пять unit-тестов покрывают округление, скидки и отрицательный ввод; HTTP QA проверяет реальные статусы ошибок.

- Общие правила эксплуатации: готовые файлы и закреплённые образы собираются вне VPS; секреты не входят в артефакты. Общесерверный docker prune не применяется. Логи backend ограничены ротацией, изменения log options требуют пересоздания контейнера.

- Перед изменением пройти code-map от функции к экрану/API, модулю и данным. После изменения обновить карту и схемы, выполнить связанные сценарии, включая ошибку, повтор и сброс. Источник истины — конкретная версия кода, а не старая диаграмма.

- Откат проверен 2026-09-07T01:00:08.238865+00:00: 20260907-verified → 20260907b → 20260907-verified. После каждого переключения подтверждены HTTPS, readiness и SHA. Это проверка конфигурации и артефактов на одном проверенном SHA; обратная миграция БД не выполнялась.

## Стек и роль

- **Symfony Validator**: Проверка DTO, налогового кода и контрактов.
- **PHP / PriceCalculator**: Целочисленный расчёт, округление и ограничение скидки.
- **Doctrine / MySQL**: Товары, страны, купоны, миграции и воспроизводимый seed.

## Code-map

|Функция|Экран / API|Модуль|Данные / проверка|
|---|---|---|---|
|Каталог|demo.html / demo/catalog|ApiController, Product|product; readiness, UI|
|Расчёт|demo/quote, calculate-price|ApiValidationService, TaxCodeValidator, PriceCalculator|country, coupon, product; PriceCalculatorTest, HTTP|
|Оплата|purchase|PaymentService|чтение каталога; success / decline / invalid|
|Миграции и seed|CLI|migrations, FillCommand|три справочника; migrate, repeat seed|

## Схемы

- Компоненты опубликованного демо — `diagrams/components.mmd`
- Основной сценарий — `diagrams/scenario.mmd`
- Размещение опубликованной версии — `diagrams/deployment.mmd`
- Состав данных и границы хранения — `diagrams/data.mmd`

## Индекс изображений

- 01-overview: Форма заказа и границы демонстрации; 2026-09-06T23:13:36.123Z; SHA 3b826a6f1ff4acb743f2235b7b5e2a6f7313140c
- 02-calculation: Исходный пример: 100 € + 24% − 6% = 116,56 €; 2026-09-06T23:13:36.758Z; SHA 3b826a6f1ff4acb743f2235b7b5e2a6f7313140c
- 03-api: Реальный JSON запроса и ответа с измеренным временем; 2026-09-06T23:13:37.111Z; SHA 3b826a6f1ff4acb743f2235b7b5e2a6f7313140c
- 04-fixed: Фиксированный купон после условного налога; 2026-09-06T23:13:37.822Z; SHA 3b826a6f1ff4acb743f2235b7b5e2a6f7313140c
- 05-no-coupon: Расчёт без скидки; 2026-09-06T23:13:38.739Z; SHA 3b826a6f1ff4acb743f2235b7b5e2a6f7313140c
- 06-invalid-tax: Некорректный налоговый код: HTTP 400 и блокировка оплаты; 2026-09-06T23:13:39.150Z; SHA 3b826a6f1ff4acb743f2235b7b5e2a6f7313140c
- 07-italy: Исправленный сценарий IT: корректный код и расчёт; 2026-09-06T23:13:39.751Z; SHA 3b826a6f1ff4acb743f2235b7b5e2a6f7313140c
- 08-payment: Успех локального мок-провайдера; 2026-09-06T23:13:40.172Z; SHA 3b826a6f1ff4acb743f2235b7b5e2a6f7313140c
- 09-declined: Воспроизводимый отказ провайдера: HTTP 402; 2026-09-06T23:13:40.624Z; SHA 3b826a6f1ff4acb743f2235b7b5e2a6f7313140c
- 10-invalid-coupon: Неизвестный купон: HTTP 422; 2026-09-06T23:13:41.291Z; SHA 3b826a6f1ff4acb743f2235b7b5e2a6f7313140c
- 11-mobile-form: Мобильная форма заказа; 2026-09-06T23:13:42.102Z; SHA 3b826a6f1ff4acb743f2235b7b5e2a6f7313140c
- 12-mobile-result: Мобильный разбор расчёта и тестовая оплата; 2026-09-06T23:13:42.635Z; SHA 3b826a6f1ff4acb743f2235b7b5e2a6f7313140c

## Подробности runtime из репозитория

# Symfony API System: демонстрационный релиз

Демо подготовлено 07.09.2026; история исходного тестового задания сохранена. Реально выполняются Symfony Validator, Doctrine ORM, чтение MySQL и расчёт целыми центами. PayPal/Stripe — детерминированные моки; внешних запросов и списаний нет. Ставки являются учебными данными проекта.

Восстановлены отсутствовавшие Product и миграция, исправлены отрицательные seed-скидки, DE/IT-коды и отказы для неизвестных стран/купонов. Symfony обновлён с 6.3 до 6.4 LTS; версии закреплены composer.lock. Composer audit после обновления не обнаружил advisories.

PriceCalculator вычисляет налог, затем скидку, с округлением до цента и нижней границей ноль. POST /calculate-price сохраняет прежний числовой ответ; /demo/quote возвращает разбор. POST /purchase заново рассчитывает цену на сервере. Процессор decline даёт HTTP 402. Ошибки ввода дают 400, неизвестный товар 404, купон 422.

Каталог read-only. История запросов находится в памяти вкладки (10 событий), сброс её очищает. При изменении параметров оплата блокируется до пересчёта.

| Функция | Экран/API | Код | Таблицы | Проверки |
|---|---|---|---|---|
| Каталог | demo.html / demo/catalog | ApiController, Product | product | readiness, UI |
| Расчёт | demo/quote, calculate-price | ApiValidationService, TaxCodeValidator, PriceCalculator | country, coupon, product | PriceCalculatorTest, HTTP |
| Оплата | purchase | PaymentService | чтение каталога | success / decline / invalid |
| Миграции и seed | CLI | migrations, FillCommand | три справочника | migrate, repeat seed |

Пять unit-тестов покрывают исходный пример 116,56 €, фиксированную скидку, округление, нижнюю границу и отрицательный ввод. Сценарии опубликованной версии записываются отдельно. Перед изменением проследить запрос до валидатора/калькулятора/таблиц; после изменения обновить карту и повторить расчёт, ошибки и оплату.

Сборка вне VPS: Dockerfile.portfolio использует закреплённую PHP 8.4 основу и Composer lock. На сервер доставляется готовый образ. Apache работает за Nginx/HTTPS; MySQL доступен только во внутренней сети. Отдельная БД/пользователь, лимиты, healthcheck /healthz и ротация логов. Seed воспроизводим.
