ti2 1.0.79 → 1.0.81

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/.cursorignore ADDED
@@ -0,0 +1 @@
1
+ # Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
package/README.md CHANGED
@@ -33,29 +33,36 @@ Plugins are the connectores to other systems and/or features you intent to use;
33
33
 
34
34
  ### Plugin Library
35
35
 
36
- | Methods | [Ventrata](https://github.com/TourConnect/ti2-ventrata) | [TravelGateX](https://github.com/TourConnect/ti2-travelgate) | [Didgigo](https://github.com/TourConnect/ti2-didgigo) | [TourConnect](https://github.com/TourConnect/ti2-tourconnect)
37
- | ---- | -------- | ---------- | ---- | ---- |
36
+ | Methods | FareHarbor | PeekPro| Zaui | Xola |[Ventrata](https://github.com/TourConnect/ti2-ventrata) | [TravelGateX](https://github.com/TourConnect/ti2-travelgate) | [Didgigo](https://github.com/TourConnect/ti2-didgigo) | [TourConnect](https://github.com/TourConnect/ti2-tourconnect) | TourPlan |
37
+ | ---- | -------- | ---------- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
38
38
  |**Base**|
39
- |validateToken|||✓|✓|
40
- |copyMedia||||✓|
39
+ |validateToken|✓|✓|✓|✓|✓|✓|✓|✓|✓|
40
+ |tokenTemplate|✓|✓|✓|✓|✓|✓|✓|✓|✓|
41
41
  |**Content**|
42
- |getProfile|||✓|✓|
43
- |updateProfile||||✓|
44
- |getLocations|||✓|✓|
45
- |getLocation|||✓|✓|
46
- |createLocation|||✓|✓|
47
- |updateLocation|||✓|✓|
48
- |getProducts|||✓|✓|
49
- |getProduct|||✓|✓|
50
- |createProduct|||✓|✓|
51
- |updateProduct|||✓|✓|
42
+ |copyMedia||||||||✓|
43
+ |getProfile|||||||✓|✓|
44
+ |updateProfile||||||||✓|
45
+ |getLocations|||||||✓|✓|
46
+ |getLocation|||||||✓|✓|
47
+ |createLocation|||||||✓|✓|
48
+ |updateLocation|||||||✓|✓|
49
+ |getProducts|||||||✓|✓|
50
+ |getProduct|||||||✓|✓|
51
+ |createProduct|||||||✓|✓|
52
+ |updateProduct|||||||✓|✓|
52
53
  |**Bookings**|
53
- |searchProducts|✓|✓|
54
- |searchBooking|✓|✓|
55
- |searchAvailability|✓|✓|
56
- |searchQuote|✓|✓|
57
- |createBooking|✓|✓|
58
- |cancelBooking|✓|✓|
54
+ |searchProducts|✓|✓|✓|✓|✓|✓|
55
+ |searchBooking|✓|✓|✓|✓|✓|✓|
56
+ |searchAvailability|✓|✓|✓|✓|✓|✓|
57
+ |searchQuote|✓|✓|✓|✓|✓|✓|
58
+ |createBooking|✓|✓|✓|✓|✓|✓|
59
+ |cancelBooking|✓|✓|✓|✓|✓|✓|
60
+ |searchProductsForItinerary|||||||||✓|
61
+ |searchAvailabilityForItinerary|||||||||✓|
62
+ |addServiceToItinerary|||||||||✓|
63
+ |getCreateItineraryFields|||||||||✓|
64
+ |searchItineraries|||||||||✓|
65
+ |queryAllotment|||||||||✓|
59
66
 
60
67
  ## Contributing
61
68
 
@@ -466,7 +466,7 @@ const getAppToken = async (req, res, next) => {
466
466
  },
467
467
  });
468
468
  if (!userAppKey) {
469
- return next({ status: 404, message: 'User integratio is not found' });
469
+ return next({ status: 404, message: `User integratio is not found for ${integrationId}:${userId}:${hint}` });
470
470
  }
471
471
  return res.json({ token: await userAppKey.token });
472
472
  };
