la-rose/docs/PLAN.md

28 KiB
Raw Permalink Blame History

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. Поэтапный порядок сборки

  1. Каркас Next.js: install → dev-server → пустая оболочка → коммит.
  2. Схема и миграции по §4, сид из §6.
  3. Каталог и карточка (витрина становится демонстрируемой).
  4. Корзина + чекаут + оплата (сначала мок).
  5. Аккаунт + история заказов.
  6. Админ-панель.
  7. Качество: тесты, 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. Кандидат на удаление (один источник истины).