diff --git a/prisma/schema.backup.20250901_020359.prisma b/prisma/schema.backup.20250901_020359.prisma
new file mode 100644
index 0000000..0c8d1a3
--- /dev/null
+++ b/prisma/schema.backup.20250901_020359.prisma
@@ -0,0 +1,1045 @@
+generator client {
+ provider = "prisma-client-js"
+}
+
+generator seed {
+ provider = "prisma-client-js"
+ output = "./generated/client"
+}
+
+datasource db {
+ provider = "postgresql"
+ url = env("DATABASE_URL")
+}
+
+model User {
+ id String @id @default(cuid())
+ phone String @unique
+ avatar String?
+ managerName String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ organizationId String?
+ sentMessages Message[] @relation("SentMessages")
+ smsCodes SmsCode[]
+ organization Organization? @relation(fields: [organizationId], references: [id])
+
+ // === НОВЫЕ СВЯЗИ С ПРИЕМКОЙ ПОСТАВОК V2 ===
+ fulfillmentSupplyOrdersReceived FulfillmentConsumableSupplyOrder[] @relation("FFSupplyOrdersReceiver")
+ sellerSupplyOrdersReceived SellerConsumableSupplyOrder[] @relation("SellerSupplyOrdersReceiver")
+
+ @@map("users")
+}
+
+model Admin {
+ id String @id @default(cuid())
+ username String @unique
+ password String
+ email String? @unique
+ isActive Boolean @default(true)
+ lastLogin DateTime?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ @@map("admins")
+}
+
+model SmsCode {
+ id String @id @default(cuid())
+ code String
+ phone String
+ expiresAt DateTime
+ isUsed Boolean @default(false)
+ attempts Int @default(0)
+ maxAttempts Int @default(3)
+ createdAt DateTime @default(now())
+ userId String?
+ user User? @relation(fields: [userId], references: [id])
+
+ @@map("sms_codes")
+}
+
+model Organization {
+ id String @id @default(cuid())
+ inn String @unique
+ kpp String?
+ name String?
+ fullName String?
+ ogrn String?
+ ogrnDate DateTime?
+ type OrganizationType
+ market String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ address String?
+ addressFull String?
+ status String?
+ actualityDate DateTime?
+ registrationDate DateTime?
+ liquidationDate DateTime?
+ managementName String?
+ managementPost String?
+ opfCode String?
+ opfFull String?
+ opfShort String?
+ okato String?
+ oktmo String?
+ okpo String?
+ okved String?
+ phones Json?
+ emails Json?
+ employeeCount Int?
+ revenue BigInt?
+ taxSystem String?
+ dadataData Json?
+ referralCode String? @unique
+ referredById String?
+ referralPoints Int @default(0)
+ apiKeys ApiKey[]
+ carts Cart?
+ counterpartyOf Counterparty[] @relation("CounterpartyOf")
+ organizationCounterparties Counterparty[] @relation("OrganizationCounterparties")
+ receivedRequests CounterpartyRequest[] @relation("ReceivedRequests")
+ sentRequests CounterpartyRequest[] @relation("SentRequests")
+ employees Employee[]
+ externalAds ExternalAd[] @relation("ExternalAds")
+ favorites Favorites[]
+ logistics Logistics[]
+ receivedMessages Message[] @relation("ReceivedMessages")
+ sentMessages Message[] @relation("SentMessages")
+ referredBy Organization? @relation("ReferralRelation", fields: [referredById], references: [id])
+ referrals Organization[] @relation("ReferralRelation")
+ products Product[]
+ referralTransactions ReferralTransaction[] @relation("ReferralTransactions")
+ referrerTransactions ReferralTransaction[] @relation("ReferrerTransactions")
+ sellerStatsCaches SellerStatsCache[] @relation("SellerStatsCaches")
+ services Service[]
+ supplies Supply[]
+ sellerSupplies Supply[] @relation("SellerSupplies")
+ fulfillmentSupplyOrders SupplyOrder[] @relation("SupplyOrderFulfillmentCenter")
+ logisticsSupplyOrders SupplyOrder[] @relation("SupplyOrderLogistics")
+ supplyOrders SupplyOrder[]
+ partnerSupplyOrders SupplyOrder[] @relation("SupplyOrderPartner")
+ supplySuppliers SupplySupplier[] @relation("SupplySuppliers")
+ users User[]
+ wbWarehouseCaches WBWarehouseCache[] @relation("WBWarehouseCaches")
+ wildberriesSupplies WildberriesSupply[]
+
+ // === НОВЫЕ СВЯЗИ С ПОСТАВКАМИ V2 ===
+ // Поставки расходников ФФ
+ fulfillmentSupplyOrdersAsFulfillment FulfillmentConsumableSupplyOrder[] @relation("FFSupplyOrdersFulfillment")
+ fulfillmentSupplyOrdersAsSupplier FulfillmentConsumableSupplyOrder[] @relation("FFSupplyOrdersSupplier")
+ fulfillmentSupplyOrdersAsLogistics FulfillmentConsumableSupplyOrder[] @relation("FFSupplyOrdersLogistics")
+
+ // Поставки расходников селлера
+ sellerSupplyOrdersAsSeller SellerConsumableSupplyOrder[] @relation("SellerSupplyOrdersSeller")
+ sellerSupplyOrdersAsFulfillment SellerConsumableSupplyOrder[] @relation("SellerSupplyOrdersFulfillment")
+ sellerSupplyOrdersAsSupplier SellerConsumableSupplyOrder[] @relation("SellerSupplyOrdersSupplier")
+
+ // === НОВЫЕ СВЯЗИ СО СКЛАДСКИМИ ОСТАТКАМИ V2 ===
+ fulfillmentInventory FulfillmentConsumableInventory[] @relation("FFInventory")
+ sellerSupplyOrdersAsLogistics SellerConsumableSupplyOrder[] @relation("SellerSupplyOrdersLogistics")
+
+ // === СВЯЗИ С ИНВЕНТАРЕМ РАСХОДНИКОВ СЕЛЛЕРА V2 ===
+ sellerInventoryAsOwner SellerConsumableInventory[] @relation("SellerInventory")
+ sellerInventoryAsWarehouse SellerConsumableInventory[] @relation("SellerInventoryWarehouse")
+
+ @@index([referralCode])
+ @@index([referredById])
+ @@map("organizations")
+}
+
+model ApiKey {
+ id String @id @default(cuid())
+ marketplace MarketplaceType
+ apiKey String
+ isActive Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ validationData Json?
+ organizationId String
+ organization Organization @relation(fields: [organizationId], references: [id])
+
+ @@unique([organizationId, marketplace])
+ @@map("api_keys")
+}
+
+model CounterpartyRequest {
+ id String @id @default(cuid())
+ status CounterpartyRequestStatus @default(PENDING)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ senderId String
+ receiverId String
+ message String?
+ receiver Organization @relation("ReceivedRequests", fields: [receiverId], references: [id])
+ sender Organization @relation("SentRequests", fields: [senderId], references: [id])
+
+ @@unique([senderId, receiverId])
+ @@map("counterparty_requests")
+}
+
+model Counterparty {
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+ organizationId String
+ counterpartyId String
+ type CounterpartyType @default(MANUAL)
+ triggeredBy String?
+ triggerEntityId String?
+ counterparty Organization @relation("CounterpartyOf", fields: [counterpartyId], references: [id])
+ organization Organization @relation("OrganizationCounterparties", fields: [organizationId], references: [id])
+
+ @@unique([organizationId, counterpartyId])
+ @@index([type])
+ @@map("counterparties")
+}
+
+model Message {
+ id String @id @default(cuid())
+ content String?
+ type MessageType @default(TEXT)
+ voiceUrl String?
+ voiceDuration Int?
+ fileUrl String?
+ fileName String?
+ fileSize Int?
+ fileType String?
+ isRead Boolean @default(false)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ senderId String
+ senderOrganizationId String
+ receiverOrganizationId String
+ receiverOrganization Organization @relation("ReceivedMessages", fields: [receiverOrganizationId], references: [id])
+ sender User @relation("SentMessages", fields: [senderId], references: [id])
+ senderOrganization Organization @relation("SentMessages", fields: [senderOrganizationId], references: [id])
+
+ @@index([senderOrganizationId, receiverOrganizationId, createdAt])
+ @@index([receiverOrganizationId, isRead])
+ @@map("messages")
+}
+
+model Service {
+ id String @id @default(cuid())
+ name String
+ description String?
+ price Decimal @db.Decimal(10, 2)
+ imageUrl String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ organizationId String
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+
+ @@map("services")
+}
+
+model Supply {
+ id String @id @default(cuid())
+ name String
+ article String
+ description String?
+ price Decimal @db.Decimal(10, 2)
+ pricePerUnit Decimal? @db.Decimal(10, 2)
+ quantity Int @default(0)
+ unit String @default("шт")
+ category String @default("Расходники")
+ status String @default("planned")
+ date DateTime @default(now())
+ supplier String @default("Не указан")
+ minStock Int @default(0)
+ currentStock Int @default(0)
+ usedStock Int @default(0)
+ imageUrl String?
+ type SupplyType @default(FULFILLMENT_CONSUMABLES)
+ sellerOwnerId String?
+ shopLocation String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ organizationId String
+ actualQuantity Int?
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+ sellerOwner Organization? @relation("SellerSupplies", fields: [sellerOwnerId], references: [id])
+
+ @@map("supplies")
+}
+
+model Category {
+ id String @id @default(cuid())
+ name String @unique
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ products Product[]
+
+ @@map("categories")
+}
+
+model Product {
+ id String @id @default(cuid())
+ name String
+ article String
+ description String?
+ price Decimal @db.Decimal(12, 2)
+ pricePerSet Decimal? @db.Decimal(12, 2)
+ quantity Int @default(0)
+ setQuantity Int?
+ ordered Int?
+ inTransit Int?
+ stock Int?
+ sold Int?
+ type ProductType @default(PRODUCT)
+ categoryId String?
+ brand String?
+ color String?
+ size String?
+ weight Decimal? @db.Decimal(8, 3)
+ dimensions String?
+ material String?
+ images Json @default("[]")
+ mainImage String?
+ isActive Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ organizationId String
+ cartItems CartItem[]
+ favorites Favorites[]
+ category Category? @relation(fields: [categoryId], references: [id])
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+ supplyOrderItems SupplyOrderItem[]
+
+ // === НОВЫЕ СВЯЗИ С ПОСТАВКАМИ V2 ===
+ fulfillmentSupplyItems FulfillmentConsumableSupplyItem[] @relation("FFSupplyItems")
+ sellerSupplyItems SellerConsumableSupplyItem[] @relation("SellerSupplyItems")
+
+ // === НОВЫЕ СВЯЗИ СО СКЛАДСКИМИ ОСТАТКАМИ V2 ===
+ inventoryRecords FulfillmentConsumableInventory[] @relation("InventoryProducts")
+ sellerInventoryRecords SellerConsumableInventory[] @relation("SellerInventoryProducts")
+
+ @@unique([organizationId, article])
+ @@map("products")
+}
+
+model Cart {
+ id String @id @default(cuid())
+ organizationId String @unique
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ items CartItem[]
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+
+ @@map("carts")
+}
+
+model CartItem {
+ id String @id @default(cuid())
+ cartId String
+ productId String
+ quantity Int @default(1)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ cart Cart @relation(fields: [cartId], references: [id], onDelete: Cascade)
+ product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
+
+ @@unique([cartId, productId])
+ @@map("cart_items")
+}
+
+model Favorites {
+ id String @id @default(cuid())
+ organizationId String
+ productId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+ product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
+
+ @@unique([organizationId, productId])
+ @@map("favorites")
+}
+
+model Employee {
+ id String @id @default(cuid())
+ firstName String
+ lastName String
+ middleName String?
+ birthDate DateTime?
+ avatar String?
+ passportPhoto String?
+ passportSeries String?
+ passportNumber String?
+ passportIssued String?
+ passportDate DateTime?
+ address String?
+ position String
+ department String?
+ hireDate DateTime
+ salary Float?
+ status EmployeeStatus @default(ACTIVE)
+ phone String
+ email String?
+ telegram String?
+ whatsapp String?
+ emergencyContact String?
+ emergencyPhone String?
+ organizationId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ scheduleRecords EmployeeSchedule[]
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+ supplyOrders SupplyOrder[] @relation("SupplyOrderResponsible")
+
+ @@map("employees")
+}
+
+model EmployeeSchedule {
+ id String @id @default(cuid())
+ date DateTime
+ status ScheduleStatus
+ hoursWorked Float?
+ overtimeHours Float?
+ notes String?
+ employeeId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
+
+ @@unique([employeeId, date])
+ @@map("employee_schedules")
+}
+
+model WildberriesSupply {
+ id String @id @default(cuid())
+ organizationId String
+ deliveryDate DateTime?
+ status WildberriesSupplyStatus @default(DRAFT)
+ totalAmount Decimal @db.Decimal(12, 2)
+ totalItems Int
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+ cards WildberriesSupplyCard[]
+
+ @@map("wildberries_supplies")
+}
+
+model WildberriesSupplyCard {
+ id String @id @default(cuid())
+ supplyId String
+ nmId String
+ vendorCode String
+ title String
+ brand String?
+ price Decimal @db.Decimal(12, 2)
+ discountedPrice Decimal? @db.Decimal(12, 2)
+ quantity Int
+ selectedQuantity Int
+ selectedMarket String?
+ selectedPlace String?
+ sellerName String?
+ sellerPhone String?
+ deliveryDate DateTime?
+ mediaFiles Json @default("[]")
+ selectedServices Json @default("[]")
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ supply WildberriesSupply @relation(fields: [supplyId], references: [id], onDelete: Cascade)
+
+ @@map("wildberries_supply_cards")
+}
+
+model Logistics {
+ id String @id @default(cuid())
+ fromLocation String
+ toLocation String
+ priceUnder1m3 Float
+ priceOver1m3 Float
+ description String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ organizationId String
+ organization Organization @relation(fields: [organizationId], references: [id])
+ routes SupplyRoute[] @relation("SupplyRouteLogistics")
+
+ @@map("logistics")
+}
+
+model SupplyOrder {
+ id String @id @default(cuid())
+ partnerId String
+ deliveryDate DateTime
+ status SupplyOrderStatus @default(PENDING)
+ totalAmount Decimal @db.Decimal(12, 2)
+ totalItems Int
+ fulfillmentCenterId String?
+ logisticsPartnerId String?
+ consumableType String?
+ // Новые поля для многоуровневой системы поставок
+ packagesCount Int? // Количество грузовых мест (от поставщика)
+ volume Float? // Объём товара в м³ (от поставщика)
+ responsibleEmployee String? // ID ответственного сотрудника ФФ
+ notes String? // Заметки и комментарии
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ organizationId String
+ items SupplyOrderItem[]
+ routes SupplyRoute[] // Связь с маршрутами поставки
+ fulfillmentCenter Organization? @relation("SupplyOrderFulfillmentCenter", fields: [fulfillmentCenterId], references: [id])
+ logisticsPartner Organization? @relation("SupplyOrderLogistics", fields: [logisticsPartnerId], references: [id])
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+ partner Organization @relation("SupplyOrderPartner", fields: [partnerId], references: [id])
+ employee Employee? @relation("SupplyOrderResponsible", fields: [responsibleEmployee], references: [id])
+
+ @@map("supply_orders")
+}
+
+// Модель для маршрутов поставки (модульная архитектура)
+model SupplyRoute {
+ id String @id @default(cuid())
+ supplyOrderId String
+ logisticsId String? // Ссылка на предустановленный маршрут из Logistics
+ fromLocation String // Точка забора (рынок/поставщик)
+ toLocation String // Точка доставки (фулфилмент)
+ fromAddress String? // Полный адрес точки забора
+ toAddress String? // Полный адрес точки доставки
+ distance Float? // Расстояние в км
+ estimatedTime Int? // Время доставки в часах
+ price Decimal? @db.Decimal(10, 2) // Стоимость логистики
+ status String? @default("pending") // Статус маршрута
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ createdDate DateTime @default(now()) // Дата создания маршрута (уровень 2)
+ supplyOrder SupplyOrder @relation(fields: [supplyOrderId], references: [id], onDelete: Cascade)
+ logistics Logistics? @relation("SupplyRouteLogistics", fields: [logisticsId], references: [id])
+
+ @@map("supply_routes")
+}
+
+model SupplyOrderItem {
+ id String @id @default(cuid())
+ supplyOrderId String
+ productId String
+ quantity Int
+ price Decimal @db.Decimal(12, 2)
+ totalPrice Decimal @db.Decimal(12, 2)
+ services String[] @default([])
+ fulfillmentConsumables String[] @default([])
+ sellerConsumables String[] @default([])
+ marketplaceCardId String?
+ // ОТКАТ: recipe Json? // Полная рецептура в JSON формате - ЗАКОММЕНТИРОВАНО
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ product Product @relation(fields: [productId], references: [id])
+ supplyOrder SupplyOrder @relation(fields: [supplyOrderId], references: [id], onDelete: Cascade)
+
+ @@unique([supplyOrderId, productId])
+ @@map("supply_order_items")
+}
+
+model SupplySupplier {
+ id String @id @default(cuid())
+ name String
+ contactName String
+ phone String
+ market String?
+ address String?
+ place String?
+ telegram String?
+ organizationId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ organization Organization @relation("SupplySuppliers", fields: [organizationId], references: [id], onDelete: Cascade)
+
+ @@map("supply_suppliers")
+}
+
+model ExternalAd {
+ id String @id @default(cuid())
+ name String
+ url String
+ cost Decimal @db.Decimal(12, 2)
+ date DateTime
+ nmId String
+ clicks Int @default(0)
+ organizationId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ organization Organization @relation("ExternalAds", fields: [organizationId], references: [id], onDelete: Cascade)
+
+ @@index([organizationId, date])
+ @@map("external_ads")
+}
+
+model WBWarehouseCache {
+ id String @id @default(cuid())
+ organizationId String
+ cacheDate DateTime
+ data Json
+ totalProducts Int @default(0)
+ totalStocks Int @default(0)
+ totalReserved Int @default(0)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ organization Organization @relation("WBWarehouseCaches", fields: [organizationId], references: [id], onDelete: Cascade)
+
+ @@unique([organizationId, cacheDate])
+ @@index([organizationId, cacheDate])
+ @@map("wb_warehouse_caches")
+}
+
+model SellerStatsCache {
+ id String @id @default(cuid())
+ organizationId String
+ cacheDate DateTime
+ period String
+ dateFrom DateTime?
+ dateTo DateTime?
+ productsData Json?
+ productsTotalSales Decimal? @db.Decimal(15, 2)
+ productsTotalOrders Int?
+ productsCount Int?
+ advertisingData Json?
+ advertisingTotalCost Decimal? @db.Decimal(15, 2)
+ advertisingTotalViews Int?
+ advertisingTotalClicks Int?
+ expiresAt DateTime
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ organization Organization @relation("SellerStatsCaches", fields: [organizationId], references: [id], onDelete: Cascade)
+
+ @@unique([organizationId, cacheDate, period, dateFrom, dateTo])
+ @@index([organizationId, cacheDate])
+ @@index([expiresAt])
+ @@map("seller_stats_caches")
+}
+
+model ReferralTransaction {
+ id String @id @default(cuid())
+ referrerId String
+ referralId String
+ points Int
+ type ReferralTransactionType
+ description String?
+ createdAt DateTime @default(now())
+ referral Organization @relation("ReferralTransactions", fields: [referralId], references: [id])
+ referrer Organization @relation("ReferrerTransactions", fields: [referrerId], references: [id])
+
+ @@index([referrerId, createdAt])
+ @@index([referralId])
+ @@map("referral_transactions")
+}
+
+enum OrganizationType {
+ FULFILLMENT
+ SELLER
+ LOGIST
+ WHOLESALE
+}
+
+enum MarketplaceType {
+ WILDBERRIES
+ OZON
+}
+
+enum CounterpartyRequestStatus {
+ PENDING
+ ACCEPTED
+ REJECTED
+ CANCELLED
+}
+
+enum MessageType {
+ TEXT
+ VOICE
+ IMAGE
+ FILE
+}
+
+enum EmployeeStatus {
+ ACTIVE
+ VACATION
+ SICK
+ FIRED
+}
+
+enum ScheduleStatus {
+ WORK
+ WEEKEND
+ VACATION
+ SICK
+ ABSENT
+}
+
+enum SupplyOrderStatus {
+ PENDING
+ CONFIRMED
+ IN_TRANSIT
+ SUPPLIER_APPROVED
+ LOGISTICS_CONFIRMED
+ SHIPPED
+ DELIVERED
+ CANCELLED
+}
+
+enum WildberriesSupplyStatus {
+ DRAFT
+ CREATED
+ IN_PROGRESS
+ DELIVERED
+ CANCELLED
+}
+
+enum ProductType {
+ PRODUCT
+ CONSUMABLE
+}
+
+enum SupplyType {
+ FULFILLMENT_CONSUMABLES
+ SELLER_CONSUMABLES
+}
+
+enum CounterpartyType {
+ MANUAL
+ REFERRAL
+ AUTO_BUSINESS
+ AUTO
+}
+
+enum ReferralTransactionType {
+ REGISTRATION
+ AUTO_PARTNERSHIP
+ FIRST_ORDER
+ MONTHLY_BONUS
+}
+
+model AuditLog {
+ id String @id @default(cuid())
+ userId String
+ organizationType OrganizationType
+ action String
+ resourceType String
+ resourceId String?
+ metadata Json @default("{}")
+ ipAddress String?
+ userAgent String?
+ timestamp DateTime @default(now())
+
+ @@index([userId])
+ @@index([timestamp])
+ @@index([action])
+ @@index([resourceType])
+ @@map("audit_logs")
+}
+
+model SecurityAlert {
+ id String @id @default(cuid())
+ type SecurityAlertType
+ severity SecurityAlertSeverity
+ userId String
+ message String
+ metadata Json @default("{}")
+ timestamp DateTime @default(now())
+ resolved Boolean @default(false)
+
+ @@index([userId])
+ @@index([timestamp])
+ @@index([resolved])
+ @@index([severity])
+ @@map("security_alerts")
+}
+
+enum SecurityAlertType {
+ EXCESSIVE_ACCESS
+ UNAUTHORIZED_ATTEMPT
+ DATA_LEAK_RISK
+ SUSPICIOUS_PATTERN
+ BULK_EXPORT_DETECTED
+ RULE_VIOLATION
+}
+
+enum SecurityAlertSeverity {
+ LOW
+ MEDIUM
+ HIGH
+ CRITICAL
+}
+
+// ===============================================
+// НОВАЯ СИСТЕМА ПОСТАВОК V2.0
+// ===============================================
+
+// Новый enum для статусов поставок v2
+enum SupplyOrderStatusV2 {
+ PENDING // Ожидает одобрения поставщика
+ SUPPLIER_APPROVED // Одобрено поставщиком
+ LOGISTICS_CONFIRMED // Логистика подтверждена
+ SHIPPED // Отгружено поставщиком
+ IN_TRANSIT // В пути
+ DELIVERED // Доставлено и принято
+ CANCELLED // Отменено
+}
+
+// 5-статусная система для поставок расходников селлера
+enum SellerSupplyOrderStatus {
+ PENDING // Ожидает одобрения поставщика
+ APPROVED // Одобрено поставщиком
+ SHIPPED // Отгружено
+ DELIVERED // Доставлено
+ COMPLETED // Завершено
+ CANCELLED // Отменено
+}
+
+// Модель для поставок расходников фулфилмента
+model FulfillmentConsumableSupplyOrder {
+ // === БАЗОВЫЕ ПОЛЯ ===
+ id String @id @default(cuid())
+ status SupplyOrderStatusV2 @default(PENDING)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // === ДАННЫЕ ФФ (создатель) ===
+ fulfillmentCenterId String // кто заказывает (FK: Organization)
+ requestedDeliveryDate DateTime // когда нужно
+ resalePricePerUnit Decimal? @db.Decimal(10, 2) // цена продажи селлерам
+ minStockLevel Int? // минимальный остаток
+ notes String? // заметки ФФ
+
+ // === ДАННЫЕ ПОСТАВЩИКА ===
+ supplierId String? // кто поставляет (FK: Organization)
+ supplierApprovedAt DateTime? // когда одобрил
+ packagesCount Int? // количество грузомест
+ estimatedVolume Decimal? @db.Decimal(8, 3) // объем груза в м³
+ supplierContractId String? // номер договора
+ supplierNotes String? // заметки поставщика
+
+ // === ДАННЫЕ ЛОГИСТИКИ ===
+ logisticsPartnerId String? // кто везет (FK: Organization)
+ estimatedDeliveryDate DateTime? // план доставки
+ routeId String? // маршрут (FK: LogisticsRoute)
+ logisticsCost Decimal? @db.Decimal(10, 2) // стоимость доставки
+ logisticsNotes String? // заметки логистики
+
+ // === ДАННЫЕ ОТГРУЗКИ ===
+ shippedAt DateTime? // факт отгрузки
+ trackingNumber String? // номер отслеживания
+
+ // === ДАННЫЕ ПРИЕМКИ ===
+ receivedAt DateTime? // факт приемки
+ receivedById String? // кто принял (FK: User)
+ actualQuantity Int? // принято количество
+ defectQuantity Int? // брак
+ receiptNotes String? // заметки приемки
+
+ // === СВЯЗИ ===
+ fulfillmentCenter Organization @relation("FFSupplyOrdersFulfillment", fields: [fulfillmentCenterId], references: [id])
+ supplier Organization? @relation("FFSupplyOrdersSupplier", fields: [supplierId], references: [id])
+ logisticsPartner Organization? @relation("FFSupplyOrdersLogistics", fields: [logisticsPartnerId], references: [id])
+ receivedBy User? @relation("FFSupplyOrdersReceiver", fields: [receivedById], references: [id])
+ items FulfillmentConsumableSupplyItem[]
+
+ @@map("fulfillment_consumable_supply_orders")
+}
+
+model FulfillmentConsumableSupplyItem {
+ id String @id @default(cuid())
+ supplyOrderId String // связь с поставкой
+ productId String // какой расходник (FK: Product)
+
+ // === КОЛИЧЕСТВА ===
+ requestedQuantity Int // запросили
+ approvedQuantity Int? // поставщик одобрил
+ shippedQuantity Int? // отгрузили
+ receivedQuantity Int? // приняли
+ defectQuantity Int? @default(0) // брак
+
+ // === ЦЕНЫ ===
+ unitPrice Decimal @db.Decimal(10, 2) // цена за единицу от поставщика
+ totalPrice Decimal @db.Decimal(12, 2) // общая стоимость
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // === СВЯЗИ ===
+ supplyOrder FulfillmentConsumableSupplyOrder @relation(fields: [supplyOrderId], references: [id], onDelete: Cascade)
+ product Product @relation("FFSupplyItems", fields: [productId], references: [id])
+
+ @@unique([supplyOrderId, productId])
+ @@map("fulfillment_consumable_supply_items")
+}
+
+// =============================================================================
+// 📦 СИСТЕМА ПОСТАВОК РАСХОДНИКОВ СЕЛЛЕРА
+// =============================================================================
+
+// Модель для поставок расходников селлера
+model SellerConsumableSupplyOrder {
+ // === БАЗОВЫЕ ПОЛЯ ===
+ id String @id @default(cuid())
+ status SellerSupplyOrderStatus @default(PENDING)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // === ДАННЫЕ СЕЛЛЕРА (создатель) ===
+ sellerId String // кто заказывает (FK: Organization SELLER)
+ fulfillmentCenterId String // куда доставлять (FK: Organization FULFILLMENT)
+ requestedDeliveryDate DateTime // когда нужно
+ notes String? // заметки селлера
+
+ // === ДАННЫЕ ПОСТАВЩИКА ===
+ supplierId String? // кто поставляет (FK: Organization WHOLESALE)
+ supplierApprovedAt DateTime? // когда одобрил
+ packagesCount Int? // количество грузомест
+ estimatedVolume Decimal? @db.Decimal(8, 3) // объем груза в м³
+ supplierContractId String? // номер договора
+ supplierNotes String? // заметки поставщика
+
+ // === ДАННЫЕ ЛОГИСТИКИ ===
+ logisticsPartnerId String? // кто везет (FK: Organization LOGIST)
+ estimatedDeliveryDate DateTime? // план доставки
+ routeId String? // маршрут (FK: LogisticsRoute)
+ logisticsCost Decimal? @db.Decimal(10, 2) // стоимость доставки
+ logisticsNotes String? // заметки логистики
+
+ // === ДАННЫЕ ОТГРУЗКИ ===
+ shippedAt DateTime? // факт отгрузки
+ trackingNumber String? // номер отслеживания
+
+ // === ДАННЫЕ ПРИЕМКИ ===
+ deliveredAt DateTime? // факт доставки в ФФ
+ receivedById String? // кто принял в ФФ (FK: User)
+ actualQuantity Int? // принято количество
+ defectQuantity Int? // брак
+ receiptNotes String? // заметки приемки
+
+ // === ЭКОНОМИКА (для будущего раздела экономики) ===
+ totalCostWithDelivery Decimal? @db.Decimal(12, 2) // общая стоимость с доставкой
+ estimatedStorageCost Decimal? @db.Decimal(10, 2) // оценочная стоимость хранения
+
+ // === СВЯЗИ ===
+ seller Organization @relation("SellerSupplyOrdersSeller", fields: [sellerId], references: [id])
+ fulfillmentCenter Organization @relation("SellerSupplyOrdersFulfillment", fields: [fulfillmentCenterId], references: [id])
+ supplier Organization? @relation("SellerSupplyOrdersSupplier", fields: [supplierId], references: [id])
+ logisticsPartner Organization? @relation("SellerSupplyOrdersLogistics", fields: [logisticsPartnerId], references: [id])
+ receivedBy User? @relation("SellerSupplyOrdersReceiver", fields: [receivedById], references: [id])
+ items SellerConsumableSupplyItem[]
+
+ @@map("seller_consumable_supply_orders")
+}
+
+// Позиции в поставке расходников селлера
+model SellerConsumableSupplyItem {
+ id String @id @default(cuid())
+ supplyOrderId String // связь с поставкой
+ productId String // какой расходник (FK: Product)
+
+ // === КОЛИЧЕСТВА ===
+ requestedQuantity Int // запросили
+ approvedQuantity Int? // поставщик одобрил
+ shippedQuantity Int? // отгрузили
+ receivedQuantity Int? // приняли в ФФ
+ defectQuantity Int? @default(0) // брак
+
+ // === ЦЕНЫ ===
+ unitPrice Decimal @db.Decimal(10, 2) // цена за единицу от поставщика
+ totalPrice Decimal @db.Decimal(12, 2) // общая стоимость
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // === СВЯЗИ ===
+ supplyOrder SellerConsumableSupplyOrder @relation(fields: [supplyOrderId], references: [id], onDelete: Cascade)
+ product Product @relation("SellerSupplyItems", fields: [productId], references: [id])
+
+ @@unique([supplyOrderId, productId])
+ @@map("seller_consumable_supply_items")
+}
+
+// ===============================================
+// INVENTORY SYSTEM V2.0 - СКЛАДСКИЕ ОСТАТКИ
+// ===============================================
+
+// Складские остатки расходников фулфилмента
+model FulfillmentConsumableInventory {
+ // === ИДЕНТИФИКАЦИЯ ===
+ id String @id @default(cuid())
+
+ // === СВЯЗИ ===
+ fulfillmentCenterId String // где хранится (FK: Organization)
+ productId String // что хранится (FK: Product)
+
+ // === СКЛАДСКИЕ ДАННЫЕ ===
+ currentStock Int @default(0) // текущий остаток на складе
+ minStock Int @default(0) // минимальный порог для заказа
+ maxStock Int? // максимальный порог (опционально)
+ reservedStock Int @default(0) // зарезервировано для отгрузок
+ totalReceived Int @default(0) // всего получено с момента создания
+ totalShipped Int @default(0) // всего отгружено селлерам
+
+ // === ЦЕНЫ ===
+ averageCost Decimal @default(0) @db.Decimal(10, 2) // средняя себестоимость
+ resalePrice Decimal? @db.Decimal(10, 2) // цена продажи селлерам
+
+ // === МЕТАДАННЫЕ ===
+ lastSupplyDate DateTime? // последняя поставка
+ lastUsageDate DateTime? // последнее использование/отгрузка
+ notes String? // заметки по складскому учету
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // === СВЯЗИ ===
+ fulfillmentCenter Organization @relation("FFInventory", fields: [fulfillmentCenterId], references: [id])
+ product Product @relation("InventoryProducts", fields: [productId], references: [id])
+
+ // === ИНДЕКСЫ ===
+ @@unique([fulfillmentCenterId, productId]) // один товар = одна запись на фулфилмент
+ @@index([fulfillmentCenterId, currentStock])
+ @@index([currentStock, minStock]) // для поиска "заканчивающихся"
+ @@index([fulfillmentCenterId, lastSupplyDate])
+ @@map("fulfillment_consumable_inventory")
+}
+
+// === V2 SELLER CONSUMABLE INVENTORY SYSTEM ===
+// Система складского учета расходников селлера на складе фулфилмента
+model SellerConsumableInventory {
+ // === ИДЕНТИФИКАЦИЯ ===
+ id String @id @default(cuid())
+
+ // === СВЯЗИ ===
+ sellerId String // кому принадлежат расходники (FK: Organization SELLER)
+ fulfillmentCenterId String // где хранятся (FK: Organization FULFILLMENT)
+ productId String // что хранится (FK: Product)
+
+ // === СКЛАДСКИЕ ДАННЫЕ ===
+ currentStock Int @default(0) // текущий остаток на складе фулфилмента
+ minStock Int @default(0) // минимальный порог для автозаказа
+ maxStock Int? // максимальный порог (опционально)
+ reservedStock Int @default(0) // зарезервировано для использования селлером
+ totalReceived Int @default(0) // всего получено с момента создания
+ totalUsed Int @default(0) // всего использовано селлером
+
+ // === ЦЕНЫ ===
+ averageCost Decimal @default(0) @db.Decimal(10, 2) // средняя себестоимость покупки
+ usagePrice Decimal? @db.Decimal(10, 2) // цена списания/использования
+
+ // === МЕТАДАННЫЕ ===
+ lastSupplyDate DateTime? // последняя поставка
+ lastUsageDate DateTime? // последнее использование
+ notes String? // заметки по складскому учету
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // === СВЯЗИ ===
+ seller Organization @relation("SellerInventory", fields: [sellerId], references: [id])
+ fulfillmentCenter Organization @relation("SellerInventoryWarehouse", fields: [fulfillmentCenterId], references: [id])
+ product Product @relation("SellerInventoryProducts", fields: [productId], references: [id])
+
+ // === ИНДЕКСЫ ===
+ @@unique([sellerId, fulfillmentCenterId, productId]) // один товар = одна запись на связку селлер-фулфилмент
+ @@index([sellerId, currentStock])
+ @@index([fulfillmentCenterId, sellerId]) // для таблицы "Детализация по магазинам"
+ @@index([currentStock, minStock]) // для поиска "заканчивающихся"
+ @@index([sellerId, lastSupplyDate])
+ @@map("seller_consumable_inventory")
+}
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 0c8d1a3..80271c6 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -27,6 +27,7 @@ model User {
// === НОВЫЕ СВЯЗИ С ПРИЕМКОЙ ПОСТАВОК V2 ===
fulfillmentSupplyOrdersReceived FulfillmentConsumableSupplyOrder[] @relation("FFSupplyOrdersReceiver")
sellerSupplyOrdersReceived SellerConsumableSupplyOrder[] @relation("SellerSupplyOrdersReceiver")
+ sellerGoodsSupplyOrdersReceived SellerGoodsSupplyOrder[] @relation("SellerGoodsSupplyOrdersReceiver")
@@map("users")
}
@@ -143,6 +144,16 @@ model Organization {
// === СВЯЗИ С ИНВЕНТАРЕМ РАСХОДНИКОВ СЕЛЛЕРА V2 ===
sellerInventoryAsOwner SellerConsumableInventory[] @relation("SellerInventory")
sellerInventoryAsWarehouse SellerConsumableInventory[] @relation("SellerInventoryWarehouse")
+
+ // === СВЯЗИ С ТОВАРНЫМИ ПОСТАВКАМИ СЕЛЛЕРА V2 ===
+ sellerGoodsSupplyOrdersAsSeller SellerGoodsSupplyOrder[] @relation("SellerGoodsSupplyOrdersSeller")
+ sellerGoodsSupplyOrdersAsFulfillment SellerGoodsSupplyOrder[] @relation("SellerGoodsSupplyOrdersFulfillment")
+ sellerGoodsSupplyOrdersAsSupplier SellerGoodsSupplyOrder[] @relation("SellerGoodsSupplyOrdersSupplier")
+ sellerGoodsSupplyOrdersAsLogistics SellerGoodsSupplyOrder[] @relation("SellerGoodsSupplyOrdersLogistics")
+
+ // === СВЯЗИ С ИНВЕНТАРЕМ ТОВАРОВ СЕЛЛЕРА V2 ===
+ sellerGoodsInventoryAsOwner SellerGoodsInventory[] @relation("SellerGoodsInventoryOwner")
+ sellerGoodsInventoryAsWarehouse SellerGoodsInventory[] @relation("SellerGoodsInventoryWarehouse")
@@index([referralCode])
@@index([referredById])
@@ -314,6 +325,10 @@ model Product {
// === НОВЫЕ СВЯЗИ СО СКЛАДСКИМИ ОСТАТКАМИ V2 ===
inventoryRecords FulfillmentConsumableInventory[] @relation("InventoryProducts")
sellerInventoryRecords SellerConsumableInventory[] @relation("SellerInventoryProducts")
+
+ // === СВЯЗИ С ТОВАРНЫМИ ПОСТАВКАМИ V2 ===
+ goodsSupplyRecipeItems GoodsSupplyRecipeItem[] @relation("GoodsSupplyRecipeItems")
+ sellerGoodsInventoryRecords SellerGoodsInventory[] @relation("SellerGoodsInventoryProduct")
@@unique([organizationId, article])
@@map("products")
@@ -1043,3 +1058,126 @@ model SellerConsumableInventory {
@@index([sellerId, lastSupplyDate])
@@map("seller_consumable_inventory")
}
+
+// ===============================================
+// 🛒 SELLER GOODS SUPPLY SYSTEM V2.0 - ТОВАРНЫЕ ПОСТАВКИ
+// ===============================================
+
+// Модель для поставок товаров селлера (V2)
+model SellerGoodsSupplyOrder {
+ // === БАЗОВЫЕ ПОЛЯ ===
+ id String @id @default(cuid())
+ status SellerSupplyOrderStatus @default(PENDING)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // === ДАННЫЕ СЕЛЛЕРА (создатель) ===
+ sellerId String // кто заказывает (FK: Organization SELLER)
+ fulfillmentCenterId String // куда доставлять (FK: Organization FULFILLMENT)
+ requestedDeliveryDate DateTime // когда нужно
+ notes String? // заметки селлера
+
+ // === ДАННЫЕ ПОСТАВЩИКА ===
+ supplierId String? // кто поставляет (FK: Organization WHOLESALE)
+ supplierApprovedAt DateTime? // когда одобрил
+ packagesCount Int? // количество грузомест
+ estimatedVolume Decimal? @db.Decimal(8, 3) // объем груза в м³
+ supplierContractId String? // номер договора
+ supplierNotes String? // заметки поставщика
+
+ // === ДАННЫЕ ЛОГИСТИКИ ===
+ logisticsPartnerId String? // кто везет (FK: Organization LOGIST)
+ estimatedDeliveryDate DateTime? // план доставки
+ routeId String? // маршрут (FK: LogisticsRoute)
+ logisticsCost Decimal? @db.Decimal(10, 2) // стоимость доставки
+ logisticsNotes String? // заметки логистики
+
+ // === ДАННЫЕ ОТГРУЗКИ ===
+ shippedAt DateTime? // факт отгрузки
+ trackingNumber String? // номер отслеживания
+
+ // === ДАННЫЕ ПОЛУЧЕНИЯ ===
+ deliveredAt DateTime? // факт доставки
+ receivedById String? // кто принял (FK: User)
+ receiptNotes String? // заметки при получении
+
+ // === ФИНАНСЫ ===
+ totalCostWithDelivery Decimal @default(0) @db.Decimal(12, 2) // общая стоимость с доставкой
+ actualDeliveryCost Decimal @default(0) @db.Decimal(10, 2) // фактическая стоимость доставки
+
+ // === СВЯЗИ ===
+ seller Organization @relation("SellerGoodsSupplyOrdersSeller", fields: [sellerId], references: [id])
+ fulfillmentCenter Organization @relation("SellerGoodsSupplyOrdersFulfillment", fields: [fulfillmentCenterId], references: [id])
+ supplier Organization? @relation("SellerGoodsSupplyOrdersSupplier", fields: [supplierId], references: [id])
+ logisticsPartner Organization? @relation("SellerGoodsSupplyOrdersLogistics", fields: [logisticsPartnerId], references: [id])
+ receivedBy User? @relation("SellerGoodsSupplyOrdersReceiver", fields: [receivedById], references: [id])
+ recipeItems GoodsSupplyRecipeItem[] // нормализованная рецептура товаров
+
+ @@map("seller_goods_supply_orders")
+}
+
+// Нормализованная рецептура для товарных поставок
+model GoodsSupplyRecipeItem {
+ id String @id @default(cuid())
+ supplyOrderId String // связь с поставкой (FK: SellerGoodsSupplyOrder)
+ productId String // какой товар (FK: Product)
+ quantity Int // количество в рецептуре
+ recipeType RecipeType // тип компонента в рецептуре
+ createdAt DateTime @default(now())
+
+ // === СВЯЗИ ===
+ supplyOrder SellerGoodsSupplyOrder @relation(fields: [supplyOrderId], references: [id], onDelete: Cascade)
+ product Product @relation("GoodsSupplyRecipeItems", fields: [productId], references: [id])
+
+ @@unique([supplyOrderId, productId]) // один товар = одна запись в рецептуре
+ @@map("goods_supply_recipe_items")
+}
+
+// Enum для типов компонентов в рецептуре
+enum RecipeType {
+ MAIN_PRODUCT // Основной товар
+ COMPONENT // Компонент товара
+ PACKAGING // Упаковка
+ ACCESSORY // Аксессуар
+}
+
+// Инвентарь товаров селлера на складе фулфилмента (V2)
+model SellerGoodsInventory {
+ // === ИДЕНТИФИКАЦИЯ ===
+ id String @id @default(cuid())
+
+ // === СВЯЗИ ===
+ sellerId String // кому принадлежат товары (FK: Organization SELLER)
+ fulfillmentCenterId String // где хранятся (FK: Organization FULFILLMENT)
+ productId String // что хранится (FK: Product)
+
+ // === СКЛАДСКИЕ ДАННЫЕ ===
+ currentStock Int @default(0) // текущий остаток на складе фулфилмента
+ reservedStock Int @default(0) // зарезервировано для отгрузок
+ inPreparationStock Int @default(0) // в подготовке к отгрузке
+ totalReceived Int @default(0) // всего получено с момента создания
+ totalShipped Int @default(0) // всего отгружено
+
+ // === ПОРОГИ ДЛЯ АВТОЗАКАЗА ===
+ minStock Int @default(0) // минимальный порог для автозаказа
+ maxStock Int? // максимальный порог (опционально)
+
+ // === ЦЕНЫ ===
+ averageCost Decimal @default(0) @db.Decimal(10, 2) // средняя себестоимость покупки
+ salePrice Decimal @default(0) @db.Decimal(10, 2) // цена продажи
+
+ // === МЕТАДАННЫЕ ===
+ lastSupplyDate DateTime? // последняя поставка
+ lastShipDate DateTime? // последняя отгрузка
+ notes String? // заметки по складскому учету
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // === СВЯЗИ ===
+ seller Organization @relation("SellerGoodsInventoryOwner", fields: [sellerId], references: [id])
+ fulfillmentCenter Organization @relation("SellerGoodsInventoryWarehouse", fields: [fulfillmentCenterId], references: [id])
+ product Product @relation("SellerGoodsInventoryProduct", fields: [productId], references: [id])
+
+ @@unique([sellerId, fulfillmentCenterId, productId]) // уникальность: селлер + фф + товар
+ @@map("seller_goods_inventory")
+}
diff --git a/src/app/seller/create/goods/page.tsx b/src/app/seller/create/goods/page.tsx
index 335535d..18642b8 100644
--- a/src/app/seller/create/goods/page.tsx
+++ b/src/app/seller/create/goods/page.tsx
@@ -1,13 +1,10 @@
import { AuthGuard } from '@/components/auth-guard'
+import { CreateSuppliersSupplyPage } from '@/components/supplies/create-suppliers'
-// TODO: Создать компонент для создания товарных поставок
export default function CreateSellerGoodsPage() {
return (
-
-
Создание поставки товаров
-
Страница в разработке
-
+
)
}
\ No newline at end of file
diff --git a/src/components/supplies/supplies-dashboard.tsx b/src/components/supplies/supplies-dashboard.tsx
index e2cff93..a484602 100644
--- a/src/components/supplies/supplies-dashboard.tsx
+++ b/src/components/supplies/supplies-dashboard.tsx
@@ -399,7 +399,7 @@ export function SuppliesDashboard() {
- supply.consumableType !== 'SELLER_CONSUMABLES'
+ supply.consumableType !== 'SELLER_CONSUMABLES',
)}
loading={mySuppliesLoading}
/>
diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts
index cf4dbfb..04330e2 100644
--- a/src/graphql/resolvers.ts
+++ b/src/graphql/resolvers.ts
@@ -10,8 +10,9 @@ import { MarketplaceService } from '@/services/marketplace-service'
import { SmsService } from '@/services/sms-service'
import { WildberriesService } from '@/services/wildberries-service'
-import { fulfillmentInventoryV2Queries } from './resolvers/fulfillment-inventory-v2'
import { fulfillmentConsumableV2Queries as fulfillmentConsumableV2QueriesRestored, fulfillmentConsumableV2Mutations as fulfillmentConsumableV2MutationsRestored } from './resolvers/fulfillment-consumables-v2-restored'
+import { fulfillmentInventoryV2Queries } from './resolvers/fulfillment-inventory-v2'
+import { sellerGoodsQueries, sellerGoodsMutations } from './resolvers/goods-supply-v2'
import { logisticsConsumableV2Queries, logisticsConsumableV2Mutations } from './resolvers/logistics-consumables-v2'
import { sellerInventoryV2Queries } from './resolvers/seller-inventory-v2'
import { CommercialDataAudit } from './security/commercial-data-audit'
@@ -2913,6 +2914,9 @@ export const resolvers = {
// V2 система складских остатков расходников селлера
...sellerInventoryV2Queries,
+
+ // V2 система товарных поставок селлера
+ ...sellerGoodsQueries,
},
Mutation: {
@@ -10298,6 +10302,9 @@ resolvers.Mutation = {
// V2 mutations для логистики
...logisticsConsumableV2Mutations,
+
+ // V2 mutations для товарных поставок селлера
+ ...sellerGoodsMutations,
}
/* // Резолвер для парсинга JSON рецептуры в SupplyOrderItem
diff --git a/src/graphql/resolvers/goods-supply-v2.ts b/src/graphql/resolvers/goods-supply-v2.ts
index ffbc2c8..5066ad0 100644
--- a/src/graphql/resolvers/goods-supply-v2.ts
+++ b/src/graphql/resolvers/goods-supply-v2.ts
@@ -1,848 +1,745 @@
+// =============================================================================
+// 🛒 РЕЗОЛВЕРЫ ДЛЯ СИСТЕМЫ ПОСТАВОК ТОВАРОВ СЕЛЛЕРА V2
+// =============================================================================
+
import { GraphQLError } from 'graphql'
+import { processSellerGoodsSupplyReceipt } from '@/lib/inventory-management-goods'
import { prisma } from '@/lib/prisma'
+import { notifyOrganization } from '@/lib/realtime'
import { Context } from '../context'
-// ========== GOODS SUPPLY V2 RESOLVERS (ЗАКОММЕНТИРОВАНО) ==========
-// Раскомментируйте для активации системы товарных поставок V2
+// =============================================================================
+// 🔍 QUERY RESOLVERS V2
+// =============================================================================
-// ========== V2 RESOLVERS START ==========
+export const sellerGoodsQueries = {
+ // Мои товарные поставки (для селлеров - заказы которые я создал)
+ mySellerGoodsSupplies: async (_: unknown, __: unknown, context: Context) => {
+ if (!context.user) {
+ throw new GraphQLError('Требуется авторизация', {
+ extensions: { code: 'UNAUTHENTICATED' },
+ })
+ }
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: context.user.id },
+ include: { organization: true },
+ })
-export const goodsSupplyV2Resolvers = {
- Query: {
- // Товарные поставки селлера
- myGoodsSupplyOrdersV2: async (_: unknown, __: unknown, context: Context) => {
- const { user } = context
-
if (!user?.organization || user.organization.type !== 'SELLER') {
- throw new GraphQLError('Доступно только для селлеров', {
- extensions: { code: 'FORBIDDEN' },
- })
+ return []
}
- try {
- const orders = await prisma.goodsSupplyOrder.findMany({
+ const supplies = await prisma.sellerGoodsSupplyOrder.findMany({
+ where: {
+ sellerId: user.organizationId!,
+ },
+ include: {
+ seller: true,
+ fulfillmentCenter: true,
+ supplier: true,
+ logisticsPartner: true,
+ receivedBy: true,
+ recipeItems: {
+ include: {
+ product: true,
+ },
+ },
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
+ })
+
+ return supplies
+ } catch (error) {
+ console.error('Error fetching seller goods supplies:', error)
+ return []
+ }
+ },
+
+ // Входящие товарные заказы от селлеров (для фулфилмента)
+ incomingSellerGoodsSupplies: async (_: unknown, __: unknown, context: Context) => {
+ if (!context.user) {
+ throw new GraphQLError('Требуется авторизация', {
+ extensions: { code: 'UNAUTHENTICATED' },
+ })
+ }
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: context.user.id },
+ include: { organization: true },
+ })
+
+ if (!user?.organization || user.organization.type !== 'FULFILLMENT') {
+ return []
+ }
+
+ const supplies = await prisma.sellerGoodsSupplyOrder.findMany({
+ where: {
+ fulfillmentCenterId: user.organizationId!,
+ },
+ include: {
+ seller: true,
+ fulfillmentCenter: true,
+ supplier: true,
+ logisticsPartner: true,
+ receivedBy: true,
+ recipeItems: {
+ include: {
+ product: true,
+ },
+ },
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
+ })
+
+ return supplies
+ } catch (error) {
+ console.error('Error fetching incoming seller goods supplies:', error)
+ return []
+ }
+ },
+
+ // Товарные заказы от селлеров (для поставщиков)
+ mySellerGoodsSupplyRequests: async (_: unknown, __: unknown, context: Context) => {
+ if (!context.user) {
+ throw new GraphQLError('Требуется авторизация', {
+ extensions: { code: 'UNAUTHENTICATED' },
+ })
+ }
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: context.user.id },
+ include: { organization: true },
+ })
+
+ if (!user?.organization || user.organization.type !== 'WHOLESALE') {
+ return []
+ }
+
+ const supplies = await prisma.sellerGoodsSupplyOrder.findMany({
+ where: {
+ supplierId: user.organizationId!,
+ },
+ include: {
+ seller: true,
+ fulfillmentCenter: true,
+ supplier: true,
+ logisticsPartner: true,
+ receivedBy: true,
+ recipeItems: {
+ include: {
+ product: true,
+ },
+ },
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
+ })
+
+ return supplies
+ } catch (error) {
+ console.error('Error fetching seller goods supply requests:', error)
+ return []
+ }
+ },
+
+ // Получение конкретной товарной поставки селлера
+ sellerGoodsSupply: async (_: unknown, args: { id: string }, context: Context) => {
+ if (!context.user) {
+ throw new GraphQLError('Требуется авторизация', {
+ extensions: { code: 'UNAUTHENTICATED' },
+ })
+ }
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: context.user.id },
+ include: { organization: true },
+ })
+
+ if (!user?.organization) {
+ throw new GraphQLError('Организация не найдена')
+ }
+
+ const supply = await prisma.sellerGoodsSupplyOrder.findUnique({
+ where: { id: args.id },
+ include: {
+ seller: true,
+ fulfillmentCenter: true,
+ supplier: true,
+ logisticsPartner: true,
+ receivedBy: true,
+ recipeItems: {
+ include: {
+ product: true,
+ },
+ },
+ },
+ })
+
+ if (!supply) {
+ throw new GraphQLError('Поставка не найдена')
+ }
+
+ // Проверка доступа
+ const hasAccess =
+ (user.organization.type === 'SELLER' && supply.sellerId === user.organizationId) ||
+ (user.organization.type === 'FULFILLMENT' && supply.fulfillmentCenterId === user.organizationId) ||
+ (user.organization.type === 'WHOLESALE' && supply.supplierId === user.organizationId) ||
+ (user.organization.type === 'LOGIST' && supply.logisticsPartnerId === user.organizationId)
+
+ if (!hasAccess) {
+ throw new GraphQLError('Нет доступа к этой поставке')
+ }
+
+ return supply
+ } catch (error) {
+ console.error('Error fetching seller goods supply:', error)
+ if (error instanceof GraphQLError) {
+ throw error
+ }
+ throw new GraphQLError('Ошибка получения товарной поставки')
+ }
+ },
+
+ // Инвентарь товаров селлера на складе фулфилмента
+ mySellerGoodsInventory: async (_: unknown, __: unknown, context: Context) => {
+ if (!context.user) {
+ throw new GraphQLError('Требуется авторизация', {
+ extensions: { code: 'UNAUTHENTICATED' },
+ })
+ }
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: context.user.id },
+ include: { organization: true },
+ })
+
+ if (!user?.organization) {
+ return []
+ }
+
+ let inventoryItems
+
+ if (user.organization.type === 'SELLER') {
+ // Селлер видит свои товары на всех складах
+ inventoryItems = await prisma.sellerGoodsInventory.findMany({
where: {
sellerId: user.organizationId!,
},
include: {
seller: true,
- fulfillmentCenter: {
- include: {
- phones: true,
- emails: true,
- },
- },
- items: {
- include: {
- product: {
- include: {
- category: true,
- sizes: true,
- },
- },
- recipe: {
- include: {
- components: {
- include: {
- material: true,
- },
- },
- services: {
- include: {
- service: true,
- },
- },
- },
- },
- },
- },
- requestedServices: {
- include: {
- service: true,
- completedBy: true,
- },
- },
- logisticsPartner: {
- include: {
- phones: true,
- },
- },
- supplier: {
- include: {
- phones: true,
- },
- },
- receivedBy: true,
+ fulfillmentCenter: true,
+ product: true,
},
orderBy: {
- createdAt: 'desc',
+ lastSupplyDate: 'desc',
},
})
-
- return orders
- } catch (error) {
- throw new GraphQLError('Ошибка получения товарных поставок', {
- extensions: { code: 'INTERNAL_ERROR', originalError: error },
- })
- }
- },
-
- // Входящие товарные поставки (для фулфилмента)
- incomingGoodsSuppliesV2: async (_: unknown, __: unknown, context: Context) => {
- const { user } = context
-
- if (!user?.organization || user.organization.type !== 'FULFILLMENT') {
- throw new GraphQLError('Доступно только для фулфилмент-центров', {
- extensions: { code: 'FORBIDDEN' },
- })
- }
-
- try {
- const orders = await prisma.goodsSupplyOrder.findMany({
+ } else if (user.organization.type === 'FULFILLMENT') {
+ // Фулфилмент видит все товары на своем складе
+ inventoryItems = await prisma.sellerGoodsInventory.findMany({
where: {
fulfillmentCenterId: user.organizationId!,
},
include: {
- seller: {
- include: {
- phones: true,
- emails: true,
- },
- },
+ seller: true,
fulfillmentCenter: true,
- items: {
- include: {
- product: {
- include: {
- category: true,
- },
- },
- recipe: {
- include: {
- components: {
- include: {
- material: {
- select: {
- id: true,
- name: true,
- unit: true,
- // НЕ показываем цены селлера
- },
- },
- },
- },
- services: {
- include: {
- service: true,
- },
- },
- },
- },
- },
- },
- requestedServices: {
- include: {
- service: true,
- completedBy: true,
- },
- },
- logisticsPartner: {
- include: {
- phones: true,
- },
- },
- supplier: true,
- receivedBy: true,
+ product: true,
},
orderBy: {
- requestedDeliveryDate: 'asc',
+ lastSupplyDate: 'desc',
},
})
-
- // Фильтруем коммерческие данные селлера
- return orders.map(order => ({
- ...order,
- items: order.items.map(item => ({
- ...item,
- price: null, // Скрываем закупочную цену селлера
- totalPrice: null, // Скрываем общую стоимость
- })),
- }))
- } catch (error) {
- throw new GraphQLError('Ошибка получения входящих поставок', {
- extensions: { code: 'INTERNAL_ERROR', originalError: error },
- })
- }
- },
-
- // Товарные заказы для поставщиков
- myGoodsSupplyRequestsV2: async (_: unknown, __: unknown, context: Context) => {
- const { user } = context
-
- if (!user?.organization || user.organization.type !== 'WHOLESALE') {
- throw new GraphQLError('Доступно только для поставщиков', {
- extensions: { code: 'FORBIDDEN' },
- })
+ } else {
+ return []
}
- try {
- const orders = await prisma.goodsSupplyOrder.findMany({
- where: {
- supplierId: user.organizationId!,
- },
- include: {
- seller: {
- include: {
- phones: true,
- },
- },
- fulfillmentCenter: true,
- items: {
- include: {
- product: {
- include: {
- category: true,
- },
- },
- },
- },
- // НЕ включаем requestedServices - поставщик не видит услуги ФФ
- },
- orderBy: {
- requestedDeliveryDate: 'asc',
- },
- })
+ return inventoryItems
+ } catch (error) {
+ console.error('Error fetching seller goods inventory:', error)
+ return []
+ }
+ },
+}
- // Показываем только релевантную для поставщика информацию
- return orders.map(order => ({
- ...order,
- items: order.items.map(item => ({
- ...item,
- recipe: null, // Поставщик не видит рецептуры
- })),
- }))
- } catch (error) {
- throw new GraphQLError('Ошибка получения заказов поставок', {
- extensions: { code: 'INTERNAL_ERROR', originalError: error },
- })
- }
- },
+// =============================================================================
+// ✏️ MUTATION RESOLVERS V2
+// =============================================================================
- // Детали конкретной поставки
- goodsSupplyOrderV2: async (_: unknown, args: { id: string }, context: Context) => {
- const { user } = context
-
- if (!user?.organizationId) {
- throw new GraphQLError('Необходима авторизация', {
- extensions: { code: 'UNAUTHENTICATED' },
- })
- }
+export const sellerGoodsMutations = {
+ // Создание поставки товаров селлера
+ createSellerGoodsSupply: async (_: unknown, args: { input: any }, context: Context) => {
+ if (!context.user) {
+ throw new GraphQLError('Требуется авторизация', {
+ extensions: { code: 'UNAUTHENTICATED' },
+ })
+ }
- try {
- const order = await prisma.goodsSupplyOrder.findUnique({
- where: { id: args.id },
- include: {
- seller: {
- include: {
- phones: true,
- emails: true,
- },
- },
- fulfillmentCenter: {
- include: {
- phones: true,
- emails: true,
- },
- },
- items: {
- include: {
- product: {
- include: {
- category: true,
- sizes: true,
- },
- },
- recipe: {
- include: {
- components: {
- include: {
- material: true,
- },
- },
- services: {
- include: {
- service: true,
- },
- },
- },
- },
- },
- },
- requestedServices: {
- include: {
- service: true,
- completedBy: true,
- },
- },
- logisticsPartner: {
- include: {
- phones: true,
- emails: true,
- },
- },
- supplier: {
- include: {
- phones: true,
- emails: true,
- },
- },
- receivedBy: true,
- },
- })
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: context.user.id },
+ include: { organization: true },
+ })
- if (!order) {
- throw new GraphQLError('Поставка не найдена', {
- extensions: { code: 'NOT_FOUND' },
- })
- }
-
- // Проверка прав доступа
- const hasAccess =
- order.sellerId === user.organizationId ||
- order.fulfillmentCenterId === user.organizationId ||
- order.supplierId === user.organizationId ||
- order.logisticsPartnerId === user.organizationId
-
- if (!hasAccess) {
- throw new GraphQLError('Доступ запрещен', {
- extensions: { code: 'FORBIDDEN' },
- })
- }
-
- // Фильтрация данных в зависимости от роли
- if (user.organization?.type === 'WHOLESALE') {
- // Поставщик не видит рецептуры и услуги ФФ
- return {
- ...order,
- items: order.items.map(item => ({
- ...item,
- recipe: null,
- })),
- requestedServices: [],
- }
- }
-
- if (user.organization?.type === 'FULFILLMENT') {
- // ФФ не видит закупочные цены селлера
- return {
- ...order,
- items: order.items.map(item => ({
- ...item,
- price: null,
- totalPrice: null,
- })),
- }
- }
-
- if (user.organization?.type === 'LOGIST') {
- // Логистика видит только логистическую информацию
- return {
- ...order,
- items: order.items.map(item => ({
- ...item,
- price: null,
- totalPrice: null,
- recipe: null,
- })),
- requestedServices: [],
- }
- }
-
- // Селлер видит все свои данные
- return order
- } catch (error) {
- if (error instanceof GraphQLError) {
- throw error
- }
- throw new GraphQLError('Ошибка получения поставки', {
- extensions: { code: 'INTERNAL_ERROR', originalError: error },
- })
- }
- },
-
- // Рецептуры товаров селлера
- myProductRecipes: async (_: unknown, __: unknown, context: Context) => {
- const { user } = context
-
if (!user?.organization || user.organization.type !== 'SELLER') {
- throw new GraphQLError('Доступно только для селлеров', {
- extensions: { code: 'FORBIDDEN' },
- })
+ throw new GraphQLError('Доступно только для селлеров')
}
- try {
- const recipes = await prisma.productRecipe.findMany({
- where: {
- product: {
- organizationId: user.organizationId!,
- },
+ const { fulfillmentCenterId, supplierId, logisticsPartnerId, requestedDeliveryDate, notes, recipeItems } = args.input
+
+ // 🔍 ВАЛИДАЦИЯ ПАРТНЕРОВ
+
+ // Проверяем фулфилмент-центр
+ const fulfillmentCenter = await prisma.organization.findUnique({
+ where: { id: fulfillmentCenterId },
+ include: {
+ counterpartiesAsCounterparty: {
+ where: { organizationId: user.organizationId! },
},
- include: {
- product: {
- include: {
- category: true,
- },
- },
- components: {
- include: {
- material: true,
- },
- },
- services: {
- include: {
- service: true,
- },
- },
+ },
+ })
+
+ if (!fulfillmentCenter || fulfillmentCenter.type !== 'FULFILLMENT') {
+ throw new GraphQLError('Фулфилмент-центр не найден или имеет неверный тип')
+ }
+
+ if (fulfillmentCenter.counterpartiesAsCounterparty.length === 0) {
+ throw new GraphQLError('Нет партнерских отношений с данным фулфилмент-центром')
+ }
+
+ // Проверяем поставщика
+ const supplier = await prisma.organization.findUnique({
+ where: { id: supplierId },
+ include: {
+ counterpartiesAsCounterparty: {
+ where: { organizationId: user.organizationId! },
},
- orderBy: {
- updatedAt: 'desc',
+ },
+ })
+
+ if (!supplier || supplier.type !== 'WHOLESALE') {
+ throw new GraphQLError('Поставщик не найден или имеет неверный тип')
+ }
+
+ if (supplier.counterpartiesAsCounterparty.length === 0) {
+ throw new GraphQLError('Нет партнерских отношений с данным поставщиком')
+ }
+
+ // 🔍 ВАЛИДАЦИЯ ТОВАРОВ И ОСТАТКОВ
+ let totalCost = 0
+ const mainProducts = recipeItems.filter((item: any) => item.recipeType === 'MAIN_PRODUCT')
+
+ if (mainProducts.length === 0) {
+ throw new GraphQLError('Должен быть хотя бы один основной товар')
+ }
+
+ // Проверяем все товары в рецептуре
+ for (const item of recipeItems) {
+ const product = await prisma.product.findUnique({
+ where: { id: item.productId },
+ })
+
+ if (!product) {
+ throw new GraphQLError(`Товар с ID ${item.productId} не найден`)
+ }
+
+ if (product.organizationId !== supplierId) {
+ throw new GraphQLError(`Товар ${product.name} не принадлежит выбранному поставщику`)
+ }
+
+ // Для основных товаров проверяем остатки
+ if (item.recipeType === 'MAIN_PRODUCT') {
+ const availableStock = (product.stock || product.quantity || 0) - (product.ordered || 0)
+
+ if (item.quantity > availableStock) {
+ throw new GraphQLError(
+ `Недостаточно остатков товара "${product.name}". ` +
+ `Доступно: ${availableStock} шт., запрашивается: ${item.quantity} шт.`,
+ )
+ }
+
+ totalCost += product.price.toNumber() * item.quantity
+ }
+ }
+
+ // 🚀 СОЗДАНИЕ ПОСТАВКИ В ТРАНЗАКЦИИ
+ const supplyOrder = await prisma.$transaction(async (tx) => {
+ // Создаем заказ поставки
+ const newOrder = await tx.sellerGoodsSupplyOrder.create({
+ data: {
+ sellerId: user.organizationId!,
+ fulfillmentCenterId,
+ supplierId,
+ logisticsPartnerId,
+ requestedDeliveryDate: new Date(requestedDeliveryDate),
+ notes,
+ status: 'PENDING',
+ totalCostWithDelivery: totalCost,
},
})
- return recipes
- } catch (error) {
- throw new GraphQLError('Ошибка получения рецептур', {
- extensions: { code: 'INTERNAL_ERROR', originalError: error },
- })
+ // Создаем записи рецептуры
+ for (const item of recipeItems) {
+ await tx.goodsSupplyRecipeItem.create({
+ data: {
+ supplyOrderId: newOrder.id,
+ productId: item.productId,
+ quantity: item.quantity,
+ recipeType: item.recipeType,
+ },
+ })
+
+ // Резервируем основные товары у поставщика
+ if (item.recipeType === 'MAIN_PRODUCT') {
+ await tx.product.update({
+ where: { id: item.productId },
+ data: {
+ ordered: {
+ increment: item.quantity,
+ },
+ },
+ })
+ }
+ }
+
+ return newOrder
+ })
+
+ // 📨 УВЕДОМЛЕНИЯ
+ await notifyOrganization(
+ supplierId,
+ `Новый заказ товаров от селлера ${user.organization.name}`,
+ 'GOODS_SUPPLY_ORDER_CREATED',
+ { orderId: supplyOrder.id },
+ )
+
+ await notifyOrganization(
+ fulfillmentCenterId,
+ `Селлер ${user.organization.name} оформил поставку товаров на ваш склад`,
+ 'INCOMING_GOODS_SUPPLY_ORDER',
+ { orderId: supplyOrder.id },
+ )
+
+ // Получаем созданную поставку с полными данными
+ const createdSupply = await prisma.sellerGoodsSupplyOrder.findUnique({
+ where: { id: supplyOrder.id },
+ include: {
+ seller: true,
+ fulfillmentCenter: true,
+ supplier: true,
+ logisticsPartner: true,
+ recipeItems: {
+ include: {
+ product: true,
+ },
+ },
+ },
+ })
+
+ return {
+ success: true,
+ message: 'Поставка товаров успешно создана',
+ supplyOrder: createdSupply,
}
- },
+ } catch (error) {
+ console.error('Error creating seller goods supply:', error)
+
+ if (error instanceof GraphQLError) {
+ throw error
+ }
+
+ throw new GraphQLError('Ошибка создания товарной поставки')
+ }
},
- Mutation: {
- // Создание товарной поставки
- createGoodsSupplyOrder: async (_: unknown, args: any, context: Context) => {
- const { user } = context
- const { input } = args
-
- if (!user?.organization || user.organization.type !== 'SELLER') {
- throw new GraphQLError('Доступно только для селлеров', {
- extensions: { code: 'FORBIDDEN' },
- })
+ // Обновление статуса товарной поставки
+ updateSellerGoodsSupplyStatus: async (
+ _: unknown,
+ args: { id: string; status: string; notes?: string },
+ context: Context,
+ ) => {
+ if (!context.user) {
+ throw new GraphQLError('Требуется авторизация')
+ }
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: context.user.id },
+ include: { organization: true },
+ })
+
+ if (!user?.organization) {
+ throw new GraphQLError('Организация не найдена')
}
- try {
- // Проверяем фулфилмент-центр
- const fulfillmentCenter = await prisma.organization.findFirst({
- where: {
- id: input.fulfillmentCenterId,
- type: 'FULFILLMENT',
+ const supply = await prisma.sellerGoodsSupplyOrder.findUnique({
+ where: { id: args.id },
+ include: {
+ seller: true,
+ supplier: true,
+ fulfillmentCenter: true,
+ recipeItems: {
+ include: {
+ product: true,
+ },
},
- })
+ },
+ })
- if (!fulfillmentCenter) {
- throw new GraphQLError('Фулфилмент-центр не найден', {
- extensions: { code: 'NOT_FOUND' },
- })
+ if (!supply) {
+ throw new GraphQLError('Поставка не найдена')
+ }
+
+ // 🔐 ПРОВЕРКА ПРАВ И ЛОГИКИ ПЕРЕХОДОВ СТАТУСОВ
+ const { status } = args
+ const currentStatus = supply.status
+ const orgType = user.organization.type
+
+ // Только поставщики могут переводить PENDING → APPROVED
+ if (status === 'APPROVED' && currentStatus === 'PENDING') {
+ if (orgType !== 'WHOLESALE' || supply.supplierId !== user.organizationId) {
+ throw new GraphQLError('Только поставщик может одобрить заказ')
}
+ }
- // Проверяем товары и рецептуры
- for (const item of input.items) {
- const product = await prisma.product.findFirst({
- where: {
- id: item.productId,
- organizationId: user.organizationId!,
- },
- })
-
- if (!product) {
- throw new GraphQLError(`Товар ${item.productId} не найден`, {
- extensions: { code: 'NOT_FOUND' },
- })
- }
-
- if (item.recipeId) {
- const recipe = await prisma.productRecipe.findFirst({
- where: {
- id: item.recipeId,
- productId: item.productId,
- },
- })
-
- if (!recipe) {
- throw new GraphQLError(`Рецептура ${item.recipeId} не найдена`, {
- extensions: { code: 'NOT_FOUND' },
- })
- }
- }
+ // Только поставщики могут переводить APPROVED → SHIPPED
+ else if (status === 'SHIPPED' && currentStatus === 'APPROVED') {
+ if (orgType !== 'WHOLESALE' || supply.supplierId !== user.organizationId) {
+ throw new GraphQLError('Только поставщик может отметить отгрузку')
}
+ }
- // Создаем поставку в транзакции
- const order = await prisma.$transaction(async (tx) => {
- // Создаем основную запись
- const newOrder = await tx.goodsSupplyOrder.create({
- data: {
- sellerId: user.organizationId!,
- fulfillmentCenterId: input.fulfillmentCenterId,
- requestedDeliveryDate: new Date(input.requestedDeliveryDate),
- notes: input.notes,
- status: 'PENDING',
+ // Только фулфилмент может переводить SHIPPED → DELIVERED
+ else if (status === 'DELIVERED' && currentStatus === 'SHIPPED') {
+ if (orgType !== 'FULFILLMENT' || supply.fulfillmentCenterId !== user.organizationId) {
+ throw new GraphQLError('Только фулфилмент-центр может подтвердить получение')
+ }
+ }
+
+ // Только фулфилмент может переводить DELIVERED → COMPLETED
+ else if (status === 'COMPLETED' && currentStatus === 'DELIVERED') {
+ if (orgType !== 'FULFILLMENT' || supply.fulfillmentCenterId !== user.organizationId) {
+ throw new GraphQLError('Только фулфилмент-центр может завершить поставку')
+ }
+ } else {
+ throw new GraphQLError('Недопустимый переход статуса')
+ }
+
+ // 📅 ОБНОВЛЕНИЕ ВРЕМЕННЫХ МЕТОК
+ const updateData: any = {
+ status,
+ updatedAt: new Date(),
+ }
+
+ if (status === 'APPROVED' && orgType === 'WHOLESALE') {
+ updateData.supplierApprovedAt = new Date()
+ updateData.supplierNotes = args.notes
+ }
+
+ if (status === 'SHIPPED' && orgType === 'WHOLESALE') {
+ updateData.shippedAt = new Date()
+ }
+
+ if (status === 'DELIVERED' && orgType === 'FULFILLMENT') {
+ updateData.deliveredAt = new Date()
+ updateData.receivedById = user.id
+ updateData.receiptNotes = args.notes
+ }
+
+ // 🔄 ОБНОВЛЕНИЕ В БАЗЕ
+ const updatedSupply = await prisma.sellerGoodsSupplyOrder.update({
+ where: { id: args.id },
+ data: updateData,
+ include: {
+ seller: true,
+ fulfillmentCenter: true,
+ supplier: true,
+ logisticsPartner: true,
+ receivedBy: true,
+ recipeItems: {
+ include: {
+ product: true,
},
- })
+ },
+ },
+ })
- // Создаем товары
- let totalAmount = 0
- let totalItems = 0
+ // 📨 УВЕДОМЛЕНИЯ О СМЕНЕ СТАТУСА
+ if (status === 'APPROVED') {
+ await notifyOrganization(
+ supply.sellerId,
+ `Поставка товаров одобрена поставщиком ${user.organization.name}`,
+ 'GOODS_SUPPLY_APPROVED',
+ { orderId: args.id },
+ )
+ }
- for (const itemInput of input.items) {
- const itemTotal = itemInput.price * itemInput.quantity
- totalAmount += itemTotal
- totalItems += itemInput.quantity
+ if (status === 'SHIPPED') {
+ await notifyOrganization(
+ supply.sellerId,
+ `Поставка товаров отгружена поставщиком ${user.organization.name}`,
+ 'GOODS_SUPPLY_SHIPPED',
+ { orderId: args.id },
+ )
- await tx.goodsSupplyOrderItem.create({
- data: {
- orderId: newOrder.id,
- productId: itemInput.productId,
- quantity: itemInput.quantity,
- price: itemInput.price,
- totalPrice: itemTotal,
- recipeId: itemInput.recipeId,
- },
- })
- }
+ await notifyOrganization(
+ supply.fulfillmentCenterId,
+ 'Поставка товаров в пути. Ожидается доставка',
+ 'GOODS_SUPPLY_IN_TRANSIT',
+ { orderId: args.id },
+ )
+ }
- // Создаем запросы услуг
- for (const serviceInput of input.requestedServices) {
- const service = await tx.service.findUnique({
- where: { id: serviceInput.serviceId },
- })
+ if (status === 'DELIVERED') {
+ // 📦 АВТОМАТИЧЕСКОЕ СОЗДАНИЕ/ОБНОВЛЕНИЕ ИНВЕНТАРЯ V2
+ await processSellerGoodsSupplyReceipt(args.id)
+
+ await notifyOrganization(
+ supply.sellerId,
+ `Поставка товаров доставлена в ${supply.fulfillmentCenter.name}`,
+ 'GOODS_SUPPLY_DELIVERED',
+ { orderId: args.id },
+ )
+ }
- if (!service) {
- throw new Error(`Услуга ${serviceInput.serviceId} не найдена`)
- }
+ if (status === 'COMPLETED') {
+ await notifyOrganization(
+ supply.sellerId,
+ `Поставка товаров завершена. Товары размещены на складе ${supply.fulfillmentCenter.name}`,
+ 'GOODS_SUPPLY_COMPLETED',
+ { orderId: args.id },
+ )
+ }
- const serviceTotal = service.price * serviceInput.quantity
- totalAmount += serviceTotal
+ return updatedSupply
+ } catch (error) {
+ console.error('Error updating seller goods supply status:', error)
- await tx.fulfillmentServiceRequest.create({
- data: {
- orderId: newOrder.id,
- serviceId: serviceInput.serviceId,
- quantity: serviceInput.quantity,
- price: service.price,
- totalPrice: serviceTotal,
- status: 'PENDING',
- },
- })
- }
+ if (error instanceof GraphQLError) {
+ throw error
+ }
- // Обновляем итоги
- await tx.goodsSupplyOrder.update({
- where: { id: newOrder.id },
- data: {
- totalAmount,
- totalItems,
+ throw new GraphQLError('Ошибка обновления статуса товарной поставки')
+ }
+ },
+
+ // Отмена товарной поставки селлером
+ cancelSellerGoodsSupply: async (_: unknown, args: { id: string }, context: Context) => {
+ if (!context.user) {
+ throw new GraphQLError('Требуется авторизация')
+ }
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: context.user.id },
+ include: { organization: true },
+ })
+
+ if (!user?.organization || user.organization.type !== 'SELLER') {
+ throw new GraphQLError('Только селлеры могут отменять свои поставки')
+ }
+
+ const supply = await prisma.sellerGoodsSupplyOrder.findUnique({
+ where: { id: args.id },
+ include: {
+ seller: true,
+ recipeItems: {
+ include: {
+ product: true,
},
- })
+ },
+ },
+ })
- return newOrder
- })
+ if (!supply) {
+ throw new GraphQLError('Поставка не найдена')
+ }
- // Получаем созданную поставку с полными данными
- const createdOrder = await prisma.goodsSupplyOrder.findUnique({
- where: { id: order.id },
+ if (supply.sellerId !== user.organizationId) {
+ throw new GraphQLError('Вы можете отменить только свои поставки')
+ }
+
+ // ✅ ПРОВЕРКА ВОЗМОЖНОСТИ ОТМЕНЫ (только PENDING и APPROVED)
+ if (!['PENDING', 'APPROVED'].includes(supply.status)) {
+ throw new GraphQLError('Поставку можно отменить только в статусе PENDING или APPROVED')
+ }
+
+ // 🔄 ОТМЕНА В ТРАНЗАКЦИИ
+ const cancelledSupply = await prisma.$transaction(async (tx) => {
+ // Обновляем статус
+ const updated = await tx.sellerGoodsSupplyOrder.update({
+ where: { id: args.id },
+ data: {
+ status: 'CANCELLED',
+ updatedAt: new Date(),
+ },
include: {
seller: true,
fulfillmentCenter: true,
- items: {
+ supplier: true,
+ recipeItems: {
include: {
product: true,
- recipe: true,
- },
- },
- requestedServices: {
- include: {
- service: true,
},
},
},
})
- return {
- success: true,
- message: 'Товарная поставка успешно создана',
- order: createdOrder,
- }
- } catch (error) {
- if (error instanceof GraphQLError) {
- throw error
- }
- throw new GraphQLError('Ошибка создания поставки', {
- extensions: { code: 'INTERNAL_ERROR', originalError: error },
- })
- }
- },
-
- // Обновление статуса товарной поставки
- updateGoodsSupplyOrderStatus: async (_: unknown, args: any, context: Context) => {
- const { user } = context
- const { id, status, notes } = args
-
- if (!user?.organizationId) {
- throw new GraphQLError('Необходима авторизация', {
- extensions: { code: 'UNAUTHENTICATED' },
- })
- }
-
- try {
- const order = await prisma.goodsSupplyOrder.findUnique({
- where: { id },
- })
-
- if (!order) {
- throw new GraphQLError('Поставка не найдена', {
- extensions: { code: 'NOT_FOUND' },
- })
- }
-
- // Проверка прав на изменение статуса
- const canUpdate =
- (status === 'SUPPLIER_APPROVED' && order.supplierId === user.organizationId) ||
- (status === 'LOGISTICS_CONFIRMED' && user.organization?.type === 'FULFILLMENT') ||
- (status === 'SHIPPED' && order.supplierId === user.organizationId) ||
- (status === 'IN_TRANSIT' && order.logisticsPartnerId === user.organizationId) ||
- (status === 'RECEIVED' && order.fulfillmentCenterId === user.organizationId) ||
- (status === 'CANCELLED' &&
- (order.sellerId === user.organizationId || order.fulfillmentCenterId === user.organizationId))
-
- if (!canUpdate) {
- throw new GraphQLError('Недостаточно прав для изменения статуса', {
- extensions: { code: 'FORBIDDEN' },
- })
- }
-
- const updateData: any = {
- status,
- notes: notes || order.notes,
- }
-
- // Устанавливаем временные метки
- if (status === 'SUPPLIER_APPROVED') {
- updateData.supplierApprovedAt = new Date()
- } else if (status === 'SHIPPED') {
- updateData.shippedAt = new Date()
- } else if (status === 'RECEIVED') {
- updateData.receivedAt = new Date()
- updateData.receivedById = user.id
- }
-
- const updatedOrder = await prisma.goodsSupplyOrder.update({
- where: { id },
- data: updateData,
- include: {
- receivedBy: true,
- },
- })
-
- return updatedOrder
- } catch (error) {
- if (error instanceof GraphQLError) {
- throw error
- }
- throw new GraphQLError('Ошибка обновления статуса', {
- extensions: { code: 'INTERNAL_ERROR', originalError: error },
- })
- }
- },
-
- // Приемка товарной поставки
- receiveGoodsSupplyOrder: async (_: unknown, args: any, context: Context) => {
- const { user } = context
- const { id, items } = args
-
- if (!user?.organization || user.organization.type !== 'FULFILLMENT') {
- throw new GraphQLError('Доступно только для фулфилмент-центров', {
- extensions: { code: 'FORBIDDEN' },
- })
- }
-
- try {
- const order = await prisma.goodsSupplyOrder.findUnique({
- where: { id },
- include: {
- items: {
- include: {
- recipe: {
- include: {
- components: {
- include: {
- material: true,
- },
- },
- },
- },
- },
- },
- },
- })
-
- if (!order) {
- throw new GraphQLError('Поставка не найдена', {
- extensions: { code: 'NOT_FOUND' },
- })
- }
-
- if (order.fulfillmentCenterId !== user.organizationId) {
- throw new GraphQLError('Доступ запрещен', {
- extensions: { code: 'FORBIDDEN' },
- })
- }
-
- if (order.status !== 'IN_TRANSIT') {
- throw new GraphQLError('Поставка должна быть в статусе "В пути"', {
- extensions: { code: 'BAD_REQUEST' },
- })
- }
-
- // Обрабатываем приемку в транзакции
- const updatedOrder = await prisma.$transaction(async (tx) => {
- // Обновляем данные приемки для каждого товара
- for (const itemInput of items) {
- const orderItem = order.items.find(item => item.id === itemInput.itemId)
- if (!orderItem) {
- throw new Error(`Товар ${itemInput.itemId} не найден в поставке`)
- }
-
- await tx.goodsSupplyOrderItem.update({
- where: { id: itemInput.itemId },
+ // Освобождаем зарезервированные товары у поставщика (только MAIN_PRODUCT)
+ for (const item of supply.recipeItems) {
+ if (item.recipeType === 'MAIN_PRODUCT') {
+ await tx.product.update({
+ where: { id: item.productId },
data: {
- receivedQuantity: itemInput.receivedQuantity,
- damagedQuantity: itemInput.damagedQuantity || 0,
- acceptanceNotes: itemInput.acceptanceNotes,
+ ordered: {
+ decrement: item.quantity,
+ },
},
})
-
- // Обновляем остатки расходников по рецептуре
- if (orderItem.recipe && itemInput.receivedQuantity > 0) {
- for (const component of orderItem.recipe.components) {
- const usedQuantity = component.quantity * itemInput.receivedQuantity
-
- await tx.supply.update({
- where: { id: component.materialId },
- data: {
- currentStock: {
- decrement: usedQuantity,
- },
- },
- })
- }
- }
}
-
- // Обновляем статус поставки
- const updated = await tx.goodsSupplyOrder.update({
- where: { id },
- data: {
- status: 'RECEIVED',
- receivedAt: new Date(),
- receivedById: user.id,
- },
- include: {
- items: {
- include: {
- product: true,
- recipe: {
- include: {
- components: {
- include: {
- material: true,
- },
- },
- },
- },
- },
- },
- requestedServices: {
- include: {
- service: true,
- },
- },
- receivedBy: true,
- },
- })
-
- return updated
- })
-
- return updatedOrder
- } catch (error) {
- if (error instanceof GraphQLError) {
- throw error
}
- throw new GraphQLError('Ошибка приемки поставки', {
- extensions: { code: 'INTERNAL_ERROR', originalError: error },
- })
- }
- },
- // Отмена товарной поставки
- cancelGoodsSupplyOrder: async (_: unknown, args: any, context: Context) => {
- const { user } = context
- const { id, reason } = args
-
- if (!user?.organizationId) {
- throw new GraphQLError('Необходима авторизация', {
- extensions: { code: 'UNAUTHENTICATED' },
- })
+ return updated
+ })
+
+ // 📨 УВЕДОМЛЕНИЯ ОБ ОТМЕНЕ
+ if (supply.supplierId) {
+ await notifyOrganization(
+ supply.supplierId,
+ `Селлер ${user.organization.name} отменил заказ товаров`,
+ 'GOODS_SUPPLY_CANCELLED',
+ { orderId: args.id },
+ )
}
- try {
- const order = await prisma.goodsSupplyOrder.findUnique({
- where: { id },
- })
+ await notifyOrganization(
+ supply.fulfillmentCenterId,
+ `Селлер ${user.organization.name} отменил поставку товаров`,
+ 'GOODS_SUPPLY_CANCELLED',
+ { orderId: args.id },
+ )
- if (!order) {
- throw new GraphQLError('Поставка не найдена', {
- extensions: { code: 'NOT_FOUND' },
- })
- }
+ return cancelledSupply
+ } catch (error) {
+ console.error('Error cancelling seller goods supply:', error)
- // Проверка прав на отмену
- const canCancel =
- order.sellerId === user.organizationId ||
- order.fulfillmentCenterId === user.organizationId ||
- (order.supplierId === user.organizationId && order.status === 'PENDING')
-
- if (!canCancel) {
- throw new GraphQLError('Недостаточно прав для отмены поставки', {
- extensions: { code: 'FORBIDDEN' },
- })
- }
-
- if (['RECEIVED', 'PROCESSING', 'COMPLETED'].includes(order.status)) {
- throw new GraphQLError('Нельзя отменить поставку в текущем статусе', {
- extensions: { code: 'BAD_REQUEST' },
- })
- }
-
- const cancelledOrder = await prisma.goodsSupplyOrder.update({
- where: { id },
- data: {
- status: 'CANCELLED',
- notes: `${order.notes ? order.notes + '\n' : ''}ОТМЕНЕНО: ${reason}`,
- },
- })
-
- return cancelledOrder
- } catch (error) {
- if (error instanceof GraphQLError) {
- throw error
- }
- throw new GraphQLError('Ошибка отмены поставки', {
- extensions: { code: 'INTERNAL_ERROR', originalError: error },
- })
+ if (error instanceof GraphQLError) {
+ throw error
}
- },
+
+ throw new GraphQLError('Ошибка отмены товарной поставки')
+ }
},
}
\ No newline at end of file
diff --git a/src/graphql/resolvers/seller-consumables.ts b/src/graphql/resolvers/seller-consumables.ts
index a388c3b..c74ed64 100644
--- a/src/graphql/resolvers/seller-consumables.ts
+++ b/src/graphql/resolvers/seller-consumables.ts
@@ -4,11 +4,11 @@
import { GraphQLError } from 'graphql'
+import { processSellerConsumableSupplyReceipt } from '@/lib/inventory-management'
import { prisma } from '@/lib/prisma'
import { notifyOrganization } from '@/lib/realtime'
import { Context } from '../context'
-import { processSellerConsumableSupplyReceipt } from '@/lib/inventory-management'
// =============================================================================
// 🔍 QUERY RESOLVERS
diff --git a/src/graphql/typedefs.ts b/src/graphql/typedefs.ts
index 80cb7f3..fc30973 100644
--- a/src/graphql/typedefs.ts
+++ b/src/graphql/typedefs.ts
@@ -1932,6 +1932,136 @@ export const typeDefs = gql`
supplyOrder: SellerConsumableSupplyOrder
}
+ # ===============================================
+ # 🛒 SELLER GOODS SUPPLY TYPES V2.0 - ТОВАРНЫЕ ПОСТАВКИ
+ # ===============================================
+
+ # Главный тип для товарных поставок селлера
+ type SellerGoodsSupplyOrder {
+ id: ID!
+ status: SellerSupplyOrderStatus!
+
+ # Данные селлера (создатель)
+ sellerId: ID!
+ seller: Organization!
+ fulfillmentCenterId: ID!
+ fulfillmentCenter: Organization!
+ requestedDeliveryDate: DateTime!
+ notes: String
+
+ # Данные поставщика
+ supplierId: ID
+ supplier: Organization
+ supplierApprovedAt: DateTime
+ packagesCount: Int
+ estimatedVolume: Float
+ supplierContractId: String
+ supplierNotes: String
+
+ # Данные логистики
+ logisticsPartnerId: ID
+ logisticsPartner: Organization
+ estimatedDeliveryDate: DateTime
+ routeId: ID
+ logisticsCost: Float
+ logisticsNotes: String
+
+ # Данные отгрузки
+ shippedAt: DateTime
+ trackingNumber: String
+
+ # Данные получения
+ deliveredAt: DateTime
+ receivedById: ID
+ receivedBy: User
+ receiptNotes: String
+
+ # Финансы
+ totalCostWithDelivery: Float!
+ actualDeliveryCost: Float!
+
+ # Нормализованная рецептура
+ recipeItems: [GoodsSupplyRecipeItem!]!
+
+ createdAt: DateTime!
+ updatedAt: DateTime!
+ }
+
+ # Нормализованная рецептура для товарных поставок
+ type GoodsSupplyRecipeItem {
+ id: ID!
+ productId: ID!
+ product: Product!
+ quantity: Int!
+ recipeType: RecipeType!
+ createdAt: DateTime!
+ }
+
+ # Типы компонентов в рецептуре
+ enum RecipeType {
+ MAIN_PRODUCT # Основной товар
+ COMPONENT # Компонент товара
+ PACKAGING # Упаковка
+ ACCESSORY # Аксессуар
+ }
+
+ # Инвентарь товаров селлера на складе
+ type SellerGoodsInventory {
+ id: ID!
+ sellerId: ID!
+ seller: Organization!
+ fulfillmentCenterId: ID!
+ fulfillmentCenter: Organization!
+ productId: ID!
+ product: Product!
+
+ # Складские данные
+ currentStock: Int!
+ reservedStock: Int!
+ inPreparationStock: Int!
+ totalReceived: Int!
+ totalShipped: Int!
+
+ # Пороги
+ minStock: Int!
+ maxStock: Int
+
+ # Цены
+ averageCost: Float!
+ salePrice: Float!
+
+ # Метаданные
+ lastSupplyDate: DateTime
+ lastShipDate: DateTime
+ notes: String
+ createdAt: DateTime!
+ updatedAt: DateTime!
+ }
+
+ # Input типы для создания товарных поставок
+ input CreateSellerGoodsSupplyInput {
+ fulfillmentCenterId: ID! # куда доставлять (FULFILLMENT партнер)
+ supplierId: ID! # от кого заказывать (WHOLESALE партнер)
+ logisticsPartnerId: ID # кто везет (LOGIST партнер, опционально)
+ requestedDeliveryDate: DateTime! # когда нужно
+ notes: String # заметки селлера
+ recipeItems: [GoodsSupplyRecipeItemInput!]! # нормализованная рецептура
+ }
+
+ # Input для компонентов рецептуры товарных поставок
+ input GoodsSupplyRecipeItemInput {
+ productId: ID! # какой товар
+ quantity: Int! # количество
+ recipeType: RecipeType! # тип компонента
+ }
+
+ # Результат создания товарной поставки
+ type CreateSellerGoodsSupplyResult {
+ success: Boolean!
+ message: String!
+ supplyOrder: SellerGoodsSupplyOrder
+ }
+
# Расширяем Query для селлерских поставок
extend type Query {
# Поставки селлера (мои заказы)
@@ -1945,6 +2075,22 @@ export const typeDefs = gql`
# Конкретная поставка селлера
sellerConsumableSupply(id: ID!): SellerConsumableSupplyOrder
+
+ # === V2 ТОВАРНЫЕ ПОСТАВКИ СЕЛЛЕРА ===
+ # Поставки товаров селлера (мои заказы)
+ mySellerGoodsSupplies: [SellerGoodsSupplyOrder!]!
+
+ # Входящие товарные заказы от селлеров (для фулфилмента)
+ incomingSellerGoodsSupplies: [SellerGoodsSupplyOrder!]!
+
+ # Товарные поставки селлеров для поставщиков
+ mySellerGoodsSupplyRequests: [SellerGoodsSupplyOrder!]!
+
+ # Конкретная товарная поставка селлера
+ sellerGoodsSupply(id: ID!): SellerGoodsSupplyOrder
+
+ # Инвентарь товаров селлера (для фулфилмента и селлера)
+ mySellerGoodsInventory: [SellerGoodsInventory!]!
}
# Расширяем Mutation для селлерских поставок
@@ -1963,6 +2109,16 @@ export const typeDefs = gql`
# Отмена поставки селлером (только PENDING/APPROVED)
cancelSellerSupply(id: ID!): SellerConsumableSupplyOrder!
+
+ # === V2 ТОВАРНЫЕ ПОСТАВКИ МУТАЦИИ ===
+ # Создание поставки товаров селлера
+ createSellerGoodsSupply(input: CreateSellerGoodsSupplyInput!): CreateSellerGoodsSupplyResult!
+
+ # Обновление статуса товарной поставки
+ updateSellerGoodsSupplyStatus(id: ID!, status: SellerSupplyOrderStatus!, notes: String): SellerGoodsSupplyOrder!
+
+ # Отмена товарной поставки селлером
+ cancelSellerGoodsSupply(id: ID!): SellerGoodsSupplyOrder!
}
# === V2 ЛОГИСТИКА РАСХОДНИКОВ ФУЛФИЛМЕНТА ===
diff --git a/src/lib/inventory-management-goods.ts b/src/lib/inventory-management-goods.ts
new file mode 100644
index 0000000..7fe855f
--- /dev/null
+++ b/src/lib/inventory-management-goods.ts
@@ -0,0 +1,250 @@
+// =============================================================================
+// 📦 INVENTORY MANAGEMENT ДЛЯ ТОВАРОВ СЕЛЛЕРА V2
+// =============================================================================
+
+import { Prisma } from '@prisma/client'
+
+import { prisma } from '@/lib/prisma'
+
+// =============================================================================
+// 🔄 ПРИЕМКА ТОВАРОВ НА СКЛАД ФУЛФИЛМЕНТА
+// =============================================================================
+
+export async function processSellerGoodsSupplyReceipt(
+ supplyOrderId: string,
+): Promise {
+ console.log(`📦 Processing seller goods supply receipt: ${supplyOrderId}`)
+
+ const supplyOrder = await prisma.sellerGoodsSupplyOrder.findUnique({
+ where: { id: supplyOrderId },
+ include: {
+ seller: true,
+ fulfillmentCenter: true,
+ recipeItems: {
+ include: {
+ product: true,
+ },
+ },
+ },
+ })
+
+ if (!supplyOrder) {
+ throw new Error(`Seller goods supply order not found: ${supplyOrderId}`)
+ }
+
+ // Обрабатываем только товары типа MAIN_PRODUCT
+ const mainProducts = supplyOrder.recipeItems.filter(
+ item => item.recipeType === 'MAIN_PRODUCT',
+ )
+
+ for (const item of mainProducts) {
+ // Находим или создаем запись инвентаря
+ const existingInventory = await prisma.sellerGoodsInventory.findUnique({
+ where: {
+ sellerId_fulfillmentCenterId_productId: {
+ sellerId: supplyOrder.sellerId,
+ fulfillmentCenterId: supplyOrder.fulfillmentCenterId,
+ productId: item.productId,
+ },
+ },
+ })
+
+ if (existingInventory) {
+ // Обновляем существующий инвентарь
+ const newAverageCost = calculateWeightedAverageCost(
+ existingInventory.currentStock,
+ existingInventory.averageCost.toNumber(),
+ item.quantity,
+ item.product.price.toNumber(),
+ )
+
+ await prisma.sellerGoodsInventory.update({
+ where: { id: existingInventory.id },
+ data: {
+ currentStock: {
+ increment: item.quantity,
+ },
+ totalReceived: {
+ increment: item.quantity,
+ },
+ averageCost: newAverageCost,
+ lastSupplyDate: new Date(),
+ },
+ })
+
+ console.log(
+ `✅ Updated inventory for ${item.product.name}: +${item.quantity} units`,
+ )
+ } else {
+ // Создаем новую запись инвентаря
+ await prisma.sellerGoodsInventory.create({
+ data: {
+ sellerId: supplyOrder.sellerId,
+ fulfillmentCenterId: supplyOrder.fulfillmentCenterId,
+ productId: item.productId,
+ currentStock: item.quantity,
+ totalReceived: item.quantity,
+ averageCost: item.product.price,
+ salePrice: item.product.price, // Можно будет настроить отдельно
+ lastSupplyDate: new Date(),
+ },
+ })
+
+ console.log(
+ `✅ Created new inventory for ${item.product.name}: ${item.quantity} units`,
+ )
+ }
+
+ // Снимаем резерв у поставщика
+ await prisma.product.update({
+ where: { id: item.productId },
+ data: {
+ ordered: {
+ decrement: item.quantity,
+ },
+ },
+ })
+ }
+
+ console.log('✅ Supply receipt processed successfully')
+}
+
+// =============================================================================
+// 🧮 РАСЧЕТ СРЕДНЕВЗВЕШЕННОЙ СТОИМОСТИ
+// =============================================================================
+
+function calculateWeightedAverageCost(
+ currentStock: number,
+ currentCost: number,
+ newQuantity: number,
+ newCost: number,
+): Prisma.Decimal {
+ const totalValue = currentStock * currentCost + newQuantity * newCost
+ const totalQuantity = currentStock + newQuantity
+ const averageCost = totalQuantity > 0 ? totalValue / totalQuantity : 0
+
+ return new Prisma.Decimal(averageCost.toFixed(2))
+}
+
+// =============================================================================
+// 📤 ИСПОЛЬЗОВАНИЕ ТОВАРОВ (заготовка для будущего)
+// =============================================================================
+
+export async function useSellerGoodsFromInventory(
+ sellerId: string,
+ fulfillmentCenterId: string,
+ productId: string,
+ quantity: number,
+ purpose: 'MARKETPLACE_SHIPMENT' | 'INTERNAL_USE' | 'DAMAGE',
+): Promise {
+ console.log(
+ `📤 Using ${quantity} units of product ${productId} for ${purpose}`,
+ )
+
+ const inventory = await prisma.sellerGoodsInventory.findUnique({
+ where: {
+ sellerId_fulfillmentCenterId_productId: {
+ sellerId,
+ fulfillmentCenterId,
+ productId,
+ },
+ },
+ })
+
+ if (!inventory) {
+ throw new Error('Inventory record not found')
+ }
+
+ if (inventory.currentStock < quantity) {
+ throw new Error(
+ `Insufficient stock. Available: ${inventory.currentStock}, requested: ${quantity}`,
+ )
+ }
+
+ // Обновляем инвентарь
+ await prisma.sellerGoodsInventory.update({
+ where: { id: inventory.id },
+ data: {
+ currentStock: {
+ decrement: quantity,
+ },
+ totalShipped: {
+ increment: purpose === 'MARKETPLACE_SHIPMENT' ? quantity : 0,
+ },
+ lastShipDate: purpose === 'MARKETPLACE_SHIPMENT' ? new Date() : undefined,
+ },
+ })
+
+ // TODO: Создать запись в таблице движений товаров (InventoryMovement)
+ console.log(`✅ Inventory updated: -${quantity} units`)
+}
+
+// =============================================================================
+// 🔄 РЕЗЕРВИРОВАНИЕ ТОВАРОВ (заготовка для будущего)
+// =============================================================================
+
+export async function reserveSellerGoods(
+ sellerId: string,
+ fulfillmentCenterId: string,
+ productId: string,
+ quantity: number,
+): Promise {
+ const inventory = await prisma.sellerGoodsInventory.findUnique({
+ where: {
+ sellerId_fulfillmentCenterId_productId: {
+ sellerId,
+ fulfillmentCenterId,
+ productId,
+ },
+ },
+ })
+
+ if (!inventory) {
+ throw new Error('Inventory record not found')
+ }
+
+ const availableStock = inventory.currentStock - inventory.reservedStock
+ if (availableStock < quantity) {
+ throw new Error(
+ `Insufficient available stock. Available: ${availableStock}, requested: ${quantity}`,
+ )
+ }
+
+ await prisma.sellerGoodsInventory.update({
+ where: { id: inventory.id },
+ data: {
+ reservedStock: {
+ increment: quantity,
+ },
+ },
+ })
+}
+
+// =============================================================================
+// 🏭 ПЕРЕВОД В ПОДГОТОВКУ (заготовка для будущего)
+// =============================================================================
+
+export async function moveToPreparation(
+ sellerId: string,
+ fulfillmentCenterId: string,
+ productId: string,
+ quantity: number,
+): Promise {
+ await prisma.sellerGoodsInventory.update({
+ where: {
+ sellerId_fulfillmentCenterId_productId: {
+ sellerId,
+ fulfillmentCenterId,
+ productId,
+ },
+ },
+ data: {
+ reservedStock: {
+ decrement: quantity,
+ },
+ inPreparationStock: {
+ increment: quantity,
+ },
+ },
+ })
+}
\ No newline at end of file