flexops 1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +196 -0
- data/lib/flexops/client.rb +53 -0
- data/lib/flexops/errors.rb +35 -0
- data/lib/flexops/http.rb +173 -0
- data/lib/flexops/resources/analytics.rb +85 -0
- data/lib/flexops/resources/api_keys.rb +40 -0
- data/lib/flexops/resources/auth.rb +57 -0
- data/lib/flexops/resources/carriers.rb +129 -0
- data/lib/flexops/resources/email_templates.rb +50 -0
- data/lib/flexops/resources/hs_codes.rb +39 -0
- data/lib/flexops/resources/insurance.rb +48 -0
- data/lib/flexops/resources/inventory.rb +39 -0
- data/lib/flexops/resources/offsets.rb +38 -0
- data/lib/flexops/resources/orders.rb +67 -0
- data/lib/flexops/resources/pickups.rb +40 -0
- data/lib/flexops/resources/recurring_shipments.rb +59 -0
- data/lib/flexops/resources/reports.rb +46 -0
- data/lib/flexops/resources/returns.rb +61 -0
- data/lib/flexops/resources/rules.rb +48 -0
- data/lib/flexops/resources/scan_forms.rb +36 -0
- data/lib/flexops/resources/shipping.rb +84 -0
- data/lib/flexops/resources/wallet.rb +41 -0
- data/lib/flexops/resources/webhooks.rb +59 -0
- data/lib/flexops/resources/workspaces.rb +53 -0
- data/lib/flexops/version.rb +11 -0
- data/lib/flexops.rb +35 -0
- metadata +111 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: de7a8ef21d6a80d52efbbb2f7254fb5184c875ab84a09834204d4757736040ce
|
|
4
|
+
data.tar.gz: 741a555c559275fe53124fa44d5cd76d3d9dbf57031b12346dfecb63e9c26c73
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e32d5a2172b700037fe706867b7111a5f6af66d099bf8f759b916fb38134b7760a3212d2f44c6eec83a10b243cf602c2fadd20792884871fbdcc878b84306f8c
|
|
7
|
+
data.tar.gz: 4446a9a607aba3ccc55b694f4438a63d6d20422825a7bd476da37dad4021885e60d2b93e65f6cf72ef65eb9ac4b930f2afd0a4fc26361a651f877be185ed6a7f
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FlexOps, LLC
|
|
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,196 @@
|
|
|
1
|
+
# FlexOps Ruby SDK
|
|
2
|
+
|
|
3
|
+
Official Ruby SDK for the [FlexOps](https://flexops.io) multi-carrier shipping platform. Supports USPS, UPS, FedEx, DHL, OnTrac, Australia Post, Canada Post, Royal Mail, and LSO with rate shopping, label generation, tracking, webhooks, wallet, insurance, returns, and more.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "flexops"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install directly:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install flexops
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require "flexops"
|
|
29
|
+
|
|
30
|
+
# API key authentication (recommended for server-to-server)
|
|
31
|
+
client = FlexOps::Client.new(
|
|
32
|
+
api_key: "fxk_live_...",
|
|
33
|
+
workspace_id: "ws_abc123"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Get shipping rates from all carriers
|
|
37
|
+
rates = client.shipping.get_rates(
|
|
38
|
+
from_address: {street1: "123 Main St", city: "New York", state: "NY", zip: "10001", country: "US"},
|
|
39
|
+
to_address: {street1: "456 Oak Ave", city: "Los Angeles", state: "CA", zip: "90210", country: "US"},
|
|
40
|
+
parcel: {weight: 16, weight_unit: "oz"}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Create a label with the cheapest rate
|
|
44
|
+
cheapest = rates["data"].min_by { |r| r["totalCost"].to_f }
|
|
45
|
+
label = client.shipping.create_label(
|
|
46
|
+
carrier: cheapest["carrier"],
|
|
47
|
+
service: cheapest["service"],
|
|
48
|
+
from_address: {name: "Warehouse", street1: "123 Main St", city: "New York", state: "NY", zip: "10001", country: "US"},
|
|
49
|
+
to_address: {name: "Customer", street1: "456 Oak Ave", city: "Los Angeles", state: "CA", zip: "90210", country: "US"},
|
|
50
|
+
parcel: {weight: 16, weight_unit: "oz"}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
puts "Label URL: #{label["data"]["labelUrl"]}"
|
|
54
|
+
puts "Tracking: #{label["data"]["trackingNumber"]}"
|
|
55
|
+
|
|
56
|
+
# Track a shipment
|
|
57
|
+
info = client.shipping.track("9400111899223456789012")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Authentication
|
|
61
|
+
|
|
62
|
+
### API key (recommended)
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
client = FlexOps::Client.new(api_key: "fxk_live_...", workspace_id: "ws_abc123")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Email / password
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
client = FlexOps::Client.new(base_url: "https://gateway.flexops.io")
|
|
72
|
+
client.auth.login(email: "user@example.com", password: "password")
|
|
73
|
+
client.workspace_id = "ws_abc123"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Sandbox / test keys
|
|
77
|
+
|
|
78
|
+
Use `fxk_test_...` (instead of `fxk_live_...`) to route to the sandbox environment. Mock carriers respond, nothing hits real carrier APIs, no charges, no real labels. Perfect for CI and integration tests.
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
client = FlexOps::Client.new(api_key: "fxk_test_...", workspace_id: "ws_abc123")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Direct carrier operations
|
|
85
|
+
|
|
86
|
+
Access carrier-specific endpoints when you need full control:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
# USPS domestic label
|
|
90
|
+
label = client.carriers.usps.create_domestic_label(
|
|
91
|
+
image_type: "PDF",
|
|
92
|
+
mail_class: "PRIORITY_MAIL",
|
|
93
|
+
weight_in_ounces: 16
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# FedEx rate quote
|
|
97
|
+
rates = client.carriers.fedex.get_rates(...)
|
|
98
|
+
|
|
99
|
+
# UPS tracking
|
|
100
|
+
info = client.carriers.ups.track(tracking_number: "1Z999AA10123456784")
|
|
101
|
+
|
|
102
|
+
# DHL shipment
|
|
103
|
+
shipment = client.carriers.dhl.create_shipment(...)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Webhook verification
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
valid = FlexOps::Resources::Webhooks.verify_signature(
|
|
110
|
+
payload: request.body.read,
|
|
111
|
+
signature: request.headers["X-FlexOps-Signature"],
|
|
112
|
+
secret: "whsec_..."
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Curl quickstart
|
|
117
|
+
|
|
118
|
+
Every SDK method is a thin wrapper around the FlexOps REST API. If you want to verify the API before committing to the SDK — or you're integrating from a language we don't ship a SDK for — these curl invocations hit the same endpoints:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Shop rates across all connected carriers
|
|
122
|
+
curl -X POST https://gateway.flexops.io/api/workspaces/ws_abc123/shipping/rates \
|
|
123
|
+
-H "X-API-Key: fxk_live_..." \
|
|
124
|
+
-H "Content-Type: application/json" \
|
|
125
|
+
-d '{
|
|
126
|
+
"fromAddress": {"street1": "123 Main St", "city": "New York", "state": "NY", "zip": "10001", "country": "US"},
|
|
127
|
+
"toAddress": {"street1": "456 Oak Ave", "city": "Los Angeles", "state": "CA", "zip": "90210", "country": "US"},
|
|
128
|
+
"parcel": {"weight": 16, "weightUnit": "oz"}
|
|
129
|
+
}'
|
|
130
|
+
|
|
131
|
+
# Create a label
|
|
132
|
+
curl -X POST https://gateway.flexops.io/api/workspaces/ws_abc123/shipping/labels \
|
|
133
|
+
-H "X-API-Key: fxk_live_..." \
|
|
134
|
+
-H "Content-Type: application/json" \
|
|
135
|
+
-d '{
|
|
136
|
+
"carrier": "USPS",
|
|
137
|
+
"service": "PRIORITY_MAIL",
|
|
138
|
+
"fromAddress": {"name": "Warehouse", "street1": "123 Main St", "city": "New York", "state": "NY", "zip": "10001", "country": "US"},
|
|
139
|
+
"toAddress": {"name": "Customer", "street1": "456 Oak Ave", "city": "Los Angeles", "state": "CA", "zip": "90210", "country": "US"},
|
|
140
|
+
"parcel": {"weight": 16, "weightUnit": "oz"}
|
|
141
|
+
}'
|
|
142
|
+
|
|
143
|
+
# Track a shipment
|
|
144
|
+
curl https://gateway.flexops.io/api/workspaces/ws_abc123/shipping/track/9400111899223456789012 \
|
|
145
|
+
-H "X-API-Key: fxk_live_..."
|
|
146
|
+
|
|
147
|
+
# Cancel a label (via the unified carrier-agnostic endpoint)
|
|
148
|
+
curl -X DELETE https://gateway.flexops.io/api/v3.0/shipping/Usps/cancel/9400111899223456789012 \
|
|
149
|
+
-H "X-API-Key: fxk_live_..."
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Use an `fxk_test_...` key instead of `fxk_live_...` to hit the sandbox environment; mock carriers respond, no real charges, no real labels.
|
|
153
|
+
|
|
154
|
+
## Resources
|
|
155
|
+
|
|
156
|
+
| Resource | Description |
|
|
157
|
+
|----------|-------------|
|
|
158
|
+
| `client.auth` | Login, register, password management |
|
|
159
|
+
| `client.workspaces` | Workspace CRUD, membership, branding |
|
|
160
|
+
| `client.shipping` | Rate shopping, labels, tracking, batch, cancel |
|
|
161
|
+
| `client.carriers` | USPS, UPS, FedEx, DHL direct endpoints |
|
|
162
|
+
| `client.webhooks` | Subscription CRUD, signature verification, delivery logs |
|
|
163
|
+
| `client.wallet` | Balance, transactions, auto-reload |
|
|
164
|
+
| `client.insurance` | Quotes, purchase, claims (first-party + U-PIC) |
|
|
165
|
+
| `client.returns` | RMA lifecycle: create, batch, QR codes, photo upload, cost recovery |
|
|
166
|
+
| `client.api_keys` | Key creation, rotation, revocation |
|
|
167
|
+
| `client.analytics` | Shipments, orders, carrier performance |
|
|
168
|
+
| `client.orders` | Order management |
|
|
169
|
+
| `client.inventory` | Warehouse inventory |
|
|
170
|
+
| `client.pickups` | Carrier pickup scheduling |
|
|
171
|
+
| `client.scan_forms` | USPS scan forms |
|
|
172
|
+
| `client.rules` | Shipping automation rules |
|
|
173
|
+
| `client.offsets` | Carbon offset purchases |
|
|
174
|
+
| `client.hs_codes` | HS code lookup for international customs |
|
|
175
|
+
| `client.recurring_shipments` | Scheduled recurring shipments |
|
|
176
|
+
| `client.email_templates` | Branded post-purchase email templates |
|
|
177
|
+
| `client.reports` | Report generation and scheduled delivery |
|
|
178
|
+
|
|
179
|
+
## Configuration
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
client = FlexOps::Client.new(
|
|
183
|
+
base_url: "https://gateway.flexops.io", # API base URL
|
|
184
|
+
api_key: "fxk_live_...", # API key auth
|
|
185
|
+
workspace_id: "ws_abc123", # Default workspace
|
|
186
|
+
timeout: 30 # Request timeout (seconds)
|
|
187
|
+
)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Requirements
|
|
191
|
+
|
|
192
|
+
- Ruby 3.0+
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
Proprietary — FlexOps, LLC
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# ***********************************************************************
|
|
2
|
+
# Package : flexops
|
|
3
|
+
# Author : FlexOps, LLC
|
|
4
|
+
# Created : 2026-03-08
|
|
5
|
+
#
|
|
6
|
+
# Copyright (c) 2021-2026 by FlexOps, LLC. All rights reserved.
|
|
7
|
+
# ***********************************************************************
|
|
8
|
+
|
|
9
|
+
module FlexOps
|
|
10
|
+
class Client
|
|
11
|
+
attr_accessor :workspace_id
|
|
12
|
+
attr_reader :auth, :workspaces, :shipping, :carriers, :webhooks, :wallet,
|
|
13
|
+
:insurance, :returns, :api_keys, :analytics, :orders, :inventory,
|
|
14
|
+
:pickups, :scan_forms, :rules, :offsets, :hs_codes,
|
|
15
|
+
:recurring_shipments, :email_templates, :reports
|
|
16
|
+
|
|
17
|
+
def initialize(api_key: nil, access_token: nil, base_url: "https://gateway.flexops.io", workspace_id: nil, timeout: 30)
|
|
18
|
+
@http = HttpClient.new(base_url: base_url, api_key: api_key, access_token: access_token, timeout: timeout)
|
|
19
|
+
@workspace_id = workspace_id
|
|
20
|
+
|
|
21
|
+
ws_id_proc = -> { @workspace_id }
|
|
22
|
+
|
|
23
|
+
@auth = Resources::Auth.new(@http)
|
|
24
|
+
@workspaces = Resources::Workspaces.new(@http, ws_id_proc)
|
|
25
|
+
@shipping = Resources::Shipping.new(@http, ws_id_proc)
|
|
26
|
+
@carriers = Resources::Carriers.new(@http)
|
|
27
|
+
@webhooks = Resources::Webhooks.new(@http, ws_id_proc)
|
|
28
|
+
@wallet = Resources::Wallet.new(@http, ws_id_proc)
|
|
29
|
+
@insurance = Resources::Insurance.new(@http, ws_id_proc)
|
|
30
|
+
@returns = Resources::Returns.new(@http, ws_id_proc)
|
|
31
|
+
@api_keys = Resources::ApiKeys.new(@http, ws_id_proc)
|
|
32
|
+
@analytics = Resources::Analytics.new(@http)
|
|
33
|
+
@orders = Resources::Orders.new(@http)
|
|
34
|
+
@inventory = Resources::Inventory.new(@http)
|
|
35
|
+
@pickups = Resources::Pickups.new(@http, ws_id_proc)
|
|
36
|
+
@scan_forms = Resources::ScanForms.new(@http, ws_id_proc)
|
|
37
|
+
@rules = Resources::Rules.new(@http, ws_id_proc)
|
|
38
|
+
@offsets = Resources::Offsets.new(@http, ws_id_proc)
|
|
39
|
+
@hs_codes = Resources::HsCodes.new(@http, ws_id_proc)
|
|
40
|
+
@recurring_shipments = Resources::RecurringShipments.new(@http, ws_id_proc)
|
|
41
|
+
@email_templates = Resources::EmailTemplates.new(@http, ws_id_proc)
|
|
42
|
+
@reports = Resources::Reports.new(@http, ws_id_proc)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def set_access_token(token)
|
|
46
|
+
@http.set_access_token(token)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def set_api_key(key)
|
|
50
|
+
@http.set_api_key(key)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# ***********************************************************************
|
|
2
|
+
# Package : flexops
|
|
3
|
+
# Author : FlexOps, LLC
|
|
4
|
+
# Created : 2026-03-08
|
|
5
|
+
#
|
|
6
|
+
# Copyright (c) 2021-2026 by FlexOps, LLC. All rights reserved.
|
|
7
|
+
# ***********************************************************************
|
|
8
|
+
|
|
9
|
+
module FlexOps
|
|
10
|
+
class Error < StandardError
|
|
11
|
+
attr_reader :status, :code, :errors
|
|
12
|
+
|
|
13
|
+
def initialize(message, status: 0, code: nil, errors: nil)
|
|
14
|
+
super(message)
|
|
15
|
+
@status = status
|
|
16
|
+
@code = code
|
|
17
|
+
@errors = errors
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class AuthError < Error
|
|
22
|
+
def initialize(message = "Authentication required. Check your access token or API key.")
|
|
23
|
+
super(message, status: 401, code: "UNAUTHORIZED")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class RateLimitError < Error
|
|
28
|
+
attr_reader :retry_after
|
|
29
|
+
|
|
30
|
+
def initialize(retry_after: 0)
|
|
31
|
+
super("Rate limited. Retry after #{retry_after}s", status: 429, code: "RATE_LIMITED")
|
|
32
|
+
@retry_after = retry_after
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/flexops/http.rb
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# ***********************************************************************
|
|
2
|
+
# Package : flexops
|
|
3
|
+
# Author : FlexOps, LLC
|
|
4
|
+
# Created : 2026-03-08
|
|
5
|
+
#
|
|
6
|
+
# Copyright (c) 2021-2026 by FlexOps, LLC. All rights reserved.
|
|
7
|
+
# ***********************************************************************
|
|
8
|
+
|
|
9
|
+
require "net/http"
|
|
10
|
+
require "uri"
|
|
11
|
+
require "json"
|
|
12
|
+
|
|
13
|
+
module FlexOps
|
|
14
|
+
class HttpClient
|
|
15
|
+
DEFAULT_BASE_URL = "https://gateway.flexops.io"
|
|
16
|
+
DEFAULT_TIMEOUT = 30
|
|
17
|
+
MAX_RETRIES = 3
|
|
18
|
+
RETRYABLE_STATUSES = [429, 500, 502, 503, 504].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(base_url: DEFAULT_BASE_URL, api_key: nil, access_token: nil, timeout: DEFAULT_TIMEOUT)
|
|
21
|
+
@base_url = base_url.chomp("/")
|
|
22
|
+
@api_key = api_key
|
|
23
|
+
@access_token = access_token
|
|
24
|
+
@timeout = timeout
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def set_access_token(token)
|
|
28
|
+
@access_token = token
|
|
29
|
+
@api_key = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def set_api_key(key)
|
|
33
|
+
@api_key = key
|
|
34
|
+
@access_token = nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def get(path, query: nil)
|
|
38
|
+
request(:get, path, query: query)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def post(path, body: nil, query: nil)
|
|
42
|
+
request(:post, path, body: body, query: query)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def put(path, body: nil)
|
|
46
|
+
request(:put, path, body: body)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def patch(path, body: nil)
|
|
50
|
+
request(:patch, path, body: body)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def delete(path)
|
|
54
|
+
request(:delete, path)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def request(method, path, body: nil, query: nil)
|
|
60
|
+
uri = build_uri(path, query)
|
|
61
|
+
last_error = nil
|
|
62
|
+
|
|
63
|
+
(0..MAX_RETRIES).each do |attempt|
|
|
64
|
+
if attempt > 0
|
|
65
|
+
sleep(calculate_backoff(attempt))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
req = build_request(method, uri, body)
|
|
69
|
+
begin
|
|
70
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
|
|
71
|
+
open_timeout: @timeout, read_timeout: @timeout) do |http|
|
|
72
|
+
http.request(req)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
status = response.code.to_i
|
|
76
|
+
|
|
77
|
+
if status >= 200 && status < 300
|
|
78
|
+
return parse_response(response)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if status == 401
|
|
82
|
+
raise AuthError
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if status == 403
|
|
86
|
+
raise Error.new("Access denied. Check your plan tier and feature entitlements.", status: 403, code: "FORBIDDEN")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if status == 429
|
|
90
|
+
retry_after = (response["retry-after"] || "0").to_i
|
|
91
|
+
last_error = RateLimitError.new(retry_after: retry_after)
|
|
92
|
+
next if RETRYABLE_STATUSES.include?(429)
|
|
93
|
+
raise last_error
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
error_body = parse_error_body(response)
|
|
97
|
+
error = Error.new(
|
|
98
|
+
error_body["message"] || "HTTP #{status}: #{response.message}",
|
|
99
|
+
status: status,
|
|
100
|
+
errors: error_body["errors"]
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if RETRYABLE_STATUSES.include?(status)
|
|
104
|
+
last_error = error
|
|
105
|
+
next
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
raise error
|
|
109
|
+
rescue Error
|
|
110
|
+
raise
|
|
111
|
+
rescue StandardError => e
|
|
112
|
+
last_error = e
|
|
113
|
+
next if attempt < MAX_RETRIES
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
raise last_error || Error.new("Request failed after retries", code: "RETRY_EXHAUSTED")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def build_uri(path, query)
|
|
121
|
+
url = "#{@base_url}#{path.start_with?("/") ? path : "/#{path}"}"
|
|
122
|
+
uri = URI.parse(url)
|
|
123
|
+
if query
|
|
124
|
+
params = URI.encode_www_form(query.compact)
|
|
125
|
+
uri.query = params unless params.empty?
|
|
126
|
+
end
|
|
127
|
+
uri
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def build_request(method, uri, body)
|
|
131
|
+
klass = case method
|
|
132
|
+
when :get then Net::HTTP::Get
|
|
133
|
+
when :post then Net::HTTP::Post
|
|
134
|
+
when :put then Net::HTTP::Put
|
|
135
|
+
when :patch then Net::HTTP::Patch
|
|
136
|
+
when :delete then Net::HTTP::Delete
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
req = klass.new(uri)
|
|
140
|
+
req["Content-Type"] = "application/json"
|
|
141
|
+
req["Accept"] = "application/json"
|
|
142
|
+
|
|
143
|
+
if @api_key
|
|
144
|
+
req["X-Api-Key"] = @api_key
|
|
145
|
+
elsif @access_token
|
|
146
|
+
req["Authorization"] = "Bearer #{@access_token}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
req.body = body.to_json if body
|
|
150
|
+
req
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def parse_response(response)
|
|
154
|
+
content_type = response["content-type"] || ""
|
|
155
|
+
if content_type.include?("application/json")
|
|
156
|
+
JSON.parse(response.body)
|
|
157
|
+
else
|
|
158
|
+
response.body
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def parse_error_body(response)
|
|
163
|
+
JSON.parse(response.body)
|
|
164
|
+
rescue StandardError
|
|
165
|
+
{}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def calculate_backoff(attempt)
|
|
169
|
+
jitter = rand(0.85..1.15)
|
|
170
|
+
[1.0 * (2**(attempt - 1)) * jitter, 30.0].min
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# ***********************************************************************
|
|
2
|
+
# Package : flexops
|
|
3
|
+
# Author : FlexOps, LLC
|
|
4
|
+
# Created : 2026-03-08
|
|
5
|
+
#
|
|
6
|
+
# Copyright (c) 2021-2026 by FlexOps, LLC. All rights reserved.
|
|
7
|
+
# ***********************************************************************
|
|
8
|
+
|
|
9
|
+
module FlexOps
|
|
10
|
+
module Resources
|
|
11
|
+
class Analytics
|
|
12
|
+
BASE = "/api/ApiProxy/api/v4/Analytics"
|
|
13
|
+
|
|
14
|
+
def initialize(http)
|
|
15
|
+
@http = http
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def shipments_trend(start_date: nil, end_date: nil)
|
|
19
|
+
@http.get("#{BASE}/ShipmentsTrend", query: date_query(start_date, end_date))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def carrier_summary(start_date: nil, end_date: nil)
|
|
23
|
+
@http.get("#{BASE}/CarrierSummary", query: date_query(start_date, end_date))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def top_destinations(start_date: nil, end_date: nil, limit: nil)
|
|
27
|
+
@http.get("#{BASE}/TopDestinations", query: date_query(start_date, end_date).merge(limit: limit).compact)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def inventory_metrics
|
|
31
|
+
@http.get("#{BASE}/InventoryMetrics")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def stock_by_warehouse
|
|
35
|
+
@http.get("#{BASE}/StockByWarehouse")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def order_metrics(start_date: nil, end_date: nil)
|
|
39
|
+
@http.get("#{BASE}/OrderMetrics", query: date_query(start_date, end_date))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def order_trend(start_date: nil, end_date: nil)
|
|
43
|
+
@http.get("#{BASE}/OrderTrend", query: date_query(start_date, end_date))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def top_selling_products(start_date: nil, end_date: nil, limit: nil)
|
|
47
|
+
@http.get("#{BASE}/TopSellingProducts", query: date_query(start_date, end_date).merge(limit: limit).compact)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def returns_metrics(start_date: nil, end_date: nil)
|
|
51
|
+
@http.get("#{BASE}/ReturnsMetrics", query: date_query(start_date, end_date))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def returns_trend(start_date: nil, end_date: nil)
|
|
55
|
+
@http.get("#{BASE}/ReturnsTrend", query: date_query(start_date, end_date))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def return_reasons(start_date: nil, end_date: nil)
|
|
59
|
+
@http.get("#{BASE}/ReturnReasons", query: date_query(start_date, end_date))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def performance_metrics(start_date: nil, end_date: nil)
|
|
63
|
+
@http.get("#{BASE}/PerformanceMetrics", query: date_query(start_date, end_date))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def carrier_performance(start_date: nil, end_date: nil)
|
|
67
|
+
@http.get("#{BASE}/CarrierPerformance", query: date_query(start_date, end_date))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def shipping_cost_analytics(start_date: nil, end_date: nil)
|
|
71
|
+
@http.get("#{BASE}/ShippingCostAnalytics", query: date_query(start_date, end_date))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def delivery_performance(start_date: nil, end_date: nil)
|
|
75
|
+
@http.get("#{BASE}/DeliveryPerformance", query: date_query(start_date, end_date))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def date_query(start_date, end_date)
|
|
81
|
+
{ startDate: start_date, endDate: end_date }.compact
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# ***********************************************************************
|
|
2
|
+
# Package : flexops
|
|
3
|
+
# Author : FlexOps, LLC
|
|
4
|
+
# Created : 2026-03-08
|
|
5
|
+
#
|
|
6
|
+
# Copyright (c) 2021-2026 by FlexOps, LLC. All rights reserved.
|
|
7
|
+
# ***********************************************************************
|
|
8
|
+
|
|
9
|
+
module FlexOps
|
|
10
|
+
module Resources
|
|
11
|
+
class ApiKeys
|
|
12
|
+
def initialize(http, ws_id_proc)
|
|
13
|
+
@http = http
|
|
14
|
+
@ws_id = ws_id_proc
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def list
|
|
18
|
+
@http.get("#{ws_path}/api-keys")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create(request)
|
|
22
|
+
@http.post("#{ws_path}/api-keys", body: request)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def revoke(key_id)
|
|
26
|
+
@http.delete("#{ws_path}/api-keys/#{key_id}")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def rotate(key_id)
|
|
30
|
+
@http.post("#{ws_path}/api-keys/#{key_id}/rotate")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def ws_path
|
|
36
|
+
"/api/workspaces/#{@ws_id.call}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# ***********************************************************************
|
|
2
|
+
# Package : flexops
|
|
3
|
+
# Author : FlexOps, LLC
|
|
4
|
+
# Created : 2026-03-08
|
|
5
|
+
#
|
|
6
|
+
# Copyright (c) 2021-2026 by FlexOps, LLC. All rights reserved.
|
|
7
|
+
# ***********************************************************************
|
|
8
|
+
|
|
9
|
+
module FlexOps
|
|
10
|
+
module Resources
|
|
11
|
+
class Auth
|
|
12
|
+
def initialize(http)
|
|
13
|
+
@http = http
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def login(email:, password:)
|
|
17
|
+
@http.post("/api/Account/login", body: { email: email, password: password })
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def register(request)
|
|
21
|
+
@http.post("/api/Account/register", body: request)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def refresh_token(refresh_token)
|
|
25
|
+
@http.post("/api/Account/refresh-token", body: { refreshToken: refresh_token })
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def logout
|
|
29
|
+
@http.post("/api/Account/logout")
|
|
30
|
+
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
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|