@thorprovider/medusa-extended 1.0.0 → 1.1.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.
@@ -0,0 +1,1072 @@
1
+ /**
2
+ * @fileoverview Tests for DropshipperClient
3
+ *
4
+ * Uses msw to intercept fetch calls and verify that each method:
5
+ * - Hits the correct URL and HTTP method
6
+ * - Sends `Authorization: Bearer <token>`
7
+ * - Forwards query-string options correctly
8
+ * - Returns the parsed response body
9
+ * - Throws ProviderAPIError on non-2xx responses
10
+ */
11
+
12
+ import { describe, expect, it } from 'vitest'
13
+ import { http, HttpResponse } from 'msw'
14
+ import { server } from '../stelorder/client.test-helpers'
15
+ import { DropshipperClient } from './dropshipper'
16
+
17
+ const BASE = 'https://medusa.example.com'
18
+ const TOKEN = 'test-jwt-token'
19
+
20
+ function client() {
21
+ return new DropshipperClient({ baseUrl: BASE, token: TOKEN })
22
+ }
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Helpers
26
+ // ---------------------------------------------------------------------------
27
+
28
+ function requireBearerToken(request: Request) {
29
+ expect(request.headers.get('Authorization')).toBe(`Bearer ${TOKEN}`)
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Onboarding (Solo X)
34
+ // ---------------------------------------------------------------------------
35
+
36
+ describe('DropshipperClient.onboardDropshipper', () => {
37
+ it('POST /admin/thor/dropshipper/onboard', async () => {
38
+ server.use(
39
+ http.post(`${BASE}/admin/thor/dropshipper/onboard`, async ({ request }) => {
40
+ requireBearerToken(request)
41
+ const body = await request.json() as any
42
+ expect(body.business_name).toBe('Acme Drops')
43
+ return HttpResponse.json({
44
+ dropshipper: {
45
+ user_id: 'usr_1',
46
+ email: 'acme@drops.com',
47
+ sales_channel_id: 'sc_01',
48
+ sales_channel_name: 'Acme Drops',
49
+ account_id: 'acc_01',
50
+ price_list_id: 'pl_01',
51
+ publishable_key_token: 'pk_live_xxx',
52
+ role: 'Dropshipper',
53
+ },
54
+ })
55
+ }),
56
+ )
57
+
58
+ const result = await client().onboardDropshipper({
59
+ business_name: 'Acme Drops',
60
+ email: 'acme@drops.com',
61
+ password: 'secret',
62
+ })
63
+ expect(result.dropshipper.account_id).toBe('acc_01')
64
+ expect(result.dropshipper.role).toBe('Dropshipper')
65
+ })
66
+ })
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Variant Costs (Solo X)
70
+ // ---------------------------------------------------------------------------
71
+
72
+ describe('DropshipperClient.getVariantCosts', () => {
73
+ it('GET /admin/thor/dropshipper/variant-costs with defaults', async () => {
74
+ server.use(
75
+ http.get(`${BASE}/admin/thor/dropshipper/variant-costs`, ({ request }) => {
76
+ requireBearerToken(request)
77
+ const url = new URL(request.url)
78
+ expect(url.searchParams.get('limit')).toBe('20')
79
+ expect(url.searchParams.get('offset')).toBe('0')
80
+ return HttpResponse.json({ variant_costs: [], count: 0, offset: 0, limit: 20 })
81
+ }),
82
+ )
83
+
84
+ const result = await client().getVariantCosts()
85
+ expect(result.count).toBe(0)
86
+ })
87
+
88
+ it('forwards account_id and currency_code filters', async () => {
89
+ server.use(
90
+ http.get(`${BASE}/admin/thor/dropshipper/variant-costs`, ({ request }) => {
91
+ const url = new URL(request.url)
92
+ expect(url.searchParams.get('account_id')).toBe('acc_01')
93
+ expect(url.searchParams.get('currency_code')).toBe('usd')
94
+ return HttpResponse.json({ variant_costs: [], count: 0, offset: 0, limit: 20 })
95
+ }),
96
+ )
97
+
98
+ await client().getVariantCosts({ account_id: 'acc_01', currency_code: 'usd' })
99
+ })
100
+ })
101
+
102
+ describe('DropshipperClient.createVariantCost', () => {
103
+ it('POST /admin/thor/dropshipper/variant-costs', async () => {
104
+ server.use(
105
+ http.post(`${BASE}/admin/thor/dropshipper/variant-costs`, async ({ request }) => {
106
+ requireBearerToken(request)
107
+ const body = await request.json() as any
108
+ expect(body.variant_id).toBe('var_1')
109
+ return HttpResponse.json({
110
+ variant_cost: {
111
+ id: 'vc_1',
112
+ account_id: 'acc_01',
113
+ variant_id: 'var_1',
114
+ cost_amount: 1000,
115
+ currency_code: 'usd',
116
+ updated_at: '2026-01-01T00:00:00Z',
117
+ },
118
+ })
119
+ }),
120
+ )
121
+
122
+ const result = await client().createVariantCost({
123
+ account_id: 'acc_01',
124
+ variant_id: 'var_1',
125
+ cost_amount: 1000,
126
+ currency_code: 'usd',
127
+ })
128
+ expect(result.variant_cost.id).toBe('vc_1')
129
+ })
130
+ })
131
+
132
+ describe('DropshipperClient.batchVariantCosts', () => {
133
+ it('POST /admin/thor/dropshipper/variant-costs/batch', async () => {
134
+ server.use(
135
+ http.post(`${BASE}/admin/thor/dropshipper/variant-costs/batch`, async ({ request }) => {
136
+ requireBearerToken(request)
137
+ const body = await request.json() as any
138
+ expect(body.costs).toHaveLength(2)
139
+ return HttpResponse.json({ updated: 2, errors: [] })
140
+ }),
141
+ )
142
+
143
+ const result = await client().batchVariantCosts({
144
+ account_id: 'acc_01',
145
+ currency_code: 'usd',
146
+ costs: [
147
+ { variant_id: 'var_1', cost_amount: 1000 },
148
+ { variant_id: 'var_2', cost_amount: 2000 },
149
+ ],
150
+ })
151
+ expect(result.updated).toBe(2)
152
+ expect(result.errors).toHaveLength(0)
153
+ })
154
+ })
155
+
156
+ describe('DropshipperClient.deleteVariantCost', () => {
157
+ it('DELETE /admin/thor/dropshipper/variant-costs/:id', async () => {
158
+ server.use(
159
+ http.delete(`${BASE}/admin/thor/dropshipper/variant-costs/vc_1`, ({ request }) => {
160
+ requireBearerToken(request)
161
+ return HttpResponse.json({ deleted: true, id: 'vc_1' })
162
+ }),
163
+ )
164
+
165
+ const result = await client().deleteVariantCost('vc_1')
166
+ expect(result.deleted).toBe(true)
167
+ expect(result.id).toBe('vc_1')
168
+ })
169
+ })
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Products (Solo Y)
173
+ // ---------------------------------------------------------------------------
174
+
175
+ describe('DropshipperClient.getProducts', () => {
176
+ it('GET /admin/thor/dropshipper/products with defaults', async () => {
177
+ server.use(
178
+ http.get(`${BASE}/admin/thor/dropshipper/products`, ({ request }) => {
179
+ requireBearerToken(request)
180
+ const url = new URL(request.url)
181
+ expect(url.searchParams.get('limit')).toBe('20')
182
+ return HttpResponse.json({ products: [], count: 0, offset: 0, limit: 20 })
183
+ }),
184
+ )
185
+
186
+ const result = await client().getProducts()
187
+ expect(result.products).toHaveLength(0)
188
+ })
189
+
190
+ it('forwards search and category filters', async () => {
191
+ server.use(
192
+ http.get(`${BASE}/admin/thor/dropshipper/products`, ({ request }) => {
193
+ const url = new URL(request.url)
194
+ expect(url.searchParams.get('q')).toBe('shirt')
195
+ expect(url.searchParams.get('category_id')).toBe('cat_1')
196
+ return HttpResponse.json({ products: [], count: 0, offset: 0, limit: 20 })
197
+ }),
198
+ )
199
+
200
+ await client().getProducts({ q: 'shirt', category_id: 'cat_1' })
201
+ })
202
+ })
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // Prices (Solo Y)
206
+ // ---------------------------------------------------------------------------
207
+
208
+ describe('DropshipperClient.updatePrices', () => {
209
+ it('PUT /admin/thor/dropshipper/prices', async () => {
210
+ server.use(
211
+ http.put(`${BASE}/admin/thor/dropshipper/prices`, async ({ request }) => {
212
+ requireBearerToken(request)
213
+ const body = await request.json() as any
214
+ expect(body.currency_code).toBe('usd')
215
+ expect(body.prices).toHaveLength(1)
216
+ return HttpResponse.json({ updated: 1, price_list_id: 'pl_01' })
217
+ }),
218
+ )
219
+
220
+ const result = await client().updatePrices({
221
+ currency_code: 'usd',
222
+ prices: [{ variant_id: 'var_1', amount: 5000 }],
223
+ })
224
+ expect(result.updated).toBe(1)
225
+ expect(result.price_list_id).toBe('pl_01')
226
+ })
227
+ })
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // Orders (Y + X)
231
+ // ---------------------------------------------------------------------------
232
+
233
+ describe('DropshipperClient.getOrders', () => {
234
+ it('GET /admin/thor/dropshipper/orders with defaults', async () => {
235
+ server.use(
236
+ http.get(`${BASE}/admin/thor/dropshipper/orders`, ({ request }) => {
237
+ requireBearerToken(request)
238
+ return HttpResponse.json({ orders: [], count: 0, offset: 0, limit: 20 })
239
+ }),
240
+ )
241
+
242
+ const result = await client().getOrders()
243
+ expect(result.orders).toHaveLength(0)
244
+ })
245
+
246
+ it('forwards status and settlement_status filters', async () => {
247
+ server.use(
248
+ http.get(`${BASE}/admin/thor/dropshipper/orders`, ({ request }) => {
249
+ const url = new URL(request.url)
250
+ expect(url.searchParams.get('status')).toBe('completed')
251
+ expect(url.searchParams.get('settlement_status')).toBe('unsettled')
252
+ return HttpResponse.json({ orders: [], count: 0, offset: 0, limit: 20 })
253
+ }),
254
+ )
255
+
256
+ await client().getOrders({ status: 'completed', settlement_status: 'unsettled' })
257
+ })
258
+ })
259
+
260
+ describe('DropshipperClient.getOrder', () => {
261
+ it('GET /admin/thor/dropshipper/orders/:id', async () => {
262
+ server.use(
263
+ http.get(`${BASE}/admin/thor/dropshipper/orders/ord_1`, ({ request }) => {
264
+ requireBearerToken(request)
265
+ return HttpResponse.json({
266
+ order: {
267
+ id: 'ord_1',
268
+ display_id: 42,
269
+ status: 'completed',
270
+ fulfillment_status: 'fulfilled',
271
+ created_at: '2026-01-01T00:00:00Z',
272
+ currency_code: 'usd',
273
+ customer: { id: 'cus_1', full_name: 'John Doe', email: 'john@doe.com' },
274
+ shipping_address: { full_name: 'John Doe', address_1: '123 Main', city: 'NY', province: null, postal_code: '10001', country_code: 'us' },
275
+ items: [],
276
+ totals: { subtotal: 5000, shipping_total: 0, discount_total: 0, total: 5000, cost_total: 2000, profit: 3000, margin_percent: 60 },
277
+ payment_collected_by: 'dropshipper',
278
+ settlement_status: 'unsettled',
279
+ timeline: [],
280
+ },
281
+ })
282
+ }),
283
+ )
284
+
285
+ const result = await client().getOrder('ord_1')
286
+ expect(result.order.id).toBe('ord_1')
287
+ expect(result.order.totals.profit).toBe(3000)
288
+ })
289
+ })
290
+
291
+ describe('DropshipperClient.createOrder', () => {
292
+ it('POST /admin/thor/dropshipper/orders', async () => {
293
+ server.use(
294
+ http.post(`${BASE}/admin/thor/dropshipper/orders`, async ({ request }) => {
295
+ requireBearerToken(request)
296
+ const body = await request.json() as any
297
+ expect(body.customer_id).toBe('cus_1')
298
+ return HttpResponse.json({
299
+ order: { id: 'ord_new', display_id: 43, status: 'pending', total: 5000, profit: 2000, created_at: '2026-01-01T00:00:00Z' },
300
+ })
301
+ }),
302
+ )
303
+
304
+ const result = await client().createOrder({
305
+ customer_id: 'cus_1',
306
+ currency_code: 'usd',
307
+ items: [{ variant_id: 'var_1', quantity: 1 }],
308
+ shipping_address: { full_name: 'John', address_1: '123 Main', city: 'NY', province: null, postal_code: '10001', country_code: 'us' },
309
+ })
310
+ expect(result.order.id).toBe('ord_new')
311
+ })
312
+ })
313
+
314
+ describe('DropshipperClient.setPaymentCollector', () => {
315
+ it('POST /admin/thor/dropshipper/orders/:id/set-payment-collector', async () => {
316
+ server.use(
317
+ http.post(`${BASE}/admin/thor/dropshipper/orders/ord_1/set-payment-collector`, async ({ request }) => {
318
+ requireBearerToken(request)
319
+ const body = await request.json() as any
320
+ expect(body.payment_collected_by).toBe('provider')
321
+ return HttpResponse.json({ order_id: 'ord_1', payment_collected_by: 'provider', updated_at: '2026-01-02T00:00:00Z' })
322
+ }),
323
+ )
324
+
325
+ const result = await client().setPaymentCollector('ord_1', { payment_collected_by: 'provider' })
326
+ expect(result.payment_collected_by).toBe('provider')
327
+ })
328
+ })
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // Orders — Cancel/Edit (Y + X)
332
+ // ---------------------------------------------------------------------------
333
+
334
+
335
+ // ---------------------------------------------------------------------------
336
+ // Order Notes
337
+ // ---------------------------------------------------------------------------
338
+
339
+ describe('DropshipperClient.getOrderNotes', () => {
340
+ it('GET /admin/thor/dropshipper/orders/:id/notes', async () => {
341
+ server.use(
342
+ http.get(`${BASE}/admin/thor/dropshipper/orders/ord_1/notes`, ({ request }) => {
343
+ requireBearerToken(request)
344
+ return HttpResponse.json({
345
+ notes: [
346
+ {
347
+ id: 'note_1',
348
+ order_id: 'ord_1',
349
+ author_id: 'usr_1',
350
+ text: 'Customer requested expedited shipping',
351
+ created_at: '2026-01-01T10:00:00Z',
352
+ },
353
+ {
354
+ id: 'note_2',
355
+ order_id: 'ord_1',
356
+ author_id: 'usr_2',
357
+ text: 'Order confirmed and ready to ship',
358
+ created_at: '2026-01-01T11:00:00Z',
359
+ },
360
+ ],
361
+ })
362
+ }),
363
+ )
364
+
365
+ const result = await client().getOrderNotes('ord_1')
366
+ expect(result.notes).toHaveLength(2)
367
+ expect(result.notes[0].id).toBe('note_1')
368
+ expect(result.notes[0].text).toBe('Customer requested expedited shipping')
369
+ expect(result.notes[1].id).toBe('note_2')
370
+ })
371
+
372
+ it('GET /admin/thor/dropshipper/orders/:id/notes returns empty list', async () => {
373
+ server.use(
374
+ http.get(`${BASE}/admin/thor/dropshipper/orders/ord_2/notes`, ({ request }) => {
375
+ requireBearerToken(request)
376
+ return HttpResponse.json({ notes: [] })
377
+ }),
378
+ )
379
+
380
+ const result = await client().getOrderNotes('ord_2')
381
+ expect(result.notes).toHaveLength(0)
382
+ })
383
+ })
384
+
385
+ describe('DropshipperClient.createOrderNote', () => {
386
+ it('POST /admin/thor/dropshipper/orders/:id/notes', async () => {
387
+ server.use(
388
+ http.post(`${BASE}/admin/thor/dropshipper/orders/ord_1/notes`, async ({ request }) => {
389
+ requireBearerToken(request)
390
+ const body = await request.json() as any
391
+ expect(body.text).toBe('Shipped via FedEx')
392
+ return HttpResponse.json({
393
+ id: 'note_3',
394
+ order_id: 'ord_1',
395
+ author_id: 'usr_1',
396
+ text: 'Shipped via FedEx',
397
+ created_at: '2026-01-02T09:00:00Z',
398
+ })
399
+ }),
400
+ )
401
+
402
+ const result = await client().createOrderNote('ord_1', { text: 'Shipped via FedEx' })
403
+ expect(result.id).toBe('note_3')
404
+ expect(result.order_id).toBe('ord_1')
405
+ expect(result.text).toBe('Shipped via FedEx')
406
+ expect(result.author_id).toBe('usr_1')
407
+ })
408
+
409
+ it('POST /admin/thor/dropshipper/orders/:id/notes with multiline text', async () => {
410
+ server.use(
411
+ http.post(`${BASE}/admin/thor/dropshipper/orders/ord_1/notes`, async ({ request }) => {
412
+ requireBearerToken(request)
413
+ const body = await request.json() as any
414
+ expect(body.text).toContain('Line 1')
415
+ expect(body.text).toContain('Line 2')
416
+ return HttpResponse.json({
417
+ id: 'note_4',
418
+ order_id: 'ord_1',
419
+ author_id: 'usr_2',
420
+ text: body.text,
421
+ created_at: '2026-01-02T10:00:00Z',
422
+ })
423
+ }),
424
+ )
425
+
426
+ const result = await client().createOrderNote('ord_1', { text: 'Line 1\nLine 2' })
427
+ expect(result.id).toBe('note_4')
428
+ })
429
+ })
430
+
431
+ describe('DropshipperClient.deleteOrderNote', () => {
432
+ it('DELETE /admin/thor/dropshipper/orders/:id/notes/:noteId', async () => {
433
+ server.use(
434
+ http.delete(`${BASE}/admin/thor/dropshipper/orders/ord_1/notes/note_1`, ({ request }) => {
435
+ requireBearerToken(request)
436
+ return HttpResponse.json({ id: 'note_1', deleted: true })
437
+ }),
438
+ )
439
+
440
+ const result = await client().deleteOrderNote('ord_1', 'note_1')
441
+ expect(result.deleted).toBe(true)
442
+ expect(result.id).toBe('note_1')
443
+ })
444
+
445
+ it('DELETE /admin/thor/dropshipper/orders/:id/notes/:noteId throws on 404', async () => {
446
+ server.use(
447
+ http.delete(`${BASE}/admin/thor/dropshipper/orders/ord_1/notes/note_missing`, () =>
448
+ HttpResponse.json({ message: 'Note not found' }, { status: 404 }),
449
+ ),
450
+ )
451
+
452
+ await expect(client().deleteOrderNote('ord_1', 'note_missing')).rejects.toMatchObject({
453
+ name: 'ProviderAPIError',
454
+ provider: 'DROPSHIPPER_API_404',
455
+ })
456
+ })
457
+ })
458
+
459
+ // ---------------------------------------------------------------------------
460
+ // Categories (Solo Y)
461
+ // ---------------------------------------------------------------------------
462
+
463
+ describe('DropshipperClient.getCategories', () => {
464
+ it('GET /admin/thor/dropshipper/categories', async () => {
465
+ server.use(
466
+ http.get(`${BASE}/admin/thor/dropshipper/categories`, ({ request }) => {
467
+ requireBearerToken(request)
468
+ return HttpResponse.json({ categories: [] })
469
+ }),
470
+ )
471
+
472
+ const result = await client().getCategories()
473
+ expect(result.categories).toHaveLength(0)
474
+ })
475
+
476
+ it('forwards parent_id and include_children', async () => {
477
+ server.use(
478
+ http.get(`${BASE}/admin/thor/dropshipper/categories`, ({ request }) => {
479
+ const url = new URL(request.url)
480
+ expect(url.searchParams.get('parent_id')).toBe('cat_root')
481
+ expect(url.searchParams.get('include_children')).toBe('true')
482
+ return HttpResponse.json({ categories: [] })
483
+ }),
484
+ )
485
+
486
+ await client().getCategories({ parent_id: 'cat_root', include_children: true })
487
+ })
488
+ })
489
+
490
+ describe('DropshipperClient.createCategory', () => {
491
+ it('POST /admin/thor/dropshipper/categories', async () => {
492
+ server.use(
493
+ http.post(`${BASE}/admin/thor/dropshipper/categories`, async ({ request }) => {
494
+ requireBearerToken(request)
495
+ const body = await request.json() as any
496
+ expect(body.name).toBe('Summer')
497
+ return HttpResponse.json({ id: 'cat_1', name: 'Summer', handle: 'summer', position: 0, parent_id: null })
498
+ }),
499
+ )
500
+
501
+ const result = await client().createCategory({ name: 'Summer' })
502
+ expect(result.id).toBe('cat_1')
503
+ })
504
+ })
505
+
506
+ describe('DropshipperClient.deleteCategory', () => {
507
+ it('DELETE /admin/thor/dropshipper/categories/:id', async () => {
508
+ server.use(
509
+ http.delete(`${BASE}/admin/thor/dropshipper/categories/cat_1`, ({ request }) => {
510
+ requireBearerToken(request)
511
+ return HttpResponse.json({ deleted: true, id: 'cat_1', unmapped_products: 3 })
512
+ }),
513
+ )
514
+
515
+ const result = await client().deleteCategory('cat_1')
516
+ expect(result.deleted).toBe(true)
517
+ expect(result.unmapped_products).toBe(3)
518
+ })
519
+ })
520
+
521
+ describe('DropshipperClient.createCategoryMapping', () => {
522
+ it('POST /admin/thor/dropshipper/category-mappings', async () => {
523
+ server.use(
524
+ http.post(`${BASE}/admin/thor/dropshipper/category-mappings`, async ({ request }) => {
525
+ requireBearerToken(request)
526
+ const body = await request.json() as any
527
+ expect(body.category_id).toBe('cat_1')
528
+ expect(body.product_id).toBe('prod_1')
529
+ return HttpResponse.json({
530
+ mapping: { id: 'map_1', category_id: 'cat_1', product_id: 'prod_1', created_at: '2026-01-01T00:00:00Z' },
531
+ })
532
+ }),
533
+ )
534
+
535
+ const result = await client().createCategoryMapping({ category_id: 'cat_1', product_id: 'prod_1' })
536
+ expect(result.mapping.id).toBe('map_1')
537
+ })
538
+ })
539
+
540
+ // ---------------------------------------------------------------------------
541
+ // Customers (Solo Y)
542
+ // ---------------------------------------------------------------------------
543
+
544
+ describe('DropshipperClient.getCustomers', () => {
545
+ it('GET /admin/thor/dropshipper/customers with defaults', async () => {
546
+ server.use(
547
+ http.get(`${BASE}/admin/thor/dropshipper/customers`, ({ request }) => {
548
+ requireBearerToken(request)
549
+ const url = new URL(request.url)
550
+ expect(url.searchParams.get('limit')).toBe('20')
551
+ return HttpResponse.json({ customers: [], count: 0, offset: 0, limit: 20 })
552
+ }),
553
+ )
554
+
555
+ const result = await client().getCustomers()
556
+ expect(result.count).toBe(0)
557
+ })
558
+ })
559
+
560
+ describe('DropshipperClient.createCustomer', () => {
561
+ it('POST /admin/thor/dropshipper/customers', async () => {
562
+ server.use(
563
+ http.post(`${BASE}/admin/thor/dropshipper/customers`, async ({ request }) => {
564
+ requireBearerToken(request)
565
+ const body = await request.json() as any
566
+ expect(body.email).toBe('customer@example.com')
567
+ return HttpResponse.json({
568
+ customer: { id: 'cus_new', email: 'customer@example.com', first_name: null, last_name: null, sales_channel_id: 'sc_01', created_at: '2026-01-01T00:00:00Z' },
569
+ })
570
+ }),
571
+ )
572
+
573
+ const result = await client().createCustomer({ email: 'customer@example.com', password: 'pass' })
574
+ expect(result.customer.id).toBe('cus_new')
575
+ })
576
+ })
577
+
578
+ // ---------------------------------------------------------------------------
579
+ // Customer Addresses (Solo Y)
580
+ // ---------------------------------------------------------------------------
581
+
582
+
583
+ // ---------------------------------------------------------------------------
584
+ // Account & Settlements (Y)
585
+ // ---------------------------------------------------------------------------
586
+
587
+ describe('DropshipperClient.getAccount', () => {
588
+ it('GET /admin/thor/dropshipper/account', async () => {
589
+ server.use(
590
+ http.get(`${BASE}/admin/thor/dropshipper/account`, ({ request }) => {
591
+ requireBearerToken(request)
592
+ return HttpResponse.json({
593
+ account: {
594
+ id: 'acc_01',
595
+ provider_name: 'Acme',
596
+ currency_code: 'usd',
597
+ payment_terms_days: 15,
598
+ balance: { payable_to_provider: 10000, receivable_from_provider: 5000, net_balance: -5000, net_balance_label: 'You owe' },
599
+ pending_orders: { payable_count: 2, payable_total: 10000, receivable_count: 1, receivable_total: 5000 },
600
+ },
601
+ })
602
+ }),
603
+ )
604
+
605
+ const result = await client().getAccount()
606
+ expect(result.account.id).toBe('acc_01')
607
+ expect(result.account.balance.net_balance).toBe(-5000)
608
+ })
609
+ })
610
+
611
+ describe('DropshipperClient.getSettlements', () => {
612
+ it('GET /admin/thor/dropshipper/settlements with defaults', async () => {
613
+ server.use(
614
+ http.get(`${BASE}/admin/thor/dropshipper/settlements`, ({ request }) => {
615
+ requireBearerToken(request)
616
+ const url = new URL(request.url)
617
+ expect(url.searchParams.get('limit')).toBe('20')
618
+ return HttpResponse.json({ settlements: [], count: 0, offset: 0, limit: 20 })
619
+ }),
620
+ )
621
+
622
+ const result = await client().getSettlements()
623
+ expect(result.settlements).toHaveLength(0)
624
+ })
625
+
626
+ it('forwards status and account_id filters', async () => {
627
+ server.use(
628
+ http.get(`${BASE}/admin/thor/dropshipper/settlements`, ({ request }) => {
629
+ const url = new URL(request.url)
630
+ expect(url.searchParams.get('status')).toBe('pending')
631
+ expect(url.searchParams.get('account_id')).toBe('acc_01')
632
+ return HttpResponse.json({ settlements: [], count: 0, offset: 0, limit: 20 })
633
+ }),
634
+ )
635
+
636
+ await client().getSettlements({ status: 'pending', account_id: 'acc_01' })
637
+ })
638
+ })
639
+
640
+ // ---------------------------------------------------------------------------
641
+ // Admin Settlements (Solo X)
642
+ // ---------------------------------------------------------------------------
643
+
644
+ describe('DropshipperClient.createSettlement', () => {
645
+ it('POST /admin/thor/dropshipper/admin/settlements', async () => {
646
+ server.use(
647
+ http.post(`${BASE}/admin/thor/dropshipper/admin/settlements`, async ({ request }) => {
648
+ requireBearerToken(request)
649
+ const body = await request.json() as any
650
+ expect(body.account_id).toBe('acc_01')
651
+ expect(body.order_ids).toHaveLength(2)
652
+ return HttpResponse.json({
653
+ settlement: { id: 'stl_1', type: 'payment_to_provider', amount: 5000, status: 'pending', notes: null, created_at: '2026-01-01T00:00:00Z', settled_at: null, confirmed_by: null },
654
+ })
655
+ }),
656
+ )
657
+
658
+ const result = await client().createSettlement({
659
+ account_id: 'acc_01',
660
+ type: 'payment_to_provider',
661
+ order_ids: ['ord_1', 'ord_2'],
662
+ amount: 5000,
663
+ })
664
+ expect(result.settlement.id).toBe('stl_1')
665
+ })
666
+ })
667
+
668
+ describe('DropshipperClient.confirmSettlement', () => {
669
+ it('POST /admin/thor/dropshipper/admin/settlements/:id/confirm', async () => {
670
+ server.use(
671
+ http.post(`${BASE}/admin/thor/dropshipper/admin/settlements/stl_1/confirm`, async ({ request }) => {
672
+ requireBearerToken(request)
673
+ return HttpResponse.json({
674
+ settlement: { id: 'stl_1', type: 'payment_to_provider', amount: 5000, status: 'confirmed', notes: 'done', created_at: '2026-01-01T00:00:00Z', settled_at: '2026-01-02T00:00:00Z', confirmed_by: 'usr_admin' },
675
+ })
676
+ }),
677
+ )
678
+
679
+ const result = await client().confirmSettlement('stl_1', { notes: 'done' })
680
+ expect(result.settlement.status).toBe('confirmed')
681
+ })
682
+ })
683
+
684
+ // ---------------------------------------------------------------------------
685
+ // Admin Settlements list (Solo X)
686
+ // ---------------------------------------------------------------------------
687
+
688
+ describe('DropshipperClient.getAdminSettlements', () => {
689
+ it('GET /admin/thor/dropshipper/admin/settlements with defaults', async () => {
690
+ server.use(
691
+ http.get(`${BASE}/admin/thor/dropshipper/admin/settlements`, ({ request }) => {
692
+ requireBearerToken(request)
693
+ const url = new URL(request.url)
694
+ expect(url.searchParams.get('limit')).toBe('20')
695
+ expect(url.searchParams.get('offset')).toBe('0')
696
+ return HttpResponse.json({ settlements: [], count: 0, offset: 0, limit: 20 })
697
+ }),
698
+ )
699
+
700
+ const result = await client().getAdminSettlements()
701
+ expect(result.settlements).toHaveLength(0)
702
+ })
703
+
704
+ it('forwards status and account_id filters', async () => {
705
+ server.use(
706
+ http.get(`${BASE}/admin/thor/dropshipper/admin/settlements`, ({ request }) => {
707
+ const url = new URL(request.url)
708
+ expect(url.searchParams.get('status')).toBe('pending')
709
+ expect(url.searchParams.get('account_id')).toBe('acc_01')
710
+ return HttpResponse.json({ settlements: [], count: 0, offset: 0, limit: 20 })
711
+ }),
712
+ )
713
+
714
+ await client().getAdminSettlements({ status: 'pending', account_id: 'acc_01' })
715
+ })
716
+ })
717
+
718
+ // ---------------------------------------------------------------------------
719
+ // Admin Account Management (Solo X)
720
+ // ---------------------------------------------------------------------------
721
+
722
+ describe('DropshipperClient.getAdminAccounts', () => {
723
+ it('GET /admin/thor/dropshipper/admin/accounts', async () => {
724
+ server.use(
725
+ http.get(`${BASE}/admin/thor/dropshipper/admin/accounts`, ({ request }) => {
726
+ requireBearerToken(request)
727
+ return HttpResponse.json({ accounts: [{ id: 'acc_01', provider_name: 'Acme', sales_channel_id: 'sc_01', sales_channel_name: 'Acme', currency_code: 'usd', payment_terms_days: 15 }] })
728
+ }),
729
+ )
730
+
731
+ const result = await client().getAdminAccounts()
732
+ expect(result.accounts).toHaveLength(1)
733
+ expect(result.accounts[0].id).toBe('acc_01')
734
+ })
735
+ })
736
+
737
+ describe('DropshipperClient.getAdminAccountBalance', () => {
738
+ it('GET /admin/thor/dropshipper/admin/accounts/:id/balance', async () => {
739
+ server.use(
740
+ http.get(`${BASE}/admin/thor/dropshipper/admin/accounts/acc_01/balance`, ({ request }) => {
741
+ requireBearerToken(request)
742
+ return HttpResponse.json({
743
+ account: { id: 'acc_01', provider_name: 'Acme', currency_code: 'usd', payment_terms_days: 15, sales_channel_id: 'sc_01' },
744
+ balance: {
745
+ payable_to_provider: 10000, receivable_from_provider: 0, net_balance: -10000, net_balance_label: 'Owes',
746
+ payable_count: 2, payable_order_ids: ['ord_1', 'ord_2'], payable_orders: [],
747
+ receivable_count: 0, receivable_order_ids: [], receivable_orders: [],
748
+ unclassified_orders: 0,
749
+ },
750
+ })
751
+ }),
752
+ )
753
+
754
+ const result = await client().getAdminAccountBalance('acc_01')
755
+ expect(result.balance.payable_count).toBe(2)
756
+ })
757
+ })
758
+
759
+ // ---------------------------------------------------------------------------
760
+ // Promotions (Solo Y)
761
+ // ---------------------------------------------------------------------------
762
+
763
+ describe('DropshipperClient.getPromotions', () => {
764
+ it('GET /admin/thor/dropshipper/promotions with defaults', async () => {
765
+ server.use(
766
+ http.get(`${BASE}/admin/thor/dropshipper/promotions`, ({ request }) => {
767
+ requireBearerToken(request)
768
+ const url = new URL(request.url)
769
+ expect(url.searchParams.get('limit')).toBe('20')
770
+ return HttpResponse.json({ promotions: [], count: 0, offset: 0, limit: 20 })
771
+ }),
772
+ )
773
+
774
+ const result = await client().getPromotions()
775
+ expect(result.promotions).toHaveLength(0)
776
+ })
777
+ })
778
+
779
+ describe('DropshipperClient.createPromotion', () => {
780
+ it('POST /admin/thor/dropshipper/promotions', async () => {
781
+ server.use(
782
+ http.post(`${BASE}/admin/thor/dropshipper/promotions`, async ({ request }) => {
783
+ requireBearerToken(request)
784
+ const body = await request.json() as any
785
+ expect(body.code).toBe('SUMMER10')
786
+ expect(body.type).toBe('percentage')
787
+ return HttpResponse.json({
788
+ id: 'promo_1', code: 'SUMMER10', type: 'percentage', value: 10,
789
+ status: 'active', usage_limit: null, usage_count: 0, starts_at: null, ends_at: null,
790
+ })
791
+ }),
792
+ )
793
+
794
+ const result = await client().createPromotion({ code: 'SUMMER10', type: 'percentage', value: 10 })
795
+ expect(result.id).toBe('promo_1')
796
+ })
797
+ })
798
+
799
+ describe('DropshipperClient.getPromotion', () => {
800
+ it('GET /admin/thor/dropshipper/promotions/:id', async () => {
801
+ server.use(
802
+ http.get(`${BASE}/admin/thor/dropshipper/promotions/promo_1`, ({ request }) => {
803
+ requireBearerToken(request)
804
+ return HttpResponse.json({
805
+ id: 'promo_1', code: 'SUMMER10', type: 'percentage', value: 10,
806
+ status: 'active', usage_limit: null, usage_count: 5, starts_at: null, ends_at: null,
807
+ })
808
+ }),
809
+ )
810
+
811
+ const result = await client().getPromotion('promo_1')
812
+ expect(result.id).toBe('promo_1')
813
+ expect(result.usage_count).toBe(5)
814
+ })
815
+ })
816
+
817
+ describe('DropshipperClient.deletePromotion', () => {
818
+ it('DELETE /admin/thor/dropshipper/promotions/:id', async () => {
819
+ server.use(
820
+ http.delete(`${BASE}/admin/thor/dropshipper/promotions/promo_1`, ({ request }) => {
821
+ requireBearerToken(request)
822
+ return HttpResponse.json({ deleted: true, id: 'promo_1' })
823
+ }),
824
+ )
825
+
826
+ const result = await client().deletePromotion('promo_1')
827
+ expect(result.deleted).toBe(true)
828
+ expect(result.id).toBe('promo_1')
829
+ })
830
+ })
831
+
832
+ // ---------------------------------------------------------------------------
833
+ // Dashboard (Solo Y)
834
+ // ---------------------------------------------------------------------------
835
+
836
+ describe('DropshipperClient.getDashboardStats', () => {
837
+ it('GET /admin/thor/dropshipper/dashboard/stats with defaults', async () => {
838
+ server.use(
839
+ http.get(`${BASE}/admin/thor/dropshipper/dashboard/stats`, ({ request }) => {
840
+ requireBearerToken(request)
841
+ return HttpResponse.json({
842
+ stats: {
843
+ period: { from: '2026-04-21', to: '2026-05-21', label: '30d' },
844
+ current: { orders_count: 5, revenue_total: 25000, profit_total: 10000, avg_margin_percent: 40, avg_order_value: 5000, currency_code: 'usd' },
845
+ previous: { orders_count: 3, revenue_total: 15000, profit_total: 6000, avg_margin_percent: 40, avg_order_value: 5000, currency_code: 'usd' },
846
+ changes: { orders_count_pct: 66, revenue_total_pct: 66, profit_total_pct: 66, avg_margin_percent_pct: 0 },
847
+ orders_by_status: { completed: 4, pending: 1 },
848
+ settlement_summary: { payable_to_provider: 5000, receivable_from_provider: 0, net_balance: -5000, net_balance_label: 'Owes', pending_settlement_records: 1, currency_code: 'usd' },
849
+ recent_orders: [],
850
+ top_products_by_profit: [],
851
+ },
852
+ })
853
+ }),
854
+ )
855
+
856
+ const result = await client().getDashboardStats()
857
+ expect(result.stats?.current.orders_count).toBe(5)
858
+ })
859
+
860
+ it('forwards period and currency_code', async () => {
861
+ server.use(
862
+ http.get(`${BASE}/admin/thor/dropshipper/dashboard/stats`, ({ request }) => {
863
+ const url = new URL(request.url)
864
+ expect(url.searchParams.get('period')).toBe('7d')
865
+ expect(url.searchParams.get('currency_code')).toBe('mxn')
866
+ return HttpResponse.json({ stats: undefined })
867
+ }),
868
+ )
869
+
870
+ await client().getDashboardStats({ period: '7d', currency_code: 'mxn' })
871
+ })
872
+ })
873
+
874
+ // ---------------------------------------------------------------------------
875
+ // Error handling
876
+ // ---------------------------------------------------------------------------
877
+
878
+ describe('DropshipperClient error handling', () => {
879
+ it('throws ProviderAPIError on 401', async () => {
880
+ server.use(
881
+ http.get(`${BASE}/admin/thor/dropshipper/account`, () =>
882
+ HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }),
883
+ ),
884
+ )
885
+
886
+ await expect(client().getAccount()).rejects.toMatchObject({
887
+ name: 'ProviderAPIError',
888
+ provider: 'DROPSHIPPER_API_401',
889
+ })
890
+ })
891
+
892
+ it('throws ProviderAPIError on 403', async () => {
893
+ server.use(
894
+ http.post(`${BASE}/admin/thor/dropshipper/onboard`, () =>
895
+ HttpResponse.json({ message: 'Forbidden' }, { status: 403 }),
896
+ ),
897
+ )
898
+
899
+ await expect(
900
+ client().onboardDropshipper({ business_name: 'X', email: 'x@x.com', password: 'x' }),
901
+ ).rejects.toMatchObject({
902
+ name: 'ProviderAPIError',
903
+ provider: 'DROPSHIPPER_API_403',
904
+ })
905
+ })
906
+
907
+ it('throws ProviderAPIError on 404', async () => {
908
+ server.use(
909
+ http.get(`${BASE}/admin/thor/dropshipper/orders/ord_missing`, () =>
910
+ HttpResponse.json({ message: 'Order not found' }, { status: 404 }),
911
+ ),
912
+ )
913
+
914
+ await expect(client().getOrder('ord_missing')).rejects.toMatchObject({
915
+ name: 'ProviderAPIError',
916
+ provider: 'DROPSHIPPER_API_404',
917
+ })
918
+ })
919
+ })
920
+
921
+ // ---------------------------------------------------------------------------
922
+ // Order Cancel & Edits — B-04
923
+ // ---------------------------------------------------------------------------
924
+
925
+ describe('DropshipperClient.cancelOrder', () => {
926
+ it('POST /admin/thor/dropshipper/orders/:id/cancel', async () => {
927
+ server.use(
928
+ http.post(`${BASE}/admin/thor/dropshipper/orders/ord_1/cancel`, ({ request }) => {
929
+ requireBearerToken(request)
930
+ return HttpResponse.json({ order: { id: 'ord_1', status: 'canceled' } })
931
+ }),
932
+ )
933
+
934
+ const result = await client().cancelOrder('ord_1')
935
+ expect(result.order.id).toBe('ord_1')
936
+ expect(result.order.status).toBe('canceled')
937
+ })
938
+ })
939
+
940
+ describe('DropshipperClient.createOrderEdit', () => {
941
+ it('POST /admin/thor/dropshipper/orders/:id/edit', async () => {
942
+ server.use(
943
+ http.post(`${BASE}/admin/thor/dropshipper/orders/ord_1/edit`, async ({ request }) => {
944
+ requireBearerToken(request)
945
+ const body = await request.json() as any
946
+ expect(body.internal_note).toBe('Fix quantity')
947
+ return HttpResponse.json({
948
+ order_edit: { id: 'oe_1', order_id: 'ord_1', status: 'created' },
949
+ })
950
+ }),
951
+ )
952
+
953
+ const result = await client().createOrderEdit('ord_1', { internal_note: 'Fix quantity' })
954
+ expect(result.order_edit.id).toBe('oe_1')
955
+ expect(result.order_edit.order_id).toBe('ord_1')
956
+ })
957
+ })
958
+
959
+ describe('DropshipperClient.addOrderEditItem', () => {
960
+ it('POST /admin/thor/dropshipper/orders/:id/edit/items', async () => {
961
+ server.use(
962
+ http.post(`${BASE}/admin/thor/dropshipper/orders/ord_1/edit/items`, async ({ request }) => {
963
+ requireBearerToken(request)
964
+ const body = await request.json() as any
965
+ expect(body.variant_id).toBe('var_99')
966
+ expect(body.quantity).toBe(2)
967
+ return HttpResponse.json({ order_edit: { id: 'oe_1' } })
968
+ }),
969
+ )
970
+
971
+ const result = await client().addOrderEditItem('ord_1', {
972
+ variant_id: 'var_99',
973
+ quantity: 2,
974
+ })
975
+ expect(result.order_edit.id).toBe('oe_1')
976
+ })
977
+ })
978
+
979
+ describe('DropshipperClient.confirmOrderEdit', () => {
980
+ it('POST /admin/thor/dropshipper/orders/:id/edit/confirm', async () => {
981
+ server.use(
982
+ http.post(`${BASE}/admin/thor/dropshipper/orders/ord_1/edit/confirm`, ({ request }) => {
983
+ requireBearerToken(request)
984
+ return HttpResponse.json({ order: { id: 'ord_1' } })
985
+ }),
986
+ )
987
+
988
+ const result = await client().confirmOrderEdit('ord_1')
989
+ expect(result.order.id).toBe('ord_1')
990
+ })
991
+ })
992
+
993
+ // ---------------------------------------------------------------------------
994
+ // Customer Addresses — B-06
995
+ // ---------------------------------------------------------------------------
996
+
997
+ describe('DropshipperClient.getCustomerAddresses', () => {
998
+ it('GET /admin/thor/dropshipper/customers/:id/addresses', async () => {
999
+ server.use(
1000
+ http.get(`${BASE}/admin/thor/dropshipper/customers/cus_1/addresses`, ({ request }) => {
1001
+ requireBearerToken(request)
1002
+ return HttpResponse.json({
1003
+ addresses: [{ id: 'addr_1', first_name: 'John', sales_channel_id: 'sc_01' }],
1004
+ })
1005
+ }),
1006
+ )
1007
+
1008
+ const result = await client().getCustomerAddresses('cus_1')
1009
+ expect(result.addresses).toHaveLength(1)
1010
+ expect(result.addresses[0].id).toBe('addr_1')
1011
+ })
1012
+ })
1013
+
1014
+ describe('DropshipperClient.createCustomerAddress', () => {
1015
+ it('POST /admin/thor/dropshipper/customers/:id/addresses', async () => {
1016
+ const body = {
1017
+ first_name: 'John',
1018
+ last_name: 'Doe',
1019
+ address_1: '123 Main St',
1020
+ city: 'Bogota',
1021
+ postal_code: '110111',
1022
+ country_code: 'co',
1023
+ }
1024
+ server.use(
1025
+ http.post(`${BASE}/admin/thor/dropshipper/customers/cus_1/addresses`, async ({ request }) => {
1026
+ requireBearerToken(request)
1027
+ const reqBody = await request.json() as any
1028
+ expect(reqBody.first_name).toBe('John')
1029
+ return HttpResponse.json({
1030
+ address: { id: 'addr_new', ...reqBody, sales_channel_id: 'sc_01' },
1031
+ })
1032
+ }),
1033
+ )
1034
+
1035
+ const result = await client().createCustomerAddress('cus_1', body)
1036
+ expect(result.address.id).toBe('addr_new')
1037
+ expect(result.address.sales_channel_id).toBe('sc_01')
1038
+ })
1039
+ })
1040
+
1041
+ describe('DropshipperClient.updateCustomerAddress', () => {
1042
+ it('PUT /admin/thor/dropshipper/customers/:id/addresses/:addressId', async () => {
1043
+ server.use(
1044
+ http.put(`${BASE}/admin/thor/dropshipper/customers/cus_1/addresses/addr_1`, async ({ request }) => {
1045
+ requireBearerToken(request)
1046
+ const body = await request.json() as any
1047
+ expect(body.city).toBe('Medellin')
1048
+ return HttpResponse.json({
1049
+ address: { id: 'addr_1', city: 'Medellin', sales_channel_id: 'sc_01' },
1050
+ })
1051
+ }),
1052
+ )
1053
+
1054
+ const result = await client().updateCustomerAddress('cus_1', 'addr_1', { city: 'Medellin' })
1055
+ expect(result.address.city).toBe('Medellin')
1056
+ })
1057
+ })
1058
+
1059
+ describe('DropshipperClient.deleteCustomerAddress', () => {
1060
+ it('DELETE /admin/thor/dropshipper/customers/:id/addresses/:addressId', async () => {
1061
+ server.use(
1062
+ http.delete(`${BASE}/admin/thor/dropshipper/customers/cus_1/addresses/addr_1`, ({ request }) => {
1063
+ requireBearerToken(request)
1064
+ return HttpResponse.json({ deleted: true, id: 'addr_1' })
1065
+ }),
1066
+ )
1067
+
1068
+ const result = await client().deleteCustomerAddress('cus_1', 'addr_1')
1069
+ expect(result.deleted).toBe(true)
1070
+ expect(result.id).toBe('addr_1')
1071
+ })
1072
+ })