# 🏗️ ТЕХНИЧЕСКАЯ ДОКУМЕНТАЦИЯ: 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_