From c5e3637c4f27e04f795fc233088899aab7d325d8 Mon Sep 17 00:00:00 2001 From: IG Date: Thu, 3 Aug 2023 13:07:00 +0300 Subject: [PATCH] Guides --- content/guides/accounts-synchronization.md | 47 ++++ content/guides/api-overview.md | 40 +++ content/guides/booking-cancelation.md | 86 ++++++ ...eate-a-booking-payments-on-partner-side.md | 160 ++++++++++++ ...te-a-booking-with-smily-payment-gateway.md | 55 ++++ content/guides/create-a-booking.md | 187 +++++++++++++ content/guides/getting-started.md | 83 ++++++ content/guides/index.md | 2 +- content/guides/introduction.md | 16 ++ content/guides/rentals-synchronization.md | 247 ++++++++++++++++++ layouts/guides.html | 40 +-- layouts/head.html | 6 +- static/images/smily-logo.png | Bin 0 -> 5551 bytes 13 files changed, 935 insertions(+), 34 deletions(-) create mode 100644 content/guides/accounts-synchronization.md create mode 100644 content/guides/api-overview.md create mode 100644 content/guides/booking-cancelation.md create mode 100644 content/guides/create-a-booking-payments-on-partner-side.md create mode 100644 content/guides/create-a-booking-with-smily-payment-gateway.md create mode 100644 content/guides/create-a-booking.md create mode 100644 content/guides/getting-started.md create mode 100644 content/guides/introduction.md create mode 100644 content/guides/rentals-synchronization.md create mode 100644 static/images/smily-logo.png diff --git a/content/guides/accounts-synchronization.md b/content/guides/accounts-synchronization.md new file mode 100644 index 0000000..51a8591 --- /dev/null +++ b/content/guides/accounts-synchronization.md @@ -0,0 +1,47 @@ +# Accounts synchronization + +1. TOC +{:toc} + +## Preface + +With this endpoint you can import all accounts who wants to publish their listings on your website. As a next step you can sign a contracts with these accounts if needed, create accounts on your side, etc. + +## Get all accounts + +You can find detailed specification in [Swagger documentation](https://demo.platforms.bookingsync.com/api-docs/index.html) + +> This endpoint does not support pagination. + +> Don't forget to disable accounts and their rentals if you don't see them anymore in current list + +> We suggest to do synchronization of accounts every 12-24 hours. + +~~~ruby +token = "" +api_url = "" +media_type = "application/vnd.api+json" +options = { + headers: { + "User-Agent" => "Api client", + "Accept" => media_type, + "Content-Type" => media_type, + "Authorization" => "Bearer #{token}" + } +} +request = Excon.new(URI.join(api_url, "/api/ota/v1/accounts").to_s, options) +response = request.request({ method: :get }) + +response.status + +already_import_accounts_ids = get_imported_accounts_ids # get an array of already imported accounts, Ex result: [1,2,3] + +json = JSON.parse(response.body) +json["data"].each do |account| + # Import accounts + import_account(account["id"], account["attributes"]["name"]) +end + +accounts_for_disabling = already_import_accounts_ids - json["data"].pluck("id") +disable_accounts_and_rentals(accounts_for_disabling) # Disable accounts +~~~ diff --git a/content/guides/api-overview.md b/content/guides/api-overview.md new file mode 100644 index 0000000..d9dd83a --- /dev/null +++ b/content/guides/api-overview.md @@ -0,0 +1,40 @@ +# Smily Channel API Overview + +1. TOC +{:toc} + +## What is Smily Channel API? + +Channel API allows you to sync Smily properties to your own website (aka Channel). You can choose which properties or accounts to display or not, get rentals availabilities, book rentals and collect payments. + +## Who is Smily Channel API for? + +TODO + +## Why would I use Smily Channel API? + +This API was developed to make integration with our partners easier. If you want to look at all features of our system, please refer to [BookingSync Universe API ](https://developers.bookingsync.com/). + +## How the Smily Channel API works? + +In general, there are 3 steps you have to do: + + 1. Manage accounts that are ready to publish their listings on your website - regularly refresh the list and onboard them. + 2. Manage rentals of onboarded accounts - regularly refresh descriptions, prices and availabilities. + 3. Manage bookings. This step depends on how you want to process the payments - on your side, or on our side. + + +The API is fully [JSONAPI 1.0 compliant](http://jsonapi.org). + +1. Get accounts +2. approve/reject them +3. sign contracts/register/oboarding/etc +4. get rentals of approved accounts + 4.1 Get rentals info (refresh once a day) + 4.2 Get rentals availabilities (refresh every hour) + 4.3 Get rentals prices (every 12 hours) + 4.4 Remove unpublished rentals +5. Booking creation: + 5.1 Create quote to confirm price and availability. + 5.2 Create booking with price not less than in quote response + 5.4 Create payment to confirm booking diff --git a/content/guides/booking-cancelation.md b/content/guides/booking-cancelation.md new file mode 100644 index 0000000..ca754ea --- /dev/null +++ b/content/guides/booking-cancelation.md @@ -0,0 +1,86 @@ +# Booking cancelation + +1. TOC +{:toc} + +## Preface + +Bookings could be canceled according to the rental's cancelation policy. You can get cancelation policy from rental's enpoint, usually it looks like: + +~~~js + "cancelation-policy-items": [ + { + "id": "1", + "penalty-percentage": 10, + "eligible-days": 2, + "message-translations": { + "en": "Lorem ipsum", + "fr": "Lorem ipsum" + } + } + ] +~~~ + +In this case you can cancel the booking not later than 2 days before check-in. And penalty will be 10% from the booking final price. + +## Booking cancelation + +You can find detailed specification in [Swagger documentation](https://demo.platforms.bookingsync.com/api-docs/index.html) + +~~~ruby +token = "" +api_url = "" +media_type = "application/vnd.api+json" +options = { + headers: { + "User-Agent" => "Api client", + "Accept" => media_type, + "Content-Type" => media_type, + "Authorization" => "Bearer #{token}" + } +} + +request = Excon.new(URI.join(api_url, "/bookings/#{booking_id}/cancel").to_s, options) +payload = { + data: { + attributes: { + "cancelation-reason": "canceled_by_traveller_other", + "cancelation-description": "health concern", + "channel-cancelation-cost": "80.0", + "currency": "USD" + }, + type: "bookings", + id: booking_id + } +} +response = request.request({ + method: :patch, + body: payload.to_json +}) + +json = JSON.parse(response.body) +if response.status == 200 + booking_canceled_at = json["data"]["attributes"]["canceled-at"] + # update new booking information +else + handle_errors(json) +end +~~~ + +In some cases you can get a 422 error. Response will look like this: + +~~~js + { + "errors" => [ + { + "code" => "100", + "detail" => "currency - provided currency for cancelation must be the same as the one from booking", + "source" => { + "pointer" => "/data/attributes/currency" + }, + "status" => "422", + "title" => "provided currency for cancelation must be the same as the one from booking" + } + ] + } +~~~ diff --git a/content/guides/create-a-booking-payments-on-partner-side.md b/content/guides/create-a-booking-payments-on-partner-side.md new file mode 100644 index 0000000..82c7aef --- /dev/null +++ b/content/guides/create-a-booking-payments-on-partner-side.md @@ -0,0 +1,160 @@ +# Create a booking and handle payment on partner's side + +1. TOC +{:toc} + +## Preface + +There are 2 ways of handling payments: using Smily payment gateway or process payment on partner side. This document explains second option - how to handle payments on partner's side. + +## Create a quote + +Before creating a booking, you have to confirm the price and availability of rental. To do that, you have to create a quote. + +~~~ruby +token = "" +api_url = "" +media_type = "application/vnd.api+json" +options = { + headers: { + "User-Agent" => "Api client", + "Accept" => media_type, + "Content-Type" => media_type, + "Authorization" => "Bearer #{token}" + } +} + +request = Excon.new(URI.join(api_url, "/api/ota/v1/quotes").to_s, options) +payload = { + data: { + attributes: { + "start-at": "2023-08-04", + "end-at": "2023-08-11", + "adults": 20, + "children": 0, + "rental-id": 428 + }, + type: "quotes" + } +} +response = request.request({ + method: :post, + body: payload.to_json +}) + +json = JSON.parse(response.body) +if response.status == 201 + price = json["data"]["attributes"]["final-price"] + booking_url = json["data"]["attributes"]["booking-url"] + # Now you can create booking via API or redirect user to booking_url +else + handle_errors(json) +end +~~~ + +## Create a booking + +Once you have successfully created a Quote, you can make a booking request. To do that you have to provide all information about the client, dates, rental ID and price. + +> Price should not be less than `final-price` you got from Quote request + +~~~ruby +token = "" +api_url = "" +media_type = "application/vnd.api+json" +options = { + headers: { + "User-Agent" => "Api client", + "Accept" => media_type, + "Content-Type" => media_type, + "Authorization" => "Bearer #{token}", + "Idempotency-Key" => get_order_uuid # optional but useful header, read comments below + } +} + +request = Excon.new(URI.join(api_url, "/api/ota/v1/bookings").to_s, options) +payload = { + data: { + attributes: { + "start-at": "2020-09-04T16:00:00.000Z", # "2020-09-04" works too + "end-at": "2020-09-11T10:00:00.000Z", # "2020-09-11" works too + "adults": 2, + "children": 1, + "final-price": "176.0", + "currency": "EUR", + "rental-id": 1, + "client-first-name": "Rich", + "client-last-name": "Piana", + "client-email": "rich@piana.com", + "client-phone-number": "123123123", + "client-country-code": "US" + }, + type: "quotes" + } +} +response = request.request({ + method: :post, + body: payload.to_json +}) + +json = JSON.parse(response.body) +if response.status == 201 + booking_id = json["data"]["id"] + # save this booking id +else + handle_errors(json) +end +~~~ + +TODO: confirm it +If created booking has field `tentative-expires-at`, it means it could be canceled automatically if you won't create any payment. So, as a next step, we have to create payments. + +We recommend to always set `Idempotency-Key` header. This header allow to avoid duplicates creation. For example if you tried to create a booking and because of network connection or some other error you could not get response, you can safely retry your request and get the correct response, because for a given key, every success response will be cached for 6 hours. +We recommend to generate UUID for each order and use it in `Idempotency-Key` header. + +## Create a payment + +When you handled payment for the booking, you have to notify our us about that. Otherwise booking could be canceled. + +~~~ruby +token = "" +api_url = "" +media_type = "application/vnd.api+json" +options = { + headers: { + "User-Agent" => "Api client", + "Accept" => media_type, + "Content-Type" => media_type, + "Authorization" => "Bearer #{token}", + "Idempotency-Key" => get_payment_uuid # optional but useful header, read comments below + } +} + +request = Excon.new(URI.join(api_url, "/api/ota/v1/payments").to_s, options) +payload = { + data: { + attributes: { + "amount": "100.0", + "currency": "EUR", + "paid-at": "2020-09-10T05:30:18.321Z", + "kind": "credit-card", + "booking-id": booking_id + }, + type: "payments" + } +} +response = request.request({ + method: :post, + body: payload.to_json +}) + +json = JSON.parse(response.body) +if response.status == 201 + booking_id = json["data"]["id"] + # save this booking id +else + handle_errors(json) +end +~~~ + +Payments endpoint also support `Idempotency-Key` header. To ensure idempotent writes and frictionless integration, it is highly recommended to provide `Idempotency-Key` header. For a given key, every success response will be cached for 6 hours. Thanks to that, you can safely retry write operation. diff --git a/content/guides/create-a-booking-with-smily-payment-gateway.md b/content/guides/create-a-booking-with-smily-payment-gateway.md new file mode 100644 index 0000000..80c7610 --- /dev/null +++ b/content/guides/create-a-booking-with-smily-payment-gateway.md @@ -0,0 +1,55 @@ +# Create a booking and handle payments with Smily payment gateway + +1. TOC +{:toc} + +## Preface + +There are 2 ways of handling payments: using Smily payment gateway or process payment on partner side. This document explains first option - how to handle payments with Smily payment gateway. + +## Create a quote + +Before creating a booking, you have to confirm the price and availability of rental. To do that, you have to create a quote. + +~~~ruby +token = "" +api_url = "" +media_type = "application/vnd.api+json" +options = { + headers: { + "User-Agent" => "Api client", + "Accept" => media_type, + "Content-Type" => media_type, + "Authorization" => "Bearer #{token}" + } +} + +request = Excon.new(URI.join(api_url, "/api/ota/v1/quotes").to_s, options) +payload = { + data: { + attributes: { + "start-at": "2023-08-04", + "end-at": "2023-08-11", + "adults": 20, + "children": 0, + "rental-id": 428 + }, + type: "quotes" + } +} +response = request.request({ + method: :post, + body: payload.to_json +}) + +json = JSON.parse(response.body) +if response.status == 201 + price = json["data"]["attributes"]["final-price"] + booking_url = json["data"]["attributes"]["booking-url"] + # Now you can create booking via API or redirect user to booking_url +else + handle_errors(json) +end +~~~ + +If you don't have own payment gateway, you can just redirect user to `booking_url`. That's it :) diff --git a/content/guides/create-a-booking.md b/content/guides/create-a-booking.md new file mode 100644 index 0000000..1997d90 --- /dev/null +++ b/content/guides/create-a-booking.md @@ -0,0 +1,187 @@ +# Create a booking + +TODO: Split into 2? When have own payment gateway and when don't? + +1. TOC +{:toc} + +## Preface + +This chapter explains the booking process. + +## Create a quote + +Before creating a booking, you have to confirm the price and availability of rental. To do that, you have to create a quote. + +~~~ruby +token = "" +api_url = "" +media_type = "application/vnd.api+json" +options = { + headers: { + "User-Agent" => "Api client", + "Accept" => media_type, + "Content-Type" => media_type, + "Authorization" => "Bearer #{token}" + } +} + +request = Excon.new(URI.join(api_url, "/api/ota/v1/quotes").to_s, options) +payload = { + data: { + attributes: { + "start-at": "2023-08-04", + "end-at": "2023-08-11", + "adults": 20, + "children": 0, + "rental-id": 428 + }, + type: "quotes" + } +} +response = request.request({ + method: :post, + body: payload.to_json +}) + +json = JSON.parse(response.body) +if response.status == 201 + price = json["data"]["attributes"]["final-price"] + booking_url = json["data"]["attributes"]["booking-url"] + # Now you can create booking via API or redirect user to booking_url +else + handle_errors(json) +end +~~~ + +Quotes endpoint could return a 422 error. Response will be like that + +~~~json +{ + "errors":[ + { + "title": "period is not available for booking", + "detail": "start-at - period is not available for booking", + "code": "100", + "source": { "pointer": "/data/attributes/start-at" }, + "status": "422" + }, + { + "title": "period is not available for booking", + "detail": "end-at - period is not available for booking", + "code": "100", + "source": { "pointer": "/data/attributes/end-at" }, + "status": "422" + } + ] +} +~~~ + +If you don't have own payment gateway, you can just redirect user to `booking_url`. If you want to handle this order with own payment gateway, follow next instructions. + +## Create a booking + +Once you have successfully created a Quote, you can make a booking request. To do that you have to provide all information about the client, dates, rental ID and price. + +> Price should not be less than `final-price` you got from Quote request + +~~~ruby +token = "" +api_url = "" +media_type = "application/vnd.api+json" +options = { + headers: { + "User-Agent" => "Api client", + "Accept" => media_type, + "Content-Type" => media_type, + "Authorization" => "Bearer #{token}", + "Idempotency-Key" => get_order_uuid # optional but useful header, read comments below + } +} + +request = Excon.new(URI.join(api_url, "/api/ota/v1/bookings").to_s, options) +payload = { + data: { + attributes: { + "start-at": "2020-09-04T16:00:00.000Z", # "2020-09-04" works too + "end-at": "2020-09-11T10:00:00.000Z", # "2020-09-11" works too + "adults": 2, + "children": 1, + "final-price": "176.0", + "currency": "EUR", + "rental-id": 1, + "client-first-name": "Rich", + "client-last-name": "Piana", + "client-email": "rich@piana.com", + "client-phone-number": "123123123", + "client-country-code": "US" + }, + type: "quotes" + } +} +response = request.request({ + method: :post, + body: payload.to_json +}) + +json = JSON.parse(response.body) +if response.status == 201 + booking_id = json["data"]["id"] + # save this booking id +else + handle_errors(json) +end +~~~ + +TODO: confirm it +If created booking has field `tentative-expires-at`, it means it could be canceled automatically if you won't create any payment. So, as a next step, we have to create payments. + +We recommend to always set `Idempotency-Key` header. This header allow to avoid duplicates creation. For example if you tried to create a booking and because of network connection or some other error you could not get response, you can safely retry your request and get the correct response, because for a given key, every success response will be cached for 6 hours. +We recommend to generate UUID for each order and use it in `Idempotency-Key` header. + +## Create a payment + +When you handled payment for the booking, you have to notify our us about that. + +~~~ruby +token = "" +api_url = "" +media_type = "application/vnd.api+json" +options = { + headers: { + "User-Agent" => "Api client", + "Accept" => media_type, + "Content-Type" => media_type, + "Authorization" => "Bearer #{token}", + "Idempotency-Key" => get_payment_uuid # optional but useful header, read comments below + } +} + +request = Excon.new(URI.join(api_url, "/api/ota/v1/payments").to_s, options) +payload = { + data: { + attributes: { + "amount": "100.0", + "currency": "EUR", + "paid-at": "2020-09-10T05:30:18.321Z", + "kind": "credit-card", + "booking-id": booking_id + }, + type: "payments" + } +} +response = request.request({ + method: :post, + body: payload.to_json +}) + +json = JSON.parse(response.body) +if response.status == 201 + booking_id = json["data"]["id"] + # save this booking id +else + handle_errors(json) +end +~~~ + +Payments endpoint also support `Idempotency-Key` header. To ensure idempotent writes and frictionless integration, it is highly recommended to provide `Idempotency-Key` header. For a given key, every success response will be cached for 6 hours. Thanks to that, you can safely retry write operation. diff --git a/content/guides/getting-started.md b/content/guides/getting-started.md new file mode 100644 index 0000000..03cc5a5 --- /dev/null +++ b/content/guides/getting-started.md @@ -0,0 +1,83 @@ +# Getting Started with Smily Channel API + +1. TOC +{:toc} + +## Preface + +These instruction describe how to make your first API call using cURL. Before you start using the API, your need to do the following: + + 1. Request a demo account from [Smily Partners Team](mailto:partners@smily.com) + 2. Get an **API Key** and **host** [Smily Partners Team](mailto:partners@smily.com) + 3. Make sure you have [curl](https://curl.haxx.se/) installed on your machine :) + +## Build your API call + +Your API call must have the following components: + + - **A host.** The host will be provided by our support and will look like `https://.platforms.bookingsync.com`. + - **An Authorization header.** An API Key must be included in the Authorization header. + - **An Accept header.** `Accept` header should always be equal `application/vnd.api+json`. + - **A request.** When submitting data to a resource via POST or PUT, you must submit your payload in JSON. + + +## Make your first call to Smily Channel API + +As a first API call, let's get all [accounts](https://demo.platforms.bookingsync.com/api-docs/index.html), who wants to publish their listings on your website: + +~~~bash +curl -i -X 'GET' '/api/ota/v1/accounts' -H 'accept: application/vnd.api+json' -H 'Authorization: Bearer ' +~~~ + + 1. Copy the curl example above. + 2. Paste the curl call into your favorite text editor. + 3. Copy your **API KEY** and paste it in the "Authorization" header. + 4. Copy your **HOST** and paste it in the url. + 5. Copy the code and paste it in your terminal. + 6. Hit **Enter**. + 7. **HTTP/2 200** at the top of response means you did everything correct! + + +Test code in Ruby with [Excon](https://github.com/excon/excon) library: + +~~~ruby +token = "" +api_url = "" +media_type = "application/vnd.api+json" +options = { + headers: { + "User-Agent" => "Api client", + "Accept" => media_type, + "Content-Type" => media_type, + "Authorization" => "Bearer #{token}" + } +} +request = Excon.new(URI.join(api_url, "/api/ota/v1/accounts").to_s, options) +response = request.request({ method: :get }) + +response.status +~~~ + +Test code in Ruby with [Faraday](https://github.com/lostisland/faraday) library: + +~~~ruby +token = "" +api_url = "" +media_type = "application/vnd.api+json" + +request = Faraday.new({ ssl: { verify: true } }) do |f| + f.adapter :net_http_persistent +end +request.headers[:accept] = media_type +request.headers[:content_type] = media_type +request.headers[:user_agent] = "Api client" +request.headers[:authorization] = "Bearer #{token}" +request.url_prefix = api_url +response = request.send(:get, "/api/ota/v1/accounts") + +response.status +~~~ + +## API response messages + +All responses are returned in JSON format. We specify this by sending the `Content-Type` header. You can find detailed responses specification in [Swagger documentation](https://demo.platforms.bookingsync.com/api-docs/index.html) \ No newline at end of file diff --git a/content/guides/index.md b/content/guides/index.md index fe99c29..ea16859 100644 --- a/content/guides/index.md +++ b/content/guides/index.md @@ -1,3 +1,3 @@ # Guides and Tutorials -Welcome to BookingSync API guides! +Welcome to BookingSync Сhannel API guides! diff --git a/content/guides/introduction.md b/content/guides/introduction.md new file mode 100644 index 0000000..d9e87cc --- /dev/null +++ b/content/guides/introduction.md @@ -0,0 +1,16 @@ +# BookingSync Channel API + +1. TOC +{:toc} + +## Preface + +This API was developed to make integration with our partners easier. If you want to look at all features of our system, please refer to [BookingSync Universe API ](https://developers.bookingsync.com/). The API is fully [JSONAPI 1.0 compliant](http://jsonapi.org). + +## How It Works + +TODO: + +If you have an own website and want to publish rentals ... You can use our API. + +Just contact us, we will create an application. And PM's who want to publish their rentals on your website could install this application. You will have access to API to get accounts, who installed your application and you can sign contracts with them, get details of their rentals, check availability, get costs, and book them. \ No newline at end of file diff --git a/content/guides/rentals-synchronization.md b/content/guides/rentals-synchronization.md new file mode 100644 index 0000000..8f7b87d --- /dev/null +++ b/content/guides/rentals-synchronization.md @@ -0,0 +1,247 @@ +# Rentals synchronization + +1. TOC +{:toc} + +## Preface + +Once you onboarded accounts, you can start import their rentals. + +## Get rentals base information + +You can find detailed specification in [Swagger documentation](https://demo.platforms.bookingsync.com/api-docs/index.html) + +> We suggest to refresh rentals base information once a day + + +TODO: REMOVE disable_rentals + +~~~ruby +token = "" +api_url = "" +media_type = "application/vnd.api+json" +options = { + headers: { + "User-Agent" => "Api client", + "Accept" => media_type, + "Content-Type" => media_type, + "Authorization" => "Bearer #{token}" + } +} + +Account.approved.each do |account| + account_rentals_ids = Rental.for_account(account).pluck(:id) + page_number = 1 + while true do + request = Excon.new(URI.join(api_url, "/api/ota/v1/rentals?page[number]=#{page_number}&page[size]=50&filter[account-id]=#{account.id}").to_s, options) + + response = request.request({ method: :get }) + json = JSON.parse(response.body) + json["data"].each do |rental| + # Import rental + import_rental(rental) + end + + account_rentals_ids -= json["data"].pluck("id").map(&:to_i) + + break if page_number >= json["meta"]["pagination"]["pages"].to_i + page_number++ + end + + disable_rentals(account_rentals_ids) +end +~~~ + +## Get rentals availabilities + +Availability is a just a field (see [Rental Schema](https://demo.platforms.bookingsync.com/api-docs/index.html)). +But it makes sense to sync availabilities more often than other information. + +> We suggest to refresh rentals availabilities every hour + +The best practice here would be to set the parameter `fields`. It allows to fetch only required fields, in our case `availability`. We also recommend to disable rentals during availabilities update. + +TODO: choose approach 1 or approach 2 + + +~~~ruby +token = "" +api_url = "" +media_type = "application/vnd.api+json" +options = { + headers: { + "User-Agent" => "Api client", + "Accept" => media_type, + "Content-Type" => media_type, + "Authorization" => "Bearer #{token}" + } +} + +Account.approved.each do |account| + account_rentals_ids = Rental.for_account(account).pluck(:id) + page_number = 1 + + while true do + request = Excon.new(URI.join(api_url, "/api/ota/v1/rentals?page[number]=#{page_number}&page[size]=50&filter[account-id]=#{account.id}&fields[rentals]=availability").to_s, options) + + response = request.request({ method: :get }) + json = JSON.parse(response.body) + json["data"].each do |rental| + update_rental_availability(rental) + end + + account_rentals_ids -= json["data"].pluck("id").map(&:to_i) + break if page_number >= json["meta"]["pagination"]["pages"].to_i + page_number++ + end + disable_rentals(account_rentals_ids) +end +~~~ + + +~~~ruby +token = "" +api_url = "" +media_type = "application/vnd.api+json" +options = { + headers: { + "User-Agent" => "Api client", + "Accept" => media_type, + "Content-Type" => media_type, + "Authorization" => "Bearer #{token}" + } +} + +Rental.each do |rental| + request = Excon.new(URI.join(api_url, "/api/ota/v1/rentals/#{rental.remote_id}&fields[rentals]=availability").to_s, options) + + response = request.request({ method: :get }) + if response.code == 200 + json = JSON.parse(response.body) + update_rental_availability(rental["data"]["attributes"]["availability"]) + make_rental_visible_if_was_hidden(rental) + else + hide_rental(rental) + end +end +~~~ + +## Understanding availabilities + +The regular availability object looks like: + +~~~ruby +{ + "map": "0001111111111111111111111111100111111111111111110000000001111111111111111111111111111111111111111000001111111100111111100011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "start-date": "2023-07-01", + "id": "2" +} +~~~ + +It has a `map` with statuses for the next 1096 days starting starting from the date indicated by `start-date`. '0' means available, '1' means unavailable. + +Scope for ActiveRecord rental model could look like (assuming you are using Postgres and named `availability[map]` as `availability_map` and `availability[start_date]` as `availability_start_date`): + +TODO: add raw SQL + +~~~ruby + scope :by_availabilities, ->(date, length) { + unavailable_status = '1' + start_point = "DATE_PART('day', TIMESTAMP :date - availability_start_date)::integer+1" + availability_to_check_sql = "SUBSTR(availability_map, #{start_point}, :length_of_stay)" + where("#{availability_to_check_sql} NOT SIMILAR TO :check_statuses", date: date, length_of_stay: length, check_statuses: unavailable_status) + } +~~~ + +## Get rentals prices + +Before we start, please read an article [Understanding LOS Records](https://developers.bookingsync.com/guides/understanding-los-records/). It explains what is LOS records and how it works. +We generate LOS records for the next 18 months, starting from yesterday. + +To get rental prices better to use `/api/ota/v1/los-record-export-urls` endpoint. It will return a list of CSV files with LOS records for rentals. CSV files will look like: + + +> /los-records for debug only! Don't use it! + +~~~bash +id;account_id;rental_id;currency;min_occupancy;max_occupancy;kind;day;rates +45086517195;1;11;EUR;1;4;final_price;2023-07-25;{0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,12501.45,13741.65,14981.85,16222.05,17462.25,18702.45,19942.65,21182.85,22423.05,23663.25,24903.45,26143.65,27383.85,28624.05,29864.25,31104.45,32344.65,33584.85,34825.05,36065.25,37305.45} +46986331108;2;22;EUR;1;1;final_price;2023-07-25;{0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,4422.6} +47234184996;3;33;EUR;1;1;final_price;2023-07-25;{0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,4422.6} +47235754161;4;44;EUR;1;1;final_price;2023-07-25;{0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,4422.6} +47844857128;5;55;EUR;1;4;final_price;2023-07-25;{0.0,0.0,0.0,0.0,0.0,0.0,1096.2,1252.8,1409.4,1566.0,1722.6,1879.2,2035.8,2192.4,2349.0,2505.6,2662.2,2818.8,2975.4,3132.0,3288.6,3445.2,3601.8,3758.4,3915.0,4071.6,4228.2,4384.8,4541.4,4698.0} +~~~ + +There are 3 kinds of LOS records ([LOS kinds](https://developers.bookingsync.com/reference/enums/#los-kinds)): + + 1. **rental_price** - Price for the rent only, after all discounts applied. + 2. **rental_price_before_special_offers** - Price for the rent only, before special offers discounts being applied. + 3. **final_price** - Price including all required fees and taxes. + +If you want to avoid price discrepancy, you have to use **rental_price** and apply all the fees and taxes on your side. Otherwise just use **final_price**. +TODO: EXPLAIN search price and booking price difference + +> We update LOS files every 12 hours. + +## Filter rentals by price + +We suggest to create a table for LOS records and import prices from CSV files into this table. + +TODO: add raw SQL + +~~~sql + create_table "eur_los_records", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.bigint "synced_id", null: false + t.date "day", null: false + t.integer "min_occupancy" + t.integer "max_occupancy" + t.bigint "rental_id", null: false + t.decimal "rates_eur", default: [], array: true + t.string "kind" + t.bigint "account_id" + t.index ["account_id", "day", "rental_id"], name: "index_elr_on_rental_account_day_rental" + t.index ["rental_id", "account_id", "day"], name: "index_elr_on_day_rental_account" + end +~~~ + +~~~ruby +class EurLosRecord < ApplicationRecord + # Helper scopes + scope :by_occupancy, -> (occupancy) { + occupancy.to_i > 0 ? where("max_occupancy >= ? AND min_occupancy <= ?", occupancy, occupancy) : by_default + } + scope :by_default, -> { where(min_occupancy: 1) } + scope :by_date, -> (date) { where(day: date) } + scope :possible_to_stay_for, -> (length) { where("rates_eur[:length] IS NOT NULL", length: length) } + scope :by_min_max_price_eur, -> (length, min_price_eur, max_price_eur) { + by_min_price_eur(length, min_price_eur).by_max_price_eur(length, max_price_eur) + } + scope :by_min_price_eur, -> (length, min_price_eur) { + if min_price_eur.present? + where("rates_eur[:length] >= :min_price_eur", length: length, min_price_eur: min_price_eur) + end + } + scope :by_max_price_eur, -> (length, max_price_eur) { + if max_price_eur.present? + where("rates_eur[:length] <= :max_price_eur", length: length, max_price_eur: max_price_eur) + end + } + # The main search scope + scope :by_occupancy_date_rate_price, ->(occupancy, date, length, min_price_eur, max_price_eur, los_kind = nil) { + scope = by_occupancy(occupancy).by_date(date) + scope = scope.by_kind(los_kind) if los_kind.present? + scope.possible_to_stay_for(length) + .by_min_max_price_eur(length, min_price_eur, max_price_eur) + } +end +~~~ + +In controller you can use this scope like: + +~~~ruby + # `by_availabilities` scope was described above + @rentals = Rental.by_availabilities(params[:date], params[:length]) + @rentals = @rentals + .joins(:eur_los_records) + .merge(EurLosRecord.by_occupancy_date_rate_price(params[:occupancy], params[:date], params[:length], params[:min_price], params[:max_price])) +~~~ \ No newline at end of file diff --git a/layouts/guides.html b/layouts/guides.html index b634dc3..8397acd 100644 --- a/layouts/guides.html +++ b/layouts/guides.html @@ -4,36 +4,16 @@
- <%= link_to_with_current("Introduction", "/guides/", - class: "list-group-item") %> - <%= link_to_with_current("GDPR", - "/guides/gdpr", class: "list-group-item") %> - <%= link_to_with_current("Create a Booking with a Source", - "/guides/create-a-booking-with-a-source", class: "list-group-item") %> - <%= link_to_with_current("Read all authorized accounts at once", - "/guides/read-all-authorized-accounts-at-once", class: "list-group-item") %> - <%= link_to_with_current("Getting Started with PHP", - "/guides/getting-started-with-php", class: "list-group-item") %> - <%= link_to_with_current("Pricing concepts", - "/guides/pricing-concepts", class: "list-group-item") %> - <%= link_to_with_current("API best practices", - "/guides/api-best-practices", class: "list-group-item") %> - <%= link_to_with_current("Secure Payments By BookingSync", - "/guides/secure-payments-by-bookingsync", class: "list-group-item") %> - <%= link_to_with_current("Webhook subscriptions", - "/guides/webhook-subscriptions", class: "list-group-item") %> - <%= link_to_with_current("OAuth scopes", - "/guides/oauth-scopes", class: "list-group-item") %> - <%= link_to_with_current("BookingSync Universe API [EARLY BETA]", - "/guides/bookigsync-universe-api-early-beta", class: "list-group-item") %> - <%= link_to_with_current("updated_since flow", - "/guides/updated-since-flow", class: "list-group-item") %> - <%= link_to_with_current("Understanding LOS Records", - "/guides/understanding-los-records", class: "list-group-item") %> - <%= link_to_with_current("Understanding Midterm Pricing", - "/guides/understanding-midterm-pricing", class: "list-group-item") %> - <%= link_to_with_current("Understanding Inbox Messaging", - "/guides/understanding-inbox-messaging", class: "list-group-item") %> + <%= link_to_with_current("Introduction", "/guides/introduction" , class: "list-group-item" ) %> + <%= link_to_with_current("API overview", "/guides/api-overview" , class: "list-group-item" ) %> + <%= link_to_with_current("Getting Started", "/guides/getting-started", class: "list-group-item") %> + <%= link_to_with_current("Accounts synchronization", "/guides/accounts-synchronization" , class: "list-group-item" ) %> + <%= link_to_with_current("Rentals synchronization", "/guides/rentals-synchronization" , class: "list-group-item" ) %> + <%= link_to_with_current("Create a booking", "/guides/create-a-booking" , class: "list-group-item" ) %> + <%= link_to_with_current("Create a booking (payments on partner side)", "/guides/create-a-booking-payments-on-partner-side" , class: "list-group-item" ) %> + <%= link_to_with_current("Create a booking (payments on our side)", "/guides/create-a-booking-with-smily-payment-gateway" , + class: "list-group-item" ) %> + <%= link_to_with_current("Booking cancelation", "/guides/booking-cancelation" , class: "list-group-item" ) %>
diff --git a/layouts/head.html b/layouts/head.html index 9539f1e..bc02352 100644 --- a/layouts/head.html +++ b/layouts/head.html @@ -38,10 +38,10 @@ Guides
  • "> - API Reference + API Reference
  • -
  • BookingSync.com
  • -
  • +
  • Smily.com
  • +
  • diff --git a/static/images/smily-logo.png b/static/images/smily-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..704d01ef5f7af11efc46b943c1f000bd5774483b GIT binary patch literal 5551 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 literal 0 HcmV?d00001