# Fullstack Test Shop

Демка · 8 сентября 2026

Тестовое задание реализовано как полный контур цифрового магазина: Angular-витрина, NestJS API, PostgreSQL через Prisma, webhook оплаты, два поставщика ключей и фоновый worker выдачи. В интерфейсе можно пройти покупку, повторить событие оплаты и проверить восстановление заказа после нехватки товара или сбоя поставщика.

Версия исходников: 767226bf3c2fbf6525f332522edc74925f869c11

История реализации: 1–3 сентября 2026. Подготовка портфолио: 7 сентября 2026.

## Задача

В рамках тестового задания требовалось собрать магазин цифровых товаров с каталогом, оформлением заказа и оплатой. Я расширил исходный сценарий до эксплуатационного: добавил идемпотентный inbox webhook, конкурентно безопасную выдачу ключей, повтор этапов и наблюдаемые состояния ошибок.

Покупка цифровых товаров. Здесь учебный вариант с моками поставщиков.

## Выполнено

- Витрина, карточка заказа и история статусов на Angular.
- API на NestJS, схема БД, воркер и HTTP-моки двух поставщиков.
- Обработка дублей и сбоев, проверки конкурентных запросов, деплой.
- Для демки добавил сессии, TTL, сброс своих данных и управляемый сбой заказа.

## Что отличает реализацию

Выдача выполняется однократно даже при повторных и параллельных webhook. Стабильный provider request ID переживает неоднозначный timeout; out_of_stock и delivery_failed восстанавливаются повторным worker-проходом. Ограничения БД и выборка FOR UPDATE SKIP LOCKED удерживают инварианты на уровне хранилища, а не только контроллера.

## Состав прикладного кода

- Angular / TypeScript: 32%
- NestJS: 30.4%
- Prisma / PostgreSQL: 18.4%
- Node.js delivery worker: 12.5%
- Node.js provider adapters: 3.8%
- TypeScript contracts: 2.9%

Доли по размеру прикладных файлов Git в закреплённой версии. Каждый файл отнесён к одной подсистеме по правилам scripts/generate-stack-share.mjs; тесты, ассеты, зависимости и generated-файлы исключены. Округление — до 0,1%. Это состав кода, а не уровень владения технологиями.

## Стек

| Технология | Роль |
| --- | --- |
| Angular / TypeScript | Витрина, сигналы состояния, HTTP-клиент и polling заказа. |
| NestJS | Контракты, валидация, webhook, операции восстановления и границы демо. |
| PostgreSQL / Prisma | Снимки денег, inbox, очередь с lease, уникальные индексы и транзакции. |
| Node.js worker | HTTP-вызовы поставщиков вне короткой транзакции, retry и финальная выдача. |

## Архитектура

- Цена и скидка вычисляются на сервере и фиксируются в заказе.
- Idempotency-Key защищён уникальным индексом; входные данные повтора сверяются с исходным намерением.
- Webhook сначала сохраняется в durable inbox. Отдельный worker обрабатывает платёж и очередь выдачи.
- Стабильный requestId поставщика сохраняется при повторе. Неоднозначный timeout не разрешает немедленно выдать второй код.

### Компоненты опубликованного демо

