# The store wizard («Магазин»)

> The wizard card in the admin gallery (Site → Scenarios → «Create a scenario» → «Магазин») installs a complete YooKassa storefront in one run (~20 seconds). Read this when the project you are working on WAS installed by the wizard, or is about to be: what exists after the run, what is safe to edit by hand, and the traps. The owner-facing doc lives at `https://dynapi.ru/docs/shop-wizard`. Since v1.7.5 the wizard is flagged experimental in the admin UI (quiet «Экспериментальный» pill on the card, in the runner and on the plan preview — same for the landing wizard): expect the artifact set to evolve between versions.

## What one run installs (29 steps; the plan preview shows the exact op list before anything runs)

**Wizard steps (29):** `shop_name`, `currency` (the store currency, RUB by default), `palette` (the accent-color picker, 4 palettes shared with the landing wizard: indigo/emerald/graphite/amber; default indigo ≈ the storefront brand #4F46E5), `site_subdomain` (v1.7.7 — the storefront address on `<sub>.dynapi.ru`: live availability check while typing, claimed as the plan's FIRST op at install; empty = keep the current address, so a re-install never silently rebinds a live shop; taken names offer alternatives chips like `<name>-shop`), `site_base` (now OPTIONAL and hidden while a subdomain is claimed — the runner derives `https://<sub>.<base>` from the claim unless a URL was typed by hand), `store_path`, `provider` (yookassa | tbank — see the payment block below), `shop_id`, `secret_key`, `receipt_vat_code` (54-ФЗ receipt VAT rate: default `1` = без НДС; the enum carries 1–12 incl. 11 = 22% / 12 = 22/122 from 2026), `receipt_tax_system` (USN income by default; ОСН/ЕСХ/patent etc.), `hook_token`, `hook_token_tb`, `terminal_key`, `tb_password`, `receipt_tb_vat`, `receipt_tb_tax` (the five T-Bank-branch steps, shown only under `provider == tbank`), `delivery` (none | cdek — pulls the seven-step СДЭК branch: `cdek_from_city`, `cdek_from_city_code`, the test+prod credential secret pairs `cdek_account_test`/`cdek_password_test`/ `cdek_account_prod`/`cdek_password_prod`, and the webhook token `cdek_hook_token`), `page_size`, `smtp_host`, `smtp_user`, `smtp_password`. The palette answer substitutes `${variant.*}` tokens into `css/tokens.css` (6 accent tokens, light + dark pairs — same mechanism as the landing wizard); the storefront CSS uses only `var(--accent…)` from there on, so the whole storefront re-skins with the one answer. Legal texts (оферта, политика конфиденциальности) are NO LONGER file uploads: they are seeded `legal-doc` content rows rendered as markdown pages — edit them in Content, not in Files.

**Payment providers (v1.7.13, generic contract):** the wizard carries a `provider` step (`yookassa` default | `tbank`); a provider table in the wizard declarations is the single source of each pack (pay flow + webhook + `cfg.payment_provider` flip + provider line of the notify) and gates every pack artifact with `shows_if: "provider == <slug>"` — only the chosen pack installs. Checkout ends in `r_dispatch`: `{ok: true, redirect: "<store_path>/api/pay/<cfg.payment_provider ?? yookassa>?order=<id>"}` (the storefront JS follows any `{ok, redirect}` — no storefront change). The pay flow finds the order by `?order=`, guards `status == "new"`, pays the ORDER ROW's amount, writes only `payment_id`, and 303s to the provider page (or back to `/order/<id>?payment=failed`). Repeat payment = re-visiting the pay route (a fresh Idempotence-Key per visit) — the old `shop_payment_retry` flow/route is gone; the order-status «Повторить оплату» button navigates to the active provider's pay route. Webhook tokens are per-provider (`hook_token` / `hook_token_tb`) so the previous provider's webhook stays mounted after a switch and in-flight payments still flip. Switching provider = reinstall (the `entity_updates` flip runs every run) or editing `payment_provider` in Content when both packs are present. Tbank Init signs `Token` as sha256 over ALL 8 root parameters (Amount, Description, FailURL, NotificationURL, OrderId, Password, SuccessURL, TerminalKey) in alphabetical order (`sha256_hex`/`secret()` in value-expr since v1.7.11); its receipt is opt-in (`receipt_tb_vat`, default `off`) and never enters the token.

**54-ФЗ receipts:** the YooKassa pack's pay flow carries a `receipt` — ONE position for the whole order (position amount = payment amount), buyer contact = `order.customer_email` (the pay flow's `g_order` selects it). Receipt rides ONLY for RUB: the body_expr is a currency ternary (`cfg.currency == "RUB" ? {…receipt…} : {old body}`) — no null members ever reach the JSON. When чеки are disabled in the YooKassa cabinet the receipt is silently ignored, so the opt-out costs nothing.

