@thorprovider/medusa-extended 1.11.8 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +29 -4
- package/dist/index.d.ts +29 -4
- package/dist/index.js +73 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +73 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/dropshipper.live-smoke.test.ts +127 -73
- package/src/dropshipper.test.ts +90 -36
- package/src/dropshipper.ts +11 -14
- package/src/recovery.ts +85 -4
- package/src/request-resilience.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thorprovider/medusa-extended",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "Thor Commerce multi-tenant admin extensions for Medusa v2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@thorprovider/adapters": "^5.0.8",
|
|
28
|
-
"@thorprovider/types": "^5.
|
|
28
|
+
"@thorprovider/types": "^5.4.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"msw": "^2.12.3",
|
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
ExtendedCreateDropshipperOrderBody,
|
|
24
24
|
GetDropshipperProductsResponse,
|
|
25
25
|
GetDropshipperCustomersResponse,
|
|
26
|
+
GetDropshipperShippingOptionsBody,
|
|
26
27
|
GetDropshipperShippingOptionsResponse,
|
|
27
28
|
GetDropshipperAccountResponse,
|
|
28
29
|
} from '@thorprovider/types'
|
|
@@ -76,6 +77,28 @@ if (!runLive) {
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
/** Build a shipping-options request body for a variant (contract v2 POST). */
|
|
81
|
+
function buildShippingOptionsBody(
|
|
82
|
+
customerId: string,
|
|
83
|
+
variantId: string,
|
|
84
|
+
currencyCode: string,
|
|
85
|
+
address?: Partial<GetDropshipperShippingOptionsBody['shipping_address']>,
|
|
86
|
+
): GetDropshipperShippingOptionsBody {
|
|
87
|
+
return {
|
|
88
|
+
customer_id: customerId,
|
|
89
|
+
currency_code: currencyCode,
|
|
90
|
+
shipping_address: {
|
|
91
|
+
address_1: '123 Test Street',
|
|
92
|
+
city: 'Test City',
|
|
93
|
+
province: 'Nuevo Leon',
|
|
94
|
+
postal_code: '00000',
|
|
95
|
+
country_code: 'mx',
|
|
96
|
+
...address,
|
|
97
|
+
},
|
|
98
|
+
items: [{ variant_id: variantId, quantity: 1 }],
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
79
102
|
beforeAll(async () => {
|
|
80
103
|
// Stop MSW so real HTTP requests reach the backend
|
|
81
104
|
server.close()
|
|
@@ -95,9 +118,18 @@ if (!runLive) {
|
|
|
95
118
|
describe('Test data discovery', () => {
|
|
96
119
|
let products: GetDropshipperProductsResponse['products']
|
|
97
120
|
let customers: GetDropshipperCustomersResponse['customers']
|
|
98
|
-
let
|
|
121
|
+
let shippingGroups: GetDropshipperShippingOptionsResponse['groups']
|
|
99
122
|
let account: GetDropshipperAccountResponse['account']
|
|
100
123
|
|
|
124
|
+
function getFirstVariantId(): string {
|
|
125
|
+
for (const product of products) {
|
|
126
|
+
if (product.variants && product.variants.length > 0) {
|
|
127
|
+
return product.variants[0].id
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
throw new Error('No products with variants found')
|
|
131
|
+
}
|
|
132
|
+
|
|
101
133
|
it('fetches products to find valid variant IDs', async () => {
|
|
102
134
|
const res = await client.getProducts({ limit: 5 })
|
|
103
135
|
expect(res.products.length).toBeGreaterThan(0)
|
|
@@ -116,15 +148,15 @@ if (!runLive) {
|
|
|
116
148
|
account = res.account
|
|
117
149
|
})
|
|
118
150
|
|
|
119
|
-
it('fetches shipping
|
|
120
|
-
const res = await client.getOrderShippingOptions(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
expect(res.
|
|
124
|
-
|
|
151
|
+
it('fetches shipping groups for the account currency', async () => {
|
|
152
|
+
const res = await client.getOrderShippingOptions(
|
|
153
|
+
buildShippingOptionsBody(customers[0].id, getFirstVariantId(), account.currency_code),
|
|
154
|
+
)
|
|
155
|
+
expect(res.groups.length).toBeGreaterThan(0)
|
|
156
|
+
shippingGroups = res.groups
|
|
125
157
|
})
|
|
126
158
|
|
|
127
|
-
it('fetches shipping
|
|
159
|
+
it('fetches shipping groups with country code (geozone filter)', async () => {
|
|
128
160
|
// Use the customer's default shipping address country if available
|
|
129
161
|
const customerDetail = await client.getCustomer(customers[0].id)
|
|
130
162
|
const addr = customerDetail.customer?.addresses?.find((a) => a.is_default_shipping)
|
|
@@ -133,15 +165,16 @@ if (!runLive) {
|
|
|
133
165
|
return
|
|
134
166
|
}
|
|
135
167
|
|
|
136
|
-
const res = await client.getOrderShippingOptions(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
168
|
+
const res = await client.getOrderShippingOptions(
|
|
169
|
+
buildShippingOptionsBody(customers[0].id, getFirstVariantId(), account.currency_code, {
|
|
170
|
+
country_code: addr.country_code,
|
|
171
|
+
province: addr.province ?? undefined,
|
|
172
|
+
city: addr.city ?? undefined,
|
|
173
|
+
}),
|
|
174
|
+
)
|
|
175
|
+
expect(res.groups).toBeInstanceOf(Array)
|
|
143
176
|
console.log(
|
|
144
|
-
`Shipping
|
|
177
|
+
`Shipping groups for ${addr.country_code}/${addr.province ?? 'any'}/${addr.city ?? 'any'}: ${res.groups.length} found`,
|
|
145
178
|
)
|
|
146
179
|
})
|
|
147
180
|
})
|
|
@@ -154,7 +187,7 @@ if (!runLive) {
|
|
|
154
187
|
let products: GetDropshipperProductsResponse['products']
|
|
155
188
|
let customers: GetDropshipperCustomersResponse['customers']
|
|
156
189
|
let account: GetDropshipperAccountResponse['account']
|
|
157
|
-
let
|
|
190
|
+
let shippingGroups: GetDropshipperShippingOptionsResponse['groups']
|
|
158
191
|
let createdOrderIds: string[] = []
|
|
159
192
|
|
|
160
193
|
beforeAll(async () => {
|
|
@@ -167,10 +200,11 @@ if (!runLive) {
|
|
|
167
200
|
customers = customersRes.customers
|
|
168
201
|
account = accountRes.account
|
|
169
202
|
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
203
|
+
const variantId = getFirstVariantId()
|
|
204
|
+
const shippingRes = await client.getOrderShippingOptions(
|
|
205
|
+
buildShippingOptionsBody(customers[0].id, variantId, account.currency_code),
|
|
206
|
+
)
|
|
207
|
+
shippingGroups = shippingRes.groups
|
|
174
208
|
})
|
|
175
209
|
|
|
176
210
|
afterAll(async () => {
|
|
@@ -226,19 +260,29 @@ if (!runLive) {
|
|
|
226
260
|
console.log(`Created order ${res.order.display_id} (${res.order.id}) — total: ${res.order.total}`)
|
|
227
261
|
})
|
|
228
262
|
|
|
229
|
-
it('creates an order with
|
|
230
|
-
if (
|
|
231
|
-
console.log('Skipping: no shipping
|
|
263
|
+
it('creates an order with shipping_groups', async () => {
|
|
264
|
+
if (shippingGroups.length === 0) {
|
|
265
|
+
console.log('Skipping: no shipping groups available')
|
|
232
266
|
return
|
|
233
267
|
}
|
|
234
268
|
|
|
235
|
-
const
|
|
269
|
+
const shippingGroupsInput = shippingGroups
|
|
270
|
+
.map((g) => {
|
|
271
|
+
const option = g.options.find((o) => o.is_default) ?? g.options[0]
|
|
272
|
+
if (!option) return null
|
|
273
|
+
return { products: g.products, option_id: option.id }
|
|
274
|
+
})
|
|
275
|
+
.filter((g): g is { products: string[]; option_id: string } => g !== null)
|
|
276
|
+
|
|
277
|
+
const body = buildOrderBody({ shipping_groups: shippingGroupsInput })
|
|
236
278
|
const res = await client.createOrder(body)
|
|
237
279
|
expect(res.order.id).toBeTruthy()
|
|
238
280
|
expect(res.order.status).toBe('pending')
|
|
281
|
+
expect(res.order.shipping_methods.length).toBe(shippingGroupsInput.length)
|
|
282
|
+
expect(res.order.shipping_total).toBeGreaterThan(0)
|
|
239
283
|
|
|
240
284
|
createdOrderIds.push(res.order.id)
|
|
241
|
-
console.log(`Created order with shipping
|
|
285
|
+
console.log(`Created order with shipping groups: ${res.order.shipping_methods.map((m) => m.name).join(', ')}`)
|
|
242
286
|
})
|
|
243
287
|
|
|
244
288
|
it('creates an order with billing_address', async () => {
|
|
@@ -571,72 +615,82 @@ if (!runLive) {
|
|
|
571
615
|
|
|
572
616
|
describe('Shipping options with geozone parameters', () => {
|
|
573
617
|
let account: GetDropshipperAccountResponse['account']
|
|
618
|
+
let customerId: string
|
|
619
|
+
let variantId: string
|
|
574
620
|
|
|
575
621
|
beforeAll(async () => {
|
|
576
|
-
const
|
|
577
|
-
|
|
622
|
+
const [accountRes, customersRes, productsRes] = await Promise.all([
|
|
623
|
+
client.getAccount(),
|
|
624
|
+
client.getCustomers({ limit: 1 }),
|
|
625
|
+
client.getProducts({ limit: 5 }),
|
|
626
|
+
])
|
|
627
|
+
account = accountRes.account
|
|
628
|
+
customerId = customersRes.customers[0].id
|
|
629
|
+
const product = productsRes.products.find((p) => p.variants && p.variants.length > 0)
|
|
630
|
+
if (!product) throw new Error('No products with variants found')
|
|
631
|
+
variantId = product.variants[0].id
|
|
578
632
|
})
|
|
579
633
|
|
|
580
|
-
it('returns shipping
|
|
581
|
-
const res = await client.getOrderShippingOptions(
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
expect(res.
|
|
585
|
-
console.log(`Shipping
|
|
586
|
-
if (res.
|
|
587
|
-
|
|
588
|
-
expect(
|
|
589
|
-
expect(
|
|
634
|
+
it('returns shipping groups with currency only', async () => {
|
|
635
|
+
const res = await client.getOrderShippingOptions(
|
|
636
|
+
buildShippingOptionsBody(customerId, variantId, account.currency_code),
|
|
637
|
+
)
|
|
638
|
+
expect(res.groups).toBeInstanceOf(Array)
|
|
639
|
+
console.log(`Shipping groups (currency only): ${res.groups.length} found`)
|
|
640
|
+
if (res.groups.length > 0) {
|
|
641
|
+
const option = res.groups[0].options[0]
|
|
642
|
+
expect(option).toHaveProperty('id')
|
|
643
|
+
expect(option).toHaveProperty('name')
|
|
644
|
+
expect(option).toHaveProperty('amount')
|
|
590
645
|
}
|
|
591
646
|
})
|
|
592
647
|
|
|
593
|
-
it('returns shipping
|
|
594
|
-
const res = await client.getOrderShippingOptions(
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
console.log(`Shipping options (MX): ${res.shipping_options.length} found`)
|
|
648
|
+
it('returns shipping groups filtered by country_code', async () => {
|
|
649
|
+
const res = await client.getOrderShippingOptions(
|
|
650
|
+
buildShippingOptionsBody(customerId, variantId, account.currency_code, { country_code: 'mx' }),
|
|
651
|
+
)
|
|
652
|
+
expect(res.groups).toBeInstanceOf(Array)
|
|
653
|
+
console.log(`Shipping groups (MX): ${res.groups.length} found`)
|
|
600
654
|
})
|
|
601
655
|
|
|
602
|
-
it('returns shipping
|
|
603
|
-
const res = await client.getOrderShippingOptions(
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
656
|
+
it('returns shipping groups filtered by country_code and province', async () => {
|
|
657
|
+
const res = await client.getOrderShippingOptions(
|
|
658
|
+
buildShippingOptionsBody(customerId, variantId, account.currency_code, {
|
|
659
|
+
country_code: 'mx',
|
|
660
|
+
province: 'Nuevo Leon',
|
|
661
|
+
}),
|
|
662
|
+
)
|
|
663
|
+
expect(res.groups).toBeInstanceOf(Array)
|
|
664
|
+
console.log(`Shipping groups (MX/Nuevo Leon): ${res.groups.length} found`)
|
|
610
665
|
})
|
|
611
666
|
|
|
612
|
-
it('returns shipping
|
|
613
|
-
const res = await client.getOrderShippingOptions(
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
667
|
+
it('returns shipping groups filtered by country_code, province, and city', async () => {
|
|
668
|
+
const res = await client.getOrderShippingOptions(
|
|
669
|
+
buildShippingOptionsBody(customerId, variantId, account.currency_code, {
|
|
670
|
+
country_code: 'mx',
|
|
671
|
+
province: 'Nuevo Leon',
|
|
672
|
+
city: 'Monterrey',
|
|
673
|
+
}),
|
|
674
|
+
)
|
|
675
|
+
expect(res.groups).toBeInstanceOf(Array)
|
|
676
|
+
console.log(`Shipping groups (MX/Nuevo Leon/Monterrey): ${res.groups.length} found`)
|
|
621
677
|
})
|
|
622
678
|
|
|
623
679
|
it('returns different results for different countries (geozone differentiation)', async () => {
|
|
624
680
|
const [mxOptions, usOptions] = await Promise.all([
|
|
625
|
-
client.getOrderShippingOptions(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
country_code: 'us',
|
|
632
|
-
}),
|
|
681
|
+
client.getOrderShippingOptions(
|
|
682
|
+
buildShippingOptionsBody(customerId, variantId, account.currency_code, { country_code: 'mx' }),
|
|
683
|
+
),
|
|
684
|
+
client.getOrderShippingOptions(
|
|
685
|
+
buildShippingOptionsBody(customerId, variantId, account.currency_code, { country_code: 'us' }),
|
|
686
|
+
),
|
|
633
687
|
])
|
|
634
688
|
|
|
635
|
-
console.log(`MX
|
|
689
|
+
console.log(`MX groups: ${mxOptions.groups.length}, US groups: ${usOptions.groups.length}`)
|
|
636
690
|
|
|
637
691
|
// The options MAY differ by country — log the difference
|
|
638
|
-
const mxNames = mxOptions.
|
|
639
|
-
const usNames = usOptions.
|
|
692
|
+
const mxNames = mxOptions.groups.flatMap((g) => g.options.map((o) => o.name)).join(', ')
|
|
693
|
+
const usNames = usOptions.groups.flatMap((g) => g.options.map((o) => o.name)).join(', ')
|
|
640
694
|
console.log(`MX: [${mxNames}]`)
|
|
641
695
|
console.log(`US: [${usNames}]`)
|
|
642
696
|
})
|
package/src/dropshipper.test.ts
CHANGED
|
@@ -590,23 +590,42 @@ describe('DropshipperClient.createOrder', () => {
|
|
|
590
590
|
})
|
|
591
591
|
|
|
592
592
|
describe('DropshipperClient.getOrderShippingOptions', () => {
|
|
593
|
-
it('
|
|
593
|
+
it('POST /admin/thor/dropshipper/orders/shipping-options with body', async () => {
|
|
594
594
|
server.use(
|
|
595
|
-
http.
|
|
595
|
+
http.post(`${BASE}/admin/thor/dropshipper/orders/shipping-options`, async ({ request }) => {
|
|
596
596
|
requireBearerToken(request)
|
|
597
|
-
const
|
|
598
|
-
expect(
|
|
599
|
-
expect(
|
|
597
|
+
const body = await request.json() as any
|
|
598
|
+
expect(body.currency_code).toBe('usd')
|
|
599
|
+
expect(body.customer_id).toBe('cus_1')
|
|
600
|
+
expect(body.shipping_address.country_code).toBe('us')
|
|
601
|
+
expect(body.items).toHaveLength(1)
|
|
600
602
|
return HttpResponse.json({
|
|
601
|
-
|
|
603
|
+
groups: [
|
|
602
604
|
{
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
605
|
+
profile_id: 'sp_1',
|
|
606
|
+
profile_label: 'Estándar',
|
|
607
|
+
products: ['variant_1'],
|
|
608
|
+
items: [
|
|
609
|
+
{
|
|
610
|
+
variant_id: 'variant_1',
|
|
611
|
+
quantity: 1,
|
|
612
|
+
product_title: 'Product',
|
|
613
|
+
variant_title: 'Default',
|
|
614
|
+
},
|
|
615
|
+
],
|
|
616
|
+
options: [
|
|
617
|
+
{
|
|
618
|
+
id: 'so_1',
|
|
619
|
+
name: 'Estándar',
|
|
620
|
+
amount: 500,
|
|
621
|
+
currency_code: 'usd',
|
|
622
|
+
price_type: 'flat',
|
|
623
|
+
is_default: true,
|
|
624
|
+
shipping_profile_id: 'sp_1',
|
|
625
|
+
stock_location_id: 'sloc_1',
|
|
626
|
+
type: null,
|
|
627
|
+
},
|
|
628
|
+
],
|
|
610
629
|
},
|
|
611
630
|
],
|
|
612
631
|
count: 1,
|
|
@@ -614,30 +633,60 @@ describe('DropshipperClient.getOrderShippingOptions', () => {
|
|
|
614
633
|
}),
|
|
615
634
|
)
|
|
616
635
|
|
|
617
|
-
const result = await client().getOrderShippingOptions({
|
|
618
|
-
|
|
619
|
-
|
|
636
|
+
const result = await client().getOrderShippingOptions({
|
|
637
|
+
customer_id: 'cus_1',
|
|
638
|
+
currency_code: 'usd',
|
|
639
|
+
shipping_address: {
|
|
640
|
+
address_1: '123 Main St',
|
|
641
|
+
city: 'Miami',
|
|
642
|
+
province: 'FL',
|
|
643
|
+
country_code: 'us',
|
|
644
|
+
},
|
|
645
|
+
items: [{ variant_id: 'variant_1', quantity: 1 }],
|
|
646
|
+
})
|
|
647
|
+
expect(result.groups).toHaveLength(1)
|
|
648
|
+
expect(result.groups[0].profile_label).toBe('Estándar')
|
|
649
|
+
expect(result.groups[0].options[0].id).toBe('so_1')
|
|
650
|
+
expect(result.count).toBe(1)
|
|
620
651
|
})
|
|
621
652
|
|
|
622
|
-
it('forwards
|
|
653
|
+
it('forwards customer, address and items and returns groups', async () => {
|
|
623
654
|
server.use(
|
|
624
|
-
http.
|
|
655
|
+
http.post(`${BASE}/admin/thor/dropshipper/orders/shipping-options`, async ({ request }) => {
|
|
625
656
|
requireBearerToken(request)
|
|
626
|
-
const
|
|
627
|
-
expect(
|
|
628
|
-
expect(
|
|
629
|
-
expect(
|
|
630
|
-
expect(
|
|
657
|
+
const body = await request.json() as any
|
|
658
|
+
expect(body.currency_code).toBe('usd')
|
|
659
|
+
expect(body.customer_id).toBe('cus_1')
|
|
660
|
+
expect(body.shipping_address.province).toBe('La Habana')
|
|
661
|
+
expect(body.shipping_address.city).toBe('Centro Habana')
|
|
662
|
+
expect(body.items[0].variant_id).toBe('variant_1')
|
|
631
663
|
return HttpResponse.json({
|
|
632
|
-
|
|
664
|
+
groups: [
|
|
633
665
|
{
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
666
|
+
profile_id: 'sp_1',
|
|
667
|
+
profile_label: 'Estándar',
|
|
668
|
+
products: ['variant_1'],
|
|
669
|
+
items: [
|
|
670
|
+
{
|
|
671
|
+
variant_id: 'variant_1',
|
|
672
|
+
quantity: 1,
|
|
673
|
+
product_title: 'Product',
|
|
674
|
+
variant_title: 'Default',
|
|
675
|
+
},
|
|
676
|
+
],
|
|
677
|
+
options: [
|
|
678
|
+
{
|
|
679
|
+
id: 'so_1',
|
|
680
|
+
name: 'La Habana Express',
|
|
681
|
+
amount: 500,
|
|
682
|
+
currency_code: 'usd',
|
|
683
|
+
price_type: 'flat',
|
|
684
|
+
is_default: true,
|
|
685
|
+
shipping_profile_id: 'sp_1',
|
|
686
|
+
stock_location_id: 'sloc_1',
|
|
687
|
+
type: null,
|
|
688
|
+
},
|
|
689
|
+
],
|
|
641
690
|
},
|
|
642
691
|
],
|
|
643
692
|
count: 1,
|
|
@@ -646,13 +695,18 @@ describe('DropshipperClient.getOrderShippingOptions', () => {
|
|
|
646
695
|
)
|
|
647
696
|
|
|
648
697
|
const result = await client().getOrderShippingOptions({
|
|
698
|
+
customer_id: 'cus_1',
|
|
649
699
|
currency_code: 'usd',
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
700
|
+
shipping_address: {
|
|
701
|
+
address_1: 'Calle 1',
|
|
702
|
+
city: 'Centro Habana',
|
|
703
|
+
province: 'La Habana',
|
|
704
|
+
country_code: 'cu',
|
|
705
|
+
},
|
|
706
|
+
items: [{ variant_id: 'variant_1', quantity: 1 }],
|
|
653
707
|
})
|
|
654
|
-
expect(result.
|
|
655
|
-
expect(result.
|
|
708
|
+
expect(result.groups).toHaveLength(1)
|
|
709
|
+
expect(result.groups[0].options[0].name).toBe('La Habana Express')
|
|
656
710
|
})
|
|
657
711
|
})
|
|
658
712
|
|
package/src/dropshipper.ts
CHANGED
|
@@ -36,8 +36,8 @@ import type {
|
|
|
36
36
|
// Orders
|
|
37
37
|
GetDropshipperOrdersOptions,
|
|
38
38
|
GetDropshipperOrdersResponse,
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
GetDropshipperShippingOptionsBody,
|
|
40
|
+
GetDropshipperShippingOptionsResponse,
|
|
41
41
|
GetDropshipperOrderDetailResponse,
|
|
42
42
|
ExtendedCreateDropshipperOrderBody,
|
|
43
43
|
ExtendedCreateDropshipperOrderResponse,
|
|
@@ -801,23 +801,20 @@ export class DropshipperClient {
|
|
|
801
801
|
}
|
|
802
802
|
|
|
803
803
|
/**
|
|
804
|
-
* List shipping options for a manual dropshipper order.
|
|
804
|
+
* List shipping groups and their valid options for a manual dropshipper order.
|
|
805
805
|
*
|
|
806
|
-
* `
|
|
806
|
+
* `POST /admin/thor/dropshipper/orders/shipping-options`
|
|
807
807
|
*/
|
|
808
808
|
async getOrderShippingOptions(
|
|
809
|
-
|
|
809
|
+
body: GetDropshipperShippingOptionsBody,
|
|
810
810
|
): Promise<GetDropshipperShippingOptionsResponse> {
|
|
811
|
-
const qs = new URLSearchParams({
|
|
812
|
-
currency_code: params.currency_code.toLowerCase(),
|
|
813
|
-
...(params.country_code ? { country_code: params.country_code.toLowerCase() } : {}),
|
|
814
|
-
...(params.province ? { province: params.province } : {}),
|
|
815
|
-
...(params.city ? { city: params.city } : {}),
|
|
816
|
-
});
|
|
817
|
-
|
|
818
811
|
return this.request<GetDropshipperShippingOptionsResponse>(
|
|
819
|
-
'
|
|
820
|
-
|
|
812
|
+
'POST',
|
|
813
|
+
'/admin/thor/dropshipper/orders/shipping-options',
|
|
814
|
+
{
|
|
815
|
+
...body,
|
|
816
|
+
currency_code: body.currency_code.toLowerCase(),
|
|
817
|
+
},
|
|
821
818
|
);
|
|
822
819
|
}
|
|
823
820
|
|
package/src/recovery.ts
CHANGED
|
@@ -14,15 +14,28 @@
|
|
|
14
14
|
* - `POST /auth/user/emailpass/update` (confirm reset, consumes token)
|
|
15
15
|
* - `GET /store/dropshipper/applications/activate/verify`(verify activation token, no consume)
|
|
16
16
|
* - `POST /store/dropshipper/applications/activate` (confirm activation, consumes token)
|
|
17
|
+
* - `POST /public/dropshipper/applications` (submit application, no auth)
|
|
17
18
|
*
|
|
18
19
|
* Confirm methods follow the two-phase contract: verify (no consume) first,
|
|
19
20
|
* then confirm (atomic consume). A re-verify after a failed confirm maps the
|
|
20
21
|
* TOCTOU state (used/expired/revoked) into `RecoveryConfirmResult.status`.
|
|
22
|
+
*
|
|
23
|
+
* `submitApplication` returns a discriminated `SubmitApplicationResult` instead
|
|
24
|
+
* of throwing on 400/409/429 so the caller can surface the backend's structured
|
|
25
|
+
* payload (existing application on 409, retry window on 429). Network/5xx
|
|
26
|
+
* errors are re-thrown as `ProviderAPIError`.
|
|
21
27
|
*/
|
|
22
28
|
|
|
23
|
-
import type {
|
|
29
|
+
import type {
|
|
30
|
+
SubmitApplicationBody,
|
|
31
|
+
SubmitApplicationConflictApplication,
|
|
32
|
+
SubmitApplicationResponse,
|
|
33
|
+
SubmitApplicationResult,
|
|
34
|
+
TokenVerifyResult,
|
|
35
|
+
RecoveryConfirmResult,
|
|
36
|
+
} from '@thorprovider/types';
|
|
24
37
|
import { createLogger, ProviderAPIError } from '@thorprovider/adapters';
|
|
25
|
-
import { fetchBackendWithResilience } from './request-resilience';
|
|
38
|
+
import { fetchBackendWithResilience, type RetryConfig } from './request-resilience';
|
|
26
39
|
|
|
27
40
|
type FetchMethod = 'GET' | 'POST';
|
|
28
41
|
|
|
@@ -66,7 +79,12 @@ export class RecoveryClient {
|
|
|
66
79
|
// Private — HTTP helper
|
|
67
80
|
// -------------------------------------------------------------------------
|
|
68
81
|
|
|
69
|
-
private async request<T>(
|
|
82
|
+
private async request<T>(
|
|
83
|
+
method: FetchMethod,
|
|
84
|
+
path: string,
|
|
85
|
+
body?: unknown,
|
|
86
|
+
retryConfig?: RetryConfig,
|
|
87
|
+
): Promise<T> {
|
|
70
88
|
const url = `${this.baseUrl}${path}`;
|
|
71
89
|
this.logger.debug(`[recovery] ${method} ${path}`);
|
|
72
90
|
|
|
@@ -82,7 +100,7 @@ export class RecoveryClient {
|
|
|
82
100
|
|
|
83
101
|
let response: Response;
|
|
84
102
|
try {
|
|
85
|
-
response = await fetchBackendWithResilience(url, init);
|
|
103
|
+
response = await fetchBackendWithResilience(url, init, retryConfig);
|
|
86
104
|
} catch (networkError: any) {
|
|
87
105
|
throw new ProviderAPIError(
|
|
88
106
|
`[recovery] Network error on ${method} ${path}: ${networkError.message}`,
|
|
@@ -93,9 +111,11 @@ export class RecoveryClient {
|
|
|
93
111
|
|
|
94
112
|
if (!response.ok) {
|
|
95
113
|
let message = `HTTP ${response.status}`;
|
|
114
|
+
let details: Record<string, unknown> | undefined;
|
|
96
115
|
try {
|
|
97
116
|
const json = await response.json();
|
|
98
117
|
message = json?.message ?? message;
|
|
118
|
+
details = json && typeof json === 'object' ? (json as Record<string, unknown>) : undefined;
|
|
99
119
|
} catch {
|
|
100
120
|
// ignore parse errors
|
|
101
121
|
}
|
|
@@ -103,6 +123,7 @@ export class RecoveryClient {
|
|
|
103
123
|
`[recovery] ${method} ${path} failed: ${message}`,
|
|
104
124
|
`RECOVERY_API_${response.status}`,
|
|
105
125
|
response.status,
|
|
126
|
+
details,
|
|
106
127
|
);
|
|
107
128
|
}
|
|
108
129
|
|
|
@@ -244,4 +265,64 @@ export class RecoveryClient {
|
|
|
244
265
|
}
|
|
245
266
|
}
|
|
246
267
|
}
|
|
268
|
+
|
|
269
|
+
// -------------------------------------------------------------------------
|
|
270
|
+
// Dropshipper application submission
|
|
271
|
+
// -------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Envía una solicitud de acceso dropshipper al endpoint público.
|
|
275
|
+
*
|
|
276
|
+
* `POST /public/dropshipper/applications`
|
|
277
|
+
*
|
|
278
|
+
* No requiere autenticación. Devuelve un resultado discriminado:
|
|
279
|
+
* - `{ ok: true }` → 201, solicitud creada (`application.number`).
|
|
280
|
+
* - `{ ok: false, reason: 'conflict' }` → 409, email ya registrado; la
|
|
281
|
+
* solicitud existente incluye `status_label`/`reason` para la UI.
|
|
282
|
+
* - `{ ok: false, reason: 'rate_limited' }` → 429, se respetó el límite del
|
|
283
|
+
* backend; `retryAfterSeconds` indica cuándo reintentar.
|
|
284
|
+
* - `{ ok: false, reason: 'invalid_data' }` → 400, validación del backend.
|
|
285
|
+
*
|
|
286
|
+
* Los errores de red/5xx se re-lanzan como `ProviderAPIError` para que el
|
|
287
|
+
* llamador los categorice (p. ej. con `parseError`). El 429 no se reintenta
|
|
288
|
+
* (`retryableStatusCodes` sin 429) para que el caller reciba de inmediato el
|
|
289
|
+
* `retry_after_seconds` del backend y pueda bloquear el envío.
|
|
290
|
+
*/
|
|
291
|
+
async submitApplication(body: SubmitApplicationBody): Promise<SubmitApplicationResult> {
|
|
292
|
+
try {
|
|
293
|
+
const response = await this.request<SubmitApplicationResponse>(
|
|
294
|
+
'POST',
|
|
295
|
+
'/public/dropshipper/applications',
|
|
296
|
+
body,
|
|
297
|
+
{ retryableStatusCodes: [408, 500, 502, 503, 504] },
|
|
298
|
+
);
|
|
299
|
+
return { ok: true, application: response.application };
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (error instanceof ProviderAPIError) {
|
|
302
|
+
const details = error.details ?? {};
|
|
303
|
+
if (error.status === 409 && details.application) {
|
|
304
|
+
return {
|
|
305
|
+
ok: false,
|
|
306
|
+
reason: 'conflict',
|
|
307
|
+
application: details.application as SubmitApplicationConflictApplication,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
if (error.status === 429 && typeof details.retry_after_seconds === 'number') {
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
reason: 'rate_limited',
|
|
314
|
+
retryAfterSeconds: details.retry_after_seconds,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (error.status === 400) {
|
|
318
|
+
return {
|
|
319
|
+
ok: false,
|
|
320
|
+
reason: 'invalid_data',
|
|
321
|
+
fields: details.fields as Record<string, string[]> | undefined,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
247
328
|
}
|