![Компоненты опубликованного демо](https://komaroff-dev.ru/evidence/test-shop/diagrams/components.svg)

[Mermaid](https://komaroff-dev.ru/evidence/test-shop/diagrams/components.mmd)

### Основной сценарий

![Основной сценарий](https://komaroff-dev.ru/evidence/test-shop/diagrams/scenario.svg)

[Mermaid](https://komaroff-dev.ru/evidence/test-shop/diagrams/scenario.mmd)

### Размещение опубликованной версии

![Размещение опубликованной версии](https://komaroff-dev.ru/evidence/test-shop/diagrams/deployment.svg)

[Mermaid](https://komaroff-dev.ru/evidence/test-shop/diagrams/deployment.mmd)

### Состав данных и границы хранения

![Состав данных и границы хранения](https://komaroff-dev.ru/evidence/test-shop/diagrams/data.svg)

[Mermaid](https://komaroff-dev.ru/evidence/test-shop/diagrams/data.mmd)

## 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 |

Перед изменением пройти связи экран → API → состояние → хранилище. Проверить входы, ошибки и повторные действия. Обновить карту и схемы вместе с кодом, выполнить связанные проверки и привязать материалы к версии.

## Сценарии

- Выберите товар и выполните успешную тестовую оплату. Дождитесь кода и просмотрите историю.
- Повторите то же платёжное событие: код и выдача должны остаться единственными.
- Создайте заказ через сценарий сбоя, оплатите и нажмите «Восстановить мой заказ».
- Проверьте отказ оплаты, чужой 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

[CI](https://github.com/komaroffsergei/fullstack-test-shop/actions/runs/34071005475) — 767226bf3c2fbf6525f332522edc74925f869c11, 2026-09-07T00:49:05Z; версия демо.

[Portfolio immutable image](https://github.com/komaroffsergei/fullstack-test-shop/actions/runs/34071005460) — 767226bf3c2fbf6525f332522edc74925f869c11, 2026-09-07T00:49:05Z; версия демо.

## Результаты

- Сохранено 14 содержательных кадров фактического интерфейса.
- Дата, сценарий и версия указаны у каждого кадра.
- Максимум RAM контейнеров в коротком тесте: 218.56 MiB. Общие службы учитываются отдельно; магазин включает свою БД.
- Измерено 6 повторений сценария; медиана 129.0 мс, максимум 1939 мс. Время включает браузер и HTTPS; методика и ограничения приложены.

![Суммарные ресурсы контейнеров проекта · 2026-09-07T01:08:27 — 01:11:22 UTC](https://komaroff-dev.ru/evidence/test-shop/charts/resources.svg)

[Данные](https://komaroff-dev.ru/evidence/test-shop/metrics/measurements.json)

![Измеренные повторения сценария через HTTPS · 2026-09-07T01:08:36 UTC; ограничения методики в JSON](https://komaroff-dev.ru/evidence/test-shop/charts/scenario-time.svg)

[Данные](https://komaroff-dev.ru/evidence/test-shop/metrics/measurements.json)

## Ограничения

- Оплата и поставщики — явные моки; деньги не списываются, коды учебные.
- До 20 заказов на сессию и 1000 новых демонстрационных заказов суммарно. TTL один час, очистка каждые пять минут.
- Пример восстановления намеренно завершает первую попытку одного заказа ошибкой. Он не меняет глобальные режимы поставщиков.

## Запуск и эксплуатация

У демки и исходного приложения одна модель заказов. Демо-guard включается через PORTFOLIO_DEMO=true. Без этого флага действуют исходные контракты API.

Сборка выполняется 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, на сервер уходят готовые файлы и образы с фиксированной версией. Секреты отдельно. Логи ротируются; после изменения log options контейнер нужно пересоздать. Общий docker prune не используется.

Code-map связывает экран или API с модулем и данными. При правке нужно проверить эти связи, обновить карту и прогнать затронутые сценарии: обычный проход, ошибку, повтор, сброс. Схема может устареть, поэтому её версия указана рядом.

Откат проверен 2026-09-07T01:07:46.255650+00:00: 20260907-verified → 20260907 → 20260907-verified. После каждого переключения подтверждены HTTPS, readiness и SHA. Это проверка конфигурации и артефактов на одном проверенном SHA; обратная миграция БД не выполнялась.

## Скриншоты

[Каталог товаров. Внешние сервисы замоканы.](https://komaroff-dev.ru/evidence/test-shop/full/01-overview.png) — 2026-09-07T00:57:57.813Z, 767226bf3c2fbf6525f332522edc74925f869c11.

[Каталог цифровых товаров с серверными ценами](https://komaroff-dev.ru/evidence/test-shop/full/02-catalog.png) — 2026-09-07T00:57:58.255Z, 767226bf3c2fbf6525f332522edc74925f869c11.

[Заказ создан: публичный UUID, снимок цены и ожидание оплаты](https://komaroff-dev.ru/evidence/test-shop/full/03-order-created.png) — 2026-09-07T00:57:59.629Z, 767226bf3c2fbf6525f332522edc74925f869c11.

[Повтор до оплаты отклоняется без создания события](https://komaroff-dev.ru/evidence/test-shop/full/04-early-replay.png) — 2026-09-07T00:58:00.295Z, 767226bf3c2fbf6525f332522edc74925f869c11.

[Оплата прошла настоящий webhook, worker выдал учебный код](https://komaroff-dev.ru/evidence/test-shop/full/05-delivery.png) — 2026-09-07T00:58:02.498Z, 767226bf3c2fbf6525f332522edc74925f869c11.

[Сохранённая история переходов заказа](https://komaroff-dev.ru/evidence/test-shop/full/06-history.png) — 2026-09-07T00:58:02.864Z, 767226bf3c2fbf6525f332522edc74925f869c11.

[Повтор того же event_id не меняет выданный код](https://komaroff-dev.ru/evidence/test-shop/full/07-replay.png) — 2026-09-07T00:58:04.146Z, 767226bf3c2fbf6525f332522edc74925f869c11.

[Чужой заказ недоступен во второй независимой сессии](https://komaroff-dev.ru/evidence/test-shop/full/08-isolation.png) — 2026-09-07T00:58:05.020Z, 767226bf3c2fbf6525f332522edc74925f869c11.

[Отказ оплаты сохраняется как отдельное конечное состояние](https://komaroff-dev.ru/evidence/test-shop/full/09-payment-declined.png) — 2026-09-07T00:58:06.962Z, 767226bf3c2fbf6525f332522edc74925f869c11.

[Управляемый сбой первой выдачи только этого заказа](https://komaroff-dev.ru/evidence/test-shop/full/10-delivery-failure.png) — 2026-09-07T00:58:08.600Z, 767226bf3c2fbf6525f332522edc74925f869c11.

[Повтор через исходную очередь завершил выдачу](https://komaroff-dev.ru/evidence/test-shop/full/11-recovered.png) — 2026-09-07T00:58:09.782Z, 767226bf3c2fbf6525f332522edc74925f869c11.

[Свой сброс удалил заказы и вернул витрину к исходному состоянию](https://komaroff-dev.ru/evidence/test-shop/full/12-reset.png) — 2026-09-07T00:58:10.315Z, 767226bf3c2fbf6525f332522edc74925f869c11.

[Мобильная витрина и описание границ демо](https://komaroff-dev.ru/evidence/test-shop/full/13-mobile-store.png) — 2026-09-07T00:58:10.742Z, 767226bf3c2fbf6525f332522edc74925f869c11.

[Мобильная карточка заказа до оплаты](https://komaroff-dev.ru/evidence/test-shop/full/14-mobile-order.png) — 2026-09-07T00:58:11.803Z, 767226bf3c2fbf6525f332522edc74925f869c11.

[Материалы](https://komaroff-dev.ru/downloads/test-shop-evidence.zip)
