28 KiB
La Rose — план переделки сайта (docs/PLAN.md)
Проект: SemianiakaVY/la-rose · Шаблон: online-shop · Стек: Next.js
Источники: SPEC.md (контракт), docs/ANALYSIS.md (факты об
источнике), docs/CATALOG.md (полный инвентарь каталога).
Дата: 2026-10-04 · Решения Q1–Q6 приняты 2026-10-04 (см. §9).
0. Этап и рамки
Текущий этап — исследование, структура, схемы и карточки задач. На этом этапе:
- Нового дизайна НЕ делаем (решение зафиксировано в
.w4c/project.json) — отдельная дизайн-спецификация не выпускается; UI следует структуре источника и токенам, выбранным на слое фронтенда. - Реальные платежи не подключаем — провайдер за интерфейсом, реализация
mock. - Код приложения на этом этапе не пишется: результатом этапа являются этот план, mermaid-схемы и карточки задач на доске.
1. Цель продукта
Интернет-магазин доставки цветов и подарков по Минску (переделка la-rose.by): каталог с
категориями и фасетными фильтрами, карточка товара со структурированными полями, корзина и
одностраничное оформление, личный кабинет с историей заказов и закрытая админ-панель.
Продукт должен работать end-to-end на сид-данных (SPEC.md §1), UI — русский.
2. Зафиксированные решения
| # | Решение | Значение |
|---|---|---|
| R1 | Стек | Next.js (App Router, TypeScript) — из диалога создания проекта |
| R2 | Деньги | Только целые минорные единицы (priceMinor, копейки); никаких float |
| R3 | Форматирование чисел | Intl.NumberFormat / toLocaleString('en-US') с кодом валюты (SPEC.md §4) |
| R4 | Валюта | BYN сразу (Q1): одна константа STORE_CURRENCY = 'BYN', никакого интерима. В диалоге шаблона были только USD/EUR/RUB, поэтому в .w4c/project.json значение переопределено на BYN. Курсовой пересчёт и мультивалюта вне рамок; цены сидов берём из аудита как есть |
| R5 | Платежи | Провайдер за одним интерфейсом PaymentProvider; при None (mock) — мок-адаптер, flow работает без ключей (SPEC.md §6) |
| R6 | Оформление | Оформление — одностраничное /checkout, как у источника (/order/): корзина → покупатель → доставка → оплата. Аккаунт: по SPEC.md §2 чекаут требует аккаунта, у источника гостевой чекаут возможен — берём SPEC как контракт (чекаут после входа), гостю показываем логин с сохранением корзины |
| R7 | Доставка | Один источник констант (lib/delivery/constants.ts): порог бесплатной доставки, тариф, за МКАД, срок, часы. Противоречие источника (50/80/100 BYN, см. ANALYSIS.md §4) снимается на одном значении |
| R8 | Фильтры | Фасеты: повод, цвет, тип цветка, размер/диапазон цены + сортировки как у источника (название, цена, хиты, оценка, дата, наличие) |
| R9 | PDP-контракт | Обязательные структурированные поля: состав, количество стеблей, высота/габариты, вариант/размер, уход, наличие, срок доставки (сейчас отсутствуют — ANALYSIS.md §3, CATALOG.md §4) |
| R10 | URL | Все существующие URL сохраняем 1:1 (Q3): и человекочитаемые слаги, и числовые /product/<id>/ (они отвечают 200), и слаги категорий. Новой схемы слагов и 301-карты нет. Единственное исключение — битый /product/1997/ (R14) |
| R11 | Контент | Информационные страницы источника переносим: О компании, B2B, Доставка, Оплата, Как заказать, Возврат, Контакты, Вакансии, Политика; FAQ делаем страницей на этом этапе, блог — позже (Q5) |
| R12 | Аналитика и чат | Паритет по Яндекс.Метрике/GA4/FB Pixel закладываем; чат Bitrix24 заменяем на ссылки WhatsApp/Telegram (Q4) — отдельная чат-интеграция не строим |
| R13 | Скрытые категории | Категории источника «Розы», «Гортензии», «Тюльпаны» и скрытая таксономия «Розы по цвету»/«Красные розы» переносятся как visible=false: доступны по прямому URL и в хлебных крошках, в меню не выводятся (Q2; CATALOG.md §2) |
| R14 | Битый товар | №59 /product/1997/ («Букет «Небесная симфония» из пушистой хризантемы») исключается из сидов: URL отдаёт 404, живой адрес владельцем не подтверждён (Q6). В выгрузке остаётся 93 товара |
3. Информационная архитектура (маршруты)
Витрина
| Маршрут | Экран | Источник |
|---|---|---|
/ |
Главная: hero, категории, хиты | SPEC.md §3 |
/catalog |
Все товары, фильтры, сортировка, пагинация | SPEC.md §3, R8 |
/catalog/[categorySlug] |
Товары категории (11 категорий источника + скрытые, CATALOG.md §1–2) | SPEC.md §3 |
/product/[slug] |
Карточка: галерея, цена/старая цена, бейдж, варианты, PDP-контракт R9, в корзину | SPEC.md §3, R9 |
/cart |
Строки, количество, промокод, итоги | SPEC.md §3, ANALYSIS.md §4 |
/checkout |
Оформление (одна страница): покупатель → доставка → оплата | SPEC.md §5.2, R6 |
/checkout/success/[orderId] |
Подтверждение с номером заказа | SPEC.md §3 |
/account/orders, /account/orders/[orderId] |
История и детали заказа | SPEC.md §3 |
/login, /register, /logout |
Аутентификация | SPEC.md §3 |
/search?query= |
Поиск по названию/описанию | SPEC.md §6 |
/pages/[slug] |
Информационные страницы (R11) | ANALYSIS.md §5 |
Админ (за ролью admin)
| Маршрут | Экран |
|---|---|
/admin |
Дашборд: последние заказы, низкий остаток, выручка за период |
/admin/products |
Список/создание/правка/архив, изображения, цена и остаток |
/admin/categories |
Список/создание/правка/порядок |
/admin/orders, /admin/orders/[orderId] |
Список со статусами, детали и смена статуса |
Карта страниц
graph TD
HOME["/ — Главная"] --> CAT["/catalog — все товары (фасеты, сортировки)"]
CAT --> C1["/catalog/bukety-tsvetov (92)"]
CAT --> C2["/catalog/sbornye-bukety (36)"]
CAT --> C3["/catalog/bukety-iz-alstromerii (15)"]
CAT --> C4["/catalog/bukety-iz-gvozdiki (16)"]
CAT --> C5["/catalog/bukety-iz-kustovoy-rozy (20)"]
CAT --> C6["/catalog/bukety-iz-khrizantemy (37)"]
CAT --> C7["/catalog/svadebnye-bukety-nevesty (3)"]
CAT --> C8["/catalog/monobuket (23)"]
CAT --> C9["/catalog/kompozitsii (7)"]
CAT --> C10["/catalog/tsvety-v-yashchike (1)"]
CAT --> C11["/catalog/tsvety-v-korzine (6)"]
C1 --> PDP["/product/[slug] — 93 товара"]
PDP --> CART["/cart — корзина + промокод"]
CART --> CHECKOUT["/checkout — одна страница"]
CHECKOUT --> OK["/checkout/success/[orderId]"]
HOME --> AUTH["/login · /register"]
AUTH --> ACC["/account/orders"]
ACC --> ORD["/account/orders/[orderId]"]
HOME --> SRC["/search?query="]
HOME --> PAGES["/pages/[slug] — о компании, доставка, оплата, как заказать, возврат, контакты, b2b, вакансии, политика, FAQ"]
HOME --> ADMIN["/admin — дашборд (роль admin)"]
ADMIN --> AP["/admin/products"]
ADMIN --> AC["/admin/categories"]
ADMIN --> AO["/admin/orders"]
4. Модель данных
Расширяем модель SPEC.md §4 полями, которые требует источник (кросс-листинг, бейджи, старая цена, варианты, промокоды, контент). Ключевые дополнения выделены.
- Category — id, slug, name, description, position, parentId?,
visible(R13: категории источника «Розы»/«Гортензии»/«Тюльпаны» и таксономия «Розы по цвету» переносятся сvisible=false— CATALOG.md §2). - Product — id, slug, name, description,
priceMinor(R2),oldPriceMinor?(старая цена),currency,badge(hit|new|sale|null),stock, status (draft|active|archived), createdAt,composition,stems?,heightCm?,care,deliveryTime(R9/R11). - ProductCategory — productId, categoryId (кросс-листинг: 256 слотов → 94 товара).
- ProductImage — productId, url, position.
- ProductVariant — productId, label («15 роз»), priceMinor, stock (у источника: 15/19/25 роз → 198/240/325 BYN).
- ProductFeature — productId, key («Кому дарим цветы»), value[] (характеристики/фасеты, R8).
- Cart / CartItem — productId, quantity, цена всегда из продукта.
- Customer / Address — email, passwordHash, name, addresses[].
- Order — number, customerId, status (
pending|paid|fulfilled|cancelled), subtotalMinor, shippingMinor,discountMinor, totalMinor, currency, shippingAddress (получатель, телефон,giftNote= текст открытки, deliveryDate/Time), promoCodeId?, createdAt. - OrderItem — orderId, productId, name, unitPriceMinor, quantity (иммутабельный снимок).
- Payment — orderId, provider, providerRef, status, amountMinor.
- PromoCode — code, type, value, activeFrom/To (
coupon_codeисточника). - ContentPage — slug, title, body (R11; FAQ — блоком или страницей).
- DeliveryOption — id, name (
todoor/pickup), priceMinor, freeOverMinor (R7).
Физические имена (проверено 2026-10-04, схема tenant_1): сущности созданы в snake_case
(product, product_category, content_page, …); исключение — заказ хранится в таблице
orders (не order), обращаться по этому имени в SQL и миграциях.
erDiagram
CATEGORY {
int id PK
string slug
string name
int parentId FK
bool visible
}
PRODUCT {
int id PK
string slug
string name
int priceMinor
int oldPriceMinor
string currency
string badge
int stock
string status
string composition
int stems
int heightCm
}
PRODUCT_CATEGORY {
int productId FK
int categoryId FK
}
PRODUCT_IMAGE {
int id PK
int productId FK
string url
int position
}
PRODUCT_VARIANT {
int id PK
int productId FK
string label
int priceMinor
int stock
}
PRODUCT_FEATURE {
int id PK
int productId FK
string key
string value
}
CART {
int id PK
int customerId FK
}
CART_ITEM {
int id PK
int cartId FK
int productId FK
int quantity
}
CUSTOMER {
int id PK
string email
string passwordHash
string name
}
ADDRESS {
int id PK
int customerId FK
string street
}
ORDER {
int id PK
string number
int customerId FK
string status
int subtotalMinor
int shippingMinor
int discountMinor
int totalMinor
string currency
string giftNote
int promoCodeId FK
datetime createdAt
}
ORDER_ITEM {
int id PK
int orderId FK
int productId FK
string name
int unitPriceMinor
int quantity
}
PAYMENT {
int id PK
int orderId FK
string provider
string providerRef
string status
int amountMinor
}
PROMO_CODE {
int id PK
string code
string type
int value
}
CONTENT_PAGE {
int id PK
string slug
string title
string body
}
DELIVERY_OPTION {
string id PK
string name
int priceMinor
int freeOverMinor
}
CATEGORY ||--o{ CATEGORY : "parent of"
CATEGORY ||--o{ PRODUCT_CATEGORY : lists
PRODUCT ||--o{ PRODUCT_CATEGORY : "listed in"
PRODUCT ||--o{ PRODUCT_IMAGE : has
PRODUCT ||--o{ PRODUCT_VARIANT : has
PRODUCT ||--o{ PRODUCT_FEATURE : has
PRODUCT ||--o{ CART_ITEM : "added as"
PRODUCT ||--o{ ORDER_ITEM : "snapshot in"
CUSTOMER ||--o| CART : owns
CART ||--o{ CART_ITEM : contains
CUSTOMER ||--o{ ADDRESS : has
CUSTOMER ||--o{ ORDER : places
PROMO_CODE ||--o{ ORDER : discounts
ORDER ||--|{ ORDER_ITEM : contains
ORDER ||--o{ PAYMENT : "paid by"
5. Ключевые потоки
5.1 Витрина: каталог → корзина
flowchart LR
A["Каталог / фасеты"] --> B["Карточка товара"]
B --> C{"В наличии?"}
C -- нет --> D["Кнопка disabled + причина"]
C -- да --> E["POST /api/cart — добавить"]
E --> F["Бейдж корзины обновлён"]
F --> B
5.2 Чекаут и оплата
sequenceDiagram
actor C as Покупатель (вошёл)
participant UI as /checkout
participant API as API
participant PAY as PaymentProvider (mock)
participant DB as БД
C->>UI: Заполняет получателя, доставку, промокод
UI->>API: POST /api/checkout (idempotency-key)
API->>DB: Ревалидация цен и остатков серверно
alt Товар распродан
API-->>UI: 409 + какой товар недоступен
else Всё доступно
API->>DB: Создать Order (status=pending) + OrderItem (снимок)
API->>PAY: charge(amountMinor)
alt Оплата успешна
PAY-->>API: succeeded, providerRef
API->>DB: Order=paid, Payment=succeeded
API-->>UI: 201 {orderNumber}
UI-->>C: /checkout/success/[orderId]
else Оплата не прошла
PAY-->>API: failed
API->>DB: Order остаётся pending
API-->>UI: 402 + путь повтора
end
end
5.3 Админ: жизненный цикл товара и заказа
stateDiagram-v2
[*] --> draft: создать товар
draft --> active: опубликовать
active --> archived: архивировать
archived --> active: вернуть
note right of archived
Архивный товар исчезает из витрины,
но остаётся в прошлых заказах.
end note
stateDiagram-v2
[*] --> pending: заказ создан
pending --> paid: оплата подтверждена
pending --> cancelled: отклонён
paid --> fulfilled: выдан/доставлен
paid --> cancelled: возврат
6. Данные для наполнения (сиды из аудита)
- Категории: 11 видимых + 3 скрытых с
visible=false(розы/гортензии/тюльпаны, R13, CATALOG.md §2); их товары продолжают жить в видимых категориях черезProductCategory. - Товары: 93 уникальных с ценами, старыми ценами и бейджами — таблица CATALOG.md §3
№1–№94 минус исключённый битый №59
/product/1997/(R14); кросс-листинг черезProductCategory. - Варианты: у части товаров («N роз» → цена) —
ProductVariant. - Константы магазина: телефоны, e-mail, часы, адрес самовывоза, юрлицо, мессенджеры
(CATALOG.md §6) →
lib/store/constants.ts. - PDP-контракт R9 заполняется вручную/скриптом при переносе: состав, стебли, высота, уход, наличие, срок доставки (в источнике отсутствуют).
7. Слои сборки и приёмка (definition of done)
| # | Слой | Владелец | Проверка (команда / runtime) |
|---|---|---|---|
| 1 | План и рамки | Startup Creator | docs/PLAN.md + docs/CATALOG.md в репозитории; каждая карточка слоя содержит проверку |
| 2 | Диаграмма архитектуры | Diagram Expert | ERD/архитектура на /diagrams открываются без потерь; сущности = §4 |
| 3 | БД и формы | DataTables Expert | миграции + сид на чистом клоне; агрегатный запрос = пересчитанному вручную итогу |
| 4 | Бэкенд и API | Code Expert | npm run lint/typecheck + неавторизованный запрос к админ-маршруту → 401/403 |
| 5 | Фронтенд | Code Expert | npm run build + browse → cart → checkout → confirmation проходится на мок-провайдере |
| 6 | Админ-панель | Code Expert | нон-админ на /admin/* получает отказ серверно; смена статуса логируется |
| 7 | Control Panel | Control Panel Builder | виджеты показывают данные или пустое состояние; нет спиннера вечно; нет гориз. скролла 390×844 |
| 8 | Workflows | Elsa Workflows Manager | тестовый вебхук создаёт задачу и запись письма; неоплаченная ветка пропускает письмо |
| 9 | Качество и поставка | Code Expert | npm run ci (install + lint + typecheck + test + build) зелёный; README quickstart на чистом клоне |
| 10 | Деплой | Server Ops | публичный URL отдаёт приложение; рестарт контейнера восстанавливает сервис |
| 11 | Изображения товаров (§11) | Web Researcher → Code Expert → DataTables Expert | в работе: pnpm ci + pnpm db:verify:images зелёные; 281 файл = 281 строка product_image; tenant_1 сверена read-only; остался smoke-тест dev-сервера |
Полные критерии приёмки — SPEC.md §8.
8. Поэтапный порядок сборки
- Каркас Next.js: install → dev-server → пустая оболочка → коммит.
- Схема и миграции по §4, сид из §6.
- Каталог и карточка (витрина становится демонстрируемой).
- Корзина + чекаут + оплата (сначала мок).
- Аккаунт + история заказов.
- Админ-панель.
- Качество: тесты, CI, README,
.env.example, доступность.
9. Решённые вопросы (Q1–Q6, 2026-10-04)
Открытых вопросов не осталось — все решения приняты пользователем и разнесены по документу.
| # | Вопрос | Решение | Где учтено |
|---|---|---|---|
| Q1 | Валюта витрины | Сразу BYN — одна константа STORE_CURRENCY = 'BYN', без интерима и пересчёта |
R4, §4 (currency), .w4c/project.json → fields.currency |
| Q2 | Скрытые категории | Перенести невидимыми (visible=false): доступны по прямому URL, не в меню |
R13, §4 Category.visible, §6 |
| Q3 | SEO-слаги | Сохранить существующие 1:1 (человекочитаемые и числовые); новой схемы и 301 нет | R10, §3 |
| Q4 | Чат Bitrix24 | Заменить на ссылки WhatsApp/Telegram; отдельной чат-интеграции не строим | R12, §3 (/pages/[slug], футер) |
| Q5 | FAQ/блог | FAQ-страница на этом этапе (нет у источника), блог — позже | R11, §3 |
| Q6 | /product/1997/ (404) |
Исключить товар из сидов; в выгрузке 93 товара | R14, §6, CATALOG.md §3 |
Что это меняет в работе: валютные проверки и форматирование идут на BYN с первого дня (никаких переключений «потом»); слой данных сидит 93 товара и 14 категорий (11 видимых + 3 скрытых); слой архитектуры рисует ERD без 301-мапы; карточки бэкенда/фронтенда не закладывают чат-виджет.
10. Вне рамок
Мультивендор-маркетплейс, подписки, склад/инвентарные интеграции, налоговые движки, мультиязычная витрина (SPEC.md §1); реальные платёжные шлюзы; новый визуальный дизайн на этом этапе; перенос аналитики и чата — только решение и закладки (R12).
11. Изображения товаров — решения (2026-10-05, карточка #64)
Источник изображений — живая галерея PDP la-rose.by (ANALYSIS.md §3 п.8: 4 превью + основное изображение). Решения приняты пользователем 2026-10-05:
| # | Решение | Значение |
|---|---|---|
| I1 | Объём | Вся галерея товара: 1–4 фото на товар, ~150–250 файлов. position = 1 — главное изображение |
| I2 | Файлы и URL | Файлы public/images/products/<slug>/<n>.jpg (<n> = позиция с 1); в БД product_image.url = /images/products/<slug>/<n>.jpg — Next.js отдаёт public/ с корня |
| I3 | Качество | Оригиналы «как есть», без ресайза/пережатия (вес репозитория принят осознанно) |
| I4 | Источники истины | И живая БД tenant_1.product_image, и сид-файлы репозитория (db/seed/images.tsv с исходными URL → src/db/seed.ts / scripts/generate-seed-sql.ts → db/seed/0001_seed*.sql); свежая установка совпадает с прод |
Текущий сид ставит по одному изображению /images/products/<slug>.jpg (плоская схема, src/db/seed.ts §5).
Схема I2 её заменяет: вложенный путь с номером позиции.
Приёмка (карточка #64): select count(distinct product_id) from product_image = 93; число строк
product_image = числу файлов в public/images/products/**; каждый url существует как файл;
pnpm ci зелёный.
11.1 Итог (2026-10-05, карточка #64)
| Что | Факт (проверено командой/запросом) |
|---|---|
| Артефакты | db/seed/images.tsv (281 запись, 93 slug'а), public/images/products/<slug>/<n>.jpg (93 каталога, 281 файл, 39.2 MiB), scripts/fetch-images.ts, src/db/images.ts, scripts/verify-images.ts |
| Сид | src/db/seed.ts §5 и scripts/generate-seed-sql.ts вставляют строку product_image на каждую позицию; db/seed/0001_seed.sql и db/seed/0001_seed.guardsafe.sql перегенерированы (guard-safe без ON CONFLICT) |
pnpm ci |
verify: OK → product 93, visible category 11/14, product_image 281, files 281, products with main photo 93, variant 3, orders 2 (коммиты ca8ef3b, 33ae6ae) |
pnpm db:verify:images |
verify-images: OK → products 93/93 (position 1: 93/93), rows 281 = files 281, missing 0, orphans 0, guard-safe rows true / on-conflict false |
Живая БД tenant_1 |
приведена к тому же набору (DELETE 93 → INSERT 281, одна транзакция); независимая read-only сверка: product_image 281, distinct product_id 93, url вне шаблона <slug>/<n>.jpg — 0, товаров без position = 1 — 0, дублей/дыр нет; product 93, category 14 (11 visible), product_variant 3 |
Отклонение от оценки I1 (честно): фактический объём — 281 изображение, у части товаров
галерея длиннее 4 фото (максимум — 8 у buket-.../id 76, по 6 у нескольких). Это следствие выбора
«все фото галереи»; текст «1–4 на товар / ~150–250 файлов» был оценкой. Если нужен жёсткий
лимит (например, максимум 4 фото на товар), это отдельная правка: scripts/fetch-images.ts
(--max) + перегон сида и БД.
Осталось по карточке: smoke-тест — dev-сервер отдаёт /images/products/<slug>/1.jpg (200) для
трёх товаров (человекочитаемый slug, числовой slug, товар с 4+ фото). Конвенция URL для слоя
фронтенда (#57) — /images/products/<slug>/<n>.jpg, не файловая система.
Гигиена: в репозиторий попал вспомогательный db/seed/product_image_apply.guardsafe.sql
(коммит 714ddf3) — ручная копия секции -- 10. images из guard-safe сида, не покрытая
verify-images. Кандидат на удаление (один источник истины).