@ordergroove/offers 2.46.0 → 2.46.1-alpha-PR-1280-4.55

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.
@@ -5,7 +5,9 @@ import {
5
5
  templatesSelector,
6
6
  makeProductSpecificDefaultFrequencySelector,
7
7
  makeProductPrepaidShipmentOptionsSelector,
8
- makeProductFrequenciesSelector
8
+ makeProductFrequenciesSelector,
9
+ makeDiscountedProductPriceSelector,
10
+ isShopifyDiscountFunctionInUseSelector
9
11
  } from '../selectors';
10
12
  import { stringifyFrequency } from '../api';
11
13
 
@@ -231,3 +233,405 @@ describe('makeProductFrequenciesSelector', () => {
231
233
  });
232
234
  });
233
235
  });
236
+
237
+ describe('makeDiscountedProductPriceSelector', () => {
238
+ /** @returns {import('../types/reducer').State} */
239
+ function makeTestState({
240
+ incentives = [
241
+ {
242
+ object: 'item',
243
+ field: 'total_price',
244
+ type: 'Discount Percent',
245
+ value: 11,
246
+ criteria: {
247
+ node_type: 'PREMISE',
248
+ standard: 'PROGRAM_WIDE'
249
+ }
250
+ }
251
+ ],
252
+ price = 1000,
253
+ currency = 'USD',
254
+ productId = 123
255
+ } = {}) {
256
+ return {
257
+ config: {
258
+ storeCurrency: currency
259
+ },
260
+ price: {
261
+ [productId]: { value: price }
262
+ },
263
+ incentives: {
264
+ [productId]: {
265
+ initial: incentives,
266
+ ongoing: []
267
+ }
268
+ }
269
+ };
270
+ }
271
+
272
+ it('applies Discount Percent incentive (USD)', () => {
273
+ const selector = makeDiscountedProductPriceSelector(123);
274
+ const result = selector(makeTestState());
275
+ expect(result).toEqual({
276
+ regularPrice: '$10.00',
277
+ discountRate: '11%',
278
+ subscriptionPrice: '$8.90'
279
+ });
280
+ });
281
+
282
+ it('applies Discount Amount incentive (USD)', () => {
283
+ const selector = makeDiscountedProductPriceSelector(123);
284
+ const result = selector(
285
+ makeTestState({
286
+ incentives: [
287
+ {
288
+ object: 'item',
289
+ field: 'total_price',
290
+ type: 'Discount Amount',
291
+ value: 2.5, // $2.50 off
292
+ criteria: {
293
+ node_type: 'PREMISE',
294
+ standard: 'PROGRAM_WIDE'
295
+ }
296
+ }
297
+ ]
298
+ })
299
+ );
300
+ expect(result).toEqual({
301
+ regularPrice: '$10.00',
302
+ discountRate: '$2.50',
303
+ subscriptionPrice: '$7.50'
304
+ });
305
+ });
306
+
307
+ it('handles Discount Amount larger than price', () => {
308
+ const selector = makeDiscountedProductPriceSelector(123);
309
+ const result = selector(
310
+ makeTestState({
311
+ incentives: [
312
+ {
313
+ object: 'item',
314
+ field: 'total_price',
315
+ type: 'Discount Amount',
316
+ value: 15, // $15.00 off
317
+ criteria: {
318
+ node_type: 'PREMISE',
319
+ standard: 'PROGRAM_WIDE'
320
+ }
321
+ }
322
+ ]
323
+ })
324
+ );
325
+ expect(result).toEqual({
326
+ regularPrice: '$10.00',
327
+ discountRate: '$10.00',
328
+ subscriptionPrice: '$0.00'
329
+ });
330
+ });
331
+
332
+ it('does not apply Discount Amount for non-USD currency', () => {
333
+ const selector = makeDiscountedProductPriceSelector(123);
334
+ const result = selector(
335
+ makeTestState({
336
+ incentives: [
337
+ {
338
+ object: 'item',
339
+ field: 'total_price',
340
+ type: 'Discount Amount',
341
+ value: 2.5,
342
+ criteria: {
343
+ node_type: 'PREMISE',
344
+ standard: 'PROGRAM_WIDE'
345
+ }
346
+ }
347
+ ],
348
+ currency: 'EUR'
349
+ })
350
+ );
351
+ expect(result).toEqual({
352
+ regularPrice: '€10.00',
353
+ discountRate: '€0.00', // no discount applied
354
+ subscriptionPrice: '€10.00'
355
+ });
356
+ });
357
+
358
+ it('applies Discount Percent for non-USD currency', () => {
359
+ const selector = makeDiscountedProductPriceSelector(123);
360
+ const result = selector(
361
+ makeTestState({
362
+ incentives: [
363
+ {
364
+ object: 'item',
365
+ field: 'total_price',
366
+ type: 'Discount Percent',
367
+ value: 20,
368
+ criteria: {
369
+ node_type: 'PREMISE',
370
+ standard: 'PROGRAM_WIDE'
371
+ }
372
+ }
373
+ ],
374
+ currency: 'GBP'
375
+ })
376
+ );
377
+ expect(result).toEqual({
378
+ regularPrice: '£10.00',
379
+ discountRate: '20%',
380
+ subscriptionPrice: '£8.00'
381
+ });
382
+ });
383
+
384
+ it('handles missing price for product', () => {
385
+ const selector = makeDiscountedProductPriceSelector(123);
386
+ const state = makeTestState({ productId: 123 });
387
+ delete state.price[123];
388
+
389
+ const result = selector(state);
390
+ expect(result).toEqual({});
391
+ });
392
+
393
+ it('handles empty incentives array', () => {
394
+ const selector = makeDiscountedProductPriceSelector(123);
395
+ const result = selector(makeTestState({ incentives: [] }));
396
+ expect(result).toEqual({
397
+ regularPrice: '$10.00',
398
+ discountRate: '$0.00',
399
+ subscriptionPrice: '$10.00'
400
+ });
401
+ });
402
+
403
+ it('applies 100% discount', () => {
404
+ const selector = makeDiscountedProductPriceSelector(123);
405
+ const result = selector(
406
+ makeTestState({
407
+ incentives: [
408
+ {
409
+ object: 'item',
410
+ field: 'total_price',
411
+ type: 'Discount Percent',
412
+ value: 100,
413
+ criteria: {
414
+ node_type: 'PREMISE',
415
+ standard: 'PROGRAM_WIDE'
416
+ }
417
+ }
418
+ ]
419
+ })
420
+ );
421
+ expect(result).toEqual({
422
+ regularPrice: '$10.00',
423
+ discountRate: '100%',
424
+ subscriptionPrice: '$0.00'
425
+ });
426
+ });
427
+
428
+ it('handles zero price', () => {
429
+ const selector = makeDiscountedProductPriceSelector(123);
430
+ const result = selector(
431
+ makeTestState({
432
+ price: 0
433
+ })
434
+ );
435
+ expect(result).toEqual({
436
+ regularPrice: '$0.00',
437
+ discountRate: '11%',
438
+ subscriptionPrice: '$0.00'
439
+ });
440
+ });
441
+
442
+ it('ignores unknown incentive types', () => {
443
+ const selector = makeDiscountedProductPriceSelector(123);
444
+ const result = selector(
445
+ makeTestState({
446
+ incentives: [
447
+ {
448
+ object: 'item',
449
+ field: 'total_price',
450
+ type: 'Unknown Type',
451
+ value: 50,
452
+ criteria: {
453
+ node_type: 'PREMISE',
454
+ standard: 'PROGRAM_WIDE'
455
+ }
456
+ }
457
+ ],
458
+ price: 1000
459
+ })
460
+ );
461
+ expect(result).toEqual({
462
+ regularPrice: '$10.00',
463
+ discountRate: '$0.00',
464
+ subscriptionPrice: '$10.00'
465
+ });
466
+ });
467
+
468
+ it('filters out irrelevant incentives', () => {
469
+ const selector = makeDiscountedProductPriceSelector(123);
470
+ const result = selector(
471
+ makeTestState({
472
+ incentives: [
473
+ {
474
+ object: 'order',
475
+ field: 'total_price',
476
+ type: 'Discount Percent',
477
+ value: 50,
478
+ criteria: {
479
+ node_type: 'PREMISE',
480
+ standard: 'PROGRAM_WIDE'
481
+ }
482
+ },
483
+ {
484
+ object: 'order',
485
+ field: 'shipping_total',
486
+ type: 'Discount Percent',
487
+ value: 25,
488
+ criteria: {
489
+ node_type: 'PREMISE',
490
+ standard: 'PROGRAM_WIDE'
491
+ }
492
+ },
493
+
494
+ {
495
+ object: 'item',
496
+ field: 'total_price',
497
+ type: 'Discount Percent',
498
+ value: 33,
499
+ criteria: {
500
+ node_type: 'PREMISE',
501
+ standard: 'PREPAID_ORDERS_PER_BILLING'
502
+ }
503
+ },
504
+ {
505
+ object: 'item',
506
+ field: 'total_price',
507
+ type: 'Discount Percent',
508
+ value: 37,
509
+ criteria: {
510
+ node_type: 'AND',
511
+ children: [
512
+ {
513
+ node_type: 'PREMISE',
514
+ standard: 'NTH_ORDER_FOR_SUBSCRIBER',
515
+ premise_value: 3,
516
+ premise_operand: 'GREATER_THAN'
517
+ },
518
+ {
519
+ node_type: 'PREMISE',
520
+ standard: 'ITEM_HAS_SUBSCRIPTION',
521
+ premise_value: true
522
+ }
523
+ ]
524
+ }
525
+ },
526
+ {
527
+ object: 'item',
528
+ field: 'total_price',
529
+ type: 'Discount Percent',
530
+ value: 10,
531
+ criteria: {
532
+ node_type: 'PREMISE',
533
+ standard: 'PROGRAM_WIDE'
534
+ }
535
+ }
536
+ ]
537
+ })
538
+ );
539
+ expect(result).toEqual({
540
+ regularPrice: '$10.00',
541
+ discountRate: '10%',
542
+ subscriptionPrice: '$9.00'
543
+ });
544
+ });
545
+
546
+ it('displays PSI incentive when available', () => {
547
+ const selector = makeDiscountedProductPriceSelector(123);
548
+ const result = selector(
549
+ makeTestState({
550
+ incentives: [
551
+ {
552
+ object: 'item',
553
+ field: 'total_price',
554
+ type: 'Discount Percent',
555
+ value: 15,
556
+ criteria: {
557
+ node_type: 'PREMISE',
558
+ standard: 'PSI'
559
+ }
560
+ }
561
+ ]
562
+ })
563
+ );
564
+ expect(result).toEqual({
565
+ regularPrice: '$10.00',
566
+ discountRate: '15%',
567
+ subscriptionPrice: '$8.50'
568
+ });
569
+ });
570
+
571
+ it('does not calculate a discount if no standardized incentives', () => {
572
+ const selector = makeDiscountedProductPriceSelector(123);
573
+ const result = selector(
574
+ makeTestState({
575
+ incentives: [
576
+ {
577
+ object: 'item',
578
+ field: 'total_price',
579
+ type: 'Discount Percent',
580
+ value: 50
581
+ }
582
+ ]
583
+ })
584
+ );
585
+ expect(result).toEqual({
586
+ regularPrice: '$10.00',
587
+ discountRate: '$0.00',
588
+ subscriptionPrice: '$10.00'
589
+ });
590
+ });
591
+ });
592
+
593
+ describe('isShopifyDiscountFunctionInUseSelector', () => {
594
+ it('should return false when productPlans is an empty object', () => {
595
+ const state = { productPlans: {} };
596
+ expect(isShopifyDiscountFunctionInUseSelector(state)).toBe(false);
597
+ });
598
+
599
+ it('should return false when productPlans has product with empty array', () => {
600
+ const state = { productPlans: { 123: [] } };
601
+ expect(isShopifyDiscountFunctionInUseSelector(state)).toBe(false);
602
+ });
603
+
604
+ it('should return false when some product plan does not have hasPriceAdjustments property', () => {
605
+ const state = {
606
+ productPlans: {
607
+ 123: [{ id: 'plan-1' }],
608
+ 456: [{ id: 'plan-2', hasPriceAdjustments: false }]
609
+ }
610
+ };
611
+ expect(isShopifyDiscountFunctionInUseSelector(state)).toBe(false);
612
+ });
613
+
614
+ it('should return false when some product plan has hasPriceAdjustments = true', () => {
615
+ const state = {
616
+ productPlans: {
617
+ 123: [{ id: 'plan-1', hasPriceAdjustments: false }],
618
+ 456: [{ id: 'plan-2', hasPriceAdjustments: true }]
619
+ }
620
+ };
621
+ expect(isShopifyDiscountFunctionInUseSelector(state)).toBe(false);
622
+ });
623
+
624
+ it('should return true when all product plans have hasPriceAdjustments = false or are prepaid', () => {
625
+ const state = {
626
+ productPlans: {
627
+ 123: [{ id: 'plan-1', hasPriceAdjustments: false }],
628
+ 456: [
629
+ { id: 'plan-2', hasPriceAdjustments: false },
630
+ { id: 'plan-3', hasPriceAdjustments: false },
631
+ { id: 'plan-3', hasPriceAdjustments: true, prepaidShipments: 3 }
632
+ ]
633
+ }
634
+ };
635
+ expect(isShopifyDiscountFunctionInUseSelector(state)).toBe(true);
636
+ });
637
+ });
@@ -45,6 +45,8 @@ export const getProductsForPurchasePost = (state = {}, productIds = []) =>
45
45
  * discountRate: '10%'
