@reactionary/source 0.3.1 → 0.3.3

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.
Files changed (46) hide show
  1. package/core/src/client/client-builder.ts +8 -0
  2. package/core/src/client/client.ts +4 -0
  3. package/core/src/initialization.ts +4 -1
  4. package/core/src/providers/identity.provider.ts +5 -0
  5. package/core/src/providers/index.ts +2 -0
  6. package/core/src/providers/product-associations.provider.ts +49 -0
  7. package/core/src/providers/product-recommendations.provider.ts +150 -0
  8. package/core/src/schemas/capabilities.schema.ts +2 -0
  9. package/core/src/schemas/models/identifiers.model.ts +8 -0
  10. package/core/src/schemas/models/index.ts +1 -0
  11. package/core/src/schemas/models/product-recommendations.model.ts +10 -0
  12. package/core/src/schemas/queries/index.ts +2 -0
  13. package/core/src/schemas/queries/product-associations.query.ts +23 -0
  14. package/core/src/schemas/queries/product-recommendations.query.ts +72 -0
  15. package/core/src/schemas/session.schema.ts +2 -1
  16. package/documentation/docs/7-marketing.md +95 -0
  17. package/documentation/docs/8-tracking.md +1 -1
  18. package/examples/node/package.json +6 -6
  19. package/examples/node/src/basic/client-creation.spec.ts +2 -2
  20. package/examples/node/src/capabilities/product-recommendations.spec.ts +96 -0
  21. package/examples/node/src/utils.ts +3 -0
  22. package/package.json +1 -1
  23. package/providers/algolia/README.md +12 -4
  24. package/providers/algolia/src/core/initialize.ts +8 -2
  25. package/providers/algolia/src/index.ts +1 -0
  26. package/providers/algolia/src/providers/analytics.provider.ts +9 -7
  27. package/providers/algolia/src/providers/index.ts +2 -1
  28. package/providers/algolia/src/providers/product-recommendations.provider.ts +234 -0
  29. package/providers/algolia/src/providers/product-search.provider.ts +5 -4
  30. package/providers/algolia/src/schema/capabilities.schema.ts +2 -1
  31. package/providers/algolia/src/schema/product-recommendation.schema.ts +9 -0
  32. package/providers/algolia/src/test/analytics.spec.ts +138 -0
  33. package/providers/commercetools/src/providers/identity.provider.ts +8 -1
  34. package/providers/commercetools/src/test/caching.spec.ts +3 -3
  35. package/providers/commercetools/src/test/identity.spec.ts +2 -2
  36. package/providers/google-analytics/package.json +0 -6
  37. package/providers/medusa/src/core/initialize.ts +6 -0
  38. package/providers/medusa/src/index.ts +1 -0
  39. package/providers/medusa/src/providers/identity.provider.ts +34 -10
  40. package/providers/medusa/src/providers/product-recommendations.provider.ts +117 -0
  41. package/providers/medusa/src/schema/capabilities.schema.ts +1 -0
  42. package/providers/meilisearch/src/core/initialize.ts +6 -0
  43. package/providers/meilisearch/src/index.ts +1 -0
  44. package/providers/meilisearch/src/providers/index.ts +1 -0
  45. package/providers/meilisearch/src/providers/product-recommendations.provider.ts +89 -0
  46. package/providers/meilisearch/src/schema/capabilities.schema.ts +1 -0
@@ -6,6 +6,7 @@ import {
6
6
  RequestContextSchema,
7
7
  type RequestContext,
8
8
  } from '../schemas/session.schema.js';
9
+ import { MulticastProductRecommendationsProvider, type ProductRecommendationsProvider } from '../providers/product-recommendations.provider.js';
9
10
 
10
11
  type CapabilityFactory<T> = (cache: Cache, context: RequestContext) => T;
11
12
 
