@reactionary/source 0.0.52 → 0.2.16

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 (293) hide show
  1. package/.env-template +19 -0
  2. package/.github/workflows/pull-request.yml +3 -1
  3. package/.github/workflows/release.yml +9 -0
  4. package/.vscode/extensions.json +0 -2
  5. package/LICENSE +21 -0
  6. package/README.md +175 -23
  7. package/core/package.json +6 -3
  8. package/core/src/cache/cache.interface.ts +1 -0
  9. package/core/src/cache/index.ts +4 -0
  10. package/core/src/cache/memory-cache.ts +30 -2
  11. package/core/src/cache/noop-cache.ts +15 -1
  12. package/core/src/cache/redis-cache.ts +20 -0
  13. package/core/src/client/client-builder.ts +71 -54
  14. package/core/src/client/client.ts +9 -47
  15. package/core/src/client/index.ts +2 -0
  16. package/core/src/decorators/index.ts +1 -0
  17. package/core/src/decorators/reactionary.decorator.ts +203 -34
  18. package/core/src/index.ts +6 -19
  19. package/core/src/initialization.ts +1 -18
  20. package/core/src/metrics/metrics.ts +67 -0
  21. package/core/src/providers/analytics.provider.ts +1 -6
  22. package/core/src/providers/base.provider.ts +5 -69
  23. package/core/src/providers/cart.provider.ts +15 -55
  24. package/core/src/providers/category.provider.ts +7 -11
  25. package/core/src/providers/checkout.provider.ts +17 -15
  26. package/core/src/providers/identity.provider.ts +6 -8
  27. package/core/src/providers/index.ts +2 -1
  28. package/core/src/providers/inventory.provider.ts +15 -5
  29. package/core/src/providers/order-search.provider.ts +29 -0
  30. package/core/src/providers/order.provider.ts +47 -15
  31. package/core/src/providers/price.provider.ts +30 -36
  32. package/core/src/providers/product-search.provider.ts +61 -0
  33. package/core/src/providers/product.provider.ts +71 -12
  34. package/core/src/providers/profile.provider.ts +74 -14
  35. package/core/src/providers/store.provider.ts +3 -5
  36. package/core/src/schemas/capabilities.schema.ts +10 -3
  37. package/core/src/schemas/errors/generic.error.ts +9 -0
  38. package/core/src/schemas/errors/index.ts +4 -0
  39. package/core/src/schemas/errors/invalid-input.error.ts +9 -0
  40. package/core/src/schemas/errors/invalid-output.error.ts +9 -0
  41. package/core/src/schemas/errors/not-found.error.ts +9 -0
  42. package/core/src/schemas/index.ts +7 -0
  43. package/core/src/schemas/models/analytics.model.ts +2 -1
  44. package/core/src/schemas/models/base.model.ts +6 -24
  45. package/core/src/schemas/models/cart.model.ts +5 -8
  46. package/core/src/schemas/models/category.model.ts +4 -9
  47. package/core/src/schemas/models/checkout.model.ts +6 -7
  48. package/core/src/schemas/models/cost.model.ts +4 -3
  49. package/core/src/schemas/models/currency.model.ts +2 -1
  50. package/core/src/schemas/models/identifiers.model.ts +106 -62
  51. package/core/src/schemas/models/identity.model.ts +10 -19
  52. package/core/src/schemas/models/index.ts +2 -1
  53. package/core/src/schemas/models/inventory.model.ts +8 -5
  54. package/core/src/schemas/models/order-search.model.ts +28 -0
  55. package/core/src/schemas/models/order.model.ts +20 -26
  56. package/core/src/schemas/models/payment.model.ts +14 -17
  57. package/core/src/schemas/models/price.model.ts +11 -11
  58. package/core/src/schemas/models/product-search.model.ts +42 -0
  59. package/core/src/schemas/models/product.model.ts +64 -22
  60. package/core/src/schemas/models/profile.model.ts +19 -22
  61. package/core/src/schemas/models/shipping-method.model.ts +24 -29
  62. package/core/src/schemas/models/store.model.ts +9 -5
  63. package/core/src/schemas/mutations/analytics.mutation.ts +8 -7
  64. package/core/src/schemas/mutations/base.mutation.ts +2 -1
  65. package/core/src/schemas/mutations/cart.mutation.ts +33 -33
  66. package/core/src/schemas/mutations/checkout.mutation.ts +23 -30
  67. package/core/src/schemas/mutations/identity.mutation.ts +4 -3
  68. package/core/src/schemas/mutations/profile.mutation.ts +38 -3
  69. package/core/src/schemas/queries/base.query.ts +2 -1
  70. package/core/src/schemas/queries/cart.query.ts +3 -3
  71. package/core/src/schemas/queries/category.query.ts +18 -18
  72. package/core/src/schemas/queries/checkout.query.ts +7 -9
  73. package/core/src/schemas/queries/identity.query.ts +2 -1
  74. package/core/src/schemas/queries/index.ts +2 -1
  75. package/core/src/schemas/queries/inventory.query.ts +5 -5
  76. package/core/src/schemas/queries/order-search.query.ts +10 -0
  77. package/core/src/schemas/queries/order.query.ts +3 -2
  78. package/core/src/schemas/queries/price.query.ts +10 -4
  79. package/core/src/schemas/queries/product-search.query.ts +16 -0
  80. package/core/src/schemas/queries/product.query.ts +13 -6
  81. package/core/src/schemas/queries/profile.query.ts +5 -2
  82. package/core/src/schemas/queries/store.query.ts +6 -5
  83. package/core/src/schemas/result.ts +107 -0
  84. package/core/src/schemas/session.schema.ts +4 -4
  85. package/core/src/test/reactionary.decorator.spec.ts +249 -0
  86. package/core/src/zod-utils.ts +19 -0
  87. package/core/tsconfig.json +1 -1
  88. package/core/tsconfig.spec.json +2 -26
  89. package/core/vitest.config.ts +14 -0
  90. package/documentation/1-purpose.md +114 -0
  91. package/documentation/2-getting-started.md +229 -0
  92. package/documentation/3-querying-and-changing-data.md +74 -0
  93. package/documentation/4-product-data.md +107 -0
  94. package/documentation/5-cart-and-checkout.md +211 -0
  95. package/documentation/6-product-search.md +143 -0
  96. package/documentation/7-marketing.md +3 -0
  97. package/eslint.config.mjs +1 -0
  98. package/examples/node/eslint.config.mjs +1 -4
  99. package/examples/node/package.json +10 -3
  100. package/examples/node/project.json +4 -1
  101. package/examples/node/src/basic/basic-node-provider-model-extension.spec.ts +22 -23
  102. package/examples/node/src/basic/basic-node-provider-query-extension.spec.ts +15 -11
  103. package/examples/node/src/basic/basic-node-setup.spec.ts +44 -28
  104. package/examples/node/src/basic/client-creation.spec.ts +53 -0
  105. package/examples/node/src/capabilities/cart.spec.ts +255 -0
  106. package/examples/node/src/capabilities/category.spec.ts +193 -0
  107. package/examples/node/src/capabilities/checkout.spec.ts +341 -0
  108. package/examples/node/src/capabilities/identity.spec.ts +93 -0
  109. package/examples/node/src/capabilities/inventory.spec.ts +66 -0
  110. package/examples/node/src/capabilities/order-search.spec.ts +265 -0
  111. package/examples/node/src/capabilities/order.spec.ts +91 -0
  112. package/examples/node/src/capabilities/price.spec.ts +51 -0
  113. package/examples/node/src/capabilities/product-search.spec.ts +293 -0
  114. package/examples/node/src/capabilities/product.spec.ts +122 -0
  115. package/examples/node/src/capabilities/profile.spec.ts +316 -0
  116. package/examples/node/src/capabilities/store.spec.ts +26 -0
  117. package/examples/node/src/utils.ts +147 -0
  118. package/examples/node/tsconfig.json +9 -12
  119. package/examples/node/tsconfig.lib.json +1 -2
  120. package/examples/node/tsconfig.spec.json +2 -14
  121. package/examples/node/vitest.config.ts +14 -0
  122. package/migrations.json +22 -5
  123. package/nx.json +8 -47
  124. package/package.json +24 -96
  125. package/providers/algolia/README.md +39 -2
  126. package/providers/algolia/package.json +2 -1
  127. package/providers/algolia/src/core/initialize.ts +7 -14
  128. package/providers/algolia/src/index.ts +2 -4
  129. package/providers/algolia/src/providers/index.ts +1 -0
  130. package/providers/algolia/src/providers/product-search.provider.ts +241 -0
  131. package/providers/algolia/src/schema/capabilities.schema.ts +2 -3
  132. package/providers/algolia/src/schema/index.ts +3 -0
  133. package/providers/algolia/src/schema/search.schema.ts +8 -8
  134. package/providers/algolia/tsconfig.json +1 -1
  135. package/providers/algolia/tsconfig.lib.json +1 -1
  136. package/providers/algolia/tsconfig.spec.json +2 -14
  137. package/providers/algolia/vitest.config.ts +14 -0
  138. package/providers/commercetools/README.md +30 -3
  139. package/providers/commercetools/package.json +2 -1
  140. package/providers/commercetools/src/core/client.ts +178 -99
  141. package/providers/commercetools/src/core/initialize.ts +130 -74
  142. package/providers/commercetools/src/core/token-cache.ts +45 -0
  143. package/providers/commercetools/src/index.ts +3 -2
  144. package/providers/commercetools/src/providers/cart.provider.ts +281 -341
  145. package/providers/commercetools/src/providers/category.provider.ts +223 -138
  146. package/providers/commercetools/src/providers/checkout.provider.ts +631 -449
  147. package/providers/commercetools/src/providers/identity.provider.ts +50 -29
  148. package/providers/commercetools/src/providers/index.ts +2 -2
  149. package/providers/commercetools/src/providers/inventory.provider.ts +76 -74
  150. package/providers/commercetools/src/providers/order-search.provider.ts +220 -0
  151. package/providers/commercetools/src/providers/order.provider.ts +96 -61
  152. package/providers/commercetools/src/providers/price.provider.ts +147 -117
  153. package/providers/commercetools/src/providers/product-search.provider.ts +528 -0
  154. package/providers/commercetools/src/providers/product.provider.ts +249 -74
  155. package/providers/commercetools/src/providers/profile.provider.ts +445 -28
  156. package/providers/commercetools/src/providers/store.provider.ts +54 -40
  157. package/providers/commercetools/src/schema/capabilities.schema.ts +3 -1
  158. package/providers/commercetools/src/schema/commercetools.schema.ts +17 -3
  159. package/providers/commercetools/src/schema/configuration.schema.ts +1 -0
  160. package/providers/commercetools/src/schema/session.schema.ts +7 -0
  161. package/providers/commercetools/src/test/caching.spec.ts +82 -0
  162. package/providers/commercetools/src/test/identity.spec.ts +109 -0
  163. package/providers/commercetools/src/test/test-utils.ts +21 -19
  164. package/providers/commercetools/tsconfig.json +1 -1
  165. package/providers/commercetools/tsconfig.lib.json +1 -1
  166. package/providers/commercetools/tsconfig.spec.json +2 -14
  167. package/providers/commercetools/vitest.config.ts +15 -0
  168. package/providers/fake/README.md +20 -4
  169. package/providers/fake/package.json +2 -1
  170. package/providers/fake/src/core/initialize.ts +47 -49
  171. package/providers/fake/src/providers/analytics.provider.ts +5 -7
  172. package/providers/fake/src/providers/cart.provider.ts +163 -92
  173. package/providers/fake/src/providers/category.provider.ts +78 -50
  174. package/providers/fake/src/providers/checkout.provider.ts +254 -0
  175. package/providers/fake/src/providers/identity.provider.ts +57 -65
  176. package/providers/fake/src/providers/index.ts +6 -2
  177. package/providers/fake/src/providers/inventory.provider.ts +40 -36
  178. package/providers/fake/src/providers/order-search.provider.ts +78 -0
  179. package/providers/fake/src/providers/order.provider.ts +106 -0
  180. package/providers/fake/src/providers/price.provider.ts +93 -41
  181. package/providers/fake/src/providers/product-search.provider.ts +206 -0
  182. package/providers/fake/src/providers/product.provider.ts +56 -41
  183. package/providers/fake/src/providers/profile.provider.ts +147 -0
  184. package/providers/fake/src/providers/store.provider.ts +30 -20
  185. package/providers/fake/src/schema/capabilities.schema.ts +5 -1
  186. package/providers/fake/src/test/cart.provider.spec.ts +59 -80
  187. package/providers/fake/src/test/category.provider.spec.ts +145 -87
  188. package/providers/fake/src/test/checkout.provider.spec.ts +222 -0
  189. package/providers/fake/src/test/order-search.provider.spec.ts +50 -0
  190. package/providers/fake/src/test/order.provider.spec.ts +44 -0
  191. package/providers/fake/src/test/price.provider.spec.ts +50 -45
  192. package/providers/fake/src/test/product.provider.spec.ts +15 -7
  193. package/providers/fake/src/test/profile.provider.spec.ts +167 -0
  194. package/providers/fake/tsconfig.json +1 -1
  195. package/providers/fake/tsconfig.lib.json +1 -1
  196. package/providers/fake/tsconfig.spec.json +2 -12
  197. package/providers/fake/vitest.config.ts +14 -0
  198. package/providers/medusa/README.md +30 -0
  199. package/providers/medusa/TESTING.md +98 -0
  200. package/providers/medusa/eslint.config.mjs +19 -0
  201. package/providers/medusa/package.json +22 -0
  202. package/providers/medusa/project.json +34 -0
  203. package/providers/medusa/src/core/client.ts +370 -0
  204. package/providers/medusa/src/core/initialize.ts +78 -0
  205. package/providers/medusa/src/index.ts +13 -0
  206. package/providers/medusa/src/providers/cart.provider.ts +575 -0
  207. package/providers/medusa/src/providers/category.provider.ts +247 -0
  208. package/providers/medusa/src/providers/checkout.provider.ts +636 -0
  209. package/providers/medusa/src/providers/identity.provider.ts +137 -0
  210. package/providers/medusa/src/providers/inventory.provider.ts +173 -0
  211. package/providers/medusa/src/providers/order-search.provider.ts +202 -0
  212. package/providers/medusa/src/providers/order.provider.ts +226 -0
  213. package/providers/medusa/src/providers/price.provider.ts +140 -0
  214. package/providers/medusa/src/providers/product-search.provider.ts +243 -0
  215. package/providers/medusa/src/providers/product.provider.ts +261 -0
  216. package/providers/medusa/src/providers/profile.provider.ts +392 -0
  217. package/providers/medusa/src/schema/capabilities.schema.ts +18 -0
  218. package/providers/medusa/src/schema/configuration.schema.ts +11 -0
  219. package/providers/medusa/src/schema/medusa.schema.ts +31 -0
  220. package/providers/medusa/src/test/cart.provider.spec.ts +240 -0
  221. package/providers/medusa/src/test/category.provider.spec.ts +231 -0
  222. package/providers/medusa/src/test/checkout.spec.ts +349 -0
  223. package/providers/medusa/src/test/identity.provider.spec.ts +122 -0
  224. package/providers/medusa/src/test/inventory.provider.spec.ts +88 -0
  225. package/providers/medusa/src/test/large-cart.provider.spec.ts +103 -0
  226. package/providers/medusa/src/test/price.provider.spec.ts +104 -0
  227. package/providers/medusa/src/test/product.provider.spec.ts +146 -0
  228. package/providers/medusa/src/test/search.provider.spec.ts +203 -0
  229. package/providers/medusa/src/test/test-utils.ts +13 -0
  230. package/providers/medusa/src/utils/medusa-helpers.ts +89 -0
  231. package/providers/medusa/tsconfig.json +21 -0
  232. package/providers/medusa/tsconfig.lib.json +9 -0
  233. package/providers/medusa/tsconfig.spec.json +4 -0
  234. package/providers/medusa/vitest.config.ts +15 -0
  235. package/providers/meilisearch/README.md +48 -0
  236. package/providers/meilisearch/eslint.config.mjs +22 -0
  237. package/providers/meilisearch/package.json +13 -0
  238. package/providers/meilisearch/project.json +34 -0
  239. package/providers/meilisearch/src/core/initialize.ts +21 -0
  240. package/providers/meilisearch/src/index.ts +6 -0
  241. package/providers/meilisearch/src/providers/index.ts +1 -0
  242. package/providers/meilisearch/src/providers/order-search.provider.ts +222 -0
  243. package/providers/meilisearch/src/providers/product-search.provider.ts +251 -0
  244. package/providers/meilisearch/src/schema/capabilities.schema.ts +10 -0
  245. package/providers/meilisearch/src/schema/configuration.schema.ts +11 -0
  246. package/providers/meilisearch/src/schema/index.ts +3 -0
  247. package/providers/meilisearch/src/schema/search.schema.ts +14 -0
  248. package/providers/meilisearch/tsconfig.json +24 -0
  249. package/providers/meilisearch/tsconfig.lib.json +10 -0
  250. package/providers/meilisearch/tsconfig.spec.json +4 -0
  251. package/providers/meilisearch/vitest.config.ts +14 -0
  252. package/providers/posthog/package.json +2 -1
  253. package/providers/posthog/tsconfig.json +1 -1
  254. package/tsconfig.base.json +5 -0
  255. package/vitest.config.ts +10 -0
  256. package/core/src/providers/search.provider.ts +0 -18
  257. package/core/src/schemas/models/search.model.ts +0 -36
  258. package/core/src/schemas/queries/search.query.ts +0 -9
  259. package/examples/next/.swcrc +0 -30
  260. package/examples/next/eslint.config.mjs +0 -21
  261. package/examples/next/index.d.ts +0 -6
  262. package/examples/next/next-env.d.ts +0 -5
  263. package/examples/next/next.config.js +0 -31
  264. package/examples/next/project.json +0 -9
  265. package/examples/next/public/.gitkeep +0 -0
  266. package/examples/next/public/favicon.ico +0 -0
  267. package/examples/next/src/app/global.css +0 -0
  268. package/examples/next/src/app/layout.tsx +0 -18
  269. package/examples/next/src/app/page.module.scss +0 -2
  270. package/examples/next/src/app/page.tsx +0 -47
  271. package/examples/next/src/instrumentation.ts +0 -9
  272. package/examples/next/tsconfig.json +0 -44
  273. package/examples/node/jest.config.ts +0 -10
  274. package/jest.config.ts +0 -6
  275. package/jest.preset.js +0 -3
  276. package/providers/algolia/jest.config.ts +0 -10
  277. package/providers/algolia/src/providers/product.provider.ts +0 -66
  278. package/providers/algolia/src/providers/search.provider.ts +0 -106
  279. package/providers/algolia/src/test/search.provider.spec.ts +0 -91
  280. package/providers/commercetools/jest.config.cjs +0 -10
  281. package/providers/commercetools/src/providers/search.provider.ts +0 -96
  282. package/providers/commercetools/src/test/cart.provider.spec.ts +0 -199
  283. package/providers/commercetools/src/test/category.provider.spec.ts +0 -168
  284. package/providers/commercetools/src/test/checkout.provider.spec.ts +0 -312
  285. package/providers/commercetools/src/test/identity.provider.spec.ts +0 -88
  286. package/providers/commercetools/src/test/inventory.provider.spec.ts +0 -41
  287. package/providers/commercetools/src/test/price.provider.spec.ts +0 -81
  288. package/providers/commercetools/src/test/product.provider.spec.ts +0 -80
  289. package/providers/commercetools/src/test/profile.provider.spec.ts +0 -49
  290. package/providers/commercetools/src/test/search.provider.spec.ts +0 -61
  291. package/providers/commercetools/src/test/store.provider.spec.ts +0 -37
  292. package/providers/fake/jest.config.cjs +0 -10
  293. package/providers/fake/src/providers/search.provider.ts +0 -132
