# 📖 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_