flexops 1.0.2 → 2.0.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 +4 -4
- data/README.md +235 -61
- data/lib/flexops/client.rb +6 -7
- data/lib/flexops/errors.rb +9 -0
- data/lib/flexops/hazmat_classification.rb +34 -0
- data/lib/flexops/http.rb +78 -45
- data/lib/flexops/resources/auth.rb +8 -29
- data/lib/flexops/resources/carbon_offsets.rb +60 -0
- data/lib/flexops/resources/carriers.rb +95 -8
- data/lib/flexops/resources/email_templates.rb +27 -17
- data/lib/flexops/resources/hs_codes.rb +10 -11
- data/lib/flexops/resources/insurance.rb +19 -9
- data/lib/flexops/resources/inventory.rb +0 -8
- data/lib/flexops/resources/orders.rb +0 -8
- data/lib/flexops/resources/pickups.rb +14 -5
- data/lib/flexops/resources/recurring_shipments.rb +30 -15
- data/lib/flexops/resources/report_schedules.rb +63 -0
- data/lib/flexops/resources/returns.rb +37 -23
- data/lib/flexops/resources/rules.rb +18 -2
- data/lib/flexops/resources/shipping.rb +51 -10
- data/lib/flexops/resources/wallet.rb +4 -20
- data/lib/flexops/resources/webhooks.rb +132 -9
- data/lib/flexops/resources/workspaces.rb +8 -13
- data/lib/flexops/version.rb +1 -1
- data/lib/flexops/webhook_event_type.rb +25 -0
- data/lib/flexops/workspace_role.rb +32 -0
- data/lib/flexops.rb +5 -3
- metadata +7 -5
- data/lib/flexops/resources/api_keys.rb +0 -40
- data/lib/flexops/resources/offsets.rb +0 -38
- data/lib/flexops/resources/reports.rb +0 -46
data/lib/flexops/http.rb
CHANGED
|
@@ -9,13 +9,24 @@
|
|
|
9
9
|
require "net/http"
|
|
10
10
|
require "uri"
|
|
11
11
|
require "json"
|
|
12
|
+
require "securerandom"
|
|
12
13
|
|
|
13
14
|
module FlexOps
|
|
14
15
|
class HttpClient
|
|
15
16
|
DEFAULT_BASE_URL = "https://gateway.flexops.io"
|
|
16
17
|
DEFAULT_TIMEOUT = 30
|
|
17
18
|
MAX_RETRIES = 3
|
|
19
|
+
# Statuses a GET is retried on.
|
|
18
20
|
RETRYABLE_STATUSES = [429, 500, 502, 503, 504].freeze
|
|
21
|
+
# Statuses where a keyed write either never ran or is replayed by the Gateway. Never 500:
|
|
22
|
+
# the Gateway does not cache a 5xx response, so a retry would run the write again.
|
|
23
|
+
WRITE_RETRYABLE_STATUSES = [429, 502, 503, 504].freeze
|
|
24
|
+
WRITE_METHODS = %i[post put patch].freeze
|
|
25
|
+
IDEMPOTENCY_KEY_HEADER = "Idempotency-Key"
|
|
26
|
+
# The Gateway's 409 while the first request with the same Idempotency-Key is still running.
|
|
27
|
+
IDEMPOTENCY_CONFLICT = "IDEMPOTENCY_CONFLICT"
|
|
28
|
+
|
|
29
|
+
attr_reader :access_token
|
|
19
30
|
|
|
20
31
|
def initialize(base_url: DEFAULT_BASE_URL, api_key: nil, access_token: nil, timeout: DEFAULT_TIMEOUT)
|
|
21
32
|
@base_url = base_url.chomp("/")
|
|
@@ -34,30 +45,33 @@ module FlexOps
|
|
|
34
45
|
@access_token = nil
|
|
35
46
|
end
|
|
36
47
|
|
|
37
|
-
def get(path, query: nil)
|
|
38
|
-
request(:get, path, query: query)
|
|
48
|
+
def get(path, query: nil, headers: {})
|
|
49
|
+
request(:get, path, query: query, headers: headers)
|
|
39
50
|
end
|
|
40
51
|
|
|
41
|
-
def post(path, body: nil, query: nil)
|
|
42
|
-
request(:post, path, body: body, query: query)
|
|
52
|
+
def post(path, body: nil, query: nil, headers: {})
|
|
53
|
+
request(:post, path, body: body, query: query, headers: headers)
|
|
43
54
|
end
|
|
44
55
|
|
|
45
|
-
def put(path, body: nil)
|
|
46
|
-
request(:put, path, body: body)
|
|
56
|
+
def put(path, body: nil, headers: {})
|
|
57
|
+
request(:put, path, body: body, headers: headers)
|
|
47
58
|
end
|
|
48
59
|
|
|
49
|
-
def patch(path, body: nil)
|
|
50
|
-
request(:patch, path, body: body)
|
|
60
|
+
def patch(path, body: nil, headers: {})
|
|
61
|
+
request(:patch, path, body: body, headers: headers)
|
|
51
62
|
end
|
|
52
63
|
|
|
53
|
-
def delete(path)
|
|
54
|
-
request(:delete, path)
|
|
64
|
+
def delete(path, headers: {})
|
|
65
|
+
request(:delete, path, headers: headers)
|
|
55
66
|
end
|
|
56
67
|
|
|
57
68
|
private
|
|
58
69
|
|
|
59
|
-
def request(method, path, body: nil, query: nil)
|
|
70
|
+
def request(method, path, body: nil, query: nil, headers: {})
|
|
60
71
|
uri = build_uri(path, query)
|
|
72
|
+
# Every write carries one key for all of its attempts, so the Gateway replays a write it
|
|
73
|
+
# already finished instead of running it (and charging the wallet) again.
|
|
74
|
+
headers = with_idempotency_key(method, headers)
|
|
61
75
|
last_error = nil
|
|
62
76
|
|
|
63
77
|
(0..MAX_RETRIES).each do |attempt|
|
|
@@ -66,57 +80,71 @@ module FlexOps
|
|
|
66
80
|
end
|
|
67
81
|
|
|
68
82
|
req = build_request(method, uri, body)
|
|
83
|
+
headers.each { |name, value| req[name] = value }
|
|
69
84
|
begin
|
|
70
85
|
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
|
|
71
86
|
open_timeout: @timeout, read_timeout: @timeout) do |http|
|
|
72
87
|
http.request(req)
|
|
73
88
|
end
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
# The Gateway may have run a timed-out or dropped request. GETs and keyed writes are
|
|
91
|
+
# safe to send again; a DELETE is not covered by the idempotency middleware.
|
|
92
|
+
raise if method == :delete
|
|
74
93
|
|
|
75
|
-
|
|
94
|
+
last_error = e
|
|
95
|
+
next
|
|
96
|
+
end
|
|
76
97
|
|
|
77
|
-
|
|
78
|
-
return parse_response(response)
|
|
79
|
-
end
|
|
98
|
+
status = response.code.to_i
|
|
80
99
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
100
|
+
if status >= 200 && status < 300
|
|
101
|
+
return parse_response(response)
|
|
102
|
+
end
|
|
84
103
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
104
|
+
if status == 401
|
|
105
|
+
raise AuthError
|
|
106
|
+
end
|
|
88
107
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
108
|
+
# Rate limited: the request was refused before it ran, so every method retries.
|
|
109
|
+
if status == 429
|
|
110
|
+
retry_after = (response["retry-after"] || "0").to_i
|
|
111
|
+
last_error = RateLimitError.new(retry_after: retry_after)
|
|
112
|
+
next
|
|
113
|
+
end
|
|
95
114
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
115
|
+
error_body = parse_error_body(response)
|
|
116
|
+
error = Error.new(
|
|
117
|
+
error_body["message"] || "HTTP #{status}: #{response.message}",
|
|
118
|
+
status: status,
|
|
119
|
+
code: error_body["errorCode"] || error_body["code"] || (status == 403 ? "FORBIDDEN" : nil),
|
|
120
|
+
errors: error_body["errors"]
|
|
121
|
+
)
|
|
102
122
|
|
|
103
|
-
|
|
104
|
-
last_error = error
|
|
105
|
-
next
|
|
106
|
-
end
|
|
123
|
+
raise error unless retryable?(method, status, error.code)
|
|
107
124
|
|
|
108
|
-
|
|
109
|
-
rescue Error
|
|
110
|
-
raise
|
|
111
|
-
rescue StandardError => e
|
|
112
|
-
last_error = e
|
|
113
|
-
next if attempt < MAX_RETRIES
|
|
114
|
-
end
|
|
125
|
+
last_error = error
|
|
115
126
|
end
|
|
116
127
|
|
|
117
128
|
raise last_error || Error.new("Request failed after retries", code: "RETRY_EXHAUSTED")
|
|
118
129
|
end
|
|
119
130
|
|
|
131
|
+
# GETs retry on every RETRYABLE_STATUSES status. A write retries only where the Gateway either
|
|
132
|
+
# did not run it or will replay it for the same Idempotency-Key. A DELETE retries only on 429.
|
|
133
|
+
def retryable?(method, status, code)
|
|
134
|
+
case method
|
|
135
|
+
when :get then RETRYABLE_STATUSES.include?(status)
|
|
136
|
+
when :delete then false
|
|
137
|
+
else status == 409 ? code == IDEMPOTENCY_CONFLICT : WRITE_RETRYABLE_STATUSES.include?(status)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def with_idempotency_key(method, headers)
|
|
142
|
+
return headers unless WRITE_METHODS.include?(method)
|
|
143
|
+
|
|
144
|
+
given = headers.any? { |name, value| name.to_s.casecmp?(IDEMPOTENCY_KEY_HEADER) && !value.to_s.empty? }
|
|
145
|
+
given ? headers : headers.merge(IDEMPOTENCY_KEY_HEADER => SecureRandom.uuid)
|
|
146
|
+
end
|
|
147
|
+
|
|
120
148
|
def build_uri(path, query)
|
|
121
149
|
url = "#{@base_url}#{path.start_with?("/") ? path : "/#{path}"}"
|
|
122
150
|
uri = URI.parse(url)
|
|
@@ -150,7 +178,11 @@ module FlexOps
|
|
|
150
178
|
req
|
|
151
179
|
end
|
|
152
180
|
|
|
181
|
+
# A 2xx with no body (204, or Content-Length 0) returns nil; JSON is parsed; anything else,
|
|
182
|
+
# such as a label file, is returned as the raw body string.
|
|
153
183
|
def parse_response(response)
|
|
184
|
+
return nil if response.code.to_i == 204 || response["content-length"] == "0" || response.body.nil? || response.body.empty?
|
|
185
|
+
|
|
154
186
|
content_type = response["content-type"] || ""
|
|
155
187
|
if content_type.include?("application/json")
|
|
156
188
|
JSON.parse(response.body)
|
|
@@ -160,7 +192,8 @@ module FlexOps
|
|
|
160
192
|
end
|
|
161
193
|
|
|
162
194
|
def parse_error_body(response)
|
|
163
|
-
JSON.parse(response.body)
|
|
195
|
+
parsed = JSON.parse(response.body)
|
|
196
|
+
parsed.is_a?(Hash) ? parsed : {}
|
|
164
197
|
rescue StandardError
|
|
165
198
|
{}
|
|
166
199
|
end
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
module FlexOps
|
|
10
10
|
module Resources
|
|
11
|
+
# Token acquisition for operations that need a signed-in user. Account management
|
|
12
|
+
# (registration, profile, passwords) belongs to the FlexOps portals.
|
|
11
13
|
class Auth
|
|
12
14
|
def initialize(http)
|
|
13
15
|
@http = http
|
|
@@ -17,41 +19,18 @@ module FlexOps
|
|
|
17
19
|
@http.post("/api/Account/login", body: { email: email, password: password })
|
|
18
20
|
end
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
# The Gateway RefreshTokenRequest requires the expired access token as well as the refresh
|
|
23
|
+
# token; it defaults to the client's current access token.
|
|
24
|
+
def refresh_token(refresh_token, access_token: nil)
|
|
25
|
+
token = access_token || @http.access_token
|
|
26
|
+
raise ArgumentError, "access_token is required to refresh a session" if token.to_s.empty?
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
@http.post("/api/Account/refresh-token", body: { refreshToken: refresh_token })
|
|
28
|
+
@http.post("/api/Account/refresh-token", body: { token: token, refreshToken: refresh_token })
|
|
26
29
|
end
|
|
27
30
|
|
|
28
31
|
def logout
|
|
29
32
|
@http.post("/api/Account/logout")
|
|
30
33
|
end
|
|
31
|
-
|
|
32
|
-
def get_profile
|
|
33
|
-
@http.get("/api/Account/profile")
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def update_profile(data)
|
|
37
|
-
@http.put("/api/Account/profile", body: data)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def change_password(current_password:, new_password:)
|
|
41
|
-
@http.post("/api/Account/change-password", body: { currentPassword: current_password, newPassword: new_password })
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def forgot_password(email:)
|
|
45
|
-
@http.post("/api/Account/forgot-password", body: { email: email })
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def reset_password(token:, new_password:)
|
|
49
|
-
@http.post("/api/Account/reset-password", body: { token: token, newPassword: new_password })
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def verify_email(token:)
|
|
53
|
-
@http.post("/api/Account/verify-email", body: { token: token })
|
|
54
|
-
end
|
|
55
34
|
end
|
|
56
35
|
end
|
|
57
36
|
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# ***********************************************************************
|
|
2
|
+
# Package : flexops
|
|
3
|
+
# Author : FlexOps, LLC
|
|
4
|
+
# Created : 2026-10-19
|
|
5
|
+
#
|
|
6
|
+
# Copyright (c) 2021-2026 by FlexOps, LLC. All rights reserved.
|
|
7
|
+
# ***********************************************************************
|
|
8
|
+
|
|
9
|
+
module FlexOps
|
|
10
|
+
module Resources
|
|
11
|
+
# Carbon offsets for shipments. Answers use the { success, data, message, errorCode } envelope.
|
|
12
|
+
class CarbonOffsets
|
|
13
|
+
def initialize(http, ws_id_proc)
|
|
14
|
+
@http = http
|
|
15
|
+
@ws_id = ws_id_proc
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def quote(weight_lbs:, distance_miles:)
|
|
19
|
+
@http.post("#{ws_path}/quote", body: { weightLbs: weight_lbs, distanceMiles: distance_miles })
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Buys an offset for a shipment and debits the workspace wallet.
|
|
23
|
+
# Pass an idempotency_key you store with the purchase to make your own retry safe: after an
|
|
24
|
+
# error or a timeout, retry with the same key and the Gateway returns the first result instead
|
|
25
|
+
# of buying again. Without one, the SDK's own retries still share a key, but a new call gets a new key.
|
|
26
|
+
def purchase(tracking_number:, weight_lbs:, distance_miles:, idempotency_key: nil)
|
|
27
|
+
@http.post("#{ws_path}/purchase",
|
|
28
|
+
body: { trackingNumber: tracking_number, weightLbs: weight_lbs, distanceMiles: distance_miles },
|
|
29
|
+
headers: idempotency_key.nil? ? {} : { "Idempotency-Key" => idempotency_key })
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def list(page: nil, page_size: nil)
|
|
33
|
+
query = { page: page, pageSize: page_size }.compact
|
|
34
|
+
@http.get(ws_path, query: query.empty? ? nil : query)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def get(offset_id)
|
|
38
|
+
@http.get("#{ws_path}/#{offset_id}")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def summary
|
|
42
|
+
@http.get("#{ws_path}/summary")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def get_settings
|
|
46
|
+
@http.get("#{ws_path}/settings")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def update_settings(auto_offset_enabled:)
|
|
50
|
+
@http.put("#{ws_path}/settings", body: { autoOffsetEnabled: auto_offset_enabled })
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def ws_path
|
|
56
|
+
"/api/workspaces/#{@ws_id.call}/carbon-offsets"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
module FlexOps
|
|
10
10
|
module Resources
|
|
11
|
+
# Carrier-specific calls through the legacy /api/ApiProxy passthrough (sunset 2027-06-30).
|
|
12
|
+
# Lookups (GET) work with an API key or a user token. The writes (POST/PUT/DELETE) are
|
|
13
|
+
# deprecated and need a user token (auth.login): the Gateway refuses API-key writes on the
|
|
14
|
+
# proxy with 403 API_KEY_SCOPE_REQUIRED. Label generation through the proxy is closed (410 Gone);
|
|
15
|
+
# buy, cancel and schedule through client.shipping, client.pickups and client.scan_forms.
|
|
11
16
|
class Carriers
|
|
12
17
|
attr_reader :usps, :ups, :fedex, :dhl
|
|
13
18
|
|
|
@@ -30,20 +35,37 @@ module FlexOps
|
|
|
30
35
|
def validate_address(params) = @http.get("#{PROXY}/api/v3/AddressValidation/getUspsValidateAndCorrectAddress", query: params)
|
|
31
36
|
def city_state_lookup(zip_code) = @http.get("#{PROXY}/api/v3/AddressValidation/getUspsCityStateLookupByZipCode", query: { zipCode: zip_code })
|
|
32
37
|
def zip_code_lookup(params) = @http.get("#{PROXY}/api/v3/AddressValidation/getUspsZipCodeLookupByAddress", query: params)
|
|
38
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
39
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
33
40
|
def get_domestic_rates(body) = @http.post("#{PROXY}/api/v3/RateCalculator/postUspsSearchDomesticBaseRates", body: body)
|
|
41
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
42
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
34
43
|
def get_domestic_products(body) = @http.post("#{PROXY}/api/v3/RateCalculator/postUspsSearchEligibleDomesticProducts", body: body)
|
|
44
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
45
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
35
46
|
def get_domestic_prices(body) = @http.post("#{PROXY}/api/v3/RateCalculator/postUspsSearchEligibleDomesticPrices", body: body)
|
|
47
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
48
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
36
49
|
def get_international_rates(body) = @http.post("#{PROXY}/api/v3/RateCalculator/postUspsSearchInternationalBaseRates", body: body)
|
|
50
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
51
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
37
52
|
def get_international_prices(body) = @http.post("#{PROXY}/api/v3/RateCalculator/postUspsSearchEligibleInternationalPrices", body: body)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def create_international_label(body) = @http.post("#{PROXY}/api/v3/Shipping/postUspsGenerateInternationalShippingLabel", body: body)
|
|
53
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
54
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
41
55
|
def cancel_domestic_label = @http.delete("#{PROXY}/api/v3/Shipping/cancelUspsDomesticShipmentLabel")
|
|
56
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
57
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
42
58
|
def cancel_international_label = @http.delete("#{PROXY}/api/v3/Shipping/cancelUspsInternationalShipmentLabel")
|
|
43
59
|
def track_summary(params) = @http.get("#{PROXY}/api/v3/Tracking/getUspsTrackingSummaryInformation", query: params)
|
|
44
60
|
def track_detail(params) = @http.get("#{PROXY}/api/v3/Tracking/getUspsTrackingDetailInformation", query: params)
|
|
61
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
62
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
45
63
|
def create_pickup(body) = @http.post("#{PROXY}/api/v3/CarrierPickup/postUspsCreateCarrierPickupSchedule", body: body)
|
|
64
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
65
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
46
66
|
def cancel_pickup = @http.delete("#{PROXY}/api/v3/CarrierPickup/cancelUspsCarrierPickupSchedule")
|
|
67
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
68
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
47
69
|
def create_scan_form(body) = @http.post("#{PROXY}/api/v3/ScanForm/postUspsCreateScanFormLabelShipment", body: body)
|
|
48
70
|
def delivery_standards(params) = @http.get("#{PROXY}/api/v3/ServiceStandards/getUspsGetDeliveryStandardsEstimates", query: params)
|
|
49
71
|
def find_drop_off_locations(params) = @http.get("#{PROXY}/api/v3/LocationSearch/getUspsFindValidDropOffLocations", query: params)
|
|
@@ -57,17 +79,36 @@ module FlexOps
|
|
|
57
79
|
@http = http
|
|
58
80
|
end
|
|
59
81
|
|
|
82
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
83
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
60
84
|
def validate_address(body) = @http.post("#{PROXY}/api/v2/ShippingLabel/postUpsVerifyAddress", body: body)
|
|
85
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
86
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
61
87
|
def get_rates(body) = @http.post("#{PROXY}/api/v2/ShippingLabel/postUpsRateCheck", body: body)
|
|
62
|
-
def create_label(body) = @http.post("#{PROXY}/api/v2/ShippingLabel/generateNewUpsShipLabel", body: body)
|
|
63
88
|
def track(params) = @http.get("#{PROXY}/api/v2/ShippingLabel/getSingleUpsTrackingDetail", query: params)
|
|
89
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
90
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
64
91
|
def create_pickup(body) = @http.post("#{PROXY}/api/v2/ShippingLabel/postUpsCreatePickup", body: body)
|
|
92
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
93
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
65
94
|
def cancel_pickup = @http.delete("#{PROXY}/api/v2/ShippingLabel/deleteUpsPickup")
|
|
95
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
96
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
66
97
|
def get_transit_times(body) = @http.post("#{PROXY}/api/v2/ShippingLabel/postUpsGetTransitTimes", body: body)
|
|
98
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
99
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
67
100
|
def get_landed_cost(body) = @http.post("#{PROXY}/api/v2/ShippingLabel/postUpsGetLandedCostQuote", body: body)
|
|
101
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
102
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
68
103
|
def search_locations(body) = @http.post("#{PROXY}/api/v2/ShippingLabel/postUpsSearchLocations", body: body)
|
|
104
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
105
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
69
106
|
def upload_document(body) = @http.post("#{PROXY}/api/v2/ShippingLabel/postUpsUploadPaperlessDocument", body: body)
|
|
107
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
108
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
70
109
|
def create_freight_shipment(body) = @http.post("#{PROXY}/api/v2/ShippingLabel/postUpsCreateFreightShipment", body: body)
|
|
110
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
111
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
71
112
|
def get_freight_rate(body) = @http.post("#{PROXY}/api/v2/ShippingLabel/postUpsGetFreightRate", body: body)
|
|
72
113
|
end
|
|
73
114
|
|
|
@@ -78,26 +119,62 @@ module FlexOps
|
|
|
78
119
|
@http = http
|
|
79
120
|
end
|
|
80
121
|
|
|
122
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
123
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
81
124
|
def validate_address(body) = @http.post("#{PROXY}/api/v3/AddressValidation/postFedExValidateAndCorrectDomesticAddress", body: body)
|
|
125
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
126
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
82
127
|
def validate_postal_code(body) = @http.post("#{PROXY}/api/v3/AddressValidation/postFedExValidatePostalCode", body: body)
|
|
128
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
129
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
83
130
|
def get_rates(body) = @http.post("#{PROXY}/api/v3/RateCalculator/postRetrieveFedExRateAndTransitTimesAsync", body: body)
|
|
84
|
-
|
|
131
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
132
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
85
133
|
def cancel_shipment(body) = @http.put("#{PROXY}/api/v3/Shipping/putFedExCancelShipment", body: body)
|
|
134
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
135
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
86
136
|
def validate_shipment(body) = @http.post("#{PROXY}/api/v3/Shipping/postFedExValidateShipment", body: body)
|
|
87
|
-
|
|
137
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
138
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
88
139
|
def track(body) = @http.post("#{PROXY}/api/v3/Tracking/postFedExRetrieveTrackingInfoByTrackingNumber", body: body)
|
|
140
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
141
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
89
142
|
def track_multi_piece(body) = @http.post("#{PROXY}/api/v3/Tracking/postFedExRetrieveTrackingInfoForMultiPieceShipment", body: body)
|
|
143
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
144
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
90
145
|
def register_tracking_notification(body) = @http.post("#{PROXY}/api/v3/Tracking/postFedExRegisterForTrackingNotification", body: body)
|
|
146
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
147
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
91
148
|
def create_pickup(body) = @http.post("#{PROXY}/api/v3/CarrierPickup/postFedExCreateCarrierPickupRequest", body: body)
|
|
149
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
150
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
92
151
|
def cancel_pickup(body) = @http.put("#{PROXY}/api/v3/CarrierPickup/putFedExCancelCarrierPickupRequest", body: body)
|
|
152
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
153
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
93
154
|
def search_locations(body) = @http.post("#{PROXY}/api/v3/LocationSearch/postFedExSearchValidLocations", body: body)
|
|
155
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
156
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
94
157
|
def get_service_standards(body) = @http.post("#{PROXY}/api/v3/ServiceStandards/postFedExRetrieveServicesAndTransitTimes", body: body)
|
|
158
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
159
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
95
160
|
def get_freight_rate(body) = @http.post("#{PROXY}/api/v3/Freight/postFedExGetFreightRateQuote", body: body)
|
|
161
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
162
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
96
163
|
def create_freight_shipment(body) = @http.post("#{PROXY}/api/v3/Freight/postFedExCreateFreightShipment", body: body)
|
|
164
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
165
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
97
166
|
def ground_close(body) = @http.post("#{PROXY}/api/v3/GroundClose/postFedExCloseWithDocuments", body: body)
|
|
167
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
168
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
98
169
|
def upload_trade_documents(body) = @http.post("#{PROXY}/api/v3/Trade/postFedExUploadTradeDocuments", body: body)
|
|
170
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
171
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
99
172
|
def create_open_shipment(body) = @http.post("#{PROXY}/api/v3/OpenShip/postFedExCreateOpenShipment", body: body)
|
|
173
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
174
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
100
175
|
def add_packages_to_open_shipment(body) = @http.post("#{PROXY}/api/v3/OpenShip/postFedExAddPackagesToOpenShipment", body: body)
|
|
176
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
177
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
101
178
|
def confirm_open_shipment(body) = @http.post("#{PROXY}/api/v3/OpenShip/postFedExConfirmOpenShipment", body: body)
|
|
102
179
|
end
|
|
103
180
|
|
|
@@ -110,16 +187,26 @@ module FlexOps
|
|
|
110
187
|
|
|
111
188
|
def validate_address(params) = @http.get("#{PROXY}/api/v2/ShippingLabel/getDhlValidateAddress", query: params)
|
|
112
189
|
def get_rates(params) = @http.get("#{PROXY}/api/v2/ShippingLabel/getDhlRates", query: params)
|
|
190
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
191
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
113
192
|
def get_multi_piece_rates(body) = @http.post("#{PROXY}/api/v2/ShippingLabel/postDhlMultiPieceRates", body: body)
|
|
114
193
|
def get_products(params) = @http.get("#{PROXY}/api/v2/ShippingLabel/getDhlProducts", query: params)
|
|
115
|
-
def create_shipment(body) = @http.post("#{PROXY}/api/v2/ShippingLabel/postDhlCreateShipment", body: body)
|
|
116
194
|
def track(params) = @http.get("#{PROXY}/api/v2/ShippingLabel/getDhlTrackSingleShipment", query: params)
|
|
117
195
|
def track_multiple(params) = @http.get("#{PROXY}/api/v2/ShippingLabel/getDhlTrackMultipleShipments", query: params)
|
|
196
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
197
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
118
198
|
def create_pickup(body) = @http.post("#{PROXY}/api/v2/ShippingLabel/postDhlCreatePickup", body: body)
|
|
119
|
-
|
|
199
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
200
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
120
201
|
def cancel_pickup = @http.delete("#{PROXY}/api/v2/ShippingLabel/deleteDhlPickup")
|
|
202
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
203
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
121
204
|
def calculate_landed_cost(body) = @http.post("#{PROXY}/api/v2/ShippingLabel/postDhlCalculateLandedCost", body: body)
|
|
205
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
206
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
122
207
|
def screen_shipment(body) = @http.post("#{PROXY}/api/v2/ShippingLabel/postDhlScreenShipment", body: body)
|
|
208
|
+
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
209
|
+
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
123
210
|
def upload_invoice(body) = @http.post("#{PROXY}/api/v2/ShippingLabel/postDhlUploadInvoice", body: body)
|
|
124
211
|
def get_proof_of_delivery(params) = @http.get("#{PROXY}/api/v2/ShippingLabel/getDhlElectronicProofOfDelivery", query: params)
|
|
125
212
|
def get_reference_data(params) = @http.get("#{PROXY}/api/v2/ShippingLabel/getDhlReferenceData", query: params)
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
# ***********************************************************************
|
|
2
2
|
# Package : flexops
|
|
3
3
|
# Author : FlexOps, LLC
|
|
4
|
-
# Created : 2026-
|
|
4
|
+
# Created : 2026-10-19
|
|
5
5
|
#
|
|
6
6
|
# Copyright (c) 2021-2026 by FlexOps, LLC. All rights reserved.
|
|
7
7
|
# ***********************************************************************
|
|
8
8
|
|
|
9
|
-
# frozen_string_literal: true
|
|
10
|
-
|
|
11
9
|
module FlexOps
|
|
12
10
|
module Resources
|
|
11
|
+
# Branded tracking email templates, one per tracking event. Answers are bare JSON (no
|
|
12
|
+
# envelope); ids are GUID strings.
|
|
13
|
+
#
|
|
14
|
+
# Reads work with any API key or a user token. Writes (upsert, delete, preview) need a user
|
|
15
|
+
# token (auth.login) or an API key granted the opt-in "email-templates:write" scope; a key
|
|
16
|
+
# without it gets 403 API_KEY_SCOPE_REQUIRED.
|
|
17
|
+
#
|
|
18
|
+
# +eventType+ is the Gateway TrackingEmailEvent number: LabelCreated 0, InTransit 1,
|
|
19
|
+
# OutForDelivery 2, Delivered 3, Exception 4, ReturnToSender 5.
|
|
13
20
|
class EmailTemplates
|
|
14
21
|
def initialize(http, ws_id_proc)
|
|
15
22
|
@http = http
|
|
@@ -17,33 +24,36 @@ module FlexOps
|
|
|
17
24
|
end
|
|
18
25
|
|
|
19
26
|
def list
|
|
20
|
-
@http.get(
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def get(id)
|
|
24
|
-
@http.get("#{ws_path}/shipment-email-templates/#{id}")
|
|
27
|
+
@http.get(ws_path)
|
|
25
28
|
end
|
|
26
29
|
|
|
27
|
-
def
|
|
28
|
-
@http.
|
|
30
|
+
def get(template_id)
|
|
31
|
+
@http.get("#{ws_path}/#{template_id}")
|
|
29
32
|
end
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
# Creates or replaces the template for request's +eventType+.
|
|
35
|
+
#
|
|
36
|
+
# request:: { eventType: (0-5), subjectTemplate: (<= 500 chars), htmlBodyTemplate:,
|
|
37
|
+
# isEnabled: (default true) }. Returns the template.
|
|
38
|
+
def upsert(request)
|
|
39
|
+
@http.put(ws_path, body: request)
|
|
33
40
|
end
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
# Returns nil (204).
|
|
43
|
+
def delete(template_id)
|
|
44
|
+
@http.delete("#{ws_path}/#{template_id}")
|
|
37
45
|
end
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
# Renders a draft with sample placeholder values. Returns { renderedSubject, renderedHtmlBody }.
|
|
48
|
+
def preview(subject_template:, html_body_template:)
|
|
49
|
+
@http.post("#{ws_path}/preview",
|
|
50
|
+
body: { subjectTemplate: subject_template, htmlBodyTemplate: html_body_template })
|
|
41
51
|
end
|
|
42
52
|
|
|
43
53
|
private
|
|
44
54
|
|
|
45
55
|
def ws_path
|
|
46
|
-
"/api/workspaces/#{@ws_id.call}"
|
|
56
|
+
"/api/workspaces/#{@ws_id.call}/email-templates"
|
|
47
57
|
end
|
|
48
58
|
end
|
|
49
59
|
end
|
|
@@ -8,31 +8,30 @@
|
|
|
8
8
|
|
|
9
9
|
# frozen_string_literal: true
|
|
10
10
|
|
|
11
|
+
require "erb"
|
|
12
|
+
|
|
11
13
|
module FlexOps
|
|
12
14
|
module Resources
|
|
15
|
+
# HS code lookup and landed cost. These routes are not workspace-scoped; each returns a bare
|
|
16
|
+
# (not enveloped) answer.
|
|
13
17
|
class HsCodes
|
|
14
|
-
def initialize(http
|
|
18
|
+
def initialize(http)
|
|
15
19
|
@http = http
|
|
16
|
-
@ws_id = ws_id_proc
|
|
17
20
|
end
|
|
18
21
|
|
|
19
22
|
def search(query, destination_country: nil, max_results: 10)
|
|
20
23
|
params = { query: query, destinationCountry: destination_country, maxResults: max_results }.compact
|
|
21
|
-
@http.get("
|
|
24
|
+
@http.get("/api/shipping/hs-codes/search", query: params)
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
def lookup(code)
|
|
25
|
-
@http.get("
|
|
28
|
+
@http.get("/api/shipping/hs-codes/#{ERB::Util.url_encode(code.to_s)}")
|
|
26
29
|
end
|
|
27
30
|
|
|
31
|
+
# request:: { destinationCountry:, items: [{ description:, quantity:, value:, weightOz:,
|
|
32
|
+
# hsCode: (optional), originCountry:, skuOrProductId: (optional) }], shippingCost: }
|
|
28
33
|
def estimate_landed_cost(request)
|
|
29
|
-
@http.post("
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
def ws_path
|
|
35
|
-
"/api/workspaces/#{@ws_id.call}"
|
|
34
|
+
@http.post("/api/shipping/landed-cost", body: request)
|
|
36
35
|
end
|
|
37
36
|
end
|
|
38
37
|
end
|