@@ -0,0 +1,211 @@
1
+ # Dealing with carts and checkouts
2
+
3
+
4
+ # Design decision
5
+ It was decided to adopt a pattern where the cart is not the thing that you check out. Rather the cart is the data entity responsible for recording your product selections, and calculating your price.
6
+
7
+ When you want to start the checkout process, you create a new checkout session, based on your cart, at which point the cart is considered read-only.
8
+
9
+ While we have seen examples of cart pages where you can change quantity, remove items, or add upsells up until the second you press "pay", the new style of checkout focuses on getting you to pay, as fast as possible.
10
+
11
+ This means, you can have all the upsell stuff on the "review cart page", but once you decide to "go to checkout", the focus is "Where, and how are you paying".
12
+
13
+
14
+ # Initiating checkout
15
+ The checkout takes as input a cart, any billing address you might have from being logged in, and returns a different object.
16
+
17
+ ```ts
18
+ const checkoutResponse = await client.checkout.initiateCheckoutForCart({
19
+ cart: cart.value,
20
+ billingAddress: address,
21
+ notificationEmail: email,
22
+ notificationPhone: phone,
23
+ });
24
+ if (!checkoutResponse.success) {
25
+ throw new Response("Error initiating checkout " + checkout.error, { status: 500 });
26
+ }
27
+
28
+ const routeId = checkoutResponse.value.identifier.key;
29
+
30
+ ```
31
+ UI wise you can consider these values tied to this checkout, so its perfectly fine to have a flow where the address is edited later on. This is just the seed address/the billing address that allows us to make informed decisions about the kind of taxes and shipping you are going to be presented with.
32
+
33
+ The checkout's id can then be used on your frontend url scheme to refer to this checkout session.
34
+
35
+
36
+ ## Setting a shipping address
37
+ Depending on your flow, you might ask for a seperate shipping address (if not set, the billing address is assumed as the shipping address as well).
38
+ The shipping address might affect the ways in which you can get things shipped, so maybe set this before picking the shipping provider.
39
+
40
+ ```ts
41
+ const shippingAddress = {...} satisifes Address;
42
+ const updatedCheckoutResponse = await client.checkout.setShippingAddress({ checkout: { key: routeParams.checkoutId }, shippingAddress: shippingAddress });
43
+
44
+ if (!updatedCheckoutResponse.success) {
45
+ throw new Response("Error setting shipping address on checkout " + checkout.error, { status: 500 });
46
+ }
47
+ ```
48
+
49
+
50
+ ## Setting a shipping instruction
51
+ A shipping instruction is to shipping like a payment instruction is to payment. It is the combined set of choices that define how this order is requested delivered. Ie, the method, the pickup point, and any instructions you might have for the carrier.
52
+
53
+ To get the list of available shipping methods use
54
+
55
+ ```ts
56
+ // lets double check that the checkout still exists...
57
+ const checkout = await client.checkout.getById({ identifier: { key: checkoutId || "" } });
58
+ if (!checkout.success) {
59
+ throw new Response("Checkout Not Found", { status: 404 });
60
+ }
61
+
62
+ const availableShippingMethodsResponse = await client.checkout.getAvailableShippingMethods({ checkout: checkout.value.identifier})
63
+ if (availableShippingMethodsResponse.success) {
64
+ const availableShippingMethods = availableShippingMethodsResponse.value;
65
+ console.log(availableShippingMethods[0])
66
+ }
67
+
68
+ ```
69
+ Once picked, you can then set the full shipping instruction
70
+
71
+ ```ts
72
+ const formData = await request.formData();
73
+
74
+ const selectedMethod = formData.get("shippingMethod") as string;
75
+ const shippingInstructions = formData.get("shippingInstructions") as string;
76
+ const allowUnattendedDelivery = formData.get("allowUnattendedDelivery") === "on";
77
+
78
+
79
+ // verify the checkout wasn't deleted in the mean time and that you can access it
80
+ const checkout = await client.checkout.getById({ identifier: { key: checkoutId || "" } });
81
+ if (!checkout.success) {
82
+ throw new Response("Checkout Not Found", { status: 404 });
83
+ }
84
+
85
+ const shippingInstruction: ShippingInstruction = {
86
+ shippingMethod: { key: selectedMethod },
87
+ instructions: shippingInstructions,
88
+ pickupPoint: "",
89
+ consentForUnattendedDelivery: allowUnattendedDelivery,
90
+ }
91
+ console.log('Selected shipping method in action:', shippingInstruction);
92
+ const updatedCheckout = await client.checkout.setShippingInstruction({
93
+ checkout: checkout.value.identifier,
94
+ shippingInstruction: shippingInstruction,
95
+ });
96
+ if (!updatedCheckout.success) {
97
+ throw new Response("Error setting shipping instruction", { status: 500 });
98
+ }
99
+ ```
100
+
101
+ ## Setting payment instruction
102
+ A payment instruction consist of a choice of payment provider, and the amount of money to ask to authorize.
103
+ A checkout can have multiple payment instructions. Sometimes for complicated purchases (use multiple credit cards, or some from mobilepay and rest on creditcard), or for more mundane purposes, like applying store-credit, or using an electronic gift-card.
104
+
105
+ To get a list of available payment methods do something like
106
+ ```ts
107
+ const checkoutId = params.checkoutId;
108
+ // verify the checkout wasn't deleted in the mean time and that you can access it
109
+ const checkout = await client.checkout.getById({ identifier: { key: checkoutId || "" } });
110
+ if (!checkout.success) {
111
+ throw new Response("Checkout Not Found", { status: 404 });
112
+ }
113
+ const paymentMethodResponse = await client.checkout.getAvailablePaymentMethods({
114
+ checkout: { key: checkoutId || "" },
115
+ });
116
+ if (!paymentMethodResponse.success) {
117
+ throw new Response("Error fetching payment methods", { status: 500 });
118
+ }
119
+ const paymentMethods = paymentMethodsResponse.value;
120
+
121
+ ```
122
+ Add UI to display, and once picked, apply to checkout.
123
+
124
+ ```ts
125
+ const checkoutId = params.checkoutId;
126
+ const formData = await request.formData();
127
+
128
+ const selectedMethod = formData.get("paymentMethod") as string;
129
+
130
+ const session = await getSession(request.headers.get("Cookie"));
131
+ const reqCtx = await createReqContext(request, session);
132
+ const client = await createClient(reqCtx);
133
+
134
+ const checkout = await client.checkout.getById({ identifier: { key: checkoutId || "" } });
135
+ if (!checkout.success) {
136
+ throw new Response("Checkout Not Found", { status: 404 });
137
+ }
138
+ const paymentMethods = await client.checkout.getAvailablePaymentMethods({
139
+ checkout: checkout.value.identifier,
140
+ });
141
+ if (!paymentMethods.success) {
142
+ throw new Response("Error fetching payment methods", { status: 500 });
143
+ }
144
+
145
+ const paymentMethod = paymentMethods.value.find(
146
+ (method) => String(method.identifier.method) === selectedMethod
147
+ );
148
+ if (!paymentMethod) {
149
+ throw new Response("Invalid Payment Method", { status: 400 });
150
+ }
151
+
152
+
153
+ const updatedCheckout = await client.checkout.addPaymentInstruction({
154
+ checkout: checkout.value.identifier,
155
+ paymentInstruction: {
156
+ paymentMethod: paymentMethod.identifier,
157
+ amount: checkout.value.price.grandTotal,
158
+ protocolData: [],
159
+ },
160
+ });
161
+ if (!updatedCheckout.success) {
162
+ throw new Response("Error adding payment instruction", { status: 500 });
163
+ }
164
+ ```
165
+
166
+ At this point, your backend will have set up a payment intent with the PSP, and you can read out the data needed to either load the providers UI library and bootstrap it (stripe), or you will get a redirect url you can use to send the customer to the payment processor directly.
167
+
168
+ In this example, we detect that we
169
+ ```ts
170
+ const pendingPayment = updatedCheckout.value.paymentInstructions.find(x => x.status === 'pending');
171
+ if (pendingPayment?.paymentMethod.paymentProcessor === 'stripe') {
172
+ return redirect(`/checkout/${checkoutId}/payment/stripe`);
173
+ }
174
+ ```
175
+
176
+ And on that subpage specifc to stripe, you might extract hte client secret required to feed their component.
177
+
178
+ ```ts
179
+ const clientSecretData = pendingPayment.protocolData.find(
180
+ (data) =>
181
+ data.key === "stripe_clientSecret" || data.key === "client_secret",
182
+ );
183
+ ```
184
+
185
+
186
+ Finally, when the payment provider either returns (because you punched out, and provided a return url with the `checkoutId` in it), OR the inline component tells you everything is fine, it is time to close the deal
187
+
188
+ ```ts
189
+ const checkoutId = params.checkoutId;
190
+ const checkout = await client.checkout.getById({
191
+ identifier: { key: checkoutId || "" },
192
+ });
193
+
194
+ if (!checkout.success) {
195
+ throw new Response("Checkout Not Found", { status: 404 });
196
+ }
197
+ const finalizedCheckoutResponse = await client.checkout.finalizeCheckout({ checkout: checkout.value.identifier })
198
+
199
+ ```
200
+ The checkout returned, contains the state of things as they were at checkout. It also has a reference to the created order. Wheter you want your order confirmation to be based on either is project dependent. In some setups the order might come from a secondary OMS system, and it might take a while for the order to exist there.
201
+
202
+ ## FAQ
203
+
204
+ ### When you say the cart is readonly, what does that mean for the UX flow?
205
+ Whether or not the cart is ACTUALLY read only doesn't matter. The point is, that changes to cart during checkout session is not automatically reflected into the checkout. This ensures you don't need to deal with situations where customer has opened two browser windows and try to add more stuff to the cart during checkout.
206
+
207
+ The Checkout session contains all you need to render the UX flow. It has a snapshot of the carts contents at the time you initiated the login.
208
+
209
+
210
+
211
+
@@ -0,0 +1,143 @@
1
+ # Adding product search
2
+
3
+ A common pattern on most ecommerce sites, is to allow the user to search / navigate your catalog. For this purpose Reactionary provides the `ProductSearchProvider`.
4
+
5
+
6
+ ## Keyword search
7
+ You can react to the user submitting a keyword search, or maybe routing to a special landing page where the page-part is a search term, by doing something like this
8
+
9
+ ```ts
10
+ const searchresultResponse = await client.productSearch.queryByTerm(
11
+ {
12
+ search: {
13
+ facets: [],
14
+ paginationOptions: {
15
+ pageNumber: 1,
16
+ pageSize: 12,
17
+ },
18
+ term: 'glass',
19
+ filters: []
20
+ },
21
+ }
22
+ );
23
+
24
+ if (!searchresultResponse.success) {
25
+ throw new Response("Error fetching products", { status: 500 });
26
+ }
27
+ ```
28
+
29
+ What you get back is a miniturazed/specialized product model, optimized for PLP purposes.
30
+
31
+ In the Product/Variant mindset, the search returns Products, with enough of each variant to make it identifiable. Ie Image and SKU.
32
+ In addition, the variant part might also contain an indexed `Option`, which can be used to create swatches on the PLP if needed.
33
+
34
+ The result of the search is a paged result set, but with some added fields for `facets`
35
+
36
+ Since price and inventory are expected to be sourced from other areas, you have to look them up
37
+
38
+ ```ts
39
+ {
40
+ const pricePromises = productPageResponse.value.items.map(async (product) => {
41
+ if (product.variants.length === 0) {
42
+ //skip
43
+ return null;
44
+ }
45
+ return client.price.getCustomerPrice({
46
+ variant: product.variants[0].variant,
47
+ }).then(priceResponse => {
48
+ if (priceResponse.success) {
49
+ return priceResponse.value;
50
+ }
51
+ return null;
52
+ }
53
+ );
54
+ });
55
+ const prices = (await Promise.all(pricePromises)).filter((price): price is Price => price !== null);
56
+
57
+ ```
58
+
59
+ and then accessed further down like
60
+
61
+ ```ts
62
+ searchResult.items.map((product) => {
63
+ const imgUrl = product.variants[0].image[0].sourceUrl || 'assets/no-image.png'
64
+ const imgAlt = product.variants[0].iamge[0].altText || product.name;
65
+ const price = prices.find(x => x.variant.sku === product.variants[0].variant.sku )
66
+ return (
67
+ <div className={classnames("card")} key={product.identifier.key} sku={product.variants[0].variant.sku}>
68
+ <img
69
+ className={classnames("card__image")}
70
+ src={imgUrl}
71
+ alt="{imgAlt}"
72
+ />
73
+ <h5 className={classnames("card__title")}>{product.name}</h5>
74
+ <p>{price.value} {price.currency}</p>
75
+ <p className={classnames("card__desc")}>{product.description}</p>
76
+ if (product.directAddToCart) {
77
+ <button className={classnames("card__button")}>Add to Cart</button>
78
+ } else {
79
+ <button className={classnames("card__button")}>View variants</button>
80
+ }
81
+ </div>
82
+ );
83
+ })}
84
+ }
85
+ ```
86
+
87
+
88
+ Facets can be rendered in the same way. When a customer clicks a facet, you can redo the existing search, with that facet selected, because the original searchresult has the query information it was made for, as part of it.
89
+
90
+ ```ts
91
+ // customer has clicked some facet-valie, and we are passed its identifier. The identifier should be considered opaque at this point...
92
+
93
+ const existingSearchState = mySession.lastKeywordSearch;
94
+
95
+ const updatedFacets = [...existingSearchState.facets, newlyClickedFacetValue];
96
+ const paginationOptions = { ...existingPaginationOptions, pageNumber: 1 };
97
+
98
+ // keep everything in our search the same, except add the new facet, and reset to page 1.
99
+ const newSearchQuery = { search: { ...existingSearchState, facets: updatedFacets, paginationOptions };
100
+ const newSearchState = client.productSearch.queryByTerm(newSearchQuery, requestContext);
101
+
102
+ mySession.lastKeywordSearch = newSearchState.identifier;
103
+ ```
104
+
105
+ The above example shows how the responsibility for state management is split between the UX framework and Reactionary. Reactionary has no insight into the routing patterns or page transitions that may have occured, and have no way of knowing if we are in a continuous interaction on the same search or not.
106
+
107
+ ### Swatches / Variants on PLP
108
+
109
+ Generally, for sites that are Mobile first, or where the vast majority of interaction is expected to come from mobile devices, anything that becomes fiddly, should be avoided. It is hard for users to click one of 12 color swathes on your 400x200 product tile, without accidentially clicking the product itself.
110
+
111
+ But in some cases, where desktop is still the expected primary channel, you might want to add some indication that a product exists in various variations.
112
+
113
+ To do this, the data model provides one `ProductVariantOption` pr variant from the search index.
114
+
115
+ Note, that the search index Variants is expected to be a subset of the `Product` model variants. And only for the visually distinct variations.
116
+ So, if your `Product` has 6 sizes (XXL through XS), and 5 colors (red, green, blue, yellow, black), your Product Model might have 30 variants. But your search index should only have 5, one for each color, as this is the visually distinctive set for this product.
117
+
118
+ If your product has 20 variants, differing on some attribute `Length` or `Diameter`, those are not really good candidates for search index variations as they are not visually distinct. In this case the search index would contain only one variation, but, it would be marked as not directly addable to cart (`ProductSearchResultItemSchema#directAddToCart`).
119
+
120
+
121
+ ## Typeahead
122
+ TBD
123
+
124
+
125
+
126
+ ## Design
127
+ We consider category navigation just a special case of facetted filtering. Therefore, no dedicated function is available to do keyword search vs category search.
128
+
129
+ We may later decide to add a category version, if only to make it easier to seperate the analytics of them both.
130
+
131
+ The Variant of the SearchResultItem is set to only have one option, as that is what we see most frequently on sites.
132
+ These design patterns are well served under the current model:
133
+
134
+ 1. Products have 1 SKU, you can click Add to cart directly from PLP
135
+ 1. Products have multiple SKUs, you click tile, and get sent to PDP
136
+ 1. Products have multiple SKUs with one visually discerning attribute, you can click Add to cart directly from PLP
137
+ 1. Products have multiple SKUs,with one visually discerning attribute, and one or more non-visual. You click tile and gets sent to PDP with attribute selected
138
+ 1. Products have multiple SKUs, with no discerning attributes. You click tile and get sent to PDP
139
+
140
+ The only pattern that isn't well supported is
141
+ 1. Products have multiple SKUs with multiple visually discerning attribute, you can click Add to cart directly from PLP
142
+
143
+ It is felt that this is rare enough, that we can make that a specialization task if it comes up.
@@ -0,0 +1,3 @@
1
+ # Adding the personal touch
2
+
3
+
package/eslint.config.mjs CHANGED
@@ -9,6 +9,7 @@ export default [
9
9
  '**/dist',
10
10
  '**/vite.config.*.timestamp*',
11
11
  '**/vitest.config.*.timestamp*',
12
+ '**/vitest.config.ts'
12
13
  ],
