@lonca/trendyol 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,35 +8,38 @@
8
8
 
9
9
  Type-safe TypeScript SDK for the [Trendyol Marketplace API](https://developers.trendyol.com).
10
10
 
11
- > **`0.6.0` — Trendyol surface complete.** 14 resources, ~70 typed methods, plus a `parseWebhookEvent` helper for inbound event handling. Every endpoint a non-AutoFT non-V1 seller can hit is covered.
11
+ > [!IMPORTANT]
12
+ > **Unofficial.** This is an independent, community-maintained SDK. It is not affiliated with, endorsed by, or supported by Trendyol. "Trendyol" and related names are trademarks of their respective owners.
13
+
14
+ > **Trendyol surface complete.** 16 resources spanning catalog, orders, claims, finance, webhooks, and Export Center, plus a `parseWebhookEvent` helper for inbound event handling. Every endpoint a non-AutoFT non-V1 seller can hit is covered. See the [npm badge](https://www.npmjs.com/package/@lonca/trendyol) above for the current release.
12
15
 
13
16
  ## Coverage
14
17
 
15
- Each entry is a method on the client. Endpoints marked `★` are discovery-first wire-verified against live Trendyol STAGE — the SDK normalizes any spec/wire mismatch.
18
+ Each entry is a method on the client.
16
19
 
17
20
  | Resource | Methods |
18
21
  | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
19
- | `brands` | `list()`, `search(name)` ★ |
20
- | `categories` | `list()`, `getAttributes(id)`, `getAttributeValues(catId, attrId)` ★, `getByBarcodes(barcodes)` (AutoFT) |
22
+ | `brands` | `list()`, `search(name)` |
23
+ | `categories` | `list()`, `getAttributes(id)`, `getAttributeValues(catId, attrId)`, `getByBarcodes(barcodes)` (AutoFT) |
21
24
  | `suppliers` | `getAddresses({forceRefresh?})` (1-hour cache; rate-limited 1 req/hour on Trendyol) |
22
- | `products` read | `list({...})` ★, `listUnapproved({...})` ★, `getBase(barcode)` ★, `getBuyboxInfo(barcodes)` ★, `getBatchStatus(id)` ★ |
23
- | `products` write | `create(items)`, `updateContent(items)`, `updateVariants(items)`, `updateUnapproved(items)` ★, `updateDeliveryInfo(items)` |
25
+ | `products` read | `list({...})`, `listInventoryAndPrice({...})` (lightweight stock + price), `listUnapproved({...})`, `getBase(barcode)`, `getBuyboxInfo(barcodes)`, `getBatchStatus(id)` |
26
+ | `products` write | `create(items)`, `updateContent(items)`, `updateVariants(items)`, `updateUnapproved(items)`, `updateDeliveryInfo(items)` |
24
27
  | `products` life | `delete(barcodes)`, `archive(barcodes)`, `unarchive(barcodes)`, `unlock(barcodes)` |
25
- | `inventory` | `update(items)` ★ (stock + price, async batch) |
26
- | `orders` read | `list({...})` ★, `listStream({...})` ★ (opaque cursor for >10K), `getCargoInvoiceItems(serial, {...})` |
28
+ | `inventory` | `update(items)` (stock + price, async batch) |
29
+ | `orders` read | `list({...})`, `listStream({...})` (opaque cursor for >10K), `getCargoInvoiceItems(serial, {...})` |
27
30
  | `orders` write | `updatePackageStatus(id, {...})`, `cancelPackageItem(id, {...})`, `extendDeliveryDate(id, 1\|2\|3)`, `processAlternativeDelivery(id, {...})` |
28
31
  | `orders` split | `splitPackage`, `splitPackageByQuantity`, `multiSplitPackage`, `splitMultiPackagesByQuantity` (4 variants) |
29
32
  | `orders` cargo | `changeCargoProvider(id, code)`, `manualDeliverByPackageId(id)`, `manualDeliverByTrackingNumber(trk)`, `markDeliveredByService(id)` |
30
33
  | `orders` ops | `updateBoxInfo(id, {...})`, `updateLaborCosts(id, items)`, `updateWarehouse(id, warehouseId)` |
31
34
  | `orders` returns | `manualReturnByPackageId(id)`, `manualReturnByTrackingNumber(trk)`, `getCompensationTickets({...})` (TEX) |
32
- | `claims` | `create({...})`, `createIssue(id, {...})` (multipart), `approveLineItems(id, {...})`, `list({...})`, `getIssueReasons()` ★, `getItemAudits(itemId)` ★ |
35
+ | `claims` | `create({...})`, `createIssue(id, {...})` (multipart), `approveLineItems(id, {...})`, `list({...})`, `getIssueReasons()`, `getItemAudits(itemId)` |
33
36
  | `webhooks` | `create({...})`, `list()`, `update(id, {...})`, `delete(id)`, `activate(id)`, `deactivate(id)` |
34
- | `questions` | `get(id)`, `list({...})` ★, `answer(id, text)` |
37
+ | `questions` | `get(id)`, `list({...})`, `answer(id, text)` |
35
38
  | `invoices` | `uploadFile({shipmentPackageId, file, ...})` (multipart), `sendLink({...})`, `deleteLink({...})` |
36
- | `finance` | `getSettlements({...})`, `getOtherFinancials({...})` — both return typed `FinancialTransaction[]` ★ |
37
- | `labels` | `createCommon(trackingNumber, {format: 'ZPL', ...})`, `getCommon(trackingNumber)` ★ |
39
+ | `finance` | `getSettlements({...})`, `getOtherFinancials({...})` — both return typed `FinancialTransaction[]` |
40
+ | `labels` | `createCommon(trackingNumber, {format: 'ZPL', ...})`, `getCommon(trackingNumber)` |
38
41
  | `testOrders` | `create({...})`, `updateStatus(id, status)`, `setClaimsWaitingInAction()` — **STAGE-only utility** |
39
- | `locations` | `getCountries()` ★, `getTurkeyCities()` ★, `getTurkeyDistricts(cityCode)`, `getTurkeyNeighborhoods(cityCode, districtCode)`, `getAzerbaijanCities()`, `getAzerbaijanDistricts(...)`, `getCitiesByCountry/getDistrictsByCity(...)` |
42
+ | `locations` | `getCountries()`, `getTurkeyCities()`, `getTurkeyDistricts(cityCode)`, `getTurkeyNeighborhoods(cityCode, districtCode)`, `getAzerbaijanCities()`, `getAzerbaijanDistricts(...)`, `getCitiesByCountry/getDistrictsByCity(...)` |
40
43
  | `exportCenter` | `listProducts({...})`, `createProducts(items)`, `updatePrices(items)`, `updateStocks(items)`, `getBatchStatus(batchId)`, `listPackagesV2/V3({...})`, `getPackageItems({packageId, ...})`, `getCategoryAttributes(id)`, `getCareInstructions()`, `getCompositions()`, `getOrigins()` — **Trendyol Export Center / İhracat Merkezi** |
41
44
  | `videos` | `create({contentId, url, ...})`, `list({id?, sellerIntegrationStatus?, ...})` — product-page video upload + status |
42
45
  | **top-level** | `parseWebhookEvent(rawBody)`, `normalizeShipmentPackage(rawNode)` — for inbound webhook handlers |
@@ -238,6 +241,7 @@ await client.suppliers.getAddresses({ forceRefresh: true });
238
241
 
239
242
  // products — read
240
243
  await client.products.list({ barcode: 'BC1' });
244
+ await client.products.listInventoryAndPrice({ status: 'onSale', limit: 100 }); // stock + price only
241
245
  await client.products.listUnapproved({ limit: 50 });
242
246
  await client.products.getBase('BC1');
243
247
  await client.products.getBuyboxInfo(['BC1', 'BC2']); // max 10 per call
@@ -379,25 +383,6 @@ const status = await client.products.getBatchStatus(batchRequestId);
379
383
 
380
384
  **Important:** Trendyol's overall batch `status` can lag at `PROCESSING` even after each `items[].status` has settled. Trust the per-item status, or re-read the affected products via `list({ barcode })` / `getBase(barcode)` to verify the change landed. Batch results are retained for **4 hours** on Trendyol's side.
381
385
 
382
- ## Discovery-first wire fixes
383
-
384
- `@lonca/trendyol` was built by hitting the live Trendyol STAGE for every endpoint before writing types. Places where the official OpenAPI spec disagrees with the live wire are normalized automatically:
385
-
386
- - `categories.getAttributeValues`: spec says `attributeValueName`, wire returns `attributeValue` → SDK normalizes to `{ id, name }`
387
- - `products.listUnapproved`: spec says `media: [{url}]`, wire returns `images: [{url}]` → SDK exposes `images: string[]`
388
- - `products.getBuyboxInfo`: wire returns extra `secondBuyboxPrice` / `thirdBuyboxPrice` fields beyond spec → both surfaced
389
- - `products.updateUnapproved`: spec marks only `barcode` required, but live endpoint returns HTTP 500 (`TypeError`) when too many optional fields are omitted → documented in JSDoc
390
- - `brands.search`: docs claim case-sensitive exact match, live is substring + case-insensitive → documented in JSDoc
391
- - `getBatchRequestResult`: returns `PROCESSING + empty items` for unknown batch IDs (not 404)
392
- - `orders.listStream` returns package ID as `id`, regular `orders.list` returns it as `shipmentPackageId` → normalizer accepts both
393
- - `orders.updateLaborCosts`: body is a **raw array** (no `{ items: [...] }` envelope) — only endpoint in the surface that does this
394
- - `getCompensationTickets`: spec says `{ data: { items: [] } }`, but SDK also accepts `{ data: [] }` and `{ content: [] }` defensively
395
- - `labels.getCommon`: response is `{ data: [{ label, format }] }` → SDK surfaces `labels[]` for ergonomic access
396
- - `finance.*`: both `getSettlements` and `getOtherFinancials` share the same `FinancialTransaction` wire schema → unified typed surface
397
- - `webhooks.list`: SDK accepts 3 envelope shapes (`[]` raw, `{ webhooks: [] }`, `{ content: [] }`) and 3 active-flag spellings (`active`, `isActive`, `status: 'ACTIVE'`)
398
-
399
- Each fix is pinned by a regression mock test using the exact STAGE shape.
400
-
401
386
  ## Authentication
402
387
 
403
388
  Trendyol uses HTTP Basic Auth. Get your `sellerId`, `apiKey`, and `apiSecret` from the [Trendyol Partner Panel → Account Info → Integration Information](https://partner.trendyol.com/account/info?tab=integrationInformation) (master-user only).
@@ -413,7 +398,7 @@ Trendyol uses HTTP Basic Auth. Get your `sellerId`, `apiKey`, and `apiSecret` fr
413
398
 
414
399
  ## Built-in robustness
415
400
 
416
- - **Retry with exponential backoff** on 429 (respects `Retry-After`) and 5xx
401
+ - **Idempotency-aware retry with exponential backoff** — reads (`GET`) retry on 429 (honoring `Retry-After`), 5xx, and network/timeout errors. Writes (`POST`/`PUT`/`DELETE`) retry **only** on 429, which the server rejects before processing; ambiguous 5xx/network/timeout failures on a write are not replayed, so a transient error can't duplicate an order action or stock/price push. A `Retry-After: 0` no longer collapses backoff to an immediate retry.
417
402
  - **Per-endpoint rate limiting** (token bucket) sized to Trendyol's documented limits — see defaults below; override per resource
418
403
  - **Per-request correlation ID** — every call gets a UUID surfaced in log messages and the `x-correlationid` header for Trendyol-side log tracing
419
404
  - **Structured errors** via `@lonca/core` (`AuthError`, `RateLimitError`, `NotFoundError`, `ServerError`, `ValidationError`, `NetworkError`, `TimeoutError`)
@@ -1,4 +1,4 @@
1
- import { Logger, TokenBucketRateLimiter, CursorPaginationParams, CursorPage, OffsetPaginationParams } from '@lonca/core';
1
+ import { Logger, BaseRequestOptions, TokenBucketRateLimiter, CursorPaginationParams, CursorPage, OffsetPaginationParams } from '@lonca/core';
2
2
 
3
3
  declare const BASE_URLS: {
4
4
  readonly prod: "https://apigw.trendyol.com";
@@ -18,34 +18,22 @@ interface TransportConfig {
18
18
  /** Override the underlying `fetch` (tests inject a mock). */
19
19
  fetch?: typeof fetch;
20
20
  }
21
- interface RequestOptions {
21
+ interface RequestOptions extends BaseRequestOptions {
22
22
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
23
23
  /** Path beginning with `/` (e.g., `/sapigw/brands`). */
24
24
  path: string;
25
25
  query?: Record<string, string | number | boolean | undefined>;
26
- body?: unknown;
27
- signal?: AbortSignal;
28
- /**
29
- * Extra per-request headers merged over the default header set (caller
30
- * headers win). Used for endpoint-specific headers like `storeFrontCode`.
31
- */
32
- headers?: Record<string, string>;
33
- /** Per-endpoint rate limiter; acquire one token before each attempt. */
34
- rateLimiter?: TokenBucketRateLimiter;
35
26
  }
36
27
  declare class TrendyolTransport {
37
28
  private readonly config;
38
29
  private readonly baseUrl;
39
- private readonly logger;
40
- private readonly timeoutMs;
41
- private readonly fetchImpl;
30
+ private readonly requester;
42
31
  constructor(config: TransportConfig);
43
32
  /** Seller ID this transport is configured with. Resources read it for path-building. */
44
33
  get sellerId(): number;
45
34
  request<T>(opts: RequestOptions): Promise<T>;
46
35
  private buildUrl;
47
36
  private buildHeaders;
48
- private composeSignal;
49
37
  }
50
38
 
51
39
  /**
@@ -89,7 +77,7 @@ declare class BrandsResource {
89
77
  * `createProducts` and don't want to page through the full `list()`
90
78
  * (1000 brands per page).
91
79
  *
92
- * **Discovery-first wire fact (verified STAGE 2026-05-25):** Trendyol's
80
+ * **Wire fact (verified STAGE 2026-05-25):** Trendyol's
93
81
  * doc claims this is a case-sensitive *exact* match, but live behaviour
94
82
  * is **substring + case-insensitive** — `search('Trendyol')` returns
95
83
  * 17 hits including `TRENDYOLMILLA`, `trendyol vavist`, `Trendyol Üyelik`.
@@ -1,4 +1,4 @@
1
- import { Logger, TokenBucketRateLimiter, CursorPaginationParams, CursorPage, OffsetPaginationParams } from '@lonca/core';
1
+ import { Logger, BaseRequestOptions, TokenBucketRateLimiter, CursorPaginationParams, CursorPage, OffsetPaginationParams } from '@lonca/core';
2
2
 
3
3
  declare const BASE_URLS: {
4
4
  readonly prod: "https://apigw.trendyol.com";
@@ -18,34 +18,22 @@ interface TransportConfig {
18
18
  /** Override the underlying `fetch` (tests inject a mock). */
19
19
  fetch?: typeof fetch;
20
20
  }
21
- interface RequestOptions {
21
+ interface RequestOptions extends BaseRequestOptions {
22
22
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
23
23
  /** Path beginning with `/` (e.g., `/sapigw/brands`). */
24
24
  path: string;
25
25
  query?: Record<string, string | number | boolean | undefined>;
26
- body?: unknown;
27
- signal?: AbortSignal;
28
- /**
29
- * Extra per-request headers merged over the default header set (caller
30
- * headers win). Used for endpoint-specific headers like `storeFrontCode`.
31
- */
32
- headers?: Record<string, string>;
33
- /** Per-endpoint rate limiter; acquire one token before each attempt. */
34
- rateLimiter?: TokenBucketRateLimiter;
35
26
  }
36
27
  declare class TrendyolTransport {
37
28
  private readonly config;
38
29
  private readonly baseUrl;
39
- private readonly logger;
40
- private readonly timeoutMs;
41
- private readonly fetchImpl;
30
+ private readonly requester;
42
31
  constructor(config: TransportConfig);
43
32
  /** Seller ID this transport is configured with. Resources read it for path-building. */
44
33
  get sellerId(): number;
45
34
  request<T>(opts: RequestOptions): Promise<T>;
46
35
  private buildUrl;
47
36
  private buildHeaders;
48
- private composeSignal;
49
37
  }
50
38
 
51
39
  /**
@@ -89,7 +77,7 @@ declare class BrandsResource {
89
77
  * `createProducts` and don't want to page through the full `list()`
90
78
  * (1000 brands per page).
91
79
  *
92
- * **Discovery-first wire fact (verified STAGE 2026-05-25):** Trendyol's
80
+ * **Wire fact (verified STAGE 2026-05-25):** Trendyol's
93
81
  * doc claims this is a case-sensitive *exact* match, but live behaviour
94
82
  * is **substring + case-insensitive** — `search('Trendyol')` returns
95
83
  * 17 hits including `TRENDYOLMILLA`, `trendyol vavist`, `Trendyol Üyelik`.
package/dist/index.cjs CHANGED
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  var core = require('@lonca/core');
4
- var crypto = require('crypto');
5
4
 
6
5
  // src/resources/brands.ts
7
6
  var DEFAULT_PAGE_SIZE = 1e3;
@@ -41,7 +40,7 @@ var BrandsResource = class {
41
40
  * `createProducts` and don't want to page through the full `list()`
42
41
  * (1000 brands per page).
43
42
  *
44
- * **Discovery-first wire fact (verified STAGE 2026-05-25):** Trendyol's
43
+ * **Wire fact (verified STAGE 2026-05-25):** Trendyol's
45
44
  * doc claims this is a case-sensitive *exact* match, but live behaviour
46
45
  * is **substring + case-insensitive** — `search('Trendyol')` returns
47
46
  * 17 hits including `TRENDYOLMILLA`, `trendyol vavist`, `Trendyol Üyelik`.
@@ -1262,7 +1261,8 @@ var OrdersResource = class {
1262
1261
  const items = (data.content ?? []).map(normalizePackage2);
1263
1262
  const totalPages = typeof data.totalPages === "number" ? data.totalPages : 0;
1264
1263
  const result = { items };
1265
- if (page + 1 < totalPages) {
1264
+ const nextPageWithinCap = (page + 2) * size <= MAX_OFFSET_RECORDS;
1265
+ if (page + 1 < totalPages && nextPageWithinCap) {
1266
1266
  result.nextCursor = String(page + 1);
1267
1267
  }
1268
1268
  return result;
@@ -2784,14 +2784,6 @@ function extractErrorsArray(body) {
2784
2784
  }
2785
2785
  return void 0;
2786
2786
  }
2787
- function parseRetryAfter(header) {
2788
- if (!header) return void 0;
2789
- const seconds = Number(header);
2790
- if (!Number.isNaN(seconds)) return Math.max(0, seconds * 1e3);
2791
- const epoch = Date.parse(header);
2792
- if (!Number.isNaN(epoch)) return Math.max(0, epoch - Date.now());
2793
- return void 0;
2794
- }
2795
2787
 
2796
2788
  // src/transport.ts
2797
2789
  var BASE_URLS = {
@@ -2802,95 +2794,26 @@ var TrendyolTransport = class {
2802
2794
  constructor(config) {
2803
2795
  this.config = config;
2804
2796
  this.baseUrl = BASE_URLS[config.env];
2805
- this.logger = config.logger ?? core.noopLogger;
2806
- this.timeoutMs = config.timeoutMs ?? 3e4;
2807
- this.fetchImpl = config.fetch ?? fetch;
2797
+ this.requester = core.createRequester({
2798
+ fetch: config.fetch ?? fetch,
2799
+ logger: config.logger,
2800
+ timeoutMs: config.timeoutMs ?? 3e4,
2801
+ label: "Trendyol",
2802
+ logPrefix: "trendyol",
2803
+ buildUrl: (opts) => this.buildUrl(opts.path, opts.query),
2804
+ buildHeaders: (correlationId) => this.buildHeaders(correlationId),
2805
+ mapHttpError
2806
+ });
2808
2807
  }
2809
2808
  config;
2810
2809
  baseUrl;
2811
- logger;
2812
- timeoutMs;
2813
- fetchImpl;
2810
+ requester;
2814
2811
  /** Seller ID this transport is configured with. Resources read it for path-building. */
2815
2812
  get sellerId() {
2816
2813
  return this.config.sellerId;
2817
2814
  }
2818
- async request(opts) {
2819
- return core.retry(
2820
- async (attempt) => {
2821
- if (opts.rateLimiter) await opts.rateLimiter.acquire(opts.signal);
2822
- const url = this.buildUrl(opts.path, opts.query);
2823
- const correlationId = crypto.randomUUID();
2824
- const headers = { ...this.buildHeaders(correlationId), ...opts.headers };
2825
- const init = {
2826
- method: opts.method,
2827
- headers,
2828
- signal: this.composeSignal(opts.signal)
2829
- };
2830
- if (opts.body !== void 0 && opts.method !== "GET") {
2831
- if (opts.body instanceof FormData) {
2832
- init.body = opts.body;
2833
- delete headers["Content-Type"];
2834
- } else {
2835
- init.body = JSON.stringify(opts.body);
2836
- }
2837
- }
2838
- this.logger.debug("trendyol.request", {
2839
- method: opts.method,
2840
- url,
2841
- correlationId,
2842
- attempt
2843
- });
2844
- let response;
2845
- try {
2846
- response = await this.fetchImpl(url, init);
2847
- } catch (err) {
2848
- if (err instanceof Error && err.name === "AbortError") {
2849
- throw new core.TimeoutError({
2850
- message: `Trendyol request timed out after ${this.timeoutMs}ms`,
2851
- cause: err
2852
- });
2853
- }
2854
- throw new core.NetworkError({
2855
- message: "Trendyol network failure",
2856
- cause: err
2857
- });
2858
- }
2859
- if (!response.ok) {
2860
- const body = await safeJson(response);
2861
- const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
2862
- const error = mapHttpError(response.status, body, retryAfterMs);
2863
- this.logger.warn("trendyol.error", {
2864
- method: opts.method,
2865
- url,
2866
- correlationId,
2867
- status: response.status,
2868
- code: error.code,
2869
- retryable: error.retryable
2870
- });
2871
- throw error;
2872
- }
2873
- this.logger.debug("trendyol.response", {
2874
- correlationId,
2875
- status: response.status
2876
- });
2877
- if (response.status === 204) return void 0;
2878
- return await safeJson(response);
2879
- },
2880
- {
2881
- signal: opts.signal,
2882
- onRetry: (err, attempt, delay2) => {
2883
- if (err instanceof core.LoncaError) {
2884
- this.logger.warn("trendyol.retry", {
2885
- attempt,
2886
- delayMs: delay2,
2887
- code: err.code,
2888
- status: err.status
2889
- });
2890
- }
2891
- }
2892
- }
2893
- );
2815
+ request(opts) {
2816
+ return this.requester(opts);
2894
2817
  }
2895
2818
  buildUrl(path, query) {
2896
2819
  const url = new URL(path, this.baseUrl);
@@ -2913,21 +2836,7 @@ var TrendyolTransport = class {
2913
2836
  Accept: "application/json"
2914
2837
  };
2915
2838
  }
2916
- composeSignal(external) {
2917
- const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
2918
- if (!external) return timeoutSignal;
2919
- return AbortSignal.any([external, timeoutSignal]);
2920
- }
2921
2839
  };
2922
- async function safeJson(response) {
2923
- const text = await response.text();
2924
- if (!text) return void 0;
2925
- try {
2926
- return JSON.parse(text);
2927
- } catch {
2928
- return text;
2929
- }
2930
- }
2931
2840
 
2932
2841
  // src/client.ts
2933
2842
  function createTrendyolClient(opts) {