**Data types (13):** `product` (`photos` is `entity[]` + `entity_ref: "media"`, `categories` a string array of category slugs, price, an optional `sale_price_kopecks` — the sale price, see "Sale pricing" below — an `is_hidden` hide switch, see "Hiding products and variants" below — and status among them, SEO `meta_title`/`meta_description`, `features` string array), `variant` (a product's option row — `product_slug` + `label`/`options`/`sku`/`price_kopecks`/`stock`/`photos`/ `sort`/`is_hidden`: its own price, stock, photos and hide flag per variant), `category` (tree — `parent` self-references `entity_ref: "category"`), `delivery-method` (`title`/`type`/`price_kopecks`/`description`/`sort`/`active` — the checkout's courier/pickup/СДЭК list), `promo` (`code`, percent or fixed `value`, `expires_ts`, `max_uses`/`used_count`, `active`), `order` (`amount` is a STRING with a `^[0-9]+([.][0-9]{1,2})?$` pattern; `member_id` is required with a `""` default — guest orders carry the empty string), `cart` (`title` / `owner_id` / `items` json), `review` (`product_slug`/`product`, `author_name`, `rating`, `body`, `status` — new rows land un-published, the owner publishes by flipping the status), `favorite` (`owner_id` + `slugs`), `cdek-config` (the СДЭК delivery settings row — city/city code, test+prod accounts, widget mode; created seeded even when СДЭК is off), `address` (a delivery pickup point the buyer picks at checkout), `legal-doc` (`title`/`slug`/`body_markdown` — the оферта and политика rows rendered as markdown pages at `<store-path>/offer` and `<store-path>/privacy`), and `shop-config` — ONE settings row with 24 named fields grouped by prefixes (Магазин: `name`/`slogan`/`description`/`legal`/`site_url`/`og_image`; Контакты: `email`/`phone`/`address`/`work_hours`/`telegram`/`social_link`; Главная/hero: `hero_kicker`/`hero_title`/`hero_subtitle`; Кнопки/CTA: `cta_add_to_cart`/`cta_checkout`/`cart_empty_title`/`filters_show`; Каталог: `filters_visible` — how many category chips show before the «Ещё N» expander, empty/0/garbage falls back to 5; plus `currency`/`payment_provider` and `has_offer`/`has_tos`, the flags that mirror the legal pages). The old `ui-text` dictionary entity was liquidated (126 seeded key/value rows, two-page fetch, compute merge): interface labels are now template fallbacks — templates read `cfg.<field> | default: "…"`, so an EMPTY field renders the built-in default and the owner overrides only what they want in one place. The seed is 4 categories, 5 demo products (published; the fifth, `termokruzhka`, has `stock: 0` — the «Нет в наличии» demo; no photos in the seed — 10 demo photos ride ONE media op linking them by slug), 3 delivery methods (courier, pickup, СДЭК), 2 legal-doc rows (оферта + политика), 1 address and 1 `cdek-config` row, and one `shop-config` row carrying only the wizard answers (`name`, `site_url`). One `owner_id` unifies member and guest carts (the sealed `shop_cart` cookie carries the anon id; signing in merges the guest cart into the member's, max-quantity per line).

**Scenarios (26 declared, 21 or 24 install):** the storefront set — `shop_catalog` (catalog + pagination), `shop_category` (category page), `shop_product` (card + related), `shop_checkout_page` (SSR checkout), `shop_checkout_api` (server-side money: cart×catalog lines at the effective — sale — price, delivery/promo, order snapshot, title, 2 emails, cart cleanup, then the provider dispatcher `r_dispatch`; a checkout failure is an honest `{ok: false, error}` respond_json — the ORDER is not created), `shop_order_status` (`/order/:id` page), `shop_cart` (GET+POST JSON API behind `js/cart.js`), `shop_review` (the moderated review endpoint the product card posts to), `shop_favorites_page` + `shop_favorites_api` (member favorites page + JSON API behind `js/favorites.js`), `contacts_page` (the contacts page at `<store-path>/contacts` fed from shop-config + the address rows), `shop_legal_offer` + `shop_legal_privacy` (the markdown legal pages behind the `shop_legal_doc` query), `shop_404` — the provider pack — `shop_pay_yookassa` + `shop_payment_webhook` or `shop_pay_tbank` + `shop_webhook_tbank` under `provider == <slug>` gates (pay flow: find by `?order=`, guard `new`, pay the row amount, write `payment_id`, 303; webhook: verify (YooKassa re-fetch / Tbank signature), mark paid) — the optional СДЭК trio under `delivery == cdek` (`shop_delivery_cdek_proxy` — the rates-widget proxy, `shop_cdek_webhook` — status callbacks, `shop_cdek_admin` — per-order operations); plus `shop_root_redirect` (GET `/` → 303 `<store_path>/`; temporary — when the owner plants a landing they replace the `/` route binding in Scenarios) and 4 member scenarios adapted from the presets: `login`, `register`, `logout`, `account`.

**Routes (27 declared):** `<store_path>` and `<store_path>/` (the catalog answers both), `<store_path>/category/:slug` and `<store_path>/product/:slug` (each carries the `sitemap_query`), `<store_path>/review` (POST — the review form endpoint), `<store_path>/favorites` + `<store_path>/api/favorites`, `<store_path>/checkout`, `<store_path>/api/checkout` (POST, TTL 0), `<store_path>/api/webhook/<hook_token>` (POST), `<store_path>/api/pay/<provider>` (GET, TTL 0 — per-provider pack, `hook_token_tb` for the tbank webhook), `<store_path>/order/:id`, `<store_path>/api/cart` (GET+POST, TTL 0), `<store_path>/offer`, `<store_path>/privacy` (the markdown legal pages), `<store_path>/contacts`, the СДЭК routes under `delivery == cdek` (`<store_path>/api/delivery/cdek/service` — the widget proxy, `<store_path>/api/cdek/webhook/<cdek_hook_token>`, `<store_path>/api/cdek/admin`), `/404`, `/` (root redirect), and the member routes on the ROOT — `/login`, `/register`, `/logout`, `/account` (`members_only` → `/login`). The store path is a wizard step: `/shop` (default), `/catalog`, or `/store`. Storefront routes pin `member_cookie: "member"` — that is what makes the SSR header (guest vs member menus) cache-safe: signed-in visitors are never served cached pages.

**Templates (22) + statics (12):** base/catalog/category/product/checkout/order-status/404/login/register/ account/favorites/contacts/legal pages + the pure-function partials `breadcrumbs` (nav + JSON-LD), `product-card` (sale strikethrough + «−N%» badge), `stock-badge`, `applied-filters`, `pagination` (the catalog page window), `currency` + the order emails (`email-layout`, `email-order-customer`, `email-order-vendor`); `css/tokens.css`, `css/shop.css`, `js/cart.js`, `js/picker.js`, `js/catalog.js`, `js/favorites.js`, `js/auth.js`, `js/dropdown.js`, `js/theme.js`, `js/cdek.js` (the СДЭК widget, with the delivery branch), `photos/placeholder.svg`, `favicon.svg`. `css/shop.css` is MOBILE-FIRST — the 320px base is the contract (grid micro-steps 30/62rem; the single MAIN desktop breakpoint is 48rem = the landing-canon 768px, where the root scale `html { font-size: calc(100vw/90) }` from tokens.css carries the proportional zoom; zero horizontal scroll at 320). The storefront follows the landing-canon design language — the full visual contract is the "Design canon" section below; the horizontal-hairline policy is «exactly three: header, footer, catalog toolbar — no other horizontal rules».

**Library queries (25):** 2 sitemap enumerations (`shop_products_sitemap` — skips hidden products, `shop_categories_sitemap`) + 23 storefront reads — catalog categories/products/count/all (chip counters), category page + children, product card/ variants/reviews/related, checkout cart/products, delivery methods + addresses (`shop_addresses` — the pickup points), order by id, cart get/merge rows, member login/register lookups, account orders, favorites get/rows, the legal page fetch (`shop_legal_doc` — by slug, feeds both legal pages), the СДЭК settings read (`shop_cdek_config`), and `shop_site_common` — the ONE settings fetch (`shopConfigs(limit: 1)`, named fields incl. `currency` and `payment_provider`) every page flow hoists as `g_site`.

**Secrets (2+1 for yookassa, 1+1 for tbank, +4 when СДЭК on):** `yookassa_secret_key` (the raw key) + the composite `yookassa_basic` = `Basic ` + base64(`<shop_id>:<key>`) — the YooKassa flow nodes read ONLY `secret://yookassa_basic`; the tbank pack carries `tbank_password` (terminal password, used via `secret("tbank_password")` in the Init token and the webhook signature); the СДЭК branch adds the test+prod pairs `cdek_account_test`/`cdek_password_test`/`cdek_account_prod`/ `cdek_password_prod`; plus the optional `shop_smtp_password` (left empty, the store installs and takes orders without mail). `terminal_key` is a TEXT step, not a secret — it rides the Init body; СДЭК credentials are OPTIONAL — without them the store works, the widget just calculates no rates.

**The owner checklist rides the plan as the last op (notify, 8 items, one per provider):** (1) paste the webhook URL `<site_base><store_path>/api/webhook/<hook_token>` into the provider's cabinet (YooKassa: Настройки → HTTP-уведомления; Tbank: EACQ → Уведомления, per-provider token); (2) demo photos are copied into the media library at install — replace them with your own and re-attach; (3) fill SMTP (wizard steps or the letter nodes) — orders work without it; (4) the shop name, contacts and storefront texts are edited in Content → «Настройки магазина» — one record, empty field = the template's built-in fallback; (5) version updates ride a RE-RUN: missing type fields are appended and file patches applied — but flow docs and library queries are refreshed to canonical (see wizards.md, "Re-runs"), so export customizations first; (6) member cart/account: do NOT rename the `member` cookie.

The install does NOT burn the content API quota — schema/flow/site/secret management mutations are quota-exempt (conventions.md, Quota model). Only the 24 demo seed rows (4 categories + 5 products + 2 variants + 3 delivery methods + 2 promos + 3 reviews + 2 legal docs + 1 address + 1 СДЭК config + 1 settings row) meter as content writes.

## Sale pricing

Every product carries an optional `sale_price_kopecks` (minor units of the shop currency, same scale as `price_kopecks`; empty or 0 = no sale). The contract, end to end:

- **The sale price replaces the effective price wholesale.** Checkout math is `sale > 0 ? sale : (variant.price_kopecks ?? product.price_kopecks)` — a set sale price overrides even a variant's own price. The order snapshot fixes the charged amount, so the cart line, the SSR checkout, the order-status page and the product JSON-LD all carry the price the buyer will actually pay.
- **The strikethrough "old price" is what would apply without the sale** — `variant.price_kopecks ?? product.price_kopecks`. Cards and the checkout draw it struck through with a quiet tinted «−N%» badge (N is the floored percentage); a product whose sale price is not below its base price draws neither the strikethrough nor the badge.
- **A fake discount is guarded against display.** A sale value at or above the base price never renders as a discount: cards and the product page fall back to the plain base price — no strikethrough, no «−N%» badge (the pair only draws when the old price is strictly higher than the sale). The field itself is still stored as typed — the engine does not validate it, and the checkout math reads it as entered — so a nonsense value hides in plain sight instead of failing loudly. Enter a value that is actually lower than the base price.
- **No «Распродажа» page.** The old dedicated sale section (`<store-path>/sale` and its queries) was removed — discounted goods are surfaced by per-product sale pricing and promo codes, not by a separate grid page. On tenants installed before the removal the page may still exist as ordinary tenant material; it is not part of the current install.

Editing is ordinary content work: set «Цена распродажи» on the product row in Content; badges, checkout math pick the change up on the next render.

## Hiding products and variants

Both `product` and `variant` carry an optional `is_hidden` boolean (the «Скрыт» toggle in Content; empty = visible, so rows created before the field keep rendering). Hiding is a storefront edit, not a deletion — the row stays in Content and in already-placed orders.

- A hidden product drops out of every storefront read: catalog and category grids, the chip counters, the sale section, related products, the sitemap and the favorites page. Its direct URL renders the ordinary «not found» page, like a slug that never existed.
- A hidden variant is not rendered at all: no swatch in the option picker, and combinations with it are not offered. An out-of-stock variant that is NOT hidden behaves differently — it stays visible but muted, with the buy button inactive.
- The checkout is guarded too: a cart line pointing at a hidden product or variant dies at checkout with an honest «доступно 0» (0 available) stock error instead of charging a price for goods the storefront no longer sells.
- The catalog filters are cumulative: filter chips carry the whole current state at once (category + search + in-stock + sort), and the «в наличии» chip (`?stock=1`) limits the grid to products actually in stock.

## The query library is live-linked

Every storefront READ graphql node carries `library_name` — the query text is resolved from the project's library AT EXECUTION TIME (flows-guide.md §3, "the query library is the source of truth"). The inline copy in the node is an editor snapshot the executor ignores while the link stands. Consequences for hand work on a wizard store:

- Editing a library entry (`flowctl query save` + `publishSiteQuery`, or the UI row button) changes EVERY linked scenario on the next render — no flow republishes.
- Node-side `variables`/`variables_expr` are link-independent — they apply to the resolved text exactly as to an inline one (the catalog node keeps its paging `variables_expr` while the text lives in the library).
- A referenced entry cannot be deleted — 409 `SITE_QUERY_IN_USE` ("query is referenced by flows") — so a link never dangles silently.
- Deliberately INLINE (do not "fix" this): the cart/order mutations (their payload shape is part of the node logic) and the money-math reads (the server-side amount calculation must not drift when the owner reshapes a display query).
- **REINSTALL OVERWRITES the 25 `shop_*` entries with the factory texts (upsert + publish) — owner customizations of the library are lost.** Re-run the wizard only deliberately, and re-apply custom query edits afterwards.

## Storefront design canon

The storefront's visual language is a CONTRACT — edits to its templates and CSS keep these rules, whether or not a guard is watching (a CI hygiene check greps the same rules upstream; on a live tenant nothing greps you, which is exactly why the rules are written here):

- **Color only through the tokens.** `css/tokens.css` defines `--accent*` and the `--shop-*` scale (light + dark pairs); every rule in `css/shop.css` consumes `var(--…)`. No raw hex, no ad-hoc color literals in templates or CSS — a new color is a new token pair, then usage.
- **Spacing and shadows only from the rem scale.** `margin`, `padding`, `box-shadow` (and `gap`) take variables from the rem-based scale — a raw `px` value in those properties is a canon violation. `1px` hairline borders are the sanctioned exception.
- **Hairlines are structural, not decorative.** Horizontal separator lines exist only on the header (border-bottom), the footer (border-top) and the sticky toolbar — «exactly three: header, footer, catalog toolbar — no other horizontal rules» is the policy. Do not introduce card/section separators.
- **Dark theme: no card shadows.** On `dynamica-dark` cards and panels separate with border + surface fill — shadows are removed there (`--shop-shadow-*` resolves to none). Do not re-add shadow depth for dark; contrast comes from the border.
- **The theme switch is parent-domain cookie + `data-theme`.** `js/theme.js` sets `<html data-theme="dynamica-dark|dynamica-light">` and persists the choice in a `dynamica-theme` cookie on the PARENT domain (shared with the rest of the platform). Server templates must not hardcode a theme; test both themes on the live/preview host — a light-only screenshot review misses half the surface.
- **The product gallery is native scroll-snap.** Horizontal swipe is compositor-driven CSS (`scroll-snap-type`), not a JS carousel — do not attach a gallery library without measuring; it would fight the snap and the touch ergonomics.
- **Motion respects `prefers-reduced-motion`** — every transition/animation the storefront ships has a reduced-motion fallback; new animation follows the same guard.
- **WCAG AA is the floor**: contrast ratios, visible focus states, focus traps in dialogs, `aria-live` on cart/status updates. A change that fails AA is a bug, not a style choice.

## YooKassa key re-issue — no reinstall

After re-issuing the secret key, update BOTH secrets in Site → Secrets (upsert by name, write-only):

1. `yookassa_secret_key` — the raw key;
2. `yookassa_basic` — rebuild the composite: `Basic ` + base64 of `<shop_id> + ":" + <key>`.

The flows read only `yookassa_basic`, so refreshing the raw key alone changes nothing. Reinstalling the wizard is NOT needed (and costs the library overwrite above).

## Traps

### A cancelled install leaves a shadow scenario

Cancelling the wizard mid-run can leave the empty container scenario the gallery created, still bound to the route — the final pass that detaches it never ran, and an empty unpublished scenario on a live route renders 404, shadowing the storefront (the catalog 404s while `<store_path>/product/:slug` works). Fix: delete the scenario, then RE-SAVE the route with only the real bindings (`deleteFlow` does NOT sweep bindings out of the draft/live route — deleting the flow alone is not enough), then publish the route. On v1.5.2+ the normal, non-cancelled path removes the container itself.

### `files push` of a single file lands at the static root

`flowctl files push css/shop.css` publishes the file UNDER ITS BASENAME at the static root (`shop.css`) with a green «✓ published» — while the live `css/shop.css` keeps the old bytes. Always push the parent DIRECTORY (paths relative to it): `flowctl files push <stage>/static --publish`. Diagnose a suspect push with `siteStaticFile(path, draft: …)` hashes and `sitePublishStatus.lastPublishedAt`.

### Wizard-processed files carry the store path

The templates/statics ON THE SITE have the store path substituted; the shipped source files carry the `__SHOP_BASE__` sentinel instead. Re-pushing a raw source file puts the sentinel on the live site — `js/cart.js` starts fetching `<store_path>/__SHOP_BASE__/api/cart` and the console fills with 404s (the cart is dead while every page renders fine). Before pushing any template or js file taken from a source pack, stage the substitution (`__SHOP_BASE__` → the project's actual store path).

### tokens.css is palette-substituted per tenant — never push the placeholder source

`css/tokens.css` is a wizard-processed file too: at install the wizard writes the tenant's palette answer into it, while the shipped source still carries the raw `${variant.*}` placeholders (6 accent tokens, light + dark pairs). Pushing the source over the live copy replaces every accent token with a literal `${variant.accent}` string — `--accent` dies and primary buttons, active chips and focus rings go transparent site-wide while the layout keeps rendering fine. Before ANY static push, grep the staged files for `${variant.` — a hit means STOP: pull the live copy first (`siteStaticFile(path)` or `curl`) and push from that. The «source == live» byte guard (next section) only proves something for files that are NOT substituted; for `tokens.css` the correct guard is «live == the tenant's substituted copy» — keep a hash of the live file taken right after install/palette change and compare against it.

### A push that "succeeded" can still ship stale bytes — verify source ↔ live

`files push` answers `✓` per file, but a page (or a CDN/browser cache in front of it) may keep serving the previous bytes: the classic case is `css/tokens.css`/embedded styles looking unchanged on the live site right after a deliberate re-skin push. Close every styling/template push with a byte comparison, not a visual glance: pull the live copy (`siteTemplate(path)` / `siteStaticFile(path)` — both default to the live side — or `curl https://<sub>.<base>/<path>`) and diff it against the staged source — `cmp`/hash, not eyeball. Also remember `?v={{ site.version }}` (SKILL.md golden rules): if a template references a static WITHOUT the version param, browsers can hold the old bytes for an hour after a correct push.

### Teardown and reinstall

`deleteEntity` cascades the data rows (one transaction, since v1.5.2), so a full teardown is (flowctl drives the whole list):

1. `flowctl flow delete <slug>` ×21–24 (the visible branch — 26 declared minus the unused provider's pair, minus the СДЭК trio when delivery is off; `flowctl flow list` is the exact inventory; an empty `shop` container from a cancelled earlier run goes too);
2. `flowctl route delete </path>` for every route, then `flowctl site publish` (route deletes are draft tombstones — a single publish takes them all live);
3. `flowctl files delete <path>` for every template and static (`flowctl template list` + `static list` first — both copies go away, so nothing resurrects);
4. `flowctl query delete <name>` for every library entry, then `flowctl site publish` (the tombstone recipe — a live-only delete resurrects on the next `publishSite`);
5. `flowctl entity delete-type <slug> --yes` ×13 (product, variant, category, delivery-method, promo, order, cart, review, favorite, cdek-config, address, legal-doc, shop-config — the type AND every row go in one cascade; the command prints the dry picture before `--yes`).

Run steps with pauses — the teardown is a long management-mutation run and the per-minute cap answers `SITE_RATE_LIMITED` to a hammering loop (conventions.md, abuse caps). Keep the `yookassa_*` secrets when reinstalling — write-only values cannot be read back, and the wizard will just re-confirm the overwrite in the UI. A reinstall re-seeds the demo rows and overwrites the library entries (above).
