diff --git a/.tool-versions b/.tool-versions index 44d859d8d66..9b62fbf70cf 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1,2 @@ ruby 2.7.0 +nodejs 16.19.0 \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 9521cccd0e6..11016b6fa82 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -22,7 +22,7 @@ GEM execjs (2.7.0) fast_blank (1.0.0) fastimage (2.1.5) - ffi (1.11.1) + ffi (1.16.3) haml (5.1.1) temple (>= 0.8.0) tilt @@ -89,7 +89,6 @@ GEM parallel (1.17.0) public_suffix (3.1.0) rack (2.0.7) - rake (12.3.2) rb-fsevent (0.10.3) rb-inotify (0.10.0) ffi (~> 1.0) @@ -100,9 +99,8 @@ GEM sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) - sassc (2.0.1) + sassc (2.4.0) ffi (~> 1.9) - rake servolux (0.13.0) sprockets (3.7.2) concurrent-ruby (~> 1.0) @@ -133,4 +131,4 @@ RUBY VERSION ruby 2.3.3p222 BUNDLED WITH - 2.1.4 + 2.2.13 diff --git a/source/includes/_entities.md b/source/includes/_entities.md new file mode 100644 index 00000000000..51c0322145d --- /dev/null +++ b/source/includes/_entities.md @@ -0,0 +1,220 @@ +# Entities + + + +## Activity + +An **Activity** represents something people do to have fun. An **Activity** have a list of dates, the date when an instance of an **Activity** occurs. And, each date, have a list of times, the specific times for the given date when the activity starts and ends. + +**Attributes** + +| Attribute | Description | +|:-----------------------------|:-------------------------------------------------------------------------------------------------------------------| +| **`id`** | id
The unique identifier for the object. | +| **`name`** | string
The name of the activity. | +| **`dates`** | array<**[AvailabiltyDate](#availabilitydate)**>
List of dates when the activity occurs. | +| **`tickets`** | array<**[Ticket](#ticket)**>
List of the supported tickets types for the activity. | +| **`requiresAdult`** | boolean
Indicates if the quote must have 1 Adult or 1 Senior or 1 Traveler ticket types along the selected tickets sent. | +| **`minTravelersPerBooking`** | integer
Regarding all the ticket types and quantities selected, you must, at least, have this number of travelers selected for the booking. | +| **`maxTravelersPerBooking`** | integer
Regarding all the ticket types and quantities selected, you must, at most, have this number of travelers selected for the booking. | + +## ActivityVariant + +An **ActivityVariant** is a variation for the **Activity** that represents little differences between the _original_ activity. + +Any particular activity may consist of a number of variants, each of which might represent different departure times, tour routes, or packaged extras like additional meals, transport and so forth. + +**Attributes** + +| Attribute | Description | +|:------------------|:------------------------------------------------------------------------------------------------------------------| +| **`id`** | id
The unique identifier for the object. | +| **`title`** | string
Human readable text with the short name of the variant. | +| **`description`** | string
Human readable text that contains specific information and details about the variant. | + +## ActivityVariantQuestion + +An **ActivityVariantQuestion** is a booking question that may be required to answer abd represents vital information specified by the supplier about the individual travelers or the group as a whole to be sent to the supplier as part of the request in order to complete a booking. + +**Implementations (or Types)** + +| Type name | +|:----------------------------| +| **`StringQuestion`** | +| **`DateQuestion`** | +| **`TimeQuestion`** | +| **`SingleChoiceQuestion`** | +| **`LocationQuestion`** | +| **`NumberAndUnitQuestion`** | + +**Attributes** + +| Attribute | Description | +|:----------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **`id`** | id
The unique identifier for the question. This must be sent with the answers when booking an activity. | +| **`required`** | boolean
Indicates if answering the question is required (true) or not (false). | +| **`group`** | array<[ActivityVariantQuestionGroup](#activityvariantquestiongroup)>
Indicates how the question should be answered: once PER_TRAVELER or once PER_BOOKING. | +| **`hint`** | string
A string that contains information on how the field is expected to be answered. It may be not present. | +| **`label`** | non-null string
A string that contains the human readable name of the question being asked. Examples: First Name, Last Name, Pickup Location, etc. | + +Other fields may be present depending on the question type. These are the specific fields for each question type: + +### StringQuestion + +**Attributes** + +| Attribute | Description | +|:-----------------|:-----------------------------------------------------------------------------------------| +| **`max_length`** | integer
The maximum allowed length for the answer to this question. | + +**Validation** + +The answer to this question must be a string that have **at most** `max_length` in size. + +### DateQuestion + +**Attributes** + +_no other attributes present_ + +**Validation** + +The answer to this question must be a valid string representing a "Date" in the format described by [ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601). + +### TimeQuestion + +_no other attributes present_ + +**Validation** + +The answer to this question must be a valid string representing a "Local time" in the format described by [ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601). + +### SingleChoiceQuestion + +**Attributes** + +| Attribute | Description | +|:----------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **`options`** | array<[ActivityVariantQuestionOption](#activityvariantquestionoption)>
The unique identifier for the question. This must be sent with the answers when booking an activity. | + +**Validation** + +The answer to this question must be a string that is contained within the provided options `value` attribute. + +### LocationQuestion + +**Attributes** + +| Attribute | Description | +|:--------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **`max_length`** | integer
The maximum allowed length for the answer to this question. | +| **`options`** | array<[ActivityVariantQuestionOption](#activityvariantquestionoption)>
The unique identifier for the question. This must be sent with the answers when booking an activity. | + +**Validation** + +The answer to this question must be a string that is contained within the provided options `value` attribute. + +### NumberAndUnitQuestion + +**Attributes** + +| Attribute | Description | +|:----------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **`units`** | array<string>
List of units for the question. The selected unit must be sent together with the answer for this type of question. | + +**Validation** + +The answer to this question must be a string that correctly represents a number (`1000`, `105.56`, `0.1234` are valid numbers) and the unit must be one of the values contained withing the `units` array. + +## ActivityVariantQuestionGroup + +Indicates how the question should be answered: once **PER_TRAVELER** or once **PER_BOOKING**. + +**Values** + +| Value | Description | +|:-------------------|:----------------------------------------------------------------------------------------------------------------------------------| +| **`PER_BOOKING`** | Indicates that the answer to the question must be answered for each traveler, i.e., answered once per ticket added to the quote . | +| **`PER_TRAVELER`** | Indicates that the answer to the question must be answered for the booked group as a whole, i.e., answered once per booking. | + +## ActivityVariantQuestionOption + +An **ActivityVariantQuestionOption** represents a possible answer for a given question inside a list of possible answers (or options). Each option may have a list of **followup questions**, i.e. questions that must be answered for a given answer to the parent question of this option. + +**Attributes** + +| Attribute | Description | +|:----------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **`name`** | string
Human readable text that indicates the value of this option. | +| **`value`** | string
The actual value for the option that must be sent as an answer for the parent question. | +| **`questions`** | array<[ActivityVariantQuestion](#activityvariantquestion)>
A list of follow up questions for the option, the may or may not be required to answer (based on each follow up question `required` attribute) | + +## AvailabilityDate + +An **AvailabilityDate** is the representation of a date when an **Activity** occurs. For a given date, multiple **AvailabilityTime** can be related to the same date, indicating possible start and end times for the activity on the given date. + +**Attributes** + +| Attribute | Description | +|:------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **`date`** | string
Human readable text that indicates the value of this option. | +| **`isAvailable`** | string
The actual value for the option that must be sent as an answer for the parent question. | +| **`times`** | array<[AvailabilityTime](#availabilitytime)>
A list of follow up questions for the option, the may or may not be required to answer (based on each follow up question `required` attribute) | + +## AvailabilityTime + +An **AvailabilityTime** represents a start and an end time for a given date when the parent **Activity** happens. + +**Attributes** + +| Attribute | Description | +|:-------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------| +| **`id`** | string
The unique identifier for this object. | +| **`localDateTimeStart`** | string
NaiveDateTime ISO8601 formatted string that indicates which date and time the activity starts, without timezone. | +| **`localDateTimeEnd`** | string
NaiveDateTime ISO8601 formatted string that indicates which date and time the activity ends, without timezone. | +| **`status`** | [AvailabilityTimeStatus](#availabilitytimestatus)
If the given time is available and if not, why. | +| **`variant`** | [ActivityVariant](#activityvariant)
The activity variant that have the availability for this specific time. | + +## AvailabilityTimeStatus + +The status of a given **AvailabilityTime**. + +**Values** + +| Value | Description | +|:----------------|:-----------------------------------------------------------------------------------------------------------------------| +| **`AVAILABLE`** | Currently available for sale, but has a fixed capacity. | +| **`CLOSED`** | Currently not available for sale, but not sold out (e.g. temporarily on stop-sell) and may be available for sale soon. | +| **`FREESALE`** | Always available. | +| **`LIMITED`** | Currently available for sale, but has a fixed capacity and may be sold out soon. | +| **`SOLD_OUT`** | Currently sold out, but additional availability may free up. | + +## CancellationReason + +The reason for the cancellation. + +**Values** + +| Value | Description | +|:---------------------------------------|:------------------------------------| +| **`BOOKED_WRONG_TOUR_DATE`** | Booked the wrong tour date. | +| **`CANCELED_ENTIRE_TRIP`** | Cancelled the entire trip. | +| **`CHOSE_A_DIFFERENT_CHEAPER_TOUR`** | Chose a different but cheaper tour. | +| **`DUPLICATE_BOOKING`** | Duplicate booking. | +| **`SIGNIFICANT_GLOBAL_EVENT`** | Significant global event. | +| **`SUPPLIER_NO_SHOW`** | Supplier no show. | +| **`UNEXPECTED_MEDICAL_CIRCUMSTANCES`** | Unexpected medical circumstances. | +| **`WEATHER`** | Weather. | + +## Ticket + +A **Ticket** represents the supported ticket type for a given **Activity**, which may have different configurations and prices supported by the activity provider. They can be **Adult**, **Child**, **Traveler**, etc + +**Attributes** + +| Attribute | Description | +|:------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------| +| **`id`** | string
The unique identifier for this object. | +| **`name`** | string
The name that represents the ticket, like Adult, Child, Traveler, etc. | +| **`minQuantity`** | integer
The minimum number of this ticket that can be booked at once. | +| **`maxQuantity`** | integer
The maximum number of this ticket that can be booked at once. | \ No newline at end of file diff --git a/source/includes/_mutations.md b/source/includes/_mutations.md index d76ad21dd82..83a325680b6 100644 --- a/source/includes/_mutations.md +++ b/source/includes/_mutations.md @@ -1,6 +1,4 @@ -# Mutations - -## CreateBookingQuote +# CreateBookingQuote A quote holds a set of inventory while locking in a price. You can think of a quote as a draft booking or a reservation. @@ -12,6 +10,23 @@ This can happen for a few reasons: 2) Certain suppliers have very complex rules on how they accept bookings. It might be that your specific tickets aren't available, or there are complex rules around time-cutoffs. +3) There are ticket rules to respect when asking for a quote. The specific activity and ticket fields are described below and how they affect the mutation result: + +### Activity + +| Attribute | Description | +|:-----------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **`requiresAdult`** | boolean
Indicates if the quote must have 1 Adult or 1 Senior or 1 Traveler ticket types along the selected tickets sent. | +| **`maxTravelersPerBooking`** | integer
Regarding all the ticket types and quantities selected, you must, at least, have this number of travelers selected for the booking. | +| **`minTravelersPerBooking`** | integer
Regarding all the ticket types and quantities selected, you must, at most, have this number of travelers selected for the booking. | + +### Ticket + +| Attribute | Description | +|:------------------|:--------------------------------------------------------------| +| **`minQuantity`** | integer
The minimum number of this ticket that can be booked at once. | +| **`maxQuantity`** | integer
The maximum number of this ticket that can be booked at once. | + With those caveats aside, making these requests might look like this: ```graphql @@ -21,9 +36,23 @@ mutation CreateQuote($request: BookingQuoteInput!) { ... on BookingQuote { id priceBreakdown { + price { + formatted + } total { formatted } + taxes { + formatted + } + } + snapshot { + activity { + variant { + id + title + } + } } } ... on Error { @@ -33,25 +62,432 @@ mutation CreateQuote($request: BookingQuoteInput!) { } ``` -> With the variables formed something like this: +> Request variables + +```json +{ + "request": { + "availabilityTimeId": "", + "tickets": [ + { + "quantity": 4, + "ticketId": "" + } + ] + } +} +``` + +> And a Response like this + +```json +{ + "data": { + "createBookingQuote": { + "__typename": "BookingQuote", + "id": "", + "priceBreakdown": { + "price": { + "formatted": "$620.00" + }, + "taxes": { + "formatted": "$0.00" + }, + "total": { + "formatted": "$620.00" + } + }, + "snapshot": { + "activity": { + "variant": { + "id": "", + "title": "Solvang Food and Photo Tour" + } + } + } + } + } +} +``` + +## CreateBookingQuote Errors + +## BadRequestError + +Given the provided activity properties on the side and summarized below, we will show different inputs with values that do not match the activity and/or tickets requirements, with the according error messages for each one: + +> Activity Response + +```json +{ + "data": { + "activities": { + "entries": [ + { + "id": "", + "maxTravelersPerBooking": 10, + "minTravelersPerBooking": 2, + "name": "Solvang Food & Photo Tour: Explore Danish Cuisine & Photography in Santa Ynez Valley", + "requiresAdult": true, + "tickets": [ + { + "id": "", + "maxQuantity": 10, + "minQuantity": 1, + "name": "Adult" + }, + { + "id": "", + "maxQuantity": 4, + "minQuantity": 0, + "name": "Youth" + } + ] + } + ], + "pageNumber": 1, + "pageSize": 200, + "totalEntries": 1, + "totalPages": 1 + } + } +} +``` + +### Activity + +| Attribute | Value | +|:-----------------------------|:------| +| **`requiresAdult`** | true | +| **`minTravelersPerBooking`** | 2 | +| **`maxTravelersPerBooking`** | 10 | + +### Adult Ticket + +| Attribute | Value | +|:------------------|:------| +| **`minQuantity`** | 1 | +| **`maxQuantity`** | 10 | + +### Youth Ticket + +| Attribute | Value | +|:------------------|:------| +| **`minQuantity`** | 0 | +| **`maxQuantity`** | 4 | + +## "Requires at least one adult or senior" + +**When it happens?** + +Not providing the **ADULT** ticket, only the **YOUTH** ticket. + +||| +|:--------------------------------------------|:---------------------------------------| +| **`requiresAdult`** | true | +| **Sending ADULT ticket in request** | false | + +||| +|:------------------------------------------------------------------|:-------------------------------------| +| **Range `minTravelersPerBooking`** - **`maxTravelersPerBooking`** | 2 - 10 | +| **Quantity sent on Request** | 2 | + +### Adult Ticket + +||| +|:--------------------------------------------|:-------------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 1 - 10 | +| **Quantity sent on Request** | 0 | + +### Youth Ticket + +||| +|:--------------------------------------------|:-------------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 0 - 4 | +| **Quantity sent on Request** | 2 | + +> Request input + +```json +{ + "request": { + "availabilityTimeId": "", + "tickets": [ + { + "quantity": 2, + "ticketId": "" + } + ] + } +} +``` + +> Response + +```json +{ + "data": { + "createBookingQuote": { + "__typename": "BadRequestError", + "message": "Requires at least one adult or senior" + } + } +} +``` + +## "Minimum # of guests is <num> and requires at least one adult or senior" + +**When it happens?** + +Not providing the **ADULT** ticket and not meeting the `minTravelersPerBooking` for the activity. + +||| +|:--------------------------------------------|:---------------------------------------| +| **`requiresAdult`** | true | +| **Sending ADULT ticket in request** | false | + +||| +|:------------------------------------------------------------------|:-------------------------------------| +| **Range `minTravelersPerBooking`** - **`maxTravelersPerBooking`** | 2 - 10 | +| **Quantity sent on Request** | 1 | + +### Adult Ticket + +||| +|:--------------------------------------------|:-------------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 1 - 10 | +| **Quantity sent on Request** | 0 | + +### Youth Ticket + +||| +|:--------------------------------------------|:-------------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 0 - 4 | +| **Quantity sent on Request** | 1 | + +> Request input + +```json +{ + "request": { + "availabilityTimeId": "", + "tickets": [ + { + "quantity": 1, + "ticketId": "" + } + ] + } +} +``` + +> Response + +```json +{ + "data": { + "createBookingQuote": { + "__typename": "BadRequestError", + "message": "Minimum # of guests is 2 and requires at least one adult or senior" + } + } +} +``` + +## "Minimum # of guests for ticket '<ticket-id>' is <num>" + +**When it happens?** + +Providing the **ADULT** ticket, the **YOUTH** ticket and not meeting the `minQuantity` for the **YOUTH** ticket. + +||| +|:--------------------------------------------|:---------------------------------------| +| **`requiresAdult`** | true | +| **Sending ADULT ticket in request** | true | + +||| +|:------------------------------------------------------------------|:-------------------------------------| +| **Range `minTravelersPerBooking`** - **`maxTravelersPerBooking`** | 3 - 10 | +| **Quantity sent on Request** | 2 | + +### Adult Ticket + +||| +|:--------------------------------------------|:-------------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 1 - 10 | +| **Quantity sent on Request** | 1 | + +### Youth Ticket + +||| +|:--------------------------------------------|:-------------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 2 - 4 | +| **Quantity sent on Request** | 1 | + +> Request input + +```json +{ + "request": { + "availabilityTimeId": "", + "tickets": [ + { + "quantity": 1, + "ticketId": "" + }, + { + "quantity": 1, + "ticketId": "" + } + ] + } +} +``` + +> Response -```javascript +```json { - "request": { - "availabilityTimeId": "at0apapwe6", - "tickets": [ - { - "quantity": 4, - "ticketId": "t06rx6q" - } - ] - } + "data": { + "createBookingQuote": { + "__typename": "BadRequestError", + "message": "Minimum # of guests for ticket '' is 2" + } + } } ``` -## UpdateBookingQuote +## "Maximum # of guests for ticket '<ticket-id>' is <num>" + +**When it happens?** + +Providing the **ADULT** ticket, the **YOUTH** ticket and not meeting the `maxQuantity` for the **YOUTH** ticket. + +||| +|:--------------------------------------------|:---------------------------------------| +| **`requiresAdult`** | true | +| **Sending ADULT ticket in request** | true | + +||| +|:------------------------------------------------------------------|:-------------------------------------| +| **Range `minTravelersPerBooking`** - **`maxTravelersPerBooking`** | 2 - 10 | +| **Quantity sent on Request** | 10 | + +### Adult Ticket + +||| +|:--------------------------------------------|:-------------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 1 - 10 | +| **Quantity sent on Request** | 5 | + +### Youth Ticket + +||| +|:--------------------------------------------|:-------------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 0 - 4 | +| **Quantity sent on Request** | 5 | -It is useful to update a quote as a customer changes the tickets, switches their desired time, etc. +> Request input + +```json +{ + "request": { + "availabilityTimeId": "", + "tickets": [ + { + "quantity": 5, + "ticketId": "" + }, + { + "quantity": 5, + "ticketId": "" + } + ] + } +} +``` + +> Response + +```json +{ + "data": { + "createBookingQuote": { + "__typename": "BadRequestError", + "message": "Maximum # of guests for ticket '' is 4" + } + } +} +``` + +## "Maximum # of guests is <num>" + +**When it happens?** + +Providing the **ADULT** ticket, the **YOUTH** ticket and not meeting the `maxTravelersPerBooking` for the activity. + +||| +|:--------------------------------------------|:---------------------------------------| +| **`requiresAdult`** | true | +| **Sending ADULT ticket in request** | true | + +||| +|:------------------------------------------------------------------|:-------------------------------------| +| **Range `minTravelersPerBooking`** - **`maxTravelersPerBooking`** | 2 - 10 | +| **Quantity sent on Request** | 11 | + +### Adult Ticket + +||| +|:--------------------------------------------|:-------------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 1 - 10 | +| **Quantity sent on Request** | 7 | + +### Youth Ticket + +||| +|:--------------------------------------------|:-------------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 0 - 4 | +| **Quantity sent on Request** | 4 | + +> Request input + +```json +{ + "request": { + "availabilityTimeId": "", + "tickets": [ + { + "quantity": 7, + "ticketId": "" + }, + { + "quantity": 4, + "ticketId": "" + } + ] + } +} +``` + +> Response + +```json +{ + "data": { + "createBookingQuote": { + "__typename": "BadRequestError", + "message": "Maximum # of guests is 10" + } + } +} +``` + +# UpdateBookingQuote + +It is useful to update a quote as a customer changes the tickets, switches their desired time, add **required booking question answers**, etc. + + This call is nearly identical to the CreateBookingQuote: @@ -83,26 +519,769 @@ mutation EditBookingQuote($request: EditBookingQuoteInput!) { } ``` -> With the variables formed something like this: +> Request variables + +```json +{ + "request": { + "bookingQuoteId": "", + "quoteInput": { + "availabilityTimeId": "", + "tickets": [ + { + "quantity": 5, + "ticketId": "" + } + ] + } + } +} +``` + +Booking Questions +----- + +Some activities may require questions answers in order to be possible to book. + +The questions may be + +- **PER_BOOKING**, meaning you must answer it once; +- **PER_TRAVELER**, meaning you must answer them for each traveler. -```javascript +Let's consider the example on the side. You have the **<available-time-id>** with the **<activity-variant-id>** for which it belongs to. So you can query that specific **ActivityVariant** and see if there are questions that must be answered. See [ActivityVariants](#activityvariants) do recall the query that produces the json output replicated on the side. + +> Request variables + +```json { - "request": { - "bookingQuoteId": "qb0nm9r", - "quoteInput": { - "availabilityTimeId": "at0apapwe6", - "tickets": [ - { - "quantity": 5, - "ticketId": "t06rx6q" - } - ] - } - } + "id": "" } ``` -## CreateBooking +> Response + +```json +{ + "data": { + "activityVariant": { + "description": "Skip the tourist traps and head out with a local foodie for a fun-filled, behind-the-scenes cultural food tour in the unique Danish village nestled in the heart of the Santa Ynez Valley (30 miles north of downtown Santa Barbara). \n\nOur Solvang food tour starts along Mission Drive and will walk you through the the downtown neighborhood streets where you will taste traditional Danish and local artisan food and drink unique to Solvang and the Central Coast. Indulge in bites of traditional Danish comfort fare and freshly baked goods as well as craft beer, local wine, & artisan eats. Along the way, you’ll get immersed in the local culture and hear interesting stories about each place you’ll visit.\n\nYou’ll also learn easy-to-shoot food photo tips from your pro tour guide on this foodie photography tour and take your photo skills to the next level using your smartphone’s camera app. By the end of your 3-4 hour tour, you’ll be eating like a local and shooting like a pro!", + "questions": [ + { + "__typename": "DateQuestion", + "group": "PER_TRAVELER", + "hint": null, + "id": "", + "label": "Date of birth", + "required": true + }, + { + "__typename": "StringQuestion", + "group": "PER_TRAVELER", + "hint": null, + "id": "", + "label": "First name", + "maxLength": 50, + "required": true + }, + { + "__typename": "StringQuestion", + "group": "PER_TRAVELER", + "hint": null, + "id": "", + "label": "Last name", + "maxLength": 50, + "required": true + }, + { + "__typename": "StringQuestion", + "group": "PER_BOOKING", + "hint": null, + "id": "", + "label": "Special requirements", + "maxLength": 1000, + "required": false + }, + { + "__typename": "SingleChoiceQuestion", + "group": "PER_BOOKING", + "hint": "Select the language you want for this tour", + "id": "", + "label": "Tour language", + "options": [ + { + "followupQuestions": [], + "name": "English (Guide)", + "value": "GUIDE|en|en/SERVICE_GUIDE" + } + ], + "required": true + } + ] + } + } +} +``` + +In order to be able to book this activity, i.e., send the quote id to the **createBooking** mutation and it succeeds, we will need to update the quote with the answers for each type of question. + +As we can see: + +- The **"Tour language"** question **IS REQUIRED** to answer, only once (**PER_BOOKING**) +- The **"Special requirements"** question **IS NOT REQUIRED** to answer, only once (**PER_BOOKING**) +- The **"Date of birth", "First Name" and "Last Name"** questions **ARE REQUIRED** to answer, for each traveler (**PER_TRAVELER**) + + + +> For the updated graphql query below (with the answers fields added for the purpose of this documentation) + +```graphql +mutation DocExampleEditBookingQuote($request: EditBookingQuoteInput!) { + editBookingQuote(input: $request) { + __typename + ... on BookingQuote { + id + priceBreakdown { + price { + formatted + } + taxes { + formatted + } + fees { + formatted + } + total { + formatted + } + } + snapshot { + activity { + variant { + id + title + } + bookingAnswers { + question + answer + } + } + tickets { + id + quantity + guestsAnswers { + guestAnswers { + question + answer + } + } + } + } + } + ... on Error { + message + } + } +} + +``` + +> The `editBooking` request variables would be like this: + +```json +{ + "request": { + "bookingQuoteId": "", + "quoteInput": { + "availabilityTimeId": "", + "tickets": [ + { + "quantity": 2, + "ticketId": "", + "guestsAnswers": [ + { + "guestAnswers": [ + { + "question": "", + "answer": "Adult1 Date of Birth" + }, + { + "question": "", + "answer": "Adult1 First Name" + }, + { + "question": "", + "answer": "Adult1 Last Name" + } + ] + }, + { + "guestAnswers": [ + { + "question": "", + "answer": "Adult2 Date of Birth" + }, + { + "question": "", + "answer": "Adult2 First Name" + }, + { + "question": "", + "answer": "Adult2 Last Name" + } + ] + } + ] + } + ], + "bookingAnswers": [ + { + "question": "", + "answer": "No special requirements" + }, + { + "question": "", + "answer": "GUIDE|en|en/SERVICE_GUIDE" + } + ] + } + } +} +``` + +> And the Response would be like + +```json +{ + "data": { + "editBookingQuote": { + "__typename": "BookingQuote", + "id": "qb098ej", + "priceBreakdown": { + "fees": { + "formatted": "$0.00" + }, + "price": { + "formatted": "$310.00" + }, + "taxes": { + "formatted": "$0.00" + }, + "total": { + "formatted": "$310.00" + } + }, + "snapshot": { + "activity": { + "bookingAnswers": [ + { + "answer": "No special requirements", + "question": "" + }, + { + "answer": "GUIDE|en|en/SERVICE_GUIDE", + "question": "" + } + ], + "variant": { + "id": "", + "title": "Solvang Food and Photo Tour" + } + }, + "tickets": [ + { + "guestsAnswers": [ + [ + { + "guestAnswers": [ + { + "answer": "Adult1 Date of Birth", + "question": "" + }, + { + "answer": "Adult1 First Name", + "question": "" + }, + { + "answer": "Adult1 Last Name", + "question": "" + } + ] + } + ], + [ + { + "guestAnswers": [ + { + "answer": "Adult2 Date of Birth", + "question": "" + }, + { + "answer": "Adult2 First Name", + "question": "" + }, + { + "answer": "Adult2 Last Name", + "question": "" + } + ] + } + ] + ], + "id": "", + "quantity": 2 + } + ] + } + } + } +} +``` + +## UpdateBookingQuote Errors + +## BadRequestError + +Given the provided activity properties on the side and summarized below, we will show different inputs with values that do not match the activity and/or tickets requirements, with the according error messages for each one: + +> Activity Response + +```json +{ + "data": { + "activities": { + "entries": [ + { + "id": "", + "maxTravelersPerBooking": 10, + "minTravelersPerBooking": 2, + "name": "Solvang Food & Photo Tour: Explore Danish Cuisine & Photography in Santa Ynez Valley", + "requiresAdult": true, + "tickets": [ + { + "id": "", + "maxQuantity": 10, + "minQuantity": 1, + "name": "Adult" + }, + { + "id": "", + "maxQuantity": 4, + "minQuantity": 0, + "name": "Youth" + } + ] + } + ], + "pageNumber": 1, + "pageSize": 200, + "totalEntries": 1, + "totalPages": 1 + } + } +} +``` + +### Activity + +| Attribute | Value | +|:-----------------------------|:------| +| **`requiresAdult`** | true | +| **`minTravelersPerBooking`** | 2 | +| **`maxTravelersPerBooking`** | 10 | + +### Adult Ticket + +| Attribute | Value | +|:------------------|:------| +| **`minQuantity`** | 1 | +| **`maxQuantity`** | 10 | + +### Youth Ticket + +| Attribute | Value | +|:------------------|:------| +| **`minQuantity`** | 0 | +| **`maxQuantity`** | 4 | + +## "Requires at least one adult or senior" + +**When it happens?** + +Not providing the **ADULT** ticket, only the **YOUTH** ticket. + +### Activity + +||| +|:--------------------------------------------|:--------------------------------------| +| **`requiresAdult`** | true | +| **Sending ADULT ticket in request** | false | + +||| +|:------------------------------------------------------------------|:------------------------------------| +| **Range `minTravelersPerBooking`** - **`maxTravelersPerBooking`** | 2 - 10 | +| **Quantity sent on Request** | 2 | + +### Adult Ticket + +||| +|:--------------------------------------------|:----------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 1 - 10 | +| **Quantity sent on Request** | 0 | + +### Youth Ticket + +||| +|:--------------------------------------------|:------------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 0 - 4 | +| **Quantity sent on Request** | 2 | + +> Request input + +```json +{ + "request": { + "bookingQuoteId": "", + "quoteInput": { + "availabilityTimeId": "", + "tickets": [ + { + "quantity": 2, + "ticketId": "" + } + ] + } + } +} +``` + +> Response + +```json +{ + "data": { + "editBookingQuote": { + "__typename": "BadRequestError", + "message": "Requires at least one adult or senior" + } + } +} +``` + +## "Minimum # of guests is <num> and requires at least one adult or senior" + +**When it happens?** + +Not providing the **ADULT** ticket and not meeting the `minTravelersPerBooking` for the activity. + +### Activity + +||| +|:--------------------------------------------|:--------------------------------------| +| **`requiresAdult`** | true | +| **Sending ADULT ticket in request** | false | + +||| +|:------------------------------------------------------------------|:------------------------------------| +| **Range `minTravelersPerBooking`** - **`maxTravelersPerBooking`** | 2 - 10 | +| **Quantity sent on Request** | 1 | + +### Adult Ticket + +||| +|:--------------------------------------------|:----------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 1 - 10 | +| **Quantity sent on Request** | 0 | + +### Youth Ticket + +||| +|:--------------------------------------------|:------------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 0 - 4 | +| **Quantity sent on Request** | 1 | + +> Request input + +```json +{ + "request": { + "bookingQuoteId": "", + "quoteInput": { + "availabilityTimeId": "", + "tickets": [ + { + "quantity": 1, + "ticketId": "" + } + ] + } + } +} +``` + +> Response + +```json +{ + "data": { + "editBookingQuote": { + "__typename": "BadRequestError", + "message": "Minimum # of guests is 2 and requires at least one adult or senior" + } + } +} +``` + +## "Minimum # of guests for ticket '<ticket-id>' is <num>" + +**When it happens?** + +Providing the **ADULT** ticket, the **YOUTH** ticket and not meeting the `minQuantity` for the **YOUTH** ticket. + +### Activity + +||| +|:--------------------------------------------|:--------------------------------------| +| **`requiresAdult`** | true | +| **Sending ADULT ticket in request** | true | + +||| +|:------------------------------------------------------------------|:------------------------------------| +| **Range `minTravelersPerBooking`** - **`maxTravelersPerBooking`** | 3 - 10 | +| **Quantity sent on Request** | 2 | + +### Adult Ticket + +||| +|:--------------------------------------------|:----------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 1 - 10 | +| **Quantity sent on Request** | 1 | + +### Youth Ticket + +||| +|:--------------------------------------------|:------------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 2 - 4 | +| **Quantity sent on Request** | 1 | + +> Request input + +```json +{ + "request": { + "bookingQuoteId": "", + "quoteInput": { + "availabilityTimeId": "", + "tickets": [ + { + "quantity": 1, + "ticketId": "" + }, + { + "quantity": 1, + "ticketId": "" + } + ] + } + } +} +``` + +> Response + +```json +{ + "data": { + "createBookingQuote": { + "__typename": "BadRequestError", + "message": "Minimum # of guests for ticket '' is 2" + } + } +} +``` + +## "Maximum # of guests for ticket '<ticket-id>' is <num>" + +**When it happens?** + +Providing the **ADULT** ticket, the **YOUTH** ticket and not meeting the `maxQuantity` for the **YOUTH** ticket + +### Activity + +||| +|:--------------------------------------------|:---------------------------------------| +| **`requiresAdult`** | true | +| **Sending ADULT ticket in request** | true | + +||| +|:------------------------------------------------------------------|:-------------------------------------| +| **Range `minTravelersPerBooking`** - **`maxTravelersPerBooking`** | 2 - 10 | +| **Quantity sent on Request** | 10 | + +### Adult Ticket + +||| +|:--------------------------------------------|:-------------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 1 - 10 | +| **Quantity sent on Request** | 5 | + +### Youth Ticket + +||| +|:--------------------------------------------|:-------------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 0 - 4 | +| **Quantity sent on Request** | 5 | + +> Request input + +```json +{ + "request": { + "bookingQuoteId": "", + "quoteInput": { + "availabilityTimeId": "", + "tickets": [ + { + "quantity": 5, + "ticketId": "" + }, + { + "quantity": 5, + "ticketId": "" + } + ] + } + } +} +``` + +> Response + +```json +{ + "data": { + "editBookingQuote": { + "__typename": "BadRequestError", + "message": "Maximum # of guests for ticket '' is 4" + } + } +} +``` + +## "Maximum # of guests is <num>" + +**When it happens?** + +Providing the **ADULT** ticket, the **YOUTH** ticket and not meeting the `maxTravelersPerBooking` for the activity + +### Activity + +||| +|:--------------------------------------------|:---------------------------------------| +| **`requiresAdult`** | true | +| **Sending ADULT ticket in request** | true | + +||| +|:------------------------------------------------------------------|:-------------------------------------| +| **Range `minTravelersPerBooking`** - **`maxTravelersPerBooking`** | 2 - 10 | +| **Quantity sent on Request** | 11 | + +### Adult Ticket + +||| +|:--------------------------------------------|:-------------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 1 - 10 | +| **Quantity sent on Request** | 7 | + +### Youth Ticket + +||| +|:--------------------------------------------|:-------------------------------------| +| **Range `minQuantity`** - **`maxQuantity`** | 0 - 4 | +| **Quantity sent on Request** | 4 | + +> Request input + +```json +{ + "request": { + "bookingQuoteId": "", + "quoteInput": { + "availabilityTimeId": "", + "tickets": [ + { + "quantity": 7, + "ticketId": "" + }, + { + "quantity": 4, + "ticketId": "" + } + ] + } + } +} +``` + +> Response + +```json +{ + "data": { + "editBookingQuote": { + "__typename": "BadRequestError", + "message": "Maximum # of guests is 10" + } + } +} +``` + + +Invalid Questions Error +----- + +If you forget to provide required questions answers or provide invalid questions answers, you will receive the InvalidQuestionsError with details about the invalid questions detected. + + + +> Request (no required questions answers provided) + +```json +{ + "request": { + "bookingQuoteId": "", + "quoteInput": { + "availabilityTimeId": "", + "tickets": [ + { + "quantity": 2, + "ticketId": "" + } + ], + "bookingAnswers": [ + { + "question": "", + "answer": "No special requirements" + } + ] + } + } +} +``` + +> Response + +```json +{ + "data": { + "createBooking": { + "__typename": "InvalidQuestionsError", + "message": "Missing guest answers for tickets: . Missing required questions: , , ..., " + } + } +} +``` + +# CreateBooking This will create a booking out of a provided quote. At this point, your booking is submitted and there's nothing left to do on your end. @@ -124,24 +1303,145 @@ mutation CreateBooking($request: CreateBookingInput!) { } ``` -> With the variables formed something like this: +> Request variables + +```json +{ + "request": { + "bookingQuoteId": "", + "customer": { + "name": "Product Demo", + "email": "valid.email.address@peek.com", + "phone": "0196488932", + "country": "US", + "postalCode": "94501" + } + } +} +``` + +> Response + +```json +{ + "data": { + "createBooking": { + "__typename": "Booking", + "id": "" + } + } +} +``` + +## CreateBooking Errors + +Invalid Customer +----- + +If you provide invalid customer details, you will receive the InvalidCustomerError with details about the invalid fields detected. -```javascript +> Request + +```json { - "request": { - "bookingQuoteId": "qb0nm9r", - "customer": { - "name": "Peek Plus Demo", - "email": "name@example.com", - "phone": "0196488932", - "country": "US", - "postalCode": "12345" - } - } + "request": { + "bookingQuoteId": "", + "customer": { + "name": "Product Demo", + "email": "invalid_email_address", + "phone": "0196488932", + "country": "US", + "postalCode": "94501" + } + } } ``` -## CancelBooking +> Response + +```json +{ + "data": { + "createBooking": { + "__typename": "InvalidCustomerError", + "invalidFields": [ + "email" + ], + "message": "Please provide valid customer details. Invalid: email." + } + } +} +``` + +Invalid Questions Error +----- + +If you forget to provide required questions answers to the quote you want to proceed and make a booking for, you will receive the InvalidQuestionsError with details about the invalid questions detected. + +> Request + +```json +{ + "request": { + "bookingQuoteId": "", + "customer": { + "name": "Product Demo", + "email": "invalid_email_address", + "phone": "0196488932", + "country": "US", + "postalCode": "94501" + } + } +} +``` + +> Response + +```json +{ + "data": { + "createBooking": { + "__typename": "InvalidQuestionsError", + "message": "Missing guest answers for tickets: . Missing required questions: , , ..., " + } + } +} +``` + +Source Error +----- + +Sometimes, when creating a booking, the activity provider may have any kind of error while processing the booking request. In this case you will receive an error like this one. + +```json +{ + "data": { + "createBooking": { + "__typename": "SourceError", + "message": "Something unexpected happened, please retry your request. Engineers are notified and will report back." + } + } +} +``` + +> Request variables + +```json +{ + "request": { + "bookingQuoteId": "qb0nm9r", + "customer": { + "name": "Peek Plus Demo", + "email": "name@example.com", + "phone": "0196488932", + "country": "US", + "postalCode": "12345" + } + } +} +``` + +# CancelBooking This will cancel the booking in our system and notify the provider that the customer will no longer be coming. We are assuming a refund will be issued on your end and update the customer in our system to indicate a refund has been granted. @@ -160,12 +1460,60 @@ mutation CancelBooking($input: CancelBookingInput!) { } ``` -> With the variables formed something like this: +> Request variables -```javascript +```json { - "input": { - "bookingId": "b0eb5p" - } + "input": { + "bookingId": "", + "cancellationReason": "WEATHER" + } } ``` + +> Response + +```json +{ + "data": { + "cancelBooking": { + "__typename": "Booking", + "status": "CANCELED" + } + } +} +``` + + + +See [CancellationReason](#cancellationreason) + +## CancelBooking Errors + +CancellationRejectionError +----- + +If you do not provide a `cancellationReason` you will get this error. + +> Request variables + +```json +{ + "input": { + "bookingId": "" + } +} +``` + +> Response + +```json +{ + "data": { + "cancelBooking": { + "__typename": "CancellationRejectionError", + "message": "You must provide a valid cancellation reason when cancelling a booking." + } + } +} +``` \ No newline at end of file diff --git a/source/includes/_queries.md b/source/includes/_queries.md index 1e3f4e45cf7..486879d150e 100644 --- a/source/includes/_queries.md +++ b/source/includes/_queries.md @@ -25,7 +25,7 @@ In the two examples above, the main difference is what you pass in for variables > Example Response: -```javascript +```json { "activities": [{ "entries": [ @@ -40,15 +40,19 @@ In the two examples above, the main difference is what you pass in for variables Determining whether something is available can be thought of as a two part process. -1. Get high-level cachable dates + times to get an idea of, roughly speaking, when a given activity is available. +1. Get high-level cacheable dates + times to get an idea of, roughly speaking, when a given activity is available. 2. Given a specific set of tickets (1x Adult, 2x Child) for a specific time, is it available and what will the price be. The first is what we're going to talk about here. The second is in the next section as we call that a BookingQuote Mutation. -With GraphQL, pulling availability is simply a "field" you can request for an activity: + + +See [ActivityVariants](/#activityvariant). + + ```graphql -query ActivityById($id: ID!, $startDate: Date!, $endDate: Date!, $quantity: Int) { +query DocExampleGetActivityById($id: ID!, $startDate: Date!, $endDate: Date!, $quantity: Int) { activity(id: $id) { name dates(startDate:$startDate, endDate: $endDate, quantity: $quantity) { @@ -58,12 +62,19 @@ query ActivityById($id: ID!, $startDate: Date!, $endDate: Date!, $quantity: Int) localDateTimeStart localDateTimeEnd status + variant { + id + title + description + } } } } } ``` +With GraphQL, pulling availability is simply a "field" you can request for an activity. + Notice with GraphQL, "fields" can accept arguments (in this case the desired availability). The power of the above is that you can do the exact same thing with a search result and in a single query to the server, answer the question "What are the top 10 things to do close by and what dates/times are available for them". @@ -74,3 +85,97 @@ With great power... asking for a very large number of activities, each with a ve We recommend running large availability requests one activity at a time and use the feed only for very short windows; perhaps only returning the next week of days w/ no times, for example. +## ActivityVariants + +An **ActivityVariant** is a variation for the **Activity** that represents little differences between the _original_ activity. + +Any particular activity may consist of a number of variants, each of which might represent different departure times, tour routes, or packaged extras like additional meals, transport and so forth. + +**Attributes** + +| Attribute | Description | +|:------------------|:------------------------------------------------------------------------------------------------------------------| +| **`id`** | id
The unique identifier for the activity variant. | +| **`title`** | string
Human readable text with the short name of the variant. | +| **`description`** | string
Human readable text that contains specific information and details about the variant. | + +```graphql +fragment SimpleOptionsFields on ActivityQuestionOption { + name + value +} + +fragment CompleteOptionsFields on ActivityQuestionOption { + name + questions { + __typename + id + group + required + label + hint + + ... on NumberAndUnitQuestion { + units + } + + ... on LocationQuestion { + allowCustomPickup + maxLength + options { + ...SimpleOptionsFields + } + } + + ... on SingleChoiceQuestion { + options { + ...SimpleOptionsFields + } + } + + ... on StringQuestion { + maxLength + } + } + value +} + +query DocExampleGetActivityVariantById($id: ID!) { + activityVariant(id: $id) { + ... on ActivityVariant { + title + description + questions { + __typename + id + group + required + label + hint + + ... on NumberAndUnitQuestion { + units + } + + ... on LocationQuestion { + allowCustomPickup + maxLength + options { + ...SimpleOptionsFields + } + } + + ... on SingleChoiceQuestion { + options { + ...CompleteOptionsFields + } + } + + ... on StringQuestion { + maxLength + } + } + } + } +} +``` diff --git a/source/includes/_updates.md b/source/includes/_updates.md new file mode 100644 index 00000000000..2ba60bdd7d4 --- /dev/null +++ b/source/includes/_updates.md @@ -0,0 +1,8 @@ +# Updates + +| Date | Description | +|:-------------|:----------------------------------------------------------------------------------| +| Nov 6, 2023 | Removed section: Mutations. Added new sections: [Entities](#entities) [CreateBookingQuote](#createbookingquote), [CreateBookingQuote Errors](#createbookingquote-errors), [UpdateBookingQuote](#updatebookingquote), [UpdateBookingQuote Errors](#updatebookingquote-errors), [CreateBooking](#createbooking), [CreateBooking Errors](#createbooking-errors), [CancelBooking](#cancelbooking), [CancelBooking Errors](#cancelbooking-errors) and [Updates](#updates)| +| Jun 7, 2022 | Added new sections: [Go Live Process](#go-live-process) and [Webhooks](#webhooks) | +| May 13, 2020 | Added new section: [Try It Out](#try-it-out) | +| Jan 31, 2020 | Initial Docs | \ No newline at end of file diff --git a/source/index.html.md b/source/index.html.md index 388d7a8dc06..c4231e88f4b 100644 --- a/source/index.html.md +++ b/source/index.html.md @@ -15,11 +15,13 @@ includes: - auth - idempotency - versioning + - entities - queries - mutations - webhooks - try_it_out - go_live_testing + - updates search: true ---