ti2 1.0.125 → 1.0.127

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.
@@ -24,7 +24,7 @@ beforeAll(async () => {
24
24
  // Initialize utils once for the entire test file
25
25
  // Ensure all plugins needed across different describe blocks are listed here.
26
26
  globalUtils = await testUtils({
27
- plugins: ['lockTestPlugin', 'travelgate'],
27
+ plugins: ['lockTestPlugin', 'travelgate', 'tourplannx'],
28
28
  pluginCapabilities: {
29
29
  lockTestPlugin: { weeklyProductCatalogSync: true },
30
30
  travelgate: { weeklyProductCatalogSync: true },
@@ -877,6 +877,42 @@ describe('user: bookings controller - searchProducts', () => {
877
877
  });
878
878
  });
879
879
 
880
+ describe('Product option identifier lookup', () => {
881
+ it.each(['tourplannx', 'travelgate'])(
882
+ 'preserves stored identifiers and applies the %s comparison policy',
883
+ async appName => {
884
+ const setup = await globalUtils.appSetup({ appName });
885
+ const plugin = globalPlugins.find(item => item.name === appName);
886
+ const products = [{
887
+ productId: 'supplier-1',
888
+ options: [
889
+ { optionId: 'ABCDEOPT ROOM01' },
890
+ { optionId: 'ABCDELEV\tFEE001' },
891
+ ],
892
+ }];
893
+ plugin.searchProducts.mockResolvedValueOnce({ products });
894
+ const url = `/products/${appName}/${setup.userId}/${setup.hint}/search`;
895
+ const token = globalUtils.createUserToken(setup.userId);
896
+ await globalDoApiPost({ url, token, payload: {} });
897
+ plugin.searchProducts.mockClear();
898
+ const result = await globalDoApiPost({
899
+ url,
900
+ token,
901
+ payload: { optionId: ['ABCDEOPTROOM01', 'ABCDELEV FEE001'] },
902
+ });
903
+ expect(result.products).toEqual(appName === 'tourplannx' ? products : []);
904
+ expect(plugin.searchProducts).not.toHaveBeenCalled();
905
+ const exact = await globalDoApiPost({
906
+ url,
907
+ token,
908
+ payload: { optionId: 'ABCDEOPT ROOM01', cacheOnly: true },
909
+ });
910
+ expect(exact.products[0].options).toEqual([products[0].options[0]]);
911
+ expect(products[0].options).toHaveLength(2);
912
+ },
913
+ );
914
+ });
915
+
880
916
  describe('Bookings Product Search Lock Mechanism (Job Queuing on Stale Cache)', () => {
881
917
  let doApiPost;
882
918
  let lockTestPlugin; // Specific plugin instance for this suite
@@ -314,7 +314,7 @@ const confirmBooking = plugins => async (req, res, next) => {
314
314
  }
315
315
  };
316
316
 
317
- const $searchProductList = (products, searchInput = '', optionId = '') => {
317
+ const $searchProductList = (products, searchInput = '', optionId = '', appKey = '') => {
318
318
  // NOTE: optionId could be a string or an array of strings
319
319
  // NOTE: searchInput should not appear at the same time as optionId
320
320
  const trimmedSearchInput = (searchInput || '').trim();
@@ -328,11 +328,16 @@ const $searchProductList = (products, searchInput = '', optionId = '') => {
328
328
  } ${R.path(['supplierId'], product) || ''}`;
329
329
  const inputValueLower = trimmedSearchInput.toLowerCase();
330
330
  const parts = inputValueLower.split(' ').filter(Boolean); // Filter out any empty strings just in case
331
+ const optionKey = value => appKey === 'tourplannx'
332
+ ? String(value || '').replace(/\s/g, '')
333
+ : value;
334
+ const requestedOptionIds = new Set(
335
+ (R.is(Array, optionId) ? optionId : [optionId]).map(optionKey),
336
+ );
331
337
  const pwFilteredOptions = products.map(product => {
332
338
  const filteredOptions = R.pathOr([], ['options'], product).filter(option => {
333
339
  if (optionId && optionId.length) {
334
- const optionIdArr = R.is(Array, optionId) ? optionId : [optionId];
335
- return optionIdArr.includes(R.path(['optionId'], option));
340
+ return requestedOptionIds.has(optionKey(R.path(['optionId'], option)));
336
341
  }
337
342
  const fullSearchStr = getFullSearchStr(product, option).toLowerCase();
338
343
  return parts.every(part => fullSearchStr.includes(part));
@@ -751,7 +756,7 @@ const $bookingsProductSearch = plugins => async ({
751
756
  // 0. `cacheOnly`: return current Ti2 cache or empty. Never call the plugin.
752
757
  if (cacheOnly) {
753
758
  if (hasProductCache(initialActualCacheContent)) {
754
- const searchResults = $searchProductList(initialActualCacheContent.products, searchInput, optionId);
759
+ const searchResults = $searchProductList(initialActualCacheContent.products, searchInput, optionId, appKey);
755
760
  emitDecision('cache_hit', {
756
761
  reason: 'cacheOnly',
757
762
  cacheProductCount: initialActualCacheContent.products.length,
@@ -773,7 +778,7 @@ const $bookingsProductSearch = plugins => async ({
773
778
  // 1. `doNotCallPluginForProducts` is true, and not `forceRefresh`: Serve from cache or empty.
774
779
  if (doNotCallPluginForProducts && !forceRefresh) {
775
780
  if (hasProductCache(initialActualCacheContent)) {
776
- const searchResults = $searchProductList(initialActualCacheContent.products, searchInput, optionId);
781
+ const searchResults = $searchProductList(initialActualCacheContent.products, searchInput, optionId, appKey);
777
782
  emitDecision('cache_hit', {
778
783
  reason: 'doNotCallPluginForProducts',
779
784
  cacheProductCount: initialActualCacheContent.products.length,
@@ -792,7 +797,7 @@ const $bookingsProductSearch = plugins => async ({
792
797
  if (forceRefresh) {
793
798
  emitDecision('force_refresh');
794
799
  const funcResults = await fetchFromPluginAndCache('force_refresh');
795
- const searchResults = $searchProductList(funcResults.products, searchInput, optionId);
800
+ const searchResults = $searchProductList(funcResults.products, searchInput, optionId, appKey);
796
801
  return {
797
802
  ...funcResults,
798
803
  products: searchResults,
@@ -814,7 +819,7 @@ const $bookingsProductSearch = plugins => async ({
814
819
  const shouldSkipEmptyCache = cacheIsEmpty && searchFilterIsEmpty;
815
820
  if (!shouldSkipEmptyCache) {
816
821
  const returnCachedResults = (action = 'cache_hit', extra = {}) => {
817
- const searchResults = $searchProductList(initialActualCacheContent.products, searchInput, optionId);
822
+ const searchResults = $searchProductList(initialActualCacheContent.products, searchInput, optionId, appKey);
818
823
  emitDecision(action, {
819
824
  cacheProductCount: initialActualCacheContent.products.length,
820
825
  cacheOptionCount: optionCount(initialActualCacheContent),
@@ -842,7 +847,7 @@ const $bookingsProductSearch = plugins => async ({
842
847
  // 4. No cache content (and not caught by previous conditions like forceRefresh or doNotCallPluginForProducts):
843
848
  // Fetch from plugin. fetchFromPluginAndCache will handle caching.
844
849
  const funcResults = await fetchFromPluginAndCache('cache_miss');
845
- const searchResults = $searchProductList(funcResults.products, searchInput, optionId);
850
+ const searchResults = $searchProductList(funcResults.products, searchInput, optionId, appKey);
846
851
  return {
847
852
  ...funcResults,
848
853
  products: searchResults,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ti2",
3
- "version": "1.0.125",
3
+ "version": "1.0.127",
4
4
  "description": "Tourist Industry Exchange (TI2)",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -14,6 +14,7 @@ jest.mock('bull', () => jest.fn(() => mockQueue));
14
14
  jest.mock('ioredis', () => jest.fn(() => mockRedis));
15
15
 
16
16
  jest.resetModules();
17
+ const Queue = require('bull');
17
18
  const { removeJob } = require('../queue');
18
19
 
19
20
  const occurrenceId = ({
@@ -26,6 +27,17 @@ const occurrenceId = ({
26
27
  return `repeat:${repeatHash}:${timestamp}`;
27
28
  };
28
29
 
30
+ describe('queue settings', () => {
31
+ it('keeps long-running jobs locked while the worker is active', () => {
32
+ expect(Queue).toHaveBeenCalledWith('work', expect.any(String), {
33
+ settings: {
34
+ lockDuration: 120e3,
35
+ lockRenewTime: 15e3,
36
+ },
37
+ });
38
+ });
39
+ });
40
+
29
41
  describe('removeJob', () => {
30
42
  beforeEach(() => {
31
43
  jest.clearAllMocks();
package/worker/queue.js CHANGED
@@ -6,7 +6,12 @@ const Redis = require('ioredis');
6
6
  const REDIS_URL = process.env.REDIS_URL || 'redis://redis:6379';
7
7
  const itemsTTL = 3 * 60 * 60; // 3 hours
8
8
 
9
- const queue = new Queue('work', `${REDIS_URL}/0`);
9
+ const queue = new Queue('work', `${REDIS_URL}/0`, {
10
+ settings: {
11
+ lockDuration: 120e3,
12
+ lockRenewTime: 15e3,
13
+ },
14
+ });
10
15
  const redisResults = new Redis(`${REDIS_URL}/1`);
11
16
 
12
17
  const getPending = async () => {