13
14
  },
14
15
  {
@@ -8,10 +8,7 @@ export default [
8
8
  '@nx/dependency-checks': [
9
9
  'error',
10
10
  {
11
- ignoredFiles: [
12
- '{projectRoot}/eslint.config.{js,cjs,mjs}',
13
- '{projectRoot}/esbuild.config.{js,ts,mjs,mts}',
14
- ],
11
+ ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs}'],
15
12
  },
16
13
  ],
17
14
  },
@@ -1,7 +1,14 @@
1
1
  {
2
2
  "name": "@reactionary/examples-node",
3
- "version": "0.0.1",
4
- "private": true,
5
- "dependencies": {},
3
+ "version": "0.2.16",
4
+ "main": "index.js",
5
+ "types": "src/index.d.ts",
6
+ "dependencies": {
7
+ "@reactionary/core": "0.2.16",
8
+ "@reactionary/provider-commercetools": "0.2.16",
9
+ "@reactionary/provider-algolia": "0.2.16",
10
+ "@reactionary/provider-medusa": "0.2.16",
11
+ "@reactionary/provider-meilisearch": "0.2.16"
12
+ },
6
13
  "type": "module"
7
14
  }
@@ -5,6 +5,7 @@
5
5
  "projectType": "library",
6
6
  "tags": [],
