
- ✅ Добавлено поле 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>
32 KiB
🏗️ ТЕХНИЧЕСКАЯ ДОКУМЕНТАЦИЯ: V2 АРХИТЕКТУРА УСЛУГ
Статус: ✅ PRODUCTION READY
Версия: V2.0
Дата создания: 03.09.2025
Область: Модульная архитектура услуг фулфилмента
🎯 ОБЗОР V2 АРХИТЕКТУРЫ УСЛУГ
ФИЛОСОФИЯ V2:
"Один домен - одна модель - одна ответственность"
V2 архитектура услуг основана на принципе доменной специализации, где каждый тип данных имеет собственную оптимизированную структуру, резолверы и логику обработки.
ОСНОВНЫЕ ПРИНЦИПЫ:
- Доменная изоляция - каждый тип услуг в отдельной таблице
- Специализированные резолверы - отдельные файлы для каждого домена
- Безопасность по умолчанию - автоматическая изоляция по fulfillmentId
- Масштабируемость - независимое развитие каждого типа
🗄️ V2 МОДЕЛИ ДАННЫХ
1. FULFILLMENT SERVICE (Услуги)
Назначение: Управление услугами, предоставляемыми фулфилментом
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 (Расходники)
Назначение: Управление расходными материалами фулфилмента
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 (Логистика)
Назначение: Управление логистическими маршрутами
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
СХЕМА ТИПОВ
# ОСНОВНЫЕ ТИПЫ
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
type Query {
# УСЛУГИ ФУЛФИЛМЕНТА
myFulfillmentServices: [FulfillmentService!]!
fulfillmentServicesByFulfillment(fulfillmentId: ID!): [FulfillmentService!]!
# РАСХОДНИКИ ФУЛФИЛМЕНТА
myFulfillmentConsumables: [FulfillmentConsumable!]!
fulfillmentConsumablesByFulfillment(fulfillmentId: ID!): [FulfillmentConsumable!]!
# ЛОГИСТИКА ФУЛФИЛМЕНТА
myFulfillmentLogistics: [FulfillmentLogistics!]!
fulfillmentLogisticsByFulfillment(fulfillmentId: ID!): [FulfillmentLogistics!]!
}
V2 MUTATIONS
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 ТИПЫ
# СОЗДАНИЕ УСЛУГИ
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 ТИПЫ
# УНИВЕРСАЛЬНЫЕ ОТВЕТЫ
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
АРХИТЕКТУРА РЕЗОЛВЕРОВ
// Файл: /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 РЕЗОЛВЕРА
// УСЛУГИ ФУЛФИЛМЕНТА (собственные)
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 РЕЗОЛВЕРА
// СОЗДАНИЕ УСЛУГИ
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,
}
}
},
БЕЗОПАСНОСТЬ РЕЗОЛВЕРОВ
ДОМЕННАЯ ИЗОЛЯЦИЯ:
// ВСЕГДА ФИЛЬТРОВАТЬ ПО fulfillmentId
const services = await prisma.fulfillmentService.findMany({
where: {
fulfillmentId: user.organization.id, // ← КРИТИЧЕСКАЯ БЕЗОПАСНОСТЬ
isActive: true,
},
})
ВАЛИДАЦИЯ ДАННЫХ:
// ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ
if (!input.name?.trim()) {
return { success: false, message: 'Название обязательно' }
}
// ПРОВЕРКА БИЗНЕС-ЛОГИКИ
if (!input.price || input.price <= 0) {
return { success: false, message: 'Цена должна быть больше нуля' }
}
ОБРАБОТКА ОШИБОК:
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
// Пример: 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
// 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
// Паттерн обновления кэша в 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
)
}
})
}
}
}
})
🚀 ПРОИЗВОДИТЕЛЬНОСТЬ И ОПТИМИЗАЦИЯ
ИНДЕКСЫ БД
-- ОСНОВНЫЕ ИНДЕКСЫ
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 ОПТИМИЗАЦИЯ
// ОПТИМИЗИРОВАННЫЕ QUERY ПОЛИТИКИ
const { data } = useQuery(GET_MY_FULFILLMENT_SERVICES_V2, {
fetchPolicy: 'cache-and-network', // Быстрый отклик + актуальность
errorPolicy: 'all', // Показ частичных данных при ошибках
notifyOnNetworkStatusChange: true, // UI обратная связь
})
ПАГИНАЦИЯ (ГОТОВНОСТЬ)
# РАСШИРЕННЫЕ QUERIES (для будущих версий)
type Query {
fulfillmentServices(
fulfillmentId: ID!
first: Int
after: String
filter: FulfillmentServiceFilter
): FulfillmentServiceConnection!
}
type FulfillmentServiceConnection {
edges: [FulfillmentServiceEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
🔒 БЕЗОПАСНОСТЬ V2
ДОМЕННАЯ ИЗОЛЯЦИЯ
// ПРИНЦИП: Пользователь видит только свои данные
const services = await prisma.fulfillmentService.findMany({
where: {
fulfillmentId: user.organization.id, // ← КРИТИЧЕСКАЯ СТРОКА
isActive: true,
},
})
ПРОВЕРКА ПРАВ ДОСТУПА
// ДОПОЛНИТЕЛЬНАЯ ПРОВЕРКА ДЛЯ КРИТИЧЕСКИХ ОПЕРАЦИЙ
if (user.organization.type !== 'FULFILLMENT') {
throw new Error('Доступ запрещен: требуется тип организации FULFILLMENT')
}
ВАЛИДАЦИЯ ВХОДНЫХ ДАННЫХ
// ЗАЩИТА ОТ НЕКОРРЕКТНЫХ ДАННЫХ
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('Цена слишком высокая')
}
}
САНИТИЗАЦИЯ ДАННЫХ
// ОЧИСТКА ВХОДЯЩИХ ДАННЫХ
const sanitizedInput = {
...input,
name: input.name?.trim(),
description: input.description?.trim() || null,
price: parseFloat(input.price.toFixed(2)), // 2 знака после запятой
}
📈 МОНИТОРИНГ И ОТЛАДКА
ЛОГИРОВАНИЕ ОПЕРАЦИЙ
// В резолверах
console.warn('V2 Service Operation:', {
operation: 'createFulfillmentService',
fulfillmentId: user.organization.id,
serviceName: input.name,
timestamp: new Date().toISOString(),
})
ERROR HANDLING
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,
}
}
ПРОИЗВОДИТЕЛЬНОСТЬ МОНИТОРИНГ
// ЗАМЕРЫ ВРЕМЕНИ ВЫПОЛНЕНИЯ
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 ТЕСТЫ РЕЗОЛВЕРОВ
// Пример теста резолвера
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')
})
})
})
})
ИНТЕГРАЦИОННЫЕ ТЕСТЫ
// Тест полного цикла 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 ТЕСТЫ КОМПОНЕНТОВ
// 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 МОДЕЛЬ:
model FulfillmentService {
// ... существующие поля
newField String? // Новое поле
}
2. ДОБАВИТЬ В GRAPHQL СХЕМУ:
type FulfillmentService {
# ... существующие поля
newField: String
}
input CreateFulfillmentServiceInput {
# ... существующие поля
newField: String
}
3. ОБНОВИТЬ РЕЗОЛВЕРЫ:
createFulfillmentService: async (_, { input }, context) => {
const service = await prisma.fulfillmentService.create({
data: {
...input,
newField: input.newField, // ← Добавить обработку
fulfillmentId: user.organization.id,
},
})
}
4. ОБНОВИТЬ UI КОМПОНЕНТЫ:
// В forms
<Input
name="newField"
value={formData.newField}
onChange={handleInputChange}
/>
// В отображении
<span>{service.newField}</span>
ДОБАВЛЕНИЕ НОВОГО ТИПА УСЛУГ
1. СОЗДАТЬ НОВУЮ 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. СОЗДАТЬ РЕЗОЛВЕРЫ:
// fulfillment-new-type-v2.ts
export const fulfillmentNewTypeQueries = {
myFulfillmentNewType: async (_, __, context) => {
// Логика запроса
},
}
export const fulfillmentNewTypeMutations = {
createFulfillmentNewType: async (_, { input }, context) => {
// Логика создания
},
}
3. ПОДКЛЮЧИТЬ К ОСНОВНЫМ РЕЗОЛВЕРАМ:
// В index.ts
import { fulfillmentNewTypeQueries, fulfillmentNewTypeMutations } from './fulfillment-new-type-v2'
const mergedResolvers = mergeResolvers(
// ... существующие резолверы
{
Query: fulfillmentNewTypeQueries,
Mutation: fulfillmentNewTypeMutations,
},
)
4. СОЗДАТЬ UI КОМПОНЕНТ:
// 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>
)
}
ИНТЕГРАЦИЯ С ВНЕШНИМИ СИСТЕМАМИ
// Пример: интеграция с внешним 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
ЗАПЛАНИРОВАННЫЕ УЛУЧШЕНИЯ:
- Q4 2025: Пагинация для больших списков
- Q1 2026: Поиск и фильтрация в GraphQL
- Q2 2026: Bulk операции (массовое создание/обновление)
- Q3 2026: Интеграция с внешними системами учета
- 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