Skip to content

feat(web-api): нормализовать контракт ответа корзины - #600

Open
Ibochkarev wants to merge 3 commits into
betafrom
feat/issue-570-cart-response-contract
Open

feat(web-api): нормализовать контракт ответа корзины#600
Ibochkarev wants to merge 3 commits into
betafrom
feat/issue-570-cart-response-contract

Conversation

@Ibochkarev

@Ibochkarev Ibochkarev commented Aug 18, 2026

Copy link
Copy Markdown
Member

Описание

Web API корзины (GET /api/v1/cart/get и мутации add/change/change-option/remove/clean) отдаёт стабильный JSON для Nuxt.

В data появляется items: всегда массив, в том числе [] для пустой корзины. Скидки (old_price, discount_price, discount_cost) лежат на позиции, не только в properties. status сохраняет те же ключи: счётчики как int, деньги с round 2, вес с round 3.

Итоги товаров по-прежнему считает CartItemManager::calculateStatus. Доставка, оплата и финальная сумма остаются на GET /api/v1/order/cost. Opt-in include_thumbs=1 подмешивает thumb одним batch-запросом по msProductData.

Пустой data.cart теперь JSON-объект {}, не []. Клиенты с Array.isArray(cart) нужно перевести на items или на object/array dual. Непустой cart по-прежнему map по product_key (legacy toArray, включая order_id).

Общие ключи total_cost / total_weight / total_discount из Cart::status() в ответе order/cost проходят через тот же CartResponseNormalizer::projectStatus(), что и cart/get.

Тип изменений

  • Исправление бага (non-breaking change)
  • Новая функциональность (non-breaking change)
  • Breaking change (изменение, ломающее обратную совместимость)
  • Рефакторинг (без изменения функциональности)
  • Документация
  • Другое (опишите):

Пустой cart: []cart: {} может затронуть клиентов, которые ждут массив. Подробности в заметках ниже.

Связанные Issues

Closes #570

Как это было протестировано?

cd core/components/minishop3
php tests/CartResponseContractTest.php
./vendor/bin/phpunit tests/Unit/Services/Cart/CartResponseNormalizerTest.php
  • Автоматические тесты
  • Ручное тестирование на сайте

Конфигурация тестирования:

  • MiniShop3: ветка feat/issue-570-cart-response-contract
  • PHP: локальный CLI

Чеклист

  • Код соответствует стилю проекта
  • Лексиконы не требуются
  • Изменения задокументированы как breaking для cart: []{}
  • PHPStan / полный CI — в GitHub Actions
  • CHANGELOG — при релизе (breaking для интеграторов)

Дополнительные заметки

Проекция живёт в CartResponseNormalizer на границе Web API. Draft и CartItemManager не переписывались.

ApiClient.buildUrl копирует ?query рядом с route и ctx.

