Merge pull request 'Refactor: Replace wholesaler with supplier terminology and add fulfillment consumables logic' (#1) from testing into main

Reviewed-on: #1
This commit is contained in:
2025-07-30 17:41:50 +03:00
31 changed files with 3343 additions and 1538 deletions

View File

@ -692,7 +692,7 @@ export const resolvers = {
});
},
// Мои расходники (объединенные данные из supply и supplyOrder)
// Расходники селлеров (материалы клиентов на складе фулфилмента)
mySupplies: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
@ -709,21 +709,86 @@ export const resolvers = {
throw new GraphQLError("У пользователя нет организации");
}
// Получаем расходники из таблицы supply (уже доставленные)
// Получаем заказы поставок, где фулфилмент является получателем,
// но НЕ создателем (т.е. селлеры заказали расходники для фулфилмента)
const sellerSupplyOrders = await prisma.supplyOrder.findMany({
where: {
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
organizationId: { not: currentUser.organization.id }, // Создатель - НЕ мы
status: "DELIVERED", // Только доставленные
},
include: {
organization: true,
partner: true,
items: {
include: {
product: {
include: {
category: true,
},
},
},
},
},
});
// Получаем расходники селлеров из таблицы supply
// Это расходники, созданные при доставке заказов от селлеров
const existingSupplies = await prisma.supply.findMany({
where: { organizationId: currentUser.organization.id },
include: { organization: true },
orderBy: { createdAt: "desc" },
});
// Логирование для отладки
console.log("🔥🔥🔥 SELLER SUPPLIES RESOLVER CALLED 🔥🔥🔥");
console.log("📊 Расходники селлеров:", {
organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type,
existingSuppliesCount: existingSupplies.length,
sellerOrdersCount: sellerSupplyOrders.length,
sellerOrders: sellerSupplyOrders.map((o) => ({
id: o.id,
sellerName: o.organization.name,
supplierName: o.partner.name,
status: o.status,
itemsCount: o.items.length,
})),
});
// Возвращаем только расходники селлеров из таблицы supply
// TODO: В будущем можно добавить фильтрацию по источнику заказа
return existingSupplies;
},
// Расходники фулфилмента (материалы для работы фулфилмента)
myFulfillmentSupplies: async (
_: unknown,
__: unknown,
context: Context
) => {
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError("У пользователя нет организации");
}
// Получаем заказы поставок, созданные этим фулфилмент-центром для себя
// Показываем только заказы, которые еще не доставлены
const ourSupplyOrders = await prisma.supplyOrder.findMany({
const fulfillmentSupplyOrders = await prisma.supplyOrder.findMany({
where: {
organizationId: currentUser.organization.id, // Создали мы
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
status: {
in: ["PENDING", "CONFIRMED", "IN_TRANSIT"], // Только не доставленные заказы
in: ["PENDING", "CONFIRMED", "IN_TRANSIT", "DELIVERED"], // Все статусы
},
},
include: {
@ -742,16 +807,16 @@ export const resolvers = {
});
// Преобразуем заказы поставок в формат supply для единообразия
const suppliesFromOrders = ourSupplyOrders.flatMap((order) =>
const fulfillmentSupplies = fulfillmentSupplyOrders.flatMap((order) =>
order.items.map((item) => ({
id: `order-${order.id}-${item.id}`,
id: `fulfillment-order-${order.id}-${item.id}`,
name: item.product.name,
description:
item.product.description || `Заказ от ${order.partner.name}`,
item.product.description || `Расходники от ${order.partner.name}`,
price: item.price,
quantity: item.quantity,
unit: "шт",
category: item.product.category?.name || "Расходники",
category: item.product.category?.name || "Расходники фулфилмента",
status:
order.status === "PENDING"
? "in-transit"
@ -764,6 +829,7 @@ export const resolvers = {
supplier: order.partner.name || order.partner.fullName || "Не указан",
minStock: Math.round(item.quantity * 0.1),
currentStock: order.status === "DELIVERED" ? item.quantity : 0,
usedStock: 0, // TODO: Подсчитывать реальное использование
imageUrl: null,
createdAt: order.createdAt,
updatedAt: order.updatedAt,
@ -773,37 +839,22 @@ export const resolvers = {
}))
);
// Проверяем все заказы для этого фулфилмент-центра для отладки
const allOurOrders = await prisma.supplyOrder.findMany({
where: {
organizationId: currentUser.organization.id,
fulfillmentCenterId: currentUser.organization.id,
},
select: { id: true, status: true, createdAt: true },
});
// Логирование для отладки
console.log("🔥🔥🔥 MY_SUPPLIES RESOLVER CALLED 🔥🔥🔥");
console.log("📊 mySupplies resolver debug:", {
console.log("🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED 🔥🔥🔥");
console.log("📊 Расходники фулфилмента:", {
organizationId: currentUser.organization.id,
existingSuppliesCount: existingSupplies.length,
ourSupplyOrdersCount: ourSupplyOrders.length,
suppliesFromOrdersCount: suppliesFromOrders.length,
allOrdersCount: allOurOrders.length,
allOrdersStatuses: allOurOrders.map((o) => ({
id: o.id,
status: o.status,
createdAt: o.createdAt,
})),
filteredOrdersStatuses: ourSupplyOrders.map((o) => ({
organizationType: currentUser.organization.type,
fulfillmentOrdersCount: fulfillmentSupplyOrders.length,
fulfillmentSuppliesCount: fulfillmentSupplies.length,
fulfillmentOrders: fulfillmentSupplyOrders.map((o) => ({
id: o.id,
supplierName: o.partner.name,
status: o.status,
itemsCount: o.items.length,
})),
});
console.log("🔥🔥🔥 END MY_SUPPLIES RESOLVER 🔥🔥🔥");
// Объединяем существующие расходники и расходники из заказов
return [...existingSupplies, ...suppliesFromOrders];
return fulfillmentSupplies;
},
// Заказы поставок расходников
@ -882,7 +933,7 @@ export const resolvers = {
// Считаем заказы поставок, требующие действий
// Наши расходники (созданные нами для себя) - требуют действий по статусам
// Расходники фулфилмента (созданные нами для себя) - требуют действий по статусам
const ourSupplyOrders = await prisma.supplyOrder.count({
where: {
organizationId: currentUser.organization.id, // Создали мы
@ -900,8 +951,17 @@ export const resolvers = {
},
});
// 🔔 ВХОДЯЩИЕ ЗАКАЗЫ ДЛЯ ПОСТАВЩИКОВ (WHOLESALE) - требуют подтверждения
const incomingSupplierOrders = await prisma.supplyOrder.count({
where: {
partnerId: currentUser.organization.id, // Мы - поставщик
status: "PENDING", // Ожидает подтверждения от поставщика
},
});
// Общий счетчик поставок
const pendingSupplyOrders = ourSupplyOrders + sellerSupplyOrders;
const pendingSupplyOrders =
ourSupplyOrders + sellerSupplyOrders + incomingSupplierOrders;
// Считаем входящие заявки на партнерство со статусом PENDING
const pendingIncomingRequests = await prisma.counterpartyRequest.count({
@ -913,8 +973,9 @@ export const resolvers = {
return {
supplyOrders: pendingSupplyOrders,
ourSupplyOrders: ourSupplyOrders, // Наши расходники
ourSupplyOrders: ourSupplyOrders, // Расходники фулфилмента
sellerSupplyOrders: sellerSupplyOrders, // Расходники селлеров
incomingSupplierOrders: incomingSupplierOrders, // 🔔 Входящие заказы для поставщиков
incomingRequests: pendingIncomingRequests,
total: pendingSupplyOrders + pendingIncomingRequests,
};
@ -997,14 +1058,30 @@ export const resolvers = {
throw new GraphQLError("Товары доступны только для поставщиков");
}
return await prisma.product.findMany({
where: { organizationId: currentUser.organization.id },
const products = await prisma.product.findMany({
where: {
organizationId: currentUser.organization.id,
type: "PRODUCT", // Показываем только товары, исключаем расходники
},
include: {
category: true,
organization: true,
},
orderBy: { createdAt: "desc" },
});
console.log("🔥 MY_PRODUCTS RESOLVER DEBUG:", {
organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type,
totalProducts: products.length,
productTypes: products.map((p) => ({
id: p.id,
name: p.name,
type: p.type,
})),
});
return products;
},
// Товары на складе фулфилмента (из доставленных заказов поставок)
@ -1086,19 +1163,30 @@ export const resolvers = {
);
for (const item of order.items) {
// Добавляем товар на склад с информацией о заказе
allProducts.push({
...item.product,
// Дополнительная информация о заказе
orderedQuantity: item.quantity,
orderedPrice: item.price,
orderId: order.id,
orderDate: order.deliveryDate,
seller: order.organization, // Селлер, который заказал
supplier: order.partner, // Поставщик товара
// Для совместимости с существующим интерфейсом
organization: order.organization, // Указываем селлера как владельца
});
// Добавляем только товары типа PRODUCT, исключаем расходники
if (item.product.type === "PRODUCT") {
allProducts.push({
...item.product,
// Дополнительная информация о заказе
orderedQuantity: item.quantity,
orderedPrice: item.price,
orderId: order.id,
orderDate: order.deliveryDate,
seller: order.organization, // Селлер, который заказал
supplier: order.partner, // Поставщик товара
// Для совместимости с существующим интерфейсом
organization: order.organization, // Указываем селлера как владельца
});
} else {
console.log(
`🚫 Исключен расходник из основного склада фулфилмента:`,
{
name: item.product.name,
type: item.product.type,
orderId: order.id,
}
);
}
}
}
@ -1123,6 +1211,7 @@ export const resolvers = {
const where: Record<string, unknown> = {
isActive: true, // Показываем только активные товары
type: "PRODUCT", // Показываем только товары, исключаем расходники
organization: {
type: "WHOLESALE", // Только товары поставщиков
},
@ -1141,7 +1230,7 @@ export const resolvers = {
where.categoryId = args.category;
}
return await prisma.product.findMany({
const products = await prisma.product.findMany({
where,
include: {
category: true,
@ -1154,6 +1243,20 @@ export const resolvers = {
orderBy: { createdAt: "desc" },
take: 100, // Ограничиваем количество результатов
});
console.log("🔥 ALL_PRODUCTS RESOLVER DEBUG:", {
searchArgs: args,
whereCondition: where,
totalProducts: products.length,
productTypes: products.map((p) => ({
id: p.id,
name: p.name,
type: p.type,
org: p.organization.name,
})),
});
return products;
},
// Все категории
@ -3376,6 +3479,94 @@ export const resolvers = {
}
},
// Использовать расходники фулфилмента
useFulfillmentSupplies: async (
_: unknown,
args: {
input: {
supplyId: string;
quantityUsed: number;
description?: string;
};
},
context: Context
) => {
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что это фулфилмент центр
if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError(
"Использование расходников доступно только для фулфилмент центров"
);
}
// Находим расходник
const existingSupply = await prisma.supply.findFirst({
where: {
id: args.input.supplyId,
organizationId: currentUser.organization.id,
},
});
if (!existingSupply) {
throw new GraphQLError("Расходник не найден или нет доступа");
}
// Проверяем, что достаточно расходников
if (existingSupply.currentStock < args.input.quantityUsed) {
throw new GraphQLError(
`Недостаточно расходников. Доступно: ${existingSupply.currentStock}, требуется: ${args.input.quantityUsed}`
);
}
try {
// Обновляем количество расходников
const updatedSupply = await prisma.supply.update({
where: { id: args.input.supplyId },
data: {
currentStock: existingSupply.currentStock - args.input.quantityUsed,
usedStock:
(existingSupply.usedStock || 0) + args.input.quantityUsed,
updatedAt: new Date(),
},
include: { organization: true },
});
console.log("🔧 Использованы расходники фулфилмента:", {
supplyName: updatedSupply.name,
quantityUsed: args.input.quantityUsed,
remainingStock: updatedSupply.currentStock,
totalUsed: updatedSupply.usedStock,
description: args.input.description,
});
return {
success: true,
message: `Использовано ${args.input.quantityUsed} ${updatedSupply.unit} расходника "${updatedSupply.name}"`,
supply: updatedSupply,
};
} catch (error) {
console.error("Error using fulfillment supplies:", error);
return {
success: false,
message: "Ошибка при использовании расходников",
};
}
},
// Создать заказ поставки расходников
// Два сценария:
// 1. Селлер → Поставщик → Фулфилмент (селлер заказывает для фулфилмент-центра)
@ -3614,6 +3805,41 @@ export const resolvers = {
data: suppliesData,
});
// 🔔 ОТПРАВЛЯЕМ УВЕДОМЛЕНИЕ ПОСТАВЩИКУ О НОВОМ ЗАКАЗЕ
try {
const orderSummary = args.input.items
.map((item) => {
const product = products.find((p) => p.id === item.productId)!;
return `${product.name} - ${item.quantity} шт.`;
})
.join(", ");
const notificationMessage = `🔔 Новый заказ поставки от ${
currentUser.organization.name || currentUser.organization.fullName
}!\n\nТовары: ${orderSummary}\ата доставки: ${new Date(
args.input.deliveryDate
).toLocaleDateString(
"ru-RU"
)}\nОбщая сумма: ${totalAmount.toLocaleString(
"ru-RU"
)}\n\ожалуйста, подтвердите заказ в разделе "Поставки".`;
await prisma.message.create({
data: {
content: notificationMessage,
type: "TEXT",
senderId: context.user.id,
senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.input.partnerId,
},
});
console.log(`✅ Уведомление отправлено поставщику ${partner.name}`);
} catch (notificationError) {
console.error("❌ Ошибка отправки уведомления:", notificationError);
// Не прерываем выполнение, если уведомление не отправилось
}
// Формируем сообщение в зависимости от роли организации
let successMessage = "";
if (organizationRole === "SELLER") {
@ -5166,7 +5392,8 @@ export const resolvers = {
console.error("Error updating external ad clicks:", error);
return {
success: false,
message: error instanceof Error ? error.message : "Ошибка обновления кликов",
message:
error instanceof Error ? error.message : "Ошибка обновления кликов",
externalAd: null,
};
}
@ -6201,21 +6428,21 @@ const externalAdQueries = {
organizationId: user.organization.id,
date: {
gte: new Date(dateFrom),
lte: new Date(dateTo + 'T23:59:59.999Z'),
lte: new Date(dateTo + "T23:59:59.999Z"),
},
},
orderBy: {
date: 'desc',
date: "desc",
},
});
return {
success: true,
message: null,
externalAds: externalAds.map(ad => ({
externalAds: externalAds.map((ad) => ({
...ad,
cost: parseFloat(ad.cost.toString()),
date: ad.date.toISOString().split('T')[0],
date: ad.date.toISOString().split("T")[0],
createdAt: ad.createdAt.toISOString(),
updatedAt: ad.updatedAt.toISOString(),
})),
@ -6224,7 +6451,10 @@ const externalAdQueries = {
console.error("Error fetching external ads:", error);
return {
success: false,
message: error instanceof Error ? error.message : "Ошибка получения внешней рекламы",
message:
error instanceof Error
? error.message
: "Ошибка получения внешней рекламы",
externalAds: [],
};
}
@ -6234,7 +6464,17 @@ const externalAdQueries = {
const externalAdMutations = {
createExternalAd: async (
_: unknown,
{ input }: { input: { name: string; url: string; cost: number; date: string; nmId: string } },
{
input,
}: {
input: {
name: string;
url: string;
cost: number;
date: string;
nmId: string;
};
},
context: Context
) => {
if (!context.user) {
@ -6270,7 +6510,7 @@ const externalAdMutations = {
externalAd: {
...externalAd,
cost: parseFloat(externalAd.cost.toString()),
date: externalAd.date.toISOString().split('T')[0],
date: externalAd.date.toISOString().split("T")[0],
createdAt: externalAd.createdAt.toISOString(),
updatedAt: externalAd.updatedAt.toISOString(),
},
@ -6279,7 +6519,10 @@ const externalAdMutations = {
console.error("Error creating external ad:", error);
return {
success: false,
message: error instanceof Error ? error.message : "Ошибка создания внешней рекламы",
message:
error instanceof Error
? error.message
: "Ошибка создания внешней рекламы",
externalAd: null,
};
}
@ -6287,7 +6530,19 @@ const externalAdMutations = {
updateExternalAd: async (
_: unknown,
{ id, input }: { id: string; input: { name: string; url: string; cost: number; date: string; nmId: string } },
{
id,
input,
}: {
id: string;
input: {
name: string;
url: string;
cost: number;
date: string;
nmId: string;
};
},
context: Context
) => {
if (!context.user) {
@ -6335,7 +6590,7 @@ const externalAdMutations = {
externalAd: {
...externalAd,
cost: parseFloat(externalAd.cost.toString()),
date: externalAd.date.toISOString().split('T')[0],
date: externalAd.date.toISOString().split("T")[0],
createdAt: externalAd.createdAt.toISOString(),
updatedAt: externalAd.updatedAt.toISOString(),
},
@ -6344,7 +6599,10 @@ const externalAdMutations = {
console.error("Error updating external ad:", error);
return {
success: false,
message: error instanceof Error ? error.message : "Ошибка обновления внешней рекламы",
message:
error instanceof Error
? error.message
: "Ошибка обновления внешней рекламы",
externalAd: null,
};
}
@ -6396,21 +6654,19 @@ const externalAdMutations = {
console.error("Error deleting external ad:", error);
return {
success: false,
message: error instanceof Error ? error.message : "Ошибка удаления внешней рекламы",
message:
error instanceof Error
? error.message
: "Ошибка удаления внешней рекламы",
externalAd: null,
};
}
},
};
// Резолверы для кеша склада WB
const wbWarehouseCacheQueries = {
getWBWarehouseData: async (
_: unknown,
__: unknown,
context: Context
) => {
getWBWarehouseData: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
@ -6438,7 +6694,7 @@ const wbWarehouseCacheQueries = {
cacheDate: today,
},
orderBy: {
createdAt: 'desc',
createdAt: "desc",
},
});
@ -6449,7 +6705,7 @@ const wbWarehouseCacheQueries = {
message: "Данные получены из кеша",
cache: {
...cache,
cacheDate: cache.cacheDate.toISOString().split('T')[0],
cacheDate: cache.cacheDate.toISOString().split("T")[0],
createdAt: cache.createdAt.toISOString(),
updatedAt: cache.updatedAt.toISOString(),
},
@ -6468,7 +6724,10 @@ const wbWarehouseCacheQueries = {
console.error("Error getting WB warehouse cache:", error);
return {
success: false,
message: error instanceof Error ? error.message : "Ошибка получения кеша склада WB",
message:
error instanceof Error
? error.message
: "Ошибка получения кеша склада WB",
cache: null,
fromCache: false,
};
@ -6479,7 +6738,16 @@ const wbWarehouseCacheQueries = {
const wbWarehouseCacheMutations = {
saveWBWarehouseCache: async (
_: unknown,
{ input }: { input: { data: string; totalProducts: number; totalStocks: number; totalReserved: number } },
{
input,
}: {
input: {
data: string;
totalProducts: number;
totalStocks: number;
totalReserved: number;
};
},
context: Context
) => {
if (!context.user) {
@ -6531,7 +6799,7 @@ const wbWarehouseCacheMutations = {
message: "Кеш склада WB успешно сохранен",
cache: {
...cache,
cacheDate: cache.cacheDate.toISOString().split('T')[0],
cacheDate: cache.cacheDate.toISOString().split("T")[0],
createdAt: cache.createdAt.toISOString(),
updatedAt: cache.updatedAt.toISOString(),
},
@ -6541,7 +6809,10 @@ const wbWarehouseCacheMutations = {
console.error("Error saving WB warehouse cache:", error);
return {
success: false,
message: error instanceof Error ? error.message : "Ошибка сохранения кеша склада WB",
message:
error instanceof Error
? error.message
: "Ошибка сохранения кеша склада WB",
cache: null,
fromCache: false,
};