# Fullstack Test Shop

**Demo · 8 September 2026**

The test assignment is implemented as a complete digital shop flow: Angular storefront, NestJS API, PostgreSQL via Prisma, payment webhook, two key providers and a background fulfillment worker. The UI covers purchase, repeated payment events and recovery after stock or provider failures.

Source version: `767226bf3c2fbf6525f332522edc74925f869c11`

Implementation history: 1–3 September 2026. Portfolio preparation: 7 September 2026.

[Project notes](https://komaroff-dev.ru/projects/test-shop/) · [Online documentation](https://komaroff-dev.ru/projects/test-shop/docs/)

[Demo](https://test-shop.komaroff-dev.ru/)

[Source repository](https://github.com/komaroffsergei/fullstack-test-shop)

## Task

The assignment asked for a digital goods shop with catalog, checkout and payment. I extended the base scenario with an idempotent webhook inbox, concurrency-safe key fulfillment, stage retries and observable failure states.

Digital goods checkout. This is a demo with mocked suppliers.

## Work done

- Angular storefront, order page and status history.
- NestJS API, database schema, worker and HTTP mocks for two suppliers.
- Duplicate and failure handling, concurrent request checks, deployment.
- Added sessions, TTL, a reset for the visitor's own data and a controlled order failure for the demo.

## What sets the implementation apart

Fulfillment happens once even under repeated or concurrent webhooks. A stable provider request ID survives an ambiguous timeout; out_of_stock and delivery_failed recover on a later worker pass. Database constraints and FOR UPDATE SKIP LOCKED enforce invariants at the storage layer.

## Application-code composition

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

Shares use Git application-file bytes at pinned revisions. Each file belongs to one subsystem under scripts/generate-stack-share.mjs rules; tests, assets, dependencies and generated files are excluded. Rounded to 0.1%. This describes code composition, not proficiency.

## Stack

| Technology | Role |
| --- | --- |
| Angular / TypeScript | Storefront, state signals, HTTP client and order polling. |
| NestJS | Contracts, validation, webhooks, recovery operations and demo limits. |
| PostgreSQL / Prisma | Monetary snapshots, inbox, leased queue, unique indexes and transactions. |
| Node.js worker | Provider HTTP calls outside short transactions, retries and final delivery. |

## Architecture and decisions

- Prices and discounts are calculated on the server and saved in the order.
- A unique index protects Idempotency-Key; replayed input is checked against the original intent.
- Webhooks are saved to a durable inbox first. A separate worker processes payments and the delivery queue.
- The provider requestId remains stable across retries. An ambiguous timeout does not immediately allow a second code to be issued.

### Components of the published demo

![Components of the published demo](https://komaroff-dev.ru/evidence/test-shop/diagrams/components.svg)

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

### Main workflow

![Main workflow](https://komaroff-dev.ru/evidence/test-shop/diagrams/scenario.svg)

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

### Deployment of the published version

![Deployment of the published version](https://komaroff-dev.ru/evidence/test-shop/diagrams/deployment.svg)

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

### Data structure and storage boundaries

![Data structure and storage boundaries](https://komaroff-dev.ru/evidence/test-shop/diagrams/data.svg)

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

## Code map

| Feature | Screen / API | Module | Data / verification |
| --- | --- | --- | --- |
| Purchase | Storefront / POST orders | storefront.component; ShopService.createOrder | orders, products, promocodes; race tests |
| Payment | POST payments/simulate → webhooks/payment | PaymentsController; acceptWebhook | payment_events; duplicate event_id |
| Delivery | Order / GET orders/:id | worker.service processDeliveryJob | delivery_jobs, provider_requests, fulfillments; recovery |
| Session | Guard for all public orders | portfolio-demo PortfolioGuard | HMAC cookie, prefix owner; tests/portfolio/run.ts |
| Reset / TTL | POST demo/reset / timer | PortfolioCleanup.clear | portfolio orders only; ownership / expiry tests |

Before changing a feature, trace its interface, API, state and storage dependencies. Check inputs, errors, retries and related workflows. Update the map and affected diagrams alongside the code, rerun the related checks and attach evidence for the source version.

## Workflows and API

1. Choose a product and complete a successful test payment. Wait for the code and inspect the history.
2. Replay the same payment event: the code and delivery must remain unique.
3. Create an order using the failure scenario, pay and select “Recover my order”.
4. Check a declined payment, another session's UUID and resetting your own example.

## Verification

- 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: CI](https://github.com/komaroffsergei/fullstack-test-shop/actions/runs/34071005475) — `767226bf3c2fbf6525f332522edc74925f869c11`, 2026-09-07T00:49:05Z; this demo's version.

[CI: Portfolio immutable image](https://github.com/komaroffsergei/fullstack-test-shop/actions/runs/34071005460) — `767226bf3c2fbf6525f332522edc74925f869c11`, 2026-09-07T00:49:05Z; this demo's version.

## Results and measurements

- 14 meaningful screenshots of the actual interface retained.
- Each screenshot includes its date, scenario and code revision.
- Peak container RAM in the short test: 218.56 MiB. Shared services are counted separately; the shop includes its own database.
- 6 scenario repetitions measured; median 129.0 ms, maximum 1939 ms. Timing includes the browser and HTTPS; methodology and limitations are included.

These checks apply to the listed version. Business metrics and model quality on an independent dataset were not measured.

![Total project container resources · 2026-09-07T01:08:27 — 01:11:22 UTC](https://komaroff-dev.ru/evidence/test-shop/charts/resources.svg)

[Total project container resources · 2026-09-07T01:08:27 — 01:11:22 UTC: source data](https://komaroff-dev.ru/evidence/test-shop/metrics/measurements.json)

![Measured scenario repetitions over HTTPS · 2026-09-07T01:08:36 UTC; methodology limitations in JSON](https://komaroff-dev.ru/evidence/test-shop/charts/scenario-time.svg)

[Measured scenario repetitions over HTTPS · 2026-09-07T01:08:36 UTC; methodology limitations in JSON: source data](https://komaroff-dev.ru/evidence/test-shop/metrics/measurements.json)

## Limitations

- Payments and providers are explicitly mocked; no money is charged and codes are for demonstration only.
- Up to 20 orders per session and 1,000 new demo orders in total. One-hour TTL, cleanup every five minutes.
- The recovery example deliberately fails the first delivery attempt for one order. It does not change global provider modes.

## Running and operating

The demo and original app use the same order model. PORTFOLIO_DEMO=true enables the demo guard. Without the flag, the original API contracts apply.

GitHub Actions builds a pinned commit SHA. A prebuilt GHCR image is uploaded to the VPS. The shop database remains separate from the new applications.

Start: docker compose --env-file .env.production -f compose.production.yaml up -d. Run prisma migrate deploy before switching releases. Secrets are stored only on the server. Do not use seed as an unconditional reset of a running shop.

Readiness: /api/health/ready checks PostgreSQL. During an incident, compare the order history, inbox and delivery job; retry delivery for the original orderId instead of creating another purchase.

Builds run outside the VPS; the server receives static files and versioned images. Secrets are kept separately. Logs rotate; changing log options requires recreating the container. No server-wide docker prune.

The code map links a screen or API to its module and data. A change needs those links checked, the map updated and the affected flows rerun: normal path, error, retry and reset. Diagrams can go stale, so their version is listed alongside them.

Rollback checked at 2026-09-07T01:07:46.255650+00:00: 20260907-verified → 20260907 → 20260907-verified. HTTPS, readiness and SHA were confirmed after each switch. This checks configuration and artifacts at one verified SHA; no reverse database migration was performed.

## Screenshot index

1. [Product catalogue. External services are mocked.](https://komaroff-dev.ru/evidence/test-shop/full/01-overview.png) — 2026-09-07T00:57:57.813Z, source `767226bf3c2fbf6525f332522edc74925f869c11`.

2. [Digital goods catalog with server-calculated prices](https://komaroff-dev.ru/evidence/test-shop/full/02-catalog.png) — 2026-09-07T00:57:58.255Z, source `767226bf3c2fbf6525f332522edc74925f869c11`.

3. [Order created: public UUID, price snapshot and payment pending](https://komaroff-dev.ru/evidence/test-shop/full/03-order-created.png) — 2026-09-07T00:57:59.629Z, source `767226bf3c2fbf6525f332522edc74925f869c11`.

4. [Replay before payment is rejected without creating an event](https://komaroff-dev.ru/evidence/test-shop/full/04-early-replay.png) — 2026-09-07T00:58:00.295Z, source `767226bf3c2fbf6525f332522edc74925f869c11`.

5. [Payment passed through the real webhook; the worker issued a demo code](https://komaroff-dev.ru/evidence/test-shop/full/05-delivery.png) — 2026-09-07T00:58:02.498Z, source `767226bf3c2fbf6525f332522edc74925f869c11`.

6. [Persisted order transition history](https://komaroff-dev.ru/evidence/test-shop/full/06-history.png) — 2026-09-07T00:58:02.864Z, source `767226bf3c2fbf6525f332522edc74925f869c11`.

7. [Replaying the same event_id leaves the issued code unchanged](https://komaroff-dev.ru/evidence/test-shop/full/07-replay.png) — 2026-09-07T00:58:04.146Z, source `767226bf3c2fbf6525f332522edc74925f869c11`.

8. [Another session cannot access this order](https://komaroff-dev.ru/evidence/test-shop/full/08-isolation.png) — 2026-09-07T00:58:05.020Z, source `767226bf3c2fbf6525f332522edc74925f869c11`.

9. [Payment rejection is saved as a separate terminal state](https://komaroff-dev.ru/evidence/test-shop/full/09-payment-declined.png) — 2026-09-07T00:58:06.962Z, source `767226bf3c2fbf6525f332522edc74925f869c11`.

10. [Controlled first-delivery failure affecting only this order](https://komaroff-dev.ru/evidence/test-shop/full/10-delivery-failure.png) — 2026-09-07T00:58:08.600Z, source `767226bf3c2fbf6525f332522edc74925f869c11`.

11. [Retry through the original queue completed delivery](https://komaroff-dev.ru/evidence/test-shop/full/11-recovered.png) — 2026-09-07T00:58:09.782Z, source `767226bf3c2fbf6525f332522edc74925f869c11`.

12. [Per-session reset removed orders and restored the storefront](https://komaroff-dev.ru/evidence/test-shop/full/12-reset.png) — 2026-09-07T00:58:10.315Z, source `767226bf3c2fbf6525f332522edc74925f869c11`.

13. [Mobile storefront and explanation of demo limits](https://komaroff-dev.ru/evidence/test-shop/full/13-mobile-store.png) — 2026-09-07T00:58:10.742Z, source `767226bf3c2fbf6525f332522edc74925f869c11`.

14. [Mobile order details before payment](https://komaroff-dev.ru/evidence/test-shop/full/14-mobile-order.png) — 2026-09-07T00:58:11.803Z, source `767226bf3c2fbf6525f332522edc74925f869c11`.

Screenshots, diagrams and the original evidence package retain their source language. Captions and this document are translated into English.

[Original evidence package](https://komaroff-dev.ru/downloads/test-shop-evidence.zip)
