@lonca/trendyol 0.9.0 → 0.10.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;
@@ -2506,6 +2596,16 @@ var WebhooksResource = class {
2506
2596
  }
2507
2597
  };
2508
2598
 
2599
+ // src/capabilities.ts
2600
+ var trendyolCapabilities = {
2601
+ /** Trendyol has no time-bounded / scheduled pricing (`pricings[]`). */
2602
+ scheduledPricing: false,
2603
+ /** `inventory.update` accepts stock-only items (`quantity` with no price). */
2604
+ stockOnlyBatch: true,
2605
+ /** Products expose `updatedAt`, so last-write-wins guards are supported. */
2606
+ listingUpdatedAt: true
2607
+ };
2608
+
2509
2609
  // src/auth.ts
2510
2610
  function buildAuthHeader(apiKey, apiSecret) {
2511
2611
  const token = Buffer.from(`${apiKey}:${apiSecret}`, "utf8").toString("base64");
@@ -2516,27 +2616,31 @@ function buildUserAgent(sellerId, integratorName) {
2516
2616
  }
2517
2617
  function mapHttpError(status, body, retryAfterMs) {
2518
2618
  const data = { body };
2619
+ const issues = normalizeErrorIssues(body);
2519
2620
  switch (status) {
2520
2621
  case 401:
2521
2622
  return new AuthError({
2522
2623
  message: "Trendyol authentication failed (check apiKey / apiSecret)",
2523
2624
  status,
2524
- data
2625
+ data,
2626
+ issues
2525
2627
  });
2526
2628
  case 403:
2527
2629
  return new ValidationError({
2528
2630
  message: "Trendyol forbidden (check User-Agent header or endpoint path)",
2529
2631
  status,
2530
- data
2632
+ data,
2633
+ issues
2531
2634
  });
2532
2635
  case 404:
2533
- return new NotFoundError({ message: "Trendyol resource not found", status, data });
2636
+ return new NotFoundError({ message: "Trendyol resource not found", status, data, issues });
2534
2637
  case 429:
2535
2638
  return new RateLimitError({
2536
2639
  message: "Trendyol rate limit exceeded",
2537
2640
  status,
2538
2641
  retryAfterMs,
2539
- data
2642
+ data,
2643
+ issues
2540
2644
  });
2541
2645
  case 500:
2542
2646
  case 502:
@@ -2545,7 +2649,8 @@ function mapHttpError(status, body, retryAfterMs) {
2545
2649
  return new ServerError({
2546
2650
  message: `Trendyol server error (${status})`,
2547
2651
  status,
2548
- data
2652
+ data,
2653
+ issues
2549
2654
  });
2550
2655
  default:
2551
2656
  return new LoncaError({
@@ -2553,10 +2658,42 @@ function mapHttpError(status, body, retryAfterMs) {
2553
2658
  message: `Trendyol unexpected response (${status})`,
2554
2659
  status,
2555
2660
  data,
2661
+ issues,
2556
2662
  retryable: false
2557
2663
  });
2558
2664
  }
2559
2665
  }
2666
+ function normalizeErrorIssues(body) {
2667
+ const errors = extractErrorsArray(body);
2668
+ if (!errors) return [];
2669
+ const issues = [];
2670
+ for (const entry of errors) {
2671
+ if (typeof entry === "string") {
2672
+ issues.push({ message: entry });
2673
+ continue;
2674
+ }
2675
+ if (entry && typeof entry === "object") {
2676
+ const e = entry;
2677
+ if (typeof e.message !== "string") continue;
2678
+ const issue = { message: e.message };
2679
+ if (typeof e.field === "string") issue.field = e.field;
2680
+ if (typeof e.code === "string") issue.code = e.code;
2681
+ issues.push(issue);
2682
+ }
2683
+ }
2684
+ return issues;
2685
+ }
2686
+ function extractErrorsArray(body) {
2687
+ if (!body || typeof body !== "object") return void 0;
2688
+ const b = body;
2689
+ if (Array.isArray(b.errors)) return b.errors;
2690
+ const nested = b.body;
2691
+ if (nested && typeof nested === "object") {
2692
+ const n2 = nested;
2693
+ if (Array.isArray(n2.errors)) return n2.errors;
2694
+ }
2695
+ return void 0;
2696
+ }
2560
2697
  function parseRetryAfter(header) {
2561
2698
  if (!header) return void 0;
2562
2699
  const seconds = Number(header);
@@ -2652,11 +2789,11 @@ var TrendyolTransport = class {
2652
2789
  },
2653
2790
  {
2654
2791
  signal: opts.signal,
2655
- onRetry: (err, attempt, delay) => {
2792
+ onRetry: (err, attempt, delay2) => {
2656
2793
  if (err instanceof LoncaError) {
2657
2794
  this.logger.warn("trendyol.retry", {
2658
2795
  attempt,
2659
- delayMs: delay,
2796
+ delayMs: delay2,
2660
2797
  code: err.code,
2661
2798
  status: err.status
2662
2799
  });
@@ -2714,12 +2851,16 @@ function createTrendyolClient(opts) {
2714
2851
  logger: opts.logger,
2715
2852
  timeoutMs: opts.timeoutMs
2716
2853
  });
2854
+ return buildClient(transport);
2855
+ }
2856
+ function buildClient(transport) {
2857
+ const products = new ProductsResource(transport);
2717
2858
  return {
2718
2859
  brands: new BrandsResource(transport),
2719
2860
  categories: new CategoriesResource(transport),
2720
2861
  suppliers: new SuppliersResource(transport),
2721
- products: new ProductsResource(transport),
2722
- inventory: new InventoryResource(transport),
2862
+ products,
2863
+ inventory: new InventoryResource(transport, (id) => products.getBatchStatus(id)),
2723
2864
  orders: new OrdersResource(transport),
2724
2865
  claims: new ClaimsResource(transport),
2725
2866
  webhooks: new WebhooksResource(transport),
@@ -2730,7 +2871,8 @@ function createTrendyolClient(opts) {
2730
2871
  testOrders: new TestOrdersResource(transport),
2731
2872
  locations: new LocationsResource(transport),
2732
2873
  exportCenter: new ExportCenterResource(transport),
2733
- videos: new VideosResource(transport)
2874
+ videos: new VideosResource(transport),
2875
+ capabilities: trendyolCapabilities
2734
2876
  };
2735
2877
  }
2736
2878
  function parseWebhookEvent(rawBody) {
@@ -2767,7 +2909,29 @@ function parseWebhookEvent(rawBody) {
2767
2909
  raw: body
2768
2910
  };
2769
2911
  }
2912
+ var statusMap = {
2913
+ Created: "created",
2914
+ Awaiting: "created",
2915
+ // new order awaiting seller acceptance
2916
+ Verified: "created",
2917
+ // accepted/verified, before picking
2918
+ Picking: "picking",
2919
+ UnPacked: "picking",
2920
+ // back in fulfillment after being unpacked
2921
+ Invoiced: "invoiced",
2922
+ Shipped: "shipped",
2923
+ AtCollectionPoint: "shipped",
2924
+ // in transit, awaiting pickup
2925
+ UnDelivered: "shipped",
2926
+ // delivery attempt failed; still in carrier's hands
2927
+ Delivered: "delivered",
2928
+ Cancelled: "cancelled",
2929
+ UnSupplied: "cancelled",
2930
+ // seller couldn't supply → effectively cancelled
2931
+ Returned: "returned"
2932
+ };
2933
+ var normalizeStatus = createStatusNormalizer(statusMap);
2770
2934
 
2771
- export { BrandsResource, CategoriesResource, ClaimsResource, ExportCenterResource, FinanceResource, InventoryResource, InvoicesResource, LabelsResource, LocationsResource, OrdersResource, ProductsResource, QuestionsResource, SuppliersResource, TestOrdersResource, VideosResource, WebhooksResource, createTrendyolClient, normalizeShipmentPackage, parseWebhookEvent };
2935
+ 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
2936
  //# sourceMappingURL=index.js.map
2773
2937
  //# sourceMappingURL=index.js.map