Реализован функционал просмотра заявок покупателей на возврат от Wildberries API в фулфилмент-складе. Добавлена интеграция с WB API /api/v1/claims для получения заявок от всех партнеров-селлеров. Создан полнофункциональный интерфейс с поиском, фильтрацией по статусам, детальным просмотром заявок и отображением медиафайлов от покупателей.
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -1,5 +1,38 @@
|
||||
import { gql } from "graphql-tag";
|
||||
|
||||
// Запрос для получения заявок покупателей на возврат от Wildberries
|
||||
export const GET_WB_RETURN_CLAIMS = gql`
|
||||
query GetWbReturnClaims($isArchive: Boolean!, $limit: Int, $offset: Int) {
|
||||
wbReturnClaims(isArchive: $isArchive, limit: $limit, offset: $offset) {
|
||||
claims {
|
||||
id
|
||||
claimType
|
||||
status
|
||||
statusEx
|
||||
nmId
|
||||
userComment
|
||||
wbComment
|
||||
dt
|
||||
imtName
|
||||
orderDt
|
||||
dtUpdate
|
||||
photos
|
||||
videoPaths
|
||||
actions
|
||||
price
|
||||
currencyCode
|
||||
srid
|
||||
sellerOrganization {
|
||||
id
|
||||
name
|
||||
inn
|
||||
}
|
||||
}
|
||||
total
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_ME = gql`
|
||||
query GetMe {
|
||||
me {
|
||||
|
@ -8171,6 +8171,147 @@ const wildberriesQueries = {
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Получение заявок покупателей на возврат от Wildberries от всех партнеров-селлеров
|
||||
wbReturnClaims: async (
|
||||
_: unknown,
|
||||
{ isArchive, limit, offset }: { isArchive: boolean; limit?: number; offset?: number },
|
||||
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("У пользователя нет организации");
|
||||
}
|
||||
|
||||
// Проверяем, что это фулфилмент организация
|
||||
if (user.organization.type !== "FULFILLMENT") {
|
||||
throw new GraphQLError("Доступ только для фулфилмент организаций");
|
||||
}
|
||||
|
||||
// Получаем всех партнеров-селлеров с активными WB API ключами
|
||||
const partnerSellerOrgs = await prisma.counterparty.findMany({
|
||||
where: {
|
||||
organizationId: user.organization.id,
|
||||
},
|
||||
include: {
|
||||
counterparty: {
|
||||
include: {
|
||||
apiKeys: {
|
||||
where: {
|
||||
marketplace: "WILDBERRIES",
|
||||
isActive: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Фильтруем только селлеров с WB API ключами
|
||||
const sellersWithWbKeys = partnerSellerOrgs.filter(
|
||||
(partner) =>
|
||||
partner.counterparty.type === "SELLER" &&
|
||||
partner.counterparty.apiKeys.length > 0
|
||||
);
|
||||
|
||||
if (sellersWithWbKeys.length === 0) {
|
||||
return {
|
||||
claims: [],
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`Found ${sellersWithWbKeys.length} seller partners with WB keys`);
|
||||
|
||||
// Получаем заявки от всех селлеров параллельно
|
||||
const claimsPromises = sellersWithWbKeys.map(async (partner) => {
|
||||
const wbApiKey = partner.counterparty.apiKeys[0].apiKey;
|
||||
const wbService = new WildberriesService(wbApiKey);
|
||||
|
||||
try {
|
||||
const claimsResponse = await wbService.getClaims({
|
||||
isArchive,
|
||||
limit: Math.ceil((limit || 50) / sellersWithWbKeys.length), // Распределяем лимит между селлерами
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
// Добавляем информацию о селлере к каждой заявке
|
||||
const claimsWithSeller = claimsResponse.claims.map((claim) => ({
|
||||
...claim,
|
||||
sellerOrganization: {
|
||||
id: partner.counterparty.id,
|
||||
name: partner.counterparty.name || "Неизвестная организация",
|
||||
inn: partner.counterparty.inn || "",
|
||||
},
|
||||
}));
|
||||
|
||||
console.log(`Got ${claimsWithSeller.length} claims from seller ${partner.counterparty.name}`);
|
||||
return claimsWithSeller;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching claims for seller ${partner.counterparty.name}:`, error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const allClaims = (await Promise.all(claimsPromises)).flat();
|
||||
console.log(`Total claims aggregated: ${allClaims.length}`);
|
||||
|
||||
// Сортируем по дате создания (новые первыми)
|
||||
allClaims.sort((a, b) => new Date(b.dt).getTime() - new Date(a.dt).getTime());
|
||||
|
||||
// Применяем пагинацию
|
||||
const paginatedClaims = allClaims.slice(offset || 0, (offset || 0) + (limit || 50));
|
||||
console.log(`Paginated claims: ${paginatedClaims.length}`);
|
||||
|
||||
// Преобразуем в формат фронтенда
|
||||
const transformedClaims = paginatedClaims.map((claim) => ({
|
||||
id: claim.id,
|
||||
claimType: claim.claim_type,
|
||||
status: claim.status,
|
||||
statusEx: claim.status_ex,
|
||||
nmId: claim.nm_id,
|
||||
userComment: claim.user_comment || "",
|
||||
wbComment: claim.wb_comment || null,
|
||||
dt: claim.dt,
|
||||
imtName: claim.imt_name,
|
||||
orderDt: claim.order_dt,
|
||||
dtUpdate: claim.dt_update,
|
||||
photos: claim.photos || [],
|
||||
videoPaths: claim.video_paths || [],
|
||||
actions: claim.actions || [],
|
||||
price: claim.price,
|
||||
currencyCode: claim.currency_code,
|
||||
srid: claim.srid,
|
||||
sellerOrganization: claim.sellerOrganization,
|
||||
}));
|
||||
|
||||
console.log(`Returning ${transformedClaims.length} transformed claims to frontend`);
|
||||
|
||||
return {
|
||||
claims: transformedClaims,
|
||||
total: allClaims.length,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching WB return claims:", error);
|
||||
throw new GraphQLError(
|
||||
error instanceof Error ? error.message : "Ошибка получения заявок на возврат"
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Резолверы для внешней рекламы
|
||||
|
@ -118,6 +118,13 @@ export const typeDefs = gql`
|
||||
# Список кампаний Wildberries
|
||||
getWildberriesCampaignsList: WildberriesCampaignsListResponse!
|
||||
|
||||
# Заявки покупателей на возврат от Wildberries (для фулфилмента)
|
||||
wbReturnClaims(
|
||||
isArchive: Boolean!
|
||||
limit: Int
|
||||
offset: Int
|
||||
): WbReturnClaimsResponse!
|
||||
|
||||
# Типы для внешней рекламы
|
||||
getExternalAds(dateFrom: String!, dateTo: String!): ExternalAdsResponse!
|
||||
|
||||
@ -1338,6 +1345,39 @@ export const typeDefs = gql`
|
||||
): WBWarehouseCacheResponse!
|
||||
}
|
||||
|
||||
# Типы для заявок на возврат WB
|
||||
type WbReturnClaim {
|
||||
id: String!
|
||||
claimType: Int!
|
||||
status: Int!
|
||||
statusEx: Int!
|
||||
nmId: Int!
|
||||
userComment: String!
|
||||
wbComment: String
|
||||
dt: String!
|
||||
imtName: String!
|
||||
orderDt: String!
|
||||
dtUpdate: String!
|
||||
photos: [String!]!
|
||||
videoPaths: [String!]!
|
||||
actions: [String!]!
|
||||
price: Int!
|
||||
currencyCode: String!
|
||||
srid: String!
|
||||
sellerOrganization: WbSellerOrganization!
|
||||
}
|
||||
|
||||
type WbSellerOrganization {
|
||||
id: String!
|
||||
name: String!
|
||||
inn: String!
|
||||
}
|
||||
|
||||
type WbReturnClaimsResponse {
|
||||
claims: [WbReturnClaim!]!
|
||||
total: Int!
|
||||
}
|
||||
|
||||
# Типы для статистики склада фулфилмента
|
||||
type FulfillmentWarehouseStats {
|
||||
products: WarehouseStatsItem!
|
||||
|
Reference in New Issue
Block a user