@reactionary/medusa 0.10.52 → 0.10.54
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/capabilities/checkout.capability.js +124 -5
- package/capabilities/store.capability.js +166 -0
- package/core/initialize.js +21 -2
- package/factories/checkout/checkout.factory.js +80 -3
- package/factories/index.js +1 -0
- package/factories/store/store.factory.js +49 -0
- package/index.js +1 -0
- package/package.json +2 -2
- package/schema/capabilities.schema.js +4 -2
- package/schema/medusa.schema.js +2 -1
- package/src/capabilities/checkout.capability.d.ts +47 -3
- package/src/capabilities/store.capability.d.ts +45 -0
- package/src/core/initialize.types.d.ts +7 -2
- package/src/factories/checkout/checkout.factory.d.ts +31 -2
- package/src/factories/index.d.ts +1 -0
- package/src/factories/store/store.factory.d.ts +13 -0
- package/src/index.d.ts +1 -0
- package/src/schema/capabilities.schema.d.ts +9 -2
- package/src/schema/medusa.schema.d.ts +4 -0
- package/src/utils/geo.d.ts +5 -0
- package/utils/geo.js +11 -0
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from "@reactionary/core";
|
|
31
31
|
import createDebug from "debug";
|
|
32
32
|
import * as z from "zod";
|
|
33
|
+
import { MedusaAdminAPI } from "../core/client.js";
|
|
33
34
|
import {
|
|
34
35
|
} from "../schema/medusa.schema.js";
|
|
35
36
|
import { handleProviderError, isNotFoundFetchError } from "../utils/medusa-helpers.js";
|
|
@@ -61,6 +62,16 @@ class MedusaCheckoutCapability extends CheckoutCapability {
|
|
|
61
62
|
* example: this.includedFields = [includedFields, '+discounts.*'].join(',');
|
|
62
63
|
*/
|
|
63
64
|
includedFields = ["+items.*", "+order.id", "+order.display_id", "+order.custom_display_id"].join(",");
|
|
65
|
+
/**
|
|
66
|
+
* This controls which fields are requested for the cart's shipping options.
|
|
67
|
+
* You can override this in a subclass to add more fields as needed.
|
|
68
|
+
*
|
|
69
|
+
* The service zone's fulfillment set tells us whether an option is in-store
|
|
70
|
+
* pickup (BOPIS) or a delivery, and the option metadata lets medusa-side
|
|
71
|
+
* providers push additional data that is mapped to the shipping method's
|
|
72
|
+
* protocolData.
|
|
73
|
+
*/
|
|
74
|
+
shippingOptionFields = ["*service_zone.fulfillment_set", "+metadata"].join(",");
|
|
64
75
|
initiateCheckoutForCartPayload(payload) {
|
|
65
76
|
return {
|
|
66
77
|
billing_address: payload.billingAddress ? this.mapAddressToStoreAddress(payload.billingAddress) : void 0,
|
|
@@ -93,7 +104,9 @@ class MedusaCheckoutCapability extends CheckoutCapability {
|
|
|
93
104
|
const response = await client.store.cart.retrieve(payload.identifier.key, {
|
|
94
105
|
fields: this.includedFields
|
|
95
106
|
});
|
|
96
|
-
return success(
|
|
107
|
+
return success(
|
|
108
|
+
this.hydrateResultingOrder(this.factory.parseCheckout(this.context, response.cart))
|
|
109
|
+
);
|
|
97
110
|
} catch (err) {
|
|
98
111
|
if (isNotFoundFetchError(err)) {
|
|
99
112
|
return error({
|
|
@@ -122,7 +135,8 @@ class MedusaCheckoutCapability extends CheckoutCapability {
|
|
|
122
135
|
}
|
|
123
136
|
getAvailableShippingMethodsPayload(payload) {
|
|
124
137
|
return {
|
|
125
|
-
cart_id: payload.checkout.key
|
|
138
|
+
cart_id: payload.checkout.key,
|
|
139
|
+
fields: this.shippingOptionFields
|
|
126
140
|
};
|
|
127
141
|
}
|
|
128
142
|
/**
|
|
@@ -270,6 +284,50 @@ class MedusaCheckoutCapability extends CheckoutCapability {
|
|
|
270
284
|
}
|
|
271
285
|
};
|
|
272
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* Serializes the client-set shipping protocol data as a JSON object string
|
|
289
|
+
* for persistence in the cart metadata, or undefined when none is set.
|
|
290
|
+
*/
|
|
291
|
+
serializeShippingProtocolData(protocolData) {
|
|
292
|
+
if (!protocolData || protocolData.length === 0) {
|
|
293
|
+
return void 0;
|
|
294
|
+
}
|
|
295
|
+
return JSON.stringify(
|
|
296
|
+
Object.fromEntries(protocolData.map((entry) => [entry.key, entry.value]))
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Resolves the shipping address to use for in-store pickup (BOPIS): the
|
|
301
|
+
* address of the selected store, so the order ships to the store. Returns
|
|
302
|
+
* undefined when the store or its address is unknown, leaving the cart's
|
|
303
|
+
* shipping address untouched.
|
|
304
|
+
*/
|
|
305
|
+
async resolveStorePickupAddress(store) {
|
|
306
|
+
try {
|
|
307
|
+
const adminClient = await new MedusaAdminAPI(this.config, this.context).getClient();
|
|
308
|
+
const response = await adminClient.admin.stockLocation.retrieve(store.key, {
|
|
309
|
+
fields: "*address"
|
|
310
|
+
});
|
|
311
|
+
const address = response.stock_location?.address;
|
|
312
|
+
if (!address) {
|
|
313
|
+
return void 0;
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
firstName: "",
|
|
317
|
+
lastName: response.stock_location.name,
|
|
318
|
+
streetAddress: address.address_1 || "",
|
|
319
|
+
streetNumber: address.address_2 || "",
|
|
320
|
+
city: address.city || "",
|
|
321
|
+
postalCode: address.postal_code || "",
|
|
322
|
+
countryCode: address.country_code || ""
|
|
323
|
+
};
|
|
324
|
+
} catch (err) {
|
|
325
|
+
if (debug.enabled) {
|
|
326
|
+
debug(`Unable to resolve address for pickup store ${store.key}`, err);
|
|
327
|
+
}
|
|
328
|
+
return void 0;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
273
331
|
async setShippingInstruction(payload) {
|
|
274
332
|
const client = await this.medusaApi.getClient();
|
|
275
333
|
const medusaId = payload.checkout;
|
|
@@ -277,13 +335,22 @@ class MedusaCheckoutCapability extends CheckoutCapability {
|
|
|
277
335
|
if (payload.shippingInstruction.shippingMethod) {
|
|
278
336
|
await client.store.cart.addShippingMethod(medusaId.key, this.setShippingInstructionPayload(payload));
|
|
279
337
|
}
|
|
280
|
-
|
|
338
|
+
const cartUpdate = {
|
|
281
339
|
metadata: {
|
|
282
340
|
consent_for_unattended_delivery: payload.shippingInstruction.consentForUnattendedDelivery + "",
|
|
283
341
|
instructions: payload.shippingInstruction.instructions || "",
|
|
284
|
-
pickup_point: payload.shippingInstruction.pickupPoint || ""
|
|
342
|
+
pickup_point: payload.shippingInstruction.pickupPoint || "",
|
|
343
|
+
pickup_store: payload.shippingInstruction.store?.key || "",
|
|
344
|
+
shipping_protocol_data: this.serializeShippingProtocolData(payload.shippingInstruction.protocolData) || ""
|
|
285
345
|
}
|
|
286
|
-
}
|
|
346
|
+
};
|
|
347
|
+
if (payload.shippingInstruction.store?.key) {
|
|
348
|
+
const storeAddress = await this.resolveStorePickupAddress(payload.shippingInstruction.store);
|
|
349
|
+
if (storeAddress) {
|
|
350
|
+
cartUpdate.shipping_address = this.mapAddressToStoreAddress(storeAddress);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
await client.store.cart.update(medusaId.key, cartUpdate);
|
|
287
354
|
const response = await client.store.cart.retrieve(medusaId.key, {
|
|
288
355
|
fields: this.includedFields
|
|
289
356
|
});
|
|
@@ -306,14 +373,24 @@ class MedusaCheckoutCapability extends CheckoutCapability {
|
|
|
306
373
|
if (!checkout.value.resultingOrder) {
|
|
307
374
|
const client = await this.medusaApi.getClient();
|
|
308
375
|
const medusaId = payload.checkout;
|
|
376
|
+
await client.store.cart.update(medusaId.key, {
|
|
377
|
+
metadata: {
|
|
378
|
+
cart_id: medusaId.key
|
|
379
|
+
}
|
|
380
|
+
});
|
|
309
381
|
const response = await client.store.cart.complete(medusaId.key);
|
|
310
382
|
if (response.type === "order") {
|
|
383
|
+
const resultingOrder = this.parseCompletedOrderIdentifier(response.order);
|
|
384
|
+
this.rememberFinalizedOrder(medusaId.key, resultingOrder);
|
|
311
385
|
checkout = await this.getById({
|
|
312
386
|
identifier: payload.checkout
|
|
313
387
|
});
|
|
314
388
|
if (!checkout.success) {
|
|
315
389
|
throw new Error(`Unable to reload checkout ${payload.checkout.key} after completion`);
|
|
316
390
|
}
|
|
391
|
+
if (!checkout.value.resultingOrder) {
|
|
392
|
+
checkout = success({ ...checkout.value, resultingOrder });
|
|
393
|
+
}
|
|
317
394
|
} else {
|
|
318
395
|
throw new Error(`Failed to finalize checkout ${payload.checkout.key}. Expected order, got ${response.type}`);
|
|
319
396
|
}
|
|
@@ -333,6 +410,48 @@ class MedusaCheckoutCapability extends CheckoutCapability {
|
|
|
333
410
|
}
|
|
334
411
|
return success(checkout.value);
|
|
335
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* Builds the order identifier from a completed-cart response's order,
|
|
415
|
+
* following the same display-id precedence as the checkout factory
|
|
416
|
+
* (custom_display_id over display_id).
|
|
417
|
+
*/
|
|
418
|
+
parseCompletedOrderIdentifier(order) {
|
|
419
|
+
const rawDisplayId = order.custom_display_id ?? order.display_id;
|
|
420
|
+
const displayId = rawDisplayId != null ? Number(`${rawDisplayId}`) : void 0;
|
|
421
|
+
return {
|
|
422
|
+
key: order.id,
|
|
423
|
+
display_id: displayId != null && Number.isFinite(displayId) ? displayId : void 0
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Remembers which order a finalized cart produced, so later checkout reads
|
|
428
|
+
* in the same session can report the resulting order even when the Medusa
|
|
429
|
+
* store cart endpoint does not expose the cart->order relation.
|
|
430
|
+
*/
|
|
431
|
+
rememberFinalizedOrder(cartKey, order) {
|
|
432
|
+
const sessionData = this.medusaApi.getSessionData();
|
|
433
|
+
this.medusaApi.setSessionData({
|
|
434
|
+
finalizedOrders: {
|
|
435
|
+
...sessionData.finalizedOrders || {},
|
|
436
|
+
[cartKey]: order
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Fills in a missing resultingOrder from the session's finalized-orders map.
|
|
442
|
+
* Checkouts that already carry a resulting order (older Medusa versions
|
|
443
|
+
* still hydrate the +order.* fields) are returned unchanged.
|
|
444
|
+
*/
|
|
445
|
+
hydrateResultingOrder(checkout) {
|
|
446
|
+
if (checkout.resultingOrder) {
|
|
447
|
+
return checkout;
|
|
448
|
+
}
|
|
449
|
+
const remembered = this.medusaApi.getSessionData().finalizedOrders?.[checkout.identifier.key];
|
|
450
|
+
if (!remembered) {
|
|
451
|
+
return checkout;
|
|
452
|
+
}
|
|
453
|
+
return { ...checkout, resultingOrder: remembered };
|
|
454
|
+
}
|
|
336
455
|
/**
|
|
337
456
|
* Extension point to map an Address to a Store Address
|
|
338
457
|
* @param address
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
4
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
5
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
6
|
+
if (decorator = decorators[i])
|
|
7
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
8
|
+
if (kind && result) __defProp(target, key, result);
|
|
9
|
+
return result;
|
|
10
|
+
};
|
|
11
|
+
import {
|
|
12
|
+
Reactionary,
|
|
13
|
+
StoreCapability,
|
|
14
|
+
StorePaginatedResultSchema,
|
|
15
|
+
StoreQueryByIdSchema,
|
|
16
|
+
StoreQueryByProximitySchema,
|
|
17
|
+
StoreQueryForListSchema,
|
|
18
|
+
StoreSchema,
|
|
19
|
+
error,
|
|
20
|
+
success
|
|
21
|
+
} from "@reactionary/core";
|
|
22
|
+
import createDebug from "debug";
|
|
23
|
+
import * as z from "zod";
|
|
24
|
+
import { MedusaAdminAPI } from "../core/client.js";
|
|
25
|
+
import { haversineDistanceKm } from "../utils/geo.js";
|
|
26
|
+
const debug = createDebug("reactionary:medusa:store");
|
|
27
|
+
class MedusaStoreCapability extends StoreCapability {
|
|
28
|
+
constructor(config, cache, context, medusaApi, factory) {
|
|
29
|
+
super(cache, context);
|
|
30
|
+
this.medusaApi = medusaApi;
|
|
31
|
+
this.config = config;
|
|
32
|
+
this.factory = factory;
|
|
33
|
+
}
|
|
34
|
+
medusaApi;
|
|
35
|
+
config;
|
|
36
|
+
factory;
|
|
37
|
+
/**
|
|
38
|
+
* The fields requested for stock locations. Override in a subclass if a
|
|
39
|
+
* project needs more relations.
|
|
40
|
+
*/
|
|
41
|
+
stockLocationFields = "*address,*fulfillment_sets";
|
|
42
|
+
/**
|
|
43
|
+
* Whether a stock location is a store customers can pick up orders from,
|
|
44
|
+
* i.e. it has a fulfillment set of type "pickup".
|
|
45
|
+
*/
|
|
46
|
+
isPickupLocation(location) {
|
|
47
|
+
return (location.fulfillment_sets || []).some((set) => set.type === "pickup");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Fetches all stock locations that are pickup stores. Medusa installations
|
|
51
|
+
* have a small, bounded number of stock locations, so fetching them all and
|
|
52
|
+
* filtering client-side is acceptable.
|
|
53
|
+
*/
|
|
54
|
+
async fetchPickupLocations() {
|
|
55
|
+
const adminClient = await new MedusaAdminAPI(this.config, this.context).getClient();
|
|
56
|
+
const pageSize = 200;
|
|
57
|
+
let offset = 0;
|
|
58
|
+
const locations = [];
|
|
59
|
+
for (; ; ) {
|
|
60
|
+
const response = await adminClient.admin.stockLocation.list({
|
|
61
|
+
fields: this.stockLocationFields,
|
|
62
|
+
limit: pageSize,
|
|
63
|
+
offset
|
|
64
|
+
});
|
|
65
|
+
locations.push(...response.stock_locations || []);
|
|
66
|
+
offset += pageSize;
|
|
67
|
+
if (offset >= response.count || (response.stock_locations || []).length === 0) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return locations.filter((location) => this.isPickupLocation(location));
|
|
72
|
+
}
|
|
73
|
+
async list(payload) {
|
|
74
|
+
const pickupLocations = await this.fetchPickupLocations();
|
|
75
|
+
if (debug.enabled) {
|
|
76
|
+
debug(`Found ${pickupLocations.length} pickup stock locations`);
|
|
77
|
+
}
|
|
78
|
+
const { pageNumber, pageSize } = payload.paginationOptions;
|
|
79
|
+
const startIndex = (pageNumber - 1) * pageSize;
|
|
80
|
+
const pagedLocations = pickupLocations.slice(startIndex, startIndex + pageSize);
|
|
81
|
+
return success({
|
|
82
|
+
pageNumber,
|
|
83
|
+
pageSize,
|
|
84
|
+
totalCount: pickupLocations.length,
|
|
85
|
+
totalPages: Math.ceil(pickupLocations.length / pageSize),
|
|
86
|
+
items: pagedLocations.map(
|
|
87
|
+
(location) => this.factory.parseStore(this.context, location)
|
|
88
|
+
)
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async getById(payload) {
|
|
92
|
+
const adminClient = await new MedusaAdminAPI(this.config, this.context).getClient();
|
|
93
|
+
try {
|
|
94
|
+
const response = await adminClient.admin.stockLocation.retrieve(payload.identifier.key, {
|
|
95
|
+
fields: this.stockLocationFields
|
|
96
|
+
});
|
|
97
|
+
if (!response.stock_location || !this.isPickupLocation(response.stock_location)) {
|
|
98
|
+
return error({ type: "NotFound", identifier: payload.identifier });
|
|
99
|
+
}
|
|
100
|
+
return success(this.factory.parseStore(this.context, response.stock_location));
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (debug.enabled) {
|
|
103
|
+
debug(`Error fetching stock location ${payload.identifier.key}`, err);
|
|
104
|
+
}
|
|
105
|
+
return error({ type: "NotFound", identifier: payload.identifier });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Extension point resolving a stock location's geo coordinates. Medusa
|
|
110
|
+
* stock locations carry no first-class coordinates, so by default they are
|
|
111
|
+
* read from the location metadata keys `latitude` and `longitude`. Returns
|
|
112
|
+
* undefined when the location has no usable coordinates.
|
|
113
|
+
*/
|
|
114
|
+
parseLocationCoordinates(location) {
|
|
115
|
+
const latitude = Number(location.metadata?.["latitude"]);
|
|
116
|
+
const longitude = Number(location.metadata?.["longitude"]);
|
|
117
|
+
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
|
|
118
|
+
return void 0;
|
|
119
|
+
}
|
|
120
|
+
return { latitude, longitude };
|
|
121
|
+
}
|
|
122
|
+
async queryByProximity(payload) {
|
|
123
|
+
const pickupLocations = await this.fetchPickupLocations();
|
|
124
|
+
const withinDistance = [];
|
|
125
|
+
for (const location of pickupLocations) {
|
|
126
|
+
const coordinates = this.parseLocationCoordinates(location);
|
|
127
|
+
if (!coordinates) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const distance = haversineDistanceKm(
|
|
131
|
+
payload.latitude,
|
|
132
|
+
payload.longitude,
|
|
133
|
+
coordinates.latitude,
|
|
134
|
+
coordinates.longitude
|
|
135
|
+
);
|
|
136
|
+
if (distance <= payload.distance) {
|
|
137
|
+
withinDistance.push({ location, distance });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
withinDistance.sort((a, b) => a.distance - b.distance);
|
|
141
|
+
return success(
|
|
142
|
+
withinDistance.slice(0, payload.limit).map((entry) => this.factory.parseStore(this.context, entry.location))
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
__decorateClass([
|
|
147
|
+
Reactionary({
|
|
148
|
+
inputSchema: StoreQueryForListSchema,
|
|
149
|
+
outputSchema: StorePaginatedResultSchema
|
|
150
|
+
})
|
|
151
|
+
], MedusaStoreCapability.prototype, "list", 1);
|
|
152
|
+
__decorateClass([
|
|
153
|
+
Reactionary({
|
|
154
|
+
inputSchema: StoreQueryByIdSchema,
|
|
155
|
+
outputSchema: StoreSchema
|
|
156
|
+
})
|
|
157
|
+
], MedusaStoreCapability.prototype, "getById", 1);
|
|
158
|
+
__decorateClass([
|
|
159
|
+
Reactionary({
|
|
160
|
+
inputSchema: StoreQueryByProximitySchema,
|
|
161
|
+
outputSchema: z.array(StoreSchema)
|
|
162
|
+
})
|
|
163
|
+
], MedusaStoreCapability.prototype, "queryByProximity", 1);
|
|
164
|
+
export {
|
|
165
|
+
MedusaStoreCapability
|
|
166
|
+
};
|
package/core/initialize.js
CHANGED
|
@@ -23,7 +23,8 @@ import {
|
|
|
23
23
|
ProductSearchResultSchema,
|
|
24
24
|
ProfileSchema,
|
|
25
25
|
PersonalizationProfileSchema,
|
|
26
|
-
ShippingMethodSchema
|
|
26
|
+
ShippingMethodSchema,
|
|
27
|
+
StoreSchema
|
|
27
28
|
} from "@reactionary/core";
|
|
28
29
|
import { MedusaCartCapability } from "../capabilities/cart.capability.js";
|
|
29
30
|
import { MedusaCategoryCapability } from "../capabilities/category.capability.js";
|
|
@@ -43,6 +44,7 @@ import { MedusaProductCapability } from "../capabilities/product.capability.js";
|
|
|
43
44
|
import { MedusaProductAssociationsCapability } from "../capabilities/product-associations.capability.js";
|
|
44
45
|
import { MedusaProfileCapability } from "../capabilities/profile.capability.js";
|
|
45
46
|
import { MedusaPersonalizationProfileCapability } from "../capabilities/personalization-profile.capability.js";
|
|
47
|
+
import { MedusaStoreCapability } from "../capabilities/store.capability.js";
|
|
46
48
|
import {
|
|
47
49
|
MedusaCapabilitiesSchema
|
|
48
50
|
} from "../schema/capabilities.schema.js";
|
|
@@ -66,7 +68,8 @@ import {
|
|
|
66
68
|
MedusaProductFactory,
|
|
67
69
|
MedusaProductSearchFactory,
|
|
68
70
|
MedusaProfileFactory,
|
|
69
|
-
MedusaPersonalizationProfileFactory
|
|
71
|
+
MedusaPersonalizationProfileFactory,
|
|
72
|
+
MedusaStoreFactory
|
|
70
73
|
} from "../factories/index.js";
|
|
71
74
|
import {
|
|
72
75
|
resolveCapabilityWithFactory,
|
|
@@ -374,6 +377,22 @@ function withMedusaCapabilities(configuration, capabilities) {
|
|
|
374
377
|
buildCapabilityArgs
|
|
375
378
|
);
|
|
376
379
|
}
|
|
380
|
+
if (caps.store?.enabled) {
|
|
381
|
+
client.store = resolveCapabilityWithFactory(
|
|
382
|
+
capabilities.store,
|
|
383
|
+
{
|
|
384
|
+
factory: new MedusaStoreFactory(StoreSchema),
|
|
385
|
+
capability: (args) => new MedusaStoreCapability(
|
|
386
|
+
args.config,
|
|
387
|
+
args.cache,
|
|
388
|
+
args.context,
|
|
389
|
+
args.medusaApi,
|
|
390
|
+
args.factory
|
|
391
|
+
)
|
|
392
|
+
},
|
|
393
|
+
buildCapabilityArgs
|
|
394
|
+
);
|
|
395
|
+
}
|
|
377
396
|
if (caps.personalizationProfile?.enabled) {
|
|
378
397
|
client.personalizationProfile = resolveCapabilityWithFactory(
|
|
379
398
|
capabilities.personalizationProfile,
|
|
@@ -105,15 +105,23 @@ class MedusaCheckoutFactory {
|
|
|
105
105
|
const backupUnattendedDelivery = data.metadata?.["consent_for_unattended_delivery"] !== void 0 ? data.metadata?.["consent_for_unattended_delivery"] === "true" : void 0;
|
|
106
106
|
const backupInstructions = data.metadata?.["instructions"] != null ? String(data.metadata["instructions"]) : void 0;
|
|
107
107
|
const backupPickupPoint = data.metadata?.["pickup_point"] != null ? String(data.metadata["pickup_point"]) : void 0;
|
|
108
|
+
const backupPickupStore = data.metadata?.["pickup_store"] != null && data.metadata["pickup_store"] !== "" ? String(data.metadata["pickup_store"]) : void 0;
|
|
109
|
+
const backupShippingProtocolData = this.parseShippingProtocolData(
|
|
110
|
+
data.metadata?.["shipping_protocol_data"]
|
|
111
|
+
);
|
|
108
112
|
let shippingInstruction;
|
|
109
113
|
data.shipping_methods?.forEach((sm) => {
|
|
110
114
|
let pickupPoint = "";
|
|
111
115
|
let instructions = "";
|
|
112
116
|
let consentForUnattendedDelivery = false;
|
|
117
|
+
let pickupStore;
|
|
118
|
+
let protocolData;
|
|
113
119
|
if (sm.data) {
|
|
114
120
|
pickupPoint = sm.data["pickup_point"] != null ? String(sm.data["pickup_point"]) : "";
|
|
115
121
|
instructions = sm.data["instructions"] != null ? String(sm.data["instructions"]) : "";
|
|
116
122
|
consentForUnattendedDelivery = sm.data["consent_for_unattended_delivery"] === "true";
|
|
123
|
+
pickupStore = sm.data["pickup_store"] != null && sm.data["pickup_store"] !== "" ? String(sm.data["pickup_store"]) : void 0;
|
|
124
|
+
protocolData = this.parseShippingProtocolData(sm.data["shipping_protocol_data"]);
|
|
117
125
|
}
|
|
118
126
|
if (!pickupPoint) {
|
|
119
127
|
pickupPoint = backupPickupPoint || "";
|
|
@@ -124,11 +132,19 @@ class MedusaCheckoutFactory {
|
|
|
124
132
|
if (!consentForUnattendedDelivery) {
|
|
125
133
|
consentForUnattendedDelivery = backupUnattendedDelivery || false;
|
|
126
134
|
}
|
|
135
|
+
if (!pickupStore) {
|
|
136
|
+
pickupStore = backupPickupStore;
|
|
137
|
+
}
|
|
138
|
+
if (!protocolData) {
|
|
139
|
+
protocolData = backupShippingProtocolData;
|
|
140
|
+
}
|
|
127
141
|
shippingInstruction = {
|
|
128
|
-
shippingMethod: { key: sm.shipping_option_id },
|
|
142
|
+
shippingMethod: { key: sm.shipping_option_id || "" },
|
|
129
143
|
consentForUnattendedDelivery,
|
|
130
144
|
instructions,
|
|
131
|
-
pickupPoint
|
|
145
|
+
pickupPoint,
|
|
146
|
+
store: pickupStore ? { key: pickupStore } : void 0,
|
|
147
|
+
protocolData
|
|
132
148
|
};
|
|
133
149
|
});
|
|
134
150
|
const paymentInstructions = new Array();
|
|
@@ -192,10 +208,71 @@ class MedusaCheckoutFactory {
|
|
|
192
208
|
value: data.calculated_price.calculated_amount || 0,
|
|
193
209
|
currency: data.calculated_price.currency_code?.toUpperCase() || _context.languageContext.currencyCode
|
|
194
210
|
}),
|
|
195
|
-
deliveryTime: ""
|
|
211
|
+
deliveryTime: "",
|
|
212
|
+
fulfillmentType: this.parseFulfillmentType(_context, data),
|
|
213
|
+
protocolData: this.parseShippingMethodProtocolData(_context, data)
|
|
196
214
|
};
|
|
197
215
|
return this.shippingMethodSchema.parse(sm);
|
|
198
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Extension point to control how a Medusa shipping option maps to the core
|
|
219
|
+
* fulfillment type. A shipping option belonging to a fulfillment set of type
|
|
220
|
+
* "pickup" is in-store pickup (BOPIS); everything else is a delivery.
|
|
221
|
+
*/
|
|
222
|
+
parseFulfillmentType(_context, data) {
|
|
223
|
+
return data.service_zone?.fulfillment_set?.type === "pickup" ? "pickup" : "delivery";
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Extension point mapping a Medusa shipping option's metadata to the
|
|
227
|
+
* shipping method's protocolData, so medusa-side providers can push
|
|
228
|
+
* additional data (e.g. available pickup time slots) that reaches the
|
|
229
|
+
* client. String and JSON-object values are kept as-is, other primitives
|
|
230
|
+
* are stringified, and arrays/null entries are skipped.
|
|
231
|
+
*/
|
|
232
|
+
parseShippingMethodProtocolData(_context, data) {
|
|
233
|
+
if (!data.metadata) {
|
|
234
|
+
return void 0;
|
|
235
|
+
}
|
|
236
|
+
const entries = [];
|
|
237
|
+
for (const [key, value] of Object.entries(data.metadata)) {
|
|
238
|
+
if (typeof value === "string") {
|
|
239
|
+
entries.push({ key, value });
|
|
240
|
+
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
241
|
+
entries.push({ key, value: String(value) });
|
|
242
|
+
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
243
|
+
entries.push({ key, value });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return entries.length > 0 ? entries : void 0;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Parses shipping protocol data persisted as a JSON object string (keys
|
|
250
|
+
* mapping to string or JSON-object values) back into protocol data entries.
|
|
251
|
+
* Returns undefined when nothing (valid) is stored, keeping the field absent
|
|
252
|
+
* for carts that never set it.
|
|
253
|
+
*/
|
|
254
|
+
parseShippingProtocolData(raw) {
|
|
255
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
256
|
+
return void 0;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const parsed = JSON.parse(raw);
|
|
260
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
261
|
+
return void 0;
|
|
262
|
+
}
|
|
263
|
+
const entries = [];
|
|
264
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
265
|
+
if (typeof value === "string") {
|
|
266
|
+
entries.push({ key, value });
|
|
267
|
+
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
268
|
+
entries.push({ key, value });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return entries.length > 0 ? entries : void 0;
|
|
272
|
+
} catch {
|
|
273
|
+
return void 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
199
276
|
parsePaymentInstruction(_context, remotePayment, checkout) {
|
|
200
277
|
const mainSession = (remotePayment.payment_sessions || []).find(
|
|
201
278
|
(session) => session.status !== "canceled" && session.status !== "error"
|
package/factories/index.js
CHANGED
|
@@ -15,3 +15,4 @@ export * from "./product-associations/product-associations.factory.js";
|
|
|
15
15
|
export * from "./product-search/product-search.factory.js";
|
|
16
16
|
export * from "./profile/profile.factory.js";
|
|
17
17
|
export * from "./personalization-profile/personalization-profile.factory.js";
|
|
18
|
+
export * from "./store/store.factory.js";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { AddressIdentifierSchema } from "@reactionary/core";
|
|
2
|
+
class MedusaStoreFactory {
|
|
3
|
+
storeSchema;
|
|
4
|
+
constructor(storeSchema) {
|
|
5
|
+
this.storeSchema = storeSchema;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Extension point to control the parsing of the stock location address
|
|
9
|
+
* into the store's address.
|
|
10
|
+
*/
|
|
11
|
+
parseAddress(_context, data) {
|
|
12
|
+
if (!data.address) {
|
|
13
|
+
return void 0;
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
identifier: AddressIdentifierSchema.parse({
|
|
17
|
+
nickName: data.address.id
|
|
18
|
+
}),
|
|
19
|
+
firstName: "",
|
|
20
|
+
lastName: data.address.company || data.name,
|
|
21
|
+
streetAddress: data.address.address_1 || "",
|
|
22
|
+
streetNumber: data.address.address_2 || "",
|
|
23
|
+
city: data.address.city || "",
|
|
24
|
+
region: data.address.province || "",
|
|
25
|
+
postalCode: data.address.postal_code || "",
|
|
26
|
+
countryCode: data.address.country_code || ""
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
parseStore(context, data) {
|
|
30
|
+
const identifier = {
|
|
31
|
+
key: data.id
|
|
32
|
+
};
|
|
33
|
+
const fulfillmentCenter = {
|
|
34
|
+
key: data.name
|
|
35
|
+
};
|
|
36
|
+
const openingHours = data.metadata?.["opening_hours"] != null ? String(data.metadata["opening_hours"]) : void 0;
|
|
37
|
+
const result = {
|
|
38
|
+
identifier,
|
|
39
|
+
name: data.name,
|
|
40
|
+
fulfillmentCenter,
|
|
41
|
+
address: this.parseAddress(context, data),
|
|
42
|
+
openingHours
|
|
43
|
+
};
|
|
44
|
+
return this.storeSchema.parse(result);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
MedusaStoreFactory
|
|
49
|
+
};
|
package/index.js
CHANGED
|
@@ -23,3 +23,4 @@ export * from "./capabilities/category.capability.js";
|
|
|
23
23
|
export * from "./capabilities/checkout.capability.js";
|
|
24
24
|
export * from "./capabilities/product.capability.js";
|
|
25
25
|
export * from "./capabilities/personalization-profile.capability.js";
|
|
26
|
+
export * from "./capabilities/store.capability.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reactionary/medusa",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.54",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@medusajs/js-sdk": "^2.13.3",
|
|
13
|
-
"@reactionary/core": "0.10.
|
|
13
|
+
"@reactionary/core": "0.10.54",
|
|
14
14
|
"debug": "^4.4.3",
|
|
15
15
|
"@medusajs/types": "^2.13.3",
|
|
16
16
|
"zod": "4.1.9"
|
|
@@ -24,7 +24,8 @@ const MedusaCapabilitiesSchema = CapabilitiesSchema.pick({
|
|
|
24
24
|
profile: true,
|
|
25
25
|
productAssociations: true,
|
|
26
26
|
personalizationProfile: true,
|
|
27
|
-
company: true
|
|
27
|
+
company: true,
|
|
28
|
+
store: true
|
|
28
29
|
}).extend({
|
|
29
30
|
product: OverridableCapabilitySchema.optional(),
|
|
30
31
|
productSearch: OverridableCapabilitySchema.optional(),
|
|
@@ -43,7 +44,8 @@ const MedusaCapabilitiesSchema = CapabilitiesSchema.pick({
|
|
|
43
44
|
profile: OverridableCapabilitySchema.optional(),
|
|
44
45
|
productAssociations: OverridableCapabilitySchema.optional(),
|
|
45
46
|
productRecommendations: DirectCapabilitySchema.optional(),
|
|
46
|
-
personalizationProfile: OverridableCapabilitySchema.optional()
|
|
47
|
+
personalizationProfile: OverridableCapabilitySchema.optional(),
|
|
48
|
+
store: OverridableCapabilitySchema.optional()
|
|
47
49
|
}).partial();
|
|
48
50
|
export {
|
|
49
51
|
MedusaCapabilitiesSchema
|
package/schema/medusa.schema.js
CHANGED
|
@@ -20,7 +20,8 @@ const MedusaSessionSchema = z.looseObject({
|
|
|
20
20
|
token: z.string().optional(),
|
|
21
21
|
expires: z.string().optional(),
|
|
22
22
|
allRegions: z.array(MedusaRegionSchema).optional(),
|
|
23
|
-
selectedRegion: MedusaRegionSchema.optional()
|
|
23
|
+
selectedRegion: MedusaRegionSchema.optional(),
|
|
24
|
+
finalizedOrders: z.record(z.string(), MedusaOrderIdentifierSchema).optional().meta({ description: "Orders created by finalizing a checkout in this session, keyed by the originating cart id. Lets checkout reads report the resulting order when the Medusa store cart endpoint does not expose the cart-to-order relation." })
|
|
24
25
|
});
|
|
25
26
|
export {
|
|
26
27
|
MedusaCachedPluginListSchema,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { StoreAddCartShippingMethods, StoreCalculatedPrice, StoreInitializePaymentSession, StoreUpdateCart } from '@medusajs/types';
|
|
2
|
-
import type { Address, Cache, CheckoutFactory, CheckoutFactoryCheckoutOutput, CheckoutFactoryPaymentMethodOutput, CheckoutFactoryShippingMethodOutput, CheckoutFactoryWithOutput, CheckoutIdentifier, CheckoutMutationAddPaymentInstruction, CheckoutMutationFinalizeCheckout, CheckoutMutationInitiateCheckout, CheckoutMutationRemovePaymentInstruction, CheckoutMutationSetShippingAddress, CheckoutMutationSetShippingInstruction, CheckoutQueryById, CheckoutQueryForAvailablePaymentMethods, CheckoutQueryForAvailableShippingMethods, NotFoundError, RequestContext, Result } from '@reactionary/core';
|
|
1
|
+
import type { StoreAddCartShippingMethods, StoreCalculatedPrice, StoreInitializePaymentSession, StoreOrder, StoreUpdateCart } from '@medusajs/types';
|
|
2
|
+
import type { Address, Cache, CheckoutFactory, CheckoutFactoryCheckoutOutput, CheckoutFactoryPaymentMethodOutput, CheckoutFactoryShippingMethodOutput, CheckoutFactoryWithOutput, CheckoutIdentifier, CheckoutMutationAddPaymentInstruction, CheckoutMutationFinalizeCheckout, CheckoutMutationInitiateCheckout, CheckoutMutationRemovePaymentInstruction, CheckoutMutationSetShippingAddress, CheckoutMutationSetShippingInstruction, CheckoutQueryById, CheckoutQueryForAvailablePaymentMethods, CheckoutQueryForAvailableShippingMethods, NotFoundError, RequestContext, Result, ShippingProtocolData, StoreIdentifier } from '@reactionary/core';
|
|
3
3
|
import { CheckoutCapability } from '@reactionary/core';
|
|
4
|
-
import type
|
|
4
|
+
import { type MedusaAPI } from '../core/client.js';
|
|
5
5
|
import type { MedusaCheckoutFactory } from '../factories/checkout/checkout.factory.js';
|
|
6
6
|
import type { MedusaConfiguration } from '../schema/configuration.schema.js';
|
|
7
|
+
import { type MedusaOrderIdentifier } from '../schema/medusa.schema.js';
|
|
7
8
|
export declare class CheckoutNotReadyForFinalizationError extends Error {
|
|
8
9
|
checkoutIdentifier: CheckoutIdentifier;
|
|
9
10
|
constructor(checkoutIdentifier: CheckoutIdentifier);
|
|
@@ -19,6 +20,16 @@ export declare class MedusaCheckoutCapability<TFactory extends CheckoutFactory =
|
|
|
19
20
|
* example: this.includedFields = [includedFields, '+discounts.*'].join(',');
|
|
20
21
|
*/
|
|
21
22
|
protected includedFields: string;
|
|
23
|
+
/**
|
|
24
|
+
* This controls which fields are requested for the cart's shipping options.
|
|
25
|
+
* You can override this in a subclass to add more fields as needed.
|
|
26
|
+
*
|
|
27
|
+
* The service zone's fulfillment set tells us whether an option is in-store
|
|
28
|
+
* pickup (BOPIS) or a delivery, and the option metadata lets medusa-side
|
|
29
|
+
* providers push additional data that is mapped to the shipping method's
|
|
30
|
+
* protocolData.
|
|
31
|
+
*/
|
|
32
|
+
protected shippingOptionFields: string;
|
|
22
33
|
constructor(config: MedusaConfiguration, cache: Cache, context: RequestContext, medusaApi: MedusaAPI, factory: CheckoutFactoryWithOutput<TFactory>);
|
|
23
34
|
protected initiateCheckoutForCartPayload(payload: CheckoutMutationInitiateCheckout): StoreUpdateCart;
|
|
24
35
|
initiateCheckoutForCart(payload: CheckoutMutationInitiateCheckout): Promise<Result<CheckoutFactoryCheckoutOutput<TFactory>>>;
|
|
@@ -27,6 +38,7 @@ export declare class MedusaCheckoutCapability<TFactory extends CheckoutFactory =
|
|
|
27
38
|
setShippingAddress(payload: CheckoutMutationSetShippingAddress): Promise<Result<CheckoutFactoryCheckoutOutput<TFactory>>>;
|
|
28
39
|
protected getAvailableShippingMethodsPayload(payload: CheckoutQueryForAvailableShippingMethods): {
|
|
29
40
|
cart_id: string;
|
|
41
|
+
fields: string;
|
|
30
42
|
};
|
|
31
43
|
/**
|
|
32
44
|
* Whether a Medusa shipping option's calculated price is usable, i.e. has an
|
|
@@ -48,8 +60,40 @@ export declare class MedusaCheckoutCapability<TFactory extends CheckoutFactory =
|
|
|
48
60
|
addPaymentInstruction(payload: CheckoutMutationAddPaymentInstruction): Promise<Result<CheckoutFactoryCheckoutOutput<TFactory>>>;
|
|
49
61
|
removePaymentInstruction(payload: CheckoutMutationRemovePaymentInstruction): Promise<Result<CheckoutFactoryCheckoutOutput<TFactory>>>;
|
|
50
62
|
protected setShippingInstructionPayload(payload: CheckoutMutationSetShippingInstruction): StoreAddCartShippingMethods;
|
|
63
|
+
/**
|
|
64
|
+
* Serializes the client-set shipping protocol data as a JSON object string
|
|
65
|
+
* for persistence in the cart metadata, or undefined when none is set.
|
|
66
|
+
*/
|
|
67
|
+
protected serializeShippingProtocolData(protocolData: ShippingProtocolData[] | undefined): string | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Resolves the shipping address to use for in-store pickup (BOPIS): the
|
|
70
|
+
* address of the selected store, so the order ships to the store. Returns
|
|
71
|
+
* undefined when the store or its address is unknown, leaving the cart's
|
|
72
|
+
* shipping address untouched.
|
|
73
|
+
*/
|
|
74
|
+
protected resolveStorePickupAddress(store: StoreIdentifier): Promise<Partial<Address> | undefined>;
|
|
51
75
|
setShippingInstruction(payload: CheckoutMutationSetShippingInstruction): Promise<Result<CheckoutFactoryCheckoutOutput<TFactory>>>;
|
|
52
76
|
finalizeCheckout(payload: CheckoutMutationFinalizeCheckout): Promise<Result<CheckoutFactoryCheckoutOutput<TFactory>>>;
|
|
77
|
+
/**
|
|
78
|
+
* Builds the order identifier from a completed-cart response's order,
|
|
79
|
+
* following the same display-id precedence as the checkout factory
|
|
80
|
+
* (custom_display_id over display_id).
|
|
81
|
+
*/
|
|
82
|
+
protected parseCompletedOrderIdentifier(order: StoreOrder & {
|
|
83
|
+
custom_display_id?: string | number;
|
|
84
|
+
}): MedusaOrderIdentifier;
|
|
85
|
+
/**
|
|
86
|
+
* Remembers which order a finalized cart produced, so later checkout reads
|
|
87
|
+
* in the same session can report the resulting order even when the Medusa
|
|
88
|
+
* store cart endpoint does not expose the cart->order relation.
|
|
89
|
+
*/
|
|
90
|
+
protected rememberFinalizedOrder(cartKey: string, order: MedusaOrderIdentifier): void;
|
|
91
|
+
/**
|
|
92
|
+
* Fills in a missing resultingOrder from the session's finalized-orders map.
|
|
93
|
+
* Checkouts that already carry a resulting order (older Medusa versions
|
|
94
|
+
* still hydrate the +order.* fields) are returned unchanged.
|
|
95
|
+
*/
|
|
96
|
+
protected hydrateResultingOrder(checkout: CheckoutFactoryCheckoutOutput<TFactory>): CheckoutFactoryCheckoutOutput<TFactory>;
|
|
53
97
|
/**
|
|
54
98
|
* Extension point to map an Address to a Store Address
|
|
55
99
|
* @param address
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AdminStockLocation } from '@medusajs/types';
|
|
2
|
+
import type { Cache, NotFoundError, RequestContext, Result, StoreFactory, StoreFactoryOutput, StoreFactoryWithOutput, StorePaginatedResult, StoreQueryById, StoreQueryByProximity, StoreQueryForList } from '@reactionary/core';
|
|
3
|
+
import { StoreCapability } from '@reactionary/core';
|
|
4
|
+
import { type MedusaAPI } from '../core/client.js';
|
|
5
|
+
import type { MedusaStoreFactory } from '../factories/store/store.factory.js';
|
|
6
|
+
import type { MedusaConfiguration } from '../schema/configuration.schema.js';
|
|
7
|
+
/**
|
|
8
|
+
* Exposes Medusa stock locations that have a fulfillment set of type "pickup"
|
|
9
|
+
* as the brand's own stores, for in-store pickup (BOPIS).
|
|
10
|
+
*/
|
|
11
|
+
export declare class MedusaStoreCapability<TFactory extends StoreFactory = MedusaStoreFactory> extends StoreCapability<StoreFactoryOutput<TFactory>> {
|
|
12
|
+
medusaApi: MedusaAPI;
|
|
13
|
+
protected config: MedusaConfiguration;
|
|
14
|
+
protected factory: StoreFactoryWithOutput<TFactory>;
|
|
15
|
+
/**
|
|
16
|
+
* The fields requested for stock locations. Override in a subclass if a
|
|
17
|
+
* project needs more relations.
|
|
18
|
+
*/
|
|
19
|
+
protected stockLocationFields: string;
|
|
20
|
+
constructor(config: MedusaConfiguration, cache: Cache, context: RequestContext, medusaApi: MedusaAPI, factory: StoreFactoryWithOutput<TFactory>);
|
|
21
|
+
/**
|
|
22
|
+
* Whether a stock location is a store customers can pick up orders from,
|
|
23
|
+
* i.e. it has a fulfillment set of type "pickup".
|
|
24
|
+
*/
|
|
25
|
+
protected isPickupLocation(location: AdminStockLocation): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Fetches all stock locations that are pickup stores. Medusa installations
|
|
28
|
+
* have a small, bounded number of stock locations, so fetching them all and
|
|
29
|
+
* filtering client-side is acceptable.
|
|
30
|
+
*/
|
|
31
|
+
protected fetchPickupLocations(): Promise<AdminStockLocation[]>;
|
|
32
|
+
list(payload: StoreQueryForList): Promise<Result<StorePaginatedResult>>;
|
|
33
|
+
getById(payload: StoreQueryById): Promise<Result<StoreFactoryOutput<TFactory>, NotFoundError>>;
|
|
34
|
+
/**
|
|
35
|
+
* Extension point resolving a stock location's geo coordinates. Medusa
|
|
36
|
+
* stock locations carry no first-class coordinates, so by default they are
|
|
37
|
+
* read from the location metadata keys `latitude` and `longitude`. Returns
|
|
38
|
+
* undefined when the location has no usable coordinates.
|
|
39
|
+
*/
|
|
40
|
+
protected parseLocationCoordinates(location: AdminStockLocation): {
|
|
41
|
+
latitude: number;
|
|
42
|
+
longitude: number;
|
|
43
|
+
} | undefined;
|
|
44
|
+
queryByProximity(payload: StoreQueryByProximity): Promise<Result<Array<StoreFactoryOutput<TFactory>>>>;
|
|
45
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CartFactory, CategoryFactory, CheckoutFactory, ClientFromCapabilities, CompanyFactory, CompanyRegistrationFactory, EmployeeFactory, EmployeeInvitationFactory, InventoryFactory, PersonalizationProfileFactory, OrderFactory, OrderSearchFactory, PriceFactory, ProductAssociationsFactory, ProductFactory, ProductSearchFactory, ProfileFactory } from '@reactionary/core';
|
|
1
|
+
import type { CartFactory, CategoryFactory, CheckoutFactory, ClientFromCapabilities, CompanyFactory, CompanyRegistrationFactory, EmployeeFactory, EmployeeInvitationFactory, InventoryFactory, PersonalizationProfileFactory, OrderFactory, OrderSearchFactory, PriceFactory, ProductAssociationsFactory, ProductFactory, ProductSearchFactory, ProfileFactory, StoreFactory } from '@reactionary/core';
|
|
2
2
|
import type { MedusaCapabilities } from '../schema/capabilities.schema.js';
|
|
3
3
|
import type { MedusaCartFactory } from '../factories/cart/cart.factory.js';
|
|
4
4
|
import type { MedusaCategoryFactory } from '../factories/category/category.factory.js';
|
|
@@ -15,6 +15,7 @@ import type { MedusaProductAssociationsFactory } from '../factories/product-asso
|
|
|
15
15
|
import type { MedusaProductFactory } from '../factories/product/product.factory.js';
|
|
16
16
|
import type { MedusaProductSearchFactory } from '../factories/product-search/product-search.factory.js';
|
|
17
17
|
import type { MedusaProfileFactory } from '../factories/profile/profile.factory.js';
|
|
18
|
+
import type { MedusaStoreFactory } from '../factories/store/store.factory.js';
|
|
18
19
|
import type { MedusaPersonalizationProfileFactory } from '../factories/personalization-profile/personalization-profile.factory.js';
|
|
19
20
|
import type { MedusaCartCapability } from '../capabilities/cart.capability.js';
|
|
20
21
|
import type { MedusaCategoryCapability } from '../capabilities/category.capability.js';
|
|
@@ -31,10 +32,11 @@ import type { MedusaProductAssociationsCapability } from '../capabilities/produc
|
|
|
31
32
|
import type { MedusaProductCapability } from '../capabilities/product.capability.js';
|
|
32
33
|
import type { MedusaProductSearchCapability } from '../capabilities/product-search.capability.js';
|
|
33
34
|
import type { MedusaProfileCapability } from '../capabilities/profile.capability.js';
|
|
35
|
+
import type { MedusaStoreCapability } from '../capabilities/store.capability.js';
|
|
34
36
|
import type { MedusaPersonalizationProfileCapability } from '../capabilities/personalization-profile.capability.js';
|
|
35
37
|
import type { MedusaProductRecommendationsCapability } from '../capabilities/product-recommendations.capability.js';
|
|
36
38
|
import type { MedusaIdentityCapability } from '../capabilities/identity.capability.js';
|
|
37
|
-
type OverridableCapabilityKey = 'product' | 'productSearch' | 'cart' | 'checkout' | 'category' | 'price' | 'order' | 'orderSearch' | 'inventory' | 'profile' | 'productAssociations' | 'personalizationProfile' | 'employee' | 'employeeInvitation' | 'company' | 'companyRegistration';
|
|
39
|
+
type OverridableCapabilityKey = 'product' | 'productSearch' | 'cart' | 'checkout' | 'category' | 'price' | 'order' | 'orderSearch' | 'inventory' | 'profile' | 'productAssociations' | 'personalizationProfile' | 'employee' | 'employeeInvitation' | 'company' | 'companyRegistration' | 'store';
|
|
38
40
|
type EnabledCapability<TCapability> = TCapability extends {
|
|
39
41
|
enabled: true;
|
|
40
42
|
} ? true : false;
|
|
@@ -69,6 +71,7 @@ type FactoryContractMap = {
|
|
|
69
71
|
employeeInvitation: EmployeeInvitationFactory;
|
|
70
72
|
company: CompanyFactory;
|
|
71
73
|
companyRegistration: CompanyRegistrationFactory;
|
|
74
|
+
store: StoreFactory;
|
|
72
75
|
};
|
|
73
76
|
type DefaultFactoryMap = {
|
|
74
77
|
product: MedusaProductFactory;
|
|
@@ -87,6 +90,7 @@ type DefaultFactoryMap = {
|
|
|
87
90
|
employeeInvitation: MedusaEmployeeInvitationFactory;
|
|
88
91
|
company: MedusaCompanyFactory;
|
|
89
92
|
companyRegistration: MedusaCompanyRegistrationFactory;
|
|
93
|
+
store: MedusaStoreFactory;
|
|
90
94
|
};
|
|
91
95
|
type ResolvedFactoryMap<T extends MedusaCapabilities> = {
|
|
92
96
|
[K in OverridableCapabilityKey]: ExtractCapabilityFactory<T[K], FactoryContractMap[K], DefaultFactoryMap[K]>;
|
|
@@ -108,6 +112,7 @@ type DefaultCapabilityMap<T extends MedusaCapabilities> = {
|
|
|
108
112
|
employeeInvitation: MedusaEmployeeInvitationCapability<ResolvedFactoryMap<T>['employeeInvitation']>;
|
|
109
113
|
company: MedusaCompanyCapability<ResolvedFactoryMap<T>['company']>;
|
|
110
114
|
companyRegistration: MedusaCompanyRegistrationCapability<ResolvedFactoryMap<T>['companyRegistration']>;
|
|
115
|
+
store: MedusaStoreCapability<ResolvedFactoryMap<T>['store']>;
|
|
111
116
|
identity: MedusaIdentityCapability;
|
|
112
117
|
productRecommendations: MedusaProductRecommendationsCapability;
|
|
113
118
|
};
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import type { StorePaymentCollection } from '@medusajs/types';
|
|
2
2
|
import { type StoreCart, type StoreCartAddress, type StoreCartLineItem, type StoreCartShippingOptionWithServiceZone, type StorePaymentProvider } from '@medusajs/types';
|
|
3
|
-
import { type Address, type AnyCheckoutSchema, type AnyPaymentMethodSchema, type AnyShippingMethodSchema, type CheckoutFactory, type CheckoutItem, type CheckoutSchema, type CostBreakDown, type Currency, type ItemCostBreakdown, type PaymentInstruction, type PaymentMethodIdentifier, type PaymentMethodSchema, type RequestContext, type ShippingInstruction, type ShippingMethodSchema } from '@reactionary/core';
|
|
3
|
+
import { type Address, type AnyCheckoutSchema, type AnyPaymentMethodSchema, type AnyShippingMethodSchema, type CheckoutFactory, type CheckoutItem, type CheckoutSchema, type CostBreakDown, type Currency, type ItemCostBreakdown, type PaymentInstruction, type PaymentMethodIdentifier, type PaymentMethodSchema, type RequestContext, type ShippingFulfillmentType, type ShippingInstruction, type ShippingMethodSchema, type ShippingProtocolData } from '@reactionary/core';
|
|
4
4
|
import type * as z from 'zod';
|
|
5
|
+
/**
|
|
6
|
+
* A cart shipping option with the optional metadata relation, which the
|
|
7
|
+
* Medusa HTTP types do not declare on the store-level shipping option even
|
|
8
|
+
* though it can be requested via the fields parameter.
|
|
9
|
+
*/
|
|
10
|
+
export type MedusaShippingOptionWithMetadata = StoreCartShippingOptionWithServiceZone & {
|
|
11
|
+
metadata?: Record<string, unknown> | null;
|
|
12
|
+
};
|
|
5
13
|
export declare class MedusaCheckoutFactory<TCheckoutSchema extends AnyCheckoutSchema = typeof CheckoutSchema, TShippingMethodSchema extends AnyShippingMethodSchema = typeof ShippingMethodSchema, TPaymentMethodSchema extends AnyPaymentMethodSchema = typeof PaymentMethodSchema> implements CheckoutFactory<TCheckoutSchema, TShippingMethodSchema, TPaymentMethodSchema> {
|
|
6
14
|
readonly checkoutSchema: TCheckoutSchema;
|
|
7
15
|
readonly shippingMethodSchema: TShippingMethodSchema;
|
|
@@ -36,7 +44,28 @@ export declare class MedusaCheckoutFactory<TCheckoutSchema extends AnyCheckoutSc
|
|
|
36
44
|
protected isReadyForFinalization(price: CostBreakDown, paymentInstructions: PaymentInstruction[], billingAddress?: Address, shippingAddress?: Address, shippingInstruction?: ShippingInstruction): boolean;
|
|
37
45
|
protected isCart(data: StoreCart): data is StoreCart;
|
|
38
46
|
parseCheckout(context: RequestContext, data: StoreCart): z.output<TCheckoutSchema>;
|
|
39
|
-
parseShippingMethod(_context: RequestContext, data:
|
|
47
|
+
parseShippingMethod(_context: RequestContext, data: MedusaShippingOptionWithMetadata): z.output<TShippingMethodSchema>;
|
|
48
|
+
/**
|
|
49
|
+
* Extension point to control how a Medusa shipping option maps to the core
|
|
50
|
+
* fulfillment type. A shipping option belonging to a fulfillment set of type
|
|
51
|
+
* "pickup" is in-store pickup (BOPIS); everything else is a delivery.
|
|
52
|
+
*/
|
|
53
|
+
protected parseFulfillmentType(_context: RequestContext, data: MedusaShippingOptionWithMetadata): ShippingFulfillmentType;
|
|
54
|
+
/**
|
|
55
|
+
* Extension point mapping a Medusa shipping option's metadata to the
|
|
56
|
+
* shipping method's protocolData, so medusa-side providers can push
|
|
57
|
+
* additional data (e.g. available pickup time slots) that reaches the
|
|
58
|
+
* client. String and JSON-object values are kept as-is, other primitives
|
|
59
|
+
* are stringified, and arrays/null entries are skipped.
|
|
60
|
+
*/
|
|
61
|
+
protected parseShippingMethodProtocolData(_context: RequestContext, data: MedusaShippingOptionWithMetadata): ShippingProtocolData[] | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Parses shipping protocol data persisted as a JSON object string (keys
|
|
64
|
+
* mapping to string or JSON-object values) back into protocol data entries.
|
|
65
|
+
* Returns undefined when nothing (valid) is stored, keeping the field absent
|
|
66
|
+
* for carts that never set it.
|
|
67
|
+
*/
|
|
68
|
+
protected parseShippingProtocolData(raw: unknown): ShippingProtocolData[] | undefined;
|
|
40
69
|
protected parsePaymentInstruction(_context: RequestContext, remotePayment: StorePaymentCollection, checkout: StoreCart): {
|
|
41
70
|
identifier: {
|
|
42
71
|
key: string;
|
package/src/factories/index.d.ts
CHANGED
|
@@ -15,3 +15,4 @@ export * from './product-associations/product-associations.factory.js';
|
|
|
15
15
|
export * from './product-search/product-search.factory.js';
|
|
16
16
|
export * from './profile/profile.factory.js';
|
|
17
17
|
export * from './personalization-profile/personalization-profile.factory.js';
|
|
18
|
+
export * from './store/store.factory.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AdminStockLocation } from '@medusajs/types';
|
|
2
|
+
import type { Address, AnyStoreSchema, RequestContext, StoreFactory, StoreSchema } from '@reactionary/core';
|
|
3
|
+
import type * as z from 'zod';
|
|
4
|
+
export declare class MedusaStoreFactory<TStoreSchema extends AnyStoreSchema = typeof StoreSchema> implements StoreFactory<TStoreSchema> {
|
|
5
|
+
readonly storeSchema: TStoreSchema;
|
|
6
|
+
constructor(storeSchema: TStoreSchema);
|
|
7
|
+
/**
|
|
8
|
+
* Extension point to control the parsing of the stock location address
|
|
9
|
+
* into the store's address.
|
|
10
|
+
*/
|
|
11
|
+
protected parseAddress(_context: RequestContext, data: AdminStockLocation): Address | undefined;
|
|
12
|
+
parseStore(context: RequestContext, data: AdminStockLocation): z.output<TStoreSchema>;
|
|
13
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -23,3 +23,4 @@ export * from './capabilities/category.capability.js';
|
|
|
23
23
|
export * from './capabilities/checkout.capability.js';
|
|
24
24
|
export * from './capabilities/product.capability.js';
|
|
25
25
|
export * from './capabilities/personalization-profile.capability.js';
|
|
26
|
+
export * from './capabilities/store.capability.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CartFactory, CartFactoryWithOutput, CartCapability, CategoryFactory, CategoryFactoryWithOutput, CategoryCapability, CheckoutFactory, CheckoutFactoryWithOutput, CheckoutCapability, CompanyFactory, CompanyFactoryWithOutput, CompanyCapability, CompanyRegistrationFactory, CompanyRegistrationFactoryWithOutput, CompanyRegistrationCapability, EmployeeFactory, EmployeeFactoryWithOutput, EmployeeCapability, EmployeeInvitationFactory, EmployeeInvitationFactoryWithOutput, EmployeeInvitationCapability, IdentityCapability, InventoryFactory, InventoryFactoryWithOutput, InventoryCapability, PersonalizationProfileFactory, PersonalizationProfileFactoryWithOutput, PersonalizationProfileCapability, OrderFactory, OrderFactoryWithOutput, OrderCapability, OrderSearchFactory, OrderSearchFactoryWithOutput, OrderSearchCapability, PriceFactory, PriceFactoryWithOutput, PriceCapability, ProductAssociationsFactory, ProductAssociationsFactoryWithOutput, ProductAssociationsCapability, ProductFactory, ProductFactoryWithOutput, ProductCapability, ProductRecommendationsCapability, ProductSearchFactory, ProductSearchFactoryWithOutput, ProductSearchCapability, ProfileFactory, ProfileFactoryWithOutput, ProfileCapability, RequestContext, Cache } from '@reactionary/core';
|
|
1
|
+
import type { CartFactory, CartFactoryWithOutput, CartCapability, CategoryFactory, CategoryFactoryWithOutput, CategoryCapability, CheckoutFactory, CheckoutFactoryWithOutput, CheckoutCapability, CompanyFactory, CompanyFactoryWithOutput, CompanyCapability, CompanyRegistrationFactory, CompanyRegistrationFactoryWithOutput, CompanyRegistrationCapability, EmployeeFactory, EmployeeFactoryWithOutput, EmployeeCapability, EmployeeInvitationFactory, EmployeeInvitationFactoryWithOutput, EmployeeInvitationCapability, IdentityCapability, InventoryFactory, InventoryFactoryWithOutput, InventoryCapability, PersonalizationProfileFactory, PersonalizationProfileFactoryWithOutput, PersonalizationProfileCapability, OrderFactory, OrderFactoryWithOutput, OrderCapability, OrderSearchFactory, OrderSearchFactoryWithOutput, OrderSearchCapability, PriceFactory, PriceFactoryWithOutput, PriceCapability, ProductAssociationsFactory, ProductAssociationsFactoryWithOutput, ProductAssociationsCapability, ProductFactory, ProductFactoryWithOutput, ProductCapability, ProductRecommendationsCapability, ProductSearchFactory, ProductSearchFactoryWithOutput, ProductSearchCapability, ProfileFactory, ProfileFactoryWithOutput, ProfileCapability, StoreFactory, StoreFactoryWithOutput, StoreCapability, RequestContext, Cache } from '@reactionary/core';
|
|
2
2
|
import type { MedusaConfiguration } from './configuration.schema.js';
|
|
3
3
|
import type { MedusaAPI } from '../core/client.js';
|
|
4
4
|
import * as z from 'zod';
|
|
@@ -91,6 +91,11 @@ export declare const MedusaCapabilitiesSchema: z.ZodObject<{
|
|
|
91
91
|
factory: z.ZodOptional<z.ZodUnknown>;
|
|
92
92
|
capability: z.ZodOptional<z.ZodUnknown>;
|
|
93
93
|
}, z.core.$loose>>>;
|
|
94
|
+
store: z.ZodOptional<z.ZodOptional<z.ZodObject<{
|
|
95
|
+
enabled: z.ZodBoolean;
|
|
96
|
+
factory: z.ZodOptional<z.ZodUnknown>;
|
|
97
|
+
capability: z.ZodOptional<z.ZodUnknown>;
|
|
98
|
+
}, z.core.$loose>>>;
|
|
94
99
|
}, z.core.$loose>;
|
|
95
100
|
export interface MedusaCapabilityFactoryArgs {
|
|
96
101
|
cache: Cache;
|
|
@@ -128,7 +133,8 @@ export type MedusaProfileCapabilityConfig = MedusaCapabilityConfig<ProfileFactor
|
|
|
128
133
|
export type MedusaProductAssociationsCapabilityConfig = MedusaCapabilityConfig<ProductAssociationsFactoryWithOutput<ProductAssociationsFactory>, ProductAssociationsCapability>;
|
|
129
134
|
export type MedusaPersonalizationProfileCapabilityConfig = MedusaCapabilityConfig<PersonalizationProfileFactoryWithOutput<PersonalizationProfileFactory>, PersonalizationProfileCapability>;
|
|
130
135
|
export type MedusaProductRecommendationsCapabilityConfig = MedusaDirectCapabilityConfig<ProductRecommendationsCapability>;
|
|
131
|
-
export type
|
|
136
|
+
export type MedusaStoreCapabilityConfig = MedusaCapabilityConfig<StoreFactoryWithOutput<StoreFactory>, StoreCapability>;
|
|
137
|
+
export type MedusaCapabilities<TProductFactory extends ProductFactory = ProductFactory, TProductCapability extends ProductCapability = ProductCapability, TProductSearchFactory extends ProductSearchFactory = ProductSearchFactory, TProductSearchCapability extends ProductSearchCapability = ProductSearchCapability, TCartFactory extends CartFactory = CartFactory, TCartCapability extends CartCapability = CartCapability, TCheckoutFactory extends CheckoutFactory = CheckoutFactory, TCheckoutCapability extends CheckoutCapability = CheckoutCapability, TCategoryFactory extends CategoryFactory = CategoryFactory, TCategoryCapability extends CategoryCapability = CategoryCapability, TPriceFactory extends PriceFactory = PriceFactory, TPriceCapability extends PriceCapability = PriceCapability, TOrderFactory extends OrderFactory = OrderFactory, TOrderCapability extends OrderCapability = OrderCapability, TOrderSearchFactory extends OrderSearchFactory = OrderSearchFactory, TOrderSearchCapability extends OrderSearchCapability = OrderSearchCapability, TInventoryFactory extends InventoryFactory = InventoryFactory, TInventoryCapability extends InventoryCapability = InventoryCapability, TIdentityCapability extends IdentityCapability = IdentityCapability, TProfileFactory extends ProfileFactory = ProfileFactory, TProfileCapability extends ProfileCapability = ProfileCapability, TProductAssociationsFactory extends ProductAssociationsFactory = ProductAssociationsFactory, TProductAssociationsCapability extends ProductAssociationsCapability = ProductAssociationsCapability, TProductRecommendationsCapability extends ProductRecommendationsCapability = ProductRecommendationsCapability, TPersonalizationProfileFactory extends PersonalizationProfileFactory = PersonalizationProfileFactory, TPersonalizationProfileCapability extends PersonalizationProfileCapability = PersonalizationProfileCapability, TEmployeeFactory extends EmployeeFactory = EmployeeFactory, TEmployeeCapability extends EmployeeCapability = EmployeeCapability, TEmployeeInvitationFactory extends EmployeeInvitationFactory = EmployeeInvitationFactory, TEmployeeInvitationCapability extends EmployeeInvitationCapability = EmployeeInvitationCapability, TCompanyFactory extends CompanyFactory = CompanyFactory, TCompanyCapability extends CompanyCapability = CompanyCapability, TCompanyRegistrationFactory extends CompanyRegistrationFactory = CompanyRegistrationFactory, TCompanyRegistrationCapability extends CompanyRegistrationCapability = CompanyRegistrationCapability, TStoreFactory extends StoreFactory = StoreFactory, TStoreCapability extends StoreCapability = StoreCapability> = {
|
|
132
138
|
product?: MedusaCapabilityConfig<ProductFactoryWithOutput<TProductFactory>, TProductCapability>;
|
|
133
139
|
productSearch?: MedusaCapabilityConfig<ProductSearchFactoryWithOutput<TProductSearchFactory>, TProductSearchCapability>;
|
|
134
140
|
cart?: MedusaCapabilityConfig<CartFactoryWithOutput<TCartFactory>, TCartCapability>;
|
|
@@ -147,5 +153,6 @@ export type MedusaCapabilities<TProductFactory extends ProductFactory = ProductF
|
|
|
147
153
|
productAssociations?: MedusaCapabilityConfig<ProductAssociationsFactoryWithOutput<TProductAssociationsFactory>, TProductAssociationsCapability>;
|
|
148
154
|
productRecommendations?: MedusaDirectCapabilityConfig<TProductRecommendationsCapability>;
|
|
149
155
|
personalizationProfile?: MedusaCapabilityConfig<PersonalizationProfileFactoryWithOutput<TPersonalizationProfileFactory>, TPersonalizationProfileCapability>;
|
|
156
|
+
store?: MedusaCapabilityConfig<StoreFactoryWithOutput<TStoreFactory>, TStoreCapability>;
|
|
150
157
|
};
|
|
151
158
|
export type ParsedMedusaCapabilities = z.infer<typeof MedusaCapabilitiesSchema>;
|
|
@@ -41,6 +41,10 @@ export declare const MedusaSessionSchema: z.ZodObject<{
|
|
|
41
41
|
name: z.ZodString;
|
|
42
42
|
currency_code: z.ZodString;
|
|
43
43
|
}, z.core.$loose>>;
|
|
44
|
+
finalizedOrders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
45
|
+
key: z.ZodString;
|
|
46
|
+
display_id: z.ZodOptional<z.ZodNumber>;
|
|
47
|
+
}, z.core.$loose>>>;
|
|
44
48
|
}, z.core.$loose>;
|
|
45
49
|
export type MedusaCartIdentifier = InferType<typeof MedusaCartIdentifierSchema>;
|
|
46
50
|
export type MedusaOrderIdentifier = z.infer<typeof MedusaOrderIdentifierSchema>;
|
package/utils/geo.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const EARTH_RADIUS_KM = 6371;
|
|
2
|
+
function haversineDistanceKm(latitude1, longitude1, latitude2, longitude2) {
|
|
3
|
+
const toRadians = (degrees) => degrees * Math.PI / 180;
|
|
4
|
+
const deltaLatitude = toRadians(latitude2 - latitude1);
|
|
5
|
+
const deltaLongitude = toRadians(longitude2 - longitude1);
|
|
6
|
+
const a = Math.sin(deltaLatitude / 2) ** 2 + Math.cos(toRadians(latitude1)) * Math.cos(toRadians(latitude2)) * Math.sin(deltaLongitude / 2) ** 2;
|
|
7
|
+
return 2 * EARTH_RADIUS_KM * Math.asin(Math.sqrt(a));
|
|
8
|
+
}
|
|
9
|
+
export {
|
|
10
|
+
haversineDistanceKm
|
|
11
|
+
};
|