flexops 2.2.0 → 2.3.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 +22 -6
- data/lib/flexops/http.rb +2 -2
- data/lib/flexops/resources/analytics.rb +21 -0
- data/lib/flexops/resources/carriers.rb +27 -4
- data/lib/flexops/resources/inventory.rb +4 -0
- data/lib/flexops/resources/orders.rb +4 -0
- data/lib/flexops/resources/pickups.rb +3 -0
- data/lib/flexops/resources/returns.rb +3 -1
- data/lib/flexops/resources/rules.rb +5 -0
- data/lib/flexops/resources/scan_forms.rb +45 -1
- data/lib/flexops/resources/shipping.rb +6 -0
- data/lib/flexops/resources/webhooks.rb +40 -0
- data/lib/flexops/resources/workspaces.rb +3 -0
- data/lib/flexops/scan_form_state.rb +31 -0
- data/lib/flexops/version.rb +1 -1
- data/lib/flexops.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 65a0d6f9908c2d73479d5a3b2987667357ffadb0849abd7091f2c56aa37ce3be
|
|
4
|
+
data.tar.gz: f1fcc33bd4e7dd7d1c46a181c04256f1a080aba0f9aaca008e2180d77872a05a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 789eeda78864db092ec75eba25d613c43dc2e9914976ed030784df0f854ccd3fa07d0c3fd7f028c51d5acb7a3b2c1a618a5d27a4b249557d5313251203a67045
|
|
7
|
+
data.tar.gz: 5f1b2001938eb3d2ed9fe3b5ad97eaca4b558a5e74fca674740c3d3c993003f8cfb90296723f7b4a58677e33d7bc92c55e2ffe25958780789b40a8aebfdcc932
|
data/README.md
CHANGED
|
@@ -82,7 +82,7 @@ Keep the same key and request across retries. An uncertain outcome needs reconci
|
|
|
82
82
|
do not start another purchase with a new key. An expired approval returns 409
|
|
83
83
|
`ApprovalExpired`; obtain and explicitly approve a fresh preview when no purchase is
|
|
84
84
|
unresolved. The ceiling bounds postage authorization, not later adjustments or separate
|
|
85
|
-
fees. Sandbox
|
|
85
|
+
fees. Sandbox keys follow the same approval and buy no postage; batch, return, and raw carrier routes have
|
|
86
86
|
separate contracts. The SDK never automatically confirms a preview.
|
|
87
87
|
|
|
88
88
|
## Authentication
|
|
@@ -115,8 +115,11 @@ client = FlexOps::Client.new(api_key: "test_...", workspace_id: "your-workspace-
|
|
|
115
115
|
`client.carriers.usps`, `.ups`, `.fedex` and `.dhl` call carrier routes through the legacy
|
|
116
116
|
`/api/ApiProxy` passthrough, which the Gateway retires on 2027-06-30.
|
|
117
117
|
|
|
118
|
-
- Lookups (GET), such as address checks, tracking and DHL rates, work with
|
|
119
|
-
|
|
118
|
+
- Lookups (GET), such as address checks, tracking and DHL rates, work with a user token, or an API
|
|
119
|
+
key that has the permission for the data: `shipping:read` for address, ZIP, city, location,
|
|
120
|
+
service point and reference lookups; `tracking:read` for tracking and proof of delivery;
|
|
121
|
+
`rates:read` for delivery standards, rates and products. Without it the Gateway returns 403
|
|
122
|
+
`API_KEY_SCOPE_REQUIRED`.
|
|
120
123
|
- The writes (POST, PUT and DELETE, such as `usps.get_domestic_rates`, `fedex.cancel_shipment`
|
|
121
124
|
or `ups.create_pickup`) are deprecated. They need a user token from `auth.login`: API keys get
|
|
122
125
|
403 `API_KEY_SCOPE_REQUIRED`, and no API-key scope will be added for them. They stop working when
|
|
@@ -248,7 +251,14 @@ transaction history are managed in the FlexOps portal.
|
|
|
248
251
|
|
|
249
252
|
## Webhook subscriptions
|
|
250
253
|
|
|
254
|
+
Managing webhooks needs a signed-in user's access token. The Gateway refuses API keys on every webhook route, reads included (`list` and `list_delivery_logs` as well as `create`, `update`, `delete`, `rotate_secret` and `test`), with 403 `SIGNED_IN_USER_REQUIRED`, whatever the key's scopes; the SDK raises `FlexOps::Error` with that `code`. Reads are refused too because webhook delivery logs carry each delivery's payload (order and customer data). A client with an API key sends the key even when an access token is also set, so use a client created without `api_key`. Verifying deliveries (below) makes no request and needs no credentials.
|
|
255
|
+
|
|
251
256
|
```ruby
|
|
257
|
+
# Sign in as a user (or pass access_token:); an API key gets 403 SIGNED_IN_USER_REQUIRED here.
|
|
258
|
+
client = FlexOps::Client.new(workspace_id: "your-workspace-id")
|
|
259
|
+
session = client.auth.login(email: "user@example.com", password: "password")
|
|
260
|
+
client.set_access_token(session["data"]["token"])
|
|
261
|
+
|
|
252
262
|
# Leave events out to receive every event.
|
|
253
263
|
webhook = client.webhooks.create(
|
|
254
264
|
url: "https://your-app.com/webhooks/flexops",
|
|
@@ -364,9 +374,9 @@ Use a `test_...` key instead of `live_...` to hit the sandbox environment; mock
|
|
|
364
374
|
| `client.wallet` | Workspace balance |
|
|
365
375
|
| `client.insurance` | Providers, quotes, policies, void, claims |
|
|
366
376
|
| `client.returns` | List and read received returns; create, read and list RMAs; return labels; receive |
|
|
367
|
-
| `client.analytics` | Shipments, orders, carrier performance (legacy proxy reads) |
|
|
368
|
-
| `client.orders` | Order reads (legacy proxy) |
|
|
369
|
-
| `client.inventory` | Inventory reads (legacy proxy) |
|
|
377
|
+
| `client.analytics` | Shipments, orders, carrier performance (legacy proxy reads; API keys need each method's read permission) |
|
|
378
|
+
| `client.orders` | Order reads (legacy proxy; API keys need `orders:read`) |
|
|
379
|
+
| `client.inventory` | Inventory reads (legacy proxy; API keys need `inventory:read`) |
|
|
370
380
|
| `client.pickups` | Schedule, list, read and cancel carrier pickups |
|
|
371
381
|
| `client.scan_forms` | USPS scan forms |
|
|
372
382
|
| `client.rules` | Shipping rules: create, read, update, delete, reorder, test |
|
|
@@ -376,6 +386,12 @@ Use a `test_...` key instead of `live_...` to hit the sandbox environment; mock
|
|
|
376
386
|
| `client.report_schedules` | Scheduled report emails: create, update, toggle, delete |
|
|
377
387
|
| `client.email_templates` | Tracking email templates: upsert, preview, delete |
|
|
378
388
|
|
|
389
|
+
`client.analytics`, `client.orders` and `client.inventory` read through the legacy proxy. With an
|
|
390
|
+
API key they need the permission for the data they read: `orders:read` for orders,
|
|
391
|
+
`inventory:read` for inventory, and for analytics the permission named on each method
|
|
392
|
+
(`shipping:read`, `inventory:read`, `orders:read` or `returns:read`). A key without it gets 403
|
|
393
|
+
`API_KEY_SCOPE_REQUIRED`. Keys created with the default permissions have all of these.
|
|
394
|
+
|
|
379
395
|
API keys, workspace settings and account management are managed in the FlexOps portals.
|
|
380
396
|
|
|
381
397
|
## Configuration
|
data/lib/flexops/http.rb
CHANGED
|
@@ -61,8 +61,8 @@ module FlexOps
|
|
|
61
61
|
request(:patch, path, body: body, headers: headers)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
def delete(path, headers: {})
|
|
65
|
-
request(:delete, path, headers: headers)
|
|
64
|
+
def delete(path, query: nil, headers: {})
|
|
65
|
+
request(:delete, path, query: query, headers: headers)
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
private
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
module FlexOps
|
|
10
10
|
module Resources
|
|
11
|
+
# Analytics through the legacy /api/ApiProxy passthrough (/api/ApiProxy/api/v4/Analytics/...).
|
|
12
|
+
# With an API key, each method requires the permission for the data it reads, named on the
|
|
13
|
+
# method: shipping:read for shipment, carrier and delivery metrics, inventory:read for
|
|
14
|
+
# inventory metrics, orders:read for order metrics and returns:read for returns metrics. A
|
|
15
|
+
# key without it gets 403 API_KEY_SCOPE_REQUIRED. Keys created with the default permissions
|
|
16
|
+
# have all four.
|
|
11
17
|
class Analytics
|
|
12
18
|
BASE = "/api/ApiProxy/api/v4/Analytics"
|
|
13
19
|
|
|
@@ -15,62 +21,77 @@ module FlexOps
|
|
|
15
21
|
@http = http
|
|
16
22
|
end
|
|
17
23
|
|
|
24
|
+
# Shipments trend over time. With an API key, requires shipping:read.
|
|
18
25
|
def shipments_trend(start_date: nil, end_date: nil)
|
|
19
26
|
@http.get("#{BASE}/ShipmentsTrend", query: date_query(start_date, end_date))
|
|
20
27
|
end
|
|
21
28
|
|
|
29
|
+
# Carrier usage summary. With an API key, requires shipping:read.
|
|
22
30
|
def carrier_summary(start_date: nil, end_date: nil)
|
|
23
31
|
@http.get("#{BASE}/CarrierSummary", query: date_query(start_date, end_date))
|
|
24
32
|
end
|
|
25
33
|
|
|
34
|
+
# Top shipping destinations. With an API key, requires shipping:read.
|
|
26
35
|
def top_destinations(start_date: nil, end_date: nil, limit: nil)
|
|
27
36
|
@http.get("#{BASE}/TopDestinations", query: date_query(start_date, end_date).merge(limit: limit).compact)
|
|
28
37
|
end
|
|
29
38
|
|
|
39
|
+
# Inventory metrics (stock levels, low-stock alerts). With an API key, requires inventory:read.
|
|
30
40
|
def inventory_metrics
|
|
31
41
|
@http.get("#{BASE}/InventoryMetrics")
|
|
32
42
|
end
|
|
33
43
|
|
|
44
|
+
# Stock levels by warehouse. With an API key, requires inventory:read.
|
|
34
45
|
def stock_by_warehouse
|
|
35
46
|
@http.get("#{BASE}/StockByWarehouse")
|
|
36
47
|
end
|
|
37
48
|
|
|
49
|
+
# Order metrics (volume, revenue). With an API key, requires orders:read.
|
|
38
50
|
def order_metrics(start_date: nil, end_date: nil)
|
|
39
51
|
@http.get("#{BASE}/OrderMetrics", query: date_query(start_date, end_date))
|
|
40
52
|
end
|
|
41
53
|
|
|
54
|
+
# Order trend over time. With an API key, requires orders:read.
|
|
42
55
|
def order_trend(start_date: nil, end_date: nil)
|
|
43
56
|
@http.get("#{BASE}/OrderTrend", query: date_query(start_date, end_date))
|
|
44
57
|
end
|
|
45
58
|
|
|
59
|
+
# Top selling products. With an API key, requires orders:read.
|
|
46
60
|
def top_selling_products(start_date: nil, end_date: nil, limit: nil)
|
|
47
61
|
@http.get("#{BASE}/TopSellingProducts", query: date_query(start_date, end_date).merge(limit: limit).compact)
|
|
48
62
|
end
|
|
49
63
|
|
|
64
|
+
# Returns metrics. With an API key, requires returns:read.
|
|
50
65
|
def returns_metrics(start_date: nil, end_date: nil)
|
|
51
66
|
@http.get("#{BASE}/ReturnsMetrics", query: date_query(start_date, end_date))
|
|
52
67
|
end
|
|
53
68
|
|
|
69
|
+
# Returns trend over time. With an API key, requires returns:read.
|
|
54
70
|
def returns_trend(start_date: nil, end_date: nil)
|
|
55
71
|
@http.get("#{BASE}/ReturnsTrend", query: date_query(start_date, end_date))
|
|
56
72
|
end
|
|
57
73
|
|
|
74
|
+
# Return reasons breakdown. With an API key, requires returns:read.
|
|
58
75
|
def return_reasons(start_date: nil, end_date: nil)
|
|
59
76
|
@http.get("#{BASE}/ReturnReasons", query: date_query(start_date, end_date))
|
|
60
77
|
end
|
|
61
78
|
|
|
79
|
+
# Fulfillment performance metrics. With an API key, requires shipping:read.
|
|
62
80
|
def performance_metrics(start_date: nil, end_date: nil)
|
|
63
81
|
@http.get("#{BASE}/PerformanceMetrics", query: date_query(start_date, end_date))
|
|
64
82
|
end
|
|
65
83
|
|
|
84
|
+
# Carrier delivery performance. With an API key, requires shipping:read.
|
|
66
85
|
def carrier_performance(start_date: nil, end_date: nil)
|
|
67
86
|
@http.get("#{BASE}/CarrierPerformance", query: date_query(start_date, end_date))
|
|
68
87
|
end
|
|
69
88
|
|
|
89
|
+
# Shipping cost analytics. With an API key, requires shipping:read.
|
|
70
90
|
def shipping_cost_analytics(start_date: nil, end_date: nil)
|
|
71
91
|
@http.get("#{BASE}/ShippingCostAnalytics", query: date_query(start_date, end_date))
|
|
72
92
|
end
|
|
73
93
|
|
|
94
|
+
# Delivery performance (on-time percentage). With an API key, requires shipping:read.
|
|
74
95
|
def delivery_performance(start_date: nil, end_date: nil)
|
|
75
96
|
@http.get("#{BASE}/DeliveryPerformance", query: date_query(start_date, end_date))
|
|
76
97
|
end
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
module FlexOps
|
|
10
10
|
module Resources
|
|
11
11
|
# Carrier-specific calls through the legacy /api/ApiProxy passthrough (sunset 2027-06-30).
|
|
12
|
-
# Lookups (GET) work with an API key
|
|
12
|
+
# Lookups (GET) work with a user token, or an API key that has the permission for the data
|
|
13
|
+
# they read: address, ZIP, city, location, service point and reference lookups require
|
|
14
|
+
# shipping:read; tracking and proof of delivery require tracking:read; delivery standards,
|
|
15
|
+
# rates and products require rates:read. A key without the permission gets 403
|
|
16
|
+
# API_KEY_SCOPE_REQUIRED; keys created with the default permissions have all three.
|
|
17
|
+
# The writes (POST/PUT/DELETE) are
|
|
13
18
|
# deprecated and need a user token (auth.login): the Gateway refuses API-key writes on the
|
|
14
19
|
# proxy with 403 API_KEY_SCOPE_REQUIRED. Label generation through the proxy is closed (410 Gone);
|
|
15
20
|
# buy, cancel and schedule through client.shipping, client.pickups and client.scan_forms.
|
|
@@ -25,6 +30,9 @@ module FlexOps
|
|
|
25
30
|
end
|
|
26
31
|
end
|
|
27
32
|
|
|
33
|
+
# USPS through the legacy proxy. With an API key, the address, ZIP, city and location lookups
|
|
34
|
+
# require shipping:read, tracking requires tracking:read and delivery standards require
|
|
35
|
+
# rates:read.
|
|
28
36
|
class UspsCarrier
|
|
29
37
|
PROXY = "/api/ApiProxy"
|
|
30
38
|
|
|
@@ -52,10 +60,12 @@ module FlexOps
|
|
|
52
60
|
def get_international_prices(body) = @http.post("#{PROXY}/api/v3/RateCalculator/postUspsSearchEligibleInternationalPrices", body: body)
|
|
53
61
|
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
54
62
|
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
55
|
-
def cancel_domestic_label =
|
|
63
|
+
def cancel_domestic_label(tracking_number) = cancel("/api/v3/Shipping/cancelUspsDomesticShipmentLabel", "trackingNumber", tracking_number)
|
|
56
64
|
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
57
65
|
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
58
|
-
def cancel_international_label
|
|
66
|
+
def cancel_international_label(international_tracking_number)
|
|
67
|
+
cancel("/api/v3/Shipping/cancelUspsInternationalShipmentLabel", "internationalTrackingNumber", international_tracking_number)
|
|
68
|
+
end
|
|
59
69
|
def track_summary(params) = @http.get("#{PROXY}/api/v3/Tracking/getUspsTrackingSummaryInformation", query: params)
|
|
60
70
|
def track_detail(params) = @http.get("#{PROXY}/api/v3/Tracking/getUspsTrackingDetailInformation", query: params)
|
|
61
71
|
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
@@ -63,15 +73,25 @@ module FlexOps
|
|
|
63
73
|
def create_pickup(body) = @http.post("#{PROXY}/api/v3/CarrierPickup/postUspsCreateCarrierPickupSchedule", body: body)
|
|
64
74
|
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
65
75
|
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
66
|
-
def cancel_pickup =
|
|
76
|
+
def cancel_pickup(confirmation_number) = cancel("/api/v3/CarrierPickup/cancelUspsCarrierPickupSchedule", "confirmationNumber", confirmation_number)
|
|
67
77
|
# @deprecated Legacy carrier proxy (sunset 2027-06-30). Needs a user token: API keys get 403
|
|
68
78
|
# API_KEY_SCOPE_REQUIRED. Use client.shipping / client.pickups / client.scan_forms where they cover this.
|
|
69
79
|
def create_scan_form(body) = @http.post("#{PROXY}/api/v3/ScanForm/postUspsCreateScanFormLabelShipment", body: body)
|
|
70
80
|
def delivery_standards(params) = @http.get("#{PROXY}/api/v3/ServiceStandards/getUspsGetDeliveryStandardsEstimates", query: params)
|
|
71
81
|
def find_drop_off_locations(params) = @http.get("#{PROXY}/api/v3/LocationSearch/getUspsFindValidDropOffLocations", query: params)
|
|
72
82
|
def find_post_offices(params) = @http.get("#{PROXY}/api/v3/LocationSearch/getUspsFindValidPostOfficeLocations", query: params)
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Sends a USPS cancel with what it cancels in the query string; without it VSCS answers 400.
|
|
87
|
+
def cancel(path, param, value)
|
|
88
|
+
raise ArgumentError, "#{param} is required." if value.to_s.strip.empty?
|
|
89
|
+
|
|
90
|
+
@http.delete("#{PROXY}#{path}", query: { param => value })
|
|
91
|
+
end
|
|
73
92
|
end
|
|
74
93
|
|
|
94
|
+
# UPS through the legacy proxy. With an API key, track requires tracking:read.
|
|
75
95
|
class UpsCarrier
|
|
76
96
|
PROXY = "/api/ApiProxy"
|
|
77
97
|
|
|
@@ -178,6 +198,9 @@ module FlexOps
|
|
|
178
198
|
def confirm_open_shipment(body) = @http.post("#{PROXY}/api/v3/OpenShip/postFedExConfirmOpenShipment", body: body)
|
|
179
199
|
end
|
|
180
200
|
|
|
201
|
+
# DHL through the legacy proxy. With an API key, address validation, service points and
|
|
202
|
+
# reference data require shipping:read; tracking and proof of delivery require
|
|
203
|
+
# tracking:read; rates and products require rates:read.
|
|
181
204
|
class DhlCarrier
|
|
182
205
|
PROXY = "/api/ApiProxy"
|
|
183
206
|
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
module FlexOps
|
|
10
10
|
module Resources
|
|
11
|
+
# Read-only inventory lookups through the legacy /api/ApiProxy passthrough
|
|
12
|
+
# (/api/ApiProxy/api/v1/Inventory/...). With an API key, every method requires the
|
|
13
|
+
# inventory:read permission; a key without it gets 403 API_KEY_SCOPE_REQUIRED. Keys created
|
|
14
|
+
# with the default permissions include inventory:read. The proxy refuses writes from API keys.
|
|
11
15
|
class Inventory
|
|
12
16
|
PROXY = "/api/ApiProxy"
|
|
13
17
|
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
module FlexOps
|
|
10
10
|
module Resources
|
|
11
|
+
# Read-only order lookups through the legacy /api/ApiProxy passthrough
|
|
12
|
+
# (/api/ApiProxy/api/v1/Order/...). With an API key, every method requires the orders:read
|
|
13
|
+
# permission; a key without it gets 403 API_KEY_SCOPE_REQUIRED. Keys created with the default
|
|
14
|
+
# permissions include orders:read. The proxy refuses writes from API keys.
|
|
11
15
|
class Orders
|
|
12
16
|
BASE = "/api/ApiProxy/api/v1/Order"
|
|
13
17
|
|
|
@@ -16,6 +16,8 @@ module FlexOps
|
|
|
16
16
|
@ws_id = ws_id_proc
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# Schedules a carrier pickup. API keys need shipping:write; the public demo keys are refused.
|
|
20
|
+
#
|
|
19
21
|
# request:: { carrierCode:, pickupDate:, street:, city:, state:, postalCode:,
|
|
20
22
|
# country: (optional, "US"), packageCount:, totalWeightOz: (optional),
|
|
21
23
|
# specialInstructions:, readyTime:, closeTime:, contactName:, contactPhone: (all optional) }
|
|
@@ -33,6 +35,7 @@ module FlexOps
|
|
|
33
35
|
@http.get("#{ws_path}/pickups/#{pickup_id}", query: carrier_code.nil? ? nil : { carrierCode: carrier_code })
|
|
34
36
|
end
|
|
35
37
|
|
|
38
|
+
# Cancels a scheduled pickup. API keys need shipping:write; the public demo keys are refused.
|
|
36
39
|
# The Gateway uses "usps" when carrier_code is left out.
|
|
37
40
|
def cancel(pickup_id, carrier_code: nil)
|
|
38
41
|
query = carrier_code.nil? ? "" : "?#{URI.encode_www_form(carrierCode: carrier_code)}"
|
|
@@ -57,7 +57,9 @@ module FlexOps
|
|
|
57
57
|
# missing fields.
|
|
58
58
|
#
|
|
59
59
|
# Only what you set is sent; the Gateway uses "usps" when carrier_code is left out.
|
|
60
|
-
# hazmat_classification (a HazmatClassification) declares what the return holds.
|
|
60
|
+
# hazmat_classification (a HazmatClassification) declares what the return holds. Return labels
|
|
61
|
+
# cannot carry HAZMAT yet: send HazmatClassification::NONE or leave it out (any other class is
|
|
62
|
+
# refused with 400 HazmatNotSupported).
|
|
61
63
|
def generate_label(rma_number, weight_oz: nil, length_in: nil, width_in: nil, height_in: nil,
|
|
62
64
|
carrier_code: nil, service_type: nil, payment_confirmed: nil, hazmat_classification: nil,
|
|
63
65
|
rate_indicator: nil)
|
|
@@ -24,21 +24,25 @@ module FlexOps
|
|
|
24
24
|
|
|
25
25
|
# rule:: { name:, priority:, isEnabled:, warehouseId: (optional),
|
|
26
26
|
# conditions: [{ field:, operator:, value:, group: (optional) }], actions: [{ type:, value: }] }
|
|
27
|
+
# Signed-in users only: API keys get 403 SIGNED_IN_USER_REQUIRED, whatever their scopes.
|
|
27
28
|
def create(rule)
|
|
28
29
|
@http.post("#{ws_path}/shipping-rules", body: rule)
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
# rule:: the fields to change, named as for #create.
|
|
33
|
+
# Signed-in users only: API keys get 403 SIGNED_IN_USER_REQUIRED, whatever their scopes.
|
|
32
34
|
def update(rule_id, rule)
|
|
33
35
|
@http.put("#{ws_path}/shipping-rules/#{rule_id}", body: rule)
|
|
34
36
|
end
|
|
35
37
|
|
|
38
|
+
# Signed-in users only: API keys get 403 SIGNED_IN_USER_REQUIRED, whatever their scopes.
|
|
36
39
|
def delete(rule_id)
|
|
37
40
|
@http.delete("#{ws_path}/shipping-rules/#{rule_id}")
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
# Sets rule priorities. +rules+ is an array of { ruleId:, priority: } hashes, or of rule ids
|
|
41
44
|
# in the order to apply (the first gets priority 0).
|
|
45
|
+
# Signed-in users only: API keys get 403 SIGNED_IN_USER_REQUIRED, whatever their scopes.
|
|
42
46
|
def reorder(rules)
|
|
43
47
|
items = rules.each_with_index.map do |rule, index|
|
|
44
48
|
rule.is_a?(Hash) ? rule : { ruleId: rule, priority: index }
|
|
@@ -47,6 +51,7 @@ module FlexOps
|
|
|
47
51
|
end
|
|
48
52
|
|
|
49
53
|
# Evaluates the workspace's rules against a sample shipment without changing anything.
|
|
54
|
+
# API keys need shipping:read; the public demo keys are refused.
|
|
50
55
|
#
|
|
51
56
|
# request:: { weightOz:, declaredValue:, destinationCountry:, destinationState:,
|
|
52
57
|
# destinationPostalCode:, orderSource:, itemCount:, shipMethod:, destinationZone:, tags: }
|
|
@@ -6,22 +6,66 @@
|
|
|
6
6
|
# Copyright (c) 2021-2026 by FlexOps, LLC. All rights reserved.
|
|
7
7
|
# ***********************************************************************
|
|
8
8
|
|
|
9
|
+
require "securerandom"
|
|
10
|
+
|
|
9
11
|
module FlexOps
|
|
10
12
|
module Resources
|
|
13
|
+
# USPS SCAN forms. Replies are the Gateway envelope as a Hash with string keys:
|
|
14
|
+
# { "success" => true, "message" => nil, "data" => form, "errors" => nil, "errorCode" => nil }.
|
|
15
|
+
# A form (the Gateway's ScanFormDto) has:
|
|
16
|
+
#
|
|
17
|
+
# "scanFormId":: 32 hexadecimal digits, no dashes; pass it to #get.
|
|
18
|
+
# "state":: FlexOps::ScanFormState::CREATED or OUTCOME_UNKNOWN.
|
|
19
|
+
# "originPostalCode":: the origin ZIP, or nil.
|
|
20
|
+
# "trackingNumbers":: the packages on the form.
|
|
21
|
+
# "createdAtUtc":: ISO 8601 in UTC, for example "2026-09-08T21:14:03.512Z".
|
|
22
|
+
# "scanFormImage":: the PS Form 5630 PDF, base64; nil for sandbox forms and unresolved
|
|
23
|
+
# operations. FlexOps::ScanFormState.pdf_bytes decodes it.
|
|
24
|
+
# "manifestNumber":: the USPS manifest number; "SBOX..." in the sandbox, "" while unresolved.
|
|
25
|
+
# "manifestDate":: the mailing date, for example "2026-09-08T00:00:00".
|
|
26
|
+
# "packageCount":: how many packages the form covers.
|
|
27
|
+
# "statusMessage":: a status line for people.
|
|
28
|
+
# "success":: true when "state" is "Created".
|
|
29
|
+
# "errorMessage":: not set by the current Gateway; refusals raise FlexOps::Error.
|
|
30
|
+
# "isSandbox":: true for a sandbox (test key) form, never sent to USPS.
|
|
11
31
|
class ScanForms
|
|
12
32
|
def initialize(http, ws_id_proc)
|
|
13
33
|
@http = http
|
|
14
34
|
@ws_id = ws_id_proc
|
|
15
35
|
end
|
|
16
36
|
|
|
37
|
+
# Creates a USPS SCAN form. API keys need shipping:write; the public demo keys are refused.
|
|
38
|
+
#
|
|
39
|
+
# request:: { trackingNumbers: [...] (1 to 1,000), manifestDate: "YYYY-MM-DD",
|
|
40
|
+
# requestId: (optional), originPostalCode: (optional), warehouseId: (optional) }
|
|
41
|
+
#
|
|
42
|
+
# Live creation needs manifestDate: without it the Gateway answers 400 "Choose the label
|
|
43
|
+
# mailing date." The sandbox defaults it to today (UTC).
|
|
44
|
+
#
|
|
45
|
+
# requestId identifies the operation. Repeating a request with the same ID returns the same
|
|
46
|
+
# form instead of creating another; reusing it with different inputs is 409. When it is left
|
|
47
|
+
# out, one is generated for this call and sent on every retry of it, so a retry cannot create
|
|
48
|
+
# a second form. Pass your own to retry the call yourself after an unknown outcome.
|
|
49
|
+
#
|
|
50
|
+
# Returns the envelope whose "data" is the created form, also when the same requestId and
|
|
51
|
+
# inputs are sent again. Raises FlexOps::Error with status 409 when the requestId was used
|
|
52
|
+
# with different inputs, when the packages already belong to a manifest, or when this or an
|
|
53
|
+
# earlier operation needs review (its message names the operation; do not resubmit those
|
|
54
|
+
# packages), and 400 when the request or package selection is refused. A 409 is not retried.
|
|
17
55
|
def create(request)
|
|
18
|
-
|
|
56
|
+
given = request[:requestId] || request["requestId"]
|
|
57
|
+
body = given ? request : request.reject { |key, _| key.to_s == "requestId" }.merge(requestId: SecureRandom.uuid)
|
|
58
|
+
@http.post("#{ws_path}/scan-forms", body: body)
|
|
19
59
|
end
|
|
20
60
|
|
|
61
|
+
# Lists the workspace's saved scan forms ("data" is an Array of forms), newest mailing date
|
|
62
|
+
# first, including any whose outcome is unknown.
|
|
21
63
|
def list
|
|
22
64
|
@http.get("#{ws_path}/scan-forms")
|
|
23
65
|
end
|
|
24
66
|
|
|
67
|
+
# Gets a saved scan form by its "scanFormId". A form that does not exist raises
|
|
68
|
+
# FlexOps::Error with status 404.
|
|
25
69
|
def get(scan_form_id)
|
|
26
70
|
@http.get("#{ws_path}/scan-forms/#{scan_form_id}")
|
|
27
71
|
end
|
|
@@ -92,10 +92,16 @@ module FlexOps
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
# The payload is sent as given. Set hazmatClassification on an item only when you have a declaration.
|
|
95
|
+
# Batch labels cannot carry HAZMAT yet: send HazmatClassification::NONE or leave hazmatClassification
|
|
96
|
+
# out (any other class is refused with 400 HazmatNotSupported). Buy HAZMAT labels one at a time
|
|
97
|
+
# with create_label.
|
|
95
98
|
def create_batch(request)
|
|
96
99
|
@http.post("#{ws_path}/labels/batch", body: request)
|
|
97
100
|
end
|
|
98
101
|
|
|
102
|
+
# Previews a batch without buying labels. The payload is sent as given; as with create_batch, an
|
|
103
|
+
# item's hazmatClassification accepts only HazmatClassification::NONE or nothing (any other class
|
|
104
|
+
# is refused with 400 HazmatNotSupported).
|
|
99
105
|
def preview_batch(request)
|
|
100
106
|
@http.post("#{ws_path}/labels/batch/preview", body: request)
|
|
101
107
|
end
|
|
@@ -10,12 +10,29 @@ require "openssl"
|
|
|
10
10
|
|
|
11
11
|
module FlexOps
|
|
12
12
|
module Resources
|
|
13
|
+
# Webhook subscription management and signature verification.
|
|
14
|
+
#
|
|
15
|
+
# Every method that calls the Gateway needs a signed-in user's access token
|
|
16
|
+
# (Client.new(access_token: ...) or Client#set_access_token with the token from
|
|
17
|
+
# auth.login). The Gateway refuses API keys on every webhook route, reads included,
|
|
18
|
+
# with 403 SIGNED_IN_USER_REQUIRED, whatever the key's scopes; the SDK raises
|
|
19
|
+
# FlexOps::Error with that +code+. Reads are refused too because webhook delivery
|
|
20
|
+
# logs carry each delivery's payload (order and customer data). A client with an API
|
|
21
|
+
# key sends the key even when an access token is also set, so call these methods
|
|
22
|
+
# from a client created without +api_key+. verify_signature and verify_signature!
|
|
23
|
+
# make no request and need no credentials.
|
|
13
24
|
class Webhooks
|
|
14
25
|
def initialize(http, ws_id_proc)
|
|
15
26
|
@http = http
|
|
16
27
|
@ws_id = ws_id_proc
|
|
17
28
|
end
|
|
18
29
|
|
|
30
|
+
# Lists the workspace's webhook subscriptions.
|
|
31
|
+
#
|
|
32
|
+
# Needs a signed-in user's access token. With an API key the Gateway answers 403
|
|
33
|
+
# and this raises FlexOps::Error with code "SIGNED_IN_USER_REQUIRED". Reads are
|
|
34
|
+
# signed-in only, like the writes, because webhook delivery logs carry each
|
|
35
|
+
# delivery's payload (order and customer data).
|
|
19
36
|
def list
|
|
20
37
|
@http.get("#{ws_path}/webhooks")
|
|
21
38
|
end
|
|
@@ -35,6 +52,9 @@ module FlexOps
|
|
|
35
52
|
# "secret", ...), not a success/data envelope: the Gateway answers 201 with the
|
|
36
53
|
# bare subscription. An enveloped answer is unwrapped, so "secret" (the whsec_...
|
|
37
54
|
# signing secret, returned only here) is always at the top level.
|
|
55
|
+
#
|
|
56
|
+
# Needs a signed-in user's access token. With an API key the Gateway answers 403
|
|
57
|
+
# and this raises FlexOps::Error with code "SIGNED_IN_USER_REQUIRED".
|
|
38
58
|
def create(request)
|
|
39
59
|
unwrap_subscription(@http.post("#{ws_path}/webhooks", body: to_wire_request(request)))
|
|
40
60
|
end
|
|
@@ -42,23 +62,43 @@ module FlexOps
|
|
|
42
62
|
# Updates a webhook subscription. +data+ takes +url+, +description+, +status+
|
|
43
63
|
# ("Active", "Paused" or "Disabled") and +events+, which replaces the subscribed
|
|
44
64
|
# events and is sent as +eventTypes+. Raises ArgumentError as #create does.
|
|
65
|
+
#
|
|
66
|
+
# Needs a signed-in user's access token. With an API key the Gateway answers 403
|
|
67
|
+
# and this raises FlexOps::Error with code "SIGNED_IN_USER_REQUIRED".
|
|
45
68
|
def update(webhook_id, data)
|
|
46
69
|
@http.put("#{ws_path}/webhooks/#{webhook_id}", body: to_wire_request(data))
|
|
47
70
|
end
|
|
48
71
|
|
|
72
|
+
# Deletes a webhook subscription.
|
|
73
|
+
#
|
|
74
|
+
# Needs a signed-in user's access token. With an API key the Gateway answers 403
|
|
75
|
+
# and this raises FlexOps::Error with code "SIGNED_IN_USER_REQUIRED".
|
|
49
76
|
def delete(webhook_id)
|
|
50
77
|
@http.delete("#{ws_path}/webhooks/#{webhook_id}")
|
|
51
78
|
end
|
|
52
79
|
|
|
80
|
+
# Rotates the subscription's signing secret.
|
|
81
|
+
#
|
|
82
|
+
# Needs a signed-in user's access token. With an API key the Gateway answers 403
|
|
83
|
+
# and this raises FlexOps::Error with code "SIGNED_IN_USER_REQUIRED".
|
|
53
84
|
def rotate_secret(webhook_id)
|
|
54
85
|
@http.post("#{ws_path}/webhooks/#{webhook_id}/rotate-secret")
|
|
55
86
|
end
|
|
56
87
|
|
|
88
|
+
# Lists the subscription's delivery logs.
|
|
89
|
+
#
|
|
90
|
+
# Needs a signed-in user's access token. With an API key the Gateway answers 403
|
|
91
|
+
# and this raises FlexOps::Error with code "SIGNED_IN_USER_REQUIRED". Reads are
|
|
92
|
+
# signed-in only, like the writes, because webhook delivery logs carry each
|
|
93
|
+
# delivery's payload (order and customer data).
|
|
57
94
|
def list_delivery_logs(webhook_id)
|
|
58
95
|
@http.get("#{ws_path}/webhooks/#{webhook_id}/deliveries")
|
|
59
96
|
end
|
|
60
97
|
|
|
61
98
|
# Sends a test event (X-FlexOps-Event "test") to the subscription's URL.
|
|
99
|
+
#
|
|
100
|
+
# Needs a signed-in user's access token. With an API key the Gateway answers 403
|
|
101
|
+
# and this raises FlexOps::Error with code "SIGNED_IN_USER_REQUIRED".
|
|
62
102
|
def test(webhook_id)
|
|
63
103
|
@http.post("#{ws_path}/webhooks/#{webhook_id}/test")
|
|
64
104
|
end
|
|
@@ -29,16 +29,19 @@ module FlexOps
|
|
|
29
29
|
|
|
30
30
|
# Invites a member. +role+ is a FlexOps::WorkspaceRole number or name (for example
|
|
31
31
|
# :member); it is required because the Gateway reads a missing role as Owner.
|
|
32
|
+
# Signed-in users only: API keys get 403 SIGNED_IN_USER_REQUIRED, whatever their scopes.
|
|
32
33
|
def invite_member(email:, role:)
|
|
33
34
|
@http.post("/api/workspaces/#{@ws_id.call}/members/invite",
|
|
34
35
|
body: { email: email, role: FlexOps::WorkspaceRole.to_wire(role) })
|
|
35
36
|
end
|
|
36
37
|
|
|
38
|
+
# Signed-in users only: API keys get 403 SIGNED_IN_USER_REQUIRED, whatever their scopes.
|
|
37
39
|
def remove_member(user_id)
|
|
38
40
|
@http.delete("/api/workspaces/#{@ws_id.call}/members/#{user_id}")
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
# Changes a member's role; +role+ is a FlexOps::WorkspaceRole number or name.
|
|
44
|
+
# Signed-in users only: API keys get 403 SIGNED_IN_USER_REQUIRED, whatever their scopes.
|
|
42
45
|
def update_member_role(user_id, role)
|
|
43
46
|
@http.put("/api/workspaces/#{@ws_id.call}/members/#{user_id}/role",
|
|
44
47
|
body: { newRole: FlexOps::WorkspaceRole.to_wire(role) })
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# ***********************************************************************
|
|
2
|
+
# Package : flexops
|
|
3
|
+
# Author : FlexOps, LLC
|
|
4
|
+
# Created : 2026-10-24
|
|
5
|
+
#
|
|
6
|
+
# Copyright (c) 2021-2026 by FlexOps, LLC. All rights reserved.
|
|
7
|
+
# ***********************************************************************
|
|
8
|
+
|
|
9
|
+
module FlexOps
|
|
10
|
+
# The "state" of a SCAN form hash returned by client.scan_forms (the Gateway's ScanFormDto).
|
|
11
|
+
# The Gateway may add other values; treat them like OUTCOME_UNKNOWN.
|
|
12
|
+
module ScanFormState
|
|
13
|
+
# USPS returned the manifest, and the Gateway saved its number and PDF.
|
|
14
|
+
CREATED = "Created"
|
|
15
|
+
# The carrier's answer was lost or did not match. The packages stay reserved: do not submit
|
|
16
|
+
# them again until the outcome is resolved.
|
|
17
|
+
OUTCOME_UNKNOWN = "OutcomeUnknown"
|
|
18
|
+
|
|
19
|
+
# True when a SCAN form hash ("data" of create/get, or an item of list) is a created form.
|
|
20
|
+
def self.created?(form)
|
|
21
|
+
form.is_a?(Hash) && form["state"] == CREATED
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Decodes a SCAN form hash's "scanFormImage" (the base64 PS Form 5630 PDF), or returns nil when
|
|
25
|
+
# it has none: sandbox forms are never sent to USPS, and an unresolved operation has no PDF.
|
|
26
|
+
def self.pdf_bytes(form)
|
|
27
|
+
image = form.is_a?(Hash) ? form["scanFormImage"] : nil
|
|
28
|
+
image.nil? ? nil : image.unpack1("m0")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/flexops/version.rb
CHANGED
data/lib/flexops.rb
CHANGED
|
@@ -11,6 +11,7 @@ require_relative "flexops/errors"
|
|
|
11
11
|
require_relative "flexops/hazmat_classification"
|
|
12
12
|
require_relative "flexops/webhook_event_type"
|
|
13
13
|
require_relative "flexops/workspace_role"
|
|
14
|
+
require_relative "flexops/scan_form_state"
|
|
14
15
|
require_relative "flexops/http"
|
|
15
16
|
require_relative "flexops/client"
|
|
16
17
|
require_relative "flexops/resources/auth"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: flexops
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- FlexOps, LLC
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: json
|
|
@@ -87,6 +87,7 @@ files:
|
|
|
87
87
|
- lib/flexops/resources/wallet.rb
|
|
88
88
|
- lib/flexops/resources/webhooks.rb
|
|
89
89
|
- lib/flexops/resources/workspaces.rb
|
|
90
|
+
- lib/flexops/scan_form_state.rb
|
|
90
91
|
- lib/flexops/version.rb
|
|
91
92
|
- lib/flexops/webhook_event_type.rb
|
|
92
93
|
- lib/flexops/workspace_role.rb
|