@ -12,6 +12,13 @@ import { WildberriesService } from '@/services/wildberries-service'
import '@/lib/seed-init' // Автоматическая инициализация БД
// 🔒 СИСТЕМА БЕЗОПАСНОСТИ - импорты
import { CommercialDataAudit } from './security/commercial-data-audit'
import { createSecurityContext } from './security/index'
import { ParticipantIsolation } from './security/participant-isolation'
import { SupplyDataFilter } from './security/supply-data-filter'
import type { SecurityContext } from './security/types'
// Сервисы
const smsService = new SmsService ( )
const dadataService = new DaDataService ( )
@ -22,25 +29,25 @@ const generateReferralCode = async (): Promise<string> => {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
let attempts = 0
const maxAttempts = 10
while ( attempts < maxAttempts ) {
let code = ''
for ( let i = 0 ; i < 10 ; i ++ ) {
code += chars . charAt ( Math . floor ( Math . random ( ) * chars . length ) )
}
// Проверяем уникальность
const existing = await prisma . organization . findUnique ( {
where : { referralCode : code } ,
} )
if ( ! existing ) {
return code
}
attempts ++
}
// Если не удалось сгенерировать уникальный код, используем cuid как fallback
return ` REF ${ Date . now ( ) } ${ Math . random ( ) . toString ( 36 ) . substr ( 2 , 5 ) . toUpperCase ( ) } `
}
@ -48,7 +55,7 @@ const generateReferralCode = async (): Promise<string> => {
// Функция для автоматического создания записи склада при новом партнерстве
const autoCreateWarehouseEntry = async ( sellerId : string , fulfillmentId : string ) = > {
console . warn ( ` 🏗️ AUTO WAREHOUSE ENTRY: Creating for seller ${ sellerId } with fulfillment ${ fulfillmentId } ` )
// Получаем данные селлера
const sellerOrg = await prisma . organization . findUnique ( {
where : { id : sellerId } ,
@ -58,13 +65,13 @@ const autoCreateWarehouseEntry = async (sellerId: string, fulfillmentId: string)
throw new Error ( ` Селлер с ID ${ sellerId } не найден ` )
}
// Проверяем что не существует уже записи для этого селлера у этого фулфилмента
// Проверяем что не существует уже записи для этого селлера у этого фулфилмента
// В будущем здесь может быть проверка в отдельной таблице warehouse_entries
// Пока используем логику проверки через контрагентов
// ЛОГИКА ОПРЕДЕЛЕНИЯ НАЗВАНИЯ МАГАЗИНА (консистентно с warehouseData resolver)
let storeName = sellerOrg . name
if ( sellerOrg . fullName && sellerOrg . name ? . includes ( 'ИП' ) ) {
// Извлекаем название из скобок, например: "ИП Антипова Д. В . (Renrel)" -> "Renrel"
const match = sellerOrg . fullName . match ( /\(([^)]+)\)/ )
@ -77,7 +84,7 @@ const autoCreateWarehouseEntry = async (sellerId: string, fulfillmentId: string)
const warehouseEntry = {
id : ` warehouse_ ${ sellerId } _ ${ Date . now ( ) } ` , // Уникальный ID записи
storeName : storeName || sellerOrg . fullName || sellerOrg . name ,
storeOwner : sellerOrg.inn || sellerOrg . fullName || sellerOrg . name ,
storeOwner : sellerOrg.inn || sellerOrg . fullName || sellerOrg . name ,
storeImage : sellerOrg.logoUrl || null ,
storeQuantity : 0 , // Пока нет поставок
partnershipDate : new Date ( ) ,
@ -947,57 +954,57 @@ export const resolvers = {
{ logisticsPartnerId : currentUser.organization.id } , // Заказы где организация - логистический партнер
] ,
} ,
include : {
partner : {
include : {
users : true ,
include : {
partner : {
include : {
users : true ,
} ,
} ,
} ,
organization : {
include : {
users : true ,
organization : {
include : {
users : true ,
} ,
} ,
} ,
fulfillmentCenter : {
include : {
users : true ,
fulfillmentCenter : {
include : {
users : true ,
} ,
} ,
} ,
logisticsPartner : true ,
items : {
include : {
product : {
include : {
category : true ,
organization : true ,
logisticsPartner : true ,
items : {
include : {
product : {
include : {
category : true ,
organization : true ,
} ,
} ,
} ,
} ,
} ,
} ,
orderBy : { createdAt : 'desc' } ,
} )
orderBy : { createdAt : 'desc' } ,
} )
console . warn ( '📦 SUPPLY ORDERS FOUND:' , {
totalOrders : orders.length ,
ordersByRole : {
asCreator : orders.filter ( o = > o . organizationId === currentUser . organization . id ) . length ,
asPartner : orders.filter ( o = > o . partnerId === currentUser . organization . id ) . length ,
asFulfillment : orders.filter ( o = > o . fulfillmentCenterId === currentUser . organization . id ) . length ,
asLogistics : orders.filter ( o = > o . logisticsPartnerId === currentUser . organization . id ) . length ,
} ,
orderStatuses : orders.reduce ( ( acc : any , order ) = > {
acc [ order . status ] = ( acc [ order . status ] || 0 ) + 1
return acc
} , { } ) ,
orderIds : orders.map ( o = > o . id ) ,
} )
console . warn ( '📦 SUPPLY ORDERS FOUND:' , {
totalOrders : orders.length ,
ordersByRole : {
asCreator : orders.filter ( ( o ) = > o . organizationId === currentUser . organization . id ) . length ,
asPartner : orders.filter ( ( o ) = > o . partnerId === currentUser . organization . id ) . length ,
asFulfillment : orders.filter ( ( o ) = > o . fulfillmentCenterId === currentUser . organization . id ) . length ,
asLogistics : orders.filter ( ( o ) = > o . logisticsPartnerId === currentUser . organization . id ) . length ,
} ,
orderStatuses : orders.reduce ( ( acc : any , order ) = > {
acc [ order . status ] = ( acc [ order . status ] || 0 ) + 1
return acc
} , { } ) ,
orderIds : orders.map ( ( o ) = > o . id ) ,
} )
return orders
} catch ( error ) {
console . error ( '❌ ERROR IN SUPPLY ORDERS RESOLVER:' , error )
throw new GraphQLError ( ` Ошибка получения заказов поставок: ${ error } ` )
}
return orders
} catch ( error ) {
console . error ( '❌ ERROR IN SUPPLY ORDERS RESOLVER:' , error )
throw new GraphQLError ( ` Ошибка получения заказов поставок: ${ error } ` )
}
} ,
// Счетчик поставок, требующих одобрения
@ -1392,7 +1399,7 @@ export const resolvers = {
// Подсчитываем прибыло по типам
const arrived = {
products : 0 ,
goods : 0 ,
goods : 0 ,
defects : 0 ,
pvzReturns : 0 ,
fulfillmentSupplies : 0 ,
@ -1769,20 +1776,20 @@ export const resolvers = {
// Получаем всех партнеров-селлеров
const counterparties = await prisma . counterparty . findMany ( {
where : {
organizationId : currentUser.organization.id ,
where : {
organizationId : currentUser.organization.id ,
} ,
include : {
counterparty : true ,
} ,
} )
const sellerPartners = counterparties . filter ( c = > c . counterparty . type === 'SELLER' )
const sellerPartners = counterparties . filter ( ( c ) = > c . counterparty . type === 'SELLER' )
console . warn ( '🤝 PARTNERS FOUND:' , {
totalCounterparties : counterparties.length ,
sellerPartners : sellerPartners.length ,
sellers : sellerPartners.map ( p = > ( {
sellers : sellerPartners.map ( ( p ) = > ( {
id : p.counterparty.id ,
name : p.counterparty.name ,
fullName : p.counterparty.fullName ,
@ -1791,15 +1798,15 @@ export const resolvers = {
} )
// Создаем данные склада для каждого партнера-селлера
const stores = sellerPartners . map ( partner = > {
const stores = sellerPartners . map ( ( partner ) = > {
const org = partner . counterparty
// ЛОГИКА ОПРЕДЕЛЕНИЯ НАЗВАНИЯ МАГАЗИНА:
// 1. Если есть name и оно не содержит "ИП" - используем name
// 2. Если есть fullName и name содержит "ИП" - извлекаем из fullName название в скобках
// 3. Fallback к name или fullName
let storeName = org . name
if ( org . fullName && org . name ? . includes ( 'ИП' ) ) {
// Извлекаем название из скобок, например: "ИП Антипова Д. В . (Renrel)" -> "Renrel"
const match = org . fullName . match ( /\(([^)]+)\)/ )
@ -1807,7 +1814,7 @@ export const resolvers = {
storeName = match [ 1 ]
}
}
return {
id : ` store_ ${ org . id } ` ,
storeName : storeName || org . fullName || org . name ,
@ -1828,7 +1835,7 @@ export const resolvers = {
console . warn ( '📦 WAREHOUSE STORES CREATED:' , {
storesCount : stores.length ,
storesPreview : stores.slice ( 0 , 3 ) . map ( s = > ( {
storesPreview : stores.slice ( 0 , 3 ) . map ( ( s ) = > ( {
storeName : s.storeName ,
storeOwner : s.storeOwner ,
storeQuantity : s.storeQuantity ,
@ -2379,7 +2386,7 @@ export const resolvers = {
where : { referrerId : context.user.organizationId } ,
include : {
referral : {
select : {
select : {
type : true ,
createdAt : true ,
} ,
@ -2394,14 +2401,14 @@ export const resolvers = {
// Партнеры за последний месяц
const lastMonth = new Date ( )
lastMonth . setMonth ( lastMonth . getMonth ( ) - 1 )
const monthlyPartners = transactions . filter ( tx = > tx . createdAt > lastMonth ) . length
const monthlyPartners = transactions . filter ( ( tx ) = > tx . createdAt > lastMonth ) . length
const monthlySpheres = transactions
. filter ( tx = > tx . createdAt > lastMonth )
. filter ( ( tx ) = > tx . createdAt > lastMonth )
. reduce ( ( sum , tx ) = > sum + tx . points , 0 )
// Группировка по типам организаций
const typeStats : Record < string , { count : number ; spheres : number } > = { }
transactions . forEach ( tx = > {
transactions . forEach ( ( tx ) = > {
const type = tx . referral . type
if ( ! typeStats [ type ] ) {
typeStats [ type ] = { count : 0 , spheres : 0 }
@ -2412,7 +2419,7 @@ export const resolvers = {
// Группировка по источникам
const sourceStats : Record < string , { count : number ; spheres : number } > = { }
transactions . forEach ( tx = > {
transactions . forEach ( ( tx ) = > {
const source = tx . type === 'REGISTRATION' ? 'REFERRAL_LINK' : 'AUTO_BUSINESS'
if ( ! sourceStats [ source ] ) {
sourceStats [ source ] = { count : 0 , spheres : 0 }
@ -2428,13 +2435,29 @@ export const resolvers = {
monthlySpheres ,
referralsByType : [
{ type : 'SELLER' , count : typeStats [ 'SELLER' ] ? . count || 0 , spheres : typeStats [ 'SELLER' ] ? . spheres || 0 } ,
{ type : 'WHOLESALE' , count : typeStats [ 'WHOLESALE' ] ? . count || 0 , spheres : typeStats [ 'WHOLESALE' ] ? . spheres || 0 } ,
{ type : 'FULFILLMENT' , count : typeStats [ 'FULFILLMENT' ] ? . count || 0 , spheres : typeStats [ 'FULFILLMENT' ] ? . spheres || 0 } ,
{
type : 'WHOLESALE' ,
count : typeStats [ 'WHOLESALE' ] ? . count || 0 ,
spheres : typeStats [ 'WHOLESALE' ] ? . spheres || 0 ,
} ,
{
type : 'FULFILLMENT' ,
count : typeStats [ 'FULFILLMENT' ] ? . count || 0 ,
spheres : typeStats [ 'FULFILLMENT' ] ? . spheres || 0 ,
} ,
{ type : 'LOGIST' , count : typeStats [ 'LOGIST' ] ? . count || 0 , spheres : typeStats [ 'LOGIST' ] ? . spheres || 0 } ,
] ,
referralsBySource : [
{ source : 'REFERRAL_LINK' , count : sourceStats [ 'REFERRAL_LINK' ] ? . count || 0 , spheres : sourceStats [ 'REFERRAL_LINK' ] ? . spheres || 0 } ,
{ source : 'AUTO_BUSINESS' , count : sourceStats [ 'AUTO_BUSINESS' ] ? . count || 0 , spheres : sourceStats [ 'AUTO_BUSINESS' ] ? . spheres || 0 } ,
{
source : 'REFERRAL_LINK' ,
count : sourceStats [ 'REFERRAL_LINK' ] ? . count || 0 ,
spheres : sourceStats [ 'REFERRAL_LINK' ] ? . spheres || 0 ,
} ,
{
source : 'AUTO_BUSINESS' ,
count : sourceStats [ 'AUTO_BUSINESS' ] ? . count || 0 ,
spheres : sourceStats [ 'AUTO_BUSINESS' ] ? . spheres || 0 ,
} ,
] ,
}
} catch ( error ) {
@ -2491,7 +2514,7 @@ export const resolvers = {
} )
// Преобразуем в формат для UI
const referrals = referralTransactions . map ( tx = > ( {
const referrals = referralTransactions . map ( ( tx ) = > ( {
id : tx.id ,
organization : tx.referral ,
source : tx.type === 'REGISTRATION' ? 'REFERRAL_LINK' : 'AUTO_BUSINESS' ,
@ -2546,7 +2569,7 @@ export const resolvers = {
}
} ,
// Мои поставки для селлера (многоуровневая таблица)
// 🔒 Мои поставки с системой безопасности (многоуровневая таблица)
mySupplyOrders : async ( _ : unknown , __ : unknown , context : Context ) = > {
if ( ! context . user ) {
throw new GraphQLError ( 'Требуется авторизация' , {
@ -2563,13 +2586,32 @@ export const resolvers = {
throw new GraphQLError ( 'У пользователя нет организации' )
}
console . warn ( '🔍 GET MY SUPPLY ORDERS:' , {
// 🔒 СОЗДАНИЕ К О Н Т Е К С Т А БЕЗОПАСНОСТИ
const securityContext = createSecurityContext ( {
user : {
id : currentUser.id ,
organizationId : currentUser.organization.id ,
organizationType : currentUser.organization.type ,
} ,
req : context.req ,
} )
console . warn ( '🔍 GET MY SUPPLY ORDERS (SECURE):' , {
userId : context.user.id ,
organizationType : currentUser.organization.type ,
organizationId : currentUser.organization.id ,
securityEnabled : true ,
} )
try {
// 🔒 ПРОВЕРКА ИЗОЛЯЦИИ УЧАСТНИКОВ
await ParticipantIsolation . validateAccess (
prisma ,
currentUser . organization . id ,
currentUser . organization . type ,
'SUPPLY_ORDER' ,
)
// Определяем логику фильтрации в зависимости от типа организации
let whereClause
if ( currentUser . organization . type === 'WHOLESALE' ) {
@ -2591,20 +2633,8 @@ export const resolvers = {
organization : true ,
fulfillmentCenter : true ,
logisticsPartner : true ,
// employee: true, // Поле не существует в SupplyOrder модели
// routes: { // Поле не существует в SupplyOrder модели
// include: {
// logistics: {
// include: {
// organization: true,
// },
// },
// },
// orderBy: {
// createdDate: 'asc', // Сортируем маршруты по дате создания
// },
// },
items : { // Товары (уровень 4)
items : {
// Товары (уровень 4)
include : {
product : {
include : {
@ -2623,55 +2653,97 @@ export const resolvers = {
} ,
} )
console . warn ( '📦 Найдено поставок:' , supplyOrders . length , {
console . warn ( '📦 Найдено поставок (до фильтрации) :' , supplyOrders . length , {
organizationType : currentUser.organization.type ,
filterType : currentUser.organization.type === 'WHOLESALE' ? 'partnerId' : 'organizationId' ,
organizationId : currentUser.organization.id ,
} )
// Преобразуем данные для GraphQL resolver с расширенной рецептурой
const _p rocessedOrders = await Promise . all (
// 🔒 ПРИМЕНЕНИЕ СИСТЕМЫ БЕЗОПАСНОСТИ К КАЖДОМУ З А К А З У
const secureP rocessedOrders = await Promise . all (
supplyOrders . map ( async ( order ) = > {
// Обрабатываем каждый товар для получения рецептуры
// 🔒 АУДИТ ДОСТУПА К КОММЕРЧЕСКИМ ДАННЫМ
await CommercialDataAudit . logAccess ( prisma , {
userId : currentUser.id ,
organizationType : currentUser.organization.type ,
action : 'VIEW_PRICE' ,
resourceType : 'SUPPLY_ORDER' ,
resourceId : order.id ,
metadata : {
orderStatus : order.status ,
totalAmount : order.totalAmount ,
partner : order.partner?.name || order . partner ? . inn ,
} ,
ipAddress : securityContext.ipAddress ,
userAgent : securityContext.userAgent ,
} )
// 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ПО РОЛИ
const filteredOrder = SupplyDataFilter . filterSupplyOrder ( order , securityContext )
// Обрабатываем каждый товар для получения рецептуры с фильтрацией
const processedItems = await Promise . all (
order . items . map ( async ( item ) = > {
filteredOrder . data . items . map ( async ( item : any ) = > {
let recipe = null
// Получаем развернутую рецептуру если есть данные
if (
item . services . length > 0 ||
item . fulfillmentConsumables . length > 0 ||
item . sellerConsumables . length > 0
item . services ? . length > 0 ||
item . fulfillmentConsumables ? . length > 0 ||
item . sellerConsumables ? . length > 0
) {
// Получаем услуги
const services = item . servi ces. length > 0
? await prisma . service . findMany ( {
where : { id : { in : item . services } } ,
include : { organization : true } ,
} )
: [ ]
// 🔒 АУДИТ ДОСТУПА К РЕЦЕПТУРЕ
await CommercialDataAudit . logAc cess ( prisma , {
userId : currentUser.id ,
organizationType : currentUser.organization.type ,
action : 'VIEW_RECIPE' ,
resourceType : 'SUPPLY_ORDER' ,
resourceId : item.id ,
metadata : {
hasServices : item.services?.length > 0 ,
hasFulfillmentConsumables : item.fulfillmentConsumables?.length > 0 ,
hasSellerConsumables : item.sellerConsumables?.length > 0 ,
} ,
ipAddress : securityContext.ipAddress ,
userAgent : securityContext.userAgent ,
} )
// Получаем расходники фулфилмента
const fulfillmentConsumables = item . fulfillmentConsumables . length > 0
? await prisma . supply . findMany ( {
where : { id : { in : item . fulfillmentConsumables } } ,
includ e: { organization : true } ,
} )
: [ ]
// Получаем услуги с фильтрацией
const services =
item . services ? . length > 0
? await prisma . service . findMany ( {
wher e : { id : { in : item . services } } ,
include : { organization : true } ,
} )
: [ ]
// Получаем расходники селлера
const sellerConsumables = item . sellerConsumables . length > 0
? await prisma . supply . findMany ( {
where : { id : { in : item . sellerConsumables } } ,
} )
: [ ]
// Получаем расходники фулфилмента с фильтрацией
const fulfillmentConsumables =
item . fulfillmentConsumables ? . length > 0
? await prisma . supply . findMany ( {
where : { id : { in : item . fulfillmentConsumables } } ,
include : { organization : true } ,
} )
: [ ]
recipe = {
services ,
fulfillmentConsumables ,
sellerConsumables ,
marketplaceCardId : item.marketplaceCardId ,
}
// Получаем расходники селлера с фильтрацией
const sellerConsumables =
item . sellerConsumables ? . length > 0
? await prisma . supply . findMany ( {
where : { id : { in : item . sellerConsumables } } ,
} )
: [ ]
// 🔒 ФИЛЬТРАЦИЯ РЕЦЕПТУРЫ ПО РОЛИ
recipe = SupplyDataFilter . filterRecipeByRole (
{
services ,
fulfillmentConsumables ,
sellerConsumables ,
marketplaceCardId : item.marketplaceCardId ,
} ,
securityContext ,
)
}
return {
@ -2682,21 +2754,27 @@ export const resolvers = {
)
return {
. . . order ,
. . . filteredOrder . data ,
items : processedItems ,
// 🔒 ДОБАВЛЯЕМ МЕТАДАННЫЕ БЕЗОПАСНОСТИ
_security : {
filtered : filteredOrder.filtered ,
removedFields : filteredOrder.removedFields ,
accessLevel : filteredOrder.accessLevel ,
} ,
}
} ) ,
)
console . warn ( '✅ Данные обработаны для многоуровневой таблицы' )
console . warn ( '✅ Данные обработаны с системой безопасности:', {
ordersTotal : secureProcessedOrders.length ,
securityApplied : true ,
organizationType : currentUser.organization.type ,
} )
// ВАРИАНТ 1: Возвращаем обработанные данные с развернутыми рецептурами
return _processedOrders
// О Т К А Т : Возвращаем необработанные данные (без цен услуг/расходников)
// return supplyOrders
return secureProcessedOrders
} catch ( error ) {
console . error ( '❌ Ошибка получения поставок селлера :' , error )
console . error ( '❌ Ошибка получения поставок (security) :' , error )
throw new GraphQLError ( ` Ошибка получения поставок: ${ error instanceof Error ? error.message : String ( error ) } ` )
}
} ,
@ -2822,7 +2900,6 @@ export const resolvers = {
} ,
context : Context ,
) = > {
if ( ! context . user ) {
throw new GraphQLError ( 'Требуется авторизация' , {
extensions : { code : 'UNAUTHENTICATED' } ,
@ -2908,7 +2985,7 @@ export const resolvers = {
type : type ,
dadataData : JSON.parse ( JSON . stringify ( organizationData . rawData ) ) ,
// Реферальная система - генерируем код автоматически
referralCode : generatedReferralCode ,
} ,
@ -2934,7 +3011,7 @@ export const resolvers = {
const referrer = await prisma . organization . findUnique ( {
where : { referralCode : referralCode } ,
} )
if ( referrer ) {
// Создаем реферальную транзакцию (100 сфер)
await prisma . referralTransaction . create ( {
@ -2966,13 +3043,11 @@ export const resolvers = {
if ( partnerCode ) {
try {
// Находим партнера по партнерскому коду
const partner = await prisma . organization . findUnique ( {
where : { referralCode : partnerCode } ,
} )
if ( partner ) {
// Создаем реферальную транзакцию (100 сфер)
await prisma . referralTransaction . create ( {
@ -3015,8 +3090,7 @@ export const resolvers = {
triggeredBy : 'PARTNER_LINK' ,
} ,
} )
}
}
} catch {
// Error processing partner code, but continue registration
}
@ -3050,7 +3124,6 @@ export const resolvers = {
} ,
context : Context ,
) = > {
if ( ! context . user ) {
throw new GraphQLError ( 'Требуется авторизация' , {
extensions : { code : 'UNAUTHENTICATED' } ,
@ -3104,7 +3177,7 @@ export const resolvers = {
const tradeMark = validationResults [ 0 ] ? . data ? . tradeMark
const sellerName = validationResults [ 0 ] ? . data ? . sellerName
const shopName = tradeMark || sellerName || 'Магазин'
// Генерируем уникальный реферальный код
const generatedReferralCode = await generateReferralCode ( )
@ -3114,7 +3187,7 @@ export const resolvers = {
name : shopName , // Используем tradeMark как основное название
fullName : sellerName ? ` ${ sellerName } ( ${ shopName } ) ` : ` Интернет-магазин " ${ shopName } " ` ,
type : 'SELLER' ,
// Реферальная система - генерируем код автоматически
referralCode : generatedReferralCode ,
} ,
@ -3152,7 +3225,7 @@ export const resolvers = {
const referrer = await prisma . organization . findUnique ( {
where : { referralCode : referralCode } ,
} )
if ( referrer ) {
// Создаем реферальную транзакцию (100 сфер)
await prisma . referralTransaction . create ( {
@ -3184,13 +3257,11 @@ export const resolvers = {
if ( partnerCode ) {
try {
// Находим партнера по партнерскому коду
const partner = await prisma . organization . findUnique ( {
where : { referralCode : partnerCode } ,
} )
if ( partner ) {
// Создаем реферальную транзакцию (100 сфер)
await prisma . referralTransaction . create ( {
@ -3233,8 +3304,7 @@ export const resolvers = {
triggeredBy : 'PARTNER_LINK' ,
} ,
} )
}
}
} catch {
// Error processing partner code, but continue registration
}
@ -3859,14 +3929,16 @@ export const resolvers = {
} ,
} ) ,
] )
// АВТОМАТИЧЕСКОЕ СОЗДАНИЕ ЗАПИСЕЙ В ТАБЛИЦЕ СКЛАДА ФУЛФИЛМЕНТА
// Проверяем, есть ли фулфилмент среди партнеров
if ( request . receiver . type === 'FULFILLMENT' && request . sender . type === 'SELLER' ) {
// Селлер становится партнером фулфилмента - создаем запись склада
try {
await autoCreateWarehouseEntry ( request . senderId , request . receiverId )
console . warn ( ` ✅ AUTO WAREHOUSE ENTRY: Created for seller ${ request . senderId } with fulfillment ${ request . receiverId } ` )
console . warn (
` ✅ AUTO WAREHOUSE ENTRY: Created for seller ${ request . senderId } with fulfillment ${ request . receiverId } ` ,
)
} catch ( error ) {
console . error ( '❌ AUTO WAREHOUSE ENTRY ERROR:' , error )
// Н е прерываем основной процесс, если не удалось создать запись склада
@ -3875,7 +3947,9 @@ export const resolvers = {
// Фулфилмент принимает заявку от селлера - создаем запись склада
try {
await autoCreateWarehouseEntry ( request . receiverId , request . senderId )
console . warn ( ` ✅ AUTO WAREHOUSE ENTRY: Created for seller ${ request . receiverId } with fulfillment ${ request . senderId } ` )
console . warn (
` ✅ AUTO WAREHOUSE ENTRY: Created for seller ${ request . receiverId } with fulfillment ${ request . senderId } ` ,
)
} catch ( error ) {
console . error ( '❌ AUTO WAREHOUSE ENTRY ERROR:' , error )
}
@ -4865,7 +4939,7 @@ export const resolvers = {
inputData : args.input ,
timestamp : new Date ( ) . toISOString ( ) ,
} )
if ( ! context . user ) {
throw new GraphQLError ( 'Требуется авторизация' , {
extensions : { code : 'UNAUTHENTICATED' } ,
@ -5055,7 +5129,7 @@ export const resolvers = {
recipe: recipeData ? JSON.stringify(recipeData) : null,
}
*/
// ВОССТАНОВЛЕННАЯ ОРИГИНАЛЬНАЯ ЛОГИКА:
return {
productId : item.productId ,
@ -5082,10 +5156,9 @@ export const resolvers = {
}
// ИСПРАВЛЕНИЕ: Автоматически определяем тип расходников на основе заказчика
const consumableType = currentUser . organization . type === 'SELLER'
? 'SELLER_CONSUMABLES'
: 'FULFILLMENT_CONSUMABLES'
const consumableType =
currentUser . organization . type === 'SELLER' ? 'SELLER_CONSUMABLES' : 'FULFILLMENT_CONSUMABLES'
console . warn ( '🔍 Автоматическое определение типа расходников:' , {
organizationType : currentUser.organization.type ,
consumableType : consumableType ,
@ -5185,11 +5258,14 @@ export const resolvers = {
fromLocation : partner.market || partner . address || 'Поставщик' ,
toLocation : fulfillmentCenterId ? 'Фулфилмент-центр' : 'Получатель' ,
fromAddress : partner.addressFull || partner . address || null ,
toAddress : fulfillmentCenterId ?
( await prisma . organization . findUnique ( {
where : { id : fulfillmentCenterId } ,
select : { addressFull : true , address : true } ,
} ) ) ? . addressFull || null : null ,
toAddress : fulfillmentCenterId
? (
await prisma . organization . findUnique ( {
where : { id : fulfillmentCenterId } ,
select : { addressFull : true , address : true } ,
} )
) ? . addressFull || null
: null ,
status : 'pending' ,
createdDate : new Date ( ) ,
}
@ -5234,12 +5310,13 @@ export const resolvers = {
)
// Проверяем, является ли это первой сделкой организации
const isFirstOrder = await prisma . supplyOrder . count ( {
where : {
organizationId : currentUser.organization.id ,
id : { not : supplyOrder.id } ,
} ,
} ) === 0
const isFirstOrder =
( await prisma . supplyOrder . count ( {
where : {
organizationId : currentUser.organization.id ,
id : { not : supplyOrder.id } ,
} ,
} ) ) === 0
// Если это первая сделка и организация была приглашена по реферальной ссылке
if ( isFirstOrder && currentUser . organization . referredById ) {
@ -5271,14 +5348,11 @@ export const resolvers = {
// Создаем расходники на основе заказанных товаров
// Расходники создаются в организации получателя (фулфилмент-центре)
// Определяем тип расходников на основе consumableType
const supplyType = args . input . consumableType === 'SELLER_CONSUMABLES'
? 'SELLER_CONSUMABLES'
: 'FULFILLMENT_CONSUMABLES'
const supplyType =
args . input . consumableType === 'SELLER_CONSUMABLES' ? 'SELLER_CONSUMABLES' : 'FULFILLMENT_CONSUMABLES'
// Определяем sellerOwnerId для расходников селлеров
const sellerOwnerId = supplyType === 'SELLER_CONSUMABLES'
? currentUser . organization ! . id
: null
const sellerOwnerId = supplyType === 'SELLER_CONSUMABLES' ? currentUser . organization ! . id : null
const suppliesData = args . input . items . map ( ( item ) = > {
const product = products . find ( ( p ) = > p . id === item . productId ) !
@ -7314,7 +7388,7 @@ export const resolvers = {
}
} ,
// Резолверы для новых действий с заказами поставок
// 🔒 МУТАЦИИ ПОСТАВЩИКА С СИСТЕМОЙ БЕЗОПАСНОСТИ
supplierApproveOrder : async ( _ : unknown , args : { id : string } , context : Context ) = > {
if ( ! context . user ) {
throw new GraphQLError ( 'Требуется авторизация' , {
@ -7331,14 +7405,45 @@ export const resolvers = {
throw new GraphQLError ( 'У пользователя нет организации' )
}
// 🔒 ПРОВЕРКА РОЛИ ПОСТАВЩИКА
if ( currentUser . organization . type !== 'WHOLESALE' ) {
throw new GraphQLError ( 'Доступ разрешен только поставщикам (WHOLESALE)' )
}
try {
// Проверяем, что пользователь - поставщик этого заказа
// 🔒 СОЗДАНИЕ К О Н Т Е К С Т А БЕЗОПАСНОСТИ
const securityContext : SecurityContext = {
userId : currentUser.id ,
organizationId : currentUser.organization.id ,
organizationType : currentUser.organization.type ,
userRole : currentUser.organization.type ,
requestMetadata : {
action : 'APPROVE_ORDER' ,
resourceId : args.id ,
timestamp : new Date ( ) . toISOString ( ) ,
ipAddress : context.req?.ip || 'unknown' ,
userAgent : context.req?.get ( 'user-agent' ) || 'unknown' ,
} ,
}
// 🔒 ПРОВЕРКА ИЗОЛЯЦИИ УЧАСТНИКОВ
await ParticipantIsolation . validateAccess (
prisma ,
currentUser . organization . id ,
currentUser . organization . type ,
'SUPPLY_ORDER' ,
)
// 🔒 ПОЛУЧЕНИЕ З А К А З А С ПРОВЕРКОЙ ДОСТУПА
const existingOrder = await prisma . supplyOrder . findFirst ( {
where : {
id : args.id ,
partnerId : currentUser.organization.id , // Только поставщик может одобрить
status : 'PENDING' , // Можно одобрить только заказы в статусе PENDING
} ,
include : {
organization : true ,
partner : true ,
} ,
} )
if ( ! existingOrder ) {
@ -7348,6 +7453,27 @@ export const resolvers = {
}
}
// 🔒 ПРОВЕРКА ПАРТНЕРСКИХ ОТНОШЕНИЙ
await ParticipantIsolation . validatePartnerAccess (
prisma ,
currentUser . organization . id ,
existingOrder . organizationId ,
)
// 🔒 АУДИТ ДОСТУПА К КОММЕРЧЕСКИМ ДАННЫМ
await CommercialDataAudit . logAccess ( prisma , {
userId : currentUser.id ,
organizationType : currentUser.organization.type ,
action : 'APPROVE_ORDER' ,
resourceType : 'SUPPLY_ORDER' ,
resourceId : args.id ,
metadata : {
partnerOrganizationId : existingOrder.organizationId ,
orderValue : existingOrder.totalAmount?.toString ( ) || '0' ,
. . . securityContext . requestMetadata ,
} ,
} )
console . warn ( ` [DEBUG] Поставщик ${ currentUser . organization . name } одобряет заказ ${ args . id } ` )
// 🔄 СИНХРОНИЗАЦИЯ О С Т А Т К О В : Резервируем товары у поставщика
@ -7417,11 +7543,21 @@ export const resolvers = {
organization : true ,
} ,
} ,
recipe : {
include : {
services : true ,
fulfillmentConsumables : true ,
sellerConsumables : true ,
} ,
} ,
} ,
} ,
} ,
} )
// 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ДЛЯ ПОСТАВЩИКА
const filteredOrder = SupplyDataFilter . filterSupplyOrder ( updatedOrder , securityContext )
console . warn ( ` [DEBUG] Заказ ${ args . id } успешно обновлен до статуса: ${ updatedOrder . status } ` )
try {
const orgIds = [
@ -7439,7 +7575,7 @@ export const resolvers = {
return {
success : true ,
message : 'Заказ поставки одобрен поставщиком. Товары зарезервированы, остатки обновлены.' ,
order : updat edOrder,
order : filter edOrder, // 🔒 Возвращаем отфильтрованные данные
}
} catch ( error ) {
console . error ( 'Error approving supply order:' , error )
@ -7466,13 +7602,46 @@ export const resolvers = {
throw new GraphQLError ( 'У пользователя нет организации' )
}
// 🔒 ПРОВЕРКА РОЛИ ПОСТАВЩИКА
if ( currentUser . organization . type !== 'WHOLESALE' ) {
throw new GraphQLError ( 'Доступ разрешен только поставщикам (WHOLESALE)' )
}
try {
// 🔒 СОЗДАНИЕ К О Н Т Е К С Т А БЕЗОПАСНОСТИ
const securityContext : SecurityContext = {
userId : currentUser.id ,
organizationId : currentUser.organization.id ,
organizationType : currentUser.organization.type ,
userRole : currentUser.organization.type ,
requestMetadata : {
action : 'REJECT_ORDER' ,
resourceId : args.id ,
timestamp : new Date ( ) . toISOString ( ) ,
ipAddress : context.req?.ip || 'unknown' ,
userAgent : context.req?.get ( 'user-agent' ) || 'unknown' ,
} ,
}
// 🔒 ПРОВЕРКА ИЗОЛЯЦИИ УЧАСТНИКОВ
await ParticipantIsolation . validateAccess (
prisma ,
currentUser . organization . id ,
currentUser . organization . type ,
'SUPPLY_ORDER' ,
)
// 🔒 ПОЛУЧЕНИЕ З А К А З А С ПРОВЕРКОЙ ДОСТУПА
const existingOrder = await prisma . supplyOrder . findFirst ( {
where : {
id : args.id ,
partnerId : currentUser.organization.id ,
status : 'PENDING' ,
} ,
include : {
organization : true ,
partner : true ,
} ,
} )
if ( ! existingOrder ) {
@ -7482,6 +7651,28 @@ export const resolvers = {
}
}
// 🔒 ПРОВЕРКА ПАРТНЕРСКИХ ОТНОШЕНИЙ
await ParticipantIsolation . validatePartnerAccess (
prisma ,
currentUser . organization . id ,
existingOrder . organizationId ,
)
// 🔒 АУДИТ ДОСТУПА К КОММЕРЧЕСКИМ ДАННЫМ
await CommercialDataAudit . logAccess ( prisma , {
userId : currentUser.id ,
organizationType : currentUser.organization.type ,
action : 'REJECT_ORDER' ,
resourceType : 'SUPPLY_ORDER' ,
resourceId : args.id ,
metadata : {
partnerOrganizationId : existingOrder.organizationId ,
orderValue : existingOrder.totalAmount?.toString ( ) || '0' ,
rejectionReason : args.reason ,
. . . securityContext . requestMetadata ,
} ,
} )
const updatedOrder = await prisma . supplyOrder . update ( {
where : { id : args.id } ,
data : { status : 'CANCELLED' } ,
@ -7498,11 +7689,21 @@ export const resolvers = {
organization : true ,
} ,
} ,
recipe : {
include : {
services : true ,
fulfillmentConsumables : true ,
sellerConsumables : true ,
} ,
} ,
} ,
} ,
} ,
} )
// 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ДЛЯ ПОСТАВЩИКА
const filteredOrder = SupplyDataFilter . filterSupplyOrder ( updatedOrder , securityContext )
// 📦 СНИМАЕМ РЕЗЕРВАЦИЮ ПРИ ОТКЛОНЕНИИ
// Восстанавливаем остатки и убираем резервацию для каждого отклоненного товара
for ( const item of updatedOrder . items ) {
@ -7555,7 +7756,7 @@ export const resolvers = {
return {
success : true ,
message : args.reason ? ` Заказ отклонен поставщиком. Причина: ${ args . reason } ` : 'Заказ отклонен поставщиком' ,
order : updat edOrder,
order : filter edOrder, // 🔒 Возвращаем отфильтрованные данные
}
} catch ( error ) {
console . error ( 'Error rejecting supply order:' , error )
@ -7582,13 +7783,46 @@ export const resolvers = {
throw new GraphQLError ( 'У пользователя нет организации' )
}
// 🔒 ПРОВЕРКА РОЛИ ПОСТАВЩИКА
if ( currentUser . organization . type !== 'WHOLESALE' ) {
throw new GraphQLError ( 'Доступ разрешен только поставщикам (WHOLESALE)' )
}
try {
// 🔒 СОЗДАНИЕ К О Н Т Е К С Т А БЕЗОПАСНОСТИ
const securityContext : SecurityContext = {
userId : currentUser.id ,
organizationId : currentUser.organization.id ,
organizationType : currentUser.organization.type ,
userRole : currentUser.organization.type ,
requestMetadata : {
action : 'SHIP_ORDER' ,
resourceId : args.id ,
timestamp : new Date ( ) . toISOString ( ) ,
ipAddress : context.req?.ip || 'unknown' ,
userAgent : context.req?.get ( 'user-agent' ) || 'unknown' ,
} ,
}
// 🔒 ПРОВЕРКА ИЗОЛЯЦИИ УЧАСТНИКОВ
await ParticipantIsolation . validateAccess (
prisma ,
currentUser . organization . id ,
currentUser . organization . type ,
'SUPPLY_ORDER' ,
)
// 🔒 ПОЛУЧЕНИЕ З А К А З А С ПРОВЕРКОЙ ДОСТУПА
const existingOrder = await prisma . supplyOrder . findFirst ( {
where : {
id : args.id ,
partnerId : currentUser.organization.id ,
status : 'LOGISTICS_CONFIRMED' ,
} ,
include : {
organization : true ,
partner : true ,
} ,
} )
if ( ! existingOrder ) {
@ -7598,6 +7832,27 @@ export const resolvers = {
}
}
// 🔒 ПРОВЕРКА ПАРТНЕРСКИХ ОТНОШЕНИЙ
await ParticipantIsolation . validatePartnerAccess (
prisma ,
currentUser . organization . id ,
existingOrder . organizationId ,
)
// 🔒 АУДИТ ДОСТУПА К КОММЕРЧЕСКИМ ДАННЫМ
await CommercialDataAudit . logAccess ( prisma , {
userId : currentUser.id ,
organizationType : currentUser.organization.type ,
action : 'SHIP_ORDER' ,
resourceType : 'SUPPLY_ORDER' ,
resourceId : args.id ,
metadata : {
partnerOrganizationId : existingOrder.organizationId ,
orderValue : existingOrder.totalAmount?.toString ( ) || '0' ,
. . . securityContext . requestMetadata ,
} ,
} )
// 🔄 СИНХРОНИЗАЦИЯ О С Т А Т К О В : Переводим товары из "заказано" в "в пути"
const orderWithItems = await prisma . supplyOrder . findUnique ( {
where : { id : args.id } ,
@ -7646,11 +7901,21 @@ export const resolvers = {
organization : true ,
} ,
} ,
recipe : {
include : {
services : true ,
fulfillmentConsumables : true ,
sellerConsumables : true ,
} ,
} ,
} ,
} ,
} ,
} )
// 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ДЛЯ ПОСТАВЩИКА
const filteredOrder = SupplyDataFilter . filterSupplyOrder ( updatedOrder , securityContext )
try {
const orgIds = [
updatedOrder . organizationId ,
@ -7667,7 +7932,7 @@ export const resolvers = {
return {
success : true ,
message : "Заказ отправлен поставщиком. Товары переведены в статус 'в пути'." ,
order : updat edOrder,
order : filter edOrder, // 🔒 Возвращаем отфильтрованные данные
}
} catch ( error ) {
console . error ( 'Error shipping supply order:' , error )
@ -8895,7 +9160,7 @@ const wildberriesQueries = {
if ( user ? . organization ) {
const whereCache : any = {
organizationId : user.organization.id ,
period : startDate && endDate ? 'custom' : period ? ? 'week' ,
period : startDate && endDate ? 'custom' : ( period ? ? 'week' ) ,
}
if ( startDate && endDate ) {
whereCache . dateFrom = new Date ( startDate )
@ -8970,8 +9235,7 @@ const wildberriesQueries = {
return {
success : true ,
data : dataFromAdv ,
message :
'Данные по продажам недоступны из-за ошибки WB API. Показаны данные по рекламе из кеша.' ,
message : 'Данные по продажам недоступны из-за ошибки WB API. Показаны данные по рекламе из кеша.' ,
}
}
} catch ( parseErr ) {
@ -9781,7 +10045,24 @@ resolvers.Mutation = {
// Сохранение кеша статистики селлера
saveSellerStatsCache : async (
_ : unknown ,
{ input } : { input : { period : string ; dateFrom? : string | null ; dateTo? : string | null ; productsData? : string | null ; productsTotalSales? : number | null ; productsTotalOrders? : number | null ; productsCount? : number | null ; advertisingData? : string | null ; advertisingTotalCost? : number | null ; advertisingTotalViews? : number | null ; advertisingTotalClicks? : number | null ; expiresAt : string } } ,
{
input ,
} : {
input : {
period : string
dateFrom? : string | null
dateTo? : string | null
productsData? : string | null
productsTotalSales? : number | null
productsTotalOrders? : number | null
productsCount? : number | null
advertisingData? : string | null
advertisingTotalCost? : number | null
advertisingTotalViews? : number | null
advertisingTotalClicks? : number | null
expiresAt : string
}
} ,
context : Context ,
) = > {
if ( ! context . user ) {