@ordergroove/offers 2.46.0 → 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.
@@ -2,6 +2,7 @@ import { Order, Incentive as ApiIncentive, MerchantSettings, OfferResponse } fro
2
2
  import { type Offer } from '../../components/Offer';
3
3
  import { ShopifyCart, ShopifyProductEntity } from '../../shopify/types/shopify';
4
4
  import reducer from '../reducer';
5
+ import { ProductPlanEntity } from '../../shopify/types/productPlan';
5
6
 
6
7
  export type State = ReturnType<typeof reducer>;
7
8
 
@@ -16,8 +17,16 @@ export type NextUpcomingOrderState = Partial<
16
17
 
17
18
  export type IncentivesState = Record<string, IncentiveObject>;
18
19
 
19
- type Incentive = ApiIncentive & {
20
+ export type Incentive = ApiIncentive & {
20
21
  id: string;
22
+ /**
23
+ * undefined when the offer profile is not standardized
24
+ */
25
+ criteria?: {
26
+ node_type: string;
27
+ premise_value: unknown;
28
+ standard: string;
29
+ };
21
30
  };
22
31
 
23
32
  export type IncentiveObject = {
@@ -68,6 +77,10 @@ export type OptedOutState = { id: string }[];
68
77
 
69
78
  export type AutoshipByDefaultState = Record<string, boolean>;
70
79
 
80
+ export type PriceState = { [productId: string]: { value: number } };
81
+
82
+ export type ProductPlansState = { [productId: string]: ProductPlanEntity[] };
83
+
71
84
  // payload types
72
85
 
73
86
  export type ReceiveOfferPayload = OfferResponse & {
@@ -308,7 +308,7 @@ describe('Shopify productPlan Reducer', () => {
308
308
 
309
309
  const productPlanCreated = mapSellingPlanToDiscount(allocation, [], 'USD');
310
310
 
311
- expect(productPlanCreated).toEqual(expectedProductPlan);
311
+ expect(productPlanCreated).toEqual(jasmine.objectContaining(expectedProductPlan));
312
312
  });
313
313
 
314
314
  it('should create prepaid product plan', () => {
@@ -371,7 +371,7 @@ describe('Shopify productPlan Reducer', () => {
371
371
 
372
372
  const productPlanCreated = mapSellingPlanToDiscount(allocation, sellingPlans, 'USD');
373
373
 
374
- expect(productPlanCreated).toEqual(expectedProductPlan);
374
+ expect(productPlanCreated).toEqual(jasmine.objectContaining(expectedProductPlan));
375
375
  });
376
376
 
377
377
  it('should create prepaid product plan that rounds subscriptionPrice to nearest decimal', () => {
@@ -512,7 +512,7 @@ describe('Shopify productPlan Reducer', () => {
512
512
 
513
513
  const productPlanCreated = mapSellingPlanToDiscount(allocation, sellingPlans, 'USD');
514
514
 
515
- expect(productPlanCreated).toEqual(expectedProductPlan);
515
+ expect(productPlanCreated).toEqual(jasmine.objectContaining(expectedProductPlan));
516
516
  });
517
517
  });
518
518
 
@@ -7,7 +7,12 @@ import {
7
7
  PRODUCT_CHANGE_PREPAID_SHIPMENTS,
8
8
  SETUP_CART
9
9
  } from '../../core/constants';
10
- import { synchronizeCartOptin, getTrackingEvent, guessProductHandle } from '../shopifyMiddleware';
10
+ import {
11
+ synchronizeCartOptin,
12
+ getTrackingEvent,
13
+ guessProductHandle,
14
+ synchronizeSellingPlan
15
+ } from '../shopifyMiddleware';
11
16
  import { getOrCreateHidden } from '../../core/utils';
12
17
 
13
18
  function makeForm(addInput = true) {
@@ -92,7 +97,7 @@ describe('getTrackingEvent', () => {
92
97
  });
93
98
 
94
99
  describe('synchronizeCartOptin', () => {
95
- let store, offer, frequency, product;
100
+ let store, offer, frequency, product, defaultState;
96
101
  beforeEach(() => {
97
102
  store = {
98
103
  dispatch: jasmine.createSpy('dispatch'),
@@ -106,6 +111,23 @@ describe('synchronizeCartOptin', () => {
106
111
  frequency = '1234';
107
112
  product = { id: '38995975209111:original-hash' };
108
113
 
114
+ defaultState = {
115
+ optedin: [],
116
+ offerId: 'offer-id-1234',
117
+ productPlans: {
118
+ 38995975209111: [
119
+ {
120
+ frequency: '688815178030'
121
+ },
122
+ {
123
+ frequency: '1234'
124
+ }
125
+ ]
126
+ }
127
+ };
128
+
129
+ store.getState.and.returnValue(defaultState);
130
+
109
131
  fetchMock.route(
110
132
  '/cart.js',
111
133
  {
@@ -134,12 +156,64 @@ describe('synchronizeCartOptin', () => {
134
156
  { method: 'POST', repeat: 1 }
135
157
  );
136
158
 
159
+ fetchMock.route(
160
+ '/cart/update.js',
161
+ {
162
+ attributes: {}
163
+ },
164
+ { method: 'POST', repeat: 1 }
165
+ );
166
+
137
167
  fetchMock.mockGlobal();
168
+
169
+ // mock the date so that the tracking key is predictable
170
+ jasmine.clock().install();
171
+ jasmine.clock().mockDate(new Date('2025-01-01T00:00:00.000Z'));
138
172
  });
139
173
 
140
174
  afterEach(() => {
141
175
  fetchMock.removeRoutes().unmockGlobal();
142
176
  offer.remove();
177
+
178
+ jasmine.clock().uninstall();
179
+ });
180
+
181
+ it('should update cart attributes', async () => {
182
+ await synchronizeCartOptin({ type: OPTIN_PRODUCT, payload: { offer, frequency, product } }, store);
183
+
184
+ const lastUpdateCall = fetchMock.callHistory.calls('/cart/update.js').at(-1);
185
+ const body = JSON.parse(lastUpdateCall.options.body);
186
+
187
+ expect(body).toEqual({
188
+ attributes: {
189
+ og__1735689600: '38995975209111:original-hash,optin_product,,1234,'
190
+ }
191
+ });
192
+ });
193
+
194
+ it('should update cart attributes when Shopify Discount Function in use', async () => {
195
+ store.getState.and.returnValue({
196
+ ...defaultState,
197
+ productPlans: {
198
+ 38995975209111: [
199
+ {
200
+ frequency: '688815178030',
201
+ hasPriceAdjustments: false
202
+ }
203
+ ]
204
+ }
205
+ });
206
+ await synchronizeCartOptin({ type: OPTIN_PRODUCT, payload: { offer, frequency, product } }, store);
207
+
208
+ const lastUpdateCall = fetchMock.callHistory.calls('/cart/update.js').at(-1);
209
+ const body = JSON.parse(lastUpdateCall.options.body);
210
+
211
+ expect(body).toEqual({
212
+ attributes: {
213
+ og__1735689600: '38995975209111:original-hash,optin_product,,1234,',
214
+ __ordergroove_offer_id: 'offer-id-1234'
215
+ }
216
+ });
143
217
  });
144
218
 
145
219
  it('should set new has product id', async () => {
@@ -173,7 +247,9 @@ describe('synchronizeCartOptin', () => {
173
247
  document.body.appendChild(sectionDiv);
174
248
 
175
249
  await synchronizeCartOptin({ type: OPTIN_PRODUCT, payload: { offer, frequency, product } }, store);
176
- expect(fetchMock.callHistory.lastCall().options.body).toContain('"sections":["123456789__cart-items"]');
250
+ expect(fetchMock.callHistory.calls('/cart/change.js').at(-1).options.body).toContain(
251
+ '"sections":["123456789__cart-items"]'
252
+ );
177
253
  sectionDiv.remove();
178
254
  });
179
255
 
@@ -185,12 +261,15 @@ describe('synchronizeCartOptin', () => {
185
261
  document.body.appendChild(sectionDiv);
186
262
 
187
263
  await synchronizeCartOptin({ type: OPTIN_PRODUCT, payload: { offer, frequency, product } }, store);
188
- expect(fetchMock.callHistory.lastCall().options.body).toContain('"sections":["123456789__cart-footer"]');
264
+ expect(fetchMock.callHistory.calls('/cart/change.js').at(-1).options.body).toContain(
265
+ '"sections":["123456789__cart-footer"]'
266
+ );
189
267
  sectionDiv.remove();
190
268
  });
191
269
 
192
270
  it('should get the subscribed frequency when no frequency is provided in the action payload', async () => {
193
271
  store.getState.and.returnValue({
272
+ ...defaultState,
194
273
  optedin: [
195
274
  {
196
275
  id: '38995975209111:original-hash',
@@ -223,11 +302,14 @@ describe('synchronizeCartOptin', () => {
223
302
  }
224
303
  });
225
304
 
226
- expect(fetchMock.callHistory.lastCall().options.body).toContain('"selling_plan":"688815178030"');
305
+ expect(fetchMock.callHistory.calls('/cart/change.js').at(-1).options.body).toContain(
306
+ '"selling_plan":"688815178030"'
307
+ );
227
308
  });
228
309
 
229
310
  it('should have no selling plan when product is opted out', async () => {
230
311
  store.getState.and.returnValue({
312
+ ...defaultState,
231
313
  optedin: []
232
314
  });
233
315
 
@@ -254,7 +336,7 @@ describe('synchronizeCartOptin', () => {
254
336
  }
255
337
  });
256
338
 
257
- expect(fetchMock.callHistory.lastCall().options.body).toContain('"selling_plan":null');
339
+ expect(fetchMock.callHistory.calls('/cart/change.js').at(-1).options.body).toContain('"selling_plan":null');
258
340
  });
259
341
  });
260
342
 
@@ -291,3 +373,142 @@ describe('guessProductHandle', () => {
291
373
  expect(guessProductHandle()).toEqual('some-meta-json');
292
374
  });
293
375
  });
376
+
377
+ describe('synchronizeSellingPlan', () => {
378
+ const testProduct = '123456';
379
+ const testSellingPlan = '688815178030';
380
+ const testSessionId = 'session-123';
381
+ const testOfferId = 'offer-id-1234';
382
+
383
+ function makeCartAddForm(productId = testProduct) {
384
+ const element = document.createElement('form');
385
+ element.action = '/cart/add';
386
+ element.innerHTML = `
387
+ <input value="${productId}" name="id" type="hidden">
388
+ `;
389
+ document.body.appendChild(element);
390
+ return element;
391
+ }
392
+
393
+ function getFormData(formElement) {
394
+ const data = Object.fromEntries(new FormData(formElement));
395
+ delete data.id;
396
+ return data;
397
+ }
398
+
399
+ let store;
400
+ let offerElement = {
401
+ isCart: false,
402
+ shouldEnableOffer: true
403
+ };
404
+
405
+ beforeEach(() => {
406
+ document.body.innerHTML = '';
407
+ store = {
408
+ getState: jasmine.createSpy('getState')
409
+ };
410
+ store.getState.and.returnValue({
411
+ sessionId: testSessionId,
412
+ optedin: [{ id: testProduct, frequency: testSellingPlan }],
413
+ productPlans: {},
414
+ offerId: testOfferId
415
+ });
416
+ });
417
+
418
+ afterEach(() => {
419
+ document.body.innerHTML = '';
420
+ });
421
+
422
+ it('should add attributes when product is opted in', () => {
423
+ const formElement = makeCartAddForm();
424
+ expect(getFormData(formElement)).toEqual({});
425
+
426
+ synchronizeSellingPlan(store, offerElement);
427
+
428
+ expect(getFormData(formElement)).toEqual({
429
+ selling_plan: testSellingPlan,
430
+ 'attributes[og__session]': testSessionId
431
+ });
432
+ });
433
+
434
+ it('should add attributes when product is opted in and Shopify Discount Function is in use', () => {
435
+ store.getState.and.returnValue({
436
+ ...store.getState(),
437
+ productPlans: {
438
+ [testProduct]: [
439
+ {
440
+ frequency: testSellingPlan,
441
+ hasPriceAdjustments: false
442
+ }
443
+ ]
444
+ }
445
+ });
446
+
447
+ const formElement = makeCartAddForm();
448
+ expect(getFormData(formElement)).toEqual({});
449
+
450
+ synchronizeSellingPlan(store, offerElement);
451
+
452
+ expect(getFormData(formElement)).toEqual({
453
+ selling_plan: testSellingPlan,
454
+ 'attributes[og__session]': testSessionId,
455
+ 'attributes[__ordergroove_offer_id]': testOfferId
456
+ });
457
+ });
458
+
459
+ it('should handle multiple forms on the page', () => {
460
+ const formElement1 = makeCartAddForm(testProduct);
461
+ const formElement2 = makeCartAddForm(testProduct);
462
+
463
+ synchronizeSellingPlan(store, offerElement);
464
+
465
+ expect(getFormData(formElement1)).toEqual({
466
+ selling_plan: testSellingPlan,
467
+ 'attributes[og__session]': testSessionId
468
+ });
469
+ expect(getFormData(formElement2)).toEqual({
470
+ selling_plan: testSellingPlan,
471
+ 'attributes[og__session]': testSessionId
472
+ });
473
+ });
474
+
475
+ it('should remove selling_plan input when product is not opted in', () => {
476
+ store.getState.and.returnValue({
477
+ ...store.getState(),
478
+ sessionId: testSessionId,
479
+ optedin: []
480
+ });
481
+
482
+ const formElement = makeCartAddForm();
483
+ // Add an existing selling_plan input
484
+ const existingInput = document.createElement('input');
485
+ existingInput.name = 'selling_plan';
486
+ existingInput.value = testSellingPlan;
487
+ existingInput.type = 'hidden';
488
+ formElement.appendChild(existingInput);
489
+
490
+ expect(getFormData(formElement).selling_plan).toEqual(testSellingPlan);
491
+
492
+ synchronizeSellingPlan(store, offerElement);
493
+
494
+ expect(getFormData(formElement).selling_plan).toEqual(undefined);
495
+ });
496
+
497
+ it('should not run when offerElement.isCart is true', () => {
498
+ const formElement = makeCartAddForm();
499
+ const offerElement = { isCart: true, shouldEnableOffer: true };
500
+
501
+ synchronizeSellingPlan(store, offerElement);
502
+
503
+ expect(getFormData(formElement)).toEqual({});
504
+ });
505
+
506
+ it('should not run when offerElement.shouldEnableOffer is false', () => {
507
+ const formElement = makeCartAddForm();
508
+ const offerElement = { isCart: false, shouldEnableOffer: false };
509
+
510
+ synchronizeSellingPlan(store, offerElement);
511
+
512
+ expect(getFormData(formElement)).toEqual({});
513
+ });
514
+ });
@@ -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)) {