Files
sfera-new/docs/development/V2_ARCHITECTURE_SERVICES.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

1087 lines
32 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 АРХИТЕКТУРА УСЛУГ
> **Статус**: ✅ **PRODUCTION READY**
> **Версия**: V2.0
> **Дата создания**: 03.09.2025
> **Область**: Модульная архитектура услуг фулфилмента
---
## 🎯 ОБЗОР V2 АРХИТЕКТУРЫ УСЛУГ
### ФИЛОСОФИЯ V2:
> **"Один домен - одна модель - одна ответственность"**
V2 архитектура услуг основана на принципе доменной специализации, где каждый тип данных имеет собственную оптимизированную структуру, резолверы и логику обработки.
### ОСНОВНЫЕ ПРИНЦИПЫ:
1. **Доменная изоляция** - каждый тип услуг в отдельной таблице
2. **Специализированные резолверы** - отдельные файлы для каждого домена
3. **Безопасность по умолчанию** - автоматическая изоляция по fulfillmentId
4. **Масштабируемость** - независимое развитие каждого типа
---
## 🗄️ V2 МОДЕЛИ ДАННЫХ
### 1. FULFILLMENT SERVICE (Услуги)
**Назначение**: Управление услугами, предоставляемыми фулфилментом
```prisma
model FulfillmentService {
id String @id @default(cuid())
fulfillmentId String // Изоляция по домену
name String
description String?
price Decimal @db.Decimal(10, 2)
unit String @default("шт")
isActive Boolean @default(true)
imageUrl String? // Поддержка изображений
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Связи
fulfillment Organization @relation("FulfillmentServicesV2", fields: [fulfillmentId], references: [id])
// Индексы производительности
@@index([fulfillmentId, isActive])
@@map("fulfillment_services_v2")
}
```
**Ключевые особенности:**
- Decimal для точных денежных расчетов
- Поддержка изображений услуг
- Сортировка для UI
- Мягкое удаление через isActive
### 2. FULFILLMENT CONSUMABLE (Расходники)
**Назначение**: Управление расходными материалами фулфилмента
```prisma
model FulfillmentConsumable {
id String @id @default(cuid())
fulfillmentId String // Доменная привязка
warehouseConsumableId String // Связь со складом
name String
description String?
pricePerUnit Decimal? @db.Decimal(10, 2)
unit String @default("шт")
imageUrl String?
warehouseStock Int @default(0)
isAvailable Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Связи
fulfillment Organization @relation("FulfillmentConsumablesV2", fields: [fulfillmentId], references: [id])
warehouseConsumable WarehouseConsumable @relation("FulfillmentWarehouseConsumables", fields: [warehouseConsumableId], references: [id])
// Индексы
@@index([fulfillmentId, isAvailable])
@@index([warehouseConsumableId])
@@unique([fulfillmentId, warehouseConsumableId])
@@map("fulfillment_consumables_v2")
}
```
**Ключевые особенности:**
- Связь с складскими остатками
- Автоматическая синхронизация наличия
- Уникальность в рамках фулфилмента
- Опциональная цена (может устанавливаться позже)
### 3. FULFILLMENT LOGISTICS (Логистика)
**Назначение**: Управление логистическими маршрутами
```prisma
model FulfillmentLogistics {
id String @id @default(cuid())
fulfillmentId String // Изоляция домена
fromLocation String
toLocation String
priceUnder1m3 Decimal @db.Decimal(10, 2)
priceOver1m3 Decimal @db.Decimal(10, 2)
estimatedDays Int @default(1)
description String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Связи
fulfillment Organization @relation("FulfillmentLogisticsV2", fields: [fulfillmentId], references: [id])
// Индексы
@@index([fulfillmentId, isActive])
@@index([fromLocation, toLocation])
@@map("fulfillment_logistics_v2")
}
```
**Ключевые особенности:**
- Объемное ценообразование (до/свыше 1м³)
- Оценка времени доставки
- Географическая индексация
- Поддержка множественных маршрутов
---
## 🔌 V2 GRAPHQL API
### СХЕМА ТИПОВ
```graphql
# ОСНОВНЫЕ ТИПЫ
type FulfillmentService {
id: ID!
fulfillmentId: String!
name: String!
description: String
price: Float!
unit: String!
isActive: Boolean!
imageUrl: String
sortOrder: Int!
createdAt: DateTime!
updatedAt: DateTime!
fulfillment: Organization!
}
type FulfillmentConsumable {
id: ID!
fulfillmentId: String!
warehouseConsumableId: String!
name: String!
description: String
pricePerUnit: Float
unit: String!
imageUrl: String
warehouseStock: Int!
isAvailable: Boolean!
createdAt: DateTime!
updatedAt: DateTime!
fulfillment: Organization!
warehouseConsumable: WarehouseConsumable!
}
type FulfillmentLogistics {
id: ID!
fulfillmentId: String!
fromLocation: String!
toLocation: String!
priceUnder1m3: Float!
priceOver1m3: Float!
estimatedDays: Int!
description: String
isActive: Boolean!
createdAt: DateTime!
updatedAt: DateTime!
fulfillment: Organization!
}
```
### V2 QUERIES
```graphql
type Query {
# УСЛУГИ ФУЛФИЛМЕНТА
myFulfillmentServices: [FulfillmentService!]!
fulfillmentServicesByFulfillment(fulfillmentId: ID!): [FulfillmentService!]!
# РАСХОДНИКИ ФУЛФИЛМЕНТА
myFulfillmentConsumables: [FulfillmentConsumable!]!
fulfillmentConsumablesByFulfillment(fulfillmentId: ID!): [FulfillmentConsumable!]!
# ЛОГИСТИКА ФУЛФИЛМЕНТА
myFulfillmentLogistics: [FulfillmentLogistics!]!
fulfillmentLogisticsByFulfillment(fulfillmentId: ID!): [FulfillmentLogistics!]!
}
```
### V2 MUTATIONS
```graphql
type Mutation {
# УСЛУГИ - CRUD
createFulfillmentService(input: CreateFulfillmentServiceInput!): FulfillmentServiceResponse!
updateFulfillmentService(input: UpdateFulfillmentServiceInput!): FulfillmentServiceResponse!
deleteFulfillmentService(id: ID!): Boolean!
# РАСХОДНИКИ - CRUD
createFulfillmentConsumable(input: CreateFulfillmentConsumableInput!): FulfillmentConsumableResponse!
updateFulfillmentConsumable(input: UpdateFulfillmentConsumableInput!): FulfillmentConsumableResponse!
deleteFulfillmentConsumable(id: ID!): Boolean!
# ЛОГИСТИКА - CRUD
createFulfillmentLogistics(input: CreateFulfillmentLogisticsInput!): FulfillmentLogisticsResponse!
updateFulfillmentLogistics(input: UpdateFulfillmentLogisticsInput!): FulfillmentLogisticsResponse!
deleteFulfillmentLogistics(id: ID!): Boolean!
}
```
### INPUT ТИПЫ
```graphql
# СОЗДАНИЕ УСЛУГИ
input CreateFulfillmentServiceInput {
name: String!
description: String
price: Float!
unit: String!
imageUrl: String
sortOrder: Int
}
# ОБНОВЛЕНИЕ УСЛУГИ
input UpdateFulfillmentServiceInput {
id: ID!
name: String
description: String
price: Float
unit: String
isActive: Boolean
imageUrl: String
sortOrder: Int
}
# СОЗДАНИЕ РАСХОДНИКА
input CreateFulfillmentConsumableInput {
warehouseConsumableId: String!
pricePerUnit: Float
}
# ОБНОВЛЕНИЕ РАСХОДНИКА
input UpdateFulfillmentConsumableInput {
id: ID!
pricePerUnit: Float
}
# СОЗДАНИЕ ЛОГИСТИКИ
input CreateFulfillmentLogisticsInput {
fromLocation: String!
toLocation: String!
priceUnder1m3: Float!
priceOver1m3: Float!
estimatedDays: Int!
description: String
}
# ОБНОВЛЕНИЕ ЛОГИСТИКИ
input UpdateFulfillmentLogisticsInput {
id: ID!
fromLocation: String
toLocation: String
priceUnder1m3: Float
priceOver1m3: Float
estimatedDays: Int
description: String
isActive: Boolean
}
```
### RESPONSE ТИПЫ
```graphql
# УНИВЕРСАЛЬНЫЕ ОТВЕТЫ
type FulfillmentServiceResponse {
success: Boolean!
message: String!
service: FulfillmentService
}
type FulfillmentConsumableResponse {
success: Boolean!
message: String!
consumable: FulfillmentConsumable
}
type FulfillmentLogisticsResponse {
success: Boolean!
message: String!
logistics: FulfillmentLogistics
}
```
---
## 🔧 V2 РЕЗОЛВЕРЫ IMPLEMENTATION
### АРХИТЕКТУРА РЕЗОЛВЕРОВ
```typescript
// Файл: /src/graphql/resolvers/fulfillment-services-v2.ts
// СТРУКТУРА:
export const fulfillmentServicesQueries = {
myFulfillmentServices: [Query resolver],
myFulfillmentConsumables: [Query resolver],
myFulfillmentLogistics: [Query resolver],
fulfillmentServicesByFulfillment: [Query resolver],
fulfillmentConsumablesByFulfillment: [Query resolver],
fulfillmentLogisticsByFulfillment: [Query resolver],
}
export const fulfillmentServicesMutations = {
createFulfillmentService: [Mutation resolver],
updateFulfillmentService: [Mutation resolver],
deleteFulfillmentService: [Mutation resolver],
createFulfillmentConsumable: [Mutation resolver],
updateFulfillmentConsumable: [Mutation resolver],
deleteFulfillmentConsumable: [Mutation resolver],
createFulfillmentLogistics: [Mutation resolver],
updateFulfillmentLogistics: [Mutation resolver],
deleteFulfillmentLogistics: [Mutation resolver],
}
```
### ПРИМЕР QUERY РЕЗОЛВЕРА
```typescript
// УСЛУГИ ФУЛФИЛМЕНТА (собственные)
myFulfillmentServices: async (_: unknown, __: unknown, context: Context) => {
try {
const { user } = context
if (!user?.organization?.id) {
throw new Error('Организация не найдена')
}
// ДОМЕННАЯ БЕЗОПАСНОСТЬ: только свои услуги
const services = await prisma.fulfillmentService.findMany({
where: {
fulfillmentId: user.organization.id,
isActive: true,
},
include: {
fulfillment: true,
},
orderBy: [
{ sortOrder: 'asc' },
{ name: 'asc' },
],
})
return services
} catch (error) {
console.error('Error fetching fulfillment services:', error)
throw error
}
},
```
### ПРИМЕР MUTATION РЕЗОЛВЕРА
```typescript
// СОЗДАНИЕ УСЛУГИ
createFulfillmentService: async (
_: unknown,
{ input }: { input: CreateFulfillmentServiceInput },
context: Context,
) => {
try {
const { user } = context
if (!user?.organization?.id) {
throw new Error('Организация не найдена')
}
// ВАЛИДАЦИЯ ВХОДНЫХ ДАННЫХ
if (!input.name?.trim()) {
return {
success: false,
message: 'Название услуги обязательно',
service: null,
}
}
if (!input.price || input.price <= 0) {
return {
success: false,
message: 'Цена должна быть больше нуля',
service: null,
}
}
// СОЗДАНИЕ С ДОМЕННОЙ ИЗОЛЯЦИЕЙ
const service = await prisma.fulfillmentService.create({
data: {
...input,
fulfillmentId: user.organization.id, // Автоматическая привязка
},
include: {
fulfillment: true,
},
})
return {
success: true,
message: 'Услуга успешно создана',
service,
}
} catch (error) {
console.error('Error creating fulfillment service:', error)
return {
success: false,
message: 'Ошибка при создании услуги',
service: null,
}
}
},
```
### БЕЗОПАСНОСТЬ РЕЗОЛВЕРОВ
#### ДОМЕННАЯ ИЗОЛЯЦИЯ:
```typescript
// ВСЕГДА ФИЛЬТРОВАТЬ ПО fulfillmentId
const services = await prisma.fulfillmentService.findMany({
where: {
fulfillmentId: user.organization.id, // ← КРИТИЧЕСКАЯ БЕЗОПАСНОСТЬ
isActive: true,
},
})
```
#### ВАЛИДАЦИЯ ДАННЫХ:
```typescript
// ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ
if (!input.name?.trim()) {
return { success: false, message: 'Название обязательно' }
}
// ПРОВЕРКА БИЗНЕС-ЛОГИКИ
if (!input.price || input.price <= 0) {
return { success: false, message: 'Цена должна быть больше нуля' }
}
```
#### ОБРАБОТКА ОШИБОК:
```typescript
try {
// Логика резолвера
} catch (error) {
console.error('Error in resolver:', error)
return {
success: false,
message: 'Внутренняя ошибка сервера',
[entity]: null,
}
}
```
---
## 🎨 V2 FRONTEND КОМПОНЕНТЫ
### АРХИТЕКТУРА КОМПОНЕНТОВ
```
/src/components/services/
├── services-dashboard.tsx # Главный dashboard с табами
├── services-tab.tsx # Управление услугами
├── supplies-tab.tsx # Управление расходниками
└── logistics-tab.tsx # Управление логистикой
```
### ПАТТЕРН КОМПОНЕНТА V2
```typescript
// Пример: services-tab.tsx
'use client'
import { useQuery, useMutation } from '@apollo/client'
import {
GET_MY_FULFILLMENT_SERVICES_V2,
CREATE_FULFILLMENT_SERVICE,
UPDATE_FULFILLMENT_SERVICE,
DELETE_FULFILLMENT_SERVICE
} from '@/graphql/queries/fulfillment-services-v2'
export function ServicesTab() {
// V2 QUERY
const { data, loading, error, refetch } = useQuery(GET_MY_FULFILLMENT_SERVICES_V2, {
fetchPolicy: 'cache-and-network',
})
// V2 MUTATIONS
const [createService] = useMutation(CREATE_FULFILLMENT_SERVICE, {
update: (cache, { data }) => {
if (data?.createFulfillmentService?.success) {
// ОБНОВЛЕНИЕ APOLLO CACHE
const existingData = cache.readQuery({ query: GET_MY_FULFILLMENT_SERVICES_V2 })
if (existingData) {
cache.writeQuery({
query: GET_MY_FULFILLMENT_SERVICES_V2,
data: {
myFulfillmentServices: [
...existingData.myFulfillmentServices,
data.createFulfillmentService.service
]
}
})
}
}
}
})
// V2 DATA ACCESS
const services = data?.myFulfillmentServices || []
// UI ЛОГИКА ОСТАЕТСЯ НЕИЗМЕННОЙ
return (
<div className="services-management">
{/* Рендер услуг */}
</div>
)
}
```
### URL МАРШРУТИЗАЦИЯ V2
```typescript
// services-dashboard.tsx - URL управление
import { usePathname, useRouter } from 'next/navigation'
export function ServicesDashboard() {
const pathname = usePathname()
const router = useRouter()
// ОПРЕДЕЛЕНИЕ АКТИВНОГО ТАБА ПО URL
const getActiveTab = () => {
if (pathname.includes('/services/services')) return 'services'
if (pathname.includes('/services/logistics')) return 'logistics'
if (pathname.includes('/services/consumables')) return 'consumables'
return 'services'
}
// НАВИГАЦИЯ МЕЖДУ ТАБАМИ
const handleTabChange = (tab: string) => {
switch (tab) {
case 'services':
router.push('/fulfillment/services/services')
break
case 'consumables':
router.push('/fulfillment/services/consumables')
break
case 'logistics':
router.push('/fulfillment/services/logistics')
break
}
}
return (
<div className="services-dashboard">
{/* Tab navigation с уникальными URL */}
</div>
)
}
```
### APOLLO CLIENT CACHE MANAGEMENT
```typescript
// Паттерн обновления кэша в V2
const [updateService] = useMutation(UPDATE_FULFILLMENT_SERVICE, {
update: (cache, { data }) => {
if (data?.updateFulfillmentService?.success && data.updateFulfillmentService.service) {
// ОБНОВЛЕНИЕ СУЩЕСТВУЮЩИХ ДАННЫХ В КЭШЕ
const existingData = cache.readQuery({
query: GET_MY_FULFILLMENT_SERVICES_V2
}) as { myFulfillmentServices: FulfillmentService[] } | null
if (existingData) {
const updatedService = data.updateFulfillmentService.service
cache.writeQuery({
query: GET_MY_FULFILLMENT_SERVICES_V2,
data: {
myFulfillmentServices: existingData.myFulfillmentServices.map(service =>
service.id === updatedService.id ? updatedService : service
)
}
})
}
}
}
})
```
---
## 🚀 ПРОИЗВОДИТЕЛЬНОСТЬ И ОПТИМИЗАЦИЯ
### ИНДЕКСЫ БД
```sql
-- ОСНОВНЫЕ ИНДЕКСЫ
CREATE INDEX "fulfillment_services_v2_fulfillmentId_isActive_idx"
ON "fulfillment_services_v2"("fulfillmentId", "isActive");
CREATE INDEX "fulfillment_consumables_v2_fulfillmentId_isAvailable_idx"
ON "fulfillment_consumables_v2"("fulfillmentId", "isAvailable");
CREATE INDEX "fulfillment_logistics_v2_fromLocation_toLocation_idx"
ON "fulfillment_logistics_v2"("fromLocation", "toLocation");
```
### APOLLO CLIENT ОПТИМИЗАЦИЯ
```typescript
// ОПТИМИЗИРОВАННЫЕ QUERY ПОЛИТИКИ
const { data } = useQuery(GET_MY_FULFILLMENT_SERVICES_V2, {
fetchPolicy: 'cache-and-network', // Быстрый отклик + актуальность
errorPolicy: 'all', // Показ частичных данных при ошибках
notifyOnNetworkStatusChange: true, // UI обратная связь
})
```
### ПАГИНАЦИЯ (ГОТОВНОСТЬ)
```graphql
# РАСШИРЕННЫЕ QUERIES (для будущих версий)
type Query {
fulfillmentServices(
fulfillmentId: ID!
first: Int
after: String
filter: FulfillmentServiceFilter
): FulfillmentServiceConnection!
}
type FulfillmentServiceConnection {
edges: [FulfillmentServiceEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
```
---
## 🔒 БЕЗОПАСНОСТЬ V2
### ДОМЕННАЯ ИЗОЛЯЦИЯ
```typescript
// ПРИНЦИП: Пользователь видит только свои данные
const services = await prisma.fulfillmentService.findMany({
where: {
fulfillmentId: user.organization.id, // ← КРИТИЧЕСКАЯ СТРОКА
isActive: true,
},
})
```
### ПРОВЕРКА ПРАВ ДОСТУПА
```typescript
// ДОПОЛНИТЕЛЬНАЯ ПРОВЕРКА ДЛЯ КРИТИЧЕСКИХ ОПЕРАЦИЙ
if (user.organization.type !== 'FULFILLMENT') {
throw new Error('Доступ запрещен: требуется тип организации FULFILLMENT')
}
```
### ВАЛИДАЦИЯ ВХОДНЫХ ДАННЫХ
```typescript
// ЗАЩИТА ОТ НЕКОРРЕКТНЫХ ДАННЫХ
const validateServiceInput = (input: CreateFulfillmentServiceInput) => {
if (!input.name?.trim()) {
throw new Error('Название услуги обязательно')
}
if (input.name.length > 255) {
throw new Error('Название слишком длинное')
}
if (input.price <= 0) {
throw new Error('Цена должна быть положительной')
}
if (input.price > 1000000) {
throw new Error('Цена слишком высокая')
}
}
```
### САНИТИЗАЦИЯ ДАННЫХ
```typescript
// ОЧИСТКА ВХОДЯЩИХ ДАННЫХ
const sanitizedInput = {
...input,
name: input.name?.trim(),
description: input.description?.trim() || null,
price: parseFloat(input.price.toFixed(2)), // 2 знака после запятой
}
```
---
## 📈 МОНИТОРИНГ И ОТЛАДКА
### ЛОГИРОВАНИЕ ОПЕРАЦИЙ
```typescript
// В резолверах
console.warn('V2 Service Operation:', {
operation: 'createFulfillmentService',
fulfillmentId: user.organization.id,
serviceName: input.name,
timestamp: new Date().toISOString(),
})
```
### ERROR HANDLING
```typescript
try {
// Бизнес-логика
} catch (error) {
// СТРУКТУРИРОВАННОЕ ЛОГИРОВАНИЕ ОШИБОК
console.error('V2 Service Error:', {
operation: 'createFulfillmentService',
error: error.message,
stack: error.stack,
input: JSON.stringify(input),
user: user.phone,
timestamp: new Date().toISOString(),
})
// ПОЛЬЗОВАТЕЛЮ ПОКАЗЫВАЕМ БЕЗОПАСНОЕ СООБЩЕНИЕ
return {
success: false,
message: 'Внутренняя ошибка сервера',
service: null,
}
}
```
### ПРОИЗВОДИТЕЛЬНОСТЬ МОНИТОРИНГ
```typescript
// ЗАМЕРЫ ВРЕМЕНИ ВЫПОЛНЕНИЯ
const startTime = Date.now()
const services = await prisma.fulfillmentService.findMany({
where: { fulfillmentId: user.organization.id },
})
const endTime = Date.now()
console.warn('V2 Query Performance:', {
operation: 'myFulfillmentServices',
duration: `${endTime - startTime}ms`,
resultCount: services.length,
})
```
---
## 🧪 ТЕСТИРОВАНИЕ V2
### UNIT ТЕСТЫ РЕЗОЛВЕРОВ
```typescript
// Пример теста резолвера
describe('FulfillmentServices V2 Resolvers', () => {
describe('myFulfillmentServices', () => {
it('should return only services for current fulfillment', async () => {
const context = mockContext({ organizationId: 'fulfillment-1' })
const result = await fulfillmentServicesQueries.myFulfillmentServices(
{},
{},
context
)
expect(result).toHaveLength(2)
result.forEach(service => {
expect(service.fulfillmentId).toBe('fulfillment-1')
})
})
})
})
```
### ИНТЕГРАЦИОННЫЕ ТЕСТЫ
```typescript
// Тест полного цикла CRUD
describe('Fulfillment Services Integration', () => {
it('should create, read, update, delete service', async () => {
// CREATE
const createResult = await createFulfillmentService({
name: 'Test Service',
price: 100,
unit: 'шт'
})
expect(createResult.success).toBe(true)
// READ
const services = await myFulfillmentServices()
expect(services).toContainEqual(
expect.objectContaining({ name: 'Test Service' })
)
// UPDATE
const updateResult = await updateFulfillmentService({
id: createResult.service.id,
price: 150
})
expect(updateResult.success).toBe(true)
// DELETE
const deleteResult = await deleteFulfillmentService(createResult.service.id)
expect(deleteResult).toBe(true)
})
})
```
### E2E ТЕСТЫ КОМПОНЕНТОВ
```typescript
// Cypress тест UI
describe('Services Management V2', () => {
it('should manage services through UI', () => {
cy.visit('/fulfillment/services/services')
// Создание услуги
cy.get('[data-testid="add-service-btn"]').click()
cy.get('[data-testid="service-name"]').type('Test Service')
cy.get('[data-testid="service-price"]').type('100')
cy.get('[data-testid="save-btn"]').click()
// Проверка создания
cy.contains('Test Service').should('exist')
cy.contains('100 ₽').should('exist')
})
})
```
---
## 📚 РУКОВОДСТВО ПО РАСШИРЕНИЮ V2
### ДОБАВЛЕНИЕ НОВОГО ПОЛЯ
#### 1. ОБНОВИТЬ PRISMA МОДЕЛЬ:
```prisma
model FulfillmentService {
// ... существующие поля
newField String? // Новое поле
}
```
#### 2. ДОБАВИТЬ В GRAPHQL СХЕМУ:
```graphql
type FulfillmentService {
# ... существующие поля
newField: String
}
input CreateFulfillmentServiceInput {
# ... существующие поля
newField: String
}
```
#### 3. ОБНОВИТЬ РЕЗОЛВЕРЫ:
```typescript
createFulfillmentService: async (_, { input }, context) => {
const service = await prisma.fulfillmentService.create({
data: {
...input,
newField: input.newField, // ← Добавить обработку
fulfillmentId: user.organization.id,
},
})
}
```
#### 4. ОБНОВИТЬ UI КОМПОНЕНТЫ:
```typescript
// В forms
<Input
name="newField"
value={formData.newField}
onChange={handleInputChange}
/>
// В отображении
<span>{service.newField}</span>
```
### ДОБАВЛЕНИЕ НОВОГО ТИПА УСЛУГ
#### 1. СОЗДАТЬ НОВУЮ PRISMA МОДЕЛЬ:
```prisma
model FulfillmentNewType {
id String @id @default(cuid())
fulfillmentId String
// ... специфичные поля
fulfillment Organization @relation("FulfillmentNewTypeV2", fields: [fulfillmentId], references: [id])
@@index([fulfillmentId])
@@map("fulfillment_new_type_v2")
}
```
#### 2. СОЗДАТЬ РЕЗОЛВЕРЫ:
```typescript
// fulfillment-new-type-v2.ts
export const fulfillmentNewTypeQueries = {
myFulfillmentNewType: async (_, __, context) => {
// Логика запроса
},
}
export const fulfillmentNewTypeMutations = {
createFulfillmentNewType: async (_, { input }, context) => {
// Логика создания
},
}
```
#### 3. ПОДКЛЮЧИТЬ К ОСНОВНЫМ РЕЗОЛВЕРАМ:
```typescript
// В index.ts
import { fulfillmentNewTypeQueries, fulfillmentNewTypeMutations } from './fulfillment-new-type-v2'
const mergedResolvers = mergeResolvers(
// ... существующие резолверы
{
Query: fulfillmentNewTypeQueries,
Mutation: fulfillmentNewTypeMutations,
},
)
```
#### 4. СОЗДАТЬ UI КОМПОНЕНТ:
```typescript
// new-type-tab.tsx
export function NewTypeTab() {
const { data } = useQuery(GET_MY_FULFILLMENT_NEW_TYPE_V2)
const newTypeItems = data?.myFulfillmentNewType || []
return (
<div className="new-type-management">
{/* UI для управления новым типом */}
</div>
)
}
```
### ИНТЕГРАЦИЯ С ВНЕШНИМИ СИСТЕМАМИ
```typescript
// Пример: интеграция с внешним API
const createFulfillmentServiceWithExternalSync = async (input, context) => {
// 1. Создать в локальной БД
const service = await prisma.fulfillmentService.create({
data: { ...input, fulfillmentId: context.user.organization.id }
})
// 2. Синхронизировать с внешней системой
try {
await ExternalAPI.syncService(service)
} catch (error) {
console.warn('External sync failed:', error)
// Продолжаем работу без внешней синхронизации
}
return { success: true, service }
}
```
---
## 🎯 ЛУЧШИЕ ПРАКТИКИ V2
### 1. ИМЕНОВАНИЕ
- **Модели**: `FulfillmentServiceType` - ясное доменное имя
- **Резолверы**: `myFulfillmentServices` - владение + тип данных
- **Файлы**: `fulfillment-services-v2.ts` - домен + версия
### 2. СТРУКТУРА ДАННЫХ
- **Всегда включать `fulfillmentId`** для доменной изоляции
- **Использовать `Decimal`** для денежных полей
- **Добавлять `createdAt/updatedAt`** для аудита
- **Использовать `isActive`** вместо жесткого удаления
### 3. БЕЗОПАСНОСТЬ
- **Фильтровать по `fulfillmentId`** во всех запросах
- **Валидировать входные данные** на уровне резолверов
- **Логировать критические операции**
- **Использовать `try/catch`** с корректной обработкой ошибок
### 4. ПРОИЗВОДИТЕЛЬНОСТЬ
- **Добавлять индексы** на часто используемые поля
- **Использовать `include`** вместо отдельных запросов
- **Применять `cache-and-network`** политику Apollo
- **Мониторить время выполнения** запросов
### 5. ПОДДЕРЖИВАЕМОСТЬ
- **Документировать сложную логику**
- **Писать unit тесты** для резолверов
- **Использовать TypeScript** для типизации
- **Следовать принципу единой ответственности**
---
## 🔮 ROADMAP V2
### ЗАПЛАНИРОВАННЫЕ УЛУЧШЕНИЯ:
1. **Q4 2025**: Пагинация для больших списков
2. **Q1 2026**: Поиск и фильтрация в GraphQL
3. **Q2 2026**: Bulk операции (массовое создание/обновление)
4. **Q3 2026**: Интеграция с внешними системами учета
5. **Q4 2026**: Аналитика и отчеты по услугам
### ПОТЕНЦИАЛЬНЫЕ РАСШИРЕНИЯ:
- **Версионирование услуг** - отслеживание изменений цен
- **Категоризация услуг** - группировка по типам
- **Шаблоны услуг** - быстрое создание похожих услуг
- **Интеграция с календарем** - планирование предоставления услуг
- **Система скидок** - гибкое ценообразование
---
## 📖 ЗАКЛЮЧЕНИЕ
V2 архитектура услуг SFERA представляет собой современное, масштабируемое и безопасное решение для управления услугами фулфилмента. Ключевые достижения:
**🎯 АРХИТЕКТУРНЫЕ ПРИНЦИПЫ:**
- Доменная специализация моделей данных
- Модульность резолверов и компонентов
- Безопасность на уровне доменов
- Производительность через оптимизированные индексы
**🚀 ТЕХНИЧЕСКИЕ ПРЕИМУЩЕСТВА:**
- Полная типизация TypeScript
- Оптимизированные GraphQL запросы
- Эффективный Apollo Client кэш
- Comprehensive error handling
**🛡️ ГОТОВНОСТЬ К PRODUCTION:**
- Тщательное тестирование (Unit + Integration + E2E)
- Monitoring и logging
- Безопасная обработка ошибок
- Документированные API
Данная архитектура служит эталоном для будущих V2 систем SFERA и может быть адаптирована для других доменов платформы.
---
окумент создан: 03.09.2025_
_Версия: V2.0_
_Статус: Production Ready_
_Команда: SFERA Development Team_