@ordergroove/offers 2.44.1-alpha-PR-1167-2.36 → 2.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ordergroove/offers",
3
- "version": "2.44.1-alpha-PR-1167-2.36+cf24ab29",
3
+ "version": "2.45.0",
4
4
  "description": "offer state component",
5
5
  "author": "Eugenio Lattanzio <eugenio63@gmail.com>",
6
6
  "homepage": "https://github.com/ordergroove/plush-toys#readme",
@@ -49,5 +49,5 @@
49
49
  "@ordergroove/offers-templates": "^0.9.7",
50
50
  "@types/lodash.memoize": "^4.1.9"
51
51
  },
52
- "gitHead": "cf24ab29685dd3a20acce3d0c6ce8eea63c073f8"
52
+ "gitHead": "221fe44cdecde025a85ff117df9639fb5068dfd5"
53
53
  }
@@ -4,7 +4,8 @@ import {
4
4
  isSameProduct,
5
5
  templatesSelector,
6
6
  makeProductSpecificDefaultFrequencySelector,
7
- makeProductPrepaidShipmentOptionsSelector
7
+ makeProductPrepaidShipmentOptionsSelector,
8
+ makeProductFrequenciesSelector
8
9
  } from '../selectors';
9
10
  import { stringifyFrequency } from '../api';
10
11
 
@@ -141,3 +142,92 @@ describe('makeProductPrepaidShipmentOptionsSelector', () => {
141
142
  ).toEqual([3, 6, 12]);
142
143
  });
143
144
  });
145
+
146
+ describe('makeProductFrequenciesSelector', () => {
147
+ const productFrequencies = {
148
+ 123: {
149
+ frequencies: ['default-group-plan-1', 'default-group-plan-2', 'default-group-plan-3'],
150
+ frequenciesEveryPeriod: ['1_3', '2_3', '3_3'],
151
+ frequenciesText: ['month', '2 months', '3 months'],
152
+ defaultFrequency: 'default-group-plan-1'
153
+ },
154
+ 456: {
155
+ frequencies: ['other-group-plan-1', 'other-group-plan-2', 'other-group-plan-3'],
156
+ frequenciesEveryPeriod: ['2_3', '4_3', '6_3'],
157
+ frequenciesText: ['2 months', '4 months', '6 months'],
158
+ defaultFrequency: 'other-group-plan-1'
159
+ }
160
+ };
161
+ // note: in real life, these "global" frequencies are likely the same as the one of the sets in productFrequencies
162
+ // for unit testing, they are different so it is obvious which set of fields was returned
163
+ const frequencies = ['fallback-group-plan-1', 'fallback-group-plan-2', 'fallback-group-plan-3'];
164
+ const frequenciesEveryPeriod = ['1_2', '2_2', '3_2'];
165
+ const frequenciesText = ['week', '2 weeks', '3 weeks'];
166
+ const defaultFrequency = 'fallback-group-plan-1';
167
+ const config = { productFrequencies, frequencies, frequenciesEveryPeriod, frequenciesText, defaultFrequency };
168
+
169
+ it('returns product frequencies for product ID', () => {
170
+ const selectorProduct1 = makeProductFrequenciesSelector(123);
171
+ expect(
172
+ selectorProduct1({
173
+ config
174
+ })
175
+ ).toEqual({
176
+ frequencies: ['default-group-plan-1', 'default-group-plan-2', 'default-group-plan-3'],
177
+ frequenciesEveryPeriod: ['1_3', '2_3', '3_3'],
178
+ frequenciesText: ['month', '2 months', '3 months'],
179
+ defaultFrequency: 'default-group-plan-1'
180
+ });
181
+
182
+ const selectorProduct2 = makeProductFrequenciesSelector(456);
183
+ expect(selectorProduct2({ config })).toEqual({
184
+ frequencies: ['other-group-plan-1', 'other-group-plan-2', 'other-group-plan-3'],
185
+ frequenciesEveryPeriod: ['2_3', '4_3', '6_3'],
186
+ frequenciesText: ['2 months', '4 months', '6 months'],
187
+ defaultFrequency: 'other-group-plan-1'
188
+ });
189
+
190
+ // product ID does not exist
191
+ const selectorProduct3 = makeProductFrequenciesSelector(789);
192
+ expect(selectorProduct3({ config })).toEqual({});
193
+ });
194
+
195
+ it('falls back to old frequency fields if productFrequencies is not defined', () => {
196
+ const selector = makeProductFrequenciesSelector(123);
197
+ expect(
198
+ selector({
199
+ config: {
200
+ ...config,
201
+ productFrequencies: null
202
+ }
203
+ })
204
+ ).toEqual({
205
+ frequencies,
206
+ frequenciesEveryPeriod,
207
+ frequenciesText,
208
+ defaultFrequency
209
+ });
210
+ });
211
+
212
+ it('does not use old frequency fields if productFrequencies is defined but empty', () => {
213
+ const selector = makeProductFrequenciesSelector(123);
214
+ expect(
215
+ selector({
216
+ config: {
217
+ ...config,
218
+ productFrequencies: {}
219
+ }
220
+ })
221
+ ).toEqual({});
222
+ });
223
+
224
+ it('returns object with undefined fields when everything is undefined', () => {
225
+ const selector = makeProductFrequenciesSelector(123);
226
+ expect(selector({ config: {} })).toEqual({
227
+ frequencies: undefined,
228
+ frequenciesEveryPeriod: undefined,
229
+ frequenciesText: undefined,
230
+ defaultFrequency: undefined
231
+ });
232
+ });
233
+ });
@@ -189,6 +189,7 @@ export const requestSessionId = () => (dispatch, getState) => {
189
189
  };
