Skip to content

Commit 2fe9894

Browse files
salacosteclaude
andcommitted
feat(analytics): add getWbWarehousesStock — WB warehouses current inventory
New method getWbWarehousesStock() returns current inventory across all WB warehouses with warehouseId, regionName, and offset pagination (up to 250K rows). Replaces deprecated GET /api/v1/supplier/stocks (disabled June 23, 2026). - 3 new types: WbWarehousesStockRequest, WbWarehouseStockItem, WbWarehousesStockResponse - Rate limit: 3 req/min, 20s interval, burst 1 - 6 new unit tests, method count updated 16→17 - JSDoc with deprecation notice and pagination examples Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent be2c5ce commit 2fe9894

File tree

4 files changed

+189
-2
lines changed

4 files changed

+189
-2
lines changed

src/config/analytics-rate-limits.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ export const analyticsRateLimits: Record<string, RateLimitConfig> = {
103103
intervalSeconds: 20,
104104
burstLimit: 3,
105105
},
106+
// v1 WB Warehouses Inventory (new endpoint, replaces deprecated /api/v1/supplier/stocks)
107+
'analytics.postStocksReportWbWarehouses': {
108+
requestsPerMinute: 3,
109+
intervalSeconds: 20,
110+
burstLimit: 1,
111+
},
106112
// v3 Sales Funnel endpoints
107113
'analytics.postSalesFunnelProducts': {
108114
requestsPerMinute: 3,

src/modules/analytics/index.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import type {
4141
TableShippingOfficeResponse,
4242
TableSizeRequest,
4343
TableSizeResponse,
44+
WbWarehousesStockRequest,
45+
WbWarehousesStockResponse,
4446
} from '../../types/analytics.types';
4547

4648
export class AnalyticsModule {
@@ -522,4 +524,54 @@ export class AnalyticsModule {
522524
{ rateLimitKey: 'analytics.postSalesFunnelGroupedHistory' }
523525
);
524526
}
527+
528+
/**
529+
* Текущие остатки на складах WB
530+
*
531+
* Возвращает актуальные остатки товаров на складах Wildberries.
532+
* Данные обновляются раз в 30 минут. Одна строка = один размер на одном складе.
533+
* Результаты отсортированы по возрастанию nmId.
534+
*
535+
* Доступен только для токенов типа Personal и Service.
536+
*
537+
* **Заменяет устаревший метод** `GET /api/v1/supplier/stocks`,
538+
* который будет отключён 23 июня 2026.
539+
*
540+
* Rate limit: 3 requests per minute, 20-second interval, burst 1
541+
*
542+
* @param data - Filter and pagination parameters (all optional)
543+
* @param data.nmIds - WB articles to filter (0-1000, empty = all)
544+
* @param data.chrtIds - Size IDs (only for articles in nmIds)
545+
* @param data.limit - Rows in response (max 250000, default 250000)
546+
* @param data.offset - Skip N results for pagination (default 0)
547+
* @returns Current inventory with warehouse IDs, region names, quantities
548+
* @throws {AuthenticationError} When API key is invalid (401/403)
549+
* @throws {RateLimitError} When rate limit exceeded (429)
550+
* @throws {ValidationError} When request data is invalid (400/422)
551+
* @throws {NetworkError} When network request fails or times out
552+
* @since 3.4.0
553+
* @see {@link https://dev.wildberries.ru/docs/openapi/analytics#tag/Istoriya-ostatkov/operation/postV1StocksReportWbWarehouses}
554+
* @example
555+
* ```typescript
556+
* // Get all inventory
557+
* const stock = await sdk.analytics.getWbWarehousesStock();
558+
* for (const item of stock.data.items) {
559+
* console.log(`${item.warehouseName} (${item.regionName}): ${item.quantity} шт.`);
560+
* }
561+
*
562+
* // With filters and pagination
563+
* const page = await sdk.analytics.getWbWarehousesStock({
564+
* nmIds: [395996251],
565+
* limit: 100,
566+
* offset: 0,
567+
* });
568+
* ```
569+
*/
570+
async getWbWarehousesStock(data?: WbWarehousesStockRequest): Promise<WbWarehousesStockResponse> {
571+
return this.client.post<WbWarehousesStockResponse>(
572+
'https://seller-analytics-api.wildberries.ru/api/analytics/v1/stocks-report/wb-warehouses',
573+
data ?? {},
574+
{ rateLimitKey: 'analytics.postStocksReportWbWarehouses' }
575+
);
576+
}
525577
}