@@ -6,6 +6,8 @@ const { typeDefs: availTypeDefs, query: availQuery } = require('./graphql-schema
6
6
  const { typeDefs: bookingTypeDefs, query: bookingQuery } = require('./graphql-schemas/booking');
7
7
  const { typeDefs: rateTypeDefs, query: rateQuery } = require('./graphql-schemas/rate');
8
8
  const { typeDefs: pickupTypeDefs, query: pickupQuery } = require('./graphql-schemas/pickup-point');
9
+ const { typeDefs: itineraryProductTypeDefs, query: itineraryProductQuery } = require('./graphql-schemas/itinerary-product');
10
+ const { typeDefs: itineraryBookingTypeDefs, query: itineraryBookingQuery } = require('./graphql-schemas/itinerary-booking');
9
11
 
10
12
  const typeDefsAndQueries = {
11
13
  productTypeDefs,
@@ -18,6 +20,10 @@ const typeDefsAndQueries = {
18
20
  rateQuery,
19
21
  pickupTypeDefs,
20
22
  pickupQuery,
23
+ itineraryProductTypeDefs,
24
+ itineraryProductQuery,
25
+ itineraryBookingTypeDefs,
26
+ itineraryBookingQuery,
21
27
  };
22
28
 
23
29
  const bookingsSearch = plugins => async (req, res, next) => {
@@ -37,8 +43,8 @@ const bookingsSearch = plugins => async (req, res, next) => {
37
43
  });
38
44
  assert(userAppKeys, 'could not find the app key');
39
45
  const token = await userAppKeys.token;
40
- assert(app.searchHotelBooking || app.searchBooking, `searchHotelBooking or searchBooking is not available for ${appKey}`);
41
- const search = (app.searchHotelBooking || app.searchBooking).bind(app);
46
+ assert(app.searchItineraries || app.searchHotelBooking || app.searchBooking, `searchItineraries or searchHotelBooking or searchBooking is not available for ${appKey}`);
47
+ const search = (app.searchHotelBooking || app.searchBooking || app.searchItineraries).bind(app);
42
48
  const results = await search({
43
49
  axios,
44
50
  token,
@@ -102,7 +108,8 @@ const $bookingsProductSearch = plugins => async ({
102
108
  }));
103
109
  assert(userAppKeys, 'could not find the app key');
104
110
  const token = await userAppKeys.token;
105
- const results = await app.searchProducts({
111
+ const func = (app.searchProducts || app.searchProductsForItinerary).bind(app);
112
+ const results = await func({
106
113
  axios,
107
114
  token,
108
115
  payload,
@@ -182,7 +189,8 @@ const bookingsAvailabilitySearch = plugins => async (req, res, next) => {
182
189
  }));
183
190
  assert(userAppKeys, 'could not find the app key');
184
191
  const token = await userAppKeys.token;
185
- const results = await app.searchAvailability({
192
+ const func = (app.searchAvailability || app.searchAvailabilityForItinerary).bind(app);
193
+ const results = await func({
186
194
  axios,
187
195
  token,
188
196
  payload,
@@ -292,8 +300,8 @@ const createBooking = plugins => async (req, res, next) => {
292
300
  }));
293
301
  assert(userAppKeys, 'could not find the app key');
294
302
  const token = await userAppKeys.token;
295
- assert(payload.id, 'the quote id is required');
296
- const results = await app.createBooking({
303
+ const func = (app.createBooking || app.addServiceToItinerary).bind(app);
304
+ const results = await func({
297
305
  axios,
298
306
  token,
299
307
  payload,
@@ -418,8 +426,9 @@ const getCreateBookingFields = plugins => async (req, res, next) => {
418
426
  }));
419
427
  assert(userAppKeys, 'could not find the app key');
420
428
  const token = await userAppKeys.token;
421
- assert(app.getCreateBookingFields, `getCreateBookingFields is not available for ${appKey}`);
422
- const results = await app.getCreateBookingFields({
429
+ assert(app.getCreateBookingFields || app.getCreateItineraryFields, `getCreateBookingFields or getCreateItineraryFields is not available for ${appKey}`);
430
+ const func = (app.getCreateItineraryFields || app.getCreateBookingFields).bind(app);
431
+ const results = await func({
423
432
  axios,
424
433
  token,
425
434
  payload,
@@ -0,0 +1,87 @@
1
+ const typeDefs = `
2
+ type Passenger {
3
+ firstName: String!
4
+ lastName: String!
5
+ passengerType: String!
6
+ age: Int
7
+ dob: String
8
+ personId: String
9
+ }
10
+
11
+ type PaxConfig {
12
+ roomType: String
13
+ adults: Int
14
+ children: Int
15
+ infants: Int
16
+ passengers: [Passenger]
17
+ }
18
+
19
+ type ServiceLine {
20
+ serviceLineId: ID!
21
+ optionId: ID!
22
+ optionName: String!
23
+ startDate: String!
24
+ supplierName: String
25
+ supplierId: String
26
+ paxList: [Passenger]
27
+ paxConfigs: [PaxConfig]
28
+ }
29
+
30
+ type Query {
31
+ bookingId: ID!
32
+ name: String!
33
+ bookingStatus: String!
34
+ ref: String!
35
+ agentRef: String
36
+ totalPrice: String!
37
+ travelDate: String!
38
+ enteredDate: String!
39
+ serviceLines: [ServiceLine]!
40
+ }
41
+ `;
42
+
43
+ const query = `{
44
+ name
45
+ bookingId
46
+ bookingStatus
47
+ ref
48
+ agentRef
49
+ totalPrice
50
+ travelDate
51
+ enteredDate
52
+ serviceLines {
53
+ serviceLineId
54
+ optionId
55
+ optionName
56
+ supplierName
57
+ supplierId
58
+ startDate
59
+ paxConfigs {
60
+ roomType
61
+ adults
62
+ children
63
+ infants
64
+ passengers {
65
+ firstName
66
+ lastName
67
+ passengerType
68
+ age
69
+ dob
70
+ personId
71
+ }
72
+ }
73
+ paxList {
74
+ firstName
75
+ lastName
76
+ passengerType
77
+ age
78
+ dob
79
+ personId
80
+ }
81
+ }
82
+ }`;
83
+
84
+ module.exports = {
85
+ typeDefs,
86
+ query,
87
+ };
@@ -0,0 +1,135 @@
1
+ const typeDefs = `
2
+
3
+ type Extra {
4
+ id: ID!
5
+ name: String!
6
+ chargeBasis: String
7
+ isCompulsory: Boolean
8
+ isPricePerPerson: Boolean
9
+ }
10
+
11
+ type UnitRestriction {
12
+ allowed: Boolean
13
+ minAge: Int
14
+ maxAge: Int
15
+ maxPax: Int
16
+ maxAdults: Int
17
+ }
18
+
19
+ type OptionRestrictions {
20
+ roomTypeRequired: Boolean
21
+ Adult: UnitRestriction
22
+ Child: UnitRestriction
23
+ Infant: UnitRestriction
24
+ Single: UnitRestriction
25
+ Double: UnitRestriction
26
+ Twin: UnitRestriction
27
+ Triple: UnitRestriction
28
+ Quad: UnitRestriction
29
+ }
30
+
31
+ type ProductUnit {
32
+ unitId: ID!
33
+ unitName: String!
34
+ restrictions: UnitRestriction
35
+ }
36
+
37
+ type ProductOption {
38
+ optionId: ID!
39
+ optionName: String!
40
+ lastUpdateTimestamp: Int
41
+ serviceType: String
42
+ extras: [Extra]
43
+ units: [ProductUnit]
44
+ restrictions: OptionRestrictions
45
+ }
46
+
47
+ type Query {
48
+ productId: ID!
49
+ productName: String!
50
+ address: String
51
+ description: String
52
+ serviceTypes: [String]
53
+ options: [ProductOption]
54
+ }
55
+ `;
56
+
57
+ const query = `{
58
+ productId
59
+ productName
60
+ description
61
+ serviceTypes
62
+ address
63
+ options {
64
+ optionId
65
+ optionName
66
+ lastUpdateTimestamp
67
+ serviceType
68
+ extras {
69
+ id
70
+ name
71
+ chargeBasis
72
+ isCompulsory
73
+ isPricePerPerson
74
+ }
75
+ units {
76
+ unitId
77
+ unitName
78
+ restrictions {
79
+ allowed
80
+ minAge
81
+ maxAge
82
+ maxPax
83
+ maxAdults
84
+ }
85
+ }
86
+ restrictions {
87
+ roomTypeRequired
88
+ Adult {
89
+ allowed
90
+ minAge
91
+ maxAge
92
+ }
93
+ Child {
94
+ allowed
95
+ minAge
96
+ maxAge
97
+ }
98
+ Infant {
99
+ allowed
100
+ minAge
101
+ maxAge
102
+ }
103
+ Single {
104
+ allowed
105
+ maxPax
106
+ maxAdults
107
+ }
108
+ Double {
109
+ allowed
110
+ maxPax
111
+ maxAdults
112
+ }
113
+ Twin {
114
+ allowed
115
+ maxPax
116
+ maxAdults
117
+ }
118
+ Triple {
119
+ allowed
120
+ maxPax
121
+ maxAdults
122
+ }
123
+ Quad {
124
+ allowed
125
+ maxPax
126
+ maxAdults
127
+ }
128
+ }
129
+ }
130
+ }`;
131
+
132
+ module.exports = {
133
+ typeDefs,
134
+ query,
135
+ };
@@ -6,9 +6,12 @@ const { UserAppKey } = require('../models');
6
6
  const userAppList = async (req, res, next) => {
7
7
  const { params: { userId } } = req;
8
8
  try {
9
- const userAppKeys = (await UserAppKey.findAll({ where: { userId } }))
10
- .map(userAppKey => userAppKey.dataValues);
11
- return res.json({ userAppKeys: userAppKeys.map(userAppKey => omit(['appKey', 'id'], userAppKey)) });
9
+ const userAppKeys = await UserAppKey.findAll({
10
+ where: { userId },
11
+ raw: true,
12
+ attributes: ['integrationId', 'userId', 'hint', 'createdAt', 'updatedAt'],
13
+ })
14
+ return res.json({ userAppKeys });
12
15
  } catch (err) {
13
16
  return next(err);
14
17
  }
package/index.js CHANGED
@@ -270,7 +270,8 @@ module.exports = async ({
270
270
  ];
271
271
  const body = req.customBody;
272
272
  if (cachingOperations.indexOf(body.operationId) > -1) {
273
- const cacheKey = hash(R.omit(['requestId', 'date'], body));
273
+ const cacheBody = R.omit(['requestId', 'date'], body);
274
+ const cacheKey = hash(cacheBody);
274
275
  req.cacheKey = cacheKey;
275
276
  const foundCache = await cache.get({
276
277
  pluginName: body.params.appKey,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ti2",
3
- "version": "1.0.79",
3
+ "version": "1.0.81",
4
4
  "description": "Tourist Industry Exchange (TI2)",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/test/plugin.js CHANGED
@@ -54,7 +54,36 @@ class Plugin {
54
54
  });
55
55
  this.searchQuote = jestPlugin.fn(() => ({ quote: [{ id: chance.guid() }] }));
56
56
  this.createBooking = jestPlugin.fn(() => {});
57
- this.queryAllotment = jestPlugin.fn(async (args) => {
57
+ this.searchItineraries = jestPlugin.fn(() => ({ bookings: [] }));
58
+ this.searchProductsForItinerary = jestPlugin.fn(() => ({ products: [] }));
59
+ this.searchAvailabilityForItinerary = jestPlugin.fn(({
60
+ token,
61
+ payload: {
62
+ optionId,
63
+ startDate,
64
+ paxConfigs,
65
+ },
66
+ }) => {
67
+ expect(token).toBeTruthy();
68
+ expect(optionId).toBeTruthy();
69
+ expect(startDate).toBeTruthy();
70
+ expect(paxConfigs).toBeTruthy();
71
+ return { bookable: true, rates: [{ rateId: 'Default' }] };
72
+ });
73
+ this.getCreateItineraryFields = jestPlugin.fn(() => ({
74
+ customFields: [],
75
+ }));
76
+ this.addServiceToItinerary = jestPlugin.fn(() => ({
77
+ message: '',
78
+ booking: {
79
+ id: chance.guid(),
80
+ reference: chance.guid(),
81
+ linePrice: chance.guid(),
82
+ lineId: chance.guid(),
83
+ },
84
+ }));
85
+
86
+ this.queryAllotment = jestPlugin.fn(async args => {
58
87
  const {
59
88
  axios,
60
89
  payload,
@@ -65,7 +94,7 @@ class Plugin {
65
94
  if (payload.keyPath === 'errorGeneral') {
66
95
  await axios.get('http://www.example.com');
67
96
  }
68
- return { allotments: [] }
97
+ return { allotments: [] };
69
98
  });
70
99
  this.tokenTemplate = jestPlugin.fn(() => ({
71
100
  apiKey: {
@@ -443,7 +472,7 @@ class Plugin {
443
472
  * @returns {object} retVal - the return object
444
473
  * @returns {Availability[]} retVal.availability - Array of availability objects
445
474
  */
446
- availabilityCalndar() {
475
+ availabilityCalendar() {
447
476
  // return (args);
448
477
  }
449
478
 
@@ -479,6 +508,216 @@ class Plugin {
479
508
  */
480
509
  createBooking() {}
481
510
 
511
+ /**
512
+ * @typedef {Object} Passenger
513
+ * @property {string} firstName - Passenger first name
514
+ * @property {string} lastName - Passenger last name
515
+ * @property {PassengerType} passengerType - Passenger type
516
+ * @property {number} [age] - Passenger age
517
+ * @property {string} [dob] - Date of birth (YYYY-MM-DD)
518
+ * @property {string} [personId] - Unique identifier for the passenger
519
+ */
520
+
521
+ /**
522
+ * @typedef {('Single'|'Double'|'Twin'|'Triple'|'Quad'|'Other')} RoomType
523
+ */
524
+
525
+ /**
526
+ * @typedef {('Adult'|'Child'|'Infant')} PassengerType
527
+ */
528
+
529
+ /**
530
+ * @typedef {Object} PaxConfig - At least one of adults, children or infants must be provided
531
+ * @property {RoomType} [roomType] - Room type
532
+ * @property {number} [adults] - Number of adults
533
+ * @property {number} [children] - Number of children
534
+ * @property {number} [infants] - Number of infants
535
+ * @property {Array<Passenger>} [passengers] - List of passengers for this pax config
536
+ */
537
+
538
+ /**
539
+ * @typedef {Object} PUDOInfo
540
+ * @property {string} [time] - Travel time
541
+ * @property {string} [location] - Location details
542
+ * @property {string} [flightDetails] - Flight information
543
+ */
544
+
545
+
546
+ // ItineraryProduct Related Types
547
+ /**
548
+ * @typedef {Object} ItineraryProductUnit
549
+ * @property {RoomType|PassengerType} unitId - Unit identifier for Accommodation products, allowed values: 'Single', 'Double', 'Twin', 'Triple', 'Quad'; for Activity products, allowed values: 'Adult', 'Child', 'Infant'
550
+ * @property {string} unitName - Unit name, can be the same as unitId
551
+ * @property {ItineraryUnitRestriction} restrictions - Unit restrictions
552
+ */
553
+
554
+ /**
555
+ * @typedef {Object} ItineraryUnitRestriction
556
+ * @property {boolean} allowed - Whether the unit is available
557
+ * @property {number} [minAge] - Minimum age requirement
558
+ * @property {number} [maxAge] - Maximum age requirement
559
+ */
560
+
561
+ /**
562
+ * @typedef {Object} ItineraryProductOption
563
+ * @property {string} optionId - Option identifier, this identifier should be unique in a sense that it should be able to represent the option and the product. For example, if in your system the option identifier are 1, 2, 3, say the productId is '123', the optionId you provided to us should be something like '123-1' or '123-2' etc.
564
+ * @property {string} optionName - Option name
565
+ * @property {number} [lastUpdateTimestamp] - Last update time
566
+ * @property {string} serviceType - Type of service, one of 'Accommodation', 'Activity', 'Transfer'
567
+ * @property {Array<Extra>} extras - Extras allowed for this product option
568
+ * @property {Array<ItineraryProductUnit>} units - Available units
569
+ * @property {Object} restrictions - Aggregated restrictions of each unit for this product option
570
+ * @property {boolean} [restrictions.roomTypeRequired] - Whether room type is required, if it's required, our IA tool will check if the roomType in the paxConfigs matches the roomType allowed in the option
571
+ * @property {ItineraryUnitRestriction} [restrictions.Adult] - restrictions for adult
572
+ * @property {ItineraryUnitRestriction} [restrictions.Child] - restrictions for child
573
+ * @property {ItineraryUnitRestriction} [restrictions.Infant] - restrictions for infant
574
+ * @property {ItineraryUnitRestriction} [restrictions.Single] - restrictions for Single room type
575
+ * @property {ItineraryUnitRestriction} [restrictions.Double] - restrictions for Double room type
576
+ * @property {ItineraryUnitRestriction} [restrictions.Twin] - restrictions for Twin room type
577
+ * @property {ItineraryUnitRestriction} [restrictions.Triple] - restrictions for Triple room type
578
+ * @property {ItineraryUnitRestriction} [restrictions.Quad] - restrictions for Quad room type
579
+ */
580
+
581
+ /**
582
+ * @typedef {Object} ItineraryProduct - An itinerary product contains two levels: product, option. For accommodation products, the product is the hotel, the option is the room name (Deluxe Room, Ocean View Room, etc). For activity products, the product is the Tour Company, the option is the activity type (e.g. Full Day, Half Day, etc). Specific data examples will be provided in the tutorial page.
583
+ * @property {string} productId - ItineraryProduct identifier
584
+ * @property {string} productName - ItineraryProduct name
585
+ * @property {Array<string>} [serviceTypes] - Types of services offered: ['Accommodation', 'Activity', 'Transfer']
586
+ * @property {Array<ItineraryProductOption>} options - Available options
587
+ */
588
+
589
+ // Booking Related Types
590
+ /**
591
+ * @typedef {Object} CustomFieldValue
592
+ * @property {string} id - Custom Field identifier
593
+ * @property {string} value - Field value
594
+ */
595
+
596
+ /**
597
+ * @typedef {Object} Extra
598
+ * @property {string} id - Extra identifier
599
+ * @property {string} name - Extra name
600
+ */
601
+
602
+ /**
603
+ * @typedef {Object} ItineraryServiceLine
604
+ * @property {string} serviceLineId - Service line identifier
605
+ * @property {string} optionId - Option identifier
606
+ * @property {string} optionName - Option name
607
+ * @property {string} [supplierName] - Supplier name
608
+ * @property {string} [supplierId] - Supplier id
609
+ * @property {string} startDate - Start date (YYYY-MM-DD)
610
+ * @property {Array<PaxConfig>} paxConfigs - List of pax configs for this service line
611
+ * @property {Array<Passenger>} paxList - List of passengers for this service line
612
+ */
613
+
614
+ /**
615
+ * @typedef {Object} ItineraryBooking
616
+ * @property {string} bookingId - Unique booking identifier
617
+ * @property {string} name - Booking name
618
+ * @property {string} bookingStatus - Booking status
619
+ * @property {string} ref - Booking reference
620
+ * @property {string} agentRef - external booking reference
621
+ * @property {string} totalPrice - Total price of the booking
622
+ * @property {string} travelDate - Start Date of travel (YYYY-MM-DD)
623
+ * @property {string} enteredDate - Date of booking (YYYY-MM-DD)
624
+ * @property {ItineraryServiceLine[]} serviceLines - Booked service lines
625
+ */
626
+
627
+ /**
628
+ * @typedef {('yes-no'|'short'|'long'|'count'|'extended-option')} CustomFieldTypes
629
+ /**
630
+ * @typedef {Object} CreateItineraryCustomField
631
+ * @property {string} id - Custom Field identifier
632
+ * @property {string} label - Custom Field label
633
+ * @property {CustomFieldTypes} type - Custom Field type
634
+ * @property {boolean} [isPerService] - Whether the custom field is per service, or per entire booking/quote
635
+ * @property {Array<Object>} [options] - Options if the type is extended-option
636
+ * @property {Object} options.value - Option value
637
+ * @property {Object} options.label - Option label
638
+ */
639
+
640
+ /**
641
+ * Search for bookable products
642
+ * @async
643
+ * @param {Object} args
644
+ * @param {Object} args.token - Authentication token
645
+ * @param {object} args.payload - Search criteria
646
+ * @param {string} [args.payload.productId] - Optional specific product to search for
647
+ * @param {string} [args.payload.optionId] - Optional specific product option to search for
648
+ * @param {string} [args.payload.searchInput] - Optional text search across all fields
649
+ * @param {boolean} [args.payload.forceRefresh] - Optional flag to bypass cache
650
+ * @returns {object} retVal - the return object.
651
+ * @returns {Array<ItineraryProduct>} retVal.products - An array of products spec objects.
652
+ */
653
+ searchProductsForItinerary() {}
654
+
655
+ /**
656
+ * @typedef {Object} SearchAvailabilityForItineraryResponse
657
+ * @property {boolean} bookable - Whether the product is bookable
658
+ * @property {Array} [rates] - If there are multiple rates, they will be returned in this array
659
+ * @property {string} rates[].rateId - It's ok if you don't use such a thing as rateId in your internal system, but you can use it to package important information so we can make a booking. For example, you can do jwt.encode({ foo: 'bar' }, secret) and send it to us here, and later when we make a booking, we send the rateId to you so you can decode it and get the foo: 'bar' information.
660
+ * @property {string} [rates[].externalRateText] - Combined rate description
661
+ */
662
+ /**
663
+ * Search for product availability
664
+ * @async
665
+ * @param {Object} args
666
+ * @param {Object} args.token - Authentication token
667
+ * @param {object} args.payload - Search criteria
668
+ * @param {string} args.payload.optionId - ItineraryProduct option ID
669
+ * @param {string} args.payload.startDate - YYYY-MM-DD format
670
+ * @param {Array<PaxConfig>} args.payload.paxConfigs - Required for room-based products
671
+ * @param {number} [args.payload.chargeUnitQuantity=1] - Number of units to book
672
+ * @returns {SearchAvailabilityForItineraryResponse}
673
+ */
674
+ searchAvailabilityForItinerary() {}
675
+
676
+ /**
677
+ * @typedef {Object} AddServiceToItineraryResponse
678
+ * @property {string} message - Error message
679
+ * @property {object} booking - Booking object
680
+ * @property {string} booking.id - Booking identifier from your system
681
+ * @property {string} booking.reference - Reference number from your system
682
+ * @property {string} booking.linePrice - Price of the booking line
683
+ * @property {string} booking.lineId - Line identifier from your system
684
+ */
685
+ /**
686
+ * Create an itinerary
687
+ * @async
688
+ * @param {Object} args
689
+ * @param {Object} args.token - Authentication token
690
+ * @param {object} args.payload - Booking/Quote details
691
+ * @param {string} args.payload.QB - 'Q' for quote, 'B' for booking
692
+ * @param {string} args.payload.quoteName - Name of the booking/quote
693
+ * @param {string} [args.payload.quoteId] - identifier of the booking/quote, if one is provided, we are expecting the service line to be inserted to an existing booking/quote
694
+ * @param {string} [args.payload.lineId] - identifier of the service line in the booking/quote, if one is provided, we are expecting an existing service line to be updated
695
+ * @param {string} [args.payload.rateId] - Rate identifier, we are sending 'Default' if no rates are provided by the check availability call
696
+ * @param {string} args.payload.optionId - Option identifier of the service line
697
+ * @param {string} args.payload.startDate - Start date always in (YYYY-MM-DD) format
698
+ * @param {string} [args.payload.reference] - Reference number from external system
699
+ * @param {Array<PaxConfig>} args.payload.paxConfigs - Passenger configurations
700
+ * @param {Array<Object>} [args.payload.extras] - Additional extras
701
+ * @param {Extra} args.payload.extras.selectedExtra - Selected extra
702
+ * @param {number} args.payload.extras.quantity - quantity
703
+ * @param {PUDOInfo} [args.payload.puInfo] - Pickup information
704
+ * @param {PUDOInfo} [args.payload.doInfo] - Dropoff information
705
+ * @param {string} [args.payload.notes] - Additional notes
706
+ * @param {Array<CustomFieldValue>} [args.payload.customFieldValues] - Custom field values
707
+ * @returns {AddServiceToItineraryResponse}
708
+ */
709
+ addServiceToItinerary() {}
710
+
711
+ /**
712
+ * Some additional fields available for the addServiceToItinerary call
713
+ * @async
714
+ * @param {Object} args
715
+ * @param {Object} args.token - Authentication token
716
+ * @returns {object} retVal - the return object.
717
+ * @returns {Array<CreateItineraryCustomField>} retVal.customFields - An array of custom fields spec objects.
718
+ */
719
+ getCreateItineraryFields() {}
720
+
482
721
  /**
483
722
  * Allotment Object
484
723
  /**
@@ -496,6 +735,23 @@ class Plugin {
496
735
  * @property {string[]} keyPaths - An array of key paths generated by combining supplier code and product codes.
497
736
  */
498
737
 
738
+ /**
739
+ * Search for itineraries
740
+ * @async
741
+ * @param {Object} args
742
+ * @param {Object} args.token - Authentication token
743
+ * @param {object} args.payload - Search criteria
744
+ * @param {string} args.payload.purchaseDateStart - Start date for purchase search (YYYY-MM-DD)
745
+ * @param {string} args.payload.purchaseDateEnd - End date for purchase search (YYYY-MM-DD)
746
+ * @param {string} args.payload.travelDateStart - Start date for travel search (YYYY-MM-DD)
747
+ * @param {string} args.payload.travelDateEnd - End date for travel search (YYYY-MM-DD)
748
+ * @param {string} args.payload.name - Search by customer name
749
+ * @param {string} args.payload.bookingId - Search by booking ID
750
+ * @returns {object} retVal - the return object.
751
+ * @returns {Array<ItineraryBooking>} retVal.bookings - An array of itinerary bookings matching search criteria.
752
+ */
753
+ searchItineraries() {}
754
+
499
755
  /**
500
756
  * Query Allotment
501
757
  * @async
@@ -4,5 +4,8 @@
4
4
  },
5
5
  "plugin-development": {
6
6
  "title": "Plugin Development"
7
+ },
8
+ "itinerary-assist-plugin-dev": {
9
+ "title": "Itinerary Assist Plugin Tutorial"
7
10
  }
8
11
  }
@@ -0,0 +1,403 @@
1
+
2
+ ### Important notes:
3
+ - Required methods:
4
+ - [searchProductsForItinerary]{@link Plugin#searchProductsForItinerary}
5
+ - [searchAvailabilityForItinerary]{@link Plugin#searchAvailabilityForItinerary}
6
+ - [addServiceToItinerary]{@link Plugin#addServiceToItinerary}
7
+ - [searchItineraries]{@link Plugin#searchItineraries}
8
+ - Optional methods:
9
+ - [getCreateItineraryFields]{@link Plugin#getCreateItineraryFields}
10
+
11
+ - If a key in the payload is said to be optional, it may be undefined or null, please handle them accordingly
12
+ - expected return structure should be exactly as described in the documentation, it's ok to include extra fields in the response, but please note that our IA tool will only handle the fields that are described in the documentation
13
+ ---
14
+
15
+ ### Existing Itinerary Assist Plugin
16
+ [Tourplan](https://github.com/tourconnect/ti2-tourplan)
17
+ Feel free to use it as a reference for how tests are written and how it handles the authentication and data mapping.
18
+
19
+
20
+ ### FAQ
21
+
22
+ 1. For searchProductsForItinerary, it says "when we send empty payload, the expected response is the entire list of ACTIVE products". How do I handle pagination if my system has thousands of products? Is there a limit?
23
+
24
+ We have to get access to the entire list of products, so we can create a vector database for AI to match the extracted text with a service. We recommend using a cache to store the products on your end and refresh it periodically.
25
+
26
+ 2. My booking system uses different terms for passenger types (e.g., "CHD" instead of "Child"). Should I handle this mapping in the plugin, or will Ti2 provide a mapping configuration?
27
+
28
+ You should handle this mapping in the plugin, as it's specific to your system.
29
+
30
+ 3. How should I handle currency conversion? My system supports multiple currencies, but I don't see currency fields in the response formats.
31
+
32
+ It's not required to include currency fields in the response, as the Itinerary Assist tool currently doesn't handle pricing related operations.
33
+
34
+ 4. For real-time availability checks in searchAvailabilityForItinerary, what's the expected response time limit? My booking system might need to query multiple suppliers.
35
+
36
+ The availbility check is always just for one service. And we expect the response time to be less than 30 seconds.
37
+
38
+ 5. Will Ti2 provide a validation suite to verify my plugin's responses match the expected format?
39
+
40
+ No, we currently rely on you to validate the response format, but we are working on a validation suite that will be available in the future.
41
+
42
+ 6. The plugin.js file shows Jest tests, but are there specific test scenarios I need to cover?
43
+
44
+ No, we don't have a specific test scenarios, but we recommend you to cover the following:
45
+ - Authentication
46
+ - Response format for all operations
47
+
48
+
49
+ ## Examples
50
+
51
+ ### [searchProductsForItinerary]{@link Plugin#searchProductsForItinerary}
52
+
53
+ **We need the entire list of ACTIVE products so that we can create vector database for AI to match the extracted text with a service**
54
+
55
+
56
+ #### Example Payload
57
+ **Note: when we send empty payload, the expected response is the entire list of ACTIVE products**
58
+ ```typescript
59
+ {}
60
+ ```
61
+
62
+ #### Example Response
63
+ **Note: We provided graphql types and queries for Itinerary Product, in hope for a easier way to map your data to our format. However, if you are not familiar with graphql, you don't have to use them, just make sure the response is in the correct format**
64
+ ```typescript
65
+ {
66
+ "products": [
67
+ // Example of an Accommodation product
68
+ {
69
+ "productId": "1",
70
+ "productName": "DoubleTree by Hilton Angel Kings Cross",
71
+ "serviceTypes": [
72
+ "Accommodation"
73
+ ],
74
+ "options": [
75
+ {
76
+ "extras": [],
77
+ "lastUpdateTimestamp": 1586595549,
78
+ "optionId": "106784",
79
+ "optionName": "Bed and English Breakfast-Executive Room",
80
+ "restrictions": {
81
+ "Adult": {
82
+ "allowed": true,
83
+ "maxAge": "999",
84
+ "minAge": "16"
85
+ },
86
+ "Child": {
87
+ "allowed": true,
88
+ "maxAge": "15",
89
+ "minAge": "2"
90
+ },
91
+ "Double": {
92
+ "allowed": true,
93
+ "maxAdults": "2",
94
+ "maxPax": "2"
95
+ },
96
+ "Infant": {
97
+ "allowed": true,
98
+ "maxAge": "1",
99
+ "minAge": "0"
100
+ },
101
+ "Quad": {
102
+ "allowed": false
103
+ },
104
+ "Single": {
105
+ "allowed": true,
106
+ "maxAdults": "1",
107
+ "maxPax": "1"
108
+ },
109
+ "Triple": {
110
+ "allowed": false
111
+ },
112
+ "Twin": {
113
+ "allowed": false
114
+ },
115
+ "roomTypeRequired": true
116
+ },
117
+ "serviceType": "Accommodation",
118
+ "units": [
119
+ {
120
+ "restrictions": {
121
+ "allowed": true,
122
+ "paxCount": "1"
123
+ },
124
+ "unitId": "Single",
125
+ "unitName": "Single"
126
+ },
127
+ {
128
+ "restrictions": {
129
+ "allowed": false
130
+ },
131
+ "unitId": "Twin",
132
+ "unitName": "Twin"
133
+ },
134
+ {
135
+ "restrictions": {
136
+ "allowed": true,
137
+ "paxCount": "2"
138
+ },
139
+ "unitId": "Double",
140
+ "unitName": "Double"
141
+ },
142
+ {
143
+ "restrictions": {
144
+ "allowed": false
145
+ },
146
+ "unitId": "Quad",
147
+ "unitName": "Quad"
148
+ }
149
+ ]
150
+ }
151
+ ]
152
+ },
153
+ // Example of a Transfers product
154
+ {
155
+ "productId": "6489",
156
+ "productName": "Davids of London Ltd",
157
+ "serviceTypes": [
158
+ "Transfers"
159
+ ],
160
+ "options": [
161
+ {
162
+ "extras": [],
163
+ "lastUpdateTimestamp": 1700750093,
164
+ "optionId": "LONTRDAVIDSHDWBVC",
165
+ "optionName": "Half-Day Warner Bros Studios (6-Hours)-FIT- V- Class (1-5 Pax)",
166
+ "restrictions": {
167
+ "Adult": {
168
+ "allowed": true,
169
+ "maxAge": "999",
170
+ "minAge": "16"
171
+ },
172
+ "Child": {
173
+ "allowed": true,
174
+ "maxAge": "15",
175
+ "minAge": "5"
176
+ },
177
+ "Double": {
178
+ "allowed": false
179
+ },
180
+ "Infant": {
181
+ "allowed": true,
182
+ "maxAge": "4",
183
+ "minAge": "0"
184
+ },
185
+ "Quad": {
186
+ "allowed": false
187
+ },
188
+ "Single": {
189
+ "allowed": false
190
+ },
191
+ "Triple": {
192
+ "allowed": false
193
+ },
194
+ "Twin": {
195
+ "allowed": false
196
+ },
197
+ "roomTypeRequired": false
198
+ },
199
+ "serviceType": "Transfers",
200
+ "units": [
201
+ {
202
+ "restrictions": {
203
+ "maxAge": "999",
204
+ "minAge": "16"
205
+ },
206
+ "unitId": "Adults",
207
+ "unitName": "Adults"
208
+ },
209
+ {
210
+ "restrictions": {
211
+ "maxAge": "15",
212
+ "minAge": "5"
213
+ },
214
+ "unitId": "Children",
215
+ "unitName": "Children"
216
+ },
217
+ {
218
+ "restrictions": {
219
+ "maxAge": "4",
220
+ "minAge": "0"
221
+ },
222
+ "unitId": "Infants",
223
+ "unitName": "Infants"
224
+ }
225
+ ]
226
+ }
227
+ ]
228
+ }
229
+ ]
230
+ }
231
+ ```
232
+
233
+
234
+ ### [searchAvailabilityForItinerary]{@link Plugin#searchAvailabilityForItinerary}
235
+
236
+ #### Example Payload
237
+
238
+ ```typescript
239
+ {
240
+ optionId: 'LONTRDAVIDSHDWBVC',
241
+ startDate: '2025-04-01',
242
+ chargeUnitQuantity: 1,
243
+ paxConfigs: [{ roomType: 'DB', adults: 2 }],
244
+ }
245
+ ```
246
+ #### ExampleResponse
247
+
248
+ ```typescript
249
+ {
250
+ bookable: true,
251
+ rates: [{
252
+ rateId: '123',
253
+ externalRateText: 'Example rate description'
254
+ }]
255
+ }
256
+ ```
257
+
258
+
259
+
260
+ ### [addServiceToItinerary]{@link Plugin#addServiceToItinerary}
261
+
262
+ #### Example Payload
263
+ ```javascript
264
+ {
265
+ quoteName: String,
266
+ rateId: String, // Optional
267
+ quoteId: String, // Optional
268
+ optionId: String,
269
+ startDate: String, // YYYY-MM-DD
270
+ reference: String, // Optional
271
+ paxConfigs: [{
272
+ roomType: String, // Optional
273
+ passengers: [{ // Optional
274
+ firstName: String,
275
+ lastName: String,
276
+ passengerType: String, // 'Adult' | 'Child' | 'Infant'
277
+ age: Number, // Optional
278
+ dob: String, // Optional, YYYY-MM-DD
279
+ personId: String // Optional
280
+ }]
281
+ }],
282
+ extras: [{ // Optional
283
+ selectedExtra: {
284
+ id: String
285
+ },
286
+ quantity: Number
287
+ }],
288
+ puInfo: { // Optional, pickup information
289
+ time: String, // Optional
290
+ location: String, // Optional
291
+ flightDetails: String // Optional
292
+ },
293
+ doInfo: { // Optional, dropoff information
294
+ time: String, // Optional
295
+ location: String, // Optional
296
+ flightDetails: String // Optional
297
+ },
298
+ notes: String, // Optional
299
+ QB: 'Q', // Q for Quote, B for Booking
300
+ customFieldValues: [{ // Optional
301
+ id: String,
302
+ value: String
303
+ }]
304
+ }
305
+ ```
306
+
307
+ #### Example Response
308
+
309
+ ```typescript
310
+ {
311
+ message: 'Booking created successfully',
312
+ booking: {
313
+ id: '123',
314
+ reference: 'ref-123',
315
+ linePrice: '100',
316
+ lineId: 'lineId-123',
317
+ }
318
+ }
319
+ ```
320
+
321
+ ### [searchItineraries]{@link Plugin#searchItineraries}
322
+
323
+
324
+ #### Example Payload
325
+
326
+ **Note: The payload must include either:**
327
+ - purchaseDateStart AND purchaseDateEnd, OR
328
+ - travelDateStart AND travelDateEnd, OR
329
+ - name, OR
330
+ - bookingId
331
+
332
+ ```javascript
333
+
334
+ {
335
+ purchaseDateStart: String, // Optional, YYYY-MM-DD
336
+ purchaseDateEnd: String, // Optional, YYYY-MM-DD
337
+ travelDateStart: String, // Optional, YYYY-MM-DD
338
+ travelDateEnd: String, // Optional, YYYY-MM-DD
339
+ name: String, // Optional
340
+ bookingId: String // Optional
341
+ }
342
+ ```
343
+
344
+
345
+ #### Example Response
346
+ **Note: We provided graphql types and queries for Itinerary Booking, in hope for a easier way to map your data to our format. However, if you are not familiar with graphql, you don't have to use them, just make sure the response is in the correct format**
347
+ ```javascript
348
+ {
349
+ "bookings": [{
350
+ "agentRef": "2356674/1",
351
+ "bookingId": "316559",
352
+ "bookingStatus": "Quotation",
353
+ "enteredDate": "2024-09-12",
354
+ "ref": "ALFI393706",
355
+ "serviceLines": [{
356
+ "optionId": "LONHOSANLONBFBDLX",
357
+ "optionName": "Bed and Full Buffet Breakfast",
358
+ "paxConfigs": [{
359
+ "adults": 1,
360
+ "children": 0,
361
+ "infants": 0,
362
+ "passengers": [{
363
+ "age": null,
364
+ "dob": null,
365
+ "firstName": "Sean",
366
+ "lastName": "Conta",
367
+ "passengerType": "Adult",
368
+ "personId": "628199",
369
+ },
370
+ ],
371
+ "roomType": "DB",
372
+ },
373
+ ],
374
+ "paxList": [{
375
+ "age": null,
376
+ "dob": null,
377
+ "firstName": "Sean",
378
+ "lastName": "Conta",
379
+ "passengerType": "Adult",
380
+ "personId": "628199",
381
+ },
382
+ ],
383
+ "serviceLineId": "745684",
384
+ "startDate": "2025-08-13",
385
+ },
386
+ ],
387
+ "totalPrice": "187795",
388
+ "travelDate": "2025-08-13",
389
+ },
390
+ ],
391
+ }
392
+ ```
393
+
394
+ #### Example
395
+ ```javascript
396
+ await searchItineraries({
397
+ payload: {
398
+ purchaseDateStart: '2024-01-01',
399
+ purchaseDateEnd: '2024-12-31',
400
+ name: 'John Doe'
401
+ }
402
+ })
403
+ ```
@@ -2,6 +2,21 @@
2
2
 
3
3
  Plugins can extend the functionality of Ti2 and/or provide access to other platforms; you should use one of the following methods to provide a stadandarized way to access other systems; if you want to implement new functionality that is not encompassed on the available methods you are more than welcome to add new methods and contribute to the Ti2 codebase.
4
4
 
5
+
6
+ ## Steps to develop a plugin
7
+ 1. Contact us to get access to the ti2-<your plugin name> repository.
8
+ - We will create a skeleton repository for you and add you as a contributor.
9
+ 2. Clone the repository and start developing your plugin.
10
+ 3. Follow respective tutorials to develop your plugin.
11
+ - [A Plugin for Itinerary Assist AI]{@tutorial itinerary-assist-plugin-dev}
12
+ 4. Make sure to add tests for your plugin. For more information, refer to the [Testing Suite](#testing-suite) section.
13
+ 5. Contact us to deploy your plugin to our hosted staging instance.
14
+ - Please let us know any environment variables that are required for your plugin to work. For more information, refer to the [Environment variables](#environment-variables) section.
15
+ - We will provide you with tokens and endpoints to test your plugin.
16
+ 6. Test your plugin against our staging instance.
17
+ 7. Contact us once you have tested your plugin and it's working as expected.
18
+ - We will deploy your plugin to our production instance.
19
+
5
20
  ## Available Methods
6
21
 
7
22
  We are using a set of standardized methods to maintain compatibility; the following methods are available, more can be supported but should be a added to the TI2 spec first to maximize it's compatibility.
@@ -24,10 +39,15 @@ We are using a set of standardized methods to maintain compatibility; the follow
24
39
  * [searchAvailability]{@link Plugin#searchAvailability}
25
40
  * [searchQuote]{@link Plugin#searchQuote}
26
41
  * [createBooking]{@link Plugin#createBooking}
27
-
42
+ * [searchProductsForItinerary]{@link Plugin#searchProductsForItinerary}
43
+ * [searchAvailabilityForItinerary]{@link Plugin#searchAvailabilityForItinerary}
44
+ * [addServiceToItinerary]{@link Plugin#addServiceToItinerary}
45
+ * [getCreateItineraryFields]{@link Plugin#getCreateItineraryFields}
46
+ * [searchItineraries]{@link Plugin#searchItineraries}
47
+ * [queryAllotment]{@link Plugin#queryAllotment}
28
48
  ---
29
49
 
30
- ## Environment variables
50
+ ## <a id="environment-variables"></a> Environment variables
31
51
 
32
52
  Plugin environment keys are passed down from the hosting ti2 instance when they are generated, this is the preferred way to access environment keys from the plugins, such values are not design to hold client / user's API keys or specific data, they are to be stored in the database via the AppKey collection.
33
53
 
@@ -40,6 +60,8 @@ ti2_tourconnect_apiUrl=http://backend:8080
40
60
 
41
61
  ## Codebase setup
42
62
 
63
+ **##Skip this section if you are using the skeleton repository.##**
64
+
43
65
  You can review some of the plugins previously developed and use them as a guide, plugin methods are encourage to include tests and to return results with the same format; however, the returned values can include additional information, currently Ti2 is expected to run on node version 12.22.8; we suggest you use the same for your codebase.
44
66
 
45
67
 
@@ -51,6 +73,8 @@ $ node init .
51
73
 
52
74
  ## Entry file / constructor
53
75
 
76
+ **##Skip this section if you are using the skeleton repository.##**
77
+
54
78
  The plugin is expected to have an index.js file that exports a Plugin Class like so:
55
79
 
56
80
  ```javascript
@@ -79,6 +103,8 @@ The plugin named ti2-ventrata would receive acceptLanguage variable and it's val
79
103
 
80
104
  ## Method calling
81
105
 
106
+ **##Skip this section if you are using the skeleton repository.##**
107
+
82
108
  On the previous example code we are declaring a validateToken method; we are normally expected to receive two parameters one is token and the second one payload.
83
109
 
84
110
  ```javascript
@@ -104,7 +130,7 @@ module.exports = Plugin;
104
130
 
105
131
  The token parameter should store all the settings for the current configured end user; on these example we are defaulting this settings to the ones configured to the pluging when it was instanced; so if the user settings do not include an apiUrl it will fall back to the environment variable on the running server.
106
132
 
107
- ## Testing Suite
133
+ ## <a id="testing-suite"></a> Testing Suite
108
134
 
109
135
  The plugin should contain a test file, for the following example assumes we will be using jest as the testing platform.
110
136
 
@@ -163,6 +189,8 @@ After setting up a [Ti2 instance]{@tutorial setup-your-instance} you can add you
163
189
 
164
190
  ## Extending the base API
165
191
 
192
+ **##Typically not needed##**
193
+
166
194
  Ti2 uses the [Swagger API specification](https://swagger.io/specification/) standard to define it's own methods, you can review the basic methods [here](https://ti2-staging.tourconnect.com/api-docs/); these methods can be extended using the same format; this allows any plugin to extend the base API endpoints that are linked to any plugin method (part of the basic methods or new ones).
167
195
 
168
196
  All the extender methods would be availble under the /\[plugin name] namespace, i.e.: /ti2-greatPlugin/ping.
@@ -212,6 +240,8 @@ paths:
212
240
 
213
241
  ## Database Migrations (plugin's own database tables)
214
242
 
243
+ **##Typically not needed##**
244
+
215
245
  Ti2 uses [Sequelize v6.13](https://sequelize.org/v6) which is the database ORM we ecourage you to use, you can add your own tables via migrations that should be placed under a migrations folder on the root of the plugin folder.
216
246
 
217
247
  Migrations should be run after the fact, from the root of ti2 instance like so: