# Fullstack Test Shop

Магазин цифровых товаров с идемпотентными заказами, платёжным inbox и очередью выдачи. В демо можно пройти покупку, повторить событие и восстановить заказ после управляемого сбоя.

Статус: passed

Версия: `767226bf3c2fbf6525f332522edc74925f869c11`. История реализации: 1–3 сентября 2026. Подготовка портфолио: 7 сентября 2026.

## Задача

Повторный клик, повтор webhook и сбой поставщика не должны создавать второй заказ или повторную выдачу. Состояние операции должно оставаться понятным покупателю и оператору.

## Аудитория

Покупатели цифровых товаров и разработчики платёжных интеграций.

## Личный вклад

- Angular-витрина, карточка заказа и история переходов.

- NestJS API, модель PostgreSQL, worker и HTTP-моки двух поставщиков.

- Идемпотентность, восстановление, конкурентные проверки и production-развёртывание.

- Для портфолио: подписанные сессии, собственный сброс, TTL и управляемый отказ одного заказа.

## Решения

- Цена и скидка вычисляются на сервере и фиксируются в заказе.

- Idempotency-Key защищён уникальным индексом; входные данные повтора сверяются с исходным намерением.

- Webhook сначала сохраняется в durable inbox. Отдельный worker обрабатывает платёж и очередь выдачи.

- Стабильный requestId поставщика сохраняется при повторе. Неоднозначный timeout не разрешает немедленно выдать второй код.

## Границы и ограничения

- Оплата и поставщики — явные моки; деньги не списываются, коды учебные.

- До 20 заказов на сессию и 1000 новых демонстрационных заказов суммарно. TTL один час, очистка каждые пять минут.

- Пример восстановления намеренно завершает первую попытку одного заказа ошибкой. Он не меняет глобальные режимы поставщиков.

## Что попробовать

- Выберите товар и выполните успешную тестовую оплату. Дождитесь кода и просмотрите историю.

- Повторите то же платёжное событие: код и выдача должны остаться единственными.

- Создайте заказ через сценарий сбоя, оплатите и нажмите «Восстановить мой заказ».

- Проверьте отказ оплаты, чужой UUID в другой сессии и сброс своего примера.

## Проверки

- Real HTTP webhook, durable inbox, worker, single fulfillment preserved on event replay

- Foreign GET/recover return 404; other session reset preserves first order

- Payment failure; isolated first-attempt delivery fault; own recovery succeeds

## Результаты

- Сохранено 14 содержательных кадров фактического интерфейса.

- Дата, сценарий и версия указаны у каждого кадра.

- Максимум RAM контейнеров в коротком тесте: 218.56 MiB. Общие службы учитываются отдельно; магазин включает свою БД.

- Измерено 6 повторений сценария; медиана 129.0 мс, максимум 1939 мс. Время включает браузер и HTTPS; методика и ограничения приложены.

## Запуск и эксплуатация

- Исходный и публичный контуры используют одну бизнес-модель. Демонстрационный guard включается только PORTFOLIO_DEMO=true; вне этого режима исходные контракты сохраняются.

- Сборка выполняется GitHub Actions из закреплённого commit SHA. На VPS загружается готовый GHCR-образ. База магазина остаётся отдельной от новых приложений.

- Запуск: docker compose --env-file .env.production -f compose.production.yaml up -d. До переключения выполнить prisma migrate deploy. Секреты хранятся только на сервере. Seed нельзя использовать как безусловный сброс данных работающего магазина.

- Readiness: /api/health/ready проверяет PostgreSQL. Во время инцидента сравнить историю заказа, inbox и delivery job; повторять выдачу по исходному orderId, не создавать новую покупку.

- Общие правила эксплуатации: готовые файлы и закреплённые образы собираются вне VPS; секреты не входят в артефакты. Общесерверный docker prune не применяется. Логи backend ограничены ротацией, изменения log options требуют пересоздания контейнера.

- Перед изменением пройти code-map от функции к экрану/API, модулю и данным. После изменения обновить карту и схемы, выполнить связанные сценарии, включая ошибку, повтор и сброс. Источник истины — конкретная версия кода, а не старая диаграмма.

- Откат проверен 2026-09-07T01:07:46.255650+00:00: 20260907-verified → 20260907 → 20260907-verified. После каждого переключения подтверждены HTTPS, readiness и SHA. Это проверка конфигурации и артефактов на одном проверенном SHA; обратная миграция БД не выполнялась.

