ti2 1.0.107 → 1.0.108-beta.0

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/api.yml CHANGED
@@ -151,6 +151,10 @@ components:
151
151
  payload:
152
152
  type: object
153
153
  example: random lorem
154
+ integrationCredentials:
155
+ description: Optional external-session credentials forwarded by the client
156
+ type: object
157
+ additionalProperties: true
154
158
  UserApp:
155
159
  description: A user + App configuration
156
160
  type: object
@@ -385,6 +389,9 @@ components:
385
389
  required: false
386
390
  description: Product Name wildcard match
387
391
  example: '*bus*'
392
+ credentials:
393
+ required: false
394
+ $ref: '#/components/schemas/integrationCredentials'
388
395
  bookingsProductSearchReturn:
389
396
  type: object
390
397
  additionalProperties: true
@@ -454,6 +461,9 @@ components:
454
461
  shellOnly:
455
462
  type: boolean
456
463
  description: Optional itinerary-assist flag to create/update booking header only without adding a service line
464
+ credentials:
465
+ required: false
466
+ $ref: '#/components/schemas/integrationCredentials'
457
467
  settingsBody:
458
468
  type: object
459
469
  required: true
@@ -1410,6 +1420,40 @@ paths:
1410
1420
  $ref: '#/components/schemas/desk'
1411
1421
  /affiliate/{appKey}/{userId}/agents:
1412
1422
  get:
1423
+ deprecated: true
1424
+ security:
1425
+ - bearerAuth: ['admin', 'user']
1426
+ tags:
1427
+ - bookings
1428
+ summary: Gets all agents for this affiliate
1429
+ operationId: getAffiliateAgents
1430
+ parameters:
1431
+ - name: appKey
1432
+ in: path
1433
+ description: name of the app to get the agents for (defaults to first token found)
1434
+ required: true
1435
+ example: 'fareharbor'
1436
+ schema:
1437
+ type: string
1438
+ - name: userId
1439
+ in: path
1440
+ description: the user/company that the appKey belongs to
1441
+ required: true
1442
+ schema:
1443
+ type: string
1444
+ responses:
1445
+ '200':
1446
+ description: agents
1447
+ content:
1448
+ application/json:
1449
+ schema:
1450
+ type: object
1451
+ properties:
1452
+ agents:
1453
+ type: array
1454
+ items:
1455
+ $ref: '#/components/schemas/agent'
1456
+ post:
1413
1457
  security:
1414
1458
  - bearerAuth: ['admin', 'user']
1415
1459
  tags:
@@ -1430,6 +1474,13 @@ paths:
1430
1474
  required: true
1431
1475
  schema:
1432
1476
  type: string
1477
+ requestBody:
1478
+ description: Optional request metadata
1479
+ required: false
1480
+ content:
1481
+ application/json:
1482
+ schema:
1483
+ $ref: '#/components/schemas/AnyValue'
1433
1484
  responses:
1434
1485
  '200':
1435
1486
  description: agents
@@ -1444,6 +1495,7 @@ paths:
1444
1495
  $ref: '#/components/schemas/agent'
1445
1496
  /affiliate/{appKey}/{userId}/{hint}/agents:
1446
1497
  get:
1498
+ deprecated: true
1447
1499
  security:
1448
1500
  - bearerAuth: ['admin', 'user']
1449
1501
  tags:
@@ -1483,6 +1535,53 @@ paths:
1483
1535
  type: array
1484
1536
  items:
1485
1537
  $ref: '#/components/schemas/agent'
1538
+ post:
1539
+ security:
1540
+ - bearerAuth: ['admin', 'user']
1541
+ tags:
1542
+ - bookings
1543
+ summary: Gets all agents for this affiliate
1544
+ operationId: getAffiliateAgents
1545
+ parameters:
1546
+ - name: appKey
1547
+ in: path
1548
+ description: name of the app to get the agents for the token using the hint
1549
+ required: true
1550
+ example: 'fareharbor'
1551
+ schema:
1552
+ type: string
1553
+ - name: userId
1554
+ in: path
1555
+ description: the user/company that the appKey belongs to
1556
+ required: true
1557
+ schema:
1558
+ type: string
1559
+ - name: hint
1560
+ in: path
1561
+ description: a specific token hint to be used
1562
+ required: true
1563
+ example: 'fareharbor-helloworld'
1564
+ schema:
1565
+ type: string
1566
+ requestBody:
1567
+ description: Optional request metadata
1568
+ required: false
1569
+ content:
1570
+ application/json:
1571
+ schema:
1572
+ $ref: '#/components/schemas/AnyValue'
1573
+ responses:
1574
+ '200':
1575
+ description: agents
1576
+ content:
1577
+ application/json:
1578
+ schema:
1579
+ type: object
1580
+ properties:
1581
+ agents:
1582
+ type: array
1583
+ items:
1584
+ $ref: '#/components/schemas/agent'
1486
1585
  /{appKey}/{userId}/pickup-points:
1487
1586
  get:
1488
1587
  security:
@@ -2256,6 +2355,38 @@ paths:
2256
2355
  $ref: '#/components/schemas/AnyValue'
2257
2356
  /bookings/{appKey}/{userId}/custom-fields:
2258
2357
  get:
2358
+ deprecated: true
2359
+ security:
2360
+ - bearerAuth: ['admin', 'user']
2361
+ tags:
2362
+ - bookings
2363
+ summary: Gets all custom fields for creating a booking
2364
+ operationId: getCreateBookingFields
2365
+ parameters:
2366
+ - name: appKey
2367
+ in: path
2368
+ description: name of the app to get the resource for (defaults to first token found)
2369
+ required: true
2370
+ example: 'fareharbor'
2371
+ schema:
2372
+ type: string
2373
+ - name: userId
2374
+ in: path
2375
+ description: the user/company that the appKey belongs to
2376
+ required: true
2377
+ schema:
2378
+ type: string
2379
+ responses:
2380
+ '200':
2381
+ description: fields
2382
+ content:
2383
+ application/json:
2384
+ schema:
2385
+ type: object
2386
+ properties:
2387
+ fields:
2388
+ type: array
2389
+ post:
2259
2390
  security:
2260
2391
  - bearerAuth: ['admin', 'user']
2261
2392
  tags:
@@ -2276,6 +2407,13 @@ paths:
2276
2407
  required: true
2277
2408
  schema:
2278
2409
  type: string
2410
+ requestBody:
2411
+ description: Optional field context and credentials
2412
+ required: false
2413
+ content:
2414
+ application/json:
2415
+ schema:
2416
+ $ref: '#/components/schemas/AnyValue'
2279
2417
  responses:
2280
2418
  '200':
2281
2419
  description: fields
@@ -2288,6 +2426,7 @@ paths:
2288
2426
  type: array
2289
2427
  /bookings/{appKey}/{userId}/{hint}/custom-fields:
2290
2428
  get:
2429
+ deprecated: true
2291
2430
  security:
2292
2431
  - bearerAuth: ['admin', 'user']
2293
2432
  tags:
@@ -2325,6 +2464,51 @@ paths:
2325
2464
  properties:
2326
2465
  fields:
2327
2466
  type: array
2467
+ post:
2468
+ security:
2469
+ - bearerAuth: ['admin', 'user']
2470
+ tags:
2471
+ - bookings
2472
+ summary: Gets all custom fields for creating a booking
2473
+ operationId: getCreateBookingFields
2474
+ parameters:
2475
+ - name: appKey
2476
+ in: path
2477
+ description: name of the app to get the resource for the token using the hint
2478
+ required: true
2479
+ example: 'xola'
2480
+ schema:
2481
+ type: string
2482
+ - name: userId
2483
+ in: path
2484
+ description: the user/company that the appKey belongs to
2485
+ required: true
2486
+ schema:
2487
+ type: string
2488
+ - name: hint
2489
+ in: path
2490
+ description: a specific token hint to be used
2491
+ required: true
2492
+ example: 'xola-helloworld'
2493
+ schema:
2494
+ type: string
2495
+ requestBody:
2496
+ description: Optional field context and credentials
2497
+ required: false
2498
+ content:
2499
+ application/json:
2500
+ schema:
2501
+ $ref: '#/components/schemas/AnyValue'
2502
+ responses:
2503
+ '200':
2504
+ description: fields
2505
+ content:
2506
+ application/json:
2507
+ schema:
2508
+ type: object
2509
+ properties:
2510
+ fields:
2511
+ type: array
2328
2512
 
2329
2513
  /allotment/{appKey}/{userId}:
2330
2514
  get:
@@ -1,4 +1,4 @@
1
- /* globals beforeAll describe it expect jest */
1
+ /* globals beforeAll beforeEach describe it expect jest */
2
2
  const axios = require('axios');
3
3
  const MockAdapter = require('axios-mock-adapter');
4
4
  const chance = require('chance').Chance();
