flexops 2.0.0 → 2.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 99bb604e4e35131d7525633fbfad7103b380170556b47250599192256636eec1
4
- data.tar.gz: b9fb3ccd224541332bd03fd9e8510937fbed4d093c6ab3443dfd41fee0565229
3
+ metadata.gz: 7266826af89b91d17327ba0dda3a7318081657cd7d63805be1c7a2665ffcc69c
4
+ data.tar.gz: ab7acc1c279b73e6c84dcf57513368d28f5b85386928260bc23b806226f2fd85
5
5
  SHA512:
6
- metadata.gz: 12823e90e21cd1eeefcf883ab5c7ed9a4e9d008b2e26bd80ac6014a05e3b6e6b57e4054a3757574173886b2c8ed6a08e6c8d3a2863f6034eb592e6e8d03da5c1
7
- data.tar.gz: fb7a48fb211dfb1af4ddd9a0be588eb58318b8ffd445578a09dcacdef625ff9dc8e3dbc593b59a21da18acf678d67d9ca0bd76aef55a22843be9748bfddcf5fa
6
+ metadata.gz: '08dcd0f4f95c49a14ef83017a81352a4d5f3e9144ec9724b2af6d86882154d1abf7efd54eb2a9dfaa5a8509871c6686f428b90dc13b8834ef605315ff00fc6bd'
7
+ data.tar.gz: 6a854d05d30916be65b30cca8d6c03873bf8f5174df48721c1193c6c54d8aa5cd742767d285b9fd3d8a96c37dff72ac0ca99213d533c926f5302b8d0eaf3bf95
data/README.md CHANGED
@@ -29,8 +29,8 @@ require "flexops"
29
29
 
30
30
  # API key authentication (recommended for server-to-server)
31
31
  client = FlexOps::Client.new(
32
- api_key: "fxk_live_...",
33
- workspace_id: "ws_abc123"
32
+ api_key: "live_...",
33
+ workspace_id: "your-workspace-id"
34
34
  )
35
35
 
36
36
  # Get shipping rates from all carriers
@@ -90,7 +90,7 @@ separate contracts. The SDK never automatically confirms a preview.
90
90
  ### API key (recommended)
91
91
 
92
92
  ```ruby
93
- client = FlexOps::Client.new(api_key: "fxk_live_...", workspace_id: "ws_abc123")
93
+ client = FlexOps::Client.new(api_key: "live_...", workspace_id: "your-workspace-id")
94
94
  ```
95
95
 
96
96
  ### Email / password
@@ -99,15 +99,15 @@ client = FlexOps::Client.new(api_key: "fxk_live_...", workspace_id: "ws_abc123")
99
99
  client = FlexOps::Client.new(base_url: "https://gateway.flexops.io")
100
100
  session = client.auth.login(email: "user@example.com", password: "password")
101
101
  client.set_access_token(session["data"]["token"])
102
- client.workspace_id = "ws_abc123"
102
+ client.workspace_id = "your-workspace-id"
103
103
  ```
104
104
 
105
105
  ## Sandbox / test keys
106
106
 
107
- 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.
107
+ Use `test_...` (instead of `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.
108
108
 
109
109
  ```ruby
110
- client = FlexOps::Client.new(api_key: "fxk_test_...", workspace_id: "ws_abc123")
110
+ client = FlexOps::Client.new(api_key: "test_...", workspace_id: "your-workspace-id")
111
111
  ```
112
112
 
113
113
  ## Carrier-specific calls
@@ -209,6 +209,34 @@ client.email_templates.upsert(eventType: 3, subjectTemplate: "Delivered: {{track
209
209
  client.email_templates.preview(subject_template: "Hi {{recipient_name}}", html_body_template: "<p>{{status}}</p>")
210
210
  ```
211
211
 
212
+ ## Multi-package labels
213
+
214
+ One order shipped in several boxes with the same carrier and service; each box gets its own label and
215
+ a box that fails does not stop the others. Multi-package labels cannot carry HAZMAT: send
216
+ `FlexOps::HazmatClassification::NONE` or leave `hazmatClassification` out. `maximumPostageAmount` is required: the
217
+ most the whole shipment may cost in USD (positive, at most two decimals). The Gateway refuses to buy a box that would
218
+ take the total above it (`PriceCeilingExceeded`; once the amount is used up the remaining boxes report
219
+ `PriceCeilingReached`), and a request without a valid ceiling is 400 `ApprovalRequired`.
220
+
221
+ ```ruby
222
+ begin
223
+ shipment = client.shipping.create_multi_package(
224
+ { orderId: 12345, carrierCode: "USPS", serviceCode: "PRIORITY",
225
+ hazmatClassification: FlexOps::HazmatClassification::NONE,
226
+ maximumPostageAmount: 25.00, # required: the most the whole shipment may cost, in USD
227
+ packages: [{ weightInOunces: 16, lengthInInches: 10, widthInInches: 8, heightInInches: 4, packageReference: "Box 1 of 2" },
228
+ { weightInOunces: 8, lengthInInches: 6, widthInInches: 6, heightInInches: 6, packageReference: "Box 2 of 2" }] },
229
+ idempotency_key: "order-12345-boxes" # optional; reuse it when you retry
230
+ )
231
+ rescue FlexOps::Error => e
232
+ # No label was bought: 422 AllPackagesFailed, or 409 OutcomeUnknown when a label may exist
233
+ # (check the order before buying again). e.data holds the per-box results.
234
+ end
235
+ ```
236
+
237
+ `client.returns.get` reads a received return by its numeric return id; read an RMA with
238
+ `client.returns.get_rma("RMA-1001")` or page through them with `client.returns.list_rmas`.
239
+
212
240
  ## Wallet balance
213
241
 
214
242
  ```ruby
@@ -287,7 +315,7 @@ Every SDK method is a thin wrapper around the FlexOps REST API. If you want to v
287
315
  ```bash
288
316
  # Shop rates across all connected carriers
289
317
  curl -X POST https://gateway.flexops.io/api/shipping/rates \
290
- -H "X-API-Key: fxk_live_..." \
318
+ -H "X-API-Key: live_..." \
291
319
  -H "Content-Type: application/json" \
292
320
  -d '{
293
321
  "origin": {"addressLine1": "123 Main St", "city": "New York", "stateProvince": "NY", "postalCode": "10001", "countryCode": "US"},
@@ -298,7 +326,7 @@ curl -X POST https://gateway.flexops.io/api/shipping/rates \
298
326
  # Preview a live label; this does not purchase it.
299
327
  # hazmatClassification 0 = not hazardous; required when there is no orderId.
300
328
  curl -X POST https://gateway.flexops.io/api/shipping/labels \
301
- -H "X-API-Key: fxk_live_..." \
329
+ -H "X-API-Key: live_..." \
302
330
  -H "Content-Type: application/json" \
303
331
  -d '{
304
332
  "carrierCode": "USPS", "serviceCode": "GROUND_ADVANTAGE",
@@ -315,14 +343,14 @@ curl -X POST https://gateway.flexops.io/api/shipping/labels \
315
343
 
316
344
  # Track a shipment
317
345
  curl https://gateway.flexops.io/api/shipping/track/9400111899223456789012 \
318
- -H "X-API-Key: fxk_live_..."
346
+ -H "X-API-Key: live_..."
319
347
 
320
348
  # Cancel a label
321
349
  curl -X DELETE "https://gateway.flexops.io/api/shipping/labels/lbl_123?carrierCode=USPS" \
322
- -H "X-API-Key: fxk_live_..."
350
+ -H "X-API-Key: live_..."
323
351
  ```
324
352
 
325
- Use an `fxk_test_...` key instead of `fxk_live_...` to hit the sandbox environment; mock carriers respond, no real charges, no real labels.
353
+ Use a `test_...` key instead of `live_...` to hit the sandbox environment; mock carriers respond, no real charges, no real labels.
326
354
 
327
355
  ## Resources
328
356
 
@@ -330,12 +358,12 @@ Use an `fxk_test_...` key instead of `fxk_live_...` to hit the sandbox environme
330
358
  |----------|-------------|
331
359
  | `client.auth` | Login, token refresh, logout |
332
360
  | `client.workspaces` | List and read workspaces; list, invite and remove members, change roles |
333
- | `client.shipping` | Rate shopping, labels, tracking, address validation, batch, cancel |
361
+ | `client.shipping` | Rate shopping, labels, multi-package labels, tracking, address validation, batch, cancel |
334
362
  | `client.carriers` | USPS, UPS, FedEx and DHL calls through the legacy proxy; writes are deprecated and need a user token |
335
363
  | `client.webhooks` | Subscription create/update/delete, secret rotation, test events, delivery logs, signature verification |
336
364
  | `client.wallet` | Workspace balance |
337
365
  | `client.insurance` | Providers, quotes, policies, void, claims |
338
- | `client.returns` | List and read returns; create RMAs, return labels, receive |
366
+ | `client.returns` | List and read received returns; create, read and list RMAs; return labels; receive |
339
367
  | `client.analytics` | Shipments, orders, carrier performance (legacy proxy reads) |
340
368
  | `client.orders` | Order reads (legacy proxy) |
341
369
  | `client.inventory` | Inventory reads (legacy proxy) |
@@ -355,8 +383,8 @@ API keys, workspace settings and account management are managed in the FlexOps p
355
383
  ```ruby
356
384
  client = FlexOps::Client.new(
357
385
  base_url: "https://gateway.flexops.io", # API base URL
358
- api_key: "fxk_live_...", # API key auth
359
- workspace_id: "ws_abc123", # Default workspace
386
+ api_key: "live_...", # API key auth
387
+ workspace_id: "your-workspace-id", # Default workspace
360
388
  timeout: 30 # Request timeout (seconds)
361
389
  )
362
390
  ```
@@ -8,13 +8,16 @@
8
8
 
9
9
  module FlexOps
10
10
  class Error < StandardError
11
- attr_reader :status, :code, :errors
11
+ # data is the envelope's "data" on an error reply, when the Gateway sends one. A
12
+ # multi-package 422 AllPackagesFailed or 409 OutcomeUnknown carries the per-box results there.
13
+ attr_reader :status, :code, :errors, :data
12
14
 
13
- def initialize(message, status: 0, code: nil, errors: nil)
15
+ def initialize(message, status: 0, code: nil, errors: nil, data: nil)
14
16
  super(message)
15
17
  @status = status
16
18
  @code = code
17
19
  @errors = errors
20
+ @data = data
18
21
  end
19
22
  end
20
23
 
data/lib/flexops/http.rb CHANGED
@@ -117,7 +117,8 @@ module FlexOps
117
117
  error_body["message"] || "HTTP #{status}: #{response.message}",
118
118
  status: status,
119
119
  code: error_body["errorCode"] || error_body["code"] || (status == 403 ? "FORBIDDEN" : nil),
120
- errors: error_body["errors"]
120
+ errors: error_body["errors"],
121
+ data: error_body["data"]
121
122
  )
