@ordergroove/offers 2.47.2 → 2.48.1-alpha-PR-1357-7.8

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.47.2",
3
+ "version": "2.48.1-alpha-PR-1357-7.8+4d0ab39c0",
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.10.0",
50
50
  "@types/lodash.memoize": "^4.1.9"
51
51
  },
52
- "gitHead": "b039a0b5286ac8736eb4501b4763512f27e58953"
52
+ "gitHead": "4d0ab39c0427f98fdf5165d98d7b00299c4d2a34"
53
53
  }
@@ -7,6 +7,7 @@ import {
7
7
  } from '../core/selectors';
8
8
  import { productChangePrepaidShipments } from '../core/actions';
9
9
  import { withProduct } from '../core/resolveProperties';
10
+ import { getDefaultPrepaidOption } from '../shopify/utils';
10
11
 
11
12
  export class PrepaidStatus extends withProduct(LitElement) {
12
13
  static get properties() {
@@ -29,7 +30,7 @@ export class PrepaidStatus extends withProduct(LitElement) {
29
30
  getDefaultPrepaidShipments() {
30
31
  return this.options.includes(this.defaultPrepaidShipments)
31
32
  ? this.defaultPrepaidShipments
32
- : this.options[1] || this.options[0];
33
+ : getDefaultPrepaidOption(this.options);
33
34
  }
34
35
 
35
36
  handleSelect({ target: { value } }) {
@@ -2,7 +2,11 @@ import { resolveAuth } from '@ordergroove/auth';
2
2
  import * as constants from './constants';
3
3
  import { api } from './api';
4
4
  import { safeOgFrequency } from './utils';
5
- import { makeFrequencyForPrepaidShipmentsSelector, makeProductFrequenciesSelector } from './selectors';
5
+ import {
6
+ makeFrequencyForPrepaidShipmentsSelector,
7
+ makePrepaidSellingPlansSelector,
8
+ makeProductFrequenciesSelector
9
+ } from './selectors';
6
10
 
7
11
  export const optinProduct = (product, frequency, offer) => ({
8
12
  type: constants.OPTIN_PRODUCT,
@@ -190,10 +194,12 @@ export const requestSessionId = () => (dispatch, getState) => {
190
194
 
191
195
  export const receiveOffer = (response, offer, productId) => (dispatch, getState) => {
192
196
  // this is a thunk so that we access the state for the selector
193
- const frequencyConfig = makeProductFrequenciesSelector(productId)(getState());
197
+ const state = getState();
198
+ const frequencyConfig = makeProductFrequenciesSelector(productId)(state);
199
+ const prepaidSellingPlans = makePrepaidSellingPlansSelector(productId)(state);
194
200
  dispatch({
195
201
  type: constants.RECEIVE_OFFER,
196
- payload: { ...response, offer, frequencyConfig }
202
+ payload: { ...response, offer, frequencyConfig, prepaidSellingPlans }
197
203
  });
198
204
  };
199
205
 
@@ -61,6 +61,10 @@ export const INCENTIVE_STANDARD_TYPES = {
61
61
  PROGRAM_WIDE: 'PROGRAM_WIDE'
62
62
  };
63
63
 
64
+ export const ELIGIBILITY_GROUPS = {
65
+ PREPAID: 'prepaid'
66
+ };
67
+
64
68
  /**
65
69
  * @event
66
70
  * Events that fires once optin/optout occurs on a cart offer
@@ -1,3 +1,4 @@
1
+ import { ELIGIBILITY_GROUPS } from './constants';
1
2
  import {
2
3
  makeSubscribedSelector,
3
4
  makeOptedoutSelector,
@@ -25,7 +26,7 @@ export const hasUpsellGroup = (state, ownProps) => {
25
26
 
26
27
  export const prepaidEligible = (state, ownProps) => {
27
28
  const groups = eligibilityGroups(state, ownProps);
28
- return groups?.some(it => it === 'prepaid') || false;
29
+ return groups?.some(it => it === ELIGIBILITY_GROUPS.PREPAID) || false;
29
30
  };
30
31
 
31
32
  export const subscribed = (state, ownProps) => makeSubscribedSelector(ownProps.product)(state);
@@ -232,6 +232,12 @@ export const makeFrequencyForPrepaidShipmentsSelector = (product: BaseProduct, p
232
232
  }
233
233
  );
234
234
 
235
+ export const makePrepaidSellingPlansSelector = (product: string) =>
236
+ createSelector(prepaidSellingPlansSelector, prepaidSellingPlans => {
237
+ const productId = safeProductId(product);
238
+ return prepaidSellingPlans[productId] || [];
239
+ });
240
+
235
241
  /** Determine the discounted price of the product, based on the incentives returned from the Offers endpoint. This assumes a pay-as-you-go subscription. */
236
242
  export const makeDiscountedProductPriceSelector = memoize((productId: string) =>
237
243
  createSelector(
@@ -55,11 +55,13 @@ export type ConfigState = Partial<{
55
55
  * @deprecated use productFrequencies instead
56
56
  */
57
57
  frequenciesText: string[];
58
- prepaidSellingPlans: Record<string, { numberShipments: number; sellingPlan: string }[]>;
58
+ prepaidSellingPlans: Record<string, PrepaidSellingPlan[]>;
59
59
  storeCurrency: string;
60
60
  productFrequencies: Record<string, ProductFrequencyConfig>;
61
61
  }>;
62
62
 
63
+ export type PrepaidSellingPlan = { numberShipments: number; sellingPlan: string };
64
+
63
65
  export type ProductFrequencyConfig = {
64
66
  frequencies?: string[];
65
67
  frequenciesEveryPeriod?: string[];
@@ -92,6 +94,7 @@ export type ReceiveOfferPayload = OfferResponse & {
92
94
  frequenciesEveryPeriod?: string[];
93
95
  frequenciesText?: string[];
94
96
  };
97
+ prepaidSellingPlans: PrepaidSellingPlan[];
95
98
  };
96
99
 
97
100
  export type OfferElement = InstanceType<typeof Offer> & { config: ConfigState };
@@ -2,6 +2,45 @@ import * as constants from '../../../core/constants';
2
2
  import { optedin } from '../../shopifyReducer';
3
3
  import { DEFAULT_PAY_AS_YOU_GO_GROUP_NAME } from '../../utils';
4
4
 
5
+ const PREPAID_PRODUCT_ID = '43017264201944';
6
+ const BOTH_ELIGIBLE_PRODUCT_ID = '43017264201946';
7
+
8
+ const PREPAID_PLAN_3_SHIPMENTS = 'prepaid-plan-3-shipments';
9
+ const PREPAID_PLAN_6_SHIPMENTS = 'prepaid-plan-6-shipments';
10
+
11
+ const prepaidPlanWith3Shipments = { sellingPlan: PREPAID_PLAN_3_SHIPMENTS, numberShipments: 3 };
12
+ const prepaidPlanWith6Shipments = { sellingPlan: PREPAID_PLAN_6_SHIPMENTS, numberShipments: 6 };
13
+
14
+ function buildReceiveOfferPayload(overrides = {}) {
15
+ return {
16
+ autoship: {},
17
+ autoship_by_default: {},
18
+ default_frequencies: {},
19
+ in_stock: {},
20
+ eligibility_groups: {},
21
+ offer: {},
22
+ frequencyConfig: {
23
+ frequencies: [],
24
+ frequenciesEveryPeriod: []
25
+ },
26
+ prepaidSellingPlans: [],
27
+ ...overrides
28
+ };
29
+ }
30
+
31
+ function buildPrepaidSellingPlanGroup(variantId, plans) {
32
+ return {
33
+ name: `Prepaid-${variantId}`,
34
+ selling_plans: plans.map((plan, index) => ({
35
+ id: plan.sellingPlan,
36
+ options: [
37
+ { name: 'Delivery every', value: `PREPAID-${index + 1} months` },
38
+ { name: 'Shipment amount', value: `${plan.numberShipments} shipments` }
39
+ ]
40
+ }))
41
+ };
42
+ }
43
+
5
44
  describe('optedin', () => {
6
45
  it('should return optins given action SETUP_CART', () => {
7
46
  const actual = optedin([], {
@@ -583,6 +622,208 @@ describe('optedin', () => {
583
622
  ]);
584
623
  });
585
624
  });
625
+
626
+ describe('given product is prepaid eligible but NOT autoship eligible', () => {
627
+ describe('given prepaidSellingPlans are populated', () => {
628
+ it('should opt into 2nd prepaid plan and set prepaidShipments when multiple plans available', () => {
629
+ const actual = optedin([], {
630
+ type: constants.RECEIVE_OFFER,
631
+ payload: buildReceiveOfferPayload({
632
+ autoship: {
633
+ [PREPAID_PRODUCT_ID]: false
634
+ },
635
+ autoship_by_default: {
636
+ [PREPAID_PRODUCT_ID]: true
637
+ },
638
+ in_stock: {
639
+ [PREPAID_PRODUCT_ID]: true
640
+ },
641
+ eligibility_groups: {
642
+ [PREPAID_PRODUCT_ID]: [constants.ELIGIBILITY_GROUPS.PREPAID]
643
+ },
644
+ prepaidSellingPlans: [prepaidPlanWith3Shipments, prepaidPlanWith6Shipments]
645
+ })
646
+ });
647
+
648
+ expect(actual).toEqual([
649
+ {
650
+ id: PREPAID_PRODUCT_ID,
651
+ frequency: PREPAID_PLAN_6_SHIPMENTS,
652
+ prepaidShipments: 6
653
+ }
654
+ ]);
655
+ });
656
+
657
+ it('should opt into 1st prepaid plan and set prepaidShipments when only one plan available', () => {
658
+ const actual = optedin([], {
659
+ type: constants.RECEIVE_OFFER,
660
+ payload: buildReceiveOfferPayload({
661
+ autoship: {
662
+ [PREPAID_PRODUCT_ID]: false
663
+ },
664
+ autoship_by_default: {
665
+ [PREPAID_PRODUCT_ID]: true
666
+ },
667
+ in_stock: {
668
+ [PREPAID_PRODUCT_ID]: true
669
+ },
670
+ eligibility_groups: {
671
+ [PREPAID_PRODUCT_ID]: [constants.ELIGIBILITY_GROUPS.PREPAID]
672
+ },
673
+ prepaidSellingPlans: [prepaidPlanWith3Shipments]
674
+ })
675
+ });
676
+
677
+ expect(actual).toEqual([
678
+ {
679
+ id: PREPAID_PRODUCT_ID,
680
+ frequency: PREPAID_PLAN_3_SHIPMENTS,
681
+ prepaidShipments: 3
682
+ }
683
+ ]);
684
+ });
685
+ });
686
+
687
+ describe('given prepaidSellingPlans are NOT populated', () => {
688
+ it('should opt into prepaid with PREPAID_PLACEHOLDER and prepaidShipments null', () => {
689
+ const actual = optedin([], {
690
+ type: constants.RECEIVE_OFFER,
691
+ payload: buildReceiveOfferPayload({
692
+ autoship: {
693
+ [PREPAID_PRODUCT_ID]: false
694
+ },
695
+ autoship_by_default: {
696
+ [PREPAID_PRODUCT_ID]: true
697
+ },
698
+ in_stock: {
699
+ [PREPAID_PRODUCT_ID]: true
700
+ },
701
+ eligibility_groups: {
702
+ [PREPAID_PRODUCT_ID]: [constants.ELIGIBILITY_GROUPS.PREPAID]
703
+ },
704
+ prepaidSellingPlans: []
705
+ })
706
+ });
707
+
708
+ expect(actual).toEqual([
709
+ {
710
+ id: PREPAID_PRODUCT_ID,
711
+ frequency: 'prepaid-replace-me',
712
+ prepaidShipments: null
713
+ }
714
+ ]);
715
+ });
716
+ });
717
+
718
+ describe('edge cases', () => {
719
+ it('should not create optin when product not in stock, not autoship_by_default, or not in eligibility_groups', () => {
720
+ // Product not in stock
721
+ const notInStock = optedin([], {
722
+ type: constants.RECEIVE_OFFER,
723
+ payload: buildReceiveOfferPayload({
724
+ autoship: {
725
+ [PREPAID_PRODUCT_ID]: false
726
+ },
727
+ autoship_by_default: {
728
+ [PREPAID_PRODUCT_ID]: true
729
+ },
730
+ in_stock: {
731
+ [PREPAID_PRODUCT_ID]: false
732
+ },
733
+ eligibility_groups: {
734
+ [PREPAID_PRODUCT_ID]: [constants.ELIGIBILITY_GROUPS.PREPAID]
735
+ },
736
+ prepaidSellingPlans: [prepaidPlanWith3Shipments]
737
+ })
738
+ });
739
+
740
+ // Product not autoship_by_default
741
+ const notAutoshipByDefault = optedin([], {
742
+ type: constants.RECEIVE_OFFER,
743
+ payload: buildReceiveOfferPayload({
744
+ autoship: {
745
+ [PREPAID_PRODUCT_ID]: false
746
+ },
747
+ autoship_by_default: {
748
+ [PREPAID_PRODUCT_ID]: false
749
+ },
750
+ in_stock: {
751
+ [PREPAID_PRODUCT_ID]: true
752
+ },
753
+ eligibility_groups: {
754
+ [PREPAID_PRODUCT_ID]: [constants.ELIGIBILITY_GROUPS.PREPAID]
755
+ },
756
+ prepaidSellingPlans: [prepaidPlanWith3Shipments]
757
+ })
758
+ });
759
+
760
+ // Product not in eligibility_groups
761
+ const notInEligibilityGroups = optedin([], {
762
+ type: constants.RECEIVE_OFFER,
763
+ payload: buildReceiveOfferPayload({
764
+ autoship: {
765
+ [PREPAID_PRODUCT_ID]: false
766
+ },
767
+ autoship_by_default: {
768
+ [PREPAID_PRODUCT_ID]: true
769
+ },
770
+ in_stock: {
771
+ [PREPAID_PRODUCT_ID]: true
772
+ },
773
+ eligibility_groups: {
774
+ [PREPAID_PRODUCT_ID]: []
775
+ },
776
+ prepaidSellingPlans: [prepaidPlanWith3Shipments]
777
+ })
778
+ });
779
+
780
+ expect(notInStock).toEqual([]);
781
+ expect(notAutoshipByDefault).toEqual([]);
782
+ expect(notInEligibilityGroups).toEqual([]);
783
+ });
784
+ });
785
+ });
786
+
787
+ describe('given product is BOTH prepaid eligible AND autoship eligible', () => {
788
+ it('should opt into regular subscription with PSDF frequency, not prepaid', () => {
789
+ const actual = optedin([], {
790
+ type: constants.RECEIVE_OFFER,
791
+ payload: buildReceiveOfferPayload({
792
+ autoship: {
793
+ [BOTH_ELIGIBLE_PRODUCT_ID]: true
794
+ },
795
+ autoship_by_default: {
796
+ [BOTH_ELIGIBLE_PRODUCT_ID]: true
797
+ },
798
+ default_frequencies: {
799
+ [BOTH_ELIGIBLE_PRODUCT_ID]: {
800
+ every: 1,
801
+ every_period: 2
802
+ }
803
+ },
804
+ in_stock: {
805
+ [BOTH_ELIGIBLE_PRODUCT_ID]: true
806
+ },
807
+ eligibility_groups: {
808
+ [BOTH_ELIGIBLE_PRODUCT_ID]: [constants.ELIGIBILITY_GROUPS.PREPAID]
809
+ },
810
+ frequencyConfig: {
811
+ frequencies: ['yum selling plan id 1', 'yum selling plan id 2'],
812
+ frequenciesEveryPeriod: ['1_1', '1_2']
813
+ },
814
+ prepaidSellingPlans: [prepaidPlanWith3Shipments, prepaidPlanWith6Shipments]
815
+ })
816
+ });
817
+
818
+ expect(actual).toEqual([
819
+ {
820
+ id: BOTH_ELIGIBLE_PRODUCT_ID,
821
+ frequency: 'yum selling plan id 2'
822
+ }
823
+ ]);
824
+ expect(actual[0].prepaidShipments).toBeUndefined();
825
+ });
826
+ });
586
827
  });
587
828
  });
588
829
 
@@ -756,6 +997,69 @@ describe('optedin', () => {
756
997
  }
757
998
  ]);
758
999
  });
1000
+
1001
+ describe('given frequency is PREPAID_PLACEHOLDER', () => {
1002
+ describe('given prepaidSellingPlans exist for variant', () => {
1003
+ it('should replace prepaid placeholder optin', () => {
1004
+ const actual = optedin(
1005
+ [
1006
+ {
1007
+ id: PREPAID_PRODUCT_ID,
1008
+ frequency: 'prepaid-replace-me',
1009
+ prepaidShipments: null
1010
+ }
1011
+ ],
1012
+ {
1013
+ type: constants.SETUP_PRODUCT,
1014
+ payload: {
1015
+ product: {
1016
+ variants: [{ id: PREPAID_PRODUCT_ID }],
1017
+ selling_plan_groups: [
1018
+ buildPrepaidSellingPlanGroup(PREPAID_PRODUCT_ID, [
1019
+ prepaidPlanWith3Shipments,
1020
+ prepaidPlanWith6Shipments
1021
+ ])
1022
+ ]
1023
+ }
1024
+ }
1025
+ }
1026
+ );
1027
+
1028
+ expect(actual).toEqual([
1029
+ {
1030
+ id: PREPAID_PRODUCT_ID,
1031
+ frequency: PREPAID_PLAN_6_SHIPMENTS,
1032
+ prepaidShipments: 6
1033
+ }
1034
+ ]);
1035
+ });
1036
+ });
1037
+
1038
+ describe('given prepaidSellingPlans do NOT exist for variant', () => {
1039
+ it('should remove placeholder optin', () => {
1040
+ const actual = optedin(
1041
+ [
1042
+ {
1043
+ id: PREPAID_PRODUCT_ID,
1044
+ frequency: 'prepaid-replace-me',
1045
+ prepaidShipments: null
1046
+ }
1047
+ ],
1048
+ {
1049
+ type: constants.SETUP_PRODUCT,
1050
+ payload: {
1051
+ product: {
1052
+ variants: [{ id: PREPAID_PRODUCT_ID }],
1053
+ selling_plan_groups: []
1054
+ }
1055
+ }
1056
+ }
1057
+ );
1058
+
1059
+ expect(actual).toEqual([]);
1060
+ });
1061
+ });
1062
+ });
759
1063
  });
760
1064
 
761
1065
  describe('given action is PRODUCT_CHANGE_PREPAID_SHIPMENTS', () => {
@@ -807,6 +1111,49 @@ describe('optedin', () => {
807
1111
  });
808
1112
  });
809
1113
 
1114
+ it('integration: SETUP_PRODUCT after RECEIVE_OFFER should handle default prepaid optin', () => {
1115
+ // Step 1: RECEIVE_OFFER creates optin with placeholder (no prepaidSellingPlans available yet)
1116
+ const afterReceiveOffer = optedin([], {
1117
+ type: constants.RECEIVE_OFFER,
1118
+ payload: buildReceiveOfferPayload({
1119
+ autoship: {
1120
+ [PREPAID_PRODUCT_ID]: false
1121
+ },
1122
+ autoship_by_default: {
1123
+ [PREPAID_PRODUCT_ID]: true
1124
+ },
1125
+ in_stock: {
1126
+ [PREPAID_PRODUCT_ID]: true
1127
+ },
1128
+ eligibility_groups: {
1129
+ [PREPAID_PRODUCT_ID]: [constants.ELIGIBILITY_GROUPS.PREPAID]
1130
+ },
1131
+ prepaidSellingPlans: []
1132
+ })
1133
+ });
1134
+
1135
+ // Step 2: SETUP_PRODUCT replaces placeholder with actual selling plan
1136
+ const afterSetupProduct = optedin(afterReceiveOffer, {
1137
+ type: constants.SETUP_PRODUCT,
1138
+ payload: {
1139
+ product: {
1140
+ variants: [{ id: PREPAID_PRODUCT_ID }],
1141
+ selling_plan_groups: [
1142
+ buildPrepaidSellingPlanGroup(PREPAID_PRODUCT_ID, [prepaidPlanWith3Shipments, prepaidPlanWith6Shipments])
1143
+ ]
1144
+ }
1145
+ }
1146
+ });
1147
+
1148
+ expect(afterSetupProduct).toEqual([
1149
+ {
1150
+ id: PREPAID_PRODUCT_ID,
1151
+ frequency: PREPAID_PLAN_6_SHIPMENTS,
1152
+ prepaidShipments: 6
1153
+ }
1154
+ ]);
1155
+ });
1156
+
810
1157
  it('should return unmodified state given unsupported action', () => {
811
1158
  const actual = optedin(
812
1159
  { 'yum existing key': 'yum existing value' },
@@ -13,7 +13,7 @@ import {
13
13
  sellingPlansToFrequencies,
14
14
  getPrepaidShipments
15
15
  } from '../utils';
16
- import { ShopifySellingPlanGroupsEntity, ShopifyVariantsEntity } from '../types/shopify';
16
+ import { ShopifyProductEntity, ShopifySellingPlanGroupsEntity, ShopifyVariantsEntity } from '../types/shopify';
17
17
 
18
18
  const config = (
19
19
  state: ConfigState = {
@@ -48,11 +48,11 @@ const config = (
48
48
  };
49
49
 
50
50
  // prepaid selling plans
51
- const prepaidSellingPlanGroups = product?.selling_plan_groups.filter(group => /^Prepaid-.*/.test(group.name));
52
- if (prepaidSellingPlanGroups.length) {
51
+ const prepaidSellingPlans = getPrepaidSellingPlans(product);
52
+ if (Object.keys(prepaidSellingPlans).length) {
53
53
  configToAdd = {
54
54
  ...configToAdd,
55
- prepaidSellingPlans: { ...state.prepaidSellingPlans, ...getPrepaidSellingPlans(prepaidSellingPlanGroups) }
55
+ prepaidSellingPlans: { ...state.prepaidSellingPlans, ...prepaidSellingPlans }
56
56
  };
57
57
  }
58
58
  return {
@@ -177,7 +177,14 @@ function getUpdatedDefaultFrequency(
177
177
  );
178
178
  }
179
179
 
180
- function getPrepaidSellingPlans(prepaidSellingPlanGroups) {
180
+ export function getPrepaidSellingPlans(
181
+ product: ShopifyProductEntity
182
+ ): Record<string, { numberShipments: number; sellingPlan: string }[]> {
183
+ const prepaidSellingPlanGroups = product?.selling_plan_groups.filter(group => /^Prepaid-.*/.test(group.name));
184
+ if (!prepaidSellingPlanGroups.length) {
185
+ return {};
186
+ }
187
+
181
188
  return prepaidSellingPlanGroups.reduce((acc, cur) => {
182
189
  const variant = cur.name.split('-')[1];
183
190