@lonca/trendyol 0.2.0 → 0.4.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
@@ -36,6 +36,29 @@ var BrandsResource = class {
36
36
  const nextCursor = page + 1 < data.totalPages ? String(page + 1) : void 0;
37
37
  return nextCursor !== void 0 ? { items, nextCursor } : { items };
38
38
  }
39
+ /**
40
+ * Search brands by name. Useful when you need a brand's numeric ID for
41
+ * `createProducts` and don't want to page through the full `list()`
42
+ * (1000 brands per page).
43
+ *
44
+ * **Discovery-first wire fact (verified STAGE 2026-05-25):** Trendyol's
45
+ * doc claims this is a case-sensitive *exact* match, but live behaviour
46
+ * is **substring + case-insensitive** — `search('Trendyol')` returns
47
+ * 17 hits including `TRENDYOLMILLA`, `trendyol vavist`, `Trendyol Üyelik`.
48
+ * Plan for ranking your results client-side if you need an exact match.
49
+ * The endpoint returns an empty array when nothing matches (no 404).
50
+ *
51
+ * @param name The brand name to search for.
52
+ */
53
+ async search(name) {
54
+ const data = await this.transport.request({
55
+ method: "GET",
56
+ path: "/integration/product/brands/by-name",
57
+ query: { name },
58
+ rateLimiter: this.limiter
59
+ });
60
+ return (data ?? []).map((b) => ({ id: String(b.id), name: b.name }));
61
+ }
39
62
  };