src/types/analytics.types.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1670,3 +1670,49 @@ export type SalesFunnelGroupedHistoryResponse = {
16701670
/** Валюта (например, "RUB") */
16711671
currency?: string;
16721672
}[];
1673+
1674+
// ============================================================================
1675+
// WB Warehouses Inventory Types (v1)
1676+
// ============================================================================
1677+
1678+
/** Request for WB warehouses current inventory
1679+
* @since 3.4.0 */
1680+
export interface WbWarehousesStockRequest {
1681+
/** WB articles (0-1000 items). Empty = all products */
1682+
nmIds?: number[];
1683+
/** Size IDs. Only used for articles specified in nmIds */
1684+
chrtIds?: number[];
1685+
/** Number of rows in response (max 250000, default 250000) */
1686+
limit?: number;
1687+
/** How many results to skip (default 0) */
1688+
offset?: number;
1689+
}
1690+
1691+
/** Single inventory item — 1 size in 1 WB warehouse
1692+
* @since 3.4.0 */
1693+
export interface WbWarehouseStockItem {
1694+
/** WB article ID */
1695+
nmId: number;
1696+
/** Size ID */
1697+
chrtId: number;
1698+
/** WB warehouse ID */
1699+
warehouseId: number;
1700+
/** WB warehouse name */
1701+
warehouseName: string;
1702+
/** Region name */
1703+
regionName: string;
1704+
/** Current quantity in warehouse */
1705+
quantity: number;
1706+
/** Quantity in transit to client */
1707+
inWayToClient: number;
1708+
/** Quantity in transit from client (returns) */
1709+
inWayFromClient: number;
1710+
}
1711+
1712+
/** Response from POST /api/analytics/v1/stocks-report/wb-warehouses
1713+
* @since 3.4.0 */
1714+
export interface WbWarehousesStockResponse {
1715+
data: {
1716+
items: WbWarehouseStockItem[];
1717+
};
1718+
}

