# 🏗️ ТЕХНИЧЕСКАЯ ДОКУМЕНТАЦИЯ: 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 (
{/* Рендер услуг */}
)
}
```
### 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 (
{/* Tab navigation с уникальными URL */}
)
}
```
### 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
// В отображении
{service.newField}
```
### ДОБАВЛЕНИЕ НОВОГО ТИПА УСЛУГ
#### 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 (
{/* UI для управления новым типом */}
)
}
```
### ИНТЕГРАЦИЯ С ВНЕШНИМИ СИСТЕМАМИ
```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_