@@ -28,6 +28,9 @@ describe('allotment', () => {
28
28
  let doApiPost;
29
29
  let plugins;
30
30
  let userToken;
31
+ beforeEach(() => {
32
+ jest.clearAllMocks();
33
+ });
31
34
  beforeAll(async () => {
32
35
  ({
33
36
  doApiGet,
@@ -72,6 +75,8 @@ describe('allotment', () => {
72
75
  const call = plugins[0].queryAllotment.mock.calls[0][0];
73
76
  expect(call.payload).toEqual(payload);
74
77
  expect(call.token).toEqual(token);
78
+ expect(call.userId).toEqual(userId);
79
+ expect(call.hint).toBeUndefined();
75
80
  });
76
81
  it('should be able to get some allotments with a hint', async () => {
77
82
  const { allotments } = await doApiGet({
@@ -84,6 +89,8 @@ describe('allotment', () => {
84
89
  const call = plugins[0].queryAllotment.mock.calls[0][0];
85
90
  expect(call.payload).toEqual(payload);
86
91
  expect(call.token).toEqual(token);
92
+ expect(call.userId).toEqual(userId);
93
+ expect(call.hint).toEqual(newIntegration.tokenHint);
87
94
  });
88
95
  it('should be able to receive an axios error', async () => {
89
96
  // Temporarily silence console.error for this test since we're expecting an error
@@ -170,6 +170,52 @@ describe('user: bookings controller', () => {
170
170
  expect(plugins[0].createBooking.mock.calls[0][0].payload).toEqual(payload);
171
171
  expect(plugins[0].createBooking.mock.calls[0][0].token).toEqual(token);
172
172
  });
173
+ it('should forward inline credentials directly to create booking', async () => {
174
+ const payload = {
175
+ id: chance.guid(),
176
+ credentials: {
177
+ sessionValue: 'abc123',
178
+ },
179
+ };
180
+ await doApiPost({
181
+ url: `/bookings/${appKey}/${userId}/testingToken/booking`,
182
+ token: userToken,
183
+ payload,
184
+ });
185
+ expect(plugins[0].createBooking).toHaveBeenCalled();
186
+ expect(plugins[0].createBooking.mock.calls[0][0].payload).toEqual(payload);
187
+ });
188
+ it('should support POST affiliate agents with inline credentials', async () => {
189
+ const payload = {
190
+ credentials: {
191
+ sessionValue: 'agent-credential',
192
+ },
193
+ };
194
+ const { agents } = await doApiPost({
195
+ url: `/affiliate/${appKey}/${userId}/testingToken/agents`,
196
+ token: userToken,
197
+ payload,
198
+ });
199
+ expect(Array.isArray(agents)).toBeTruthy();
200
+ expect(plugins[0].getAffiliateAgents).toHaveBeenCalledTimes(1);
201
+ expect(plugins[0].getAffiliateAgents.mock.calls[0][0].payload).toEqual(payload);
202
+ });
203
+ it('should support POST custom-fields with inline credentials', async () => {
204
+ const payload = {
205
+ productId: 'prod-1',
206
+ credentials: {
207
+ sessionValue: 'fields-credential',
208
+ },
209
+ };
210
+ const response = await doApiPost({
211
+ url: `/bookings/${appKey}/${userId}/testingToken/custom-fields`,
212
+ token: userToken,
213
+ payload,
214
+ });
215
+ expect(response.customFields).toEqual([]);
216
+ expect(plugins[0].getCreateItineraryFields).toHaveBeenCalledTimes(1);
217
+ expect(plugins[0].getCreateItineraryFields.mock.calls[0][0].payload).toEqual(payload);
218
+ });
173
219
  it('should return 422 when stored app token is corrupted on bookings endpoint', async () => {
174
220
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
175
221
  const [originalRows] = await sqldb.query(
@@ -301,5 +347,25 @@ describe('user: bookings controller', () => {
301
347
  expect(products).toHaveLength(1);
302
348
  expect(products[0].productName).toBe('Cached Product');
303
349
  });
350
+
351
+ it('should forward inline credentials directly to product search', async () => {
352
+ const payload = {
353
+ credentials: {
354
+ sessionValue: 'product-credential',
355
+ },
356
+ };
357
+
358
+ await doApiPost({
359
+ url: `/products/${appKey}/${userId}/testingToken/search`,
360
+ token: userToken,
361
+ payload,
362
+ });
363
+
364
+ expect(plugins[0].searchProducts).toHaveBeenCalledTimes(1);
365
+ expect(plugins[0].searchProducts.mock.calls[0][0].payload).toEqual({
366
+ ...payload,
367
+ forceRefresh: false,
368
+ });
369
+ });
304
370
  });
305
371
  });
@@ -19,7 +19,7 @@ const queryAllotment = plugins => async (req, res, next) => {
19
19
  where: {
20
20
  userId,
21
21
  integrationId: appKey,
22
- ...(hint ? { hint } : {}),
22
+ ...(hint && { hint }),
23
23
  },
24
24
  });
25
25
  assert(userAppKeys, 'could not find the app key');
@@ -29,6 +29,8 @@ const queryAllotment = plugins => async (req, res, next) => {
29
29
  axios,
30
30
  token,
31
31
  payload: query,
32
+ userId,
33
+ hint,
32
34
  requestId: req.requestId,
33
35
  });
34
36
  return res.json(results);
@@ -28,6 +28,21 @@ const typeDefsAndQueries = {
28
28
  itineraryBookingQuery,
29
29
  };
30
30
 
31
+ const getAppAndToken = async ({ plugins, appKey, userId, hint }) => {
32
+ const app = plugins.find(({ name }) => name === appKey);
33
+ assert(app, 'could not find the app ' + appKey);
34
+ const userAppKeys = await UserAppKey.findOne({
35
+ where: {
36
+ userId,
37
+ integrationId: appKey,
38
+ ...(hint && { hint }),
39
+ },
40
+ });
41
+ assert(userAppKeys, 'could not find the app key');
42
+ const token = await userAppKeys.token;
43
+ return { app, token };
44
+ };
45
+
31
46
  const bookingsSearch = plugins => async (req, res, next) => {
32
47
  const {
33
48
  axios,
@@ -35,16 +50,7 @@ const bookingsSearch = plugins => async (req, res, next) => {
35
50
  body,
36
51
  } = req;
37
52
  try {
38
- const app = plugins.find(({ name }) => name === appKey);
39
- const userAppKeys = await UserAppKey.findOne({
40
- where: {
41
- userId,
42
- integrationId: appKey,
43
- ...(hint ? { hint } : {}),
44
- },
45
- });
46
- assert(userAppKeys, 'could not find the app key');
47
- const token = await userAppKeys.token;
53
+ const { app, token } = await getAppAndToken({ plugins, appKey, userId, hint });
48
54
  assert(app.searchItineraries || app.searchHotelBooking || app.searchBooking, `searchItineraries or searchHotelBooking or searchBooking is not available for ${appKey}`);
49
55
  const search = (app.searchHotelBooking || app.searchBooking || app.searchItineraries).bind(app);
50
56
  const results = await search({
@@ -52,6 +58,8 @@ const bookingsSearch = plugins => async (req, res, next) => {
52
58
  token,
53
59
  payload: body,
54
60
  typeDefsAndQueries,
61
+ userId,
62
+ hint,
55
63
  requestId: req.requestId,
56
64
  });
57
65
  return res.json(results);
@@ -67,22 +75,14 @@ const bookingsCancel = plugins => async (req, res, next) => {
67
75
  body,
68
76
  } = req;
69
77
  try {
70
- const app = plugins.find(({ name }) => name === appKey);
71
- // const app = load(appKey);
72
- const userAppKeys = (await UserAppKey.findOne({
73
- where: {
74
- userId,
75
- integrationId: appKey,
76
- ...(hint ? { hint } : {}),
77
- },
78
- }));
79
- assert(userAppKeys, 'could not find the app key');
80
- const token = await userAppKeys.token;
78
+ const { app, token } = await getAppAndToken({ plugins, appKey, userId, hint });
81
79
  const results = await app.cancelBooking({
82
80
  axios,
83
81
  token,
84
82
  payload: body,
85
83
  typeDefsAndQueries,
84
+ userId,
85
+ hint,
86
86
  requestId: req.requestId,
87
87
  });
88
88
  return res.json(results);
@@ -140,16 +140,12 @@ const $bookingsProductSearch = plugins => async ({
140
140
 
141
141
  // Payload for the plugin function (func) - pass forceRefresh so plugins can trigger background cache rebuilds
142
142
  const payloadForPlugin = { ...R.omit(['forceRefresh'], originalRequestBody), forceRefresh };
143
+ const payloadForBackgroundJob = R.omit(['credentials'], payloadForPlugin);
143
144
 
144
- const app = plugins.find(({ name }) => name === appKey);
145
+ const { app, token } = await getAppAndToken({ plugins, appKey, userId, hint });
145
146
  assert(userId, 'userId is required');
146
147
  assert(appKey, 'appKey is required');
147
148
  assert(app.searchProducts || app.searchProductsForItinerary, `searchProducts or searchProductsForItinerary is not available for ${appKey}`);
148
- const userAppKeys = (await UserAppKey.findOne({
149
- where: { userId, integrationId: appKey, ...(hint ? { hint } : {}) },
150
- }));
151
- assert(userAppKeys, 'could not find the app key');
152
- const token = await userAppKeys.token;
153
149
  const func = (app.searchProducts || app.searchProductsForItinerary).bind(app);
154
150
 
155
151
  const cacheKey = hash({ userId, hint, operationId: 'bookingsProductSearch' });
@@ -170,7 +166,13 @@ const $bookingsProductSearch = plugins => async ({
170
166
  let pluginResults;
171
167
  try {
172
168
  pluginResults = await func({
173
- axios, token, payload: payloadForPlugin, typeDefsAndQueries, requestId, userId,
169
+ axios,
170
+ token,
171
+ payload: payloadForPlugin,
172
+ typeDefsAndQueries,
173
+ requestId,
174
+ userId,
175
+ hint,
174
176
  });
175
177
 
176
178
  // This function is responsible for caching if it fetched good results.
@@ -244,7 +246,7 @@ const $bookingsProductSearch = plugins => async ({
244
246
  pluginName: appKey,
245
247
  method: app.searchProducts ? 'searchProducts' : 'searchProductsForItinerary',
246
248
  token,
247
- payload: { payload: payloadForPlugin, userId },
249
+ payload: { payload: payloadForBackgroundJob, userId, hint },
248
250
  postProcess: {
249
251
  controller: 'bookings',
250
252
  action: '$updateProductSearchCache',
@@ -294,23 +296,15 @@ const getProductPackages = plugins => async (req, res, next) => {
294
296
  body: payload,
295
297
  } = req;
296
298
  try {
297
- const app = plugins.find(({ name }) => name === appKey);
298
- // const app = load(appKey);
299
- const userAppKeys = (await UserAppKey.findOne({
300
- where: {
301
- userId,
302
- integrationId: appKey,
303
- ...(hint ? { hint } : {}),
304
- },
305
- }));
306
- assert(userAppKeys, 'could not find the app key');
299
+ const { app, token } = await getAppAndToken({ plugins, appKey, userId, hint });
307
300
  assert(app.getProductPackages, `getProductPackages is not available for ${appKey}`);
308
- const token = await userAppKeys.token;
309
301
  const results = await app.getProductPackages({
310
302
  axios,
311
303
  token,
312
304
  payload,
313
305
  typeDefsAndQueries,
306
+ userId,
307
+ hint,
314
308
  requestId: req.requestId,
315
309
  });
316
310
  return res.json(results);
@@ -326,23 +320,15 @@ const bookingsAvailabilitySearch = plugins => async (req, res, next) => {
326
320
  body: payload,
327
321
  } = req;
328
322
  try {
329
- const app = plugins.find(({ name }) => name === appKey);
330
- // const app = load(appKey);
331
- const userAppKeys = (await UserAppKey.findOne({
332
- where: {
333
- userId,
334
- integrationId: appKey,
335
- ...(hint ? { hint } : {}),
336
- },
337
- }));
338
- assert(userAppKeys, 'could not find the app key');
339
- const token = await userAppKeys.token;
323
+ const { app, token } = await getAppAndToken({ plugins, appKey, userId, hint });
340
324
  const func = (app.searchAvailability || app.searchAvailabilityForItinerary).bind(app);
341
325
  const results = await func({
342
326
  axios,
343
327
  token,
344
328
  payload,
345
329
  typeDefsAndQueries,
330
+ userId,
331
+ hint,
346
332
  requestId: req.requestId,
347
333
  });
348
334
  return res.json(results);
@@ -359,23 +345,15 @@ const $bookingsAvailabilityCalendar = plugins => async ({
359
345
  payload,
360
346
  requestId,
361
347
  }) => {
362
- const app = plugins.find(({ name }) => name === appKey);
363
- // const app = load(appKey);
364
- const userAppKeys = (await UserAppKey.findOne({
365
- where: {
366
- userId,
367
- integrationId: appKey,
368
- ...(hint ? { hint } : {}),
369
- },
370
- }));
371
- assert(userAppKeys, 'could not find the app key');
348
+ const { app, token } = await getAppAndToken({ plugins, appKey, userId, hint });
372
349
  assert(app.availabilityCalendar, `availabilityCalendar is not available for ${appKey}`);
373
- const token = await userAppKeys.token;
374
350
  return app.availabilityCalendar({
375
351
  axios,
376
352
  token,
377
353
  payload,
378
354
  typeDefsAndQueries,
355
+ userId,
356
+ hint,
379
357
  requestId,
380
358
  });
381
359
  };
@@ -406,22 +384,14 @@ const searchQuote = plugins => async (req, res, next) => {
406
384
  body: payload,
407
385
  } = req;
408
386
  try {
409
- const app = plugins.find(({ name }) => name === appKey);
410
- // const app = load(appKey);
411
- const userAppKeys = (await UserAppKey.findOne({
412
- where: {
413
- userId,
414
- integrationId: appKey,
415
- ...(hint ? { hint } : {}),
416
- },
417
- }));
418
- assert(userAppKeys, 'could not find the app key');
419
- const token = await userAppKeys.token;
387
+ const { app, token } = await getAppAndToken({ plugins, appKey, userId, hint });
420
388
  const results = await app.searchQuote({
421
389
  axios,
422
390
  token,
423
391
  payload,
424
392
  typeDefsAndQueries,
393
+ userId,
394
+ hint,
425
395
  requestId: req.requestId,
426
396
  });
427
397
  return res.json(results);
@@ -437,17 +407,7 @@ const createBooking = plugins => async (req, res, next) => {
437
407
  body: payload,
438
408
  } = req;
439
409
  try {
440
- const app = plugins.find(({ name }) => name === appKey);
441
- // const app = load(appKey);
442
- const userAppKeys = (await UserAppKey.findOne({
443
- where: {
444
- userId,
445
- integrationId: appKey,
446
- ...(hint ? { hint } : {}),
447
- },
448
- }));
449
- assert(userAppKeys, 'could not find the app key');
450
- const token = await userAppKeys.token;
410
+ const { app, token } = await getAppAndToken({ plugins, appKey, userId, hint });
451
411
  const func = (app.createBooking || app.addServiceToItinerary).bind(app);
452
412
  let results;
453
413
  if (payload.mock) {
@@ -458,10 +418,12 @@ const createBooking = plugins => async (req, res, next) => {
458
418
  token,
459
419
  payload,
460
420
  typeDefsAndQueries,
421
+ userId,
422
+ hint,
461
423
  requestId: req.requestId,
462
424
  });
463
425
  }
464
- console.log(`emitting bookingsCreateBooking event for ${appKey}, user ${userId}, hint ${hint}, results: ${JSON.stringify(results)}`);
426
+ console.debug(`emitting bookingsCreateBooking event for ${appKey}, user ${userId}, hint ${hint}, results: ${JSON.stringify(results)}`);
465
427
  app.events.emit('bookingsCreateBooking', {
466
428
  userId,
467
429
  hint,
@@ -483,22 +445,14 @@ const getAffiliateAgents = plugins => async (req, res, next) => {
483
445
  body: payload,
484
446
  } = req;
485
447
  try {
486
- const app = plugins.find(({ name }) => name === appKey);
487
- // const app = load(appKey);
488
- const userAppKeys = (await UserAppKey.findOne({
489
- where: {
490
- userId,
491
- integrationId: appKey,
492
- ...(hint ? { hint } : {}),
493
- },
494
- }));
495
- assert(userAppKeys, 'could not find the app key');
496
- const token = await userAppKeys.token;
448
+ const { app, token } = await getAppAndToken({ plugins, appKey, userId, hint });
497
449
  assert(app.getAffiliateAgents, `getAffiliateAgents is not available for ${appKey}`);
498
450
  const results = await app.getAffiliateAgents({
499
451
  axios,
500
452
  token,
501
453
  payload,
454
+ userId,
455
+ hint,
502
456
  requestId: req.requestId,
503
457
  });
504
458
  return res.json(results);
@@ -514,22 +468,14 @@ const getAffiliateDesks = plugins => async (req, res, next) => {
514
468
  body: payload,
515
469
  } = req;
516
470
  try {
517
- const app = plugins.find(({ name }) => name === appKey);
518
- // const app = load(appKey);
519
- const userAppKeys = (await UserAppKey.findOne({
520
- where: {
521
- userId,
522
- integrationId: appKey,
523
- ...(hint ? { hint } : {}),
524
- },
525
- }));
526
- assert(userAppKeys, 'could not find the app key');
527
- const token = await userAppKeys.token;
471
+ const { app, token } = await getAppAndToken({ plugins, appKey, userId, hint });
528
472
  assert(app.getAffiliateDesks, `getAffiliateDesks is not available for ${appKey}`);
529
473
  const results = await app.getAffiliateDesks({
530
474
  axios,
531
475
  token,
532
476
  payload,
477
+ userId,
478
+ hint,
533
479
  requestId: req.requestId,
534
480
  });
535
481
  return res.json(results);
@@ -545,23 +491,16 @@ const getPickupPoints = plugins => async (req, res, next) => {
545
491
  body: payload,
546
492
  } = req;
547
493
  try {
548
- const app = plugins.find(({ name }) => name === appKey);
549
- // const app = load(appKey);
550
- const userAppKeys = (await UserAppKey.findOne({
551
- where: {
552
- userId,
553
- integrationId: appKey,
554
- ...(hint ? { hint } : {}),
555
- },
556
- }));
557
- assert(userAppKeys, 'could not find the app key');
558
- const token = await userAppKeys.token;
494
+ const { app, token } = await getAppAndToken({ plugins, appKey, userId, hint });
559
495
  assert(app.getPickupPoints, `getPickupPoints is not available for ${appKey}`);
560
496
  const results = await app.getPickupPoints({
561
497
  axios,
562
498
  token,
563
499
  payload,
564
500
  typeDefsAndQueries,
501
+ userId,
502
+ hint,
503
+ requestId: req.requestId,
565
504
  });
566
505
  return res.json(results);
567
506
  } catch (err) {
@@ -577,17 +516,7 @@ const getCreateBookingFields = plugins => async (req, res, next) => {
577
516
  body: payload,
578
517
  } = req;
579
518
  try {
580
- const app = plugins.find(({ name }) => name === appKey);
581
- // const app = load(appKey);
582
- const userAppKeys = (await UserAppKey.findOne({
583
- where: {
584
- userId,
585
- integrationId: appKey,
586
- ...(hint ? { hint } : {}),
587
- },
588
- }));
589
- assert(userAppKeys, 'could not find the app key');
590
- const token = await userAppKeys.token;
519
+ const { app, token } = await getAppAndToken({ plugins, appKey, userId, hint });
591
520
  assert(app.getCreateBookingFields || app.getCreateItineraryFields, `getCreateBookingFields or getCreateItineraryFields is not available for ${appKey}`);
592
521
  const func = (app.getCreateItineraryFields || app.getCreateBookingFields).bind(app);
593
522
  const results = await func({
@@ -596,6 +525,9 @@ const getCreateBookingFields = plugins => async (req, res, next) => {
596
525
  payload,
597
526
  query,
598
527
  typeDefsAndQueries,
528
+ userId,
529
+ hint,
530
+ requestId: req.requestId,
599
531
  });
600
532
  return res.json(results);
601
533
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ti2",
3
- "version": "1.0.107",
3
+ "version": "1.0.108-beta.0",
4
4
  "description": "Tourist Industry Exchange (TI2)",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/test/plugin.js CHANGED
@@ -75,6 +75,7 @@ class Plugin {
75
75
  });
76
76
  this.searchQuote = jestPlugin.fn(() => ({ quote: [{ id: chance.guid() }] }));
77
77
  this.createBooking = jestPlugin.fn(() => {});
78
+ this.getAffiliateAgents = jestPlugin.fn(() => ({ agents: [] }));
78
79
  this.searchItineraries = jestPlugin.fn(() => ({ bookings: [] }));
79
80
  this.searchProductsForItinerary = jestPlugin.fn(() => ({ products: [] }));
80
81
  this.searchAvailabilityForItinerary = jestPlugin.fn(({
package/.windsurfrules DELETED
@@ -1,9 +0,0 @@
1
- This is a Node.js project using Sequelize for database operations and Express.js for the web server.
2
- The API is defined in api.yml using OpenAPI 3.0.2
3
- The database is defined in models/index.js
4
- The models are defined in models/
5
- to run tests you have to run them on the ti2 named docker container, and consider this codebase is part of node_modules/ti2 path for example :
6
- `` $ docker exec ti2 bash -c "cd /ti2 && npx jest lib/__tests__/callback.js --forceExit"``
7
- When writing / editing tests, make use of test/utils.js and review their implementation to understand how to mock the environment.
8
- Do not make any changes until you 95% confident of the changes you want to make, ask follow up questions until you have that confidence.
9
- when sending jobs to the background worker using the queue, asssume a worker is always runing to execute it but is on a different instance thread, so to review / wait for jobs we have to use the wokrer/queue.js listJobs fn to check for it
package/CLAUDE.md DELETED
@@ -1,159 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Project Overview
6
-
7
- Ti2 (Tourism Information Interchange) is an open-source integration framework for the tourism industry. It provides standardized functions for bookings, content, and rates management through a plugin-based architecture.
8
-
9
- ## Critical Development Rules
10
-
11
- 1. **Docker Container Execution**: ALL commands must be run inside the `ti2` Docker container
12
- - The codebase is located at `/ti2` path inside the container
13
- - This project is part of `node_modules/ti2` in the container environment
14
- - **NEVER run npm, node, or npx commands on the host machine**
15
- - Always use: `docker exec ti2 bash -c "cd /ti2 && <command>"`
16
- - Example for npm install: `docker exec ti2 bash -c "cd /ti2 && npm install <package>"`
17
-
18
- 2. **Testing Rules**:
19
- - Tests MUST be run inside the Docker container
20
- - Example: `docker exec ti2 bash -c "cd /ti2 && npx jest lib/__tests__/callback.js --forceExit"`
21
- - Use `test/utils.js` for mocking the environment
22
- - Review test utility implementations before writing/editing tests
23
-
24
- 3. **Background Jobs**:
25
- - Jobs sent to the queue run on a separate worker thread/instance
26
- - Use `worker/queue.js` `listJobs` function to check job status
27
- - Workers are always assumed to be running
28
-
29
- 4. **Code Confidence**: Do not make changes until 95% confident. Ask follow-up questions if uncertain.
30
-
31
- ## Common Development Commands
32
-
33
- ```bash
34
- # Run tests (inside Docker container)
35
- docker exec ti2 bash -c "cd /ti2 && npm test"
36
- docker exec ti2 bash -c "cd /ti2 && npx jest [test-file] --forceExit"
37
-
38
- # Run specific test file
39
- docker exec ti2 bash -c "cd /ti2 && npx jest controllers/__tests__/bookings.js --forceExit"
40
-
41
- # Database migrations
42
- docker exec ti2 bash -c "cd /ti2 && npm run sequelize db:migrate"
43
-
44
- # Generate documentation
45
- docker exec ti2 bash -c "cd /ti2 && npm run doc"
46
- ```
47
-
48
- ## Architecture
49
-
50
- ### Core Components
51
-
52
- 1. **Main Entry Point** (`index.js`):
53
- - Initializes Express server on port 10010 (default)
54
- - Sets up Swagger documentation at `/api-docs`
55
- - Handles plugin instantiation and middleware
56
- - Implements caching mechanism
57
- - Manages background job processing via `worker/queue.js`
58
-
59
- 2. **API Definition** (`api.yml`):
60
- - OpenAPI 3.0.0 specification
61
- - Defines all endpoints and schemas
62
- - JWT Bearer authentication
63
-
64
- 3. **Plugin System**:
65
- - Two types: Integration Plugins (connect systems) and App Plugins (value-added tools)
66
- - Plugins are instantiated with cache, axios, events, and configuration
67
- - Plugin schemas are merged with main API schema
68
- - Each plugin gets its own routes under `/apps/{pluginName}`
69
-
70
- 4. **Controllers** (`controllers/`):
71
- - `app.js`: Core application controller
72
- - `bookings.js`: Booking operations (searchProducts, createBooking, etc.)
73
- - `admin.js`: Administrative functions
74
- - `user.js`: User management
75
- - `ping.js`: Health check endpoints
76
- - `allotment.js`: Allotment management
77
-
78
- 5. **Database Models** (`models/`):
79
- - Sequelize ORM with MySQL
80
- - Key models: Integration, User, UserAppKey, CronJobs, ApiCronJobs
81
- - Migrations in `migrations/` directory
82
-
83
- 6. **Worker System** (`worker/`):
84
- - Bull queue for background job processing
85
- - Redis-backed job queue
86
- - Supports plugin jobs, API jobs, and callback jobs
87
- - Multi-worker support with throng
88
-
89
- 7. **Authentication** (`auth/authHandler.js`):
90
- - JWT-based authentication
91
- - Bearer token scheme
92
-
93
- ### Key Features
94
-
95
- - **Caching**: Redis-based caching with configurable TTL
96
- - **Event System**: EventEmitter2 for plugin communication
97
- - **Background Jobs**: Async job processing with Bull/Redis
98
- - **Cron Jobs**: Scheduled task execution
99
- - **Multi-tenancy**: Support for multiple integrations/plugins
100
-
101
- ### Plugin Development
102
-
103
- Plugins must implement standard methods:
104
- - `validateToken`: Token validation
105
- - `tokenTemplate`: Token configuration template
106
- - Booking methods: `searchProducts`, `searchAvailability`, `createBooking`, `cancelBooking`
107
- - Content methods: `getProducts`, `getProduct`, `createProduct`, `updateProduct`
108
-
109
- ### Testing Strategy
110
-
111
- - Jest testing framework
112
- - Test fixtures in `__fixtures__/`
113
- - Mock plugins in `__mocks__/`
114
- - Use `test/utils.js` for creating test environments
115
- - Tests organized by controller in `controllers/__tests__/`
116
-
117
- ## Database
118
-
119
- - Sequelize ORM with MySQL/MariaDB
120
- - Connection managed through `models/db.js`
121
- - Migrations handled via sequelize-cli
122
- - Models auto-loaded from `models/` directory
123
-
124
- ## Environment Variables
125
-
126
- Key variables:
127
- - `PORT`: Server port (default: 10010)
128
- - `REDIS_URL`: Redis connection (default: redis://redis:6379)
129
- - `jwtSecret`: JWT signing secret
130
- - `adminKey`: Admin authentication key
131
- - `WEB_CONCURRENCY`: Number of workers
132
- - `MAX_JOBS_PER_WORKER`: Job concurrency per worker
133
- - `SSL_INSECURE_ALLOWED_DOMAINS`: Pipe-separated list of domains where SSL certificate verification is skipped (e.g., `localhost|staging.example.com|192.168.1.100`)
134
-
135
- Plugin-specific variables follow pattern: `ti2_{pluginName}_{setting}`
136
-
137
- ### SSL Configuration
138
-
139
- The `SSL_INSECURE_ALLOWED_DOMAINS` environment variable allows you to specify domains where SSL certificate verification should be skipped. This is useful for:
140
- - Development environments with self-signed certificates
141
- - Testing against staging servers with invalid certificates
142
- - Local development with HTTPS
143
-
144
- Example usage:
145
- ```bash
146
- # Skip SSL verification for specific domains
147
- export SSL_INSECURE_ALLOWED_DOMAINS="localhost|staging.api.com|192.168.1.100"
148
-
149
- # Multiple domains separated by pipe
150
- export SSL_INSECURE_ALLOWED_DOMAINS="dev.example.com|test.example.com"
151
- ```
152
-
153
- **Important Implementation Notes**:
154
- - SSL configuration is ONLY applied to plugin axios instances (for external API calls) and worker axios instances
155
- - Internal communication (callbacks, per-request axios) do NOT use SSL configuration
156
- - SSL configuration uses a per-request interceptor approach for better isolation
157
- - The configuration checks domains dynamically on each request
158
-
159
- **Warning**: Only use this in development/testing environments. Never skip SSL verification in production for security reasons.