@scayle/storefront-core 8.45.1 → 8.47.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/CHANGELOG.md CHANGED
@@ -1,5 +1,101 @@
1
1
  # @scayle/storefront-core
2
2
 
3
+ ## 8.47.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Enhanced basket RPC methods to support order custom data functionality.
8
+
9
+ All basket RPC methods now integrate with the `getOrderCustomData` RPC method to fetch and include order custom data in SAPI requests.
10
+ This enables developers to attach order-specific custom metadata, business rules, or additional context to orders throughout the basket lifecycle.
11
+ The existing `orderCustomData` parameter is deprecated and will be removed in the next major version.
12
+ For now, both parameters are supported and merged together, where attributes from the parameter take precedence.
13
+
14
+ On default, this RPC returns an empty object but can be overridden by the developer to return a custom object.
15
+
16
+ Example `getOrderCustomData` RPC:
17
+
18
+ ```typescript
19
+ export const getOrderCustomData: RpcHandler<Record<string, unknown>> =
20
+ defineRpcHandler<Record<string, unknown>>(async (context: RpcContext) => {
21
+ return {
22
+ customer: {
23
+ groups: context.user?.groups,
24
+ },
25
+ }
26
+ })
27
+ ```
28
+
29
+ For further information, please refer to the [Overriding Core RPC Methods](https://scayle.dev/en/core-documentation/storefront-guide/storefront-application/technical-foundation/rpc-methods?sourceText=RPC%2520Methods#overriding-core-rpc-methods).
30
+
31
+ - Updated all basket RPC methods to pass the customer access token to the SAPI client, enabling the `X-Customer-Token` header for promotion validation.
32
+
33
+ The customer access token is now automatically included in basket requests when available.
34
+ If the token has expired, it will be omitted from the request to prevent validation errors.
35
+
36
+ ### Patch Changes
37
+
38
+ **Dependencies**
39
+
40
+ - Updated dependency to @scayle/unstorage-scayle-kv-driver@2.0.7
41
+ - Updated dependency to @scayle/storefront-api@18.18.0
42
+
43
+ ## 8.46.0
44
+
45
+ ### Minor Changes
46
+
47
+ - Added `getAttributeValuesByGroupId` function to the `attributeHelpers` helper for retrieving attribute values by their numeric attribute group ID.
48
+
49
+ This function allows you to retrieve attribute values filtered by a specific attribute group ID, providing a more targeted approach when working with grouped product attributes.
50
+ The existing `getAttributeValues` function has been renamed to `getAttributeValuesByName` to better differentiate between the two lookup methods.
51
+
52
+ ### Usage Examples
53
+
54
+ Both functions return an array of values for consistency, making them safe to use in iterations. For single-select attributes, the value is wrapped in an array. For multi-select attributes, the array is returned directly.
55
+
56
+ #### Looking up by name (existing, now renamed):
57
+
58
+ ```typescript
59
+ import { getAttributeValuesByName } from '@scayle/storefront-core'
60
+
61
+ // Single-select attribute
62
+ const sizes = getAttributeValuesByName(product.attributes, 'size')
63
+ // Returns: [{ id: 1, label: 'M', value: 'medium' }]
64
+
65
+ // Multi-select attribute
66
+ const colors = getAttributeValuesByName(product.attributes, 'color')
67
+ // Returns: [{ id: 1, label: 'Red' }, { id: 2, label: 'Blue' }]
68
+
69
+ // Non-existent attribute
70
+ const missing = getAttributeValuesByName(product.attributes, 'doesNotExist')
71
+ // Returns: []
72
+ ```
73
+
74
+ #### Looking up by group ID (new):
75
+
76
+ ```typescript
77
+ import { getAttributeValuesByGroupId } from '@scayle/storefront-core'
78
+
79
+ // Look up attribute by its numeric ID from the SCAYLE API
80
+ const promotionValues = getAttributeValuesByGroupId(product.attributes, 42)
81
+ // Returns: [{ id: 123, label: 'Summer Sale', value: 'summer-2024' }]
82
+
83
+ // Non-existent attribute group
84
+ const missing = getAttributeValuesByGroupId(product.attributes, 9999)
85
+ // Returns: []
86
+ ```
87
+
88
+ ### When to Use Each Function
89
+
90
+ - **`getAttributeValuesByName`**: Use when you know the attribute name key (e.g., 'color', 'size'). This is the most common use case in application code.
91
+ - **`getAttributeValuesByGroupId`**: Use when you have the numeric attribute group ID from the SCAYLE API, such as when processing API configurations, handling dynamic attribute mappings, or working with attribute group references.
92
+
93
+ ### Patch Changes
94
+
95
+ **Dependencies**
96
+
97
+ - Updated dependency to @scayle/storefront-api@18.17.0
98
+
3
99
  ## 8.45.1
4
100
 
5
101
  ### Patch Changes
@@ -1,5 +1,64 @@
1
1
  import type { Attributes, Value } from '@scayle/storefront-api';
2
- import { getAttributeValues, getFirstAttributeValue } from '@scayle/storefront-api';
2
+ /**
3
+ * Returns the first value of an attribute.
4
+ * This function is useful when you need to get the first value of an attribute.
5
+ *
6
+ * This returns undefined, if the attribute doesn't exist or if it doesn't have any values.
7
+ *
8
+ * @param attributes - The product attributes object
9
+ * @param attributeName - The name of the attribute to look up
10
+ *
11
+ * @returns The first value of the attribute, or undefined if the attribute doesn't exist or has no values
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const firstColor = getFirstAttributeValue(product.attributes, 'color')
16
+ * // Returns: { id: 1, label: 'Red', value: 'red' }
17
+ * ```
18
+ */
19
+ export declare const getFirstAttributeValue: (attributes: Attributes | undefined, attributeName: string) => Value | undefined;
20
+ /**
21
+ * Returns all values of an attribute group by its name key.
22
+ * This function always returns an array, making it safe to use in iterations.
23
+ * For single-select attributes, the value is wrapped in an array.
24
+ * For multi-select attributes, the array is returned directly.
25
+ *
26
+ * @param attributes - The product attributes object
27
+ * @param name - The name key of the attribute group to look up (e.g., 'color', 'size')
28
+ *
29
+ * @returns An array of values, or an empty array if the attribute doesn't exist or has no values
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * // Single-select attribute
34
+ * const sizes = getAttributeValuesByName(product.attributes, 'size')
35
+ * // Returns: [{ id: 1, label: 'M', value: 'medium' }]
36
+ *
37
+ * // Multi-select attribute
38
+ * const colors = getAttributeValuesByName(product.attributes, 'color')
39
+ * // Returns: [{ id: 1, label: 'Red' }, { id: 2, label: 'Blue' }]
40
+ * ```
41
+ */
42
+ export declare const getAttributeValuesByName: (attributes: Attributes | undefined, name: string) => Value[];
43
+ /**
44
+ * Returns all values of an attribute group by its numeric ID.
45
+ *
46
+ * This function searches through all attribute groups to find one matching the given ID.
47
+ * This is useful when you need to look up attributes by their SCAYLE API identifier rather than by name.
48
+ * Like {@link getAttributeValuesByName}, this always returns an array for consistency.
49
+ *
50
+ * @param attributes - The product attributes object
51
+ * @param groupId - The numeric ID of the attribute group to look up
52
+ * @returns An array of values, or an empty array if the attribute doesn't exist or has no values
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * // Look up by attribute group ID instead of name
57
+ * const promotionValues = getAttributeValuesByGroupId(product.attributes, 42)
58
+ * // Returns: [{ id: 123, label: 'Summer Sale', value: 'summer-2024' }]
59
+ * ```
60
+ */
61
+ export declare const getAttributeValuesByGroupId: (attributes: Attributes | undefined, groupId: number) => Value[];
3
62
  /**
4
63
  * Retrieves the value or label of the first attribute value for a given attribute name.
5
64
  *
@@ -18,4 +77,7 @@ export declare const getAttributeValue: (attributes: Attributes | undefined, att
18
77
  * @returns An array of attribute values, filtered to remove any null or undefined values.
19
78
  */
20
79
  export declare const getManyAttributeValueTuples: (attributes: Attributes | undefined, attributeNames: string[]) => Value[];
21
- export { getAttributeValues as getAttributeValueTuples, getFirstAttributeValue };
80
+ /**
81
+ * @deprecated This function will be removed in the next major version. Use {@link getAttributeValuesByName} instead.
82
+ */
83
+ export { getAttributeValuesByName as getAttributeValueTuples };
@@ -1,7 +1,36 @@
1
- import {
2
- getAttributeValues,
3
- getFirstAttributeValue
4
- } from "@scayle/storefront-api";
1
+ export const getFirstAttributeValue = (attributes, attributeName) => {
2
+ const attribute = attributes && attributes[attributeName];
3
+ if (!attribute || !attribute.values) {
4
+ return;
5
+ }
6
+ if (attribute.multiSelect) {
7
+ return attribute.values.length > 0 ? attribute.values[0] : void 0;
8
+ }
9
+ return attribute.values;
10
+ };
11
+ export const getAttributeValuesByName = (attributes, name) => {
12
+ const attribute = attributes && attributes[name];
13
+ if (!attribute || !attribute.values) {
14
+ return [];
15
+ }
16
+ if (attribute.multiSelect) {
17
+ return attribute.values;
18
+ }
19
+ return [attribute.values];
20
+ };
21
+ export const getAttributeValuesByGroupId = (attributes, groupId) => {
22
+ if (!attributes) {
23
+ return [];
24
+ }
25
+ const attribute = Object.values(attributes).find((attribute2) => attribute2?.id === groupId);
26
+ if (!attribute || !attribute.values) {
27
+ return [];
28
+ }
29
+ if (attribute.multiSelect) {
30
+ return attribute.values;
31
+ }
32
+ return [attribute.values];
33
+ };
5
34
  export const getAttributeValue = (attributes, attributeName) => {
6
35
  const values = getFirstAttributeValue(attributes, attributeName);
7
36
  return values?.value ?? values?.label;
@@ -9,4 +38,4 @@ export const getAttributeValue = (attributes, attributeName) => {
9
38
  export const getManyAttributeValueTuples = (attributes, attributeNames) => {
10
39
  return attributeNames.map((key) => getFirstAttributeValue(attributes, key)).filter((entry) => !!entry);
11
40
  };
12
- export { getAttributeValues as getAttributeValueTuples, getFirstAttributeValue };
41
+ export { getAttributeValuesByName as getAttributeValueTuples };
@@ -15,7 +15,7 @@ import { mergeBaskets as mergeBasketFunction } from '../../../utils/user';
15
15
  * @param params.existingItemHandling How to handle existing items, default to `'AddQuantityToExisting'`.
16
16
  * @param params.itemGroup The item group.
17
17
  * @param params.with withParams for basket.
18
- * @param params.orderCustomData Custom data for order.
18
+ * @param params.orderCustomData @deprecated - will be removed in the next major version. Use `getOrderCustomData` RPC method instead.
19
19
  * @param context The RPC context.
20
20
  *
21
21
  * @returns The updated basket, potentially with errors. It will return
@@ -36,7 +36,7 @@ export declare const addItemToBasket: RpcHandler<AddOrUpdateItemType & {
36
36
  * @param params.items An array of items to add.
37
37
  * @param params.existingItemHandling How to handle existing items.
38
38
  * @param params.with withParams for basket.
39
- * @param params.orderCustomData Order custom data.
39
+ * @param params.orderCustomData @deprecated - will be removed in the next major version. Use `getOrderCustomData` RPC method instead.
40
40
  * @param context The RPC context.
41
41
  *
42
42
  * @returns The updated basket, potentially with errors. It will return
@@ -55,7 +55,7 @@ export declare const addItemsToBasket: RpcHandler<{
55
55
  * Retrieves the current basket.
56
56
  *
57
57
  * @param options The options for retrieving the basket.
58
- * @param options.orderCustomData Custom data for the order.
58
+ * @param options.orderCustomData @deprecated - will be removed in the next major version. Use `getOrderCustomData` RPC method instead.
59
59
  * @param context The RPC context.
60
60
  *
61
61
  * @returns The basket data. It will return an `ErrorResponse` alternatively
@@ -72,7 +72,7 @@ export declare const getBasket: RpcHandler<BasketWithOptions & {
72
72
  * @param options The options for removing the item.
73
73
  * @param options.itemKey The key of the item to remove.
74
74
  * @param options.with withParams for basket.
75
- * @param options.orderCustomData Custom data for the order.
75
+ * @param options.orderCustomData @deprecated - will be removed in the next major version. Use `getOrderCustomData` RPC method instead.
76
76
  * @param context The RPC context.
77
77
  *
78
78
  * @returns The updated basket. It will return an `ErrorResponse` alternatively
@@ -101,7 +101,7 @@ export declare const clearBasket: RpcHandler<boolean>;
101
101
  * @param params.fromBasketKey The key of the source basket.
102
102
  * @param params.toBasketKey The key of the target basket.
103
103
  * @param params.with withParams for basket.
104
- * @param params.orderCustomData Custom data for the order.
104
+ * @param params.orderCustomData @deprecated - will be removed in the next major version. Use `getOrderCustomData` RPC method instead.
105
105
  * @param context The RPC context.
106
106
  *
107
107
  * @returns The new merged basket as result of the merge operation.
@@ -135,6 +135,7 @@ export interface UpdateBasketItemRequestParameter {
135
135
  * An object describing which data the returned basket should contain.
136
136
  */
137
137
  with?: BasketWithOptions;
138
+ orderCustomData?: Record<string, unknown>;
138
139
  }
139
140
  /**
140
141
  * Applies the provided update data to a specific item in a basket.
@@ -143,6 +144,7 @@ export interface UpdateBasketItemRequestParameter {
143
144
  * @param params.basketItemKey The key of the item to be updated.
144
145
  * @param params.update The update data that should be applied to the basket item.
145
146
  * @param params.with An object describing which data the returned basket should contain.
147
+ * @param params.orderCustomData Custom data for the order.
146
148
  * @param context The RPC context.
147
149
  *
148
150
  * @returns A promise that resolves with the updated basket.
@@ -6,9 +6,16 @@ import {
6
6
  HttpStatusMessage,
7
7
  MIN_WITH_PARAMS_BASKET
8
8
  } from "../../../constants/index.mjs";
9
- import { mergeBaskets as mergeBasketFunction } from "../../../utils/user.mjs";
9
+ import {
10
+ mergeBaskets as mergeBasketFunction,
11
+ getValidatedAccessToken
12
+ } from "../../../utils/user.mjs";
10
13
  import { unwrap } from "../../../utils/response.mjs";
11
- import { defineRpcHandler, wasAddedWithReducedQuantity } from "../../../utils/index.mjs";
14
+ import {
15
+ defineRpcHandler,
16
+ mergeOrderCustomData,
17
+ wasAddedWithReducedQuantity
18
+ } from "../../../utils/index.mjs";
12
19
  import { mapSAPIFetchErrorToResponse } from "../../../utils/sapi.mjs";
13
20
  const SAPI_ERROR_NAME = "SAPI ERROR";
14
21
  function getWithParams(params, context) {
@@ -35,6 +42,7 @@ export const addItemToBasket = defineRpcHandler(async ({
35
42
  }
36
43
  const { sapiClient, basketKey } = context;
37
44
  const campaignKey = await context.callRpc?.("getCampaignKey");
45
+ const _orderCustomData = await context.callRpc?.("getOrderCustomData");
38
46
  const resolvedWith = getWithParams(
39
47
  { with: _with },
40
48
  context
@@ -60,7 +68,8 @@ export const addItemToBasket = defineRpcHandler(async ({
60
68
  ],
61
69
  {
62
70
  skipAvailabilityCheck: false,
63
- orderCustomData
71
+ orderCustomData: mergeOrderCustomData(_orderCustomData, orderCustomData),
72
+ customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
64
73
  },
65
74
  {
66
75
  existingItemHandling
@@ -97,6 +106,7 @@ export const addItemsToBasket = defineRpcHandler(async (params, context) => {
97
106
  }
98
107
  const { sapiClient, basketKey } = context;
99
108
  const campaignKey = await context.callRpc?.("getCampaignKey");
109
+ const _orderCustomData = await context.callRpc?.("getOrderCustomData");
100
110
  const resolvedWith = getWithParams(params, context);
101
111
  const itemsToBeAddedOrUpdated = params.items.map((item) => ({
102
112
  variantId: item.variantId,
@@ -119,7 +129,11 @@ export const addItemsToBasket = defineRpcHandler(async (params, context) => {
119
129
  {
120
130
  skipAvailabilityCheck: false,
121
131
  includeItemsWithoutProductData: resolvedWith?.includeItemsWithoutProductData,
122
- orderCustomData: params.orderCustomData
132
+ orderCustomData: mergeOrderCustomData(
133
+ _orderCustomData,
134
+ params.orderCustomData
135
+ ),
136
+ customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
123
137
  },
124
138
  {
125
139
  existingItemHandling: params.existingItemHandling || ExistingItemHandling.ADD_QUANTITY_TO_EXISTING
@@ -159,6 +173,7 @@ export const getBasket = defineRpcHandler(async (options, context) => {
159
173
  }
160
174
  const { sapiClient, basketKey } = context;
161
175
  const campaignKey = await context.callRpc?.("getCampaignKey");
176
+ const _orderCustomData = await context.callRpc?.("getOrderCustomData");
162
177
  const resolvedWith = getWithParams(
163
178
  { with: options },
164
179
  context
@@ -167,7 +182,11 @@ export const getBasket = defineRpcHandler(async (options, context) => {
167
182
  with: resolvedWith,
168
183
  campaignKey,
169
184
  includeItemsWithoutProductData: resolvedWith.includeItemsWithoutProductData,
170
- orderCustomData: options?.orderCustomData
185
+ orderCustomData: mergeOrderCustomData(
186
+ _orderCustomData,
187
+ options?.orderCustomData
188
+ ),
189
+ customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
171
190
  });
172
191
  if (response.type === "failure") {
173
192
  const { statusCode, message } = parseBasketError(response);
@@ -192,6 +211,7 @@ export const removeItemFromBasket = defineRpcHandler(async (options, context) =>
192
211
  }
193
212
  const { sapiClient, basketKey } = context;
194
213
  const campaignKey = await context.callRpc?.("getCampaignKey");
214
+ const _orderCustomData = await context.callRpc?.("getOrderCustomData");
195
215
  const resolvedWith = getWithParams(options, context);
196
216
  const basket = await sapiClient.basket.deleteItem(
197
217
  basketKey,
@@ -200,7 +220,11 @@ export const removeItemFromBasket = defineRpcHandler(async (options, context) =>
200
220
  with: resolvedWith,
201
221
  campaignKey,
202
222
  includeItemsWithoutProductData: resolvedWith?.includeItemsWithoutProductData,
203
- orderCustomData: options.orderCustomData
223
+ orderCustomData: mergeOrderCustomData(
224
+ _orderCustomData,
225
+ options.orderCustomData
226
+ ),
227
+ customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
204
228
  }
205
229
  );
206
230
  return { basket };
@@ -226,14 +250,18 @@ export const mergeBaskets = defineRpcHandler(async ({ fromBasketKey, toBasketKey
226
250
  { with: _with },
227
251
  context
228
252
  );
253
+ const _orderCustomData = await context.callRpc?.("getOrderCustomData");
229
254
  return await mergeBasketFunction(
230
255
  fromBasketKey,
231
256
  toBasketKey,
232
- { ...resolvedWith, orderCustomData },
257
+ {
258
+ ...resolvedWith,
259
+ orderCustomData: mergeOrderCustomData(_orderCustomData, orderCustomData)
260
+ },
233
261
  context
234
262
  );
235
263
  });
236
- export const updateBasketItem = defineRpcHandler(async ({ basketItemKey, update, with: _with }, context) => {
264
+ export const updateBasketItem = defineRpcHandler(async ({ basketItemKey, update, with: _with, orderCustomData }, context) => {
237
265
  if (!hasSession(context)) {
238
266
  return new ErrorResponse(
239
267
  HttpStatusCode.BAD_REQUEST,
@@ -248,11 +276,18 @@ export const updateBasketItem = defineRpcHandler(async ({ basketItemKey, update,
248
276
  );
249
277
  const { quantity, ...updateParams } = update;
250
278
  const campaignKey = update.campaignKey ?? await context.callRpc?.("getCampaignKey");
279
+ const _orderCustomData = await context.callRpc?.("getOrderCustomData");
251
280
  const result = await sapiClient.basket.updateItem(
252
281
  basketKey,
253
282
  basketItemKey,
254
283
  quantity,
255
- { ...updateParams, campaignKey, with: resolvedWith }
284
+ {
285
+ ...updateParams,
286
+ campaignKey,
287
+ with: resolvedWith,
288
+ orderCustomData: mergeOrderCustomData(_orderCustomData, orderCustomData),
289
+ customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
290
+ }
256
291
  );
257
292
  if (result.type === "success") {
258
293
  return { basket: result.basket };
@@ -287,12 +322,18 @@ export const getApplicablePromotionsByCode = defineRpcHandler(async ({ promotion
287
322
  context
288
323
  );
289
324
  const campaignKey = await context.callRpc?.("getCampaignKey");
325
+ const orderCustomData = await context.callRpc?.("getOrderCustomData");
290
326
  const result = await mapSAPIFetchErrorToResponse(
291
327
  sapiClient.basket.getApplicablePromotionsByCode
292
328
  )(
293
329
  basketKey,
294
330
  promotionCode,
295
- { campaignKey, with: resolvedWith }
331
+ {
332
+ campaignKey,
333
+ with: resolvedWith,
334
+ orderCustomData,
335
+ customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
336
+ }
296
337
  );
297
338
  if (result instanceof Response || result.type === "failure") {
298
339
  context.log.info("Getting applicable promotion codes failed", {
@@ -318,6 +359,7 @@ export const updatePromotions = defineRpcHandler(async (params, context) => {
318
359
  }
319
360
  const { basketKey, sapiClient } = context;
320
361
  const campaignKey = params.campaignKey ?? await context.callRpc?.("getCampaignKey");
362
+ const orderCustomData = await context.callRpc?.("getOrderCustomData");
321
363
  const resolvedWith = getWithParams(
322
364
  { with: params.with },
323
365
  context
@@ -329,7 +371,9 @@ export const updatePromotions = defineRpcHandler(async (params, context) => {
329
371
  {
330
372
  ...params,
331
373
  campaignKey,
332
- with: resolvedWith
374
+ with: resolvedWith,
375
+ orderCustomData,
376
+ customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
333
377
  }
334
378
  );
335
379
  if (result instanceof Response || result.type === "failure") {
@@ -45,6 +45,8 @@ interface CheckoutJwtPayload {
45
45
  * @param jwtPayload The payload for the JWT. Provides checkout-related data to embed within the token.
46
46
  * @param context The RPC context.
47
47
  *
48
+ * @see https://scayle.dev/en/core-documentation/storefront/checkout-guide/implementation/webcomponent#jwt-attribute
49
+ *
48
50
  * @returns An object containing the access token and checkout JWT.
49
51
  * It will return an `ErrorResponse` if no session is found, no access token is present.
50
52
  */
@@ -26,14 +26,18 @@ export const getCheckoutToken = defineRpcHandler(async (jwtPayload = {}, context
26
26
  const now = /* @__PURE__ */ new Date();
27
27
  const { voucher, customData, preferredCollectionPoint, carrier } = jwtPayload;
28
28
  const campaignKey = await context.callRpc?.("getCampaignKey");
29
+ const orderCustomData = await context.callRpc?.("getOrderCustomData");
29
30
  const checkoutJwt = await new SignJWT({
30
31
  voucher,
31
- customData,
32
32
  preferredCollectionPoint,
33
33
  carrier,
34
34
  basketId: context.basketKey,
35
- campaignKey
36
- }).setIssuedAt(now).setNotBefore(now).setExpirationTime("1h").setIssuer(`${"@scayle/storefront-core"}@${"8.45.1"}`).setProtectedHeader({ alg: "HS256", typ: "JWT" }).sign(secret);
35
+ campaignKey,
36
+ customData: {
37
+ ...customData,
38
+ ...orderCustomData
39
+ }
40
+ }).setIssuedAt(now).setNotBefore(now).setExpirationTime("1h").setIssuer(`${"@scayle/storefront-core"}@${"8.47.0"}`).setProtectedHeader({ alg: "HS256", typ: "JWT" }).sign(secret);
37
41
  return {
38
42
  accessToken: refreshedAccessToken,
39
43
  checkoutJwt
@@ -14,3 +14,4 @@ import type { Order, RpcHandler } from '../../../types';
14
14
  export declare const getOrderById: RpcHandler<{
15
15
  orderId: number;
16
16
  }, Order>;
17
+ export declare const getOrderCustomData: RpcHandler<Record<string, unknown>>;
@@ -51,3 +51,6 @@ export const getOrderById = defineRpcHandler(async ({ orderId }, context) => {
51
51
  );
52
52
  }
53
53
  });
54
+ export const getOrderCustomData = defineRpcHandler(async () => {
55
+ return {};
56
+ });
@@ -23,3 +23,44 @@ import type { AddOrUpdateItemError } from '@scayle/storefront-api';
23
23
  * ```
24
24
  */
25
25
  export declare const wasAddedWithReducedQuantity: (errors?: AddOrUpdateItemError[]) => boolean;
26
+ /**
27
+ * Merges the two orderCustomData objects when both RPC and parameter values are provided.
28
+ * If an attribute exists in both, the parameter value takes precedence.
29
+ *
30
+ * @param orderCustomDataRpc The order custom data from the RPC context.
31
+ * @param orderCustomDataParam The order custom data from the parameter.
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * const orderCustomDataRpc = {
36
+ * orderId: '1',
37
+ * }
38
+ * const orderCustomDataParam = {
39
+ * orderId: '2',
40
+ * }
41
+ * const mergedOrderCustomData = mergeOrderCustomData(orderCustomDataRpc, orderCustomDataParam) // { orderId: '2' }
42
+ *
43
+ * const orderCustomDataRpc = {
44
+ * orderId: '1',
45
+ * }
46
+ * const orderCustomDataParam = undefined
47
+ * const mergedOrderCustomData = mergeOrderCustomData(orderCustomDataRpc, orderCustomDataParam) // { orderId: '1' }
48
+ *
49
+ * const orderCustomDataRpc = undefined
50
+ * const orderCustomDataParam = {
51
+ * orderId: '2',
52
+ * }
53
+ * const mergedOrderCustomData = mergeOrderCustomData(orderCustomDataRpc, orderCustomDataParam) // { orderId: '2' }
54
+ *
55
+ * const orderCustomDataRpc = {
56
+ * orderId: '1',
57
+ * }
58
+ * const orderCustomDataParam = {
59
+ * orderName: 'Order 2',
60
+ * }
61
+ * const mergedOrderCustomData = mergeOrderCustomData(orderCustomDataRpc, orderCustomDataParam) // { orderId: '1', orderName: 'Order 2' }
62
+ * ```
63
+ *
64
+ * @returns The merged order custom data.
65
+ */
66
+ export declare const mergeOrderCustomData: (orderCustomDataRpc?: Record<string, unknown>, orderCustomDataParam?: Record<string, unknown>) => Record<string, unknown>;
@@ -10,3 +10,12 @@ export const wasAddedWithReducedQuantity = (errors) => {
10
10
  (error) => error.operation === "add" && error.kind === AddToBasketFailureKind.ITEM_ADDED_WITH_REDUCED_QUANTITY || error.operation === "update" && error.kind === UpdateBasketItemFailureKind.ITEM_ADDED_WITH_REDUCED_QUANTITY
11
11
  );
12
12
  };
13
+ export const mergeOrderCustomData = (orderCustomDataRpc, orderCustomDataParam) => {
14
+ if (!orderCustomDataParam) {
15
+ return orderCustomDataRpc ?? {};
16
+ }
17
+ if (!orderCustomDataRpc) {
18
+ return orderCustomDataParam ?? {};
19
+ }
20
+ return { ...orderCustomDataRpc, ...orderCustomDataParam };
21
+ };
@@ -1,5 +1,13 @@
1
1
  import type { BasketResponse, AddManyItemsBasketResponse } from '@scayle/storefront-api';
2
2
  import type { BasketWithOptions, Product, RpcContext, Variant } from '../types';
3
+ /**
4
+ * Checks if the access token is expired.
5
+ *
6
+ * @param accessToken The access token to validate.
7
+ *
8
+ * @returns The validated access token or undefined if the access token is not present or expired.
9
+ */
10
+ export declare const getValidatedAccessToken: (accessToken?: string) => string | undefined;
3
11
  /**
4
12
  * Merges two baskets. Adds the items from the `fromBasketKey` basket to the `toBasketKey` basket
5
13
  * and then clears the `fromBasketKey` basket. Handles existing items by adding quantities.
@@ -1,11 +1,23 @@
1
1
  import { ExistingItemHandling } from "@scayle/storefront-api";
2
+ import { decodeJwt } from "jose";
3
+ export const getValidatedAccessToken = (accessToken) => {
4
+ if (!accessToken) {
5
+ return;
6
+ }
7
+ const payload = decodeJwt(accessToken);
8
+ if (!payload.exp || payload.exp < Date.now() / 1e3) {
9
+ return;
10
+ }
11
+ return accessToken;
12
+ };
2
13
  export const mergeBaskets = async (fromBasketKey, toBasketKey, withOptions, context) => {
3
14
  const { sapiClient } = context;
4
15
  const campaignKey = await context.callRpc?.("getCampaignKey");
5
16
  const fromOriginBasket = await sapiClient.basket.get(fromBasketKey, {
6
17
  with: withOptions,
7
18
  campaignKey,
8
- orderCustomData: withOptions.orderCustomData
19
+ orderCustomData: withOptions.orderCustomData,
20
+ customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
9
21
  });
10
22
  context.log.debug(`Merging basket ${fromBasketKey} to ${toBasketKey}`);
11
23
  if (fromBasketKey === toBasketKey) {
@@ -31,7 +43,8 @@ export const mergeBaskets = async (fromBasketKey, toBasketKey, withOptions, cont
31
43
  with: withOptions,
32
44
  campaignKey,
33
45
  includeItemsWithoutProductData: withOptions?.includeItemsWithoutProductData,
34
- orderCustomData: withOptions.orderCustomData
46
+ orderCustomData: withOptions.orderCustomData,
47
+ customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
35
48
  },
36
49
  {
37
50
  existingItemHandling: ExistingItemHandling.ADD_QUANTITY_TO_EXISTING
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scayle/storefront-core",
3
- "version": "8.45.1",
3
+ "version": "8.47.0",
4
4
  "description": "Collection of essential utilities to work with the Storefront API",
5
5
  "author": "SCAYLE Commerce Engine",
6
6
  "license": "MIT",
@@ -48,29 +48,29 @@
48
48
  "ufo": "^1.5.3",
49
49
  "uncrypto": "^0.1.3",
50
50
  "utility-types": "^3.11.0",
51
- "@scayle/storefront-api": "18.16.1",
52
- "@scayle/unstorage-scayle-kv-driver": "2.0.6"
51
+ "@scayle/storefront-api": "18.18.0",
52
+ "@scayle/unstorage-scayle-kv-driver": "2.0.7"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/crypto-js": "4.2.2",
56
- "@types/node": "22.18.11",
56
+ "@types/node": "22.18.13",
57
57
  "@types/webpack-env": "1.18.8",
58
58
  "@vitest/coverage-v8": "3.2.4",
59
59
  "dprint": "0.50.2",
60
60
  "eslint-formatter-gitlab": "6.0.1",
61
- "eslint": "9.37.0",
61
+ "eslint": "9.38.0",
62
62
  "fishery": "2.3.1",
63
- "publint": "0.3.14",
63
+ "publint": "0.3.15",
64
64
  "rimraf": "6.0.1",
65
65
  "typescript": "5.9.3",
66
66
  "unbuild": "3.6.1",
67
67
  "unstorage": "1.17.1",
68
68
  "vitest": "3.2.4",
69
- "@scayle/eslint-config-storefront": "4.7.10",
69
+ "@scayle/eslint-config-storefront": "4.7.11",
70
70
  "@scayle/vitest-config-storefront": "1.0.0"
71
71
  },
72
72
  "volta": {
73
- "node": "22.20.0"
73
+ "node": "22.21.0"
74
74
  },
75
75
  "scripts": {
76
76
  "clean": "rimraf ./dist",