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