Files
sfera-new/docs/development/V2_MIGRATION_PLAYBOOK.md
Veronika Smirnova cdeee82237 feat: реализовать полную автосинхронизацию V2 системы расходников с nameForSeller и анализ миграции
-  Добавлено поле nameForSeller в FulfillmentConsumable для кастомизации названий
-  Добавлено поле inventoryId для связи между каталогом и складом
-  Реализована автосинхронизация FulfillmentConsumableInventory → FulfillmentConsumable
-  Обновлен UI с колонкой "Название для селлера" в /fulfillment/services/consumables
-  Исправлены GraphQL запросы (удалено поле description, добавлены новые поля)
-  Создан скрипт sync-inventory-to-catalog.ts для миграции существующих данных
-  Добавлена техническая документация архитектуры системы инвентаря
-  Создан отчет о статусе миграции V1→V2 с детальным планом

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 23:17:42 +03:00

792 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 📖 V2 MIGRATION PLAYBOOK: Руководство по безопасной миграции
> **Статус**: ✅ **ПРОВЕРЕНО НА ПРАКТИКЕ**
> **Основано на**: Успешной миграции раздела "Услуги" 03.09.2025
> **Применимо к**: Любым разделам SFERA требующим V1→V2 миграции
---
## 🎯 ОБЗОР МЕТОДОЛОГИИ
### ФИЛОСОФИЯ БЕЗОПАСНОЙ МИГРАЦИИ:
> **"Измерь дважды, отрежь один раз"** - полный анализ перед любыми изменениями
### КЛЮЧЕВЫЕ ПРИНЦИПЫ:
1. **Изоляция V2 от V1** - никаких пересечений во время разработки
2. **Поэтапность** - маленькие шаги с проверкой после каждого
3. **Rollback готовность** - возможность отката на любом этапе
4. **Сохранение функциональности** - пользователь не должен заметить изменений
---
## 📋 УНИВЕРСАЛЬНЫЙ ЧЕКЛИСТ МИГРАЦИИ
### ПОДГОТОВИТЕЛЬНЫЙ ЭТАП (КРИТИЧЕСКИ ВАЖЕН):
```
□ Проанализировать все компоненты использующие старую систему
□ Определить зависимости и связанные системы
□ Создать список файлов требующих обновления
□ Убедиться в понимании бизнес-логики домена
□ Проверить наличие тестов для критических функций
□ Создать backup стратегию для критических данных
```
### ЭТАП СОЗДАНИЯ V2 АРХИТЕКТУРЫ:
```
□ Создать новые Prisma модели с доменной изоляцией
□ Реализовать полный набор V2 резолверов (Query + Mutation)
□ Создать GraphQL типы и схемы
□ Подключить резолверы к основному GraphQL API
□ Протестировать V2 API независимо от V1
□ Проверить npm run build на отсутствие ошибок
```
### ЭТАП МИГРАЦИИ КОМПОНЕНТОВ:
```
□ Обновить импорты на V2 запросы
□ Изменить data references в компонентах
□ Обновить мутации на V2 версии
□ Исправить refetchQueries в формах
□ Проверить Apollo Client кэш обновления
□ Протестировать каждый обновленный компонент
```
### ЭТАП ОТКЛЮЧЕНИЯ V1:
```
□ Убедиться что все компоненты используют V2
□ Отключить V1 резолверы в основном файле резолверов
□ Удалить неиспользуемые V1 импорты
□ Проверить отсутствие ошибок в консоли браузера
□ Провести полное функциональное тестирование
□ Подтвердить npm run build проходит без ошибок
```
---
## 🔧 ПОШАГОВЫЙ АЛГОРИТМ МИГРАЦИИ
### ШАГ 1: ГЛУБОКИЙ АНАЛИЗ СУЩЕСТВУЮЩЕЙ СИСТЕМЫ
#### 1.1 Найти все компоненты домена:
```bash
# Поиск компонентов использующих V1 запросы
rg "GET_MY_SERVICES|GET_MY_SUPPLIES|GET_MY_LOGISTICS" --type ts
# Поиск direct data access
rg "data\?\.myServices|data\?\.mySupplies|data\?\.myLogistics" --type ts
# Поиск мутаций V1
rg "CREATE_SERVICE|UPDATE_SERVICE|DELETE_SERVICE" --type ts
```
#### 1.2 Проанализировать структуру данных:
```bash
# Найти Prisma модели
rg "model.*Service|model.*Supply|model.*Logistics" prisma/schema.prisma
# Найти связанные типы
rg "Service.*{|Supply.*{|Logistics.*{" --type ts
```
#### 1.3 Понять бизнес-логику:
```typescript
// КРИТИЧЕСКИ ВАЖНО: Прочитать все резолверы V1
// Понять какие поля обязательные, какие расчеты происходят
// Найти все места где данные трансформируются
```
### ШАГ 2: СОЗДАНИЕ V2 МОДЕЛЕЙ ДАННЫХ
#### 2.1 Шаблон V2 Prisma модели:
```prisma
model DomainEntityV2 {
id String @id @default(cuid())
organizationId String // ОБЯЗАТЕЛЬНО: доменная изоляция
name String
description String?
price Decimal? @db.Decimal(10, 2) // Для денежных полей
isActive Boolean @default(true) // Мягкое удаление
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Связи
organization Organization @relation("OrganizationDomainV2", fields: [organizationId], references: [id])
// Индексы производительности
@@index([organizationId, isActive])
@@map("domain_entity_v2")
}
```
#### 2.2 Ключевые принципы моделирования:
- **`organizationId`** - ОБЯЗАТЕЛЬНО для доменной изоляции
- **`Decimal`** - для всех денежных полей (точность)
- **`isActive`** - вместо физического удаления
- **Индексы** - на часто используемые комбинации полей
- **`@@map`** - явное имя таблицы с суффиксом `_v2`
### ШАГ 3: РЕАЛИЗАЦИЯ V2 РЕЗОЛВЕРОВ
#### 3.1 Структура файла резолвера:
```typescript
// domain-entity-v2.ts
import { prisma } from '@/lib/prisma'
import { Context } from '../context'
// ===== QUERIES =====
export const domainEntityQueries = {
myDomainEntities: async (_: unknown, __: unknown, context: Context) => {
const { user } = context
if (!user?.organization?.id) {
throw new Error('Организация не найдена')
}
return await prisma.domainEntityV2.findMany({
where: {
organizationId: user.organization.id, // ДОМЕННАЯ ИЗОЛЯЦИЯ
isActive: true,
},
include: {
organization: true,
},
orderBy: [
{ name: 'asc' },
],
})
},
domainEntitiesByOrganization: async (
_: unknown,
{ organizationId }: { organizationId: string },
context: Context,
) => {
// Публичные данные для других участников
return await prisma.domainEntityV2.findMany({
where: {
organizationId,
isActive: true,
},
include: {
organization: true,
},
})
},
}
// ===== MUTATIONS =====
export const domainEntityMutations = {
createDomainEntity: async (
_: unknown,
{ input }: { input: CreateDomainEntityInput },
context: Context,
) => {
try {
const { user } = context
if (!user?.organization?.id) {
return {
success: false,
message: 'Организация не найдена',
entity: null,
}
}
// ВАЛИДАЦИЯ
if (!input.name?.trim()) {
return {
success: false,
message: 'Название обязательно',
entity: null,
}
}
const entity = await prisma.domainEntityV2.create({
data: {
...input,
organizationId: user.organization.id, // AUTO-ASSIGN
},
include: {
organization: true,
},
})
return {
success: true,
message: 'Успешно создано',
entity,
}
} catch (error) {
console.error('Error creating domain entity:', error)
return {
success: false,
message: 'Внутренняя ошибка сервера',
entity: null,
}
}
},
updateDomainEntity: async (
_: unknown,
{ input }: { input: UpdateDomainEntityInput },
context: Context,
) => {
try {
const { user } = context
if (!user?.organization?.id) {
return { success: false, message: 'Организация не найдена', entity: null }
}
// ПРОВЕРКА ПРИНАДЛЕЖНОСТИ
const existingEntity = await prisma.domainEntityV2.findFirst({
where: {
id: input.id,
organizationId: user.organization.id, // SECURITY CHECK
},
})
if (!existingEntity) {
return { success: false, message: 'Объект не найден', entity: null }
}
const entity = await prisma.domainEntityV2.update({
where: { id: input.id },
data: {
...input,
id: undefined, // Убираем id из данных для обновления
},
include: {
organization: true,
},
})
return {
success: true,
message: 'Успешно обновлено',
entity,
}
} catch (error) {
console.error('Error updating domain entity:', error)
return {
success: false,
message: 'Ошибка при обновлении',
entity: null,
}
}
},
deleteDomainEntity: async (
_: unknown,
{ id }: { id: string },
context: Context,
) => {
try {
const { user } = context
if (!user?.organization?.id) {
throw new Error('Организация не найдена')
}
// МЯГКОЕ УДАЛЕНИЕ
const result = await prisma.domainEntityV2.updateMany({
where: {
id,
organizationId: user.organization.id, // SECURITY CHECK
},
data: {
isActive: false,
},
})
return result.count > 0
} catch (error) {
console.error('Error deleting domain entity:', error)
return false
}
},
}
```
#### 3.2 Подключение к основным резолверам:
```typescript
// В /src/graphql/resolvers/index.ts
import { domainEntityQueries, domainEntityMutations } from './domain-entity-v2'
const mergedResolvers = mergeResolvers(
// ... existing resolvers
// V2 DOMAIN ENTITY
{
Query: domainEntityQueries,
Mutation: domainEntityMutations,
},
)
```
### ШАГ 4: СОЗДАНИЕ GraphQL СХЕМЫ
#### 4.1 Добавить в typedefs.ts:
```graphql
# V2 DOMAIN ENTITY TYPES
type DomainEntityV2 {
id: ID!
organizationId: String!
name: String!
description: String
price: Float
isActive: Boolean!
createdAt: DateTime!
updatedAt: DateTime!
organization: Organization!
}
type DomainEntityResponse {
success: Boolean!
message: String!
entity: DomainEntityV2
}
# V2 DOMAIN ENTITY INPUTS
input CreateDomainEntityInput {
name: String!
description: String
price: Float
}
input UpdateDomainEntityInput {
id: ID!
name: String
description: String
price: Float
isActive: Boolean
}
extend type Query {
myDomainEntities: [DomainEntityV2!]!
domainEntitiesByOrganization(organizationId: ID!): [DomainEntityV2!]!
}
extend type Mutation {
createDomainEntity(input: CreateDomainEntityInput!): DomainEntityResponse!
updateDomainEntity(input: UpdateDomainEntityInput!): DomainEntityResponse!
deleteDomainEntity(id: ID!): Boolean!
}
```
### ШАГ 5: СОЗДАНИЕ GraphQL ЗАПРОСОВ
#### 5.1 Создать queries файл:
```typescript
// /src/graphql/queries/domain-entity-v2.ts
import { gql } from '@apollo/client'
export const GET_MY_DOMAIN_ENTITIES_V2 = gql`
query GetMyDomainEntitiesV2 {
myDomainEntities {
id
organizationId
name
description
price
isActive
createdAt
updatedAt
organization {
id
name
}
}
}
`
export const GET_DOMAIN_ENTITIES_BY_ORGANIZATION = gql`
query GetDomainEntitiesByOrganization($organizationId: ID!) {
domainEntitiesByOrganization(organizationId: $organizationId) {
id
organizationId
name
description
price
isActive
organization {
id
name
}
}
}
`
export const CREATE_DOMAIN_ENTITY = gql`
mutation CreateDomainEntity($input: CreateDomainEntityInput!) {
createDomainEntity(input: $input) {
success
message
entity {
id
name
description
price
organization {
id
name
}
}
}
}
`
export const UPDATE_DOMAIN_ENTITY = gql`
mutation UpdateDomainEntity($input: UpdateDomainEntityInput!) {
updateDomainEntity(input: $input) {
success
message
entity {
id
name
description
price
isActive
}
}
}
`
export const DELETE_DOMAIN_ENTITY = gql`
mutation DeleteDomainEntity($id: ID!) {
deleteDomainEntity(id: $id)
}
`
```
### ШАГ 6: ТЕСТИРОВАНИЕ V2 API
#### 6.1 Проверить через GraphQL Playground:
```graphql
# Тест создания
mutation {
createDomainEntity(input: {
name: "Test Entity"
description: "Test Description"
price: 100.50
}) {
success
message
entity {
id
name
price
}
}
}
# Тест получения данных
query {
myDomainEntities {
id
name
price
isActive
}
}
```
#### 6.2 Проверить сборку:
```bash
npm run build
```
### ШАГ 7: МИГРАЦИЯ КОМПОНЕНТОВ
#### 7.1 Алгоритм обновления компонента:
```typescript
// БЫЛО (V1):
import { GET_MY_OLD_ENTITIES } from '@/graphql/queries'
const { data } = useQuery(GET_MY_OLD_ENTITIES)
const entities = data?.myOldEntities || []
const [createEntity] = useMutation(CREATE_OLD_ENTITY, {
refetchQueries: [{ query: GET_MY_OLD_ENTITIES }]
})
// СТАЛО (V2):
import { GET_MY_DOMAIN_ENTITIES_V2, CREATE_DOMAIN_ENTITY } from '@/graphql/queries/domain-entity-v2'
const { data } = useQuery(GET_MY_DOMAIN_ENTITIES_V2)
const entities = data?.myDomainEntities || []
const [createEntity] = useMutation(CREATE_DOMAIN_ENTITY, {
refetchQueries: [{ query: GET_MY_DOMAIN_ENTITIES_V2 }],
update: (cache, { data }) => {
if (data?.createDomainEntity?.success) {
// ОБНОВИТЬ APOLLO CACHE
const existingData = cache.readQuery({ query: GET_MY_DOMAIN_ENTITIES_V2 })
if (existingData) {
cache.writeQuery({
query: GET_MY_DOMAIN_ENTITIES_V2,
data: {
myDomainEntities: [
...existingData.myDomainEntities,
data.createDomainEntity.entity
]
}
})
}
}
}
})
```
#### 7.2 Чеклист обновления компонента:
```
□ Заменить импорты V1→V2 запросов
□ Обновить useQuery на V2 версию
□ Изменить data references (data?.myOldEntities → data?.myDomainEntities)
□ Обновить мутации на V2 версии
□ Исправить refetchQueries arrays
□ Добавить Apollo cache update логику
□ Протестировать все CRUD операции
□ Проверить отсутствие console errors
```
### ШАГ 8: ОТКЛЮЧЕНИЕ V1 СИСТЕМЫ
#### 8.1 Проверить что все компоненты мигрированы:
```bash
# НЕ ДОЛЖНО БЫТЬ РЕЗУЛЬТАТОВ:
rg "GET_MY_OLD_ENTITIES" --type ts
rg "data\?\.myOldEntities" --type ts
rg "CREATE_OLD_ENTITY" --type ts
```
#### 8.2 Отключить V1 резолверы:
```typescript
// В /src/graphql/resolvers/index.ts
Query: (() => {
const {
myOldEntities: _myOldEntities, // ← ОТКЛЮЧИТЬ V1
// ... другие отключаемые
...filteredQuery
} = oldResolvers.Query || {}
return filteredQuery
})(),
```
#### 8.3 Финальная проверка:
```bash
npm run build # Должно пройти без ошибок
npm run lint # Проверить warnings
```
---
## 🚨 КРИТИЧЕСКИЕ ПРАВИЛА МИГРАЦИИ
### ❌ НИКОГДА НЕ ДЕЛАТЬ:
1. **Изменять V1 и V2 одновременно** - риск поломки обеих систем
2. **Мигрировать все компоненты разом** - невозможность rollback
3. **Отключать V1 до полной миграции V2** - потеря функциональности
4. **Игнорировать ошибки сборки** - скрытые проблемы в production
5. **Пропускать тестирование** - риск багов в production
### ✅ ВСЕГДА ДЕЛАТЬ:
1. **Создавать V2 полностью независимо от V1**
2. **Тестировать каждый этап отдельно**
3. **Мигрировать компоненты поочередно**
4. **Проверять npm run build после каждого изменения**
5. **Отключать V1 только после полной проверки V2**
---
## 🛡️ СТРАТЕГИЯ ROLLBACK
### БЫСТРЫЙ ОТКАТ КОМПОНЕНТА:
```typescript
// В компоненте: вернуть старые импорты
- import { GET_MY_DOMAIN_ENTITIES_V2 } from '@/graphql/queries/domain-entity-v2'
+ import { GET_MY_OLD_ENTITIES } from '@/graphql/queries'
- const entities = data?.myDomainEntities || []
+ const entities = data?.myOldEntities || []
```
### ОТКАТ V1 РЕЗОЛВЕРОВ:
```typescript
// В index.ts: убрать из исключений
Query: (() => {
const {
// myOldEntities: _myOldEntities, // ← ЗАКОММЕНТИРОВАТЬ ОТКЛЮЧЕНИЕ
...filteredQuery
} = oldResolvers.Query || {}
return filteredQuery
})(),
```
### ПОЛНЫЙ ОТКАТ ЧЕРЕЗ GIT:
```bash
# Откат конкретных файлов
git checkout HEAD~1 -- src/components/domain/entity-component.tsx
# Откат всей ветки
git reset --hard HEAD~5 # Осторожно!
```
---
## 📊 МЕТРИКИ УСПЕШНОЙ МИГРАЦИИ
### КОЛИЧЕСТВЕННЫЕ ПОКАЗАТЕЛИ:
| Метрика | Цель | Способ измерения |
|---------|------|------------------|
| **Компоненты мигрированы** | 100% | `rg "V1_QUERIES" --type ts` должен быть пуст |
| **Тесты проходят** | 100% | `npm test` без ошибок |
| **Сборка успешна** | ✅ | `npm run build` без ошибок |
| **ESLint warnings** | ≤ уровня до миграции | `npm run lint` |
| **Console errors** | 0 | Браузерная консоль |
### КАЧЕСТВЕННЫЕ ПОКАЗАТЕЛИ:
- ✅ Пользователи не заметили изменений в UI
- ✅ Все функции работают как раньше
- ✅ Производительность не ухудшилась
- ✅ V1 система полностью отключена
- ✅ V2 система масштабируема для новых функций
---
## 🎯 ШАБЛОН MIGRATION COMMIT
```bash
git commit -m "feat: migrate [DOMAIN] from V1 to V2
✅ COMPLETED:
- Created [N] V2 Prisma models with domain isolation
- Implemented full CRUD GraphQL V2 resolvers
- Migrated [N] components from V1 to V2 queries
- Connected V2 mutations to main resolvers
- Disabled V1 resolvers: [list]
🏗️ ARCHITECTURE:
- Domain isolation by organizationId
- Specialized tables replace universal V1 models
- Full TypeScript typing and validation
- Optimized Apollo Client cache management
🧪 VERIFICATION:
- npm run build: ✅ successful
- All UI functions: ✅ working
- V1 resolvers: ✅ disabled
- User experience: ✅ unchanged
🔧 FILES CHANGED:
- NEW: /src/graphql/resolvers/[domain]-v2.ts
- NEW: /src/graphql/queries/[domain]-v2.ts
- UPDATED: [N] component files V1→V2
- UPDATED: /src/graphql/resolvers/index.ts (V1 disabled)
- UPDATED: /prisma/schema.prisma (V2 models)
📊 IMPACT:
- [N] components fully migrated
- [N] V1 resolvers safely disabled
- 100% backward compatibility maintained
- Ready for production deployment
Co-authored-by: [Team members]"
```
---
## 📚 ПОЛЕЗНЫЕ КОМАНДЫ
### ПОИСК И АНАЛИЗ:
```bash
# Найти все использования V1 запросов
rg "GET_MY_[A-Z_]+" --type ts | grep -v "_V2"
# Найти компоненты с прямым доступом к V1 данным
rg "data\?\.my[A-Z]" --type ts
# Найти мутации V1
rg "(CREATE|UPDATE|DELETE)_[A-Z_]+" --type ts | grep -v "_V2"
# Проверить что V1 отключен
rg "myOldEntities:" src/graphql/resolvers/index.ts
```
### ПРОВЕРКИ КАЧЕСТВА:
```bash
# Проверка типов
npx tsc --noEmit
# Проверка линтера
npm run lint
# Проверка сборки
npm run build
# Поиск TODO/FIXME от миграции
rg "TODO.*V2|FIXME.*V2" --type ts
```
### ТЕСТИРОВАНИЕ:
```bash
# Unit тесты
npm test -- --grep "V2"
# E2E тесты конкретного домена
npm run e2e:test -- --spec "**/domain-entity-v2.cy.ts"
# Проверка покрытия
npm run test:coverage
```
---
## 🎓 ОБУЧАЮЩИЕ МАТЕРИАЛЫ
### ДЛЯ НОВИЧКОВ В V2 МИГРАЦИИ:
1. Прочитать **V2_SERVICES_MIGRATION_REPORT.md** - реальный пример
2. Изучить **V2_ARCHITECTURE_SERVICES.md** - техническая документация
3. Посмотреть код миграции Services как reference implementation
### ДЛЯ ЭКСПЕРТОВ:
1. Адаптировать шаблоны под специфику конкретного домена
2. Расширить метрики успеха под требования проекта
3. Создать автоматизацию для повторяющихся задач миграции
### TROUBLESHOOTING GUIDE:
- **"V2 мутации не работают"** → Проверить подключение к index.ts resolvers
- **"Данные не отображаются"** → Проверить data references в компонентах
- **"Apollo cache не обновляется"** → Добавить update функции в мутации
- **"TypeScript ошибки"** → Проверить соответствие types в GraphQL схеме
---
## 🚀 ЗАКЛЮЧЕНИЕ
Этот playbook основан на реальном опыте успешной миграции и содержит все критические знания для безопасного перехода любого домена SFERA с V1 на V2.
**🎯 КЛЮЧЕВЫЕ TAKEAWAYS:**
- **Безопасность превыше скорости** - лучше медленно, но без ошибок
- **Тестирование на каждом этапе** - предотвращение проблем в production
- **Документация изменений** - возможность rollback и понимания для команды
- **Доменная изоляция V2** - фундамент масштабируемости архитектуры
**🏆 РЕЗУЛЬТАТ ПРИМЕНЕНИЯ:**
Используя этот playbook, любой разработчик SFERA сможет провести V1→V2 миграцию безопасно, эффективно и с полным пониманием процесса.
---
_Создано: 03.09.2025_
_Основано на: Реальном опыте миграции раздела "Услуги"_
_Статус: Production Ready Playbook_
рименимость: Универсальная для всех доменов SFERA_