@ordergroove/offers 2.45.6 → 2.46.1-alpha-PR-1285-2.53

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.
@@ -1,6 +1,5 @@
1
1
  import * as constants from '../../core/constants';
2
- import { autoshipEligible, inStock, offer, optedin, productOffer, productPlans } from '../shopifyReducer';
3
- import { getObjectStructuredProductPlans } from '../../core/adapters';
2
+ import { autoshipEligible, inStock, offer, optedin, price, productOffer, productPlans } from '../shopifyReducer';
4
3
  import { DEFAULT_PAY_AS_YOU_GO_GROUP_NAME } from '../utils';
5
4
 
6
5
  describe('autoshipEligible', () => {
@@ -1052,6 +1051,65 @@ describe('optedin', () => {
1052
1051
  });
1053
1052
  });
1054
1053
 
1054
+ describe('price', () => {
1055
+ it('should return price for each variant given action SETUP_PRODUCT', () => {
1056
+ const actual = price(
1057
+ {},
1058
+ {
1059
+ type: constants.SETUP_PRODUCT,
1060
+ payload: {
1061
+ product: {
1062
+ id: 'yum product id',
1063
+ variants: [
1064
+ {
1065
+ id: 'yum variant id 1',
1066
+ price: 1000
1067
+ },
1068
+ {
1069
+ id: 'yum variant id 2',
1070
+ price: 2500
1071
+ }
1072
+ ]
1073
+ }
1074
+ }
1075
+ }
1076
+ );
1077
+
1078
+ expect(actual).toEqual({
1079
+ 'yum variant id 1': { value: 1000 },
1080
+ 'yum variant id 2': { value: 2500 }
1081
+ });
1082
+ });
1083
+
1084
+ it('should return unmodified state given action SETUP_PRODUCT with no variants', () => {
1085
+ const actual = price(
1086
+ { 'yum existing key': 'yum existing value' },
1087
+ {
1088
+ type: constants.SETUP_PRODUCT,
1089
+ payload: {
1090
+ product: {
1091
+ id: 'yum product id'
1092
+ }
1093
+ }
1094
+ }
1095
+ );
1096
+
1097
+ expect(actual).toEqual({ 'yum existing key': 'yum existing value' });
1098
+ });
1099
+
1100
+ it('should return unmodified state given unsupported action', () => {
1101
+ const actual = price(
1102
+ { 'yum existing key': 'yum existing value' },
1103
+ {
1104
+ type: 'yum unsupported action',
1105
+ payload: {}
1106
+ }
1107
+ );
1108
+
1109
+ expect(actual).toEqual({ 'yum existing key': 'yum existing value' });
1110
+ });
1111
+ });
1112
+
1055
1113
  describe('productOffer', () => {
1056
1114
  it('should return unmodified state', () => {
1057
1115
  const actual = productOffer({ 'yum existing key': 'yum existing value' });
@@ -1399,7 +1457,8 @@ describe('productPlans', () => {
1399
1457
  prepaidShipments: null,
1400
1458
  regularPrice: '$0.50',
1401
1459
  subscriptionPrice: '$0.25',
1402
- discountRate: '$0.25'
1460
+ discountRate: '$0.25',
1461
+ hasPriceAdjustments: true
1403
1462
  }
1404
1463
  ],
1405
1464
  'yum variant id 2': [
@@ -1408,7 +1467,8 @@ describe('productPlans', () => {
1408
1467
  prepaidShipments: null,
1409
1468
  regularPrice: '$0.10',
1410
1469
  subscriptionPrice: '$0.08',
1411
- discountRate: '$0.02'
1470
+ discountRate: '$0.02',
1471
+ hasPriceAdjustments: false
1412
1472
  }
1413
1473
  ]
1414
1474
  });
@@ -1424,7 +1484,8 @@ describe('productPlans', () => {
1424
1484
  regularPrice: '$0.50',
1425
1485
  subscriptionPrice: '$0.25',
1426
1486
  discountRate: '$0.25',
1427
- prepaidShipments: null
1487
+ prepaidShipments: null,
1488
+ hasPriceAdjustments: true
1428
1489
  },
1429
1490
  {
1430
1491
  frequency: 'yum selling plan id prepaid 3 shipments',
@@ -1434,7 +1495,8 @@ describe('productPlans', () => {
1434
1495
  prepaidShipments: 3,
1435
1496
  regularPrepaidPrice: '$1.20',
1436
1497
  prepaidSavingsPerShipment: '$0.10',
1437
- prepaidSavingsTotal: '$0.30'
1498
+ prepaidSavingsTotal: '$0.30',
1499
+ hasPriceAdjustments: false
1438
1500
  },
1439
1501
  {
1440
1502
  frequency: 'yum selling plan id prepaid 6 shipments',
@@ -1444,7 +1506,8 @@ describe('productPlans', () => {
1444
1506
  prepaidShipments: 6,
1445
1507
  regularPrepaidPrice: '$2.40',
1446
1508
  prepaidSavingsPerShipment: '$0.10',
1447
- prepaidSavingsTotal: '$0.60'
1509
+ prepaidSavingsTotal: '$0.60',
1510
+ hasPriceAdjustments: false
1448
1511
  },
1449
1512
  {
1450
1513
  frequency: 'yum selling plan id prepaid 12 shipments',
@@ -1454,7 +1517,8 @@ describe('productPlans', () => {
1454
1517
  prepaidShipments: 12,
1455
1518
  regularPrepaidPrice: '$4.80',
1456
1519
  prepaidSavingsPerShipment: '$0.10',
1457
- prepaidSavingsTotal: '$1.20'
1520
+ prepaidSavingsTotal: '$1.20',
1521
+ hasPriceAdjustments: false
1458
1522
  }
1459
1523
  ],
1460
1524
  'yum variant id 2': [
@@ -1463,7 +1527,8 @@ describe('productPlans', () => {
1463
1527
  regularPrice: '$0.10',
1464
1528
  subscriptionPrice: '$0.08',
1465
1529
  discountRate: '$0.02',
1466
- prepaidShipments: null
1530
+ prepaidShipments: null,
1531
+ hasPriceAdjustments: false
1467
1532
  }
1468
1533
  ]
1469
1534
  });
@@ -1479,7 +1544,8 @@ describe('productPlans', () => {
1479
1544
  prepaidShipments: null,
1480
1545
  regularPrice: '£0.50',
1481
1546
  subscriptionPrice: '£0.25',
1482
- discountRate: '£0.25'
1547
+ discountRate: '£0.25',
1548
+ hasPriceAdjustments: true
1483
1549
  }
1484
1550
  ],
1485
1551
  'yum variant id 2': [
@@ -1488,7 +1554,8 @@ describe('productPlans', () => {
1488
1554
  prepaidShipments: null,
1489
1555
  regularPrice: '£0.10',
1490
1556
  subscriptionPrice: '£0.08',
1491
- discountRate: '£0.02'
1557
+ discountRate: '£0.02',
1558
+ hasPriceAdjustments: false
1492
1559
  }
1493
1560
  ]
1494
1561
  });
@@ -1504,7 +1571,8 @@ describe('productPlans', () => {
1504
1571
  prepaidShipments: null,
1505
1572
  regularPrice: '$0.50',
1506
1573
  subscriptionPrice: '$0.25',
1507
- discountRate: '$0.25'
1574
+ discountRate: '$0.25',
1575
+ hasPriceAdjustments: true
1508
1576
  }
1509
1577
  ],
1510
1578
  'yum variant key 2': [
@@ -1513,7 +1581,8 @@ describe('productPlans', () => {
1513
1581
  prepaidShipments: null,
1514
1582
  regularPrice: '$0.10',
1515
1583
  subscriptionPrice: '$0.08',
1516
- discountRate: '$0.02'
1584
+ discountRate: '$0.02',
1585
+ hasPriceAdjustments: false
1517
1586
  }
1518
1587
  ]
1519
1588
  });
@@ -1532,7 +1601,8 @@ describe('productPlans', () => {
1532
1601
  prepaidShipments: 12,
1533
1602
  regularPrepaidPrice: '$4.80',
1534
1603
  prepaidSavingsPerShipment: '$0.10',
1535
- prepaidSavingsTotal: '$1.20'
1604
+ prepaidSavingsTotal: '$1.20',
1605
+ hasPriceAdjustments: true
1536
1606
  }
1537
1607
  ],
1538
1608
  'yum variant key 2': [
@@ -1541,7 +1611,8 @@ describe('productPlans', () => {
1541
1611
  prepaidShipments: null,
1542
1612
  regularPrice: '$0.10',
1543
1613
  subscriptionPrice: '$0.08',
1544
- discountRate: '$0.02'
1614
+ discountRate: '$0.02',
1615
+ hasPriceAdjustments: false
1545
1616
  }
1546
1617
  ]
1547
1618
  });
@@ -1557,7 +1628,8 @@ describe('productPlans', () => {
1557
1628
  prepaidShipments: null,
1558
1629
  regularPrice: '£0.50',
1559
1630
  subscriptionPrice: '£0.25',
1560
- discountRate: '£0.25'
1631
+ discountRate: '£0.25',
1632
+ hasPriceAdjustments: true
1561
1633
  }
1562
1634
  ],
1563
1635
  'yum variant key 2': [
@@ -1566,7 +1638,8 @@ describe('productPlans', () => {
1566
1638
  prepaidShipments: null,
1567
1639
  regularPrice: '£0.10',
1568
1640
  subscriptionPrice: '£0.08',
1569
- discountRate: '£0.02'
1641
+ discountRate: '£0.02',
1642
+ hasPriceAdjustments: false
1570
1643
  }
1571
1644
  ]
1572
1645
  });
@@ -106,7 +106,8 @@ export const mapSellingPlanToDiscount = (
106
106
  regularPrice: getAllocationRegularPrice(allocation, currency),
107
107
  subscriptionPrice: getAllocationSubscriptionPrice(allocation, currency),
108
108
  discountRate: getAllocationDiscountRate(allocation, currency),
109
- prepaidShipments: getAllocationNumberOfShipments(allocation)
109
+ prepaidShipments: getAllocationNumberOfShipments(allocation),
110
+ hasPriceAdjustments: allocation.price_adjustments?.length > 0
110
111
  };
111
112
 
112
113
  if (isPrepaidAllocation(allocation)) {
@@ -14,16 +14,18 @@ import {
14
14
  SETUP_PRODUCT
15
15
  } from '../core/constants';
16
16
 
17
- import { makeSubscribedSelector } from '../core/selectors';
17
+ import { isShopifyDiscountFunctionInUseSelector, makeSubscribedSelector } from '../core/selectors';
18
18
  import { getOrCreateHidden, safeProductId } from '../core/utils';
19
19
  import { getTrackingKey } from './shopifyTrackingMiddleware';
20
20
  import { ShopifyCart, ShopifyProductEntity } from './types/shopify';
21
- import { SetupProductPayload, SetupCartPayload, OfferElement } from '../core/types/reducer';
21
+ import { SetupProductPayload, SetupCartPayload, OfferElement, State } from '../core/types/reducer';
22
+ import { type Store } from 'redux';
22
23
 
23
24
  const SHOPIFY_ROOT = window.Shopify?.routes?.root || '/';
24
25
  const CART_PAGE_URL = '/cart';
25
26
  const CART_JS_URL = `${SHOPIFY_ROOT}cart.js`;
26
27
  const CART_CHANGE_URL = `${SHOPIFY_ROOT}cart/change.js`;
28
+ const CART_UPDATE_URL = `${SHOPIFY_ROOT}cart/update.js`;
27
29
  const PRODUCTS_URL = `${SHOPIFY_ROOT}products/`;
28
30
 
29
31
  /**
@@ -189,23 +191,42 @@ export async function synchronizeCartOptin(action: any, store: any) {
189
191
  const qty = item.quantity;
190
192
  const productId = safeProductId(key);
191
193
 
192
- const res = await fetch(CART_CHANGE_URL, {
194
+ const changeRes = await fetch(CART_CHANGE_URL, {
193
195
  method: 'POST',
194
196
  credentials: 'same-origin',
195
197
  headers: { 'Content-Type': 'application/json' },
196
198
  body: JSON.stringify({
197
199
  id: key,
198
200
  quantity: qty,
199
- attributes: Object.fromEntries([trackingEvent]),
200
201
  properties: item.properties,
201
202
  selling_plan: selling_plan || null,
202
203
  sections: sectionsToUpdate.map((el: HTMLElement) => el.id.replace(/^shopify-section-/, ''))
203
204
  })
204
205
  });
205
206
 
206
- if (res.status !== 200) throw new Error('Cart not updated');
207
+ if (changeRes.status !== 200) throw new Error('Cart not updated');
208
+
209
+ const offerIdAttribute = getOfferIdAttribute(store);
210
+ const attributes = {
211
+ ...Object.fromEntries([trackingEvent]),
212
+ ...(offerIdAttribute ? { [OFFER_ATTRIBUTE_NAME]: offerIdAttribute } : {})
213
+ };
214
+
215
+ if (Object.keys(attributes).length > 0) {
216
+ // update the cart attributes
217
+ const updateRes = await fetch(CART_UPDATE_URL, {
218
+ method: 'POST',
219
+ credentials: 'same-origin',
220
+ headers: { 'Content-Type': 'application/json' },
221
+ body: JSON.stringify({
222
+ attributes
223
+ })
224
+ });
225
+
226
+ if (updateRes.status !== 200) throw new Error('Cart attributes not updated');
227
+ }
207
228
 
208
- const newCart: ShopifyCart = await res.json();
229
+ const newCart: ShopifyCart = await changeRes.json();
209
230
 
210
231
  // If both carts have same length we can update the item.key
211
232
  // to the original offer element, at least provide
@@ -332,12 +353,23 @@ export function getSubscribedFrequency(productId, store) {
332
353
  return sellingPlanId;
333
354
  }
334
355
 
356
+ const OFFER_ATTRIBUTE_NAME = '__ordergroove_offer_id';
357
+
358
+ function getOfferIdAttribute(store: Store<State>) {
359
+ const state = store.getState();
360
+ // if the Shopify Discount Function is being used, we need to pass along the offer ID as a cart attribute so the discount is calculated correctly
361
+ if (isShopifyDiscountFunctionInUseSelector(state)) {
362
+ return state.offerId;
363
+ }
364
+ return null;
365
+ }
366
+
335
367
  /**
336
368
  * // update <input type="hidden" name="selling_plan"/> if available
337
369
  *
338
370
  * @param store
339
371
  */
340
- function synchronizeSellingPlan(store: any, offerElement?: OfferElement) {
372
+ export function synchronizeSellingPlan(store: any, offerElement?: OfferElement) {
341
373
  if (offerElement?.isCart) return; // hidden inputs are used when product page, not cart.
342
374
  if (!offerElement?.shouldEnableOffer) return; // do not set a selling plan if we're hiding the offer
343
375
 
@@ -348,6 +380,12 @@ function synchronizeSellingPlan(store: any, offerElement?: OfferElement) {
348
380
 
349
381
  getOrCreateHidden(productIdInput.form, 'selling_plan', sellingPlanId);
350
382
  getOrCreateHidden(productIdInput.form, `attributes[og__session]`, store.getState().sessionId);
383
+
384
+ const offerIdAttribute = getOfferIdAttribute(store);
385
+ if (offerIdAttribute) {
386
+ getOrCreateHidden(productIdInput.form, `attributes[${OFFER_ATTRIBUTE_NAME}]`, offerIdAttribute);
387
+ }
388
+
351
389
  if (offerElement) {
352
390
  // use this to update the product attributes in future
353
391
  }
@@ -36,6 +36,7 @@ import type {
36
36
  OfferElement,
37
37
  OptedInState,
38
38
  OptInItem,
39
+ PriceState,
39
40
  ReceiveOfferPayload,
40
41
  SetupProductPayload
41
42
  } from '../core/types/reducer';
@@ -289,6 +290,25 @@ export const optedin = (state: OptedInState = [], action): OptedInState => {
289
290
  return coreOptedin(state, action);
290
291
  };
291
292
 
293
+ export const price = (state: PriceState = {}, action): PriceState => {
294
+ if (constants.SETUP_PRODUCT === action.type) {
295
+ const {
296
+ payload: { product }
297
+ } = action as { payload: SetupProductPayload };
298
+ return (
299
+ product.variants?.reduce(
300
+ (acc, cur) => ({
301
+ ...acc,
302
+ [cur.id]: { value: cur.price }
303
+ }),
304
+ state
305
+ ) || state
306
+ );
307
+ }
308
+
309
+ return state;
310
+ };
311
+
292
312
  export const productOffer = (state = {}, _action) => state;
293
313
 
294
314
  export const productPlans = (state = {}, action) => {
@@ -352,6 +372,7 @@ const reducer = combineReducers({
352
372
  optedout,
353
373
  previewStandardOffer,
354
374
  previewUpsellOffer,
375
+ price,
355
376
  productOffer,
356
377
  productPlans,
357
378
  productToSubscribe,
@@ -8,4 +8,5 @@ export interface ProductPlanEntity {
8
8
  prepaidSavingsPerShipment?: string;
9
9
  prepaidSavingsTotal?: string;
10
10
  prepaidExtraSavingsPercentage?: string;
11
+ hasPriceAdjustments?: boolean;
11
12
  }