Вне scope: include_order_costs=1, tax/VAT, Manager cart API. CI lint витрины assets/.../js/web/** — отдельно (#667).

@Ibochkarev Ibochkarev added priority: medium Средний приоритет enhancement New feature or request labels Aug 18, 2026
@Ibochkarev
Ibochkarev requested a review from biz87 August 18, 2026 03:57
@biz87

biz87 commented Aug 18, 2026

Copy link
Copy Markdown
Member

Проверил: сам контракт сделан аккуратно — старый ключ cart сохранён как map ({} для пустой корзины), items добавлен рядом как всегда-массив. Ровно то, чего требовал RFC про {} vs [], обратная совместимость витрины не ломается. Конфликтов с beta нет.

Но нужен ребейз: после вливания #595 (journey suite) PHPUnit даёт 5 ошибок.

Error: Call to a member function normalize() on null
  at src/Controllers/Api/Web/CartController.php:276

Причина — гонка, а не баг в коде: CartController::transformResponse() резолвит ms3_cart_response_normalizer из DI, а тестовый контейнер journey (tests/Integration/WebApi/Support/*) о нём не знает — там регистрируются лексиконы и ms3_customer_order, нового сервиса нет, поэтому services->get() возвращает null.

В самом ядре регистрация на месте (ServiceRegistry:296, ServiceRegistryFactories:101), обычные тесты зелёные (smoke 90) — падает только journey.

Достаточно добавить ms3_cart_response_normalizer в харнесс journey (или подменить его стабом, если в suite не нужен полноценный нормализатор).

Заодно предупреждение на весь оставшийся пакет: #598 и #599 сейчас тоже конфликтуют с beta — все P1-PR пересекаются в ServiceRegistry/ServiceRegistryFactories и ProductController. Возможно, проще ребейзнуть их одной серией после каждого вливания, чем по одному.

@Ibochkarev
Ibochkarev force-pushed the feat/issue-570-cart-response-contract branch from 8306de4 to 52f0cd5 Compare August 19, 2026 02:44
@Ibochkarev

Copy link
Copy Markdown
Member Author

Ребейзнул на текущую beta и зарегистрировал ms3_cart_response_normalizer в journey-харнессе (JourneyWebApiModx). Это тот же ручной DI, что ms3_customer_order: services->get() больше не отдаёт null, normalize() вызывается.

--testsuite WebApi: 15/15. composer test: 259 тестов, без тех пяти ошибок.

#598 и #599 уже ребейзнуты на эту же beta.

@AgelxNash

Copy link
Copy Markdown

Этот PR включён в тестовую интеграционную сборку всех открытых PR MiniShop3: AgelxNash/MiniShop3, ветка integration/open-prs-20260906 (28/28 открытых).

Сборка нужна, чтобы проверить совместимость взаимозависимых серий PR до их мержа — при последовательном слиянии они конфликтуют друг с другом. Это не ревью и не конкурирующий PR: авторство сохранено (1 PR = 1 коммит с исходным автором), ветка пересобирается по мере обновления PR.

Как вошёл в сборку: Слился чисто.

@AgelxNash

Copy link
Copy Markdown

Удачи с PR! Пусть дойдёт до релиза как можно скорее — спасибо за вклад в MiniShop3! 🚀

Nuxt получает стабильный items[] и типизированный status без клиентского reduce. Пустой cart сериализуется как объект, скидки вынесены на позицию.
Suite #595 резолвит DI вручную. Без ms3_cart_response_normalizer CartController::transformResponse падает на null->normalize().
@Ibochkarev
Ibochkarev force-pushed the feat/issue-570-cart-response-contract branch from 52f0cd5 to 575dd5b Compare September 8, 2026 00:52
@biz87

biz87 commented Sep 8, 2026

Copy link
Copy Markdown
Member

Технически PR в порядке. После мержа с актуальной beta (конфликт только в use-секции CartController после #638, механический): smoke 99/99, PHPUnit 309 тестов / 794 assertions, --testsuite WebApi 24/24, PHPStan из vendor/bin — 0 ошибок.

Совместимость с #633/#638 подтверждена по смыслу, а не только по факту чистого мержа: те нормализуют входные options до вызова cart->add()/changeOption(), этот PR нормализует выходной ответ после — два непересекающихся этапа одного метода.

Встроенная витрина не ломается. Единственное место в assets/components/minishop3/js/web/**, которое реально разбирает форму data.cart, — ProductCardUI.js:63-124, и оно уже было защитным: Object.values({}) даёт [] так же, как и для старого []. Fenom тоже не задет — ms3_cart.php ходит через внутренний фасад $ms3->cart->get(), а не через HTTP, так что нормализатор в этом пути не участвует вовсе.

Фикс ApiClient.buildUrl проверил отдельно: раньше searchParams.set('route', endpoint) заталкивал ?include_thumbs=1 внутрь значения route, и до PHP параметр не доходил. Правка точная.

Но есть проблема ровно в том, что PR заявляет своей ценностью.

cart/get и order/cost начнут расходиться в числах

CartResponseNormalizer округляет total_cost/total_discount до 2 знаков и total_weight до 3.

А OrderCostCalculator::getTotalCost() (строки ~311-316) мержит в свой ответ тот же самый Cart::status(), но мимо нормализатора:

$response = $this->ms3->getCart()->status();
if ($response['success']) {
    $status = $response['data'];
    $data = array_merge($data, $status);
}

А CartItemManager::calculateStatus() накапливает суммы через += без округления:

$status['total_cost'] += (float)($item['cost'] ?? 0);
$status['total_weight'] += (float)($item['weight'] ?? 0) * $count;

До этого PR оба эндпоинта отдавали одинаковый сырой float и совпадали байт в байт. После — под одним и тем же ключом total_cost возможны 0.3 и 0.30000000000000004.

Issue #570 прямо говорит «rounding зафиксирован» и «документированная граница с order/cost». Граница проведена как разделение ответственности, но не как гарантия согласованности чисел. CartResponseContractTest проверяет только, что строка order/cost упомянута в PHPDoc (str_contains), а не совпадение значений; HeadlessStorefrontJourneyTest не ловит, потому что суммы там круглые (300.0, 50.0) и артефактов плавающей точки не дают.

Для чекаута это не критично — финальная сумма к оплате считается отдельно и через нормализатор не проходит. Но Nuxt, который показывает итог корзины на одном экране и на чекауте на другом, получит два разных числа под одинаковым по смыслу ключом. То есть PR про согласованность контракта вносит рассогласование именно там, где обещает его убрать.

Предлагаю на выбор: либо округлять и в getTotalCost() при мерже, либо явно записать в документации контракта, что precision между эндпоинтами не гарантируется и сверять их нельзя.

Мелочь — тип изменения в чек-листе

Отмечено «Новая функциональность (non-breaking change)», хотя в описании тут же сказано, что пустой cart: [] становится cart: {} и это затронет клиентов с Array.isArray(). Web API живёт с 1.0.0-alpha.2, сейчас 1.13.0-beta1 — внешние потребители за это время вполне могли появиться.

Само решение не плодить две формы разумно, и issue #570 его явно рассматривал среди альтернатив — вопрос не к решению, а к галочке. Если пометить как breaking, это не потеряется при сборке CHANGELOG к релизу и попадёт в заметки для интеграторов.

К сведению, не к правке

При прогоне обнаружилось, что CI-job vueManager lint физически не покрывает assets/components/minishop3/js/web/** — он выполняется с working-directory: vueManager и командой eslint ., а эти файлы лежат вне базового пути. То есть зелёный чек в PR ничего не говорит о линте этих файлов, ни здесь, ни в предыдущих PR. Проверил на версии из beta до твоего PR — те же замечания там же, так что это не регрессия. Завёл отдельно.

Reuse CartResponseNormalizer::projectStatus when OrderCostCalculator
merges Cart::status so shared total_* keys stay consistent (#570 review).
@Ibochkarev

Copy link
Copy Markdown
Member Author

@biz87 Спасибо за ревью.

По расхождению cart/get и order/cost: выбрал первый вариант — при мерже Cart::status() в OrderCostCalculator::getTotalCost() теперь вызывается тот же CartResponseNormalizer::projectStatus() (money 2dp / weight 3dp). Smoke + unit на артефакт 0.1+0.2 добавлены.

Чек-лист PR обновил на Breaking change из‑за пустого cart: []{}.

Про lint витрины — ок, это #667, в этот PR не тащил.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request priority: medium Средний приоритет

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Web API: нормализовать контракт ответа корзины (cart/get) для headless

3 participants