baseUrl = $baseUrl; if (!str_ends_with($this->baseUrl, '/')) { $this->baseUrl .= '/'; } } // Add our key to the URL $this->baseUrl .= "api/host/{$publicIntegrationKey}/"; } /* * Do an API call using 'method' to 'endpoint' with $data being * an array of parameters. * * Modified from Stack Overflow original: * https://stackoverflow.com/questions/9802788/call-a-rest-api-in-php */ public function api($method, $endpoint, $data=array()) { $curl = curl_init(); switch ($method) { case "POST": curl_setopt($curl, CURLOPT_POST, 1); if (!empty($data)) { curl_setopt($curl, CURLOPT_POSTFIELDS, $data); } break; case "PUT": curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PUT'); if (!empty($data)) { curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($data) ); } break; default: if ($data) { $endpoint = sprintf("%s?%s", $endpoint, http_build_query($data) ); } } curl_setopt($curl, CURLOPT_URL, "{$this->baseUrl}{$endpoint}"); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); $result = curl_exec($curl); curl_close($curl); return json_decode($result, true); } /* * Perform a GET request on an API endpoint with the given * parameters */ public function get($endpoint, $parameters) { return $this->api('GET', $endpoint, $parameters); } /* * Perform a PUT request on an API endpoint with the given * parameters */ public function put($endpoint, $parameters) { return $this->api('PUT', $endpoint, $parameters); } /* * Perform a POST request on an API endpoint with the given * parameters */ public function post($endpoint, $parameters) { return $this->api('POST', $endpoint, $parameters); } /* * Helper function to 'normalize' dates */ protected function normalize_date(string $date) { if (strpos($date, '/') !== false) { $tmp = explode('/', $date); $date = "{$tmp[2]}-{$tmp[0]}-{$tmp[1]}"; } return $date; } /* * Fetch locations from the API. This will fetch an object that maps * two-letter state codes to array lists of cities available in that * state. * * { * "NC": [ * "Cary", * "Raleigh", * ... * ], * ... * } * * This will rarely change so it is recommended you cache these * results in some fashion for performance reasons. */ public function getLocations() { return $this->get('locations', array()); } /* * This fetches available units based on the parameters provided. * * From and to should be dates in the format of either "YYYY-MM-DD" or * "MM/DD/YYYY" in string format. * * num_guests should be some number of guests. It may be a string or * an integer. * * state and city should be strings, populated by city/state from * getLocations. City can be an empty string to search all cities. * * Results are returned in the following structure: * * [ * "id": "uuid based ID for unit", * "title": "unit title", * "description": "a brief description", * "url": "a URL for more information", * "max_guests": maximum number of guests, * "num_bedrooms": integer number of bedrooms, * "num_bathrooms": decimal number of bedrooms, * "price": integer price in 'stripe' format * "nights": number of nights, * "price_per_night": price per night in 'stripe' format, * "latitude": latitude geo location * "longitude": longitude geo location, * "type": This will be a text such as "House", "Apartment", etc., * "square_feet": size of unit in square feet * "neighborhood": some brief text description about the location * "bed_info": [ * { * "King": integer, * "Queen": integer, * ... (keys will be missing if 0) * }, * ... * ] * }, * ... * ] * * This array might be empty if there are no units available. bed_info * may also be empty, and the totals in bed_info may not match "num_beds". * It is up to the host to make sure those add up correctly. * * If there is a 'user interface error', meaning, any of the required * parameters (from, to, num_guests, state, city) are missing or * otherwise invalid (to before from, for example), you will instead * get this structure: * * { * "error": true, * "message": a helpful message but maybe not the best for the user * } * * Note: HTTP status will still be 200 * * The message -will- be relevant to a developer but might look strange * to an end user, so it is recommended you implement your own error * checking prior to submitting the form and present errors in a clean * fashion. */ function getAvailability($from, $to, $num_guests, $state, $city) { // Normalize dates to YYYY-MM-DD format $from = $this->normalize_date($from); $to = $this->normalize_date($to); return $this->get( 'units/availability', array( 'checkin' => $from, 'checkout' => $to, 'city' => $city, 'province' => $state, 'guests' => $num_guests, ) ); } /* * This fetches available units based on the parameters provided. * * From and to should be dates in the format of either "YYYY-MM-DD" or * "MM/DD/YYYY" in string format. * * num_guests should be some number of guests. It may be a string or * an integer. * * query is a query string; it can be anything. It will be processed * by HERE's search API and turned into at lat/lng coordinate that is * used to locate nearby units. * * center is a centerpoint for doing the query. It would be wise for * this to be central to where the units are, however it is technically * optional. Be warned that if you don't set it, you will likely get * false hits in other states or even countries. * * Results are returned in the following structure: * * { * found: { * // results from HERE API, documented here: * https://www.here.com/docs/bundle/geocoding-and-search-api-developer-guide/page/topics-api/code-geocode-spatial-reference.html * }, * results: [ * { * "id": "uuid based ID for unit", * "title": "unit title", * "distance": distance in meters to location * "description": "a brief description", * "url": "a URL for more information", * "max_guests": maximum number of guests, * "num_bedrooms": integer number of bedrooms, * "num_bathrooms": decimal number of bedrooms, * "price": integer price in 'stripe' format * "nights": number of nights, * "price_per_night": price per night in 'stripe' format, * "latitude": latitude geo location * "longitude": longitude geo location, * "type": This will be a text such as "House", "Apartment", etc., * "square_feet": size of unit in square feet * "neighborhood": some brief text description about the location * "bed_info": [ * { * "King": integer, * "Queen": integer, * ... (keys will be missing if 0) * }, * ... * ] * }, * ... * ] * } * * This could make an error, in which case 'found' will be empty. * The only error this could be is we couldn't find the location * they want to find units close to. * * If there is a 'user interface error', meaning, any of the required * parameters (from, to, num_guests, state, city) are missing or * otherwise invalid (to before from, for example), you will instead * get this structure: * * { * "error": true, * "message": a helpful message but maybe not the best for the user * } * * Note: HTTP status will still be 200 * * The message -will- be relevant to a developer but might look strange * to an end user, so it is recommended you implement your own error * checking prior to submitting the form and present errors in a clean * fashion. */ function getAvailabilityNear($from, $to, $num_guests, $query, $center=null) { // Normalize dates to YYYY-MM-DD format $args = array( 'checkin' => $this->normalize_date($from), 'checkout' => $this->normalize_date($to), 'guests' => $num_guests, 'q' => $query, ); if (!empty($center)) { $args['center'] = $center; } return $this->get( 'units/near', $args ); } /* * This fetches the next availability based on a number of nights * instead of specific days. * * Takes parameters: * * state * city * nights * guests * * Returns a structure like this: * * [ * { * "id": "uuid based ID for unit", * "title": "unit title", * "url": "a URL for more information", * "neighborhood": some brief text description about the location, * "nights_available": Number of nights available * "starting": date the nights_available starts, * "city": city, * "province": province * "num_bedrooms": number of bedrooms, * "num_bathrooms": number of bathrooms * }, * ... * ] * * This array might be empty if there are no units available. * * If there is a 'user interface error', meaning, any of the required * parameters (nights, num_guests, state, city) are missing or * otherwise invalid (to before from, for example), you will instead * get this structure: * * { * "error": true, * "message": a helpful message but maybe not the best for the user * } * * Note: HTTP status will still be 200 * * The message -will- be relevant to a developer but might look strange * to an end user, so it is recommended you implement your own error * checking prior to submitting the form and present errors in a clean * fashion. */ function getNextAvailability($nights, $num_guests, $state, $city) { return $this->get( 'units/next_availability', array( 'nights' => $nights, 'city' => $city, 'province' => $state, 'guests' => $num_guests, ) ); } /* * Fetches final pricing (factoring in pets, etc.) for a unit. * * If the unit is no longer available, we will return alternatives. * * Dates are in the same format as getAvailability * * * If unit is available: * * { * "available": true, * "num_nights": integer number of nights in the stay, * "pay_schedule": [ * { * "charge_on": "YYYY-MM-DD", * "amount": integer in stripe format * "amount_tax": integer in stripe format * } * ], * "totals": { * "num_nights": integer number of nights of the stay * "cat_deposit": integer amount in 'stripe' format * "pet_fee": integer amount in 'stripe' format * "authorize": integer amount in 'stripe' format * "charge": integer amount in 'stripe' format * "charge_tax": integer amount in 'stripe' format * "monthly": integer amount in 'stripe' format * "monthly_tax": integer amount in 'stripe' format * "last_month": integer amount in 'stripe' format * "last_month_tax": integer amount in 'stripe' format * "total_cost": integer amount in 'stripe' format * "total_tax": integer amount in 'stripe' format * "utility_fee": integer amount in 'stripe' format * }, * "title": "unit title", * "description": "a brief description", * "url": "a URL for more information", * "price_per_night": price per night in 'stripe' format, * "neighborhood": some brief text description about the location * } * * If unit is NOT available: * * { * "available": false, * "next_available": { Next time the unit requested is available * "available_start": "YYYY-MM-DD" date unit is available * "available_checkout": "YYYY-MM-DD" proposed checkout date * "available_nights": integer night count, or null if calendar is open. * "title": "unit title", * "description": "a brief description", * "url": "a URL for more information", * "neighborhood": some brief text description about the location, * "max_guests": maximum number of guests, * "num_bedrooms": integer number of bedrooms, * "num_bathrooms": decimal number of bedrooms, * "latitude": latitude geo location * "longitude": longitude geo location, * "type": This will be a text such as "House", "Apartment", etc., * "square_feet": size of unit in square feet * "price": integer price in 'stripe' format * "nights": number of nights, * "price_per_night": price per night in 'stripe' format, * "bed_info": [ * { * "King": integer, * "Queen": integer, * ... (keys will be missing if 0) * }, * ... * ] * }, * "alternatives": [ * // same structure as 'availability' * ] * } * * If there is a 'user interface error', meaning, any of the required * parameters (from, to, num_guests, state, city) are missing or * otherwise invalid (to before from, for example), you will instead * get this structure: * * { * "error": true, * "message": a helpful message but maybe not the best for the user * } * * Note: HTTP status will still be 200 * * The message -will- be relevant to a developer but might look strange * to an end user, so it is recommended you implement your own error * checking prior to submitting the form and present errors in a clean * fashion. */ function getPricing($id, $from, $to, $num_guests, $num_dogs, $num_cats, $city, $state, $customer_id = false) { // Normalize dates to YYYY-MM-DD format $from = $this->normalize_date($from); $to = $this->normalize_date($to); // Scrub ID $id = preg_replace('/[^\d\w\-]/', '', $id); return $this->get( "units/{$id}/pricing", array( 'checkin' => $from, 'checkout' => $to, 'guests' => $num_guests, 'dogs' => $num_dogs, 'cats' => $num_cats, 'city' => $city, 'province' => $state, 'customer_id' => $customer_id, ) ); } /* * Get a unit ID based on the unit URL. * * This is used by WordPress to get a unit ID from a permalink. * * Parameter: 'url' the URL to check * * We match on an 'ends with' basis. We will match both with and * without a trailing /. * * This could match multiple results, so this returns an array * of potentially matching unit information. The structure will be * as follows: * * [ * { * "id": the unit ID * "url": the URL we have on record * "title": the title we have for the unit * } * ] * * The array may be empty if we didn't match anything. This shouldn't * make an error. */ public function unitFromUrl($url) { return $this->post( 'unit_from_url', array( 'url' => $url ) ); } /* * Get blocked dates for a unit * * * Blocked dates will be an array of start/end date pairs in the format * yyyy-mm-dd (as strings) * * So... * * [ * [start_date, end_date], * [start_date, end_date], * ... * ] */ public function blockedDates($id) { return $this->get( "units/{$id}/blocked_dates", array() ); } /* * This attempts to 'login' a user. It will send the user an email * with a code they need to provide. The code is used in validateUser * * The response from this is as follows: * * { * "message": "some message", * "success": true if success, false if failed, message has error * } */ public function loginUser ($email) { return $this->post( 'customers/login', array( 'email' => $email, ) ); } /* * This attempts to validate a user. The code from the email is passed * into this, at which point we confirm the email and return a stripe * customer that can be used to set up payment using the bookUnit call. * * The response from this is as follows: * * { * "message": "some message", * "success": true if success, false if failed, message has error * "stripe_customer": stripe customer ID string (if success) * } */ public function validateUser ($email, $code) { return $this->post( 'customers/validate', array( 'email' => $email, 'email_code' => $code, ) ); } /* * This attempts to book the unit. It requires the following parameters: * * unit_id - the ID of the unit to book * from - checkin date in mm/dd/yyyy or yyyy-mm-dd format * to - checkout date in mm/dd/yyyy or yyyy-mm-dd format * guests - number of guests * dogs - number of dogs * cats - number of cats/other pets * customer_id - stripe customer ID from validateUser * city - the city * state - the state/province * * If the unit is still available, the response will be: * * { * 'available' => true, * 'client_secret' => Stripe client secret for payment intent * 'expires' => expiration UNIX timestamp, in seconds * 'contract_url' => URL to send the customer to sign contract * AFTER payment is done. Use this as the * stripe return URL. * } * * If the unit is no longer available, the response is the same as * with pricing. * * { * "available": false, * "alternatives": [ * // same structure as 'availability' * ] * } */ public function bookUnit ($id, $from, $to, $num_guests, $num_dogs, $num_cats, $customer_id, $city, $state) { // Normalize dates to YYYY-MM-DD format $from = $this->normalize_date($from); $to = $this->normalize_date($to); // Scrub ID $id = preg_replace('/[^\d\w\-]/', '', $id); return $this->post( "units/{$id}/book", array( 'checkin' => $from, 'checkout' => $to, 'guests' => $num_guests, 'dogs' => $num_dogs, 'cats' => $num_cats, 'city' => $city, 'province' => $state, 'customer_id' => $customer_id, ) ); } }