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>
This commit is contained in:
Veronika Smirnova
2025-09-03 23:10:16 +03:00
parent 65fba5d911
commit cdeee82237
35 changed files with 7869 additions and 311 deletions

View File

@ -0,0 +1,792 @@
# 📖 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_