ti2 1.0.80 → 1.0.82

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,94 @@
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: String!
22
+ optionName: String
23
+ linePrice: String
24
+ quantity: Int
25
+ startDate: String!
26
+ supplierName: String
27
+ supplierId: String
28
+ paxList: [Passenger]
29
+ paxConfigs: [PaxConfig]
30
+ }
31
+
32
+ type Query {
33
+ bookingId: ID!
34
+ name: String!
35
+ bookingStatus: String!
36
+ ref: String!
37
+ agentRef: String
38
+ totalPrice: String!
39
+ travelDate: String!
40
+ enteredDate: String!
41
+ canEdit: Boolean!
42
+ serviceLines: [ServiceLine]!
43
+ }
44
+ `;
45
+
46
+ const query = `{
47
+ name
48
+ bookingId
49
+ bookingStatus
50
+ ref
51
+ agentRef
52
+ totalPrice
53
+ travelDate
54
+ enteredDate
55
+ canEdit
56
+ serviceLines {
57
+ serviceLineId
58
+ linePrice
59
+ quantity
60
+ optionId
61
+ optionName
62
+ supplierId
63
+ supplierName
64
+ supplierId
65
+ startDate
66
+ paxConfigs {
67
+ roomType
68
+ adults
69
+ children
70
+ infants
71
+ passengers {
72
+ firstName
73
+ lastName
74
+ passengerType
75
+ age
76
+ dob
77
+ personId
78
+ }
79
+ }
80
+ paxList {
81
+ firstName
82
+ lastName
83
+ passengerType
84
+ age
85
+ dob
86
+ personId
87
+ }
88
+ }
89
+ }`;
90
+
91
+ module.exports = {
92
+ typeDefs,
93
+ query,
94
+ };
@@ -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
+ };
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.80",
3
+ "version": "1.0.82",
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,219 @@ 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} [linePrice] - Line price
610
+ * @property {string} [quantity] - Quantity
611
+ * @property {string} startDate - Start date (YYYY-MM-DD)
612
+ * @property {Array<PaxConfig>} paxConfigs - List of pax configs for this service line
613
+ * @property {Array<Passenger>} paxList - List of passengers for this service line
614
+ */
615
+
616
+ /**
617
+ * @typedef {Object} ItineraryBooking
618
+ * @property {string} bookingId - Unique booking identifier
619
+ * @property {string} name - Booking name
620
+ * @property {string} bookingStatus - Booking status
621
+ * @property {string} ref - Booking reference
622
+ * @property {string} agentRef - external booking reference
623
+ * @property {string} totalPrice - Total price of the booking
624
+ * @property {string} travelDate - Start Date of travel (YYYY-MM-DD)
625
+ * @property {string} enteredDate - Date of booking (YYYY-MM-DD)
626
+ * @property {boolean} canEdit - Whether the booking can be edited
627
+ * @property {ItineraryServiceLine[]} serviceLines - Booked service lines
628
+ */
629
+
630
+ /**
631
+ * @typedef {('yes-no'|'short'|'long'|'count'|'extended-option')} CustomFieldTypes
632
+ /**
633
+ * @typedef {Object} CreateItineraryCustomField
634
+ * @property {string} id - Custom Field identifier
635
+ * @property {string} label - Custom Field label
636
+ * @property {CustomFieldTypes} type - Custom Field type
637
+ * @property {boolean} [isPerService] - Whether the custom field is per service, or per entire booking/quote
638
+ * @property {Array<Object>} [options] - Options if the type is extended-option
639
+ * @property {Object} options.value - Option value
640
+ * @property {Object} options.label - Option label
641
+ */
642
+
643
+ /**
644
+ * Search for bookable products
645
+ * @async
646
+ * @param {Object} args
647
+ * @param {Object} args.token - Authentication token
648
+ * @param {object} args.payload - Search criteria
649
+ * @param {string} [args.payload.productId] - Optional specific product to search for
650
+ * @param {string} [args.payload.optionId] - Optional specific product option to search for
651
+ * @param {string} [args.payload.searchInput] - Optional text search across all fields
652
+ * @param {boolean} [args.payload.forceRefresh] - Optional flag to bypass cache
653
+ * @returns {object} retVal - the return object.
654
+ * @returns {Array<ItineraryProduct>} retVal.products - An array of products spec objects.
655
+ */
656
+ searchProductsForItinerary() {}
657
+
658
+ /**
659
+ * @typedef {Object} SearchAvailabilityForItineraryResponse
660
+ * @property {boolean} bookable - Whether the product is bookable
661
+ * @property {Array} [rates] - If there are multiple rates, they will be returned in this array
662
+ * @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.
663
+ * @property {string} [rates[].externalRateText] - Combined rate description
664
+ */
665
+ /**
666
+ * Search for product availability
667
+ * @async
668
+ * @param {Object} args
669
+ * @param {Object} args.token - Authentication token
670
+ * @param {object} args.payload - Search criteria
671
+ * @param {string} args.payload.optionId - ItineraryProduct option ID
672
+ * @param {string} args.payload.startDate - YYYY-MM-DD format
673
+ * @param {Array<PaxConfig>} args.payload.paxConfigs - Required for room-based products
674
+ * @param {number} [args.payload.chargeUnitQuantity=1] - Number of units to book
675
+ * @returns {SearchAvailabilityForItineraryResponse}
676
+ */
677
+ searchAvailabilityForItinerary() {}
678
+
679
+ /**
680
+ * @typedef {Object} AddServiceToItineraryResponse
681
+ * @property {string} message - Error message
682
+ * @property {object} booking - Booking object
683
+ * @property {string} booking.id - Booking identifier from your system
684
+ * @property {string} booking.reference - Reference number from your system
685
+ * @property {string} booking.linePrice - Price of the booking line
686
+ * @property {string} booking.lineId - Line identifier from your system
687
+ */
688
+ /**
689
+ * Create an itinerary
690
+ * @async
691
+ * @param {Object} args
692
+ * @param {Object} args.token - Authentication token
693
+ * @param {object} args.payload - Booking/Quote details
694
+ * @param {string} args.payload.QB - 'Q' for quote, 'B' for booking
695
+ * @param {string} args.payload.quoteName - Name of the booking/quote
696
+ * @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
697
+ * @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
698
+ * @param {string} [args.payload.rateId] - Rate identifier, we are sending 'Default' if no rates are provided by the check availability call
699
+ * @param {string} args.payload.optionId - Option identifier of the service line
700
+ * @param {string} args.payload.startDate - Start date always in (YYYY-MM-DD) format
701
+ * @param {string} [args.payload.reference] - Reference number from external system
702
+ * @param {Array<PaxConfig>} args.payload.paxConfigs - Passenger configurations
703
+ * @param {Array<Object>} [args.payload.extras] - Additional extras
704
+ * @param {Extra} args.payload.extras.selectedExtra - Selected extra
705
+ * @param {number} args.payload.extras.quantity - quantity
706
+ * @param {PUDOInfo} [args.payload.puInfo] - Pickup information
707
+ * @param {PUDOInfo} [args.payload.doInfo] - Dropoff information
708
+ * @param {string} [args.payload.notes] - Additional notes
709
+ * @param {Array<CustomFieldValue>} [args.payload.customFieldValues] - Custom field values
710
+ * @returns {AddServiceToItineraryResponse}
711
+ */
712
+ addServiceToItinerary() {}
713
+
714
+ /**
715
+ * Some additional fields available for the addServiceToItinerary call
716
+ * @async
717
+ * @param {Object} args
718
+ * @param {Object} args.token - Authentication token
719
+ * @returns {object} retVal - the return object.
720
+ * @returns {Array<CreateItineraryCustomField>} retVal.customFields - An array of custom fields spec objects.
721
+ */
722
+ getCreateItineraryFields() {}
723
+
482
724
  /**
483
725
  * Allotment Object
484
726
  /**
@@ -496,6 +738,23 @@ class Plugin {
496
738
  * @property {string[]} keyPaths - An array of key paths generated by combining supplier code and product codes.
497
739
  */
498
740
 
741
+ /**
742
+ * Search for itineraries
743
+ * @async
744
+ * @param {Object} args
745
+ * @param {Object} args.token - Authentication token
746
+ * @param {object} args.payload - Search criteria
747
+ * @param {string} args.payload.purchaseDateStart - Start date for purchase search (YYYY-MM-DD)
748
+ * @param {string} args.payload.purchaseDateEnd - End date for purchase search (YYYY-MM-DD)
749
+ * @param {string} args.payload.travelDateStart - Start date for travel search (YYYY-MM-DD)
750
+ * @param {string} args.payload.travelDateEnd - End date for travel search (YYYY-MM-DD)
751
+ * @param {string} args.payload.name - Search by customer name
752
+ * @param {string} args.payload.bookingId - Search by booking ID
753
+ * @returns {object} retVal - the return object.
754
+ * @returns {Array<ItineraryBooking>} retVal.bookings - An array of itinerary bookings matching search criteria.
755
+ */
756
+ searchItineraries() {}
757
+
499
758
  /**
500
759
  * Query Allotment
501
760
  * @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: