@lonca/trendyol 0.3.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/README.md CHANGED
@@ -1,15 +1,37 @@
1
1
  # @lonca/trendyol
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@lonca/trendyol.svg)](https://www.npmjs.com/package/@lonca/trendyol)
4
+
3
5
  Type-safe TypeScript SDK for the [Trendyol Marketplace API](https://developers.trendyol.com).
4
6
 
5
- > ⚠️ **Alpha** — Only the `brands` resource is implemented in this initial scaffold. More endpoints (orders, products, inventory, webhooks) land in follow-up releases.
7
+ > **`0.3.0` — bottom-up Phase 2 complete.** The full product surface is now wire-verified: brands · categories · suppliers · products (read + write + lifecycle) · inventory · orders.
8
+
9
+ ## Coverage
10
+
11
+ Each entry is a method on the client. Endpoints behind `★` are discovery-first wire-verified against live Trendyol STAGE — the SDK normalizes any spec/wire mismatch.
12
+
13
+ | Resource | Methods |
14
+ | ---------------- | -------------------------------------------------------------------------------------------------------------------------- |
15
+ | `brands` | `list()`, `search(name)` ★ |
16
+ | `categories` | `list()`, `getAttributes(id)`, `getAttributeValues(catId, attrId)` ★, `getByBarcodes(barcodes)` (AutoFT) |
17
+ | `suppliers` | `getAddresses({forceRefresh?})` (1-hour cache; rate-limited 1 req/hour on Trendyol) |
18
+ | `products` | `list({...})` ★, `listUnapproved({...})` ★, `getBase(barcode)`, `getBuyboxInfo(barcodes)`, `getBatchStatus(id)` |
19
+ | `products` write | `create(items)`, `updateContent(items)`, `updateVariants(items)`, `updateUnapproved(items)` ★, `updateDeliveryInfo(items)` |
20
+ | `products` life | `delete(barcodes)`, `archive(barcodes)`, `unarchive(barcodes)`, `unlock(barcodes)` |
21
+ | `inventory` | `update(items)` (stock + price, async batch) ★ |
22
+ | `orders` | `list({...})` (shipment packages) ★ |
23
+
24
+ Trendyol's product API has 12 V2 read/write/lifecycle endpoints — all 12 are covered. Categories V2 (tree + attributes + values + barcode→category lookup) is complete. V1 endpoints are intentionally skipped (Trendyol sunsets them August 2026). Order sub-endpoints (`updatePackageStatus`, `splitShipmentPackage`, returns, claims, webhooks) land in subsequent phases.
6
25
 
7
26
  ## Install
8
27
 
9
28
  ```bash
10
- pnpm add @lonca/trendyol
29
+ pnpm add @lonca/trendyol @lonca/core
30
+ # or npm install / yarn add
11
31
  ```
12
32
 
33
+ `@lonca/core` is a peer dependency (provides `paginate`, `CursorPage`, error classes, the token-bucket limiter).
34
+
13
35
  ## Quick start
14
36
 
15
37
  ```ts
@@ -24,24 +46,151 @@ const client = createTrendyolClient({
24
46
  integratorName: 'MyCompany', // optional; defaults to 'SelfIntegration'
25
47
  });
26
48
 
27
- for await (const brand of paginate((p) => client.brands.list(p))) {
28
- console.log(brand.id, brand.name);
49
+ // Iterate every product page-by-page.
50
+ for await (const product of paginate((p) => client.products.list(p))) {
51
+ for (const variant of product.variants) {
52
+ console.log(variant.barcode, product.title, variant.stock ?? '?');
53
+ }
29
54
  }
30
55
  ```
31
56
 
32
- ## Authentication
57
+ ## End-to-end: create a product
33
58
 
34
- 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).
59
+ The chain `brand → category → category attributes (+ values) → addresses → create → poll → verify` is the canonical "create a real listing" flow.
60
+
61
+ ```ts
62
+ import { createTrendyolClient } from '@lonca/trendyol';
35
63
 
36
- **Production vs Stage** have different credentials. Stage also requires IP whitelisting — register your CI/server IP with Trendyol support.
64
+ const client = createTrendyolClient({ ... });
65
+
66
+ // 1. Resolve brand + category IDs.
67
+ const [brand] = await client.brands.search('TRENDYOLMİLLA');
68
+ const tree = await client.categories.list();
69
+ const category = findLeaf(tree, /Elbise/); // your own walker
70
+
71
+ // 2. Fetch required attributes + a value (Renk = Kırmızı).
72
+ const attrs = await client.categories.getAttributes(category.id);
73
+ const renk = attrs.find((a) => a.name === 'Renk')!;
74
+ const renkValues = await client.categories.getAttributeValues(category.id, renk.id);
75
+ const kirmizi = renkValues.items.find((v) => v.name === 'Kırmızı')!;
76
+
77
+ // 3. Resolve shipment / returning warehouse IDs.
78
+ const addresses = await client.suppliers.getAddresses();
79
+ const shipment = addresses.find((a) => a.isShipmentAddress)!;
80
+ const returning = addresses.find((a) => a.isReturningAddress)!;
81
+
82
+ // 4. Submit the create (async batch).
83
+ const { batchRequestId } = await client.products.create([
84
+ {
85
+ barcode: 'MY-SKU-001',
86
+ title: 'Kırmızı Elbise',
87
+ productMainId: 'MY-MAIN-001',
88
+ brandId: Number(brand.id),
89
+ categoryId: Number(category.id),
90
+ quantity: 10,
91
+ stockCode: 'MY-SC-001',
92
+ dimensionalWeight: 1,
93
+ description: '<p>...</p>',
94
+ listPrice: 299.9,
95
+ salePrice: 199.9,
96
+ images: [{ url: 'https://cdn.example.com/dress.jpg' }],
97
+ vatRate: 20,
98
+ attributes: [{ attributeId: Number(renk.id), attributeValueIds: [Number(kirmizi.id)] }],
99
+ shipmentAddressId: Number(shipment.id),
100
+ returningAddressId: Number(returning.id),
101
+ },
102
+ ]);
103
+
104
+ // 5. Poll until Trendyol finishes content review.
105
+ let result;
106
+ do {
107
+ await new Promise((r) => setTimeout(r, 2000));
108
+ result = await client.products.getBatchStatus(batchRequestId);
109
+ } while (result.items[0]?.status === 'PROCESSING');
110
+
111
+ // 6. Detect approval (or surface a rejection reason).
112
+ if (result.items[0]?.status === 'SUCCESS') {
113
+ const base = await client.products.getBase('MY-SKU-001');
114
+ console.log('Approved:', base.approved, 'contentId:', base.contentId);
115
+ }
116
+ ```
37
117
 
38
- ## Built-in robustness
118
+ ## Per-resource cheat sheet
39
119
 
40
- - **Retry with exponential backoff** on 429 (respects `Retry-After`) and 5xx
41
- - **Per-endpoint rate limiting** (token bucket) sized to Trendyol's documented limits
42
- - **Structured errors** via `@lonca/core` (`AuthError`, `RateLimitError`, `NotFoundError`, `ServerError`, `ValidationError`, `NetworkError`, `TimeoutError`)
43
- - **Correlation ID** auto-generated per request (`x-correlationid` header) for Trendyol-side log tracing
44
- - **`AbortSignal` support** for cancellation throughout
120
+ ```ts
121
+ // brands
122
+ await client.brands.list({ limit: 1000 });
123
+ await client.brands.search('TRENDYOLMİLLA'); // substring + case-insensitive
124
+
125
+ // categories
126
+ const tree = await client.categories.list();
127
+ const attrs = await client.categories.getAttributes(catId);
128
+ const values = await client.categories.getAttributeValues(catId, attrId);
129
+ await client.categories.getByBarcodes(['BC1', 'BC2']); // requires AutoFT enrollment
130
+
131
+ // suppliers (cached 1h)
132
+ await client.suppliers.getAddresses();
133
+ await client.suppliers.getAddresses({ forceRefresh: true });
134
+
135
+ // products — read
136
+ await client.products.list({ barcode: 'BC1' });
137
+ await client.products.listUnapproved({ limit: 50 });
138
+ await client.products.getBase('BC1');
139
+ await client.products.getBuyboxInfo(['BC1', 'BC2']); // max 10 per call
140
+ await client.products.getBatchStatus(batchRequestId);
141
+
142
+ // products — write (all return { batchRequestId }; max 1000 items)
143
+ await client.products.create([...]);
144
+ await client.products.updateContent([{ contentId: 123, title: '...' }]);
145
+ await client.products.updateVariants([{ barcode: 'BC1', stockCode: 'NEW' }]);
146
+ await client.products.updateUnapproved([{ barcode: 'BC1', title: '...', /* fuller payload */ }]);
147
+ await client.products.updateDeliveryInfo([{ barcode: 'BC1', deliveryOptions: { deliveryDuration: 3 } }]);
148
+
149
+ // products — lifecycle
150
+ await client.products.delete(['BC1']); // separately rate-limited (100/min)
151
+ await client.products.archive(['BC1']); // PUT archived=true
152
+ await client.products.unarchive(['BC1']); // PUT archived=false
153
+ await client.products.unlock(['BC1']); // restore after Trendyol price-lock
154
+
155
+ // inventory — async batch
156
+ await client.inventory.update([{ barcode: 'BC1', quantity: 50, salePrice: 199.9, listPrice: 299.9 }]);
157
+
158
+ // orders — shipment packages
159
+ for await (const pkg of paginate((p) => client.orders.list(p))) {
160
+ console.log(pkg.id, pkg.status, pkg.lines.length);
161
+ }
162
+ ```
163
+
164
+ ## Async batch + polling
165
+
166
+ Every write endpoint (`products.create`, `updateContent`, `updateVariants`, `updateUnapproved`, `updateDeliveryInfo`, `delete`, `archive`, `unarchive`, `unlock`, `inventory.update`) is **asynchronous**: Trendyol accepts the batch and returns a `{ batchRequestId }`. Poll the result with:
167
+
168
+ ```ts
169
+ const status = await client.products.getBatchStatus(batchRequestId);
170
+ // status.status: 'PROCESSING' | 'COMPLETED' | 'FAILED'
171
+ // status.items[].status: per-item outcome
172
+ ```
173
+
174
+ **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.
175
+
176
+ ## Discovery-first wire fixes
177
+
178
+ `@lonca/trendyol` was built by hitting the live Trendyol STAGE for every endpoint before writing types. Several places where the official OpenAPI spec disagrees with the live wire are normalized automatically:
179
+
180
+ - `categories.getAttributeValues`: spec says `attributeValueName`, wire returns `attributeValue` → SDK normalizes to `{ id, name }`
181
+ - `products.listUnapproved`: spec says `media: [{url}]`, wire returns `images: [{url}]` → SDK exposes `images: string[]`
182
+ - `products.getBuyboxInfo`: wire returns extra `secondBuyboxPrice` / `thirdBuyboxPrice` fields beyond spec → both surfaced
183
+ - `products.updateUnapproved`: spec marks only `barcode` required, but live endpoint returns HTTP 500 (`TypeError`) when too many optional fields are omitted → documented in JSDoc
184
+ - `brands.search`: docs claim case-sensitive exact match, live is substring + case-insensitive → documented in JSDoc
185
+ - `getBatchRequestResult`: returns `PROCESSING + empty items` for unknown batch IDs (not 404)
186
+
187
+ Each fix is pinned by a regression mock test using the exact STAGE shape.
188
+
189
+ ## Authentication
190
+
191
+ 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).
192
+
193
+ **Production vs Stage have different credentials.** Stage also requires IP whitelisting — register your CI/server IP with Trendyol support (0850 258 58 00). The SDK auto-sends the 5 mandatory headers (`Authorization`, `x-clientip`, `x-correlationid`, `x-agentname`, `User-Agent`).
45
194
 
46
195
  ## Environments
47
196
 
@@ -50,9 +199,18 @@ Trendyol uses HTTP Basic Auth. Get your `sellerId`, `apiKey`, and `apiSecret` fr
50
199
  | `prod` | `https://apigw.trendyol.com` | No IP whitelist |
51
200
  | `stage` | `https://stageapigw.trendyol.com` | IP whitelist required — call Trendyol support |
52
201
 
202
+ ## Built-in robustness
203
+
204
+ - **Retry with exponential backoff** on 429 (respects `Retry-After`) and 5xx
205
+ - **Per-endpoint rate limiting** (token bucket) sized to Trendyol's documented limits — separate buckets for filter (2000/min), batch read (1000/min), buybox (1000/min), writes (1000/min), and delete (100/min)
206
+ - **Structured errors** via `@lonca/core` (`AuthError`, `RateLimitError`, `NotFoundError`, `ServerError`, `ValidationError`, `NetworkError`, `TimeoutError`)
207
+ - **Client-side validation** before the network: empty batches, oversized batches (>1000 items), >10 buybox barcodes throw `ValidationError`
208
+ - **Correlation ID** auto-generated per request for Trendyol-side log tracing
209
+ - **`AbortSignal` support** throughout
210
+
53
211
  ## Stability
54
212
 
55
- `0.x` — alpha. Public APIs may change between minor versions until `1.0.0`.
213
+ `0.x` — alpha. The product surface is feature-complete and STAGE-verified, but public types may still adjust between minor versions until `1.0.0`.
56
214
 
57
215
  ## License
58
216
 
package/dist/index.cjs CHANGED
@@ -359,8 +359,9 @@ function normalizeCustomer(node) {
359
359
  return out;
360
360
  }
361
361
  function normalizePackage(node) {
362
+ const pkgId = node.shipmentPackageId ?? node.id;
362
363
  const out = {
363
- id: node.shipmentPackageId !== void 0 ? String(node.shipmentPackageId) : "",
364
+ id: pkgId !== void 0 ? String(pkgId) : "",
364
365
  orderNumber: node.orderNumber ?? "",
365
366
  status: node.status ?? "",
366
367
  customer: normalizeCustomer(node),
@@ -468,6 +469,342 @@ var OrdersResource = class {
468
469
  }
469
470
  return result;
470
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
+ }
471
808
  };
472
809
  var DEFAULT_PAGE_SIZE3 = 50;
473
810
  var MAX_PAGE_SIZE2 = 1e3;