## Стек и роль

- **Angular / TypeScript**: Витрина, сигналы состояния, HTTP-клиент и polling заказа.
- **NestJS**: Контракты, валидация, webhook, операции восстановления и границы демо.
- **PostgreSQL / Prisma**: Снимки денег, inbox, очередь с lease, уникальные индексы и транзакции.
- **Node.js worker**: HTTP-вызовы поставщиков вне короткой транзакции, retry и финальная выдача.

## Code-map

|Функция|Экран / API|Модуль|Данные / проверка|
|---|---|---|---|
|Покупка|Storefront / POST orders|storefront.component; ShopService.createOrder|orders, products, promocodes; race tests|
|Оплата|POST payments/simulate → webhooks/payment|PaymentsController; acceptWebhook|payment_events; повтор event_id|
|Выдача|Order / GET orders/:id|worker.service processDeliveryJob|delivery_jobs, provider_requests, fulfillments; recovery|
|Сессия|Guard всех публичных заказов|portfolio-demo PortfolioGuard|HMAC cookie, prefix owner; tests/portfolio/run.ts|
|Сброс / TTL|POST demo/reset / timer|PortfolioCleanup.clear|только portfolio orders; ownership / expiry tests|

## Схемы

- Компоненты опубликованного демо — `diagrams/components.mmd`
- Основной сценарий — `diagrams/scenario.mmd`
- Размещение опубликованной версии — `diagrams/deployment.mmd`
- Состав данных и границы хранения — `diagrams/data.mmd`

## Индекс изображений

- 01-overview: Опубликованная витрина: реальные компоненты и явные моки; 2026-09-07T00:57:57.813Z; SHA 767226bf3c2fbf6525f332522edc74925f869c11
- 02-catalog: Каталог цифровых товаров с серверными ценами; 2026-09-07T00:57:58.255Z; SHA 767226bf3c2fbf6525f332522edc74925f869c11
- 03-order-created: Заказ создан: публичный UUID, снимок цены и ожидание оплаты; 2026-09-07T00:57:59.629Z; SHA 767226bf3c2fbf6525f332522edc74925f869c11
- 04-early-replay: Повтор до оплаты отклоняется без создания события; 2026-09-07T00:58:00.295Z; SHA 767226bf3c2fbf6525f332522edc74925f869c11
- 05-delivery: Оплата прошла настоящий webhook, worker выдал учебный код; 2026-09-07T00:58:02.498Z; SHA 767226bf3c2fbf6525f332522edc74925f869c11
- 06-history: Сохранённая история переходов заказа; 2026-09-07T00:58:02.864Z; SHA 767226bf3c2fbf6525f332522edc74925f869c11
- 07-replay: Повтор того же event_id не меняет выданный код; 2026-09-07T00:58:04.146Z; SHA 767226bf3c2fbf6525f332522edc74925f869c11
- 08-isolation: Чужой заказ недоступен во второй независимой сессии; 2026-09-07T00:58:05.020Z; SHA 767226bf3c2fbf6525f332522edc74925f869c11
- 09-payment-declined: Отказ оплаты сохраняется как отдельное конечное состояние; 2026-09-07T00:58:06.962Z; SHA 767226bf3c2fbf6525f332522edc74925f869c11
- 10-delivery-failure: Управляемый сбой первой выдачи только этого заказа; 2026-09-07T00:58:08.600Z; SHA 767226bf3c2fbf6525f332522edc74925f869c11
- 11-recovered: Повтор через исходную очередь завершил выдачу; 2026-09-07T00:58:09.782Z; SHA 767226bf3c2fbf6525f332522edc74925f869c11
- 12-reset: Свой сброс удалил заказы и вернул витрину к исходному состоянию; 2026-09-07T00:58:10.315Z; SHA 767226bf3c2fbf6525f332522edc74925f869c11
- 13-mobile-store: Мобильная витрина и описание границ демо; 2026-09-07T00:58:10.742Z; SHA 767226bf3c2fbf6525f332522edc74925f869c11
- 14-mobile-order: Мобильная карточка заказа до оплаты; 2026-09-07T00:58:11.803Z; SHA 767226bf3c2fbf6525f332522edc74925f869c11
