ti2 1.0.123 → 1.0.124

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/README.md CHANGED
@@ -73,11 +73,12 @@ calling the integration; `cacheFound` distinguishes a cache miss from a cached
73
73
  empty catalog.
74
74
 
75
75
  A forced request is a full-catalog refresh by default. `fullSyncTrigger` may be
76
- `scheduled`, `manual`, or `organic`; scheduled and manual refreshes reject
77
- `searchInput`, `optionId`, `productId`, `productName`, or `lastUpdatedFrom`
78
- selectors. An empty `searchInput` or `searchInput: "*"` represents the full
79
- catalog. Scheduler-owned requests may also carry `fullSyncStartedAt`,
80
- `fullSyncAdmissionToken`, and an `admissionOverrideReason`.
76
+ `scheduled`, `manual`, or `organic`; an omitted trigger on a forced request is
77
+ treated as manual. Manual refreshes require a non-blank `admissionOverrideReason`.
78
+ Scheduled and manual refreshes reject `searchInput`, `optionId`, `productId`,
79
+ `productName`, or `lastUpdatedFrom` selectors. An empty `searchInput` or
80
+ `searchInput: "*"` represents the full catalog. Scheduler-owned requests may
81
+ also carry `fullSyncStartedAt` and `fullSyncAdmissionToken`.
81
82
 
82
83
  Unscoped forced responses include `catalogRefreshOutcome`, `cacheUpdated`,
83
84
  `cachePreserved`, and `cachedProductCount`. Consumers that update a downstream
package/api.yml CHANGED
@@ -455,7 +455,7 @@ components:
455
455
  forceRefresh:
456
456
  type: boolean
457
457
  default: false
458
- description: Call the plugin directly. Full-catalog refreshes require the app's weeklyProductCatalogSync capability and default to a scheduled trigger when fullSyncTrigger is omitted.
458
+ description: Call the plugin directly. Full-catalog refreshes require the app's weeklyProductCatalogSync capability and default to a manual trigger when fullSyncTrigger is omitted. Manual refreshes require admissionOverrideReason.
459
459
  cacheOnly:
460
460
  type: boolean
461
461
  default: false
@@ -466,14 +466,14 @@ components:
466
466
  description: Execute the request through the Ti2 worker.
467
467
  fullSyncTrigger:
468
468
  type: string
469
- description: Catalog refresh owner. Scheduled and manual triggers reject selector fields.
469
+ description: Catalog refresh owner. Forced requests default to manual when omitted. Scheduled and manual triggers reject selector fields.
470
470
  enum:
471
471
  - organic
472
472
  - scheduled
473
473
  - manual
474
474
  admissionOverrideReason:
475
475
  type: string
476
- description: Audit reason for an admitted manual refresh override.
476
+ description: Required non-blank audit reason for a manual refresh override.
477
477
  fullSyncStartedAt:
478
478
  type: number
479
479
  description: Start time supplied by the full-sync admission owner.
@@ -29,6 +29,7 @@ The `$bookingsProductSearch` function in `controllers/bookings.js` implements a
29
29
  * **Condition 3: `forceRefresh` is true**:
30
30
  * This is the catalog refresh owner. The system fetches fresh data from the plugin.
31
31
  * Ti2 rejects the request unless the host declared `weeklyProductCatalogSync: true` for the plugin. Manual audit reasons may override timing admission, but not catalog-completeness eligibility.
32
+ * An omitted `fullSyncTrigger` is treated as `manual`, and manual refreshes require a non-blank `admissionOverrideReason`. Weekly scheduler requests must explicitly send `fullSyncTrigger: scheduled`.
32
33
  * Unless `fullSyncTrigger: organic` is explicit, a force refresh is a scheduled or manual full-catalog request and rejects `searchInput`, `optionId`, `productId`, `productName`, and `lastUpdatedFrom` selectors. `searchInput: "*"` is unscoped.
33
34
  * The `fetchFromPluginAndCache` helper function is invoked. This function:
34
35
  * Sets the `pluginExecutionLockKey` before calling the plugin to prevent other concurrent direct calls.
@@ -134,7 +134,7 @@ describe('user: bookings controller - searchProducts', () => {
134
134
  const response = await doApiPost({
135
135
  url: `/products/${testAppName}/${testUserId}/${testHint}/search`,
136
136
  token: userToken,
137
- payload: { forceRefresh: true },
137
+ payload: { forceRefresh: true, fullSyncTrigger: 'scheduled' },
138
138
  expectStatusCode: 500,
139
139
  });
140
140
 
@@ -146,6 +146,7 @@ describe('user: bookings controller - searchProducts', () => {
146
146
  // NOTE: we SHOULD NOT need to remove the cache first, since we are forceRefreshing, we are testing the endpoint get's called while having a cache created
147
147
  const payload = {
148
148
  forceRefresh: true,
149
+ fullSyncTrigger: 'scheduled',
149
150
  };
150
151
  const { products } = await doApiPost({
151
152
  url: `/products/${testAppName}/${testUserId}/${testHint}/search`,
@@ -253,7 +254,10 @@ describe('user: bookings controller - searchProducts', () => {
253
254
  await doApiPost({
254
255
  url: `/products/${testAppName}/${testUserId}/${testHint}/search`,
255
256
  token: userToken,
256
- payload: { forceRefresh: true },
257
+ payload: {
258
+ forceRefresh: true,
259
+ admissionOverrideReason: 'legacy manual refresh',
260
+ },
257
261
  });
258
262
  const cacheSavePayload = emitSpy.mock.calls
259
263
  .filter(call => call[0] === 'bookingsProductSearch:cache:save')
@@ -265,6 +269,8 @@ describe('user: bookings controller - searchProducts', () => {
265
269
  expect(cacheSavePayload.hint).toBe(testHint);
266
270
  expect(cacheSavePayload.userIdHash).toBeTruthy();
267
271
  expect(cacheSavePayload.hintHash).toBeTruthy();
272
+ expect(cacheSavePayload.fullSyncTrigger).toBe('manual');
273
+ expect(cacheSavePayload.admissionOverrideReason).toBe('legacy manual refresh');
268
274
  emitSpy.mockRestore();
269
275
  });
270
276
 
@@ -355,7 +361,7 @@ describe('user: bookings controller - searchProducts', () => {
355
361
  await doApiPost({
356
362
  url: `/products/${testAppName}/${testUserId}/${doNotCallHint}/search`,
357
363
  token: userToken,
358
- payload: { forceRefresh: true },
364
+ payload: { forceRefresh: true, fullSyncTrigger: 'scheduled' },
359
365
  }).then(({ products: p }) => {
360
366
  products = p; // Assign to the outer scoped 'products'
361
367
  });
@@ -415,7 +421,11 @@ describe('user: bookings controller - searchProducts', () => {
415
421
  await doApiPost({
416
422
  url: `/products/${testAppName}/${testUserId}/${configuredOmitHint}/search`,
417
423
  token: userToken,
418
- payload: { forceRefresh: true, omitServiceCodes: [] },
424
+ payload: {
425
+ forceRefresh: true,
426
+ fullSyncTrigger: 'scheduled',
427
+ omitServiceCodes: [],
428
+ },
419
429
  });
420
430
 
421
431
  expect(travelgatePlugin.searchProducts).toHaveBeenCalledTimes(1);
@@ -642,7 +652,7 @@ describe('user: bookings controller - searchProducts', () => {
642
652
  const result = await doApiPost({
643
653
  url: `/products/${testAppName}/${testUserId}/${staleCacheTestHint}/search`,
644
654
  token: userToken,
645
- payload: { forceRefresh: true },
655
+ payload: { forceRefresh: true, fullSyncTrigger: 'scheduled' },
646
656
  });
647
657
 
648
658
  const cached = await cache.get({ pluginName: testAppName, key: cacheKeyForTest });
@@ -687,7 +697,7 @@ describe('user: bookings controller - searchProducts', () => {
687
697
  await doApiPost({
688
698
  url: `/products/${testAppName}/${testUserId}/${staleCacheTestHint}/search`,
689
699
  token: userToken,
690
- payload: { forceRefresh: true },
700
+ payload: { forceRefresh: true, fullSyncTrigger: 'scheduled' },
691
701
  });
692
702
 
693
703
  const cached = await cache.get({ pluginName: testAppName, key: cacheKeyForTest });
@@ -725,7 +735,7 @@ describe('user: bookings controller - searchProducts', () => {
725
735
  const result = await doApiPost({
726
736
  url: `/products/${testAppName}/${testUserId}/${staleCacheTestHint}/search`,
727
737
  token: userToken,
728
- payload: { forceRefresh: true },
738
+ payload: { forceRefresh: true, fullSyncTrigger: 'scheduled' },
729
739
  });
730
740
 
731
741
  const cached = await cache.get({ pluginName: testAppName, key: cacheKeyForTest });
@@ -764,15 +774,22 @@ describe('user: bookings controller - searchProducts', () => {
764
774
  },
765
775
  );
766
776
 
767
- it('rejects a forced selector when fullSyncTrigger is omitted', async () => {
777
+ it.each([
778
+ ['an omitted trigger', { forceRefresh: true }],
779
+ ['a blank reason', {
780
+ forceRefresh: true,
781
+ fullSyncTrigger: 'manual',
782
+ admissionOverrideReason: ' ',
783
+ }],
784
+ ])('rejects a manual catalog refresh with %s', async (_description, payload) => {
768
785
  const response = await doApiPost({
769
786
  url: `/products/${testAppName}/${testUserId}/${staleCacheTestHint}/search`,
770
787
  token: userToken,
771
- payload: { forceRefresh: true, optionId: 'option-1' },
788
+ payload,
772
789
  expectStatusCode: 400,
773
790
  });
774
791
 
775
- expect(response.message).toContain('must not include product selectors');
792
+ expect(response.message).toContain('requires a non-blank admissionOverrideReason');
776
793
  expect(travelgatePlugin.searchProducts).not.toHaveBeenCalled();
777
794
  });
778
795
 
@@ -847,7 +864,7 @@ describe('user: bookings controller - searchProducts', () => {
847
864
  const result = await doApiPost({
848
865
  url: `/products/${testAppName}/${testUserId}/${configuredHint}/search`,
849
866
  token: userToken,
850
- payload: { forceRefresh: true },
867
+ payload: { forceRefresh: true, fullSyncTrigger: 'scheduled' },
851
868
  });
852
869
 
853
870
  expect(result).toEqual(expect.objectContaining({
@@ -1130,7 +1147,7 @@ describe('Bookings Product Search Lock Mechanism (Job Queuing on Stale Cache)',
1130
1147
  const makeForceRequest = () => doApiPost({
1131
1148
  url: `/products/${testAppName}/${testUserId}/${ttrTestHint}/search`,
1132
1149
  token: userToken,
1133
- payload: { forceRefresh: true },
1150
+ payload: { forceRefresh: true, fullSyncTrigger: 'scheduled' },
1134
1151
  });
1135
1152
 
1136
1153
  const requestPromises = [makeForceRequest(), makeForceRequest(), makeForceRequest()];
@@ -1185,7 +1202,7 @@ describe('Bookings Product Search Lock Mechanism (Job Queuing on Stale Cache)',
1185
1202
  const makeForceRequest = () => doApiPost({
1186
1203
  url: `/products/${testAppName}/${testUserId}/${ttrTestHint}/search`,
1187
1204
  token: userToken,
1188
- payload: { forceRefresh: true },
1205
+ payload: { forceRefresh: true, fullSyncTrigger: 'scheduled' },
1189
1206
  });
1190
1207
 
1191
1208
  const requestPromises = [makeForceRequest(), makeForceRequest(), makeForceRequest()];
@@ -1236,7 +1253,7 @@ describe('Bookings Product Search Lock Mechanism (Job Queuing on Stale Cache)',
1236
1253
  const result = await doApiPost({
1237
1254
  url: `/products/${testAppName}/${testUserId}/${ttrTestHint}/search`,
1238
1255
  token: userToken,
1239
- payload: { forceRefresh: true },
1256
+ payload: { forceRefresh: true, fullSyncTrigger: 'scheduled' },
1240
1257
  });
1241
1258
 
1242
1259
  expect(result).toEqual(expect.objectContaining({
@@ -193,6 +193,14 @@ const createScopedCatalogRefreshError = () => {
193
193
  return err;
194
194
  };
195
195
 
196
+ const createManualCatalogRefreshReasonError = () => {
197
+ const err = new Error(
198
+ 'A manual catalog refresh requires a non-blank admissionOverrideReason',
199
+ );
200
+ err.status = 400;
201
+ return err;
202
+ };
203
+
196
204
  const createUnsupportedCatalogRefreshError = appKey => {
197
205
  const err = new Error(
198
206
  `${appKey} does not support complete product catalog refreshes`,
@@ -402,7 +410,7 @@ const $bookingsProductSearch = plugins => async ({
402
410
  requestedFullSyncTrigger,
403
411
  ) >= 0
404
412
  ? requestedFullSyncTrigger
405
- : (forceRefresh ? 'scheduled' : 'organic');
413
+ : (forceRefresh ? 'manual' : 'organic');
406
414
  const admissionOverrideReason = typeof requestedOverrideReason === 'string'
407
415
  ? requestedOverrideReason.trim() || undefined
408
416
  : undefined;
@@ -431,6 +439,14 @@ const $bookingsProductSearch = plugins => async ({
431
439
  const isScopedSearch = hasProductSearchSelector(normalizedRequestBody);
432
440
  const isDeclaredFullCatalogRefresh = forceRefresh
433
441
  && ['scheduled', 'manual'].indexOf(fullSyncTrigger) >= 0;
442
+ if (
443
+ forceRefresh
444
+ && !cacheOnly
445
+ && fullSyncTrigger === 'manual'
446
+ && !admissionOverrideReason
447
+ ) {
448
+ throw createManualCatalogRefreshReasonError();
449
+ }
434
450
  if (isDeclaredFullCatalogRefresh && isScopedSearch) {
435
451
  throw createScopedCatalogRefreshError();
436
452
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ti2",
3
- "version": "1.0.123",
3
+ "version": "1.0.124",
4
4
  "description": "Tourist Industry Exchange (TI2)",
5
5
  "main": "index.js",
6
6
  "scripts": {