@@ -52,6 +53,8 @@ export class ClientBuilder<TClient = Client> {
52
53
 
53
54
  const mergedAnalytics: AnalyticsProvider[] = [];
54
55
 
56
+ const mergedProductRecommendationsProviders: ProductRecommendationsProvider[] = [];
57
+
55
58
  for (const factory of this.factories) {
56
59
  const provider = factory(sharedCache, this.context);
57
60
  client = {
@@ -62,12 +65,17 @@ export class ClientBuilder<TClient = Client> {
62
65
  if (provider.analytics) {
63
66
  mergedAnalytics.push(provider.analytics);
64
67
  }
68
+
69
+ if (provider.productRecommendations) {
70
+ mergedProductRecommendationsProviders.push(provider.productRecommendations);
71
+ }
65
72
  }
66
73
 
67
74
  // Add cache to complete the client
68
75
  const completeClient = {
69
76
  ...client,
70
77
  analytics: new MulticastAnalyticsProvider(sharedCache, this.context, mergedAnalytics),
78
+ productRecommendations: new MulticastProductRecommendationsProvider(sharedCache, this.context, mergedProductRecommendationsProviders),
71
79
  cache: sharedCache,
72
80
  } as TClient & { cache: Cache };
73
81
 
@@ -8,10 +8,14 @@ import type { Cache } from "../cache/cache.interface.js";
8
8
  import type { CategoryProvider } from "../providers/category.provider.js";
9
9
  import type { AnalyticsProvider, CheckoutProvider, OrderProvider, ProfileProvider, StoreProvider } from "../providers/index.js";
10
10
  import type { OrderSearchProvider } from "../providers/order-search.provider.js";
11
+ import type { ProductRecommendationsProvider } from "../providers/product-recommendations.provider.js";
12
+ import type { ProductAssociationsProvider } from "../providers/product-associations.provider.js";
11
13
 
12
14
  export interface Client {
13
15
  product: ProductProvider,
14
16
  productSearch: ProductSearchProvider,
17
+ productRecommendations: ProductRecommendationsProvider,
18
+ productAssociations: ProductAssociationsProvider,
15
19
  identity: IdentityProvider,
16
20
  cache: Cache,
17
21
  cart: CartProvider,
@@ -1,3 +1,4 @@
1
+ import type { AnonymousIdentity } from './schemas/index.js';
1
2
  import type { RequestContext } from './schemas/session.schema.js';
2
3
 
3
4
  export function createInitialRequestContext(): RequestContext {
@@ -17,7 +18,9 @@ export function createInitialRequestContext(): RequestContext {
17
18
  },
18
19
  session: {
19
20
  identityContext: {
20
- identifier: { userId: '' },
21
+ identity: {
22
+ type: 'Anonymous'
23
+ } satisfies AnonymousIdentity,
21
24
  lastUpdated: new Date(),
22
25
  personalizationKey: crypto.randomUUID(),
23
26
  },
@@ -13,4 +13,9 @@ export abstract class IdentityProvider extends BaseProvider {
13
13
  protected override getResourceName(): string {
14
14
  return 'identity';
15
15
  }
16
+
17
+ protected updateIdentityContext(identity: Identity) {
18
+ this.context.session.identityContext.lastUpdated = new Date();
19
+ this.context.session.identityContext.identity = identity;
20
+ }
16
21
  }
@@ -9,6 +9,8 @@ export * from './price.provider.js';
9
9
  export * from './product.provider.js';
10
10
  export * from './profile.provider.js';
11
11
  export * from './product-search.provider.js';
12
+ export * from './product-recommendations.provider.js';
13
+ export * from './product-associations.provider.js';
12
14
  export * from './store.provider.js';
13
15
  export * from './order.provider.js'
14
16
  export * from './order-search.provider.js'
@@ -0,0 +1,49 @@
1
+ import type { ProductIdentifier, ProductVariantIdentifier } from "../schemas/index.js";
2
+ import type { ProductAssociationsGetAccessoriesQuery, ProductAssociationsGetSparepartsQuery, ProductAssociationsGetReplacementsQuery } from "../schemas/queries/product-associations.query.js";
3
+ import { BaseProvider } from "./base.provider.js";
4
+
5
+
6
+ /**
7
+ * The product association provider is responsible for providing evidence based associations between products, such as
8
+ * accessories, spareparts, and replacements. These associations are typically used to provide recommendations to customers on the product detail page, but can also be used in other contexts such as the cart or post-purchase, but
9
+ * do not carry any personalization concept to them.
10
+ */
11
+ export abstract class ProductAssociationsProvider extends BaseProvider {
12
+
13
+ /**
14
+ * Returns a list of product identifiers which are accessories to the given product.
15
+ * Accessories in are products in their own right, but are commonly purchased alongside or recommended as complementary to the main product. Examples of accessories include:
16
+ * - A phone case for a smartphone
17
+ * - A camera bag for a camera
18
+ *
19
+ *
20
+ * Usecase:
21
+ * - PDP: Accessories for this product
22
+ */
23
+ public abstract getAccessories(query: ProductAssociationsGetAccessoriesQuery): Promise<ProductVariantIdentifier[]>;
24
+
25
+ /**
26
+ * Returns a list of product identifiers which are spareparts to the given product.
27
+ * Spareparts are products which are necessary for the use of the main product, but are not typically purchased alongside it. Examples of spareparts include:
28
+ *
29
+ * Usecase:
30
+ * - PDP: Accessories for this product
31
+ */
32
+ public abstract getSpareparts(query: ProductAssociationsGetSparepartsQuery): Promise<ProductVariantIdentifier[]>;
33
+
34
+
35
+ /**
36
+ * This product is replaced by these equivalent or newer products
37
+ * @param query
38
+ */
39
+ public abstract getReplacements(query: ProductAssociationsGetReplacementsQuery): Promise<ProductVariantIdentifier[]>;
40
+
41
+
42
+ getResourceName(): string {
43
+ return 'product-associations';
44
+ }
45
+
46
+
47
+
48
+
49
+ }
@@ -0,0 +1,150 @@
1
+ import type { Cache } from '../cache/cache.interface.js';
2
+ import { Reactionary } from "../decorators/reactionary.decorator.js";
3
+ import { success, type ProductRecommendation, type ProductRecommendationsByCollectionQuery, type RequestContext, type Result } from "../schemas/index.js";
4
+ import { ProductRecommendationsQuerySchema, type ProductRecommendationAlgorithmAlsoViewedProductsQuery, type ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuery, type ProductRecommendationAlgorithmPopuplarProductsQuery, type ProductRecommendationAlgorithmRelatedProductsQuery, type ProductRecommendationAlgorithmSimilarProductsQuery, type ProductRecommendationAlgorithmTopPicksProductsQuery, type ProductRecommendationAlgorithmTrendingInCategoryQuery, type ProductRecommendationsQuery } from "../schemas/queries/product-recommendations.query.js";
5
+ import { BaseProvider } from "./base.provider.js";
6
+
7
+ export abstract class ProductRecommendationsProvider extends BaseProvider {
8
+
9
+ /**
10
+ * returns a list of recommended products, based on the selected algorithm and the provided query parameters. The recommendations should be relevant to the product specified in the query, and can be personalized based on the customer segments or contexts provided. The provider should return a list of product variant identifiers that are recommended for the given product, which can then be used to fetch the full product details from the product provider if needed.
11
+ * *
12
+ * Usecase:
13
+ * - PDP - "Customers who viewed this product also viewed"
14
+ * - Cart - "You might also like"
15
+ * - Post-purchase - "Customers who bought this product also bought"
16
+ * - Article page: "Products related to the product mentioned in this article"
17
+ * @param query
18
+ */
19
+ public async getRecommendations(query: ProductRecommendationsQuery): Promise<Result<ProductRecommendation[]>> {
20
+ if (query.algorithm === 'frequentlyBoughtTogether') {
21
+ return success(await this.getFrequentlyBoughtTogetherRecommendations(query));
22
+ }
23
+
24
+ if (query.algorithm === 'similar') {
25
+ return success(await this.getSimilarProductsRecommendations(query));
26
+ }
27
+
28
+ if (query.algorithm === 'related') {
29
+ return success(await this.getRelatedProductsRecommendations(query));
30
+ }
31
+
32
+ if (query.algorithm === 'trendingInCategory') {
33
+ return success(await this.getTrendingInCategoryRecommendations(query));
34
+ }
35
+
36
+ if (query.algorithm === 'popular') {
37
+ return success(await this.getPopularProductsRecommendations(query));
38
+ }
39
+
40
+ if (query.algorithm === 'topPicks') {
41
+ return success(await this.getTopPicksProductsRecommendations(query));
42
+ }
43
+
44
+ if (query.algorithm === 'alsoViewed') {
45
+ return success(await this.getAlsoViewedProductsRecommendations(query));
46
+ }
47
+ return success([]);
48
+ }
49
+
50
+ public async getCollection(query: ProductRecommendationsByCollectionQuery): Promise<Result<ProductRecommendation[]>> {
51
+ return success([]);
52
+ }
53
+
54
+ protected async getFrequentlyBoughtTogetherRecommendations(query: ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuery): Promise<ProductRecommendation[]> {
55
+ return [];
56
+ }
57
+
58
+ protected async getSimilarProductsRecommendations(query: ProductRecommendationAlgorithmSimilarProductsQuery): Promise<ProductRecommendation[]> {
59
+ return [];
60
+ }
61
+
62
+ protected async getTrendingInCategoryRecommendations(query: ProductRecommendationAlgorithmTrendingInCategoryQuery): Promise<ProductRecommendation[]> {
63
+ return [];
64
+ }
65
+
66
+ protected async getRelatedProductsRecommendations(query: ProductRecommendationAlgorithmRelatedProductsQuery): Promise<ProductRecommendation[]> {
67
+ return [];
68
+ }
69
+
70
+ protected async getPopularProductsRecommendations(query: ProductRecommendationAlgorithmPopuplarProductsQuery): Promise<ProductRecommendation[]> {
71
+ return [];
72
+ }
73
+
74
+ protected async getTopPicksProductsRecommendations(query: ProductRecommendationAlgorithmTopPicksProductsQuery): Promise<ProductRecommendation[]> {
75
+ return [];
76
+ }
77
+
78
+ protected async getAlsoViewedProductsRecommendations(query: ProductRecommendationAlgorithmAlsoViewedProductsQuery): Promise<ProductRecommendation[]> {
79
+ return [];
80
+ }
81
+
82
+
83
+ protected override getResourceName(): string {
84
+ return 'product-recommendations';
85
+ }
86
+
87
+
88
+ }
89
+
90
+
91
+
92
+ export class MulticastProductRecommendationsProvider extends ProductRecommendationsProvider {
93
+ protected providers: Array<ProductRecommendationsProvider>;
94
+
95
+ constructor(
96
+ cache: Cache,
97
+ requestContext: RequestContext,
98
+ providers: Array<ProductRecommendationsProvider>
99
+ ) {
100
+ super(cache, requestContext);
101
+
102
+ this.providers = providers;
103
+ }
104
+
105
+ @Reactionary({
106
+ inputSchema: ProductRecommendationsQuerySchema,
107
+ })
108
+ public override async getRecommendations(query: ProductRecommendationsQuery): Promise<Result<ProductRecommendation[]>> {
109
+ const output = [];
110
+ for (const provider of this.providers) {
111
+ const providerOutput = await provider.getRecommendations(query);
112
+ if (providerOutput.success) {
113
+
114
+ output.push(...providerOutput.value);
115
+ } else {
116
+ // For other types of errors, we might want to log them or handle them differently
117
+ console.error(`Error from provider ${provider.constructor.name}:`, providerOutput.error);
118
+ return providerOutput
119
+ }
120
+ if (output.length >= query.numberOfRecommendations) {
121
+ break;
122
+ }
123
+ }
124
+ return success(output.slice(0, query.numberOfRecommendations));
125
+ }
126
+
127
+ public override async getCollection(query: ProductRecommendationsByCollectionQuery): Promise<Result<ProductRecommendation[]>> {
128
+ const output = [];
129
+ for (const provider of this.providers) {
130
+ const providerOutput = await provider.getCollection(query);
131
+ if (providerOutput.success) {
132
+ output.push(...providerOutput.value);
133
+ } else {
134
+ if (providerOutput.error.type === 'NotFound') {
135
+ // If the error is a NotFound error, we can ignore it and continue to the next provider
136
+ continue;
137
+ } else {
138
+ // For other types of errors, we might want to log them or handle them differently
139
+ console.error(`Error from provider ${provider.constructor.name}:`, providerOutput.error);
140
+ return providerOutput;
141
+ }
142
+ }
143
+ if (output.length >= query.numberOfRecommendations) {
144
+ break;
145
+ }
146
+ }
147
+ return success(output.slice(0, query.numberOfRecommendations));
148
+ }
149
+
150
+ }
@@ -5,6 +5,8 @@ import type { Client } from '../client/client.js';
5
5
  export const CapabilitiesSchema = z.looseObject({
6
6
  product: z.boolean(),
7
7
  productSearch: z.boolean(),
8
+ productAssociations: z.boolean(),
9
+ productRecommendations: z.boolean(),
8
10
  analytics: z.boolean(),
9
11
  identity: z.boolean(),
10
12
  cart: z.boolean(),
@@ -118,6 +118,11 @@ export const PickupPointIdentifierSchema = z.looseObject({
118
118
  key: z.string(),
119
119
  });
120
120
 
121
+ export const ProductRecommendationIdentifierSchema = z.looseObject({
122
+ key: z.string(),
123
+ algorithm: z.string(),
124
+ });
125
+
121
126
 
122
127
  export const ProductSearchIdentifierSchema = z.looseObject({
123
128
  term: z.string().describe('The search term used to find products.'),
@@ -141,6 +146,7 @@ export const OrderSearchIdentifierSchema = z.looseObject({
141
146
  paginationOptions: PaginationOptionsSchema.describe('Pagination options for the search results.'),
142
147
  });
143
148
 
149
+
144
150
  export type OrderSearchIdentifier = InferType<typeof OrderSearchIdentifierSchema>;
145
151
  export type ProductIdentifier = InferType<typeof ProductIdentifierSchema>;
146
152
  export type ProductVariantIdentifier = InferType<typeof ProductVariantIdentifierSchema>;
@@ -178,6 +184,7 @@ export type ProductOptionIdentifier = InferType<typeof ProductOptionIdentifierSc
178
184
  export type ProductOptionValueIdentifier = InferType<typeof ProductOptionValueIdentifierSchema>;
179
185
  export type ProductAttributeIdentifier = InferType<typeof ProductAttributeIdentifierSchema>;
180
186
  export type ProductAttributeValueIdentifier = InferType<typeof ProductAttributeValueIdentifierSchema>;
187
+ export type ProductRecommendationIdentifier = InferType<typeof ProductRecommendationIdentifierSchema>;
181
188
 
182
189
  export type IdentifierType =
183
190
  | ProductIdentifier
@@ -191,6 +198,7 @@ export type IdentifierType =
191
198
  | CategoryIdentifier
192
199
  | WebStoreIdentifier
193
200
  | InventoryIdentifier
201
+ | ProductRecommendationIdentifier
194
202
  | FulfillmentCenterIdentifier
195
203
  | IdentityIdentifier
196
204
  | ShippingMethodIdentifier
@@ -18,3 +18,4 @@ export * from './cost.model.js';
18
18
  export * from './checkout.model.js';
19
19
  export * from './payment.model.js';
20
20
  export * from './order-search.model.js'
21
+ export * from './product-recommendations.model.js';
@@ -0,0 +1,10 @@
1
+ import z from "zod";
2
+ import { ProductIdentifierSchema, ProductRecommendationIdentifierSchema, ProductVariantIdentifierSchema } from "./identifiers.model.js";
3
+
4
+ export const ProductRecommendationSchema = z.looseObject({
5
+ recommendationIdentifier: ProductRecommendationIdentifierSchema.describe('The identifier for the product recommendation, which includes a key and an algorithm and any other vendor specific/instance specific data '),
6
+ product: ProductIdentifierSchema.describe('The identifier for the recommended product.'),
7
+ });
8
+
9
+
10
+ export type ProductRecommendation = z.infer<typeof ProductRecommendationSchema>;
@@ -12,3 +12,5 @@ export * from './store.query.js';
12
12
  export * from './order.query.js';
13
13
  export * from './checkout.query.js';
14
14
  export * from './order-search.query.js'
15
+ export * from './product-recommendations.query.js';
16
+ export * from './product-associations.query.js';
@@ -0,0 +1,23 @@
1
+ import z from "zod";
2
+ import { BaseQuerySchema } from "./base.query.js";
3
+ import { ProductIdentifierSchema, ProductVariantIdentifierSchema } from "../models/identifiers.model.js";
4
+ import { CartItemSchema } from "../models/cart.model.js";
5
+
6
+ export const ProductAssociationsGetAccessoriesQuerySchema = BaseQuerySchema.extend({
7
+ forProductVariant: ProductVariantIdentifierSchema.describe('The product variant identifier for which to get accessory recommendations. The provider should return recommendations that are relevant to this product, e.g., products that are frequently bought together, products that are similar in style or category, or products that are popular among users with similar preferences.'),
8
+ numberOfAccessories: z.number().min(1).max(12).describe('The number of accessory recommendations requested. The provider may return fewer than this number, but should not return more.'),
9
+ });
10
+
11
+ export const ProductAssociationsGetSparepartsQuerySchema = BaseQuerySchema.extend({
12
+ forProductVariant: ProductVariantIdentifierSchema.describe('The product variant identifier for which to get similar item recommendations. The provider should return recommendations that are relevant to this product, e.g., products that are frequently bought together, products that are similar in style or category, or products that are popular among users with similar preferences.'),
13
+ });
14
+
15
+ export const ProductAssociationsGetReplacementsQuerySchema = BaseQuerySchema.extend({
16
+ forProductVariant: ProductVariantIdentifierSchema.describe('The product variant identifier for which to get replacement recommendations. The provider should return recommendations that are relevant to this product, e.g., products that are frequently bought together, products that are similar in style or category, or products that are popular among users with similar preferences.'),
17
+ });
18
+
19
+
20
+
21
+ export type ProductAssociationsGetAccessoriesQuery = z.infer<typeof ProductAssociationsGetAccessoriesQuerySchema>;
22
+ export type ProductAssociationsGetSparepartsQuery = z.infer<typeof ProductAssociationsGetSparepartsQuerySchema>;
23
+ export type ProductAssociationsGetReplacementsQuery = z.infer<typeof ProductAssociationsGetReplacementsQuerySchema>;
@@ -0,0 +1,72 @@
1
+ import z from "zod";
2
+ import type { InferType } from "../../zod-utils.js";
3
+ import { CategoryIdentifierSchema, ProductIdentifierSchema } from "../models/identifiers.model.js";
4
+ import { BaseQuerySchema } from "./base.query.js";
5
+
6
+ export const ProductRecommendationBaseQuerySchema = BaseQuerySchema.extend({
7
+ numberOfRecommendations: z.number().min(1).max(12).describe('The number of recommendations requested. The provider may return fewer than this number, but should not return more.'),
8
+ labels: z.array(z.string()).optional().describe('The customer segments, quirks, chirps or other labels to which the recommendations can optimize themselves to be relevant. This can be used by the provider to personalize the recommendations based on the preferences and behaviors of users in these segments.'),
9
+ });
10
+
11
+ export const ProductRecommendationsByCollectionQuerySchema = ProductRecommendationBaseQuerySchema.extend({
12
+ collectionName: z.string().describe('The name of the collection for which to get product recommendations. This is to access either manually curated lists, or interface marketing rules engines that define zones by name'),
13
+ sourceProduct: z.array(ProductIdentifierSchema).optional().describe('The products on screen or in the context you are asking for the recommendations. Could be all the variants from the current cart (resolved into their products), or just the variant of the PDP, or the 4 first products of a category.'),
14
+ sourceCategory: CategoryIdentifierSchema.optional().describe('The category identifier to use as a seed for the recommendations. The provider should return recommendations that are relevant to this category, e.g., products that are frequently bought together, products that are similar in style or category, or products that are popular among users with similar preferences. This is optional, as the collection may already be curated to be relevant to a specific product or category, but it can be used by the provider to further personalize the recommendations based on the preferences and behaviors of users who have interacted with this category.'),
15
+ });
16
+
17
+ export const ProductRecommendationProductBasedBaseQuerySchema = ProductRecommendationBaseQuerySchema.extend({
18
+ sourceProduct: ProductIdentifierSchema.describe('The product identifiers for which to get recommendations. The provider should return recommendations that are relevant to these products, e.g., products that are frequently bought together, products that are similar in style or category, or products that are popular among users with similar preferences.'),
19
+ });
20
+
21
+ export const ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuerySchema = ProductRecommendationProductBasedBaseQuerySchema.extend({
22
+ algorithm: z.literal('frequentlyBoughtTogether').describe('The provider should return recommendations based on products that are frequently bought together with the source products. The provider should leverage the Request Context to personalize the recommendations as much as possible, taking into account factors such as the user\'s browsing history, purchase history, and demographic information.'),
23
+ });
24
+
25
+ export const ProductRecommendationAlgorithmSimilarProductsQuerySchema = ProductRecommendationProductBasedBaseQuerySchema.extend({
26
+ algorithm: z.literal('similar').describe('The provider should return recommendations based on products that are similar to the source products either visually or data wise'),
27
+ });
28
+
29
+ export const ProductRecommendationAlgorithmRelatedProductsQuerySchema = ProductRecommendationProductBasedBaseQuerySchema.extend({
30
+ algorithm: z.literal('related').describe('The provider should return recommendations based on products that are related to the source products. '),
31
+ });
32
+
33
+ export const ProductRecommendationAlgorithmTrendingInCategoryQuerySchema = ProductRecommendationBaseQuerySchema.extend({
34
+ algorithm: z.literal('trendingInCategory').describe('The provider should return recommendations based on products that are trending in the specified category. The provider should leverage the Request Context to personalize the recommendations as much as possible, taking into account factors such as the user\'s browsing history, purchase history, and demographic information.'),
35
+ sourceCategory: CategoryIdentifierSchema.describe('The category identifier for which to get trending product recommendations. The provider should return recommendations that are relevant to this category, e.g., products that are frequently bought together, products that are similar in style or category, or products that are popular among users with similar preferences.'),
36
+ });
37
+
38
+
39
+ // unsure if we need both Popular and TopPicks, as they are quite similar. Maybe we can merge them into one algorithm with a parameter to specify the type of popularity? For now, I'll keep them separate for clarity.
40
+ export const ProductRecommendationAlgorithmPopuplarProductsQuerySchema = ProductRecommendationBaseQuerySchema.extend({
41
+ algorithm: z.literal('popular').describe('The provider should return recommendations based on products that are popular among users. The provider should leverage the Request Context to personalize the recommendations as much as possible, taking into account factors such as the user\'s browsing history, purchase history, and demographic information.'),
42
+ });
43
+
44
+ export const ProductRecommendationAlgorithmTopPicksProductsQuerySchema = ProductRecommendationBaseQuerySchema.extend({
45
+ algorithm: z.literal('topPicks').describe('The provider should return recommendations based on products that are top picks among users. The provider should leverage the Request Context to personalize the recommendations as much as possible, taking into account factors such as the user\'s browsing history, purchase history, and demographic information.') ,
46
+ });
47
+
48
+ export const ProductRecommendationAlgorithmAlsoViewedProductsQuerySchema = ProductRecommendationProductBasedBaseQuerySchema.extend({
49
+ algorithm: z.literal('alsoViewed').describe('The provider should return recommendations based on products that are also viewed by users. The provider should leverage the Request Context to personalize the recommendations as much as possible, taking into account factors such as the user\'s browsing history, purchase history, and demographic information.') ,
50
+ });
51
+
52
+
53
+
54
+ export const ProductRecommendationsQuerySchema = z.discriminatedUnion('algorithm', [
55
+ ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuerySchema,
56
+ ProductRecommendationAlgorithmTrendingInCategoryQuerySchema,
57
+ ProductRecommendationAlgorithmSimilarProductsQuerySchema,
58
+ ProductRecommendationAlgorithmRelatedProductsQuerySchema,
59
+ ProductRecommendationAlgorithmPopuplarProductsQuerySchema,
60
+ ProductRecommendationAlgorithmTopPicksProductsQuerySchema,
61
+ ProductRecommendationAlgorithmAlsoViewedProductsQuerySchema
62
+ ]);
63
+
64
+ export type ProductRecommendationsQuery = InferType<typeof ProductRecommendationsQuerySchema>;
65
+ export type ProductRecommendationsByCollectionQuery = InferType<typeof ProductRecommendationsByCollectionQuerySchema>;
66
+ export type ProductRecommendationAlgorithmTopPicksProductsQuery = InferType<typeof ProductRecommendationAlgorithmTopPicksProductsQuerySchema>;
67
+ export type ProductRecommendationAlgorithmPopuplarProductsQuery = InferType<typeof ProductRecommendationAlgorithmPopuplarProductsQuerySchema>;
68
+ export type ProductRecommendationAlgorithmTrendingInCategoryQuery = InferType<typeof ProductRecommendationAlgorithmTrendingInCategoryQuerySchema>;
69
+ export type ProductRecommendationAlgorithmRelatedProductsQuery = InferType<typeof ProductRecommendationAlgorithmRelatedProductsQuerySchema>;
70
+ export type ProductRecommendationAlgorithmSimilarProductsQuery= InferType<typeof ProductRecommendationAlgorithmSimilarProductsQuerySchema>;
71
+ export type ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuery = InferType<typeof ProductRecommendationAlgorithmFrequentlyBoughtTogetherQuerySchema>;
72
+ export type ProductRecommendationAlgorithmAlsoViewedProductsQuery = InferType<typeof ProductRecommendationAlgorithmAlsoViewedProductsQuerySchema>;
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { IdentityIdentifierSchema, WebStoreIdentifierSchema } from './models/identifiers.model.js';
3
3
  import { CurrencySchema } from './models/currency.model.js';
4
+ import { IdentitySchema } from './models/identity.model.js';
4
5
 
5
6
  /**
6
7
  * The language and locale context for the current request.
@@ -11,7 +12,7 @@ export const LanguageContextSchema = z.looseObject( {
11
12
  });
12
13
 
13
14
  export const IdentityContextSchema = z.looseObject({
14
- identifier: IdentityIdentifierSchema,
15
+ identity: IdentitySchema,
15
16
  personalizationKey: z.string(),
16
17
  lastUpdated: z.date()
17
18
  });
@@ -1,3 +1,98 @@
1
1
  # Adding the personal touch
2
2
 
3
+ Reactionary supports many mechanisms by which to make the customer journey unique to a customer. The main mechanism is via the `ProductRecommendationsProvider` which offers 7 unique ways to find some relevant products to display for the user.
4
+
5
+ It is assumed, that any basic-direct-assign logic (ie editor has picked 4 products to show) is already handled at the CMS level. The Reactionary provider is for the usecase where you want a dynamic result back, based on the users behavior, tags, or whatever you have.
6
+
7
+ The supported algorithms are
8
+
9
+ ```
10
+ - Frequently-Bought-Together (Product based)
11
+ - Trending-In-Category (Category based)
12
+ - Similar (Product)
13
+ - Related-Items (Product)
14
+ - Popular-Products (User)
15
+ - Top-Picks-For-You (User)
16
+ - Also-Viewed (Product, User)
17
+ ```
18
+
19
+ Reactionary supports having multiple providers of product recommendations, each of which will be asked in order, until the result is fully populated.
20
+ Every provider might not support every algorithm though.
21
+
22
+ But, the point of this is, that you can either hardcode something on the PDP
23
+
24
+ ```ts
25
+ const recommendations = client.productRecommendations.getRecommendations({
26
+ algorithm: 'similar',
27
+ numberOfRecommendations: 8,
28
+ labels: [
29
+ (
30
+ isLoggedIn: 'Registered' : 'Guest',
31
+ registeredThisSession? 'FirstSession': 'ReturningCustomer',
32
+ customerSegments.map(x => x.name),
33
+ new Date().getHour() > 12? "Afternoon": "Morning",
34
+ getTemperatureAround(context.clientIp) < 20: 'Cold': 'Warm'
35
+ )
36
+ ]
37
+ });
38
+
39
+ if (recommendations.success) {
40
+ // we can now resolve them
41
+ const allProducts = Promise.all(
42
+ recommendations.value.products.map(x => client.product.getByID(x)).map(x => x.successs? x.value : null )).filter(y => !!y);
43
+ // and draw them...
44
+ }
45
+ ```
46
+
47
+ Instead of using the full `client.product` you could have resolved it via the `.productSearch` to get a smaller data-footprint. However, since you are hopefully just about to navigate to one of these pages, it might be better for overall cache performance to use the full data model.
48
+
49
+
50
+
51
+
52
+ ## Collections
53
+
54
+ Some systems offer named access to a potentially rules based selection of products. An example is the HCL Commerce espot system, where your frontpage migth have a spot called "Newest arrivals", and behind the scenes some rules engine decides what to show there.
55
+
56
+ It can also be simply the name of a CMS entity, or other system specific functionality.
57
+
58
+ You can call that as well here:
59
+
60
+ ```ts
61
+ const recommendations = client.productRecommendations.getCollection({
62
+ collectionName: 'newest-arrivals',
63
+ numberOfRecommendations: 8,
64
+ labels: [
65
+ (
66
+ isLoggedIn: 'Registered' : 'Guest',
67
+ registeredThisSession? 'FirstSession': 'ReturningCustomer',
68
+ customerSegments.map(x => x.name),
69
+ new Date().getHour() > 12? "Afternoon": "Morning",
70
+ getTemperatureAround(context.clientIp) < 20: 'Cold': 'Warm'
71
+ )
72
+ ]
73
+ });
74
+
75
+ if (recommendations.success) {
76
+ // we can now resolve them
77
+ const allProducts = Promise.all(
78
+ recommendations.value.products.map(x => client.product.getByID(x)).map(x => x.successs? x.value : null )).filter(y => !!y);
79
+ // and draw them...
80
+ }
81
+ ```
82
+
83
+ ### Configuration
84
+ Since not every system can provide every recommendation algorithm, Reactionary supports enabling this capability on multiple providers.
85
+
86
+ So if you have both algolia and medusa, you can set `productRecommendations: true` on both, and the system will then ask each system in turn until the required number of results have been gathered.
87
+
88
+
89
+
90
+ ## Design decisions
91
+ Since not all systems can deliver full product data back, we are initially only returning the IDs of the products, so you have to use the `product` provider to resolve it into something displayable.
92
+
93
+ Later, we might add a discriminator, allowing the same bare-bones/tile-ready data to be returned from this, as is returned from the product search.
94
+
95
+ It was also decided to have the source and target unit be a product (Forest Ranger T-Shirt), and not the individual sku (Forest Ranger T-Shirt, Yellow, Size-32).
96
+
97
+ This is from emperical studies showign that you will get more results from the toplevel product, than from a rarely used SKU.
3
98
 
@@ -6,4 +6,4 @@ Reactionary takes the approach that tracking customer data should be structured
6
6
  - Structure: it should be possible to reason about what is tracked on the site, how it is tracked and when it is tracked without having to visit seven different tag managers.
7
7
  - Security: pulling in all the embedded and inline scripts from every tag manager is a security incident waiting to happen.
8
8
 
9
- To this end the client exposes a single provider in the form of `client.analytics`. This client is internally responsible for delegating events to all relevant subscribers capabilities used to build the client. This means that a single call to record a pageview or attribution will be enough, even if that data internally needs to be multiplexed to GA4, Algolia and Posthog as an example.
9
+ To this end the client exposes a single provider in the form of `client.analytics`. This client is internally responsible for delegating events to all relevant subscribers capabilities used to build the client. This means that a single call to record a pageview or attribution will be enough, even if that data internally needs to be multiplexed to GA4, Algolia and Posthog as an example.
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@reactionary/examples-node",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "main": "index.js",
5
5
  "types": "src/index.d.ts",
6
6
  "dependencies": {
7
- "@reactionary/core": "0.3.1",
8
- "@reactionary/provider-commercetools": "0.3.1",
9
- "@reactionary/provider-algolia": "0.3.1",
10
- "@reactionary/provider-medusa": "0.3.1",
11
- "@reactionary/provider-meilisearch": "0.3.1"
7
+ "@reactionary/core": "0.3.3",
8
+ "@reactionary/provider-commercetools": "0.3.3",
9
+ "@reactionary/provider-algolia": "0.3.3",
10
+ "@reactionary/provider-medusa": "0.3.3",
11
+ "@reactionary/provider-meilisearch": "0.3.3"
12
12
  },
13
13
  "type": "module"
14
14
  }
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
2
2
  import { ClientBuilder, createInitialRequestContext, NoOpCache } from '@reactionary/core';
3
3
  import { FakeProductProvider, withFakeCapabilities } from '@reactionary/provider-fake';
4
4
  import { CommercetoolsCartProvider, withCommercetoolsCapabilities } from '@reactionary/provider-commercetools';
5
- import { AlgoliaSearchProvider, withAlgoliaCapabilities } from '@reactionary/provider-algolia';
5
+ import { AlgoliaProductSearchProvider, withAlgoliaCapabilities } from '@reactionary/provider-algolia';
6
6
 
7
7
  describe('client creation', () => {
8
8
  it('should be able to mix providers and get a valid, typed client', async () => {
@@ -48,6 +48,6 @@ describe('client creation', () => {
48
48
 
49
49
  expect(client.cart).toBeInstanceOf(CommercetoolsCartProvider);
50
50
  expect(client.product).toBeInstanceOf(FakeProductProvider);
51
- expect(client.productSearch).toBeInstanceOf(AlgoliaSearchProvider);
51
+ expect(client.productSearch).toBeInstanceOf(AlgoliaProductSearchProvider);
52
52
  });
53
53
  });