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
|
package/controllers/bookings.js
CHANGED
|
@@ -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
|
-
|
|
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
|
@@ -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 () => {
|