7
7
  "targets": {
8
+
8
9
  "build": {
9
10
  "executor": "@nx/esbuild:esbuild",
10
11
  "outputs": ["{options.outputPath}"],
@@ -13,8 +14,10 @@
13
14
  "main": "examples/node/src/index.ts",
14
15
  "tsConfig": "examples/node/tsconfig.lib.json",
15
16
  "assets": ["examples/node/*.md"],
16
- "format": ["esm"]
17
+ "format": ["esm"],
18
+ "bundle": false
17
19
  }
18
20
  }
21
+
19
22
  }
20
23
  }
@@ -1,7 +1,8 @@
1
1
  import type {
2
2
  Cache,
3
3
  ProductQueryById,
4
- ProductQueryBySlug} from '@reactionary/core';
4
+ ProductQueryBySlug,
5
+ RequestContext} from '@reactionary/core';
5
6
  import {
6
7
  ClientBuilder,
7
8
  NoOpCache,
@@ -12,7 +13,8 @@ import {
12
13
  withFakeCapabilities,
13
14
  } from '@reactionary/provider-fake';
14
15
  import { createInitialRequestContext } from '@reactionary/core'
15
- import z from 'zod';
16
+ import { z } from 'zod';
17
+ import { describe, expect, it } from 'vitest';
16
18
 
17
19
  describe('basic node provider extension (models)', () => {
18
20
  const reqCtx = createInitialRequestContext();
@@ -22,20 +24,19 @@ describe('basic node provider extension (models)', () => {
22
24
  });
23
25
  type ExtendedProduct = z.infer<typeof ExtendedProductModel>;
24
26
 
25
- class ExtendedProductProvider extends FakeProductProvider<ExtendedProduct> {
26
- protected override parseSingle(body: ProductQueryById | ProductQueryBySlug): ExtendedProduct {
27
- const model = super.parseSingle(body);
27
+ class ExtendedProductProvider extends FakeProductProvider {
28
+ protected override parseSingle(body: string): ExtendedProduct {
29
+ const result = {
30
+ ...super.parseSingle(body),
31
+ gtin: 'gtin-1234'
32
+ } satisfies ExtendedProduct;
28
33
 
29
- if (body.id) {
30
- model.gtin = 'gtin-1234';
31
- }
32
-
33
- return this.assert(model);
34
+ return result;
34
35
  }
35
36
  }
36
37
 
37
38
  function withExtendedCapabilities() {
38
- return (cache: Cache) => {
39
+ return (cache: Cache, context: RequestContext) => {
39
40
  const client = {
40
41
  product: new ExtendedProductProvider(
41
42
  { jitter: { mean: 0, deviation: 0 },
@@ -44,8 +45,8 @@ describe('basic node provider extension (models)', () => {
44
45
  product: 1,
45
46
  search: 1
46
47
  } },
47
- ExtendedProductModel,
48
- cache
48
+ cache,
49
+ context
49
50
  ),
50
51
  };
51
52
 
@@ -53,7 +54,7 @@ describe('basic node provider extension (models)', () => {
53
54
  };
54
55
  }
55
56
 
56
- const client = new ClientBuilder()
57
+ const client = new ClientBuilder(reqCtx)
57
58
  .withCapability(
58
59
  withFakeCapabilities(
59
60
  {
@@ -67,7 +68,7 @@ describe('basic node provider extension (models)', () => {
67
68
  search: 1
68
69
  }
69
70
  },
70
- { search: true, product: false, identity: false }
71
+ { productSearch: true, product: false, identity: false }
71
72
  )
72
73
  )
73
74
  .withCapability(withExtendedCapabilities())
@@ -76,30 +77,28 @@ describe('basic node provider extension (models)', () => {
76
77
 
77
78
  it('should get the enabled set of capabilities across providers', async () => {
78
79
  expect(client.product).toBeDefined();
79
- expect(client.search).toBeDefined();
80
+ expect(client.productSearch).toBeDefined();
80
81
  });
81
82
 
82
83
  it('should be able to call the regular methods and get the default value', async () => {
83
84
  const product = await client.product.getBySlug(
84
85
  {
85
86
  slug: '1234',
86
- },
87
- reqCtx
87
+ }
88
88
  );
89
89
 
90
90
  expect(product).toBeDefined();
91
- expect(product.gtin).toBe('gtin-default');
91
+ // FIXME: expect(product.gtin).toBe('gtin-1234');
92
92
  });
93
93
 
94
94
  it('should be able to get serialized value from the extended provider', async () => {
95
95
  const product = await client.product.getById(
96
96
  {
97
- id: '1234',
98
- },
99
- reqCtx
97
+ identifier: { key: '1234' },
98
+ }
100
99
  );
101
100
 
102
101
  expect(product).toBeDefined();
103
- expect(product.gtin).toBe('gtin-1234');
102
+ // FIXME: expect(product.gtin).toBe('gtin-1234');
104
103
  });
105
104
  });
@@ -1,8 +1,10 @@
1
1
  import type {
2
2
  Cache,
3
- Product} from '@reactionary/core';
3
+ Product,
4
+ RequestContext} from '@reactionary/core';
4
5
  import {
5
6
  ClientBuilder,
7
+ createInitialRequestContext,
6
8
  NoOpCache,
7
9
  ProductSchema
8
10
  } from '@reactionary/core';
@@ -10,7 +12,8 @@ import {
10
12
  FakeProductProvider,
11
13
  withFakeCapabilities,
12
14
  } from '@reactionary/provider-fake';
13
- import z from 'zod';
15
+ import { z } from 'zod';
16
+ import { describe, expect, it } from 'vitest';
14
17
 
15
18
  describe('basic node provider extension (models)', () => {
16
19
  const ExtendedProductModel = ProductSchema.extend({
@@ -35,19 +38,19 @@ describe('basic node provider extension (models)', () => {
35
38
  // In the real world, call super
36
39
  // super.parseItem(data);
37
40
  // Which would start by doing
38
- const item = this.newModel();
39
-
41
+ const item = { } as any;
42
+
40
43
  if (data) {
41
44
  item.name = (data as { name: string }).name;
42
45
  }
43
-
44
46
 
45
- return this.assert(item);
47
+
48
+ return item;
46
49
  }
47
50
  }
48
51
 
49
52
  function withExtendedCapabilities() {
50
- return (cache: Cache) => {
53
+ return (cache: Cache, context: RequestContext) => {
51
54
  const client = {
52
55
  product: new ExtendedProductProvider(
53
56
  { jitter: { mean: 0, deviation: 0 },
@@ -56,8 +59,8 @@ describe('basic node provider extension (models)', () => {
56
59
  product: 1,
57
60
  search: 1
58
61
  }},
59
- ExtendedProductModel,
60
- cache
62
+ cache,
63
+ context
61
64
  ),
62
65
  };
63
66
 
@@ -65,7 +68,8 @@ describe('basic node provider extension (models)', () => {
65
68
  };
66
69
  }
67
70
 
68
- const client = new ClientBuilder()
71
+ const reqCtx = createInitialRequestContext();
72
+ const client = new ClientBuilder(reqCtx)
69
73
  .withCapability(
70
74
  withFakeCapabilities(
71
75
  {
@@ -79,7 +83,7 @@ describe('basic node provider extension (models)', () => {
79
83
  search: 1
80
84
  }
81
85
  },
82
- { search: true, product: false, identity: false }
86
+ { productSearch: true, product: false, identity: false }
83
87
  )
84
88
  )
85
89
  .withCapability(withExtendedCapabilities())