Vizen Shop

Orders and the cart: one snapshot, four statuses

/docs/ordersdraftМашинный перевод с английского· проверено 2026-08-20

Статус: draft · проверенный: 2026-08-20, 25 измеренных сценариев против реальных PostgreSQL плюс code якоря (§14)) владелец: гладиатр API документация · Служить: GET /docs/orders · мастерство: none еще Соседи: `promotions.md` (Правила, ключи, кампании, округление), `catalogue.md` (products и их цены).

Проект, не current: Маршрут уже существует (GET /docs/orders, зарегистрированный 2026-08-20, но не external Бег использовал это document Но пока никто не имеет failed Продвигайте его, когда у вас есть.

1.Что вы можете сделать здесь

Прочти shop's cart и изменить то, что в нем, применить и удалить промо code, поворот тот cart входить в order в price Покупатель согласился, читать orders как продавец или как покупатель, export а shop's orders то CSV, move ан order между четырьмя статусы (в любом направлении)), cancel открыть его, пометить возврат, начать онлайн payment, и обмен сообщения на order.

Что ты делаешь? cannot Do: Переписать order. Нет конечной точки, которая меняет order's items, цены или их сумма, и нет конечной точки, которая удаляет order. § 3 — это тот факт, что вся территория построена на.

2. Решать first (вилки)

