@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 +172 -14
- package/dist/index.cjs +338 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +296 -1
- package/dist/index.d.ts +296 -1
- package/dist/index.js +338 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,37 @@
|
|
|
1
1
|
# @lonca/trendyol
|
|
2
2
|
|
|
3
|
+
[](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
|
-
>
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
##
|
|
57
|
+
## End-to-end: create a product
|
|
33
58
|
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
118
|
+
## Per-resource cheat sheet
|
|
39
119
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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.
|
|
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:
|
|
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;
|