122
123
 
123
124
  raise error unless retryable?(method, status, error.code)
@@ -6,6 +6,8 @@
6
6
  # Copyright (c) 2021-2026 by FlexOps, LLC. All rights reserved.
7
7
  # ***********************************************************************
8
8
 
9
+ require "erb"
10
+
9
11
  module FlexOps
10
12
  module Resources
11
13
  class Returns
@@ -14,15 +16,30 @@ module FlexOps
14
16
  @ws_id = ws_id_proc
15
17
  end
16
18
 
19
+ # Lists received returns (start_date and end_date are ISO 8601; the Gateway defaults to the
20
+ # last 30 days). To list return authorizations, use list_rmas.
17
21
  def list(page: nil, page_size: nil, start_date: nil, end_date: nil)
18
22
  query = { page: page, pageSize: page_size, startDate: start_date, endDate: end_date }.compact
19
23
  @http.get("#{ws_path}/returns", query: query.empty? ? nil : query)
20
24
  end
21
25
 
26
+ # Reads a received return by its numeric return id ("returnId" on a listed return or an RMA).
27
+ # An RMA number such as "RMA-1001" is not a return id and matches no Gateway route; use get_rma.
22
28
  def get(return_id)
23
29
  @http.get("#{ws_path}/returns/#{return_id}")
