tipi-cloudbeds
Version:
Node.js library to connect to cloudbeds REST API
576 lines (543 loc) • 19 kB
JavaScript
const { Oops } = require('@gokiteam/oops')
const Joi = require('joi')
const _ = require('lodash')
const request = require('request-promise-native')
const QueryParamsMaker = require('./helpers/QueryParamsMaker')
const GrantType = require('./enums/GrantType')
const Schemas = require('./Schemas')
const ErrorReason = require('./enums/ErrorReason')
class Cloudbeds {
get defaults () {
return Object.freeze({
base: 'https://hotels.cloudbeds.com/api/v1.1',
resourceName: 'tipi-cloudbeds'
})
}
handleError (error) {
if (error.isOops) throw error
let oopsError = Oops.internal('something went wrong').reason(ErrorReason.unknown)
if (error.statusCode) {
const { statusCode, error: { error: errorType, error_description: errorDescription } = {} } = error
if (statusCode === 401 && errorType === 'access_denied') {
oopsError = Oops.unauthorized('The access token is invalid or expired.')
.reason(ErrorReason.invalidAccessToken)
} else if (statusCode === 401 && ['invalid_request', 'invalid_client'].includes(errorType)) {
oopsError = Oops.unauthorized('The refresh token is invalid or expired.')
.reason(ErrorReason.invalidRefreshToken)
} else if (errorDescription === 'The authorization code is invalid or has expired.') {
oopsError = Oops.unauthorized('The authorization code is invalid or expired')
.reason(ErrorReason.invalidAuthorizationCode)
} else if (errorDescription === 'The client secret supplied for a confidential client is invalid.') {
oopsError = Oops.unauthorized('The client secret supplied for a confidential client is invalid.')
.reason(ErrorReason.invalidClientSecret)
} else if (errorType === 'server_error' && !errorDescription) {
oopsError = Oops.unauthorized('The client id is invalid.')
.reason(ErrorReason.invalidClientId)
}
}
throw oopsError.resource(this.defaults.resourceName).meta(error.toString())
}
constructor ({ clientId, clientSecret, mockBase = undefined } = {}) {
if (!clientId || !clientSecret)
throw Oops.failedPrecondition('client id and client secret are required')
this.clientId = clientId
this.clientSecret = clientSecret
if (mockBase) {
this.mockBase = mockBase
}
}
get baseUrl () {
if (this.mockBase) {
return this.mockBase
}
return this.defaults.base
}
validate (data, schema) {
const validationResult = Joi.validate(data, schema)
if (validationResult.error)
throw Oops.invalidArgument(validationResult.error.toString())
return true
}
async generateToken (data) {
this.validate(data, Schemas.generateToken)
const { grantType: grant_type, redirectUri: redirect_uri, code, refreshToken: refresh_token } = data
const requestData = {
method: 'POST',
uri: `${this.baseUrl}/access_token`,
json: true,
formData: {
client_secret: this.clientSecret,
client_id: this.clientId,
grant_type
}
}
if (code) requestData.formData.code = code
if (redirect_uri) requestData.formData.redirect_uri = redirect_uri
if (refresh_token) requestData.formData.refresh_token = refresh_token
try {
const { access_token: accessToken, token_type: tokenType, expires_in: expiresIn, refresh_token: refreshToken } =
await request(requestData)
return { accessToken, tokenType, expiresIn, refreshToken }
} catch (error) {
this.handleError(error)
}
}
async generateTokenByCode (data) {
this.validate(data, Schemas.generateTokenByCode)
data.grantType = GrantType.authorizationCode
return this.generateToken(data)
}
async generateTokenByRefreshToken (data) {
this.validate(data, Schemas.generateTokenByRefreshToken)
data.grantType = GrantType.refreshToken
return this.generateToken(data)
}
async checkAccessToken (tokens) {
this.validate(tokens, Schemas.checkAccessToken)
const { accessToken, tokenType } = tokens
const requestData = {
method: 'POST',
uri: `${this.baseUrl}/access_token_check`,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
json: true
}
try {
const response = await request(requestData)
return response.success
} catch (error) {
return false
}
}
async refreshAccessToken (tokens) {
this.validate(tokens, Schemas.refreshAccessToken)
const { accessToken, refreshToken, tokenType } = tokens
const isValid = await this.checkAccessToken({ accessToken, tokenType })
if (isValid) return tokens
const newTokens = await this.generateTokenByRefreshToken({ refreshToken })
return newTokens
}
async subscribeToEvent (tokens, data) {
this.validate({ tokens, data }, Schemas.subscribeToEvent)
const { tokenType, accessToken } = tokens
const requestData = {
method: 'POST',
uri: `${this.baseUrl}/postWebhook`,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
formData: data,
json: true
}
try {
const response = await request(requestData)
if (response.success === false) throw Oops.alreadyExists('Subscription already exists')
return response.data.subscriptionID
} catch (error) {
this.handleError(error)
}
}
async unSubscribeFromEvent (tokens, data) {
this.validate({ tokens, data }, Schemas.unSubscribeFromEvent)
const { tokenType, accessToken } = tokens
const { subscriptionId } = data
const requestData = {
method: 'DELETE',
uri: `${this.baseUrl}/deleteWebhook?subscriptionID=${subscriptionId}`,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
json: true
}
try {
const response = await request(requestData)
if (response.success === false) throw Oops.notFound('Subscription not found')
return true
} catch (error) {
this.handleError(error)
}
}
async getReservationById (tokens, data) {
this.validate({ tokens, data }, Schemas.getReservationById)
const { tokenType, accessToken } = tokens
const { reservationId } = data
const requestData = {
method: 'GET',
uri: `${this.baseUrl}/getReservation?reservationID=${reservationId}`,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
json: true
}
try {
const response = await request(requestData)
if (response.success === false) throw Oops.notFound('Reservation not found')
return response.data
} catch (error) {
this.handleError(error)
}
}
async getReservations (tokens, data) {
this.validate({ tokens, data }, Schemas.getReservations)
const { tokenType, accessToken } = tokens
const { propertyId: propertyID, ...rest } = data
let uri = `${this.baseUrl}/getReservations`
if (!_.isEmpty(data)) {
const queryParams = QueryParamsMaker({ propertyID, ...rest })
uri = `${uri}?${queryParams}`
}
const requestData = {
method: 'GET',
uri,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
json: true
}
try {
const response = await request(requestData)
if (response.success === false) throw Oops.notFound('Something went wrong on getting reservations from cloudbeds!')
return response
} catch (error) {
this.handleError(error)
}
}
async postAppState (tokens, data) {
this.validate({ tokens, data }, Schemas.postAppState)
const { tokenType, accessToken } = tokens
const { propertyId: propertyID, appState: app_state } = data
const requestData = {
method: 'POST',
uri: `${this.baseUrl}/postAppState`,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
formData: { propertyID, app_state },
json: true
}
try {
const response = await request(requestData)
return response
} catch (error) {
this.handleError(error)
}
}
async postRoomCheckIn (tokens, data) {
this.validate({ tokens, data }, Schemas.postRoomCheckIn)
const { tokenType, accessToken } = tokens
const { reservationId: reservationID, subReservationId: subReservationID, roomId: roomID } = data
const payload = { reservationID }
if (subReservationID) payload.subReservationID = subReservationID
if (roomID) payload.roomID = roomID
const requestData = {
method: 'POST',
uri: `${this.baseUrl}/postRoomCheckIn`,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
formData: payload,
json: true
}
try {
const response = await request(requestData)
return response
} catch (error) {
this.handleError(error)
}
}
async getHotels (tokens) {
this.validate(tokens, Schemas.getHotels)
const { tokenType, accessToken } = tokens
const requestData = {
method: 'GET',
uri: `${this.baseUrl}/getHotels`,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
json: true
}
try {
const response = await request(requestData)
if (response.success === false) throw Oops.notFound('Hostels not found')
return response.data
} catch (error) {
this.handleError(error)
}
}
async getRooms (tokens, data) {
this.validate({ tokens, data }, Schemas.getRooms)
const { tokenType, accessToken } = tokens
const { page, size } = data
const requestData = {
method: 'GET',
uri: `${this.baseUrl}/getRooms?pageNumber=${page}&pageSize=${size}`,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
json: true
}
try {
const response = await request(requestData)
if (response.success === false) throw Oops.notFound('Rooms not found')
return response.data
} catch (error) {
this.handleError(error)
}
}
async getReservationsWithRateDetails (tokens, data) {
this.validate({ tokens, data }, Schemas.getReservationsWithRateDetails)
const { tokenType, accessToken } = tokens
const { reservationId } = data
const requestData = {
method: 'GET',
uri: `${this.baseUrl}/getReservationsWithRateDetails?reservationID=${reservationId}`,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
json: true
}
try {
const response = await request(requestData)
if (response.success === false) throw Oops.notFound('Reservation not found')
const reservation = response.data.find(item => item.reservationID === reservationId)
if (!reservation) throw Oops.notFound('Reservation not found')
return reservation
} catch (error) {
this.handleError(error)
}
}
// We need this function because /getReservationsWithRateDetails endpoint implemented in another
// function (getReservationsWithRateDetails) and used in services to get one item
async batchGetReservationsWithRateDetails (tokens, data) {
this.validate({ tokens, data }, Schemas.getReservationsWithRateDetails)
const { tokenType, accessToken } = tokens
const { reservationId } = data
const requestData = {
method: 'GET',
uri: `${this.baseUrl}/getReservationsWithRateDetails?reservationID=${reservationId}`,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
json: true
}
try {
const response = await request(requestData)
if (response.success === false) throw Oops.notFound('Reservation not found')
return response
} catch (error) {
this.handleError(error)
}
}
async getHousekeepingStatus (tokens, data) {
this.validate({ tokens, data }, Schemas.getHousekeepingStatus)
const { tokenType, accessToken } = tokens
const { roomTypeIds: roomTypeIDs, housekeeperIds: housekeeperIDs, ...rest } = data
let uri = `${this.baseUrl}/getHousekeepingStatus`
if (!_.isEmpty(data)) {
const queryParams = QueryParamsMaker({ roomTypeIDs, housekeeperIDs, ...rest })
uri = `${uri}?${queryParams}`
}
const requestData = {
method: 'GET',
uri,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
json: true
}
try {
const response = await request(requestData)
if (response.success === false) {
throw Oops.notFound('House keeping status not found').reason(ErrorReason.houseKeepingStatusNotFound)
}
return response
} catch (error) {
this.handleError(error)
}
}
async postReservationNote (tokens, data) {
this.validate({ tokens, data }, Schemas.postReservationNote)
const { tokenType, accessToken } = tokens
const { reservationId: reservationID, reservationNote } = data
const payload = { reservationID, reservationNote }
const requestData = {
method: 'POST',
uri: `${this.baseUrl}/postReservationNote`,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
formData: payload,
json: true
}
try {
const response = await request(requestData)
return response
} catch (error) {
this.handleError(error)
}
}
async getWebhooks (tokens) {
this.validate({ tokens }, Schemas.getWebhooks)
const { tokenType, accessToken } = tokens
const requestData = {
method: 'GET',
uri: `${this.baseUrl}/getWebhooks`,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
json: true
}
try {
const response = await request(requestData)
if (response.success === false) throw Oops.notFound('Webhooks not found')
return response.data
} catch (error) {
this.handleError(error)
}
}
async getGuestsByFilter (tokens, data) {
this.validate({ tokens, data }, Schemas.getGuestsByFilter)
const { tokenType, accessToken } = tokens
const { reservationId: reservationID, roomId: roomID, propertyIds: propertyIDs } = data
const queryParams = QueryParamsMaker({ reservationID, roomID, propertyIDs })
const requestData = {
method: 'GET',
uri: `${this.baseUrl}/getGuestsByFilter?${queryParams}`,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
json: true
}
try {
const response = await request(requestData)
if (response.success === false) {
throw Oops.unknown(response.message || 'unknown error').reason(ErrorReason.unsuccessfulRequest)
}
return response.data
} catch (error) {
this.handleError(error)
}
}
async getGuest (tokens, data) {
this.validate({ tokens, data }, Schemas.getGuest)
const { tokenType, accessToken } = tokens
const { reservationId: reservationID, guestId: guestID, propertyId: propertyID } = data
const queryParams = QueryParamsMaker({ reservationID, guestID, propertyID })
const requestData = {
method: 'GET',
uri: `${this.baseUrl}/getGuest?${queryParams}`,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
json: true
}
try {
const response = await request(requestData)
if (response.success === false) {
throw Oops.unknown(response.message || 'unknown error').reason(ErrorReason.unsuccessfulRequest)
}
return response.data
} catch (error) {
this.handleError(error)
}
}
async putGuest (tokens, data) {
this.validate({ tokens, data }, Schemas.putGuest)
const { tokenType, accessToken } = tokens
const { guestId: guestID, propertyId: propertyID, ...rest } = data
const payload = {
guestID,
...propertyID && { propertyID },
...rest
}
const requestData = {
method: 'PUT',
uri: `${this.baseUrl}/putGuest`,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
body: payload,
json: true
}
try {
const response = await request(requestData)
if (response.success === false) {
throw Oops.unknown(response.message || 'unknown error').reason(ErrorReason.unsuccessfulRequest)
}
return response.data
} catch (error) {
this.handleError(error)
}
}
async postGuest (tokens, data) {
this.validate({ tokens, data }, Schemas.postGuest)
const { tokenType, accessToken } = tokens
const { reservationId: reservationID, propertyId: propertyID, ...rest } = data
const payload = {
reservationID,
...propertyID && { propertyID },
...rest
}
const requestData = {
method: 'POST',
uri: `${this.baseUrl}/postGuest`,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
body: payload,
json: true
}
try {
const response = await request(requestData)
if (response.success === false) {
if (response.message === 'Max number of guests exceeded') {
throw Oops.notAcceptable(response.message).reason(ErrorReason.reservationCapacityIsFull)
}
throw Oops.unknown(response.message || 'unknown error').reason(ErrorReason.unsuccessfulRequest)
}
return { guestId: response.guestID }
} catch (error) {
this.handleError(error)
}
}
async postGuestsToRoom (tokens, data) {
this.validate({ tokens, data }, Schemas.postGuestsToRoom)
const { tokenType, accessToken } = tokens
const {
reservationId: reservationID,
propertyId: propertyID,
roomId: roomID,
guestIds: guestIDs,
removeGuestIds: removeGuestIDs,
...rest
} = data
const payload = {
reservationID,
roomID,
guestIDs,
...removeGuestIDs && { removeGuestIDs },
...propertyID && { propertyID },
...rest
}
const requestData = {
method: 'POST',
uri: `${this.baseUrl}/postGuestsToRoom`,
headers: {
Authorization: `${tokenType} ${accessToken}`
},
body: payload,
json: true
}
try {
const response = await request(requestData)
if (response.success === false) {
throw Oops.unknown(response.message || 'unknown error').reason(ErrorReason.unsuccessfulRequest)
}
return response.data
} catch (error) {
this.handleError(error)
}
}
}
module.exports = Cloudbeds