@reactionary/commercetools 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.
@@ -9,6 +9,12 @@ var __decorateClass = (decorators, target, key, kind) => {
9
9
  return result;
10
10
  };
11
11
  import { PROTOCOL_DATA_FIELD, serializeProtocolData } from "../core/payment-protocol-data.js";
12
+ import {
13
+ PICKUP_STORE_FIELD,
14
+ SHIPPING_PROTOCOL_DATA_FIELD,
15
+ serializeShippingProtocolData
16
+ } from "../core/shipping-protocol-data.js";
17
+ import { resolveShippingFulfillmentType } from "../core/shipping-fulfillment.js";
12
18
  import {
13
19
  CheckoutMutationAddPaymentInstructionSchema,
14
20
  CheckoutMutationFinalizeCheckoutSchema,
@@ -174,10 +180,25 @@ class CommercetoolsCheckoutCapability extends CheckoutCapability {
174
180
  client.carts.withId({ ID: payload.checkout.key }).get().execute()
175
181
  ]);
176
182
  const result = shippingMethodsResponse.body.results.map(
177
- (shippingMethod) => this.factory.parseShippingMethod(this.context, this.withFreeShipping(shippingMethod, cartResponse.body))
183
+ (shippingMethod) => this.withFulfillmentType(
184
+ this.factory.parseShippingMethod(this.context, this.withFreeShipping(shippingMethod, cartResponse.body)),
185
+ shippingMethod
186
+ )
178
187
  );
179
188
  return success(result);
180
189
  }
190
+ /**
191
+ * Tags a parsed shipping method with its fulfillment type: methods whose
192
+ * key is listed in the `bopis.shippingMethodKeys` configuration are
193
+ * in-store pickup (BOPIS), everything else is a delivery. The tagging lives
194
+ * here rather than in the factory, so factories stay configuration-free.
195
+ */
196
+ withFulfillmentType(shippingMethod, remote) {
197
+ return {
198
+ ...shippingMethod,
199
+ fulfillmentType: resolveShippingFulfillmentType(remote, this.config.bopis?.shippingMethodKeys)
200
+ };
201
+ }
181
202
  /**
182
203
  * Quotes the matching shipping rates as free where the cart reaches the
183
204
  * rate's free-above threshold. commercetools' matching-cart query returns
@@ -289,6 +310,20 @@ class CommercetoolsCheckoutCapability extends CheckoutCapability {
289
310
  name: "pickupPointId",
290
311
  value: payload.shippingInstruction.pickupPoint
291
312
  });
313
+ if (payload.shippingInstruction.store?.key) {
314
+ actions.push({
315
+ action: "setCustomField",
316
+ name: PICKUP_STORE_FIELD,
317
+ value: payload.shippingInstruction.store.key
318
+ });
319
+ }
320
+ if (payload.shippingInstruction.protocolData && payload.shippingInstruction.protocolData.length > 0) {
321
+ actions.push({
322
+ action: "setCustomField",
323
+ name: SHIPPING_PROTOCOL_DATA_FIELD,
324
+ value: serializeShippingProtocolData(payload.shippingInstruction.protocolData)
325
+ });
326
+ }
292
327
  const result = await this.applyActions(
293
328
  payload.checkout,
294
329
  actions
@@ -8,7 +8,17 @@ var __decorateClass = (decorators, target, key, kind) => {
8
8
  if (kind && result) __defProp(target, key, result);
9
9
  return result;
10
10
  };
11
- import { Reactionary, StoreCapability, StoreQueryByProximitySchema, StoreSchema, success, error } from "@reactionary/core";
11
+ import {
12
+ Reactionary,
13
+ StoreCapability,
14
+ StorePaginatedResultSchema,
15
+ StoreQueryByIdSchema,
16
+ StoreQueryByProximitySchema,
17
+ StoreQueryForListSchema,
18
+ StoreSchema,
19
+ success,
20
+ error
21
+ } from "@reactionary/core";
12
22
  import * as z from "zod";
13
23
  class CommercetoolsStoreCapability extends StoreCapability {
14
24
  config;
@@ -38,6 +48,48 @@ class CommercetoolsStoreCapability extends StoreCapability {
38
48
  }
39
49
  return success(results);
40
50
  }
51
+ /**
52
+ * The channel predicate selecting the channels that represent the brand's
53
+ * own stores. Override in a subclass if a project models stores differently.
54
+ */
55
+ getStoreChannelPredicate() {
56
+ return 'roles contains any ("InventorySupply")';
57
+ }
58
+ async list(payload) {
59
+ const client = await this.getClient();
60
+ const { pageNumber, pageSize } = payload.paginationOptions;
61
+ const remote = await client.channels().get({
62
+ queryArgs: {
63
+ limit: pageSize,
64
+ offset: (pageNumber - 1) * pageSize,
65
+ withTotal: true,
66
+ where: this.getStoreChannelPredicate()
67
+ }
68
+ }).execute();
69
+ const totalCount = remote.body.total ?? remote.body.results.length;
70
+ return success({
71
+ pageNumber,
72
+ pageSize,
73
+ totalCount,
74
+ totalPages: Math.ceil(totalCount / pageSize),
75
+ items: remote.body.results.map((r) => this.factory.parseStore(this.context, r))
76
+ });
77
+ }
78
+ async getById(payload) {
79
+ const client = await this.getClient();
80
+ const remote = await client.channels().get({
81
+ queryArgs: {
82
+ limit: 1,
83
+ where: `key = :key AND ${this.getStoreChannelPredicate()}`,
84
+ "var.key": payload.identifier.key
85
+ }
86
+ }).execute();
87
+ const channel = remote.body.results[0];
88
+ if (!channel) {
89
+ return error({ type: "NotFound", identifier: payload.identifier });
90
+ }
91
+ return success(this.factory.parseStore(this.context, channel));
92
+ }
41
93
  }