24
30
  end
25
31
 
32
+ # Lists return authorizations (RMAs), one page at a time.
33
+ def list_rmas(page: nil, page_size: nil)
34
+ query = { page: page, pageSize: page_size }.compact
35
+ @http.get("#{ws_path}/returns/rma", query: query.empty? ? nil : query)
36
+ end
37
+
38
+ # Reads a return authorization by its RMA number, for example "RMA-1001".
39
+ def get_rma(rma_number)
40
+ @http.get("#{ws_path}/returns/rma/#{ERB::Util.url_encode(rma_number.to_s)}")
41
+ end
42
+
26
43
  # Creates an RMA and returns the enveloped RMA (201).
27
44
  #
28
45
  # request:: { orderId: (a number), orderNumber: (optional), reason:,
@@ -60,6 +60,37 @@ module FlexOps
60
60
  @http.post("/api/addresses/validate", body: to_validation_address(address))
61
61
  end
62
62
 
63
+ # Buys labels for one order shipped in 1 to 99 boxes with the same carrier and service
64
+ # (POST /api/shipping/multi-package, the Gateway MultiPackageShipmentRequest):
65
+ #
66
+ # { orderId: 12345, carrierCode: "USPS", serviceCode: (optional), labelFormat: (optional),
67
+ # shipDate: (optional), hazmatClassification: (optional), maximumPostageAmount: 25.00,
68
+ # packages: [{ weightInOunces:, lengthInInches:, widthInInches:, heightInInches:,
69
+ # deliveryConfirmationRequired: (optional), signatureConfirmationRequired: (optional),
70
+ # insuranceCoverageRequested: (optional), insuredValue: (optional),
71
+ # packageReference: (optional) }] }
72
+ #
73
+ # maximumPostageAmount is required: the most the whole shipment may cost, in USD (positive, at most
74
+ # two decimals). The Gateway refuses to buy a box that would take the total above it, so you never
75
+ # pay more: a box priced above what is left reports PriceCeilingExceeded, and once the amount is
76
+ # used up the remaining boxes report PriceCeilingReached and are not bought. Boxes already bought
77
+ # stay bought. A missing or invalid value is 400 ApprovalRequired.
78
+ #
79
+ # Weight is in ounces and every dimension in inches; all four are required. Multi-package
80
+ # labels cannot carry HAZMAT yet: send HazmatClassification::NONE or leave hazmatClassification
81
+ # out (any other class is refused with 400 HazmatNotSupported).
82
+ #
83
+ # Returns the envelope when at least one label was bought (check data["failedPackages"] and
84
+ # each box in data["labels"]). When none was, the Gateway answers 422 AllPackagesFailed, or 409
85
+ # OutcomeUnknown when a box's carrier outcome is unknown and a label may exist: both raise
86
+ # FlexOps::Error whose data holds the per-box results. After OutcomeUnknown, check the order
87
+ # before buying again. Pass your own idempotency_key (and reuse it) to make a retry you run
88
+ # yourself safe.
89
+ def create_multi_package(request, idempotency_key: nil)
90
+ @http.post("/api/shipping/multi-package", body: request,
91
+ headers: idempotency_key.nil? ? {} : { "Idempotency-Key" => idempotency_key })
92
+ end
93
+
63
94
  # The payload is sent as given. Set hazmatClassification on an item only when you have a declaration.
64
95
  def create_batch(request)
65
96
  @http.post("#{ws_path}/labels/batch", body: request)
@@ -7,5 +7,5 @@
7
7
  # ***********************************************************************
8
8
 
9
9
  module FlexOps
10
- VERSION = "2.0.0"
10
+ VERSION = "2.2.0"
11
11
  end
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.0.0
4
+ version: 2.2.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-20 00:00:00.000000000 Z
11
+ date: 2026-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json