tests/unit/modules/analytics.test.ts

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Unit Tests for Analytics Module (EPIC 41 & 42)
33
*
44
* Tests cover:
5-
* - rateLimitKey wiring for all 16 active methods
5+
* - rateLimitKey wiring for all 17 active methods
66
* - Type fixes (AvailabilityFilters, TableProductItem)
77
* - Return type fixes (getDownloadsFile → ArrayBuffer)
88
*
@@ -16,6 +16,8 @@
1616
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
1717
import { AnalyticsModule } from '../../../src/modules/analytics';
1818
import type { BaseClient } from '../../../src/client/base-client';
19+
import { AuthenticationError } from '../../../src/errors/auth-error';
20+
import { RateLimitError } from '../../../src/errors/rate-limit-error';
1921
import type {
2022
AvailabilityFilters,
2123
TableProductItem,
@@ -402,7 +404,7 @@ describe('AnalyticsModule', () => {
402404
// ==========================================================================
403405

404406
describe('All module methods exist', () => {
405-
it('should have all 16 active methods', () => {
407+
it('should have all 17 active methods', () => {
406408
// CSV Export (4)
407409
expect(typeof module.getNmReportDownloads).toBe('function');
408410
expect(typeof module.createNmReportDownload).toBe('function');
@@ -426,9 +428,90 @@ describe('AnalyticsModule', () => {
426428
expect(typeof module.getSalesFunnelProducts).toBe('function');
427429
expect(typeof module.getSalesFunnelProductsHistory).toBe('function');
428430
expect(typeof module.getSalesFunnelGroupedHistory).toBe('function');
431+
432+
// v1 WB Warehouses Inventory (1)
433+
expect(typeof module.getWbWarehousesStock).toBe('function');
429434
});
430435

431436
// Note: Deprecated v2 wrapper methods (createNmReportDetail, createDetailHistory,
432437
// createGroupedHistory) have been removed in v3.0.0
433438
});
439+
440+
// ============================================================================
441+
// getWbWarehousesStock (v1 WB Warehouses Inventory)
442+
// ============================================================================
443+
444+
describe('getWbWarehousesStock()', () => {
445+
const WB_WAREHOUSES_URL = `${BASE_URL}/api/analytics/v1/stocks-report/wb-warehouses`;
446+
447+
it('should return inventory items with warehouse and region data', async () => {
448+
const mockResponse = {
449+
data: {
450+
items: [
451+
{
452+
nmId: 395996251,
453+
chrtId: 572682891,
454+
warehouseId: 130744,
455+
warehouseName: 'Краснодар',
456+
regionName: 'Южный + Северо-Кавказский',
457+
quantity: 10,
458+
inWayToClient: 3,
459+
inWayFromClient: 0,
460+
},
461+
],
462+
},
463+
};
464+
mockClient.post.mockResolvedValue(mockResponse);
465+
466+
const result = await module.getWbWarehousesStock({ nmIds: [395996251] });
467+
468+
expect(result.data.items).toHaveLength(1);
469+
expect(result.data.items[0].warehouseId).toBe(130744);
470+
expect(result.data.items[0].warehouseName).toBe('Краснодар');
471+
expect(result.data.items[0].regionName).toBe('Южный + Северо-Кавказский');
472+
expect(result.data.items[0].quantity).toBe(10);
473+
});
474+
475+
it('should call correct URL and rateLimitKey', async () => {
476+
mockClient.post.mockResolvedValue({ data: { items: [] } });
477+
478+
await module.getWbWarehousesStock({ nmIds: [123], limit: 100, offset: 50 });
479+
480+
expect(mockClient.post).toHaveBeenCalledWith(
481+
WB_WAREHOUSES_URL,
482+
{ nmIds: [123], limit: 100, offset: 50 },
483+
{ rateLimitKey: 'analytics.postStocksReportWbWarehouses' }
484+
);
485+
});
486+
487+
it('should send empty object when no params provided', async () => {
488+
mockClient.post.mockResolvedValue({ data: { items: [] } });
489+
490+
await module.getWbWarehousesStock();
491+
492+
expect(mockClient.post).toHaveBeenCalledWith(
493+
WB_WAREHOUSES_URL,
494+
{},
495+
{ rateLimitKey: 'analytics.postStocksReportWbWarehouses' }
496+
);
497+
});
498+
499+
it('should propagate AuthenticationError', async () => {
500+
mockClient.post.mockRejectedValue(new AuthenticationError('Invalid API key'));
501+
await expect(module.getWbWarehousesStock()).rejects.toThrow(AuthenticationError);
502+
});
503+
504+
it('should propagate RateLimitError', async () => {
505+
mockClient.post.mockRejectedValue(new RateLimitError('Rate limit exceeded', 5000));
506+
await expect(module.getWbWarehousesStock()).rejects.toThrow(RateLimitError);
507+
});
508+
509+
it('should handle empty items array', async () => {
510+
mockClient.post.mockResolvedValue({ data: { items: [] } });
511+
512+
const result = await module.getWbWarehousesStock({ nmIds: [999999] });
513+
514+
expect(result.data.items).toEqual([]);
515+
});
516+
});
434517
});

0 commit comments

Comments
 (0)