40
63
  function normalizeCategory(node) {
41
64
  return {
@@ -65,12 +88,16 @@ function normalizeAttribute(node) {
65
88
  }
66
89
  return out;
67
90
  }
91
+ var MAX_ATTR_VALUES_PAGE_SIZE = 1e3;
92
+ var DEFAULT_ATTR_VALUES_PAGE_SIZE = 100;
68
93
  var CategoriesResource = class {
69
- constructor(transport, limiter) {
94
+ constructor(transport, limiter, sellerId) {
70
95
  this.transport = transport;
96
+ this.sellerId = sellerId;
71
97
  this.limiter = limiter ?? new core.TokenBucketRateLimiter({ capacity: 50, intervalMs: 6e4 });
72
98
  }
73
99
  transport;
100
+ sellerId;
74
101
  limiter;
75
102
  /**
76
103
  * Fetch the full Trendyol category tree.
@@ -112,6 +139,87 @@ var CategoriesResource = class {
112
139
  }
113
140
  return list.map(normalizeAttribute);
114
141
  }
142
+ /**
143
+ * Fetch the allowed values for a single category attribute (paginated).
144
+ *
145
+ * `getCategoryAttributes` returns attribute metadata + flags but typically
146
+ * omits the value catalog. Use this method to fetch the catalog for an
147
+ * attribute when `allowCustom` is `false` and you need to map your data
148
+ * onto Trendyol's accepted values.
149
+ *
150
+ * @param categoryId Trendyol numeric category ID; accepts `string` or `number`.
151
+ * @param attributeId Attribute ID returned by `getAttributes`.
152
+ * @param params Cursor pagination (max page size 1000; default 100).
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * import { paginate } from '@lonca/core';
157
+ * for await (const value of paginate((p) =>
158
+ * client.categories.getAttributeValues(catId, attrId, p),
159
+ * )) {
160
+ * console.log(value.id, value.name);
161
+ * }
162
+ * ```
163
+ */
164
+ async getAttributeValues(categoryId, attributeId, params = {}) {
165
+ const catId = encodeURIComponent(String(categoryId));
166
+ const attrId = encodeURIComponent(String(attributeId));
167
+ const size = Math.min(params.limit ?? DEFAULT_ATTR_VALUES_PAGE_SIZE, MAX_ATTR_VALUES_PAGE_SIZE);
168
+ const page = params.cursor ? Number.parseInt(params.cursor, 10) : 0;
169
+ const data = await this.transport.request({
170
+ method: "GET",
171
+ path: `/integration/product/categories/${catId}/attributes/${attrId}/values`,
172
+ query: { page, size },
173
+ rateLimiter: this.limiter
174
+ });
175
+ const items = (data.content ?? []).map((item) => ({
176
+ id: item.attributeValueId !== void 0 ? String(item.attributeValueId) : "",
177
+ name: item.attributeValue ?? item.attributeValueName ?? ""
178
+ }));
179
+ const result = { items };
180
+ const totalPages = typeof data.totalPages === "number" ? data.totalPages : 0;
181
+ if (page + 1 < totalPages) {
182
+ result.nextCursor = String(page + 1);
183
+ }
184
+ return result;
185
+ }
186
+ /**
187
+ * Look up category info for a list of barcodes (Trendyol Export Center
188
+ * / AutoFT endpoint).
189
+ *
190
+ * **Requires Export Center enrollment.** Sellers who have not joined
191
+ * Trendyol's "İhracat Merkezi" program will get an auth error on this
192
+ * endpoint even though their regular Marketplace credentials are valid.
193
+ *
194
+ * @param barcodes 1–N barcodes to look up.
195
+ * @throws {ValidationError} when `barcodes` is empty.
196
+ * @throws Error when the client was created without a `sellerId` and this
197
+ * method is called directly (use `createTrendyolClient` to wire it).
198
+ */
199
+ async getByBarcodes(barcodes) {
200
+ if (!Array.isArray(barcodes) || barcodes.length === 0) {
201
+ throw new core.ValidationError({ message: "getByBarcodes: barcodes must not be empty" });
202
+ }
203
+ if (this.sellerId === void 0) {
204
+ throw new Error(
205
+ "CategoriesResource.getByBarcodes requires a sellerId; instantiate via createTrendyolClient"
206
+ );
207
+ }
208
+ const data = await this.transport.request({
209
+ method: "POST",
210
+ path: `/integration/ecgw/v1/${this.sellerId}/lookup/product-categories/by-barcodes`,
211
+ body: { barcodes },
212
+ rateLimiter: this.limiter
213
+ });
214
+ const matches = Object.entries(data.barcodeCategories ?? {}).map(([barcode, cat]) => ({
215
+ barcode,
216
+ category: {
217
+ id: cat.id !== void 0 ? String(cat.id) : "",
218
+ name: cat.displayName ?? ""
219
+ }
220
+ }));
221
+ return { matches, notFound: data.notFound ?? [] };
222
+ }
115
223
  };
116
224
  function extractAttributeList(data) {
117
225
  if (Array.isArray(data)) return data;
@@ -251,8 +359,9 @@ function normalizeCustomer(node) {
251
359
  return out;
252
360
  }
253
361
  function normalizePackage(node) {
362
+ const pkgId = node.shipmentPackageId ?? node.id;
254
363
  const out = {
255
- id: node.shipmentPackageId !== void 0 ? String(node.shipmentPackageId) : "",
364
+ id: pkgId !== void 0 ? String(pkgId) : "",
256
365
  orderNumber: node.orderNumber ?? "",
257
366
  status: node.status ?? "",
258
367
  customer: normalizeCustomer(node),
@@ -360,9 +469,346 @@ var OrdersResource = class {
360
469
  }
361
470
  return result;
362
471
  }
472
+ /**
473
+ * Push a shipment-package status update.
474
+ *
475
+ * Trendyol restricts the seller-side push to two transitions:
476
+ * - `Picking` — order picked up from the shelf / being prepared
477
+ * - `Invoiced` — invoice issued, ready for cargo handoff
478
+ *
479
+ * Other transitions (`Shipped`, `Delivered`, etc.) are driven by Trendyol
480
+ * or the cargo provider — call `processAlternativeDelivery` or
481
+ * `manualDeliverByPackageId` if you ship outside Trendyol's cargo
482
+ * network.
483
+ *
484
+ * Returns void; Trendyol responds with 200 + empty body on success.
485
+ */
486
+ async updatePackageStatus(packageId, input) {
487
+ await this.transport.request({
488
+ method: "PUT",
489
+ path: this.packagePath(packageId),
490
+ body: input,
491
+ rateLimiter: this.limiter
492
+ });
493
+ }
494
+ /**
495
+ * Notify Trendyol that one or more line items cannot be supplied
496
+ * ("Tedarik Edememe Bildirimi"). Marks the listed line IDs as
497
+ * `UnSupplied`. Trendyol cancels those quantities and notifies the
498
+ * customer.
499
+ *
500
+ * `reasonId` is a numeric code Trendyol publishes separately — consult
501
+ * the seller panel or the "Tedarik Edememe" docs for current values.
502
+ *
503
+ * Returns void; Trendyol responds with 200 + empty body on success.
504
+ */
505
+ async cancelPackageItem(packageId, input) {
506
+ if (!Array.isArray(input?.lines) || input.lines.length === 0) {
507
+ throw new core.ValidationError({ message: "cancelPackageItem: lines must not be empty" });
508
+ }
509
+ await this.transport.request({
510
+ method: "PUT",
511
+ path: `${this.packagePath(packageId)}/items/unsupplied`,
512
+ body: input,
513
+ rateLimiter: this.limiter
514
+ });
515
+ }
516
+ /**
517
+ * Extend the agreed delivery date for a shipment package by 1, 2, or 3 days.
518
+ * Trendyol enforces the [1, 3] range server-side; the SDK validates client-side
519
+ * to fail fast.
520
+ *
521
+ * Returns void; Trendyol responds with 200 + empty body on success.
522
+ */
523
+ async extendDeliveryDate(packageId, extendedDayCount) {
524
+ if (![1, 2, 3].includes(extendedDayCount)) {
525
+ throw new core.ValidationError({
526
+ message: `extendDeliveryDate: extendedDayCount must be 1, 2, or 3 (got ${extendedDayCount})`
527
+ });
528
+ }
529
+ await this.transport.request({
530
+ method: "PUT",
531
+ path: `${this.packagePath(packageId)}/extended-agreed-delivery-date`,
532
+ body: { extendedDayCount },
533
+ rateLimiter: this.limiter
534
+ });
535
+ }
536
+ /**
537
+ * Notify Trendyol of an alternative delivery channel — used when the
538
+ * seller is shipping via a non-Trendyol cargo provider. Trendyol then
539
+ * either SMSes the customer the tracking link (when `isPhoneNumber` is
540
+ * `true`) or stores the tracking URL on the package directly.
541
+ *
542
+ * Returns void; Trendyol responds with 200 + empty body on success.
543
+ */
544
+ async processAlternativeDelivery(packageId, input) {
545
+ await this.transport.request({
546
+ method: "PUT",
547
+ path: `${this.packagePath(packageId)}/alternative-delivery`,
548
+ body: input,
549
+ rateLimiter: this.limiter
550
+ });
551
+ }
552
+ /**
553
+ * Split a shipment package by moving a set of line IDs into a new
554
+ * package. The original package keeps the remaining lines.
555
+ *
556
+ * @param packageId The package to split.
557
+ * @param orderLineIds Line IDs to move into the new package (1+).
558
+ * @throws {ValidationError} when `orderLineIds` is empty.
559
+ */
560
+ async splitPackage(packageId, orderLineIds) {
561
+ if (!Array.isArray(orderLineIds) || orderLineIds.length === 0) {
562
+ throw new core.ValidationError({ message: "splitPackage: orderLineIds must not be empty" });
563
+ }
564
+ await this.transport.request({
565
+ method: "POST",
566
+ path: `${this.packagePath(packageId)}/split`,
567
+ body: { orderLineIds },
568
+ rateLimiter: this.limiter
569
+ });
570
+ }
571
+ /**
572
+ * Split a shipment package by quantity. Each `QuantitySplit` entry
573
+ * carves a single line into multiple packages — e.g. `{ orderLineId: 100,
574
+ * quantities: [2, 2, 1] }` splits 5 units of line 100 into three packages
575
+ * of 2 + 2 + 1.
576
+ *
577
+ * @throws {ValidationError} when `quantitySplit` is empty.
578
+ */
579
+ async splitPackageByQuantity(packageId, quantitySplit) {
580
+ if (!Array.isArray(quantitySplit) || quantitySplit.length === 0) {
581
+ throw new core.ValidationError({
582
+ message: "splitPackageByQuantity: quantitySplit must not be empty"
583
+ });
584
+ }
585
+ await this.transport.request({
586
+ method: "POST",
587
+ path: `${this.packagePath(packageId)}/quantity-split`,
588
+ body: { quantitySplit },
589
+ rateLimiter: this.limiter
590
+ });
591
+ }
592
+ /**
593
+ * Split a shipment package into multiple new packages by grouping line
594
+ * IDs. Each `SplitGroup` becomes one new package containing the listed
595
+ * line IDs.
596
+ *
597
+ * @throws {ValidationError} when `splitGroups` is empty.
598
+ */
599
+ async multiSplitPackage(packageId, splitGroups) {
600
+ if (!Array.isArray(splitGroups) || splitGroups.length === 0) {
601
+ throw new core.ValidationError({ message: "multiSplitPackage: splitGroups must not be empty" });
602
+ }
603
+ await this.transport.request({
604
+ method: "POST",
605
+ path: `${this.packagePath(packageId)}/multi-split`,
606
+ body: { splitGroups },
607
+ rateLimiter: this.limiter
608
+ });
609
+ }
610
+ /**
611
+ * Split a shipment package into multiple new packages, each containing a
612
+ * mix of line items at specific quantities. This is the most expressive
613
+ * split — use it when you need fine-grained control over which line IDs
614
+ * and how many of each end up in each new package.
615
+ *
616
+ * @throws {ValidationError} when `splitPackages` is empty.
617
+ */
618
+ async splitMultiPackagesByQuantity(packageId, splitPackages) {
619
+ if (!Array.isArray(splitPackages) || splitPackages.length === 0) {
620
+ throw new core.ValidationError({
621
+ message: "splitMultiPackagesByQuantity: splitPackages must not be empty"
622
+ });
623
+ }
624
+ await this.transport.request({
625
+ method: "POST",
626
+ path: `${this.packagePath(packageId)}/split-packages`,
627
+ body: { splitPackages },
628
+ rateLimiter: this.limiter
629
+ });
630
+ }
631
+ /**
632
+ * Change the cargo provider on an existing shipment package. Use one of
633
+ * Trendyol's documented marketplace cargo codes (`'YKMP'`, `'ARASMP'`,
634
+ * `'SURATMP'`, etc.) — see `TrendyolCargoProvider` for the full list.
635
+ */
636
+ async changeCargoProvider(packageId, cargoProvider) {
637
+ await this.transport.request({
638
+ method: "PUT",
639
+ path: `${this.packagePath(packageId)}/cargo-providers`,
640
+ body: { cargoProvider },
641
+ rateLimiter: this.limiter
642
+ });
643
+ }
644
+ /**
645
+ * Mark a shipment package as manually delivered via its package ID.
646
+ * Used when the seller delivered the order outside Trendyol's cargo
647
+ * network and needs to flip the package to `Delivered` after handover.
648
+ *
649
+ * No request body; Trendyol responds with 200 + empty body on success.
650
+ */
651
+ async manualDeliverByPackageId(packageId) {
652
+ await this.transport.request({
653
+ method: "PUT",
654
+ path: `${this.packagePath(packageId)}/manual-invoice-delivery`,
655
+ rateLimiter: this.limiter
656
+ });
657
+ }
658
+ /**
659
+ * Manual-deliver variant that takes the cargo tracking number instead
660
+ * of the package ID. Useful when you only have the tracking number on
661
+ * hand (e.g., from a cargo provider webhook).
662
+ *
663
+ * Note the path structure: tracking number sits at a sibling location,
664
+ * not under `/shipment-packages/{id}/...`.
665
+ */
666
+ async manualDeliverByTrackingNumber(cargoTrackingNumber) {
667
+ await this.transport.request({
668
+ method: "PUT",
669
+ path: `/integration/order/sellers/${this.sellerId}/shipment-packages/manual-invoice-delivery-by-tracking-number/${encodeURIComponent(String(cargoTrackingNumber))}`,
670
+ rateLimiter: this.limiter
671
+ });
672
+ }
673
+ /**
674
+ * Mark a package as delivered through an authorized service ("yetkili
675
+ * servis"). For appliance / installation-required products that are
676
+ * delivered + installed by a third-party service partner.
677
+ */
678
+ async markDeliveredByService(packageId) {
679
+ await this.transport.request({
680
+ method: "PUT",
681
+ path: `${this.packagePath(packageId)}/delivered-by-service`,
682
+ rateLimiter: this.limiter
683
+ });
684
+ }
685
+ /**
686
+ * Update box / packaging metadata on a shipment package (desi value
687
+ * and/or number of boxes). Either field can be sent alone.
688
+ */
689
+ async updateBoxInfo(packageId, input) {
690
+ await this.transport.request({
691
+ method: "PUT",
692
+ path: `${this.packagePath(packageId)}/box-info`,
693
+ body: input,
694
+ rateLimiter: this.limiter
695
+ });
696
+ }
697
+ /**
698
+ * Update labor costs for one or more order lines.
699
+ *
700
+ * **Wire note:** Trendyol's request body is a raw array (no envelope),
701
+ * not `{ items: [...] }`. The SDK forwards `items` verbatim.
702
+ *
703
+ * @throws {ValidationError} when `items` is empty.
704
+ */
705
+ async updateLaborCosts(packageId, items) {
706
+ if (!Array.isArray(items) || items.length === 0) {
707
+ throw new core.ValidationError({ message: "updateLaborCosts: items must not be empty" });
708
+ }
709
+ await this.transport.request({
710
+ method: "PUT",
711
+ path: `${this.packagePath(packageId)}/labor-costs`,
712
+ body: items,
713
+ rateLimiter: this.limiter
714
+ });
715
+ }
716
+ /**
717
+ * Reassign a shipment package to a different warehouse. `warehouseId`
718
+ * comes from `client.suppliers.getAddresses()` (filter by `isShipmentAddress`).
719
+ */
720
+ async updateWarehouse(packageId, warehouseId) {
721
+ await this.transport.request({
722
+ method: "PUT",
723
+ path: `${this.packagePath(packageId)}/warehouse`,
724
+ body: { warehouseId },
725
+ rateLimiter: this.limiter
726
+ });
727
+ }
728
+ /**
729
+ * Stream variant of `list()`. Trendyol's `getShipmentPackagesStream`
730
+ * returns the same `ShipmentPackage` shape but paginates with an opaque
731
+ * cursor — useful when the dataset is large and page-based pagination
732
+ * would hit the 10 000-record cap.
733
+ *
734
+ * @example
735
+ * ```ts
736
+ * import { paginate } from '@lonca/core';
737
+ * for await (const pkg of paginate((p) =>
738
+ * client.orders.listStream({ ...p, packageItemStatuses: 'Created,Picking' }),
739
+ * )) {
740
+ * console.log(pkg.id, pkg.status);
741
+ * }
742
+ * ```
743
+ */
744
+ async listStream(params = {}) {
745
+ const size = Math.min(params.limit ?? DEFAULT_PAGE_SIZE2, MAX_PAGE_SIZE);
746
+ const query = { size };
747
+ if (params.cursor) query.nextCursor = params.cursor;
748
+ if (params.packageItemStatuses) query.packageItemStatuses = params.packageItemStatuses;
749
+ if (params.lastModifiedStartDate) {
750
+ query.lastModifiedStartDate = params.lastModifiedStartDate.getTime();
751
+ }
752
+ if (params.lastModifiedEndDate) {
753
+ query.lastModifiedEndDate = params.lastModifiedEndDate.getTime();
754
+ }
755
+ const data = await this.transport.request({
756
+ method: "GET",
757
+ path: `/integration/order/sellers/${this.sellerId}/orders/stream`,
758
+ query,
759
+ rateLimiter: this.limiter
760
+ });
761
+ const items = (data.content ?? []).map(normalizePackage);
762
+ const result = { items };
763
+ if (data.hasMore && data.nextCursor) {
764
+ result.nextCursor = data.nextCursor;
765
+ }
766
+ return result;
767
+ }
768
+ /**
769
+ * Fetch the per-parcel cargo-fee breakdown for a single cargo invoice
770
+ * (Trendyol's `getCargoInvoiceItems`). Useful for reconciling Trendyol's
771
+ * cargo deductions against your shipped packages.
772
+ *
773
+ * `invoiceSerialNumber` is sourced from the Current Account Statement
774
+ * ("Cari Hesap Ekstresi") with `transactionType=DeductionInvoices`.
775
+ *
776
+ * Page-based pagination internally (cursor encodes the page index).
777
+ */
778
+ async getCargoInvoiceItems(invoiceSerialNumber, params = {}) {
779
+ const size = Math.min(params.limit ?? 500, 500);
780
+ const page = params.cursor ? Number.parseInt(params.cursor, 10) : 0;
781
+ const data = await this.transport.request({
782
+ method: "GET",
783
+ path: `/integration/finance/che/sellers/${this.sellerId}/cargo-invoice/${encodeURIComponent(invoiceSerialNumber)}/items`,
784
+ query: { page, size },
785
+ rateLimiter: this.limiter
786
+ });
787
+ const items = (data.content ?? []).map((row) => {
788
+ const out = { raw: row };
789
+ if (row.shipmentPackageType !== void 0) out.shipmentPackageType = row.shipmentPackageType;
790
+ if (row.parcelUniqueId !== void 0) {
791
+ out.parcelUniqueId = typeof row.parcelUniqueId === "string" ? row.parcelUniqueId : String(row.parcelUniqueId);
792
+ }
793
+ if (row.orderNumber !== void 0) out.orderNumber = row.orderNumber;
794
+ if (typeof row.amount === "number") out.amount = row.amount;
795
+ if (typeof row.desi === "number") out.desi = row.desi;
796
+ return out;
797
+ });
798
+ const totalPages = typeof data.totalPages === "number" ? data.totalPages : 0;
799
+ const result = { items };
800
+ if (page + 1 < totalPages) {
801
+ result.nextCursor = String(page + 1);
802
+ }
803
+ return result;
804
+ }
805
+ packagePath(packageId) {
806
+ return `/integration/order/sellers/${this.sellerId}/shipment-packages/${encodeURIComponent(String(packageId))}`;
807
+ }
363
808
  };
364
809
  var DEFAULT_PAGE_SIZE3 = 50;
365
810
  var MAX_PAGE_SIZE2 = 1e3;
811
+ var MAX_BUYBOX_BARCODES = 10;
366
812
  function normalizeRef(ref) {
367
813
  return {
368
814
  id: ref?.id !== void 0 ? String(ref.id) : "",
@@ -427,6 +873,82 @@ function normalizeProduct(node) {
427
873
  if (node.lastModifiedBy !== void 0) out.lastModifiedBy = node.lastModifiedBy;
428
874
  return out;
429
875
  }
876
+ function normalizeUnapprovedRejectReason(r) {
877
+ const out = {};
878
+ if (r.rejectReason !== void 0) out.rejectReason = r.rejectReason;
879
+ if (r.rejectReasonDetail !== void 0) out.rejectReasonDetail = r.rejectReasonDetail;
880
+ return out;
881
+ }
882
+ function normalizeUnapproved(node) {
883
+ const imagesSource = node.images ?? node.media;
884
+ const out = {
885
+ productMainId: node.productMainId ?? "",
886
+ brand: normalizeRef(node.brand),
887
+ category: normalizeRef(node.category),
888
+ barcode: node.barcode ?? "",
889
+ title: node.title ?? "",
890
+ images: normalizeImages(imagesSource),
891
+ attributes: (node.attributes ?? []).map(normalizeAttribute2),
892
+ rejectReasonDetails: (node.rejectReasonDetails ?? []).map(normalizeUnapprovedRejectReason),
893
+ raw: node
894
+ };
895
+ if (node.supplierId !== void 0) out.supplierId = String(node.supplierId);
896
+ if (node.status !== void 0) out.status = node.status;
897
+ if (node.description !== void 0) out.description = node.description;
898
+ if (typeof node.quantity === "number") out.quantity = node.quantity;
899
+ if (typeof node.listPrice === "number") out.listPrice = node.listPrice;
900
+ if (typeof node.salePrice === "number") out.salePrice = node.salePrice;
901
+ if (typeof node.vatRate === "number") out.vatRate = node.vatRate;
902
+ if (node.dimensionalWeight !== void 0 && node.dimensionalWeight !== null) {
903
+ out.dimensionalWeight = node.dimensionalWeight;
904
+ }
905
+ if (node.stockCode !== void 0) out.stockCode = node.stockCode;
906
+ if (node.origin !== void 0) out.origin = node.origin;
907
+ if (node.locationBasedDelivery !== void 0) {
908
+ out.locationBasedDelivery = node.locationBasedDelivery;
909
+ }
910
+ if (node.lotNumber !== void 0) out.lotNumber = node.lotNumber;
911
+ if (node.specialConsumptionTax !== void 0) {
912
+ out.specialConsumptionTax = node.specialConsumptionTax;
913
+ }
914
+ if (node.sgrPrice !== void 0) out.sgrPrice = node.sgrPrice;
915
+ const createdAt = toIso2(node.createDateTime);
916
+ if (createdAt) out.createdAt = createdAt;
917
+ const updatedAt = toIso2(node.lastUpdateDate);
918
+ if (updatedAt) out.updatedAt = updatedAt;
919
+ const priceChanged = toIso2(node.lastPriceChangeDate);
920
+ if (priceChanged) out.lastPriceChangedAt = priceChanged;
921
+ const stockChanged = toIso2(node.lastStockChangeDate);
922
+ if (stockChanged) out.lastStockChangedAt = stockChanged;
923
+ return out;
924
+ }
925
+ function normalizeBuyboxInfo(node) {
926
+ const out = {
927
+ barcode: node.barcode ?? "",
928
+ raw: node
929
+ };
930
+ if (typeof node.buyboxOrder === "number") out.buyboxOrder = node.buyboxOrder;
931
+ if (typeof node.buyboxPrice === "number") out.buyboxPrice = node.buyboxPrice;
932
+ if (typeof node.hasMultipleSeller === "boolean") {
933
+ out.hasMultipleSeller = node.hasMultipleSeller;
934
+ }
935
+ if (node.secondBuyboxPrice !== void 0) out.secondBuyboxPrice = node.secondBuyboxPrice;
936
+ if (node.thirdBuyboxPrice !== void 0) out.thirdBuyboxPrice = node.thirdBuyboxPrice;
937
+ return out;
938
+ }
939
+ function normalizeProductBase(data) {
940
+ const out = {
941
+ barcode: data.barcode ?? "",
942
+ approved: !!data.approved,
943
+ archived: !!data.archived,
944
+ raw: data
945
+ };
946
+ const approvedAt = toIso2(data.approvedDate);
947
+ if (approvedAt) out.approvedAt = approvedAt;
948
+ if (data.listingId !== void 0) out.listingId = data.listingId;
949
+ if (data.contentId !== void 0) out.contentId = String(data.contentId);
950
+ return out;
951
+ }
430
952
  function normalizeBatchResult(data) {
431
953
  const items = (data.items ?? []).map((item) => {
432
954
  const result = {};
@@ -454,17 +976,52 @@ function normalizeBatchResult(data) {
454
976
  if (data.storeFrontCode !== void 0) out.storeFrontCode = data.storeFrontCode;
455
977
  return out;
456
978
  }
979
+ var MAX_WRITE_ITEMS = 1e3;
980
+ var DELETE_RATE_PER_MIN = 100;
457
981
  var ProductsResource = class {
458
982
  constructor(transport, sellerId, options = {}) {
459
983
  this.transport = transport;
460
984
  this.sellerId = sellerId;
461
985
  this.filterLimiter = options.filterLimiter ?? new core.TokenBucketRateLimiter({ capacity: 2e3, intervalMs: 6e4 });
462
986
  this.batchLimiter = options.batchLimiter ?? new core.TokenBucketRateLimiter({ capacity: 1e3, intervalMs: 6e4 });
987
+ this.buyboxLimiter = options.buyboxLimiter ?? new core.TokenBucketRateLimiter({ capacity: 1e3, intervalMs: 6e4 });
988
+ this.writeLimiter = options.writeLimiter ?? new core.TokenBucketRateLimiter({ capacity: 1e3, intervalMs: 6e4 });
989
+ this.deleteLimiter = options.deleteLimiter ?? new core.TokenBucketRateLimiter({ capacity: DELETE_RATE_PER_MIN, intervalMs: 6e4 });
463
990
  }
464
991
  transport;
465
992
  sellerId;
466
993
  filterLimiter;
467
994
  batchLimiter;
995
+ buyboxLimiter;
996
+ writeLimiter;
997
+ deleteLimiter;
998
+ validateBarcodes(barcodes, methodLabel) {
999
+ if (!Array.isArray(barcodes) || barcodes.length === 0) {
1000
+ throw new core.ValidationError({ message: `${methodLabel}: barcodes must not be empty` });
1001
+ }
1002
+ if (barcodes.length > MAX_WRITE_ITEMS) {
1003
+ throw new core.ValidationError({
1004
+ message: `${methodLabel}: max ${MAX_WRITE_ITEMS} barcodes per call (got ${barcodes.length})`
1005
+ });
1006
+ }
1007
+ }
1008
+ async submitWrite(endpoint, items, methodLabel) {
1009
+ if (!Array.isArray(items) || items.length === 0) {
1010
+ throw new core.ValidationError({ message: `${methodLabel}: items must not be empty` });
1011
+ }
1012
+ if (items.length > MAX_WRITE_ITEMS) {
1013
+ throw new core.ValidationError({
1014
+ message: `${methodLabel}: max ${MAX_WRITE_ITEMS} items per call (got ${items.length})`
1015
+ });
1016
+ }
1017
+ const data = await this.transport.request({
1018
+ method: "POST",
1019
+ path: `/integration/product/sellers/${this.sellerId}${endpoint}`,
1020
+ body: { items },
1021
+ rateLimiter: this.writeLimiter
1022
+ });
1023
+ return { batchRequestId: data?.batchRequestId ?? "" };
1024
+ }
468
1025
  /**
469
1026
  * List approved products. Use `paginate()` from `@lonca/core` to iterate
470
1027
  * lazily across pages.
@@ -526,6 +1083,235 @@ var ProductsResource = class {
526
1083
  });
527
1084
  return normalizeBatchResult(data);
528
1085
  }
1086
+ /**
1087
+ * List **unapproved** (draft / rejected / pending-review) products.
1088
+ *
1089
+ * Wire shape is intentionally flatter than the approved-product shape:
1090
+ * each barcode is one top-level item with `barcode`, `quantity`, `salePrice`
1091
+ * etc. at the root. Rejected drafts carry `rejectReasonDetails` so you can
1092
+ * surface why Trendyol's content team turned them down.
1093
+ *
1094
+ * Pagination follows the same convention as `list()`: `cursor` from the
1095
+ * previous response forwards as `nextPageToken`.
1096
+ *
1097
+ * @example
1098
+ * ```ts
1099
+ * const page = await client.products.listUnapproved({ limit: 50 });
1100
+ * for (const draft of page.items) {
1101
+ * if (draft.status === 'rejected') {
1102
+ * console.warn(draft.barcode, draft.rejectReasonDetails);
1103
+ * }
1104
+ * }
1105
+ * ```
1106
+ */
1107
+ async listUnapproved(params = {}) {
1108
+ const size = Math.min(params.limit ?? DEFAULT_PAGE_SIZE3, MAX_PAGE_SIZE2);
1109
+ const query = { size };
1110
+ if (params.cursor) {
1111
+ query.nextPageToken = params.cursor;
1112
+ } else {
1113
+ query.page = 0;
1114
+ }
1115
+ if (params.barcode) query.barcode = params.barcode;
1116
+ if (params.startDate) query.startDate = params.startDate.getTime();
1117
+ if (params.endDate) query.endDate = params.endDate.getTime();
1118
+ if (params.dateQueryType) query.dateQueryType = params.dateQueryType;
1119
+ if (params.supplierId !== void 0) query.supplierId = params.supplierId;
1120
+ const data = await this.transport.request({
1121
+ method: "GET",
1122
+ path: `/integration/product/sellers/${this.sellerId}/products/unapproved`,
1123
+ query,
1124
+ rateLimiter: this.filterLimiter
1125
+ });
1126
+ const items = (data.content ?? []).map(normalizeUnapproved);
1127
+ const page = { items };
1128
+ if (data.nextPageToken) {
1129
+ page.nextCursor = data.nextPageToken;
1130
+ }
1131
+ return page;
1132
+ }
1133
+ /**
1134
+ * Fetch the basic lifecycle status of a single product by barcode.
1135
+ *
1136
+ * Cheap and useful as a polling primitive after `createProducts`: poll
1137
+ * this endpoint until `approved` flips to `true` (or use
1138
+ * `client.products.getBatchStatus()` to track the originating batch).
1139
+ *
1140
+ * @param barcode The product barcode to look up.
1141
+ */
1142
+ async getBase(barcode) {
1143
+ const code = encodeURIComponent(barcode);
1144
+ const data = await this.transport.request({
1145
+ method: "GET",
1146
+ path: `/integration/product/sellers/${this.sellerId}/product/${code}`,
1147
+ rateLimiter: this.filterLimiter
1148
+ });
1149
+ return normalizeProductBase(data);
1150
+ }
1151
+ /**
1152
+ * Fetch buybox information for up to 10 barcodes in one call.
1153
+ *
1154
+ * Returns rank (`buyboxOrder === 1` means you hold the buybox), the
1155
+ * current buybox price, and — beyond the spec — the second and third
1156
+ * competing prices when other sellers are present.
1157
+ *
1158
+ * @param barcodes 1–10 product barcodes.
1159
+ * @throws {ValidationError} when `barcodes` is empty or longer than 10.
1160
+ */
1161
+ async getBuyboxInfo(barcodes) {
1162
+ if (!Array.isArray(barcodes) || barcodes.length === 0) {
1163
+ throw new core.ValidationError({
1164
+ message: "getBuyboxInfo: barcodes must be a non-empty array"
1165
+ });
1166
+ }
1167
+ if (barcodes.length > MAX_BUYBOX_BARCODES) {
1168
+ throw new core.ValidationError({
1169
+ message: `getBuyboxInfo: max ${MAX_BUYBOX_BARCODES} barcodes per call (got ${barcodes.length})`
1170
+ });
1171
+ }
1172
+ const data = await this.transport.request({
1173
+ method: "POST",
1174
+ path: `/integration/product/sellers/${this.sellerId}/products/buybox-information`,
1175
+ body: { barcodes },
1176
+ rateLimiter: this.buyboxLimiter
1177
+ });
1178
+ const list = data.buyboxInfo ?? data.content ?? [];
1179
+ return list.map(normalizeBuyboxInfo);
1180
+ }
1181
+ /**
1182
+ * Create products (V2). Async batch — returns a `batchRequestId` you can
1183
+ * poll with `getBatchStatus`. Max 1000 items per call.
1184
+ *
1185
+ * Trendyol requires the full V2 attribute payload — fetch via
1186
+ * `categories.getAttributes` (and `categories.getAttributeValues` for
1187
+ * values when `allowCustom === false`). Shipment / returning warehouse
1188
+ * IDs come from `suppliers.getAddresses`.
1189
+ *
1190
+ * @throws {ValidationError} when `items` is empty or longer than 1000.
1191
+ */
1192
+ async create(items) {
1193
+ return this.submitWrite("/v2/products", items, "createProducts");
1194
+ }
1195
+ /**
1196
+ * Update **content** of approved products (title, description, images,
1197
+ * attributes). Identified by `contentId`. Partial update is supported
1198
+ * except for attributes — if you update ANY attribute, send ALL of them.
1199
+ *
1200
+ * @throws {ValidationError} when `items` is empty or longer than 1000.
1201
+ */
1202
+ async updateContent(items) {
1203
+ return this.submitWrite("/products/content-bulk-update", items, "updateContent");
1204
+ }
1205
+ /**
1206
+ * Update **variant** fields of approved products (stockCode, vatRate,
1207
+ * dimensionalWeight, warehouse IDs, location-based delivery, lot). Identified
1208
+ * by `barcode`. The barcode itself cannot be changed via this endpoint.
1209
+ *
1210
+ * @throws {ValidationError} when `items` is empty or longer than 1000.
1211
+ */
1212
+ async updateVariants(items) {
1213
+ return this.submitWrite("/products/variant-bulk-update", items, "updateVariants");
1214
+ }
1215
+ /**
1216
+ * Update **unapproved** (draft) products. Identified by `barcode`. All
1217
+ * other fields are optional partial updates. Use this to fix drafts that
1218
+ * Trendyol rejected — `client.products.listUnapproved` surfaces the
1219
+ * `rejectReasonDetails` you need to act on.
1220
+ *
1221
+ * **Gotcha (verified live STAGE 2026-05-25):** Trendyol's V2 spec claims
1222
+ * only `barcode` is required, but the endpoint returns HTTP 500
1223
+ * (`TrendyolSystemException` / `TypeError`) when too many optional fields
1224
+ * are omitted. In practice, send at least `title`, `description`,
1225
+ * `productMainId`, `brandId`, `categoryId`, `stockCode`,
1226
+ * `dimensionalWeight`, `vatRate`, `images[]`, and `attributes[]` (an
1227
+ * empty array is OK for the latter). The SDK forwards your payload
1228
+ * as-is; trim fields only if you have verified the server accepts it.
1229
+ *
1230
+ * @throws {ValidationError} when `items` is empty or longer than 1000.
1231
+ */
1232
+ async updateUnapproved(items) {
1233
+ return this.submitWrite("/products/unapproved-bulk-update", items, "updateUnapproved");
1234
+ }
1235
+ /**
1236
+ * Update product **delivery information** (deliveryDuration,
1237
+ * fastDeliveryType). Identified by `barcode`.
1238
+ *
1239
+ * @throws {ValidationError} when `items` is empty or longer than 1000.
1240
+ */
1241
+ async updateDeliveryInfo(items) {
1242
+ return this.submitWrite("/products/delivery-info-bulk-update", items, "updateDeliveryInfo");
1243
+ }
1244
+ /**
1245
+ * Delete products by barcode. Trendyol allows deletion of unapproved
1246
+ * products and approved products that have been archived for more than a
1247
+ * day (and have not been sales-stopped by Trendyol).
1248
+ *
1249
+ * Async batch — returns `{ batchRequestId }` to poll via `getBatchStatus`.
1250
+ * Separately rate-limited at **100 req/min** (much tighter than create/update).
1251
+ *
1252
+ * @param barcodes 1–1000 barcodes.
1253
+ * @throws {ValidationError} when `barcodes` is empty or longer than 1000.
1254
+ */
1255
+ async delete(barcodes) {
1256
+ this.validateBarcodes(barcodes, "delete");
1257
+ const data = await this.transport.request({
1258
+ method: "DELETE",
1259
+ path: `/integration/product/sellers/${this.sellerId}/products`,
1260
+ body: { items: barcodes.map((barcode) => ({ barcode })) },
1261
+ rateLimiter: this.deleteLimiter
1262
+ });
1263
+ return { batchRequestId: data?.batchRequestId ?? "" };
1264
+ }
1265
+ /**
1266
+ * Archive products by barcode (Trendyol's `archived=true` state).
1267
+ * Archived products are not visible to customers; pair with `delete`
1268
+ * after the 24-hour archive cool-down to remove them entirely.
1269
+ *
1270
+ * Async batch — returns `{ batchRequestId }`.
1271
+ *
1272
+ * @throws {ValidationError} when `barcodes` is empty or longer than 1000.
1273
+ */
1274
+ async archive(barcodes) {
1275
+ return this.setArchivedState(barcodes, true, "archive");
1276
+ }
1277
+ /**
1278
+ * Unarchive products by barcode (Trendyol's `archived=false` state).
1279
+ * Restores visibility for previously-archived products.
1280
+ *
1281
+ * @throws {ValidationError} when `barcodes` is empty or longer than 1000.
1282
+ */
1283
+ async unarchive(barcodes) {
1284
+ return this.setArchivedState(barcodes, false, "unarchive");
1285
+ }
1286
+ async setArchivedState(barcodes, archived, methodLabel) {
1287
+ this.validateBarcodes(barcodes, methodLabel);
1288
+ const data = await this.transport.request({
1289
+ method: "PUT",
1290
+ path: `/integration/product/sellers/${this.sellerId}/products/archive-state`,
1291
+ body: { items: barcodes.map((barcode) => ({ barcode, archived })) },
1292
+ rateLimiter: this.writeLimiter
1293
+ });
1294
+ return { batchRequestId: data?.batchRequestId ?? "" };
1295
+ }
1296
+ /**
1297
+ * Unlock products whose sale was paused by Trendyol due to pricing
1298
+ * issues (under/over-pricing, critical price error, supplier issues).
1299
+ * Restores selling status for the listed barcodes.
1300
+ *
1301
+ * Async batch — returns `{ batchRequestId }`.
1302
+ *
1303
+ * @throws {ValidationError} when `barcodes` is empty or longer than 1000.
1304
+ */
1305
+ async unlock(barcodes) {
1306
+ this.validateBarcodes(barcodes, "unlock");
1307
+ const data = await this.transport.request({
1308
+ method: "PUT",
1309
+ path: `/integration/product/sellers/${this.sellerId}/products/unlock`,
1310
+ body: { items: barcodes.map((barcode) => ({ barcode })) },
1311
+ rateLimiter: this.writeLimiter
1312
+ });
1313
+ return { batchRequestId: data?.batchRequestId ?? "" };
1314
+ }
529
1315
  };
530
1316
  var VALID_TYPES = /* @__PURE__ */ new Set([
531
1317
  "SHIPMENT",
@@ -810,7 +1596,7 @@ function createTrendyolClient(opts) {
810
1596
  });
811
1597
  return {
812
1598
  brands: new BrandsResource(transport),
813
- categories: new CategoriesResource(transport),
1599
+ categories: new CategoriesResource(transport, void 0, opts.sellerId),
814
1600
  suppliers: new SuppliersResource(transport, opts.sellerId),
815
1601
  products: new ProductsResource(transport, opts.sellerId),
816
1602
  inventory: new InventoryResource(transport, opts.sellerId),