46
46
  * },
47
47
  * ]
48
+ *
49
+ * @returns {import('./types/reducer').ProductPlansState}
48
50
  */
49
51
  export const getObjectStructuredProductPlans = (productPlans = {}) => {
50
52
  const adaptedProductPlans = {};
@@ -54,6 +54,13 @@ export const ENV_PROD = 'prod';
54
54
  export const STATIC_HOST = 'static.ordergroove.com';
55
55
  export const STAGING_STATIC_HOST = 'staging.static.ordergroove.com';
56
56
 
57
+ export const INCENTIVE_STANDARD_TYPES = {
58
+ PSI: 'PSI',
59
+ // note: this is a "pseudo-standard" that we set in our own code
60
+ // the API actually returns no criteria for program wide incentives
61
+ PROGRAM_WIDE: 'PROGRAM_WIDE'
62
+ };
63
+
57
64
  /**
58
65
  * @event
59
66
  * Events that fires once optin/optout occurs on a cart offer
@@ -71,8 +71,9 @@ export function experimentsReducer(state = {}, action) {
71
71
  * object.
72
72
  */
73
73
  function resolveShopifySetupProductWhenExperiment(variant, product, experimentSettings) {
74
- // So, on the sellingPlanGroup appId we are doing "ordergroove-subscribe-and-save-{experiment_variant_id}"
75
- // this logic will only be enabled if we are in an experiment, there are at least 2 groups in the OG selling plan group
74
+ // When an experiment is running, you may have multiple selling plan groups with the app_id "ordergroove-subscribe-and-save-{experiment_variant_id}"
75
+ // We want offers to use only the selling plan group that matches the assigned experiment variant
76
+ // Note: whether or not you have variant-specific selling plan groups depends on if the Shopify Discount Function is being used. If it is, then the selling plan groups do not contain price adjustments and there is only a single selling plan group (all variants use the same group).
76
77
  if (!variant) return;
77
78
 
78
79
  if (experimentSettings.variants.length === 0) return;
@@ -10,15 +10,19 @@ import {
10
10
  AutoshipByDefaultState,
11
11
  AutoshipEligibleState,
12
12
  ConfigState,
13
+ Incentive,
13
14
  IncentiveObject,
14
15
  IncentivesState,
15
16
  NextUpcomingOrderState,
16
17
  OptedInState,
17
18
  OptedOutState,
18
19
  PrepaidShipmentsSelectedState,
20
+ PriceState,
21
+ ProductPlansState,
19
22
  ReceiveOfferPayload
20
23
  } from './types/reducer';
21
24
  import { EmptyObject } from './types/utility';
25
+ import { IncentiveDisplay, IncentivesDisplayEnhanced } from './types/api';
22
26
 
23
27
  export const optedin = (state: OptedInState = [], action): OptedInState => {
24
28
  switch (action.type) {
@@ -160,11 +164,28 @@ export const eligibilityGroups = (state = {}, action) => {
160
164
  }
161
165
  };
162
166
 
163
- const mapIncentive = (incentive, incentiveDisplay) => {
164
- return incentive.map(i => ({
165
- ...incentiveDisplay[i],
166
- id: [i][0]
167
- }));
167
+ const mapIncentive = (
168
+ incentive: string[],
169
+ incentiveDisplay: IncentiveDisplay,
170
+ incentiveDisplayEnhanced?: IncentivesDisplayEnhanced
171
+ ): Incentive[] => {
172
+ return incentive.map(i => {
173
+ const enhanced = incentiveDisplayEnhanced?.[i];
174
+ return {
175
+ ...incentiveDisplay[i],
176
+ // for standard incentives, include the criteria so we know which kind of incentive (e.g. PSI, prepaid, etc)
177
+ ...(enhanced
178
+ ? {
179
+ criteria: enhanced.criteria
180
+ ? enhanced.criteria
181
+ : // when there is no criteria in the enhanced incentive, it means it's a program wide incentive
182
+ // for ease-of-use, we set use a "PROGRAM_WIDE" pseudo-standard here
183
+ { node_type: 'PREMISE', standard: constants.INCENTIVE_STANDARD_TYPES.PROGRAM_WIDE, premise_value: null }
184
+ }
185
+ : {}),
186
+ id: [i][0]
187
+ };
188
+ });
168
189
  };
169
190
 
170
191
  export const incentives = (
@@ -185,11 +206,19 @@ export const incentives = (
185
206
  ...incentiveObj,
186
207
  initial: [
187
208
  ...(incentiveObj.initial || []),
188
- ...mapIncentive(initial, action.payload.incentives_display)
209
+ ...mapIncentive(
210
+ initial,
211
+ action.payload.incentives_display,
212
+ action.payload.incentives_display_enhanced
213
+ )
189
214
  ],
190
215
  ongoing: [
191
216
  ...(incentiveObj.ongoing || []),
192
- ...mapIncentive(ongoing, action.payload.incentives_display)
217
+ ...mapIncentive(
218
+ ongoing,
219
+ action.payload.incentives_display,
220
+ action.payload.incentives_display_enhanced
221
+ )
193
222
  ]
194
223
  }),
195
224
  {}
@@ -501,7 +530,7 @@ export const templates = (state = [], action) => {
501
530
  }
502
531
  };
503
532
 
504
- export const productPlans = (state = {}, action) => {
533
+ export const productPlans = (state: ProductPlansState = {}, action): ProductPlansState => {
505
534
  switch (action.type) {
506
535
  case constants.RECEIVE_PRODUCT_PLANS:
507
536
  return getObjectStructuredProductPlans(action.payload);
@@ -533,6 +562,8 @@ export const prepaidShipmentsSelected = (
533
562
  }
534
563
  };
535
564
 
565
+ export const price = (state: PriceState = {}, _action) => state;
566
+
536
567
  export default combineReducers({
537
568
  optedin,
538
569
  optedout,
@@ -561,5 +592,6 @@ export default combineReducers({
561
592
  defaultFrequencies,
562
593
  templates,
563
594
  productPlans,
564
- prepaidShipmentsSelected
595
+ prepaidShipmentsSelected,
596
+ price
565
597
  });
@@ -3,7 +3,9 @@ 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, ProductFrequencyConfig, State } from './types/reducer';
6
+ import { Incentive, OfferElement, ProductFrequencyConfig, State } from './types/reducer';
7
+ import { money, percentage } from '../shopify/utils';
8
+ import { INCENTIVE_STANDARD_TYPES } from './constants';
7
9
 
8
10
  memoize.Cache = Map;
9
11
 
@@ -230,6 +232,59 @@ export const makeFrequencyForPrepaidShipmentsSelector = (product: BaseProduct, p
230
232
  }
231
233
  );
232
234
 
235
+ /** 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
+ export const makeDiscountedProductPriceSelector = memoize((productId: string) =>
237
+ createSelector(
238
+ (state: State) => state.price || {},
239
+ (state: State) => state.incentives || {},
240
+ (state: State) => state.config.storeCurrency,
241
+ (prices, incentives, currency) => {
242
+ const productPriceObj = prices[safeProductId(productId)];
243
+ if (productPriceObj === undefined || productPriceObj === null || !currency) return {};
244
+
245
+ const productPrice = productPriceObj.value;
246
+ let regularPrice = productPrice;
247
+ let subscriptionPrice = productPrice;
248
+
249
+ const productIncentives = incentives[safeProductId(productId)];
250
+ const incentive = productIncentives?.initial.find(findRelevantIncentive);
251
+
252
+ let formatted_discount = '';
253
+
254
+ if (incentive) {
255
+ if (incentive.type === 'Discount Percent') {
256
+ // note: productPrice is in cents ($10 => 1000), so we round to the nearest whole number after applying the discount
257
+ subscriptionPrice = Math.round((productPrice * (100 - incentive.value)) / 100);
258
+ formatted_discount = percentage(incentive.value);
259
+ } else if (incentive.type === 'Discount Amount' && currency === 'USD') {
260
+ // for now, we only support USD for "dollar-off" discounts
261
+ // productPrice is in cents, while the incentive value is in dollars, so we multiply by 100
262
+ subscriptionPrice = Math.max(0, productPrice - Math.round(incentive.value * 100));
263
+ }
264
+ }
265
+ return {
266
+ regularPrice: money(regularPrice, currency),
267
+ subscriptionPrice: money(subscriptionPrice, currency),
268
+ discountRate: formatted_discount || money(regularPrice - subscriptionPrice, currency)
269
+ };
270
+ }
271
+ )
272
+ );
273
+
274
+ const validIncentiveStandards = [INCENTIVE_STANDARD_TYPES.PROGRAM_WIDE, INCENTIVE_STANDARD_TYPES.PSI];
275
+
276
+ function findRelevantIncentive(incentive: Incentive) {
277
+ return (
278
+ incentive.object === 'item' &&
279
+ (incentive.type === 'Discount Percent' || incentive.type === 'Discount Amount') &&
280
+ // only attempt to determine a discount if the incentive is standardized, i.e. we have a criteria object
281
+ incentive.criteria &&
282
+ // note: the API should return either a PSI or a program-wide, not both
283
+ incentive.criteria.node_type === 'PREMISE' &&
284
+ validIncentiveStandards.includes(incentive.criteria.standard)
285
+ );
286
+ }
287
+
233
288
  /**
234
289
  * Convert a string from camel case to kebab case.
235
290
  */
@@ -246,3 +301,13 @@ export const getFallbackValue = (element: HTMLElement & { offer: OfferElement },
246
301
  * Returns a list of opted in products id from the state
247
302
  */
248
303
  export const templatesSelector = (state: State) => ({ templates: state.templates || [] });
304
+
305
+ /**
306
+ * Returns true if no selling plan has price adjustments (except for prepaid, which still use price adjustments). This means that we are calculating the subscription discount using a Shopify Discount Function instead of that information being stored in the selling plan.
307
+ * Generally, the Shopify Discount Function is used when the merchant is using standard flex incentives, i.e. the offer profile is standardized
308
+ */
309
+ export const isShopifyDiscountFunctionInUseSelector = (state: State) => {
310
+ const plans = Object.values(state.productPlans).flat();
311
+
312
+ return plans.length > 0 && plans.every(plan => plan.hasPriceAdjustments === false || plan.prepaidShipments);
313
+ };
@@ -32,6 +32,19 @@ export type Incentive = {
32
32
  value: number;
33
33
  };
34
34
 
35
+ export type StandardIncentive = {
36
+ incentive_target: string;
37
+ incentive_type: string;
38
+ incentive_value: string;
39
+ threshold_field: string | null;
40
+ threshold_value: string | null;
41
+ criteria?: {
42
+ node_type: string;
43
+ premise_value: unknown;
44
+ standard: string;
45
+ };
46
+ };
47
+
35
48
  type ExperimentVariant = {
36
49
  public_id: string;
37
50
  parameters: any;
@@ -67,5 +80,10 @@ export type OfferResponse = {
67
80
  initial: string[];
68
81
  }
69
82
  >;
70
- incentives_display: Record<string, Incentive>;
83
+ incentives_display: IncentiveDisplay;
84
+ // only present for standardized offer profiles
85
+ incentives_display_enhanced?: IncentivesDisplayEnhanced;
71
86
  };
87
+
88
+ export type IncentiveDisplay = Record<string, Incentive>;
89
+ export type IncentivesDisplayEnhanced = Record<string, StandardIncentive>;