@lonca/trendyol 0.9.0 → 0.11.0

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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { TokenBucketRateLimiter, ValidationError, noopLogger, retry, TimeoutError, NetworkError, LoncaError, ServerError, RateLimitError, NotFoundError, AuthError } from '@lonca/core';
1
+ import { createStatusNormalizer, TokenBucketRateLimiter, ValidationError, TimeoutError, ServerError, noopLogger, retry, NetworkError, LoncaError, RateLimitError, NotFoundError, AuthError } from '@lonca/core';
2
2
  import { randomUUID } from 'crypto';
3
3
 
4
4
  // src/resources/brands.ts
@@ -737,11 +737,37 @@ var FinanceResource = class {
737
737
  }
738
738
  };
739
739
  var MAX_ITEMS_PER_REQUEST = 1e3;
740
+ async function pollBatchStatus(getStatus, batchRequestId, opts = {}) {
741
+ const pollIntervalMs = opts.pollIntervalMs ?? 2e3;
742
+ const timeoutMs = opts.timeoutMs ?? 12e4;
743
+ const deadline = Date.now() + timeoutMs;
744
+ for (; ; ) {
745
+ opts.signal?.throwIfAborted();
746
+ const result = await getStatus(batchRequestId);
747
+ if (result.status === "COMPLETED" || result.status === "FAILED") {
748
+ return result;
749
+ }
750
+ if (Date.now() >= deadline) {
751
+ throw new TimeoutError({
752
+ message: `Trendyol batch ${batchRequestId} did not settle within ${timeoutMs}ms (last status: ${result.status})`,
753
+ data: { batchRequestId, lastStatus: result.status, lastResult: result }
754
+ });
755
+ }
756
+ await delay(pollIntervalMs, opts.signal);
757
+ }
758
+ }
740
759
  var InventoryResource = class {
741
- constructor(transport) {
760
+ /**
761
+ * @param transport Trendyol transport.
762
+ * @param getBatchStatus Batch-status poller (wired by `createTrendyolClient`
763
+ * to `products.getBatchStatus`). Required only for `updateAndWait`.
764
+ */
765
+ constructor(transport, getBatchStatus) {
742
766
  this.transport = transport;
767
+ this.getBatchStatus = getBatchStatus;
743
768
  }
744
769
  transport;
770
+ getBatchStatus;
745
771
  /**
746
772
  * Update price and/or stock for one or more SKUs (by barcode).
747
773
  *
@@ -755,6 +781,9 @@ var InventoryResource = class {
755
781
  * ```
756
782
  *
757
783
  * @throws {ValidationError} when `items` is empty or longer than 1000.
784
+ * @throws {ServerError} when Trendyol accepts the request but returns no
785
+ * `batchRequestId` (an unpollable response — surfaced loudly instead of
786
+ * handing back an empty id).
758
787
  */
759
788
  async update(items) {
760
789
  if (items.length === 0) {
@@ -770,9 +799,70 @@ var InventoryResource = class {
770
799
  path: `/integration/inventory/sellers/${this.transport.sellerId}/products/price-and-inventory`,
771
800
  body: { items }
772
801
  });
773
- return { batchRequestId: data.batchRequestId ?? "" };
802
+ if (!data.batchRequestId) {
803
+ throw new ServerError({
804
+ message: "Trendyol updatePriceAndInventory returned no batchRequestId",
805
+ data: { response: data }
806
+ });
807
+ }
808
+ return { batchRequestId: data.batchRequestId };
809
+ }
810
+ /**
811
+ * Submit price/stock updates and wait for them to settle.
812
+ *
813
+ * Splits `items` into chunks of ≤1000, submits each via {@link update}, and
814
+ * polls each `batchRequestId` to a terminal state. Returns one
815
+ * `BatchRequestResult` per chunk (read `failedItemCount` / `items[]` for
816
+ * per-barcode outcomes). Compose `@lonca/core`'s `retry` around this for
817
+ * transient-error resilience.
818
+ *
819
+ * @throws {ValidationError} when `items` is empty.
820
+ * @throws {TimeoutError} when any chunk does not settle within `timeoutMs`;
821
+ * `error.data.batchRequestId` identifies the stuck chunk.
822
+ */
823
+ async updateAndWait(items, opts = {}) {
824
+ if (items.length === 0) {
825
+ throw new ValidationError({ message: "updateAndWait: items must not be empty" });
826
+ }
827
+ const getStatus = this.getBatchStatus;
828
+ if (!getStatus) {
829
+ throw new Error(
830
+ "updateAndWait requires a batch-status poller; obtain the client via createTrendyolClient()"
831
+ );
832
+ }
833
+ const results = [];
834
+ for (const batch of chunk(items, MAX_ITEMS_PER_REQUEST)) {
835
+ const { batchRequestId } = await this.update(batch);
836
+ results.push(await pollBatchStatus(getStatus, batchRequestId, opts));
837
+ }
838
+ return results;
774
839
  }
775
840
  };
841
+ function chunk(items, size) {
842
+ if (items.length <= size) return [items];
843
+ const chunks = [];
844
+ for (let i = 0; i < items.length; i += size) {
845
+ chunks.push(items.slice(i, i + size));
846
+ }
847
+ return chunks;
848
+ }
849
+ function delay(ms, signal) {
850
+ return new Promise((resolve, reject) => {
851
+ if (signal?.aborted) {
852
+ reject(signal.reason ?? new Error("Aborted"));
853
+ return;
854
+ }
855
+ const onAbort = () => {
856
+ clearTimeout(timer);
857
+ reject(signal?.reason ?? new Error("Aborted"));
858
+ };
859
+ const timer = setTimeout(() => {
860
+ signal?.removeEventListener("abort", onAbort);
861
+ resolve();
862
+ }, ms);
863
+ signal?.addEventListener("abort", onAbort, { once: true });
864
+ });
865
+ }
776
866
  var InvoicesResource = class {
777
867
  constructor(transport, limiter) {
778
868
  this.transport = transport;
@@ -974,6 +1064,7 @@ var LocationsResource = class {
974
1064
  };
975
1065
  var MAX_PAGE_SIZE = 200;
976
1066
  var DEFAULT_PAGE_SIZE2 = 50;
1067
+ var MAX_OFFSET_RECORDS = 1e4;
977
1068
  function toIso3(epochMs) {
978
1069
  if (typeof epochMs !== "number" || !Number.isFinite(epochMs) || epochMs === 0) {
979
1070
  return void 0;
@@ -1150,6 +1241,11 @@ var OrdersResource = class {
1150
1241
  async list(params = {}) {
1151
1242
  const size = Math.min(params.limit ?? DEFAULT_PAGE_SIZE2, MAX_PAGE_SIZE);
1152
1243
  const page = params.cursor ? Number.parseInt(params.cursor, 10) : 0;
1244
+ if ((page + 1) * size > MAX_OFFSET_RECORDS) {
1245
+ throw new ValidationError({
1246
+ message: `getShipmentPackages can reach at most ${MAX_OFFSET_RECORDS} records (page ${page} \xD7 size ${size} exceeds it); use listStream() for full scans`
1247
+ });
1248
+ }
1153
1249
  const query = { page, size };
1154
1250
  if (params.status) query.status = params.status;
1155
1251
  if (params.orderNumber) query.orderNumber = params.orderNumber;
@@ -1603,6 +1699,7 @@ var OrdersResource = class {
1603
1699
  };
1604
1700
  var DEFAULT_PAGE_SIZE3 = 50;
1605
1701
  var MAX_PAGE_SIZE2 = 1e3;
1702
+ var MAX_INVENTORY_PAGE_SIZE = 100;
1606
1703
  var MAX_BUYBOX_BARCODES = 10;
1607
1704
  function normalizeRef(ref) {
1608
1705
  return {
@@ -1668,6 +1765,30 @@ function normalizeProduct(node) {
1668
1765
  if (node.lastModifiedBy !== void 0) out.lastModifiedBy = node.lastModifiedBy;
1669
1766
  return out;
1670
1767
  }
1768
+ function normalizeStockPriceVariant(node) {
1769
+ const out = {
1770
+ variantId: node.variantId !== void 0 ? String(node.variantId) : "",
1771
+ barcode: node.barcode ?? "",
1772
+ raw: node
1773
+ };
1774
+ if (typeof node.salePrice === "number") out.salePrice = node.salePrice;
1775
+ if (typeof node.listPrice === "number") out.listPrice = node.listPrice;
1776
+ if (typeof node.quantity === "number") out.quantity = node.quantity;
1777
+ if (node.stockCode !== void 0) out.stockCode = node.stockCode;
1778
+ if (node.stockLastModifiedDate !== void 0 && node.stockLastModifiedDate !== null) {
1779
+ const iso = toIso4(node.stockLastModifiedDate);
1780
+ if (iso) out.stockLastModifiedAt = iso;
1781
+ }
1782
+ return out;
1783
+ }
1784
+ function normalizeStockPrice(node) {
1785
+ return {
1786
+ contentId: node.contentId !== void 0 ? String(node.contentId) : "",
1787
+ productMainId: node.productMainId ?? "",
1788
+ variants: (node.variants ?? []).map(normalizeStockPriceVariant),
1789
+ raw: node
1790
+ };
1791
+ }
1671
1792
  function normalizeUnapprovedRejectReason(r) {
1672
1793
  const out = {};
1673
1794
  if (r.rejectReason !== void 0) out.rejectReason = r.rejectReason;
@@ -1858,6 +1979,63 @@ var ProductsResource = class {
1858
1979
  }
1859
1980
  return page;
1860
1981
  }
1982
+ /**
1983
+ * List **stock and price** for approved products — Trendyol's lightweight
1984
+ * `inventory-and-price` filter. A slim alternative to {@link list} when you
1985
+ * only need pricing + stock: the response carries `contentId`,
1986
+ * `productMainId`, and a `variants[]` array with `barcode`, `salePrice`,
1987
+ * `listPrice`, `quantity`, `stockCode`, and `stockLastModifiedAt` — nothing
1988
+ * else.
1989
+ *
1990
+ * Filter by `barcode`, `contentId`, `stockCode`, `productMainId`, or listing
1991
+ * `status`. Sort with `orderByDirection` (`SellerCreatedDate`). `size` caps
1992
+ * at **100** here (tighter than `list`'s 1000).
1993
+ *
1994
+ * Pagination follows the same convention as {@link list}: pass our opaque
1995
+ * `cursor` from the previous response and we forward it as `nextPageToken`
1996
+ * (required once the dataset exceeds 10,000 items).
1997
+ *
1998
+ * @example
1999
+ * ```ts
2000
+ * import { paginate } from '@lonca/core';
2001
+ * for await (const p of paginate((q) =>
2002
+ * client.products.listInventoryAndPrice({ ...q, status: 'onSale' }),
2003
+ * )) {
2004
+ * for (const v of p.variants) {
2005
+ * console.log(v.barcode, v.quantity, v.salePrice);
2006
+ * }
2007
+ * }
2008
+ * ```
2009
+ */
2010
+ async listInventoryAndPrice(params = {}) {
2011
+ const size = Math.min(params.limit ?? DEFAULT_PAGE_SIZE3, MAX_INVENTORY_PAGE_SIZE);
2012
+ const query = { size };
2013
+ if (params.cursor) {
2014
+ query.nextPageToken = params.cursor;
2015
+ } else {
2016
+ query.page = 0;
2017
+ }
2018
+ if (params.barcode) query.barcode = params.barcode;
2019
+ if (params.contentId) query.contentId = params.contentId;
2020
+ if (params.stockCode) query.stockCode = params.stockCode;
2021
+ if (params.productMainId) query.productMainId = params.productMainId;
2022
+ if (params.status) query.status = params.status;
2023
+ if (params.orderByDirection) query.orderByDirection = params.orderByDirection;
2024
+ const headers = params.storeFrontCode ? { storeFrontCode: params.storeFrontCode } : void 0;
2025
+ const data = await this.transport.request({
2026
+ method: "GET",
2027
+ path: `/integration/product/sellers/${this.transport.sellerId}/products/approved/inventory-and-price`,
2028
+ query,
2029
+ headers,
2030
+ rateLimiter: this.filterLimiter
2031
+ });
2032
+ const items = (data.content ?? []).map(normalizeStockPrice);
2033
+ const page = { items };
2034
+ if (data.nextPageToken) {
2035
+ page.nextCursor = data.nextPageToken;
2036
+ }
2037
+ return page;
2038
+ }
1861
2039
  /**
1862
2040
  * Poll a batch request returned by an async write (e.g. `createProducts`,
1863
2041
  * `updatePriceAndInventory`).
@@ -2506,6 +2684,16 @@ var WebhooksResource = class {
2506
2684
  }
2507
2685
  };
2508
2686
 
2687
+ // src/capabilities.ts
2688
+ var trendyolCapabilities = {
2689
+ /** Trendyol has no time-bounded / scheduled pricing (`pricings[]`). */
2690
+ scheduledPricing: false,
2691
+ /** `inventory.update` accepts stock-only items (`quantity` with no price). */
2692
+ stockOnlyBatch: true,
2693
+ /** Products expose `updatedAt`, so last-write-wins guards are supported. */
2694
+ listingUpdatedAt: true
2695
+ };
2696
+
2509
2697
  // src/auth.ts
2510
2698
  function buildAuthHeader(apiKey, apiSecret) {
2511
2699
  const token = Buffer.from(`${apiKey}:${apiSecret}`, "utf8").toString("base64");
@@ -2516,27 +2704,31 @@ function buildUserAgent(sellerId, integratorName) {
2516
2704
  }
2517
2705
  function mapHttpError(status, body, retryAfterMs) {
2518
2706
  const data = { body };
2707
+ const issues = normalizeErrorIssues(body);
2519
2708
  switch (status) {
2520
2709
  case 401:
2521
2710
  return new AuthError({
2522
2711
  message: "Trendyol authentication failed (check apiKey / apiSecret)",
2523
2712
  status,
2524
- data
2713
+ data,
2714
+ issues
2525
2715
  });
2526
2716
  case 403:
2527
2717
  return new ValidationError({
2528
2718
  message: "Trendyol forbidden (check User-Agent header or endpoint path)",
2529
2719
  status,
2530
- data
2720
+ data,
2721
+ issues
2531
2722
  });
2532
2723
  case 404:
2533
- return new NotFoundError({ message: "Trendyol resource not found", status, data });
2724
+ return new NotFoundError({ message: "Trendyol resource not found", status, data, issues });
2534
2725
  case 429:
2535
2726
  return new RateLimitError({
2536
2727
  message: "Trendyol rate limit exceeded",
2537
2728
  status,
2538
2729
  retryAfterMs,
2539
- data
2730
+ data,
2731
+ issues
2540
2732
  });
2541
2733
  case 500:
2542
2734
  case 502:
@@ -2545,7 +2737,8 @@ function mapHttpError(status, body, retryAfterMs) {
2545
2737
  return new ServerError({
2546
2738
  message: `Trendyol server error (${status})`,
2547
2739
  status,
2548
- data
2740
+ data,
2741
+ issues
2549
2742
  });
2550
2743
  default:
2551
2744
  return new LoncaError({
@@ -2553,10 +2746,42 @@ function mapHttpError(status, body, retryAfterMs) {
2553
2746
  message: `Trendyol unexpected response (${status})`,
2554
2747
  status,
2555
2748
  data,
2749
+ issues,
2556
2750
  retryable: false
2557
2751
  });
2558
2752
  }
2559
2753
  }
2754
+ function normalizeErrorIssues(body) {
2755
+ const errors = extractErrorsArray(body);
2756
+ if (!errors) return [];
2757
+ const issues = [];
2758
+ for (const entry of errors) {
2759
+ if (typeof entry === "string") {
2760
+ issues.push({ message: entry });
2761
+ continue;
2762
+ }
2763
+ if (entry && typeof entry === "object") {
2764
+ const e = entry;
2765
+ if (typeof e.message !== "string") continue;
2766
+ const issue = { message: e.message };
2767
+ if (typeof e.field === "string") issue.field = e.field;
2768
+ if (typeof e.code === "string") issue.code = e.code;
2769
+ issues.push(issue);
2770
+ }
2771
+ }
2772
+ return issues;
2773
+ }
2774
+ function extractErrorsArray(body) {
2775
+ if (!body || typeof body !== "object") return void 0;
2776
+ const b = body;
2777
+ if (Array.isArray(b.errors)) return b.errors;
2778
+ const nested = b.body;
2779
+ if (nested && typeof nested === "object") {
2780
+ const n2 = nested;
2781
+ if (Array.isArray(n2.errors)) return n2.errors;
2782
+ }
2783
+ return void 0;
2784
+ }
2560
2785
  function parseRetryAfter(header) {
2561
2786
  if (!header) return void 0;
2562
2787
  const seconds = Number(header);
@@ -2594,7 +2819,7 @@ var TrendyolTransport = class {
2594
2819
  if (opts.rateLimiter) await opts.rateLimiter.acquire(opts.signal);
2595
2820
  const url = this.buildUrl(opts.path, opts.query);
2596
2821
  const correlationId = randomUUID();
2597
- const headers = this.buildHeaders(correlationId);
2822
+ const headers = { ...this.buildHeaders(correlationId), ...opts.headers };
2598
2823
  const init = {
2599
2824
  method: opts.method,
2600
2825
  headers,
@@ -2652,11 +2877,11 @@ var TrendyolTransport = class {
2652
2877
  },
2653
2878
  {
2654
2879
  signal: opts.signal,
2655
- onRetry: (err, attempt, delay) => {
2880
+ onRetry: (err, attempt, delay2) => {
2656
2881
  if (err instanceof LoncaError) {
2657
2882
  this.logger.warn("trendyol.retry", {
2658
2883
  attempt,
2659
- delayMs: delay,
2884
+ delayMs: delay2,
2660
2885
  code: err.code,
2661
2886
  status: err.status
2662
2887
  });
@@ -2714,12 +2939,16 @@ function createTrendyolClient(opts) {
2714
2939
  logger: opts.logger,
2715
2940
  timeoutMs: opts.timeoutMs
2716
2941
  });
2942
+ return buildClient(transport);
2943
+ }
2944
+ function buildClient(transport) {
2945
+ const products = new ProductsResource(transport);
2717
2946
  return {
2718
2947
  brands: new BrandsResource(transport),
2719
2948
  categories: new CategoriesResource(transport),
2720
2949
  suppliers: new SuppliersResource(transport),
2721
- products: new ProductsResource(transport),
2722
- inventory: new InventoryResource(transport),
2950
+ products,
2951
+ inventory: new InventoryResource(transport, (id) => products.getBatchStatus(id)),
2723
2952
  orders: new OrdersResource(transport),
2724
2953
  claims: new ClaimsResource(transport),
2725
2954
  webhooks: new WebhooksResource(transport),
@@ -2730,7 +2959,8 @@ function createTrendyolClient(opts) {
2730
2959
  testOrders: new TestOrdersResource(transport),
2731
2960
  locations: new LocationsResource(transport),
2732
2961
  exportCenter: new ExportCenterResource(transport),
2733
- videos: new VideosResource(transport)
2962
+ videos: new VideosResource(transport),
2963
+ capabilities: trendyolCapabilities
2734
2964
  };
2735
2965
  }
2736
2966
  function parseWebhookEvent(rawBody) {
@@ -2767,7 +2997,29 @@ function parseWebhookEvent(rawBody) {
2767
2997
  raw: body
2768
2998
  };
2769
2999
  }
3000
+ var statusMap = {
3001
+ Created: "created",
3002
+ Awaiting: "created",
3003
+ // new order awaiting seller acceptance
3004
+ Verified: "created",
3005
+ // accepted/verified, before picking
3006
+ Picking: "picking",
3007
+ UnPacked: "picking",
3008
+ // back in fulfillment after being unpacked
3009
+ Invoiced: "invoiced",
3010
+ Shipped: "shipped",
3011
+ AtCollectionPoint: "shipped",
3012
+ // in transit, awaiting pickup
3013
+ UnDelivered: "shipped",
3014
+ // delivery attempt failed; still in carrier's hands
3015
+ Delivered: "delivered",
3016
+ Cancelled: "cancelled",
3017
+ UnSupplied: "cancelled",
3018
+ // seller couldn't supply → effectively cancelled
3019
+ Returned: "returned"
3020
+ };
3021
+ var normalizeStatus = createStatusNormalizer(statusMap);
2770
3022
 
2771
- export { BrandsResource, CategoriesResource, ClaimsResource, ExportCenterResource, FinanceResource, InventoryResource, InvoicesResource, LabelsResource, LocationsResource, OrdersResource, ProductsResource, QuestionsResource, SuppliersResource, TestOrdersResource, VideosResource, WebhooksResource, createTrendyolClient, normalizeShipmentPackage, parseWebhookEvent };
3023
+ export { BrandsResource, CategoriesResource, ClaimsResource, ExportCenterResource, FinanceResource, InventoryResource, InvoicesResource, LabelsResource, LocationsResource, OrdersResource, ProductsResource, QuestionsResource, SuppliersResource, TestOrdersResource, VideosResource, WebhooksResource, createTrendyolClient, normalizeShipmentPackage, normalizeStatus, parseWebhookEvent, pollBatchStatus, statusMap, trendyolCapabilities };
2772
3024
  //# sourceMappingURL=index.js.map
2773
3025
  //# sourceMappingURL=index.js.map