listo-delivery 0.1.1
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/LICENSE +21 -0
- data/README.md +152 -0
- data/lib/listo_delivery/client.rb +170 -0
- data/lib/listo_delivery/errors.rb +53 -0
- data/lib/listo_delivery/generated/api_version.rb +12 -0
- data/lib/listo_delivery/generated/client_operations.rb +82 -0
- data/lib/listo_delivery/generated/loader.rb +69 -0
- data/lib/listo_delivery/generated/models/balance.rb +48 -0
- data/lib/listo_delivery/generated/models/cancel_delivery_request.rb +29 -0
- data/lib/listo_delivery/generated/models/card_onboarding_link.rb +21 -0
- data/lib/listo_delivery/generated/models/card_status.rb +22 -0
- data/lib/listo_delivery/generated/models/connect_request.rb +28 -0
- data/lib/listo_delivery/generated/models/courier_ping.rb +58 -0
- data/lib/listo_delivery/generated/models/create_connect_request_request.rb +35 -0
- data/lib/listo_delivery/generated/models/create_connection_request.rb +21 -0
- data/lib/listo_delivery/generated/models/create_delivery_request.rb +189 -0
- data/lib/listo_delivery/generated/models/create_quote_request.rb +111 -0
- data/lib/listo_delivery/generated/models/create_store_owner_request.rb +26 -0
- data/lib/listo_delivery/generated/models/create_store_request.rb +104 -0
- data/lib/listo_delivery/generated/models/create_withdrawal_request.rb +29 -0
- data/lib/listo_delivery/generated/models/delivery.rb +879 -0
- data/lib/listo_delivery/generated/models/delivery_courier_location_event.rb +23 -0
- data/lib/listo_delivery/generated/models/delivery_list.rb +34 -0
- data/lib/listo_delivery/generated/models/delivery_message_skipped.rb +37 -0
- data/lib/listo_delivery/generated/models/delivery_message_skipped_event.rb +23 -0
- data/lib/listo_delivery/generated/models/delivery_status_changed_event.rb +23 -0
- data/lib/listo_delivery/generated/models/delivery_updated_event.rb +23 -0
- data/lib/listo_delivery/generated/models/error.rb +56 -0
- data/lib/listo_delivery/generated/models/error_response.rb +20 -0
- data/lib/listo_delivery/generated/models/event_list.rb +34 -0
- data/lib/listo_delivery/generated/models/ledger_entry.rb +62 -0
- data/lib/listo_delivery/generated/models/ledger_entry_list.rb +34 -0
- data/lib/listo_delivery/generated/models/manifest.rb +20 -0
- data/lib/listo_delivery/generated/models/manifest_item.rb +36 -0
- data/lib/listo_delivery/generated/models/owner_invitation.rb +27 -0
- data/lib/listo_delivery/generated/models/ping.rb +56 -0
- data/lib/listo_delivery/generated/models/quote.rb +99 -0
- data/lib/listo_delivery/generated/models/refund_delivery_request.rb +28 -0
- data/lib/listo_delivery/generated/models/request_log.rb +91 -0
- data/lib/listo_delivery/generated/models/request_log_detail.rb +105 -0
- data/lib/listo_delivery/generated/models/request_log_list.rb +34 -0
- data/lib/listo_delivery/generated/models/reschedule_delivery_request.rb +41 -0
- data/lib/listo_delivery/generated/models/same_day_terms.rb +106 -0
- data/lib/listo_delivery/generated/models/send_test_webhook_request.rb +22 -0
- data/lib/listo_delivery/generated/models/store.rb +235 -0
- data/lib/listo_delivery/generated/models/store_balance.rb +48 -0
- data/lib/listo_delivery/generated/models/store_connected_event.rb +23 -0
- data/lib/listo_delivery/generated/models/store_list.rb +34 -0
- data/lib/listo_delivery/generated/models/store_updated_event.rb +23 -0
- data/lib/listo_delivery/generated/models/update_store_request.rb +94 -0
- data/lib/listo_delivery/generated/models/webhook_delivery.rb +99 -0
- data/lib/listo_delivery/generated/models/webhook_delivery_list.rb +34 -0
- data/lib/listo_delivery/generated/models/webhook_event.rb +69 -0
- data/lib/listo_delivery/generated/models/webhook_event_envelope.rb +35 -0
- data/lib/listo_delivery/generated/models/withdrawal.rb +91 -0
- data/lib/listo_delivery/generated/models/withdrawal_list.rb +34 -0
- data/lib/listo_delivery/generated/models/withdrawal_updated_event.rb +23 -0
- data/lib/listo_delivery/generated/resources/balance.rb +43 -0
- data/lib/listo_delivery/generated/resources/card_onboarding.rb +70 -0
- data/lib/listo_delivery/generated/resources/connect_requests.rb +31 -0
- data/lib/listo_delivery/generated/resources/connections.rb +31 -0
- data/lib/listo_delivery/generated/resources/deliveries.rb +128 -0
- data/lib/listo_delivery/generated/resources/events.rb +59 -0
- data/lib/listo_delivery/generated/resources/ledger.rb +47 -0
- data/lib/listo_delivery/generated/resources/quotes.rb +31 -0
- data/lib/listo_delivery/generated/resources/request_logs.rb +45 -0
- data/lib/listo_delivery/generated/resources/stores.rb +143 -0
- data/lib/listo_delivery/generated/resources/webhooks.rb +46 -0
- data/lib/listo_delivery/generated/resources/withdrawals.rb +60 -0
- data/lib/listo_delivery/model.rb +69 -0
- data/lib/listo_delivery/util.rb +76 -0
- data/lib/listo_delivery/version.rb +7 -0
- data/lib/listo_delivery/webhooks.rb +98 -0
- data/lib/listo_delivery.rb +16 -0
- data/listo_delivery.gemspec +27 -0
- metadata +120 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4550bd5dde54839887c33c6e4b6d3503f76511ff792fbe42a3892b3cfc8f4023
|
|
4
|
+
data.tar.gz: b03b099ec41ea855d5f64a65a43a61c72f75c6cd8c0941dcd84ed14016870e8e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3fb3e87d92ffe548aae67303864a604ab4f3b998bbcd1e83d0c3a1622d02757273c59c3c57d90d0caa702e438536b3d29dd3e5d8c3f0cdbfce397711bee02779
|
|
7
|
+
data.tar.gz: c805469a01fcb20b242b100aceee19d3a5f662eeac069ba1836583172b1ef9a4ec7c4ecac629c23731363fc10e1ab1515b3b75d336c7daecfa69e0419a1e7f85
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cheto Labs, S.A.P.I. de C.V.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# listo-delivery (Ruby)
|
|
2
|
+
|
|
3
|
+
The official Ruby client for the Listo Delivery API. Standard library only
|
|
4
|
+
(`net/http`, `json`, `openssl`, `securerandom`), Ruby 3.1 or newer.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
gem install listo-delivery
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quickstart
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
require "listo_delivery"
|
|
16
|
+
|
|
17
|
+
client = ListoDelivery::Client.new(api_key: ENV.fetch("LISTO_API_KEY")) # lst_test_... or lst_live_...
|
|
18
|
+
client.mode # => "test"
|
|
19
|
+
|
|
20
|
+
store = "st_4bR8kM2wQ7xT1nV5cZ9pLdYe"
|
|
21
|
+
quote = client.quotes.create(
|
|
22
|
+
store_id: store,
|
|
23
|
+
dropoff: {street: "Blvd. Puerta de Hierro 4965, Zapopan, Jalisco 45116"},
|
|
24
|
+
payment_method: "cash",
|
|
25
|
+
order_value_cents: 19_600
|
|
26
|
+
)
|
|
27
|
+
puts quote.fee_cents
|
|
28
|
+
|
|
29
|
+
delivery = client.deliveries.create(
|
|
30
|
+
store_id: store,
|
|
31
|
+
quote_id: quote.id,
|
|
32
|
+
external_id: "order-1042",
|
|
33
|
+
payment_method: "cash",
|
|
34
|
+
order_value_cents: 19_600,
|
|
35
|
+
dropoff: {name: "Ana", phone: "+523312345678", street: "Blvd. Puerta de Hierro 4965, Zapopan, Jalisco 45116"},
|
|
36
|
+
idempotency_key: "order-1042" # your own order id makes a retry safe
|
|
37
|
+
)
|
|
38
|
+
puts delivery.id, delivery.status
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The request fields are the API's wire names (`store_id`, `order_value_cents`).
|
|
42
|
+
Pass them as keywords, as one Hash (`create({"store_id" => "..."})`), or both.
|
|
43
|
+
Responses are model objects with a reader per documented field, `#to_h` and
|
|
44
|
+
`#[]` by wire name; a field the API adds later is kept in `#to_h`, never a
|
|
45
|
+
parsing error.
|
|
46
|
+
|
|
47
|
+
Path parameters are positional: `client.deliveries.cancel(delivery_id, reason: "customer_canceled")`.
|
|
48
|
+
Reads take their filters the same way: `client.deliveries.list(status: "pending")`.
|
|
49
|
+
Every operation of the API is a method, named after it in snake_case
|
|
50
|
+
(`deliveries.retrieve`, `stores.cancel_plan`, `ledger.list_for_store`, ...;
|
|
51
|
+
`client.ping` lives on the client itself); the API reference at
|
|
52
|
+
[docs.listo.delivery](https://docs.listo.delivery) lists them all.
|
|
53
|
+
|
|
54
|
+
## Verify a webhook
|
|
55
|
+
|
|
56
|
+
Verification needs the secret, not an API key, so it is also a plain module.
|
|
57
|
+
Pass the body exactly as received (never re-serialized JSON):
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
begin
|
|
61
|
+
event = ListoDelivery::Webhooks.construct_event(
|
|
62
|
+
request.body.read, request.headers["X-Listo-Signature"], ENV.fetch("LISTO_WEBHOOK_SECRET")
|
|
63
|
+
)
|
|
64
|
+
rescue ListoDelivery::Error
|
|
65
|
+
head 400 # invalid_signature or invalid_payload: do not act on it
|
|
66
|
+
else
|
|
67
|
+
case event
|
|
68
|
+
when ListoDelivery::Models::DeliveryStatusChangedEvent then event.data.status
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`construct_event` raises `ListoDelivery::SignatureError` when the signature
|
|
74
|
+
does not verify, and `ListoDelivery::Error` with code `invalid_payload` when the
|
|
75
|
+
signed body is not a JSON object; both are `ListoDelivery::Error`, so a receiver
|
|
76
|
+
that answers 400 rescues `ListoDelivery::Error`. `ListoDelivery::Webhooks.verify(raw_body, signature_header, secrets)`
|
|
77
|
+
returns true or false and never raises. `secrets` is one string or a list (two
|
|
78
|
+
during a rotation); `tolerance:` (default 300 seconds, in both directions) and
|
|
79
|
+
`now:` (unix seconds) are keywords. `client.webhooks.verify` and
|
|
80
|
+
`client.webhooks.construct_event` are the same functions. An event whose
|
|
81
|
+
`type` this SDK does not know comes back as a plain `Models::WebhookEvent`
|
|
82
|
+
(`id`, `type`, `created_at`, and `data` as the raw Hash), never an error.
|
|
83
|
+
|
|
84
|
+
## Errors
|
|
85
|
+
|
|
86
|
+
Every non-2xx answer raises `ListoDelivery::Error` with `code` (branch on this),
|
|
87
|
+
`message`, `param`, `retryable`, `status`, `request_id`, `doc_url` and the
|
|
88
|
+
parsed `body`. A body that is not the API's error envelope (a proxy's HTML
|
|
89
|
+
page) raises the same class with `code` `"http_<status>"`. A request that got
|
|
90
|
+
no answer (DNS, refused, timeout) raises `ListoDelivery::ConnectionError`
|
|
91
|
+
(`code` `"connection_error"`, `status` nil, `retryable` false: a write may
|
|
92
|
+
have happened, so you decide). The API key is never put in an error message.
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
begin
|
|
96
|
+
client.deliveries.retrieve("dlv_missing")
|
|
97
|
+
rescue ListoDelivery::Error => e
|
|
98
|
+
warn "#{e.code} (#{e.status}) request #{e.request_id}"
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Idempotency
|
|
103
|
+
|
|
104
|
+
`deliveries.create`, `deliveries.refund`, `stores.create`, `stores.reactivate_plan`
|
|
105
|
+
and `stores.cancel_plan` (the operations whose schema declares the
|
|
106
|
+
`Idempotency-Key` header) accept `idempotency_key:`. Without one, a fresh UUID
|
|
107
|
+
is sent per call and reused only by that call's own retries. No other
|
|
108
|
+
operation sends the header, and passing `idempotency_key:` to one raises
|
|
109
|
+
`ArgumentError` before any request (a body field really named that goes inside
|
|
110
|
+
the Hash argument). An empty or blank key raises `ArgumentError` too.
|
|
111
|
+
|
|
112
|
+
## Pagination
|
|
113
|
+
|
|
114
|
+
Lists return the page as the API has it: `data`, `has_more` and `cursor`. Pass
|
|
115
|
+
the cursor back to read the next page:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
cursor = nil
|
|
119
|
+
loop do
|
|
120
|
+
page = client.deliveries.list(cursor: cursor)
|
|
121
|
+
page.data.each { |delivery| puts delivery.id }
|
|
122
|
+
break unless page.has_more
|
|
123
|
+
|
|
124
|
+
cursor = page.cursor
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Retries and timeouts
|
|
129
|
+
|
|
130
|
+
Nothing is retried by default. With `max_retries: n`, only a 429 is retried,
|
|
131
|
+
after its `Retry-After` (seconds or an HTTP date, one second when missing,
|
|
132
|
+
capped at 60), at most `n` times, with the same idempotency key. 5xx answers
|
|
133
|
+
and network failures are never retried for you. `timeout:` (seconds, default
|
|
134
|
+
30) bounds connecting, writing and reading.
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
ListoDelivery::Client.new(api_key: key, max_retries: 2, timeout: 10, base_url: "https://api.listo.delivery")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Differences from the other SDKs
|
|
141
|
+
|
|
142
|
+
Ruby spells operations in snake_case, takes
|
|
143
|
+
request fields as keywords or one Hash, and has no top-level `Listo`
|
|
144
|
+
constant (it could clash with an application's own); the client is
|
|
145
|
+
`ListoDelivery::Client`.
|
|
146
|
+
|
|
147
|
+
## About this repository
|
|
148
|
+
|
|
149
|
+
This code is generated from Listo's API definition. Issues and questions are
|
|
150
|
+
welcome in this repository. Changes are made upstream by Listo and released
|
|
151
|
+
here, so a pull request cannot be merged as it stands: open an issue and the
|
|
152
|
+
change is made at the source.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
require "time"
|
|
8
|
+
require "uri"
|
|
9
|
+
|
|
10
|
+
module ListoDelivery
|
|
11
|
+
# The entry point: one Client per API key.
|
|
12
|
+
#
|
|
13
|
+
# client = ListoDelivery::Client.new(api_key: ENV.fetch("LISTO_API_KEY"))
|
|
14
|
+
# client.deliveries.retrieve("dlv_...")
|
|
15
|
+
#
|
|
16
|
+
# Resources (`deliveries`, `stores`, ...) and the root `ping` come from
|
|
17
|
+
# ClientOperations (generated); `client.webhooks` carries both the webhook
|
|
18
|
+
# operations and the signature helpers (`verify`, `construct_event`).
|
|
19
|
+
class Client
|
|
20
|
+
include ClientOperations
|
|
21
|
+
|
|
22
|
+
KEY_PREFIXES = {"lst_live_" => "live", "lst_test_" => "test"}.freeze
|
|
23
|
+
MAX_RETRY_WAIT = 60
|
|
24
|
+
DEFAULT_RETRY_WAIT = 1
|
|
25
|
+
# Transport failures that mean "no answer arrived".
|
|
26
|
+
NETWORK_ERRORS = [
|
|
27
|
+
SocketError, SystemCallError, IOError, Timeout::Error, OpenSSL::SSL::SSLError,
|
|
28
|
+
Net::HTTPBadResponse, Net::ProtocolError
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
# @return [String] "live" or "test", read from the key's prefix
|
|
32
|
+
attr_reader :mode
|
|
33
|
+
attr_reader :base_url, :max_retries, :timeout
|
|
34
|
+
|
|
35
|
+
# @param api_key [String] a `lst_live_` or `lst_test_` key
|
|
36
|
+
# @param base_url [String, nil] defaults to the schema's server
|
|
37
|
+
# @param max_retries [Integer] how many times a 429 is retried (0 = never)
|
|
38
|
+
# @param timeout [Numeric] seconds for connecting, writing and reading
|
|
39
|
+
def initialize(api_key:, base_url: nil, max_retries: 0, timeout: 30)
|
|
40
|
+
@mode = KEY_PREFIXES.find { |prefix, _| api_key.is_a?(String) && api_key.start_with?(prefix) }&.last
|
|
41
|
+
raise ArgumentError, "api_key must start with lst_live_ or lst_test_" unless @mode
|
|
42
|
+
raise ArgumentError, "max_retries must be a non-negative Integer" unless max_retries.is_a?(Integer) && max_retries >= 0
|
|
43
|
+
raise ArgumentError, "timeout must be a positive number" unless timeout.is_a?(Numeric) && timeout.positive?
|
|
44
|
+
|
|
45
|
+
@api_key = api_key
|
|
46
|
+
@base_url = (base_url || DEFAULT_BASE_URL).to_s.chomp("/")
|
|
47
|
+
raise ArgumentError, "base_url must be an http(s) URL" unless URI.parse(@base_url).is_a?(URI::HTTP)
|
|
48
|
+
|
|
49
|
+
@max_retries = max_retries
|
|
50
|
+
@timeout = timeout
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The key never shows up in logs or exception output.
|
|
54
|
+
def inspect
|
|
55
|
+
"#<#{self.class.name} mode=#{mode.inspect} base_url=#{base_url.inspect}>"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Runs one operation. The generated resource methods call this; it is
|
|
59
|
+
# not meant for direct use.
|
|
60
|
+
# @api private
|
|
61
|
+
def execute(http_method, path, query: nil, body: nil, idempotent: false, idempotency_key: nil, model: nil)
|
|
62
|
+
check_idempotency_key(idempotency_key)
|
|
63
|
+
uri = URI.parse("#{base_url}#{path}")
|
|
64
|
+
uri.query = Util.encode_query(query) if query && !query.empty?
|
|
65
|
+
payload = body.nil? ? nil : JSON.generate(body)
|
|
66
|
+
headers = request_headers(payload, idempotent ? (idempotency_key || SecureRandom.uuid) : nil)
|
|
67
|
+
|
|
68
|
+
attempts = 0
|
|
69
|
+
loop do
|
|
70
|
+
response = perform(http_method, uri, headers, payload)
|
|
71
|
+
if response.code.to_i == 429 && attempts < max_retries
|
|
72
|
+
attempts += 1
|
|
73
|
+
sleep(retry_delay(response))
|
|
74
|
+
next
|
|
75
|
+
end
|
|
76
|
+
return handle(response, model)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# nil means "mint one"; a key that is present must be a real one, since
|
|
83
|
+
# an empty header would defeat the retry protection it stands for.
|
|
84
|
+
def check_idempotency_key(key)
|
|
85
|
+
return if key.nil?
|
|
86
|
+
return if key.is_a?(String) && !key.strip.empty?
|
|
87
|
+
|
|
88
|
+
raise ArgumentError, "idempotency_key must be a non-blank String"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def request_headers(payload, idempotency_key)
|
|
92
|
+
agent = "listo-ruby/#{VERSION} api/#{API_VERSION}"
|
|
93
|
+
headers = {
|
|
94
|
+
"Authorization" => "Bearer #{@api_key}",
|
|
95
|
+
"Accept" => "application/json",
|
|
96
|
+
"User-Agent" => agent,
|
|
97
|
+
"X-Listo-Client" => agent
|
|
98
|
+
}
|
|
99
|
+
headers["Content-Type"] = "application/json" if payload
|
|
100
|
+
headers["Idempotency-Key"] = idempotency_key if idempotency_key
|
|
101
|
+
headers
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def perform(http_method, uri, headers, payload)
|
|
105
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
106
|
+
http.use_ssl = uri.scheme == "https"
|
|
107
|
+
http.open_timeout = http.read_timeout = http.write_timeout = timeout
|
|
108
|
+
has_body = !payload.nil? || %w[POST PUT PATCH].include?(http_method)
|
|
109
|
+
request = Net::HTTPGenericRequest.new(http_method, has_body, true, uri.request_uri, headers)
|
|
110
|
+
# An empty body still sends Content-Length: 0, which a POST needs.
|
|
111
|
+
request.body = payload || "" if has_body
|
|
112
|
+
http.start { |conn| conn.request(request) }
|
|
113
|
+
rescue *NETWORK_ERRORS => e
|
|
114
|
+
raise ConnectionError, "Could not reach the Listo API (#{e.class}: #{e.message})"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Retry-After is whole seconds or an HTTP date; anything else waits a second.
|
|
118
|
+
def retry_delay(response)
|
|
119
|
+
value = response["retry-after"].to_s.strip
|
|
120
|
+
seconds =
|
|
121
|
+
if value.match?(/\A\d+\z/)
|
|
122
|
+
value.to_i
|
|
123
|
+
else
|
|
124
|
+
begin
|
|
125
|
+
Time.httpdate(value) - Time.now
|
|
126
|
+
rescue ArgumentError
|
|
127
|
+
DEFAULT_RETRY_WAIT
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
seconds.clamp(0, MAX_RETRY_WAIT)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def handle(response, model)
|
|
134
|
+
status = response.code.to_i
|
|
135
|
+
text = response.body.to_s
|
|
136
|
+
parsed = parse(text)
|
|
137
|
+
raise api_error(response, status, text, parsed) unless status.between?(200, 299)
|
|
138
|
+
return nil if status == 204 || text.strip.empty?
|
|
139
|
+
raise api_error(response, status, text, parsed) if parsed == :invalid
|
|
140
|
+
|
|
141
|
+
model ? model.build(parsed) : parsed
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def parse(text)
|
|
145
|
+
return nil if text.strip.empty?
|
|
146
|
+
|
|
147
|
+
JSON.parse(text)
|
|
148
|
+
rescue JSON::ParserError
|
|
149
|
+
:invalid
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def api_error(response, status, text, parsed)
|
|
153
|
+
header_id = response["x-request-id"]
|
|
154
|
+
envelope = parsed.is_a?(Hash) ? parsed["error"] : nil
|
|
155
|
+
if envelope.is_a?(Hash) && envelope["code"].is_a?(String)
|
|
156
|
+
return Error.new(
|
|
157
|
+
envelope["message"].to_s, code: envelope["code"], param: envelope["param"],
|
|
158
|
+
retryable: envelope["retryable"], status: status, request_id: envelope["request_id"] || header_id,
|
|
159
|
+
doc_url: envelope["doc_url"], body: parsed
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
Error.new(
|
|
164
|
+
"Unexpected response with status #{status}",
|
|
165
|
+
code: "http_#{status}", retryable: [429, 502, 503, 504].include?(status), status: status,
|
|
166
|
+
request_id: header_id, body: (parsed == :invalid || parsed.nil?) ? text : parsed
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ListoDelivery
|
|
4
|
+
# Raised for every non-2xx answer. The fields come from the API's `error`
|
|
5
|
+
# envelope; a body that is not that envelope (a proxy's HTML page, other
|
|
6
|
+
# JSON) still raises this class, with `code` "http_<status>".
|
|
7
|
+
class Error < StandardError
|
|
8
|
+
# @return [String, nil] the stable code to branch on, never `message`
|
|
9
|
+
attr_reader :code
|
|
10
|
+
# @return [String, nil] the request field the error is about
|
|
11
|
+
attr_reader :param
|
|
12
|
+
# @return [Boolean, nil] whether the same call may be repeated
|
|
13
|
+
attr_reader :retryable
|
|
14
|
+
# @return [Integer, nil] the HTTP status; nil when no answer arrived
|
|
15
|
+
attr_reader :status
|
|
16
|
+
# @return [String, nil] quote this when asking for support
|
|
17
|
+
attr_reader :request_id
|
|
18
|
+
# @return [String, nil] the docs page for `code`
|
|
19
|
+
attr_reader :doc_url
|
|
20
|
+
# @return [Object, nil] the parsed response body, or the raw text when it is not JSON
|
|
21
|
+
attr_reader :body
|
|
22
|
+
|
|
23
|
+
def initialize(message = nil, code: nil, param: nil, retryable: nil, status: nil, request_id: nil, doc_url: nil, body: nil)
|
|
24
|
+
super(message)
|
|
25
|
+
@code = code
|
|
26
|
+
@param = param
|
|
27
|
+
@retryable = retryable
|
|
28
|
+
@status = status
|
|
29
|
+
@request_id = request_id
|
|
30
|
+
@doc_url = doc_url
|
|
31
|
+
@body = body
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def retryable?
|
|
35
|
+
retryable == true
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The request never got an answer (DNS, refused, timeout). A write may or
|
|
40
|
+
# may not have happened, so this is never retried for you.
|
|
41
|
+
class ConnectionError < Error
|
|
42
|
+
def initialize(message = nil, **rest)
|
|
43
|
+
super(message, code: "connection_error", retryable: false, **rest)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# A webhook whose signature does not verify (see Webhooks.construct_event).
|
|
48
|
+
class SignatureError < Error
|
|
49
|
+
def initialize(message = "The webhook signature does not match.", **rest)
|
|
50
|
+
super(message, code: "invalid_signature", retryable: false, **rest)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# GENERATED from Listo's API definition (API 1.37.0). Do not edit by hand:
|
|
4
|
+
# this file is rewritten whenever the SDK is regenerated.
|
|
5
|
+
|
|
6
|
+
module ListoDelivery
|
|
7
|
+
# The API release this SDK was generated from (the schema's info.version).
|
|
8
|
+
API_VERSION = "1.37.0"
|
|
9
|
+
|
|
10
|
+
# The schema's first server.
|
|
11
|
+
DEFAULT_BASE_URL = "https://api.listo.delivery"
|
|
12
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# GENERATED from Listo's API definition (API 1.37.0). Do not edit by hand:
|
|
4
|
+
# this file is rewritten whenever the SDK is regenerated.
|
|
5
|
+
|
|
6
|
+
module ListoDelivery
|
|
7
|
+
# The resource readers and the root operations of Client, from Listo's API definition.
|
|
8
|
+
module ClientOperations
|
|
9
|
+
# @return [Resources::Balance]
|
|
10
|
+
def balance
|
|
11
|
+
@balance ||= Resources::Balance.new(self)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @return [Resources::CardOnboarding]
|
|
15
|
+
def card_onboarding
|
|
16
|
+
@card_onboarding ||= Resources::CardOnboarding.new(self)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [Resources::ConnectRequests]
|
|
20
|
+
def connect_requests
|
|
21
|
+
@connect_requests ||= Resources::ConnectRequests.new(self)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Resources::Connections]
|
|
25
|
+
def connections
|
|
26
|
+
@connections ||= Resources::Connections.new(self)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Resources::Deliveries]
|
|
30
|
+
def deliveries
|
|
31
|
+
@deliveries ||= Resources::Deliveries.new(self)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [Resources::Events]
|
|
35
|
+
def events
|
|
36
|
+
@events ||= Resources::Events.new(self)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Resources::Ledger]
|
|
40
|
+
def ledger
|
|
41
|
+
@ledger ||= Resources::Ledger.new(self)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Resources::Quotes]
|
|
45
|
+
def quotes
|
|
46
|
+
@quotes ||= Resources::Quotes.new(self)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Resources::RequestLogs]
|
|
50
|
+
def request_logs
|
|
51
|
+
@request_logs ||= Resources::RequestLogs.new(self)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Resources::Stores]
|
|
55
|
+
def stores
|
|
56
|
+
@stores ||= Resources::Stores.new(self)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [Resources::Webhooks]
|
|
60
|
+
def webhooks
|
|
61
|
+
@webhooks ||= Resources::Webhooks.new(self)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [Resources::Withdrawals]
|
|
65
|
+
def withdrawals
|
|
66
|
+
@withdrawals ||= Resources::Withdrawals.new(self)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Check that your API key works.
|
|
70
|
+
#
|
|
71
|
+
# GET /v1/ping
|
|
72
|
+
# @return [Models::Ping]
|
|
73
|
+
# @raise [ListoDelivery::Error]
|
|
74
|
+
def ping
|
|
75
|
+
execute(
|
|
76
|
+
"GET",
|
|
77
|
+
"/v1/ping",
|
|
78
|
+
model: Models::Ping
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# GENERATED from Listo's API definition (API 1.37.0). Do not edit by hand:
|
|
4
|
+
# this file is rewritten whenever the SDK is regenerated.
|
|
5
|
+
|
|
6
|
+
require_relative "api_version"
|
|
7
|
+
require_relative "models/webhook_event"
|
|
8
|
+
require_relative "models/balance"
|
|
9
|
+
require_relative "models/cancel_delivery_request"
|
|
10
|
+
require_relative "models/card_onboarding_link"
|
|
11
|
+
require_relative "models/card_status"
|
|
12
|
+
require_relative "models/connect_request"
|
|
13
|
+
require_relative "models/courier_ping"
|
|
14
|
+
require_relative "models/create_connect_request_request"
|
|
15
|
+
require_relative "models/create_connection_request"
|
|
16
|
+
require_relative "models/create_delivery_request"
|
|
17
|
+
require_relative "models/create_quote_request"
|
|
18
|
+
require_relative "models/create_store_owner_request"
|
|
19
|
+
require_relative "models/create_store_request"
|
|
20
|
+
require_relative "models/create_withdrawal_request"
|
|
21
|
+
require_relative "models/delivery"
|
|
22
|
+
require_relative "models/delivery_courier_location_event"
|
|
23
|
+
require_relative "models/delivery_list"
|
|
24
|
+
require_relative "models/delivery_message_skipped"
|
|
25
|
+
require_relative "models/delivery_message_skipped_event"
|
|
26
|
+
require_relative "models/delivery_status_changed_event"
|
|
27
|
+
require_relative "models/delivery_updated_event"
|
|
28
|
+
require_relative "models/error"
|
|
29
|
+
require_relative "models/error_response"
|
|
30
|
+
require_relative "models/event_list"
|
|
31
|
+
require_relative "models/ledger_entry"
|
|
32
|
+
require_relative "models/ledger_entry_list"
|
|
33
|
+
require_relative "models/manifest"
|
|
34
|
+
require_relative "models/manifest_item"
|
|
35
|
+
require_relative "models/owner_invitation"
|
|
36
|
+
require_relative "models/ping"
|
|
37
|
+
require_relative "models/quote"
|
|
38
|
+
require_relative "models/refund_delivery_request"
|
|
39
|
+
require_relative "models/request_log"
|
|
40
|
+
require_relative "models/request_log_detail"
|
|
41
|
+
require_relative "models/request_log_list"
|
|
42
|
+
require_relative "models/reschedule_delivery_request"
|
|
43
|
+
require_relative "models/same_day_terms"
|
|
44
|
+
require_relative "models/send_test_webhook_request"
|
|
45
|
+
require_relative "models/store"
|
|
46
|
+
require_relative "models/store_balance"
|
|
47
|
+
require_relative "models/store_connected_event"
|
|
48
|
+
require_relative "models/store_list"
|
|
49
|
+
require_relative "models/store_updated_event"
|
|
50
|
+
require_relative "models/update_store_request"
|
|
51
|
+
require_relative "models/webhook_delivery"
|
|
52
|
+
require_relative "models/webhook_delivery_list"
|
|
53
|
+
require_relative "models/webhook_event_envelope"
|
|
54
|
+
require_relative "models/withdrawal"
|
|
55
|
+
require_relative "models/withdrawal_list"
|
|
56
|
+
require_relative "models/withdrawal_updated_event"
|
|
57
|
+
require_relative "resources/balance"
|
|
58
|
+
require_relative "resources/card_onboarding"
|
|
59
|
+
require_relative "resources/connect_requests"
|
|
60
|
+
require_relative "resources/connections"
|
|
61
|
+
require_relative "resources/deliveries"
|
|
62
|
+
require_relative "resources/events"
|
|
63
|
+
require_relative "resources/ledger"
|
|
64
|
+
require_relative "resources/quotes"
|
|
65
|
+
require_relative "resources/request_logs"
|
|
66
|
+
require_relative "resources/stores"
|
|
67
|
+
require_relative "resources/webhooks"
|
|
68
|
+
require_relative "resources/withdrawals"
|
|
69
|
+
require_relative "client_operations"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# GENERATED from Listo's API definition (API 1.37.0). Do not edit by hand:
|
|
4
|
+
# this file is rewritten whenever the SDK is regenerated.
|
|
5
|
+
|
|
6
|
+
module ListoDelivery
|
|
7
|
+
module Models
|
|
8
|
+
# Your prepaid balance for the world the key acts in.
|
|
9
|
+
class Balance < Model
|
|
10
|
+
TYPES = {
|
|
11
|
+
"balance_cents" => nil,
|
|
12
|
+
"available_cents" => nil,
|
|
13
|
+
"currency" => nil,
|
|
14
|
+
"same_day" => ["model", "SameDayTerms"],
|
|
15
|
+
"in_review_cents" => nil
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
# Everything in the account, in centavos.
|
|
19
|
+
# @return [Integer]
|
|
20
|
+
def balance_cents
|
|
21
|
+
read("balance_cents")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# What you can withdraw today.
|
|
25
|
+
# @return [Integer]
|
|
26
|
+
def available_cents
|
|
27
|
+
read("available_cents")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# One of: "MXN".
|
|
31
|
+
# @return [String]
|
|
32
|
+
def currency
|
|
33
|
+
read("currency")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Models::SameDayTerms]
|
|
37
|
+
def same_day
|
|
38
|
+
read("same_day")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The part of `balance_cents` that is cash from an order we are still reviewing.
|
|
42
|
+
# @return [Integer]
|
|
43
|
+
def in_review_cents
|
|
44
|
+
read("in_review_cents")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# GENERATED from Listo's API definition (API 1.37.0). Do not edit by hand:
|
|
4
|
+
# this file is rewritten whenever the SDK is regenerated.
|
|
5
|
+
|
|
6
|
+
module ListoDelivery
|
|
7
|
+
module Models
|
|
8
|
+
# Why the order is being canceled.
|
|
9
|
+
class CancelDeliveryRequest < Model
|
|
10
|
+
TYPES = {
|
|
11
|
+
"reason" => nil,
|
|
12
|
+
"description" => nil
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
# Why the order is canceled.
|
|
16
|
+
# One of: "customer_canceled", "store_closed", "item_unavailable", "wrong_address", "no_longer_needed", "other".
|
|
17
|
+
# @return [String]
|
|
18
|
+
def reason
|
|
19
|
+
read("reason")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# A sentence of explanation.
|
|
23
|
+
# @return [String, nil]
|
|
24
|
+
def description
|
|
25
|
+
read("description")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# GENERATED from Listo's API definition (API 1.37.0). Do not edit by hand:
|
|
4
|
+
# this file is rewritten whenever the SDK is regenerated.
|
|
5
|
+
|
|
6
|
+
module ListoDelivery
|
|
7
|
+
module Models
|
|
8
|
+
# A one-time hosted link to complete card onboarding.
|
|
9
|
+
class CardOnboardingLink < Model
|
|
10
|
+
TYPES = {
|
|
11
|
+
"url" => nil
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
# Send this to whoever can answer for the business.
|
|
15
|
+
# @return [String]
|
|
16
|
+
def url
|
|
17
|
+
read("url")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|