42
94
  __decorateClass([
43
95
  Reactionary({
@@ -45,6 +97,18 @@ __decorateClass([
45
97
  outputSchema: z.array(StoreSchema)
46
98
  })
47
99
  ], CommercetoolsStoreCapability.prototype, "queryByProximity", 1);
100
+ __decorateClass([
101
+ Reactionary({
102
+ inputSchema: StoreQueryForListSchema,
103
+ outputSchema: StorePaginatedResultSchema
104
+ })
105
+ ], CommercetoolsStoreCapability.prototype, "list", 1);
106
+ __decorateClass([
107
+ Reactionary({
108
+ inputSchema: StoreQueryByIdSchema,
109
+ outputSchema: StoreSchema
110
+ })
111
+ ], CommercetoolsStoreCapability.prototype, "getById", 1);
48
112
  export {
49
113
  CommercetoolsStoreCapability
50
114
  };
@@ -0,0 +1,6 @@
1
+ function resolveShippingFulfillmentType(shippingMethod, bopisShippingMethodKeys) {
2
+ return shippingMethod.key && (bopisShippingMethodKeys || []).includes(shippingMethod.key) ? "pickup" : "delivery";
3
+ }
4
+ export {
5
+ resolveShippingFulfillmentType
6
+ };
@@ -0,0 +1,33 @@
1
+ const SHIPPING_PROTOCOL_DATA_FIELD = "shippingProtocolData";
2
+ const PICKUP_STORE_FIELD = "pickupStoreKey";
3
+ function serializeShippingProtocolData(entries) {
4
+ return JSON.stringify(Object.fromEntries(entries.map((entry) => [entry.key, entry.value])));
5
+ }
6
+ function parseShippingProtocolDataField(value) {
7
+ if (typeof value !== "string" || value.length === 0) {
8
+ return void 0;
9
+ }
10
+ try {
11
+ const parsed = JSON.parse(value);
12
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
13
+ return void 0;
14
+ }
15
+ const entries = [];
16
+ for (const [key, entry] of Object.entries(parsed)) {
17
+ if (typeof entry === "string") {
18
+ entries.push({ key, value: entry });
19
+ } else if (typeof entry === "object" && entry !== null && !Array.isArray(entry)) {
20
+ entries.push({ key, value: entry });
21
+ }
22
+ }
23
+ return entries.length > 0 ? entries : void 0;
24
+ } catch {
25
+ return void 0;
26
+ }
27
+ }
28
+ export {
29
+ PICKUP_STORE_FIELD,
30
+ SHIPPING_PROTOCOL_DATA_FIELD,
31
+ parseShippingProtocolDataField,
32
+ serializeShippingProtocolData
33
+ };
@@ -3,6 +3,11 @@ import {
3
3
  CheckoutItemSchema
4
4
  } from "@reactionary/core";
5
5
  import { getLanguageCodeFromLocale } from "../../core/locale-utils.js";
6
+ import {
7
+ PICKUP_STORE_FIELD,
8
+ SHIPPING_PROTOCOL_DATA_FIELD,
9
+ parseShippingProtocolDataField
10
+ } from "../../core/shipping-protocol-data.js";
6
11
  class CommercetoolsCheckoutFactory {
7
12
  checkoutSchema;
8
13
  shippingMethodSchema;
@@ -221,13 +226,20 @@ class CommercetoolsCheckoutFactory {
221
226
  const instructions = data.custom?.fields["shippingInstruction"] || "";
222
227
  const consentForUnattendedDelivery = data.custom?.fields["consentForUnattendedDelivery"] === "true";
223
228
  const pickupPoint = data.custom?.fields["pickupPointId"] || "";
229
+ const pickupStoreKey = data.custom?.fields[PICKUP_STORE_FIELD];
230
+ const store = typeof pickupStoreKey === "string" && pickupStoreKey.length > 0 ? { key: pickupStoreKey } : void 0;
231
+ const protocolData = parseShippingProtocolDataField(
232
+ data.custom?.fields[SHIPPING_PROTOCOL_DATA_FIELD]
233
+ );
224
234
  const result = {
225
235
  shippingMethod: {
226
236
  key: data.shippingInfo.shippingMethod?.obj?.key || ""
227
237
  },
228
238
  pickupPoint,
229
239
  instructions,
230
- consentForUnattendedDelivery
240
+ consentForUnattendedDelivery,
241
+ store,
242
+ protocolData
231
243
  };
232
244
  return result;
233
245
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reactionary/commercetools",
3
- "version": "0.10.52",
3
+ "version": "0.10.54",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "types": "./src/index.d.ts",
@@ -9,7 +9,7 @@
9
9
  "url": "git+https://github.com/Solteq/reactionary.git"
10
10
  },
11
11
  "dependencies": {
12
- "@reactionary/core": "0.10.52",
12
+ "@reactionary/core": "0.10.54",
13
13
  "zod": "4.1.9",
14
14
  "@commercetools/ts-client": "^4.9.1",
15
15
  "@commercetools/platform-sdk": "^8.25.0",
@@ -12,7 +12,10 @@ const CommercetoolsConfigurationSchema = z.looseObject({
12
12
  paymentMethods: PaymentMethodSchema.array().optional().default(() => []),
13
13
  facetFieldsForSearch: z.array(z.string()).default(() => []),
14
14
  listPriceChannelKey: z.string().optional(),
15
- customerPriceChannelKey: z.string().optional()
15
+ customerPriceChannelKey: z.string().optional(),
16
+ bopis: z.looseObject({
17
+ shippingMethodKeys: z.array(z.string()).default(() => []).meta({ description: 'The keys of the commercetools shipping methods that represent in-store pickup (BOPIS). Shipping methods with these keys are tagged with fulfillmentType "pickup"; all others are tagged "delivery".' })
18
+ }).optional().meta({ description: "Optional buy-online-pickup-in-store (BOPIS) configuration." })
16
19
  });
17
20
  export {
18
21
  CommercetoolsConfigurationSchema
@@ -23,6 +23,13 @@ export declare class CommercetoolsCheckoutCapability<TFactory extends CheckoutFa
23
23
  getById(payload: CheckoutQueryById): Promise<Result<CheckoutFactoryCheckoutOutput<TFactory>, NotFoundError>>;
24
24
  setShippingAddress(payload: CheckoutMutationSetShippingAddress): Promise<Result<CheckoutFactoryCheckoutOutput<TFactory>>>;
25
25
  getAvailableShippingMethods(payload: CheckoutQueryForAvailableShippingMethods): Promise<Result<CheckoutFactoryShippingMethodOutput<TFactory>[]>>;
26
+ /**
27
+ * Tags a parsed shipping method with its fulfillment type: methods whose
28
+ * key is listed in the `bopis.shippingMethodKeys` configuration are
29
+ * in-store pickup (BOPIS), everything else is a delivery. The tagging lives
30
+ * here rather than in the factory, so factories stay configuration-free.
31
+ */
32
+ protected withFulfillmentType(shippingMethod: CheckoutFactoryShippingMethodOutput<TFactory>, remote: CTShippingMethod): CheckoutFactoryShippingMethodOutput<TFactory>;
26
33
  /**
27
34
  * Quotes the matching shipping rates as free where the cart reaches the
28
35
  * rate's free-above threshold. commercetools' matching-cart query returns
@@ -1,4 +1,4 @@
1
- import type { RequestContext, Cache, StoreFactory, StoreFactoryOutput, StoreFactoryWithOutput, StoreQueryByProximity, Result } from '@reactionary/core';
1
+ import type { RequestContext, Cache, NotFoundError, StoreFactory, StoreFactoryOutput, StoreFactoryWithOutput, StorePaginatedResult, StoreQueryById, StoreQueryByProximity, StoreQueryForList, Result } from '@reactionary/core';
2
2
  import { StoreCapability } from '@reactionary/core';
3
3
  import type { CommercetoolsConfiguration } from '../schema/configuration.schema.js';
4
4
  import type { CommercetoolsAPI } from '../core/client.js';
@@ -10,4 +10,11 @@ export declare class CommercetoolsStoreCapability<TFactory extends StoreFactory
10
10
  constructor(config: CommercetoolsConfiguration, cache: Cache, context: RequestContext, commercetools: CommercetoolsAPI, factory: StoreFactoryWithOutput<TFactory>);
11
11
  protected getClient(): Promise<import("@commercetools/platform-sdk").ByProjectKeyRequestBuilder>;
12
12
  queryByProximity(payload: StoreQueryByProximity): Promise<Result<Array<StoreFactoryOutput<TFactory>>>>;
13
+ /**
14
+ * The channel predicate selecting the channels that represent the brand's
15
+ * own stores. Override in a subclass if a project models stores differently.
16
+ */
17
+ protected getStoreChannelPredicate(): string;
18
+ list(payload: StoreQueryForList): Promise<Result<StorePaginatedResult>>;
19
+ getById(payload: StoreQueryById): Promise<Result<StoreFactoryOutput<TFactory>, NotFoundError>>;
13
20
  }
@@ -0,0 +1,8 @@
1
+ import type { ShippingMethod as CTShippingMethod } from '@commercetools/platform-sdk';
2
+ import type { ShippingFulfillmentType } from '@reactionary/core';
3
+ /**
4
+ * Resolves the core fulfillment type of a commercetools shipping method from
5
+ * the `bopis.shippingMethodKeys` configuration: shipping methods whose key is
6
+ * listed are in-store pickup (BOPIS), everything else is a delivery.
7
+ */
8
+ export declare function resolveShippingFulfillmentType(shippingMethod: CTShippingMethod, bopisShippingMethodKeys: string[] | undefined): ShippingFulfillmentType;
@@ -0,0 +1,16 @@
1
+ import type { ShippingProtocolData } from '@reactionary/core';
2
+ /**
3
+ * The cart custom field carrying the shipping instruction's protocol data,
4
+ * following the same one-declared-JSON-field mechanism as the payment
5
+ * protocol data (see payment-protocol-data.ts): commercetools rejects
6
+ * undeclared custom fields, so one declared field carries arbitrary keys.
7
+ */
8
+ export declare const SHIPPING_PROTOCOL_DATA_FIELD = "shippingProtocolData";
9
+ /**
10
+ * The cart custom field carrying the key of the brand's own store selected
11
+ * for in-store pickup (BOPIS). Distinct from `pickupPointId`, which is
12
+ * reserved for third-party carrier service points.
13
+ */
14
+ export declare const PICKUP_STORE_FIELD = "pickupStoreKey";
15
+ export declare function serializeShippingProtocolData(entries: ShippingProtocolData[]): string;
16
+ export declare function parseShippingProtocolDataField(value: unknown): ShippingProtocolData[] | undefined;
@@ -26,5 +26,8 @@ export declare const CommercetoolsConfigurationSchema: z.ZodObject<{
26
26
  facetFieldsForSearch: z.ZodDefault<z.ZodArray<z.ZodString>>;
27
27
  listPriceChannelKey: z.ZodOptional<z.ZodString>;
28
28
  customerPriceChannelKey: z.ZodOptional<z.ZodString>;
29
+ bopis: z.ZodOptional<z.ZodObject<{
30
+ shippingMethodKeys: z.ZodDefault<z.ZodArray<z.ZodString>>;
31
+ }, z.core.$loose>>;
29
32
  }, z.core.$loose>;
30
33
  export type CommercetoolsConfiguration = z.infer<typeof CommercetoolsConfigurationSchema>;