guesty-client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +34 -0
- data/LICENSE +21 -0
- data/README.md +178 -0
- data/lib/guesty/access_tokens/upsert.rb +61 -0
- data/lib/guesty/api_error.rb +73 -0
- data/lib/guesty/client.rb +138 -0
- data/lib/guesty/config.rb +29 -0
- data/lib/guesty/conversations/attachments/index.rb +42 -0
- data/lib/guesty/conversations/index.rb +58 -0
- data/lib/guesty/conversations/messages/reply.rb +55 -0
- data/lib/guesty/conversations/posts/index.rb +121 -0
- data/lib/guesty/conversations/posts/since.rb +37 -0
- data/lib/guesty/conversations/search.rb +31 -0
- data/lib/guesty/conversations/show.rb +25 -0
- data/lib/guesty/conversations.rb +15 -0
- data/lib/guesty/error.rb +32 -0
- data/lib/guesty/guest_folio/invoice_items/index.rb +61 -0
- data/lib/guesty/guests/profiles/create.rb +25 -0
- data/lib/guesty/guests/profiles/show.rb +23 -0
- data/lib/guesty/guests/profiles/update.rb +36 -0
- data/lib/guesty/guests.rb +5 -0
- data/lib/guesty/homes/calendars/show.rb +134 -0
- data/lib/guesty/homes/search.rb +46 -0
- data/lib/guesty/memory_rate_limiter.rb +28 -0
- data/lib/guesty/memory_token_store.rb +33 -0
- data/lib/guesty/operation.rb +37 -0
- data/lib/guesty/rate_limiter_middleware.rb +23 -0
- data/lib/guesty/reservations/accommodation_fare/update.rb +44 -0
- data/lib/guesty/reservations/approve.rb +23 -0
- data/lib/guesty/reservations/create.rb +50 -0
- data/lib/guesty/reservations/custom_fields/update.rb +26 -0
- data/lib/guesty/reservations/dates/update.rb +41 -0
- data/lib/guesty/reservations/decline.rb +30 -0
- data/lib/guesty/reservations/flags/update.rb +33 -0
- data/lib/guesty/reservations/payments/create.rb +72 -0
- data/lib/guesty/reservations/pre_approve.rb +23 -0
- data/lib/guesty/reservations/recover.rb +27 -0
- data/lib/guesty/reservations/reviews/extractors/airbnb.rb +66 -0
- data/lib/guesty/reservations/reviews/extractors/base.rb +36 -0
- data/lib/guesty/reservations/reviews/extractors/booking_com.rb +35 -0
- data/lib/guesty/reservations/reviews/extractors/vrbo.rb +46 -0
- data/lib/guesty/reservations/reviews/index.rb +67 -0
- data/lib/guesty/reservations/search.rb +63 -0
- data/lib/guesty/reservations/show.rb +28 -0
- data/lib/guesty/reservations/status/cancel.rb +34 -0
- data/lib/guesty/reservations/status/update.rb +21 -0
- data/lib/guesty/reservations/stays/update.rb +42 -0
- data/lib/guesty/reservations/update.rb +19 -0
- data/lib/guesty/reservations.rb +13 -0
- data/lib/guesty/reviews/respond.rb +19 -0
- data/lib/guesty/version.rb +3 -0
- data/lib/guesty/views/create.rb +19 -0
- data/lib/guesty/views/update.rb +33 -0
- data/lib/guesty/webhooks/create.rb +21 -0
- data/lib/guesty/webhooks/index.rb +18 -0
- data/lib/guesty/webhooks/signatures/verify.rb +76 -0
- data/lib/guesty/webhooks/sync.rb +36 -0
- data/lib/guesty/webhooks/update.rb +22 -0
- data/lib/guesty-client.rb +1 -0
- data/lib/guesty.rb +78 -0
- data/sig/guesty.rbs +427 -0
- metadata +191 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Guesty
|
|
2
|
+
# The default token store: keeps one token per credential slot in memory for
|
|
3
|
+
# the life of the process. Guesty mints only 5 tokens per client id per 24
|
|
4
|
+
# hours, so a multi-process host must inject a shared, persistent store, such
|
|
5
|
+
# as a database table behind a cache.
|
|
6
|
+
class MemoryTokenStore
|
|
7
|
+
Token = Struct.new(:access_token, :expires_at, keyword_init: true) do
|
|
8
|
+
def expired?
|
|
9
|
+
expires_at.nil? || expires_at < Time.now
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@tokens = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @rbs (::String slot_key) -> Token?
|
|
18
|
+
def read(slot_key)
|
|
19
|
+
token = @tokens[slot_key]
|
|
20
|
+
|
|
21
|
+
token if token && !token.expired?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @rbs (slot_key: ::String, access_token: ::String, expires_at: ::Time) -> Token
|
|
25
|
+
def save(slot_key:, access_token:, expires_at:)
|
|
26
|
+
@tokens[slot_key] = Token.new(access_token:, expires_at:)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def clear
|
|
30
|
+
@tokens = {}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Guesty
|
|
2
|
+
# Shared monadic contract for the client operations: #call returns a Result,
|
|
3
|
+
# #call! unwraps it and raises the underlying failure. An error raised inside
|
|
4
|
+
# #call becomes Failure(error), mirroring the host's operation base.
|
|
5
|
+
module Operation
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.include Dry::Monads[:result]
|
|
8
|
+
base.prepend(Call)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module Call
|
|
12
|
+
def call(*, **, &)
|
|
13
|
+
super
|
|
14
|
+
rescue StandardError => e
|
|
15
|
+
Failure(e)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call!(...)
|
|
20
|
+
result = call(...)
|
|
21
|
+
raise result.failure if result.failure?
|
|
22
|
+
|
|
23
|
+
result
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def argument!(fatal: false, **attrs)
|
|
29
|
+
key, reason = attrs.first
|
|
30
|
+
raise Guesty::ArgumentError.new("#{key}: #{reason}", fatal:)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def error!(message, fatal: false)
|
|
34
|
+
raise Guesty::Error.new(message, fatal:)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Guesty
|
|
2
|
+
# Faraday request middleware delegating throttling to the injected limiter
|
|
3
|
+
# (Guesty.config.rate_limiter), so a host can share the windows across
|
|
4
|
+
# processes. Registered as :guesty_rate_limiter.
|
|
5
|
+
class RateLimiterMiddleware < Faraday::Middleware
|
|
6
|
+
# @rbs (untyped app, ?::Hash[::Symbol, untyped] options) -> void
|
|
7
|
+
def initialize(app, options = {})
|
|
8
|
+
super
|
|
9
|
+
|
|
10
|
+
@key = options[:key]
|
|
11
|
+
@threshold = options[:threshold]
|
|
12
|
+
@interval = options[:interval]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def on_request(_env)
|
|
16
|
+
key = @key.is_a?(Proc) ? @key.call : @key
|
|
17
|
+
|
|
18
|
+
Guesty.config.rate_limiter.throttle!(key:, threshold: @threshold, interval: @interval)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Faraday::Request.register_middleware(guesty_rate_limiter: RateLimiterMiddleware)
|
|
23
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Guesty
|
|
2
|
+
module Reservations
|
|
3
|
+
module AccommodationFare
|
|
4
|
+
class Update
|
|
5
|
+
include Guesty::Operation
|
|
6
|
+
|
|
7
|
+
FARE_TYPES = %w[AF AFA AFD].freeze #: ::Array[::String]
|
|
8
|
+
ADJUSTED_TYPE = 'AF'.freeze #: ::String
|
|
9
|
+
|
|
10
|
+
# @rbs (reservation_id: ::String, amount: ::Integer | ::Float | ::BigDecimal) -> Dry::Monads::Result[::BigDecimal]
|
|
11
|
+
def call(reservation_id:, amount:)
|
|
12
|
+
argument! reservation_id: :required if reservation_id.blank?
|
|
13
|
+
argument! amount: :negative, fatal: true if amount.negative?
|
|
14
|
+
|
|
15
|
+
adjustment = amount.to_d - current_fare(reservation_id)
|
|
16
|
+
return Success(adjustment) if adjustment.zero?
|
|
17
|
+
|
|
18
|
+
response = Guesty.do_request :post, 'price-adjustments/manual-total-amount', payload(reservation_id, adjustment), retries: 0
|
|
19
|
+
return response unless response.success?
|
|
20
|
+
|
|
21
|
+
Success(adjustment)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# @rbs (::String reservation_id) -> ::BigDecimal
|
|
27
|
+
def current_fare(reservation_id)
|
|
28
|
+
reservation = Show.new.call!(reservation_id:, fields: %w[money.invoiceItems]).value!
|
|
29
|
+
items = reservation.dig(:money, :invoiceItems)
|
|
30
|
+
|
|
31
|
+
items.select { |item| FARE_TYPES.include?(item[:normalType]) }.sum(BigDecimal('0')) { |item| item[:amount].to_d }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @rbs (::String reservation_id, ::BigDecimal adjustment) -> ::Hash[::Symbol, untyped]
|
|
35
|
+
def payload(reservation_id, adjustment)
|
|
36
|
+
{
|
|
37
|
+
reservationId: reservation_id,
|
|
38
|
+
priceAdjustments: [{ normalType: ADJUSTED_TYPE, amount: adjustment.to_f }]
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Guesty
|
|
2
|
+
module Reservations
|
|
3
|
+
# Approves a request to book (a reservation in status `reserved`). Runs
|
|
4
|
+
# asynchronously on Guesty's side; a repeat answers 400 DUPLICATE.
|
|
5
|
+
class Approve
|
|
6
|
+
include Guesty::Operation
|
|
7
|
+
|
|
8
|
+
# @rbs (reservation_id: ::String) -> Dry::Monads::Result[bool]
|
|
9
|
+
def call(reservation_id:)
|
|
10
|
+
argument! reservation_id: :required if reservation_id.blank?
|
|
11
|
+
|
|
12
|
+
response = Guesty.do_request(
|
|
13
|
+
:post,
|
|
14
|
+
"reservations-v3/#{reservation_id}/approve",
|
|
15
|
+
retries: 0
|
|
16
|
+
)
|
|
17
|
+
return response unless response.success?
|
|
18
|
+
|
|
19
|
+
Success(true)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Guesty
|
|
2
|
+
module Reservations
|
|
3
|
+
class Create
|
|
4
|
+
include Guesty::Operation
|
|
5
|
+
|
|
6
|
+
REQUIRED_ATTRIBUTE_KEYS = %i[
|
|
7
|
+
guestId
|
|
8
|
+
listingId
|
|
9
|
+
source
|
|
10
|
+
status
|
|
11
|
+
ignoreCalendar
|
|
12
|
+
ignoreTerms
|
|
13
|
+
checkInDateLocalized
|
|
14
|
+
checkOutDateLocalized
|
|
15
|
+
].freeze #: ::Array[::Symbol]
|
|
16
|
+
GUEST_COUNT_KEYS = %i[numberOfAdults numberOfChildren].freeze #: ::Array[::Symbol]
|
|
17
|
+
|
|
18
|
+
# @rbs (attributes: ::Hash[::Symbol, untyped]) -> Dry::Monads::Result[::String]
|
|
19
|
+
def call(attributes:)
|
|
20
|
+
validate_attributes!(attributes)
|
|
21
|
+
|
|
22
|
+
response = Guesty.do_request :post, 'reservations-v3', attributes, retries: 0, timeout: 60
|
|
23
|
+
Success(handle_response(response))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def validate_attributes!(attributes)
|
|
29
|
+
argument! attributes: :required if attributes.blank?
|
|
30
|
+
|
|
31
|
+
REQUIRED_ATTRIBUTE_KEYS.each do |key|
|
|
32
|
+
argument! key => :required if attributes[key].nil?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
guests = attributes[:numberOfGuests]
|
|
36
|
+
return unless guests
|
|
37
|
+
|
|
38
|
+
GUEST_COUNT_KEYS.each do |key|
|
|
39
|
+
argument! key => :required if guests[key].blank?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def handle_response(response)
|
|
44
|
+
raise response.failure unless response.success?
|
|
45
|
+
|
|
46
|
+
response.value!.body.fetch(:reservationId)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Guesty
|
|
2
|
+
module Reservations
|
|
3
|
+
module CustomFields
|
|
4
|
+
class Update
|
|
5
|
+
include Guesty::Operation
|
|
6
|
+
|
|
7
|
+
# @rbs (reservation_id: ::String, custom_fields: ::Array[::Hash[::Symbol, untyped]]) -> Dry::Monads::Result[::Hash[::Symbol, untyped]]
|
|
8
|
+
def call(reservation_id:, custom_fields:)
|
|
9
|
+
argument! reservation_id: :required if reservation_id.blank?
|
|
10
|
+
argument! custom_fields: :required if custom_fields.blank?
|
|
11
|
+
|
|
12
|
+
response = Guesty.do_request :put, "reservations-v3/#{reservation_id}/custom-fields", { customFields: custom_fields }
|
|
13
|
+
Success(handle_response(response))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def handle_response(response)
|
|
19
|
+
raise response.failure unless response.success?
|
|
20
|
+
|
|
21
|
+
response.value!.body
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Guesty
|
|
2
|
+
module Reservations
|
|
3
|
+
module Dates
|
|
4
|
+
class Update
|
|
5
|
+
include Guesty::Operation
|
|
6
|
+
|
|
7
|
+
# @rbs (reservation_id: ::String, attributes: ::Hash[::Symbol, untyped]) -> Dry::Monads::Result[bool]
|
|
8
|
+
def call(reservation_id:, attributes:)
|
|
9
|
+
argument! reservation_id: :required if reservation_id.blank?
|
|
10
|
+
argument! attributes: :required if attributes.blank?
|
|
11
|
+
|
|
12
|
+
payload = date_attributes(attributes)
|
|
13
|
+
response = Guesty.do_request(
|
|
14
|
+
:put,
|
|
15
|
+
"reservations-v3/#{reservation_id}/dates",
|
|
16
|
+
payload
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
raise response.failure if response.failure?
|
|
20
|
+
|
|
21
|
+
Success(true)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# @rbs (::Hash[::Symbol, untyped] attributes) -> ::Hash[::Symbol, untyped]
|
|
27
|
+
def date_attributes(attributes)
|
|
28
|
+
{
|
|
29
|
+
checkInDateLocalized: attributes[:start_date]&.iso8601,
|
|
30
|
+
checkOutDateLocalized: attributes[:end_date]&.iso8601,
|
|
31
|
+
plannedArrival: attributes[:check_in]&.strftime('%H:%M'),
|
|
32
|
+
plannedDeparture: attributes[:check_out]&.strftime('%H:%M'),
|
|
33
|
+
ignoreCalendar: attributes[:ignore_calendar],
|
|
34
|
+
ignoreTerms: attributes[:ignore_terms],
|
|
35
|
+
applyRecalculation: false
|
|
36
|
+
}.compact
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Guesty
|
|
2
|
+
module Reservations
|
|
3
|
+
# Declines a request to book. Airbnb answers 422 without a reason; Guesty
|
|
4
|
+
# documents only `dates_not_available`.
|
|
5
|
+
class Decline
|
|
6
|
+
include Guesty::Operation
|
|
7
|
+
|
|
8
|
+
# @rbs (reservation_id: ::String, reason: ::String, message: ::String) -> Dry::Monads::Result[bool]
|
|
9
|
+
def call(reservation_id:, reason:, message:)
|
|
10
|
+
argument! reservation_id: :required if reservation_id.blank?
|
|
11
|
+
argument! reason: :required if reason.blank?
|
|
12
|
+
argument! message: :required if message.blank?
|
|
13
|
+
|
|
14
|
+
body = {
|
|
15
|
+
reason:,
|
|
16
|
+
messageToGuest: message
|
|
17
|
+
}
|
|
18
|
+
response = Guesty.do_request(
|
|
19
|
+
:post,
|
|
20
|
+
"reservations-v3/#{reservation_id}/decline",
|
|
21
|
+
body,
|
|
22
|
+
retries: 0
|
|
23
|
+
)
|
|
24
|
+
return response unless response.success?
|
|
25
|
+
|
|
26
|
+
Success(true)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Guesty
|
|
2
|
+
module Reservations
|
|
3
|
+
module Flags
|
|
4
|
+
# Sets account custom fields on a reservation. The field ids are
|
|
5
|
+
# account-specific, so the host injects them (Guesty.config.custom_fields,
|
|
6
|
+
# keyed by flag name: blacklisted, grants_created, grants_partial).
|
|
7
|
+
class Update
|
|
8
|
+
include Guesty::Operation
|
|
9
|
+
|
|
10
|
+
# @rbs (reservation_id: ::String, flags: ::Hash[::Symbol, bool]) -> Dry::Monads::Result[bool]
|
|
11
|
+
def call(reservation_id:, flags:)
|
|
12
|
+
custom_fields = flags.map { |name, value| { fieldId: field_ids.fetch(name), value: value.to_s } }
|
|
13
|
+
body = { customFields: custom_fields }
|
|
14
|
+
response = Guesty.do_request(
|
|
15
|
+
:put,
|
|
16
|
+
"reservations-v3/#{reservation_id}/custom-fields",
|
|
17
|
+
body
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
raise response.failure if response.failure?
|
|
21
|
+
|
|
22
|
+
Success(true)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def field_ids
|
|
28
|
+
Guesty.config.custom_fields
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Guesty
|
|
2
|
+
module Reservations
|
|
3
|
+
module Payments
|
|
4
|
+
# Records a payment taken outside Guesty. Idempotent per payment intent:
|
|
5
|
+
# a reservation already fully paid, or with a payment noting the intent,
|
|
6
|
+
# is returned without a post.
|
|
7
|
+
class Create
|
|
8
|
+
include Guesty::Operation
|
|
9
|
+
|
|
10
|
+
PAYMENT_METHOD_NAME = 'RECORDED_CASH'.freeze #: ::String
|
|
11
|
+
HISTORY_UNAVAILABLE = 'Guesty has not returned the payment history yet. Try again later.'.freeze #: ::String
|
|
12
|
+
|
|
13
|
+
# @rbs (reservation_id: ::String, attributes: ::Hash[::Symbol, untyped]) -> Dry::Monads::Result[::Hash[::Symbol, untyped]]
|
|
14
|
+
def call(reservation_id:, attributes:)
|
|
15
|
+
validate_arguments!(reservation_id:, attributes:)
|
|
16
|
+
|
|
17
|
+
@reservation_id = reservation_id
|
|
18
|
+
|
|
19
|
+
return Success(reservation) if fully_paid? || recorded?(attributes)
|
|
20
|
+
|
|
21
|
+
response = Guesty.do_request :post, "reservations/#{reservation_id}/payments", payload(attributes), retries: 0
|
|
22
|
+
return response unless response.success?
|
|
23
|
+
|
|
24
|
+
Success(response.value!.body)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def validate_arguments!(reservation_id:, attributes:)
|
|
30
|
+
argument! reservation_id: :required if reservation_id.blank?
|
|
31
|
+
argument! attributes: :required if attributes.blank?
|
|
32
|
+
argument! amount: :required if attributes[:amount].blank?
|
|
33
|
+
argument! amount: :must_be_positive if attributes[:amount].present? && attributes[:amount] <= 0
|
|
34
|
+
argument! paid_at: :required if attributes[:paid_at].blank?
|
|
35
|
+
argument! payment_intent_id: :required if attributes[:payment_intent_id].blank?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @rbs () -> ::Hash[::Symbol, untyped]
|
|
39
|
+
def reservation
|
|
40
|
+
@reservation ||= Show.new.call!(reservation_id: @reservation_id, fields: %w[money.isFullyPaid money.payments money.totalPaid]).value!
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @rbs () -> bool
|
|
44
|
+
def fully_paid?
|
|
45
|
+
reservation[:money][:isFullyPaid]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @rbs (::Hash[::Symbol, untyped] attributes) -> bool
|
|
49
|
+
def recorded?(attributes)
|
|
50
|
+
payments = reservation[:money][:payments]
|
|
51
|
+
|
|
52
|
+
return payments.any? { |payment| payment[:note]&.include?(attributes[:payment_intent_id]) } if payments
|
|
53
|
+
return false if reservation[:money][:totalPaid]&.zero?
|
|
54
|
+
|
|
55
|
+
error! HISTORY_UNAVAILABLE
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @rbs (::Hash[::Symbol, untyped] attributes) -> ::Hash[::Symbol, untyped]
|
|
59
|
+
def payload(attributes)
|
|
60
|
+
{
|
|
61
|
+
paymentMethod: {
|
|
62
|
+
method: PAYMENT_METHOD_NAME
|
|
63
|
+
},
|
|
64
|
+
amount: attributes[:amount],
|
|
65
|
+
paidAt: attributes[:paid_at].strftime('%Y-%m-%d'),
|
|
66
|
+
note: "Payment recorded in Guestit.se / Stripe #{attributes[:payment_intent_id]}"
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Guesty
|
|
2
|
+
module Reservations
|
|
3
|
+
# Pre-approves an inquiry (Airbnb only, asynchronous). An inquiry cannot be
|
|
4
|
+
# declined through the API.
|
|
5
|
+
class PreApprove
|
|
6
|
+
include Guesty::Operation
|
|
7
|
+
|
|
8
|
+
# @rbs (reservation_id: ::String) -> Dry::Monads::Result[bool]
|
|
9
|
+
def call(reservation_id:)
|
|
10
|
+
argument! reservation_id: :required if reservation_id.blank?
|
|
11
|
+
|
|
12
|
+
response = Guesty.do_request(
|
|
13
|
+
:post,
|
|
14
|
+
"reservations-v3/#{reservation_id}/pre-approve",
|
|
15
|
+
retries: 0
|
|
16
|
+
)
|
|
17
|
+
return response unless response.success?
|
|
18
|
+
|
|
19
|
+
Success(true)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Guesty
|
|
2
|
+
module Reservations
|
|
3
|
+
# Finds the Guesty reservation matching a confirmation code and stay window,
|
|
4
|
+
# skipping canceled ones. Returns nil when none matches.
|
|
5
|
+
class Recover
|
|
6
|
+
include Guesty::Operation
|
|
7
|
+
|
|
8
|
+
# @rbs (confirmation_code: ::String, start_date: ::Date, end_date: ::Date) -> Dry::Monads::Result[::String?]
|
|
9
|
+
def call(confirmation_code:, start_date:, end_date:)
|
|
10
|
+
fields = %w[_id listingId checkInDateLocalized checkOutDateLocalized status confirmationCode]
|
|
11
|
+
attributes = { confirmation_code: }
|
|
12
|
+
result = Guesty::Reservations::Search.new.call!(
|
|
13
|
+
fields:,
|
|
14
|
+
attributes:,
|
|
15
|
+
take: 2
|
|
16
|
+
).value!
|
|
17
|
+
candidate = result.fetch(:results).find do |reservation|
|
|
18
|
+
reservation[:status] != CANCELLED_STATUS &&
|
|
19
|
+
reservation[:checkInDateLocalized] == start_date.iso8601 &&
|
|
20
|
+
reservation[:checkOutDateLocalized] == end_date.iso8601
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Success(candidate&.dig(:_id))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module Guesty
|
|
2
|
+
module Reservations
|
|
3
|
+
module Reviews
|
|
4
|
+
module Extractors
|
|
5
|
+
# Airbnb raw reviews carry category_ratings[].review_category_tags.
|
|
6
|
+
class Airbnb < Base
|
|
7
|
+
POSITIVE_TAG = 'GUEST_REVIEW_HOST_POSITIVE_'.freeze #: ::String
|
|
8
|
+
NEGATIVE_TAG = 'GUEST_REVIEW_HOST_NEGATIVE_'.freeze #: ::String
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# @rbs (::Hash[::Symbol, untyped] raw_review) -> ::Hash[::Symbol, untyped]
|
|
13
|
+
def extract(raw_review)
|
|
14
|
+
codes = tag_codes(raw_review)
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
**super,
|
|
18
|
+
positive_tags: tags(codes, POSITIVE_TAG),
|
|
19
|
+
negative_tags: tags(codes, NEGATIVE_TAG),
|
|
20
|
+
other_tags: codes.reject { it.start_with?(POSITIVE_TAG, NEGATIVE_TAG) }.map(&:downcase)
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def extract_text(raw_review)
|
|
25
|
+
{
|
|
26
|
+
public: raw_review[:public_review],
|
|
27
|
+
private: raw_review[:private_feedback]
|
|
28
|
+
}.compact_blank
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def extract_scores(raw_review)
|
|
32
|
+
scores = { rating: raw_review[:overall_rating]&.to_f }
|
|
33
|
+
|
|
34
|
+
category_ratings = raw_review[:category_ratings]
|
|
35
|
+
return unless category_ratings.is_a?(Array)
|
|
36
|
+
|
|
37
|
+
category_ratings.each do |rating_entry|
|
|
38
|
+
next unless rating_entry.is_a?(Hash)
|
|
39
|
+
|
|
40
|
+
category = rating_entry[:category].downcase.to_sym
|
|
41
|
+
rating = rating_entry[:rating]
|
|
42
|
+
|
|
43
|
+
scores[category] = rating.to_f if category.present? && rating.present?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
scores.compact
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @rbs (::Hash[::Symbol, untyped] raw_review) -> ::Array[::String]
|
|
50
|
+
def tag_codes(raw_review)
|
|
51
|
+
category_ratings = raw_review[:category_ratings]
|
|
52
|
+
|
|
53
|
+
return [] unless category_ratings.is_a?(Array)
|
|
54
|
+
|
|
55
|
+
category_ratings.flat_map { it[:review_category_tags] }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @rbs (::Array[::String] codes, ::String prefix) -> ::Array[::String]
|
|
59
|
+
def tags(codes, prefix)
|
|
60
|
+
codes.select { it.start_with?(prefix) }.map { it.delete_prefix(prefix).downcase }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Guesty
|
|
2
|
+
module Reservations
|
|
3
|
+
module Reviews
|
|
4
|
+
module Extractors
|
|
5
|
+
class Base
|
|
6
|
+
include Guesty::Operation
|
|
7
|
+
|
|
8
|
+
# @rbs (raw_review: ::Hash[::Symbol, untyped]) -> Dry::Monads::Result[::Hash[::Symbol, untyped]]
|
|
9
|
+
def call(raw_review:)
|
|
10
|
+
argument! raw_review: :required, fatal: true if raw_review.blank?
|
|
11
|
+
|
|
12
|
+
Success(extract(raw_review))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# @rbs (::Hash[::Symbol, untyped] raw_review) -> ::Hash[::Symbol, untyped]
|
|
18
|
+
def extract(raw_review)
|
|
19
|
+
{
|
|
20
|
+
text: extract_text(raw_review),
|
|
21
|
+
scores: extract_scores(raw_review)
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def extract_text(raw_review)
|
|
26
|
+
raise NotImplementedError, "#{self.class} must implement #extract_text"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def extract_scores(raw_review)
|
|
30
|
+
raise NotImplementedError, "#{self.class} must implement #extract_scores"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Guesty
|
|
2
|
+
module Reservations
|
|
3
|
+
module Reviews
|
|
4
|
+
module Extractors
|
|
5
|
+
class BookingCom < Base
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def extract_text(raw_review)
|
|
9
|
+
{
|
|
10
|
+
public: [
|
|
11
|
+
raw_review.dig(:content, :headline),
|
|
12
|
+
raw_review.dig(:content, :positive),
|
|
13
|
+
raw_review.dig(:content, :negative)
|
|
14
|
+
].compact_blank.join(' ').presence
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def extract_scores(raw_review)
|
|
19
|
+
scores = {} #: ::Hash[::Symbol, untyped]
|
|
20
|
+
category_ratings = raw_review[:scoring]
|
|
21
|
+
return unless category_ratings.is_a?(Hash)
|
|
22
|
+
|
|
23
|
+
category_ratings.each do |category, rating|
|
|
24
|
+
scores[category.to_sym] = (rating.to_f / 2).round(2) # Booking.com gives 1-10, we want 0-5
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
scores[:rating] = scores[:review_score]
|
|
28
|
+
scores[:cleanliness] = scores[:clean]
|
|
29
|
+
scores.except(:clean, :review_score).compact
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Guesty
|
|
2
|
+
module Reservations
|
|
3
|
+
module Reviews
|
|
4
|
+
module Extractors
|
|
5
|
+
class Vrbo < Base
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def extract_text(raw_review)
|
|
9
|
+
{
|
|
10
|
+
public: [
|
|
11
|
+
raw_review.dig(:title, :value),
|
|
12
|
+
raw_review.dig(:body, :value)
|
|
13
|
+
].compact.reject(&:empty?).join(' ').presence
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def extract_scores(raw_review)
|
|
18
|
+
scores = { rating: raw_review[:starRatingOverall].to_f }
|
|
19
|
+
|
|
20
|
+
raw_ratings = raw_review[:starRatings]
|
|
21
|
+
return unless raw_ratings.is_a?(Array)
|
|
22
|
+
|
|
23
|
+
raw_ratings.each do |rating|
|
|
24
|
+
next unless rating.is_a?(Hash)
|
|
25
|
+
|
|
26
|
+
category_key = normalize_category(rating[:category].downcase.to_sym)
|
|
27
|
+
scores[category_key] ||= rating[:value].to_f
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
scores.compact
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def normalize_category(category)
|
|
34
|
+
case category
|
|
35
|
+
when :roomcleanliness then :cleanliness
|
|
36
|
+
when :onlinelisting then :accuracy
|
|
37
|
+
when :overall then :rating
|
|
38
|
+
else
|
|
39
|
+
category
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|