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

24 KiB
Raw Blame History

📖 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 Найти все компоненты домена:

# Поиск компонентов использующих 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 Проанализировать структуру данных:

# Найти Prisma модели
rg "model.*Service|model.*Supply|model.*Logistics" prisma/schema.prisma

# Найти связанные типы
rg "Service.*{|Supply.*{|Logistics.*{" --type ts

1.3 Понять бизнес-логику:

// КРИТИЧЕСКИ ВАЖНО: Прочитать все резолверы V1
// Понять какие поля обязательные, какие расчеты происходят
// Найти все места где данные трансформируются

ШАГ 2: СОЗДАНИЕ V2 МОДЕЛЕЙ ДАННЫХ

2.1 Шаблон V2 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 Структура файла резолвера:

// 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 Подключение к основным резолверам:

// В /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:

# 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 файл:

// /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:

# Тест создания
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 Проверить сборку:

npm run build

ШАГ 7: МИГРАЦИЯ КОМПОНЕНТОВ

7.1 Алгоритм обновления компонента:

// БЫЛО (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 Проверить что все компоненты мигрированы:

# НЕ ДОЛЖНО БЫТЬ РЕЗУЛЬТАТОВ:
rg "GET_MY_OLD_ENTITIES" --type ts
rg "data\?\.myOldEntities" --type ts  
rg "CREATE_OLD_ENTITY" --type ts

8.2 Отключить V1 резолверы:

// В /src/graphql/resolvers/index.ts

Query: (() => {
  const {
    myOldEntities: _myOldEntities, // ← ОТКЛЮЧИТЬ V1
    // ... другие отключаемые
    ...filteredQuery
  } = oldResolvers.Query || {}
  return filteredQuery
})(),

8.3 Финальная проверка:

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

БЫСТРЫЙ ОТКАТ КОМПОНЕНТА:

// В компоненте: вернуть старые импорты
- 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 РЕЗОЛВЕРОВ:

// В index.ts: убрать из исключений
Query: (() => {
  const {
    // myOldEntities: _myOldEntities, // ← ЗАКОММЕНТИРОВАТЬ ОТКЛЮЧЕНИЕ
    ...filteredQuery
  } = oldResolvers.Query || {}
  return filteredQuery
})(),

ПОЛНЫЙ ОТКАТ ЧЕРЕЗ GIT:

# Откат конкретных файлов
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

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]"

📚 ПОЛЕЗНЫЕ КОМАНДЫ

ПОИСК И АНАЛИЗ:

# Найти все использования 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

ПРОВЕРКИ КАЧЕСТВА:

# Проверка типов
npx tsc --noEmit

# Проверка линтера  
npm run lint

# Проверка сборки
npm run build

# Поиск TODO/FIXME от миграции
rg "TODO.*V2|FIXME.*V2" --type ts

ТЕСТИРОВАНИЕ:

# 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