190
190
 
191
191
  export const receiveOffer = (response, offer, productId) => (dispatch, getState) => {
192
+ // this is a thunk so that we access the state for the selector
192
193
  const frequencyConfig = makeProductFrequenciesSelector(productId)(getState());
193
194
  dispatch({
194
195
  type: constants.RECEIVE_OFFER,
@@ -419,6 +419,7 @@ export const config = (
419
419
  return {
420
420
  ...state,
421
421
  ...action.payload,
422
+ // these are not populated by default; only if the merchant calls the config method on the Offers API
422
423
  defaultFrequency: action.payload.defaultFrequency
423
424
  ? stringifyFrequency(action.payload.defaultFrequency)
424
425
  : state.defaultFrequency,
@@ -3,7 +3,7 @@ import memoize from 'lodash.memoize';
3
3
  import { stringifyFrequency } from './api';
4
4
  import platform from '../platform';
5
5
  import { mapFrequencyToSellingPlan, safeProductId } from './utils';
6
- import { OfferElement, State } from './types/reducer';
6
+ import { OfferElement, ProductFrequencyConfig, State } from './types/reducer';
7
7
 
8
8
  memoize.Cache = Map;
9
9
 
@@ -151,7 +151,7 @@ export const makeProductPrepaidShipmentOptionsSelector = memoize((productId: str
151
151
  );
152
152
 
153
153
  /**
154
- * If the product has a product-specific default frequency, return that frequency
154
+ * If the product has a product-specific default frequency configured in OG, return that frequency
155
155
  */
156
156
  export const makeProductSpecificDefaultFrequencySelector = memoize((productId: string) =>
157
157
  createSelector(
@@ -168,15 +168,48 @@ export const makeProductFrequencyOptionsSelector = memoize((productId: string) =
168
168
  createSelector(makeProductFrequenciesSelector(productId), productFrequencies => productFrequencies.frequencies)
169
169
  );
170
170
 
171
+ /**
172
+ * returns the default frequency for the product from the config state
173
+ * all products have a defaultFrequency stored in state, even if a specific frequency is not configured in OG's database
174
+ * this takes more into account, e.g. whether the customer had opted into a specific frequency previously - see the config reducer for how this is calculated
175
+ */
171
176
  export const makeProductDefaultFrequencySelector = memoize((productId: string) =>
172
177
  createSelector(makeProductFrequenciesSelector(productId), productFrequencies => productFrequencies.defaultFrequency)
173
178
  );
174
179
 
180
+ /**
181
+ * Get the configured frequencies for the given product IDs
182
+ * Using this selector should be preferred over accessing config values directly
183
+ */
175
184
  export const makeProductFrequenciesSelector = memoize((productId: string) =>
176
185
  createSelector(
177
- (state: State) => state?.config?.productFrequencies || {},
178
- productFrequencies => {
179
- return productFrequencies[safeProductId(productId)] || {};
186
+ (state: State) => state?.config?.productFrequencies,
187
+ (state: State) => state?.config?.frequencies,
188
+ (state: State) => state?.config?.frequenciesEveryPeriod,
189
+ (state: State) => state?.config?.frequenciesText,
190
+ (state: State) => state?.config?.defaultFrequency,
191
+ (
192
+ productFrequencies,
193
+ oldFrequencies,
194
+ oldFrequenciesEveryPeriod,
195
+ oldFrequenciesText,
196
+ oldDefaultFrequency
197
+ ): ProductFrequencyConfig => {
198
+ if (productFrequencies) {
199
+ // for Shopify, always use productFrequencies
200
+ // this is necessary to handle cases where different product variants have different selling plans associated with them
201
+ return productFrequencies[safeProductId(productId)] || {};
202
+ } else {
203
+ // productFrequencies are only populated for Shopify
204
+ // fall back to the old "global" frequency values if it is not set
205
+ // these would only be present if the merchant explicitly called `offers.config({ frequencies: [...] })`, so they generally won't be defined
206
+ return {
207
+ frequencies: oldFrequencies,
208
+ frequenciesEveryPeriod: oldFrequenciesEveryPeriod,
209
+ frequenciesText: oldFrequenciesText,
210
+ defaultFrequency: oldDefaultFrequency
211
+ };
212
+ }
180
213
  }
181
214
  )
182
215
  );
@@ -49,7 +49,7 @@ export type ConfigState = Partial<{
49
49
  productFrequencies: Record<string, ProductFrequencyConfig>;
50
50
  }>;
51
51
 
52
- type ProductFrequencyConfig = {
52
+ export type ProductFrequencyConfig = {
53
53
  frequencies?: string[];
54
54
  frequenciesEveryPeriod?: string[];
55
55
  frequenciesText?: string[];
@@ -69,7 +69,10 @@ describe('config', () => {
69
69
  'yum product id': jasmine.objectContaining({
70
70
  defaultFrequency: 'yum selling plan id 2'
71
71
  })
72
- }
72
+ },
73
+ // fallback value in case a merchant was accessing our config directly
74
+ // productFrequencies should be used instead
75
+ defaultFrequency: 'yum selling plan id 2'
73
76
  })
74
77
  );
75
78
  });
@@ -432,6 +435,136 @@ describe('config', () => {
432
435
  );
433
436
  });
434
437
 
438
+ const multiVariantSellingPlanPayload = {
439
+ product: {
440
+ id: 'product-id',
441
+ variants: [
442
+ {
443
+ id: 'variant-id-1',
444
+ selling_plan_allocations: [
445
+ {
446
+ selling_plan_id: 'default-group-plan-1',
447
+ selling_plan_group_id: 'default-group-id'
448
+ },
449
+ {
450
+ selling_plan_id: 'default-group-plan-2',
451
+ selling_plan_group_id: 'default-group-id'
452
+ },
453
+ {
454
+ selling_plan_id: 'default-group-plan-3',
455
+ selling_plan_group_id: 'default-group-id'
456
+ }
457
+ ]
458
+ },
459
+ {
460
+ id: 'variant-id-2',
461
+ selling_plan_allocations: [
462
+ {
463
+ selling_plan_id: 'psi-group-plan-1',
464
+ selling_plan_group_id: 'psi-group-id'
465
+ },
466
+ {
467
+ selling_plan_id: 'psi-group-plan-2',
468
+ selling_plan_group_id: 'psi-group-id'
469
+ },
470
+ {
471
+ selling_plan_id: 'psi-group-plan-3',
472
+ selling_plan_group_id: 'psi-group-id'
473
+ }
474
+ ]
475
+ }
476
+ ],
477
+ selling_plan_groups: [
478
+ {
479
+ id: 'default-group-id',
480
+ name: 'Subscribe and Save',
481
+ options: [{ name: 'Delivery every', position: 1, values: ['month', '2 months', '3 months'] }],
482
+ selling_plans: [
483
+ {
484
+ id: 'default-group-plan-1',
485
+ name: 'Delivered every month. Get 10% off today and all future orders.',
486
+ options: [{ name: 'Delivery every', position: 1, value: 'month' }],
487
+ price_adjustments: [{ order_count: null, position: 1, value_type: 'percentage', value: 10 }]
488
+ },
489
+ {
490
+ id: 'default-group-plan-2',
491
+ name: 'Delivered every 2 months. Get 10% off today and all future orders.',
492
+ options: [{ name: 'Delivery every', position: 1, value: '2 months' }],
493
+ price_adjustments: [{ order_count: null, position: 1, value_type: 'percentage', value: 10 }]
494
+ },
495
+ {
496
+ id: 'default-group-plan-3',
497
+ name: 'Delivered every 3 months. Get 10% off today and all future orders.',
498
+ options: [{ name: 'Delivery every', position: 1, value: '3 months' }],
499
+ price_adjustments: [{ order_count: null, position: 1, value_type: 'percentage', value: 10 }]
500
+ }
501
+ ],
502
+ app_id: 'ordergroove-subscribe-and-save'
503
+ },
504
+ {
505
+ id: 'psi-group-id',
506
+ name: 'test-incentive-group',
507
+ options: [{ name: 'Delivery every', position: 1, values: ['week', '2 weeks', '3 weeks'] }],
508
+ selling_plans: [
509
+ {
510
+ id: 'psi-group-plan-1',
511
+ name: 'Delivered every week. Get 10% off today and 21% off future orders.',
512
+ options: [{ name: 'Delivery every', position: 1, value: 'week' }],
513
+ price_adjustments: [{ order_count: null, position: 1, value_type: 'percentage', value: 10 }]
514
+ },
515
+ {
516
+ id: 'psi-group-plan-2',
517
+ name: 'Delivered every 2 weeks. Get 10% off today and 21% off future orders.',
518
+ options: [{ name: 'Delivery every', position: 1, value: '2 weeks' }],
519
+ price_adjustments: [{ order_count: null, position: 1, value_type: 'percentage', value: 10 }]
520
+ },
521
+ {
522
+ id: 'psi-group-plan-3',
523
+ name: 'Delivered every 3 weeks. Get 10% off today and 21% off future orders.',
524
+ options: [{ name: 'Delivery every', position: 1, value: '3 weeks' }],
525
+ price_adjustments: [{ order_count: null, position: 1, value_type: 'percentage', value: 10 }]
526
+ }
527
+ ],
528
+ app_id: 'ordergroove-subscribe-and-save'
529
+ }
530
+ ]
531
+ }
532
+ };
533
+
534
+ it('should populate frequencies for each product variant', () => {
535
+ const actual = config(
536
+ {
537
+ productFrequencies: {}
538
+ },
539
+ {
540
+ type: constants.SETUP_PRODUCT,
541
+ payload: multiVariantSellingPlanPayload
542
+ }
543
+ );
544
+
545
+ expect(actual).toEqual(
546
+ jasmine.objectContaining({
547
+ productFrequencies: {
548
+ 'variant-id-1': {
549
+ frequencies: ['default-group-plan-1', 'default-group-plan-2', 'default-group-plan-3'],
550
+ frequenciesEveryPeriod: ['1_3', '2_3', '3_3'],
551
+ frequenciesText: ['month', '2 months', '3 months']
552
+ },
553
+ 'variant-id-2': {
554
+ frequencies: ['psi-group-plan-1', 'psi-group-plan-2', 'psi-group-plan-3'],
555
+ frequenciesEveryPeriod: ['1_2', '2_2', '3_2'],
556
+ frequenciesText: ['week', '2 weeks', '3 weeks']
557
+ }
558
+ },
559
+ // fallback values in case a merchant was accessing our config directly
560
+ // productFrequencies should be used instead
561
+ frequencies: ['default-group-plan-1', 'default-group-plan-2', 'default-group-plan-3'],
562
+ frequenciesEveryPeriod: ['1_3', '2_3', '3_3'],
563
+ frequenciesText: ['month', '2 months', '3 months']
564
+ })
565
+ );
566
+ });
567
+
435
568
  it('should set prepaidSellingPlans', () => {
436
569
  const sellingPlanGroups = [
437
570
  {
@@ -591,13 +724,14 @@ describe('config', () => {
591
724
  }
592
725
  }
593
726
  });
594
- expect(actual).toEqual({
595
- ...initial,
596
- productFrequencies: {
597
- 123: jasmine.objectContaining({
598
- defaultFrequency: 'yum selling plan id 1'
599
- })
600
- }
601
- });
727
+ expect(actual).toEqual(
728
+ jasmine.objectContaining({
729
+ productFrequencies: {
730
+ 123: jasmine.objectContaining({
731
+ defaultFrequency: 'yum selling plan id 1'
732
+ })
733
+ }
734
+ })
735
+ );
602
736
  });
603
737
  });
@@ -18,7 +18,9 @@ import { ShopifySellingPlanGroupsEntity, ShopifyVariantsEntity } from '../types/
18
18
  const config = (
19
19
  state: ConfigState = {
20
20
  offerType: 'radio',
21
- productFrequencies: {}
21
+ productFrequencies: {},
22
+ frequencies: [],
23
+ frequenciesEveryPeriod: []
22
24
  },
23
25
  action
24
26
  ): ConfigState => {
@@ -27,18 +29,22 @@ const config = (
27
29
  payload: { product, currency }
28
30
  } = action as { payload: SetupProductPayload };
29
31
  let configToAdd: ConfigState = {};
30
- let productFrequencies = product.variants?.reduce(
32
+ let productFrequencies: ConfigState['productFrequencies'] = product.variants?.reduce(
31
33
  (acc, variant) => reduceSellingPlansToFrequencies(acc, variant, product.selling_plan_groups, state),
32
34
  {}
33
35
  );
34
36
 
37
+ let updatedProductFrequencies = {
38
+ ...state.productFrequencies,
39
+ ...productFrequencies
40
+ };
41
+
35
42
  configToAdd = {
36
43
  ...configToAdd,
37
-
38
- productFrequencies: {
39
- ...state.productFrequencies,
40
- ...productFrequencies
41
- }
44
+ productFrequencies: updatedProductFrequencies,
45
+ // populate the old frequency fields for backwards compatibility
46
+ // these are only needed if someone was reading our config state directly, which shouldn't be common but is possible
47
+ ...Object.values(updatedProductFrequencies)[0]
42
48
  };
43
49
 
44
50
  // prepaid selling plans
@@ -68,21 +74,25 @@ const config = (
68
74
  const productId = safeProductId(product?.id);
69
75
  const currentProductFrequencies = state.productFrequencies[productId];
70
76
 
77
+ let updatedProductFrequencies: ConfigState['productFrequencies'] = {
78
+ ...state.productFrequencies,
79
+ [productId]: {
80
+ ...currentProductFrequencies,
81
+ defaultFrequency: getUpdatedDefaultFrequency(
82
+ productId,
83
+ defaultFrequency,
84
+ prepaidSellingPlans,
85
+ currentProductFrequencies?.frequencies,
86
+ currentProductFrequencies?.frequenciesEveryPeriod
87
+ )
88
+ }
89
+ };
90
+
71
91
  return {
72
92
  ...state,
73
- productFrequencies: {
74
- ...state.productFrequencies,
75
- [productId]: {
76
- ...currentProductFrequencies,
77
- defaultFrequency: getUpdatedDefaultFrequency(
78
- productId,
79
- defaultFrequency,
80
- prepaidSellingPlans,
81
- currentProductFrequencies?.frequencies,
82
- currentProductFrequencies?.frequenciesEveryPeriod
83
- )
84
- }
85
- }
93
+ productFrequencies: updatedProductFrequencies,
94
+ // populate the old frequency fields for backwards compatibility
95
+ ...Object.values(updatedProductFrequencies)[0]
86
96
  };
87
97
  }
88
98