exporto_api 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 +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +189 -0
- data/lib/exporto_api/auth_client.rb +41 -0
- data/lib/exporto_api/client.rb +51 -0
- data/lib/exporto_api/errors.rb +27 -0
- data/lib/exporto_api/object.rb +71 -0
- data/lib/exporto_api/objects/label_method_response.rb +7 -0
- data/lib/exporto_api/objects/label_response.rb +7 -0
- data/lib/exporto_api/objects/return_shipment_response.rb +7 -0
- data/lib/exporto_api/objects/shipment_response.rb +7 -0
- data/lib/exporto_api/objects/token_response.rb +7 -0
- data/lib/exporto_api/resource.rb +94 -0
- data/lib/exporto_api/resources/auth_resource.rb +12 -0
- data/lib/exporto_api/resources/label_method_resource.rb +9 -0
- data/lib/exporto_api/resources/label_resource.rb +9 -0
- data/lib/exporto_api/resources/return_shipment_resource.rb +24 -0
- data/lib/exporto_api/resources/shipment_resource.rb +33 -0
- data/lib/exporto_api/version.rb +5 -0
- data/lib/exporto_api.rb +30 -0
- metadata +165 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 18c37f30a8751a2ca78301ecb299f4b3caf58958442d8d66b03bc6d2f62aa796
|
|
4
|
+
data.tar.gz: 1919fdc84b42b1b74542c61778fe9ccd82339bc7294afa77e0f0b7ca3c9d1b14
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 583ed97d21592c273c8e9ccb1e8f95634a0e1dfac4ab6c0c7f71e5d2b28c19071ccfe1a7c60a96fb2ab58ab3a0dee3e433ce83d2e59c2c575e1a2c56caa66a18
|
|
7
|
+
data.tar.gz: 97c0adbe8af0faf45090f10ed227204da9d37b945593b7cca964d098ba0f40bfeafa9ad9fa6214cbe8f30164410337ff70f0d27824721086729620be945b7de3
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-09-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Rails-independent live and staging clients with injectable Faraday adapters.
|
|
10
|
+
- OAuth client-credentials authentication and Bearer-authenticated API requests.
|
|
11
|
+
- Snake-case response objects with deeply frozen access to the original provider response.
|
|
12
|
+
- Typed authentication, validation, not-found, rate-limit, server, and transport errors with safe metadata.
|
|
13
|
+
- Label-method discovery through `client.label_method.all`.
|
|
14
|
+
- Shipment search and retrieval through `client.shipment.search` and `client.shipment.find`.
|
|
15
|
+
- Label creation through `client.label.create` without automatic mutation retries.
|
|
16
|
+
- Return-shipment registration through `client.return_shipment.create`, returning a typed success response for every `2xx` response.
|
|
17
|
+
|
|
18
|
+
### Out of scope
|
|
19
|
+
|
|
20
|
+
- Token caching, expiry buffers, refresh, and concurrency policy remain caller responsibilities.
|
|
21
|
+
- Shipment deletion and carrier-label cancellation are not part of the initial public API.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PostCo
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# ExportoAPI
|
|
2
|
+
|
|
3
|
+
Rails-independent Ruby client for the Exporto API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "exporto_api", "~> 0.1.0"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run `bundle install`.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### Initialize clients
|
|
18
|
+
|
|
19
|
+
Exporto uses a separate OAuth client to issue an access token. Set the environment once and pass the same `sandbox:` value to both clients so the token is used only in the environment that issued it.
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require "exporto_api"
|
|
23
|
+
|
|
24
|
+
sandbox = true # Use Exporto's staging environment
|
|
25
|
+
|
|
26
|
+
auth_client = ExportoAPI::AuthClient.new(
|
|
27
|
+
username: "EXPORTO_USERNAME",
|
|
28
|
+
password: "EXPORTO_PASSWORD",
|
|
29
|
+
sandbox: sandbox
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
token = auth_client.token
|
|
33
|
+
|
|
34
|
+
client = ExportoAPI::Client.new(
|
|
35
|
+
access_token: token.access_token,
|
|
36
|
+
sandbox: sandbox
|
|
37
|
+
)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`AuthClient#token` also accepts an optional space-delimited `scope:` string. Its response exposes `access_token`, `token_type`, `expires_in`, and `scope`.
|
|
41
|
+
|
|
42
|
+
The gem does not cache or refresh tokens. The caller owns token caching by Exporto account, environment, and requested scope, using the returned `expires_in` value and an application-defined safety buffer.
|
|
43
|
+
|
|
44
|
+
### List label methods
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
label_methods = client.label_method.all
|
|
48
|
+
|
|
49
|
+
label_methods.each do |method|
|
|
50
|
+
puts [method.method_id, method.name, method.direction, method.carrier_name]
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The gem returns every method available to the authenticated account. The caller is responsible for selecting an approved method.
|
|
55
|
+
|
|
56
|
+
### Search shipments
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
shipments = client.shipment.search(
|
|
60
|
+
type: "outbound",
|
|
61
|
+
foreign_outbound_tracking_id: "OUTBOUND-TRACKING-ID",
|
|
62
|
+
page: 0,
|
|
63
|
+
page_size: 50
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Shipment search also accepts `foreign_inbound_tracking_id`, `processed_at`, `carrier_received_at_updated_at`, and `carrier_delivered_at_updated_at`.
|
|
68
|
+
|
|
69
|
+
### Retrieve a shipment
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
shipment = client.shipment.find(shipment_id: "EXPORTO-SHIPMENT-ID")
|
|
73
|
+
|
|
74
|
+
puts shipment.shipment_id
|
|
75
|
+
puts shipment.order_id
|
|
76
|
+
puts shipment.status
|
|
77
|
+
puts shipment.carrier_state
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Create a label
|
|
81
|
+
|
|
82
|
+
Pass an Exporto-shaped payload to `client.label.create`:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
label = client.label.create(
|
|
86
|
+
"order" => {"customerFacingId" => "ORDER-123"},
|
|
87
|
+
"product" => {
|
|
88
|
+
"methodId" => 123,
|
|
89
|
+
"format" => "PDF"
|
|
90
|
+
},
|
|
91
|
+
"package" => {
|
|
92
|
+
"weight" => 1_000,
|
|
93
|
+
"reference" => "RETURN-123"
|
|
94
|
+
},
|
|
95
|
+
"address" => {
|
|
96
|
+
"name" => "Return Sender",
|
|
97
|
+
"line1" => "1 Example Street",
|
|
98
|
+
"city" => "London",
|
|
99
|
+
"postCode" => "SW1A 1AA",
|
|
100
|
+
"countryCode" => "GB",
|
|
101
|
+
"email" => "sender@example.com"
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
puts label.carrier_name
|
|
106
|
+
puts label.tracking_code
|
|
107
|
+
puts label.tracking_url
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
For an inbound label, `address` is the return sender's address. The account-specific `methodId` determines the carrier and shipment direction.
|
|
111
|
+
|
|
112
|
+
### Register a return shipment
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
registration = client.return_shipment.create(
|
|
116
|
+
order_id: "EXPORTO-ORDER-ID",
|
|
117
|
+
shipment_id: "POSTCO-GENERATED-SHIPMENT-ID",
|
|
118
|
+
foreign_inbound_tracking_id: label.tracking_code
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
registration.success # => true
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Return-shipment registration maps its Ruby keyword arguments to Exporto's request body for `POST /order/return-shipment`. Provide exactly one of `order_id` or `customer_facing_id`, together with `shipment_id` and `foreign_inbound_tracking_id`. Because Exporto returns no response body for this operation, a successful `2xx` response returns an `ExportoAPI::Objects::ReturnShipmentResponse` with `success: true`; unsuccessful responses raise the corresponding `ExportoAPI` error.
|
|
125
|
+
|
|
126
|
+
The gem does not automatically retry mutating requests. The caller owns idempotency, persistence, retry, and reconciliation policy.
|
|
127
|
+
|
|
128
|
+
### Response objects
|
|
129
|
+
|
|
130
|
+
Response objects expose Exporto's camel-case keys through snake-case Ruby methods, including nested hashes and arrays. The original provider response remains available as a deeply frozen snapshot through `raw`.
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
shipment.shipment_id
|
|
134
|
+
shipment.line_items.first.article_id
|
|
135
|
+
shipment.raw
|
|
136
|
+
shipment.raw.frozen? # => true
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Error handling
|
|
140
|
+
|
|
141
|
+
HTTP failures raise typed subclasses of `ExportoAPI::Error`:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
begin
|
|
145
|
+
client.shipment.find(shipment_id: "EXPORTO-SHIPMENT-ID")
|
|
146
|
+
rescue ExportoAPI::AuthenticationError => error
|
|
147
|
+
# 401/403 responses
|
|
148
|
+
puts error.message
|
|
149
|
+
rescue ExportoAPI::ValidationError => error
|
|
150
|
+
# 400 responses
|
|
151
|
+
puts error.message
|
|
152
|
+
rescue ExportoAPI::NotFoundError => error
|
|
153
|
+
# 404 responses
|
|
154
|
+
puts error.message
|
|
155
|
+
rescue ExportoAPI::RateLimitError => error
|
|
156
|
+
# 429 responses
|
|
157
|
+
puts error.retry_after
|
|
158
|
+
rescue ExportoAPI::ServerError => error
|
|
159
|
+
# 500-599 responses
|
|
160
|
+
puts error.message
|
|
161
|
+
rescue ExportoAPI::APIError => error
|
|
162
|
+
# Other HTTP and transport failures
|
|
163
|
+
puts error.message
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Errors retain safe metadata where available through `status_code`, `request_id`, and `retry_after`. Transport failures retain the original Faraday exception as their cause.
|
|
168
|
+
|
|
169
|
+
### Sandbox mode
|
|
170
|
+
|
|
171
|
+
Both clients use Exporto's live environment by default. Set `sandbox: true` for staging, and always use the same value for token creation and authenticated requests:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
sandbox = false # Live: https://api.exporto.de/v1/
|
|
175
|
+
# sandbox = true # Staging: https://staging.api.exporto.de/v1/
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Development
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
bundle install
|
|
182
|
+
bundle exec rspec
|
|
183
|
+
bundle exec standardrb
|
|
184
|
+
gem build exporto_api.gemspec
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT License. See [LICENSE.txt](LICENSE.txt).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
|
|
5
|
+
module ExportoAPI
|
|
6
|
+
class AuthClient
|
|
7
|
+
attr_reader :username, :password, :adapter
|
|
8
|
+
|
|
9
|
+
def initialize(username:, password:, sandbox: false, adapter: Faraday.default_adapter)
|
|
10
|
+
@username = username
|
|
11
|
+
@password = password
|
|
12
|
+
@sandbox = sandbox
|
|
13
|
+
@adapter = adapter
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def token(scope: nil)
|
|
17
|
+
auth.token(scope: scope)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def connection
|
|
21
|
+
@connection ||= Faraday.new do |connection|
|
|
22
|
+
connection.url_prefix = sandbox? ? Client::TEST_BASE_URL : Client::LIVE_BASE_URL
|
|
23
|
+
connection.headers["Accept"] = "application/json"
|
|
24
|
+
connection.request :authorization, :basic, username, password
|
|
25
|
+
connection.request :json
|
|
26
|
+
connection.response :json, content_type: /\bjson/
|
|
27
|
+
connection.adapter adapter
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def auth
|
|
34
|
+
@auth ||= AuthResource.new(self)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def sandbox?
|
|
38
|
+
@sandbox
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
|
|
5
|
+
module ExportoAPI
|
|
6
|
+
class Client
|
|
7
|
+
LIVE_BASE_URL = "https://api.exporto.de/v1/"
|
|
8
|
+
TEST_BASE_URL = "https://staging.api.exporto.de/v1/"
|
|
9
|
+
|
|
10
|
+
attr_reader :access_token, :adapter
|
|
11
|
+
|
|
12
|
+
def initialize(access_token:, sandbox: false, adapter: Faraday.default_adapter)
|
|
13
|
+
@access_token = access_token
|
|
14
|
+
@sandbox = sandbox
|
|
15
|
+
@adapter = adapter
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def label_method
|
|
19
|
+
@label_method ||= LabelMethodResource.new(self)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def shipment
|
|
23
|
+
@shipment ||= ShipmentResource.new(self)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def connection
|
|
27
|
+
@connection ||= Faraday.new do |connection|
|
|
28
|
+
connection.url_prefix = sandbox? ? TEST_BASE_URL : LIVE_BASE_URL
|
|
29
|
+
connection.headers["Authorization"] = "Bearer #{access_token}"
|
|
30
|
+
connection.headers["Accept"] = "application/json"
|
|
31
|
+
connection.request :json
|
|
32
|
+
connection.response :json, content_type: /\bjson/
|
|
33
|
+
connection.adapter adapter
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def label
|
|
38
|
+
@label ||= LabelResource.new(self)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def return_shipment
|
|
42
|
+
@return_shipment ||= ReturnShipmentResource.new(self)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def sandbox?
|
|
48
|
+
@sandbox
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ExportoAPI
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
attr_reader :response, :status_code, :request_id, :retry_after
|
|
6
|
+
|
|
7
|
+
def initialize(message = nil, response: nil, status_code: nil, request_id: nil, retry_after: nil)
|
|
8
|
+
super(message)
|
|
9
|
+
@response = response
|
|
10
|
+
@status_code = status_code
|
|
11
|
+
@request_id = request_id
|
|
12
|
+
@retry_after = retry_after
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class APIError < Error; end
|
|
17
|
+
|
|
18
|
+
class AuthenticationError < Error; end
|
|
19
|
+
|
|
20
|
+
class ValidationError < Error; end
|
|
21
|
+
|
|
22
|
+
class NotFoundError < Error; end
|
|
23
|
+
|
|
24
|
+
class RateLimitError < Error; end
|
|
25
|
+
|
|
26
|
+
class ServerError < Error; end
|
|
27
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ostruct"
|
|
4
|
+
require "active_support/core_ext/string/inflections"
|
|
5
|
+
|
|
6
|
+
module ExportoAPI
|
|
7
|
+
class Base < OpenStruct
|
|
8
|
+
attr_reader :original_response
|
|
9
|
+
|
|
10
|
+
def self.new(attributes)
|
|
11
|
+
return attributes.map { |item| super(item) } if attributes.is_a?(Array)
|
|
12
|
+
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(attributes)
|
|
17
|
+
@original_response = deep_freeze(attributes)
|
|
18
|
+
super(to_ostruct(attributes))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_hash
|
|
22
|
+
ostruct_to_hash(self)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
alias_method :to_h, :to_hash
|
|
26
|
+
|
|
27
|
+
def raw
|
|
28
|
+
@original_response
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def to_ostruct(object)
|
|
34
|
+
case object
|
|
35
|
+
when Hash
|
|
36
|
+
OpenStruct.new(
|
|
37
|
+
object.transform_keys { |key| key.to_s.underscore }
|
|
38
|
+
.transform_values { |value| to_ostruct(value) }
|
|
39
|
+
)
|
|
40
|
+
when Array
|
|
41
|
+
object.map { |value| to_ostruct(value) }
|
|
42
|
+
else
|
|
43
|
+
object
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def deep_freeze(object)
|
|
48
|
+
case object
|
|
49
|
+
when Hash then object.transform_values { |value| deep_freeze(value) }.freeze
|
|
50
|
+
when Array then object.map { |item| deep_freeze(item) }.freeze
|
|
51
|
+
else object.respond_to?(:freeze) ? object.freeze : object
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ostruct_to_hash(object)
|
|
56
|
+
case object
|
|
57
|
+
when OpenStruct
|
|
58
|
+
object.each_pair.to_h
|
|
59
|
+
.transform_keys(&:to_s)
|
|
60
|
+
.transform_values { |value| ostruct_to_hash(value) }
|
|
61
|
+
when Array
|
|
62
|
+
object.map { |value| ostruct_to_hash(value) }
|
|
63
|
+
when Hash
|
|
64
|
+
object.transform_keys(&:to_s)
|
|
65
|
+
.transform_values { |value| ostruct_to_hash(value) }
|
|
66
|
+
else
|
|
67
|
+
object
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module ExportoAPI
|
|
6
|
+
class Resource
|
|
7
|
+
attr_reader :client
|
|
8
|
+
|
|
9
|
+
def initialize(client)
|
|
10
|
+
@client = client
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def get_request(path, params: {}, headers: {})
|
|
16
|
+
handle_response(client.connection.get(path, params, headers))
|
|
17
|
+
rescue Faraday::Error => error
|
|
18
|
+
raise_transport_error(error)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def post_request(path, body:, headers: {})
|
|
22
|
+
handle_response(client.connection.post(path, body, headers))
|
|
23
|
+
rescue Faraday::Error => error
|
|
24
|
+
raise_transport_error(error)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def handle_response(response)
|
|
28
|
+
body = parse_body(response.body)
|
|
29
|
+
return body if response.status.between?(200, 299)
|
|
30
|
+
|
|
31
|
+
error_class, prefix = error_mapping(response.status)
|
|
32
|
+
message = "#{prefix} (HTTP #{response.status}): #{extract_error_message(body)}"
|
|
33
|
+
raise error_class.new(
|
|
34
|
+
message,
|
|
35
|
+
response: response,
|
|
36
|
+
status_code: response.status,
|
|
37
|
+
request_id: extract_request_id(response, body),
|
|
38
|
+
retry_after: extract_retry_after(response)
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def parse_body(body)
|
|
43
|
+
return body unless body.is_a?(String)
|
|
44
|
+
|
|
45
|
+
JSON.parse(body)
|
|
46
|
+
rescue JSON::ParserError
|
|
47
|
+
body
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def extract_error_message(body)
|
|
51
|
+
case body
|
|
52
|
+
when Hash
|
|
53
|
+
[body["message"], body[:message], body["error"], body[:error]]
|
|
54
|
+
.find { |value| value.is_a?(String) } || "Unknown error"
|
|
55
|
+
when String then body
|
|
56
|
+
else "Unknown error"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def error_mapping(status)
|
|
61
|
+
case status
|
|
62
|
+
when 400 then [ValidationError, "Bad request"]
|
|
63
|
+
when 401, 403 then [AuthenticationError, "Authentication failed"]
|
|
64
|
+
when 404 then [NotFoundError, "Resource not found"]
|
|
65
|
+
when 429 then [RateLimitError, "Rate limited"]
|
|
66
|
+
when 500..599 then [ServerError, "Server error"]
|
|
67
|
+
else [APIError, "API error"]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def extract_request_id(response, body)
|
|
72
|
+
response.headers["x-request-id"] || request_id_from(body)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def request_id_from(body)
|
|
76
|
+
return unless body.is_a?(Hash)
|
|
77
|
+
|
|
78
|
+
body["requestId"] || body[:requestId] || body["request_id"] || body[:request_id]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def extract_retry_after(response)
|
|
82
|
+
Integer(response.headers["retry-after"], exception: false)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def raise_transport_error(error)
|
|
86
|
+
wrapped_error = APIError.new(
|
|
87
|
+
"Network request failed",
|
|
88
|
+
response: error.response,
|
|
89
|
+
status_code: error.response_status
|
|
90
|
+
)
|
|
91
|
+
raise wrapped_error, cause: error
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ExportoAPI
|
|
4
|
+
class AuthResource < Resource
|
|
5
|
+
def token(scope: nil)
|
|
6
|
+
body = {"grant_type" => "client_credentials"}
|
|
7
|
+
body["scope"] = scope unless scope.nil?
|
|
8
|
+
|
|
9
|
+
Objects::TokenResponse.new(post_request("auth/token", body: body))
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ExportoAPI
|
|
4
|
+
class ReturnShipmentResource < Resource
|
|
5
|
+
def create(
|
|
6
|
+
shipment_id:,
|
|
7
|
+
foreign_inbound_tracking_id:,
|
|
8
|
+
order_id: nil,
|
|
9
|
+
customer_facing_id: nil
|
|
10
|
+
)
|
|
11
|
+
body = {
|
|
12
|
+
"orderId" => order_id,
|
|
13
|
+
"customerFacingId" => customer_facing_id,
|
|
14
|
+
"shipmentId" => shipment_id,
|
|
15
|
+
"foreignInboundTrackingId" => foreign_inbound_tracking_id
|
|
16
|
+
}.compact
|
|
17
|
+
|
|
18
|
+
post_request("order/return-shipment", body: body)
|
|
19
|
+
|
|
20
|
+
# Exporto returns no response body for a successful return-shipment registration.
|
|
21
|
+
Objects::ReturnShipmentResponse.new(success: true)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ExportoAPI
|
|
4
|
+
class ShipmentResource < Resource
|
|
5
|
+
def search(
|
|
6
|
+
foreign_outbound_tracking_id: nil,
|
|
7
|
+
foreign_inbound_tracking_id: nil,
|
|
8
|
+
processed_at: nil,
|
|
9
|
+
carrier_received_at_updated_at: nil,
|
|
10
|
+
carrier_delivered_at_updated_at: nil,
|
|
11
|
+
page: nil,
|
|
12
|
+
page_size: nil,
|
|
13
|
+
type: nil
|
|
14
|
+
)
|
|
15
|
+
params = {
|
|
16
|
+
"foreignOutboundTrackingId" => foreign_outbound_tracking_id,
|
|
17
|
+
"foreignInboundTrackingId" => foreign_inbound_tracking_id,
|
|
18
|
+
"processedAt" => processed_at,
|
|
19
|
+
"carrierReceivedAtUpdatedAt" => carrier_received_at_updated_at,
|
|
20
|
+
"carrierDeliveredAtUpdatedAt" => carrier_delivered_at_updated_at,
|
|
21
|
+
"page" => page,
|
|
22
|
+
"pageSize" => page_size,
|
|
23
|
+
"type" => type
|
|
24
|
+
}.compact
|
|
25
|
+
|
|
26
|
+
Objects::ShipmentResponse.new(get_request("shipment/search", params: params))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def find(shipment_id:)
|
|
30
|
+
Objects::ShipmentResponse.new(get_request("shipment/#{shipment_id}"))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/exporto_api.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "exporto_api/version"
|
|
4
|
+
|
|
5
|
+
module ExportoAPI
|
|
6
|
+
autoload :AuthClient, "exporto_api/auth_client"
|
|
7
|
+
autoload :Client, "exporto_api/client"
|
|
8
|
+
autoload :Base, "exporto_api/object"
|
|
9
|
+
autoload :Resource, "exporto_api/resource"
|
|
10
|
+
autoload :Error, "exporto_api/errors"
|
|
11
|
+
autoload :APIError, "exporto_api/errors"
|
|
12
|
+
autoload :AuthenticationError, "exporto_api/errors"
|
|
13
|
+
autoload :ValidationError, "exporto_api/errors"
|
|
14
|
+
autoload :NotFoundError, "exporto_api/errors"
|
|
15
|
+
autoload :RateLimitError, "exporto_api/errors"
|
|
16
|
+
autoload :ServerError, "exporto_api/errors"
|
|
17
|
+
autoload :AuthResource, "exporto_api/resources/auth_resource"
|
|
18
|
+
autoload :LabelResource, "exporto_api/resources/label_resource"
|
|
19
|
+
autoload :LabelMethodResource, "exporto_api/resources/label_method_resource"
|
|
20
|
+
autoload :ReturnShipmentResource, "exporto_api/resources/return_shipment_resource"
|
|
21
|
+
autoload :ShipmentResource, "exporto_api/resources/shipment_resource"
|
|
22
|
+
|
|
23
|
+
module Objects
|
|
24
|
+
autoload :LabelResponse, "exporto_api/objects/label_response"
|
|
25
|
+
autoload :LabelMethodResponse, "exporto_api/objects/label_method_response"
|
|
26
|
+
autoload :ReturnShipmentResponse, "exporto_api/objects/return_shipment_response"
|
|
27
|
+
autoload :ShipmentResponse, "exporto_api/objects/shipment_response"
|
|
28
|
+
autoload :TokenResponse, "exporto_api/objects/token_response"
|
|
29
|
+
end
|
|
30
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: exporto_api
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- PostCo
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: faraday
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: faraday-net_http
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: activesupport
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '7.0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '7.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rake
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rspec
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: webmock
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '3.0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '3.0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: standard
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
111
|
+
description: A small Ruby client foundation for integrating with Exporto without depending
|
|
112
|
+
on Rails.
|
|
113
|
+
email:
|
|
114
|
+
- engineering@postco.co
|
|
115
|
+
executables: []
|
|
116
|
+
extensions: []
|
|
117
|
+
extra_rdoc_files: []
|
|
118
|
+
files:
|
|
119
|
+
- CHANGELOG.md
|
|
120
|
+
- LICENSE.txt
|
|
121
|
+
- README.md
|
|
122
|
+
- lib/exporto_api.rb
|
|
123
|
+
- lib/exporto_api/auth_client.rb
|
|
124
|
+
- lib/exporto_api/client.rb
|
|
125
|
+
- lib/exporto_api/errors.rb
|
|
126
|
+
- lib/exporto_api/object.rb
|
|
127
|
+
- lib/exporto_api/objects/label_method_response.rb
|
|
128
|
+
- lib/exporto_api/objects/label_response.rb
|
|
129
|
+
- lib/exporto_api/objects/return_shipment_response.rb
|
|
130
|
+
- lib/exporto_api/objects/shipment_response.rb
|
|
131
|
+
- lib/exporto_api/objects/token_response.rb
|
|
132
|
+
- lib/exporto_api/resource.rb
|
|
133
|
+
- lib/exporto_api/resources/auth_resource.rb
|
|
134
|
+
- lib/exporto_api/resources/label_method_resource.rb
|
|
135
|
+
- lib/exporto_api/resources/label_resource.rb
|
|
136
|
+
- lib/exporto_api/resources/return_shipment_resource.rb
|
|
137
|
+
- lib/exporto_api/resources/shipment_resource.rb
|
|
138
|
+
- lib/exporto_api/version.rb
|
|
139
|
+
homepage: https://github.com/PostCo/exporto_api
|
|
140
|
+
licenses:
|
|
141
|
+
- MIT
|
|
142
|
+
metadata:
|
|
143
|
+
source_code_uri: https://github.com/PostCo/exporto_api
|
|
144
|
+
changelog_uri: https://github.com/PostCo/exporto_api/blob/main/CHANGELOG.md
|
|
145
|
+
rubygems_mfa_required: 'true'
|
|
146
|
+
post_install_message:
|
|
147
|
+
rdoc_options: []
|
|
148
|
+
require_paths:
|
|
149
|
+
- lib
|
|
150
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
151
|
+
requirements:
|
|
152
|
+
- - ">="
|
|
153
|
+
- !ruby/object:Gem::Version
|
|
154
|
+
version: 3.3.0
|
|
155
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
156
|
+
requirements:
|
|
157
|
+
- - ">="
|
|
158
|
+
- !ruby/object:Gem::Version
|
|
159
|
+
version: '0'
|
|
160
|
+
requirements: []
|
|
161
|
+
rubygems_version: 3.5.22
|
|
162
|
+
signing_key:
|
|
163
|
+
specification_version: 4
|
|
164
|
+
summary: Rails-independent Ruby client for the Exporto API
|
|
165
|
+
test_files: []
|