карьерчик.ПРОЕКТНАЯ ДОКУМЕНТАЦИЯ / V1.0.0 / 16 ОКТЯБРЯ 2026
SYSTEM REQUIREMENTS / SRS

Один расчёт.
Одна согласованная модель.

1. Архитектура v1

Браузер
  │ HTTP :3000 · Next.js / React / TypeScript
  │ GET страницы, SVG, HTML-документация
  │ fetch('/api/...') — тот же origin
  ▼
Next.js rewrite /api/*
  │ HTTP loopback :5080, API_URL на стороне сервера
  ▼
ASP.NET Core 10 · C# Minimal API
  ├─ Каталог и зоны (серверные константы)
  ├─ Валидация и расчёт decimal
  ├─ Ограничение запросов
  └─ PortalStore · SemaphoreSlim
       └─ App_Data/portal.json (атомарная замена файла)

Стек выбран в соответствии с запросом: C# на бэкенде и Next.js/React на фронтенде. Не используется подмена C# логики обработчиками Next.js. Весь расчёт цены и создание заявки выполняются в API.

ADR-01: для локального однопроцессного MVP выбран JSON без внешних пакетов. Это облегчает запуск, но не подходит для нескольких экземпляров API, крупного каталога или промышленного хранения персональных данных. Следующий этап — PostgreSQL, миграции, транзакции и уникальность Order.QuoteId на уровне БД.

ADR-02: серверный proxy позволяет браузеру использовать один origin. CORS для прямого доступа к API не включён. Оба процесса по умолчанию слушают только 127.0.0.1. ADR-03: нет оплаты и состояния «Подтверждено», пока не реализованы поставщики, остатки и диспетчеризация.

2. Модель данных

Product 1 ─── N Quote 1 ─── 0..1 Order
Zone    1 ─── N Quote

Order содержит полный снимок Quote.
Product и Zone живут в Catalog (код).
Quote и Order сохраняются в одном StoreState (JSON).
Receipt — ограниченная проекция Order для клиента.
СущностьПоляПравила
Productid, name, category, fraction, price, density, description, use, standard, colorprice: RUB/м³; density: т/м³; id стабилен; тестовые данные
Zoneid, name, pricePerTrip, note5 зон, pickup имеет нулевой тариф
Quoteid, productId/name, quantity/unit, volume/weight, zoneId/name, trips, materialCost, deliveryCost, total, currency, expiresAt, tariffVersion, isEstimateGUID без дефисов; 24 часа; tariffVersion=demo-v1; monetary decimal
Orderid, token, quote, name, phone, address, deliveryDate, customerType, company, comment, consentAt, consentVersion, createdAtid: KR- + 12 hex; token: 64 hex; один Order на Quote
Receiptid, token, status, total, productName, volume, deliveryDate, createdAtНе возвращает имя, телефон, адрес, компанию и комментарий
StoreStatequotes[], orders[]Все записи одного экземпляра сервера в одном файле

Ссылочная целостность проверяется приложением при создании расчёта. Заявка сохраняет снимок, поэтому последующие изменения каталога не изменяют уже зафиксированную сумму. Устаревшие свободные расчёты очищаются при следующем создании расчёта; снимки в заявках остаются. Автоматической очистки заявок нет.

Точность

Деньги — C# decimal, округление до копеек AwayFromZero. Объём при переводе тонн округляется вверх до 0,001 м³, чтобы не занизить загрузку машины. Отображаемая масса округляется до 0,001 т. Число рейсов вычисляется по неокруглённой массе из нормализованного объёма. Даты API — UTC ISO 8601, дата поставки — календарная YYYY-MM-DD по Москве.

3. Алгоритм расчёта

Вход: productId, quantity, unit ∈ {m3, t}, zoneId
1. Проверить материал и зону.
2. quantity > 0 и ≤ 1000.
3. rawVolume = unit == t ? quantity / density : quantity.
4. volume = ceil(rawVolume × 1000) / 1000.
5. Проверить 3 ≤ volume ≤ 500.
6. weightExact = volume × density.
7. trips = pickup ? 0 : ceil(max(volume / 12, weightExact / 20)).
8. materialCost = round(volume × price, 2, AwayFromZero).
9. deliveryCost = trips × zone.pricePerTrip.
10. total = materialCost + deliveryCost.
11. Зафиксировать расчёт; expiresAt = UTC now + 24h.
ПримерМатериалРейсы / доставкаИтого
10 м³ карьерного песка, до 30 км10 × 650 = 6 500 ₽; 15 т1 × 6 500 = 6 500 ₽13 000 ₽
24 м³ мытого песка, до 30 км24 × 980 = 23 520 ₽; 38,4 т2 × 6 500 = 13 000 ₽36 520 ₽
25 м³ ПГС, Москва25 × 1 250 = 31 250 ₽; 41,25 т3 × 5 500 = 16 500 ₽47 750 ₽
15 т карьерного песка, самовывоз15 / 1,5 = 10 м³; 6 500 ₽0 рейсов; 0 ₽6 500 ₽

Объёмный помощник на фронтенде: V = площадь(м²) × слой(см) / 100. Он не включает коэффициент уплотнения, потери или технологический запас. Пользователь переносит результат в серверный калькулятор.

4. HTTP API

Base URL для прямых локальных запросов: http://127.0.0.1:5080; браузер обращается к http://127.0.0.1:3000/api. JSON: camelCase, UTF-8, числа в машинном формате с точкой. Лимит тела — 16 КБ. Некорректный JSON и несовместимые типы дают HTTP 400; инфраструктурные ошибки — Problem Details 500 без внутренних подробностей.

МетодПутьВход / доступОтвет
GET/api/healthНет200: status, version, mode
GET/api/products?category=sand&search=…Нет200: Product[]; неизвестная категория — пустой массив
GET/api/products/{id}Нет200: Product; 404
GET/api/delivery-zonesНет200: Zone[]
POST/api/quotesQuoteRequest200: Quote; 400 — некорректный ввод
POST/api/ordersOrderRequest201 — создана; 200 — повтор; 400 — поля/расчёт
GET/api/orders/{id}X-Order-Token: секрет200: Receipt; 404 — номер/секрет неверны

Для всех операций, кроме health: максимум 60 запросов в минуту на IP, без очереди; превышение — 429. Тело 429 может быть пустым. Прокси пересылает запросы с loopback, поэтому в текущем локальном режиме лимит фактически общий для браузеров этого сервера.

Создание расчёта

POST /api/quotes
Content-Type: application/json

{"productId":"sand-quarry","quantity":10,"unit":"m3","zoneId":"near"}

200 OK
{
  "id": "<32 hex>",
  "productId": "sand-quarry", "productName": "Песок карьерный",
  "quantity": 10, "unit": "m3", "volume": 10, "weight": 15,
  "zoneId": "near", "zoneName": "До 30 км от МКАД", "trips": 1,
  "materialCost": 6500, "deliveryCost": 6500, "total": 13000,
  "currency": "RUB", "expiresAt": "<UTC ISO 8601>",
  "tariffVersion": "demo-v1", "isEstimate": true
}

Создание заявки

POST /api/orders
{
  "quoteId": "<id расчёта>",
  "name": "Тестовый клиент", "phone": "+7 900 000-00-00",
  "address": "Тестовый объект, улица 1",
  "deliveryDate": "<сегодня..+90 дней, YYYY-MM-DD>",
  "customerType": "private", "company": null,
  "comment": "Нужен подъезд для самосвала", "consent": true
}

201 Created → Receipt
{
  "id":"KR-<12 hex>", "token":"<64 hex>",
  "status":"Новая заявка", "total":13000,
  "productName":"Песок карьерный", "volume":10,
  "deliveryDate":"<YYYY-MM-DD>", "createdAt":"<UTC ISO 8601>"
}

Цена в OrderRequest отсутствует. Лишние JSON-поля игнорируются стандартным десериализатором и не меняют итог. При повторе одного quoteId с теми же именем и телефоном возвращается прежний Receipt, даже если повторное тело содержит другой комментарий или дату: заявка не обновляется. Для изменений нужен новый расчёт.

Валидация заявки

ПолеПравило
quoteIdСуществует; ещё не истёк при первом создании
nameПосле Trim от 2 до 80 символов
phone11 цифр; разрешены +, пробелы, скобки, дефисы, длина строки 10–24; владение номером не проверяется
addressПосле Trim 5–300 символов, в том числе для самовывоза
deliveryDateТочный формат yyyy-MM-dd, диапазон сегодня..+90 дней по UTC+3
customerTypeprivate или business
companyОбязательно для business; до 160 символов
commentНеобязательно; до 1000 символов
consentДолжно быть true; сервер ставит timestamp и версию

Проверка заявки

GET /api/orders/KR-…
X-Order-Token: <64 hex>

200 → Receipt
404 → {"message":"Заявка не найдена или код доступа неверен"}

Общий формат доменной ошибки: {"message":"Текст для пользователя"}. Фронтенд обрабатывает также пустые/не-JSON ответы и недоступность API; не показывает успешную отправку до ответа сервера. Во время запроса кнопка блокируется. Устаревший ответ расчёта после изменения параметров отбрасывается по локальной ревизии.

5. Безопасность и ограничения

Мера v1НазначениеОстаточный риск / следующий шаг
Loopback по умолчаниюНе публиковать локальное демо в сетиПри публичном запуске TLS, reverse proxy, конфигурация hosts
Секрет 256 бит, сравнение constant-timeДоступ к статусу без открытого списка заявокСекрет хранится в файле и localStorage; нужны хеширование токена, срок и отзыв
Receipt без контактных данныхНе выдавать персональные поля через публичный GETНомер, сумма и дата тоже требуют бережного обращения
Серверная цена и snapshotНельзя подменить сумму из браузераТарифы требуют контроля версий и аудита
Rate limit и 16 КБ bodyОграничить поток и размер запросовНе замена антиботу, квотам хранения и WAF
Согласие с версией и временемЗафиксировать действие пользователяНет проверки личности; юридические тексты требуют владельца
React escaping, нет HTML из формыСнизить риск инъекции в клиентский UIНужны CSP и аудит для публичной эксплуатации
Нет cookies авторизации и аккаунтовМеньше состояния в локальном MVPБудущая админка требует RBAC, MFA, журналирования
X-Frame-Options, nosniff, no-referrerБазовые заголовки фронтендаПолитика CSP не настроена; публичный аудит обязателен

В v1 нет сторонних аналитических трекеров и удалённых шрифтов; изображения — собственные локальные SVG. Метаданные запрещают индексацию демо. Эти меры не делают прототип готовым для публичного приёма реальных данных.

6. Хранение, эксплуатация и развёртывание

Один процесс API. SemaphoreSlim сериализует операции чтения/изменения/записи. Новый JSON сначала пишется во временный файл, затем заменяет основной в той же директории. Это защищает от частичной записи при обычном завершении операции, но не заменяет транзакционную БД, fsync и резервное копирование. Несколько процессов с одним файлом запрещены.

Путь: DataDirectory из конфигурации, по умолчанию backend/Karerchik.Api/App_Data. Каталог создаётся автоматически. Ошибка чтения JSON не приводит к тихой потере данных — запрос завершается ошибкой. На диске данные не зашифрованы. Автоматической очистки заказов и интерфейса удаления нет.

Запуск Windows

# Из папки v1:
powershell -ExecutionPolicy Bypass -File .scriptsstart.ps1
# Портал: http://127.0.0.1:3000
# API:    http://127.0.0.1:5080/api/health

# Остановка только процессов проекта:
powershell -ExecutionPolicy Bypass -File .scriptsstop.ps1

# Проверки:
dotnet build .ackendKarerchik.Api -c Release
cd frontend
npm ci
npm run build
npm run typecheck
cd ..
node --test tests/api.test.mjs

Требуются .NET SDK 10 и Node.js 24 (проверено в среде проекта). Пакеты фронтенда фиксируются package-lock.json; запускать npm ci. API не использует внешние NuGet-пакеты. Скрипт start использует production-сборку Next.js, при её отсутствии выполняет сборку.

Восстановление и резервная копия

  1. Остановить оба процесса штатным скриптом.
  2. Скопировать App_Data/portal.json в защищённое резервное хранилище.
  3. Для восстановления заменить файл из копии при остановленном API.
  4. Запустить, проверить health и известную тестовую заявку по номеру и секрету.

Не включать рабочие заявки, токены, логи и зависимости в Git. Каталог App_Data исключён .gitignore. Тесты используют отдельное временное хранилище и не затрагивают рабочие заявки.

Нефункциональные требования

IDТребованиеСтатус / измерение
NFR-01Адаптивность от 360 px, без горизонтального скролла приложенияCSS media queries; ручная браузерная проверка
NFR-02Клавиатура, видимый фокус, подписи полей, native dialogРеализованы; полный WCAG-аудит не выполнен
NFR-03Устойчивое сохранение после успешного 201Проверяется интеграционно с перезапуском API
NFR-04Согласованный расчёт денег и грузоподъёмностиdecimal и граничные API-тесты
NFR-05Ошибки без внутренних трассировокProblem Details / сообщения домена
NFR-06Локальная воспроизводимая сборкаLockfile, фиксированный стек, README и сценарии
NFR-07Промышленная доступность и нагрузкаSLO и нагрузочные тесты не определены; до запуска выбрать целевые значения

7. Источники по стеку

Next.js — официальная установка; Microsoft Learn — ASP.NET Core APIs. Версии npm проверены 16.10.2026: Next.js 16.4.2, React 19.3.1; фактические зависимости зафиксированы lockfile.