Если ты…Возьми это pathСтоимость
является Агент с персональным токеном (PAT) И хотят работать с ordersPOST /orders, GET /orders/{id}, GET /orders/export, PUT /orders/{id}/status, POST /orders/{id}/refund, POST /cart/confirm — Эти шесть являются целым PAT Поверхность (читать нужно) orders:read и а prod key; пишет orders:write)Все else В этой области ответы PAT_METHOD_NOT_ALLOWED. Ты cannot заполнить cart с токеном, так /cart/confirm Вы можете быть легко доступны, но бесполезны: это оборачивается *жетон owner's own* cart входить в order, И единственный способ вложить в это cart это live Не планируй "агента помещает в офис". order" поток.
это Витрина с луком live сессияВся эта cart поверхность, /cart/confirm, затем order конечные точкиНичего. extra. Это единственный поддерживаемый checkout path сегодня.
строят ан неподписанный (гость)) cartkeep Содержимое на клиенте, price с ними вместе POST /cart/quote, и магазин с ними вместе POST /cart/merge на loginСервер не имеет анонимного cart — каждый cart row зашифровано user_id — Так что это две разные работы, а не два способа сделать одну. суммы Вся информация, которая есть на сервере, и есть не идемпотентный — Слияние одних и тех же местных cart дважды удвоить (§ 4.4)).
Хочется, чтобы гость увидел gift line и code скидка before входить вPOST /cart/quote — public, Нет, не пишет, ничего не пишетНичего. extra. Цена в браузере вместо этого является дорогой веткой: то есть вторым движком, он не согласен с сервером.'s, Покупатель обнаруживает несогласие в checkout, В деньгах, цитата управляет *то же самое* Функция как GET /cart/items (§4.4).
Хотите быть уверенными, что покупатель платит тот number Они виделиотправлять expected_total на /cart/confirmНичего. extra, и пропуская его, это дорогостоящая ветка: без нее сервер бесшумно создает order В какой бы то ни было форме price Это так и есть (§5.1).).
нуждающийся в order's total before скидкивычислять items_total + discount_totalНет такого field на order, И суммирование линий дает третью, неправильную. number (§6.2).
нужно знать какой скидкой был промо codeчитать promotions[] и look в codeЛинии не скажут вам: order-level steps Не распространяется на них.
хотеть частичный возвратесть nonePOST /orders/{id}/refund это full-amount маркер на payment row И ничего else (§8.3 Деньги перемещаются вручную в поставщике's Кабинет; платформа только записывает, что это произошло.
Хочется order то move когда Деньги приходятписать status себяПодтвержденный payment Не трогать orders.status; Он посылает два письма и огонь. order.paid (§8.2).
Соединяют а shop's CRMПодписывайтесь на четыре order.* Webhooks и опрос POST /orders с filter.updated_from один раз в час (§ 6.4))Каждый order.* body Это full order snapshot — рецепт и field в списке /docs/webhooks §4.3, §6.1.
хотеть shop's целое order списокPOST /orders с filter.company_idЗаметьте глагол: POST /orders список orders. Создать одно из POST /cart/confirm.

The first row Вилка стоит целой интеграции. Всё агенты Хочет сделать с cart — Добавить, удалить, применить code — только сеанс.

3. The main Факт: a order это snapshot, И ничто не переписывает его

Каждая денежная колонка в order Запись производится один раз, внутри сделки, которая Создает, и нет code path Обновить его после. Когда нет, promotion это Редактировать, а не когда deleted, Когда не будет shop's currency изменения, а не когда тот product's price изменения, а не когда order это cancelled и вновь открыт.

Это не политическое заявление — это собственность государства. code, и одной заповедью показывает:

grep -rn 'UPDATE order_items' --include='*.go' internal/   # → nothing at all
grep -rn 'UPDATE orders'      --include='*.go' --exclude='*_test.go' internal/   # → exactly four places
grep -rn 'DELETE FROM orders' --include='*.go' internal/   # → nothing at all

Четыре UPDATE orders есть pg/orders.go UpdateOrderStatus (status, status_changed_at, status_by, updated_at), pg/order_messages.go (тот ""Временные метки чата", и два в pg/shop_payments.go — MarkOrderPaymentPaid (paid ветвь) и RefundOrderPayment — который set только updated_at = NOW(), внутри payment's own сделки, так что a payment или возврат денежных средств, видимый для filter.updated_from Опрос (§6.4). Четыре прикосновения items_total, discount_total, promotions, total или currency. order_items только вставки, написанные в одном месте (pg/orders.go). Нет базы данных triggers на любом столе (проверено с помощью) \d, 0 rows в pg_trigger).

Два последствия, которые стоит усвоить before Вы читаете что-нибудь else:

  • Личность в § 6.2 сохраняется год спустя, после orders чьих правил больше нет Есть. Измеряется: order по цене, установленной −20 % rule держать items_total: 800, discount_total: 200 и promotions[0].name: "-20%" after тот rule это deleted через API;
  • ан order это *Факт заключения сделки*, не view Каталог.Если вам нужно изменить то, что было согласовано, model Ответа нет, кроме одного. new order — который является также why "вновь открыться" а cancelled order (§7.3 Восстанавливает *старый* price.

4. The cart

4.1 Конечные точки

GET    /cart/items?company_id=N          the whole cart of one shop
POST   /cart/items/{product_id}/{key}    add (body: {quantity, data})
PUT    /cart/items/{product_id}/{key}    set exact quantity (body: {quantity})
DELETE /cart/items/{product_id}/{key}    remove one line
POST   /cart/merge                       {company_id, items[]} → the whole cart
POST   /cart/promo                       {company_id, code}    → the whole cart
DELETE /cart/promo                       {company_id}          → the whole cart
POST   /cart/quote                       {company_id, items[], promo_code} → the whole cart

Те, кто first Семь из них только для сессии (§2). POST /cart/quote Это тот, конечная точка в этой области, которая Никаких знаков и пишет ничего — это цены а cart клиент держит (§ 4.4). Все они отвечают одинаково GetCartResponse кроме формы add/update/remove, какой ответ {} — читать cart после.

⚠️ `{key}` это path сегмента и не должны быть empty. А. plain product не имеет конфигурация, а платформа storefront отправляет буквальную строку default для него (vizen-market/storefront/src/store/cart.ts, add(): line.key || 'default'). Ан empty сегмент (POST /cart/items/12/) Не соответствует маршруту: ответы чи 404 — измеренный против одного и того же роутера version (go-chi/chi/v5 v5.2.1) с помощью pattern тот генерируемый code регистры, "/cart/items/{product_id}/{key}".

The cart это за shop: а cart row's company_id исходит из product, не по Вашему запросу и GET /cart/items По нему фильтры. shop где Покупатель ничего не имеет — или упускает company_id полностью, возвращает empty cart с 200, never ан error.

4.2 Если а cart line's price исходит из

Двигатель, который применяет promotions never Выбрать a base price; работает на top Какими бы ни были эти четыре источника resolved в этом order из priority (get_cart.go:113-152, зеркально в pg/orders.go:130-152):

#ИсточникКто владеет number
1combo set — тот product это combo, Так что price Производится: Σ эффективных цен его компонентовсервер, always; ручной печатающий base_price на combo product это ignored
2variant — data.variant_id Решения для a live, active variant из *это* productсервер: COALESCE(variants.price, products.base_price); тот variant Также преобладает preview
33D конфигурация — data.price.total присутствуетклиент, Смотрите предостережение ниже
4base price — none вышеперечисленныхсервер: products.base_price

⚠️ Источник 3 - клиентоориентированный number И становится он тем price. измеряемый: Добавить product чей base_price 1000 с помощью data = {"price":{"total":7,"oldTotal":99}} производит cart line ценовой 7, и POST /cart/confirm пишется order с items_total: 7. Это то, что Документированное состояние 3D path — конфигурация document принимается *как snapshot*, и сервер-side Рекомпьютинг против опубликованного артефакта является запланированный stage (domain/cart.go:47). Единственный санитар applied округление и зажим для [0, 2 000 000 000]. Если вы пишете витрину, отнеситесь к этому Как дырка для keep Закрытая, а не используемая функция; она снова названа в § 16.

А. variant_id Это делает не решать (deleted, неактивный, принадлежащий другой product) возвращается к source 4 и устраняет клиента's price, breakdown и display Ключи от того, что эхом отозвано назад — другие ключи клиента Выжить. Измерить: {"variant_id":"999999","price":{...},"breakdown":{...}, "display":"красный","note":"…"} На 1000-₽ product Возвращается как {"variant_id":999999,"note":"…"} 1000, в том числе в cart и в том, order одинаково.

4.3 Чтение cart

ПолеЧто это такое
subtotalвыплачиваемый — after Каждый шаг скидки. name является исторической
subtotal_beforebefore Скидки - но см. предупреждение
discount_totalsubtotal_before − subtotal
applied[]тот promotions который выстрелил, rule, с amount, kind, code
promo_codeтот code хранимые в этом cart (Верхний этаж сервера)
promo_rejectedwhy тот code не производили скидку — хранили одну, или же та, которая была размещена на /cart/quote — коды в `promotions.md` §3.5
countединицы, включая gift единицы
totalnumber линии, включая gift линии
items[].is_giftэто line это gift: price.price = 0, base_unit_price = его value
items[].is_combo, combo_items[]это line это combo set Что находится внутри него
items[].base_unit_priceprice before пер-line скидка; 0 означает "нет" на-line скидка", не "free"

⚠️ subtotal_before может ответить 0 с товарами в cart — в shop что имеет нет active promotion В общем, потому что шаг, который его заполняет never Бежит. через subtotal + discount_total Вместо этого, та же ловушка, тот же фикс, что и на order side (§6.2). promo_rejected: "NEED_AUTH" это не сервер code: Платформа Магазин изобретает его локально для анонимного посетителя, который набрал code это Не мог вынести вердикт — нет /cart/quote на этом page, или цитата вызов failed (storefront/src/store/cart.ts, хмурый NEED_AUTH). На шее cart и checkout pages Вместо этого, вердикт исходит от двигателя, в promo_rejected из /cart/quote ответ.

4.4 Гость против входа: цитата, чтобы показать его, слиться с keep это

Гость cart Два серверных вызова касаются его, и они не Альтернативы — один вычисляет, другой хранит:

Звони.TokenПишетОтветы
POST /cart/quotenoneничегоценой cart
POST /cart/mergeсессияпокупатель's cart rowsценой cart

Цена гостя cart — POST /cart/quote

{ "company_id": 12,
  "items": [ {"product_id": 41, "key": "default", "quantity": 2, "data": {}} ],
  "promo_code": "SUMMER25" }

The body несёт то же самое `items[]` как слиться, И ответом будет то же самое `GetCartResponse` как `GET /cart/items`: то же самое subtotal / subtotal_before / discount_total, то же самое applied[], то же самое gift линии, те же promo_rejected Коды. Это не совпадение формы. Обработчик гидратирует размещенные линии. вводить cart Линии и руки к ним *то же самое* Функция, которая служит для входа cart (quote_cart.go → computeCart в get_cart.go), так одинаковое содержание Производить те же деньги before и after login — с закрытым входом TestB5Quote_ParityWithSignedIn, который ставит category Продажа, а combo set, а gift rule и промо code В одной корзине и сравнивает все деньги field.

Что он сознательно делает не делать:

  • Ничего не пишет — нет cart row, нет cart_promo, И, прежде всего, нет искупление. code здесь *показанный*, не тратить: promo_keys.used не move и promo_redemptions оставаться empty Сколько раз вы его называете (встречается с) TestB5Quote_PromoCodeShownButNotRedeemed и TestB5Quote_NothingWritten). Искупление живет в checkout Сделка и нигде else;
  • У него нет покупателя, так личный купонные ответы promo_rejected: "NOT_FOUND" — Та же маска, которую получает иностранный купон, на цель (promotions.md §3.5. Скажите посетителю войти, а не что code недействительным;
  • не является filter запас. А. line чей product Убежала, все еще цена - точно так же, как и для зарегистрированного покупателя. *checkout* Отказ (§5.2); фильтрация здесь сделает гостя's cart отличающиеся от своих own cart Через секунду, after login.

Линии сбрасываются молча когда product это unknown, deleted, неопубликованный или принадлежат другому shop — сливаться's толерантность, для слияния's reason: несвежий localStorage не должен стоить покупателю всего расчета. more Вещь здесь — это id цены или исчезают, так что конечная точка cannot ходить как "это draft "Настоящий?" оракул. Дублировать (product_id, key) линии являются Сжатый и зажатый в 9999, позже data выигрыш, который является арифметикой сливаться's ON CONFLICT DO UPDATE: Одна корзина не должна превращаться в две разные суммы. через login.

Одно дело. более строгий чем слиться: 101 items Это 400 (InvalidArgument, от прото-валидатора), где слияние бесшумно падает Все, что было после 100-го, слияние переиграет cart его own owner уже было; цитата Всем известно, что мы «расценили» first 100 из 5000 «ошибочно» number носить одежду правой руки.

Это ограниченный по ставке IP в его own ведро, 60/minute через default (§13) — Это единственное место на платформе, где проходит промо-акция. code можно исследовать без account вообще.

Сохраняй это — POST /cart/merge

Анонимных не бывает cart на сервере. Витрина магазина держит одну в localStorage и, наконец, login, Posts It to Показать POST /cart/merge, который пересчитывает все line через ту же вставку AddToCart использует, а затем возвращает полученный cart.

Эта вставка ON CONFLICT … DO UPDATE SET quantity = LEAST(cart.quantity + EXCLUDED.quantity, 9999), data = EXCLUDED.data, И три вещи следуют:

  • количества добавить, Слияние в два раза больше, чем слияние. done;
  • `data` Последующий line победа — Добавить то же самое (product_id, key) снова заменяет сохраненную конфигурацию и сохраняет суммированную quantity;
  • Плохие линии skipped, не отказываться. Измерение: 123 опубликованных items (один unknown product, с одним quantity: -5, и 120 more), Звонок вернулся 200 28 линий и 101 единица — first 100 items были обработаны (merge_cart.go:23 усечения, unknown product был устранен, и -5 стал 1.

5. Запись. — POST /cart/confirm

{ "company_id": 12, "contact": {"name":"…","phone":"…","email":"…"},
  "comment": "…", "address": { …any JSON… }, "expected_total": 2165 }

contact.name и contact.phone есть required after Обрезка — только белое пространство ответы CONTACT_REQUIRED. contact.email это optional и ан order это created без него. comment и address не подтверждены или ограничены этой областью: 20 000 персонажей comment и 5 KB из address были приняты.

5.1 expected_total — Единственное, что стоит между покупателем и сюрпризом

Серверы ценят order снова, от live Правила, внутри checkout С момента совершения сделки cart Это было доказано, и момент подтверждения был нажатый, а promotion Может быть, закончилось однократное использование code может быть взято на кто-то else, Или бюджет кампании может иссякнуть. Показал покупатель и сервер отказывается вместо тихой зарядки другого number:

Ты посылаешьЧто происходит
Сумма, которую покупатель виделнесоответствие → PRICE_CHANGED, нет order created, тот cart Оставлено нетронутым для перечитывания
ничего (field отсутствие)проверить skipped — Вот как пишут клиенты before тот field keep работа
0 или отрицательный numberКак и любая другая сумма — тот field это optional, "не отправлен" означает "отсутствует", а не "нулевая".-empty order подтвержденный 0 ответы PRICE_CHANGED

какая сумма. Это зависит от того, выполняет ли запрос checkout методы:

  • с checkout методы (delivery_method_id / payment_method_id, §5.4) — expected_total Это order total: товары after скидки + delivery стоимость + payment-метод доплаты, i.e. Что? checkout page Показать и что тот order будет носить как total. Не совпадают только предметы: 1000 товаров 500 курьер подтвердил expected_total: 1000 ответы PRICE_CHANGED, с 1500 Он создает order с total: 1500;
  • Без них (shop не имеет настроенных методов или none избранный) — Это сумма товаров, result.subtotal из cart Вы сделали то же самое, number тот order будет носить как items_total. Без него delivery и нет Налог, который также является total, Таким образом, оба правила согласуются.

Измерение: а cart 1000 подтвержденных expected_total: 999 ответы FailedPrecondition / PRICE_CHANGED и оставляет после себя cart со своим одним line неповрежденный; 0 Ответы те же. Доставка 500 + доплата 200 свыше 2000 товаров сдавать с собой 2500 и отказывает 2300 (checkout_expected_total_integration_test.go).

5.2 Отказы

ПосланиеgRPC codeHTTPзначение
CONTACT_REQUIREDInvalidArgument400name или phone пропавший after отделка
CART_EMPTYFailedPrecondition400ничего не делать order — В том числе "все, что было в нем" deleted Из каталога"
COMPANY_UNAVAILABLEFailedPrecondition400тот shop заблокированы или deleted. Его cart Читать далее Хорошо
ORDER_TOO_LARGEFailedPrecondition400сумма before скидки 2 000 000 000 (§ 13))
PRICE_CHANGEDFailedPrecondition400expected_total не соответствует вновь созданному total
PROMO_CODE_EXHAUSTEDFailedPrecondition400Последнее использование code Между ценообразованием и написанием
CAMPAIGN_BUDGET_SPENTFailedPrecondition400Кампания за спиной code Выбежал в то же окно
out_of_stockFailedPrecondition400а line's Акции — или A gift's — Между ценообразованием и написанием. Вся сделка откатывает назад: измеренная, cart Сохраняет свои линии, а акции не move. Производится, не измеряется на проводе: Руководитель возвращает голую обернутую domain.ErrOutOfStock (измеряется как CreateOrderFromCart: out of stock) и apierr.UnaryErrorMapper Карта для этого code — note Написание отличается от cart's, §12

⚠️ `FailedPrecondition` Прибывает как HTTP 400, а не 412. грип-gateway картографировать его Так нарочно (runtime.HTTPStatusFromCode, v2.22.0), и этот проект Устанавливать не custom Картофель — его error писатель (internal/api/apierr/gateway_error.go) Повторно использовать библиотеку's таблица стенографических отчетов. body это {"error":"rpc error: code = FailedPrecondition desc = PRICE_CHANGED"}, так матч на code на струне, а не на HTTP status: Восемь различных результатов выше 400 акций.

5.3 Какой из них POST /cart/confirm Действительно,

Все это в одной транзакции, сериализованной на (user, shop) Консультативный Замок — двойной submit cannot Создать два orders (pg/orders.go:36):

  1. Проверить shop это active и не deleted; принимает currency как order's currency snapshot;
  2. перечитывает cart FOR UPDATE, исключая deleted products но *не* неопубликованных (а) product взятый off Шельфы все еще корабли);
  3. - цены по §4.2 и суммы; свыше 2 000 000 000 → ORDER_TOO_LARGE;
  4. бежит promotion Двигатель — это *то же самое* promotions.ApplyWith тот cart бегать, с правилами, shop маржинальная капитализация и представленная key Все это читается внутри сделки, то есть order cannot Не согласен с тем, что cart Изготовлено из;
  5. Решает выбранный delivery и payment методы и их стоимость (§5.4)), Затем сравнивают с expected_total (§5.1 - с использованием методов, против total включая;
  6. вставки orders + один order_items row за line;
  7. искупить key: Одно условие UPDATE promo_keys SET used = used + 1 это несёт limit Проверь внутри - ноль rows означает кто-то else взял Последнее использование и вся order Отказывается, а не created В другой раз price. Пишет а promo_redemptions row и очищает code off тот cart;
  8. Каждый участник кампании, имевший rule Огонь — не только code's. Если бюджет cannot cover Сделка, только code's Кампания отказывается от order; для обычных promotions Бюджет идет отрицательно по крайней мере на одну сделку, потому что Покупателю уже было показано, что price;
  9. Запасы для каждого line, дары, включенные в ту же сделку;
  10. Удалить The cart rows который вошел в order, плюс rows указывать на deleted products (В противном случае они были бы невидимыми и неудаляемыми. Навеки. line Добавлено одновременно другой вкладкой выживает.

5.4 Доставка, payment и checkout form

А. shop что имеет never выполненный с возможностью checkout ведет себя точно так же, как before: нет метод является required, нет field за пределы contact.name/contact.phone это required, и POST /cart/confirm принимает тот же body это always Все ниже, все ниже. Включается только после того, как продавец создает методы..

Форма его. Одно целое, checkout_methods, По обе стороны по обе стороны; kind Рассказывает им об отдельности и side-Особая часть жизни в settings, сырой JSON с одним пером-kind белый список:

kindsettingsзначение
delivery`{"type":"courier\pickup\none","free_from":10000}`extra_cost Если товар не доставляется free_from (0 = нет порога)
payment`{"type":"at_checkout\on_receipt\by_agreement","provider":"robokassa","kassa_id":7,"start_status":1,"after":{"kind":"thankyou\order\gateway","page_id":11}}`start_status Это status тот order это created с (default 1, "new"); after Решает, где покупатель приземлится; kassa_id выбор из которых shop's киль Забирает деньги (0 или отсутствует = shop's default до)

kassa_id принимает a number или в виде тройки, и валидируется против shop's own Для другого это – удел, для другого – удел. shop, или deleted Один из них с отказом CHECKOUT_KASSA_NOT_FOUND (400) — одинаковый ответ для обоих, так что исследование ids Ничего не говорит о соседе. never оставляет owner поверхность: GET /public/checkout не несет его, поскольку договор Деньги, которые проходят через none покупателя's бизнес.

Настройте его с GET/POST/PUT/DELETE /checkout/methods[/{id}], PUT /checkout/methods/order (full reorder одного kind), PUT /checkout/methods/{id}/links (заменить-set Допустимые аналоги, от любого side) и GET/PUT /checkout/settings (тот field form, тот order number Формат, спасибо page). Сфера охвата: checkout:read / checkout:write — Они делают не открытый orders, и orders:* не открывает их. Методы и settings не иметь draft: Они меняют live checkout В общем, сразу, так что письмо от Сессия кабинета министров перешла на draft contour Отказано в DEV_MODE_UNAVAILABLE (а draft API key получать PAT_CONTOUR_MISMATCH).

Метод, не имеющий link не показана, когда есть что-то, что link в том числе. GET /public/checkout?company_id=… — Аноним, конечная точка витрины Читать далее — Возвращение enabled, пригодный для использования Методы. А. payment метод type: at_checkout Он может быть использован только тогда, когда он окупается (его стоимость). kassa_id, else тот shop's default Если есть, то есть enabled У него есть поставщик услуг — До тех пор, пока POST /orders/{id}/pay Он будет использоваться, иначе он исчезнет из витрина (оно остается в шкафу)'s GET /checkout/methods, Продавец видит Что исправить и /cart/confirm С помощью которого отвечает CHECKOUT_METHOD_UNAVAILABLE. Ссылки есть required Только когда shop имеет полезные методы обоих видов: затем Метод без a live контрагентом является hidden и каждый delivery несет в себе payment_ids (Пару покупатель cannot Юридически выбор - ловушка. shop с И все они не являются ни платными, ни платными, ни незавершенными. /cart/confirm Принимает только один метод. Одна и та же конечная точка несет form (тот field schema) и order_no_format; это never несет уведомление получателей, то есть payment поставщиком или start_status.

Метод также определяет, какой fields Покупатель видит. рядом required_fields, метод's settings.fields Список field кодировать его шоу — optional Витрина магазина представляет собой союз: schema's own required fields (name, phone), плюс fields и required_fields Избранный delivery, И еще два избранных payment. Если оба выбранные методы оставляют fields empty, Ничего не сужается и в целом schema И это то, что каждый из них shop выполненный с возможностью before это Требование остается неизменным: schema's required профсоюз выбранные методы' required_fields, и POST /cart/confirm Проверяем его сервер-side. Метод может не требовать field не показывает (CHECKOUT_BAD_SETTINGS В письменном виде, потому что это будет тупик для Покупатель: An error примерно field без участия экрана. hidden fields Они просто не отправляются, и сервер падает. unknown Коды в любом случае (§ 12)).

Что подтверждается сейчас, все optional:

{ "company_id": 12, "contact": {"name":"…","phone":"…"},
  "delivery_method_id": 5, "payment_method_id": 9,
  "fields": {"city":"Москва","street":"Ленина 1","comment":"…"},
  "utm": {"utm_source":"vk"}, "page_url": "…", "referer": "…",
  "client_ids": {"ym":"…"}, "expected_total": 2165 }

Четыре отказа являются new, Все четыре решены внутри тот checkout Транзакция, поэтому метод переключается off середина-checkout cannot проскользнуть:

ОтказКогда
FailedPrecondition / CHECKOUT_PAIR_INVALIDпара не связана; или выбранный метод отключен, deleted, Неправильное kind или иной shop's; или shop Есть полезные методы этого kind и none был выбран
FailedPrecondition / CHECKOUT_METHOD_UNAVAILABLEвыбранный payment метод является at_checkout И его долина off, deleted или неподдерживаемый — витрина магазина также не предлагает его.
InvalidArgument / CHECKOUT_FIELD_REQUIRED:<code>а field тот form (или выбранный метод's required_fields) знаки required прибыл empty. The code является частью message Клиент может выделить field
InvalidArgument / CONTACT_REQUIREDнеизмененный — name и phone все еще входит contact, не входить fields

The order держится на snapshot, не является ссылкой. delivery и payment на order перевозить {id, code, name, type, cost} замороженный checkout; переименование или Последующее удаление метода не изменяет существующий order (Нет никакого иностранного key от order - метод, нарочно. - Вместе с ними: order_no (тот shop's own number, {yy}{mm}-{n} через default, Уникальный по shop), delivery_cost, payment_cost, total = items_total + delivery_cost + payment_cost — и items_total это уже Пост-дисквот, так что ничего не вычитается дважды (§6.2, идентичность 1)).

`after` Скажите клиенту, куда идти, вычисляется из payment метод: thankyou → /thankyou?order={id}, order → /orders/{id}, gateway → /orders/{id} (Платить с витрины магазина - это более поздняя волна; обещая, что вы будете платить за него). Перенаправить в до тех пор, пока этого не существует, было бы хуже, чем не обещать Не выбранный метод или unknown value → order, Что случилось, то случилось before Все это существовало.

The shop's own спасибо page. PUT /checkout/settings с thankyou_page_id Нажмите «Спасибо» в одном из shop's own pages (а category любой type — тот id GET /categories Когда он возвращается. set и тот order's payment метод имеет after.kind = thankyou — или order не имеет payment метод вообще — after становится {"kind":"thankyou","url":"/{page path}?order={id}"}, где path Это page's канонический магазин path (/{parent-slug}/{slug}, Тот же, который использует карта сайта; дом page это /). Методы с after.kind order или gateway Не перегружены. page должен быть live page из это shop: кто-то else's, а deleted Один или изобретенный id → InvalidArgument / CHECKOUT_BAD_THANKYOU_PAGE, И ничего не спасается; thankyou_page_id: 0 Если это не так, page это deleted, Неопубликованный или проигравший его slug Позже, ничего не проваливается: after.url возвращается к /thankyou?order={id} (или /orders/{id} для одного order без payment метод). after вычисляется на каждом прочтении, а не на хранении — изменение настроек меняет его на старый orders слишком.

Каждый order Теперь ведет дневник. GET /orders/{id} возврат events[]: {kind, from, to, comment, actor_role, at} с kind в created | status_changed | payment | note | notified и actor_role в 1 buyer | 2 staff | 3 system. created написана в той же самой сделке, что и тот order и status_changed в той же сделке, что и status, так журнал cannot Не согласен с тем, что order описывает. PUT /orders/{id}/status принимает a comment который приземляется на event. Конечная точка списка (POST /orders) делать не перевозить events — Это было бы a query за row.

Ан order может быть отражена в shop's CRM. Когда shop подключенный external CRM (Битрикс24 и повернулся order delivery Включая, продавец's view из order перевозить crm: {provider, remote_url, broken, pending, last_error} — тот Сделка, если изменения все еще ждут, чтобы быть отправленным и why это failed. The field отсутствует, когда order never Пошел в а CRM и является never показаны покупателю. А. status Изменился от CRM side В журнале как обычный status_changed event с source: "bitrix24" (empty — Изменился в Визене). Продавец's view Также носит contact_key — тот key Клиент в The кабинет's CRM контакты (phone цифрами, или e:<email> Когда нет никакого phone); empty когда order Сам список контактов только для кабинета.. Соединение и настройка CRM Это только кабинет action (owner или администратор, сессия; API key cannot делать.

фильтрация orders Получил форму leads Совет уже был: filter.from / filter.to (RFC3339), filter.utm_source, filter.utm_medium, filter.delivery_method_id, filter.payment_method_id.

5.5 Уведомления: кто слышит о order, когда и когда

А. new order Отправляет два вида писем, оба after тот order совершенный, из фоновой горутины — POST /cart/confirm never Ждет письма, и почтовая ошибка never отменять order (он зарегистрирован):

  • Продавцу — "new order" — Идет в shop owner и каждому address в notify.emails из GET/PUT /checkout/settings, Одна буква на каждый address, тот owner's address never дважды;
  • Покупателю — "order №… Получено" - идет на contact.email из order (нет email, без письма, на языке покупателя's Профиль (русский, когда) Он перечисляет линии (name × qty — sum), выбранный delivery и payment С их стоимостью, total, "что дальше"" line выбранный payment метод's type в snapshot (at_checkout → платить с button на order page; on_receipt → Оплата по квитанции; by_agreement или нет метода менеджер будет contact Вы и а link то /orders/{id} на shop's витрина (нет) link когда витрина хоста не настроена. Письмо оставляет notified event В журнале с meta = {"channel":"email","to":"buyer"}.

`notify` Это owner's, Не то key's. На проводе он сырой JSON, {"emails":["sales@shop.ru"]}. PUT /checkout/settings с не-empty notify заменять список: до 10 адресов, каждый с подрезанным, нижним корпусом и проверено как правдоподобное электронное письмо, дубликаты удалены, unknown ключи отброшены; {"emails":[]} Проясняет: плохой список ответов InvalidArgument / CHECKOUT_BAD_NOTIFY. GET возвращает его (как {"emails":[]} Когда ничто не set) — Только для сеанса: через PAT тот field это empty Читать и писать, неся ответы PermissionDenied / CHECKOUT_NOTIFY_CABINET_ONLY, Потому что address Это то же самое class из secret как бот токен.

А. status Изменение постов a system message в order's Чат (§9)): kind = 2, тот author Тот, кто передвигается status (author_role 1 покупатель или 2 staff, Решили так же, как и журнал's актера, и body Машина - это text status_changed:<from>><to>, e.g. status_changed:1>2. Клиент выдает Словами из его own status Словарь, не написав его never отменять тот status Переоденься. email Отправляется в A status изменение.

6. order

6.1 Форма

Order: id, buyer_id, company_id, company_name, status, contact{}, comment, address, items_total, currency, items[], discount_total, promotions[], payment_status, can_pay, pay_now, receipt_sent, created_at, updated_at.

OrderItem: product_id, name, key, data, unit_price, quantity, line_total, preview, base_unit_price, promotion{}, combo_items[].

combo_items[] Присутствует только на combo-set line: Что находится внутри set в checkout, {product_id, name, quantity, unit_price} за один set (unit_price является компонентом's price внутри set). Это а snapshot, как name: редактировать set В дальнейшем не меняет положенный order. Тот же список ездить в items[].combo_items каждого order.* Webhook и его CSV export Записывает в скобки: Living room set (Sofa × 1, Chair × 2) × 1.

items_total это выплачиваемый, after Каждая скидка. name, preview и data Снимки, сделанные на checkout; переименовывать product Позже не меняет а размещенный order. currency Заморожено: измеряется — order помещенный в RUB Читать далее RUB after тот shop переключаться на USD, И только следующий order читать USD.

buyer_id несёт id из account который помещал order. Контракт резервы 0 для гостя order; сегодня /cart/confirm Требуется аутентифицированный Сессия (метод находится в рамках Catalog/* role Wildcard и читать его пользователя из паспорта, поэтому нет path Производит один — см. §15.

6.2 Идентификаторы - используйте их вместо ваших own арифметика

Позволь R = Линии, которые не являются подарками, K = promotions[] Войны, которые kind == "order_discount", I = остальной promotions[].

#ИдентичностьЧто вам дает
1Σ_R line_total − Σ_K amount == items_totalЕдинственный правильный способ добраться до кредитора total Из линий
2Σ_R (base_unit_price or unit_price) × quantity == items_total + discount_totalтот total before Скидки — An order не имеет field для него
3Σ promotions[].amount == discount_totalтот snapshot Объясняет скидку без остатка
4Σ_R promotion.amount == Σ_I amountпер-rule Соответствует агрегации per-line акции

Три правила, которые делают эти личности пригодными для использования:

  • order-level steps не распространяются по линиям. Ан order Скидка и а рекламный ролик code уменьшать items_total и появляться в promotions[], и нет line Знает о них. Σ line_total Поэтому *кто-то else's number* — в Пример работы ниже 685 слишком высок, и ничего в этом нет. payload Так говорит;
  • Скажи steps отдельно `kind`: order_discount с ан empty code это тот order шаг, order_discount с а code является промо code или купон, что-либо else Это уже внутри линий. Это надежно, а не Случайность: валидатор отказывается class: "order" и class: "key" на любой другой kind, такой order-level вход cannot приходить в ношении product_discount;
  • а gift Это реально line Это в не total. unit_price: 0, line_total: 0, base_unit_price = его value, promotion.kind == "gift". Исключите его из каждой суммы. order иметь нет `is_gift` флаг — тот cart делать; на order Дискриминатором является promotion kind.

base_unit_price: 0 Это значит "это" line Не надо никого-line Скидка, а не "Скидка" price «Нулевой» — «Возвращение» unit_price.

6.3 Пример работы — один cart Через каждый layer, с цифрами

Измеряется, а не сочиняется.: max_total_discount_percent = 30. Карт: product А. (1000 ₽ ×1), product B (2000 ⁇ ×1). Правила: per-line −15 % На А; order −10 % 2000 г.; а class: "key" rule −20 % За спиной code SPRING; а gift rule Выдать 300-₽ product; тот code applied.

Шаг впередарифметикабегать total
base цены1000 + 2000subtotal_before = 3000
item: −15% на АA: 1000, 850, benefit 1502850
order: −10% от 2850benefit 2852565
key: −20 %base = линии с нет за-line Скидка = 2000 → benefit 4002165
shop колпачок 30 %надбавка = 3000 × 30 % = 900; скидка до сих пор = 835 ≤ 900 → Не стрелять2165
gift300-₽ line в unit_price 0, материализованный последний2165

The order что выходит: items_total: 2165, discount_total: 835, три promotions[] Входы (150) product_discount, 285 order_discount, 400 order_discount с code: "SPRING"), и три линии — А в 850, В в 2000 г. и в gift при 0 с base_unit_price: 300.

Проверьте личности против него:

Σ line_total (all three lines)      = 850 + 2000 + 0 = 2850   ← NOT the total
identity 1: 2850 − (285 + 400)      = 2165 = items_total      ✓
identity 2: 1000 + 2000             = 3000 = 2165 + 835       ✓
identity 3: 150 + 285 + 400         = 835  = discount_total   ✓

The layer Огонь не разъясняет, что model Лучше, чем те, которые сделали. The shop's Маржинальная охрана разрешила 900, и только 835 было дано, поэтому она не входила в список. Измените эту, пожалуйста. condition — капелька 20 % Вместо 30% — и то же самое. cart меры: надбавка 600, и комплектация для охраны top-вниз, вытаскивать его из тот code first: тот key's benefit 400 капель → 165, тот order Шаг держит свой 285, то есть-line 150 это never касаться. subtotal: 2400, discount_total: 600 = 150 + 285 + 165, Per.-line Скидки освобождаются от цели: их обрезка будет перераспределение денег по линиям, и line snapshot перестать быть тот price Покупатель согласился.

The gift, Между тем, находится в none Из него: не в subtotal_before, Ни в коем случае порог, а не в applied[], не входить discount_total. Это в бухгалтерской книге — тот gift product's stock_quantity Прошли 5, 4.

6.4 Чтение orders

POST /orders            body {"filter":{"company_id":12,"status":1},"page":{"number":1,"limit":20}}
GET  /orders/{id}

POST /orders список; filter.company_id означает «как продавец» и требует Звонивший должен быть staff из этого shop, нет filter означает "мой" own orders как Покупатель. first, сломленный галстуком id так pages cannot Повторить или потерять row. "Персонал shop" Один из них rule для списка, GET /orders/{id} и PUT /orders/{id}/status: тот shop (в том числе и в том, что касается shop в кабинете, или, для сессия, член owner|admin|editor в shop — витрина магазина's токен never несёт в себе shop (a storefront mints токен, подкупленный покупателем, и подписание В нескольких магазинах дают none Пока не будет выбран один, так что витрина "сделки" pages Работа с членством, как чат. А. PAT это never расширенный член: Он видит только то, shop Он был выпущен для, даже если его owner это staff в другом месте (TestOrdersStorefrontStaffByMembership). Витрина магазина's токен-счет членство только в shop в этом магазине: /auth/session скрепляет Покупатель-скупщик токен к shop из page's Origin (его поддомен или active custom домен, так что script на shop B's витрина cannot работа shop А.'s сделки от имени члена, посещающего А; от имени А's own Витрина одного и того же члена работает как staff. Тот же самый rule Для чат и сделок badge (TestOrdersBuyerScopedStaffOnlyInOriginShop). Доступ к одному order: Покупатель этого или staff его shop. Кто-нибудь else получать `NotFound / ORDER_NOT_FOUND` — Ответ на вопрос точно для id не существует, поэтому последовательный ids cannot Перечислить, чтобы узнать, какой orders Одна маска для целого order поверхность: GET /orders/{id}, PUT /orders/{id}/status (Незнакомец; покупатель order Сохраняет смысл BUYER_CAN_ONLY_CANCEL_NEW), POST /orders/{id}/refund и order's Все ответы в чате одинаковые (orders_isolation_integration_test.go).

Опрос на изменения — `filter.updated_from`. RFC3339, инклюзивный, сравниваемый против COALESCE(updated_at, created_at): а new order, а stage перемены, а подтверждено payment и возвратить все move (§3), а external system Сохраняет Начало последнего опроса и просит все, что с тех пор:

POST /orders   body {"filter":{"company_id":12,"updated_from":"2026-09-24T09:00:00Z"},"page":{"number":1,"limit":100}}
GET  /orders/export?updated_from=2026-09-24T09:00:00Z

неразборчивый value → InvalidArgument / BAD_TIME_FILTER, как для from/to. Соедините это с order.* веб-хулиган (/docs/webhooks §4.3): events есть Быстро, опрос закрывает пробелы.

экспортировать CSV.

GET /orders/export?status=3&from=2026-09-01T00:00:00Z&to=2026-09-30T23:59:59Z
→ {"csv":"<base64>","rows":2}

Звонивший's own shop (только) main Компания Токена — нет shop параметр, поэтому нечего указывать на кого-то else's), scope orders:read, закрытой для a dev key Как и все остальные order Читать полностью фильтры являются optional И что они означают в POST /orders: status 1–4, from/to RFC3339, Включительно, на created_at; updated_from RFC3339, Включительно, на последнем изменении (выше); неразрывное время → InvalidArgument / BAD_TIME_FILTER. The file имеет ту же форму, что и свинец export: а UTF-8 BOM, ; как разделитель, CRLF, старейший first, максимум 10 000 rows; csv это bytes, так JSON - несет его кодировку в 64-й базе.: order_no (тот id когда order предшествует нумерации), id (тот order id — тот key соответствовать rows в CRM), created_at (RFC3339, UTC), stage (new|processing|done|cancelled), payment_status (последний payment's государство, empty Когда существовал none), name, phone, email, delivery, delivery_cost, payment, payment_cost (от order's snapshot), items (name × quantity, соединенный с |), items_total, discount_total, total, currency, city, address (Остальная часть address Узел, соединенный с , ), comment, fields (checkout fields как key=value, соединенный с |), затем последний прикосновение utm_source … ttclid и те же десять, что и first_…. Любой text клетка который начинается с = + - @, накладной или CR префиксируется с ' Так что таблица не является формулой, то есть phone +7… Прибывает как '+7….

7. Статус

7.1 Четыре, и кто может move их

ценностьИмяКто может set это
1newПродавец (любой) - покупатель never
2processingтолько продавец
3doneтолько продавец
4cancelledПродавец из любого места; Покупатель только из `new`, на их own order

Все действительные status И пусть каждый из них будет верным. status, включая себя: AllowedOrderTransition Буквально ValidOrderStatus(from) && ValidOrderStatus(to) (domain/order.go:26). The owner's Решение было в том, что этапы ведут себя как кнопки., Включенный backs. Все шестнадцать пар были пробегом, каждая на своем own order: ноль отказы. Отказы поступают из четырех других мест.:

  • BAD_STATUS — тот number не является 1...4 (измеряется с помощью 0 и 7, оба InvalidArgument);
  • BUYER_CAN_ONLY_CANCEL_NEW — Покупатель просил ничего, кроме отмены new order их own;
  • STATUS_CONFLICT — Кто-то изменил status между вашим чтением и вашим (обновление обусловлено тем, что status Ты видел);
  • REFUND_REQUIRED — аннулировать ан order чей payment это paid. Марк. возврат first (§8.3).

"Продавец" здесь staff из order's shop — то же самое rule в качестве чтения (§ 6.4)): жетон's shop, или членство в сессии; а PAT только его own shop. до тех пор, пока 2026-09-27 Это был токен's shop Только, и сеанс витрины получил ORDER_NOT_FOUND на status Это можно обсудить в чате (§ 16)., item 5).

7.2 Что делает каждый переход и является ли повтор безопасным

Переходный периодпоследствияПовторение делает…
→ cancelledОтказывается, если payment это paid; возвращает акции внутри status сделка; after Обещание, откатывает искупление: promo_keys.used − 1, Бюджет кампании refunded, клиент limit освобожденныйДвижение акций уникально для каждого (order, product, reason, round); Откат только касается rows с reverted_at IS NULL. Измеряется: три последовательных отмены 2-единицы order Оставшийся запас на 10, а не 12 или 14
cancelled → что-либоповторно применяет искупление безусловно (used + 1, пересчитанный бюджет) и вновь вычитает запасы по его own reason reorder. А. gift line чей product При этом не перепродается: line Оставаться в order, Открытый проходит через (TestB54Reopen_SoldOutGiftDoesNotBlockReopen)Ничего: повторное применение только касается rows с reverted_at IS NOT NULL (TestB54Reopen_Idempotent). Следующая отмена возвращает то, что order Он все еще держится и never удержанный gift: когда gift является отдельным product Не было никакого реестра row вообще (TestB54Reopen_WithheldGiftKeepsLedgerBalanced); Когда это будет *то же самое* product как paid line Движение разделено, поэтому вновь открытое пишет уменьшенный reorder row и возврата аннулирования по последнему вычету, а не по checkout row (TestB54Reopen_MixedGiftOfSameProductLedger)
→ doneВыдает купоны на каждый issue_key rule это order удовлетворяет, отправляет их покупателю, увольняет key.issued за купонВторой купон не нужен. Импотенция опирается на уникальный index (idx_promo_keys_issued_once), не на чеке — двойной клик производит два *параллельная* Звонки, и проигравший регистрируется как нормальный результат, а не как результат. error
Все те же самые valueразрешенный; updated_at, status_changed_at, status_by все move; тот order.status_changed Webhook снова загорается old_status == new_statusСмотри. rows Выше — это *последствия* Идеально, идемпотентно, *бухгалтерский учет* не является

Две асимметрии являются преднамеренными и заслуживают внимания. before Вы создаете петли:

  • paid акции отказываются; промо и подарки не. Открытие order чей купленный Товары были проданы в то же время не весь переход — там Нечего доставлять (TestStockRound_SoldOutReopenChangesNothing). Открытие order чье одноразовое использование code В то же время он был принят и позволил перерасход средств по одной сделке — покупатель уже имеет price в размещенный order, и не далее order Увидит ли он rule (TestB54Reopen_ExhaustedKeyOverspendsByOneDeal). Открытие order чей gift Продажа также удалась: gift Не зарезервировано снова, его line оставаться в order Как только он узнает, что ему было дано, продавец решает, что ему делать. судовладелец's Решение No6 от 2026-08-20 — а free line покупатель never выбираемый и cannot не должны запирать помещённое order в cancelled (TestB54Reopen_SoldOutGiftDoesNotBlockReopen). Когда gift Это то же самое product как paid line ("Купить два, третий - free"), тот paid Часть остается вычитаемый в full или повторный запуск не удался: нехватка, которая достигает купленного единицы — это отказ, а не деградация (TestB54Reopen_SecondRoundKeepsPaidUnitsPaid). Один случай не покрытый Это решение: а gift product продавец *deleted* пока order был cancelled Не удается вновь открыться, с not_found Вместо того, чтобы out_of_stock — измеренный, неодетый, названный в stock.md D10 (а);
  • Откат искупления продолжается after тот status совершать, в его own сделка, и ее сбой регистрируется, а не повышается — продавец's status Изменение не должно провалиться через счетчик.Цена - это гонка, названная в §16..

7.3 Что является необратимым

  • выданный купон. Отмена завершенного order Не отменяет его — key row - Остается, письмо отправлено. code удаляет выданный key об отмене;
  • деньги. А. payment обозначенный refunded cannot Вернуться назад на paid через это API, и can_pay оставаться false для refunded order — а new order Это ответ;
  • тот price. Открытие A cancelled order возвращает свои snapshot, не сегодня's Каталог. Вот в чем смысл §3.

Все else — status, запас, key бюджеты, бюджеты избирательных кампаний – идет в обе стороны.

8.Оплата

8.1 Начало одного

POST /orders/{id}/pay → {"pay_url": "…"}. Только для покупателей, только для покупателей: даже для Продавец получает ORDER_NOT_FOUND на кого-нибудь else's order, Потому что у незнакомца нет Бизнес-обучение, которое существует. link Он построен и подписан сервером с тот shop's Ключи провайдера — платформа never Затронуты деньги, они принадлежат для торговца's own Договор с поставщиком.

Отказы: PAYMENTS_NOT_CONFIGURED (Сервис не подключен вообще — измеряется На стенде без него), PAYMENTS_DISABLED (тот shop Не переключается до на), ORDER_NOT_PAYABLE (cancelled или done), CURRENCY_NOT_SUPPORTED (дождевик Только рубль, и нет конвертации), ORDER_ALREADY_PAID, PAYMENT_ALREADY_STARTED (AlreadyExists → HTTP 409; а live link blocks а Второй, потому что два обвинения оставляют без ответа вопрос.).

The amount это items_total × 100 копейки, квитанция address это contact.email Если присутствует, то в противном случае account's address — Квитанция должна идти Где-то и в этом order не требует какого-либо email.

payment_status и can_pay Возвращайтесь ко всем order читать и вычисляться сервер (domain.CanPayOrder): '' значит, нет payment когда-либо начиналось; pending, paid, expired, failed, cancelled, superseded, overpaid Все остальное. fields молчаливо отсутствует, когда payment Сервис не является проводной — a order Читать дальше, просто ответы payment_status: "" и can_pay: false.

pay_now Он более узкий и отвечает на другой вопрос.: checkout не является До тех пор, пока не закончится order это paid. Это true когда can_pay это true *и* тот order's payment метод является type: at_checkout. Витрина магазина звонит POST /orders/{id}/pay справа от него after ConfirmCart, Вместо того чтобы гадать покупатель's Намерение от метода's name.

8.1.1 Несколько кивок на shop

А. shop может подключаться more чем один до (kassa) — а production один и один и test Компания и единственный трейдер, card и SBP. Который принимает order это решенный в payment метод's settings.kassa_id; Метод, который называет none использует shop's default До тех пор. link является ссылкой, а не snapshot: тот order snapshot Сохраняет то, что покупатель paid для (name, type, стоимость, при этом получая account это shop Продавец может move Без переписывания прошлого orders.

Каждый из них несет два sets ключи, live и test, Потому что проблемы Робокассы отдельный test Пароли — один set Для обоих режимов не удается с его error 29 и нет Объяснение продавцу. set а payment подписаны с сохраненным на payment само по себе, поэтому уведомление проверяется с помощью тех же ключей, даже если Продавец переворачивает кильку на другую contour Между тем.

Тиллы управляются только из кабинета. (GET/POST/PUT/DELETE /v1/orgs/{org_id}/kassas[/{kassa_id}], owner пишет, owner или admin читать); нет PAT scope И не отвечаешь им, и не отвечаешь им. key — Только последние четыре Персонажи и "это" key это set" флаг. robokassa, tbank и sber может быть включенный; yookassa может быть сохранен, но не включен (provider_supported: false), и дать ему возможность ответить PROVIDER_NOT_SUPPORTED.

Поставщики. Каждый до's webhook_url является его поставщиком's own уведомление address — /payments/robokassa/result (GET или POST form), /payments/tbank/result (POST JSON, ответ OK), /payments/sber/result (GET ответный звонок, ответ с голыми 200; вставьте его в провайдера's кабинет. Ключ fields Поставщик: Т-банк — login Это TerminalKey, secret тот Терминальный пароль, secret2 тот же пароль (T-Bank подписывает запросы и Уведомления с одним key); тот test contour Это другой хозяин (rest-api-test.tinkoff.ru) с тем же терминалом. Token извещение Проверяется, order В Т-банке как OrderId = тот order number, и Квитанция отправляется вместе с payment — ставки 20 % и 20/120 есть не в Т-банк's Словарь, так что shop В результате этого, они получают явный отказ вместо link. Сбер — login/secret Это они. API account (уходить login empty отправлять secret как открытый токен), secret2 Callback Checksum (Обратная проверка) key (симметричный HMAC-SHA256; асимметричный RSA Схема не поддерживается и отклоняется с объяснение); test contour это 3dsec.sberbank.ru. Сбер проходит мимо cart в партнер кассовый аппарат, который пробивал квитанцию — платформа never знаки

  • Для обоих, провайдера, это квитанция, как отправлено.'s own payment id хранится на

payment когда link выдается, и уведомление matched только в этом случае; "paid" Уведомление урегулировало order — Убытки, отмены, удержания и Возврат денег признается поставщику и ничего не меняет..

POST /v1/orgs/{org_id}/kassas/{kassa_id}/verify (owner или admin, нет PAT) спрашивать Поставщику, если до's active-contour ключи работают, не создавая payment: {"ok": true|false, "message": "..."}. Неправильная key, недостижимый провайдер и «нет проверки для этого провайдера» (Робокасса) ok: false с а различный message — never ан error status.

Платформа's HTTP Клиент доверяет system Корни плюс, когда API_PAYMENTS_EXTRA_CA_FILE указывает на то, что PEM file, тот extra Корни в нем: Т-Банк и Sber обслуживают сертификаты, прикованные к российскому Министерству цифровых технологий Развитие CA, который большинство system - Нет, нет. Проверка сертификатов never переключатель off.

Удаление кассы мягкое: платежи указывают на нее, а уведомление приходит в дни Позже все еще нужно проверить. "Отключите кассету" от старого payment settings Экран действительно протирает ключи, что и требовал продавец..

8.2 Подтвержденный payment не move тот order

Он посылает два письма (покупатель): "paid"; продавец: «Деньги пришли, можно доставить» и Огонь, order.paid веб-хук (full order snapshot + тот payment, /docs/webhooks §6, пишет a payment вхождение в order Журнал (§9) и только перемещение orders.updated_at (§3), так что payment появляется в updated_from Опрос. status Остается там, где продавец оставил его - смотрите note в services/shop_payments.go: *"тот order оставаться в status 'new' И не будет никакого знамения. сам по себе"*, который является именно why продавец's письмо существует.

Уведомление's amount сравнивается в копейках, не как text. Робокасса посылать OutSum с двумя десятичными знаками на test contour и шесть на этом live один, так Сравнение струн против 990.00 Отвергать все live уведомление, то Поставщику придется вернуться на день, и order Неоплачиваемый навсегда.

8.3 POST /orders/{id}/refund — Что он делает и чего не делает

Он переворачивается один row: тот order's paid payment становится refunded, и в том, Одна и та же операция добавляет payment Въезд в страну order's журнал — comment Возврат оформлен: <amount>, meta {"refund":true,"amount_kop":N}, актер Продавец, который нажал button (TestRefundWritesOrderEvent); orders.updated_at (§3), и в order.refunded Webhook зажигает с помощью order snapshot и тот refunded amount в копейках. Вот и все effect. Измеряется на order 1800 с оплатой за-line скидка:

  • order status неизмененный (1), items_total Неизмененный (1800)), discount_total Неизменные (200), линии неизмененные;
  • Неизменный (все еще 5 из 7) after 2-х единицы order), Регистр акций по-прежнему один движение;
  • Купон остается потраченным и кампания's Бюджетные расходы. promo_keys.used Не возвращаясь, promo_campaigns.budget_used не ходит Сзади, нет row в promo_redemptions получает reverted_at, и представить его однократное использование code Опять ответы EXHAUSTED. Это owner's решение №1 от 2026-08-20, а не надзор: тот order *случилось*. Покупатель использовал code, Сделка прошла, и передав счетчик обратно, потому что деньги были Возвращается только один раз code Второй жизни за каждый возврат. TestB54Refund_PartialRefundDoesNotReturnTheCoupon;
  • Второй звонок Ответы NOTHING_TO_REFUND, Как и призыв к неоплаченному order;
  • после, → cancelled проходит REFUND_REQUIRED ворота и *затем* возврат запас и Откатывает выкуп обратно, как любая отмена делает. Отмена является action Это отменяет сделку; возврат только разблокирует его..

Есть Нет частичного возврата: нет amount принимается, и payment row иметь один status За всю сумму торговец возвращает половину суммы order в поставщик's Кабинету негде это записать. gap, не является надзора см. §16.

9. Сообщения на order

GET  /orders/{order_id}/messages    oldest first, page.limit default 20
POST /orders/{order_id}/messages    {"body": "…"}  1…5000 characters
POST /orders/{order_id}/seen        clears the badge of the calling side
GET  /deals/badge                   unread counter

author_role (1 покупатель, 2 staff) и kind (1 text, 2 system) есть set от Сервер от вызывающего's доступ, never принятый от клиента. kind = 2 это написаны только сервером, на status изменение: body это status_changed:<from>><to> (§5.5) — разобрать его, не показывать его сырым. system message Считать по отношению к другому side's непрочитанный badge Как и любой другой. Доступ - это Покупатель, order или любой staff член shop членство (owner|admin|editor; витрина's только в том, что в shop в этом магазине, §6.4. любой else получать ORDER_NOT_FOUND Вместо отрицания, Такой незнакомец cannot зондировать, order ids существует. body Это только белое пространство ответы EMPTY_BODY.

Все четыре являются только для сессии — PAT-опровергнутый design, И это охраняется: TestDealsChatPATDenied В тот день, когда один из них будет добавлен в scope карта.

10. Целые числа и округление

Деньги в этой сфере – это целое. currency единицы — рубли, а не копейки — в каждом field из cart и order. Единственное место, где появляются копейки, это payment amount Передано в доску (items_total × 100).

Округление never случается *в* Эта область: каждая фракция производится promotion Двигатель и одиночный source Истину, по которой каждый шаг это `promotions.md` §3.10, стол, за которым проверяли ворота двигатель's own Два зажима должны быть здесь.:

  • деньги fields на проводе находятся int32. А. cart total выше 2 147 483 647 зажатый, Не отказывался: а cart 9 999 × 250 000 считываний subtotal: 2147483647;
  • checkout Отказывается от того же cart с ORDER_TOO_LARGE, и измеряет его потолок before Скидки. Измеряется: то же самое cart с а −100 % rule на От него по-прежнему отказываются.

11. Рецепты

$API Это shop API корень, $TOKEN Сеансовый токен (The Session Token) cart только сеанс, §2). $CID Это shop id.

11.1 Поместить order в price Покупатель увидел

# 1) put something in the cart — note "default" as the key of a plain product
curl -X POST "$API/cart/items/101/default" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"quantity":2}'

# 2) read the cart and take the payable total from it
curl -s "$API/cart/items?company_id=$CID" -H "Authorization: Bearer $TOKEN" \
  | jq '{subtotal, subtotal_before, discount_total, promo_code, promo_rejected}'

# 3) confirm with exactly that number
curl -X POST "$API/cart/confirm" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d "{\"company_id\":$CID,
      \"contact\":{\"name\":\"Иван\",\"phone\":\"+70000000000\"},
      \"expected_total\":2165}"

проверять: Ответ несет result.items_total == 2165 и result.id. Если бы ответил PRICE_CHANGED Вместо этого перечитайте cart — тот price двигались и *не* создавать order - Не повторяй это снова. expected_total: 0 "Сделать так, чтобы это прошло"": 0 сравнивается, как и любой другой сумма и отказано; только *опущение* тот field Пропустить чек — и это Ветвь, которая взимает плату number Никто не видел.

11.2 Читать далее order's Деньги правильно

curl -s "$API/orders/321" -H "Authorization: Bearer $TOKEN" | jq '
  .result as $o
  | ($o.items | map(select(.promotion.kind != "gift"))) as $R
  | ($o.promotions | map(select(.kind == "order_discount")) | map(.amount) | add // 0) as $K
  | { payable:        $o.items_total,
      lines_sum:      ($R | map(.line_total) | add // 0),
      from_lines:     (($R | map(.line_total) | add // 0) - $K),
      before_discount:($R | map(((.base_unit_price // 0) | if . == 0 then .unit_price else . end) * .quantity) | add // 0),
      check_2:        ($o.items_total + $o.discount_total) }'

проверять: from_lines == payable (удостоверение личности 1) и before_discount == check_2 (личность 2). lines_sum Печатается рядом с ними Нарочно — это number Наивный импортер будет писать, и это отличается от того, что он пишет. order-level steps.

11.3 Прогулка order то "done" И посмотреть, как появляется купон

curl -X PUT "$API/orders/321/status" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"id":321,"status":2}'   # processing
curl -X PUT "$API/orders/321/status" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"id":321,"status":3}'   # done

проверять: как покупатель, GET /my-coupons Теперь выписывает купон, если shop имеет live issue_key rule чей порог это order's выплачиваемый total встреча. Отправка status: 3 Второй раз разрешается и ничего не дает. more — тот Выпуск охраняется уникальным index, Не по чекам.

11.4 Отменить и посмотреть акции и code возвращаться

curl -X PUT "$API/orders/321/status" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"id":321,"status":4}'

проверять: GET /products/{id} шоу stock_quantity Подкрепить приказом quantity, и однократного использования code использованный в этом order Опять работает для того же Покупатель, если звонок ответит REFUND_REQUIRED, тот order это paid — отметка возврат first (11.6 Отмена дважды не изменяет ничего дальше: измеренный, запас Остановки в трех последовательных отменах.

11.5 Откройте заново

curl -X PUT "$API/orders/321/status" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"id":321,"status":2}'

проверять: Снова упала и снова key Счетчик возвращается вверх — order Возвращение к работе *в его own snapshot price*, который является why тот code должен быть Ожидайте отказа, если вы купленный Товары продаются, когда order лежать cancelled: вновь открыть то, что cannot быть переданным - это обещание shop cannot keep. Две вещи never Причина этого отказа — code (Он перерасходует на одну сделку вместо) и а gift чей product Продано: открытые проходы, gift Просто не вновь зарезервировано, и его line Оставаться в order для вас, чтобы иметь дело с судоходством (§7.2).

11.6 Отметьте возврат — и посмотрите, как мало он делает

curl -X POST "$API/orders/321/refund" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"id":321}'

проверять: перечитать order. payment_status это refunded; status, items_total, discount_total и каждый line есть неизмененный, Акция имеет не Переехали, и промо-счетчики не Возвращается — купон Остатки, потраченные на принятие решения, а не бездействие (§ 8.3). NOTHING_TO_REFUND. Если вы хотите вернуть товар на полку и купон освобожденный, то есть аннулирование (11.4) — возврат только разблокирует его.

12.Безмолвно ignored

Каждый row измеряется на 2026-08-20 против live Руководитель, не читать off тот code. "200" ниже включает в себя empty {} телах, cart мутации.

Что посылаетеЧто происходитПочему
quantity выше 9999 (добавить, обновить, объединить))Связь с 9999, 200Антисемит-overflow Потолок, а не validation. Измеряется: 100 000 → 9999 и PUT 50 000 — 9999.
quantity из 0 или отрицательной добавлять или сливатьсястановится 1Тот же зажим от другого side; слияние line с -5 Прибыв в 1. `PUT` отличается: Его валидатор работает, поэтому quantity: 0 Отказано в InvalidArgument и line Сохраняет свою старую quantity — измеренный.
Добавить то же самое (product_id, key) дваждыколичество добавлять, второй data заменять тот firstОдин ON CONFLICT DO UPDATE. Следствием слияния является §4.4: слияние дважды удваивается..
page на GET /cart/itemsignored полностьюThe cart это never Измеренный.: {number:3, limit:1} более чем 3-line cart Возвращаем все 3 с current_page: 1.
page.limit Вне 1...100 в POST /ordersмолча 20Прото заявляет, что 1…100, но GetOrders never - вызывает валидатора.: limit: 500 → 20 rows, limit: 0 → 20 rows, limit: 5 → 5.
filter.status: 0нет status filter вообще0 - "не отправлено". number принимается без validation: 99 ответы 200 с ан empty Список, а не error.
expected_total отсутствующий в запросетот price Проверка skippedКлиенты пишут before тот field keep работа. 0 или отрицательный number это *не* "отсутствующее": field это optional, и такой value Сравнение с любой суммой (§ 5.1)).
more более 100 items в POST /cart/mergeВсе, что прошло 100-й, падениеАнтисемит-DoS в списке, предоставленном клиентом. Измеряется: 123 items 100 обработанных.
слияние line неприхотливый или неприхотливый dataэто line это skipped, Остальная часть слияний увенчалась успехомОдин плохой line из localStorage не должны стоить покупателю в целом cart. Тот же самый data отправленный в POST /cart/items/… ответы DATA_TOO_LARGE Вместо этого — измеряется 70 KB.
слияние line называть product Это то, что unknown, deleted неопубликованныйничего не вставляется И ничего не сказаноВставка представляет собой SELECT … FROM products WHERE … не соответствует row. Измеряется с помощью unknown id.
а POST /cart/quote line называть product Это то, что unknown, deleted, неопубликованным или другого shopэто line это skipped; Остальное - по цене 200слияние's терпимость (несвежее состояние) localStorage не должен стоить весь расчет, плюс один more effect: ан id либо цены, либо исчезновение, то есть public Конечная точка не является "делает это" draft Есть ли оракул?. TestB5Quote_ForeignAndUnpublishedSkipped.
а fields key Этого нет в shop's checkout formЭтот вход является падение, Остальная часть order это createdThe form Клиент не должен иметь возможность вставлять произвольные ключи в белый список. order's snapshot, но extra key от старой витрины сборки не должен стоить покупателю их order либо.
а required_fields code В методе, который form не содержитЭто требование является ignoredВ противном случае метод блокировки checkout за спиной field Покупателю негде заполнить.
а utm или client_ids key вне белого списка (utm_*, gclid, yclid, fbclid, ysclid, ttclid; ym, ga, ga4)Этот вход является падениеВ том же белом списке leads Использование — эти nodes не должен стать местом захоронения личных data извне address бар.
delivery_method_id / payment_method_id в shop который сконфигурирован нет Методы вообщеignored, тот order это created без snapshot"Никогда не конфигурируется" должен быть keep вести себя точно так же, как before.
то же самое (product_id, key) Дважды в одном POST /cart/quoteколичество добавлять, в 9999 г.; позднее data победаПреднамеренно арифметика слияния's ON CONFLICT DO UPDATE — одна корзина не должна иметь двух сумм в зависимости от того, зарегистрирован ли покупатель. TestB5Quote_DuplicateLinesMergedLikeAddToCart.
рекламное объявление code на POST /cart/quoteСкидка является показанный; тот code это не тратитьpromo_keys.used не move и promo_redemptions оставаться empty. Не является искуплением, искупление находится в checkout только сделки.
а личный купон на POST /cart/quotepromo_rejected: "NOT_FOUND", Без скидок, 200У котировки нет покупателя, чтобы соответствовать owner_user_id Против. Отличительная reason Подтвердить, что купон существует.
а quantity 0 или выше 9999 на POST /cart/quote lineстановится 1 / 9999Те же зажимы, что и слияние, и для паритета reason: Гость's корзина должна увлажняться в точно rows AddToCart Если бы писал.
more более 100 items в POST /cart/quote400, InvalidArgument, Ничего не ценитсяЕдинственное место, где цитата *более строгий* чем слиться (§4.4). Громкое намеренное: truncated Корзина возвращает правдоподобную неправоту total.
слияние line называть product из другой shopОн приземляется в это shop's cart И поэтому в этом ответе отсутствуетА. cart row's company_id исходит из product, Не от твоего company_id — это field Выбирает только тот, который cart Прочитано обратно (pg/cart.go:24-27). Происходит от SQL, не измеряется; row будет look Потерянный, а не неуместный.
неопубликованный a product Что уже есть в вагонахэто оставаться В каждом cart и вступает в ordersThe owner's Решение: принимать product off Стелла не стирает историю. new AddToCart отказано.
исключить product Это в a cartОн исчезает из cart В следующей статье, и ее rows есть сметать в checkoutВ противном случае rows Он был бы невидим и неустраним навсегда. line, checkout ответы CART_EMPTY.
data.price / breakdown / display на line чей variant_id не разрешаетЭти три ключа являются раздетый От эха и от price возвращается к base_price; Другие ключи выживаютАнти-спуфинг: неразрешенный set не должен быть в состоянии price Измеряется с помощью variant_id: "999999".
data.price.total на line с нет variant_idэто становится price3D конфигурация path (§4.2) Не контроль для обхода - дыра, названная в §16.
перемещать order в status Он уже находится в200; никакого второго движения акций, никакого второго купона; updated_at двигается и order.status_changed Огни с помощью old == newПереходы не ограничены design, так *последствия* Вместо этого они стали идемпотентами. Потребитель веб-хуков, который считает status Изменения прекратятся-count.
отменять дваждыАкции возвращаются один разРеестр key это (order, product, reason, round). Измеряется в трех отменах в row.
contact.email на ordernever Используется для купон письмоКупон является личным и идет в account's address; Отправляя его в address вписанный в order form кто-то кому-то отдаст else's Скидка для незнакомца. grep -n ContactEmail internal/api/catalog/coupon_notify.go → Ничего. *это* используется для фискальной квитанции и показывается в продавце's письмо.
платить orderтот status не moveПродавец отправляется, поэтому продавец продвигает stage. The payment Он посылает два письма и order.paid вместо.
POST /orders/{id}/refundтолько payment row изменения (плюс payment журнальная запись)Ни акций, ни промо, ни status, нет amount — §8.3 Потраченный на проведение кампании бюджет и купон через owner's Решение No1, с закрытым входом TestB54Refund_PartialRefundDoesNotReturnTheCoupon.
comment и address на checkoutнеобъявленным и необоснованнымИзмеряется: 20 000 символов comment и 5 KB произвольный JSON address. Эта область sets нет limit его own.
GET /cart/items без company_id, или с помощью shop Покупатель не имеет cart вempty cart, 200, currency: ""The cart пер shop и 0 - "не отправлено". Никаких ошибок, поэтому интеграция указывает на неправильный shop Выглядит как empty cart, Не как ошибка.
promo_rejected: "NEED_AUTH"тот сервер never Отправляет егоПлатформа Storefront изобретает его локально для анонимного посетителя, который набрал code. Сервер's set в promotions.md.

Громкие неудачи, так что вы заметите: CONTACT_REQUIRED, DATA_TOO_LARGE, BAD_STATUS, EMPTY_BODY, и таблица §5.2. Некоторые из них не стабильный Коды. Добавление unknown product Ответить на сырой text product not found и Добавить распроданный один ответ out of stock — оба FailedPrecondition, оба форматируется с %v прямо из домена error. The *то же самое* Нехватка в checkout Приходит запечатанный out_of_stock, Потому что это path проходит через error Вместо этого картограф: одна ситуация, две струны. Прото-уверенизатор утечка его own также формулировка — quantity: 0 на PUT /cart/items/… ответы invalid UpdateCartItemRequest.Quantity: value must be greater than or equal to 1. Сопоставьте любой из них с вашей опасностью; они перечислены в § 16.

13. Пределы

Читайте из констант на 2026-08-20. Ничто не проверяет эту таблицу механически Но все же — нет BuildOrderReference() противоположность тому, что promotions ссылка и что gap Назван в §16.

LimitценностьГде он применяется
quantity одного cart line9999domain.MaxCartQty (domain/order.go:31), И снова как LEAST(…, 9999) в SQL
data одного cart/order line65 536 bytesdomain.MaxCartDataBytes (domain/product_variant.go:126)
items принятый одним POST /cart/merge100merge_cart.go:23 — молча усеченный
items принятый одним POST /cart/quote100прото repeated.max_items, и этот отказываться с InvalidArgument вместо усечения
order total (before скидки)2 000 000 000maxOrderTotal (pg/orders.go:17) → ORDER_TOO_LARGE
клиентская конфигурация price2 000 000 000maxConfigPrice (domain/cart.go:41)
Любые деньги field на проводе2 147 483 647clampInt32 — Зажимы, не отказывается
POST /orders page sizedefault 20, max 100get_orders.go; из range → 20, молчаливо
rows в одном GET /orders/export10 000, старейший firstpg.ExportOrdersMax — Остальное вырезано молча; узкое from/to
order message body1.5000 символовПрото-валидатор
GET /orders/{id}/messages page sizedefault 20, 1.100 Вынужденныйpg/order_messages.go:21 + Прото-валидатор, который этот обработчик называет — измеренный: limit: 500 → InvalidArgument, не POST /orders
payment link жизнь30 минутdomain.PaymentLinkTTL — ан expired link не block а new один
payment currencyRUB только (empty как обращаться с рублями)domain.PaymentCurrency
POST /cart/promoограниченный по ставке IP, own ведро, 30/minute через defaultservices/ratelimit.go (rlProtected), API_RATELIMIT_AUTH_PER_MIN (cmd/core/main.go:874)
POST /cart/quoteограниченный по ставке IP, его own ведро, 60/minute через defaultservices/ratelimit.go (RLClassQuote), API_RATELIMIT_QUOTE_PER_MIN (cmd/core/main.go:874)
checkout Методы одного kind за shop30domain.CheckoutMethodsPerKindMax → FailedPrecondition / CHECKOUT_LIMIT
fields в checkout form40domain.CheckoutFormMaxFields — Более строгий, чем 64-й лид form Позволяет: каждый, кто покупает, проходит через это form
settings одного метода8 192 bytesdomain.CheckoutSettingsMaxBytes
checkout form schema65 536 bytesdomain.CheckoutFormMaxBytes
один checkout field value4 096 символовdomain.CheckoutFieldValueMaxLen — truncated, не отказываться
метод code^[a-z][a-z0-9_-]*$, ≤ 64domain.ValidCheckoutCode → InvalidArgument / CHECKOUT_BAD_CODE; Уникальный по (shop, kind) Среди них live методы
order_no_format≤ 64 символа, должны содержать {n}UpdateCheckoutSettings → CHECKOUT_BAD_ORDER_NO_FORMAT; {yy}, {yyyy}, {mm} являются другими держателями
comment на status изменение≤ 2000 персонажейПрото-валидатор
notify.emails из checkout settings≤ 10 уникальных адресовdomain.CheckoutNotifyEmailsMax → InvalidArgument / CHECKOUT_BAD_NOTIFY

Ни одна другая конечная точка в этой области не ограничена скоростью: перехватчик's список Пять методов аута, ApplyPromoCode и QuoteCart, ничего else (grep -n rlProtected -A 16 internal/core/services/ratelimit.go). Три ведра отдельный, ключ (IP, class): Посетитель переоценивает свои cart шестьдесят раз cannot запирать login Всех, кто стоит за одним и тем же NAT, поворачиваться Один циферблат не move Другие — вот что API_RATELIMIT_QUOTE_PER_MIN существует для.

14.Как это было проверено

  • 25 Измеренные сценарии Написано против настоящих кураторов над Постгресом (B5_TEST_DSN=…/tz_m4, миграция applied по прибору) на 2026-08-20: quantity зажимы и data переписывать; page ignored на cart; клиент конфигурация price становясь line price; expected_total в четыре variants; Все шестнадцать status пары плюс три последовательных отмены с бухгалтерский учет читать after каждый; POST /orders page хвастовство и выпадение-range status filter; Возврат неоплаченного и на paid order; а snapshot выживший удаление его rule; Слияние с 123 items в том числе сломанные; покупатель и доступ к иностранным продавцам; неопубликованный и deleted products в checkout; ан неразрешенный variant_id; негабаритный data; contact validation; Промо code жизненный цикл через failed И успешный checkout; ORDER_TOO_LARGE с без скидки 100%; заблокирован shop; тот currency snapshot; где Прото-валидатор работает и не работает; Все шестнадцать status парами, каждая из которых own order; и full Пример §6.3 в двух разных вариантах shop Зонд, зонд. files были временные и удаленные; каждый number Вышеприведенные цитаты исходят из их output.
  • Существующие сюиты повторно запускают зеленый на той же базе данных, И они являются теми, это keep Эти заявления true идти вперед: TestB5Sum_* (4 — личности §6.2, включая умышленно нарушенные Проверка, чтобы доказать, что шашка не работает), TestB54Reopen_* (6 - возобновление возврата тот key и бюджет, является идемпотентным, перерасходуется ровно на одну сделку, и С 2026 по 08-20 гг. также проходит, когда order's gift Продано во время отмена, сохраняя баланс реестра в течение двух последующих циклов), TestStockRound_* (4 - три cancel/reopen циклы, одна регистрационная пара за цикл, a аншлаг возобновить ничего не меняя, параллельно отменяет возврат акций один раз), TestB5NoRules_* (2), TestB54_* (9 — бюджеты, выдача купонов, иностранные маскировка купона, а code на empty cart).
  • §8.3 Теперь у вас есть ворота, которого не было, когда document был написан: TestB54Refund_PartialRefundDoesNotReturnTheCoupon местоимение order с а одноразовый code В рамках бюджетной кампании, отмечает ее payment paid, звонки POST /orders/{id}/refund, и требует promo_keys.used, promo_campaigns.budget_used, тот live искупление rows и order's status Быть именно тем, кем они были before, с помощью code до сих пор отвечает EXHAUSTED. Записывает owner's Решение No1, не только не current Поведение: оно было зеленым на first Бегите, и в тот день, когда краснеет, решение отменяется..
  • Якорь кода Вместо того, чтобы запоминать что-либо, чего не может достичь обработчик: Прилагаемое имущество §3 (три грепа, процитированные там), PAT поверхность §2 (grep -c '"/vizenpro.api.catalog.v1.Catalog/<method>":' internal/core/services/guard.go → 1 для шести разрешенных методов, 0 для восьми методов, предназначенных только для сеансов, с TestDealsChatPATDenied Доказательство того, что означает 0 во время выполнения, и купон письмо's address (grep -n ContactEmail internal/api/catalog/coupon_notify.go → ничего).
  • HTTP status коды выводятся, а не измеряются по проводу: Ни один случай не был за это зацепился draft. Они приходят из runtime.HTTPStatusFromCode (грип-gateway v2.22.0, тот version в go.mod) Плюс к этому, что этот проект Устанавливать не custom картограф — internal/api/apierr/gateway_error.go Это называется одинаковые функции для любого error нести gRPC status. The gRPC коды и те message Сами же струны были измерены..
  • The empty-`{key}` 404 4.1. был оценен в go-chi/chi/v5 v5.2.1 с помощью pattern генерируемый маршрутизатор регистрируется, а не против запущенного сервера;.
  • Перечислимый content здесь рукописный, не promotions.md. Это - а известные слабости и first item § 16.

15. Отрицания и претензии в отношении повторной проверки after Каждая волна (§ 3.9))

Каждый line Ниже приводится заявление о form "X не происходит. Ни один из них не является Охраняемый a test Красный цвет, когда перестает быть true, Каждый из них проверяется рука after Каждая волна - и чек записывается рядом с ней. Получить выход из этого списка.

Один line оставлено на 2026-08-20: *"§8.3: возврат не затрагивает ничего, кроме payment row"* Теперь закрепляется TestB54Refund_PartialRefundDoesNotReturnTheCoupon (§14), которая является why Его больше нет в таблице ниже..

Утверждение в этом documentПроверить
§3: Нет code path переписывать order's деньгиТри грабли §3. Тривиальный для автоматизации — это first Врата, которые должны существовать здесь
§2: cart поверхность, StartOrderPayment И чаты будут PAT-отрицатьgrep -c В соответствии с методом в guard.go; Только чат покрывается а test (TestDealsChatPATDenied)
§12: contact.email это never Используется для купонной буквыgrep -n ContactEmail internal/api/catalog/coupon_notify.go → должен оставаться empty
§12: Платить не надо move тот order statusgrep -rn 'UPDATE orders' internal/ — Остаться в четырех hits; Два в pg/shop_payments.go set updated_at только
§7.3: отмена завершенного order не отменяет выданный купоннет code Удалить a key Отмена — перепроверка по чтению UpdateOrderStatus
§6.1: нет path создает buyer_id: 0 order/cart/confirm читает его пользователю из паспорта и сидит под Catalog/* role Дикая карта.Если гость checkout Корабли, которые всегда line и прото comment Оба становятся true — Идентичность §6.2 должна быть переоценена для этого.
§6.2: а order не имеет is_gift флагперепроверить OrderItem в прото, то есть cart Один из них – и один – в него вписывается. order Это было бы хорошим изменением, которое бы безмолвно фальсифицировало это. text
§10: в этой области не происходит закругленияПроверить, что cart и checkout Позвонить по-прежнему promotions.ApplyWith Каждый из них не может самостоятельно вычислить
§13: здесь нет ничего ограниченного, кроме /cart/promogrep -n rlProtected -A 12 internal/core/services/ratelimit.go

16. Известные пробелы — названы, поэтому следующий человек не удивляется

  1. Нет сгенерированной машиной ссылки. promotions.md cannot Несвежий потому что Каждый список в нем сравнивается с BuildPromoReference(). Ничего из этого kind существует для orders: статусы, коды отказа и тот limits § 13 набираются вручную и дрейфуют. Шесть ворот якоря находятся в место — gate:price-sources, gate:refusals, gate:identities, gate:statuses, gate:transitions, gate:limits — и Не проверяйте ничего еще. (Названы они здесь без comment разграничители по назначению: якорь должно произойти ровно один раз, и ворота, которые matched Это предложение вместо Стол будет хуже, чем отсутствие ворот..)
  2. `data.price.total` является клиентоконтролируемым price (§4.2) Измеряется: 7 ⁇ на 1000-₽ product, прямо в него order. Сервер-side пересчитать против Опубликованный конфигурационный артефакт является плановым stage и не отгружается.
  3. Откат искупления проходит за пределами status сделка. Его own code Он говорит то, что остается открытым: если вновь открытая победа в гонке против отмена's Откат, откат приземляется после на live order — тот key это free Опять же и опять order - У меня есть скидка. - У меня есть скидка. откат в сторону status изменение предыдущего решения — что противодействовать never Продавец не удаётся's status изменения, а значит, и owner's Звонок, а не патч. The size Окно не было измерено и нет number Ибо в этом появляется document.
  4. Нет частичного возврата (§8.3): нет amount принимается, и payment row иметь один status Половину этой ячейки, которая *был* Открытый — является ли refunded order возвращает право на его купон — был решенный 2026-08-20 (owner's Решение No1: не, Потому что order случилось. Эта половина теперь закрыта (§ 8.3, § 14); что остается gap записывает частичную *amount* вообще.
  5. ~~Два разных definitions «Продавец» на том же order~~ — закрыто 2026-09-27 Для списка, для order и его status: staff из shop по токену's shop или Для участия в сессии, например, в чате (§6.4. Все еще на токене's shop только: POST /orders/{id}/refund и GET /orders/export — Действия кабинета, а не на витрине магазина.
  6. Два отказа являются сырым доменом text, Нестабильные коды: product not found и out of stock от cart Все клиенты, которые на них похожи,
    • Соответствие английскому предложению. Прото-убедитель сливает его own формулировка Точно так же — invalid UpdateCartItemRequest.Quantity: value must be greater than or equal to 1 Это то, что клиент видит quantity: 0.
  7. ~~Один и тот же незнакомец получает два разных ответа на один и тот же вопрос. order~~ — закрыто 2026-09-24: GET /orders/{id}, PUT /orders/{id}/status и POST /orders/{id}/refund Ответить на вопрос незнакомец NotFound / ORDER_NOT_FOUND, Как в чате (§ 6.4)). POST /orders с кем-то else's filter.company_id Ответы по-прежнему DENIED — Называется a shop, не является order.
  8. Финансирование не принадлежит этому line. The order side вычислить a за-line доля в одной order скидка только на квитанцию — пропорциональная line_total, остаток в последнем ненулевом line — И эта доля является
    • Заведомо отсутствует в контракте. else около 54-FZ это неустановленное (§2 таблицы владения земельным участком)).
  9. Нет. email на status изменение. Покупатель не получает письма, если order движение то processing или done — только system message в order's Чат (§5.5)). Письма, которые посылает эта область, являются продавцом.'s "new order", покупатель's "order "получено" и покупатель's "Вот твой купон".

Исходник: https://api.vizen.shop/docs/orders?full=1

Работает в демо-режиме